From 0d11331d185a60adf84e64ec98642baf6a15a95a Mon Sep 17 00:00:00 2001 From: Jorropo Date: Sat, 3 Jan 2026 21:19:24 +0100 Subject: [PATCH 1/4] add a .clang-format file (#9154) --- .clang-format | 2 + .clusterfuzzlite/router_fuzzer.cpp | 265 +- src/AmbientLightingThread.h | 239 +- src/AudioThread.h | 127 +- src/BluetoothCommon.cpp | 18 +- src/BluetoothCommon.h | 15 +- src/BluetoothStatus.h | 159 +- src/DebugConfiguration.cpp | 234 +- src/DebugConfiguration.h | 65 +- src/DisplayFormatters.cpp | 155 +- src/DisplayFormatters.h | 10 +- src/FSCommon.cpp | 404 +- src/Fusion/FusionAhrs.c | 622 ++- src/Fusion/FusionAhrs.h | 73 +- src/Fusion/FusionAxes.h | 293 +- src/Fusion/FusionCalibration.h | 13 +- src/Fusion/FusionCompass.c | 44 +- src/Fusion/FusionCompass.h | 3 +- src/Fusion/FusionConvention.h | 6 +- src/Fusion/FusionMath.h | 360 +- src/Fusion/FusionOffset.c | 47 +- src/Fusion/FusionOffset.h | 8 +- src/GPSStatus.h | 192 +- src/GpioLogic.cpp | 122 +- src/GpioLogic.h | 162 +- src/Led.cpp | 38 +- src/MessageStore.cpp | 529 +- src/MessageStore.h | 119 +- src/NodeStatus.h | 88 +- src/Observer.h | 125 +- src/Power.cpp | 1899 ++++--- src/PowerFSM.cpp | 500 +- src/PowerFSM.h | 29 +- src/PowerFSMThread.h | 52 +- src/PowerMon.cpp | 47 +- src/PowerMon.h | 35 +- src/PowerStatus.h | 127 +- src/RedirectablePrint.cpp | 625 ++- src/RedirectablePrint.h | 65 +- src/SPILock.cpp | 7 +- src/SafeFile.cpp | 158 +- src/SafeFile.h | 52 +- src/SerialConsole.cpp | 140 +- src/SerialConsole.h | 54 +- src/Status.h | 60 +- src/airtime.cpp | 319 +- src/airtime.h | 73 +- src/buzz/BuzzerFeedbackThread.cpp | 102 +- src/buzz/BuzzerFeedbackThread.h | 13 +- src/buzz/buzz.cpp | 184 +- src/commands.h | 22 +- src/concurrency/BinarySemaphoreFreeRTOS.cpp | 28 +- src/concurrency/BinarySemaphoreFreeRTOS.h | 26 +- src/concurrency/BinarySemaphorePosix.cpp | 10 +- src/concurrency/BinarySemaphorePosix.h | 26 +- src/concurrency/InterruptableDelay.cpp | 26 +- src/concurrency/InterruptableDelay.h | 29 +- src/concurrency/Lock.cpp | 32 +- src/concurrency/Lock.h | 34 +- src/concurrency/LockGuard.cpp | 13 +- src/concurrency/LockGuard.h | 20 +- src/concurrency/NotifiedWorkerThread.cpp | 95 +- src/concurrency/NotifiedWorkerThread.h | 72 +- src/concurrency/OSThread.cpp | 138 +- src/concurrency/OSThread.h | 67 +- src/concurrency/Periodic.h | 18 +- src/configuration.h | 4 +- src/detect/LoRaRadioType.h | 22 +- src/detect/ScanI2C.cpp | 102 +- src/detect/ScanI2C.h | 257 +- src/detect/ScanI2CConsumer.cpp | 14 +- src/detect/ScanI2CConsumer.h | 9 +- src/detect/ScanI2CTwoWire.cpp | 1123 ++-- src/detect/ScanI2CTwoWire.h | 52 +- src/detect/einkScan.h | 102 +- src/freertosinc.h | 4 +- src/gps/GPS.cpp | 2983 ++++++----- src/gps/GPS.h | 317 +- src/gps/GPSUpdateScheduling.cpp | 132 +- src/gps/GPSUpdateScheduling.h | 37 +- src/gps/GeoCoord.cpp | 857 ++-- src/gps/GeoCoord.h | 193 +- src/gps/NMEAWPL.cpp | 101 +- src/gps/RTC.cpp | 588 ++- src/gps/RTC.h | 28 +- src/gps/ubx.h | 50 +- src/graphics/EInkDisplay2.cpp | 377 +- src/graphics/EInkDisplay2.h | 99 +- src/graphics/EInkDynamicDisplay.cpp | 726 ++- src/graphics/EInkDynamicDisplay.h | 211 +- src/graphics/GxEPD2Multi.h | 235 +- src/graphics/Panel_sdl.cpp | 1088 ++-- src/graphics/Panel_sdl.hpp | 187 +- src/graphics/PointStruct.h | 4 +- src/graphics/Screen.cpp | 2483 +++++---- src/graphics/Screen.h | 1048 ++-- src/graphics/ScreenFonts.h | 6 +- src/graphics/SharedUIDisplay.cpp | 844 ++- src/graphics/SharedUIDisplay.h | 6 +- src/graphics/TFTDisplay.cpp | 2269 ++++---- src/graphics/TFTDisplay.h | 79 +- src/graphics/TimeFormatters.cpp | 217 +- src/graphics/VirtualKeyboard.cpp | 1263 +++-- src/graphics/VirtualKeyboard.h | 103 +- src/graphics/draw/ClockRenderer.cpp | 750 ++- src/graphics/draw/ClockRenderer.h | 6 +- src/graphics/draw/CompassRenderer.cpp | 180 +- src/graphics/draw/CompassRenderer.h | 6 +- src/graphics/draw/DebugRenderer.cpp | 1079 ++-- src/graphics/draw/DebugRenderer.h | 6 +- src/graphics/draw/DrawRenderers.h | 6 +- src/graphics/draw/MenuHandler.cpp | 4543 ++++++++--------- src/graphics/draw/MenuHandler.h | 236 +- src/graphics/draw/MessageRenderer.cpp | 1675 +++--- src/graphics/draw/MessageRenderer.h | 9 +- src/graphics/draw/NodeListRenderer.cpp | 1127 ++-- src/graphics/draw/NodeListRenderer.h | 15 +- src/graphics/draw/NotificationRenderer.cpp | 1282 +++-- src/graphics/draw/NotificationRenderer.h | 64 +- src/graphics/draw/UIRenderer.cpp | 2176 ++++---- src/graphics/draw/UIRenderer.h | 74 +- src/graphics/emotes.cpp | 469 +- src/graphics/emotes.h | 11 +- src/graphics/fonts/EinkDisplayFonts.cpp | 1756 ++++--- src/graphics/fonts/OLEDDisplayFontsCS.cpp | 2143 ++++---- src/graphics/fonts/OLEDDisplayFontsRU.cpp | 1992 ++++---- src/graphics/fonts/OLEDDisplayFontsUA.cpp | 2199 ++++---- src/graphics/images.h | 100 +- .../Drivers/Backlight/LatchingBacklight.cpp | 109 +- .../Drivers/Backlight/LatchingBacklight.h | 42 +- .../niche/Drivers/EInk/DEPG0213BNS800.cpp | 140 +- .../niche/Drivers/EInk/DEPG0213BNS800.h | 32 +- .../niche/Drivers/EInk/DEPG0290BNS800.cpp | 128 +- .../niche/Drivers/EInk/DEPG0290BNS800.h | 32 +- src/graphics/niche/Drivers/EInk/E0213A367.cpp | 113 +- src/graphics/niche/Drivers/EInk/E0213A367.h | 30 +- src/graphics/niche/Drivers/EInk/EInk.cpp | 103 +- src/graphics/niche/Drivers/EInk/EInk.h | 62 +- .../niche/Drivers/EInk/GDEY0154D67.cpp | 66 +- src/graphics/niche/Drivers/EInk/GDEY0154D67.h | 30 +- .../niche/Drivers/EInk/GDEY0213B74.cpp | 66 +- src/graphics/niche/Drivers/EInk/GDEY0213B74.h | 30 +- .../niche/Drivers/EInk/HINK_E0213A289.cpp | 68 +- .../niche/Drivers/EInk/HINK_E0213A289.h | 30 +- .../niche/Drivers/EInk/HINK_E042A87.cpp | 65 +- .../niche/Drivers/EInk/HINK_E042A87.h | 28 +- .../niche/Drivers/EInk/LCMEN2R13ECC1.cpp | 82 +- .../niche/Drivers/EInk/LCMEN2R13ECC1.h | 30 +- .../niche/Drivers/EInk/LCMEN2R13EFC1.cpp | 332 +- .../niche/Drivers/EInk/LCMEN2R13EFC1.h | 74 +- src/graphics/niche/Drivers/EInk/SSD1682.cpp | 47 +- src/graphics/niche/Drivers/EInk/SSD1682.h | 14 +- src/graphics/niche/Drivers/EInk/SSD16XX.cpp | 371 +- src/graphics/niche/Drivers/EInk/SSD16XX.h | 72 +- .../Drivers/EInk/ZJY122250_0213BAAMFGN.cpp | 86 +- .../Drivers/EInk/ZJY122250_0213BAAMFGN.h | 30 +- .../Drivers/EInk/ZJY128296_029EAAMFGN.cpp | 68 +- .../niche/Drivers/EInk/ZJY128296_029EAAMFGN.h | 30 +- .../Drivers/EInk/ZJY200200_0154DAAMFGN.h | 3 +- src/graphics/niche/InkHUD/Applet.cpp | 1269 +++-- src/graphics/niche/InkHUD/Applet.h | 237 +- src/graphics/niche/InkHUD/AppletFont.cpp | 1278 +++-- src/graphics/niche/InkHUD/AppletFont.h | 52 +- .../InkHUD/Applets/Bases/Map/MapApplet.cpp | 890 ++-- .../InkHUD/Applets/Bases/Map/MapApplet.h | 58 +- .../Applets/Bases/NodeList/NodeListApplet.cpp | 435 +- .../Applets/Bases/NodeList/NodeListApplet.h | 62 +- .../BasicExample/BasicExampleApplet.cpp | 13 +- .../BasicExample/BasicExampleApplet.h | 14 +- .../NewMsgExample/NewMsgExampleApplet.cpp | 64 +- .../NewMsgExample/NewMsgExampleApplet.h | 46 +- .../System/AlignStick/AlignStickApplet.cpp | 263 +- .../System/AlignStick/AlignStickApplet.h | 46 +- .../System/BatteryIcon/BatteryIconApplet.cpp | 133 +- .../System/BatteryIcon/BatteryIconApplet.h | 24 +- .../InkHUD/Applets/System/Logo/LogoApplet.cpp | 248 +- .../InkHUD/Applets/System/Logo/LogoApplet.h | 34 +- .../InkHUD/Applets/System/Menu/MenuAction.h | 41 +- .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 1337 +++-- .../InkHUD/Applets/System/Menu/MenuApplet.h | 149 +- .../InkHUD/Applets/System/Menu/MenuItem.h | 30 +- .../InkHUD/Applets/System/Menu/MenuPage.h | 19 +- .../System/Notification/Notification.h | 33 +- .../Notification/NotificationApplet.cpp | 371 +- .../System/Notification/NotificationApplet.h | 52 +- .../Applets/System/Pairing/PairingApplet.cpp | 111 +- .../Applets/System/Pairing/PairingApplet.h | 28 +- .../System/Placeholder/PlaceholderApplet.cpp | 7 +- .../System/Placeholder/PlaceholderApplet.h | 16 +- .../InkHUD/Applets/System/Tips/TipsApplet.cpp | 400 +- .../InkHUD/Applets/System/Tips/TipsApplet.h | 46 +- .../User/AllMessage/AllMessageApplet.cpp | 199 +- .../User/AllMessage/AllMessageApplet.h | 31 +- .../niche/InkHUD/Applets/User/DM/DMApplet.cpp | 189 +- .../niche/InkHUD/Applets/User/DM/DMApplet.h | 31 +- .../InkHUD/Applets/User/Heard/HeardApplet.cpp | 169 +- .../InkHUD/Applets/User/Heard/HeardApplet.h | 22 +- .../User/Positions/PositionsApplet.cpp | 190 +- .../Applets/User/Positions/PositionsApplet.h | 28 +- .../User/RecentsList/RecentsListApplet.cpp | 192 +- .../User/RecentsList/RecentsListApplet.h | 46 +- .../ThreadedMessage/ThreadedMessageApplet.cpp | 365 +- .../ThreadedMessage/ThreadedMessageApplet.h | 34 +- src/graphics/niche/InkHUD/DisplayHealth.cpp | 206 +- src/graphics/niche/InkHUD/DisplayHealth.h | 39 +- src/graphics/niche/InkHUD/Events.cpp | 542 +- src/graphics/niche/InkHUD/Events.h | 78 +- src/graphics/niche/InkHUD/InkHUD.cpp | 307 +- src/graphics/niche/InkHUD/InkHUD.h | 122 +- src/graphics/niche/InkHUD/MessageStore.cpp | 222 +- src/graphics/niche/InkHUD/MessageStore.h | 36 +- src/graphics/niche/InkHUD/Persistence.cpp | 69 +- src/graphics/niche/InkHUD/Persistence.h | 192 +- src/graphics/niche/InkHUD/Renderer.cpp | 559 +- src/graphics/niche/InkHUD/Renderer.h | 96 +- src/graphics/niche/InkHUD/SystemApplet.h | 31 +- src/graphics/niche/InkHUD/Tile.cpp | 289 +- src/graphics/niche/InkHUD/Tile.h | 52 +- src/graphics/niche/InkHUD/WindowManager.cpp | 839 ++- src/graphics/niche/InkHUD/WindowManager.h | 78 +- src/graphics/niche/Inputs/TwoButton.cpp | 362 +- src/graphics/niche/Inputs/TwoButton.h | 125 +- .../niche/Inputs/TwoButtonExtended.cpp | 671 ++- src/graphics/niche/Inputs/TwoButtonExtended.h | 167 +- .../niche/Utils/CannedMessageStore.cpp | 188 +- src/graphics/niche/Utils/CannedMessageStore.h | 34 +- src/graphics/niche/Utils/FlashData.h | 265 +- src/graphics/tftSetup.cpp | 190 +- src/input/BBQ10Keyboard.cpp | 210 +- src/input/BBQ10Keyboard.h | 57 +- src/input/ButtonThread.cpp | 476 +- src/input/ButtonThread.h | 161 +- src/input/ExpressLRSFiveWay.cpp | 321 +- src/input/ExpressLRSFiveWay.h | 85 +- src/input/HackadayCommunicatorKeyboard.cpp | 173 +- src/input/HackadayCommunicatorKeyboard.h | 39 +- src/input/InputBroker.cpp | 87 +- src/input/InputBroker.h | 92 +- src/input/LinuxInput.cpp | 301 +- src/input/LinuxInput.h | 78 +- src/input/LinuxInputImpl.cpp | 5 +- src/input/LinuxInputImpl.h | 9 +- src/input/MPR121Keyboard.cpp | 589 +-- src/input/MPR121Keyboard.h | 67 +- src/input/RotaryEncoderImpl.cpp | 189 +- src/input/RotaryEncoderImpl.h | 48 +- src/input/RotaryEncoderInterruptBase.cpp | 222 +- src/input/RotaryEncoderInterruptBase.h | 77 +- src/input/RotaryEncoderInterruptImpl1.cpp | 55 +- src/input/RotaryEncoderInterruptImpl1.h | 15 +- src/input/SeesawRotary.cpp | 127 +- src/input/SeesawRotary.h | 21 +- src/input/SerialKeyboard.cpp | 287 +- src/input/SerialKeyboard.h | 35 +- src/input/SerialKeyboardImpl.cpp | 13 +- src/input/SerialKeyboardImpl.h | 9 +- src/input/TCA8418Keyboard.cpp | 158 +- src/input/TCA8418Keyboard.h | 29 +- src/input/TCA8418KeyboardBase.cpp | 548 +- src/input/TCA8418KeyboardBase.h | 277 +- src/input/TDeckProKeyboard.cpp | 206 +- src/input/TDeckProKeyboard.h | 41 +- src/input/TLoraPagerKeyboard.cpp | 233 +- src/input/TLoraPagerKeyboard.h | 47 +- src/input/TouchScreenBase.cpp | 229 +- src/input/TouchScreenBase.h | 71 +- src/input/TouchScreenImpl1.cpp | 103 +- src/input/TouchScreenImpl1.h | 17 +- src/input/TrackballInterruptBase.cpp | 354 +- src/input/TrackballInterruptBase.h | 99 +- src/input/TrackballInterruptImpl1.cpp | 89 +- src/input/TrackballInterruptImpl1.h | 19 +- src/input/UpDownInterruptBase.cpp | 260 +- src/input/UpDownInterruptBase.h | 100 +- src/input/UpDownInterruptImpl1.cpp | 55 +- src/input/UpDownInterruptImpl1.h | 15 +- src/input/cardKbI2cImpl.cpp | 105 +- src/input/cardKbI2cImpl.h | 9 +- src/input/i2cButton.cpp | 104 +- src/input/i2cButton.h | 11 +- src/input/kbI2cBase.cpp | 975 ++-- src/input/kbI2cBase.h | 27 +- src/input/kbMatrixBase.cpp | 182 +- src/input/kbMatrixBase.h | 23 +- src/input/kbMatrixImpl.cpp | 13 +- src/input/kbMatrixImpl.h | 9 +- src/main.cpp | 1725 ++++--- src/memGet.cpp | 71 +- src/memGet.h | 13 +- src/mesh/Channels.cpp | 588 +-- src/mesh/Channels.h | 189 +- src/mesh/CryptoEngine.cpp | 305 +- src/mesh/CryptoEngine.h | 109 +- src/mesh/Default.cpp | 62 +- src/mesh/Default.h | 68 +- src/mesh/FloodingRouter.cpp | 199 +- src/mesh/FloodingRouter.h | 81 +- src/mesh/LLCC68Interface.cpp | 7 +- src/mesh/LLCC68Interface.h | 13 +- src/mesh/LR1110Interface.cpp | 7 +- src/mesh/LR1110Interface.h | 8 +- src/mesh/LR1120Interface.cpp | 12 +- src/mesh/LR1120Interface.h | 10 +- src/mesh/LR1121Interface.cpp | 12 +- src/mesh/LR1121Interface.h | 10 +- src/mesh/LR11x0Interface.cpp | 331 +- src/mesh/LR11x0Interface.h | 92 +- src/mesh/MemoryPool.h | 231 +- src/mesh/MeshModule.cpp | 429 +- src/mesh/MeshModule.h | 284 +- src/mesh/MeshPacketQueue.cpp | 250 +- src/mesh/MeshPacketQueue.h | 55 +- src/mesh/MeshRadio.h | 20 +- src/mesh/MeshService.cpp | 624 ++- src/mesh/MeshService.h | 225 +- src/mesh/MeshTypes.h | 21 +- src/mesh/NextHopRouter.cpp | 478 +- src/mesh/NextHopRouter.h | 212 +- src/mesh/NodeDB.cpp | 3110 ++++++----- src/mesh/NodeDB.h | 454 +- src/mesh/PacketCache.cpp | 361 +- src/mesh/PacketCache.h | 105 +- src/mesh/PacketHistory.cpp | 613 ++- src/mesh/PacketHistory.h | 108 +- src/mesh/PhoneAPI.cpp | 1384 +++-- src/mesh/PhoneAPI.h | 251 +- src/mesh/PointerQueue.h | 29 +- src/mesh/ProtobufModule.h | 204 +- src/mesh/RF95Interface.cpp | 382 +- src/mesh/RF95Interface.h | 94 +- src/mesh/RadioInterface.cpp | 754 ++- src/mesh/RadioInterface.h | 348 +- src/mesh/RadioLibInterface.cpp | 776 ++- src/mesh/RadioLibInterface.h | 355 +- src/mesh/RadioLibRF95.cpp | 88 +- src/mesh/RadioLibRF95.h | 83 +- src/mesh/ReliableRouter.cpp | 288 +- src/mesh/ReliableRouter.h | 55 +- src/mesh/Router.cpp | 1245 +++-- src/mesh/Router.h | 237 +- src/mesh/STM32WLE5JCInterface.cpp | 35 +- src/mesh/STM32WLE5JCInterface.h | 10 +- src/mesh/SX1262Interface.cpp | 7 +- src/mesh/SX1262Interface.h | 8 +- src/mesh/SX1268Interface.cpp | 20 +- src/mesh/SX1268Interface.h | 10 +- src/mesh/SX126xInterface.cpp | 443 +- src/mesh/SX126xInterface.h | 104 +- src/mesh/SX1280Interface.cpp | 7 +- src/mesh/SX1280Interface.h | 8 +- src/mesh/SX128xInterface.cpp | 354 +- src/mesh/SX128xInterface.h | 94 +- src/mesh/SinglePortModule.h | 50 +- src/mesh/StaticPointerQueue.h | 103 +- src/mesh/StreamAPI.cpp | 309 +- src/mesh/StreamAPI.h | 115 +- src/mesh/Throttle.cpp | 36 +- src/mesh/Throttle.h | 9 +- src/mesh/TypeConversions.cpp | 177 +- src/mesh/TypeConversions.h | 15 +- src/mesh/TypedQueue.h | 148 +- src/mesh/aes-ccm.cpp | 298 +- src/mesh/aes-ccm.h | 8 +- src/mesh/api/PacketAPI.cpp | 184 +- src/mesh/api/PacketAPI.h | 39 +- src/mesh/api/ServerAPI.cpp | 92 +- src/mesh/api/ServerAPI.h | 60 +- src/mesh/api/WiFiServerAPI.cpp | 33 +- src/mesh/api/WiFiServerAPI.h | 14 +- src/mesh/api/ethServerAPI.cpp | 22 +- src/mesh/api/ethServerAPI.h | 14 +- src/mesh/compression/unishox2.cpp | 2117 ++++---- src/mesh/compression/unishox2.h | 180 +- src/mesh/eth/ethClient.cpp | 244 +- src/mesh/http/ContentHandler.cpp | 1614 +++--- src/mesh/http/ContentHandler.h | 17 +- src/mesh/http/ContentHelper.cpp | 17 +- src/mesh/http/WebServer.cpp | 257 +- src/mesh/http/WebServer.h | 21 +- src/mesh/mesh-pb-constants.cpp | 85 +- src/mesh/mesh-pb-constants.h | 27 +- src/mesh/raspihttp/PiWebServer.cpp | 711 ++- src/mesh/raspihttp/PiWebServer.h | 58 +- src/mesh/udp/UdpMulticastHandler.h | 113 +- src/mesh/wifi/WiFiAPClient.cpp | 750 ++- src/meshUtils.cpp | 125 +- src/meshUtils.h | 13 +- src/modules/AdminModule.cpp | 2429 +++++---- src/modules/AdminModule.h | 100 +- src/modules/AtakPluginModule.cpp | 318 +- src/modules/AtakPluginModule.h | 27 +- src/modules/CannedMessageModule.cpp | 4120 ++++++++------- src/modules/CannedMessageModule.h | 393 +- src/modules/DetectionSensorModule.cpp | 219 +- src/modules/DetectionSensorModule.h | 27 +- src/modules/DropzoneModule.cpp | 123 +- src/modules/DropzoneModule.h | 38 +- src/modules/ExternalNotificationModule.cpp | 876 ++-- src/modules/ExternalNotificationModule.h | 77 +- src/modules/GenericThreadModule.cpp | 23 +- src/modules/GenericThreadModule.h | 15 +- src/modules/KeyVerificationModule.cpp | 521 +- src/modules/KeyVerificationModule.h | 94 +- src/modules/Modules.cpp | 219 +- src/modules/NeighborInfoModule.cpp | 337 +- src/modules/NeighborInfoModule.h | 103 +- src/modules/NodeInfoModule.cpp | 312 +- src/modules/NodeInfoModule.h | 64 +- src/modules/OnScreenKeyboardModule.cpp | 436 +- src/modules/OnScreenKeyboardModule.h | 57 +- src/modules/PositionModule.cpp | 847 ++- src/modules/PositionModule.h | 106 +- src/modules/PowerStressModule.cpp | 214 +- src/modules/PowerStressModule.h | 45 +- src/modules/RangeTestModule.cpp | 513 +- src/modules/RangeTestModule.h | 65 +- src/modules/RemoteHardwareModule.cpp | 213 +- src/modules/RemoteHardwareModule.h | 57 +- src/modules/ReplyModule.cpp | 23 +- src/modules/ReplyModule.h | 23 +- src/modules/RoutingModule.cpp | 112 +- src/modules/RoutingModule.h | 46 +- src/modules/SerialModule.cpp | 980 ++-- src/modules/SerialModule.h | 85 +- src/modules/StatusLEDModule.cpp | 167 +- src/modules/StatusLEDModule.h | 45 +- src/modules/StoreForwardModule.cpp | 871 ++-- src/modules/StoreForwardModule.h | 164 +- src/modules/SystemCommandsModule.cpp | 182 +- src/modules/SystemCommandsModule.h | 13 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 360 +- src/modules/Telemetry/AirQualityTelemetry.h | 89 +- src/modules/Telemetry/DeviceTelemetry.cpp | 309 +- src/modules/Telemetry/DeviceTelemetry.h | 94 +- .../Telemetry/EnvironmentTelemetry.cpp | 879 ++-- src/modules/Telemetry/EnvironmentTelemetry.h | 84 +- src/modules/Telemetry/HealthTelemetry.cpp | 374 +- src/modules/Telemetry/HealthTelemetry.h | 73 +- src/modules/Telemetry/HostMetrics.cpp | 160 +- src/modules/Telemetry/HostMetrics.h | 59 +- src/modules/Telemetry/PowerTelemetry.cpp | 415 +- src/modules/Telemetry/PowerTelemetry.h | 73 +- src/modules/Telemetry/Sensor/AHT10.cpp | 42 +- src/modules/Telemetry/Sensor/AHT10.h | 15 +- src/modules/Telemetry/Sensor/BH1750Sensor.cpp | 56 +- src/modules/Telemetry/Sensor/BH1750Sensor.h | 15 +- src/modules/Telemetry/Sensor/BME280Sensor.cpp | 50 +- src/modules/Telemetry/Sensor/BME280Sensor.h | 15 +- src/modules/Telemetry/Sensor/BME680Sensor.cpp | 221 +- src/modules/Telemetry/Sensor/BME680Sensor.h | 51 +- src/modules/Telemetry/Sensor/BMP085Sensor.cpp | 28 +- src/modules/Telemetry/Sensor/BMP085Sensor.h | 15 +- src/modules/Telemetry/Sensor/BMP280Sensor.cpp | 46 +- src/modules/Telemetry/Sensor/BMP280Sensor.h | 15 +- src/modules/Telemetry/Sensor/BMP3XXSensor.cpp | 113 +- src/modules/Telemetry/Sensor/BMP3XXSensor.h | 52 +- .../Telemetry/Sensor/CGRadSensSensor.cpp | 76 +- .../Telemetry/Sensor/CGRadSensSensor.h | 23 +- src/modules/Telemetry/Sensor/CurrentSensor.h | 7 +- .../Telemetry/Sensor/DFRobotGravitySensor.cpp | 51 +- .../Telemetry/Sensor/DFRobotGravitySensor.h | 17 +- .../Telemetry/Sensor/DFRobotLarkSensor.cpp | 62 +- .../Telemetry/Sensor/DFRobotLarkSensor.h | 15 +- src/modules/Telemetry/Sensor/DPS310Sensor.cpp | 48 +- src/modules/Telemetry/Sensor/DPS310Sensor.h | 15 +- src/modules/Telemetry/Sensor/INA219Sensor.cpp | 48 +- src/modules/Telemetry/Sensor/INA219Sensor.h | 23 +- src/modules/Telemetry/Sensor/INA226Sensor.cpp | 89 +- src/modules/Telemetry/Sensor/INA226Sensor.h | 33 +- src/modules/Telemetry/Sensor/INA260Sensor.cpp | 39 +- src/modules/Telemetry/Sensor/INA260Sensor.h | 21 +- .../Telemetry/Sensor/INA3221Sensor.cpp | 128 +- src/modules/Telemetry/Sensor/INA3221Sensor.h | 51 +- .../Telemetry/Sensor/IndicatorSensor.cpp | 252 +- .../Telemetry/Sensor/IndicatorSensor.h | 15 +- .../Telemetry/Sensor/LPS22HBSensor.cpp | 38 +- src/modules/Telemetry/Sensor/LPS22HBSensor.h | 15 +- .../Telemetry/Sensor/LTR390UVSensor.cpp | 83 +- src/modules/Telemetry/Sensor/LTR390UVSensor.h | 19 +- .../Telemetry/Sensor/MAX17048Sensor.cpp | 240 +- src/modules/Telemetry/Sensor/MAX17048Sensor.h | 100 +- .../Telemetry/Sensor/MAX30102Sensor.cpp | 116 +- src/modules/Telemetry/Sensor/MAX30102Sensor.h | 21 +- .../Telemetry/Sensor/MCP9808Sensor.cpp | 30 +- src/modules/Telemetry/Sensor/MCP9808Sensor.h | 15 +- .../Telemetry/Sensor/MLX90614Sensor.cpp | 50 +- src/modules/Telemetry/Sensor/MLX90614Sensor.h | 19 +- .../Telemetry/Sensor/MLX90632Sensor.cpp | 36 +- src/modules/Telemetry/Sensor/MLX90632Sensor.h | 15 +- .../Telemetry/Sensor/NAU7802Sensor.cpp | 203 +- src/modules/Telemetry/Sensor/NAU7802Sensor.h | 31 +- .../Telemetry/Sensor/OPT3001Sensor.cpp | 58 +- src/modules/Telemetry/Sensor/OPT3001Sensor.h | 17 +- .../Telemetry/Sensor/PCT2075Sensor.cpp | 20 +- src/modules/Telemetry/Sensor/PCT2075Sensor.h | 15 +- .../Telemetry/Sensor/RAK12035Sensor.cpp | 173 +- src/modules/Telemetry/Sensor/RAK12035Sensor.h | 19 +- .../Telemetry/Sensor/RAK9154Sensor.cpp | 274 +- src/modules/Telemetry/Sensor/RAK9154Sensor.h | 29 +- .../Telemetry/Sensor/RCWL9620Sensor.cpp | 94 +- src/modules/Telemetry/Sensor/RCWL9620Sensor.h | 29 +- src/modules/Telemetry/Sensor/SHT31Sensor.cpp | 26 +- src/modules/Telemetry/Sensor/SHT31Sensor.h | 15 +- src/modules/Telemetry/Sensor/SHT4XSensor.cpp | 54 +- src/modules/Telemetry/Sensor/SHT4XSensor.h | 15 +- src/modules/Telemetry/Sensor/SHTC3Sensor.cpp | 28 +- src/modules/Telemetry/Sensor/SHTC3Sensor.h | 15 +- src/modules/Telemetry/Sensor/T1000xSensor.cpp | 130 +- src/modules/Telemetry/Sensor/T1000xSensor.h | 15 +- .../Telemetry/Sensor/TSL2561Sensor.cpp | 36 +- src/modules/Telemetry/Sensor/TSL2561Sensor.h | 17 +- .../Telemetry/Sensor/TSL2591Sensor.cpp | 40 +- src/modules/Telemetry/Sensor/TSL2591Sensor.h | 15 +- .../Telemetry/Sensor/TelemetrySensor.h | 84 +- .../Telemetry/Sensor/VEML7700Sensor.cpp | 61 +- src/modules/Telemetry/Sensor/VEML7700Sensor.h | 25 +- src/modules/Telemetry/Sensor/VoltageSensor.h | 7 +- src/modules/Telemetry/Sensor/nullSensor.cpp | 20 +- src/modules/Telemetry/Sensor/nullSensor.h | 21 +- src/modules/Telemetry/UnitConversions.cpp | 20 +- src/modules/Telemetry/UnitConversions.h | 13 +- src/modules/TextMessageModule.cpp | 52 +- src/modules/TextMessageModule.h | 29 +- src/modules/TraceRouteModule.cpp | 1618 +++--- src/modules/TraceRouteModule.h | 97 +- src/modules/WaypointModule.cpp | 236 +- src/modules/WaypointModule.h | 33 +- src/modules/esp32/AudioModule.cpp | 450 +- src/modules/esp32/AudioModule.h | 79 +- src/modules/esp32/PaxcounterModule.cpp | 163 +- src/modules/esp32/PaxcounterModule.h | 31 +- src/motion/AccelerometerThread.h | 224 +- src/motion/BMA423Sensor.cpp | 76 +- src/motion/BMA423Sensor.h | 17 +- src/motion/BMM150Sensor.cpp | 98 +- src/motion/BMM150Sensor.h | 52 +- src/motion/BMX160Sensor.cpp | 195 +- src/motion/BMX160Sensor.h | 28 +- src/motion/ICM20948Sensor.cpp | 474 +- src/motion/ICM20948Sensor.h | 69 +- src/motion/LIS3DHSensor.cpp | 46 +- src/motion/LIS3DHSensor.h | 15 +- src/motion/LSM6DS3Sensor.cpp | 36 +- src/motion/LSM6DS3Sensor.h | 15 +- src/motion/MPU6050Sensor.cpp | 38 +- src/motion/MPU6050Sensor.h | 15 +- src/motion/MotionSensor.cpp | 97 +- src/motion/MotionSensor.h | 57 +- src/motion/QMA6100PSensor.cpp | 227 +- src/motion/QMA6100PSensor.h | 54 +- src/motion/STK8XXXSensor.cpp | 40 +- src/motion/STK8XXXSensor.h | 22 +- src/mqtt/MQTT.cpp | 1399 +++-- src/mqtt/MQTT.h | 139 +- src/mqtt/ServiceEnvelope.cpp | 20 +- src/mqtt/ServiceEnvelope.h | 12 +- src/network-stubs.cpp | 20 +- src/nimble/NimbleBluetooth.cpp | 1521 +++--- src/nimble/NimbleBluetooth.h | 31 +- src/platform/esp32/BleOta.cpp | 61 +- src/platform/esp32/BleOta.h | 17 +- src/platform/esp32/ESP32CryptoEngine.cpp | 54 +- src/platform/esp32/SimpleAllocator.cpp | 32 +- src/platform/esp32/SimpleAllocator.h | 30 +- src/platform/esp32/WiFiOTA.cpp | 123 +- src/platform/esp32/WiFiOTA.h | 3 +- src/platform/esp32/architecture.h | 4 +- src/platform/esp32/iram-quirk.c | 5 +- src/platform/esp32/main-esp32.cpp | 291 +- .../heltec_wireless_tracker/variant.cpp | 35 +- .../extra_variants/t_deck_pro/variant.cpp | 24 +- .../extra_variants/t_lora_pager/variant.cpp | 27 +- .../tbeam_displayshield/variant.cpp | 48 +- src/platform/nrf52/AsyncUDP.cpp | 80 +- src/platform/nrf52/AsyncUDP.h | 61 +- src/platform/nrf52/BLEDfuScure.cpp | 164 +- src/platform/nrf52/BLEDfuSecure.h | 13 +- src/platform/nrf52/NRF52Bluetooth.cpp | 641 ++- src/platform/nrf52/NRF52Bluetooth.h | 33 +- src/platform/nrf52/NRF52CryptoEngine.cpp | 42 +- src/platform/nrf52/aes-256/tiny-aes.cpp | 341 +- src/platform/nrf52/aes-256/tiny-aes.h | 4 +- src/platform/nrf52/alloc.cpp | 28 +- src/platform/nrf52/architecture.h | 4 +- src/platform/nrf52/hardfault.cpp | 147 +- src/platform/nrf52/main-nrf52.cpp | 602 ++- src/platform/nrf52/softdevice/ble.h | 241 +- src/platform/nrf52/softdevice/ble_err.h | 4 +- src/platform/nrf52/softdevice/ble_gap.h | 1782 ++++--- src/platform/nrf52/softdevice/ble_gatt.h | 84 +- src/platform/nrf52/softdevice/ble_gattc.h | 390 +- src/platform/nrf52/softdevice/ble_gatts.h | 536 +- src/platform/nrf52/softdevice/ble_hci.h | 4 +- src/platform/nrf52/softdevice/ble_l2cap.h | 189 +- src/platform/nrf52/softdevice/ble_types.h | 142 +- src/platform/nrf52/softdevice/nrf52/nrf_mbr.h | 61 +- src/platform/nrf52/softdevice/nrf_error_sdm.h | 9 +- src/platform/nrf52/softdevice/nrf_nvic.h | 284 +- src/platform/nrf52/softdevice/nrf_sdm.h | 116 +- src/platform/nrf52/softdevice/nrf_soc.h | 377 +- src/platform/nrf52/softdevice/nrf_svc.h | 27 +- src/platform/portduino/PortduinoGlue.cpp | 1521 +++--- src/platform/portduino/PortduinoGlue.h | 884 ++-- src/platform/portduino/SimRadio.cpp | 530 +- src/platform/portduino/SimRadio.h | 117 +- src/platform/portduino/USBHal.h | 228 +- .../hardware_rosc/include/hardware/rosc.h | 31 +- src/platform/rp2xx0/hardware_rosc/rosc.c | 77 +- src/platform/rp2xx0/main-rp2xx0.cpp | 211 +- .../rp2xx0/pico_sleep/include/pico/sleep.h | 20 +- src/platform/rp2xx0/pico_sleep/sleep.c | 165 +- src/platform/stm32wl/LittleFS.cpp | 159 +- src/platform/stm32wl/LittleFS.h | 11 +- src/platform/stm32wl/STM32_LittleFS.cpp | 344 +- src/platform/stm32wl/STM32_LittleFS.h | 95 +- src/platform/stm32wl/STM32_LittleFS_File.cpp | 592 +-- src/platform/stm32wl/STM32_LittleFS_File.h | 99 +- src/platform/stm32wl/architecture.h | 2 +- src/platform/stm32wl/littlefs/lfs.c | 4302 ++++++++-------- src/platform/stm32wl/littlefs/lfs.h | 304 +- src/platform/stm32wl/littlefs/lfs_util.c | 21 +- src/platform/stm32wl/littlefs/lfs_util.h | 110 +- src/platform/stm32wl/main-stm32wl.cpp | 48 +- src/power.h | 59 +- src/serialization/JSON.cpp | 250 +- src/serialization/JSON.h | 44 +- src/serialization/JSONValue.cpp | 951 ++-- src/serialization/JSONValue.h | 93 +- src/serialization/MeshPacketSerializer.cpp | 833 ++- src/serialization/MeshPacketSerializer.h | 28 +- .../MeshPacketSerializer_nRF52.cpp | 751 ++- src/serialization/cobs.cpp | 226 +- src/serialization/cobs.h | 24 +- src/sleep.cpp | 598 ++- src/xmodem.cpp | 361 +- src/xmodem.h | 45 +- test/TestUtil.cpp | 17 +- test/test_crypto/test_main.cpp | 299 +- .../ports/test_encrypted.cpp | 71 +- .../ports/test_nodeinfo.cpp | 66 +- .../ports/test_position.cpp | 76 +- .../ports/test_telemetry.cpp | 776 ++- .../ports/test_text_message.cpp | 133 +- .../ports/test_waypoint.cpp | 70 +- .../test_meshpacket_serializer/test_helpers.h | 61 +- .../test_serializer.cpp | 56 +- test/test_mqtt/MQTT.cpp | 1186 ++--- test/test_serial/SerialModule.cpp | 162 +- variants/esp32/chatter2/variant.h | 43 +- variants/esp32/diy/dr-dev/variant.h | 6 +- variants/esp32/diy/hydra/variant.h | 5 +- .../esp32/heltec_wireless_bridge/variant.h | 3 +- variants/esp32/nano-g1-explorer/variant.h | 4 +- variants/esp32/nano-g1/variant.h | 4 +- .../esp32/radiomaster_900_bandit/variant.h | 8 +- variants/esp32/rak11200/variant.h | 3 +- variants/esp32/station-g1/variant.h | 6 +- variants/esp32/tbeam/variant.h | 8 +- variants/esp32/tlora_v2/variant.h | 6 +- variants/esp32/wiphone/variant.h | 10 +- variants/esp32c6/m5stack_unitc6l/variant.cpp | 81 +- variants/esp32s3/CDEBYTE_EoRa-S3/variant.h | 25 +- variants/esp32s3/EBYTE_ESP32-S3/variant.h | 172 +- variants/esp32s3/dreamcatcher/rfswitch.h | 3 +- .../heltec_vision_master_e213/einkDetect.h | 41 +- .../heltec_vision_master_e213/nicheGraphics.h | 129 +- .../heltec_vision_master_e290/nicheGraphics.h | 109 +- .../heltec_wireless_paper/einkDetect.h | 41 +- .../heltec_wireless_paper/nicheGraphics.h | 117 +- .../esp32s3/heltec_wireless_tracker/variant.h | 9 +- variants/esp32s3/picomputer-s3/variant.h | 12 +- .../seeed-sensecap-indicator/variant.h | 8 +- variants/esp32s3/t-deck-pro/variant.h | 4 +- variants/esp32s3/t-deck/variant.h | 4 +- variants/esp32s3/t-eth-elite/rfswitch.h | 6 +- variants/esp32s3/t-eth-elite/variant.h | 4 +- variants/esp32s3/tbeam-s3-core/rfswitch.h | 6 +- variants/esp32s3/tbeam-s3-core/variant.h | 8 +- variants/esp32s3/tlora-pager/rfswitch.h | 6 +- .../esp32s3/tlora_t3s3_epaper/nicheGraphics.h | 93 +- variants/esp32s3/tlora_t3s3_v1/rfswitch.h | 6 +- variants/esp32s3/tlora_t3s3_v1/variant.h | 4 +- .../esp32s3/tracksenger/internal/variant.h | 12 +- variants/esp32s3/tracksenger/lcd/variant.h | 12 +- variants/esp32s3/tracksenger/oled/variant.h | 12 +- variants/esp32s3/unphone/variant.cpp | 27 +- variants/esp32s3/unphone/variant.h | 8 +- .../Dongle_nRF52840-pca10059-v1/variant.cpp | 9 +- .../ELECROW-ThinkNode-M1/nicheGraphics.h | 137 +- .../nrf52840/ELECROW-ThinkNode-M1/variant.cpp | 17 +- .../nrf52840/ELECROW-ThinkNode-M1/variant.h | 4 +- .../nrf52840/ELECROW-ThinkNode-M3/rfswitch.h | 6 +- .../nrf52840/ELECROW-ThinkNode-M3/variant.cpp | 121 +- .../nrf52840/ELECROW-ThinkNode-M6/variant.cpp | 55 +- variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h | 6 +- variants/nrf52840/ME25LS01-4Y10TD/variant.cpp | 11 +- .../nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h | 6 +- .../ME25LS01-4Y10TD_e-ink/variant.cpp | 11 +- .../MakePython_nRF52840_eink/variant.cpp | 13 +- .../MakePython_nRF52840_oled/variant.cpp | 13 +- variants/nrf52840/TWC_mesh_v4/variant.cpp | 13 +- variants/nrf52840/canaryone/variant.cpp | 35 +- variants/nrf52840/canaryone/variant.h | 3 +- variants/nrf52840/cpp_overrides/lfs_util.h | 130 +- .../nrf52_promicro_diy_tcxo/nicheGraphics.h | 91 +- .../diy/nrf52_promicro_diy_tcxo/rfswitch.h | 3 +- .../diy/nrf52_promicro_diy_tcxo/variant.cpp | 9 +- .../diy/nrf52_promicro_diy_tcxo/variant.h | 8 +- .../variant.cpp | 9 +- .../gat562_mesh_trial_tracker/variant.cpp | 19 +- .../gat562_mesh_trial_tracker/variant.h | 12 +- .../nicheGraphics.h | 91 +- .../heltec_mesh_node_t114-inkhud/variant.cpp | 9 +- .../heltec_mesh_node_t114-inkhud/variant.h | 7 +- .../heltec_mesh_node_t114/variant.cpp | 9 +- .../nrf52840/heltec_mesh_node_t114/variant.h | 7 +- .../heltec_mesh_pocket/nicheGraphics.h | 95 +- .../nrf52840/heltec_mesh_pocket/variant.h | 4 +- .../heltec_mesh_solar/nicheGraphics.h | 95 +- .../nrf52840/heltec_mesh_solar/variant.cpp | 9 +- variants/nrf52840/heltec_mesh_solar/variant.h | 4 +- variants/nrf52840/meshlink/variant.cpp | 13 +- variants/nrf52840/meshtiny/variant.cpp | 33 +- variants/nrf52840/monteops_hw1/variant.cpp | 13 +- variants/nrf52840/monteops_hw1/variant.h | 4 +- variants/nrf52840/muzi_base/rfswitch.h | 6 +- variants/nrf52840/muzi_base/variant.cpp | 33 +- variants/nrf52840/muzi_base/variant.h | 57 +- variants/nrf52840/nano-g2-ultra/variant.cpp | 5 +- variants/nrf52840/nano-g2-ultra/variant.h | 3 +- variants/nrf52840/r1-neo/variant.cpp | 19 +- variants/nrf52840/rak2560/variant.cpp | 19 +- variants/nrf52840/rak2560/variant.h | 12 +- variants/nrf52840/rak3401_1watt/variant.cpp | 19 +- variants/nrf52840/rak3401_1watt/variant.h | 8 +- variants/nrf52840/rak4631/variant.cpp | 19 +- variants/nrf52840/rak4631/variant.h | 12 +- variants/nrf52840/rak4631_epaper/variant.cpp | 19 +- variants/nrf52840/rak4631_epaper/variant.h | 7 +- .../rak4631_epaper_onrxtx/variant.cpp | 19 +- .../nrf52840/rak4631_epaper_onrxtx/variant.h | 9 +- variants/nrf52840/rak4631_eth_gw/variant.cpp | 19 +- variants/nrf52840/rak4631_eth_gw/variant.h | 12 +- .../rak4631_nomadstar_meteor_pro/variant.cpp | 19 +- .../rak4631_nomadstar_meteor_pro/variant.h | 12 +- variants/nrf52840/rak_wismeshtag/variant.cpp | 19 +- variants/nrf52840/rak_wismeshtag/variant.h | 4 +- variants/nrf52840/rak_wismeshtap/variant.cpp | 19 +- variants/nrf52840/rak_wismeshtap/variant.h | 12 +- .../nrf52840/seeed_solar_node/variant.cpp | 35 +- variants/nrf52840/seeed_solar_node/variant.h | 9 +- .../nrf52840/seeed_wio_tracker_L1/variant.cpp | 25 +- .../nrf52840/seeed_wio_tracker_L1/variant.h | 3 +- .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 131 +- .../seeed_wio_tracker_L1_eink/variant.cpp | 23 +- .../seeed_wio_tracker_L1_eink/variant.h | 3 +- .../seeed_xiao_nrf52840_kit/variant.cpp | 23 +- variants/nrf52840/t-echo-lite/variant.cpp | 17 +- variants/nrf52840/t-echo/nicheGraphics.h | 141 +- variants/nrf52840/t-echo/variant.cpp | 17 +- variants/nrf52840/t-echo/variant.h | 12 +- variants/nrf52840/tracker-t1000-e/rfswitch.h | 4 +- variants/nrf52840/tracker-t1000-e/variant.cpp | 43 +- variants/nrf52840/wio-sdk-wm1110/rfswitch.h | 6 +- variants/nrf52840/wio-sdk-wm1110/variant.cpp | 19 +- variants/nrf52840/wio-t1000-s/rfswitch.h | 4 +- variants/nrf52840/wio-t1000-s/variant.cpp | 43 +- .../nrf52840/wio-tracker-wm1110/rfswitch.h | 6 +- .../nrf52840/wio-tracker-wm1110/variant.cpp | 19 +- variants/rp2040/ec_catsniffer/variant.cpp | 17 +- variants/stm32/CDEBYTE_E77-MBL/rfswitch.h | 4 +- 771 files changed, 77752 insertions(+), 83184 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..5b4c5bf86 --- /dev/null +++ b/.clang-format @@ -0,0 +1,2 @@ +BasedOnStyle: LLVM +ColumnLimit: 150 diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index 71e88dbff..e87b8c094 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -16,8 +16,7 @@ #include "mesh/TypeConversions.h" #include "mesh/mesh-pb-constants.h" -namespace -{ +namespace { constexpr uint32_t nodeId = 0x12345678; // Set to true when lateInitVariant finishes. Used to ensure lateInitVariant was called during startup. bool hasBeenConfigured = false; @@ -35,135 +34,130 @@ std::condition_variable loopCV; std::thread meshtasticThread; // This exception is thrown when the portuino main thread should exit. -class ShouldExitException : public std::runtime_error -{ - public: - using std::runtime_error::runtime_error; +class ShouldExitException : public std::runtime_error { +public: + using std::runtime_error::runtime_error; }; // Start the loop for one test case and wait till the loop has completed. This ensures fuzz // test cases do not overlap with one another. This helps the fuzzer attribute a crash to the // single, currently running, test case. -void runLoopOnce() -{ - realHardware = true; // Avoids delay(100) within portduino/main.cpp - std::unique_lock lck(loopLock); - fuzzerRunning = true; - loopCanRun = true; - loopCV.notify_one(); - loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; }); +void runLoopOnce() { + realHardware = true; // Avoids delay(100) within portduino/main.cpp + std::unique_lock lck(loopLock); + fuzzerRunning = true; + loopCanRun = true; + loopCV.notify_one(); + loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; }); } } // namespace // Called in the main Arduino loop function to determine if the loop can delay/sleep before running again. // We use this as a way to block the loop from sleeping and to start the loop function immediately when a // fuzzer input is ready. -bool loopCanSleep() -{ - std::unique_lock lck(loopLock); - loopIsWaiting = true; - loopCV.notify_one(); - loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; }); - loopIsWaiting = false; - if (loopShouldExit) - throw ShouldExitException("exit"); - if (!fuzzerRunning) - return true; // The loop can sleep before the fuzzer starts. - loopCanRun = false; // Only run the loop once before waiting again. - return false; +bool loopCanSleep() { + std::unique_lock lck(loopLock); + loopIsWaiting = true; + loopCV.notify_one(); + loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; }); + loopIsWaiting = false; + if (loopShouldExit) + throw ShouldExitException("exit"); + if (!fuzzerRunning) + return true; // The loop can sleep before the fuzzer starts. + loopCanRun = false; // Only run the loop once before waiting again. + return false; } // Called just prior to starting Meshtastic. Allows for setting config values before startup. -void lateInitVariant() -{ - portduino_config.logoutputlevel = level_error; - channelFile.channels[0] = meshtastic_Channel{ - .has_settings = true, - .settings = - meshtastic_ChannelSettings{ - .psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}}, - .name = "LongFast", - .uplink_enabled = true, - .has_module_settings = true, - .module_settings = {.position_precision = 16}, - }, - .role = meshtastic_Channel_Role_PRIMARY, - }; - config.security.admin_key[0] = { - .size = 32, - .bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, - 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c}, - }; - config.security.admin_key_count = 1; - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; - moduleConfig.has_mqtt = true; - moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{ - .enabled = true, - .proxy_to_client_enabled = true, - }; - moduleConfig.has_store_forward = true; - moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{ - .enabled = true, - .history_return_max = 4, - .history_return_window = 600, - .is_server = true, - }; - meshtastic_Position fixedGPS = meshtastic_Position{ - .has_latitude_i = true, - .latitude_i = static_cast(1 * 1e7), - .has_longitude_i = true, - .longitude_i = static_cast(3 * 1e7), - .has_altitude = true, - .altitude = 64, - .location_source = meshtastic_Position_LocSource_LOC_MANUAL, - }; - nodeDB->setLocalPosition(fixedGPS); - config.has_position = true; - config.position.fixed_position = true; - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum()); - info->has_position = true; - info->position = TypeConversions::ConvertToPositionLite(fixedGPS); - hasBeenConfigured = true; +void lateInitVariant() { + portduino_config.logoutputlevel = level_error; + channelFile.channels[0] = meshtastic_Channel{ + .has_settings = true, + .settings = + meshtastic_ChannelSettings{ + .psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}}, + .name = "LongFast", + .uplink_enabled = true, + .has_module_settings = true, + .module_settings = {.position_precision = 16}, + }, + .role = meshtastic_Channel_Role_PRIMARY, + }; + config.security.admin_key[0] = { + .size = 32, + .bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, + 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c}, + }; + config.security.admin_key_count = 1; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + moduleConfig.has_mqtt = true; + moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{ + .enabled = true, + .proxy_to_client_enabled = true, + }; + moduleConfig.has_store_forward = true; + moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{ + .enabled = true, + .history_return_max = 4, + .history_return_window = 600, + .is_server = true, + }; + meshtastic_Position fixedGPS = meshtastic_Position{ + .has_latitude_i = true, + .latitude_i = static_cast(1 * 1e7), + .has_longitude_i = true, + .longitude_i = static_cast(3 * 1e7), + .has_altitude = true, + .altitude = 64, + .location_source = meshtastic_Position_LocSource_LOC_MANUAL, + }; + nodeDB->setLocalPosition(fixedGPS); + config.has_position = true; + config.position.fixed_position = true; + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum()); + info->has_position = true; + info->position = TypeConversions::ConvertToPositionLite(fixedGPS); + hasBeenConfigured = true; } extern "C" { int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary. // Start Meshtastic in a thread and wait till it has reached the ON state. -int LLVMFuzzerInitialize(int *argc, char ***argv) -{ - portduino_config.maxtophone = 5; +int LLVMFuzzerInitialize(int *argc, char ***argv) { + portduino_config.maxtophone = 5; - meshtasticThread = std::thread([program = *argv[0]]() { - char nodeIdStr[12]; - strcpy(nodeIdStr, std::to_string(nodeId).c_str()); - int argc = 7; - char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr}; - try { - portduino_main(argc, argv); - } catch (const ShouldExitException &) { - } - }); - std::atexit([] { - { - const std::lock_guard lck(loopLock); - loopShouldExit = true; - loopCV.notify_one(); - } - meshtasticThread.join(); - }); - - // Wait for startup. - for (int i = 1; i < 20; ++i) { - if (powerFSM.getState() == &stateON) { - assert(hasBeenConfigured); - assert(router); - assert(nodeDB); - return 0; - } - std::this_thread::sleep_for(std::chrono::seconds(1)); + meshtasticThread = std::thread([program = *argv[0]]() { + char nodeIdStr[12]; + strcpy(nodeIdStr, std::to_string(nodeId).c_str()); + int argc = 7; + char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr}; + try { + portduino_main(argc, argv); + } catch (const ShouldExitException &) { } - return 1; + }); + std::atexit([] { + { + const std::lock_guard lck(loopLock); + loopShouldExit = true; + loopCV.notify_one(); + } + meshtasticThread.join(); + }); + + // Wait for startup. + for (int i = 1; i < 20; ++i) { + if (powerFSM.getState() == &stateON) { + assert(hasBeenConfigured); + assert(router); + assert(nodeDB); + return 0; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + return 1; } // This is the main entrypoint for the fuzzer (the fuzz target). The fuzzer will provide an array of bytes to be @@ -173,34 +167,33 @@ int LLVMFuzzerInitialize(int *argc, char ***argv) // // This guide provides best practices for writing a fuzzer target. // https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length) -{ - meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default; - pb_istream_t stream = pb_istream_from_buffer(data, length); - // Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa. - if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed || - p.public_key.size || p.next_hop || p.relay_node || p.tx_after) - return -1; // Reject: The input will not be added to the corpus. - if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - meshtastic_Data d; - stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size); - if (!pb_decode(&stream, &meshtastic_Data_msg, &d)) - return -1; // Reject: The input will not be added to the corpus. - } +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length) { + meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default; + pb_istream_t stream = pb_istream_from_buffer(data, length); + // Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa. + if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed || p.public_key.size || + p.next_hop || p.relay_node || p.tx_after) + return -1; // Reject: The input will not be added to the corpus. + if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + meshtastic_Data d; + stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size); + if (!pb_decode(&stream, &meshtastic_Data_msg, &d)) + return -1; // Reject: The input will not be added to the corpus. + } - // Provide default values for a few fields so the fuzzer doesn't need to guess them. - if (p.from == 0) - p.from = nodeDB->getNodeNum(); - if (p.to == 0) - p.to = nodeDB->getNodeNum(); - static uint32_t packetId = 0; - if (p.id == 0) - p.id == ++packetId; - if (p.pki_encrypted && config.security.admin_key_count) - memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key)); + // Provide default values for a few fields so the fuzzer doesn't need to guess them. + if (p.from == 0) + p.from = nodeDB->getNodeNum(); + if (p.to == 0) + p.to = nodeDB->getNodeNum(); + static uint32_t packetId = 0; + if (p.id == 0) + p.id == ++packetId; + if (p.pki_encrypted && config.security.admin_key_count) + memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key)); - router->enqueueReceivedMessage(packetPool.allocCopy(p)); - runLoopOnce(); - return 0; // Accept: The input may be added to the corpus. + router->enqueueReceivedMessage(packetPool.allocCopy(p)); + runLoopOnce(); + return 0; // Accept: The input may be added to the corpus. } } \ No newline at end of file diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index 947b1e054..59068f4e0 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -21,192 +21,183 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); extern unPhone unphone; #endif -namespace concurrency -{ -class AmbientLightingThread : public concurrency::OSThread -{ - public: - explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") - { - notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. +namespace concurrency { +class AmbientLightingThread : public concurrency::OSThread { +public: + explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") { + notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. // Enables Ambient Lighting by default if conditions are meet. #ifdef HAS_RGB_LED #ifdef ENABLE_AMBIENTLIGHTING - moduleConfig.ambient_lighting.led_state = true; + moduleConfig.ambient_lighting.led_state = true; #endif #endif - // Uncomment to test module - // moduleConfig.ambient_lighting.led_state = true; - // moduleConfig.ambient_lighting.current = 10; - // Default to a color based on our node number - // moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; - // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; - // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; + // Uncomment to test module + // moduleConfig.ambient_lighting.led_state = true; + // moduleConfig.ambient_lighting.current = 10; + // Default to a color based on our node number + // moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; + // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; + // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; #if defined(HAS_NCP5623) || defined(HAS_LP5562) - _type = type; - if (_type == ScanI2C::DeviceType::NONE) { - LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus"); - disable(); - return; - } + _type = type; + if (_type == ScanI2C::DeviceType::NONE) { + LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus"); + disable(); + return; + } #endif #ifdef HAS_RGB_LED - if (!moduleConfig.ambient_lighting.led_state) { - LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); - disable(); - return; - } - LOG_DEBUG("AmbientLighting init"); + if (!moduleConfig.ambient_lighting.led_state) { + LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); + disable(); + return; + } + LOG_DEBUG("AmbientLighting init"); #ifdef HAS_NCP5623 - if (_type == ScanI2C::NCP5623) { - rgb.begin(); + if (_type == ScanI2C::NCP5623) { + rgb.begin(); #endif #ifdef HAS_LP5562 - if (_type == ScanI2C::LP5562) { - rgbw.begin(); + if (_type == ScanI2C::LP5562) { + rgbw.begin(); #endif #ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); + pinMode(RGBLED_RED, OUTPUT); + pinMode(RGBLED_GREEN, OUTPUT); + pinMode(RGBLED_BLUE, OUTPUT); #endif #ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); + pixels.begin(); // Initialise the pixel(s) + pixels.clear(); // Set all pixel colors to 'off' + pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - setLighting(); + setLighting(); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) - } + } #endif - } + } - protected: - int32_t runOnce() override - { + protected: + int32_t runOnce() override { #ifdef HAS_RGB_LED #if defined(HAS_NCP5623) || defined(HAS_LP5562) - if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { + if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif - setLighting(); - return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification + setLighting(); + return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) - } + } #endif #endif - return disable(); - } + return disable(); + } - // When shutdown() is issued, setLightingOff will be called. - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &AmbientLightingThread::setLightingOff); + // When shutdown() is issued, setLightingOff will be called. + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &AmbientLightingThread::setLightingOff); - private: - ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; + private: + ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; - // Turn RGB lighting off, is used in junction to shutdown() - int setLightingOff(void *unused) - { + // Turn RGB lighting off, is used in junction to shutdown() + int setLightingOff(void *unused) { #ifdef HAS_NCP5623 - rgb.setCurrent(0); - rgb.setRed(0); - rgb.setGreen(0); - rgb.setBlue(0); - LOG_INFO("OFF: NCP5623 Ambient lighting"); + rgb.setCurrent(0); + rgb.setRed(0); + rgb.setGreen(0); + rgb.setBlue(0); + LOG_INFO("OFF: NCP5623 Ambient lighting"); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(0); - rgbw.setRed(0); - rgbw.setGreen(0); - rgbw.setBlue(0); - rgbw.setWhite(0); - LOG_INFO("OFF: LP5562 Ambient lighting"); + rgbw.setCurrent(0); + rgbw.setRed(0); + rgbw.setGreen(0); + rgbw.setBlue(0); + rgbw.setWhite(0); + LOG_INFO("OFF: LP5562 Ambient lighting"); #endif #ifdef HAS_NEOPIXEL - pixels.clear(); - pixels.show(); - LOG_INFO("OFF: NeoPixel Ambient lighting"); + pixels.clear(); + pixels.show(); + LOG_INFO("OFF: NeoPixel Ambient lighting"); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - 0); - analogWrite(RGBLED_GREEN, 255 - 0); - analogWrite(RGBLED_BLUE, 255 - 0); - LOG_INFO("OFF: Ambient light RGB Common Anode"); + analogWrite(RGBLED_RED, 255 - 0); + analogWrite(RGBLED_GREEN, 255 - 0); + analogWrite(RGBLED_BLUE, 255 - 0); + LOG_INFO("OFF: Ambient light RGB Common Anode"); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, 0); - analogWrite(RGBLED_GREEN, 0); - analogWrite(RGBLED_BLUE, 0); - LOG_INFO("OFF: Ambient light RGB Common Cathode"); + analogWrite(RGBLED_RED, 0); + analogWrite(RGBLED_GREEN, 0); + analogWrite(RGBLED_BLUE, 0); + LOG_INFO("OFF: Ambient light RGB Common Cathode"); #endif #ifdef UNPHONE - unphone.rgb(0, 0, 0); - LOG_INFO("OFF: unPhone Ambient lighting"); + unphone.rgb(0, 0, 0); + LOG_INFO("OFF: unPhone Ambient lighting"); #endif - return 0; - } + return 0; + } - void setLighting() - { + void setLighting() { #ifdef HAS_NCP5623 - rgb.setCurrent(moduleConfig.ambient_lighting.current); - rgb.setRed(moduleConfig.ambient_lighting.red); - rgb.setGreen(moduleConfig.ambient_lighting.green); - rgb.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgb.setCurrent(moduleConfig.ambient_lighting.current); + rgb.setRed(moduleConfig.ambient_lighting.red); + rgb.setGreen(moduleConfig.ambient_lighting.green); + rgb.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, + moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(moduleConfig.ambient_lighting.current); - rgbw.setRed(moduleConfig.ambient_lighting.red); - rgbw.setGreen(moduleConfig.ambient_lighting.green); - rgbw.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgbw.setCurrent(moduleConfig.ambient_lighting.current); + rgbw.setRed(moduleConfig.ambient_lighting.red); + rgbw.setGreen(moduleConfig.ambient_lighting.green); + rgbw.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, + moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue), - 0, NEOPIXEL_COUNT); + pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue), 0, + NEOPIXEL_COUNT); // RadioMaster Bandit has addressable LED at the two buttons // this allow us to set different lighting for them in variant.h file. #ifdef RADIOMASTER_900_BANDIT #if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX) - pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); + pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) - pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); + pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); #endif #endif - pixels.show(); - // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + pixels.show(); + // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", + // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); + analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); + analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); + analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); + analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef UNPHONE - unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue); #endif - } - }; + } + }; } // namespace concurrency diff --git a/src/AudioThread.h b/src/AudioThread.h index 23552c421..79f57f730 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -18,93 +18,86 @@ extern ExtensionIOXL9555 io; #define AUDIO_THREAD_INTERVAL_MS 100 -class AudioThread : public concurrency::OSThread -{ - public: - AudioThread() : OSThread("Audio") { initOutput(); } +class AudioThread : public concurrency::OSThread { +public: + AudioThread() : OSThread("Audio") { initOutput(); } - void beginRttl(const void *data, uint32_t len) - { + void beginRttl(const void *data, uint32_t len) { #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif - setCPUFast(true); - rtttlFile = new AudioFileSourcePROGMEM(data, len); - i2sRtttl = new AudioGeneratorRTTTL(); - i2sRtttl->begin(rtttlFile, audioOut); + setCPUFast(true); + rtttlFile = new AudioFileSourcePROGMEM(data, len); + i2sRtttl = new AudioGeneratorRTTTL(); + i2sRtttl->begin(rtttlFile, audioOut); + } + + // Also handles actually playing the RTTTL, needs to be called in loop + bool isPlaying() { + if (i2sRtttl != nullptr) { + return i2sRtttl->isRunning() && i2sRtttl->loop(); + } + return false; + } + + void stop() { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; } - // Also handles actually playing the RTTTL, needs to be called in loop - bool isPlaying() - { - if (i2sRtttl != nullptr) { - return i2sRtttl->isRunning() && i2sRtttl->loop(); - } - return false; + if (rtttlFile != nullptr) { + delete rtttlFile; + rtttlFile = nullptr; } - void stop() - { - if (i2sRtttl != nullptr) { - i2sRtttl->stop(); - delete i2sRtttl; - i2sRtttl = nullptr; - } - - if (rtttlFile != nullptr) { - delete rtttlFile; - rtttlFile = nullptr; - } - - setCPUFast(false); + setCPUFast(false); #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.digitalWrite(EXPANDS_AMP_EN, LOW); #endif - } + } - void readAloud(const char *text) - { - if (i2sRtttl != nullptr) { - i2sRtttl->stop(); - delete i2sRtttl; - i2sRtttl = nullptr; - } + void readAloud(const char *text) { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; + } #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif - ESP8266SAM *sam = new ESP8266SAM; - sam->Say(audioOut, text); - delete sam; - setCPUFast(false); + ESP8266SAM *sam = new ESP8266SAM; + sam->Say(audioOut, text); + delete sam; + setCPUFast(false); #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.digitalWrite(EXPANDS_AMP_EN, LOW); #endif - } + } - protected: - int32_t runOnce() override - { - canSleep = true; // Assume we should not keep the board awake +protected: + int32_t runOnce() override { + canSleep = true; // Assume we should not keep the board awake - // if (i2sRtttl != nullptr && i2sRtttl->isRunning()) { - // i2sRtttl->loop(); - // } - return AUDIO_THREAD_INTERVAL_MS; - } + // if (i2sRtttl != nullptr && i2sRtttl->isRunning()) { + // i2sRtttl->loop(); + // } + return AUDIO_THREAD_INTERVAL_MS; + } - private: - void initOutput() - { - audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); - audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); - audioOut->SetGain(0.2); - }; +private: + void initOutput() { + audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); + audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); + audioOut->SetGain(0.2); + }; - AudioGeneratorRTTTL *i2sRtttl = nullptr; - AudioOutputI2S *audioOut = nullptr; + AudioGeneratorRTTTL *i2sRtttl = nullptr; + AudioOutputI2S *audioOut = nullptr; - AudioFileSourcePROGMEM *rtttlFile = nullptr; + AudioFileSourcePROGMEM *rtttlFile = nullptr; }; #endif diff --git a/src/BluetoothCommon.cpp b/src/BluetoothCommon.cpp index d9502e4f5..7fc6be4df 100644 --- a/src/BluetoothCommon.cpp +++ b/src/BluetoothCommon.cpp @@ -3,15 +3,9 @@ // NRF52 wants these constants as byte arrays // Generated here https://yupana-engineering.com/online-uuid-to-c-array-converter - but in REVERSE BYTE ORDER -const uint8_t MESH_SERVICE_UUID_16[16u] = {0xfd, 0xea, 0x73, 0xe2, 0xca, 0x5d, 0xa8, 0x9f, - 0x1f, 0x46, 0xa8, 0x15, 0x18, 0xb2, 0xa1, 0x6b}; -const uint8_t TORADIO_UUID_16[16u] = {0xe7, 0x01, 0x44, 0x12, 0x66, 0x78, 0xdd, 0xa1, - 0xad, 0x4d, 0x9e, 0x12, 0xd2, 0x76, 0x5c, 0xf7}; -const uint8_t FROMRADIO_UUID_16[16u] = {0x02, 0x00, 0x12, 0xac, 0x42, 0x02, 0x78, 0xb8, - 0xed, 0x11, 0x93, 0x49, 0x9e, 0xe6, 0x55, 0x2c}; -const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70, 0xa6, - 0x66, 0x4f, 0x00, 0xa8, 0x8c, 0xa1, 0x9d, 0xed}; -const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa, - 0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c}; -const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99, - 0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A}; \ No newline at end of file +const uint8_t MESH_SERVICE_UUID_16[16u] = {0xfd, 0xea, 0x73, 0xe2, 0xca, 0x5d, 0xa8, 0x9f, 0x1f, 0x46, 0xa8, 0x15, 0x18, 0xb2, 0xa1, 0x6b}; +const uint8_t TORADIO_UUID_16[16u] = {0xe7, 0x01, 0x44, 0x12, 0x66, 0x78, 0xdd, 0xa1, 0xad, 0x4d, 0x9e, 0x12, 0xd2, 0x76, 0x5c, 0xf7}; +const uint8_t FROMRADIO_UUID_16[16u] = {0x02, 0x00, 0x12, 0xac, 0x42, 0x02, 0x78, 0xb8, 0xed, 0x11, 0x93, 0x49, 0x9e, 0xe6, 0x55, 0x2c}; +const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70, 0xa6, 0x66, 0x4f, 0x00, 0xa8, 0x8c, 0xa1, 0x9d, 0xed}; +const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa, 0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c}; +const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99, 0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A}; \ No newline at end of file diff --git a/src/BluetoothCommon.h b/src/BluetoothCommon.h index 440d13844..d47180872 100644 --- a/src/BluetoothCommon.h +++ b/src/BluetoothCommon.h @@ -21,12 +21,11 @@ extern const uint8_t MESH_SERVICE_UUID_16[], TORADIO_UUID_16[16u], FROMRADIO_UUI /// Given a level between 0-100, update the BLE attribute void updateBatteryLevel(uint8_t level); -class BluetoothApi -{ - public: - virtual void setup(); - virtual void shutdown(); - virtual void clearBonds(); - virtual bool isConnected(); - virtual int getRssi() = 0; +class BluetoothApi { +public: + virtual void setup(); + virtual void shutdown(); + virtual void clearBonds(); + virtual bool isConnected(); + virtual int getRssi() = 0; }; \ No newline at end of file diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 680aec929..851843093 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -5,113 +5,106 @@ #include "meshUtils.h" #include -namespace meshtastic -{ +namespace meshtastic { // Describes the state of the Bluetooth connection // Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code -class BluetoothStatus : public Status -{ - public: - enum class ConnectionState { - DISCONNECTED, - PAIRING, - CONNECTED, - }; +class BluetoothStatus : public Status { +public: + enum class ConnectionState { + DISCONNECTED, + PAIRING, + CONNECTED, + }; - private: - CallbackObserver statusObserver = - CallbackObserver(this, &BluetoothStatus::updateStatus); +private: + CallbackObserver statusObserver = + CallbackObserver(this, &BluetoothStatus::updateStatus); - ConnectionState state = ConnectionState::DISCONNECTED; - std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero + ConnectionState state = ConnectionState::DISCONNECTED; + std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero - public: - BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; } +public: + BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; } - // New BluetoothStatus: connected or disconnected - explicit BluetoothStatus(ConnectionState state) - { - assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey - statusType = STATUS_TYPE_BLUETOOTH; - this->state = state; + // New BluetoothStatus: connected or disconnected + explicit BluetoothStatus(ConnectionState state) { + assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey + statusType = STATUS_TYPE_BLUETOOTH; + this->state = state; + } + + // New BluetoothStatus: pairing, with passkey + explicit BluetoothStatus(const std::string &passkey) : Status() { + statusType = STATUS_TYPE_BLUETOOTH; + this->state = ConnectionState::PAIRING; + this->passkey = passkey; + } + + ConnectionState getConnectionState() const { return this->state; } + + std::string getPasskey() const { + assert(state == ConnectionState::PAIRING); + return this->passkey; + } + + void observe(Observable *source) { statusObserver.observe(source); } + + bool matches(const BluetoothStatus *newStatus) const { + if (this->state == newStatus->getConnectionState()) { + // Same state: CONNECTED / DISCONNECTED + if (this->state != ConnectionState::PAIRING) + return true; + // Same state: PAIRING, and passkey matches + else if (this->getPasskey() == newStatus->getPasskey()) + return true; } - // New BluetoothStatus: pairing, with passkey - explicit BluetoothStatus(const std::string &passkey) : Status() - { - statusType = STATUS_TYPE_BLUETOOTH; - this->state = ConnectionState::PAIRING; - this->passkey = passkey; - } + return false; + } - ConnectionState getConnectionState() const { return this->state; } + int updateStatus(const BluetoothStatus *newStatus) { + // Has the status changed? + if (!matches(newStatus)) { + // Copy the members + state = newStatus->getConnectionState(); + if (state == ConnectionState::PAIRING) + passkey = newStatus->getPasskey(); - std::string getPasskey() const - { - assert(state == ConnectionState::PAIRING); - return this->passkey; - } + // Tell anyone interested that we have an update + onNewStatus.notifyObservers(this); - void observe(Observable *source) { statusObserver.observe(source); } - - bool matches(const BluetoothStatus *newStatus) const - { - if (this->state == newStatus->getConnectionState()) { - // Same state: CONNECTED / DISCONNECTED - if (this->state != ConnectionState::PAIRING) - return true; - // Same state: PAIRING, and passkey matches - else if (this->getPasskey() == newStatus->getPasskey()) - return true; - } - - return false; - } - - int updateStatus(const BluetoothStatus *newStatus) - { - // Has the status changed? - if (!matches(newStatus)) { - // Copy the members - state = newStatus->getConnectionState(); - if (state == ConnectionState::PAIRING) - passkey = newStatus->getPasskey(); - - // Tell anyone interested that we have an update - onNewStatus.notifyObservers(this); - - // Debug only: - switch (state) { - case ConnectionState::PAIRING: - LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str()); - break; - case ConnectionState::CONNECTED: - LOG_DEBUG("BluetoothStatus CONNECTED"); + // Debug only: + switch (state) { + case ConnectionState::PAIRING: + LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str()); + break; + case ConnectionState::CONNECTED: + LOG_DEBUG("BluetoothStatus CONNECTED"); #ifdef BLE_LED #ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, LOW); + digitalWrite(BLE_LED, LOW); #else - digitalWrite(BLE_LED, HIGH); + digitalWrite(BLE_LED, HIGH); #endif #endif - break; + break; - case ConnectionState::DISCONNECTED: - LOG_DEBUG("BluetoothStatus DISCONNECTED"); + case ConnectionState::DISCONNECTED: + LOG_DEBUG("BluetoothStatus DISCONNECTED"); #ifdef BLE_LED #ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); + digitalWrite(BLE_LED, HIGH); #else - digitalWrite(BLE_LED, LOW); + digitalWrite(BLE_LED, LOW); #endif #endif - break; - } - } - - return 0; + break; + } } + + return 0; + } }; } // namespace meshtastic diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index d65c4f1e8..def8155d7 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -31,168 +31,150 @@ SOFTWARE.*/ #endif /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic -extern "C" void logLegacy(const char *level, const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - if (console) - console->vprintf(level, fmt, args); - va_end(args); +extern "C" void logLegacy(const char *level, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + if (console) + console->vprintf(level, fmt, args); + va_end(args); } #if HAS_NETWORKING -Syslog::Syslog(UDP &client) -{ - this->_client = &client; +Syslog::Syslog(UDP &client) { + this->_client = &client; + this->_server = NULL; + this->_port = 0; + this->_deviceHostname = SYSLOG_NILVALUE; + this->_appName = SYSLOG_NILVALUE; + this->_priDefault = LOGLEVEL_KERN; +} + +Syslog &Syslog::server(const char *server, uint16_t port) { + if (this->_ip.fromString(server)) { this->_server = NULL; - this->_port = 0; - this->_deviceHostname = SYSLOG_NILVALUE; - this->_appName = SYSLOG_NILVALUE; - this->_priDefault = LOGLEVEL_KERN; + } else { + this->_server = server; + } + this->_port = port; + return *this; } -Syslog &Syslog::server(const char *server, uint16_t port) -{ - if (this->_ip.fromString(server)) { - this->_server = NULL; - } else { - this->_server = server; - } - this->_port = port; - return *this; +Syslog &Syslog::server(IPAddress ip, uint16_t port) { + this->_ip = ip; + this->_server = NULL; + this->_port = port; + return *this; } -Syslog &Syslog::server(IPAddress ip, uint16_t port) -{ - this->_ip = ip; - this->_server = NULL; - this->_port = port; - return *this; +Syslog &Syslog::deviceHostname(const char *deviceHostname) { + this->_deviceHostname = (deviceHostname == NULL) ? SYSLOG_NILVALUE : deviceHostname; + return *this; } -Syslog &Syslog::deviceHostname(const char *deviceHostname) -{ - this->_deviceHostname = (deviceHostname == NULL) ? SYSLOG_NILVALUE : deviceHostname; - return *this; +Syslog &Syslog::appName(const char *appName) { + this->_appName = (appName == NULL) ? SYSLOG_NILVALUE : appName; + return *this; } -Syslog &Syslog::appName(const char *appName) -{ - this->_appName = (appName == NULL) ? SYSLOG_NILVALUE : appName; - return *this; +Syslog &Syslog::defaultPriority(uint16_t pri) { + this->_priDefault = pri; + return *this; } -Syslog &Syslog::defaultPriority(uint16_t pri) -{ - this->_priDefault = pri; - return *this; +Syslog &Syslog::logMask(uint8_t priMask) { + this->_priMask = priMask; + return *this; } -Syslog &Syslog::logMask(uint8_t priMask) -{ - this->_priMask = priMask; - return *this; +void Syslog::enable() { + this->_client->begin(this->_port); + this->_enabled = true; } -void Syslog::enable() -{ - this->_client->begin(this->_port); - this->_enabled = true; +void Syslog::disable() { + this->_enabled = false; + this->_client->stop(); } -void Syslog::disable() -{ - this->_enabled = false; - this->_client->stop(); -} +bool Syslog::isEnabled() { return this->_enabled; } -bool Syslog::isEnabled() -{ - return this->_enabled; -} +bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args) { return this->vlogf(pri, this->_appName, fmt, args); } -bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args) -{ - return this->vlogf(pri, this->_appName, fmt, args); -} +bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) { + char *message; + size_t initialLen; + size_t len; + bool result; -bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) -{ - char *message; - size_t initialLen; - size_t len; - bool result; + initialLen = strlen(fmt); - initialLen = strlen(fmt); - - message = new char[initialLen + 1]; - - len = vsnprintf(message, initialLen + 1, fmt, args); - if (len > initialLen) { - delete[] message; - message = new char[len + 1]; - - vsnprintf(message, len + 1, fmt, args); - } - - result = this->_sendLog(pri, appName, message); + message = new char[initialLen + 1]; + len = vsnprintf(message, initialLen + 1, fmt, args); + if (len > initialLen) { delete[] message; - return result; + message = new char[len + 1]; + + vsnprintf(message, len + 1, fmt, args); + } + + result = this->_sendLog(pri, appName, message); + + delete[] message; + return result; } -inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message) -{ - int result; +inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message) { + int result; #ifdef ARCH_PORTDUINO - bool utf = !portduino_config.ascii_logs; + bool utf = !portduino_config.ascii_logs; #else - bool utf = true; + bool utf = true; #endif - if (!this->_enabled) - return false; + if (!this->_enabled) + return false; - if ((this->_server == NULL && this->_ip == INADDR_NONE) || this->_port == 0) - return false; - - // Check priority against priMask values. - if ((LOG_MASK(LOG_PRI(pri)) & this->_priMask) == 0) - return true; - - // Set default facility if none specified. - if ((pri & LOG_FACMASK) == 0) - pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); - - if (this->_server != NULL) { - result = this->_client->beginPacket(this->_server, this->_port); - } else { - result = this->_client->beginPacket(this->_ip, this->_port); - } - - if (result != 1) - return false; - - this->_client->print('<'); - this->_client->print(pri); - this->_client->print(F(">1 - ")); - this->_client->print(this->_deviceHostname); - this->_client->print(' '); - this->_client->print(appName); - this->_client->print(F(" - - - ")); - if (utf) { - this->_client->print(F("\xEF\xBB\xBF")); - } else { - this->_client->print(F(" ")); - } - this->_client->print(F("[")); - this->_client->print(int(millis() / 1000)); - this->_client->print(F("]: ")); - this->_client->print(message); - this->_client->endPacket(); + if ((this->_server == NULL && this->_ip == INADDR_NONE) || this->_port == 0) + return false; + // Check priority against priMask values. + if ((LOG_MASK(LOG_PRI(pri)) & this->_priMask) == 0) return true; + + // Set default facility if none specified. + if ((pri & LOG_FACMASK) == 0) + pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); + + if (this->_server != NULL) { + result = this->_client->beginPacket(this->_server, this->_port); + } else { + result = this->_client->beginPacket(this->_ip, this->_port); + } + + if (result != 1) + return false; + + this->_client->print('<'); + this->_client->print(pri); + this->_client->print(F(">1 - ")); + this->_client->print(this->_deviceHostname); + this->_client->print(' '); + this->_client->print(appName); + this->_client->print(F(" - - - ")); + if (utf) { + this->_client->print(F("\xEF\xBB\xBF")); + } else { + this->_client->print(F(" ")); + } + this->_client->print(F("[")); + this->_client->print(int(millis() / 1000)); + this->_client->print(F("]: ")); + this->_client->print(message); + this->_client->endPacket(); + + return true; } #endif diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 98bbe0f72..44481ab1c 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -74,13 +74,13 @@ extern MemGet memGet; // Macro-based heap debugging #define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); -#define DEBUG_HEAP_AFTER(context, ptr) \ - do { \ - auto heapAfter = memGet.getFreeHeap(); \ - if (heapBefore != heapAfter) { \ - LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ - } \ - } while (0) +#define DEBUG_HEAP_AFTER(context, ptr) \ + do { \ + auto heapAfter = memGet.getFreeHeap(); \ + if (heapBefore != heapAfter) { \ + LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ + } \ + } while (0) #else #define LOG_HEAP(...) @@ -162,37 +162,36 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...); #if HAS_NETWORKING -class Syslog -{ - private: - UDP *_client; - IPAddress _ip; - const char *_server; - uint16_t _port; - const char *_deviceHostname; - const char *_appName; - uint16_t _priDefault; - uint8_t _priMask = 0xff; - bool _enabled = false; +class Syslog { +private: + UDP *_client; + IPAddress _ip; + const char *_server; + uint16_t _port; + const char *_deviceHostname; + const char *_appName; + uint16_t _priDefault; + uint8_t _priMask = 0xff; + bool _enabled = false; - bool _sendLog(uint16_t pri, const char *appName, const char *message); + bool _sendLog(uint16_t pri, const char *appName, const char *message); - public: - explicit Syslog(UDP &client); +public: + explicit Syslog(UDP &client); - Syslog &server(const char *server, uint16_t port); - Syslog &server(IPAddress ip, uint16_t port); - Syslog &deviceHostname(const char *deviceHostname); - Syslog &appName(const char *appName); - Syslog &defaultPriority(uint16_t pri = LOGLEVEL_KERN); - Syslog &logMask(uint8_t priMask); + Syslog &server(const char *server, uint16_t port); + Syslog &server(IPAddress ip, uint16_t port); + Syslog &deviceHostname(const char *deviceHostname); + Syslog &appName(const char *appName); + Syslog &defaultPriority(uint16_t pri = LOGLEVEL_KERN); + Syslog &logMask(uint8_t priMask); - void enable(); - void disable(); - bool isEnabled(); + void enable(); + void disable(); + bool isEnabled(); - bool vlogf(uint16_t pri, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); - bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); + bool vlogf(uint16_t pri, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); + bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); }; #endif // HAS_NETWORKING \ No newline at end of file diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index d88f9fc9f..eea9bf33b 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -1,86 +1,83 @@ #include "DisplayFormatters.h" -const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, - bool usePreset) -{ +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset) { - // If use_preset is false, always return "Custom" - if (!usePreset) { - return "Custom"; - } + // If use_preset is false, always return "Custom" + if (!usePreset) { + return "Custom"; + } - switch (preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - return useShortName ? "ShortT" : "ShortTurbo"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - return useShortName ? "ShortS" : "ShortSlow"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - return useShortName ? "ShortF" : "ShortFast"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - return useShortName ? "MedS" : "MediumSlow"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - return useShortName ? "MedF" : "MediumFast"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - return useShortName ? "LongS" : "LongSlow"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: - return useShortName ? "LongF" : "LongFast"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: - return useShortName ? "LongT" : "LongTurbo"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - return useShortName ? "LongM" : "LongMod"; - break; - default: - return useShortName ? "Custom" : "Invalid"; - break; - } + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return useShortName ? "ShortT" : "ShortTurbo"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + return useShortName ? "ShortS" : "ShortSlow"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + return useShortName ? "ShortF" : "ShortFast"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + return useShortName ? "MedS" : "MediumSlow"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return useShortName ? "MedF" : "MediumFast"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + return useShortName ? "LongS" : "LongSlow"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return useShortName ? "LongF" : "LongFast"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + return useShortName ? "LongT" : "LongTurbo"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + return useShortName ? "LongM" : "LongMod"; + break; + default: + return useShortName ? "Custom" : "Invalid"; + break; + } } -const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) -{ - switch (role) { - case meshtastic_Config_DeviceConfig_Role_CLIENT: - return "Client"; - break; - case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: - return "Client Mute"; - break; - case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: - return "Client Hidden"; - break; - case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: - return "Client Base"; - break; - case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: - return "Lost and Found"; - break; - case meshtastic_Config_DeviceConfig_Role_TRACKER: - return "Tracker"; - break; - case meshtastic_Config_DeviceConfig_Role_SENSOR: - return "Sensor"; - break; - case meshtastic_Config_DeviceConfig_Role_TAK: - return "TAK"; - break; - case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: - return "TAK Tracker"; - break; - case meshtastic_Config_DeviceConfig_Role_ROUTER: - return "Router"; - break; - case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: - return "Router Late"; - break; - default: - return "Unknown"; - break; - } +const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) { + switch (role) { + case meshtastic_Config_DeviceConfig_Role_CLIENT: + return "Client"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: + return "Client Mute"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: + return "Client Hidden"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: + return "Client Base"; + break; + case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: + return "Lost and Found"; + break; + case meshtastic_Config_DeviceConfig_Role_TRACKER: + return "Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_SENSOR: + return "Sensor"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK: + return "TAK"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: + return "TAK Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER: + return "Router"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: + return "Router Late"; + break; + default: + return "Unknown"; + break; + } } \ No newline at end of file diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index 981010b33..025225ec3 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -1,10 +1,8 @@ #pragma once #include "NodeDB.h" -class DisplayFormatters -{ - public: - static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, - bool usePreset); - static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); +class DisplayFormatters { +public: + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset); + static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); }; diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index f215be80f..512fb5a57 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -1,11 +1,11 @@ /** * @file FSCommon.cpp - * @brief This file contains functions for common filesystem operations such as copying, renaming, listing and deleting files and - * directories. + * @brief This file contains functions for common filesystem operations such as copying, renaming, listing and deleting + * files and directories. * - * The functions in this file are used to perform common filesystem operations such as copying, renaming, listing and deleting - * files and directories. These functions are used in the Meshtastic-device project to manage files and directories on the - * device's filesystem. + * The functions in this file are used to perform common filesystem operations such as copying, renaming, listing and + * deleting files and directories. These functions are used in the Meshtastic-device project to manage files and + * directories on the device's filesystem. * */ #include "FSCommon.h" @@ -37,34 +37,33 @@ SPIClass SPI_HSPI(HSPI); * @param to The path of the destination file. * @return true if the file was successfully copied, false otherwise. */ -bool copyFile(const char *from, const char *to) -{ +bool copyFile(const char *from, const char *to) { #ifdef FSCom - // take SPI Lock - concurrency::LockGuard g(spiLock); - unsigned char cbuffer[16]; + // take SPI Lock + concurrency::LockGuard g(spiLock); + unsigned char cbuffer[16]; - File f1 = FSCom.open(from, FILE_O_READ); - if (!f1) { - LOG_ERROR("Failed to open source file %s", from); - return false; - } + File f1 = FSCom.open(from, FILE_O_READ); + if (!f1) { + LOG_ERROR("Failed to open source file %s", from); + return false; + } - File f2 = FSCom.open(to, FILE_O_WRITE); - if (!f2) { - LOG_ERROR("Failed to open destination file %s", to); - return false; - } + File f2 = FSCom.open(to, FILE_O_WRITE); + if (!f2) { + LOG_ERROR("Failed to open destination file %s", to); + return false; + } - while (f1.available() > 0) { - byte i = f1.read(cbuffer, 16); - f2.write(cbuffer, i); - } + while (f1.available() > 0) { + byte i = f1.read(cbuffer, 16); + f2.write(cbuffer, i); + } - f2.flush(); - f2.close(); - f1.close(); - return true; + f2.flush(); + f2.close(); + f1.close(); + return true; #endif } @@ -76,24 +75,23 @@ bool copyFile(const char *from, const char *to) * * @return True if the file was successfully renamed, false otherwise. */ -bool renameFile(const char *pathFrom, const char *pathTo) -{ +bool renameFile(const char *pathFrom, const char *pathTo) { #ifdef FSCom #ifdef ARCH_ESP32 - // take SPI Lock - spiLock->lock(); - // rename was fixed for ESP32 IDF LittleFS in April - bool result = FSCom.rename(pathFrom, pathTo); - spiLock->unlock(); - return result; + // take SPI Lock + spiLock->lock(); + // rename was fixed for ESP32 IDF LittleFS in April + bool result = FSCom.rename(pathFrom, pathTo); + spiLock->unlock(); + return result; #else - // copyFile does its own locking. - if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { - return true; - } else { - return false; - } + // copyFile does its own locking. + if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { + return true; + } else { + return false; + } #endif #endif @@ -111,45 +109,44 @@ bool renameFile(const char *pathFrom, const char *pathTo) * @param levels The number of levels of subdirectories to list. * @return A vector of strings containing the full path of each file in the directory. */ -std::vector getFiles(const char *dirname, uint8_t levels) -{ - std::vector filenames = {}; +std::vector getFiles(const char *dirname, uint8_t levels) { + std::vector filenames = {}; #ifdef FSCom - File root = FSCom.open(dirname, FILE_O_READ); - if (!root) - return filenames; - if (!root.isDirectory()) - return filenames; - - File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { -#ifdef ARCH_ESP32 - std::vector subDirFilenames = getFiles(file.path(), levels - 1); -#else - std::vector subDirFilenames = getFiles(file.name(), levels - 1); -#endif - filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end()); - file.close(); - } - } else { - meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; -#ifdef ARCH_ESP32 - strcpy(fileInfo.file_name, file.path()); -#else - strcpy(fileInfo.file_name, file.name()); -#endif - if (!String(fileInfo.file_name).endsWith(".")) { - filenames.push_back(fileInfo); - } - file.close(); - } - file = root.openNextFile(); - } - root.close(); -#endif + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) return filenames; + if (!root.isDirectory()) + return filenames; + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { +#ifdef ARCH_ESP32 + std::vector subDirFilenames = getFiles(file.path(), levels - 1); +#else + std::vector subDirFilenames = getFiles(file.name(), levels - 1); +#endif + filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end()); + file.close(); + } + } else { + meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; +#ifdef ARCH_ESP32 + strcpy(fileInfo.file_name, file.path()); +#else + strcpy(fileInfo.file_name, file.name()); +#endif + if (!String(fileInfo.file_name).endsWith(".")) { + filenames.push_back(fileInfo); + } + file.close(); + } + file = root.openNextFile(); + } + root.close(); +#endif + return filenames; } /** @@ -160,100 +157,98 @@ std::vector getFiles(const char *dirname, uint8_t levels) * @param levels The number of levels of subdirectories to list. * @param del Whether or not to delete the contents of the directory after listing. */ -void listDir(const char *dirname, uint8_t levels, bool del) -{ +void listDir(const char *dirname, uint8_t levels, bool del) { #ifdef FSCom #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - char buffer[255]; + char buffer[255]; #endif - File root = FSCom.open(dirname, FILE_O_READ); - if (!root) { - return; - } - if (!root.isDirectory()) { - return; - } + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) { + return; + } + if (!root.isDirectory()) { + return; + } - File file = root.openNextFile(); - while ( - file && - file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395) - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { + File file = root.openNextFile(); + while (file && file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 + // glue (see issue 4395) + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { #ifdef ARCH_ESP32 - listDir(file.path(), levels - 1, del); - if (del) { - LOG_DEBUG("Remove %s", file.path()); - strncpy(buffer, file.path(), sizeof(buffer)); - file.close(); - FSCom.rmdir(buffer); - } else { - file.close(); - } -#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - listDir(file.name(), levels - 1, del); - if (del) { - LOG_DEBUG("Remove %s", file.name()); - strncpy(buffer, file.name(), sizeof(buffer)); - file.close(); - FSCom.rmdir(buffer); - } else { - file.close(); - } -#else - LOG_DEBUG(" %s (directory)", file.name()); - listDir(file.name(), levels - 1, del); - file.close(); -#endif - } + listDir(file.path(), levels - 1, del); + if (del) { + LOG_DEBUG("Remove %s", file.path()); + strncpy(buffer, file.path(), sizeof(buffer)); + file.close(); + FSCom.rmdir(buffer); } else { -#ifdef ARCH_ESP32 - if (del) { - LOG_DEBUG("Delete %s", file.path()); - strncpy(buffer, file.path(), sizeof(buffer)); - file.close(); - FSCom.remove(buffer); - } else { - LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); - file.close(); - } -#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - if (del) { - LOG_DEBUG("Delete %s", file.name()); - strncpy(buffer, file.name(), sizeof(buffer)); - file.close(); - FSCom.remove(buffer); - } else { - LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); - file.close(); - } -#else - LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); - file.close(); -#endif + file.close(); } - file = root.openNextFile(); - } -#ifdef ARCH_ESP32 - if (del) { - LOG_DEBUG("Remove %s", root.path()); - strncpy(buffer, root.path(), sizeof(buffer)); - root.close(); - FSCom.rmdir(buffer); - } else { - root.close(); - } #elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - if (del) { - LOG_DEBUG("Remove %s", root.name()); - strncpy(buffer, root.name(), sizeof(buffer)); - root.close(); - FSCom.rmdir(buffer); - } else { - root.close(); - } + listDir(file.name(), levels - 1, del); + if (del) { + LOG_DEBUG("Remove %s", file.name()); + strncpy(buffer, file.name(), sizeof(buffer)); + file.close(); + FSCom.rmdir(buffer); + } else { + file.close(); + } #else + LOG_DEBUG(" %s (directory)", file.name()); + listDir(file.name(), levels - 1, del); + file.close(); +#endif + } + } else { +#ifdef ARCH_ESP32 + if (del) { + LOG_DEBUG("Delete %s", file.path()); + strncpy(buffer, file.path(), sizeof(buffer)); + file.close(); + FSCom.remove(buffer); + } else { + LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); + file.close(); + } +#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + if (del) { + LOG_DEBUG("Delete %s", file.name()); + strncpy(buffer, file.name(), sizeof(buffer)); + file.close(); + FSCom.remove(buffer); + } else { + LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); + file.close(); + } +#else + LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); + file.close(); +#endif + } + file = root.openNextFile(); + } +#ifdef ARCH_ESP32 + if (del) { + LOG_DEBUG("Remove %s", root.path()); + strncpy(buffer, root.path(), sizeof(buffer)); root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + if (del) { + LOG_DEBUG("Remove %s", root.name()); + strncpy(buffer, root.name(), sizeof(buffer)); + root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#else + root.close(); #endif #endif } @@ -265,15 +260,14 @@ void listDir(const char *dirname, uint8_t levels, bool del) * * @param dirname The name of the directory to remove. */ -void rmDir(const char *dirname) -{ +void rmDir(const char *dirname) { #ifdef FSCom #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - listDir(dirname, 10, true); + listDir(dirname, 10, true); #elif defined(ARCH_NRF52) - // nRF52 implementation of LittleFS has a recursive delete function - FSCom.rmdir_r(dirname); + // nRF52 implementation of LittleFS has a recursive delete function + FSCom.rmdir_r(dirname); #endif #endif @@ -284,55 +278,53 @@ void rmDir(const char *dirname) */ __attribute__((weak, noinline)) void preFSBegin() {} -void fsInit() -{ +void fsInit() { #ifdef FSCom - concurrency::LockGuard g(spiLock); - preFSBegin(); - if (!FSBegin()) { - LOG_ERROR("Filesystem mount failed"); - // assert(0); This auto-formats the partition, so no need to fail here. - } + concurrency::LockGuard g(spiLock); + preFSBegin(); + if (!FSBegin()) { + LOG_ERROR("Filesystem mount failed"); + // assert(0); This auto-formats the partition, so no need to fail here. + } #if defined(ARCH_ESP32) - LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes()); + LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes()); #else - LOG_DEBUG("Filesystem files:"); + LOG_DEBUG("Filesystem files:"); #endif - listDir("/", 10); + listDir("/", 10); #endif } /** * Initializes the SD card and mounts the file system. */ -void setupSDCard() -{ +void setupSDCard() { #if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - concurrency::LockGuard g(spiLock); - SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); - if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { - LOG_DEBUG("No SD_MMC card detected"); - return; - } - uint8_t cardType = SD.cardType(); - if (cardType == CARD_NONE) { - LOG_DEBUG("No SD_MMC card attached"); - return; - } - LOG_DEBUG("SD_MMC Card Type: "); - if (cardType == CARD_MMC) { - LOG_DEBUG("MMC"); - } else if (cardType == CARD_SD) { - LOG_DEBUG("SDSC"); - } else if (cardType == CARD_SDHC) { - LOG_DEBUG("SDHC"); - } else { - LOG_DEBUG("UNKNOWN"); - } + concurrency::LockGuard g(spiLock); + SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { + LOG_DEBUG("No SD_MMC card detected"); + return; + } + uint8_t cardType = SD.cardType(); + if (cardType == CARD_NONE) { + LOG_DEBUG("No SD_MMC card attached"); + return; + } + LOG_DEBUG("SD_MMC Card Type: "); + if (cardType == CARD_MMC) { + LOG_DEBUG("MMC"); + } else if (cardType == CARD_SD) { + LOG_DEBUG("SDSC"); + } else if (cardType == CARD_SDHC) { + LOG_DEBUG("SDHC"); + } else { + LOG_DEBUG("UNKNOWN"); + } - uint64_t cardSize = SD.cardSize() / (1024 * 1024); - LOG_DEBUG("SD Card Size: %lu MB", (uint32_t)cardSize); - LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); - LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); + uint64_t cardSize = SD.cardSize() / (1024 * 1024); + LOG_DEBUG("SD Card Size: %lu MB", (uint32_t)cardSize); + LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); + LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); #endif } \ No newline at end of file diff --git a/src/Fusion/FusionAhrs.c b/src/Fusion/FusionAhrs.c index d6c1d0215..c5ac6016a 100644 --- a/src/Fusion/FusionAhrs.c +++ b/src/Fusion/FusionAhrs.c @@ -43,18 +43,17 @@ static inline int Clamp(const int value, const int min, const int max); * @brief Initialises the AHRS algorithm structure. * @param ahrs AHRS algorithm structure. */ -void FusionAhrsInitialise(FusionAhrs *const ahrs) -{ - const FusionAhrsSettings settings = { - .convention = FusionConventionNwu, - .gain = 0.5f, - .gyroscopeRange = 0.0f, - .accelerationRejection = 90.0f, - .magneticRejection = 90.0f, - .recoveryTriggerPeriod = 0, - }; - FusionAhrsSetSettings(ahrs, &settings); - FusionAhrsReset(ahrs); +void FusionAhrsInitialise(FusionAhrs *const ahrs) { + const FusionAhrsSettings settings = { + .convention = FusionConventionNwu, + .gain = 0.5f, + .gyroscopeRange = 0.0f, + .accelerationRejection = 90.0f, + .magneticRejection = 90.0f, + .recoveryTriggerPeriod = 0, + }; + FusionAhrsSetSettings(ahrs, &settings); + FusionAhrsReset(ahrs); } /** @@ -62,21 +61,20 @@ void FusionAhrsInitialise(FusionAhrs *const ahrs) * algorithm while maintaining the current settings. * @param ahrs AHRS algorithm structure. */ -void FusionAhrsReset(FusionAhrs *const ahrs) -{ - ahrs->quaternion = FUSION_IDENTITY_QUATERNION; - ahrs->accelerometer = FUSION_VECTOR_ZERO; - ahrs->initialising = true; - ahrs->rampedGain = INITIAL_GAIN; - ahrs->angularRateRecovery = false; - ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO; - ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO; - ahrs->accelerometerIgnored = false; - ahrs->accelerationRecoveryTrigger = 0; - ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - ahrs->magnetometerIgnored = false; - ahrs->magneticRecoveryTrigger = 0; - ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; +void FusionAhrsReset(FusionAhrs *const ahrs) { + ahrs->quaternion = FUSION_IDENTITY_QUATERNION; + ahrs->accelerometer = FUSION_VECTOR_ZERO; + ahrs->initialising = true; + ahrs->rampedGain = INITIAL_GAIN; + ahrs->angularRateRecovery = false; + ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO; + ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO; + ahrs->accelerometerIgnored = false; + ahrs->accelerationRecoveryTrigger = 0; + ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + ahrs->magnetometerIgnored = false; + ahrs->magneticRecoveryTrigger = 0; + ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; } /** @@ -84,28 +82,25 @@ void FusionAhrsReset(FusionAhrs *const ahrs) * @param ahrs AHRS algorithm structure. * @param settings Settings. */ -void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings) -{ - ahrs->settings.convention = settings->convention; - ahrs->settings.gain = settings->gain; - ahrs->settings.gyroscopeRange = settings->gyroscopeRange == 0.0f ? FLT_MAX : 0.98f * settings->gyroscopeRange; - ahrs->settings.accelerationRejection = settings->accelerationRejection == 0.0f - ? FLT_MAX - : powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2); - ahrs->settings.magneticRejection = - settings->magneticRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2); - ahrs->settings.recoveryTriggerPeriod = settings->recoveryTriggerPeriod; - ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - if ((settings->gain == 0.0f) || - (settings->recoveryTriggerPeriod == 0)) { // disable acceleration and magnetic rejection features if gain is zero - ahrs->settings.accelerationRejection = FLT_MAX; - ahrs->settings.magneticRejection = FLT_MAX; - } - if (ahrs->initialising == false) { - ahrs->rampedGain = ahrs->settings.gain; - } - ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD; +void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings) { + ahrs->settings.convention = settings->convention; + ahrs->settings.gain = settings->gain; + ahrs->settings.gyroscopeRange = settings->gyroscopeRange == 0.0f ? FLT_MAX : 0.98f * settings->gyroscopeRange; + ahrs->settings.accelerationRejection = + settings->accelerationRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2); + ahrs->settings.magneticRejection = + settings->magneticRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2); + ahrs->settings.recoveryTriggerPeriod = settings->recoveryTriggerPeriod; + ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + if ((settings->gain == 0.0f) || (settings->recoveryTriggerPeriod == 0)) { // disable acceleration and magnetic rejection features if gain is zero + ahrs->settings.accelerationRejection = FLT_MAX; + ahrs->settings.magneticRejection = FLT_MAX; + } + if (ahrs->initialising == false) { + ahrs->rampedGain = ahrs->settings.gain; + } + ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD; } /** @@ -117,119 +112,113 @@ void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *con * @param magnetometer Magnetometer measurement in arbitrary units. * @param deltaTime Delta time in seconds. */ -void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, - const FusionVector magnetometer, const float deltaTime) -{ +void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, + const float deltaTime) { #define Q ahrs->quaternion.element - // Store accelerometer - ahrs->accelerometer = accelerometer; + // Store accelerometer + ahrs->accelerometer = accelerometer; - // Reinitialise if gyroscope range exceeded - if ((fabsf(gyroscope.axis.x) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.y) > ahrs->settings.gyroscopeRange) || - (fabsf(gyroscope.axis.z) > ahrs->settings.gyroscopeRange)) { - const FusionQuaternion quaternion = ahrs->quaternion; - FusionAhrsReset(ahrs); - ahrs->quaternion = quaternion; - ahrs->angularRateRecovery = true; + // Reinitialise if gyroscope range exceeded + if ((fabsf(gyroscope.axis.x) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.y) > ahrs->settings.gyroscopeRange) || + (fabsf(gyroscope.axis.z) > ahrs->settings.gyroscopeRange)) { + const FusionQuaternion quaternion = ahrs->quaternion; + FusionAhrsReset(ahrs); + ahrs->quaternion = quaternion; + ahrs->angularRateRecovery = true; + } + + // Ramp down gain during initialisation + if (ahrs->initialising) { + ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime; + if ((ahrs->rampedGain < ahrs->settings.gain) || (ahrs->settings.gain == 0.0f)) { + ahrs->rampedGain = ahrs->settings.gain; + ahrs->initialising = false; + ahrs->angularRateRecovery = false; + } + } + + // Calculate direction of gravity indicated by algorithm + const FusionVector halfGravity = HalfGravity(ahrs); + + // Calculate accelerometer feedback + FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO; + ahrs->accelerometerIgnored = true; + if (FusionVectorIsZero(accelerometer) == false) { + + // Calculate accelerometer feedback scaled by 0.5 + ahrs->halfAccelerometerFeedback = Feedback(FusionVectorNormalise(accelerometer), halfGravity); + + // Don't ignore accelerometer if acceleration error below threshold + if (ahrs->initialising || ((FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection))) { + ahrs->accelerometerIgnored = false; + ahrs->accelerationRecoveryTrigger -= 9; + } else { + ahrs->accelerationRecoveryTrigger += 1; } - // Ramp down gain during initialisation - if (ahrs->initialising) { - ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime; - if ((ahrs->rampedGain < ahrs->settings.gain) || (ahrs->settings.gain == 0.0f)) { - ahrs->rampedGain = ahrs->settings.gain; - ahrs->initialising = false; - ahrs->angularRateRecovery = false; - } + // Don't ignore accelerometer during acceleration recovery + if (ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout) { + ahrs->accelerationRecoveryTimeout = 0; + ahrs->accelerometerIgnored = false; + } else { + ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + } + ahrs->accelerationRecoveryTrigger = Clamp(ahrs->accelerationRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); + + // Apply accelerometer feedback + if (ahrs->accelerometerIgnored == false) { + halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback; + } + } + + // Calculate magnetometer feedback + FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO; + ahrs->magnetometerIgnored = true; + if (FusionVectorIsZero(magnetometer) == false) { + + // Calculate direction of magnetic field indicated by algorithm + const FusionVector halfMagnetic = HalfMagnetic(ahrs); + + // Calculate magnetometer feedback scaled by 0.5 + ahrs->halfMagnetometerFeedback = Feedback(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfMagnetic); + + // Don't ignore magnetometer if magnetic error below threshold + if (ahrs->initialising || ((FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection))) { + ahrs->magnetometerIgnored = false; + ahrs->magneticRecoveryTrigger -= 9; + } else { + ahrs->magneticRecoveryTrigger += 1; } - // Calculate direction of gravity indicated by algorithm - const FusionVector halfGravity = HalfGravity(ahrs); - - // Calculate accelerometer feedback - FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO; - ahrs->accelerometerIgnored = true; - if (FusionVectorIsZero(accelerometer) == false) { - - // Calculate accelerometer feedback scaled by 0.5 - ahrs->halfAccelerometerFeedback = Feedback(FusionVectorNormalise(accelerometer), halfGravity); - - // Don't ignore accelerometer if acceleration error below threshold - if (ahrs->initialising || - ((FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection))) { - ahrs->accelerometerIgnored = false; - ahrs->accelerationRecoveryTrigger -= 9; - } else { - ahrs->accelerationRecoveryTrigger += 1; - } - - // Don't ignore accelerometer during acceleration recovery - if (ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout) { - ahrs->accelerationRecoveryTimeout = 0; - ahrs->accelerometerIgnored = false; - } else { - ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - } - ahrs->accelerationRecoveryTrigger = Clamp(ahrs->accelerationRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); - - // Apply accelerometer feedback - if (ahrs->accelerometerIgnored == false) { - halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback; - } + // Don't ignore magnetometer during magnetic recovery + if (ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout) { + ahrs->magneticRecoveryTimeout = 0; + ahrs->magnetometerIgnored = false; + } else { + ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; } + ahrs->magneticRecoveryTrigger = Clamp(ahrs->magneticRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); - // Calculate magnetometer feedback - FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO; - ahrs->magnetometerIgnored = true; - if (FusionVectorIsZero(magnetometer) == false) { - - // Calculate direction of magnetic field indicated by algorithm - const FusionVector halfMagnetic = HalfMagnetic(ahrs); - - // Calculate magnetometer feedback scaled by 0.5 - ahrs->halfMagnetometerFeedback = - Feedback(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfMagnetic); - - // Don't ignore magnetometer if magnetic error below threshold - if (ahrs->initialising || - ((FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection))) { - ahrs->magnetometerIgnored = false; - ahrs->magneticRecoveryTrigger -= 9; - } else { - ahrs->magneticRecoveryTrigger += 1; - } - - // Don't ignore magnetometer during magnetic recovery - if (ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout) { - ahrs->magneticRecoveryTimeout = 0; - ahrs->magnetometerIgnored = false; - } else { - ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - } - ahrs->magneticRecoveryTrigger = Clamp(ahrs->magneticRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); - - // Apply magnetometer feedback - if (ahrs->magnetometerIgnored == false) { - halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback; - } + // Apply magnetometer feedback + if (ahrs->magnetometerIgnored == false) { + halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback; } + } - // Convert gyroscope to radians per second scaled by 0.5 - const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f)); + // Convert gyroscope to radians per second scaled by 0.5 + const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f)); - // Apply feedback to gyroscope - const FusionVector adjustedHalfGyroscope = FusionVectorAdd( - halfGyroscope, - FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain)); + // Apply feedback to gyroscope + const FusionVector adjustedHalfGyroscope = FusionVectorAdd( + halfGyroscope, FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain)); - // Integrate rate of change of quaternion - ahrs->quaternion = FusionQuaternionAdd( - ahrs->quaternion, - FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime))); + // Integrate rate of change of quaternion + ahrs->quaternion = FusionQuaternionAdd( + ahrs->quaternion, FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime))); - // Normalise quaternion - ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion); + // Normalise quaternion + ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion); #undef Q } @@ -238,29 +227,28 @@ void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, cons * @param ahrs AHRS algorithm structure. * @return Direction of gravity scaled by 0.5. */ -static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) -{ +static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) { #define Q ahrs->quaternion.element - switch (ahrs->settings.convention) { - case FusionConventionNwu: - case FusionConventionEnu: { - const FusionVector halfGravity = {.axis = { - .x = Q.x * Q.z - Q.w * Q.y, - .y = Q.y * Q.z + Q.w * Q.x, - .z = Q.w * Q.w - 0.5f + Q.z * Q.z, - }}; // third column of transposed rotation matrix scaled by 0.5 - return halfGravity; - } - case FusionConventionNed: { - const FusionVector halfGravity = {.axis = { - .x = Q.w * Q.y - Q.x * Q.z, - .y = -1.0f * (Q.y * Q.z + Q.w * Q.x), - .z = 0.5f - Q.w * Q.w - Q.z * Q.z, - }}; // third column of transposed rotation matrix scaled by -0.5 - return halfGravity; - } - } - return FUSION_VECTOR_ZERO; // avoid compiler warning + switch (ahrs->settings.convention) { + case FusionConventionNwu: + case FusionConventionEnu: { + const FusionVector halfGravity = {.axis = { + .x = Q.x * Q.z - Q.w * Q.y, + .y = Q.y * Q.z + Q.w * Q.x, + .z = Q.w * Q.w - 0.5f + Q.z * Q.z, + }}; // third column of transposed rotation matrix scaled by 0.5 + return halfGravity; + } + case FusionConventionNed: { + const FusionVector halfGravity = {.axis = { + .x = Q.w * Q.y - Q.x * Q.z, + .y = -1.0f * (Q.y * Q.z + Q.w * Q.x), + .z = 0.5f - Q.w * Q.w - Q.z * Q.z, + }}; // third column of transposed rotation matrix scaled by -0.5 + return halfGravity; + } + } + return FUSION_VECTOR_ZERO; // avoid compiler warning #undef Q } @@ -269,36 +257,35 @@ static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) * @param ahrs AHRS algorithm structure. * @return Direction of the magnetic field scaled by 0.5. */ -static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) -{ +static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) { #define Q ahrs->quaternion.element - switch (ahrs->settings.convention) { - case FusionConventionNwu: { - const FusionVector halfMagnetic = {.axis = { - .x = Q.x * Q.y + Q.w * Q.z, - .y = Q.w * Q.w - 0.5f + Q.y * Q.y, - .z = Q.y * Q.z - Q.w * Q.x, - }}; // second column of transposed rotation matrix scaled by 0.5 - return halfMagnetic; - } - case FusionConventionEnu: { - const FusionVector halfMagnetic = {.axis = { - .x = 0.5f - Q.w * Q.w - Q.x * Q.x, - .y = Q.w * Q.z - Q.x * Q.y, - .z = -1.0f * (Q.x * Q.z + Q.w * Q.y), - }}; // first column of transposed rotation matrix scaled by -0.5 - return halfMagnetic; - } - case FusionConventionNed: { - const FusionVector halfMagnetic = {.axis = { - .x = -1.0f * (Q.x * Q.y + Q.w * Q.z), - .y = 0.5f - Q.w * Q.w - Q.y * Q.y, - .z = Q.w * Q.x - Q.y * Q.z, - }}; // second column of transposed rotation matrix scaled by -0.5 - return halfMagnetic; - } - } - return FUSION_VECTOR_ZERO; // avoid compiler warning + switch (ahrs->settings.convention) { + case FusionConventionNwu: { + const FusionVector halfMagnetic = {.axis = { + .x = Q.x * Q.y + Q.w * Q.z, + .y = Q.w * Q.w - 0.5f + Q.y * Q.y, + .z = Q.y * Q.z - Q.w * Q.x, + }}; // second column of transposed rotation matrix scaled by 0.5 + return halfMagnetic; + } + case FusionConventionEnu: { + const FusionVector halfMagnetic = {.axis = { + .x = 0.5f - Q.w * Q.w - Q.x * Q.x, + .y = Q.w * Q.z - Q.x * Q.y, + .z = -1.0f * (Q.x * Q.z + Q.w * Q.y), + }}; // first column of transposed rotation matrix scaled by -0.5 + return halfMagnetic; + } + case FusionConventionNed: { + const FusionVector halfMagnetic = {.axis = { + .x = -1.0f * (Q.x * Q.y + Q.w * Q.z), + .y = 0.5f - Q.w * Q.w - Q.y * Q.y, + .z = Q.w * Q.x - Q.y * Q.z, + }}; // second column of transposed rotation matrix scaled by -0.5 + return halfMagnetic; + } + } + return FUSION_VECTOR_ZERO; // avoid compiler warning #undef Q } @@ -308,12 +295,11 @@ static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) * @param reference Reference. * @return Feedback. */ -static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference) -{ - if (FusionVectorDotProduct(sensor, reference) < 0.0f) { // if error is >90 degrees - return FusionVectorNormalise(FusionVectorCrossProduct(sensor, reference)); - } - return FusionVectorCrossProduct(sensor, reference); +static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference) { + if (FusionVectorDotProduct(sensor, reference) < 0.0f) { // if error is >90 degrees + return FusionVectorNormalise(FusionVectorCrossProduct(sensor, reference)); + } + return FusionVectorCrossProduct(sensor, reference); } /** @@ -323,15 +309,14 @@ static inline FusionVector Feedback(const FusionVector sensor, const FusionVecto * @param max Maximum value. * @return Value limited to maximum and minimum. */ -static inline int Clamp(const int value, const int min, const int max) -{ - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; +static inline int Clamp(const int value, const int min, const int max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; } /** @@ -342,17 +327,15 @@ static inline int Clamp(const int value, const int min, const int max) * @param accelerometer Accelerometer measurement in g. * @param deltaTime Delta time in seconds. */ -void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, - const float deltaTime) -{ +void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime) { - // Update AHRS algorithm - FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime); + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime); - // Zero heading during initialisation - if (ahrs->initialising) { - FusionAhrsSetHeading(ahrs, 0.0f); - } + // Zero heading during initialisation + if (ahrs->initialising) { + FusionAhrsSetHeading(ahrs, 0.0f); + } } /** @@ -364,25 +347,24 @@ void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector g * @param heading Heading measurement in degrees. * @param deltaTime Delta time in seconds. */ -void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, - const float heading, const float deltaTime) -{ +void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, + const float deltaTime) { #define Q ahrs->quaternion.element - // Calculate roll - const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x); + // Calculate roll + const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x); - // Calculate magnetometer - const float headingRadians = FusionDegreesToRadians(heading); - const float sinHeadingRadians = sinf(headingRadians); - const FusionVector magnetometer = {.axis = { - .x = cosf(headingRadians), - .y = -1.0f * cosf(roll) * sinHeadingRadians, - .z = sinHeadingRadians * sinf(roll), - }}; + // Calculate magnetometer + const float headingRadians = FusionDegreesToRadians(heading); + const float sinHeadingRadians = sinf(headingRadians); + const FusionVector magnetometer = {.axis = { + .x = cosf(headingRadians), + .y = -1.0f * cosf(roll) * sinHeadingRadians, + .z = sinHeadingRadians * sinf(roll), + }}; - // Update AHRS algorithm - FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime); + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime); #undef Q } @@ -391,20 +373,14 @@ void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector * @param ahrs AHRS algorithm structure. * @return Quaternion describing the sensor relative to the Earth. */ -FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) -{ - return ahrs->quaternion; -} +FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) { return ahrs->quaternion; } /** * @brief Sets the quaternion describing the sensor relative to the Earth. * @param ahrs AHRS algorithm structure. * @param quaternion Quaternion describing the sensor relative to the Earth. */ -void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion) -{ - ahrs->quaternion = quaternion; -} +void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion) { ahrs->quaternion = quaternion; } /** * @brief Returns the linear acceleration measurement equal to the accelerometer @@ -412,28 +388,27 @@ void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quat * @param ahrs AHRS algorithm structure. * @return Linear acceleration measurement in g. */ -FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) -{ +FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) { #define Q ahrs->quaternion.element - // Calculate gravity in the sensor coordinate frame - const FusionVector gravity = {.axis = { - .x = 2.0f * (Q.x * Q.z - Q.w * Q.y), - .y = 2.0f * (Q.y * Q.z + Q.w * Q.x), - .z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), - }}; // third column of transposed rotation matrix + // Calculate gravity in the sensor coordinate frame + const FusionVector gravity = {.axis = { + .x = 2.0f * (Q.x * Q.z - Q.w * Q.y), + .y = 2.0f * (Q.y * Q.z + Q.w * Q.x), + .z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), + }}; // third column of transposed rotation matrix - // Remove gravity from accelerometer measurement - switch (ahrs->settings.convention) { - case FusionConventionNwu: - case FusionConventionEnu: { - return FusionVectorSubtract(ahrs->accelerometer, gravity); - } - case FusionConventionNed: { - return FusionVectorAdd(ahrs->accelerometer, gravity); - } - } - return FUSION_VECTOR_ZERO; // avoid compiler warning + // Remove gravity from accelerometer measurement + switch (ahrs->settings.convention) { + case FusionConventionNwu: + case FusionConventionEnu: { + return FusionVectorSubtract(ahrs->accelerometer, gravity); + } + case FusionConventionNed: { + return FusionVectorAdd(ahrs->accelerometer, gravity); + } + } + return FUSION_VECTOR_ZERO; // avoid compiler warning #undef Q } @@ -443,36 +418,35 @@ FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) * @param ahrs AHRS algorithm structure. * @return Earth acceleration measurement in g. */ -FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) -{ +FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) { #define Q ahrs->quaternion.element #define A ahrs->accelerometer.axis - // Calculate accelerometer measurement in the Earth coordinate frame - const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations - const float qwqx = Q.w * Q.x; - const float qwqy = Q.w * Q.y; - const float qwqz = Q.w * Q.z; - const float qxqy = Q.x * Q.y; - const float qxqz = Q.x * Q.z; - const float qyqz = Q.y * Q.z; - FusionVector accelerometer = {.axis = { - .x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), - .y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), - .z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z), - }}; // rotation matrix multiplied with the accelerometer + // Calculate accelerometer measurement in the Earth coordinate frame + const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations + const float qwqx = Q.w * Q.x; + const float qwqy = Q.w * Q.y; + const float qwqz = Q.w * Q.z; + const float qxqy = Q.x * Q.y; + const float qxqz = Q.x * Q.z; + const float qyqz = Q.y * Q.z; + FusionVector accelerometer = {.axis = { + .x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), + .y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), + .z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z), + }}; // rotation matrix multiplied with the accelerometer - // Remove gravity from accelerometer measurement - switch (ahrs->settings.convention) { - case FusionConventionNwu: - case FusionConventionEnu: - accelerometer.axis.z -= 1.0f; - break; - case FusionConventionNed: - accelerometer.axis.z += 1.0f; - break; - } - return accelerometer; + // Remove gravity from accelerometer measurement + switch (ahrs->settings.convention) { + case FusionConventionNwu: + case FusionConventionEnu: + accelerometer.axis.z -= 1.0f; + break; + case FusionConventionNed: + accelerometer.axis.z += 1.0f; + break; + } + return accelerometer; #undef Q #undef A } @@ -482,22 +456,18 @@ FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) * @param ahrs AHRS algorithm structure. * @return AHRS algorithm internal states. */ -FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs) -{ - const FusionAhrsInternalStates internalStates = { - .accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))), - .accelerometerIgnored = ahrs->accelerometerIgnored, - .accelerationRecoveryTrigger = - ahrs->settings.recoveryTriggerPeriod == 0 - ? 0.0f - : (float)ahrs->accelerationRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, - .magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))), - .magnetometerIgnored = ahrs->magnetometerIgnored, - .magneticRecoveryTrigger = ahrs->settings.recoveryTriggerPeriod == 0 - ? 0.0f - : (float)ahrs->magneticRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, - }; - return internalStates; +FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs) { + const FusionAhrsInternalStates internalStates = { + .accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))), + .accelerometerIgnored = ahrs->accelerometerIgnored, + .accelerationRecoveryTrigger = + ahrs->settings.recoveryTriggerPeriod == 0 ? 0.0f : (float)ahrs->accelerationRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, + .magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))), + .magnetometerIgnored = ahrs->magnetometerIgnored, + .magneticRecoveryTrigger = + ahrs->settings.recoveryTriggerPeriod == 0 ? 0.0f : (float)ahrs->magneticRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, + }; + return internalStates; } /** @@ -505,15 +475,14 @@ FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahr * @param ahrs AHRS algorithm structure. * @return AHRS algorithm flags. */ -FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) -{ - const FusionAhrsFlags flags = { - .initialising = ahrs->initialising, - .angularRateRecovery = ahrs->angularRateRecovery, - .accelerationRecovery = ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout, - .magneticRecovery = ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout, - }; - return flags; +FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) { + const FusionAhrsFlags flags = { + .initialising = ahrs->initialising, + .angularRateRecovery = ahrs->angularRateRecovery, + .accelerationRecovery = ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout, + .magneticRecovery = ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout, + }; + return flags; } /** @@ -523,18 +492,17 @@ FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) * @param ahrs AHRS algorithm structure. * @param heading Heading angle in degrees. */ -void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) -{ +void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) { #define Q ahrs->quaternion.element - const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z); - const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading)); - const FusionQuaternion rotation = {.element = { - .w = cosf(halfYawMinusHeading), - .x = 0.0f, - .y = 0.0f, - .z = -1.0f * sinf(halfYawMinusHeading), - }}; - ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion); + const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z); + const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading)); + const FusionQuaternion rotation = {.element = { + .w = cosf(halfYawMinusHeading), + .x = 0.0f, + .y = 0.0f, + .z = -1.0f * sinf(halfYawMinusHeading), + }}; + ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion); #undef Q } diff --git a/src/Fusion/FusionAhrs.h b/src/Fusion/FusionAhrs.h index aa2326e43..aa4e40e71 100644 --- a/src/Fusion/FusionAhrs.h +++ b/src/Fusion/FusionAhrs.h @@ -22,12 +22,12 @@ * @brief AHRS algorithm settings. */ typedef struct { - FusionConvention convention; - float gain; - float gyroscopeRange; - float accelerationRejection; - float magneticRejection; - unsigned int recoveryTriggerPeriod; + FusionConvention convention; + float gain; + float gyroscopeRange; + float accelerationRejection; + float magneticRejection; + unsigned int recoveryTriggerPeriod; } FusionAhrsSettings; /** @@ -35,43 +35,43 @@ typedef struct { * must not be accessed by the application. */ typedef struct { - FusionAhrsSettings settings; - FusionQuaternion quaternion; - FusionVector accelerometer; - bool initialising; - float rampedGain; - float rampedGainStep; - bool angularRateRecovery; - FusionVector halfAccelerometerFeedback; - FusionVector halfMagnetometerFeedback; - bool accelerometerIgnored; - int accelerationRecoveryTrigger; - int accelerationRecoveryTimeout; - bool magnetometerIgnored; - int magneticRecoveryTrigger; - int magneticRecoveryTimeout; + FusionAhrsSettings settings; + FusionQuaternion quaternion; + FusionVector accelerometer; + bool initialising; + float rampedGain; + float rampedGainStep; + bool angularRateRecovery; + FusionVector halfAccelerometerFeedback; + FusionVector halfMagnetometerFeedback; + bool accelerometerIgnored; + int accelerationRecoveryTrigger; + int accelerationRecoveryTimeout; + bool magnetometerIgnored; + int magneticRecoveryTrigger; + int magneticRecoveryTimeout; } FusionAhrs; /** * @brief AHRS algorithm internal states. */ typedef struct { - float accelerationError; - bool accelerometerIgnored; - float accelerationRecoveryTrigger; - float magneticError; - bool magnetometerIgnored; - float magneticRecoveryTrigger; + float accelerationError; + bool accelerometerIgnored; + float accelerationRecoveryTrigger; + float magneticError; + bool magnetometerIgnored; + float magneticRecoveryTrigger; } FusionAhrsInternalStates; /** * @brief AHRS algorithm flags. */ typedef struct { - bool initialising; - bool angularRateRecovery; - bool accelerationRecovery; - bool magneticRecovery; + bool initialising; + bool angularRateRecovery; + bool accelerationRecovery; + bool magneticRecovery; } FusionAhrsFlags; //------------------------------------------------------------------------------ @@ -83,14 +83,13 @@ void FusionAhrsReset(FusionAhrs *const ahrs); void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings); -void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, - const FusionVector magnetometer, const float deltaTime); +void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, + const float deltaTime); -void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, - const float deltaTime); +void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime); -void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, - const float heading, const float deltaTime); +void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, + const float deltaTime); FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs); diff --git a/src/Fusion/FusionAxes.h b/src/Fusion/FusionAxes.h index 9673c88ff..20b41e129 100644 --- a/src/Fusion/FusionAxes.h +++ b/src/Fusion/FusionAxes.h @@ -22,30 +22,30 @@ * then alignment is +Y-X+Z. */ typedef enum { - FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */ - FusionAxesAlignmentPXNZPY, /* +X-Z+Y */ - FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */ - FusionAxesAlignmentPXPZNY, /* +X+Z-Y */ - FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */ - FusionAxesAlignmentNXPZPY, /* -X+Z+Y */ - FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */ - FusionAxesAlignmentNXNZNY, /* -X-Z-Y */ - FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */ - FusionAxesAlignmentPYNZNX, /* +Y-Z-X */ - FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */ - FusionAxesAlignmentPYPZPX, /* +Y+Z+X */ - FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */ - FusionAxesAlignmentNYNZPX, /* -Y-Z+X */ - FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */ - FusionAxesAlignmentNYPZNX, /* -Y+Z-X */ - FusionAxesAlignmentPZPYNX, /* +Z+Y-X */ - FusionAxesAlignmentPZPXPY, /* +Z+X+Y */ - FusionAxesAlignmentPZNYPX, /* +Z-Y+X */ - FusionAxesAlignmentPZNXNY, /* +Z-X-Y */ - FusionAxesAlignmentNZPYPX, /* -Z+Y+X */ - FusionAxesAlignmentNZNXPY, /* -Z-X+Y */ - FusionAxesAlignmentNZNYNX, /* -Z-Y-X */ - FusionAxesAlignmentNZPXNY, /* -Z+X-Y */ + FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */ + FusionAxesAlignmentPXNZPY, /* +X-Z+Y */ + FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */ + FusionAxesAlignmentPXPZNY, /* +X+Z-Y */ + FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */ + FusionAxesAlignmentNXPZPY, /* -X+Z+Y */ + FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */ + FusionAxesAlignmentNXNZNY, /* -X-Z-Y */ + FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */ + FusionAxesAlignmentPYNZNX, /* +Y-Z-X */ + FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */ + FusionAxesAlignmentPYPZPX, /* +Y+Z+X */ + FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */ + FusionAxesAlignmentNYNZPX, /* -Y-Z+X */ + FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */ + FusionAxesAlignmentNYPZNX, /* -Y+Z-X */ + FusionAxesAlignmentPZPYNX, /* +Z+Y-X */ + FusionAxesAlignmentPZPXPY, /* +Z+X+Y */ + FusionAxesAlignmentPZNYPX, /* +Z-Y+X */ + FusionAxesAlignmentPZNXNY, /* +Z-X-Y */ + FusionAxesAlignmentNZPYPX, /* -Z+Y+X */ + FusionAxesAlignmentNZNXPY, /* -Z-X+Y */ + FusionAxesAlignmentNZNYNX, /* -Z-Y-X */ + FusionAxesAlignmentNZPXNY, /* -Z+X-Y */ } FusionAxesAlignment; //------------------------------------------------------------------------------ @@ -57,129 +57,128 @@ typedef enum { * @param alignment Axes alignment. * @return Sensor axes aligned with the body axes. */ -static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment) -{ - FusionVector result; - switch (alignment) { - case FusionAxesAlignmentPXPYPZ: - break; - case FusionAxesAlignmentPXNZPY: - result.axis.x = +sensor.axis.x; - result.axis.y = -sensor.axis.z; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentPXNYNZ: - result.axis.x = +sensor.axis.x; - result.axis.y = -sensor.axis.y; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentPXPZNY: - result.axis.x = +sensor.axis.x; - result.axis.y = +sensor.axis.z; - result.axis.z = -sensor.axis.y; - return result; - case FusionAxesAlignmentNXPYNZ: - result.axis.x = -sensor.axis.x; - result.axis.y = +sensor.axis.y; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentNXPZPY: - result.axis.x = -sensor.axis.x; - result.axis.y = +sensor.axis.z; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentNXNYPZ: - result.axis.x = -sensor.axis.x; - result.axis.y = -sensor.axis.y; - result.axis.z = +sensor.axis.z; - return result; - case FusionAxesAlignmentNXNZNY: - result.axis.x = -sensor.axis.x; - result.axis.y = -sensor.axis.z; - result.axis.z = -sensor.axis.y; - return result; - case FusionAxesAlignmentPYNXPZ: - result.axis.x = +sensor.axis.y; - result.axis.y = -sensor.axis.x; - result.axis.z = +sensor.axis.z; - return result; - case FusionAxesAlignmentPYNZNX: - result.axis.x = +sensor.axis.y; - result.axis.y = -sensor.axis.z; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentPYPXNZ: - result.axis.x = +sensor.axis.y; - result.axis.y = +sensor.axis.x; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentPYPZPX: - result.axis.x = +sensor.axis.y; - result.axis.y = +sensor.axis.z; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentNYPXPZ: - result.axis.x = -sensor.axis.y; - result.axis.y = +sensor.axis.x; - result.axis.z = +sensor.axis.z; - return result; - case FusionAxesAlignmentNYNZPX: - result.axis.x = -sensor.axis.y; - result.axis.y = -sensor.axis.z; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentNYNXNZ: - result.axis.x = -sensor.axis.y; - result.axis.y = -sensor.axis.x; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentNYPZNX: - result.axis.x = -sensor.axis.y; - result.axis.y = +sensor.axis.z; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentPZPYNX: - result.axis.x = +sensor.axis.z; - result.axis.y = +sensor.axis.y; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentPZPXPY: - result.axis.x = +sensor.axis.z; - result.axis.y = +sensor.axis.x; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentPZNYPX: - result.axis.x = +sensor.axis.z; - result.axis.y = -sensor.axis.y; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentPZNXNY: - result.axis.x = +sensor.axis.z; - result.axis.y = -sensor.axis.x; - result.axis.z = -sensor.axis.y; - return result; - case FusionAxesAlignmentNZPYPX: - result.axis.x = -sensor.axis.z; - result.axis.y = +sensor.axis.y; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentNZNXPY: - result.axis.x = -sensor.axis.z; - result.axis.y = -sensor.axis.x; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentNZNYNX: - result.axis.x = -sensor.axis.z; - result.axis.y = -sensor.axis.y; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentNZPXNY: - result.axis.x = -sensor.axis.z; - result.axis.y = +sensor.axis.x; - result.axis.z = -sensor.axis.y; - return result; - } - return sensor; // avoid compiler warning +static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment) { + FusionVector result; + switch (alignment) { + case FusionAxesAlignmentPXPYPZ: + break; + case FusionAxesAlignmentPXNZPY: + result.axis.x = +sensor.axis.x; + result.axis.y = -sensor.axis.z; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentPXNYNZ: + result.axis.x = +sensor.axis.x; + result.axis.y = -sensor.axis.y; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentPXPZNY: + result.axis.x = +sensor.axis.x; + result.axis.y = +sensor.axis.z; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentNXPYNZ: + result.axis.x = -sensor.axis.x; + result.axis.y = +sensor.axis.y; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentNXPZPY: + result.axis.x = -sensor.axis.x; + result.axis.y = +sensor.axis.z; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentNXNYPZ: + result.axis.x = -sensor.axis.x; + result.axis.y = -sensor.axis.y; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentNXNZNY: + result.axis.x = -sensor.axis.x; + result.axis.y = -sensor.axis.z; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentPYNXPZ: + result.axis.x = +sensor.axis.y; + result.axis.y = -sensor.axis.x; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentPYNZNX: + result.axis.x = +sensor.axis.y; + result.axis.y = -sensor.axis.z; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPYPXNZ: + result.axis.x = +sensor.axis.y; + result.axis.y = +sensor.axis.x; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentPYPZPX: + result.axis.x = +sensor.axis.y; + result.axis.y = +sensor.axis.z; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNYPXPZ: + result.axis.x = -sensor.axis.y; + result.axis.y = +sensor.axis.x; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentNYNZPX: + result.axis.x = -sensor.axis.y; + result.axis.y = -sensor.axis.z; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNYNXNZ: + result.axis.x = -sensor.axis.y; + result.axis.y = -sensor.axis.x; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentNYPZNX: + result.axis.x = -sensor.axis.y; + result.axis.y = +sensor.axis.z; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPZPYNX: + result.axis.x = +sensor.axis.z; + result.axis.y = +sensor.axis.y; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPZPXPY: + result.axis.x = +sensor.axis.z; + result.axis.y = +sensor.axis.x; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentPZNYPX: + result.axis.x = +sensor.axis.z; + result.axis.y = -sensor.axis.y; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentPZNXNY: + result.axis.x = +sensor.axis.z; + result.axis.y = -sensor.axis.x; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentNZPYPX: + result.axis.x = -sensor.axis.z; + result.axis.y = +sensor.axis.y; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNZNXPY: + result.axis.x = -sensor.axis.z; + result.axis.y = -sensor.axis.x; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentNZNYNX: + result.axis.x = -sensor.axis.z; + result.axis.y = -sensor.axis.y; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentNZPXNY: + result.axis.x = -sensor.axis.z; + result.axis.y = +sensor.axis.x; + result.axis.z = -sensor.axis.y; + return result; + } + return sensor; // avoid compiler warning } #endif diff --git a/src/Fusion/FusionCalibration.h b/src/Fusion/FusionCalibration.h index be7102b73..05fad4207 100644 --- a/src/Fusion/FusionCalibration.h +++ b/src/Fusion/FusionCalibration.h @@ -23,11 +23,9 @@ * @param offset Offset. * @return Calibrated measurement. */ -static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment, - const FusionVector sensitivity, const FusionVector offset) -{ - return FusionMatrixMultiplyVector(misalignment, - FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity)); +static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment, const FusionVector sensitivity, + const FusionVector offset) { + return FusionMatrixMultiplyVector(misalignment, FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity)); } /** @@ -38,9 +36,8 @@ static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibr * @return Calibrated measurement. */ static inline FusionVector FusionCalibrationMagnetic(const FusionVector uncalibrated, const FusionMatrix softIronMatrix, - const FusionVector hardIronOffset) -{ - return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset)); + const FusionVector hardIronOffset) { + return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset)); } #endif diff --git a/src/Fusion/FusionCompass.c b/src/Fusion/FusionCompass.c index 6a6f9591a..7e4ddc6ec 100644 --- a/src/Fusion/FusionCompass.c +++ b/src/Fusion/FusionCompass.c @@ -22,29 +22,27 @@ * @param magnetometer Magnetometer measurement in any calibrated units. * @return Heading angle in degrees. */ -float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, - const FusionVector magnetometer) -{ - switch (convention) { - case FusionConventionNwu: { - const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); - const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); - return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); - } - case FusionConventionEnu: { - const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); - const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); - const FusionVector east = FusionVectorMultiplyScalar(west, -1.0f); - return FusionRadiansToDegrees(atan2f(north.axis.x, east.axis.x)); - } - case FusionConventionNed: { - const FusionVector up = FusionVectorMultiplyScalar(accelerometer, -1.0f); - const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(up, magnetometer)); - const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, up)); - return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); - } - } - return 0; // avoid compiler warning +float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, const FusionVector magnetometer) { + switch (convention) { + case FusionConventionNwu: { + const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); + const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); + return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); + } + case FusionConventionEnu: { + const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); + const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); + const FusionVector east = FusionVectorMultiplyScalar(west, -1.0f); + return FusionRadiansToDegrees(atan2f(north.axis.x, east.axis.x)); + } + case FusionConventionNed: { + const FusionVector up = FusionVectorMultiplyScalar(accelerometer, -1.0f); + const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(up, magnetometer)); + const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, up)); + return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); + } + } + return 0; // avoid compiler warning } //------------------------------------------------------------------------------ diff --git a/src/Fusion/FusionCompass.h b/src/Fusion/FusionCompass.h index a3d0b466a..78326c064 100644 --- a/src/Fusion/FusionCompass.h +++ b/src/Fusion/FusionCompass.h @@ -17,8 +17,7 @@ //------------------------------------------------------------------------------ // Function declarations -float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, - const FusionVector magnetometer); +float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, const FusionVector magnetometer); #endif diff --git a/src/Fusion/FusionConvention.h b/src/Fusion/FusionConvention.h index 0b0d43adc..c1fb77f21 100644 --- a/src/Fusion/FusionConvention.h +++ b/src/Fusion/FusionConvention.h @@ -14,9 +14,9 @@ * @brief Earth axes convention. */ typedef enum { - FusionConventionNwu, /* North-West-Up */ - FusionConventionEnu, /* East-North-Up */ - FusionConventionNed, /* North-East-Down */ + FusionConventionNwu, /* North-West-Up */ + FusionConventionEnu, /* East-North-Up */ + FusionConventionNed, /* North-East-Down */ } FusionConvention; #endif diff --git a/src/Fusion/FusionMath.h b/src/Fusion/FusionMath.h index c3fc34b2d..e8d50a7dd 100644 --- a/src/Fusion/FusionMath.h +++ b/src/Fusion/FusionMath.h @@ -21,27 +21,27 @@ * @brief 3D vector. */ typedef union { - float array[3]; + float array[3]; - struct { - float x; - float y; - float z; - } axis; + struct { + float x; + float y; + float z; + } axis; } FusionVector; /** * @brief Quaternion. */ typedef union { - float array[4]; + float array[4]; - struct { - float w; - float x; - float y; - float z; - } element; + struct { + float w; + float x; + float y; + float z; + } element; } FusionQuaternion; /** @@ -49,19 +49,19 @@ typedef union { * See http://en.wikipedia.org/wiki/Row-major_order */ typedef union { - float array[3][3]; + float array[3][3]; - struct { - float xx; - float xy; - float xz; - float yx; - float yy; - float yz; - float zx; - float zy; - float zz; - } element; + struct { + float xx; + float xy; + float xz; + float yx; + float yy; + float yz; + float zx; + float zy; + float zz; + } element; } FusionMatrix; /** @@ -69,13 +69,13 @@ typedef union { * X, Y, and Z respectively. */ typedef union { - float array[3]; + float array[3]; - struct { - float roll; - float pitch; - float yaw; - } angle; + struct { + float roll; + float pitch; + float yaw; + } angle; } FusionEuler; /** @@ -124,20 +124,14 @@ typedef union { * @param degrees Degrees. * @return Radians. */ -static inline float FusionDegreesToRadians(const float degrees) -{ - return degrees * ((float)M_PI / 180.0f); -} +static inline float FusionDegreesToRadians(const float degrees) { return degrees * ((float)M_PI / 180.0f); } /** * @brief Converts radians to degrees. * @param radians Radians. * @return Degrees. */ -static inline float FusionRadiansToDegrees(const float radians) -{ - return radians * (180.0f / (float)M_PI); -} +static inline float FusionRadiansToDegrees(const float radians) { return radians * (180.0f / (float)M_PI); } //------------------------------------------------------------------------------ // Inline functions - Arc sine @@ -147,15 +141,14 @@ static inline float FusionRadiansToDegrees(const float radians) * @param value Value. * @return Arc sine of the value. */ -static inline float FusionAsin(const float value) -{ - if (value <= -1.0f) { - return (float)M_PI / -2.0f; - } - if (value >= 1.0f) { - return (float)M_PI / 2.0f; - } - return asinf(value); +static inline float FusionAsin(const float value) { + if (value <= -1.0f) { + return (float)M_PI / -2.0f; + } + if (value >= 1.0f) { + return (float)M_PI / 2.0f; + } + return asinf(value); } //------------------------------------------------------------------------------ @@ -169,17 +162,16 @@ static inline float FusionAsin(const float value) * @param x Operand. * @return Reciprocal of the square root of x. */ -static inline float FusionFastInverseSqrt(const float x) -{ +static inline float FusionFastInverseSqrt(const float x) { - typedef union { - float f; - int32_t i; - } Union32; + typedef union { + float f; + int32_t i; + } Union32; - Union32 union32 = {.f = x}; - union32.i = 0x5F1F1412 - (union32.i >> 1); - return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f); + Union32 union32 = {.f = x}; + union32.i = 0x5F1F1412 - (union32.i >> 1); + return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f); } #endif @@ -192,9 +184,8 @@ static inline float FusionFastInverseSqrt(const float x) * @param vector Vector. * @return True if the vector is zero. */ -static inline bool FusionVectorIsZero(const FusionVector vector) -{ - return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f); +static inline bool FusionVectorIsZero(const FusionVector vector) { + return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f); } /** @@ -203,14 +194,13 @@ static inline bool FusionVectorIsZero(const FusionVector vector) * @param vectorB Vector B. * @return Sum of two vectors. */ -static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) -{ - const FusionVector result = {.axis = { - .x = vectorA.axis.x + vectorB.axis.x, - .y = vectorA.axis.y + vectorB.axis.y, - .z = vectorA.axis.z + vectorB.axis.z, - }}; - return result; +static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) { + const FusionVector result = {.axis = { + .x = vectorA.axis.x + vectorB.axis.x, + .y = vectorA.axis.y + vectorB.axis.y, + .z = vectorA.axis.z + vectorB.axis.z, + }}; + return result; } /** @@ -219,14 +209,13 @@ static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const Fus * @param vectorB Vector B. * @return Vector B subtracted from vector A. */ -static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) -{ - const FusionVector result = {.axis = { - .x = vectorA.axis.x - vectorB.axis.x, - .y = vectorA.axis.y - vectorB.axis.y, - .z = vectorA.axis.z - vectorB.axis.z, - }}; - return result; +static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) { + const FusionVector result = {.axis = { + .x = vectorA.axis.x - vectorB.axis.x, + .y = vectorA.axis.y - vectorB.axis.y, + .z = vectorA.axis.z - vectorB.axis.z, + }}; + return result; } /** @@ -234,10 +223,7 @@ static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, cons * @param vector Vector. * @return Sum of the elements. */ -static inline float FusionVectorSum(const FusionVector vector) -{ - return vector.axis.x + vector.axis.y + vector.axis.z; -} +static inline float FusionVectorSum(const FusionVector vector) { return vector.axis.x + vector.axis.y + vector.axis.z; } /** * @brief Returns the multiplication of a vector by a scalar. @@ -245,14 +231,13 @@ static inline float FusionVectorSum(const FusionVector vector) * @param scalar Scalar. * @return Multiplication of a vector by a scalar. */ -static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) -{ - const FusionVector result = {.axis = { - .x = vector.axis.x * scalar, - .y = vector.axis.y * scalar, - .z = vector.axis.z * scalar, - }}; - return result; +static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) { + const FusionVector result = {.axis = { + .x = vector.axis.x * scalar, + .y = vector.axis.y * scalar, + .z = vector.axis.z * scalar, + }}; + return result; } /** @@ -261,14 +246,13 @@ static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, * @param vectorB Vector B. * @return Hadamard product. */ -static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) -{ - const FusionVector result = {.axis = { - .x = vectorA.axis.x * vectorB.axis.x, - .y = vectorA.axis.y * vectorB.axis.y, - .z = vectorA.axis.z * vectorB.axis.z, - }}; - return result; +static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) { + const FusionVector result = {.axis = { + .x = vectorA.axis.x * vectorB.axis.x, + .y = vectorA.axis.y * vectorB.axis.y, + .z = vectorA.axis.z * vectorB.axis.z, + }}; + return result; } /** @@ -277,16 +261,15 @@ static inline FusionVector FusionVectorHadamardProduct(const FusionVector vector * @param vectorB Vector B. * @return Cross product. */ -static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) -{ +static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) { #define A vectorA.axis #define B vectorB.axis - const FusionVector result = {.axis = { - .x = A.y * B.z - A.z * B.y, - .y = A.z * B.x - A.x * B.z, - .z = A.x * B.y - A.y * B.x, - }}; - return result; + const FusionVector result = {.axis = { + .x = A.y * B.z - A.z * B.y, + .y = A.z * B.x - A.x * B.z, + .z = A.x * B.y - A.y * B.x, + }}; + return result; #undef A #undef B } @@ -297,9 +280,8 @@ static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, * @param vectorB Vector B. * @return Dot product. */ -static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB) -{ - return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB)); +static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB) { + return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB)); } /** @@ -307,34 +289,27 @@ static inline float FusionVectorDotProduct(const FusionVector vectorA, const Fus * @param vector Vector. * @return Vector magnitude squared. */ -static inline float FusionVectorMagnitudeSquared(const FusionVector vector) -{ - return FusionVectorSum(FusionVectorHadamardProduct(vector, vector)); -} +static inline float FusionVectorMagnitudeSquared(const FusionVector vector) { return FusionVectorSum(FusionVectorHadamardProduct(vector, vector)); } /** * @brief Returns the vector magnitude. * @param vector Vector. * @return Vector magnitude. */ -static inline float FusionVectorMagnitude(const FusionVector vector) -{ - return sqrtf(FusionVectorMagnitudeSquared(vector)); -} +static inline float FusionVectorMagnitude(const FusionVector vector) { return sqrtf(FusionVectorMagnitudeSquared(vector)); } /** * @brief Returns the normalised vector. * @param vector Vector. * @return Normalised vector. */ -static inline FusionVector FusionVectorNormalise(const FusionVector vector) -{ +static inline FusionVector FusionVectorNormalise(const FusionVector vector) { #ifdef FUSION_USE_NORMAL_SQRT - const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector)); + const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector)); #else - const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector)); + const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector)); #endif - return FusionVectorMultiplyScalar(vector, magnitudeReciprocal); + return FusionVectorMultiplyScalar(vector, magnitudeReciprocal); } //------------------------------------------------------------------------------ @@ -346,15 +321,14 @@ static inline FusionVector FusionVectorNormalise(const FusionVector vector) * @param quaternionB Quaternion B. * @return Sum of two quaternions. */ -static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) -{ - const FusionQuaternion result = {.element = { - .w = quaternionA.element.w + quaternionB.element.w, - .x = quaternionA.element.x + quaternionB.element.x, - .y = quaternionA.element.y + quaternionB.element.y, - .z = quaternionA.element.z + quaternionB.element.z, - }}; - return result; +static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { + const FusionQuaternion result = {.element = { + .w = quaternionA.element.w + quaternionB.element.w, + .x = quaternionA.element.x + quaternionB.element.x, + .y = quaternionA.element.y + quaternionB.element.y, + .z = quaternionA.element.z + quaternionB.element.z, + }}; + return result; } /** @@ -363,17 +337,16 @@ static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quater * @param quaternionB Quaternion B (to be pre-multiplied). * @return Multiplication of two quaternions. */ -static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) -{ +static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { #define A quaternionA.element #define B quaternionB.element - const FusionQuaternion result = {.element = { - .w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z, - .x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y, - .y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x, - .z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w, - }}; - return result; + const FusionQuaternion result = {.element = { + .w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z, + .x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y, + .y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x, + .z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w, + }}; + return result; #undef A #undef B } @@ -387,17 +360,16 @@ static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion q * @param vector Vector. * @return Multiplication of a quaternion with a vector. */ -static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) -{ +static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) { #define Q quaternion.element #define V vector.axis - const FusionQuaternion result = {.element = { - .w = -Q.x * V.x - Q.y * V.y - Q.z * V.z, - .x = Q.w * V.x + Q.y * V.z - Q.z * V.y, - .y = Q.w * V.y - Q.x * V.z + Q.z * V.x, - .z = Q.w * V.z + Q.x * V.y - Q.y * V.x, - }}; - return result; + const FusionQuaternion result = {.element = { + .w = -Q.x * V.x - Q.y * V.y - Q.z * V.z, + .x = Q.w * V.x + Q.y * V.z - Q.z * V.y, + .y = Q.w * V.y - Q.x * V.z + Q.z * V.x, + .z = Q.w * V.z + Q.x * V.y - Q.y * V.x, + }}; + return result; #undef Q #undef V } @@ -407,21 +379,20 @@ static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuater * @param quaternion Quaternion. * @return Normalised quaternion. */ -static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion) -{ +static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion) { #define Q quaternion.element #ifdef FUSION_USE_NORMAL_SQRT - const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); + const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); #else - const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); + const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); #endif - const FusionQuaternion result = {.element = { - .w = Q.w * magnitudeReciprocal, - .x = Q.x * magnitudeReciprocal, - .y = Q.y * magnitudeReciprocal, - .z = Q.z * magnitudeReciprocal, - }}; - return result; + const FusionQuaternion result = {.element = { + .w = Q.w * magnitudeReciprocal, + .x = Q.x * magnitudeReciprocal, + .y = Q.y * magnitudeReciprocal, + .z = Q.z * magnitudeReciprocal, + }}; + return result; #undef Q } @@ -434,15 +405,14 @@ static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion * @param vector Vector. * @return Multiplication of a matrix with a vector. */ -static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) -{ +static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) { #define R matrix.element - const FusionVector result = {.axis = { - .x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z, - .y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z, - .z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z, - }}; - return result; + const FusionVector result = {.axis = { + .x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z, + .y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z, + .z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z, + }}; + return result; #undef R } @@ -454,28 +424,27 @@ static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, * @param quaternion Quaternion. * @return Rotation matrix. */ -static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion) -{ +static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion) { #define Q quaternion.element - const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations - const float qwqx = Q.w * Q.x; - const float qwqy = Q.w * Q.y; - const float qwqz = Q.w * Q.z; - const float qxqy = Q.x * Q.y; - const float qxqz = Q.x * Q.z; - const float qyqz = Q.y * Q.z; - const FusionMatrix matrix = {.element = { - .xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x), - .xy = 2.0f * (qxqy - qwqz), - .xz = 2.0f * (qxqz + qwqy), - .yx = 2.0f * (qxqy + qwqz), - .yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y), - .yz = 2.0f * (qyqz - qwqx), - .zx = 2.0f * (qxqz - qwqy), - .zy = 2.0f * (qyqz + qwqx), - .zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z), - }}; - return matrix; + const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations + const float qwqx = Q.w * Q.x; + const float qwqy = Q.w * Q.y; + const float qwqz = Q.w * Q.z; + const float qxqy = Q.x * Q.y; + const float qxqz = Q.x * Q.z; + const float qyqz = Q.y * Q.z; + const FusionMatrix matrix = {.element = { + .xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x), + .xy = 2.0f * (qxqy - qwqz), + .xz = 2.0f * (qxqz + qwqy), + .yx = 2.0f * (qxqy + qwqz), + .yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y), + .yz = 2.0f * (qyqz - qwqx), + .zx = 2.0f * (qxqz - qwqy), + .zy = 2.0f * (qyqz + qwqx), + .zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z), + }}; + return matrix; #undef Q } @@ -484,16 +453,15 @@ static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quate * @param quaternion Quaternion. * @return Euler angles in degrees. */ -static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) -{ +static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) { #define Q quaternion.element - const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations - const FusionEuler euler = {.angle = { - .roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)), - .pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))), - .yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)), - }}; - return euler; + const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations + const FusionEuler euler = {.angle = { + .roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)), + .pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))), + .yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)), + }}; + return euler; #undef Q } diff --git a/src/Fusion/FusionOffset.c b/src/Fusion/FusionOffset.c index d4334c874..4a0546a88 100644 --- a/src/Fusion/FusionOffset.c +++ b/src/Fusion/FusionOffset.c @@ -37,12 +37,11 @@ * @param offset Gyroscope offset algorithm structure. * @param sampleRate Sample rate in Hz. */ -void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate) -{ - offset->filterCoefficient = 2.0f * (float)M_PI * CUTOFF_FREQUENCY * (1.0f / (float)sampleRate); - offset->timeout = TIMEOUT * sampleRate; - offset->timer = 0; - offset->gyroscopeOffset = FUSION_VECTOR_ZERO; +void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate) { + offset->filterCoefficient = 2.0f * (float)M_PI * CUTOFF_FREQUENCY * (1.0f / (float)sampleRate); + offset->timeout = TIMEOUT * sampleRate; + offset->timer = 0; + offset->gyroscopeOffset = FUSION_VECTOR_ZERO; } /** @@ -52,28 +51,26 @@ void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampl * @param gyroscope Gyroscope measurement in degrees per second. * @return Corrected gyroscope measurement in degrees per second. */ -FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) -{ +FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) { - // Subtract offset from gyroscope measurement - gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset); + // Subtract offset from gyroscope measurement + gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset); - // Reset timer if gyroscope not stationary - if ((fabsf(gyroscope.axis.x) > THRESHOLD) || (fabsf(gyroscope.axis.y) > THRESHOLD) || (fabsf(gyroscope.axis.z) > THRESHOLD)) { - offset->timer = 0; - return gyroscope; - } - - // Increment timer while gyroscope stationary - if (offset->timer < offset->timeout) { - offset->timer++; - return gyroscope; - } - - // Adjust offset if timer has elapsed - offset->gyroscopeOffset = - FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient)); + // Reset timer if gyroscope not stationary + if ((fabsf(gyroscope.axis.x) > THRESHOLD) || (fabsf(gyroscope.axis.y) > THRESHOLD) || (fabsf(gyroscope.axis.z) > THRESHOLD)) { + offset->timer = 0; return gyroscope; + } + + // Increment timer while gyroscope stationary + if (offset->timer < offset->timeout) { + offset->timer++; + return gyroscope; + } + + // Adjust offset if timer has elapsed + offset->gyroscopeOffset = FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient)); + return gyroscope; } //------------------------------------------------------------------------------ diff --git a/src/Fusion/FusionOffset.h b/src/Fusion/FusionOffset.h index 51ae4a896..7ad945460 100644 --- a/src/Fusion/FusionOffset.h +++ b/src/Fusion/FusionOffset.h @@ -21,10 +21,10 @@ * internally and must not be accessed by the application. */ typedef struct { - float filterCoefficient; - unsigned int timeout; - unsigned int timer; - FusionVector gyroscopeOffset; + float filterCoefficient; + unsigned int timeout; + unsigned int timer; + FusionVector gyroscopeOffset; } FusionOffset; //------------------------------------------------------------------------------ diff --git a/src/GPSStatus.h b/src/GPSStatus.h index a1a9f2c56..162be8b5a 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -4,136 +4,124 @@ #include "configuration.h" #include -namespace meshtastic -{ +namespace meshtastic { /// Describes the state of the GPS system. -class GPSStatus : public Status -{ +class GPSStatus : public Status { - private: - CallbackObserver statusObserver = - CallbackObserver(this, &GPSStatus::updateStatus); +private: + CallbackObserver statusObserver = CallbackObserver(this, &GPSStatus::updateStatus); - bool hasLock = false; // default to false, until we complete our first read - bool isConnected = false; // Do we have a GPS we are talking to + bool hasLock = false; // default to false, until we complete our first read + bool isConnected = false; // Do we have a GPS we are talking to - bool isPowerSaving = false; // Are we in power saving state + bool isPowerSaving = false; // Are we in power saving state - meshtastic_Position p = meshtastic_Position_init_default; + meshtastic_Position p = meshtastic_Position_init_default; - /// Time of last valid GPS fix (millis since boot) - uint32_t lastFixMillis = 0; + /// Time of last valid GPS fix (millis since boot) + uint32_t lastFixMillis = 0; - public: - GPSStatus() { statusType = STATUS_TYPE_GPS; } +public: + GPSStatus() { statusType = STATUS_TYPE_GPS; } - // preferred method - GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos) : Status() - { - this->hasLock = hasLock; - this->isConnected = isConnected; - this->isPowerSaving = isPowerSaving; + // preferred method + GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos) : Status() { + this->hasLock = hasLock; + this->isConnected = isConnected; + this->isPowerSaving = isPowerSaving; - // all-in-one struct copy - this->p = pos; + // all-in-one struct copy + this->p = pos; + } + + GPSStatus(const GPSStatus &); + GPSStatus &operator=(const GPSStatus &); + + void observe(Observable *source) { statusObserver.observe(source); } + + bool getHasLock() const { return hasLock; } + + bool getIsConnected() const { return isConnected; } + + bool getIsPowerSaving() const { return isPowerSaving; } + + int32_t getLatitude() const { + if (config.position.fixed_position) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.latitude_i; + } else { + return p.latitude_i; } + } - GPSStatus(const GPSStatus &); - GPSStatus &operator=(const GPSStatus &); - - void observe(Observable *source) { statusObserver.observe(source); } - - bool getHasLock() const { return hasLock; } - - bool getIsConnected() const { return isConnected; } - - bool getIsPowerSaving() const { return isPowerSaving; } - - int32_t getLatitude() const - { - if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.latitude_i; - } else { - return p.latitude_i; - } + int32_t getLongitude() const { + if (config.position.fixed_position) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.longitude_i; + } else { + return p.longitude_i; } + } - int32_t getLongitude() const - { - if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.longitude_i; - } else { - return p.longitude_i; - } + int32_t getAltitude() const { + if (config.position.fixed_position) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.altitude; + } else { + return p.altitude; } + } - int32_t getAltitude() const - { - if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.altitude; - } else { - return p.altitude; - } - } + uint32_t getDOP() const { return p.PDOP; } - uint32_t getDOP() const { return p.PDOP; } + uint32_t getHeading() const { return p.ground_track; } - uint32_t getHeading() const { return p.ground_track; } + uint32_t getNumSatellites() const { return p.sats_in_view; } - uint32_t getNumSatellites() const { return p.sats_in_view; } + /// Return millis() when the last GPS fix occurred (0 = never) + uint32_t getLastFixMillis() const { return lastFixMillis; } - /// Return millis() when the last GPS fix occurred (0 = never) - uint32_t getLastFixMillis() const { return lastFixMillis; } - - bool matches(const GPSStatus *newStatus) const - { + bool matches(const GPSStatus *newStatus) const { #ifdef GPS_DEBUG - LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp); + LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp); #endif - return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || - newStatus->isPowerSaving != isPowerSaving || newStatus->p.latitude_i != p.latitude_i || - newStatus->p.longitude_i != p.longitude_i || newStatus->p.altitude != p.altitude || - newStatus->p.altitude_hae != p.altitude_hae || newStatus->p.PDOP != p.PDOP || - newStatus->p.ground_track != p.ground_track || newStatus->p.ground_speed != p.ground_speed || - newStatus->p.sats_in_view != p.sats_in_view); + return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || newStatus->isPowerSaving != isPowerSaving || + newStatus->p.latitude_i != p.latitude_i || newStatus->p.longitude_i != p.longitude_i || newStatus->p.altitude != p.altitude || + newStatus->p.altitude_hae != p.altitude_hae || newStatus->p.PDOP != p.PDOP || newStatus->p.ground_track != p.ground_track || + newStatus->p.ground_speed != p.ground_speed || newStatus->p.sats_in_view != p.sats_in_view); + } + + int updateStatus(const GPSStatus *newStatus) { + // Only update the status if values have actually changed + bool isDirty = matches(newStatus); + + if (isDirty && p.timestamp && (newStatus->p.timestamp == p.timestamp)) { + // We can NEVER be in two locations at the same time! (also PR #886) + LOG_ERROR("BUG: Positional timestamp unchanged from prev solution"); } - int updateStatus(const GPSStatus *newStatus) - { - // Only update the status if values have actually changed - bool isDirty = matches(newStatus); + initialized = true; + hasLock = newStatus->hasLock; + isConnected = newStatus->isConnected; - if (isDirty && p.timestamp && (newStatus->p.timestamp == p.timestamp)) { - // We can NEVER be in two locations at the same time! (also PR #886) - LOG_ERROR("BUG: Positional timestamp unchanged from prev solution"); - } + p = newStatus->p; - initialized = true; - hasLock = newStatus->hasLock; - isConnected = newStatus->isConnected; + if (isDirty) { + if (hasLock) { + // Record time of last valid GPS fix + lastFixMillis = millis(); - p = newStatus->p; - - if (isDirty) { - if (hasLock) { - // Record time of last valid GPS fix - lastFixMillis = millis(); - - // In debug logs, identify position by @timestamp:stage (stage 3 = notify) - LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, - p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, - p.ground_speed * 1e-2, p.sats_in_view); - } else { - LOG_DEBUG("No GPS lock"); - } - onNewStatus.notifyObservers(this); - } - return 0; + // In debug logs, identify position by @timestamp:stage (stage 3 = notify) + LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, p.latitude_i * 1e-7, + p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, p.ground_speed * 1e-2, p.sats_in_view); + } else { + LOG_DEBUG("No GPS lock"); + } + onNewStatus.notifyObservers(this); } + return 0; + } }; } // namespace meshtastic diff --git a/src/GpioLogic.cpp b/src/GpioLogic.cpp index ecdf514e4..1bb34ca78 100644 --- a/src/GpioLogic.cpp +++ b/src/GpioLogic.cpp @@ -1,102 +1,90 @@ #include "GpioLogic.h" #include -void GpioVirtPin::set(bool value) -{ - if (value != this->value) { - this->value = value ? PinState::On : PinState::Off; - if (dependentPin) - dependentPin->update(); - } +void GpioVirtPin::set(bool value) { + if (value != this->value) { + this->value = value ? PinState::On : PinState::Off; + if (dependentPin) + dependentPin->update(); + } } -void GpioHwPin::set(bool value) -{ - pinMode(num, OUTPUT); - digitalWrite(num, value); +void GpioHwPin::set(bool value) { + pinMode(num, OUTPUT); + digitalWrite(num, value); } GpioTransformer::GpioTransformer(GpioPin *outPin) : outPin(outPin) {} -void GpioTransformer::set(bool value) -{ - outPin->set(value); -} +void GpioTransformer::set(bool value) { outPin->set(value); } -GpioUnaryTransformer::GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioTransformer(outPin), inPin(inPin) -{ - assert(!inPin->dependentPin); // We only allow one dependent pin - inPin->dependentPin = this; +GpioUnaryTransformer::GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioTransformer(outPin), inPin(inPin) { + assert(!inPin->dependentPin); // We only allow one dependent pin + inPin->dependentPin = this; - // Don't update at construction time, because various GpioPins might be global constructor based not yet initied because - // order of operations for global constructors is not defined. - // update(); + // Don't update at construction time, because various GpioPins might be global constructor based not yet initied + // because order of operations for global constructors is not defined. update(); } /** * Update the output pin based on the current state of the input pin. */ -void GpioUnaryTransformer::update() -{ - auto p = inPin->get(); - if (p == GpioVirtPin::PinState::Unset) - return; // Not yet fully initialized +void GpioUnaryTransformer::update() { + auto p = inPin->get(); + if (p == GpioVirtPin::PinState::Unset) + return; // Not yet fully initialized - set(p); + set(p); } /** * Update the output pin based on the current state of the input pin. */ -void GpioNotTransformer::update() -{ - auto p = inPin->get(); - if (p == GpioVirtPin::PinState::Unset) - return; // Not yet fully initialized +void GpioNotTransformer::update() { + auto p = inPin->get(); + if (p == GpioVirtPin::PinState::Unset) + return; // Not yet fully initialized - set(!p); + set(!p); } GpioBinaryTransformer::GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation) - : GpioTransformer(outPin), inPin1(inPin1), inPin2(inPin2), operation(operation) -{ - assert(!inPin1->dependentPin); // We only allow one dependent pin - inPin1->dependentPin = this; - assert(!inPin2->dependentPin); // We only allow one dependent pin - inPin2->dependentPin = this; + : GpioTransformer(outPin), inPin1(inPin1), inPin2(inPin2), operation(operation) { + assert(!inPin1->dependentPin); // We only allow one dependent pin + inPin1->dependentPin = this; + assert(!inPin2->dependentPin); // We only allow one dependent pin + inPin2->dependentPin = this; - // Don't update at construction time, because various GpioPins might be global constructor based not yet initiated because - // order of operations for global constructors is not defined. - // update(); + // Don't update at construction time, because various GpioPins might be global constructor based not yet initiated + // because order of operations for global constructors is not defined. update(); } -void GpioBinaryTransformer::update() -{ - auto p1 = inPin1->get(), p2 = inPin2->get(); - GpioVirtPin::PinState newValue = GpioVirtPin::PinState::Unset; +void GpioBinaryTransformer::update() { + auto p1 = inPin1->get(), p2 = inPin2->get(); + GpioVirtPin::PinState newValue = GpioVirtPin::PinState::Unset; - if (p1 == GpioVirtPin::PinState::Unset) - newValue = p2; // Not yet fully initialized - else if (p2 == GpioVirtPin::PinState::Unset) - newValue = p1; // Not yet fully initialized + if (p1 == GpioVirtPin::PinState::Unset) + newValue = p2; // Not yet fully initialized + else if (p2 == GpioVirtPin::PinState::Unset) + newValue = p1; // Not yet fully initialized - // If we've already found our value just use it, otherwise need to do the operation - if (newValue == GpioVirtPin::PinState::Unset) { - switch (operation) { - case And: - newValue = (GpioVirtPin::PinState)(p1 && p2); - break; - case Or: - newValue = (GpioVirtPin::PinState)(p1 || p2); - break; - case Xor: - newValue = (GpioVirtPin::PinState)(p1 != p2); - break; - default: - assert(false); - } + // If we've already found our value just use it, otherwise need to do the operation + if (newValue == GpioVirtPin::PinState::Unset) { + switch (operation) { + case And: + newValue = (GpioVirtPin::PinState)(p1 && p2); + break; + case Or: + newValue = (GpioVirtPin::PinState)(p1 || p2); + break; + case Xor: + newValue = (GpioVirtPin::PinState)(p1 != p2); + break; + default: + assert(false); } - set(newValue); + } + set(newValue); } GpioSplitter::GpioSplitter(GpioPin *outPin1, GpioPin *outPin2) : outPin1(outPin1), outPin2(outPin2) {} diff --git a/src/GpioLogic.h b/src/GpioLogic.h index 947d49625..096721ee8 100644 --- a/src/GpioLogic.h +++ b/src/GpioLogic.h @@ -3,8 +3,9 @@ #include "configuration.h" /**This is a set of classes to mediate access to GPIOs in a structured way. Most usage of GPIOs do not - require these classes! But if your hardware has a GPIO that is 'shared' between multiple devices (i.e. a shared power enable) - then using these classes might be able to let you cleanly turn on that enable when either dependent device is needed. + require these classes! But if your hardware has a GPIO that is 'shared' between multiple devices (i.e. a shared + power enable) then using these classes might be able to let you cleanly turn on that enable when either dependent + device is needed. Note: these classes are intended to be 99% inline for the common case so should have minimal impact on flash or RAM requirements. @@ -13,23 +14,21 @@ /** * A logical GPIO pin (not necessary raw hardware). */ -class GpioPin -{ - public: - virtual void set(bool value) = 0; +class GpioPin { +public: + virtual void set(bool value) = 0; }; /** * A physical GPIO hw pin. */ -class GpioHwPin : public GpioPin -{ - uint32_t num; +class GpioHwPin : public GpioPin { + uint32_t num; - public: - explicit GpioHwPin(uint32_t num) : num(num) {} +public: + explicit GpioHwPin(uint32_t num) : num(num) {} - void set(bool value); + void set(bool value); }; class GpioTransformer; @@ -39,122 +38,115 @@ class GpioBinaryTransformer; /** * A virtual GPIO pin. */ -class GpioVirtPin : public GpioPin -{ - friend class GpioBinaryTransformer; - friend class GpioUnaryTransformer; +class GpioVirtPin : public GpioPin { + friend class GpioBinaryTransformer; + friend class GpioUnaryTransformer; - public: - enum PinState { On = true, Off = false, Unset = 2 }; +public: + enum PinState { On = true, Off = false, Unset = 2 }; - void set(bool value); - PinState get() const { return value; } + void set(bool value); + PinState get() const { return value; } - private: - PinState value = PinState::Unset; - GpioTransformer *dependentPin = NULL; +private: + PinState value = PinState::Unset; + GpioTransformer *dependentPin = NULL; }; #include /** - * A 'smart' trigger that can depend in a fake GPIO and if that GPIO changes, drive some other downstream GPIO to change. - * notably: the set method is not public (because it always is calculated by a subclass) + * A 'smart' trigger that can depend in a fake GPIO and if that GPIO changes, drive some other downstream GPIO to + * change. notably: the set method is not public (because it always is calculated by a subclass) */ -class GpioTransformer -{ - public: - /** - * Update the output pin based on the current state of the input pin. - */ - virtual void update() = 0; +class GpioTransformer { +public: + /** + * Update the output pin based on the current state of the input pin. + */ + virtual void update() = 0; - protected: - GpioTransformer(GpioPin *outPin); +protected: + GpioTransformer(GpioPin *outPin); - void set(bool value); + void set(bool value); - private: - GpioPin *outPin; +private: + GpioPin *outPin; }; /** * A transformer that just drives a hw pin based on a virtual pin. */ -class GpioUnaryTransformer : public GpioTransformer -{ - public: - GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin); +class GpioUnaryTransformer : public GpioTransformer { +public: + GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin); - protected: - friend class GpioVirtPin; +protected: + friend class GpioVirtPin; - /** - * Update the output pin based on the current state of the input pin. - */ - virtual void update(); + /** + * Update the output pin based on the current state of the input pin. + */ + virtual void update(); - GpioVirtPin *inPin; + GpioVirtPin *inPin; }; /** * A transformer that performs a unary NOT operation from an input. */ -class GpioNotTransformer : public GpioUnaryTransformer -{ - public: - GpioNotTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioUnaryTransformer(inPin, outPin) {} +class GpioNotTransformer : public GpioUnaryTransformer { +public: + GpioNotTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioUnaryTransformer(inPin, outPin) {} - protected: - friend class GpioVirtPin; +protected: + friend class GpioVirtPin; - /** - * Update the output pin based on the current state of the input pin. - */ - void update(); + /** + * Update the output pin based on the current state of the input pin. + */ + void update(); }; /** * A transformer that combines multiple virtual pins to drive an output pin */ -class GpioBinaryTransformer : public GpioTransformer -{ +class GpioBinaryTransformer : public GpioTransformer { - public: - enum Operation { And, Or, Xor }; +public: + enum Operation { And, Or, Xor }; - GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation); + GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation); - protected: - friend class GpioVirtPin; +protected: + friend class GpioVirtPin; - /** - * Update the output pin based on the current state of the input pins. - */ - void update(); + /** + * Update the output pin based on the current state of the input pins. + */ + void update(); - private: - GpioVirtPin *inPin1; - GpioVirtPin *inPin2; - Operation operation; +private: + GpioVirtPin *inPin1; + GpioVirtPin *inPin2; + Operation operation; }; /** * Sometimes a single output GPIO single needs to drive multiple physical GPIOs. This class provides that. */ -class GpioSplitter : public GpioPin -{ +class GpioSplitter : public GpioPin { - public: - GpioSplitter(GpioPin *outPin1, GpioPin *outPin2); +public: + GpioSplitter(GpioPin *outPin1, GpioPin *outPin2); - void set(bool value) - { - outPin1->set(value); - outPin2->set(value); - } + void set(bool value) { + outPin1->set(value); + outPin2->set(value); + } - private: - GpioPin *outPin1; - GpioPin *outPin2; +private: + GpioPin *outPin1; + GpioPin *outPin2; }; \ No newline at end of file diff --git a/src/Led.cpp b/src/Led.cpp index 6406cd2f7..ecd0058b6 100644 --- a/src/Led.cpp +++ b/src/Led.cpp @@ -23,16 +23,14 @@ static GpioPin &ledHwPin = ledRawHwPin; /** * A GPIO controlled by the PMU */ -class GpioPmuPin : public GpioPin -{ - public: - void set(bool value) - { - if (pmu_found && PMU) { - // blink the axp led - PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); - } +class GpioPmuPin : public GpioPin { +public: + void set(bool value) { + if (pmu_found && PMU) { + // blink the axp led + PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); } + } } ledPmuHwPin; // In some cases we need to drive a PMU LED and a normal LED @@ -45,19 +43,17 @@ static GpioPin &ledFinalPin = ledHwPin; /** * We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff. */ -class MonitoredLedPin : public GpioPin -{ - public: - void set(bool value) - { - if (powerMon) { - if (value) - powerMon->setState(meshtastic_PowerMon_State_LED_On); - else - powerMon->clearState(meshtastic_PowerMon_State_LED_On); - } - ledFinalPin.set(value); +class MonitoredLedPin : public GpioPin { +public: + void set(bool value) { + if (powerMon) { + if (value) + powerMon->setState(meshtastic_PowerMon_State_LED_On); + else + powerMon->clearState(meshtastic_PowerMon_State_LED_On); } + ledFinalPin.set(value); + } } monitoredLedPin; #else static GpioPin &monitoredLedPin = ledFinalPin; diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index c96645b1c..f6b5eece5 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -18,257 +18,238 @@ static char *g_messagePool = nullptr; static size_t g_poolWritePos = 0; // Reset pool (called on boot or clear) -static inline void resetMessagePool() -{ +static inline void resetMessagePool() { + if (!g_messagePool) { + g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); if (!g_messagePool) { - g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); - if (!g_messagePool) { - LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); - return; - } + LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); + return; } - g_poolWritePos = 0; - memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); + } + g_poolWritePos = 0; + memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); } // Allocate text in pool and return offset // If not enough space remains, wrap around (ring buffer style) -static inline uint16_t storeTextInPool(const char *src, size_t len) -{ - if (len >= MAX_MESSAGE_SIZE) - len = MAX_MESSAGE_SIZE - 1; +static inline uint16_t storeTextInPool(const char *src, size_t len) { + if (len >= MAX_MESSAGE_SIZE) + len = MAX_MESSAGE_SIZE - 1; - // Wrap pool if out of space - if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { - g_poolWritePos = 0; - } + // Wrap pool if out of space + if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { + g_poolWritePos = 0; + } - uint16_t offset = g_poolWritePos; - memcpy(&g_messagePool[g_poolWritePos], src, len); - g_messagePool[g_poolWritePos + len] = '\0'; - g_poolWritePos += (len + 1); - return offset; + uint16_t offset = g_poolWritePos; + memcpy(&g_messagePool[g_poolWritePos], src, len); + g_messagePool[g_poolWritePos + len] = '\0'; + g_poolWritePos += (len + 1); + return offset; } // Retrieve a const pointer to message text by offset -static inline const char *getTextFromPool(uint16_t offset) -{ - if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) - return ""; - return &g_messagePool[offset]; +static inline const char *getTextFromPool(uint16_t offset) { + if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) + return ""; + return &g_messagePool[offset]; } // Helper: assign a timestamp (RTC if available, else boot-relative) -static inline void assignTimestamp(StoredMessage &sm) -{ - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - if (nowSecs) { - sm.timestamp = nowSecs; - sm.isBootRelative = false; - } else { - sm.timestamp = millis() / 1000; - sm.isBootRelative = true; - } +static inline void assignTimestamp(StoredMessage &sm) { + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs) { + sm.timestamp = nowSecs; + sm.isBootRelative = false; + } else { + sm.timestamp = millis() / 1000; + sm.isBootRelative = true; + } } // Generic push with cap (used by live + persisted queues) -template static inline void pushWithLimit(std::deque &queue, const T &msg) -{ - if (queue.size() >= MAX_MESSAGES_SAVED) - queue.pop_front(); - queue.push_back(msg); +template static inline void pushWithLimit(std::deque &queue, const T &msg) { + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.push_back(msg); } -template static inline void pushWithLimit(std::deque &queue, T &&msg) -{ - if (queue.size() >= MAX_MESSAGES_SAVED) - queue.pop_front(); - queue.emplace_back(std::move(msg)); +template static inline void pushWithLimit(std::deque &queue, T &&msg) { + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.emplace_back(std::move(msg)); } -MessageStore::MessageStore(const std::string &label) -{ - filename = "/Messages_" + label + ".msgs"; - resetMessagePool(); // initialize text pool on boot +MessageStore::MessageStore(const std::string &label) { + filename = "/Messages_" + label + ".msgs"; + resetMessagePool(); // initialize text pool on boot } // Live message handling (RAM only) -void MessageStore::addLiveMessage(StoredMessage &&msg) -{ - pushWithLimit(liveMessages, std::move(msg)); -} -void MessageStore::addLiveMessage(const StoredMessage &msg) -{ - pushWithLimit(liveMessages, msg); -} +void MessageStore::addLiveMessage(StoredMessage &&msg) { pushWithLimit(liveMessages, std::move(msg)); } +void MessageStore::addLiveMessage(const StoredMessage &msg) { pushWithLimit(liveMessages, msg); } // Add from incoming/outgoing packet -const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) -{ - StoredMessage sm; - assignTimestamp(sm); - sm.channelIndex = packet.channel; +const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { + StoredMessage sm; + assignTimestamp(sm); + sm.channelIndex = packet.channel; - const char *payload = reinterpret_cast(packet.decoded.payload.bytes); - size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); - sm.textOffset = storeTextInPool(payload, len); - sm.textLength = len; + const char *payload = reinterpret_cast(packet.decoded.payload.bytes); + size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); + sm.textOffset = storeTextInPool(payload, len); + sm.textLength = len; - // Determine sender - uint32_t localNode = nodeDB->getNodeNum(); - sm.sender = (packet.from == 0) ? localNode : packet.from; + // Determine sender + uint32_t localNode = nodeDB->getNodeNum(); + sm.sender = (packet.from == 0) ? localNode : packet.from; - sm.dest = packet.to; + sm.dest = packet.to; - bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); + bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); - if (packet.from == 0) { - sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; - sm.ackStatus = AckStatus::NONE; - } else { - sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; - sm.ackStatus = AckStatus::ACKED; - } + if (packet.from == 0) { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::NONE; + } else { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::ACKED; + } - addLiveMessage(sm); - return liveMessages.back(); + addLiveMessage(sm); + return liveMessages.back(); } // Outgoing/manual message -void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) -{ - StoredMessage sm; +void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) { + StoredMessage sm; - // Always use our local time (helper handles RTC vs boot time) - assignTimestamp(sm); + // Always use our local time (helper handles RTC vs boot time) + assignTimestamp(sm); - sm.sender = sender; - sm.channelIndex = channelIndex; - sm.textOffset = storeTextInPool(text.c_str(), text.size()); - sm.textLength = text.size(); + sm.sender = sender; + sm.channelIndex = channelIndex; + sm.textOffset = storeTextInPool(text.c_str(), text.size()); + sm.textLength = text.size(); - // Use the provided destination - sm.dest = sender; - sm.type = MessageType::DM_TO_US; + // Use the provided destination + sm.dest = sender; + sm.type = MessageType::DM_TO_US; - // Outgoing messages always start with unknown ack status - sm.ackStatus = AckStatus::NONE; + // Outgoing messages always start with unknown ack status + sm.ackStatus = AckStatus::NONE; - addLiveMessage(sm); + addLiveMessage(sm); } #if ENABLE_MESSAGE_PERSISTENCE // Compact, fixed-size on-flash representation using offset + length struct __attribute__((packed)) StoredMessageRecord { - uint32_t timestamp; - uint32_t sender; - uint8_t channelIndex; - uint32_t dest; - uint8_t isBootRelative; - uint8_t ackStatus; // static_cast(AckStatus) - uint8_t type; // static_cast(MessageType) - uint16_t textLength; // message length - char text[MAX_MESSAGE_SIZE]; // store actual text here + uint32_t timestamp; + uint32_t sender; + uint8_t channelIndex; + uint32_t dest; + uint8_t isBootRelative; + uint8_t ackStatus; // static_cast(AckStatus) + uint8_t type; // static_cast(MessageType) + uint16_t textLength; // message length + char text[MAX_MESSAGE_SIZE]; // store actual text here }; // Serialize one StoredMessage to flash -static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) -{ - StoredMessageRecord rec = {}; - rec.timestamp = m.timestamp; - rec.sender = m.sender; - rec.channelIndex = m.channelIndex; - rec.dest = m.dest; - rec.isBootRelative = m.isBootRelative; - rec.ackStatus = static_cast(m.ackStatus); - rec.type = static_cast(m.type); - rec.textLength = m.textLength; +static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) { + StoredMessageRecord rec = {}; + rec.timestamp = m.timestamp; + rec.sender = m.sender; + rec.channelIndex = m.channelIndex; + rec.dest = m.dest; + rec.isBootRelative = m.isBootRelative; + rec.ackStatus = static_cast(m.ackStatus); + rec.type = static_cast(m.type); + rec.textLength = m.textLength; - // Copy the actual text into the record from RAM pool - const char *txt = getTextFromPool(m.textOffset); - strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); - rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; + // Copy the actual text into the record from RAM pool + const char *txt = getTextFromPool(m.textOffset); + strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); + rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; - f.write(reinterpret_cast(&rec), sizeof(rec)); + f.write(reinterpret_cast(&rec), sizeof(rec)); } // Deserialize one StoredMessage from flash; returns false on short read -static inline bool readMessageRecord(File &f, StoredMessage &m) -{ - StoredMessageRecord rec = {}; - if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) - return false; +static inline bool readMessageRecord(File &f, StoredMessage &m) { + StoredMessageRecord rec = {}; + if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) + return false; - m.timestamp = rec.timestamp; - m.sender = rec.sender; - m.channelIndex = rec.channelIndex; - m.dest = rec.dest; - m.isBootRelative = rec.isBootRelative; - m.ackStatus = static_cast(rec.ackStatus); - m.type = static_cast(rec.type); - m.textLength = rec.textLength; + m.timestamp = rec.timestamp; + m.sender = rec.sender; + m.channelIndex = rec.channelIndex; + m.dest = rec.dest; + m.isBootRelative = rec.isBootRelative; + m.ackStatus = static_cast(rec.ackStatus); + m.type = static_cast(rec.type); + m.textLength = rec.textLength; - // 💡 Re-store text into pool and update offset - m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); - m.textOffset = storeTextInPool(rec.text, m.textLength); + // 💡 Re-store text into pool and update offset + m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); + m.textOffset = storeTextInPool(rec.text, m.textLength); - return true; + return true; } -void MessageStore::saveToFlash() -{ +void MessageStore::saveToFlash() { #ifdef FSCom - // Ensure root exists - spiLock->lock(); - FSCom.mkdir("/"); - spiLock->unlock(); + // Ensure root exists + spiLock->lock(); + FSCom.mkdir("/"); + spiLock->unlock(); - SafeFile f(filename.c_str(), false); + SafeFile f(filename.c_str(), false); - spiLock->lock(); - uint8_t count = static_cast(liveMessages.size()); - if (count > MAX_MESSAGES_SAVED) - count = MAX_MESSAGES_SAVED; - f.write(&count, 1); + spiLock->lock(); + uint8_t count = static_cast(liveMessages.size()); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; + f.write(&count, 1); - for (uint8_t i = 0; i < count; ++i) { - writeMessageRecord(f, liveMessages[i]); - } - spiLock->unlock(); + for (uint8_t i = 0; i < count; ++i) { + writeMessageRecord(f, liveMessages[i]); + } + spiLock->unlock(); - f.close(); + f.close(); #endif } -void MessageStore::loadFromFlash() -{ - std::deque().swap(liveMessages); - resetMessagePool(); // reset pool when loading +void MessageStore::loadFromFlash() { + std::deque().swap(liveMessages); + resetMessagePool(); // reset pool when loading #ifdef FSCom - concurrency::LockGuard guard(spiLock); + concurrency::LockGuard guard(spiLock); - if (!FSCom.exists(filename.c_str())) - return; + if (!FSCom.exists(filename.c_str())) + return; - auto f = FSCom.open(filename.c_str(), FILE_O_READ); - if (!f) - return; + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + if (!f) + return; - uint8_t count = 0; - f.readBytes(reinterpret_cast(&count), 1); - if (count > MAX_MESSAGES_SAVED) - count = MAX_MESSAGES_SAVED; + uint8_t count = 0; + f.readBytes(reinterpret_cast(&count), 1); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; - for (uint8_t i = 0; i < count; ++i) { - StoredMessage m; - if (!readMessageRecord(f, m)) - break; - liveMessages.push_back(m); - } + for (uint8_t i = 0; i < count; ++i) { + StoredMessage m; + if (!readMessageRecord(f, m)) + break; + liveMessages.push_back(m); + } - f.close(); + f.close(); #endif } @@ -279,146 +260,134 @@ void MessageStore::loadFromFlash() {} #endif // Clear all messages (RAM + persisted queue) -void MessageStore::clearAllMessages() -{ - std::deque().swap(liveMessages); - resetMessagePool(); +void MessageStore::clearAllMessages() { + std::deque().swap(liveMessages); + resetMessagePool(); #ifdef FSCom - SafeFile f(filename.c_str(), false); - uint8_t count = 0; - f.write(&count, 1); // write "0 messages" - f.close(); + SafeFile f(filename.c_str(), false); + uint8_t count = 0; + f.write(&count, 1); // write "0 messages" + f.close(); #endif } // Internal helper: erase first or last message matching a predicate -template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) -{ - if (fromBack) { - // Iterate from the back and erase all matches from the end - for (auto it = deque.rbegin(); it != deque.rend();) { - if (pred(*it)) { - it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); - } else { - ++it; - } - } - } else { - // Manual forward search to erase all matches - for (auto it = deque.begin(); it != deque.end();) { - if (pred(*it)) { - it = deque.erase(it); - } else { - ++it; - } - } +template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) { + if (fromBack) { + // Iterate from the back and erase all matches from the end + for (auto it = deque.rbegin(); it != deque.rend();) { + if (pred(*it)) { + it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); + } else { + ++it; + } } + } else { + // Manual forward search to erase all matches + for (auto it = deque.begin(); it != deque.end();) { + if (pred(*it)) { + it = deque.erase(it); + } else { + ++it; + } + } + } } // Delete oldest message (RAM + persisted queue) -void MessageStore::deleteOldestMessage() -{ - eraseIf(liveMessages, [](StoredMessage &) { return true; }); - saveToFlash(); +void MessageStore::deleteOldestMessage() { + eraseIf(liveMessages, [](StoredMessage &) { return true; }); + saveToFlash(); } // Delete oldest message in a specific channel -void MessageStore::deleteOldestMessageInChannel(uint8_t channel) -{ - auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; - eraseIf(liveMessages, pred); - saveToFlash(); +void MessageStore::deleteOldestMessageInChannel(uint8_t channel) { + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred); + saveToFlash(); } -void MessageStore::deleteAllMessagesInChannel(uint8_t channel) -{ - auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; - eraseIf(liveMessages, pred, false /* delete ALL, not just first */); - saveToFlash(); +void MessageStore::deleteAllMessagesInChannel(uint8_t channel) { + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred, false /* delete ALL, not just first */); + saveToFlash(); } -void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) -{ - uint32_t local = nodeDB->getNodeNum(); - auto pred = [&](const StoredMessage &m) { - if (m.type != MessageType::DM_TO_US) - return false; - uint32_t other = (m.sender == local) ? m.dest : m.sender; - return other == peer; - }; - eraseIf(liveMessages, pred, false); - saveToFlash(); +void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) { + uint32_t local = nodeDB->getNodeNum(); + auto pred = [&](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == local) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred, false); + saveToFlash(); } // Delete oldest message in a direct chat with a node -void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) -{ - auto pred = [peer](const StoredMessage &m) { - if (m.type != MessageType::DM_TO_US) - return false; - uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; - return other == peer; - }; - eraseIf(liveMessages, pred); - saveToFlash(); +void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) { + auto pred = [peer](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred); + saveToFlash(); } -std::deque MessageStore::getChannelMessages(uint8_t channel) const -{ - std::deque result; - for (const auto &m : liveMessages) { - if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { - result.push_back(m); - } +std::deque MessageStore::getChannelMessages(uint8_t channel) const { + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { + result.push_back(m); } - return result; + } + return result; } -std::deque MessageStore::getDirectMessages() const -{ - std::deque result; - for (const auto &m : liveMessages) { - if (m.type == MessageType::DM_TO_US) { - result.push_back(m); - } +std::deque MessageStore::getDirectMessages() const { + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::DM_TO_US) { + result.push_back(m); } - return result; + } + return result; } // Upgrade boot-relative timestamps once RTC is valid // Only same-boot boot-relative messages are healed. // Persisted boot-relative messages from old boots stay ??? forever. -void MessageStore::upgradeBootRelativeTimestamps() -{ - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - if (nowSecs == 0) - return; // Still no valid RTC +void MessageStore::upgradeBootRelativeTimestamps() { + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs == 0) + return; // Still no valid RTC - uint32_t bootNow = millis() / 1000; + uint32_t bootNow = millis() / 1000; - auto fix = [&](std::deque &dq) { - for (auto &m : dq) { - if (m.isBootRelative && m.timestamp <= bootNow) { - uint32_t bootOffset = nowSecs - bootNow; - m.timestamp += bootOffset; - m.isBootRelative = false; - } - } - }; - fix(liveMessages); + auto fix = [&](std::deque &dq) { + for (auto &m : dq) { + if (m.isBootRelative && m.timestamp <= bootNow) { + uint32_t bootOffset = nowSecs - bootNow; + m.timestamp += bootOffset; + m.isBootRelative = false; + } + } + }; + fix(liveMessages); } -const char *MessageStore::getText(const StoredMessage &msg) -{ - // Wrapper around the internal helper - return getTextFromPool(msg.textOffset); +const char *MessageStore::getText(const StoredMessage &msg) { + // Wrapper around the internal helper + return getTextFromPool(msg.textOffset); } -uint16_t MessageStore::storeText(const char *src, size_t len) -{ - // Wrapper around the internal helper - return storeTextInPool(src, len); +uint16_t MessageStore::storeText(const char *src, size_t len) { + // Wrapper around the internal helper + return storeTextInPool(src, len); } // Global definition diff --git a/src/MessageStore.h b/src/MessageStore.h index 41eb56b66..362f0a9e9 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -39,90 +39,87 @@ // Explicit message classification enum class MessageType : uint8_t { - BROADCAST = 0, // broadcast message - DM_TO_US = 1 // direct message addressed to this node + BROADCAST = 0, // broadcast message + DM_TO_US = 1 // direct message addressed to this node }; // Delivery status for messages we sent enum class AckStatus : uint8_t { - NONE = 0, // just sent, waiting (no symbol shown) - ACKED = 1, // got a valid ACK from destination - NACKED = 2, // explicitly failed - TIMEOUT = 3, // no ACK after retry window - RELAYED = 4 // got an ACK from relay, not destination + NONE = 0, // just sent, waiting (no symbol shown) + ACKED = 1, // got a valid ACK from destination + NACKED = 2, // explicitly failed + TIMEOUT = 3, // no ACK after retry window + RELAYED = 4 // got an ACK from relay, not destination }; struct StoredMessage { - uint32_t timestamp; // When message was created (secs since boot or RTC) - uint32_t sender; // NodeNum of sender - uint8_t channelIndex; // Channel index used - uint32_t dest; // Destination node (broadcast or direct) - MessageType type; // Derived from dest (explicit classification) - bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute - AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) + uint32_t timestamp; // When message was created (secs since boot or RTC) + uint32_t sender; // NodeNum of sender + uint8_t channelIndex; // Channel index used + uint32_t dest; // Destination node (broadcast or direct) + MessageType type; // Derived from dest (explicit classification) + bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute + AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) - // Text storage metadata — rebuilt from flash at boot - uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) - uint16_t textLength; // Length of text in bytes + // Text storage metadata — rebuilt from flash at boot + uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) + uint16_t textLength; // Length of text in bytes - // Default constructor initializes all fields safely - StoredMessage() - : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), - ackStatus(AckStatus::NONE), textOffset(0), textLength(0) - { - } + // Default constructor initializes all fields safely + StoredMessage() + : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), ackStatus(AckStatus::NONE), + textOffset(0), textLength(0) {} }; -class MessageStore -{ - public: - explicit MessageStore(const std::string &label); +class MessageStore { +public: + explicit MessageStore(const std::string &label); - // Live RAM methods (always current, used by UI and runtime) - void addLiveMessage(StoredMessage &&msg); - void addLiveMessage(const StoredMessage &msg); // convenience overload - const std::deque &getLiveMessages() const { return liveMessages; } + // Live RAM methods (always current, used by UI and runtime) + void addLiveMessage(StoredMessage &&msg); + void addLiveMessage(const StoredMessage &msg); // convenience overload + const std::deque &getLiveMessages() const { return liveMessages; } - // Add new messages from packets or manual input - const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only - void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add + // Add new messages from packets or manual input + const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only + void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add - // Persistence methods (used only on boot/shutdown) - void saveToFlash(); // Save messages to flash - void loadFromFlash(); // Load messages from flash + // Persistence methods (used only on boot/shutdown) + void saveToFlash(); // Save messages to flash + void loadFromFlash(); // Load messages from flash - // Clear all messages (RAM + persisted queue + text pool) - void clearAllMessages(); + // Clear all messages (RAM + persisted queue + text pool) + void clearAllMessages(); - // Delete helpers - void deleteOldestMessage(); // remove oldest from RAM (and flash on save) - void deleteOldestMessageInChannel(uint8_t channel); - void deleteOldestMessageWithPeer(uint32_t peer); - void deleteAllMessagesInChannel(uint8_t channel); - void deleteAllMessagesWithPeer(uint32_t peer); + // Delete helpers + void deleteOldestMessage(); // remove oldest from RAM (and flash on save) + void deleteOldestMessageInChannel(uint8_t channel); + void deleteOldestMessageWithPeer(uint32_t peer); + void deleteAllMessagesInChannel(uint8_t channel); + void deleteAllMessagesWithPeer(uint32_t peer); - // Unified accessor (for UI code, defaults to RAM buffer) - const std::deque &getMessages() const { return liveMessages; } + // Unified accessor (for UI code, defaults to RAM buffer) + const std::deque &getMessages() const { return liveMessages; } - // Helper filters for future use - std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel - std::deque getDirectMessages() const; // Only direct messages + // Helper filters for future use + std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel + std::deque getDirectMessages() const; // Only direct messages - // Upgrade boot-relative timestamps once RTC is valid - void upgradeBootRelativeTimestamps(); + // Upgrade boot-relative timestamps once RTC is valid + void upgradeBootRelativeTimestamps(); - // Retrieve the C-string text for a stored message - static const char *getText(const StoredMessage &msg); + // Retrieve the C-string text for a stored message + static const char *getText(const StoredMessage &msg); - // Allocate text into pool (used by sender-side code) - static uint16_t storeText(const char *src, size_t len); + // Allocate text into pool (used by sender-side code) + static uint16_t storeText(const char *src, size_t len); - // Used when loading from flash to rebuild the text pool - static uint16_t rebuildTextFromFlash(const char *src, size_t len); + // Used when loading from flash to rebuild the text pool + static uint16_t rebuildTextFromFlash(const char *src, size_t len); - private: - std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) - std::string filename; // Flash filename for persistence +private: + std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) + std::string filename; // Flash filename for persistence }; // Global instance (defined in MessageStore.cpp) diff --git a/src/NodeStatus.h b/src/NodeStatus.h index 550f6254a..e75bc20c9 100644 --- a/src/NodeStatus.h +++ b/src/NodeStatus.h @@ -3,64 +3,56 @@ #include "configuration.h" #include -namespace meshtastic -{ +namespace meshtastic { /// Describes the state of the NodeDB system. -class NodeStatus : public Status -{ +class NodeStatus : public Status { - private: - CallbackObserver statusObserver = - CallbackObserver(this, &NodeStatus::updateStatus); +private: + CallbackObserver statusObserver = CallbackObserver(this, &NodeStatus::updateStatus); - uint16_t numOnline = 0; - uint16_t numTotal = 0; + uint16_t numOnline = 0; + uint16_t numTotal = 0; - uint16_t lastNumTotal = 0; + uint16_t lastNumTotal = 0; - public: - bool forceUpdate = false; +public: + bool forceUpdate = false; - NodeStatus() { statusType = STATUS_TYPE_NODE; } - NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status() + NodeStatus() { statusType = STATUS_TYPE_NODE; } + NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status() { + this->forceUpdate = forceUpdate; + this->numOnline = numOnline; + this->numTotal = numTotal; + } + NodeStatus(const NodeStatus &); + NodeStatus &operator=(const NodeStatus &); + + void observe(Observable *source) { statusObserver.observe(source); } + + uint16_t getNumOnline() const { return numOnline; } + + uint16_t getNumTotal() const { return numTotal; } + + uint16_t getLastNumTotal() const { return lastNumTotal; } + + bool matches(const NodeStatus *newStatus) const { return (newStatus->getNumOnline() != numOnline || newStatus->getNumTotal() != numTotal); } + int updateStatus(const NodeStatus *newStatus) { + // Only update the status if values have actually changed + lastNumTotal = numTotal; + bool isDirty; { - this->forceUpdate = forceUpdate; - this->numOnline = numOnline; - this->numTotal = numTotal; + isDirty = matches(newStatus); + initialized = true; + numOnline = newStatus->getNumOnline(); + numTotal = newStatus->getNumTotal(); } - NodeStatus(const NodeStatus &); - NodeStatus &operator=(const NodeStatus &); - - void observe(Observable *source) { statusObserver.observe(source); } - - uint16_t getNumOnline() const { return numOnline; } - - uint16_t getNumTotal() const { return numTotal; } - - uint16_t getLastNumTotal() const { return lastNumTotal; } - - bool matches(const NodeStatus *newStatus) const - { - return (newStatus->getNumOnline() != numOnline || newStatus->getNumTotal() != numTotal); - } - int updateStatus(const NodeStatus *newStatus) - { - // Only update the status if values have actually changed - lastNumTotal = numTotal; - bool isDirty; - { - isDirty = matches(newStatus); - initialized = true; - numOnline = newStatus->getNumOnline(); - numTotal = newStatus->getNumTotal(); - } - if (isDirty || newStatus->forceUpdate) { - LOG_DEBUG("Node status update: %u online, %u total", numOnline, numTotal); - onNewStatus.notifyObservers(this); - } - return 0; + if (isDirty || newStatus->forceUpdate) { + LOG_DEBUG("Node status update: %u online, %u total", numOnline, numTotal); + onNewStatus.notifyObservers(this); } + return 0; + } }; } // namespace meshtastic diff --git a/src/Observer.h b/src/Observer.h index 6e1ec44c8..53466905b 100644 --- a/src/Observer.h +++ b/src/Observer.h @@ -8,99 +8,90 @@ template class Observable; /** * An observer which can be mixed in as a baseclass. Implement onNotify as a method in your class. */ -template class Observer -{ - std::list *> observables; +template class Observer { + std::list *> observables; - public: - virtual ~Observer(); +public: + virtual ~Observer(); - /// Stop watching the observable - void unobserve(Observable *o); + /// Stop watching the observable + void unobserve(Observable *o); - /// Start watching a specified observable - void observe(Observable *o); + /// Start watching a specified observable + void observe(Observable *o); - private: - friend class Observable; +private: + friend class Observable; - protected: - /** - * returns 0 if other observers should continue to be called - * returns !0 if the observe calls should be aborted and this result code returned for notifyObservers - **/ - virtual int onNotify(T arg) = 0; +protected: + /** + * returns 0 if other observers should continue to be called + * returns !0 if the observe calls should be aborted and this result code returned for notifyObservers + **/ + virtual int onNotify(T arg) = 0; }; /** * An observer that calls an arbitrary method */ -template class CallbackObserver : public Observer -{ - typedef int (Callback::*ObserverCallback)(T arg); +template class CallbackObserver : public Observer { + typedef int (Callback::*ObserverCallback)(T arg); - Callback *objPtr; - ObserverCallback method; + Callback *objPtr; + ObserverCallback method; - public: - CallbackObserver(Callback *_objPtr, ObserverCallback _method) : objPtr(_objPtr), method(_method) {} +public: + CallbackObserver(Callback *_objPtr, ObserverCallback _method) : objPtr(_objPtr), method(_method) {} - protected: - virtual int onNotify(T arg) override { return (objPtr->*method)(arg); } +protected: + virtual int onNotify(T arg) override { return (objPtr->*method)(arg); } }; /** - * An observable class that will notify observers anytime notifyObservers is called. Argument type T can be any type, but for - * performance reasons a pointer or word sized object is recommended. + * An observable class that will notify observers anytime notifyObservers is called. Argument type T can be any type, + * but for performance reasons a pointer or word sized object is recommended. */ -template class Observable -{ - std::list *> observers; +template class Observable { + std::list *> observers; - public: - /** - * Tell all observers about a change, observers can process arg as they wish - * - * returns !0 if an observer chose to abort processing by returning this code - */ - int notifyObservers(T arg) - { - for (typename std::list *>::const_iterator iterator = observers.begin(); iterator != observers.end(); - ++iterator) { - int result = (*iterator)->onNotify(arg); - if (result != 0) - return result; - } - - return 0; +public: + /** + * Tell all observers about a change, observers can process arg as they wish + * + * returns !0 if an observer chose to abort processing by returning this code + */ + int notifyObservers(T arg) { + for (typename std::list *>::const_iterator iterator = observers.begin(); iterator != observers.end(); ++iterator) { + int result = (*iterator)->onNotify(arg); + if (result != 0) + return result; } - private: - friend class Observer; + return 0; + } - // Not called directly, instead call observer.observe - void addObserver(Observer *o) { observers.push_back(o); } +private: + friend class Observer; - void removeObserver(Observer *o) { observers.remove(o); } + // Not called directly, instead call observer.observe + void addObserver(Observer *o) { observers.push_back(o); } + + void removeObserver(Observer *o) { observers.remove(o); } }; -template Observer::~Observer() -{ - for (typename std::list *>::const_iterator iterator = observables.begin(); iterator != observables.end(); - ++iterator) { - (*iterator)->removeObserver(this); - } - observables.clear(); +template Observer::~Observer() { + for (typename std::list *>::const_iterator iterator = observables.begin(); iterator != observables.end(); ++iterator) { + (*iterator)->removeObserver(this); + } + observables.clear(); } -template void Observer::unobserve(Observable *o) -{ - o->removeObserver(this); - observables.remove(o); +template void Observer::unobserve(Observable *o) { + o->removeObserver(this); + observables.remove(o); } -template void Observer::observe(Observable *o) -{ - observables.push_back(o); - o->addObserver(this); +template void Observer::observe(Observable *o) { + observables.push_back(o); + o->addObserver(this); } \ No newline at end of file diff --git a/src/Power.cpp b/src/Power.cpp index 33dda8e11..51fd1e8b3 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1,11 +1,11 @@ /** * @file Power.cpp - * @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality - * of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The - * Power class is used by the main device class to manage power-related functionality. + * @brief This file contains the implementation of the Power class, which is responsible for managing power-related + * functionality of the device. It includes battery level sensing, power management unit (PMU) control, and power state + * machine management. The Power class is used by the main device class to manage power-related functionality. * - * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes - * the battery voltage is attached via a voltage-divider to an analog input. + * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which + * assumes the battery voltage is attached via a voltage-divider to an analog input. * * This file is part of the Meshtastic project. * For more information, see: https://meshtastic.org/ @@ -142,26 +142,25 @@ XPowersLibInterface *PMU = NULL; // Copy of the base class defined in axp20x.h. // I'd rather not include axp20x.h as it brings Wire dependency. -class HasBatteryLevel -{ - public: - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() { return -1; } +class HasBatteryLevel { +public: + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() { return -1; } - /** - * The raw voltage of the battery or NAN if unknown - */ - virtual uint16_t getBattVoltage() { return 0; } + /** + * The raw voltage of the battery or NAN if unknown + */ + virtual uint16_t getBattVoltage() { return 0; } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() { return false; } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() { return false; } - virtual bool isVbusIn() { return false; } - virtual bool isCharging() { return false; } + virtual bool isVbusIn() { return false; } + virtual bool isCharging() { return false; } }; #endif @@ -195,36 +194,34 @@ static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level se #ifdef BATTERY_PIN -void battery_adcEnable() -{ +void battery_adcEnable() { #ifdef ADC_CTRL // enable adc voltage divider when we need to read #ifdef ADC_USE_PULLUP - pinMode(ADC_CTRL, INPUT_PULLUP); + pinMode(ADC_CTRL, INPUT_PULLUP); #else #ifdef HELTEC_V3 - pinMode(ADC_CTRL, INPUT); - uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL)); - pinMode(ADC_CTRL, OUTPUT); - digitalWrite(ADC_CTRL, adc_ctl_enable_value); + pinMode(ADC_CTRL, INPUT); + uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL)); + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, adc_ctl_enable_value); #else - pinMode(ADC_CTRL, OUTPUT); - digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED); + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED); #endif #endif - delay(10); + delay(10); #endif } -static void battery_adcDisable() -{ +static void battery_adcDisable() { #ifdef ADC_CTRL // disable adc voltage divider when we need to read #ifdef ADC_USE_PULLUP - pinMode(ADC_CTRL, INPUT_PULLDOWN); + pinMode(ADC_CTRL, INPUT_PULLDOWN); #else #ifdef HELTEC_V3 - pinMode(ADC_CTRL, ANALOG); + pinMode(ADC_CTRL, ANALOG); #else - digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); + digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); #endif #endif #endif @@ -235,74 +232,70 @@ static void battery_adcDisable() /** * A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input */ -class AnalogBatteryLevel : public HasBatteryLevel -{ - public: - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override - { +class AnalogBatteryLevel : public HasBatteryLevel { +public: + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { #if defined(HAS_RAKPROT) && !defined(HAS_PMU) - if (hasRAK()) { - return rak9154Sensor.getBusBatteryPercent(); - } + if (hasRAK()) { + return rak9154Sensor.getBusBatteryPercent(); + } #endif - float v = getBattVoltage(); + float v = getBattVoltage(); - if (v < noBatVolt) - return -1; // If voltage is super low assume no battery installed + if (v < noBatVolt) + return -1; // If voltage is super low assume no battery installed #ifdef NO_BATTERY_LEVEL_ON_CHARGE - // This does not work on a RAK4631 with battery connected - if (v > chargingVolt) - return 0; // While charging we can't report % full on the battery + // This does not work on a RAK4631 with battery connected + if (v > chargingVolt) + return 0; // While charging we can't report % full on the battery #endif - /** - * @brief Battery voltage lookup table interpolation to obtain a more - * precise percentage rather than the old proportional one. - * @author Gabriele Russo - * @date 06/02/2024 - */ - float battery_SOC = 0.0; - uint16_t voltage = v / NUM_CELLS; // single cell voltage (average) - for (int i = 0; i < NUM_OCV_POINTS; i++) { - if (OCV[i] <= voltage) { - if (i == 0) { - battery_SOC = 100.0; // 100% full - } else { - // interpolate between OCV[i] and OCV[i-1] - battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) * - (NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i])); - } - break; - } - } -#if defined(BATTERY_CHARGING_INV) - // bit of trickery to show 99% up until the charge finishes - if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99) - battery_SOC = 99; -#endif - return clamp((int)(battery_SOC), 0, 100); - } - /** - * The raw voltage of the batteryin millivolts or NAN if unknown + * @brief Battery voltage lookup table interpolation to obtain a more + * precise percentage rather than the old proportional one. + * @author Gabriele Russo + * @date 06/02/2024 */ - virtual uint16_t getBattVoltage() override - { + float battery_SOC = 0.0; + uint16_t voltage = v / NUM_CELLS; // single cell voltage (average) + for (int i = 0; i < NUM_OCV_POINTS; i++) { + if (OCV[i] <= voltage) { + if (i == 0) { + battery_SOC = 100.0; // 100% full + } else { + // interpolate between OCV[i] and OCV[i-1] + battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) * (NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i])); + } + break; + } + } +#if defined(BATTERY_CHARGING_INV) + // bit of trickery to show 99% up until the charge finishes + if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99) + battery_SOC = 99; +#endif + return clamp((int)(battery_SOC), 0, 100); + } + + /** + * The raw voltage of the batteryin millivolts or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { #if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (hasRAK()) { - return getRAKVoltage(); - } + if (hasRAK()) { + return getRAKVoltage(); + } #endif #if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (hasINA()) { - return getINAVoltage(); - } + if (hasINA()) { + return getINAVoltage(); + } #endif #ifndef ADC_MULTIPLIER @@ -310,314 +303,294 @@ class AnalogBatteryLevel : public HasBatteryLevel #endif #ifndef BATTERY_SENSE_SAMPLES -#define BATTERY_SENSE_SAMPLES \ - 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment. +#define BATTERY_SENSE_SAMPLES 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment. #endif #ifdef BATTERY_PIN - // Override variant or default ADC_MULTIPLIER if we have the override pref - float operativeAdcMultiplier = - config.power.adc_multiplier_override > 0 ? config.power.adc_multiplier_override : ADC_MULTIPLIER; - // Do not call analogRead() often. - const uint32_t min_read_interval = 5000; - if (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) { - last_read_time_ms = millis(); + // Override variant or default ADC_MULTIPLIER if we have the override pref + float operativeAdcMultiplier = config.power.adc_multiplier_override > 0 ? config.power.adc_multiplier_override : ADC_MULTIPLIER; + // Do not call analogRead() often. + const uint32_t min_read_interval = 5000; + if (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) { + last_read_time_ms = millis(); - uint32_t raw = 0; - float scaled = 0; + uint32_t raw = 0; + float scaled = 0; - battery_adcEnable(); + battery_adcEnable(); #ifdef ARCH_ESP32 // ADC block for espressif platforms - raw = espAdcRead(); - scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); - scaled *= operativeAdcMultiplier; + raw = espAdcRead(); + scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); + scaled *= operativeAdcMultiplier; #else // block for all other platforms - for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - raw += analogRead(BATTERY_PIN); - } - raw = raw / BATTERY_SENSE_SAMPLES; - scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw; + for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + raw += analogRead(BATTERY_PIN); + } + raw = raw / BATTERY_SENSE_SAMPLES; + scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw; #endif - battery_adcDisable(); + battery_adcDisable(); - if (!initial_read_done) { - // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct - if (scaled > last_read_value) - last_read_value = scaled; - initial_read_done = true; - } else { - // Already initialized - filter this reading - last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF - } + if (!initial_read_done) { + // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct + if (scaled > last_read_value) + last_read_value = scaled; + initial_read_done = true; + } else { + // Already initialized - filter this reading + last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF + } - // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) - // (last_read_value)); - } - return last_read_value; -#endif // BATTERY_PIN - return 0; + // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) + // (last_read_value)); } + return last_read_value; +#endif // BATTERY_PIN + return 0; + } #if defined(ARCH_ESP32) && !defined(HAS_PMU) && defined(BATTERY_PIN) - /** - * ESP32 specific function for getting calibrated ADC reads - */ - uint32_t espAdcRead() - { + /** + * ESP32 specific function for getting calibrated ADC reads + */ + uint32_t espAdcRead() { - uint32_t raw = 0; - uint8_t raw_c = 0; // raw reading counter + uint32_t raw = 0; + uint8_t raw_c = 0; // raw reading counter #ifndef BAT_MEASURE_ADC_UNIT // ADC1 - for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - int val_ = adc1_get_raw(adc_channel); - if (val_ >= 0) { // save only valid readings - raw += val_; - raw_c++; - } - // delayMicroseconds(100); - } + for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + int val_ = adc1_get_raw(adc_channel); + if (val_ >= 0) { // save only valid readings + raw += val_; + raw_c++; + } + // delayMicroseconds(100); + } #else // ADC2 #ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 - // ADC2 wifi bug workaround not required, breaks compile - // On ESP32S3, ADC2 can take turns with Wifi (?) + // ADC2 wifi bug workaround not required, breaks compile + // On ESP32S3, ADC2 can take turns with Wifi (?) - int32_t adc_buf; - esp_err_t read_result; + int32_t adc_buf; + esp_err_t read_result; - // Multiple samples - for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - adc_buf = 0; - read_result = -1; + // Multiple samples + for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + adc_buf = 0; + read_result = -1; - read_result = adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); - if (read_result == ESP_OK) { - raw += adc_buf; - raw_c++; // Count valid samples - } else { - LOG_DEBUG("An attempt to sample ADC2 failed"); - } - } + read_result = adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); + if (read_result == ESP_OK) { + raw += adc_buf; + raw_c++; // Count valid samples + } else { + LOG_DEBUG("An attempt to sample ADC2 failed"); + } + } #else // Other ESP32 - int32_t adc_buf = 0; - for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - // ADC2 wifi bug workaround, see - // https://github.com/espressif/arduino-esp32/issues/102 - WRITE_PERI_REG(SENS_SAR_READ_CTRL2_REG, RTC_reg_b); - SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV); - adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); - raw += adc_buf; - raw_c++; - } + int32_t adc_buf = 0; + for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + // ADC2 wifi bug workaround, see + // https://github.com/espressif/arduino-esp32/issues/102 + WRITE_PERI_REG(SENS_SAR_READ_CTRL2_REG, RTC_reg_b); + SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV); + adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); + raw += adc_buf; + raw_c++; + } #endif // BAT_MEASURE_ADC_UNIT #endif // End BAT_MEASURE_ADC_UNIT - return (raw / (raw_c < 1 ? 1 : raw_c)); - } + return (raw / (raw_c < 1 ? 1 : raw_c)); + } #endif - /** - * return true if there is a battery installed in this unit - */ - // if we have a integrated device with a battery, we can assume that the battery is always connected + /** + * return true if there is a battery installed in this unit + */ + // if we have a integrated device with a battery, we can assume that the battery is always connected #ifdef BATTERY_IMMUTABLE - virtual bool isBatteryConnect() override { return true; } + virtual bool isBatteryConnect() override { return true; } #elif defined(ADC_V) - virtual bool isBatteryConnect() override - { - int lastReading = digitalRead(ADC_V); - // 判断值是否变化 - for (int i = 2; i < 500; i++) { - int reading = digitalRead(ADC_V); - if (reading != lastReading) { - return false; // 有变化,USB供电, 没接电池 - } - } - - return true; + virtual bool isBatteryConnect() override { + int lastReading = digitalRead(ADC_V); + // 判断值是否变化 + for (int i = 2; i < 500; i++) { + int reading = digitalRead(ADC_V); + if (reading != lastReading) { + return false; // 有变化,USB供电, 没接电池 + } } + + return true; + } #else - virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } + virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power - /// On some boards we don't have the power management chip (like AXPxxxx) - /// so we use EXT_PWR_DETECT GPIO pin to detect external power source - virtual bool isVbusIn() override - { + /// If we see a battery voltage higher than physics allows - assume charger is pumping + /// in power + /// On some boards we don't have the power management chip (like AXPxxxx) + /// so we use EXT_PWR_DETECT GPIO pin to detect external power source + virtual bool isVbusIn() override { #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - // if external powered that pin will be pulled down - if (digitalRead(EXT_PWR_DETECT) == LOW) { - return true; - } - // if it's not LOW - check the battery + // if external powered that pin will be pulled down + if (digitalRead(EXT_PWR_DETECT) == LOW) { + return true; + } + // if it's not LOW - check the battery #else - // if external powered that pin will be pulled up - if (digitalRead(EXT_PWR_DETECT) == HIGH) { - return true; - } - // if it's not HIGH - check the battery + // if external powered that pin will be pulled up + if (digitalRead(EXT_PWR_DETECT) == HIGH) { + return true; + } + // if it's not HIGH - check the battery #endif #elif defined(MUZI_BASE) - return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; + return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; #endif - return getBattVoltage() > chargingVolt; - } + return getBattVoltage() > chargingVolt; + } - /// Assume charging if we have a battery and external power is connected. - /// we can't be smart enough to say 'full'? - virtual bool isCharging() override - { + /// Assume charging if we have a battery and external power is connected. + /// we can't be smart enough to say 'full'? + virtual bool isCharging() override { #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) - if (hasRAK()) { - return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; - } + if (hasRAK()) { + return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; + } #endif #ifdef EXT_CHRG_DETECT - return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #elif defined(BATTERY_CHARGING_INV) - return !digitalRead(BATTERY_CHARGING_INV); + return !digitalRead(BATTERY_CHARGING_INV); #else #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) - if (hasINA()) { - // get current flow from INA sensor - negative value means power flowing into the battery - // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD - LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); + if (hasINA()) { + // get current flow from INA sensor - negative value means power flowing into the battery + // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD + LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); #if defined(INA_CHARGING_DETECTION_INVERT) - return getINACurrent() > 0; + return getINACurrent() > 0; #else - return getINACurrent() < 0; + return getINACurrent() < 0; #endif - } - return isBatteryConnect() && isVbusIn(); -#endif -#endif - // by default, we check the battery voltage only - return isVbusIn(); } + return isBatteryConnect() && isVbusIn(); +#endif +#endif + // by default, we check the battery voltage only + return isVbusIn(); + } - private: - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power +private: + /// If we see a battery voltage higher than physics allows - assume charger is pumping + /// in power - /// For heltecs with no battery connected, the measured voltage is 2204, so - // need to be higher than that, in this case is 2500mV (3000-500) - const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY}; - const float chargingVolt = (OCV[0] + 10) * NUM_CELLS; - const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; - // Start value from minimum voltage for the filter to not start from 0 - // that could trigger some events. - // This value is over-written by the first ADC reading, it the voltage seems reasonable. - bool initial_read_done = false; - float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); - uint32_t last_read_time_ms = 0; + /// For heltecs with no battery connected, the measured voltage is 2204, so + // need to be higher than that, in this case is 2500mV (3000-500) + const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY}; + const float chargingVolt = (OCV[0] + 10) * NUM_CELLS; + const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; + // Start value from minimum voltage for the filter to not start from 0 + // that could trigger some events. + // This value is over-written by the first ADC reading, it the voltage seems reasonable. + bool initial_read_done = false; + float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); + uint32_t last_read_time_ms = 0; #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) - uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } + uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } - bool hasRAK() - { - if (!rak9154Sensor.isInitialized()) - return rak9154Sensor.runOnce() > 0; - return rak9154Sensor.isRunning(); - } + bool hasRAK() { + if (!rak9154Sensor.isInitialized()) + return rak9154Sensor.runOnce() > 0; + return rak9154Sensor.isRunning(); + } #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - uint16_t getINAVoltage() - { - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { - return ina219Sensor.getBusVoltageMv(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == - config.power.device_battery_ina_address) { - return ina226Sensor.getBusVoltageMv(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == - config.power.device_battery_ina_address) { - return ina260Sensor.getBusVoltageMv(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == - config.power.device_battery_ina_address) { - return ina3221Sensor.getBusVoltageMv(); - } - return 0; + uint16_t getINAVoltage() { + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + return ina219Sensor.getBusVoltageMv(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { + return ina226Sensor.getBusVoltageMv(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == config.power.device_battery_ina_address) { + return ina260Sensor.getBusVoltageMv(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { + return ina3221Sensor.getBusVoltageMv(); } + return 0; + } - int16_t getINACurrent() - { - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { - return ina219Sensor.getCurrentMa(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == - config.power.device_battery_ina_address) { - return ina226Sensor.getCurrentMa(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == - config.power.device_battery_ina_address) { - return ina3221Sensor.getCurrentMa(); - } - return 0; + int16_t getINACurrent() { + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + return ina219Sensor.getCurrentMa(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { + return ina226Sensor.getCurrentMa(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { + return ina3221Sensor.getCurrentMa(); } + return 0; + } - bool hasINA() - { - if (!config.power.device_battery_ina_address) { - return false; - } - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { - if (!ina219Sensor.isInitialized()) - return ina219Sensor.runOnce() > 0; - return ina219Sensor.isRunning(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == - config.power.device_battery_ina_address) { - if (!ina226Sensor.isInitialized()) - return ina226Sensor.runOnce() > 0; - return ina226Sensor.isRunning(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == - config.power.device_battery_ina_address) { - if (!ina260Sensor.isInitialized()) - return ina260Sensor.runOnce() > 0; - return ina260Sensor.isRunning(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == - config.power.device_battery_ina_address) { - if (!ina3221Sensor.isInitialized()) - return ina3221Sensor.runOnce() > 0; - return ina3221Sensor.isRunning(); - } - return false; + bool hasINA() { + if (!config.power.device_battery_ina_address) { + return false; } + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + if (!ina219Sensor.isInitialized()) + return ina219Sensor.runOnce() > 0; + return ina219Sensor.isRunning(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { + if (!ina226Sensor.isInitialized()) + return ina226Sensor.runOnce() > 0; + return ina226Sensor.isRunning(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == config.power.device_battery_ina_address) { + if (!ina260Sensor.isInitialized()) + return ina260Sensor.runOnce() > 0; + return ina260Sensor.isRunning(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { + if (!ina3221Sensor.isInitialized()) + return ina3221Sensor.runOnce() > 0; + return ina3221Sensor.isRunning(); + } + return false; + } #endif }; static AnalogBatteryLevel analogLevel; -Power::Power() : OSThread("Power") -{ - statusHandler = {}; - low_voltage_counter = 0; +Power::Power() : OSThread("Power") { + statusHandler = {}; + low_voltage_counter = 0; #ifdef DEBUG_HEAP - lastheap = memGet.getFreeHeap(); + lastheap = memGet.getFreeHeap(); #endif } -bool Power::analogInit() -{ +bool Power::analogInit() { #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - pinMode(EXT_PWR_DETECT, INPUT_PULLUP); + pinMode(EXT_PWR_DETECT, INPUT_PULLUP); #else - pinMode(EXT_PWR_DETECT, INPUT); + pinMode(EXT_PWR_DETECT, INPUT); #endif #endif #ifdef EXT_CHRG_DETECT - pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); + pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); #endif #ifdef BATTERY_PIN - LOG_DEBUG("Use analog input %d for battery level", BATTERY_PIN); + LOG_DEBUG("Use analog input %d for battery level", BATTERY_PIN); - // disable any internal pullups - pinMode(BATTERY_PIN, INPUT); + // disable any internal pullups + pinMode(BATTERY_PIN, INPUT); #ifndef BATTERY_SENSE_RESOLUTION_BITS #define BATTERY_SENSE_RESOLUTION_BITS 10 @@ -626,56 +599,56 @@ bool Power::analogInit() #ifdef ARCH_ESP32 // ESP32 needs special analog stuff #ifndef ADC_WIDTH // max resolution by default - static const adc_bits_width_t width = ADC_WIDTH_BIT_12; + static const adc_bits_width_t width = ADC_WIDTH_BIT_12; #else - static const adc_bits_width_t width = ADC_WIDTH; + static const adc_bits_width_t width = ADC_WIDTH; #endif #ifndef BAT_MEASURE_ADC_UNIT // ADC1 - adc1_config_width(width); - adc1_config_channel_atten(adc_channel, atten); + adc1_config_width(width); + adc1_config_channel_atten(adc_channel, atten); #else // ADC2 - adc2_config_channel_atten(adc_channel, atten); + adc2_config_channel_atten(adc_channel, atten); #ifndef CONFIG_IDF_TARGET_ESP32S3 - // ADC2 wifi bug workaround - // Not required with ESP32S3, breaks compile - RTC_reg_b = READ_PERI_REG(SENS_SAR_READ_CTRL2_REG); + // ADC2 wifi bug workaround + // Not required with ESP32S3, breaks compile + RTC_reg_b = READ_PERI_REG(SENS_SAR_READ_CTRL2_REG); #endif #endif - // calibrate ADC - esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_characs); - // show ADC characterization base - if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { - LOG_INFO("ADC config based on Two Point values stored in eFuse"); - } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { - LOG_INFO("ADC config based on reference voltage stored in eFuse"); - } + // calibrate ADC + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_characs); + // show ADC characterization base + if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + LOG_INFO("ADC config based on Two Point values stored in eFuse"); + } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + LOG_INFO("ADC config based on reference voltage stored in eFuse"); + } #ifdef CONFIG_IDF_TARGET_ESP32S3 - // ESP32S3 - else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { - LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse"); - } + // ESP32S3 + else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { + LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse"); + } #endif - else { - LOG_INFO("ADC config based on default reference voltage"); - } + else { + LOG_INFO("ADC config based on default reference voltage"); + } #endif // ARCH_ESP32 #ifdef ARCH_NRF52 #ifdef VBAT_AR_INTERNAL - analogReference(VBAT_AR_INTERNAL); + analogReference(VBAT_AR_INTERNAL); #else - analogReference(AR_INTERNAL); // 3.6V + analogReference(AR_INTERNAL); // 3.6V #endif #endif // ARCH_NRF52 #ifndef ARCH_ESP32 - analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); + analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); #endif - batteryLevel = &analogLevel; - return true; + batteryLevel = &analogLevel; + return true; #else - return false; + return false; #endif } @@ -684,308 +657,302 @@ bool Power::analogInit() * * @return true if the setup was successful, false otherwise. */ -bool Power::setup() -{ - bool found = false; - if (axpChipInit()) { - found = true; - } else if (lipoInit()) { - found = true; - } else if (lipoChargerInit()) { - found = true; - } else if (meshSolarInit()) { - found = true; - } else if (analogInit()) { - found = true; - } +bool Power::setup() { + bool found = false; + if (axpChipInit()) { + found = true; + } else if (lipoInit()) { + found = true; + } else if (lipoChargerInit()) { + found = true; + } else if (meshSolarInit()) { + found = true; + } else if (analogInit()) { + found = true; + } #ifdef NRF_APM - found = true; + found = true; #endif #ifdef EXT_PWR_DETECT - attachInterrupt( - EXT_PWR_DETECT, - []() { - power->setIntervalFromNow(0); - runASAP = true; - }, - CHANGE); + attachInterrupt( + EXT_PWR_DETECT, + []() { + power->setIntervalFromNow(0); + runASAP = true; + }, + CHANGE); #endif #ifdef BATTERY_CHARGING_INV - attachInterrupt( - BATTERY_CHARGING_INV, - []() { - power->setIntervalFromNow(0); - runASAP = true; - }, - CHANGE); + attachInterrupt( + BATTERY_CHARGING_INV, + []() { + power->setIntervalFromNow(0); + runASAP = true; + }, + CHANGE); #endif - enabled = found; - low_voltage_counter = 0; + enabled = found; + low_voltage_counter = 0; - return found; + return found; } -void Power::powerCommandsCheck() -{ - if (rebootAtMsec && millis() > rebootAtMsec) { - LOG_INFO("Rebooting"); - reboot(); - } +void Power::powerCommandsCheck() { + if (rebootAtMsec && millis() > rebootAtMsec) { + LOG_INFO("Rebooting"); + reboot(); + } - if (shutdownAtMsec && millis() > shutdownAtMsec) { - shutdownAtMsec = 0; - shutdown(); - } + if (shutdownAtMsec && millis() > shutdownAtMsec) { + shutdownAtMsec = 0; + shutdown(); + } } -void Power::reboot() -{ - notifyReboot.notifyObservers(NULL); +void Power::reboot() { + notifyReboot.notifyObservers(NULL); #if defined(ARCH_ESP32) - ESP.restart(); + ESP.restart(); #elif defined(ARCH_NRF52) - NVIC_SystemReset(); + NVIC_SystemReset(); #elif defined(ARCH_RP2040) - rp2040.reboot(); + rp2040.reboot(); #elif defined(ARCH_PORTDUINO) - deInitApiServer(); - if (aLinuxInputImpl) - aLinuxInputImpl->deInit(); - SPI.end(); - Wire.end(); - Serial1.end(); - if (screen) { - delete screen; - screen = nullptr; - } - LOG_DEBUG("final reboot!"); - ::reboot(); + deInitApiServer(); + if (aLinuxInputImpl) + aLinuxInputImpl->deInit(); + SPI.end(); + Wire.end(); + Serial1.end(); + if (screen) { + delete screen; + screen = nullptr; + } + LOG_DEBUG("final reboot!"); + ::reboot(); #elif defined(ARCH_STM32WL) - HAL_NVIC_SystemReset(); + HAL_NVIC_SystemReset(); #else - rebootAtMsec = -1; - LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied"); + rebootAtMsec = -1; + LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied"); #endif } -void Power::shutdown() -{ +void Power::shutdown() { #if HAS_SCREEN - if (screen) { + if (screen) { #ifdef T_DECK_PRO - screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button #elif defined(USE_EINK) - screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen + screen->showSimpleBanner("Shutting Down...", + 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen #else - screen->showSimpleBanner("Shutting Down...", 0); // stays on screen + screen->showSimpleBanner("Shutting Down...", 0); // stays on screen #endif - } + } #endif #if !defined(ARCH_STM32WL) - playShutdownMelody(); + playShutdownMelody(); #endif - nodeDB->saveToDisk(); + nodeDB->saveToDisk(); #if HAS_SCREEN - messageStore.saveToFlash(); + messageStore.saveToFlash(); #endif #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #ifdef PIN_LED1 - ledOff(PIN_LED1); + ledOff(PIN_LED1); #endif #ifdef PIN_LED2 - ledOff(PIN_LED2); + ledOff(PIN_LED2); #endif #ifdef PIN_LED3 - ledOff(PIN_LED3); + ledOff(PIN_LED3); #endif - doDeepSleep(DELAY_FOREVER, true, true); + doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) - exit(EXIT_SUCCESS); + exit(EXIT_SUCCESS); #else - LOG_WARN("FIXME implement shutdown for this platform"); + LOG_WARN("FIXME implement shutdown for this platform"); #endif } /// Reads power status to powerStatus singleton. // // TODO(girts): move this and other axp stuff to power.h/power.cpp. -void Power::readPowerStatus() -{ - int32_t batteryVoltageMv = -1; // Assume unknown - int8_t batteryChargePercent = -1; - OptionalBool usbPowered = OptUnknown; - OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time - OptionalBool isChargingNow = OptUnknown; +void Power::readPowerStatus() { + int32_t batteryVoltageMv = -1; // Assume unknown + int8_t batteryChargePercent = -1; + OptionalBool usbPowered = OptUnknown; + OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time + OptionalBool isChargingNow = OptUnknown; - if (batteryLevel) { - hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; - usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; - isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; - if (hasBattery) { - batteryVoltageMv = batteryLevel->getBattVoltage(); - // If the AXP192 returns a valid battery percentage, use it - if (batteryLevel->getBatteryPercent() >= 0) { - batteryChargePercent = batteryLevel->getBatteryPercent(); - } else { - // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error - // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined - // in power.h - batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / - ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), - 0, 100); - } - } + if (batteryLevel) { + hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; + usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; + isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; + if (hasBattery) { + batteryVoltageMv = batteryLevel->getBattVoltage(); + // If the AXP192 returns a valid battery percentage, use it + if (batteryLevel->getBatteryPercent() >= 0) { + batteryChargePercent = batteryLevel->getBatteryPercent(); + } else { + // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error + // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined + // in power.h + batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / + ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), + 0, 100); + } } + } -// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass -// (which shares a superclass with the BatteryLevel stuff) -// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current -// practice. -#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect - // changes. +// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered +// subclass (which shares a superclass with the BatteryLevel stuff) that just provides a few methods. But in the +// interest of fixing this bug I'm going to follow current practice. +#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so + // to detect changes. - nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); - // LOG_DEBUG("NRF Power %d", nrf_usb_state); + nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); + // LOG_DEBUG("NRF Power %d", nrf_usb_state); - // If changed to DISCONNECTED - if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED) - isChargingNow = usbPowered = OptFalse; - // If changed to CONNECTED / READY - else - isChargingNow = usbPowered = OptTrue; + // If changed to DISCONNECTED + if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED) + isChargingNow = usbPowered = OptFalse; + // If changed to CONNECTED / READY + else + isChargingNow = usbPowered = OptTrue; #endif - // Notify any status instances that are observing us - const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent); - if (millis() > lastLogTime + 50 * 1000) { - LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), - powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); - lastLogTime = millis(); - } - newStatus.notifyObservers(&powerStatus2); + // Notify any status instances that are observing us + const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent); + if (millis() > lastLogTime + 50 * 1000) { + LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), powerStatus2.getIsCharging(), + powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); + lastLogTime = millis(); + } + newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP - if (lastheap != memGet.getFreeHeap()) { - // Use stack-allocated buffer to avoid heap allocations in monitoring code - char threadlist[256] = "Threads running:"; - int threadlistLen = strlen(threadlist); - int running = 0; - for (int i = 0; i < MAX_THREADS; i++) { - auto thread = concurrency::mainController.get(i); - if ((thread != nullptr) && (thread->enabled)) { - // Use snprintf to safely append to stack buffer without heap allocation - int remaining = sizeof(threadlist) - threadlistLen - 1; - if (remaining > 0) { - int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); - if (written > 0 && written < remaining) { - threadlistLen += written; - } - } - running++; - } + if (lastheap != memGet.getFreeHeap()) { + // Use stack-allocated buffer to avoid heap allocations in monitoring code + char threadlist[256] = "Threads running:"; + int threadlistLen = strlen(threadlist); + int running = 0; + for (int i = 0; i < MAX_THREADS; i++) { + auto thread = concurrency::mainController.get(i); + if ((thread != nullptr) && (thread->enabled)) { + // Use snprintf to safely append to stack buffer without heap allocation + int remaining = sizeof(threadlist) - threadlistLen - 1; + if (remaining > 0) { + int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); + if (written > 0 && written < remaining) { + threadlistLen += written; + } } - LOG_HEAP(threadlist); - LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), - memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); - lastheap = memGet.getFreeHeap(); + running++; + } } + LOG_HEAP(threadlist); + LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreeHeap() - lastheap, + running, concurrency::mainController.size(false)); + lastheap = memGet.getFreeHeap(); + } #ifdef DEBUG_HEAP_MQTT - if (mqtt) { - // send MQTT-Packet with Heap-Size - uint8_t dmac[6]; - getMacAddr(dmac); // Get our hardware ID - char mac[18]; - sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); + if (mqtt) { + // send MQTT-Packet with Heap-Size + uint8_t dmac[6]; + getMacAddr(dmac); // Get our hardware ID + char mac[18]; + sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); - auto newHeap = memGet.getFreeHeap(); - // Use stack-allocated buffers to avoid heap allocations in monitoring code - char heapTopic[128]; - snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); - char heapString[16]; - snprintf(heapString, sizeof(heapString), "%u", newHeap); - mqtt->pubSub.publish(heapTopic, heapString, false); + auto newHeap = memGet.getFreeHeap(); + // Use stack-allocated buffers to avoid heap allocations in monitoring code + char heapTopic[128]; + snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char heapString[16]; + snprintf(heapString, sizeof(heapString), "%u", newHeap); + mqtt->pubSub.publish(heapTopic, heapString, false); - auto wifiRSSI = WiFi.RSSI(); - char wifiTopic[128]; - snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); - char wifiString[16]; - snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); - mqtt->pubSub.publish(wifiTopic, wifiString, false); - } + auto wifiRSSI = WiFi.RSSI(); + char wifiTopic[128]; + snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char wifiString[16]; + snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); + mqtt->pubSub.publish(wifiTopic, wifiString, false); + } #endif #endif - // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in - // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. - // + // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in + // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. + // - if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { - if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { - low_voltage_counter++; - LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); - if (low_voltage_counter > 10) { - LOG_INFO("Low voltage detected, trigger deep sleep"); - powerFSM.trigger(EVENT_LOW_BATTERY); - } - } else { - low_voltage_counter = 0; - } + if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { + if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { + low_voltage_counter++; + LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); + if (low_voltage_counter > 10) { + LOG_INFO("Low voltage detected, trigger deep sleep"); + powerFSM.trigger(EVENT_LOW_BATTERY); + } + } else { + low_voltage_counter = 0; } + } } -int32_t Power::runOnce() -{ - readPowerStatus(); +int32_t Power::runOnce() { + readPowerStatus(); #ifdef HAS_PMU - // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll - // the IRQ status by reading the registers over I2C - if (PMU) { + // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll + // the IRQ status by reading the registers over I2C + if (PMU) { - PMU->getIrqStatus(); + PMU->getIrqStatus(); - if (PMU->isVbusRemoveIrq()) { - LOG_INFO("USB unplugged"); - powerFSM.trigger(EVENT_POWER_DISCONNECTED); - } + if (PMU->isVbusRemoveIrq()) { + LOG_INFO("USB unplugged"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } - if (PMU->isVbusInsertIrq()) { - LOG_INFO("USB plugged In"); - powerFSM.trigger(EVENT_POWER_CONNECTED); - } + if (PMU->isVbusInsertIrq()) { + LOG_INFO("USB plugged In"); + powerFSM.trigger(EVENT_POWER_CONNECTED); + } - /* - Other things we could check if we cared... + /* + Other things we could check if we cared... - if (PMU->isBatChagerStartIrq()) { - LOG_DEBUG("Battery start charging"); - } - if (PMU->isBatChagerDoneIrq()) { - LOG_DEBUG("Battery fully charged"); - } - if (PMU->isBatInsertIrq()) { - LOG_DEBUG("Battery inserted"); - } - if (PMU->isBatRemoveIrq()) { - LOG_DEBUG("Battery removed"); - } - */ + if (PMU->isBatChagerStartIrq()) { + LOG_DEBUG("Battery start charging"); + } + if (PMU->isBatChagerDoneIrq()) { + LOG_DEBUG("Battery fully charged"); + } + if (PMU->isBatInsertIrq()) { + LOG_DEBUG("Battery inserted"); + } + if (PMU->isBatRemoveIrq()) { + LOG_DEBUG("Battery removed"); + } + */ #ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? - if (PMU->isPekeyLongPressIrq()) { - LOG_DEBUG("PEK long button press"); - if (screen) - screen->setOn(false); - } -#endif - - PMU->clearIrqStatus(); + if (PMU->isPekeyLongPressIrq()) { + LOG_DEBUG("PEK long button press"); + if (screen) + screen->setOn(false); } #endif - // Only read once every 20 seconds once the power status for the app has been initialized - return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; + + PMU->clearIrqStatus(); + } +#endif + // Only read once every 20 seconds once the power status for the app has been initialized + return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; } /** @@ -993,251 +960,237 @@ int32_t Power::runOnce() * * axp192 power DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the - axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this - on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of - days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS + axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep + this on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for + a couple of days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS * */ -bool Power::axpChipInit() -{ +bool Power::axpChipInit() { #ifdef HAS_PMU - TwoWire *w = NULL; + TwoWire *w = NULL; - // Use macro to distinguish which wire is used by PMU + // Use macro to distinguish which wire is used by PMU #ifdef PMU_USE_WIRE1 - w = &Wire1; + w = &Wire1; #else - w = &Wire; + w = &Wire; #endif - /** - * It is not necessary to specify the wire pin, - * just input the wire, because the wire has been initialized in main.cpp - */ - if (!PMU) { - PMU = new XPowersAXP2101(*w); - if (!PMU->init()) { - LOG_WARN("No AXP2101 power management"); - delete PMU; - PMU = NULL; - } else { - LOG_INFO("AXP2101 PMU init succeeded"); - } + /** + * It is not necessary to specify the wire pin, + * just input the wire, because the wire has been initialized in main.cpp + */ + if (!PMU) { + PMU = new XPowersAXP2101(*w); + if (!PMU->init()) { + LOG_WARN("No AXP2101 power management"); + delete PMU; + PMU = NULL; + } else { + LOG_INFO("AXP2101 PMU init succeeded"); } + } - if (!PMU) { - PMU = new XPowersAXP192(*w); - if (!PMU->init()) { - LOG_WARN("No AXP192 power management"); - delete PMU; - PMU = NULL; - } else { - LOG_INFO("AXP192 PMU init succeeded"); - } + if (!PMU) { + PMU = new XPowersAXP192(*w); + if (!PMU->init()) { + LOG_WARN("No AXP192 power management"); + delete PMU; + PMU = NULL; + } else { + LOG_INFO("AXP192 PMU init succeeded"); } + } - if (!PMU) { - /* - * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time. - * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once, - * if there are multiple devices sharing the bus. - * * */ + if (!PMU) { + /* + * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time. + * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized + * once, if there are multiple devices sharing the bus. + * * */ #ifndef PMU_USE_WIRE1 - w->begin(I2C_SDA, I2C_SCL); + w->begin(I2C_SDA, I2C_SCL); #endif - return false; - } + return false; + } - batteryLevel = PMU; + batteryLevel = PMU; - if (PMU->getChipModel() == XPOWERS_AXP192) { + if (PMU->getChipModel() == XPOWERS_AXP192) { - // lora radio power channel - PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); - PMU->enablePowerOutput(XPOWERS_LDO2); + // lora radio power channel + PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); + PMU->enablePowerOutput(XPOWERS_LDO2); - // oled module power channel, - // disable it will cause abnormal communication between boot and AXP power supply, - // do not turn it off - PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); - // enable oled power - PMU->enablePowerOutput(XPOWERS_DCDC1); + // oled module power channel, + // disable it will cause abnormal communication between boot and AXP power supply, + // do not turn it off + PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); + // enable oled power + PMU->enablePowerOutput(XPOWERS_DCDC1); - // gnss module power channel - now turned on in setGpsPower - PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); - // PMU->enablePowerOutput(XPOWERS_LDO3); + // gnss module power channel - now turned on in setGpsPower + PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); + // PMU->enablePowerOutput(XPOWERS_LDO3); - // protected oled power source - PMU->setProtectedChannel(XPOWERS_DCDC1); - // protected esp32 power source - PMU->setProtectedChannel(XPOWERS_DCDC3); + // protected oled power source + PMU->setProtectedChannel(XPOWERS_DCDC1); + // protected esp32 power source + PMU->setProtectedChannel(XPOWERS_DCDC3); - // disable not use channel - PMU->disablePowerOutput(XPOWERS_DCDC2); + // disable not use channel + PMU->disablePowerOutput(XPOWERS_DCDC2); - // disable all axp chip interrupt - PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); + // disable all axp chip interrupt + PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); - // Set constant current charging current - PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA); + // Set constant current charging current + PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA); - // Set up the charging voltage - PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); - } else if (PMU->getChipModel() == XPOWERS_AXP2101) { + // Set up the charging voltage + PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); + } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/ - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - // Unuse power channel - PMU->disablePowerOutput(XPOWERS_DCDC2); - PMU->disablePowerOutput(XPOWERS_DCDC3); - PMU->disablePowerOutput(XPOWERS_DCDC4); - PMU->disablePowerOutput(XPOWERS_DCDC5); - PMU->disablePowerOutput(XPOWERS_ALDO1); - PMU->disablePowerOutput(XPOWERS_ALDO4); - PMU->disablePowerOutput(XPOWERS_BLDO1); - PMU->disablePowerOutput(XPOWERS_BLDO2); - PMU->disablePowerOutput(XPOWERS_DLDO1); - PMU->disablePowerOutput(XPOWERS_DLDO2); + /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/ + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + // Unuse power channel + PMU->disablePowerOutput(XPOWERS_DCDC2); + PMU->disablePowerOutput(XPOWERS_DCDC3); + PMU->disablePowerOutput(XPOWERS_DCDC4); + PMU->disablePowerOutput(XPOWERS_DCDC5); + PMU->disablePowerOutput(XPOWERS_ALDO1); + PMU->disablePowerOutput(XPOWERS_ALDO4); + PMU->disablePowerOutput(XPOWERS_BLDO1); + PMU->disablePowerOutput(XPOWERS_BLDO2); + PMU->disablePowerOutput(XPOWERS_DLDO1); + PMU->disablePowerOutput(XPOWERS_DLDO2); - // GNSS RTC PowerVDD 3300mV - PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); - PMU->enablePowerOutput(XPOWERS_VBACKUP); + // GNSS RTC PowerVDD 3300mV + PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); + PMU->enablePowerOutput(XPOWERS_VBACKUP); - // ESP32 VDD 3300mV - // ! No need to set, automatically open , Don't close it - // PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); - // PMU->setProtectedChannel(XPOWERS_DCDC1); + // ESP32 VDD 3300mV + // ! No need to set, automatically open , Don't close it + // PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); + // PMU->setProtectedChannel(XPOWERS_DCDC1); - // LoRa VDD 3300mV - PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO2); + // LoRa VDD 3300mV + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO2); - // GNSS VDD 3300mV - PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO3); - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || - HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { - // t-beam s3 core - /** - * gnss module power channel - * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during - * initialization - */ - PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO4); + // GNSS VDD 3300mV + PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO3); + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { + // t-beam s3 core + /** + * gnss module power channel + * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during + * initialization + */ + PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO4); - // lora radio power channel - PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO3); + // lora radio power channel + PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO3); - // m.2 interface - PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300); - PMU->enablePowerOutput(XPOWERS_DCDC3); + // m.2 interface + PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300); + PMU->enablePowerOutput(XPOWERS_DCDC3); - /** - * ALDO2 cannot be turned off. - * It is a necessary condition for sensor communication. - * It must be turned on to properly access the sensor and screen - * It is also responsible for the power supply of PCF8563 - */ - PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO2); + /** + * ALDO2 cannot be turned off. + * It is a necessary condition for sensor communication. + * It must be turned on to properly access the sensor and screen + * It is also responsible for the power supply of PCF8563 + */ + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO2); - // 6-axis , magnetometer ,bme280 , oled screen power channel - PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO1); + // 6-axis , magnetometer ,bme280 , oled screen power channel + PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO1); - // sdcard power channel - PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); - PMU->enablePowerOutput(XPOWERS_BLDO1); + // sdcard power channel + PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO1); #ifdef T_WATCH_S3 - // DRV2605 power channel - PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); - PMU->enablePowerOutput(XPOWERS_BLDO2); + // DRV2605 power channel + PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO2); #endif - // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); - // PMU->enablePowerOutput(XPOWERS_DCDC4); + // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); + // PMU->enablePowerOutput(XPOWERS_DCDC4); - // not use channel - PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited - PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited - PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist - PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist - PMU->disablePowerOutput(XPOWERS_VBACKUP); - } - - // disable all axp chip interrupt - PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); - - // Set the constant current charging current of AXP2101, temporarily use 500mA by default - PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); - - // Set up the charging voltage - PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); + // not use channel + PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited + PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited + PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist + PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist + PMU->disablePowerOutput(XPOWERS_VBACKUP); } - PMU->clearIrqStatus(); + // disable all axp chip interrupt + PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); - // TBeam1.1 /T-Beam S3-Core has no external TS detection, - // it needs to be disabled, otherwise it will cause abnormal charging - PMU->disableTSPinMeasure(); + // Set the constant current charging current of AXP2101, temporarily use 500mA by default + PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); - // PMU->enableSystemVoltageMeasure(); - PMU->enableVbusVoltageMeasure(); - PMU->enableBattVoltageMeasure(); + // Set up the charging voltage + PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); + } - if (PMU->isChannelAvailable(XPOWERS_DCDC1)) { - LOG_DEBUG("DC1 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC1) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_DCDC1)); - } - if (PMU->isChannelAvailable(XPOWERS_DCDC2)) { - LOG_DEBUG("DC2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC2) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_DCDC2)); - } - if (PMU->isChannelAvailable(XPOWERS_DCDC3)) { - LOG_DEBUG("DC3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC3) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_DCDC3)); - } - if (PMU->isChannelAvailable(XPOWERS_DCDC4)) { - LOG_DEBUG("DC4 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC4) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_DCDC4)); - } - if (PMU->isChannelAvailable(XPOWERS_LDO2)) { - LOG_DEBUG("LDO2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO2) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_LDO2)); - } - if (PMU->isChannelAvailable(XPOWERS_LDO3)) { - LOG_DEBUG("LDO3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO3) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_LDO3)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO1)) { - LOG_DEBUG("ALDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO1) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_ALDO1)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO2)) { - LOG_DEBUG("ALDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO2) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_ALDO2)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO3)) { - LOG_DEBUG("ALDO3: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO3) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_ALDO3)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO4)) { - LOG_DEBUG("ALDO4: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO4) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_ALDO4)); - } - if (PMU->isChannelAvailable(XPOWERS_BLDO1)) { - LOG_DEBUG("BLDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_BLDO1)); - } - if (PMU->isChannelAvailable(XPOWERS_BLDO2)) { - LOG_DEBUG("BLDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO2) ? "+" : "-", - PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); - } + PMU->clearIrqStatus(); + + // TBeam1.1 /T-Beam S3-Core has no external TS detection, + // it needs to be disabled, otherwise it will cause abnormal charging + PMU->disableTSPinMeasure(); + + // PMU->enableSystemVoltageMeasure(); + PMU->enableVbusVoltageMeasure(); + PMU->enableBattVoltageMeasure(); + + if (PMU->isChannelAvailable(XPOWERS_DCDC1)) { + LOG_DEBUG("DC1 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC1) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC1)); + } + if (PMU->isChannelAvailable(XPOWERS_DCDC2)) { + LOG_DEBUG("DC2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC2)); + } + if (PMU->isChannelAvailable(XPOWERS_DCDC3)) { + LOG_DEBUG("DC3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC3) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC3)); + } + if (PMU->isChannelAvailable(XPOWERS_DCDC4)) { + LOG_DEBUG("DC4 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC4) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC4)); + } + if (PMU->isChannelAvailable(XPOWERS_LDO2)) { + LOG_DEBUG("LDO2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_LDO2)); + } + if (PMU->isChannelAvailable(XPOWERS_LDO3)) { + LOG_DEBUG("LDO3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO3) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_LDO3)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO1)) { + LOG_DEBUG("ALDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO1) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO1)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO2)) { + LOG_DEBUG("ALDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO2)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO3)) { + LOG_DEBUG("ALDO3: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO3) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO3)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO4)) { + LOG_DEBUG("ALDO4: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO4) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO4)); + } + if (PMU->isChannelAvailable(XPOWERS_BLDO1)) { + LOG_DEBUG("BLDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_BLDO1)); + } + if (PMU->isChannelAvailable(XPOWERS_BLDO2)) { + LOG_DEBUG("BLDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); + } // We can safely ignore this approach for most (or all) boards because MCU turned off // earlier than battery discharged to 2.6V. @@ -1245,40 +1198,40 @@ bool Power::axpChipInit() // Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with // battery voltage measurement. Probably it sometimes drops to low values. #ifndef RAK4630 - // Set PMU shutdown voltage at 2.6V to maximize battery utilization - PMU->setSysPowerDownVoltage(2600); + // Set PMU shutdown voltage at 2.6V to maximize battery utilization + PMU->setSysPowerDownVoltage(2600); #endif #ifdef PMU_IRQ - uint64_t pmuIrqMask = 0; + uint64_t pmuIrqMask = 0; - if (PMU->getChipModel() == XPOWERS_AXP192) { - pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ; - } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ; - } + if (PMU->getChipModel() == XPOWERS_AXP192) { + pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ; + } else if (PMU->getChipModel() == XPOWERS_AXP2101) { + pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ; + } - pinMode(PMU_IRQ, INPUT); - attachInterrupt( - PMU_IRQ, [] { pmu_irq = true; }, FALLING); + pinMode(PMU_IRQ, INPUT); + attachInterrupt( + PMU_IRQ, [] { pmu_irq = true; }, FALLING); - // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is - // no battery also it could cause inadvertent waking from light sleep just because the battery filled - // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed - // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus - PMU->enableIRQ(pmuIrqMask); + // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is + // no battery also it could cause inadvertent waking from light sleep just because the battery filled + // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed + // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus + PMU->enableIRQ(pmuIrqMask); - PMU->clearIrqStatus(); + PMU->clearIrqStatus(); #endif /*PMU_IRQ*/ - readPowerStatus(); + readPowerStatus(); - pmu_found = true; + pmu_found = true; - return pmu_found; + return pmu_found; #else - return false; + return false; #endif } @@ -1287,52 +1240,50 @@ bool Power::axpChipInit() /** * Wrapper class for an I2C MAX17048 Lipo battery sensor. */ -class LipoBatteryLevel : public HasBatteryLevel -{ - private: - MAX17048Singleton *max17048 = nullptr; +class LipoBatteryLevel : public HasBatteryLevel { +private: + MAX17048Singleton *max17048 = nullptr; - public: - /** - * Init the I2C MAX17048 Lipo battery level sensor - */ - bool runOnce() - { - if (max17048 == nullptr) { - max17048 = MAX17048Singleton::GetInstance(); - } - - // try to start if the sensor has been detected - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].first != 0) { - return max17048->runOnce(nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].second); - } - return false; +public: + /** + * Init the I2C MAX17048 Lipo battery level sensor + */ + bool runOnce() { + if (max17048 == nullptr) { + max17048 = MAX17048Singleton::GetInstance(); } - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override { return max17048->getBusBatteryPercent(); } + // try to start if the sensor has been detected + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].first != 0) { + return max17048->runOnce(nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].second); + } + return false; + } - /** - * The raw voltage of the battery in millivolts, or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { return max17048->getBusVoltageMv(); } + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return max17048->getBusBatteryPercent(); } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() override { return max17048->isBatteryConnected(); } + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return max17048->getBusVoltageMv(); } - /** - * return true if there is an external power source detected - */ - virtual bool isVbusIn() override { return max17048->isExternallyPowered(); } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return max17048->isBatteryConnected(); } - /** - * return true if the battery is currently charging - */ - virtual bool isCharging() override { return max17048->isBatteryCharging(); } + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return max17048->isExternallyPowered(); } + + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return max17048->isBatteryCharging(); } }; LipoBatteryLevel lipoLevel; @@ -1340,24 +1291,20 @@ LipoBatteryLevel lipoLevel; /** * Init the Lipo battery level sensor */ -bool Power::lipoInit() -{ - bool result = lipoLevel.runOnce(); - LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet"); - if (!result) - return false; - batteryLevel = &lipoLevel; - return true; +bool Power::lipoInit() { + bool result = lipoLevel.runOnce(); + LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &lipoLevel; + return true; } #else /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::lipoInit() -{ - return false; -} +bool Power::lipoInit() { return false; } #endif #if defined(HAS_PPM) && HAS_PPM @@ -1365,114 +1312,110 @@ bool Power::lipoInit() /** * Adapter class for BQ25896/BQ27220 Lipo battery charger. */ -class LipoCharger : public HasBatteryLevel -{ - private: - BQ27220 *bq = nullptr; +class LipoCharger : public HasBatteryLevel { +private: + BQ27220 *bq = nullptr; - public: - /** - * Init the I2C BQ25896 Lipo battery charger - */ - bool runOnce() - { - if (PPM == nullptr) { - PPM = new XPowersPPM; - bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); - if (result) { - LOG_INFO("PPM BQ25896 init succeeded"); - // Set the minimum operating voltage. Below this voltage, the PPM will protect - // PPM->setSysPowerDownVoltage(3100); +public: + /** + * Init the I2C BQ25896 Lipo battery charger + */ + bool runOnce() { + if (PPM == nullptr) { + PPM = new XPowersPPM; + bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); + if (result) { + LOG_INFO("PPM BQ25896 init succeeded"); + // Set the minimum operating voltage. Below this voltage, the PPM will protect + // PPM->setSysPowerDownVoltage(3100); - // Set input current limit, default is 500mA - // PPM->setInputCurrentLimit(800); + // Set input current limit, default is 500mA + // PPM->setInputCurrentLimit(800); - // Disable current limit pin - // PPM->disableCurrentLimitPin(); + // Disable current limit pin + // PPM->disableCurrentLimitPin(); - // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV - PPM->setChargeTargetVoltage(4288); + // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV + PPM->setChargeTargetVoltage(4288); - // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA - // PPM->setPrechargeCurr(64); + // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA + // PPM->setPrechargeCurr(64); - // The premise is that limit pin is disabled, or it will - // only follow the maximum charging current set by limit pin. - // Set the charging current , Range:0~5056mA ,step:64mA - PPM->setChargerConstantCurr(1024); + // The premise is that limit pin is disabled, or it will + // only follow the maximum charging current set by limit pin. + // Set the charging current , Range:0~5056mA ,step:64mA + PPM->setChargerConstantCurr(1024); - // To obtain voltage data, the ADC must be enabled first - PPM->enableMeasure(); + // To obtain voltage data, the ADC must be enabled first + PPM->enableMeasure(); - // Turn on charging function - // If there is no battery connected, do not turn on the charging function - PPM->enableCharge(); - } else { - LOG_WARN("PPM BQ25896 init failed"); - delete PPM; - PPM = nullptr; - return false; - } - } - if (bq == nullptr) { - bq = new BQ27220; - bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY); - - bool result = bq->init(); - if (result) { - LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity()); - LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity()); - LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity()); - return true; - } else { - LOG_WARN("BQ27220 init failed"); - delete bq; - bq = nullptr; - return false; - } - } + // Turn on charging function + // If there is no battery connected, do not turn on the charging function + PPM->enableCharge(); + } else { + LOG_WARN("PPM BQ25896 init failed"); + delete PPM; + PPM = nullptr; return false; + } } + if (bq == nullptr) { + bq = new BQ27220; + bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY); - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override - { - return -1; - // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated + bool result = bq->init(); + if (result) { + LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity()); + LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity()); + LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity()); + return true; + } else { + LOG_WARN("BQ27220 init failed"); + delete bq; + bq = nullptr; + return false; + } } + return false; + } - /** - * The raw voltage of the battery in millivolts, or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { return bq->getVoltage(); } + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { + return -1; + // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated + } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return bq->getVoltage(); } - /** - * return true if there is an external power source detected - */ - virtual bool isVbusIn() override { return PPM->isVbusIn(); } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } - /** - * return true if the battery is currently charging - */ - virtual bool isCharging() override - { - bool isCharging = PPM->isCharging(); - if (isCharging) { - LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); - } else { - if (!PPM->isVbusIn()) { - LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); - } - } - return isCharging; + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return PPM->isVbusIn(); } + + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { + bool isCharging = PPM->isCharging(); + if (isCharging) { + LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); + } else { + if (!PPM->isVbusIn()) { + LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); + } } + return isCharging; + } }; LipoCharger lipoCharger; @@ -1480,24 +1423,20 @@ LipoCharger lipoCharger; /** * Init the Lipo battery charger */ -bool Power::lipoChargerInit() -{ - bool result = lipoCharger.runOnce(); - LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet"); - if (!result) - return false; - batteryLevel = &lipoCharger; - return true; +bool Power::lipoChargerInit() { + bool result = lipoCharger.runOnce(); + LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &lipoCharger; + return true; } #else /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::lipoChargerInit() -{ - return false; -} +bool Power::lipoChargerInit() { return false; } #endif #ifdef HELTEC_MESH_SOLAR @@ -1506,43 +1445,41 @@ bool Power::lipoChargerInit() /** * meshSolar class for an SMBUS battery sensor. */ -class meshSolarBatteryLevel : public HasBatteryLevel -{ +class meshSolarBatteryLevel : public HasBatteryLevel { - public: - /** - * Init the I2C meshSolar battery level sensor - */ - bool runOnce() - { - meshSolarStart(); - return true; - } +public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() { + meshSolarStart(); + return true; + } - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } - /** - * The raw voltage of the battery in millivolts, or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } - /** - * return true if there is an external power source detected - */ - virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } - /** - * return true if the battery is currently charging - */ - virtual bool isCharging() override { return meshSolarIsCharging(); } + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return meshSolarIsCharging(); } }; meshSolarBatteryLevel meshSolarLevel; @@ -1550,22 +1487,18 @@ meshSolarBatteryLevel meshSolarLevel; /** * Init the meshSolar battery level sensor */ -bool Power::meshSolarInit() -{ - bool result = meshSolarLevel.runOnce(); - LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); - if (!result) - return false; - batteryLevel = &meshSolarLevel; - return true; +bool Power::meshSolarInit() { + bool result = meshSolarLevel.runOnce(); + LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &meshSolarLevel; + return true; } #else /** * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::meshSolarInit() -{ - return false; -} +bool Power::meshSolarInit() { return false; } #endif diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 9f8097b84..2bbfc3742 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -31,222 +31,202 @@ FakeFsm powerFSM; void PowerFSM_setup(){}; #else /// Should we behave as if we have AC power now? -static bool isPowered() -{ +static bool isPowered() { // Circumvent the battery sensing logic and assumes constant power if no battery pin or power mgmt IC #if !defined(BATTERY_PIN) && !defined(HAS_AXP192) && !defined(HAS_AXP2101) && !defined(NRF_APM) - return true; + return true; #endif - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); - // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON - // We assume routers might be powered all the time, but from a low current (solar) source - bool isPowerSavingMode = config.power.is_power_saving || isRouter; + // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON + // We assume routers might be powered all the time, but from a low current (solar) source + bool isPowerSavingMode = config.power.is_power_saving || isRouter; - /* To determine if we're externally powered, assumptions - 1) If we're powered up and there's no battery, we must be getting power externally. (because we'd be dead otherwise) + /* To determine if we're externally powered, assumptions + 1) If we're powered up and there's no battery, we must be getting power externally. (because we'd be dead + otherwise) - 2) If we detect USB power from the power management chip, we must be getting power externally. + 2) If we detect USB power from the power management chip, we must be getting power externally. - 3) On some boards we don't have the power management chip (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect - external power source (see `isVbusIn()` in `Power.cpp`) - */ - return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB()); + 3) On some boards we don't have the power management chip (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to + detect external power source (see `isVbusIn()` in `Power.cpp`) + */ + return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB()); } -static void sdsEnter() -{ - LOG_POWERFSM("State: SDS"); - // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw - doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); +static void sdsEnter() { + LOG_POWERFSM("State: SDS"); + // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw + doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); } -static void lowBattSDSEnter() -{ - LOG_POWERFSM("State: Lower batt SDS"); - doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); +static void lowBattSDSEnter() { + LOG_POWERFSM("State: Lower batt SDS"); + doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); } extern Power *power; -static void shutdownEnter() -{ - LOG_POWERFSM("State: SHUTDOWN"); - shutdownAtMsec = millis(); +static void shutdownEnter() { + LOG_POWERFSM("State: SHUTDOWN"); + shutdownAtMsec = millis(); } #include "error.h" static uint32_t secsSlept; -static void lsEnter() -{ - LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); - if (screen) - screen->setOn(false); - secsSlept = 0; // How long have we been sleeping this time +static void lsEnter() { + LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); + if (screen) + screen->setOn(false); + secsSlept = 0; // How long have we been sleeping this time - // LOG_INFO("lsEnter end"); + // LOG_INFO("lsEnter end"); } -static void lsIdle() -{ - // LOG_INFO("lsIdle begin ls_secs=%u", getPref_ls_secs()); +static void lsIdle() { + // LOG_INFO("lsIdle begin ls_secs=%u", getPref_ls_secs()); #ifdef ARCH_ESP32 - // Do we have more sleeping to do? - if (secsSlept < config.power.ls_secs) { - // If some other service would stall sleep, don't let sleep happen yet - if (doPreflightSleep()) { - // Briefly come out of sleep long enough to blink the led once every few seconds - uint32_t sleepTime = SLEEP_TIME; + // Do we have more sleeping to do? + if (secsSlept < config.power.ls_secs) { + // If some other service would stall sleep, don't let sleep happen yet + if (doPreflightSleep()) { + // Briefly come out of sleep long enough to blink the led once every few seconds + uint32_t sleepTime = SLEEP_TIME; - powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); - ledBlink.set(false); // Never leave led on while in light sleep - esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); - powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); + powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); + ledBlink.set(false); // Never leave led on while in light sleep + esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); + powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); - switch (wakeCause2) { - case ESP_SLEEP_WAKEUP_TIMER: - // Normal case: timer expired, we should just go back to sleep ASAP + switch (wakeCause2) { + case ESP_SLEEP_WAKEUP_TIMER: + // Normal case: timer expired, we should just go back to sleep ASAP - ledBlink.set(true); // briefly turn on led - wakeCause2 = doLightSleep(100); // leave led on for 1ms + ledBlink.set(true); // briefly turn on led + wakeCause2 = doLightSleep(100); // leave led on for 1ms - secsSlept += sleepTime; - // LOG_INFO("Sleep, flash led!"); - break; + secsSlept += sleepTime; + // LOG_INFO("Sleep, flash led!"); + break; - case ESP_SLEEP_WAKEUP_UART: - // Not currently used (because uart triggers in hw have problems) - powerFSM.trigger(EVENT_SERIAL_CONNECTED); - break; + case ESP_SLEEP_WAKEUP_UART: + // Not currently used (because uart triggers in hw have problems) + powerFSM.trigger(EVENT_SERIAL_CONNECTED); + break; - default: - // We woke for some other reason (button press, device IRQ interrupt) + default: + // We woke for some other reason (button press, device IRQ interrupt) #ifdef BUTTON_PIN - bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); + bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); #else - bool pressed = false; + bool pressed = false; #endif - if (pressed) { // If we woke because of press, instead generate a PRESS event. - powerFSM.trigger(EVENT_PRESS); - } else { - // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) - // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code - powerFSM.trigger(EVENT_WAKE_TIMER); - } - break; - } + if (pressed) { // If we woke because of press, instead generate a PRESS event. + powerFSM.trigger(EVENT_PRESS); } else { - // Someone says we can't sleep now, so just save some power by sleeping the CPU for 100ms or so - delay(100); + // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) + // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code + powerFSM.trigger(EVENT_WAKE_TIMER); } + break; + } } else { - // Time to stop sleeping! - ledBlink.set(false); - LOG_INFO("Reached ls_secs, service loop()"); - powerFSM.trigger(EVENT_WAKE_TIMER); + // Someone says we can't sleep now, so just save some power by sleeping the CPU for 100ms or so + delay(100); } + } else { + // Time to stop sleeping! + ledBlink.set(false); + LOG_INFO("Reached ls_secs, service loop()"); + powerFSM.trigger(EVENT_WAKE_TIMER); + } #endif } -static void lsExit() -{ - LOG_POWERFSM("State: lsExit"); -} +static void lsExit() { LOG_POWERFSM("State: lsExit"); } -static void nbEnter() -{ - LOG_POWERFSM("State: nbEnter"); - if (screen) - screen->setOn(false); +static void nbEnter() { + LOG_POWERFSM("State: nbEnter"); + if (screen) + screen->setOn(false); #ifdef ARCH_ESP32 - // Only ESP32 should turn off bluetooth - setBluetoothEnable(false); + // Only ESP32 should turn off bluetooth + setBluetoothEnable(false); #endif - // FIXME - check if we already have packets for phone and immediately trigger EVENT_PACKETS_FOR_PHONE + // FIXME - check if we already have packets for phone and immediately trigger EVENT_PACKETS_FOR_PHONE } -static void darkEnter() -{ - LOG_POWERFSM("State: darkEnter"); - setBluetoothEnable(true); +static void darkEnter() { + LOG_POWERFSM("State: darkEnter"); + setBluetoothEnable(true); + if (screen) + screen->setOn(false); +} + +static void serialEnter() { + LOG_POWERFSM("State: serialEnter"); + setBluetoothEnable(false); + if (screen) { + screen->setOn(true); + } +} + +static void serialExit() { + LOG_POWERFSM("State: serialExit"); + // Turn bluetooth back on when we leave serial stream API + setBluetoothEnable(true); +} + +static void powerEnter() { + LOG_POWERFSM("State: powerEnter"); + if (!isPowered()) { + // If we got here, we are in the wrong state - we should be in powered, let that state handle things + LOG_INFO("Loss of power in Powered"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } else { if (screen) - screen->setOn(false); -} - -static void serialEnter() -{ - LOG_POWERFSM("State: serialEnter"); - setBluetoothEnable(false); - if (screen) { - screen->setOn(true); - } -} - -static void serialExit() -{ - LOG_POWERFSM("State: serialExit"); - // Turn bluetooth back on when we leave serial stream API + screen->setOn(true); setBluetoothEnable(true); + // within enter() the function getState() returns the state we came from + } } -static void powerEnter() -{ - LOG_POWERFSM("State: powerEnter"); - if (!isPowered()) { - // If we got here, we are in the wrong state - we should be in powered, let that state handle things - LOG_INFO("Loss of power in Powered"); - powerFSM.trigger(EVENT_POWER_DISCONNECTED); - } else { - if (screen) - screen->setOn(true); - setBluetoothEnable(true); - // within enter() the function getState() returns the state we came from - } +static void powerIdle() { + // LOG_POWERFSM("State: powerIdle"); // very chatty + if (!isPowered()) { + // If we got here, we are in the wrong state + LOG_INFO("Loss of power in Powered"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } } -static void powerIdle() -{ - // LOG_POWERFSM("State: powerIdle"); // very chatty - if (!isPowered()) { - // If we got here, we are in the wrong state - LOG_INFO("Loss of power in Powered"); - powerFSM.trigger(EVENT_POWER_DISCONNECTED); - } +static void powerExit() { + LOG_POWERFSM("State: powerExit"); + setBluetoothEnable(true); } -static void powerExit() -{ - LOG_POWERFSM("State: powerExit"); - setBluetoothEnable(true); +static void onEnter() { + LOG_POWERFSM("State: onEnter"); + if (screen) + screen->setOn(true); + setBluetoothEnable(true); } -static void onEnter() -{ - LOG_POWERFSM("State: onEnter"); - if (screen) - screen->setOn(true); - setBluetoothEnable(true); +static void onIdle() { + LOG_POWERFSM("State: onIdle"); + if (isPowered()) { + // If we got here, we are in the wrong state - we should be in powered, let that state handle things + powerFSM.trigger(EVENT_POWER_CONNECTED); + } } -static void onIdle() -{ - LOG_POWERFSM("State: onIdle"); - if (isPowered()) { - // If we got here, we are in the wrong state - we should be in powered, let that state handle things - powerFSM.trigger(EVENT_POWER_CONNECTED); - } -} - -static void bootEnter() -{ - LOG_POWERFSM("State: bootEnter"); -} +static void bootEnter() { LOG_POWERFSM("State: bootEnter"); } State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN"); State stateSDS(sdsEnter, NULL, NULL, "SDS"); @@ -260,147 +240,141 @@ State stateON(onEnter, onIdle, NULL, "ON"); State statePOWER(powerEnter, powerIdle, powerExit, "POWER"); Fsm powerFSM(&stateBOOT); -void PowerFSM_setup() -{ - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); - bool hasPower = isPowered(); +void PowerFSM_setup() { + bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool hasPower = isPowered(); - LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); - powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); + LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); + powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); - // wake timer expired or a packet arrived - // if we are a router node, we go to NB (no need for bluetooth) otherwise we go to DARK (so we can send message to phone) + // wake timer expired or a packet arrived + // if we are a router node, we go to NB (no need for bluetooth) otherwise we go to DARK (so we can send message to + // phone) #ifdef ARCH_ESP32 - powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); + powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); #else // Don't go into a no-bluetooth state on low power platforms - powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); + powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); #endif - // We need this transition, because we might not transition if we were waiting to enter light-sleep, because when we wake from - // light sleep we _always_ transition to NB or dark and - powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, - "Received packet, exiting light sleep"); - powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake"); + // We need this transition, because we might not transition if we were waiting to enter light-sleep, because when we + // wake from light sleep we _always_ transition to NB or dark and + powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, exiting light sleep"); + powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake"); - // Handle press events - note: we ignore button presses when in API mode - powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers - powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, - "Press"); // Allow button to work while in serial API + // Handle press events - note: we ignore button presses when in API mode + powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, + "Press"); // Allow button to work while in serial API - // Handle critically low power battery by forcing deep sleep - powerFSM.add_transition(&stateBOOT, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateLS, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateNB, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateDARK, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateON, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateSERIAL, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + // Handle critically low power battery by forcing deep sleep + powerFSM.add_transition(&stateBOOT, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateLS, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateNB, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateDARK, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateON, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateSERIAL, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - // Handle being told to power off - powerFSM.add_transition(&stateBOOT, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateLS, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateNB, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateDARK, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateON, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + // Handle being told to power off + powerFSM.add_transition(&stateBOOT, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateLS, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateNB, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateDARK, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateON, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - // Inputbroker - powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer + // Inputbroker + powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device"); + powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer - powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); - powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); + powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); - // if we are a router we don't turn the screen on for these things - if (!isRouter) { - // if any packet destined for phone arrives, turn on bluetooth at least - powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); + // if we are a router we don't turn the screen on for these things + if (!isRouter) { + // if any packet destined for phone arrives, turn on bluetooth at least + powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); - // Show the received text message - powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); - powerFSM.add_transition(&stateON, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); // restarts the sleep timer - } + // Show the received text message + powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); + powerFSM.add_transition(&stateON, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); // restarts the sleep timer + } - // If we are not in statePOWER but get a serial connection, suppress sleep (and keep the screen on) while connected - powerFSM.add_transition(&stateLS, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateNB, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&statePOWER, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + // If we are not in statePOWER but get a serial connection, suppress sleep (and keep the screen on) while connected + powerFSM.add_transition(&stateLS, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateNB, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&statePOWER, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - // If we get power connected, go to the power connect state - powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + // If we get power connected, go to the power connect state + powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); - // powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + // powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); - // the only way to leave state serial is for the client to disconnect (or we timeout and force disconnect them) - // when we leave, go to ON (which might not be the correct state if we have power connected, we will fix that in onEnter) - powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_SERIAL_DISCONNECTED, NULL, "serial disconnect"); + // the only way to leave state serial is for the client to disconnect (or we timeout and force disconnect them) + // when we leave, go to ON (which might not be the correct state if we have power connected, we will fix that in + // onEnter) + powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_SERIAL_DISCONNECTED, NULL, "serial disconnect"); - powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); + powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); #ifdef USE_EINK - // Allow E-Ink devices to suppress the screensaver, if screen timeout set to 0 - if (config.display.screen_on_secs > 0) + // Allow E-Ink devices to suppress the screensaver, if screen timeout set to 0 + if (config.display.screen_on_secs > 0) #endif - { - powerFSM.add_timed_transition(&stateON, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); - powerFSM.add_timed_transition(&statePOWER, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); - } + { + powerFSM.add_timed_transition(&stateON, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + powerFSM.add_timed_transition(&statePOWER, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + } // We never enter light-sleep or NB states on NRF52 (because the CPU uses so little power normally) #ifdef ARCH_ESP32 - // See: https://github.com/meshtastic/firmware/issues/1071 - // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated - // through the modules + // See: https://github.com/meshtastic/firmware/issues/1071 + // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be + // initiated through the modules #if HAS_WIFI && !defined(MESHTASTIC_EXCLUDE_WIFI) - bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; + bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; - if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { - powerFSM.add_timed_transition(&stateNB, &stateLS, - Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, - "Min wake timeout"); + if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { + powerFSM.add_timed_transition(&stateNB, &stateLS, Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, + "Min wake timeout"); - // If ESP32 and using power-saving, timer mover from DARK to light-sleep - // Also serves purpose of the old DARK to DARK transition(?) See https://github.com/meshtastic/firmware/issues/3517 - powerFSM.add_timed_transition( - &stateDARK, &stateLS, - Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, - "Bluetooth timeout"); - } else { - // If ESP32, but not using power-saving, check periodically if config has drifted out of stateDark - powerFSM.add_timed_transition(&stateDARK, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); - } + // If ESP32 and using power-saving, timer mover from DARK to light-sleep + // Also serves purpose of the old DARK to DARK transition(?) See https://github.com/meshtastic/firmware/issues/3517 + powerFSM.add_timed_transition(&stateDARK, &stateLS, + Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, + "Bluetooth timeout"); + } else { + // If ESP32, but not using power-saving, check periodically if config has drifted out of stateDark + powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + } #endif // HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) #else // (not) ARCH_ESP32 - // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark - powerFSM.add_timed_transition(&stateDARK, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, - "Screen-on timeout"); + // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark + powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); #endif - powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state + powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state } #endif diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 182ac082a..47725181b 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -17,7 +17,8 @@ #define EVENT_RECEIVED_MSG 5 // #define EVENT_BOOT 6 // now done with a timed transition #define EVENT_BLUETOOTH_PAIR 7 -// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen +// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on +// the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth #define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 @@ -29,21 +30,19 @@ #define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen #if MESHTASTIC_EXCLUDE_POWER_FSM -class FakeFsm -{ - public: - void trigger(int event) - { - if (event == EVENT_SERIAL_CONNECTED) { - serialConnected = true; - } else if (event == EVENT_SERIAL_DISCONNECTED) { - serialConnected = false; - } - }; - bool getState() { return serialConnected; }; +class FakeFsm { +public: + void trigger(int event) { + if (event == EVENT_SERIAL_CONNECTED) { + serialConnected = true; + } else if (event == EVENT_SERIAL_DISCONNECTED) { + serialConnected = false; + } + }; + bool getState() { return serialConnected; }; - private: - bool serialConnected = false; +private: + bool serialConnected = false; }; extern FakeFsm powerFSM; void PowerFSM_setup(); diff --git a/src/PowerFSMThread.h b/src/PowerFSMThread.h index 135f53298..1696c8454 100644 --- a/src/PowerFSMThread.h +++ b/src/PowerFSMThread.h @@ -6,40 +6,36 @@ #include "main.h" #include "power.h" -namespace concurrency -{ +namespace concurrency { /// Wrapper to convert our powerFSM stuff into a 'thread' -class PowerFSMThread : public OSThread -{ - public: - // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - PowerFSMThread() : OSThread("PowerFSM") {} +class PowerFSMThread : public OSThread { +public: + // callback returns the period for the next callback invocation (or 0 if we should no longer be called) + PowerFSMThread() : OSThread("PowerFSM") {} - protected: - int32_t runOnce() override - { +protected: + int32_t runOnce() override { #if !MESHTASTIC_EXCLUDE_POWER_FSM - powerFSM.run_machine(); + powerFSM.run_machine(); - /// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake - /// cpu for serial rx - FIXME) - const State *state = powerFSM.getState(); - canSleep = (state != &statePOWER) && (state != &stateSERIAL); + /// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake + /// cpu for serial rx - FIXME) + const State *state = powerFSM.getState(); + canSleep = (state != &statePOWER) && (state != &stateSERIAL); - if (powerStatus->getHasUSB()) { - timeLastPowered = millis(); - } else if (config.power.on_battery_shutdown_after_secs > 0 && config.power.on_battery_shutdown_after_secs != UINT32_MAX && - millis() > (timeLastPowered + - Default::getConfiguredOrDefaultMs( - config.power.on_battery_shutdown_after_secs))) { // shutdown after 30 minutes unpowered - powerFSM.trigger(EVENT_SHUTDOWN); - } - - return 100; -#else - return INT32_MAX; -#endif + if (powerStatus->getHasUSB()) { + timeLastPowered = millis(); + } else if (config.power.on_battery_shutdown_after_secs > 0 && config.power.on_battery_shutdown_after_secs != UINT32_MAX && + millis() > (timeLastPowered + + Default::getConfiguredOrDefaultMs(config.power.on_battery_shutdown_after_secs))) { // shutdown after 30 minutes unpowered + powerFSM.trigger(EVENT_SHUTDOWN); } + + return 100; +#else + return INT32_MAX; +#endif + } }; } // namespace concurrency \ No newline at end of file diff --git a/src/PowerMon.cpp b/src/PowerMon.cpp index 38740b6ae..6cca5a626 100644 --- a/src/PowerMon.cpp +++ b/src/PowerMon.cpp @@ -2,46 +2,39 @@ #include "NodeDB.h" // Use the 'live' config flag to figure out if we should be showing this message -bool PowerMon::is_power_enabled(uint64_t m) -{ - // FIXME: VERY STRANGE BUG: if I or in "force_enabled || " the flashed image on a rak4631 is not accepted by the bootloader as - // valid!!! Possibly a linker/gcc/bootloader bug somewhere? - return ((m & config.power.powermon_enables) ? true : false); +bool PowerMon::is_power_enabled(uint64_t m) { + // FIXME: VERY STRANGE BUG: if I or in "force_enabled || " the flashed image on a rak4631 is not accepted by the + // bootloader as valid!!! Possibly a linker/gcc/bootloader bug somewhere? + return ((m & config.power.powermon_enables) ? true : false); } -void PowerMon::setState(_meshtastic_PowerMon_State state, const char *reason) -{ +void PowerMon::setState(_meshtastic_PowerMon_State state, const char *reason) { #ifdef USE_POWERMON - auto oldstates = states; - states |= state; - if (oldstates != states && is_power_enabled(state)) { - emitLog(reason); - } + auto oldstates = states; + states |= state; + if (oldstates != states && is_power_enabled(state)) { + emitLog(reason); + } #endif } -void PowerMon::clearState(_meshtastic_PowerMon_State state, const char *reason) -{ +void PowerMon::clearState(_meshtastic_PowerMon_State state, const char *reason) { #ifdef USE_POWERMON - auto oldstates = states; - states &= ~state; - if (oldstates != states && is_power_enabled(state)) { - emitLog(reason); - } + auto oldstates = states; + states &= ~state; + if (oldstates != states && is_power_enabled(state)) { + emitLog(reason); + } #endif } -void PowerMon::emitLog(const char *reason) -{ +void PowerMon::emitLog(const char *reason) { #ifdef USE_POWERMON - // The nrf52 printf doesn't understand 64 bit ints, so if we ever reach that point this function will need to change. - LOG_INFO("S:PM:0x%08lx,%s", (uint32_t)states, reason); + // The nrf52 printf doesn't understand 64 bit ints, so if we ever reach that point this function will need to change. + LOG_INFO("S:PM:0x%08lx,%s", (uint32_t)states, reason); #endif } PowerMon *powerMon; -void powerMonInit() -{ - powerMon = new PowerMon(); -} \ No newline at end of file +void powerMonInit() { powerMon = new PowerMon(); } \ No newline at end of file diff --git a/src/PowerMon.h b/src/PowerMon.h index a19a6d010..6338db935 100644 --- a/src/PowerMon.h +++ b/src/PowerMon.h @@ -13,30 +13,29 @@ * * For more information see the PowerMon docs. */ -class PowerMon -{ - uint64_t states = 0UL; +class PowerMon { + uint64_t states = 0UL; - friend class PowerStressModule; + friend class PowerStressModule; - /** - * If stress testing we always want all events logged - */ - bool force_enabled = false; + /** + * If stress testing we always want all events logged + */ + bool force_enabled = false; - public: - PowerMon() {} +public: + PowerMon() {} - // Mark entry/exit of a power consuming state - void setState(_meshtastic_PowerMon_State state, const char *reason = ""); - void clearState(_meshtastic_PowerMon_State state, const char *reason = ""); + // Mark entry/exit of a power consuming state + void setState(_meshtastic_PowerMon_State state, const char *reason = ""); + void clearState(_meshtastic_PowerMon_State state, const char *reason = ""); - private: - // Emit the coded log message - void emitLog(const char *reason); +private: + // Emit the coded log message + void emitLog(const char *reason); - // Use the 'live' config flag to figure out if we should be showing this message - bool is_power_enabled(uint64_t m); + // Use the 'live' config flag to figure out if we should be showing this message + bool is_power_enabled(uint64_t m); }; extern PowerMon *powerMon; diff --git a/src/PowerStatus.h b/src/PowerStatus.h index fe4543e08..799db32c5 100644 --- a/src/PowerStatus.h +++ b/src/PowerStatus.h @@ -3,8 +3,7 @@ #include "configuration.h" #include -namespace meshtastic -{ +namespace meshtastic { /** * A boolean where we have a third state of Unknown @@ -12,90 +11,84 @@ namespace meshtastic enum OptionalBool { OptFalse = 0, OptTrue = 1, OptUnknown = 2 }; /// Describes the state of the Power system. -class PowerStatus : public Status -{ +class PowerStatus : public Status { - private: - CallbackObserver statusObserver = - CallbackObserver(this, &PowerStatus::updateStatus); +private: + CallbackObserver statusObserver = + CallbackObserver(this, &PowerStatus::updateStatus); - /// Whether we have a battery connected - OptionalBool hasBattery = OptUnknown; - /// Battery voltage in mV, valid if haveBattery is true - int batteryVoltageMv = 0; - /// Battery charge percentage, either read directly or estimated - int8_t batteryChargePercent = 0; - /// Whether USB is connected - OptionalBool hasUSB = OptUnknown; - /// Whether we are charging the battery - OptionalBool isCharging = OptUnknown; + /// Whether we have a battery connected + OptionalBool hasBattery = OptUnknown; + /// Battery voltage in mV, valid if haveBattery is true + int batteryVoltageMv = 0; + /// Battery charge percentage, either read directly or estimated + int8_t batteryChargePercent = 0; + /// Whether USB is connected + OptionalBool hasUSB = OptUnknown; + /// Whether we are charging the battery + OptionalBool isCharging = OptUnknown; - public: - PowerStatus() { statusType = STATUS_TYPE_POWER; } - PowerStatus(OptionalBool hasBattery, OptionalBool hasUSB, OptionalBool isCharging, int batteryVoltageMv = -1, - int8_t batteryChargePercent = 0) - : Status() - { - this->hasBattery = hasBattery; - this->hasUSB = hasUSB; - this->isCharging = isCharging; - this->batteryVoltageMv = batteryVoltageMv; - this->batteryChargePercent = batteryChargePercent; - } - PowerStatus(const PowerStatus &); - PowerStatus &operator=(const PowerStatus &); +public: + PowerStatus() { statusType = STATUS_TYPE_POWER; } + PowerStatus(OptionalBool hasBattery, OptionalBool hasUSB, OptionalBool isCharging, int batteryVoltageMv = -1, int8_t batteryChargePercent = 0) + : Status() { + this->hasBattery = hasBattery; + this->hasUSB = hasUSB; + this->isCharging = isCharging; + this->batteryVoltageMv = batteryVoltageMv; + this->batteryChargePercent = batteryChargePercent; + } + PowerStatus(const PowerStatus &); + PowerStatus &operator=(const PowerStatus &); - void observe(Observable *source) { statusObserver.observe(source); } + void observe(Observable *source) { statusObserver.observe(source); } - bool getHasBattery() const { return hasBattery == OptTrue; } + bool getHasBattery() const { return hasBattery == OptTrue; } - bool getHasUSB() const { return hasUSB == OptTrue; } + bool getHasUSB() const { return hasUSB == OptTrue; } - /// Can we even know if this board has USB power or not - bool knowsUSB() const { return hasUSB != OptUnknown; } + /// Can we even know if this board has USB power or not + bool knowsUSB() const { return hasUSB != OptUnknown; } - bool getIsCharging() const { return isCharging == OptTrue; } + bool getIsCharging() const { return isCharging == OptTrue; } - int getBatteryVoltageMv() const { return batteryVoltageMv; } + int getBatteryVoltageMv() const { return batteryVoltageMv; } - /** - * Note: for boards with battery pin or PMU, 0% battery means 'unknown/this board doesn't have a battery installed' - */ + /** + * Note: for boards with battery pin or PMU, 0% battery means 'unknown/this board doesn't have a battery installed' + */ #if defined(HAS_PMU) || defined(BATTERY_PIN) - uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 0; } + uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 0; } #endif - /** - * Note: for boards without battery pin and PMU, 101% battery means 'the board is using external power' - */ + /** + * Note: for boards without battery pin and PMU, 101% battery means 'the board is using external power' + */ #if !defined(HAS_PMU) && !defined(BATTERY_PIN) - uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 101; } + uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 101; } #endif - bool matches(const PowerStatus *newStatus) const + bool matches(const PowerStatus *newStatus) const { + return (newStatus->getHasBattery() != hasBattery || newStatus->getHasUSB() != hasUSB || newStatus->getBatteryVoltageMv() != batteryVoltageMv); + } + int updateStatus(const PowerStatus *newStatus) { + // Only update the status if values have actually changed + bool isDirty; { - return (newStatus->getHasBattery() != hasBattery || newStatus->getHasUSB() != hasUSB || - newStatus->getBatteryVoltageMv() != batteryVoltageMv); + isDirty = matches(newStatus); + initialized = true; + hasBattery = newStatus->hasBattery; + batteryVoltageMv = newStatus->getBatteryVoltageMv(); + batteryChargePercent = newStatus->getBatteryChargePercent(); + hasUSB = newStatus->hasUSB; + isCharging = newStatus->isCharging; } - int updateStatus(const PowerStatus *newStatus) - { - // Only update the status if values have actually changed - bool isDirty; - { - isDirty = matches(newStatus); - initialized = true; - hasBattery = newStatus->hasBattery; - batteryVoltageMv = newStatus->getBatteryVoltageMv(); - batteryChargePercent = newStatus->getBatteryChargePercent(); - hasUSB = newStatus->hasUSB; - isCharging = newStatus->isCharging; - } - if (isDirty) { - // LOG_DEBUG("Battery %dmV %d%%", batteryVoltageMv, batteryChargePercent); - onNewStatus.notifyObservers(this); - } - return 0; + if (isDirty) { + // LOG_DEBUG("Battery %dmV %d%%", batteryVoltageMv, batteryChargePercent); + onNewStatus.notifyObservers(this); } + return 0; + } }; } // namespace meshtastic diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 895dcb147..fcc1fdc37 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -20,387 +20,376 @@ #if HAS_NETWORKING extern Syslog syslog; #endif -void RedirectablePrint::rpInit() -{ +void RedirectablePrint::rpInit() { #ifdef HAS_FREE_RTOS - inDebugPrint = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace); + inDebugPrint = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace); #endif } -void RedirectablePrint::setDestination(Print *_dest) -{ - assert(_dest); - dest = _dest; +void RedirectablePrint::setDestination(Print *_dest) { + assert(_dest); + dest = _dest; } -size_t RedirectablePrint::write(uint8_t c) -{ - // Always send the characters to our segger JTAG debugger +size_t RedirectablePrint::write(uint8_t c) { + // Always send the characters to our segger JTAG debugger #ifdef USE_SEGGER - SEGGER_RTT_PutChar(SEGGER_STDOUT_CH, c); + SEGGER_RTT_PutChar(SEGGER_STDOUT_CH, c); #endif - // Account for legacy config transition - bool serialEnabled = config.has_security ? config.security.serial_enabled : config.device.serial_enabled; - if (!config.has_lora || serialEnabled) - dest->write(c); + // Account for legacy config transition + bool serialEnabled = config.has_security ? config.security.serial_enabled : config.device.serial_enabled; + if (!config.has_lora || serialEnabled) + dest->write(c); - return 1; // We always claim one was written, rather than trusting what the - // serial port said (which could be zero) + return 1; // We always claim one was written, rather than trusting what the + // serial port said (which could be zero) } -size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg) -{ - va_list copy; +size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg) { + va_list copy; #if ENABLE_JSON_LOGGING || ARCH_PORTDUINO - static char printBuf[512]; + static char printBuf[512]; #else - static char printBuf[160]; + static char printBuf[160]; #endif #ifdef ARCH_PORTDUINO - bool color = !portduino_config.ascii_logs; + bool color = !portduino_config.ascii_logs; #else - bool color = true; + bool color = true; #endif - va_copy(copy, arg); - size_t len = vsnprintf(printBuf, sizeof(printBuf), format, copy); - va_end(copy); + va_copy(copy, arg); + size_t len = vsnprintf(printBuf, sizeof(printBuf), format, copy); + va_end(copy); - // If the resulting string is longer than sizeof(printBuf)-1 characters, the remaining characters are still counted for the - // return value + // If the resulting string is longer than sizeof(printBuf)-1 characters, the remaining characters are still counted + // for the return value - if (len > sizeof(printBuf) - 1) { - len = sizeof(printBuf) - 1; - printBuf[sizeof(printBuf) - 2] = '\n'; - } - for (size_t f = 0; f < len; f++) { - if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') - printBuf[f] = '#'; - } - if (color && logLevel != nullptr) { - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - Print::write("\u001b[34m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - Print::write("\u001b[32m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - Print::write("\u001b[33m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) - Print::write("\u001b[31m", 5); - } - len = Print::write(printBuf, len); - if (color && logLevel != nullptr) { - Print::write("\u001b[0m", 4); - } - return len; + if (len > sizeof(printBuf) - 1) { + len = sizeof(printBuf) - 1; + printBuf[sizeof(printBuf) - 2] = '\n'; + } + for (size_t f = 0; f < len; f++) { + if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') + printBuf[f] = '#'; + } + if (color && logLevel != nullptr) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 5); + } + len = Print::write(printBuf, len); + if (color && logLevel != nullptr) { + Print::write("\u001b[0m", 4); + } + return len; } -void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) -{ - size_t r = 0; +void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) { + size_t r = 0; #ifdef ARCH_PORTDUINO - bool color = !portduino_config.ascii_logs; + bool color = !portduino_config.ascii_logs; #else - bool color = true; + bool color = true; #endif - // include the header + // include the header + if (color) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) + Print::write("\u001b[35m", 5); + } + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + +#ifdef ARCH_PORTDUINO + ::printf("%s ", logLevel); if (color) { - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - Print::write("\u001b[34m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - Print::write("\u001b[32m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - Print::write("\u001b[33m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) - Print::write("\u001b[31m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) - Print::write("\u001b[35m", 5); + ::printf("\u001b[0m"); } - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - -#ifdef ARCH_PORTDUINO - ::printf("%s ", logLevel); - if (color) { - ::printf("\u001b[0m"); - } - ::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); + ::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); #else - printf("%s ", logLevel); - if (color) { - printf("\u001b[0m"); - } - printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); + printf("%s ", logLevel); + if (color) { + printf("\u001b[0m"); + } + printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); #endif - } else { + } else { #ifdef ARCH_PORTDUINO - ::printf("%s ", logLevel); - if (color) { - ::printf("\u001b[0m"); - } - ::printf("| ??:??:?? %u ", millis() / 1000); + ::printf("%s ", logLevel); + if (color) { + ::printf("\u001b[0m"); + } + ::printf("| ??:??:?? %u ", millis() / 1000); #else - printf("%s ", logLevel); - if (color) { - printf("\u001b[0m"); - } - printf("| ??:??:?? %u ", millis() / 1000); + printf("%s ", logLevel); + if (color) { + printf("\u001b[0m"); + } + printf("| ??:??:?? %u ", millis() / 1000); #endif - } - auto thread = concurrency::OSThread::currentThread; - if (thread) { - print("["); - // printf("%p ", thread); - // assert(thread->ThreadName.length()); - print(thread->ThreadName); - print("] "); - } + } + auto thread = concurrency::OSThread::currentThread; + if (thread) { + print("["); + // printf("%p ", thread); + // assert(thread->ThreadName.length()); + print(thread->ThreadName); + print("] "); + } #ifdef DEBUG_HEAP - // Add heap free space bytes prefix before every log message + // Add heap free space bytes prefix before every log message #ifdef ARCH_PORTDUINO - ::printf("[heap %u] ", memGet.getFreeHeap()); + ::printf("[heap %u] ", memGet.getFreeHeap()); #else - printf("[heap %u] ", memGet.getFreeHeap()); + printf("[heap %u] ", memGet.getFreeHeap()); #endif #endif // DEBUG_HEAP - r += vprintf(logLevel, format, arg); + r += vprintf(logLevel, format, arg); } -void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) -{ +void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) { #if HAS_NETWORKING && !defined(ARCH_PORTDUINO) - // if syslog is in use, collect the log messages and send them to syslog - if (syslog.isEnabled()) { - int ll = 0; - switch (logLevel[0]) { - case 'D': - ll = SYSLOG_DEBUG; - break; - case 'I': - ll = SYSLOG_INFO; - break; - case 'W': - ll = SYSLOG_WARN; - break; - case 'E': - ll = SYSLOG_ERR; - break; - case 'C': - ll = SYSLOG_CRIT; - break; - default: - ll = 0; - } - auto thread = concurrency::OSThread::currentThread; - if (thread) { - syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); - } else { - syslog.vlogf(ll, format, arg); - } - } -#endif -} - -void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_list arg) -{ -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) { - bool isBleConnected = false; -#ifdef ARCH_ESP32 - isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); -#elif defined(ARCH_NRF52) - isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); -#endif - if (isBleConnected) { - char *message; - size_t initialLen; - size_t len; - initialLen = strlen(format); - message = new char[initialLen + 1]; - len = vsnprintf(message, initialLen + 1, format, arg); - if (len > initialLen) { - delete[] message; - message = new char[len + 1]; - vsnprintf(message, len + 1, format, arg); - } - auto thread = concurrency::OSThread::currentThread; - meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; - logRecord.level = getLogLevel(logLevel); - strcpy(logRecord.message, message); - if (thread) - strcpy(logRecord.source, thread->ThreadName.c_str()); - logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); - - uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size]; - size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); -#ifdef ARCH_ESP32 - nimbleBluetooth->sendLog(buffer, size); -#elif defined(ARCH_NRF52) - nrf52Bluetooth->sendLog(buffer, size); -#endif - delete[] message; - delete[] buffer; - } - } -#else - (void)logLevel; - (void)format; - (void)arg; -#endif -} - -meshtastic_LogRecord_Level RedirectablePrint::getLogLevel(const char *logLevel) -{ - meshtastic_LogRecord_Level ll = meshtastic_LogRecord_Level_UNSET; // default to unset + // if syslog is in use, collect the log messages and send them to syslog + if (syslog.isEnabled()) { + int ll = 0; switch (logLevel[0]) { case 'D': - ll = meshtastic_LogRecord_Level_DEBUG; - break; + ll = SYSLOG_DEBUG; + break; case 'I': - ll = meshtastic_LogRecord_Level_INFO; - break; + ll = SYSLOG_INFO; + break; case 'W': - ll = meshtastic_LogRecord_Level_WARNING; - break; + ll = SYSLOG_WARN; + break; case 'E': - ll = meshtastic_LogRecord_Level_ERROR; - break; + ll = SYSLOG_ERR; + break; case 'C': - ll = meshtastic_LogRecord_Level_CRITICAL; - break; + ll = SYSLOG_CRIT; + break; + default: + ll = 0; } - return ll; + auto thread = concurrency::OSThread::currentThread; + if (thread) { + syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); + } else { + syslog.vlogf(ll, format, arg); + } + } +#endif } -void RedirectablePrint::log(const char *logLevel, const char *format, ...) -{ +void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_list arg) { +#if !MESHTASTIC_EXCLUDE_BLUETOOTH + if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) { + bool isBleConnected = false; +#ifdef ARCH_ESP32 + isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); +#elif defined(ARCH_NRF52) + isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); +#endif + if (isBleConnected) { + char *message; + size_t initialLen; + size_t len; + initialLen = strlen(format); + message = new char[initialLen + 1]; + len = vsnprintf(message, initialLen + 1, format, arg); + if (len > initialLen) { + delete[] message; + message = new char[len + 1]; + vsnprintf(message, len + 1, format, arg); + } + auto thread = concurrency::OSThread::currentThread; + meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; + logRecord.level = getLogLevel(logLevel); + strcpy(logRecord.message, message); + if (thread) + strcpy(logRecord.source, thread->ThreadName.c_str()); + logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); - // append \n to format - size_t len = strlen(format); - char *newFormat = new char[len + 2]; - strcpy(newFormat, format); - newFormat[len] = '\n'; - newFormat[len + 1] = '\0'; + uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size]; + size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); +#ifdef ARCH_ESP32 + nimbleBluetooth->sendLog(buffer, size); +#elif defined(ARCH_NRF52) + nrf52Bluetooth->sendLog(buffer, size); +#endif + delete[] message; + delete[] buffer; + } + } +#else + (void)logLevel; + (void)format; + (void)arg; +#endif +} + +meshtastic_LogRecord_Level RedirectablePrint::getLogLevel(const char *logLevel) { + meshtastic_LogRecord_Level ll = meshtastic_LogRecord_Level_UNSET; // default to unset + switch (logLevel[0]) { + case 'D': + ll = meshtastic_LogRecord_Level_DEBUG; + break; + case 'I': + ll = meshtastic_LogRecord_Level_INFO; + break; + case 'W': + ll = meshtastic_LogRecord_Level_WARNING; + break; + case 'E': + ll = meshtastic_LogRecord_Level_ERROR; + break; + case 'C': + ll = meshtastic_LogRecord_Level_CRITICAL; + break; + } + return ll; +} + +void RedirectablePrint::log(const char *logLevel, const char *format, ...) { + + // append \n to format + size_t len = strlen(format); + char *newFormat = new char[len + 2]; + strcpy(newFormat, format); + newFormat[len] = '\n'; + newFormat[len + 1] = '\0'; #if ARCH_PORTDUINO - // level trace is special, two possible ways to handle it. - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - if (portduino_config.traceFilename != "") { - va_list arg; - va_start(arg, format); - try { - traceFile << va_arg(arg, char *) << std::endl; - } catch (const std::ios_base::failure &e) { - } - va_end(arg); - } - if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - delete[] newFormat; - return; - } + // level trace is special, two possible ways to handle it. + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + if (portduino_config.traceFilename != "") { + va_list arg; + va_start(arg, format); + try { + traceFile << va_arg(arg, char *) << std::endl; + } catch (const std::ios_base::failure &e) { + } + va_end(arg); } - if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; - return; - } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { - delete[] newFormat; - return; - } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { - delete[] newFormat; - return; + if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + delete[] newFormat; + return; } -#endif - if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; - return; - } - -#ifdef HAS_FREE_RTOS - if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { -#else - if (!inDebugPrint) { - inDebugPrint = true; -#endif - - va_list arg; - va_start(arg, format); - - log_to_serial(logLevel, newFormat, arg); - log_to_syslog(logLevel, newFormat, arg); - log_to_ble(logLevel, newFormat, arg); - - va_end(arg); -#ifdef HAS_FREE_RTOS - xSemaphoreGive(inDebugPrint); -#else - inDebugPrint = false; -#endif - } - + } + if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { delete[] newFormat; return; + } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { + delete[] newFormat; + return; + } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { + delete[] newFormat; + return; + } +#endif + if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + delete[] newFormat; + return; + } + +#ifdef HAS_FREE_RTOS + if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { +#else + if (!inDebugPrint) { + inDebugPrint = true; +#endif + + va_list arg; + va_start(arg, format); + + log_to_serial(logLevel, newFormat, arg); + log_to_syslog(logLevel, newFormat, arg); + log_to_ble(logLevel, newFormat, arg); + + va_end(arg); +#ifdef HAS_FREE_RTOS + xSemaphoreGive(inDebugPrint); +#else + inDebugPrint = false; +#endif + } + + delete[] newFormat; + return; } -void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) -{ - const char alphabet[17] = "0123456789abcdef"; - log(logLevel, " +------------------------------------------------+ +----------------+"); - log(logLevel, " |.0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .a .b .c .d .e .f | | ASCII |"); - for (uint16_t i = 0; i < len; i += 16) { - if (i % 128 == 0) - log(logLevel, " +------------------------------------------------+ +----------------+"); - char s[] = " | | | |\n"; - uint8_t ix = 5, iy = 56; - for (uint8_t j = 0; j < 16; j++) { - if (i + j < len) { - uint8_t c = buf[i + j]; - s[ix++] = alphabet[(c >> 4) & 0x0F]; - s[ix++] = alphabet[c & 0x0F]; - ix++; - if (c > 31 && c < 128) - s[iy++] = c; - else - s[iy++] = '.'; - } - } - uint8_t index = i / 16; - sprintf(s, "%03x", index); - s[3] = '.'; - log(logLevel, s); - } - log(logLevel, " +------------------------------------------------+ +----------------+"); -} - -std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) -{ - int n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */ - std::unique_ptr formatted; - va_list ap; - while (1) { - formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */ - strcpy(&formatted[0], fmt_str.c_str()); - va_start(ap, fmt_str); - int final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap); - va_end(ap); - if (final_n < 0 || final_n >= n) - n += abs(final_n - n + 1); +void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) { + const char alphabet[17] = "0123456789abcdef"; + log(logLevel, " +------------------------------------------------+ +----------------+"); + log(logLevel, " |.0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .a .b .c .d .e .f | | ASCII |"); + for (uint16_t i = 0; i < len; i += 16) { + if (i % 128 == 0) + log(logLevel, " +------------------------------------------------+ +----------------+"); + char s[] = " | | | |\n"; + uint8_t ix = 5, iy = 56; + for (uint8_t j = 0; j < 16; j++) { + if (i + j < len) { + uint8_t c = buf[i + j]; + s[ix++] = alphabet[(c >> 4) & 0x0F]; + s[ix++] = alphabet[c & 0x0F]; + ix++; + if (c > 31 && c < 128) + s[iy++] = c; else - break; + s[iy++] = '.'; + } } - return std::string(formatted.get()); + uint8_t index = i / 16; + sprintf(s, "%03x", index); + s[3] = '.'; + log(logLevel, s); + } + log(logLevel, " +------------------------------------------------+ +----------------+"); +} + +std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) { + int n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */ + std::unique_ptr formatted; + va_list ap; + while (1) { + formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */ + strcpy(&formatted[0], fmt_str.c_str()); + va_start(ap, fmt_str); + int final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap); + va_end(ap); + if (final_n < 0 || final_n >= n) + n += abs(final_n - n + 1); + else + break; + } + return std::string(formatted.get()); } diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 45b62b7af..44e418c74 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -11,49 +11,48 @@ * This class is mostly useful to allow debug printing to be redirected away from Serial * to some other transport if we switch Serial usage (on the fly) to some other purpose. */ -class RedirectablePrint : public Print -{ - Print *dest; +class RedirectablePrint : public Print { + Print *dest; #ifdef HAS_FREE_RTOS - SemaphoreHandle_t inDebugPrint = nullptr; - StaticSemaphore_t _MutexStorageSpace; + SemaphoreHandle_t inDebugPrint = nullptr; + StaticSemaphore_t _MutexStorageSpace; #else - volatile bool inDebugPrint = false; + volatile bool inDebugPrint = false; #endif - public: - explicit RedirectablePrint(Print *_dest) : dest(_dest) {} +public: + explicit RedirectablePrint(Print *_dest) : dest(_dest) {} - /** - * Set a new destination - */ - void rpInit(); - void setDestination(Print *dest); + /** + * Set a new destination + */ + void rpInit(); + void setDestination(Print *dest); - virtual size_t write(uint8_t c); + virtual size_t write(uint8_t c); - /** - * Debug logging print message - * - * If the provide format string ends with a newline we assume it is the final print of a single - * log message. Otherwise we assume more prints will come before the log message ends. This - * allows you to call logDebug a few times to build up a single log message line if you wish. - */ - void log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); + /** + * Debug logging print message + * + * If the provide format string ends with a newline we assume it is the final print of a single + * log message. Otherwise we assume more prints will come before the log message ends. This + * allows you to call logDebug a few times to build up a single log message line if you wish. + */ + void log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); - /** like printf but va_list based */ - size_t vprintf(const char *logLevel, const char *format, va_list arg); + /** like printf but va_list based */ + size_t vprintf(const char *logLevel, const char *format, va_list arg); - void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); + void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); - std::string mt_sprintf(const std::string fmt_str, ...); + std::string mt_sprintf(const std::string fmt_str, ...); - protected: - /// Subclasses can override if they need to change how we format over the serial port - virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); - meshtastic_LogRecord_Level getLogLevel(const char *logLevel); +protected: + /// Subclasses can override if they need to change how we format over the serial port + virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); + meshtastic_LogRecord_Level getLogLevel(const char *logLevel); - private: - void log_to_syslog(const char *logLevel, const char *format, va_list arg); - void log_to_ble(const char *logLevel, const char *format, va_list arg); +private: + void log_to_syslog(const char *logLevel, const char *format, va_list arg); + void log_to_ble(const char *logLevel, const char *format, va_list arg); }; \ No newline at end of file diff --git a/src/SPILock.cpp b/src/SPILock.cpp index 13fa556fc..0a0e0fbfd 100644 --- a/src/SPILock.cpp +++ b/src/SPILock.cpp @@ -5,8 +5,7 @@ concurrency::Lock *spiLock; -void initSPI() -{ - assert(!spiLock); - spiLock = new concurrency::Lock(); +void initSPI() { + assert(!spiLock); + spiLock = new concurrency::Lock(); } \ No newline at end of file diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp index 45b96ad07..3a7face87 100644 --- a/src/SafeFile.cpp +++ b/src/SafeFile.cpp @@ -3,54 +3,48 @@ #ifdef FSCom // Only way to work on both esp32 and nrf52 -static File openFile(const char *filename, bool fullAtomic) -{ - concurrency::LockGuard g(spiLock); - LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); +static File openFile(const char *filename, bool fullAtomic) { + concurrency::LockGuard g(spiLock); + LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); #ifdef ARCH_NRF52 - FSCom.remove(filename); - return FSCom.open(filename, FILE_O_WRITE); + FSCom.remove(filename); + return FSCom.open(filename, FILE_O_WRITE); #endif - if (!fullAtomic) { - FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) - } + if (!fullAtomic) { + FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) + } - String filenameTmp = filename; - filenameTmp += ".tmp"; + String filenameTmp = filename; + filenameTmp += ".tmp"; - // FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now - // if (fullAtomic) { - // FSCom.remove(filename); - // } + // FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now + // if (fullAtomic) { + // FSCom.remove(filename); + // } - // clear any previous LFS errors - return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE); + // clear any previous LFS errors + return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE); } -SafeFile::SafeFile(const char *_filename, bool fullAtomic) - : filename(_filename), f(openFile(_filename, fullAtomic)), fullAtomic(fullAtomic) -{ +SafeFile::SafeFile(const char *_filename, bool fullAtomic) : filename(_filename), f(openFile(_filename, fullAtomic)), fullAtomic(fullAtomic) {} + +size_t SafeFile::write(uint8_t ch) { + if (!f) + return 0; + + hash ^= ch; + return f.write(ch); } -size_t SafeFile::write(uint8_t ch) -{ - if (!f) - return 0; +size_t SafeFile::write(const uint8_t *buffer, size_t size) { + if (!f) + return 0; - hash ^= ch; - return f.write(ch); -} - -size_t SafeFile::write(const uint8_t *buffer, size_t size) -{ - if (!f) - return 0; - - for (size_t i = 0; i < size; i++) { - hash ^= buffer[i]; - } - return f.write((uint8_t const *)buffer, size); // This nasty cast is _IMPORTANT_ otherwise the correct adafruit method does - // not get used (they made a mistake in their typing) + for (size_t i = 0; i < size; i++) { + hash ^= buffer[i]; + } + return f.write((uint8_t const *)buffer, size); // This nasty cast is _IMPORTANT_ otherwise the correct adafruit method + // does not get used (they made a mistake in their typing) } /** @@ -58,66 +52,64 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size) * * @return false for failure */ -bool SafeFile::close() -{ - if (!f) - return false; +bool SafeFile::close() { + if (!f) + return false; - spiLock->lock(); - f.close(); - spiLock->unlock(); + spiLock->lock(); + f.close(); + spiLock->unlock(); #ifdef ARCH_NRF52 - return true; + return true; #endif - if (!testReadback()) - return false; + if (!testReadback()) + return false; - { // Scope for lock - concurrency::LockGuard g(spiLock); - // brief window of risk here ;-) - if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) { - LOG_ERROR("Can't remove old pref file"); - return false; - } + { // Scope for lock + concurrency::LockGuard g(spiLock); + // brief window of risk here ;-) + if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) { + LOG_ERROR("Can't remove old pref file"); + return false; } + } - String filenameTmp = filename; - filenameTmp += ".tmp"; - if (!renameFile(filenameTmp.c_str(), filename.c_str())) { - LOG_ERROR("Error: can't rename new pref file"); - return false; - } + String filenameTmp = filename; + filenameTmp += ".tmp"; + if (!renameFile(filenameTmp.c_str(), filename.c_str())) { + LOG_ERROR("Error: can't rename new pref file"); + return false; + } - return true; + return true; } /// Read our (closed) tempfile back in and compare the hash -bool SafeFile::testReadback() -{ - concurrency::LockGuard g(spiLock); +bool SafeFile::testReadback() { + concurrency::LockGuard g(spiLock); - String filenameTmp = filename; - filenameTmp += ".tmp"; - auto f2 = FSCom.open(filenameTmp.c_str(), FILE_O_READ); - if (!f2) { - LOG_ERROR("Can't open tmp file for readback"); - return false; - } + String filenameTmp = filename; + filenameTmp += ".tmp"; + auto f2 = FSCom.open(filenameTmp.c_str(), FILE_O_READ); + if (!f2) { + LOG_ERROR("Can't open tmp file for readback"); + return false; + } - int c = 0; - uint8_t test_hash = 0; - while ((c = f2.read()) >= 0) { - test_hash ^= (uint8_t)c; - } - f2.close(); + int c = 0; + uint8_t test_hash = 0; + while ((c = f2.read()) >= 0) { + test_hash ^= (uint8_t)c; + } + f2.close(); - if (test_hash != hash) { - LOG_ERROR("Readback failed hash mismatch"); - return false; - } + if (test_hash != hash) { + LOG_ERROR("Readback failed hash mismatch"); + return false; + } - return true; + return true; } #endif \ No newline at end of file diff --git a/src/SafeFile.h b/src/SafeFile.h index 3d0f81cad..a03b3b4f9 100644 --- a/src/SafeFile.h +++ b/src/SafeFile.h @@ -10,41 +10,41 @@ * This class provides 'safe'/paranoid file writing. * * Some of our filesystems (in particular the nrf52) may have bugs beneath our layer. Therefore we want to - * be very careful about how we write files. This class provides a restricted (Stream only) writing API for writing to files. + * be very careful about how we write files. This class provides a restricted (Stream only) writing API for writing to + * files. * * Notably: * - we keep a simple xor hash of all characters that were written. * - We do not allow seeking (because we want to maintain our hash) - * - we provide an close() method which is similar to close but returns false if we were unable to successfully write the - * file. Also this method - * - atomically replaces any old version of the file on the disk with our new file (after first rereading the file from the disk - * to confirm the hash matches) - * - Some files are super huge so we can't do the full atomic rename/copy (because of filesystem size limits). If !fullAtomic - * then we still do the readback to verify file is valid so higher level code can handle failures. + * - we provide an close() method which is similar to close but returns false if we were unable to successfully write + * the file. Also this method + * - atomically replaces any old version of the file on the disk with our new file (after first rereading the file from + * the disk to confirm the hash matches) + * - Some files are super huge so we can't do the full atomic rename/copy (because of filesystem size limits). If + * !fullAtomic then we still do the readback to verify file is valid so higher level code can handle failures. */ -class SafeFile : public Print -{ - public: - explicit SafeFile(char const *filepath, bool fullAtomic = false); +class SafeFile : public Print { +public: + explicit SafeFile(char const *filepath, bool fullAtomic = false); - virtual size_t write(uint8_t); - virtual size_t write(const uint8_t *buffer, size_t size); + virtual size_t write(uint8_t); + virtual size_t write(const uint8_t *buffer, size_t size); - /** - * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches - * - * @return false for failure - */ - bool close(); + /** + * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches + * + * @return false for failure + */ + bool close(); - private: - /// Read our (closed) tempfile back in and compare the hash - bool testReadback(); +private: + /// Read our (closed) tempfile back in and compare the hash + bool testReadback(); - String filename; - File f; - bool fullAtomic; - uint8_t hash = 0; + String filename; + File f; + bool fullAtomic; + uint8_t hash = 0; }; #endif \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index dd2acb599..268e3f19e 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -28,121 +28,105 @@ SerialConsole *console; -void consoleInit() -{ - auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread +void consoleInit() { + auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread #if defined(SERIAL_HAS_ON_RECEIVE) - // onReceive does only exist for HardwareSerial not for USB CDC serial - Port.onReceive([sc]() { sc->rxInt(); }); + // onReceive does only exist for HardwareSerial not for USB CDC serial + Port.onReceive([sc]() { sc->rxInt(); }); #endif - DEBUG_PORT.rpInit(); // Simply sets up semaphore + DEBUG_PORT.rpInit(); // Simply sets up semaphore } -void consolePrintf(const char *format, ...) -{ - va_list arg; - va_start(arg, format); - console->vprintf(nullptr, format, arg); - va_end(arg); - console->flush(); +void consolePrintf(const char *format, ...) { + va_list arg; + va_start(arg, format); + console->vprintf(nullptr, format, arg); + va_end(arg); + console->flush(); } -SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole") -{ - api_type = TYPE_SERIAL; - assert(!console); - console = this; - canWrite = false; // We don't send packets to our port until it has talked to us first +SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole") { + api_type = TYPE_SERIAL; + assert(!console); + console = this; + canWrite = false; // We don't send packets to our port until it has talked to us first #ifdef RP2040_SLOW_CLOCK - Port.setTX(SERIAL2_TX); - Port.setRX(SERIAL2_RX); + Port.setTX(SERIAL2_TX); + Port.setRX(SERIAL2_RX); #endif - Port.begin(SERIAL_BAUD); -#if defined(ARCH_NRF52) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(ARCH_RP2040) || \ + Port.begin(SERIAL_BAUD); +#if defined(ARCH_NRF52) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(ARCH_RP2040) || \ defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) - time_t timeout = millis(); - while (!Port) { - if (Throttle::isWithinTimespanMs(timeout, FIVE_SECONDS_MS)) { - delay(100); - } else { - break; - } + time_t timeout = millis(); + while (!Port) { + if (Throttle::isWithinTimespanMs(timeout, FIVE_SECONDS_MS)) { + delay(100); + } else { + break; } + } #endif #if !ARCH_PORTDUINO - emitRebooted(); + emitRebooted(); #endif } -int32_t SerialConsole::runOnce() -{ +int32_t SerialConsole::runOnce() { #ifdef HELTEC_MESH_SOLAR - // After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. - if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port && - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) { - return 250; - } + // After enabling the mesh solar serial port module configuration, command processing is handled by the serial port + // module. + if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port && + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) { + return 250; + } #endif - int32_t delay = runOncePart(); + int32_t delay = runOncePart(); #if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2) - return Port.available() ? delay : INT32_MAX; + return Port.available() ? delay : INT32_MAX; #elif defined(IS_USB_SERIAL) - return HWCDC::isPlugged() ? delay : (1000 * 20); + return HWCDC::isPlugged() ? delay : (1000 * 20); #else - return delay; + return delay; #endif } -void SerialConsole::flush() -{ - Port.flush(); -} +void SerialConsole::flush() { Port.flush(); } // trigger tx of serial data -void SerialConsole::onNowHasData(uint32_t fromRadioNum) -{ - setIntervalFromNow(0); -} +void SerialConsole::onNowHasData(uint32_t fromRadioNum) { setIntervalFromNow(0); } // trigger rx of serial data -void SerialConsole::rxInt() -{ - setIntervalFromNow(0); -} +void SerialConsole::rxInt() { setIntervalFromNow(0); } -// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages -bool SerialConsole::checkIsConnected() -{ - return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); -} +// For the serial port we can't really detect if any client is on the other side, so instead just look for recent +// messages +bool SerialConsole::checkIsConnected() { return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); } /** * we override this to notice when we've received a protobuf over the serial * stream. Then we shut off debug serial output. */ -bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len) -{ - // only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled. - if (config.has_lora && config.security.serial_enabled) { - // Switch to protobufs for log messages - usingProtobufs = true; - canWrite = true; +bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len) { + // only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled. + if (config.has_lora && config.security.serial_enabled) { + // Switch to protobufs for log messages + usingProtobufs = true; + canWrite = true; - return StreamAPI::handleToRadio(buf, len); - } else { - return false; - } + return StreamAPI::handleToRadio(buf, len); + } else { + return false; + } } -void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg) -{ - if (usingProtobufs && config.security.debug_log_api_enabled) { - meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel); - auto thread = concurrency::OSThread::currentThread; - emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg); - } else - RedirectablePrint::log_to_serial(logLevel, format, arg); +void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg) { + if (usingProtobufs && config.security.debug_log_api_enabled) { + meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel); + auto thread = concurrency::OSThread::currentThread; + emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg); + } else + RedirectablePrint::log_to_serial(logLevel, format, arg); } \ No newline at end of file diff --git a/src/SerialConsole.h b/src/SerialConsole.h index 98577e4bc..e920ee80d 100644 --- a/src/SerialConsole.h +++ b/src/SerialConsole.h @@ -6,42 +6,40 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -class SerialConsole : public StreamAPI, public RedirectablePrint, private concurrency::OSThread -{ - /** - * If true we are talking to a smart host and all messages (including log messages) must be framed as protobufs. - */ - bool usingProtobufs = false; +class SerialConsole : public StreamAPI, public RedirectablePrint, private concurrency::OSThread { + /** + * If true we are talking to a smart host and all messages (including log messages) must be framed as protobufs. + */ + bool usingProtobufs = false; - public: - SerialConsole(); +public: + SerialConsole(); - /** - * we override this to notice when we've received a protobuf over the serial stream. Then we shunt off - * debug serial output. - */ - virtual bool handleToRadio(const uint8_t *buf, size_t len) override; + /** + * we override this to notice when we've received a protobuf over the serial stream. Then we shunt off + * debug serial output. + */ + virtual bool handleToRadio(const uint8_t *buf, size_t len) override; - virtual size_t write(uint8_t c) override - { - if (c == '\n') // prefix any newlines with carriage return - RedirectablePrint::write('\r'); - return RedirectablePrint::write(c); - } + virtual size_t write(uint8_t c) override { + if (c == '\n') // prefix any newlines with carriage return + RedirectablePrint::write('\r'); + return RedirectablePrint::write(c); + } - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - void flush(); - void rxInt(); + void flush(); + void rxInt(); - protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; +protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; - virtual void onNowHasData(uint32_t fromRadioNum) override; + virtual void onNowHasData(uint32_t fromRadioNum) override; - /// Possibly switch to protobufs if we see a valid protobuf message - virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); + /// Possibly switch to protobufs if we see a valid protobuf message + virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); }; // A simple wrapper to allow non class aware code write to the console diff --git a/src/Status.h b/src/Status.h index 59d443ab7..893c40544 100644 --- a/src/Status.h +++ b/src/Status.h @@ -9,49 +9,45 @@ #define STATUS_TYPE_NODE 3 #define STATUS_TYPE_BLUETOOTH 4 -namespace meshtastic -{ +namespace meshtastic { // A base class for observable status -class Status -{ - protected: - // Allows us to observe an Observable - CallbackObserver statusObserver = - CallbackObserver(this, &Status::updateStatus); - bool initialized = false; - // Workaround for no typeid support - int statusType = 0; +class Status { +protected: + // Allows us to observe an Observable + CallbackObserver statusObserver = CallbackObserver(this, &Status::updateStatus); + bool initialized = false; + // Workaround for no typeid support + int statusType = 0; - public: - // Allows us to generate observable events - Observable onNewStatus; +public: + // Allows us to generate observable events + Observable onNewStatus; - // Enable polymorphism ? - virtual ~Status() = default; + // Enable polymorphism ? + virtual ~Status() = default; - Status() - { - if (!statusType) { - statusType = STATUS_TYPE_BASE; - } + Status() { + if (!statusType) { + statusType = STATUS_TYPE_BASE; } + } - // Prevent object copy/move - Status(const Status &) = delete; - Status &operator=(const Status &) = delete; + // Prevent object copy/move + Status(const Status &) = delete; + Status &operator=(const Status &) = delete; - // Start observing a source of data - void observe(Observable *source) { statusObserver.observe(source); } + // Start observing a source of data + void observe(Observable *source) { statusObserver.observe(source); } - // Determines whether or not existing data matches the data in another Status instance - bool matches(const Status *otherStatus) const { return true; } + // Determines whether or not existing data matches the data in another Status instance + bool matches(const Status *otherStatus) const { return true; } - bool isInitialized() const { return initialized; } + bool isInitialized() const { return initialized; } - int getStatusType() const { return statusType; } + int getStatusType() const { return statusType; } - // Called when the Observable we're observing generates a new notification - int updateStatus(const Status *newStatus) { return 0; } + // Called when the Observable we're observing generates a new notification + int updateStatus(const Status *newStatus) { return 0; } }; }; // namespace meshtastic diff --git a/src/airtime.cpp b/src/airtime.cpp index a7736d667..e5702544e 100644 --- a/src/airtime.cpp +++ b/src/airtime.cpp @@ -9,202 +9,175 @@ AirTime *airTime = NULL; uint32_t air_period_tx[PERIODS_TO_LOG]; uint32_t air_period_rx[PERIODS_TO_LOG]; -void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms) -{ +void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms) { - if (reportType == TX_LOG) { - LOG_DEBUG("Packet TX: %ums", airtime_ms); - this->airtimes.periodTX[0] = this->airtimes.periodTX[0] + airtime_ms; - air_period_tx[0] = air_period_tx[0] + airtime_ms; + if (reportType == TX_LOG) { + LOG_DEBUG("Packet TX: %ums", airtime_ms); + this->airtimes.periodTX[0] = this->airtimes.periodTX[0] + airtime_ms; + air_period_tx[0] = air_period_tx[0] + airtime_ms; - this->utilizationTX[this->getPeriodUtilHour()] = this->utilizationTX[this->getPeriodUtilHour()] + airtime_ms; - } else if (reportType == RX_LOG) { - LOG_DEBUG("Packet RX: %ums", airtime_ms); - this->airtimes.periodRX[0] = this->airtimes.periodRX[0] + airtime_ms; - air_period_rx[0] = air_period_rx[0] + airtime_ms; - } else if (reportType == RX_ALL_LOG) { - LOG_DEBUG("Packet RX (noise?) : %ums", airtime_ms); - this->airtimes.periodRX_ALL[0] = this->airtimes.periodRX_ALL[0] + airtime_ms; + this->utilizationTX[this->getPeriodUtilHour()] = this->utilizationTX[this->getPeriodUtilHour()] + airtime_ms; + } else if (reportType == RX_LOG) { + LOG_DEBUG("Packet RX: %ums", airtime_ms); + this->airtimes.periodRX[0] = this->airtimes.periodRX[0] + airtime_ms; + air_period_rx[0] = air_period_rx[0] + airtime_ms; + } else if (reportType == RX_ALL_LOG) { + LOG_DEBUG("Packet RX (noise?) : %ums", airtime_ms); + this->airtimes.periodRX_ALL[0] = this->airtimes.periodRX_ALL[0] + airtime_ms; + } + + // Log all airtime type for channel utilization + this->channelUtilization[this->getPeriodUtilMinute()] = channelUtilization[this->getPeriodUtilMinute()] + airtime_ms; +} + +uint8_t AirTime::currentPeriodIndex() { return ((getSecondsSinceBoot() / SECONDS_PER_PERIOD) % PERIODS_TO_LOG); } + +uint8_t AirTime::getPeriodUtilMinute() { return (getSecondsSinceBoot() / 10) % CHANNEL_UTILIZATION_PERIODS; } + +uint8_t AirTime::getPeriodUtilHour() { return (getSecondsSinceBoot() / 60) % MINUTES_IN_HOUR; } + +void AirTime::airtimeRotatePeriod() { + + if (this->airtimes.lastPeriodIndex != this->currentPeriodIndex()) { + LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex()); + + for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) { + this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i]; + this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i]; + this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i]; + + air_period_tx[i + 1] = this->airtimes.periodTX[i]; + air_period_rx[i + 1] = this->airtimes.periodRX[i]; } - // Log all airtime type for channel utilization - this->channelUtilization[this->getPeriodUtilMinute()] = channelUtilization[this->getPeriodUtilMinute()] + airtime_ms; + this->airtimes.periodTX[0] = 0; + this->airtimes.periodRX[0] = 0; + this->airtimes.periodRX_ALL[0] = 0; + + air_period_tx[0] = 0; + air_period_rx[0] = 0; + + this->airtimes.lastPeriodIndex = this->currentPeriodIndex(); + } } -uint8_t AirTime::currentPeriodIndex() -{ - return ((getSecondsSinceBoot() / SECONDS_PER_PERIOD) % PERIODS_TO_LOG); +uint32_t *AirTime::airtimeReport(reportTypes reportType) { + + if (reportType == TX_LOG) { + return this->airtimes.periodTX; + } else if (reportType == RX_LOG) { + return this->airtimes.periodRX; + } else if (reportType == RX_ALL_LOG) { + return this->airtimes.periodRX_ALL; + } + return 0; } -uint8_t AirTime::getPeriodUtilMinute() -{ - return (getSecondsSinceBoot() / 10) % CHANNEL_UTILIZATION_PERIODS; +uint8_t AirTime::getPeriodsToLog() { return PERIODS_TO_LOG; } + +uint32_t AirTime::getSecondsPerPeriod() { return SECONDS_PER_PERIOD; } + +uint32_t AirTime::getSecondsSinceBoot() { return this->secSinceBoot; } + +float AirTime::channelUtilizationPercent() { + uint32_t sum = 0; + for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { + sum += this->channelUtilization[i]; + } + + return (float(sum) / float(CHANNEL_UTILIZATION_PERIODS * 10 * 1000)) * 100; } -uint8_t AirTime::getPeriodUtilHour() -{ - return (getSecondsSinceBoot() / 60) % MINUTES_IN_HOUR; +float AirTime::utilizationTXPercent() { + uint32_t sum = 0; + for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { + sum += this->utilizationTX[i]; + } + + return (float(sum) / float(MS_IN_HOUR)) * 100; } -void AirTime::airtimeRotatePeriod() -{ - - if (this->airtimes.lastPeriodIndex != this->currentPeriodIndex()) { - LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex()); - - for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) { - this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i]; - this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i]; - this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i]; - - air_period_tx[i + 1] = this->airtimes.periodTX[i]; - air_period_rx[i + 1] = this->airtimes.periodRX[i]; - } - - this->airtimes.periodTX[0] = 0; - this->airtimes.periodRX[0] = 0; - this->airtimes.periodRX_ALL[0] = 0; - - air_period_tx[0] = 0; - air_period_rx[0] = 0; - - this->airtimes.lastPeriodIndex = this->currentPeriodIndex(); - } -} - -uint32_t *AirTime::airtimeReport(reportTypes reportType) -{ - - if (reportType == TX_LOG) { - return this->airtimes.periodTX; - } else if (reportType == RX_LOG) { - return this->airtimes.periodRX; - } else if (reportType == RX_ALL_LOG) { - return this->airtimes.periodRX_ALL; - } - return 0; -} - -uint8_t AirTime::getPeriodsToLog() -{ - return PERIODS_TO_LOG; -} - -uint32_t AirTime::getSecondsPerPeriod() -{ - return SECONDS_PER_PERIOD; -} - -uint32_t AirTime::getSecondsSinceBoot() -{ - return this->secSinceBoot; -} - -float AirTime::channelUtilizationPercent() -{ - uint32_t sum = 0; - for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { - sum += this->channelUtilization[i]; - } - - return (float(sum) / float(CHANNEL_UTILIZATION_PERIODS * 10 * 1000)) * 100; -} - -float AirTime::utilizationTXPercent() -{ - uint32_t sum = 0; - for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { - sum += this->utilizationTX[i]; - } - - return (float(sum) / float(MS_IN_HOUR)) * 100; -} - -bool AirTime::isTxAllowedChannelUtil(bool polite) -{ - uint8_t percentage = (polite ? polite_channel_util_percent : max_channel_util_percent); - if (channelUtilizationPercent() < percentage) { - return true; - } else { - LOG_WARN("Ch. util >%d%%. Skip send", percentage); - return false; - } -} - -bool AirTime::isTxAllowedAirUtil() -{ - if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { - if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) { - return true; - } else { - LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100); - return false; - } - } +bool AirTime::isTxAllowedChannelUtil(bool polite) { + uint8_t percentage = (polite ? polite_channel_util_percent : max_channel_util_percent); + if (channelUtilizationPercent() < percentage) { return true; + } else { + LOG_WARN("Ch. util >%d%%. Skip send", percentage); + return false; + } +} + +bool AirTime::isTxAllowedAirUtil() { + if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { + if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) { + return true; + } else { + LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100); + return false; + } + } + return true; } // Get the amount of minutes we have to be silent before we can send again -uint8_t AirTime::getSilentMinutes(float txPercent, float dutyCycle) -{ - float newTxPercent = txPercent; - for (int8_t i = MINUTES_IN_HOUR - 1; i >= 0; --i) { - newTxPercent -= ((float)this->utilizationTX[i] / (MS_IN_MINUTE * MINUTES_IN_HOUR / 100)); - if (newTxPercent < dutyCycle) - return MINUTES_IN_HOUR - 1 - i; - } +uint8_t AirTime::getSilentMinutes(float txPercent, float dutyCycle) { + float newTxPercent = txPercent; + for (int8_t i = MINUTES_IN_HOUR - 1; i >= 0; --i) { + newTxPercent -= ((float)this->utilizationTX[i] / (MS_IN_MINUTE * MINUTES_IN_HOUR / 100)); + if (newTxPercent < dutyCycle) + return MINUTES_IN_HOUR - 1 - i; + } - return MINUTES_IN_HOUR; + return MINUTES_IN_HOUR; } AirTime::AirTime() : concurrency::OSThread("AirTime"), airtimes({}) {} -int32_t AirTime::runOnce() -{ - secSinceBoot++; +int32_t AirTime::runOnce() { + secSinceBoot++; - uint8_t utilPeriod = this->getPeriodUtilMinute(); - uint8_t utilPeriodTX = this->getPeriodUtilHour(); + uint8_t utilPeriod = this->getPeriodUtilMinute(); + uint8_t utilPeriodTX = this->getPeriodUtilHour(); - if (firstTime) { + if (firstTime) { - // Init utilizationTX window to all 0 - for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { - this->utilizationTX[i] = 0; - } - - // Init channelUtilization window to all 0 - for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { - this->channelUtilization[i] = 0; - } - - // Init airtime windows to all 0 - for (int i = 0; i < PERIODS_TO_LOG; i++) { - this->airtimes.periodTX[i] = 0; - this->airtimes.periodRX[i] = 0; - this->airtimes.periodRX_ALL[i] = 0; - - // air_period_tx[i] = 0; - // air_period_rx[i] = 0; - } - - firstTime = false; - lastUtilPeriod = utilPeriod; - } else { - this->airtimeRotatePeriod(); - - // Reset the channelUtilization window when we roll over - if (lastUtilPeriod != utilPeriod) { - lastUtilPeriod = utilPeriod; - - this->channelUtilization[utilPeriod] = 0; - } - - if (lastUtilPeriodTX != utilPeriodTX) { - lastUtilPeriodTX = utilPeriodTX; - - this->utilizationTX[utilPeriodTX] = 0; - } + // Init utilizationTX window to all 0 + for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { + this->utilizationTX[i] = 0; } - return (1000 * 1); + + // Init channelUtilization window to all 0 + for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { + this->channelUtilization[i] = 0; + } + + // Init airtime windows to all 0 + for (int i = 0; i < PERIODS_TO_LOG; i++) { + this->airtimes.periodTX[i] = 0; + this->airtimes.periodRX[i] = 0; + this->airtimes.periodRX_ALL[i] = 0; + + // air_period_tx[i] = 0; + // air_period_rx[i] = 0; + } + + firstTime = false; + lastUtilPeriod = utilPeriod; + } else { + this->airtimeRotatePeriod(); + + // Reset the channelUtilization window when we roll over + if (lastUtilPeriod != utilPeriod) { + lastUtilPeriod = utilPeriod; + + this->channelUtilization[utilPeriod] = 0; + } + + if (lastUtilPeriodTX != utilPeriodTX) { + lastUtilPeriodTX = utilPeriodTX; + + this->utilizationTX[utilPeriodTX] = 0; + } + } + return (1000 * 1); } diff --git a/src/airtime.h b/src/airtime.h index 3ed7b6d7c..48cc659c6 100644 --- a/src/airtime.h +++ b/src/airtime.h @@ -39,51 +39,50 @@ void logAirtime(reportTypes reportType, uint32_t airtime_ms); uint32_t *airtimeReport(reportTypes reportType); -class AirTime : private concurrency::OSThread -{ +class AirTime : private concurrency::OSThread { - public: - AirTime(); +public: + AirTime(); - void logAirtime(reportTypes reportType, uint32_t airtime_ms); - float channelUtilizationPercent(); - float utilizationTXPercent(); + void logAirtime(reportTypes reportType, uint32_t airtime_ms); + float channelUtilizationPercent(); + float utilizationTXPercent(); - float UtilizationPercentTX(); - uint32_t channelUtilization[CHANNEL_UTILIZATION_PERIODS] = {0}; - uint32_t utilizationTX[MINUTES_IN_HOUR] = {0}; + float UtilizationPercentTX(); + uint32_t channelUtilization[CHANNEL_UTILIZATION_PERIODS] = {0}; + uint32_t utilizationTX[MINUTES_IN_HOUR] = {0}; - void airtimeRotatePeriod(); - uint8_t getPeriodsToLog(); - uint32_t getSecondsPerPeriod(); - uint32_t getSecondsSinceBoot(); - uint32_t *airtimeReport(reportTypes reportType); - uint8_t getSilentMinutes(float txPercent, float dutyCycle); - bool isTxAllowedChannelUtil(bool polite = false); - bool isTxAllowedAirUtil(); + void airtimeRotatePeriod(); + uint8_t getPeriodsToLog(); + uint32_t getSecondsPerPeriod(); + uint32_t getSecondsSinceBoot(); + uint32_t *airtimeReport(reportTypes reportType); + uint8_t getSilentMinutes(float txPercent, float dutyCycle); + bool isTxAllowedChannelUtil(bool polite = false); + bool isTxAllowedAirUtil(); - private: - bool firstTime = true; - uint8_t lastUtilPeriod = 0; - uint8_t lastUtilPeriodTX = 0; - uint32_t secSinceBoot = 0; - uint8_t max_channel_util_percent = 40; - uint8_t polite_channel_util_percent = 25; - uint8_t polite_duty_cycle_percent = 50; // half of Duty Cycle allowance is ok for metadata +private: + bool firstTime = true; + uint8_t lastUtilPeriod = 0; + uint8_t lastUtilPeriodTX = 0; + uint32_t secSinceBoot = 0; + uint8_t max_channel_util_percent = 40; + uint8_t polite_channel_util_percent = 25; + uint8_t polite_duty_cycle_percent = 50; // half of Duty Cycle allowance is ok for metadata - struct airtimeStruct { - uint32_t periodTX[PERIODS_TO_LOG]; // AirTime transmitted - uint32_t periodRX[PERIODS_TO_LOG]; // AirTime received and repeated (Only valid mesh packets) - uint32_t periodRX_ALL[PERIODS_TO_LOG]; // AirTime received regardless of valid mesh packet. Could include noise. - uint8_t lastPeriodIndex; - } airtimes; + struct airtimeStruct { + uint32_t periodTX[PERIODS_TO_LOG]; // AirTime transmitted + uint32_t periodRX[PERIODS_TO_LOG]; // AirTime received and repeated (Only valid mesh packets) + uint32_t periodRX_ALL[PERIODS_TO_LOG]; // AirTime received regardless of valid mesh packet. Could include noise. + uint8_t lastPeriodIndex; + } airtimes; - uint8_t getPeriodUtilMinute(); - uint8_t getPeriodUtilHour(); - uint8_t currentPeriodIndex(); + uint8_t getPeriodUtilMinute(); + uint8_t getPeriodUtilHour(); + uint8_t currentPeriodIndex(); - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; }; extern AirTime *airTime; diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 0716dedd0..218a101b5 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -5,60 +5,58 @@ BuzzerFeedbackThread *buzzerFeedbackThread; -BuzzerFeedbackThread::BuzzerFeedbackThread() -{ - if (inputBroker) - inputObserver.observe(inputBroker); +BuzzerFeedbackThread::BuzzerFeedbackThread() { + if (inputBroker) + inputObserver.observe(inputBroker); } -int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) -{ - // Only provide feedback if buzzer is enabled for notifications - if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) { - return 0; // Let other handlers process the event +int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) { + // Only provide feedback if buzzer is enabled for notifications + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) { + return 0; // Let other handlers process the event + } + + // Handle different input events with appropriate buzzer feedback + switch (event->inputEvent) { + case INPUT_BROKER_USER_PRESS: + case INPUT_BROKER_ALT_PRESS: + playClick(); // Low delay feedback + break; + + case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: + playBeep(); // Confirmation feedback + break; + + case INPUT_BROKER_UP: + case INPUT_BROKER_UP_LONG: + case INPUT_BROKER_DOWN: + case INPUT_BROKER_DOWN_LONG: + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: + playChirp(); // Navigation feedback + break; + + case INPUT_BROKER_CANCEL: + case INPUT_BROKER_BACK: + playBoop(); // Cancel/back feedback + break; + + case INPUT_BROKER_SEND_PING: + playComboTune(); // Ping sent feedback + break; + + default: + // For other events, check if it's a printable character + if (event->kbchar >= 32 && event->kbchar <= 126) { + // Typing feedback - very short boop + // Removing this for now, too chatty + // playChirp(); } + break; + } - // Handle different input events with appropriate buzzer feedback - switch (event->inputEvent) { - case INPUT_BROKER_USER_PRESS: - case INPUT_BROKER_ALT_PRESS: - playClick(); // Low delay feedback - break; - - case INPUT_BROKER_SELECT: - case INPUT_BROKER_SELECT_LONG: - playBeep(); // Confirmation feedback - break; - - case INPUT_BROKER_UP: - case INPUT_BROKER_UP_LONG: - case INPUT_BROKER_DOWN: - case INPUT_BROKER_DOWN_LONG: - case INPUT_BROKER_LEFT: - case INPUT_BROKER_RIGHT: - playChirp(); // Navigation feedback - break; - - case INPUT_BROKER_CANCEL: - case INPUT_BROKER_BACK: - playBoop(); // Cancel/back feedback - break; - - case INPUT_BROKER_SEND_PING: - playComboTune(); // Ping sent feedback - break; - - default: - // For other events, check if it's a printable character - if (event->kbchar >= 32 && event->kbchar <= 126) { - // Typing feedback - very short boop - // Removing this for now, too chatty - // playChirp(); - } - break; - } - - return 0; // Allow other handlers to process the event + return 0; // Allow other handlers to process the event } diff --git a/src/buzz/BuzzerFeedbackThread.h b/src/buzz/BuzzerFeedbackThread.h index 7dc08ead5..df8243524 100644 --- a/src/buzz/BuzzerFeedbackThread.h +++ b/src/buzz/BuzzerFeedbackThread.h @@ -4,14 +4,13 @@ #include "concurrency/OSThread.h" #include "input/InputBroker.h" -class BuzzerFeedbackThread -{ - CallbackObserver inputObserver = - CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); +class BuzzerFeedbackThread { + CallbackObserver inputObserver = + CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); - public: - BuzzerFeedbackThread(); - int handleInputEvent(const InputEvent *event); +public: + BuzzerFeedbackThread(); + int handleInputEvent(const InputEvent *event); }; extern BuzzerFeedbackThread *buzzerFeedbackThread; diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 00ad71031..a5b91189e 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -11,8 +11,8 @@ extern "C" void delay(uint32_t dwMs); #endif struct ToneDuration { - int frequency_khz; - int duration_ms; + int frequency_khz; + int duration_ms; }; // Some common frequencies. @@ -42,105 +42,92 @@ const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note -void playTones(const ToneDuration *tone_durations, int size) -{ - if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { - // Buzzer is disabled or not set to system tones - return; - } +void playTones(const ToneDuration *tone_durations, int size) { + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + // Buzzer is disabled or not set to system tones + return; + } #ifdef PIN_BUZZER - if (!config.device.buzzer_gpio) - config.device.buzzer_gpio = PIN_BUZZER; + if (!config.device.buzzer_gpio) + config.device.buzzer_gpio = PIN_BUZZER; #endif - if (config.device.buzzer_gpio) { - for (int i = 0; i < size; i++) { - const auto &tone_duration = tone_durations[i]; - tone(config.device.buzzer_gpio, tone_duration.frequency_khz, tone_duration.duration_ms); - // to distinguish the notes, set a minimum time between them. - delay(1.3 * tone_duration.duration_ms); - } + if (config.device.buzzer_gpio) { + for (int i = 0; i < size; i++) { + const auto &tone_duration = tone_durations[i]; + tone(config.device.buzzer_gpio, tone_duration.frequency_khz, tone_duration.duration_ms); + // to distinguish the notes, set a minimum time between them. + delay(1.3 * tone_duration.duration_ms); } + } } -void playBeep() -{ - ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playBeep() { + ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playLongBeep() -{ - ToneDuration melody[] = {{NOTE_B3, DURATION_1_1}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playLongBeep() { + ToneDuration melody[] = {{NOTE_B3, DURATION_1_1}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playGPSEnableBeep() -{ +void playGPSEnableBeep() { #if defined(R1_NEO) || defined(MUZI_BASE) - ToneDuration melody[] = { - {NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}}; + ToneDuration melody[] = {{NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}}; #else - ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; + ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; #endif - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playGPSDisableBeep() -{ +void playGPSDisableBeep() { #if defined(R1_NEO) || defined(MUZI_BASE) - ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, - {NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, - {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}}; + ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, {NOTE_F3, DURATION_1_16}, + {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}}; #else - ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; + ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; #endif - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playStartMelody() -{ - ToneDuration melody[] = {{NOTE_FS3, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_CS4, DURATION_1_4}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playStartMelody() { + ToneDuration melody[] = {{NOTE_FS3, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_CS4, DURATION_1_4}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playShutdownMelody() -{ - ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playShutdownMelody() { + ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playChirp() -{ - // A short, friendly "chirp" sound for key presses - ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playChirp() { + // A short, friendly "chirp" sound for key presses + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playClick() -{ - // A very short "click" sound with minimum delay; ideal for rotary encoder events - ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playClick() { + // A very short "click" sound with minimum delay; ideal for rotary encoder events + ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playBoop() -{ - // A short, friendly "boop" sound for button presses - ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playBoop() { + // A short, friendly "boop" sound for button presses + ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playLongPressLeadUp() -{ - // An ascending lead-up sequence for long press - builds anticipation - ToneDuration melody[] = { - {NOTE_C3, 100}, // Start low - {NOTE_E3, 100}, // Step up - {NOTE_G3, 100}, // Keep climbing - {NOTE_B3, 150} // Peak with longer note for emphasis - }; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playLongPressLeadUp() { + // An ascending lead-up sequence for long press - builds anticipation + ToneDuration melody[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } // Static state for progressive lead-up notes @@ -153,39 +140,34 @@ static const ToneDuration leadUpNotes[] = { }; static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration); -bool playNextLeadUpNote() -{ - if (leadUpNoteIndex >= leadUpNotesCount) { - return false; // All notes have been played - } +bool playNextLeadUpNote() { + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // All notes have been played + } - // Use playTones to handle buzzer logic consistently - const auto ¬e = leadUpNotes[leadUpNoteIndex]; - playTones(¬e, 1); // Play single note using existing playTones function + // Use playTones to handle buzzer logic consistently + const auto ¬e = leadUpNotes[leadUpNoteIndex]; + playTones(¬e, 1); // Play single note using existing playTones function - leadUpNoteIndex++; + leadUpNoteIndex++; - if (leadUpNoteIndex >= leadUpNotesCount) { - return false; // this was the final note - } - return true; // Note was played (playTones handles buzzer availability internally) + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // this was the final note + } + return true; // Note was played (playTones handles buzzer availability internally) } -void resetLeadUpSequence() -{ - leadUpNoteIndex = 0; -} +void resetLeadUpSequence() { leadUpNoteIndex = 0; } -void playComboTune() -{ - // Quick high-pitched notes with trills - ToneDuration melody[] = { - {NOTE_G3, 80}, // Quick chirp - {NOTE_B3, 60}, // Higher chirp - {NOTE_CS4, 80}, // Even higher - {NOTE_G3, 60}, // Quick trill down - {NOTE_CS4, 60}, // Quick trill up - {NOTE_B3, 120} // Ending chirp - }; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playComboTune() { + // Quick high-pitched notes with trills + ToneDuration melody[] = { + {NOTE_G3, 80}, // Quick chirp + {NOTE_B3, 60}, // Higher chirp + {NOTE_CS4, 80}, // Even higher + {NOTE_G3, 60}, // Quick trill down + {NOTE_CS4, 60}, // Quick trill up + {NOTE_B3, 120} // Ending chirp + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } diff --git a/src/commands.h b/src/commands.h index 603003e5c..687e08637 100644 --- a/src/commands.h +++ b/src/commands.h @@ -4,15 +4,15 @@ */ enum class Cmd { - INVALID, - SET_ON, - SET_OFF, - ON_PRESS, - START_ALERT_FRAME, - STOP_ALERT_FRAME, - START_FIRMWARE_UPDATE_SCREEN, - STOP_BOOT_SCREEN, - SHOW_PREV_FRAME, - SHOW_NEXT_FRAME, - NOOP + INVALID, + SET_ON, + SET_OFF, + ON_PRESS, + START_ALERT_FRAME, + STOP_ALERT_FRAME, + START_FIRMWARE_UPDATE_SCREEN, + STOP_BOOT_SCREEN, + SHOW_PREV_FRAME, + SHOW_NEXT_FRAME, + NOOP }; \ No newline at end of file diff --git a/src/concurrency/BinarySemaphoreFreeRTOS.cpp b/src/concurrency/BinarySemaphoreFreeRTOS.cpp index 36e55eae7..411e284c2 100644 --- a/src/concurrency/BinarySemaphoreFreeRTOS.cpp +++ b/src/concurrency/BinarySemaphoreFreeRTOS.cpp @@ -4,35 +4,21 @@ #ifdef HAS_FREE_RTOS -namespace concurrency -{ +namespace concurrency { -BinarySemaphoreFreeRTOS::BinarySemaphoreFreeRTOS() : semaphore(xSemaphoreCreateBinary()) -{ - assert(semaphore); -} +BinarySemaphoreFreeRTOS::BinarySemaphoreFreeRTOS() : semaphore(xSemaphoreCreateBinary()) { assert(semaphore); } -BinarySemaphoreFreeRTOS::~BinarySemaphoreFreeRTOS() -{ - vSemaphoreDelete(semaphore); -} +BinarySemaphoreFreeRTOS::~BinarySemaphoreFreeRTOS() { vSemaphoreDelete(semaphore); } /** * Returns false if we were interrupted */ -bool BinarySemaphoreFreeRTOS::take(uint32_t msec) -{ - return xSemaphoreTake(semaphore, pdMS_TO_TICKS(msec)); -} +bool BinarySemaphoreFreeRTOS::take(uint32_t msec) { return xSemaphoreTake(semaphore, pdMS_TO_TICKS(msec)); } -void BinarySemaphoreFreeRTOS::give() -{ - xSemaphoreGive(semaphore); -} +void BinarySemaphoreFreeRTOS::give() { xSemaphoreGive(semaphore); } -IRAM_ATTR void BinarySemaphoreFreeRTOS::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) -{ - xSemaphoreGiveFromISR(semaphore, pxHigherPriorityTaskWoken); +IRAM_ATTR void BinarySemaphoreFreeRTOS::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) { + xSemaphoreGiveFromISR(semaphore, pxHigherPriorityTaskWoken); } } // namespace concurrency diff --git a/src/concurrency/BinarySemaphoreFreeRTOS.h b/src/concurrency/BinarySemaphoreFreeRTOS.h index 2883151d8..aee910480 100644 --- a/src/concurrency/BinarySemaphoreFreeRTOS.h +++ b/src/concurrency/BinarySemaphoreFreeRTOS.h @@ -2,27 +2,25 @@ #include "../freertosinc.h" -namespace concurrency -{ +namespace concurrency { #ifdef HAS_FREE_RTOS -class BinarySemaphoreFreeRTOS -{ - SemaphoreHandle_t semaphore; +class BinarySemaphoreFreeRTOS { + SemaphoreHandle_t semaphore; - public: - BinarySemaphoreFreeRTOS(); - ~BinarySemaphoreFreeRTOS(); +public: + BinarySemaphoreFreeRTOS(); + ~BinarySemaphoreFreeRTOS(); - /** - * Returns false if we timed out - */ - bool take(uint32_t msec); + /** + * Returns false if we timed out + */ + bool take(uint32_t msec); - void give(); + void give(); - void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); + void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); }; #endif diff --git a/src/concurrency/BinarySemaphorePosix.cpp b/src/concurrency/BinarySemaphorePosix.cpp index dc49a489b..1ae9b216f 100644 --- a/src/concurrency/BinarySemaphorePosix.cpp +++ b/src/concurrency/BinarySemaphorePosix.cpp @@ -3,8 +3,7 @@ #ifndef HAS_FREE_RTOS -namespace concurrency -{ +namespace concurrency { BinarySemaphorePosix::BinarySemaphorePosix() {} @@ -13,10 +12,9 @@ BinarySemaphorePosix::~BinarySemaphorePosix() {} /** * Returns false if we timed out */ -bool BinarySemaphorePosix::take(uint32_t msec) -{ - delay(msec); // FIXME - return false; +bool BinarySemaphorePosix::take(uint32_t msec) { + delay(msec); // FIXME + return false; } void BinarySemaphorePosix::give() {} diff --git a/src/concurrency/BinarySemaphorePosix.h b/src/concurrency/BinarySemaphorePosix.h index 475b29874..e5add3780 100644 --- a/src/concurrency/BinarySemaphorePosix.h +++ b/src/concurrency/BinarySemaphorePosix.h @@ -2,27 +2,25 @@ #include "../freertosinc.h" -namespace concurrency -{ +namespace concurrency { #ifndef HAS_FREE_RTOS -class BinarySemaphorePosix -{ - // SemaphoreHandle_t semaphore; +class BinarySemaphorePosix { + // SemaphoreHandle_t semaphore; - public: - BinarySemaphorePosix(); - ~BinarySemaphorePosix(); +public: + BinarySemaphorePosix(); + ~BinarySemaphorePosix(); - /** - * Returns false if we timed out - */ - bool take(uint32_t msec); + /** + * Returns false if we timed out + */ + bool take(uint32_t msec); - void give(); + void give(); - void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); + void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); }; #endif diff --git a/src/concurrency/InterruptableDelay.cpp b/src/concurrency/InterruptableDelay.cpp index 8bc85436b..bec4f2e83 100644 --- a/src/concurrency/InterruptableDelay.cpp +++ b/src/concurrency/InterruptableDelay.cpp @@ -1,8 +1,7 @@ #include "concurrency/InterruptableDelay.h" #include "configuration.h" -namespace concurrency -{ +namespace concurrency { InterruptableDelay::InterruptableDelay() {} @@ -11,25 +10,18 @@ InterruptableDelay::~InterruptableDelay() {} /** * Returns false if we were interrupted */ -bool InterruptableDelay::delay(uint32_t msec) -{ - // LOG_DEBUG("delay %u ", msec); +bool InterruptableDelay::delay(uint32_t msec) { + // LOG_DEBUG("delay %u ", msec); - // sem take will return false if we timed out (i.e. were not interrupted) - bool r = semaphore.take(msec); + // sem take will return false if we timed out (i.e. were not interrupted) + bool r = semaphore.take(msec); - // LOG_DEBUG("interrupt=%d", r); - return !r; + // LOG_DEBUG("interrupt=%d", r); + return !r; } -void InterruptableDelay::interrupt() -{ - semaphore.give(); -} +void InterruptableDelay::interrupt() { semaphore.give(); } -IRAM_ATTR void InterruptableDelay::interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken) -{ - semaphore.giveFromISR(pxHigherPriorityTaskWoken); -} +IRAM_ATTR void InterruptableDelay::interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken) { semaphore.giveFromISR(pxHigherPriorityTaskWoken); } } // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/InterruptableDelay.h b/src/concurrency/InterruptableDelay.h index 41bc40a21..50a4e74c7 100644 --- a/src/concurrency/InterruptableDelay.h +++ b/src/concurrency/InterruptableDelay.h @@ -10,32 +10,31 @@ #define BinarySemaphore BinarySemaphorePosix #endif -namespace concurrency -{ +namespace concurrency { /** * An object that provides delay(msec) like functionality, but can be interrupted by calling interrupt(). * - * Useful for they top level loop() delay call to keep the CPU powered down until our next scheduled event or some external event. + * Useful for they top level loop() delay call to keep the CPU powered down until our next scheduled event or some + * external event. * * This is implemented for FreeRTOS but should be easy to port to other operating systems. */ -class InterruptableDelay -{ - BinarySemaphore semaphore; +class InterruptableDelay { + BinarySemaphore semaphore; - public: - InterruptableDelay(); - ~InterruptableDelay(); +public: + InterruptableDelay(); + ~InterruptableDelay(); - /** - * Returns false if we were interrupted - */ - bool delay(uint32_t msec); + /** + * Returns false if we were interrupted + */ + bool delay(uint32_t msec); - void interrupt(); + void interrupt(); - void interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken); + void interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken); }; } // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/Lock.cpp b/src/concurrency/Lock.cpp index 0fe80e455..15c59d081 100644 --- a/src/concurrency/Lock.cpp +++ b/src/concurrency/Lock.cpp @@ -2,30 +2,26 @@ #include "configuration.h" #include -namespace concurrency -{ +namespace concurrency { #ifdef HAS_FREE_RTOS -Lock::Lock() : handle(xSemaphoreCreateBinary()) -{ - assert(handle); - if (xSemaphoreGive(handle) == false) { - abort(); - } +Lock::Lock() : handle(xSemaphoreCreateBinary()) { + assert(handle); + if (xSemaphoreGive(handle) == false) { + abort(); + } } -void Lock::lock() -{ - if (xSemaphoreTake(handle, portMAX_DELAY) == false) { - abort(); - } +void Lock::lock() { + if (xSemaphoreTake(handle, portMAX_DELAY) == false) { + abort(); + } } -void Lock::unlock() -{ - if (xSemaphoreGive(handle) == false) { - abort(); - } +void Lock::unlock() { + if (xSemaphoreGive(handle) == false) { + abort(); + } } #else Lock::Lock() {} diff --git a/src/concurrency/Lock.h b/src/concurrency/Lock.h index 1b9ea20d5..e4bcfe5f6 100644 --- a/src/concurrency/Lock.h +++ b/src/concurrency/Lock.h @@ -2,33 +2,31 @@ #include "../freertosinc.h" -namespace concurrency -{ +namespace concurrency { /** * @brief Simple wrapper around FreeRTOS API for implementing a mutex lock */ -class Lock -{ - public: - Lock(); +class Lock { +public: + Lock(); - Lock(const Lock &) = delete; - Lock &operator=(const Lock &) = delete; + Lock(const Lock &) = delete; + Lock &operator=(const Lock &) = delete; - /// Locks the lock. - // - // Must not be called from an ISR. - void lock(); + /// Locks the lock. + // + // Must not be called from an ISR. + void lock(); - // Unlocks the lock. - // - // Must not be called from an ISR. - void unlock(); + // Unlocks the lock. + // + // Must not be called from an ISR. + void unlock(); - private: +private: #ifdef HAS_FREE_RTOS - SemaphoreHandle_t handle; + SemaphoreHandle_t handle; #endif }; diff --git a/src/concurrency/LockGuard.cpp b/src/concurrency/LockGuard.cpp index d855266cb..fc0c3b81d 100644 --- a/src/concurrency/LockGuard.cpp +++ b/src/concurrency/LockGuard.cpp @@ -1,17 +1,10 @@ #include "LockGuard.h" #include "configuration.h" -namespace concurrency -{ +namespace concurrency { -LockGuard::LockGuard(Lock *lock) : lock(lock) -{ - lock->lock(); -} +LockGuard::LockGuard(Lock *lock) : lock(lock) { lock->lock(); } -LockGuard::~LockGuard() -{ - lock->unlock(); -} +LockGuard::~LockGuard() { lock->unlock(); } } // namespace concurrency diff --git a/src/concurrency/LockGuard.h b/src/concurrency/LockGuard.h index 647e7960d..a4428d4c8 100644 --- a/src/concurrency/LockGuard.h +++ b/src/concurrency/LockGuard.h @@ -2,23 +2,21 @@ #include "Lock.h" -namespace concurrency -{ +namespace concurrency { /** * @brief RAII lock guard */ -class LockGuard -{ - public: - explicit LockGuard(Lock *lock); - ~LockGuard(); +class LockGuard { +public: + explicit LockGuard(Lock *lock); + ~LockGuard(); - LockGuard(const LockGuard &) = delete; - LockGuard &operator=(const LockGuard &) = delete; + LockGuard(const LockGuard &) = delete; + LockGuard &operator=(const LockGuard &) = delete; - private: - Lock *lock; +private: + Lock *lock; }; } // namespace concurrency diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp index 0e4e31d9b..655c8d4f1 100644 --- a/src/concurrency/NotifiedWorkerThread.cpp +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -2,45 +2,42 @@ #include "configuration.h" #include "main.h" -namespace concurrency -{ +namespace concurrency { static bool debugNotification; /** * Notify this thread so it can run */ -bool NotifiedWorkerThread::notify(uint32_t v, bool overwrite) -{ - bool r = notifyCommon(v, overwrite); +bool NotifiedWorkerThread::notify(uint32_t v, bool overwrite) { + bool r = notifyCommon(v, overwrite); - if (r) - mainDelay.interrupt(); + if (r) + mainDelay.interrupt(); - return r; + return r; } /** * Notify this thread so it can run */ -IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) -{ - if (overwrite || notification == 0) { - enabled = true; - setInterval(0); // Run ASAP - runASAP = true; +IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) { + if (overwrite || notification == 0) { + enabled = true; + setInterval(0); // Run ASAP + runASAP = true; - notification = v; - if (debugNotification) { - LOG_DEBUG("Set notification %d", v); - } - return true; - } else { - if (debugNotification) { - LOG_DEBUG("Drop notification %d", v); - } - return false; + notification = v; + if (debugNotification) { + LOG_DEBUG("Set notification %d", v); } + return true; + } else { + if (debugNotification) { + LOG_DEBUG("Drop notification %d", v); + } + return false; + } } /** @@ -48,47 +45,43 @@ IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) * * This must be inline or IRAM_ATTR on ESP32 */ -IRAM_ATTR bool NotifiedWorkerThread::notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite) -{ - bool r = notifyCommon(v, overwrite); - if (r) - mainDelay.interruptFromISR(highPriWoken); +IRAM_ATTR bool NotifiedWorkerThread::notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite) { + bool r = notifyCommon(v, overwrite); + if (r) + mainDelay.interruptFromISR(highPriWoken); - return r; + return r; } /** * Schedule a notification to fire in delay msecs */ -bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrite) -{ - bool didIt = notify(v, overwrite); +bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrite) { + bool didIt = notify(v, overwrite); - if (didIt) { // If we didn't already have something queued, override the delay to be larger - setIntervalFromNow(delay); // a new version of setInterval relative to the current time - if (debugNotification) { - LOG_DEBUG("Delay notification %u", delay); - } + if (didIt) { // If we didn't already have something queued, override the delay to be larger + setIntervalFromNow(delay); // a new version of setInterval relative to the current time + if (debugNotification) { + LOG_DEBUG("Delay notification %u", delay); } + } - return didIt; + return didIt; } -void NotifiedWorkerThread::checkNotification() -{ - auto n = notification; - notification = 0; // clear notification - if (n) { - onNotify(n); - } +void NotifiedWorkerThread::checkNotification() { + auto n = notification; + notification = 0; // clear notification + if (n) { + onNotify(n); + } } -int32_t NotifiedWorkerThread::runOnce() -{ - enabled = false; // Only run once per notification - checkNotification(); +int32_t NotifiedWorkerThread::runOnce() { + enabled = false; // Only run once per notification + checkNotification(); - return RUN_SAME; + return RUN_SAME; } } // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h index 7a150b0b0..b914db8d0 100644 --- a/src/concurrency/NotifiedWorkerThread.h +++ b/src/concurrency/NotifiedWorkerThread.h @@ -2,55 +2,53 @@ #include "OSThread.h" -namespace concurrency -{ +namespace concurrency { /** * @brief A worker thread that waits on a freertos notification */ -class NotifiedWorkerThread : public OSThread -{ - /** - * The notification that was most recently used to wake the thread. Read from runOnce() - */ - uint32_t notification = 0; +class NotifiedWorkerThread : public OSThread { + /** + * The notification that was most recently used to wake the thread. Read from runOnce() + */ + uint32_t notification = 0; - public: - NotifiedWorkerThread(const char *name) : OSThread(name) {} +public: + NotifiedWorkerThread(const char *name) : OSThread(name) {} - /** - * Notify this thread so it can run - */ - bool notify(uint32_t v, bool overwrite); + /** + * Notify this thread so it can run + */ + bool notify(uint32_t v, bool overwrite); - /** - * Notify from an ISR - * - * This must be inline or IRAM_ATTR on ESP32 - */ - bool notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite); + /** + * Notify from an ISR + * + * This must be inline or IRAM_ATTR on ESP32 + */ + bool notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite); - /** - * Schedule a notification to fire in delay msecs - */ - bool notifyLater(uint32_t delay, uint32_t v, bool overwrite); + /** + * Schedule a notification to fire in delay msecs + */ + bool notifyLater(uint32_t delay, uint32_t v, bool overwrite); - protected: - virtual void onNotify(uint32_t notification) = 0; +protected: + virtual void onNotify(uint32_t notification) = 0; - /// just calls checkNotification() - virtual int32_t runOnce() override; + /// just calls checkNotification() + virtual int32_t runOnce() override; - /// Sometimes we might want to check notifications independently of when our thread was getting woken up (i.e. if we are about - /// to change radio transmit/receive modes we want to handle any pending interrupts first). You can call this method and if - /// any notifications are currently pending they will be handled immediately. - void checkNotification(); + /// Sometimes we might want to check notifications independently of when our thread was getting woken up (i.e. if we + /// are about to change radio transmit/receive modes we want to handle any pending interrupts first). You can call + /// this method and if any notifications are currently pending they will be handled immediately. + void checkNotification(); - private: - /** - * Notify this thread so it can run - */ - bool notifyCommon(uint32_t v, bool overwrite); +private: + /** + * Notify this thread so it can run + */ + bool notifyCommon(uint32_t v, bool overwrite); }; } // namespace concurrency diff --git a/src/concurrency/OSThread.cpp b/src/concurrency/OSThread.cpp index ce9a256b7..8e0f81995 100644 --- a/src/concurrency/OSThread.cpp +++ b/src/concurrency/OSThread.cpp @@ -3,8 +3,7 @@ #include "memGet.h" #include -namespace concurrency -{ +namespace concurrency { /// Show debugging info for disabled threads bool OSThread::showDisabled; @@ -20,93 +19,85 @@ const OSThread *OSThread::currentThread; ThreadController mainController, timerController; InterruptableDelay mainDelay; -void OSThread::setup() -{ - mainController.ThreadName = "mainController"; - timerController.ThreadName = "timerController"; +void OSThread::setup() { + mainController.ThreadName = "mainController"; + timerController.ThreadName = "timerController"; } -OSThread::OSThread(const char *_name, uint32_t period, ThreadController *_controller) - : Thread(NULL, period), controller(_controller) -{ - assertIsSetup(); +OSThread::OSThread(const char *_name, uint32_t period, ThreadController *_controller) : Thread(NULL, period), controller(_controller) { + assertIsSetup(); - ThreadName = _name; + ThreadName = _name; - if (controller) { - bool added = controller->add(this); - assert(added); - } + if (controller) { + bool added = controller->add(this); + assert(added); + } } -OSThread::~OSThread() -{ - if (controller) - controller->remove(this); +OSThread::~OSThread() { + if (controller) + controller->remove(this); } /** * Wait a specified number msecs starting from the current time (rather than the last time we were run) */ -void OSThread::setIntervalFromNow(unsigned long _interval) -{ - // Save interval - interval = _interval; +void OSThread::setIntervalFromNow(unsigned long _interval) { + // Save interval + interval = _interval; - // Cache the next run based on the last_run - _cached_next_run = millis() + interval; + // Cache the next run based on the last_run + _cached_next_run = millis() + interval; } -bool OSThread::shouldRun(unsigned long time) -{ - bool r = Thread::shouldRun(time); +bool OSThread::shouldRun(unsigned long time) { + bool r = Thread::shouldRun(time); - if (showRun && r) { - LOG_DEBUG("Thread %s: run", ThreadName.c_str()); - } + if (showRun && r) { + LOG_DEBUG("Thread %s: run", ThreadName.c_str()); + } - if (showWaiting && enabled && !r) { - LOG_DEBUG("Thread %s: wait %lu", ThreadName.c_str(), interval); - } + if (showWaiting && enabled && !r) { + LOG_DEBUG("Thread %s: wait %lu", ThreadName.c_str(), interval); + } - if (showDisabled && !enabled) { - LOG_DEBUG("Thread %s: disabled", ThreadName.c_str()); - } + if (showDisabled && !enabled) { + LOG_DEBUG("Thread %s: disabled", ThreadName.c_str()); + } - return r; + return r; } -void OSThread::run() -{ +void OSThread::run() { #ifdef DEBUG_HEAP - auto heap = memGet.getFreeHeap(); + auto heap = memGet.getFreeHeap(); #endif - currentThread = this; - auto newDelay = runOnce(); + currentThread = this; + auto newDelay = runOnce(); #ifdef DEBUG_HEAP - auto newHeap = memGet.getFreeHeap(); - if (newHeap < heap) - LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); - if (heap < newHeap) - LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); + auto newHeap = memGet.getFreeHeap(); + if (newHeap < heap) + LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); + if (heap < newHeap) + LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); #endif #ifdef DEBUG_LOOP_TIMING - LOG_DEBUG("====== Thread next run in: %d", newDelay); + LOG_DEBUG("====== Thread next run in: %d", newDelay); #endif - runned(); + runned(); - if (newDelay >= 0) - setInterval(newDelay); + if (newDelay >= 0) + setInterval(newDelay); - currentThread = NULL; + currentThread = NULL; } -int32_t OSThread::disable() -{ - enabled = false; - setInterval(INT32_MAX); +int32_t OSThread::disable() { + enabled = false; + setInterval(INT32_MAX); - return INT32_MAX; + return INT32_MAX; } /** @@ -122,23 +113,22 @@ int32_t OSThread::disable() */ bool hasBeenSetup; -void assertIsSetup() -{ +void assertIsSetup() { - /** - * Dear developer comrade - If this assert fails() that means you need to fix the following: - * - * This flag is set **only** when setup() starts, to provide a way for us to check for sloppy static constructor calls. - * Call assertIsSetup() to force a crash if someone tries to create an instance too early. - * - * it is super important to never allocate those object statically. instead, you should explicitly - * new them at a point where you are guaranteed that other objects that this instance - * depends on have already been created. - * - * in particular, for OSThread that means "all instances must be declared via new() in setup() or later" - - * this makes it guaranteed that the global mainController is fully constructed first. - */ - assert(hasBeenSetup); + /** + * Dear developer comrade - If this assert fails() that means you need to fix the following: + * + * This flag is set **only** when setup() starts, to provide a way for us to check for sloppy static constructor + * calls. Call assertIsSetup() to force a crash if someone tries to create an instance too early. + * + * it is super important to never allocate those object statically. instead, you should explicitly + * new them at a point where you are guaranteed that other objects that this instance + * depends on have already been created. + * + * in particular, for OSThread that means "all instances must be declared via new() in setup() or later" - + * this makes it guaranteed that the global mainController is fully constructed first. + */ + assert(hasBeenSetup); } } // namespace concurrency diff --git a/src/concurrency/OSThread.h b/src/concurrency/OSThread.h index 0fbfe2e17..ba94043d9 100644 --- a/src/concurrency/OSThread.h +++ b/src/concurrency/OSThread.h @@ -7,8 +7,7 @@ #include "ThreadController.h" #include "concurrency/InterruptableDelay.h" -namespace concurrency -{ +namespace concurrency { extern ThreadController mainController, timerController; extern InterruptableDelay mainDelay; @@ -18,7 +17,8 @@ extern InterruptableDelay mainDelay; /** * @brief Base threading * - * This is a pseudo threading layer that is super easy to port, well suited to our slow network and very ram & power efficient. + * This is a pseudo threading layer that is super easy to port, well suited to our slow network and very ram & power + * efficient. * * TODO FIXME @geeksville * @@ -28,49 +28,48 @@ extern InterruptableDelay mainDelay; * move typedQueue into concurrency * remove freertos from typedqueue */ -class OSThread : public Thread -{ - ThreadController *controller; +class OSThread : public Thread { + ThreadController *controller; - /// Show debugging info for disabled threads - static bool showDisabled; + /// Show debugging info for disabled threads + static bool showDisabled; - /// Show debugging info for threads when we run them - static bool showRun; + /// Show debugging info for threads when we run them + static bool showRun; - /// Show debugging info for threads we decide not to run; - static bool showWaiting; + /// Show debugging info for threads we decide not to run; + static bool showWaiting; - public: - /// For debug printing only (might be null) - static const OSThread *currentThread; +public: + /// For debug printing only (might be null) + static const OSThread *currentThread; - OSThread(const char *name, uint32_t period = 0, ThreadController *controller = &mainController); + OSThread(const char *name, uint32_t period = 0, ThreadController *controller = &mainController); - virtual ~OSThread(); + virtual ~OSThread(); - virtual bool shouldRun(unsigned long time); + virtual bool shouldRun(unsigned long time); - static void setup(); + static void setup(); - virtual int32_t disable(); + virtual int32_t disable(); - /** - * Wait a specified number msecs starting from the current time (rather than the last time we were run) - */ - void setIntervalFromNow(unsigned long _interval); + /** + * Wait a specified number msecs starting from the current time (rather than the last time we were run) + */ + void setIntervalFromNow(unsigned long _interval); - protected: - /** - * The method that will be called each time our thread gets a chance to run - * - * Returns desired period for next invocation (or RUN_SAME for no change) - */ - virtual int32_t runOnce() = 0; - bool sleepOnNextExecution = false; +protected: + /** + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce() = 0; + bool sleepOnNextExecution = false; - // Do not override this - virtual void run(); + // Do not override this + virtual void run(); }; /** diff --git a/src/concurrency/Periodic.h b/src/concurrency/Periodic.h index db07145a6..9364d94c0 100644 --- a/src/concurrency/Periodic.h +++ b/src/concurrency/Periodic.h @@ -2,23 +2,21 @@ #include "concurrency/OSThread.h" -namespace concurrency -{ +namespace concurrency { /** * @brief Periodically invoke a callback. This just provides C-style callback conventions * rather than a virtual function - FIXME, remove? */ -class Periodic : public OSThread -{ - int32_t (*callback)(); +class Periodic : public OSThread { + int32_t (*callback)(); - public: - // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {} +public: + // callback returns the period for the next callback invocation (or 0 if we should no longer be called) + Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {} - protected: - int32_t runOnce() override { return callback(); } +protected: + int32_t runOnce() override { return callback(); } }; } // namespace concurrency diff --git a/src/configuration.h b/src/configuration.h index 650e1cc71..643bea072 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -68,8 +68,8 @@ along with this program. If not, see . #error APP_VERSION must be set by the build environment #endif -// FIXME: This is still needed by the Bluetooth Stack and needs to be replaced by something better. Remnant of the old versioning -// system. +// FIXME: This is still needed by the Bluetooth Stack and needs to be replaced by something better. Remnant of the old +// versioning system. #ifndef HW_VERSION #define HW_VERSION "1.0" #endif diff --git a/src/detect/LoRaRadioType.h b/src/detect/LoRaRadioType.h index a059a3668..fd3d9b852 100644 --- a/src/detect/LoRaRadioType.h +++ b/src/detect/LoRaRadioType.h @@ -1,17 +1,17 @@ #pragma once enum LoRaRadioType { - NO_RADIO, - STM32WLx_RADIO, - SIM_RADIO, - RF95_RADIO, - SX1262_RADIO, - SX1268_RADIO, - LLCC68_RADIO, - SX1280_RADIO, - LR1110_RADIO, - LR1120_RADIO, - LR1121_RADIO + NO_RADIO, + STM32WLx_RADIO, + SIM_RADIO, + RF95_RADIO, + SX1262_RADIO, + SX1268_RADIO, + LLCC68_RADIO, + SX1280_RADIO, + LR1110_RADIO, + LR1120_RADIO, + LR1121_RADIO }; extern LoRaRadioType radioType; \ No newline at end of file diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 83a455de7..c1d7113a6 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -8,82 +8,60 @@ ScanI2C::ScanI2C() = default; void ScanI2C::scanPort(ScanI2C::I2CPort port) {} void ScanI2C::scanPort(ScanI2C::I2CPort port, uint8_t *address, uint8_t asize) {} -void ScanI2C::setSuppressScreen() -{ - shouldSuppressScreen = true; -} +void ScanI2C::setSuppressScreen() { shouldSuppressScreen = true; } -ScanI2C::FoundDevice ScanI2C::firstScreen() const -{ - // Allow to override the scanner results for screen - if (shouldSuppressScreen) - return DEVICE_NONE; - - ScanI2C::DeviceType types[] = {SCREEN_SSD1306, SCREEN_SH1106, SCREEN_ST7567, SCREEN_UNKNOWN}; - return firstOfOrNONE(4, types); -} - -ScanI2C::FoundDevice ScanI2C::firstRTC() const -{ - ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE}; - return firstOfOrNONE(4, types); -} - -ScanI2C::FoundDevice ScanI2C::firstKeyboard() const -{ - ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; - return firstOfOrNONE(6, types); -} - -ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const -{ - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; - return firstOfOrNONE(9, types); -} - -ScanI2C::FoundDevice ScanI2C::firstAQI() const -{ - ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; - return firstOfOrNONE(2, types); -} - -ScanI2C::FoundDevice ScanI2C::firstRGBLED() const -{ - ScanI2C::DeviceType types[] = {NCP5623, LP5562}; - return firstOfOrNONE(2, types); -} - -ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const -{ +ScanI2C::FoundDevice ScanI2C::firstScreen() const { + // Allow to override the scanner results for screen + if (shouldSuppressScreen) return DEVICE_NONE; + + ScanI2C::DeviceType types[] = {SCREEN_SSD1306, SCREEN_SH1106, SCREEN_ST7567, SCREEN_UNKNOWN}; + return firstOfOrNONE(4, types); } -bool ScanI2C::exists(ScanI2C::DeviceType) const -{ - return false; +ScanI2C::FoundDevice ScanI2C::firstRTC() const { + ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE}; + return firstOfOrNONE(4, types); } -ScanI2C::FoundDevice ScanI2C::firstOfOrNONE(size_t count, ScanI2C::DeviceType *types) const -{ - return DEVICE_NONE; +ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; + return firstOfOrNONE(6, types); } -size_t ScanI2C::countDevices() const -{ - return 0; +ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; + return firstOfOrNONE(9, types); } +ScanI2C::FoundDevice ScanI2C::firstAQI() const { + ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + return firstOfOrNONE(2, types); +} + +ScanI2C::FoundDevice ScanI2C::firstRGBLED() const { + ScanI2C::DeviceType types[] = {NCP5623, LP5562}; + return firstOfOrNONE(2, types); +} + +ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const { return DEVICE_NONE; } + +bool ScanI2C::exists(ScanI2C::DeviceType) const { return false; } + +ScanI2C::FoundDevice ScanI2C::firstOfOrNONE(size_t count, ScanI2C::DeviceType *types) const { return DEVICE_NONE; } + +size_t ScanI2C::countDevices() const { return 0; } + ScanI2C::DeviceAddress::DeviceAddress(ScanI2C::I2CPort port, uint8_t address) : port(port), address(address) {} ScanI2C::DeviceAddress::DeviceAddress() : DeviceAddress(I2CPort::NO_I2C, 0) {} -bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) const -{ - return - // If this one has no port and other has a port - (port == NO_I2C && other.port != NO_I2C) - // if both have a port and this one's address is lower - || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); +bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) const { + return + // If this one has no port and other has a port + (port == NO_I2C && other.port != NO_I2C) + // if both have a port and this one's address is lower + || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); } ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 3a79d97c5..48c5b807b 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -3,156 +3,155 @@ #include #include -class ScanI2C -{ - public: - typedef enum DeviceType { - NONE, - SCREEN_SSD1306, - SCREEN_SH1106, - SCREEN_UNKNOWN, // has the same address as the two above but does not respond to the same commands - SCREEN_ST7567, - RTC_RV3028, - RTC_PCF8563, - RTC_PCF85063, - RTC_RX8130CE, - CARDKB, - TDECKKB, - BBQ10KB, - RAK14004, - PMU_AXP192_AXP2101, // has the same adress as the TCA8418KB - BME_680, - BME_280, - BMP_280, - BMP_085, - BMP_3XX, - INA260, - INA219, - INA3221, - MAX17048, - MCP9808, - SHT31, - SHT4X, - SHTC3, - LPS22HB, - QMC6310, - QMI8658, - QMC5883L, - HMC5883L, - PMSA0031, - QMA6100P, - MPU6050, - LIS3DH, - BMA423, - BQ24295, - LSM6DS3, - TCA9535, - TCA9555, - VEML7700, - RCWL9620, - NCP5623, - LP5562, - TSL2591, - OPT3001, - MLX90632, - MLX90614, - AHT10, - BMX160, - DFROBOT_LARK, - NAU7802, - FT6336U, - STK8BAXX, - ICM20948, - SCD4X, - MAX30102, - TPS65233, - MPR121KB, - CGRADSENS, - INA226, - NXP_SE050, - DFROBOT_RAIN, - DPS310, - LTR390UV, - RAK12035, - TCA8418KB, - PCT2075, - CST328, - BQ25896, - BQ27220, - LTR553ALS, - BHI260AP, - BMM150, - TSL2561, - DRV2605, - BH1750, - DA217, - CHSC6X, - CST226SE - } DeviceType; +class ScanI2C { +public: + typedef enum DeviceType { + NONE, + SCREEN_SSD1306, + SCREEN_SH1106, + SCREEN_UNKNOWN, // has the same address as the two above but does not respond to the same commands + SCREEN_ST7567, + RTC_RV3028, + RTC_PCF8563, + RTC_PCF85063, + RTC_RX8130CE, + CARDKB, + TDECKKB, + BBQ10KB, + RAK14004, + PMU_AXP192_AXP2101, // has the same adress as the TCA8418KB + BME_680, + BME_280, + BMP_280, + BMP_085, + BMP_3XX, + INA260, + INA219, + INA3221, + MAX17048, + MCP9808, + SHT31, + SHT4X, + SHTC3, + LPS22HB, + QMC6310, + QMI8658, + QMC5883L, + HMC5883L, + PMSA0031, + QMA6100P, + MPU6050, + LIS3DH, + BMA423, + BQ24295, + LSM6DS3, + TCA9535, + TCA9555, + VEML7700, + RCWL9620, + NCP5623, + LP5562, + TSL2591, + OPT3001, + MLX90632, + MLX90614, + AHT10, + BMX160, + DFROBOT_LARK, + NAU7802, + FT6336U, + STK8BAXX, + ICM20948, + SCD4X, + MAX30102, + TPS65233, + MPR121KB, + CGRADSENS, + INA226, + NXP_SE050, + DFROBOT_RAIN, + DPS310, + LTR390UV, + RAK12035, + TCA8418KB, + PCT2075, + CST328, + BQ25896, + BQ27220, + LTR553ALS, + BHI260AP, + BMM150, + TSL2561, + DRV2605, + BH1750, + DA217, + CHSC6X, + CST226SE + } DeviceType; - // typedef uint8_t DeviceAddress; - typedef enum I2CPort { - NO_I2C, - WIRE, - WIRE1, - } I2CPort; + // typedef uint8_t DeviceAddress; + typedef enum I2CPort { + NO_I2C, + WIRE, + WIRE1, + } I2CPort; - typedef struct DeviceAddress { - // set default values for ADDRESS_NONE - I2CPort port = I2CPort::NO_I2C; - uint8_t address = 0; + typedef struct DeviceAddress { + // set default values for ADDRESS_NONE + I2CPort port = I2CPort::NO_I2C; + uint8_t address = 0; - explicit DeviceAddress(I2CPort port, uint8_t address); - DeviceAddress(); + explicit DeviceAddress(I2CPort port, uint8_t address); + DeviceAddress(); - bool operator<(const DeviceAddress &other) const; - } DeviceAddress; + bool operator<(const DeviceAddress &other) const; + } DeviceAddress; - static const DeviceAddress ADDRESS_NONE; + static const DeviceAddress ADDRESS_NONE; - typedef uint8_t RegisterAddress; + typedef uint8_t RegisterAddress; - typedef struct FoundDevice { - DeviceType type; - DeviceAddress address; + typedef struct FoundDevice { + DeviceType type; + DeviceAddress address; - explicit FoundDevice(DeviceType = DeviceType::NONE, DeviceAddress = ADDRESS_NONE); - } FoundDevice; + explicit FoundDevice(DeviceType = DeviceType::NONE, DeviceAddress = ADDRESS_NONE); + } FoundDevice; - static const FoundDevice DEVICE_NONE; + static const FoundDevice DEVICE_NONE; - public: - ScanI2C(); +public: + ScanI2C(); - virtual void scanPort(ScanI2C::I2CPort); - virtual void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t); + virtual void scanPort(ScanI2C::I2CPort); + virtual void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t); - /* - * A bit of a hack, this tells the scanner not to tell later systems there is a screen to avoid enabling it. - */ - void setSuppressScreen(); + /* + * A bit of a hack, this tells the scanner not to tell later systems there is a screen to avoid enabling it. + */ + void setSuppressScreen(); - FoundDevice firstScreen() const; + FoundDevice firstScreen() const; - FoundDevice firstRTC() const; + FoundDevice firstRTC() const; - FoundDevice firstKeyboard() const; + FoundDevice firstKeyboard() const; - FoundDevice firstAccelerometer() const; + FoundDevice firstAccelerometer() const; - FoundDevice firstAQI() const; + FoundDevice firstAQI() const; - FoundDevice firstRGBLED() const; + FoundDevice firstRGBLED() const; - virtual FoundDevice find(DeviceType) const; + virtual FoundDevice find(DeviceType) const; - virtual bool exists(DeviceType) const; + virtual bool exists(DeviceType) const; - virtual size_t countDevices() const; + virtual size_t countDevices() const; - protected: - virtual FoundDevice firstOfOrNONE(size_t, DeviceType[]) const; +protected: + virtual FoundDevice firstOfOrNONE(size_t, DeviceType[]) const; - private: - bool shouldSuppressScreen = false; +private: + bool shouldSuppressScreen = false; }; diff --git a/src/detect/ScanI2CConsumer.cpp b/src/detect/ScanI2CConsumer.cpp index a70fa5398..b8ba10d39 100644 --- a/src/detect/ScanI2CConsumer.cpp +++ b/src/detect/ScanI2CConsumer.cpp @@ -3,14 +3,10 @@ static std::forward_list ScanI2CConsumers; -ScanI2CConsumer::ScanI2CConsumer() -{ - ScanI2CConsumers.push_front(this); -} +ScanI2CConsumer::ScanI2CConsumer() { ScanI2CConsumers.push_front(this); } -void ScanI2CCompleted(ScanI2C *i2cScanner) -{ - for (ScanI2CConsumer *consumer : ScanI2CConsumers) { - consumer->i2cScanFinished(i2cScanner); - } +void ScanI2CCompleted(ScanI2C *i2cScanner) { + for (ScanI2CConsumer *consumer : ScanI2CConsumers) { + consumer->i2cScanFinished(i2cScanner); + } } \ No newline at end of file diff --git a/src/detect/ScanI2CConsumer.h b/src/detect/ScanI2CConsumer.h index fd97f7edc..01b0cc1fb 100644 --- a/src/detect/ScanI2CConsumer.h +++ b/src/detect/ScanI2CConsumer.h @@ -3,11 +3,10 @@ #include "ScanI2C.h" #include -class ScanI2CConsumer -{ - public: - ScanI2CConsumer(); - virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0; +class ScanI2CConsumer { +public: + ScanI2CConsumer(); + virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0; }; void ScanI2CCompleted(ScanI2C *i2cScanner); \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 8e91d1787..3f0031320 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -10,655 +10,636 @@ #include "meshUtils.h" // vformat #endif -bool in_array(uint8_t *array, int size, uint8_t lookfor) -{ - int i; - for (i = 0; i < size; i++) - if (lookfor == array[i]) - return true; - return false; +bool in_array(uint8_t *array, int size, uint8_t lookfor) { + int i; + for (i = 0; i < size; i++) + if (lookfor == array[i]) + return true; + return false; } -ScanI2C::FoundDevice ScanI2CTwoWire::find(ScanI2C::DeviceType type) const -{ - concurrency::LockGuard guard((concurrency::Lock *)&lock); +ScanI2C::FoundDevice ScanI2CTwoWire::find(ScanI2C::DeviceType type) const { + concurrency::LockGuard guard((concurrency::Lock *)&lock); - return exists(type) ? ScanI2C::FoundDevice(type, deviceAddresses.at(type)) : DEVICE_NONE; + return exists(type) ? ScanI2C::FoundDevice(type, deviceAddresses.at(type)) : DEVICE_NONE; } -bool ScanI2CTwoWire::exists(ScanI2C::DeviceType type) const -{ - return deviceAddresses.find(type) != deviceAddresses.end(); -} +bool ScanI2CTwoWire::exists(ScanI2C::DeviceType type) const { return deviceAddresses.find(type) != deviceAddresses.end(); } -ScanI2C::FoundDevice ScanI2CTwoWire::firstOfOrNONE(size_t count, DeviceType types[]) const -{ - concurrency::LockGuard guard((concurrency::Lock *)&lock); +ScanI2C::FoundDevice ScanI2CTwoWire::firstOfOrNONE(size_t count, DeviceType types[]) const { + concurrency::LockGuard guard((concurrency::Lock *)&lock); - for (size_t k = 0; k < count; k++) { - ScanI2C::DeviceType current = types[k]; + for (size_t k = 0; k < count; k++) { + ScanI2C::DeviceType current = types[k]; - if (exists(current)) { - return ScanI2C::FoundDevice(current, deviceAddresses.at(current)); - } + if (exists(current)) { + return ScanI2C::FoundDevice(current, deviceAddresses.at(current)); } + } - return DEVICE_NONE; + return DEVICE_NONE; } -ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const -{ - TwoWire *i2cBus = fetchI2CBus(addr); +ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const { + TwoWire *i2cBus = fetchI2CBus(addr); - uint8_t r = 0; - uint8_t r_prev = 0; - uint8_t c = 0; - ScanI2C::DeviceType o_probe = ScanI2C::DeviceType::SCREEN_UNKNOWN; - do { - r_prev = r; - i2cBus->beginTransmission(addr.address); - i2cBus->write((uint8_t)0x00); - i2cBus->endTransmission(); - i2cBus->requestFrom((int)addr.address, 1); - if (i2cBus->available()) { - r = i2cBus->read(); - } - r &= 0x0f; - - if (r == 0x08 || r == 0x00) { - logFoundDevice("SH1106", (uint8_t)addr.address); - o_probe = SCREEN_SH1106; // SH1106 - } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { - logFoundDevice("SSD1306", (uint8_t)addr.address); - o_probe = SCREEN_SSD1306; // SSD1306 - } - c++; - } while ((r != r_prev) && (c < 4)); - LOG_DEBUG("0x%x subtype probed in %i tries ", r, c); - - return o_probe; -} -uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, - ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const -{ - uint16_t value = 0x00; - TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); - - i2cBus->beginTransmission(registerLocation.i2cAddress.address); - i2cBus->write(registerLocation.registerAddress); - if (zeropad) { - // Lark Commands need the argument list length in 2 bytes. - i2cBus->write((int)0); - i2cBus->write((int)0); - } + uint8_t r = 0; + uint8_t r_prev = 0; + uint8_t c = 0; + ScanI2C::DeviceType o_probe = ScanI2C::DeviceType::SCREEN_UNKNOWN; + do { + r_prev = r; + i2cBus->beginTransmission(addr.address); + i2cBus->write((uint8_t)0x00); i2cBus->endTransmission(); - delay(20); - i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth); - if (i2cBus->available() > 1) { - // Read MSB, then LSB - value = (uint16_t)i2cBus->read() << 8; - value |= i2cBus->read(); - } else if (i2cBus->available()) { - value = i2cBus->read(); + i2cBus->requestFrom((int)addr.address, 1); + if (i2cBus->available()) { + r = i2cBus->read(); } - // Drain excess bytes - for (uint8_t i = 0; i < responseWidth - 1; i++) { - if (i2cBus->available()) - i2cBus->read(); + r &= 0x0f; + + if (r == 0x08 || r == 0x00) { + logFoundDevice("SH1106", (uint8_t)addr.address); + o_probe = SCREEN_SH1106; // SH1106 + } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { + logFoundDevice("SSD1306", (uint8_t)addr.address); + o_probe = SCREEN_SSD1306; // SSD1306 } - LOG_DEBUG("Register value: 0x%x", value); - return value; + c++; + } while ((r != r_prev) && (c < 4)); + LOG_DEBUG("0x%x subtype probed in %i tries ", r, c); + + return o_probe; +} +uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, ScanI2CTwoWire::ResponseWidth responseWidth, + bool zeropad = false) const { + uint16_t value = 0x00; + TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); + + i2cBus->beginTransmission(registerLocation.i2cAddress.address); + i2cBus->write(registerLocation.registerAddress); + if (zeropad) { + // Lark Commands need the argument list length in 2 bytes. + i2cBus->write((int)0); + i2cBus->write((int)0); + } + i2cBus->endTransmission(); + delay(20); + i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth); + if (i2cBus->available() > 1) { + // Read MSB, then LSB + value = (uint16_t)i2cBus->read() << 8; + value |= i2cBus->read(); + } else if (i2cBus->available()) { + value = i2cBus->read(); + } + // Drain excess bytes + for (uint8_t i = 0; i < responseWidth - 1; i++) { + if (i2cBus->available()) + i2cBus->read(); + } + LOG_DEBUG("Register value: 0x%x", value); + return value; } -#define SCAN_SIMPLE_CASE(ADDR, T, ...) \ - case ADDR: \ - logFoundDevice(__VA_ARGS__); \ - type = T; \ +#define SCAN_SIMPLE_CASE(ADDR, T, ...) \ + case ADDR: \ + logFoundDevice(__VA_ARGS__); \ + type = T; \ + break; + +void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) { + concurrency::LockGuard guard((concurrency::Lock *)&lock); + + LOG_DEBUG("Scan for I2C devices on port %d", port); + + uint8_t err; + + DeviceAddress addr(port, 0x00); + + uint16_t registerValue = 0x00; + ScanI2C::DeviceType type; + TwoWire *i2cBus; +#ifdef RV3028_RTC + Melopero_RV3028 rtc; +#endif + +#if WIRE_INTERFACES_COUNT == 2 + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; + } else { +#endif + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + // We only need to scan 112 addresses, the rest is reserved for special purposes + // 0x00 General Call + // 0x01 CBUS addresses + // 0x02 Reserved for different bus formats + // 0x03 Reserved for future purposes + // 0x04-0x07 High Speed Master Code + // 0x78-0x7B 10-bit slave addressing + // 0x7C-0x7F Reserved for future purposes + + for (addr.address = 8; addr.address < 120; addr.address++) { + if (asize != 0) { + if (!in_array(address, asize, (uint8_t)addr.address)) + continue; + LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address); + } + i2cBus->beginTransmission(addr.address); +#ifdef ARCH_PORTDUINO + err = 2; + if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) { + if (i2cBus->read() != -1) + err = 0; + } else { + err = i2cBus->writeQuick((uint8_t)0); + } + if (err != 0) + err = 2; +#else + err = i2cBus->endTransmission(); +#endif + type = NONE; + if (err == 0) { + switch (addr.address) { + case SSD1306_ADDRESS: + type = probeOLED(addr); break; -void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) -{ - concurrency::LockGuard guard((concurrency::Lock *)&lock); - - LOG_DEBUG("Scan for I2C devices on port %d", port); - - uint8_t err; - - DeviceAddress addr(port, 0x00); - - uint16_t registerValue = 0x00; - ScanI2C::DeviceType type; - TwoWire *i2cBus; #ifdef RV3028_RTC - Melopero_RV3028 rtc; -#endif - -#if WIRE_INTERFACES_COUNT == 2 - if (port == I2CPort::WIRE1) { - i2cBus = &Wire1; - } else { -#endif - i2cBus = &Wire; -#if WIRE_INTERFACES_COUNT == 2 - } -#endif - - // We only need to scan 112 addresses, the rest is reserved for special purposes - // 0x00 General Call - // 0x01 CBUS addresses - // 0x02 Reserved for different bus formats - // 0x03 Reserved for future purposes - // 0x04-0x07 High Speed Master Code - // 0x78-0x7B 10-bit slave addressing - // 0x7C-0x7F Reserved for future purposes - - for (addr.address = 8; addr.address < 120; addr.address++) { - if (asize != 0) { - if (!in_array(address, asize, (uint8_t)addr.address)) - continue; - LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address); + case RV3028_RTC: + // foundDevices[addr] = RTC_RV3028; + type = RTC_RV3028; + logFoundDevice("RV3028", (uint8_t)addr.address); + rtc.initI2C(*i2cBus); + // Update RTC EEPROM settings, if necessary + if (rtc.readEEPROMRegister(0x35) != 0x07) { + rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout } - i2cBus->beginTransmission(addr.address); -#ifdef ARCH_PORTDUINO - err = 2; - if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) { - if (i2cBus->read() != -1) - err = 0; - } else { - err = i2cBus->writeQuick((uint8_t)0); + if (rtc.readEEPROMRegister(0x37) != 0xB4) { + rtc.writeEEPROMRegister(0x37, 0xB4); } - if (err != 0) - err = 2; -#else - err = i2cBus->endTransmission(); -#endif - type = NONE; - if (err == 0) { - switch (addr.address) { - case SSD1306_ADDRESS: - type = probeOLED(addr); - break; - -#ifdef RV3028_RTC - case RV3028_RTC: - // foundDevices[addr] = RTC_RV3028; - type = RTC_RV3028; - logFoundDevice("RV3028", (uint8_t)addr.address); - rtc.initI2C(*i2cBus); - // Update RTC EEPROM settings, if necessary - if (rtc.readEEPROMRegister(0x35) != 0x07) { - rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout - } - if (rtc.readEEPROMRegister(0x37) != 0xB4) { - rtc.writeEEPROMRegister(0x37, 0xB4); - } - break; + break; #endif #ifdef PCF8563_RTC - SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address) #endif #ifdef RX8130CE_RTC - SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) #endif #ifdef PCF85063_RTC - SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) #endif - case CARDKB_ADDR: - // Do we have the RAK14006 instead? - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); - if (registerValue == 0x02) { - // KEYPAD_VERSION - logFoundDevice("RAK14004", (uint8_t)addr.address); - type = RAK14004; - } else { - logFoundDevice("M5 cardKB", (uint8_t)addr.address); - type = CARDKB; - } - break; + case CARDKB_ADDR: + // Do we have the RAK14006 instead? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); + if (registerValue == 0x02) { + // KEYPAD_VERSION + logFoundDevice("RAK14004", (uint8_t)addr.address); + type = RAK14004; + } else { + logFoundDevice("M5 cardKB", (uint8_t)addr.address); + type = CARDKB; + } + break; - case TDECK_KB_ADDR: - // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor? - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); - if (registerValue != 0) { - logFoundDevice("BQ27220", (uint8_t)addr.address); - type = BQ27220; - } else { - logFoundDevice("TDECKKB", (uint8_t)addr.address); - type = TDECKKB; - } - break; - SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address); + case TDECK_KB_ADDR: + // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); + if (registerValue != 0) { + logFoundDevice("BQ27220", (uint8_t)addr.address); + type = BQ27220; + } else { + logFoundDevice("TDECKKB", (uint8_t)addr.address); + type = TDECKKB; + } + break; + SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); #ifdef HAS_NCP5623 - SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); #endif #ifdef HAS_LP5562 - SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); #endif - case XPOWERS_AXP192_AXP2101_ADDRESS: - // Do we have the axp2101/192 or the TCA8418 - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1); - if (registerValue == 0x0) { - logFoundDevice("TCA8418", (uint8_t)addr.address); - type = TCA8418KB; - } else { - logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address); - type = PMU_AXP192_AXP2101; - } - break; - case BME_ADDR: - case BME_ADDR_ALTERNATE: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID - switch (registerValue) { - case 0x61: - logFoundDevice("BME680", (uint8_t)addr.address); - type = BME_680; - break; - case 0x60: - logFoundDevice("BME280", (uint8_t)addr.address); - type = BME_280; - break; - case 0x55: - logFoundDevice("BMP085/BMP180", (uint8_t)addr.address); - type = BMP_085; - break; - case 0x00: - // do we have a DPS310 instead? - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1); - switch (registerValue) { - case 0x10: - logFoundDevice("DPS310", (uint8_t)addr.address); - type = DPS310; - break; - } - break; - default: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID - switch (registerValue) { - case 0x50: // BMP-388 should be 0x50 - logFoundDevice("BMP-388", (uint8_t)addr.address); - type = BMP_3XX; - break; - case 0x60: // BMP-390 should be 0x60 - logFoundDevice("BMP-390", (uint8_t)addr.address); - type = BMP_3XX; - break; - case 0x58: // BMP-280 should be 0x58 - default: - logFoundDevice("BMP-280", (uint8_t)addr.address); - type = BMP_280; - break; - } - break; - } - break; + case XPOWERS_AXP192_AXP2101_ADDRESS: + // Do we have the axp2101/192 or the TCA8418 + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1); + if (registerValue == 0x0) { + logFoundDevice("TCA8418", (uint8_t)addr.address); + type = TCA8418KB; + } else { + logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address); + type = PMU_AXP192_AXP2101; + } + break; + case BME_ADDR: + case BME_ADDR_ALTERNATE: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID + switch (registerValue) { + case 0x61: + logFoundDevice("BME680", (uint8_t)addr.address); + type = BME_680; + break; + case 0x60: + logFoundDevice("BME280", (uint8_t)addr.address); + type = BME_280; + break; + case 0x55: + logFoundDevice("BMP085/BMP180", (uint8_t)addr.address); + type = BMP_085; + break; + case 0x00: + // do we have a DPS310 instead? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1); + switch (registerValue) { + case 0x10: + logFoundDevice("DPS310", (uint8_t)addr.address); + type = DPS310; + break; + } + break; + default: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID + switch (registerValue) { + case 0x50: // BMP-388 should be 0x50 + logFoundDevice("BMP-388", (uint8_t)addr.address); + type = BMP_3XX; + break; + case 0x60: // BMP-390 should be 0x60 + logFoundDevice("BMP-390", (uint8_t)addr.address); + type = BMP_3XX; + break; + case 0x58: // BMP-280 should be 0x58 + default: + logFoundDevice("BMP-280", (uint8_t)addr.address); + type = BMP_280; + break; + } + break; + } + break; #ifndef HAS_NCP5623 - case AHT10_ADDR: - logFoundDevice("AHT10", (uint8_t)addr.address); - type = AHT10; - break; + case AHT10_ADDR: + logFoundDevice("AHT10", (uint8_t)addr.address); + type = AHT10; + break; #endif #if !defined(M5STACK_UNITC6L) - case INA_ADDR: - case INA_ADDR_ALTERNATE: - case INA_ADDR_WAVESHARE_UPS: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); - LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); - if (registerValue == 0x5449) { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); - LOG_DEBUG("Register DIE_UID: 0x%x", registerValue); + case INA_ADDR: + case INA_ADDR_ALTERNATE: + case INA_ADDR_WAVESHARE_UPS: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); + LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); + if (registerValue == 0x5449) { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); + LOG_DEBUG("Register DIE_UID: 0x%x", registerValue); - if (registerValue == 0x2260) { - logFoundDevice("INA226", (uint8_t)addr.address); - type = INA226; - } else { - logFoundDevice("INA260", (uint8_t)addr.address); - type = INA260; - } - } else { // Assume INA219 if INA260 ID is not found - logFoundDevice("INA219", (uint8_t)addr.address); - type = INA219; - } - break; - case INA3221_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); - LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue); - if (registerValue == 0x5449) { - logFoundDevice("INA3221", (uint8_t)addr.address); - type = INA3221; - } else { - /* check the first 2 bytes of the 6 byte response register - LARK FW 1.0 should return: - RESPONSE_STATUS STATUS_SUCCESS (0x53) - RESPONSE_CMD CMD_GET_VERSION (0x05) - RESPONSE_LEN_L 0x02 - RESPONSE_LEN_H 0x00 - RESPONSE_PAYLOAD 0x01 - RESPONSE_PAYLOAD+1 0x00 - */ - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true); - LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue); - if (registerValue == 0x5305) { - logFoundDevice("DFRobot Lark", (uint8_t)addr.address); - type = DFROBOT_LARK; - } - // else: probably a RAK12500/UBLOX GPS on I2C - } - break; + if (registerValue == 0x2260) { + logFoundDevice("INA226", (uint8_t)addr.address); + type = INA226; + } else { + logFoundDevice("INA260", (uint8_t)addr.address); + type = INA260; + } + } else { // Assume INA219 if INA260 ID is not found + logFoundDevice("INA219", (uint8_t)addr.address); + type = INA219; + } + break; + case INA3221_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); + LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue); + if (registerValue == 0x5449) { + logFoundDevice("INA3221", (uint8_t)addr.address); + type = INA3221; + } else { + /* check the first 2 bytes of the 6 byte response register + LARK FW 1.0 should return: + RESPONSE_STATUS STATUS_SUCCESS (0x53) + RESPONSE_CMD CMD_GET_VERSION (0x05) + RESPONSE_LEN_L 0x02 + RESPONSE_LEN_H 0x00 + RESPONSE_PAYLOAD 0x01 + RESPONSE_PAYLOAD+1 0x00 + */ + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true); + LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue); + if (registerValue == 0x5305) { + logFoundDevice("DFRobot Lark", (uint8_t)addr.address); + type = DFROBOT_LARK; + } + // else: probably a RAK12500/UBLOX GPS on I2C + } + break; #endif - case MCP9808_ADDR: - // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some - // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. - { + case MCP9808_ADDR: + // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some + // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. + { #ifdef HAS_STK8XXX - // Check register 0x00 for 0x8700 response to ID STK8BA53 chip. - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); - if (registerValue == 0x8700) { - type = STK8BAXX; - logFoundDevice("STK8BAXX", (uint8_t)addr.address); - break; - } + // Check register 0x00 for 0x8700 response to ID STK8BA53 chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); + if (registerValue == 0x8700) { + type = STK8BAXX; + logFoundDevice("STK8BAXX", (uint8_t)addr.address); + break; + } #endif - // Check register 0x07 for 0x0400 response to ID MCP9808 chip. - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2); - if (registerValue == 0x0400) { - type = MCP9808; - logFoundDevice("MCP9808", (uint8_t)addr.address); - break; - } + // Check register 0x07 for 0x0400 response to ID MCP9808 chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2); + if (registerValue == 0x0400) { + type = MCP9808; + logFoundDevice("MCP9808", (uint8_t)addr.address); + break; + } - // Check register 0x0F for 0x3300 response to ID LIS3DH chip. - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); - if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 - type = LIS3DH; - logFoundDevice("LIS3DH", (uint8_t)addr.address); - } - break; - } - case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT - case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); - if (registerValue == 0x5449) { - type = OPT3001; - logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number - type = SHT4X; - logFoundDevice("SHT4X", (uint8_t)addr.address); - } else { - type = SHT31; - logFoundDevice("SHT31", (uint8_t)addr.address); - } + // Check register 0x0F for 0x3300 response to ID LIS3DH chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); + if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 + type = LIS3DH; + logFoundDevice("LIS3DH", (uint8_t)addr.address); + } + break; + } + case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT + case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); + if (registerValue == 0x5449) { + type = OPT3001; + logFoundDevice("OPT3001", (uint8_t)addr.address); + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number + type = SHT4X; + logFoundDevice("SHT4X", (uint8_t)addr.address); + } else { + type = SHT31; + logFoundDevice("SHT31", (uint8_t)addr.address); + } - break; + break; - SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) - case RCWL9620_ADDR: - // get MAX30102 PARTID - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); - if (registerValue == 0x15) { - type = MAX30102; - logFoundDevice("MAX30102", (uint8_t)addr.address); - break; - } else { - type = RCWL9620; - logFoundDevice("RCWL9620", (uint8_t)addr.address); - } - break; + SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) + case RCWL9620_ADDR: + // get MAX30102 PARTID + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); + if (registerValue == 0x15) { + type = MAX30102; + logFoundDevice("MAX30102", (uint8_t)addr.address); + break; + } else { + type = RCWL9620; + logFoundDevice("RCWL9620", (uint8_t)addr.address); + } + break; - case LPS22HB_ADDR_ALT: - SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) + case LPS22HB_ADDR_ALT: + SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) - case QMI8658_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID - if (registerValue == 0xC0) { - type = BQ24295; - logFoundDevice("BQ24295", (uint8_t)addr.address); - break; - } - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID - if ((registerValue & 0b00000011) == 0b00000010) { - type = BQ25896; - logFoundDevice("BQ25896", (uint8_t)addr.address); - break; - } - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID - if (registerValue == 0x6A) { - type = LSM6DS3; - logFoundDevice("LSM6DS3", (uint8_t)addr.address); - } else { - type = QMI8658; - logFoundDevice("QMI8658", (uint8_t)addr.address); - } - break; + case QMI8658_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID + if (registerValue == 0xC0) { + type = BQ24295; + logFoundDevice("BQ24295", (uint8_t)addr.address); + break; + } + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID + if ((registerValue & 0b00000011) == 0b00000010) { + type = BQ25896; + logFoundDevice("BQ25896", (uint8_t)addr.address); + break; + } + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID + if (registerValue == 0x6A) { + type = LSM6DS3; + logFoundDevice("LSM6DS3", (uint8_t)addr.address); + } else { + type = QMI8658; + logFoundDevice("QMI8658", (uint8_t)addr.address); + } + break; - SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L", (uint8_t)addr.address) #ifdef HAS_QMA6100P - SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else - SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) #endif - case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); - if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 - type = LIS3DH; - logFoundDevice("LIS3DH", (uint8_t)addr.address); - } else { - type = BMA423; - logFoundDevice("BMA423", (uint8_t)addr.address); - } - break; - case TCA9535_ADDR: - case RAK120352_ADDR: - case RAK120353_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); - if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) - type = RAK12035; - logFoundDevice("RAK12035", (uint8_t)addr.address); - } else { - type = TCA9535; - logFoundDevice("TCA9535", (uint8_t)addr.address); - } + case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); + if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 + type = LIS3DH; + logFoundDevice("LIS3DH", (uint8_t)addr.address); + } else { + type = BMA423; + logFoundDevice("BMA423", (uint8_t)addr.address); + } + break; + case TCA9535_ADDR: + case RAK120352_ADDR: + case RAK120353_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); + if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) + type = RAK12035; + logFoundDevice("RAK12035", (uint8_t)addr.address); + } else { + type = TCA9535; + logFoundDevice("TCA9535", (uint8_t)addr.address); + } - break; + break; - SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); - case TCA9555_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1); - if (registerValue == 0x13) { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); - if (registerValue == 0x81) { - type = DA217; - logFoundDevice("DA217", (uint8_t)addr.address); - } else { - type = TCA9555; - logFoundDevice("TCA9555", (uint8_t)addr.address); - } - } else { - type = TCA9555; - logFoundDevice("TCA9555", (uint8_t)addr.address); - } - break; - case TSL25911_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); - if (registerValue == 0x50) { - type = TSL2591; - logFoundDevice("TSL25911", (uint8_t)addr.address); - } else { - type = TSL2561; - logFoundDevice("TSL2561", (uint8_t)addr.address); - } - break; + SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); + case TCA9555_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1); + if (registerValue == 0x13) { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + if (registerValue == 0x81) { + type = DA217; + logFoundDevice("DA217", (uint8_t)addr.address); + } else { + type = TCA9555; + logFoundDevice("TCA9555", (uint8_t)addr.address); + } + } else { + type = TCA9555; + logFoundDevice("TCA9555", (uint8_t)addr.address); + } + break; + case TSL25911_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + if (registerValue == 0x50) { + type = TSL2591; + logFoundDevice("TSL25911", (uint8_t)addr.address); + } else { + type = TSL2561; + logFoundDevice("TSL2561", (uint8_t)addr.address); + } + break; - SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); - case CST328_ADDR: - // Do we have the CST328 or the CST226SE - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); - if (registerValue == 0xA9) { - type = CST226SE; - logFoundDevice("CST226SE", (uint8_t)addr.address); - } else { - type = CST328; - logFoundDevice("CST328", (uint8_t)addr.address); - } - break; + SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); + case CST328_ADDR: + // Do we have the CST328 or the CST226SE + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); + if (registerValue == 0xA9) { + type = CST226SE; + logFoundDevice("CST226SE", (uint8_t)addr.address); + } else { + type = CST328; + logFoundDevice("CST328", (uint8_t)addr.address); + } + break; - SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); - case LTR553ALS_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register - if (registerValue == 0x92) { // LTR553ALS Part ID - type = LTR553ALS; - logFoundDevice("LTR553ALS", (uint8_t)addr.address); - } else { - // Test BH1750 - send power on command - i2cBus->beginTransmission(addr.address); - i2cBus->write(0x01); // Power On command - uint8_t bh1750_error = i2cBus->endTransmission(); - if (bh1750_error == 0) { - type = BH1750; - logFoundDevice("BH1750", (uint8_t)addr.address); - } else { - LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); - } - } - break; + SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); + case LTR553ALS_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register + if (registerValue == 0x92) { // LTR553ALS Part ID + type = LTR553ALS; + logFoundDevice("LTR553ALS", (uint8_t)addr.address); + } else { + // Test BH1750 - send power on command + i2cBus->beginTransmission(addr.address); + i2cBus->write(0x01); // Power On command + uint8_t bh1750_error = i2cBus->endTransmission(); + if (bh1750_error == 0) { + type = BH1750; + logFoundDevice("BH1750", (uint8_t)addr.address); + } else { + LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); + } + } + break; - SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 - SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); #endif - case MLX90614_ADDR_DEF: - // Do we have the MLX90614 or the MPR121KB or the CST226SE - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); - if (registerValue == 0xAB) { - type = CST226SE; - logFoundDevice("CST226SE", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) { - type = MLX90614; - logFoundDevice("MLX90614", (uint8_t)addr.address); - } else { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS - if (registerValue == 0xe0) { - type = DRV2605; - logFoundDevice("DRV2605", (uint8_t)addr.address); - } else { - type = MPR121KB; - logFoundDevice("MPR121KB", (uint8_t)addr.address); - } - } - break; + case MLX90614_ADDR_DEF: + // Do we have the MLX90614 or the MPR121KB or the CST226SE + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); + if (registerValue == 0xAB) { + type = CST226SE; + logFoundDevice("CST226SE", (uint8_t)addr.address); + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) { + type = MLX90614; + logFoundDevice("MLX90614", (uint8_t)addr.address); + } else { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS + if (registerValue == 0xe0) { + type = DRV2605; + logFoundDevice("DRV2605", (uint8_t)addr.address); + } else { + type = MPR121KB; + logFoundDevice("MPR121KB", (uint8_t)addr.address); + } + } + break; - case ICM20948_ADDR: // same as BMX160_ADDR - case ICM20948_ADDR_ALT: // same as MPU6050_ADDR - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + case ICM20948_ADDR: // same as BMX160_ADDR + case ICM20948_ADDR_ALT: // same as MPU6050_ADDR + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 - type = ICM20948; - logFoundDevice("ICM20948", (uint8_t)addr.address); - break; + type = ICM20948; + logFoundDevice("ICM20948", (uint8_t)addr.address); + break; #endif - if (registerValue == 0xEA) { - type = ICM20948; - logFoundDevice("ICM20948", (uint8_t)addr.address); - break; - } else if (addr.address == BMX160_ADDR) { - type = BMX160; - logFoundDevice("BMX160", (uint8_t)addr.address); - break; - } else { - type = MPU6050; - logFoundDevice("MPU6050", (uint8_t)addr.address); - break; - } - break; - - case CGRADSENS_ADDR: - // Register 0x00 of the RadSens sensor contains is product identifier 0x7D - // Undocumented, but some devices return a product identifier of 0x7A - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); - if (registerValue == 0x7D || registerValue == 0x7A) { - type = CGRADSENS; - logFoundDevice("ClimateGuard RadSens", (uint8_t)addr.address); - break; - } else { - LOG_DEBUG("Unexpected Device ID for RadSense: addr=0x%x id=0x%x", CGRADSENS_ADDR, registerValue); - } - break; - - case 0x48: { - i2cBus->beginTransmission(addr.address); - uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC}; - uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19}; - uint8_t info[5]; - size_t len = 0; - i2cBus->write(getInfo, 5); - i2cBus->endTransmission(); - len = i2cBus->readBytes(info, 5); - if (len == 5 && memcmp(expectedInfo, info, len) == 0) { - LOG_INFO("NXP SE050 crypto chip found"); - type = NXP_SE050; - - } else { - LOG_INFO("FT6336U touchscreen found"); - type = FT6336U; - } - break; - } - - default: - LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); - } - } else if (err == 4) { - LOG_ERROR("Unknown error at address 0x%x", (uint8_t)addr.address); + if (registerValue == 0xEA) { + type = ICM20948; + logFoundDevice("ICM20948", (uint8_t)addr.address); + break; + } else if (addr.address == BMX160_ADDR) { + type = BMX160; + logFoundDevice("BMX160", (uint8_t)addr.address); + break; + } else { + type = MPU6050; + logFoundDevice("MPU6050", (uint8_t)addr.address); + break; } + break; - // Check if a type was found for the enumerated device - save, if so - if (type != NONE) { - deviceAddresses[type] = addr; - foundDevices[addr] = type; + case CGRADSENS_ADDR: + // Register 0x00 of the RadSens sensor contains is product identifier 0x7D + // Undocumented, but some devices return a product identifier of 0x7A + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + if (registerValue == 0x7D || registerValue == 0x7A) { + type = CGRADSENS; + logFoundDevice("ClimateGuard RadSens", (uint8_t)addr.address); + break; + } else { + LOG_DEBUG("Unexpected Device ID for RadSense: addr=0x%x id=0x%x", CGRADSENS_ADDR, registerValue); } + break; + + case 0x48: { + i2cBus->beginTransmission(addr.address); + uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC}; + uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19}; + uint8_t info[5]; + size_t len = 0; + i2cBus->write(getInfo, 5); + i2cBus->endTransmission(); + len = i2cBus->readBytes(info, 5); + if (len == 5 && memcmp(expectedInfo, info, len) == 0) { + LOG_INFO("NXP SE050 crypto chip found"); + type = NXP_SE050; + + } else { + LOG_INFO("FT6336U touchscreen found"); + type = FT6336U; + } + break; + } + + default: + LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); + } + } else if (err == 4) { + LOG_ERROR("Unknown error at address 0x%x", (uint8_t)addr.address); } + + // Check if a type was found for the enumerated device - save, if so + if (type != NONE) { + deviceAddresses[type] = addr; + foundDevices[addr] = type; + } + } } -void ScanI2CTwoWire::scanPort(I2CPort port) -{ - scanPort(port, nullptr, 0); -} +void ScanI2CTwoWire::scanPort(I2CPort port) { scanPort(port, nullptr, 0); } -TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) -{ - if (address.port == ScanI2C::I2CPort::WIRE) { - return &Wire; - } else { +TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) { + if (address.port == ScanI2C::I2CPort::WIRE) { + return &Wire; + } else { #if WIRE_INTERFACES_COUNT == 2 - return &Wire1; + return &Wire1; #else - return &Wire; + return &Wire; #endif - } + } } -size_t ScanI2CTwoWire::countDevices() const -{ - return foundDevices.size(); -} +size_t ScanI2CTwoWire::countDevices() const { return foundDevices.size(); } -void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) -{ - LOG_INFO("%s found at address 0x%x", device, address); -} +void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) { LOG_INFO("%s found at address 0x%x", device, address); } #endif diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index c5b791920..a6f9432bf 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -14,49 +14,45 @@ #include "../concurrency/Lock.h" -class ScanI2CTwoWire : public ScanI2C -{ - public: - void scanPort(ScanI2C::I2CPort) override; +class ScanI2CTwoWire : public ScanI2C { +public: + void scanPort(ScanI2C::I2CPort) override; - void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t) override; + void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t) override; - ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override; + ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override; - bool exists(ScanI2C::DeviceType) const override; + bool exists(ScanI2C::DeviceType) const override; - size_t countDevices() const override; + size_t countDevices() const override; - static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress); + static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress); - protected: - FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; +protected: + FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; - private: - typedef struct RegisterLocation { - DeviceAddress i2cAddress; - RegisterAddress registerAddress; +private: + typedef struct RegisterLocation { + DeviceAddress i2cAddress; + RegisterAddress registerAddress; - RegisterLocation(DeviceAddress deviceAddress, RegisterAddress registerAddress) - : i2cAddress(deviceAddress), registerAddress(registerAddress) - { - } + RegisterLocation(DeviceAddress deviceAddress, RegisterAddress registerAddress) : i2cAddress(deviceAddress), registerAddress(registerAddress) {} - } RegisterLocation; + } RegisterLocation; - typedef uint8_t ResponseWidth; + typedef uint8_t ResponseWidth; - std::map foundDevices; + std::map foundDevices; - // note: prone to overwriting if multiple devices of a type are added at different addresses (rare?) - std::map deviceAddresses; + // note: prone to overwriting if multiple devices of a type are added at different addresses (rare?) + std::map deviceAddresses; - concurrency::Lock lock; + concurrency::Lock lock; - uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; + uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; - DeviceType probeOLED(ScanI2C::DeviceAddress) const; + DeviceType probeOLED(ScanI2C::DeviceAddress) const; - static void logFoundDevice(const char *device, uint8_t address); + static void logFoundDevice(const char *device, uint8_t address); }; #endif \ No newline at end of file diff --git a/src/detect/einkScan.h b/src/detect/einkScan.h index d20c7b6e5..d347d90e9 100644 --- a/src/detect/einkScan.h +++ b/src/detect/einkScan.h @@ -4,64 +4,60 @@ #include "../main.h" #include -void d_writeCommand(uint8_t c) -{ - SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); - if (PIN_EINK_DC >= 0) - digitalWrite(PIN_EINK_DC, LOW); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, LOW); - SPI1.transfer(c); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, HIGH); - if (PIN_EINK_DC >= 0) - digitalWrite(PIN_EINK_DC, HIGH); - SPI1.endTransaction(); +void d_writeCommand(uint8_t c) { + SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + if (PIN_EINK_DC >= 0) + digitalWrite(PIN_EINK_DC, LOW); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, LOW); + SPI1.transfer(c); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, HIGH); + if (PIN_EINK_DC >= 0) + digitalWrite(PIN_EINK_DC, HIGH); + SPI1.endTransaction(); } -void d_writeData(uint8_t d) -{ - SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, LOW); - SPI1.transfer(d); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, HIGH); - SPI1.endTransaction(); +void d_writeData(uint8_t d) { + SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, LOW); + SPI1.transfer(d); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, HIGH); + SPI1.endTransaction(); } -unsigned long d_waitWhileBusy(uint16_t busy_time) -{ - if (PIN_EINK_BUSY >= 0) { - delay(1); // add some margin to become active - unsigned long start = micros(); - while (1) { - if (digitalRead(PIN_EINK_BUSY) != HIGH) - break; - delay(1); - if (digitalRead(PIN_EINK_BUSY) != HIGH) - break; - if (micros() - start > 10000000) - break; - } - unsigned long elapsed = micros() - start; - (void)start; - return elapsed; - } else - return busy_time; +unsigned long d_waitWhileBusy(uint16_t busy_time) { + if (PIN_EINK_BUSY >= 0) { + delay(1); // add some margin to become active + unsigned long start = micros(); + while (1) { + if (digitalRead(PIN_EINK_BUSY) != HIGH) + break; + delay(1); + if (digitalRead(PIN_EINK_BUSY) != HIGH) + break; + if (micros() - start > 10000000) + break; + } + unsigned long elapsed = micros() - start; + (void)start; + return elapsed; + } else + return busy_time; } -void scanEInkDevice(void) -{ - SPI1.begin(); - d_writeCommand(0x22); - d_writeData(0x83); - d_writeCommand(0x20); - eink_found = (d_waitWhileBusy(150) > 0) ? true : false; - if (eink_found) - LOG_DEBUG("EInk display found"); - else - LOG_DEBUG("EInk display not found"); - SPI1.end(); +void scanEInkDevice(void) { + SPI1.begin(); + d_writeCommand(0x22); + d_writeData(0x83); + d_writeCommand(0x20); + eink_found = (d_waitWhileBusy(150) > 0) ? true : false; + if (eink_found) + LOG_DEBUG("EInk display found"); + else + LOG_DEBUG("EInk display not found"); + SPI1.end(); } #endif \ No newline at end of file diff --git a/src/freertosinc.h b/src/freertosinc.h index e9e6cd53a..5867c9c5f 100644 --- a/src/freertosinc.h +++ b/src/freertosinc.h @@ -1,7 +1,7 @@ #pragma once -// The FreeRTOS includes are in a different directory on ESP32 and I can't figure out how to make that work with platformio gcc -// options so this is my quick hack to make things work +// The FreeRTOS includes are in a different directory on ESP32 and I can't figure out how to make that work with +// platformio gcc options so this is my quick hack to make things work #if defined(ARDUINO_ARCH_ESP32) #define HAS_FREE_RTOS diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index a61a71dde..67b537891 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -33,10 +33,7 @@ #endif // Not all platforms have std::size(). -template std::size_t array_count(const T (&)[N]) -{ - return N; -} +template std::size_t array_count(const T (&)[N]) { return N; } #ifndef GPS_SERIAL_PORT #define GPS_SERIAL_PORT Serial1 @@ -61,34 +58,33 @@ static GPSUpdateScheduling scheduling; static bool didSerialInit; static struct uBloxGnssModelInfo { - char swVersion[30]; - char hwVersion[10]; - uint8_t extensionNo; - char extension[10][30]; - uint8_t protocol_version; + char swVersion[30]; + char hwVersion[10]; + uint8_t extensionNo; + char extension[10][30]; + uint8_t protocol_version; } ublox_info; #define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway #define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc) // For logging -static const char *getGPSPowerStateString(GPSPowerState state) -{ - switch (state) { - case GPS_ACTIVE: - return "ACTIVE"; - case GPS_IDLE: - return "IDLE"; - case GPS_SOFTSLEEP: - return "SOFTSLEEP"; - case GPS_HARDSLEEP: - return "HARDSLEEP"; - case GPS_OFF: - return "OFF"; - default: - assert(false); // Unhandled enum value.. - return "FALSE"; // to make new ESP-IDF happy - } +static const char *getGPSPowerStateString(GPSPowerState state) { + switch (state) { + case GPS_ACTIVE: + return "ACTIVE"; + case GPS_IDLE: + return "IDLE"; + case GPS_SOFTSLEEP: + return "SOFTSLEEP"; + case GPS_HARDSLEEP: + return "HARDSLEEP"; + case GPS_OFF: + return "OFF"; + default: + assert(false); // Unhandled enum value.. + return "FALSE"; // to make new ESP-IDF happy + } } #ifdef PIN_GPS_SWITCH @@ -98,377 +94,369 @@ static const char *getGPSPowerStateString(GPSPowerState state) int lastState = LOW; bool firstrun = true; -static int32_t gpsSwitch() -{ - if (gps) { - int currentState = digitalRead(PIN_GPS_SWITCH); +static int32_t gpsSwitch() { + if (gps) { + int currentState = digitalRead(PIN_GPS_SWITCH); - // if the switch is set to zero, disable the GPS Thread - if (firstrun) - if (currentState == LOW) - lastState = HIGH; + // if the switch is set to zero, disable the GPS Thread + if (firstrun) + if (currentState == LOW) + lastState = HIGH; - if (currentState != lastState) { - if (currentState == LOW) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - if (!firstrun) - playGPSDisableBeep(); - gps->disable(); - } else { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - if (!firstrun) - playGPSEnableBeep(); - gps->enable(); - } - lastState = currentState; - } - firstrun = false; + if (currentState != lastState) { + if (currentState == LOW) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + if (!firstrun) + playGPSDisableBeep(); + gps->disable(); + } else { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + if (!firstrun) + playGPSEnableBeep(); + gps->enable(); + } + lastState = currentState; } - return 1000; + firstrun = false; + } + return 1000; } static concurrency::Periodic *gpsPeriodic; #endif -static void UBXChecksum(uint8_t *message, size_t length) -{ - uint8_t CK_A = 0, CK_B = 0; +static void UBXChecksum(uint8_t *message, size_t length) { + uint8_t CK_A = 0, CK_B = 0; - // Calculate the checksum, starting from the CLASS field (which is message[2]) - for (size_t i = 2; i < length - 2; i++) { - CK_A = (CK_A + message[i]) & 0xFF; - CK_B = (CK_B + CK_A) & 0xFF; - } + // Calculate the checksum, starting from the CLASS field (which is message[2]) + for (size_t i = 2; i < length - 2; i++) { + CK_A = (CK_A + message[i]) & 0xFF; + CK_B = (CK_B + CK_A) & 0xFF; + } - // Place the calculated checksum values in the message - message[length - 2] = CK_A; - message[length - 1] = CK_B; + // Place the calculated checksum values in the message + message[length - 2] = CK_A; + message[length - 1] = CK_B; } // Calculate the checksum for a CAS packet -static void CASChecksum(uint8_t *message, size_t length) -{ - uint32_t cksum = ((uint32_t)message[5] << 24); // Message ID - cksum += ((uint32_t)message[4]) << 16; // Class - cksum += message[2]; // Payload Len +static void CASChecksum(uint8_t *message, size_t length) { + uint32_t cksum = ((uint32_t)message[5] << 24); // Message ID + cksum += ((uint32_t)message[4]) << 16; // Class + cksum += message[2]; // Payload Len - // Iterate over the payload as a series of uint32_t's and - // accumulate the cksum - for (size_t i = 0; i < (length - 10) / 4; i++) { - uint32_t pl = 0; - memcpy(&pl, (message + 6) + (i * sizeof(uint32_t)), sizeof(uint32_t)); // avoid pointer dereference - cksum += pl; - } + // Iterate over the payload as a series of uint32_t's and + // accumulate the cksum + for (size_t i = 0; i < (length - 10) / 4; i++) { + uint32_t pl = 0; + memcpy(&pl, (message + 6) + (i * sizeof(uint32_t)), sizeof(uint32_t)); // avoid pointer dereference + cksum += pl; + } - // Place the checksum values in the message - message[length - 4] = (cksum & 0xFF); - message[length - 3] = (cksum & (0xFF << 8)) >> 8; - message[length - 2] = (cksum & (0xFF << 16)) >> 16; - message[length - 1] = (cksum & (0xFF << 24)) >> 24; + // Place the checksum values in the message + message[length - 4] = (cksum & 0xFF); + message[length - 3] = (cksum & (0xFF << 8)) >> 8; + message[length - 2] = (cksum & (0xFF << 16)) >> 16; + message[length - 1] = (cksum & (0xFF << 24)) >> 24; } // Function to create a ublox packet for editing in memory -uint8_t GPS::makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) -{ - // Construct the UBX packet - UBXscratch[0] = 0xB5; // header - UBXscratch[1] = 0x62; // header - UBXscratch[2] = class_id; // class - UBXscratch[3] = msg_id; // id - UBXscratch[4] = payload_size; // length - UBXscratch[5] = 0x00; +uint8_t GPS::makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) { + // Construct the UBX packet + UBXscratch[0] = 0xB5; // header + UBXscratch[1] = 0x62; // header + UBXscratch[2] = class_id; // class + UBXscratch[3] = msg_id; // id + UBXscratch[4] = payload_size; // length + UBXscratch[5] = 0x00; - UBXscratch[6 + payload_size] = 0x00; // CK_A - UBXscratch[7 + payload_size] = 0x00; // CK_B + UBXscratch[6 + payload_size] = 0x00; // CK_A + UBXscratch[7 + payload_size] = 0x00; // CK_B - for (int i = 0; i < payload_size; i++) { - UBXscratch[6 + i] = pgm_read_byte(&msg[i]); - } - UBXChecksum(UBXscratch, (payload_size + 8)); - return (payload_size + 8); + for (int i = 0; i < payload_size; i++) { + UBXscratch[6 + i] = pgm_read_byte(&msg[i]); + } + UBXChecksum(UBXscratch, (payload_size + 8)); + return (payload_size + 8); } // Function to create a CAS packet for editing in memory -uint8_t GPS::makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) -{ - // General CAS structure - // | H1 | H2 | payload_len | cls | msg | Payload ... | Checksum | - // Size: | 1 | 1 | 2 | 1 | 1 | payload_len | 4 | - // Pos: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 ... | 6 + payload_len ... | - // |------|------|-------------|------|------|------|--------------|---------------------------| - // | 0xBA | 0xCE | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX ... | 0xXX | 0xXX | 0xXX | 0xXX | +uint8_t GPS::makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) { + // General CAS structure + // | H1 | H2 | payload_len | cls | msg | Payload ... | Checksum | + // Size: | 1 | 1 | 2 | 1 | 1 | payload_len | 4 | + // Pos: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 ... | 6 + payload_len ... | + // |------|------|-------------|------|------|------|--------------|---------------------------| + // | 0xBA | 0xCE | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX ... | 0xXX | 0xXX | 0xXX | 0xXX | - // Construct the CAS packet - UBXscratch[0] = 0xBA; // header 1 (0xBA) - UBXscratch[1] = 0xCE; // header 2 (0xCE) - UBXscratch[2] = payload_size; // length 1 - UBXscratch[3] = 0; // length 2 - UBXscratch[4] = class_id; // class - UBXscratch[5] = msg_id; // id + // Construct the CAS packet + UBXscratch[0] = 0xBA; // header 1 (0xBA) + UBXscratch[1] = 0xCE; // header 2 (0xCE) + UBXscratch[2] = payload_size; // length 1 + UBXscratch[3] = 0; // length 2 + UBXscratch[4] = class_id; // class + UBXscratch[5] = msg_id; // id - UBXscratch[6 + payload_size] = 0x00; // Checksum - UBXscratch[7 + payload_size] = 0x00; - UBXscratch[8 + payload_size] = 0x00; - UBXscratch[9 + payload_size] = 0x00; + UBXscratch[6 + payload_size] = 0x00; // Checksum + UBXscratch[7 + payload_size] = 0x00; + UBXscratch[8 + payload_size] = 0x00; + UBXscratch[9 + payload_size] = 0x00; - for (int i = 0; i < payload_size; i++) { - UBXscratch[6 + i] = pgm_read_byte(&msg[i]); - } - CASChecksum(UBXscratch, (payload_size + 10)); + for (int i = 0; i < payload_size; i++) { + UBXscratch[6 + i] = pgm_read_byte(&msg[i]); + } + CASChecksum(UBXscratch, (payload_size + 10)); #if defined(GPS_DEBUG) && defined(DEBUG_PORT) - LOG_DEBUG("CAS packet: "); - DEBUG_PORT.hexDump(MESHTASTIC_LOG_LEVEL_DEBUG, UBXscratch, payload_size + 10); + LOG_DEBUG("CAS packet: "); + DEBUG_PORT.hexDump(MESHTASTIC_LOG_LEVEL_DEBUG, UBXscratch, payload_size + 10); #endif - return (payload_size + 10); + return (payload_size + 10); } -GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis) -{ - uint8_t buffer[768] = {0}; - uint8_t b; - int bytesRead = 0; - uint32_t startTimeout = millis() + waitMillis; +GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis) { + uint8_t buffer[768] = {0}; + uint8_t b; + int bytesRead = 0; + uint32_t startTimeout = millis() + waitMillis; #ifdef GPS_DEBUG - std::string debugmsg = ""; + std::string debugmsg = ""; #endif - while (millis() < startTimeout) { - if (_serial_gps->available()) { - b = _serial_gps->read(); + while (millis() < startTimeout) { + if (_serial_gps->available()) { + b = _serial_gps->read(); #ifdef GPS_DEBUG - debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.'); + debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.'); #endif - buffer[bytesRead] = b; - bytesRead++; - if ((bytesRead == 767) || (b == '\r')) { + buffer[bytesRead] = b; + bytesRead++; + if ((bytesRead == 767) || (b == '\r')) { #ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); + LOG_DEBUG(debugmsg.c_str()); #endif - if (strnstr((char *)buffer, message, bytesRead) != nullptr) { + if (strnstr((char *)buffer, message, bytesRead) != nullptr) { #ifdef GPS_DEBUG - LOG_DEBUG("Found: %s", message); // Log the found message + LOG_DEBUG("Found: %s", message); // Log the found message #endif - return GNSS_RESPONSE_OK; - } else { - bytesRead = 0; - } - } + return GNSS_RESPONSE_OK; + } else { + bytesRead = 0; } + } } - return GNSS_RESPONSE_NONE; + } + return GNSS_RESPONSE_NONE; } -GPS_RESPONSE GPS::getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) -{ - uint32_t startTime = millis(); - uint8_t buffer[CAS_ACK_NACK_MSG_SIZE] = {0}; - uint8_t bufferPos = 0; +GPS_RESPONSE GPS::getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) { + uint32_t startTime = millis(); + uint8_t buffer[CAS_ACK_NACK_MSG_SIZE] = {0}; + uint8_t bufferPos = 0; - // CAS-ACK-(N)ACK structure - // | H1 | H2 | Payload Len | cls | msg | Payload | Checksum (4) | - // | | | | | | Cls | Msg | Reserved | | - // |------|------|-------------|------|------|------|------|-------------|---------------------------| - // ACK-NACK| 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x00 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | - // ACK-ACK | 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x01 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | + // CAS-ACK-(N)ACK structure + // | H1 | H2 | Payload Len | cls | msg | Payload | Checksum (4) | + // | | | | | | Cls | Msg | Reserved | | + // |------|------|-------------|------|------|------|------|-------------|---------------------------| + // ACK-NACK| 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x00 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | + // ACK-ACK | 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x01 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | - while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { - if (_serial_gps->available()) { - buffer[bufferPos++] = _serial_gps->read(); + while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { + if (_serial_gps->available()) { + buffer[bufferPos++] = _serial_gps->read(); - // keep looking at the first two bytes of buffer until - // we have found the CAS frame header (0xBA, 0xCE), if not - // keep reading bytes until we find a frame header or we run - // out of time. - if ((bufferPos == 2) && !(buffer[0] == 0xBA && buffer[1] == 0xCE)) { - buffer[0] = buffer[1]; - buffer[1] = 0; - bufferPos = 1; - } - } - - // we have read all the bytes required for the Ack/Nack (14-bytes) - // and we must have found a frame to get this far - if (bufferPos == sizeof(buffer) - 1) { - uint8_t msg_cls = buffer[4]; // message class should be 0x05 - uint8_t msg_msg_id = buffer[5]; // message id should be 0x00 or 0x01 - uint8_t payload_cls = buffer[6]; // payload class id - uint8_t payload_msg = buffer[7]; // payload message id - - // Check for an ACK-ACK for the specified class and message id - if ((msg_cls == 0x05) && (msg_msg_id == 0x01) && payload_cls == class_id && payload_msg == msg_id) { -#ifdef GPS_DEBUG - LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); -#endif - return GNSS_RESPONSE_OK; - } - - // Check for an ACK-NACK for the specified class and message id - if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) { -#ifdef GPS_DEBUG - LOG_WARN("Got NACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); -#endif - return GNSS_RESPONSE_NAK; - } - - // This isn't the frame we are looking for, clear the buffer - // and try again until we run out of time. - memset(buffer, 0x0, sizeof(buffer)); - bufferPos = 0; - } + // keep looking at the first two bytes of buffer until + // we have found the CAS frame header (0xBA, 0xCE), if not + // keep reading bytes until we find a frame header or we run + // out of time. + if ((bufferPos == 2) && !(buffer[0] == 0xBA && buffer[1] == 0xCE)) { + buffer[0] = buffer[1]; + buffer[1] = 0; + bufferPos = 1; + } } - return GNSS_RESPONSE_NONE; + + // we have read all the bytes required for the Ack/Nack (14-bytes) + // and we must have found a frame to get this far + if (bufferPos == sizeof(buffer) - 1) { + uint8_t msg_cls = buffer[4]; // message class should be 0x05 + uint8_t msg_msg_id = buffer[5]; // message id should be 0x00 or 0x01 + uint8_t payload_cls = buffer[6]; // payload class id + uint8_t payload_msg = buffer[7]; // payload message id + + // Check for an ACK-ACK for the specified class and message id + if ((msg_cls == 0x05) && (msg_msg_id == 0x01) && payload_cls == class_id && payload_msg == msg_id) { +#ifdef GPS_DEBUG + LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); +#endif + return GNSS_RESPONSE_OK; + } + + // Check for an ACK-NACK for the specified class and message id + if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) { +#ifdef GPS_DEBUG + LOG_WARN("Got NACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); +#endif + return GNSS_RESPONSE_NAK; + } + + // This isn't the frame we are looking for, clear the buffer + // and try again until we run out of time. + memset(buffer, 0x0, sizeof(buffer)); + bufferPos = 0; + } + } + return GNSS_RESPONSE_NONE; } -GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) -{ - uint8_t b; - uint8_t ack = 0; - const uint8_t ackP[2] = {class_id, msg_id}; - uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00}; - uint32_t startTime = millis(); - const char frame_errors[] = "More than 100 frame errors"; - int sCounter = 0; +GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) { + uint8_t b; + uint8_t ack = 0; + const uint8_t ackP[2] = {class_id, msg_id}; + uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint32_t startTime = millis(); + const char frame_errors[] = "More than 100 frame errors"; + int sCounter = 0; #ifdef GPS_DEBUG - std::string debugmsg = ""; + std::string debugmsg = ""; #endif - for (int j = 2; j < 6; j++) { - buf[8] += buf[j]; - buf[9] += buf[8]; - } + for (int j = 2; j < 6; j++) { + buf[8] += buf[j]; + buf[9] += buf[8]; + } - for (int j = 0; j < 2; j++) { - buf[6 + j] = ackP[j]; - buf[8] += buf[6 + j]; - buf[9] += buf[8]; - } + for (int j = 0; j < 2; j++) { + buf[6 + j] = ackP[j]; + buf[8] += buf[6 + j]; + buf[9] += buf[8]; + } - while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { - if (ack > 9) { + while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { + if (ack > 9) { #ifdef GPS_DEBUG - LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); + LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); #endif - return GNSS_RESPONSE_OK; // ACK received + return GNSS_RESPONSE_OK; // ACK received + } + if (_serial_gps->available()) { + b = _serial_gps->read(); + if (b == frame_errors[sCounter]) { + sCounter++; + if (sCounter == 26) { +#ifdef GPS_DEBUG + + LOG_DEBUG(debugmsg.c_str()); +#endif + return GNSS_RESPONSE_FRAME_ERRORS; } - if (_serial_gps->available()) { - b = _serial_gps->read(); - if (b == frame_errors[sCounter]) { - sCounter++; - if (sCounter == 26) { + } else { + sCounter = 0; + } #ifdef GPS_DEBUG - - LOG_DEBUG(debugmsg.c_str()); + debugmsg += vformat("%02X", b); #endif - return GNSS_RESPONSE_FRAME_ERRORS; - } - } else { - sCounter = 0; - } + if (b == buf[ack]) { + ack++; + } else { + if (ack == 3 && b == 0x00) { // UBX-ACK-NAK message #ifdef GPS_DEBUG - debugmsg += vformat("%02X", b); + LOG_DEBUG(debugmsg.c_str()); #endif - if (b == buf[ack]) { - ack++; - } else { - if (ack == 3 && b == 0x00) { // UBX-ACK-NAK message -#ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); -#endif - LOG_WARN("Got NAK for class %02X message %02X", class_id, msg_id); - return GNSS_RESPONSE_NAK; // NAK received - } - ack = 0; // Reset the acknowledgement counter - } + LOG_WARN("Got NAK for class %02X message %02X", class_id, msg_id); + return GNSS_RESPONSE_NAK; // NAK received } + ack = 0; // Reset the acknowledgement counter + } } + } #ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); - LOG_WARN("No response for class %02X message %02X", class_id, msg_id); + LOG_DEBUG(debugmsg.c_str()); + LOG_WARN("No response for class %02X message %02X", class_id, msg_id); #endif - return GNSS_RESPONSE_NONE; // No response received within timeout + return GNSS_RESPONSE_NONE; // No response received within timeout } /** * @brief * @note New method, this method can wait for the specified class and message ID, and return the payload - * @param *buffer: The message buffer, if there is a response payload message, it will be returned through the buffer parameter + * @param *buffer: The message buffer, if there is a response payload message, it will be returned through the buffer + * parameter * @param size: size of buffer * @param requestedClass: request class constant * @param requestedID: request message ID constant * @retval length of payload message */ -int GPS::getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis) -{ - uint16_t ubxFrameCounter = 0; - uint32_t startTime = millis(); - uint16_t needRead = 0; +int GPS::getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis) { + uint16_t ubxFrameCounter = 0; + uint32_t startTime = millis(); + uint16_t needRead = 0; - while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { - if (_serial_gps->available()) { - int c = _serial_gps->read(); - switch (ubxFrameCounter) { - case 0: - // ubxFrame 'μ' - if (c == 0xB5) { - ubxFrameCounter++; - } - break; - case 1: - // ubxFrame 'b' - if (c == 0x62) { - ubxFrameCounter++; - } else { - ubxFrameCounter = 0; - } - break; - case 2: - // Class - if (c == requestedClass) { - ubxFrameCounter++; - } else { - ubxFrameCounter = 0; - } - break; - case 3: - // Message ID - if (c == requestedID) { - ubxFrameCounter++; - } else { - ubxFrameCounter = 0; - } - break; - case 4: - // Payload length lsb - needRead = c; - ubxFrameCounter++; - break; - case 5: - // Payload length msb - needRead |= (c << 8); - ubxFrameCounter++; - // Check for buffer overflow - if (needRead >= size) { - ubxFrameCounter = 0; - break; - } - if (_serial_gps->readBytes(buffer, needRead) != needRead) { - ubxFrameCounter = 0; - } else { - // return payload length -#ifdef GPS_DEBUG - LOG_INFO("Got ACK for class %02X message %02X in %dms", requestedClass, requestedID, millis() - startTime); -#endif - return needRead; - } - break; - - default: - break; - } + while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { + if (_serial_gps->available()) { + int c = _serial_gps->read(); + switch (ubxFrameCounter) { + case 0: + // ubxFrame 'μ' + if (c == 0xB5) { + ubxFrameCounter++; } + break; + case 1: + // ubxFrame 'b' + if (c == 0x62) { + ubxFrameCounter++; + } else { + ubxFrameCounter = 0; + } + break; + case 2: + // Class + if (c == requestedClass) { + ubxFrameCounter++; + } else { + ubxFrameCounter = 0; + } + break; + case 3: + // Message ID + if (c == requestedID) { + ubxFrameCounter++; + } else { + ubxFrameCounter = 0; + } + break; + case 4: + // Payload length lsb + needRead = c; + ubxFrameCounter++; + break; + case 5: + // Payload length msb + needRead |= (c << 8); + ubxFrameCounter++; + // Check for buffer overflow + if (needRead >= size) { + ubxFrameCounter = 0; + break; + } + if (_serial_gps->readBytes(buffer, needRead) != needRead) { + ubxFrameCounter = 0; + } else { + // return payload length +#ifdef GPS_DEBUG + LOG_INFO("Got ACK for class %02X message %02X in %dms", requestedClass, requestedID, millis() - startTime); +#endif + return needRead; + } + break; + + default: + break; + } } - return 0; + } + return 0; } #if GPS_BAUDRATE_FIXED @@ -491,1150 +479,1128 @@ static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE}; * to known GPS responses. * @retval Whether setup reached the end of its potential to configure the GPS. */ -bool GPS::setup() -{ - if (!didSerialInit) { - int msglen = 0; - if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { - if (probeTries < GPS_PROBETRIES) { - gnssModel = probe(serialSpeeds[speedSelect]); - if (gnssModel == GNSS_MODEL_UNKNOWN) { - if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) { - speedSelect = 0; - ++probeTries; - } - } - } - // Rare Serial Speeds +bool GPS::setup() { + if (!didSerialInit) { + int msglen = 0; + if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { + if (probeTries < GPS_PROBETRIES) { + gnssModel = probe(serialSpeeds[speedSelect]); + if (gnssModel == GNSS_MODEL_UNKNOWN) { + if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) { + speedSelect = 0; + ++probeTries; + } + } + } + // Rare Serial Speeds #ifndef CONFIG_IDF_TARGET_ESP32C6 - if (probeTries == GPS_PROBETRIES) { - gnssModel = probe(rareSerialSpeeds[speedSelect]); - if (gnssModel == GNSS_MODEL_UNKNOWN) { - if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) { - LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE); - return true; - } - } - } + if (probeTries == GPS_PROBETRIES) { + gnssModel = probe(rareSerialSpeeds[speedSelect]); + if (gnssModel == GNSS_MODEL_UNKNOWN) { + if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) { + LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE); + return true; + } + } + } #endif - } - - if (gnssModel != GNSS_MODEL_UNKNOWN) { - setConnected(); - } else { - return false; - } - - if (gnssModel == GNSS_MODEL_MTK) { - /* - * t-beam-s3-core uses the same L76K GNSS module as t-echo. - * Unlike t-echo, L76K uses 9600 baud rate for communication by default. - * */ - - // Initialize the L76K Chip, use GPS + GLONASS + BEIDOU - _serial_gps->write("$PCAS04,7*1E\r\n"); - delay(250); - // only ask for RMC and GGA - _serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n"); - delay(250); - // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g - _serial_gps->write("$PCAS11,3*1E\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_MTK_L76B) { - // Waveshare Pico-GPS hat uses the L76B with 9600 baud - // Initialize the L76B Chip, use GPS + GLONASS - // See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29 - _serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n"); - // Above command will reset the GPS and takes longer before it will accept new commands - delay(1000); - // only ask for RMC and GGA (GNRMC and GNGGA) - // See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1 - _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); - delay(250); - // Enable SBAS - _serial_gps->write("$PMTK301,2*2E\r\n"); - delay(250); - // Enable PPS for 2D/3D fix only - _serial_gps->write("$PMTK285,3,100*3F\r\n"); - delay(250); - // Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s) - _serial_gps->write("$PMTK886,1*29\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_MTK_PA1010D) { - // PA1010D is used in the Pimoroni GPS board. - - // Enable all constellations. - _serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n"); - // Above command will reset the GPS and takes longer before it will accept new commands - delay(1000); - // Only ask for RMC and GGA (GNRMC and GNGGA) - _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); - delay(250); - // Enable SBAS / WAAS - _serial_gps->write("$PMTK301,2*2E\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_MTK_PA1616S) { - // PA1616S is used in some GPS breakout boards from Adafruit - // PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here. - _serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n"); - // Above command will reset the GPS and takes longer before it will accept new commands - delay(1000); - // Only ask for RMC and GGA (GNRMC and GNGGA) - _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); - delay(250); - // Enable SBAS / WAAS - _serial_gps->write("$PMTK301,2*2E\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_ATGM336H) { - // Set the intial configuration of the device - these _should_ work for most AT6558 devices - msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); - _serial_gps->write(UBXscratch, msglen); - if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { - LOG_WARN("ATGM336H: Could not set Config"); - } - - // Set the update frequence to 1Hz - msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); - _serial_gps->write(UBXscratch, msglen); - if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { - LOG_WARN("ATGM336H: Could not set Update Frequency"); - } - - // Set the NEMA output messages - // Ask for only RMC and GGA - uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA}; - for (unsigned int i = 0; i < sizeof(fields); i++) { - // Construct a CAS-CFG-MSG packet - uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00}; - msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet); - _serial_gps->write(UBXscratch, msglen); - if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) { - LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]); - } - } - } else if (gnssModel == GNSS_MODEL_UC6580) { - // The Unicore UC6580 can use a lot of sat systems, enable it to - // use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS - // This will reset the receiver, so wait a bit afterwards - // The paranoid will wait for the OK*04 confirmation response after each command. - _serial_gps->write("$CFGSYS,h35155\r\n"); - delay(750); - // Must be done after the CFGSYS command - // Turn off GSV messages, we don't really care about which and where the sats are, maybe someday. - _serial_gps->write("$CFGMSG,0,3,0\r\n"); - delay(250); - // Turn off GSA messages, TinyGPS++ doesn't use this message. - _serial_gps->write("$CFGMSG,0,2,0\r\n"); - delay(250); - // Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care. - _serial_gps->write("$CFGMSG,6,0,0\r\n"); - delay(250); - _serial_gps->write("$CFGMSG,6,1,0\r\n"); - delay(250); - } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) { - - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN || - config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) { - _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC - // GPS GLONASS GALILEO BDS QZSS NAVIC - // 1 0 1 0 0 1 - } else { - _serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS - // GPS GLONASS GALILEO BDS QZSS NAVIC - // 1 1 1 1 0 0 - } - // Configure NMEA (sentences will output once per fix) - _serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON - _serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF - _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF - _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF - _serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON - _serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF - _serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON - - delay(250); - _serial_gps->write("$PAIR513*3D\r\n"); // save configuration - } else if (gnssModel == GNSS_MODEL_UBLOX6) { - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); - SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); - SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); - - // Turn off unwanted NMEA messages, set update rate - SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); - - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500); - SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500); - - msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); - _serial_gps->write(UBXscratch, msglen); - if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { - LOG_WARN("Unable to save GNSS module config"); - } else { - LOG_INFO("GNSS module config saved!"); - } - } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) { - if (gnssModel == GNSS_MODEL_UBLOX7) { - LOG_DEBUG("Set GPS+SBAS"); - msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7); - _serial_gps->write(UBXscratch, msglen); - } else { // 8,9 - msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8); - _serial_gps->write(UBXscratch, msglen); - } - - if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) { - // It's not critical if the module doesn't acknowledge this configuration. - LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?"); - } else { - if (gnssModel == GNSS_MODEL_UBLOX7) { - LOG_INFO("GPS+SBAS configured"); - } else { // 8,9 - LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); - } - // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next - // commands for the M8 it tends to be more... 1 sec should be enough ;>) - delay(1000); - } - - // Disable Text Info messages //6,7,8,9 - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); - - if (gnssModel == GNSS_MODEL_UBLOX8) { // 8 - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500); - - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500); - } else { // 6,7,9 - SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); - SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); - } - // Turn off unwanted NMEA messages, set update rate - SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); - - if (ublox_info.protocol_version >= 18) { - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); - SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - - // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats. - if (gnssModel == GNSS_MODEL_UBLOX8) { - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); - } - } else { - SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500); - SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - } - - msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); - _serial_gps->write(UBXscratch, msglen); - if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { - LOG_WARN("Unable to save GNSS module config"); - } else { - LOG_INFO("GNSS module configuration saved!"); - } - } else if (gnssModel == GNSS_MODEL_UBLOX10) { - delay(1000); - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300); - delay(750); - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300); - delay(750); - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300); - delay(750); - // Next disable Info txt messages in BBR layer - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300); - delay(750); - // Do M10 configuration for Power Management. - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300); - delay(750); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300); - delay(750); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300); - delay(750); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300); - delay(750); - // Here is where the init commands should go to do further M10 initialization. - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300); - delay(750); // will cause a receiver restart so wait a bit - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300); - delay(750); // will cause a receiver restart so wait a bit - - // Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic - // sleep. - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300); - delay(750); - // Next enable wanted NMEA messages in RAM layer - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500); - delay(750); - - // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. - // BBR will survive a restart, and power off for a while, but modules with small backup - // batteries or super caps will not retain the config for a long power off time. - msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10); - _serial_gps->write(UBXscratch, msglen); - if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { - LOG_WARN("Unable to save GNSS module config"); - } else { - LOG_INFO("GNSS module configuration saved!"); - } - } else if (gnssModel == GNSS_MODEL_CM121) { - // only ask for RMC and GGA - // enable GGA - _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); - delay(250); - // enable RMC - _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); - delay(250); - } - didSerialInit = true; } - notifyDeepSleepObserver.observe(¬ifyDeepSleep); + if (gnssModel != GNSS_MODEL_UNKNOWN) { + setConnected(); + } else { + return false; + } - return true; + if (gnssModel == GNSS_MODEL_MTK) { + /* + * t-beam-s3-core uses the same L76K GNSS module as t-echo. + * Unlike t-echo, L76K uses 9600 baud rate for communication by default. + * */ + + // Initialize the L76K Chip, use GPS + GLONASS + BEIDOU + _serial_gps->write("$PCAS04,7*1E\r\n"); + delay(250); + // only ask for RMC and GGA + _serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n"); + delay(250); + // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g + _serial_gps->write("$PCAS11,3*1E\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_L76B) { + // Waveshare Pico-GPS hat uses the L76B with 9600 baud + // Initialize the L76B Chip, use GPS + GLONASS + // See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29 + _serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // only ask for RMC and GGA (GNRMC and GNGGA) + // See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1 + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); + // Enable PPS for 2D/3D fix only + _serial_gps->write("$PMTK285,3,100*3F\r\n"); + delay(250); + // Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s) + _serial_gps->write("$PMTK886,1*29\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_PA1010D) { + // PA1010D is used in the Pimoroni GPS board. + + // Enable all constellations. + _serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // Only ask for RMC and GGA (GNRMC and GNGGA) + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS / WAAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_PA1616S) { + // PA1616S is used in some GPS breakout boards from Adafruit + // PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here. + _serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // Only ask for RMC and GGA (GNRMC and GNGGA) + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS / WAAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_ATGM336H) { + // Set the intial configuration of the device - these _should_ work for most AT6558 devices + msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); + _serial_gps->write(UBXscratch, msglen); + if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { + LOG_WARN("ATGM336H: Could not set Config"); + } + + // Set the update frequence to 1Hz + msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); + _serial_gps->write(UBXscratch, msglen); + if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { + LOG_WARN("ATGM336H: Could not set Update Frequency"); + } + + // Set the NEMA output messages + // Ask for only RMC and GGA + uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA}; + for (unsigned int i = 0; i < sizeof(fields); i++) { + // Construct a CAS-CFG-MSG packet + uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00}; + msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet); + _serial_gps->write(UBXscratch, msglen); + if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) { + LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]); + } + } + } else if (gnssModel == GNSS_MODEL_UC6580) { + // The Unicore UC6580 can use a lot of sat systems, enable it to + // use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS + // This will reset the receiver, so wait a bit afterwards + // The paranoid will wait for the OK*04 confirmation response after each command. + _serial_gps->write("$CFGSYS,h35155\r\n"); + delay(750); + // Must be done after the CFGSYS command + // Turn off GSV messages, we don't really care about which and where the sats are, maybe someday. + _serial_gps->write("$CFGMSG,0,3,0\r\n"); + delay(250); + // Turn off GSA messages, TinyGPS++ doesn't use this message. + _serial_gps->write("$CFGMSG,0,2,0\r\n"); + delay(250); + // Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care. + _serial_gps->write("$CFGMSG,6,0,0\r\n"); + delay(250); + _serial_gps->write("$CFGMSG,6,1,0\r\n"); + delay(250); + } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) { + + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN || config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) { + _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC + // GPS GLONASS GALILEO BDS QZSS NAVIC + // 1 0 1 0 0 1 + } else { + _serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS + // GPS GLONASS GALILEO BDS QZSS NAVIC + // 1 1 1 1 0 0 + } + // Configure NMEA (sentences will output once per fix) + _serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON + _serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF + _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF + _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF + _serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON + _serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF + _serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON + + delay(250); + _serial_gps->write("$PAIR513*3D\r\n"); // save configuration + } else if (gnssModel == GNSS_MODEL_UBLOX6) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); + SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); + SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); + + // Turn off unwanted NMEA messages, set update rate + SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); + + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500); + + msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); + _serial_gps->write(UBXscratch, msglen); + if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { + LOG_WARN("Unable to save GNSS module config"); + } else { + LOG_INFO("GNSS module config saved!"); + } + } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) { + if (gnssModel == GNSS_MODEL_UBLOX7) { + LOG_DEBUG("Set GPS+SBAS"); + msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7); + _serial_gps->write(UBXscratch, msglen); + } else { // 8,9 + msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8); + _serial_gps->write(UBXscratch, msglen); + } + + if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) { + // It's not critical if the module doesn't acknowledge this configuration. + LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?"); + } else { + if (gnssModel == GNSS_MODEL_UBLOX7) { + LOG_INFO("GPS+SBAS configured"); + } else { // 8,9 + LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); + } + // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next + // commands for the M8 it tends to be more... 1 sec should be enough ;>) + delay(1000); + } + + // Disable Text Info messages //6,7,8,9 + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); + + if (gnssModel == GNSS_MODEL_UBLOX8) { // 8 + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500); + + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500); + } else { // 6,7,9 + SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); + SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); + } + // Turn off unwanted NMEA messages, set update rate + SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); + + if (ublox_info.protocol_version >= 18) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); + + // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats. + if (gnssModel == GNSS_MODEL_UBLOX8) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); + } + } else { + SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); + } + + msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); + _serial_gps->write(UBXscratch, msglen); + if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { + LOG_WARN("Unable to save GNSS module config"); + } else { + LOG_INFO("GNSS module configuration saved!"); + } + } else if (gnssModel == GNSS_MODEL_UBLOX10) { + delay(1000); + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300); + delay(750); + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300); + delay(750); + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300); + delay(750); + // Next disable Info txt messages in BBR layer + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300); + delay(750); + // Do M10 configuration for Power Management. + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300); + delay(750); + // Here is where the init commands should go to do further M10 initialization. + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300); + delay(750); // will cause a receiver restart so wait a bit + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300); + delay(750); // will cause a receiver restart so wait a bit + + // Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic + // sleep. + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300); + delay(750); + // Next enable wanted NMEA messages in RAM layer + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500); + delay(750); + + // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. + // BBR will survive a restart, and power off for a while, but modules with small backup + // batteries or super caps will not retain the config for a long power off time. + msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10); + _serial_gps->write(UBXscratch, msglen); + if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { + LOG_WARN("Unable to save GNSS module config"); + } else { + LOG_INFO("GNSS module configuration saved!"); + } + } else if (gnssModel == GNSS_MODEL_CM121) { + // only ask for RMC and GGA + // enable GGA + _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); + delay(250); + // enable RMC + _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); + delay(250); + } + didSerialInit = true; + } + + notifyDeepSleepObserver.observe(¬ifyDeepSleep); + + return true; } -GPS::~GPS() -{ - // we really should unregister our sleep observer - notifyDeepSleepObserver.unobserve(¬ifyDeepSleep); +GPS::~GPS() { + // we really should unregister our sleep observer + notifyDeepSleepObserver.unobserve(¬ifyDeepSleep); } // Put the GPS hardware into a specified state -void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) -{ - // Update the stored GPSPowerstate, and create local copies - GPSPowerState oldState = powerState; - powerState = newState; - LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); +void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) { + // Update the stored GPSPowerstate, and create local copies + GPSPowerState oldState = powerState; + powerState = newState; + LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); - switch (newState) { - case GPS_ACTIVE: - case GPS_IDLE: - if (oldState == GPS_ACTIVE || oldState == GPS_IDLE) // If hardware already awake, no changes needed - break; - if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer - clearBuffer(); - powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(true); // Power (EN pin): on - setPowerPMU(true); // Power (PMU): on - writePinStandby(false); // Standby (pin): awake (not standby) - setPowerUBLOX(true); // Standby (UBLOX): awake - break; + switch (newState) { + case GPS_ACTIVE: + case GPS_IDLE: + if (oldState == GPS_ACTIVE || oldState == GPS_IDLE) // If hardware already awake, no changes needed + break; + if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer + clearBuffer(); + powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(true); // Power (EN pin): on + setPowerPMU(true); // Power (PMU): on + writePinStandby(false); // Standby (pin): awake (not standby) + setPowerUBLOX(true); // Standby (UBLOX): awake + break; - case GPS_SOFTSLEEP: - powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(true); // Power (EN pin): on - setPowerPMU(true); // Power (PMU): on - writePinStandby(true); // Standby (pin): asleep (not awake) - setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed - break; + case GPS_SOFTSLEEP: + powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(true); // Power (EN pin): on + setPowerPMU(true); // Power (PMU): on + writePinStandby(true); // Standby (pin): asleep (not awake) + setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed + break; - case GPS_HARDSLEEP: - powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(false); // Power (EN pin): off - setPowerPMU(false); // Power (PMU): off - writePinStandby(true); // Standby (pin): asleep (not awake) - setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed + case GPS_HARDSLEEP: + powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(false); // Power (EN pin): off + setPowerPMU(false); // Power (PMU): off + writePinStandby(true); // Standby (pin): asleep (not awake) + setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed #ifdef GNSS_AIROHA - digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); #endif - break; + break; - case GPS_OFF: - assert(sleepTime == 0); // This is an indefinite sleep - powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(false); // Power (EN pin): off - setPowerPMU(false); // Power (PMU): off - writePinStandby(true); // Standby (pin): asleep - setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely + case GPS_OFF: + assert(sleepTime == 0); // This is an indefinite sleep + powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(false); // Power (EN pin): off + setPowerPMU(false); // Power (PMU): off + writePinStandby(true); // Standby (pin): asleep + setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely #ifdef GNSS_AIROHA - digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); #endif - break; - } + break; + } } // Set power with EN pin, if relevant -void GPS::writePinEN(bool on) -{ - // Abort: if conflict with Canned Messages when using Wisblock(?) - if ((HW_VENDOR == meshtastic_HardwareModel_RAK4631 || HW_VENDOR == meshtastic_HardwareModel_WISMESH_TAP) && - (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1)) - return; +void GPS::writePinEN(bool on) { + // Abort: if conflict with Canned Messages when using Wisblock(?) + if ((HW_VENDOR == meshtastic_HardwareModel_RAK4631 || HW_VENDOR == meshtastic_HardwareModel_WISMESH_TAP) && + (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1)) + return; - // Write and log - enablePin->set(on); + // Write and log + enablePin->set(on); #ifdef GPS_DEBUG - LOG_DEBUG("Pin EN %s", on == HIGH ? "HI" : "LOW"); + LOG_DEBUG("Pin EN %s", on == HIGH ? "HI" : "LOW"); #endif } // Set the value of the STANDBY pin, if relevant // true for standby state, false for awake -void GPS::writePinStandby(bool standby) -{ +void GPS::writePinStandby(bool standby) { #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones // Determine the new value for the pin // Normally: active HIGH for awake #ifdef PIN_GPS_STANDBY_INVERTED - bool val = standby; + bool val = standby; #else - bool val = !standby; + bool val = !standby; #endif - // Write and log - pinMode(PIN_GPS_STANDBY, OUTPUT); - digitalWrite(PIN_GPS_STANDBY, val); + // Write and log + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, val); #ifdef GPS_DEBUG - LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); + LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); #endif #endif } // Enable / Disable GPS with PMU, if present -void GPS::setPowerPMU(bool on) -{ - // We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera, - // so treat as a standby. +void GPS::setPowerPMU(bool on) { + // We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera, + // so treat as a standby. #ifdef HAS_PMU - // Abort: if no PMU - if (!pmu_found) - return; + // Abort: if no PMU + if (!pmu_found) + return; - // Abort: if PMU not initialized - if (!PMU) - return; + // Abort: if PMU not initialized + if (!PMU) + return; - uint8_t model = PMU->getChipModel(); - if (model == XPOWERS_AXP2101) { - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - // t-beam v1.2 GNSS power channel - on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3); - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { - // t-beam-s3-core GNSS power channel - on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4); - } - } else if (model == XPOWERS_AXP192) { - // t-beam v1.1 GNSS power channel - on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3); + uint8_t model = PMU->getChipModel(); + if (model == XPOWERS_AXP2101) { + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + // t-beam v1.2 GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3); + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { + // t-beam-s3-core GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4); } + } else if (model == XPOWERS_AXP192) { + // t-beam v1.1 GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3); + } #ifdef GPS_DEBUG - LOG_DEBUG("PMU %s", on ? "on" : "off"); + LOG_DEBUG("PMU %s", on ? "on" : "off"); #endif #endif } // Set UBLOX power, if relevant -void GPS::setPowerUBLOX(bool on, uint32_t sleepMs) -{ - // Abort: if not UBLOX hardware - if (!IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) - return; +void GPS::setPowerUBLOX(bool on, uint32_t sleepMs) { + // Abort: if not UBLOX hardware + if (!IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) + return; - // If waking - if (on) { - gps->_serial_gps->write(0xFF); - clearBuffer(); // This often returns old data, so drop it + // If waking + if (on) { + gps->_serial_gps->write(0xFF); + clearBuffer(); // This often returns old data, so drop it + } + + // If putting to sleep + else { + uint8_t msglen; + + // If we're being asked to sleep indefinitely, make *sure* we're awake first, to process the new sleep command + if (sleepMs == 0) { + setPowerUBLOX(true); + delay(500); } - // If putting to sleep - else { - uint8_t msglen; + // Determine hardware version + if (gnssModel != GNSS_MODEL_UBLOX10) { + // Encode the sleep time in millis into the packet + for (int i = 0; i < 4; i++) + _message_PMREQ[0 + i] = sleepMs >> (i * 8); - // If we're being asked to sleep indefinitely, make *sure* we're awake first, to process the new sleep command - if (sleepMs == 0) { - setPowerUBLOX(true); - delay(500); - } + // Record the message length + msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), _message_PMREQ); + } else { + // Encode the sleep time in millis into the packet + for (int i = 0; i < 4; i++) + _message_PMREQ_10[4 + i] = sleepMs >> (i * 8); - // Determine hardware version - if (gnssModel != GNSS_MODEL_UBLOX10) { - // Encode the sleep time in millis into the packet - for (int i = 0; i < 4; i++) - _message_PMREQ[0 + i] = sleepMs >> (i * 8); + // Record the message length + msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), _message_PMREQ_10); + } - // Record the message length - msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), _message_PMREQ); - } else { - // Encode the sleep time in millis into the packet - for (int i = 0; i < 4; i++) - _message_PMREQ_10[4 + i] = sleepMs >> (i * 8); - - // Record the message length - msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), _message_PMREQ_10); - } - - // Send the UBX packet - gps->_serial_gps->write(gps->UBXscratch, msglen); + // Send the UBX packet + gps->_serial_gps->write(gps->UBXscratch, msglen); #ifdef GPS_DEBUG - LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs); + LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs); #endif - } + } } /// Record that we have a GPS -void GPS::setConnected() -{ - if (!hasGPS) { - hasGPS = true; - shouldPublish = true; - } +void GPS::setConnected() { + if (!hasGPS) { + hasGPS = true; + shouldPublish = true; + } } // We want a GPS lock. Wake the hardware -void GPS::up() -{ - scheduling.informSearching(); - setPowerState(GPS_ACTIVE); +void GPS::up() { + scheduling.informSearching(); + setPowerState(GPS_ACTIVE); } // We've got a GPS lock. Enter a low power state, potentially. -void GPS::down() -{ - scheduling.informGotLock(); - uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs(); - uint32_t sleepTime = scheduling.msUntilNextSearch(); - uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); +void GPS::down() { + scheduling.informGotLock(); + uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs(); + uint32_t sleepTime = scheduling.msUntilNextSearch(); + uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); - LOG_DEBUG("%us until next search", sleepTime / 1000); + LOG_DEBUG("%us until next search", sleepTime / 1000); - // If update interval less than 10 seconds, no attempt to sleep - if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0) - setPowerState(GPS_IDLE); + // If update interval less than 10 seconds, no attempt to sleep + if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0) + setPowerState(GPS_IDLE); - else { + else { // Check whether the GPS hardware is capable of GPS_SOFTSLEEP // If not, fallback to GPS_HARDSLEEP instead #ifdef PIN_GPS_STANDBY // L76B, L76K and clones have a standby pin - bool softsleepSupported = true; + bool softsleepSupported = true; #else - bool softsleepSupported = false; + bool softsleepSupported = false; #endif - // U-blox is supported via PMREQ - if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) - softsleepSupported = true; + // U-blox is supported via PMREQ + if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) + softsleepSupported = true; - if (softsleepSupported) { - // How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than - // GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M - // and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an - // improvement over a single, fixed threshold - uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22)); - LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep", hardsleepThreshold / 1000); + if (softsleepSupported) { + // How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than + // GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M + // and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an + // improvement over a single, fixed threshold + uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22)); + LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep", hardsleepThreshold / 1000); - // If update interval too short: softsleep (if supported by hardware) - if (updateInterval < hardsleepThreshold) { - setPowerState(GPS_SOFTSLEEP, sleepTime); - return; - } - } - // If update interval long enough (or softsleep unsupported): hardsleep instead - setPowerState(GPS_HARDSLEEP, sleepTime); - // Reset the fix quality to 0, since we're off. - fixQual = 0; + // If update interval too short: softsleep (if supported by hardware) + if (updateInterval < hardsleepThreshold) { + setPowerState(GPS_SOFTSLEEP, sleepTime); + return; + } } + // If update interval long enough (or softsleep unsupported): hardsleep instead + setPowerState(GPS_HARDSLEEP, sleepTime); + // Reset the fix quality to 0, since we're off. + fixQual = 0; + } } -void GPS::publishUpdate() -{ - if (shouldPublish) { - shouldPublish = false; +void GPS::publishUpdate() { + if (shouldPublish) { + shouldPublish = false; - // In debug logs, identify position by @timestamp:stage (stage 2 = publish) - LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock()); + // In debug logs, identify position by @timestamp:stage (stage 2 = publish) + LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock()); - // Notify any status instances that are observing us - const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p); - newStatus.notifyObservers(&status); - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - positionModule->handleNewPosition(); - } + // Notify any status instances that are observing us + const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p); + newStatus.notifyObservers(&status); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + positionModule->handleNewPosition(); } + } } -int32_t GPS::runOnce() -{ - if (!GPSInitFinished) { - if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - LOG_INFO("GPS set to not-present. Skip probe"); - return disable(); - } - if (!setup()) - return currentDelay; // Setup failed, re-run in two seconds +int32_t GPS::runOnce() { + if (!GPSInitFinished) { + if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + LOG_INFO("GPS set to not-present. Skip probe"); + return disable(); + } + if (!setup()) + return currentDelay; // Setup failed, re-run in two seconds - // We have now loaded our saved preferences from flash - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - return disable(); - } - GPSInitFinished = true; + // We have now loaded our saved preferences from flash + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + return disable(); + } + GPSInitFinished = true; + publishUpdate(); + } + + // ======================== GPS_ACTIVE state ======================== + // In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. + // We use the following logic to determine when to update the local position + // or time by running GPS::publishUpdate. + // Note: Local position update is asynchronous to position broadcast. We + // generally run this state every gps_update_interval seconds, and in most cases + // gps_update_interval is faster than the position broadcast interval so there's a + // fresh position ready when the device wants to broadcast one on the mesh. + // + // 1. Got a time for the first time --> set the time, don't publish. + // 2. Got a lock for the first time + // --> If gps_update_interval is <= 10s --> publishUpdate + // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) + // 3. Got a lock after turning back on + // --> If gps_update_interval is <= 10s --> publishUpdate + // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) + // 4. Hold has expired + // --> If we have a time and a location --> publishUpdate + // --> down() + // 5. Search time has expired + // --> If we have a time and a location --> publishUpdate + // --> If we had a location before but don't now --> publishUpdate + // --> down() + if (whileActive()) { + // if we have received valid NMEA claim we are connected + setConnected(); + } + + // If we're due for an update, wake the GPS + if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) + up(); + + // quality of the previous fix. We set it to 0 when we go down, so it's a way + // to check if we're getting a lock after being GPS_OFF. + uint8_t prev_fixQual = fixQual; + + if (powerState == GPS_ACTIVE) { + // if gps_update_interval is <=10s, GPS never goes off, so we treat that differently + uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); + + // 1. Got a time for the first time + bool gotTime = (getRTCQuality() >= RTCQualityGPS); + if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time + gotTime = true; + } + + // 2. Got a lock for the first time, or 3. Got a lock after turning back on + bool gotLoc = lookForLocation(); + if (gotLoc) { +#ifdef GPS_DEBUG + if (!hasValidLocation) { // declare that we have location ASAP + LOG_DEBUG("hasValidLocation RISING EDGE"); + } +#endif + if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) { + hasValidLocation = true; + shouldPublish = true; + } else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) { + hasValidLocation = true; + // Hold for up to 20secs after getting a lock to download ephemeris etc + uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS; + if (holdTime > GPS_FIX_HOLD_MAX_MS) + holdTime = GPS_FIX_HOLD_MAX_MS; + fixHoldEnds = millis() + holdTime; +#ifdef GPS_DEBUG + LOG_DEBUG("Holding for %ums after lock", holdTime); +#endif + } + } + + bool tooLong = scheduling.searchedTooLong(); + if (tooLong && !gotLoc) { + LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time"); + // we didn't get a location during this ack window, therefore declare loss of lock + if (hasValidLocation) { + p = meshtastic_Position_init_default; + hasValidLocation = false; + shouldPublish = true; +#ifdef GPS_DEBUG + LOG_DEBUG("hasValidLocation FALLING EDGE"); +#endif + } + } + + // Hold has expired , Search time has expired, we got a time only, or we never needed to hold. + bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds); + if (shouldPublish || tooLong || holdExpired) { + if (gotTime && hasValidLocation) { + shouldPublish = true; + } + if (shouldPublish) { + fixHoldEnds = 0; publishUpdate(); + } + + // There's a chance we just got a time, so keep going to see if we can get a location too + if (tooLong || holdExpired) { + down(); + } + +#ifdef GPS_DEBUG + } else if (fixHoldEnds != 0) { + LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); +#endif } + } + // ===================== end GPS_ACTIVE state ======================== - // ======================== GPS_ACTIVE state ======================== - // In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. - // We use the following logic to determine when to update the local position - // or time by running GPS::publishUpdate. - // Note: Local position update is asynchronous to position broadcast. We - // generally run this state every gps_update_interval seconds, and in most cases - // gps_update_interval is faster than the position broadcast interval so there's a - // fresh position ready when the device wants to broadcast one on the mesh. - // - // 1. Got a time for the first time --> set the time, don't publish. - // 2. Got a lock for the first time - // --> If gps_update_interval is <= 10s --> publishUpdate - // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) - // 3. Got a lock after turning back on - // --> If gps_update_interval is <= 10s --> publishUpdate - // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) - // 4. Hold has expired - // --> If we have a time and a location --> publishUpdate - // --> down() - // 5. Search time has expired - // --> If we have a time and a location --> publishUpdate - // --> If we had a location before but don't now --> publishUpdate - // --> down() - if (whileActive()) { - // if we have received valid NMEA claim we are connected - setConnected(); - } + if (config.position.fixed_position == true && hasValidLocation) + return disable(); // This should trigger when we have a fixed position, and get that first position - // If we're due for an update, wake the GPS - if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) - up(); - - // quality of the previous fix. We set it to 0 when we go down, so it's a way - // to check if we're getting a lock after being GPS_OFF. - uint8_t prev_fixQual = fixQual; - - if (powerState == GPS_ACTIVE) { - // if gps_update_interval is <=10s, GPS never goes off, so we treat that differently - uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); - - // 1. Got a time for the first time - bool gotTime = (getRTCQuality() >= RTCQualityGPS); - if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time - gotTime = true; - } - - // 2. Got a lock for the first time, or 3. Got a lock after turning back on - bool gotLoc = lookForLocation(); - if (gotLoc) { -#ifdef GPS_DEBUG - if (!hasValidLocation) { // declare that we have location ASAP - LOG_DEBUG("hasValidLocation RISING EDGE"); - } -#endif - if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) { - hasValidLocation = true; - shouldPublish = true; - } else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) { - hasValidLocation = true; - // Hold for up to 20secs after getting a lock to download ephemeris etc - uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS; - if (holdTime > GPS_FIX_HOLD_MAX_MS) - holdTime = GPS_FIX_HOLD_MAX_MS; - fixHoldEnds = millis() + holdTime; -#ifdef GPS_DEBUG - LOG_DEBUG("Holding for %ums after lock", holdTime); -#endif - } - } - - bool tooLong = scheduling.searchedTooLong(); - if (tooLong && !gotLoc) { - LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time"); - // we didn't get a location during this ack window, therefore declare loss of lock - if (hasValidLocation) { - p = meshtastic_Position_init_default; - hasValidLocation = false; - shouldPublish = true; -#ifdef GPS_DEBUG - LOG_DEBUG("hasValidLocation FALLING EDGE"); -#endif - } - } - - // Hold has expired , Search time has expired, we got a time only, or we never needed to hold. - bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds); - if (shouldPublish || tooLong || holdExpired) { - if (gotTime && hasValidLocation) { - shouldPublish = true; - } - if (shouldPublish) { - fixHoldEnds = 0; - publishUpdate(); - } - - // There's a chance we just got a time, so keep going to see if we can get a location too - if (tooLong || holdExpired) { - down(); - } - -#ifdef GPS_DEBUG - } else if (fixHoldEnds != 0) { - LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); -#endif - } - } - // ===================== end GPS_ACTIVE state ======================== - - if (config.position.fixed_position == true && hasValidLocation) - return disable(); // This should trigger when we have a fixed position, and get that first position - - // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms - // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake. - return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; + // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms + // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake. + return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; } // clear the GPS rx/tx buffer as quickly as possible -void GPS::clearBuffer() -{ +void GPS::clearBuffer() { #ifdef ARCH_ESP32 - _serial_gps->flush(false); + _serial_gps->flush(false); #else - int x = _serial_gps->available(); - while (x--) - _serial_gps->read(); + int x = _serial_gps->available(); + while (x--) + _serial_gps->read(); #endif } /// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs -int GPS::prepareDeepSleep(void *unused) -{ - LOG_INFO("GPS deep sleep!"); - disable(); - return 0; +int GPS::prepareDeepSleep(void *unused) { + LOG_INFO("GPS deep sleep!"); + disable(); + return 0; } static const char *PROBE_MESSAGE = "Trying %s (%s)..."; static const char *DETECTED_MESSAGE = "%s detected"; -#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \ - do { \ - LOG_DEBUG(PROBE_MESSAGE, TOWRITE, CHIP); \ - clearBuffer(); \ - _serial_gps->write(TOWRITE "\r\n"); \ - if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \ - LOG_INFO(DETECTED_MESSAGE, CHIP); \ - return DRIVER; \ - } \ - } while (0) +#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \ + do { \ + LOG_DEBUG(PROBE_MESSAGE, TOWRITE, CHIP); \ + clearBuffer(); \ + _serial_gps->write(TOWRITE "\r\n"); \ + if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \ + LOG_INFO(DETECTED_MESSAGE, CHIP); \ + return DRIVER; \ + } \ + } while (0) -#define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \ - do { \ - LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ - clearBuffer(); \ - _serial_gps->write(COMMAND "\r\n"); \ - GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ - if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ - return detectedDriver; \ - } \ - } while (0) +#define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \ + do { \ + LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ + clearBuffer(); \ + _serial_gps->write(COMMAND "\r\n"); \ + GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ + if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ + return detectedDriver; \ + } \ + } while (0) -GnssModel_t GPS::probe(int serialSpeed) -{ - uint8_t buffer[768] = {0}; +GnssModel_t GPS::probe(int serialSpeed) { + uint8_t buffer[768] = {0}; - switch (currentStep) { - case 0: { + switch (currentStep) { + case 0: { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) - _serial_gps->end(); - _serial_gps->begin(serialSpeed); + _serial_gps->end(); + _serial_gps->begin(serialSpeed); #elif defined(ARCH_RP2040) - _serial_gps->end(); - _serial_gps->setFIFOSize(256); - _serial_gps->begin(serialSpeed); + _serial_gps->end(); + _serial_gps->setFIFOSize(256); + _serial_gps->begin(serialSpeed); #else - if (_serial_gps->baudRate() != serialSpeed) { - LOG_DEBUG("Set GPS Baud to %i", serialSpeed); - _serial_gps->updateBaudRate(serialSpeed); - } + if (_serial_gps->baudRate() != serialSpeed) { + LOG_DEBUG("Set GPS Baud to %i", serialSpeed); + _serial_gps->updateBaudRate(serialSpeed); + } #endif - memset(&ublox_info, 0, sizeof(ublox_info)); - delay(100); + memset(&ublox_info, 0, sizeof(ublox_info)); + delay(100); #if defined(PIN_GPS_RESET) && PIN_GPS_RESET != -1 - digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms - delay(10); - digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); + digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms + delay(10); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); - // attempt to detect the chip based on boot messages - std::vector passive_detect = { - {"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, - {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, - {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}, - {"UC6580", "UC6580", GNSS_MODEL_UC6580}, - // as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now. - /*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/}; - GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed); - if (detectedDriver != GNSS_MODEL_UNKNOWN) { - return detectedDriver; - } + // attempt to detect the chip based on boot messages + std::vector passive_detect = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, + {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, + {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}, + {"UC6580", "UC6580", GNSS_MODEL_UC6580}, + // as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now. + /*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/}; + GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed); + if (detectedDriver != GNSS_MODEL_UNKNOWN) { + return detectedDriver; + } #endif - // Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices) - _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); - delay(20); - // Close NMEA sequences on Ublox - _serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"); - _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); - _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); - delay(20); - // Close NMEA sequences on CM121 - _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); - _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); - _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); - currentDelay = 20; - currentStep = 1; - return GNSS_MODEL_UNKNOWN; - } - case 1: { - - // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 - std::vector unicore = { - {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; - PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); - currentDelay = 20; - currentStep = 2; - return GNSS_MODEL_UNKNOWN; - } - case 2: { - std::vector atgm = { - {"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H}, - /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */ - {"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}}; - PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500); - currentDelay = 20; - currentStep = 3; - return GNSS_MODEL_UNKNOWN; - } - case 3: { - /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */ - _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume - _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume - _serial_gps->write("$PAIR513*3D\r\n"); // save configuration - std::vector airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, - {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, - {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}}; - PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000); - currentDelay = 20; - currentStep = 4; - return GNSS_MODEL_UNKNOWN; - } - case 4: { - PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500); - PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500); - currentDelay = 20; - currentStep = 5; - return GNSS_MODEL_UNKNOWN; - } - case 5: { - - // Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms - _serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); - delay(20); - std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, - {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, - {"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B}, - {"L80", "_3339_", GNSS_MODEL_MTK_L76B}}; - - PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); - currentDelay = 20; - currentStep = 6; - return GNSS_MODEL_UNKNOWN; - } - case 6: { - uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; - UBXChecksum(cfg_rate, sizeof(cfg_rate)); - clearBuffer(); - _serial_gps->write(cfg_rate, sizeof(cfg_rate)); - // Check that the returned response class and message ID are correct - GPS_RESPONSE response = getACK(0x06, 0x08, 750); - if (response == GNSS_RESPONSE_NONE) { - LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); - currentDelay = 2000; - currentStep = 0; - return GNSS_MODEL_UNKNOWN; - } else if (response == GNSS_RESPONSE_FRAME_ERRORS) { - LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed); - } - - memset(buffer, 0, sizeof(buffer)); - uint8_t _message_MONVER[8] = { - 0xB5, 0x62, // Sync message for UBX protocol - 0x0A, 0x04, // Message class and ID (UBX-MON-VER) - 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload) - 0x00, 0x00 // Checksum - }; - // Get Ublox gnss module hardware and software info - UBXChecksum(_message_MONVER, sizeof(_message_MONVER)); - clearBuffer(); - _serial_gps->write(_message_MONVER, sizeof(_message_MONVER)); - - uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200); - if (len) { - uint16_t position = 0; - for (int i = 0; i < 30; i++) { - ublox_info.swVersion[i] = buffer[position]; - position++; - } - for (int i = 0; i < 10; i++) { - ublox_info.hwVersion[i] = buffer[position]; - position++; - } - - while (len >= position + 30) { - for (int i = 0; i < 30; i++) { - ublox_info.extension[ublox_info.extensionNo][i] = buffer[position]; - position++; - } - ublox_info.extensionNo++; - if (ublox_info.extensionNo > 9) - break; - } - - LOG_DEBUG("Module Info : "); - LOG_DEBUG("Soft version: %s", ublox_info.swVersion); - LOG_DEBUG("Hard version: %s", ublox_info.hwVersion); - LOG_DEBUG("Extensions:%d", ublox_info.extensionNo); - for (int i = 0; i < ublox_info.extensionNo; i++) { - LOG_DEBUG(" %s", ublox_info.extension[i]); - } - - memset(buffer, 0, sizeof(buffer)); - - // tips: extensionNo field is 0 on some 6M GNSS modules - for (int i = 0; i < ublox_info.extensionNo; ++i) { - if (!strncmp(ublox_info.extension[i], "MOD=", 4)) { - strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer)); - } else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) { - char *ptr = nullptr; - memset(buffer, 0, sizeof(buffer)); - strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer)); - LOG_DEBUG("Protocol Version:%s", (char *)buffer); - if (strlen((char *)buffer)) { - ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10); - LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version); - } else { - ublox_info.protocol_version = 0; - } - } - } - if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6"); - return GNSS_MODEL_UBLOX6; - } else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7"); - return GNSS_MODEL_UBLOX7; - } else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8"); - return GNSS_MODEL_UBLOX8; - } else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9"); - return GNSS_MODEL_UBLOX9; - } else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10"); - return GNSS_MODEL_UBLOX10; - } - } - } - } - LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); - currentDelay = 2000; - currentStep = 0; + // Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices) + _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); + delay(20); + // Close NMEA sequences on Ublox + _serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"); + _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); + _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); + delay(20); + // Close NMEA sequences on CM121 + _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); + _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); + _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); + currentDelay = 20; + currentStep = 1; return GNSS_MODEL_UNKNOWN; -} + } + case 1: { -GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) -{ - // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline - // Higher baud rates get proportionally larger buffers to handle more data - int bufferSize = (serialSpeed * 256) / 9600; - // Clamp buffer size between reasonable limits - if (bufferSize < 128) - bufferSize = 128; - if (bufferSize > 2048) - bufferSize = 2048; + // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 + std::vector unicore = { + {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; + PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); + currentDelay = 20; + currentStep = 2; + return GNSS_MODEL_UNKNOWN; + } + case 2: { + std::vector atgm = {{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H}, + /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), + -71-0(GPS+BDS+GLONASS)) based on AT6558 */ + {"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}}; + PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500); + currentDelay = 20; + currentStep = 3; + return GNSS_MODEL_UNKNOWN; + } + case 3: { + /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */ + _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume + _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume + _serial_gps->write("$PAIR513*3D\r\n"); // save configuration + std::vector airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, + {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, + {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}}; + PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000); + currentDelay = 20; + currentStep = 4; + return GNSS_MODEL_UNKNOWN; + } + case 4: { + PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500); + PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500); + currentDelay = 20; + currentStep = 5; + return GNSS_MODEL_UNKNOWN; + } + case 5: { - char *response = new char[bufferSize](); // Dynamically allocate based on baud rate - uint16_t responseLen = 0; - unsigned long start = millis(); - while (millis() - start < timeout) { - if (_serial_gps->available()) { - char c = _serial_gps->read(); + // Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms + _serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); + delay(20); + std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, + {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, + {"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B}, + {"L80", "_3339_", GNSS_MODEL_MTK_L76B}}; - // Add char to buffer if there's space - if (responseLen < bufferSize - 1) { - response[responseLen++] = c; - response[responseLen] = '\0'; - } - - if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { - // check if we can see our chips - for (const auto &chipInfo : responseMap) { - if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { -#ifdef GPS_DEBUG - LOG_DEBUG(response); -#endif - LOG_INFO("%s detected", chipInfo.chipName.c_str()); - delete[] response; // Cleanup before return - return chipInfo.driver; - } - } - } - if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { -#ifdef GPS_DEBUG - LOG_DEBUG(response); -#endif - // Reset the response buffer for the next potential message - responseLen = 0; - response[0] = '\0'; - } - } + PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); + currentDelay = 20; + currentStep = 6; + return GNSS_MODEL_UNKNOWN; + } + case 6: { + uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; + UBXChecksum(cfg_rate, sizeof(cfg_rate)); + clearBuffer(); + _serial_gps->write(cfg_rate, sizeof(cfg_rate)); + // Check that the returned response class and message ID are correct + GPS_RESPONSE response = getACK(0x06, 0x08, 750); + if (response == GNSS_RESPONSE_NONE) { + LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); + currentDelay = 2000; + currentStep = 0; + return GNSS_MODEL_UNKNOWN; + } else if (response == GNSS_RESPONSE_FRAME_ERRORS) { + LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed); } -#ifdef GPS_DEBUG - LOG_DEBUG(response); -#endif - delete[] response; // Cleanup before return - return GNSS_MODEL_UNKNOWN; // Return unknown on timeout + + memset(buffer, 0, sizeof(buffer)); + uint8_t _message_MONVER[8] = { + 0xB5, 0x62, // Sync message for UBX protocol + 0x0A, 0x04, // Message class and ID (UBX-MON-VER) + 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload) + 0x00, 0x00 // Checksum + }; + // Get Ublox gnss module hardware and software info + UBXChecksum(_message_MONVER, sizeof(_message_MONVER)); + clearBuffer(); + _serial_gps->write(_message_MONVER, sizeof(_message_MONVER)); + + uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200); + if (len) { + uint16_t position = 0; + for (int i = 0; i < 30; i++) { + ublox_info.swVersion[i] = buffer[position]; + position++; + } + for (int i = 0; i < 10; i++) { + ublox_info.hwVersion[i] = buffer[position]; + position++; + } + + while (len >= position + 30) { + for (int i = 0; i < 30; i++) { + ublox_info.extension[ublox_info.extensionNo][i] = buffer[position]; + position++; + } + ublox_info.extensionNo++; + if (ublox_info.extensionNo > 9) + break; + } + + LOG_DEBUG("Module Info : "); + LOG_DEBUG("Soft version: %s", ublox_info.swVersion); + LOG_DEBUG("Hard version: %s", ublox_info.hwVersion); + LOG_DEBUG("Extensions:%d", ublox_info.extensionNo); + for (int i = 0; i < ublox_info.extensionNo; i++) { + LOG_DEBUG(" %s", ublox_info.extension[i]); + } + + memset(buffer, 0, sizeof(buffer)); + + // tips: extensionNo field is 0 on some 6M GNSS modules + for (int i = 0; i < ublox_info.extensionNo; ++i) { + if (!strncmp(ublox_info.extension[i], "MOD=", 4)) { + strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer)); + } else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) { + char *ptr = nullptr; + memset(buffer, 0, sizeof(buffer)); + strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer)); + LOG_DEBUG("Protocol Version:%s", (char *)buffer); + if (strlen((char *)buffer)) { + ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10); + LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version); + } else { + ublox_info.protocol_version = 0; + } + } + } + if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6"); + return GNSS_MODEL_UBLOX6; + } else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7"); + return GNSS_MODEL_UBLOX7; + } else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8"); + return GNSS_MODEL_UBLOX8; + } else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9"); + return GNSS_MODEL_UBLOX9; + } else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10"); + return GNSS_MODEL_UBLOX10; + } + } + } + } + LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); + currentDelay = 2000; + currentStep = 0; + return GNSS_MODEL_UNKNOWN; } -GPS *GPS::createGps() -{ - int8_t _rx_gpio = config.position.rx_gpio; - int8_t _tx_gpio = config.position.tx_gpio; - int8_t _en_gpio = config.position.gps_en_gpio; +GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { + // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline + // Higher baud rates get proportionally larger buffers to handle more data + int bufferSize = (serialSpeed * 256) / 9600; + // Clamp buffer size between reasonable limits + if (bufferSize < 128) + bufferSize = 128; + if (bufferSize > 2048) + bufferSize = 2048; + + char *response = new char[bufferSize](); // Dynamically allocate based on baud rate + uint16_t responseLen = 0; + unsigned long start = millis(); + while (millis() - start < timeout) { + if (_serial_gps->available()) { + char c = _serial_gps->read(); + + // Add char to buffer if there's space + if (responseLen < bufferSize - 1) { + response[responseLen++] = c; + response[responseLen] = '\0'; + } + + if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { + // check if we can see our chips + for (const auto &chipInfo : responseMap) { + if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { +#ifdef GPS_DEBUG + LOG_DEBUG(response); +#endif + LOG_INFO("%s detected", chipInfo.chipName.c_str()); + delete[] response; // Cleanup before return + return chipInfo.driver; + } + } + } + if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { +#ifdef GPS_DEBUG + LOG_DEBUG(response); +#endif + // Reset the response buffer for the next potential message + responseLen = 0; + response[0] = '\0'; + } + } + } +#ifdef GPS_DEBUG + LOG_DEBUG(response); +#endif + delete[] response; // Cleanup before return + return GNSS_MODEL_UNKNOWN; // Return unknown on timeout +} + +GPS *GPS::createGps() { + int8_t _rx_gpio = config.position.rx_gpio; + int8_t _tx_gpio = config.position.tx_gpio; + int8_t _en_gpio = config.position.gps_en_gpio; #if defined(GPS_RX_PIN) - if (!_rx_gpio) - _rx_gpio = GPS_RX_PIN; + if (!_rx_gpio) + _rx_gpio = GPS_RX_PIN; #endif #if defined(GPS_TX_PIN) - if (!_tx_gpio) - _tx_gpio = GPS_TX_PIN; + if (!_tx_gpio) + _tx_gpio = GPS_TX_PIN; #endif #if defined(PIN_GPS_EN) - if (!_en_gpio) - _en_gpio = PIN_GPS_EN; + if (!_en_gpio) + _en_gpio = PIN_GPS_EN; #endif #ifdef ARCH_PORTDUINO - if (!portduino_config.has_gps) - return nullptr; + if (!portduino_config.has_gps) + return nullptr; #endif - if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all - return nullptr; + if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all + return nullptr; - GPS *new_gps = new GPS; - new_gps->rx_gpio = _rx_gpio; - new_gps->tx_gpio = _tx_gpio; + GPS *new_gps = new GPS; + new_gps->rx_gpio = _rx_gpio; + new_gps->tx_gpio = _tx_gpio; - GpioVirtPin *virtPin = new GpioVirtPin(); - new_gps->enablePin = virtPin; // Always at least populate a virtual pin - if (_en_gpio) { - GpioPin *p = new GpioHwPin(_en_gpio); + GpioVirtPin *virtPin = new GpioVirtPin(); + new_gps->enablePin = virtPin; // Always at least populate a virtual pin + if (_en_gpio) { + GpioPin *p = new GpioHwPin(_en_gpio); - if (!GPS_EN_ACTIVE) { // Need to invert the pin before hardware - new GpioNotTransformer( - virtPin, - p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio - } else { - new GpioUnaryTransformer( - virtPin, - p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio - } + if (!GPS_EN_ACTIVE) { // Need to invert the pin before hardware + new GpioNotTransformer(virtPin, + p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio + } else { + new GpioUnaryTransformer(virtPin, + p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio } + } #ifdef PIN_GPS_PPS - // pulse per second - pinMode(PIN_GPS_PPS, INPUT); + // pulse per second + pinMode(PIN_GPS_PPS, INPUT); #endif #ifdef PIN_GPS_SWITCH - // toggle GPS via external GPIO switch - pinMode(PIN_GPS_SWITCH, INPUT); - gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); + // toggle GPS via external GPIO switch + pinMode(PIN_GPS_SWITCH, INPUT); + gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); #endif // Currently disabled per issue #525 (TinyGPS++ crash bug) // when fixed upstream, can be un-disabled to enable 3D FixType and PDOP #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - // see NMEAGPS.h - gsafixtype.begin(reader, NMEA_MSG_GXGSA, 2); - gsapdop.begin(reader, NMEA_MSG_GXGSA, 15); - LOG_DEBUG("Use " NMEA_MSG_GXGSA " for 3DFIX and PDOP"); + // see NMEAGPS.h + gsafixtype.begin(reader, NMEA_MSG_GXGSA, 2); + gsapdop.begin(reader, NMEA_MSG_GXGSA, 15); + LOG_DEBUG("Use " NMEA_MSG_GXGSA " for 3DFIX and PDOP"); #endif - // Make sure the GPS is awake before performing any init. - new_gps->up(); + // Make sure the GPS is awake before performing any init. + new_gps->up(); #ifdef PIN_GPS_RESET - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); #endif - if (_serial_gps) { + if (_serial_gps) { #ifdef ARCH_ESP32 - // In esp32 framework, setRxBufferSize needs to be initialized before Serial - _serial_gps->setRxBufferSize(SERIAL_BUFFER_SIZE); // the default is 256 + // In esp32 framework, setRxBufferSize needs to be initialized before Serial + _serial_gps->setRxBufferSize(SERIAL_BUFFER_SIZE); // the default is 256 #endif - LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio); - LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio); + LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio); + LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio); // ESP32 has a special set of parameters vs other arduino ports #if defined(ARCH_ESP32) - _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, new_gps->rx_gpio, new_gps->tx_gpio); + _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, new_gps->rx_gpio, new_gps->tx_gpio); #elif defined(ARCH_RP2040) - _serial_gps->setPinout(new_gps->tx_gpio, new_gps->rx_gpio); - _serial_gps->setFIFOSize(256); - _serial_gps->begin(GPS_BAUDRATE); + _serial_gps->setPinout(new_gps->tx_gpio, new_gps->rx_gpio); + _serial_gps->setFIFOSize(256); + _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_NRF52) - _serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio); - _serial_gps->begin(GPS_BAUDRATE); + _serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio); + _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_STM32WL) - _serial_gps->setTx(new_gps->tx_gpio); - _serial_gps->setRx(new_gps->rx_gpio); - _serial_gps->begin(GPS_BAUDRATE); + _serial_gps->setTx(new_gps->tx_gpio); + _serial_gps->setRx(new_gps->rx_gpio); + _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_PORTDUINO) - // Portduino can't set the GPS pins directly. - _serial_gps->begin(GPS_BAUDRATE); + // Portduino can't set the GPS pins directly. + _serial_gps->begin(GPS_BAUDRATE); #else #error Unsupported architecture! #endif - } - return new_gps; + } + return new_gps; } -static int32_t toDegInt(RawDegrees d) -{ - int32_t degMult = 10000000; // 1e7 - int32_t r = d.deg * degMult + d.billionths / 100; - if (d.negative) - r *= -1; - return r; +static int32_t toDegInt(RawDegrees d) { + int32_t degMult = 10000000; // 1e7 + int32_t r = d.deg * degMult + d.billionths / 100; + if (d.negative) + r *= -1; + return r; } /** @@ -1643,35 +1609,33 @@ static int32_t toDegInt(RawDegrees d) * * @return true if we've set a new time */ -bool GPS::lookForTime() -{ - auto ti = reader.time; - auto d = reader.date; - if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed - /* Convert to unix time -The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, -1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). +bool GPS::lookForTime() { + auto ti = reader.time; + auto d = reader.date; + if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed + /* Convert to unix time +The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January +1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). */ - struct tm t; - t.tm_sec = ti.second() + round(ti.age() / 1000); - t.tm_min = ti.minute(); - t.tm_hour = ti.hour(); - t.tm_mday = d.day(); - t.tm_mon = d.month() - 1; - t.tm_year = d.year() - 1900; - t.tm_isdst = false; - if (t.tm_mon > -1) { - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { - LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, - t.tm_min, t.tm_sec, ti.age()); - return true; - } else { - return false; - } - } else - return false; - } else + struct tm t; + t.tm_sec = ti.second() + round(ti.age() / 1000); + t.tm_min = ti.minute(); + t.tm_hour = ti.hour(); + t.tm_mday = d.day(); + t.tm_mon = d.month() - 1; + t.tm_year = d.year() - 1900; + t.tm_isdst = false; + if (t.tm_mon > -1) { + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); + return true; + } else { return false; + } + } else + return false; + } else + return false; } /** @@ -1680,238 +1644,227 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s * * @return true if we've acquired a new location */ -bool GPS::lookForLocation() -{ - // By default, TinyGPS++ does not parse GPGSA lines, which give us - // the 2D/3D fixType (see NMEAGPS.h) - // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) - fixQual = reader.fixQuality(); +bool GPS::lookForLocation() { + // By default, TinyGPS++ does not parse GPGSA lines, which give us + // the 2D/3D fixType (see NMEAGPS.h) + // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) + fixQual = reader.fixQuality(); #ifndef TINYGPS_OPTION_NO_STATISTICS - if (reader.failedChecksum() > lastChecksumFailCount) { + if (reader.failedChecksum() > lastChecksumFailCount) { // In a GPS_DEBUG build we want to log all of these. In production, we only care if there are many of them. #ifndef GPS_DEBUG - if (reader.failedChecksum() > 4) + if (reader.failedChecksum() > 4) #endif - LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount, - reader.failedChecksum()); - lastChecksumFailCount = reader.failedChecksum(); - } + LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount, reader.failedChecksum()); + lastChecksumFailCount = reader.failedChecksum(); + } #endif #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - fixType = atoi(gsafixtype.value()); // will set to zero if no data + fixType = atoi(gsafixtype.value()); // will set to zero if no data #endif - // check if GPS has an acceptable lock - if (!hasLock()) - return false; + // check if GPS has an acceptable lock + if (!hasLock()) + return false; #ifdef GPS_DEBUG - LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(), + LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(), #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - gsafixtype.age(), + gsafixtype.age(), #else - 0, + 0, #endif - reader.date.age(), reader.time.age()); + reader.date.age(), reader.time.age()); #endif // GPS_DEBUG - // Is this a new point or are we re-reading the previous one? - if (!reader.location.isUpdated() && !reader.altitude.isUpdated()) - return false; - - // check if a complete GPS solution set is available for reading - // tinyGPSDatum::age() also includes isValid() test - // FIXME - if (!((reader.location.age() < GPS_SOL_EXPIRY_MS) && -#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - (gsafixtype.age() < GPS_SOL_EXPIRY_MS) && -#endif - (reader.time.age() < GPS_SOL_EXPIRY_MS) && (reader.date.age() < GPS_SOL_EXPIRY_MS))) { - LOG_WARN("SOME data is TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age()); - return false; - } - - // We know the solution is fresh and valid, so just read the data - auto loc = reader.location.value(); - - // Bail out EARLY to avoid overwriting previous good data (like #857) - if (toDegInt(loc.lat) > 900000000) { -#ifdef GPS_DEBUG - LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat)); -#endif - return false; - } - if (toDegInt(loc.lng) > 1800000000) { -#ifdef GPS_DEBUG - LOG_DEBUG("Bail out EARLY on LNG %i", toDegInt(loc.lng)); -#endif - return false; - } - - p.location_source = meshtastic_Position_LocSource_LOC_INTERNAL; - - // Dilution of precision (an accuracy metric) is reported in 10^2 units, so we need to scale down when we use it -#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - p.HDOP = reader.hdop.value(); - p.PDOP = TinyGPSPlus::parseDecimal(gsapdop.value()); -#else - // FIXME! naive PDOP emulation (assumes VDOP==HDOP) - // correct formula is PDOP = SQRT(HDOP^2 + VDOP^2) - p.HDOP = reader.hdop.value(); - p.PDOP = 1.41 * reader.hdop.value(); -#endif - - // Discard incomplete or erroneous readings - if (reader.hdop.value() == 0) { - LOG_WARN("BOGUS hdop.value() REJECTED: %d", reader.hdop.value()); - return false; - } - - p.latitude_i = toDegInt(loc.lat); - p.longitude_i = toDegInt(loc.lng); - - p.altitude_geoidal_separation = reader.geoidHeight.meters(); - p.altitude_hae = reader.altitude.meters() + p.altitude_geoidal_separation; - p.altitude = reader.altitude.meters(); - - p.fix_quality = fixQual; -#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - p.fix_type = fixType; -#endif - - // positional timestamp - struct tm t; - t.tm_sec = reader.time.second(); - t.tm_min = reader.time.minute(); - t.tm_hour = reader.time.hour(); - t.tm_mday = reader.date.day(); - t.tm_mon = reader.date.month() - 1; - t.tm_year = reader.date.year() - 1900; - t.tm_isdst = false; - p.timestamp = gm_mktime(&t); - - // Nice to have, if available - if (reader.satellites.isUpdated()) { - p.sats_in_view = reader.satellites.value(); - } - - if (reader.course.isUpdated() && reader.course.isValid()) { - if (reader.course.value() < 36000) { // sanity check - p.ground_track = - reader.course.value() * 1e3; // Scale the heading (in degrees * 10^-2) to match the expected degrees * 10^-5 - } else { - LOG_WARN("BOGUS course.value() REJECTED: %d", reader.course.value()); - } - } - - if (reader.speed.isUpdated() && reader.speed.isValid()) { - p.ground_speed = reader.speed.kmph(); - } - - return true; -} - -bool GPS::hasLock() -{ - // Using GPGGA fix quality indicator - if (fixQual >= 1 && fixQual <= 5) { -#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - // Use GPGSA fix type 2D/3D (better) if available - if (fixType == 3 || fixType == 0) // zero means "no data received" -#endif - return true; - } - + // Is this a new point or are we re-reading the previous one? + if (!reader.location.isUpdated() && !reader.altitude.isUpdated()) return false; -} -bool GPS::hasFlow() -{ - return reader.passedChecksum() > 0; -} - -bool GPS::whileActive() -{ - unsigned int charsInBuf = 0; - bool isValid = false; -#ifdef GPS_DEBUG - std::string debugmsg = ""; + // check if a complete GPS solution set is available for reading + // tinyGPSDatum::age() also includes isValid() test + // FIXME + if (!((reader.location.age() < GPS_SOL_EXPIRY_MS) && +#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS + (gsafixtype.age() < GPS_SOL_EXPIRY_MS) && #endif - if (powerState != GPS_ACTIVE) { - clearBuffer(); - return false; + (reader.time.age() < GPS_SOL_EXPIRY_MS) && (reader.date.age() < GPS_SOL_EXPIRY_MS))) { + LOG_WARN("SOME data is TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age()); + return false; + } + + // We know the solution is fresh and valid, so just read the data + auto loc = reader.location.value(); + + // Bail out EARLY to avoid overwriting previous good data (like #857) + if (toDegInt(loc.lat) > 900000000) { +#ifdef GPS_DEBUG + LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat)); +#endif + return false; + } + if (toDegInt(loc.lng) > 1800000000) { +#ifdef GPS_DEBUG + LOG_DEBUG("Bail out EARLY on LNG %i", toDegInt(loc.lng)); +#endif + return false; + } + + p.location_source = meshtastic_Position_LocSource_LOC_INTERNAL; + + // Dilution of precision (an accuracy metric) is reported in 10^2 units, so we need to scale down when we use it +#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS + p.HDOP = reader.hdop.value(); + p.PDOP = TinyGPSPlus::parseDecimal(gsapdop.value()); +#else + // FIXME! naive PDOP emulation (assumes VDOP==HDOP) + // correct formula is PDOP = SQRT(HDOP^2 + VDOP^2) + p.HDOP = reader.hdop.value(); + p.PDOP = 1.41 * reader.hdop.value(); +#endif + + // Discard incomplete or erroneous readings + if (reader.hdop.value() == 0) { + LOG_WARN("BOGUS hdop.value() REJECTED: %d", reader.hdop.value()); + return false; + } + + p.latitude_i = toDegInt(loc.lat); + p.longitude_i = toDegInt(loc.lng); + + p.altitude_geoidal_separation = reader.geoidHeight.meters(); + p.altitude_hae = reader.altitude.meters() + p.altitude_geoidal_separation; + p.altitude = reader.altitude.meters(); + + p.fix_quality = fixQual; +#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS + p.fix_type = fixType; +#endif + + // positional timestamp + struct tm t; + t.tm_sec = reader.time.second(); + t.tm_min = reader.time.minute(); + t.tm_hour = reader.time.hour(); + t.tm_mday = reader.date.day(); + t.tm_mon = reader.date.month() - 1; + t.tm_year = reader.date.year() - 1900; + t.tm_isdst = false; + p.timestamp = gm_mktime(&t); + + // Nice to have, if available + if (reader.satellites.isUpdated()) { + p.sats_in_view = reader.satellites.value(); + } + + if (reader.course.isUpdated() && reader.course.isValid()) { + if (reader.course.value() < 36000) { // sanity check + p.ground_track = reader.course.value() * 1e3; // Scale the heading (in degrees * 10^-2) to match the expected degrees * 10^-5 + } else { + LOG_WARN("BOGUS course.value() REJECTED: %d", reader.course.value()); } + } + + if (reader.speed.isUpdated() && reader.speed.isValid()) { + p.ground_speed = reader.speed.kmph(); + } + + return true; +} + +bool GPS::hasLock() { + // Using GPGGA fix quality indicator + if (fixQual >= 1 && fixQual <= 5) { +#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS + // Use GPGSA fix type 2D/3D (better) if available + if (fixType == 3 || fixType == 0) // zero means "no data received" +#endif + return true; + } + + return false; +} + +bool GPS::hasFlow() { return reader.passedChecksum() > 0; } + +bool GPS::whileActive() { + unsigned int charsInBuf = 0; + bool isValid = false; +#ifdef GPS_DEBUG + std::string debugmsg = ""; +#endif + if (powerState != GPS_ACTIVE) { + clearBuffer(); + return false; + } #ifdef SERIAL_BUFFER_SIZE - if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) { - LOG_WARN("GPS Buffer full with %u bytes waiting. Flush to avoid corruption", _serial_gps->available()); - clearBuffer(); - } + if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) { + LOG_WARN("GPS Buffer full with %u bytes waiting. Flush to avoid corruption", _serial_gps->available()); + clearBuffer(); + } #endif - // First consume any chars that have piled up at the receiver - while (_serial_gps->available() > 0) { - int c = _serial_gps->read(); - UBXscratch[charsInBuf] = c; + // First consume any chars that have piled up at the receiver + while (_serial_gps->available() > 0) { + int c = _serial_gps->read(); + UBXscratch[charsInBuf] = c; #ifdef GPS_DEBUG - debugmsg += vformat("%c", (c >= 32 && c <= 126) ? c : '.'); + debugmsg += vformat("%c", (c >= 32 && c <= 126) ? c : '.'); #endif - isValid |= reader.encode(c); - if (charsInBuf > sizeof(UBXscratch) - 10 || c == '\r') { - if (strnstr((char *)UBXscratch, "$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50", charsInBuf)) { - rebootsSeen++; - } - charsInBuf = 0; - } else { - charsInBuf++; - } + isValid |= reader.encode(c); + if (charsInBuf > sizeof(UBXscratch) - 10 || c == '\r') { + if (strnstr((char *)UBXscratch, "$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50", charsInBuf)) { + rebootsSeen++; + } + charsInBuf = 0; + } else { + charsInBuf++; } + } #ifdef GPS_DEBUG - if (debugmsg != "") { - LOG_DEBUG(debugmsg.c_str()); - } + if (debugmsg != "") { + LOG_DEBUG(debugmsg.c_str()); + } #endif - return isValid; + return isValid; } -void GPS::enable() -{ - // Clear the old scheduling info (reset the lock-time prediction) - scheduling.reset(); +void GPS::enable() { + // Clear the old scheduling info (reset the lock-time prediction) + scheduling.reset(); - enabled = true; - setInterval(GPS_THREAD_INTERVAL); + enabled = true; + setInterval(GPS_THREAD_INTERVAL); - scheduling.informSearching(); - setPowerState(GPS_ACTIVE); + scheduling.informSearching(); + setPowerState(GPS_ACTIVE); } -int32_t GPS::disable() -{ - enabled = false; - setInterval(INT32_MAX); - setPowerState(GPS_OFF); +int32_t GPS::disable() { + enabled = false; + setInterval(INT32_MAX); + setPowerState(GPS_OFF); - return INT32_MAX; + return INT32_MAX; } -void GPS::toggleGpsMode() -{ - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - LOG_INFO("User toggled GpsMode. Now DISABLED"); - playGPSDisableBeep(); +void GPS::toggleGpsMode() { + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + LOG_INFO("User toggled GpsMode. Now DISABLED"); + playGPSDisableBeep(); #ifdef GNSS_AIROHA - if (powerState == GPS_ACTIVE) { - LOG_DEBUG("User power Off GPS"); - digitalWrite(PIN_GPS_EN, LOW); - } -#endif - disable(); - } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - LOG_INFO("User toggled GpsMode. Now ENABLED"); - playGPSEnableBeep(); - enable(); + if (powerState == GPS_ACTIVE) { + LOG_DEBUG("User power Off GPS"); + digitalWrite(PIN_GPS_EN, LOW); } +#endif + disable(); + } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + LOG_INFO("User toggled GpsMode. Now ENABLED"); + playGPSEnableBeep(); + enable(); + } } #endif // Exclude GPS diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 59cee7113..32c29f813 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -20,235 +20,234 @@ static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; typedef enum { - GNSS_MODEL_ATGM336H, - GNSS_MODEL_MTK, - GNSS_MODEL_UBLOX6, - GNSS_MODEL_UBLOX7, - GNSS_MODEL_UBLOX8, - GNSS_MODEL_UBLOX9, - GNSS_MODEL_UBLOX10, - GNSS_MODEL_UC6580, - GNSS_MODEL_UNKNOWN, - GNSS_MODEL_MTK_L76B, - GNSS_MODEL_MTK_PA1010D, - GNSS_MODEL_MTK_PA1616S, - GNSS_MODEL_AG3335, - GNSS_MODEL_AG3352, - GNSS_MODEL_LS20031, - GNSS_MODEL_CM121 + GNSS_MODEL_ATGM336H, + GNSS_MODEL_MTK, + GNSS_MODEL_UBLOX6, + GNSS_MODEL_UBLOX7, + GNSS_MODEL_UBLOX8, + GNSS_MODEL_UBLOX9, + GNSS_MODEL_UBLOX10, + GNSS_MODEL_UC6580, + GNSS_MODEL_UNKNOWN, + GNSS_MODEL_MTK_L76B, + GNSS_MODEL_MTK_PA1010D, + GNSS_MODEL_MTK_PA1616S, + GNSS_MODEL_AG3335, + GNSS_MODEL_AG3352, + GNSS_MODEL_LS20031, + GNSS_MODEL_CM121 } GnssModel_t; typedef enum { - GNSS_RESPONSE_NONE, - GNSS_RESPONSE_NAK, - GNSS_RESPONSE_FRAME_ERRORS, - GNSS_RESPONSE_OK, + GNSS_RESPONSE_NONE, + GNSS_RESPONSE_NAK, + GNSS_RESPONSE_FRAME_ERRORS, + GNSS_RESPONSE_OK, } GPS_RESPONSE; enum GPSPowerState : uint8_t { - GPS_ACTIVE, // Awake and want a position - GPS_IDLE, // Awake, but not wanting another position yet - GPS_SOFTSLEEP, // Physically powered on, but soft-sleeping - GPS_HARDSLEEP, // Physically powered off, but scheduled to wake - GPS_OFF // Powered off indefinitely + GPS_ACTIVE, // Awake and want a position + GPS_IDLE, // Awake, but not wanting another position yet + GPS_SOFTSLEEP, // Physically powered on, but soft-sleeping + GPS_HARDSLEEP, // Physically powered off, but scheduled to wake + GPS_OFF // Powered off indefinitely }; struct ChipInfo { - String chipName; // The name of the chip (for logging) - String detectionString; // The string to match in the response - GnssModel_t driver; // The driver to use + String chipName; // The name of the chip (for logging) + String detectionString; // The string to match in the response + GnssModel_t driver; // The driver to use }; /** * A gps class that only reads from the GPS periodically and keeps the gps powered down except when reading * * When new data is available it will notify observers. */ -class GPS : private concurrency::OSThread -{ - public: - meshtastic_Position p = meshtastic_Position_init_default; +class GPS : private concurrency::OSThread { +public: + meshtastic_Position p = meshtastic_Position_init_default; - /** This is normally bound to config.position.gps_en_gpio but some rare boards (like heltec tracker) need more advanced - * implementations. Those boards will set this public variable to a custom implementation. - * - * Normally set by GPS::createGPS() - */ - GpioVirtPin *enablePin = NULL; + /** This is normally bound to config.position.gps_en_gpio but some rare boards (like heltec tracker) need more + * advanced implementations. Those boards will set this public variable to a custom implementation. + * + * Normally set by GPS::createGPS() + */ + GpioVirtPin *enablePin = NULL; - virtual ~GPS(); + virtual ~GPS(); - /** We will notify this observable anytime GPS state has changed meaningfully */ - Observable newStatus; + /** We will notify this observable anytime GPS state has changed meaningfully */ + Observable newStatus; - /** - * Returns true if we succeeded - */ - virtual bool setup(); + /** + * Returns true if we succeeded + */ + virtual bool setup(); - // re-enable the thread - void enable(); + // re-enable the thread + void enable(); - // Disable the thread - int32_t disable() override; + // Disable the thread + int32_t disable() override; - // toggle between enabled/disabled - void toggleGpsMode(); + // toggle between enabled/disabled + void toggleGpsMode(); - // Change the power state of the GPS - for power saving / shutdown - void setPowerState(GPSPowerState newState, uint32_t sleepMs = 0); + // Change the power state of the GPS - for power saving / shutdown + void setPowerState(GPSPowerState newState, uint32_t sleepMs = 0); - /// Returns true if we have acquired GPS lock. - virtual bool hasLock(); + /// Returns true if we have acquired GPS lock. + virtual bool hasLock(); - /// Returns true if there's valid data flow with the chip. - virtual bool hasFlow(); + /// Returns true if there's valid data flow with the chip. + virtual bool hasFlow(); - /// Return true if we are connected to a GPS - bool isConnected() const { return hasGPS; } + /// Return true if we are connected to a GPS + bool isConnected() const { return hasGPS; } - bool isPowerSaving() const { return config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED; } + bool isPowerSaving() const { return config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED; } - // Empty the input buffer as quickly as possible - void clearBuffer(); + // Empty the input buffer as quickly as possible + void clearBuffer(); - // Creates an instance of the GPS class. - // Returns the new instance or null if the GPS is not present. - static GPS *createGps(); + // Creates an instance of the GPS class. + // Returns the new instance or null if the GPS is not present. + static GPS *createGps(); - // Wake the GPS hardware - ready for an update - void up(); + // Wake the GPS hardware - ready for an update + void up(); - // Let the GPS hardware save power between updates - void down(); + // Let the GPS hardware save power between updates + void down(); - private: - GPS() : concurrency::OSThread("GPS") {} +private: + GPS() : concurrency::OSThread("GPS") {} - /// Record that we have a GPS - void setConnected(); + /// Record that we have a GPS + void setConnected(); - /** Subclasses should look for serial rx characters here and feed it to their GPS parser - * - * Return true if we received a valid message from the GPS - */ - virtual bool whileActive(); + /** Subclasses should look for serial rx characters here and feed it to their GPS parser + * + * Return true if we received a valid message from the GPS + */ + virtual bool whileActive(); - /** - * Perform any processing that should be done only while the GPS is awake and looking for a fix. - * Override this method to check for new locations - * - * @return true if we've acquired a time - */ - virtual bool lookForTime(); + /** + * Perform any processing that should be done only while the GPS is awake and looking for a fix. + * Override this method to check for new locations + * + * @return true if we've acquired a time + */ + virtual bool lookForTime(); - /** - * Perform any processing that should be done only while the GPS is awake and looking for a fix. - * Override this method to check for new locations - * - * @return true if we've acquired a new location - */ - virtual bool lookForLocation(); + /** + * Perform any processing that should be done only while the GPS is awake and looking for a fix. + * Override this method to check for new locations + * + * @return true if we've acquired a new location + */ + virtual bool lookForLocation(); - GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; + GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; - TinyGPSPlus reader; - uint8_t fixQual = 0; // fix quality from GPGGA - uint32_t lastChecksumFailCount = 0; - uint8_t currentStep = 0; - int32_t currentDelay = 2000; + TinyGPSPlus reader; + uint8_t fixQual = 0; // fix quality from GPGGA + uint32_t lastChecksumFailCount = 0; + uint8_t currentStep = 0; + int32_t currentDelay = 2000; #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - // (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field - // via optional feature "custom fields", currently disabled (bug #525) - TinyGPSCustom gsafixtype; // custom extract fix type from GPGSA - TinyGPSCustom gsapdop; // custom extract PDOP from GPGSA - uint8_t fixType = 0; // fix type from GPGSA + // (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field + // via optional feature "custom fields", currently disabled (bug #525) + TinyGPSCustom gsafixtype; // custom extract fix type from GPGSA + TinyGPSCustom gsapdop; // custom extract PDOP from GPGSA + uint8_t fixType = 0; // fix type from GPGSA #endif - uint32_t fixHoldEnds = 0; - uint32_t rx_gpio = 0; - uint32_t tx_gpio = 0; + uint32_t fixHoldEnds = 0; + uint32_t rx_gpio = 0; + uint32_t tx_gpio = 0; - uint8_t speedSelect = 0; - uint8_t probeTries = 0; + uint8_t speedSelect = 0; + uint8_t probeTries = 0; - /** - * hasValidLocation - indicates that the position variables contain a complete - * GPS location, valid and fresh (< gps_update_interval + position_broadcast_secs) - */ - bool hasValidLocation = false; // default to false, until we complete our first read + /** + * hasValidLocation - indicates that the position variables contain a complete + * GPS location, valid and fresh (< gps_update_interval + position_broadcast_secs) + */ + bool hasValidLocation = false; // default to false, until we complete our first read - bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop() + bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop() - bool hasGPS = false; // Do we have a GPS we are talking to + bool hasGPS = false; // Do we have a GPS we are talking to - bool GPSInitFinished = false; // Init thread finished? - bool GPSInitStarted = false; // Init thread finished? + bool GPSInitFinished = false; // Init thread finished? + bool GPSInitStarted = false; // Init thread finished? - GPSPowerState powerState = GPS_OFF; // GPS_ACTIVE if we want a location right now + GPSPowerState powerState = GPS_OFF; // GPS_ACTIVE if we want a location right now - uint8_t numSatellites = 0; + uint8_t numSatellites = 0; - CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); + CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); - /** If !NULL we will use this serial port to construct our GPS */ + /** If !NULL we will use this serial port to construct our GPS */ #if defined(ARCH_RP2040) - static SerialUART *_serial_gps; + static SerialUART *_serial_gps; #elif defined(ARCH_NRF52) - static Uart *_serial_gps; + static Uart *_serial_gps; #else - static HardwareSerial *_serial_gps; + static HardwareSerial *_serial_gps; #endif - // Create a ublox packet for editing in memory - uint8_t makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); - uint8_t makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); + // Create a ublox packet for editing in memory + uint8_t makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); + uint8_t makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); - // scratch space for creating ublox packets - uint8_t UBXscratch[250] = {0}; + // scratch space for creating ublox packets + uint8_t UBXscratch[250] = {0}; - int rebootsSeen = 0; + int rebootsSeen = 0; - int getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis); - GPS_RESPONSE getACK(uint8_t c, uint8_t i, uint32_t waitMillis); - GPS_RESPONSE getACK(const char *message, uint32_t waitMillis); + int getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis); + GPS_RESPONSE getACK(uint8_t c, uint8_t i, uint32_t waitMillis); + GPS_RESPONSE getACK(const char *message, uint32_t waitMillis); - GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis); + GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis); - /// Prepare the GPS for the cpu entering deep sleep, expect to be gone for at least 100s of msecs - /// always returns 0 to indicate okay to sleep - int prepareDeepSleep(void *unused); + /// Prepare the GPS for the cpu entering deep sleep, expect to be gone for at least 100s of msecs + /// always returns 0 to indicate okay to sleep + int prepareDeepSleep(void *unused); - /** Set power with EN pin, if relevant - */ - void writePinEN(bool on); + /** Set power with EN pin, if relevant + */ + void writePinEN(bool on); - /** Set the value of the STANDBY pin, if relevant - */ - void writePinStandby(bool standby); + /** Set the value of the STANDBY pin, if relevant + */ + void writePinStandby(bool standby); - /** Set GPS power with PMU, if relevant - */ - void setPowerPMU(bool on); + /** Set GPS power with PMU, if relevant + */ + void setPowerPMU(bool on); - /** Set UBLOX power, if relevant - */ - void setPowerUBLOX(bool on, uint32_t sleepMs = 0); + /** Set UBLOX power, if relevant + */ + void setPowerUBLOX(bool on, uint32_t sleepMs = 0); - /** - * Tell users we have new GPS readings - */ - void publishUpdate(); + /** + * Tell users we have new GPS readings + */ + void publishUpdate(); - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); + GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); - // Get GNSS model - GnssModel_t probe(int serialSpeed); + // Get GNSS model + GnssModel_t probe(int serialSpeed); - // delay counter to allow more sats before fixed position stops GPS thread - uint8_t fixeddelayCtr = 0; + // delay counter to allow more sats before fixed position stops GPS thread + uint8_t fixeddelayCtr = 0; }; extern GPS *gps; diff --git a/src/gps/GPSUpdateScheduling.cpp b/src/gps/GPSUpdateScheduling.cpp index 5eaf7a8ba..91a3cb1be 100644 --- a/src/gps/GPSUpdateScheduling.cpp +++ b/src/gps/GPSUpdateScheduling.cpp @@ -3,116 +3,100 @@ #include "Default.h" // Mark the time when searching for GPS position begins -void GPSUpdateScheduling::informSearching() -{ - searchStartedMs = millis(); -} +void GPSUpdateScheduling::informSearching() { searchStartedMs = millis(); } // Mark the time when searching for GPS is complete, // then update the predicted lock-time -void GPSUpdateScheduling::informGotLock() -{ - searchEndedMs = millis(); - LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000); - updateLockTimePrediction(); +void GPSUpdateScheduling::informGotLock() { + searchEndedMs = millis(); + LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000); + updateLockTimePrediction(); } // Clear old lock-time prediction data. // When re-enabling GPS with user button. -void GPSUpdateScheduling::reset() -{ - searchStartedMs = 0; - searchEndedMs = 0; - searchCount = 0; - predictedMsToGetLock = 0; +void GPSUpdateScheduling::reset() { + searchStartedMs = 0; + searchEndedMs = 0; + searchCount = 0; + predictedMsToGetLock = 0; } // How many milliseconds before we should next search for GPS position // Used by GPS hardware directly, to enter timed hardware sleep -uint32_t GPSUpdateScheduling::msUntilNextSearch() -{ - uint32_t now = millis(); +uint32_t GPSUpdateScheduling::msUntilNextSearch() { + uint32_t now = millis(); - // Target interval (seconds), between GPS updates - uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval); + // Target interval (seconds), between GPS updates + uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval); - // Check how long until we should start searching, to hopefully hit our target interval - uint32_t dueAtMs = searchEndedMs + updateInterval; - uint32_t compensatedStart = dueAtMs - predictedMsToGetLock; - int32_t remainingMs = compensatedStart - now; + // Check how long until we should start searching, to hopefully hit our target interval + uint32_t dueAtMs = searchEndedMs + updateInterval; + uint32_t compensatedStart = dueAtMs - predictedMsToGetLock; + int32_t remainingMs = compensatedStart - now; - // If we should have already started (negative value), start ASAP - if (remainingMs < 0) - remainingMs = 0; + // If we should have already started (negative value), start ASAP + if (remainingMs < 0) + remainingMs = 0; - return (uint32_t)remainingMs; + return (uint32_t)remainingMs; } // How long have we already been searching? // Used to abort a search in progress, if it runs unacceptably long -uint32_t GPSUpdateScheduling::elapsedSearchMs() -{ - // If searching - if (searchStartedMs > searchEndedMs) - return millis() - searchStartedMs; +uint32_t GPSUpdateScheduling::elapsedSearchMs() { + // If searching + if (searchStartedMs > searchEndedMs) + return millis() - searchStartedMs; - // If not searching - 0ms. We shouldn't really consume this value - else - return 0; + // If not searching - 0ms. We shouldn't really consume this value + else + return 0; } // Is it now time to begin searching for a GPS position? -bool GPSUpdateScheduling::isUpdateDue() -{ - return (msUntilNextSearch() == 0); -} +bool GPSUpdateScheduling::isUpdateDue() { return (msUntilNextSearch() == 0); } // Have we been searching for a GPS position for too long? -bool GPSUpdateScheduling::searchedTooLong() -{ - uint32_t minimumOrConfiguredSecs = - Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs); - uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs); - // If broadcast interval set to max, no such thing as "too long" - if (maxSearchMs == UINT32_MAX) - return false; +bool GPSUpdateScheduling::searchedTooLong() { + uint32_t minimumOrConfiguredSecs = Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs); + uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs); + // If broadcast interval set to max, no such thing as "too long" + if (maxSearchMs == UINT32_MAX) + return false; - // If we've been searching longer than our position broadcast interval: that's too long - else if (elapsedSearchMs() > maxSearchMs) - return true; + // If we've been searching longer than our position broadcast interval: that's too long + else if (elapsedSearchMs() > maxSearchMs) + return true; - // Otherwise, not too long yet! - else - return false; + // Otherwise, not too long yet! + else + return false; } // Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation -void GPSUpdateScheduling::updateLockTimePrediction() -{ +void GPSUpdateScheduling::updateLockTimePrediction() { - // How long did it take to get GPS lock this time? - // Duration between down() calls - int32_t lockTime = searchEndedMs - searchStartedMs; - if (lockTime < 0) - lockTime = 0; + // How long did it take to get GPS lock this time? + // Duration between down() calls + int32_t lockTime = searchEndedMs - searchStartedMs; + if (lockTime < 0) + lockTime = 0; - // Ignore the first lock-time: likely to be long, will skew data + // Ignore the first lock-time: likely to be long, will skew data - // Second locktime: likely stable. Use to initialize the smoothing filter - if (searchCount == 1) - predictedMsToGetLock = lockTime; + // Second locktime: likely stable. Use to initialize the smoothing filter + if (searchCount == 1) + predictedMsToGetLock = lockTime; - // Third locktime and after: predict using exponential smoothing. Respond slowly to changes - else if (searchCount > 1) - predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting)); + // Third locktime and after: predict using exponential smoothing. Respond slowly to changes + else if (searchCount > 1) + predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting)); - searchCount++; // Only tracked so we can disregard initial lock-times + searchCount++; // Only tracked so we can disregard initial lock-times - LOG_DEBUG("Predict %us to get next lock", predictedMsToGetLock / 1000); + LOG_DEBUG("Predict %us to get next lock", predictedMsToGetLock / 1000); } // How long do we expect to spend searching for a lock? -uint32_t GPSUpdateScheduling::predictedSearchDurationMs() -{ - return GPSUpdateScheduling::predictedMsToGetLock; -} +uint32_t GPSUpdateScheduling::predictedSearchDurationMs() { return GPSUpdateScheduling::predictedMsToGetLock; } diff --git a/src/gps/GPSUpdateScheduling.h b/src/gps/GPSUpdateScheduling.h index 7e121c9b6..b7ba09524 100644 --- a/src/gps/GPSUpdateScheduling.h +++ b/src/gps/GPSUpdateScheduling.h @@ -3,27 +3,26 @@ #include "configuration.h" // Encapsulates code responsible for the timing of GPS updates -class GPSUpdateScheduling -{ - public: - // Marks the time of these events, for calculation use - void informSearching(); - void informGotLock(); // Predicted lock-time is recalculated here +class GPSUpdateScheduling { +public: + // Marks the time of these events, for calculation use + void informSearching(); + void informGotLock(); // Predicted lock-time is recalculated here - void reset(); // Reset the prediction - after GPS::disable() / GPS::enable() - bool isUpdateDue(); // Is it time to begin searching for a GPS position? - bool searchedTooLong(); // Have we been searching for too long? + void reset(); // Reset the prediction - after GPS::disable() / GPS::enable() + bool isUpdateDue(); // Is it time to begin searching for a GPS position? + bool searchedTooLong(); // Have we been searching for too long? - uint32_t msUntilNextSearch(); // How long until we need to begin searching for a GPS? Info provided to GPS hardware for sleep - uint32_t elapsedSearchMs(); // How long have we been searching so far? - uint32_t predictedSearchDurationMs(); // How long do we expect to spend searching for a lock? + uint32_t msUntilNextSearch(); // How long until we need to begin searching for a GPS? Info provided to GPS hardware for sleep + uint32_t elapsedSearchMs(); // How long have we been searching so far? + uint32_t predictedSearchDurationMs(); // How long do we expect to spend searching for a lock? - private: - void updateLockTimePrediction(); // Called from informGotLock - uint32_t searchStartedMs = 0; - uint32_t searchEndedMs = 0; - uint32_t searchCount = 0; - uint32_t predictedMsToGetLock = 0; +private: + void updateLockTimePrediction(); // Called from informGotLock + uint32_t searchStartedMs = 0; + uint32_t searchEndedMs = 0; + uint32_t searchCount = 0; + uint32_t predictedMsToGetLock = 0; - const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time". + const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time". }; \ No newline at end of file diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp index 6d1f2da6d..3bba9ff65 100644 --- a/src/gps/GeoCoord.cpp +++ b/src/gps/GeoCoord.cpp @@ -1,402 +1,378 @@ #include "GeoCoord.h" -GeoCoord::GeoCoord() -{ - _dirty = true; +GeoCoord::GeoCoord() { _dirty = true; } + +GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _longitude(lon), _altitude(alt) { GeoCoord::setCoords(); } + +GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) { + // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + _latitude = int32_t(lat * 1e+7); + _longitude = int32_t(lon * 1e+7); + GeoCoord::setCoords(); } -GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _longitude(lon), _altitude(alt) -{ - GeoCoord::setCoords(); -} - -GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) -{ - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 - _latitude = int32_t(lat * 1e+7); - _longitude = int32_t(lon * 1e+7); - GeoCoord::setCoords(); -} - -GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) -{ - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 - _latitude = int32_t(lat * 1e+7); - _longitude = int32_t(lon * 1e+7); - GeoCoord::setCoords(); +GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) { + // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + _latitude = int32_t(lat * 1e+7); + _longitude = int32_t(lon * 1e+7); + GeoCoord::setCoords(); } // Initialize all the coordinate systems -void GeoCoord::setCoords() -{ - double lat = _latitude * 1e-7; - double lon = _longitude * 1e-7; - GeoCoord::latLongToDMS(lat, lon, _dms); - GeoCoord::latLongToUTM(lat, lon, _utm); - GeoCoord::latLongToMGRS(lat, lon, _mgrs); - GeoCoord::latLongToOSGR(lat, lon, _osgr); - GeoCoord::latLongToOLC(lat, lon, _olc); - _dirty = false; +void GeoCoord::setCoords() { + double lat = _latitude * 1e-7; + double lon = _longitude * 1e-7; + GeoCoord::latLongToDMS(lat, lon, _dms); + GeoCoord::latLongToUTM(lat, lon, _utm); + GeoCoord::latLongToMGRS(lat, lon, _mgrs); + GeoCoord::latLongToOSGR(lat, lon, _osgr); + GeoCoord::latLongToOLC(lat, lon, _olc); + _dirty = false; } -void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt) -{ - // If marked dirty or new coordinates - if (_dirty || _latitude != lat || _longitude != lon || _altitude != alt) { - _dirty = true; - _latitude = lat; - _longitude = lon; - _altitude = alt; - setCoords(); - } +void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt) { + // If marked dirty or new coordinates + if (_dirty || _latitude != lat || _longitude != lon || _altitude != alt) { + _dirty = true; + _latitude = lat; + _longitude = lon; + _altitude = alt; + setCoords(); + } } -void GeoCoord::updateCoords(const double lat, const double lon, const int32_t alt) -{ - int32_t iLat = lat * 1e+7; - int32_t iLon = lon * 1e+7; - // If marked dirty or new coordinates - if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { - _dirty = true; - _latitude = iLat; - _longitude = iLon; - _altitude = alt; - setCoords(); - } +void GeoCoord::updateCoords(const double lat, const double lon, const int32_t alt) { + int32_t iLat = lat * 1e+7; + int32_t iLon = lon * 1e+7; + // If marked dirty or new coordinates + if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { + _dirty = true; + _latitude = iLat; + _longitude = iLon; + _altitude = alt; + setCoords(); + } } -void GeoCoord::updateCoords(const float lat, const float lon, const int32_t alt) -{ - int32_t iLat = lat * 1e+7; - int32_t iLon = lon * 1e+7; - // If marked dirty or new coordinates - if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { - _dirty = true; - _latitude = iLat; - _longitude = iLon; - _altitude = alt; - setCoords(); - } +void GeoCoord::updateCoords(const float lat, const float lon, const int32_t alt) { + int32_t iLat = lat * 1e+7; + int32_t iLon = lon * 1e+7; + // If marked dirty or new coordinates + if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { + _dirty = true; + _latitude = iLat; + _longitude = iLon; + _altitude = alt; + setCoords(); + } } /** * Converts lat long coordinates from decimal degrees to degrees minutes seconds format. * DD°MM'SS"C DDD°MM'SS"C */ -void GeoCoord::latLongToDMS(const double lat, const double lon, DMS &dms) -{ - if (lat < 0) - dms.latCP = 'S'; - else - dms.latCP = 'N'; +void GeoCoord::latLongToDMS(const double lat, const double lon, DMS &dms) { + if (lat < 0) + dms.latCP = 'S'; + else + dms.latCP = 'N'; - double latDeg = lat; + double latDeg = lat; - if (lat < 0) - latDeg = latDeg * -1; + if (lat < 0) + latDeg = latDeg * -1; - dms.latDeg = floor(latDeg); - double latMin = (latDeg - dms.latDeg) * 60; - dms.latMin = floor(latMin); - dms.latSec = (latMin - dms.latMin) * 60; + dms.latDeg = floor(latDeg); + double latMin = (latDeg - dms.latDeg) * 60; + dms.latMin = floor(latMin); + dms.latSec = (latMin - dms.latMin) * 60; - if (lon < 0) - dms.lonCP = 'W'; - else - dms.lonCP = 'E'; + if (lon < 0) + dms.lonCP = 'W'; + else + dms.lonCP = 'E'; - double lonDeg = lon; + double lonDeg = lon; - if (lon < 0) - lonDeg = lonDeg * -1; + if (lon < 0) + lonDeg = lonDeg * -1; - dms.lonDeg = floor(lonDeg); - double lonMin = (lonDeg - dms.lonDeg) * 60; - dms.lonMin = floor(lonMin); - dms.lonSec = (lonMin - dms.lonMin) * 60; + dms.lonDeg = floor(lonDeg); + double lonMin = (lonDeg - dms.lonDeg) * 60; + dms.lonMin = floor(lonMin); + dms.lonSec = (lonMin - dms.lonMin) * 60; } /** * Converts lat long coordinates to UTM. * based on this: https://github.com/walvok/LatLonToUTM/blob/master/latlon_utm.ino */ -void GeoCoord::latLongToUTM(const double lat, const double lon, UTM &utm) -{ +void GeoCoord::latLongToUTM(const double lat, const double lon, UTM &utm) { - const std::string latBands = "CDEFGHJKLMNPQRSTUVWXX"; - utm.zone = int((lon + 180) / 6 + 1); - utm.band = latBands[int(lat / 8 + 10)]; - double a = 6378137; // WGS84 - equatorial radius - double k0 = 0.9996; // UTM point scale on the central meridian - double eccSquared = 0.00669438; // eccentricity squared - double lonTemp = (lon + 180) - int((lon + 180) / 360) * 360 - 180; // Make sure the longitude is between -180.00 .. 179.9 - double latRad = toRadians(lat); - double lonRad = toRadians(lonTemp); + const std::string latBands = "CDEFGHJKLMNPQRSTUVWXX"; + utm.zone = int((lon + 180) / 6 + 1); + utm.band = latBands[int(lat / 8 + 10)]; + double a = 6378137; // WGS84 - equatorial radius + double k0 = 0.9996; // UTM point scale on the central meridian + double eccSquared = 0.00669438; // eccentricity squared + double lonTemp = (lon + 180) - int((lon + 180) / 360) * 360 - 180; // Make sure the longitude is between -180.00 .. 179.9 + double latRad = toRadians(lat); + double lonRad = toRadians(lonTemp); - // Special Zones for Norway and Svalbard - if (lat >= 56.0 && lat < 64.0 && lonTemp >= 3.0 && lonTemp < 12.0) // Norway - utm.zone = 32; - if (lat >= 72.0 && lat < 84.0) { // Svalbard - if (lonTemp >= 0.0 && lonTemp < 9.0) - utm.zone = 31; - else if (lonTemp >= 9.0 && lonTemp < 21.0) - utm.zone = 33; - else if (lonTemp >= 21.0 && lonTemp < 33.0) - utm.zone = 35; - else if (lonTemp >= 33.0 && lonTemp < 42.0) - utm.zone = 37; - } + // Special Zones for Norway and Svalbard + if (lat >= 56.0 && lat < 64.0 && lonTemp >= 3.0 && lonTemp < 12.0) // Norway + utm.zone = 32; + if (lat >= 72.0 && lat < 84.0) { // Svalbard + if (lonTemp >= 0.0 && lonTemp < 9.0) + utm.zone = 31; + else if (lonTemp >= 9.0 && lonTemp < 21.0) + utm.zone = 33; + else if (lonTemp >= 21.0 && lonTemp < 33.0) + utm.zone = 35; + else if (lonTemp >= 33.0 && lonTemp < 42.0) + utm.zone = 37; + } - double lonOrigin = (utm.zone - 1) * 6 - 180 + 3; // puts origin in middle of zone - double lonOriginRad = toRadians(lonOrigin); - double eccPrimeSquared = (eccSquared) / (1 - eccSquared); - double N = a / sqrt(1 - eccSquared * sin(latRad) * sin(latRad)); - double T = tan(latRad) * tan(latRad); - double C = eccPrimeSquared * cos(latRad) * cos(latRad); - double A = cos(latRad) * (lonRad - lonOriginRad); - double M = - a * ((1 - eccSquared / 4 - 3 * eccSquared * eccSquared / 64 - 5 * eccSquared * eccSquared * eccSquared / 256) * latRad - - (3 * eccSquared / 8 + 3 * eccSquared * eccSquared / 32 + 45 * eccSquared * eccSquared * eccSquared / 1024) * - sin(2 * latRad) + - (15 * eccSquared * eccSquared / 256 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(4 * latRad) - - (35 * eccSquared * eccSquared * eccSquared / 3072) * sin(6 * latRad)); - utm.easting = (double)(k0 * N * - (A + (1 - T + C) * pow(A, 3) / 6 + - (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120) + - 500000.0); - utm.northing = - (double)(k0 * (M + N * tan(latRad) * - (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 + - (61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720))); + double lonOrigin = (utm.zone - 1) * 6 - 180 + 3; // puts origin in middle of zone + double lonOriginRad = toRadians(lonOrigin); + double eccPrimeSquared = (eccSquared) / (1 - eccSquared); + double N = a / sqrt(1 - eccSquared * sin(latRad) * sin(latRad)); + double T = tan(latRad) * tan(latRad); + double C = eccPrimeSquared * cos(latRad) * cos(latRad); + double A = cos(latRad) * (lonRad - lonOriginRad); + double M = a * ((1 - eccSquared / 4 - 3 * eccSquared * eccSquared / 64 - 5 * eccSquared * eccSquared * eccSquared / 256) * latRad - + (3 * eccSquared / 8 + 3 * eccSquared * eccSquared / 32 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(2 * latRad) + + (15 * eccSquared * eccSquared / 256 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(4 * latRad) - + (35 * eccSquared * eccSquared * eccSquared / 3072) * sin(6 * latRad)); + utm.easting = (double)(k0 * N * (A + (1 - T + C) * pow(A, 3) / 6 + (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120) + + 500000.0); + utm.northing = (double)(k0 * (M + N * tan(latRad) * + (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 + + (61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720))); - if (lat < 0) - utm.northing += 10000000.0; // 10000000 meter offset for southern hemisphere + if (lat < 0) + utm.northing += 10000000.0; // 10000000 meter offset for southern hemisphere } // Converts lat long coordinates to an MGRS. -void GeoCoord::latLongToMGRS(const double lat, const double lon, MGRS &mgrs) -{ - const std::string e100kLetters[3] = {"ABCDEFGH", "JKLMNPQR", "STUVWXYZ"}; - const std::string n100kLetters[2] = {"ABCDEFGHJKLMNPQRSTUV", "FGHJKLMNPQRSTUVABCDE"}; - UTM utm; - latLongToUTM(lat, lon, utm); - mgrs.zone = utm.zone; - mgrs.band = utm.band; - double col = floor(utm.easting / 100000); - mgrs.east100k = e100kLetters[(mgrs.zone - 1) % 3][col - 1]; - double row = (int32_t)floor(utm.northing / 100000.0) % 20; - mgrs.north100k = n100kLetters[(mgrs.zone - 1) % 2][row]; - mgrs.easting = (int32_t)utm.easting % 100000; - mgrs.northing = (int32_t)utm.northing % 100000; +void GeoCoord::latLongToMGRS(const double lat, const double lon, MGRS &mgrs) { + const std::string e100kLetters[3] = {"ABCDEFGH", "JKLMNPQR", "STUVWXYZ"}; + const std::string n100kLetters[2] = {"ABCDEFGHJKLMNPQRSTUV", "FGHJKLMNPQRSTUVABCDE"}; + UTM utm; + latLongToUTM(lat, lon, utm); + mgrs.zone = utm.zone; + mgrs.band = utm.band; + double col = floor(utm.easting / 100000); + mgrs.east100k = e100kLetters[(mgrs.zone - 1) % 3][col - 1]; + double row = (int32_t)floor(utm.northing / 100000.0) % 20; + mgrs.north100k = n100kLetters[(mgrs.zone - 1) % 2][row]; + mgrs.easting = (int32_t)utm.easting % 100000; + mgrs.northing = (int32_t)utm.northing % 100000; } /** * Converts lat long coordinates to Ordnance Survey Grid Reference (UK National Grid Ref). * Based on: https://www.movable-type.co.uk/scripts/latlong-os-gridref.html */ -void GeoCoord::latLongToOSGR(const double lat, const double lon, OSGR &osgr) -{ - const char letter[] = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; // No 'I' in OSGR - double a = 6377563.396; // Airy 1830 semi-major axis - double b = 6356256.909; // Airy 1830 semi-minor axis - double f0 = 0.9996012717; // National Grid point scale factor on the central meridian - double phi0 = toRadians(49); - double lambda0 = toRadians(-2); - double n0 = -100000; - double e0 = 400000; - double e2 = 1 - (b * b) / (a * a); // eccentricity squared - double n = (a - b) / (a + b); +void GeoCoord::latLongToOSGR(const double lat, const double lon, OSGR &osgr) { + const char letter[] = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; // No 'I' in OSGR + double a = 6377563.396; // Airy 1830 semi-major axis + double b = 6356256.909; // Airy 1830 semi-minor axis + double f0 = 0.9996012717; // National Grid point scale factor on the central meridian + double phi0 = toRadians(49); + double lambda0 = toRadians(-2); + double n0 = -100000; + double e0 = 400000; + double e2 = 1 - (b * b) / (a * a); // eccentricity squared + double n = (a - b) / (a + b); - double osgb_Latitude; - double osgb_Longitude; - convertWGS84ToOSGB36(lat, lon, osgb_Latitude, osgb_Longitude); - double phi = osgb_Latitude; // already in radians - double lambda = osgb_Longitude; // already in radians - double v = a * f0 / sqrt(1 - e2 * sin(phi) * sin(phi)); - double rho = a * f0 * (1 - e2) / pow(1 - e2 * sin(phi) * sin(phi), 1.5); - double eta2 = v / rho - 1; - double mA = (1 + n + (5 / 4) * n * n + (5 / 4) * n * n * n) * (phi - phi0); - double mB = (3 * n + 3 * n * n + (21 / 8) * n * n * n) * sin(phi - phi0) * cos(phi + phi0); - // loss of precision in mC & mD due to floating point rounding can cause inaccuracy of northing by a few meters - double mC = (15 / 8 * n * n + 15 / 8 * n * n * n) * sin(2 * (phi - phi0)) * cos(2 * (phi + phi0)); - double mD = (35 / 24) * n * n * n * sin(3 * (phi - phi0)) * cos(3 * (phi + phi0)); - double m = b * f0 * (mA - mB + mC - mD); + double osgb_Latitude; + double osgb_Longitude; + convertWGS84ToOSGB36(lat, lon, osgb_Latitude, osgb_Longitude); + double phi = osgb_Latitude; // already in radians + double lambda = osgb_Longitude; // already in radians + double v = a * f0 / sqrt(1 - e2 * sin(phi) * sin(phi)); + double rho = a * f0 * (1 - e2) / pow(1 - e2 * sin(phi) * sin(phi), 1.5); + double eta2 = v / rho - 1; + double mA = (1 + n + (5 / 4) * n * n + (5 / 4) * n * n * n) * (phi - phi0); + double mB = (3 * n + 3 * n * n + (21 / 8) * n * n * n) * sin(phi - phi0) * cos(phi + phi0); + // loss of precision in mC & mD due to floating point rounding can cause inaccuracy of northing by a few meters + double mC = (15 / 8 * n * n + 15 / 8 * n * n * n) * sin(2 * (phi - phi0)) * cos(2 * (phi + phi0)); + double mD = (35 / 24) * n * n * n * sin(3 * (phi - phi0)) * cos(3 * (phi + phi0)); + double m = b * f0 * (mA - mB + mC - mD); - double cos3Phi = cos(phi) * cos(phi) * cos(phi); - double cos5Phi = cos3Phi * cos(phi) * cos(phi); - double tan2Phi = tan(phi) * tan(phi); - double tan4Phi = tan2Phi * tan2Phi; - double I = m + n0; - double II = (v / 2) * sin(phi) * cos(phi); - double III = (v / 24) * sin(phi) * cos3Phi * (5 - tan2Phi + 9 * eta2); - double IIIA = (v / 720) * sin(phi) * cos5Phi * (61 - 58 * tan2Phi + tan4Phi); - double IV = v * cos(phi); - double V = (v / 6) * cos3Phi * (v / rho - tan2Phi); - double VI = (v / 120) * cos5Phi * (5 - 18 * tan2Phi + tan4Phi + 14 * eta2 - 58 * tan2Phi * eta2); + double cos3Phi = cos(phi) * cos(phi) * cos(phi); + double cos5Phi = cos3Phi * cos(phi) * cos(phi); + double tan2Phi = tan(phi) * tan(phi); + double tan4Phi = tan2Phi * tan2Phi; + double I = m + n0; + double II = (v / 2) * sin(phi) * cos(phi); + double III = (v / 24) * sin(phi) * cos3Phi * (5 - tan2Phi + 9 * eta2); + double IIIA = (v / 720) * sin(phi) * cos5Phi * (61 - 58 * tan2Phi + tan4Phi); + double IV = v * cos(phi); + double V = (v / 6) * cos3Phi * (v / rho - tan2Phi); + double VI = (v / 120) * cos5Phi * (5 - 18 * tan2Phi + tan4Phi + 14 * eta2 - 58 * tan2Phi * eta2); - double deltaLambda = lambda - lambda0; - double deltaLambda2 = deltaLambda * deltaLambda; - double northing = - I + II * deltaLambda2 + III * deltaLambda2 * deltaLambda2 + IIIA * deltaLambda2 * deltaLambda2 * deltaLambda2; - double easting = e0 + IV * deltaLambda + V * deltaLambda2 * deltaLambda + VI * deltaLambda2 * deltaLambda2 * deltaLambda; + double deltaLambda = lambda - lambda0; + double deltaLambda2 = deltaLambda * deltaLambda; + double northing = I + II * deltaLambda2 + III * deltaLambda2 * deltaLambda2 + IIIA * deltaLambda2 * deltaLambda2 * deltaLambda2; + double easting = e0 + IV * deltaLambda + V * deltaLambda2 * deltaLambda + VI * deltaLambda2 * deltaLambda2 * deltaLambda; - if (easting < 0 || easting > 700000 || northing < 0 || northing > 1300000) // Check if out of boundaries - osgr = {'I', 'I', 0, 0}; - else { - uint32_t e100k = floor(easting / 100000); - uint32_t n100k = floor(northing / 100000); - int8_t l1 = (19 - n100k) - (19 - n100k) % 5 + floor((e100k + 10) / 5); - int8_t l2 = (19 - n100k) * 5 % 25 + e100k % 5; - osgr.e100k = letter[l1]; - osgr.n100k = letter[l2]; - osgr.easting = floor((int)easting % 100000); - osgr.northing = floor((int)northing % 100000); - } + if (easting < 0 || easting > 700000 || northing < 0 || northing > 1300000) // Check if out of boundaries + osgr = {'I', 'I', 0, 0}; + else { + uint32_t e100k = floor(easting / 100000); + uint32_t n100k = floor(northing / 100000); + int8_t l1 = (19 - n100k) - (19 - n100k) % 5 + floor((e100k + 10) / 5); + int8_t l2 = (19 - n100k) * 5 % 25 + e100k % 5; + osgr.e100k = letter[l1]; + osgr.n100k = letter[l2]; + osgr.easting = floor((int)easting % 100000); + osgr.northing = floor((int)northing % 100000); + } } /** * Converts lat long coordinates to Open Location Code. * Based on: https://github.com/google/open-location-code/blob/main/c/src/olc.c */ -void GeoCoord::latLongToOLC(double lat, double lon, OLC &olc) -{ - char tempCode[] = "1234567890abc"; - const char kAlphabet[] = "23456789CFGHJMPQRVWX"; - double latitude; - double longitude = lon; - double latitude_degrees = std::min(90.0, std::max(-90.0, lat)); +void GeoCoord::latLongToOLC(double lat, double lon, OLC &olc) { + char tempCode[] = "1234567890abc"; + const char kAlphabet[] = "23456789CFGHJMPQRVWX"; + double latitude; + double longitude = lon; + double latitude_degrees = std::min(90.0, std::max(-90.0, lat)); - if (latitude_degrees < 90) // Check latitude less than lat max - latitude = latitude_degrees; - else { - double precision; - if (OLC_CODE_LEN <= 10) - precision = pow_neg(20, floor((OLC_CODE_LEN / -2) + 2)); - else - precision = pow_neg(20, -3) / pow(5, OLC_CODE_LEN - 10); - latitude = latitude_degrees - precision / 2; + if (latitude_degrees < 90) // Check latitude less than lat max + latitude = latitude_degrees; + else { + double precision; + if (OLC_CODE_LEN <= 10) + precision = pow_neg(20, floor((OLC_CODE_LEN / -2) + 2)); + else + precision = pow_neg(20, -3) / pow(5, OLC_CODE_LEN - 10); + latitude = latitude_degrees - precision / 2; + } + while (longitude < -180) // Normalize longitude + longitude += 360; + while (longitude >= 180) + longitude -= 360; + int64_t lat_val = 90 * 2.5e7; + int64_t lng_val = 180 * 8.192e6; + lat_val += latitude * 2.5e7; + lng_val += longitude * 8.192e6; + size_t pos = OLC_CODE_LEN; + + if (OLC_CODE_LEN > 10) { // Compute grid part of code if needed + for (size_t i = 0; i < 5; i++) { + int lat_digit = lat_val % 5; + int lng_digit = lng_val % 4; + int ndx = lat_digit * 4 + lng_digit; + tempCode[pos--] = kAlphabet[ndx]; + lat_val /= 5; + lng_val /= 4; } - while (longitude < -180) // Normalize longitude - longitude += 360; - while (longitude >= 180) - longitude -= 360; - int64_t lat_val = 90 * 2.5e7; - int64_t lng_val = 180 * 8.192e6; - lat_val += latitude * 2.5e7; - lng_val += longitude * 8.192e6; - size_t pos = OLC_CODE_LEN; + } else { + lat_val /= pow(5, 5); + lng_val /= pow(4, 5); + } - if (OLC_CODE_LEN > 10) { // Compute grid part of code if needed - for (size_t i = 0; i < 5; i++) { - int lat_digit = lat_val % 5; - int lng_digit = lng_val % 4; - int ndx = lat_digit * 4 + lng_digit; - tempCode[pos--] = kAlphabet[ndx]; - lat_val /= 5; - lng_val /= 4; - } - } else { - lat_val /= pow(5, 5); - lng_val /= pow(4, 5); - } + pos = 10; - pos = 10; + for (size_t i = 0; i < 5; i++) { // Compute pair section of code + int lat_ndx = lat_val % 20; + int lng_ndx = lng_val % 20; + tempCode[pos--] = kAlphabet[lng_ndx]; + tempCode[pos--] = kAlphabet[lat_ndx]; + lat_val /= 20; + lng_val /= 20; - for (size_t i = 0; i < 5; i++) { // Compute pair section of code - int lat_ndx = lat_val % 20; - int lng_ndx = lng_val % 20; - tempCode[pos--] = kAlphabet[lng_ndx]; - tempCode[pos--] = kAlphabet[lat_ndx]; - lat_val /= 20; - lng_val /= 20; + if (i == 0) + tempCode[pos--] = '+'; + } - if (i == 0) - tempCode[pos--] = '+'; - } + if (OLC_CODE_LEN < 9) { // Add padding if needed + for (size_t i = OLC_CODE_LEN; i < 9; i++) + tempCode[i] = '0'; + tempCode[9] = '+'; + } - if (OLC_CODE_LEN < 9) { // Add padding if needed - for (size_t i = OLC_CODE_LEN; i < 9; i++) - tempCode[i] = '0'; - tempCode[9] = '+'; - } - - size_t char_count = OLC_CODE_LEN; - if (10 > char_count) { - char_count = 10; - } - for (size_t i = 0; i < char_count; i++) { - olc.code[i] = tempCode[i]; - } - olc.code[char_count] = '\0'; + size_t char_count = OLC_CODE_LEN; + if (10 > char_count) { + char_count = 10; + } + for (size_t i = 0; i < char_count; i++) { + olc.code[i] = tempCode[i]; + } + olc.code[char_count] = '\0'; } // Converts the coordinate in WGS84 datum to the OSGB36 datum. -void GeoCoord::convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude) -{ - // Convert lat long to cartesian - double phi = toRadians(lat); - double lambda = toRadians(lon); - double h = 0.0; // No OSTN height data used, some loss of accuracy (up to 5m) - double wgsA = 6378137; // WGS84 datum semi major axis - double wgsF = 1 / 298.257223563; // WGS84 datum flattening - double ecc = 2 * wgsF - wgsF * wgsF; - double vee = wgsA / sqrt(1 - ecc * pow(sin(phi), 2)); - double wgsX = (vee + h) * cos(phi) * cos(lambda); - double wgsY = (vee + h) * cos(phi) * sin(lambda); - double wgsZ = ((1 - ecc) * vee + h) * sin(phi); +void GeoCoord::convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude) { + // Convert lat long to cartesian + double phi = toRadians(lat); + double lambda = toRadians(lon); + double h = 0.0; // No OSTN height data used, some loss of accuracy (up to 5m) + double wgsA = 6378137; // WGS84 datum semi major axis + double wgsF = 1 / 298.257223563; // WGS84 datum flattening + double ecc = 2 * wgsF - wgsF * wgsF; + double vee = wgsA / sqrt(1 - ecc * pow(sin(phi), 2)); + double wgsX = (vee + h) * cos(phi) * cos(lambda); + double wgsY = (vee + h) * cos(phi) * sin(lambda); + double wgsZ = ((1 - ecc) * vee + h) * sin(phi); - // 7-parameter Helmert transform - double tx = -446.448; // x shift in meters - double ty = 125.157; // y shift in meters - double tz = -542.060; // z shift in meters - double s = 20.4894 / 1e6 + 1; // scale normalized parts per million to (s + 1) - double rx = toRadians(-0.1502 / 3600); // x rotation normalize arcseconds to radians - double ry = toRadians(-0.2470 / 3600); // y rotation normalize arcseconds to radians - double rz = toRadians(-0.8421 / 3600); // z rotation normalize arcseconds to radians - double osgbX = tx + wgsX * s - wgsY * rz + wgsZ * ry; - double osgbY = ty + wgsX * rz + wgsY * s - wgsZ * rx; - double osgbZ = tz - wgsX * ry + wgsY * rx + wgsZ * s; + // 7-parameter Helmert transform + double tx = -446.448; // x shift in meters + double ty = 125.157; // y shift in meters + double tz = -542.060; // z shift in meters + double s = 20.4894 / 1e6 + 1; // scale normalized parts per million to (s + 1) + double rx = toRadians(-0.1502 / 3600); // x rotation normalize arcseconds to radians + double ry = toRadians(-0.2470 / 3600); // y rotation normalize arcseconds to radians + double rz = toRadians(-0.8421 / 3600); // z rotation normalize arcseconds to radians + double osgbX = tx + wgsX * s - wgsY * rz + wgsZ * ry; + double osgbY = ty + wgsX * rz + wgsY * s - wgsZ * rx; + double osgbZ = tz - wgsX * ry + wgsY * rx + wgsZ * s; - // Convert cartesian to lat long - double airyA = 6377563.396; // Airy1830 datum semi major axis - double airyB = 6356256.909; // Airy1830 datum semi minor axis - double airyF = 1 / 299.3249646; // Airy1830 datum flattening - double airyEcc = 2 * airyF - airyF * airyF; - double airyEcc2 = airyEcc / (1 - airyEcc); - double p = sqrt(osgbX * osgbX + osgbY * osgbY); - double R = sqrt(p * p + osgbZ * osgbZ); - double tanBeta = (airyB * osgbZ) / (airyA * p) * (1 + airyEcc2 * airyB / R); - double sinBeta = tanBeta / sqrt(1 + tanBeta * tanBeta); - double cosBeta = sinBeta / tanBeta; - osgb_Latitude = atan2(osgbZ + airyEcc2 * airyB * sinBeta * sinBeta * sinBeta, - p - airyEcc * airyA * cosBeta * cosBeta * cosBeta); // leave in radians - osgb_Longitude = atan2(osgbY, osgbX); // leave in radians - // osgb height = p*cos(osgb.latitude) + osgbZ*sin(osgb.latitude) - - //(airyA*airyA/(airyA / sqrt(1 - airyEcc*sin(osgb.latitude)*sin(osgb.latitude)))); // Not used, no OSTN data + // Convert cartesian to lat long + double airyA = 6377563.396; // Airy1830 datum semi major axis + double airyB = 6356256.909; // Airy1830 datum semi minor axis + double airyF = 1 / 299.3249646; // Airy1830 datum flattening + double airyEcc = 2 * airyF - airyF * airyF; + double airyEcc2 = airyEcc / (1 - airyEcc); + double p = sqrt(osgbX * osgbX + osgbY * osgbY); + double R = sqrt(p * p + osgbZ * osgbZ); + double tanBeta = (airyB * osgbZ) / (airyA * p) * (1 + airyEcc2 * airyB / R); + double sinBeta = tanBeta / sqrt(1 + tanBeta * tanBeta); + double cosBeta = sinBeta / tanBeta; + osgb_Latitude = atan2(osgbZ + airyEcc2 * airyB * sinBeta * sinBeta * sinBeta, + p - airyEcc * airyA * cosBeta * cosBeta * cosBeta); // leave in radians + osgb_Longitude = atan2(osgbY, osgbX); // leave in radians + // osgb height = p*cos(osgb.latitude) + osgbZ*sin(osgb.latitude) - + //(airyA*airyA/(airyA / sqrt(1 - + // airyEcc*sin(osgb.latitude)*sin(osgb.latitude)))); // Not used, no OSTN data } /// Ported from my old java code, returns distance in meters along the globe /// surface (by Haversine formula) -float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) -{ - // Don't do math if the points are the same - if (lat_a == lat_b && lng_a == lng_b) - return 0.0; +float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) { + // Don't do math if the points are the same + if (lat_a == lat_b && lng_a == lng_b) + return 0.0; - double a1 = lat_a / DEG_CONVERT; - double a2 = lng_a / DEG_CONVERT; - double b1 = lat_b / DEG_CONVERT; - double b2 = lng_b / DEG_CONVERT; - double cos_b1 = cos(b1); - double cos_a1 = cos(a1); - double t1 = cos_a1 * cos(a2) * cos_b1 * cos(b2); - double t2 = cos_a1 * sin(a2) * cos_b1 * sin(b2); - double t3 = sin(a1) * sin(b1); - double tt = acos(t1 + t2 + t3); - if (std::isnan(tt)) - tt = 0.0; // Must have been the same point? + double a1 = lat_a / DEG_CONVERT; + double a2 = lng_a / DEG_CONVERT; + double b1 = lat_b / DEG_CONVERT; + double b2 = lng_b / DEG_CONVERT; + double cos_b1 = cos(b1); + double cos_a1 = cos(a1); + double t1 = cos_a1 * cos(a2) * cos_b1 * cos(b2); + double t2 = cos_a1 * sin(a2) * cos_b1 * sin(b2); + double t3 = sin(a1) * sin(b1); + double tt = acos(t1 + t2 + t3); + if (std::isnan(tt)) + tt = 0.0; // Must have been the same point? - return (float)(6366000 * tt); + return (float)(6366000 * tt); } /** @@ -414,14 +390,13 @@ float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double * @return Bearing from point 1 to point 2 in radians. A value of 0 means due * north. */ -float GeoCoord::bearing(double lat1, double lon1, double lat2, double lon2) -{ - double lat1Rad = toRadians(lat1); - double lat2Rad = toRadians(lat2); - double deltaLonRad = toRadians(lon2 - lon1); - double y = sin(deltaLonRad) * cos(lat2Rad); - double x = cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad) * cos(deltaLonRad)); - return atan2(y, x); +float GeoCoord::bearing(double lat1, double lon1, double lat2, double lon2) { + double lat1Rad = toRadians(lat1); + double lat2Rad = toRadians(lat2); + double deltaLonRad = toRadians(lon2 - lon1); + double y = sin(deltaLonRad) * cos(lat2Rad); + double x = cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad) * cos(deltaLonRad)); + return atan2(y, x); } /** @@ -431,11 +406,10 @@ float GeoCoord::bearing(double lat1, double lon1, double lat2, double lon2) * The range in meters * @return range in radians on a great circle */ -float GeoCoord::rangeMetersToRadians(double range_meters) -{ - // 1 nm is 1852 meters - double distance_nm = range_meters * 1852; - return (PI / (180 * 60)) * distance_nm; +float GeoCoord::rangeMetersToRadians(double range_meters) { + // 1 nm is 1852 meters + double distance_nm = range_meters * 1852; + return (PI / (180 * 60)) * distance_nm; } /** @@ -445,25 +419,20 @@ float GeoCoord::rangeMetersToRadians(double range_meters) * The range in radians * @return Range in meters on a great circle */ -float GeoCoord::rangeRadiansToMeters(double range_radians) -{ - double distance_nm = ((180 * 60) / PI) * range_radians; - // 1 meter is 0.000539957 nm - return distance_nm * 0.000539957; +float GeoCoord::rangeRadiansToMeters(double range_radians) { + double distance_nm = ((180 * 60) / PI) * range_radians; + // 1 meter is 0.000539957 nm + return distance_nm * 0.000539957; } // Find distance from point to passed in point -int32_t GeoCoord::distanceTo(const GeoCoord &pointB) -{ - return latLongToMeter(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, - pointB.getLongitude() * 1e-7); +int32_t GeoCoord::distanceTo(const GeoCoord &pointB) { + return latLongToMeter(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, pointB.getLongitude() * 1e-7); } // Find bearing from point to passed in point -int32_t GeoCoord::bearingTo(const GeoCoord &pointB) -{ - return bearing(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, - pointB.getLongitude() * 1e-7); +int32_t GeoCoord::bearingTo(const GeoCoord &pointB) { + return bearing(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, pointB.getLongitude() * 1e-7); } /** @@ -475,16 +444,15 @@ int32_t GeoCoord::bearingTo(const GeoCoord &pointB) * range in meters * @return GeoCoord object of point at bearing and range from initial point */ -std::shared_ptr GeoCoord::pointAtDistance(double bearing, double range_meters) -{ - double range_radians = rangeMetersToRadians(range_meters); - double lat1 = this->getLatitude() * 1e-7; - double lon1 = this->getLongitude() * 1e-7; - double lat = asin(sin(lat1) * cos(range_radians) + cos(lat1) * sin(range_radians) * cos(bearing)); - double dlon = atan2(sin(bearing) * sin(range_radians) * cos(lat1), cos(range_radians) - sin(lat1) * sin(lat)); - double lon = fmod(lon1 - dlon + PI, 2 * PI) - PI; +std::shared_ptr GeoCoord::pointAtDistance(double bearing, double range_meters) { + double range_radians = rangeMetersToRadians(range_meters); + double lat1 = this->getLatitude() * 1e-7; + double lon1 = this->getLongitude() * 1e-7; + double lat = asin(sin(lat1) * cos(range_radians) + cos(lat1) * sin(range_radians) * cos(bearing)); + double dlon = atan2(sin(bearing) * sin(range_radians) * cos(lat1), cos(range_radians) - sin(lat1) * sin(lat)); + double lon = fmod(lon1 - dlon + PI, 2 * PI) - PI; - return std::make_shared(double(lat), double(lon), this->getAltitude()); + return std::make_shared(double(lat), double(lon), this->getAltitude()); } /** @@ -493,42 +461,41 @@ std::shared_ptr GeoCoord::pointAtDistance(double bearing, double range * The bearing in string format * @return Bearing in degrees */ -unsigned int GeoCoord::bearingToDegrees(const char *bearing) -{ - if (strcmp(bearing, "N") == 0) - return 0; - else if (strcmp(bearing, "NNE") == 0) - return 22; - else if (strcmp(bearing, "NE") == 0) - return 45; - else if (strcmp(bearing, "ENE") == 0) - return 67; - else if (strcmp(bearing, "E") == 0) - return 90; - else if (strcmp(bearing, "ESE") == 0) - return 112; - else if (strcmp(bearing, "SE") == 0) - return 135; - else if (strcmp(bearing, "SSE") == 0) - return 157; - else if (strcmp(bearing, "S") == 0) - return 180; - else if (strcmp(bearing, "SSW") == 0) - return 202; - else if (strcmp(bearing, "SW") == 0) - return 225; - else if (strcmp(bearing, "WSW") == 0) - return 247; - else if (strcmp(bearing, "W") == 0) - return 270; - else if (strcmp(bearing, "WNW") == 0) - return 292; - else if (strcmp(bearing, "NW") == 0) - return 315; - else if (strcmp(bearing, "NNW") == 0) - return 337; - else - return 0; +unsigned int GeoCoord::bearingToDegrees(const char *bearing) { + if (strcmp(bearing, "N") == 0) + return 0; + else if (strcmp(bearing, "NNE") == 0) + return 22; + else if (strcmp(bearing, "NE") == 0) + return 45; + else if (strcmp(bearing, "ENE") == 0) + return 67; + else if (strcmp(bearing, "E") == 0) + return 90; + else if (strcmp(bearing, "ESE") == 0) + return 112; + else if (strcmp(bearing, "SE") == 0) + return 135; + else if (strcmp(bearing, "SSE") == 0) + return 157; + else if (strcmp(bearing, "S") == 0) + return 180; + else if (strcmp(bearing, "SSW") == 0) + return 202; + else if (strcmp(bearing, "SW") == 0) + return 225; + else if (strcmp(bearing, "WSW") == 0) + return 247; + else if (strcmp(bearing, "W") == 0) + return 270; + else if (strcmp(bearing, "WNW") == 0) + return 292; + else if (strcmp(bearing, "NW") == 0) + return 315; + else if (strcmp(bearing, "NNW") == 0) + return 337; + else + return 0; } /** @@ -537,60 +504,52 @@ unsigned int GeoCoord::bearingToDegrees(const char *bearing) * The bearing in degrees * @return Bearing in string format */ -const char *GeoCoord::degreesToBearing(unsigned int degrees) -{ - if (degrees >= 348 || degrees < 11) - return "N"; - else if (degrees >= 11 && degrees < 34) - return "NNE"; - else if (degrees >= 34 && degrees < 56) - return "NE"; - else if (degrees >= 56 && degrees < 79) - return "ENE"; - else if (degrees >= 79 && degrees < 101) - return "E"; - else if (degrees >= 101 && degrees < 124) - return "ESE"; - else if (degrees >= 124 && degrees < 146) - return "SE"; - else if (degrees >= 146 && degrees < 169) - return "SSE"; - else if (degrees >= 169 && degrees < 191) - return "S"; - else if (degrees >= 191 && degrees < 214) - return "SSW"; - else if (degrees >= 214 && degrees < 236) - return "SW"; - else if (degrees >= 236 && degrees < 259) - return "WSW"; - else if (degrees >= 259 && degrees < 281) - return "W"; - else if (degrees >= 281 && degrees < 304) - return "WNW"; - else if (degrees >= 304 && degrees < 326) - return "NW"; - else if (degrees >= 326 && degrees < 348) - return "NNW"; - else - return "N"; +const char *GeoCoord::degreesToBearing(unsigned int degrees) { + if (degrees >= 348 || degrees < 11) + return "N"; + else if (degrees >= 11 && degrees < 34) + return "NNE"; + else if (degrees >= 34 && degrees < 56) + return "NE"; + else if (degrees >= 56 && degrees < 79) + return "ENE"; + else if (degrees >= 79 && degrees < 101) + return "E"; + else if (degrees >= 101 && degrees < 124) + return "ESE"; + else if (degrees >= 124 && degrees < 146) + return "SE"; + else if (degrees >= 146 && degrees < 169) + return "SSE"; + else if (degrees >= 169 && degrees < 191) + return "S"; + else if (degrees >= 191 && degrees < 214) + return "SSW"; + else if (degrees >= 214 && degrees < 236) + return "SW"; + else if (degrees >= 236 && degrees < 259) + return "WSW"; + else if (degrees >= 259 && degrees < 281) + return "W"; + else if (degrees >= 281 && degrees < 304) + return "WNW"; + else if (degrees >= 304 && degrees < 326) + return "NW"; + else if (degrees >= 326 && degrees < 348) + return "NNW"; + else + return "N"; } -double GeoCoord::pow_neg(double base, double exponent) -{ - if (exponent == 0) { - return 1; - } else if (exponent > 0) { - return pow(base, exponent); - } - return 1 / pow(base, -exponent); +double GeoCoord::pow_neg(double base, double exponent) { + if (exponent == 0) { + return 1; + } else if (exponent > 0) { + return pow(base, exponent); + } + return 1 / pow(base, -exponent); } -double GeoCoord::toRadians(double deg) -{ - return deg * PI / 180; -} +double GeoCoord::toRadians(double deg) { return deg * PI / 180; } -double GeoCoord::toDegrees(double r) -{ - return r * 180 / PI; -} \ No newline at end of file +double GeoCoord::toDegrees(double r) { return r * 180 / PI; } \ No newline at end of file diff --git a/src/gps/GeoCoord.h b/src/gps/GeoCoord.h index 658c177b3..5129c605e 100644 --- a/src/gps/GeoCoord.h +++ b/src/gps/GeoCoord.h @@ -16,133 +16,132 @@ // GeoCoord structs/classes // A struct to hold the data for a DMS coordinate. struct DMS { - uint8_t latDeg; - uint8_t latMin; - uint32_t latSec; - char latCP; - uint8_t lonDeg; - uint8_t lonMin; - uint32_t lonSec; - char lonCP; + uint8_t latDeg; + uint8_t latMin; + uint32_t latSec; + char latCP; + uint8_t lonDeg; + uint8_t lonMin; + uint32_t lonSec; + char lonCP; }; // A struct to hold the data for a UTM coordinate, this is also used when creating an MGRS coordinate. struct UTM { - uint8_t zone; - char band; - uint32_t easting; - uint32_t northing; + uint8_t zone; + char band; + uint32_t easting; + uint32_t northing; }; // A struct to hold the data for a MGRS coordinate. struct MGRS { - uint8_t zone; - char band; - char east100k; - char north100k; - uint32_t easting; - uint32_t northing; + uint8_t zone; + char band; + char east100k; + char north100k; + uint32_t easting; + uint32_t northing; }; // A struct to hold the data for a OSGR coordinate struct OSGR { - char e100k; - char n100k; - uint32_t easting; - uint32_t northing; + char e100k; + char n100k; + uint32_t easting; + uint32_t northing; }; // A struct to hold the data for a OLC coordinate struct OLC { - char code[OLC_CODE_LEN + 1]; // +1 for null termination + char code[OLC_CODE_LEN + 1]; // +1 for null termination }; -class GeoCoord -{ - private: - int32_t _latitude = 0; - int32_t _longitude = 0; - int32_t _altitude = 0; +class GeoCoord { +private: + int32_t _latitude = 0; + int32_t _longitude = 0; + int32_t _altitude = 0; - DMS _dms = {}; - UTM _utm = {}; - MGRS _mgrs = {}; - OSGR _osgr = {}; - OLC _olc = {}; + DMS _dms = {}; + UTM _utm = {}; + MGRS _mgrs = {}; + OSGR _osgr = {}; + OLC _olc = {}; - bool _dirty = true; + bool _dirty = true; - void setCoords(); + void setCoords(); - public: - GeoCoord(); - GeoCoord(int32_t lat, int32_t lon, int32_t alt); - GeoCoord(double lat, double lon, int32_t alt); - GeoCoord(float lat, float lon, int32_t alt); +public: + GeoCoord(); + GeoCoord(int32_t lat, int32_t lon, int32_t alt); + GeoCoord(double lat, double lon, int32_t alt); + GeoCoord(float lat, float lon, int32_t alt); - void updateCoords(const int32_t lat, const int32_t lon, const int32_t alt); - void updateCoords(const double lat, const double lon, const int32_t alt); - void updateCoords(const float lat, const float lon, const int32_t alt); + void updateCoords(const int32_t lat, const int32_t lon, const int32_t alt); + void updateCoords(const double lat, const double lon, const int32_t alt); + void updateCoords(const float lat, const float lon, const int32_t alt); - // Conversions - static void latLongToDMS(const double lat, const double lon, DMS &dms); - static void latLongToUTM(const double lat, const double lon, UTM &utm); - static void latLongToMGRS(const double lat, const double lon, MGRS &mgrs); - static void latLongToOSGR(const double lat, const double lon, OSGR &osgr); - static void latLongToOLC(const double lat, const double lon, OLC &olc); - static void convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude); - static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b); - static float bearing(double lat1, double lon1, double lat2, double lon2); - static float rangeRadiansToMeters(double range_radians); - static float rangeMetersToRadians(double range_meters); - static unsigned int bearingToDegrees(const char *bearing); - static const char *degreesToBearing(unsigned int degrees); + // Conversions + static void latLongToDMS(const double lat, const double lon, DMS &dms); + static void latLongToUTM(const double lat, const double lon, UTM &utm); + static void latLongToMGRS(const double lat, const double lon, MGRS &mgrs); + static void latLongToOSGR(const double lat, const double lon, OSGR &osgr); + static void latLongToOLC(const double lat, const double lon, OLC &olc); + static void convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude); + static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b); + static float bearing(double lat1, double lon1, double lat2, double lon2); + static float rangeRadiansToMeters(double range_radians); + static float rangeMetersToRadians(double range_meters); + static unsigned int bearingToDegrees(const char *bearing); + static const char *degreesToBearing(unsigned int degrees); - // Raises a number to an exponent, handling negative exponents. - static double pow_neg(double base, double exponent); - static double toRadians(double deg); - static double toDegrees(double r); + // Raises a number to an exponent, handling negative exponents. + static double pow_neg(double base, double exponent); + static double toRadians(double deg); + static double toDegrees(double r); - // Point to point conversions - int32_t distanceTo(const GeoCoord &pointB); - int32_t bearingTo(const GeoCoord &pointB); - std::shared_ptr pointAtDistance(double bearing, double range); + // Point to point conversions + int32_t distanceTo(const GeoCoord &pointB); + int32_t bearingTo(const GeoCoord &pointB); + std::shared_ptr pointAtDistance(double bearing, double range); - // Lat lon alt getters - int32_t getLatitude() const { return _latitude; } - int32_t getLongitude() const { return _longitude; } - int32_t getAltitude() const { return _altitude; } + // Lat lon alt getters + int32_t getLatitude() const { return _latitude; } + int32_t getLongitude() const { return _longitude; } + int32_t getAltitude() const { return _altitude; } - // DMS getters - uint8_t getDMSLatDeg() const { return _dms.latDeg; } - uint8_t getDMSLatMin() const { return _dms.latMin; } - uint32_t getDMSLatSec() const { return _dms.latSec; } - char getDMSLatCP() const { return _dms.latCP; } - uint8_t getDMSLonDeg() const { return _dms.lonDeg; } - uint8_t getDMSLonMin() const { return _dms.lonMin; } - uint32_t getDMSLonSec() const { return _dms.lonSec; } - char getDMSLonCP() const { return _dms.lonCP; } + // DMS getters + uint8_t getDMSLatDeg() const { return _dms.latDeg; } + uint8_t getDMSLatMin() const { return _dms.latMin; } + uint32_t getDMSLatSec() const { return _dms.latSec; } + char getDMSLatCP() const { return _dms.latCP; } + uint8_t getDMSLonDeg() const { return _dms.lonDeg; } + uint8_t getDMSLonMin() const { return _dms.lonMin; } + uint32_t getDMSLonSec() const { return _dms.lonSec; } + char getDMSLonCP() const { return _dms.lonCP; } - // UTM getters - uint8_t getUTMZone() const { return _utm.zone; } - char getUTMBand() const { return _utm.band; } - uint32_t getUTMEasting() const { return _utm.easting; } - uint32_t getUTMNorthing() const { return _utm.northing; } + // UTM getters + uint8_t getUTMZone() const { return _utm.zone; } + char getUTMBand() const { return _utm.band; } + uint32_t getUTMEasting() const { return _utm.easting; } + uint32_t getUTMNorthing() const { return _utm.northing; } - // MGRS getters - uint8_t getMGRSZone() const { return _mgrs.zone; } - char getMGRSBand() const { return _mgrs.band; } - char getMGRSEast100k() const { return _mgrs.east100k; } - char getMGRSNorth100k() const { return _mgrs.north100k; } - uint32_t getMGRSEasting() const { return _mgrs.easting; } - uint32_t getMGRSNorthing() const { return _mgrs.northing; } + // MGRS getters + uint8_t getMGRSZone() const { return _mgrs.zone; } + char getMGRSBand() const { return _mgrs.band; } + char getMGRSEast100k() const { return _mgrs.east100k; } + char getMGRSNorth100k() const { return _mgrs.north100k; } + uint32_t getMGRSEasting() const { return _mgrs.easting; } + uint32_t getMGRSNorthing() const { return _mgrs.northing; } - // OSGR getters - char getOSGRE100k() const { return _osgr.e100k; } - char getOSGRN100k() const { return _osgr.n100k; } - uint32_t getOSGREasting() const { return _osgr.easting; } - uint32_t getOSGRNorthing() const { return _osgr.northing; } + // OSGR getters + char getOSGRE100k() const { return _osgr.e100k; } + char getOSGRN100k() const { return _osgr.n100k; } + uint32_t getOSGREasting() const { return _osgr.easting; } + uint32_t getOSGRNorthing() const { return _osgr.northing; } - // OLC getter - void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination + // OLC getter + void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination }; \ No newline at end of file diff --git a/src/gps/NMEAWPL.cpp b/src/gps/NMEAWPL.cpp index f4249ca62..2d6dc094f 100644 --- a/src/gps/NMEAWPL.cpp +++ b/src/gps/NMEAWPL.cpp @@ -19,36 +19,32 @@ * ------------------------------------------- */ -uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_PositionLite &pos, const char *name, bool isCaltopoMode) -{ - GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); - char type = isCaltopoMode ? 'P' : 'N'; - uint32_t len = snprintf(buf, bufsz, "\r\n$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), - (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), - geoCoord.getDMSLonDeg(), (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, - geoCoord.getDMSLonCP(), name); - uint32_t chk = 0; - for (uint32_t i = 1; i < len; i++) { - chk ^= buf[i]; - } - len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); - return len; +uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_PositionLite &pos, const char *name, bool isCaltopoMode) { + GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); + char type = isCaltopoMode ? 'P' : 'N'; + uint32_t len = snprintf(buf, bufsz, "\r\n$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), + (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), + (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), name); + uint32_t chk = 0; + for (uint32_t i = 1; i < len; i++) { + chk ^= buf[i]; + } + len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); + return len; } -uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const char *name, bool isCaltopoMode) -{ - GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); - char type = isCaltopoMode ? 'P' : 'N'; - uint32_t len = snprintf(buf, bufsz, "$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), - (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), - geoCoord.getDMSLonDeg(), (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, - geoCoord.getDMSLonCP(), name); - uint32_t chk = 0; - for (uint32_t i = 1; i < len; i++) { - chk ^= buf[i]; - } - len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); - return len; +uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const char *name, bool isCaltopoMode) { + GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); + char type = isCaltopoMode ? 'P' : 'N'; + uint32_t len = snprintf(buf, bufsz, "$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), + (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), + (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), name); + uint32_t chk = 0; + for (uint32_t i = 1; i < len; i++) { + chk ^= buf[i]; + } + len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); + return len; } /* ------------------------------------------- * 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @@ -66,37 +62,36 @@ uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const * 8 Horizontal Dilution of precision (meters) * 9 Antenna Altitude above/below mean-sea-level (geoid) (in meters) * 10 Units of antenna altitude, meters - * 11 Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level (geoid), "-" means mean-sea-level - * below ellipsoid 12 Units of geoidal separation, meters 13 Age of differential GPS data, time in seconds since last SC104 type 1 - * or 9 update, null field when DGPS is not used 14 Differential reference station ID, 0000-1023 15 Checksum + * 11 Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level (geoid), "-" means + * mean-sea-level below ellipsoid 12 Units of geoidal separation, meters 13 Age of differential GPS data, time in + * seconds since last SC104 type 1 or 9 update, null field when DGPS is not used 14 Differential reference station ID, + * 0000-1023 15 Checksum * ------------------------------------------- */ -uint32_t printGGA(char *buf, size_t bufsz, const meshtastic_Position &pos) -{ - GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); - time_t timestamp = pos.timestamp; +uint32_t printGGA(char *buf, size_t bufsz, const meshtastic_Position &pos) { + GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); + time_t timestamp = pos.timestamp; - tm *t = gmtime(×tamp); - if (getRTCQuality() > 0) { // use the device clock if we got time from somewhere. If not, use the GPS timestamp. - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice); - timestamp = rtc_sec; - t = gmtime(×tamp); - } + tm *t = gmtime(×tamp); + if (getRTCQuality() > 0) { // use the device clock if we got time from somewhere. If not, use the GPS timestamp. + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice); + timestamp = rtc_sec; + t = gmtime(×tamp); + } - uint32_t len = snprintf( - buf, bufsz, "$GNGGA,%02d%02d%02d.%02d,%02d%07.4f,%c,%03d%07.4f,%c,%u,%02u,%04u,%04d,%c,%04d,%c,%d,%04d", t->tm_hour, - t->tm_min, t->tm_sec, pos.timestamp_millis_adjust, geoCoord.getDMSLatDeg(), - (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), - (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), pos.fix_quality, - pos.sats_in_view, pos.HDOP, geoCoord.getAltitude(), 'M', pos.altitude_geoidal_separation, 'M', 0, 0); + uint32_t len = snprintf(buf, bufsz, "$GNGGA,%02d%02d%02d.%02d,%02d%07.4f,%c,%03d%07.4f,%c,%u,%02u,%04u,%04d,%c,%04d,%c,%d,%04d", t->tm_hour, + t->tm_min, t->tm_sec, pos.timestamp_millis_adjust, geoCoord.getDMSLatDeg(), + (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), + (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), pos.fix_quality, + pos.sats_in_view, pos.HDOP, geoCoord.getAltitude(), 'M', pos.altitude_geoidal_separation, 'M', 0, 0); - uint32_t chk = 0; - for (uint32_t i = 1; i < len; i++) { - chk ^= buf[i]; - } - len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); - return len; + uint32_t chk = 0; + for (uint32_t i = 1; i < len; i++) { + chk ^= buf[i]; + } + len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); + return len; } #endif \ No newline at end of file diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 25cd3ceff..8c2efd420 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -12,149 +12,145 @@ uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds -RTCQuality getRTCQuality() -{ - return currentQuality; -} +RTCQuality getRTCQuality() { return currentQuality; } // stuff that really should be in in the instance instead... -static uint32_t - timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time +static uint32_t timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds + // to that time static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock /** * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -RTCSetResult readFromRTC() -{ - struct timeval tv; /* btw settimeofday() is helpful here too*/ +RTCSetResult readFromRTC() { + struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC - if (rtc_found.address == RV3028_RTC) { - uint32_t now = millis(); - Melopero_RV3028 rtc; + if (rtc_found.address == RV3028_RTC) { + uint32_t now = millis(); + Melopero_RV3028 rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.initI2C(); + rtc.initI2C(); #endif - tm t; - t.tm_year = rtc.getYear() - 1900; - t.tm_mon = rtc.getMonth() - 1; - t.tm_mday = rtc.getDate(); - t.tm_hour = rtc.getHour(); - t.tm_min = rtc.getMinute(); - t.tm_sec = rtc.getSecond(); - tv.tv_sec = gm_mktime(&t); - tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + tm t; + t.tm_year = rtc.getYear() - 1900; + t.tm_mon = rtc.getMonth() - 1; + t.tm_mday = rtc.getDate(); + t.tm_hour = rtc.getHour(); + t.tm_min = rtc.getMinute(); + t.tm_sec = rtc.getSecond(); + tv.tv_sec = gm_mktime(&t); + tv.tv_usec = 0; + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - } - return RTCSetResultInvalidTime; - } + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + } + return RTCSetResultInvalidTime; + } #endif - LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, - t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - if (currentQuality == RTCQualityNone) { - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - currentQuality = RTCQualityDevice; - } - return RTCSetResultSuccess; - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, + t.tm_min, t.tm_sec, printableEpoch); + if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + } #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) - if (rtc_found.address == PCF8563_RTC) { + if (rtc_found.address == PCF8563_RTC) { #elif defined(PCF85063_RTC) - if (rtc_found.address == PCF85063_RTC) { + if (rtc_found.address == PCF85063_RTC) { #endif - uint32_t now = millis(); - SensorRtcHelper rtc; + uint32_t now = millis(); + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(Wire); + rtc.begin(Wire); #endif - RTC_DateTime datetime = rtc.getDateTime(); - tm t = datetime.toUnixTime(); - tv.tv_sec = gm_mktime(&t); - tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + RTC_DateTime datetime = rtc.getDateTime(); + tm t = datetime.toUnixTime(); + tv.tv_sec = gm_mktime(&t); + tv.tv_usec = 0; + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } #endif - LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, - t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - if (currentQuality == RTCQualityNone) { - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - currentQuality = RTCQualityDevice; - } - return RTCSetResultSuccess; - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, + t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); + if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + } #elif defined(RX8130CE_RTC) - if (rtc_found.address == RX8130CE_RTC) { - uint32_t now = millis(); + if (rtc_found.address == RX8130CE_RTC) { + uint32_t now = millis(); #ifdef MUZI_BASE - ArtronShop_RX8130CE rtc(&Wire1); + ArtronShop_RX8130CE rtc(&Wire1); #else - ArtronShop_RX8130CE rtc(&Wire); + ArtronShop_RX8130CE rtc(&Wire); #endif - tm t; - if (rtc.getTime(&t)) { - tv.tv_sec = gm_mktime(&t); - tv.tv_usec = 0; + tm t; + if (rtc.getTime(&t)) { + tv.tv_sec = gm_mktime(&t); + tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms - LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, - t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, + t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } -#endif - if (currentQuality == RTCQualityNone) { - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - currentQuality = RTCQualityDevice; - } - return RTCSetResultSuccess; + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); } - } -#else - if (!gettimeofday(&tv, NULL)) { - uint32_t now = millis(); - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms - LOG_DEBUG("Read RTC time as %ld", printableEpoch); + return RTCSetResultInvalidTime; + } +#endif + if (currentQuality == RTCQualityNone) { timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; - return RTCSetResultSuccess; + currentQuality = RTCQualityDevice; + } + return RTCSetResultSuccess; } + } +#else + if (!gettimeofday(&tv, NULL)) { + uint32_t now = millis(); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + LOG_DEBUG("Read RTC time as %ld", printableEpoch); + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + return RTCSetResultSuccess; + } #endif - return RTCSetResultNotSet; + return RTCSetResultNotSet; } /** @@ -166,143 +162,140 @@ RTCSetResult readFromRTC() * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ -RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) -{ - static uint32_t lastSetMsec = 0; - uint32_t now = millis(); - uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms +RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) { + static uint32_t lastSetMsec = 0; + uint32_t now = millis(); + uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv->tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - // Calculate max allowed time safely to avoid overflow in logging - uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; - uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, - (uint32_t)BUILD_EPOCH, maxAllowedPrintable); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; + if (tv->tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); } + return RTCSetResultInvalidTime; + } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, (uint32_t)BUILD_EPOCH, + maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } #endif - bool shouldSet; - if (forceUpdate) { - shouldSet = true; - LOG_DEBUG("Override current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality), - RtcName(q)); - } else if (q > currentQuality) { - shouldSet = true; - LOG_DEBUG("Upgrade time to quality %s", RtcName(q)); - } else if (q == RTCQualityGPS) { - shouldSet = true; - LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch); - } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (12 * 60 * 60 * 1000UL))) { - // Every 12 hrs we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift - shouldSet = true; - LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch); - } else { - shouldSet = false; - LOG_DEBUG("Current RTC quality: %s. Ignore time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); + bool shouldSet; + if (forceUpdate) { + shouldSet = true; + LOG_DEBUG("Override current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); + } else if (q > currentQuality) { + shouldSet = true; + LOG_DEBUG("Upgrade time to quality %s", RtcName(q)); + } else if (q == RTCQualityGPS) { + shouldSet = true; + LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch); + } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (12 * 60 * 60 * 1000UL))) { + // Every 12 hrs we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift + shouldSet = true; + LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch); + } else { + shouldSet = false; + LOG_DEBUG("Current RTC quality: %s. Ignore time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); + } + + if (shouldSet) { + currentQuality = q; + lastSetMsec = now; + if (currentQuality >= RTCQualityNTP) { + lastSetFromPhoneNtpOrGps = now; } - if (shouldSet) { - currentQuality = q; - lastSetMsec = now; - if (currentQuality >= RTCQualityNTP) { - lastSetFromPhoneNtpOrGps = now; - } - - // This delta value works on all platforms - timeStartMsec = now; - zeroOffsetSecs = tv->tv_sec; - // If this platform has a setable RTC, set it + // This delta value works on all platforms + timeStartMsec = now; + zeroOffsetSecs = tv->tv_sec; + // If this platform has a setable RTC, set it #ifdef RV3028_RTC - if (rtc_found.address == RV3028_RTC) { - Melopero_RV3028 rtc; + if (rtc_found.address == RV3028_RTC) { + Melopero_RV3028 rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.initI2C(); + rtc.initI2C(); #endif - tm *t = gmtime(&tv->tv_sec); - rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); - LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, - t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); - } + tm *t = gmtime(&tv->tv_sec); + rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); + LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, + t->tm_sec, printableEpoch); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + } #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) - if (rtc_found.address == PCF8563_RTC) { + if (rtc_found.address == PCF8563_RTC) { #elif defined(PCF85063_RTC) - if (rtc_found.address == PCF85063_RTC) { + if (rtc_found.address == PCF85063_RTC) { #endif - SensorRtcHelper rtc; + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(Wire); + rtc.begin(Wire); #endif - tm *t = gmtime(&tv->tv_sec); - rtc.setDateTime(*t); - LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, - t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); - } -#elif defined(RX8130CE_RTC) - if (rtc_found.address == RX8130CE_RTC) { -#ifdef MUZI_BASE - ArtronShop_RX8130CE rtc(&Wire1); -#else - ArtronShop_RX8130CE rtc(&Wire); -#endif - tm *t = gmtime(&tv->tv_sec); - if (rtc.setTime(*t)) { - LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, - t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); - } else { - LOG_WARN("Failed to set time for RX8130CE"); - } - } -#elif defined(ARCH_ESP32) - settimeofday(tv, NULL); -#endif - - // nrf52 doesn't have a readable RTC (yet - software not written) -#if HAS_RTC - readFromRTC(); -#endif - - return RTCSetResultSuccess; + tm *t = gmtime(&tv->tv_sec); + rtc.setDateTime(*t); + LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, + t->tm_min, t->tm_sec, printableEpoch); } else { - return RTCSetResultNotSet; // RTC was already set with a higher quality time + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } +#elif defined(RX8130CE_RTC) + if (rtc_found.address == RX8130CE_RTC) { +#ifdef MUZI_BASE + ArtronShop_RX8130CE rtc(&Wire1); +#else + ArtronShop_RX8130CE rtc(&Wire); +#endif + tm *t = gmtime(&tv->tv_sec); + if (rtc.setTime(*t)) { + LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, + t->tm_sec, printableEpoch); + } else { + LOG_WARN("Failed to set time for RX8130CE"); + } + } +#elif defined(ARCH_ESP32) + settimeofday(tv, NULL); +#endif + + // nrf52 doesn't have a readable RTC (yet - software not written) +#if HAS_RTC + readFromRTC(); +#endif + + return RTCSetResultSuccess; + } else { + return RTCSetResultNotSet; // RTC was already set with a higher quality time + } } -const char *RtcName(RTCQuality quality) -{ - switch (quality) { - case RTCQualityNone: - return "None"; - case RTCQualityDevice: - return "Device"; - case RTCQualityFromNet: - return "Net"; - case RTCQualityNTP: - return "NTP"; - case RTCQualityGPS: - return "GPS"; - default: - return "Unknown"; - } +const char *RtcName(RTCQuality quality) { + switch (quality) { + case RTCQualityNone: + return "None"; + case RTCQualityDevice: + return "Device"; + case RTCQualityFromNet: + return "Net"; + case RTCQualityNTP: + return "NTP"; + case RTCQualityGPS: + return "GPS"; + default: + return "Unknown"; + } } /** @@ -312,46 +305,45 @@ const char *RtcName(RTCQuality quality) * @param t The time to potentially set the RTC to. * @return True if the RTC was set to the provided time, false otherwise. */ -RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) -{ - /* Convert to unix time - The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 - (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). - */ - // horrible hack to make mktime TZ agnostic - best practise according to - // https://www.gnu.org/software/libc/manual/html_node/Broken_002ddown-Time.html - time_t res = gm_mktime(&t); - struct timeval tv; - tv.tv_sec = res; - tv.tv_usec = 0; // time.centisecond() * (10 / 1000); - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms +RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) { + /* Convert to unix time + The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January + 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). + */ + // horrible hack to make mktime TZ agnostic - best practise according to + // https://www.gnu.org/software/libc/manual/html_node/Broken_002ddown-Time.html + time_t res = gm_mktime(&t); + struct timeval tv; + tv.tv_sec = res; + tv.tv_usec = 0; // time.centisecond() * (10 / 1000); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - // Calculate max allowed time safely to avoid overflow in logging - uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; - uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; - LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch, - (uint32_t)BUILD_EPOCH, maxAllowedPrintable); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); } + return RTCSetResultInvalidTime; + } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch, (uint32_t)BUILD_EPOCH, + maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } #endif - // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); - if (t.tm_year < 0 || t.tm_year >= 300) { - // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); - return RTCSetResultInvalidTime; - } else { - return perhapsSetRTC(q, &tv); - } + // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); + if (t.tm_year < 0 || t.tm_year >= 300) { + // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); + return RTCSetResultInvalidTime; + } else { + return perhapsSetRTC(q, &tv); + } } /** @@ -359,16 +351,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) * * @return The timezone offset in seconds. */ -int32_t getTZOffset() -{ +int32_t getTZOffset() { #if MESHTASTIC_EXCLUDE_TZ - return 0; + return 0; #else - time_t now = getTime(false); - struct tm *gmt; - gmt = gmtime(&now); - gmt->tm_isdst = -1; - return (int32_t)difftime(now, mktime(gmt)); + time_t now = getTime(false); + struct tm *gmt; + gmt = gmtime(&now); + gmt->tm_isdst = -1; + return (int32_t)difftime(now, mktime(gmt)); #endif } @@ -377,13 +368,12 @@ int32_t getTZOffset() * * @return The current time in seconds since the Unix epoch. */ -uint32_t getTime(bool local) -{ - if (local) { - return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs + getTZOffset(); - } else { - return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs; - } +uint32_t getTime(bool local) { + if (local) { + return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs + getTZOffset(); + } else { + return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs; + } } /** @@ -392,49 +382,45 @@ uint32_t getTime(bool local) * @param minQuality The minimum quality of the RTC time required for it to be considered valid. * @return The current time from the RTC if it meets the minimum quality requirement, or 0 if the time is not valid. */ -uint32_t getValidTime(RTCQuality minQuality, bool local) -{ - return (currentQuality >= minQuality) ? getTime(local) : 0; -} +uint32_t getValidTime(RTCQuality minQuality, bool local) { return (currentQuality >= minQuality) ? getTime(local) : 0; } -time_t gm_mktime(struct tm *tm) -{ +time_t gm_mktime(struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ - time_t result = 0; + time_t result = 0; - // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. - int year = 1900 + tm->tm_year; // tm_year is years since 1900 - int year_minus_one = year - 1; - int days_before_this_year = 0; - days_before_this_year += year_minus_one * 365; - // leap days: every 4 years, except 100s, but including 400s. - days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; - // subtract from 1970-01-01 to get days since epoch - days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); - // Now, within this tm->year, compute the days *before* this tm->month starts. - int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year - int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + // Now, within this tm->year, compute the days *before* this tm->month starts. + int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 - // If this is a leap year, and we're past February, add a day: - if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { - days_this_year_before_this_month += 1; - } + // If this is a leap year, and we're past February, add a day: + if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { + days_this_year_before_this_month += 1; + } - // And within this month: - int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 - // Now combine them all together, and convert days to seconds: - result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); - result *= 86400L; + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; - // Finally, add in the hours, minutes, and seconds of today: - result += tm->tm_hour * 3600; - result += tm->tm_min * 60; - result += tm->tm_sec; + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; - return result; + return result; #else - return mktime(tm); + return mktime(tm); #endif } diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 06dd34c16..efdc677f0 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -10,29 +10,29 @@ enum RTCQuality { - /// We haven't had our RTC set yet - RTCQualityNone = 0, + /// We haven't had our RTC set yet + RTCQualityNone = 0, - /// We got time from an onboard peripheral after boot. - RTCQualityDevice = 1, + /// We got time from an onboard peripheral after boot. + RTCQualityDevice = 1, - /// Some other node gave us a time we can use - RTCQualityFromNet = 2, + /// Some other node gave us a time we can use + RTCQualityFromNet = 2, - /// Our time is based on NTP - RTCQualityNTP = 3, + /// Our time is based on NTP + RTCQualityNTP = 3, - /// Our time is based on our own GPS - RTCQualityGPS = 4 + /// Our time is based on our own GPS + RTCQualityGPS = 4 }; /// The RTC set result codes /// Used to indicate the result of an attempt to set the RTC. enum RTCSetResult { - RTCSetResultNotSet = 0, ///< RTC was set successfully - RTCSetResultSuccess = 1, ///< RTC was set successfully - RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) - RTCSetResultError = 4 ///< An error occurred while setting the RTC + RTCSetResultNotSet = 0, ///< RTC was set successfully + RTCSetResultSuccess = 1, ///< RTC was set successfully + RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) + RTCSetResultError = 4 ///< An error occurred while setting the RTC }; RTCQuality getRTCQuality(); diff --git a/src/gps/ubx.h b/src/gps/ubx.h index 0fe2f01fb..8bc0b091e 100644 --- a/src/gps/ubx.h +++ b/src/gps/ubx.h @@ -1,13 +1,13 @@ static const char *failMessage = "Unable to %s"; -#define SEND_UBX_PACKET(TYPE, ID, DATA, ERRMSG, TIMEOUT) \ - do { \ - msglen = makeUBXPacket(TYPE, ID, sizeof(DATA), DATA); \ - _serial_gps->write(UBXscratch, msglen); \ - if (getACK(TYPE, ID, TIMEOUT) != GNSS_RESPONSE_OK) { \ - LOG_WARN(failMessage, #ERRMSG); \ - } \ - } while (0) +#define SEND_UBX_PACKET(TYPE, ID, DATA, ERRMSG, TIMEOUT) \ + do { \ + msglen = makeUBXPacket(TYPE, ID, sizeof(DATA), DATA); \ + _serial_gps->write(UBXscratch, msglen); \ + if (getACK(TYPE, ID, TIMEOUT) != GNSS_RESPONSE_OK) { \ + LOG_WARN(failMessage, #ERRMSG); \ + } \ + } while (0) // Power Management @@ -337,8 +337,8 @@ static const uint8_t _message_SAVE_10[] = { // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. // BBR will survive a restart, and power off for a while, but modules with small backup // batteries or super caps will not retain the config for a long power off time. -// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast aquisition after -// sleep +// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast +// aquisition after sleep // VALSET Commands for M10 // Please refer to the M10 Protocol Specification: @@ -370,11 +370,13 @@ EXTINTACTIVITY U4 0 no ext ints LIMITPEAKCURRENT L 1 // Ram layer config message: -// b5 62 06 8a 26 00 00 01 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 10 00 d0 +// b5 62 06 8a 26 00 00 01 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 +10 00 d0 // 10 01 8b de // BBR layer config message: -// b5 62 06 8a 26 00 00 02 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 10 00 d0 +// b5 62 06 8a 26 00 00 02 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 +10 00 d0 // 10 01 8c 03 */ static const uint8_t _message_VALSET_PM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xd0, 0x20, 0x02, 0x02, 0x00, 0xd0, 0x40, @@ -396,21 +398,21 @@ CFG-ITFM replaced by 5 valset messages which can be combined into one for RAM an b5 62 06 8a 0e 00 00 01 00 00 0d 00 41 10 01 13 00 41 10 01 63 c6 */ -static const uint8_t _message_VALSET_ITFM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x41, - 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; -static const uint8_t _message_VALSET_ITFM_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x41, - 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; +static const uint8_t _message_VALSET_ITFM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x41, 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; +static const uint8_t _message_VALSET_ITFM_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x41, 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; // Turn off all NMEA messages: // Ram layer config message: -// b5 62 06 8a 22 00 00 01 00 00 c0 00 91 20 00 ca 00 91 20 00 c5 00 91 20 00 ac 00 91 20 00 b1 00 91 20 00 bb 00 91 20 00 40 8f +// b5 62 06 8a 22 00 00 01 00 00 c0 00 91 20 00 ca 00 91 20 00 c5 00 91 20 00 ac 00 91 20 00 b1 00 91 20 00 bb 00 91 20 +// 00 40 8f // Disable GLL, GSV, VTG messages in BBR layer // BBR layer config message: // b5 62 06 8a 13 00 00 02 00 00 ca 00 91 20 00 c5 00 91 20 00 b1 00 91 20 00 f8 4e static const uint8_t _message_VALSET_DISABLE_NMEA_RAM[] = { - /*0x00, 0x01, 0x00, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00 */ + /*0x00, 0x01, 0x00, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00 + */ 0x00, 0x01, 0x00, 0x00, 0xc0, 0x00, 0x91, 0x20, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xac, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x00}; @@ -437,14 +439,10 @@ static const uint8_t _message_VALSET_DISABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x0 static const uint8_t _message_VALSET_DISABLE_TXT_INFO_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; static const uint8_t _message_VALSET_DISABLE_TXT_INFO_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; -static const uint8_t _message_VALSET_ENABLE_NMEA_RAM[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, - 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; -static const uint8_t _message_VALSET_ENABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, - 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; -static const uint8_t _message_VALSET_DISABLE_SBAS_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x20, 0x00, 0x31, - 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; -static const uint8_t _message_VALSET_DISABLE_SBAS_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x31, - 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; +static const uint8_t _message_VALSET_ENABLE_NMEA_RAM[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; +static const uint8_t _message_VALSET_ENABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; +static const uint8_t _message_VALSET_DISABLE_SBAS_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x20, 0x00, 0x31, 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; +static const uint8_t _message_VALSET_DISABLE_SBAS_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x31, 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; /* Operational issues with the M10: diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 4209baf5d..920bf82e2 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -33,252 +33,243 @@ */ // Constructor -EInkDisplay::EInkDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) -{ - // Set dimensions in OLEDDisplay base class - this->geometry = GEOMETRY_RAWMODE; - this->displayWidth = EINK_WIDTH; - this->displayHeight = EINK_HEIGHT; +EInkDisplay::EInkDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) { + // Set dimensions in OLEDDisplay base class + this->geometry = GEOMETRY_RAWMODE; + this->displayWidth = EINK_WIDTH; + this->displayHeight = EINK_HEIGHT; - // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer - uint16_t shortSide = min(EINK_WIDTH, EINK_HEIGHT); - uint16_t longSide = max(EINK_WIDTH, EINK_HEIGHT); - if (shortSide % 8 != 0) - shortSide = (shortSide | 7) + 1; + // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer + uint16_t shortSide = min(EINK_WIDTH, EINK_HEIGHT); + uint16_t longSide = max(EINK_WIDTH, EINK_HEIGHT); + if (shortSide % 8 != 0) + shortSide = (shortSide | 7) + 1; - this->displayBufferSize = longSide * (shortSide / 8); + this->displayBufferSize = longSide * (shortSide / 8); } /** * Force a display update if we haven't drawn within the specified msecLimit */ -bool EInkDisplay::forceDisplay(uint32_t msecLimit) -{ - // No need to grab this lock because we are on our own SPI bus - // concurrency::LockGuard g(spiLock); +bool EInkDisplay::forceDisplay(uint32_t msecLimit) { + // No need to grab this lock because we are on our own SPI bus + // concurrency::LockGuard g(spiLock); - uint32_t now = millis(); - uint32_t sinceLast = now - lastDrawMsec; + uint32_t now = millis(); + uint32_t sinceLast = now - lastDrawMsec; - if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) - lastDrawMsec = now; - else - return false; + if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) + lastDrawMsec = now; + else + return false; - // FIXME - only draw bits have changed (use backbuf similar to the other displays) - const bool flipped = config.display.flip_screen; - // HACK for L1 EInk + // FIXME - only draw bits have changed (use backbuf similar to the other displays) + const bool flipped = config.display.flip_screen; + // HACK for L1 EInk #if defined(SEEED_WIO_TRACKER_L1_EINK) - // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes - for (uint32_t y = 0; y < displayHeight; y++) { - for (uint32_t x = 0; x < displayWidth; x++) { - auto b = buffer[x + (y / 8) * displayWidth]; - auto isset = b & (1 << (y & 7)); - adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); - } + // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); } + } #else - for (uint32_t y = 0; y < displayHeight; y++) { - for (uint32_t x = 0; x < displayWidth; x++) { - auto b = buffer[x + (y / 8) * displayWidth]; - auto isset = b & (1 << (y & 7)); - if (flipped) - adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); - else - adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); - } + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + if (flipped) + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + else + adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); } + } #endif - // Trigger the refresh in GxEPD2 - LOG_DEBUG("Update E-Paper"); - adafruitDisplay->nextPage(); + // Trigger the refresh in GxEPD2 + LOG_DEBUG("Update E-Paper"); + adafruitDisplay->nextPage(); - // End the update process - endUpdate(); + // End the update process + endUpdate(); - LOG_DEBUG("done"); - return true; + LOG_DEBUG("done"); + return true; } // End the update process - virtual method, overriden in derived class -void EInkDisplay::endUpdate() -{ - // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) - adafruitDisplay->hibernate(); +void EInkDisplay::endUpdate() { + // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) + adafruitDisplay->hibernate(); } // Write the buffer to the display memory -void EInkDisplay::display(void) -{ - // We don't allow regular 'dumb' display() calls to draw on eink until we've shown - // at least one forceDisplay() keyframe. This prevents flashing when we should the critical - // bootscreen (that we want to look nice) +void EInkDisplay::display(void) { + // We don't allow regular 'dumb' display() calls to draw on eink until we've shown + // at least one forceDisplay() keyframe. This prevents flashing when we should the critical + // bootscreen (that we want to look nice) - if (lastDrawMsec) { - forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower - } + if (lastDrawMsec) { + forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower + } } // Send a command to the display (low level function) -void EInkDisplay::sendCommand(uint8_t com) -{ - (void)com; - // Drop all commands to device (we just update the buffer) +void EInkDisplay::sendCommand(uint8_t com) { + (void)com; + // Drop all commands to device (we just update the buffer) } -void EInkDisplay::setDetected(uint8_t detected) -{ - (void)detected; -} +void EInkDisplay::setDetected(uint8_t detected) { (void)detected; } // Connect to the display - variant specific -bool EInkDisplay::connect() -{ - LOG_INFO("Do EInk init"); +bool EInkDisplay::connect() { + LOG_INFO("Do EInk init"); #ifdef PIN_EINK_EN - // backlight power, HIGH is backlight on, LOW is off - pinMode(PIN_EINK_EN, OUTPUT); + // backlight power, HIGH is backlight on, LOW is off + pinMode(PIN_EINK_EN, OUTPUT); #ifdef ELECROW_ThinkNode_M1 - // ThinkNode M1 has a hardware dimmable backlight. Start enabled - digitalWrite(PIN_EINK_EN, HIGH); + // ThinkNode M1 has a hardware dimmable backlight. Start enabled + digitalWrite(PIN_EINK_EN, HIGH); #else - digitalWrite(PIN_EINK_EN, LOW); + digitalWrite(PIN_EINK_EN, LOW); #endif #endif #if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(); -#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) - adafruitDisplay->setRotation(4); -#else - adafruitDisplay->setRotation(3); -#endif - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } -#elif defined(ELECROW_ThinkNode_M5) - { - // Start HSPI - hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); - - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(); - - adafruitDisplay->setRotation(4); - - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } -#elif defined(MESHLINK) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); - - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(); - adafruitDisplay->setRotation(3); - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } -#elif defined(RAK4630) || defined(MAKERPYTHON) - { - if (eink_found) { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 10, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - // RAK14000 2.13 inch b/w 250x122 does actually now support fast refresh - adafruitDisplay->setRotation(3); - // Fast refresh support for 1.54, 2.13 RAK14000 b/w , 2.9 and 4.2 - // adafruitDisplay->setRotation(1); - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } else { - (void)adafruitDisplay; - } - } - -#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ - defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) - { - // Start HSPI - hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - // VExt already enabled in setup() - // RTC GPIO hold disabled in setup() - - // Create GxEPD2 objects - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - - // Init GxEPD2 - adafruitDisplay->init(); - adafruitDisplay->setRotation(3); -#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) - adafruitDisplay->setRotation(0); -#endif - } -#elif defined(PCA10059) || defined(ME25LS01) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 40, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - adafruitDisplay->setRotation(0); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); - } -#elif defined(M5_COREINK) || defined(T_DECK_PRO) - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - adafruitDisplay->setRotation(0); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); -#elif defined(my) || defined(ESP32_S3_PICO) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - adafruitDisplay->setRotation(1); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); - } -#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) - { - spi1 = &SPI1; - spi1->begin(); - // VExt already enabled in setup() - // RTC GPIO hold disabled in setup() - - // Create GxEPD2 objects - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - - // Init GxEPD2 - adafruitDisplay->init(); - adafruitDisplay->setRotation(3); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); - } -#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) - - // Detect display model, before starting SPI - EInkDetectionResult displayModel = detectEInk(); - + adafruitDisplay->init(); +#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) + adafruitDisplay->setRotation(4); +#else + adafruitDisplay->setRotation(3); +#endif + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } +#elif defined(ELECROW_ThinkNode_M5) + { // Start HSPI hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - // Create GxEPD2 object - adafruitDisplay = new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, - PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(); + + adafruitDisplay->setRotation(4); + + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } +#elif defined(MESHLINK) + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); + + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } +#elif defined(RAK4630) || defined(MAKERPYTHON) + { + if (eink_found) { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 10, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + // RAK14000 2.13 inch b/w 250x122 does actually now support fast refresh + adafruitDisplay->setRotation(3); + // Fast refresh support for 1.54, 2.13 RAK14000 b/w , 2.9 and 4.2 + // adafruitDisplay->setRotation(1); + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } else { + (void)adafruitDisplay; + } + } + +#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ + defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) + { + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + // VExt already enabled in setup() + // RTC GPIO hold disabled in setup() + + // Create GxEPD2 objects + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + adafruitDisplay = new GxEPD2_BW(*lowLevel); // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); +#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) + adafruitDisplay->setRotation(0); +#endif + } +#elif defined(PCA10059) || defined(ME25LS01) + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 40, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + adafruitDisplay->setRotation(0); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); + } +#elif defined(M5_COREINK) || defined(T_DECK_PRO) + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + adafruitDisplay->setRotation(0); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); +#elif defined(my) || defined(ESP32_S3_PICO) + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + adafruitDisplay->setRotation(1); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); + } +#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) + { + spi1 = &SPI1; + spi1->begin(); + // VExt already enabled in setup() + // RTC GPIO hold disabled in setup() + + // Create GxEPD2 objects + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); + } +#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) + + // Detect display model, before starting SPI + EInkDetectionResult displayModel = detectEInk(); + + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + + // Create GxEPD2 object + adafruitDisplay = + new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); #endif - return true; + return true; } #endif diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 9975527aa..5be061e90 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -22,75 +22,74 @@ * turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted? * Suggestion: perhaps similar to HELTEC_WIRELESS_PAPER issue, which resolved with rtc_gpio_hold_dis() */ -class EInkDisplay : public OLEDDisplay -{ - /// How often should we update the display - /// thereafter we do once per 5 minutes - uint32_t slowUpdateMsec = 5 * 60 * 1000; +class EInkDisplay : public OLEDDisplay { + /// How often should we update the display + /// thereafter we do once per 5 minutes + uint32_t slowUpdateMsec = 5 * 60 * 1000; - public: - /* constructor - FIXME - the parameters are not used, just a temporary hack to keep working like the old displays - */ - EInkDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); +public: + /* constructor + FIXME - the parameters are not used, just a temporary hack to keep working like the old displays + */ + EInkDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); - // Write the buffer to the display memory (for eink we only do this occasionally) - virtual void display(void) override; + // Write the buffer to the display memory (for eink we only do this occasionally) + virtual void display(void) override; - /** - * Force a display update if we haven't drawn within the specified msecLimit - * - * @return true if we did draw the screen - */ - virtual bool forceDisplay(uint32_t msecLimit = 1000); + /** + * Force a display update if we haven't drawn within the specified msecLimit + * + * @return true if we did draw the screen + */ + virtual bool forceDisplay(uint32_t msecLimit = 1000); - /** - * Run any code needed to complete an update, after the physical refresh has completed. - * Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class. - * - */ - virtual void endUpdate(); + /** + * Run any code needed to complete an update, after the physical refresh has completed. + * Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class. + * + */ + virtual void endUpdate(); - /** - * shim to make the abstraction happy - * - */ - void setDetected(uint8_t detected); + /** + * shim to make the abstraction happy + * + */ + void setDetected(uint8_t detected); - protected: - // the header size of the buffer used, e.g. for the SPI command header - virtual int getBufferOffset(void) override { return 0; } +protected: + // the header size of the buffer used, e.g. for the SPI command header + virtual int getBufferOffset(void) override { return 0; } - // Send a command to the display (low level function) - virtual void sendCommand(uint8_t com) override; + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) override; - // Connect to the display - virtual bool connect() override; + // Connect to the display + virtual bool connect() override; #ifdef GXEPD2_DRIVER_0 - // AdafruitGFX display object - wrapper for multiple drivers - // Allows runtime detection of multiple displays - // Avoid this situation if possible! - GxEPD2_Multi *adafruitDisplay = NULL; + // AdafruitGFX display object - wrapper for multiple drivers + // Allows runtime detection of multiple displays + // Avoid this situation if possible! + GxEPD2_Multi *adafruitDisplay = NULL; #else - // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific - GxEPD2_BW *adafruitDisplay = NULL; + // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific + GxEPD2_BW *adafruitDisplay = NULL; #endif - // If display uses HSPI -#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ - defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ + // If display uses HSPI +#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ + defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) - SPIClass *hspi = NULL; + SPIClass *hspi = NULL; #endif #if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) - SPIClass *spi1 = NULL; + SPIClass *spi1 = NULL; #endif - private: - // FIXME quick hack to limit drawing to a very slow rate - uint32_t lastDrawMsec = 0; +private: + // FIXME quick hack to limit drawing to a very slow rate + uint32_t lastDrawMsec = 0; }; #endif diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp index 8e4adf87e..b98c0f369 100644 --- a/src/graphics/EInkDynamicDisplay.cpp +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -6,558 +6,524 @@ // Constructor EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) - : EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay") -{ - // If tracking ghost pixels, grab memory + : EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay") { + // If tracking ghost pixels, grab memory #ifdef EINK_LIMIT_GHOSTING_PX - dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros + dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros #endif } // Destructor -EInkDynamicDisplay::~EInkDynamicDisplay() -{ - // If we were tracking ghost pixels, free the memory +EInkDynamicDisplay::~EInkDynamicDisplay() { + // If we were tracking ghost pixels, free the memory #ifdef EINK_LIMIT_GHOSTING_PX - delete[] dirtyPixels; + delete[] dirtyPixels; #endif } // Screen requests a BACKGROUND frame -void EInkDynamicDisplay::display() -{ - addFrameFlag(BACKGROUND); - update(); +void EInkDynamicDisplay::display() { + addFrameFlag(BACKGROUND); + update(); } // Screen requests a RESPONSIVE frame -bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit) -{ - addFrameFlag(RESPONSIVE); - return update(); // (Unutilized) Base class promises to return true if update ran +bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit) { + addFrameFlag(RESPONSIVE); + return update(); // (Unutilized) Base class promises to return true if update ran } // Add flag for the next frame -void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag) -{ - // OR the new flag into the existing flags - this->frameFlags = (frameFlagTypes)(this->frameFlags | flag); +void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag) { + // OR the new flag into the existing flags + this->frameFlags = (frameFlagTypes)(this->frameFlags | flag); } // GxEPD2 code to set fast refresh -void EInkDynamicDisplay::configForFastRefresh() -{ - // Variant-specific code can go here +void EInkDynamicDisplay::configForFastRefresh() { + // Variant-specific code can go here #if defined(PRIVATE_HW) #else - // Otherwise: - adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); + // Otherwise: + adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); #endif } // GxEPD2 code to set full refresh -void EInkDynamicDisplay::configForFullRefresh() -{ - // Variant-specific code can go here +void EInkDynamicDisplay::configForFullRefresh() { + // Variant-specific code can go here #if defined(PRIVATE_HW) #else - // Otherwise: - adafruitDisplay->setFullWindow(); + // Otherwise: + adafruitDisplay->setFullWindow(); #endif } // Run any relevant GxEPD2 code, so next update will use correct refresh type -void EInkDynamicDisplay::applyRefreshMode() -{ - // Change from FULL to FAST - if (currentConfig == FULL && refresh == FAST) { - configForFastRefresh(); - currentConfig = FAST; - } +void EInkDynamicDisplay::applyRefreshMode() { + // Change from FULL to FAST + if (currentConfig == FULL && refresh == FAST) { + configForFastRefresh(); + currentConfig = FAST; + } - // Change from FAST back to FULL - else if (currentConfig == FAST && refresh == FULL) { - configForFullRefresh(); - currentConfig = FULL; - } + // Change from FAST back to FULL + else if (currentConfig == FAST && refresh == FULL) { + configForFullRefresh(); + currentConfig = FULL; + } } // Update fastRefreshCount -void EInkDynamicDisplay::adjustRefreshCounters() -{ - if (refresh == FAST) - fastRefreshCount++; +void EInkDynamicDisplay::adjustRefreshCounters() { + if (refresh == FAST) + fastRefreshCount++; - else if (refresh == FULL) - fastRefreshCount = 0; + else if (refresh == FULL) + fastRefreshCount = 0; } // Trigger the display update by calling base class -bool EInkDynamicDisplay::update() -{ - // Detemine the refresh mode to use, and start the update - bool refreshApproved = determineMode(); - if (refreshApproved) { - EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system - storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach() - endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL) - } else - storeAndReset(); // No update, no post-update code, just store the results +bool EInkDynamicDisplay::update() { + // Detemine the refresh mode to use, and start the update + bool refreshApproved = determineMode(); + if (refreshApproved) { + EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system + storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach() + endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL) + } else + storeAndReset(); // No update, no post-update code, just store the results - return refreshApproved; // (Unutilized) Base class promises to return true if update ran + return refreshApproved; // (Unutilized) Base class promises to return true if update ran } // Figure out who runs the post-update code -void EInkDynamicDisplay::endOrDetach() -{ - // If the GxEPD2 version reports that it has the async modifications +void EInkDynamicDisplay::endOrDetach() { + // If the GxEPD2 version reports that it has the async modifications #ifdef HAS_EINK_ASYNCFULL - if (previousRefresh == FULL) { - asyncRefreshRunning = true; // Set the flag - checked in determineMode(); cleared by onNotify() + if (previousRefresh == FULL) { + asyncRefreshRunning = true; // Set the flag - checked in determineMode(); cleared by onNotify() - if (previousFrameFlags & BLOCKING) - awaitRefresh(); - else { - // Async begins - LOG_DEBUG("Async full-refresh begins (drop frames)"); - notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread - } + if (previousFrameFlags & BLOCKING) + awaitRefresh(); + else { + // Async begins + LOG_DEBUG("Async full-refresh begins (drop frames)"); + notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread } + } - // Fast Refresh - else if (previousRefresh == FAST) - EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves. + // Fast Refresh + else if (previousRefresh == FAST) + EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves. - // Fallback - If using an unmodified version of GxEPD2 for some reason + // Fallback - If using an unmodified version of GxEPD2 for some reason #else - if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..) - LOG_WARN( - "GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update lib_deps in " - "variant's platformio.ini file"); - EInkDisplay::endUpdate(); - } + if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..) + LOG_WARN("GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update " + "lib_deps in " + "variant's platformio.ini file"); + EInkDisplay::endUpdate(); + } #endif } // Assess situation, pick a refresh type -bool EInkDynamicDisplay::determineMode() -{ - checkInitialized(); - checkForPromotion(); +bool EInkDynamicDisplay::determineMode() { + checkInitialized(); + checkForPromotion(); #if defined(HAS_EINK_ASYNCFULL) - checkBusyAsyncRefresh(); + checkBusyAsyncRefresh(); #endif - checkRateLimiting(); + checkRateLimiting(); - // If too soon for a new frame, or display busy, abort early - if (refresh == SKIPPED) - return false; // No refresh + // If too soon for a new frame, or display busy, abort early + if (refresh == SKIPPED) + return false; // No refresh - // -- New frame is due -- + // -- New frame is due -- - resetRateLimiting(); // Once determineMode() ends, will have to wait again - hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check - LOG_DEBUG("determineMode(): "); // Begin log entry + resetRateLimiting(); // Once determineMode() ends, will have to wait again + hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check + LOG_DEBUG("determineMode(): "); // Begin log entry - // Once mode determined, any remaining checks will bypass - checkCosmetic(); - checkDemandingFast(); - checkFrameMatchesPrevious(); - checkConsecutiveFastRefreshes(); + // Once mode determined, any remaining checks will bypass + checkCosmetic(); + checkDemandingFast(); + checkFrameMatchesPrevious(); + checkConsecutiveFastRefreshes(); #ifdef EINK_LIMIT_GHOSTING_PX - checkExcessiveGhosting(); + checkExcessiveGhosting(); #endif - checkFastRequested(); + checkFastRequested(); - if (refresh == UNSPECIFIED) - LOG_WARN("There was a flaw in the determineMode() logic"); + if (refresh == UNSPECIFIED) + LOG_WARN("There was a flaw in the determineMode() logic"); - // -- Decision has been reached -- - applyRefreshMode(); - adjustRefreshCounters(); + // -- Decision has been reached -- + applyRefreshMode(); + adjustRefreshCounters(); #ifdef EINK_LIMIT_GHOSTING_PX - // Full refresh clears any ghosting - if (refresh == FULL) - resetGhostPixelTracking(); + // Full refresh clears any ghosting + if (refresh == FULL) + resetGhostPixelTracking(); #endif - // Return - call a refresh or not? - if (refresh == SKIPPED) - return false; // Don't trigger a refresh - else - return true; // Do trigger a refresh + // Return - call a refresh or not? + if (refresh == SKIPPED) + return false; // Don't trigger a refresh + else + return true; // Do trigger a refresh } // Is this the very first frame? -void EInkDynamicDisplay::checkInitialized() -{ - if (!initialized) { - // Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect() - configForFullRefresh(); +void EInkDynamicDisplay::checkInitialized() { + if (!initialized) { + // Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect() + configForFullRefresh(); - // Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write - adafruitDisplay->clearScreen(); + // Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write + adafruitDisplay->clearScreen(); - LOG_DEBUG("initialized, "); - initialized = true; + LOG_DEBUG("initialized, "); + initialized = true; - // Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep - addFrameFlag(DEMAND_FAST); - } + // Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep + addFrameFlag(DEMAND_FAST); + } } // Was a frame skipped (rate, display busy) that should have been a FAST refresh? -void EInkDynamicDisplay::checkForPromotion() -{ - // If a frame was skipped (rate, display busy), then promote a BACKGROUND frame - // Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it +void EInkDynamicDisplay::checkForPromotion() { + // If a frame was skipped (rate, display busy), then promote a BACKGROUND frame + // Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it - switch (previousReason) { - case ASYNC_REFRESH_BLOCKED_DEMANDFAST: - addFrameFlag(DEMAND_FAST); - break; - case ASYNC_REFRESH_BLOCKED_COSMETIC: - addFrameFlag(COSMETIC); - break; - case ASYNC_REFRESH_BLOCKED_RESPONSIVE: - case EXCEEDED_RATELIMIT_FAST: - addFrameFlag(RESPONSIVE); - break; - default: - break; - } + switch (previousReason) { + case ASYNC_REFRESH_BLOCKED_DEMANDFAST: + addFrameFlag(DEMAND_FAST); + break; + case ASYNC_REFRESH_BLOCKED_COSMETIC: + addFrameFlag(COSMETIC); + break; + case ASYNC_REFRESH_BLOCKED_RESPONSIVE: + case EXCEEDED_RATELIMIT_FAST: + addFrameFlag(RESPONSIVE); + break; + default: + break; + } } // Is it too soon for another frame of this type? -void EInkDynamicDisplay::checkRateLimiting() -{ - // Sanity check: millis() overflow - just let the update run.. - if (previousRunMs > millis()) - return; +void EInkDynamicDisplay::checkRateLimiting() { + // Sanity check: millis() overflow - just let the update run.. + if (previousRunMs > millis()) + return; - // Skip update: too soon for BACKGROUND - if (frameFlags == BACKGROUND) { - if (Throttle::isWithinTimespanMs(previousRunMs, 30000)) { - refresh = SKIPPED; - reason = EXCEEDED_RATELIMIT_FULL; - return; - } + // Skip update: too soon for BACKGROUND + if (frameFlags == BACKGROUND) { + if (Throttle::isWithinTimespanMs(previousRunMs, 30000)) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FULL; + return; } + } - // No rate-limit for these special cases - if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST) - return; + // No rate-limit for these special cases + if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST) + return; - // Skip update: too soon for RESPONSIVE - if (frameFlags & RESPONSIVE) { - if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) { - refresh = SKIPPED; - reason = EXCEEDED_RATELIMIT_FAST; - LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags); - return; - } + // Skip update: too soon for RESPONSIVE + if (frameFlags & RESPONSIVE) { + if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FAST; + LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags); + return; } + } } // Is this frame COSMETIC (splash screens?) -void EInkDynamicDisplay::checkCosmetic() -{ - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkCosmetic() { + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // A full refresh is requested for cosmetic purposes: we have a decision - if (frameFlags & COSMETIC) { - refresh = FULL; - reason = FLAGGED_COSMETIC; - LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags); - } + // A full refresh is requested for cosmetic purposes: we have a decision + if (frameFlags & COSMETIC) { + refresh = FULL; + reason = FLAGGED_COSMETIC; + LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags); + } } // Is this a one-off special circumstance, where we REALLY want a fast refresh? -void EInkDynamicDisplay::checkDemandingFast() -{ - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkDemandingFast() { + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // A fast refresh is demanded: we have a decision - if (frameFlags & DEMAND_FAST) { - refresh = FAST; - reason = FLAGGED_DEMAND_FAST; - LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags); - } + // A fast refresh is demanded: we have a decision + if (frameFlags & DEMAND_FAST) { + refresh = FAST; + reason = FLAGGED_DEMAND_FAST; + LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags); + } } // Does the new frame match the currently displayed image? -void EInkDynamicDisplay::checkFrameMatchesPrevious() -{ - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkFrameMatchesPrevious() { + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // If frame is *not* a duplicate, abort the check - if (imageHash != previousImageHash) - return; + // If frame is *not* a duplicate, abort the check + if (imageHash != previousImageHash) + return; #if !defined(EINK_BACKGROUND_USES_FAST) - // If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality) - if (frameFlags == BACKGROUND && fastRefreshCount > 0) { - refresh = FULL; - reason = REDRAW_WITH_FULL; - LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags); - return; - } + // If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality) + if (frameFlags == BACKGROUND && fastRefreshCount > 0) { + refresh = FULL; + reason = REDRAW_WITH_FULL; + LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags); + return; + } #endif - // Not redrawn, not COSMETIC, not DEMAND_FAST - refresh = SKIPPED; - reason = FRAME_MATCHED_PREVIOUS; - LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); + // Not redrawn, not COSMETIC, not DEMAND_FAST + refresh = SKIPPED; + reason = FRAME_MATCHED_PREVIOUS; + LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); } // Have too many fast-refreshes occured consecutively, since last full refresh? -void EInkDynamicDisplay::checkConsecutiveFastRefreshes() -{ - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkConsecutiveFastRefreshes() { + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // Bypass limit if UNLIMITED_FAST mode is active - if (frameFlags & UNLIMITED_FAST) { - refresh = FAST; - reason = NO_OBJECTIONS; - LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags); - return; - } + // Bypass limit if UNLIMITED_FAST mode is active + if (frameFlags & UNLIMITED_FAST) { + refresh = FAST; + reason = NO_OBJECTIONS; + LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags); + return; + } - // If too many FAST refreshes consecutively - force a FULL refresh - if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) { - refresh = FULL; - reason = EXCEEDED_LIMIT_FASTREFRESH; - LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags); - } + // If too many FAST refreshes consecutively - force a FULL refresh + if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) { + refresh = FULL; + reason = EXCEEDED_LIMIT_FASTREFRESH; + LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags); + } } // No objections, we can perform fast-refresh, if desired -void EInkDynamicDisplay::checkFastRequested() -{ - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkFastRequested() { + if (refresh != UNSPECIFIED) + return; - if (frameFlags == BACKGROUND) { + if (frameFlags == BACKGROUND) { #ifdef EINK_BACKGROUND_USES_FAST - // If we want BACKGROUND to use fast. (FULL only when a limit is hit) - refresh = FAST; - reason = BACKGROUND_USES_FAST; - LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, - frameFlags); + // If we want BACKGROUND to use fast. (FULL only when a limit is hit) + refresh = FAST; + reason = BACKGROUND_USES_FAST; + LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags); #else - // If we do want to use FULL for BACKGROUND updates - refresh = FULL; - reason = FLAGGED_BACKGROUND; - LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND"); + // If we do want to use FULL for BACKGROUND updates + refresh = FULL; + reason = FLAGGED_BACKGROUND; + LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND"); #endif - } + } - // Sanity: confirm that we did ask for a RESPONSIVE frame. - if (frameFlags & RESPONSIVE) { - refresh = FAST; - reason = NO_OBJECTIONS; - LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags); - } + // Sanity: confirm that we did ask for a RESPONSIVE frame. + if (frameFlags & RESPONSIVE) { + refresh = FAST; + reason = NO_OBJECTIONS; + LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags); + } } // Reset the timer used for rate-limiting -void EInkDynamicDisplay::resetRateLimiting() -{ - previousRunMs = millis(); -} +void EInkDynamicDisplay::resetRateLimiting() { previousRunMs = millis(); } // Generate a hash of this frame, to compare against previous update -void EInkDynamicDisplay::hashImage() -{ - imageHash = 0; +void EInkDynamicDisplay::hashImage() { + imageHash = 0; - // Sum all bytes of the image buffer together - for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { - imageHash ^= buffer[b] << b; - } + // Sum all bytes of the image buffer together + for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { + imageHash ^= buffer[b] << b; + } } // Store the results of determineMode() for future use, and reset for next call -void EInkDynamicDisplay::storeAndReset() -{ - previousFrameFlags = frameFlags; - previousRefresh = refresh; - previousReason = reason; +void EInkDynamicDisplay::storeAndReset() { + previousFrameFlags = frameFlags; + previousRefresh = refresh; + previousReason = reason; - // Only store image hash if the display will update - if (refresh != SKIPPED) { - previousImageHash = imageHash; - } + // Only store image hash if the display will update + if (refresh != SKIPPED) { + previousImageHash = imageHash; + } - frameFlags = BACKGROUND; - refresh = UNSPECIFIED; + frameFlags = BACKGROUND; + refresh = UNSPECIFIED; } #ifdef EINK_LIMIT_GHOSTING_PX // Count how many ghost pixels the new image will display -void EInkDynamicDisplay::countGhostPixels() -{ - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::countGhostPixels() { + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // Start a new count - ghostPixelCount = 0; + // Start a new count + ghostPixelCount = 0; - // Check new image, bit by bit, for any white pixels at locations marked "dirty" - for (uint16_t i = 0; i < displayBufferSize; i++) { - for (uint8_t bit = 0; bit < 7; bit++) { + // Check new image, bit by bit, for any white pixels at locations marked "dirty" + for (uint16_t i = 0; i < displayBufferSize; i++) { + for (uint8_t bit = 0; bit < 7; bit++) { - const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh? - const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image? + const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh? + const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image? - // If pixel is (or has been) black since last full-refresh, and now is white: ghosting - if (dirty && shouldBeBlank) - ghostPixelCount++; + // If pixel is (or has been) black since last full-refresh, and now is white: ghosting + if (dirty && shouldBeBlank) + ghostPixelCount++; - // Update the dirty status for this pixel - will this location become a ghost if set white in future? - if (!dirty && !shouldBeBlank) - dirtyPixels[i] |= (1 << bit); - } + // Update the dirty status for this pixel - will this location become a ghost if set white in future? + if (!dirty && !shouldBeBlank) + dirtyPixels[i] |= (1 << bit); } + } - LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount); + LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount); } // Check if ghost pixel count exceeds the defined limit -void EInkDynamicDisplay::checkExcessiveGhosting() -{ - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkExcessiveGhosting() { + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - countGhostPixels(); + countGhostPixels(); - // If too many ghost pixels, select full refresh - if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) { - refresh = FULL; - reason = EXCEEDED_GHOSTINGLIMIT; - LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags); - } + // If too many ghost pixels, select full refresh + if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) { + refresh = FULL; + reason = EXCEEDED_GHOSTINGLIMIT; + LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags); + } } // Clear the dirty pixels array. Call when full-refresh cleans the display. -void EInkDynamicDisplay::resetGhostPixelTracking() -{ - // Copy the current frame into dirtyPixels[] from the display buffer - memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); +void EInkDynamicDisplay::resetGhostPixelTracking() { + // Copy the current frame into dirtyPixels[] from the display buffer + memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); } #endif // EINK_LIMIT_GHOSTING_PX // Handle any asyc tasks -void EInkDynamicDisplay::onNotify(uint32_t notification) -{ - // Which task - switch (notification) { - case DUE_POLL_ASYNCREFRESH: - pollAsyncRefresh(); - break; - } +void EInkDynamicDisplay::onNotify(uint32_t notification) { + // Which task + switch (notification) { + case DUE_POLL_ASYNCREFRESH: + pollAsyncRefresh(); + break; + } } #ifdef HAS_EINK_ASYNCFULL // Public: wait for an refresh already in progress, then run the post-update code. See Screen::setScreensaverFrames() -void EInkDynamicDisplay::joinAsyncRefresh() -{ - // If no async refresh running, nothing to do - if (!asyncRefreshRunning) - return; +void EInkDynamicDisplay::joinAsyncRefresh() { + // If no async refresh running, nothing to do + if (!asyncRefreshRunning) + return; - LOG_DEBUG("Join an async refresh in progress"); + LOG_DEBUG("Join an async refresh in progress"); - // Continually poll the BUSY pin - while (adafruitDisplay->epd2.isBusy()) - yield(); + // Continually poll the BUSY pin + while (adafruitDisplay->epd2.isBusy()) + yield(); - // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done - adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code - EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) - asyncRefreshRunning = false; // Unset the flag - LOG_DEBUG("Refresh complete"); + // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done + adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code + EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) + asyncRefreshRunning = false; // Unset the flag + LOG_DEBUG("Refresh complete"); - // Note: this code only works because of a modification to meshtastic/GxEPD2. - // It is only equipped to intercept calls to nextPage() + // Note: this code only works because of a modification to meshtastic/GxEPD2. + // It is only equipped to intercept calls to nextPage() } // Called from NotifiedWorkerThread. Run the post-update code if the hardware is ready -void EInkDynamicDisplay::pollAsyncRefresh() -{ - // In theory, this condition should never be met - if (!asyncRefreshRunning) - return; +void EInkDynamicDisplay::pollAsyncRefresh() { + // In theory, this condition should never be met + if (!asyncRefreshRunning) + return; - // Still running, check back later - if (adafruitDisplay->epd2.isBusy()) { - // Schedule next call of pollAsyncRefresh() - NotifiedWorkerThread::notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); - return; - } + // Still running, check back later + if (adafruitDisplay->epd2.isBusy()) { + // Schedule next call of pollAsyncRefresh() + NotifiedWorkerThread::notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); + return; + } - // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done - adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code - EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) - asyncRefreshRunning = false; // Unset the flag - LOG_DEBUG("Async full-refresh complete"); + // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done + adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code + EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) + asyncRefreshRunning = false; // Unset the flag + LOG_DEBUG("Async full-refresh complete"); - // Note: this code only works because of a modification to meshtastic/GxEPD2. - // It is only equipped to intercept calls to nextPage() + // Note: this code only works because of a modification to meshtastic/GxEPD2. + // It is only equipped to intercept calls to nextPage() } // Check the status of "async full-refresh"; skip if running -void EInkDynamicDisplay::checkBusyAsyncRefresh() -{ - // No refresh taking place, continue with determineMode() - if (!asyncRefreshRunning) - return; +void EInkDynamicDisplay::checkBusyAsyncRefresh() { + // No refresh taking place, continue with determineMode() + if (!asyncRefreshRunning) + return; - // Full refresh still running - if (adafruitDisplay->epd2.isBusy()) { - // No refresh - refresh = SKIPPED; + // Full refresh still running + if (adafruitDisplay->epd2.isBusy()) { + // No refresh + refresh = SKIPPED; - // Set the reason, marking what type of frame we're skipping - if (frameFlags & DEMAND_FAST) - reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST; - else if (frameFlags & COSMETIC) - reason = ASYNC_REFRESH_BLOCKED_COSMETIC; - else if (frameFlags & RESPONSIVE) - reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE; - else - reason = ASYNC_REFRESH_BLOCKED_BACKGROUND; - - return; - } - - // Async refresh appears to have stopped, but wasn't caught by onNotify() + // Set the reason, marking what type of frame we're skipping + if (frameFlags & DEMAND_FAST) + reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST; + else if (frameFlags & COSMETIC) + reason = ASYNC_REFRESH_BLOCKED_COSMETIC; + else if (frameFlags & RESPONSIVE) + reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE; else - pollAsyncRefresh(); // Check (and terminate) the async refresh manually + reason = ASYNC_REFRESH_BLOCKED_BACKGROUND; + + return; + } + + // Async refresh appears to have stopped, but wasn't caught by onNotify() + else + pollAsyncRefresh(); // Check (and terminate) the async refresh manually } // Hold control while an async refresh runs -void EInkDynamicDisplay::awaitRefresh() -{ - // Continually poll the BUSY pin - while (adafruitDisplay->epd2.isBusy()) - yield(); +void EInkDynamicDisplay::awaitRefresh() { + // Continually poll the BUSY pin + while (adafruitDisplay->epd2.isBusy()) + yield(); - // End the full-refresh process - adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code - EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) - asyncRefreshRunning = false; // Unset the flag + // End the full-refresh process + adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code + EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) + asyncRefreshRunning = false; // Unset the flag } #endif // HAS_EINK_ASYNCFULL diff --git a/src/graphics/EInkDynamicDisplay.h b/src/graphics/EInkDynamicDisplay.h index d5e29e3f0..78b6476ec 100644 --- a/src/graphics/EInkDynamicDisplay.h +++ b/src/graphics/EInkDynamicDisplay.h @@ -15,131 +15,130 @@ (Full, Fast, Skip) */ -class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWorkerThread -{ - public: - // Constructor - // ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class ) - EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus); - ~EInkDynamicDisplay(); +class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWorkerThread { +public: + // Constructor + // ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class ) + EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus); + ~EInkDynamicDisplay(); - // Methods to enable or disable unlimited fast refresh mode - void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); } - void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); } + // Methods to enable or disable unlimited fast refresh mode + void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); } + void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); } - // What kind of frame is this - enum frameFlagTypes : uint8_t { - BACKGROUND = (1 << 0), // For frames via display() - RESPONSIVE = (1 << 1), // For frames via forceDisplay() - COSMETIC = (1 << 2), // For splashes - DEMAND_FAST = (1 << 3), // Special case only - BLOCKING = (1 << 4), // Modifier - block while refresh runs - UNLIMITED_FAST = (1 << 5) - }; - void addFrameFlag(frameFlagTypes flag); + // What kind of frame is this + enum frameFlagTypes : uint8_t { + BACKGROUND = (1 << 0), // For frames via display() + RESPONSIVE = (1 << 1), // For frames via forceDisplay() + COSMETIC = (1 << 2), // For splashes + DEMAND_FAST = (1 << 3), // Special case only + BLOCKING = (1 << 4), // Modifier - block while refresh runs + UNLIMITED_FAST = (1 << 5) + }; + void addFrameFlag(frameFlagTypes flag); - // Set the correct frame flag, then call universal "update()" method - void display() override; - bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused. + // Set the correct frame flag, then call universal "update()" method + void display() override; + bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused. - protected: - enum refreshTypes : uint8_t { // Which refresh operation will be used - UNSPECIFIED, - FULL, - FAST, - SKIPPED, - }; - enum reasonTypes : uint8_t { // How was the decision reached - NO_OBJECTIONS, - ASYNC_REFRESH_BLOCKED_DEMANDFAST, - ASYNC_REFRESH_BLOCKED_COSMETIC, - ASYNC_REFRESH_BLOCKED_RESPONSIVE, - ASYNC_REFRESH_BLOCKED_BACKGROUND, - EXCEEDED_RATELIMIT_FAST, - EXCEEDED_RATELIMIT_FULL, - FLAGGED_COSMETIC, - FLAGGED_DEMAND_FAST, - EXCEEDED_LIMIT_FASTREFRESH, - EXCEEDED_GHOSTINGLIMIT, - FRAME_MATCHED_PREVIOUS, - BACKGROUND_USES_FAST, - FLAGGED_BACKGROUND, - REDRAW_WITH_FULL, - }; +protected: + enum refreshTypes : uint8_t { // Which refresh operation will be used + UNSPECIFIED, + FULL, + FAST, + SKIPPED, + }; + enum reasonTypes : uint8_t { // How was the decision reached + NO_OBJECTIONS, + ASYNC_REFRESH_BLOCKED_DEMANDFAST, + ASYNC_REFRESH_BLOCKED_COSMETIC, + ASYNC_REFRESH_BLOCKED_RESPONSIVE, + ASYNC_REFRESH_BLOCKED_BACKGROUND, + EXCEEDED_RATELIMIT_FAST, + EXCEEDED_RATELIMIT_FULL, + FLAGGED_COSMETIC, + FLAGGED_DEMAND_FAST, + EXCEEDED_LIMIT_FASTREFRESH, + EXCEEDED_GHOSTINGLIMIT, + FRAME_MATCHED_PREVIOUS, + BACKGROUND_USES_FAST, + FLAGGED_BACKGROUND, + REDRAW_WITH_FULL, + }; - enum notificationTypes : uint8_t { // What was onNotify() called for - NONE = 0, // This behavior (NONE=0) is fixed by NotifiedWorkerThread class - DUE_POLL_ASYNCREFRESH = 1, - }; - const uint32_t intervalPollAsyncRefresh = 100; + enum notificationTypes : uint8_t { // What was onNotify() called for + NONE = 0, // This behavior (NONE=0) is fixed by NotifiedWorkerThread class + DUE_POLL_ASYNCREFRESH = 1, + }; + const uint32_t intervalPollAsyncRefresh = 100; - void onNotify(uint32_t notification) override; // Handle any async tasks - overrides NotifiedWorkerThread - void configForFastRefresh(); // GxEPD2 code to set fast-refresh - void configForFullRefresh(); // GxEPD2 code to set full-refresh - bool determineMode(); // Assess situation, pick a refresh type - void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type - void adjustRefreshCounters(); // Update fastRefreshCount - bool update(); // Trigger the display update - determine mode, then call base class - void endOrDetach(); // Run the post-update code, or delegate it off to checkBusyAsyncRefresh() + void onNotify(uint32_t notification) override; // Handle any async tasks - overrides NotifiedWorkerThread + void configForFastRefresh(); // GxEPD2 code to set fast-refresh + void configForFullRefresh(); // GxEPD2 code to set full-refresh + bool determineMode(); // Assess situation, pick a refresh type + void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type + void adjustRefreshCounters(); // Update fastRefreshCount + bool update(); // Trigger the display update - determine mode, then call base class + void endOrDetach(); // Run the post-update code, or delegate it off to checkBusyAsyncRefresh() - // Checks as part of determineMode() - void checkInitialized(); // Is this the very first frame? - void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh? - void checkRateLimiting(); // Is this frame too soon? - void checkCosmetic(); // Was the COSMETIC flag set? - void checkDemandingFast(); // Was the DEMAND_FAST flag set? - void checkFrameMatchesPrevious(); // Does the new frame match the existing display image? - void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively? - void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND? + // Checks as part of determineMode() + void checkInitialized(); // Is this the very first frame? + void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh? + void checkRateLimiting(); // Is this frame too soon? + void checkCosmetic(); // Was the COSMETIC flag set? + void checkDemandingFast(); // Was the DEMAND_FAST flag set? + void checkFrameMatchesPrevious(); // Does the new frame match the existing display image? + void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively? + void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND? - void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting - void hashImage(); // Generate a hashed version of this frame, to compare against previous update - void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call + void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting + void hashImage(); // Generate a hashed version of this frame, to compare against previous update + void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call - // What we are determining for this frame - frameFlagTypes frameFlags = BACKGROUND; // Frame characteristics - determineMode() input - refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output - reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used + // What we are determining for this frame + frameFlagTypes frameFlags = BACKGROUND; // Frame characteristics - determineMode() input + refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output + reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used - // What happened last time determineMode() ran - frameFlagTypes previousFrameFlags = BACKGROUND; // (Previous) Frame flags - refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome - reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason + // What happened last time determineMode() ran + frameFlagTypes previousFrameFlags = BACKGROUND; // (Previous) Frame flags + refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome + reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason - bool initialized = false; // Have we drawn at least one frame yet? - uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting) - uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed! - uint32_t previousImageHash = 0; // Hash of the previous update's frame - uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh? - refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for + bool initialized = false; // Have we drawn at least one frame yet? + uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting) + uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed! + uint32_t previousImageHash = 0; // Hash of the previous update's frame + uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh? + refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for - // Optional - track ghosting, pixel by pixel - // May 2024: no longer used by any display. Kept for possible future use. + // Optional - track ghosting, pixel by pixel + // May 2024: no longer used by any display. Kept for possible future use. #ifdef EINK_LIMIT_GHOSTING_PX - void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh - void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit - void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. - uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) - uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use + void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh + void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit + void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. + uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) + uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use #endif - // Conditional - async full refresh - only with modified meshtastic/GxEPD2 + // Conditional - async full refresh - only with modified meshtastic/GxEPD2 #if defined(HAS_EINK_ASYNCFULL) - public: - void joinAsyncRefresh(); // Main thread joins an async refresh already in progress. Blocks, then runs post-update code +public: + void joinAsyncRefresh(); // Main thread joins an async refresh already in progress. Blocks, then runs post-update code - protected: - void pollAsyncRefresh(); // Run the post-update code if the hardware is ready - void checkBusyAsyncRefresh(); // Check if display is busy running an async full-refresh (rejecting new frames) - void awaitRefresh(); // Hold control while an async refresh runs - void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay() - bool asyncRefreshRunning = false; // Flag, checked by checkBusyAsyncRefresh() +protected: + void pollAsyncRefresh(); // Run the post-update code if the hardware is ready + void checkBusyAsyncRefresh(); // Check if display is busy running an async full-refresh (rejecting new frames) + void awaitRefresh(); // Hold control while an async refresh runs + void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay() + bool asyncRefreshRunning = false; // Flag, checked by checkBusyAsyncRefresh() #else - public: - void joinAsyncRefresh() {} // Dummy method +public: + void joinAsyncRefresh() {} // Dummy method - protected: - void pollAsyncRefresh() {} // Dummy method. In theory, not reachable +protected: + void pollAsyncRefresh() {} // Dummy method. In theory, not reachable #endif }; diff --git a/src/graphics/GxEPD2Multi.h b/src/graphics/GxEPD2Multi.h index f3807c9de..0ab5e5415 100644 --- a/src/graphics/GxEPD2Multi.h +++ b/src/graphics/GxEPD2Multi.h @@ -4,132 +4,117 @@ // Workaround for issue of GxEPD2_BW objects not having a shared base class // Only exposes methods which we are actually using -template class GxEPD2_Multi -{ +template class GxEPD2_Multi { +public: + void drawPixel(int16_t x, int16_t y, uint16_t color) { + if (which == 0) + driver0->drawPixel(x, y, color); + else + driver1->drawPixel(x, y, color); + } + + bool nextPage() { + if (which == 0) + return driver0->nextPage(); + else + return driver1->nextPage(); + } + + void hibernate() { + if (which == 0) + driver0->hibernate(); + else + driver1->hibernate(); + } + + void init(uint32_t serial_diag_bitrate = 0) { + if (which == 0) + driver0->init(serial_diag_bitrate); + else + driver1->init(serial_diag_bitrate); + } + + void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) { + if (which == 0) + driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + else + driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + } + + void setRotation(uint8_t x) { + if (which == 0) + driver0->setRotation(x); + else + driver1->setRotation(x); + } + + void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + if (which == 0) + driver0->setPartialWindow(x, y, w, h); + else + driver1->setPartialWindow(x, y, w, h); + } + + void setFullWindow() { + if (which == 0) + driver0->setFullWindow(); + else + driver1->setFullWindow(); + } + + int16_t width() { + if (which == 0) + return driver0->width(); + else + return driver1->width(); + } + + int16_t height() { + if (which == 0) + return driver0->height(); + else + return driver1->height(); + } + + void clearScreen(uint8_t value = 0xFF) { + if (which == 0) + driver0->clearScreen(); + else + driver1->clearScreen(); + } + + void endAsyncFull() { + if (which == 0) + driver0->endAsyncFull(); + else + driver1->endAsyncFull(); + } + + // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd + class Epd2Wrapper { public: - void drawPixel(int16_t x, int16_t y, uint16_t color) - { - if (which == 0) - driver0->drawPixel(x, y, color); - else - driver1->drawPixel(x, y, color); + bool isBusy() { return m_epd2->isBusy(); } + GxEPD2_EPD *m_epd2; + } epd2; + + // Constructor + // Select driver by passing whichDriver as 0 or 1 + GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) { + assert(whichDriver == 0 || whichDriver == 1); + which = whichDriver; + LOG_DEBUG("GxEPD2_Multi driver: %d", which); + + if (which == 0) { + driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver0->epd2); + } else if (which == 1) { + driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver1->epd2); } + } - bool nextPage() - { - if (which == 0) - return driver0->nextPage(); - else - return driver1->nextPage(); - } - - void hibernate() - { - if (which == 0) - driver0->hibernate(); - else - driver1->hibernate(); - } - - void init(uint32_t serial_diag_bitrate = 0) - { - if (which == 0) - driver0->init(serial_diag_bitrate); - else - driver1->init(serial_diag_bitrate); - } - - void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) - { - if (which == 0) - driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); - else - driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); - } - - void setRotation(uint8_t x) - { - if (which == 0) - driver0->setRotation(x); - else - driver1->setRotation(x); - } - - void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) - { - if (which == 0) - driver0->setPartialWindow(x, y, w, h); - else - driver1->setPartialWindow(x, y, w, h); - } - - void setFullWindow() - { - if (which == 0) - driver0->setFullWindow(); - else - driver1->setFullWindow(); - } - - int16_t width() - { - if (which == 0) - return driver0->width(); - else - return driver1->width(); - } - - int16_t height() - { - if (which == 0) - return driver0->height(); - else - return driver1->height(); - } - - void clearScreen(uint8_t value = 0xFF) - { - if (which == 0) - driver0->clearScreen(); - else - driver1->clearScreen(); - } - - void endAsyncFull() - { - if (which == 0) - driver0->endAsyncFull(); - else - driver1->endAsyncFull(); - } - - // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd - class Epd2Wrapper - { - public: - bool isBusy() { return m_epd2->isBusy(); } - GxEPD2_EPD *m_epd2; - } epd2; - - // Constructor - // Select driver by passing whichDriver as 0 or 1 - GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) - { - assert(whichDriver == 0 || whichDriver == 1); - which = whichDriver; - LOG_DEBUG("GxEPD2_Multi driver: %d", which); - - if (which == 0) { - driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); - epd2.m_epd2 = &(driver0->epd2); - } else if (which == 1) { - driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); - epd2.m_epd2 = &(driver1->epd2); - } - } - - private: - uint8_t which; - GxEPD2_BW *driver0; - GxEPD2_BW *driver1; +private: + uint8_t which; + GxEPD2_BW *driver0; + GxEPD2_BW *driver1; }; \ No newline at end of file diff --git a/src/graphics/Panel_sdl.cpp b/src/graphics/Panel_sdl.cpp index bad6072f9..3aa2f3bd3 100644 --- a/src/graphics/Panel_sdl.cpp +++ b/src/graphics/Panel_sdl.cpp @@ -33,10 +33,8 @@ Porting for SDL: #define M_PI 3.14159265358979323846 #endif -namespace lgfx -{ -inline namespace v1 -{ +namespace lgfx { +inline namespace v1 { SDL_Keymod Panel_sdl::_keymod = KMOD_NONE; static SDL_semaphore *_update_in_semaphore = nullptr; static SDL_semaphore *_update_out_semaphore = nullptr; @@ -47,637 +45,593 @@ static bool _all_close = false; volatile uint8_t Panel_sdl::_gpio_dummy_values[EMULATED_GPIO_MAX]; -static inline void *heap_alloc_dma(size_t length) -{ - return malloc(length); -} // aligned_alloc(16, length); -static inline void heap_free(void *buf) -{ - free(buf); -} +static inline void *heap_alloc_dma(size_t length) { return malloc(length); } // aligned_alloc(16, length); +static inline void heap_free(void *buf) { free(buf); } static std::list _list_monitor; -static monitor_t *const getMonitorByWindowID(uint32_t windowID) -{ - for (auto &m : _list_monitor) { - if (SDL_GetWindowID(m->window) == windowID) { - return m; - } +static monitor_t *const getMonitorByWindowID(uint32_t windowID) { + for (auto &m : _list_monitor) { + if (SDL_GetWindowID(m->window) == windowID) { + return m; } - return nullptr; + } + return nullptr; } //---------------------------------------------------------------------------- static std::vector _key_code_map; -void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio) -{ - if (gpio > EMULATED_GPIO_MAX) - return; - KeyCodeMapping_t map; - map.keycode = keyCode; - map.gpio = gpio; - _key_code_map.push_back(map); +void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio) { + if (gpio > EMULATED_GPIO_MAX) + return; + KeyCodeMapping_t map; + map.keycode = keyCode; + map.gpio = gpio; + _key_code_map.push_back(map); } -int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode) -{ - for (const auto &i : _key_code_map) { - if (i.keycode == keyCode) - return i.gpio; - } - return -1; +int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode) { + for (const auto &i : _key_code_map) { + if (i.keycode == keyCode) + return i.gpio; + } + return -1; } -void Panel_sdl::_event_proc(void) -{ - SDL_Event event; - while (SDL_PollEvent(&event)) { - if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) { - auto mon = getMonitorByWindowID(event.button.windowID); - int gpio = -1; +void Panel_sdl::_event_proc(void) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) { + auto mon = getMonitorByWindowID(event.button.windowID); + int gpio = -1; - /// Check key mapping - gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym); - if (gpio < 0) { - switch (event.key.keysym.sym) { /// M5StackのBtnA~BtnCのエミュレート; - // case SDLK_LEFT: gpio = 39; break; - // case SDLK_DOWN: gpio = 38; break; - // case SDLK_RIGHT: gpio = 37; break; - // case SDLK_UP: gpio = 36; break; + /// Check key mapping + gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym); + if (gpio < 0) { + switch (event.key.keysym.sym) { /// M5StackのBtnA~BtnCのエミュレート; + // case SDLK_LEFT: gpio = 39; break; + // case SDLK_DOWN: gpio = 38; break; + // case SDLK_RIGHT: gpio = 37; break; + // case SDLK_UP: gpio = 36; break; - /// L/Rキーで画面回転 - case SDLK_r: - case SDLK_l: - if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { - if (mon != nullptr) { - mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1); - int x, y, w, h; - SDL_GetWindowSize(mon->window, &w, &h); - SDL_GetWindowPosition(mon->window, &x, &y); - SDL_SetWindowSize(mon->window, h, w); - SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2); - mon->panel->sdl_invalidate(); - } - } - break; - - /// 1~6キーで画面拡大率変更 - case SDLK_1: - case SDLK_2: - case SDLK_3: - case SDLK_4: - case SDLK_5: - case SDLK_6: - if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { - if (mon != nullptr) { - int size = 1 + (event.key.keysym.sym - SDLK_1); - _update_scaling(mon, size, size); - } - } - break; - default: - continue; - } - } - - if (event.type == SDL_KEYDOWN) { - Panel_sdl::gpio_lo(gpio); - } else { - Panel_sdl::gpio_hi(gpio); - } - } else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) { - auto mon = getMonitorByWindowID(event.button.windowID); + /// L/Rキーで画面回転 + case SDLK_r: + case SDLK_l: + if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { if (mon != nullptr) { - { - int x, y, w, h; - SDL_GetWindowSize(mon->window, &w, &h); - SDL_GetMouseState(&x, &y); - float sf = sinf(mon->frame_angle * M_PI / 180); - float cf = cosf(mon->frame_angle * M_PI / 180); - x -= w / 2.0f; - y -= h / 2.0f; - float nx = y * sf + x * cf; - float ny = y * cf - x * sf; - if (mon->frame_rotation & 1) { - std::swap(w, h); - } - x = (nx * mon->frame_width / w) + (mon->frame_width >> 1); - y = (ny * mon->frame_height / h) + (mon->frame_height >> 1); - mon->touch_x = x - mon->frame_inner_x; - mon->touch_y = y - mon->frame_inner_y; - } - if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) { - mon->touched = true; - } - if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) { - mon->touched = false; - } + mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1); + int x, y, w, h; + SDL_GetWindowSize(mon->window, &w, &h); + SDL_GetWindowPosition(mon->window, &x, &y); + SDL_SetWindowSize(mon->window, h, w); + SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2); + mon->panel->sdl_invalidate(); } - } else if (event.type == SDL_WINDOWEVENT) { - auto monitor = getMonitorByWindowID(event.window.windowID); - if (monitor) { - if (event.window.event == SDL_WINDOWEVENT_RESIZED) { - int mw, mh; - SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh); - if (monitor->frame_rotation & 1) { - std::swap(mw, mh); - } - monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f; - monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f; - monitor->panel->sdl_invalidate(); - } else if (event.window.event == SDL_WINDOWEVENT_CLOSE) { - monitor->closing = true; - } - } - } else if (event.type == SDL_QUIT) { - for (auto &m : _list_monitor) { - m->closing = true; + } + break; + + /// 1~6キーで画面拡大率変更 + case SDLK_1: + case SDLK_2: + case SDLK_3: + case SDLK_4: + case SDLK_5: + case SDLK_6: + if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { + if (mon != nullptr) { + int size = 1 + (event.key.keysym.sym - SDLK_1); + _update_scaling(mon, size, size); } + } + break; + default: + continue; } + } + + if (event.type == SDL_KEYDOWN) { + Panel_sdl::gpio_lo(gpio); + } else { + Panel_sdl::gpio_hi(gpio); + } + } else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) { + auto mon = getMonitorByWindowID(event.button.windowID); + if (mon != nullptr) { + { + int x, y, w, h; + SDL_GetWindowSize(mon->window, &w, &h); + SDL_GetMouseState(&x, &y); + float sf = sinf(mon->frame_angle * M_PI / 180); + float cf = cosf(mon->frame_angle * M_PI / 180); + x -= w / 2.0f; + y -= h / 2.0f; + float nx = y * sf + x * cf; + float ny = y * cf - x * sf; + if (mon->frame_rotation & 1) { + std::swap(w, h); + } + x = (nx * mon->frame_width / w) + (mon->frame_width >> 1); + y = (ny * mon->frame_height / h) + (mon->frame_height >> 1); + mon->touch_x = x - mon->frame_inner_x; + mon->touch_y = y - mon->frame_inner_y; + } + if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) { + mon->touched = true; + } + if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) { + mon->touched = false; + } + } + } else if (event.type == SDL_WINDOWEVENT) { + auto monitor = getMonitorByWindowID(event.window.windowID); + if (monitor) { + if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + int mw, mh; + SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh); + if (monitor->frame_rotation & 1) { + std::swap(mw, mh); + } + monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f; + monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f; + monitor->panel->sdl_invalidate(); + } else if (event.window.event == SDL_WINDOWEVENT_CLOSE) { + monitor->closing = true; + } + } + } else if (event.type == SDL_QUIT) { + for (auto &m : _list_monitor) { + m->closing = true; + } } + } } /// デバッガでステップ実行されていることを検出するスレッド用関数。 -static int detectDebugger(bool *running) -{ - uint32_t prev_ms = SDL_GetTicks(); - do { - SDL_Delay(1); - uint32_t ms = SDL_GetTicks(); - /// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。 - /// また、解除されたと判断した後も1023msecほど状態を維持する。 - if (ms - prev_ms > 64) { - _in_step_exec = _msec_step_exec; - } else if (_in_step_exec) { - --_in_step_exec; - } - prev_ms = ms; - } while (*running); - return 0; -} - -void Panel_sdl::_update_proc(void) -{ - for (auto it = _list_monitor.begin(); it != _list_monitor.end();) { - if ((*it)->closing) { - if ((*it)->texture_frameimage) { - SDL_DestroyTexture((*it)->texture_frameimage); - } - SDL_DestroyTexture((*it)->texture); - SDL_DestroyRenderer((*it)->renderer); - SDL_DestroyWindow((*it)->window); - _list_monitor.erase(it++); - if (_list_monitor.empty()) { - _all_close = true; - return; - } - continue; - } - (*it)->panel->sdl_update(); - ++it; +static int detectDebugger(bool *running) { + uint32_t prev_ms = SDL_GetTicks(); + do { + SDL_Delay(1); + uint32_t ms = SDL_GetTicks(); + /// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。 + /// また、解除されたと判断した後も1023msecほど状態を維持する。 + if (ms - prev_ms > 64) { + _in_step_exec = _msec_step_exec; + } else if (_in_step_exec) { + --_in_step_exec; } + prev_ms = ms; + } while (*running); + return 0; } -int Panel_sdl::setup(void) -{ - if (_inited) - return 1; - _inited = true; - - /// Add default keycode mapping - /// M5StackのBtnA~BtnCのエミュレート; - addKeyCodeMapping(SDLK_LEFT, 39); - addKeyCodeMapping(SDLK_DOWN, 38); - addKeyCodeMapping(SDLK_RIGHT, 37); - addKeyCodeMapping(SDLK_UP, 36); - - SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited); - - _update_in_semaphore = SDL_CreateSemaphore(0); - _update_out_semaphore = SDL_CreateSemaphore(0); - for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) { - gpio_hi(pin); +void Panel_sdl::_update_proc(void) { + for (auto it = _list_monitor.begin(); it != _list_monitor.end();) { + if ((*it)->closing) { + if ((*it)->texture_frameimage) { + SDL_DestroyTexture((*it)->texture_frameimage); + } + SDL_DestroyTexture((*it)->texture); + SDL_DestroyRenderer((*it)->renderer); + SDL_DestroyWindow((*it)->window); + _list_monitor.erase(it++); + if (_list_monitor.empty()) { + _all_close = true; + return; + } + continue; } - /*Initialize the SDL*/ - SDL_Init(SDL_INIT_VIDEO); - SDL_StartTextInput(); - - // SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH); - return 0; + (*it)->panel->sdl_update(); + ++it; + } } -int Panel_sdl::loop(void) -{ - if (!_inited) - return 1; +int Panel_sdl::setup(void) { + if (_inited) + return 1; + _inited = true; - _event_proc(); - SDL_SemWaitTimeout(_update_in_semaphore, 1); - _update_proc(); - _event_proc(); - if (SDL_SemValue(_update_out_semaphore) == 0) { - SDL_SemPost(_update_out_semaphore); + /// Add default keycode mapping + /// M5StackのBtnA~BtnCのエミュレート; + addKeyCodeMapping(SDLK_LEFT, 39); + addKeyCodeMapping(SDLK_DOWN, 38); + addKeyCodeMapping(SDLK_RIGHT, 37); + addKeyCodeMapping(SDLK_UP, 36); + + SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited); + + _update_in_semaphore = SDL_CreateSemaphore(0); + _update_out_semaphore = SDL_CreateSemaphore(0); + for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) { + gpio_hi(pin); + } + /*Initialize the SDL*/ + SDL_Init(SDL_INIT_VIDEO); + SDL_StartTextInput(); + + // SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH); + return 0; +} + +int Panel_sdl::loop(void) { + if (!_inited) + return 1; + + _event_proc(); + SDL_SemWaitTimeout(_update_in_semaphore, 1); + _update_proc(); + _event_proc(); + if (SDL_SemValue(_update_out_semaphore) == 0) { + SDL_SemPost(_update_out_semaphore); + } + + return _all_close; +} + +int Panel_sdl::close(void) { + if (!_inited) + return 1; + _inited = false; + + SDL_StopTextInput(); + SDL_DestroySemaphore(_update_in_semaphore); + SDL_DestroySemaphore(_update_out_semaphore); + SDL_Quit(); + return 0; +} + +int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec) { + _msec_step_exec = msec_step_exec; + + /// SDLの準備 + if (0 != Panel_sdl::setup()) { + return 1; + } + + /// ユーザコード関数の動作・停止フラグ + bool running = true; + + /// ユーザコード関数を起動する + auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running); + + /// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続 + while (0 == Panel_sdl::loop()) { + }; + + /// ユーザコード関数を終了する + running = false; + SDL_WaitThread(thread, nullptr); + + /// SDLを終了する + return Panel_sdl::close(); +} + +void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y) { + monitor.scaling_x = scaling_x; + monitor.scaling_y = scaling_y; +} + +void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y) { + monitor.frame_image = frame_image; + monitor.frame_width = frame_width; + monitor.frame_height = frame_height; + monitor.frame_inner_x = inner_x; + monitor.frame_inner_y = inner_y; +} + +void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation) { + monitor.frame_rotation = frame_rotation; + monitor.frame_angle = (monitor.frame_rotation) * 90; +} + +Panel_sdl::~Panel_sdl(void) { + _list_monitor.remove(&monitor); + SDL_DestroyMutex(_sdl_mutex); +} + +Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase() { + _sdl_mutex = SDL_CreateMutex(); + _auto_display = true; + monitor.panel = this; +} + +bool Panel_sdl::init(bool use_reset) { + initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height); + bool res = Panel_FrameBufferBase::init(use_reset); + + _list_monitor.push_back(&monitor); + + return res; +} + +color_depth_t Panel_sdl::setColorDepth(color_depth_t depth) { + auto bits = depth & color_depth_t::bit_mask; + if (bits >= 16) { + depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte; + } else { + depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte; + } + _write_depth = depth; + _read_depth = depth; + + return depth; +} + +Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent} { SDL_LockMutex(parent->_sdl_mutex); }; + +Panel_sdl::lock_t::~lock_t(void) { + ++_parent->_modified_counter; + SDL_UnlockMutex(_parent->_sdl_mutex); + if (SDL_SemValue(_update_in_semaphore) < 2) { + SDL_SemPost(_update_in_semaphore); + if (!_in_step_exec) { + SDL_SemWaitTimeout(_update_out_semaphore, 1); } - - return _all_close; -} - -int Panel_sdl::close(void) -{ - if (!_inited) - return 1; - _inited = false; - - SDL_StopTextInput(); - SDL_DestroySemaphore(_update_in_semaphore); - SDL_DestroySemaphore(_update_out_semaphore); - SDL_Quit(); - return 0; -} - -int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec) -{ - _msec_step_exec = msec_step_exec; - - /// SDLの準備 - if (0 != Panel_sdl::setup()) { - return 1; - } - - /// ユーザコード関数の動作・停止フラグ - bool running = true; - - /// ユーザコード関数を起動する - auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running); - - /// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続 - while (0 == Panel_sdl::loop()) { - }; - - /// ユーザコード関数を終了する - running = false; - SDL_WaitThread(thread, nullptr); - - /// SDLを終了する - return Panel_sdl::close(); -} - -void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y) -{ - monitor.scaling_x = scaling_x; - monitor.scaling_y = scaling_y; -} - -void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y) -{ - monitor.frame_image = frame_image; - monitor.frame_width = frame_width; - monitor.frame_height = frame_height; - monitor.frame_inner_x = inner_x; - monitor.frame_inner_y = inner_y; -} - -void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation) -{ - monitor.frame_rotation = frame_rotation; - monitor.frame_angle = (monitor.frame_rotation) * 90; -} - -Panel_sdl::~Panel_sdl(void) -{ - _list_monitor.remove(&monitor); - SDL_DestroyMutex(_sdl_mutex); -} - -Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase() -{ - _sdl_mutex = SDL_CreateMutex(); - _auto_display = true; - monitor.panel = this; -} - -bool Panel_sdl::init(bool use_reset) -{ - initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height); - bool res = Panel_FrameBufferBase::init(use_reset); - - _list_monitor.push_back(&monitor); - - return res; -} - -color_depth_t Panel_sdl::setColorDepth(color_depth_t depth) -{ - auto bits = depth & color_depth_t::bit_mask; - if (bits >= 16) { - depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte; - } else { - depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte; - } - _write_depth = depth; - _read_depth = depth; - - return depth; -} - -Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent} -{ - SDL_LockMutex(parent->_sdl_mutex); + } }; -Panel_sdl::lock_t::~lock_t(void) -{ - ++_parent->_modified_counter; - SDL_UnlockMutex(_parent->_sdl_mutex); - if (SDL_SemValue(_update_in_semaphore) < 2) { +void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) { + lock_t lock(this); + Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor); +} + +void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) { + lock_t lock(this); + Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor); +} + +void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length) { + // lock_t lock(this); + Panel_FrameBufferBase::writeBlock(rawcolor, length); +} + +void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) { + lock_t lock(this); + Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma); +} + +void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) { + lock_t lock(this); + Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param); +} + +void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) { + lock_t lock(this); + Panel_FrameBufferBase::writePixels(param, len, use_dma); +} + +void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) { + (void)x; + (void)y; + (void)w; + (void)h; + if (_in_step_exec) { + if (_display_counter != _modified_counter) { + do { SDL_SemPost(_update_in_semaphore); - if (!_in_step_exec) { - SDL_SemWaitTimeout(_update_out_semaphore, 1); - } + SDL_SemWaitTimeout(_update_out_semaphore, 1); + } while (_display_counter != _modified_counter); + SDL_Delay(1); } -}; - -void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) -{ - lock_t lock(this); - Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor); + } } -void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) -{ - lock_t lock(this); - Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor); +uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count) { + (void)count; + tp->x = monitor.touch_x; + tp->y = monitor.touch_y; + tp->size = monitor.touched ? 1 : 0; + tp->id = 0; + return monitor.touched; } -void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length) -{ - // lock_t lock(this); - Panel_FrameBufferBase::writeBlock(rawcolor, length); +void Panel_sdl::setWindowTitle(const char *title) { + _window_title = title; + if (monitor.window) { + SDL_SetWindowTitle(monitor.window, _window_title); + } } -void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) -{ - lock_t lock(this); - Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma); +void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy) { + mon->scaling_x = sx; + mon->scaling_y = sy; + int nw = mon->frame_width; + int nh = mon->frame_height; + if (mon->frame_rotation & 1) { + std::swap(nw, nh); + } + + int x, y, w, h; + int rw, rh; + SDL_GetRendererOutputSize(mon->renderer, &rw, &rh); + SDL_GetWindowSize(mon->window, &w, &h); + nw = nw * sx * w / rw; + nh = nh * sy * h / rh; + SDL_GetWindowPosition(mon->window, &x, &y); + SDL_SetWindowSize(mon->window, nw, nh); + SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2); + mon->panel->sdl_invalidate(); } -void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) -{ - lock_t lock(this); - Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param); -} - -void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) -{ - lock_t lock(this); - Panel_FrameBufferBase::writePixels(param, len, use_dma); -} - -void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) -{ - (void)x; - (void)y; - (void)w; - (void)h; - if (_in_step_exec) { - if (_display_counter != _modified_counter) { - do { - SDL_SemPost(_update_in_semaphore); - SDL_SemWaitTimeout(_update_out_semaphore, 1); - } while (_display_counter != _modified_counter); - SDL_Delay(1); - } - } -} - -uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count) -{ - (void)count; - tp->x = monitor.touch_x; - tp->y = monitor.touch_y; - tp->size = monitor.touched ? 1 : 0; - tp->id = 0; - return monitor.touched; -} - -void Panel_sdl::setWindowTitle(const char *title) -{ - _window_title = title; - if (monitor.window) { - SDL_SetWindowTitle(monitor.window, _window_title); - } -} - -void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy) -{ - mon->scaling_x = sx; - mon->scaling_y = sy; - int nw = mon->frame_width; - int nh = mon->frame_height; - if (mon->frame_rotation & 1) { - std::swap(nw, nh); - } - - int x, y, w, h; - int rw, rh; - SDL_GetRendererOutputSize(mon->renderer, &rw, &rh); - SDL_GetWindowSize(mon->window, &w, &h); - nw = nw * sx * w / rw; - nh = nh * sy * h / rh; - SDL_GetWindowPosition(mon->window, &x, &y); - SDL_SetWindowSize(mon->window, nw, nh); - SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2); - mon->panel->sdl_invalidate(); -} - -void Panel_sdl::sdl_create(monitor_t *m) -{ - int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; +void Panel_sdl::sdl_create(monitor_t *m) { + int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; #if SDL_FULLSCREEN - flag |= SDL_WINDOW_FULLSCREEN; + flag |= SDL_WINDOW_FULLSCREEN; #endif - if (m->frame_width < _cfg.panel_width) { - m->frame_width = _cfg.panel_width; - } - if (m->frame_height < _cfg.panel_height) { - m->frame_height = _cfg.panel_height; - } + if (m->frame_width < _cfg.panel_width) { + m->frame_width = _cfg.panel_width; + } + if (m->frame_height < _cfg.panel_height) { + m->frame_height = _cfg.panel_height; + } - int window_width = m->frame_width * m->scaling_x; - int window_height = m->frame_height * m->scaling_y; - int scaling_x = m->scaling_x; - int scaling_y = m->scaling_y; - if (m->frame_rotation & 1) { - std::swap(window_width, window_height); - std::swap(scaling_x, scaling_y); - } + int window_width = m->frame_width * m->scaling_x; + int window_height = m->frame_height * m->scaling_y; + int scaling_x = m->scaling_x; + int scaling_y = m->scaling_y; + if (m->frame_rotation & 1) { + std::swap(window_width, window_height); + std::swap(scaling_x, scaling_y); + } - { - m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, - flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/ - } - m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); - m->texture = - SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height); - SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE); + { + m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, + flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/ + } + m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + m->texture = SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height); + SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE); - if (m->frame_image) { - // 枠画像用のサーフェイスを作成 - auto sf = SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4, - 0xFF000000, 0xFF0000, 0xFF00, 0xFF); - if (sf != nullptr) { - // 枠画像からテクスチャを作成 - m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf); - SDL_FreeSurface(sf); - } + if (m->frame_image) { + // 枠画像用のサーフェイスを作成 + auto sf = + SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4, 0xFF000000, 0xFF0000, 0xFF00, 0xFF); + if (sf != nullptr) { + // 枠画像からテクスチャを作成 + m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf); + SDL_FreeSurface(sf); } - SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND); - _update_scaling(m, scaling_x, scaling_y); + } + SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND); + _update_scaling(m, scaling_x, scaling_y); } -void Panel_sdl::sdl_update(void) -{ - if (monitor.renderer == nullptr) { - sdl_create(&monitor); +void Panel_sdl::sdl_update(void) { + if (monitor.renderer == nullptr) { + sdl_create(&monitor); + } + + bool step_exec = _in_step_exec; + + if (_texupdate_counter != _modified_counter) { + pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false); + if (_write_depth == rgb565_2Byte) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } else if (_write_depth == rgb888_3Byte) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } else if (_write_depth == rgb332_1Byte) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } else if (_write_depth == grayscale_8bit) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; } - bool step_exec = _in_step_exec; - - if (_texupdate_counter != _modified_counter) { - pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false); - if (_write_depth == rgb565_2Byte) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } else if (_write_depth == rgb888_3Byte) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } else if (_write_depth == rgb332_1Byte) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } else if (_write_depth == grayscale_8bit) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } - - if (0 == SDL_LockMutex(_sdl_mutex)) { - _texupdate_counter = _modified_counter; - for (int y = 0; y < _cfg.panel_height; ++y) { - pc.src_x32 = 0; - pc.src_data = _lines_buffer[y]; - pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc); - } - SDL_UnlockMutex(_sdl_mutex); - SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t)); - } + if (0 == SDL_LockMutex(_sdl_mutex)) { + _texupdate_counter = _modified_counter; + for (int y = 0; y < _cfg.panel_height; ++y) { + pc.src_x32 = 0; + pc.src_data = _lines_buffer[y]; + pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc); + } + SDL_UnlockMutex(_sdl_mutex); + SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t)); } + } - int angle = monitor.frame_angle; - int target = (monitor.frame_rotation) * 90; - angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3); + int angle = monitor.frame_angle; + int target = (monitor.frame_rotation) * 90; + angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3); - if (monitor.frame_angle != angle) { // 表示する向きを変える - monitor.frame_angle = angle; - sdl_invalidate(); - } else if (monitor.frame_rotation & ~3u) { - monitor.frame_rotation &= 3; - monitor.frame_angle = (monitor.frame_rotation) * 90; - sdl_invalidate(); + if (monitor.frame_angle != angle) { // 表示する向きを変える + monitor.frame_angle = angle; + sdl_invalidate(); + } else if (monitor.frame_rotation & ~3u) { + monitor.frame_rotation &= 3; + monitor.frame_angle = (monitor.frame_rotation) * 90; + sdl_invalidate(); + } + + if (_invalidated || (_display_counter != _texupdate_counter)) { + SDL_RendererInfo info; + if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) { + // ステップ実行中はVSYNCを待機しない + if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) { + SDL_RenderSetVSync(monitor.renderer, !step_exec); + } } - - if (_invalidated || (_display_counter != _texupdate_counter)) { - SDL_RendererInfo info; - if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) { - // ステップ実行中はVSYNCを待機しない - if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) { - SDL_RenderSetVSync(monitor.renderer, !step_exec); - } - } - { - int red = 0; - int green = 0; - int blue = 0; + { + int red = 0; + int green = 0; + int blue = 0; #if defined(M5GFX_BACK_COLOR) - red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF; - green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF; - blue = ((M5GFX_BACK_COLOR)) & 0xFF; + red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF; + green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF; + blue = ((M5GFX_BACK_COLOR)) & 0xFF; #endif - SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF); - } - SDL_RenderClear(monitor.renderer); - if (_invalidated) { - _invalidated = false; - int mw, mh; - SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); - } - render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); - render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); - SDL_RenderPresent(monitor.renderer); - _display_counter = _texupdate_counter; - if (_invalidated) { - _invalidated = false; - SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF); - SDL_RenderClear(monitor.renderer); - render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, - angle); - render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); - SDL_RenderPresent(monitor.renderer); - } + SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF); } + SDL_RenderClear(monitor.renderer); + if (_invalidated) { + _invalidated = false; + int mw, mh; + SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); + } + render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); + render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); + SDL_RenderPresent(monitor.renderer); + _display_counter = _texupdate_counter; + if (_invalidated) { + _invalidated = false; + SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF); + SDL_RenderClear(monitor.renderer); + render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); + render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); + SDL_RenderPresent(monitor.renderer); + } + } } -void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle) -{ - SDL_Point pivot; - pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x; - pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y; - SDL_Rect dstrect; - dstrect.w = tw * monitor.scaling_x; - dstrect.h = th * monitor.scaling_y; - int mw, mh; - SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); - dstrect.x = mw / 2.0f - pivot.x; - dstrect.y = mh / 2.0f - pivot.y; - SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE); +void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle) { + SDL_Point pivot; + pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x; + pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y; + SDL_Rect dstrect; + dstrect.w = tw * monitor.scaling_x; + dstrect.h = th * monitor.scaling_y; + int mw, mh; + SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); + dstrect.x = mw / 2.0f - pivot.x; + dstrect.y = mh / 2.0f - pivot.y; + SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE); } -bool Panel_sdl::initFrameBuffer(size_t width, size_t height) -{ - uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *)); - if (nullptr == lineArray) { - return false; +bool Panel_sdl::initFrameBuffer(size_t width, size_t height) { + uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *)); + if (nullptr == lineArray) { + return false; + } + + _texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t)); + + /// 8byte alignment; + width = (width + 7) & ~7u; + + _lines_buffer = lineArray; + memset(lineArray, 0, height * sizeof(uint8_t *)); + + uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16); + + auto fb = framebuffer; + { + for (size_t y = 0; y < height; ++y) { + lineArray[y] = fb; + fb += width; } - - _texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t)); - - /// 8byte alignment; - width = (width + 7) & ~7u; - - _lines_buffer = lineArray; - memset(lineArray, 0, height * sizeof(uint8_t *)); - - uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16); - - auto fb = framebuffer; - { - for (size_t y = 0; y < height; ++y) { - lineArray[y] = fb; - fb += width; - } - } - return true; + } + return true; } -void Panel_sdl::deinitFrameBuffer(void) -{ - auto lines = _lines_buffer; - _lines_buffer = nullptr; - if (lines != nullptr) { - heap_free(lines[0]); - heap_free(lines); - } - if (_texturebuf) { - heap_free(_texturebuf); - _texturebuf = nullptr; - } +void Panel_sdl::deinitFrameBuffer(void) { + auto lines = _lines_buffer; + _lines_buffer = nullptr; + if (lines != nullptr) { + heap_free(lines[0]); + heap_free(lines); + } + if (_texturebuf) { + heap_free(_texturebuf); + _texturebuf = nullptr; + } } //---------------------------------------------------------------------------- diff --git a/src/graphics/Panel_sdl.hpp b/src/graphics/Panel_sdl.hpp index 802c6c5dc..0d72ad26e 100644 --- a/src/graphics/Panel_sdl.hpp +++ b/src/graphics/Panel_sdl.hpp @@ -36,129 +36,126 @@ Porting for SDL: #include "lgfx/v1/panel/Panel_FrameBufferBase.hpp" #include -namespace lgfx -{ -inline namespace v1 -{ +namespace lgfx { +inline namespace v1 { struct Panel_sdl; struct monitor_t { - SDL_Window *window = nullptr; - SDL_Renderer *renderer = nullptr; - SDL_Texture *texture = nullptr; - SDL_Texture *texture_frameimage = nullptr; - Panel_sdl *panel = nullptr; + SDL_Window *window = nullptr; + SDL_Renderer *renderer = nullptr; + SDL_Texture *texture = nullptr; + SDL_Texture *texture_frameimage = nullptr; + Panel_sdl *panel = nullptr; - // 外枠 - const void *frame_image = 0; - uint_fast16_t frame_width = 0; - uint_fast16_t frame_height = 0; - uint_fast16_t frame_inner_x = 0; - uint_fast16_t frame_inner_y = 0; - int_fast16_t frame_rotation = 0; - int_fast16_t frame_angle = 0; + // 外枠 + const void *frame_image = 0; + uint_fast16_t frame_width = 0; + uint_fast16_t frame_height = 0; + uint_fast16_t frame_inner_x = 0; + uint_fast16_t frame_inner_y = 0; + int_fast16_t frame_rotation = 0; + int_fast16_t frame_angle = 0; - float scaling_x = 1; - float scaling_y = 1; - int_fast16_t touch_x, touch_y; - bool touched = false; - bool closing = false; + float scaling_x = 1; + float scaling_y = 1; + int_fast16_t touch_x, touch_y; + bool touched = false; + bool closing = false; }; //---------------------------------------------------------------------------- struct Touch_sdl : public ITouch { - bool init(void) override { return true; } - void wakeup(void) override {} - void sleep(void) override {} - bool isEnable(void) override { return true; }; - uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; } + bool init(void) override { return true; } + void wakeup(void) override {} + void sleep(void) override {} + bool isEnable(void) override { return true; }; + uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; } }; //---------------------------------------------------------------------------- struct Panel_sdl : public Panel_FrameBufferBase { - static constexpr size_t EMULATED_GPIO_MAX = 128; - static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX]; + static constexpr size_t EMULATED_GPIO_MAX = 128; + static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX]; - public: - Panel_sdl(void); - virtual ~Panel_sdl(void); +public: + Panel_sdl(void); + virtual ~Panel_sdl(void); - bool init(bool use_reset) override; + bool init(bool use_reset) override; - color_depth_t setColorDepth(color_depth_t depth) override; + color_depth_t setColorDepth(color_depth_t depth) override; - void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; + void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; - // void setInvert(bool invert) override {} - void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override; - void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override; - void writeBlock(uint32_t rawcolor, uint32_t length) override; - void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, - bool use_dma) override; - void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override; - void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override; + // void setInvert(bool invert) override {} + void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override; + void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override; + void writeBlock(uint32_t rawcolor, uint32_t length) override; + void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) override; + void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override; + void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override; - uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override; + uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override; - void setWindowTitle(const char *title); - void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y); - void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y); - void setFrameRotation(uint_fast16_t frame_rotaion); - void setBrightness(uint8_t brightness) override{}; + void setWindowTitle(const char *title); + void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y); + void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y); + void setFrameRotation(uint_fast16_t frame_rotaion); + void setBrightness(uint8_t brightness) override{}; - static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; } - static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; } - static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; } + static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; } + static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; } + static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; } - static int setup(void); - static int loop(void); - static int close(void); + static int setup(void); + static int loop(void); + static int close(void); - static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512); + static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512); - static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; } + static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; } - struct KeyCodeMapping_t { - SDL_KeyCode keycode = SDLK_UNKNOWN; - uint8_t gpio = 0; - }; - static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio); - static int getKeyCodeMapping(SDL_KeyCode keyCode); + struct KeyCodeMapping_t { + SDL_KeyCode keycode = SDLK_UNKNOWN; + uint8_t gpio = 0; + }; + static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio); + static int getKeyCodeMapping(SDL_KeyCode keyCode); + +protected: + const char *_window_title = "LGFX Simulator"; + SDL_mutex *_sdl_mutex = nullptr; + + void sdl_create(monitor_t *m); + void sdl_update(void); + + touch_point_t _touch_point; + monitor_t monitor; + + rgb888_t *_texturebuf = nullptr; + uint_fast16_t _modified_counter; + uint_fast16_t _texupdate_counter; + uint_fast16_t _display_counter; + bool _invalidated; + + static void _event_proc(void); + static void _update_proc(void); + static void _update_scaling(monitor_t *m, float sx, float sy); + void sdl_invalidate(void) { _invalidated = true; } + void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle); + bool initFrameBuffer(size_t width, size_t height); + void deinitFrameBuffer(void); + + static SDL_Keymod _keymod; + + struct lock_t { + lock_t(Panel_sdl *parent); + ~lock_t(); protected: - const char *_window_title = "LGFX Simulator"; - SDL_mutex *_sdl_mutex = nullptr; - - void sdl_create(monitor_t *m); - void sdl_update(void); - - touch_point_t _touch_point; - monitor_t monitor; - - rgb888_t *_texturebuf = nullptr; - uint_fast16_t _modified_counter; - uint_fast16_t _texupdate_counter; - uint_fast16_t _display_counter; - bool _invalidated; - - static void _event_proc(void); - static void _update_proc(void); - static void _update_scaling(monitor_t *m, float sx, float sy); - void sdl_invalidate(void) { _invalidated = true; } - void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle); - bool initFrameBuffer(size_t width, size_t height); - void deinitFrameBuffer(void); - - static SDL_Keymod _keymod; - - struct lock_t { - lock_t(Panel_sdl *parent); - ~lock_t(); - - protected: - Panel_sdl *_parent; - }; + Panel_sdl *_parent; + }; }; //---------------------------------------------------------------------------- } // namespace v1 diff --git a/src/graphics/PointStruct.h b/src/graphics/PointStruct.h index 218731978..890f255fa 100644 --- a/src/graphics/PointStruct.h +++ b/src/graphics/PointStruct.h @@ -1,4 +1,4 @@ struct PointStruct { - int x; - int y; + int x; + int y; }; \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0012aeb5d..32d0aa2e0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -92,8 +92,7 @@ uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); using namespace meshtastic; /** @todo remove */ -namespace graphics -{ +namespace graphics { // This means the *visible* area (sh1106 can address 132, but shows 128 for example) #define IDLE_FRAMERATE 1 // in fps @@ -140,126 +139,117 @@ extern bool hasUnreadMessage; // Displays a temporary centered banner message (e.g., warning, status, etc.) // The banner appears in the center of the screen and disappears after the specified duration -void Screen::showSimpleBanner(const char *message, uint32_t durationMs) -{ - BannerOverlayOptions options; - options.message = message; - options.durationMs = durationMs; - options.notificationType = notificationTypeEnum::text_banner; - showOverlayBanner(options); +void Screen::showSimpleBanner(const char *message, uint32_t durationMs) { + BannerOverlayOptions options; + options.message = message; + options.durationMs = durationMs; + options.notificationType = notificationTypeEnum::text_banner; + showOverlayBanner(options); } // Called to trigger a banner with custom message and duration -void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) -{ +void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) { #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif - // Store the message and set the expiration timestamp - strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination - NotificationRenderer::alertBannerUntil = - (banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs; - NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr; - NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr; - NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount; - NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback; - NotificationRenderer::curSelected = banner_overlay_options.InitialSelected; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs; + NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr; + NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr; + NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount; + NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback; + NotificationRenderer::curSelected = banner_overlay_options.InitialSelected; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } // Called to trigger a banner with custom message and duration -void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback) -{ +void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback) { #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif - nodeDB->pause_sort(true); - // Store the message and set the expiration timestamp - strncpy(NotificationRenderer::alertBannerMessage, message, 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::alertBannerCallback = bannerCallback; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::curSelected = 0; - NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; + nodeDB->pause_sort(true); + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; + NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } // Called to trigger a banner with custom message and duration -void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, - std::function bannerCallback) -{ +void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback) { #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif - // Store the message and set the expiration timestamp - strncpy(NotificationRenderer::alertBannerMessage, message, 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::alertBannerCallback = bannerCallback; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::curSelected = 0; - NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker; - NotificationRenderer::numDigits = digits; - NotificationRenderer::currentNumber = 0; + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; + NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker; + NotificationRenderer::numDigits = digits; + NotificationRenderer::currentNumber = 0; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } -void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback) -{ - LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); +void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, std::function textCallback) { + LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - // Start OnScreenKeyboardModule session (non-touch variant) - OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); - NotificationRenderer::textInputCallback = textCallback; + // Start OnScreenKeyboardModule session (non-touch variant) + OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); + NotificationRenderer::textInputCallback = textCallback; - // Store the message and set the expiration timestamp (use same pattern as other notifications) - strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; + // Store the message and set the expiration timestamp (use same pattern as other notifications) + strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; - // Set the overlay using the same pattern as other notification types - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + // Set the overlay using the same pattern as other notification types + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } -static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - uint8_t module_frame; - // there's a little but in the UI transition code - // where it invokes the function at the correct offset - // in the array of "drawScreen" functions; however, - // the passed-state doesn't quite reflect the "current" - // screen, so we have to detect it. - if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) { - // if we're transitioning from the end of the frame list back around to the first - // frame, then we want this to be `0` - module_frame = state->transitionFrameTarget; - } else { - // otherwise, just display the module frame that's aligned with the current frame - module_frame = state->currentFrame; - } - MeshModule &pi = *moduleFrames.at(module_frame); - pi.drawFrame(display, state, x, y); +static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + uint8_t module_frame; + // there's a little but in the UI transition code + // where it invokes the function at the correct offset + // in the array of "drawScreen" functions; however, + // the passed-state doesn't quite reflect the "current" + // screen, so we have to detect it. + if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) { + // if we're transitioning from the end of the frame list back around to the first + // frame, then we want this to be `0` + module_frame = state->transitionFrameTarget; + } else { + // otherwise, just display the module frame that's aligned with the current frame + module_frame = state->currentFrame; + } + MeshModule &pi = *moduleFrames.at(module_frame); + pi.drawFrame(display, state, x, y); } /** @@ -268,28 +258,27 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int * We keep a series of "after you've gone 10 meters, what is your heading since * the last reference point?" */ -float Screen::estimatedHeading(double lat, double lon) -{ - static double oldLat, oldLon; - static float b; +float Screen::estimatedHeading(double lat, double lon) { + static double oldLat, oldLon; + static float b; - if (oldLat == 0) { - // just prepare for next time - oldLat = lat; - oldLon = lon; - - return b; - } - - float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon); - if (d < 10) // haven't moved enough, just keep current bearing - return b; - - b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; + if (oldLat == 0) { + // just prepare for next time oldLat = lat; oldLon = lon; return b; + } + + float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon); + if (d < 10) // haven't moved enough, just keep current bearing + return b; + + b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; + oldLat = lat; + oldLon = lon; + + return b; } /// We will skip one node - the one for us, so we just blindly loop over all @@ -304,1495 +293,1447 @@ SPIClass SPI1(HSPI); #endif Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry) - : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) -{ - graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; + : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) { + graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; - int32_t rawRGB = uiconfig.screen_rgb_color; + int32_t rawRGB = uiconfig.screen_rgb_color; - // Only validate the combined value once - if (rawRGB > 0 && rawRGB <= 255255255) { - // Extract each component as a normal int first - int r = (rawRGB >> 16) & 0xFF; - int g = (rawRGB >> 8) & 0xFF; - int b = rawRGB & 0xFF; - if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { - TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); - } + // Only validate the combined value once + if (rawRGB > 0 && rawRGB <= 255255255) { + // Extract each component as a normal int first + int r = (rawRGB >> 16) & 0xFF; + int g = (rawRGB >> 8) & 0xFF; + int b = rawRGB & 0xFF; + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); } + } #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) - dispdev = new SH1106Wire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new SH1106Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_ST7789) #ifdef ESP_PLATFORM - dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7789_SDA, - ST7789_MISO, ST7789_SCK); + dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7789_SDA, ST7789_MISO, ST7789_SCK); #else - dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif #elif defined(USE_ST7796) #ifdef ESP_PLATFORM - dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA, - ST7796_MISO, ST7796_SCK, TFT_SPI_FREQUENCY); + dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA, ST7796_MISO, ST7796_SCK, + TFT_SPI_FREQUENCY); #else - dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif #elif defined(USE_SSD1306) - dispdev = new SSD1306Wire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_SPISSD1306) - dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); - if (!dispdev->init()) { - LOG_DEBUG("Error: SSD1306 not detected!"); - } else { - static_cast(dispdev)->setHorizontalOffset(32); - LOG_INFO("SSD1306 init success"); - } -#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) - dispdev = new TFTDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); + if (!dispdev->init()) { + LOG_DEBUG("Error: SSD1306 not detected!"); + } else { + static_cast(dispdev)->setHorizontalOffset(32); + LOG_INFO("SSD1306 init success"); + } +#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || defined(RAK14014) || \ + defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) + dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) - dispdev = new EInkDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) - dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_ST7567) - dispdev = new ST7567Wire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (portduino_config.displayPanel != no_screen) { - LOG_DEBUG("Make TFTDisplay!"); - dispdev = new TFTDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - } else { - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (portduino_config.displayPanel != no_screen) { + LOG_DEBUG("Make TFTDisplay!"); + dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + } else { + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; } + } #else - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; #endif #if defined(USE_ST7789) - static_cast(dispdev)->setRGB(TFT_MESH); + static_cast(dispdev)->setRGB(TFT_MESH); #elif defined(USE_ST7796) - static_cast(dispdev)->setRGB(TFT_MESH); + static_cast(dispdev)->setRGB(TFT_MESH); #endif - ui = new OLEDDisplayUi(dispdev); - cmdQueue.setReader(this); + ui = new OLEDDisplayUi(dispdev); + cmdQueue.setReader(this); } -Screen::~Screen() -{ - delete[] graphics::normalFrames; -} +Screen::~Screen() { delete[] graphics::normalFrames; } /** * Prepare the display for the unit going to the lowest power mode possible. Most screens will just * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code */ -void Screen::doDeepSleep() -{ +void Screen::doDeepSleep() { #ifdef USE_EINK - setOn(false, graphics::UIRenderer::drawDeepSleepFrame); + setOn(false, graphics::UIRenderer::drawDeepSleepFrame); #else - // Without E-Ink display: - setOn(false); + // Without E-Ink display: + setOn(false); #endif } -void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) -{ - if (!useDisplay) - return; +void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) { + if (!useDisplay) + return; - if (on != screenOn) { - if (on) { - LOG_INFO("Turn on screen"); - powerMon->setState(meshtastic_PowerMon_State_Screen_On); + if (on != screenOn) { + if (on) { + LOG_INFO("Turn on screen"); + powerMon->setState(meshtastic_PowerMon_State_Screen_On); #ifdef T_WATCH_S3 - PMU->enablePowerOutput(XPOWERS_ALDO2); + PMU->enablePowerOutput(XPOWERS_ALDO2); #endif #if defined(MUZI_BASE) - dispdev->init(); - dispdev->setBrightness(brightness); - dispdev->flipScreenVertically(); - dispdev->resetDisplay(); - digitalWrite(SCREEN_12V_ENABLE, HIGH); - delay(100); + dispdev->init(); + dispdev->setBrightness(brightness); + dispdev->flipScreenVertically(); + dispdev->resetDisplay(); + digitalWrite(SCREEN_12V_ENABLE, HIGH); + delay(100); #endif #if !ARCH_PORTDUINO - dispdev->displayOn(); + dispdev->displayOn(); #endif #ifdef PIN_EINK_EN - if (uiconfig.screen_brightness == 1) - digitalWrite(PIN_EINK_EN, HIGH); + if (uiconfig.screen_brightness == 1) + digitalWrite(PIN_EINK_EN, HIGH); #elif defined(PCA_PIN_EINK_EN) - if (uiconfig.screen_brightness > 0) - io.digitalWrite(PCA_PIN_EINK_EN, HIGH); + if (uiconfig.screen_brightness > 0) + io.digitalWrite(PCA_PIN_EINK_EN, HIGH); #endif -#if defined(ST7789_CS) && \ - !defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here. - static_cast(dispdev)->setDisplayBrightness(brightness); +#if defined(ST7789_CS) && !defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here. + static_cast(dispdev)->setDisplayBrightness(brightness); #endif - dispdev->displayOn(); + dispdev->displayOn(); #if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) - ui->init(); + ui->init(); #endif #ifdef USE_ST7789 - pinMode(VTFT_CTRL, OUTPUT); - digitalWrite(VTFT_CTRL, LOW); - ui->init(); + pinMode(VTFT_CTRL, OUTPUT); + digitalWrite(VTFT_CTRL, LOW); + ui->init(); #ifdef ESP_PLATFORM - analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); + analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); #else - pinMode(VTFT_LEDA, OUTPUT); - digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); #endif #endif #ifdef USE_ST7796 - ui->init(); + ui->init(); #ifdef ESP_PLATFORM - analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); + analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); #else - pinMode(VTFT_LEDA, OUTPUT); - digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); #endif #endif - enabled = true; - setInterval(0); // Draw ASAP - runASAP = true; - } else { - powerMon->clearState(meshtastic_PowerMon_State_Screen_On); + enabled = true; + setInterval(0); // Draw ASAP + runASAP = true; + } else { + powerMon->clearState(meshtastic_PowerMon_State_Screen_On); #ifdef USE_EINK - // eInkScreensaver parameter is usually NULL (default argument), default frame used instead - setScreensaverFrames(einkScreensaver); + // eInkScreensaver parameter is usually NULL (default argument), default frame used instead + setScreensaverFrames(einkScreensaver); #endif #ifdef PIN_EINK_EN - digitalWrite(PIN_EINK_EN, LOW); + digitalWrite(PIN_EINK_EN, LOW); #elif defined(PCA_PIN_EINK_EN) - io.digitalWrite(PCA_PIN_EINK_EN, LOW); + io.digitalWrite(PCA_PIN_EINK_EN, LOW); #endif - dispdev->displayOff(); + dispdev->displayOff(); #ifdef SCREEN_12V_ENABLE - digitalWrite(SCREEN_12V_ENABLE, LOW); + digitalWrite(SCREEN_12V_ENABLE, LOW); #endif #ifdef USE_ST7789 - SPI1.end(); + SPI1.end(); #if defined(ARCH_ESP32) - pinMode(VTFT_LEDA, ANALOG); - pinMode(VTFT_CTRL, ANALOG); - pinMode(ST7789_RESET, ANALOG); - pinMode(ST7789_RS, ANALOG); - pinMode(ST7789_NSS, ANALOG); + pinMode(VTFT_LEDA, ANALOG); + pinMode(VTFT_CTRL, ANALOG); + pinMode(ST7789_RESET, ANALOG); + pinMode(ST7789_RS, ANALOG); + pinMode(ST7789_NSS, ANALOG); #else - nrf_gpio_cfg_default(VTFT_LEDA); - nrf_gpio_cfg_default(VTFT_CTRL); - nrf_gpio_cfg_default(ST7789_RESET); - nrf_gpio_cfg_default(ST7789_RS); - nrf_gpio_cfg_default(ST7789_NSS); + nrf_gpio_cfg_default(VTFT_LEDA); + nrf_gpio_cfg_default(VTFT_CTRL); + nrf_gpio_cfg_default(ST7789_RESET); + nrf_gpio_cfg_default(ST7789_RS); + nrf_gpio_cfg_default(ST7789_NSS); #endif #endif #ifdef USE_ST7796 - SPI1.end(); + SPI1.end(); #if defined(ARCH_ESP32) - pinMode(VTFT_LEDA, OUTPUT); - digitalWrite(VTFT_LEDA, LOW); - pinMode(ST7796_RESET, ANALOG); - pinMode(ST7796_RS, ANALOG); - pinMode(ST7796_NSS, ANALOG); + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, LOW); + pinMode(ST7796_RESET, ANALOG); + pinMode(ST7796_RS, ANALOG); + pinMode(ST7796_NSS, ANALOG); #else - nrf_gpio_cfg_default(VTFT_LEDA); - nrf_gpio_cfg_default(ST7796_RESET); - nrf_gpio_cfg_default(ST7796_RS); - nrf_gpio_cfg_default(ST7796_NSS); + nrf_gpio_cfg_default(VTFT_LEDA); + nrf_gpio_cfg_default(ST7796_RESET); + nrf_gpio_cfg_default(ST7796_RS); + nrf_gpio_cfg_default(ST7796_NSS); #endif #endif #ifdef T_WATCH_S3 - PMU->disablePowerOutput(XPOWERS_ALDO2); + PMU->disablePowerOutput(XPOWERS_ALDO2); #endif - enabled = false; - } - screenOn = on; + enabled = false; } + screenOn = on; + } } -void Screen::setup() -{ +void Screen::setup() { - // Enable display rendering - useDisplay = true; + // Enable display rendering + useDisplay = true; - // Load saved brightness from UI config - // For OLED displays (SSD1306), default brightness is 255 if not set - if (uiconfig.screen_brightness == 0) { + // Load saved brightness from UI config + // For OLED displays (SSD1306), default brightness is 255 if not set + if (uiconfig.screen_brightness == 0) { #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - brightness = 255; // Default for OLED + brightness = 255; // Default for OLED #else - brightness = BRIGHTNESS_DEFAULT; + brightness = BRIGHTNESS_DEFAULT; #endif - } else { - brightness = uiconfig.screen_brightness; - } + } else { + brightness = uiconfig.screen_brightness; + } - // Detect OLED subtype (if supported by board variant) + // Detect OLED subtype (if supported by board variant) #ifdef AutoOLEDWire_h - if (isAUTOOled) - static_cast(dispdev)->setDetected(model); + if (isAUTOOled) + static_cast(dispdev)->setDetected(model); #endif #if defined(USE_SH1107_128_64) || defined(USE_SH1107) - static_cast(dispdev)->setSubtype(7); + static_cast(dispdev)->setSubtype(7); #endif #if defined(USE_ST7789) && defined(TFT_MESH) - // Apply custom RGB color (e.g. Heltec T114/T190) - static_cast(dispdev)->setRGB(TFT_MESH); + // Apply custom RGB color (e.g. Heltec T114/T190) + static_cast(dispdev)->setRGB(TFT_MESH); #endif #if defined(MUZI_BASE) - dispdev->delayPoweron = true; + dispdev->delayPoweron = true; #endif #if defined(USE_ST7796) && defined(TFT_MESH) - // Custom text color, if defined in variant.h - static_cast(dispdev)->setRGB(TFT_MESH); + // Custom text color, if defined in variant.h + static_cast(dispdev)->setRGB(TFT_MESH); #endif - // Initialize display and UI system - ui->init(); - displayWidth = dispdev->width(); - displayHeight = dispdev->height(); + // Initialize display and UI system + ui->init(); + displayWidth = dispdev->width(); + displayHeight = dispdev->height(); - ui->setTimePerTransition(0); // Disable animation delays - ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) - ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) - ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active - ui->disableAllIndicators(); // Disable page indicator dots - ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance + ui->setTimePerTransition(0); // Disable animation delays + ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) + ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) + ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active + ui->disableAllIndicators(); // Disable page indicator dots + ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - // Apply loaded brightness + // Apply loaded brightness #if defined(ST7789_CS) - static_cast(dispdev)->setDisplayBrightness(brightness); + static_cast(dispdev)->setDisplayBrightness(brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) - dispdev->setBrightness(brightness); + dispdev->setBrightness(brightness); #endif - LOG_INFO("Applied screen brightness: %d", brightness); + LOG_INFO("Applied screen brightness: %d", brightness); - // Set custom overlay callbacks - static OverlayCallback overlays[] = { - graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame - }; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + // Set custom overlay callbacks + static OverlayCallback overlays[] = { + graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - // Enable UTF-8 to display mapping - dispdev->setFontTableLookupFunction(customFontTableLookup); + // Enable UTF-8 to display mapping + dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT - logo_timeout *= 2; // Give more time for branded boot logos + logo_timeout *= 2; // Give more time for branded boot logos #endif - // Configure alert frames (e.g., "Resuming..." or region name) - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh - alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Configure alert frames (e.g., "Resuming..." or region name) + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) - graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); - else + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) + graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); + else #endif - { - const char *region = myRegion ? myRegion->name : nullptr; - graphics::UIRenderer::drawIconScreen(region, display, state, x, y); - } - }; - ui->setFrames(alertFrames, 1); - ui->disableAutoTransition(); // Require manual navigation between frames + { + const char *region = myRegion ? myRegion->name : nullptr; + graphics::UIRenderer::drawIconScreen(region, display, state, x, y); + } + }; + ui->setFrames(alertFrames, 1); + ui->disableAutoTransition(); // Require manual navigation between frames - // Log buffer for on-screen logs (3 lines max) - dispdev->setLogBuffer(3, 32); + // Log buffer for on-screen logs (3 lines max) + dispdev->setLogBuffer(3, 32); - // Optional screen mirroring or flipping (e.g. for T-Beam orientation) + // Optional screen mirroring or flipping (e.g. for T-Beam orientation) #ifdef SCREEN_MIRROR - dispdev->mirrorScreen(); + dispdev->mirrorScreen(); #else - if (!config.display.flip_screen) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) - static_cast(dispdev)->flipScreenVertically(); + if (!config.display.flip_screen) { +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || defined(RAK14014) || \ + defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) + static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) - static_cast(dispdev)->flipScreenVertically(); + static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7796) - static_cast(dispdev)->mirrorScreen(); + static_cast(dispdev)->mirrorScreen(); #elif !defined(M5STACK_UNITC6L) - dispdev->flipScreenVertically(); + dispdev->flipScreenVertically(); #endif - } + } #endif - // Generate device ID from MAC address - uint8_t dmac[6]; - getMacAddr(dmac); - snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + // Generate device ID from MAC address + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); #if ARCH_PORTDUINO - handleSetOn(false); // Ensure proper init for Arduino targets + handleSetOn(false); // Ensure proper init for Arduino targets #endif - // Turn on display and trigger first draw - handleSetOn(true); - graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); - ui->update(); + // Turn on display and trigger first draw + handleSetOn(true); + graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); + ui->update(); #ifndef USE_EINK - ui->update(); // Some SSD1306 clones drop the first draw, so run twice + ui->update(); // Some SSD1306 clones drop the first draw, so run twice #endif - serialSinceMsec = millis(); + serialSinceMsec = millis(); #if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (portduino_config.touchscreenModule) { - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (portduino_config.touchscreenModule) { + touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); } + } #elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); + touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); #endif - // Subscribe to device status updates - powerStatusObserver.observe(&powerStatus->onNewStatus); - gpsStatusObserver.observe(&gpsStatus->onNewStatus); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + // Subscribe to device status updates + powerStatusObserver.observe(&powerStatus->onNewStatus); + gpsStatusObserver.observe(&gpsStatus->onNewStatus); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe(adminModule); #endif - if (inputBroker) - inputObserver.observe(inputBroker); + if (inputBroker) + inputObserver.observe(inputBroker); - // Load persisted messages into RAM - messageStore.loadFromFlash(); - LOG_INFO("MessageStore loaded from flash"); + // Load persisted messages into RAM + messageStore.loadFromFlash(); + LOG_INFO("MessageStore loaded from flash"); - // Notify modules that support UI events - MeshModule::observeUIEvents(&uiFrameEventObserver); + // Notify modules that support UI events + MeshModule::observeUIEvents(&uiFrameEventObserver); } -void Screen::setOn(bool on, FrameCallback einkScreensaver) -{ +void Screen::setOn(bool on, FrameCallback einkScreensaver) { #if defined(T_LORA_PAGER) - if (cardKbI2cImpl) - cardKbI2cImpl->toggleBacklight(on); + if (cardKbI2cImpl) + cardKbI2cImpl->toggleBacklight(on); #endif - if (!on) - // We handle off commands immediately, because they might be called because the CPU is shutting down - handleSetOn(false, einkScreensaver); - else - enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON}); + if (!on) + // We handle off commands immediately, because they might be called because the CPU is shutting down + handleSetOn(false, einkScreensaver); + else + enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON}); } -void Screen::forceDisplay(bool forceUiUpdate) -{ - // Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup. +void Screen::forceDisplay(bool forceUiUpdate) { + // Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup. #ifdef USE_EINK - // If requested, make sure queued commands are run, and UI has rendered a new frame - if (forceUiUpdate) { - // Force a display refresh, in addition to the UI update - // Changing the GPS status bar icon apparently doesn't register as a change in image - // (False negative of the image hashing algorithm used to skip identical frames) - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); + // If requested, make sure queued commands are run, and UI has rendered a new frame + if (forceUiUpdate) { + // Force a display refresh, in addition to the UI update + // Changing the GPS status bar icon apparently doesn't register as a change in image + // (False negative of the image hashing algorithm used to skip identical frames) + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - // No delay between UI frame rendering - setFastFramerate(); - - // Make sure all CMDs have run first - while (!cmdQueue.isEmpty()) - runOnce(); - - // Ensure at least one frame has drawn - uint64_t startUpdate; - do { - startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. - delay(10); - ui->update(); - } while (ui->getUiState()->lastUpdate < startUpdate); - - // Return to normal frame rate - targetFramerate = IDLE_FRAMERATE; - ui->setTargetFPS(targetFramerate); - } - - // Tell EInk class to update the display - static_cast(dispdev)->forceDisplay(); -#else // No delay between UI frame rendering - if (forceUiUpdate) { - setFastFramerate(); - } + setFastFramerate(); + + // Make sure all CMDs have run first + while (!cmdQueue.isEmpty()) + runOnce(); + + // Ensure at least one frame has drawn + uint64_t startUpdate; + do { + startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. + delay(10); + ui->update(); + } while (ui->getUiState()->lastUpdate < startUpdate); + + // Return to normal frame rate + targetFramerate = IDLE_FRAMERATE; + ui->setTargetFPS(targetFramerate); + } + + // Tell EInk class to update the display + static_cast(dispdev)->forceDisplay(); +#else + // No delay between UI frame rendering + if (forceUiUpdate) { + setFastFramerate(); + } #endif } static uint32_t lastScreenTransition; -int32_t Screen::runOnce() -{ - // If we don't have a screen, don't ever spend any CPU for us. - if (!useDisplay) { - enabled = false; - return RUN_SAME; +int32_t Screen::runOnce() { + // If we don't have a screen, don't ever spend any CPU for us. + if (!useDisplay) { + enabled = false; + return RUN_SAME; + } + + if (displayHeight == 0) { + displayHeight = dispdev->getHeight(); + } + + // Detect frame transitions and clear message cache when leaving text message screen + { + static int8_t lastFrameIndex = -1; + int8_t currentFrameIndex = ui->getUiState()->currentFrame; + int8_t textMsgIndex = framesetInfo.positions.textMessage; + + if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { + + if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { + graphics::MessageRenderer::clearMessageCache(); + } } - if (displayHeight == 0) { - displayHeight = dispdev->getHeight(); - } + lastFrameIndex = currentFrameIndex; + } - // Detect frame transitions and clear message cache when leaving text message screen - { - static int8_t lastFrameIndex = -1; - int8_t currentFrameIndex = ui->getUiState()->currentFrame; - int8_t textMsgIndex = framesetInfo.positions.textMessage; + menuHandler::handleMenuSwitch(dispdev); - if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { - - if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { - graphics::MessageRenderer::clearMessageCache(); - } - } - - lastFrameIndex = currentFrameIndex; - } - - menuHandler::handleMenuSwitch(dispdev); - - // Show boot screen for first logo_timeout seconds, then switch to normal operation. - // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup - static bool showingBootScreen = true; - if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) { - LOG_INFO("Done with boot screen"); - stopBootScreen(); - showingBootScreen = false; - } + // Show boot screen for first logo_timeout seconds, then switch to normal operation. + // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup + static bool showingBootScreen = true; + if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) { + LOG_INFO("Done with boot screen"); + stopBootScreen(); + showingBootScreen = false; + } #ifdef USERPREFS_OEM_TEXT - static bool showingOEMBootScreen = true; - if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { - LOG_INFO("Switch to OEM screen..."); - // Change frames. - static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; - static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); - ui->setFrames(bootOEMFrames, bootOEMFrameCount); - ui->update(); + static bool showingOEMBootScreen = true; + if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { + LOG_INFO("Switch to OEM screen..."); + // Change frames. + static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; + static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); + ui->setFrames(bootOEMFrames, bootOEMFrameCount); + ui->update(); #ifndef USE_EINK - ui->update(); + ui->update(); #endif - showingOEMBootScreen = false; - } + showingOEMBootScreen = false; + } #endif #ifndef DISABLE_WELCOME_UNSET - if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { #if defined(M5STACK_UNITC6L) - menuHandler::LoraRegionPicker(); + menuHandler::LoraRegionPicker(); #else - menuHandler::OnboardMessage(); + menuHandler::OnboardMessage(); #endif - } + } #endif - if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { - showSimpleBanner("Rebooting...", 0); - } + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + showSimpleBanner("Rebooting...", 0); + } - // Process incoming commands. - for (;;) { - ScreenCmd cmd; - if (!cmdQueue.dequeue(&cmd, 0)) { - break; - } - switch (cmd.cmd) { - case Cmd::SET_ON: - handleSetOn(true); - break; - case Cmd::SET_OFF: - handleSetOn(false); - break; - case Cmd::ON_PRESS: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - showFrame(FrameDirection::NEXT); - } - break; - case Cmd::SHOW_PREV_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - showFrame(FrameDirection::PREVIOUS); - } - break; - case Cmd::SHOW_NEXT_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - showFrame(FrameDirection::NEXT); - } - break; - case Cmd::START_ALERT_FRAME: { - showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away - showingNormalScreen = false; - NotificationRenderer::pauseBanner = true; - alertFrames[0] = alertFrame; + // Process incoming commands. + for (;;) { + ScreenCmd cmd; + if (!cmdQueue.dequeue(&cmd, 0)) { + break; + } + switch (cmd.cmd) { + case Cmd::SET_ON: + handleSetOn(true); + break; + case Cmd::SET_OFF: + handleSetOn(false); + break; + case Cmd::ON_PRESS: + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + showFrame(FrameDirection::NEXT); + } + break; + case Cmd::SHOW_PREV_FRAME: + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + showFrame(FrameDirection::PREVIOUS); + } + break; + case Cmd::SHOW_NEXT_FRAME: + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + showFrame(FrameDirection::NEXT); + } + break; + case Cmd::START_ALERT_FRAME: { + showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away + showingNormalScreen = false; + NotificationRenderer::pauseBanner = true; + alertFrames[0] = alertFrame; #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) #endif - setFrameImmediateDraw(alertFrames); - break; - } - case Cmd::START_FIRMWARE_UPDATE_SCREEN: - handleStartFirmwareUpdateScreen(); - break; - case Cmd::STOP_ALERT_FRAME: - NotificationRenderer::pauseBanner = false; - break; - case Cmd::STOP_BOOT_SCREEN: - EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - setFrames(); - } - break; - case Cmd::NOOP: - break; - default: - LOG_ERROR("Invalid screen cmd"); - } + setFrameImmediateDraw(alertFrames); + break; } - - if (!screenOn) { // If we didn't just wake and the screen is still off, then - // stop updating until it is on again - enabled = false; - return 0; + case Cmd::START_FIRMWARE_UPDATE_SCREEN: + handleStartFirmwareUpdateScreen(); + break; + case Cmd::STOP_ALERT_FRAME: + NotificationRenderer::pauseBanner = false; + break; + case Cmd::STOP_BOOT_SCREEN: + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } + break; + case Cmd::NOOP: + break; + default: + LOG_ERROR("Invalid screen cmd"); } + } - // this must be before the frameState == FIXED check, because we always - // want to draw at least one FIXED frame before doing forceDisplay - ui->update(); + if (!screenOn) { // If we didn't just wake and the screen is still off, then + // stop updating until it is on again + enabled = false; + return 0; + } - // Switch to a low framerate (to save CPU) when we are not in transition - // but we should only call setTargetFPS when framestate changes, because - // otherwise that breaks animations. + // this must be before the frameState == FIXED check, because we always + // want to draw at least one FIXED frame before doing forceDisplay + ui->update(); - if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { - // oldFrameState = ui->getUiState()->frameState; - targetFramerate = IDLE_FRAMERATE; + // Switch to a low framerate (to save CPU) when we are not in transition + // but we should only call setTargetFPS when framestate changes, because + // otherwise that breaks animations. - ui->setTargetFPS(targetFramerate); - forceDisplay(); - } + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { + // oldFrameState = ui->getUiState()->frameState; + targetFramerate = IDLE_FRAMERATE; - // While showing the bootscreen or Bluetooth pair screen all of our - // standard screen switching is stopped. - if (showingNormalScreen) { - // standard screen loop handling here - if (config.display.auto_screen_carousel_secs > 0 && - NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && - !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { + ui->setTargetFPS(targetFramerate); + forceDisplay(); + } - // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead - // Carousel is potentially a major source of E-Ink display wear + // While showing the bootscreen or Bluetooth pair screen all of our + // standard screen switching is stopped. + if (showingNormalScreen) { + // standard screen loop handling here + if (config.display.auto_screen_carousel_secs > 0 && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && + !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { + + // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead + // Carousel is potentially a major source of E-Ink display wear #if !defined(EINK_BACKGROUND_USES_FAST) - EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); #endif - LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition)); - handleOnPress(); - } + LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition)); + handleOnPress(); } + } - // LOG_DEBUG("want fps %d, fixed=%d", targetFramerate, - // ui->getUiState()->frameState); If we are scrolling we need to be called - // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice - // as fast as we really need so that any rounding errors still result with - // the correct framerate - return (1000 / targetFramerate); + // LOG_DEBUG("want fps %d, fixed=%d", targetFramerate, + // ui->getUiState()->frameState); If we are scrolling we need to be called + // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice + // as fast as we really need so that any rounding errors still result with + // the correct framerate + return (1000 / targetFramerate); } /* show a message that the SSL cert is being built * it is expected that this will be used during the boot phase */ -void Screen::setSSLFrames() -{ - if (address_found.address) { - // LOG_DEBUG("Show SSL frames"); - static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; - ui->setFrames(sslFrames, 1); - ui->update(); - } +void Screen::setSSLFrames() { + if (address_found.address) { + // LOG_DEBUG("Show SSL frames"); + static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; + ui->setFrames(sslFrames, 1); + ui->update(); + } } #ifdef USE_EINK /// Determine which screensaver frame to use, then set the FrameCallback -void Screen::setScreensaverFrames(FrameCallback einkScreensaver) -{ - // Retain specified frame / overlay callback beyond scope of this method - static FrameCallback screensaverFrame; - static OverlayCallback screensaverOverlay; +void Screen::setScreensaverFrames(FrameCallback einkScreensaver) { + // Retain specified frame / overlay callback beyond scope of this method + static FrameCallback screensaverFrame; + static OverlayCallback screensaverOverlay; #if defined(HAS_EINK_ASYNCFULL) && defined(USE_EINK_DYNAMICDISPLAY) - // Join (await) a currently running async refresh, then run the post-update code. - // Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread. - EINK_JOIN_ASYNCREFRESH(dispdev); + // Join (await) a currently running async refresh, then run the post-update code. + // Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread. + EINK_JOIN_ASYNCREFRESH(dispdev); #endif - // If: one-off screensaver frame passed as argument. Handles doDeepSleep() - if (einkScreensaver != NULL) { - screensaverFrame = einkScreensaver; - ui->setFrames(&screensaverFrame, 1); - } + // If: one-off screensaver frame passed as argument. Handles doDeepSleep() + if (einkScreensaver != NULL) { + screensaverFrame = einkScreensaver; + ui->setFrames(&screensaverFrame, 1); + } - // Else, display the usual "overlay" screensaver - else { - screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; - ui->setOverlays(&screensaverOverlay, 1); - } + // Else, display the usual "overlay" screensaver + else { + screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; + ui->setOverlays(&screensaverOverlay, 1); + } - // Request new frame, ASAP - setFastFramerate(); - uint64_t startUpdate; - do { - startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. - delay(1); - ui->update(); - } while (ui->getUiState()->lastUpdate < startUpdate); + // Request new frame, ASAP + setFastFramerate(); + uint64_t startUpdate; + do { + startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. + delay(1); + ui->update(); + } while (ui->getUiState()->lastUpdate < startUpdate); - // Old EInkDisplay class + // Old EInkDisplay class #if !defined(USE_EINK_DYNAMICDISPLAY) - static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit + static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit #endif - // Prepare now for next frame, shown when display wakes - ui->setOverlays(NULL, 0); // Clear overlay - setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally + // Prepare now for next frame, shown when display wakes + ui->setOverlays(NULL, 0); // Clear overlay + setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally - // Pick a refresh method, for when display wakes + // Pick a refresh method, for when display wakes #ifdef EINK_HASQUIRK_GHOSTING - EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" #else - EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh + EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh #endif } #endif // Regenerate the normal set of frames, focusing a specific frame if requested // Called when a frame should be added / removed, or custom frames should be cleared -void Screen::setFrames(FrameFocus focus) -{ - // Block setFrames calls when virtual keyboard is active to prevent overlay interference - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return; - } +void Screen::setFrames(FrameFocus focus) { + // Block setFrames calls when virtual keyboard is active to prevent overlay interference + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return; + } - uint8_t originalPosition = ui->getUiState()->currentFrame; - uint8_t previousFrameCount = framesetInfo.frameCount; - FramesetInfo fsi; // Location of specific frames, for applying focus parameter + uint8_t originalPosition = ui->getUiState()->currentFrame; + uint8_t previousFrameCount = framesetInfo.frameCount; + FramesetInfo fsi; // Location of specific frames, for applying focus parameter - graphics::UIRenderer::rebuildFavoritedNodes(); + graphics::UIRenderer::rebuildFavoritedNodes(); - LOG_DEBUG("Show standard frames"); - showingNormalScreen = true; + LOG_DEBUG("Show standard frames"); + showingNormalScreen = true; - indicatorIcons.clear(); + indicatorIcons.clear(); - size_t numframes = 0; + size_t numframes = 0; - // If we have a critical fault, show it first - fsi.positions.fault = numframes; - if (error_code) { - normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; - indicatorIcons.push_back(icon_error); - focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame - } + // If we have a critical fault, show it first + fsi.positions.fault = numframes; + if (error_code) { + normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; + indicatorIcons.push_back(icon_error); + focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame + } #if defined(DISPLAY_CLOCK_FRAME) - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; #if defined(M5STACK_UNITC6L) - normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; + normalFrames[numframes++] = + uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; #endif - indicatorIcons.push_back(digital_icon_clock); - } + indicatorIcons.push_back(digital_icon_clock); + } #endif - if (!hiddenFrames.home) { - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); - } + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } - fsi.positions.textMessage = numframes; - normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; - indicatorIcons.push_back(icon_mail); + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist_nodes) { - fsi.positions.nodelist_nodes = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; - indicatorIcons.push_back(icon_nodes); - } - if (!hiddenFrames.nodelist_location) { - fsi.positions.nodelist_location = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; - indicatorIcons.push_back(icon_list); - } + if (!hiddenFrames.nodelist_nodes) { + fsi.positions.nodelist_nodes = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_location) { + fsi.positions.nodelist_location = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; + indicatorIcons.push_back(icon_list); + } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - if (!hiddenFrames.nodelist_lastheard) { - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - } - if (!hiddenFrames.nodelist_hopsignal) { - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - } - if (!hiddenFrames.nodelist_distance) { - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); - } + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS #ifdef USE_EINK - if (!hiddenFrames.nodelist_bearings) { - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - } + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } #endif - if (!hiddenFrames.gps) { - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); - } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance && !hiddenFrames.lora) { - fsi.positions.lora = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; - indicatorIcons.push_back(icon_radio); - } - if (!hiddenFrames.system) { - fsi.positions.system = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; - indicatorIcons.push_back(icon_system); - } + if (RadioLibInterface::instance && !hiddenFrames.lora) { + fsi.positions.lora = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + } + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); + } #if !defined(DISPLAY_CLOCK_FRAME) - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); - } + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = + uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif - if (!hiddenFrames.chirpy) { - fsi.positions.chirpy = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; - indicatorIcons.push_back(chirpy_small); - } + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(chirpy_small); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!hiddenFrames.wifi && isWifiAvailable()) { - fsi.positions.wifi = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; - indicatorIcons.push_back(icon_wifi); - } + if (!hiddenFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); + } #endif - // Beware of what changes you make in this code! - // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! - // Inside of that callback, goes over to MeshModule.cpp and we run - // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr - // entries until we're ready to start building the matching entries. - // We are doing our best to keep the normalFrames vector - // and the moduleFrames vector in lock step. - moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); - LOG_DEBUG("Show %d module frames", moduleFrames.size()); + // Beware of what changes you make in this code! + // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! + // Inside of that callback, goes over to MeshModule.cpp and we run + // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr + // entries until we're ready to start building the matching entries. + // We are doing our best to keep the normalFrames vector + // and the moduleFrames vector in lock step. + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); + LOG_DEBUG("Show %d module frames", moduleFrames.size()); - for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { - // Draw the module frame, using the hack described above - if (*i != nullptr) { - normalFrames[numframes] = drawModuleFrame; + for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { + // Draw the module frame, using the hack described above + if (*i != nullptr) { + normalFrames[numframes] = drawModuleFrame; - // Check if the module being drawn has requested focus - // We will honor this request later, if setFrames was triggered by a UIFrameEvent - MeshModule *m = *i; - if (m && m->isRequestingFocus()) - fsi.positions.focusedModule = numframes; - if (m && m == waypointModule) - fsi.positions.waypoint = numframes; + // Check if the module being drawn has requested focus + // We will honor this request later, if setFrames was triggered by a UIFrameEvent + MeshModule *m = *i; + if (m && m->isRequestingFocus()) + fsi.positions.focusedModule = numframes; + if (m && m == waypointModule) + fsi.positions.waypoint = numframes; - indicatorIcons.push_back(icon_module); - numframes++; - } + indicatorIcons.push_back(icon_module); + numframes++; + } + } + + LOG_DEBUG("Added modules. numframes: %d", numframes); + + // We don't show the node info of our node (if we have it yet - we should) + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (numMeshNodes > 0) + numMeshNodes--; + + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; + + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } } - LOG_DEBUG("Added modules. numframes: %d", numframes); - - // We don't show the node info of our node (if we have it yet - we should) - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (numMeshNodes > 0) - numMeshNodes--; - - if (!hiddenFrames.show_favorites) { - // Temporary array to hold favorite node frames - std::vector favoriteFrames; - - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); - } - } - - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); - } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; - } + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; } + } - fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE - this->frameCount = numframes; // Save frame count for use in custom overlay - LOG_DEBUG("Finished build frames. numframes: %d", numframes); + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + this->frameCount = numframes; // Save frame count for use in custom overlay + LOG_DEBUG("Finished build frames. numframes: %d", numframes); - ui->setFrames(normalFrames, numframes); - ui->disableAllIndicators(); + ui->setFrames(normalFrames, numframes); + ui->disableAllIndicators(); - // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) - // Focus on a specific frame, in the frame set we just created - switch (focus) { - case FOCUS_DEFAULT: - ui->switchToFrame(fsi.positions.deviceFocused); - break; - case FOCUS_FAULT: - ui->switchToFrame(fsi.positions.fault); - break; - case FOCUS_MODULE: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.focusedModule); - break; - case FOCUS_CLOCK: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.clock); - break; - case FOCUS_SYSTEM: - ui->switchToFrame(fsi.positions.system); - break; + // Focus on a specific frame, in the frame set we just created + switch (focus) { + case FOCUS_DEFAULT: + ui->switchToFrame(fsi.positions.deviceFocused); + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_MODULE: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.focusedModule); + break; + case FOCUS_CLOCK: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.clock); + break; + case FOCUS_SYSTEM: + ui->switchToFrame(fsi.positions.system); + break; - case FOCUS_PRESERVE: - // No more adjustment — force stay on same index - if (previousFrameCount > fsi.frameCount) { - ui->switchToFrame(originalPosition - 1); - } else if (previousFrameCount < fsi.frameCount) { - ui->switchToFrame(originalPosition + 1); - } else { - ui->switchToFrame(originalPosition); - } - break; + case FOCUS_PRESERVE: + // No more adjustment — force stay on same index + if (previousFrameCount > fsi.frameCount) { + ui->switchToFrame(originalPosition - 1); + } else if (previousFrameCount < fsi.frameCount) { + ui->switchToFrame(originalPosition + 1); + } else { + ui->switchToFrame(originalPosition); } + break; + } - // Store the info about this frameset, for future setFrames calls - this->framesetInfo = fsi; + // Store the info about this frameset, for future setFrames calls + this->framesetInfo = fsi; - setFastFramerate(); // Draw ASAP + setFastFramerate(); // Draw ASAP } -void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) -{ - ui->disableAllIndicators(); - ui->setFrames(drawFrames, 1); - setFastFramerate(); +void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) { + ui->disableAllIndicators(); + ui->setFrames(drawFrames, 1); + setFastFramerate(); } -void Screen::toggleFrameVisibility(const std::string &frameName) -{ +void Screen::toggleFrameVisibility(const std::string &frameName) { #ifndef USE_EINK - if (frameName == "nodelist_nodes") { - hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; - } - if (frameName == "nodelist_location") { - hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; - } + if (frameName == "nodelist_nodes") { + hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; + } + if (frameName == "nodelist_location") { + hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; + } #endif #ifdef USE_EINK - if (frameName == "nodelist_lastheard") { - hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; - } - if (frameName == "nodelist_hopsignal") { - hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; - } - if (frameName == "nodelist_distance") { - hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; - } + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } #endif #if HAS_GPS #ifdef USE_EINK - if (frameName == "nodelist_bearings") { - hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; - } + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } #endif - if (frameName == "gps") { - hiddenFrames.gps = !hiddenFrames.gps; - } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } #endif - if (frameName == "lora") { - hiddenFrames.lora = !hiddenFrames.lora; - } - if (frameName == "clock") { - hiddenFrames.clock = !hiddenFrames.clock; - } - if (frameName == "show_favorites") { - hiddenFrames.show_favorites = !hiddenFrames.show_favorites; - } - if (frameName == "chirpy") { - hiddenFrames.chirpy = !hiddenFrames.chirpy; - } + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; + } } -bool Screen::isFrameHidden(const std::string &frameName) const -{ +bool Screen::isFrameHidden(const std::string &frameName) const { #ifndef USE_EINK - if (frameName == "nodelist_nodes") - return hiddenFrames.nodelist_nodes; - if (frameName == "nodelist_location") - return hiddenFrames.nodelist_location; + if (frameName == "nodelist_nodes") + return hiddenFrames.nodelist_nodes; + if (frameName == "nodelist_location") + return hiddenFrames.nodelist_location; #endif #ifdef USE_EINK - if (frameName == "nodelist_lastheard") - return hiddenFrames.nodelist_lastheard; - if (frameName == "nodelist_hopsignal") - return hiddenFrames.nodelist_hopsignal; - if (frameName == "nodelist_distance") - return hiddenFrames.nodelist_distance; + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; #endif #if HAS_GPS #ifdef USE_EINK - if (frameName == "nodelist_bearings") - return hiddenFrames.nodelist_bearings; + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; #endif - if (frameName == "gps") - return hiddenFrames.gps; + if (frameName == "gps") + return hiddenFrames.gps; #endif - if (frameName == "lora") - return hiddenFrames.lora; - if (frameName == "clock") - return hiddenFrames.clock; - if (frameName == "show_favorites") - return hiddenFrames.show_favorites; - if (frameName == "chirpy") - return hiddenFrames.chirpy; + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; - return false; + return false; } -void Screen::handleStartFirmwareUpdateScreen() -{ - LOG_DEBUG("Show firmware screen"); - showingNormalScreen = false; - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame +void Screen::handleStartFirmwareUpdateScreen() { + LOG_DEBUG("Show firmware screen"); + showingNormalScreen = false; + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; - setFrameImmediateDraw(frames); + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; + setFrameImmediateDraw(frames); } -void Screen::blink() -{ +void Screen::blink() { + setFastFramerate(); + uint8_t count = 10; + dispdev->setBrightness(254); + while (count > 0) { + dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); + dispdev->display(); + delay(50); + dispdev->clear(); + dispdev->display(); + delay(50); + count = count - 1; + } + // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in + // OLEDDisplay. + dispdev->setBrightness(brightness); +} + +void Screen::increaseBrightness() { + brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); + +#if defined(ST7789_CS) + // run the setDisplayBrightness function. This works on t-decks + static_cast(dispdev)->setDisplayBrightness(brightness); +#endif + + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ +} + +void Screen::decreaseBrightness() { + brightness = (brightness < 70) ? brightness : (brightness - 62); + +#if defined(ST7789_CS) + static_cast(dispdev)->setDisplayBrightness(brightness); +#endif + + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ +} + +void Screen::handleOnPress() { + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->nextFrame(); + lastScreenTransition = millis(); setFastFramerate(); - uint8_t count = 10; - dispdev->setBrightness(254); - while (count > 0) { - dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); - dispdev->display(); - delay(50); - dispdev->clear(); - dispdev->display(); - delay(50); - count = count - 1; + } +} + +void Screen::showFrame(FrameDirection direction) { + // Only advance frames when UI is stable + if (ui->getUiState()->frameState == FIXED) { + + if (direction == FrameDirection::NEXT) { + ui->nextFrame(); + } else { + ui->previousFrame(); } - // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in - // OLEDDisplay. - dispdev->setBrightness(brightness); -} -void Screen::increaseBrightness() -{ - brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); - -#if defined(ST7789_CS) - // run the setDisplayBrightness function. This works on t-decks - static_cast(dispdev)->setDisplayBrightness(brightness); -#endif - - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ -} - -void Screen::decreaseBrightness() -{ - brightness = (brightness < 70) ? brightness : (brightness - 62); - -#if defined(ST7789_CS) - static_cast(dispdev)->setDisplayBrightness(brightness); -#endif - - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ -} - -void Screen::handleOnPress() -{ - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } -} - -void Screen::showFrame(FrameDirection direction) -{ - // Only advance frames when UI is stable - if (ui->getUiState()->frameState == FIXED) { - - if (direction == FrameDirection::NEXT) { - ui->nextFrame(); - } else { - ui->previousFrame(); - } - - lastScreenTransition = millis(); - setFastFramerate(); - } + lastScreenTransition = millis(); + setFastFramerate(); + } } #ifndef SCREEN_TRANSITION_FRAMERATE #define SCREEN_TRANSITION_FRAMERATE 30 // fps #endif -void Screen::setFastFramerate() -{ +void Screen::setFastFramerate() { #if defined(M5STACK_UNITC6L) - dispdev->clear(); - dispdev->display(); + dispdev->clear(); + dispdev->display(); #endif - // We are about to start a transition so speed up fps - targetFramerate = SCREEN_TRANSITION_FRAMERATE; + // We are about to start a transition so speed up fps + targetFramerate = SCREEN_TRANSITION_FRAMERATE; - ui->setTargetFPS(targetFramerate); - setInterval(0); // redraw ASAP - runASAP = true; + ui->setTargetFPS(targetFramerate); + setInterval(0); // redraw ASAP + runASAP = true; } -int Screen::handleStatusUpdate(const meshtastic::Status *arg) -{ - switch (arg->getStatusType()) { - case STATUS_TYPE_NODE: - if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { - setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) - } - nodeDB->updateGUI = false; - break; - case STATUS_TYPE_POWER: - forceDisplay(true); - break; +int Screen::handleStatusUpdate(const meshtastic::Status *arg) { + switch (arg->getStatusType()) { + case STATUS_TYPE_NODE: + if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { + setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) } + nodeDB->updateGUI = false; + break; + case STATUS_TYPE_POWER: + forceDisplay(true); + break; + } - return 0; + return 0; } // Handles when message is received; will jump to text message frame. -int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) -{ - if (showingNormalScreen) { - if (packet->from == 0) { - // Outgoing message (likely sent from phone) - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - hiddenFrames.textMessage = true; - hasUnreadMessage = false; // Clear unread state when user replies +int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { + if (showingNormalScreen) { + if (packet->from == 0) { + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + hiddenFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies - setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list + } else { + // Incoming message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) + + // Only wake/force display if the configuration allows it + if (shouldWakeOnReceivedMessage()) { + setOn(true); // Wake up the screen first + forceDisplay(); // Forces screen redraw + } + // === Prepare banner/popup content === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + const meshtastic_Channel channel = channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); + const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + + const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); + + char banner[256]; + + bool isAlert = false; + + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_bell_buzzer) + // Check for bell character to determine if this message is an alert + for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == ASCII_BELL) { + isAlert = true; + break; + } + } + + // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any + // 'mute' preferences set to any specific node or channel. + // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + // Wake and force redraw so popup is visible immediately + if (shouldWakeOnReceivedMessage()) { + setOn(true); + forceDisplay(); + } + + // Build popup: title = message source name, content = message text (sanitized) + // Title + char titleBuf[64] = {0}; + if (longName && longName[0]) { + // Sanitize sender name + std::string t = sanitizeString(longName); + strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1); } else { - // Incoming message - devicestate.has_rx_text_message = true; // Needed to include the message frame - hasUnreadMessage = true; // Enables mail icon in the header - setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) + strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); + } - // Only wake/force display if the configuration allows it - if (shouldWakeOnReceivedMessage()) { - setOn(true); // Wake up the screen first - forceDisplay(); // Forces screen redraw - } - // === Prepare banner/popup content === - const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); - const meshtastic_Channel channel = - channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); - const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize + char content[256] = {0}; + { + std::string raw; + raw.reserve(packet->decoded.payload.size); + for (size_t i = 0; i < packet->decoded.payload.size; ++i) { + char c = msgRaw[i]; + if (c == ASCII_BELL) + continue; // strip bell + raw.push_back(c); + } + std::string sanitized = sanitizeString(raw); + strncpy(content, sanitized.c_str(), sizeof(content) - 1); + } - const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); - - char banner[256]; - - bool isAlert = false; - - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_bell_buzzer) - // Check for bell character to determine if this message is an alert - for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { - if (msgRaw[i] == ASCII_BELL) { - isAlert = true; - break; - } - } - - // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any - // 'mute' preferences set to any specific node or channel. - // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - // Wake and force redraw so popup is visible immediately - if (shouldWakeOnReceivedMessage()) { - setOn(true); - forceDisplay(); - } - - // Build popup: title = message source name, content = message text (sanitized) - // Title - char titleBuf[64] = {0}; - if (longName && longName[0]) { - // Sanitize sender name - std::string t = sanitizeString(longName); - strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1); - } else { - strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); - } - - // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize - char content[256] = {0}; - { - std::string raw; - raw.reserve(packet->decoded.payload.size); - for (size_t i = 0; i < packet->decoded.payload.size; ++i) { - char c = msgRaw[i]; - if (c == ASCII_BELL) - continue; // strip bell - raw.push_back(c); - } - std::string sanitized = sanitizeString(raw); - strncpy(content, sanitized.c_str(), sizeof(content) - 1); - } - - NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000); + NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000); // Maintain existing buzzer behavior on M5 if applicable #if defined(M5STACK_UNITC6L) - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || - (!isBroadcast(packet->to) && isToUs(packet))) { - playLongBeep(); - } -#endif - } else { - // No keyboard active: use regular banner flow, respecting mute settings - if (isAlert) { - if (longName && longName[0]) { - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); - } else { - strcpy(banner, "Alert Received"); - } - screen->showSimpleBanner(banner, 3000); - } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { - if (longName && longName[0]) { - if (currentResolution == ScreenResolution::UltraLow) { - strcpy(banner, "New Message"); - } else { - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); - } - } else { - strcpy(banner, "New Message"); - } -#if defined(M5STACK_UNITC6L) - screen->setOn(true); - screen->showSimpleBanner(banner, 1500); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || - (!isBroadcast(packet->to) && isToUs(packet))) { - // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either - // - packet contains an alert and alert bell buzzer is enabled - // - packet is a non-broadcast that is addressed to this node - playLongBeep(); - } -#else - screen->showSimpleBanner(banner, 3000); -#endif - } - } + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || + (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { + playLongBeep(); } +#endif + } else { + // No keyboard active: use regular banner flow, respecting mute settings + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + } else { + strcpy(banner, "Alert Received"); + } + screen->showSimpleBanner(banner, 3000); + } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else { + strcpy(banner, "New Message"); + } +#if defined(M5STACK_UNITC6L) + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || + (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { + // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either + // - packet contains an alert and alert bell buzzer is enabled + // - packet is a non-broadcast that is addressed to this node + playLongBeep(); + } +#else + screen->showSimpleBanner(banner, 3000); +#endif + } + } } + } - return 0; + return 0; } // Triggered by MeshModules -int Screen::handleUIFrameEvent(const UIFrameEvent *event) -{ - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return 0; - } - - if (showingNormalScreen) { - // Regenerate the frameset, potentially honoring a module's internal requestFocus() call - if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { - setFrames(FOCUS_MODULE); - } - - // Regenerate the frameset, while attempting to maintain focus on the current frame - else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { - setFrames(FOCUS_PRESERVE); - } - - // Don't regenerate the frameset, just re-draw whatever is on screen ASAP - else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { - setFastFramerate(); - } - - // Jump directly to the Text Message screen - else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { - setFrames(FOCUS_PRESERVE); // preserve current frame ordering - ui->switchToFrame(framesetInfo.positions.textMessage); - setFastFramerate(); // force redraw ASAP - } - } - +int Screen::handleUIFrameEvent(const UIFrameEvent *event) { + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { return 0; + } + + if (showingNormalScreen) { + // Regenerate the frameset, potentially honoring a module's internal requestFocus() call + if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { + setFrames(FOCUS_MODULE); + } + + // Regenerate the frameset, while attempting to maintain focus on the current frame + else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { + setFrames(FOCUS_PRESERVE); + } + + // Don't regenerate the frameset, just re-draw whatever is on screen ASAP + else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { + setFastFramerate(); + } + + // Jump directly to the Text Message screen + else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { + setFrames(FOCUS_PRESERVE); // preserve current frame ordering + ui->switchToFrame(framesetInfo.positions.textMessage); + setFastFramerate(); // force redraw ASAP + } + } + + return 0; } -int Screen::handleInputEvent(const InputEvent *event) -{ - LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar); - if (!screenOn) - return 0; +int Screen::handleInputEvent(const InputEvent *event) { + LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar); + if (!screenOn) + return 0; - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + return 0; + } -#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) - setFastFramerate(); // Draw ASAP +#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a + // screen draw. + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + setFastFramerate(); // Draw ASAP #endif - if (NotificationRenderer::isOverlayBannerShowing()) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); - menuHandler::handleMenuSwitch(dispdev); + menuHandler::handleMenuSwitch(dispdev); + return 0; + } + // UP/DOWN in message screen scrolls through message threads + if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + + if (event->inputEvent == INPUT_BROKER_UP) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollUp(); + setFastFramerate(); // match existing behavior return 0; + } } - // UP/DOWN in message screen scrolls through message threads - if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (event->inputEvent == INPUT_BROKER_UP) { - if (messageStore.getMessages().empty()) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else { - graphics::MessageRenderer::scrollUp(); - setFastFramerate(); // match existing behavior - return 0; - } - } - - if (event->inputEvent == INPUT_BROKER_DOWN) { - if (messageStore.getMessages().empty()) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else { - graphics::MessageRenderer::scrollDown(); - setFastFramerate(); - return 0; - } - } + if (event->inputEvent == INPUT_BROKER_DOWN) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollDown(); + setFastFramerate(); + return 0; + } } - // UP/DOWN in node list screens scrolls through node pages - if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { - if (event->inputEvent == INPUT_BROKER_UP) { - graphics::NodeListRenderer::scrollUp(); - setFastFramerate(); - return 0; - } - - if (event->inputEvent == INPUT_BROKER_DOWN) { - graphics::NodeListRenderer::scrollDown(); - setFastFramerate(); - return 0; - } + } + // UP/DOWN in node list screens scrolls through node pages + if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + if (event->inputEvent == INPUT_BROKER_UP) { + graphics::NodeListRenderer::scrollUp(); + setFastFramerate(); + return 0; } - // Use left or right input from a keyboard to move between frames, - // so long as a mesh module isn't using these events for some other purpose - if (showingNormalScreen) { - // Ask any MeshModules if they're handling keyboard input right now - bool inputIntercepted = false; - for (MeshModule *module : moduleFrames) { - if (module && module->interceptingKeyboardInput()) - inputIntercepted = true; - } + if (event->inputEvent == INPUT_BROKER_DOWN) { + graphics::NodeListRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } + // Use left or right input from a keyboard to move between frames, + // so long as a mesh module isn't using these events for some other purpose + if (showingNormalScreen) { - // If no modules are using the input, move between frames - if (!inputIntercepted) { + // Ask any MeshModules if they're handling keyboard input right now + bool inputIntercepted = false; + for (MeshModule *module : moduleFrames) { + if (module && module->interceptingKeyboardInput()) + inputIntercepted = true; + } + + // If no modules are using the input, move between frames + if (!inputIntercepted) { #if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2 - bool handledEncoderScroll = false; - const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 && - this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && - !messageStore.getMessages().empty()); - if (isTextMessageFrame) { - if (event->inputEvent == INPUT_BROKER_UP_LONG) { - graphics::MessageRenderer::nudgeScroll(-1); - handledEncoderScroll = true; - } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { - graphics::MessageRenderer::nudgeScroll(1); - handledEncoderScroll = true; - } - } - - if (handledEncoderScroll) { - setFastFramerate(); - return 0; - } -#endif - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showFrame(FrameDirection::PREVIOUS); - } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showFrame(FrameDirection::NEXT); - } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { - // Long press up button for fast frame switching - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { - // Long press down button for fast frame switching - showNextFrame(); - } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && - this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { - menuHandler::systemBaseMenu(); -#if HAS_GPS - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { - menuHandler::positionBaseMenu(); -#endif - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { - menuHandler::clockMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::loraMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (!messageStore.getMessages().empty()) { - menuHandler::messageResponseMenu(); - } else { - if (currentResolution == ScreenResolution::UltraLow) { - menuHandler::textMessageMenu(); - } else { - menuHandler::textMessageBaseMenu(); - } - } - } else if (framesetInfo.positions.firstFavorite != 255 && - this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && - this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { - menuHandler::favoriteBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { - menuHandler::nodeListMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { - menuHandler::wifiBaseMenu(); - } - } else if (event->inputEvent == INPUT_BROKER_BACK) { - showFrame(FrameDirection::PREVIOUS); - } else if (event->inputEvent == INPUT_BROKER_CANCEL) { - setOn(false); - } + bool handledEncoderScroll = false; + const bool isTextMessageFrame = + (framesetInfo.positions.textMessage != 255 && this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && + !messageStore.getMessages().empty()); + if (isTextMessageFrame) { + if (event->inputEvent == INPUT_BROKER_UP_LONG) { + graphics::MessageRenderer::nudgeScroll(-1); + handledEncoderScroll = true; + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + graphics::MessageRenderer::nudgeScroll(1); + handledEncoderScroll = true; } + } + + if (handledEncoderScroll) { + setFastFramerate(); + return 0; + } +#endif + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { + showFrame(FrameDirection::PREVIOUS); + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { + showFrame(FrameDirection::NEXT); + } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { + // Long press up button for fast frame switching + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + // Long press down button for fast frame switching + showNextFrame(); + } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && + this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + menuHandler::homeBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { + menuHandler::systemBaseMenu(); +#if HAS_GPS + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { + menuHandler::positionBaseMenu(); +#endif + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { + menuHandler::clockMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { + menuHandler::loraMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + if (!messageStore.getMessages().empty()) { + menuHandler::messageResponseMenu(); + } else { + if (currentResolution == ScreenResolution::UltraLow) { + menuHandler::textMessageMenu(); + } else { + menuHandler::textMessageBaseMenu(); + } + } + } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && + this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { + menuHandler::favoriteBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + menuHandler::nodeListMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { + menuHandler::wifiBaseMenu(); + } + } else if (event->inputEvent == INPUT_BROKER_BACK) { + showFrame(FrameDirection::PREVIOUS); + } else if (event->inputEvent == INPUT_BROKER_CANCEL) { + setOn(false); + } } + } - return 0; + return 0; } -int Screen::handleAdminMessage(AdminModule_ObserverData *arg) -{ - switch (arg->request->which_payload_variant) { - // Node removed manually (i.e. via app) - case meshtastic_AdminMessage_remove_by_nodenum_tag: - setFrames(FOCUS_PRESERVE); - *arg->result = AdminMessageHandleResult::HANDLED; - break; +int Screen::handleAdminMessage(AdminModule_ObserverData *arg) { + switch (arg->request->which_payload_variant) { + // Node removed manually (i.e. via app) + case meshtastic_AdminMessage_remove_by_nodenum_tag: + setFrames(FOCUS_PRESERVE); + *arg->result = AdminMessageHandleResult::HANDLED; + break; - // Default no-op, in case the admin message observable gets used by other classes in future - default: - break; - } - return 0; + // Default no-op, in case the admin message observable gets used by other classes in future + default: + break; + } + return 0; } -bool Screen::isOverlayBannerShowing() -{ - return NotificationRenderer::isOverlayBannerShowing(); -} +bool Screen::isOverlayBannerShowing() { return NotificationRenderer::isOverlayBannerShowing(); } } // namespace graphics @@ -1800,24 +1741,22 @@ bool Screen::isOverlayBannerShowing() graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN -bool shouldWakeOnReceivedMessage() -{ - /* - The goal here is to determine when we do NOT wake up the screen on message received: - - Any ext. notifications are turned on - - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - - If the battery level is very low - */ - if (moduleConfig.external_notification.enabled) { - return false; - } - if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, - meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, - meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { - return false; - } - if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { - return false; - } - return true; +bool shouldWakeOnReceivedMessage() { + /* + The goal here is to determine when we do NOT wake up the screen on message received: + - Any ext. notifications are turned on + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE + - If the battery level is very low + */ + if (moduleConfig.external_notification.enabled) { + return false; + } + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, + meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { + return false; + } + if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { + return false; + } + return true; } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 4bb808970..388e7b53f 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -10,19 +10,18 @@ #include #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) -namespace graphics -{ +namespace graphics { enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; struct BannerOverlayOptions { - const char *message; - uint32_t durationMs = 30000; - const char **optionsArrayPtr = nullptr; - const int *optionsEnumPtr = nullptr; - uint8_t optionsCount = 0; - std::function bannerCallback = nullptr; - int8_t InitialSelected = 0; - notificationTypeEnum notificationType = notificationTypeEnum::text_banner; + const char *message; + uint32_t durationMs = 30000; + const char **optionsArrayPtr = nullptr; + const int *optionsEnumPtr = nullptr; + uint8_t optionsCount = 0; + std::function bannerCallback = nullptr; + int8_t InitialSelected = 0; + notificationTypeEnum notificationType = notificationTypeEnum::text_banner; }; } // namespace graphics @@ -30,35 +29,33 @@ bool shouldWakeOnReceivedMessage(); #if !HAS_SCREEN #include "power.h" -namespace graphics -{ +namespace graphics { // Noop class for boards without screen. -class Screen -{ - public: - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - FOCUS_CLOCK, - FOCUS_SYSTEM, - }; +class Screen { +public: + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, + FOCUS_SYSTEM, + }; - explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - void onPress() {} - void setup() {} - void setOn(bool) {} - void doDeepSleep() {} - void forceDisplay(bool forceUiUpdate = false) {} - void startFirmwareUpdateScreen() {} - void increaseBrightness() {} - void decreaseBrightness() {} - void startAlert(const char *) {} - void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} - void showOverlayBanner(BannerOverlayOptions) {} - void setFrames(FrameFocus focus) {} - void endAlert() {} + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + void onPress() {} + void setup() {} + void setOn(bool) {} + void doDeepSleep() {} + void forceDisplay(bool forceUiUpdate = false) {} + void startFirmwareUpdateScreen() {} + void increaseBrightness() {} + void decreaseBrightness() {} + void startAlert(const char *) {} + void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} + void showOverlayBanner(BannerOverlayOptions) {} + void setFrames(FrameFocus focus) {} + void endAlert() {} }; } // namespace graphics #else @@ -129,45 +126,39 @@ class Screen /// Convert an integer GPS coords to a floating point #define DegD(i) (i * 1e-7) extern bool hasUnreadMessage; -namespace -{ +namespace { /// A basic 2D point class for drawing -class Point -{ - public: - float x, y; +class Point { +public: + float x, y; - Point(float _x, float _y) : x(_x), y(_y) {} + Point(float _x, float _y) : x(_x), y(_y) {} - /// Apply a rotation around zero (standard rotation matrix math) - void rotate(float radian) - { - float cos = cosf(radian), sin = sinf(radian); - float rx = x * cos + y * sin, ry = -x * sin + y * cos; + /// Apply a rotation around zero (standard rotation matrix math) + void rotate(float radian) { + float cos = cosf(radian), sin = sinf(radian); + float rx = x * cos + y * sin, ry = -x * sin + y * cos; - x = rx; - y = ry; - } + x = rx; + y = ry; + } - void translate(int16_t dx, int dy) - { - x += dx; - y += dy; - } + void translate(int16_t dx, int dy) { + x += dx; + y += dy; + } - void scale(float f) - { - // We use -f here to counter the flip that happens - // on the y axis when drawing and rotating on screen - x *= f; - y *= -f; - } + void scale(float f) { + // We use -f here to counter the flip that happens + // on the y axis when drawing and rotating on screen + x *= f; + y *= -f; + } }; } // namespace -namespace graphics -{ +namespace graphics { enum class FrameDirection { NEXT, PREVIOUS }; @@ -175,24 +166,23 @@ enum class FrameDirection { NEXT, PREVIOUS }; class Screen; /// Handles gathering and displaying debug information. -class DebugInfo -{ - public: - DebugInfo(const DebugInfo &) = delete; - DebugInfo &operator=(const DebugInfo &) = delete; +class DebugInfo { +public: + DebugInfo(const DebugInfo &) = delete; + DebugInfo &operator=(const DebugInfo &) = delete; - private: - friend Screen; +private: + friend Screen; - DebugInfo() {} + DebugInfo() {} - /// Renders the debug screen. - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + /// Renders the debug screen. + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - /// Protects all of internal state. - concurrency::Lock lock; + /// Protects all of internal state. + concurrency::Lock lock; }; /** @@ -202,535 +192,523 @@ class DebugInfo * multiple times simultaneously. All state-changing calls are queued and executed * when the main loop calls us. */ -class Screen : public concurrency::OSThread -{ - CallbackObserver powerStatusObserver = - CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver gpsStatusObserver = - CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver uiFrameEventObserver = - CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules - CallbackObserver inputObserver = - CallbackObserver(this, &Screen::handleInputEvent); - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Screen::handleAdminMessage); +class Screen : public concurrency::OSThread { + CallbackObserver powerStatusObserver = + CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver gpsStatusObserver = + CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver uiFrameEventObserver = + CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules + CallbackObserver inputObserver = CallbackObserver(this, &Screen::handleInputEvent); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Screen::handleAdminMessage); - public: - OLEDDisplay *getDisplayDevice() { return dispdev; } - explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); +public: + OLEDDisplay *getDisplayDevice() { return dispdev; } + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - // Screen dimension accessors - inline int getHeight() const { return displayHeight; } - inline int getWidth() const { return displayWidth; } - size_t frameCount = 0; // Total number of active frames - ~Screen(); + // Screen dimension accessors + inline int getHeight() const { return displayHeight; } + inline int getWidth() const { return displayWidth; } + size_t frameCount = 0; // Total number of active frames + ~Screen(); - // Which frame we want to be displayed, after we regen the frameset by calling setFrames - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - FOCUS_CLOCK, - FOCUS_SYSTEM, - }; + // Which frame we want to be displayed, after we regen the frameset by calling setFrames + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, + FOCUS_SYSTEM, + }; - // Regenerate the normal set of frames, focusing a specific frame if requested - // Call when a frame should be added / removed, or custom frames should be cleared - void setFrames(FrameFocus focus = FOCUS_DEFAULT); + // Regenerate the normal set of frames, focusing a specific frame if requested + // Call when a frame should be added / removed, or custom frames should be cleared + void setFrames(FrameFocus focus = FOCUS_DEFAULT); - std::vector indicatorIcons; // Per-frame custom icon pointers - Screen(const Screen &) = delete; - Screen &operator=(const Screen &) = delete; + std::vector indicatorIcons; // Per-frame custom icon pointers + Screen(const Screen &) = delete; + Screen &operator=(const Screen &) = delete; - ScanI2C::DeviceAddress address_found; - meshtastic_Config_DisplayConfig_OledType model; - OLEDDISPLAY_GEOMETRY geometry; + ScanI2C::DeviceAddress address_found; + meshtastic_Config_DisplayConfig_OledType model; + OLEDDISPLAY_GEOMETRY geometry; - bool isOverlayBannerShowing(); + bool isOverlayBannerShowing(); - bool isScreenOn() { return screenOn; } + bool isScreenOn() { return screenOn; } - // Stores the last 4 of our hardware ID, to make finding the device for pairing easier - // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class - char ourId[5]; + // Stores the last 4 of our hardware ID, to make finding the device for pairing easier + // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class + char ourId[5]; - /// Initializes the UI, turns on the display, starts showing boot screen. - // - // Not thread safe - must be called before any other methods are called. - void setup(); + /// Initializes the UI, turns on the display, starts showing boot screen. + // + // Not thread safe - must be called before any other methods are called. + void setup(); - /// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink - void setOn(bool on, FrameCallback einkScreensaver = NULL); - /** - * Prepare the display for the unit going to the lowest power mode possible. Most screens will just - * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code - */ - void doDeepSleep(); + /// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink + void setOn(bool on, FrameCallback einkScreensaver = NULL); + /** + * Prepare the display for the unit going to the lowest power mode possible. Most screens will just + * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code + */ + void doDeepSleep(); - void blink(); + void blink(); - // Draw north - float estimatedHeading(double lat, double lon); + // Draw north + float estimatedHeading(double lat, double lon); - /// Handle button press, trackball or swipe action) - void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } - void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } - void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } - void showFrame(FrameDirection direction); + /// Handle button press, trackball or swipe action) + void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } + void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } + void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } + void showFrame(FrameDirection direction); - // generic alert start - void startAlert(FrameCallback _alertFrame) - { - alertFrame = _alertFrame; - ScreenCmd cmd; - cmd.cmd = Cmd::START_ALERT_FRAME; - enqueueCmd(cmd); + // generic alert start + void startAlert(FrameCallback _alertFrame) { + alertFrame = _alertFrame; + ScreenCmd cmd; + cmd.cmd = Cmd::START_ALERT_FRAME; + enqueueCmd(cmd); + } + + void startAlert(const char *_alertMessage) { + startAlert([_alertMessage](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, _alertMessage); + }); + } + + void endAlert() { + ScreenCmd cmd; + cmd.cmd = Cmd::STOP_ALERT_FRAME; + enqueueCmd(cmd); + } + + void showSimpleBanner(const char *message, uint32_t durationMs = 0); + void showOverlayBanner(BannerOverlayOptions); + + void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); + void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); + void showTextInput(const char *header, const char *initialText, uint32_t durationMs, std::function textCallback); + + void requestMenu(graphics::menuHandler::screenMenus menuToShow) { + graphics::menuHandler::menuQueue = menuToShow; + runNow(); + } + + void startFirmwareUpdateScreen() { + ScreenCmd cmd; + cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; + enqueueCmd(cmd); + } + + // Function to allow the AccelerometerThread to set the heading if a sensor provides it + // Mutex needed? + void setHeading(long _heading) { + hasCompass = true; + compassHeading = fmod(_heading, 360); + } + + bool hasHeading() { return hasCompass; } + + long getHeading() { return compassHeading; } + + void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } + uint32_t getEndCalibration() { return endCalibrationAt; } + + // functions for display brightness + void increaseBrightness(); + void decreaseBrightness(); + + /// Stops showing the boot screen. + void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } + + void runNow() { + setFastFramerate(); + enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP}); + } + + /// Overrides the default utf8 character conversion, to replace empty space with question marks + static char customFontTableLookup(const uint8_t ch) { + // UTF-8 to font table index converter + // Code from http://playground.arduino.cc/Main/Utf8ascii + static uint8_t LASTCHAR; + static bool SKIPREST; // Only display a single unconvertable-character symbol per sequence of unconvertable characters + + if (ch < 128) { // Standard ASCII-set 0..0x7F handling + LASTCHAR = 0; + SKIPREST = false; + return ch; } - void startAlert(const char *_alertMessage) - { - startAlert([_alertMessage](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, _alertMessage); - }); + uint8_t last = LASTCHAR; // get last char + LASTCHAR = ch; + + switch (last) { + case 0xC2: { + SKIPREST = false; + return (uint8_t)ch; } - void endAlert() - { - ScreenCmd cmd; - cmd.cmd = Cmd::STOP_ALERT_FRAME; - enqueueCmd(cmd); + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } } - void showSimpleBanner(const char *message, uint32_t durationMs = 0); - void showOverlayBanner(BannerOverlayOptions); - - void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); - void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); - void showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback); - - void requestMenu(graphics::menuHandler::screenMenus menuToShow) - { - graphics::menuHandler::menuQueue = menuToShow; - runNow(); - } - - void startFirmwareUpdateScreen() - { - ScreenCmd cmd; - cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; - enqueueCmd(cmd); - } - - // Function to allow the AccelerometerThread to set the heading if a sensor provides it - // Mutex needed? - void setHeading(long _heading) - { - hasCompass = true; - compassHeading = fmod(_heading, 360); - } - - bool hasHeading() { return hasCompass; } - - long getHeading() { return compassHeading; } - - void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } - uint32_t getEndCalibration() { return endCalibrationAt; } - - // functions for display brightness - void increaseBrightness(); - void decreaseBrightness(); - - /// Stops showing the boot screen. - void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } - - void runNow() - { - setFastFramerate(); - enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP}); - } - - /// Overrides the default utf8 character conversion, to replace empty space with question marks - static char customFontTableLookup(const uint8_t ch) - { - // UTF-8 to font table index converter - // Code from http://playground.arduino.cc/Main/Utf8ascii - static uint8_t LASTCHAR; - static bool SKIPREST; // Only display a single unconvertable-character symbol per sequence of unconvertable characters - - if (ch < 128) { // Standard ASCII-set 0..0x7F handling - LASTCHAR = 0; - SKIPREST = false; - return ch; - } - - uint8_t last = LASTCHAR; // get last char - LASTCHAR = ch; - - switch (last) { - case 0xC2: { - SKIPREST = false; - return (uint8_t)ch; - } - - case 0xC3: { - SKIPREST = false; - return (uint8_t)(ch | 0xC0); - } - } - - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3) - return (uint8_t)0; + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3) + return (uint8_t)0; #if defined(OLED_PL) - switch (last) { - case 0xC3: { + switch (last) { + case 0xC3: { - if (ch == 147) - return (uint8_t)(ch); // Ó - else if (ch == 179) - return (uint8_t)(148); // ó - else - return (uint8_t)(ch | 0xC0); - break; - } + if (ch == 147) + return (uint8_t)(ch); // Ó + else if (ch == 179) + return (uint8_t)(148); // ó + else + return (uint8_t)(ch | 0xC0); + break; + } - case 0xC4: { - SKIPREST = false; - return (uint8_t)(ch); - } + case 0xC4: { + SKIPREST = false; + return (uint8_t)(ch); + } - case 0xC5: { - SKIPREST = false; - if (ch == 132) - return (uint8_t)(136); // ń - else if (ch == 186) - return (uint8_t)(137); // ź - else - return (uint8_t)(ch); - break; - } - } + case 0xC5: { + SKIPREST = false; + if (ch == 132) + return (uint8_t)(136); // ń + else if (ch == 186) + return (uint8_t)(137); // ź + else + return (uint8_t)(ch); + break; + } + } - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) - return (uint8_t)0; + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) + return (uint8_t)0; #endif #if defined(OLED_UA) || defined(OLED_RU) - switch (last) { - case 0xC3: { - SKIPREST = false; - return (uint8_t)(ch | 0xC0); - } - // map UTF-8 cyrillic chars to it Windows-1251 (CP-1251) ASCII codes - // note: in this case we must use compatible font - provided ArialMT_Plain_10/16/24 by 'ThingPulse/esp8266-oled-ssd1306' - // library have empty chars for non-latin ASCII symbols - case 0xD0: { - SKIPREST = false; - if (ch == 132) - return (uint8_t)(170); // Є - if (ch == 134) - return (uint8_t)(178); // І - if (ch == 135) - return (uint8_t)(175); // Ї - if (ch == 129) - return (uint8_t)(168); // Ё - if (ch > 143 && ch < 192) - return (uint8_t)(ch + 48); - break; - } - case 0xD1: { - SKIPREST = false; - if (ch == 148) - return (uint8_t)(186); // є - if (ch == 150) - return (uint8_t)(179); // і - if (ch == 151) - return (uint8_t)(191); // ї - if (ch == 145) - return (uint8_t)(184); // ё - if (ch > 127 && ch < 144) - return (uint8_t)(ch + 112); - break; - } - case 0xD2: { - SKIPREST = false; - if (ch == 144) - return (uint8_t)(165); // Ґ - if (ch == 145) - return (uint8_t)(180); // ґ - break; - } - } + switch (last) { + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } + // map UTF-8 cyrillic chars to it Windows-1251 (CP-1251) ASCII codes + // note: in this case we must use compatible font - provided ArialMT_Plain_10/16/24 by + // 'ThingPulse/esp8266-oled-ssd1306' library have empty chars for non-latin ASCII symbols + case 0xD0: { + SKIPREST = false; + if (ch == 132) + return (uint8_t)(170); // Є + if (ch == 134) + return (uint8_t)(178); // І + if (ch == 135) + return (uint8_t)(175); // Ї + if (ch == 129) + return (uint8_t)(168); // Ё + if (ch > 143 && ch < 192) + return (uint8_t)(ch + 48); + break; + } + case 0xD1: { + SKIPREST = false; + if (ch == 148) + return (uint8_t)(186); // є + if (ch == 150) + return (uint8_t)(179); // і + if (ch == 151) + return (uint8_t)(191); // ї + if (ch == 145) + return (uint8_t)(184); // ё + if (ch > 127 && ch < 144) + return (uint8_t)(ch + 112); + break; + } + case 0xD2: { + SKIPREST = false; + if (ch == 144) + return (uint8_t)(165); // Ґ + if (ch == 145) + return (uint8_t)(180); // ґ + break; + } + } - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3 || ch == 0x82 || ch == 0xD0 || ch == 0xD1) - return (uint8_t)0; + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0x82 || ch == 0xD0 || ch == 0xD1) + return (uint8_t)0; #endif #if defined(OLED_CS) - switch (last) { - case 0xC2: { - SKIPREST = false; - return (uint8_t)ch; - } - - case 0xC3: { - SKIPREST = false; - return (uint8_t)(ch | 0xC0); - } - - case 0xC4: { - SKIPREST = false; - if (ch == 140) - return (uint8_t)(129); // Č - if (ch == 141) - return (uint8_t)(138); // č - if (ch == 142) - return (uint8_t)(130); // Ď - if (ch == 143) - return (uint8_t)(139); // ď - if (ch == 154) - return (uint8_t)(131); // Ě - if (ch == 155) - return (uint8_t)(140); // ě - // Slovak specific glyphs - if (ch == 185) - return (uint8_t)(147); // Ĺ - if (ch == 186) - return (uint8_t)(148); // ĺ - if (ch == 189) - return (uint8_t)(149); // Ľ - if (ch == 190) - return (uint8_t)(150); // ľ - break; - } - - case 0xC5: { - SKIPREST = false; - if (ch == 135) - return (uint8_t)(132); // Ň - if (ch == 136) - return (uint8_t)(141); // ň - if (ch == 152) - return (uint8_t)(133); // Ř - if (ch == 153) - return (uint8_t)(142); // ř - if (ch == 160) - return (uint8_t)(134); // Š - if (ch == 161) - return (uint8_t)(143); // š - if (ch == 164) - return (uint8_t)(135); // Ť - if (ch == 165) - return (uint8_t)(144); // ť - if (ch == 174) - return (uint8_t)(136); // Ů - if (ch == 175) - return (uint8_t)(145); // ů - if (ch == 189) - return (uint8_t)(137); // Ž - if (ch == 190) - return (uint8_t)(146); // ž - // Slovak specific glyphs - if (ch == 148) - return (uint8_t)(151); // Ŕ - if (ch == 149) - return (uint8_t)(152); // ŕ - break; - } - } - - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) - return (uint8_t)0; - -#endif - - // If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the - // rest of it - if (SKIPREST) - return (uint8_t)0; - SKIPREST = true; - - return (uint8_t)191; // otherwise: return ¿ if character can't be converted (note that the font map we're using doesn't - // stick to standard EASCII codes) + switch (last) { + case 0xC2: { + SKIPREST = false; + return (uint8_t)ch; } - /// Returns a handle to the DebugInfo screen. - // - // Use this handle to set things like battery status, user count, GPS status, etc. - DebugInfo *debug_info() { return &debugInfo; } + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } - // Handle observer events - int handleStatusUpdate(const meshtastic::Status *arg); - int handleTextMessage(const meshtastic_MeshPacket *packet); - int handleUIFrameEvent(const UIFrameEvent *arg); - int handleInputEvent(const InputEvent *arg); - int handleAdminMessage(AdminModule_ObserverData *arg); + case 0xC4: { + SKIPREST = false; + if (ch == 140) + return (uint8_t)(129); // Č + if (ch == 141) + return (uint8_t)(138); // č + if (ch == 142) + return (uint8_t)(130); // Ď + if (ch == 143) + return (uint8_t)(139); // ď + if (ch == 154) + return (uint8_t)(131); // Ě + if (ch == 155) + return (uint8_t)(140); // ě + // Slovak specific glyphs + if (ch == 185) + return (uint8_t)(147); // Ĺ + if (ch == 186) + return (uint8_t)(148); // ĺ + if (ch == 189) + return (uint8_t)(149); // Ľ + if (ch == 190) + return (uint8_t)(150); // ľ + break; + } - /// Used to force (super slow) eink displays to draw critical frames - void forceDisplay(bool forceUiUpdate = false); + case 0xC5: { + SKIPREST = false; + if (ch == 135) + return (uint8_t)(132); // Ň + if (ch == 136) + return (uint8_t)(141); // ň + if (ch == 152) + return (uint8_t)(133); // Ř + if (ch == 153) + return (uint8_t)(142); // ř + if (ch == 160) + return (uint8_t)(134); // Š + if (ch == 161) + return (uint8_t)(143); // š + if (ch == 164) + return (uint8_t)(135); // Ť + if (ch == 165) + return (uint8_t)(144); // ť + if (ch == 174) + return (uint8_t)(136); // Ů + if (ch == 175) + return (uint8_t)(145); // ů + if (ch == 189) + return (uint8_t)(137); // Ž + if (ch == 190) + return (uint8_t)(146); // ž + // Slovak specific glyphs + if (ch == 148) + return (uint8_t)(151); // Ŕ + if (ch == 149) + return (uint8_t)(152); // ŕ + break; + } + } - /// Draws our SSL cert screen during boot (called from WebServer) - void setSSLFrames(); + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) + return (uint8_t)0; - // Menu-driven Show / Hide Toggle - void toggleFrameVisibility(const std::string &frameName); - bool isFrameHidden(const std::string &frameName) const; - -#ifdef USE_EINK - /// Draw an image to remain on E-Ink display after screen off - void setScreensaverFrames(FrameCallback einkScreensaver = NULL); #endif - protected: - /// Updates the UI. - // - // Called periodically from the main loop. - int32_t runOnce() final; + // If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs + // for the rest of it + if (SKIPREST) + return (uint8_t)0; + SKIPREST = true; - bool isAUTOOled = false; + return (uint8_t)191; // otherwise: return ¿ if character can't be converted (note that the font map we're using + // doesn't stick to standard EASCII codes) + } - // Screen dimensions (for convenience) - // Defined during Screen::setup - uint16_t displayWidth = 0; - uint16_t displayHeight = 0; + /// Returns a handle to the DebugInfo screen. + // + // Use this handle to set things like battery status, user count, GPS status, etc. + DebugInfo *debug_info() { return &debugInfo; } - private: - FrameCallback alertFrames[1]; - struct ScreenCmd { - Cmd cmd; - union { - uint32_t bluetooth_pin; - char *print_text; - }; + // Handle observer events + int handleStatusUpdate(const meshtastic::Status *arg); + int handleTextMessage(const meshtastic_MeshPacket *packet); + int handleUIFrameEvent(const UIFrameEvent *arg); + int handleInputEvent(const InputEvent *arg); + int handleAdminMessage(AdminModule_ObserverData *arg); + + /// Used to force (super slow) eink displays to draw critical frames + void forceDisplay(bool forceUiUpdate = false); + + /// Draws our SSL cert screen during boot (called from WebServer) + void setSSLFrames(); + + // Menu-driven Show / Hide Toggle + void toggleFrameVisibility(const std::string &frameName); + bool isFrameHidden(const std::string &frameName) const; + +#ifdef USE_EINK + /// Draw an image to remain on E-Ink display after screen off + void setScreensaverFrames(FrameCallback einkScreensaver = NULL); +#endif + +protected: + /// Updates the UI. + // + // Called periodically from the main loop. + int32_t runOnce() final; + + bool isAUTOOled = false; + + // Screen dimensions (for convenience) + // Defined during Screen::setup + uint16_t displayWidth = 0; + uint16_t displayHeight = 0; + +private: + FrameCallback alertFrames[1]; + struct ScreenCmd { + Cmd cmd; + union { + uint32_t bluetooth_pin; + char *print_text; }; + }; - /// Enques given command item to be processed by main loop(). - bool enqueueCmd(const ScreenCmd &cmd) - { - if (!useDisplay) - return false; // not enqueued if our display is not in use - else { - bool success = cmdQueue.enqueue(cmd, 0); - enabled = true; // handle ASAP (we are the registered reader for cmdQueue, but might have been disabled) - return success; - } + /// Enques given command item to be processed by main loop(). + bool enqueueCmd(const ScreenCmd &cmd) { + if (!useDisplay) + return false; // not enqueued if our display is not in use + else { + bool success = cmdQueue.enqueue(cmd, 0); + enabled = true; // handle ASAP (we are the registered reader for cmdQueue, but might have been disabled) + return success; } + } - // Implementations of various commands, called from doTask(). - void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); - void handleOnPress(); - void handleStartFirmwareUpdateScreen(); + // Implementations of various commands, called from doTask(). + void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); + void handleOnPress(); + void handleStartFirmwareUpdateScreen(); - // Info collected by setFrames method. - // Index location of specific frames. - // - Used to apply the FrameFocus parameter of setFrames - // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo - struct FramesetInfo { - struct FramePositions { - uint8_t fault = 255; - uint8_t waypoint = 255; - uint8_t focusedModule = 255; - uint8_t log = 255; - uint8_t settings = 255; - uint8_t wifi = 255; - uint8_t deviceFocused = 255; - uint8_t system = 255; - uint8_t gps = 255; - uint8_t home = 255; - uint8_t textMessage = 255; - uint8_t nodelist_nodes = 255; - uint8_t nodelist_location = 255; - uint8_t nodelist_lastheard = 255; - uint8_t nodelist_hopsignal = 255; - uint8_t nodelist_distance = 255; - uint8_t nodelist_bearings = 255; - uint8_t clock = 255; - uint8_t chirpy = 255; - uint8_t firstFavorite = 255; - uint8_t lastFavorite = 255; - uint8_t lora = 255; - } positions; + // Info collected by setFrames method. + // Index location of specific frames. + // - Used to apply the FrameFocus parameter of setFrames + // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo + struct FramesetInfo { + struct FramePositions { + uint8_t fault = 255; + uint8_t waypoint = 255; + uint8_t focusedModule = 255; + uint8_t log = 255; + uint8_t settings = 255; + uint8_t wifi = 255; + uint8_t deviceFocused = 255; + uint8_t system = 255; + uint8_t gps = 255; + uint8_t home = 255; + uint8_t textMessage = 255; + uint8_t nodelist_nodes = 255; + uint8_t nodelist_location = 255; + uint8_t nodelist_lastheard = 255; + uint8_t nodelist_hopsignal = 255; + uint8_t nodelist_distance = 255; + uint8_t nodelist_bearings = 255; + uint8_t clock = 255; + uint8_t chirpy = 255; + uint8_t firstFavorite = 255; + uint8_t lastFavorite = 255; + uint8_t lora = 255; + } positions; - uint8_t frameCount = 0; - } framesetInfo; + uint8_t frameCount = 0; + } framesetInfo; - struct hiddenFrames { - bool textMessage = false; - bool waypoint = false; - bool wifi = false; - bool system = false; - bool home = false; - bool clock = false; + struct hiddenFrames { + bool textMessage = false; + bool waypoint = false; + bool wifi = false; + bool system = false; + bool home = false; + bool clock = false; #ifndef USE_EINK - bool nodelist_nodes = false; - bool nodelist_location = false; + bool nodelist_nodes = false; + bool nodelist_location = false; #endif #ifdef USE_EINK - bool nodelist_lastheard = false; - bool nodelist_hopsignal = false; - bool nodelist_distance = false; + bool nodelist_lastheard = false; + bool nodelist_hopsignal = false; + bool nodelist_distance = false; #endif #if HAS_GPS #ifdef USE_EINK - bool nodelist_bearings = false; + bool nodelist_bearings = false; #endif - bool gps = false; + bool gps = false; #endif - bool lora = false; - bool show_favorites = false; - bool chirpy = true; - } hiddenFrames; + bool lora = false; + bool show_favorites = false; + bool chirpy = true; + } hiddenFrames; - /// Try to start drawing ASAP - void setFastFramerate(); + /// Try to start drawing ASAP + void setFastFramerate(); - // Sets frame up for immediate drawing - void setFrameImmediateDraw(FrameCallback *drawFrames); + // Sets frame up for immediate drawing + void setFrameImmediateDraw(FrameCallback *drawFrames); - /// callback for current alert frame - FrameCallback alertFrame; + /// callback for current alert frame + FrameCallback alertFrame; - /// Queue of commands to execute in doTask. - TypedQueue cmdQueue; - /// Whether we are using a display - bool useDisplay = false; - /// Whether the display is currently powered - bool screenOn = false; - // Whether we are showing the regular screen (as opposed to booth screen or - // Bluetooth PIN screen) - bool showingNormalScreen = false; + /// Queue of commands to execute in doTask. + TypedQueue cmdQueue; + /// Whether we are using a display + bool useDisplay = false; + /// Whether the display is currently powered + bool screenOn = false; + // Whether we are showing the regular screen (as opposed to booth screen or + // Bluetooth PIN screen) + bool showingNormalScreen = false; - // Implementation to Adjust Brightness - uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 + // Implementation to Adjust Brightness + uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 - bool hasCompass = false; - float compassHeading; - uint32_t endCalibrationAt; + bool hasCompass = false; + float compassHeading; + uint32_t endCalibrationAt; - /// Holds state for debug information - DebugInfo debugInfo; + /// Holds state for debug information + DebugInfo debugInfo; - /// Display device - OLEDDisplay *dispdev; + /// Display device + OLEDDisplay *dispdev; - /// UI helper for rendering to frames and switching between them - OLEDDisplayUi *ui; + /// UI helper for rendering to frames and switching between them + OLEDDisplayUi *ui; }; } // namespace graphics diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index d54fc9958..ff085d7e9 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -72,9 +72,9 @@ #endif #endif -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ + defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ + defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 8f06fcf9f..3cf693184 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -13,46 +13,43 @@ #include #include -namespace graphics -{ +namespace graphics { -ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) -{ +ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) { #ifdef FORCE_LOW_RES - return ScreenResolution::Low; + return ScreenResolution::Low; #else - // Unit C6L and other ultra low res screens - if (screenwidth <= 64 || screenheight <= 48) { - return ScreenResolution::UltraLow; - } + // Unit C6L and other ultra low res screens + if (screenwidth <= 64 || screenheight <= 48) { + return ScreenResolution::UltraLow; + } - // Standard OLED screens - if (screenwidth > 128 && screenheight <= 64) { - return ScreenResolution::Low; - } - - // High Resolutions screens like T114, TDeck, TLora Pager, etc - if (screenwidth > 128) { - return ScreenResolution::High; - } - - // Default to low resolution + // Standard OLED screens + if (screenwidth > 128 && screenheight <= 64) { return ScreenResolution::Low; + } + + // High Resolutions screens like T114, TDeck, TLora Pager, etc + if (screenwidth > 128) { + return ScreenResolution::High; + } + + // Default to low resolution + return ScreenResolution::Low; #endif } -void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) -{ - hour = 0; - minute = 0; - second = 0; - if (rtc_sec == 0) - return; - uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - hour = hms / SEC_PER_HOUR; - minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - second = hms % SEC_PER_MIN; +void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) { + hour = 0; + minute = 0; + second = 0; + if (rtc_sec == 0) + return; + uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = hms % SEC_PER_MIN; } // === Shared External State === @@ -68,457 +65,448 @@ uint32_t lastMailBlink = 0; // ********************************* // * Rounded Header when inverted * // ********************************* -void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) -{ - // Draw the center and side rectangles - display->fillRect(x + r, y, w - 2 * r, h); // center bar - display->fillRect(x, y + r, r, h - 2 * r); // left edge - display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { + // Draw the center and side rectangles + display->fillRect(x + r, y, w - 2 * r, h); // center bar + display->fillRect(x, y + r, r, h - 2 * r); // left edge + display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge - // Draw the rounded corners using filled circles - display->fillCircle(x + r + 1, y + r, r); // top-left - display->fillCircle(x + w - r - 1, y + r, r); // top-right - display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left - display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right + // Draw the rounded corners using filled circles + display->fillCircle(x + r + 1, y + r, r); // top-left + display->fillCircle(x + w - r - 1, y + r, r); // top-right + display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right } // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) -{ - constexpr int HEADER_OFFSET_Y = 1; - y += HEADER_OFFSET_Y; +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) { + constexpr int HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); - const int xOffset = 4; - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - const bool isBold = config.display.heading_bold; + const int xOffset = 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); - if (!force_no_invert) { - // === Inverted Header Background === - if (isInverted) { - display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 2); - display->setColor(WHITE); - drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 2); - display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - display->drawLine(0, 20, screenW, 20); - } else { - display->drawLine(0, 14, screenW, 14); - } - } - - // === Screen Title === - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(SCREEN_WIDTH / 2, y, titleStr); - if (config.display.heading_bold) { - display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); - } - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Battery State === - int chargePercent = powerStatus->getBatteryChargePercent(); - bool isCharging = powerStatus->getIsCharging(); - bool usbPowered = powerStatus->getHasUSB(); - - if (chargePercent >= 100) { - isCharging = false; - } - if (chargePercent == 101) { - usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable - // plugged in + if (!force_no_invert) { + // === Inverted Header Background === + if (isInverted) { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } } - uint32_t now = millis(); + // === Screen Title === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(SCREEN_WIDTH / 2, y, titleStr); + if (config.display.heading_bold) { + display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + } + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Battery State === + int chargePercent = powerStatus->getBatteryChargePercent(); + bool isCharging = powerStatus->getIsCharging(); + bool usbPowered = powerStatus->getHasUSB(); + + if (chargePercent >= 100) { + isCharging = false; + } + if (chargePercent == 101) { + usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB + // cable plugged in + } + + uint32_t now = millis(); #ifndef USE_EINK - if (isCharging && now - lastBlinkShared > 500) { - isBoltVisibleShared = !isBoltVisibleShared; - lastBlinkShared = now; - } + if (isCharging && now - lastBlinkShared > 500) { + isBoltVisibleShared = !isBoltVisibleShared; + lastBlinkShared = now; + } #endif - bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - int batteryX = 1; - int batteryY = HEADER_OFFSET_Y + 1; + int batteryX = 1; + int batteryY = HEADER_OFFSET_Y + 1; #if !defined(M5STACK_UNITC6L) - // === Battery Icons === - if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging - batteryX += 1; - batteryY += 2; - if (currentResolution == ScreenResolution::High) { - display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); - batteryX += 20; // Icon + 1 pixel - } else { - display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); - batteryX += 11; // Icon + 1 pixel - } + // === Battery Icons === + if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging + batteryX += 1; + batteryY += 2; + if (currentResolution == ScreenResolution::High) { + display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); + batteryX += 20; // Icon + 1 pixel + } else { + display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); + batteryX += 11; // Icon + 1 pixel + } + } else { + if (useHorizontalBattery) { + batteryX += 1; + batteryY += 2; + display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); + display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); + else { + display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); + display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); + int fillWidth = 14 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); + } + batteryX += 18; // Icon + 2 pixels } else { - if (useHorizontalBattery) { - batteryX += 1; - batteryY += 2; - display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); - display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); - if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); - else { - display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); - display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); - int fillWidth = 14 * chargePercent / 100; - display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); - } - batteryX += 18; // Icon + 2 pixels - } else { #ifdef USE_EINK - batteryY += 2; + batteryY += 2; #endif - display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); - if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); - else { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); - int fillHeight = 8 * chargePercent / 100; - int fillY = batteryY - fillHeight; - display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); - } - batteryX += 9; // Icon + 2 pixels - } + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); + else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + } + batteryX += 9; // Icon + 2 pixels } + } - if (chargePercent != 101) { - // === Battery % Display === - char chargeStr[4]; - snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); - int chargeNumWidth = display->getStringWidth(chargeStr); - display->drawString(batteryX, textY, chargeStr); - display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); - if (isBold) { - display->drawString(batteryX + 1, textY, chargeStr); - display->drawString(batteryX + chargeNumWidth, textY, "%"); - } + if (chargePercent != 101) { + // === Battery % Display === + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); + display->drawString(batteryX, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); + if (isBold) { + display->drawString(batteryX + 1, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth, textY, "%"); } + } - // === Time and Right-aligned Icons === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char timeStr[10] = "--:--"; // Fallback display - int timeStrWidth = display->getStringWidth("12:34"); // Default alignment - int timeX = screenW - xOffset - timeStrWidth + 4; + // === Time and Right-aligned Icons === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char timeStr[10] = "--:--"; // Fallback display + int timeStrWidth = display->getStringWidth("12:34"); // Default alignment + int timeX = screenW - xOffset - timeStrWidth + 4; - if (rtc_sec > 0) { - // === Build Time String === - long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - int hour, minute, second; - graphics::decomposeTime(rtc_sec, hour, minute, second); - snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + if (rtc_sec > 0) { + // === Build Time String === + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + int hour, minute, second; + graphics::decomposeTime(rtc_sec, hour, minute, second); + snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); - // === Build Date String === - char datetimeStr[25]; - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); - char dateLine[40]; - - if (currentResolution == ScreenResolution::High) { - snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); - } else { - if (hasUnreadMessage) { - snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); - } else { - snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); - } - } - - if (config.display.use_12h_clock) { - bool isPM = hour >= 12; - hour %= 12; - if (hour == 0) - hour = 12; - snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); - } - - if (show_date) { - timeStrWidth = display->getStringWidth(dateLine); - } else { - timeStrWidth = display->getStringWidth(timeStr); - } - timeX = screenW - xOffset - timeStrWidth + 3; - - // === Show Mail or Mute Icon to the Left of Time === - int iconRightEdge = timeX - 2; - - bool showMail = false; - -#ifndef USE_EINK - if (hasUnreadMessage) { - if (now - lastMailBlink > 500) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; - } - showMail = isMailIconVisible; - } -#else - if (hasUnreadMessage) { - showMail = true; - } -#endif - - if (showMail) { - if (useHorizontalBattery) { - int iconW = 16, iconH = 12; - int iconX = iconRightEdge - iconW; - int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); - display->setColor(WHITE); - } - display->drawRect(iconX, iconY, iconW + 1, iconH); - display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); - display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); - } else { - int iconX = iconRightEdge - (mail_width - 2); - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); - } - } else if (externalNotificationModule->getMute()) { - if (currentResolution == ScreenResolution::High) { - int iconX = iconRightEdge - mute_symbol_big_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; - - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); - } else { - int iconX = iconRightEdge - mute_symbol_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); - } - } - - if (show_date) { - // === Draw Date === - display->drawString(timeX, textY, dateLine); - if (isBold) - display->drawString(timeX - 1, textY, dateLine); - } else { - // === Draw Time === - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); - } - - } else { - // === No Time Available: Mail/Mute Icon Moves to Far Right === - int iconRightEdge = screenW - xOffset; - - bool showMail = false; - -#ifndef USE_EINK - if (hasUnreadMessage) { - if (now - lastMailBlink > 500) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; - } - showMail = isMailIconVisible; - } -#else - if (hasUnreadMessage) { - showMail = true; - } -#endif - - if (showMail) { - if (useHorizontalBattery) { - int iconW = 16, iconH = 12; - int iconX = iconRightEdge - iconW; - int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - display->drawRect(iconX, iconY, iconW + 1, iconH); - display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); - display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); - } else { - int iconX = iconRightEdge - mail_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); - } - } else if (externalNotificationModule->getMute()) { - if (currentResolution == ScreenResolution::High) { - int iconX = iconRightEdge - mute_symbol_big_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); - } else { - int iconX = iconRightEdge - mute_symbol_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); - } - } - } -#endif - display->setColor(WHITE); // Reset for other UI -} - -const int *getTextPositions(OLEDDisplay *display) -{ - static int textPositions[7]; // Static array that persists beyond function scope + // === Build Date String === + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char dateLine[40]; if (currentResolution == ScreenResolution::High) { - textPositions[0] = textZeroLine; - textPositions[1] = textFirstLine_medium; - textPositions[2] = textSecondLine_medium; - textPositions[3] = textThirdLine_medium; - textPositions[4] = textFourthLine_medium; - textPositions[5] = textFifthLine_medium; - textPositions[6] = textSixthLine_medium; + snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); } else { - textPositions[0] = textZeroLine; - textPositions[1] = textFirstLine; - textPositions[2] = textSecondLine; - textPositions[3] = textThirdLine; - textPositions[4] = textFourthLine; - textPositions[5] = textFifthLine; - textPositions[6] = textSixthLine; + if (hasUnreadMessage) { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); + } else { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); + } } - return textPositions; + + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + hour %= 12; + if (hour == 0) + hour = 12; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + } + + if (show_date) { + timeStrWidth = display->getStringWidth(dateLine); + } else { + timeStrWidth = display->getStringWidth(timeStr); + } + timeX = screenW - xOffset - timeStrWidth + 3; + + // === Show Mail or Mute Icon to the Left of Time === + int iconRightEdge = timeX - 2; + + bool showMail = false; + +#ifndef USE_EINK + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(WHITE); + } + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - (mail_width - 2); + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + + if (show_date) { + // === Draw Date === + display->drawString(timeX, textY, dateLine); + if (isBold) + display->drawString(timeX - 1, textY, dateLine); + } else { + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + } + + } else { + // === No Time Available: Mail/Mute Icon Moves to Far Right === + int iconRightEdge = screenW - xOffset; + + bool showMail = false; + +#ifndef USE_EINK + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + } +#endif + display->setColor(WHITE); // Reset for other UI +} + +const int *getTextPositions(OLEDDisplay *display) { + static int textPositions[7]; // Static array that persists beyond function scope + + if (currentResolution == ScreenResolution::High) { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine_medium; + textPositions[2] = textSecondLine_medium; + textPositions[3] = textThirdLine_medium; + textPositions[4] = textFourthLine_medium; + textPositions[5] = textFifthLine_medium; + textPositions[6] = textSixthLine_medium; + } else { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine; + textPositions[2] = textSecondLine; + textPositions[3] = textThirdLine; + textPositions[4] = textFourthLine; + textPositions[5] = textFifthLine; + textPositions[6] = textSixthLine; + } + return textPositions; } // ************************* // * Common Footer Drawing * // ************************* -void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) -{ - bool drawConnectionState = false; - if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || - service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET || - service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) { - drawConnectionState = true; - } +void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) { + bool drawConnectionState = false; + if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || service->api_state == service->STATE_SERIAL || + service->api_state == service->STATE_PACKET || service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) { + drawConnectionState = true; + } - if (drawConnectionState) { - const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; - display->setColor(BLACK); - display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), - (connection_icon_height * scale) + (2 * scale)); - display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - const int bytesPerRow = (connection_icon_width + 7) / 8; - int iconX = 0; - int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); + if (drawConnectionState) { + const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; + display->setColor(BLACK); + display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), + (connection_icon_height * scale) + (2 * scale)); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { + const int bytesPerRow = (connection_icon_width + 7) / 8; + int iconX = 0; + int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); - for (int yy = 0; yy < connection_icon_height; ++yy) { - const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; - for (int xx = 0; xx < connection_icon_width; ++xx) { - const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); - const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first - if (byteVal & bitMask) { - display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); - } - } - } - - } else { - display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, - connection_icon); + for (int yy = 0; yy < connection_icon_height; ++yy) { + const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; + for (int xx = 0; xx < connection_icon_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); + } } + } + + } else { + display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, connection_icon); } + } } -bool isAllowedPunctuation(char c) -{ - const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; - return allowed.find(c) != std::string::npos; +bool isAllowedPunctuation(char c) { + const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; + return allowed.find(c) != std::string::npos; } -static void replaceAll(std::string &s, const std::string &from, const std::string &to) -{ - if (from.empty()) - return; - size_t pos = 0; - while ((pos = s.find(from, pos)) != std::string::npos) { - s.replace(pos, from.size(), to); - pos += to.size(); - } +static void replaceAll(std::string &s, const std::string &from, const std::string &to) { + if (from.empty()) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.size(), to); + pos += to.size(); + } } -std::string sanitizeString(const std::string &input) -{ - std::string output; - bool inReplacement = false; +std::string sanitizeString(const std::string &input) { + std::string output; + bool inReplacement = false; - // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. - std::string s = input; + // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. + std::string s = input; - // Curly single quotes: ‘ ’ - replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 - replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 + // Curly single quotes: ‘ ’ + replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 + replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 - // Curly double quotes: “ ” - replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C - replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D + // Curly double quotes: “ ” + replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C + replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D - // En dash / Em dash: – — - replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 - replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 + // En dash / Em dash: – — + replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 + replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 - // Non-breaking space - replaceAll(s, "\xC2\xA0", " "); // U+00A0 + // Non-breaking space + replaceAll(s, "\xC2\xA0", " "); // U+00A0 - // Now do your original sanitize pass over the normalized string. - for (unsigned char uc : s) { - char c = static_cast(uc); - if (std::isalnum(uc) || isAllowedPunctuation(c)) { - output += c; - inReplacement = false; - } else { - if (!inReplacement) { - output += static_cast(0xBF); // ISO-8859-1 for inverted question mark - inReplacement = true; - } - } + // Now do your original sanitize pass over the normalized string. + for (unsigned char uc : s) { + char c = static_cast(uc); + if (std::isalnum(uc) || isAllowedPunctuation(c)) { + output += c; + inReplacement = false; + } else { + if (!inReplacement) { + output += static_cast(0xBF); // ISO-8859-1 for inverted question mark + inReplacement = true; + } } + } - return output; + return output; } } // namespace graphics diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index a8ecdfada..6af4ff9d0 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -3,8 +3,7 @@ #include #include -namespace graphics -{ +namespace graphics { // ======================= // Shared UI Helpers @@ -51,8 +50,7 @@ void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, - bool show_date = false); +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, bool show_date = false); // Shared battery/time/mail header void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 12fac4f34..6ca766514 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -29,85 +29,82 @@ uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #define TFT_INVERT true #endif -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Panel_ST7735S _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Panel_ST7735S _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; - public: - LGFX(void) +public: + LGFX(void) { { - { - auto cfg = _bus_instance.config(); + auto cfg = _bus_instance.config(); - // configure SPI - cfg.spi_host = ST7735_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing - // 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ST7735_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ST7735_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ST7735_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ST7735_RS; // Set SPI DC pin number (-1 = disable) + // configure SPI + cfg.spi_host = ST7735_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by + // dividing 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7735_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7735_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7735_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7735_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ST7735_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = ST7735_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = ST7735_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ST7735_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7735_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7735_BUSY; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = - false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef TFT_BL - // Set the backlight control - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected - cfg.invert = true; // true to invert the brightness of the backlight - // cfg.freq = 44100; // PWM frequency of backlight - // cfg.pwm_channel = 1; // PWM channel number to use + cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected + cfg.invert = true; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif - setPanel(&_panel_instance); - } + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -119,10 +116,7 @@ TFT_eSPI *tft = nullptr; FT6336U ft6336u; static uint8_t _rak14014_touch_int = false; // TP interrupt generation flag. -static void rak14014_tpIntHandle(void) -{ - _rak14014_touch_int = true; -} +static void rak14014_tpIntHandle(void) { _rak14014_touch_int = true; } #elif defined(HACKADAY_COMMUNICATOR) #include @@ -136,164 +130,161 @@ Arduino_GFX *tft = nullptr; #include TCA9534 ioex; -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Bus_RGB _bus_instance; - lgfx::Panel_RGB _panel_instance; - lgfx::Touch_GT911 _touch_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Bus_RGB _bus_instance; + lgfx::Panel_RGB _panel_instance; + lgfx::Touch_GT911 _touch_instance; - public: - const uint16_t screenWidth = TFT_WIDTH; - const uint16_t screenHeight = TFT_HEIGHT; +public: + const uint16_t screenWidth = TFT_WIDTH; + const uint16_t screenHeight = TFT_HEIGHT; - bool init_impl(bool use_reset, bool use_clear) override + bool init_impl(bool use_reset, bool use_clear) override { + ioex.attach(Wire); + ioex.setDeviceAddress(0x18); + ioex.config(1, TCA9534::Config::OUT); + ioex.config(2, TCA9534::Config::OUT); + ioex.config(3, TCA9534::Config::OUT); + ioex.config(4, TCA9534::Config::OUT); + + ioex.output(1, TCA9534::Level::H); + ioex.output(3, TCA9534::Level::L); + ioex.output(4, TCA9534::Level::H); + + pinMode(1, OUTPUT); + digitalWrite(1, LOW); + ioex.output(2, TCA9534::Level::L); + delay(20); + ioex.output(2, TCA9534::Level::H); + delay(100); + pinMode(1, INPUT); + + return LGFX_Device::init_impl(use_reset, use_clear); + } + + LGFX(void) { { - ioex.attach(Wire); - ioex.setDeviceAddress(0x18); - ioex.config(1, TCA9534::Config::OUT); - ioex.config(2, TCA9534::Config::OUT); - ioex.config(3, TCA9534::Config::OUT); - ioex.config(4, TCA9534::Config::OUT); + auto cfg = _panel_instance.config(); - ioex.output(1, TCA9534::Level::H); - ioex.output(3, TCA9534::Level::L); - ioex.output(4, TCA9534::Level::H); - - pinMode(1, OUTPUT); - digitalWrite(1, LOW); - ioex.output(2, TCA9534::Level::L); - delay(20); - ioex.output(2, TCA9534::Level::H); - delay(100); - pinMode(1, INPUT); - - return LGFX_Device::init_impl(use_reset, use_clear); + cfg.memory_width = screenWidth; + cfg.memory_height = screenHeight; + cfg.panel_width = screenWidth; + cfg.panel_height = screenHeight; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + _panel_instance.config(cfg); } - LGFX(void) { - { - auto cfg = _panel_instance.config(); + auto cfg = _panel_instance.config_detail(); + cfg.use_psram = 0; + _panel_instance.config_detail(cfg); + } - cfg.memory_width = screenWidth; - cfg.memory_height = screenHeight; - cfg.panel_width = screenWidth; - cfg.panel_height = screenHeight; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 0; - _panel_instance.config(cfg); - } + { + auto cfg = _bus_instance.config(); + cfg.panel = &_panel_instance; + cfg.pin_d0 = ST72xx_B0; // B0 + cfg.pin_d1 = ST72xx_B1; // B1 + cfg.pin_d2 = ST72xx_B2; // B2 + cfg.pin_d3 = ST72xx_B3; // B3 + cfg.pin_d4 = ST72xx_B4; // B4 + cfg.pin_d5 = ST72xx_G0; // G0 + cfg.pin_d6 = ST72xx_G1; // G1 + cfg.pin_d7 = ST72xx_G2; // G2 + cfg.pin_d8 = ST72xx_G3; // G3 + cfg.pin_d9 = ST72xx_G4; // G4 + cfg.pin_d10 = ST72xx_G5; // G5 + cfg.pin_d11 = ST72xx_R0; // R0 + cfg.pin_d12 = ST72xx_R1; // R1 + cfg.pin_d13 = ST72xx_R2; // R2 + cfg.pin_d14 = ST72xx_R3; // R3 + cfg.pin_d15 = ST72xx_R4; // R4 - { - auto cfg = _panel_instance.config_detail(); - cfg.use_psram = 0; - _panel_instance.config_detail(cfg); - } - - { - auto cfg = _bus_instance.config(); - cfg.panel = &_panel_instance; - cfg.pin_d0 = ST72xx_B0; // B0 - cfg.pin_d1 = ST72xx_B1; // B1 - cfg.pin_d2 = ST72xx_B2; // B2 - cfg.pin_d3 = ST72xx_B3; // B3 - cfg.pin_d4 = ST72xx_B4; // B4 - cfg.pin_d5 = ST72xx_G0; // G0 - cfg.pin_d6 = ST72xx_G1; // G1 - cfg.pin_d7 = ST72xx_G2; // G2 - cfg.pin_d8 = ST72xx_G3; // G3 - cfg.pin_d9 = ST72xx_G4; // G4 - cfg.pin_d10 = ST72xx_G5; // G5 - cfg.pin_d11 = ST72xx_R0; // R0 - cfg.pin_d12 = ST72xx_R1; // R1 - cfg.pin_d13 = ST72xx_R2; // R2 - cfg.pin_d14 = ST72xx_R3; // R3 - cfg.pin_d15 = ST72xx_R4; // R4 - - cfg.pin_henable = ST72xx_DE; - cfg.pin_vsync = ST72xx_VSYNC; - cfg.pin_hsync = ST72xx_HSYNC; - cfg.pin_pclk = ST72xx_PCLK; - cfg.freq_write = 13000000; + cfg.pin_henable = ST72xx_DE; + cfg.pin_vsync = ST72xx_VSYNC; + cfg.pin_hsync = ST72xx_HSYNC; + cfg.pin_pclk = ST72xx_PCLK; + cfg.freq_write = 13000000; #ifdef ST7265_HSYNC_POLARITY - cfg.hsync_polarity = ST7265_HSYNC_POLARITY; - cfg.hsync_front_porch = ST7265_HSYNC_FRONT_PORCH; // 8; - cfg.hsync_pulse_width = ST7265_HSYNC_PULSE_WIDTH; // 4; - cfg.hsync_back_porch = ST7265_HSYNC_BACK_PORCH; // 8; + cfg.hsync_polarity = ST7265_HSYNC_POLARITY; + cfg.hsync_front_porch = ST7265_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = ST7265_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = ST7265_HSYNC_BACK_PORCH; // 8; - cfg.vsync_polarity = ST7265_VSYNC_POLARITY; // 0; - cfg.vsync_front_porch = ST7265_VSYNC_FRONT_PORCH; // 8; - cfg.vsync_pulse_width = ST7265_VSYNC_PULSE_WIDTH; // 4; - cfg.vsync_back_porch = ST7265_VSYNC_BACK_PORCH; // 8; + cfg.vsync_polarity = ST7265_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = ST7265_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = ST7265_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = ST7265_VSYNC_BACK_PORCH; // 8; - cfg.pclk_idle_high = 1; - cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif #ifdef ST7262_HSYNC_POLARITY - cfg.hsync_polarity = ST7262_HSYNC_POLARITY; - cfg.hsync_front_porch = ST7262_HSYNC_FRONT_PORCH; // 8; - cfg.hsync_pulse_width = ST7262_HSYNC_PULSE_WIDTH; // 4; - cfg.hsync_back_porch = ST7262_HSYNC_BACK_PORCH; // 8; + cfg.hsync_polarity = ST7262_HSYNC_POLARITY; + cfg.hsync_front_porch = ST7262_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = ST7262_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = ST7262_HSYNC_BACK_PORCH; // 8; - cfg.vsync_polarity = ST7262_VSYNC_POLARITY; // 0; - cfg.vsync_front_porch = ST7262_VSYNC_FRONT_PORCH; // 8; - cfg.vsync_pulse_width = ST7262_VSYNC_PULSE_WIDTH; // 4; - cfg.vsync_back_porch = ST7262_VSYNC_BACK_PORCH; // 8; + cfg.vsync_polarity = ST7262_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = ST7262_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = ST7262_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = ST7262_VSYNC_BACK_PORCH; // 8; - cfg.pclk_idle_high = 1; - cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif #ifdef SC7277_HSYNC_POLARITY - cfg.hsync_polarity = SC7277_HSYNC_POLARITY; - cfg.hsync_front_porch = SC7277_HSYNC_FRONT_PORCH; // 8; - cfg.hsync_pulse_width = SC7277_HSYNC_PULSE_WIDTH; // 4; - cfg.hsync_back_porch = SC7277_HSYNC_BACK_PORCH; // 8; + cfg.hsync_polarity = SC7277_HSYNC_POLARITY; + cfg.hsync_front_porch = SC7277_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = SC7277_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = SC7277_HSYNC_BACK_PORCH; // 8; - cfg.vsync_polarity = SC7277_VSYNC_POLARITY; // 0; - cfg.vsync_front_porch = SC7277_VSYNC_FRONT_PORCH; // 8; - cfg.vsync_pulse_width = SC7277_VSYNC_PULSE_WIDTH; // 4; - cfg.vsync_back_porch = SC7277_VSYNC_BACK_PORCH; // 8; + cfg.vsync_polarity = SC7277_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = SC7277_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = SC7277_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = SC7277_VSYNC_BACK_PORCH; // 8; - cfg.pclk_idle_high = 1; - cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif - _bus_instance.config(cfg); - } - _panel_instance.setBus(&_bus_instance); - - { - auto cfg = _touch_instance.config(); - cfg.x_min = 0; - cfg.x_max = TFT_WIDTH; - cfg.y_min = 0; - cfg.y_max = TFT_HEIGHT; - cfg.pin_int = -1; - cfg.pin_rst = -1; - cfg.bus_shared = true; - cfg.offset_rotation = 0; - - cfg.i2c_port = 0; - cfg.i2c_addr = 0x5D; - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; - cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } - - setPanel(&_panel_instance); + _bus_instance.config(cfg); } + _panel_instance.setBus(&_bus_instance); + + { + auto cfg = _touch_instance.config(); + cfg.x_min = 0; + cfg.x_max = TFT_WIDTH; + cfg.y_min = 0; + cfg.y_max = TFT_HEIGHT; + cfg.pin_int = -1; + cfg.pin_rst = -1; + cfg.bus_shared = true; + cfg.offset_rotation = 0; + + cfg.i2c_port = 0; + cfg.i2c_addr = 0x5D; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; + cfg.freq = 400000; + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -301,127 +292,124 @@ static LGFX *tft = nullptr; #elif defined(ILI9488_CS) #include // Graphics and font library for ILI9488 driver chip -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Panel_ILI9488 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; - lgfx::Touch_GT911 _touch_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Panel_ILI9488 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_GT911 _touch_instance; - public: - LGFX(void) +public: + LGFX(void) { { - { - auto cfg = _bus_instance.config(); + auto cfg = _bus_instance.config(); - // configure SPI - cfg.spi_host = ILI9488_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing - // 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ILI9488_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ILI9488_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ILI9488_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ILI9488_RS; // Set SPI DC pin number (-1 = disable) + // configure SPI + cfg.spi_host = ILI9488_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by + // dividing 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ILI9488_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ILI9488_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ILI9488_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ILI9488_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ILI9488_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ILI9488_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS - cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = - false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef ILI9488_BL - // Set the backlight control - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = ILI9488_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - // cfg.freq = 44100; // PWM frequency of backlight - // cfg.pwm_channel = 1; // PWM channel number to use + cfg.pin_bl = ILI9488_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif #if HAS_TOUCHSCREEN - // Configure settings for touch screen control. - { - auto cfg = _touch_instance.config(); + // Configure settings for touch screen control. + { + auto cfg = _touch_instance.config(); - cfg.pin_cs = -1; - cfg.x_min = 0; - cfg.x_max = TFT_HEIGHT - 1; - cfg.y_min = 0; - cfg.y_max = TFT_WIDTH - 1; - cfg.pin_int = SCREEN_TOUCH_INT; + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = TFT_HEIGHT - 1; + cfg.y_min = 0; + cfg.y_max = TFT_WIDTH - 1; + cfg.pin_int = SCREEN_TOUCH_INT; #ifdef SCREEN_TOUCH_RST - cfg.pin_rst = SCREEN_TOUCH_RST; + cfg.pin_rst = SCREEN_TOUCH_RST; #endif - cfg.bus_shared = true; - cfg.offset_rotation = TFT_OFFSET_ROTATION; - // cfg.freq = 2500000; + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + // cfg.freq = 2500000; - // I2C - cfg.i2c_port = TOUCH_I2C_PORT; - cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + // I2C + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; #ifdef SCREEN_TOUCH_USE_I2C1 - cfg.pin_sda = I2C_SDA1; - cfg.pin_scl = I2C_SCL1; + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; #else - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; #endif - // cfg.freq = 400000; + // cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } -#endif - - setPanel(&_panel_instance); + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); } +#endif + + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -431,191 +419,182 @@ static LGFX *tft = nullptr; #ifdef HELTEC_V4_TFT #include "chsc6x.h" #include "lgfx/v1/Touch.hpp" -namespace lgfx -{ -inline namespace v1 -{ -class TOUCH_CHSC6X : public ITouch -{ - public: - TOUCH_CHSC6X(void) - { - _cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; - _cfg.x_min = 0; - _cfg.x_max = 240; - _cfg.y_min = 0; - _cfg.y_max = 320; - }; +namespace lgfx { +inline namespace v1 { +class TOUCH_CHSC6X : public ITouch { +public: + TOUCH_CHSC6X(void) { + _cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + _cfg.x_min = 0; + _cfg.x_max = 240; + _cfg.y_min = 0; + _cfg.y_max = 320; + }; - bool init(void) override - { - if (chsc6xTouch == nullptr) { - chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN); - } - chsc6xTouch->chsc6x_init(); - return true; - }; + bool init(void) override { + if (chsc6xTouch == nullptr) { + chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN); + } + chsc6xTouch->chsc6x_init(); + return true; + }; - uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override - { - uint16_t raw_x, raw_y; - if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) { - tp[0].x = 320 - 1 - raw_y; - tp[0].y = 240 - 1 - raw_x; - tp[0].size = 1; - tp[0].id = 1; - return 1; - } - tp[0].size = 0; - return 0; - }; + uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { + uint16_t raw_x, raw_y; + if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) { + tp[0].x = 320 - 1 - raw_y; + tp[0].y = 240 - 1 - raw_x; + tp[0].size = 1; + tp[0].id = 1; + return 1; + } + tp[0].size = 0; + return 0; + }; - void wakeup(void) override{}; - void sleep(void) override{}; + void wakeup(void) override{}; + void sleep(void) override{}; - private: - chsc6x *chsc6xTouch = nullptr; +private: + chsc6x *chsc6xTouch = nullptr; }; } // namespace v1 } // namespace lgfx #endif -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Panel_ST7789 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Panel_ST7789 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; #if HAS_TOUCHSCREEN #if defined(T_WATCH_S3) || defined(ELECROW) - lgfx::Touch_FT5x06 _touch_instance; + lgfx::Touch_FT5x06 _touch_instance; #elif defined(HELTEC_V4_TFT) - lgfx::TOUCH_CHSC6X _touch_instance; + lgfx::TOUCH_CHSC6X _touch_instance; #else - lgfx::Touch_GT911 _touch_instance; + lgfx::Touch_GT911 _touch_instance; #endif #endif - public: - LGFX(void) +public: + LGFX(void) { { - { - auto cfg = _bus_instance.config(); + auto cfg = _bus_instance.config(); - // SPI - cfg.spi_host = ST7789_SPI_HOST; - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing - // 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ST7789_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ST7789_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ST7789_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ST7789_RS; // Set SPI DC pin number (-1 = disable) + // SPI + cfg.spi_host = ST7789_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by + // dividing 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7789_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7789_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7789_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7789_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. #if defined(T_WATCH_S3) - cfg.panel_width = 240; - cfg.panel_height = 240; - cfg.memory_width = 240; - cfg.memory_height = 320; - cfg.offset_x = 0; - cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned - cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout + cfg.panel_width = 240; + cfg.panel_height = 240; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned + cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout #else - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #endif #ifdef TFT_DUMMY_READ_PIXELS - cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = - false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef ST7789_BL - // Set the backlight control. (delete if not necessary) - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = ST7789_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - // cfg.pwm_channel = 0; + cfg.pin_bl = ST7789_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.pwm_channel = 0; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif #if HAS_TOUCHSCREEN - // Configure settings for touch screen control. - { - auto cfg = _touch_instance.config(); + // Configure settings for touch screen control. + { + auto cfg = _touch_instance.config(); - cfg.pin_cs = -1; - cfg.x_min = 0; - cfg.x_max = TFT_HEIGHT - 1; - cfg.y_min = 0; - cfg.y_max = TFT_WIDTH - 1; - cfg.pin_int = SCREEN_TOUCH_INT; + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = TFT_HEIGHT - 1; + cfg.y_min = 0; + cfg.y_max = TFT_WIDTH - 1; + cfg.pin_int = SCREEN_TOUCH_INT; #ifdef SCREEN_TOUCH_RST - cfg.pin_rst = SCREEN_TOUCH_RST; + cfg.pin_rst = SCREEN_TOUCH_RST; #endif - cfg.bus_shared = true; - cfg.offset_rotation = TFT_OFFSET_ROTATION; - // cfg.freq = 2500000; + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + // cfg.freq = 2500000; - // I2C - cfg.i2c_port = TOUCH_I2C_PORT; - cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + // I2C + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; #ifdef SCREEN_TOUCH_USE_I2C1 - cfg.pin_sda = I2C_SDA1; - cfg.pin_scl = I2C_SCL1; + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; #else - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; #endif - // cfg.freq = 400000; + // cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } -#endif - - setPanel(&_panel_instance); // Sets the panel to use. + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. + } }; static LGFX *tft = nullptr; @@ -623,84 +602,81 @@ static LGFX *tft = nullptr; #elif defined(ST7796_CS) #include // Graphics and font library for ST7796 driver chip -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Panel_ST7796 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Panel_ST7796 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; - public: - LGFX(void) +public: + LGFX(void) { { - { - auto cfg = _bus_instance.config(); + auto cfg = _bus_instance.config(); - // SPI - cfg.spi_host = ST7796_SPI_HOST; - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing - // 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) + // SPI + cfg.spi_host = ST7796_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by + // dividing 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) - // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS - cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = - false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - _panel_instance.config(cfg); - } + _panel_instance.config(cfg); + } #ifdef ST7796_BL - // Set the backlight control. (delete if not necessary) - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - cfg.freq = 44100; - cfg.pwm_channel = 7; + cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + cfg.freq = 44100; + cfg.pwm_channel = 7; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif - setPanel(&_panel_instance); // Sets the panel to use. - } + setPanel(&_panel_instance); // Sets the panel to use. + } }; static LGFX *tft = nullptr; @@ -713,93 +689,90 @@ static LGFX *tft = nullptr; #define TFT_BL ILI9341_BACKLIGHT_EN #endif -class LGFX : public lgfx::LGFX_Device -{ +class LGFX : public lgfx::LGFX_Device { #if defined(ILI9341_DRIVER) - lgfx::Panel_ILI9341 _panel_instance; + lgfx::Panel_ILI9341 _panel_instance; #elif defined(ILI9342_DRIVER) - lgfx::Panel_ILI9342 _panel_instance; + lgfx::Panel_ILI9342 _panel_instance; #endif - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; - public: - LGFX(void) +public: + LGFX(void) { { - { - auto cfg = _bus_instance.config(); + auto cfg = _bus_instance.config(); - // configure SPI + // configure SPI #if defined(ILI9341_DRIVER) - cfg.spi_host = ILI9341_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_host = ILI9341_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST #elif defined(ILI9342_DRIVER) - cfg.spi_host = ILI9342_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_host = ILI9342_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST #endif - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing - // 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = TFT_SCLK; // Set SPI SCLK pin number - cfg.pin_mosi = TFT_MOSI; // Set SPI MOSI pin number - cfg.pin_miso = TFT_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = TFT_DC; // Set SPI DC pin number (-1 = disable) + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by + // dividing 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = TFT_SCLK; // Set SPI SCLK pin number + cfg.pin_mosi = TFT_MOSI; // Set SPI MOSI pin number + cfg.pin_miso = TFT_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = TFT_DC; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = TFT_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = TFT_RST; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = TFT_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = TFT_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = TFT_RST; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = TFT_BUSY; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = false; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = - false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = false; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef TFT_BL - // Set the backlight control - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - // cfg.freq = 44100; // PWM frequency of backlight - // cfg.pwm_channel = 1; // PWM channel number to use + cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif - setPanel(&_panel_instance); - } + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -812,108 +785,106 @@ static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h #include "Panel_sdl.hpp" #include // Graphics and font library for ST7735 driver chip -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Bus_SPI _bus_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Bus_SPI _bus_instance; - lgfx::ITouch *_touch_instance; + lgfx::ITouch *_touch_instance; - public: - lgfx::Panel_Device *_panel_instance; +public: + lgfx::Panel_Device *_panel_instance; - LGFX(void) - { - if (portduino_config.displayPanel == st7789) - _panel_instance = new lgfx::Panel_ST7789; - else if (portduino_config.displayPanel == st7735) - _panel_instance = new lgfx::Panel_ST7735; - else if (portduino_config.displayPanel == st7735s) - _panel_instance = new lgfx::Panel_ST7735S; - else if (portduino_config.displayPanel == st7796) - _panel_instance = new lgfx::Panel_ST7796; - else if (portduino_config.displayPanel == ili9341) - _panel_instance = new lgfx::Panel_ILI9341; - else if (portduino_config.displayPanel == ili9342) - _panel_instance = new lgfx::Panel_ILI9342; - else if (portduino_config.displayPanel == ili9488) - _panel_instance = new lgfx::Panel_ILI9488; - else if (portduino_config.displayPanel == hx8357d) - _panel_instance = new lgfx::Panel_HX8357D; + LGFX(void) { + if (portduino_config.displayPanel == st7789) + _panel_instance = new lgfx::Panel_ST7789; + else if (portduino_config.displayPanel == st7735) + _panel_instance = new lgfx::Panel_ST7735; + else if (portduino_config.displayPanel == st7735s) + _panel_instance = new lgfx::Panel_ST7735S; + else if (portduino_config.displayPanel == st7796) + _panel_instance = new lgfx::Panel_ST7796; + else if (portduino_config.displayPanel == ili9341) + _panel_instance = new lgfx::Panel_ILI9341; + else if (portduino_config.displayPanel == ili9342) + _panel_instance = new lgfx::Panel_ILI9342; + else if (portduino_config.displayPanel == ili9488) + _panel_instance = new lgfx::Panel_ILI9488; + else if (portduino_config.displayPanel == hx8357d) + _panel_instance = new lgfx::Panel_HX8357D; #if defined(SDL_h_) - else if (portduino_config.displayPanel == x11) - _panel_instance = new lgfx::Panel_sdl; + else if (portduino_config.displayPanel == x11) + _panel_instance = new lgfx::Panel_sdl; #endif - else { - _panel_instance = new lgfx::Panel_NULL; - LOG_ERROR("Unknown display panel configured!"); - } - - auto buscfg = _bus_instance.config(); - buscfg.spi_mode = 0; - buscfg.spi_host = portduino_config.display_spi_dev_int; - - buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) - - _bus_instance.config(buscfg); // applies the set value to the bus. - if (portduino_config.displayPanel != x11) - _panel_instance->setBus(&_bus_instance); // set the bus on the panel. - - auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); - cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = portduino_config.displayReset.pin; - if (portduino_config.displayRotate) { - cfg.panel_width = portduino_config.displayHeight; // actual displayable width - cfg.panel_height = portduino_config.displayWidth; // actual displayable height - } else { - cfg.panel_width = portduino_config.displayWidth; // actual displayable width - cfg.panel_height = portduino_config.displayHeight; // actual displayable height - } - cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction - cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction - cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) - cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed - - _panel_instance->config(cfg); - - // Configure settings for touch control. - if (portduino_config.touchscreenModule) { - if (portduino_config.touchscreenModule == xpt2046) { - _touch_instance = new lgfx::Touch_XPT2046; - } else if (portduino_config.touchscreenModule == stmpe610) { - _touch_instance = new lgfx::Touch_STMPE610; - } else if (portduino_config.touchscreenModule == ft5x06) { - _touch_instance = new lgfx::Touch_FT5x06; - } - auto touch_cfg = _touch_instance->config(); - - touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; - touch_cfg.x_min = 0; - touch_cfg.x_max = portduino_config.displayHeight - 1; - touch_cfg.y_min = 0; - touch_cfg.y_max = portduino_config.displayWidth - 1; - touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; - touch_cfg.bus_shared = true; - touch_cfg.offset_rotation = portduino_config.touchscreenRotate; - if (portduino_config.touchscreenI2CAddr != -1) { - touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; - } else { - touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; - } - - _touch_instance->config(touch_cfg); - _panel_instance->setTouch(_touch_instance); - } -#if defined(SDL_h_) - if (portduino_config.displayPanel == x11) { - lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; - sdl_panel_->setup(); - sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); - } -#endif - setPanel(_panel_instance); // Sets the panel to use. + else { + _panel_instance = new lgfx::Panel_NULL; + LOG_ERROR("Unknown display panel configured!"); } + + auto buscfg = _bus_instance.config(); + buscfg.spi_mode = 0; + buscfg.spi_host = portduino_config.display_spi_dev_int; + + buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(buscfg); // applies the set value to the bus. + if (portduino_config.displayPanel != x11) + _panel_instance->setBus(&_bus_instance); // set the bus on the panel. + + auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. + LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); + cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = portduino_config.displayReset.pin; + if (portduino_config.displayRotate) { + cfg.panel_width = portduino_config.displayHeight; // actual displayable width + cfg.panel_height = portduino_config.displayWidth; // actual displayable height + } else { + cfg.panel_width = portduino_config.displayWidth; // actual displayable width + cfg.panel_height = portduino_config.displayHeight; // actual displayable height + } + cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction + cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction + cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed + + _panel_instance->config(cfg); + + // Configure settings for touch control. + if (portduino_config.touchscreenModule) { + if (portduino_config.touchscreenModule == xpt2046) { + _touch_instance = new lgfx::Touch_XPT2046; + } else if (portduino_config.touchscreenModule == stmpe610) { + _touch_instance = new lgfx::Touch_STMPE610; + } else if (portduino_config.touchscreenModule == ft5x06) { + _touch_instance = new lgfx::Touch_FT5x06; + } + auto touch_cfg = _touch_instance->config(); + + touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; + touch_cfg.x_min = 0; + touch_cfg.x_max = portduino_config.displayHeight - 1; + touch_cfg.y_min = 0; + touch_cfg.y_max = portduino_config.displayWidth - 1; + touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; + touch_cfg.bus_shared = true; + touch_cfg.offset_rotation = portduino_config.touchscreenRotate; + if (portduino_config.touchscreenI2CAddr != -1) { + touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; + } else { + touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; + } + + _touch_instance->config(touch_cfg); + _panel_instance->setTouch(_touch_instance); + } +#if defined(SDL_h_) + if (portduino_config.displayPanel == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; + sdl_panel_->setup(); + sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); + } +#endif + setPanel(_panel_instance); // Sets the panel to use. + } }; static LGFX *tft = nullptr; @@ -921,82 +892,80 @@ static LGFX *tft = nullptr; #elif defined(HX8357_CS) #include // Graphics and font library for HX8357 driver chip -class LGFX : public lgfx::LGFX_Device -{ - lgfx::Panel_HX8357D _panel_instance; - lgfx::Bus_SPI _bus_instance; +class LGFX : public lgfx::LGFX_Device { + lgfx::Panel_HX8357D _panel_instance; + lgfx::Bus_SPI _bus_instance; #if defined(USE_XPT2046) - lgfx::Touch_XPT2046 _touch_instance; + lgfx::Touch_XPT2046 _touch_instance; #endif - public: - LGFX(void) +public: + LGFX(void) { + // Panel_HX8357D { - // Panel_HX8357D - { - // configure SPI - auto cfg = _bus_instance.config(); + // configure SPI + auto cfg = _bus_instance.config(); - cfg.spi_host = HX8357_SPI_HOST; - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing - // 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = HX8357_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = HX8357_MOSI; // Set SPI MOSI pin number - cfg.pin_miso = HX8357_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = HX8357_RS; // Set SPI DC pin number (-1 = disable) + cfg.spi_host = HX8357_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by + // dividing 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = HX8357_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = HX8357_MOSI; // Set SPI MOSI pin number + cfg.pin_miso = HX8357_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = HX8357_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } - { - // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - - cfg.pin_cs = HX8357_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = HX8357_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = HX8357_BUSY; // Pin number where BUSY is connected (-1 = disable) - - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is upside down) - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - - _panel_instance.config(cfg); - } -#if defined(USE_XPT2046) - { - // Configure settings for touch control. - auto touch_cfg = _touch_instance.config(); - - touch_cfg.pin_cs = TOUCH_CS; - touch_cfg.x_min = 0; - touch_cfg.x_max = TFT_HEIGHT - 1; - touch_cfg.y_min = 0; - touch_cfg.y_max = TFT_WIDTH - 1; - touch_cfg.pin_int = -1; - touch_cfg.bus_shared = true; - touch_cfg.offset_rotation = 1; - - _touch_instance.config(touch_cfg); - _panel_instance.setTouch(&_touch_instance); - } -#endif - setPanel(&_panel_instance); + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. } + { + // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = HX8357_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = HX8357_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = HX8357_BUSY; // Pin number where BUSY is connected (-1 = disable) + + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + _panel_instance.config(cfg); + } +#if defined(USE_XPT2046) + { + // Configure settings for touch control. + auto touch_cfg = _touch_instance.config(); + + touch_cfg.pin_cs = TOUCH_CS; + touch_cfg.x_min = 0; + touch_cfg.x_max = TFT_HEIGHT - 1; + touch_cfg.y_min = 0; + touch_cfg.y_max = TFT_WIDTH - 1; + touch_cfg.pin_int = -1; + touch_cfg.bus_shared = true; + touch_cfg.offset_rotation = 1; + + _touch_instance.config(touch_cfg); + _panel_instance.setTouch(&_touch_instance); + } +#endif + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -1006,133 +975,129 @@ static LGFX *tft = nullptr; #include #include -class PanelInit_ST7701 : public lgfx::Panel_ST7701 -{ - public: - const uint8_t *getInitCommands(uint8_t listno) const override - { - // 180 degree hw rotation: vertical flip, horizontal flip - static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip - 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL - 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip) - 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS - 0xFF, 0xFF}; - switch (listno) { - case 1: - return list1; - default: - return lgfx::Panel_ST7701::getInitCommands(listno); - } +class PanelInit_ST7701 : public lgfx::Panel_ST7701 { +public: + const uint8_t *getInitCommands(uint8_t listno) const override { + // 180 degree hw rotation: vertical flip, horizontal flip + static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL + 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip) + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS + 0xFF, 0xFF}; + switch (listno) { + case 1: + return list1; + default: + return lgfx::Panel_ST7701::getInitCommands(listno); } + } }; -class LGFX : public lgfx::LGFX_Device -{ - PanelInit_ST7701 _panel_instance; - lgfx::Bus_RGB _bus_instance; - lgfx::Light_PWM _light_instance; - lgfx::Touch_FT5x06 _touch_instance; +class LGFX : public lgfx::LGFX_Device { + PanelInit_ST7701 _panel_instance; + lgfx::Bus_RGB _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_FT5x06 _touch_instance; - public: - LGFX(void) +public: + LGFX(void) { { - { - auto cfg = _panel_instance.config(); - cfg.memory_width = 800; - cfg.memory_height = 480; - cfg.panel_width = TFT_WIDTH; - cfg.panel_height = TFT_HEIGHT; - cfg.offset_x = TFT_OFFSET_X; - cfg.offset_y = TFT_OFFSET_Y; - _panel_instance.config(cfg); - } - - { - auto cfg = _panel_instance.config_detail(); - cfg.pin_cs = ST7701_CS; - cfg.pin_sclk = ST7701_SCK; - cfg.pin_mosi = ST7701_SDA; - // cfg.use_psram = 1; - _panel_instance.config_detail(cfg); - } - - { - auto cfg = _bus_instance.config(); - cfg.panel = &_panel_instance; -#ifdef SENSECAP_INDICATOR - cfg.pin_d0 = GPIO_NUM_15; // B0 - cfg.pin_d1 = GPIO_NUM_14; // B1 - cfg.pin_d2 = GPIO_NUM_13; // B2 - cfg.pin_d3 = GPIO_NUM_12; // B3 - cfg.pin_d4 = GPIO_NUM_11; // B4 - - cfg.pin_d5 = GPIO_NUM_10; // G0 - cfg.pin_d6 = GPIO_NUM_9; // G1 - cfg.pin_d7 = GPIO_NUM_8; // G2 - cfg.pin_d8 = GPIO_NUM_7; // G3 - cfg.pin_d9 = GPIO_NUM_6; // G4 - cfg.pin_d10 = GPIO_NUM_5; // G5 - - cfg.pin_d11 = GPIO_NUM_4; // R0 - cfg.pin_d12 = GPIO_NUM_3; // R1 - cfg.pin_d13 = GPIO_NUM_2; // R2 - cfg.pin_d14 = GPIO_NUM_1; // R3 - cfg.pin_d15 = GPIO_NUM_0; // R4 - - cfg.pin_henable = GPIO_NUM_18; - cfg.pin_vsync = GPIO_NUM_17; - cfg.pin_hsync = GPIO_NUM_16; - cfg.pin_pclk = GPIO_NUM_21; - cfg.freq_write = 12000000; - - cfg.hsync_polarity = 0; - cfg.hsync_front_porch = 10; - cfg.hsync_pulse_width = 8; - cfg.hsync_back_porch = 50; - - cfg.vsync_polarity = 0; - cfg.vsync_front_porch = 10; - cfg.vsync_pulse_width = 8; - cfg.vsync_back_porch = 20; - - cfg.pclk_active_neg = 0; - cfg.de_idle_high = 1; - cfg.pclk_idle_high = 0; -#endif - _bus_instance.config(cfg); - } - _panel_instance.setBus(&_bus_instance); - - { - auto cfg = _light_instance.config(); - cfg.pin_bl = ST7701_BL; - _light_instance.config(cfg); - } - _panel_instance.light(&_light_instance); - - { - auto cfg = _touch_instance.config(); - cfg.pin_cs = -1; - cfg.x_min = 0; - cfg.x_max = 479; - cfg.y_min = 0; - cfg.y_max = 479; - cfg.pin_int = -1; // don't use SCREEN_TOUCH_INT; - cfg.pin_rst = SCREEN_TOUCH_RST; - cfg.bus_shared = true; - cfg.offset_rotation = TFT_OFFSET_ROTATION; - - cfg.i2c_port = TOUCH_I2C_PORT; - cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; - cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } - - setPanel(&_panel_instance); + auto cfg = _panel_instance.config(); + cfg.memory_width = 800; + cfg.memory_height = 480; + cfg.panel_width = TFT_WIDTH; + cfg.panel_height = TFT_HEIGHT; + cfg.offset_x = TFT_OFFSET_X; + cfg.offset_y = TFT_OFFSET_Y; + _panel_instance.config(cfg); } + + { + auto cfg = _panel_instance.config_detail(); + cfg.pin_cs = ST7701_CS; + cfg.pin_sclk = ST7701_SCK; + cfg.pin_mosi = ST7701_SDA; + // cfg.use_psram = 1; + _panel_instance.config_detail(cfg); + } + + { + auto cfg = _bus_instance.config(); + cfg.panel = &_panel_instance; +#ifdef SENSECAP_INDICATOR + cfg.pin_d0 = GPIO_NUM_15; // B0 + cfg.pin_d1 = GPIO_NUM_14; // B1 + cfg.pin_d2 = GPIO_NUM_13; // B2 + cfg.pin_d3 = GPIO_NUM_12; // B3 + cfg.pin_d4 = GPIO_NUM_11; // B4 + + cfg.pin_d5 = GPIO_NUM_10; // G0 + cfg.pin_d6 = GPIO_NUM_9; // G1 + cfg.pin_d7 = GPIO_NUM_8; // G2 + cfg.pin_d8 = GPIO_NUM_7; // G3 + cfg.pin_d9 = GPIO_NUM_6; // G4 + cfg.pin_d10 = GPIO_NUM_5; // G5 + + cfg.pin_d11 = GPIO_NUM_4; // R0 + cfg.pin_d12 = GPIO_NUM_3; // R1 + cfg.pin_d13 = GPIO_NUM_2; // R2 + cfg.pin_d14 = GPIO_NUM_1; // R3 + cfg.pin_d15 = GPIO_NUM_0; // R4 + + cfg.pin_henable = GPIO_NUM_18; + cfg.pin_vsync = GPIO_NUM_17; + cfg.pin_hsync = GPIO_NUM_16; + cfg.pin_pclk = GPIO_NUM_21; + cfg.freq_write = 12000000; + + cfg.hsync_polarity = 0; + cfg.hsync_front_porch = 10; + cfg.hsync_pulse_width = 8; + cfg.hsync_back_porch = 50; + + cfg.vsync_polarity = 0; + cfg.vsync_front_porch = 10; + cfg.vsync_pulse_width = 8; + cfg.vsync_back_porch = 20; + + cfg.pclk_active_neg = 0; + cfg.de_idle_high = 1; + cfg.pclk_idle_high = 0; +#endif + _bus_instance.config(cfg); + } + _panel_instance.setBus(&_bus_instance); + + { + auto cfg = _light_instance.config(); + cfg.pin_bl = ST7701_BL; + _light_instance.config(cfg); + } + _panel_instance.light(&_light_instance); + + { + auto cfg = _touch_instance.config(); + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = 479; + cfg.y_min = 0; + cfg.y_max = 479; + cfg.pin_int = -1; // don't use SCREEN_TOUCH_INT; + cfg.pin_rst = SCREEN_TOUCH_RST; + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; + cfg.freq = 400000; + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -1150,370 +1115,354 @@ extern unPhone unphone; GpioPin *TFTDisplay::backlightEnable = NULL; -TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) -{ - LOG_DEBUG("TFTDisplay!"); +TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) { + LOG_DEBUG("TFTDisplay!"); #ifdef TFT_BL - GpioPin *p = new GpioHwPin(TFT_BL); + GpioPin *p = new GpioHwPin(TFT_BL); - if (!TFT_BACKLIGHT_ON) { // Need to invert the pin before hardware - auto virtPin = new GpioVirtPin(); - new GpioNotTransformer( - virtPin, p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio - p = virtPin; - } + if (!TFT_BACKLIGHT_ON) { // Need to invert the pin before hardware + auto virtPin = new GpioVirtPin(); + new GpioNotTransformer(virtPin, + p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio + p = virtPin; + } #else - GpioPin *p = new GpioVirtPin(); // Just simulate a pin + GpioPin *p = new GpioVirtPin(); // Just simulate a pin #endif - backlightEnable = p; + backlightEnable = p; #if ARCH_PORTDUINO - if (portduino_config.displayRotate) { - setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); - } else { - setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); - } + if (portduino_config.displayRotate) { + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); + } else { + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); + } #elif defined(SCREEN_ROTATE) - setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); + setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); #else - setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif } -TFTDisplay::~TFTDisplay() -{ - // Clean up allocated line pixel buffer to prevent memory leak - if (linePixelBuffer != nullptr) { - free(linePixelBuffer); - linePixelBuffer = nullptr; - } +TFTDisplay::~TFTDisplay() { + // Clean up allocated line pixel buffer to prevent memory leak + if (linePixelBuffer != nullptr) { + free(linePixelBuffer); + linePixelBuffer = nullptr; + } } // Write the buffer to the display memory -void TFTDisplay::display(bool fromBlank) -{ - if (fromBlank) - tft->fillScreen(TFT_BLACK); +void TFTDisplay::display(bool fromBlank) { + if (fromBlank) + tft->fillScreen(TFT_BLACK); - concurrency::LockGuard g(spiLock); + concurrency::LockGuard g(spiLock); - uint32_t x, y; - uint32_t y_byteIndex; - uint8_t y_byteMask; - uint32_t x_FirstPixelUpdate; - uint32_t x_LastPixelUpdate; - bool isset, dblbuf_isset; - uint16_t colorTftMesh, colorTftBlack; - bool somethingChanged = false; + uint32_t x, y; + uint32_t y_byteIndex; + uint8_t y_byteMask; + uint32_t x_FirstPixelUpdate; + uint32_t x_LastPixelUpdate; + bool isset, dblbuf_isset; + uint16_t colorTftMesh, colorTftBlack; + bool somethingChanged = false; - // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step - colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); - colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step + colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); + colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); - y = 0; - while (y < displayHeight) { - y_byteIndex = (y / 8) * displayWidth; - y_byteMask = (1 << (y & 7)); + y = 0; + while (y < displayHeight) { + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); - // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. - if (y_byteMask == 1) { - if (!fromBlank) { - for (x = 0; x < displayWidth; x++) { - if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) - break; - } - } else { - for (x = 0; x < displayWidth; x++) { - if (buffer[x + y_byteIndex] != 0) - break; - } - } - if (x >= displayWidth) { - // No changed pixels found in these 8 rows, fast-forward to the next 8 - y = y + 8; - continue; - } + // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. + if (y_byteMask == 1) { + if (!fromBlank) { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) + break; } - - // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating - for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { - isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; - - if (!fromBlank) { - // get src pixel in the page based ordering the OLED lib uses - dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; - if (isset != dblbuf_isset) { - break; - } - } else if (isset) { - break; - } + } else { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != 0) + break; } - - // Did we find a pixel that needs updating on this row? - if (x_FirstPixelUpdate < displayWidth) { - - // Quickly write out the first changed pixel (saves another array lookup) - linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; - x_LastPixelUpdate = x_FirstPixelUpdate; - - // Step 3: copy all remaining pixels in this row into the pixel line buffer, - // while also recording the last pixel in the row that needs updating - for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { - isset = buffer[x + y_byteIndex] & y_byteMask; - linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; - - if (!fromBlank) { - dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; - if (isset != dblbuf_isset) { - x_LastPixelUpdate = x; - } - } else if (isset) { - x_LastPixelUpdate = x; - } - } -#if defined(HACKADAY_COMMUNICATOR) - tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], - (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); -#else - // Step 4: Send the changed pixels on this line to the screen as a single block transfer. - // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. - tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, - &linePixelBuffer[x_FirstPixelUpdate]); -#endif - somethingChanged = true; - } - y++; + } + if (x >= displayWidth) { + // No changed pixels found in these 8 rows, fast-forward to the next 8 + y = y + 8; + continue; + } } - // Copy the Buffer to the Back Buffer - if (somethingChanged) - memcpy(buffer_back, buffer, displayBufferSize); + + // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating + for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { + isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses + dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + break; + } + } else if (isset) { + break; + } + } + + // Did we find a pixel that needs updating on this row? + if (x_FirstPixelUpdate < displayWidth) { + + // Quickly write out the first changed pixel (saves another array lookup) + linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; + x_LastPixelUpdate = x_FirstPixelUpdate; + + // Step 3: copy all remaining pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating + for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; + linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; + + if (!fromBlank) { + dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + x_LastPixelUpdate = x; + } + } else if (isset) { + x_LastPixelUpdate = x; + } + } +#if defined(HACKADAY_COMMUNICATOR) + tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); +#else + // Step 4: Send the changed pixels on this line to the screen as a single block transfer. + // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. + tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, &linePixelBuffer[x_FirstPixelUpdate]); +#endif + somethingChanged = true; + } + y++; + } + // Copy the Buffer to the Back Buffer + if (somethingChanged) + memcpy(buffer_back, buffer, displayBufferSize); } -void TFTDisplay::sdlLoop() -{ +void TFTDisplay::sdlLoop() { #if defined(SDL_h_) - static int lastPressed = 0; - static int shuttingDown = false; - if (portduino_config.displayPanel == x11) { - lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; - if (sdl_panel_->loop() && !shuttingDown) { - LOG_WARN("Window Closed!"); - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } - // debounce - if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed)) - return; - if (!sdl_panel_->gpio_in(37)) { - lastPressed = 37; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(36)) { - lastPressed = 36; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(38)) { - lastPressed = 38; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(39)) { - lastPressed = 39; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) { - lastPressed = SDL_SCANCODE_KP_ENTER; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else { - lastPressed = 0; - } + static int lastPressed = 0; + static int shuttingDown = false; + if (portduino_config.displayPanel == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; + if (sdl_panel_->loop() && !shuttingDown) { + LOG_WARN("Window Closed!"); + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); } + // debounce + if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed)) + return; + if (!sdl_panel_->gpio_in(37)) { + lastPressed = 37; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(36)) { + lastPressed = 36; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(38)) { + lastPressed = 38; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(39)) { + lastPressed = 39; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) { + lastPressed = SDL_SCANCODE_KP_ENTER; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else { + lastPressed = 0; + } + } #endif } // Send a command to the display (low level function) -void TFTDisplay::sendCommand(uint8_t com) -{ - // handle display on/off directly - switch (com) { - case DISPLAYON: { - // LOG_DEBUG("Display on"); - backlightEnable->set(true); -#if ARCH_PORTDUINO - display(true); - if (portduino_config.displayBacklight.pin > 0) - digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); -#elif defined(HACKADAY_COMMUNICATOR) - tft->displayOn(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) - tft->wakeup(); - tft->powerSaveOff(); -#endif - -#ifdef VTFT_CTRL - digitalWrite(VTFT_CTRL, LOW); -#endif -#ifdef UNPHONE - unphone.backlight(true); // using unPhone library -#endif -#ifdef RAK14014 -#elif !defined(M5STACK) && !defined(ST7789_CS) && \ - !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function - tft->setBrightness(172); -#endif - break; - } - case DISPLAYOFF: { - // LOG_DEBUG("Display off"); - backlightEnable->set(false); -#if ARCH_PORTDUINO - tft->clear(); - if (portduino_config.displayBacklight.pin > 0) - digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); -#elif defined(HACKADAY_COMMUNICATOR) - tft->displayOff(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) - tft->sleep(); - tft->powerSaveOn(); -#endif - -#ifdef VTFT_CTRL - digitalWrite(VTFT_CTRL, HIGH); -#endif -#ifdef UNPHONE - unphone.backlight(false); // using unPhone library -#endif -#ifdef RAK14014 -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) - tft->setBrightness(0); -#endif - break; - } - default: - break; - } - - // Drop all other commands to device (we just update the buffer) -} - -void TFTDisplay::setDisplayBrightness(uint8_t _brightness) -{ -#ifdef RAK14014 - // todo -#elif !defined(HACKADAY_COMMUNICATOR) - tft->setBrightness(_brightness); - LOG_DEBUG("Brightness is set to value: %i ", _brightness); -#endif -} - -void TFTDisplay::flipScreenVertically() -{ -#if defined(T_WATCH_S3) - LOG_DEBUG("Flip TFT vertically"); // T-Watch S3 right-handed orientation - tft->setRotation(0); -#endif -} - -bool TFTDisplay::hasTouch(void) -{ -#ifdef RAK14014 - return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) - return tft->touch() != nullptr; -#else - return false; -#endif -} - -bool TFTDisplay::getTouch(int16_t *x, int16_t *y) -{ -#ifdef RAK14014 - if (_rak14014_touch_int) { - _rak14014_touch_int = false; - /* The X and Y axes have to be switched */ - *y = ft6336u.read_touch1_x(); - *x = TFT_HEIGHT - ft6336u.read_touch1_y(); - return true; - } else { - return false; - } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) - return tft->getTouch(x, y); -#else - return false; -#endif -} - -void TFTDisplay::setDetected(uint8_t detected) -{ - (void)detected; -} - -// Connect to the display -bool TFTDisplay::connect() -{ - concurrency::LockGuard g(spiLock); - LOG_INFO("Do TFT init"); -#ifdef RAK14014 - tft = new TFT_eSPI; -#elif defined(HACKADAY_COMMUNICATOR) - bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); - tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, - 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, - sizeof(nv3007_279_init_operations)); - -#else - tft = new LGFX; -#endif - +void TFTDisplay::sendCommand(uint8_t com) { + // handle display on/off directly + switch (com) { + case DISPLAYON: { + // LOG_DEBUG("Display on"); backlightEnable->set(true); - LOG_INFO("Power to TFT Backlight"); +#if ARCH_PORTDUINO + display(true); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); +#elif defined(HACKADAY_COMMUNICATOR) + tft->displayOn(); +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) + tft->wakeup(); + tft->powerSaveOff(); +#endif +#ifdef VTFT_CTRL + digitalWrite(VTFT_CTRL, LOW); +#endif #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#ifdef HACKADAY_COMMUNICATOR - bool beginStatus = tft->begin(); - if (beginStatus) - LOG_DEBUG("TFT Success!"); - else - LOG_ERROR("TFT Fail!"); +#ifdef RAK14014 +#elif !defined(M5STACK) && !defined(ST7789_CS) && \ + !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function + tft->setBrightness(172); +#endif + break; + } + case DISPLAYOFF: { + // LOG_DEBUG("Display off"); + backlightEnable->set(false); +#if ARCH_PORTDUINO + tft->clear(); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); +#elif defined(HACKADAY_COMMUNICATOR) + tft->displayOff(); +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) + tft->sleep(); + tft->powerSaveOn(); +#endif + +#ifdef VTFT_CTRL + digitalWrite(VTFT_CTRL, HIGH); +#endif +#ifdef UNPHONE + unphone.backlight(false); // using unPhone library +#endif +#ifdef RAK14014 +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) + tft->setBrightness(0); +#endif + break; + } + default: + break; + } + + // Drop all other commands to device (we just update the buffer) +} + +void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { +#ifdef RAK14014 + // todo +#elif !defined(HACKADAY_COMMUNICATOR) + tft->setBrightness(_brightness); + LOG_DEBUG("Brightness is set to value: %i ", _brightness); +#endif +} + +void TFTDisplay::flipScreenVertically() { +#if defined(T_WATCH_S3) + LOG_DEBUG("Flip TFT vertically"); // T-Watch S3 right-handed orientation + tft->setRotation(0); +#endif +} + +bool TFTDisplay::hasTouch(void) { +#ifdef RAK14014 + return true; +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) + return tft->touch() != nullptr; #else - tft->init(); + return false; +#endif +} + +bool TFTDisplay::getTouch(int16_t *x, int16_t *y) { +#ifdef RAK14014 + if (_rak14014_touch_int) { + _rak14014_touch_int = false; + /* The X and Y axes have to be switched */ + *y = ft6336u.read_touch1_x(); + *x = TFT_HEIGHT - ft6336u.read_touch1_y(); + return true; + } else { + return false; + } +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) + return tft->getTouch(x, y); +#else + return false; +#endif +} + +void TFTDisplay::setDetected(uint8_t detected) { (void)detected; } + +// Connect to the display +bool TFTDisplay::connect() { + concurrency::LockGuard g(spiLock); + LOG_INFO("Do TFT init"); +#ifdef RAK14014 + tft = new TFT_eSPI; +#elif defined(HACKADAY_COMMUNICATOR) + bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); + tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, 0 /* row offset 1 */, + 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, sizeof(nv3007_279_init_operations)); + +#else + tft = new LGFX; +#endif + + backlightEnable->set(true); + LOG_INFO("Power to TFT Backlight"); + +#ifdef UNPHONE + unphone.backlight(true); // using unPhone library +#endif +#ifdef HACKADAY_COMMUNICATOR + bool beginStatus = tft->begin(); + if (beginStatus) + LOG_DEBUG("TFT Success!"); + else + LOG_ERROR("TFT Fail!"); +#else + tft->init(); #endif #if defined(M5STACK) - tft->setRotation(0); + tft->setRotation(0); #elif defined(RAK14014) - tft->setRotation(1); - tft->setSwapBytes(true); - // tft->fillScreen(TFT_BLACK); - ft6336u.begin(); - pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); + tft->setRotation(1); + tft->setSwapBytes(true); + // tft->fillScreen(TFT_BLACK); + ft6336u.begin(); + pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); #elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) - tft->setRotation(1); // T-Deck has the TFT in landscape + tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) - tft->setRotation(2); // T-Watch S3 left-handed orientation + tft->setRotation(2); // T-Watch S3 left-handed orientation #elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) - tft->setRotation(0); // use config.yaml to set rotation + tft->setRotation(0); // use config.yaml to set rotation #else - tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label + tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif - tft->fillScreen(TFT_BLACK); + tft->fillScreen(TFT_BLACK); - if (this->linePixelBuffer == NULL) { - this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); + if (this->linePixelBuffer == NULL) { + this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); - if (!this->linePixelBuffer) { - LOG_ERROR("Not enough memory to create TFT line buffer\n"); - return false; - } + if (!this->linePixelBuffer) { + LOG_ERROR("Not enough memory to create TFT line buffer\n"); + return false; } - return true; + } + return true; } #endif // USE_TFTDISPLAY diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index a64922d23..0fa65633a 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -12,55 +12,54 @@ * * turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted? */ -class TFTDisplay : public OLEDDisplay -{ - public: - /* constructor - FIXME - the parameters are not used, just a temporary hack to keep working like the old displays - */ - TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); +class TFTDisplay : public OLEDDisplay { +public: + /* constructor + FIXME - the parameters are not used, just a temporary hack to keep working like the old displays + */ + TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); - // Destructor to clean up allocated memory - ~TFTDisplay(); + // Destructor to clean up allocated memory + ~TFTDisplay(); - // Write the buffer to the display memory - virtual void display() override { display(false); }; - virtual void display(bool fromBlank); - void sdlLoop(); + // Write the buffer to the display memory + virtual void display() override { display(false); }; + virtual void display(bool fromBlank); + void sdlLoop(); - // Turn the display upside down - virtual void flipScreenVertically(); + // Turn the display upside down + virtual void flipScreenVertically(); - // Touch screen (static handlers) - static bool hasTouch(void); - static bool getTouch(int16_t *x, int16_t *y); + // Touch screen (static handlers) + static bool hasTouch(void); + static bool getTouch(int16_t *x, int16_t *y); - // Functions for changing display brightness - void setDisplayBrightness(uint8_t); + // Functions for changing display brightness + void setDisplayBrightness(uint8_t); - /** - * shim to make the abstraction happy - * - */ - void setDetected(uint8_t detected); + /** + * shim to make the abstraction happy + * + */ + void setDetected(uint8_t detected); - /** - * This is normally managed entirely by TFTDisplay, but some rare applications (heltec tracker) might need to replace the - * default GPIO behavior with something a bit more complex. - * - * We (cruftily) make it static so that variant.cpp can access it without needing a ptr to the TFTDisplay instance. - */ - static GpioPin *backlightEnable; + /** + * This is normally managed entirely by TFTDisplay, but some rare applications (heltec tracker) might need to replace + * the default GPIO behavior with something a bit more complex. + * + * We (cruftily) make it static so that variant.cpp can access it without needing a ptr to the TFTDisplay instance. + */ + static GpioPin *backlightEnable; - protected: - // the header size of the buffer used, e.g. for the SPI command header - virtual int getBufferOffset(void) override { return 0; } +protected: + // the header size of the buffer used, e.g. for the SPI command header + virtual int getBufferOffset(void) override { return 0; } - // Send a command to the display (low level function) - virtual void sendCommand(uint8_t com) override; + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) override; - // Connect to the display - virtual bool connect() override; + // Connect to the display + virtual bool connect() override; - uint16_t *linePixelBuffer = nullptr; + uint16_t *linePixelBuffer = nullptr; }; \ No newline at end of file diff --git a/src/graphics/TimeFormatters.cpp b/src/graphics/TimeFormatters.cpp index 0a1c23341..6f41a934f 100644 --- a/src/graphics/TimeFormatters.cpp +++ b/src/graphics/TimeFormatters.cpp @@ -4,120 +4,117 @@ #include "mesh/NodeDB.h" #include -bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) -{ - // Cache the result - avoid frequent recalculation - static uint8_t hoursCached = 0, minutesCached = 0; - static uint32_t daysAgoCached = 0; - static uint32_t secondsAgoCached = 0; - static bool validCached = false; +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) { + // Cache the result - avoid frequent recalculation + static uint8_t hoursCached = 0, minutesCached = 0; + static uint32_t daysAgoCached = 0; + static uint32_t secondsAgoCached = 0; + static bool validCached = false; - // Abort: if timezone not set - if (strlen(config.device.tzdef) == 0) { - validCached = false; - return validCached; - } - - // Abort: if invalid pointers passed - if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { - validCached = false; - return validCached; - } - - // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) - if (secondsAgo > SEC_PER_DAY * 30UL * 6) { - validCached = false; - return validCached; - } - - // If repeated request, don't bother recalculating - if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { - if (validCached) { - *hours = hoursCached; - *minutes = minutesCached; - *daysAgo = daysAgoCached; - } - return validCached; - } - - // Get local time - uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time - - // Abort: if RTC not set - if (!secondsRTC) { - validCached = false; - return validCached; - } - - // Get absolute time when last seen - uint32_t secondsSeenAt = secondsRTC - secondsAgo; - - // Calculate daysAgo - *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed - - // Get seconds since midnight - uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into hours and minutes - *hours = hms / SEC_PER_HOUR; - *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - - // Cache the result - daysAgoCached = *daysAgo; - hoursCached = *hours; - minutesCached = *minutes; - secondsAgoCached = secondsAgo; - - validCached = true; + // Abort: if timezone not set + if (strlen(config.device.tzdef) == 0) { + validCached = false; return validCached; -} + } -void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) -{ - // Use an absolute timestamp in some cases. - // Particularly useful with E-Ink displays. Static UI, fewer refreshes. - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); + // Abort: if invalid pointers passed + if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { + validCached = false; + return validCached; + } - if (agoSecs < 120) // last 2 mins? - snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); - // -- if suitable for timestamp -- - else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); - else if (useTimestamp && daysAgo == 0) // Today - snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); - else if (useTimestamp && daysAgo == 1) // Yesterday - snprintf(timeStr, maxLength, "Seen yesterday"); - else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) - snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); - // -- if using time delta instead -- - else if (agoSecs < 120 * 60) // last 2 hrs - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); - // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. - else if ((agoSecs / 60 / 60) < (730 * 6)) - snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); - else - snprintf(timeStr, maxLength, "unknown age"); -} + // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) + if (secondsAgo > SEC_PER_DAY * 30UL * 6) { + validCached = false; + return validCached; + } -void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs) -{ - uint32_t days = uptimeMillis / 86400000; - uint32_t hours = (uptimeMillis % 86400000) / 3600000; - uint32_t mins = (uptimeMillis % 3600000) / 60000; - uint32_t secs = (uptimeMillis % 60000) / 1000; - - if (days) { - snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours); - } else if (hours) { - snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins); - } else if (!includeSecs) { - snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins); - } else if (mins) { - snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs); - } else { - snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs); + // If repeated request, don't bother recalculating + if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { + if (validCached) { + *hours = hoursCached; + *minutes = minutesCached; + *daysAgo = daysAgoCached; } + return validCached; + } + + // Get local time + uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time + + // Abort: if RTC not set + if (!secondsRTC) { + validCached = false; + return validCached; + } + + // Get absolute time when last seen + uint32_t secondsSeenAt = secondsRTC - secondsAgo; + + // Calculate daysAgo + *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed + + // Get seconds since midnight + uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into hours and minutes + *hours = hms / SEC_PER_HOUR; + *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Cache the result + daysAgoCached = *daysAgo; + hoursCached = *hours; + minutesCached = *minutes; + secondsAgoCached = secondsAgo; + + validCached = true; + return validCached; +} + +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) { + // Use an absolute timestamp in some cases. + // Particularly useful with E-Ink displays. Static UI, fewer refreshes. + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); + + if (agoSecs < 120) // last 2 mins? + snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); + // -- if suitable for timestamp -- + else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); + else if (useTimestamp && daysAgo == 0) // Today + snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); + else if (useTimestamp && daysAgo == 1) // Yesterday + snprintf(timeStr, maxLength, "Seen yesterday"); + else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) + snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); + // -- if using time delta instead -- + else if (agoSecs < 120 * 60) // last 2 hrs + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); + // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. + else if ((agoSecs / 60 / 60) < (730 * 6)) + snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); + else + snprintf(timeStr, maxLength, "unknown age"); +} + +void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs) { + uint32_t days = uptimeMillis / 86400000; + uint32_t hours = (uptimeMillis % 86400000) / 3600000; + uint32_t mins = (uptimeMillis % 3600000) / 60000; + uint32_t secs = (uptimeMillis % 60000) / 1000; + + if (days) { + snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours); + } else if (hours) { + snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins); + } else if (!includeSecs) { + snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins); + } else if (mins) { + snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs); + } else { + snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs); + } } diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index a24f5b15c..6672eb5e9 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -8,734 +8,695 @@ #include #include -namespace graphics -{ +namespace graphics { -VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) -{ - initializeKeyboard(); - // Set cursor to H(2, 5) - cursorRow = 2; - cursorCol = 5; +VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) { + initializeKeyboard(); + // Set cursor to H(2, 5) + cursorRow = 2; + cursorCol = 5; } VirtualKeyboard::~VirtualKeyboard() {} -void VirtualKeyboard::initializeKeyboard() -{ - // New 4 row, 11 column keyboard layout: - static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, - {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; +void VirtualKeyboard::initializeKeyboard() { + // New 4 row, 11 column keyboard layout: + static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; - // Derive layout dimensions and assert they match the configured keyboard grid - constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); - constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); - static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); - static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); + // Derive layout dimensions and assert they match the configured keyboard grid + constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); + constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); + static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); + static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); - // Initialize all keys to empty first - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; - } + // Initialize all keys to empty first + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; } + } - // Fill keyboard from the 2D layout - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - char ch = LAYOUT[row][col]; - // No empty slots in the simplified layout + // Fill keyboard from the 2D layout + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + char ch = LAYOUT[row][col]; + // No empty slots in the simplified layout - VirtualKeyType type = VK_CHAR; - if (ch == '\b') { - type = VK_BACKSPACE; - } else if (ch == '\n') { - type = VK_ENTER; - } else if (ch == '\x1b') { // ESC - type = VK_ESC; - } else if (ch == ' ') { - type = VK_SPACE; - } + VirtualKeyType type = VK_CHAR; + if (ch == '\b') { + type = VK_BACKSPACE; + } else if (ch == '\n') { + type = VK_ENTER; + } else if (ch == '\x1b') { // ESC + type = VK_ESC; + } else if (ch == ' ') { + type = VK_SPACE; + } - // Make action keys wider to fit text while keeping the last column aligned - uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; - keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; - } + // Make action keys wider to fit text while keeping the last column aligned + uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; + keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; } + } } -void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) -{ - // Repeat ticking is driven by NotificationRenderer once per frame - // Base styles - display->setColor(WHITE); +void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) { + // Repeat ticking is driven by NotificationRenderer once per frame + // Base styles + display->setColor(WHITE); + display->setFont(FONT_SMALL); + + // Screen geometry + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column + // labels Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide + const bool isWide = screenW >= 200; + + // Determine last-column label max width + display->setFont(FONT_SMALL); + const int wENTER = display->getStringWidth("ENTER"); + int lastColLabelW = wENTER; // ENTER is usually the widest + // Smaller padding on very small screens to avoid excessive whitespace + const int lastColPad = (screenW <= 128 ? 2 : 6); + const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys + + // Always reserve width for the rightmost text column to avoid overlap on small screens + int cellW = 0; + int leftoverW = 0; + { + const int leftCols = KEYBOARD_COLS - 1; // 10 input characters + int usableW = screenW - reservedLastColW; + if (usableW < leftCols) { + // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) + usableW = leftCols; + } + cellW = usableW / leftCols; + leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) + } + + // Dynamic key geometry + int cellH = KEY_HEIGHT; + int keyboardStartY = 0; + if (screenH <= 64) { + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); + const int gapBelowHeader = 0; + const int singleLineBoxHeight = FONT_HEIGHT_SMALL; + const int gapAboveKeyboard = 0; + keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; + if (keyboardStartY < 0) + keyboardStartY = 0; + if (keyboardStartY > screenH) + keyboardStartY = screenH; + int keyboardHeight = screenH - keyboardStartY; + cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); + } else if (isWide) { + // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. + cellH = std::max((int)KEY_HEIGHT, cellW); + + // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. + // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 display->setFont(FONT_SMALL); - - // Screen geometry - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); - - // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels - // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide - const bool isWide = screenW >= 200; - - // Determine last-column label max width - display->setFont(FONT_SMALL); - const int wENTER = display->getStringWidth("ENTER"); - int lastColLabelW = wENTER; // ENTER is usually the widest - // Smaller padding on very small screens to avoid excessive whitespace - const int lastColPad = (screenW <= 128 ? 2 : 6); - const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys - - // Always reserve width for the rightmost text column to avoid overlap on small screens - int cellW = 0; - int leftoverW = 0; - { - const int leftCols = KEYBOARD_COLS - 1; // 10 input characters - int usableW = screenW - reservedLastColW; - if (usableW < leftCols) { - // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) - usableW = leftCols; - } - cellW = usableW / leftCols; - leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); + const int headerToBoxGap = 1; + const int gapAboveKb = 1; + const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom + int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); + int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; + if (maxCellHAllowed < (int)KEY_HEIGHT) + maxCellHAllowed = KEY_HEIGHT; + if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { + cellH = maxCellHAllowed; } + // Keyboard placement from bottom for wide screens + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } else { + // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom + cellH = KEY_HEIGHT; + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } - // Dynamic key geometry - int cellH = KEY_HEIGHT; - int keyboardStartY = 0; - if (screenH <= 64) { - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); - const int gapBelowHeader = 0; - const int singleLineBoxHeight = FONT_HEIGHT_SMALL; - const int gapAboveKeyboard = 0; - keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; - if (keyboardStartY < 0) - keyboardStartY = 0; - if (keyboardStartY > screenH) - keyboardStartY = screenH; - int keyboardHeight = screenH - keyboardStartY; - cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); - } else if (isWide) { - // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. - cellH = std::max((int)KEY_HEIGHT, cellW); + // Draw input area above keyboard + drawInputArea(display, offsetX, offsetY, keyboardStartY); - // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. - // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 - display->setFont(FONT_SMALL); - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); - const int headerToBoxGap = 1; - const int gapAboveKb = 1; - const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom - int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); - int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; - if (maxCellHAllowed < (int)KEY_HEIGHT) - maxCellHAllowed = KEY_HEIGHT; - if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { - cellH = maxCellHAllowed; - } - // Keyboard placement from bottom for wide screens - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } else { - // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom - cellH = KEY_HEIGHT; - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } - - // Draw input area above keyboard - drawInputArea(display, offsetX, offsetY, keyboardStartY); - - // Precompute per-column x and width with leftover distributed over left columns for even spacing - int colX[KEYBOARD_COLS]; - int colW[KEYBOARD_COLS]; - int runningX = offsetX; - for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { - int wcol = cellW + (col < leftoverW ? 1 : 0); - colX[col] = runningX; - colW[col] = wcol; - runningX += wcol; - } - // Last column - colX[KEYBOARD_COLS - 1] = runningX; - colW[KEYBOARD_COLS - 1] = reservedLastColW; - - // Draw keyboard grid - for (int row = 0; row < KEYBOARD_ROWS; row++) { - for (int col = 0; col < KEYBOARD_COLS; col++) { - const VirtualKey &k = keyboard[row][col]; - if (k.character != 0 || k.type != VK_CHAR) { - const bool isLastCol = (col == KEYBOARD_COLS - 1); - int x = colX[col]; - int w = colW[col]; - int y = offsetY + keyboardStartY + row * cellH; - int h = cellH; - bool selected = (row == cursorRow && col == cursorCol); - drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); - } - } + // Precompute per-column x and width with leftover distributed over left columns for even spacing + int colX[KEYBOARD_COLS]; + int colW[KEYBOARD_COLS]; + int runningX = offsetX; + for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { + int wcol = cellW + (col < leftoverW ? 1 : 0); + colX[col] = runningX; + colW[col] = wcol; + runningX += wcol; + } + // Last column + colX[KEYBOARD_COLS - 1] = runningX; + colW[KEYBOARD_COLS - 1] = reservedLastColW; + + // Draw keyboard grid + for (int row = 0; row < KEYBOARD_ROWS; row++) { + for (int col = 0; col < KEYBOARD_COLS; col++) { + const VirtualKey &k = keyboard[row][col]; + if (k.character != 0 || k.type != VK_CHAR) { + const bool isLastCol = (col == KEYBOARD_COLS - 1); + int x = colX[col]; + int w = colW[col]; + int y = offsetY + keyboardStartY + row * cellH; + int h = cellH; + bool selected = (row == cursorRow && col == cursorCol); + drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); + } } + } } -void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) -{ - display->setColor(WHITE); +void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) { + display->setColor(WHITE); - const int screenWidth = display->getWidth(); - const int screenHeight = display->getHeight(); - // Use the standard small font metrics for input box sizing (restore original size) - const int inputLineH = FONT_HEIGHT_SMALL; + const int screenWidth = display->getWidth(); + const int screenHeight = display->getHeight(); + // Use the standard small font metrics for input box sizing (restore original size) + const int inputLineH = FONT_HEIGHT_SMALL; - // Header uses the standard small (which may be larger on big screens) - display->setFont(FONT_SMALL); - int headerHeight = 0; - if (!headerText.empty()) { - // Draw header and reserve exact font height (plus a tighter gap) to maximize input area - display->drawString(offsetX + 2, offsetY, headerText.c_str()); - if (screenHeight <= 64) { - headerHeight = FONT_HEIGHT_SMALL - 2; // 11px - } else { - headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in - } - } - - const int boxX = offsetX; - const int boxWidth = screenWidth; - int boxY; - int boxHeight; + // Header uses the standard small (which may be larger on big screens) + display->setFont(FONT_SMALL); + int headerHeight = 0; + if (!headerText.empty()) { + // Draw header and reserve exact font height (plus a tighter gap) to maximize input area + display->drawString(offsetX + 2, offsetY, headerText.c_str()); if (screenHeight <= 64) { - const int gapBelowHeader = 0; - const int fixedBoxHeight = inputLineH; - const int gapAboveKeyboard = 0; - boxY = offsetY + headerHeight + gapBelowHeader; - boxHeight = fixedBoxHeight; - if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { - int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; - boxHeight = std::max(1, fixedBoxHeight - over); - } + headerHeight = FONT_HEIGHT_SMALL - 2; // 11px } else { - const int gapBelowHeader = 1; - int gapAboveKeyboard = 1; - int tmpBoxY = offsetY + headerHeight + gapBelowHeader; - const int minBoxHeight = inputLineH + 2; - int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; - if (availableH < minBoxHeight) - availableH = minBoxHeight; - boxY = tmpBoxY; - boxHeight = availableH; + headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in + } + } + + const int boxX = offsetX; + const int boxWidth = screenWidth; + int boxY; + int boxHeight; + if (screenHeight <= 64) { + const int gapBelowHeader = 0; + const int fixedBoxHeight = inputLineH; + const int gapAboveKeyboard = 0; + boxY = offsetY + headerHeight + gapBelowHeader; + boxHeight = fixedBoxHeight; + if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { + int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; + boxHeight = std::max(1, fixedBoxHeight - over); + } + } else { + const int gapBelowHeader = 1; + int gapAboveKeyboard = 1; + int tmpBoxY = offsetY + headerHeight + gapBelowHeader; + const int minBoxHeight = inputLineH + 2; + int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; + if (availableH < minBoxHeight) + availableH = minBoxHeight; + boxY = tmpBoxY; + boxHeight = availableH; + } + + // Draw box border + display->drawRect(boxX, boxY, boxWidth, boxHeight); + + display->setFont(FONT_SMALL); + + // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis + const int textX = boxX + 2; + const int maxTextWidth = boxWidth - 4; + const int maxLines = (boxHeight - 2) / inputLineH; + if (maxLines >= 2) { + // Inner bounds for caret clamping + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Wrap text greedily into lines that fit maxTextWidth + std::vector lines; + { + std::string remaining = inputText; + while (!remaining.empty()) { + int bestLen = 0; + for (int len = 1; len <= (int)remaining.size(); ++len) { + int w = display->getStringWidth(remaining.substr(0, len).c_str()); + if (w <= maxTextWidth) + bestLen = len; + else + break; + } + if (bestLen == 0) { + // At least show one character to make progress + bestLen = 1; + } + lines.emplace_back(remaining.substr(0, bestLen)); + remaining.erase(0, bestLen); + } } - // Draw box border - display->drawRect(boxX, boxY, boxWidth, boxHeight); + const bool scrolledUp = ((int)lines.size() > maxLines); + int caretX = textX; + int caretY = innerTop; - display->setFont(FONT_SMALL); + // Leave a small top gap to render '...' without replacing the first line + const int topInset = 2; + const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height + int lineY = innerTop + topInset; - // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis - const int textX = boxX + 2; - const int maxTextWidth = boxWidth - 4; - const int maxLines = (boxHeight - 2) / inputLineH; - if (maxLines >= 2) { - // Inner bounds for caret clamping - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Wrap text greedily into lines that fit maxTextWidth - std::vector lines; - { - std::string remaining = inputText; - while (!remaining.empty()) { - int bestLen = 0; - for (int len = 1; len <= (int)remaining.size(); ++len) { - int w = display->getStringWidth(remaining.substr(0, len).c_str()); - if (w <= maxTextWidth) - bestLen = len; - else - break; - } - if (bestLen == 0) { - // At least show one character to make progress - bestLen = 1; - } - lines.emplace_back(remaining.substr(0, bestLen)); - remaining.erase(0, bestLen); - } - } - - const bool scrolledUp = ((int)lines.size() > maxLines); - int caretX = textX; - int caretY = innerTop; - - // Leave a small top gap to render '...' without replacing the first line - const int topInset = 2; - const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height - int lineY = innerTop + topInset; - - if (scrolledUp) { - // Draw three small dots centered horizontally, vertically at the midpoint of the gap - // between the inner top and the first line's top baseline. This avoids using a tall glyph. - const int firstLineTop = lineY; // baseline top for the first visible line - const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested - const int centerX = boxX + boxWidth / 2; - const int dotSpacing = 3; // px between dots - const int dotSize = 1; // small square dot - display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); - display->fillRect(centerX, gapMidY, dotSize, dotSize); - display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); - } - - // How many lines fit with our top inset and tighter step - const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); - const int linesToShow = std::min((int)lines.size(), linesCapacity); - const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; - - for (int i = 0; i < linesToShow; ++i) { - const std::string &chunk = lines[startIndex + i]; - display->drawString(textX, lineY, chunk.c_str()); - caretX = textX + display->getStringWidth(chunk.c_str()); - caretY = lineY; - lineY += lineStep; - } - - // Draw caret at end of the last visible line - int caretPadY = 2; - if (boxHeight >= inputLineH + 4) - caretPadY = 3; - int cursorTop = caretY + caretPadY; - // Use lineStep so caret height matches the row spacing - int cursorH = lineStep - caretPadY * 2; - if (cursorH < 1) - cursorH = 1; - // Clamp vertical bounds to stay inside the inner rect - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - // Only draw if cursor is inside inner bounds - if (caretX >= innerLeft && caretX <= innerRight) { - display->drawVerticalLine(caretX, cursorTop, cursorH); - } - } else { - std::string displayText = inputText; - int textW = display->getStringWidth(displayText.c_str()); - std::string scrolled = displayText; - if (textW > maxTextWidth) { - // Trim from the left until it fits - while (textW > maxTextWidth && !scrolled.empty()) { - scrolled.erase(0, 1); - textW = display->getStringWidth(scrolled.c_str()); - } - // Add leading ellipsis and ensure it still fits - if (scrolled != displayText) { - scrolled = "..." + scrolled; - textW = display->getStringWidth(scrolled.c_str()); - // If adding ellipsis causes overflow, trim more after the ellipsis - while (textW > maxTextWidth && scrolled.size() > 3) { - scrolled.erase(3, 1); // remove chars after the ellipsis - textW = display->getStringWidth(scrolled.c_str()); - } - } - } else { - // Keep textW in sync with what we draw - textW = display->getStringWidth(scrolled.c_str()); - } - - int textY; - if (screenHeight <= 64) { - textY = boxY + (boxHeight - inputLineH) / 2; - } else { - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Center text vertically within inner box for single-line, then clamp so it never overlaps borders - int innerH = innerBottom - innerTop + 1; - textY = innerTop + std::max(0, (innerH - inputLineH) / 2); - // Clamp fully inside the inner rect - if (textY < innerTop) - textY = innerTop; - int maxTop = innerBottom - inputLineH + 1; - if (textY > maxTop) - textY = maxTop; - } - - if (!scrolled.empty()) { - display->drawString(textX, textY, scrolled.c_str()); - } - - int cursorX = textX + textW; - if (screenHeight > 64) { - const int innerRight = boxX + boxWidth - 2; - if (cursorX > innerRight) - cursorX = innerRight; - } - - int cursorTop, cursorH; - if (screenHeight <= 64) { - cursorH = 10; - cursorTop = boxY + (boxHeight - cursorH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - cursorTop = boxY + 2; - cursorH = boxHeight - 4; - if (cursorH < 1) - cursorH = 1; - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - - if (cursorX < innerLeft || cursorX > innerRight) - return; - } - - display->drawVerticalLine(cursorX, cursorTop, cursorH); + if (scrolledUp) { + // Draw three small dots centered horizontally, vertically at the midpoint of the gap + // between the inner top and the first line's top baseline. This avoids using a tall glyph. + const int firstLineTop = lineY; // baseline top for the first visible line + const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested + const int centerX = boxX + boxWidth / 2; + const int dotSpacing = 3; // px between dots + const int dotSize = 1; // small square dot + display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); + display->fillRect(centerX, gapMidY, dotSize, dotSize); + display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); } + + // How many lines fit with our top inset and tighter step + const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); + const int linesToShow = std::min((int)lines.size(), linesCapacity); + const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; + + for (int i = 0; i < linesToShow; ++i) { + const std::string &chunk = lines[startIndex + i]; + display->drawString(textX, lineY, chunk.c_str()); + caretX = textX + display->getStringWidth(chunk.c_str()); + caretY = lineY; + lineY += lineStep; + } + + // Draw caret at end of the last visible line + int caretPadY = 2; + if (boxHeight >= inputLineH + 4) + caretPadY = 3; + int cursorTop = caretY + caretPadY; + // Use lineStep so caret height matches the row spacing + int cursorH = lineStep - caretPadY * 2; + if (cursorH < 1) + cursorH = 1; + // Clamp vertical bounds to stay inside the inner rect + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + // Only draw if cursor is inside inner bounds + if (caretX >= innerLeft && caretX <= innerRight) { + display->drawVerticalLine(caretX, cursorTop, cursorH); + } + } else { + std::string displayText = inputText; + int textW = display->getStringWidth(displayText.c_str()); + std::string scrolled = displayText; + if (textW > maxTextWidth) { + // Trim from the left until it fits + while (textW > maxTextWidth && !scrolled.empty()) { + scrolled.erase(0, 1); + textW = display->getStringWidth(scrolled.c_str()); + } + // Add leading ellipsis and ensure it still fits + if (scrolled != displayText) { + scrolled = "..." + scrolled; + textW = display->getStringWidth(scrolled.c_str()); + // If adding ellipsis causes overflow, trim more after the ellipsis + while (textW > maxTextWidth && scrolled.size() > 3) { + scrolled.erase(3, 1); // remove chars after the ellipsis + textW = display->getStringWidth(scrolled.c_str()); + } + } + } else { + // Keep textW in sync with what we draw + textW = display->getStringWidth(scrolled.c_str()); + } + + int textY; + if (screenHeight <= 64) { + textY = boxY + (boxHeight - inputLineH) / 2; + } else { + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Center text vertically within inner box for single-line, then clamp so it never overlaps borders + int innerH = innerBottom - innerTop + 1; + textY = innerTop + std::max(0, (innerH - inputLineH) / 2); + // Clamp fully inside the inner rect + if (textY < innerTop) + textY = innerTop; + int maxTop = innerBottom - inputLineH + 1; + if (textY > maxTop) + textY = maxTop; + } + + if (!scrolled.empty()) { + display->drawString(textX, textY, scrolled.c_str()); + } + + int cursorX = textX + textW; + if (screenHeight > 64) { + const int innerRight = boxX + boxWidth - 2; + if (cursorX > innerRight) + cursorX = innerRight; + } + + int cursorTop, cursorH; + if (screenHeight <= 64) { + cursorH = 10; + cursorTop = boxY + (boxHeight - cursorH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + cursorTop = boxY + 2; + cursorH = boxHeight - 4; + if (cursorH < 1) + cursorH = 1; + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + + if (cursorX < innerLeft || cursorX > innerRight) + return; + } + + display->drawVerticalLine(cursorX, cursorTop, cursorH); + } } -void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, - uint8_t height, bool isLastCol) -{ - // Draw key content - display->setFont(FONT_SMALL); - const int fontH = FONT_HEIGHT_SMALL; - // Build label and metrics first - std::string keyText; - if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { - // Keep literal text labels for the action keys on the rightmost column - keyText = (key.type == VK_BACKSPACE) ? "BACK" - : (key.type == VK_ENTER) ? "ENTER" - : (key.type == VK_SPACE) ? "SPACE" - : (key.type == VK_ESC) ? "ESC" - : ""; +void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, uint8_t height, + bool isLastCol) { + // Draw key content + display->setFont(FONT_SMALL); + const int fontH = FONT_HEIGHT_SMALL; + // Build label and metrics first + std::string keyText; + if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { + // Keep literal text labels for the action keys on the rightmost column + keyText = (key.type == VK_BACKSPACE) ? "BACK" + : (key.type == VK_ENTER) ? "ENTER" + : (key.type == VK_SPACE) ? "SPACE" + : (key.type == VK_ESC) ? "ESC" + : ""; + } else { + char c = getCharForKey(key, false); + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + } + + int textWidth = display->getStringWidth(keyText.c_str()); + // Label alignment + // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. + // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. + int textX; + if (isLastCol) { + const int rightPad = 1; + textX = x + width - textWidth - rightPad; + if (textX < x) + textX = x; // guard + } else { + if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { + textX = x + (width - textWidth + 1) / 2; } else { - char c = getCharForKey(key, false); - if (c >= 'a' && c <= 'z') { - c = c - 'a' + 'A'; - } - keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + textX = x + (width - textWidth) / 2; } + } + int contentTop = y; + int contentH = height; + if (selected) { + display->setColor(WHITE); + bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); - int textWidth = display->getStringWidth(keyText.c_str()); - // Label alignment - // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. - // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. - int textX; - if (isLastCol) { - const int rightPad = 1; - textX = x + width - textWidth - rightPad; - if (textX < x) - textX = x; // guard + if (display->getHeight() <= 64 && !isAction) { + display->fillRect(x, y, width, height); + } else if (isAction) { + const int padX = 1; + const int padY = 2; + int hlW = textWidth + padX * 2; + int hlX = textX - padX; + + if (hlX < x) { + hlW -= (x - hlX); + hlX = x; + } + int maxW = (x + width) - hlX; + if (hlW > maxW) + hlW = maxW; + if (hlW < 1) + hlW = 1; + + int hlH = std::min(fontH + padY * 2, (int)height); + int hlY = y + (height - hlH) / 2; + display->fillRect(hlX, hlY, hlW, hlH); + contentTop = hlY; + contentH = hlH; } else { - if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { - textX = x + (width - textWidth + 1) / 2; - } else { - textX = x + (width - textWidth) / 2; - } + display->fillRect(x, y, width, height); } - int contentTop = y; - int contentH = height; - if (selected) { - display->setColor(WHITE); - bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); + display->setColor(BLACK); + } else { + display->setColor(WHITE); + } - if (display->getHeight() <= 64 && !isAction) { - display->fillRect(x, y, width, height); - } else if (isAction) { - const int padX = 1; - const int padY = 2; - int hlW = textWidth + padX * 2; - int hlX = textX - padX; + int centeredTextY; + if (display->getHeight() <= 64) { + centeredTextY = y + (height - fontH) / 2; + } else { + centeredTextY = contentTop + (contentH - fontH) / 2; + } + if (display->getHeight() > 64) { + if (centeredTextY < contentTop) + centeredTextY = contentTop; + if (centeredTextY + fontH > contentTop + contentH) + centeredTextY = std::max(contentTop, contentTop + contentH - fontH); + } - if (hlX < x) { - hlW -= (x - hlX); - hlX = x; - } - int maxW = (x + width) - hlX; - if (hlW > maxW) - hlW = maxW; - if (hlW < 1) - hlW = 1; - - int hlH = std::min(fontH + padY * 2, (int)height); - int hlY = y + (height - hlH) / 2; - display->fillRect(hlX, hlY, hlW, hlH); - contentTop = hlY; - contentH = hlH; - } else { - display->fillRect(x, y, width, height); - } - display->setColor(BLACK); - } else { - display->setColor(WHITE); - } - - int centeredTextY; - if (display->getHeight() <= 64) { - centeredTextY = y + (height - fontH) / 2; - } else { - centeredTextY = contentTop + (contentH - fontH) / 2; - } - if (display->getHeight() > 64) { - if (centeredTextY < contentTop) - centeredTextY = contentTop; - if (centeredTextY + fontH > contentTop + contentH) - centeredTextY = std::max(contentTop, contentTop + contentH - fontH); - } - - if (display->getHeight() <= 64 && keyText.size() == 1) { - char ch = keyText[0]; - if (ch == '.' || ch == ',' || ch == ';') { - centeredTextY -= 1; - } + if (display->getHeight() <= 64 && keyText.size() == 1) { + char ch = keyText[0]; + if (ch == '.' || ch == ',' || ch == ';') { + centeredTextY -= 1; } + } #ifdef MUZI_BASE // Correct issue with character vertical position on MUZI_BASE - centeredTextY -= 2; + centeredTextY -= 2; #endif - display->drawString(textX, centeredTextY, keyText.c_str()); + display->drawString(textX, centeredTextY, keyText.c_str()); } -char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) -{ - if (key.type != VK_CHAR) { - return key.character; - } +char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) { + if (key.type != VK_CHAR) { + return key.character; + } - char c = key.character; + char c = key.character; - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); - } + // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings + if (isLongPress && c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } - return c; + return c; } -void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) -{ - resetTimeout(); - // wrap around rows and cols in the 4x11 grid - int r = (int)cursorRow + dRow; - int c = (int)cursorCol + dCol; - if (r < 0) - r = KEYBOARD_ROWS - 1; - else if (r >= KEYBOARD_ROWS) - r = 0; - if (c < 0) - c = KEYBOARD_COLS - 1; - else if (c >= KEYBOARD_COLS) - c = 0; - cursorRow = (uint8_t)r; - cursorCol = (uint8_t)c; +void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) { + resetTimeout(); + // wrap around rows and cols in the 4x11 grid + int r = (int)cursorRow + dRow; + int c = (int)cursorCol + dCol; + if (r < 0) + r = KEYBOARD_ROWS - 1; + else if (r >= KEYBOARD_ROWS) + r = 0; + if (c < 0) + c = KEYBOARD_COLS - 1; + else if (c >= KEYBOARD_COLS) + c = 0; + cursorRow = (uint8_t)r; + cursorCol = (uint8_t)c; } -void VirtualKeyboard::moveCursorUp() -{ - moveCursorDelta(-1, 0); -} -void VirtualKeyboard::moveCursorDown() -{ - moveCursorDelta(1, 0); -} -void VirtualKeyboard::moveCursorLeft() -{ - resetTimeout(); +void VirtualKeyboard::moveCursorUp() { moveCursorDelta(-1, 0); } +void VirtualKeyboard::moveCursorDown() { moveCursorDelta(1, 0); } +void VirtualKeyboard::moveCursorLeft() { + resetTimeout(); - if (cursorCol > 0) { - cursorCol--; + if (cursorCol > 0) { + cursorCol--; + } else { + if (cursorRow > 0) { + cursorRow--; + cursorCol = KEYBOARD_COLS - 1; } else { - if (cursorRow > 0) { - cursorRow--; - cursorCol = KEYBOARD_COLS - 1; - } else { - cursorRow = KEYBOARD_ROWS - 1; - cursorCol = KEYBOARD_COLS - 1; - } + cursorRow = KEYBOARD_ROWS - 1; + cursorCol = KEYBOARD_COLS - 1; } + } } -void VirtualKeyboard::moveCursorRight() -{ - resetTimeout(); +void VirtualKeyboard::moveCursorRight() { + resetTimeout(); - if (cursorCol < KEYBOARD_COLS - 1) { - cursorCol++; + if (cursorCol < KEYBOARD_COLS - 1) { + cursorCol++; + } else { + if (cursorRow < KEYBOARD_ROWS - 1) { + cursorRow++; + cursorCol = 0; } else { - if (cursorRow < KEYBOARD_ROWS - 1) { - cursorRow++; - cursorCol = 0; - } else { - cursorRow = 0; - cursorCol = 0; - } + cursorRow = 0; + cursorCol = 0; } + } } -void VirtualKeyboard::handlePress() -{ - resetTimeout(); // Reset timeout on any input activity +void VirtualKeyboard::handlePress() { + resetTimeout(); // Reset timeout on any input activity - const VirtualKey &key = keyboard[cursorRow][cursorCol]; + const VirtualKey &key = keyboard[cursorRow][cursorCol]; - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert lowercase character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char + return; + } + + // Handle non-character keys immediately + switch (key.type) { + case VK_BACKSPACE: + deleteCharacter(); + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + std::function callback = onTextEntered; + onTextEntered = nullptr; + inputText = ""; + callback(""); } + return; + default: + break; + } +} - // For character keys, insert lowercase character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char - return; - } +void VirtualKeyboard::handleLongPress() { + resetTimeout(); // Reset timeout on any input activity - // Handle non-character keys immediately - switch (key.type) { - case VK_BACKSPACE: - deleteCharacter(); - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - std::function callback = onTextEntered; - onTextEntered = nullptr; - inputText = ""; - callback(""); - } - return; - default: + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert uppercase/alternate character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char + return; + } + + switch (key.type) { + case VK_BACKSPACE: + // One-shot: delete up to 5 characters on long press + for (int i = 0; i < 5; ++i) { + if (inputText.empty()) break; + deleteCharacter(); } -} - -void VirtualKeyboard::handleLongPress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + onTextEntered(""); } + break; + default: + break; + } +} - // For character keys, insert uppercase/alternate character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char - return; - } - - switch (key.type) { - case VK_BACKSPACE: - // One-shot: delete up to 5 characters on long press - for (int i = 0; i < 5; ++i) { - if (inputText.empty()) - break; - deleteCharacter(); - } - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - onTextEntered(""); - } - break; - default: - break; +void VirtualKeyboard::insertCharacter(char c) { + if (inputText.length() < 160) { // Reasonable text length limit + inputText += c; + } +} + +void VirtualKeyboard::deleteCharacter() { + if (!inputText.empty()) { + inputText.pop_back(); + } +} + +void VirtualKeyboard::submitText() { + LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); + + // Only submit if text is not empty + if (!inputText.empty() && onTextEntered) { + // Store callback and text to submit before clearing callback + std::function callback = onTextEntered; + std::string textToSubmit = inputText; + onTextEntered = nullptr; + // Don't clear inputText here - let the calling module handle cleanup + // inputText = ""; // Removed: keep text visible until module cleans up + callback(textToSubmit); + } else if (inputText.empty()) { + // For empty text, just ignore the submission - don't clear callback + // This keeps the virtual keyboard responsive for further input + LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); + } else { + // No callback available + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } + } } -void VirtualKeyboard::insertCharacter(char c) -{ - if (inputText.length() < 160) { // Reasonable text length limit - inputText += c; - } -} +void VirtualKeyboard::setInputText(const std::string &text) { inputText = text; } -void VirtualKeyboard::deleteCharacter() -{ - if (!inputText.empty()) { - inputText.pop_back(); - } -} +std::string VirtualKeyboard::getInputText() const { return inputText; } -void VirtualKeyboard::submitText() -{ - LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); +void VirtualKeyboard::setHeader(const std::string &header) { headerText = header; } - // Only submit if text is not empty - if (!inputText.empty() && onTextEntered) { - // Store callback and text to submit before clearing callback - std::function callback = onTextEntered; - std::string textToSubmit = inputText; - onTextEntered = nullptr; - // Don't clear inputText here - let the calling module handle cleanup - // inputText = ""; // Removed: keep text visible until module cleans up - callback(textToSubmit); - } else if (inputText.empty()) { - // For empty text, just ignore the submission - don't clear callback - // This keeps the virtual keyboard responsive for further input - LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); - } else { - // No callback available - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } -} +void VirtualKeyboard::setCallback(std::function callback) { onTextEntered = callback; } -void VirtualKeyboard::setInputText(const std::string &text) -{ - inputText = text; -} +void VirtualKeyboard::resetTimeout() { lastActivityTime = millis(); } -std::string VirtualKeyboard::getInputText() const -{ - return inputText; -} - -void VirtualKeyboard::setHeader(const std::string &header) -{ - headerText = header; -} - -void VirtualKeyboard::setCallback(std::function callback) -{ - onTextEntered = callback; -} - -void VirtualKeyboard::resetTimeout() -{ - lastActivityTime = millis(); -} - -bool VirtualKeyboard::isTimedOut() const -{ - return (millis() - lastActivityTime) > TIMEOUT_MS; -} +bool VirtualKeyboard::isTimedOut() const { return (millis() - lastActivityTime) > TIMEOUT_MS; } } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h index 169163b57..6c6c80ff7 100644 --- a/src/graphics/VirtualKeyboard.h +++ b/src/graphics/VirtualKeyboard.h @@ -5,76 +5,73 @@ #include #include -namespace graphics -{ +namespace graphics { enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; struct VirtualKey { - char character; - VirtualKeyType type; - uint8_t x; - uint8_t y; - uint8_t width; - uint8_t height; + char character; + VirtualKeyType type; + uint8_t x; + uint8_t y; + uint8_t width; + uint8_t height; }; -class VirtualKeyboard -{ - public: - VirtualKeyboard(); - ~VirtualKeyboard(); +class VirtualKeyboard { +public: + VirtualKeyboard(); + ~VirtualKeyboard(); - void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); - void setInputText(const std::string &text); - std::string getInputText() const; - void setHeader(const std::string &header); - void setCallback(std::function callback); + void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); + void setInputText(const std::string &text); + std::string getInputText() const; + void setHeader(const std::string &header); + void setCallback(std::function callback); - // Navigation methods for encoder input - void moveCursorUp(); - void moveCursorDown(); - void moveCursorLeft(); - void moveCursorRight(); - void handlePress(); - void handleLongPress(); + // Navigation methods for encoder input + void moveCursorUp(); + void moveCursorDown(); + void moveCursorLeft(); + void moveCursorRight(); + void handlePress(); + void handleLongPress(); - // Timeout management - void resetTimeout(); - bool isTimedOut() const; + // Timeout management + void resetTimeout(); + bool isTimedOut() const; - private: - static const uint8_t KEYBOARD_ROWS = 4; - static const uint8_t KEYBOARD_COLS = 11; - static const uint8_t KEY_WIDTH = 9; - static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays - static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom +private: + static const uint8_t KEYBOARD_ROWS = 4; + static const uint8_t KEYBOARD_COLS = 11; + static const uint8_t KEY_WIDTH = 9; + static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays + static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom - VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; + VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; - std::string inputText; - std::string headerText; - std::function onTextEntered; + std::string inputText; + std::string headerText; + std::function onTextEntered; - uint8_t cursorRow; - uint8_t cursorCol; + uint8_t cursorRow; + uint8_t cursorCol; - // Timeout management for auto-exit - uint32_t lastActivityTime; - static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout + // Timeout management for auto-exit + uint32_t lastActivityTime; + static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout - void initializeKeyboard(); - void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, - bool isLastCol); - void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); + void initializeKeyboard(); + void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, bool isLastCol); + void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); - // Unified cursor movement helper - void moveCursorDelta(int dRow, int dCol); + // Unified cursor movement helper + void moveCursorDelta(int dRow, int dCol); - char getCharForKey(const VirtualKey &key, bool isLongPress = false); - void insertCharacter(char c); - void deleteCharacter(); - void submitText(); + char getCharForKey(const VirtualKey &key, bool isLongPress = false); + void insertCharacter(char c); + void deleteCharacter(); + void submitText(); }; } // namespace graphics diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 66bbe1bfe..617a31fc4 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -12,11 +12,9 @@ #include "nimble/NimbleBluetooth.h" #endif -namespace graphics -{ +namespace graphics { -namespace ClockRenderer -{ +namespace ClockRenderer { // Segment bitmaps for numerals 0-9 stored in flash to save RAM. // Each row is a digit, each column is a segment state (1 = on, 0 = off). @@ -43,436 +41,426 @@ static const uint8_t PROGMEM digitSegments[10][7] = { {1, 1, 1, 1, 0, 1, 1} // 9 }; -void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; + uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - uint16_t topAndBottomX = x + static_cast(4 * scale); + uint16_t topAndBottomX = x + static_cast(4 * scale); - uint16_t quarterCellHeight = cellHeight / 4; + uint16_t quarterCellHeight = cellHeight / 4; - uint16_t topY = y + quarterCellHeight; - uint16_t bottomY = y + (quarterCellHeight * 3); + uint16_t topY = y + quarterCellHeight; + uint16_t bottomY = y + (quarterCellHeight * 3); - display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); - display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); } -void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) -{ - // Read 7-segment pattern for the digit from flash - uint8_t seg[7]; - for (uint8_t i = 0; i < 7; i++) { - seg[i] = pgm_read_byte(&digitSegments[number][i]); +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) { + // Read 7-segment pattern for the digit from flash + uint8_t seg[7]; + for (uint8_t i = 0; i < 7; i++) { + seg[i] = pgm_read_byte(&digitSegments[number][i]); + } + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // Precompute segment positions + uint16_t segmentOneX = x + segmentHeight + 2; + uint16_t segmentOneY = y; + + uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; + uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; + + uint16_t segmentThreeX = segmentTwoX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; + + uint16_t segmentFourX = segmentOneX; + uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; + + uint16_t segmentFiveX = x; + uint16_t segmentFiveY = segmentThreeY; + + uint16_t segmentSixX = x; + uint16_t segmentSixY = segmentTwoY; + + uint16_t segmentSevenX = segmentOneX; + uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; + + // Draw only the active segments + if (seg[0]) + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + if (seg[1]) + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + if (seg[2]) + drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + if (seg[3]) + drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + if (seg[4]) + drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + if (seg[5]) + drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + if (seg[6]) + drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); +} + +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) { + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, width, height); + + // draw end triangles + display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); + + display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); +} + +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) { + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, height, width); + + // draw end triangles + display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); + + display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); +} + +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true, true); + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + char timeString[16]; + int hour = 0; + int minute = 0; + int second = 0; + + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + } + + bool isPM = hour >= 12; + if (config.display.use_12h_clock) { + hour %= 12; + if (hour == 0) { + hour = 12; + } + snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); + } else { + snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); + } + + // Format seconds string + char secondString[8]; + snprintf(secondString, sizeof(secondString), "%02d", second); + + static bool scaleInitialized = false; + static float scale = 0.75f; + static float segmentWidth = SEGMENT_WIDTH * 0.75f; + static float segmentHeight = SEGMENT_HEIGHT * 0.75f; + + if (!scaleInitialized) { + float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable) + float max_scale = 3.5f; // Safety limit to avoid runaway scaling + float step = 0.05f; // Step increment per iteration + + float target_width = display->getWidth() * screenwidth_target_ratio; + float target_height = + display->getHeight() - ((currentResolution == ScreenResolution::High) + ? 46 + : 33); // Be careful adjusting this number, we have to account for header and the text under the time + + float calculated_width_size = 0.0f; + float calculated_height_size = 0.0f; + + while (true) { + segmentWidth = SEGMENT_WIDTH * scale; + segmentHeight = SEGMENT_HEIGHT * scale; + + calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4); + calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2); + + if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) { + break; + } + + scale += step; } - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // Precompute segment positions - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentTwoX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; - - uint16_t segmentFourX = segmentOneX; - uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; - - uint16_t segmentFiveX = x; - uint16_t segmentFiveY = segmentThreeY; - - uint16_t segmentSixX = x; - uint16_t segmentSixY = segmentTwoY; - - uint16_t segmentSevenX = segmentOneX; - uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; - - // Draw only the active segments - if (seg[0]) - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - if (seg[1]) - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - if (seg[2]) - drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - if (seg[3]) - drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - if (seg[4]) - drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); - if (seg[5]) - drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); - if (seg[6]) - drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); -} - -void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, width, height); - - // draw end triangles - display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); - - display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); -} - -void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, height, width); - - // draw end triangles - display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); - - display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); -} - -void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Set Title, Blank for Clock - const char *titleStr = ""; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true, true); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - char timeString[16]; - int hour = 0; - int minute = 0; - int second = 0; - - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - hour = hms / SEC_PER_HOUR; - minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + // If we overshot width, back off one step and recompute segment sizes + if (calculated_width_size > target_width || calculated_height_size > target_height) { + scale -= step; + segmentWidth = SEGMENT_WIDTH * scale; + segmentHeight = SEGMENT_HEIGHT * scale; } - bool isPM = hour >= 12; - if (config.display.use_12h_clock) { - hour %= 12; - if (hour == 0) { - hour = 12; - } - snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); + scaleInitialized = true; + } + + // calculate hours:minutes string width + size_t len = strlen(timeString); + uint16_t timeStringWidth = len * 5; + + for (size_t i = 0; i < len; i++) { + char character = timeString[i]; + + if (character == ':') { + timeStringWidth += segmentHeight; } else { - snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); + timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; + } + } + + uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); + uint16_t startingHourMinuteTextX = hourMinuteTextX; + + uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2; + + // iterate over characters in hours:minutes string and draw segmented characters + for (size_t i = 0; i < len; i++) { + char character = timeString[i]; + + if (character == ':') { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + if (scale >= 2.0f) { + hourMinuteTextX += (uint16_t)(4.5f * scale); + } + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); + + hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; } - // Format seconds string - char secondString[8]; - snprintf(secondString, sizeof(secondString), "%02d", second); + hourMinuteTextX += 5; + } - static bool scaleInitialized = false; - static float scale = 0.75f; - static float segmentWidth = SEGMENT_WIDTH * 0.75f; - static float segmentHeight = SEGMENT_HEIGHT * 0.75f; - - if (!scaleInitialized) { - float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable) - float max_scale = 3.5f; // Safety limit to avoid runaway scaling - float step = 0.05f; // Step increment per iteration - - float target_width = display->getWidth() * screenwidth_target_ratio; - float target_height = - display->getHeight() - - ((currentResolution == ScreenResolution::High) - ? 46 - : 33); // Be careful adjusting this number, we have to account for header and the text under the time - - float calculated_width_size = 0.0f; - float calculated_height_size = 0.0f; - - while (true) { - segmentWidth = SEGMENT_WIDTH * scale; - segmentHeight = SEGMENT_HEIGHT * scale; - - calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4); - calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2); - - if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) { - break; - } - - scale += step; - } - - // If we overshot width, back off one step and recompute segment sizes - if (calculated_width_size > target_width || calculated_height_size > target_height) { - scale -= step; - segmentWidth = SEGMENT_WIDTH * scale; - segmentHeight = SEGMENT_HEIGHT * scale; - } - - scaleInitialized = true; - } - - // calculate hours:minutes string width - size_t len = strlen(timeString); - uint16_t timeStringWidth = len * 5; - - for (size_t i = 0; i < len; i++) { - char character = timeString[i]; - - if (character == ':') { - timeStringWidth += segmentHeight; - } else { - timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; - } - } - - uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); - uint16_t startingHourMinuteTextX = hourMinuteTextX; - - uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2; - - // iterate over characters in hours:minutes string and draw segmented characters - for (size_t i = 0; i < len; i++) { - char character = timeString[i]; - - if (character == ':') { - drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); - - hourMinuteTextX += segmentHeight + 6; - if (scale >= 2.0f) { - hourMinuteTextX += (uint16_t)(4.5f * scale); - } - } else { - drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); - - hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; - } - - hourMinuteTextX += 5; - } - - // draw seconds string + AM/PM - display->setFont(FONT_SMALL); - int xOffset = -1; + // draw seconds string + AM/PM + display->setFont(FONT_SMALL); + int xOffset = -1; + if (currentResolution == ScreenResolution::High) { + xOffset = 0; + } + if (hour >= 10) { if (currentResolution == ScreenResolution::High) { - xOffset = 0; - } - if (hour >= 10) { - if (currentResolution == ScreenResolution::High) { - xOffset += 32; - } else { - xOffset += 18; - } + xOffset += 32; + } else { + xOffset += 18; } + } - if (config.display.use_12h_clock) { - display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am"); - } + if (config.display.use_12h_clock) { + display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am"); + } #ifndef USE_EINK - xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; - if (scale >= 2.0f) { - xOffset -= (int)(4.5f * scale); - } - display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1, - secondString); + xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; + if (scale >= 2.0f) { + xOffset -= (int)(4.5f * scale); + } + display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1, secondString); #endif - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // Draw an analog clock -void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - // === Set Title, Blank for Clock - const char *titleStr = ""; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true, true); +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true, true); - // clock face center coordinates - int16_t centerX = display->getWidth() / 2; - int16_t centerY = display->getHeight() / 2; + // clock face center coordinates + int16_t centerX = display->getWidth() / 2; + int16_t centerY = display->getHeight() / 2; - // clock face radius - int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; + // clock face radius + int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; #ifdef T_WATCH_S3 - radius = (display->getWidth() / 2) * 0.8; + radius = (display->getWidth() / 2) * 0.8; #endif - // noon (0 deg) coordinates (outermost circle) - int16_t noonX = centerX; - int16_t noonY = centerY - radius; + // noon (0 deg) coordinates (outermost circle) + int16_t noonX = centerX; + int16_t noonY = centerY - radius; - // second hand radius and y coordinate (outermost circle) - int16_t secondHandNoonY = noonY + 1; + // second hand radius and y coordinate (outermost circle) + int16_t secondHandNoonY = noonY + 1; - // tick mark outer y coordinate; (first nested circle) - int16_t tickMarkOuterNoonY = secondHandNoonY; + // tick mark outer y coordinate; (first nested circle) + int16_t tickMarkOuterNoonY = secondHandNoonY; - double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); - double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); + double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); + double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); - // minute hand y coordinate - int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; + // minute hand y coordinate + int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; - // hour string y coordinate - int16_t hourStringNoonY = minuteHandNoonY + 18; + // hour string y coordinate + int16_t hourStringNoonY = minuteHandNoonY + 18; - // hour hand radius and y coordinate - int16_t hourHandRadius = radius * 0.35; - if (currentResolution == ScreenResolution::High) { - hourHandRadius = radius * 0.55; + // hour hand radius and y coordinate + int16_t hourHandRadius = radius * 0.35; + if (currentResolution == ScreenResolution::High) { + hourHandRadius = radius * 0.55; + } + int16_t hourHandNoonY = centerY - hourHandRadius; + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + display->drawCircle(centerX, centerY, radius); + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + int hour, minute, second; + decomposeTime(rtc_sec, hour, minute, second); + + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + display->setFont(FONT_SMALL); + int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; +#ifdef USE_EINK + yOffset += 3; +#endif + display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, isPM ? "pm" : "am"); } - int16_t hourHandNoonY = centerY - hourHandRadius; + hour %= 12; + if (hour == 0) + hour = 12; - display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawCircle(centerX, centerY, radius); + int16_t degreesPerHour = 30; + int16_t degreesPerMinuteOrSecond = 6; - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - int hour, minute, second; - decomposeTime(rtc_sec, hour, minute, second); + double hourBaseAngle = hour * degreesPerHour; + double hourAngleOffset = ((double)minute / 60) * degreesPerHour; + double hourAngle = radians(hourBaseAngle + hourAngleOffset); - if (config.display.use_12h_clock) { - bool isPM = hour >= 12; - display->setFont(FONT_SMALL); - int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; -#ifdef USE_EINK - yOffset += 3; -#endif - display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, - isPM ? "pm" : "am"); + double minuteBaseAngle = minute * degreesPerMinuteOrSecond; + double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; + double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); + + double secondAngle = radians(second * degreesPerMinuteOrSecond); + + double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; + double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; + + double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; + double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; + + double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; + double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; + + display->setFont(FONT_MEDIUM); + + // draw minute and hour tick marks and hour numbers + for (uint16_t angle = 0; angle < 360; angle += 6) { + double angleInRadians = radians(angle); + + double sineAngleInRadians = sin(-angleInRadians); + double cosineAngleInRadians = cos(-angleInRadians); + + double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; + double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; + + if (angle % degreesPerHour == 0) { + double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; + + // draw hour tick mark + display->drawLine(startX, startY, endX, endY); + + static char buffer[2]; + + uint8_t hourInt = (angle / 30); + + if (hourInt == 0) { + hourInt = 12; } - hour %= 12; - if (hour == 0) - hour = 12; - int16_t degreesPerHour = 30; - int16_t degreesPerMinuteOrSecond = 6; + // hour number x offset needs to be adjusted for some cases + int8_t hourStringXOffset; + int8_t hourStringYOffset = 13; - double hourBaseAngle = hour * degreesPerHour; - double hourAngleOffset = ((double)minute / 60) * degreesPerHour; - double hourAngle = radians(hourBaseAngle + hourAngleOffset); + switch (hourInt) { + case 3: + hourStringXOffset = 5; + break; + case 9: + hourStringXOffset = 7; + break; + case 10: + case 11: + hourStringXOffset = 8; + break; + case 12: + hourStringXOffset = 13; + break; + default: + hourStringXOffset = 6; + break; + } - double minuteBaseAngle = minute * degreesPerMinuteOrSecond; - double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; - double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); - - double secondAngle = radians(second * degreesPerMinuteOrSecond); - - double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; - double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; - - double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; - double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; - - double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; - double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; - - display->setFont(FONT_MEDIUM); - - // draw minute and hour tick marks and hour numbers - for (uint16_t angle = 0; angle < 360; angle += 6) { - double angleInRadians = radians(angle); - - double sineAngleInRadians = sin(-angleInRadians); - double cosineAngleInRadians = cos(-angleInRadians); - - double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; - double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; - - if (angle % degreesPerHour == 0) { - double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; - - // draw hour tick mark - display->drawLine(startX, startY, endX, endY); - - static char buffer[2]; - - uint8_t hourInt = (angle / 30); - - if (hourInt == 0) { - hourInt = 12; - } - - // hour number x offset needs to be adjusted for some cases - int8_t hourStringXOffset; - int8_t hourStringYOffset = 13; - - switch (hourInt) { - case 3: - hourStringXOffset = 5; - break; - case 9: - hourStringXOffset = 7; - break; - case 10: - case 11: - hourStringXOffset = 8; - break; - case 12: - hourStringXOffset = 13; - break; - default: - hourStringXOffset = 6; - break; - } - - double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; - double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; + double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; + double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; #ifdef T_WATCH_S3 - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); #else #ifdef USE_EINK - if (currentResolution == ScreenResolution::High) { - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } -#else - if (currentResolution == ScreenResolution::High && - (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } -#endif -#endif - } - - if (angle % degreesPerMinuteOrSecond == 0) { - double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - - if (currentResolution == ScreenResolution::High) { - // draw minute tick mark - display->drawLine(startX, startY, endX, endY); - } - } + if (currentResolution == ScreenResolution::High) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } +#else + if (currentResolution == ScreenResolution::High && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } +#endif +#endif + } - // draw hour hand - display->drawLine(centerX, centerY, hourX, hourY); + if (angle % degreesPerMinuteOrSecond == 0) { + double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - // draw minute hand - display->drawLine(centerX, centerY, minuteX, minuteY); + if (currentResolution == ScreenResolution::High) { + // draw minute tick mark + display->drawLine(startX, startY, endX, endY); + } + } + } + + // draw hour hand + display->drawLine(centerX, centerY, hourX, hourY); + + // draw minute hand + display->drawLine(centerX, centerY, minuteX, minuteY); #ifndef USE_EINK - // draw second hand - display->drawLine(centerX, centerY, secondX, secondY); + // draw second hand + display->drawLine(centerX, centerY, secondX, secondY); #endif - } - graphics::drawCommonFooter(display, x, y); + } + graphics::drawCommonFooter(display, x, y); } } // namespace ClockRenderer diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h index eace26cf5..e3354bc64 100644 --- a/src/graphics/draw/ClockRenderer.h +++ b/src/graphics/draw/ClockRenderer.h @@ -3,14 +3,12 @@ #include #include -namespace graphics -{ +namespace graphics { /// Forward declarations class Screen; -namespace ClockRenderer -{ +namespace ClockRenderer { // Clock frame functions void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 42600ce96..b5450b1f2 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -9,130 +9,120 @@ #include "graphics/SharedUIDisplay.h" #include -namespace graphics -{ -namespace CompassRenderer -{ +namespace graphics { +namespace CompassRenderer { // Point helper class for compass calculations struct Point { - float x, y; - Point(float x, float y) : x(x), y(y) {} + float x, y; + Point(float x, float y) : x(x), y(y) {} - void rotate(float angle) - { - float cos_a = cos(angle); - float sin_a = sin(angle); - float new_x = x * cos_a - y * sin_a; - float new_y = x * sin_a + y * cos_a; - x = new_x; - y = new_y; - } + void rotate(float angle) { + float cos_a = cos(angle); + float sin_a = sin(angle); + float new_x = x * cos_a - y * sin_a; + float new_y = x * sin_a + y * cos_a; + x = new_x; + y = new_y; + } - void scale(float factor) - { - x *= factor; - y *= factor; - } + void scale(float factor) { + x *= factor; + y *= factor; + } - void translate(float dx, float dy) - { - x += dx; - y += dy; - } + void translate(float dx, float dy) { + x += dx; + y += dy; + } }; -void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) -{ - // Show the compass heading (not implemented in original) - // This could draw a "N" indicator or north arrow - // For now, we'll draw a simple north indicator - // const float radius = 17.0f; - if (currentResolution == ScreenResolution::High) { - radius += 4; - } - Point north(0, -radius); - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - north.rotate(-myHeading); - north.translate(compassX, compassY); +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) { + // Show the compass heading (not implemented in original) + // This could draw a "N" indicator or north arrow + // For now, we'll draw a simple north indicator + // const float radius = 17.0f; + if (currentResolution == ScreenResolution::High) { + radius += 4; + } + Point north(0, -radius); + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + north.rotate(-myHeading); + north.translate(compassX, compassY); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setColor(BLACK); - if (currentResolution == ScreenResolution::High) { - display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); - } else { - display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); - } - display->setColor(WHITE); - display->drawString(north.x, north.y - 3, "N"); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setColor(BLACK); + if (currentResolution == ScreenResolution::High) { + display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + } else { + display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + } + display->setColor(WHITE); + display->drawString(north.x, north.y - 3, "N"); } -void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) -{ - Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; - Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { + Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially + float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; + Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); - Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; + Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; - for (int i = 0; i < 4; i++) { - arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(compassDiam * 0.6); - arrowPoints[i]->translate(compassX, compassY); - } + for (int i = 0; i < 4; i++) { + arrowPoints[i]->rotate(headingRadian); + arrowPoints[i]->scale(compassDiam * 0.6); + arrowPoints[i]->translate(compassX, compassY); + } #ifdef USE_EINK - display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); + display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #else - display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #endif - display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); + display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); } -void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) -{ - float radians = bearing * DEG_TO_RAD; +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) { + float radians = bearing * DEG_TO_RAD; - Point tip(0, -size / 2); - Point left(-size / 6, size / 4); - Point right(size / 6, size / 4); - Point tail(0, size / 4.5); + Point tip(0, -size / 2); + Point left(-size / 6, size / 4); + Point right(size / 6, size / 4); + Point tail(0, size / 4.5); - tip.rotate(radians); - left.rotate(radians); - right.rotate(radians); - tail.rotate(radians); + tip.rotate(radians); + left.rotate(radians); + right.rotate(radians); + tail.rotate(radians); - tip.translate(x, y); - left.translate(x, y); - right.translate(x, y); - tail.translate(x, y); + tip.translate(x, y); + left.translate(x, y); + right.translate(x, y); + tail.translate(x, y); - display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); - display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); } -float estimatedHeading(double lat, double lon) -{ - // Simple magnetic declination estimation - // This is a very basic implementation - the original might be more sophisticated - return 0.0f; // Return 0 for now, indicating no heading available +float estimatedHeading(double lat, double lon) { + // Simple magnetic declination estimation + // This is a very basic implementation - the original might be more sophisticated + return 0.0f; // Return 0 for now, indicating no heading available } -uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) -{ - // Calculate appropriate compass diameter based on display size - uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; - uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) { + // Calculate appropriate compass diameter based on display size + uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; + uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension - // Ensure minimum and maximum bounds - if (maxDiam < 16) - maxDiam = 16; - if (maxDiam > 64) - maxDiam = 64; + // Ensure minimum and maximum bounds + if (maxDiam < 16) + maxDiam = 16; + if (maxDiam > 64) + maxDiam = 64; - return maxDiam; + return maxDiam; } } // namespace CompassRenderer diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index ca7532b66..0e937cea1 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -5,8 +5,7 @@ #include #include -namespace graphics -{ +namespace graphics { /// Forward declarations class Screen; @@ -17,8 +16,7 @@ class Screen; * Contains all functions related to drawing compass elements, headings, * navigation arrows, and location-based UI components. */ -namespace CompassRenderer -{ +namespace CompassRenderer { // Compass drawing functions void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius); void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 75b65c65f..da0343c36 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -50,696 +50,667 @@ extern bool heartbeat; extern StoreForwardModule *storeForwardModule; #endif -namespace graphics -{ -namespace DebugRenderer -{ +namespace graphics { +namespace DebugRenderer { -void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setFont(FONT_SMALL); - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } - char channelStr[20]; - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - // Display nodes status - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); - } else { - UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); - } + char channelStr[20]; + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + // Display nodes status + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); + } else { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); + } #if HAS_GPS - // Display GPS status - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); + // Display GPS status + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); + } else { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); } else { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); - } else { - UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); - } + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); } + } #endif - display->setColor(WHITE); - // Draw the channel name - display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); - // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo - if (moduleConfig.store_forward.enabled) { + display->setColor(WHITE); + // Draw the channel name + display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); + // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo + if (moduleConfig.store_forward.enabled) { #ifdef ARCH_ESP32 - if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, - (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ + if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, + (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ + defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ + defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, - 8, imgQuestionL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, - 8, imgQuestionL2); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, - 8, imgQuestion); -#endif - } else { -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, - 8, imgSFL1); - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, - 8, imgSFL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, - 8, imgSF); -#endif - } + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgQuestion); #endif } else { - // TODO: Raspberry Pi supports more than just the one screen size -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ + defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ + defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, - imgInfoL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, - imgInfoL2); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, imgSFL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, - imgInfo); + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, imgSF); #endif } +#endif + } else { + // TODO: Raspberry Pi supports more than just the one screen size +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ + defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ + defined(USE_ST7796) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); +#endif + } - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); - // Draw any log messages - display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); + // Draw any log messages + display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; #endif } // **************************** // * WiFi Screen * // **************************** -void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #if HAS_WIFI && !defined(ARCH_PORTDUINO) - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "WiFi"; + // === Set Title + const char *titleStr = "WiFi"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - const char *wifiName = config.network.wifi_ssid; + const char *wifiName = config.network.wifi_ssid; - if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); - } else { - display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); + if (WiFi.status() != WL_CONNECTED) { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); + } else { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); - char rssiStr[32]; - snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); - display->drawString(x, getTextPositions(display)[line++], rssiStr); - } + char rssiStr[32]; + snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); + display->drawString(x, getTextPositions(display)[line++], rssiStr); + } - /* - - WL_CONNECTED: assigned when connected to a WiFi network; - - WL_NO_SSID_AVAIL: assigned when no SSID are available; - - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; - - WL_CONNECTION_LOST: assigned when the connection is lost; - - WL_DISCONNECTED: assigned when disconnected from a network; - - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of - attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); - - WL_SCAN_COMPLETED: assigned when the scan networks is completed; - - WL_NO_SHIELD: assigned when no WiFi shield is present; + /* + - WL_CONNECTED: assigned when connected to a WiFi network; + - WL_NO_SSID_AVAIL: assigned when no SSID are available; + - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; + - WL_CONNECTION_LOST: assigned when the connection is lost; + - WL_DISCONNECTED: assigned when disconnected from a network; + - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of + attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); + - WL_SCAN_COMPLETED: assigned when the scan networks is completed; + - WL_NO_SHIELD: assigned when no WiFi shield is present; - */ - if (WiFi.status() == WL_CONNECTED) { - char ipStr[64]; - snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); - display->drawString(x, getTextPositions(display)[line++], ipStr); - } else if (WiFi.status() == WL_NO_SSID_AVAIL) { - display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); - } else if (WiFi.status() == WL_CONNECTION_LOST) { - display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); - } else if (WiFi.status() == WL_IDLE_STATUS) { - display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); - } else if (WiFi.status() == WL_CONNECT_FAILED) { - display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); - } + */ + if (WiFi.status() == WL_CONNECTED) { + char ipStr[64]; + snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); + display->drawString(x, getTextPositions(display)[line++], ipStr); + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); + } #ifdef ARCH_ESP32 - else { - // Codes: - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - display->drawString(x, getTextPositions(display)[line++], - WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); - } + else { + // Codes: + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + display->drawString(x, getTextPositions(display)[line++], WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); + } #else - else { - char statusStr[32]; - snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); - display->drawString(x, getTextPositions(display)[line++], statusStr); - } + else { + char statusStr[32]; + snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); + display->drawString(x, getTextPositions(display)[line++], statusStr); + } #endif - char ssidStr[64]; - snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); - display->drawString(x, getTextPositions(display)[line++], ssidStr); + char ssidStr[64]; + snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); + display->drawString(x, getTextPositions(display)[line++], ssidStr); - display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); + display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; #endif #endif } -void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setFont(FONT_SMALL); - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } - char batStr[20]; - if (powerStatus->getHasBattery()) { - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), - powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); + snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), + powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); - // Line 1 - display->drawString(x, y, batStr); - if (config.display.heading_bold) - display->drawString(x + 1, y, batStr); + // Line 1 + display->drawString(x, y, batStr); + if (config.display.heading_bold) + display->drawString(x + 1, y, batStr); + } else { + // Line 1 + display->drawString(x, y, "USB"); + if (config.display.heading_bold) + display->drawString(x + 1, y, "USB"); + } + + uint32_t currentMillis = millis(); + uint32_t seconds = currentMillis / 1000; + uint32_t minutes = seconds / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + // currentMillis %= 1000; + // seconds %= 60; + // minutes %= 60; + // hours %= 24; + + // Show uptime as days, hours, minutes OR seconds + std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); + + // Line 1 (Still) + if (currentResolution != graphics::ScreenResolution::UltraLow) { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + + display->setColor(WHITE); + } + // Setup string to assemble analogClock string + std::string analogClock = ""; + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour, min, sec; + graphics::decomposeTime(rtc_sec, hour, min, sec); + + char timebuf[12]; + + if (config.display.use_12h_clock) { + std::string meridiem = "am"; + if (hour >= 12) { + if (hour > 12) + hour -= 12; + meridiem = "pm"; + } + if (hour == 00) { + hour = 12; + } + snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); } else { - // Line 1 - display->drawString(x, y, "USB"); - if (config.display.heading_bold) - display->drawString(x + 1, y, "USB"); + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); } + analogClock += timebuf; + } - uint32_t currentMillis = millis(); - uint32_t seconds = currentMillis / 1000; - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - // currentMillis %= 1000; - // seconds %= 60; - // minutes %= 60; - // hours %= 24; + // Line 2 + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); - // Show uptime as days, hours, minutes OR seconds - std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); - - // Line 1 (Still) - if (currentResolution != graphics::ScreenResolution::UltraLow) { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - if (config.display.heading_bold) - display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - - display->setColor(WHITE); - } - // Setup string to assemble analogClock string - std::string analogClock = ""; - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour, min, sec; - graphics::decomposeTime(rtc_sec, hour, min, sec); - - char timebuf[12]; - - if (config.display.use_12h_clock) { - std::string meridiem = "am"; - if (hour >= 12) { - if (hour > 12) - hour -= 12; - meridiem = "pm"; - } - if (hour == 00) { - hour = 12; - } - snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); - } else { - snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); - } - analogClock += timebuf; - } - - // Line 2 - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); - - // Display Channel Utilization - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); #if HAS_GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - // Line 3 - if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude - UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // Line 3 + if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - // Line 4 - UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); - } else { - UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - } + // Line 4 + UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); + } else { + UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + } #endif /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; #endif } // Trampoline functions for DebugInfo class access -void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawFrame(display, state, x, y); +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrame(display, state, x, y); } + +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawFrameSettings(display, state, x, y); } -void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawFrameSettings(display, state, x, y); -} - -void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawFrameWiFi(display, state, x, y); -} +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrameWiFi(display, state, x, y); } // **************************** // * LoRa Focused Screen * // **************************** -void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; + // === Set Title + const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === First Row: Region / BLE Name === - graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); + // === First Row: Region / BLE Name === + graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); - uint8_t dmac[6]; - char shortnameble[35]; - getMacAddr(dmac); - snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); + } else { + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); + } + int textWidth = display->getStringWidth(shortnameble); + int nameX = (SCREEN_WIDTH - textWidth); + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + + // === Second Row: Role === + auto role = DisplayFormatters::getDeviceRole(config.device.role); + char device_role[25]; + snprintf(device_role, sizeof(device_role), "Role: %s", role); + textWidth = display->getStringWidth(device_role); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], device_role); + + // === Third Row: Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + if (region != nullptr) { if (currentResolution == ScreenResolution::UltraLow) { - snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); } else { - snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); } - int textWidth = display->getStringWidth(shortnameble); - int nameX = (SCREEN_WIDTH - textWidth); - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + } + textWidth = display->getStringWidth(regionradiopreset); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); - // === Second Row: Role === - auto role = DisplayFormatters::getDeviceRole(config.device.role); - char device_role[25]; - snprintf(device_role, sizeof(device_role), "Role: %s", role); - textWidth = display->getStringWidth(device_role); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], device_role); - - // === Third Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); - - char regionradiopreset[25]; - const char *region = myRegion ? myRegion->name : NULL; - if (region != nullptr) { - if (currentResolution == ScreenResolution::UltraLow) { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); - } else { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); - } - } - textWidth = display->getStringWidth(regionradiopreset); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); - - // === Fourth Row: Frequency / ChanNum === - char frequencyslot[35]; - char freqStr[16]; - float freq = RadioLibInterface::instance->getFreq(); - snprintf(freqStr, sizeof(freqStr), "%.3f", freq); - if (config.lora.channel_num == 0) { - if (currentResolution == ScreenResolution::UltraLow) { - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); - } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); - } + // === Fourth Row: Frequency / ChanNum === + char frequencyslot[35]; + char freqStr[16]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqStr, sizeof(freqStr), "%.3f", freq); + if (config.lora.channel_num == 0) { + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); } else { - if (currentResolution == ScreenResolution::UltraLow) { - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); - } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); - } + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); } - size_t len = strlen(frequencyslot); - if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { - frequencyslot[len - 4] = '\0'; // Remove the last three characters + } else { + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); } - textWidth = display->getStringWidth(frequencyslot); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); + } + size_t len = strlen(frequencyslot); + if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { + frequencyslot[len - 4] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(frequencyslot); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); #if !defined(M5STACK_UNITC6L) - // === Fifth Row: Channel Utilization === - const char *chUtil = "ChUtil:"; - char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + // === Fifth Row: Channel Utilization === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 - : display->getStringWidth(chUtil) + 5; - int chUtil_y = getTextPositions(display)[line] + 3; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; - int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; - int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; - int chutil_percent = airTime->channelUtilizationPercent(); + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; + int chutil_percent = airTime->channelUtilizationPercent(); - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; - display->drawString(starting_position, getTextPositions(display)[line], chUtil); + display->drawString(starting_position, getTextPositions(display)[line], chUtil); - // Force 56% or higher to show a full 100% bar, text would still show related percent. - if (chutil_percent >= 61) { - chutil_percent = 100; - } + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0–25% - float weight2 = 0.35; // Weight for 25–40% - float weight3 = 0.20; // Weight for 40–100% - float totalWeight = weight1 + weight2 + weight3; + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); - int fillRight = 0; + int fillRight = 0; - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); - } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } - // Draw outline - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); - // Fill progress - if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); - } + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], - chUtilPercentage); + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], chUtilPercentage); #endif - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // **************************** // * System Screen * // **************************** -void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); - // === Set Title - const char *titleStr = "System"; + // === Set Title + const char *titleStr = "System"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === Layout === - int line = 1; - const int barHeight = 6; - const int labelX = x; - int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; + // === Layout === + int line = 1; + const int barHeight = 6; + const int labelX = x; + int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; #ifdef USE_EINK #ifndef T_DECK_PRO - barsOffset -= 12; + barsOffset -= 12; #endif #endif - int barX = x + barsOffset; - if (currentResolution == ScreenResolution::UltraLow) { - barX += 45; + int barX = x + barsOffset; + if (currentResolution == ScreenResolution::UltraLow) { + barX += 45; + } else { + barX += 40; + } + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { + if (total == 0) + return; + + int percent = (used * 100) / total; + + char combinedStr[24]; + if (currentResolution == ScreenResolution::High) { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); } else { - barX += 40; - } - auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { - if (total == 0) - return; - - int percent = (used * 100) / total; - - char combinedStr[24]; - if (currentResolution == ScreenResolution::High) { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, - total / 1024); - } else { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); - } - - int textWidth = display->getStringWidth(combinedStr); - int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; - if (adjustedBarWidth < 10) - adjustedBarWidth = 10; - - int fillWidth = (used * adjustedBarWidth) / total; - - // Label - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(labelX, getTextPositions(display)[line], label); -#if !defined(M5STACK_UNITC6L) - // Bar - int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; - display->setColor(WHITE); - display->drawRect(barX, barY, adjustedBarWidth, barHeight); - - display->fillRect(barX, barY, fillWidth, barHeight); - display->setColor(WHITE); -#endif - // Value string - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); - }; - - // === Memory values === - uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); - uint32_t heapTotal = memGet.getHeapSize(); - - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); - - uint32_t flashUsed = 0, flashTotal = 0; -#ifdef ESP32 - flashUsed = FSCom.usedBytes(); - flashTotal = FSCom.totalBytes(); -#endif - - uint32_t sdUsed = 0, sdTotal = 0; - bool hasSD = false; - /* - #ifdef HAS_SDCARD - hasSD = SD.cardType() != CARD_NONE; - if (hasSD) { - sdUsed = SD.usedBytes(); - sdTotal = SD.totalBytes(); - } - #endif - */ - // === Draw memory rows - drawUsageRow("Heap:", heapUsed, heapTotal, true); -#ifdef ESP32 - if (psramUsed > 0) { - line += 1; - drawUsageRow("PSRAM:", psramUsed, psramTotal); - } - if (flashTotal > 0) { - line += 1; - drawUsageRow("Flash:", flashUsed, flashTotal); - } -#endif - if (hasSD && sdTotal > 0) { - line += 1; - drawUsageRow("SD:", sdUsed, sdTotal); + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); } + int textWidth = display->getStringWidth(combinedStr); + int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; + if (adjustedBarWidth < 10) + adjustedBarWidth = 10; + + int fillWidth = (used * adjustedBarWidth) / total; + + // Label display->setTextAlignment(TEXT_ALIGN_LEFT); - // System Uptime - if (line < 2) { - line += 1; - } + display->drawString(labelX, getTextPositions(display)[line], label); +#if !defined(M5STACK_UNITC6L) + // Bar + int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; + display->setColor(WHITE); + display->drawRect(barX, barY, adjustedBarWidth, barHeight); + + display->fillRect(barX, barY, fillWidth, barHeight); + display->setColor(WHITE); +#endif + // Value string + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); + }; + + // === Memory values === + uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); + uint32_t heapTotal = memGet.getHeapSize(); + + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); + + uint32_t flashUsed = 0, flashTotal = 0; +#ifdef ESP32 + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); +#endif + + uint32_t sdUsed = 0, sdTotal = 0; + bool hasSD = false; + /* + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif + */ + // === Draw memory rows + drawUsageRow("Heap:", heapUsed, heapTotal, true); +#ifdef ESP32 + if (psramUsed > 0) { line += 1; + drawUsageRow("PSRAM:", psramUsed, psramTotal); + } + if (flashTotal > 0) { + line += 1; + drawUsageRow("Flash:", flashUsed, flashTotal); + } +#endif + if (hasSD && sdTotal > 0) { + line += 1; + drawUsageRow("SD:", sdUsed, sdTotal); + } - char appversionstr[35]; - char appversionstr_formatted[40]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + // System Uptime + if (line < 2) { + line += 1; + } + line += 1; - const char *ver = optstr(APP_VERSION); - char verbuf[32]; - strncpy(verbuf, ver, sizeof(verbuf) - 1); - verbuf[sizeof(verbuf) - 1] = '\0'; + char appversionstr[35]; + char appversionstr_formatted[40]; - char *lastDot = strrchr(verbuf, '.'); + const char *ver = optstr(APP_VERSION); + char verbuf[32]; + strncpy(verbuf, ver, sizeof(verbuf) - 1); + verbuf[sizeof(verbuf) - 1] = '\0'; - if (currentResolution == ScreenResolution::UltraLow) { - if (lastDot != nullptr) { - *lastDot = '\0'; - } - snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + char *lastDot = strrchr(verbuf, '.'); + + if (currentResolution == ScreenResolution::UltraLow) { + if (lastDot != nullptr) { + *lastDot = '\0'; + } + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + } else { + if (lastDot) { + size_t prefixLen = (size_t)(lastDot - verbuf); + snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); + strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); + appversionstr[sizeof(appversionstr) - 1] = '\0'; } else { - if (lastDot) { - size_t prefixLen = (size_t)(lastDot - verbuf); - snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); - strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); - appversionstr[sizeof(appversionstr) - 1] = '\0'; - } else { - snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); - } + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); } - int textWidth = display->getStringWidth(appversionstr); - int nameX = (SCREEN_WIDTH - textWidth) / 2; + } + int textWidth = display->getStringWidth(appversionstr); + int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], appversionstr); + display->drawString(nameX, getTextPositions(display)[line++], appversionstr); - if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it - char uptimeStr[32] = ""; - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); - textWidth = display->getStringWidth(uptimeStr); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it + char uptimeStr[32] = ""; + getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + textWidth = display->getStringWidth(uptimeStr); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); + } + + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show API state if the screen can show it + char api_state[32] = ""; + const char *clientWord = nullptr; + + // Determine if narrow or wide screen + if (currentResolution == ScreenResolution::High) { + clientWord = "Client"; + } else { + clientWord = "App"; } + snprintf(api_state, sizeof(api_state), "No %ss Connected", clientWord); - if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show API state if the screen can show it - char api_state[32] = ""; - const char *clientWord = nullptr; - - // Determine if narrow or wide screen - if (currentResolution == ScreenResolution::High) { - clientWord = "Client"; - } else { - clientWord = "App"; - } - snprintf(api_state, sizeof(api_state), "No %ss Connected", clientWord); - - if (service->api_state == service->STATE_BLE) { - snprintf(api_state, sizeof(api_state), "%s Connected (BLE)", clientWord); - } else if (service->api_state == service->STATE_WIFI) { - snprintf(api_state, sizeof(api_state), "%s Connected (WiFi)", clientWord); - } else if (service->api_state == service->STATE_SERIAL) { - snprintf(api_state, sizeof(api_state), "%s Connected (Serial)", clientWord); - } else if (service->api_state == service->STATE_PACKET) { - snprintf(api_state, sizeof(api_state), "%s Connected (Internal)", clientWord); - } else if (service->api_state == service->STATE_HTTP) { - snprintf(api_state, sizeof(api_state), "%s Connected (HTTP)", clientWord); - } else if (service->api_state == service->STATE_ETH) { - snprintf(api_state, sizeof(api_state), "%s Connected (Ethernet)", clientWord); - } - if (api_state[0] != '\0') { - display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++], - api_state); - } + if (service->api_state == service->STATE_BLE) { + snprintf(api_state, sizeof(api_state), "%s Connected (BLE)", clientWord); + } else if (service->api_state == service->STATE_WIFI) { + snprintf(api_state, sizeof(api_state), "%s Connected (WiFi)", clientWord); + } else if (service->api_state == service->STATE_SERIAL) { + snprintf(api_state, sizeof(api_state), "%s Connected (Serial)", clientWord); + } else if (service->api_state == service->STATE_PACKET) { + snprintf(api_state, sizeof(api_state), "%s Connected (Internal)", clientWord); + } else if (service->api_state == service->STATE_HTTP) { + snprintf(api_state, sizeof(api_state), "%s Connected (HTTP)", clientWord); + } else if (service->api_state == service->STATE_ETH) { + snprintf(api_state, sizeof(api_state), "%s Connected (Ethernet)", clientWord); } + if (api_state[0] != '\0') { + display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++], api_state); + } + } - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // **************************** // * Chirpy Screen * // **************************** -void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; - int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); - int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; - int textX_offset = 10; - if (currentResolution == ScreenResolution::High) { - textX_offset = textX_offset * 4; - const int scale = 2; - const int bytesPerRow = (chirpy_width + 7) / 8; +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); + int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; + int textX_offset = 10; + if (currentResolution == ScreenResolution::High) { + textX_offset = textX_offset * 4; + const int scale = 2; + const int bytesPerRow = (chirpy_width + 7) / 8; - for (int yy = 0; yy < chirpy_height; ++yy) { - iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); - iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; - const uint8_t *rowPtr = chirpy + yy * bytesPerRow; - for (int xx = 0; xx < chirpy_width; ++xx) { - const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); - const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first - if (byteVal & bitMask) { - display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); - } - } + for (int yy = 0; yy < chirpy_height; ++yy) { + iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); + iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; + const uint8_t *rowPtr = chirpy + yy * bytesPerRow; + for (int xx = 0; xx < chirpy_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); } - } else { - display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); + } } + } else { + display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); + } - int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); - display->drawString(textX, getTextPositions(display)[line++], "Hello"); - textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); - display->drawString(textX, getTextPositions(display)[line++], "World!"); + int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); + display->drawString(textX, getTextPositions(display)[line++], "Hello"); + textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); + display->drawString(textX, getTextPositions(display)[line++], "World!"); } } // namespace DebugRenderer diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index 65fa74ca6..ecbed6769 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -3,8 +3,7 @@ #include #include -namespace graphics -{ +namespace graphics { /// Forward declarations class Screen; @@ -16,8 +15,7 @@ class DebugInfo; * Contains all functions related to drawing debug information, * WiFi status, settings screens, and diagnostic data. */ -namespace DebugRenderer -{ +namespace DebugRenderer { // Debug frame functions void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h index c55e66ede..174b638bf 100644 --- a/src/graphics/draw/DrawRenderers.h +++ b/src/graphics/draw/DrawRenderers.h @@ -13,8 +13,7 @@ #include "graphics/draw/NodeListRenderer.h" #include "graphics/draw/UIRenderer.h" -namespace graphics -{ +namespace graphics { /** * @brief Collection of all draw renderers @@ -22,8 +21,7 @@ namespace graphics * This namespace provides access to all the specialized rendering * functions organized by category. */ -namespace DrawRenderers -{ +namespace DrawRenderers { // Re-export all renderer namespaces for convenience using namespace ClockRenderer; using namespace CompassRenderer; diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 5687c8620..91f71a893 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -30,30 +30,27 @@ extern uint16_t TFT_MESH; -namespace graphics -{ +namespace graphics { -namespace -{ +namespace { // Caller must ensure the provided options array outlives the banner callback. template -BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], - std::array &labels, Callback &&onSelection) -{ - for (size_t i = 0; i < N; ++i) { - labels[i] = options[i].label; - } +BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], std::array &labels, + Callback &&onSelection) { + for (size_t i = 0; i < N; ++i) { + labels[i] = options[i].label; + } - const MenuOption *optionsPtr = options; - auto callback = std::function &, int)>(std::forward(onSelection)); + const MenuOption *optionsPtr = options; + auto callback = std::function &, int)>(std::forward(onSelection)); - BannerOverlayOptions bannerOptions; - bannerOptions.message = message; - bannerOptions.optionsArrayPtr = labels.data(); - bannerOptions.optionsCount = static_cast(N); - bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; - return bannerOptions; + BannerOverlayOptions bannerOptions; + bannerOptions.message = message; + bannerOptions.optionsArrayPtr = labels.data(); + bannerOptions.optionsCount = static_cast(N); + bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; + return bannerOptions; } } // namespace @@ -62,2497 +59,2415 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; -void menuHandler::loraMenu() -{ - static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"}; - enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "LoRa Actions"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - // No action - } else if (selected == device_role_picker) { - menuHandler::menuQueue = menuHandler::device_role_picker; - } else if (selected == radio_preset_picker) { - menuHandler::menuQueue = menuHandler::radio_preset_picker; - } else if (selected == lora_picker) { - menuHandler::menuQueue = menuHandler::lora_picker; - } - }; - screen->showOverlayBanner(bannerOptions); +void menuHandler::loraMenu() { + static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"}; + enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "LoRa Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + // No action + } else if (selected == device_role_picker) { + menuHandler::menuQueue = menuHandler::device_role_picker; + } else if (selected == radio_preset_picker) { + menuHandler::menuQueue = menuHandler::radio_preset_picker; + } else if (selected == lora_picker) { + menuHandler::menuQueue = menuHandler::lora_picker; + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::OnboardMessage() -{ - static const char *optionsArray[] = {"OK", "Got it!"}; - enum optionsNumbers { OK, got }; - BannerOverlayOptions bannerOptions; +void menuHandler::OnboardMessage() { + static const char *optionsArray[] = {"OK", "Got it!"}; + enum optionsNumbers { OK, got }; + BannerOverlayOptions bannerOptions; #if HAS_TFT - bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu."; + bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu."; #elif defined(BUTTON_PIN) - bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu."; + bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu."; #else - bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections."; + bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections."; #endif - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; - screen->runNow(); - }; - screen->showOverlayBanner(bannerOptions); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; + screen->runNow(); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::LoraRegionPicker(uint32_t duration) -{ - static const LoraRegionOption regionOptions[] = { - {"Back", OptionsAction::Back}, - {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, - {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, - {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, - {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, - {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, - {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, - {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, - {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, - {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, - {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, - {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, - {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, - {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, - {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, - {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, - {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, - {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, - {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, - {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, - {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, - {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, - {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, - {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, - {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, - {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, - {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, - }; +void menuHandler::LoraRegionPicker(uint32_t duration) { + static const LoraRegionOption regionOptions[] = { + {"Back", OptionsAction::Back}, + {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, + {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, + {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, + {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, + {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, + {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, + {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, + {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, + {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, + {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, + {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, + {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, + {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, + {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, + {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, + {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, + {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, + {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, + {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, + {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, + {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, + {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, + {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, + {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, + {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, + {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, + }; - constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); - static std::array regionLabels{}; + constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); + static std::array regionLabels{}; - const char *bannerMessage = "Set the LoRa region"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerMessage = "LoRa Region"; + const char *bannerMessage = "Set the LoRa region"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerMessage = "LoRa Region"; + } + + auto bannerOptions = createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { + if (!option.hasValue) { + return; } - auto bannerOptions = - createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { - if (!option.hasValue) { - return; - } + auto selectedRegion = option.value; + if (config.lora.region == selectedRegion) { + return; + } - auto selectedRegion = option.value; - if (config.lora.region == selectedRegion) { - return; - } + config.lora.region = selectedRegion; + auto changes = SEGMENT_CONFIG; - config.lora.region = selectedRegion; - auto changes = SEGMENT_CONFIG; - - // FIXME: This should be a method consolidated with the same logic in the admin message as well - // This is needed as we wait til picking the LoRa region to generate keys for the first time. + // FIXME: This should be a method consolidated with the same logic in the admin message as well + // This is needed as we wait til picking the LoRa region to generate keys for the first time. #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - // public key is derived from private, so this will always have the same result. - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } -#endif - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default broker is in use, so subscribe to the appropriate MQTT root topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes |= SEGMENT_MODULECONFIG; - } - - service->reloadConfig(changes); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }); - - bannerOptions.durationMs = duration; - - int initialSelection = 0; - for (size_t i = 0; i < regionCount; ++i) { - if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { - initialSelection = static_cast(i); - break; + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + // public key is derived from private, so this will always have the same result. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; } + + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } +#endif + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit } - bannerOptions.InitialSelected = initialSelection; - screen->showOverlayBanner(bannerOptions); + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + // Default broker is in use, so subscribe to the appropriate MQTT root topic for this region + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes |= SEGMENT_MODULECONFIG; + } + + service->reloadConfig(changes); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + bannerOptions.durationMs = duration; + + int initialSelection = 0; + for (size_t i = 0; i < regionCount; ++i) { + if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); } -void menuHandler::DeviceRolePicker() -{ - static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; - enum optionsNumbers { - Back = 0, - devicerole_client = 1, - devicerole_clientmute = 2, - devicerole_lostandfound = 3, - devicerole_tracker = 4 - }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Device Role"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 5; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; - screen->runNow(); - return; - } else if (selected == devicerole_client) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - } else if (selected == devicerole_clientmute) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; - } else if (selected == devicerole_lostandfound) { - config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; - } else if (selected == devicerole_tracker) { - config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; - } - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }; - screen->showOverlayBanner(bannerOptions); +void menuHandler::DeviceRolePicker() { + static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; + enum optionsNumbers { Back = 0, devicerole_client = 1, devicerole_clientmute = 2, devicerole_lostandfound = 3, devicerole_tracker = 4 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Device Role"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } else if (selected == devicerole_client) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else if (selected == devicerole_clientmute) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; + } else if (selected == devicerole_lostandfound) { + config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; + } else if (selected == devicerole_tracker) { + config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::RadioPresetPicker() -{ - static const RadioPresetOption presetOptions[] = { - {"Back", OptionsAction::Back}, - {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, - {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, - {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, - {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, - {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, - {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, - {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, - {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, - }; +void menuHandler::RadioPresetPicker() { + static const RadioPresetOption presetOptions[] = { + {"Back", OptionsAction::Back}, + {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, + {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, + {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, + {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, + {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, + {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, + {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, + {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, + }; - constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); - static std::array presetLabels{}; + constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); + static std::array presetLabels{}; - auto bannerOptions = - createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; - screen->runNow(); - return; - } + auto bannerOptions = createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } - if (!option.hasValue) { - return; - } + if (!option.hasValue) { + return; + } - config.lora.modem_preset = option.value; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }); + config.lora.modem_preset = option.value; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); - screen->showOverlayBanner(bannerOptions); + screen->showOverlayBanner(bannerOptions); } -void menuHandler::TwelveHourPicker() -{ - static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; - enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Time Format"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == twelve) { - config.display.use_12h_clock = true; - } else { - config.display.use_12h_clock = false; - } - service->reloadConfig(SEGMENT_CONFIG); - }; - screen->showOverlayBanner(bannerOptions); +void menuHandler::TwelveHourPicker() { + static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; + enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Time Format"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + } else if (selected == twelve) { + config.display.use_12h_clock = true; + } else { + config.display.use_12h_clock = false; + } + service->reloadConfig(SEGMENT_CONFIG); + }; + screen->showOverlayBanner(bannerOptions); } // Reusable confirmation prompt function -void menuHandler::showConfirmationBanner(const char *message, std::function onConfirm) -{ - static const char *confirmOptions[] = {"No", "Yes"}; - BannerOverlayOptions confirmBanner; - confirmBanner.message = message; - confirmBanner.optionsArrayPtr = confirmOptions; - confirmBanner.optionsCount = 2; - confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void { - if (confirmSelected == 1) { - onConfirm(); - } - }; - screen->showOverlayBanner(confirmBanner); -} - -void menuHandler::ClockFacePicker() -{ - static const ClockFaceOption clockFaceOptions[] = { - {"Back", OptionsAction::Back}, - {"Digital", OptionsAction::Select, false}, - {"Analog", OptionsAction::Select, true}, - }; - - constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); - static std::array clockFaceLabels{}; - - auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, - [](const ClockFaceOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (uiconfig.is_clockface_analog == option.value) { - return; - } - - uiconfig.is_clockface_analog = option.value; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - }); - - bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::TZPicker() -{ - static const TimezoneOption timezoneOptions[] = { - {"Back", OptionsAction::Back}, - {"US/Hawaii", OptionsAction::Select, "HST10"}, - {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, - {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, - {"US/Arizona", OptionsAction::Select, "MST7"}, - {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, - {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, - {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, - {"BR/Brasilia", OptionsAction::Select, "BRT3"}, - {"UTC", OptionsAction::Select, "UTC0"}, - {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, - {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, - {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, - {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, - {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, - {"AU/AWST", OptionsAction::Select, "AWST-8"}, - {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, - {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, - {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, - }; - - constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); - static std::array timezoneLabels{}; - - auto bannerOptions = createStaticBannerOptions( - "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { - return; - } - - strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); - config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; - - setenv("TZ", config.device.tzdef, 1); - service->reloadConfig(SEGMENT_CONFIG); - }); - - int initialSelection = 0; - for (size_t i = 0; i < timezoneCount; ++i) { - if (timezoneOptions[i].hasValue && - strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { - initialSelection = static_cast(i); - break; - } +void menuHandler::showConfirmationBanner(const char *message, std::function onConfirm) { + static const char *confirmOptions[] = {"No", "Yes"}; + BannerOverlayOptions confirmBanner; + confirmBanner.message = message; + confirmBanner.optionsArrayPtr = confirmOptions; + confirmBanner.optionsCount = 2; + confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void { + if (confirmSelected == 1) { + onConfirm(); } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); + }; + screen->showOverlayBanner(confirmBanner); } -void menuHandler::clockMenu() -{ +void menuHandler::ClockFacePicker() { + static const ClockFaceOption clockFaceOptions[] = { + {"Back", OptionsAction::Back}, + {"Digital", OptionsAction::Select, false}, + {"Analog", OptionsAction::Select, true}, + }; + + constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); + static std::array clockFaceLabels{}; + + auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, [](const ClockFaceOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.is_clockface_analog == option.value) { + return; + } + + uiconfig.is_clockface_analog = option.value; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + }); + + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TZPicker() { + static const TimezoneOption timezoneOptions[] = { + {"Back", OptionsAction::Back}, + {"US/Hawaii", OptionsAction::Select, "HST10"}, + {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, + {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, + {"US/Arizona", OptionsAction::Select, "MST7"}, + {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, + {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, + {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, + {"BR/Brasilia", OptionsAction::Select, "BRT3"}, + {"UTC", OptionsAction::Select, "UTC0"}, + {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, + {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, + {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, + {"AU/AWST", OptionsAction::Select, "AWST-8"}, + {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + }; + + constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); + static std::array timezoneLabels{}; + + auto bannerOptions = createStaticBannerOptions("Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { + return; + } + + strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + + setenv("TZ", config.device.tzdef, 1); + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < timezoneCount; ++i) { + if (timezoneOptions[i].hasValue && strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::clockMenu() { #if defined(M5STACK_UNITC6L) - static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; + static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; #else - static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; + static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; #endif - enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Clock Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Clock) { - menuHandler::menuQueue = menuHandler::clock_face_picker; - screen->runNow(); - } else if (selected == Time) { - menuHandler::menuQueue = menuHandler::twelve_hour_picker; - screen->runNow(); - } else if (selected == Timezone) { - menuHandler::menuQueue = menuHandler::TZ_picker; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Clock Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Clock) { + menuHandler::menuQueue = menuHandler::clock_face_picker; + screen->runNow(); + } else if (selected == Time) { + menuHandler::menuQueue = menuHandler::twelve_hour_picker; + screen->runNow(); + } else if (selected == Timezone) { + menuHandler::menuQueue = menuHandler::TZ_picker; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::messageResponseMenu() -{ - enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd }; +void menuHandler::messageResponseMenu() { + enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd }; - static const char *optionsArray[enumEnd]; - static int optionsEnumArray[enumEnd]; - int options = 0; + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + // New Reply submenu (replaces Preset and Freetext directly in this menu) + optionsArray[options] = "Reply"; + optionsEnumArray[options++] = ReplyMenu; + + optionsArray[options] = "View Chats"; + optionsEnumArray[options++] = ViewMode; + + // Delete submenu + optionsArray[options] = "Delete"; + optionsEnumArray[options++] = 900; + +#ifdef HAS_I2S + optionsArray[options] = "Read Aloud"; + optionsEnumArray[options++] = Aloud; +#endif + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Message"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + LOG_DEBUG("messageResponseMenu: selected %d", selected); auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - optionsArray[options] = "Back"; - optionsEnumArray[options++] = Back; + LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); - // New Reply submenu (replaces Preset and Freetext directly in this menu) - optionsArray[options] = "Reply"; - optionsEnumArray[options++] = ReplyMenu; + if (selected == ViewMode) { + menuHandler::menuQueue = menuHandler::message_viewmode_menu; + screen->runNow(); - optionsArray[options] = "View Chats"; - optionsEnumArray[options++] = ViewMode; + // Reply submenu + } else if (selected == ReplyMenu) { + menuHandler::menuQueue = menuHandler::reply_menu; + screen->runNow(); - // Delete submenu - optionsArray[options] = "Delete"; - optionsEnumArray[options++] = 900; + // Delete submenu + } else if (selected == 900) { + menuHandler::menuQueue = menuHandler::delete_messages_menu; + screen->runNow(); + + // Delete oldest FIRST (only change) + } else if (selected == DeleteOldest) { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + // Global oldest + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + // Oldest in current channel + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + // Oldest in current DM + messageStore.deleteOldestMessageWithPeer(peer); + } + + // Delete all messages + } else if (selected == DeleteAll) { + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); #ifdef HAS_I2S - optionsArray[options] = "Read Aloud"; - optionsEnumArray[options++] = Aloud; + } else if (selected == Aloud) { + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + audioThread->readAloud(msg); #endif - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Message Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Message"; } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - LOG_DEBUG("messageResponseMenu: selected %d", selected); - - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); - - if (selected == ViewMode) { - menuHandler::menuQueue = menuHandler::message_viewmode_menu; - screen->runNow(); - - // Reply submenu - } else if (selected == ReplyMenu) { - menuHandler::menuQueue = menuHandler::reply_menu; - screen->runNow(); - - // Delete submenu - } else if (selected == 900) { - menuHandler::menuQueue = menuHandler::delete_messages_menu; - screen->runNow(); - - // Delete oldest FIRST (only change) - } else if (selected == DeleteOldest) { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - // Global oldest - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - // Oldest in current channel - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - // Oldest in current DM - messageStore.deleteOldestMessageWithPeer(peer); - } - - // Delete all messages - } else if (selected == DeleteAll) { - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); - -#ifdef HAS_I2S - } else if (selected == Aloud) { - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - audioThread->readAloud(msg); -#endif - } - }; - screen->showOverlayBanner(bannerOptions); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::replyMenu() -{ - enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; +void menuHandler::replyMenu() { + enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; - static const char *optionsArray[enumEnd]; - static int optionsEnumArray[enumEnd]; - int options = 0; + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; - // Back - optionsArray[options] = "Back"; - optionsEnumArray[options++] = Back; + // Back + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + // Preset reply + optionsArray[options] = "With Preset"; + optionsEnumArray[options++] = ReplyPreset; + + // Freetext reply (only when keyboard exists) + if (kb_found) { + optionsArray[options] = "With Freetext"; + optionsEnumArray[options++] = ReplyFreetext; + } + + BannerOverlayOptions bannerOptions; + + // Dynamic title based on thread mode + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + bannerOptions.message = "Reply to Channel"; + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + bannerOptions.message = "Reply to DM"; + } else { + // View All + bannerOptions.message = "Reply to Last Msg"; + } + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.InitialSelected = 1; + + bannerOptions.bannerCallback = [](int selected) -> void { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } // Preset reply - optionsArray[options] = "With Preset"; - optionsEnumArray[options++] = ReplyPreset; + if (selected == ReplyPreset) { - // Freetext reply (only when keyboard exists) - if (kb_found) { - optionsArray[options] = "With Freetext"; - optionsEnumArray[options++] = ReplyFreetext; - } + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); - BannerOverlayOptions bannerOptions; + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchWithDestination(peer); - // Dynamic title based on thread mode - auto mode = graphics::MessageRenderer::getThreadMode(); - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - bannerOptions.message = "Reply to Channel"; - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - bannerOptions.message = "Reply to DM"; - } else { - // View All - bannerOptions.message = "Reply to Last Msg"; - } - - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.InitialSelected = 1; - - bannerOptions.bannerCallback = [](int selected) -> void { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; - screen->runNow(); - return; - } - - // Preset reply - if (selected == ReplyPreset) { - - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); - - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - cannedMessageModule->LaunchWithDestination(peer); - - } else { - // Fallback for last received message - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); - } - } - - return; - } - - // Freetext reply - if (selected == ReplyFreetext) { - - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); - - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - cannedMessageModule->LaunchFreetextWithDestination(peer); - - } else { - // Fallback for last received message - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); - } - } - - return; - } - }; - screen->showOverlayBanner(bannerOptions); -} -void menuHandler::deleteMessagesMenu() -{ - enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; - - static const char *optionsArray[enumEnd]; - static int optionsEnumArray[enumEnd]; - int options = 0; - - auto mode = graphics::MessageRenderer::getThreadMode(); - - optionsArray[options] = "Back"; - optionsEnumArray[options++] = Back; - - optionsArray[options] = "Delete Oldest"; - optionsEnumArray[options++] = DeleteOldest; - - // If viewing ALL chats → hide “Delete This Chat” - if (mode != graphics::MessageRenderer::ThreadMode::ALL) { - optionsArray[options] = "Delete This Chat"; - optionsEnumArray[options++] = DeleteThis; - } - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "Delete All"; - } else { - optionsArray[options] = "Delete All Chats"; - } - optionsEnumArray[options++] = DeleteAll; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Delete Messages"; - - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [mode](int selected) -> void { - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; - screen->runNow(); - return; - } - - if (selected == DeleteAll) { - LOG_INFO("Deleting all messages"); - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); - return; - } - - if (selected == DeleteOldest) { - LOG_INFO("Deleting oldest message"); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - messageStore.deleteOldestMessageWithPeer(peer); - } - - return; - } - - // This only appears in non-ALL modes - if (selected == DeleteThis) { - LOG_INFO("Deleting all messages in this thread"); - - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - messageStore.deleteAllMessagesInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - messageStore.deleteAllMessagesWithPeer(peer); - } - - return; - } - }; - - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::messageViewModeMenu() -{ - auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; - auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; - - static std::vector labels; - static std::vector ids; - static std::vector idToPeer; // DM lookup - - labels.clear(); - ids.clear(); - idToPeer.clear(); - - labels.push_back("Back"); - ids.push_back(-1); - labels.push_back("View All Chats"); - ids.push_back(-2); - - // Channels with messages - for (int ch = 0; ch < 8; ++ch) { - auto msgs = messageStore.getChannelMessages((uint8_t)ch); - if (!msgs.empty()) { - char buf[40]; - const char *cname = channels.getName(ch); - snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); - labels.push_back(buf); - ids.push_back(encodeChannelId(ch)); - LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); - } - } - - // Registry channels - for (int ch : graphics::MessageRenderer::getSeenChannels()) { - if (ch < 0 || ch >= 8) - continue; - auto msgs = messageStore.getChannelMessages((uint8_t)ch); - if (msgs.empty()) - continue; - int enc = encodeChannelId(ch); - if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { - char buf[40]; - const char *cname = channels.getName(ch); - snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); - labels.push_back(buf); - ids.push_back(enc); - LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); - } - } - - // Gather unique peers - auto dms = messageStore.getDirectMessages(); - std::vector uniquePeers; - for (auto &m : dms) { - uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; - if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) - uniquePeers.push_back(peer); - } - for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { - if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) - uniquePeers.push_back(peer); - } - std::sort(uniquePeers.begin(), uniquePeers.end()); - - // Encode peers - for (size_t i = 0; i < uniquePeers.size(); ++i) { - uint32_t peer = uniquePeers[i]; - auto node = nodeDB->getMeshNode(peer); - std::string name; - if (node && node->has_user) - name = sanitizeString(node->user.long_name).substr(0, 15); - else { - char buf[20]; - snprintf(buf, sizeof(buf), "Node %08X", peer); - name = buf; - } - labels.push_back("@" + name); - int encPeer = 1000 + (int)idToPeer.size(); - ids.push_back(encPeer); - idToPeer.push_back(peer); - LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); - } - - // Active ID - int activeId = -2; - auto mode = graphics::MessageRenderer::getThreadMode(); - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) - activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); - else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - uint32_t cur = graphics::MessageRenderer::getThreadPeer(); - for (size_t i = 0; i < idToPeer.size(); ++i) - if (idToPeer[i] == cur) { - activeId = 1000 + (int)i; - break; - } - } - - LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); - - // Build banner - static std::vector options; - static std::vector optionIds; - options.clear(); - optionIds.clear(); - - int initialIndex = 0; - for (size_t i = 0; i < labels.size(); i++) { - options.push_back(labels[i].c_str()); - optionIds.push_back(ids[i]); - if (ids[i] == activeId) - initialIndex = (int)i; - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Select Conversation"; - bannerOptions.optionsArrayPtr = options.data(); - bannerOptions.optionsEnumPtr = optionIds.data(); - bannerOptions.optionsCount = options.size(); - bannerOptions.InitialSelected = initialIndex; - - bannerOptions.bannerCallback = [=](int selected) -> void { - LOG_DEBUG("messageViewModeMenu: selected=%d", selected); - if (selected == -1) { - menuHandler::menuQueue = menuHandler::message_response_menu; - screen->runNow(); - } else if (selected == -2) { - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); - } else if (isChannelSel(selected)) { - int ch = selected - 100; - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); - } else if (selected >= 1000) { - int idx = selected - 1000; - if (idx >= 0 && (size_t)idx < idToPeer.size()) { - uint32_t peer = idToPeer[idx]; - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); - } - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::homeBaseMenu() -{ - enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; - - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - if (moduleConfig.external_notification.enabled && externalNotificationModule && - config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { - if (!externalNotificationModule->getMute()) { - optionsArray[options] = "Temporarily Mute"; + } else { + // Fallback for last received message + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); } else { - optionsArray[options] = "Unmute"; + cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); } - optionsEnumArray[options++] = Mute; + } + + return; } + + // Freetext reply + if (selected == ReplyFreetext) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchFreetextWithDestination(peer); + + } else { + // Fallback for last received message + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + } + } + + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} +void menuHandler::deleteMessagesMenu() { + enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; + + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + optionsArray[options] = "Delete Oldest"; + optionsEnumArray[options++] = DeleteOldest; + + // If viewing ALL chats → hide “Delete This Chat” + if (mode != graphics::MessageRenderer::ThreadMode::ALL) { + optionsArray[options] = "Delete This Chat"; + optionsEnumArray[options++] = DeleteThis; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Delete All"; + } else { + optionsArray[options] = "Delete All Chats"; + } + optionsEnumArray[options++] = DeleteAll; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Delete Messages"; + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [mode](int selected) -> void { + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + if (selected == DeleteAll) { + LOG_INFO("Deleting all messages"); + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); + return; + } + + if (selected == DeleteOldest) { + LOG_INFO("Deleting oldest message"); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteOldestMessageWithPeer(peer); + } + + return; + } + + // This only appears in non-ALL modes + if (selected == DeleteThis) { + LOG_INFO("Deleting all messages in this thread"); + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteAllMessagesInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteAllMessagesWithPeer(peer); + } + + return; + } + }; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageViewModeMenu() { + auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; + auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; + + static std::vector labels; + static std::vector ids; + static std::vector idToPeer; // DM lookup + + labels.clear(); + ids.clear(); + idToPeer.clear(); + + labels.push_back("Back"); + ids.push_back(-1); + labels.push_back("View All Chats"); + ids.push_back(-2); + + // Channels with messages + for (int ch = 0; ch < 8; ++ch) { + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (!msgs.empty()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(encodeChannelId(ch)); + LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); + } + } + + // Registry channels + for (int ch : graphics::MessageRenderer::getSeenChannels()) { + if (ch < 0 || ch >= 8) + continue; + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (msgs.empty()) + continue; + int enc = encodeChannelId(ch); + if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(enc); + LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); + } + } + + // Gather unique peers + auto dms = messageStore.getDirectMessages(); + std::vector uniquePeers; + for (auto &m : dms) { + uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + std::sort(uniquePeers.begin(), uniquePeers.end()); + + // Encode peers + for (size_t i = 0; i < uniquePeers.size(); ++i) { + uint32_t peer = uniquePeers[i]; + auto node = nodeDB->getMeshNode(peer); + std::string name; + if (node && node->has_user) + name = sanitizeString(node->user.long_name).substr(0, 15); + else { + char buf[20]; + snprintf(buf, sizeof(buf), "Node %08X", peer); + name = buf; + } + labels.push_back("@" + name); + int encPeer = 1000 + (int)idToPeer.size(); + ids.push_back(encPeer); + idToPeer.push_back(peer); + LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); + } + + // Active ID + int activeId = -2; + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) + activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); + else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + uint32_t cur = graphics::MessageRenderer::getThreadPeer(); + for (size_t i = 0; i < idToPeer.size(); ++i) + if (idToPeer[i] == cur) { + activeId = 1000 + (int)i; + break; + } + } + + LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); + + // Build banner + static std::vector options; + static std::vector optionIds; + options.clear(); + optionIds.clear(); + + int initialIndex = 0; + for (size_t i = 0; i < labels.size(); i++) { + options.push_back(labels[i].c_str()); + optionIds.push_back(ids[i]); + if (ids[i] == activeId) + initialIndex = (int)i; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Select Conversation"; + bannerOptions.optionsArrayPtr = options.data(); + bannerOptions.optionsEnumPtr = optionIds.data(); + bannerOptions.optionsCount = options.size(); + bannerOptions.InitialSelected = initialIndex; + + bannerOptions.bannerCallback = [=](int selected) -> void { + LOG_DEBUG("messageViewModeMenu: selected=%d", selected); + if (selected == -1) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + } else if (selected == -2) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); + } else if (isChannelSel(selected)) { + int ch = selected - 100; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); + } else if (selected >= 1000) { + int idx = selected - 1000; + if (idx >= 0 && (size_t)idx < idToPeer.size()) { + uint32_t peer = idToPeer[idx]; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); + } + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::homeBaseMenu() { + enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + if (moduleConfig.external_notification.enabled && externalNotificationModule && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { + if (!externalNotificationModule->getMute()) { + optionsArray[options] = "Temporarily Mute"; + } else { + optionsArray[options] = "Unmute"; + } + optionsEnumArray[options++] = Mute; + } #if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) - optionsArray[options] = "Toggle Backlight"; - optionsEnumArray[options++] = Backlight; + optionsArray[options] = "Toggle Backlight"; + optionsEnumArray[options++] = Backlight; #else - optionsArray[options] = "Sleep Screen"; - optionsEnumArray[options++] = Sleep; + optionsArray[options] = "Sleep Screen"; + optionsEnumArray[options++] = Sleep; #endif - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - optionsArray[options] = "Send Position"; - } else { - optionsArray[options] = "Send Node Info"; - } - optionsEnumArray[options++] = Position; + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + optionsArray[options] = "Send Position"; + } else { + optionsArray[options] = "Send Node Info"; + } + optionsEnumArray[options++] = Position; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Home Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Home"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Mute) { - if (moduleConfig.external_notification.enabled && externalNotificationModule) { - externalNotificationModule->setMute(!externalNotificationModule->getMute()); - IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) - } - } else if (selected == Backlight) { - screen->setOn(false); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Home Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Home"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Mute) { + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + } + } else if (selected == Backlight) { + screen->setOn(false); #if defined(PIN_EINK_EN) - if (uiconfig.screen_brightness == 1) { - uiconfig.screen_brightness = 0; - digitalWrite(PIN_EINK_EN, LOW); - } else { - uiconfig.screen_brightness = 1; - digitalWrite(PIN_EINK_EN, HIGH); - } - saveUIConfig(); + if (uiconfig.screen_brightness == 1) { + uiconfig.screen_brightness = 0; + digitalWrite(PIN_EINK_EN, LOW); + } else { + uiconfig.screen_brightness = 1; + digitalWrite(PIN_EINK_EN, HIGH); + } + saveUIConfig(); #elif defined(PCA_PIN_EINK_EN) - if (uiconfig.screen_brightness > 0) { - uiconfig.screen_brightness = 0; - io.digitalWrite(PCA_PIN_EINK_EN, LOW); - } else { - uiconfig.screen_brightness = 1; - io.digitalWrite(PCA_PIN_EINK_EN, HIGH); - } - saveUIConfig(); + if (uiconfig.screen_brightness > 0) { + uiconfig.screen_brightness = 0; + io.digitalWrite(PCA_PIN_EINK_EN, LOW); + } else { + uiconfig.screen_brightness = 1; + io.digitalWrite(PCA_PIN_EINK_EN, HIGH); + } + saveUIConfig(); #endif - } else if (selected == Sleep) { - screen->setOn(false); - } else if (selected == Position) { - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); - } else { - IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); - } - } else if (selected == Preset) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (selected == Freetext) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); - } - }; - screen->showOverlayBanner(bannerOptions); + } else if (selected == Sleep) { + screen->setOn(false); + } else if (selected == Position) { + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } + } else if (selected == Preset) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (selected == Freetext) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::textMessageMenu() -{ - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); +void menuHandler::textMessageMenu() { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } + +void menuHandler::textMessageBaseMenu() { + enum optionsNumbers { Back, Preset, Freetext, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + optionsArray[options] = "New Preset Msg"; + optionsEnumArray[options++] = Preset; + if (kb_found) { + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Preset) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (selected == Freetext) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::textMessageBaseMenu() -{ - enum optionsNumbers { Back, Preset, Freetext, enumEnd }; +void menuHandler::systemBaseMenu() { + enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; + optionsArray[options] = "Notifications"; + optionsEnumArray[options++] = Notifications; + + optionsArray[options] = "Display Options"; + optionsEnumArray[options++] = ScreenOptions; + + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Bluetooth"; + } else { + optionsArray[options] = "Bluetooth Toggle"; + } + optionsEnumArray[options++] = Bluetooth; +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + optionsArray[options] = "WiFi Toggle"; + optionsEnumArray[options++] = WiFiToggle; +#endif + + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Power"; + } else { + optionsArray[options] = "Reboot/Shutdown"; + } + optionsEnumArray[options++] = PowerMenu; + + if (test_enabled) { + optionsArray[options] = "Test Menu"; + optionsEnumArray[options++] = Test; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "System Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "System"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Notifications) { + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + screen->runNow(); + } else if (selected == ScreenOptions) { + menuHandler::menuQueue = menuHandler::screen_options_menu; + screen->runNow(); + } else if (selected == PowerMenu) { + menuHandler::menuQueue = menuHandler::power_menu; + screen->runNow(); + } else if (selected == Test) { + menuHandler::menuQueue = menuHandler::test_menu; + screen->runNow(); + } else if (selected == Bluetooth) { + menuQueue = bluetooth_toggle_menu; + screen->runNow(); +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + } else if (selected == WiFiToggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); +#endif + } else if (selected == Back && !test_enabled) { + test_count++; + if (test_count > 4) { + test_enabled = true; + } + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::favoriteBaseMenu() { + enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + // Only show "View Conversation" if a message exists with this node + uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; + bool hasConversation = false; + for (const auto &m : messageStore.getMessages()) { + if ((m.sender == peer || m.dest == peer)) { + hasConversation = true; + break; + } + } + if (hasConversation) { + optionsArray[options] = "Go To Chat"; + optionsEnumArray[options++] = GoToChat; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "New Preset"; + } else { optionsArray[options] = "New Preset Msg"; - optionsEnumArray[options++] = Preset; - if (kb_found) { - optionsArray[options] = "New Freetext Msg"; - optionsEnumArray[options++] = Freetext; - } + } + optionsEnumArray[options++] = Preset; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Message Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Preset) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (selected == Freetext) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); - } - }; - screen->showOverlayBanner(bannerOptions); -} + if (kb_found) { + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; + } -void menuHandler::systemBaseMenu() -{ - enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - optionsArray[options] = "Notifications"; - optionsEnumArray[options++] = Notifications; - - optionsArray[options] = "Display Options"; - optionsEnumArray[options++] = ScreenOptions; - - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "Bluetooth"; - } else { - optionsArray[options] = "Bluetooth Toggle"; - } - optionsEnumArray[options++] = Bluetooth; -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - optionsArray[options] = "WiFi Toggle"; - optionsEnumArray[options++] = WiFiToggle; -#endif - - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "Power"; - } else { - optionsArray[options] = "Reboot/Shutdown"; - } - optionsEnumArray[options++] = PowerMenu; - - if (test_enabled) { - optionsArray[options] = "Test Menu"; - optionsEnumArray[options++] = Test; - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "System Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "System"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; - screen->runNow(); - } else if (selected == ScreenOptions) { - menuHandler::menuQueue = menuHandler::screen_options_menu; - screen->runNow(); - } else if (selected == PowerMenu) { - menuHandler::menuQueue = menuHandler::power_menu; - screen->runNow(); - } else if (selected == Test) { - menuHandler::menuQueue = menuHandler::test_menu; - screen->runNow(); - } else if (selected == Bluetooth) { - menuQueue = bluetooth_toggle_menu; - screen->runNow(); -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - } else if (selected == WiFiToggle) { - menuQueue = wifi_toggle_menu; - screen->runNow(); -#endif - } else if (selected == Back && !test_enabled) { - test_count++; - if (test_count > 4) { - test_enabled = true; - } - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::favoriteBaseMenu() -{ - enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; - - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - // Only show "View Conversation" if a message exists with this node - uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; - bool hasConversation = false; - for (const auto &m : messageStore.getMessages()) { - if ((m.sender == peer || m.dest == peer)) { - hasConversation = true; - break; - } - } - if (hasConversation) { - optionsArray[options] = "Go To Chat"; - optionsEnumArray[options++] = GoToChat; - } - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "New Preset"; - } else { - optionsArray[options] = "New Preset Msg"; - } - optionsEnumArray[options++] = Preset; - - if (kb_found) { - optionsArray[options] = "New Freetext Msg"; - optionsEnumArray[options++] = Freetext; - } - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; - } - optionsArray[options] = "Remove Favorite"; - optionsEnumArray[options++] = Remove; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Favorites Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Favorites"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Preset) { - cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } else if (selected == Freetext) { - cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } - // Handle new Go To Thread action - else if (selected == GoToChat) { - // Switch thread to direct conversation with this node - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, - graphics::UIRenderer::currentFavoriteNodeNum); - - // Manually create and send a UIFrameEvent to trigger the jump - UIFrameEvent evt; - evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; - screen->handleUIFrameEvent(&evt); - } else if (selected == Remove) { - menuHandler::menuQueue = menuHandler::remove_favorite; - screen->runNow(); - } else if (selected == TraceRoute) { - if (traceRouteModule) { - traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); - } - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::positionBaseMenu() -{ - enum class PositionAction { - GpsToggle, - GpsFormat, - CompassMenu, - CompassCalibrate, - GPSSmartPosition, - GPSUpdateInterval, - GPSPositionBroadcast - }; - - static const PositionMenuOption baseOptions[] = { - {"Back", OptionsAction::Back}, - {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, - {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, - {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, - {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, - {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, - {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, - }; - - static const PositionMenuOption calibrateOptions[] = { - {"Back", OptionsAction::Back}, - {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, - {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, - {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, - {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, - {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, - {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, - {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, - }; - - constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); - constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); - static std::array baseLabels{}; - static std::array calibrateLabels{}; - - auto onSelection = [](const PositionMenuOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - return; - } - - if (!option.hasValue) { - return; - } - - auto action = static_cast(option.value); - switch (action) { - case PositionAction::GpsToggle: - menuQueue = gps_toggle_menu; - screen->runNow(); - break; - case PositionAction::GpsFormat: - menuQueue = gps_format_menu; - screen->runNow(); - break; - case PositionAction::CompassMenu: - menuQueue = compass_point_north_menu; - screen->runNow(); - break; - case PositionAction::CompassCalibrate: - if (accelerometerThread) { - accelerometerThread->calibrate(30); - } - break; - case PositionAction::GPSSmartPosition: - menuQueue = gps_smart_position_menu; - screen->runNow(); - break; - case PositionAction::GPSUpdateInterval: - menuQueue = gps_update_interval_menu; - screen->runNow(); - break; - case PositionAction::GPSPositionBroadcast: - menuQueue = gps_position_broadcast_menu; - screen->runNow(); - break; - } - }; - - BannerOverlayOptions bannerOptions; - if (accelerometerThread) { - bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); - } else { - bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); - } - - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::nodeListMenu() -{ - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - optionsArray[options] = "Add Favorite"; - optionsEnumArray[options++] = Favorite; + if (currentResolution != ScreenResolution::UltraLow) { optionsArray[options] = "Trace Route"; optionsEnumArray[options++] = TraceRoute; + } + optionsArray[options] = "Remove Favorite"; + optionsEnumArray[options++] = Remove; - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Key Verification"; - optionsEnumArray[options++] = Verify; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Favorites Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Favorites"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Preset) { + cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if (selected == Freetext) { + cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); } + // Handle new Go To Thread action + else if (selected == GoToChat) { + // Switch thread to direct conversation with this node + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, graphics::UIRenderer::currentFavoriteNodeNum); - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Show Long/Short Name"; - optionsEnumArray[options++] = NodeNameLength; + // Manually create and send a UIFrameEvent to trigger the jump + UIFrameEvent evt; + evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + screen->handleUIFrameEvent(&evt); + } else if (selected == Remove) { + menuHandler::menuQueue = menuHandler::remove_favorite; + screen->runNow(); + } else if (selected == TraceRoute) { + if (traceRouteModule) { + traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); + } } - optionsArray[options] = "Reset NodeDB"; - optionsEnumArray[options++] = Reset; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Node Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Favorite) { - menuQueue = add_favorite; - screen->runNow(); - } else if (selected == Verify) { - menuQueue = key_verification_init; - screen->runNow(); - } else if (selected == Reset) { - menuQueue = reset_node_db_menu; - screen->runNow(); - } else if (selected == TraceRoute) { - menuQueue = trace_route_menu; - screen->runNow(); - } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::nodeNameLengthMenu() -{ - static const NodeNameOption nodeNameOptions[] = { - {"Back", OptionsAction::Back}, - {"Long", OptionsAction::Select, true}, - {"Short", OptionsAction::Select, false}, - }; +void menuHandler::positionBaseMenu() { + enum class PositionAction { GpsToggle, GpsFormat, CompassMenu, CompassCalibrate, GPSSmartPosition, GPSUpdateInterval, GPSPositionBroadcast }; - constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); - static std::array nodeNameLabels{}; + static const PositionMenuOption baseOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + }; - auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, - [](const NodeNameOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = node_base_menu; - screen->runNow(); - return; - } + static const PositionMenuOption calibrateOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, + }; - if (!option.hasValue) { - return; - } + constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); + static std::array baseLabels{}; + static std::array calibrateLabels{}; - if (config.display.use_long_node_name == option.value) { - return; - } - - config.display.use_long_node_name = option.value; - LOG_INFO("Setting names to %s", option.value ? "long" : "short"); - }); - - int initialSelection = config.display.use_long_node_name ? 1 : 2; - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::resetNodeDBMenu() -{ - static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Confirm Reset NodeDB"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1 || selected == 2) { - disableBluetooth(); - screen->setFrames(Screen::FOCUS_DEFAULT); - } - if (selected == 1) { - LOG_INFO("Initiate node-db reset"); - nodeDB->resetNodes(); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == 2) { - LOG_INFO("Initiate node-db reset but keeping favorites"); - nodeDB->resetNodes(1); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == 0) { - menuQueue = node_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::compassNorthMenu() -{ - static const CompassOption compassOptions[] = { - {"Back", OptionsAction::Back}, - {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, - {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, - {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, - }; - - constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); - static std::array compassLabels{}; - - auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, - [](const CompassOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (uiconfig.compass_mode == option.value) { - return; - } - - uiconfig.compass_mode = option.value; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); - - int initialSelection = 0; - for (size_t i = 0; i < compassCount; ++i) { - if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { - initialSelection = static_cast(i); - break; - } + auto onSelection = [](const PositionMenuOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + return; } - bannerOptions.InitialSelected = initialSelection; - screen->showOverlayBanner(bannerOptions); + if (!option.hasValue) { + return; + } + + auto action = static_cast(option.value); + switch (action) { + case PositionAction::GpsToggle: + menuQueue = gps_toggle_menu; + screen->runNow(); + break; + case PositionAction::GpsFormat: + menuQueue = gps_format_menu; + screen->runNow(); + break; + case PositionAction::CompassMenu: + menuQueue = compass_point_north_menu; + screen->runNow(); + break; + case PositionAction::CompassCalibrate: + if (accelerometerThread) { + accelerometerThread->calibrate(30); + } + break; + case PositionAction::GPSSmartPosition: + menuQueue = gps_smart_position_menu; + screen->runNow(); + break; + case PositionAction::GPSUpdateInterval: + menuQueue = gps_update_interval_menu; + screen->runNow(); + break; + case PositionAction::GPSPositionBroadcast: + menuQueue = gps_position_broadcast_menu; + screen->runNow(); + break; + } + }; + + BannerOverlayOptions bannerOptions; + if (accelerometerThread) { + bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); + } else { + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); + } + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::nodeListMenu() { + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + optionsArray[options] = "Add Favorite"; + optionsEnumArray[options++] = Favorite; + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = Verify; + } + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Show Long/Short Name"; + optionsEnumArray[options++] = NodeNameLength; + } + optionsArray[options] = "Reset NodeDB"; + optionsEnumArray[options++] = Reset; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Node Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Favorite) { + menuQueue = add_favorite; + screen->runNow(); + } else if (selected == Verify) { + menuQueue = key_verification_init; + screen->runNow(); + } else if (selected == Reset) { + menuQueue = reset_node_db_menu; + screen->runNow(); + } else if (selected == TraceRoute) { + menuQueue = trace_route_menu; + screen->runNow(); + } else if (selected == NodeNameLength) { + menuHandler::menuQueue = menuHandler::node_name_length_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::nodeNameLengthMenu() { + static const NodeNameOption nodeNameOptions[] = { + {"Back", OptionsAction::Back}, + {"Long", OptionsAction::Select, true}, + {"Short", OptionsAction::Select, false}, + }; + + constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); + static std::array nodeNameLabels{}; + + auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, [](const NodeNameOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.display.use_long_node_name == option.value) { + return; + } + + config.display.use_long_node_name = option.value; + LOG_INFO("Setting names to %s", option.value ? "long" : "short"); + }); + + int initialSelection = config.display.use_long_node_name ? 1 : 2; + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::resetNodeDBMenu() { + static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Confirm Reset NodeDB"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1 || selected == 2) { + disableBluetooth(); + screen->setFrames(Screen::FOCUS_DEFAULT); + } + if (selected == 1) { + LOG_INFO("Initiate node-db reset"); + nodeDB->resetNodes(); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + LOG_INFO("Initiate node-db reset but keeping favorites"); + nodeDB->resetNodes(1); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 0) { + menuQueue = node_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::compassNorthMenu() { + static const CompassOption compassOptions[] = { + {"Back", OptionsAction::Back}, + {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, + {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, + {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, + }; + + constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); + static std::array compassLabels{}; + + auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, [](const CompassOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.compass_mode == option.value) { + return; + } + + uiconfig.compass_mode = option.value; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); + + int initialSelection = 0; + for (size_t i = 0; i < compassCount; ++i) { + if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); } #if !MESHTASTIC_EXCLUDE_GPS -void menuHandler::GPSToggleMenu() -{ - static const GPSToggleOption gpsToggleOptions[] = { - {"Back", OptionsAction::Back}, - {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, - {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, - }; +void menuHandler::GPSToggleMenu() { + static const GPSToggleOption gpsToggleOptions[] = { + {"Back", OptionsAction::Back}, + {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, + {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, + }; - constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); - static std::array toggleLabels{}; + constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); + static std::array toggleLabels{}; - auto bannerOptions = - createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (config.position.gps_mode == option.value) { - return; - } - - config.position.gps_mode = option.value; - if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - playGPSEnableBeep(); - gps->enable(); - } else { - playGPSDisableBeep(); - gps->disable(); - } - service->reloadConfig(SEGMENT_CONFIG); - }); - - int initialSelection = 0; - for (size_t i = 0; i < toggleCount; ++i) { - if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { - initialSelection = static_cast(i); - break; - } + auto bannerOptions = createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; } - bannerOptions.InitialSelected = initialSelection; - screen->showOverlayBanner(bannerOptions); + if (!option.hasValue) { + return; + } + + if (config.position.gps_mode == option.value) { + return; + } + + config.position.gps_mode = option.value; + if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + playGPSEnableBeep(); + gps->enable(); + } else { + playGPSDisableBeep(); + gps->disable(); + } + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < toggleCount; ++i) { + if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); } -void menuHandler::GPSFormatMenu() -{ - static const GPSFormatOption formatOptionsHigh[] = { - {"Back", OptionsAction::Back}, - {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, - {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, - {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, - {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, - {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, - {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, - {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, - }; +void menuHandler::GPSFormatMenu() { + static const GPSFormatOption formatOptionsHigh[] = { + {"Back", OptionsAction::Back}, + {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; - static const GPSFormatOption formatOptionsLow[] = { - {"Back", OptionsAction::Back}, - {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, - {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, - {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, - {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, - {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, - {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, - {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, - }; + static const GPSFormatOption formatOptionsLow[] = { + {"Back", OptionsAction::Back}, + {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; - constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); - static std::array formatLabelsHigh{}; - static std::array formatLabelsLow{}; + constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); + static std::array formatLabelsHigh{}; + static std::array formatLabelsLow{}; - auto onSelection = [](const GPSFormatOption &option, int) -> void { + auto onSelection = [](const GPSFormatOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.gps_format == option.value) { + return; + } + + uiconfig.gps_format = option.value; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + }; + + BannerOverlayOptions bannerOptions; + int initialSelection = 0; + + if (currentResolution == ScreenResolution::High) { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { + initialSelection = static_cast(i); + break; + } + } + } else { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { + initialSelection = static_cast(i); + break; + } + } + } + + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSSmartPositionMenu() { + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Smart Position"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Smrt Postn"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_smart_enabled = true; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + config.position.position_broadcast_smart_enabled = false; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSUpdateIntervalMenu() { + static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", "2 minutes", "5 minutes", + "10 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "24 hours", "At Boot Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Update Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 16; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.gps_update_interval = 8; + } else if (selected == 2) { + config.position.gps_update_interval = 20; + } else if (selected == 3) { + config.position.gps_update_interval = 40; + } else if (selected == 4) { + config.position.gps_update_interval = 60; + } else if (selected == 5) { + config.position.gps_update_interval = 80; + } else if (selected == 6) { + config.position.gps_update_interval = 120; + } else if (selected == 7) { + config.position.gps_update_interval = 300; + } else if (selected == 8) { + config.position.gps_update_interval = 600; + } else if (selected == 9) { + config.position.gps_update_interval = 900; + } else if (selected == 10) { + config.position.gps_update_interval = 1800; + } else if (selected == 11) { + config.position.gps_update_interval = 3600; + } else if (selected == 12) { + config.position.gps_update_interval = 21600; + } else if (selected == 13) { + config.position.gps_update_interval = 43200; + } else if (selected == 14) { + config.position.gps_update_interval = 86400; + } else if (selected == 15) { + config.position.gps_update_interval = 2147483647; // At Boot Only + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.gps_update_interval == 8) { + bannerOptions.InitialSelected = 1; + } else if (config.position.gps_update_interval == 20) { + bannerOptions.InitialSelected = 2; + } else if (config.position.gps_update_interval == 40) { + bannerOptions.InitialSelected = 3; + } else if (config.position.gps_update_interval == 60) { + bannerOptions.InitialSelected = 4; + } else if (config.position.gps_update_interval == 80) { + bannerOptions.InitialSelected = 5; + } else if (config.position.gps_update_interval == 120) { + bannerOptions.InitialSelected = 6; + } else if (config.position.gps_update_interval == 300) { + bannerOptions.InitialSelected = 7; + } else if (config.position.gps_update_interval == 600) { + bannerOptions.InitialSelected = 8; + } else if (config.position.gps_update_interval == 900) { + bannerOptions.InitialSelected = 9; + } else if (config.position.gps_update_interval == 1800) { + bannerOptions.InitialSelected = 10; + } else if (config.position.gps_update_interval == 3600) { + bannerOptions.InitialSelected = 11; + } else if (config.position.gps_update_interval == 21600) { + bannerOptions.InitialSelected = 12; + } else if (config.position.gps_update_interval == 43200) { + bannerOptions.InitialSelected = 13; + } else if (config.position.gps_update_interval == 86400) { + bannerOptions.InitialSelected = 14; + } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only + bannerOptions.InitialSelected = 15; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSPositionBroadcastMenu() { + static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", "2 hours", "3 hours", "4 hours", + "5 hours", "6 hours", "12 hours", "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Broadcast Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_secs = 60; + } else if (selected == 2) { + config.position.position_broadcast_secs = 90; + } else if (selected == 3) { + config.position.position_broadcast_secs = 300; + } else if (selected == 4) { + config.position.position_broadcast_secs = 900; + } else if (selected == 5) { + config.position.position_broadcast_secs = 3600; + } else if (selected == 6) { + config.position.position_broadcast_secs = 7200; + } else if (selected == 7) { + config.position.position_broadcast_secs = 10800; + } else if (selected == 8) { + config.position.position_broadcast_secs = 14400; + } else if (selected == 9) { + config.position.position_broadcast_secs = 18000; + } else if (selected == 10) { + config.position.position_broadcast_secs = 21600; + } else if (selected == 11) { + config.position.position_broadcast_secs = 43200; + } else if (selected == 12) { + config.position.position_broadcast_secs = 64800; + } else if (selected == 13) { + config.position.position_broadcast_secs = 86400; + } else if (selected == 14) { + config.position.position_broadcast_secs = 129600; + } else if (selected == 15) { + config.position.position_broadcast_secs = 172800; + } else if (selected == 16) { + config.position.position_broadcast_secs = 259200; + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.position_broadcast_secs == 60) { + bannerOptions.InitialSelected = 1; + } else if (config.position.position_broadcast_secs == 90) { + bannerOptions.InitialSelected = 2; + } else if (config.position.position_broadcast_secs == 300) { + bannerOptions.InitialSelected = 3; + } else if (config.position.position_broadcast_secs == 900) { + bannerOptions.InitialSelected = 4; + } else if (config.position.position_broadcast_secs == 3600) { + bannerOptions.InitialSelected = 5; + } else if (config.position.position_broadcast_secs == 7200) { + bannerOptions.InitialSelected = 6; + } else if (config.position.position_broadcast_secs == 10800) { + bannerOptions.InitialSelected = 7; + } else if (config.position.position_broadcast_secs == 14400) { + bannerOptions.InitialSelected = 8; + } else if (config.position.position_broadcast_secs == 18000) { + bannerOptions.InitialSelected = 9; + } else if (config.position.position_broadcast_secs == 21600) { + bannerOptions.InitialSelected = 10; + } else if (config.position.position_broadcast_secs == 43200) { + bannerOptions.InitialSelected = 11; + } else if (config.position.position_broadcast_secs == 64800) { + bannerOptions.InitialSelected = 12; + } else if (config.position.position_broadcast_secs == 86400) { + bannerOptions.InitialSelected = 13; + } else if (config.position.position_broadcast_secs == 129600) { + bannerOptions.InitialSelected = 14; + } else if (config.position.position_broadcast_secs == 172800) { + bannerOptions.InitialSelected = 15; + } else if (config.position.position_broadcast_secs == 259200) { + bannerOptions.InitialSelected = 16; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +#endif + +void menuHandler::BluetoothToggleMenu() { + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Bluetooth"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Bluetooth"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) + return; + else if (selected != (config.bluetooth.enabled ? 1 : 2)) { + InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + }; + bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BuzzerModeMenu() { + static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Notification Sounds"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; + service->reloadConfig(SEGMENT_CONFIG); + }; + bannerOptions.InitialSelected = config.device.buzzer_mode; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BrightnessPickerMenu() { + static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; + + // Get current brightness level to set initial selection + int currentSelection = 1; // Default to Medium + if (uiconfig.screen_brightness >= 255) { + currentSelection = 3; // Very High + } else if (uiconfig.screen_brightness >= 128) { + currentSelection = 2; // High + } else { + currentSelection = 1; // Medium + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Brightness"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { // Medium + uiconfig.screen_brightness = 64; + } else if (selected == 2) { // High + uiconfig.screen_brightness = 128; + } else if (selected == 3) { // Very High + uiconfig.screen_brightness = 255; + } + + if (selected != 0) { // Not "Back" + // Apply brightness immediately +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) + // For HELTEC devices, use analogWrite to control backlight + analogWrite(VTFT_LEDA, uiconfig.screen_brightness); +#elif defined(ST7789_CS) || defined(ST7796_CS) + static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); +#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); +#endif + + // Save to device + saveUIConfig(); + + LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); + } + }; + bannerOptions.InitialSelected = currentSelection; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::switchToMUIMenu() { + static const char *optionsArray[] = {"No", "Yes"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Switch to MUI?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) { + static const ScreenColorOption colorOptions[] = { + {"Back", OptionsAction::Back}, + {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, + {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, + {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, + {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, + {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, + {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, + {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, + {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, + {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, + {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, + {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, + {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, + {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, + }; + + constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); + static std::array colorLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; - screen->runNow(); - return; + menuQueue = system_base_menu; + screen->runNow(); + return; } if (!option.hasValue) { - return; + return; } - if (uiconfig.gps_format == option.value) { - return; +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT || \ + defined(HACKADAY_COMMUNICATOR) + const ScreenColor &color = option.value; + if (color.useVariant) { + LOG_INFO("Setting color to system default or defined variant"); + } else { + LOG_INFO("Setting color to %s", option.label); } - uiconfig.gps_format = option.value; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - }; + uint8_t r = color.r; + uint8_t g = color.g; + uint8_t b = color.b; - BannerOverlayOptions bannerOptions; - int initialSelection = 0; + display->setColor(BLACK); + display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); - for (size_t i = 0; i < formatCount; ++i) { - if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { - initialSelection = static_cast(i); - break; - } - } - } else { - bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); - for (size_t i = 0; i < formatCount; ++i) { - if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { - initialSelection = static_cast(i); - break; - } - } - } - - bannerOptions.InitialSelected = initialSelection; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::GPSSmartPositionMenu() -{ - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle Smart Position"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Smrt Postn"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuQueue = position_base_menu; - screen->runNow(); - } else if (selected == 1) { - config.position.position_broadcast_smart_enabled = true; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == 2) { - config.position.position_broadcast_smart_enabled = false; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::GPSUpdateIntervalMenu() -{ - static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", - "2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", - "6 hours", "12 hours", "24 hours", "At Boot Only"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Update Interval"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 16; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuQueue = position_base_menu; - screen->runNow(); - } else if (selected == 1) { - config.position.gps_update_interval = 8; - } else if (selected == 2) { - config.position.gps_update_interval = 20; - } else if (selected == 3) { - config.position.gps_update_interval = 40; - } else if (selected == 4) { - config.position.gps_update_interval = 60; - } else if (selected == 5) { - config.position.gps_update_interval = 80; - } else if (selected == 6) { - config.position.gps_update_interval = 120; - } else if (selected == 7) { - config.position.gps_update_interval = 300; - } else if (selected == 8) { - config.position.gps_update_interval = 600; - } else if (selected == 9) { - config.position.gps_update_interval = 900; - } else if (selected == 10) { - config.position.gps_update_interval = 1800; - } else if (selected == 11) { - config.position.gps_update_interval = 3600; - } else if (selected == 12) { - config.position.gps_update_interval = 21600; - } else if (selected == 13) { - config.position.gps_update_interval = 43200; - } else if (selected == 14) { - config.position.gps_update_interval = 86400; - } else if (selected == 15) { - config.position.gps_update_interval = 2147483647; // At Boot Only - } - - if (selected != 0) { - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - - if (config.position.gps_update_interval == 8) { - bannerOptions.InitialSelected = 1; - } else if (config.position.gps_update_interval == 20) { - bannerOptions.InitialSelected = 2; - } else if (config.position.gps_update_interval == 40) { - bannerOptions.InitialSelected = 3; - } else if (config.position.gps_update_interval == 60) { - bannerOptions.InitialSelected = 4; - } else if (config.position.gps_update_interval == 80) { - bannerOptions.InitialSelected = 5; - } else if (config.position.gps_update_interval == 120) { - bannerOptions.InitialSelected = 6; - } else if (config.position.gps_update_interval == 300) { - bannerOptions.InitialSelected = 7; - } else if (config.position.gps_update_interval == 600) { - bannerOptions.InitialSelected = 8; - } else if (config.position.gps_update_interval == 900) { - bannerOptions.InitialSelected = 9; - } else if (config.position.gps_update_interval == 1800) { - bannerOptions.InitialSelected = 10; - } else if (config.position.gps_update_interval == 3600) { - bannerOptions.InitialSelected = 11; - } else if (config.position.gps_update_interval == 21600) { - bannerOptions.InitialSelected = 12; - } else if (config.position.gps_update_interval == 43200) { - bannerOptions.InitialSelected = 13; - } else if (config.position.gps_update_interval == 86400) { - bannerOptions.InitialSelected = 14; - } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only - bannerOptions.InitialSelected = 15; - } else { - bannerOptions.InitialSelected = 0; - } - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::GPSPositionBroadcastMenu() -{ - static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", - "2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours", - "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Broadcast Interval"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 17; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuQueue = position_base_menu; - screen->runNow(); - } else if (selected == 1) { - config.position.position_broadcast_secs = 60; - } else if (selected == 2) { - config.position.position_broadcast_secs = 90; - } else if (selected == 3) { - config.position.position_broadcast_secs = 300; - } else if (selected == 4) { - config.position.position_broadcast_secs = 900; - } else if (selected == 5) { - config.position.position_broadcast_secs = 3600; - } else if (selected == 6) { - config.position.position_broadcast_secs = 7200; - } else if (selected == 7) { - config.position.position_broadcast_secs = 10800; - } else if (selected == 8) { - config.position.position_broadcast_secs = 14400; - } else if (selected == 9) { - config.position.position_broadcast_secs = 18000; - } else if (selected == 10) { - config.position.position_broadcast_secs = 21600; - } else if (selected == 11) { - config.position.position_broadcast_secs = 43200; - } else if (selected == 12) { - config.position.position_broadcast_secs = 64800; - } else if (selected == 13) { - config.position.position_broadcast_secs = 86400; - } else if (selected == 14) { - config.position.position_broadcast_secs = 129600; - } else if (selected == 15) { - config.position.position_broadcast_secs = 172800; - } else if (selected == 16) { - config.position.position_broadcast_secs = 259200; - } - - if (selected != 0) { - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - - if (config.position.position_broadcast_secs == 60) { - bannerOptions.InitialSelected = 1; - } else if (config.position.position_broadcast_secs == 90) { - bannerOptions.InitialSelected = 2; - } else if (config.position.position_broadcast_secs == 300) { - bannerOptions.InitialSelected = 3; - } else if (config.position.position_broadcast_secs == 900) { - bannerOptions.InitialSelected = 4; - } else if (config.position.position_broadcast_secs == 3600) { - bannerOptions.InitialSelected = 5; - } else if (config.position.position_broadcast_secs == 7200) { - bannerOptions.InitialSelected = 6; - } else if (config.position.position_broadcast_secs == 10800) { - bannerOptions.InitialSelected = 7; - } else if (config.position.position_broadcast_secs == 14400) { - bannerOptions.InitialSelected = 8; - } else if (config.position.position_broadcast_secs == 18000) { - bannerOptions.InitialSelected = 9; - } else if (config.position.position_broadcast_secs == 21600) { - bannerOptions.InitialSelected = 10; - } else if (config.position.position_broadcast_secs == 43200) { - bannerOptions.InitialSelected = 11; - } else if (config.position.position_broadcast_secs == 64800) { - bannerOptions.InitialSelected = 12; - } else if (config.position.position_broadcast_secs == 86400) { - bannerOptions.InitialSelected = 13; - } else if (config.position.position_broadcast_secs == 129600) { - bannerOptions.InitialSelected = 14; - } else if (config.position.position_broadcast_secs == 172800) { - bannerOptions.InitialSelected = 15; - } else if (config.position.position_broadcast_secs == 259200) { - bannerOptions.InitialSelected = 16; - } else { - bannerOptions.InitialSelected = 0; - } - screen->showOverlayBanner(bannerOptions); -} - -#endif - -void menuHandler::BluetoothToggleMenu() -{ - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle Bluetooth"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Bluetooth"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) - return; - else if (selected != (config.bluetooth.enabled ? 1 : 2)) { - InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } - }; - bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::BuzzerModeMenu() -{ - static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Notification Sounds"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 5; - bannerOptions.bannerCallback = [](int selected) -> void { - config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; - service->reloadConfig(SEGMENT_CONFIG); - }; - bannerOptions.InitialSelected = config.device.buzzer_mode; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::BrightnessPickerMenu() -{ - static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; - - // Get current brightness level to set initial selection - int currentSelection = 1; // Default to Medium - if (uiconfig.screen_brightness >= 255) { - currentSelection = 3; // Very High - } else if (uiconfig.screen_brightness >= 128) { - currentSelection = 2; // High - } else { - currentSelection = 1; // Medium - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Brightness"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { // Medium - uiconfig.screen_brightness = 64; - } else if (selected == 2) { // High - uiconfig.screen_brightness = 128; - } else if (selected == 3) { // Very High - uiconfig.screen_brightness = 255; - } - - if (selected != 0) { // Not "Back" - // Apply brightness immediately -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) - // For HELTEC devices, use analogWrite to control backlight - analogWrite(VTFT_LEDA, uiconfig.screen_brightness); -#elif defined(ST7789_CS) || defined(ST7796_CS) - static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); -#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); -#endif - - // Save to device - saveUIConfig(); - - LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); - } - }; - bannerOptions.InitialSelected = currentSelection; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::switchToMUIMenu() -{ - static const char *optionsArray[] = {"No", "Yes"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Switch to MUI?"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - config.bluetooth.enabled = false; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) -{ - static const ScreenColorOption colorOptions[] = { - {"Back", OptionsAction::Back}, - {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, - {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, - {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, - {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, - {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, - {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, - {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, - {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, - {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, - {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, - {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, - {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, - {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, - }; - - constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); - static std::array colorLabels{}; - - auto bannerOptions = createStaticBannerOptions( - "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = system_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ - HAS_TFT || defined(HACKADAY_COMMUNICATOR) - const ScreenColor &color = option.value; - if (color.useVariant) { - LOG_INFO("Setting color to system default or defined variant"); - } else { - LOG_INFO("Setting color to %s", option.label); - } - - uint8_t r = color.r; - uint8_t g = color.g; - uint8_t b = color.b; - - display->setColor(BLACK); - display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - display->setColor(WHITE); - - if (color.useVariant || (r == 0 && g == 0 && b == 0)) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { #ifdef TFT_MESH_OVERRIDE - TFT_MESH = TFT_MESH_OVERRIDE; + TFT_MESH = TFT_MESH_OVERRIDE; #else TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif - } else { - TFT_MESH = COLOR565(r, g, b); - } + } else { + TFT_MESH = COLOR565(r, g, b); + } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) - static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); + static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); #endif - screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (color.useVariant || (r == 0 && g == 0 && b == 0)) { - uiconfig.screen_rgb_color = 0; - } else { - uiconfig.screen_rgb_color = - (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); - } - LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); - saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_SYSTEM); + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { + uiconfig.screen_rgb_color = 0; + } else { + uiconfig.screen_rgb_color = (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); + } + LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); + saveUIConfig(); #endif - }); + }); - int initialSelection = 0; - if (uiconfig.screen_rgb_color == 0) { - initialSelection = 1; + int initialSelection = 0; + if (uiconfig.screen_rgb_color == 0) { + initialSelection = 1; + } else { + uint32_t currentColor = uiconfig.screen_rgb_color; + for (size_t i = 0; i < colorCount; ++i) { + if (!colorOptions[i].hasValue) { + continue; + } + const ScreenColor &color = colorOptions[i].value; + if (color.useVariant) { + continue; + } + uint32_t encoded = (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); + if (encoded == currentColor) { + initialSelection = static_cast(i); + break; + } + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::rebootMenu() { + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot Device?"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Reboot"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); + messageStore.saveToFlash(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { - uint32_t currentColor = uiconfig.screen_rgb_color; - for (size_t i = 0; i < colorCount; ++i) { - if (!colorOptions[i].hasValue) { - continue; - } - const ScreenColor &color = colorOptions[i].value; - if (color.useVariant) { - continue; - } - uint32_t encoded = - (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); - if (encoded == currentColor) { - initialSelection = static_cast(i); - break; - } - } + menuQueue = power_menu; + screen->runNow(); } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::rebootMenu() -{ - static const char *optionsArray[] = {"Back", "Confirm"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Reboot Device?"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Reboot"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); - nodeDB->saveToDisk(); - messageStore.saveToFlash(); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } else { - menuQueue = power_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::shutdownMenu() -{ - static const char *optionsArray[] = {"Back", "Confirm"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Shutdown Device?"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Shutdown"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else { - menuQueue = power_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::addFavoriteMenu() -{ - const char *NODE_PICKER_TITLE; - if (currentResolution == ScreenResolution::UltraLow) { - NODE_PICKER_TITLE = "Node Favorite"; +void menuHandler::shutdownMenu() { + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Shutdown Device?"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Shutdown"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); } else { - NODE_PICKER_TITLE = "Node To Favorite"; + menuQueue = power_menu; + screen->runNow(); } - screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { - LOG_WARN("Nodenum: %u", nodenum); - nodeDB->set_favorite(true, nodenum); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::removeFavoriteMenu() -{ +void menuHandler::addFavoriteMenu() { + const char *NODE_PICKER_TITLE; + if (currentResolution == ScreenResolution::UltraLow) { + NODE_PICKER_TITLE = "Node Favorite"; + } else { + NODE_PICKER_TITLE = "Node To Favorite"; + } + screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { + LOG_WARN("Nodenum: %u", nodenum); + nodeDB->set_favorite(true, nodenum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); +} - static const char *optionsArray[] = {"Back", "Yes"}; - BannerOverlayOptions bannerOptions; - std::string message = "Unfavorite This Node?\n"; - auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); - if (node && node->has_user) { - message += sanitizeString(node->user.long_name).substr(0, 15); +void menuHandler::removeFavoriteMenu() { + + static const char *optionsArray[] = {"Back", "Yes"}; + BannerOverlayOptions bannerOptions; + std::string message = "Unfavorite This Node?\n"; + auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); + if (node && node->has_user) { + message += sanitizeString(node->user.long_name).substr(0, 15); + } + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum); + nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); + screen->setFrames(graphics::Screen::FOCUS_DEFAULT); } - bannerOptions.message = message.c_str(); - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum); - nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); - screen->setFrames(graphics::Screen::FOCUS_DEFAULT); - } - }; - screen->showOverlayBanner(bannerOptions); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::traceRouteMenu() -{ - screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { - LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); - if (traceRouteModule) { - traceRouteModule->startTraceRoute(nodenum); - } - }); +void menuHandler::traceRouteMenu() { + screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(nodenum); + } + }); } -void menuHandler::testMenu() -{ +void menuHandler::testMenu() { - enum optionsNumbers { Back, NumberPicker, ShowChirpy }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; - int options = 1; + enum optionsNumbers { Back, NumberPicker, ShowChirpy }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; - optionsArray[options] = "Number Picker"; - optionsEnumArray[options++] = NumberPicker; + optionsArray[options] = "Number Picker"; + optionsEnumArray[options++] = NumberPicker; - optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; - optionsEnumArray[options++] = ShowChirpy; + optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; + optionsEnumArray[options++] = ShowChirpy; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Hidden Test Menu"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == NumberPicker) { - menuQueue = number_test; - screen->runNow(); - } else if (selected == ShowChirpy) { - screen->toggleFrameVisibility("chirpy"); - screen->setFrames(Screen::FOCUS_SYSTEM); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Hidden Test Menu"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == NumberPicker) { + menuQueue = number_test; + screen->runNow(); + } else if (selected == ShowChirpy) { + screen->toggleFrameVisibility("chirpy"); + screen->setFrames(Screen::FOCUS_SYSTEM); - } else { - menuQueue = system_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + } else { + menuQueue = system_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::numberTest() -{ - screen->showNumberPicker("Pick a number\n ", 30000, 4, - [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); +void menuHandler::numberTest() { + screen->showNumberPicker("Pick a number\n ", 30000, 4, [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); } -void menuHandler::wifiBaseMenu() -{ - enum optionsNumbers { Back, Wifi_toggle }; +void menuHandler::wifiBaseMenu() { + enum optionsNumbers { Back, Wifi_toggle }; - static const char *optionsArray[] = {"Back", "WiFi Toggle"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "WiFi Menu"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Wifi_toggle) { - menuQueue = wifi_toggle_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + static const char *optionsArray[] = {"Back", "WiFi Toggle"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "WiFi Menu"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_toggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::wifiToggleMenu() -{ - enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; +void menuHandler::wifiToggleMenu() { + enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; - static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "WiFi Actions"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - if (config.network.wifi_enabled == true) - bannerOptions.InitialSelected = 2; - else - bannerOptions.InitialSelected = 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Wifi_disable) { - config.network.wifi_enabled = false; - config.bluetooth.enabled = true; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == Wifi_enable) { - config.network.wifi_enabled = true; - config.bluetooth.enabled = false; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - screen->showOverlayBanner(bannerOptions); + static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "WiFi Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + if (config.network.wifi_enabled == true) + bannerOptions.InitialSelected = 2; + else + bannerOptions.InitialSelected = 1; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_disable) { + config.network.wifi_enabled = false; + config.bluetooth.enabled = true; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == Wifi_enable) { + config.network.wifi_enabled = true; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::screenOptionsMenu() -{ - // Check if brightness is supported - bool hasSupportBrightness = false; +void menuHandler::screenOptionsMenu() { + // Check if brightness is supported + bool hasSupportBrightness = false; #if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - hasSupportBrightness = true; + hasSupportBrightness = true; #endif #if defined(T_DECK) - // TDeck Doesn't seem to support brightness at all, at least not reliably - hasSupportBrightness = false; + // TDeck Doesn't seem to support brightness at all, at least not reliably + hasSupportBrightness = false; #endif - enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; - static const char *optionsArray[5] = {"Back"}; - static int optionsEnumArray[5] = {Back}; - int options = 1; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; + static const char *optionsArray[5] = {"Back"}; + static int optionsEnumArray[5] = {Back}; + int options = 1; - // Only show brightness for B&W displays - if (hasSupportBrightness) { - optionsArray[options] = "Brightness"; - optionsEnumArray[options++] = Brightness; + // Only show brightness for B&W displays + if (hasSupportBrightness) { + optionsArray[options] = "Brightness"; + optionsEnumArray[options++] = Brightness; + } + + // Only show screen color for TFT displays +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT || \ + defined(HACKADAY_COMMUNICATOR) + optionsArray[options] = "Screen Color"; + optionsEnumArray[options++] = ScreenColor; +#endif + + optionsArray[options] = "Frame Visibility"; + optionsEnumArray[options++] = FrameToggles; + + optionsArray[options] = "Display Units"; + optionsEnumArray[options++] = DisplayUnits; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Display Options"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Brightness) { + menuHandler::menuQueue = menuHandler::brightness_picker; + screen->runNow(); + } else if (selected == ScreenColor) { + menuHandler::menuQueue = menuHandler::tftcolormenupicker; + screen->runNow(); + } else if (selected == FrameToggles) { + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == DisplayUnits) { + menuHandler::menuQueue = menuHandler::DisplayUnits; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); } - - // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ - HAS_TFT || defined(HACKADAY_COMMUNICATOR) - optionsArray[options] = "Screen Color"; - optionsEnumArray[options++] = ScreenColor; -#endif - - optionsArray[options] = "Frame Visibility"; - optionsEnumArray[options++] = FrameToggles; - - optionsArray[options] = "Display Units"; - optionsEnumArray[options++] = DisplayUnits; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Display Options"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Brightness) { - menuHandler::menuQueue = menuHandler::brightness_picker; - screen->runNow(); - } else if (selected == ScreenColor) { - menuHandler::menuQueue = menuHandler::tftcolormenupicker; - screen->runNow(); - } else if (selected == FrameToggles) { - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == DisplayUnits) { - menuHandler::menuQueue = menuHandler::DisplayUnits; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::powerMenu() -{ +void menuHandler::powerMenu() { - enum optionsNumbers { Back, Reboot, Shutdown, MUI }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; - int options = 1; + enum optionsNumbers { Back, Reboot, Shutdown, MUI }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; - optionsArray[options] = "Reboot"; - optionsEnumArray[options++] = Reboot; + optionsArray[options] = "Reboot"; + optionsEnumArray[options++] = Reboot; - optionsArray[options] = "Shutdown"; - optionsEnumArray[options++] = Shutdown; + optionsArray[options] = "Shutdown"; + optionsEnumArray[options++] = Shutdown; #if HAS_TFT - optionsArray[options] = "Switch to MUI"; - optionsEnumArray[options++] = MUI; + optionsArray[options] = "Switch to MUI"; + optionsEnumArray[options++] = MUI; #endif - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Reboot / Shutdown"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Power"; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot / Shutdown"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Power"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Reboot) { + menuHandler::menuQueue = menuHandler::reboot_menu; + screen->runNow(); + } else if (selected == Shutdown) { + menuHandler::menuQueue = menuHandler::shutdown_menu; + screen->runNow(); + } else if (selected == MUI) { + menuHandler::menuQueue = menuHandler::mui_picker; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Reboot) { - menuHandler::menuQueue = menuHandler::reboot_menu; - screen->runNow(); - } else if (selected == Shutdown) { - menuHandler::menuQueue = menuHandler::shutdown_menu; - screen->runNow(); - } else if (selected == MUI) { - menuHandler::menuQueue = menuHandler::mui_picker; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); - } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::keyVerificationInitMenu() { + screen->showNodePicker("Node to Verify", 30000, [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); }); +} + +void menuHandler::keyVerificationFinalPrompt() { + char message[40] = {0}; + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet + + if (screen) { + static const char *optionsArray[] = {"Reject", "Accept"}; + graphics::BannerOverlayOptions options; + options.message = message; + options.durationMs = 30000; + options.optionsArrayPtr = optionsArray; + options.optionsCount = 2; + options.notificationType = graphics::notificationTypeEnum::selection_picker; + options.bannerCallback = [=](int selected) { + if (selected == 1) { + auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode()); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } }; - screen->showOverlayBanner(bannerOptions); + screen->showOverlayBanner(options); + } } -void menuHandler::keyVerificationInitMenu() -{ - screen->showNodePicker("Node to Verify", 30000, - [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); }); -} +void menuHandler::FrameToggles_menu() { + enum optionsNumbers { + Finish, + nodelist_nodes, + nodelist_location, + nodelist_lastheard, + nodelist_hopsignal, + nodelist_distance, + nodelist_bearings, + gps, + lora, + clock, + show_favorites, + show_telemetry, + show_power, + enumEnd + }; + static const char *optionsArray[enumEnd] = {"Finish"}; + static int optionsEnumArray[enumEnd] = {Finish}; + int options = 1; -void menuHandler::keyVerificationFinalPrompt() -{ - char message[40] = {0}; - memset(message, 0, sizeof(message)); - sprintf(message, "Verification: \n"); - keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet - - if (screen) { - static const char *optionsArray[] = {"Reject", "Accept"}; - graphics::BannerOverlayOptions options; - options.message = message; - options.durationMs = 30000; - options.optionsArrayPtr = optionsArray; - options.optionsCount = 2; - options.notificationType = graphics::notificationTypeEnum::selection_picker; - options.bannerCallback = [=](int selected) { - if (selected == 1) { - auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode()); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - } - }; - screen->showOverlayBanner(options); - } -} - -void menuHandler::FrameToggles_menu() -{ - enum optionsNumbers { - Finish, - nodelist_nodes, - nodelist_location, - nodelist_lastheard, - nodelist_hopsignal, - nodelist_distance, - nodelist_bearings, - gps, - lora, - clock, - show_favorites, - show_telemetry, - show_power, - enumEnd - }; - static const char *optionsArray[enumEnd] = {"Finish"}; - static int optionsEnumArray[enumEnd] = {Finish}; - int options = 1; - - // Track last selected index (not enum value!) - static int lastSelectedIndex = 0; + // Track last selected index (not enum value!) + static int lastSelectedIndex = 0; #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; - optionsEnumArray[options++] = nodelist_nodes; + optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; + optionsEnumArray[options++] = nodelist_nodes; #else - optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; - optionsEnumArray[options++] = nodelist_lastheard; - optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; - optionsEnumArray[options++] = nodelist_hopsignal; + optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; + optionsEnumArray[options++] = nodelist_lastheard; + optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; + optionsEnumArray[options++] = nodelist_hopsignal; #endif #if HAS_GPS #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; - optionsEnumArray[options++] = nodelist_location; + optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; + optionsEnumArray[options++] = nodelist_location; #else - optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; - optionsEnumArray[options++] = nodelist_distance; - optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; - optionsEnumArray[options++] = nodelist_bearings; + optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; + optionsEnumArray[options++] = nodelist_distance; + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; + optionsEnumArray[options++] = nodelist_bearings; #endif - optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; - optionsEnumArray[options++] = gps; + optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; + optionsEnumArray[options++] = gps; #endif - optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; - optionsEnumArray[options++] = lora; + optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; + optionsEnumArray[options++] = lora; - optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; - optionsEnumArray[options++] = clock; + optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; + optionsEnumArray[options++] = clock; - optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; - optionsEnumArray[options++] = show_favorites; + optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; + optionsEnumArray[options++] = show_favorites; - optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; - optionsEnumArray[options++] = show_telemetry; + optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; + optionsEnumArray[options++] = show_telemetry; - optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; - optionsEnumArray[options++] = show_power; + optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; + optionsEnumArray[options++] = show_power; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Show/Hide Frames"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Show/Hide Frames"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value - bannerOptions.bannerCallback = [options](int selected) mutable -> void { - // Find the index of selected in optionsEnumArray - int idx = 0; - for (; idx < options; ++idx) { - if (optionsEnumArray[idx] == selected) - break; - } - lastSelectedIndex = idx; - - if (selected == Finish) { - screen->setFrames(Screen::FOCUS_DEFAULT); - } else if (selected == nodelist_nodes) { - screen->toggleFrameVisibility("nodelist_nodes"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_location) { - screen->toggleFrameVisibility("nodelist_location"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_lastheard) { - screen->toggleFrameVisibility("nodelist_lastheard"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_hopsignal) { - screen->toggleFrameVisibility("nodelist_hopsignal"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_distance) { - screen->toggleFrameVisibility("nodelist_distance"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_bearings) { - screen->toggleFrameVisibility("nodelist_bearings"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == gps) { - screen->toggleFrameVisibility("gps"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == lora) { - screen->toggleFrameVisibility("lora"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == clock) { - screen->toggleFrameVisibility("clock"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == show_favorites) { - screen->toggleFrameVisibility("show_favorites"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == show_telemetry) { - moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == show_power) { - moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::DisplayUnits_menu() -{ - enum optionsNumbers { Back, MetricUnits, ImperialUnits }; - - static const char *optionsArray[] = {"Back", "Metric", "Imperial"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = " Select display units"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - bannerOptions.InitialSelected = 2; - else - bannerOptions.InitialSelected = 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == MetricUnits) { - config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == ImperialUnits) { - config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; - service->reloadConfig(SEGMENT_CONFIG); - } else { - menuHandler::menuQueue = menuHandler::screen_options_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::handleMenuSwitch(OLEDDisplay *display) -{ - if (menuQueue != menu_none) - test_count = 0; - switch (menuQueue) { - case menu_none: - break; - case lora_Menu: - loraMenu(); - break; - case lora_picker: - LoraRegionPicker(); - break; - case device_role_picker: - DeviceRolePicker(); - break; - case radio_preset_picker: - RadioPresetPicker(); - break; - case no_timeout_lora_picker: - LoraRegionPicker(0); - break; - case TZ_picker: - TZPicker(); - break; - case twelve_hour_picker: - TwelveHourPicker(); - break; - case clock_face_picker: - ClockFacePicker(); - break; - case clock_menu: - clockMenu(); - break; - case system_base_menu: - systemBaseMenu(); - break; - case position_base_menu: - positionBaseMenu(); - break; - case node_base_menu: - nodeListMenu(); - break; -#if !MESHTASTIC_EXCLUDE_GPS - case gps_toggle_menu: - GPSToggleMenu(); - break; - case gps_format_menu: - GPSFormatMenu(); - break; - case gps_smart_position_menu: - GPSSmartPositionMenu(); - break; - case gps_update_interval_menu: - GPSUpdateIntervalMenu(); - break; - case gps_position_broadcast_menu: - GPSPositionBroadcastMenu(); - break; -#endif - case compass_point_north_menu: - compassNorthMenu(); - break; - case reset_node_db_menu: - resetNodeDBMenu(); - break; - case buzzermodemenupicker: - BuzzerModeMenu(); - break; - case mui_picker: - switchToMUIMenu(); - break; - case tftcolormenupicker: - TFTColorPickerMenu(display); - break; - case brightness_picker: - BrightnessPickerMenu(); - break; - case node_name_length_menu: - nodeNameLengthMenu(); - break; - case reboot_menu: - rebootMenu(); - break; - case shutdown_menu: - shutdownMenu(); - break; - case add_favorite: - addFavoriteMenu(); - break; - case remove_favorite: - removeFavoriteMenu(); - break; - case trace_route_menu: - traceRouteMenu(); - break; - case test_menu: - testMenu(); - break; - case number_test: - numberTest(); - break; - case wifi_toggle_menu: - wifiToggleMenu(); - break; - case key_verification_init: - keyVerificationInitMenu(); - break; - case key_verification_final_prompt: - keyVerificationFinalPrompt(); - break; - case bluetooth_toggle_menu: - BluetoothToggleMenu(); - break; - case screen_options_menu: - screenOptionsMenu(); - break; - case power_menu: - powerMenu(); - break; - case FrameToggles: - FrameToggles_menu(); - break; - case DisplayUnits: - DisplayUnits_menu(); - break; - case throttle_message: - screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); - break; - case message_response_menu: - messageResponseMenu(); - break; - case reply_menu: - replyMenu(); - break; - case delete_messages_menu: - deleteMessagesMenu(); - break; - case message_viewmode_menu: - messageViewModeMenu(); + bannerOptions.bannerCallback = [options](int selected) mutable -> void { + // Find the index of selected in optionsEnumArray + int idx = 0; + for (; idx < options; ++idx) { + if (optionsEnumArray[idx] == selected) break; } - menuQueue = menu_none; + lastSelectedIndex = idx; + + if (selected == Finish) { + screen->setFrames(Screen::FOCUS_DEFAULT); + } else if (selected == nodelist_nodes) { + screen->toggleFrameVisibility("nodelist_nodes"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_location) { + screen->toggleFrameVisibility("nodelist_location"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_lastheard) { + screen->toggleFrameVisibility("nodelist_lastheard"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_hopsignal) { + screen->toggleFrameVisibility("nodelist_hopsignal"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_distance) { + screen->toggleFrameVisibility("nodelist_distance"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_bearings) { + screen->toggleFrameVisibility("nodelist_bearings"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == gps) { + screen->toggleFrameVisibility("gps"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == lora) { + screen->toggleFrameVisibility("lora"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == clock) { + screen->toggleFrameVisibility("clock"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_favorites) { + screen->toggleFrameVisibility("show_favorites"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_telemetry) { + moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_power) { + moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::saveUIConfig() -{ - nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); +void menuHandler::DisplayUnits_menu() { + enum optionsNumbers { Back, MetricUnits, ImperialUnits }; + + static const char *optionsArray[] = {"Back", "Metric", "Imperial"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = " Select display units"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + bannerOptions.InitialSelected = 2; + else + bannerOptions.InitialSelected = 1; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == MetricUnits) { + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == ImperialUnits) { + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuHandler::menuQueue = menuHandler::screen_options_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::handleMenuSwitch(OLEDDisplay *display) { + if (menuQueue != menu_none) + test_count = 0; + switch (menuQueue) { + case menu_none: + break; + case lora_Menu: + loraMenu(); + break; + case lora_picker: + LoraRegionPicker(); + break; + case device_role_picker: + DeviceRolePicker(); + break; + case radio_preset_picker: + RadioPresetPicker(); + break; + case no_timeout_lora_picker: + LoraRegionPicker(0); + break; + case TZ_picker: + TZPicker(); + break; + case twelve_hour_picker: + TwelveHourPicker(); + break; + case clock_face_picker: + ClockFacePicker(); + break; + case clock_menu: + clockMenu(); + break; + case system_base_menu: + systemBaseMenu(); + break; + case position_base_menu: + positionBaseMenu(); + break; + case node_base_menu: + nodeListMenu(); + break; +#if !MESHTASTIC_EXCLUDE_GPS + case gps_toggle_menu: + GPSToggleMenu(); + break; + case gps_format_menu: + GPSFormatMenu(); + break; + case gps_smart_position_menu: + GPSSmartPositionMenu(); + break; + case gps_update_interval_menu: + GPSUpdateIntervalMenu(); + break; + case gps_position_broadcast_menu: + GPSPositionBroadcastMenu(); + break; +#endif + case compass_point_north_menu: + compassNorthMenu(); + break; + case reset_node_db_menu: + resetNodeDBMenu(); + break; + case buzzermodemenupicker: + BuzzerModeMenu(); + break; + case mui_picker: + switchToMUIMenu(); + break; + case tftcolormenupicker: + TFTColorPickerMenu(display); + break; + case brightness_picker: + BrightnessPickerMenu(); + break; + case node_name_length_menu: + nodeNameLengthMenu(); + break; + case reboot_menu: + rebootMenu(); + break; + case shutdown_menu: + shutdownMenu(); + break; + case add_favorite: + addFavoriteMenu(); + break; + case remove_favorite: + removeFavoriteMenu(); + break; + case trace_route_menu: + traceRouteMenu(); + break; + case test_menu: + testMenu(); + break; + case number_test: + numberTest(); + break; + case wifi_toggle_menu: + wifiToggleMenu(); + break; + case key_verification_init: + keyVerificationInitMenu(); + break; + case key_verification_final_prompt: + keyVerificationFinalPrompt(); + break; + case bluetooth_toggle_menu: + BluetoothToggleMenu(); + break; + case screen_options_menu: + screenOptionsMenu(); + break; + case power_menu: + powerMenu(); + break; + case FrameToggles: + FrameToggles_menu(); + break; + case DisplayUnits: + DisplayUnits_menu(); + break; + case throttle_message: + screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); + break; + case message_response_menu: + messageResponseMenu(); + break; + case reply_menu: + replyMenu(); + break; + case delete_messages_menu: + deleteMessagesMenu(); + break; + case message_viewmode_menu: + messageViewModeMenu(); + break; + } + menuQueue = menu_none; +} + +void menuHandler::saveUIConfig() { + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); } } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 445513e25..c06c7a706 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -1,143 +1,135 @@ #pragma once #if HAS_SCREEN #include "configuration.h" -namespace graphics -{ +namespace graphics { -class menuHandler -{ - public: - enum screenMenus { - menu_none, - lora_Menu, - lora_picker, - device_role_picker, - radio_preset_picker, - no_timeout_lora_picker, - TZ_picker, - twelve_hour_picker, - clock_face_picker, - clock_menu, - position_base_menu, - node_base_menu, - gps_toggle_menu, - gps_format_menu, - gps_smart_position_menu, - gps_update_interval_menu, - gps_position_broadcast_menu, - compass_point_north_menu, - reset_node_db_menu, - buzzermodemenupicker, - mui_picker, - tftcolormenupicker, - brightness_picker, - reboot_menu, - shutdown_menu, - add_favorite, - remove_favorite, - test_menu, - number_test, - wifi_toggle_menu, - bluetooth_toggle_menu, - screen_options_menu, - power_menu, - system_base_menu, - key_verification_init, - key_verification_final_prompt, - trace_route_menu, - throttle_message, - message_response_menu, - message_viewmode_menu, - reply_menu, - delete_messages_menu, - node_name_length_menu, - FrameToggles, - DisplayUnits - }; - static screenMenus menuQueue; +class menuHandler { +public: + enum screenMenus { + menu_none, + lora_Menu, + lora_picker, + device_role_picker, + radio_preset_picker, + no_timeout_lora_picker, + TZ_picker, + twelve_hour_picker, + clock_face_picker, + clock_menu, + position_base_menu, + node_base_menu, + gps_toggle_menu, + gps_format_menu, + gps_smart_position_menu, + gps_update_interval_menu, + gps_position_broadcast_menu, + compass_point_north_menu, + reset_node_db_menu, + buzzermodemenupicker, + mui_picker, + tftcolormenupicker, + brightness_picker, + reboot_menu, + shutdown_menu, + add_favorite, + remove_favorite, + test_menu, + number_test, + wifi_toggle_menu, + bluetooth_toggle_menu, + screen_options_menu, + power_menu, + system_base_menu, + key_verification_init, + key_verification_final_prompt, + trace_route_menu, + throttle_message, + message_response_menu, + message_viewmode_menu, + reply_menu, + delete_messages_menu, + node_name_length_menu, + FrameToggles, + DisplayUnits + }; + static screenMenus menuQueue; - static void OnboardMessage(); - static void LoraRegionPicker(uint32_t duration = 30000); - static void loraMenu(); - static void DeviceRolePicker(); - static void RadioPresetPicker(); - static void handleMenuSwitch(OLEDDisplay *display); - static void showConfirmationBanner(const char *message, std::function onConfirm); - static void clockMenu(); - static void TZPicker(); - static void TwelveHourPicker(); - static void ClockFacePicker(); - static void messageResponseMenu(); - static void messageViewModeMenu(); - static void replyMenu(); - static void deleteMessagesMenu(); - static void homeBaseMenu(); - static void textMessageBaseMenu(); - static void systemBaseMenu(); - static void favoriteBaseMenu(); - static void positionBaseMenu(); - static void compassNorthMenu(); - static void GPSToggleMenu(); - static void GPSFormatMenu(); - static void GPSSmartPositionMenu(); - static void GPSUpdateIntervalMenu(); - static void GPSPositionBroadcastMenu(); - static void BuzzerModeMenu(); - static void switchToMUIMenu(); - static void TFTColorPickerMenu(OLEDDisplay *display); - static void nodeListMenu(); - static void resetNodeDBMenu(); - static void BrightnessPickerMenu(); - static void rebootMenu(); - static void shutdownMenu(); - static void addFavoriteMenu(); - static void removeFavoriteMenu(); - static void traceRouteMenu(); - static void testMenu(); - static void numberTest(); - static void wifiBaseMenu(); - static void wifiToggleMenu(); - static void screenOptionsMenu(); - static void powerMenu(); - static void nodeNameLengthMenu(); - static void FrameToggles_menu(); - static void DisplayUnits_menu(); - static void textMessageMenu(); + static void OnboardMessage(); + static void LoraRegionPicker(uint32_t duration = 30000); + static void loraMenu(); + static void DeviceRolePicker(); + static void RadioPresetPicker(); + static void handleMenuSwitch(OLEDDisplay *display); + static void showConfirmationBanner(const char *message, std::function onConfirm); + static void clockMenu(); + static void TZPicker(); + static void TwelveHourPicker(); + static void ClockFacePicker(); + static void messageResponseMenu(); + static void messageViewModeMenu(); + static void replyMenu(); + static void deleteMessagesMenu(); + static void homeBaseMenu(); + static void textMessageBaseMenu(); + static void systemBaseMenu(); + static void favoriteBaseMenu(); + static void positionBaseMenu(); + static void compassNorthMenu(); + static void GPSToggleMenu(); + static void GPSFormatMenu(); + static void GPSSmartPositionMenu(); + static void GPSUpdateIntervalMenu(); + static void GPSPositionBroadcastMenu(); + static void BuzzerModeMenu(); + static void switchToMUIMenu(); + static void TFTColorPickerMenu(OLEDDisplay *display); + static void nodeListMenu(); + static void resetNodeDBMenu(); + static void BrightnessPickerMenu(); + static void rebootMenu(); + static void shutdownMenu(); + static void addFavoriteMenu(); + static void removeFavoriteMenu(); + static void traceRouteMenu(); + static void testMenu(); + static void numberTest(); + static void wifiBaseMenu(); + static void wifiToggleMenu(); + static void screenOptionsMenu(); + static void powerMenu(); + static void nodeNameLengthMenu(); + static void FrameToggles_menu(); + static void DisplayUnits_menu(); + static void textMessageMenu(); - private: - static void saveUIConfig(); - static void keyVerificationInitMenu(); - static void keyVerificationFinalPrompt(); - static void BluetoothToggleMenu(); +private: + static void saveUIConfig(); + static void keyVerificationInitMenu(); + static void keyVerificationFinalPrompt(); + static void BluetoothToggleMenu(); }; /* Generic Menu Options designations */ enum class OptionsAction { Back, Select }; template struct MenuOption { - const char *label; - OptionsAction action; - bool hasValue; - T value; + const char *label; + OptionsAction action; + bool hasValue; + T value; - MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) - : label(labelIn), action(actionIn), hasValue(true), value(valueIn) - { - } + MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) : label(labelIn), action(actionIn), hasValue(true), value(valueIn) {} - MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} + MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} }; struct ScreenColor { - uint8_t r; - uint8_t g; - uint8_t b; - bool useVariant; + uint8_t r; + uint8_t g; + uint8_t b; + bool useVariant; - ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) - : r(rIn), g(gIn), b(bIn), useVariant(variantIn) - { - } + ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) : r(rIn), g(gIn), b(bIn), useVariant(variantIn) {} }; using RadioPresetOption = MenuOption; diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 09b798e06..2b11451bf 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -27,51 +27,47 @@ using graphics::Emote; using graphics::emotes; using graphics::numEmotes; -namespace graphics -{ -namespace MessageRenderer -{ +namespace graphics { +namespace MessageRenderer { static std::vector cachedLines; static std::vector cachedHeights; static bool manualScrolling = false; // UTF-8 skip helper -static inline size_t utf8CharLen(uint8_t c) -{ - if ((c & 0xE0) == 0xC0) - return 2; - if ((c & 0xF0) == 0xE0) - return 3; - if ((c & 0xF8) == 0xF0) - return 4; - return 1; +static inline size_t utf8CharLen(uint8_t c) { + if ((c & 0xE0) == 0xC0) + return 2; + if ((c & 0xF0) == 0xE0) + return 3; + if ((c & 0xF8) == 0xF0) + return 4; + return 1; } // Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels -std::string normalizeEmoji(const std::string &s) -{ - std::string out; - for (size_t i = 0; i < s.size();) { - uint8_t c = static_cast(s[i]); - size_t len = utf8CharLen(c); +std::string normalizeEmoji(const std::string &s) { + std::string out; + for (size_t i = 0; i < s.size();) { + uint8_t c = static_cast(s[i]); + size_t len = utf8CharLen(c); - if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { - i += 3; - continue; - } - - // Skip skin tone modifiers - if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && - ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { - i += 4; - continue; - } - - out.append(s, i, len); - i += len; + if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { + i += 3; + continue; } - return out; + + // Skip skin tone modifiers + if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && + ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { + i += 4; + continue; + } + + out.append(s, i, len); + i += len; + } + return out; } // Scroll state (file scope so we can reset on new message) @@ -83,211 +79,205 @@ bool waitingToReset = false; bool scrollStarted = false; static bool didReset = false; -void scrollUp() -{ - manualScrolling = true; - scrollY -= 12; - if (scrollY < 0) - scrollY = 0; +void scrollUp() { + manualScrolling = true; + scrollY -= 12; + if (scrollY < 0) + scrollY = 0; } -void scrollDown() -{ - manualScrolling = true; +void scrollDown() { + manualScrolling = true; - int totalHeight = 0; - for (int h : cachedHeights) - totalHeight += h; + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; - int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); - int maxScroll = totalHeight - visibleHeight; - if (maxScroll < 0) - maxScroll = 0; + int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); + int maxScroll = totalHeight - visibleHeight; + if (maxScroll < 0) + maxScroll = 0; - scrollY += 12; - if (scrollY > maxScroll) - scrollY = maxScroll; + scrollY += 12; + if (scrollY > maxScroll) + scrollY = maxScroll; } -void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) -{ - std::string renderLine; - for (size_t i = 0; i < line.size();) { - uint8_t c = (uint8_t)line[i]; - size_t len = utf8CharLen(c); - if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) { - i += 3; - continue; - } - if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F && - ((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) { - i += 4; - continue; - } - renderLine.append(line, i, len); - i += len; +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { + std::string renderLine; + for (size_t i = 0; i < line.size();) { + uint8_t c = (uint8_t)line[i]; + size_t len = utf8CharLen(c); + if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) { + i += 3; + continue; } - int cursorX = x; - const int fontHeight = FONT_HEIGHT_SMALL; + if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F && + ((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) { + i += 4; + continue; + } + renderLine.append(line, i, len); + i += len; + } + int cursorX = x; + const int fontHeight = FONT_HEIGHT_SMALL; - // Step 1: Find tallest emote in the line - int maxIconHeight = fontHeight; - for (size_t i = 0; i < line.length();) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (line.compare(i, emojiLen, emotes[e].label) == 0) { - if (emotes[e].height > maxIconHeight) - maxIconHeight = emotes[e].height; - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - i += utf8CharLen(static_cast(line[i])); - } + // Step 1: Find tallest emote in the line + int maxIconHeight = fontHeight; + for (size_t i = 0; i < line.length();) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (line.compare(i, emojiLen, emotes[e].label) == 0) { + if (emotes[e].height > maxIconHeight) + maxIconHeight = emotes[e].height; + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + i += utf8CharLen(static_cast(line[i])); + } + } + + // Step 2: Baseline alignment + int lineHeight = std::max(fontHeight, maxIconHeight); + int baselineOffset = (lineHeight - fontHeight) / 2; + int fontY = y + baselineOffset; + + // Step 3: Render line in segments + size_t i = 0; + bool inBold = false; + + while (i < line.length()) { + // Check for ** start/end for faux bold + if (line.compare(i, 2, "**") == 0) { + inBold = !inBold; + i += 2; + continue; } - // Step 2: Baseline alignment - int lineHeight = std::max(fontHeight, maxIconHeight); - int baselineOffset = (lineHeight - fontHeight) / 2; - int fontY = y + baselineOffset; + // Look ahead for the next emote match + size_t nextEmotePos = std::string::npos; + const Emote *matchedEmote = nullptr; + size_t emojiLen = 0; - // Step 3: Render line in segments - size_t i = 0; - bool inBold = false; + for (int e = 0; e < emoteCount; ++e) { + size_t pos = line.find(emotes[e].label, i); + if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { + nextEmotePos = pos; + matchedEmote = &emotes[e]; + emojiLen = strlen(emotes[e].label); + } + } - while (i < line.length()) { - // Check for ** start/end for faux bold - if (line.compare(i, 2, "**") == 0) { - inBold = !inBold; - i += 2; - continue; - } + // Render normal text segment up to the emote or bold toggle + size_t nextControl = std::min(nextEmotePos, line.find("**", i)); + if (nextControl == std::string::npos) + nextControl = line.length(); - // Look ahead for the next emote match - size_t nextEmotePos = std::string::npos; - const Emote *matchedEmote = nullptr; - size_t emojiLen = 0; - - for (int e = 0; e < emoteCount; ++e) { - size_t pos = line.find(emotes[e].label, i); - if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { - nextEmotePos = pos; - matchedEmote = &emotes[e]; - emojiLen = strlen(emotes[e].label); - } - } - - // Render normal text segment up to the emote or bold toggle - size_t nextControl = std::min(nextEmotePos, line.find("**", i)); - if (nextControl == std::string::npos) - nextControl = line.length(); - - if (nextControl > i) { - std::string textChunk = line.substr(i, nextControl - i); - if (inBold) { - // Faux bold: draw twice, offset by 1px - display->drawString(cursorX + 1, fontY, textChunk.c_str()); - } - display->drawString(cursorX, fontY, textChunk.c_str()); + if (nextControl > i) { + std::string textChunk = line.substr(i, nextControl - i); + if (inBold) { + // Faux bold: draw twice, offset by 1px + display->drawString(cursorX + 1, fontY, textChunk.c_str()); + } + display->drawString(cursorX, fontY, textChunk.c_str()); #if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); + cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); #else - cursorX += display->getStringWidth(textChunk.c_str()); + cursorX += display->getStringWidth(textChunk.c_str()); #endif - i = nextControl; - continue; - } - - // Render the emote (if found) - if (matchedEmote && i == nextEmotePos) { - // Vertically center emote relative to font baseline (not just midline) - int iconY = fontY + (fontHeight - matchedEmote->height) / 2; - display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); - cursorX += matchedEmote->width + 1; - i += emojiLen; - continue; - } else { - // No more emotes — render the rest of the line - std::string remaining = line.substr(i); - if (inBold) { - display->drawString(cursorX + 1, fontY, remaining.c_str()); - } - display->drawString(cursorX, fontY, remaining.c_str()); -#if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); -#else - cursorX += display->getStringWidth(remaining.c_str()); -#endif - break; - } + i = nextControl; + continue; } + + // Render the emote (if found) + if (matchedEmote && i == nextEmotePos) { + // Vertically center emote relative to font baseline (not just midline) + int iconY = fontY + (fontHeight - matchedEmote->height) / 2; + display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); + cursorX += matchedEmote->width + 1; + i += emojiLen; + continue; + } else { + // No more emotes — render the rest of the line + std::string remaining = line.substr(i); + if (inBold) { + display->drawString(cursorX + 1, fontY, remaining.c_str()); + } + display->drawString(cursorX, fontY, remaining.c_str()); +#if defined(OLED_UA) || defined(OLED_RU) + cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); +#else + cursorX += display->getStringWidth(remaining.c_str()); +#endif + break; + } + } } // Reset scroll state when new messages arrive -void resetScrollState() -{ - scrollY = 0.0f; - scrollStarted = false; - waitingToReset = false; - scrollStartDelay = millis(); - lastTime = millis(); - manualScrolling = false; - didReset = false; +void resetScrollState() { + scrollY = 0.0f; + scrollStarted = false; + waitingToReset = false; + scrollStartDelay = millis(); + lastTime = millis(); + manualScrolling = false; + didReset = false; } -void nudgeScroll(int8_t direction) -{ - if (direction == 0) - return; +void nudgeScroll(int8_t direction) { + if (direction == 0) + return; - if (cachedHeights.empty()) { - scrollY = 0.0f; - return; - } + if (cachedHeights.empty()) { + scrollY = 0.0f; + return; + } - OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; - const int displayHeight = display ? display->getHeight() : 64; - const int navHeight = FONT_HEIGHT_SMALL; - const int usableHeight = std::max(0, displayHeight - navHeight); + OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; + const int displayHeight = display ? display->getHeight() : 64; + const int navHeight = FONT_HEIGHT_SMALL; + const int usableHeight = std::max(0, displayHeight - navHeight); - int totalHeight = 0; - for (int h : cachedHeights) - totalHeight += h; + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; - if (totalHeight <= usableHeight) { - scrollY = 0.0f; - return; - } + if (totalHeight <= usableHeight) { + scrollY = 0.0f; + return; + } - const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); - const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); + const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); + const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); - float newScroll = scrollY + static_cast(direction) * static_cast(step); - if (newScroll < 0.0f) - newScroll = 0.0f; - if (newScroll > scrollStop) - newScroll = static_cast(scrollStop); + float newScroll = scrollY + static_cast(direction) * static_cast(step); + if (newScroll < 0.0f) + newScroll = 0.0f; + if (newScroll > scrollStop) + newScroll = static_cast(scrollStop); - if (newScroll != scrollY) { - scrollY = newScroll; - waitingToReset = false; - scrollStarted = false; - scrollStartDelay = millis(); - lastTime = millis(); - } + if (newScroll != scrollY) { + scrollY = newScroll; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = millis(); + lastTime = millis(); + } } // Fully free cached message data from heap -void clearMessageCache() -{ - std::vector().swap(cachedLines); - std::vector().swap(cachedHeights); +void clearMessageCache() { + std::vector().swap(cachedLines); + std::vector().swap(cachedHeights); - // Reset scroll so we rebuild cleanly next time we enter the screen - resetScrollState(); + // Reset scroll so we rebuild cleanly next time we enter the screen + resetScrollState(); } // Current thread state @@ -300,733 +290,702 @@ static std::vector seenChannels; static std::vector seenPeers; // Public helper so menus / store can clear stale registries -void clearThreadRegistries() -{ - seenChannels.clear(); - seenPeers.clear(); +void clearThreadRegistries() { + seenChannels.clear(); + seenPeers.clear(); } // Setter so other code can switch threads -void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) -{ - currentMode = mode; - currentChannel = channel; - currentPeer = peer; - didReset = false; // force reset when mode changes +void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) { + currentMode = mode; + currentChannel = channel; + currentPeer = peer; + didReset = false; // force reset when mode changes - // Track channels we’ve seen - if (mode == ThreadMode::CHANNEL && channel >= 0) { - if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { - seenChannels.push_back(channel); - } + // Track channels we’ve seen + if (mode == ThreadMode::CHANNEL && channel >= 0) { + if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { + seenChannels.push_back(channel); } + } - // Track DMs we’ve seen - if (mode == ThreadMode::DIRECT && peer != 0) { - if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { - seenPeers.push_back(peer); - } + // Track DMs we’ve seen + if (mode == ThreadMode::DIRECT && peer != 0) { + if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { + seenPeers.push_back(peer); } + } } -ThreadMode getThreadMode() -{ - return currentMode; -} +ThreadMode getThreadMode() { return currentMode; } -int getThreadChannel() -{ - return currentChannel; -} +int getThreadChannel() { return currentChannel; } -uint32_t getThreadPeer() -{ - return currentPeer; -} +uint32_t getThreadPeer() { return currentPeer; } // Accessors for menuHandler -const std::vector &getSeenChannels() -{ - return seenChannels; -} -const std::vector &getSeenPeers() -{ - return seenPeers; -} +const std::vector &getSeenChannels() { return seenChannels; } +const std::vector &getSeenPeers() { return seenPeers; } -static int centerYForRow(int y, int size) -{ - int midY = y + (FONT_HEIGHT_SMALL / 2); - return midY - (size / 2); +static int centerYForRow(int y, int size) { + int midY = y + (FONT_HEIGHT_SMALL / 2); + return midY - (size / 2); } // Helpers for drawing status marks (thickened strokes) -static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) -{ - int topY = centerYForRow(y, size); - display->setColor(WHITE); - display->drawLine(x, topY + size / 2, x + size / 3, topY + size); - display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); - display->drawLine(x + size / 3, topY + size, x + size, topY); - display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); +static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) { + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY + size / 2, x + size / 3, topY + size); + display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); + display->drawLine(x + size / 3, topY + size, x + size, topY); + display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); } -static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) -{ - int topY = centerYForRow(y, size); - display->setColor(WHITE); - display->drawLine(x, topY, x + size, topY + size); - display->drawLine(x, topY + 1, x + size, topY + size + 1); - display->drawLine(x + size, topY, x, topY + size); - display->drawLine(x + size, topY + 1, x, topY + size + 1); +static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) { + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY, x + size, topY + size); + display->drawLine(x, topY + 1, x + size, topY + size + 1); + display->drawLine(x + size, topY, x, topY + size); + display->drawLine(x + size, topY + 1, x, topY + size + 1); } -static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) -{ - int r = size / 2; - int centerY = centerYForRow(y, size) + r; - int centerX = x + r; - display->setColor(WHITE); - display->drawCircle(centerX, centerY, r); - display->drawLine(centerX, centerY - 2, centerX, centerY); - display->setPixel(centerX, centerY + 2); - display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); +static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) { + int r = size / 2; + int centerY = centerYForRow(y, size) + r; + int centerX = x + r; + display->setColor(WHITE); + display->drawCircle(centerX, centerY, r); + display->drawLine(centerX, centerY - 2, centerX, centerY); + display->setPixel(centerX, centerY + 2); + display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); } -static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) -{ - std::string normalized = normalizeEmoji(line); - int totalWidth = 0; +static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) { + std::string normalized = normalizeEmoji(line); + int totalWidth = 0; - size_t i = 0; - while (i < normalized.length()) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { - totalWidth += emotes[e].width + 1; // +1 spacing - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - size_t charLen = utf8CharLen(static_cast(normalized[i])); + size_t i = 0; + while (i < normalized.length()) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { + totalWidth += emotes[e].width + 1; // +1 spacing + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + size_t charLen = utf8CharLen(static_cast(normalized[i])); #if defined(OLED_UA) || defined(OLED_RU) - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); + totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); #else - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); + totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); #endif - i += charLen; - } + i += charLen; } - return totalWidth; + } + return totalWidth; } -static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) -{ - if (totalHeight <= visibleHeight) - return; // no scrollbar needed +static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) { + if (totalHeight <= visibleHeight) + return; // no scrollbar needed - int scrollbarX = display->getWidth() - 2; - int scrollbarHeight = visibleHeight; - int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); - int maxScroll = std::max(1, totalHeight - visibleHeight); - int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = visibleHeight; + int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); + int maxScroll = std::max(1, totalHeight - visibleHeight); + int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; - for (int i = 0; i < thumbHeight; i++) { - display->setPixel(scrollbarX, thumbY + i); - } + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } } -void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Ensure any boot-relative timestamps are upgraded if RTC is valid - messageStore.upgradeBootRelativeTimestamps(); +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Ensure any boot-relative timestamps are upgraded if RTC is valid + messageStore.upgradeBootRelativeTimestamps(); - if (!didReset) { - resetScrollState(); - didReset = true; - } + if (!didReset) { + resetScrollState(); + didReset = true; + } - // Clear the unread message indicator when viewing the message - hasUnreadMessage = false; + // Clear the unread message indicator when viewing the message + hasUnreadMessage = false; - // Filter messages based on thread mode - std::deque filtered; - for (const auto &m : messageStore.getLiveMessages()) { - bool include = false; - switch (currentMode) { - case ThreadMode::ALL: - include = true; - break; - case ThreadMode::CHANNEL: - if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) - include = true; - break; - case ThreadMode::DIRECT: - if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) - include = true; - break; - } - if (include) - filtered.push_back(m); - } - - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - const int navHeight = FONT_HEIGHT_SMALL; - const int scrollBottom = SCREEN_HEIGHT - navHeight; - const int usableHeight = scrollBottom; - constexpr int LEFT_MARGIN = 2; - constexpr int RIGHT_MARGIN = 2; - constexpr int SCROLLBAR_WIDTH = 3; - - const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; - - const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; - - // Title string depending on mode - static char titleBuf[32]; - const char *titleStr = "Messages"; + // Filter messages based on thread mode + std::deque filtered; + for (const auto &m : messageStore.getLiveMessages()) { + bool include = false; switch (currentMode) { case ThreadMode::ALL: - titleStr = "Messages"; - break; - case ThreadMode::CHANNEL: { - const char *cname = channels.getName(currentChannel); - if (cname && cname[0]) { - snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); - } else { - snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); - } - titleStr = titleBuf; - break; + include = true; + break; + case ThreadMode::CHANNEL: + if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) + include = true; + break; + case ThreadMode::DIRECT: + if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) + include = true; + break; } - case ThreadMode::DIRECT: { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); - if (node && node->has_user) { - snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); - } else { - snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); - } - titleStr = titleBuf; - break; + if (include) + filtered.push_back(m); + } + + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + const int navHeight = FONT_HEIGHT_SMALL; + const int scrollBottom = SCREEN_HEIGHT - navHeight; + const int usableHeight = scrollBottom; + constexpr int LEFT_MARGIN = 2; + constexpr int RIGHT_MARGIN = 2; + constexpr int SCROLLBAR_WIDTH = 3; + + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; + + const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; + + // Title string depending on mode + static char titleBuf[32]; + const char *titleStr = "Messages"; + switch (currentMode) { + case ThreadMode::ALL: + titleStr = "Messages"; + break; + case ThreadMode::CHANNEL: { + const char *cname = channels.getName(currentChannel); + if (cname && cname[0]) { + snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); + } else { + snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); } + titleStr = titleBuf; + break; + } + case ThreadMode::DIRECT: { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); + if (node && node->has_user) { + snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); + } else { + snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); + } + titleStr = titleBuf; + break; + } + } + + if (filtered.empty()) { + // If current conversation is empty go back to ALL view + if (currentMode != ThreadMode::ALL) { + setThreadMode(ThreadMode::ALL); + resetScrollState(); + return; // Next draw will rerun in ALL mode } - if (filtered.empty()) { - // If current conversation is empty go back to ALL view - if (currentMode != ThreadMode::ALL) { - setThreadMode(ThreadMode::ALL); - resetScrollState(); - return; // Next draw will rerun in ALL mode - } + // Still in ALL mode and no messages at all → show placeholder + graphics::drawCommonHeader(display, x, y, titleStr); + didReset = false; + const char *messageString = "No messages"; + int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); + display->drawString(center_text, getTextPositions(display)[2], messageString); + graphics::drawCommonFooter(display, x, y); + return; + } - // Still in ALL mode and no messages at all → show placeholder - graphics::drawCommonHeader(display, x, y, titleStr); - didReset = false; - const char *messageString = "No messages"; - int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); - display->drawString(center_text, getTextPositions(display)[2], messageString); - graphics::drawCommonFooter(display, x, y); - return; + // Build lines for filtered messages (newest first) + std::vector allLines; + std::vector isMine; // track alignment + std::vector isHeader; // track header lines + std::vector ackForLine; + + for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { + const auto &m = *it; + + // Channel / destination labeling + char chanType[32] = ""; + if (currentMode == ThreadMode::ALL) { + if (m.dest == NODENUM_BROADCAST) { + snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex)); + } else { + snprintf(chanType, sizeof(chanType), "(DM)"); + } } - // Build lines for filtered messages (newest first) - std::vector allLines; - std::vector isMine; // track alignment - std::vector isHeader; // track header lines - std::vector ackForLine; + // Calculate how long ago + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + uint32_t seconds = 0; + bool invalidTime = true; - for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { - const auto &m = *it; - - // Channel / destination labeling - char chanType[32] = ""; - if (currentMode == ThreadMode::ALL) { - if (m.dest == NODENUM_BROADCAST) { - snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex)); - } else { - snprintf(chanType, sizeof(chanType), "(DM)"); - } - } - - // Calculate how long ago - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - uint32_t seconds = 0; - bool invalidTime = true; - - if (m.timestamp > 0 && nowSecs > 0) { - if (nowSecs >= m.timestamp) { - seconds = nowSecs - m.timestamp; - invalidTime = (seconds > 315360000); // >10 years - } else { - uint32_t ahead = m.timestamp - nowSecs; - if (ahead <= 600) { // allow small skew - seconds = 0; - invalidTime = false; - } - } - } else if (m.timestamp > 0 && nowSecs == 0) { - // RTC not valid: only trust boot-relative if same boot - uint32_t bootNow = millis() / 1000; - if (m.isBootRelative && m.timestamp <= bootNow) { - seconds = bootNow - m.timestamp; - invalidTime = false; - } else { - invalidTime = true; // old persisted boot-relative, ignore until healed - } - } - - char timeBuf[16]; - if (invalidTime) { - snprintf(timeBuf, sizeof(timeBuf), "???"); - } else if (seconds < 60) { - snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); - } else if (seconds < 3600) { - snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); - } else if (seconds < 86400) { - snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); - } else { - snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); - } - - // Build header line for this message - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); - meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); - - char senderBuf[48] = ""; - if (node && node->has_user) { - // Use long name if present - strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); - senderBuf[sizeof(senderBuf) - 1] = '\0'; - } else { - // No long/short name → show NodeID in parentheses - snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); - } - - // If this is *our own* message, override senderBuf to who the recipient was - bool mine = (m.sender == nodeDB->getNodeNum()); - if (mine && node_recipient && node_recipient->has_user) { - strcpy(senderBuf, node_recipient->user.long_name); - } - - // Shrink Sender name if needed - int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - - display->getStringWidth(" @...") - 10; - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(senderBuf); - while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { - senderBuf[strlen(senderBuf) - 1] = '\0'; - } - - // If we actually truncated, append "..." - if (strlen(senderBuf) < origLen) { - strcat(senderBuf, "..."); - } - - // Final header line - char headerStr[96]; - if (mine) { - if (currentMode == ThreadMode::ALL) { - if (strcmp(chanType, "(DM)") == 0) { - snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); - } else { - snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); - } - } else { - snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); - } - } else { - snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); - } - - // Push header line - allLines.push_back(std::string(headerStr)); - isMine.push_back(mine); - isHeader.push_back(true); - ackForLine.push_back(m.ackStatus); - - const char *msgText = MessageStore::getText(m); - - int wrapWidth = mine ? rightTextWidth : leftTextWidth; - std::vector wrapped = generateLines(display, "", msgText, wrapWidth); - for (auto &ln : wrapped) { - allLines.push_back(ln); - isMine.push_back(mine); - isHeader.push_back(false); - ackForLine.push_back(AckStatus::NONE); + if (m.timestamp > 0 && nowSecs > 0) { + if (nowSecs >= m.timestamp) { + seconds = nowSecs - m.timestamp; + invalidTime = (seconds > 315360000); // >10 years + } else { + uint32_t ahead = m.timestamp - nowSecs; + if (ahead <= 600) { // allow small skew + seconds = 0; + invalidTime = false; } + } + } else if (m.timestamp > 0 && nowSecs == 0) { + // RTC not valid: only trust boot-relative if same boot + uint32_t bootNow = millis() / 1000; + if (m.isBootRelative && m.timestamp <= bootNow) { + seconds = bootNow - m.timestamp; + invalidTime = false; + } else { + invalidTime = true; // old persisted boot-relative, ignore until healed + } } - // Cache lines and heights - cachedLines = allLines; - cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); + char timeBuf[16]; + if (invalidTime) { + snprintf(timeBuf, sizeof(timeBuf), "???"); + } else if (seconds < 60) { + snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); + } else if (seconds < 3600) { + snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); + } else if (seconds < 86400) { + snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); + } else { + snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); + } - // Scrolling logic (unchanged) - int totalHeight = 0; - for (size_t i = 0; i < cachedHeights.size(); ++i) - totalHeight += cachedHeights[i]; - int usableScrollHeight = usableHeight; - int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); + // Build header line for this message + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); + meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); + + char senderBuf[48] = ""; + if (node && node->has_user) { + // Use long name if present + strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); + senderBuf[sizeof(senderBuf) - 1] = '\0'; + } else { + // No long/short name → show NodeID in parentheses + snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); + } + + // If this is *our own* message, override senderBuf to who the recipient was + bool mine = (m.sender == nodeDB->getNodeNum()); + if (mine && node_recipient && node_recipient->has_user) { + strcpy(senderBuf, node_recipient->user.long_name); + } + + // Shrink Sender name if needed + int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - display->getStringWidth(" @...") - 10; + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(senderBuf); + while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { + senderBuf[strlen(senderBuf) - 1] = '\0'; + } + + // If we actually truncated, append "..." + if (strlen(senderBuf) < origLen) { + strcat(senderBuf, "..."); + } + + // Final header line + char headerStr[96]; + if (mine) { + if (currentMode == ThreadMode::ALL) { + if (strcmp(chanType, "(DM)") == 0) { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + } else { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + } + + // Push header line + allLines.push_back(std::string(headerStr)); + isMine.push_back(mine); + isHeader.push_back(true); + ackForLine.push_back(m.ackStatus); + + const char *msgText = MessageStore::getText(m); + + int wrapWidth = mine ? rightTextWidth : leftTextWidth; + std::vector wrapped = generateLines(display, "", msgText, wrapWidth); + for (auto &ln : wrapped) { + allLines.push_back(ln); + isMine.push_back(mine); + isHeader.push_back(false); + ackForLine.push_back(AckStatus::NONE); + } + } + + // Cache lines and heights + cachedLines = allLines; + cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); + + // Scrolling logic (unchanged) + int totalHeight = 0; + for (size_t i = 0; i < cachedHeights.size(); ++i) + totalHeight += cachedHeights[i]; + int usableScrollHeight = usableHeight; + int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); #ifndef USE_EINK - uint32_t now = millis(); - float delta = (now - lastTime) / 400.0f; - lastTime = now; - const float scrollSpeed = 2.0f; + uint32_t now = millis(); + float delta = (now - lastTime) / 400.0f; + lastTime = now; + const float scrollSpeed = 2.0f; - if (scrollStartDelay == 0) - scrollStartDelay = now; - if (!scrollStarted && now - scrollStartDelay > 2000) - scrollStarted = true; + if (scrollStartDelay == 0) + scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) + scrollStarted = true; - if (!manualScrolling && totalHeight > usableScrollHeight) { - if (scrollStarted) { - if (!waitingToReset) { - scrollY += delta * scrollSpeed; - if (scrollY >= scrollStop) { - scrollY = scrollStop; - waitingToReset = true; - pauseStart = lastTime; - } - } else if (lastTime - pauseStart > 3000) { - scrollY = 0; - waitingToReset = false; - scrollStarted = false; - scrollStartDelay = lastTime; - } + if (!manualScrolling && totalHeight > usableScrollHeight) { + if (scrollStarted) { + if (!waitingToReset) { + scrollY += delta * scrollSpeed; + if (scrollY >= scrollStop) { + scrollY = scrollStop; + waitingToReset = true; + pauseStart = lastTime; } - } else if (!manualScrolling) { + } else if (lastTime - pauseStart > 3000) { scrollY = 0; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = lastTime; + } } + } else if (!manualScrolling) { + scrollY = 0; + } #else - // E-Ink: disable autoscroll - scrollY = 0.0f; - waitingToReset = false; - scrollStarted = false; - lastTime = millis(); + // E-Ink: disable autoscroll + scrollY = 0.0f; + waitingToReset = false; + scrollStarted = false; + lastTime = millis(); #endif - int finalScroll = (int)scrollY; - int yOffset = -finalScroll + getTextPositions(display)[1]; + int finalScroll = (int)scrollY; + int yOffset = -finalScroll + getTextPositions(display)[1]; - // Render visible lines - for (size_t i = 0; i < cachedLines.size(); ++i) { - int lineY = yOffset; - for (size_t j = 0; j < i; ++j) - lineY += cachedHeights[j]; + // Render visible lines + for (size_t i = 0; i < cachedLines.size(); ++i) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += cachedHeights[j]; - if (lineY > -cachedHeights[i] && lineY < scrollBottom) { - if (isHeader[i]) { + if (lineY > -cachedHeights[i] && lineY < scrollBottom) { + if (isHeader[i]) { - int w = display->getStringWidth(cachedLines[i].c_str()); - int headerX; - if (isMine[i]) { - // push header left to avoid overlap with scrollbar - headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN; - if (headerX < LEFT_MARGIN) - headerX = LEFT_MARGIN; - } else { - headerX = x; - } - display->drawString(headerX, lineY, cachedLines[i].c_str()); - - // Draw ACK/NACK mark for our own messages - if (isMine[i]) { - int markX = headerX - 10; - int markY = lineY; - if (ackForLine[i] == AckStatus::ACKED) { - // Destination ACK - drawCheckMark(display, markX, markY, 8); - } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { - // Failure or timeout - drawXMark(display, markX, markY, 8); - } else if (ackForLine[i] == AckStatus::RELAYED) { - // Relay ACK - drawRelayMark(display, markX, markY, 8); - } - // AckStatus::NONE → show nothing - } - - // Draw underline just under header text - int underlineY = lineY + FONT_HEIGHT_SMALL; - for (int px = 0; px < w; ++px) { - display->setPixel(headerX + px, underlineY); - } - } else { - // Render message line - if (isMine[i]) { - // Calculate actual rendered width including emotes - int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); - int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN; - if (rightX < LEFT_MARGIN) - rightX = LEFT_MARGIN; - - drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); - } else { - drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); - } - } + int w = display->getStringWidth(cachedLines[i].c_str()); + int headerX; + if (isMine[i]) { + // push header left to avoid overlap with scrollbar + headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN; + if (headerX < LEFT_MARGIN) + headerX = LEFT_MARGIN; + } else { + headerX = x; } - } - int totalContentHeight = totalHeight; - int visibleHeight = usableHeight; + display->drawString(headerX, lineY, cachedLines[i].c_str()); - // Draw scrollbar - drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]); - graphics::drawCommonHeader(display, x, y, titleStr); - graphics::drawCommonFooter(display, x, y); + // Draw ACK/NACK mark for our own messages + if (isMine[i]) { + int markX = headerX - 10; + int markY = lineY; + if (ackForLine[i] == AckStatus::ACKED) { + // Destination ACK + drawCheckMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { + // Failure or timeout + drawXMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::RELAYED) { + // Relay ACK + drawRelayMark(display, markX, markY, 8); + } + // AckStatus::NONE → show nothing + } + + // Draw underline just under header text + int underlineY = lineY + FONT_HEIGHT_SMALL; + for (int px = 0; px < w; ++px) { + display->setPixel(headerX + px, underlineY); + } + } else { + // Render message line + if (isMine[i]) { + // Calculate actual rendered width including emotes + int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN; + if (rightX < LEFT_MARGIN) + rightX = LEFT_MARGIN; + + drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); + } else { + drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); + } + } + } + } + int totalContentHeight = totalHeight; + int visibleHeight = usableHeight; + + // Draw scrollbar + drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]); + graphics::drawCommonHeader(display, x, y, titleStr); + graphics::drawCommonFooter(display, x, y); } -std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) -{ - std::vector lines; +std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) { + std::vector lines; - // Only push headerStr if it's not empty (prevents extra blank line after headers) - if (headerStr && headerStr[0] != '\0') { - lines.push_back(std::string(headerStr)); + // Only push headerStr if it's not empty (prevents extra blank line after headers) + if (headerStr && headerStr[0] != '\0') { + lines.push_back(std::string(headerStr)); + } + + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { + char ch = messageBuf[i]; + if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && (unsigned char)messageBuf[i + 2] == 0x99) { + ch = '\''; // plain apostrophe + i += 2; // skip over the extra UTF-8 bytes } - - std::string line, word; - for (int i = 0; messageBuf[i]; ++i) { - char ch = messageBuf[i]; - if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && - (unsigned char)messageBuf[i + 2] == 0x99) { - ch = '\''; // plain apostrophe - i += 2; // skip over the extra UTF-8 bytes - } - if (ch == '\n') { - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); - line.clear(); - word.clear(); - } else if (ch == ' ') { - line += word + ' '; - word.clear(); - } else { - word += ch; - std::string test = line + word; -#if defined(OLED_UA) || defined(OLED_RU) - uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); -#else - uint16_t strWidth = display->getStringWidth(test.c_str()); -#endif - if (strWidth > textWidth) { - if (!line.empty()) - lines.push_back(line); - line = word; - word.clear(); - } - } - } - - if (!word.empty()) + if (ch == '\n') { + if (!word.empty()) line += word; - if (!line.empty()) + if (!line.empty()) lines.push_back(line); - - return lines; -} -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, - const std::vector &isHeaderVec) -{ - // Tunables for layout control - constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line - constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) - constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines - constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header - constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) - constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) - - std::vector rowHeights; - rowHeights.reserve(lines.size()); - - for (size_t idx = 0; idx < lines.size(); ++idx) { - const auto &line = lines[idx]; - const int baseHeight = FONT_HEIGHT_SMALL; - - // Detect if THIS line or NEXT line contains an emote - bool hasEmote = false; - int tallestEmote = baseHeight; - for (int i = 0; i < numEmotes; ++i) { - if (line.find(emotes[i].label) != std::string::npos) { - hasEmote = true; - tallestEmote = std::max(tallestEmote, emotes[i].height); - } - } - - bool nextHasEmote = false; - if (idx + 1 < lines.size()) { - for (int i = 0; i < numEmotes; ++i) { - if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { - nextHasEmote = true; - break; - } - } - } - - int lineHeight = baseHeight; - - if (isHeaderVec[idx]) { - // Header line spacing - lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; - } else { - // Base spacing for normal lines - int desiredBody = baseHeight + BODY_LINE_LEADING; - - if (hasEmote) { - // Emote line: add overshoot + bottom padding - int overshoot = std::max(0, tallestEmote - baseHeight); - lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; - } else { - // Regular line: no emote → standard spacing - lineHeight = desiredBody; - - // If next line has an emote → add top padding *here* - if (nextHasEmote) { - lineHeight += EMOTE_PADDING_ABOVE; - } - } - - // Add block gap if next is a header - if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { - lineHeight += MESSAGE_BLOCK_GAP; - } - } - - rowHeights.push_back(lineHeight); - } - - return rowHeights; -} - -void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) -{ - if (packet.from != 0) { - hasUnreadMessage = true; - - // Determine if message belongs to a muted channel - bool isChannelMuted = false; - if (sm.type == MessageType::BROADCAST) { - const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); - if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) - isChannelMuted = true; - } - - // Banner logic - const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); - char longName[48] = "???"; - if (node && node->user.long_name) { - strncpy(longName, node->user.long_name, sizeof(longName) - 1); - longName[sizeof(longName) - 1] = '\0'; - } - int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(longName); - while (longName[0] && display->getStringWidth(longName) > availWidth) { - longName[strlen(longName) - 1] = '\0'; - } - if (strlen(longName) < origLen) { - strcat(longName, "..."); - } - const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); - - char banner[256]; - bool isAlert = false; - - // Check if alert detection is enabled via external notification module - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_bell_buzzer) { - for (size_t i = 0; i < packet.decoded.payload.size && i < 100; i++) { - if (msgRaw[i] == '\x07') { - isAlert = true; - break; - } - } - } - - if (isAlert) { - if (longName && longName[0]) - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); - else - strcpy(banner, "Alert Received"); - } else { - // Skip muted channels unless it's an alert - if (isChannelMuted) - return; - - if (longName && longName[0]) { - if (currentResolution == ScreenResolution::UltraLow) { - strcpy(banner, "New Message"); - } else { - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); - } - } else - strcpy(banner, "New Message"); - } - - // Append context (which channel or DM) so the banner shows where the message arrived - { - char contextBuf[64] = ""; - if (sm.type == MessageType::BROADCAST) { - const char *cname = channels.getName(sm.channelIndex); - if (cname && cname[0]) - snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); - else - snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); - } - - if (contextBuf[0]) { - size_t cur = strlen(banner); - if (cur + 1 < sizeof(banner)) { - if (cur > 0 && banner[cur - 1] != '\n') { - banner[cur] = '\n'; - banner[cur + 1] = '\0'; - cur++; - } - strncat(banner, contextBuf, sizeof(banner) - cur - 1); - } - } - } - - // Shorter banner if already in a conversation (Channel or Direct) - bool inThread = (getThreadMode() != ThreadMode::ALL); - - if (shouldWakeOnReceivedMessage()) { - screen->setOn(true); - } - - screen->showSimpleBanner(banner, inThread ? 1000 : 3000); - } - - // Always focus into the correct conversation thread when a message with real text arrives - const char *msgText = MessageStore::getText(sm); - if (msgText && msgText[0] != '\0') { - setThreadFor(sm, packet); - } - - // Reset scroll for a clean start - resetScrollState(); -} - -void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) -{ - if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { - setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); } else { - uint32_t localNode = nodeDB->getNodeNum(); - uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; - setThreadMode(ThreadMode::DIRECT, -1, peer); + word += ch; + std::string test = line + word; +#if defined(OLED_UA) || defined(OLED_RU) + uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); +#else + uint16_t strWidth = display->getStringWidth(test.c_str()); +#endif + if (strWidth > textWidth) { + if (!line.empty()) + lines.push_back(line); + line = word; + word.clear(); + } } + } + + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + + return lines; +} +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, const std::vector &isHeaderVec) { + // Tunables for layout control + constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line + constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) + constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines + constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header + constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) + constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) + + std::vector rowHeights; + rowHeights.reserve(lines.size()); + + for (size_t idx = 0; idx < lines.size(); ++idx) { + const auto &line = lines[idx]; + const int baseHeight = FONT_HEIGHT_SMALL; + + // Detect if THIS line or NEXT line contains an emote + bool hasEmote = false; + int tallestEmote = baseHeight; + for (int i = 0; i < numEmotes; ++i) { + if (line.find(emotes[i].label) != std::string::npos) { + hasEmote = true; + tallestEmote = std::max(tallestEmote, emotes[i].height); + } + } + + bool nextHasEmote = false; + if (idx + 1 < lines.size()) { + for (int i = 0; i < numEmotes; ++i) { + if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { + nextHasEmote = true; + break; + } + } + } + + int lineHeight = baseHeight; + + if (isHeaderVec[idx]) { + // Header line spacing + lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; + } else { + // Base spacing for normal lines + int desiredBody = baseHeight + BODY_LINE_LEADING; + + if (hasEmote) { + // Emote line: add overshoot + bottom padding + int overshoot = std::max(0, tallestEmote - baseHeight); + lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; + } else { + // Regular line: no emote → standard spacing + lineHeight = desiredBody; + + // If next line has an emote → add top padding *here* + if (nextHasEmote) { + lineHeight += EMOTE_PADDING_ABOVE; + } + } + + // Add block gap if next is a header + if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { + lineHeight += MESSAGE_BLOCK_GAP; + } + } + + rowHeights.push_back(lineHeight); + } + + return rowHeights; +} + +void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) { + if (packet.from != 0) { + hasUnreadMessage = true; + + // Determine if message belongs to a muted channel + bool isChannelMuted = false; + if (sm.type == MessageType::BROADCAST) { + const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); + if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) + isChannelMuted = true; + } + + // Banner logic + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); + char longName[48] = "???"; + if (node && node->user.long_name) { + strncpy(longName, node->user.long_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } + int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(longName); + while (longName[0] && display->getStringWidth(longName) > availWidth) { + longName[strlen(longName) - 1] = '\0'; + } + if (strlen(longName) < origLen) { + strcat(longName, "..."); + } + const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); + + char banner[256]; + bool isAlert = false; + + // Check if alert detection is enabled via external notification module + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_bell_buzzer) { + for (size_t i = 0; i < packet.decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; + } + } + } + + if (isAlert) { + if (longName && longName[0]) + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + else + strcpy(banner, "Alert Received"); + } else { + // Skip muted channels unless it's an alert + if (isChannelMuted) + return; + + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else + strcpy(banner, "New Message"); + } + + // Append context (which channel or DM) so the banner shows where the message arrived + { + char contextBuf[64] = ""; + if (sm.type == MessageType::BROADCAST) { + const char *cname = channels.getName(sm.channelIndex); + if (cname && cname[0]) + snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); + else + snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); + } + + if (contextBuf[0]) { + size_t cur = strlen(banner); + if (cur + 1 < sizeof(banner)) { + if (cur > 0 && banner[cur - 1] != '\n') { + banner[cur] = '\n'; + banner[cur + 1] = '\0'; + cur++; + } + strncat(banner, contextBuf, sizeof(banner) - cur - 1); + } + } + } + + // Shorter banner if already in a conversation (Channel or Direct) + bool inThread = (getThreadMode() != ThreadMode::ALL); + + if (shouldWakeOnReceivedMessage()) { + screen->setOn(true); + } + + screen->showSimpleBanner(banner, inThread ? 1000 : 3000); + } + + // Always focus into the correct conversation thread when a message with real text arrives + const char *msgText = MessageStore::getText(sm); + if (msgText && msgText[0] != '\0') { + setThreadFor(sm, packet); + } + + // Reset scroll for a clean start + resetScrollState(); +} + +void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) { + if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { + setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); + } else { + uint32_t localNode = nodeDB->getNodeNum(); + uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; + setThreadMode(ThreadMode::DIRECT, -1, peer); + } } } // namespace MessageRenderer diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index 7dec6adec..cf27d0c1e 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -9,10 +9,8 @@ #include #include -namespace graphics -{ -namespace MessageRenderer -{ +namespace graphics { +namespace MessageRenderer { // Thread filter modes enum class ThreadMode { ALL, CHANNEL, DIRECT }; @@ -45,8 +43,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); // Function to calculate heights for each line -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, - const std::vector &isHeaderVec); +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, const std::vector &isHeaderVec); // Reset scroll state when new messages arrive void resetScrollState(); diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index e10d8c40a..3315856e9 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -13,8 +13,7 @@ #include // Forward declarations for functions defined in Screen.cpp -namespace graphics -{ +namespace graphics { extern bool haveGlyphs(const char *str); } // namespace graphics @@ -24,24 +23,21 @@ extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; #endif -namespace graphics -{ -namespace NodeListRenderer -{ +namespace graphics { +namespace NodeListRenderer { // Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here -void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) -{ - for (int row = 0; row < height; row++) { - uint8_t rowMask = (1 << row); - for (int col = 0; col < width; col++) { - uint8_t colData = pgm_read_byte(&bitmapXBM[col]); - if (colData & rowMask) { - // Note: rows become X, columns become Y after transpose - display->fillRect(x + row * 2, y + col * 2, 2, 2); - } - } +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) { + for (int row = 0; row < height; row++) { + uint8_t rowMask = (1 << row); + for (int col = 0; col < width; col++) { + uint8_t colData = pgm_read_byte(&bitmapXBM[col]); + if (colData & rowMask) { + // Note: rows become X, columns become Y after transpose + display->fillRect(x + row * 2, y + col * 2, 2, 2); + } } + } } // Static variables for dynamic cycling @@ -61,575 +57,543 @@ static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible // ============================= // Scrolling Logic // ============================= -void scrollUp() -{ - if (scrollIndex > 0) - scrollIndex--; +void scrollUp() { + if (scrollIndex > 0) + scrollIndex--; - popupTime = millis(); // show popup + popupTime = millis(); // show popup } -void scrollDown() -{ - scrollIndex++; - popupTime = millis(); +void scrollDown() { + scrollIndex++; + popupTime = millis(); } // ============================= // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) -{ - static char nodeName[25]; // single static buffer we return - nodeName[0] = '\0'; +const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { + static char nodeName[25]; // single static buffer we return + nodeName[0] = '\0'; - auto writeFallbackId = [&] { - std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); - }; + auto writeFallbackId = [&] { std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); }; - // 1) Choose target candidate (long vs short) only if present - const char *raw = nullptr; - if (node && node->has_user) { - raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; + // 1) Choose target candidate (long vs short) only if present + const char *raw = nullptr; + if (node && node->has_user) { + raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; + } + + // 2) Sanitize (empty if raw is null/empty) + std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; + + // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) + if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) { + writeFallbackId(); + } else { + // %.*s ensures null-termination and safe truncation to buffer size - 1 + std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); + } + + // 4) Width-based truncation + ellipsis (long-name mode only) + if (config.display.use_long_node_name && display) { + int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); + if (availWidth < 0) + availWidth = 0; + + const size_t beforeLen = std::strlen(nodeName); + + // Trim from the end until it fits or is empty + size_t len = beforeLen; + while (len && display->getStringWidth(nodeName) > availWidth) { + nodeName[--len] = '\0'; } - // 2) Sanitize (empty if raw is null/empty) - std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - - // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) - if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) { - writeFallbackId(); - } else { - // %.*s ensures null-termination and safe truncation to buffer size - 1 - std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); + // If truncated, append "..." (respect buffer size) + if (len < beforeLen) { + // Make sure there's room for "..." and '\0' + const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' + const size_t needed = 3; // "..." + if (len > capForText - needed) { + len = capForText - needed; + nodeName[len] = '\0'; + } + std::strcat(nodeName, "..."); } + } - // 4) Width-based truncation + ellipsis (long-name mode only) - if (config.display.use_long_node_name && display) { - int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); - if (availWidth < 0) - availWidth = 0; - - const size_t beforeLen = std::strlen(nodeName); - - // Trim from the end until it fits or is empty - size_t len = beforeLen; - while (len && display->getStringWidth(nodeName) > availWidth) { - nodeName[--len] = '\0'; - } - - // If truncated, append "..." (respect buffer size) - if (len < beforeLen) { - // Make sure there's room for "..." and '\0' - const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' - const size_t needed = 3; // "..." - if (len > capForText - needed) { - len = capForText - needed; - nodeName[len] = '\0'; - } - std::strcat(nodeName, "..."); - } - } - - return nodeName; + return nodeName; } -const char *getCurrentModeTitle_Nodes(int screenWidth) -{ - switch (currentMode_Nodes) { - case MODE_LAST_HEARD: - return "Last Heard"; - case MODE_HOP_SIGNAL: +const char *getCurrentModeTitle_Nodes(int screenWidth) { + switch (currentMode_Nodes) { + case MODE_LAST_HEARD: + return "Last Heard"; + case MODE_HOP_SIGNAL: #ifdef USE_EINK - return "Hops/Sig"; + return "Hops/Sig"; #else - return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; + return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; #endif - default: - return "Nodes"; - } + default: + return "Nodes"; + } } -const char *getCurrentModeTitle_Location(int screenWidth) -{ - switch (currentMode_Location) { - case MODE_DISTANCE: - return "Distance"; - case MODE_BEARING: - return "Bearings"; - default: - return "Nodes"; - } +const char *getCurrentModeTitle_Location(int screenWidth) { + switch (currentMode_Location) { + case MODE_DISTANCE: + return "Distance"; + case MODE_BEARING: + return "Bearings"; + default: + return "Nodes"; + } } // Use dynamic timing based on mode -unsigned long getModeCycleIntervalMs() -{ - return 3000; +unsigned long getModeCycleIntervalMs() { return 3000; } + +int calculateMaxScroll(int totalEntries, int visibleRows) { return std::max(0, (totalEntries - 1) / (visibleRows * 2)); } + +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { + for (int y = yStart; y <= yEnd; y += 2) { + display->setPixel(x, y); + } } -int calculateMaxScroll(int totalEntries, int visibleRows) -{ - return std::max(0, (totalEntries - 1) / (visibleRows * 2)); -} +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) { + if (totalEntries <= visibleNodeRows * columns) + return; -void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) -{ - for (int y = yStart; y <= yEnd; y += 2) { - display->setPixel(x, y); - } -} + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = display->getHeight() - scrollStartY - 10; + int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); + int perPage = visibleNodeRows * columns; + int maxScroll = std::max(0, (totalEntries - 1) / perPage); + int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); -void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) -{ - if (totalEntries <= visibleNodeRows * columns) - return; - - int scrollbarX = display->getWidth() - 2; - int scrollbarHeight = display->getHeight() - scrollStartY - 10; - int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); - int perPage = visibleNodeRows * columns; - int maxScroll = std::max(0, (totalEntries - 1) / perPage); - int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); - - for (int i = 0; i < thumbHeight; i++) { - display->setPixel(scrollbarX, thumbY + i); - } + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } } // ============================= // Entry Renderers // ============================= -void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const char *nodeName = getSafeNodeName(display, node, columnWidth); - char timeStr[10]; - uint32_t seconds = sinceLastSeen(node); - if (seconds == 0 || seconds == UINT32_MAX) { - snprintf(timeStr, sizeof(timeStr), "?"); + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "?"); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } + } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } - } - - int rightEdge = x + columnWidth - timeOffset; - if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time - rightEdge -= 1; - int textWidth = display->getStringWidth(timeStr); - display->drawString(rightEdge - textWidth, y, timeStr); + int rightEdge = x + columnWidth - timeOffset; + if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time + rightEdge -= 1; + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); } -void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - 25; - int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); - int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + int nameMaxWidth = columnWidth - 25; + int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); - int barsXOffset = columnWidth - barsOffset; + int barsXOffset = columnWidth - barsOffset; - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const char *nodeName = getSafeNodeName(display, node, columnWidth); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } + } - // Draw signal strength bars - int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; - int barWidth = 2; - int barStartX = x + barsXOffset; - int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + // Draw signal strength bars + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; - for (int b = 0; b < 4; b++) { - if (b < bars) { - int height = (b * 2); - display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); - } + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); } + } - // Draw hop count - char hopStr[6] = ""; - if (node->has_hops_away && node->hops_away > 0) - snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + // Draw hop count + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); - if (hopStr[0] != '\0') { - int rightEdge = x + columnWidth - hopOffset; - int textWidth = display->getStringWidth(hopStr); - display->drawString(rightEdge - textWidth, y, hopStr); - } + if (hopStr[0] != '\0') { + int rightEdge = x + columnWidth - hopOffset; + int textWidth = display->getStringWidth(hopStr); + display->drawString(rightEdge - textWidth, y, hopStr); + } } -void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = - columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node, columnWidth); - char distStr[10] = ""; + const char *nodeName = getSafeNodeName(display, node, columnWidth); + char distStr[10] = ""; - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; + double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet < 1000) - snprintf(distStr, sizeof(distStr), "%dft", feet); - else - snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max - } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles < 1000) - snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); - else - snprintf(distStr, sizeof(distStr), "999"); // Max display cap - } - } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters < 1000) - snprintf(distStr, sizeof(distStr), "%dm", meters); - else - snprintf(distStr, sizeof(distStr), "1k"); - } else { - int km = (int)(distanceKm + 0.5); - if (km < 1000) - snprintf(distStr, sizeof(distStr), "%dk", km); - else - snprintf(distStr, sizeof(distStr), "999"); - } - } + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet < 1000) + snprintf(distStr, sizeof(distStr), "%dft", feet); + else + snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles < 1000) + snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); + else + snprintf(distStr, sizeof(distStr), "999"); // Max display cap + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters < 1000) + snprintf(distStr, sizeof(distStr), "%dm", meters); + else + snprintf(distStr, sizeof(distStr), "1k"); + } else { + int km = (int)(distanceKm + 0.5); + if (km < 1000) + snprintf(distStr, sizeof(distStr), "%dk", km); + else + snprintf(distStr, sizeof(distStr), "999"); + } } + } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } + } - if (strlen(distStr) > 0) { - int offset = (currentResolution == ScreenResolution::High) - ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) - int rightEdge = x + columnWidth - offset; - int textWidth = display->getStringWidth(distStr); - display->drawString(rightEdge - textWidth, y, distStr); - } + if (strlen(distStr) > 0) { + int offset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distStr); + display->drawString(rightEdge - textWidth, y, distStr); + } } -void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - switch (currentMode_Nodes) { - case MODE_LAST_HEARD: - drawEntryLastHeard(display, node, x, y, columnWidth); - break; - case MODE_HOP_SIGNAL: - drawEntryHopSignal(display, node, x, y, columnWidth); - break; - default: - break; - } +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + switch (currentMode_Nodes) { + case MODE_LAST_HEARD: + drawEntryLastHeard(display, node, x, y, columnWidth); + break; + case MODE_HOP_SIGNAL: + drawEntryHopSignal(display, node, x, y, columnWidth); + break; + default: + break; + } } -void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + bool isLeftCol = (x < SCREEN_WIDTH / 2); - // Adjust max text width depending on column and screen width - int nameMaxWidth = - columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const char *nodeName = getSafeNodeName(display, node, columnWidth); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } + } } -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon) -{ - if (!nodeDB->hasValidPosition(node)) - return; +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, + double userLon) { + if (!nodeDB->hasValidPosition(node)) + return; - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); - int centerX = x + columnWidth - arrowXOffset; - int centerY = y + FONT_HEIGHT_SMALL / 2; + int centerX = x + columnWidth - arrowXOffset; + int centerY = y + FONT_HEIGHT_SMALL / 2; - double nodeLat = node->position.latitude_i * 1e-7; - double nodeLon = node->position.longitude_i * 1e-7; - float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); - float bearingToNode = RAD_TO_DEG * bearing; - float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); - // Shrink size by 2px - int size = FONT_HEIGHT_SMALL - 5; - CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); - /* - float angle = relativeBearing * DEG_TO_RAD; - float halfSize = size / 2.0; + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); + float bearingToNode = RAD_TO_DEG * bearing; + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + // Shrink size by 2px + int size = FONT_HEIGHT_SMALL - 5; + CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); + /* + float angle = relativeBearing * DEG_TO_RAD; + float halfSize = size / 2.0; - // Point of the arrow - int tipX = centerX + halfSize * cos(angle); - int tipY = centerY - halfSize * sin(angle); + // Point of the arrow + int tipX = centerX + halfSize * cos(angle); + int tipY = centerY - halfSize * sin(angle); - float baseAngle = radians(35); - float sideLen = halfSize * 0.95; - float notchInset = halfSize * 0.35; + float baseAngle = radians(35); + float sideLen = halfSize * 0.95; + float notchInset = halfSize * 0.35; - // Left and right corners - int leftX = centerX + sideLen * cos(angle + PI - baseAngle); - int leftY = centerY - sideLen * sin(angle + PI - baseAngle); + // Left and right corners + int leftX = centerX + sideLen * cos(angle + PI - baseAngle); + int leftY = centerY - sideLen * sin(angle + PI - baseAngle); - int rightX = centerX + sideLen * cos(angle + PI + baseAngle); - int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + int rightX = centerX + sideLen * cos(angle + PI + baseAngle); + int rightY = centerY - sideLen * sin(angle + PI + baseAngle); - // Center notch (cut-in) - int notchX = centerX - notchInset * cos(angle); - int notchY = centerY + notchInset * sin(angle); + // Center notch (cut-in) + int notchX = centerX - notchInset * cos(angle); + int notchY = centerY + notchInset * sin(angle); - // Draw the chevron-style arrowhead - display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); - display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); - */ + // Draw the chevron-style arrowhead + display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); + display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); + */ } // ============================= // Main Screen Functions // ============================= -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) -{ - const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; - bool locationScreen = false; +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, + NodeExtrasRenderer extras, float heading, double lat, double lon) { + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + bool locationScreen = false; - if (strcmp(title, "Bearings") == 0) - locationScreen = true; - else if (strcmp(title, "Distance") == 0) - locationScreen = true; - display->clear(); + if (strcmp(title, "Bearings") == 0) + locationScreen = true; + else if (strcmp(title, "Distance") == 0) + locationScreen = true; + display->clear(); - // Draw the battery/time header - graphics::drawCommonHeader(display, x, y, title); + // Draw the battery/time header + graphics::drawCommonHeader(display, x, y, title); - // Space below header - y += COMMON_HEADER_HEIGHT; + // Space below header + y += COMMON_HEADER_HEIGHT; - int totalColumns = 1; // Default to 1 column + int totalColumns = 1; // Default to 1 column - if (config.display.use_long_node_name) { - if (SCREEN_WIDTH <= 240) { - totalColumns = 1; - } else if (SCREEN_WIDTH > 240) { - totalColumns = 2; - } + if (config.display.use_long_node_name) { + if (SCREEN_WIDTH <= 240) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 240) { + totalColumns = 2; + } + } else { + if (SCREEN_WIDTH <= 64) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { + totalColumns = 2; } else { - if (SCREEN_WIDTH <= 64) { - totalColumns = 1; - } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { - totalColumns = 2; - } else { - totalColumns = 3; - } + totalColumns = 3; } + } - int columnWidth = display->getWidth() / totalColumns; + int columnWidth = display->getWidth() / totalColumns; - int totalEntries = nodeDB->getNumMeshNodes(); - int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; - int numskipped = 0; - int visibleNodeRows = totalRowsAvailable; + int totalEntries = nodeDB->getNumMeshNodes(); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + int numskipped = 0; + int visibleNodeRows = totalRowsAvailable; - // Build filtered + ordered list - std::vector drawList; - drawList.reserve(totalEntries); - for (int i = 0; i < totalEntries; i++) { - auto *n = nodeDB->getMeshNodeByIndex(i); + // Build filtered + ordered list + std::vector drawList; + drawList.reserve(totalEntries); + for (int i = 0; i < totalEntries; i++) { + auto *n = nodeDB->getMeshNodeByIndex(i); - if (!n) - continue; - if (n->num == nodeDB->getNodeNum()) - continue; - if (locationScreen && !n->has_position) - continue; + if (!n) + continue; + if (n->num == nodeDB->getNodeNum()) + continue; + if (locationScreen && !n->has_position) + continue; - drawList.push_back(n->num); + drawList.push_back(n->num); + } + totalEntries = drawList.size(); + int perPage = visibleNodeRows * totalColumns; + + int maxScroll = 0; + if (perPage > 0) { + maxScroll = std::max(0, (totalEntries - 1) / perPage); + } + + if (scrollIndex > maxScroll) + scrollIndex = maxScroll; + int startIndex = scrollIndex * visibleNodeRows * totalColumns; + int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + int yOffset = 0; + int col = 0; + int lastNodeY = y; + int shownCount = 0; + int rowCount = 0; + + for (int idx = startIndex; idx < endIndex; idx++) { + uint32_t nodeNum = drawList[idx]; + auto *node = nodeDB->getMeshNode(nodeNum); + int xPos = x + (col * columnWidth); + int yPos = y + yOffset; + + renderer(display, node, xPos, yPos, columnWidth); + + if (extras) + extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); + + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; + shownCount++; + rowCount++; + + if (rowCount >= totalRowsAvailable) { + yOffset = 0; + rowCount = 0; + col++; + if (col > (totalColumns - 1)) + break; } - totalEntries = drawList.size(); + } + + // This should correct the scrollbar + totalEntries -= numskipped; + + // Draw column separator + if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { + const int firstNodeY = y + 3; + for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { + drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); + } + } + + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); + graphics::drawCommonFooter(display, x, y); + + // Scroll Popup Overlay + if (millis() - popupTime < POPUP_DURATION_MS) { + popupTotal = totalEntries; + int perPage = visibleNodeRows * totalColumns; - int maxScroll = 0; - if (perPage > 0) { - maxScroll = std::max(0, (totalEntries - 1) / perPage); - } + popupStart = startIndex + 1; + popupEnd = std::min(startIndex + perPage, totalEntries); - if (scrollIndex > maxScroll) - scrollIndex = maxScroll; - int startIndex = scrollIndex * visibleNodeRows * totalColumns; - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); - int yOffset = 0; - int col = 0; - int lastNodeY = y; - int shownCount = 0; - int rowCount = 0; + popupPage = (scrollIndex + 1); + popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); - for (int idx = startIndex; idx < endIndex; idx++) { - uint32_t nodeNum = drawList[idx]; - auto *node = nodeDB->getMeshNode(nodeNum); - int xPos = x + (col * columnWidth); - int yPos = y + yOffset; + char buf[32]; + snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); - renderer(display, node, xPos, yPos, columnWidth); + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (extras) - extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); + // Box padding + int padding = 2; + int textW = display->getStringWidth(buf); + int textH = FONT_HEIGHT_SMALL; + int boxWidth = textW + padding * 3; + int boxHeight = textH + padding * 2; - lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); - yOffset += rowYOffset; - shownCount++; - rowCount++; + // Center of usable screen area: + int headerHeight = FONT_HEIGHT_SMALL - 1; + int footerHeight = FONT_HEIGHT_SMALL + 2; - if (rowCount >= totalRowsAvailable) { - yOffset = 0; - rowCount = 0; - col++; - if (col > (totalColumns - 1)) - break; - } - } + int usableTop = headerHeight; + int usableBottom = display->getHeight() - footerHeight; + int usableHeight = usableBottom - usableTop; - // This should correct the scrollbar - totalEntries -= numskipped; + // Center point inside usable area + int boxLeft = (display->getWidth() - boxWidth) / 2; + int boxTop = usableTop + (usableHeight - boxHeight) / 2; - // Draw column separator - if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { - const int firstNodeY = y + 3; - for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { - drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); - } - } + // Draw Box + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); - const int scrollStartY = y + 3; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); - graphics::drawCommonFooter(display, x, y); - - // Scroll Popup Overlay - if (millis() - popupTime < POPUP_DURATION_MS) { - popupTotal = totalEntries; - - int perPage = visibleNodeRows * totalColumns; - - popupStart = startIndex + 1; - popupEnd = std::min(startIndex + perPage, totalEntries); - - popupPage = (scrollIndex + 1); - popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); - - char buf[32]; - snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // Box padding - int padding = 2; - int textW = display->getStringWidth(buf); - int textH = FONT_HEIGHT_SMALL; - int boxWidth = textW + padding * 3; - int boxHeight = textH + padding * 2; - - // Center of usable screen area: - int headerHeight = FONT_HEIGHT_SMALL - 1; - int footerHeight = FONT_HEIGHT_SMALL + 2; - - int usableTop = headerHeight; - int usableBottom = display->getHeight() - footerHeight; - int usableHeight = usableBottom - usableTop; - - // Center point inside usable area - int boxLeft = (display->getWidth() - boxWidth) / 2; - int boxTop = usableTop + (usableHeight - boxHeight) / 2; - - // Draw Box - display->setColor(BLACK); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); - display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); - display->setColor(WHITE); - - // Text - display->drawString(boxLeft + padding, boxTop + padding, buf); - } + // Text + display->drawString(boxLeft + padding, boxTop + padding, buf); + } } // ============================= @@ -638,161 +602,154 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t #ifndef USE_EINK // Node list for Last Heard and Hop Signal views -void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Static variables to track mode and duration - static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; - static unsigned long modeStartTime = 0; +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Static variables to track mode and duration + static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; + static unsigned long modeStartTime = 0; - unsigned long now = millis(); + unsigned long now = millis(); #if defined(M5STACK_UNITC6L) - display->clear(); - if (now - lastSwitchTime >= 3000) { - display->display(); - lastSwitchTime = now; - } + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } #endif - // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT_NODE) { - currentMode_Nodes = MODE_LAST_HEARD; - modeStartTime = now; - } + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_NODE) { + currentMode_Nodes = MODE_LAST_HEARD; + modeStartTime = now; + } - // Time to switch to next mode? - if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); - modeStartTime = now; - } + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); + modeStartTime = now; + } - // Render screen based on currentMode - const char *title = getCurrentModeTitle_Nodes(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Nodes(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); - // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode_Nodes; + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Nodes; } // Node list for Distance and Bearings views -void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Static variables to track mode and duration - static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; - static unsigned long modeStartTime = 0; +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Static variables to track mode and duration + static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; + static unsigned long modeStartTime = 0; - unsigned long now = millis(); + unsigned long now = millis(); #if defined(M5STACK_UNITC6L) - display->clear(); - if (now - lastSwitchTime >= 3000) { - display->display(); - lastSwitchTime = now; - } + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } #endif - // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT_LOCATION) { - currentMode_Location = MODE_DISTANCE; - modeStartTime = now; - } + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_LOCATION) { + currentMode_Location = MODE_DISTANCE; + modeStartTime = now; + } - // Time to switch to next mode? - if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); - modeStartTime = now; - } + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); + modeStartTime = now; + } - // Render screen based on currentMode - const char *title = getCurrentModeTitle_Location(display->getWidth()); + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Location(display->getWidth()); - // Render screen based on currentMode_Location - if (currentMode_Location == MODE_DISTANCE) { - drawNodeListScreen(display, state, x, y, title, drawNodeDistance); - } else if (currentMode_Location == MODE_BEARING) { - drawNodeListWithCompasses(display, state, x, y); - } + // Render screen based on currentMode_Location + if (currentMode_Location == MODE_DISTANCE) { + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); + } else if (currentMode_Location == MODE_BEARING) { + drawNodeListWithCompasses(display, state, x, y); + } - // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode_Location; + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Location; } #endif #ifdef USE_EINK -void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const char *title = "Last Heard"; - drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + const char *title = "Last Heard"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); } -void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef USE_EINK - const char *title = "Hops/Sig"; + const char *title = "Hops/Sig"; #else - const char *title = "Hops/Signal"; + const char *title = "Hops/Signal"; #endif - drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); + drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); } -void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const char *title = "Distance"; - drawNodeListScreen(display, state, x, y, title, drawNodeDistance); +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + const char *title = "Distance"; + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); } #endif -void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - float heading = 0; - bool validHeading = false; - auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - double lat = DegD(ourNode->position.latitude_i); - double lon = DegD(ourNode->position.longitude_i); +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + float heading = 0; + bool validHeading = false; + auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + double lat = DegD(ourNode->position.latitude_i); + double lon = DegD(ourNode->position.longitude_i); #if defined(M5STACK_UNITC6L) - display->clear(); - uint32_t now = millis(); - if (now - lastSwitchTime >= 2000) { - display->display(); - lastSwitchTime = now; - } + display->clear(); + uint32_t now = millis(); + if (now - lastSwitchTime >= 2000) { + display->display(); + lastSwitchTime = now; + } #endif - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { #if HAS_GPS - if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees - validHeading = true; - } else { - heading = screen->estimatedHeading(lat, lon); - validHeading = !isnan(heading); - } + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } #endif - if (!validHeading) - return; - } - drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); + if (!validHeading) + return; + } + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } /// Draw a series of fields in a column, wrapping to multiple columns if needed -void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) -{ - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) { + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - const char **f = fields; - int xo = x, yo = y; - while (*f) { - display->drawString(xo, yo, *f); - if ((display->getColor() == BLACK) && config.display.heading_bold) - display->drawString(xo + 1, yo, *f); + const char **f = fields; + int xo = x, yo = y; + while (*f) { + display->drawString(xo, yo, *f); + if ((display->getColor() == BLACK) && config.display.heading_bold) + display->drawString(xo + 1, yo, *f); - display->setColor(WHITE); - yo += FONT_HEIGHT_SMALL; - if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { - xo += SCREEN_WIDTH / 2; - yo = 0; - } - f++; + display->setColor(WHITE); + yo += FONT_HEIGHT_SMALL; + if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { + xo += SCREEN_WIDTH / 2; + yo = 0; } + f++; + } } } // namespace NodeListRenderer diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index e212c031b..612e9c627 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -5,8 +5,7 @@ #include #include -namespace graphics -{ +namespace graphics { /// Forward declarations class Screen; @@ -17,8 +16,7 @@ class Screen; * Contains all functions related to drawing node lists and individual node entries * including last heard, hop signal, distance, and compass views. */ -namespace NodeListRenderer -{ +namespace NodeListRenderer { // Entry renderer function types typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); @@ -30,9 +28,8 @@ enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 }; // Main node list screen function -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, - double lon = 0); +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, + NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, double lon = 0); // Entry renderers void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); @@ -42,8 +39,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon); +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, + double userLon); // Screen frame functions void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 8d76b4592..aaca9b988 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -36,8 +36,7 @@ extern std::vector functionSymbol; extern std::string functionSymbolString; extern bool hasUnreadMessage; -namespace graphics -{ +namespace graphics { int bannerSignalBars = -1; InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; @@ -54,730 +53,711 @@ uint32_t NotificationRenderer::currentNumber = 0; VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; std::function NotificationRenderer::textInputCallback = nullptr; -uint32_t pow_of_10(uint32_t n) -{ - uint32_t ret = 1; - for (uint32_t i = 0; i < n; i++) { - ret *= 10; - } - return ret; +uint32_t pow_of_10(uint32_t n) { + uint32_t ret = 1; + for (uint32_t i = 0; i < n; i++) { + ret *= 10; + } + return ret; } // Used on boot when a certificate is being created -void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(64 + x, y, "Creating SSL certificate"); +void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(64 + x, y, "Creating SSL certificate"); #ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); + yield(); + esp_task_wdt_reset(); #endif - display->setFont(FONT_SMALL); - if ((millis() / 1000) % 2) { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); - } else { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); - } + display->setFont(FONT_SMALL); + if ((millis() / 1000) % 2) { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); + } else { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); + } } -void NotificationRenderer::resetBanner() -{ - notificationTypeEnum previousType = current_notification_type; +void NotificationRenderer::resetBanner() { + notificationTypeEnum previousType = current_notification_type; - alertBannerMessage[0] = '\0'; - current_notification_type = notificationTypeEnum::none; + alertBannerMessage[0] = '\0'; + current_notification_type = notificationTypeEnum::none; - OnScreenKeyboardModule::instance().clearPopup(); + OnScreenKeyboardModule::instance().clearPopup(); - inEvent.inputEvent = INPUT_BROKER_NONE; - inEvent.kbchar = 0; - curSelected = 0; - alertBannerOptions = 0; // last x lines are seelctable options - optionsArrayPtr = nullptr; - optionsEnumPtr = nullptr; - alertBannerCallback = NULL; - pauseBanner = false; - numDigits = 0; - currentNumber = 0; + inEvent.inputEvent = INPUT_BROKER_NONE; + inEvent.kbchar = 0; + curSelected = 0; + alertBannerOptions = 0; // last x lines are seelctable options + optionsArrayPtr = nullptr; + optionsEnumPtr = nullptr; + alertBannerCallback = NULL; + pauseBanner = false; + numDigits = 0; + currentNumber = 0; - nodeDB->pause_sort(false); + nodeDB->pause_sort(false); - // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update - // to ensure any messages received during keyboard use are now displayed - if (previousType == notificationTypeEnum::text_input && screen) { - OnScreenKeyboardModule::instance().stop(false); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } + // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update + // to ensure any messages received during keyboard use are now displayed + if (previousType == notificationTypeEnum::text_input && screen) { + OnScreenKeyboardModule::instance().stop(false); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } } -void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // Handle text_input notifications first - they have their own timeout/banner logic - if (current_notification_type == notificationTypeEnum::text_input) { - // Check for timeout and reset if needed for text input - if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); - return; - } - drawTextInput(display, state); - return; - } - +void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { + // Handle text_input notifications first - they have their own timeout/banner logic + if (current_notification_type == notificationTypeEnum::text_input) { + // Check for timeout and reset if needed for text input if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); + resetBanner(); + return; } + drawTextInput(display, state); + return; + } - // Exit if no banner is showing or banner is paused - if (!isOverlayBannerShowing() || pauseBanner) { - return; - } + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + } - switch (current_notification_type) { - case notificationTypeEnum::none: - // Do nothing - no notification to display - break; - case notificationTypeEnum::text_input: - // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. - break; - case notificationTypeEnum::text_banner: - case notificationTypeEnum::selection_picker: - drawAlertBannerOverlay(display, state); - break; - case notificationTypeEnum::node_picker: - drawNodePicker(display, state); - break; - case notificationTypeEnum::number_picker: - drawNumberPicker(display, state); - break; - } + // Exit if no banner is showing or banner is paused + if (!isOverlayBannerShowing() || pauseBanner) { + return; + } + + switch (current_notification_type) { + case notificationTypeEnum::none: + // Do nothing - no notification to display + break; + case notificationTypeEnum::text_input: + // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. + break; + case notificationTypeEnum::text_banner: + case notificationTypeEnum::selection_picker: + drawAlertBannerOverlay(display, state); + break; + case notificationTypeEnum::node_picker: + drawNodePicker(display, state); + break; + case notificationTypeEnum::number_picker: + drawNumberPicker(display, state); + break; + } } -void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - const char *lineStarts[MAX_LINES + 1] = {0}; - uint16_t lineCount = 0; +void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) { + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; - // Parse lines - char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); - lineStarts[lineCount] = alertBannerMessage; + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; - // Find lines - while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); - if (lineStarts[lineCount + 1][0] == '\n') - lineStarts[lineCount + 1] += 1; - lineCount++; + // Find lines + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + // modulo to extract + uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); + // Handle input + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { + if (this_digit == 9) { + currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber += (pow_of_10(numDigits - curSelected - 1)); } - // modulo to extract - uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); - // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || + inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { + if (this_digit == 0) { + currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber -= (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) { + if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit + currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1)); + currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1)); + curSelected++; + } + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { + curSelected--; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + resetBanner(); + return; + } + if (curSelected == static_cast(numDigits)) { + alertBannerCallback(currentNumber); + resetBanner(); + return; + } + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + 2; + const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (uint16_t i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + std::string digits = " "; + std::string arrowPointer = " "; + for (uint16_t i = 0; i < numDigits; i++) { + // Modulo minus modulo to return just the current number + digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; + if (curSelected == i) { + arrowPointer += "^ "; + } else { + arrowPointer += "_ "; + } + } + + linePointers[lineCount++] = digits.c_str(); + linePointers[lineCount++] = arrowPointer.c_str(); + + drawNotificationBox(display, state, linePointers, totalLines, 0); +} + +void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) { + static uint32_t selectedNodenum = 0; + + // === Layout Configuration === + constexpr uint16_t vPadding = 2; + alertBannerOptions = nodeDB->getNumMeshNodes() - 1; + + // let the box drawing function calculate the widths? + + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + + // Handle input + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || + inEvent.inputEvent == INPUT_BROKER_UP_LONG) { + curSelected--; + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || + inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + alertBannerCallback(selectedNodenum); + resetBanner(); + return; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + resetBanner(); + return; + } + + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + char scratchLineBuffer[visibleTotalLines - lineCount][40]; + + uint8_t firstOptionToShow = 0; + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + int scratchLineNum = 0; + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + char temp_name[16] = {0}; + if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { + std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); + strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + } else { + snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + } + if (i == curSelected) { + selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + if (currentResolution == ScreenResolution::High) { + strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + } else { + strncpy(scratchLineBuffer[scratchLineNum], ">", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + } + scratchLineBuffer[scratchLineNum][39] = '\0'; + } else { + strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); + scratchLineBuffer[scratchLineNum][39] = '\0'; + } + linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; + } + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); +} + +void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { + // === Layout Configuration === + constexpr uint16_t vPadding = 2; + + uint16_t optionWidths[alertBannerOptions] = {0}; + uint16_t maxWidth = 0; + uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); + uint16_t lineWidths[MAX_LINES] = {0}; + uint16_t lineLengths[MAX_LINES] = {0}; + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + char lineBuffer[40] = {0}; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; + lineCount++; + } + + // Measure option widths + for (int i = 0; i < alertBannerOptions; i++) { + optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); + if (optionWidths[i] > maxWidth) + maxWidth = optionWidths[i]; + if (optionWidths[i] + arrowsWidth > maxWidth) + maxWidth = optionWidths[i] + arrowsWidth; + } + + // Handle input + if (alertBannerOptions > 0) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - if (this_digit == 9) { - currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); - } else { - currentNumber += (pow_of_10(numDigits - curSelected - 1)); - } - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || + curSelected--; + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - if (this_digit == 0) { - currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); - } else { - currentNumber -= (pow_of_10(numDigits - curSelected - 1)); - } - } else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) { - if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit - currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1)); - currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1)); - curSelected++; - } - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) { - curSelected++; - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - curSelected--; - } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && - alertBannerUntil != 0) { - resetBanner(); - return; - } - if (curSelected == static_cast(numDigits)) { - alertBannerCallback(currentNumber); - resetBanner(); - return; - } - - inEvent.inputEvent = INPUT_BROKER_NONE; - if (alertBannerMessage[0] == '\0') - return; - - uint16_t totalLines = lineCount + 2; - const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation - - // copy the linestarts to display to the linePointers holder - for (uint16_t i = 0; i < lineCount; i++) { - linePointers[i] = lineStarts[i]; - } - std::string digits = " "; - std::string arrowPointer = " "; - for (uint16_t i = 0; i < numDigits; i++) { - // Modulo minus modulo to return just the current number - digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; - if (curSelected == i) { - arrowPointer += "^ "; - } else { - arrowPointer += "_ "; - } - } - - linePointers[lineCount++] = digits.c_str(); - linePointers[lineCount++] = arrowPointer.c_str(); - - drawNotificationBox(display, state, linePointers, totalLines, 0); -} - -void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - static uint32_t selectedNodenum = 0; - - // === Layout Configuration === - constexpr uint16_t vPadding = 2; - alertBannerOptions = nodeDB->getNumMeshNodes() - 1; - - // let the box drawing function calculate the widths? - - const char *lineStarts[MAX_LINES + 1] = {0}; - uint16_t lineCount = 0; - - // Parse lines - char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); - lineStarts[lineCount] = alertBannerMessage; - - while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); - if (lineStarts[lineCount + 1][0] == '\n') - lineStarts[lineCount + 1] += 1; - lineCount++; - } - - // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || - inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || - inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - curSelected++; + curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - alertBannerCallback(selectedNodenum); - resetBanner(); - return; - } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && - alertBannerUntil != 0) { - resetBanner(); - return; + if (optionsEnumPtr != nullptr) { + alertBannerCallback(optionsEnumPtr[curSelected]); + optionsEnumPtr = nullptr; + } else { + alertBannerCallback(curSelected); + } + resetBanner(); + return; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + resetBanner(); + return; } if (curSelected == -1) - curSelected = alertBannerOptions - 1; + curSelected = alertBannerOptions - 1; if (curSelected == alertBannerOptions) - curSelected = 0; - - inEvent.inputEvent = INPUT_BROKER_NONE; - if (alertBannerMessage[0] == '\0') - return; - - uint16_t totalLines = lineCount + alertBannerOptions; - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint8_t linesShown = lineCount; - const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation - - // copy the linestarts to display to the linePointers holder - for (int i = 0; i < lineCount; i++) { - linePointers[i] = lineStarts[i]; + curSelected = 0; + } else { + if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_ALT_LONG || inEvent.inputEvent == INPUT_BROKER_CANCEL) { + resetBanner(); + return; } - char scratchLineBuffer[visibleTotalLines - lineCount][40]; + } - uint8_t firstOptionToShow = 0; - if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { - if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) - firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; - else - firstOptionToShow = curSelected - 1; + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + + uint8_t firstOptionToShow = 0; + if (alertBannerOptions > 0) { + if (visibleTotalLines - lineCount == 1) { + firstOptionToShow = curSelected; + } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; } else { - firstOptionToShow = 0; + firstOptionToShow = 0; } - int scratchLineNum = 0; - for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - char temp_name[16] = {0}; - if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { - std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); - strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); - } else { - snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); - } - if (i == curSelected) { - selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; - if (currentResolution == ScreenResolution::High) { - strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); - strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); - } else { - strncpy(scratchLineBuffer[scratchLineNum], ">", 2); - strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); - } - scratchLineBuffer[scratchLineNum][39] = '\0'; - } else { - strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); - scratchLineBuffer[scratchLineNum][39] = '\0'; - } - linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; + } + // Useful log line for troubleshooting: + /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u", + alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */ + + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + if (i == curSelected) { + if (currentResolution == ScreenResolution::High) { + strncpy(lineBuffer, "> ", 3); + strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); + } else { + strncpy(lineBuffer, ">", 2); + strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); + } + lineBuffer[39] = '\0'; + linePointers[linesShown] = lineBuffer; + } else { + linePointers[linesShown] = optionsArrayPtr[i]; } + } + if (alertBannerOptions > 0) { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); + } else { drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); + } } -void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // === Layout Configuration === - constexpr uint16_t vPadding = 2; +void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], uint16_t totalLines, + uint8_t firstOptionToShow, uint16_t maxWidth) { - uint16_t optionWidths[alertBannerOptions] = {0}; - uint16_t maxWidth = 0; - uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); - uint16_t lineWidths[MAX_LINES] = {0}; - uint16_t lineLengths[MAX_LINES] = {0}; - const char *lineStarts[MAX_LINES + 1] = {0}; - uint16_t lineCount = 0; - char lineBuffer[40] = {0}; + bool is_picker = false; + uint16_t lineCount = 0; + // Layout Configuration + constexpr uint16_t hPadding = 5; + constexpr uint16_t vPadding = 2; + bool needs_bell = false; + uint16_t lineWidths[totalLines] = {0}; + uint16_t lineLengths[totalLines] = {0}; - // Parse lines - char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); - lineStarts[lineCount] = alertBannerMessage; + if (maxWidth != 0) + is_picker = true; - while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); - lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; - if (lineStarts[lineCount + 1][0] == '\n') - lineStarts[lineCount + 1] += 1; - lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); - if (lineWidths[lineCount] > maxWidth) - maxWidth = lineWidths[lineCount]; - lineCount++; + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Track widest line INCLUDING bars (but don't change per-line widths) + uint16_t widestLineWithBars = 0; + + while (lines[lineCount] != nullptr) { + auto newlinePointer = strchr(lines[lineCount], '\n'); + if (newlinePointer) + lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first + else // if the newline wasn't found, then pull string length from strlen + lineLengths[lineCount] = strlen(lines[lineCount]); + + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + + // Consider extra width for signal bars on lines that contain "Signal:" + uint16_t potentialWidth = lineWidths[lineCount]; + if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int gap = 6; // space between text and bars + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + potentialWidth += barsWidth; } - // Measure option widths - for (int i = 0; i < alertBannerOptions; i++) { - optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); - if (optionWidths[i] > maxWidth) - maxWidth = optionWidths[i]; - if (optionWidths[i] + arrowsWidth > maxWidth) - maxWidth = optionWidths[i] + arrowsWidth; + if (potentialWidth > widestLineWithBars) + widestLineWithBars = potentialWidth; + + if (!is_picker) { + needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; } + lineCount++; + } + // count lines - // Handle input - if (alertBannerOptions > 0) { - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || - inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || - inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - curSelected++; - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - if (optionsEnumPtr != nullptr) { - alertBannerCallback(optionsEnumPtr[curSelected]); - optionsEnumPtr = nullptr; - } else { - alertBannerCallback(curSelected); - } - resetBanner(); - return; - } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && - alertBannerUntil != 0) { - resetBanner(); - return; - } + // Ensure box accounts for signal bars if present + if (widestLineWithBars > maxWidth) + maxWidth = widestLineWithBars; - if (curSelected == -1) - curSelected = alertBannerOptions - 1; - if (curSelected == alertBannerOptions) - curSelected = 0; - } else { - if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_ALT_LONG || - inEvent.inputEvent == INPUT_BROKER_CANCEL) { - resetBanner(); - return; - } - } + uint16_t boxWidth = hPadding * 2 + maxWidth; - inEvent.inputEvent = INPUT_BROKER_NONE; - if (alertBannerMessage[0] == '\0') - return; + if (needs_bell) { + if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) + boxWidth += 26; + if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) + boxWidth += 20; + } - uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; + uint16_t boxHeight = contentHeight + vPadding * 2; + if (visibleTotalLines == 1) { + boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; + } - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint8_t linesShown = lineCount; - const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation - - // copy the linestarts to display to the linePointers holder - for (int i = 0; i < lineCount; i++) { - linePointers[i] = lineStarts[i]; - } - - uint8_t firstOptionToShow = 0; - if (alertBannerOptions > 0) { - if (visibleTotalLines - lineCount == 1) { - firstOptionToShow = curSelected; - } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { - if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) - firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; - else - firstOptionToShow = curSelected - 1; - } else { - firstOptionToShow = 0; - } - } - // Useful log line for troubleshooting: - /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u", - alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */ - - for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - if (i == curSelected) { - if (currentResolution == ScreenResolution::High) { - strncpy(lineBuffer, "> ", 3); - strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); - strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); - } else { - strncpy(lineBuffer, ">", 2); - strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); - strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); - } - lineBuffer[39] = '\0'; - linePointers[linesShown] = lineBuffer; - } else { - linePointers[linesShown] = optionsArrayPtr[i]; - } - } - if (alertBannerOptions > 0) { - drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); - } else { - drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); - } -} - -void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], - uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth) -{ - - bool is_picker = false; - uint16_t lineCount = 0; - // Layout Configuration - constexpr uint16_t hPadding = 5; - constexpr uint16_t vPadding = 2; - bool needs_bell = false; - uint16_t lineWidths[totalLines] = {0}; - uint16_t lineLengths[totalLines] = {0}; - - if (maxWidth != 0) - is_picker = true; - - // Setup font and alignment - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // Track widest line INCLUDING bars (but don't change per-line widths) - uint16_t widestLineWithBars = 0; - - while (lines[lineCount] != nullptr) { - auto newlinePointer = strchr(lines[lineCount], '\n'); - if (newlinePointer) - lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first - else // if the newline wasn't found, then pull string length from strlen - lineLengths[lineCount] = strlen(lines[lineCount]); - - lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); - - // Consider extra width for signal bars on lines that contain "Signal:" - uint16_t potentialWidth = lineWidths[lineCount]; - if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { - const int totalBars = 5; - const int barWidth = 3; - const int barSpacing = 2; - const int gap = 6; // space between text and bars - int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; - potentialWidth += barsWidth; - } - - if (potentialWidth > widestLineWithBars) - widestLineWithBars = potentialWidth; - - if (!is_picker) { - needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); - if (lineWidths[lineCount] > maxWidth) - maxWidth = lineWidths[lineCount]; - } - lineCount++; - } - // count lines - - // Ensure box accounts for signal bars if present - if (widestLineWithBars > maxWidth) - maxWidth = widestLineWithBars; - - uint16_t boxWidth = hPadding * 2 + maxWidth; - - if (needs_bell) { - if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) - boxWidth += 26; - if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) - boxWidth += 20; - } - - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; - uint16_t boxHeight = contentHeight + vPadding * 2; - if (visibleTotalLines == 1) { - boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; - } - - int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - if (totalLines > visibleTotalLines) { - boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; - } - int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + if (totalLines > visibleTotalLines) { + boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; + } + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; #if defined(M5STACK_UNITC6L) - if (visibleTotalLines == 1) { - boxTop += 25; - } - if (alertBannerOptions < 3) { - int missingLines = 3 - alertBannerOptions; - int moveUp = missingLines * (effectiveLineHeight / 2); - boxTop -= moveUp; - if (boxTop < 0) - boxTop = 0; - } + if (visibleTotalLines == 1) { + boxTop += 25; + } + if (alertBannerOptions < 3) { + int missingLines = 3 - alertBannerOptions; + int moveUp = missingLines * (effectiveLineHeight / 2); + boxTop -= moveUp; + if (boxTop < 0) + boxTop = 0; + } #endif - // Draw Box - display->setColor(BLACK); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); - display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); - display->setColor(WHITE); + // Draw Box + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); - // Draw Content - int16_t lineY = boxTop + vPadding; - for (int i = 0; i < lineCount; i++) { - int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; - if (needs_bell && i == 0) { - int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; - display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); - display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); - } - char lineBuffer[lineLengths[i] + 1]; - strncpy(lineBuffer, lines[i], lineLengths[i]); - lineBuffer[lineLengths[i]] = '\0'; - // Determine if this is a pop-up or a pick list - if (alertBannerOptions > 0 && i == 0) { - // Pick List - display->setColor(WHITE); - int background_yOffset = 1; - // Determine if we have low hanging characters - if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { - background_yOffset = -1; - } - display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); - display->setColor(BLACK); - int yOffset = 3; - display->drawString(textX, lineY - yOffset, lineBuffer); - display->setColor(WHITE); - lineY += (effectiveLineHeight - 2 - background_yOffset); - } else { - // Pop-up - // If this is the Signal line, center text + bars as one group - bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); - if (isSignalLine) { - const int totalBars = 5; - const int barWidth = 3; - const int barSpacing = 2; - const int barHeightStep = 2; - const int gap = 6; - - int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); - int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; - int totalWidth = textWidth + barsWidth; - int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; - - display->drawString(groupStartX, lineY, lineBuffer); - - int baseX = groupStartX + textWidth + gap; - int baseY = lineY + effectiveLineHeight - 1; - for (int b = 0; b < totalBars; b++) { - int barHeight = (b + 1) * barHeightStep; - int x = baseX + b * (barWidth + barSpacing); - int y = baseY - barHeight; - - if (b < graphics::bannerSignalBars) { - display->fillRect(x, y, barWidth, barHeight); - } else { - display->drawRect(x, y, barWidth, barHeight); - } - } - } else { - display->drawString(textX, lineY, lineBuffer); - } - lineY += (effectiveLineHeight); - } + // Draw Content + int16_t lineY = boxTop + vPadding; + for (int i = 0; i < lineCount; i++) { + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + if (needs_bell && i == 0) { + int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); + display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); } + char lineBuffer[lineLengths[i] + 1]; + strncpy(lineBuffer, lines[i], lineLengths[i]); + lineBuffer[lineLengths[i]] = '\0'; + // Determine if this is a pop-up or a pick list + if (alertBannerOptions > 0 && i == 0) { + // Pick List + display->setColor(WHITE); + int background_yOffset = 1; + // Determine if we have low hanging characters + if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { + background_yOffset = -1; + } + display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); + display->setColor(BLACK); + int yOffset = 3; + display->drawString(textX, lineY - yOffset, lineBuffer); + display->setColor(WHITE); + lineY += (effectiveLineHeight - 2 - background_yOffset); + } else { + // Pop-up + // If this is the Signal line, center text + bars as one group + bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); + if (isSignalLine) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int barHeightStep = 2; + const int gap = 6; - // Scroll Bar (Thicker, inside box, not over title) - if (totalLines > visibleTotalLines) { - const uint8_t scrollBarWidth = 5; - int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; - int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; - uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + int totalWidth = textWidth + barsWidth; + int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; - float ratio = (float)visibleTotalLines / totalLines; - uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); - float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); - uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + display->drawString(groupStartX, lineY, lineBuffer); - display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); - display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); + int baseX = groupStartX + textWidth + gap; + int baseY = lineY + effectiveLineHeight - 1; + for (int b = 0; b < totalBars; b++) { + int barHeight = (b + 1) * barHeightStep; + int x = baseX + b * (barWidth + barSpacing); + int y = baseY - barHeight; + + if (b < graphics::bannerSignalBars) { + display->fillRect(x, y, barWidth, barHeight); + } else { + display->drawRect(x, y, barWidth, barHeight); + } + } + } else { + display->drawString(textX, lineY, lineBuffer); + } + lineY += (effectiveLineHeight); } + } + + // Scroll Bar (Thicker, inside box, not over title) + if (totalLines > visibleTotalLines) { + const uint8_t scrollBarWidth = 5; + int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; + uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + + float ratio = (float)visibleTotalLines / totalLines; + uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); + float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); + uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + + display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); + display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); + } } /// Draw the last text message we received -void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); +void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); - char tempBuf[24]; - snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); - display->drawString(0 + x, 0 + y, tempBuf); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); + char tempBuf[24]; + snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); + display->drawString(0 + x, 0 + y, tempBuf); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); } -void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, y, "Updating"); +void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(64 + x, y, "Updating"); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), - "Please be patient and do not power off."); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), "Please be patient and do not power off."); } -void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - if (virtualKeyboard) { - // Check for timeout and auto-exit if needed - if (virtualKeyboard->isTimedOut()) { - LOG_INFO("Virtual keyboard timeout - auto-exiting"); - // Cancel virtual keyboard - call callback with empty string to indicate timeout - auto callback = textInputCallback; // Store callback before clearing +void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) { + if (virtualKeyboard) { + // Check for timeout and auto-exit if needed + if (virtualKeyboard->isTimedOut()) { + LOG_INFO("Virtual keyboard timeout - auto-exiting"); + // Cancel virtual keyboard - call callback with empty string to indicate timeout + auto callback = textInputCallback; // Store callback before clearing - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); - // Call callback after cleanup - if (callback) { - callback(""); - } + // Call callback after cleanup + if (callback) { + callback(""); + } - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - if (inEvent.inputEvent != INPUT_BROKER_NONE) { - bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); - if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { - auto callback = textInputCallback; - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - if (callback) { - callback(""); - } - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Consume the event after processing for virtual keyboard - inEvent.inputEvent = INPUT_BROKER_NONE; - } - - // Re-check pointer before drawing to avoid use-after-free and crashes - if (!virtualKeyboard) { - // Ensure we exit text_input state and restore frames - if (current_notification_type == notificationTypeEnum::text_input) { - resetBanner(); - } - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - // If screen is null, do nothing (safe fallback) - return; - } - - // Clear the screen to avoid overlapping with underlying frames or overlays - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - // Draw the virtual keyboard - virtualKeyboard->draw(display, 0, 0); - - // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule - OnScreenKeyboardModule::instance().drawPopupOverlay(display); - } else { - // If virtualKeyboard is null, reset the banner to avoid getting stuck - LOG_INFO("Virtual keyboard is null - resetting banner"); - resetBanner(); + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; } -} -bool NotificationRenderer::isOverlayBannerShowing() -{ - return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); -} - -void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) -{ - if (!title || !content || current_notification_type != notificationTypeEnum::text_input) + if (inEvent.inputEvent != INPUT_BROKER_NONE) { + bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); + if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { + auto callback = textInputCallback; + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + if (callback) { + callback(""); + } + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } return; - OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); + } + + // Consume the event after processing for virtual keyboard + inEvent.inputEvent = INPUT_BROKER_NONE; + } + + // Re-check pointer before drawing to avoid use-after-free and crashes + if (!virtualKeyboard) { + // Ensure we exit text_input state and restore frames + if (current_notification_type == notificationTypeEnum::text_input) { + resetBanner(); + } + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + // If screen is null, do nothing (safe fallback) + return; + } + + // Clear the screen to avoid overlapping with underlying frames or overlays + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + // Draw the virtual keyboard + virtualKeyboard->draw(display, 0, 0); + + // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule + OnScreenKeyboardModule::instance().drawPopupOverlay(display); + } else { + // If virtualKeyboard is null, reset the banner to avoid getting stuck + LOG_INFO("Virtual keyboard is null - resetting banner"); + resetBanner(); + } +} + +bool NotificationRenderer::isOverlayBannerShowing() { + return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); +} + +void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) { + if (!title || !content || current_notification_type != notificationTypeEnum::text_input) + return; + OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); } } // namespace graphics diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index e51bfa5ab..bdc85f3b0 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -9,44 +9,42 @@ #include #define MAX_LINES 5 -namespace graphics -{ +namespace graphics { -class NotificationRenderer -{ - public: - static InputEvent inEvent; - static char inKeypress; - static int8_t curSelected; - static char alertBannerMessage[256]; - static uint32_t alertBannerUntil; // 0 is a special case meaning forever - static const char **optionsArrayPtr; - static const int *optionsEnumPtr; - static uint8_t alertBannerOptions; // last x lines are seelctable options - static std::function alertBannerCallback; - static uint32_t numDigits; - static uint32_t currentNumber; - static VirtualKeyboard *virtualKeyboard; - static std::function textInputCallback; +class NotificationRenderer { +public: + static InputEvent inEvent; + static char inKeypress; + static int8_t curSelected; + static char alertBannerMessage[256]; + static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static const char **optionsArrayPtr; + static const int *optionsEnumPtr; + static uint8_t alertBannerOptions; // last x lines are seelctable options + static std::function alertBannerCallback; + static uint32_t numDigits; + static uint32_t currentNumber; + static VirtualKeyboard *virtualKeyboard; + static std::function textInputCallback; - static bool pauseBanner; + static bool pauseBanner; - static void resetBanner(); - static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); - static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], - uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); + static void resetBanner(); + static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); + static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, + uint8_t firstOptionToShow, uint16_t maxWidth = 0); - static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static bool isOverlayBannerShowing(); + static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static bool isOverlayBannerShowing(); - static graphics::notificationTypeEnum current_notification_type; + static graphics::notificationTypeEnum current_notification_type; }; } // namespace graphics diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 7ce9d5afe..fa41e4e30 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -21,35 +21,32 @@ extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; #endif -namespace graphics -{ +namespace graphics { NodeNum UIRenderer::currentFavoriteNodeNum = 0; std::vector graphics::UIRenderer::favoritedNodes; -static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) -{ - int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); - } else { - display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); - } +static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) { + int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); + } } -void graphics::UIRenderer::rebuildFavoritedNodes() -{ - favoritedNodes.clear(); - size_t total = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < total; i++) { - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (!n || n->num == nodeDB->getNodeNum()) - continue; - if (n->is_favorite) - favoritedNodes.push_back(n); - } +void graphics::UIRenderer::rebuildFavoritedNodes() { + favoritedNodes.clear(); + size_t total = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < total; i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (!n || n->num == nodeDB->getNodeNum()) + continue; + if (n->is_favorite) + favoritedNodes.push_back(n); + } - std::sort(favoritedNodes.begin(), favoritedNodes.end(), - [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + std::sort(favoritedNodes.begin(), favoritedNodes.end(), + [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } #if !MESHTASTIC_EXCLUDE_GPS @@ -60,1147 +57,1109 @@ extern GeoCoord geoCoord; extern uint32_t dopThresholds[5]; // Draw GPS status summary -void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) -{ - // Draw satellite image - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); - } else { - display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); - } - char textString[10]; +void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { + // Draw satellite image + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); + } + char textString[10]; - if (config.position.fixed_position) { - // GPS coordinates are currently fixed - snprintf(textString, sizeof(textString), "Fixed"); - } - if (!gps->getIsConnected()) { - snprintf(textString, sizeof(textString), "No Lock"); - } - if (!gps->getHasLock()) { - // Draw "No sats" to the right of the icon with slightly more gap - snprintf(textString, sizeof(textString), "No Sats"); - } else { - snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); - } - if (currentResolution == ScreenResolution::High) { - display->drawString(x + 18, y, textString); - } else { - display->drawString(x + 11, y, textString); - } + if (config.position.fixed_position) { + // GPS coordinates are currently fixed + snprintf(textString, sizeof(textString), "Fixed"); + } + if (!gps->getIsConnected()) { + snprintf(textString, sizeof(textString), "No Lock"); + } + if (!gps->getHasLock()) { + // Draw "No sats" to the right of the icon with slightly more gap + snprintf(textString, sizeof(textString), "No Sats"); + } else { + snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); + } + if (currentResolution == ScreenResolution::High) { + display->drawString(x + 18, y, textString); + } else { + display->drawString(x + 11, y, textString); + } } // Draw status when GPS is disabled or not present -void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) -{ - const char *displayLine; - int pos; - if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - pos = display->getWidth() - display->getStringWidth(displayLine); - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" - : "GPS is disabled"; - pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; - } - display->drawString(x + pos, y, displayLine); +void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { + const char *displayLine; + int pos; + if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + pos = display->getWidth() - display->getStringWidth(displayLine); + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" : "GPS is disabled"; + pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; + } + display->drawString(x + pos, y, displayLine); } -void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) -{ - char displayLine[32]; - if (!gps->getIsConnected() && !config.position.fixed_position) { - // displayLine = "No GPS Module"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - // displayLine = "No GPS Lock"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); - else - snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); - display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } +void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { + char displayLine[32]; + if (!gps->getIsConnected() && !config.position.fixed_position) { + // displayLine = "No GPS Module"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + // displayLine = "No GPS Lock"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + else + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } } // Draw GPS status coordinates -void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, - const char *mode) -{ - auto gpsFormat = uiconfig.gps_format; - char displayLine[32]; +void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, const char *mode) { + auto gpsFormat = uiconfig.gps_format; + char displayLine[32]; - if (!gps->getIsConnected() && !config.position.fixed_position) { - if (strcmp(mode, "line1") == 0) { - strcpy(displayLine, "No GPS present"); - display->drawString(x, y, displayLine); - } - } else if (!gps->getHasLock() && !config.position.fixed_position) { - if (strcmp(mode, "line1") == 0) { - strcpy(displayLine, "No GPS Lock"); - display->drawString(x, y, displayLine); - } - } else { - - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - - if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { - char coordinateLine_1[22]; - char coordinateLine_2[22]; - if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), - geoCoord.getUTMBand(), geoCoord.getUTMEasting()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), - geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), - geoCoord.getMGRSNorthing()); - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine_1); - coordinateLine_2[0] = '\0'; - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); - coordinateLine_2[0] = '\0'; - } else { - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), - geoCoord.getOSGRN100k()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), - geoCoord.getOSGRNorthing()); - } - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System - double lat = geoCoord.getLatitude() * 1e-7; - double lon = geoCoord.getLongitude() * 1e-7; - - // Normalize - if (lat > 90.0) - lat = 90.0; - if (lat < -90.0) - lat = -90.0; - while (lon < -180.0) - lon += 360.0; - while (lon >= 180.0) - lon -= 360.0; - - double adjLon = lon + 180.0; - double adjLat = lat + 90.0; - - char maiden[10]; // enough for 8-char + null - - // Field (2 letters) - int lonField = int(adjLon / 20.0); - int latField = int(adjLat / 10.0); - adjLon -= lonField * 20.0; - adjLat -= latField * 10.0; - - // Square (2 digits) - int lonSquare = int(adjLon / 2.0); - int latSquare = int(adjLat / 1.0); - adjLon -= lonSquare * 2.0; - adjLat -= latSquare * 1.0; - - // Subsquare (2 letters) - double lonUnit = 2.0 / 24.0; - double latUnit = 1.0 / 24.0; - int lonSub = int(adjLon / lonUnit); - int latSub = int(adjLat / latUnit); - - snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare, - 'A' + lonSub, 'A' + latSub); - - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden); - coordinateLine_2[0] = '\0'; // only need one line - } - - if (strcmp(mode, "line1") == 0) { - display->drawString(x, y, coordinateLine_1); - } else if (strcmp(mode, "line2") == 0) { - display->drawString(x, y, coordinateLine_2); - } else if (strcmp(mode, "combined") == 0) { - display->drawString(x, y, coordinateLine_1); - if (coordinateLine_2[0] != '\0') { - display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); - } - } - - } else { - char coordinateLine_1[22]; - char coordinateLine_2[22]; - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), - geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), - geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - if (strcmp(mode, "line1") == 0) { - display->drawString(x, y, coordinateLine_1); - } else if (strcmp(mode, "line2") == 0) { - display->drawString(x, y, coordinateLine_2); - } else { // both - display->drawString(x, y, coordinateLine_1); - display->drawString(x, y + 10, coordinateLine_2); - } - } + if (!gps->getIsConnected() && !config.position.fixed_position) { + if (strcmp(mode, "line1") == 0) { + strcpy(displayLine, "No GPS present"); + display->drawString(x, y, displayLine); } + } else if (!gps->getHasLock() && !config.position.fixed_position) { + if (strcmp(mode, "line1") == 0) { + strcpy(displayLine, "No GPS Lock"); + display->drawString(x, y, displayLine); + } + } else { + + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + + if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { + char coordinateLine_1[22]; + char coordinateLine_2[22]; + if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), geoCoord.getUTMBand(), geoCoord.getUTMEasting()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), geoCoord.getMGRSBand(), + geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code + geoCoord.getOLCCode(coordinateLine_1); + coordinateLine_2[0] = '\0'; + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); + coordinateLine_2[0] = '\0'; + } else { + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), geoCoord.getOSGRN100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + } + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System + double lat = geoCoord.getLatitude() * 1e-7; + double lon = geoCoord.getLongitude() * 1e-7; + + // Normalize + if (lat > 90.0) + lat = 90.0; + if (lat < -90.0) + lat = -90.0; + while (lon < -180.0) + lon += 360.0; + while (lon >= 180.0) + lon -= 360.0; + + double adjLon = lon + 180.0; + double adjLat = lat + 90.0; + + char maiden[10]; // enough for 8-char + null + + // Field (2 letters) + int lonField = int(adjLon / 20.0); + int latField = int(adjLat / 10.0); + adjLon -= lonField * 20.0; + adjLat -= latField * 10.0; + + // Square (2 digits) + int lonSquare = int(adjLon / 2.0); + int latSquare = int(adjLat / 1.0); + adjLon -= lonSquare * 2.0; + adjLat -= latSquare * 1.0; + + // Subsquare (2 letters) + double lonUnit = 2.0 / 24.0; + double latUnit = 1.0 / 24.0; + int lonSub = int(adjLon / lonUnit); + int latSub = int(adjLat / latUnit); + + snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare, 'A' + lonSub, + 'A' + latSub); + + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden); + coordinateLine_2[0] = '\0'; // only need one line + } + + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else if (strcmp(mode, "combined") == 0) { + display->drawString(x, y, coordinateLine_1); + if (coordinateLine_2[0] != '\0') { + display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); + } + } + + } else { + char coordinateLine_1[22]; + char coordinateLine_2[22]; + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), + geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), + geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else { // both + display->drawString(x, y, coordinateLine_1); + display->drawString(x, y + 10, coordinateLine_2); + } + } + } } #endif // !MESHTASTIC_EXCLUDE_GPS // Draw nodes status -void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, - bool show_total, const char *additional_words) -{ - char usersString[20]; - int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; +void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, bool show_total, + const char *additional_words) { + char usersString[20]; + int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); + snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); - if (show_total) { - int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); - } + if (show_total) { + int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; + snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); + } -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ + defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ + defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); - } else { - display->drawFastImage(x, y + 3, 8, 8, imgUser); - } + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 3, 8, 8, imgUser); + } #else - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); - } else { - display->drawFastImage(x, y + 1, 8, 8, imgUser); - } + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 1, 8, 8, imgUser); + } #endif - int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; - display->drawString(x + 10 + string_offset, y - 2, usersString); + int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; + display->drawString(x + 10 + string_offset, y - 2, usersString); } // ********************** // * Favorite Node Info * // ********************** -void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - if (favoritedNodes.empty()) - return; +void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (favoritedNodes.empty()) + return; - // --- Only display if index is valid --- - int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); - if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) - return; + // --- Only display if index is valid --- + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; - meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; - if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) - return; - display->clear(); + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; + display->clear(); #if defined(M5STACK_UNITC6L) - uint32_t now = millis(); - if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 - { - display->display(); - lastSwitchTime = now; - } + uint32_t now = millis(); + if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 + { + display->display(); + lastSwitchTime = now; + } #endif - currentFavoriteNodeNum = node->num; - // === Create the shortName and title string === - const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - char titlestr[32] = {0}; - snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); + currentFavoriteNodeNum = node->num; + // === Create the shortName and title string === + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + char titlestr[32] = {0}; + snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); - // === Draw battery/time/mail header (common across screens) === - graphics::drawCommonHeader(display, x, y, titlestr); + // === Draw battery/time/mail header (common across screens) === + graphics::drawCommonHeader(display, x, y, titlestr); - // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== - // 1. Each potential info row has a macro-defined Y position (not regular increments!). - // 2. Each row is only shown if it has valid data. - // 3. Each row "moves up" if previous are empty, so there are never any blank rows. - // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. + // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== + // 1. Each potential info row has a macro-defined Y position (not regular increments!). + // 2. Each row is only shown if it has valid data. + // 3. Each row "moves up" if previous are empty, so there are never any blank rows. + // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. - // List of available macro Y positions in order, from top to bottom. - int line = 1; // which slot to use next - std::string usernameStr; - // === 1. Long Name (always try to show first) === - const char *username; - if (currentResolution == ScreenResolution::UltraLow) { - username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; + // List of available macro Y positions in order, from top to bottom. + int line = 1; // which slot to use next + std::string usernameStr; + // === 1. Long Name (always try to show first) === + const char *username; + if (currentResolution == ScreenResolution::UltraLow) { + username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; + } else { + username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + } + + if (username) { + usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case + // Print node's long name (e.g. "Backpack Node") + display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); + } + + // === 2. Signal and Hops (combined on one line, if available) === + // If both are present: "Sig: 97% [2hops]" + // If only one: show only that one + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + + // Always use "Sig" for the label + const char *signalLabel = " Sig"; + + // --- Build the Signal/Hops line --- + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + // Decide between "1 Hop" and "N Hops" + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); } else { - username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], signalHopsStr); + } - if (username) { - usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case - // Print node's long name (e.g. "Backpack Node") - display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); - } - - // === 2. Signal and Hops (combined on one line, if available) === - // If both are present: "Sig: 97% [2hops]" - // If only one: show only that one - char signalHopsStr[32] = ""; - bool haveSignal = false; - int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); - - // Always use "Sig" for the label - const char *signalLabel = " Sig"; - - // --- Build the Signal/Hops line --- - // If SNR looks reasonable, show signal - if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { - snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); - haveSignal = true; - } - // If hops is valid (>0), show right after signal - if (node->hops_away > 0) { - size_t len = strlen(signalHopsStr); - // Decide between "1 Hop" and "N Hops" - if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, - (node->hops_away == 1 ? "Hop" : "Hops")); - } else { - snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); - } - } - if (signalHopsStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], signalHopsStr); - } - - // === 3. Heard (last seen, skip if node never seen) === - char seenStr[20] = ""; - uint32_t seconds = sinceLastSeen(node); - if (seconds != 0 && seconds != UINT32_MAX) { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" - snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); - } - if (seenStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], seenStr); - } + // === 3. Heard (last seen, skip if node never seen) === + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], seenStr); + } #if !defined(M5STACK_UNITC6L) - // === 4. Uptime (only show if metric is present) === - char uptimeStr[32] = ""; - if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { - getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr)); - } - if (uptimeStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], uptimeStr); - } + // === 4. Uptime (only show if metric is present) === + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr)); + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], uptimeStr); + } - // === 5. Distance (only if both nodes have GPS position) === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - char distStr[24] = ""; // Make buffer big enough for any string - bool haveDistance = false; + // === 5. Distance (only if both nodes have GPS position) === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; // Make buffer big enough for any string + bool haveDistance = false; - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); - haveDistance = true; - } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); - haveDistance = true; - } - } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); - haveDistance = true; - } - } - } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); - haveDistance = true; - } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: 1km"); - haveDistance = true; - } - } else { - int km = (int)(distanceKm + 0.5); - if (km > 0 && km < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); - haveDistance = true; - } - } + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); + haveDistance = true; } - } - // Only display if we actually have a value! - if (haveDistance && distStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], distStr); - } - - // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + haveDistance = true; } - if (showCompass) { - const int16_t topY = getTextPositions(display)[1]; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); - const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; - } else { - bearing -= myHeading; - } - - display->drawCircle(compassX, compassY, compassRadius); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - } - // else show nothing + } } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + haveDistance = true; + } else if (meters >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: 1km"); + haveDistance = true; } - if (showCompass) { - int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) - : getTextPositions(display)[1]; - const int margin = 4; + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + haveDistance = true; + } + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], distStr); + } + + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = 0; + } else { + bearing -= myHeading; + } + + display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + } + // else show nothing + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) : getTextPositions(display)[1]; + const int margin = 4; // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- #if defined(USE_EINK) - const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; - const int navBarHeight = iconSize + 6; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; + const int navBarHeight = iconSize + 6; #else - const int navBarHeight = 0; + const int navBarHeight = 0; #endif - int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; - // --------- END PATCH FOR EINK NAV BAR ----------- + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + // --------- END PATCH FOR EINK NAV BAR ----------- - if (availableHeight < FONT_HEIGHT_SMALL * 2) - return; + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; - const auto &op = ourNode->position; - float myHeading = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + const auto &op = ourNode->position; + float myHeading = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearing -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) + bearing -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); - display->drawCircle(compassX, compassY, compassRadius); - } - // else show nothing + display->drawCircle(compassX, compassY, compassRadius); } + // else show nothing + } #endif - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // **************************** // * Device Focused Screen * // **************************** -void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); +void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - // === Header === - if (currentResolution == ScreenResolution::UltraLow) { - graphics::drawCommonHeader(display, x, y, "Home"); - } else { - graphics::drawCommonHeader(display, x, y, ""); - } + // === Header === + if (currentResolution == ScreenResolution::UltraLow) { + graphics::drawCommonHeader(display, x, y, "Home"); + } else { + graphics::drawCommonHeader(display, x, y, ""); + } - // === Content below header === + // === Content below header === - // Determine if we need to show 4 or 5 rows on the screen - int rows = 4; - if (!config.bluetooth.enabled) { - rows = 5; - } + // Determine if we need to show 4 or 5 rows on the screen + int rows = 4; + if (!config.bluetooth.enabled) { + rows = 5; + } - // === First Row: Region / Channel Utilization and Uptime === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; + // === First Row: Region / Channel Utilization and Uptime === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; - // Display Region and Channel Utilization - if (currentResolution == ScreenResolution::UltraLow) { - drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); - } else { - drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); - } - char uptimeStr[32] = ""; - if (currentResolution != ScreenResolution::UltraLow) { - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); - } - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); + // Display Region and Channel Utilization + if (currentResolution == ScreenResolution::UltraLow) { + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } else { + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } + char uptimeStr[32] = ""; + if (currentResolution != ScreenResolution::UltraLow) { + getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + } + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); - // === Second Row: Satellites and Voltage === - config.display.heading_bold = false; + // === Second Row: Satellites and Voltage === + config.display.heading_bold = false; #if HAS_GPS - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - const char *displayLine; - if (config.position.fixed_position) { - displayLine = "Fixed GPS"; - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - } - drawSatelliteIcon(display, x, getTextPositions(display)[line]); - int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; - display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + const char *displayLine; + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; } else { - UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); + } else { + UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); + } #endif #if defined(M5STACK_UNITC6L) - line += 1; + line += 1; - // === Node Identity === - int textWidth = 0; - int nameX = 0; - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + // === Node Identity === + int textWidth = 0; + int nameX = 0; + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + // === ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); +#else + if (powerStatus->getHasBattery()) { + char batStr[20]; + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); + } else { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); + } + + config.display.heading_bold = origBold; + + // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + if (!config.bluetooth.enabled) { +#if defined(USE_EINK) + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; +#else + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; +#endif + } + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; + if (!config.bluetooth.enabled) { + extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; + } + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + if (!config.bluetooth.enabled) { + starting_position = 0; + } + + display->drawString(starting_position, getTextPositions(display)[line], chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], chUtilPercentage); + + if (!config.bluetooth.enabled) { + display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); + } + + line += 1; + + // === Fourth & Fifth Rows: Node Identity === + int textWidth = 0; + int nameX = 0; + int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; + std::string longNameStr; + + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + longNameStr = sanitizeString(ourNode->user.long_name); + } + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + char combinedName[50]; + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); + if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) { + size_t len = strlen(combinedName); + if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { + combinedName[len - 3] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(combinedName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); + } else { + // === LongName Centered === + textWidth = display->getStringWidth(longNameStr.c_str()); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); // === ShortName Centered === textWidth = display->getStringWidth(shortnameble); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], shortnameble); -#else - if (powerStatus->getHasBattery()) { - char batStr[20]; - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); - } else { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); - } - - config.display.heading_bold = origBold; - - // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === - const char *chUtil = "ChUtil:"; - char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - - int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 - : display->getStringWidth(chUtil) + 5; - int chUtil_y = getTextPositions(display)[line] + 3; - - int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; - if (!config.bluetooth.enabled) { -#if defined(USE_EINK) - chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; -#else - chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; + } #endif - } - int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; - int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; - if (!config.bluetooth.enabled) { - extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; - } - int chutil_percent = airTime->channelUtilizationPercent(); - - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; - if (!config.bluetooth.enabled) { - starting_position = 0; - } - - display->drawString(starting_position, getTextPositions(display)[line], chUtil); - - // Force 56% or higher to show a full 100% bar, text would still show related percent. - if (chutil_percent >= 61) { - chutil_percent = 100; - } - - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0–25% - float weight2 = 0.35; // Weight for 25–40% - float weight3 = 0.20; // Weight for 40–100% - float totalWeight = weight1 + weight2 + weight3; - - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); - - int fillRight = 0; - - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); - } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } - - // Draw outline - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); - - // Fill progress - if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); - } - - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], - chUtilPercentage); - - if (!config.bluetooth.enabled) { - display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); - } - - line += 1; - - // === Fourth & Fifth Rows: Node Identity === - int textWidth = 0; - int nameX = 0; - int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; - std::string longNameStr; - - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - longNameStr = sanitizeString(ourNode->user.long_name); - } - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - char combinedName[50]; - snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); - if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) { - size_t len = strlen(combinedName); - if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { - combinedName[len - 3] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(combinedName); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString( - nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); - } else { - // === LongName Centered === - textWidth = display->getStringWidth(longNameStr.c_str()); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); - - // === ShortName Centered === - textWidth = display->getStringWidth(shortnameble); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - } -#endif - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // Start Functions to write date/time to the screen // Helper function to check if a year is a leap year -constexpr bool isLeapYear(int year) -{ - return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); -} +constexpr bool isLeapYear(int year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } // Array of days in each month (non-leap year) const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // Fills the buffer with a formatted date/time string and returns pixel width -int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) -{ - int sec = rtc_sec % 60; - rtc_sec /= 60; - int min = rtc_sec % 60; - rtc_sec /= 60; - int hour = rtc_sec % 24; - rtc_sec /= 24; +int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) { + int sec = rtc_sec % 60; + rtc_sec /= 60; + int min = rtc_sec % 60; + rtc_sec /= 60; + int hour = rtc_sec % 24; + rtc_sec /= 24; - int year = 1970; - while (true) { - int daysInYear = isLeapYear(year) ? 366 : 365; - if (rtc_sec >= (uint32_t)daysInYear) { - rtc_sec -= daysInYear; - year++; - } else { - break; - } - } - - int month = 0; - while (month < 12) { - int dim = daysInMonth[month]; - if (month == 1 && isLeapYear(year)) - dim++; - if (rtc_sec >= (uint32_t)dim) { - rtc_sec -= dim; - month++; - } else { - break; - } - } - - int day = rtc_sec + 1; - - if (includeTime) { - snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + int year = 1970; + while (true) { + int daysInYear = isLeapYear(year) ? 366 : 365; + if (rtc_sec >= (uint32_t)daysInYear) { + rtc_sec -= daysInYear; + year++; } else { - snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + break; } + } - return display->getStringWidth(buf); + int month = 0; + while (month < 12) { + int dim = daysInMonth[month]; + if (month == 1 && isLeapYear(year)) + dim++; + if (rtc_sec >= (uint32_t)dim) { + rtc_sec -= dim; + month++; + } else { + break; + } + } + + int day = rtc_sec + 1; + + if (includeTime) { + snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + } else { + snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + } + + return display->getStringWidth(buf); } // Check if the display can render a string (detect special chars; emoji) -bool UIRenderer::haveGlyphs(const char *str) -{ +bool UIRenderer::haveGlyphs(const char *str) { #if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) - // Don't want to make any assumptions about custom language support - return true; + // Don't want to make any assumptions about custom language support + return true; #endif - // Check each character with the lookup function for the OLED library - // We're not really meant to use this directly.. - bool have = true; - for (uint16_t i = 0; i < strlen(str); i++) { - uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); - // If font doesn't support a character, it is substituted for ¿ - if (result == 191 && (uint8_t)str[i] != 191) { - have = false; - break; - } + // Check each character with the lookup function for the OLED library + // We're not really meant to use this directly.. + bool have = true; + for (uint16_t i = 0; i < strlen(str); i++) { + uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); + // If font doesn't support a character, it is substituted for ¿ + if (result == 191 && (uint8_t)str[i] != 191) { + have = false; + break; } + } - // LOG_DEBUG("haveGlyphs=%d", have); - return have; + // LOG_DEBUG("haveGlyphs=%d", have); + return have; } #ifdef USE_EINK /// Used on eink displays while in deep sleep -void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ +void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // Next frame should use full-refresh, and block while running, else device will sleep before async callback - EINK_ADD_FRAMEFLAG(display, COSMETIC); - EINK_ADD_FRAMEFLAG(display, BLOCKING); + // Next frame should use full-refresh, and block while running, else device will sleep before async callback + EINK_ADD_FRAMEFLAG(display, COSMETIC); + EINK_ADD_FRAMEFLAG(display, BLOCKING); - LOG_DEBUG("Draw deep sleep screen"); + LOG_DEBUG("Draw deep sleep screen"); - // Display displayStr on the screen - graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); + // Display displayStr on the screen + graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); } /// Used on eink displays when screen updates are paused -void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - LOG_DEBUG("Draw screensaver overlay"); +void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { + LOG_DEBUG("Draw screensaver overlay"); - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver - // Config - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *pauseText = "Screen Paused"; - const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); - constexpr uint8_t padding = 2; - constexpr uint8_t dividerGap = 1; + // Config + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *pauseText = "Screen Paused"; + const char *idText = owner.short_name; + const bool useId = haveGlyphs(idText); + constexpr uint8_t padding = 2; + constexpr uint8_t dividerGap = 1; - // Text widths - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); - const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); - const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; - const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); + // Text widths + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); + const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); - // Flush with bottom - const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - const int16_t boxTop = display->height() - boxHeight; - const int16_t boxBottom = display->height() - 1; - const int16_t idTextLeft = boxLeft + padding; - const int16_t idTextTop = boxTop + padding; - const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; - const int16_t pauseTextTop = boxTop + padding; - const int16_t dividerX = boxLeft + padding + idTextWidth + padding; - const int16_t dividerTop = boxTop + dividerGap; - const int16_t dividerBottom = boxBottom - dividerGap; + // Flush with bottom + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + const int16_t boxTop = display->height() - boxHeight; + const int16_t boxBottom = display->height() - 1; + const int16_t idTextLeft = boxLeft + padding; + const int16_t idTextTop = boxTop + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; + const int16_t pauseTextTop = boxTop + padding; + const int16_t dividerX = boxLeft + padding + idTextWidth + padding; + const int16_t dividerTop = boxTop + dividerGap; + const int16_t dividerBottom = boxBottom - dividerGap; - // Draw: box - display->setColor(EINK_WHITE); - display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(EINK_BLACK); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + // Draw: box + display->setColor(EINK_WHITE); + display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(EINK_BLACK); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - // Draw: text - if (useId) - display->drawString(idTextLeft, idTextTop, idText); - display->drawString(pauseTextLeft, pauseTextTop, pauseText); - display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold + // Draw: text + if (useId) + display->drawString(idTextLeft, idTextTop, idText); + display->drawString(pauseTextLeft, pauseTextTop, pauseText); + display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold - // Draw: divider - if (useId) - display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); + // Draw: divider + if (useId) + display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); } #endif /** * Draw the icon with extra info printed around the corners */ -void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // draw an xbm image. - // Please note that everything that should be transitioned - // needs to be drawn relative to x and y +void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // draw an xbm image. + // Please note that everything that should be transitioned + // needs to be drawn relative to x and y - // draw centered icon left to right and centered above the one line of app text + // draw centered icon left to right and centered above the one line of app text #if defined(M5STACK_UNITC6L) - display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - // Draw region in upper left - if (upperMsg) { - int msgWidth = display->getStringWidth(upperMsg); - int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; - int msgY = y; - display->drawString(msgX, msgY, upperMsg); - } - // Draw version and short name in bottom middle - char buf[25]; - snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) { + int msgWidth = display->getStringWidth(upperMsg); + int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; + int msgY = y; + display->drawString(msgX, msgY, upperMsg); + } + // Draw version and short name in bottom middle + char buf[25]; + snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); - screen->forceDisplay(); + display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); + screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code #else - display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, - icon_width, icon_height, icon_bits); + display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, icon_width, icon_height, + icon_bits); - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = "meshtastic.org"; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = "meshtastic.org"; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); - // Draw version and short name in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + // Draw version and short name in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code #endif } // **************************** // * My Position Screen * // **************************** -void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "Position"; + // === Set Title + const char *titleStr = "Position"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === First Row: My Location === + // === First Row: My Location === #if HAS_GPS - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; - const char *displayLine = ""; // Initialize to empty string by default - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const char *displayLine = ""; // Initialize to empty string by default + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - if (config.position.fixed_position) { - displayLine = "Fixed GPS"; - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - } - drawSatelliteIcon(display, x, getTextPositions(display)[line]); - int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; - display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; } else { - // Onboard GPS - UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + } else { + // Onboard GPS + UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); + } - config.display.heading_bold = origBold; + config.display.heading_bold = origBold; - // === Update GeoCoord === - geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), - int32_t(gpsStatus->getAltitude())); + // === Update GeoCoord === + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); - // === Determine Compass Heading === - float heading = 0; - bool validHeading = false; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - validHeading = true; + // === Determine Compass Heading === + float heading = 0; + bool validHeading = false; + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + validHeading = true; + } else { + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; } else { - if (screen->hasHeading()) { - heading = radians(screen->getHeading()); - validHeading = true; - } else { - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); - validHeading = !isnan(heading); - } + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); } + } - // If GPS is off, no need to display these parts - if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { - // === Second Row: Last GPS Fix === - if (gpsStatus->getLastFixMillis() > 0) { - uint32_t delta = millis() - gpsStatus->getLastFixMillis(); - char uptimeStr[32]; + // If GPS is off, no need to display these parts + if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = millis() - gpsStatus->getLastFixMillis(); + char uptimeStr[32]; #if defined(USE_EINK) - // E-Ink: skip seconds, show only days/hours/mins - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false); + // E-Ink: skip seconds, show only days/hours/mins + getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false); #else - // Non E-Ink: include seconds where useful - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true); + // Non E-Ink: include seconds where useful + getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true); #endif - display->drawString(0, getTextPositions(display)[line++], uptimeStr); - } else { - display->drawString(0, getTextPositions(display)[line++], "Last: ?"); - } - - // === Third Row: Line 1 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); - - if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && - uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { - // === Fourth Row: Line 2 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); - } - - // === Final Row: Altitude === - char altitudeLine[32] = {0}; - int32_t alt = geoCoord.getAltitude(); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); - } else { - snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); - } - display->drawString(x, getTextPositions(display)[line++], altitudeLine); + display->drawString(0, getTextPositions(display)[line++], uptimeStr); + } else { + display->drawString(0, getTextPositions(display)[line++], "Last: ?"); } + + // === Third Row: Line 1 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); + + if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && + uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { + // === Fourth Row: Line 2 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); + } + + // === Final Row: Altitude === + char altitudeLine[32] = {0}; + int32_t alt = geoCoord.getAltitude(); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); + } else { + snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); + } + display->drawString(x, getTextPositions(display)[line++], altitudeLine); + } #if !defined(M5STACK_UNITC6L) - // === Draw Compass if heading is valid === - if (validHeading) { - // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - const int16_t topY = getTextPositions(display)[1]; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height - const int16_t usableHeight = bottomY - topY - 5; + // === Draw Compass if heading is valid === + if (validHeading) { + // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - // Center vertically and nudge down slightly to keep "N" clear of header - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); - display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassRadius); - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; - const int margin = 4; - int availableHeight = + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + // For E-Ink screens, account for navigation bar at the bottom! + int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; + const int margin = 4; + int availableHeight = #if defined(USE_EINK) - SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink + SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink #else - SCREEN_HEIGHT - yBelowContent - margin; + SCREEN_HEIGHT - yBelowContent - margin; #endif - if (availableHeight < FONT_HEIGHT_SMALL * 2) - return; + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); - display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + display->drawCircle(compassX, compassY, compassRadius); - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); } + } #endif #endif // HAS_GPS - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } #ifdef USERPREFS_OEM_TEXT -void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - if (currentResolution == ScreenResolution::High) { - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); - } else { +void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; + if (currentResolution == ScreenResolution::High) { + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } else { - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); - } + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, + USERPREFS_OEM_IMAGE_WIDTH, USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } - switch (USERPREFS_OEM_FONT_SIZE) { - case 0: - display->setFont(FONT_SMALL); - break; - case 2: - display->setFont(FONT_LARGE); - break; - default: - display->setFont(FONT_MEDIUM); - break; - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = USERPREFS_OEM_TEXT; - if (currentResolution == ScreenResolution::High) { - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - } + switch (USERPREFS_OEM_FONT_SIZE) { + case 0: display->setFont(FONT_SMALL); + break; + case 2: + display->setFont(FONT_LARGE); + break; + default: + display->setFont(FONT_MEDIUM); + break; + } - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = USERPREFS_OEM_TEXT; + if (currentResolution == ScreenResolution::High) { + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + } + display->setFont(FONT_SMALL); - // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code } -void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawOEMIconScreen(region, display, state, x, y); +void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawOEMIconScreen(region, display, state, x, y); } #endif @@ -1210,172 +1169,169 @@ static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; -void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - int currentFrame = state->currentFrame; +void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { + int currentFrame = state->currentFrame; - // Detect frame change and record time - if (currentFrame != lastFrameIndex) { - lastFrameIndex = currentFrame; - lastFrameChangeTime = millis(); - } + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } - const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; - const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; - const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; + const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; + const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; - const size_t totalIcons = screen->indicatorIcons.size(); - if (totalIcons == 0) - return; + const size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) + return; - const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side + const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side - int usableWidth = SCREEN_WIDTH - (navPadding * 2); - if (usableWidth < iconSize) - usableWidth = iconSize; + int usableWidth = SCREEN_WIDTH - (navPadding * 2); + if (usableWidth < iconSize) + usableWidth = iconSize; - const size_t iconsPerPage = usableWidth / (iconSize + spacing); - const size_t currentPage = currentFrame / iconsPerPage; - const size_t pageStart = currentPage * iconsPerPage; - const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); + const size_t iconsPerPage = usableWidth / (iconSize + spacing); + const size_t currentPage = currentFrame / iconsPerPage; + const size_t pageStart = currentPage * iconsPerPage; + const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); - const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; - const int xStart = (SCREEN_WIDTH - totalWidth) / 2; + const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; + const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; - int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; + bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; + int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; #if defined(USE_EINK) - // Only show bar briefly after switching frames - static uint32_t navBarLastShown = 0; - static bool cosmeticRefreshDone = false; - static bool navBarPrevVisible = false; + // Only show bar briefly after switching frames + static uint32_t navBarLastShown = 0; + static bool cosmeticRefreshDone = false; + static bool navBarPrevVisible = false; - if (navBarVisible && !navBarPrevVisible) { - EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar - cosmeticRefreshDone = false; - navBarLastShown = millis(); + if (navBarVisible && !navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar + cosmeticRefreshDone = false; + navBarLastShown = millis(); + } + + if (!navBarVisible && navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar + navBarLastShown = millis(); // Mark when it disappeared + } + + if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { + if (millis() - navBarLastShown > 10000) { // 10s after hidden + EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup + cosmeticRefreshDone = true; } + } - if (!navBarVisible && navBarPrevVisible) { - EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar - navBarLastShown = millis(); // Mark when it disappeared - } - - if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { - if (millis() - navBarLastShown > 10000) { // 10s after hidden - EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup - cosmeticRefreshDone = true; - } - } - - navBarPrevVisible = navBarVisible; + navBarPrevVisible = navBarVisible; #endif - // Pre-calculate bounding rect - const int rectX = xStart - 2 - bigOffset; - const int rectWidth = totalWidth + 4 + (bigOffset * 2); - const int rectHeight = iconSize + 6; + // Pre-calculate bounding rect + const int rectX = xStart - 2 - bigOffset; + const int rectWidth = totalWidth + 4 + (bigOffset * 2); + const int rectHeight = iconSize + 6; - // Clear background and draw border - display->setColor(BLACK); - display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); + // Clear background and draw border + display->setColor(BLACK); + display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); + display->setColor(WHITE); + display->drawRect(rectX, y - 2, rectWidth, rectHeight); + + // Icon drawing loop for the current page + for (size_t i = pageStart; i < pageEnd; ++i) { + const uint8_t *icon = screen->indicatorIcons[i]; + const int x = xStart + (i - pageStart) * (iconSize + spacing); + const bool isActive = (i == static_cast(currentFrame)); + + if (isActive) { + display->setColor(WHITE); + display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); + display->setColor(BLACK); + } + + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } + + if (isActive) { + display->setColor(WHITE); + } + } + + // Compact arrow drawer + auto drawArrow = [&](bool rightSide) { display->setColor(WHITE); - display->drawRect(rectX, y - 2, rectWidth, rectHeight); - // Icon drawing loop for the current page - for (size_t i = pageStart; i < pageEnd; ++i) { - const uint8_t *icon = screen->indicatorIcons[i]; - const int x = xStart + (i - pageStart) * (iconSize + spacing); - const bool isActive = (i == static_cast(currentFrame)); + const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; + const int halfH = rectHeight / 2; - if (isActive) { - display->setColor(WHITE); - display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); - display->setColor(BLACK); - } + const int top = (y - 2) + (rectHeight - halfH) / 2; + const int bottom = top + halfH - 1; + const int midY = top + (halfH / 2); - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); - } else { - display->drawXbm(x, y, iconSize, iconSize, icon); - } + const int maxW = 4; - if (isActive) { - display->setColor(WHITE); - } + // Determine left X coordinate + int baseX = rightSide ? (rectX + rectWidth + offset) : // right arrow + (rectX - offset - 1); // left arrow + + for (int yy = top; yy <= bottom; yy++) { + int dist = abs(yy - midY); + int lineW = maxW - (dist * maxW / (halfH / 2)); + if (lineW < 1) + lineW = 1; + + if (rightSide) { + display->drawHorizontalLine(baseX, yy, lineW); + } else { + display->drawHorizontalLine(baseX - lineW + 1, yy, lineW); + } } + }; + // Right arrow + if (pageEnd < totalIcons) { + drawArrow(true); + } - // Compact arrow drawer - auto drawArrow = [&](bool rightSide) { - display->setColor(WHITE); + // Left arrow + if (pageStart > 0) { + drawArrow(false); + } - const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; - const int halfH = rectHeight / 2; - - const int top = (y - 2) + (rectHeight - halfH) / 2; - const int bottom = top + halfH - 1; - const int midY = top + (halfH / 2); - - const int maxW = 4; - - // Determine left X coordinate - int baseX = rightSide ? (rectX + rectWidth + offset) : // right arrow - (rectX - offset - 1); // left arrow - - for (int yy = top; yy <= bottom; yy++) { - int dist = abs(yy - midY); - int lineW = maxW - (dist * maxW / (halfH / 2)); - if (lineW < 1) - lineW = 1; - - if (rightSide) { - display->drawHorizontalLine(baseX, yy, lineW); - } else { - display->drawHorizontalLine(baseX - lineW + 1, yy, lineW); - } - } - }; - // Right arrow - if (pageEnd < totalIcons) { - drawArrow(true); - } - - // Left arrow - if (pageStart > 0) { - drawArrow(false); - } - - // Knock the corners off the square - display->setColor(BLACK); - display->drawRect(rectX, y - 2, 1, 1); - display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); - display->setColor(WHITE); + // Knock the corners off the square + display->setColor(BLACK); + display->drawRect(rectX, y - 2, 1, 1); + display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->setColor(WHITE); } -void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) -{ - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, message); +void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) { + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, message); } -std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) -{ - std::string uptime; +std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) { + std::string uptime; - if (days > (HOURS_IN_MONTH * 6)) - uptime = "?"; - else if (days >= 2) - uptime = std::to_string(days) + "d"; - else if (hours >= 2) - uptime = std::to_string(hours) + "h"; - else if (minutes >= 1) - uptime = std::to_string(minutes) + "m"; - else - uptime = std::to_string(seconds) + "s"; - return uptime; + if (days > (HOURS_IN_MONTH * 6)) + uptime = "?"; + else if (days >= 2) + uptime = std::to_string(days) + "d"; + else if (hours >= 2) + uptime = std::to_string(hours) + "h"; + else if (minutes >= 1) + uptime = std::to_string(minutes) + "m"; + else + uptime = std::to_string(seconds) + "s"; + return uptime; } } // namespace graphics diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 6e37b68f2..f6aba487e 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -10,15 +10,13 @@ #define HOURS_IN_MONTH 730 // Forward declarations for status types -namespace meshtastic -{ +namespace meshtastic { class PowerStatus; class NodeStatus; class GPSStatus; } // namespace meshtastic -namespace graphics -{ +namespace graphics { /// Forward declarations class Screen; @@ -29,59 +27,57 @@ class Screen; * Contains utility functions for drawing common UI elements, overlays, * battery indicators, and other shared graphical components. */ -class UIRenderer -{ - public: - // Common UI elements - static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, - int node_offset = 0, bool show_total = true, const char *additional_words = ""); +class UIRenderer { +public: + // Common UI elements + static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, + bool show_total = true, const char *additional_words = ""); - // GPS status functions - static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, - const char *mode = "line1"); - static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + // GPS status functions + static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, const char *mode = "line1"); + static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - // Overlay and special screens - static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); + // Overlay and special screens + static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); - // Navigation bar overlay - static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); + // Navigation bar overlay + static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - // Icon and screen drawing functions - static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // Icon and screen drawing functions + static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - // Compass and location screen - static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // Compass and location screen + static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static NodeNum currentFavoriteNodeNum; - static std::vector favoritedNodes; - static void rebuildFavoritedNodes(); + static NodeNum currentFavoriteNodeNum; + static std::vector favoritedNodes; + static void rebuildFavoritedNodes(); // OEM screens #ifdef USERPREFS_OEM_TEXT - static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif #ifdef USE_EINK - /// Used on eink displays while in deep sleep - static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + /// Used on eink displays while in deep sleep + static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - /// Used on eink displays when screen updates are paused - static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + /// Used on eink displays when screen updates are paused + static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); #endif - static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); - static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); + static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); - // Check if the display can render a string (detect special chars; emoji) - static bool haveGlyphs(const char *str); + // Check if the display can render a string (detect special chars; emoji) + static bool haveGlyphs(const char *str); }; // namespace UIRenderer } // namespace graphics diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp index aa54ef2f1..24b154dad 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -2,8 +2,7 @@ #if HAS_SCREEN #include "emotes.h" -namespace graphics -{ +namespace graphics { // Always define Emote list and count const Emote emotes[] = { @@ -13,13 +12,13 @@ const Emote emotes[] = { {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- - {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes - {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face - {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face - {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face + {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes + {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face + {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face + {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face {"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // 😁 Grinning Smiling Eyes {"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // 😍 Heart Eyes - {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts + {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts // --- Question/Alert --- {"\u2753", question, question_width, question_height}, // ❓ Question Mark @@ -27,17 +26,16 @@ const Emote emotes[] = { {"\u26A0\uFE0F", caution, caution_width, caution_height}, // ⚠️ Warning Sign // --- Laughing Faces --- - {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy - {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing - {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes - {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat - {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, - grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes - {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face - {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting - {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses - {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes - {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye + {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy + {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing + {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat + {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes + {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face + {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting + {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses + {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes + {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye // --- Gestures and People --- {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand @@ -56,38 +54,33 @@ const Emote emotes[] = { {"\U0001F3E0", house, house_width, house_height}, // 🏠 House // --- Weather --- - {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) - {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector) - {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain - {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud - {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog - {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake - {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet - {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer - {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, - sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud - {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud - {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud - {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow - {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, - cloud_with_lightning_height}, // 🌩️ Cloud with Lightning - {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, - cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain - {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, - cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain - {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face + {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog + {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake + {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet + {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer + {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud + {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud + {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud + {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow + {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, cloud_with_lightning_height}, // 🌩️ Cloud with Lightning + {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain + {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain + {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face // --- Moon Phases --- - {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon - {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon - {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon - {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon - {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon - {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon - {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon - {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon - {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, - first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face + {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon + {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon + {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon + {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon + {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon + {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon + {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon + {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon + {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face // --- Misc Faces --- {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns @@ -156,325 +149,249 @@ const Emote emotes[] = { const int numEmotes = sizeof(emotes) / sizeof(emotes[0]); #ifndef EXCLUDE_EMOJI -const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18, - 0x02, 0x06, 0x3F, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, - 0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00}; +const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18, 0x02, 0x06, 0x3F, 0x06, 0x40, + 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00}; -const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06, - 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, - 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; +const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, + 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; -const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, - 0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, - 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, + 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, - 0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, + 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, - 0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, + 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, - 0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, 0x46, 0x02, 0x40, 0x02, 0x40, + 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52, - 0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, - 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; +const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52, 0x0A, 0x02, 0xD8, 0x02, 0xF8, + 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; -const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, - 0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, - 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, 0x5F, 0x72, 0x4E, 0x22, 0x44, + 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48, - 0x12, 0x48, 0x12, 0x30, 0x11, 0x80, 0x08, 0x40, 0x04, 0x40, 0x02, - 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01}; +const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x11, + 0x80, 0x08, 0x40, 0x04, 0x40, 0x02, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01}; -const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, - 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C, - 0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00}; +const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, + 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00}; -const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, - 0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x0A, 0x50, 0x0E, 0x70, + 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, - 0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, - 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, 0x4C, 0x02, 0x4A, 0x1A, 0x49, + 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, - 0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, + 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, - 0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, - 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; +const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, 0x52, 0x12, 0x48, 0x12, 0x48, + 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; -const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24, - 0x25, 0x4A, 0x24, 0x12, 0x44, 0x22, 0x44, 0x04, 0x40, 0x08, 0x40, - 0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00}; +const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24, 0x25, 0x4A, 0x24, 0x12, 0x44, + 0x22, 0x44, 0x04, 0x40, 0x08, 0x40, 0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00}; -const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E, - 0x78, 0xE2, 0x47, 0x42, 0x42, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E, 0x78, 0xE2, 0x47, 0x42, 0x42, + 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A, - 0x50, 0x0E, 0x70, 0x11, 0x88, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, - 0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00}; +const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A, 0x50, 0x0E, 0x70, 0x11, 0x88, + 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00}; -const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, - 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, - 0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00}; +const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, + 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00}; -const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, - 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, - 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; +const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; -const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, - 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, - 0x00, 0x44, 0x44, 0xAA, 0x2A, 0x11, 0x11, 0x00, 0x00, 0x88, 0x88, - 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x44, 0x44, 0xAA, 0x2A, + 0x11, 0x11, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A, - 0x5C, 0x32, 0x4C, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A, 0x5C, 0x32, 0x4C, 0x52, 0x4A, + 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE, - 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F, - 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00}; +const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00}; -const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0, - 0x08, 0x10, 0x10, 0x48, 0x12, 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40, - 0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00}; +const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0, 0x08, 0x10, 0x10, 0x48, 0x12, + 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40, 0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00}; -const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, - 0x0F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F, - 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00}; +const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xF8, 0x1F, 0xF8, 0x1F, + 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00}; -const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32, - 0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, - 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32, 0x40, 0x02, 0x58, 0x82, 0x5B, + 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, - 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, - 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; +const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, + 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; -const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38, - 0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, - 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38, 0x0A, 0x54, 0x68, 0x54, 0x58, + 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, - 0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, - 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; +const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, 0x09, 0x98, 0x19, 0x94, 0x29, + 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; -const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, - 0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, - 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; +const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, 0x1F, 0x80, 0x3F, 0xE0, 0xFF, + 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; -const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72, - 0x4E, 0x22, 0x44, 0x82, 0x41, 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F, - 0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72, 0x4E, 0x22, 0x44, 0x82, 0x41, + 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F, 0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74, - 0x2E, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A, - 0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00}; +const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74, 0x2E, 0x52, 0x4A, 0x72, 0x4E, + 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A, 0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00}; -const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C, - 0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00}; +const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C, 0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00}; -const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00, - 0x38, 0x00, 0x28, 0x78, 0x44, 0x84, 0x82, 0x22, 0x83, 0x52, 0x83, - 0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00}; +const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00, 0x38, 0x00, 0x28, 0x78, 0x44, + 0x84, 0x82, 0x22, 0x83, 0x52, 0x83, 0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00}; -const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, - 0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, - 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, 0x0A, 0x02, 0x38, 0x04, 0x48, + 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, - 0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, - 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; +const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, 0xA0, 0x31, 0x8C, 0x51, 0x8A, + 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; -const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, - 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, - 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; +const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, + 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; -const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, - 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, - 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; +const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, 0xE3, 0x87, 0xE1, 0x87, 0xE1, + 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; -const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, - 0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, - 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; +const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, 0x44, 0x42, 0x42, 0x22, 0x44, + 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; -const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, - 0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, 0x5F, 0x72, 0x4E, 0x02, 0x40, + 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, - 0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, - 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; +const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, 0xB8, 0x10, 0x87, 0xC8, 0x80, + 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; -const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, - 0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, - 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; +const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x70, 0x0E, 0x70, 0x0E, + 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; -const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, - 0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, - 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; +const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, 0x5F, 0x8A, 0x54, 0xFA, 0x5F, + 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; -const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, - 0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, - 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; +const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, 0x03, 0x32, 0x26, 0x1C, 0x1C, + 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; -const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, - 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, - 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; +const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, + 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; -const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, - 0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, - 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; +const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, 0x00, 0x70, 0x01, 0xE0, 0x02, + 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; -const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, - 0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, - 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; +const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, 0x77, 0xDE, 0x7B, 0x3E, 0x7C, + 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; -const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, - 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, + 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, - 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, - 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; +const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; -const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, - 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, - 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; +const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21, 0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01}; -const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, - 0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, - 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; +const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, 0x01, 0x99, 0x01, 0xF9, 0x01, + 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; -const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, - 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, - 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, + 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, - 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, - 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, + 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, - 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, - 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, + 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, - 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, - 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, + 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, - 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, - 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, - 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, + 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, - 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, - 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, + 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, - 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, - 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, + 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00, 0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80, 0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07}; -const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, - 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, - 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; +const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, + 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; -const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, - 0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, - 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; +const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, 0x21, 0x2C, 0x56, 0x14, 0x58, + 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; -const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, - 0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, - 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; +const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, 0x01, 0x10, 0x0E, 0x20, 0x30, + 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; -const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, - 0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, - 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; +const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, 0x1F, 0x80, 0x0F, 0xC2, 0x07, + 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; -const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, - 0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, - 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; +const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x43, + 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; -const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, - 0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, - 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; +const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0xE6, 0x03, + 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; -const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, - 0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, - 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0xC0, 0x67, + 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, - 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, - 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; +const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; -const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, - 0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, - 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, 0x00, 0xFE, 0x3F, 0xFF, 0x3F, + 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, - 0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, - 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; +const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; -const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, - 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, - 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; +const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, + 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; -const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, - 0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, - 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; +const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, 0x00, 0x11, 0x3C, 0x11, 0x42, + 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; -const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, - 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, - 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; +const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x07, + 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; -const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, - 0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, - 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; +const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, 0x20, 0x02, 0x40, 0xFF, 0xFF, + 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; -const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, - 0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, - 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; +const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, 0x09, 0x27, 0xE4, 0x49, 0x92, + 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; -const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, - 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, - 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; +const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; -const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, - 0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, - 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, 0x2F, 0x7A, 0x5E, 0x39, 0x9C, + 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #endif } // namespace graphics diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h index 0637712cc..773f5edd0 100644 --- a/src/graphics/emotes.h +++ b/src/graphics/emotes.h @@ -1,15 +1,14 @@ #pragma once #include -namespace graphics -{ +namespace graphics { // === Emote List === struct Emote { - const char *label; - const unsigned char *bitmap; - int width; - int height; + const char *label; + const unsigned char *bitmap; + int width; + int height; }; extern const Emote emotes[/* numEmotes */]; diff --git a/src/graphics/fonts/EinkDisplayFonts.cpp b/src/graphics/fonts/EinkDisplayFonts.cpp index 497b3b389..b1b008802 100644 --- a/src/graphics/fonts/EinkDisplayFonts.cpp +++ b/src/graphics/fonts/EinkDisplayFonts.cpp @@ -237,952 +237,926 @@ const uint8_t Monospaced_plain_30[] PROGMEM = { 0x44, 0x11, 0x52, 0x12, // 255:17425 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, - 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, // 33 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 34 - 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x38, 0x08, 0x00, 0x00, 0x70, 0xB8, 0x0F, 0x00, 0x00, - 0x70, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, - 0x38, 0x08, 0x00, 0xC0, 0x70, 0xB8, 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x3F, - 0x00, 0x00, 0xC0, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, 0x38, 0x00, 0x00, 0xC0, 0x70, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x00, 0x70, // 35 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x07, 0x00, 0x00, - 0xF8, 0x03, 0x07, 0x00, 0x00, 0xFC, 0x03, 0x0E, 0x00, 0x00, 0x1E, 0x07, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0x00, 0x0E, - 0x06, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x0C, - 0x0E, 0x00, 0x00, 0x0E, 0x1C, 0x0F, 0x00, 0x00, 0x1C, 0xF8, 0x07, 0x00, 0x00, 0x1C, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, + 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, // 33 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 34 + 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x38, 0x08, 0x00, 0x00, 0x70, 0xB8, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, + 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, 0x38, 0x08, 0x00, 0xC0, 0x70, 0xB8, + 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, + 0x38, 0x00, 0x00, 0xC0, 0x70, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 35 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x07, 0x00, 0x00, 0xF8, 0x03, 0x07, + 0x00, 0x00, 0xFC, 0x03, 0x0E, 0x00, 0x00, 0x1E, 0x07, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, + 0xFF, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x1C, 0x0F, 0x00, 0x00, 0x1C, + 0xF8, 0x07, 0x00, 0x00, 0x1C, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x01, // 36 - 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x10, 0x00, 0x00, 0xC0, 0x71, 0x18, 0x00, 0x00, 0xC0, 0x60, 0x08, 0x00, 0x00, 0xC0, - 0x60, 0x0C, 0x00, 0x00, 0xC0, 0x60, 0x04, 0x00, 0x00, 0xC0, 0x71, 0x06, 0x00, 0x00, 0x80, 0x3F, 0x02, 0x00, 0x00, 0x00, 0x1F, - 0xE3, 0x03, 0x00, 0x00, 0x00, 0xF1, 0x07, 0x00, 0x00, 0x80, 0x39, 0x0E, 0x00, 0x00, 0x80, 0x18, 0x0C, 0x00, 0x00, 0xC0, 0x18, - 0x0C, 0x00, 0x00, 0x40, 0x18, 0x0C, 0x00, 0x00, 0x60, 0x38, 0x0E, 0x00, 0x00, 0x20, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x10, 0x00, 0x00, 0xC0, 0x71, 0x18, 0x00, 0x00, 0xC0, 0x60, 0x08, 0x00, 0x00, 0xC0, 0x60, 0x0C, 0x00, + 0x00, 0xC0, 0x60, 0x04, 0x00, 0x00, 0xC0, 0x71, 0x06, 0x00, 0x00, 0x80, 0x3F, 0x02, 0x00, 0x00, 0x00, 0x1F, 0xE3, 0x03, 0x00, 0x00, 0x00, 0xF1, + 0x07, 0x00, 0x00, 0x80, 0x39, 0x0E, 0x00, 0x00, 0x80, 0x18, 0x0C, 0x00, 0x00, 0xC0, 0x18, 0x0C, 0x00, 0x00, 0x40, 0x18, 0x0C, 0x00, 0x00, 0x60, + 0x38, 0x0E, 0x00, 0x00, 0x20, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, - 0x1F, 0x87, 0x07, 0x00, 0x80, 0xFF, 0x03, 0x0F, 0x00, 0x80, 0xFF, 0x01, 0x0F, 0x00, 0xC0, 0xE3, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1F, 0x0E, 0x00, 0xC0, 0x01, 0x3E, 0x0E, 0x00, 0xC0, 0x01, 0x7C, 0x07, 0x00, 0x80, 0x03, 0xF0, - 0x07, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x0E, - 0x00, 0x00, 0x00, 0x1E, 0x08, // 38 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, - 0x00, 0x00, 0x00, 0xC0, 0x3F, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xFE, - 0xFF, 0x0F, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x1F, 0x87, 0x07, + 0x00, 0x80, 0xFF, 0x03, 0x0F, 0x00, 0x80, 0xFF, 0x01, 0x0F, 0x00, 0xC0, 0xE3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1F, + 0x0E, 0x00, 0xC0, 0x01, 0x3E, 0x0E, 0x00, 0xC0, 0x01, 0x7C, 0x07, 0x00, 0x80, 0x03, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, + 0xC0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x08, // 38 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x80, 0x3F, 0x80, + 0x3F, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, 0x80, // 40 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0x80, 0x3F, - 0x80, 0x3F, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, - 0xCC, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xC0, 0xFF, - 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, - 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, 0x84, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF8, - 0xFF, 0x07, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, - 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 44 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0x00, 0x00, 0xFE, 0xFF, + 0x0F, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xC0, + 0x7F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, + 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x86, + 0x01, 0x00, 0x00, 0x00, + 0x84, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0xF8, 0xFF, + 0x07, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 44 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, - 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 46 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, - 0xFE, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0x7E, 0x00, - 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x01, - 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x02, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0x80, 0x0F, 0xC0, - 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 46 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x80, 0x3F, + 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x07, + 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, + 0x0E, 0x00, 0xC0, 0x01, 0x02, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, + 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, + 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, - 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0E, 0x00, 0xC0, 0x01, 0xF0, 0x0E, 0x00, 0xC0, 0x01, - 0x78, 0x0E, 0x00, 0xC0, 0x01, 0x3C, 0x0E, 0x00, 0xC0, 0x01, 0x1E, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0x80, 0x87, 0x0F, - 0x0E, 0x00, 0x80, 0xFF, 0x07, 0x0E, 0x00, 0x00, 0xFF, 0x01, 0x0E, 0x00, 0x00, 0xFC, 0x00, 0x0E, // 50 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, - 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0x80, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, - 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, - 0x00, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0x77, 0x00, 0x00, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0x78, 0x70, 0x00, 0x00, 0x00, 0x1E, - 0x70, 0x00, 0x00, 0x00, 0x0F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xC0, 0xFF, 0x03, 0x07, 0x00, 0xC0, - 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, - 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x07, 0x00, 0xC0, 0x81, 0x87, - 0x07, 0x00, 0xC0, 0x81, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x1F, 0x87, 0x07, 0x00, 0x80, 0x87, 0x03, 0x0F, 0x00, 0xC0, 0x83, 0x01, 0x0E, 0x00, 0xC0, 0xC1, - 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0F, 0x00, 0xC0, 0x81, 0x87, - 0x07, 0x00, 0x80, 0x83, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFC, // 54 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, - 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x08, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0F, 0x00, 0xC0, 0x01, - 0xF8, 0x07, 0x00, 0xC0, 0x01, 0xFF, 0x01, 0x00, 0xC0, 0xE1, 0x3F, 0x00, 0x00, 0xC0, 0xF9, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, - 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x01, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, - 0x7F, 0xFE, 0x07, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, - 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x80, - 0xFF, 0x07, 0x07, 0x00, 0x80, 0x87, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, - 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0F, 0x00, 0xC0, 0x03, 0x87, 0x07, 0x00, 0x80, 0x87, 0xE1, - 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, - 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x01, 0x00, 0xF0, - 0x00, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x3F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, - 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x80, 0x33, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0xC0, - 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, - 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x38, 0x80, 0x03, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, - 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, - 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, - 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x80, 0x03, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, - 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, - 0x71, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x3E, 0x0F, 0x00, 0xC0, 0x01, - 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x03, 0x00, 0x00, 0xC0, 0xF3, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x00, - 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x1E, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x00, - 0x38, 0x00, 0x38, 0x00, 0x00, 0x0C, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x7C, 0xE0, 0x00, 0x00, 0x06, 0xFF, 0xC1, 0x00, 0x00, 0x83, - 0x83, 0xC3, 0x00, 0x00, 0x83, 0x01, 0x87, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, - 0x86, 0x01, 0x00, 0xC7, 0x00, 0x86, 0x01, 0x00, 0x86, 0x01, 0xC3, 0x01, 0x00, 0x9E, 0x83, 0xC3, 0x01, 0x00, 0xFC, 0xFF, 0x07, - 0x00, 0x00, 0xF0, 0xFF, 0x07, // 64 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, - 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x07, 0x0F, 0x00, 0x80, 0xFF, 0x0E, - 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x01, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, - 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, - 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, - 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, - 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x01, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, - 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, - 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0x80, 0x03, 0xFF, 0x07, 0x00, 0x00, 0x07, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x80, 0x0F, + 0x00, 0xC0, 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0E, 0x00, 0xC0, 0x01, 0xF0, 0x0E, 0x00, 0xC0, 0x01, 0x78, 0x0E, 0x00, 0xC0, 0x01, 0x3C, + 0x0E, 0x00, 0xC0, 0x01, 0x1E, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0x80, 0x87, 0x0F, 0x0E, 0x00, 0x80, 0xFF, 0x07, 0x0E, 0x00, 0x00, 0xFF, + 0x01, 0x0E, 0x00, 0x00, 0xFC, 0x00, 0x0E, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0E, + 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0x80, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, + 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, + 0x00, 0x00, 0xC0, 0x77, 0x00, 0x00, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0x78, 0x70, 0x00, 0x00, 0x00, 0x1E, 0x70, 0x00, 0x00, 0x00, 0x0F, 0x70, + 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xC0, 0xFF, 0x03, 0x07, 0x00, 0xC0, 0xFF, 0x01, 0x0E, + 0x00, 0xC0, 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, + 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x07, 0x00, 0xC0, 0x81, 0x87, 0x07, 0x00, 0xC0, 0x81, 0xFF, 0x03, 0x00, 0x00, 0x00, + 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x1F, 0x87, 0x07, 0x00, 0x80, 0x87, 0x03, 0x0F, 0x00, 0xC0, 0x83, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, + 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0F, 0x00, 0xC0, 0x81, 0x87, 0x07, 0x00, 0x80, 0x83, 0xFF, 0x07, 0x00, 0x00, 0x00, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFC, // 54 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, + 0x00, 0xC0, 0x01, 0x00, 0x08, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x07, 0x00, 0xC0, 0x01, 0xFF, + 0x01, 0x00, 0xC0, 0xE1, 0x3F, 0x00, 0x00, 0xC0, 0xF9, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x07, + 0x00, 0x00, 0x00, 0xC0, + 0x01, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x7F, 0xFE, 0x07, + 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, + 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x07, 0x07, + 0x00, 0x80, 0x87, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, + 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0F, 0x00, 0xC0, 0x03, 0x87, 0x07, 0x00, 0x80, 0x87, 0xE1, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, + 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x00, 0x00, 0xF0, 0x00, + 0x3F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, + 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x80, 0x33, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, + 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x30, + 0x80, 0x01, 0x00, 0x00, 0x38, 0x80, 0x03, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, + 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, + 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, + 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x80, 0x03, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, + 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x31, + 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x3E, 0x0F, 0x00, 0xC0, 0x01, 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x3F, + 0x0F, 0x00, 0xC0, 0xC1, 0x03, 0x00, 0x00, 0xC0, 0xF3, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, + 0x1E, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x00, 0x38, 0x00, 0x38, + 0x00, 0x00, 0x0C, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x7C, 0xE0, 0x00, 0x00, 0x06, 0xFF, 0xC1, 0x00, 0x00, 0x83, 0x83, 0xC3, 0x00, 0x00, 0x83, 0x01, + 0x87, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC7, 0x00, 0x86, 0x01, 0x00, 0x86, + 0x01, 0xC3, 0x01, 0x00, 0x9E, 0x83, 0xC3, 0x01, 0x00, 0xFC, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, // 64 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, + 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x07, 0x0F, 0x00, 0x80, 0xFF, 0x0E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, + 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x01, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, + 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, + 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 67 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, + 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 68 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, + 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, + 0x03, 0x00, 0x00, 0xC0, + 0x01, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, + 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0x80, 0x03, + 0xFF, 0x07, 0x00, 0x00, 0x07, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x03, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, - 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, - 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x00, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, - 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xF0, - 0x07, 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0x00, 0x0F, 0xF0, - 0x01, 0x00, 0x80, 0x03, 0xE0, 0x07, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0x40, 0x00, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x00, 0x08, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, + 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, + 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x1F, + 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x01, 0x00, 0x80, 0x03, 0xE0, 0x07, 0x00, 0xC0, 0x01, + 0x80, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0x40, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, // 75 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0x03, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, - 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x00, + 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, - 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x80, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, - 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, - 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x83, 0x07, - 0x00, 0x00, 0x80, 0xC7, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7C, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x3E, 0x00, 0x80, 0x03, 0x00, - 0x7F, 0x00, 0x80, 0x0F, 0xC0, 0xF7, 0x00, 0x00, 0xFF, 0xFF, 0x63, 0x00, 0x00, 0xFE, 0xFF, 0x41, 0x00, 0x00, 0xF0, 0x3F, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, - 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0x00, 0xC0, 0x83, 0x1F, 0x00, 0x00, 0x80, 0xC7, 0x7D, - 0x00, 0x00, 0x80, 0xFF, 0xF9, 0x01, 0x00, 0x00, 0xFF, 0xF0, 0x07, 0x00, 0x00, 0x7C, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, - 0x00, 0x00, 0x00, 0x00, 0x08, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x80, - 0xFF, 0x01, 0x0E, 0x00, 0x80, 0xC7, 0x01, 0x0E, 0x00, 0xC0, 0xC3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x8F, - 0x07, 0x00, 0x80, 0x03, 0xFE, 0x07, 0x00, 0x80, 0x03, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 83 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, - 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, - 0x00, 0xC0, 0x01, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, - 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, - 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 85 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0x80, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, - 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xF8, 0x3F, - 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, // 86 - 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, - 0x01, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xE0, - 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, - 0x00, 0xC0, 0x01, // 87 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, - 0x07, 0xC0, 0x0F, 0x00, 0x80, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x7E, 0x00, 0x00, 0x00, 0xF0, - 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x7C, 0x7C, - 0x00, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x80, 0x0F, 0xE0, 0x03, 0x00, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x0F, - 0x00, 0xC0, 0x00, 0x00, 0x0C, // 88 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x00, - 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, - 0x00, 0x40, // 89 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, - 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xF0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x0E, 0x00, 0xC0, 0x01, 0x7E, 0x0E, 0x00, 0xC0, 0x01, - 0x1F, 0x0E, 0x00, 0xC0, 0xC1, 0x0F, 0x0E, 0x00, 0xC0, 0xE1, 0x03, 0x0E, 0x00, 0xC0, 0xF9, 0x01, 0x0E, 0x00, 0xC0, 0x7D, 0x00, - 0x0E, 0x00, 0xC0, 0x3F, 0x00, 0x0E, 0x00, 0xC0, 0x0F, 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, + 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, + 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0x80, 0xC7, 0x03, 0x00, 0x00, 0x80, 0xFF, + 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7C, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x3E, 0x00, 0x80, 0x03, 0x00, 0x7F, 0x00, 0x80, 0x0F, 0xC0, 0xF7, 0x00, 0x00, 0xFF, + 0xFF, 0x63, 0x00, 0x00, 0xFE, 0xFF, 0x41, 0x00, 0x00, 0xF0, 0x3F, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, + 0x00, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0x00, 0xC0, 0x83, 0x1F, 0x00, 0x00, 0x80, 0xC7, 0x7D, 0x00, 0x00, 0x80, 0xFF, 0xF9, 0x01, 0x00, 0x00, 0xFF, + 0xF0, 0x07, 0x00, 0x00, 0x7C, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x08, // 82 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x80, 0xFF, 0x01, 0x0E, + 0x00, 0x80, 0xC7, 0x01, 0x0E, 0x00, 0xC0, 0xC3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, + 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x8F, 0x07, 0x00, 0x80, 0x03, 0xFE, 0x07, 0x00, 0x80, 0x03, + 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 83 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, + 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, + 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, + 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, + 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 85 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, + 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, + 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, + 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, // 86 + 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0x01, + 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFE, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x01, // 87 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x07, 0xC0, 0x0F, + 0x00, 0x80, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x00, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x80, 0x0F, + 0xE0, 0x03, 0x00, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0C, // 88 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, + 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 89 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x01, 0xC0, 0x0F, + 0x00, 0xC0, 0x01, 0xF0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x0E, 0x00, 0xC0, 0x01, 0x7E, 0x0E, 0x00, 0xC0, 0x01, 0x1F, 0x0E, 0x00, 0xC0, 0xC1, 0x0F, + 0x0E, 0x00, 0xC0, 0xE1, 0x03, 0x0E, 0x00, 0xC0, 0xF9, 0x01, 0x0E, 0x00, 0xC0, 0x7D, 0x00, 0x0E, 0x00, 0xC0, 0x3F, 0x00, 0x0E, 0x00, 0xC0, 0x0F, + 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 90 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, - 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, - 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, // 91 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, - 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, - 0x0F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, + 0xFF, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, // 91 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, + 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, + 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x40, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, - 0x00, 0xE0, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, - 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, - 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x20, // 94 - 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, - 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, // 95 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x02, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, - 0xE0, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, - 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, - 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, - 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, - 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, - 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, - 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, - 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, - 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, - 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, - 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, - 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 101 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, - 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, - 0xE0, 0xFF, 0xC7, 0x01, 0x00, 0xE0, 0x81, 0xC7, 0x01, 0x00, 0xF0, 0x00, 0x8F, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, - 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0xE0, 0x00, 0xC7, 0x03, 0x00, 0xC0, 0x81, - 0xE3, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0xF0, 0xFF, 0x3F, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, - 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, - 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, - 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 105 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, - 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, - 0x00, 0xC0, 0x03, 0xE0, 0xF0, 0xFF, 0xFF, 0x01, 0xE0, 0xF0, 0xFF, 0xFF, 0x00, 0xE0, 0xF0, 0xFF, 0x7F, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, - 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, - 0x1F, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFB, 0x00, 0x00, 0x00, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0xF0, 0xC1, - 0x07, 0x00, 0x00, 0xF0, 0x80, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0xFF, 0xFF, + 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x20, // 94 + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, // 95 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x02, // 96 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, + 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, + 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, + 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, + 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, + 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, + 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 99 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, + 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, + 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, + 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, + 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 101 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, + 0x00, 0x00, 0x00, 0xE0, + 0x70, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0xC7, + 0x01, 0x00, 0xE0, 0x81, 0xC7, 0x01, 0x00, 0xF0, 0x00, 0x8F, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, + 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0xE0, 0x00, 0xC7, 0x03, 0x00, 0xC0, 0x81, 0xE3, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x01, 0x00, 0xF0, + 0xFF, 0xFF, 0x00, 0x00, 0xF0, 0xFF, 0x3F, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, + 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, + 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, + 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, + 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 105 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, + 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0xC0, 0x03, 0xE0, 0xF0, 0xFF, + 0xFF, 0x01, 0xE0, 0xF0, 0xFF, 0xFF, 0x00, 0xE0, 0xF0, 0xFF, 0x7F, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, + 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x7F, + 0x00, 0x00, 0x00, 0xC0, 0xFB, 0x00, 0x00, 0x00, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0x80, 0x0F, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, 0x08, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0xE0, 0xFF, - 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, - 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0xE0, 0xFF, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, + 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, - 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, - 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, - 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, - 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, - 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, - 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, - 0x03, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, - 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0x00, - 0xE0, 0x07, 0x07, 0x00, 0x00, 0xE0, 0x0F, 0x07, 0x00, 0x00, 0xF0, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, - 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x1E, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0F, 0x00, 0x00, 0x70, 0xFC, - 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x01, // 115 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0xFF, 0xFF, 0x07, 0x00, 0x80, 0xFF, - 0xFF, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, - 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 117 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, - 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, - 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x3F, - 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 118 - 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x00, 0x30, // 119 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, - 0x70, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, - 0x7F, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xF0, 0xC1, - 0x07, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, + 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, + 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, + 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, + 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, + 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x07, + 0x00, 0x00, 0xE0, 0x0F, 0x07, 0x00, 0x00, 0xF0, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x1E, + 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x00, + 0xF0, 0x01, // 115 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0xFF, 0xFF, 0x07, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x70, 0x00, + 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, + 0x00, 0x0E, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, + 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 117 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, + 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, + 0x10, // 118 + 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x0F, + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x30, // 119 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0F, + 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x30, + 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, - 0xF0, 0x03, 0x80, 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, - 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x80, 0x7F, - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, - 0x70, 0x00, 0x0F, 0x00, 0x00, 0x70, 0xC0, 0x0F, 0x00, 0x00, 0x70, 0xE0, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, - 0x7C, 0x0E, 0x00, 0x00, 0x70, 0x3F, 0x0E, 0x00, 0x00, 0xF0, 0x0F, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x0E, 0x00, 0x00, 0xF0, 0x03, - 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0xFF, - 0xFB, 0x7F, 0x00, 0xC0, 0xFF, 0xFB, 0xFF, 0x00, 0xE0, 0xFF, 0xF1, 0xFF, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0x00, - 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, // 123 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, - 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0x07, // 124 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, - 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0xFF, - 0xF1, 0xFF, 0x00, 0xC0, 0xFF, 0xFB, 0xFF, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, - 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0E, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, + 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, + 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, + 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 121 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0F, + 0x00, 0x00, 0x70, 0xC0, 0x0F, 0x00, 0x00, 0x70, 0xE0, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x7C, 0x0E, 0x00, 0x00, 0x70, 0x3F, + 0x0E, 0x00, 0x00, 0xF0, 0x0F, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x0E, 0x00, 0x00, 0xF0, 0x03, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0E, 0x00, 0x00, 0x70, + 0x00, 0x0E, // 122 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0xC0, 0xFF, 0xFB, + 0xFF, 0x00, 0xE0, 0xFF, 0xF1, 0xFF, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, + 0x00, 0xC0, 0x01, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, + 0xFF, 0x07, // 124 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xC0, + 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0xFF, 0xF1, 0xFF, 0x00, 0xC0, 0xFF, 0xFB, + 0xFF, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0E, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 127 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 128 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 129 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 130 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 131 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 132 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 133 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 134 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 135 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 136 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 137 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 138 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 139 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 140 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 141 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 142 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 143 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 144 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 145 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 146 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 147 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 148 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 149 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 150 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 151 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 152 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 153 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 154 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 155 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 156 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 157 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 158 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, - 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, + 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 159 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, - 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, // 161 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, - 0x80, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, // 162 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, - 0x00, 0x07, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0xFC, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, - 0xFF, 0x0F, 0x00, 0x80, 0x07, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, - 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, + 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, + 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, + 0x00, 0x07, // 162 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0E, + 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0xFC, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0x07, 0x07, + 0x0E, 0x00, 0xC0, 0x03, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0E, // 163 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, - 0x70, 0x80, 0x03, 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x61, - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, + 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, + 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xE0, + 0xDE, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 164 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, - 0xCF, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xE0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xFC, 0x18, - 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0xC0, 0xCF, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, - 0x00, 0x40, // 165 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, - 0xC3, 0xFF, 0x01, 0x80, 0xFF, 0xC3, 0xFF, 0x01, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x8F, 0x1F, 0x38, 0x00, 0x80, 0xDF, 0x1F, 0x70, 0x00, 0x80, 0xFF, 0x38, 0x60, 0x00, 0xC0, 0x71, 0x30, 0x60, 0x00, 0xC0, 0xE0, - 0x60, 0x60, 0x00, 0xC0, 0xC0, 0xE0, 0x60, 0x00, 0xC0, 0xC0, 0xC1, 0x71, 0x00, 0xC0, 0x80, 0xE3, 0x3F, 0x00, 0xC0, 0x81, 0xBF, - 0x3F, 0x00, 0x80, 0x03, 0x3F, 0x0E, 0x00, 0x00, 0x00, 0x1E, // 167 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0x00, 0xE0, // 168 - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, - 0x8E, 0xCF, 0x01, 0x00, 0x00, 0xE6, 0x9F, 0x01, 0x00, 0x00, 0x63, 0x38, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, - 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x63, 0x18, 0x03, 0x00, 0x00, 0x06, 0x80, - 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, - 0x00, 0x00, 0xC0, 0x0F, // 169 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xF0, 0x30, 0x00, 0x00, 0x80, 0xF9, 0x31, 0x00, 0x00, 0xC0, 0x98, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, - 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x8C, 0x31, 0x00, 0x00, 0x80, 0xCD, 0x30, - 0x00, 0x00, 0x80, 0xFF, 0x33, 0x00, 0x00, 0x00, 0xFF, 0x33, // 170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, - 0xC0, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, - 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x3F, // 172 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xCF, 0x18, 0x00, + 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0xC0, 0xCF, + 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0x40, // 165 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xC3, 0xFF, 0x01, 0x80, 0xFF, 0xC3, + 0xFF, 0x01, // 166 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8F, 0x1F, 0x38, + 0x00, 0x80, 0xDF, 0x1F, 0x70, 0x00, 0x80, 0xFF, 0x38, 0x60, 0x00, 0xC0, 0x71, 0x30, 0x60, 0x00, 0xC0, 0xE0, 0x60, 0x60, 0x00, 0xC0, 0xC0, 0xE0, + 0x60, 0x00, 0xC0, 0xC0, 0xC1, 0x71, 0x00, 0xC0, 0x80, 0xE3, 0x3F, 0x00, 0xC0, 0x81, 0xBF, 0x3F, 0x00, 0x80, 0x03, 0x3F, 0x0E, 0x00, 0x00, 0x00, + 0x1E, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, // 168 + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x8E, 0xCF, 0x01, + 0x00, 0x00, 0xE6, 0x9F, 0x01, 0x00, 0x00, 0x63, 0x38, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, + 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x63, 0x18, 0x03, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, + 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, // 169 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x30, 0x00, + 0x00, 0x80, 0xF9, 0x31, 0x00, 0x00, 0xC0, 0x98, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, + 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x8C, 0x31, 0x00, 0x00, 0x80, 0xCD, 0x30, 0x00, 0x00, 0x80, 0xFF, 0x33, 0x00, 0x00, 0x00, 0xFF, + 0x33, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, + 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, + 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, + 0x3F, 0x00, 0x00, 0x00, 0x80, 0x3F, // 172 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 173 - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, - 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0x33, - 0x07, 0x03, 0x00, 0x00, 0x33, 0x1F, 0x03, 0x00, 0x00, 0xF3, 0x3D, 0x03, 0x00, 0x00, 0xE3, 0x30, 0x03, 0x00, 0x00, 0x06, 0xA0, - 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, - 0x00, 0x00, 0xC0, 0x0F, // 174 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, - 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0x00, 0xC0, 0x01, // 175 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, - 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1F, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, - 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0xF8, - 0x7F, 0x0C, 0x00, 0x00, 0xF8, 0x7F, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, - 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, // 177 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0xE0, - 0x03, 0x00, 0x00, 0xC0, 0x70, 0x03, 0x00, 0x00, 0xC0, 0x39, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x03, 0x00, 0x00, 0x00, 0x07, + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x0E, 0xC0, 0x01, + 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0x33, 0x07, 0x03, 0x00, 0x00, 0x33, 0x1F, + 0x03, 0x00, 0x00, 0xF3, 0x3D, 0x03, 0x00, 0x00, 0xE3, 0x30, 0x03, 0x00, 0x00, 0x06, 0xA0, 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, + 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, // 174 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, // 175 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, + 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1F, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, + 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0xF8, 0x7F, 0x0C, 0x00, 0x00, 0xF8, 0x7F, + 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, + 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, // 177 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x70, 0x03, + 0x00, 0x00, 0xC0, 0x39, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x03, 0x00, 0x00, 0x00, 0x07, 0x03, // 178 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, - 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x9C, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, - 0x00, 0x00, 0x80, 0xF3, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x00, 0x10, // 180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, - 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, - 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, - 0x00, 0x00, 0x00, 0x00, 0x0E, // 181 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x80, - 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, - 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, - 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, // 184 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0xFF, - 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, + 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x9C, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xF3, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x10, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, + 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 181 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, + 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, + 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x80, 0x0F, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x30, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, // 184 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, // 185 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x7E, 0x30, 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0xC0, 0x00, - 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0x80, 0xC3, 0x31, - 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x7E, 0x30, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, - 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, - 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 187 - 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, - 0xFF, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x18, 0x00, 0x00, 0xC0, - 0x06, 0x1E, 0x00, 0x00, 0x00, 0x06, 0x1B, 0x00, 0x00, 0x00, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, - 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x30, 0x00, + 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, + 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x7E, + 0x30, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, + 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 187 + 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, + 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x18, 0x00, 0x00, 0xC0, 0x06, 0x1E, 0x00, 0x00, 0x00, 0x06, + 0x1B, 0x00, 0x00, 0x00, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, + 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 188 - 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, - 0xFF, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, - 0x66, 0xC0, 0x00, 0x00, 0x00, 0x36, 0xE0, 0x00, 0x00, 0x00, 0x33, 0xF0, 0x00, 0x00, 0x00, 0x33, 0xF8, 0x00, 0x00, 0x00, 0x33, - 0xDC, 0x00, 0x00, 0x00, 0x73, 0xCE, 0x00, 0x00, 0x80, 0xE1, 0xC7, 0x00, 0x00, 0x80, 0xC1, 0xC1, // 189 - 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x60, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0x30, - 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x18, 0x00, 0x30, 0xE7, - 0x06, 0x1E, 0x00, 0xE0, 0x7F, 0x06, 0x1B, 0x00, 0xE0, 0x3C, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, - 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, + 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x66, 0xC0, 0x00, 0x00, 0x00, 0x36, + 0xE0, 0x00, 0x00, 0x00, 0x33, 0xF0, 0x00, 0x00, 0x00, 0x33, 0xF8, 0x00, 0x00, 0x00, 0x33, 0xDC, 0x00, 0x00, 0x00, 0x73, 0xCE, 0x00, 0x00, 0x80, + 0xE1, 0xC7, 0x00, 0x00, 0x80, 0xC1, 0xC1, // 189 + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x60, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x0C, 0x00, + 0x00, 0x30, 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x18, 0x00, 0x30, 0xE7, 0x06, 0x1E, 0x00, 0xE0, 0x7F, 0x06, + 0x1B, 0x00, 0xE0, 0x3C, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, + 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, - 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0xF0, - 0xFC, 0x81, 0x03, 0x00, 0xF0, 0xFC, 0x80, 0x03, 0x00, 0xF0, 0x7C, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, // 191 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xCE, 0x3F, - 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 192 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0xD8, 0x3F, - 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0x02, 0xFC, 0x7F, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 193 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, - 0x70, 0x00, 0x00, 0xC2, 0x03, 0x70, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0x18, 0xFC, 0x7F, - 0x00, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 194 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x0C, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, - 0x70, 0x00, 0x00, 0xCE, 0x03, 0x70, 0x00, 0x00, 0xCC, 0x3F, 0x70, 0x00, 0x00, 0x8C, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, - 0x00, 0x00, 0x06, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 195 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, - 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, - 0x00, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 196 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0xC3, 0x1F, - 0x70, 0x00, 0x00, 0xC3, 0x01, 0x70, 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0x7E, 0xFE, 0x7F, - 0x00, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x0C, // 197 - 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x77, 0x00, 0x00, 0xC0, 0x7F, 0x70, 0x00, 0x00, 0xC0, 0x07, 0x70, 0x00, 0x00, 0xC0, 0x01, - 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0xF0, 0xFC, 0x81, 0x03, 0x00, 0xF0, 0xFC, + 0x80, 0x03, 0x00, 0xF0, 0x7C, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, + 0x00, 0xE0, // 191 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, + 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 192 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, + 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 193 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0xC2, 0x03, 0x70, + 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 194 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x0C, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0xCE, 0x03, 0x70, + 0x00, 0x00, 0xCC, 0x3F, 0x70, 0x00, 0x00, 0x8C, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 195 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, + 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 196 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, + 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xC3, 0x01, 0x70, + 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 197 + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x80, 0xFF, 0x77, 0x00, 0x00, 0xC0, 0x7F, 0x70, 0x00, 0x00, 0xC0, 0x07, 0x70, 0x00, 0x00, 0xC0, 0x01, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 198 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, - 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x03, 0xC0, 0x01, - 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x3E, 0x03, 0xC0, 0x01, 0x00, 0xFE, 0x03, 0xC0, 0x01, 0x00, - 0xCE, 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 199 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, - 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 200 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, - 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 201 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, - 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, - 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 202 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, - 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 203 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, - 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0xFF, - 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 204 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, - 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0xFF, - 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 205 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, - 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0xFF, - 0xFF, 0x0F, 0x00, 0xC2, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, - 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 206 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, - 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, - 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 207 - 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, - 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 208 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, - 0xFF, 0xFF, 0x0F, 0x00, 0xC7, 0x07, 0x00, 0x00, 0x00, 0x03, 0x3F, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x03, 0xE0, - 0x07, 0x00, 0x00, 0x06, 0x80, 0x1F, 0x00, 0x00, 0x06, 0x00, 0xFC, 0x00, 0x00, 0x06, 0x00, 0xF0, 0x03, 0x00, 0x07, 0x00, 0x80, - 0x0F, 0x00, 0xC3, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 209 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, - 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, - 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 210 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, - 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0x82, 0x03, 0x00, - 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 211 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, - 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0x98, 0x03, 0x00, - 0x07, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x8C, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, - 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, - 0x07, 0x00, 0x86, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 213 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, - 0x07, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 214 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, - 0xE0, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0xC0, 0xE1, - 0x00, 0x00, 0x00, 0xE0, 0xC0, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 215 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xF0, 0x3F, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, - 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xF0, 0x07, 0x00, 0x80, 0x03, 0x38, 0x07, 0x00, 0xC0, 0x01, 0x1C, 0x0E, 0x00, 0xC0, 0x01, - 0x06, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xE1, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, - 0x07, 0x00, 0x80, 0x1F, 0xC0, 0x07, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00, 0xC0, 0xF1, 0x3F, 0x00, - 0x00, 0xC0, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, - 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x02, 0x00, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, - 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, - 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 217 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, - 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, - 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x00, 0x02, 0x00, 0x80, - 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, - 0xFF, 0xFF, 0x07, 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, - 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x80, - 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 219 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, - 0xFF, 0xFF, 0x07, 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x80, - 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 220 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x10, 0xF8, 0x01, 0x00, 0x00, 0x18, 0xE0, - 0xFF, 0x0F, 0x00, 0x1C, 0x80, 0xFF, 0x0F, 0x00, 0x0E, 0xE0, 0xFF, 0x0F, 0x00, 0x06, 0xF8, 0x01, 0x00, 0x00, 0x02, 0xFC, 0x00, - 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, - 0x00, 0x40, // 221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, - 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x78, - 0x00, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, - 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0xE0, - 0x01, 0x0E, 0x00, 0xE0, 0xF0, 0x03, 0x0E, 0x00, 0xE0, 0xF8, 0x07, 0x0E, 0x00, 0xE0, 0x19, 0x06, 0x0E, 0x00, 0xC0, 0x0F, 0x0E, - 0x0E, 0x00, 0x80, 0x0F, 0x1C, 0x0F, 0x00, 0x00, 0x0E, 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, + 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x03, 0xC0, 0x01, 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, + 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x3E, 0x03, 0xC0, 0x01, 0x00, 0xFE, 0x03, 0xC0, 0x01, 0x00, 0xCE, 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, + 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 199 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, + 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, + 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xD0, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, + 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 202 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, + 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, + 0x0F, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 204 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, + 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, + 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 205 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, + 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0xFF, 0xFF, + 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 206 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, + 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 207 + 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, + 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 208 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, + 0x00, 0xC7, 0x07, 0x00, 0x00, 0x00, 0x03, 0x3F, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0x00, 0x00, 0x06, 0x80, 0x1F, + 0x00, 0x00, 0x06, 0x00, 0xFC, 0x00, 0x00, 0x06, 0x00, 0xF0, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0x00, 0xC3, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 209 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, + 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, + 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 210 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, + 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, + 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, + 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, + 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x8C, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, + 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0x86, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, + 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, + 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 214 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0xE0, 0xC0, 0x01, + 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0xE0, 0xC0, 0x01, 0x00, 0x00, 0x70, + 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 215 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xF0, 0x3F, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, + 0x00, 0x80, 0x0F, 0xF0, 0x07, 0x00, 0x80, 0x03, 0x38, 0x07, 0x00, 0xC0, 0x01, 0x1C, 0x0E, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xE1, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, 0x07, 0x00, 0x80, 0x1F, 0xC0, 0x07, 0x00, 0x00, 0xFE, + 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00, 0xC0, 0xF1, 0x3F, 0x00, 0x00, 0xC0, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, + 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x02, 0x00, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, + 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, + 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 217 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, + 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x00, 0x02, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, + 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, + 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, + 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, + 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, + 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 220 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x10, 0xF8, 0x01, 0x00, 0x00, 0x18, 0xE0, 0xFF, 0x0F, 0x00, 0x1C, 0x80, 0xFF, + 0x0F, 0x00, 0x0E, 0xE0, 0xFF, 0x0F, 0x00, 0x06, 0xF8, 0x01, 0x00, 0x00, 0x02, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, + 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, + 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0xFF, 0x0F, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0xE0, 0x01, 0x0E, 0x00, 0xE0, 0xF0, 0x03, + 0x0E, 0x00, 0xE0, 0xF8, 0x07, 0x0E, 0x00, 0xE0, 0x19, 0x06, 0x0E, 0x00, 0xC0, 0x0F, 0x0E, 0x0E, 0x00, 0x80, 0x0F, 0x1C, 0x0F, 0x00, 0x00, 0x0E, + 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x01, // 223 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x10, - 0xE0, 0xF8, 0x07, 0x00, 0x30, 0xE0, 0x38, 0x0F, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, - 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, - 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, - 0xE0, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, - 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x06, 0x00, 0x70, 0x70, 0x1C, 0x07, 0x00, 0x30, 0xE0, 0x9C, - 0x01, 0x00, 0x10, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 225 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, - 0xE2, 0xF8, 0x07, 0x00, 0x80, 0xE3, 0x38, 0x0F, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, - 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x73, 0x1C, 0x07, 0x00, 0x00, 0xE2, 0x9C, - 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 226 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0xC0, - 0xE1, 0xF8, 0x07, 0x00, 0xE0, 0xE1, 0x38, 0x0F, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, - 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x71, 0x1C, 0x07, 0x00, 0xE0, 0xE1, 0x9C, - 0x01, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 227 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, - 0xE0, 0xF8, 0x07, 0x00, 0xE0, 0xE0, 0x38, 0x0F, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, - 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0xE0, 0x70, 0x1C, 0x07, 0x00, 0xE0, 0xE0, 0x9C, - 0x01, 0x00, 0xE0, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 228 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, - 0xE0, 0xF8, 0x07, 0x00, 0xF0, 0xE0, 0x38, 0x0F, 0x00, 0xF8, 0x71, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, - 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x06, 0x00, 0xF8, 0x71, 0x1C, 0x07, 0x00, 0xF0, 0xE0, 0x9C, - 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 229 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, - 0x70, 0x3C, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0F, 0x00, 0x00, 0xE0, - 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, - 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1F, 0x0E, 0x00, 0x00, 0xE0, 0x1F, 0x07, - 0x00, 0x00, 0x80, 0x1F, 0x07, // 230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, - 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, - 0x00, 0x0F, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x3E, 0x03, 0x00, 0x70, 0x00, - 0xFE, 0x03, 0x00, 0x70, 0x00, 0xCE, 0x01, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 231 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x10, - 0xC0, 0xFF, 0x03, 0x00, 0x30, 0xE0, 0x9D, 0x07, 0x00, 0x70, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, - 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, - 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 232 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, - 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, - 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0xF0, 0x1C, - 0x0E, 0x00, 0x10, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 233 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, - 0xC2, 0xFF, 0x03, 0x00, 0x80, 0xE3, 0x9D, 0x07, 0x00, 0xC0, 0xE1, 0x1C, 0x07, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, - 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0xF2, 0x1C, - 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 234 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, - 0xC0, 0xFF, 0x03, 0x00, 0xE0, 0xE0, 0x9D, 0x07, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, - 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0xF0, 0x1C, - 0x0E, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 235 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x10, - 0x70, 0x00, 0x0E, 0x00, 0x30, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0xF3, - 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0xFF, 0x0F, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 236 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, - 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF3, - 0xFF, 0x0F, 0x00, 0xC0, 0xF3, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, - 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 237 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, - 0x72, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0xF0, 0x70, 0x00, 0x0E, 0x00, 0x30, 0xF0, - 0xFF, 0x0F, 0x00, 0xF0, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 238 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, - 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, - 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 239 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, - 0xE6, 0xFF, 0x07, 0x00, 0x60, 0xE2, 0x81, 0x07, 0x00, 0xE0, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x77, - 0x00, 0x0E, 0x00, 0x80, 0x7F, 0x00, 0x0E, 0x00, 0x80, 0x7E, 0x00, 0x0E, 0x00, 0xC0, 0x7C, 0x00, 0x0F, 0x00, 0xC0, 0xF8, 0x81, - 0x07, 0x00, 0x40, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, // 240 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, - 0xF1, 0xFF, 0x0F, 0x00, 0xE0, 0xF1, 0xFF, 0x0F, 0x00, 0x60, 0xC0, 0x01, 0x00, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x60, - 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xF1, 0x00, - 0x00, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 241 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x10, - 0xE0, 0xFF, 0x07, 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x73, - 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, - 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 242 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, - 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0x73, - 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0x30, 0xE0, 0x81, - 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 243 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, - 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x70, 0x70, - 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0x00, 0xE2, 0x81, - 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 244 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xC0, - 0xE1, 0xFF, 0x07, 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xF0, 0x00, 0x0F, 0x00, 0x60, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, - 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF1, 0x00, 0x0F, 0x00, 0xE0, 0xE1, 0x81, - 0x07, 0x00, 0x60, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 245 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xE0, - 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0xE0, 0x81, - 0x07, 0x00, 0xE0, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 246 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, - 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 247 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x7F, 0x0E, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, - 0xE0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0xE0, 0x07, 0x00, 0x00, 0x70, 0x70, 0x0E, 0x00, 0x00, 0x70, - 0x38, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x00, 0xE0, 0x83, - 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x38, 0xFE, 0x00, 0x00, 0x00, 0x10, // 248 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x10, - 0xF0, 0xFF, 0x07, 0x00, 0x30, 0xF0, 0xFF, 0x07, 0x00, 0x70, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, - 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, - 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x03, - 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x30, 0x00, 0x80, - 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, - 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF2, 0xFF, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xF0, 0x00, - 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x07, 0x00, 0x80, 0x03, 0x80, - 0x01, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 251 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, - 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x80, - 0x01, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 252 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, - 0xF0, 0x03, 0x80, 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x02, 0xFC, 0xF3, 0x01, 0x00, 0x03, - 0xF0, 0xFF, 0x01, 0xC0, 0x03, 0xC0, 0x7F, 0x00, 0xE0, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x30, 0x80, 0x7F, - 0x00, 0x00, 0x10, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 253 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, - 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, - 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 254 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, - 0xF0, 0x03, 0x80, 0x03, 0xE0, 0xE0, 0x1F, 0x80, 0x03, 0xE0, 0x80, 0x7F, 0xC0, 0x03, 0xE0, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, - 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F, - 0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x10, 0xE0, 0xF8, 0x07, + 0x00, 0x30, 0xE0, 0x38, 0x0F, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, + 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, + 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, + 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x06, 0x00, 0x70, 0x70, 0x1C, 0x07, 0x00, 0x30, 0xE0, 0x9C, 0x01, 0x00, 0x10, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE2, 0xF8, 0x07, + 0x00, 0x80, 0xE3, 0x38, 0x0F, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, + 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x73, 0x1C, 0x07, 0x00, 0x00, 0xE2, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0xC0, 0xE1, 0xF8, 0x07, + 0x00, 0xE0, 0xE1, 0x38, 0x0F, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, + 0x0E, 0x00, 0x80, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x71, 0x1C, 0x07, 0x00, 0xE0, 0xE1, 0x9C, 0x01, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 227 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, + 0x00, 0xE0, 0xE0, 0x38, 0x0F, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, + 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0xE0, 0x70, 0x1C, 0x07, 0x00, 0xE0, 0xE0, 0x9C, 0x01, 0x00, 0xE0, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, + 0x00, 0xF0, 0xE0, 0x38, 0x0F, 0x00, 0xF8, 0x71, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, + 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x06, 0x00, 0xF8, 0x71, 0x1C, 0x07, 0x00, 0xF0, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 229 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x3C, 0x0F, + 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, + 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, + 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1F, 0x0E, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x07, // 230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, + 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x03, 0x00, 0x70, 0x00, + 0x0E, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x3E, 0x03, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x70, 0x00, 0xCE, 0x01, 0x00, 0xE0, + 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 231 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x10, 0xC0, 0xFF, 0x03, + 0x00, 0x30, 0xE0, 0x9D, 0x07, 0x00, 0x70, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, + 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, + 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, + 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, + 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0xF0, 0x1C, 0x0E, 0x00, 0x10, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, + 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC2, 0xFF, 0x03, + 0x00, 0x80, 0xE3, 0x9D, 0x07, 0x00, 0xC0, 0xE1, 0x1C, 0x07, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, + 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0xF2, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, + 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, + 0x00, 0xE0, 0xE0, 0x9D, 0x07, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, + 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0xF0, 0x1C, 0x0E, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, + 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 235 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x70, 0x00, 0x0E, + 0x00, 0x30, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0xF3, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0xFF, + 0x0F, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 236 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, + 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF3, 0xFF, 0x0F, 0x00, 0xC0, 0xF3, 0xFF, + 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, + 0x00, 0x80, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0xF0, 0x70, 0x00, 0x0E, 0x00, 0x30, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0xF0, 0xFF, + 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 238 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, + 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, + 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 239 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0xE6, 0xFF, 0x07, + 0x00, 0x60, 0xE2, 0x81, 0x07, 0x00, 0xE0, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x77, 0x00, 0x0E, 0x00, 0x80, 0x7F, 0x00, + 0x0E, 0x00, 0x80, 0x7E, 0x00, 0x0E, 0x00, 0xC0, 0x7C, 0x00, 0x0F, 0x00, 0xC0, 0xF8, 0x81, 0x07, 0x00, 0x40, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, + 0x00, 0xE0, 0xF1, 0xFF, 0x0F, 0x00, 0x60, 0xC0, 0x01, 0x00, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, + 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xF1, 0x00, 0x00, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x10, 0xE0, 0xFF, 0x07, + 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, + 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 242 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, + 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, + 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 243 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, + 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, + 0x0E, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 244 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xC0, 0xE1, 0xFF, 0x07, + 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xF0, 0x00, 0x0F, 0x00, 0x60, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, + 0x0E, 0x00, 0x80, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF1, 0x00, 0x0F, 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 245 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xE0, 0xE0, 0xFF, 0x07, + 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, + 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 247 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x7F, 0x0E, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x03, + 0x00, 0x00, 0xE0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0xE0, 0x07, 0x00, 0x00, 0x70, 0x70, 0x0E, 0x00, 0x00, 0x70, 0x38, 0x0E, 0x00, 0x00, 0x70, 0x1C, + 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x00, 0xE0, 0x83, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, + 0xFF, 0x03, 0x00, 0x00, 0x38, 0xFE, 0x00, 0x00, 0x00, 0x10, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x07, + 0x00, 0x30, 0xF0, 0xFF, 0x07, 0x00, 0x70, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, + 0x0E, 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, + 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, + 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x30, 0x00, 0x80, 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, + 0x00, 0x00, 0xF2, 0xFF, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, + 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x07, 0x00, 0x80, 0x03, 0x80, 0x01, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, + 0x00, 0xE0, 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x80, 0x01, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, + 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x02, 0xFC, 0xF3, 0x01, 0x00, 0x03, 0xF0, 0xFF, 0x01, 0xC0, 0x03, 0xC0, + 0x7F, 0x00, 0xE0, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x30, 0x80, 0x7F, 0x00, 0x00, 0x10, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, + 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 253 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, + 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, + 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, + 0x03, 0xE0, 0xE0, 0x1F, 0x80, 0x03, 0xE0, 0x80, 0x7F, 0xC0, 0x03, 0xE0, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, + 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F, 0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, + 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255 }; #endif // USE_EINK diff --git a/src/graphics/fonts/OLEDDisplayFontsCS.cpp b/src/graphics/fonts/OLEDDisplayFontsCS.cpp index c8045285e..f78ea13f5 100644 --- a/src/graphics/fonts/OLEDDisplayFontsCS.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsCS.cpp @@ -234,219 +234,220 @@ const uint8_t ArialMT_Plain_10_CS[] PROGMEM = { 0x08, 0x0A, 0x0A, 0x06, // 254 0x08, 0x14, 0x09, 0x06, // 255 // Font Data: - 0x00, 0x00, 0xF8, 0x02, // 33 - 0x38, 0x00, 0x00, 0x00, 0x38, // 34 - 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 - 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 - 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 - 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 - 0x38, // 39 - 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 - 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 - 0x28, 0x00, 0x18, 0x00, 0x28, // 42 - 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 - 0x00, 0x00, 0x00, 0x06, // 44 - 0x80, 0x00, 0x80, // 45 - 0x00, 0x00, 0x00, 0x02, // 46 - 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 - 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 - 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 - 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 - 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 - 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 - 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 - 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 - 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 - 0x00, 0x00, 0x20, 0x02, // 58 - 0x00, 0x00, 0x20, 0x06, // 59 - 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 - 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 - 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 - 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 - 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 - 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 - 0x00, 0x00, 0xF8, 0x03, // 73 - 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 - 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 - 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 - 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 - 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 - 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 - 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 - 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 - 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 - 0x08, 0x08, 0xF8, 0x0F, // 93 - 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 - 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 - 0x08, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 - 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 - 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 - 0x00, 0x00, 0xE8, 0x03, // 105 - 0x00, 0x08, 0xE8, 0x07, // 106 - 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 - 0x00, 0x00, 0xF8, 0x03, // 108 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 - 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 - 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 - 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 - 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 - 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 - 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 - 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 - 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 - 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 - 0x00, 0x00, 0xF8, 0x0F, // 124 - 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 - 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 - 0x00, 0x00, 0xF0, 0x01, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 129 - 0x00, 0x00, 0xF8, 0x03, 0x09, 0x02, 0x0A, 0x02, 0x11, 0x01, 0xE0, // 130 - 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 131 - 0x00, 0x00, 0xF8, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 132 - 0x00, 0x00, 0xF8, 0x03, 0x49, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 133 - 0x00, 0x00, 0x30, 0x01, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 134 - 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0xFA, 0x03, 0x09, 0x00, 0x08, // 135 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x05, 0x02, 0x02, 0x02, 0xF8, 0x01, // 136 - 0x08, 0x03, 0x88, 0x02, 0xC9, 0x02, 0x6A, 0x02, 0x39, 0x02, 0x18, 0x02, // 137 - 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0x44, 0x01, // 138 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, 0x00, 0x00, 0x18, // 139 - 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC4, 0x02, // 140 - 0x00, 0x00, 0xE0, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 141 - 0x00, 0x00, 0xE4, 0x03, 0x28, 0x00, 0x04, // 142 - 0x40, 0x02, 0xA4, 0x02, 0xA8, 0x02, 0x24, 0x01, // 143 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x18, // 144 - 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x14, 0x02, 0xE8, 0x03, // 145 - 0x20, 0x02, 0x24, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 146 - 0x00, 0x00, 0xFA, 0x03, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 147 - 0x00, 0x00, 0xFA, 0x03, 0x01, // 148 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x18, 0x02, 0x00, 0x02, // 149 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x18, // 150 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 151 - 0x00, 0x00, 0xE0, 0x03, 0x28, 0x00, 0x04, // 152 - 0x00, 0x00, 0xA0, 0x0F, // 161 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 - 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 - 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 - 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 - 0x00, 0x00, 0x38, 0x0F, // 166 - 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 - 0x08, 0x00, 0x00, 0x00, 0x08, // 168 - 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 - 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 - 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 - 0x80, 0x00, 0x80, // 173 - 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 - 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 - 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 - 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 - 0x48, 0x00, 0x68, 0x00, 0x58, // 178 - 0x48, 0x00, 0x58, 0x00, 0x68, // 179 - 0x00, 0x00, 0x10, 0x00, 0x08, // 180 - 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 - 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 - 0x00, 0x00, 0x40, // 183 - 0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 - 0x00, 0x00, 0x10, 0x00, 0x78, // 185 - 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 - 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 - 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 - 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 - 0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 - 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 - 0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 - 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 - 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 - 0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 - 0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 - 0x00, 0x00, 0xF9, 0x03, 0x02, // 204 - 0x02, 0x00, 0xF9, 0x03, // 205 - 0x01, 0x00, 0xFA, 0x03, // 206 - 0x02, 0x00, 0xF8, 0x03, 0x02, // 207 - 0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 - 0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 - 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 - 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 - 0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 - 0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 - 0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 - 0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 - 0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 - 0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 - 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 - 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 - 0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 - 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 - 0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 - 0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 - 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 - 0x00, 0x00, 0xE4, 0x03, 0x08, // 236 - 0x08, 0x00, 0xE4, 0x03, // 237 - 0x08, 0x00, 0xE4, 0x03, 0x08, // 238 - 0x08, 0x00, 0xE0, 0x03, 0x08, // 239 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 - 0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 - 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 - 0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 - 0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 - 0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 - 0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 - 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 - 0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 - 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 - 0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 - 0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 - 0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, + 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xF0, 0x01, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 129 + 0x00, 0x00, 0xF8, 0x03, 0x09, 0x02, 0x0A, 0x02, 0x11, 0x01, 0xE0, // 130 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 131 + 0x00, 0x00, 0xF8, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 132 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 133 + 0x00, 0x00, 0x30, 0x01, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 134 + 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0xFA, 0x03, 0x09, 0x00, 0x08, // 135 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x05, 0x02, 0x02, 0x02, 0xF8, 0x01, // 136 + 0x08, 0x03, 0x88, 0x02, 0xC9, 0x02, 0x6A, 0x02, 0x39, 0x02, 0x18, 0x02, // 137 + 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0x44, 0x01, // 138 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, 0x00, 0x00, 0x18, // 139 + 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC4, 0x02, // 140 + 0x00, 0x00, 0xE0, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 141 + 0x00, 0x00, 0xE4, 0x03, 0x28, 0x00, 0x04, // 142 + 0x40, 0x02, 0xA4, 0x02, 0xA8, 0x02, 0x24, 0x01, // 143 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x18, // 144 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x14, 0x02, 0xE8, 0x03, // 145 + 0x20, 0x02, 0x24, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 146 + 0x00, 0x00, 0xFA, 0x03, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 147 + 0x00, 0x00, 0xFA, 0x03, 0x01, // 148 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x18, 0x02, 0x00, 0x02, // 149 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x18, // 150 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 151 + 0x00, 0x00, 0xE0, 0x03, 0x28, 0x00, 0x04, // 152 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x08, 0x00, 0x00, 0x00, 0x08, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x48, 0x00, 0x68, 0x00, 0x58, // 178 + 0x48, 0x00, 0x58, 0x00, 0x68, // 179 + 0x00, 0x00, 0x10, 0x00, 0x08, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 + 0x00, 0x00, 0x10, 0x00, 0x78, // 185 + 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 + 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 + 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 + 0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 + 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 + 0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 + 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 + 0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 + 0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 + 0x00, 0x00, 0xF9, 0x03, 0x02, // 204 + 0x02, 0x00, 0xF9, 0x03, // 205 + 0x01, 0x00, 0xFA, 0x03, // 206 + 0x02, 0x00, 0xF8, 0x03, 0x02, // 207 + 0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 + 0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 + 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 + 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 + 0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 + 0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 + 0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 + 0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 + 0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 + 0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 + 0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 + 0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 + 0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 + 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 + 0x00, 0x00, 0xE4, 0x03, 0x08, // 236 + 0x08, 0x00, 0xE4, 0x03, // 237 + 0x08, 0x00, 0xE4, 0x03, 0x08, // 238 + 0x08, 0x00, 0xE0, 0x03, 0x08, // 239 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 + 0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 + 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 + 0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 + 0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 + 0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 + 0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 + 0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 + 0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 + 0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 }; const uint8_t ArialMT_Plain_16_CS[] PROGMEM = { @@ -682,354 +683,344 @@ const uint8_t ArialMT_Plain_16_CS[] PROGMEM = { // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 - 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, - 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 - 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, - 0x20, 0x1C, // 36 - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, - 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, - 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 - 0x00, 0x00, 0x00, 0x78, // 39 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 - 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x02, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 - 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, - 0xE0, 0x1F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 - 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, - 0xE0, 0x40, // 50 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, - 0x00, 0x1C, // 51 - 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x08, // 52 - 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, - 0x08, 0x1E, // 53 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, - 0x20, 0x1E, // 54 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, + 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, + 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, + 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x08, 0x07, 0x00, 0xC8, 0x00, 0x00, 0x28, 0x00, 0x00, - 0x18, // 55 - 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, - 0x60, 0x1C, // 56 - 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, - 0xE0, 0x1F, // 57 - 0x00, 0x00, 0x00, 0x40, 0x40, // 58 - 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, - 0x40, 0x10, // 60 - 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, - 0x80, 0x08, // 61 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, - 0x00, 0x02, // 62 + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, - 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, - 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, // 70 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, - 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, // 76 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, - 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, - 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 84 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, + 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, + 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, + 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, + 0x4F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, + 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, + 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, - 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x08, // 89 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, - 0x18, 0x40, 0x00, 0x08, 0x40, // 90 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 - 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 97 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xF8, 0x7F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 101 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, - 0xC0, 0xFF, // 103 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, - 0x40, 0x40, // 107 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 111 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xC0, 0xFF, 0x03, // 113 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 - 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 - 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 - 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x01, // 126 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, - 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 129 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, - 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 130 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x41, 0x00, - 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 131 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x81, 0x00, 0x00, 0x02, 0x03, 0x00, 0x02, 0x04, 0x00, - 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 132 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x09, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x0A, 0x06, 0x00, - 0x09, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 133 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x42, 0x00, - 0x09, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 134 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 135 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x06, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, - 0x06, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 136 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x09, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x8A, 0x40, 0x00, 0x69, 0x40, 0x00, - 0x18, 0x40, 0x00, 0x08, 0x40, // 137 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, // 138 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 139 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x88, 0x24, 0x00, - 0x00, 0x17, // 140 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x88, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 141 - 0x00, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 142 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x38, // 143 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, // 144 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x0C, 0x40, 0x00, 0x12, 0x40, 0x00, 0x12, 0x40, 0x00, 0x0C, 0x20, 0x00, 0xC0, 0x7F, // 145 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x48, 0x58, 0x00, 0x50, 0x44, 0x00, 0x50, 0x43, 0x00, 0xC8, 0x40, 0x00, 0x40, 0x40, // 146 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, // 147 - 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 148 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x38, 0x40, 0x00, - 0x00, 0x40, // 149 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 150 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x09, 0x06, 0x00, - 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 151 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 152 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, - 0x00, 0x11, // 162 - 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, - 0x20, 0x20, // 163 - 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 - 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, - 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 - 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 - 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, - 0x00, 0x0C, // 167 - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, - 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 - 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, - 0x80, 0x0F, // 172 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, - 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 - 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x04, // 175 - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 - 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, // 177 - 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, - 0xC0, 0x7F, // 181 - 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0xF8, 0xFF, 0x03, 0x08, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 - 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, - 0x00, 0x04, // 187 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, - 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, - 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x01, 0x00, 0xC0, // 191 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, - 0x82, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, - 0x81, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 - 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, - 0x08, 0x04, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, // 198 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, - 0x08, 0x40, 0x03, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 199 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 200 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 201 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, - 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 202 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 203 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, + 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, + 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, + 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, + 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, + 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, + 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, + 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, + 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, + 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, + 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, + 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, + 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 129 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 130 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 131 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x81, 0x00, 0x00, 0x02, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x7F, // 132 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x09, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x0A, 0x06, 0x00, 0x09, 0x1A, 0x00, + 0x10, 0x21, 0x00, 0xE0, 0x40, // 133 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x42, 0x00, 0x09, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x1C, // 134 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 135 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x06, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x06, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 136 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x09, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x8A, 0x40, 0x00, 0x69, 0x40, 0x00, 0x18, 0x40, 0x00, + 0x08, 0x40, // 137 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, + 0x20, // 138 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x00, 0x00, 0x38, // 139 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x88, 0x24, 0x00, 0x00, 0x17, // 140 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x88, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, + 0x7F, // 141 + 0x00, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 142 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, + 0x38, // 143 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, // 144 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x0C, 0x40, 0x00, 0x12, 0x40, 0x00, 0x12, 0x40, 0x00, 0x0C, 0x20, 0x00, 0xC0, + 0x7F, // 145 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x48, 0x58, 0x00, 0x50, 0x44, 0x00, 0x50, 0x43, 0x00, 0xC8, 0x40, 0x00, 0x40, + 0x40, // 146 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 147 + 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 148 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x38, 0x40, 0x00, 0x00, 0x40, // 149 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 150 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x09, 0x06, 0x00, 0x08, 0x1A, 0x00, + 0x10, 0x21, 0x00, 0xE0, 0x40, // 151 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 152 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 + 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, + 0x0B, // 164 + 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, + 0x08, 0x0A, // 165 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, + 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, + 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, // 175 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 + 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, + 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, + 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x20, // 188 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, + 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, + 0x4C, // 189 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, + 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x20, // 190 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, + 0x00, 0xC0, // 191 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, 0x82, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, 0x81, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 + 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, 0x08, 0x04, 0x00, + 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, // 198 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, 0x08, 0x40, 0x03, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 199 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 200 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 201 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 202 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 203 0x01, 0x00, 0x00, 0xFA, 0x7F, // 204 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 205 0x02, 0x00, 0x00, 0xF9, 0x7F, 0x00, 0x01, 0x00, 0x00, 0x02, // 206 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 207 - 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 208 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, - 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 210 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 211 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, - 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 212 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, - 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 213 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 214 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, - 0x40, 0x10, // 215 - 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, - 0xC8, 0x40, 0x00, 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, 0x0F, // 216 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, - 0x02, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x08, // 221 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, - 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 222 - 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x38, // 223 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 224 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 225 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, - 0x80, 0x7F, // 226 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, - 0x80, 0x7F, // 227 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 228 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 229 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x3F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, 0x20, // 231 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 232 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 233 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, - 0x00, 0x17, // 234 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 235 - 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 - 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 - 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 - 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, - 0x00, 0x1F, // 240 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 241 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 242 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 243 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, - 0x00, 0x1F, // 244 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, - 0x00, 0x1F, // 245 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 246 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x02, // 247 - 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, - 0x40, 0x1F, // 248 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 249 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, 0x7F, // 250 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, 0x7F, // 251 - 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 252 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 253 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 254 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 255 + 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 208 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 210 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 211 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 212 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 213 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 214 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 215 + 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, 0xC8, 0x40, 0x00, + 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, + 0x0F, // 216 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x08, // 221 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, + 0x40, 0x08, 0x00, 0x80, 0x07, // 222 + 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 223 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 225 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, 0x80, 0x7F, // 226 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, 0x80, 0x7F, // 227 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 228 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 229 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x3F, 0x00, + 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, + 0x20, // 231 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 232 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 233 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 234 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 235 + 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 + 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 + 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 + 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, 0x00, 0x1F, // 240 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, + 0x7F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 242 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 243 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, 0x00, 0x1F, // 244 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, 0x00, 0x1F, // 245 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 246 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 247 + 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, 0x40, 0x1F, // 248 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, + 0x7F, // 249 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, + 0x7F, // 250 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, + 0x7F, // 251 + 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, + 0x7F, // 252 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 253 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 254 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 255 }; const uint8_t ArialMT_Plain_24_CS[] PROGMEM = { @@ -1263,604 +1254,542 @@ const uint8_t ArialMT_Plain_24_CS[] PROGMEM = { 0x26, 0x59, 0x2F, 0x0D, // 254 0x26, 0x88, 0x2A, 0x0C, // 255 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, - 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 - 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, - 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, - 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, - 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, - 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, - 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, - 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, - 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, - 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, - 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, - 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, - 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, + 0x33, // 33 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, + 0xE0, 0x07, // 34 + 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, + 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, + 0x00, 0x0C, 0x03, // 35 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, + 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, + 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, + 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, 0x3C, 0x30, 0x00, + 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, + 0x20, 0x00, 0x00, 0x04, // 40 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, + 0x00, 0xF0, 0x0F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, + 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, + 0x80, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, + 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, + 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x60, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, - 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, - 0x00, 0x33, 0x00, 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, - 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, - 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, - 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x03, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, - 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, - 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, - 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, - 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, - 0x80, 0x3F, 0x00, 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, - 0x00, 0x00, 0x60, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, - 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, - 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, - 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, - 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, - 0x03, 0x00, 0x00, 0x03, 0x06, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, - 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, - 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x20, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, - 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, - 0x00, 0x00, 0x00, 0x07, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, - 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, - 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, - 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, - 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, - 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, + 0x60, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, + 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, + 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, + 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, + 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, + 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, + 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, + 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, + 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, + 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, + 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, + 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, + 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, + 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, + 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, + 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, + 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, - 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, - 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, - 0x00, 0x00, 0xE2, 0x0F, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, - 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, + 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, + 0x0F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, + 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, - 0x00, 0xE0, 0xFF, 0x3F, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, - 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, - 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, - 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, - 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, + 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, + 0x0F, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, + 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, + 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, - 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, - 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 - 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, - 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, - 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, - 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, - 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 - 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, - 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, - 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, - 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 - 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, - 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, - 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, - 0xFF, 0xFF, 0x07, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, + 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 + 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, + 0x20, // 86 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, + 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, + 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, + 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, + 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, + 0x06, // 91 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, + 0x00, 0x00, 0x30, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, + 0x07, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, - 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, - 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, - 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, + 0x00, 0xE0, 0x03, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, - 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, - 0x06, 0x00, 0x00, 0x60, 0x06, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, - 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, - 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, - 0x30, 0x00, 0x00, 0x00, 0x20, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, - 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, - 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, - 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, + 0x60, 0x06, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, + 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, + 0x00, 0xE0, 0x03, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, + 0x07, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, - 0x00, 0x00, 0x0E, // 119 - 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, - 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, + 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, - 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, + 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, - 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, + 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, - 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, + 0x00, 0x80, 0x01, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0x60, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 129 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, - 0x30, 0x00, 0xE2, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0xE2, 0x00, 0x18, 0x00, + 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 130 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, - 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 131 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x02, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x06, 0x80, - 0x03, 0x00, 0x02, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 132 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x62, 0x30, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, 0x6C, 0x70, 0x00, 0x00, 0x6C, 0xF0, 0x00, 0x00, 0x66, 0xF0, - 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 131 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x02, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x06, 0x80, 0x03, 0x00, 0x02, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 132 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x62, 0x30, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, 0x6C, 0x70, 0x00, 0x00, 0x6C, 0xF0, 0x00, 0x00, 0x66, 0xF0, 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, + 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 133 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x62, - 0x38, 0x38, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, - 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 134 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x66, - 0x00, 0x00, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0x66, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 135 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x11, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x0E, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 136 - 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x62, - 0xC0, 0x31, 0x00, 0x66, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x6C, 0x1C, 0x30, 0x00, 0x66, 0x0E, 0x30, 0x00, 0x62, 0x07, - 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 137 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, - 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x62, 0x38, 0x38, 0x00, + 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, + 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 134 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, + 0xEC, 0xFF, 0x3F, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0x66, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, // 135 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x0E, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x11, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 136 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x62, 0xC0, 0x31, 0x00, + 0x66, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x6C, 0x1C, 0x30, 0x00, 0x66, 0x0E, 0x30, 0x00, 0x62, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, + 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 137 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, + 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 138 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, - 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 139 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, - 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xCE, 0x38, 0x00, 0x20, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 140 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x20, 0x18, 0x00, 0x00, 0x60, - 0x0C, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 141 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, - 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 142 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x20, 0xEE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, - 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, + 0x01, // 139 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, + 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xCE, 0x38, 0x00, 0x20, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x20, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, + 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 141 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, + 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 142 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x20, 0xEE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, + 0xC0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 143 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 144 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x70, - 0x00, 0x30, 0x00, 0xD8, 0x00, 0x30, 0x00, 0x88, 0x00, 0x30, 0x00, 0xD8, 0x00, 0x18, 0x00, 0x70, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 145 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x20, 0x06, 0x37, 0x00, 0x60, - 0xC6, 0x33, 0x00, 0xC0, 0xE6, 0x30, 0x00, 0xC0, 0x76, 0x30, 0x00, 0x60, 0x3E, 0x30, 0x00, 0x20, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, + 0x01, // 144 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x70, 0x00, 0x30, 0x00, + 0xD8, 0x00, 0x30, 0x00, 0x88, 0x00, 0x30, 0x00, 0xD8, 0x00, 0x18, 0x00, 0x70, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 145 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x20, 0x06, 0x37, 0x00, 0x60, 0xC6, 0x33, 0x00, + 0xC0, 0xE6, 0x30, 0x00, 0xC0, 0x76, 0x30, 0x00, 0x60, 0x3E, 0x30, 0x00, 0x20, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 146 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, - 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, // 147 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, + 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 147 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, // 148 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x03, 0x30, 0x00, 0xE0, 0x01, - 0x30, 0x00, 0x00, 0x00, 0x30, // 149 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, - 0x03, 0x00, 0x00, 0xE0, 0x01, // 150 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x68, 0x70, 0x00, 0x00, 0x6E, 0xF0, 0x00, 0x00, 0x66, 0xF0, - 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x03, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x00, 0x00, 0x30, // 149 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, + 0xE0, 0x01, // 150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x68, 0x70, 0x00, 0x00, 0x6E, 0xF0, 0x00, 0x00, 0x66, 0xF0, 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, + 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 151 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x80, 0x0C, 0x00, 0x00, 0xE0, - 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 152 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, - 0x06, 0x3F, 0x00, 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x80, 0x0C, 0x00, 0x00, 0xE0, 0x06, 0x00, 0x00, + 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 152 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, + 0x07, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, + 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 - 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, - 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, - 0x38, 0x00, 0x00, 0x00, 0x10, // 163 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, - 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, - 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 - 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, - 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, - 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 + 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, + 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, + 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, + 0x00, 0x02, 0x04, // 164 + 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, + 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, + 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, - 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, - 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x60, // 168 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, - 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, - 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, - 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, - 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, - 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, - 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, - 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, - 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, - 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 - 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, - 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, - 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x20, // 180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 - 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, - 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, - 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, + 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, // 168 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, + 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, + 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, + 0xC0, 0x3F, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, + 0x10, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x80, 0x01, // 173 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, + 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, + 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, // 175 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, + 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, + 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, + 0x00, 0x60, 0x30, // 177 + 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, + 0xC0, 0x21, // 178 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, + 0xC0, 0x1D, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x20, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 + 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, + 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, + 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, - 0x00, 0x00, 0x01, // 184 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, + 0x01, // 184 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, // 185 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, - 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, - 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x80, // 187 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, - 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x38, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, - 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, - 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, - 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, - 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, - 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, - 0x00, 0x07, 0x07, 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, // 191 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, - 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, - 0x8F, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, - 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, - 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, - 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, - 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, - 0x8F, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, - 0xBC, 0x01, 0x00, 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, - 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, - 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, - 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, + 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, // 187 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, 0x3F, 0x1C, 0x00, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, + 0x00, 0xC0, 0x21, // 189 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, + 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, + 0x00, 0x00, 0x08, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, 0x00, 0x07, 0x07, + 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, + 0x00, 0x00, 0xC0, // 191 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, + 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, + 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, 0x8F, 0x01, 0x00, + 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, 0x8F, 0x01, 0x00, + 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, + 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, + 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xBC, 0x01, 0x00, + 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 199 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, - 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, - 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, - 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x08, // 204 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x02, // 205 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, // 206 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 207 - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, + 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 208 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, - 0x03, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, - 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, - 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, - 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, - 0x00, 0x18, 0x00, 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, - 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, - 0xF8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, 0x03, 0x00, 0x00, + 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, + 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, + 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, 0x00, 0x18, 0x00, + 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, + 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, + 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, 0x03, // 215 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, - 0x00, 0x1F, 0x00, 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, - 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, - 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, - 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, - 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, - 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, - 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, - 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x08, 0x00, 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, - 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, - 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, - 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, - 0x86, 0x31, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, - 0x86, 0x31, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, - 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, - 0x86, 0x31, 0x00, 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, - 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, - 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, - 0x06, 0x30, 0x02, 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, 0x00, 0x1F, 0x00, + 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, + 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, + 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, + 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, + 0xF8, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x08, 0x00, + 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, + 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, + 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, 0x86, 0x31, 0x00, + 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, + 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, + 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, 0x86, 0x31, 0x00, + 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, + 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, 0x06, 0x30, 0x02, + 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 231 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, - 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, - 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, - 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, + 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, 0xC6, 0x30, 0x00, + 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, + 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 0x80, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 238 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 239 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, - 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, - 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, - 0x0C, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, - 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, - 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, - 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, - 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, - 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 247 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, - 0x0E, 0x3F, 0x00, 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, - 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0x07, // 248 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, - 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, - 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, 0x06, 0x30, 0x00, + 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, + 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, + 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, + 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, + 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, + 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, // 247 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x3F, 0x00, + 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, + 0x00, 0xF3, 0x07, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, + 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, - 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, + 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 }; diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp index 3a1159511..c3bb7b139 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -236,195 +236,196 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { 0x07, 0x7E, 0x0C, 0x07, // 255 // Font Data: - 0x00, 0x00, 0xF8, 0x02, // 33 - 0x38, 0x00, 0x00, 0x00, 0x38, // 34 - 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 - 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 - 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 - 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 - 0x38, // 39 - 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 - 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 - 0x28, 0x00, 0x18, 0x00, 0x28, // 42 - 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 - 0x00, 0x00, 0x00, 0x06, // 44 - 0x80, 0x00, 0x80, // 45 - 0x00, 0x00, 0x00, 0x02, // 46 - 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 - 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 - 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 - 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 - 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 - 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 - 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 - 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 - 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 - 0x00, 0x00, 0x20, 0x02, // 58 - 0x00, 0x00, 0x20, 0x06, // 59 - 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 - 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 - 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 - 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 - 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 - 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 - 0x00, 0x00, 0xF8, 0x03, // 73 - 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 - 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 - 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 - 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 - 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 - 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 - 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 - 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 - 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 - 0x08, 0x08, 0xF8, 0x0F, // 93 - 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 - 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 - 0x08, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 - 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 - 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 - 0x00, 0x00, 0xE8, 0x03, // 105 - 0x00, 0x08, 0xE8, 0x07, // 106 - 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 - 0x00, 0x00, 0xF8, 0x03, // 108 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 - 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 - 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 - 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 - 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 - 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 - 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 - 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 - 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 - 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 - 0x00, 0x00, 0xF8, 0x0F, // 124 - 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 - 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 - 0x00, 0x00, 0xA0, 0x0F, // 161 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 - 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 - 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 - 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 - 0x00, 0x00, 0x38, 0x0F, // 166 - 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 - 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 - 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 - 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 - 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 - 0x80, 0x00, 0x80, // 173 - 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 - 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 - 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 - 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 - 0x48, 0x00, 0x68, 0x00, 0x58, // 178 - 0x48, 0x00, 0x58, 0x00, 0x68, // 179 - 0x00, 0x00, 0x10, 0x00, 0x08, // 180 - 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 - 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 - 0x00, 0x00, 0x40, // 183 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 - 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 - 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 - 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 - 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 - 0x00, 0x00, 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 - 0x00, 0x00, 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 - 0x00, 0x00, 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 - 0x00, 0x00, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 - 0x00, 0x00, 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 - 0x00, 0x00, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 - 0x00, 0x00, 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 - 0x00, 0x00, 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 - 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 - 0x00, 0x00, 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 - 0x00, 0x00, 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 - 0x00, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 - 0x00, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 - 0x00, 0x00, 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 - 0x00, 0x00, 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x03, // 224 - 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 - 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 - 0x00, 0x00, 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 - 0x00, 0x00, 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 - 0x00, 0x00, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x98, 0x00, 0x40, 0x00, 0xE0, 0x03, // 233 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 234 - 0x00, 0x00, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 235 - 0x00, 0x00, 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 - 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 240 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0x40, 0x02, // 241 - 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, // 242 - 0x00, 0x00, 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 - 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 - 0x00, 0x00, 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, // 246 - 0x00, 0x00, 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 - 0x00, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 250 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 - 0x00, 0x00, 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 - 0x00, 0x00, 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0xE0, 0x03, // 255 + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, + 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x48, 0x00, 0x68, 0x00, 0x58, // 178 + 0x48, 0x00, 0x58, 0x00, 0x68, // 179 + 0x00, 0x00, 0x10, 0x00, 0x08, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 + 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 + 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 + 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 + 0x00, 0x00, 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 + 0x00, 0x00, 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 + 0x00, 0x00, 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 + 0x00, 0x00, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 + 0x00, 0x00, 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 + 0x00, 0x00, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 + 0x00, 0x00, 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 + 0x00, 0x00, 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 + 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 + 0x00, 0x00, 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 + 0x00, 0x00, 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 + 0x00, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 + 0x00, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 + 0x00, 0x00, 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 + 0x00, 0x00, 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x03, // 224 + 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 + 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 + 0x00, 0x00, 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 + 0x00, 0x00, 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 + 0x00, 0x00, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x98, 0x00, 0x40, 0x00, 0xE0, 0x03, // 233 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 234 + 0x00, 0x00, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 235 + 0x00, 0x00, 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 + 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 240 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0x40, 0x02, // 241 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, // 242 + 0x00, 0x00, 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 + 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 + 0x00, 0x00, 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, // 246 + 0x00, 0x00, 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 + 0x00, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 250 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 + 0x00, 0x00, 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x00, 0x00, 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0xE0, 0x03, // 255 }; // Font generated or edited with the glyphEditor (@mrekin) @@ -661,327 +662,321 @@ const uint8_t ArialMT_Plain_16_RU[] PROGMEM = { // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 - 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, - 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 - 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00, - 0x30, 0x1C, // 36 - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, - 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, - 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 - 0x00, 0x00, 0x00, 0x78, // 39 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 - 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x02, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 - 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, - 0xE0, 0x1F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 - 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00, - 0xE0, 0x40, // 50 - 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, - 0x00, 0x1C, // 51 - 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x08, // 52 - 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, - 0x08, 0x1E, // 53 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, - 0x20, 0x1E, // 54 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, + 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00, 0x30, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, + 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, + 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00, 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, - 0x18, // 55 - 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, - 0x60, 0x1C, // 56 - 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, - 0xE0, 0x1F, // 57 - 0x00, 0x00, 0x00, 0x40, 0x40, // 58 - 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, - 0x40, 0x10, // 60 - 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, - 0x80, 0x08, // 61 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, - 0x00, 0x02, // 62 + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, - 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, - 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, // 70 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, - 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, // 76 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, - 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x5F, // 81 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, - 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 84 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, + 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, + 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, + 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, + 0x5F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, + 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, + 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, - 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x08, // 89 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, - 0x18, 0x40, 0x00, 0x08, 0x40, // 90 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 - 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 97 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xF8, 0x7F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 101 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, - 0xC0, 0xFF, // 103 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, - 0x40, 0x40, // 107 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 111 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xC0, 0xFF, 0x03, // 113 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 - 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 - 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 - 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x01, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, - 0x00, 0x11, // 162 - 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, - 0x20, 0x20, // 163 - 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 - 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, - 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 - 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 - 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, - 0x00, 0x0C, // 167 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, - 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 168 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, - 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 - 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, - 0x80, 0x0F, // 172 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, - 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 - 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x04, // 175 - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 - 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, // 177 - 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, - 0xC0, 0x7F, // 181 - 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0xF8, 0xFF, 0x03, 0x08, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00, - 0x00, 0x17, // 184 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 - 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, - 0x00, 0x04, // 187 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, - 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, - 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02, - 0x00, 0x00, 0x03, 0x00, 0x80, 0x01, // 191 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x66, 0x00, 0x08, 0x3C, // 193 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 194 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x18, // 195 - 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 197 - 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00, - 0x08, 0x40, // 198 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0xE0, 0x1D, // 199 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 200 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00, - 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 201 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00, - 0x08, 0x20, 0x00, 0x08, 0x40, // 202 - 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0xF8, 0x7F, // 203 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 206 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 208 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 209 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 210 - 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00, - 0xC0, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 211 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, - 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212 - 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, - 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, 0x40, // 213 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 214 - 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x04, 0x00, 0xF8, 0x7F, // 215 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, + 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, + 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, + 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, + 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, + 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, + 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, + 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, + 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, + 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, + 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, + 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, + 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 + 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, + 0x0B, // 164 + 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, + 0x08, 0x0A, // 165 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 168 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, + 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, + 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, // 175 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 + 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, + 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 184 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, + 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x20, // 188 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, + 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, + 0x4C, // 189 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, + 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x20, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02, 0x00, 0x00, 0x03, + 0x00, 0x80, 0x01, // 191 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x66, 0x00, + 0x08, 0x3C, // 193 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x90, 0x22, 0x00, 0x60, 0x1C, // 194 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x18, // 195 + 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 197 + 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00, 0x08, 0x40, // 198 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, + 0xE0, 0x1D, // 199 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, + 0x7F, // 200 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00, 0x01, 0x01, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, + 0x7F, // 201 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, + 0x08, 0x40, // 202 + 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0xF8, 0x7F, // 203 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 206 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x10, 0x01, 0x00, 0xE0, // 208 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 209 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 210 + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, + 0x60, 0x00, 0x00, 0x18, // 211 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, + 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212 + 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, + 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, + 0x40, // 213 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, + 0x01, // 214 + 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0xF8, 0x7F, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, + 0x7F, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 217 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x63, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x63, 0x00, 0x00, 0x3E, // 220 - 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x10, 0x61, 0x00, 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00, - 0x30, 0x30, 0x00, 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00, - 0x80, 0x0F, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 223 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 224 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00, - 0x18, 0x3F, 0x00, 0x00, 0x0C, // 225 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, 0x3B, // 226 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227 - 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, - 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 228 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 229 - 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, // 230 - 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, - 0xC0, 0x7F, // 232 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00, - 0xC0, 0x7F, // 233 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, 0x40, // 234 - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0xC0, 0x7F, // 235 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, - 0xC0, 0x07, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0xC0, 0x7F, // 237 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 238 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0xC0, 0x7F, // 239 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 240 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 241 - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 242 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 243 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03, - 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244 - 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, - 0x40, 0x40, // 245 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 246 - 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 247 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249 - 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x64, 0x00, 0x00, 0x38, // 250 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, - 0x00, 0x38, // 252 - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, 0x1F, // 253 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00, - 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, 0x1F, // 254 - 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00, - 0xC0, 0x7F, // 255 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, + 0x00, 0x3E, // 220 + 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x10, 0x61, 0x00, + 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00, 0x30, 0x30, 0x00, + 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00, 0x80, 0x0F, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, + 0x7F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00, 0x18, 0x3F, 0x00, + 0x00, 0x0C, // 225 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, + 0x3B, // 226 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227 + 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0xC0, 0x01, // 228 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 229 + 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, + 0x40, // 230 + 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x7F, // 232 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00, 0xC0, 0x7F, // 233 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, + 0x40, // 234 + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 235 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, 0xC0, 0x07, 0x00, + 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, // 237 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 238 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 239 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 240 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, + 0x20, // 241 + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0x40, // 242 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 243 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03, 0xC0, 0x60, 0x00, + 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244 + 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 245 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0xC0, // 246 + 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, + 0x7F, // 247 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249 + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, + 0x00, 0x38, // 250 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x38, // 252 + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, + 0x1F, // 253 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, + 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, + 0x1F, // 254 + 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00, 0xC0, 0x7F, // 255 }; // Font generated or edited with the glyphEditor (@mrekin) @@ -1216,553 +1211,506 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { 0x23, 0x83, 0x43, 0x12, // 254 0x23, 0xC6, 0x2B, 0x0D, // 255 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, - 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 - 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, - 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, - 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, - 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, - 0x1F, 0x00, 0x80, 0x81, 0x07, // 36 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, - 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, - 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20, - 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0, - 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, - 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, - 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, - 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, + 0x33, // 33 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, + 0xE0, 0x07, // 34 + 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, + 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, + 0x00, 0x0C, 0x03, // 35 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, + 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x80, 0x81, 0x07, // 36 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, + 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, + 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0, 0x3C, 0x30, 0x00, + 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, + 0x20, 0x00, 0x00, 0x04, // 40 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, + 0x00, 0xF0, 0x0F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, + 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, + 0x80, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, + 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, + 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x60, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, - 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, - 0x00, 0x33, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, - 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, - 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, - 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x03, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, - 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, - 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, - 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, - 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, - 0x80, 0x3F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, - 0x00, 0x00, 0x60, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7, - 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, - 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, - 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, - 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, - 0x03, 0x00, 0x00, 0x03, 0x06, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, - 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, - 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x20, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, - 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, - 0x00, 0x00, 0x00, 0x07, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, - 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, - 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, - 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, - 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, - 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, + 0x60, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, + 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, + 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, + 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, + 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, + 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, + 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, + 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, + 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, + 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, + 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, + 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, + 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, + 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, + 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, + 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, + 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, - 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, - 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, - 0x00, 0x00, 0xE2, 0x0F, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, - 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, + 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, + 0x0F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, + 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, - 0x00, 0xE0, 0xFF, 0x3F, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, - 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, - 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, - 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F, - 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, - 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, + 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, + 0x0F, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, + 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, + 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, - 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70, - 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85 - 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, - 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, - 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, - 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, - 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 - 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00, - 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, - 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, - 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 - 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, - 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, - 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, - 0xFF, 0xFF, 0x07, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, + 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85 + 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, + 0x20, // 86 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00, 0xCF, 0x03, 0x00, + 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, + 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, + 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, + 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, + 0x06, // 91 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, + 0x00, 0x00, 0x30, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, + 0x07, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, - 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, - 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, - 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, + 0x00, 0xF0, 0x03, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, - 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, - 0x06, 0x00, 0x00, 0x60, 0x06, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, - 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, - 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, - 0x30, 0x00, 0x00, 0x00, 0x20, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, - 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, - 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, - 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, + 0x60, 0x06, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, + 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, + 0x00, 0xF0, 0x03, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, + 0x07, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, - 0x00, 0x00, 0x0E, // 119 - 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, - 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, + 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x06, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, - 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, + 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, - 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, + 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, - 0x06, 0x3F, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, + 0x00, 0x80, 0x01, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0x60, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, + 0x07, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, + 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 - 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, - 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, - 0x38, 0x00, 0x00, 0x00, 0x10, // 163 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, - 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, - 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 - 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, - 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, - 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 + 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, + 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, + 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, + 0x00, 0x02, 0x04, // 164 + 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, + 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, + 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, - 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, - 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, - 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, - 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0, - 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, - 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, - 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, - 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, - 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, - 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, - 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, - 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, - 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, - 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, - 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 - 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, - 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, - 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x20, // 180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 - 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, - 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, - 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, + 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, + 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, + 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, + 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, + 0xC0, 0x3F, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, + 0x10, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x80, 0x01, // 173 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, + 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, + 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, // 175 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, + 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, + 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, + 0x00, 0x60, 0x30, // 177 + 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, + 0xC0, 0x21, // 178 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, + 0xC0, 0x1D, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x20, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 + 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, + 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, + 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00, - 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8, - 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, + 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8, 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, // 185 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, - 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, - 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x80, // 187 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, - 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, - 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, - 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, - 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, - 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09, - 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, - 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, - 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 195 - 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, - 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197 - 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00, - 0xCF, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, - 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, - 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00, - 0x60, 0x00, 0x20, // 198 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C, - 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, 0x00, 0xC7, 0x0F, // 199 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, - 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, - 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06, - 0x00, 0x0F, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E, - 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00, - 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, - 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, - 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, - 0x00, 0xE0, 0xFF, 0x3F, // 204 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, - 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, - 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, - 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 208 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, - 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, - 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, + 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, // 187 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, 0x3F, 0x1C, 0x00, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, + 0x00, 0xC0, 0x21, // 189 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, + 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, + 0x00, 0x00, 0x08, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, + 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, + 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, + 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, // 195 + 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197 + 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, + 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, + 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x20, // 198 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C, 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, + 0x00, 0xC7, 0x0F, // 199 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06, 0x00, 0x0F, 0x00, + 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 204 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, + 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, + 0x0F, // 208 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 209 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, - 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 210 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, - 0x78, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C, - 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x20, // 211 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, - 0x01, 0x06, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01, - 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01, - 0x00, 0x00, 0xF8, // 212 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, - 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, - 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, - 0x00, 0x00, 0x00, 0xF0, // 214 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, - 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, - 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, - 0x00, 0x00, 0xF0, 0x01, // 217 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, - 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, - 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, - 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, - 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, - 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 219 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, - 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, - 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, - 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, - 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, // 210 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x78, 0x30, 0x00, + 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x20, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, 0x01, 0x06, 0x00, + 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, + 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 212 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, + 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, + 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, // 214 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, // 217 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, + 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, + 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, + 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0x3F, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, + 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, + 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, + 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x03, // 221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03, - 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, - 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, - 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0, - 0xB8, 0x0F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, - 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, - 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60, - 0x0C, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78, - 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, 0x10, 0xC0, 0x03, // 225 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, - 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78, - 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227 - 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00, - 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0xF0, // 228 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, - 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229 - 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, - 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03, 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, + 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, + 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0, 0xB8, 0x0F, 0x00, + 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60, 0x0C, 0x30, 0x00, + 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78, 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, + 0x10, 0xC0, 0x03, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, + 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78, 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227 + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, + 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, + 0x00, 0x00, 0xF0, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229 + 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, + 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04, 0x20, // 230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, - 0x86, 0x20, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, 0x1F, // 231 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80, - 0x00, 0x0F, 0x00, 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, 0x30, // 234 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, - 0x78, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, - 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, - 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, 0x86, 0x20, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, + 0x1F, // 231 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80, 0x00, 0x0F, 0x00, + 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, + 0x30, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 239 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, - 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, - 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 240 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, + 0x00, 0xF0, 0x03, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 241 - 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, - 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, + 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 242 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, - 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, + 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x06, // 243 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, - 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF, - 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, - 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 - 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, + 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, + 0x00, 0xF0, 0x07, // 244 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 245 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 246 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, + 0x00, 0x00, 0xF0, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 247 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, - 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, - 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, - 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, - 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, - 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, - 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, - 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, - 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, + 0x00, 0x00, 0xF0, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, + 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, + 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, + 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, + 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, + 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 253 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, - 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F, - 0x00, 0x00, 0xE0, 0x07, // 254 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00, - 0x84, 0x03, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xE0, 0x07, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00, 0x84, 0x03, 0x00, + 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 255 }; diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp index 8bc56ea94..25b390ebf 100644 --- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -234,195 +234,196 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = { 0x06, 0xD8, 0x0A, 0x06, // 254 0x06, 0xE2, 0x08, 0x05, // 255 // Font Data: - 0x00, 0x00, 0xF8, 0x02, // 33 - 0x38, 0x00, 0x00, 0x00, 0x38, // 34 - 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 - 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 - 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 - 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 - 0x38, // 39 - 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 - 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 - 0x28, 0x00, 0x18, 0x00, 0x28, // 42 - 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 - 0x00, 0x00, 0x00, 0x06, // 44 - 0x80, 0x00, 0x80, // 45 - 0x00, 0x00, 0x00, 0x02, // 46 - 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 - 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 - 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 - 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 - 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 - 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 - 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 - 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 - 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 - 0x00, 0x00, 0x20, 0x02, // 58 - 0x00, 0x00, 0x20, 0x06, // 59 - 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 - 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 - 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 - 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 - 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 - 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 - 0x00, 0x00, 0xF8, 0x03, // 73 - 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 - 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 - 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 - 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 - 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 - 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 - 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 - 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 - 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 - 0x08, 0x08, 0xF8, 0x0F, // 93 - 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 - 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 - 0x08, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 - 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 - 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 - 0x00, 0x00, 0xE8, 0x03, // 105 - 0x00, 0x08, 0xE8, 0x07, // 106 - 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 - 0x00, 0x00, 0xF8, 0x03, // 108 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 - 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 - 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 - 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 - 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 - 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 - 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 - 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 - 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 - 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 - 0x00, 0x00, 0xF8, 0x0F, // 124 - 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 - 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 - 0x00, 0x00, 0xA0, 0x0F, // 161 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 - 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 - 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x0C, // 165 - 0x00, 0x00, 0x38, 0x0F, // 166 - 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 - 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 - 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 - 0x00, 0x00, 0xF0, 0x01, 0x58, 0x03, 0x48, 0x02, 0x08, 0x02, 0x10, 0x01, // 170 - 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 - 0x80, 0x00, 0x80, // 173 - 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 - 0x00, 0x00, 0x08, 0x02, 0x0A, 0x02, 0xF8, 0x03, 0x0A, 0x02, 0x08, 0x02, // 175 - 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 - 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 - 0x00, 0x00, 0x08, 0x02, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, // 178 - 0x00, 0x00, 0x20, 0x02, 0x20, 0x02, 0xE8, 0x03, 0x20, 0x02, 0x20, 0x02, // 179 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x30, // 180 - 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 - 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 - 0x00, 0x00, 0x40, // 183 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 - 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 - 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x10, 0x02, 0x20, 0x01, // 186 - 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 - 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 - 0x28, 0x02, 0xE0, 0x03, 0x28, 0x02, // 191 - 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 - 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 - 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 - 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 - 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 - 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 - 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 - 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 - 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 - 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 - 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 - 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 - 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 - 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 - 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 - 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 - 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 - 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 - 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 - 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 - 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 - 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 - 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 - 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 - 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 - 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 - 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 - 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 - 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 - 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x01, // 224 - 0xE0, 0x01, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 - 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 - 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 - 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 - 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 - 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 - 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 - 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 - 0xE0, 0x03, 0x08, 0x01, 0x88, 0x00, 0x48, 0x00, 0xE0, 0x03, // 233 - 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 234 - 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xE0, 0x03, // 235 - 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 - 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 - 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 - 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 - 0xE0, 0x03, 0xA0, 0x00, 0x40, // 240 - 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 241 - 0x20, 0x00, 0xE0, 0x03, 0x20, // 242 - 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 - 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 - 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 246 - 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 - 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 - 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 - 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, // 250 - 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 - 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 - 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 - 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 - 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xE0, 0x03, // 255 + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, + 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x0C, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x00, 0x00, 0xF0, 0x01, 0x58, 0x03, 0x48, 0x02, 0x08, 0x02, 0x10, 0x01, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x00, 0x00, 0x08, 0x02, 0x0A, 0x02, 0xF8, 0x03, 0x0A, 0x02, 0x08, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x00, 0x00, 0x08, 0x02, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, // 178 + 0x00, 0x00, 0x20, 0x02, 0x20, 0x02, 0xE8, 0x03, 0x20, 0x02, 0x20, 0x02, // 179 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x30, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 + 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 + 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x10, 0x02, 0x20, 0x01, // 186 + 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x28, 0x02, 0xE0, 0x03, 0x28, 0x02, // 191 + 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 + 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 + 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 + 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 + 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 + 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 + 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 + 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 + 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 + 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 + 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 + 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 + 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 + 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 + 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 + 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 + 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 + 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 + 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 + 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 + 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 + 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 + 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 + 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 + 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 + 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 + 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 + 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 + 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 + 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x01, // 224 + 0xE0, 0x01, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 + 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 + 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 + 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 + 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 + 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 + 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 + 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 + 0xE0, 0x03, 0x08, 0x01, 0x88, 0x00, 0x48, 0x00, 0xE0, 0x03, // 233 + 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 234 + 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xE0, 0x03, // 235 + 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 + 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 + 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 + 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 + 0xE0, 0x03, 0xA0, 0x00, 0x40, // 240 + 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 241 + 0x20, 0x00, 0xE0, 0x03, 0x20, // 242 + 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 + 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 + 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 246 + 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 + 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 + 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 + 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, // 250 + 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 + 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 + 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 + 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xE0, 0x03, // 255 }; const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { @@ -658,189 +659,181 @@ const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 - 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, - 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 - 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x43, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x18, 0x22, 0x00, - 0x20, 0x1C, // 36 - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, - 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, - 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 - 0x00, 0x00, 0x00, 0x78, // 39 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 - 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x02, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 - 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, - 0xE0, 0x1F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 - 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, - 0xE0, 0x40, // 50 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, - 0x00, 0x1C, // 51 - 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x08, // 52 - 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, - 0x08, 0x1E, // 53 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, - 0x20, 0x1E, // 54 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, + 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x43, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x18, 0x22, 0x00, 0x20, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, + 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, + 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, - 0x18, // 55 - 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, - 0x60, 0x1C, // 56 - 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, - 0xE0, 0x1F, // 57 - 0x00, 0x00, 0x00, 0x40, 0x40, // 58 - 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, - 0x40, 0x10, // 60 - 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, - 0x80, 0x08, // 61 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, - 0x00, 0x02, // 62 + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, - 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, - 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, // 70 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, - 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, // 76 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, - 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x0E, 0x00, - 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 84 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, + 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, + 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, + 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, + 0x4F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x1A, 0x00, + 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, + 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, - 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x08, // 89 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, - 0x18, 0x40, 0x00, 0x08, 0x40, // 90 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 - 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 97 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xF8, 0x7F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 101 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, - 0xC0, 0xFF, // 103 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, - 0x40, 0x40, // 107 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 111 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0xC0, 0xFF, 0x03, // 113 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 - 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 - 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 - 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x01, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, - 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0xE0, 0x7F, // 127 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1026 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 1027 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, + 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, + 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, + 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, + 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, + 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, + 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, + 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, + 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, + 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, + 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, + 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, + 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, + 0x20, 0x40, 0x00, 0xE0, 0x7F, // 127 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, + 0x3E, // 1026 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 1027 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 8218 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x40, // 1107 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 8222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 8230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, // 8230 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0x40, // 8224 - 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, - 0x40, 0x40, // 8225 - 0x00, 0x05, 0x00, 0xC0, 0x1F, 0x00, 0x20, 0x25, 0x00, 0x10, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, - 0x10, 0x20, // 8364 - 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x71, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x3C, 0x00, - 0x18, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x42, 0x00, - 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 8240 - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x3E, // 1033 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 8249 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x3E, // 1034 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x82, 0x02, 0x00, 0x61, 0x04, 0x00, 0x10, 0x08, 0x00, - 0x08, 0x30, 0x00, 0x08, 0x40, // 1036 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, - 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7E, // 1035 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1039 + 0x40, // 8224 + 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 8225 + 0x00, 0x05, 0x00, 0xC0, 0x1F, 0x00, 0x20, 0x25, 0x00, 0x10, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x10, 0x20, // 8364 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x71, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x3C, 0x00, 0x18, 0x42, 0x00, + 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, + 0x00, 0x3C, // 8240 + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1033 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 8249 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1034 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x82, 0x02, 0x00, 0x61, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, + 0x08, 0x40, // 1036 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7E, // 1035 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1039 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x80, 0xFF, 0x03, // 1106 0x00, 0x00, 0x00, 0x38, // 8216 @@ -848,157 +841,157 @@ const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, // 8220 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, // 8221 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, // 8226 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x04, // 8211 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x04, // 8212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, - 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0xE0, 0x7F, // 65533 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x01, 0x00, 0xE0, 0x00, 0x00, 0x18, 0x00, 0x00, - 0xF8, 0x01, // 8482 - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, // 8211 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 8212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, + 0x20, 0x40, 0x00, 0xE0, 0x7F, // 65533 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0x01, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x01, 0x00, 0xE0, 0x00, 0x00, 0x18, 0x00, 0x00, 0xF8, 0x01, // 8482 + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, 0x00, 0x38, // 1113 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 8250 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1114 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1114 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x10, 0x04, 0x00, 0x08, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1116 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 1115 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1119 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x81, 0x41, 0x00, 0x02, 0x66, 0x00, 0x02, 0x18, 0x00, 0x02, 0x06, 0x00, - 0x81, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 1038 - 0xC0, 0x01, 0x00, 0x08, 0x06, 0x02, 0x10, 0x38, 0x02, 0x10, 0xC0, 0x01, 0x10, 0x38, 0x00, 0x08, 0x07, 0x00, 0xC0, // 1118 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x81, 0x41, 0x00, 0x02, 0x66, 0x00, 0x02, 0x18, 0x00, 0x02, 0x06, 0x00, 0x81, 0x01, 0x00, + 0x60, 0x00, 0x00, 0x18, // 1038 + 0xC0, 0x01, 0x00, 0x08, 0x06, 0x02, 0x10, 0x38, 0x02, 0x10, 0xC0, 0x01, 0x10, 0x38, 0x00, 0x08, 0x07, 0x00, + 0xC0, // 1118 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 1032 - 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, + 0x0B, // 164 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x0F, // 1168 - 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 - 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, - 0x00, 0x0C, // 167 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 1025 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, - 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x11, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 1028 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, - 0x80, 0x0F, // 172 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, - 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 - 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 1031 - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 - 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, // 177 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1030 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 1110 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x78, // 1169 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, - 0xC0, 0x7F, // 181 - 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0xF8, 0xFF, 0x03, 0x08, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 1105 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x08, 0x00, - 0x00, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x27, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, - 0x40, 0x28, 0x00, 0x80, 0x27, // 8470 + 0x0F, // 1168 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 1025 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x11, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 1028 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, + 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, + 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 1031 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1030 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 1110 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x78, // 1169 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, + 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 1105 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x27, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x80, 0x27, // 8470 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x20, 0x00, 0x00, - 0x11, // 1108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, - 0x00, 0x04, // 187 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 1112 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, - 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 1029 + 0x11, // 1108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 1112 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0x20, 0x1C, // 1029 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 1109 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 1111 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 1040 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x00, 0x3E, // 1041 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 1042 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 1043 - 0x00, 0xC0, 0x03, 0x00, 0x60, 0x00, 0x00, 0x5C, 0x00, 0xF8, 0x43, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1044 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 1045 - 0x08, 0x30, 0x00, 0x10, 0x08, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, // 1040 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x00, 0x3E, // 1041 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x90, 0x22, 0x00, 0x60, 0x1C, // 1042 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 1043 + 0x00, 0xC0, 0x03, 0x00, 0x60, 0x00, 0x00, 0x5C, 0x00, 0xF8, 0x43, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1044 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x40, // 1045 + 0x08, 0x30, 0x00, 0x10, 0x08, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, 0x40, // 1046 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x42, 0x00, - 0x70, 0x42, 0x00, 0x00, 0x3C, // 1047 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1048 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x01, 0x04, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, - 0x82, 0x00, 0x00, 0x41, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1049 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, - 0x08, 0x30, 0x00, 0x08, 0x40, // 1050 - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1051 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1052 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 1053 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 1054 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1055 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 1056 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 1057 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x08, // 1058 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x80, 0x41, 0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, - 0x80, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 1059 - 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x40, 0x08, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, - 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 1060 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, - 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 1061 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1062 - 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x02, 0x00, 0xF8, 0x7F, // 1063 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1064 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1065 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1066 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1067 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1068 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x10, 0x21, 0x00, 0x20, 0x11, 0x00, 0xC0, 0x0F, // 1069 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, - 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, - 0xC0, 0x0F, // 1070 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40, 0x00, 0x10, 0x21, 0x00, 0x08, 0x1A, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 1071 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, - 0x80, 0x7F, // 1072 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x90, 0x20, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x88, 0x20, 0x00, - 0x08, 0x1F, // 1073 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, - 0x80, 0x3B, // 1074 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1075 - 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0xC0, 0x4F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0xC0, 0x01, // 1076 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, - 0x00, 0x17, // 1077 - 0xC0, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1078 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x42, 0x00, 0x70, 0x42, 0x00, + 0x00, 0x3C, // 1047 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, + 0x7F, // 1048 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x01, 0x04, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x82, 0x00, 0x00, + 0x41, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, + 0x7F, // 1049 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, + 0x08, 0x40, // 1050 + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1051 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1052 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0xF8, 0x7F, // 1053 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, + 0x0F, // 1054 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1055 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x10, 0x01, 0x00, 0xE0, // 1056 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, + 0x10, // 1057 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, // 1058 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x80, 0x41, 0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x01, 0x00, + 0x60, 0x00, 0x00, 0x18, // 1059 + 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x40, 0x08, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, + 0x40, 0x08, 0x00, 0x80, 0x07, // 1060 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, + 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, + 0x40, // 1061 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, + 0x03, // 1062 + 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, + 0xF8, 0x7F, // 1063 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1064 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, + 0x03, // 1065 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1066 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, + 0x7F, // 1067 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x3E, // 1068 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x10, 0x21, 0x00, 0x20, 0x11, 0x00, 0xC0, + 0x0F, // 1069 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 1070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40, 0x00, 0x10, 0x21, 0x00, 0x08, 0x1A, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, + 0x7F, // 1071 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 1072 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x90, 0x20, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x88, 0x20, 0x00, 0x08, 0x1F, // 1073 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 1074 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1075 + 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0xC0, 0x4F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, + 0x01, // 1076 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 1077 + 0xC0, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, + 0xC0, 0x20, 0x00, 0x40, 0x40, // 1078 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 1079 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, 0xC0, 0x7F, // 1080 @@ -1008,44 +1001,45 @@ const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { 0x40, // 1082 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 1083 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0xC0, 0x7F, // 1084 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, + 0xC0, 0x01, 0x00, 0xC0, 0x7F, // 1084 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, - 0x7F, // 1085 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 1086 + 0x7F, // 1085 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1086 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, - 0x7F, // 1087 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, - 0x00, 0x1F, // 1088 + 0x7F, // 1087 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1088 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, - 0x20, // 1089 - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1090 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 1091 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0xFF, 0x03, - 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1092 + 0x20, // 1089 + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0x40, // 1090 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, + 0xC0, // 1091 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, + 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1092 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 1093 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1094 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0xC0, 0x03, // 1094 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 1095 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1096 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1097 - 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x44, 0x00, 0x00, 0x38, // 1098 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 1099 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x38, // 1100 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1096 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, + 0x03, // 1097 + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x38, // 1098 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0x7F, // 1099 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1100 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x1F, // 1101 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, - 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1102 - 0x00, 0x00, 0x00, 0x80, 0x63, 0x00, 0x40, 0x14, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, - 0xC0, 0x7F, // 1103 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, + 0x1F, // 1102 + 0x00, 0x00, 0x00, 0x80, 0x63, 0x00, 0x40, 0x14, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x7F, // 1103 }; const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { @@ -1279,648 +1273,597 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0x29, 0xDD, 0x43, 0x12, // 1102=�.�.:10717 0x2A, 0x20, 0x2B, 0x0D, // 1103=�.�.:10784 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xCF, 0x00, 0x80, 0xFF, 0xCF, // 33 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, - 0x1F, 0x00, 0x00, 0x80, 0x1F, // 34 - 0x00, 0x30, 0x0C, 0x00, 0x00, 0x30, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x80, - 0x3F, 0x0C, 0x00, 0x80, 0x31, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0D, 0x00, 0x80, 0x3F, - 0x0C, 0x00, 0x80, 0x31, 0x0C, 0x00, 0x00, 0x30, 0x0C, // 35 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x18, 0x00, 0x00, 0x3F, 0x78, 0x00, 0x00, 0x63, 0x70, 0x00, 0x80, 0x61, 0xE0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x07, - 0x7F, 0x00, 0x00, 0x04, 0x1E, // 36 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, - 0x80, 0x80, 0x00, 0x80, 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0x78, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, - 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x0F, 0x7F, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x80, 0x80, 0x80, - 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x8E, 0x73, 0x00, 0x00, 0xDF, 0xE1, 0x00, 0x80, - 0xF3, 0xC0, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0xE1, 0xC1, 0x00, 0x80, 0xB3, 0xE3, 0x00, 0x00, 0x3F, 0x6E, 0x00, 0x00, 0x0E, - 0x7C, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xE2, 0x00, 0x00, 0x00, 0x40, // 38 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x80, - 0x01, 0x00, 0x18, 0x80, 0x00, 0x00, 0x10, // 40 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x80, 0x01, 0x00, 0x18, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x3E, 0xC0, 0x07, 0x00, - 0xF8, 0xFF, 0x01, 0x00, 0xC0, 0x3F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, - 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xCF, 0x00, 0x80, 0xFF, + 0xCF, // 33 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, + 0x80, 0x1F, // 34 + 0x00, 0x30, 0x0C, 0x00, 0x00, 0x30, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x80, 0x3F, 0x0C, 0x00, + 0x80, 0x31, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0D, 0x00, 0x80, 0x3F, 0x0C, 0x00, 0x80, 0x31, 0x0C, 0x00, + 0x00, 0x30, 0x0C, // 35 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x18, 0x00, 0x00, 0x3F, 0x78, 0x00, 0x00, 0x63, 0x70, 0x00, 0x80, 0x61, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0xC0, 0xFF, 0xFF, 0x03, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x07, 0x7F, 0x00, 0x00, 0x04, 0x1E, // 36 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, + 0x80, 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0x78, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x0F, 0x7F, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, + 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x8E, 0x73, 0x00, 0x00, 0xDF, 0xE1, 0x00, 0x80, 0xF3, 0xC0, 0x00, + 0x80, 0xE1, 0xC0, 0x00, 0x80, 0xE1, 0xC1, 0x00, 0x80, 0xB3, 0xE3, 0x00, 0x00, 0x3F, 0x6E, 0x00, 0x00, 0x0E, 0x7C, 0x00, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xE2, 0x00, 0x00, 0x00, 0x40, // 38 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x80, 0x01, 0x00, 0x18, + 0x80, 0x00, 0x00, 0x10, // 40 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x80, 0x01, 0x00, 0x18, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0xF8, 0xFF, 0x01, + 0x00, 0xC0, 0x3F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x80, 0x01, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 44 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 45 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 46 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, - 0x0F, 0x00, 0x00, 0x80, 0x01, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x07, 0x70, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, - 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xF8, 0x0F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, - 0x01, 0xCC, 0x00, 0x80, 0x01, 0xC6, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0xC3, 0xC0, 0x00, 0x00, 0x7F, - 0xC0, 0x00, 0x00, 0x3C, 0xC0, // 50 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x18, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x03, 0x70, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, - 0x3F, 0x00, 0x00, 0x00, 0x1E, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF0, 0x0C, 0x00, 0x00, - 0x38, 0x0C, 0x00, 0x00, 0x1E, 0x0C, 0x00, 0x00, 0x07, 0x0C, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x0C, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x00, 0xFE, 0x38, 0x00, 0x80, 0x7F, 0x60, 0x00, 0x80, 0x21, 0xC0, 0x00, 0x80, - 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x61, 0x70, 0x00, 0x80, 0xC1, - 0x3F, 0x00, 0x00, 0x80, 0x0F, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x8F, 0x71, 0x00, 0x00, 0xC3, 0xE0, 0x00, 0x80, - 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0xC3, 0x60, 0x00, 0x00, 0xC7, - 0x3F, 0x00, 0x00, 0x06, 0x1F, // 54 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, - 0x01, 0xFE, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xE1, 0x01, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x0D, 0x00, 0x00, 0x80, 0x07, - 0x00, 0x00, 0x80, 0x01, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, - 0x7F, 0x00, 0x00, 0x00, 0x1E, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x30, 0x00, 0x00, 0xFE, 0x71, 0x00, 0x00, 0x87, 0xE1, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, - 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0xC7, 0x78, 0x00, 0x00, 0xFE, - 0x3F, 0x00, 0x00, 0xF8, 0x07, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x0C, 0x00, 0x18, 0xC0, 0x07, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, - 0x60, 0x03, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, - 0x0C, 0x00, 0x00, 0x0C, 0x18, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, - 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, - 0x06, 0x00, 0x00, 0x30, 0x06, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, - 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xC0, - 0x01, 0x00, 0x00, 0x80, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, - 0x01, 0xCE, 0x00, 0x80, 0x01, 0xCF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0x7F, - 0x00, 0x00, 0x00, 0x1C, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x78, 0xC0, 0x03, 0x00, 0x1C, 0x00, 0x07, 0x00, - 0x0E, 0x1F, 0x06, 0x00, 0xC7, 0x7F, 0x0E, 0x00, 0xE3, 0x60, 0x0C, 0x00, 0x33, 0xC0, 0x0C, 0x80, 0x39, 0xC0, 0x18, 0x80, 0x19, - 0xC0, 0x18, 0x80, 0x19, 0x60, 0x18, 0x80, 0x19, 0x30, 0x18, 0x80, 0x31, 0x78, 0x18, 0x80, 0xE1, 0xFF, 0x18, 0x80, 0xFB, 0xC7, - 0x18, 0x00, 0x3B, 0xC0, 0x18, 0x00, 0x07, 0x60, 0x0C, 0x00, 0x0E, 0x70, 0x0C, 0x00, 0x1C, 0x3C, 0x06, 0x00, 0xF8, 0x1F, 0x06, - 0x00, 0xE0, 0x07, 0x03, 0x00, 0x00, 0x00, 0x01, // 64 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, - 0x3E, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, - 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, + 0x80, 0x01, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x07, 0x70, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xF8, 0x0F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x01, 0xCC, 0x00, + 0x80, 0x01, 0xC6, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0xC3, 0xC0, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x00, 0x3C, 0xC0, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x18, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x03, 0x70, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x38, 0x0C, 0x00, + 0x00, 0x1E, 0x0C, 0x00, 0x00, 0x07, 0x0C, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x00, 0xFE, 0x38, 0x00, 0x80, 0x7F, 0x60, 0x00, 0x80, 0x21, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, + 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x61, 0x70, 0x00, 0x80, 0xC1, 0x3F, 0x00, 0x00, 0x80, 0x0F, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x8F, 0x71, 0x00, 0x00, 0xC3, 0xE0, 0x00, 0x80, 0x61, 0xC0, 0x00, + 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0xC3, 0x60, 0x00, 0x00, 0xC7, 0x3F, 0x00, 0x00, 0x06, 0x1F, // 54 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xFE, 0x00, + 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xE1, 0x01, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x30, 0x00, 0x00, 0xFE, 0x71, 0x00, 0x00, 0x87, 0xE1, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, + 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0xC7, 0x78, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xF8, 0x07, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x0C, 0x00, 0x18, 0xC0, 0x07, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, + 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, + 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x30, 0x06, 0x00, + 0x00, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0xCE, 0x00, + 0x80, 0x01, 0xCF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x1C, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x78, 0xC0, 0x03, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x0E, 0x1F, 0x06, + 0x00, 0xC7, 0x7F, 0x0E, 0x00, 0xE3, 0x60, 0x0C, 0x00, 0x33, 0xC0, 0x0C, 0x80, 0x39, 0xC0, 0x18, 0x80, 0x19, 0xC0, 0x18, 0x80, 0x19, 0x60, 0x18, + 0x80, 0x19, 0x30, 0x18, 0x80, 0x31, 0x78, 0x18, 0x80, 0xE1, 0xFF, 0x18, 0x80, 0xFB, 0xC7, 0x18, 0x00, 0x3B, 0xC0, 0x18, 0x00, 0x07, 0x60, 0x0C, + 0x00, 0x0E, 0x70, 0x0C, 0x00, 0x1C, 0x3C, 0x06, 0x00, 0xF8, 0x1F, 0x06, 0x00, 0xE0, 0x07, 0x03, 0x00, 0x00, 0x00, 0x01, // 64 + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x3E, 0x06, 0x00, + 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, + 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, 0x0C, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, - 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0x60, 0x00, + 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, - 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, - 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x01, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x81, - 0xC1, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x87, 0x61, 0x00, 0x00, 0x8E, 0x3F, - 0x00, 0x00, 0x88, 0x3F, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x9C, 0x07, 0x00, 0x00, 0x0E, - 0x1E, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x80, 0x03, 0x78, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, + 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, + 0x80, 0xC1, 0x00, 0x00, 0x80, 0x01, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x80, 0x81, 0xC1, 0x00, + 0x80, 0x83, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x87, 0x61, 0x00, 0x00, 0x8E, 0x3F, 0x00, 0x00, 0x88, 0x3F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, + 0x3F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x9C, 0x07, 0x00, 0x00, 0x0E, 0x1E, 0x00, 0x00, 0x07, 0x3C, 0x00, + 0x80, 0x03, 0x78, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, - 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, - 0x00, 0x80, 0xFF, 0xFF, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, - 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, - 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, - 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x3C, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x30, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xD8, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x03, 0xF0, 0x00, 0x00, 0x03, 0x70, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0xFC, - 0x00, 0x00, 0xFC, 0xDF, 0x01, 0x00, 0xF0, 0x87, 0x01, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, - 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, - 0x0F, 0x00, 0x80, 0xC1, 0x1E, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, + 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, + 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, + 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x3C, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x30, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x01, 0xD8, 0x00, + 0x80, 0x03, 0xF0, 0x00, 0x00, 0x03, 0x70, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0xFC, 0xDF, 0x01, 0x00, 0xF0, 0x87, + 0x01, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, + 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x1E, 0x00, + 0x80, 0x63, 0x7C, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x00, 0x80, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, - 0xE1, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 83 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, // 85 - 0x80, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, - 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, - 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, // 86 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, - 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xFE, 0x03, 0x00, - 0x80, 0x1F, 0x00, 0x00, 0x80, 0x01, // 87 - 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, - 0x3C, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, - 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 88 - 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, - 0x78, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x78, - 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, // 89 - 0x00, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xDC, 0x00, 0x80, 0x01, 0xCE, 0x00, 0x80, - 0x01, 0xC7, 0x00, 0x80, 0x81, 0xC3, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0x71, 0xC0, 0x00, 0x80, 0x39, 0xC0, 0x00, 0x80, 0x1D, - 0xC0, 0x00, 0x80, 0x07, 0xC0, 0x00, 0x80, 0x03, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 90 - 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, // 91 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xFF, 0x1F, 0x80, - 0xFF, 0xFF, 0x1F, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, - 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, 0xE1, 0xE0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, + 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 83 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, // 85 + 0x80, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, + 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, + 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, + 0x80, // 86 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xFE, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x01, // 87 + 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, 0x3C, 0x0F, 0x00, + 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, + 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 88 + 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, // 89 + 0x00, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xDC, 0x00, 0x80, 0x01, 0xCE, 0x00, 0x80, 0x01, 0xC7, 0x00, + 0x80, 0x81, 0xC3, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0x71, 0xC0, 0x00, 0x80, 0x39, 0xC0, 0x00, 0x80, 0x1D, 0xC0, 0x00, 0x80, 0x07, 0xC0, 0x00, + 0x80, 0x03, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 90 + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, + 0x18, // 91 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, + 0x00, 0x00, 0xC0, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, + 0x1F, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 94 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 95 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x18, // 95 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x02, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, - 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, - 0xFF, 0x00, 0x00, 0x00, 0x80, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, - 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x80, 0x0F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC6, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0x80, 0x0F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, 0x30, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x70, 0x30, 0x00, 0x80, 0xFF, - 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, - 0x33, 0x00, 0x00, 0xC0, 0x13, // 101 - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, - 0x19, 0x00, 0x00, 0x80, 0x19, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x06, 0x00, 0xE0, 0x3F, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x00, 0x38, 0xE0, 0x18, 0x00, - 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x30, 0x60, 0x1C, 0x00, 0x60, 0x30, 0x0E, 0x00, 0xF8, - 0xFF, 0x07, 0x00, 0xF8, 0xFF, 0x03, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, - 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 105 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x80, 0x03, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x60, 0x1E, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x08, - 0xC0, 0x00, 0x00, 0x00, 0x80, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, - 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, - 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, - 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, - 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x0F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xF8, - 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x70, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 101 + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, + 0x80, 0x19, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x06, 0x00, 0xE0, 0x3F, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x00, 0x38, 0xE0, 0x18, 0x00, 0x18, 0xC0, 0x18, + 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x30, 0x60, 0x1C, 0x00, 0x60, 0x30, 0x0E, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xF8, 0xFF, + 0x03, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 105 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, + 0x00, 0xC0, 0x07, 0x00, 0x00, 0x60, 0x1E, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x08, 0xC0, 0x00, 0x00, 0x00, 0x80, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0xC0, 0x0F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, + 0x1F, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, 0x3C, // 115 - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 117 - 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 117 + 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 118 - 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x80, - 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, - 0x00, 0x00, 0x38, // 119 - 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, + 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x38, // 119 + 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, 0x80, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, - 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, 0x00, 0xF8, 0x0F, + 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x18, 0xF8, 0x00, 0x00, 0x18, 0xDC, 0x00, 0x00, - 0x18, 0xCF, 0x00, 0x00, 0x98, 0xC3, 0x00, 0x00, 0xD8, 0xC1, 0x00, 0x00, 0xF8, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x18, 0xF8, 0x00, 0x00, 0x18, 0xDC, 0x00, 0x00, 0x18, 0xCF, 0x00, + 0x00, 0x98, 0xC3, 0x00, 0x00, 0xD8, 0xC1, 0x00, 0x00, 0xF8, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xF9, 0x0F, 0x80, 0xFF, 0xF0, 0x1F, 0x80, - 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xF9, 0x0F, 0x80, 0xFF, 0xF0, 0x1F, 0x80, 0x01, 0x00, 0x18, + 0x80, 0x01, 0x00, 0x18, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFF, 0x3F, // 124 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xF0, 0x1F, 0x00, 0xFF, 0xFD, 0x0F, 0x00, - 0x00, 0x0F, 0x00, 0x00, 0x00, 0x06, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, // 126 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, - 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x60, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x00, 0x60, 0xC0, - 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x1F, // 1026 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0xA0, 0x01, 0x00, 0x00, 0xB8, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1027 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xF0, 0x1F, 0x00, 0xFF, 0xFD, 0x0F, 0x00, 0x00, 0x0F, 0x00, + 0x00, 0x00, 0x06, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, + 0x00, 0x80, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, + 0x80, 0x61, 0x60, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x00, 0x60, 0xC0, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0xC0, 0x71, 0x00, + 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x1F, // 1026 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0xA0, 0x01, 0x00, 0x00, 0xB8, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, // 1027 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 8218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x80, - 0x1B, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0x00, 0x18, // 1107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 8222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 8230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 8224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, - 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 8225 - 0x00, 0x30, 0x03, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x36, 0x3B, 0x00, 0x00, 0x33, 0x73, 0x00, 0x00, - 0x33, 0x63, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, - 0xC0, 0x00, 0x00, 0x03, 0x60, // 8364 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, - 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, - 0x3E, 0x00, 0x80, 0x0F, 0x7F, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, - 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, - 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 8240 - 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, - 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1033 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, - 0x10, 0x40, // 8249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, - 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, - 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1034 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x20, 0x60, 0x03, 0x00, 0x38, 0x78, 0x07, 0x00, 0x18, 0x1E, 0x0E, 0x00, 0x08, 0x07, - 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1036 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, - 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0x60, 0x00, - 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xFF, // 1035 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1039 - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x18, 0x00, 0xF0, - 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 1106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x80, 0x1B, 0x00, 0x00, + 0x80, 0x19, 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0x00, 0x18, // 1107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, + 0x00, 0x00, 0xC0, 0x07, // 8222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, // 8230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, // 8224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, // 8225 + 0x00, 0x30, 0x03, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x36, 0x3B, 0x00, 0x00, 0x33, 0x73, 0x00, 0x00, 0x33, 0x63, 0x00, + 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x00, 0x03, 0x60, // 8364 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0xE0, 0x00, + 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x80, 0x0F, 0x7F, 0x00, + 0x80, 0x81, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, + 0x00, 0x00, 0x3E, // 8240 + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, + 0x1E, // 1033 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x40, // 8249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1034 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x01, 0x00, 0x20, 0x60, 0x03, 0x00, 0x38, 0x78, 0x07, 0x00, 0x18, 0x1E, 0x0E, 0x00, 0x08, 0x07, 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1036 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, + 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xFF, // 1035 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1039 + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, + 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x18, 0x00, 0xF0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, + 0x0F, // 1106 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, // 8216 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, // 8217 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1F, 0x00, 0x00, 0x80, 0x19, // 8220 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, - 0x19, 0x00, 0x00, 0x80, 0x0F, // 8221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, - 0xE0, 0x07, 0x00, 0x00, 0xC0, 0x03, // 8226 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 8211 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 8212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1E, 0x00, - 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, - 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, // 8482 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, - 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x80, 0x19, // 8220 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, + 0x80, 0x0F, // 8221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, + 0x00, 0xC0, 0x03, // 8226 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, // 8211 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x06, // 8212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0xF0, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, // 8482 + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, + 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, + 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, - 0x80, 0x0F, 0x00, 0x00, 0x00, 0x02, // 8250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, - 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x02, 0x02, 0x00, 0x80, - 0x03, 0x07, 0x00, 0x80, 0xC1, 0x0D, 0x00, 0x80, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, + 0x00, 0x00, 0x02, // 8250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, + 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0x3C, // 1114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x02, 0x02, 0x00, 0x80, 0x03, 0x07, 0x00, + 0x80, 0xC1, 0x0D, 0x00, 0x80, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1116 - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, - 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 1115 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1119 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x18, 0x78, 0xC0, 0x00, 0x30, - 0xE0, 0xC1, 0x00, 0x20, 0x80, 0x77, 0x00, 0x20, 0x00, 0x3E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x30, 0xE0, 0x01, 0x00, 0x18, 0x78, - 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1038 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x80, 0xC1, 0x07, 0x18, 0x00, 0x03, 0x3E, 0x1C, 0x00, - 0x02, 0xF8, 0x0F, 0x00, 0x02, 0xF0, 0x03, 0x00, 0x02, 0x7F, 0x00, 0x00, 0xE3, 0x0F, 0x00, 0x80, 0xF9, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, + 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 1115 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1119 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x18, 0x78, 0xC0, 0x00, 0x30, 0xE0, 0xC1, 0x00, + 0x20, 0x80, 0x77, 0x00, 0x20, 0x00, 0x3E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x30, 0xE0, 0x01, 0x00, 0x18, 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1038 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x80, 0xC1, 0x07, 0x18, 0x00, 0x03, 0x3E, 0x1C, 0x00, 0x02, 0xF8, 0x0F, + 0x00, 0x02, 0xF0, 0x03, 0x00, 0x02, 0x7F, 0x00, 0x00, 0xE3, 0x0F, 0x00, 0x80, 0xF9, 0x00, 0x00, 0x00, 0x18, // 1118 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, // 1032 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, - 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0xF8, - 0x1F, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0x08, 0x10, // 164 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, + 0x3F, // 1032 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0x18, 0x18, 0x00, + 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xDC, 0x3B, 0x00, + 0x00, 0x08, 0x10, // 164 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 1168 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xE1, 0x3F, 0x80, 0xFF, 0xE1, 0x3F, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xCE, 0x07, 0x03, 0x00, 0x7F, 0x0C, 0x0F, 0x80, 0x33, 0x1C, 0x0C, 0x80, - 0x71, 0x18, 0x18, 0x80, 0x61, 0x30, 0x18, 0x80, 0xC1, 0x70, 0x18, 0x80, 0xC3, 0xE1, 0x1C, 0x00, 0x87, 0xD3, 0x0F, 0x00, 0x06, - 0x9F, 0x07, 0x00, 0x00, 0x0E, // 167 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, - 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, - 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1025 - 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0xE7, 0x71, 0x00, 0x00, - 0xFB, 0x67, 0x00, 0x80, 0x19, 0xC6, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, - 0xCC, 0x00, 0x80, 0x1D, 0xCE, 0x00, 0x00, 0x1B, 0x66, 0x00, 0x00, 0x17, 0x72, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, - 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 169 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCE, 0x3C, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, - 0xC3, 0x60, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xCE, 0x07, 0x03, 0x00, 0x7F, 0x0C, 0x0F, 0x80, 0x33, 0x1C, 0x0C, 0x80, 0x71, 0x18, 0x18, + 0x80, 0x61, 0x30, 0x18, 0x80, 0xC1, 0x70, 0x18, 0x80, 0xC3, 0xE1, 0x1C, 0x00, 0x87, 0xD3, 0x0F, 0x00, 0x06, 0x9F, 0x07, 0x00, 0x00, 0x0E, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, + 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1025 + 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0xE7, 0x71, 0x00, 0x00, 0xFB, 0x67, 0x00, + 0x80, 0x19, 0xC6, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x1D, 0xCE, 0x00, + 0x00, 0x1B, 0x66, 0x00, 0x00, 0x17, 0x72, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 169 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCE, 0x3C, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, 0xC3, 0x60, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, + 0x00, 0x03, 0x60, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x08, 0x08, // 1028 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, - 0x10, 0x42, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x40, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xF0, - 0x07, 0x00, 0x00, 0xF0, 0x07, // 172 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 173 - 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, - 0xFB, 0x6F, 0x00, 0x80, 0xF9, 0xCF, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC3, 0x00, 0x80, 0xF9, - 0xC7, 0x00, 0x80, 0xF1, 0xCC, 0x00, 0x00, 0x03, 0x68, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, - 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 174 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x42, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, + 0x40, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF0, 0x07, // 172 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, // 173 + 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFB, 0x6F, 0x00, + 0x80, 0xF9, 0xCF, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC3, 0x00, 0x80, 0xF9, 0xC7, 0x00, 0x80, 0xF1, 0xCC, 0x00, + 0x00, 0x03, 0x68, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 174 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 1031 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, - 0x20, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0E, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, - 0x80, 0xC1, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, - 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, // 177 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, + 0x00, 0x11, 0x00, 0x00, 0x00, 0x0E, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, + 0x00, 0xFC, 0xFF, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, + 0x00, 0x80, 0xC1, // 177 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1030 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 1110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 1169 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 181 - 0x00, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, - 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, - 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, - 0x1B, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x73, 0x63, 0x00, 0x00, 0xE0, - 0x33, 0x00, 0x00, 0xC0, 0x13, // 1105 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, - 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x63, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0x18, 0x6C, 0x00, - 0x00, 0x18, 0x6C, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xE0, 0x63, // 8470 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 1169 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 181 + 0x00, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0xFF, 0x1F, + 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x1B, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x73, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 1105 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, + 0x00, 0x00, 0x78, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x63, 0x00, + 0x00, 0xF0, 0x67, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0x18, 0x6C, 0x00, 0x00, 0x18, 0x6C, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0xF0, 0x67, 0x00, + 0x00, 0xE0, 0x63, // 8470 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x60, 0x18, // 1108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, - 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x10, 0x42, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, - 0x0F, 0x00, 0x00, 0x00, 0x02, // 187 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 1112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, - 0xE1, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 1029 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, + 0x00, 0x80, 0x0F, 0x00, 0x00, 0x10, 0x42, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x02, // 187 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 1112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, 0xE1, 0xE0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, + 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 1029 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, 0x3C, // 1109 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 1111 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, - 0x3E, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, - 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 1040 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1041 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1042 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1043 - 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xDF, 0x00, 0x80, 0xFF, 0xC7, 0x00, 0x80, - 0x7F, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x3E, 0x06, 0x00, + 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 1040 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0x81, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1041 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, + 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1042 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, // 1043 + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xDF, 0x00, 0x80, 0xFF, 0xC7, 0x00, 0x80, 0x7F, 0xC0, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x0F, // 1044 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, - 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1045 - 0x80, 0x01, 0x80, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, - 0x38, 0x07, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, - 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x03, - 0x00, 0x00, 0x38, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x80, 0x01, 0x80, // 1046 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x78, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, - 0x01, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, - 0xE1, 0x00, 0x00, 0xBF, 0x73, 0x00, 0x00, 0x1C, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 1047 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x38, - 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1048 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x18, - 0x00, 0x3C, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x20, 0xC0, 0x01, 0x00, 0x20, 0xF0, 0x00, 0x00, 0x30, 0x38, - 0x00, 0x00, 0x18, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1049 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x78, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, - 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1050 - 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1051 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, - 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, - 0x00, 0x80, 0xFF, 0xFF, // 1052 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1053 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, - 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1054 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1055 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, - 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, - 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x3C, // 1056 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, - 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1045 + 0x80, 0x01, 0x80, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x38, 0x07, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x38, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, + 0x00, 0x07, 0x3C, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1046 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x78, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xE0, 0x00, + 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xE1, 0x00, 0x00, 0xBF, 0x73, 0x00, + 0x00, 0x1C, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 1047 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1048 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x18, 0x00, 0x3C, 0x00, + 0x30, 0x00, 0x0E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x20, 0xC0, 0x01, 0x00, 0x20, 0xF0, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0x1E, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1049 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x78, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1050 + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1051 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, + 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1052 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1053 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1054 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1055 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, + 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, + 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x3C, // 1056 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, 0x0C, // 1057 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1058 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, - 0xE0, 0xC1, 0x00, 0x00, 0x80, 0x77, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, - 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1059 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, - 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x06, - 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, - 0x00, 0x00, 0xE0, 0x03, // 1060 - 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, - 0x3C, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, - 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 1061 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, - 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1062 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, - 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1063 - 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, - 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1058 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0xE0, 0xC1, 0x00, + 0x00, 0x80, 0x77, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1059 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x0E, 0x38, 0x00, + 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, + 0x00, 0x0E, 0x38, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 1060 + 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, 0x3C, 0x0F, 0x00, + 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, + 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 1061 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1062 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1063 + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1064 - 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, - 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1065 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, - 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, - 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1066 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, - 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, - 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1067 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, - 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, - 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1068 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, - 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, - 0xC0, 0x00, 0x00, 0xC3, 0x60, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1065 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1066 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x80, 0xFF, 0xFF, // 1067 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1068 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xC3, 0x60, 0x00, + 0x00, 0xC7, 0x70, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1069 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, - 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, - 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, - 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1070 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x80, - 0xC1, 0x1E, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, - 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1071 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, - 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, - 0xFF, 0x00, 0x00, 0x00, 0x80, // 1072 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x73, 0x70, 0x00, 0x00, 0x31, 0xE0, 0x00, 0x80, - 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x39, 0xE0, 0x00, 0x80, 0x71, - 0x70, 0x00, 0x80, 0xE1, 0x3F, 0x00, 0x80, 0x80, 0x0F, // 1073 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0xF0, 0xE7, 0x00, 0x00, 0xF0, - 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1074 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1075 - 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xF8, 0xDF, 0x00, 0x00, 0xF8, 0xC3, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x0F, // 1076 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, - 0x33, 0x00, 0x00, 0xC0, 0x13, // 1077 - 0x00, 0x18, 0xE0, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, - 0x0D, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1078 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE7, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0x3C, // 1079 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1080 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, - 0x03, 0x3C, 0x00, 0x00, 0x02, 0x0F, 0x00, 0x00, 0x82, 0x07, 0x00, 0x00, 0xE2, 0x01, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x80, 0xF9, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1081 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0xC0, 0x0D, 0x00, 0x00, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, + 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x80, 0xC1, 0x1E, 0x00, + 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, + 0x80, 0xC1, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1071 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC6, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, // 1072 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x73, 0x70, 0x00, 0x00, 0x31, 0xE0, 0x00, 0x80, 0x19, 0xC0, 0x00, + 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x39, 0xE0, 0x00, 0x80, 0x71, 0x70, 0x00, 0x80, 0xE1, 0x3F, 0x00, + 0x80, 0x80, 0x0F, // 1073 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0xF0, 0xE7, 0x00, 0x00, 0xF0, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1074 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1075 + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xF8, 0xDF, 0x00, 0x00, 0xF8, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, + 0x00, 0x00, 0xC0, 0x0F, // 1076 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 1077 + 0x00, 0x18, 0xE0, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0xF8, 0x38, 0x00, + 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1078 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE7, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, + 0x3C, // 1079 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1080 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x03, 0x3C, 0x00, + 0x00, 0x02, 0x0F, 0x00, 0x00, 0x82, 0x07, 0x00, 0x00, 0xE2, 0x01, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1081 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0xC0, 0x0D, 0x00, 0x00, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1082 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1083 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, - 0xF0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, - 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1084 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1085 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, - 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1086 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1087 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, - 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x0F, // 1088 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1083 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1084 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1085 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1086 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1087 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0xC0, 0x0F, // 1088 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, 0x30, // 1089 - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, - 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1090 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, - 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x18, // 1090 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, 0x00, 0xF8, 0x0F, + 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 1091 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, - 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x70, 0x60, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, - 0xFF, 0x1F, 0x00, 0x70, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xE0, - 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1092 - 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x70, 0x60, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x00, 0x70, 0x60, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, + 0x00, 0xC0, 0x1F, // 1092 + 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, 0x80, // 1093 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1094 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, + 0x00, 0x00, 0xC0, 0x1F, // 1094 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1095 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1096 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, - 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, - 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1097 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, - 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, - 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1098 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, - 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, - 0x7E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1099 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, - 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, - 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, - 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1096 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, + 0x00, 0x00, 0xC0, + 0x1F, // 1097 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, + 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, + 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1098 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, + 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1099 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, + 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC3, 0x00, + 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xC0, 0x1F, // 1101 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, - 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, - 0x00, 0x00, 0xC0, 0x1F, // 1102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00, - 0x18, 0x0E, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, + 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00, 0x18, 0x0E, 0x00, + 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1103 }; diff --git a/src/graphics/images.h b/src/graphics/images.h index ef9ffef78..9ed9402b9 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -2,9 +2,8 @@ #define SATELLITE_IMAGE_WIDTH 16 #define SATELLITE_IMAGE_HEIGHT 15 -const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02, - 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54, - 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; +const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02, 0xF8, 0x00, 0xF0, 0x01, 0xE0, + 0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; #define imgSatellite_width 8 #define imgSatellite_height 8 @@ -16,8 +15,7 @@ const uint8_t imgUSB[] PROGMEM = {0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe const uint8_t imgUSB_HighResolution[] PROGMEM = {0x00, 0x3e, 0xf8, 0x80, 0x43, 0xf8, 0xc0, 0xc2, 0xff, 0x60, 0x42, 0xfc, 0x3c, 0xc2, 0xff, 0x22, 0x42, 0xf8, 0x3d, 0x42, 0xf8, 0x22, 0xc2, 0xff, 0x61, 0x42, 0xfc, 0xc0, 0xc2, 0xff, 0x80, 0x43, 0xf8, 0x00, 0x3e, 0xf8}; -const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, - 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; +const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3C}; const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF}; const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF}; @@ -26,18 +24,16 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ + defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_ST7796) || \ + defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgInfoL1[] PROGMEM = {0xff, 0x01, 0x01, 0x01, 0x1e, 0x7f, 0x1e, 0x01, 0x01, 0x01, 0x01, 0xff}; const uint8_t imgInfoL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; -const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01, - 0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb}; -const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, - 0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f}; +const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01, 0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb}; +const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, 0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f}; #else const uint8_t imgInfo[] PROGMEM = {0xff, 0x81, 0x00, 0xfb, 0xfb, 0x00, 0x81, 0xff}; const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1, 0xdf}; @@ -46,21 +42,21 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, // === Horizontal battery === // Basic battery design and all related pieces -const unsigned char batteryBitmap_h_bottom[] PROGMEM = { - 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, - 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, - 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; +const unsigned char batteryBitmap_h_bottom[] PROGMEM = {0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, + 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, + 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; -const unsigned char batteryBitmap_h_top[] PROGMEM = { - 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, - 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, - 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; +const unsigned char batteryBitmap_h_top[] PROGMEM = {0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, + 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, + 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; // Lightning Bolt -const unsigned char lightning_bolt_h[] PROGMEM = { - 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, - 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000, - 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; +const unsigned char lightning_bolt_h[] PROGMEM = {0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, + 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, + 0b01111000, 0b00000000, 0b00111100, 0b00000000, 0b00011100, 0b00000000, 0b00001100, + 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; // === Vertical battery === // Basic battery design and all related pieces @@ -131,8 +127,7 @@ const uint8_t icon_system[] PROGMEM = { }; // 🌐 Wi-Fi -const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, - 0b11011011, 0b00011000, 0b00011000, 0b00000000}; +const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000, 0b00011000, 0b00000000}; const uint8_t icon_nodes[] PROGMEM = { 0xF9, // Row 0 #..####### @@ -232,27 +227,23 @@ const uint8_t mute_symbol[] PROGMEM = { #define mute_symbol_big_width 16 #define mute_symbol_big_height 16 -const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, - 0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, - 0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, - 0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010, - 0b10000000, 0b01000001, 0b00000000, 0b10000000}; +const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, 0b00001000, + 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, 0b10001000, 0b00010000, + 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, 0b00010100, 0b00000100, 0b00101000, + 0b11111100, 0b00111111, 0b01000000, 0b00100010, 0b10000000, 0b01000001, 0b00000000, 0b10000000}; // Bell icon for Alert Message #define bell_alert_width 8 #define bell_alert_height 8 -const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, - 0b01000010, 0b01000010, 0b11111111, 0b00011000}; +const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000}; #define key_symbol_width 8 #define key_symbol_height 8 -const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, - 0b10101001, 0b10000110, 0b00000000, 0b00000000}; +const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, 0b10101001, 0b10000110, 0b00000000, 0b00000000}; #define placeholder_width 8 #define placeholder_height 8 -const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, - 0b11111111, 0b11111111, 0b11111111, 0b11111111}; +const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111}; #define icon_node_width 8 #define icon_node_height 8 @@ -269,40 +260,35 @@ static const uint8_t icon_node[] PROGMEM = { #define bluetoothdisabled_width 8 #define bluetoothdisabled_height 8 -const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, - 0b01001100, 0b00000000, 0b00000000, 0b00000000}; +const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, 0b01001100, 0b00000000, 0b00000000, 0b00000000}; #define smallbulletpoint_width 8 #define smallbulletpoint_height 8 -const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000}; +const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; // Digital Clock #define digital_icon_clock_width 8 #define digital_icon_clock_height 8 -const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, - 0b10010001, 0b10000001, 0b01000010, 0b00111100}; +const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, 0b10010001, 0b10000001, 0b01000010, 0b00111100}; // Analog Clock #define analog_icon_clock_width 8 #define analog_icon_clock_height 8 -const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, - 0b00100100, 0b01000010, 0b01000010, 0b11111111}; +const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; #define chirpy_width 38 #define chirpy_height 50 const uint8_t chirpy[] = { - 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, - 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, - 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, - 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, - 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, - 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, - 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, - 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, - 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, - 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, - 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, - 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; + 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, + 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x81, + 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, + 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, + 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, + 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, + 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, + 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, + 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, + 0x0c, 0x0c, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, + 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; #define chirpy_small_image_width 8 #define chirpy_small_image_height 8 diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp index 6d9b709b1..14af1d351 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp @@ -10,99 +10,86 @@ using namespace NicheGraphics::Drivers; // Private constructor // Called by getInstance -LatchingBacklight::LatchingBacklight() -{ - // Attach the deep sleep callback - deepSleepObserver.observe(¬ifyDeepSleep); +LatchingBacklight::LatchingBacklight() { + // Attach the deep sleep callback + deepSleepObserver.observe(¬ifyDeepSleep); } // Get access to (or create) the singleton instance of this class -LatchingBacklight *LatchingBacklight::getInstance() -{ - // Instantiate the class the first time this method is called - static LatchingBacklight *const singletonInstance = new LatchingBacklight; +LatchingBacklight *LatchingBacklight::getInstance() { + // Instantiate the class the first time this method is called + static LatchingBacklight *const singletonInstance = new LatchingBacklight; - return singletonInstance; + return singletonInstance; } // Which pin controls the backlight? // Is the light active HIGH (default) or active LOW? -void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) -{ - this->pin = pin; - this->logicActive = activeWhen; +void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) { + this->pin = pin; + this->logicActive = activeWhen; - pinMode(pin, OUTPUT); - off(); // Explicit off seem required by T-Echo? + pinMode(pin, OUTPUT); + off(); // Explicit off seem required by T-Echo? } // Called when device is shutting down // Ensures the backlight is off -int LatchingBacklight::beforeDeepSleep(void *unused) -{ - // Contingency only - // - pin wasn't set - if (pin != (uint8_t)-1) { - off(); - pinMode(pin, INPUT); // High impedance - unnecessary? - } else - LOG_WARN("LatchingBacklight instantiated, but pin not set"); - return 0; // Continue with deep sleep +int LatchingBacklight::beforeDeepSleep(void *unused) { + // Contingency only + // - pin wasn't set + if (pin != (uint8_t)-1) { + off(); + pinMode(pin, INPUT); // High impedance - unnecessary? + } else + LOG_WARN("LatchingBacklight instantiated, but pin not set"); + return 0; // Continue with deep sleep } // Turn the backlight on *temporarily* // This should be used for momentary illumination, such as while a button is held // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling -void LatchingBacklight::peek() -{ - assert(pin != (uint8_t)-1); - digitalWrite(pin, logicActive); // On - on = true; - latched = false; +void LatchingBacklight::peek() { + assert(pin != (uint8_t)-1); + digitalWrite(pin, logicActive); // On + on = true; + latched = false; } // Turn the backlight on, and keep it on // This should be used when the backlight should remain active, even after user input ends // e.g. when enabled via the menu // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling -void LatchingBacklight::latch() -{ - assert(pin != (uint8_t)-1); - - // Blink if moving from peek to latch - // Indicates to user that the transition has taken place - if (on && !latched) { - digitalWrite(pin, !logicActive); // Off - delay(25); - digitalWrite(pin, logicActive); // On - delay(25); - digitalWrite(pin, !logicActive); // Off - delay(25); - } +void LatchingBacklight::latch() { + assert(pin != (uint8_t)-1); + // Blink if moving from peek to latch + // Indicates to user that the transition has taken place + if (on && !latched) { + digitalWrite(pin, !logicActive); // Off + delay(25); digitalWrite(pin, logicActive); // On - on = true; - latched = true; + delay(25); + digitalWrite(pin, !logicActive); // Off + delay(25); + } + + digitalWrite(pin, logicActive); // On + on = true; + latched = true; } // Turn the backlight off // Suitable for ending both peek and latch -void LatchingBacklight::off() -{ - assert(pin != (uint8_t)-1); - digitalWrite(pin, !logicActive); // Off - on = false; - latched = false; +void LatchingBacklight::off() { + assert(pin != (uint8_t)-1); + digitalWrite(pin, !logicActive); // Off + on = false; + latched = false; } -bool LatchingBacklight::isOn() -{ - return on; -} +bool LatchingBacklight::isOn() { return on; } -bool LatchingBacklight::isLatched() -{ - return latched; -} +bool LatchingBacklight::isLatched() { return latched; } #endif diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h index 0097cae4c..be7eb41de 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h @@ -15,36 +15,34 @@ #include "Observer.h" -namespace NicheGraphics::Drivers -{ +namespace NicheGraphics::Drivers { -class LatchingBacklight -{ - public: - static LatchingBacklight *getInstance(); // Create or get the singleton instance - void setPin(uint8_t pin, bool activeWhen = HIGH); +class LatchingBacklight { +public: + static LatchingBacklight *getInstance(); // Create or get the singleton instance + void setPin(uint8_t pin, bool activeWhen = HIGH); - int beforeDeepSleep(void *unused); // Callback for auto-shutoff + int beforeDeepSleep(void *unused); // Callback for auto-shutoff - void peek(); // Backlight on temporarily, e.g. while button held - void latch(); // Backlight on permanently, e.g. toggled via menu - void off(); // Backlight off. Suitable for both peek and latch + void peek(); // Backlight on temporarily, e.g. while button held + void latch(); // Backlight on permanently, e.g. toggled via menu + void off(); // Backlight off. Suitable for both peek and latch - bool isOn(); // Either peek or latch - bool isLatched(); + bool isOn(); // Either peek or latch + bool isLatched(); - private: - LatchingBacklight(); // Constructor made private: force use of getInstance +private: + LatchingBacklight(); // Constructor made private: force use of getInstance - // Get notified when the system is shutting down - CallbackObserver deepSleepObserver = - CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); + // Get notified when the system is shutting down + CallbackObserver deepSleepObserver = + CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); - uint8_t pin = (uint8_t)-1; - bool logicActive = HIGH; // Is light active HIGH or active LOW + uint8_t pin = (uint8_t)-1; + bool logicActive = HIGH; // Is light active HIGH or active LOW - bool on = false; // Is light on (either peek or latched) - bool latched = false; // Is light latched on + bool on = false; // Is light on (either peek or latched) + bool latched = false; // Is light latched on }; } // namespace NicheGraphics::Drivers \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp index 2c8df96ed..6c47d7f28 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp @@ -30,103 +30,99 @@ static const uint8_t LUT_FAST[] = { }; // How strongly the pixels are pulled and pushed -void DEPG0213BNS800::configVoltages() -{ - switch (updateType) { - case FAST: - // Reference: display datasheet, GxEPD1 - sendCommand(0x03); // Gate voltage - sendData(0x17); // VGH: 20V +void DEPG0213BNS800::configVoltages() { + switch (updateType) { + case FAST: + // Reference: display datasheet, GxEPD1 + sendCommand(0x03); // Gate voltage + sendData(0x17); // VGH: 20V - // Reference: display datasheet, GxEPD1 - sendCommand(0x04); // Source voltage - sendData(0x41); // VSH1: 15V - sendData(0x00); // VSH2: NA - sendData(0x32); // VSL: -15V + // Reference: display datasheet, GxEPD1 + sendCommand(0x04); // Source voltage + sendData(0x41); // VSH1: 15V + sendData(0x00); // VSH2: NA + sendData(0x32); // VSL: -15V - // GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard - sendCommand(0x2C); // VCOM voltage - sendData(0x08); // VCOM: -0.2V - break; + // GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard + sendCommand(0x2C); // VCOM voltage + sendData(0x08); // VCOM: -0.2V + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory -void DEPG0213BNS800::configWaveform() -{ - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x80); // VSS +void DEPG0213BNS800::configWaveform() { + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VSS - sendCommand(0x32); // Write LUT register from MCU: - sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) - break; + sendCommand(0x32); // Write LUT register from MCU: + sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void DEPG0213BNS800::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xCF); // Differential, use manually loaded waveform - break; +void DEPG0213BNS800::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xCF); // Differential, use manually loaded waveform + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void DEPG0213BNS800::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms, then poll every 50ms - case FULL: - default: - return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms - } +void DEPG0213BNS800::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms, then poll every 50ms + case FULL: + default: + return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms + } } // For this display, we do not need to re-write the new image. // We're overriding SSD16XX::finalizeUpdate to make this small optimization. // The display does also work just fine with the generic SSD16XX method, though. -void DEPG0213BNS800::finalizeUpdate() -{ - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place - // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. - if (updateType != FULL) { - // writeNewImage(); // Not required for this display - writeOldImage(); - sendCommand(0x7F); // Terminate image write without update - wait(); - } +void DEPG0213BNS800::finalizeUpdate() { + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in + // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST + // etc. + if (updateType != FULL) { + // writeNewImage(); // Not required for this display + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } - // Enter deep-sleep to save a few µA - // Waking from this requires that display's reset pin is broken out - if (pin_rst != 0xFF) - deepSleep(); + // Enter deep-sleep to save a few µA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h index 3ce16e473..a17f274d1 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -19,25 +19,23 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class DEPG0213BNS800 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class DEPG0213BNS800 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte +public: + DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte - protected: - void configVoltages() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization +protected: + void configVoltages() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; + void finalizeUpdate() override; // Only overriden for a slight optimization }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp index 15134d5ad..2664cec21 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp @@ -31,95 +31,91 @@ static const uint8_t LUT_FAST[] = { }; // How strongly the pixels are pulled and pushed -void DEPG0290BNS800::configVoltages() -{ - switch (updateType) { - case FAST: - // Listed as "typical" in datasheet - sendCommand(0x04); - sendData(0x41); // VSH1 15V - sendData(0x00); // VSH2 NA - sendData(0x32); // VSL -15V - break; +void DEPG0290BNS800::configVoltages() { + switch (updateType) { + case FAST: + // Listed as "typical" in datasheet + sendCommand(0x04); + sendData(0x41); // VSH1 15V + sendData(0x00); // VSH2 NA + sendData(0x32); // VSL -15V + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory -void DEPG0290BNS800::configWaveform() -{ - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x60); // Actively hold screen border during update +void DEPG0290BNS800::configWaveform() { + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x60); // Actively hold screen border during update - sendCommand(0x32); // Write LUT register from MCU: - sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) - break; + sendCommand(0x32); // Write LUT register from MCU: + sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void DEPG0290BNS800::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xCF); // Differential, use manually loaded waveform - break; +void DEPG0290BNS800::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xCF); // Differential, use manually loaded waveform + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void DEPG0290BNS800::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 450); // At least 450ms for fast refresh - case FULL: - default: - return beginPolling(100, 3000); // At least 3 seconds for full refresh - } +void DEPG0290BNS800::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 450); // At least 450ms for fast refresh + case FULL: + default: + return beginPolling(100, 3000); // At least 3 seconds for full refresh + } } // For this display, we do not need to re-write the new image. // We're overriding SSD16XX::finalizeUpdate to make this small optimization. // The display does also work just fine with the generic SSD16XX method, though. -void DEPG0290BNS800::finalizeUpdate() -{ - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place - // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. - if (updateType != FULL) { - // writeNewImage(); // Not required for this display - writeOldImage(); - sendCommand(0x7F); // Terminate image write without update - wait(); - } +void DEPG0290BNS800::finalizeUpdate() { + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in + // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST + // etc. + if (updateType != FULL) { + // writeNewImage(); // Not required for this display + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } - // Enter deep-sleep to save a few µA - // Waking from this requires that display's reset pin is broken out - if (pin_rst != 0xFF) - deepSleep(); + // Enter deep-sleep to save a few µA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h index 257fed1a6..d6f02184a 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -17,25 +17,23 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class DEPG0290BNS800 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 128; - static constexpr uint32_t height = 296; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class DEPG0290BNS800 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 128; + static constexpr uint32_t height = 296; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte +public: + DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte - protected: - void configVoltages() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization +protected: + void configVoltages() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; + void finalizeUpdate() override; // Only overriden for a slight optimization }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.cpp b/src/graphics/niche/Drivers/EInk/E0213A367.cpp index f19cb4ff7..a12688df9 100644 --- a/src/graphics/niche/Drivers/EInk/E0213A367.cpp +++ b/src/graphics/niche/Drivers/EInk/E0213A367.cpp @@ -5,80 +5,77 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void E0213A367::configScanning() -{ - // "Driver output control" - // Scan gates from 0 to 249 (vertical resolution 250px) - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); +void E0213A367::configScanning() { + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels -void E0213A367::configWaveform() -{ - // This command (0x37) is poorly documented - // As of July 2025, the datasheet for this display's controller IC is unavailable - // The values are supplied by Heltec, who presumably have privileged access to information from the display manufacturer - // Datasheet for the similar SSD1680 IC hints at the function of this command: +void E0213A367::configWaveform() { + // This command (0x37) is poorly documented + // As of July 2025, the datasheet for this display's controller IC is unavailable + // The values are supplied by Heltec, who presumably have privileged access to information from the display + // manufacturer Datasheet for the similar SSD1680 IC hints at the function of this command: - // "Spare VCOM OTP selection": - // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. - // Maybe value is redundant? No noticeable impact when set to 0x00. - // We'll leave it set to 0x40, following Heltec's lead, just in case. + // "Spare VCOM OTP selection": + // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. + // Maybe value is redundant? No noticeable impact when set to 0x00. + // We'll leave it set to 0x40, following Heltec's lead, just in case. - // "Display Mode" - // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential refresh) + // "Display Mode" + // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential + // refresh) - // Unusual that waveforms are programmed to OTP, but this meta information is not ..? + // Unusual that waveforms are programmed to OTP, but this meta information is not ..? - sendCommand(0x37); // "Write Register for Display Option" ? - sendData(0x40); // "Spare VCOM OTP selection" ? - sendData(0x80); // "Display Mode for WS[7:0]" ? - sendData(0x03); // "Display Mode for WS[15:8]" ? - sendData(0x0E); // "Display Mode [23:16]" ? + sendCommand(0x37); // "Write Register for Display Option" ? + sendData(0x40); // "Spare VCOM OTP selection" ? + sendData(0x80); // "Display Mode for WS[7:0]" ? + sendData(0x03); // "Display Mode for WS[15:8]" ? + sendData(0x0E); // "Display Mode [23:16]" ? - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. - break; - case FULL: - default: - sendCommand(0x3C); // Border waveform: - sendData(0x01); // Follow LUT 1 (blink same as white pixels) - break; - } + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } } // Tell controller IC which operations to run -void E0213A367::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh" - break; - } +void E0213A367::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh" + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void E0213A367::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 1500); // At least 1.5 seconds for full refresh - } +void E0213A367::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.h b/src/graphics/niche/Drivers/EInk/E0213A367.h index a36fcb407..fc089eb42 100644 --- a/src/graphics/niche/Drivers/EInk/E0213A367.h +++ b/src/graphics/niche/Drivers/EInk/E0213A367.h @@ -17,24 +17,22 @@ E-Ink display driver #include "./SSD1682.h" -namespace NicheGraphics::Drivers -{ -class E0213A367 : public SSD1682 -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class E0213A367 : public SSD1682 { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - E0213A367() : SSD1682(width, height, supported, 0) {} +public: + E0213A367() : SSD1682(width, height, supported, 0) {} - protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/EInk.cpp b/src/graphics/niche/Drivers/EInk/EInk.cpp index cd2e9dc98..b45699a88 100644 --- a/src/graphics/niche/Drivers/EInk/EInk.cpp +++ b/src/graphics/niche/Drivers/EInk/EInk.cpp @@ -6,81 +6,76 @@ using namespace NicheGraphics::Drivers; // Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported) - : concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported) -{ - OSThread::disable(); + : concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported) { + OSThread::disable(); } // Used by NicheGraphics implementations to check if a display supports a specific refresh operation. // Whether or not the update type is supported is specified in the constructor -bool EInk::supports(UpdateTypes type) -{ - // The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set. - if (supportedUpdateTypes & type) - return true; - else - return false; +bool EInk::supports(UpdateTypes type) { + // The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set. + if (supportedUpdateTypes & type) + return true; + else + return false; } // Begins using the OSThread to detect when a display update is complete // This allows the refresh operation to run "asynchronously". -// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin -// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes. -// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", -// provided its isUpdateDone() override always returns true. -void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) -{ - updateRunning = true; - pollingInterval = interval; - pollingBegunAt = millis(); +// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY +// pin The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an +// update takes. Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", provided its +// isUpdateDone() override always returns true. +void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) { + updateRunning = true; + pollingInterval = interval; + pollingBegunAt = millis(); - // To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take - // By default, expectedDuration is 0, and we'll start polling immediately - OSThread::setIntervalFromNow(expectedDuration); - OSThread::enabled = true; + // To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will + // take By default, expectedDuration is 0, and we'll start polling immediately + OSThread::setIntervalFromNow(expectedDuration); + OSThread::enabled = true; } // Meshtastic's pseudo-threading layer // We're using this as a timer, to periodically check if an update is complete // This is what allows us to update the display asynchronously -int32_t EInk::runOnce() -{ - // Check for polling timeout - // Manually set at 10 seconds, in case some big task holds up the firmware's cooperative multitasking - if (millis() - pollingBegunAt > 10000) - failed = true; +int32_t EInk::runOnce() { + // Check for polling timeout + // Manually set at 10 seconds, in case some big task holds up the firmware's cooperative multitasking + if (millis() - pollingBegunAt > 10000) + failed = true; - // Handle failure - // - polling timeout - // - other error (derived classes) - if (failed) { - LOG_WARN("Display update failed. Check wiring & power supply."); - updateRunning = false; - failed = false; - return disable(); - } + // Handle failure + // - polling timeout + // - other error (derived classes) + if (failed) { + LOG_WARN("Display update failed. Check wiring & power supply."); + updateRunning = false; + failed = false; + return disable(); + } - // If update not yet done - if (!isUpdateDone()) - return pollingInterval; // Poll again in a few ms + // If update not yet done + if (!isUpdateDone()) + return pollingInterval; // Poll again in a few ms - // If update done - finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc - updateRunning = false; // Change what we report via EInk::busy() - return disable(); // Stop polling + // If update done + finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc + updateRunning = false; // Change what we report via EInk::busy() + return disable(); // Stop polling } // Wait for an in progress update to complete before continuing // Run a normal (async) update first, *then* call await -void EInk::await() -{ - // Stop our concurrency thread - OSThread::disable(); +void EInk::await() { + // Stop our concurrency thread + OSThread::disable(); - // Sit and block until the update is complete - while (updateRunning) { - runOnce(); - yield(); - } + // Sit and block until the update is complete + while (updateRunning) { + runOnce(); + yield(); + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/EInk.h b/src/graphics/niche/Drivers/EInk/EInk.h index 3c51d4f1d..2b39b3e97 100644 --- a/src/graphics/niche/Drivers/EInk/EInk.h +++ b/src/graphics/niche/Drivers/EInk/EInk.h @@ -12,44 +12,42 @@ #include "concurrency/OSThread.h" #include -namespace NicheGraphics::Drivers -{ +namespace NicheGraphics::Drivers { -class EInk : private concurrency::OSThread -{ - public: - // Different possible operations used to update an E-Ink display - // Some displays will not support all operations - // Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType) - enum UpdateTypes : uint8_t { - UNSPECIFIED = 0, - FULL = 1 << 0, - FAST = 1 << 1, // "Partial Refresh" - }; +class EInk : private concurrency::OSThread { +public: + // Different possible operations used to update an E-Ink display + // Some displays will not support all operations + // Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType) + enum UpdateTypes : uint8_t { + UNSPECIFIED = 0, + FULL = 1 << 0, + FAST = 1 << 1, // "Partial Refresh" + }; - EInk(uint16_t width, uint16_t height, UpdateTypes supported); - virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0; - virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image - void await(); // Wait for an in-progress update to complete before proceeding - bool supports(UpdateTypes type); // Can display perform a certain update type - bool busy() { return updateRunning; } // Display able to update right now? + EInk(uint16_t width, uint16_t height, UpdateTypes supported); + virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0; + virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image + void await(); // Wait for an in-progress update to complete before proceeding + bool supports(UpdateTypes type); // Can display perform a certain update type + bool busy() { return updateRunning; } // Display able to update right now? - const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const. - const uint16_t height; + const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const. + const uint16_t height; - protected: - void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished - virtual bool isUpdateDone() = 0; // Check once if update finished - virtual void finalizeUpdate() {} // Run any post-update code - bool failed = false; // If an error occurred during update +protected: + void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished + virtual bool isUpdateDone() = 0; // Check once if update finished + virtual void finalizeUpdate() {} // Run any post-update code + bool failed = false; // If an error occurred during update - private: - int32_t runOnce() override; // Repeated checking if update finished +private: + int32_t runOnce() override; // Repeated checking if update finished - const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class - bool updateRunning = false; // see EInk::busy() - uint32_t pollingInterval = 0; // How often to check if update complete (ms) - uint32_t pollingBegunAt = 0; // To timeout during polling + const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class + bool updateRunning = false; // see EInk::busy() + uint32_t pollingInterval = 0; // How often to check if update complete (ms) + uint32_t pollingBegunAt = 0; // To timeout during polling }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp index 9a06fa841..3bdb2980b 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp @@ -5,54 +5,50 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void GDEY0154D67::configScanning() -{ - // "Driver output control" - sendCommand(0x01); - sendData(0xC7); // Scan until gate 199 (200px vertical res.) - sendData(0x00); - sendData(0x00); +void GDEY0154D67::configScanning() { + // "Driver output control" + sendCommand(0x01); + sendData(0xC7); // Scan until gate 199 (200px vertical res.) + sendData(0x00); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void GDEY0154D67::configWaveform() -{ - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void GDEY0154D67::configWaveform() { + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void GDEY0154D67::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void GDEY0154D67::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void GDEY0154D67::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 300); // At least 300ms for fast refresh - case FULL: - default: - return beginPolling(100, 1500); // At least 1.5 seconds for full refresh - } +void GDEY0154D67::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 300); // At least 300ms for fast refresh + case FULL: + default: + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h index e391eea50..6e4025b54 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h @@ -17,24 +17,22 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class GDEY0154D67 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 200; - static constexpr uint32_t height = 200; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class GDEY0154D67 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 200; + static constexpr uint32_t height = 200; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - GDEY0154D67() : SSD16XX(width, height, supported) {} +public: + GDEY0154D67() : SSD16XX(width, height, supported) {} - protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp index b3a585eb7..ac214dfc8 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp @@ -5,54 +5,50 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void GDEY0213B74::configScanning() -{ - // "Driver output control" - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); - sendData(0x00); +void GDEY0213B74::configScanning() { + // "Driver output control" + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void GDEY0213B74::configWaveform() -{ - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void GDEY0213B74::configWaveform() { + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void GDEY0213B74::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void GDEY0213B74::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void GDEY0213B74::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh - } +void GDEY0213B74::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h index 1c36f295d..50ffc8e65 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h @@ -19,24 +19,22 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class GDEY0213B74 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class GDEY0213B74 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - GDEY0213B74() : SSD16XX(width, height, supported) {} +public: + GDEY0213B74() : SSD16XX(width, height, supported) {} - protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp index 0509b0502..3f8c4f8b2 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp @@ -5,57 +5,53 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void HINK_E0213A289::configScanning() -{ - // "Driver output control" - // Scan gates from 0 to 249 (vertical resolution 250px) - sendCommand(0x01); - sendData(0xF9); // Maximum gate # (249, bits 0-7) - sendData(0x00); // Maximum gate # (bit 8) - sendData(0x00); // (Do not invert scanning order) +void HINK_E0213A289::configScanning() { + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); // Maximum gate # (249, bits 0-7) + sendData(0x00); // Maximum gate # (bit 8) + sendData(0x00); // (Do not invert scanning order) } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void HINK_E0213A289::configWaveform() -{ - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void HINK_E0213A289::configWaveform() { + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void HINK_E0213A289::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void HINK_E0213A289::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void HINK_E0213A289::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) - } +void HINK_E0213A289::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h index eab0bf59d..765413864 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h @@ -19,24 +19,22 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class HINK_E0213A289 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class HINK_E0213A289 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} +public: + HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} - protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp b/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp index 1b72bc4a9..b2e172e13 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp +++ b/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp @@ -7,52 +7,49 @@ using namespace NicheGraphics::Drivers; // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory -void HINK_E042A87::configWaveform() -{ - sendCommand(0x3C); // Border waveform: - sendData(0x01); // Follow LUT for VSH1 +void HINK_E042A87::configWaveform() { + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT for VSH1 - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void HINK_E042A87::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x21); // Use both "old" and "new" image memory (differential) - sendData(0x00); - sendData(0x00); +void HINK_E042A87::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x21); // Use both "old" and "new" image memory (differential) + sendData(0x00); + sendData(0x00); - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Differential, load waveform from OTP - break; + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Differential, load waveform from OTP + break; - case FULL: - default: - sendCommand(0x21); // Bypass "old" image memory (non-differential) - sendData(0x40); - sendData(0x00); + case FULL: + default: + sendCommand(0x21); // Bypass "old" image memory (non-differential) + sendData(0x40); + sendData(0x00); - sendCommand(0x22); // Set "update sequence": - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } + sendCommand(0x22); // Set "update sequence": + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void HINK_E042A87::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 1000); // At least 1 second, then check every 50ms - case FULL: - default: - return beginPolling(100, 3500); // At least 3.5 seconds, then check every 100ms - } +void HINK_E042A87::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 1000); // At least 1 second, then check every 50ms + case FULL: + default: + return beginPolling(100, 3500); // At least 3.5 seconds, then check every 100ms + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h index 612072b50..c32d159fb 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h +++ b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h @@ -20,23 +20,21 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class HINK_E042A87 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 400; - static constexpr uint32_t height = 300; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class HINK_E042A87 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 400; + static constexpr uint32_t height = 300; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - HINK_E042A87() : SSD16XX(width, height, supported) {} +public: + HINK_E042A87() : SSD16XX(width, height, supported) {} - protected: - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp index e9a663f80..090945216 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp @@ -5,64 +5,60 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void LCMEN2R13ECC1::configScanning() -{ - // "Driver output control" - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); - sendData(0x00); +void LCMEN2R13ECC1::configScanning() { + // "Driver output control" + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); - // To-do: delete this method? - // Values set here might be redundant: F9, 00, 00 seems to be default + // To-do: delete this method? + // Values set here might be redundant: F9, 00, 00 seems to be default } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void LCMEN2R13ECC1::configWaveform() -{ - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x85); - break; +void LCMEN2R13ECC1::configWaveform() { + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x85); + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } -void LCMEN2R13ECC1::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void LCMEN2R13ECC1::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void LCMEN2R13ECC1::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 800); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 2500); // At least 2 seconds for full refresh - } +void LCMEN2R13ECC1::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 800); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2500); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h index 9fa6eaac9..19a7d2ca2 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h @@ -16,24 +16,22 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class LCMEN2R13ECC1 : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class LCMEN2R13ECC1 : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte +public: + LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte - protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp index fb37544b2..37c9b47d3 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp @@ -68,239 +68,223 @@ static const uint8_t LUT_FAST_BB[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; -LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) -{ - // Pre-calculate size of the image buffer, for convenience +LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) { + // Pre-calculate size of the image buffer, for convenience - // Determine the X dimension of the image buffer, in bytes. - // Along rows, pixels are stored 8 per byte. - // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. - bufferRowSize = ((width - 1) / 8) + 1; + // Determine the X dimension of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + bufferRowSize = ((width - 1) / 8) + 1; - // Total size of image buffer, in bytes. - bufferSize = bufferRowSize * height; + // Total size of image buffer, in bytes. + bufferSize = bufferRowSize * height; } -void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) -{ - this->spi = spi; - this->pin_dc = pin_dc; - this->pin_cs = pin_cs; - this->pin_busy = pin_busy; - this->pin_rst = pin_rst; +void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; - pinMode(pin_dc, OUTPUT); - pinMode(pin_cs, OUTPUT); - pinMode(pin_busy, INPUT); + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); - // Reset is active low, hold high - pinMode(pin_rst, INPUT_PULLUP); + // Reset is active low, hold high + pinMode(pin_rst, INPUT_PULLUP); - reset(); + reset(); } // Display an image on the display -void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) -{ - this->updateType = type; - this->buffer = imageData; +void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) { + this->updateType = type; + this->buffer = imageData; - reset(); + reset(); - // Config - if (updateType == FULL) - configFull(); - else - configFast(); + // Config + if (updateType == FULL) + configFull(); + else + configFast(); - // Transfer image data - if (updateType == FULL) { - writeNewImage(); - writeOldImage(); - } else { - writeNewImage(); - } + // Transfer image data + if (updateType == FULL) { + writeNewImage(); + writeOldImage(); + } else { + writeNewImage(); + } - sendCommand(0x04); // Power on the panel voltage - wait(); + sendCommand(0x04); // Power on the panel voltage + wait(); - sendCommand(0x12); // Begin executing the update + sendCommand(0x12); // Begin executing the update - // Let the update run async, on display hardware. Base class will poll completion, then finalize. - // For a blocking update, call await after update - detachFromUpdate(); + // Let the update run async, on display hardware. Base class will poll completion, then finalize. + // For a blocking update, call await after update + detachFromUpdate(); } -void LCMEN213EFC1::wait() -{ - // Busy when LOW - while (digitalRead(pin_busy) == LOW) - yield(); +void LCMEN213EFC1::wait() { + // Busy when LOW + while (digitalRead(pin_busy) == LOW) + yield(); } -void LCMEN213EFC1::reset() -{ - pinMode(pin_rst, OUTPUT); - digitalWrite(pin_rst, LOW); - delay(10); - pinMode(pin_rst, INPUT_PULLUP); - wait(); +void LCMEN213EFC1::reset() { + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, LOW); + delay(10); + pinMode(pin_rst, INPUT_PULLUP); + wait(); - sendCommand(0x12); - wait(); + sendCommand(0x12); + wait(); } -void LCMEN213EFC1::sendCommand(const uint8_t command) -{ - // Take firmware's SPI lock - spiLock->lock(); +void LCMEN213EFC1::sendCommand(const uint8_t command) { + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, LOW); // DC pin low indicates command - digitalWrite(pin_cs, LOW); - spi->transfer(command); - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); // DC pin low indicates command + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void LCMEN213EFC1::sendData(uint8_t data) -{ - sendData(&data, 1); -} +void LCMEN213EFC1::sendData(uint8_t data) { sendData(&data, 1); } -void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) -{ - // Take firmware's SPI lock - spiLock->lock(); +void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) { + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command - digitalWrite(pin_cs, LOW); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + digitalWrite(pin_cs, LOW); - // Platform-specific SPI command - // Mothballing. This display model is only used by Heltec Wireless Paper (ESP32) + // Platform-specific SPI command + // Mothballing. This display model is only used by Heltec Wireless Paper (ESP32) #if defined(ARCH_ESP32) - spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer + spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer #elif defined(ARCH_NRF52) - spi->transfer(data, NULL, size); // NULL for a "write only" transfer + spi->transfer(data, NULL, size); // NULL for a "write only" transfer #else #error Not implemented yet? Feel free to add other platforms here. #endif - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void LCMEN213EFC1::configFull() -{ - sendCommand(0x00); // Panel setting register - sendData(0b11 << 6 // Display resolution - | 1 << 4 // B&W only - | 1 << 3 // Vertical scan direction - | 1 << 2 // Horizontal scan direction - | 1 << 1 // Shutdown: no - | 1 << 0 // Reset: no - ); +void LCMEN213EFC1::configFull() { + sendCommand(0x00); // Panel setting register + sendData(0b11 << 6 // Display resolution + | 1 << 4 // B&W only + | 1 << 3 // Vertical scan direction + | 1 << 2 // Horizontal scan direction + | 1 << 1 // Shutdown: no + | 1 << 0 // Reset: no + ); - sendCommand(0x50); // VCOM and data interval setting register - sendData(0b10 << 6 // Border driven white - | 0b11 << 4 // Invert image colors: no - | 0b0111 << 0 // Interval between VCOM on and image data (default) - ); + sendCommand(0x50); // VCOM and data interval setting register + sendData(0b10 << 6 // Border driven white + | 0b11 << 4 // Invert image colors: no + | 0b0111 << 0 // Interval between VCOM on and image data (default) + ); } -void LCMEN213EFC1::configFast() -{ - sendCommand(0x00); // Panel setting register - sendData(0b11 << 6 // Display resolution - | 1 << 5 // LUT from registers (set below) - | 1 << 4 // B&W only - | 1 << 3 // Vertical scan direction - | 1 << 2 // Horizontal scan direction - | 1 << 1 // Shutdown: no - | 1 << 0 // Reset: no - ); +void LCMEN213EFC1::configFast() { + sendCommand(0x00); // Panel setting register + sendData(0b11 << 6 // Display resolution + | 1 << 5 // LUT from registers (set below) + | 1 << 4 // B&W only + | 1 << 3 // Vertical scan direction + | 1 << 2 // Horizontal scan direction + | 1 << 1 // Shutdown: no + | 1 << 0 // Reset: no + ); - sendCommand(0x50); // VCOM and data interval setting register - sendData(0b11 << 6 // Border floating - | 0b01 << 4 // Invert image colors: no - | 0b0111 << 0 // Interval between VCOM on and image data (default) - ); + sendCommand(0x50); // VCOM and data interval setting register + sendData(0b11 << 6 // Border floating + | 0b01 << 4 // Invert image colors: no + | 0b0111 << 0 // Interval between VCOM on and image data (default) + ); - // Load the various LUTs - sendCommand(0x20); // VCOM - sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC)); + // Load the various LUTs + sendCommand(0x20); // VCOM + sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC)); - sendCommand(0x21); // White -> White - sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW)); + sendCommand(0x21); // White -> White + sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW)); - sendCommand(0x22); // Black -> White - sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW)); + sendCommand(0x22); // Black -> White + sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW)); - sendCommand(0x23); // White -> Black - sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB)); + sendCommand(0x23); // White -> Black + sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB)); - sendCommand(0x24); // Black -> Black - sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB)); + sendCommand(0x24); // Black -> Black + sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB)); } -void LCMEN213EFC1::writeNewImage() -{ - sendCommand(0x13); - sendData(buffer, bufferSize); +void LCMEN213EFC1::writeNewImage() { + sendCommand(0x13); + sendData(buffer, bufferSize); } -void LCMEN213EFC1::writeOldImage() -{ - sendCommand(0x10); - sendData(buffer, bufferSize); +void LCMEN213EFC1::writeOldImage() { + sendCommand(0x10); + sendData(buffer, bufferSize); } -void LCMEN213EFC1::detachFromUpdate() -{ - // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types - // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed - // If not implemented, we'll just poll right from the get-go - switch (updateType) { - case FULL: - EInk::beginPolling(10, 3650); - break; - case FAST: - EInk::beginPolling(10, 720); - break; - default: - assert(false); - } +void LCMEN213EFC1::detachFromUpdate() { + // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types + // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed + // If not implemented, we'll just poll right from the get-go + switch (updateType) { + case FULL: + EInk::beginPolling(10, 3650); + break; + case FAST: + EInk::beginPolling(10, 720); + break; + default: + assert(false); + } } -bool LCMEN213EFC1::isUpdateDone() -{ - // Busy when LOW - if (digitalRead(pin_busy) == LOW) - return false; - else - return true; +bool LCMEN213EFC1::isUpdateDone() { + // Busy when LOW + if (digitalRead(pin_busy) == LOW) + return false; + else + return true; } -void LCMEN213EFC1::finalizeUpdate() -{ - // Power off the panel voltages - sendCommand(0x02); +void LCMEN213EFC1::finalizeUpdate() { + // Power off the panel voltages + sendCommand(0x02); + wait(); + + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in + // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST + // etc. + if (updateType != FULL) { + writeOldImage(); wait(); - - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place - // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. - if (updateType != FULL) { - writeOldImage(); - wait(); - } + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h index 499daef05..103d15bcf 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h @@ -20,50 +20,48 @@ It is implemented as a "one-off", directly inheriting the EInk base class, unlik #include "./EInk.h" -namespace NicheGraphics::Drivers -{ +namespace NicheGraphics::Drivers { -class LCMEN213EFC1 : public EInk -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +class LCMEN213EFC1 : public EInk { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - LCMEN213EFC1(); - void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst); - void update(uint8_t *imageData, UpdateTypes type) override; +public: + LCMEN213EFC1(); + void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst); + void update(uint8_t *imageData, UpdateTypes type) override; - protected: - void wait(); - void reset(); - void sendCommand(const uint8_t command); - void sendData(const uint8_t data); - void sendData(const uint8_t *data, uint32_t size); - void configFull(); // Configure display for FULL refresh - void configFast(); // Configure display for FAST refresh - void writeNewImage(); - void writeOldImage(); // Used for "differential update", aka FAST refresh +protected: + void wait(); + void reset(); + void sendCommand(const uint8_t command); + void sendData(const uint8_t data); + void sendData(const uint8_t *data, uint32_t size); + void configFull(); // Configure display for FULL refresh + void configFast(); // Configure display for FAST refresh + void writeNewImage(); + void writeOldImage(); // Used for "differential update", aka FAST refresh - void detachFromUpdate(); - bool isUpdateDone(); - void finalizeUpdate(); + void detachFromUpdate(); + bool isUpdateDone(); + void finalizeUpdate(); - protected: - uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? - uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) - uint32_t bufferSize = 0; // In bytes. Rows * Columns - uint8_t *buffer = nullptr; - UpdateTypes updateType = UpdateTypes::UNSPECIFIED; +protected: + uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? + uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) + uint32_t bufferSize = 0; // In bytes. Rows * Columns + uint8_t *buffer = nullptr; + UpdateTypes updateType = UpdateTypes::UNSPECIFIED; - uint8_t pin_dc = -1; - uint8_t pin_cs = -1; - uint8_t pin_busy = -1; - uint8_t pin_rst = -1; - SPIClass *spi = nullptr; - SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0); + uint8_t pin_dc = -1; + uint8_t pin_cs = -1; + uint8_t pin_busy = -1; + uint8_t pin_rst = -1; + SPIClass *spi = nullptr; + SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.cpp b/src/graphics/niche/Drivers/EInk/SSD1682.cpp index c3d7f7786..b797cdfa6 100644 --- a/src/graphics/niche/Drivers/EInk/SSD1682.cpp +++ b/src/graphics/niche/Drivers/EInk/SSD1682.cpp @@ -5,37 +5,34 @@ using namespace NicheGraphics::Drivers; SSD1682::SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX) - : SSD16XX(width, height, supported, bufferOffsetX) -{ -} + : SSD16XX(width, height, supported, bufferOffsetX) {} // SSD1682 only accepts single-byte x and y values // This causes an incompatibility with the default SSD16XX::configFullscreen -void SSD1682::configFullscreen() -{ - // Define the boundaries of the "fullscreen" region, for the controller IC - static const uint8_t sx = bufferOffsetX; // Notice the offset - static const uint8_t sy = 0; - static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this - static const uint8_t ey = height; +void SSD1682::configFullscreen() { + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint8_t sx = bufferOffsetX; // Notice the offset + static const uint8_t sy = 0; + static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint8_t ey = height; - // Data entry mode - Left to Right, Top to Bottom - sendCommand(0x11); - sendData(0x03); + // Data entry mode - Left to Right, Top to Bottom + sendCommand(0x11); + sendData(0x03); - // Select controller IC memory region to display a fullscreen image - sendCommand(0x44); // Memory X start - end - sendData(sx); - sendData(ex); - sendCommand(0x45); // Memory Y start - end - sendData(sy); - sendData(ey); + // Select controller IC memory region to display a fullscreen image + sendCommand(0x44); // Memory X start - end + sendData(sx); + sendData(ex); + sendCommand(0x45); // Memory Y start - end + sendData(sy); + sendData(ey); - // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 - sendCommand(0x4E); // Memory cursor X - sendData(sx); - sendCommand(0x4F); // Memory cursor y - sendData(sy); + // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 + sendCommand(0x4E); // Memory cursor X + sendData(sx); + sendCommand(0x4F); // Memory cursor y + sendData(sy); } #endif \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.h b/src/graphics/niche/Drivers/EInk/SSD1682.h index ba3008537..cbb8909c4 100644 --- a/src/graphics/niche/Drivers/EInk/SSD1682.h +++ b/src/graphics/niche/Drivers/EInk/SSD1682.h @@ -15,15 +15,13 @@ to avoid re-implementing them every time we need to add a new SSD1682-based disp #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ +namespace NicheGraphics::Drivers { -class SSD1682 : public SSD16XX -{ - public: - SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); - virtual void configFullscreen(); // Select memory region on controller IC - virtual void deepSleep() {} // Not usable (image memory not retained) +class SSD1682 : public SSD16XX { +public: + SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void deepSleep() {} // Not usable (image memory not retained) }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp index d0d030be8..add2031be 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp @@ -7,266 +7,249 @@ using namespace NicheGraphics::Drivers; SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX) - : EInk(width, height, supported), bufferOffsetX(bufferOffsetX) -{ - // Pre-calculate size of the image buffer, for convenience + : EInk(width, height, supported), bufferOffsetX(bufferOffsetX) { + // Pre-calculate size of the image buffer, for convenience - // Determine the X dimension of the image buffer, in bytes. - // Along rows, pixels are stored 8 per byte. - // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. - bufferRowSize = ((width - 1) / 8) + 1; + // Determine the X dimension of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + bufferRowSize = ((width - 1) / 8) + 1; - // Total size of image buffer, in bytes. - bufferSize = bufferRowSize * height; + // Total size of image buffer, in bytes. + bufferSize = bufferRowSize * height; } -void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) -{ - this->spi = spi; - this->pin_dc = pin_dc; - this->pin_cs = pin_cs; - this->pin_busy = pin_busy; - this->pin_rst = pin_rst; +void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; - pinMode(pin_dc, OUTPUT); - pinMode(pin_cs, OUTPUT); - pinMode(pin_busy, INPUT); + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); - // If using a reset pin, hold high - // Reset is active low for Solomon Systech ICs - if (pin_rst != 0xFF) - pinMode(pin_rst, INPUT_PULLUP); + // If using a reset pin, hold high + // Reset is active low for Solomon Systech ICs + if (pin_rst != 0xFF) + pinMode(pin_rst, INPUT_PULLUP); - reset(); + reset(); } // Poll the displays busy pin until an operation is complete // Timeout and set fail flag if something went wrong and the display got stuck -void SSD16XX::wait(uint32_t timeout) -{ - // Don't bother waiting if part of the update sequence failed - // In that situation, we're now just failing-through the process, until we can try again with next update. - if (failed) - return; +void SSD16XX::wait(uint32_t timeout) { + // Don't bother waiting if part of the update sequence failed + // In that situation, we're now just failing-through the process, until we can try again with next update. + if (failed) + return; - uint32_t startMs = millis(); + uint32_t startMs = millis(); - // Busy when HIGH - while (digitalRead(pin_busy) == HIGH) { - // Check for timeout - if (millis() - startMs > timeout) { - failed = true; - break; - } - yield(); + // Busy when HIGH + while (digitalRead(pin_busy) == HIGH) { + // Check for timeout + if (millis() - startMs > timeout) { + failed = true; + break; } + yield(); + } } -void SSD16XX::reset() -{ - // Check if reset pin is defined - if (pin_rst != 0xFF) { - pinMode(pin_rst, OUTPUT); - digitalWrite(pin_rst, LOW); - delay(10); - digitalWrite(pin_rst, HIGH); - delay(10); - wait(); - } - - sendCommand(0x12); +void SSD16XX::reset() { + // Check if reset pin is defined + if (pin_rst != 0xFF) { + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, LOW); + delay(10); + digitalWrite(pin_rst, HIGH); + delay(10); wait(); + } + + sendCommand(0x12); + wait(); } -void SSD16XX::sendCommand(const uint8_t command) -{ - // Abort if part of the update sequence failed - // This will unlock again once we have failed-through the entire process - if (failed) - return; +void SSD16XX::sendCommand(const uint8_t command) { + // Abort if part of the update sequence failed + // This will unlock again once we have failed-through the entire process + if (failed) + return; - // Take firmware's SPI lock - spiLock->lock(); + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, LOW); // DC pin low indicates command - digitalWrite(pin_cs, LOW); - spi->transfer(command); - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); // DC pin low indicates command + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void SSD16XX::sendData(uint8_t data) -{ - sendData(&data, 1); -} +void SSD16XX::sendData(uint8_t data) { sendData(&data, 1); } -void SSD16XX::sendData(const uint8_t *data, uint32_t size) -{ - // Abort if part of the update sequence failed - // This will unlock again once we have failed-through the entire process - if (failed) - return; +void SSD16XX::sendData(const uint8_t *data, uint32_t size) { + // Abort if part of the update sequence failed + // This will unlock again once we have failed-through the entire process + if (failed) + return; - // Take firmware's SPI lock - spiLock->lock(); + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command - digitalWrite(pin_cs, LOW); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + digitalWrite(pin_cs, LOW); - // Platform-specific SPI command + // Platform-specific SPI command #if defined(ARCH_ESP32) - spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer + spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer #elif defined(ARCH_NRF52) - spi->transfer(data, NULL, size); // NULL for a "write only" transfer + spi->transfer(data, NULL, size); // NULL for a "write only" transfer #else #error Not implemented yet? Feel free to add other platforms here. #endif - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void SSD16XX::configFullscreen() -{ - // Placing this code in a separate method because it's probably pretty consistent between displays - // Should make it tidier to override SSD16XX::configure +void SSD16XX::configFullscreen() { + // Placing this code in a separate method because it's probably pretty consistent between displays + // Should make it tidier to override SSD16XX::configure - // Define the boundaries of the "fullscreen" region, for the controller IC - static const uint16_t sx = bufferOffsetX; // Notice the offset - static const uint16_t sy = 0; - static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this - static const uint16_t ey = height; + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint16_t sx = bufferOffsetX; // Notice the offset + static const uint16_t sy = 0; + static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint16_t ey = height; - // Split into bytes - static const uint8_t sy1 = sy & 0xFF; - static const uint8_t sy2 = (sy >> 8) & 0xFF; - static const uint8_t ey1 = ey & 0xFF; - static const uint8_t ey2 = (ey >> 8) & 0xFF; + // Split into bytes + static const uint8_t sy1 = sy & 0xFF; + static const uint8_t sy2 = (sy >> 8) & 0xFF; + static const uint8_t ey1 = ey & 0xFF; + static const uint8_t ey2 = (ey >> 8) & 0xFF; - // Data entry mode - Left to Right, Top to Bottom - sendCommand(0x11); - sendData(0x03); + // Data entry mode - Left to Right, Top to Bottom + sendCommand(0x11); + sendData(0x03); - // Select controller IC memory region to display a fullscreen image - sendCommand(0x44); // Memory X start - end - sendData(sx); - sendData(ex); - sendCommand(0x45); // Memory Y start - end - sendData(sy1); - sendData(sy2); - sendData(ey1); - sendData(ey2); + // Select controller IC memory region to display a fullscreen image + sendCommand(0x44); // Memory X start - end + sendData(sx); + sendData(ex); + sendCommand(0x45); // Memory Y start - end + sendData(sy1); + sendData(sy2); + sendData(ey1); + sendData(ey2); - // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 - sendCommand(0x4E); // Memory cursor X - sendData(sx); - sendCommand(0x4F); // Memory cursor y - sendData(sy1); - sendData(sy2); + // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 + sendCommand(0x4E); // Memory cursor X + sendData(sx); + sendCommand(0x4F); // Memory cursor y + sendData(sy1); + sendData(sy2); } -void SSD16XX::update(uint8_t *imageData, UpdateTypes type) -{ - this->updateType = type; - this->buffer = imageData; +void SSD16XX::update(uint8_t *imageData, UpdateTypes type) { + this->updateType = type; + this->buffer = imageData; - reset(); + reset(); - configFullscreen(); - configScanning(); // Virtual, unused by base class - configVoltages(); // Virtual, unused by base class - configWaveform(); // Virtual, unused by base class - wait(); + configFullscreen(); + configScanning(); // Virtual, unused by base class + configVoltages(); // Virtual, unused by base class + configWaveform(); // Virtual, unused by base class + wait(); - if (updateType == FULL) { - writeNewImage(); - writeOldImage(); - } else { - writeNewImage(); - } + if (updateType == FULL) { + writeNewImage(); + writeOldImage(); + } else { + writeNewImage(); + } - configUpdateSequence(); - sendCommand(0x20); // Begin executing the update + configUpdateSequence(); + sendCommand(0x20); // Begin executing the update - // Let the update run async, on display hardware. Base class will poll completion, then finalize. - // For a blocking update, call await after update - detachFromUpdate(); + // Let the update run async, on display hardware. Base class will poll completion, then finalize. + // For a blocking update, call await after update + detachFromUpdate(); } // Send SPI commands for controller IC to begin executing the refresh operation -void SSD16XX::configUpdateSequence() -{ - switch (updateType) { - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } +void SSD16XX::configUpdateSequence() { + switch (updateType) { + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } -void SSD16XX::writeNewImage() -{ - sendCommand(0x24); - sendData(buffer, bufferSize); +void SSD16XX::writeNewImage() { + sendCommand(0x24); + sendData(buffer, bufferSize); } -void SSD16XX::writeOldImage() -{ - sendCommand(0x26); - sendData(buffer, bufferSize); +void SSD16XX::writeOldImage() { + sendCommand(0x26); + sendData(buffer, bufferSize); } -void SSD16XX::detachFromUpdate() -{ - // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types - // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed - // If not implemented, we'll just poll right from the get-go - switch (updateType) { - default: - EInk::beginPolling(100, 0); - } +void SSD16XX::detachFromUpdate() { + // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types + // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed + // If not implemented, we'll just poll right from the get-go + switch (updateType) { + default: + EInk::beginPolling(100, 0); + } } -bool SSD16XX::isUpdateDone() -{ - // Busy when HIGH - if (digitalRead(pin_busy) == HIGH) - return false; - else - return true; +bool SSD16XX::isUpdateDone() { + // Busy when HIGH + if (digitalRead(pin_busy) == HIGH) + return false; + else + return true; } -void SSD16XX::finalizeUpdate() -{ - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place - // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. - if (updateType != FULL) { - writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678? - writeOldImage(); - sendCommand(0x7F); // Terminate image write without update - wait(); - } +void SSD16XX::finalizeUpdate() { + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in + // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST + // etc. + if (updateType != FULL) { + writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678? + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } - // Enter deep-sleep to save a few µA - // Waking from this requires that display's reset pin is broken out - if (pin_rst != 0xFF) - deepSleep(); + // Enter deep-sleep to save a few µA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } // Enter a lower-power state // May only save a few µA.. -void SSD16XX::deepSleep() -{ - sendCommand(0x10); // Enter deep sleep - sendData(0x01); // Mode 1: preserve image RAM +void SSD16XX::deepSleep() { + sendCommand(0x10); // Enter deep sleep + sendData(0x01); // Mode 1: preserve image RAM } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.h b/src/graphics/niche/Drivers/EInk/SSD16XX.h index 3f92818ce..232939bbd 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.h +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.h @@ -16,49 +16,47 @@ See DEPG0154BNS800 and DEPG0290BNS800 for examples. #include "./EInk.h" -namespace NicheGraphics::Drivers -{ +namespace NicheGraphics::Drivers { -class SSD16XX : public EInk -{ - public: - SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0); - virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1); - virtual void update(uint8_t *imageData, UpdateTypes type) override; +class SSD16XX : public EInk { +public: + SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1); + virtual void update(uint8_t *imageData, UpdateTypes type) override; - protected: - virtual void wait(uint32_t timeout = 1000); - virtual void reset(); - virtual void sendCommand(const uint8_t command); - virtual void sendData(const uint8_t data); - virtual void sendData(const uint8_t *data, uint32_t size); - virtual void configFullscreen(); // Select memory region on controller IC - virtual void configScanning() {} // Optional. First & last gates, scan direction, etc - virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc - virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc - virtual void configUpdateSequence(); // Tell controller IC which operations to run +protected: + virtual void wait(uint32_t timeout = 1000); + virtual void reset(); + virtual void sendCommand(const uint8_t command); + virtual void sendData(const uint8_t data); + virtual void sendData(const uint8_t *data, uint32_t size); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void configScanning() {} // Optional. First & last gates, scan direction, etc + virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc + virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc + virtual void configUpdateSequence(); // Tell controller IC which operations to run - virtual void writeNewImage(); - virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh" + virtual void writeNewImage(); + virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh" - virtual void detachFromUpdate(); - virtual bool isUpdateDone() override; - virtual void finalizeUpdate() override; - virtual void deepSleep(); + virtual void detachFromUpdate(); + virtual bool isUpdateDone() override; + virtual void finalizeUpdate() override; + virtual void deepSleep(); - protected: - uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? - uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) - uint32_t bufferSize = 0; // In bytes. Rows * Columns - uint8_t *buffer = nullptr; - UpdateTypes updateType = UpdateTypes::UNSPECIFIED; +protected: + uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? + uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) + uint32_t bufferSize = 0; // In bytes. Rows * Columns + uint8_t *buffer = nullptr; + UpdateTypes updateType = UpdateTypes::UNSPECIFIED; - uint8_t pin_dc = -1; - uint8_t pin_cs = -1; - uint8_t pin_busy = -1; - uint8_t pin_rst = -1; - SPIClass *spi = nullptr; - SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); + uint8_t pin_dc = -1; + uint8_t pin_cs = -1; + uint8_t pin_busy = -1; + uint8_t pin_rst = -1; + SPIClass *spi = nullptr; + SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp index e83588905..56b1bbbb9 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp @@ -5,64 +5,60 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void ZJY122250_0213BAAMFGN::configScanning() -{ - // "Driver output control" - // Scan gates from 0 to 249 (vertical resolution 250px) - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); - sendData(0x00); +void ZJY122250_0213BAAMFGN::configScanning() { + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void ZJY122250_0213BAAMFGN::configWaveform() -{ - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x80); // VCOM - break; - case FULL: - default: - sendCommand(0x3C); // Border waveform: - sendData(0x01); // Follow LUT 1 (blink same as white pixels) - break; - } +void ZJY122250_0213BAAMFGN::configWaveform() { + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VCOM + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void ZJY122250_0213BAAMFGN::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void ZJY122250_0213BAAMFGN::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void ZJY122250_0213BAAMFGN::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh - } +void ZJY122250_0213BAAMFGN::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h index 82c4ec107..e1faa30c0 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h @@ -17,24 +17,22 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class ZJY122250_0213BAAMFGN : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class ZJY122250_0213BAAMFGN : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} +public: + ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} - protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp index a8f43420f..62f439873 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp @@ -5,55 +5,51 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void ZJY128296_029EAAMFGN::configScanning() -{ - // "Driver output control" - // Scan gates from 0 to 295 (vertical resolution 296px) - sendCommand(0x01); - sendData(0x27); // Number of gates (295, bits 0-7) - sendData(0x01); // Number of gates (295, bit 8) - sendData(0x00); // (Do not invert scanning order) +void ZJY128296_029EAAMFGN::configScanning() { + // "Driver output control" + // Scan gates from 0 to 295 (vertical resolution 296px) + sendCommand(0x01); + sendData(0x27); // Number of gates (295, bits 0-7) + sendData(0x01); // Number of gates (295, bit 8) + sendData(0x00); // (Do not invert scanning order) } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void ZJY128296_029EAAMFGN::configWaveform() -{ - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void ZJY128296_029EAAMFGN::configWaveform() { + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void ZJY128296_029EAAMFGN::configUpdateSequence() -{ - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void ZJY128296_029EAAMFGN::configUpdateSequence() { + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void ZJY128296_029EAAMFGN::detachFromUpdate() -{ - switch (updateType) { - case FAST: - return beginPolling(50, 300); // At least 300ms for fast refresh - case FULL: - default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh - } +void ZJY128296_029EAAMFGN::detachFromUpdate() { + switch (updateType) { + case FAST: + return beginPolling(50, 300); // At least 300ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h index 27644e709..d718544ae 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h @@ -19,24 +19,22 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers -{ -class ZJY128296_029EAAMFGN : public SSD16XX -{ - // Display properties - private: - static constexpr uint32_t width = 128; - static constexpr uint32_t height = 296; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers { +class ZJY128296_029EAAMFGN : public SSD16XX { + // Display properties +private: + static constexpr uint32_t width = 128; + static constexpr uint32_t height = 296; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); - public: - ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} +public: + ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} - protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; +protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h index fb16bcf2f..af89f3f8e 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h +++ b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h @@ -22,8 +22,7 @@ E-Ink display driver #include "./GDEY0154D67.h" -namespace NicheGraphics::Drivers -{ +namespace NicheGraphics::Drivers { typedef GDEY0154D67 ZJY200200_0154DAAMFGN; diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index 1e89ebe1b..b496e2270 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -13,127 +13,109 @@ InkHUD::AppletFont InkHUD::Applet::fontMedium; InkHUD::AppletFont InkHUD::Applet::fontSmall; constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo -InkHUD::Applet::Applet() : GFX(0, 0) -{ - // GFX is given initial dimensions of 0 - // The width and height will change dynamically, depending on Applet tiling - // If you're getting a "divide by zero error", consider it an assert: - // WindowManager should be the only one controlling the rendering +InkHUD::Applet::Applet() : GFX(0, 0) { + // GFX is given initial dimensions of 0 + // The width and height will change dynamically, depending on Applet tiling + // If you're getting a "divide by zero error", consider it an assert: + // WindowManager should be the only one controlling the rendering - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; - latestMessage = &inkhud->persistence->latestMessage; + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; + latestMessage = &inkhud->persistence->latestMessage; } // Draw a single pixel // The raw pixel output generated by AdafruitGFX drawing all passes through here // Hand off to the applet's tile, which will in-turn pass to the renderer -void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) -{ - // Only render pixels if they fall within user's cropped region - if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) - assignedTile->handleAppletPixel(x, y, (Color)color); +void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) { + // Only render pixels if they fall within user's cropped region + if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) + assignedTile->handleAppletPixel(x, y, (Color)color); } // Link our applet to a tile // This can only be called by Tile::assignApplet // The tile determines the applets dimensions // Pixel output is passed to tile during render() -void InkHUD::Applet::setTile(Tile *t) -{ - // If we're setting (not clearing), make sure the link is "reciprocal" - if (t) - assert(t->getAssignedApplet() == this); +void InkHUD::Applet::setTile(Tile *t) { + // If we're setting (not clearing), make sure the link is "reciprocal" + if (t) + assert(t->getAssignedApplet() == this); - assignedTile = t; + assignedTile = t; } // The tile to which our applet is assigned -InkHUD::Tile *InkHUD::Applet::getTile() -{ - return assignedTile; -} +InkHUD::Tile *InkHUD::Applet::getTile() { return assignedTile; } // Draw the applet -void InkHUD::Applet::render() -{ - assert(assignedTile); // Ensure that we have a tile - assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile +void InkHUD::Applet::render() { + assert(assignedTile); // Ensure that we have a tile + assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile - // WindowManager::update has now consumed the info about our update request - // Clear everything for future requests - wantRender = false; // Flag set by requestUpdate - wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. - wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. + // WindowManager::update has now consumed the info about our update request + // Clear everything for future requests + wantRender = false; // Flag set by requestUpdate + wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. + wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. - updateDimensions(); - resetDrawingSpace(); - onRender(); // Derived applet's drawing takes place here + updateDimensions(); + resetDrawingSpace(); + onRender(); // Derived applet's drawing takes place here - // Handle "Tile Highlighting" - // Some devices may use an auxiliary button to switch between tiles - // When this happens, we temporarily highlight the newly focused tile with a border + // Handle "Tile Highlighting" + // Some devices may use an auxiliary button to switch between tiles + // When this happens, we temporarily highlight the newly focused tile with a border - // If our tile is (or was) highlighted, to indicate a change in focus - if (Tile::highlightTarget == assignedTile) { - // Draw the highlight - if (!Tile::highlightShown) { - drawRect(0, 0, width(), height(), BLACK); - Tile::startHighlightTimeout(); - Tile::highlightShown = true; - } - - // Clear the highlight - else { - Tile::cancelHighlightTimeout(); - Tile::highlightShown = false; - Tile::highlightTarget = nullptr; - } + // If our tile is (or was) highlighted, to indicate a change in focus + if (Tile::highlightTarget == assignedTile) { + // Draw the highlight + if (!Tile::highlightShown) { + drawRect(0, 0, width(), height(), BLACK); + Tile::startHighlightTimeout(); + Tile::highlightShown = true; } + + // Clear the highlight + else { + Tile::cancelHighlightTimeout(); + Tile::highlightShown = false; + Tile::highlightTarget = nullptr; + } + } } // Does the applet want to render now? // Checks whether the applet called requestUpdate recently, in response to an event // Used by WindowManager::update -bool InkHUD::Applet::wantsToRender() -{ - return wantRender; -} +bool InkHUD::Applet::wantsToRender() { return wantRender; } // Does the applet want to be moved to foreground before next render, to show new data? // User specifies whether an applet has permission for this, using the on-screen menu // Used by WindowManager::update -bool InkHUD::Applet::wantsToAutoshow() -{ - return wantAutoshow; -} +bool InkHUD::Applet::wantsToAutoshow() { return wantAutoshow; } // Which technique would this applet prefer that the display use to change the image? // Used by WindowManager::update -Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() -{ - return wantUpdateType; -} +Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() { return wantUpdateType; } // Get size of the applet's drawing space from its tile // Performed immediately before derived applet's drawing code runs -void InkHUD::Applet::updateDimensions() -{ - assert(assignedTile); - WIDTH = assignedTile->getWidth(); - HEIGHT = assignedTile->getHeight(); - _width = WIDTH; - _height = HEIGHT; +void InkHUD::Applet::updateDimensions() { + assert(assignedTile); + WIDTH = assignedTile->getWidth(); + HEIGHT = assignedTile->getHeight(); + _width = WIDTH; + _height = HEIGHT; } // Ensure that render() always starts with the same initial drawing config -void InkHUD::Applet::resetDrawingSpace() -{ - resetCrop(); // Allow pixel from any region of the applet to draw - setTextColor(BLACK); // Reset text params - setCursor(0, 0); - setTextWrap(false); - setFont(fontSmall); +void InkHUD::Applet::resetDrawingSpace() { + resetCrop(); // Allow pixel from any region of the applet to draw + setTextColor(BLACK); // Reset text params + setCursor(0, 0); + setTextWrap(false); + setFont(fontSmall); } // Tell InkHUD::Renderer that we want to render now @@ -142,29 +124,24 @@ void InkHUD::Applet::resetDrawingSpace() // Once the renderer has given other applets a chance to process whatever event we just detected, // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow -void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) -{ - wantRender = true; - wantUpdateType = type; - inkhud->requestUpdate(); +void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) { + wantRender = true; + wantUpdateType = type; + inkhud->requestUpdate(); } // Ask window manager to move this applet to foreground at start of next render // Users select which applets have permission for this using the on-screen menu -void InkHUD::Applet::requestAutoshow() -{ - wantAutoshow = true; -} +void InkHUD::Applet::requestAutoshow() { wantAutoshow = true; } // Called when an Applet begins running // Active applets are considered "enabled" // They should now listen for events, and request their own updates // They may also be unexpectedly renderer at any time by other InkHUD components // Applets can be activated at run-time through the on-screen menu -void InkHUD::Applet::activate() -{ - onActivate(); // Call derived class' handler - active = true; +void InkHUD::Applet::activate() { + onActivate(); // Call derived class' handler + active = true; } // Called when an Applet stops running @@ -172,48 +149,42 @@ void InkHUD::Applet::activate() // They should not listen for events, process data // They will not be rendered // Applets can be deactivated at run-time through the on-screen menu -void InkHUD::Applet::deactivate() -{ - // If applet is still in foreground, run its onBackground code first - if (isForeground()) - sendToBackground(); +void InkHUD::Applet::deactivate() { + // If applet is still in foreground, run its onBackground code first + if (isForeground()) + sendToBackground(); - // If applet is active, run its onDeactivate code first - if (isActive()) - onDeactivate(); // Derived class' handler - active = false; + // If applet is active, run its onDeactivate code first + if (isActive()) + onDeactivate(); // Derived class' handler + active = false; } // Is the Applet running? // Note: active / inactive is not related to background / foreground // An inactive applet is *fully* disabled -bool InkHUD::Applet::isActive() -{ - return active; -} +bool InkHUD::Applet::isActive() { return active; } // Begin showing the Applet // It will be rendered immediately to whichever tile it is assigned // The Renderer will also now honor requestUpdate() calls from this applet -void InkHUD::Applet::bringToForeground() -{ - if (!foreground) { - foreground = true; - onForeground(); // Run derived applet class' handler - } +void InkHUD::Applet::bringToForeground() { + if (!foreground) { + foreground = true; + onForeground(); // Run derived applet class' handler + } - requestUpdate(); + requestUpdate(); } // Stop showing the Applet // Calls to requestUpdate() will no longer be honored // When one applet moves to background, another should move to foreground (exception: some system applets) -void InkHUD::Applet::sendToBackground() -{ - if (foreground) { - foreground = false; - onBackground(); // Run derived applet class' handler - } +void InkHUD::Applet::sendToBackground() { + if (foreground) { + foreground = false; + onBackground(); // Run derived applet class' handler + } } // Is the applet currently displayed on a tile @@ -221,534 +192,493 @@ void InkHUD::Applet::sendToBackground() // This can occur when a system applet is covering the screen (e.g. during BLE pairing) // This is not our applets responsibility to handle, // as in those situations, the system applet will have "locked" rendering -bool InkHUD::Applet::isForeground() -{ - return foreground; -} +bool InkHUD::Applet::isForeground() { return foreground; } // Limit drawing to a certain region of the applet // Pixels outside this region will be discarded -void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) -{ - cropLeft = left; - cropTop = top; - cropWidth = width; - cropHeight = height; +void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) { + cropLeft = left; + cropTop = top; + cropWidth = width; + cropHeight = height; } // Allow drawing to any region of the Applet // Reverses Applet::setCrop -void InkHUD::Applet::resetCrop() -{ - setCrop(0, 0, width(), height()); -} +void InkHUD::Applet::resetCrop() { setCrop(0, 0, width(), height()); } // Convert relative width to absolute width, in px // X(0) is 0 // X(0.5) is width() / 2 // X(1) is width() -uint16_t InkHUD::Applet::X(float f) -{ - return width() * f; -} +uint16_t InkHUD::Applet::X(float f) { return width() * f; } // Convert relative hight to absolute height, in px // Y(0) is 0 // Y(0.5) is height() / 2 // Y(1) is height() -uint16_t InkHUD::Applet::Y(float f) -{ - return height() * f; +uint16_t InkHUD::Applet::Y(float f) { return height() * f; } + +// Print text, specifying the position of any edge / corner of the textbox +void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) { + // We do still have to run getTextBounds to find the width + int16_t textOffsetX, textOffsetY; + uint16_t textWidth, textHeight; + getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); + + int16_t cursorX = 0; + int16_t cursorY = 0; + + switch (ha) { + case LEFT: + cursorX = x - textOffsetX; + break; + case CENTER: + cursorX = (x - textOffsetX) - (textWidth / 2); + break; + case RIGHT: + cursorX = (x - textOffsetX) - textWidth; + break; + } + + // We're using a fixed line height, rather than sizing to text (getTextBounds) + + switch (va) { + case TOP: + cursorY = y + currentFont.heightAboveCursor(); + break; + case MIDDLE: + cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); + break; + case BOTTOM: + cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); + break; + } + + setCursor(cursorX, cursorY); + print(text); } // Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) -{ - // We do still have to run getTextBounds to find the width - int16_t textOffsetX, textOffsetY; - uint16_t textWidth, textHeight; - getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); - - int16_t cursorX = 0; - int16_t cursorY = 0; - - switch (ha) { - case LEFT: - cursorX = x - textOffsetX; - break; - case CENTER: - cursorX = (x - textOffsetX) - (textWidth / 2); - break; - case RIGHT: - cursorX = (x - textOffsetX) - textWidth; - break; - } - - // We're using a fixed line height, rather than sizing to text (getTextBounds) - - switch (va) { - case TOP: - cursorY = y + currentFont.heightAboveCursor(); - break; - case MIDDLE: - cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); - break; - case BOTTOM: - cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); - break; - } - - setCursor(cursorX, cursorY); - print(text); -} - -// Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) -{ - printAt(x, y, text.c_str(), ha, va); +void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) { + printAt(x, y, text.c_str(), ha, va); } // Set which font should be used for subsequent drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data -void InkHUD::Applet::setFont(AppletFont f) -{ - GFX::setFont(f.gfxFont); - currentFont = f; +void InkHUD::Applet::setFont(AppletFont f) { + GFX::setFont(f.gfxFont); + currentFont = f; } // Get which font is currently being used for drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data -InkHUD::AppletFont InkHUD::Applet::getFont() -{ - return currentFont; -} +InkHUD::AppletFont InkHUD::Applet::getFont() { return currentFont; } // Parse any text which might have "special characters" // Re-encodes UTF-8 characters to match our 8-bit encoded fonts -std::string InkHUD::Applet::parse(std::string text) -{ - return getFont().decodeUTF8(text); -} +std::string InkHUD::Applet::parse(std::string text) { return getFont().decodeUTF8(text); } // Get the best version of a node's short name available to us // Parses any non-ascii chars // Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji) -std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) -{ - assert(node); +std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) { + assert(node); - // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) - if (node->has_user) { - std::string parsed = parse(node->user.short_name); - if (isPrintable(parsed)) - return parsed; - } + // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) + if (node->has_user) { + std::string parsed = parse(node->user.short_name); + if (isPrintable(parsed)) + return parsed; + } - // Otherwise, use the "last 4" of node id - // - if short name unknown, or - // - if short name is emoji (we can't render this) - std::string nodeID = hexifyNodeNum(node->num); - return nodeID.substr(nodeID.length() - 4); + // Otherwise, use the "last 4" of node id + // - if short name unknown, or + // - if short name is emoji (we can't render this) + std::string nodeID = hexifyNodeNum(node->num); + return nodeID.substr(nodeID.length() - 4); } // Determine if all characters of a string are printable using the current font -bool InkHUD::Applet::isPrintable(std::string text) -{ - // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled - for (char &c : text) { - if (c == '\x1A') - return false; - } +bool InkHUD::Applet::isPrintable(std::string text) { + // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled + for (char &c : text) { + if (c == '\x1A') + return false; + } - // No unprintable characters found - return true; + // No unprintable characters found + return true; } // Gets rendered width of a string // Wrapper for getTextBounds -uint16_t InkHUD::Applet::getTextWidth(const char *text) -{ - // We do still have to run getTextBounds to find the width - int16_t textOffsetX, textOffsetY; - uint16_t textWidth, textHeight; - getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); +uint16_t InkHUD::Applet::getTextWidth(const char *text) { + // We do still have to run getTextBounds to find the width + int16_t textOffsetX, textOffsetY; + uint16_t textWidth, textHeight; + getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); - return textWidth; + return textWidth; } // Gets rendered width of a string // Wrapper for getTextBounds -uint16_t InkHUD::Applet::getTextWidth(std::string text) -{ - return getTextWidth(text.c_str()); -} +uint16_t InkHUD::Applet::getTextWidth(std::string text) { return getTextWidth(text.c_str()); } // Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels // Roughly comparable to values used by the iOS app; // I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator -InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) -{ - uint8_t score = 0; +InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) { + uint8_t score = 0; - // Give a score for the SNR - if (snr > -17.5) - score += 2; - else if (snr > -26.0) - score += 1; + // Give a score for the SNR + if (snr > -17.5) + score += 2; + else if (snr > -26.0) + score += 1; - // Give a score for the RSSI - if (rssi > -115.0) - score += 3; - else if (rssi > -120.0) - score += 2; - else if (rssi > -126.0) - score += 1; + // Give a score for the RSSI + if (rssi > -115.0) + score += 3; + else if (rssi > -120.0) + score += 2; + else if (rssi > -126.0) + score += 1; - // Combine scores, then give a result - if (score >= 5) - return SIGNAL_GOOD; - else if (score >= 4) - return SIGNAL_FAIR; - else if (score > 0) - return SIGNAL_BAD; - else - return SIGNAL_NONE; + // Combine scores, then give a result + if (score >= 5) + return SIGNAL_GOOD; + else if (score >= 4) + return SIGNAL_FAIR; + else if (score > 0) + return SIGNAL_BAD; + else + return SIGNAL_NONE; } // Apply the standard "node id" formatting to a nodenum int: !0123abdc -std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) -{ - // Not found in nodeDB, show a hex nodeid instead - char nodeIdHex[10]; - sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format - return std::string(nodeIdHex); +std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) { + // Not found in nodeDB, show a hex nodeid instead + char nodeIdHex[10]; + sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format + return std::string(nodeIdHex); } // Print text, with word wrapping // Avoids splitting words in half, instead moving the entire word to a new line wherever possible -void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) -{ - // Place the AdafruitGFX cursor to suit our "top" coord - setCursor(left, top + getFont().heightAboveCursor()); +void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) { + // Place the AdafruitGFX cursor to suit our "top" coord + setCursor(left, top + getFont().heightAboveCursor()); - // How wide a space character is - // Used when simulating print, for dimensioning - // Works around issues where getTextDimensions() doesn't account for whitespace - const uint8_t wSp = getFont().widthBetweenWords(); + // How wide a space character is + // Used when simulating print, for dimensioning + // Works around issues where getTextDimensions() doesn't account for whitespace + const uint8_t wSp = getFont().widthBetweenWords(); - // Move through our text, character by character - uint16_t wordStart = 0; - for (uint16_t i = 0; i < text.length(); i++) { + // Move through our text, character by character + uint16_t wordStart = 0; + for (uint16_t i = 0; i < text.length(); i++) { - // Found: end of word (split by spaces or newline) - // Also handles end of string - if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { - // Isolate this word - uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 - std::string word = text.substr(wordStart, wordLength); - wordStart = i + 1; // Next word starts *after* the space + // Found: end of word (split by spaces or newline) + // Also handles end of string + if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { + // Isolate this word + uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 + std::string word = text.substr(wordStart, wordLength); + wordStart = i + 1; // Next word starts *after* the space - // If word is terminated by a newline char, don't actually print it. - // We'll manually add a new line later - if (word.back() == '\n') - word.pop_back(); + // If word is terminated by a newline char, don't actually print it. + // We'll manually add a new line later + if (word.back() == '\n') + word.pop_back(); - // Measure the word, in px - int16_t l, t; - uint16_t w, h; - getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); + // Measure the word, in px + int16_t l, t; + uint16_t w, h; + getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); - // Word is short - if (w < width) { - // Word fits on current line - if ((l + w + wSp) < left + width) - print(word.c_str()); + // Word is short + if (w < width) { + // Word fits on current line + if ((l + w + wSp) < left + width) + print(word.c_str()); - // Word doesn't fit on current line - else { - setCursor(left, getCursorY() + getFont().lineHeight()); // Newline - print(word.c_str()); - } - } - - // Word is really long - // (wider than applet) - else { - // Horribly inefficient: - // Rather than working directly with the glyph sizes, - // we're going to run everything through getTextBounds as a c-string of length 1 - // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, - // which would be a pain to add manually here. - // These super-long strings probably don't come up often so we can maybe tolerate this. - - // Todo: rewrite making use of AdafruitGFX native text wrapping - char cstr[] = {0, 0}; - int16_t l, t; - uint16_t w, h; - for (uint16_t c = 0; c < word.length(); c++) { - // Shove next char into a c string - cstr[0] = word[c]; - getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); - - // Manual newline, if next character will spill beyond screen edge - if ((l + w) > left + width) - setCursor(left, getCursorY() + getFont().lineHeight()); - - // Print next character - print(word[c]); - } - } + // Word doesn't fit on current line + else { + setCursor(left, getCursorY() + getFont().lineHeight()); // Newline + print(word.c_str()); } + } - // If word was terminated by a newline char, manually add the new line now - if (text[i] == '\n') { - setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline - wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line + // Word is really long + // (wider than applet) + else { + // Horribly inefficient: + // Rather than working directly with the glyph sizes, + // we're going to run everything through getTextBounds as a c-string of length 1 + // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, + // which would be a pain to add manually here. + // These super-long strings probably don't come up often so we can maybe tolerate this. + + // Todo: rewrite making use of AdafruitGFX native text wrapping + char cstr[] = {0, 0}; + int16_t l, t; + uint16_t w, h; + for (uint16_t c = 0; c < word.length(); c++) { + // Shove next char into a c string + cstr[0] = word[c]; + getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); + + // Manual newline, if next character will spill beyond screen edge + if ((l + w) > left + width) + setCursor(left, getCursorY() + getFont().lineHeight()); + + // Print next character + print(word[c]); } + } } + + // If word was terminated by a newline char, manually add the new line now + if (text[i] == '\n') { + setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline + wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line + } + } } // Simulate running printWrapped, to determine how tall the block of text will be. // This is a wasteful way of handling things. Maybe some way to optimize in future? -uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) -{ - // Cache the current crop region - int16_t cL = cropLeft; - int16_t cT = cropTop; - uint16_t cW = cropWidth; - uint16_t cH = cropHeight; +uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) { + // Cache the current crop region + int16_t cL = cropLeft; + int16_t cT = cropTop; + uint16_t cW = cropWidth; + uint16_t cH = cropHeight; - setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels - printWrapped(left, 0, width, text); // Simulate only - no pixels drawn + setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels + printWrapped(left, 0, width, text); // Simulate only - no pixels drawn - // Restore previous crop region - cropLeft = cL; - cropTop = cT; - cropWidth = cW; - cropHeight = cH; + // Restore previous crop region + cropLeft = cL; + cropTop = cT; + cropWidth = cW; + cropHeight = cH; - // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, - // so we need to account for that when determining the height - return (getCursorY() + getFont().heightBelowCursor()); + // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, + // so we need to account for that when determining the height + return (getCursorY() + getFont().heightBelowCursor()); } // Fill a region with sparse diagonal lines, to create a pseudo-translucent fill -void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) -{ - // Cache the currently cropped region - int16_t oldCropL = cropLeft; - int16_t oldCropT = cropTop; - uint16_t oldCropW = cropWidth; - uint16_t oldCropH = cropHeight; +void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) { + // Cache the currently cropped region + int16_t oldCropL = cropLeft; + int16_t oldCropT = cropTop; + uint16_t oldCropW = cropWidth; + uint16_t oldCropH = cropHeight; - setCrop(x, y, w, h); + setCrop(x, y, w, h); - // Draw lines starting along the top edge, every few px - for (int16_t ix = x; ix < x + w; ix += spacing) { - for (int16_t i = 0; i < w || i < h; i++) { - drawPixel(ix + i, y + i, color); - } + // Draw lines starting along the top edge, every few px + for (int16_t ix = x; ix < x + w; ix += spacing) { + for (int16_t i = 0; i < w || i < h; i++) { + drawPixel(ix + i, y + i, color); } + } - // Draw lines starting along the left edge, every few px - for (int16_t iy = y; iy < y + h; iy += spacing) { - for (int16_t i = 0; i < w || i < h; i++) { - drawPixel(x + i, iy + i, color); - } + // Draw lines starting along the left edge, every few px + for (int16_t iy = y; iy < y + h; iy += spacing) { + for (int16_t i = 0; i < w || i < h; i++) { + drawPixel(x + i, iy + i, color); } + } - // Restore any previous crop - // If none was set, this will clear - cropLeft = oldCropL; - cropTop = oldCropT; - cropWidth = oldCropW; - cropHeight = oldCropH; + // Restore any previous crop + // If none was set, this will clear + cropLeft = oldCropL; + cropTop = oldCropT; + cropWidth = oldCropW; + cropHeight = oldCropH; } // Get a human readable time representation of an epoch time (seconds since 1970) // If time is invalid, this will be an empty string -std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) -{ +std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) { #ifdef BUILD_EPOCH - constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build + constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build #else - constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT + constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT #endif - uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); + uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); - int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; - int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; + int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; + int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; - // Times are invalid: rtc is much older than when code was built - // Don't give any human readable string - if (epochNow <= validAfterEpoch) - return ""; + // Times are invalid: rtc is much older than when code was built + // Don't give any human readable string + if (epochNow <= validAfterEpoch) + return ""; - // Times are invalid: argument time is significantly ahead of RTC - // Don't give any human readable string - if (daysAgo < -2) - return ""; + // Times are invalid: argument time is significantly ahead of RTC + // Don't give any human readable string + if (daysAgo < -2) + return ""; - // Times are probably invalid: more than 6 months ago - if (daysAgo > 6 * 30) - return ""; + // Times are probably invalid: more than 6 months ago + if (daysAgo > 6 * 30) + return ""; - if (daysAgo > 1) - return to_string(daysAgo) + " days ago"; + if (daysAgo > 1) + return to_string(daysAgo) + " days ago"; - else if (hoursAgo > 18) - return "Yesterday"; + else if (hoursAgo > 18) + return "Yesterday"; - else { + else { - uint32_t hms = epochSeconds % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + uint32_t hms = epochSeconds % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - // Tear apart hms into h:m - uint32_t hour = hms / SEC_PER_HOUR; - uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + // Tear apart hms into h:m + uint32_t hour = hms / SEC_PER_HOUR; + uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - // Format the clock string, either 12 hour or 24 hour - char clockStr[11]; - if (config.display.use_12h_clock) - sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); - else - sprintf(clockStr, "%02u:%02u", hour, min); + // Format the clock string, either 12 hour or 24 hour + char clockStr[11]; + if (config.display.use_12h_clock) + sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); + else + sprintf(clockStr, "%02u:%02u", hour, min); - return clockStr; - } + return clockStr; + } } // If no argument specified, get time string for the current RTC time -std::string InkHUD::Applet::getTimeString() -{ - return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); -} +std::string InkHUD::Applet::getTimeString() { return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); } // Calculate how many nodes have been seen within our preferred window of activity // This period is set by user, via the menu // Todo: optimize to calculate once only per WindowManager::render -uint16_t InkHUD::Applet::getActiveNodeCount() -{ - // Don't even try to count nodes if RTC isn't set - // The last heard values in nodedb will be incomprehensible - if (getRTCQuality() == RTCQualityNone) - return 0; +uint16_t InkHUD::Applet::getActiveNodeCount() { + // Don't even try to count nodes if RTC isn't set + // The last heard values in nodedb will be incomprehensible + if (getRTCQuality() == RTCQualityNone) + return 0; - uint16_t count = 0; + uint16_t count = 0; - // For each node in db - for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // For each node in db + for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Check if heard recently, and not our own node - if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) - count++; - } + // Check if heard recently, and not our own node + if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) + count++; + } - return count; + return count; } // Get an abbreviated, human readable, distance string // Honors config.display.units, to offer both metric and imperial -std::string InkHUD::Applet::localizeDistance(uint32_t meters) -{ - constexpr float FEET_PER_METER = 3.28084; - constexpr uint16_t FEET_PER_MILE = 5280; +std::string InkHUD::Applet::localizeDistance(uint32_t meters) { + constexpr float FEET_PER_METER = 3.28084; + constexpr uint16_t FEET_PER_MILE = 5280; - // Resulting string - std::string localized; + // Resulting string + std::string localized; - // Imperial - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - uint32_t feet = meters * FEET_PER_METER; - // Distant (miles, rounded) - if (feet > FEET_PER_MILE / 2) { - localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); - localized += "mi"; - } - // Nearby (feet) - else { - localized += to_string(feet); - localized += "ft"; - } + // Imperial + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + uint32_t feet = meters * FEET_PER_METER; + // Distant (miles, rounded) + if (feet > FEET_PER_MILE / 2) { + localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); + localized += "mi"; } - - // Metric + // Nearby (feet) else { - // Distant (kilometers, rounded) - if (meters >= 500) { - localized += to_string((uint32_t)roundf(meters / 1000.0)); - localized += "km"; - } - // Nearby (meters) - else { - localized += to_string(meters); - localized += "m"; - } + localized += to_string(feet); + localized += "ft"; } + } - return localized; + // Metric + else { + // Distant (kilometers, rounded) + if (meters >= 500) { + localized += to_string((uint32_t)roundf(meters / 1000.0)); + localized += "km"; + } + // Nearby (meters) + else { + localized += to_string(meters); + localized += "m"; + } + } + + return localized; } // Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly -void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) -{ - // How many times to draw along x axis - int16_t xStart; - int16_t xEnd; - switch (thicknessX) { - case 0: - assert(false); - case 1: - xStart = xCenter; - xEnd = xCenter; - break; - case 2: - xStart = xCenter; - xEnd = xCenter + 1; - break; - default: - xStart = xCenter - (thicknessX / 2); - xEnd = xCenter + (thicknessX / 2); - } +void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) { + // How many times to draw along x axis + int16_t xStart; + int16_t xEnd; + switch (thicknessX) { + case 0: + assert(false); + case 1: + xStart = xCenter; + xEnd = xCenter; + break; + case 2: + xStart = xCenter; + xEnd = xCenter + 1; + break; + default: + xStart = xCenter - (thicknessX / 2); + xEnd = xCenter + (thicknessX / 2); + } - // How many times to draw along Y axis - int16_t yStart; - int16_t yEnd; - switch (thicknessY) { - case 0: - assert(false); - case 1: - yStart = yCenter; - yEnd = yCenter; - break; - case 2: - yStart = yCenter; - yEnd = yCenter + 1; - break; - default: - yStart = yCenter - (thicknessY / 2); - yEnd = yCenter + (thicknessY / 2); - } + // How many times to draw along Y axis + int16_t yStart; + int16_t yEnd; + switch (thicknessY) { + case 0: + assert(false); + case 1: + yStart = yCenter; + yEnd = yCenter; + break; + case 2: + yStart = yCenter; + yEnd = yCenter + 1; + break; + default: + yStart = yCenter - (thicknessY / 2); + yEnd = yCenter + (thicknessY / 2); + } - // Print multiple times, overlapping - for (int16_t x = xStart; x <= xEnd; x++) { - for (int16_t y = yStart; y <= yEnd; y++) { - printAt(x, y, text, CENTER, MIDDLE); - } + // Print multiple times, overlapping + for (int16_t x = xStart; x <= xEnd; x++) { + for (int16_t y = yStart; y <= yEnd; y++) { + printAt(x, y, text, CENTER, MIDDLE); } + } } // Allow this applet to suppress notifications // Asked before a notification is shown via the NotificationApplet // An applet might want to suppress a notification if the applet itself already displays this info // Example: AllMessageApplet should not approve notifications for messages, if it is in foreground -bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) -{ - // By default, no objection - return true; +bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) { + // By default, no objection + return true; } // Draw the standard header, used by most Applets @@ -761,60 +691,56 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) │ │ └───────────────────────────────┘ */ -void InkHUD::Applet::drawHeader(std::string text) -{ - // Y position for divider - // - between header text and messages - constexpr int16_t padDivH = 2; - const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; +void InkHUD::Applet::drawHeader(std::string text) { + // Y position for divider + // - between header text and messages + constexpr int16_t padDivH = 2; + const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; - // Print header - printAt(0, padDivH, text); + // Print header + printAt(0, padDivH, text); - // Divider - // - below header text: separates message - // - above header text: separates other applets - for (int16_t x = 0; x < width(); x += 2) { - drawPixel(x, 0, BLACK); - drawPixel(x, headerDivY, BLACK); // Dotted 50% - } + // Divider + // - below header text: separates message + // - above header text: separates other applets + for (int16_t x = 0; x < width(); x += 2) { + drawPixel(x, 0, BLACK); + drawPixel(x, headerDivY, BLACK); // Dotted 50% + } } // Get the height of the standard applet header // This will vary, depending on font // Applets use this value to avoid drawing overtop the header -uint16_t InkHUD::Applet::getHeaderHeight() -{ - // Y position for divider - // - between header text and messages - constexpr int16_t padDivH = 2; - const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; +uint16_t InkHUD::Applet::getHeaderHeight() { + // Y position for divider + // - between header text and messages + constexpr int16_t padDivH = 2; + const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; - return headerDivY + 1; // "Plus one": height is always one more than Y position + return headerDivY + 1; // "Plus one": height is always one more than Y position } // "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio -uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) -{ - // Determine whether we're limited by width or height - // Makes sure we draw the logo as large as possible, within the specified region, - // while still maintaining correct aspect ratio - if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) - return limitHeight * LOGO_ASPECT_RATIO; - else - return limitWidth; +uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) { + // Determine whether we're limited by width or height + // Makes sure we draw the logo as large as possible, within the specified region, + // while still maintaining correct aspect ratio + if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) + return limitHeight * LOGO_ASPECT_RATIO; + else + return limitWidth; } // "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio -uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) -{ - // Determine whether we're limited by width or height - // Makes sure we draw the logo as large as possible, within the specified region, - // while still maintaining correct aspect ratio - if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) - return limitWidth / LOGO_ASPECT_RATIO; - else - return limitHeight; +uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) { + // Determine whether we're limited by width or height + // Makes sure we draw the logo as large as possible, within the specified region, + // while still maintaining correct aspect ratio + if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) + return limitWidth / LOGO_ASPECT_RATIO; + else + return limitHeight; } // Draw a scalable Meshtastic logo @@ -829,150 +755,149 @@ uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight // // \\ */ -void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color) -{ - struct Point { - int x; - int y; - }; - typedef Point Distance; +void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color) { + struct Point { + int x; + int y; + }; + typedef Point Distance; - int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. - int16_t logoL = centerX - (width / 2) + (logoTh / 2); - int16_t logoT = centerY - (height / 2) + (logoTh / 2); - int16_t logoW = width - logoTh; - int16_t logoH = height - logoTh; - int16_t logoR = logoL + logoW - 1; - int16_t logoB = logoT + logoH - 1; + int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. + int16_t logoL = centerX - (width / 2) + (logoTh / 2); + int16_t logoT = centerY - (height / 2) + (logoTh / 2); + int16_t logoW = width - logoTh; + int16_t logoH = height - logoTh; + int16_t logoR = logoL + logoW - 1; + int16_t logoB = logoT + logoH - 1; - // Points for paths (a, b, and c) - /* - +-----------------------------+ - --| a2 b2/c1 | - | | - | | - | | - --| a1 b1 c2 | - +-----------------------------+ - | | | | - */ + // Points for paths (a, b, and c) + /* + +-----------------------------+ + --| a2 b2/c1 | + | | + | | + | | + --| a1 b1 c2 | + +-----------------------------+ + | | | | + */ - Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; - Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; - Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; - Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; - Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; - Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; + Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; + Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; + Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; + Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; + Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; + Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; - // Find angle of the path(s) - // Used to thicken the single pixel paths - /* - +-------------------------------+ - | a2 | - | -| | - | -/ | | - | -/ | | - | -/# | | - | -/ # | | - | / # | | - | a1---------- | - +-------------------------------+ - */ + // Find angle of the path(s) + // Used to thicken the single pixel paths + /* + +-------------------------------+ + | a2 | + | -| | + | -/ | | + | -/ | | + | -/# | | + | -/ # | | + | / # | | + | a1---------- | + +-------------------------------+ + */ - Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; - float angle = tanh((float)deltaA.y / deltaA.x); + Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; + float angle = tanh((float)deltaA.y / deltaA.x); - // Distance (at right angle to the paths), which will give corners for our "quads" - // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner - /* - | a2 - | . - | .. - | aq1 .. - | # .. - | | # .. - |fromPath.y | # .. - | +----a1 - | - | fromPath.x - +-------------------------------- - */ + // Distance (at right angle to the paths), which will give corners for our "quads" + // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner + /* + | a2 + | . + | .. + | aq1 .. + | # .. + | | # .. + |fromPath.y | # .. + | +----a1 + | + | fromPath.x + +-------------------------------- + */ - Distance fromPath; - fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; - fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; + Distance fromPath; + fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; + fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; - // Make the paths thick - // Corner points for the rectangles (quads): - /* + // Make the paths thick + // Corner points for the rectangles (quads): + /* - aq2 - a2 - / aq3 - / - / - aq1 / - a1 - aq3 - */ + aq2 + a2 + / aq3 + / + / + aq1 / + a1 + aq3 + */ - // Filled as two triangles per quad: - /* - aq2 # - # ### - ## # aq3 - ## ### - - ## #### -/ - ## ### -/ - ## #### -/ - aq1 ## -/ - --- -/ - \---aq4 - */ + // Filled as two triangles per quad: + /* + aq2 # + # ### + ## # aq3 + ## ### - + ## #### -/ + ## ### -/ + ## #### -/ + aq1 ## -/ + --- -/ + \---aq4 + */ - // Make the path thick: path a becomes quad a - Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; - Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; - Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; - Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; - fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, color); - fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color); + // Make the path thick: path a becomes quad a + Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; + Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; + Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; + Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; + fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, color); + fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color); - // Make the path thick: path b becomes quad b - Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; - Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; - Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; - Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; - fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, color); - fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color); + // Make the path thick: path b becomes quad b + Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; + Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; + Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; + Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; + fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, color); + fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color); - // Make the path thick: path c becomes quad c - Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; - Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; - Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; - Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; - fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, color); - fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color); + // Make the path thick: path c becomes quad c + Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; + Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; + Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; + Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; + fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, color); + fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color); - // Radius the intersection of quad b and quad c - /* - b2 / c1 - #### - ## ## - / \ - / \/ \ - / /\ \ - / / \ \ + // Radius the intersection of quad b and quad c + /* + b2 / c1 + #### + ## ## + / \ + / \/ \ + / /\ \ + / / \ \ - */ + */ - // Don't attempt if logo is tiny - if (logoTh > 3) { - // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding - // We get better results just re-deriving it - int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); - fillCircle(b2.x, b2.y, capRad, color); - } + // Don't attempt if logo is tiny + if (logoTh > 3) { + // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding + // We get better results just re-deriving it + int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); + fillCircle(b2.x, b2.y, capRad, color); + } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index b35ca5cc0..cbfe468e2 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -24,157 +24,160 @@ #include "./Tile.h" #include "graphics/niche/Drivers/EInk/EInk.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { using NicheGraphics::Drivers::EInk; using std::to_string; -class Applet : public GFX -{ - public: - // Which edge Applet::printAt will place on the Y parameter - enum VerticalAlignment : uint8_t { - TOP, - MIDDLE, - BOTTOM, - }; +class Applet : public GFX { +public: + // Which edge Applet::printAt will place on the Y parameter + enum VerticalAlignment : uint8_t { + TOP, + MIDDLE, + BOTTOM, + }; - // Which edge Applet::printAt will place on the X parameter - enum HorizontalAlignment : uint8_t { - LEFT, - RIGHT, - CENTER, - }; + // Which edge Applet::printAt will place on the X parameter + enum HorizontalAlignment : uint8_t { + LEFT, + RIGHT, + CENTER, + }; - // An easy-to-understand interpretation of SNR and RSSI - // Calculate with Applet::getSignalStrength - enum SignalStrength : int8_t { - SIGNAL_UNKNOWN = -1, - SIGNAL_NONE, - SIGNAL_BAD, - SIGNAL_FAIR, - SIGNAL_GOOD, - }; + // An easy-to-understand interpretation of SNR and RSSI + // Calculate with Applet::getSignalStrength + enum SignalStrength : int8_t { + SIGNAL_UNKNOWN = -1, + SIGNAL_NONE, + SIGNAL_BAD, + SIGNAL_FAIR, + SIGNAL_GOOD, + }; - Applet(); + Applet(); - void setTile(Tile *t); // Should only be called via Tile::setApplet - Tile *getTile(); // Tile with which this applet is linked + void setTile(Tile *t); // Should only be called via Tile::setApplet + Tile *getTile(); // Tile with which this applet is linked - // Rendering + // Rendering - void render(); // Draw the applet - bool wantsToRender(); // Check whether applet wants to render - bool wantsToAutoshow(); // Check whether applet wants to become foreground - Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer - void updateDimensions(); // Get current size from tile - void resetDrawingSpace(); // Makes sure every render starts with same parameters + void render(); // Draw the applet + bool wantsToRender(); // Check whether applet wants to render + bool wantsToAutoshow(); // Check whether applet wants to become foreground + Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer + void updateDimensions(); // Get current size from tile + void resetDrawingSpace(); // Makes sure every render starts with same parameters - // State of the applet + // State of the applet - void activate(); // Begin running - void deactivate(); // Stop running - void bringToForeground(); // Show - void sendToBackground(); // Hide - bool isActive(); - bool isForeground(); + void activate(); // Begin running + void deactivate(); // Stop running + void bringToForeground(); // Show + void sendToBackground(); // Hide + bool isActive(); + bool isForeground(); - // Event handlers + // Event handlers - virtual void onRender() = 0; // All drawing happens here - virtual void onActivate() {} - virtual void onDeactivate() {} - virtual void onForeground() {} - virtual void onBackground() {} - virtual void onShutdown() {} - virtual void onButtonShortPress() {} - virtual void onButtonLongPress() {} - virtual void onExitShort() {} - virtual void onExitLong() {} - virtual void onNavUp() {} - virtual void onNavDown() {} - virtual void onNavLeft() {} - virtual void onNavRight() {} + virtual void onRender() = 0; // All drawing happens here + virtual void onActivate() {} + virtual void onDeactivate() {} + virtual void onForeground() {} + virtual void onBackground() {} + virtual void onShutdown() {} + virtual void onButtonShortPress() {} + virtual void onButtonLongPress() {} + virtual void onExitShort() {} + virtual void onExitLong() {} + virtual void onNavUp() {} + virtual void onNavDown() {} + virtual void onNavLeft() {} + virtual void onNavRight() {} - virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification + virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification - static uint16_t getHeaderHeight(); // How tall the "standard" applet header is + static uint16_t getHeaderHeight(); // How tall the "standard" applet header is - static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets + static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets - const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet + const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet - protected: - void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here +protected: + void drawPixel(int16_t x, int16_t y, + uint16_t color) override; // Place a single pixel. All drawing output passes through here - void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update - void requestAutoshow(); // Ask for applet to be moved to foreground + void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update + void requestAutoshow(); // Ask for applet to be moved to foreground - uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 - uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 - void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region - void resetCrop(); // Removes setCrop() + uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 + uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 + void setCrop(int16_t left, int16_t top, uint16_t width, + uint16_t height); // Ignore pixels drawn outside a certain region + void resetCrop(); // Removes setCrop() - // Text + // Text - void setFont(AppletFont f); - AppletFont getFont(); - uint16_t getTextWidth(std::string text); - uint16_t getTextWidth(const char *text); - uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped - void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold - void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping + void setFont(AppletFont f); + AppletFont getFont(); + uint16_t getTextWidth(std::string text); + uint16_t getTextWidth(const char *text); + uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped + void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, + uint8_t thicknessY); // Faux bold + void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping - void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines - void drawHeader(std::string text); // Draw the standard applet header + void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, + Color color); // Fill with sparse lines + void drawHeader(std::string text); // Draw the standard applet header - // Meshtastic Logo + // Meshtastic Logo - static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo - uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region - uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region - void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, - Color color = BLACK); // Draw the Meshtastic logo + static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo + uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region + uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region + void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, + Color color = BLACK); // Draw the Meshtastic logo - std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc - SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value - std::string getTimeString(uint32_t epochSeconds); // Human readable - std::string getTimeString(); // Current time, human readable - uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu - std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric - std::string parse(std::string text); // Handle text which might contain special chars - std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars - bool isPrintable(std::string); // Check for characters which the font can't print + std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc + SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value + std::string getTimeString(uint32_t epochSeconds); // Human readable + std::string getTimeString(); // Current time, human readable + uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu + std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric + std::string parse(std::string text); // Handle text which might contain special chars + std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars + bool isPrintable(std::string); // Check for characters which the font can't print - // Convenient references + // Convenient references - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; - Persistence::LatestMessage *latestMessage = nullptr; + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; + Persistence::LatestMessage *latestMessage = nullptr; - private: - Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM - bool active = false; // Has the user enabled this applet (at run-time)? - bool foreground = false; // Is the applet currently drawn on a tile? +private: + Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM + bool active = false; // Has the user enabled this applet (at run-time)? + bool foreground = false; // Is the applet currently drawn on a tile? - bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing. - bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? - NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = - NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display + bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing. + bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? + NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = + NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the + // display - using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly - using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. + using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly + using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. - AppletFont currentFont; // As passed to setFont + AppletFont currentFont; // As passed to setFont - // As set by setCrop - int16_t cropLeft = 0; - int16_t cropTop = 0; - uint16_t cropWidth = 0; - uint16_t cropHeight = 0; + // As set by setCrop + int16_t cropLeft = 0; + int16_t cropTop = 0; + uint16_t cropWidth = 0; + uint16_t cropHeight = 0; }; }; // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 6c7a7b491..946378532 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -6,53 +6,51 @@ using namespace NicheGraphics; -InkHUD::AppletFont::AppletFont() -{ - // Default constructor uses the in-built AdafruitGFX font (not recommended) +InkHUD::AppletFont::AppletFont() { + // Default constructor uses the in-built AdafruitGFX font (not recommended) } InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding, int8_t paddingTop, int8_t paddingBottom) - : gfxFont(&adafruitGFXFont), encoding(encoding) -{ - // AdafruitGFX fonts are drawn relative to a "cursor line"; - // they print as if the glyphs are resting on the line of piece of ruled paper. - // The glyphs also each have a different height. + : gfxFont(&adafruitGFXFont), encoding(encoding) { + // AdafruitGFX fonts are drawn relative to a "cursor line"; + // they print as if the glyphs are resting on the line of piece of ruled paper. + // The glyphs also each have a different height. - // To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text - // We also need to know where that "cursor line" sits inside this "line height"; - // we need this additional info in order to align text by top-left, bottom-right, etc + // To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text + // We also need to know where that "cursor line" sits inside this "line height"; + // we need this additional info in order to align text by top-left, bottom-right, etc - // AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding, - // which we'd rather not deal with. If we want padding, we'll add it manually. + // AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding, + // which we'd rather not deal with. If we want padding, we'll add it manually. - this->ascenderHeight = 0; - this->descenderHeight = 0; - this->height = 0; + this->ascenderHeight = 0; + this->descenderHeight = 0; + this->height = 0; - // Scan each glyph in the AdafruitGFX font - for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) { - uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph - this->height = max(this->height, glyphHeight); // Store if it's a new max + // Scan each glyph in the AdafruitGFX font + for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) { + uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph + this->height = max(this->height, glyphHeight); // Store if it's a new max - // Calculate how far the glyph rises the cursor line - // Store if new max value - // Caution: signed and unsigned types - int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; - if (glyphAscender > 0) - this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); + // Calculate how far the glyph rises the cursor line + // Store if new max value + // Caution: signed and unsigned types + int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; + if (glyphAscender > 0) + this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); - int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; - if (glyphDescender > 0) - this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); - } + int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; + if (glyphDescender > 0) + this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); + } - // Apply any manual padding to grow or shrink the line size - // Helpful if a font has one or two exceptionally large characters, which would make the lines ridiculously tall - ascenderHeight += paddingTop; - descenderHeight += paddingBottom; + // Apply any manual padding to grow or shrink the line size + // Helpful if a font has one or two exceptionally large characters, which would make the lines ridiculously tall + ascenderHeight += paddingTop; + descenderHeight += paddingBottom; - // Find how far the cursor advances when we "print" a space character - spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; + // Find how far the cursor advances when we "print" a space character + spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; } /* @@ -68,664 +66,650 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding ▼ ### ▼ */ -uint8_t InkHUD::AppletFont::lineHeight() -{ - return this->height; -} +uint8_t InkHUD::AppletFont::lineHeight() { return this->height; } // AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). // This value is the height of the font, above that imaginary line. // Used to calculate the true height of the font -uint8_t InkHUD::AppletFont::heightAboveCursor() -{ - return this->ascenderHeight; -} +uint8_t InkHUD::AppletFont::heightAboveCursor() { return this->ascenderHeight; } // AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). // This value is the height of the font, below that imaginary line. // Used to calculate the true height of the font -uint8_t InkHUD::AppletFont::heightBelowCursor() -{ - return this->descenderHeight; -} +uint8_t InkHUD::AppletFont::heightBelowCursor() { return this->descenderHeight; } // Width of the space character // Used with Applet::printWrapped -uint8_t InkHUD::AppletFont::widthBetweenWords() -{ - return this->spaceCharWidth; -} +uint8_t InkHUD::AppletFont::widthBetweenWords() { return this->spaceCharWidth; } // Convert a unicode char from set of UTF-8 bytes to UTF-32 // Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value -uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) -{ - uint32_t utf32 = 0; +uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) { + uint32_t utf32 = 0; - switch (utf8.length()) { - case 2: - // 5 bits + 6 bits - utf32 |= (utf8.at(0) & 0b00011111) << 6; - utf32 |= (utf8.at(1) & 0b00111111); - break; + switch (utf8.length()) { + case 2: + // 5 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00011111) << 6; + utf32 |= (utf8.at(1) & 0b00111111); + break; - case 3: - // 4 bits + 6 bits + 6 bits - utf32 |= (utf8.at(0) & 0b00001111) << (6 + 6); - utf32 |= (utf8.at(1) & 0b00111111) << 6; - utf32 |= (utf8.at(2) & 0b00111111); - break; + case 3: + // 4 bits + 6 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00001111) << (6 + 6); + utf32 |= (utf8.at(1) & 0b00111111) << 6; + utf32 |= (utf8.at(2) & 0b00111111); + break; - case 4: - // 3 bits + 6 bits + 6 bits + 6 bits - utf32 |= (utf8.at(0) & 0b00000111) << (6 + 6 + 6); - utf32 |= (utf8.at(1) & 0b00111111) << (6 + 6); - utf32 |= (utf8.at(2) & 0b00111111) << 6; - utf32 |= (utf8.at(3) & 0b00111111); - break; - default: - return 0; - } + case 4: + // 3 bits + 6 bits + 6 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00000111) << (6 + 6 + 6); + utf32 |= (utf8.at(1) & 0b00111111) << (6 + 6); + utf32 |= (utf8.at(2) & 0b00111111) << 6; + utf32 |= (utf8.at(3) & 0b00111111); + break; + default: + return 0; + } - return utf32; + return utf32; } // Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII // Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars -std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) -{ - // Final processed output - std::string decoded; +std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) { + // Final processed output + std::string decoded; - // Holds bytes for one UTF-8 char during parsing - std::string utf8Char; - uint8_t utf8CharSize = 0; + // Holds bytes for one UTF-8 char during parsing + std::string utf8Char; + uint8_t utf8CharSize = 0; - for (char &c : encoded) { + for (char &c : encoded) { - // If first byte - if (utf8Char.empty()) { - // If MSB is unset, byte is an ASCII char - // If MSB is set, byte is part of a UTF-8 char. Counting number of higher-order bits tells how many bytes in char - if ((c & 0x80)) { - char c1 = c; - while (c1 & 0x80) { - c1 <<= 1; - utf8CharSize++; - } - } + // If first byte + if (utf8Char.empty()) { + // If MSB is unset, byte is an ASCII char + // If MSB is set, byte is part of a UTF-8 char. Counting number of higher-order bits tells how many bytes in char + if ((c & 0x80)) { + char c1 = c; + while (c1 & 0x80) { + c1 <<= 1; + utf8CharSize++; } + } + } - // Append the byte to the UTF-8 char we're building - utf8Char += c; + // Append the byte to the UTF-8 char we're building + utf8Char += c; - // More bytes left to collect. Iterate. - if (utf8Char.length() < utf8CharSize) - continue; + // More bytes left to collect. Iterate. + if (utf8Char.length() < utf8CharSize) + continue; - // Now collected all bytes for this char - // Remap the value to match the encoding of our 8-bit AppletFont - decoded += applyEncoding(utf8Char); + // Now collected all bytes for this char + // Remap the value to match the encoding of our 8-bit AppletFont + decoded += applyEncoding(utf8Char); - // Reset, ready to build next UTF-8 char from the encoded bytes - utf8Char.clear(); - utf8CharSize = 0; - } // For each char + // Reset, ready to build next UTF-8 char from the encoded bytes + utf8Char.clear(); + utf8CharSize = 0; + } // For each char - // All chars processed, return result - return decoded; + // All chars processed, return result + return decoded; } // Re-encode a single UTF-8 character to extended ASCII // Target encoding depends on the font -char InkHUD::AppletFont::applyEncoding(std::string utf8) -{ - // ##################################################### Syntactic Sugar ##################################################### -#define REMAP(in, out) \ - case in: \ - return out; - // ########################################################################################################################### +char InkHUD::AppletFont::applyEncoding(std::string utf8) { + // ##################################################### Syntactic Sugar + // ##################################################### +#define REMAP(in, out) \ + case in: \ + return out; + // ########################################################################################################################### - // Latin - Central Europe - // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1250.TXT - if (encoding == WINDOWS_1250) { - // 1-Byte chars: no remapping - if (utf8.length() == 1) - return utf8.at(0); + // Latin - Central Europe + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1250.TXT + if (encoding == WINDOWS_1250) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); - // Multi-byte chars: - switch (toUtf32(utf8)) { - REMAP(0x20AC, 0x80); // EURO SIGN - REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK - REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK - REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS - REMAP(0x2020, 0x86); // DAGGER - REMAP(0x2021, 0x87); // DOUBLE DAGGER - REMAP(0x2030, 0x89); // PER MILLE SIGN - REMAP(0x0160, 0x8A); // LATIN CAPITAL LETTER S WITH CARON - REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - REMAP(0x015A, 0x8C); // LATIN CAPITAL LETTER S WITH ACUTE - REMAP(0x0164, 0x8D); // LATIN CAPITAL LETTER T WITH CARON - REMAP(0x017D, 0x8E); // LATIN CAPITAL LETTER Z WITH CARON - REMAP(0x0179, 0x8F); // LATIN CAPITAL LETTER Z WITH ACUTE - - REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK - REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK - REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK - REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK - REMAP(0x2022, 0x95); // BULLET - REMAP(0x2013, 0x96); // EN DASH - REMAP(0x2014, 0x97); // EM DASH - REMAP(0x2122, 0x99); // TRADE MARK SIGN - REMAP(0x0161, 0x9A); // LATIN SMALL LETTER S WITH CARON - REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - REMAP(0x015B, 0x9C); // LATIN SMALL LETTER S WITH ACUTE - REMAP(0x0165, 0x9D); // LATIN SMALL LETTER T WITH CARON - REMAP(0x017E, 0x9E); // LATIN SMALL LETTER Z WITH CARON - REMAP(0x017A, 0x9F); // LATIN SMALL LETTER Z WITH ACUTE - - REMAP(0x00A0, 0xA0); // NO-BREAK SPACE - REMAP(0x02C7, 0xA1); // CARON - REMAP(0x02D8, 0xA2); // BREVE - REMAP(0x0141, 0xA3); // LATIN CAPITAL LETTER L WITH STROKE - REMAP(0x00A4, 0xA4); // CURRENCY SIGN - REMAP(0x0104, 0xA5); // LATIN CAPITAL LETTER A WITH OGONEK - REMAP(0x00A6, 0xA6); // BROKEN BAR - REMAP(0x00A7, 0xA7); // SECTION SIGN - REMAP(0x00A8, 0xA8); // DIAERESIS - REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN - REMAP(0x015E, 0xAA); // LATIN CAPITAL LETTER S WITH CEDILLA - REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00AC, 0xAC); // NOT SIGN - REMAP(0x00AD, 0xAD); // SOFT HYPHEN - REMAP(0x00AE, 0xAE); // REGISTERED SIGN - REMAP(0x017B, 0xAF); // LATIN CAPITAL LETTER Z WITH DOT ABOVE - - REMAP(0x00B0, 0xB0); // DEGREE SIGN - REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN - REMAP(0x02DB, 0xB2); // OGONEK - REMAP(0x0142, 0xB3); // LATIN SMALL LETTER L WITH STROKE - REMAP(0x00B4, 0xB4); // ACUTE ACCENT - REMAP(0x00B5, 0xB5); // MICRO SIGN - REMAP(0x00B6, 0xB6); // PILCROW SIGN - REMAP(0x00B7, 0xB7); // MIDDLE DOT - REMAP(0x00B8, 0xB8); // CEDILLA - REMAP(0x0105, 0xB9); // LATIN SMALL LETTER A WITH OGONEK - REMAP(0x015F, 0xBA); // LATIN SMALL LETTER S WITH CEDILLA - REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x013D, 0xBC); // LATIN CAPITAL LETTER L WITH CARON - REMAP(0x02DD, 0xBD); // DOUBLE ACUTE ACCENT - REMAP(0x013E, 0xBE); // LATIN SMALL LETTER L WITH CARON - REMAP(0x017C, 0xBF); // LATIN SMALL LETTER Z WITH DOT ABOVE - - REMAP(0x0154, 0xC0); // LATIN CAPITAL LETTER R WITH ACUTE - REMAP(0x00C1, 0xC1); // LATIN CAPITAL LETTER A WITH ACUTE - REMAP(0x00C2, 0xC2); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX - REMAP(0x0102, 0xC3); // LATIN CAPITAL LETTER A WITH BREVE - REMAP(0x00C4, 0xC4); // LATIN CAPITAL LETTER A WITH DIAERESIS - REMAP(0x0139, 0xC5); // LATIN CAPITAL LETTER L WITH ACUTE - REMAP(0x0106, 0xC6); // LATIN CAPITAL LETTER C WITH ACUTE - REMAP(0x00C7, 0xC7); // LATIN CAPITAL LETTER C WITH CEDILLA - REMAP(0x010C, 0xC8); // LATIN CAPITAL LETTER C WITH CARON - REMAP(0x00C9, 0xC9); // LATIN CAPITAL LETTER E WITH ACUTE - REMAP(0x0118, 0xCA); // LATIN CAPITAL LETTER E WITH OGONEK - REMAP(0x00CB, 0xCB); // LATIN CAPITAL LETTER E WITH DIAERESIS - REMAP(0x011A, 0xCC); // LATIN CAPITAL LETTER E WITH CARON - REMAP(0x00CD, 0xCD); // LATIN CAPITAL LETTER I WITH ACUTE - REMAP(0x00CE, 0xCE); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX - REMAP(0x010E, 0xCF); // LATIN CAPITAL LETTER D WITH CARON - - REMAP(0x0110, 0xD0); // LATIN CAPITAL LETTER D WITH STROKE - REMAP(0x0143, 0xD1); // LATIN CAPITAL LETTER N WITH ACUTE - REMAP(0x0147, 0xD2); // LATIN CAPITAL LETTER N WITH CARON - REMAP(0x00D3, 0xD3); // LATIN CAPITAL LETTER O WITH ACUTE - REMAP(0x00D4, 0xD4); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX - REMAP(0x0150, 0xD5); // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE - REMAP(0x00D6, 0xD6); // LATIN CAPITAL LETTER O WITH DIAERESIS - REMAP(0x00D7, 0xD7); // MULTIPLICATION SIGN - REMAP(0x0158, 0xD8); // LATIN CAPITAL LETTER R WITH CARON - REMAP(0x016E, 0xD9); // LATIN CAPITAL LETTER U WITH RING ABOVE - REMAP(0x00DA, 0xDA); // LATIN CAPITAL LETTER U WITH ACUTE - REMAP(0x0170, 0xDB); // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE - REMAP(0x00DC, 0xDC); // LATIN CAPITAL LETTER U WITH DIAERESIS - REMAP(0x00DD, 0xDD); // LATIN CAPITAL LETTER Y WITH ACUTE - REMAP(0x0162, 0xDE); // LATIN CAPITAL LETTER T WITH CEDILLA - REMAP(0x00DF, 0xDF); // LATIN SMALL LETTER SHARP S - - REMAP(0x0155, 0xE0); // LATIN SMALL LETTER R WITH ACUTE - REMAP(0x00E1, 0xE1); // LATIN SMALL LETTER A WITH ACUTE - REMAP(0x00E2, 0xE2); // LATIN SMALL LETTER A WITH CIRCUMFLEX - REMAP(0x0103, 0xE3); // LATIN SMALL LETTER A WITH BREVE - REMAP(0x00E4, 0xE4); // LATIN SMALL LETTER A WITH DIAERESIS - REMAP(0x013A, 0xE5); // LATIN SMALL LETTER L WITH ACUTE - REMAP(0x0107, 0xE6); // LATIN SMALL LETTER C WITH ACUTE - REMAP(0x00E7, 0xE7); // LATIN SMALL LETTER C WITH CEDILLA - REMAP(0x010D, 0xE8); // LATIN SMALL LETTER C WITH CARON - REMAP(0x00E9, 0xE9); // LATIN SMALL LETTER E WITH ACUTE - REMAP(0x0119, 0xEA); // LATIN SMALL LETTER E WITH OGONEK - REMAP(0x00EB, 0xEB); // LATIN SMALL LETTER E WITH DIAERESIS - REMAP(0x011B, 0xEC); // LATIN SMALL LETTER E WITH CARON - REMAP(0x00ED, 0xED); // LATIN SMALL LETTER I WITH ACUTE - REMAP(0x00EE, 0xEE); // LATIN SMALL LETTER I WITH CIRCUMFLEX - REMAP(0x010F, 0xEF); // LATIN SMALL LETTER D WITH CARON - - REMAP(0x0111, 0xF0); // LATIN SMALL LETTER D WITH STROKE - REMAP(0x0144, 0xF1); // LATIN SMALL LETTER N WITH ACUTE - REMAP(0x0148, 0xF2); // LATIN SMALL LETTER N WITH CARON - REMAP(0x00F3, 0xF3); // LATIN SMALL LETTER O WITH ACUTE - REMAP(0x00F4, 0xF4); // LATIN SMALL LETTER O WITH CIRCUMFLEX - REMAP(0x0151, 0xF5); // LATIN SMALL LETTER O WITH DOUBLE ACUTE - REMAP(0x00F6, 0xF6); // LATIN SMALL LETTER O WITH DIAERESIS - REMAP(0x00F7, 0xF7); // DIVISION SIGN - REMAP(0x0159, 0xF8); // LATIN SMALL LETTER R WITH CARON - REMAP(0x016F, 0xF9); // LATIN SMALL LETTER U WITH RING ABOVE - REMAP(0x00FA, 0xFA); // LATIN SMALL LETTER U WITH ACUTE - REMAP(0x0171, 0xFB); // LATIN SMALL LETTER U WITH DOUBLE ACUTE - REMAP(0x00FC, 0xFC); // LATIN SMALL LETTER U WITH DIAERESIS - REMAP(0x00FD, 0xFD); // LATIN SMALL LETTER Y WITH ACUTE - REMAP(0x0163, 0xFE); // LATIN SMALL LETTER T WITH CEDILLA - REMAP(0x02D9, 0xFF); // DOT ABOVE - } - } - - // Latin - Cyrillic - // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1251.TXT - else if (encoding == WINDOWS_1251) { - // 1-Byte chars: no remapping - if (utf8.length() == 1) - return utf8.at(0); - - // Multi-byte chars: - switch (toUtf32(utf8)) { - REMAP(0x0402, 0x80); // CYRILLIC CAPITAL LETTER DJE - REMAP(0x0403, 0x81); // CYRILLIC CAPITAL LETTER GJE - REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK - REMAP(0x0453, 0x83); // CYRILLIC SMALL LETTER GJE - REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK - REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS - REMAP(0x2020, 0x86); // DAGGER - REMAP(0x2021, 0x87); // DOUBLE DAGGER - REMAP(0x20AC, 0x88); // EURO SIGN - REMAP(0x2030, 0x89); // PER MILLE SIGN - REMAP(0x0409, 0x8A); // CYRILLIC CAPITAL LETTER LJE - REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - REMAP(0x040A, 0x8C); // CYRILLIC CAPITAL LETTER NJE - REMAP(0x040C, 0x8D); // CYRILLIC CAPITAL LETTER KJE - REMAP(0x040B, 0x8E); // CYRILLIC CAPITAL LETTER TSHE - REMAP(0x040F, 0x8F); // CYRILLIC CAPITAL LETTER DZHE - - REMAP(0x0452, 0x90); // CYRILLIC SMALL LETTER DJE - REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK - REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK - REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK - REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK - REMAP(0x2022, 0x95); // BULLET - REMAP(0x2013, 0x96); // EN DASH - REMAP(0x2014, 0x97); // EM DASH - REMAP(0x2122, 0x99); // TRADE MARK SIGN - REMAP(0x0459, 0x9A); // CYRILLIC SMALL LETTER LJE - REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - REMAP(0x045A, 0x9C); // CYRILLIC SMALL LETTER NJE - REMAP(0x045C, 0x9D); // CYRILLIC SMALL LETTER KJE - REMAP(0x045B, 0x9E); // CYRILLIC SMALL LETTER TSHE - REMAP(0x045F, 0x9F); // CYRILLIC SMALL LETTER DZHE - - REMAP(0x00A0, 0xA0); // NO-BREAK SPACE - REMAP(0x040E, 0xA1); // CYRILLIC CAPITAL LETTER SHORT U - REMAP(0x045E, 0xA2); // CYRILLIC SMALL LETTER SHORT U - REMAP(0x0408, 0xA3); // CYRILLIC CAPITAL LETTER JE - REMAP(0x00A4, 0xA4); // CURRENCY SIGN - REMAP(0x0490, 0xA5); // CYRILLIC CAPITAL LETTER GHE WITH UPTURN - REMAP(0x00A6, 0xA6); // BROKEN BAR - REMAP(0x00A7, 0xA7); // SECTION SIGN - REMAP(0x0401, 0xA8); // CYRILLIC CAPITAL LETTER IO - REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN - REMAP(0x0404, 0xAA); // CYRILLIC CAPITAL LETTER UKRAINIAN IE - REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00AC, 0xAC); // NOT SIGN - REMAP(0x00AD, 0xAD); // SOFT HYPHEN - REMAP(0x00AE, 0xAE); // REGISTERED SIGN - REMAP(0x0407, 0xAF); // CYRILLIC CAPITAL LETTER YI - - REMAP(0x00B0, 0xB0); // DEGREE SIGN - REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN - REMAP(0x0406, 0xB2); // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I - REMAP(0x0456, 0xB3); // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - REMAP(0x0491, 0xB4); // CYRILLIC SMALL LETTER GHE WITH UPTURN - REMAP(0x00B5, 0xB5); // MICRO SIGN - REMAP(0x00B6, 0xB6); // PILCROW SIGN - REMAP(0x00B7, 0xB7); // MIDDLE DOT - REMAP(0x0451, 0xB8); // CYRILLIC SMALL LETTER IO - REMAP(0x2116, 0xB9); // NUMERO SIGN - REMAP(0x0454, 0xBA); // CYRILLIC SMALL LETTER UKRAINIAN IE - REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x0458, 0xBC); // CYRILLIC SMALL LETTER JE - REMAP(0x0405, 0xBD); // CYRILLIC CAPITAL LETTER DZE - REMAP(0x0455, 0xBE); // CYRILLIC SMALL LETTER DZE - REMAP(0x0457, 0xBF); // CYRILLIC SMALL LETTER YI - - REMAP(0x0410, 0xC0); // CYRILLIC CAPITAL LETTER A - REMAP(0x0411, 0xC1); // CYRILLIC CAPITAL LETTER BE - REMAP(0x0412, 0xC2); // CYRILLIC CAPITAL LETTER VE - REMAP(0x0413, 0xC3); // CYRILLIC CAPITAL LETTER GHE - REMAP(0x0414, 0xC4); // CYRILLIC CAPITAL LETTER DE - REMAP(0x0415, 0xC5); // CYRILLIC CAPITAL LETTER IE - REMAP(0x0416, 0xC6); // CYRILLIC CAPITAL LETTER ZHE - REMAP(0x0417, 0xC7); // CYRILLIC CAPITAL LETTER ZE - REMAP(0x0418, 0xC8); // CYRILLIC CAPITAL LETTER I - REMAP(0x0419, 0xC9); // CYRILLIC CAPITAL LETTER SHORT I - REMAP(0x041A, 0xCA); // CYRILLIC CAPITAL LETTER KA - REMAP(0x041B, 0xCB); // CYRILLIC CAPITAL LETTER EL - REMAP(0x041C, 0xCC); // CYRILLIC CAPITAL LETTER EM - REMAP(0x041D, 0xCD); // CYRILLIC CAPITAL LETTER EN - REMAP(0x041E, 0xCE); // CYRILLIC CAPITAL LETTER O - REMAP(0x041F, 0xCF); // CYRILLIC CAPITAL LETTER PE - - REMAP(0x0420, 0xD0); // CYRILLIC CAPITAL LETTER ER - REMAP(0x0421, 0xD1); // CYRILLIC CAPITAL LETTER ES - REMAP(0x0422, 0xD2); // CYRILLIC CAPITAL LETTER TE - REMAP(0x0423, 0xD3); // CYRILLIC CAPITAL LETTER U - REMAP(0x0424, 0xD4); // CYRILLIC CAPITAL LETTER EF - REMAP(0x0425, 0xD5); // CYRILLIC CAPITAL LETTER HA - REMAP(0x0426, 0xD6); // CYRILLIC CAPITAL LETTER TSE - REMAP(0x0427, 0xD7); // CYRILLIC CAPITAL LETTER CHE - REMAP(0x0428, 0xD8); // CYRILLIC CAPITAL LETTER SHA - REMAP(0x0429, 0xD9); // CYRILLIC CAPITAL LETTER SHCHA - REMAP(0x042A, 0xDA); // CYRILLIC CAPITAL LETTER HARD SIGN - REMAP(0x042B, 0xDB); // CYRILLIC CAPITAL LETTER YERU - REMAP(0x042C, 0xDC); // CYRILLIC CAPITAL LETTER SOFT SIGN - REMAP(0x042D, 0xDD); // CYRILLIC CAPITAL LETTER E - REMAP(0x042E, 0xDE); // CYRILLIC CAPITAL LETTER YU - REMAP(0x042F, 0xDF); // CYRILLIC CAPITAL LETTER YA - - REMAP(0x0430, 0xE0); // CYRILLIC SMALL LETTER A - REMAP(0x0431, 0xE1); // CYRILLIC SMALL LETTER BE - REMAP(0x0432, 0xE2); // CYRILLIC SMALL LETTER VE - REMAP(0x0433, 0xE3); // CYRILLIC SMALL LETTER GHE - REMAP(0x0434, 0xE4); // CYRILLIC SMALL LETTER DE - REMAP(0x0435, 0xE5); // CYRILLIC SMALL LETTER IE - REMAP(0x0436, 0xE6); // CYRILLIC SMALL LETTER ZHE - REMAP(0x0437, 0xE7); // CYRILLIC SMALL LETTER ZE - REMAP(0x0438, 0xE8); // CYRILLIC SMALL LETTER I - REMAP(0x0439, 0xE9); // CYRILLIC SMALL LETTER SHORT I - REMAP(0x043A, 0xEA); // CYRILLIC SMALL LETTER KA - REMAP(0x043B, 0xEB); // CYRILLIC SMALL LETTER EL - REMAP(0x043C, 0xEC); // CYRILLIC SMALL LETTER EM - REMAP(0x043D, 0xED); // CYRILLIC SMALL LETTER EN - REMAP(0x043E, 0xEE); // CYRILLIC SMALL LETTER O - REMAP(0x043F, 0xEF); // CYRILLIC SMALL LETTER PE - - REMAP(0x0440, 0xF0); // CYRILLIC SMALL LETTER ER - REMAP(0x0441, 0xF1); // CYRILLIC SMALL LETTER ES - REMAP(0x0442, 0xF2); // CYRILLIC SMALL LETTER TE - REMAP(0x0443, 0xF3); // CYRILLIC SMALL LETTER U - REMAP(0x0444, 0xF4); // CYRILLIC SMALL LETTER EF - REMAP(0x0445, 0xF5); // CYRILLIC SMALL LETTER HA - REMAP(0x0446, 0xF6); // CYRILLIC SMALL LETTER TSE - REMAP(0x0447, 0xF7); // CYRILLIC SMALL LETTER CHE - REMAP(0x0448, 0xF8); // CYRILLIC SMALL LETTER SHA - REMAP(0x0449, 0xF9); // CYRILLIC SMALL LETTER SHCHA - REMAP(0x044A, 0xFA); // CYRILLIC SMALL LETTER HARD SIGN - REMAP(0x044B, 0xFB); // CYRILLIC SMALL LETTER YERU - REMAP(0x044C, 0xFC); // CYRILLIC SMALL LETTER SOFT SIGN - REMAP(0x044D, 0xFD); // CYRILLIC SMALL LETTER E - REMAP(0x044E, 0xFE); // CYRILLIC SMALL LETTER YU - REMAP(0x044F, 0xFF); // CYRILLIC SMALL LETTER YA - } - } - - // Latin - Western Europe - // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT - else if (encoding == WINDOWS_1252) { - // 1-Byte chars: no remapping - if (utf8.length() == 1) - return utf8.at(0); - - // Multi-byte chars: - switch (toUtf32(utf8)) { - REMAP(0x20AC, 0x80) // EURO SIGN - REMAP(0x201A, 0x82) // SINGLE LOW-9 QUOTATION MARK - REMAP(0x0192, 0x83) // LATIN SMALL LETTER F WITH HOOK - REMAP(0x201E, 0x84) // DOUBLE LOW-9 QUOTATION MARK - REMAP(0x2026, 0x85) // HORIZONTAL ELLIPSIS - REMAP(0x2020, 0x86) // DAGGER - REMAP(0x2021, 0x87) // DOUBLE DAGGER - REMAP(0x02C6, 0x88) // MODIFIER LETTER CIRCUMFLEX ACCENT - REMAP(0x2030, 0x89) // PER MILLE SIGN - REMAP(0x0160, 0x8A) // LATIN CAPITAL LETTER S WITH CARON - REMAP(0x2039, 0x8B) // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - REMAP(0x0152, 0x8C) // LATIN CAPITAL LIGATURE OE - REMAP(0x017D, 0x8E) // LATIN CAPITAL LETTER Z WITH CARON - - REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK - REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK - REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK - REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK - REMAP(0x2022, 0x95) // BULLET - REMAP(0x2013, 0x96) // EN DASH - REMAP(0x2014, 0x97) // EM DASH - REMAP(0x02DC, 0x98) // SMALL TILDE - REMAP(0x2122, 0x99) // TRADE MARK SIGN - REMAP(0x0161, 0x9A) // LATIN SMALL LETTER S WITH CARON - REMAP(0x203A, 0x9B) // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - REMAP(0x0153, 0x9C) // LATIN SMALL LIGATURE OE - REMAP(0x017E, 0x9E) // LATIN SMALL LETTER Z WITH CARON - REMAP(0x0178, 0x9F) // LATIN CAPITAL LETTER Y WITH DIAERESIS - - REMAP(0x00A0, 0xA0) // NO-BREAK SPACE - REMAP(0x00A1, 0xA1) // INVERTED EXCLAMATION MARK - REMAP(0x00A2, 0xA2) // CENT SIGN - REMAP(0x00A3, 0xA3) // POUND SIGN - REMAP(0x00A4, 0xA4) // CURRENCY SIGN - REMAP(0x00A5, 0xA5) // YEN SIGN - REMAP(0x00A6, 0xA6) // BROKEN BAR - REMAP(0x00A7, 0xA7) // SECTION SIGN - REMAP(0x00A8, 0xA8) // DIAERESIS - REMAP(0x00A9, 0xA9) // COPYRIGHT SIGN - REMAP(0x00AA, 0xAA) // FEMININE ORDINAL INDICATOR - REMAP(0x00AB, 0xAB) // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00AC, 0xAC) // NOT SIGN - REMAP(0x00AD, 0xAD) // SOFT HYPHEN - REMAP(0x00AE, 0xAE) // REGISTERED SIGN - REMAP(0x00AF, 0xAF) // MACRON - - REMAP(0x00B0, 0xB0) // DEGREE SIGN - REMAP(0x00B1, 0xB1) // PLUS-MINUS SIGN - REMAP(0x00B2, 0xB2) // SUPERSCRIPT TWO - REMAP(0x00B3, 0xB3) // SUPERSCRIPT THREE - REMAP(0x00B4, 0xB4) // ACUTE ACCENT - REMAP(0x00B5, 0xB5) // MICRO SIGN - REMAP(0x00B6, 0xB6) // PILCROW SIGN - REMAP(0x00B7, 0xB7) // MIDDLE DOT - REMAP(0x00B8, 0xB8) // CEDILLA - REMAP(0x00B9, 0xB9) // SUPERSCRIPT ONE - REMAP(0x00BA, 0xBA) // MASCULINE ORDINAL INDICATOR - REMAP(0x00BB, 0xBB) // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00BC, 0xBC) // VULGAR FRACTION ONE QUARTER - REMAP(0x00BD, 0xBD) // VULGAR FRACTION ONE HALF - REMAP(0x00BE, 0xBE) // VULGAR FRACTION THREE QUARTERS - REMAP(0x00BF, 0xBF) // INVERTED QUESTION MARK - - REMAP(0x00C0, 0xC0) // LATIN CAPITAL LETTER A WITH GRAVE - REMAP(0x00C1, 0xC1) // LATIN CAPITAL LETTER A WITH ACUTE - REMAP(0x00C2, 0xC2) // LATIN CAPITAL LETTER A WITH CIRCUMFLEX - REMAP(0x00C3, 0xC3) // LATIN CAPITAL LETTER A WITH TILDE - REMAP(0x00C4, 0xC4) // LATIN CAPITAL LETTER A WITH DIAERESIS - REMAP(0x00C5, 0xC5) // LATIN CAPITAL LETTER A WITH RING ABOVE - REMAP(0x00C6, 0xC6) // LATIN CAPITAL LETTER AE - REMAP(0x00C7, 0xC7) // LATIN CAPITAL LETTER C WITH CEDILLA - REMAP(0x00C8, 0xC8) // LATIN CAPITAL LETTER E WITH GRAVE - REMAP(0x00C9, 0xC9) // LATIN CAPITAL LETTER E WITH ACUTE - REMAP(0x00CA, 0xCA) // LATIN CAPITAL LETTER E WITH CIRCUMFLEX - REMAP(0x00CB, 0xCB) // LATIN CAPITAL LETTER E WITH DIAERESIS - REMAP(0x00CC, 0xCC) // LATIN CAPITAL LETTER I WITH GRAVE - REMAP(0x00CD, 0xCD) // LATIN CAPITAL LETTER I WITH ACUTE - REMAP(0x00CE, 0xCE) // LATIN CAPITAL LETTER I WITH CIRCUMFLEX - REMAP(0x00CF, 0xCF) // LATIN CAPITAL LETTER I WITH DIAERESIS - - REMAP(0x00D0, 0xD0) // LATIN CAPITAL LETTER ETH - REMAP(0x00D1, 0xD1) // LATIN CAPITAL LETTER N WITH TILDE - REMAP(0x00D2, 0xD2) // LATIN CAPITAL LETTER O WITH GRAVE - REMAP(0x00D3, 0xD3) // LATIN CAPITAL LETTER O WITH ACUTE - REMAP(0x00D4, 0xD4) // LATIN CAPITAL LETTER O WITH CIRCUMFLEX - REMAP(0x00D5, 0xD5) // LATIN CAPITAL LETTER O WITH TILDE - REMAP(0x00D6, 0xD6) // LATIN CAPITAL LETTER O WITH DIAERESIS - REMAP(0x00D7, 0xD7) // MULTIPLICATION SIGN - REMAP(0x00D8, 0xD8) // LATIN CAPITAL LETTER O WITH STROKE - REMAP(0x00D9, 0xD9) // LATIN CAPITAL LETTER U WITH GRAVE - REMAP(0x00DA, 0xDA) // LATIN CAPITAL LETTER U WITH ACUTE - REMAP(0x00DB, 0xDB) // LATIN CAPITAL LETTER U WITH CIRCUMFLEX - REMAP(0x00DC, 0xDC) // LATIN CAPITAL LETTER U WITH DIAERESIS - REMAP(0x00DD, 0xDD) // LATIN CAPITAL LETTER Y WITH ACUTE - REMAP(0x00DE, 0xDE) // LATIN CAPITAL LETTER THORN - REMAP(0x00DF, 0xDF) // LATIN SMALL LETTER SHARP S - - REMAP(0x00E0, 0xE0) // LATIN SMALL LETTER A WITH GRAVE - REMAP(0x00E1, 0xE1) // LATIN SMALL LETTER A WITH ACUTE - REMAP(0x00E2, 0xE2) // LATIN SMALL LETTER A WITH CIRCUMFLEX - REMAP(0x00E3, 0xE3) // LATIN SMALL LETTER A WITH TILDE - REMAP(0x00E4, 0xE4) // LATIN SMALL LETTER A WITH DIAERESIS - REMAP(0x00E5, 0xE5) // LATIN SMALL LETTER A WITH RING ABOVE - REMAP(0x00E6, 0xE6) // LATIN SMALL LETTER AE - REMAP(0x00E7, 0xE7) // LATIN SMALL LETTER C WITH CEDILLA - REMAP(0x00E8, 0xE8) // LATIN SMALL LETTER E WITH GRAVE - REMAP(0x00E9, 0xE9) // LATIN SMALL LETTER E WITH ACUTE - REMAP(0x00EA, 0xEA) // LATIN SMALL LETTER E WITH CIRCUMFLEX - REMAP(0x00EB, 0xEB) // LATIN SMALL LETTER E WITH DIAERESIS - REMAP(0x00EC, 0xEC) // LATIN SMALL LETTER I WITH GRAVE - REMAP(0x00ED, 0xED) // LATIN SMALL LETTER I WITH ACUTE - REMAP(0x00EE, 0xEE) // LATIN SMALL LETTER I WITH CIRCUMFLEX - REMAP(0x00EF, 0xEF) // LATIN SMALL LETTER I WITH DIAERESIS - - REMAP(0x00F0, 0xF0) // LATIN SMALL LETTER ETH - REMAP(0x00F1, 0xF1) // LATIN SMALL LETTER N WITH TILDE - REMAP(0x00F2, 0xF2) // LATIN SMALL LETTER O WITH GRAVE - REMAP(0x00F3, 0xF3) // LATIN SMALL LETTER O WITH ACUTE - REMAP(0x00F4, 0xF4) // LATIN SMALL LETTER O WITH CIRCUMFLEX - REMAP(0x00F5, 0xF5) // LATIN SMALL LETTER O WITH TILDE - REMAP(0x00F6, 0xF6) // LATIN SMALL LETTER O WITH DIAERESIS - REMAP(0x00F7, 0xF7) // DIVISION SIGN - REMAP(0x00F8, 0xF8) // LATIN SMALL LETTER O WITH STROKE - REMAP(0x00F9, 0xF9) // LATIN SMALL LETTER U WITH GRAVE - REMAP(0x00FA, 0xFA) // LATIN SMALL LETTER U WITH ACUTE - REMAP(0x00FB, 0xFB) // LATIN SMALL LETTER U WITH CIRCUMFLEX - REMAP(0x00FC, 0xFC) // LATIN SMALL LETTER U WITH DIAERESIS - REMAP(0x00FD, 0xFD) // LATIN SMALL LETTER Y WITH ACUTE - REMAP(0x00FE, 0xFE) // LATIN SMALL LETTER THORN - REMAP(0x00FF, 0xFF) // LATIN SMALL LETTER Y WITH DIAERESIS - } - } - - else /*ASCII or Unhandled*/ { - if (utf8.length() == 1) - return utf8.at(0); - } - - // All single-byte (ASCII) characters should have been handled by now - // Only unhandled multi-byte UTF8 characters should remain - assert(utf8.length() > 1); - - // Parse emoji - // Strip emoji modifiers + // Multi-byte chars: switch (toUtf32(utf8)) { - REMAP(0x1F44D, 0x01) // 👍 Thumbs Up - REMAP(0x1F44E, 0x02) // 👎 Thumbs Down + REMAP(0x20AC, 0x80); // EURO SIGN + REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK + REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86); // DAGGER + REMAP(0x2021, 0x87); // DOUBLE DAGGER + REMAP(0x2030, 0x89); // PER MILLE SIGN + REMAP(0x0160, 0x8A); // LATIN CAPITAL LETTER S WITH CARON + REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x015A, 0x8C); // LATIN CAPITAL LETTER S WITH ACUTE + REMAP(0x0164, 0x8D); // LATIN CAPITAL LETTER T WITH CARON + REMAP(0x017D, 0x8E); // LATIN CAPITAL LETTER Z WITH CARON + REMAP(0x0179, 0x8F); // LATIN CAPITAL LETTER Z WITH ACUTE - REMAP(0x1F60A, 0x03) // 😊 Smiling Face with Smiling Eyes - REMAP(0x1F642, 0x03) // 🙂 Slightly Smiling Face - REMAP(0x1F601, 0x03) // 😁 Grinning Face with Smiling Eye + REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95); // BULLET + REMAP(0x2013, 0x96); // EN DASH + REMAP(0x2014, 0x97); // EM DASH + REMAP(0x2122, 0x99); // TRADE MARK SIGN + REMAP(0x0161, 0x9A); // LATIN SMALL LETTER S WITH CARON + REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x015B, 0x9C); // LATIN SMALL LETTER S WITH ACUTE + REMAP(0x0165, 0x9D); // LATIN SMALL LETTER T WITH CARON + REMAP(0x017E, 0x9E); // LATIN SMALL LETTER Z WITH CARON + REMAP(0x017A, 0x9F); // LATIN SMALL LETTER Z WITH ACUTE - REMAP(0x1F602, 0x04) // 😂 Face with Tears of Joy - REMAP(0x1F923, 0x04) // 🤣 Rolling on the Floor Laughing - REMAP(0x1F606, 0x04) // 😆 Smiling with Open Mouth and Closed Eyes + REMAP(0x00A0, 0xA0); // NO-BREAK SPACE + REMAP(0x02C7, 0xA1); // CARON + REMAP(0x02D8, 0xA2); // BREVE + REMAP(0x0141, 0xA3); // LATIN CAPITAL LETTER L WITH STROKE + REMAP(0x00A4, 0xA4); // CURRENCY SIGN + REMAP(0x0104, 0xA5); // LATIN CAPITAL LETTER A WITH OGONEK + REMAP(0x00A6, 0xA6); // BROKEN BAR + REMAP(0x00A7, 0xA7); // SECTION SIGN + REMAP(0x00A8, 0xA8); // DIAERESIS + REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN + REMAP(0x015E, 0xAA); // LATIN CAPITAL LETTER S WITH CEDILLA + REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC); // NOT SIGN + REMAP(0x00AD, 0xAD); // SOFT HYPHEN + REMAP(0x00AE, 0xAE); // REGISTERED SIGN + REMAP(0x017B, 0xAF); // LATIN CAPITAL LETTER Z WITH DOT ABOVE - REMAP(0x1F44B, 0x05) // 👋 Waving Hand + REMAP(0x00B0, 0xB0); // DEGREE SIGN + REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN + REMAP(0x02DB, 0xB2); // OGONEK + REMAP(0x0142, 0xB3); // LATIN SMALL LETTER L WITH STROKE + REMAP(0x00B4, 0xB4); // ACUTE ACCENT + REMAP(0x00B5, 0xB5); // MICRO SIGN + REMAP(0x00B6, 0xB6); // PILCROW SIGN + REMAP(0x00B7, 0xB7); // MIDDLE DOT + REMAP(0x00B8, 0xB8); // CEDILLA + REMAP(0x0105, 0xB9); // LATIN SMALL LETTER A WITH OGONEK + REMAP(0x015F, 0xBA); // LATIN SMALL LETTER S WITH CEDILLA + REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x013D, 0xBC); // LATIN CAPITAL LETTER L WITH CARON + REMAP(0x02DD, 0xBD); // DOUBLE ACUTE ACCENT + REMAP(0x013E, 0xBE); // LATIN SMALL LETTER L WITH CARON + REMAP(0x017C, 0xBF); // LATIN SMALL LETTER Z WITH DOT ABOVE - REMAP(0x02600, 0x06) // ☀ Sun - REMAP(0x1F31E, 0x06) // 🌞 Sun with Face + REMAP(0x0154, 0xC0); // LATIN CAPITAL LETTER R WITH ACUTE + REMAP(0x00C1, 0xC1); // LATIN CAPITAL LETTER A WITH ACUTE + REMAP(0x00C2, 0xC2); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + REMAP(0x0102, 0xC3); // LATIN CAPITAL LETTER A WITH BREVE + REMAP(0x00C4, 0xC4); // LATIN CAPITAL LETTER A WITH DIAERESIS + REMAP(0x0139, 0xC5); // LATIN CAPITAL LETTER L WITH ACUTE + REMAP(0x0106, 0xC6); // LATIN CAPITAL LETTER C WITH ACUTE + REMAP(0x00C7, 0xC7); // LATIN CAPITAL LETTER C WITH CEDILLA + REMAP(0x010C, 0xC8); // LATIN CAPITAL LETTER C WITH CARON + REMAP(0x00C9, 0xC9); // LATIN CAPITAL LETTER E WITH ACUTE + REMAP(0x0118, 0xCA); // LATIN CAPITAL LETTER E WITH OGONEK + REMAP(0x00CB, 0xCB); // LATIN CAPITAL LETTER E WITH DIAERESIS + REMAP(0x011A, 0xCC); // LATIN CAPITAL LETTER E WITH CARON + REMAP(0x00CD, 0xCD); // LATIN CAPITAL LETTER I WITH ACUTE + REMAP(0x00CE, 0xCE); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + REMAP(0x010E, 0xCF); // LATIN CAPITAL LETTER D WITH CARON - // 0x07 - Bell character (unused) - REMAP(0x1F327, 0x08) // 🌧️ Cloud with Rain + REMAP(0x0110, 0xD0); // LATIN CAPITAL LETTER D WITH STROKE + REMAP(0x0143, 0xD1); // LATIN CAPITAL LETTER N WITH ACUTE + REMAP(0x0147, 0xD2); // LATIN CAPITAL LETTER N WITH CARON + REMAP(0x00D3, 0xD3); // LATIN CAPITAL LETTER O WITH ACUTE + REMAP(0x00D4, 0xD4); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + REMAP(0x0150, 0xD5); // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + REMAP(0x00D6, 0xD6); // LATIN CAPITAL LETTER O WITH DIAERESIS + REMAP(0x00D7, 0xD7); // MULTIPLICATION SIGN + REMAP(0x0158, 0xD8); // LATIN CAPITAL LETTER R WITH CARON + REMAP(0x016E, 0xD9); // LATIN CAPITAL LETTER U WITH RING ABOVE + REMAP(0x00DA, 0xDA); // LATIN CAPITAL LETTER U WITH ACUTE + REMAP(0x0170, 0xDB); // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + REMAP(0x00DC, 0xDC); // LATIN CAPITAL LETTER U WITH DIAERESIS + REMAP(0x00DD, 0xDD); // LATIN CAPITAL LETTER Y WITH ACUTE + REMAP(0x0162, 0xDE); // LATIN CAPITAL LETTER T WITH CEDILLA + REMAP(0x00DF, 0xDF); // LATIN SMALL LETTER SHARP S - REMAP(0x02601, 0x09) // ☁️ Cloud - REMAP(0x1F32B, 0x09) // Fog + REMAP(0x0155, 0xE0); // LATIN SMALL LETTER R WITH ACUTE + REMAP(0x00E1, 0xE1); // LATIN SMALL LETTER A WITH ACUTE + REMAP(0x00E2, 0xE2); // LATIN SMALL LETTER A WITH CIRCUMFLEX + REMAP(0x0103, 0xE3); // LATIN SMALL LETTER A WITH BREVE + REMAP(0x00E4, 0xE4); // LATIN SMALL LETTER A WITH DIAERESIS + REMAP(0x013A, 0xE5); // LATIN SMALL LETTER L WITH ACUTE + REMAP(0x0107, 0xE6); // LATIN SMALL LETTER C WITH ACUTE + REMAP(0x00E7, 0xE7); // LATIN SMALL LETTER C WITH CEDILLA + REMAP(0x010D, 0xE8); // LATIN SMALL LETTER C WITH CARON + REMAP(0x00E9, 0xE9); // LATIN SMALL LETTER E WITH ACUTE + REMAP(0x0119, 0xEA); // LATIN SMALL LETTER E WITH OGONEK + REMAP(0x00EB, 0xEB); // LATIN SMALL LETTER E WITH DIAERESIS + REMAP(0x011B, 0xEC); // LATIN SMALL LETTER E WITH CARON + REMAP(0x00ED, 0xED); // LATIN SMALL LETTER I WITH ACUTE + REMAP(0x00EE, 0xEE); // LATIN SMALL LETTER I WITH CIRCUMFLEX + REMAP(0x010F, 0xEF); // LATIN SMALL LETTER D WITH CARON - REMAP(0x1F9E1, 0x0B) // 🧡 Orange Heart - REMAP(0x02763, 0x0B) // ❣ Heart Exclamation - REMAP(0x02764, 0x0B) // ❤ Heart - REMAP(0x1F495, 0x0B) // 💕 Two Hearts - REMAP(0x1F496, 0x0B) // 💖 Sparkling Heart - REMAP(0x1F497, 0x0B) // 💗 Growing Heart - REMAP(0x1F498, 0x0B) // 💘 Heart with Arrow - - REMAP(0x1F4A9, 0x0C) // 💩 Pile of Poo - // 0x0D - Carriage return (unused) - REMAP(0x1F514, 0x0E) // 🔔 Bell - - REMAP(0x1F62D, 0x0F) // 😭 Loudly Crying Face - REMAP(0x1F622, 0x0F) // 😢 Crying Face - - REMAP(0x1F64F, 0x10) // 🙏 Person with Folded Hands - REMAP(0x1F618, 0x11) // 😘 Face Throwing a Kiss - REMAP(0x1F389, 0x12) // 🎉 Party Popper - - REMAP(0x1F600, 0x13) // 😀 Grinning Face - REMAP(0x1F603, 0x13) // 😃 Smiling Face with Open Mouth - REMAP(0x1F604, 0x13) // 😄 Smiling Face with Open Mouth and Smiling Eyes - - REMAP(0x1F97A, 0x14) // 🥺 Face with Pleading Eyes - REMAP(0x1F605, 0x15) // 😅 Smiling with Sweat - REMAP(0x1F525, 0x16) // 🔥 Fire - REMAP(0x1F926, 0x17) // 🤦 Face Palm - REMAP(0x1F937, 0x18) // 🤷 Shrug - REMAP(0x1F644, 0x19) // 🙄 Face with Rolling Eyes - // 0x1A Substitution (unused) - REMAP(0x1F917, 0x1B) // 🤗 Hugging Face - - REMAP(0x1F609, 0x1C) // 😉 Winking Face - REMAP(0x1F61C, 0x1C) // 😜 Face with Stuck-Out Tongue and Winking Eye - REMAP(0x1F60F, 0x1C) // 😏 Smirking Face - - REMAP(0x1F914, 0x1D) // 🤔 Thinking Face - REMAP(0x1FAE1, 0x1E) // 🫡 Saluting Face - REMAP(0x1F44C, 0x1F) // 👌 OK Hand Sign - - REMAP(0x02755, '!') // ❕ - REMAP(0x02757, '!') // ❗ - REMAP(0x0203C, '!') // ‼ - REMAP(0x02753, '?') // ❓ - REMAP(0x02754, '?') // ❔ - REMAP(0x02049, '?') // ⁉ - - // Modifiers (deleted) - REMAP(0x02640, 0x7F) // Gender - REMAP(0x02642, 0x7F) - REMAP(0x1F3FB, 0x7F) // Skin Tones - REMAP(0x1F3FC, 0x7F) - REMAP(0x1F3FD, 0x7F) - REMAP(0x1F3FE, 0x7F) - REMAP(0x1F3FF, 0x7F) - REMAP(0x0FE00, 0x7F) // Variation Selectors - REMAP(0x0FE01, 0x7F) - REMAP(0x0FE02, 0x7F) - REMAP(0x0FE03, 0x7F) - REMAP(0x0FE04, 0x7F) - REMAP(0x0FE05, 0x7F) - REMAP(0x0FE06, 0x7F) - REMAP(0x0FE07, 0x7F) - REMAP(0x0FE08, 0x7F) - REMAP(0x0FE09, 0x7F) - REMAP(0x0FE0A, 0x7F) - REMAP(0x0FE0B, 0x7F) - REMAP(0x0FE0C, 0x7F) - REMAP(0x0FE0D, 0x7F) - REMAP(0x0FE0E, 0x7F) - REMAP(0x0FE0F, 0x7F) - REMAP(0x0200D, 0x7F) // Zero Width Joiner + REMAP(0x0111, 0xF0); // LATIN SMALL LETTER D WITH STROKE + REMAP(0x0144, 0xF1); // LATIN SMALL LETTER N WITH ACUTE + REMAP(0x0148, 0xF2); // LATIN SMALL LETTER N WITH CARON + REMAP(0x00F3, 0xF3); // LATIN SMALL LETTER O WITH ACUTE + REMAP(0x00F4, 0xF4); // LATIN SMALL LETTER O WITH CIRCUMFLEX + REMAP(0x0151, 0xF5); // LATIN SMALL LETTER O WITH DOUBLE ACUTE + REMAP(0x00F6, 0xF6); // LATIN SMALL LETTER O WITH DIAERESIS + REMAP(0x00F7, 0xF7); // DIVISION SIGN + REMAP(0x0159, 0xF8); // LATIN SMALL LETTER R WITH CARON + REMAP(0x016F, 0xF9); // LATIN SMALL LETTER U WITH RING ABOVE + REMAP(0x00FA, 0xFA); // LATIN SMALL LETTER U WITH ACUTE + REMAP(0x0171, 0xFB); // LATIN SMALL LETTER U WITH DOUBLE ACUTE + REMAP(0x00FC, 0xFC); // LATIN SMALL LETTER U WITH DIAERESIS + REMAP(0x00FD, 0xFD); // LATIN SMALL LETTER Y WITH ACUTE + REMAP(0x0163, 0xFE); // LATIN SMALL LETTER T WITH CEDILLA + REMAP(0x02D9, 0xFF); // DOT ABOVE } + } - // If not handled, return SUB - return '\x1A'; + // Latin - Cyrillic + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1251.TXT + else if (encoding == WINDOWS_1251) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); + + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x0402, 0x80); // CYRILLIC CAPITAL LETTER DJE + REMAP(0x0403, 0x81); // CYRILLIC CAPITAL LETTER GJE + REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK + REMAP(0x0453, 0x83); // CYRILLIC SMALL LETTER GJE + REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86); // DAGGER + REMAP(0x2021, 0x87); // DOUBLE DAGGER + REMAP(0x20AC, 0x88); // EURO SIGN + REMAP(0x2030, 0x89); // PER MILLE SIGN + REMAP(0x0409, 0x8A); // CYRILLIC CAPITAL LETTER LJE + REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x040A, 0x8C); // CYRILLIC CAPITAL LETTER NJE + REMAP(0x040C, 0x8D); // CYRILLIC CAPITAL LETTER KJE + REMAP(0x040B, 0x8E); // CYRILLIC CAPITAL LETTER TSHE + REMAP(0x040F, 0x8F); // CYRILLIC CAPITAL LETTER DZHE + + REMAP(0x0452, 0x90); // CYRILLIC SMALL LETTER DJE + REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95); // BULLET + REMAP(0x2013, 0x96); // EN DASH + REMAP(0x2014, 0x97); // EM DASH + REMAP(0x2122, 0x99); // TRADE MARK SIGN + REMAP(0x0459, 0x9A); // CYRILLIC SMALL LETTER LJE + REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x045A, 0x9C); // CYRILLIC SMALL LETTER NJE + REMAP(0x045C, 0x9D); // CYRILLIC SMALL LETTER KJE + REMAP(0x045B, 0x9E); // CYRILLIC SMALL LETTER TSHE + REMAP(0x045F, 0x9F); // CYRILLIC SMALL LETTER DZHE + + REMAP(0x00A0, 0xA0); // NO-BREAK SPACE + REMAP(0x040E, 0xA1); // CYRILLIC CAPITAL LETTER SHORT U + REMAP(0x045E, 0xA2); // CYRILLIC SMALL LETTER SHORT U + REMAP(0x0408, 0xA3); // CYRILLIC CAPITAL LETTER JE + REMAP(0x00A4, 0xA4); // CURRENCY SIGN + REMAP(0x0490, 0xA5); // CYRILLIC CAPITAL LETTER GHE WITH UPTURN + REMAP(0x00A6, 0xA6); // BROKEN BAR + REMAP(0x00A7, 0xA7); // SECTION SIGN + REMAP(0x0401, 0xA8); // CYRILLIC CAPITAL LETTER IO + REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN + REMAP(0x0404, 0xAA); // CYRILLIC CAPITAL LETTER UKRAINIAN IE + REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC); // NOT SIGN + REMAP(0x00AD, 0xAD); // SOFT HYPHEN + REMAP(0x00AE, 0xAE); // REGISTERED SIGN + REMAP(0x0407, 0xAF); // CYRILLIC CAPITAL LETTER YI + + REMAP(0x00B0, 0xB0); // DEGREE SIGN + REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN + REMAP(0x0406, 0xB2); // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I + REMAP(0x0456, 0xB3); // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + REMAP(0x0491, 0xB4); // CYRILLIC SMALL LETTER GHE WITH UPTURN + REMAP(0x00B5, 0xB5); // MICRO SIGN + REMAP(0x00B6, 0xB6); // PILCROW SIGN + REMAP(0x00B7, 0xB7); // MIDDLE DOT + REMAP(0x0451, 0xB8); // CYRILLIC SMALL LETTER IO + REMAP(0x2116, 0xB9); // NUMERO SIGN + REMAP(0x0454, 0xBA); // CYRILLIC SMALL LETTER UKRAINIAN IE + REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x0458, 0xBC); // CYRILLIC SMALL LETTER JE + REMAP(0x0405, 0xBD); // CYRILLIC CAPITAL LETTER DZE + REMAP(0x0455, 0xBE); // CYRILLIC SMALL LETTER DZE + REMAP(0x0457, 0xBF); // CYRILLIC SMALL LETTER YI + + REMAP(0x0410, 0xC0); // CYRILLIC CAPITAL LETTER A + REMAP(0x0411, 0xC1); // CYRILLIC CAPITAL LETTER BE + REMAP(0x0412, 0xC2); // CYRILLIC CAPITAL LETTER VE + REMAP(0x0413, 0xC3); // CYRILLIC CAPITAL LETTER GHE + REMAP(0x0414, 0xC4); // CYRILLIC CAPITAL LETTER DE + REMAP(0x0415, 0xC5); // CYRILLIC CAPITAL LETTER IE + REMAP(0x0416, 0xC6); // CYRILLIC CAPITAL LETTER ZHE + REMAP(0x0417, 0xC7); // CYRILLIC CAPITAL LETTER ZE + REMAP(0x0418, 0xC8); // CYRILLIC CAPITAL LETTER I + REMAP(0x0419, 0xC9); // CYRILLIC CAPITAL LETTER SHORT I + REMAP(0x041A, 0xCA); // CYRILLIC CAPITAL LETTER KA + REMAP(0x041B, 0xCB); // CYRILLIC CAPITAL LETTER EL + REMAP(0x041C, 0xCC); // CYRILLIC CAPITAL LETTER EM + REMAP(0x041D, 0xCD); // CYRILLIC CAPITAL LETTER EN + REMAP(0x041E, 0xCE); // CYRILLIC CAPITAL LETTER O + REMAP(0x041F, 0xCF); // CYRILLIC CAPITAL LETTER PE + + REMAP(0x0420, 0xD0); // CYRILLIC CAPITAL LETTER ER + REMAP(0x0421, 0xD1); // CYRILLIC CAPITAL LETTER ES + REMAP(0x0422, 0xD2); // CYRILLIC CAPITAL LETTER TE + REMAP(0x0423, 0xD3); // CYRILLIC CAPITAL LETTER U + REMAP(0x0424, 0xD4); // CYRILLIC CAPITAL LETTER EF + REMAP(0x0425, 0xD5); // CYRILLIC CAPITAL LETTER HA + REMAP(0x0426, 0xD6); // CYRILLIC CAPITAL LETTER TSE + REMAP(0x0427, 0xD7); // CYRILLIC CAPITAL LETTER CHE + REMAP(0x0428, 0xD8); // CYRILLIC CAPITAL LETTER SHA + REMAP(0x0429, 0xD9); // CYRILLIC CAPITAL LETTER SHCHA + REMAP(0x042A, 0xDA); // CYRILLIC CAPITAL LETTER HARD SIGN + REMAP(0x042B, 0xDB); // CYRILLIC CAPITAL LETTER YERU + REMAP(0x042C, 0xDC); // CYRILLIC CAPITAL LETTER SOFT SIGN + REMAP(0x042D, 0xDD); // CYRILLIC CAPITAL LETTER E + REMAP(0x042E, 0xDE); // CYRILLIC CAPITAL LETTER YU + REMAP(0x042F, 0xDF); // CYRILLIC CAPITAL LETTER YA + + REMAP(0x0430, 0xE0); // CYRILLIC SMALL LETTER A + REMAP(0x0431, 0xE1); // CYRILLIC SMALL LETTER BE + REMAP(0x0432, 0xE2); // CYRILLIC SMALL LETTER VE + REMAP(0x0433, 0xE3); // CYRILLIC SMALL LETTER GHE + REMAP(0x0434, 0xE4); // CYRILLIC SMALL LETTER DE + REMAP(0x0435, 0xE5); // CYRILLIC SMALL LETTER IE + REMAP(0x0436, 0xE6); // CYRILLIC SMALL LETTER ZHE + REMAP(0x0437, 0xE7); // CYRILLIC SMALL LETTER ZE + REMAP(0x0438, 0xE8); // CYRILLIC SMALL LETTER I + REMAP(0x0439, 0xE9); // CYRILLIC SMALL LETTER SHORT I + REMAP(0x043A, 0xEA); // CYRILLIC SMALL LETTER KA + REMAP(0x043B, 0xEB); // CYRILLIC SMALL LETTER EL + REMAP(0x043C, 0xEC); // CYRILLIC SMALL LETTER EM + REMAP(0x043D, 0xED); // CYRILLIC SMALL LETTER EN + REMAP(0x043E, 0xEE); // CYRILLIC SMALL LETTER O + REMAP(0x043F, 0xEF); // CYRILLIC SMALL LETTER PE + + REMAP(0x0440, 0xF0); // CYRILLIC SMALL LETTER ER + REMAP(0x0441, 0xF1); // CYRILLIC SMALL LETTER ES + REMAP(0x0442, 0xF2); // CYRILLIC SMALL LETTER TE + REMAP(0x0443, 0xF3); // CYRILLIC SMALL LETTER U + REMAP(0x0444, 0xF4); // CYRILLIC SMALL LETTER EF + REMAP(0x0445, 0xF5); // CYRILLIC SMALL LETTER HA + REMAP(0x0446, 0xF6); // CYRILLIC SMALL LETTER TSE + REMAP(0x0447, 0xF7); // CYRILLIC SMALL LETTER CHE + REMAP(0x0448, 0xF8); // CYRILLIC SMALL LETTER SHA + REMAP(0x0449, 0xF9); // CYRILLIC SMALL LETTER SHCHA + REMAP(0x044A, 0xFA); // CYRILLIC SMALL LETTER HARD SIGN + REMAP(0x044B, 0xFB); // CYRILLIC SMALL LETTER YERU + REMAP(0x044C, 0xFC); // CYRILLIC SMALL LETTER SOFT SIGN + REMAP(0x044D, 0xFD); // CYRILLIC SMALL LETTER E + REMAP(0x044E, 0xFE); // CYRILLIC SMALL LETTER YU + REMAP(0x044F, 0xFF); // CYRILLIC SMALL LETTER YA + } + } + + // Latin - Western Europe + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT + else if (encoding == WINDOWS_1252) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); + + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x20AC, 0x80) // EURO SIGN + REMAP(0x201A, 0x82) // SINGLE LOW-9 QUOTATION MARK + REMAP(0x0192, 0x83) // LATIN SMALL LETTER F WITH HOOK + REMAP(0x201E, 0x84) // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85) // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86) // DAGGER + REMAP(0x2021, 0x87) // DOUBLE DAGGER + REMAP(0x02C6, 0x88) // MODIFIER LETTER CIRCUMFLEX ACCENT + REMAP(0x2030, 0x89) // PER MILLE SIGN + REMAP(0x0160, 0x8A) // LATIN CAPITAL LETTER S WITH CARON + REMAP(0x2039, 0x8B) // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x0152, 0x8C) // LATIN CAPITAL LIGATURE OE + REMAP(0x017D, 0x8E) // LATIN CAPITAL LETTER Z WITH CARON + + REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95) // BULLET + REMAP(0x2013, 0x96) // EN DASH + REMAP(0x2014, 0x97) // EM DASH + REMAP(0x02DC, 0x98) // SMALL TILDE + REMAP(0x2122, 0x99) // TRADE MARK SIGN + REMAP(0x0161, 0x9A) // LATIN SMALL LETTER S WITH CARON + REMAP(0x203A, 0x9B) // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x0153, 0x9C) // LATIN SMALL LIGATURE OE + REMAP(0x017E, 0x9E) // LATIN SMALL LETTER Z WITH CARON + REMAP(0x0178, 0x9F) // LATIN CAPITAL LETTER Y WITH DIAERESIS + + REMAP(0x00A0, 0xA0) // NO-BREAK SPACE + REMAP(0x00A1, 0xA1) // INVERTED EXCLAMATION MARK + REMAP(0x00A2, 0xA2) // CENT SIGN + REMAP(0x00A3, 0xA3) // POUND SIGN + REMAP(0x00A4, 0xA4) // CURRENCY SIGN + REMAP(0x00A5, 0xA5) // YEN SIGN + REMAP(0x00A6, 0xA6) // BROKEN BAR + REMAP(0x00A7, 0xA7) // SECTION SIGN + REMAP(0x00A8, 0xA8) // DIAERESIS + REMAP(0x00A9, 0xA9) // COPYRIGHT SIGN + REMAP(0x00AA, 0xAA) // FEMININE ORDINAL INDICATOR + REMAP(0x00AB, 0xAB) // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC) // NOT SIGN + REMAP(0x00AD, 0xAD) // SOFT HYPHEN + REMAP(0x00AE, 0xAE) // REGISTERED SIGN + REMAP(0x00AF, 0xAF) // MACRON + + REMAP(0x00B0, 0xB0) // DEGREE SIGN + REMAP(0x00B1, 0xB1) // PLUS-MINUS SIGN + REMAP(0x00B2, 0xB2) // SUPERSCRIPT TWO + REMAP(0x00B3, 0xB3) // SUPERSCRIPT THREE + REMAP(0x00B4, 0xB4) // ACUTE ACCENT + REMAP(0x00B5, 0xB5) // MICRO SIGN + REMAP(0x00B6, 0xB6) // PILCROW SIGN + REMAP(0x00B7, 0xB7) // MIDDLE DOT + REMAP(0x00B8, 0xB8) // CEDILLA + REMAP(0x00B9, 0xB9) // SUPERSCRIPT ONE + REMAP(0x00BA, 0xBA) // MASCULINE ORDINAL INDICATOR + REMAP(0x00BB, 0xBB) // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00BC, 0xBC) // VULGAR FRACTION ONE QUARTER + REMAP(0x00BD, 0xBD) // VULGAR FRACTION ONE HALF + REMAP(0x00BE, 0xBE) // VULGAR FRACTION THREE QUARTERS + REMAP(0x00BF, 0xBF) // INVERTED QUESTION MARK + + REMAP(0x00C0, 0xC0) // LATIN CAPITAL LETTER A WITH GRAVE + REMAP(0x00C1, 0xC1) // LATIN CAPITAL LETTER A WITH ACUTE + REMAP(0x00C2, 0xC2) // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + REMAP(0x00C3, 0xC3) // LATIN CAPITAL LETTER A WITH TILDE + REMAP(0x00C4, 0xC4) // LATIN CAPITAL LETTER A WITH DIAERESIS + REMAP(0x00C5, 0xC5) // LATIN CAPITAL LETTER A WITH RING ABOVE + REMAP(0x00C6, 0xC6) // LATIN CAPITAL LETTER AE + REMAP(0x00C7, 0xC7) // LATIN CAPITAL LETTER C WITH CEDILLA + REMAP(0x00C8, 0xC8) // LATIN CAPITAL LETTER E WITH GRAVE + REMAP(0x00C9, 0xC9) // LATIN CAPITAL LETTER E WITH ACUTE + REMAP(0x00CA, 0xCA) // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + REMAP(0x00CB, 0xCB) // LATIN CAPITAL LETTER E WITH DIAERESIS + REMAP(0x00CC, 0xCC) // LATIN CAPITAL LETTER I WITH GRAVE + REMAP(0x00CD, 0xCD) // LATIN CAPITAL LETTER I WITH ACUTE + REMAP(0x00CE, 0xCE) // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + REMAP(0x00CF, 0xCF) // LATIN CAPITAL LETTER I WITH DIAERESIS + + REMAP(0x00D0, 0xD0) // LATIN CAPITAL LETTER ETH + REMAP(0x00D1, 0xD1) // LATIN CAPITAL LETTER N WITH TILDE + REMAP(0x00D2, 0xD2) // LATIN CAPITAL LETTER O WITH GRAVE + REMAP(0x00D3, 0xD3) // LATIN CAPITAL LETTER O WITH ACUTE + REMAP(0x00D4, 0xD4) // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + REMAP(0x00D5, 0xD5) // LATIN CAPITAL LETTER O WITH TILDE + REMAP(0x00D6, 0xD6) // LATIN CAPITAL LETTER O WITH DIAERESIS + REMAP(0x00D7, 0xD7) // MULTIPLICATION SIGN + REMAP(0x00D8, 0xD8) // LATIN CAPITAL LETTER O WITH STROKE + REMAP(0x00D9, 0xD9) // LATIN CAPITAL LETTER U WITH GRAVE + REMAP(0x00DA, 0xDA) // LATIN CAPITAL LETTER U WITH ACUTE + REMAP(0x00DB, 0xDB) // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + REMAP(0x00DC, 0xDC) // LATIN CAPITAL LETTER U WITH DIAERESIS + REMAP(0x00DD, 0xDD) // LATIN CAPITAL LETTER Y WITH ACUTE + REMAP(0x00DE, 0xDE) // LATIN CAPITAL LETTER THORN + REMAP(0x00DF, 0xDF) // LATIN SMALL LETTER SHARP S + + REMAP(0x00E0, 0xE0) // LATIN SMALL LETTER A WITH GRAVE + REMAP(0x00E1, 0xE1) // LATIN SMALL LETTER A WITH ACUTE + REMAP(0x00E2, 0xE2) // LATIN SMALL LETTER A WITH CIRCUMFLEX + REMAP(0x00E3, 0xE3) // LATIN SMALL LETTER A WITH TILDE + REMAP(0x00E4, 0xE4) // LATIN SMALL LETTER A WITH DIAERESIS + REMAP(0x00E5, 0xE5) // LATIN SMALL LETTER A WITH RING ABOVE + REMAP(0x00E6, 0xE6) // LATIN SMALL LETTER AE + REMAP(0x00E7, 0xE7) // LATIN SMALL LETTER C WITH CEDILLA + REMAP(0x00E8, 0xE8) // LATIN SMALL LETTER E WITH GRAVE + REMAP(0x00E9, 0xE9) // LATIN SMALL LETTER E WITH ACUTE + REMAP(0x00EA, 0xEA) // LATIN SMALL LETTER E WITH CIRCUMFLEX + REMAP(0x00EB, 0xEB) // LATIN SMALL LETTER E WITH DIAERESIS + REMAP(0x00EC, 0xEC) // LATIN SMALL LETTER I WITH GRAVE + REMAP(0x00ED, 0xED) // LATIN SMALL LETTER I WITH ACUTE + REMAP(0x00EE, 0xEE) // LATIN SMALL LETTER I WITH CIRCUMFLEX + REMAP(0x00EF, 0xEF) // LATIN SMALL LETTER I WITH DIAERESIS + + REMAP(0x00F0, 0xF0) // LATIN SMALL LETTER ETH + REMAP(0x00F1, 0xF1) // LATIN SMALL LETTER N WITH TILDE + REMAP(0x00F2, 0xF2) // LATIN SMALL LETTER O WITH GRAVE + REMAP(0x00F3, 0xF3) // LATIN SMALL LETTER O WITH ACUTE + REMAP(0x00F4, 0xF4) // LATIN SMALL LETTER O WITH CIRCUMFLEX + REMAP(0x00F5, 0xF5) // LATIN SMALL LETTER O WITH TILDE + REMAP(0x00F6, 0xF6) // LATIN SMALL LETTER O WITH DIAERESIS + REMAP(0x00F7, 0xF7) // DIVISION SIGN + REMAP(0x00F8, 0xF8) // LATIN SMALL LETTER O WITH STROKE + REMAP(0x00F9, 0xF9) // LATIN SMALL LETTER U WITH GRAVE + REMAP(0x00FA, 0xFA) // LATIN SMALL LETTER U WITH ACUTE + REMAP(0x00FB, 0xFB) // LATIN SMALL LETTER U WITH CIRCUMFLEX + REMAP(0x00FC, 0xFC) // LATIN SMALL LETTER U WITH DIAERESIS + REMAP(0x00FD, 0xFD) // LATIN SMALL LETTER Y WITH ACUTE + REMAP(0x00FE, 0xFE) // LATIN SMALL LETTER THORN + REMAP(0x00FF, 0xFF) // LATIN SMALL LETTER Y WITH DIAERESIS + } + } + + else /*ASCII or Unhandled*/ { + if (utf8.length() == 1) + return utf8.at(0); + } + + // All single-byte (ASCII) characters should have been handled by now + // Only unhandled multi-byte UTF8 characters should remain + assert(utf8.length() > 1); + + // Parse emoji + // Strip emoji modifiers + switch (toUtf32(utf8)) { + REMAP(0x1F44D, 0x01) // 👍 Thumbs Up + REMAP(0x1F44E, 0x02) // 👎 Thumbs Down + + REMAP(0x1F60A, 0x03) // 😊 Smiling Face with Smiling Eyes + REMAP(0x1F642, 0x03) // 🙂 Slightly Smiling Face + REMAP(0x1F601, 0x03) // 😁 Grinning Face with Smiling Eye + + REMAP(0x1F602, 0x04) // 😂 Face with Tears of Joy + REMAP(0x1F923, 0x04) // 🤣 Rolling on the Floor Laughing + REMAP(0x1F606, 0x04) // 😆 Smiling with Open Mouth and Closed Eyes + + REMAP(0x1F44B, 0x05) // 👋 Waving Hand + + REMAP(0x02600, 0x06) // ☀ Sun + REMAP(0x1F31E, 0x06) // 🌞 Sun with Face + + // 0x07 - Bell character (unused) + REMAP(0x1F327, 0x08) // 🌧️ Cloud with Rain + + REMAP(0x02601, 0x09) // ☁️ Cloud + REMAP(0x1F32B, 0x09) // Fog + + REMAP(0x1F9E1, 0x0B) // 🧡 Orange Heart + REMAP(0x02763, 0x0B) // ❣ Heart Exclamation + REMAP(0x02764, 0x0B) // ❤ Heart + REMAP(0x1F495, 0x0B) // 💕 Two Hearts + REMAP(0x1F496, 0x0B) // 💖 Sparkling Heart + REMAP(0x1F497, 0x0B) // 💗 Growing Heart + REMAP(0x1F498, 0x0B) // 💘 Heart with Arrow + + REMAP(0x1F4A9, 0x0C) // 💩 Pile of Poo + // 0x0D - Carriage return (unused) + REMAP(0x1F514, 0x0E) // 🔔 Bell + + REMAP(0x1F62D, 0x0F) // 😭 Loudly Crying Face + REMAP(0x1F622, 0x0F) // 😢 Crying Face + + REMAP(0x1F64F, 0x10) // 🙏 Person with Folded Hands + REMAP(0x1F618, 0x11) // 😘 Face Throwing a Kiss + REMAP(0x1F389, 0x12) // 🎉 Party Popper + + REMAP(0x1F600, 0x13) // 😀 Grinning Face + REMAP(0x1F603, 0x13) // 😃 Smiling Face with Open Mouth + REMAP(0x1F604, 0x13) // 😄 Smiling Face with Open Mouth and Smiling Eyes + + REMAP(0x1F97A, 0x14) // 🥺 Face with Pleading Eyes + REMAP(0x1F605, 0x15) // 😅 Smiling with Sweat + REMAP(0x1F525, 0x16) // 🔥 Fire + REMAP(0x1F926, 0x17) // 🤦 Face Palm + REMAP(0x1F937, 0x18) // 🤷 Shrug + REMAP(0x1F644, 0x19) // 🙄 Face with Rolling Eyes + // 0x1A Substitution (unused) + REMAP(0x1F917, 0x1B) // 🤗 Hugging Face + + REMAP(0x1F609, 0x1C) // 😉 Winking Face + REMAP(0x1F61C, 0x1C) // 😜 Face with Stuck-Out Tongue and Winking Eye + REMAP(0x1F60F, 0x1C) // 😏 Smirking Face + + REMAP(0x1F914, 0x1D) // 🤔 Thinking Face + REMAP(0x1FAE1, 0x1E) // 🫡 Saluting Face + REMAP(0x1F44C, 0x1F) // 👌 OK Hand Sign + + REMAP(0x02755, '!') // ❕ + REMAP(0x02757, '!') // ❗ + REMAP(0x0203C, '!') // ‼ + REMAP(0x02753, '?') // ❓ + REMAP(0x02754, '?') // ❔ + REMAP(0x02049, '?') // ⁉ + + // Modifiers (deleted) + REMAP(0x02640, 0x7F) // Gender + REMAP(0x02642, 0x7F) + REMAP(0x1F3FB, 0x7F) // Skin Tones + REMAP(0x1F3FC, 0x7F) + REMAP(0x1F3FD, 0x7F) + REMAP(0x1F3FE, 0x7F) + REMAP(0x1F3FF, 0x7F) + REMAP(0x0FE00, 0x7F) // Variation Selectors + REMAP(0x0FE01, 0x7F) + REMAP(0x0FE02, 0x7F) + REMAP(0x0FE03, 0x7F) + REMAP(0x0FE04, 0x7F) + REMAP(0x0FE05, 0x7F) + REMAP(0x0FE06, 0x7F) + REMAP(0x0FE07, 0x7F) + REMAP(0x0FE08, 0x7F) + REMAP(0x0FE09, 0x7F) + REMAP(0x0FE0A, 0x7F) + REMAP(0x0FE0B, 0x7F) + REMAP(0x0FE0C, 0x7F) + REMAP(0x0FE0D, 0x7F) + REMAP(0x0FE0E, 0x7F) + REMAP(0x0FE0F, 0x7F) + REMAP(0x0200D, 0x7F) // Zero Width Joiner + } + + // If not handled, return SUB + return '\x1A'; // Sweep up the syntactic sugar // Don't want ants in the house diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index e1fe37974..560608601 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -14,42 +14,40 @@ #include // GFXRoot drawing lib -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { // An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD -class AppletFont -{ - public: - enum Encoding { - ASCII, - WINDOWS_1250, - WINDOWS_1251, - WINDOWS_1252, - }; +class AppletFont { +public: + enum Encoding { + ASCII, + WINDOWS_1250, + WINDOWS_1251, + WINDOWS_1252, + }; - AppletFont(); - AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); + AppletFont(); + AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); - uint8_t lineHeight(); - uint8_t heightAboveCursor(); - uint8_t heightBelowCursor(); - uint8_t widthBetweenWords(); // Width of the space character + uint8_t lineHeight(); + uint8_t heightAboveCursor(); + uint8_t heightBelowCursor(); + uint8_t widthBetweenWords(); // Width of the space character - std::string decodeUTF8(std::string encoded); + std::string decodeUTF8(std::string encoded); - const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font + const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font - private: - uint32_t toUtf32(std::string utf8); - char applyEncoding(std::string utf8); +private: + uint32_t toUtf32(std::string utf8); + char applyEncoding(std::string utf8); - uint8_t height = 8; // Default value: in-built AdafruitGFX font - uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font - uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font - uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font + uint8_t height = 8; // Default value: in-built AdafruitGFX font + uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font + uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font + uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font - Encoding encoding = ASCII; + Encoding encoding = ASCII; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index d383a11e4..946b4dcb4 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -4,157 +4,156 @@ using namespace NicheGraphics; -void InkHUD::MapApplet::onRender() -{ - // Abort if no markers to render - if (!enoughMarkers()) { - printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE); - printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); - return; +void InkHUD::MapApplet::onRender() { + // Abort if no markers to render + if (!enoughMarkers()) { + printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE); + printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); + return; + } + + // Helper: draw rounded rectangle centered at x,y + auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) { + int16_t x = cx - (w / 2); + int16_t y = cy - (h / 2); + + // center rects + fillRect(x + r, y, w - 2 * r, h, color); + fillRect(x, y + r, r, h - 2 * r, color); + fillRect(x + w - r, y + r, r, h - 2 * r, color); + + // corners + fillCircle(x + r, y + r, r, color); + fillCircle(x + w - r - 1, y + r, r, color); + fillCircle(x + r, y + h - r - 1, r, color); + fillCircle(x + w - r - 1, y + h - r - 1, r, color); + }; + + // Find center of map + getMapCenter(&latCenter, &lngCenter); + calculateAllMarkers(); + getMapSize(&widthMeters, &heightMeters); + calculateMapScale(); + + // Draw all markers first + for (Marker m : markers) { + int16_t x = X(0.5) + (m.eastMeters * metersToPx); + int16_t y = Y(0.5) - (m.northMeters * metersToPx); + + // Add white halo outline first + constexpr int outlinePad = 1; + int boxSize = 11; + int radius = 2; // rounded corner radius + + // White halo background + fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE); + + // Draw inner box + fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK); + + // Text inside + setFont(fontSmall); + setTextColor(WHITE); + + // Draw actual marker on top + if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) { + printAt(x + 1, y + 1, "X", CENTER, MIDDLE); + } else if (!m.hasHopsAway) { + printAt(x + 1, y + 1, "?", CENTER, MIDDLE); + } else { + char hopStr[4]; + snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway); + printAt(x, y + 1, hopStr, CENTER, MIDDLE); } - // Helper: draw rounded rectangle centered at x,y - auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) { - int16_t x = cx - (w / 2); - int16_t y = cy - (h / 2); + // Restore default font and color + setFont(fontSmall); + setTextColor(BLACK); + } - // center rects - fillRect(x + r, y, w - 2 * r, h, color); - fillRect(x, y + r, r, h - 2 * r, color); - fillRect(x + w - r, y + r, r, h - 2 * r, color); + // Dual map scale bars + int16_t horizPx = width() * 0.25f; + int16_t vertPx = height() * 0.25f; + float horizMeters = horizPx / metersToPx; + float vertMeters = vertPx / metersToPx; - // corners - fillCircle(x + r, y + r, r, color); - fillCircle(x + w - r - 1, y + r, r, color); - fillCircle(x + r, y + h - r - 1, r, color); - fillCircle(x + w - r - 1, y + h - r - 1, r, color); - }; - - // Find center of map - getMapCenter(&latCenter, &lngCenter); - calculateAllMarkers(); - getMapSize(&widthMeters, &heightMeters); - calculateMapScale(); - - // Draw all markers first - for (Marker m : markers) { - int16_t x = X(0.5) + (m.eastMeters * metersToPx); - int16_t y = Y(0.5) - (m.northMeters * metersToPx); - - // Add white halo outline first - constexpr int outlinePad = 1; - int boxSize = 11; - int radius = 2; // rounded corner radius - - // White halo background - fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE); - - // Draw inner box - fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK); - - // Text inside - setFont(fontSmall); - setTextColor(WHITE); - - // Draw actual marker on top - if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) { - printAt(x + 1, y + 1, "X", CENTER, MIDDLE); - } else if (!m.hasHopsAway) { - printAt(x + 1, y + 1, "?", CENTER, MIDDLE); - } else { - char hopStr[4]; - snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway); - printAt(x, y + 1, hopStr, CENTER, MIDDLE); - } - - // Restore default font and color - setFont(fontSmall); - setTextColor(BLACK); + auto formatDistance = [&](float meters, char *out, size_t len) { + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + float feet = meters * 3.28084f; + if (feet < 528) + snprintf(out, len, "%.0f ft", feet); + else { + float miles = feet / 5280.0f; + snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles); + } + } else { + if (meters >= 1000) + snprintf(out, len, "%.1f km", meters / 1000.0f); + else + snprintf(out, len, "%.0f m", meters); } + }; - // Dual map scale bars - int16_t horizPx = width() * 0.25f; - int16_t vertPx = height() * 0.25f; - float horizMeters = horizPx / metersToPx; - float vertMeters = vertPx / metersToPx; + // Horizontal scale bar + int16_t horizBarY = height() - 2; + int16_t horizBarX = 1; + drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK); + drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK); + drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK); - auto formatDistance = [&](float meters, char *out, size_t len) { - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - float feet = meters * 3.28084f; - if (feet < 528) - snprintf(out, len, "%.0f ft", feet); - else { - float miles = feet / 5280.0f; - snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles); - } - } else { - if (meters >= 1000) - snprintf(out, len, "%.1f km", meters / 1000.0f); - else - snprintf(out, len, "%.0f m", meters); - } - }; + char horizLabel[32]; + formatDistance(horizMeters, horizLabel, sizeof(horizLabel)); + int16_t horizLabelW = getTextWidth(horizLabel); + int16_t horizLabelH = getFont().lineHeight(); + int16_t horizLabelX = horizBarX + horizPx + 4; + int16_t horizLabelY = horizBarY - horizLabelH + 1; + fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE); + printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM); - // Horizontal scale bar - int16_t horizBarY = height() - 2; - int16_t horizBarX = 1; - drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK); - drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK); - drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK); + // Vertical scale bar + int16_t vertBarX = 1; + int16_t vertBarBottom = horizBarY; + int16_t vertBarTop = vertBarBottom - vertPx; + drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK); + drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK); + drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK); - char horizLabel[32]; - formatDistance(horizMeters, horizLabel, sizeof(horizLabel)); - int16_t horizLabelW = getTextWidth(horizLabel); - int16_t horizLabelH = getFont().lineHeight(); - int16_t horizLabelX = horizBarX + horizPx + 4; - int16_t horizLabelY = horizBarY - horizLabelH + 1; - fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE); - printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM); + char vertTopLabel[32]; + formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel)); + int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2; + int16_t topLabelW = getTextWidth(vertTopLabel); + int16_t topLabelH = getFont().lineHeight(); + fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE); + printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE); - // Vertical scale bar - int16_t vertBarX = 1; - int16_t vertBarBottom = horizBarY; - int16_t vertBarTop = vertBarBottom - vertPx; - drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK); - drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK); - drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK); + char vertBottomLabel[32]; + formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel)); + int16_t bottomLabelY = vertBarBottom + 4; + int16_t bottomLabelW = getTextWidth(vertBottomLabel); + int16_t bottomLabelH = getFont().lineHeight(); + fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE); + printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); - char vertTopLabel[32]; - formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel)); - int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2; - int16_t topLabelW = getTextWidth(vertTopLabel); - int16_t topLabelH = getFont().lineHeight(); - fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE); - printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE); + // Draw our node LAST with full white fill + outline + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && nodeDB->hasValidPosition(ourNode)) { + Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); - char vertBottomLabel[32]; - formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel)); - int16_t bottomLabelY = vertBarBottom + 4; - int16_t bottomLabelW = getTextWidth(vertBottomLabel); - int16_t bottomLabelH = getFont().lineHeight(); - fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE); - printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); + int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); + int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); - // Draw our node LAST with full white fill + outline - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && nodeDB->hasValidPosition(ourNode)) { - Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); + // White fill background + halo + fillCircle(centerX, centerY, 8, WHITE); // big white base + drawCircle(centerX, centerY, 8, WHITE); // crisp edge - int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); - int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); + // Black bullseye on top + drawCircle(centerX, centerY, 6, BLACK); + fillCircle(centerX, centerY, 2, BLACK); - // White fill background + halo - fillCircle(centerX, centerY, 8, WHITE); // big white base - drawCircle(centerX, centerY, 8, WHITE); // crisp edge - - // Black bullseye on top - drawCircle(centerX, centerY, 6, BLACK); - fillCircle(centerX, centerY, 2, BLACK); - - // Crosshairs - drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK); - drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK); - } + // Crosshairs + drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK); + drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK); + } } // Find the center point, in the middle of all node positions @@ -163,396 +162,387 @@ void InkHUD::MapApplet::onRender() // - Calculates furthest nodes from "mean lat long" // - Place map center directly between these furthest nodes -void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) -{ - // If we have a valid position for our own node, use that as the anchor - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && nodeDB->hasValidPosition(ourNode)) { - *lat = ourNode->position.latitude_i * 1e-7; - *lng = ourNode->position.longitude_i * 1e-7; - } else { - // Find mean lat long coords - // ============================ - // - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet - // - averages the x, y and z coords - // - uses tan to find angles for lat / long degrees - // - longitude: triangle formed by x and y (on plane of the equator) - // - latitude: triangle formed by z (north south), - // and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's - // surface +void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) { + // If we have a valid position for our own node, use that as the anchor + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && nodeDB->hasValidPosition(ourNode)) { + *lat = ourNode->position.latitude_i * 1e-7; + *lng = ourNode->position.longitude_i * 1e-7; + } else { + // Find mean lat long coords + // ============================ + // - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet + // - averages the x, y and z coords + // - uses tan to find angles for lat / long degrees + // - longitude: triangle formed by x and y (on plane of the equator) + // - latitude: triangle formed by z (north south), + // and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's + // surface - // Working totals, averaged after nodeDB processed - uint32_t positionCount = 0; - float xAvg = 0; - float yAvg = 0; - float zAvg = 0; + // Working totals, averaged after nodeDB processed + uint32_t positionCount = 0; + float xAvg = 0; + float yAvg = 0; + float zAvg = 0; - // For each node in db - for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // For each node in db + for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Skip if no position - if (!nodeDB->hasValidPosition(node)) - continue; + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; - // Skip if derived applet doesn't want to show this node on the map - if (!shouldDrawNode(node)) - continue; + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; - // Latitude and Longitude of node, in radians - float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; - float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; + // Latitude and Longitude of node, in radians + float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; + float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; - // Convert to cartesian points, with center of earth at 0, 0, 0 - // Exact distance from center is irrelevant, as we're only interested in the vector - float x = cos(latRad) * cos(lngRad); - float y = cos(latRad) * sin(lngRad); - float z = sin(latRad); + // Convert to cartesian points, with center of earth at 0, 0, 0 + // Exact distance from center is irrelevant, as we're only interested in the vector + float x = cos(latRad) * cos(lngRad); + float y = cos(latRad) * sin(lngRad); + float z = sin(latRad); - // To find mean values shortly - xAvg += x; - yAvg += y; - zAvg += z; - positionCount++; - } - - // All NodeDB processed, find mean values - xAvg /= positionCount; - yAvg /= positionCount; - zAvg /= positionCount; - - // Longitude from cartesian coords - // (Angle from 3D coords describing a point of globe's surface) - /* - UK - /-------\ - (Top View) /- -\ - /- (You) -\ - /- . -\ - /- . X -\ - Asia - ... - USA - \- Y -/ - \- -/ - \- -/ - \- -/ - \- -----/ - Pacific - - */ - - *lng = atan2(yAvg, xAvg) * RAD_TO_DEG; - - // Latitude from cartesian coords - // (Angle from 3D coords describing a point on the globe's surface) - // As latitude increases, distance from the Earth's north-south axis out to our surface point decreases. - // Means we need to first find the hypotenuse which becomes base of our triangle in the second step - /* - UK North - /-------\ (Front View) /-------\ - (Top View) /- -\ /- -\ - /- (You) -\ /-(You) -\ - /- /. -\ /- . -\ - /- √X²+Y²/ . X -\ /- Z . -\ - Asia - /... - USA - ..... - - \- Y -/ \- √X²+Y² -/ - \- -/ \- -/ - \- -/ \- -/ - \- -/ \- -/ - \- -----/ \- -----/ - Pacific South - */ - - float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect - *lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG; + // To find mean values shortly + xAvg += x; + yAvg += y; + zAvg += z; + positionCount++; } - // Use either our node position, or the mean fallback as the center - latCenter = *lat; - lngCenter = *lng; + // All NodeDB processed, find mean values + xAvg /= positionCount; + yAvg /= positionCount; + zAvg /= positionCount; - // ---------------------------------------------- - // This has given us either: - // - our actual position (preferred), or - // - a mean position (fallback if we had no fix) - // - // What we actually want is to place our center so that our outermost nodes - // end up on the border of our map. The only real use of our "center" is to give - // us a reference frame: which direction is east, and which is west. - //------------------------------------------------ + // Longitude from cartesian coords + // (Angle from 3D coords describing a point of globe's surface) + /* + UK + /-------\ + (Top View) /- -\ + /- (You) -\ + /- . -\ + /- . X -\ + Asia - ... - USA + \- Y -/ + \- -/ + \- -/ + \- -/ + \- -----/ + Pacific - // Find furthest nodes from our center - // ======================================== - float northernmost = latCenter; - float southernmost = latCenter; - float easternmost = lngCenter; - float westernmost = lngCenter; + */ - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + *lng = atan2(yAvg, xAvg) * RAD_TO_DEG; - // Skip if no position - if (!nodeDB->hasValidPosition(node)) - continue; + // Latitude from cartesian coords + // (Angle from 3D coords describing a point on the globe's surface) + // As latitude increases, distance from the Earth's north-south axis out to our surface point decreases. + // Means we need to first find the hypotenuse which becomes base of our triangle in the second step + /* + UK North + /-------\ (Front View) /-------\ + (Top View) /- -\ /- -\ + /- (You) -\ /-(You) -\ + /- /. -\ /- . -\ + /- √X²+Y²/ . X -\ /- Z . -\ + Asia - /... - USA - ..... - + \- Y -/ \- √X²+Y² -/ + \- -/ \- -/ + \- -/ \- -/ + \- -/ \- -/ + \- -----/ \- -----/ + Pacific South + */ - // Skip if derived applet doesn't want to show this node on the map - if (!shouldDrawNode(node)) - continue; + float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect + *lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG; + } - // Check for a new top or bottom latitude - float latNode = node->position.latitude_i * 1e-7; - northernmost = max(northernmost, latNode); - southernmost = min(southernmost, latNode); + // Use either our node position, or the mean fallback as the center + latCenter = *lat; + lngCenter = *lng; - // Longitude is trickier - float lngNode = node->position.longitude_i * 1e-7; - float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node - float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node - if (degEastward < degWestward) - easternmost = max(easternmost, lngCenter + degEastward); - else - westernmost = min(westernmost, lngCenter - degWestward); - } + // ---------------------------------------------- + // This has given us either: + // - our actual position (preferred), or + // - a mean position (fallback if we had no fix) + // + // What we actually want is to place our center so that our outermost nodes + // end up on the border of our map. The only real use of our "center" is to give + // us a reference frame: which direction is east, and which is west. + //------------------------------------------------ - // Todo: check for issues with map spans >180 deg. MQTT only.. - latCenter = (northernmost + southernmost) / 2; - lngCenter = (westernmost + easternmost) / 2; + // Find furthest nodes from our center + // ======================================== + float northernmost = latCenter; + float southernmost = latCenter; + float easternmost = lngCenter; + float westernmost = lngCenter; - // In case our new center is west of -180, or east of +180, for some reason - lngCenter = fmod(lngCenter, 180); + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; + + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; + + // Check for a new top or bottom latitude + float latNode = node->position.latitude_i * 1e-7; + northernmost = max(northernmost, latNode); + southernmost = min(southernmost, latNode); + + // Longitude is trickier + float lngNode = node->position.longitude_i * 1e-7; + float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node + float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node + if (degEastward < degWestward) + easternmost = max(easternmost, lngCenter + degEastward); + else + westernmost = min(westernmost, lngCenter - degWestward); + } + + // Todo: check for issues with map spans >180 deg. MQTT only.. + latCenter = (northernmost + southernmost) / 2; + lngCenter = (westernmost + easternmost) / 2; + + // In case our new center is west of -180, or east of +180, for some reason + lngCenter = fmod(lngCenter, 180); } // Size of map in meters // Grown to fit the nodes furthest from map center // Overridable if derived applet wants a custom map size (fixed size?) -void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) -{ - // Reset the value - *widthMeters = 0; - *heightMeters = 0; +void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) { + // Reset the value + *widthMeters = 0; + *heightMeters = 0; - // Find the greatest distance horizontally and vertically from map center - for (Marker m : markers) { - *widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2); - *heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2); - } + // Find the greatest distance horizontally and vertically from map center + for (Marker m : markers) { + *widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2); + *heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2); + } - // Add padding - *widthMeters *= 1.1; - *heightMeters *= 1.1; + // Add padding + *widthMeters *= 1.1; + *heightMeters *= 1.1; } // Convert and store info we need for drawing a marker // Lat / long to "meters relative to map center", for position on screen // Info about hopsAway, for marker size -InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) -{ - assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling. +InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) { + assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling. - // Bearing and distance from map center to node - float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng); - float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians + // Bearing and distance from map center to node + float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng); + float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians - // Split into meters north and meters east components (signed) - // - signedness of cos / sin automatically sets negative if south or west - float northMeters = cos(bearingFromCenter) * distanceFromCenter; - float eastMeters = sin(bearingFromCenter) * distanceFromCenter; + // Split into meters north and meters east components (signed) + // - signedness of cos / sin automatically sets negative if south or west + float northMeters = cos(bearingFromCenter) * distanceFromCenter; + float eastMeters = sin(bearingFromCenter) * distanceFromCenter; - // Store this as a new marker - Marker m; - m.eastMeters = eastMeters; - m.northMeters = northMeters; - m.hasHopsAway = hasHopsAway; - m.hopsAway = hopsAway; - return m; + // Store this as a new marker + Marker m; + m.eastMeters = eastMeters; + m.northMeters = northMeters; + m.hasHopsAway = hasHopsAway; + m.hopsAway = hopsAway; + return m; } // Draw a marker on the map for a node, with a shortname label, and backing box -void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) -{ - // Find x and y position based on node's position in nodeDB - assert(nodeDB->hasValidPosition(node)); - Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style - node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style - node->has_hops_away, // Is the hopsAway number valid - node->hops_away // Hops away - ); +void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) { + // Find x and y position based on node's position in nodeDB + assert(nodeDB->hasValidPosition(node)); + Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + ); - // Convert to pixel coords - int16_t markerX = X(0.5) + (m.eastMeters * metersToPx); - int16_t markerY = Y(0.5) - (m.northMeters * metersToPx); + // Convert to pixel coords + int16_t markerX = X(0.5) + (m.eastMeters * metersToPx); + int16_t markerY = Y(0.5) - (m.northMeters * metersToPx); - constexpr uint16_t paddingH = 2; - constexpr uint16_t paddingW = 4; - uint16_t paddingInnerW = 2; // Zero'd out if no text - constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) - constexpr uint16_t markerSizeMin = 5; + constexpr uint16_t paddingH = 2; + constexpr uint16_t paddingW = 4; + uint16_t paddingInnerW = 2; // Zero'd out if no text + constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) + constexpr uint16_t markerSizeMin = 5; - int16_t textX; - int16_t textY; - uint16_t textW; - uint16_t textH; - int16_t labelX; - int16_t labelY; - uint16_t labelW; - uint16_t labelH; - uint8_t markerSize; + int16_t textX; + int16_t textY; + uint16_t textW; + uint16_t textH; + int16_t labelX; + int16_t labelY; + uint16_t labelW; + uint16_t labelH; + uint8_t markerSize; - bool tooManyHops = node->hops_away > config.lora.hop_limit; - bool isOurNode = node->num == nodeDB->getNodeNum(); - bool unknownHops = !node->has_hops_away && !isOurNode; + bool tooManyHops = node->hops_away > config.lora.hop_limit; + bool isOurNode = node->num == nodeDB->getNodeNum(); + bool unknownHops = !node->has_hops_away && !isOurNode; - // Parse any non-ascii chars in the short name, - // and use last 4 instead if unknown / can't render - std::string shortName = parseShortName(node); + // Parse any non-ascii chars in the short name, + // and use last 4 instead if unknown / can't render + std::string shortName = parseShortName(node); - // We will draw a left or right hand variant, to place text towards screen center - // Hopefully avoid text spilling off screen - // Most values are the same, regardless of left-right handedness + // We will draw a left or right hand variant, to place text towards screen center + // Hopefully avoid text spilling off screen + // Most values are the same, regardless of left-right handedness - // Pick emblem style - if (tooManyHops) - markerSize = getTextWidth("!"); - else if (unknownHops) - markerSize = markerSizeMin; - else - markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); + // Pick emblem style + if (tooManyHops) + markerSize = getTextWidth("!"); + else if (unknownHops) + markerSize = markerSizeMin; + else + markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); - // Common dimensions (left or right variant) - textW = getTextWidth(shortName); - if (textW == 0) - paddingInnerW = 0; // If no text, no padding for text - textH = fontSmall.lineHeight(); - labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH; - labelY = markerY - (labelH / 2); - textY = markerY; - labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant + // Common dimensions (left or right variant) + textW = getTextWidth(shortName); + if (textW == 0) + paddingInnerW = 0; // If no text, no padding for text + textH = fontSmall.lineHeight(); + labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH; + labelY = markerY - (labelH / 2); + textY = markerY; + labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant - // Left-side variant - if (markerX < width() / 2) { - labelX = markerX - (markerSize / 2) - paddingW; - textX = labelX + paddingW + markerSize + paddingInnerW; - } + // Left-side variant + if (markerX < width() / 2) { + labelX = markerX - (markerSize / 2) - paddingW; + textX = labelX + paddingW + markerSize + paddingInnerW; + } - // Right-side variant - else { - labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW; - textX = labelX + paddingW; - } + // Right-side variant + else { + labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW; + textX = labelX + paddingW; + } - // Prevent overlap with scale bars and their labels - // Define a "safe zone" in the bottom-left where the scale bars and text are drawn - constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height - constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone - bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth); + // Prevent overlap with scale bars and their labels + // Define a "safe zone" in the bottom-left where the scale bars and text are drawn + constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height + constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone + bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth); - // If it overlaps, shift label upward slightly above the safe zone - if (overlapsScale) { - labelY = height() - safeZoneHeight - labelH - 2; - textY = labelY + (labelH / 2); - } + // If it overlaps, shift label upward slightly above the safe zone + if (overlapsScale) { + labelY = height() - safeZoneHeight - labelH - 2; + textY = labelY + (labelH / 2); + } - // Backing box - fillRect(labelX, labelY, labelW, labelH, WHITE); - drawRect(labelX, labelY, labelW, labelH, BLACK); + // Backing box + fillRect(labelX, labelY, labelW, labelH, WHITE); + drawRect(labelX, labelY, labelW, labelH, BLACK); - // Short name - printAt(textX, textY, shortName, LEFT, MIDDLE); + // Short name + printAt(textX, textY, shortName, LEFT, MIDDLE); - // If the label is for our own node, - // fade it by overdrawing partially with white - if (node == nodeDB->getMeshNode(nodeDB->getNodeNum())) - hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE); + // If the label is for our own node, + // fade it by overdrawing partially with white + if (node == nodeDB->getMeshNode(nodeDB->getNodeNum())) + hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE); - // Draw the marker emblem - // - after the fading, because hatching (own node) can align with cross and make it look weird - if (tooManyHops) - printAt(markerX, markerY, "!", CENTER, MIDDLE); - else - drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops + // Draw the marker emblem + // - after the fading, because hatching (own node) can align with cross and make it look weird + if (tooManyHops) + printAt(markerX, markerY, "!", CENTER, MIDDLE); + else + drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops } // Check if we actually have enough nodes which would be shown on the map // Need at least two, to draw a sensible map -bool InkHUD::MapApplet::enoughMarkers() -{ - size_t count = 0; - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); +bool InkHUD::MapApplet::enoughMarkers() { + size_t count = 0; + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Count nodes - if (nodeDB->hasValidPosition(node) && shouldDrawNode(node)) - count++; + // Count nodes + if (nodeDB->hasValidPosition(node) && shouldDrawNode(node)) + count++; - // We need to find two - if (count == 2) - return true; // Two nodes is enough for a sensible map - } + // We need to find two + if (count == 2) + return true; // Two nodes is enough for a sensible map + } - return false; // No nodes would be drawn (or just the one, uselessly at 0,0) + return false; // No nodes would be drawn (or just the one, uselessly at 0,0) } // Calculate how far north and east of map center each node is // Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode -void InkHUD::MapApplet::calculateAllMarkers() -{ - // Clear old markers - markers.clear(); +void InkHUD::MapApplet::calculateAllMarkers() { + // Clear old markers + markers.clear(); - // For each node in db - for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // For each node in db + for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Skip if no position - if (!nodeDB->hasValidPosition(node)) - continue; + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; - // Skip if derived applet doesn't want to show this node on the map - if (!shouldDrawNode(node)) - continue; + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; - // Skip if our own node - // - special handling in render() - if (node->num == nodeDB->getNodeNum()) - continue; + // Skip if our own node + // - special handling in render() + if (node->num == nodeDB->getNodeNum()) + continue; - // Calculate marker and store it - markers.push_back( - calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style - node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style - node->has_hops_away, // Is the hopsAway number valid - node->hops_away // Hops away - )); - } + // Calculate marker and store it + markers.push_back(calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + )); + } } // Determine the conversion factor between metres, and pixels on screen // May be overriden by derived applet, if custom scale required (fixed map size?) -void InkHUD::MapApplet::calculateMapScale() -{ - // Aspect ratio of map and screen - // - larger = wide, smaller = tall - // - used to set scale, so that widest map dimension fits in applet - float mapAspectRatio = (float)widthMeters / heightMeters; - float appletAspectRatio = (float)width() / height(); +void InkHUD::MapApplet::calculateMapScale() { + // Aspect ratio of map and screen + // - larger = wide, smaller = tall + // - used to set scale, so that widest map dimension fits in applet + float mapAspectRatio = (float)widthMeters / heightMeters; + float appletAspectRatio = (float)width() / height(); - // "Shrink to fit" - // Scale the map so that the largest dimension is fully displayed - // Because aspect ratio will be maintained, the other dimension will appear "padded" - if (mapAspectRatio > appletAspectRatio) - metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width. - else - metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height. + // "Shrink to fit" + // Scale the map so that the largest dimension is fully displayed + // Because aspect ratio will be maintained, the other dimension will appear "padded" + if (mapAspectRatio > appletAspectRatio) + metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width. + else + metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height. } // Draw an x, centered on a specific point // Most markers will draw with this method -void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) -{ - int16_t x0 = x - (size / 2); - int16_t y0 = y - (size / 2); - int16_t x1 = x0 + size - 1; - int16_t y1 = y0 + size - 1; - drawLine(x0, y0, x1, y1, BLACK); - drawLine(x0, y1, x1, y0, BLACK); +void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) { + int16_t x0 = x - (size / 2); + int16_t y0 = y - (size / 2); + int16_t x1 = x0 + size - 1; + int16_t y1 = y0 + size - 1; + drawLine(x0, y0, x1, y1, BLACK); + drawLine(x0, y1, x1, y0, BLACK); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h index f45a36071..a3f780a6d 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h @@ -21,43 +21,41 @@ The base applet doesn't handle any events; this is left to the derived applets. #include "MeshModule.h" #include "gps/GeoCoord.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class MapApplet : public Applet -{ - public: - void onRender() override; +class MapApplet : public Applet { +public: + void onRender() override; - protected: - virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes - virtual void getMapCenter(float *lat, float *lng); - virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters); +protected: + virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes + virtual void getMapCenter(float *lat, float *lng); + virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters); - bool enoughMarkers(); // Anything to draw? - void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker + bool enoughMarkers(); // Anything to draw? + void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker - private: - // Position and size of a marker to be drawn - struct Marker { - float eastMeters = 0; // Meters east of map center. Negative if west. - float northMeters = 0; // Meters north of map center. Negative if south. - bool hasHopsAway = false; - uint8_t hopsAway = 0; // Determines marker size - }; +private: + // Position and size of a marker to be drawn + struct Marker { + float eastMeters = 0; // Meters east of map center. Negative if west. + float northMeters = 0; // Meters north of map center. Negative if south. + bool hasHopsAway = false; + uint8_t hopsAway = 0; // Determines marker size + }; - Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway); - void calculateAllMarkers(); - void calculateMapScale(); // Conversion factor for meters to pixels - void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers + Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway); + void calculateAllMarkers(); + void calculateMapScale(); // Conversion factor for meters to pixels + void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers - float metersToPx = 0; // Conversion factor for meters to pixels - float latCenter = 0; // Map center: latitude - float lngCenter = 0; // Map center: longitude + float metersToPx = 0; // Conversion factor for meters to pixels + float latCenter = 0; // Map center: latitude + float lngCenter = 0; // Map center: longitude - std::list markers; - uint32_t widthMeters = 0; // Map width: meters - uint32_t heightMeters = 0; // Map height: meters + std::list markers; + uint32_t widthMeters = 0; // Map width: meters + uint32_t heightMeters = 0; // Map height: meters }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 5c9906fba..79487bbde 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -9,283 +9,276 @@ using namespace NicheGraphics; -InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) -{ - // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule - // For all other packets, we manually act as if isPromiscuous=false, in wantPacket - MeshModule::isPromiscuous = true; +InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) { + // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule + // For all other packets, we manually act as if isPromiscuous=false, in wantPacket + MeshModule::isPromiscuous = true; } // Do we want to process this packet with handleReceived()? -bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) -{ - // Only interested if: - return isActive() // Applet is active - && !isFromUs(p) // Packet is incoming (not outgoing) - && (isToUs(p) || isBroadcast(p->to) || // Either: intended for us, - p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo +bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) { + // Only interested if: + return isActive() // Applet is active + && !isFromUs(p) // Packet is incoming (not outgoing) + && (isToUs(p) || isBroadcast(p->to) || // Either: intended for us, + p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo - // To match the behavior seen in the client apps: - // - NodeInfoModule's ProtoBufModule base is "promiscuous" - // - All other activity is *not* promiscuous + // To match the behavior seen in the client apps: + // - NodeInfoModule's ProtoBufModule base is "promiscuous" + // - All other activity is *not* promiscuous - // To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here, - // to match the code in MeshModule::callModules + // To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here, + // to match the code in MeshModule::callModules } // MeshModule packets arrive here // Extract the info and pass it to the derived applet // Derived applet will store the CardInfo, and perform any required sorting of the CardInfo collection // Derived applet might also need to keep other tallies (active nodes count?) -ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) -{ - // Abort if applet fully deactivated - // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return - if (!isActive()) - return ProcessMessage::CONTINUE; +ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) { + // Abort if applet fully deactivated + // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early + // return + if (!isActive()) + return ProcessMessage::CONTINUE; - // Assemble info: from this event - CardInfo c; - c.nodeNum = mp.from; - c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); + // Assemble info: from this event + CardInfo c; + c.nodeNum = mp.from; + c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); - // Assemble info: from nodeDB (needed to detect changes) - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (node) { - if (node->has_hops_away) - c.hopsAway = node->hops_away; + // Assemble info: from nodeDB (needed to detect changes) + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (node) { + if (node->has_hops_away) + c.hopsAway = node->hops_away; - if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { - // Get lat and long as float - // Meshtastic stores these as integers internally - float ourLat = ourNode->position.latitude_i * 1e-7; - float ourLong = ourNode->position.longitude_i * 1e-7; - float theirLat = node->position.latitude_i * 1e-7; - float theirLong = node->position.longitude_i * 1e-7; + if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { + // Get lat and long as float + // Meshtastic stores these as integers internally + float ourLat = ourNode->position.latitude_i * 1e-7; + float ourLong = ourNode->position.longitude_i * 1e-7; + float theirLat = node->position.latitude_i * 1e-7; + float theirLong = node->position.longitude_i * 1e-7; - c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); - } + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); } + } - // Pass to the derived applet - // Derived applet is responsible for requesting update, if justified - // That request will eventually trigger our class' onRender method - handleParsed(c); + // Pass to the derived applet + // Derived applet is responsible for requesting update, if justified + // That request will eventually trigger our class' onRender method + handleParsed(c); - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } // Calculate maximum number of cards we may ever need to render, in our tallest layout config // Number might be slightly in excess of the true value: applet header text not accounted for -uint8_t InkHUD::NodeListApplet::maxCards() -{ - // Cache result. Shouldn't change during execution - static uint8_t cards = 0; +uint8_t InkHUD::NodeListApplet::maxCards() { + // Cache result. Shouldn't change during execution + static uint8_t cards = 0; - if (!cards) { - const uint16_t height = Tile::maxDisplayDimension(); + if (!cards) { + const uint16_t height = Tile::maxDisplayDimension(); - // Use a loop instead of arithmetic, because it's easier for my brain to follow - // Add cards one by one, until the latest card extends below screen + // Use a loop instead of arithmetic, because it's easier for my brain to follow + // Add cards one by one, until the latest card extends below screen - uint16_t y = cardH; // First card: no margin above - cards = 1; + uint16_t y = cardH; // First card: no margin above + cards = 1; - while (y < height) { - y += cardMarginH; - y += cardH; - cards++; - } + while (y < height) { + y += cardMarginH; + y += cardH; + cards++; } + } - return cards; + return cards; } // Draw, using info which derived applet placed into NodeListApplet::cards for us -void InkHUD::NodeListApplet::onRender() -{ +void InkHUD::NodeListApplet::onRender() { - // ================================ - // Draw the standard applet header - // ================================ + // ================================ + // Draw the standard applet header + // ================================ - drawHeader(getHeaderText()); // Ask derived applet for the title + drawHeader(getHeaderText()); // Ask derived applet for the title - // Dimensions of the header - int16_t headerDivY = getHeaderHeight() - 1; - constexpr uint16_t padDivH = 2; + // Dimensions of the header + int16_t headerDivY = getHeaderHeight() - 1; + constexpr uint16_t padDivH = 2; - // ======================== - // Draw the main node list - // ======================== + // ======================== + // Draw the main node list + // ======================== - // Imaginary vertical line dividing left-side and right-side info - // Long-name will crop here - const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); + // Imaginary vertical line dividing left-side and right-side info + // Long-name will crop here + const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); - // Y value (top) of the current card. Increases as we draw. - uint16_t cardTopY = headerDivY + padDivH; + // Y value (top) of the current card. Increases as we draw. + uint16_t cardTopY = headerDivY + padDivH; - // Clean up deleted nodes before drawing - cards.erase( - std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }), - cards.end()); + // Clean up deleted nodes before drawing + cards.erase(std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }), cards.end()); - // -- Each node in list -- - for (auto card = cards.begin(); card != cards.end(); ++card) { + // -- Each node in list -- + for (auto card = cards.begin(); card != cards.end(); ++card) { - // Gather info - // ======================================== - NodeNum &nodeNum = card->nodeNum; - SignalStrength &signal = card->signal; - std::string longName; // handled below - std::string shortName; // handled below - std::string distance; // handled below; - uint8_t &hopsAway = card->hopsAway; + // Gather info + // ======================================== + NodeNum &nodeNum = card->nodeNum; + SignalStrength &signal = card->signal; + std::string longName; // handled below + std::string shortName; // handled below + std::string distance; // handled below; + uint8_t &hopsAway = card->hopsAway; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); - // Skip deleted nodes - if (!node) { - continue; - } - - // -- Shortname -- - // Parse special chars in the short name - // Use "?" if unknown - if (node) - shortName = parseShortName(node); - else - shortName = "?"; - - // -- Longname -- - // Parse special chars in long name - // Use node id if unknown - if (node && node->has_user) - longName = parse(node->user.long_name); // Found in nodeDB - else { - // Not found in nodeDB, show a hex nodeid instead - longName = hexifyNodeNum(nodeNum); - } - - // -- Distance -- - if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN) - distance = localizeDistance(card->distanceMeters); - - // Draw the info - // ==================================== - - // Define two lines of text for the card - // We will center our text on these lines - uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2); - uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2); - - // Print the short name - setFont(fontMedium); - printAt(0, lineAY, shortName, LEFT, MIDDLE); - - // Print the distance - setFont(fontSmall); - printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); - - // If we have a direct connection to the node, draw the signal indicator - if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { - uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label - uint16_t signalH = fontMedium.lineHeight() * 0.75; - int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); - int16_t signalX = width() - signalW; - drawSignalIndicator(signalX, signalY, signalW, signalH, signal); - } - // Otherwise, print "hops away" info, if available - else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) { - std::string hopString = to_string(node->hops_away); - hopString += " Hop"; - if (node->hops_away != 1) - hopString += "s"; // Append s for "Hops", rather than "Hop" - - printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); - } - - // Print the long name, cropping to prevent overflow onto the right-side info - setCrop(0, 0, dividerX - 1, height()); - printAt(0, lineBY, longName, LEFT, MIDDLE); - - // GFX effect: "hatch" the right edge of longName area - // If a longName has been cropped, it will appear to fade out, - // creating a soft barrier with the right-side info - const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); - const int16_t hatchWidth = fontSmall.lineHeight(); - hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); - - // Prepare to draw the next card - resetCrop(); - cardTopY += cardH; - - // Once we've run out of screen, stop drawing cards - // Depending on tiles / rotation, this may be before we hit maxCards - if (cardTopY > height()) - break; + // Skip deleted nodes + if (!node) { + continue; } + + // -- Shortname -- + // Parse special chars in the short name + // Use "?" if unknown + if (node) + shortName = parseShortName(node); + else + shortName = "?"; + + // -- Longname -- + // Parse special chars in long name + // Use node id if unknown + if (node && node->has_user) + longName = parse(node->user.long_name); // Found in nodeDB + else { + // Not found in nodeDB, show a hex nodeid instead + longName = hexifyNodeNum(nodeNum); + } + + // -- Distance -- + if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN) + distance = localizeDistance(card->distanceMeters); + + // Draw the info + // ==================================== + + // Define two lines of text for the card + // We will center our text on these lines + uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2); + uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2); + + // Print the short name + setFont(fontMedium); + printAt(0, lineAY, shortName, LEFT, MIDDLE); + + // Print the distance + setFont(fontSmall); + printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + + // If we have a direct connection to the node, draw the signal indicator + if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { + uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label + uint16_t signalH = fontMedium.lineHeight() * 0.75; + int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); + int16_t signalX = width() - signalW; + drawSignalIndicator(signalX, signalY, signalW, signalH, signal); + } + // Otherwise, print "hops away" info, if available + else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) { + std::string hopString = to_string(node->hops_away); + hopString += " Hop"; + if (node->hops_away != 1) + hopString += "s"; // Append s for "Hops", rather than "Hop" + + printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); + } + + // Print the long name, cropping to prevent overflow onto the right-side info + setCrop(0, 0, dividerX - 1, height()); + printAt(0, lineBY, longName, LEFT, MIDDLE); + + // GFX effect: "hatch" the right edge of longName area + // If a longName has been cropped, it will appear to fade out, + // creating a soft barrier with the right-side info + const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); + const int16_t hatchWidth = fontSmall.lineHeight(); + hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); + + // Prepare to draw the next card + resetCrop(); + cardTopY += cardH; + + // Once we've run out of screen, stop drawing cards + // Depending on tiles / rotation, this may be before we hit maxCards + if (cardTopY > height()) + break; + } } // Draw element: a "mobile phone" style signal indicator // We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc // This prevents issues with premature rounding when rendering tiny elements -void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) -{ +void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) { - /* - +-------------------------------------------+ - | | - | | - | barHeightRelative=1.0 - | +--+ ^ | - | gutterW +--+ | | | | - | <--> +--+ | | | | | | - | +--+ | | | | | | | | - | | | | | | | | | | | - | <-> +--+ +--+ +--+ +--+ v | - | paddingW ^ | - | paddingH | | - | v | - +-------------------------------------------+ - */ + /* + +-------------------------------------------+ + | | + | | + | barHeightRelative=1.0 + | +--+ ^ | + | gutterW +--+ | | | | + | <--> +--+ | | | | | | + | +--+ | | | | | | | | + | | | | | | | | | | | + | <-> +--+ +--+ +--+ +--+ v | + | paddingW ^ | + | paddingH | | + | v | + +-------------------------------------------+ + */ - constexpr float paddingW = 0.1; // Either side - constexpr float paddingH = 0.1; // Above and below - constexpr float gutterW = 0.1; // Between bars + constexpr float paddingW = 0.1; // Either side + constexpr float paddingH = 0.1; // Above and below + constexpr float gutterW = 0.1; // Between bars - constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest - constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count. + constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest + constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count. - // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions - float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount; - float barHMax = 1.0 - (paddingH + paddingH); + // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions + float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount; + float barHMax = 1.0 - (paddingH + paddingH); - // Draw signal bar rectangles, then placeholder lines once strength reached - for (uint8_t i = 0; i < barCount; i++) { - // Coords for this specific bar - float barH = barHMax * barHRel[i]; - float barX = paddingW + (i * (gutterW + barW)); - float barY = paddingH + (barHMax - barH); + // Draw signal bar rectangles, then placeholder lines once strength reached + for (uint8_t i = 0; i < barCount; i++) { + // Coords for this specific bar + float barH = barHMax * barHRel[i]; + float barX = paddingW + (i * (gutterW + barW)); + float barY = paddingH + (barHMax - barH); - // Rasterize to px coords at the last moment - int16_t rX = (x + (w * barX)) + 0.5; - int16_t rY = (y + (h * barY)) + 0.5; - uint16_t rW = (w * barW) + 0.5; - uint16_t rH = (h * barH) + 0.5; + // Rasterize to px coords at the last moment + int16_t rX = (x + (w * barX)) + 0.5; + int16_t rY = (y + (h * barY)) + 0.5; + uint16_t rW = (w * barW) + 0.5; + uint16_t rH = (h * barH) + 0.5; - // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines - if (i <= strength) - drawRect(rX, rY, rW, rH, BLACK); - else { - // Just draw a placeholder line - float lineY = barY + barH; - uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize - drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK); - } + // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines + if (i <= strength) + drawRect(rX, rY, rW, rH, BLACK); + else { + // Just draw a placeholder line + float lineY = barY + barH; + uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize + drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK); } + } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index c2340027b..8fadd1339 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -25,48 +25,46 @@ Used by the "Recents" and "Heard" applets. Possibly more in future? #include "main.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class NodeListApplet : public Applet, public MeshModule -{ - protected: - // Info needed to draw a node card to the list - // - generated each time we hear a node - struct CardInfo { - static constexpr uint8_t HOPS_UNKNOWN = -1; - static constexpr uint32_t DISTANCE_UNKNOWN = -1; +class NodeListApplet : public Applet, public MeshModule { +protected: + // Info needed to draw a node card to the list + // - generated each time we hear a node + struct CardInfo { + static constexpr uint8_t HOPS_UNKNOWN = -1; + static constexpr uint32_t DISTANCE_UNKNOWN = -1; - NodeNum nodeNum = 0; - SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN; - uint32_t distanceMeters = DISTANCE_UNKNOWN; - uint8_t hopsAway = HOPS_UNKNOWN; - }; + NodeNum nodeNum = 0; + SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN; + uint32_t distanceMeters = DISTANCE_UNKNOWN; + uint8_t hopsAway = HOPS_UNKNOWN; + }; - public: - NodeListApplet(const char *name); +public: + NodeListApplet(const char *name); - void onRender() override; + void onRender() override; - bool wantPacket(const meshtastic_MeshPacket *p) override; - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + bool wantPacket(const meshtastic_MeshPacket *p) override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - protected: - virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node - virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be +protected: + virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node + virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be - uint8_t maxCards(); // Max number of cards which could ever fit on screen + uint8_t maxCards(); // Max number of cards which could ever fit on screen - std::deque cards; // Cards to be rendered. Derived applet fills this. + std::deque cards; // Cards to be rendered. Derived applet fills this. - private: - void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, - SignalStrength signal); // Draw a "mobile phone" style signal indicator +private: + void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, + SignalStrength signal); // Draw a "mobile phone" style signal indicator - // Card Dimensions - // - for rendering and for maxCards calc - uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards - uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card + // Card Dimensions + // - for rendering and for maxCards calc + uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards + uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp index c52719e55..6d749d804 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp @@ -6,15 +6,14 @@ using namespace NicheGraphics; // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. -void InkHUD::BasicExampleApplet::onRender() -{ - printAt(0, 0, "Hello, World!"); +void InkHUD::BasicExampleApplet::onRender() { + printAt(0, 0, "Hello, World!"); - // If text might contain "special characters", is needs parsing first - // This applies to data such as text-messages and and node names + // If text might contain "special characters", is needs parsing first + // This applies to data such as text-messages and and node names - // std::string greeting = parse("Grüezi mitenand!"); - // printAt(0, 0, greeting); + // std::string greeting = parse("Grüezi mitenand!"); + // printAt(0, 0, greeting); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h index aed63cdc8..c3d4d7b83 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h @@ -19,16 +19,14 @@ In variants//nicheGraphics.h: #include "graphics/niche/InkHUD/Applet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class BasicExampleApplet : public Applet -{ - public: - // You must have an onRender() method - // All drawing happens here +class BasicExampleApplet : public Applet { +public: + // You must have an onRender() method + // All drawing happens here - void onRender() override; + void onRender() override; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp index 6b02f4c92..a949ff200 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp @@ -5,49 +5,47 @@ using namespace NicheGraphics; // We configured the Module API to call this method when we receive a new text message -ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) -{ +ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) { - // Abort if applet fully deactivated - // Don't waste time: we wouldn't be rendered anyway - if (!isActive()) - return ProcessMessage::CONTINUE; - - // Check that this is an incoming message - // Outgoing messages (sent by us) will also call handleReceived - - if (!isFromUs(&mp)) { - // Store the sender's nodenum - // We need to keep this information, so we can re-use it anytime render() is called - haveMessage = true; - fromWho = mp.from; - - // Tell InkHUD that we have something new to show on the screen - requestUpdate(); - } - - // Tell Module API to continue informing other firmware components about this message - // We're not the only component which is interested in new text messages + // Abort if applet fully deactivated + // Don't waste time: we wouldn't be rendered anyway + if (!isActive()) return ProcessMessage::CONTINUE; + + // Check that this is an incoming message + // Outgoing messages (sent by us) will also call handleReceived + + if (!isFromUs(&mp)) { + // Store the sender's nodenum + // We need to keep this information, so we can re-use it anytime render() is called + haveMessage = true; + fromWho = mp.from; + + // Tell InkHUD that we have something new to show on the screen + requestUpdate(); + } + + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages + return ProcessMessage::CONTINUE; } // All drawing happens here // We can trigger a render by calling requestUpdate() // Render might be called by some external source // We should always be ready to draw -void InkHUD::NewMsgExampleApplet::onRender() -{ - printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) +void InkHUD::NewMsgExampleApplet::onRender() { + printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) - int16_t centerX = X(0.5); // Same as width() / 2 - int16_t centerY = Y(0.5); // Same as height() / 2 + int16_t centerX = X(0.5); // Same as width() / 2 + int16_t centerY = Y(0.5); // Same as height() / 2 - if (haveMessage) { - printAt(centerX, centerY, "New Message", CENTER, BOTTOM); - printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP); - } else { - printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY) - } + if (haveMessage) { + printAt(centerX, centerY, "New Message", CENTER, BOTTOM); + printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP); + } else { + printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY) + } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h index 22670a0f0..1a0aab23f 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -24,36 +24,34 @@ In variants//nicheGraphics.h: #include "mesh/SinglePortModule.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class NewMsgExampleApplet : public Applet, public SinglePortModule -{ - public: - // The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages. - NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} +class NewMsgExampleApplet : public Applet, public SinglePortModule { +public: + // The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages. + NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} - // All drawing happens here - void onRender() override; + // All drawing happens here + void onRender() override; - // Your applet might also want to use some of these - // Useful for setting up or tidying up + // Your applet might also want to use some of these + // Useful for setting up or tidying up - /* - void onActivate(); // When started - void onDeactivate(); // When stopped - void onForeground(); // When shown by short-press - void onBackground(); // When hidden by short-press - */ + /* + void onActivate(); // When started + void onDeactivate(); // When stopped + void onForeground(); // When shown by short-press + void onBackground(); // When hidden by short-press + */ - private: - // Called when we receive new text messages - // Part of the MeshModule API - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; +private: + // Called when we receive new text messages + // Part of the MeshModule API + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - // Store info from handleReceived - bool haveMessage = false; - NodeNum fromWho = 0; + // Store info from handleReceived + bool haveMessage = false; + NodeNum fromWho = 0; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp index 67ef87f41..62517d2d0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -4,79 +4,76 @@ using namespace NicheGraphics; -InkHUD::AlignStickApplet::AlignStickApplet() -{ - if (!settings->joystick.aligned) - bringToForeground(); +InkHUD::AlignStickApplet::AlignStickApplet() { + if (!settings->joystick.aligned) + bringToForeground(); } -void InkHUD::AlignStickApplet::onRender() -{ - setFont(fontMedium); - printAt(0, 0, "Align Joystick:"); - setFont(fontSmall); - std::string instructions = "Move joystick in the direction indicated"; - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); +void InkHUD::AlignStickApplet::onRender() { + setFont(fontMedium); + printAt(0, 0, "Align Joystick:"); + setFont(fontSmall); + std::string instructions = "Move joystick in the direction indicated"; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); - // Size of the region in which the joystick graphic should fit - uint16_t joyXLimit = X(0.8); - uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; - if (getTextWidth(instructions) > width()) - contentH += fontSmall.lineHeight(); - uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; - uint16_t joyYLimit = freeY * 0.8; + // Size of the region in which the joystick graphic should fit + uint16_t joyXLimit = X(0.8); + uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; + if (getTextWidth(instructions) > width()) + contentH += fontSmall.lineHeight(); + uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; + uint16_t joyYLimit = freeY * 0.8; - // Use the shorter of the two - uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; + // Use the shorter of the two + uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; - // Center the joystick graphic - uint16_t centerX = X(0.5); - uint16_t centerY = contentH + freeY * 0.5; + // Center the joystick graphic + uint16_t centerX = X(0.5); + uint16_t centerY = contentH + freeY * 0.5; - // Draw joystick graphic - drawStick(centerX, centerY, joyWidth); + // Draw joystick graphic + drawStick(centerX, centerY, joyWidth); - setFont(fontSmall); - printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); + setFont(fontSmall); + printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); } // Draw a scalable joystick graphic -void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) -{ - if (width < 9) // too small to draw - return; +void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) { + if (width < 9) // too small to draw + return; - else if (width < 40) { // only draw up arrow - uint16_t chamfer = width < 20 ? 1 : 2; + else if (width < 40) { // only draw up arrow + uint16_t chamfer = width < 20 ? 1 : 2; - // Draw filled up arrow - drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); + // Draw filled up arrow + drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); - } else { // large enough to draw the full thing - uint16_t chamfer = width < 80 ? 1 : 2; - uint16_t stroke = 3; // pixels - uint16_t arrowW = width * 0.22; - uint16_t hollowW = arrowW - stroke * 2; + } else { // large enough to draw the full thing + uint16_t chamfer = width < 80 ? 1 : 2; + uint16_t stroke = 3; // pixels + uint16_t arrowW = width * 0.22; + uint16_t hollowW = arrowW - stroke * 2; - // Draw center circle - fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); - fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); + // Draw center circle + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); - // Draw filled up arrow - drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); + // Draw filled up arrow + drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); - // Draw down arrow - drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); - drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); + // Draw down arrow + drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); + drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); - // Draw left arrow - drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); - drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); + // Draw left arrow + drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); + drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); - // Draw right arrow - drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); - drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); - } + // Draw right arrow + drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); + drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); + } } // Draw a scalable joystick direction arrow @@ -90,116 +87,98 @@ void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uin v |_________| */ -void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, - uint16_t chamfer, Color color) -{ - uint16_t chamferW = chamfer * 2 + 1; - uint16_t triangleW = size - chamferW; +void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color) { + uint16_t chamferW = chamfer * 2 + 1; + uint16_t triangleW = size - chamferW; - // Draw arrow - switch (direction) { - case Direction::UP: - fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); - fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); - fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, - pointY + triangleW, color); - fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, - pointY + triangleW, color); - break; - case Direction::DOWN: - fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); - fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); - fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, - pointY - triangleW, color); - fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, - pointY - triangleW, color); - break; - case Direction::LEFT: - fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); - fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); - fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, - pointY - chamfer, color); - fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, - pointY + chamfer, color); - break; - case Direction::RIGHT: - fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); - fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); - fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, - pointY - chamfer, color); - fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, - pointY + chamfer, color); - break; - } + // Draw arrow + switch (direction) { + case Direction::UP: + fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, pointY + triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, pointY + triangleW, color); + break; + case Direction::DOWN: + fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, pointY - triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, pointY - triangleW, color); + break; + case Direction::LEFT: + fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, pointY + chamfer, color); + break; + case Direction::RIGHT: + fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, pointY + chamfer, color); + break; + } } -void InkHUD::AlignStickApplet::onForeground() -{ - // Prevent most other applets from requesting update, and skip their rendering entirely - // Another system applet with a higher precedence can potentially ignore this - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; +void InkHUD::AlignStickApplet::onForeground() { + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; - handleInput = true; // Intercept the button input for our applet + handleInput = true; // Intercept the button input for our applet } -void InkHUD::AlignStickApplet::onBackground() -{ - // Allow normal update behavior to resume - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; +void InkHUD::AlignStickApplet::onBackground() { + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onButtonLongPress() -{ - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::AlignStickApplet::onButtonLongPress() { + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onExitLong() -{ - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::AlignStickApplet::onExitLong() { + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavUp() -{ - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavUp() { + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavDown() -{ - inkhud->rotateJoystick(2); // 180 deg - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavDown() { + inkhud->rotateJoystick(2); // 180 deg + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavLeft() -{ - inkhud->rotateJoystick(3); // 270 deg - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavLeft() { + inkhud->rotateJoystick(3); // 270 deg + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavRight() -{ - inkhud->rotateJoystick(1); // 90 deg - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavRight() { + inkhud->rotateJoystick(1); // 90 deg + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h index 8dba33165..68893657a 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -15,34 +15,32 @@ and not aligned to the screen #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class AlignStickApplet : public SystemApplet -{ - public: - AlignStickApplet(); +class AlignStickApplet : public SystemApplet { +public: + AlignStickApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onButtonLongPress() override; - void onExitLong() override; - void onNavUp() override; - void onNavDown() override; - void onNavLeft() override; - void onNavRight() override; + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonLongPress() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; - protected: - enum Direction { - UP, - DOWN, - LEFT, - RIGHT, - }; +protected: + enum Direction { + UP, + DOWN, + LEFT, + RIGHT, + }; - void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); - void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); + void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index 4f99d99ee..64c878565 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -4,98 +4,95 @@ using namespace NicheGraphics; -InkHUD::BatteryIconApplet::BatteryIconApplet() -{ - // Show at boot, if user has previously enabled the feature - if (settings->optionalFeatures.batteryIcon) - bringToForeground(); +InkHUD::BatteryIconApplet::BatteryIconApplet() { + // Show at boot, if user has previously enabled the feature + if (settings->optionalFeatures.batteryIcon) + bringToForeground(); - // Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available - // This happens whether or not the battery icon feature is enabled - powerStatusObserver.observe(&powerStatus->onNewStatus); + // Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available + // This happens whether or not the battery icon feature is enabled + powerStatusObserver.observe(&powerStatus->onNewStatus); } // We handle power status' even when the feature is disabled, // so that we have up to date data ready if the feature is enabled later. // Otherwise could be 30s before new status update, with weird battery value displayed -int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) -{ - // System applets are always active - assert(isActive()); +int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) { + // System applets are always active + assert(isActive()); - // This method should only receive power statuses - // If we get a different type of status, something has gone weird elsewhere - assert(status->getStatusType() == STATUS_TYPE_POWER); + // This method should only receive power statuses + // If we get a different type of status, something has gone weird elsewhere + assert(status->getStatusType() == STATUS_TYPE_POWER); - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; + meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; - // Get the new state of charge %, and round to the nearest 10% - uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; + // Get the new state of charge %, and round to the nearest 10% + uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; - // If rounded value has changed, trigger a display update - // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() - // Don't trigger an update if the feature is disabled - if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon) - requestUpdate(); + // If rounded value has changed, trigger a display update + // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() + // Don't trigger an update if the feature is disabled + if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon) + requestUpdate(); - // Store the new value - this->socRounded = newSocRounded; + // Store the new value + this->socRounded = newSocRounded; - return 0; // Tell Observable to continue informing other observers + return 0; // Tell Observable to continue informing other observers } -void InkHUD::BatteryIconApplet::onRender() -{ - // Fill entire tile - // - size of icon controlled by size of tile - int16_t l = 0; - int16_t t = 0; - uint16_t w = width(); - int16_t h = height(); +void InkHUD::BatteryIconApplet::onRender() { + // Fill entire tile + // - size of icon controlled by size of tile + int16_t l = 0; + int16_t t = 0; + uint16_t w = width(); + int16_t h = height(); - // Clear the region beneath the tile - // Most applets are drawing onto an empty frame buffer and don't need to do this - // We do need to do this with the battery though, as it is an "overlay" - fillRect(l, t, w, h, WHITE); + // Clear the region beneath the tile + // Most applets are drawing onto an empty frame buffer and don't need to do this + // We do need to do this with the battery though, as it is an "overlay" + fillRect(l, t, w, h, WHITE); - // Vertical centerline - const int16_t m = t + (h / 2); + // Vertical centerline + const int16_t m = t + (h / 2); - // ===================== - // Draw battery outline - // ===================== + // ===================== + // Draw battery outline + // ===================== - // Positive terminal "bump" - const int16_t &bumpL = l; - const uint16_t bumpH = h / 2; - const int16_t bumpT = m - (bumpH / 2); - constexpr uint16_t bumpW = 2; - fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); + // Positive terminal "bump" + const int16_t &bumpL = l; + const uint16_t bumpH = h / 2; + const int16_t bumpT = m - (bumpH / 2); + constexpr uint16_t bumpW = 2; + fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); - // Main body of battery - const int16_t bodyL = bumpL + bumpW; - const int16_t &bodyT = t; - const int16_t &bodyH = h; - const int16_t bodyW = w - bumpW; - drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); + // Main body of battery + const int16_t bodyL = bumpL + bumpW; + const int16_t &bodyT = t; + const int16_t &bodyH = h; + const int16_t bodyW = w - bumpW; + drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); - // Erase join between bump and body - drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE); + // Erase join between bump and body + drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE); - // =================== - // Draw battery level - // =================== + // =================== + // Draw battery level + // =================== - constexpr int16_t slicePad = 2; - const int16_t sliceL = bodyL + slicePad; - const int16_t sliceT = bodyT + slicePad; - const uint16_t sliceH = bodyH - (slicePad * 2); - uint16_t sliceW = bodyW - (slicePad * 2); + constexpr int16_t slicePad = 2; + const int16_t sliceL = bodyL + slicePad; + const int16_t sliceT = bodyT + slicePad; + const uint16_t sliceH = bodyH - (slicePad * 2); + uint16_t sliceW = bodyW - (slicePad * 2); - sliceW = (sliceW * socRounded) / 100; // Apply percentage + sliceW = (sliceW * socRounded) / 100; // Apply percentage - hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); - drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); + hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); + drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h index e5b4172be..60fe29e8b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h @@ -15,23 +15,21 @@ It should be optional, enabled by the on-screen menu #include "PowerStatus.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class BatteryIconApplet : public SystemApplet -{ - public: - BatteryIconApplet(); +class BatteryIconApplet : public SystemApplet { +public: + BatteryIconApplet(); - void onRender() override; - int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available + void onRender() override; + int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available - private: - // Get informed when new information about the battery is available (via onPowerStatusUpdate method) - CallbackObserver powerStatusObserver = - CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); +private: + // Get informed when new information about the battery is available (via onPowerStatusUpdate method) + CallbackObserver powerStatusObserver = + CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); - uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10% + uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10% }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index ecaa7cea3..32e360af5 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -6,172 +6,166 @@ using namespace NicheGraphics; -InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") -{ - OSThread::setIntervalFromNow(8 * 1000UL); - OSThread::enabled = true; +InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") { + OSThread::setIntervalFromNow(8 * 1000UL); + OSThread::enabled = true; - // During onboarding, show the default short name as well as the version string - // This behavior assists manufacturers during mass production, and should not be modified without good reason - if (!settings->tips.safeShutdownSeen) { - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - fontTitle = fontMedium; - textLeft = xstr(APP_VERSION_SHORT); - textRight = parseShortName(ourNode); - textTitle = "Meshtastic"; - } else { - fontTitle = fontSmall; - textLeft = ""; - textRight = ""; - textTitle = xstr(APP_VERSION_SHORT); - } + // During onboarding, show the default short name as well as the version string + // This behavior assists manufacturers during mass production, and should not be modified without good reason + if (!settings->tips.safeShutdownSeen) { + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + fontTitle = fontMedium; + textLeft = xstr(APP_VERSION_SHORT); + textRight = parseShortName(ourNode); + textTitle = "Meshtastic"; + } else { + fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = xstr(APP_VERSION_SHORT); + } - bringToForeground(); - // This is then drawn with a FULL refresh by Renderer::begin + bringToForeground(); + // This is then drawn with a FULL refresh by Renderer::begin } -void InkHUD::LogoApplet::onRender() -{ - // Size of the region which the logo should "scale to fit" - uint16_t logoWLimit = X(0.8); - uint16_t logoHLimit = Y(0.5); +void InkHUD::LogoApplet::onRender() { + // Size of the region which the logo should "scale to fit" + uint16_t logoWLimit = X(0.8); + uint16_t logoHLimit = Y(0.5); - // Get the max width and height we can manage within the region, while still maintaining aspect ratio - uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); - uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); + // Get the max width and height we can manage within the region, while still maintaining aspect ratio + uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); + uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); - // Where to place the center of the logo - int16_t logoCX = X(0.5); - int16_t logoCY = Y(0.5 - 0.05); + // Where to place the center of the logo + int16_t logoCX = X(0.5); + int16_t logoCY = Y(0.5 - 0.05); - // Invert colors if black-on-white - // Used during shutdown, to resport display health - // Todo: handle this in InkHUD::Renderer instead - if (inverted) { - fillScreen(BLACK); - setTextColor(WHITE); - } + // Invert colors if black-on-white + // Used during shutdown, to resport display health + // Todo: handle this in InkHUD::Renderer instead + if (inverted) { + fillScreen(BLACK); + setTextColor(WHITE); + } #ifdef USERPREFS_OEM_IMAGE_DATA // Custom boot screen, if defined in userPrefs.jsonc - // Only show the custom screen at startup - // This allows us to draw the usual Meshtastic logo at shutdown - // The effect is similar to the two-stage userPrefs boot screen used by BaseUI - if (millis() < 10 * 1000UL) { + // Only show the custom screen at startup + // This allows us to draw the usual Meshtastic logo at shutdown + // The effect is similar to the two-stage userPrefs boot screen used by BaseUI + if (millis() < 10 * 1000UL) { - // Draw the custom logo - const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; - drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left - logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top - logo, // XBM data - USERPREFS_OEM_IMAGE_WIDTH, // Width - USERPREFS_OEM_IMAGE_HEIGHT, // Height - inverted ? WHITE : BLACK // Color - ); + // Draw the custom logo + const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; + drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left + logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top + logo, // XBM data + USERPREFS_OEM_IMAGE_WIDTH, // Width + USERPREFS_OEM_IMAGE_HEIGHT, // Height + inverted ? WHITE : BLACK // Color + ); - // Select the largest font which will still comfortably fit the custom text - setFont(fontLarge); - if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) - setFont(fontMedium); - if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) - setFont(fontSmall); + // Select the largest font which will still comfortably fit the custom text + setFont(fontLarge); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontMedium); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontSmall); - // Draw custom text below logo - int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo - printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); + // Draw custom text below logo + int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo + printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); - // Don't draw the normal boot screen, we've already drawn our custom version - return; - } + // Don't draw the normal boot screen, we've already drawn our custom version + return; + } #endif - drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); + drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); - if (!textLeft.empty()) { - setFont(fontSmall); - printAt(0, 0, textLeft, LEFT, TOP); - } + if (!textLeft.empty()) { + setFont(fontSmall); + printAt(0, 0, textLeft, LEFT, TOP); + } - if (!textRight.empty()) { - setFont(fontSmall); - printAt(X(1), 0, textRight, RIGHT, TOP); - } + if (!textRight.empty()) { + setFont(fontSmall); + printAt(X(1), 0, textRight, RIGHT, TOP); + } - if (!textTitle.empty()) { - int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo - setFont(fontTitle); - printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP); - } + if (!textTitle.empty()) { + int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo + setFont(fontTitle); + printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP); + } } -void InkHUD::LogoApplet::onForeground() -{ - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; - SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it. +void InkHUD::LogoApplet::onForeground() { + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; + SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it. } -void InkHUD::LogoApplet::onBackground() -{ - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; +void InkHUD::LogoApplet::onBackground() { + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // Begin displaying the screen which is shown at shutdown -void InkHUD::LogoApplet::onShutdown() -{ - bringToForeground(); +void InkHUD::LogoApplet::onShutdown() { + bringToForeground(); - textLeft = ""; - textRight = ""; - textTitle = "Shutting Down..."; - fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = "Shutting Down..."; + fontTitle = fontSmall; - // Draw a shutting down screen, twice. - // Once white on black, once black on white. - // Intention is to restore display health. + // Draw a shutting down screen, twice. + // Once white on black, once black on white. + // Intention is to restore display health. - inverted = true; - inkhud->forceUpdate(Drivers::EInk::FULL, false); - delay(1000); // Cooldown. Back to back updates aren't great for health. - inverted = false; - inkhud->forceUpdate(Drivers::EInk::FULL, false); - delay(1000); // Cooldown + inverted = true; + inkhud->forceUpdate(Drivers::EInk::FULL, false); + delay(1000); // Cooldown. Back to back updates aren't great for health. + inverted = false; + inkhud->forceUpdate(Drivers::EInk::FULL, false); + delay(1000); // Cooldown - // Prepare for the powered-off screen now - // We can change these values because the initial "shutting down" screen has already rendered at this point - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - textLeft = ""; - textRight = ""; - textTitle = parseShortName(ourNode); - fontTitle = fontMedium; + // Prepare for the powered-off screen now + // We can change these values because the initial "shutting down" screen has already rendered at this point + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + textLeft = ""; + textRight = ""; + textTitle = parseShortName(ourNode); + fontTitle = fontMedium; - // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete + // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is + // complete } -void InkHUD::LogoApplet::onReboot() -{ - bringToForeground(); +void InkHUD::LogoApplet::onReboot() { + bringToForeground(); - textLeft = ""; - textRight = ""; - textTitle = "Rebooting..."; - fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = "Rebooting..."; + fontTitle = fontSmall; - inkhud->forceUpdate(Drivers::EInk::FULL, false); - // Perform the update right now, waiting here until complete + inkhud->forceUpdate(Drivers::EInk::FULL, false); + // Perform the update right now, waiting here until complete } -int32_t InkHUD::LogoApplet::runOnce() -{ - sendToBackground(); - return OSThread::disable(); +int32_t InkHUD::LogoApplet::runOnce() { + sendToBackground(); + return OSThread::disable(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h index 3f604baed..7b0b0b377 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -14,27 +14,25 @@ #include "concurrency/OSThread.h" #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class LogoApplet : public SystemApplet, public concurrency::OSThread -{ - public: - LogoApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onShutdown() override; - void onReboot() override; +class LogoApplet : public SystemApplet, public concurrency::OSThread { +public: + LogoApplet(); + void onRender() override; + void onForeground() override; + void onBackground() override; + void onShutdown() override; + void onReboot() override; - protected: - int32_t runOnce() override; +protected: + int32_t runOnce() override; - std::string textLeft; - std::string textRight; - std::string textTitle; - AppletFont fontTitle; - bool inverted = false; // Invert colors. Used during shutdown, to restore display health. + std::string textLeft; + std::string textRight; + std::string textTitle; + AppletFont fontTitle; + bool inverted = false; // Invert colors. Used during shutdown, to restore display health. }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index debe2b719..7ef655d9d 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -13,29 +13,28 @@ Behaviors assigned in MenuApplet::execute #include "configuration.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { enum MenuAction { - NO_ACTION, - SEND_PING, - STORE_CANNEDMESSAGE_SELECTION, - SEND_CANNEDMESSAGE, - SHUTDOWN, - NEXT_TILE, - TOGGLE_BACKLIGHT, - TOGGLE_GPS, - ENABLE_BLUETOOTH, - TOGGLE_APPLET, - TOGGLE_AUTOSHOW_APPLET, - SET_RECENTS, - ROTATE, - ALIGN_JOYSTICK, - LAYOUT, - TOGGLE_BATTERY_ICON, - TOGGLE_NOTIFICATIONS, - TOGGLE_INVERT_COLOR, - TOGGLE_12H_CLOCK, + NO_ACTION, + SEND_PING, + STORE_CANNEDMESSAGE_SELECTION, + SEND_CANNEDMESSAGE, + SHUTDOWN, + NEXT_TILE, + TOGGLE_BACKLIGHT, + TOGGLE_GPS, + ENABLE_BLUETOOTH, + TOGGLE_APPLET, + TOGGLE_AUTOSHOW_APPLET, + SET_RECENTS, + ROTATE, + ALIGN_JOYSTICK, + LAYOUT, + TOGGLE_BATTERY_ICON, + TOGGLE_NOTIFICATIONS, + TOGGLE_INVERT_COLOR, + TOGGLE_12H_CLOCK, }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 7e7093857..1599456b2 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -22,856 +22,829 @@ static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu // These are offered to users as possible values for settings.recentlyActiveSeconds static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; -InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") -{ - // No timer tasks at boot - OSThread::disable(); +InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") { + // No timer tasks at boot + OSThread::disable(); - // Note: don't get instance if we're not actually using the backlight, - // or else you will unintentionally instantiate it - if (settings->optionalMenuItems.backlight) { - backlight = Drivers::LatchingBacklight::getInstance(); - } + // Note: don't get instance if we're not actually using the backlight, + // or else you will unintentionally instantiate it + if (settings->optionalMenuItems.backlight) { + backlight = Drivers::LatchingBacklight::getInstance(); + } - // Initialize the Canned Message store - // This is a shared nicheGraphics component - // - handles loading & parsing the canned messages - // - handles setting / getting of canned messages via apps (Client API Admin Messages) - cm.store = CannedMessageStore::getInstance(); + // Initialize the Canned Message store + // This is a shared nicheGraphics component + // - handles loading & parsing the canned messages + // - handles setting / getting of canned messages via apps (Client API Admin Messages) + cm.store = CannedMessageStore::getInstance(); } -void InkHUD::MenuApplet::onForeground() -{ - // We do need this before we render, but we can optimize by just calculating it once now - systemInfoPanelHeight = getSystemInfoPanelHeight(); +void InkHUD::MenuApplet::onForeground() { + // We do need this before we render, but we can optimize by just calculating it once now + systemInfoPanelHeight = getSystemInfoPanelHeight(); - // Display initial menu page - showPage(MenuPage::ROOT); + // Display initial menu page + showPage(MenuPage::ROOT); - // If device has a backlight which isn't controlled by aux button: - // backlight on always when menu opens. - // Courtesy to T-Echo users who removed the capacitive touch button - if (settings->optionalMenuItems.backlight) { - assert(backlight); - if (!backlight->isOn()) - backlight->peek(); - } + // If device has a backlight which isn't controlled by aux button: + // backlight on always when menu opens. + // Courtesy to T-Echo users who removed the capacitive touch button + if (settings->optionalMenuItems.backlight) { + assert(backlight); + if (!backlight->isOn()) + backlight->peek(); + } - // Prevent user applets requesting update while menu is open - // Handle button input with this applet - SystemApplet::lockRequests = true; - SystemApplet::handleInput = true; + // Prevent user applets requesting update while menu is open + // Handle button input with this applet + SystemApplet::lockRequests = true; + SystemApplet::handleInput = true; - // Begin the auto-close timeout - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - OSThread::enabled = true; + // Begin the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; - // Upgrade the refresh to FAST, for guaranteed responsiveness - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Upgrade the refresh to FAST, for guaranteed responsiveness + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } -void InkHUD::MenuApplet::onBackground() -{ - // Discard any data we generated while selecting a canned message - // Frees heap mem - freeCannedMessageResources(); +void InkHUD::MenuApplet::onBackground() { + // Discard any data we generated while selecting a canned message + // Frees heap mem + freeCannedMessageResources(); - // If device has a backlight which isn't controlled by aux button: - // Item in options submenu allows keeping backlight on after menu is closed - // If this item is deselected we will turn backlight off again, now that menu is closing - if (settings->optionalMenuItems.backlight) { - assert(backlight); - if (!backlight->isLatched()) - backlight->off(); - } + // If device has a backlight which isn't controlled by aux button: + // Item in options submenu allows keeping backlight on after menu is closed + // If this item is deselected we will turn backlight off again, now that menu is closing + if (settings->optionalMenuItems.backlight) { + assert(backlight); + if (!backlight->isLatched()) + backlight->off(); + } - // Stop the auto-timeout - OSThread::disable(); + // Stop the auto-timeout + OSThread::disable(); - // Resume normal rendering and button behavior of user applets - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; + // Resume normal rendering and button behavior of user applets + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Restore the user applet whose tile we borrowed - if (borrowedTileOwner) - borrowedTileOwner->bringToForeground(); - Tile *t = getTile(); - t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one) - borrowedTileOwner = nullptr; + // Restore the user applet whose tile we borrowed + if (borrowedTileOwner) + borrowedTileOwner->bringToForeground(); + Tile *t = getTile(); + t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one) + borrowedTileOwner = nullptr; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Open the menu // Parameter specifies which user-tile the menu will use // The user applet originally on this tile will be restored when the menu closes -void InkHUD::MenuApplet::show(Tile *t) -{ - // Remember who *really* owns this tile - borrowedTileOwner = t->getAssignedApplet(); +void InkHUD::MenuApplet::show(Tile *t) { + // Remember who *really* owns this tile + borrowedTileOwner = t->getAssignedApplet(); - // Hide the owner, if it is a valid applet - if (borrowedTileOwner) - borrowedTileOwner->sendToBackground(); + // Hide the owner, if it is a valid applet + if (borrowedTileOwner) + borrowedTileOwner->sendToBackground(); - // Break the owner's link with tile - // Relink it to menu applet - t->assignApplet(this); + // Break the owner's link with tile + // Relink it to menu applet + t->assignApplet(this); - // Show menu - bringToForeground(); + // Show menu + bringToForeground(); } // Auto-exit the menu applet after a period of inactivity // The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open. // By exiting the menu, we prevent users mistakenly believing that the data will update. -int32_t InkHUD::MenuApplet::runOnce() -{ - // runOnce's interval is pushed back when a button is pressed - // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC, - // so we close the menu. - showPage(EXIT); +int32_t InkHUD::MenuApplet::runOnce() { + // runOnce's interval is pushed back when a button is pressed + // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC, + // so we close the menu. + showPage(EXIT); - // Timer should disable after firing - // This is redundant, as onBackground() will also disable - return OSThread::disable(); + // Timer should disable after firing + // This is redundant, as onBackground() will also disable + return OSThread::disable(); } // Perform action for a menu item, then change page // Behaviors for MenuActions are defined here -void InkHUD::MenuApplet::execute(MenuItem item) -{ - // Perform an action - // ------------------ - switch (item.action) { +void InkHUD::MenuApplet::execute(MenuItem item) { + // Perform an action + // ------------------ + switch (item.action) { - // Open a submenu without performing any action - // Also handles exit - case NO_ACTION: - break; + // Open a submenu without performing any action + // Also handles exit + case NO_ACTION: + break; - case NEXT_TILE: - inkhud->nextTile(); - break; + case NEXT_TILE: + inkhud->nextTile(); + break; - case SEND_PING: - service->refreshLocalMeshNode(); - service->trySendPosition(NODENUM_BROADCAST, true); + case SEND_PING: + service->refreshLocalMeshNode(); + service->trySendPosition(NODENUM_BROADCAST, true); - // Force the next refresh to use FULL, to protect the display, as some users will probably spam this button - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); - break; + // Force the next refresh to use FULL, to protect the display, as some users will probably spam this button + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); + break; - case STORE_CANNEDMESSAGE_SELECTION: - cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry - break; + case STORE_CANNEDMESSAGE_SELECTION: + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + break; - case SEND_CANNEDMESSAGE: - cm.selectedRecipientItem = &cm.recipientItems.at(cursor); - sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here - break; + case SEND_CANNEDMESSAGE: + cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here + break; - case ROTATE: - inkhud->rotate(); - break; + case ROTATE: + inkhud->rotate(); + break; - case ALIGN_JOYSTICK: - inkhud->openAlignStick(); - break; + case ALIGN_JOYSTICK: + inkhud->openAlignStick(); + break; - case LAYOUT: - // Todo: smarter incrementing of tile count - settings->userTiles.count++; + case LAYOUT: + // Todo: smarter incrementing of tile count + settings->userTiles.count++; - if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet - settings->userTiles.count++; + if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet + settings->userTiles.count++; - if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high - settings->userTiles.count = 1; + if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high + settings->userTiles.count = 1; - inkhud->updateLayout(); - break; + inkhud->updateLayout(); + break; - case TOGGLE_APPLET: - settings->userApplets.active[cursor] = !settings->userApplets.active[cursor]; - inkhud->updateAppletSelection(); - break; + case TOGGLE_APPLET: + settings->userApplets.active[cursor] = !settings->userApplets.active[cursor]; + inkhud->updateAppletSelection(); + break; - case TOGGLE_AUTOSHOW_APPLET: - // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() - *items.at(cursor).checkState = !(*items.at(cursor).checkState); - break; + case TOGGLE_AUTOSHOW_APPLET: + // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() + *items.at(cursor).checkState = !(*items.at(cursor).checkState); + break; - case TOGGLE_NOTIFICATIONS: - settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; - break; + case TOGGLE_NOTIFICATIONS: + settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; + break; - case TOGGLE_INVERT_COLOR: - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - else - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + case TOGGLE_INVERT_COLOR: + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + else + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; - nodeDB->saveToDisk(SEGMENT_CONFIG); - break; + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; - case SET_RECENTS: - // Set value of settings.recentlyActiveSeconds - // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) - assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); - settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes - break; + case SET_RECENTS: + // Set value of settings.recentlyActiveSeconds + // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) + assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); + settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes + break; - case SHUTDOWN: - LOG_INFO("Shutting down from menu"); - shutdownAtMsec = millis(); - // Menu is then sent to background via onShutdown - break; + case SHUTDOWN: + LOG_INFO("Shutting down from menu"); + shutdownAtMsec = millis(); + // Menu is then sent to background via onShutdown + break; - case TOGGLE_BATTERY_ICON: - inkhud->toggleBatteryIcon(); - break; + case TOGGLE_BATTERY_ICON: + inkhud->toggleBatteryIcon(); + break; - case TOGGLE_BACKLIGHT: - // Note: backlight is already on in this situation - // We're marking that it should *remain* on once menu closes - assert(backlight); - if (backlight->isLatched()) - backlight->off(); - else - backlight->latch(); - break; + case TOGGLE_BACKLIGHT: + // Note: backlight is already on in this situation + // We're marking that it should *remain* on once menu closes + assert(backlight); + if (backlight->isLatched()) + backlight->off(); + else + backlight->latch(); + break; - case TOGGLE_12H_CLOCK: - config.display.use_12h_clock = !config.display.use_12h_clock; - nodeDB->saveToDisk(SEGMENT_CONFIG); - break; + case TOGGLE_12H_CLOCK: + config.display.use_12h_clock = !config.display.use_12h_clock; + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; - case TOGGLE_GPS: - gps->toggleGpsMode(); - nodeDB->saveToDisk(SEGMENT_CONFIG); - break; + case TOGGLE_GPS: + gps->toggleGpsMode(); + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; - case ENABLE_BLUETOOTH: - // This helps users recover from a bad wifi config - LOG_INFO("Enabling Bluetooth"); - config.network.wifi_enabled = false; - config.bluetooth.enabled = true; - nodeDB->saveToDisk(); - rebootAtMsec = millis() + 2000; - break; + case ENABLE_BLUETOOTH: + // This helps users recover from a bad wifi config + LOG_INFO("Enabling Bluetooth"); + config.network.wifi_enabled = false; + config.bluetooth.enabled = true; + nodeDB->saveToDisk(); + rebootAtMsec = millis() + 2000; + break; - default: - LOG_WARN("Action not implemented"); - } + default: + LOG_WARN("Action not implemented"); + } - // Move to next page, as defined for the MenuItem - showPage(item.nextPage); + // Move to next page, as defined for the MenuItem + showPage(item.nextPage); } // Display a new page of MenuItems // May reload same page, or exit menu applet entirely // Fills the MenuApplet::items vector -void InkHUD::MenuApplet::showPage(MenuPage page) -{ - items.clear(); - items.shrink_to_fit(); +void InkHUD::MenuApplet::showPage(MenuPage page) { + items.clear(); + items.shrink_to_fit(); - switch (page) { - case ROOT: - // Optional: next applet - if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) - items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown + switch (page) { + case ROOT: + // Optional: next applet + if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) + items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown - items.push_back(MenuItem("Send", MenuPage::SEND)); - items.push_back(MenuItem("Options", MenuPage::OPTIONS)); - // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO - items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::EXIT; - break; + items.push_back(MenuItem("Send", MenuPage::SEND)); + items.push_back(MenuItem("Options", MenuPage::OPTIONS)); + // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO + items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::EXIT; + break; - case SEND: - populateSendPage(); - previousPage = MenuPage::ROOT; - break; + case SEND: + populateSendPage(); + previousPage = MenuPage::ROOT; + break; - case CANNEDMESSAGE_RECIPIENT: - populateRecipientPage(); - previousPage = MenuPage::OPTIONS; - break; + case CANNEDMESSAGE_RECIPIENT: + populateRecipientPage(); + previousPage = MenuPage::OPTIONS; + break; - case OPTIONS: - // Optional: backlight - if (settings->optionalMenuItems.backlight) - items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label - MenuAction::TOGGLE_BACKLIGHT, // Action - MenuPage::EXIT // Exit once complete - )); + case OPTIONS: + // Optional: backlight + if (settings->optionalMenuItems.backlight) + items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label + MenuAction::TOGGLE_BACKLIGHT, // Action + MenuPage::EXIT // Exit once complete + )); - // Optional: GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) - items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); + // Optional: GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) + items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - // Optional: Enable Bluetooth, in case of lost wifi connection - if (!config.bluetooth.enabled || config.network.wifi_enabled) - items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT)); + // Optional: Enable Bluetooth, in case of lost wifi connection + if (!config.bluetooth.enabled || config.network.wifi_enabled) + items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT)); - items.push_back(MenuItem("Applets", MenuPage::APPLETS)); - items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); - items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); - if (settings->userTiles.maxCount > 1) - items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); - items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); - if (settings->joystick.enabled) - items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); - items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, - &settings->optionalFeatures.notifications)); - items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, - &settings->optionalFeatures.batteryIcon)); + items.push_back(MenuItem("Applets", MenuPage::APPLETS)); + items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); + items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); + if (settings->userTiles.maxCount > 1) + items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); + items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); + if (settings->joystick.enabled) + items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); + items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); + items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings->optionalFeatures.batteryIcon)); - invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); + invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); - items.push_back( - MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::ROOT; - break; + items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::ROOT; + break; - case APPLETS: - populateAppletPage(); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::OPTIONS; - break; + case APPLETS: + populateAppletPage(); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; + break; - case AUTOSHOW: - populateAutoshowPage(); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::OPTIONS; - break; + case AUTOSHOW: + populateAutoshowPage(); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; + break; - case RECENTS: - populateRecentsPage(); - previousPage = MenuPage::OPTIONS; - break; + case RECENTS: + populateRecentsPage(); + previousPage = MenuPage::OPTIONS; + break; - case EXIT: - sendToBackground(); // Menu applet dismissed, allow normal behavior to resume - break; + case EXIT: + sendToBackground(); // Menu applet dismissed, allow normal behavior to resume + break; - default: - LOG_WARN("Page not implemented"); + default: + LOG_WARN("Page not implemented"); + } + + // Reset the cursor, unless reloading same page + // (or now out-of-bounds) + if (page != currentPage || cursor >= items.size()) { + cursor = 0; + + // ROOT menu has special handling: unselected at first, to emphasise the system info panel + if (page == ROOT) + cursorShown = false; + } + + // Remember which page we are on now + currentPage = page; +} + +void InkHUD::MenuApplet::onRender() { + if (items.size() == 0) + LOG_ERROR("Empty Menu"); + + // Dimensions for the slots where we will draw menuItems + const float padding = 0.05; + const uint16_t itemH = fontSmall.lineHeight() * 2; + const int16_t itemW = width() - X(padding) - X(padding); + const int16_t itemL = X(padding); + const int16_t itemR = X(1 - padding); + int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu. + + // How many full menuItems will fit on screen + uint8_t slotCount = (height() - itemT) / itemH; + + // System info panel at the top of the menu + // ========================================= + + uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground + const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel + + // System info - top + // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen. + // This is the same behavior we expect from the non-root menus. + // Implementing this with the systemp panel is slightly annoying though, + // and required adding the MenuApplet::getSystemInfoPanelHeight method + int16_t siT; + if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count) + siT = 0; + else + siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH); + + // If showing ROOT menu, + // and the panel isn't yet scrolled off screen top + if (currentPage == ROOT) { + drawSystemInfoPanel(0, siT, width()); // Draw the panel. + itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel + } + + // Draw menu items + // =================== + + // Which item will be drawn to the top-most slot? + // Initially, this is the item 0, but may increase once we begin scrolling + uint8_t firstItem; + if (cursor < slotCount) + firstItem = 0; + else + firstItem = cursor - (slotCount - 1); + + // Which item will be drawn to the bottom-most slot? + // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow + // This may be less than the slot-count, if we are reaching the end of the menuItems + uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1); + + // -- Loop: draw each (visible) menu item -- + for (uint8_t i = firstItem; i <= lastItem; i++) { + // Grab the menuItem + MenuItem item = items.at(i); + + // Center-line for the text + int16_t center = itemT + (itemH / 2); + + // Box, if currently selected + if (cursorShown && i == cursor) + drawRect(itemL, itemT, itemW, itemH, BLACK); + + // Item's text + printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + + // Checkbox, if relevant + if (item.checkState) { + const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height + const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left + const int16_t cbT = center - (cbWH / 2); // Checkbox : top + // Checkbox ticked + if (*(item.checkState)) { + drawRect(cbL, cbT, cbWH, cbWH, BLACK); + // First point of tick: pen down + const int16_t t1Y = center; + const int16_t t1X = cbL + 3; + // Second point of tick: base + const int16_t t2Y = center + (cbWH / 2) - 2; + const int16_t t2X = cbL + (cbWH / 2); + // Third point of tick: end of tail + const int16_t t3Y = center - (cbWH / 2) - 2; + const int16_t t3X = cbL + cbWH + 2; + // Draw twice: faux bold + drawLine(t1X, t1Y, t2X, t2Y, BLACK); + drawLine(t2X, t2Y, t3X, t3Y, BLACK); + drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK); + drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK); + } + // Checkbox ticked + else + drawRect(cbL, cbT, cbWH, cbWH, BLACK); } - // Reset the cursor, unless reloading same page - // (or now out-of-bounds) - if (page != currentPage || cursor >= items.size()) { - cursor = 0; - - // ROOT menu has special handling: unselected at first, to emphasise the system info panel - if (page == ROOT) - cursorShown = false; - } - - // Remember which page we are on now - currentPage = page; + // Increment the y value (top) as we go + itemT += itemH; + } } -void InkHUD::MenuApplet::onRender() -{ - if (items.size() == 0) - LOG_ERROR("Empty Menu"); - - // Dimensions for the slots where we will draw menuItems - const float padding = 0.05; - const uint16_t itemH = fontSmall.lineHeight() * 2; - const int16_t itemW = width() - X(padding) - X(padding); - const int16_t itemL = X(padding); - const int16_t itemR = X(1 - padding); - int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu. - - // How many full menuItems will fit on screen - uint8_t slotCount = (height() - itemT) / itemH; - - // System info panel at the top of the menu - // ========================================= - - uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground - const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel - - // System info - top - // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen. - // This is the same behavior we expect from the non-root menus. - // Implementing this with the systemp panel is slightly annoying though, - // and required adding the MenuApplet::getSystemInfoPanelHeight method - int16_t siT; - if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count) - siT = 0; - else - siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH); - - // If showing ROOT menu, - // and the panel isn't yet scrolled off screen top - if (currentPage == ROOT) { - drawSystemInfoPanel(0, siT, width()); // Draw the panel. - itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel - } - - // Draw menu items - // =================== - - // Which item will be drawn to the top-most slot? - // Initially, this is the item 0, but may increase once we begin scrolling - uint8_t firstItem; - if (cursor < slotCount) - firstItem = 0; - else - firstItem = cursor - (slotCount - 1); - - // Which item will be drawn to the bottom-most slot? - // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow - // This may be less than the slot-count, if we are reaching the end of the menuItems - uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1); - - // -- Loop: draw each (visible) menu item -- - for (uint8_t i = firstItem; i <= lastItem; i++) { - // Grab the menuItem - MenuItem item = items.at(i); - - // Center-line for the text - int16_t center = itemT + (itemH / 2); - - // Box, if currently selected - if (cursorShown && i == cursor) - drawRect(itemL, itemT, itemW, itemH, BLACK); - - // Item's text - printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); - - // Checkbox, if relevant - if (item.checkState) { - const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height - const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left - const int16_t cbT = center - (cbWH / 2); // Checkbox : top - // Checkbox ticked - if (*(item.checkState)) { - drawRect(cbL, cbT, cbWH, cbWH, BLACK); - // First point of tick: pen down - const int16_t t1Y = center; - const int16_t t1X = cbL + 3; - // Second point of tick: base - const int16_t t2Y = center + (cbWH / 2) - 2; - const int16_t t2X = cbL + (cbWH / 2); - // Third point of tick: end of tail - const int16_t t3Y = center - (cbWH / 2) - 2; - const int16_t t3X = cbL + cbWH + 2; - // Draw twice: faux bold - drawLine(t1X, t1Y, t2X, t2Y, BLACK); - drawLine(t2X, t2Y, t3X, t3Y, BLACK); - drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK); - drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK); - } - // Checkbox ticked - else - drawRect(cbL, cbT, cbWH, cbWH, BLACK); - } - - // Increment the y value (top) as we go - itemT += itemH; - } -} - -void InkHUD::MenuApplet::onButtonShortPress() -{ - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - if (!settings->joystick.enabled) { - // Move menu cursor to next entry, then update - if (cursorShown) - cursor = (cursor + 1) % items.size(); - else - cursorShown = true; - requestUpdate(Drivers::EInk::UpdateTypes::FAST); - } else { - if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); - if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); - } -} - -void InkHUD::MenuApplet::onButtonLongPress() -{ - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close - - // If we didn't already request a specialized update, when handling a menu action, - // then perform the usual fast update. - // FAST keeps things responsive: important because we're dealing with user input - if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onExitShort() -{ - // Exit the menu - showPage(MenuPage::EXIT); - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavUp() -{ - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - // Move menu cursor to previous entry, then update - if (cursor == 0) - cursor = items.size() - 1; - else - cursor--; - - if (!cursorShown) - cursorShown = true; - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavDown() -{ - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); +void InkHUD::MenuApplet::onButtonShortPress() { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!settings->joystick.enabled) { // Move menu cursor to next entry, then update if (cursorShown) - cursor = (cursor + 1) % items.size(); + cursor = (cursor + 1) % items.size(); else - cursorShown = true; - + cursorShown = true; requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavLeft() -{ - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - // Go to the previous menu page - showPage(previousPage); - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavRight() -{ - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - + } else { if (cursorShown) - execute(items.at(cursor)); + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onButtonLongPress() { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + + // If we didn't already request a specialized update, when handling a menu action, + // then perform the usual fast update. + // FAST keeps things responsive: important because we're dealing with user input + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onExitShort() { + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavUp() { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to previous entry, then update + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + + if (!cursorShown) + cursorShown = true; + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavDown() { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to next entry, then update + if (cursorShown) + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavLeft() { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Go to the previous menu page + showPage(previousPage); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavRight() { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } // Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu -void InkHUD::MenuApplet::populateAppletPage() -{ - assert(items.size() == 0); +void InkHUD::MenuApplet::populateAppletPage() { + assert(items.size() == 0); - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - const char *name = inkhud->userApplets.at(i)->name; - bool *isActive = &(settings->userApplets.active[i]); - items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive)); - } + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + const char *name = inkhud->userApplets.at(i)->name; + bool *isActive = &(settings->userApplets.active[i]); + items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive)); + } } -// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data -// We only populate this menu page with applets which are actually active -// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient. -void InkHUD::MenuApplet::populateAutoshowPage() -{ - assert(items.size() == 0); +// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have +// new data We only populate this menu page with applets which are actually active We use the MenuItem::checkState +// pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient. +void InkHUD::MenuApplet::populateAutoshowPage() { + assert(items.size() == 0); - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - // Only add a menu item if applet is active - if (settings->userApplets.active[i]) { - const char *name = inkhud->userApplets.at(i)->name; - bool *isActive = &(settings->userApplets.autoshow[i]); - items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive)); - } + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + // Only add a menu item if applet is active + if (settings->userApplets.active[i]) { + const char *name = inkhud->userApplets.at(i)->name; + bool *isActive = &(settings->userApplets.autoshow[i]); + items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive)); } + } } // Create MenuItem entries to select our definition of "Recent" // Controls how long data will remain in any "Recents" flavored applets -void InkHUD::MenuApplet::populateRecentsPage() -{ - // How many values are shown for use to choose from - constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); +void InkHUD::MenuApplet::populateRecentsPage() { + // How many values are shown for use to choose from + constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); - // Create an entry for each item in RECENTS_OPTIONS_MINUTES array - // (Defined at top of this file) - for (uint8_t i = 0; i < optionCount; i++) { - std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; - items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT)); - } + // Create an entry for each item in RECENTS_OPTIONS_MINUTES array + // (Defined at top of this file) + for (uint8_t i = 0; i < optionCount; i++) { + std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; + items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT)); + } } // MenuItem entries for the "send" page // Dynamically creates menu items based on available canned messages -void InkHUD::MenuApplet::populateSendPage() -{ - // Position / NodeInfo packet - items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); +void InkHUD::MenuApplet::populateSendPage() { + // Position / NodeInfo packet + items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); - // One menu item for each canned message - uint8_t count = cm.store->size(); - for (uint8_t i = 0; i < count; i++) { - // Gather the information for this item - CannedMessages::MessageItem messageItem; - messageItem.rawText = cm.store->at(i); - messageItem.label = parse(messageItem.rawText); + // One menu item for each canned message + uint8_t count = cm.store->size(); + for (uint8_t i = 0; i < count; i++) { + // Gather the information for this item + CannedMessages::MessageItem messageItem; + messageItem.rawText = cm.store->at(i); + messageItem.label = parse(messageItem.rawText); - // Store the item (until the menu closes) - cm.messageItems.push_back(messageItem); + // Store the item (until the menu closes) + cm.messageItems.push_back(messageItem); - // Create a menu item - const char *itemText = cm.messageItems.back().label.c_str(); - items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); - } + // Create a menu item + const char *itemText = cm.messageItems.back().label.c_str(); + items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); + } - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); } // Dynamically create MenuItem entries for possible canned message destinations // All available channels are shown // Favorite nodes are shown, provided we don't have an *excessive* amount -void InkHUD::MenuApplet::populateRecipientPage() -{ - // Create recipient data (and menu items) for any channels - // -------------------------------------------------------- +void InkHUD::MenuApplet::populateRecipientPage() { + // Create recipient data (and menu items) for any channels + // -------------------------------------------------------- - for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { - // Get the channel, and check if it's enabled - meshtastic_Channel &channel = channels.getByIndex(i); - if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) - continue; + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + // Get the channel, and check if it's enabled + meshtastic_Channel &channel = channels.getByIndex(i); + if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) + continue; - CannedMessages::RecipientItem r; + CannedMessages::RecipientItem r; - // Set index - r.channelIndex = channel.index; + // Set index + r.channelIndex = channel.index; - // Set a label for the menu item - r.label = "Ch " + to_string(i) + ": "; - if (channel.role == meshtastic_Channel_Role_PRIMARY) - r.label += "Primary"; - else - r.label += parse(channel.settings.name); + // Set a label for the menu item + r.label = "Ch " + to_string(i) + ": "; + if (channel.role == meshtastic_Channel_Role_PRIMARY) + r.label += "Primary"; + else + r.label += parse(channel.settings.name); - // Add to the list of recipients - cm.recipientItems.push_back(r); + // Add to the list of recipients + cm.recipientItems.push_back(r); - // Add a menu item for this recipient - const char *itemText = cm.recipientItems.back().label.c_str(); - items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); - } + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } - // Create recipient data (and menu items) for favorite nodes - // --------------------------------------------------------- + // Create recipient data (and menu items) for favorite nodes + // --------------------------------------------------------- - uint32_t nodeCount = nodeDB->getNumMeshNodes(); - uint32_t favoriteCount = 0; + uint32_t nodeCount = nodeDB->getNumMeshNodes(); + uint32_t favoriteCount = 0; - // Count favorites + // Count favorites + for (uint32_t i = 0; i < nodeCount; i++) { + if (nodeDB->getMeshNodeByIndex(i)->is_favorite) + favoriteCount++; + } + + // Only add favorites if the number is reasonable + // Don't want some monstrous list that takes 100 clicks to reach exit + if (favoriteCount < 20) { for (uint32_t i = 0; i < nodeCount; i++) { - if (nodeDB->getMeshNodeByIndex(i)->is_favorite) - favoriteCount++; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip node if not a favorite + if (!node->is_favorite) + continue; + + CannedMessages::RecipientItem r; + + r.dest = node->num; + r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) + + // Set a label for the menu item + r.label = "DM: "; + if (node->has_user) + r.label += parse(node->user.long_name); + else + r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); } + } - // Only add favorites if the number is reasonable - // Don't want some monstrous list that takes 100 clicks to reach exit - if (favoriteCount < 20) { - for (uint32_t i = 0; i < nodeCount; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - - // Skip node if not a favorite - if (!node->is_favorite) - continue; - - CannedMessages::RecipientItem r; - - r.dest = node->num; - r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) - - // Set a label for the menu item - r.label = "DM: "; - if (node->has_user) - r.label += parse(node->user.long_name); - else - r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? - - // Add to the list of recipients - cm.recipientItems.push_back(r); - - // Add a menu item for this recipient - const char *itemText = cm.recipientItems.back().label.c_str(); - items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); - } - } - - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); } // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. -void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight) -{ - // Reset the height - // We'll add to this as we add elements - uint16_t height = 0; +void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight) { + // Reset the height + // We'll add to this as we add elements + uint16_t height = 0; - // Clock (potentially) - // ==================== - std::string clockString = getTimeString(); - if (clockString.length() > 0) { - setFont(fontMedium); - printAt(width / 2, top, clockString, CENTER, TOP); + // Clock (potentially) + // ==================== + std::string clockString = getTimeString(); + if (clockString.length() > 0) { + setFont(fontMedium); + printAt(width / 2, top, clockString, CENTER, TOP); - height += fontMedium.lineHeight(); - height += fontMedium.lineHeight() * 0.1; // Padding below clock - } + height += fontMedium.lineHeight(); + height += fontMedium.lineHeight() * 0.1; // Padding below clock + } - // Stats - // =================== + // Stats + // =================== - setFont(fontSmall); + setFont(fontSmall); - // Position of the label row for the system info - const int16_t labelT = top + height; - height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing + // Position of the label row for the system info + const int16_t labelT = top + height; + height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing - // Position of the data row for the system info - const int16_t valT = top + height; - height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider) + // Position of the data row for the system info + const int16_t valT = top + height; + height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider) - // Position of divider between the info panel and the menu entries - const int16_t divY = top + height; - height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) + // Position of divider between the info panel and the menu entries + const int16_t divY = top + height; + height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) - // Create a variable number of columns - // Either 3 or 4, depending on whether we have GPS - // Todo - constexpr uint8_t N_COL = 3; - int16_t colL[N_COL]; - int16_t colC[N_COL]; - int16_t colR[N_COL]; - for (uint8_t i = 0; i < N_COL; i++) { - colL[i] = left + ((width / N_COL) * i); - colC[i] = colL[i] + ((width / N_COL) / 2); - colR[i] = colL[i] + (width / N_COL); - } + // Create a variable number of columns + // Either 3 or 4, depending on whether we have GPS + // Todo + constexpr uint8_t N_COL = 3; + int16_t colL[N_COL]; + int16_t colC[N_COL]; + int16_t colR[N_COL]; + for (uint8_t i = 0; i < N_COL; i++) { + colL[i] = left + ((width / N_COL) * i); + colC[i] = colL[i] + ((width / N_COL) / 2); + colR[i] = colL[i] + (width / N_COL); + } - // Info blocks, left to right + // Info blocks, left to right - // Voltage - float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; - char voltageStr[6]; // "XX.XV" - sprintf(voltageStr, "%.2fV", voltage); - printAt(colC[0], labelT, "Bat", CENTER, TOP); - printAt(colC[0], valT, voltageStr, CENTER, TOP); + // Voltage + float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; + char voltageStr[6]; // "XX.XV" + sprintf(voltageStr, "%.2fV", voltage); + printAt(colC[0], labelT, "Bat", CENTER, TOP); + printAt(colC[0], valT, voltageStr, CENTER, TOP); - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[0], y, BLACK); + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[0], y, BLACK); - // Channel Util - char chUtilStr[4]; // "XX%" - sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); - printAt(colC[1], labelT, "Ch", CENTER, TOP); - printAt(colC[1], valT, chUtilStr, CENTER, TOP); + // Channel Util + char chUtilStr[4]; // "XX%" + sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); + printAt(colC[1], labelT, "Ch", CENTER, TOP); + printAt(colC[1], valT, chUtilStr, CENTER, TOP); - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[1], y, BLACK); + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[1], y, BLACK); - // Duty Cycle (AirTimeTx) - char dutyUtilStr[4]; // "XX%" - sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); - printAt(colC[2], labelT, "Duty", CENTER, TOP); - printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); + // Duty Cycle (AirTimeTx) + char dutyUtilStr[4]; // "XX%" + sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); + printAt(colC[2], labelT, "Duty", CENTER, TOP); + printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); - /* - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[2], y, BLACK); + /* + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[2], y, BLACK); - // GPS satellites - todo - printAt(colC[3], labelT, "Sats", CENTER, TOP); - printAt(colC[3], valT, "ToDo", CENTER, TOP); - */ + // GPS satellites - todo + printAt(colC[3], labelT, "Sats", CENTER, TOP); + printAt(colC[3], valT, "ToDo", CENTER, TOP); + */ - // Horizontal divider, at bottom of system info panel - for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item - drawPixel(x, divY, BLACK); + // Horizontal divider, at bottom of system info panel + for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item + drawPixel(x, divY, BLACK); - if (renderedHeight != nullptr) - *renderedHeight = height; + if (renderedHeight != nullptr) + *renderedHeight = height; } // Get the height of the the panel drawn at the top of the menu // This is inefficient, as we do actually have to render the panel to determine the height // It solves a catch-22 situation, where slotCount needs to know panel height, and panel height needs to know slotCount -uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() -{ - // Render *far* off screen - uint16_t height = 0; - drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height); +uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() { + // Render *far* off screen + uint16_t height = 0; + drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height); - return height; + return height; } // Send a text message to the mesh // Used to send our canned messages -void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) -{ - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - p->to = dest; - p->channel = channel; - p->want_ack = true; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); +void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) { + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - // Tack on a bell character if requested - if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator - p->decoded.payload.size++; - } + // Tack on a bell character if requested + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator + p->decoded.payload.size++; + } - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone + service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone } // Free up any heap mmemory we'd used while selecting / sending canned messages -void InkHUD::MenuApplet::freeCannedMessageResources() -{ - cm.selectedMessageItem = nullptr; - cm.selectedRecipientItem = nullptr; - cm.messageItems.clear(); - cm.recipientItems.clear(); +void InkHUD::MenuApplet::freeCannedMessageResources() { + cm.selectedMessageItem = nullptr; + cm.selectedRecipientItem = nullptr; + cm.messageItems.clear(); + cm.recipientItems.clear(); } #endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 4f9f92227..cbf2f3340 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -14,91 +14,88 @@ #include "Channels.h" #include "concurrency/OSThread.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { class Applet; -class MenuApplet : public SystemApplet, public concurrency::OSThread -{ +class MenuApplet : public SystemApplet, public concurrency::OSThread { +public: + MenuApplet(); + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + void onRender() override; + + void show(Tile *t); // Open the menu, onto a user tile + +protected: + Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton + + int32_t runOnce() override; + + void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any + void showPage(MenuPage page); // Load and display a MenuPage + + void populateSendPage(); // Dynamically create MenuItems including canned messages + void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message + void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets + void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow + void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + + uint16_t getSystemInfoPanelHeight(); + void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, + uint16_t *height = nullptr); // Info panel at top of root menu + void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh + void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data + + MenuPage currentPage = MenuPage::ROOT; + MenuPage previousPage = MenuPage::EXIT; + uint8_t cursor = 0; // Which menu item is currently highlighted + bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) + + uint16_t systemInfoPanelHeight = 0; // Need to know before we render + + std::vector items; // MenuItems for the current page. Filled by ShowPage + + // Data for selecting and sending canned messages via the menu + // Placed into a sub-class for organization only + class CannedMessages { public: - MenuApplet(); - void onForeground() override; - void onBackground() override; - void onButtonShortPress() override; - void onButtonLongPress() override; - void onExitShort() override; - void onNavUp() override; - void onNavDown() override; - void onNavLeft() override; - void onNavRight() override; - void onRender() override; + // Share NicheGraphics component + // Handles loading, getting, setting + CannedMessageStore *store; - void show(Tile *t); // Open the menu, onto a user tile + // One canned message + // Links the menu item to the true message text + struct MessageItem { + std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed + std::string rawText; // The message which will be sent, if this item is selected + } *selectedMessageItem; - protected: - Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton + // One possible destination for a canned message + // Links the menu item to the intended recipient + // May represent either broadcast or DM + struct RecipientItem { + std::string label; // Shown in menu + NodeNum dest = NODENUM_BROADCAST; + uint8_t channelIndex = 0; + } *selectedRecipientItem; - int32_t runOnce() override; + // These lists are generated when the menu page is populated + // Cleared onBackground (when MenuApplet closes) + std::vector messageItems; + std::vector recipientItems; + } cm; - void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any - void showPage(MenuPage page); // Load and display a MenuPage + Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu - void populateSendPage(); // Dynamically create MenuItems including canned messages - void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message - void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets - void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow - void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds - - uint16_t getSystemInfoPanelHeight(); - void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, - uint16_t *height = nullptr); // Info panel at top of root menu - void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh - void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data - - MenuPage currentPage = MenuPage::ROOT; - MenuPage previousPage = MenuPage::EXIT; - uint8_t cursor = 0; // Which menu item is currently highlighted - bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) - - uint16_t systemInfoPanelHeight = 0; // Need to know before we render - - std::vector items; // MenuItems for the current page. Filled by ShowPage - - // Data for selecting and sending canned messages via the menu - // Placed into a sub-class for organization only - class CannedMessages - { - public: - // Share NicheGraphics component - // Handles loading, getting, setting - CannedMessageStore *store; - - // One canned message - // Links the menu item to the true message text - struct MessageItem { - std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed - std::string rawText; // The message which will be sent, if this item is selected - } *selectedMessageItem; - - // One possible destination for a canned message - // Links the menu item to the intended recipient - // May represent either broadcast or DM - struct RecipientItem { - std::string label; // Shown in menu - NodeNum dest = NODENUM_BROADCAST; - uint8_t channelIndex = 0; - } *selectedRecipientItem; - - // These lists are generated when the menu page is populated - // Cleared onBackground (when MenuApplet closes) - std::vector messageItems; - std::vector recipientItems; - } cm; - - Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu - - bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options + bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h index c74fe3d8a..b483fd6d1 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h @@ -19,27 +19,23 @@ Added to MenuPages in InkHUD::showPage #include "./MenuAction.h" #include "./MenuPage.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { // One item of a MenuPage -class MenuItem -{ - public: - std::string label; - MenuAction action = NO_ACTION; - MenuPage nextPage = EXIT; - bool *checkState = nullptr; +class MenuItem { +public: + std::string label; + MenuAction action = NO_ACTION; + MenuPage nextPage = EXIT; + bool *checkState = nullptr; - // Various constructors, depending on the intended function of the item + // Various constructors, depending on the intended function of the item - MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {} - MenuItem(const char *label, MenuAction action) : label(label), action(action) {} - MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {} - MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState) - : label(label), action(action), nextPage(nextPage), checkState(checkState) - { - } + MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {} + MenuItem(const char *label, MenuAction action) : label(label), action(action) {} + MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {} + MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState) + : label(label), action(action), nextPage(nextPage), checkState(checkState) {} }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index 389e411c3..ab2ddc170 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -11,19 +11,18 @@ Structure of the menu is defined in InkHUD::showPage #include "configuration.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { // Sub-menu for MenuApplet enum MenuPage : uint8_t { - ROOT, // Initial menu page - SEND, - CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message - OPTIONS, - APPLETS, - AUTOSHOW, - RECENTS, // Select length of "recentlyActiveSeconds" - EXIT, // Dismiss the menu applet + ROOT, // Initial menu page + SEND, + CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message + OPTIONS, + APPLETS, + AUTOSHOW, + RECENTS, // Select length of "recentlyActiveSeconds" + EXIT, // Dismiss the menu applet }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h b/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h index d8c4f8366..c82e667e8 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h @@ -4,8 +4,9 @@ A notification which might be displayed by the NotificationApplet -An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification. -An Applet should veto a notification if it is already displaying the same info which the notification would convey. +An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the +notification. An Applet should veto a notification if it is already displaying the same info which the notification +would convey. */ @@ -13,26 +14,24 @@ An Applet should veto a notification if it is already displaying the same info w #include "configuration.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class Notification -{ - public: - enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type; +class Notification { +public: + enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type; - uint32_t timestamp; + uint32_t timestamp; - uint8_t getChannel() { return channel; } - uint32_t getSender() { return sender; } - uint8_t getBatteryPercentage() { return batteryPercentage; } + uint8_t getChannel() { return channel; } + uint32_t getSender() { return sender; } + uint8_t getBatteryPercentage() { return batteryPercentage; } - friend class NotificationApplet; + friend class NotificationApplet; - protected: - uint8_t channel; - uint32_t sender; - uint8_t batteryPercentage; +protected: + uint8_t channel; + uint32_t sender; + uint8_t batteryPercentage; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index 2ea9c7fe0..dc759a80c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -12,268 +12,247 @@ using namespace NicheGraphics; -InkHUD::NotificationApplet::NotificationApplet() -{ - textMessageObserver.observe(textMessageModule); -} +InkHUD::NotificationApplet::NotificationApplet() { textMessageObserver.observe(textMessageModule); } // Collect meta-info about the text message, and ask for approval for the notification // No need to save the message itself; we can use the cached InkHUD::latestMessage data during render() -int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) -{ - // System applets are always active - assert(isActive()); +int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { + // System applets are always active + assert(isActive()); - // Abort if feature disabled - // This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled - if (!settings->optionalFeatures.notifications) - return 0; - - // Abort if this is an outgoing message - if (getFrom(p) == nodeDB->getNodeNum()) - return 0; - - Notification n; - n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - - // Gather info: in-channel message - if (isBroadcast(p->to)) { - n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; - n.channel = p->channel; - } - - // Gather info: DM - else { - n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT; - n.sender = p->from; - } - - // Close an old notification, if shown - dismiss(); - - // Check if we should display the notification - // A foreground applet might already be displaying this info - hasNotification = true; - currentNotification = n; - if (isApproved()) { - bringToForeground(); - inkhud->forceUpdate(); - } else - hasNotification = false; // Clear the pending notification: it was rejected - - // Return zero: no issues here, carry on notifying other observers! + // Abort if feature disabled + // This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled + if (!settings->optionalFeatures.notifications) return 0; + + // Abort if this is an outgoing message + if (getFrom(p) == nodeDB->getNodeNum()) + return 0; + + Notification n; + n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + + // Gather info: in-channel message + if (isBroadcast(p->to)) { + n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + n.channel = p->channel; + } + + // Gather info: DM + else { + n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT; + n.sender = p->from; + } + + // Close an old notification, if shown + dismiss(); + + // Check if we should display the notification + // A foreground applet might already be displaying this info + hasNotification = true; + currentNotification = n; + if (isApproved()) { + bringToForeground(); + inkhud->forceUpdate(); + } else + hasNotification = false; // Clear the pending notification: it was rejected + + // Return zero: no issues here, carry on notifying other observers! + return 0; } -void InkHUD::NotificationApplet::onRender() -{ - // Clear the region beneath the tile - // Most applets are drawing onto an empty frame buffer and don't need to do this - // We do need to do this with the battery though, as it is an "overlay" - fillRect(0, 0, width(), height(), WHITE); +void InkHUD::NotificationApplet::onRender() { + // Clear the region beneath the tile + // Most applets are drawing onto an empty frame buffer and don't need to do this + // We do need to do this with the battery though, as it is an "overlay" + fillRect(0, 0, width(), height(), WHITE); - // Padding (horizontal) - const uint16_t padW = 4; + // Padding (horizontal) + const uint16_t padW = 4; - // Main border - drawRect(0, 0, width(), height(), BLACK); - // drawRect(1, 1, width() - 2, height() - 2, BLACK); + // Main border + drawRect(0, 0, width(), height(), BLACK); + // drawRect(1, 1, width() - 2, height() - 2, BLACK); - // Timestamp (potentially) - // ==================== - std::string ts = getTimeString(currentNotification.timestamp); - uint16_t tsW = 0; - int16_t divX = 0; + // Timestamp (potentially) + // ==================== + std::string ts = getTimeString(currentNotification.timestamp); + uint16_t tsW = 0; + int16_t divX = 0; - // Timestamp available - if (ts.length() > 0) { - tsW = getTextWidth(ts); - divX = padW + tsW + padW; + // Timestamp available + if (ts.length() > 0) { + tsW = getTextWidth(ts); + divX = padW + tsW + padW; - hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background - drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text + hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background + drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text - setCrop(1, 1, divX - 1, height() - 2); - - // Drop shadow - setTextColor(WHITE); - printThick(padW + (tsW / 2), height() / 2, ts, 4, 4); - - // Bold text - setTextColor(BLACK); - printThick(padW + (tsW / 2), height() / 2, ts, 2, 1); - } - - // Main text - // ===================== - - // Background fill - // - medium dark (1/3) - hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK); - - uint16_t availableWidth = width() - divX - padW; - std::string text = getNotificationText(availableWidth); - - int16_t textM = divX + padW + (getTextWidth(text) / 2); - - // Restrict area for printing - // - don't overlap border, or divider - setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); + setCrop(1, 1, divX - 1, height() - 2); // Drop shadow - // - thick white text setTextColor(WHITE); - printThick(textM, height() / 2, text, 4, 4); + printThick(padW + (tsW / 2), height() / 2, ts, 4, 4); - // Main text - // - faux bold: double width + // Bold text setTextColor(BLACK); - printThick(textM, height() / 2, text, 2, 1); + printThick(padW + (tsW / 2), height() / 2, ts, 2, 1); + } + + // Main text + // ===================== + + // Background fill + // - medium dark (1/3) + hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK); + + uint16_t availableWidth = width() - divX - padW; + std::string text = getNotificationText(availableWidth); + + int16_t textM = divX + padW + (getTextWidth(text) / 2); + + // Restrict area for printing + // - don't overlap border, or divider + setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); + + // Drop shadow + // - thick white text + setTextColor(WHITE); + printThick(textM, height() / 2, text, 4, 4); + + // Main text + // - faux bold: double width + setTextColor(BLACK); + printThick(textM, height() / 2, text, 2, 1); } -void InkHUD::NotificationApplet::onForeground() -{ - handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification +void InkHUD::NotificationApplet::onForeground() { + handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification } -void InkHUD::NotificationApplet::onBackground() -{ - handleInput = false; +void InkHUD::NotificationApplet::onBackground() { handleInput = false; } + +void InkHUD::NotificationApplet::onButtonShortPress() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onButtonShortPress() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onButtonLongPress() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onButtonLongPress() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onExitShort() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onExitShort() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onExitLong() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onExitLong() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavUp() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavUp() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavDown() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavDown() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavLeft() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavLeft() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); -} - -void InkHUD::NotificationApplet::onNavRight() -{ - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavRight() { + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification // Called internally when we first get a "notifiable event", and then again before render, // in case autoshow swapped which applet was displayed -bool InkHUD::NotificationApplet::isApproved() -{ - // Instead of an assert - if (!hasNotification) { - LOG_WARN("No notif to approve"); - return false; - } +bool InkHUD::NotificationApplet::isApproved() { + // Instead of an assert + if (!hasNotification) { + LOG_WARN("No notif to approve"); + return false; + } - // Ask all visible user applets for approval - for (Applet *ua : inkhud->userApplets) { - if (ua->isForeground() && !ua->approveNotification(currentNotification)) - return false; - } + // Ask all visible user applets for approval + for (Applet *ua : inkhud->userApplets) { + if (ua->isForeground() && !ua->approveNotification(currentNotification)) + return false; + } - return true; + return true; } // Mark that the notification should no-longer be rendered // In addition to calling thing method, code needs to request a re-render of all applets -void InkHUD::NotificationApplet::dismiss() -{ - sendToBackground(); - hasNotification = false; - // Not requesting update directly from this method, - // as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn +void InkHUD::NotificationApplet::dismiss() { + sendToBackground(); + hasNotification = false; + // Not requesting update directly from this method, + // as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever + // drawn } // Get a string for the main body text of a notification // Formatted to suit screen width // Takes info from InkHUD::currentNotification -std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) -{ - assert(hasNotification); +std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) { + assert(hasNotification); - std::string text; + std::string text; - // Text message - // ============== + // Text message + // ============== - if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, - Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { + if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { - // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently - bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently + bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; - // Pick source of message - MessageStore::Message *message = - isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; + // Pick source of message + MessageStore::Message *message = isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; - // Find info about the sender - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); + // Find info about the sender + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); - // Leading tag (channel vs. DM) - text += isBroadcast ? "From:" : "DM: "; + // Leading tag (channel vs. DM) + text += isBroadcast ? "From:" : "DM: "; - // Sender id - if (node && node->has_user) - text += parseShortName(node); - else - text += hexifyNodeNum(message->sender); + // Sender id + if (node && node->has_user) + text += parseShortName(node); + else + text += hexifyNodeNum(message->sender); - // Check if text fits - // - use a longer string, if we have the space - if (getTextWidth(text) < widthAvailable * 0.5) { - text.clear(); + // Check if text fits + // - use a longer string, if we have the space + if (getTextWidth(text) < widthAvailable * 0.5) { + text.clear(); - // Leading tag (channel vs. DM) - text += isBroadcast ? "Msg from " : "DM from "; + // Leading tag (channel vs. DM) + text += isBroadcast ? "Msg from " : "DM from "; - // Sender id - if (node && node->has_user) - text += parseShortName(node); - else - text += hexifyNodeNum(message->sender); + // Sender id + if (node && node->has_user) + text += parseShortName(node); + else + text += hexifyNodeNum(message->sender); - text += ": "; - text += message->text; - } + text += ": "; + text += message->text; } + } - // Parse any non-ascii characters and return - return parse(text); + // Parse any non-ascii characters and return + return parse(text); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index 16ea13407..795ab36e8 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -18,40 +18,38 @@ Feature should be optional; enable disable via on-screen menu #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class NotificationApplet : public SystemApplet -{ - public: - NotificationApplet(); +class NotificationApplet : public SystemApplet { +public: + NotificationApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onButtonShortPress() override; - void onButtonLongPress() override; - void onExitShort() override; - void onExitLong() override; - void onNavUp() override; - void onNavDown() override; - void onNavLeft() override; - void onNavRight() override; + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + int onReceiveTextMessage(const meshtastic_MeshPacket *p); - bool isApproved(); // Does a foreground applet make notification redundant? - void dismiss(); // Close the Notification Popup + bool isApproved(); // Does a foreground applet make notification redundant? + void dismiss(); // Close the Notification Popup - protected: - // Get notified when a new text message arrives - CallbackObserver textMessageObserver = - CallbackObserver(this, &NotificationApplet::onReceiveTextMessage); +protected: + // Get notified when a new text message arrives + CallbackObserver textMessageObserver = + CallbackObserver(this, &NotificationApplet::onReceiveTextMessage); - std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width + std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width - bool hasNotification = false; // Only used for assert. Todo: remove? - Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render() + bool hasNotification = false; // Only used for assert. Todo: remove? + Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render() }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index 09931f109..e777488fb 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -4,74 +4,67 @@ using namespace NicheGraphics; -InkHUD::PairingApplet::PairingApplet() -{ - bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); +InkHUD::PairingApplet::PairingApplet() { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); } + +void InkHUD::PairingApplet::onRender() { + // Header + setFont(fontMedium); + printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM); + setFont(fontSmall); + printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP); + + // Passkey + setFont(fontMedium); + printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2); + + // Device's bluetooth name, if it will fit + setFont(fontSmall); + std::string name = "Name: " + parse(getDeviceName()); + if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " + name = parse(getDeviceName()); + if (getTextWidth(name) < width()) // Does it fit? + printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); } -void InkHUD::PairingApplet::onRender() -{ - // Header - setFont(fontMedium); - printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM); - setFont(fontSmall); - printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP); +void InkHUD::PairingApplet::onForeground() { + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; +} +void InkHUD::PairingApplet::onBackground() { + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; - // Passkey - setFont(fontMedium); - printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2); - - // Device's bluetooth name, if it will fit - setFont(fontSmall); - std::string name = "Name: " + parse(getDeviceName()); - if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " - name = parse(getDeviceName()); - if (getTextWidth(name) < width()) // Does it fit? - printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::PairingApplet::onForeground() -{ - // Prevent most other applets from requesting update, and skip their rendering entirely - // Another system applet with a higher precedence can potentially ignore this - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; -} -void InkHUD::PairingApplet::onBackground() -{ - // Allow normal update behavior to resume - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; +int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) { + // The standard Meshtastic convention is to pass these "generic" Status objects, + // check their type, and then cast them. + // We'll mimic that behavior, just to keep in line with the other Statuses, + // even though I'm not sure what the original reason for jumping through these extra hoops was. + assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); + meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); -} + // When pairing begins + if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { + // Store the passkey for rendering + passkey = bluetoothStatus->getPasskey(); -int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) -{ - // The standard Meshtastic convention is to pass these "generic" Status objects, - // check their type, and then cast them. - // We'll mimic that behavior, just to keep in line with the other Statuses, - // even though I'm not sure what the original reason for jumping through these extra hoops was. - assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; + // Show pairing screen + bringToForeground(); + } - // When pairing begins - if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { - // Store the passkey for rendering - passkey = bluetoothStatus->getPasskey(); + // When pairing ends + // or rather, when something changes, and we shouldn't be showing the pairing screen + else if (isForeground()) + sendToBackground(); - // Show pairing screen - bringToForeground(); - } - - // When pairing ends - // or rather, when something changes, and we shouldn't be showing the pairing screen - else if (isForeground()) - sendToBackground(); - - return 0; // No special result to report back to Observable + return 0; // No special result to report back to Observable } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h index b89783a25..a60a79fb6 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h @@ -14,26 +14,24 @@ #include "main.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class PairingApplet : public SystemApplet -{ - public: - PairingApplet(); +class PairingApplet : public SystemApplet { +public: + PairingApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; + void onRender() override; + void onForeground() override; + void onBackground() override; - int onBluetoothStatusUpdate(const meshtastic::Status *status); + int onBluetoothStatusUpdate(const meshtastic::Status *status); - protected: - // Get notified when status of the Bluetooth connection changes - CallbackObserver bluetoothStatusObserver = - CallbackObserver(this, &PairingApplet::onBluetoothStatusUpdate); +protected: + // Get notified when status of the Bluetooth connection changes + CallbackObserver bluetoothStatusObserver = + CallbackObserver(this, &PairingApplet::onBluetoothStatusUpdate); - std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros + std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp index 99cdeb0ac..ae6a466e8 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp @@ -4,10 +4,9 @@ using namespace NicheGraphics; -void InkHUD::PlaceholderApplet::onRender() -{ - // This placeholder applet fills its area with sparse diagonal lines - hatchRegion(0, 0, width(), height(), 8, BLACK); +void InkHUD::PlaceholderApplet::onRender() { + // This placeholder applet fills its area with sparse diagonal lines + hatchRegion(0, 0, width(), height(), 8, BLACK); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h index 78ba5cd89..b10147eb5 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h @@ -11,17 +11,15 @@ Fills the area with diagonal lines #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class PlaceholderApplet : public SystemApplet -{ - public: - void onRender() override; +class PlaceholderApplet : public SystemApplet { +public: + void onRender() override; - // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. - // The window manager decides when and where it should be rendered - // It may be drawn to several different tiles during an Renderer::render call + // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. + // The window manager decides when and where it should be rendered + // It may be drawn to several different tiles during an Renderer::render call }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index a9d579873..415118281 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -8,248 +8,240 @@ using namespace NicheGraphics; -InkHUD::TipsApplet::TipsApplet() -{ - // Decide which tips (if any) should be shown to user after the boot screen +InkHUD::TipsApplet::TipsApplet() { + // Decide which tips (if any) should be shown to user after the boot screen - // Welcome screen - if (settings->tips.firstBoot) - tipQueue.push_back(Tip::WELCOME); + // Welcome screen + if (settings->tips.firstBoot) + tipQueue.push_back(Tip::WELCOME); - // Antenna, region, timezone - // Shown at boot if region not yet set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) - tipQueue.push_back(Tip::FINISH_SETUP); + // Antenna, region, timezone + // Shown at boot if region not yet set + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + tipQueue.push_back(Tip::FINISH_SETUP); - // Shutdown info - // Shown until user performs one valid shutdown - if (!settings->tips.safeShutdownSeen) - tipQueue.push_back(Tip::SAFE_SHUTDOWN); + // Shutdown info + // Shown until user performs one valid shutdown + if (!settings->tips.safeShutdownSeen) + tipQueue.push_back(Tip::SAFE_SHUTDOWN); - // Using the UI - if (settings->tips.firstBoot) { - tipQueue.push_back(Tip::CUSTOMIZATION); - tipQueue.push_back(Tip::BUTTONS); - } + // Using the UI + if (settings->tips.firstBoot) { + tipQueue.push_back(Tip::CUSTOMIZATION); + tipQueue.push_back(Tip::BUTTONS); + } - // Catch an incorrect attempt at rotating display - if (config.display.flip_screen) - tipQueue.push_back(Tip::ROTATION); + // Catch an incorrect attempt at rotating display + if (config.display.flip_screen) + tipQueue.push_back(Tip::ROTATION); - // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground - // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector - if (!tipQueue.empty()) - bringToForeground(); + // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground + // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets + // vector + if (!tipQueue.empty()) + bringToForeground(); } -void InkHUD::TipsApplet::onRender() -{ - switch (tipQueue.front()) { - case Tip::WELCOME: - renderWelcome(); - break; +void InkHUD::TipsApplet::onRender() { + switch (tipQueue.front()) { + case Tip::WELCOME: + renderWelcome(); + break; - case Tip::FINISH_SETUP: { - setFont(fontMedium); - printAt(0, 0, "Tip: Finish Setup"); + case Tip::FINISH_SETUP: { + setFont(fontMedium); + printAt(0, 0, "Tip: Finish Setup"); - setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; - printAt(0, cursorY, "- connect antenna"); + setFont(fontSmall); + int16_t cursorY = fontMedium.lineHeight() * 1.5; + printAt(0, cursorY, "- connect antenna"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- connect a client app"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- connect a client app"); - // Only if region not set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set region"); - } - - // Only if tz not set - if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set timezone"); - } - - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "More info at meshtastic.org"); - - setFont(fontSmall); - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; - - case Tip::SAFE_SHUTDOWN: { - setFont(fontMedium); - printAt(0, 0, "Tip: Shutdown"); - - setFont(fontSmall); - std::string shutdown; - shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; - shutdown += "\n"; - shutdown += "This ensures data is saved."; - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - - } break; - - case Tip::CUSTOMIZATION: { - setFont(fontMedium); - printAt(0, 0, "Tip: Customization"); - - setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; - - case Tip::BUTTONS: { - setFont(fontMedium); - printAt(0, 0, "Tip: Buttons"); - - setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; - - if (!settings->joystick.enabled) { - printAt(0, cursorY, "User Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: next"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- long press: select / open menu"); - } else { - printAt(0, cursorY, "Joystick"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- open menu / select"); - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "Exit Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- switch tile / close menu"); - } - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; - - case Tip::ROTATION: { - setFont(fontMedium); - printAt(0, 0, "Tip: Rotation"); - - setFont(fontSmall); - if (!settings->joystick.enabled) { - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); - } else { - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); - } - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - - // Revert the "flip screen" setting, preventing this message showing again - config.display.flip_screen = false; - nodeDB->saveToDisk(SEGMENT_DEVICESTATE); - } break; + // Only if region not set + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- set region"); } + + // Only if tz not set + if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- set timezone"); + } + + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "More info at meshtastic.org"); + + setFont(fontSmall); + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::SAFE_SHUTDOWN: { + setFont(fontMedium); + printAt(0, 0, "Tip: Shutdown"); + + setFont(fontSmall); + std::string shutdown; + shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; + shutdown += "\n"; + shutdown += "This ensures data is saved."; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + + } break; + + case Tip::CUSTOMIZATION: { + setFont(fontMedium); + printAt(0, 0, "Tip: Customization"); + + setFont(fontSmall); + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::BUTTONS: { + setFont(fontMedium); + printAt(0, 0, "Tip: Buttons"); + + setFont(fontSmall); + int16_t cursorY = fontMedium.lineHeight() * 1.5; + + if (!settings->joystick.enabled) { + printAt(0, cursorY, "User Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- short press: next"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- long press: select / open menu"); + } else { + printAt(0, cursorY, "Joystick"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- open menu / select"); + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "Exit Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- switch tile / close menu"); + } + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::ROTATION: { + setFont(fontMedium); + printAt(0, 0, "Tip: Rotation"); + + setFont(fontSmall); + if (!settings->joystick.enabled) { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + } else { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); + } + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + + // Revert the "flip screen" setting, preventing this message showing again + config.display.flip_screen = false; + nodeDB->saveToDisk(SEGMENT_DEVICESTATE); + } break; + } } // This tip has its own render method, only because it's a big block of code // Didn't want to clutter up the switch in onRender too much -void InkHUD::TipsApplet::renderWelcome() -{ - uint16_t padW = X(0.05); +void InkHUD::TipsApplet::renderWelcome() { + uint16_t padW = X(0.05); - // Block 1 - logo & title - // ======================== + // Block 1 - logo & title + // ======================== - // Logo size - uint16_t logoWLimit = X(0.3); - uint16_t logoHLimit = Y(0.3); - uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); - uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); + // Logo size + uint16_t logoWLimit = X(0.3); + uint16_t logoHLimit = Y(0.3); + uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); + uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); - // Title size - setFont(fontMedium); - std::string title; - if (width() >= 200) // Future proofing: hide if *tiny* display - title = "meshtastic.org"; - uint16_t titleW = getTextWidth(title); + // Title size + setFont(fontMedium); + std::string title; + if (width() >= 200) // Future proofing: hide if *tiny* display + title = "meshtastic.org"; + uint16_t titleW = getTextWidth(title); - // Center the block - // Desired effect: equal margin from display edge for logo left and title right - int16_t block1Y = Y(0.3); - int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); - int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); - int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); + // Center the block + // Desired effect: equal margin from display edge for logo left and title right + int16_t block1Y = Y(0.3); + int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); + int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); + int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); - // Draw block - drawLogo(logoCX, block1Y, logoW, logoH); - printAt(titleCX, block1Y, title, CENTER, MIDDLE); + // Draw block + drawLogo(logoCX, block1Y, logoW, logoH); + printAt(titleCX, block1Y, title, CENTER, MIDDLE); - // Block 2 - subtitle - // ======================= - setFont(fontSmall); - std::string subtitle = "InkHUD"; - if (width() >= 200) - subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display - printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); + // Block 2 - subtitle + // ======================= + setFont(fontSmall); + std::string subtitle = "InkHUD"; + if (width() >= 200) + subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display + printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); - // Block 3 - press to continue - // ============================ - printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); + // Block 3 - press to continue + // ============================ + printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); } -void InkHUD::TipsApplet::onForeground() -{ - // Prevent most other applets from requesting update, and skip their rendering entirely - // Another system applet with a higher precedence can potentially ignore this - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; +void InkHUD::TipsApplet::onForeground() { + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; - SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first) + SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first) } -void InkHUD::TipsApplet::onBackground() -{ - // Allow normal update behavior to resume - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; +void InkHUD::TipsApplet::onBackground() { + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // While our SystemApplet::handleInput flag is true -void InkHUD::TipsApplet::onButtonShortPress() -{ - tipQueue.pop_front(); +void InkHUD::TipsApplet::onButtonShortPress() { + tipQueue.pop_front(); - // All tips done - if (tipQueue.empty()) { - // Record that user has now seen the "tutorial" set of tips - // Don't show them on subsequent boots - if (settings->tips.firstBoot) { - settings->tips.firstBoot = false; - inkhud->persistence->saveSettings(); - } - - // Close applet, and full refresh to clean the screen - // Need to force update, because our request would be ignored otherwise, as we are now background - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // All tips done + if (tipQueue.empty()) { + // Record that user has now seen the "tutorial" set of tips + // Don't show them on subsequent boots + if (settings->tips.firstBoot) { + settings->tips.firstBoot = false; + inkhud->persistence->saveSettings(); } - // More tips left - else - requestUpdate(); + // Close applet, and full refresh to clean the screen + // Need to force update, because our request would be ignored otherwise, as we are now background + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); + } + + // More tips left + else + requestUpdate(); } // Functions the same as the user button in this instance -void InkHUD::TipsApplet::onExitShort() -{ - onButtonShortPress(); -} +void InkHUD::TipsApplet::onExitShort() { onButtonShortPress(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index 159e6f58f..2ed6e234c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -14,36 +14,34 @@ #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class TipsApplet : public SystemApplet -{ - protected: - enum class Tip { - WELCOME, - FINISH_SETUP, - SAFE_SHUTDOWN, - CUSTOMIZATION, - BUTTONS, - ROTATION, - }; +class TipsApplet : public SystemApplet { +protected: + enum class Tip { + WELCOME, + FINISH_SETUP, + SAFE_SHUTDOWN, + CUSTOMIZATION, + BUTTONS, + ROTATION, + }; - public: - TipsApplet(); +public: + TipsApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onButtonShortPress() override; - void onExitShort() override; + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onExitShort() override; - protected: - void renderWelcome(); // Very first screen of tutorial +protected: + void renderWelcome(); // Very first screen of tutorial - std::deque tipQueue; // List of tips to show, one after another + std::deque tipQueue; // List of tips to show, one after another - WindowManager *windowManager = nullptr; // For convenience. Set in constructor. + WindowManager *windowManager = nullptr; // For convenience. Set in constructor. }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 7c6232f3b..2c5e8f78d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -4,136 +4,127 @@ using namespace NicheGraphics; -void InkHUD::AllMessageApplet::onActivate() -{ - textMessageObserver.observe(textMessageModule); -} +void InkHUD::AllMessageApplet::onActivate() { textMessageObserver.observe(textMessageModule); } -void InkHUD::AllMessageApplet::onDeactivate() -{ - textMessageObserver.unobserve(textMessageModule); -} +void InkHUD::AllMessageApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); } // We're not consuming the data passed to this method; // we're just just using it to trigger a render -int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) -{ - // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets - if (!isActive()) - return 0; - - // Abort if this is an outgoing message - if (getFrom(p) == nodeDB->getNodeNum()) - return 0; - - requestAutoshow(); // Want to become foreground, if permitted - requestUpdate(); // Want to update display, if applet is foreground - - // Return zero: no issues here, carry on notifying other observers! +int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + if (!isActive()) return 0; + + // Abort if this is an outgoing message + if (getFrom(p) == nodeDB->getNodeNum()) + return 0; + + requestAutoshow(); // Want to become foreground, if permitted + requestUpdate(); // Want to update display, if applet is foreground + + // Return zero: no issues here, carry on notifying other observers! + return 0; } -void InkHUD::AllMessageApplet::onRender() -{ - // Find newest message, regardless of whether DM or broadcast - MessageStore::Message *message; - if (latestMessage->wasBroadcast) - message = &latestMessage->broadcast; - else - message = &latestMessage->dm; +void InkHUD::AllMessageApplet::onRender() { + // Find newest message, regardless of whether DM or broadcast + MessageStore::Message *message; + if (latestMessage->wasBroadcast) + message = &latestMessage->broadcast; + else + message = &latestMessage->dm; - // Short circuit: no text message - if (!message->sender) { - printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE); - return; - } + // Short circuit: no text message + if (!message->sender) { + printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE); + return; + } - // =========================== - // Header (sender, timestamp) - // =========================== + // =========================== + // Header (sender, timestamp) + // =========================== - // Y position for divider - // - between header text and messages + // Y position for divider + // - between header text and messages - std::string header; + std::string header; - // RX Time - // - if valid - std::string timeString = getTimeString(message->timestamp); - if (timeString.length() > 0) { - header += timeString; - header += ": "; - } + // RX Time + // - if valid + std::string timeString = getTimeString(message->timestamp); + if (timeString.length() > 0) { + header += timeString; + header += ": "; + } - // Sender's id - // - short name and long name, if available, or - // - node id - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); - if (sender && sender->has_user) { - header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) - header += " ("; - header += parse(sender->user.long_name); - header += ")"; - } else - header += hexifyNodeNum(message->sender); + // Sender's id + // - short name and long name, if available, or + // - node id + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); + if (sender && sender->has_user) { + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) + header += " ("; + header += parse(sender->user.long_name); + header += ")"; + } else + header += hexifyNodeNum(message->sender); - // Draw a "standard" applet header - drawHeader(header); + // Draw a "standard" applet header + drawHeader(header); - // Fade the right edge of the header, if text spills over edge - uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect - uint8_t hF = getHeaderHeight(); // Height of fade effect - if (getCursorX() > width()) - hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); + // Fade the right edge of the header, if text spills over edge + uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect + uint8_t hF = getHeaderHeight(); // Height of fade effect + if (getCursorX() > width()) + hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); - // Dimensions of the header - constexpr int16_t padDivH = 2; - const int16_t headerDivY = Applet::getHeaderHeight() - 1; + // Dimensions of the header + constexpr int16_t padDivH = 2; + const int16_t headerDivY = Applet::getHeaderHeight() - 1; - // =================== - // Print message text - // =================== + // =================== + // Print message text + // =================== - // Parse any non-ascii chars in the message - std::string text = parse(message->text); + // Parse any non-ascii chars in the message + std::string text = parse(message->text); - // Extra gap below the header - int16_t textTop = headerDivY + padDivH; + // Extra gap below the header + int16_t textTop = headerDivY + padDivH; - // Attempt to print with fontLarge - uint32_t textHeight; - setFont(fontLarge); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { - printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): attempt to print with fontMedium - setFont(fontMedium); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { - printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): print with fontSmall - setFont(fontSmall); + // Attempt to print with fontLarge + uint32_t textHeight; + setFont(fontLarge); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): attempt to print with fontMedium + setFont(fontMedium); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): print with fontSmall + setFont(fontSmall); + printWrapped(0, textTop, width(), text); } // Don't show notifications for text messages when our applet is displayed -bool InkHUD::AllMessageApplet::approveNotification(Notification &n) -{ - if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST) - return false; +bool InkHUD::AllMessageApplet::approveNotification(Notification &n) { + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST) + return false; - else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) - return false; + else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) + return false; - else - return true; + else + return true; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h index c74e16196..71ad56d00 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h @@ -6,8 +6,9 @@ Shows the latest incoming text message, as well as sender. Both broadcast and direct messages will be shown here, from all channels. This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages -This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. -This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage +This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text +message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via +InkHUD::latestMessage We do still receive notifications from the text message module though, to know when a new message has arrived, and trigger the update. @@ -22,26 +23,24 @@ to know when a new message has arrived, and trigger the update. #include "modules/TextMessageModule.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { class Applet; -class AllMessageApplet : public Applet -{ - public: - void onRender() override; +class AllMessageApplet : public Applet { +public: + void onRender() override; - void onActivate() override; - void onDeactivate() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + void onActivate() override; + void onDeactivate() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); - bool approveNotification(Notification &n) override; // Which notifications to suppress + bool approveNotification(Notification &n) override; // Which notifications to suppress - protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, &AllMessageApplet::onReceiveTextMessage); +protected: + // Used to register our text message callback + CallbackObserver textMessageObserver = + CallbackObserver(this, &AllMessageApplet::onReceiveTextMessage); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index a3b9615a5..4e4d0cb43 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -4,129 +4,120 @@ using namespace NicheGraphics; -void InkHUD::DMApplet::onActivate() -{ - textMessageObserver.observe(textMessageModule); -} +void InkHUD::DMApplet::onActivate() { textMessageObserver.observe(textMessageModule); } -void InkHUD::DMApplet::onDeactivate() -{ - textMessageObserver.unobserve(textMessageModule); -} +void InkHUD::DMApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); } // We're not consuming the data passed to this method; // we're just just using it to trigger a render -int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) -{ - // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets - if (!isActive()) - return 0; - - // If DM (not broadcast) - if (!isBroadcast(p->to)) { - // Want to update display, if applet is foreground - requestUpdate(); - - // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(p) != nodeDB->getNodeNum()) - requestAutoshow(); - } - - // Return zero: no issues here, carry on notifying other observers! +int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + if (!isActive()) return 0; + + // If DM (not broadcast) + if (!isBroadcast(p->to)) { + // Want to update display, if applet is foreground + requestUpdate(); + + // If this was an incoming message, suggest that our applet becomes foreground, if permitted + if (getFrom(p) != nodeDB->getNodeNum()) + requestAutoshow(); + } + + // Return zero: no issues here, carry on notifying other observers! + return 0; } -void InkHUD::DMApplet::onRender() -{ - // Abort if no text message - if (!latestMessage->dm.sender) { - printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE); - return; - } +void InkHUD::DMApplet::onRender() { + // Abort if no text message + if (!latestMessage->dm.sender) { + printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE); + return; + } - // =========================== - // Header (sender, timestamp) - // =========================== + // =========================== + // Header (sender, timestamp) + // =========================== - // Y position for divider - // - between header text and messages + // Y position for divider + // - between header text and messages - std::string header; + std::string header; - // RX Time - // - if valid - std::string timeString = getTimeString(latestMessage->dm.timestamp); - if (timeString.length() > 0) { - header += timeString; - header += ": "; - } + // RX Time + // - if valid + std::string timeString = getTimeString(latestMessage->dm.timestamp); + if (timeString.length() > 0) { + header += timeString; + header += ": "; + } - // Sender's id - // - shortname and long name, if available, or - // - node id - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); - if (sender && sender->has_user) { - header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) - header += " ("; - header += parse(sender->user.long_name); - header += ")"; - } else - header += hexifyNodeNum(latestMessage->dm.sender); + // Sender's id + // - shortname and long name, if available, or + // - node id + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); + if (sender && sender->has_user) { + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) + header += " ("; + header += parse(sender->user.long_name); + header += ")"; + } else + header += hexifyNodeNum(latestMessage->dm.sender); - // Draw a "standard" applet header - drawHeader(header); + // Draw a "standard" applet header + drawHeader(header); - // Fade the right edge of the header, if text spills over edge - uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect - uint8_t hF = getHeaderHeight(); // Height of fade effect - if (getCursorX() > width()) - hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); + // Fade the right edge of the header, if text spills over edge + uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect + uint8_t hF = getHeaderHeight(); // Height of fade effect + if (getCursorX() > width()) + hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); - // Dimensions of the header - constexpr int16_t padDivH = 2; - const int16_t headerDivY = Applet::getHeaderHeight() - 1; + // Dimensions of the header + constexpr int16_t padDivH = 2; + const int16_t headerDivY = Applet::getHeaderHeight() - 1; - // =================== - // Print message text - // =================== + // =================== + // Print message text + // =================== - // Parse any non-ascii chars in the message - std::string text = parse(latestMessage->dm.text); + // Parse any non-ascii chars in the message + std::string text = parse(latestMessage->dm.text); - // Extra gap below the header - int16_t textTop = headerDivY + padDivH; + // Extra gap below the header + int16_t textTop = headerDivY + padDivH; - // Attempt to print with fontLarge - uint32_t textHeight; - setFont(fontLarge); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { - printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): attempt to print with fontMedium - setFont(fontMedium); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { - printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): print with fontSmall - setFont(fontSmall); + // Attempt to print with fontLarge + uint32_t textHeight; + setFont(fontLarge); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): attempt to print with fontMedium + setFont(fontMedium); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): print with fontSmall + setFont(fontSmall); + printWrapped(0, textTop, width(), text); } // Don't show notifications for direct messages when our applet is displayed -bool InkHUD::DMApplet::approveNotification(Notification &n) -{ - if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) - return false; +bool InkHUD::DMApplet::approveNotification(Notification &n) { + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) + return false; - else - return true; + else + return true; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h index b3dc36e66..9da61598d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h @@ -6,8 +6,9 @@ Shows the latest incoming *Direct Message* (DM), as well as sender. This compliments the threaded message applets This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages -This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. -This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage +This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text +message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via +InkHUD::latestMessage We do still receive notifications from the text message module though, to know when a new message has arrived, and trigger the update. @@ -22,26 +23,24 @@ to know when a new message has arrived, and trigger the update. #include "modules/TextMessageModule.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { class Applet; -class DMApplet : public Applet -{ - public: - void onRender() override; +class DMApplet : public Applet { +public: + void onRender() override; - void onActivate() override; - void onDeactivate() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + void onActivate() override; + void onDeactivate() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); - bool approveNotification(Notification &n) override; // Which notifications to suppress + bool approveNotification(Notification &n) override; // Which notifications to suppress - protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, &DMApplet::onReceiveTextMessage); +protected: + // Used to register our text message callback + CallbackObserver textMessageObserver = + CallbackObserver(this, &DMApplet::onReceiveTextMessage); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp index 5a659c606..2255c2e7b 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -8,117 +8,112 @@ using namespace NicheGraphics; -void InkHUD::HeardApplet::onActivate() -{ - // When applet begins, pre-fill with stale info from NodeDB - populateFromNodeDB(); +void InkHUD::HeardApplet::onActivate() { + // When applet begins, pre-fill with stale info from NodeDB + populateFromNodeDB(); } -void InkHUD::HeardApplet::onDeactivate() -{ - // Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB - cards.clear(); +void InkHUD::HeardApplet::onDeactivate() { + // Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB + cards.clear(); } // When base applet hears a new packet, it extracts the info and passes it to us as CardInfo // We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result -void InkHUD::HeardApplet::handleParsed(CardInfo c) -{ - // Grab the previous entry. - // To check if the new data is different enough to justify re-render - // Need to cache now, before we manipulate the deque - CardInfo previous; - if (!cards.empty()) - previous = cards.at(0); +void InkHUD::HeardApplet::handleParsed(CardInfo c) { + // Grab the previous entry. + // To check if the new data is different enough to justify re-render + // Need to cache now, before we manipulate the deque + CardInfo previous; + if (!cards.empty()) + previous = cards.at(0); - // If we're updating an existing entry, remove the old one. Will reinsert at front - for (auto it = cards.begin(); it != cards.end(); ++it) { - if (it->nodeNum == c.nodeNum) { - cards.erase(it); - break; - } + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = cards.begin(); it != cards.end(); ++it) { + if (it->nodeNum == c.nodeNum) { + cards.erase(it); + break; } + } - cards.push_front(c); // Insert into base class' card collection - cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen - cards.shrink_to_fit(); + cards.push_front(c); // Insert into base class' card collection + cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen + cards.shrink_to_fit(); - // Our rendered image needs to change if: - if (previous.nodeNum != c.nodeNum // Different node - || previous.signal != c.signal // or different signal strength - || previous.distanceMeters != c.distanceMeters // or different position - || previous.hopsAway != c.hopsAway) // or different hops away - { - requestAutoshow(); - requestUpdate(); - } + // Our rendered image needs to change if: + if (previous.nodeNum != c.nodeNum // Different node + || previous.signal != c.signal // or different signal strength + || previous.distanceMeters != c.distanceMeters // or different position + || previous.hopsAway != c.hopsAway) // or different hops away + { + requestAutoshow(); + requestUpdate(); + } } // When applet is activated, pre-fill with stale data from NodeDB // We're sorting using the last_heard value. Susceptible to weirdness if node's RTC changes. // No SNR is available in node db, so we can't calculate signal either -// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead -void InkHUD::HeardApplet::populateFromNodeDB() -{ - // Fill a collection with pointers to each node in db - std::vector ordered; - for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) { - // Only copy if valid, and not our own node - if (mn->num != 0 && mn->num != nodeDB->getNodeNum()) - ordered.push_back(&*mn); +// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet +// instead +void InkHUD::HeardApplet::populateFromNodeDB() { + // Fill a collection with pointers to each node in db + std::vector ordered; + for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) { + // Only copy if valid, and not our own node + if (mn->num != 0 && mn->num != nodeDB->getNodeNum()) + ordered.push_back(&*mn); + } + + // Sort the collection by age + std::sort(ordered.begin(), ordered.end(), + [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { return (top->last_heard > bottom->last_heard); }); + + // Keep the most recent entries only + // Just enough to fill the screen + if (ordered.size() > maxCards()) + ordered.resize(maxCards()); + + // Create card info for these (stale) node observations + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + for (meshtastic_NodeInfoLite *node : ordered) { + CardInfo c; + c.nodeNum = node->num; + + if (node->has_hops_away) + c.hopsAway = node->hops_away; + + if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { + // Get lat and long as float + // Meshtastic stores these as integers internally + float ourLat = ourNode->position.latitude_i * 1e-7; + float ourLong = ourNode->position.longitude_i * 1e-7; + float theirLat = node->position.latitude_i * 1e-7; + float theirLong = node->position.longitude_i * 1e-7; + + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); } - // Sort the collection by age - std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { - return (top->last_heard > bottom->last_heard); - }); - - // Keep the most recent entries only - // Just enough to fill the screen - if (ordered.size() > maxCards()) - ordered.resize(maxCards()); - - // Create card info for these (stale) node observations - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - for (meshtastic_NodeInfoLite *node : ordered) { - CardInfo c; - c.nodeNum = node->num; - - if (node->has_hops_away) - c.hopsAway = node->hops_away; - - if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { - // Get lat and long as float - // Meshtastic stores these as integers internally - float ourLat = ourNode->position.latitude_i * 1e-7; - float ourLong = ourNode->position.longitude_i * 1e-7; - float theirLat = node->position.latitude_i * 1e-7; - float theirLong = node->position.longitude_i * 1e-7; - - c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); - } - - // Insert into the card collection (member of base class) - cards.push_back(c); - } + // Insert into the card collection (member of base class) + cards.push_back(c); + } } // Text drawn in the usual applet header // Handled by base class: ChronoListApplet -std::string InkHUD::HeardApplet::getHeaderText() -{ - uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node +std::string InkHUD::HeardApplet::getHeaderText() { + uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node - std::string text = "Heard: "; + std::string text = "Heard: "; - // Print node count, if nodeDB not yet nearing full - if (nodeCount < MAX_NUM_NODES) { - text += to_string(nodeCount); // Max nodes - text += " "; - text += (nodeCount == 1) ? "node" : "nodes"; - } + // Print node count, if nodeDB not yet nearing full + if (nodeCount < MAX_NUM_NODES) { + text += to_string(nodeCount); // Max nodes + text += " "; + text += (nodeCount == 1) ? "node" : "nodes"; + } - return text; + return text; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h index 932b5a75e..fbdaca33c 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h @@ -13,21 +13,19 @@ Most of the work is done by the InkHUD::NodeListApplet base class #include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class HeardApplet : public NodeListApplet -{ - public: - HeardApplet() : NodeListApplet("HeardApplet") {} - void onActivate() override; - void onDeactivate() override; +class HeardApplet : public NodeListApplet { +public: + HeardApplet() : NodeListApplet("HeardApplet") {} + void onActivate() override; + void onDeactivate() override; - protected: - void handleParsed(CardInfo c) override; // Store new info, and update display if needed - std::string getHeaderText() override; // Set title for this applet +protected: + void handleParsed(CardInfo c) override; // Store new info, and update display if needed + std::string getHeaderText() override; // Set title for this applet - void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB + void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp index ad0f9fc47..af14a8426 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -5,107 +5,105 @@ using namespace NicheGraphics; -void InkHUD::PositionsApplet::onRender() -{ - // Draw the usual map applet first - MapApplet::onRender(); +void InkHUD::PositionsApplet::onRender() { + // Draw the usual map applet first + MapApplet::onRender(); - // Draw our latest "node of interest" as a special marker - // ------------------------------------------------------- - // We might be rendering because we got a position packet from them - // We might be rendering because our own position updated - // Either way, we still highlight which node most recently sent us a position packet - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); - if (node && nodeDB->hasValidPosition(node) && enoughMarkers()) - drawLabeledMarker(node); + // Draw our latest "node of interest" as a special marker + // ------------------------------------------------------- + // We might be rendering because we got a position packet from them + // We might be rendering because our own position updated + // Either way, we still highlight which node most recently sent us a position packet + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); + if (node && nodeDB->hasValidPosition(node) && enoughMarkers()) + drawLabeledMarker(node); } // Determine if we need to redraw the map, when we receive a new position packet -ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) -{ - // If applet is not active, we shouldn't be handling any data - // It's good practice for all applets to implement an early return like this - // for PositionsApplet, this is **required** - it's where we're handling active vs deactive - if (!isActive()) - return ProcessMessage::CONTINUE; - - // Try decode a position from the packet - bool hasPosition = false; - float lat; - float lng; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { - meshtastic_Position position = meshtastic_Position_init_default; - if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { - if (position.has_latitude_i && position.has_longitude_i // Actually has position - && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" - { - hasPosition = true; - lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format - lng = position.longitude_i * 1e-7; - } - } - } - - // Skip if we didn't get a valid position - if (!hasPosition) - return ProcessMessage::CONTINUE; - - const int8_t hopsAway = getHopsAway(mp); - const bool hasHopsAway = hopsAway >= 0; - - // Determine if the position packet would change anything on-screen - // ----------------------------------------------------------------- - - bool somethingChanged = false; - - // If our own position - if (isFromUs(&mp)) { - // We get frequent position updates from connected phone - // Only update if we're travelled some distance, for rate limiting - // Todo: smarter detection of position changes - if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { - somethingChanged = true; - ourLastLat = lat; - ourLastLng = lng; - } - } - - // If someone else's position - else { - // Check if this position is from someone different than our previous position packet - if (mp.from != lastFrom) { - somethingChanged = true; - lastFrom = mp.from; - lastLat = lat; - lastLng = lng; - lastHopsAway = hopsAway; - } - - // Same sender: check if position changed - // Todo: smarter detection of position changes - else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { - somethingChanged = true; - lastLat = lat; - lastLng = lng; - } - - // Same sender, same position: check if hops changed - // Only pay attention if the hopsAway value is valid - else if (hasHopsAway && (hopsAway != lastHopsAway)) { - somethingChanged = true; - lastHopsAway = hopsAway; - } - } - - // Decision reached - // ----------------- - - if (somethingChanged) { - requestAutoshow(); // Todo: only request this in some situations? - requestUpdate(); - } - +ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) { + // If applet is not active, we shouldn't be handling any data + // It's good practice for all applets to implement an early return like this + // for PositionsApplet, this is **required** - it's where we're handling active vs deactive + if (!isActive()) return ProcessMessage::CONTINUE; + + // Try decode a position from the packet + bool hasPosition = false; + float lat; + float lng; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position position = meshtastic_Position_init_default; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { + if (position.has_latitude_i && position.has_longitude_i // Actually has position + && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" + { + hasPosition = true; + lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format + lng = position.longitude_i * 1e-7; + } + } + } + + // Skip if we didn't get a valid position + if (!hasPosition) + return ProcessMessage::CONTINUE; + + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; + + // Determine if the position packet would change anything on-screen + // ----------------------------------------------------------------- + + bool somethingChanged = false; + + // If our own position + if (isFromUs(&mp)) { + // We get frequent position updates from connected phone + // Only update if we're travelled some distance, for rate limiting + // Todo: smarter detection of position changes + if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { + somethingChanged = true; + ourLastLat = lat; + ourLastLng = lng; + } + } + + // If someone else's position + else { + // Check if this position is from someone different than our previous position packet + if (mp.from != lastFrom) { + somethingChanged = true; + lastFrom = mp.from; + lastLat = lat; + lastLng = lng; + lastHopsAway = hopsAway; + } + + // Same sender: check if position changed + // Todo: smarter detection of position changes + else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { + somethingChanged = true; + lastLat = lat; + lastLng = lng; + } + + // Same sender, same position: check if hops changed + // Only pay attention if the hopsAway value is valid + else if (hasHopsAway && (hopsAway != lastHopsAway)) { + somethingChanged = true; + lastHopsAway = hopsAway; + } + } + + // Decision reached + // ----------------- + + if (somethingChanged) { + requestAutoshow(); // Todo: only request this in some situations? + requestUpdate(); + } + + return ProcessMessage::CONTINUE; } #endif diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h index 28a53cb0f..74278f7dd 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h @@ -17,25 +17,23 @@ The node which has most recently sent a position will be labeled. #include "SinglePortModule.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class PositionsApplet : public MapApplet, public SinglePortModule -{ - public: - PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} - void onRender() override; +class PositionsApplet : public MapApplet, public SinglePortModule { +public: + PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} + void onRender() override; - protected: - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; +protected: + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet - float lastLat = 0.0; - float lastLng = 0.0; - float lastHopsAway = 0; + NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet + float lastLat = 0.0; + float lastLng = 0.0; + float lastHopsAway = 0; - float ourLastLat = 0.0; // Info about the most recent (non-local) position packet - float ourLastLng = 0.0; // Info about most recent *local* position + float ourLastLat = 0.0; // Info about the most recent (non-local) position packet + float ourLastLng = 0.0; // Info about most recent *local* position }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp index 1ccf7fc14..18687ef16 100644 --- a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp @@ -6,148 +6,140 @@ using namespace NicheGraphics; -InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") -{ - // No scheduled tasks initially - OSThread::disable(); +InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") { + // No scheduled tasks initially + OSThread::disable(); } -void InkHUD::RecentsListApplet::onActivate() -{ - // When the applet is activated, begin scheduled purging of any nodes which are no longer "active" - OSThread::enabled = true; - OSThread::setIntervalFromNow(60 * 1000UL); // Every minute +void InkHUD::RecentsListApplet::onActivate() { + // When the applet is activated, begin scheduled purging of any nodes which are no longer "active" + OSThread::enabled = true; + OSThread::setIntervalFromNow(60 * 1000UL); // Every minute } -void InkHUD::RecentsListApplet::onDeactivate() -{ - // Halt scheduled purging - OSThread::disable(); +void InkHUD::RecentsListApplet::onDeactivate() { + // Halt scheduled purging + OSThread::disable(); } -int32_t InkHUD::RecentsListApplet::runOnce() -{ - prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently - return OSThread::interval; +int32_t InkHUD::RecentsListApplet::runOnce() { + prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently + return OSThread::interval; } // When base applet hears a new packet, it extracts the info and passes it to us as CardInfo // We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result // We also need to record the current time against the nodenum, so we know when it becomes inactive -void InkHUD::RecentsListApplet::handleParsed(CardInfo c) -{ - // Grab the previous entry. - // To check if the new data is different enough to justify re-render - // Need to cache now, before we manipulate the deque - CardInfo previous; - if (!cards.empty()) - previous = cards.at(0); +void InkHUD::RecentsListApplet::handleParsed(CardInfo c) { + // Grab the previous entry. + // To check if the new data is different enough to justify re-render + // Need to cache now, before we manipulate the deque + CardInfo previous; + if (!cards.empty()) + previous = cards.at(0); - // If we're updating an existing entry, remove the old one. Will reinsert at front - for (auto it = cards.begin(); it != cards.end(); ++it) { - if (it->nodeNum == c.nodeNum) { - cards.erase(it); - break; - } + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = cards.begin(); it != cards.end(); ++it) { + if (it->nodeNum == c.nodeNum) { + cards.erase(it); + break; } + } - cards.push_front(c); // Store this CardInfo - cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen - cards.shrink_to_fit(); + cards.push_front(c); // Store this CardInfo + cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen + cards.shrink_to_fit(); - // Record the time of this observation - // Used to count active nodes, and to know when to prune inactive nodes - seenNow(c.nodeNum); + // Record the time of this observation + // Used to count active nodes, and to know when to prune inactive nodes + seenNow(c.nodeNum); - // Our rendered image needs to change if: - if (previous.nodeNum != c.nodeNum // Different node - || previous.signal != c.signal // or different signal strength - || previous.distanceMeters != c.distanceMeters // or different position - || previous.hopsAway != c.hopsAway) // or different hops away - { - prune(); // Take the opportunity now to remove inactive nodes - requestAutoshow(); - requestUpdate(); - } + // Our rendered image needs to change if: + if (previous.nodeNum != c.nodeNum // Different node + || previous.signal != c.signal // or different signal strength + || previous.distanceMeters != c.distanceMeters // or different position + || previous.hopsAway != c.hopsAway) // or different hops away + { + prune(); // Take the opportunity now to remove inactive nodes + requestAutoshow(); + requestUpdate(); + } } // Record the time (millis, right now) that we hear a node -// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly -void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) -{ - // If we're updating an existing entry, remove the old one. Will reinsert at front - for (auto it = ages.begin(); it != ages.end(); ++it) { - if (it->nodeNum == nodeNum) { - ages.erase(it); - break; - } +// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs +// regularly +void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) { + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = ages.begin(); it != ages.end(); ++it) { + if (it->nodeNum == nodeNum) { + ages.erase(it); + break; } + } - Age a; - a.nodeNum = nodeNum; - a.seenAtMs = millis(); + Age a; + a.nodeNum = nodeNum; + a.seenAtMs = millis(); - ages.push_front(a); + ages.push_front(a); } // Remove Card and Age info for any nodes which are now inactive // Determined by when a node was last heard, in our internal record (not from nodeDB) -void InkHUD::RecentsListApplet::prune() -{ - // Iterate age records from newest to oldest - for (uint16_t i = 0; i < ages.size(); i++) { - // Found the first record which is too old - if (!isActive(ages.at(i).seenAtMs)) { - // Drop this item, and all others behind it - ages.resize(i); - ages.shrink_to_fit(); - cards.resize(i); - cards.shrink_to_fit(); +void InkHUD::RecentsListApplet::prune() { + // Iterate age records from newest to oldest + for (uint16_t i = 0; i < ages.size(); i++) { + // Found the first record which is too old + if (!isActive(ages.at(i).seenAtMs)) { + // Drop this item, and all others behind it + ages.resize(i); + ages.shrink_to_fit(); + cards.resize(i); + cards.shrink_to_fit(); - // Request an update, if pruning did modify our data - // Required if pruning was scheduled. Redundant if pruning was prior to rendering. - requestAutoshow(); - requestUpdate(); + // Request an update, if pruning did modify our data + // Required if pruning was scheduled. Redundant if pruning was prior to rendering. + requestAutoshow(); + requestUpdate(); - break; - } + break; } + } - // Push next scheduled pruning back - // Pruning may be called from by handleParsed, immediately prior to rendering - // In that case, we can slightly delay our scheduled pruning - OSThread::setIntervalFromNow(60 * 1000UL); + // Push next scheduled pruning back + // Pruning may be called from by handleParsed, immediately prior to rendering + // In that case, we can slightly delay our scheduled pruning + OSThread::setIntervalFromNow(60 * 1000UL); } // Is a timestamp old enough that it would make a node inactive, and in need of purging? -bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) -{ - uint32_t now = millis(); - uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe +bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) { + uint32_t now = millis(); + uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe - return (secsAgo < settings->recentlyActiveSeconds); + return (secsAgo < settings->recentlyActiveSeconds); } // Text to be shown at top of applet // ChronoListApplet base class allows us to set this dynamically // Might want to adjust depending on node count, RTC status, etc -std::string InkHUD::RecentsListApplet::getHeaderText() -{ - std::string text; +std::string InkHUD::RecentsListApplet::getHeaderText() { + std::string text; - // Print the length of our "Recents" time-window - text += "Last "; - text += to_string(settings->recentlyActiveSeconds / 60); - text += " mins"; + // Print the length of our "Recents" time-window + text += "Last "; + text += to_string(settings->recentlyActiveSeconds / 60); + text += " mins"; - // Print the node count - const uint16_t nodeCount = ages.size(); - text += ": "; - text += to_string(nodeCount); - text += " "; - text += (nodeCount == 1) ? "node" : "nodes"; + // Print the node count + const uint16_t nodeCount = ages.size(); + text += ": "; + text += to_string(nodeCount); + text += " "; + text += (nodeCount == 1) ? "node" : "nodes"; - return text; + return text; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h index 74f5f3e57..12a4d24f1 100644 --- a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h @@ -15,36 +15,34 @@ Most of the work is done by the shared InkHUD::NodeListApplet base class #include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class RecentsListApplet : public NodeListApplet, public concurrency::OSThread -{ - protected: - // Used internally to count the number of active nodes - // We count for ourselves, instead of using the value provided by NodeDB, - // as the values occasionally differ, due to the timing of our Applet's purge method - struct Age { - uint32_t nodeNum; - uint32_t seenAtMs; - }; +class RecentsListApplet : public NodeListApplet, public concurrency::OSThread { +protected: + // Used internally to count the number of active nodes + // We count for ourselves, instead of using the value provided by NodeDB, + // as the values occasionally differ, due to the timing of our Applet's purge method + struct Age { + uint32_t nodeNum; + uint32_t seenAtMs; + }; - public: - RecentsListApplet(); - void onActivate() override; - void onDeactivate() override; +public: + RecentsListApplet(); + void onActivate() override; + void onDeactivate() override; - protected: - int32_t runOnce() override; +protected: + int32_t runOnce() override; - void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed - std::string getHeaderText() override; // Set title for this applet + void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed + std::string getHeaderText() override; // Set title for this applet - void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count - void prune(); // Remove cards for nodes which we haven't seen recently - bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it? + void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count + void prune(); // Remove cards for nodes which we haven't seen recently + bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it? - std::deque ages; // Information about when we last heard nodes. Independent of NodeDB + std::deque ages; // Information about when we last heard nodes. Independent of NodeDB }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index fdb5a168d..478904b8a 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -14,255 +14,246 @@ constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) - : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) -{ - // Create the message store - // Will shortly attempt to load messages from RAM, if applet is active - // Label (filename in flash) is set from channel index - store = new MessageStore("ch" + to_string(channelIndex)); + : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { + // Create the message store + // Will shortly attempt to load messages from RAM, if applet is active + // Label (filename in flash) is set from channel index + store = new MessageStore("ch" + to_string(channelIndex)); } -void InkHUD::ThreadedMessageApplet::onRender() -{ - // ============= - // Draw a header - // ============= +void InkHUD::ThreadedMessageApplet::onRender() { + // ============= + // Draw a header + // ============= - // Header text - std::string headerText; - headerText += "Channel "; - headerText += to_string(channelIndex); - headerText += ": "; - if (channels.isDefaultChannel(channelIndex)) - headerText += "Public"; - else - headerText += channels.getByIndex(channelIndex).settings.name; + // Header text + std::string headerText; + headerText += "Channel "; + headerText += to_string(channelIndex); + headerText += ": "; + if (channels.isDefaultChannel(channelIndex)) + headerText += "Public"; + else + headerText += channels.getByIndex(channelIndex).settings.name; - // Draw a "standard" applet header - drawHeader(headerText); + // Draw a "standard" applet header + drawHeader(headerText); - // Y position for divider - const int16_t dividerY = Applet::getHeaderHeight() - 1; + // Y position for divider + const int16_t dividerY = Applet::getHeaderHeight() - 1; - // ================== - // Draw each message - // ================== + // ================== + // Draw each message + // ================== - // Restrict drawing area - // - don't overdraw the header - // - small gap below divider - setCrop(0, dividerY + 2, width(), height() - (dividerY + 2)); + // Restrict drawing area + // - don't overdraw the header + // - small gap below divider + setCrop(0, dividerY + 2, width(), height() - (dividerY + 2)); - // Set padding - // - separates text from the vertical line which marks its edge - constexpr uint16_t padW = 2; - constexpr int16_t msgL = padW; - const int16_t msgR = (width() - 1) - padW; - const uint16_t msgW = (msgR - msgL) + 1; + // Set padding + // - separates text from the vertical line which marks its edge + constexpr uint16_t padW = 2; + constexpr int16_t msgL = padW; + const int16_t msgR = (width() - 1) - padW; + const uint16_t msgW = (msgR - msgL) + 1; - int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. - uint8_t i = 0; // Index of stored message + int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. + uint8_t i = 0; // Index of stored message - // Loop over messages - // - until no messages left, or - // - until no part of message fits on screen - while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { + // Loop over messages + // - until no messages left, or + // - until no part of message fits on screen + while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { - // Grab data for message - MessageStore::Message &m = store->messages.at(i); - bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message - std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message + // Grab data for message + MessageStore::Message &m = store->messages.at(i); + bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message + std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message - // Cache bottom Y of message text - // - Used when drawing vertical line alongside - const int16_t dotsB = msgB; + // Cache bottom Y of message text + // - Used when drawing vertical line alongside + const int16_t dotsB = msgB; - // Get dimensions for message text - uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText); - int16_t bodyT = msgB - bodyH; + // Get dimensions for message text + uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText); + int16_t bodyT = msgB - bodyH; - // Print message - // - if incoming - if (!outgoing) - printWrapped(msgL, bodyT, msgW, bodyText); + // Print message + // - if incoming + if (!outgoing) + printWrapped(msgL, bodyT, msgW, bodyText); - // Print message - // - if outgoing - else { - if (getTextWidth(bodyText) < width()) // If short, - printAt(msgR, bodyT, bodyText, RIGHT); // print right align - else // If long, - printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align - } + // Print message + // - if outgoing + else { + if (getTextWidth(bodyText) < width()) // If short, + printAt(msgR, bodyT, bodyText, RIGHT); // print right align + else // If long, + printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align + } - // Move cursor up - // - above message text - msgB -= bodyH; - msgB -= getFont().lineHeight() * 0.2; // Padding between message and header + // Move cursor up + // - above message text + msgB -= bodyH; + msgB -= getFont().lineHeight() * 0.2; // Padding between message and header - // Compose info string - // - shortname, if possible, or "me" - // - time received, if possible - std::string info; - if (outgoing) - info += "Me"; - else { - // Check if sender is node db - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); - if (sender) - info += parseShortName(sender); // Handle any unprintable chars in short name - else - info += hexifyNodeNum(m.sender); // No node info at all. Print the node num - } + // Compose info string + // - shortname, if possible, or "me" + // - time received, if possible + std::string info; + if (outgoing) + info += "Me"; + else { + // Check if sender is node db + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); + if (sender) + info += parseShortName(sender); // Handle any unprintable chars in short name + else + info += hexifyNodeNum(m.sender); // No node info at all. Print the node num + } - std::string timeString = getTimeString(m.timestamp); - if (timeString.length() > 0) { - info += " - "; - info += timeString; - } + std::string timeString = getTimeString(m.timestamp); + if (timeString.length() > 0) { + info += " - "; + info += timeString; + } - // Print the info string - // - Faux bold: printed twice, shifted horizontally by one px - printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); - printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); + // Print the info string + // - Faux bold: printed twice, shifted horizontally by one px + printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); + printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); - // Underline the info string - const int16_t divY = msgB; - int16_t divL; - int16_t divR; - if (!outgoing) { - // Left side - incoming - divL = msgL; - divR = getTextWidth(info) + getFont().lineHeight() / 2; - } else { - // Right side - outgoing - divR = msgR; - divL = divR - getTextWidth(info) - getFont().lineHeight() / 2; - } - for (int16_t x = divL; x <= divR; x += 2) - drawPixel(x, divY, BLACK); + // Underline the info string + const int16_t divY = msgB; + int16_t divL; + int16_t divR; + if (!outgoing) { + // Left side - incoming + divL = msgL; + divR = getTextWidth(info) + getFont().lineHeight() / 2; + } else { + // Right side - outgoing + divR = msgR; + divL = divR - getTextWidth(info) - getFont().lineHeight() / 2; + } + for (int16_t x = divL; x <= divR; x += 2) + drawPixel(x, divY, BLACK); - // Move cursor up: above info string - msgB -= fontSmall.lineHeight(); + // Move cursor up: above info string + msgB -= fontSmall.lineHeight(); - // Vertical line alongside message - for (int16_t y = msgB; y < dotsB; y += 1) - drawPixel(outgoing ? width() - 1 : 0, y, BLACK); + // Vertical line alongside message + for (int16_t y = msgB; y < dotsB; y += 1) + drawPixel(outgoing ? width() - 1 : 0, y, BLACK); - // Move cursor up: padding before next message - msgB -= fontSmall.lineHeight() * 0.5; + // Move cursor up: padding before next message + msgB -= fontSmall.lineHeight() * 0.5; - i++; - } // End of loop: drawing each message + i++; + } // End of loop: drawing each message - // Fade effect: - // Area immediately below the divider. Overdraw with sparse white lines. - // Make text appear to pass behind the header - hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); + // Fade effect: + // Area immediately below the divider. Overdraw with sparse white lines. + // Make text appear to pass behind the header + hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); - // If we've run out of screen to draw messages, we can drop any leftover data from the queue - // Those messages have been pushed off the screen-top by newer ones - while (i < store->messages.size()) - store->messages.pop_back(); + // If we've run out of screen to draw messages, we can drop any leftover data from the queue + // Those messages have been pushed off the screen-top by newer ones + while (i < store->messages.size()) + store->messages.pop_back(); } // Code which runs when the applet begins running // This might happen at boot, or if user enables the applet at run-time, via the menu -void InkHUD::ThreadedMessageApplet::onActivate() -{ - loadMessagesFromFlash(); - loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) +void InkHUD::ThreadedMessageApplet::onActivate() { + loadMessagesFromFlash(); + loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) } // Code which runs when the applet stop running // This might be at shutdown, or if the user disables the applet at run-time, via the menu -void InkHUD::ThreadedMessageApplet::onDeactivate() -{ - loopbackOk = false; // Slightly reduce our impact if the applet is disabled +void InkHUD::ThreadedMessageApplet::onDeactivate() { + loopbackOk = false; // Slightly reduce our impact if the applet is disabled } // Handle new text messages // These might be incoming, from the mesh, or outgoing from phone // Each instance of the ThreadMessageApplet will only listen on one specific channel -ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) -{ - // Abort if applet fully deactivated - if (!isActive()) - return ProcessMessage::CONTINUE; - - // Abort if wrong channel - if (mp.channel != this->channelIndex) - return ProcessMessage::CONTINUE; - - // Abort if message was a DM - if (mp.to != NODENUM_BROADCAST) - return ProcessMessage::CONTINUE; - - // Extract info into our slimmed-down "StoredMessage" type - MessageStore::Message newMessage; - newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - newMessage.sender = mp.from; - newMessage.channelIndex = mp.channel; - newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); - - // Store newest message at front - // These records are used when rendering, and also stored in flash at shutdown - store->messages.push_front(newMessage); - - // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(&mp) != nodeDB->getNodeNum()) - requestAutoshow(); - - // Redraw the applet, perhaps. - requestUpdate(); // Want to update display, if applet is foreground - - // Tell Module API to continue informing other firmware components about this message - // We're not the only component which is interested in new text messages +ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) { + // Abort if applet fully deactivated + if (!isActive()) return ProcessMessage::CONTINUE; + + // Abort if wrong channel + if (mp.channel != this->channelIndex) + return ProcessMessage::CONTINUE; + + // Abort if message was a DM + if (mp.to != NODENUM_BROADCAST) + return ProcessMessage::CONTINUE; + + // Extract info into our slimmed-down "StoredMessage" type + MessageStore::Message newMessage; + newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + newMessage.sender = mp.from; + newMessage.channelIndex = mp.channel; + newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); + + // Store newest message at front + // These records are used when rendering, and also stored in flash at shutdown + store->messages.push_front(newMessage); + + // If this was an incoming message, suggest that our applet becomes foreground, if permitted + if (getFrom(&mp) != nodeDB->getNodeNum()) + requestAutoshow(); + + // Redraw the applet, perhaps. + requestUpdate(); // Want to update display, if applet is foreground + + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages + return ProcessMessage::CONTINUE; } // Don't show notifications for text messages broadcast to our channel, when the applet is displayed -bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) -{ - if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex) - return false; +bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) { + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex) + return false; - // None of our business. Allow the notification. - else - return true; + // None of our business. Allow the notification. + else + return true; } // Save several recent messages to flash // Stores the contents of ThreadedMessageApplet::messages // Just enough messages to fill the display // Messages are packed "back-to-back", to minimize blocks of flash used -void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() -{ - // Create a label (will become the filename in flash) - std::string label = "ch" + to_string(channelIndex); +void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() { + // Create a label (will become the filename in flash) + std::string label = "ch" + to_string(channelIndex); - store->saveToFlash(); + store->saveToFlash(); } // Load recent messages to flash // Fills ThreadedMessageApplet::messages with previous messages // Just enough messages have been stored to cover the display -void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() -{ - // Create a label (will become the filename in flash) - std::string label = "ch" + to_string(channelIndex); +void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() { + // Create a label (will become the filename in flash) + std::string label = "ch" + to_string(channelIndex); - store->loadFromFlash(); + store->loadFromFlash(); } // Code to run when device is shutting down // This is in addition to any onDeactivate() code, which will also run // Todo: implement before a reboot also -void InkHUD::ThreadedMessageApplet::onShutdown() -{ - // Save our current set of messages to flash, provided the applet isn't disabled - if (isActive()) - saveMessagesToFlash(); +void InkHUD::ThreadedMessageApplet::onShutdown() { + // Save our current set of messages to flash, provided the applet isn't disabled + if (isActive()) + saveMessagesToFlash(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index c986539b3..d6012795d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -25,32 +25,30 @@ Suggest a max of two channel, to minimize fs usage? #include "modules/TextMessageModule.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { class Applet; -class ThreadedMessageApplet : public Applet, public SinglePortModule -{ - public: - explicit ThreadedMessageApplet(uint8_t channelIndex); - ThreadedMessageApplet() = delete; +class ThreadedMessageApplet : public Applet, public SinglePortModule { +public: + explicit ThreadedMessageApplet(uint8_t channelIndex); + ThreadedMessageApplet() = delete; - void onRender() override; + void onRender() override; - void onActivate() override; - void onDeactivate() override; - void onShutdown() override; - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + void onActivate() override; + void onDeactivate() override; + void onShutdown() override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - bool approveNotification(Notification &n) override; // Which notifications to suppress + bool approveNotification(Notification &n) override; // Which notifications to suppress - protected: - void saveMessagesToFlash(); - void loadMessagesFromFlash(); +protected: + void saveMessagesToFlash(); + void loadMessagesFromFlash(); - MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown - uint8_t channelIndex = 0; + MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown + uint8_t channelIndex = 0; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index e8849b72e..76716d871 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -10,167 +10,157 @@ using namespace NicheGraphics; static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; -InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") -{ - // Timer disabled by default - OSThread::disable(); +InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") { + // Timer disabled by default + OSThread::disable(); } // Request which update type we would prefer, when the display image next changes // DisplayHealth class will consider our suggestion, and weigh it against other requests -void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type) -{ - // Update our "working decision", to decide if this request is important enough to change our plan - if (!forced) - workingDecision = prioritize(workingDecision, type); +void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type) { + // Update our "working decision", to decide if this request is important enough to change our plan + if (!forced) + workingDecision = prioritize(workingDecision, type); } // Demand that a specific update type be used, when the display image next changes // Note: multiple DisplayHealth::force calls should not be made, // but if they are, the importance of the type will be weighed the same as if both calls were to DisplayHealth::request -void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type) -{ - if (!forced) - workingDecision = type; - else - workingDecision = prioritize(workingDecision, type); +void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type) { + if (!forced) + workingDecision = type; + else + workingDecision = prioritize(workingDecision, type); - forced = true; + forced = true; } // Find out which update type the DisplayHealth has chosen for us // Calling this method consumes the result, and resets for the next update -Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType() -{ - LOG_DEBUG("FULL-update debt:%f", debt); +Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType() { + LOG_DEBUG("FULL-update debt:%f", debt); - // For convenience - typedef Drivers::EInk::UpdateTypes UpdateTypes; + // For convenience + typedef Drivers::EInk::UpdateTypes UpdateTypes; - // Grab our final decision for the update type, so we can reset now, for the next update - // We do this at top of the method, so we can return early - UpdateTypes finalDecision = workingDecision; - workingDecision = UpdateTypes::UNSPECIFIED; - forced = false; + // Grab our final decision for the update type, so we can reset now, for the next update + // We do this at top of the method, so we can return early + UpdateTypes finalDecision = workingDecision; + workingDecision = UpdateTypes::UNSPECIFIED; + forced = false; - // Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress) - // This maintenance behavior will also have opportunity to halt itself when the timer next fires, - // but that could be an hour away, so we can stop it early here and free up resources - if (OSThread::enabled && debt == 0.0) - endMaintenance(); + // Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress) + // This maintenance behavior will also have opportunity to halt itself when the timer next fires, + // but that could be an hour away, so we can stop it early here and free up resources + if (OSThread::enabled && debt == 0.0) + endMaintenance(); - // Explicitly requested FULL - if (finalDecision == UpdateTypes::FULL) { - LOG_DEBUG("Explicit FULL"); - debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt - return UpdateTypes::FULL; - } + // Explicitly requested FULL + if (finalDecision == UpdateTypes::FULL) { + LOG_DEBUG("Explicit FULL"); + debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt + return UpdateTypes::FULL; + } - // Explicitly requested FAST - if (finalDecision == UpdateTypes::FAST) { - LOG_DEBUG("Explicit FAST"); - // Add to the FULL refresh debt - if (debt < 1.0) - debt += 1.0 / fastPerFull; - else - debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes + // Explicitly requested FAST + if (finalDecision == UpdateTypes::FAST) { + LOG_DEBUG("Explicit FAST"); + // Add to the FULL refresh debt + if (debt < 1.0) + debt += 1.0 / fastPerFull; + else + debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes - // If *significant debt*, begin occasionally refreshing *unprovoked* - // This maintenance behavior is only triggered here, by periods of user interaction - // Debt would otherwise not be able to climb above 1.0 - if (debt >= 2.0) - beginMaintenance(); + // If *significant debt*, begin occasionally refreshing *unprovoked* + // This maintenance behavior is only triggered here, by periods of user interaction + // Debt would otherwise not be able to climb above 1.0 + if (debt >= 2.0) + beginMaintenance(); - return UpdateTypes::FAST; // Give them what the asked for - } + return UpdateTypes::FAST; // Give them what the asked for + } - // Handling UpdateTypes::UNSPECIFIED - // ----------------------------------- - // In this case, the UI doesn't care which refresh we use + // Handling UpdateTypes::UNSPECIFIED + // ----------------------------------- + // In this case, the UI doesn't care which refresh we use - // Not much debt: suggest FAST - if (debt < 1.0) { - LOG_DEBUG("UNSPECIFIED: using FAST"); - debt += 1.0 / fastPerFull; - return UpdateTypes::FAST; - } + // Not much debt: suggest FAST + if (debt < 1.0) { + LOG_DEBUG("UNSPECIFIED: using FAST"); + debt += 1.0 / fastPerFull; + return UpdateTypes::FAST; + } - // In debt: suggest FULL - else { - LOG_DEBUG("UNSPECIFIED: using FULL"); - debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt + // In debt: suggest FULL + else { + LOG_DEBUG("UNSPECIFIED: using FULL"); + debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt - // When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so) - // If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh - // We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically - if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) - OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow + // When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so) + // If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh + // We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically + if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) + OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow - return UpdateTypes::FULL; - } + return UpdateTypes::FULL; + } } // Determine which of two update types is more important to honor // Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness // Explicit FULL is more important than explicit FAST - prioritize image quality: explicit FULL is rare // Used when multiple applets have all requested update simultaneously, each with their own preferred UpdateType -Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) -{ - switch (type1) { - case Drivers::EInk::UpdateTypes::UNSPECIFIED: - return type2; +Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) { + switch (type1) { + case Drivers::EInk::UpdateTypes::UNSPECIFIED: + return type2; - case Drivers::EInk::UpdateTypes::FAST: - return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST; + case Drivers::EInk::UpdateTypes::FAST: + return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST; - case Drivers::EInk::UpdateTypes::FULL: - return type1; - } + case Drivers::EInk::UpdateTypes::FULL: + return type1; + } - return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only + return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only } // We're using the timer to perform "maintenance" // If significant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked. // This prevents gradual build-up of debt, // in case we aren't doing enough UNSPECIFIED refreshes to pay the debt back organically. -// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration -// Subsequent refreshes take place *much* less frequently. -// Hopefully an applet will want to render before this, meaning we can cancel the maintenance. -int32_t InkHUD::DisplayHealth::runOnce() -{ - if (debt > 0.0) { - LOG_DEBUG("debt=%f: performing maintenance", debt); +// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the +// restoration Subsequent refreshes take place *much* less frequently. Hopefully an applet will want to render before +// this, meaning we can cancel the maintenance. +int32_t InkHUD::DisplayHealth::runOnce() { + if (debt > 0.0) { + LOG_DEBUG("debt=%f: performing maintenance", debt); - // Ask WindowManager to redraw everything, purely for the refresh - // Todo: optimize? Could update without re-rendering - InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL); + // Ask WindowManager to redraw everything, purely for the refresh + // Todo: optimize? Could update without re-rendering + InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL); - // Record that we have paid back (some of) the FULL refresh debt - debt = max(debt - 1.0, 0.0); + // Record that we have paid back (some of) the FULL refresh debt + debt = max(debt - 1.0, 0.0); - // Next maintenance refresh scheduled - long wait (an hour?) - return MAINTENANCE_MS; - } + // Next maintenance refresh scheduled - long wait (an hour?) + return MAINTENANCE_MS; + } - else - return endMaintenance(); + else + return endMaintenance(); } // Begin periodically refreshing the display, to repay FULL-refresh debt // We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED // After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently // This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable -void InkHUD::DisplayHealth::beginMaintenance() -{ - OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL); - OSThread::enabled = true; +void InkHUD::DisplayHealth::beginMaintenance() { + OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL); + OSThread::enabled = true; } // FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates -int32_t InkHUD::DisplayHealth::endMaintenance() -{ - return OSThread::disable(); -} +int32_t InkHUD::DisplayHealth::endMaintenance() { return OSThread::disable(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/DisplayHealth.h b/src/graphics/niche/InkHUD/DisplayHealth.h index 2bd887f9d..ed9d8fceb 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.h +++ b/src/graphics/niche/InkHUD/DisplayHealth.h @@ -18,34 +18,31 @@ Responsible for maintaining display health, by optimizing the ratio of FAST vs F #include "graphics/niche/Drivers/EInk/EInk.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class DisplayHealth : protected concurrency::OSThread -{ - public: - DisplayHealth(); +class DisplayHealth : protected concurrency::OSThread { +public: + DisplayHealth(); - void requestUpdateType(Drivers::EInk::UpdateTypes type); - void forceUpdateType(Drivers::EInk::UpdateTypes type); - Drivers::EInk::UpdateTypes decideUpdateType(); + void requestUpdateType(Drivers::EInk::UpdateTypes type); + void forceUpdateType(Drivers::EInk::UpdateTypes type); + Drivers::EInk::UpdateTypes decideUpdateType(); - uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes - float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull? + uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes + float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull? - private: - int32_t runOnce() override; - void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health - int32_t endMaintenance(); // End unprovoked refreshing: debt paid +private: + int32_t runOnce() override; + void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health + int32_t endMaintenance(); // End unprovoked refreshing: debt paid - Drivers::EInk::UpdateTypes - prioritize(Drivers::EInk::UpdateTypes type1, - Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor + Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1, + Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor - bool forced = false; - Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED; + bool forced = false; + Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED; - float debt = 0.0; // How many full refreshes are due + float debt = 0.0; // How many full refreshes are due }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index 5382d2391..3cdc8ce8f 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -14,30 +14,78 @@ using namespace NicheGraphics; -InkHUD::Events::Events() -{ - // Get convenient references - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; +InkHUD::Events::Events() { + // Get convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } -void InkHUD::Events::begin() -{ - // Register our callbacks for the various events +void InkHUD::Events::begin() { + // Register our callbacks for the various events - deepSleepObserver.observe(¬ifyDeepSleep); - rebootObserver.observe(¬ifyReboot); - textMessageObserver.observe(textMessageModule); + deepSleepObserver.observe(¬ifyDeepSleep); + rebootObserver.observe(¬ifyReboot); + textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe((Observable *)adminModule); + adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 - lightSleepObserver.observe(¬ifyLightSleep); + lightSleepObserver.observe(¬ifyLightSleep); #endif } -void InkHUD::Events::onButtonShort() -{ +void InkHUD::Events::onButtonShort() { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + // or open menu if joystick is enabled + if (consumer) { + consumer->onButtonShortPress(); + } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } +} + +void InkHUD::Events::onButtonLong() { + // Audio feedback (via buzzer) + // Slightly longer than playChirp + playBoop(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to open the menu + if (consumer) + consumer->onButtonLongPress(); + else + inkhud->openMenu(); +} + +void InkHUD::Events::onExitShort() { + if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Short tone playChirp(); @@ -48,26 +96,22 @@ void InkHUD::Events::onButtonShort() // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + if (sa->handleInput) { + consumer = sa; + break; + } } - // If no system applet is handling input, default behavior instead is to cycle applets - // or open menu if joystick is enabled - if (consumer) { - consumer->onButtonShortPress(); - } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module - if (!settings->joystick.enabled) - inkhud->nextApplet(); - else - inkhud->openMenu(); - } + // If no system applet is handling input, default behavior instead is change tiles + if (consumer) + consumer->onExitShort(); + else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module + inkhud->nextTile(); + } } -void InkHUD::Events::onButtonLong() -{ +void InkHUD::Events::onExitLong() { + if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); @@ -75,325 +119,265 @@ void InkHUD::Events::onButtonLong() // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + if (sa->handleInput) { + consumer = sa; + break; + } } - // If no system applet is handling input, default behavior instead is to open the menu if (consumer) - consumer->onButtonLongPress(); - else - inkhud->openMenu(); + consumer->onExitLong(); + } } -void InkHUD::Events::onExitShort() -{ - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onNavUp() { + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - // If no system applet is handling input, default behavior instead is change tiles - if (consumer) - consumer->onExitShort(); - else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module - inkhud->nextTile(); + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } } + + if (consumer) + consumer->onNavUp(); + } } -void InkHUD::Events::onExitLong() -{ - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Slightly longer than playChirp - playBoop(); +void InkHUD::Events::onNavDown() { + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - if (consumer) - consumer->onExitLong(); + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } } + + if (consumer) + consumer->onNavDown(); + } } -void InkHUD::Events::onNavUp() -{ - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onNavLeft() { + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - if (consumer) - consumer->onNavUp(); + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavLeft(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->prevApplet(); + } } -void InkHUD::Events::onNavDown() -{ - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onNavRight() { + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - if (consumer) - consumer->onNavDown(); + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } } -} -void InkHUD::Events::onNavLeft() -{ - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) - consumer->onNavLeft(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->prevApplet(); - } -} - -void InkHUD::Events::onNavRight() -{ - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) - consumer->onNavRight(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->nextApplet(); - } + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavRight(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->nextApplet(); + } } // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now -int InkHUD::Events::beforeDeepSleep(void *unused) -{ - // If a previous display update is in progress, wait for it to complete. - inkhud->awaitUpdate(); +int InkHUD::Events::beforeDeepSleep(void *unused) { + // If a previous display update is in progress, wait for it to complete. + inkhud->awaitUpdate(); - // Notify all applets that we're shutting down - for (Applet *ua : inkhud->userApplets) { - ua->onDeactivate(); - ua->onShutdown(); - } - for (SystemApplet *sa : inkhud->systemApplets) { - // Note: no onDeactivate. System applets are always active. - sa->onShutdown(); - } + // Notify all applets that we're shutting down + for (Applet *ua : inkhud->userApplets) { + ua->onDeactivate(); + ua->onShutdown(); + } + for (SystemApplet *sa : inkhud->systemApplets) { + // Note: no onDeactivate. System applets are always active. + sa->onShutdown(); + } - // User has successful executed a safe shutdown - // We don't need to nag at boot anymore - settings->tips.safeShutdownSeen = true; + // User has successful executed a safe shutdown + // We don't need to nag at boot anymore + settings->tips.safeShutdownSeen = true; - inkhud->persistence->saveSettings(); - inkhud->persistence->saveLatestMessage(); + inkhud->persistence->saveSettings(); + inkhud->persistence->saveLatestMessage(); - // LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice, - // then prepared a final powered-off screen for us, which shows device shortname. - // We're updating to show that one now. + // LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice, + // then prepared a final powered-off screen for us, which shows device shortname. + // We're updating to show that one now. - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); - delay(1000); // Cooldown, before potentially yanking display power + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + delay(1000); // Cooldown, before potentially yanking display power - // InkHUD shutdown complete - // Firmware shutdown continues for several seconds more; flash write still pending - playShutdownMelody(); + // InkHUD shutdown complete + // Firmware shutdown continues for several seconds more; flash write still pending + playShutdownMelody(); - return 0; // We agree: deep sleep now + return 0; // We agree: deep sleep now } // Callback for rebootObserver // Same as shutdown, without drawing the logoApplet // Makes sure we don't lose message history / InkHUD config -int InkHUD::Events::beforeReboot(void *unused) -{ +int InkHUD::Events::beforeReboot(void *unused) { - // Notify all applets that we're "shutting down" - // They don't need to know that it's really a reboot - for (Applet *a : inkhud->userApplets) { - a->onDeactivate(); - a->onShutdown(); - } - for (SystemApplet *sa : inkhud->systemApplets) { - // Note: no onDeactivate. System applets are always active. - sa->onReboot(); - } + // Notify all applets that we're "shutting down" + // They don't need to know that it's really a reboot + for (Applet *a : inkhud->userApplets) { + a->onDeactivate(); + a->onShutdown(); + } + for (SystemApplet *sa : inkhud->systemApplets) { + // Note: no onDeactivate. System applets are always active. + sa->onReboot(); + } - // Save settings to flash, or erase if factory reset in progress - if (!eraseOnReboot) { - inkhud->persistence->saveSettings(); - inkhud->persistence->saveLatestMessage(); - } else { - NicheGraphics::clearFlashData(); - } + // Save settings to flash, or erase if factory reset in progress + if (!eraseOnReboot) { + inkhud->persistence->saveSettings(); + inkhud->persistence->saveLatestMessage(); + } else { + NicheGraphics::clearFlashData(); + } - // Note: no forceUpdate call here - // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen + // Note: no forceUpdate call here + // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen - return 0; // No special status to report. Ignored anyway by this Observable + return 0; // No special status to report. Ignored anyway by this Observable } // Callback when a new text message is received // Caches the most recently received message, for use by applets // Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc. // Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message -int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) -{ - // Short circuit: don't store outgoing messages - if (getFrom(packet) == nodeDB->getNodeNum()) - return 0; +int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) { + // Short circuit: don't store outgoing messages + if (getFrom(packet) == nodeDB->getNodeNum()) + return 0; - // Determine whether the message is broadcast or a DM - // Store this info to prevent confusion after a reboot - // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set - inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); + // Determine whether the message is broadcast or a DM + // Store this info to prevent confusion after a reboot + // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not + // set + inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); - // Pick the appropriate variable to store the message in - MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast - ? &inkhud->persistence->latestMessage.broadcast - : &inkhud->persistence->latestMessage.dm; + // Pick the appropriate variable to store the message in + MessageStore::Message *storedMessage = + inkhud->persistence->latestMessage.wasBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; - // Store nodenum of the sender - // Applets can use this to fetch user data from nodedb, if they want - storedMessage->sender = packet->from; + // Store nodenum of the sender + // Applets can use this to fetch user data from nodedb, if they want + storedMessage->sender = packet->from; - // Store the time (epoch seconds) when message received - storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + // Store the time (epoch seconds) when message received + storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - // Store the channel - // - (potentially) used to determine whether notification shows - // - (potentially) used to determine which applet to focus - storedMessage->channelIndex = packet->channel; + // Store the channel + // - (potentially) used to determine whether notification shows + // - (potentially) used to determine which applet to focus + storedMessage->channelIndex = packet->channel; - // Store the text - // Need to specify manually how many bytes, because source not null-terminated - storedMessage->text = - std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); + // Store the text + // Need to specify manually how many bytes, because source not null-terminated + storedMessage->text = std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); - return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } -int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) -{ - switch (data->request->which_payload_variant) { - // Factory reset - // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. - case meshtastic_AdminMessage_factory_reset_device_tag: - case meshtastic_AdminMessage_factory_reset_config_tag: - eraseOnReboot = true; - *data->result = AdminMessageHandleResult::HANDLED; - break; +int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { + switch (data->request->which_payload_variant) { + // Factory reset + // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. + case meshtastic_AdminMessage_factory_reset_device_tag: + case meshtastic_AdminMessage_factory_reset_config_tag: + eraseOnReboot = true; + *data->result = AdminMessageHandleResult::HANDLED; + break; - default: - break; - } + default: + break; + } - return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } #ifdef ARCH_ESP32 // Callback for lightSleepObserver // Make sure the display is not partway through an update when we begin light sleep -// This is because some displays require active input from us to terminate the update process, and protect the panel hardware -int InkHUD::Events::beforeLightSleep(void *unused) -{ - inkhud->awaitUpdate(); - return 0; // No special status to report. Ignored anyway by this Observable +// This is because some displays require active input from us to terminate the update process, and protect the panel +// hardware +int InkHUD::Events::beforeLightSleep(void *unused) { + inkhud->awaitUpdate(); + return 0; // No special status to report. Ignored anyway by this Observable } #endif // Silence all ongoing beeping, blinking, buzzing, coming from the external notification module // Returns true if an external notification was active, and we dismissed it // Button handling changes depending on our result -bool InkHUD::Events::dismissExternalNotification() -{ - // Abort if not using external notifications - if (!moduleConfig.external_notification.enabled) - return false; +bool InkHUD::Events::dismissExternalNotification() { + // Abort if not using external notifications + if (!moduleConfig.external_notification.enabled) + return false; - // Abort if nothing to dismiss - if (!externalNotificationModule->nagging()) - return false; + // Abort if nothing to dismiss + if (!externalNotificationModule->nagging()) + return false; - // Stop the beep buzz blink - externalNotificationModule->stopNow(); + // Stop the beep buzz blink + externalNotificationModule->stopNow(); - // Inform that we did indeed dismiss an external notification - return true; + // Inform that we did indeed dismiss an external notification + return true; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 664ca19f0..4d4519581 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -18,61 +18,59 @@ however this class handles general events which concern InkHUD as a whole, e.g. #include "./InkHUD.h" #include "./Persistence.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class Events -{ - public: - Events(); - void begin(); +class Events { +public: + Events(); + void begin(); - void onButtonShort(); // User button: short press - void onButtonLong(); // User button: long press - void onExitShort(); // Exit button: short press - void onExitLong(); // Exit button: long press - void onNavUp(); // Navigate up - void onNavDown(); // Navigate down - void onNavLeft(); // Navigate left - void onNavRight(); // Navigate right + void onButtonShort(); // User button: short press + void onButtonLong(); // User button: long press + void onExitShort(); // Exit button: short press + void onExitLong(); // Exit button: long press + void onNavUp(); // Navigate up + void onNavDown(); // Navigate down + void onNavLeft(); // Navigate left + void onNavRight(); // Navigate right - int beforeDeepSleep(void *unused); // Prepare for shutdown - int beforeReboot(void *unused); // Prepare for reboot - int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message - int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + int beforeDeepSleep(void *unused); // Prepare for shutdown + int beforeReboot(void *unused); // Prepare for reboot + int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); // Prepare for light sleep + int beforeLightSleep(void *unused); // Prepare for light sleep #endif - private: - // For convenience - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; +private: + // For convenience + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; - // Get notified when the system is shutting down - CallbackObserver deepSleepObserver = CallbackObserver(this, &Events::beforeDeepSleep); + // Get notified when the system is shutting down + CallbackObserver deepSleepObserver = CallbackObserver(this, &Events::beforeDeepSleep); - // Get notified when the system is rebooting - CallbackObserver rebootObserver = CallbackObserver(this, &Events::beforeReboot); + // Get notified when the system is rebooting + CallbackObserver rebootObserver = CallbackObserver(this, &Events::beforeReboot); - // Cache *incoming* text messages, for use by applets - CallbackObserver textMessageObserver = - CallbackObserver(this, &Events::onReceiveTextMessage); + // Cache *incoming* text messages, for use by applets + CallbackObserver textMessageObserver = + CallbackObserver(this, &Events::onReceiveTextMessage); - // Get notified of incoming admin messages, and handle any which are relevant to InkHUD - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Events::onAdminMessage); + // Get notified of incoming admin messages, and handle any which are relevant to InkHUD + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Events::onAdminMessage); #ifdef ARCH_ESP32 - // Get notified when the system is entering light sleep - CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); + // Get notified when the system is entering light sleep + CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); #endif - // End any externalNotification beeping, buzzing, blinking etc - bool dismissExternalNotification(); + // End any externalNotification beeping, buzzing, blinking etc + bool dismissExternalNotification(); - // If set, InkHUD's data will be erased during onReboot - bool eraseOnReboot = false; + // If set, InkHUD's data will be erased during onReboot + bool eraseOnReboot = false; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 9f05ae5bb..b89de8d4e 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -13,222 +13,174 @@ using namespace NicheGraphics; // Get or create the singleton -InkHUD::InkHUD *InkHUD::InkHUD::getInstance() -{ - // Create the singleton instance of our class, if not yet done - static InkHUD *instance = nullptr; - if (!instance) { - instance = new InkHUD; +InkHUD::InkHUD *InkHUD::InkHUD::getInstance() { + // Create the singleton instance of our class, if not yet done + static InkHUD *instance = nullptr; + if (!instance) { + instance = new InkHUD; - instance->persistence = new Persistence; - instance->windowManager = new WindowManager; - instance->renderer = new Renderer; - instance->events = new Events; - } + instance->persistence = new Persistence; + instance->windowManager = new WindowManager; + instance->renderer = new Renderer; + instance->events = new Events; + } - return instance; + return instance; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called -void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) -{ - renderer->setDriver(driver); -} +void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) { renderer->setDriver(driver); } // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health -void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) -{ - renderer->setDisplayResilience(fastPerFull, stressMultiplier); +void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { + renderer->setDisplayResilience(fastPerFull, stressMultiplier); } // Register a user applet with InkHUD // A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method // Passing an applet to this method is all that is required to make it available to the user in your InkHUD build -void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) -{ - windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); +void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { + windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); } // Start InkHUD! // Call this only after you have configured InkHUD -void InkHUD::InkHUD::begin() -{ - persistence->loadSettings(); - persistence->loadLatestMessage(); +void InkHUD::InkHUD::begin() { + persistence->loadSettings(); + persistence->loadLatestMessage(); - windowManager->begin(); - events->begin(); - renderer->begin(); - // LogoApplet shows boot screen here + windowManager->begin(); + events->begin(); + renderer->begin(); + // LogoApplet shows boot screen here } // Call this when your user button gets a short press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) -void InkHUD::InkHUD::shortpress() -{ - events->onButtonShort(); -} +void InkHUD::InkHUD::shortpress() { events->onButtonShort(); } // Call this when your user button gets a long press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) -void InkHUD::InkHUD::longpress() -{ - events->onButtonLong(); -} +void InkHUD::InkHUD::longpress() { events->onButtonLong(); } // Call this when your exit button gets a short press -void InkHUD::InkHUD::exitShort() -{ - events->onExitShort(); -} +void InkHUD::InkHUD::exitShort() { events->onExitShort(); } // Call this when your exit button gets a long press -void InkHUD::InkHUD::exitLong() -{ - events->onExitLong(); -} +void InkHUD::InkHUD::exitLong() { events->onExitLong(); } // Call this when your joystick gets an up input -void InkHUD::InkHUD::navUp() -{ - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavLeft(); - break; - case 2: // 180 deg - events->onNavDown(); - break; - case 3: // 270 deg - events->onNavRight(); - break; - default: // 0 deg - events->onNavUp(); - break; - } +void InkHUD::InkHUD::navUp() { + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavLeft(); + break; + case 2: // 180 deg + events->onNavDown(); + break; + case 3: // 270 deg + events->onNavRight(); + break; + default: // 0 deg + events->onNavUp(); + break; + } } // Call this when your joystick gets a down input -void InkHUD::InkHUD::navDown() -{ - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavRight(); - break; - case 2: // 180 deg - events->onNavUp(); - break; - case 3: // 270 deg - events->onNavLeft(); - break; - default: // 0 deg - events->onNavDown(); - break; - } +void InkHUD::InkHUD::navDown() { + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavRight(); + break; + case 2: // 180 deg + events->onNavUp(); + break; + case 3: // 270 deg + events->onNavLeft(); + break; + default: // 0 deg + events->onNavDown(); + break; + } } // Call this when your joystick gets a left input -void InkHUD::InkHUD::navLeft() -{ - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavDown(); - break; - case 2: // 180 deg - events->onNavRight(); - break; - case 3: // 270 deg - events->onNavUp(); - break; - default: // 0 deg - events->onNavLeft(); - break; - } +void InkHUD::InkHUD::navLeft() { + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavDown(); + break; + case 2: // 180 deg + events->onNavRight(); + break; + case 3: // 270 deg + events->onNavUp(); + break; + default: // 0 deg + events->onNavLeft(); + break; + } } // Call this when your joystick gets a right input -void InkHUD::InkHUD::navRight() -{ - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavUp(); - break; - case 2: // 180 deg - events->onNavLeft(); - break; - case 3: // 270 deg - events->onNavDown(); - break; - default: // 0 deg - events->onNavRight(); - break; - } +void InkHUD::InkHUD::navRight() { + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavUp(); + break; + case 2: // 180 deg + events->onNavLeft(); + break; + case 3: // 270 deg + events->onNavDown(); + break; + default: // 0 deg + events->onNavRight(); + break; + } } // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" -void InkHUD::InkHUD::nextApplet() -{ - windowManager->nextApplet(); -} +void InkHUD::InkHUD::nextApplet() { windowManager->nextApplet(); } // Cycle the previous user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" -void InkHUD::InkHUD::prevApplet() -{ - windowManager->prevApplet(); -} +void InkHUD::InkHUD::prevApplet() { windowManager->prevApplet(); } // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes -void InkHUD::InkHUD::openMenu() -{ - windowManager->openMenu(); -} +void InkHUD::InkHUD::openMenu() { windowManager->openMenu(); } // Bring AlignStick applet to the foreground -void InkHUD::InkHUD::openAlignStick() -{ - windowManager->openAlignStick(); -} +void InkHUD::InkHUD::openAlignStick() { windowManager->openAlignStick(); } // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press -void InkHUD::InkHUD::nextTile() -{ - windowManager->nextTile(); -} +void InkHUD::InkHUD::nextTile() { windowManager->nextTile(); } // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press -void InkHUD::InkHUD::prevTile() -{ - windowManager->prevTile(); -} +void InkHUD::InkHUD::prevTile() { windowManager->prevTile(); } // Rotate the display image by 90 degrees -void InkHUD::InkHUD::rotate() -{ - windowManager->rotate(); -} +void InkHUD::InkHUD::rotate() { windowManager->rotate(); } // rotate the joystick in 90 degree increments -void InkHUD::InkHUD::rotateJoystick(uint8_t angle) -{ - persistence->settings.joystick.alignment += angle; - persistence->settings.joystick.alignment %= 4; +void InkHUD::InkHUD::rotateJoystick(uint8_t angle) { + persistence->settings.joystick.alignment += angle; + persistence->settings.joystick.alignment %= 4; } // Show / hide the battery indicator in top-right -void InkHUD::InkHUD::toggleBatteryIcon() -{ - windowManager->toggleBatteryIcon(); -} +void InkHUD::InkHUD::toggleBatteryIcon() { windowManager->toggleBatteryIcon(); } // An applet asking for the display to be updated // This does not occur immediately @@ -236,64 +188,40 @@ void InkHUD::InkHUD::toggleBatteryIcon() // This allows multiple applets to observe the same event, and then share the same opportunity to update // Applets should requestUpdate, whether or not they are currently displayed ("foreground") // This is because they *might* be automatically brought to foreground by WindowManager::autoshow -void InkHUD::InkHUD::requestUpdate() -{ - renderer->requestUpdate(); -} +void InkHUD::InkHUD::requestUpdate() { renderer->requestUpdate(); } // Demand that the display be updated // Ignores all diplomacy: // - the display *will* update // - the specified update type *will* be used // If the async parameter is false, code flow is blocked while the update takes place -void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) -{ - renderer->forceUpdate(type, async); -} +void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) { renderer->forceUpdate(type, async); } // Wait for any in-progress display update to complete before continuing -void InkHUD::InkHUD::awaitUpdate() -{ - renderer->awaitUpdate(); -} +void InkHUD::InkHUD::awaitUpdate() { renderer->awaitUpdate(); } // Ask the window manager to potentially bring a different user applet to foreground // An applet will be brought to foreground if it has just received new and relevant info // For Example: AllMessagesApplet has just received a new text message // Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis // If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event -void InkHUD::InkHUD::autoshow() -{ - windowManager->autoshow(); -} +void InkHUD::InkHUD::autoshow() { windowManager->autoshow(); } // Tell the window manager that the Persistence::Settings value for applet activation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or when the user enables / disabled applets via the on-screen menu -void InkHUD::InkHUD::updateAppletSelection() -{ - windowManager->changeActivatedApplets(); -} +void InkHUD::InkHUD::updateAppletSelection() { windowManager->changeActivatedApplets(); } // Tell the window manager that the Persistence::Settings value for layout or rotation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or by rotate / layout options in the on-screen menu -void InkHUD::InkHUD::updateLayout() -{ - windowManager->changeLayout(); -} +void InkHUD::InkHUD::updateLayout() { windowManager->changeLayout(); } // Width of the display, in the context of the current rotation -uint16_t InkHUD::InkHUD::width() -{ - return renderer->width(); -} +uint16_t InkHUD::InkHUD::width() { return renderer->width(); } // Height of the display, in the context of the current rotation -uint16_t InkHUD::InkHUD::height() -{ - return renderer->height(); -} +uint16_t InkHUD::InkHUD::height() { return renderer->height(); } // A collection of any user tiles which do not have a valid user applet // This can occur in various situations, such as when a user enables fewer applets than their layout has tiles @@ -301,23 +229,19 @@ uint16_t InkHUD::InkHUD::height() // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- -std::vector InkHUD::InkHUD::getEmptyTiles() -{ - return windowManager->getEmptyTiles(); -} +std::vector InkHUD::InkHUD::getEmptyTiles() { return windowManager->getEmptyTiles(); } // Get a system applet by its name // This isn't particularly elegant, but it does avoid: // - passing around a big set of references // - having two sets of references (systemApplet vector for iteration) -InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) -{ - for (SystemApplet *sa : systemApplets) { - if (strcmp(name, sa->name) == 0) - return sa; - } +InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) { + for (SystemApplet *sa : systemApplets) { + if (strcmp(name, sa->name) == 0) + return sa; + } - assert(false); // Invalid name + assert(false); // Invalid name } // Place a pixel into the image buffer @@ -326,9 +250,6 @@ InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) // - Tiles pass translated pixels to this method // - this methods (Renderer) places rotated pixels into the image buffer // This method provides the final formatting step required. The image buffer is suitable for writing to display -void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) -{ - renderer->handlePixel(x, y, c); -} +void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) { renderer->handlePixel(x, y, c); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 7325d8262..6f6f191c1 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -18,14 +18,13 @@ #include -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { // Color, understood by display controller IC (as bit values) // Also suitable for use as AdafruitGFX colors enum Color : uint8_t { - BLACK = 0, - WHITE = 1, + BLACK = 0, + WHITE = 1, }; class Applet; @@ -36,83 +35,82 @@ class SystemApplet; class Tile; class WindowManager; -class InkHUD -{ - public: - static InkHUD *getInstance(); // Access to this singleton class +class InkHUD { +public: + static InkHUD *getInstance(); // Access to this singleton class - // Configuration - // - before InkHUD::begin, in variant nicheGraphics.h, + // Configuration + // - before InkHUD::begin, in variant nicheGraphics.h, - void setDriver(Drivers::EInk *driver); - void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); - void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); + void setDriver(Drivers::EInk *driver); + void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); + void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); - void begin(); + void begin(); - // Handle user-button press - // - connected to an input source, in variant nicheGraphics.h + // Handle user-button press + // - connected to an input source, in variant nicheGraphics.h - void shortpress(); - void longpress(); - void exitShort(); - void exitLong(); - void navUp(); - void navDown(); - void navLeft(); - void navRight(); + void shortpress(); + void longpress(); + void exitShort(); + void exitLong(); + void navUp(); + void navDown(); + void navLeft(); + void navRight(); - // Trigger UI changes - // - called by various InkHUD components - // - suitable(?) for use by aux button, connected in variant nicheGraphics.h + // Trigger UI changes + // - called by various InkHUD components + // - suitable(?) for use by aux button, connected in variant nicheGraphics.h - void nextApplet(); - void prevApplet(); - void openMenu(); - void openAlignStick(); - void nextTile(); - void prevTile(); - void rotate(); - void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default - void toggleBatteryIcon(); + void nextApplet(); + void prevApplet(); + void openMenu(); + void openAlignStick(); + void nextTile(); + void prevTile(); + void rotate(); + void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default + void toggleBatteryIcon(); - // Updating the display - // - called by various InkHUD components + // Updating the display + // - called by various InkHUD components - void requestUpdate(); - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true); - void awaitUpdate(); + void requestUpdate(); + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true); + void awaitUpdate(); - // (Re)configuring WindowManager + // (Re)configuring WindowManager - void autoshow(); // Bring an applet to foreground - void updateAppletSelection(); // Change which applets are active - void updateLayout(); // Change multiplexing (count, rotation) + void autoshow(); // Bring an applet to foreground + void updateAppletSelection(); // Change which applets are active + void updateLayout(); // Change multiplexing (count, rotation) - // Information passed between components + // Information passed between components - uint16_t width(); // From E-Ink driver - uint16_t height(); // From E-Ink driver - std::vector getEmptyTiles(); // From WindowManager + uint16_t width(); // From E-Ink driver + uint16_t height(); // From E-Ink driver + std::vector getEmptyTiles(); // From WindowManager - // Applets + // Applets - SystemApplet *getSystemApplet(const char *name); - std::vector userApplets; - std::vector systemApplets; + SystemApplet *getSystemApplet(const char *name); + std::vector userApplets; + std::vector systemApplets; - // Pass drawing output to Renderer - void drawPixel(int16_t x, int16_t y, Color c); + // Pass drawing output to Renderer + void drawPixel(int16_t x, int16_t y, Color c); - // Shared data which persists between boots - Persistence *persistence = nullptr; + // Shared data which persists between boots + Persistence *persistence = nullptr; - private: - InkHUD() {} // Constructor made private to force use of InkHUD::getInstance +private: + InkHUD() {} // Constructor made private to force use of InkHUD::getInstance - Events *events = nullptr; // Handle non-specific firmware events - Renderer *renderer = nullptr; // Co-ordinate display updates - WindowManager *windowManager = nullptr; // Multiplexing of applets + Events *events = nullptr; // Handle non-specific firmware events + Renderer *renderer = nullptr; // Co-ordinate display updates + WindowManager *windowManager = nullptr; // Multiplexing of applets }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp index 94e0aa661..0833bd0a6 100644 --- a/src/graphics/niche/InkHUD/MessageStore.cpp +++ b/src/graphics/niche/InkHUD/MessageStore.cpp @@ -12,142 +12,140 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::MessageStore::MessageStore(std::string label) -{ - filename = ""; - filename += "/NicheGraphics"; - filename += "/"; - filename += label; - filename += ".msgs"; +InkHUD::MessageStore::MessageStore(std::string label) { + filename = ""; + filename += "/NicheGraphics"; + filename += "/"; + filename += label; + filename += ".msgs"; } // Write the contents of the MessageStore::messages object to flash -// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. -// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally -void InkHUD::MessageStore::saveToFlash() -{ - assert(!filename.empty()); +// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD +// card. Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally +void InkHUD::MessageStore::saveToFlash() { + assert(!filename.empty()); #ifdef FSCom - // Make the directory, if doesn't already exist - // This is the same directory accessed by NicheGraphics::FlashData - spiLock->lock(); - FSCom.mkdir("/NicheGraphics"); - spiLock->unlock(); + // Make the directory, if doesn't already exist + // This is the same directory accessed by NicheGraphics::FlashData + spiLock->lock(); + FSCom.mkdir("/NicheGraphics"); + spiLock->unlock(); - // Open or create the file - // No "full atomic": don't save then rename - auto f = SafeFile(filename.c_str(), false); + // Open or create the file + // No "full atomic": don't save then rename + auto f = SafeFile(filename.c_str(), false); - LOG_INFO("Saving messages in %s", filename.c_str()); + LOG_INFO("Saving messages in %s", filename.c_str()); - // Take firmware's SPI Lock while writing - spiLock->lock(); + // Take firmware's SPI Lock while writing + spiLock->lock(); - // 1st byte: how many messages will be written to store - f.write(messages.size()); + // 1st byte: how many messages will be written to store + f.write(messages.size()); - // For each message - for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { - Message &m = messages.at(i); - f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes - f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes - f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte - f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length - f.write('\0'); // Append null term - LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); - } + // For each message + for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { + Message &m = messages.at(i); + f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes + f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes + f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte + f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length + f.write('\0'); // Append null term + LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); + } - // Release firmware's SPI lock, because SafeFile::close needs it - spiLock->unlock(); + // Release firmware's SPI lock, because SafeFile::close needs it + spiLock->unlock(); - bool writeSucceeded = f.close(); + bool writeSucceeded = f.close(); - if (!writeSucceeded) { - LOG_ERROR("Can't write data!"); - } + if (!writeSucceeded) { + LOG_ERROR("Can't write data!"); + } #else - LOG_ERROR("ERROR: Filesystem not implemented\n"); + LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } // Attempt to load the previous contents of the MessageStore:message deque from flash. // Filename is controlled by the "label" parameter -// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. -void InkHUD::MessageStore::loadFromFlash() -{ - // Hopefully redundant. Initial intention is to only load / save once per boot. - messages.clear(); +// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD +// card. +void InkHUD::MessageStore::loadFromFlash() { + // Hopefully redundant. Initial intention is to only load / save once per boot. + messages.clear(); #ifdef FSCom - // Take the firmware's SPI Lock, in case filesystem is on SD card - concurrency::LockGuard guard(spiLock); + // Take the firmware's SPI Lock, in case filesystem is on SD card + concurrency::LockGuard guard(spiLock); - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_WARN("'%s' not found. Using default values", filename.c_str()); - return; - } - - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_INFO("'%s' not found.", filename.c_str()); - return; - } - - // Open the file - auto f = FSCom.open(filename.c_str(), FILE_O_READ); - - if (f.size() == 0) { - LOG_INFO("%s is empty", filename.c_str()); - f.close(); - return; - } - - // If opened, start reading - if (f) { - LOG_INFO("Loading threaded messages '%s'", filename.c_str()); - - // First byte: how many messages are in the flash store - uint8_t flashMessageCount = 0; - f.readBytes((char *)&flashMessageCount, 1); - LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); - - // For each message - for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { - Message m; - - // Read meta data (fixed width) - f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); - f.readBytes((char *)&m.sender, sizeof(m.sender)); - f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); - - // Read characters until we find a null term - char c; - while (m.text.size() < MAX_MESSAGE_SIZE) { - f.readBytes(&c, 1); - if (c != '\0') - m.text += c; - else - break; - } - - // Store in RAM - messages.push_back(m); - - LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); - } - - f.close(); - } else { - LOG_ERROR("Could not open / read %s", filename.c_str()); - } -#else - LOG_ERROR("Filesystem not implemented"); - state = LoadFileState::NO_FILESYSTEM; -#endif + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_WARN("'%s' not found. Using default values", filename.c_str()); return; + } + + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_INFO("'%s' not found.", filename.c_str()); + return; + } + + // Open the file + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + + if (f.size() == 0) { + LOG_INFO("%s is empty", filename.c_str()); + f.close(); + return; + } + + // If opened, start reading + if (f) { + LOG_INFO("Loading threaded messages '%s'", filename.c_str()); + + // First byte: how many messages are in the flash store + uint8_t flashMessageCount = 0; + f.readBytes((char *)&flashMessageCount, 1); + LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); + + // For each message + for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { + Message m; + + // Read meta data (fixed width) + f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); + f.readBytes((char *)&m.sender, sizeof(m.sender)); + f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); + + // Read characters until we find a null term + char c; + while (m.text.size() < MAX_MESSAGE_SIZE) { + f.readBytes(&c, 1); + if (c != '\0') + m.text += c; + else + break; + } + + // Store in RAM + messages.push_back(m); + + LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); + } + + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename.c_str()); + } +#else + LOG_ERROR("Filesystem not implemented"); + state = LoadFileState::NO_FILESYSTEM; +#endif + return; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h index 745c3b2eb..2b7eab70f 100644 --- a/src/graphics/niche/InkHUD/MessageStore.h +++ b/src/graphics/niche/InkHUD/MessageStore.h @@ -16,30 +16,28 @@ and methods for serializing them to flash. #include "mesh/MeshTypes.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class MessageStore -{ - public: - // A stored message - struct Message { - uint32_t timestamp; // Epoch seconds - NodeNum sender = 0; - uint8_t channelIndex; - std::string text; - }; +class MessageStore { +public: + // A stored message + struct Message { + uint32_t timestamp; // Epoch seconds + NodeNum sender = 0; + uint8_t channelIndex; + std::string text; + }; - MessageStore() = delete; - explicit MessageStore(std::string label); // Label determines filename in flash + MessageStore() = delete; + explicit MessageStore(std::string label); // Label determines filename in flash - void saveToFlash(); - void loadFromFlash(); + void saveToFlash(); + void loadFromFlash(); - std::deque messages; // Interact with this object! + std::deque messages; // Interact with this object! - private: - std::string filename; +private: + std::string filename; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Persistence.cpp b/src/graphics/niche/InkHUD/Persistence.cpp index 20909f2dc..a68594888 100644 --- a/src/graphics/niche/InkHUD/Persistence.cpp +++ b/src/graphics/niche/InkHUD/Persistence.cpp @@ -5,52 +5,47 @@ using namespace NicheGraphics; // Load settings and latestMessage data -void InkHUD::Persistence::loadSettings() -{ - // Load the InkHUD settings from flash, and check version number - // We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data - Settings loadedSettings; - bool loadSucceeded = FlashData::load(&loadedSettings, "settings"); - if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0) - settings = loadedSettings; // Version matched, replace the defaults with the loaded values - else - LOG_WARN("Settings version changed. Using defaults"); +void InkHUD::Persistence::loadSettings() { + // Load the InkHUD settings from flash, and check version number + // We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load + // flash data + Settings loadedSettings; + bool loadSucceeded = FlashData::load(&loadedSettings, "settings"); + if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0) + settings = loadedSettings; // Version matched, replace the defaults with the loaded values + else + LOG_WARN("Settings version changed. Using defaults"); } // Load settings and latestMessage data -void InkHUD::Persistence::loadLatestMessage() -{ - // Load previous "latestMessages" data from flash - MessageStore store("latest"); - store.loadFromFlash(); +void InkHUD::Persistence::loadLatestMessage() { + // Load previous "latestMessages" data from flash + MessageStore store("latest"); + store.loadFromFlash(); - // Place into latestMessage struct, for convenient access - // Number of strings loaded determines whether last message was broadcast or dm - if (store.messages.size() == 1) { - latestMessage.dm = store.messages.at(0); - latestMessage.wasBroadcast = false; - } else if (store.messages.size() == 2) { - latestMessage.dm = store.messages.at(0); - latestMessage.broadcast = store.messages.at(1); - latestMessage.wasBroadcast = true; - } + // Place into latestMessage struct, for convenient access + // Number of strings loaded determines whether last message was broadcast or dm + if (store.messages.size() == 1) { + latestMessage.dm = store.messages.at(0); + latestMessage.wasBroadcast = false; + } else if (store.messages.size() == 2) { + latestMessage.dm = store.messages.at(0); + latestMessage.broadcast = store.messages.at(1); + latestMessage.wasBroadcast = true; + } } // Save the InkHUD settings to flash -void InkHUD::Persistence::saveSettings() -{ - FlashData::save(&settings, "settings"); -} +void InkHUD::Persistence::saveSettings() { FlashData::save(&settings, "settings"); } // Save latestMessage data to flash -void InkHUD::Persistence::saveLatestMessage() -{ - // Number of strings saved determines whether last message was broadcast or dm - MessageStore store("latest"); - store.messages.push_back(latestMessage.dm); - if (latestMessage.wasBroadcast) - store.messages.push_back(latestMessage.broadcast); - store.saveToFlash(); +void InkHUD::Persistence::saveLatestMessage() { + // Number of strings saved determines whether last message was broadcast or dm + MessageStore store("latest"); + store.messages.push_back(latestMessage.dm); + if (latestMessage.wasBroadcast) + store.messages.push_back(latestMessage.broadcast); + store.saveToFlash(); } /* diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index 5054b7234..b69e15140 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -18,126 +18,124 @@ The save / load mechanism is a shared NicheGraphics feature. #include "graphics/niche/InkHUD/MessageStore.h" #include "graphics/niche/Utils/FlashData.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class Persistence -{ - public: - static constexpr uint8_t MAX_TILES_GLOBAL = 4; - static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16; +class Persistence { +public: + static constexpr uint8_t MAX_TILES_GLOBAL = 4; + static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16; - // Used to invalidate old settings, if needed - // Version 0 is reserved for testing, and will always load defaults - static constexpr uint32_t SETTINGS_VERSION = 3; + // Used to invalidate old settings, if needed + // Version 0 is reserved for testing, and will always load defaults + static constexpr uint32_t SETTINGS_VERSION = 3; - struct Settings { - struct Meta { - // Used to invalidate old savefiles, if we make breaking changes - uint32_t version = SETTINGS_VERSION; - } meta; + struct Settings { + struct Meta { + // Used to invalidate old savefiles, if we make breaking changes + uint32_t version = SETTINGS_VERSION; + } meta; - struct UserTiles { - // How many tiles are shown - uint8_t count = 1; + struct UserTiles { + // How many tiles are shown + uint8_t count = 1; - // Maximum amount of tiles for this display - uint8_t maxCount = 4; + // Maximum amount of tiles for this display + uint8_t maxCount = 4; - // Which tile is focused (responding to user button input) - uint8_t focused = 0; + // Which tile is focused (responding to user button input) + uint8_t focused = 0; - // Which applet is displayed on which tile - // Index of array: which tile, as indexed in WindowManager::userTiles - // Value of array: which applet, as indexed in InkHUD::userApplets - uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3}; - } userTiles; + // Which applet is displayed on which tile + // Index of array: which tile, as indexed in WindowManager::userTiles + // Value of array: which applet, as indexed in InkHUD::userApplets + uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3}; + } userTiles; - struct UserApplets { - // Which applets are running (either displayed, or in the background) - // Index of array: which applet, as indexed in InkHUD::userApplets - // Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method - bool active[MAX_USERAPPLETS_GLOBAL]{false}; + struct UserApplets { + // Which applets are running (either displayed, or in the background) + // Index of array: which applet, as indexed in InkHUD::userApplets + // Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method + bool active[MAX_USERAPPLETS_GLOBAL]{false}; - // Which user applets should be automatically shown when they have important data to show - // If none set, foreground applets should remain foreground without manual user input - // If multiple applets request this at once, - // priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method - bool autoshow[MAX_USERAPPLETS_GLOBAL]{false}; - } userApplets; + // Which user applets should be automatically shown when they have important data to show + // If none set, foreground applets should remain foreground without manual user input + // If multiple applets request this at once, + // priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method + bool autoshow[MAX_USERAPPLETS_GLOBAL]{false}; + } userApplets; - // Features which the user can enable / disable via the on-screen menu - struct OptionalFeatures { - bool notifications = true; - bool batteryIcon = false; - } optionalFeatures; + // Features which the user can enable / disable via the on-screen menu + struct OptionalFeatures { + bool notifications = true; + bool batteryIcon = false; + } optionalFeatures; - // Some menu items may not be required, based on device / configuration - // We can enable them only when needed, to de-clutter the menu - struct OptionalMenuItems { - // If aux button is used to swap between tiles, we have no need for this menu item - bool nextTile = true; + // Some menu items may not be required, based on device / configuration + // We can enable them only when needed, to de-clutter the menu + struct OptionalMenuItems { + // If aux button is used to swap between tiles, we have no need for this menu item + bool nextTile = true; - // Used if backlight present, and not controlled by AUX button - // If this item is added to menu: backlight is always active when menu is open - // The added menu items then allows the user to "Keep Backlight On", globally. - bool backlight = false; - } optionalMenuItems; + // Used if backlight present, and not controlled by AUX button + // If this item is added to menu: backlight is always active when menu is open + // The added menu items then allows the user to "Keep Backlight On", globally. + bool backlight = false; + } optionalMenuItems; - // Allows tips to be run once only - struct Tips { - // Enables the longer "tutorial" shown only on first boot - // Once tutorial has been completed, it is no longer shown - bool firstBoot = true; + // Allows tips to be run once only + struct Tips { + // Enables the longer "tutorial" shown only on first boot + // Once tutorial has been completed, it is no longer shown + bool firstBoot = true; - // User is advised to shut down before removing device power - // Once user executes a shutdown (either via menu or client app), - // this tip is no longer shown - bool safeShutdownSeen = false; - } tips; + // User is advised to shut down before removing device power + // Once user executes a shutdown (either via menu or client app), + // this tip is no longer shown + bool safeShutdownSeen = false; + } tips; - // Joystick settings for enabling and aligning to the screen - struct Joystick { - // Modifies the UI for joystick use - bool enabled = false; + // Joystick settings for enabling and aligning to the screen + struct Joystick { + // Modifies the UI for joystick use + bool enabled = false; - // gets set to true when AlignStick applet is completed - bool aligned = false; + // gets set to true when AlignStick applet is completed + bool aligned = false; - // Rotation of the joystick - // Multiples of 90 degrees clockwise - uint8_t alignment = 0; - } joystick; + // Rotation of the joystick + // Multiples of 90 degrees clockwise + uint8_t alignment = 0; + } joystick; - // Rotation of the display - // Multiples of 90 degrees clockwise - // Most commonly: rotation is 0 when flex connector is oriented below display - uint8_t rotation = 0; + // Rotation of the display + // Multiples of 90 degrees clockwise + // Most commonly: rotation is 0 when flex connector is oriented below display + uint8_t rotation = 0; - // How long do we consider another node to be "active"? - // Used when applets want to filter for "active nodes" only - uint32_t recentlyActiveSeconds = 2 * 60; - }; + // How long do we consider another node to be "active"? + // Used when applets want to filter for "active nodes" only + uint32_t recentlyActiveSeconds = 2 * 60; + }; - // Most recently received text message - // Value is updated by InkHUD::WindowManager, as a courtesy to applets - // Note: different from devicestate.rx_text_message, - // which may contain an *outgoing message* to broadcast - struct LatestMessage { - MessageStore::Message broadcast; // Most recent message received broadcast - MessageStore::Message dm; // Most recent received DM - bool wasBroadcast; // True if most recent broadcast is newer than most recent dm - }; + // Most recently received text message + // Value is updated by InkHUD::WindowManager, as a courtesy to applets + // Note: different from devicestate.rx_text_message, + // which may contain an *outgoing message* to broadcast + struct LatestMessage { + MessageStore::Message broadcast; // Most recent message received broadcast + MessageStore::Message dm; // Most recent received DM + bool wasBroadcast; // True if most recent broadcast is newer than most recent dm + }; - void loadSettings(); - void saveSettings(); - void loadLatestMessage(); - void saveLatestMessage(); + void loadSettings(); + void saveSettings(); + void loadLatestMessage(); + void saveLatestMessage(); - // void printSettings(Settings *settings); // Debugging use only + // void printSettings(Settings *settings); // Debugging use only - Settings settings; - LatestMessage latestMessage; + Settings settings; + LatestMessage latestMessage; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index 072e9dbd6..8db6cca92 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -10,67 +10,61 @@ using namespace NicheGraphics; -InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer") -{ - // Nothing for the timer to do just yet - OSThread::disable(); +InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer") { + // Nothing for the timer to do just yet + OSThread::disable(); - // Convenient references - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; + // Convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called -void InkHUD::Renderer::setDriver(Drivers::EInk *driver) -{ - // Make sure not already set - if (this->driver) { - LOG_ERROR("Driver already set"); - delay(2000); // Wait for native serial.. - assert(false); - } +void InkHUD::Renderer::setDriver(Drivers::EInk *driver) { + // Make sure not already set + if (this->driver) { + LOG_ERROR("Driver already set"); + delay(2000); // Wait for native serial.. + assert(false); + } - // Store the driver which was created in setupNicheGraphics() - this->driver = driver; + // Store the driver which was created in setupNicheGraphics() + this->driver = driver; - // Determine the dimensions of the image buffer, in bytes. - // Along rows, pixels are stored 8 per byte. - // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. - imageBufferWidth = ((driver->width - 1) / 8) + 1; - imageBufferHeight = driver->height; + // Determine the dimensions of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + imageBufferWidth = ((driver->width - 1) / 8) + 1; + imageBufferHeight = driver->height; - // Allocate the image buffer - imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight]; + // Allocate the image buffer + imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight]; } // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health -void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) -{ - displayHealth.fastPerFull = fastPerFull; - displayHealth.stressMultiplier = stressMultiplier; +void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { + displayHealth.fastPerFull = fastPerFull; + displayHealth.stressMultiplier = stressMultiplier; } -void InkHUD::Renderer::begin() -{ - forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); -} +void InkHUD::Renderer::begin() { forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); } // Set a flag, which will be picked up by runOnce, ASAP. // Quite likely, multiple applets will all want to respond to one event (Observable, etc) -// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce -void InkHUD::Renderer::requestUpdate() -{ - requested = true; +// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next +// runOnce +void InkHUD::Renderer::requestUpdate() { + requested = true; - // We will run the thread as soon as we loop(), - // after all Applets have had a chance to observe whatever event set this off - OSThread::setIntervalFromNow(0); - OSThread::enabled = true; - runASAP = true; + // We will run the thread as soon as we loop(), + // after all Applets have had a chance to observe whatever event set this off + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; } // requestUpdate will not actually update if no requests were made by applets which are actually visible @@ -79,174 +73,166 @@ void InkHUD::Renderer::requestUpdate() // Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event // Display health, for example. // In these situations, we use forceUpdate -void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) -{ - requested = true; - forced = true; - displayHealth.forceUpdateType(type); +void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) { + requested = true; + forced = true; + displayHealth.forceUpdateType(type); - // Normally, we need to start the timer, in case the display is busy and we briefly defer the update - if (async) { - // We will run the thread as soon as we loop(), - // after all Applets have had a chance to observe whatever event set this off - OSThread::setIntervalFromNow(0); - OSThread::enabled = true; - runASAP = true; - } + // Normally, we need to start the timer, in case the display is busy and we briefly defer the update + if (async) { + // We will run the thread as soon as we loop(), + // after all Applets have had a chance to observe whatever event set this off + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; + } - // If the update is *not* asynchronous, we begin the render process directly here - // so that it can block code flow while running - else - render(false); + // If the update is *not* asynchronous, we begin the render process directly here + // so that it can block code flow while running + else + render(false); } // Wait for any in-progress display update to complete before continuing -void InkHUD::Renderer::awaitUpdate() -{ - if (driver->busy()) { - LOG_INFO("Waiting for display"); - driver->await(); // Wait here for update to complete - } +void InkHUD::Renderer::awaitUpdate() { + if (driver->busy()) { + LOG_INFO("Waiting for display"); + driver->await(); // Wait here for update to complete + } } // Set a ready-to-draw pixel into the image buffer // All rotations / translations have already taken place: this buffer data is formatted ready for the driver -void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c) -{ - rotatePixelCoords(&x, &y); +void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c) { + rotatePixelCoords(&x, &y); - uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte - uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte. + uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte + uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte. - bitWrite(imageBuffer[byteNum], bitNum, c); + bitWrite(imageBuffer[byteNum], bitNum, c); } // Width of the display, relative to rotation -uint16_t InkHUD::Renderer::width() -{ - if (settings->rotation % 2) - return driver->height; - else - return driver->width; +uint16_t InkHUD::Renderer::width() { + if (settings->rotation % 2) + return driver->height; + else + return driver->width; } // Height of the display, relative to rotation -uint16_t InkHUD::Renderer::height() -{ - if (settings->rotation % 2) - return driver->width; - else - return driver->height; +uint16_t InkHUD::Renderer::height() { + if (settings->rotation % 2) + return driver->width; + else + return driver->height; } // Runs at regular intervals // - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render // - queuing another render: while one is already is progress -int32_t InkHUD::Renderer::runOnce() -{ - // If an applet asked to render, and hardware is able, lets try now - if (requested && !driver->busy()) { - render(); - } +int32_t InkHUD::Renderer::runOnce() { + // If an applet asked to render, and hardware is able, lets try now + if (requested && !driver->busy()) { + render(); + } - // If our render() call failed, try again shortly - // otherwise, stop our thread until next update due - if (requested) - return 250UL; - else - return OSThread::disable(); + // If our render() call failed, try again shortly + // otherwise, stop our thread until next update due + if (requested) + return 250UL; + else + return OSThread::disable(); } // Applies the system-wide rotation to pixel positions // This step is applied to image data which has already been translated by a Tile object // This is the final step before the pixel is placed into the image buffer // No return: values of the *x and *y parameters are modified by the method -void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y) -{ - // Apply a global rotation to pixel locations - int16_t x1 = 0; - int16_t y1 = 0; - switch (settings->rotation) { - case 0: - x1 = *x; - y1 = *y; - break; - case 1: - x1 = (driver->width - 1) - *y; - y1 = *x; - break; - case 2: - x1 = (driver->width - 1) - *x; - y1 = (driver->height - 1) - *y; - break; - case 3: - x1 = *y; - y1 = (driver->height - 1) - *x; - break; - } - *x = x1; - *y = y1; +void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y) { + // Apply a global rotation to pixel locations + int16_t x1 = 0; + int16_t y1 = 0; + switch (settings->rotation) { + case 0: + x1 = *x; + y1 = *y; + break; + case 1: + x1 = (driver->width - 1) - *y; + y1 = *x; + break; + case 2: + x1 = (driver->width - 1) - *x; + y1 = (driver->height - 1) - *y; + break; + case 3: + x1 = *y; + y1 = (driver->height - 1) - *x; + break; + } + *x = x1; + *y = y1; } // Make an attempt to gather image data from some / all applets, and update the display // Might not be possible right now, if update already is progress. -void InkHUD::Renderer::render(bool async) -{ - // Make sure the display is ready for a new update - if (async) { - // Previous update still running, Will try again shortly, via runOnce() - if (driver->busy()) - return; - } else { - // Wait here for previous update to complete - driver->await(); +void InkHUD::Renderer::render(bool async) { + // Make sure the display is ready for a new update + if (async) { + // Previous update still running, Will try again shortly, via runOnce() + if (driver->busy()) + return; + } else { + // Wait here for previous update to complete + driver->await(); + } + + // Determine if a system applet has requested exclusive rights to request an update, + // or exclusive rights to render + checkLocks(); + + // (Potentially) change applet to display new info, + // then check if this newly displayed applet makes a pending notification redundant + inkhud->autoshow(); + + // If an update is justified. + // We don't know this until after autoshow has run, as new applets may now be in foreground + if (shouldUpdate()) { + + // Decide which technique the display will use to change image + // Done early, as rendering resets the Applets' requested types + Drivers::EInk::UpdateTypes updateType = decideUpdateType(); + + // Render the new image + clearBuffer(); + renderUserApplets(); + renderPlaceholders(); + renderSystemApplets(); + + // Invert Buffer if set by user + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) { + imageBuffer[i] = ~imageBuffer[i]; + } } - // Determine if a system applet has requested exclusive rights to request an update, - // or exclusive rights to render - checkLocks(); + // Tell display to begin process of drawing new image + LOG_INFO("Updating display"); + driver->update(imageBuffer, updateType); - // (Potentially) change applet to display new info, - // then check if this newly displayed applet makes a pending notification redundant - inkhud->autoshow(); + // If not async, wait here until the update is complete + if (!async) + driver->await(); + } - // If an update is justified. - // We don't know this until after autoshow has run, as new applets may now be in foreground - if (shouldUpdate()) { + // Our part is done now. + // If update is async, the display hardware is still performing the update process, + // but that's all handled by NicheGraphics::Drivers::EInk - // Decide which technique the display will use to change image - // Done early, as rendering resets the Applets' requested types - Drivers::EInk::UpdateTypes updateType = decideUpdateType(); - - // Render the new image - clearBuffer(); - renderUserApplets(); - renderPlaceholders(); - renderSystemApplets(); - - // Invert Buffer if set by user - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) { - imageBuffer[i] = ~imageBuffer[i]; - } - } - - // Tell display to begin process of drawing new image - LOG_INFO("Updating display"); - driver->update(imageBuffer, updateType); - - // If not async, wait here until the update is complete - if (!async) - driver->await(); - } - - // Our part is done now. - // If update is async, the display hardware is still performing the update process, - // but that's all handled by NicheGraphics::Drivers::EInk - - // Tidy up, ready for a new request - requested = false; - forced = false; + // Tidy up, ready for a new request + requested = false; + forced = false; } // Manually fill the image buffer with WHITE @@ -254,166 +240,157 @@ void InkHUD::Renderer::render(bool async) // Note: benchmarking revealed that this is *much* faster than setting pixels individually // So much so that it's more efficient to re-render all applets, // rather than rendering selectively, and manually blanking a portion of the display -void InkHUD::Renderer::clearBuffer() -{ - memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); +void InkHUD::Renderer::clearBuffer() { memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } + +void InkHUD::Renderer::checkLocks() { + lockRendering = nullptr; + lockRequests = nullptr; + + for (SystemApplet *sa : inkhud->systemApplets) { + if (!lockRendering && sa->lockRendering && sa->isForeground()) { + lockRendering = sa; + } + if (!lockRequests && sa->lockRequests && sa->isForeground()) { + lockRequests = sa; + } + } } -void InkHUD::Renderer::checkLocks() -{ - lockRendering = nullptr; - lockRequests = nullptr; +bool InkHUD::Renderer::shouldUpdate() { + bool should = false; - for (SystemApplet *sa : inkhud->systemApplets) { - if (!lockRendering && sa->lockRendering && sa->isForeground()) { - lockRendering = sa; - } - if (!lockRequests && sa->lockRequests && sa->isForeground()) { - lockRequests = sa; - } + // via forceUpdate + should |= forced; + + // via a system applet (which has locked update requests) + if (lockRequests) { + should |= lockRequests->wantsToRender(); + return should; // Early exit - no other requests considered + } + + // via system applet (not locked) + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->wantsToRender() // This applet requested + && sa->isForeground()) // This applet is currently shown + { + should = true; + break; } -} + } -bool InkHUD::Renderer::shouldUpdate() -{ - bool should = false; - - // via forceUpdate - should |= forced; - - // via a system applet (which has locked update requests) - if (lockRequests) { - should |= lockRequests->wantsToRender(); - return should; // Early exit - no other requests considered + // via user applet + for (Applet *ua : inkhud->userApplets) { + if (ua // Tile has valid applet + && ua->wantsToRender() // This applet requested display update + && ua->isForeground()) // This applet is currently shown + { + should = true; + break; } + } - // via system applet (not locked) - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->wantsToRender() // This applet requested - && sa->isForeground()) // This applet is currently shown - { - should = true; - break; - } - } - - // via user applet - for (Applet *ua : inkhud->userApplets) { - if (ua // Tile has valid applet - && ua->wantsToRender() // This applet requested display update - && ua->isForeground()) // This applet is currently shown - { - should = true; - break; - } - } - - return should; + return should; } // Determine which type of E-Ink update the display will perform, to change the image. // Considers the needs of the various applets, then weighs against display health. // An update type specified by forceUpdate will be granted with no further questioning. -Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() -{ - // Ask applets which update type they would prefer - // Some update types take priority over others +Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() { + // Ask applets which update type they would prefer + // Some update types take priority over others - // No need to consider the "requests" if somebody already forced an update - if (!forced) { - // User applets - for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isForeground()) - displayHealth.requestUpdateType(ua->wantsUpdateType()); - } - // System Applets - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa && sa->isForeground()) - displayHealth.requestUpdateType(sa->wantsUpdateType()); - } + // No need to consider the "requests" if somebody already forced an update + if (!forced) { + // User applets + for (Applet *ua : inkhud->userApplets) { + if (ua && ua->isForeground()) + displayHealth.requestUpdateType(ua->wantsUpdateType()); } + // System Applets + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa && sa->isForeground()) + displayHealth.requestUpdateType(sa->wantsUpdateType()); + } + } - return displayHealth.decideUpdateType(); + return displayHealth.decideUpdateType(); } // Run the drawing operations of any user applets which are currently displayed // Pixel output is placed into the framebuffer, ready for handoff to the EInk driver -void InkHUD::Renderer::renderUserApplets() -{ - // Don't render user applets if a system applet has demanded the whole display to itself - if (lockRendering) - return; +void InkHUD::Renderer::renderUserApplets() { + // Don't render user applets if a system applet has demanded the whole display to itself + if (lockRendering) + return; - // Render any user applets which are currently visible - for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isActive() && ua->isForeground()) { - uint32_t start = millis(); - ua->render(); // Draw! - uint32_t stop = millis(); - LOG_DEBUG("%s took %dms to render", ua->name, stop - start); - } + // Render any user applets which are currently visible + for (Applet *ua : inkhud->userApplets) { + if (ua && ua->isActive() && ua->isForeground()) { + uint32_t start = millis(); + ua->render(); // Draw! + uint32_t stop = millis(); + LOG_DEBUG("%s took %dms to render", ua->name, stop - start); } + } } // Run the drawing operations of any system applets which are currently displayed // Pixel output is placed into the framebuffer, ready for handoff to the EInk driver -void InkHUD::Renderer::renderSystemApplets() -{ - SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon"); - SystemApplet *menu = inkhud->getSystemApplet("Menu"); - SystemApplet *notifications = inkhud->getSystemApplet("Notification"); +void InkHUD::Renderer::renderSystemApplets() { + SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon"); + SystemApplet *menu = inkhud->getSystemApplet("Menu"); + SystemApplet *notifications = inkhud->getSystemApplet("Notification"); - // Each system applet - for (SystemApplet *sa : inkhud->systemApplets) { + // Each system applet + for (SystemApplet *sa : inkhud->systemApplets) { - // Skip if not shown - if (!sa->isForeground()) - continue; + // Skip if not shown + if (!sa->isForeground()) + continue; - // Skip if locked by another applet - if (lockRendering && lockRendering != sa) - continue; + // Skip if locked by another applet + if (lockRendering && lockRendering != sa) + continue; - // Don't draw the battery or notifications overtop the menu - // Todo: smarter way to handle this - if (menu->isForeground() && (sa == battery || sa == notifications)) - continue; + // Don't draw the battery or notifications overtop the menu + // Todo: smarter way to handle this + if (menu->isForeground() && (sa == battery || sa == notifications)) + continue; - assert(sa->getTile()); + assert(sa->getTile()); - // uint32_t start = millis(); - sa->render(); // Draw! - // uint32_t stop = millis(); - // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); - } + // uint32_t start = millis(); + sa->render(); // Draw! + // uint32_t stop = millis(); + // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); + } } // In some situations (e.g. layout or applet selection changes), // a user tile can end up without an assigned applet. // In this case, we will fill the empty space with diagonal lines. -void InkHUD::Renderer::renderPlaceholders() -{ - // Don't fill empty space with placeholders if a system applet wants exclusive use of the display - if (lockRendering) - return; +void InkHUD::Renderer::renderPlaceholders() { + // Don't fill empty space with placeholders if a system applet wants exclusive use of the display + if (lockRendering) + return; - // Ask the window manager which tiles are empty - std::vector emptyTiles = inkhud->getEmptyTiles(); + // Ask the window manager which tiles are empty + std::vector emptyTiles = inkhud->getEmptyTiles(); - // No empty tiles - if (emptyTiles.size() == 0) - return; + // No empty tiles + if (emptyTiles.size() == 0) + return; - SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder"); + SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder"); - // uint32_t start = millis(); - for (Tile *t : emptyTiles) { - t->assignApplet(placeholder); - placeholder->render(); - t->assignApplet(nullptr); - } - // uint32_t stop = millis(); - // LOG_DEBUG("Placeholders took %dms to render", stop - start); + // uint32_t start = millis(); + for (Tile *t : emptyTiles) { + t->assignApplet(placeholder); + placeholder->render(); + t->assignApplet(nullptr); + } + // uint32_t stop = millis(); + // LOG_DEBUG("Placeholders took %dms to render", stop - start); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Renderer.h b/src/graphics/niche/InkHUD/Renderer.h index b6cf9e215..a0a466567 100644 --- a/src/graphics/niche/InkHUD/Renderer.h +++ b/src/graphics/niche/InkHUD/Renderer.h @@ -19,76 +19,74 @@ Orchestrates updating of the display image #include "./Persistence.h" #include "graphics/niche/Drivers/EInk/EInk.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class Renderer : protected concurrency::OSThread -{ +class Renderer : protected concurrency::OSThread { - public: - Renderer(); +public: + Renderer(); - // Configuration, before begin + // Configuration, before begin - void setDriver(Drivers::EInk *driver); - void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); + void setDriver(Drivers::EInk *driver); + void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); - void begin(); + void begin(); - // Call these to make the image change + // Call these to make the image change - void requestUpdate(); // Update display, if a foreground applet has info it wants to show - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, - bool async = true); // Update display, regardless of whether any applets requested this + void requestUpdate(); // Update display, if a foreground applet has info it wants to show + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, + bool async = true); // Update display, regardless of whether any applets requested this - // Wait for an update to complete - void awaitUpdate(); + // Wait for an update to complete + void awaitUpdate(); - // Receives pixel output from an applet (via a tile, which translates the coordinates) - void handlePixel(int16_t x, int16_t y, Color c); + // Receives pixel output from an applet (via a tile, which translates the coordinates) + void handlePixel(int16_t x, int16_t y, Color c); - // Size of display, in context of current rotation + // Size of display, in context of current rotation - uint16_t width(); - uint16_t height(); + uint16_t width(); + uint16_t height(); - private: - // Make attemps to render / update, once triggered by requestUpdate or forceUpdate - int32_t runOnce() override; +private: + // Make attemps to render / update, once triggered by requestUpdate or forceUpdate + int32_t runOnce() override; - // Apply the display rotation to handled pixels - void rotatePixelCoords(int16_t *x, int16_t *y); + // Apply the display rotation to handled pixels + void rotatePixelCoords(int16_t *x, int16_t *y); - // Execute the render process now, then hand off to driver for display update - void render(bool async = true); + // Execute the render process now, then hand off to driver for display update + void render(bool async = true); - // Steps of the rendering process + // Steps of the rendering process - void clearBuffer(); - void checkLocks(); - bool shouldUpdate(); - Drivers::EInk::UpdateTypes decideUpdateType(); - void renderUserApplets(); - void renderSystemApplets(); - void renderPlaceholders(); + void clearBuffer(); + void checkLocks(); + bool shouldUpdate(); + Drivers::EInk::UpdateTypes decideUpdateType(); + void renderUserApplets(); + void renderSystemApplets(); + void renderPlaceholders(); - Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware - DisplayHealth displayHealth; // Manages display health by controlling type of update + Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware + DisplayHealth displayHealth; // Manages display health by controlling type of update - uint8_t *imageBuffer = nullptr; // Fed into driver - uint16_t imageBufferHeight = 0; - uint16_t imageBufferWidth = 0; - uint32_t imageBufferSize = 0; // Bytes + uint8_t *imageBuffer = nullptr; // Fed into driver + uint16_t imageBufferHeight = 0; + uint16_t imageBufferWidth = 0; + uint32_t imageBufferSize = 0; // Bytes - SystemApplet *lockRendering = nullptr; // Render this applet *only* - SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only* + SystemApplet *lockRendering = nullptr; // Render this applet *only* + SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only* - bool requested = false; - bool forced = false; + bool requested = false; + bool forced = false; - // For convenience - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; + // For convenience + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/SystemApplet.h b/src/graphics/niche/InkHUD/SystemApplet.h index 7ee47eeb9..2ac15986f 100644 --- a/src/graphics/niche/InkHUD/SystemApplet.h +++ b/src/graphics/niche/InkHUD/SystemApplet.h @@ -14,28 +14,27 @@ For features like the menu, and the battery icon. #include "./Applet.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class SystemApplet : public Applet -{ - public: - // System applets have the right to: +class SystemApplet : public Applet { +public: + // System applets have the right to: - bool handleInput = false; // - respond to input from the user button - bool lockRendering = false; // - prevent other applets from being rendered during an update - bool lockRequests = false; // - prevent other applets from triggering display updates + bool handleInput = false; // - respond to input from the user button + bool lockRendering = false; // - prevent other applets from being rendered during an update + bool lockRequests = false; // - prevent other applets from triggering display updates - virtual void onReboot() { onShutdown(); } // - handle reboot specially + virtual void onReboot() { onShutdown(); } // - handle reboot specially - // Other system applets may take precedence over our own system applet though - // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank) + // Other system applets may take precedence over our own system applet though + // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher + // rank) - private: - // System applets are always running (active), but may not be visible (foreground) +private: + // System applets are always running (active), but may not be visible (foreground) - void onActivate() override {} - void onDeactivate() override {} + void onActivate() override {} + void onDeactivate() override {} }; }; // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Tile.cpp b/src/graphics/niche/InkHUD/Tile.cpp index 5e548de74..7f05a7dee 100644 --- a/src/graphics/niche/InkHUD/Tile.cpp +++ b/src/graphics/niche/InkHUD/Tile.cpp @@ -13,138 +13,132 @@ bool InkHUD::Tile::highlightShown; // For dismissing the highlight indicator, after a few seconds // Highlighting is used to inform user of which tile is now focused static concurrency::Periodic *taskHighlight; -static int32_t runtaskHighlight() -{ - LOG_DEBUG("Dismissing Highlight"); - InkHUD::Tile::highlightShown = false; - InkHUD::Tile::highlightTarget = nullptr; - InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting - return taskHighlight->disable(); +static int32_t runtaskHighlight() { + LOG_DEBUG("Dismissing Highlight"); + InkHUD::Tile::highlightShown = false; + InkHUD::Tile::highlightTarget = nullptr; + InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting + return taskHighlight->disable(); } -static void inittaskHighlight() -{ - static bool doneOnce = false; - if (!doneOnce) { - taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight); - taskHighlight->disable(); - doneOnce = true; - } +static void inittaskHighlight() { + static bool doneOnce = false; + if (!doneOnce) { + taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight); + taskHighlight->disable(); + doneOnce = true; + } } -InkHUD::Tile::Tile() -{ - inkhud = InkHUD::getInstance(); +InkHUD::Tile::Tile() { + inkhud = InkHUD::getInstance(); - inittaskHighlight(); - Tile::highlightTarget = nullptr; - Tile::highlightShown = false; + inittaskHighlight(); + Tile::highlightTarget = nullptr; + Tile::highlightShown = false; } -InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height) -{ - assert(width > 0 && height > 0); +InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height) { + assert(width > 0 && height > 0); - this->left = left; - this->top = top; - this->width = width; - this->height = height; + this->left = left; + this->top = top; + this->width = width; + this->height = height; } // Set the region of the tile automatically, based on the user's chosen layout // This method places tiles which will host user applets // The WindowManager multiplexes the applets to these tiles automatically -void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex) -{ - uint16_t displayWidth = inkhud->width(); - uint16_t displayHeight = inkhud->height(); +void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex) { + uint16_t displayWidth = inkhud->width(); + uint16_t displayHeight = inkhud->height(); - bool landscape = displayWidth > displayHeight; + bool landscape = displayWidth > displayHeight; - // Check for any stray tiles - if (tileIndex > (userTileCount - 1)) { - // Dummy values to prevent rendering - LOG_WARN("Tile index out of bounds"); - left = -2; - top = -2; - width = 1; - height = 1; - return; + // Check for any stray tiles + if (tileIndex > (userTileCount - 1)) { + // Dummy values to prevent rendering + LOG_WARN("Tile index out of bounds"); + left = -2; + top = -2; + width = 1; + height = 1; + return; + } + + // Todo: special handling for 3 tile layout + + // Gutters between tiles + const uint16_t spacing = 4; + + switch (userTileCount) { + // One tile only + case 1: + left = 0; + top = 0; + width = displayWidth; + height = displayHeight; + break; + + // Two tiles + case 2: + if (landscape) { + // Side by side + left = ((displayWidth / 2) + (spacing / 2)) * tileIndex; + top = 0; + width = (displayWidth / 2) - (spacing / 2); + height = displayHeight; + } else { + // Above and below + left = 0; + top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex); + width = displayWidth; + height = (displayHeight / 2) - (spacing / 2); } + break; - // Todo: special handling for 3 tile layout - - // Gutters between tiles - const uint16_t spacing = 4; - - switch (userTileCount) { - // One tile only + // Four tiles + case 4: + width = (displayWidth / 2) - (spacing / 2); + height = (displayHeight / 2) - (spacing / 2); + switch (tileIndex) { + case 0: + left = 0; + top = 0; + break; case 1: - left = 0; - top = 0; - width = displayWidth; - height = displayHeight; - break; - - // Two tiles + left = 0 + (width - 1) + spacing; + top = 0; + break; case 2: - if (landscape) { - // Side by side - left = ((displayWidth / 2) + (spacing / 2)) * tileIndex; - top = 0; - width = (displayWidth / 2) - (spacing / 2); - height = displayHeight; - } else { - // Above and below - left = 0; - top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex); - width = displayWidth; - height = (displayHeight / 2) - (spacing / 2); - } - break; - - // Four tiles - case 4: - width = (displayWidth / 2) - (spacing / 2); - height = (displayHeight / 2) - (spacing / 2); - switch (tileIndex) { - case 0: - left = 0; - top = 0; - break; - case 1: - left = 0 + (width - 1) + spacing; - top = 0; - break; - case 2: - left = 0; - top = 0 + (height - 1) + spacing; - break; - case 3: - left = 0 + (width - 1) + spacing; - top = 0 + (height - 1) + spacing; - break; - } - break; - - default: - LOG_ERROR("Unsupported tile layout"); - assert(0); + left = 0; + top = 0 + (height - 1) + spacing; + break; + case 3: + left = 0 + (width - 1) + spacing; + top = 0 + (height - 1) + spacing; + break; } + break; - assert(width > 0 && height > 0); + default: + LOG_ERROR("Unsupported tile layout"); + assert(0); + } + + assert(width > 0 && height > 0); } // Manually set the region for a tile // This is only done for tiles which will host certain "System Applets", which have unique position / sizes: // Things like the NotificationApplet, BatteryIconApplet, etc -void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height) -{ - assert(width > 0 && height > 0); +void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height) { + assert(width > 0 && height > 0); - this->left = left; - this->top = top; - this->width = width; - this->height = height; + this->left = left; + this->top = top; + this->width = width; + this->height = height; } // Place an applet onto a tile @@ -154,88 +148,73 @@ void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t // This is enforced with asserts // Assigning a new applet will break a previous link // Link may also be broken by assigning a nullptr -void InkHUD::Tile::assignApplet(Applet *a) -{ - // Break the link between old applet and this tile - if (assignedApplet) - assignedApplet->setTile(nullptr); +void InkHUD::Tile::assignApplet(Applet *a) { + // Break the link between old applet and this tile + if (assignedApplet) + assignedApplet->setTile(nullptr); - // Store the new applet - assignedApplet = a; + // Store the new applet + assignedApplet = a; - // Create the reciprocal link between the new applet and this tile - if (a) - a->setTile(this); + // Create the reciprocal link between the new applet and this tile + if (a) + a->setTile(this); } // Get pointer to whichever applet is displayed on this tile -InkHUD::Applet *InkHUD::Tile::getAssignedApplet() -{ - return assignedApplet; -} +InkHUD::Applet *InkHUD::Tile::getAssignedApplet() { return assignedApplet; } // Receive drawing output from the assigned applet, // and translate it from "applet-space" coordinates, to it's true location. // The final "rotation" step is performed by the windowManager -void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) -{ - // Move pixels from applet-space to tile-space - x += left; - y += top; +void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) { + // Move pixels from applet-space to tile-space + x += left; + y += top; - // Crop to tile borders - if (x >= left && x < (left + width) && y >= top && y < (top + height)) { - // Pass to the renderer - inkhud->drawPixel(x, y, c); - } + // Crop to tile borders + if (x >= left && x < (left + width) && y >= top && y < (top + height)) { + // Pass to the renderer + inkhud->drawPixel(x, y, c); + } } // Called by Applet base class, when setting applet dimensions, immediately before render -uint16_t InkHUD::Tile::getWidth() -{ - return width; -} +uint16_t InkHUD::Tile::getWidth() { return width; } // Called by Applet base class, when setting applet dimensions, immediately before render -uint16_t InkHUD::Tile::getHeight() -{ - return height; -} +uint16_t InkHUD::Tile::getHeight() { return height; } // Longest edge of the display, in pixels // A 296px x 250px display will return 296, for example // Maximum possible size of any tile's width / height // Used by some components to allocate resources for the "worst possible situation" // "Sizing the cathedral for christmas eve" -uint16_t InkHUD::Tile::maxDisplayDimension() -{ - InkHUD *inkhud = InkHUD::getInstance(); - return max(inkhud->height(), inkhud->width()); +uint16_t InkHUD::Tile::maxDisplayDimension() { + InkHUD *inkhud = InkHUD::getInstance(); + return max(inkhud->height(), inkhud->width()); } // Ask for this tile to be highlighted // Used to indicate which tile is now indicated after focus changes // Only used for aux button focus changes, not changes via menu -void InkHUD::Tile::requestHighlight() -{ - Tile::highlightTarget = this; - Tile::highlightShown = false; - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST); +void InkHUD::Tile::requestHighlight() { + Tile::highlightTarget = this; + Tile::highlightShown = false; + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST); } // Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first -void InkHUD::Tile::startHighlightTimeout() -{ - taskHighlight->setIntervalFromNow(5 * 1000UL); - taskHighlight->enabled = true; +void InkHUD::Tile::startHighlightTimeout() { + taskHighlight->setIntervalFromNow(5 * 1000UL); + taskHighlight->enabled = true; } // Stop the timer which would automatically dismiss the highlighting // Called if the tile organically renders before the timer is up -void InkHUD::Tile::cancelHighlightTimeout() -{ - if (taskHighlight->enabled) - taskHighlight->disable(); +void InkHUD::Tile::cancelHighlightTimeout() { + if (taskHighlight->enabled) + taskHighlight->disable(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Tile.h b/src/graphics/niche/InkHUD/Tile.h index 0f5444f17..48a518781 100644 --- a/src/graphics/niche/InkHUD/Tile.h +++ b/src/graphics/niche/InkHUD/Tile.h @@ -17,41 +17,39 @@ #include "./InkHUD.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class Tile -{ - public: - Tile(); - Tile(int16_t left, int16_t top, uint16_t width, uint16_t height); +class Tile { +public: + Tile(); + Tile(int16_t left, int16_t top, uint16_t width, uint16_t height); - void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout - void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually - void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet - uint16_t getWidth(); - uint16_t getHeight(); - static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter + void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout + void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually + void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet + uint16_t getWidth(); + uint16_t getHeight(); + static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter - void assignApplet(Applet *a); // Link an applet with this tile - Applet *getAssignedApplet(); // Applet which is currently linked with this tile + void assignApplet(Applet *a); // Link an applet with this tile + Applet *getAssignedApplet(); // Applet which is currently linked with this tile - void requestHighlight(); // Ask for this tile to be highlighted - static void startHighlightTimeout(); // Start the auto-dismissal timer - static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed + void requestHighlight(); // Ask for this tile to be highlighted + static void startHighlightTimeout(); // Start the auto-dismissal timer + static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed - static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?) - static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss + static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?) + static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss - private: - InkHUD *inkhud = nullptr; +private: + InkHUD *inkhud = nullptr; - int16_t left = 0; - int16_t top = 0; - uint16_t width = 0; - uint16_t height = 0; + int16_t left = 0; + int16_t top = 0; + uint16_t width = 0; + uint16_t height = 0; - Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile + Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index 0548de1eb..fbd7db157 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -14,11 +14,10 @@ using namespace NicheGraphics; -InkHUD::WindowManager::WindowManager() -{ - // Convenient references - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; +InkHUD::WindowManager::WindowManager() { + // Convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } // Register a user applet with InkHUD @@ -26,365 +25,351 @@ InkHUD::WindowManager::WindowManager() // This should be the only time that specific user applets are mentioned in the code // If a user applet is not added with this method, its code should not be built // Call before begin -void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) -{ - inkhud->userApplets.push_back(a); +void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { + inkhud->userApplets.push_back(a); - // If requested, mark in settings that this applet should be active by default - // This means that it will be available for the user to cycle to with short-press of the button - // This is the default state only: user can activate or deactivate applets through the menu. - // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present - if (defaultActive) - settings->userApplets.active[inkhud->userApplets.size() - 1] = true; + // If requested, mark in settings that this applet should be active by default + // This means that it will be available for the user to cycle to with short-press of the button + // This is the default state only: user can activate or deactivate applets through the menu. + // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present + if (defaultActive) + settings->userApplets.active[inkhud->userApplets.size() - 1] = true; - // If requested, mark in settings that this applet should "autoshow" by default - // This means that the applet will be automatically brought to foreground when it has new data to show - // This is the default state only: user can select which applets have this behavior through the menu - // User's selection is stored in settings, and will be honored instead of these defaults, if present - if (defaultAutoshow) - settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true; + // If requested, mark in settings that this applet should "autoshow" by default + // This means that the applet will be automatically brought to foreground when it has new data to show + // This is the default state only: user can select which applets have this behavior through the menu + // User's selection is stored in settings, and will be honored instead of these defaults, if present + if (defaultAutoshow) + settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true; - // If specified, mark this as the default applet for a given tile index - // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile - if (onTile != (uint8_t)-1) - settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1; + // If specified, mark this as the default applet for a given tile index + // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile + if (onTile != (uint8_t)-1) + settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1; - // The label that will be show in the applet selection menu, on the device - a->name = name; + // The label that will be show in the applet selection menu, on the device + a->name = name; } // Initial configuration at startup -void InkHUD::WindowManager::begin() -{ - assert(inkhud); +void InkHUD::WindowManager::begin() { + assert(inkhud); - createSystemApplets(); - placeSystemTiles(); + createSystemApplets(); + placeSystemTiles(); - createUserApplets(); - createUserTiles(); - placeUserTiles(); - assignUserAppletsToTiles(); - refocusTile(); + createUserApplets(); + createUserTiles(); + placeUserTiles(); + assignUserAppletsToTiles(); + refocusTile(); } // Focus on a different tile // The "focused tile" is the one which cycles applets on user button press, // and the one where the menu will be displayed -void InkHUD::WindowManager::nextTile() -{ - // Close the menu applet if open - // We don't *really* want to do this, but it simplifies handling *a lot* - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - bool menuWasOpen = false; - if (menu->isForeground()) { - menu->sendToBackground(); - menuWasOpen = true; - } +void InkHUD::WindowManager::nextTile() { + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } - // Swap to next tile - settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count; + // Swap to next tile + settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count; - // Make sure that we don't get stuck on the placeholder tile - refocusTile(); + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); - if (menuWasOpen) - menu->show(userTiles.at(settings->userTiles.focused)); + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); - // Ask the tile to draw an indicator showing which tile is now focused - // Requests a render - // We only draw this indicator if the device uses an aux button to switch tiles. - // Assume aux button is used to switch tiles if the "next tile" menu item is hidden - if (!settings->optionalMenuItems.nextTile) - userTiles.at(settings->userTiles.focused)->requestHighlight(); + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); } // Focus on a different tile but decrement index -void InkHUD::WindowManager::prevTile() -{ - // Close the menu applet if open - // We don't *really* want to do this, but it simplifies handling *a lot* - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - bool menuWasOpen = false; - if (menu->isForeground()) { - menu->sendToBackground(); - menuWasOpen = true; - } +void InkHUD::WindowManager::prevTile() { + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } - // Swap to next tile - if (settings->userTiles.focused == 0) - settings->userTiles.focused = settings->userTiles.count - 1; - else - settings->userTiles.focused--; + // Swap to next tile + if (settings->userTiles.focused == 0) + settings->userTiles.focused = settings->userTiles.count - 1; + else + settings->userTiles.focused--; - // Make sure that we don't get stuck on the placeholder tile - refocusTile(); + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); - if (menuWasOpen) - menu->show(userTiles.at(settings->userTiles.focused)); + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); - // Ask the tile to draw an indicator showing which tile is now focused - // Requests a render - // We only draw this indicator if the device uses an aux button to switch tiles. - // Assume aux button is used to switch tiles if the "next tile" menu item is hidden - if (!settings->optionalMenuItems.nextTile) - userTiles.at(settings->userTiles.focused)->requestHighlight(); + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); } // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes -void InkHUD::WindowManager::openMenu() -{ - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - menu->show(userTiles.at(settings->userTiles.focused)); +void InkHUD::WindowManager::openMenu() { + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + menu->show(userTiles.at(settings->userTiles.focused)); } // Bring the AlignStick applet to the foreground -void InkHUD::WindowManager::openAlignStick() -{ - if (settings->joystick.enabled) { - AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); - alignStick->bringToForeground(); - } +void InkHUD::WindowManager::openAlignStick() { + if (settings->joystick.enabled) { + AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); + alignStick->bringToForeground(); + } } // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile -void InkHUD::WindowManager::nextApplet() -{ - Tile *t = userTiles.at(settings->userTiles.focused); +void InkHUD::WindowManager::nextApplet() { + Tile *t = userTiles.at(settings->userTiles.focused); - // Abort if zero applets available - // nullptr means WindowManager::refocusTile determined that there were no available applets - if (!t->getAssignedApplet()) - return; + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; - // Find the index of the applet currently shown on the tile - uint8_t appletIndex = -1; - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { - appletIndex = i; - break; - } + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; } + } - // Confirm that we did find the applet - assert(appletIndex != (uint8_t)-1); + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); - // Iterate forward through the WindowManager::applets, looking for the next valid applet - Applet *nextValidApplet = nullptr; - for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { - uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size(); - Applet *a = inkhud->userApplets.at(newAppletIndex); + // Iterate forward through the WindowManager::applets, looking for the next valid applet + Applet *nextValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size(); + Applet *a = inkhud->userApplets.at(newAppletIndex); - // Looking for an applet which is active (enabled by user), but currently in background - if (a->isActive() && !a->isForeground()) { - nextValidApplet = a; - settings->userTiles.displayedUserApplet[settings->userTiles.focused] = - newAppletIndex; // Remember this setting between boots! - break; - } + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + nextValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! + break; } + } - // Confirm that we found another applet - if (!nextValidApplet) - return; + // Confirm that we found another applet + if (!nextValidApplet) + return; - // Hide old applet, show new applet - t->getAssignedApplet()->sendToBackground(); - t->assignApplet(nextValidApplet); - nextValidApplet->bringToForeground(); - inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(nextValidApplet); + nextValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } // On the currently focussed tile: cycle to the previous available user applet // Applets available for this must be activated, and not already displayed on another tile -void InkHUD::WindowManager::prevApplet() -{ - Tile *t = userTiles.at(settings->userTiles.focused); +void InkHUD::WindowManager::prevApplet() { + Tile *t = userTiles.at(settings->userTiles.focused); - // Abort if zero applets available - // nullptr means WindowManager::refocusTile determined that there were no available applets - if (!t->getAssignedApplet()) - return; + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; - // Find the index of the applet currently shown on the tile - uint8_t appletIndex = -1; - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { - appletIndex = i; - break; - } + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; } + } - // Confirm that we did find the applet - assert(appletIndex != (uint8_t)-1); + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); - // Iterate forward through the WindowManager::applets, looking for the previous valid applet - Applet *prevValidApplet = nullptr; - for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { - uint8_t newAppletIndex = 0; - if (i > appletIndex) - newAppletIndex = inkhud->userApplets.size() + appletIndex - i; - else - newAppletIndex = (appletIndex - i); - Applet *a = inkhud->userApplets.at(newAppletIndex); + // Iterate forward through the WindowManager::applets, looking for the previous valid applet + Applet *prevValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = 0; + if (i > appletIndex) + newAppletIndex = inkhud->userApplets.size() + appletIndex - i; + else + newAppletIndex = (appletIndex - i); + Applet *a = inkhud->userApplets.at(newAppletIndex); - // Looking for an applet which is active (enabled by user), but currently in background - if (a->isActive() && !a->isForeground()) { - prevValidApplet = a; - settings->userTiles.displayedUserApplet[settings->userTiles.focused] = - newAppletIndex; // Remember this setting between boots! - break; - } + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + prevValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! + break; } + } - // Confirm that we found another applet - if (!prevValidApplet) - return; + // Confirm that we found another applet + if (!prevValidApplet) + return; - // Hide old applet, show new applet - t->getAssignedApplet()->sendToBackground(); - t->assignApplet(prevValidApplet); - prevValidApplet->bringToForeground(); - inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(prevValidApplet); + prevValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } // Rotate the display image by 90 degrees -void InkHUD::WindowManager::rotate() -{ - settings->rotation = (settings->rotation + 1) % 4; - changeLayout(); +void InkHUD::WindowManager::rotate() { + settings->rotation = (settings->rotation + 1) % 4; + changeLayout(); } // Change whether the battery icon is displayed (top right corner) // Don't toggle the OptionalFeatures value before calling this, our method handles it internally -void InkHUD::WindowManager::toggleBatteryIcon() -{ - BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon"); +void InkHUD::WindowManager::toggleBatteryIcon() { + BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon"); - settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots + settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots - // Show or hide the applet - if (settings->optionalFeatures.batteryIcon) - batteryIcon->bringToForeground(); - else - batteryIcon->sendToBackground(); + // Show or hide the applet + if (settings->optionalFeatures.batteryIcon) + batteryIcon->bringToForeground(); + else + batteryIcon->sendToBackground(); - // Force-render - // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time // Call after changing settings.tiles.count -void InkHUD::WindowManager::changeLayout() -{ - // Recreate tiles - // - correct number created, from settings.userTiles.count - // - set dimension and position of tiles, according to layout - createUserTiles(); - placeUserTiles(); - placeSystemTiles(); +void InkHUD::WindowManager::changeLayout() { + // Recreate tiles + // - correct number created, from settings.userTiles.count + // - set dimension and position of tiles, according to layout + createUserTiles(); + placeUserTiles(); + placeSystemTiles(); - // Handle fewer tiles - // - background any applets which have lost their tile - findOrphanApplets(); + // Handle fewer tiles + // - background any applets which have lost their tile + findOrphanApplets(); - // Handle more tiles - // - create extra applets - // - assign them to the new extra tiles - createUserApplets(); - assignUserAppletsToTiles(); + // Handle more tiles + // - create extra applets + // - assign them to the new extra tiles + createUserApplets(); + assignUserAppletsToTiles(); - // Focus a valid tile - // - info: focused tile is the one which cycles applets when user button pressed - // - may now be out of bounds if tile count has decreased - refocusTile(); + // Focus a valid tile + // - info: focused tile is the one which cycles applets when user button pressed + // - may now be out of bounds if tile count has decreased + refocusTile(); - // Restore menu - // - its tile was just destroyed and recreated (createUserTiles) - // - its assignment was cleared (assignUserAppletsToTiles) - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - if (menu->isForeground()) { - Tile *ft = userTiles.at(settings->userTiles.focused); - menu->show(ft); - } + // Restore menu + // - its tile was just destroyed and recreated (createUserTiles) + // - its assignment was cleared (assignUserAppletsToTiles) + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + if (menu->isForeground()) { + Tile *ft = userTiles.at(settings->userTiles.focused); + menu->show(ft); + } - // Force-render - // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user activates or deactivates applets at run-time // Call after changing settings.userApplets.active -void InkHUD::WindowManager::changeActivatedApplets() -{ - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); +void InkHUD::WindowManager::changeActivatedApplets() { + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - assert(menu->isForeground()); + assert(menu->isForeground()); - // Activate or deactivate applets - // - to match value of settings.userApplets.active - createUserApplets(); + // Activate or deactivate applets + // - to match value of settings.userApplets.active + createUserApplets(); - // Assign the placeholder applet - // - if applet was foreground on a tile when deactivated, swap it with a placeholder - // - placeholder applet may be assigned to multiple tiles, if needed - assignUserAppletsToTiles(); + // Assign the placeholder applet + // - if applet was foreground on a tile when deactivated, swap it with a placeholder + // - placeholder applet may be assigned to multiple tiles, if needed + assignUserAppletsToTiles(); - // Ensure focused tile has a valid applet - // - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder - // - reason: nextApplet() won't cycle applets if placeholder is shown - refocusTile(); + // Ensure focused tile has a valid applet + // - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder + // - reason: nextApplet() won't cycle applets if placeholder is shown + refocusTile(); - // Restore menu - // - its assignment was cleared (assignUserAppletsToTiles) - if (menu->isForeground()) { - Tile *ft = userTiles.at(settings->userTiles.focused); - menu->show(ft); - } + // Restore menu + // - its assignment was cleared (assignUserAppletsToTiles) + if (menu->isForeground()) { + Tile *ft = userTiles.at(settings->userTiles.focused); + menu->show(ft); + } - // Force-render - // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Some applets may be permitted to bring themselves to foreground, to show new data // User selects which applets have this permission via on-screen menu // Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics // We will only autoshow one applet -void InkHUD::WindowManager::autoshow() -{ - // Don't perform autoshow if a system applet has exclusive use of the display right now - // Note: lockRequests prevents autoshow attempting to hide menuApplet - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->lockRendering || sa->lockRequests) - return; - } - - NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification"); - - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - Applet *a = inkhud->userApplets.at(i); - if (a->wantsToAutoshow() // Applet wants to become foreground - && !a->isForeground() // Not yet foreground - && settings->userApplets.autoshow[i]) // User permits this applet to autoshow - { - Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile - t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile - t->assignApplet(a); // Assign our new applet to tile - a->bringToForeground(); // Foreground our new applet - - // Check if autoshown applet shows the same information as notification intended to - // In this case, we can dismiss the notification before it is shown - // Note: we are re-running the approval process. This normally occurs when the notification is initially triggered. - if (notificationApplet->isForeground() && !notificationApplet->isApproved()) - notificationApplet->dismiss(); - - break; // One autoshow only! Avoid conflicts - } +void InkHUD::WindowManager::autoshow() { + // Don't perform autoshow if a system applet has exclusive use of the display right now + // Note: lockRequests prevents autoshow attempting to hide menuApplet + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->lockRendering || sa->lockRequests) + return; + } + + NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification"); + + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a->wantsToAutoshow() // Applet wants to become foreground + && !a->isForeground() // Not yet foreground + && settings->userApplets.autoshow[i]) // User permits this applet to autoshow + { + Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile + t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile + t->assignApplet(a); // Assign our new applet to tile + a->bringToForeground(); // Foreground our new applet + + // Check if autoshown applet shows the same information as notification intended to + // In this case, we can dismiss the notification before it is shown + // Note: we are re-running the approval process. This normally occurs when the notification is initially + // triggered. + if (notificationApplet->isForeground() && !notificationApplet->isApproved()) + notificationApplet->dismiss(); + + break; // One autoshow only! Avoid conflicts } + } } // A collection of any user tiles which do not have a valid user applet @@ -393,17 +378,16 @@ void InkHUD::WindowManager::autoshow() // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- -std::vector InkHUD::WindowManager::getEmptyTiles() -{ - std::vector empty; +std::vector InkHUD::WindowManager::getEmptyTiles() { + std::vector empty; - for (Tile *t : userTiles) { - Applet *a = t->getAssignedApplet(); - if (!a || !a->isActive()) - empty.push_back(t); - } + for (Tile *t : userTiles) { + Applet *a = t->getAssignedApplet(); + if (!a || !a->isActive()) + empty.push_back(t); + } - return empty; + return empty; } // Complete the configuration of one newly instantiated system applet @@ -415,118 +399,112 @@ std::vector InkHUD::WindowManager::getEmptyTiles() // The name is our only reference to specific system applets, via InkHUD->getSystemApplet // - add it to the list of system applets -void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile) -{ - // Some system applets might not have their own tile (e.g. menu, placeholder) - if (tile) - tile->assignApplet(applet); +void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile) { + // Some system applets might not have their own tile (e.g. menu, placeholder) + if (tile) + tile->assignApplet(applet); - applet->name = name; - inkhud->systemApplets.push_back(applet); + applet->name = name; + inkhud->systemApplets.push_back(applet); } // Create the "system applets" // These handle things like bootscreen, pop-up notifications etc // They are processed separately from the user applets, because they might need to do "weird things" -void InkHUD::WindowManager::createSystemApplets() -{ - addSystemApplet("Logo", new LogoApplet, new Tile); - addSystemApplet("Pairing", new PairingApplet, new Tile); - addSystemApplet("Tips", new TipsApplet, new Tile); - if (settings->joystick.enabled) - addSystemApplet("AlignStick", new AlignStickApplet, new Tile); +void InkHUD::WindowManager::createSystemApplets() { + addSystemApplet("Logo", new LogoApplet, new Tile); + addSystemApplet("Pairing", new PairingApplet, new Tile); + addSystemApplet("Tips", new TipsApplet, new Tile); + if (settings->joystick.enabled) + addSystemApplet("AlignStick", new AlignStickApplet, new Tile); - addSystemApplet("Menu", new MenuApplet, nullptr); + addSystemApplet("Menu", new MenuApplet, nullptr); - // Battery and notifications *behind* the menu - addSystemApplet("Notification", new NotificationApplet, new Tile); - addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); + // Battery and notifications *behind* the menu + addSystemApplet("Notification", new NotificationApplet, new Tile); + addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); - // Special handling only, via Rendering::renderPlaceholders - addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); + // Special handling only, via Rendering::renderPlaceholders + addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); - // System applets are always active - for (SystemApplet *sa : inkhud->systemApplets) - sa->activate(); + // System applets are always active + for (SystemApplet *sa : inkhud->systemApplets) + sa->activate(); } // Set the position and size of most system applets // Most system applets have their own tile. We manually set the region this tile occupies -void InkHUD::WindowManager::placeSystemTiles() -{ - inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - if (settings->joystick.enabled) - inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); +void InkHUD::WindowManager::placeSystemTiles() { + inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + if (settings->joystick.enabled) + inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); + inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); - const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; - const uint16_t batteryIconWidth = batteryIconHeight * 1.8; - inkhud->getSystemApplet("BatteryIcon") - ->getTile() - ->setRegion(inkhud->width() - batteryIconWidth, // x - 2, // y - batteryIconWidth, // width - batteryIconHeight); // height + const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; + const uint16_t batteryIconWidth = batteryIconHeight * 1.8; + inkhud->getSystemApplet("BatteryIcon") + ->getTile() + ->setRegion(inkhud->width() - batteryIconWidth, // x + 2, // y + batteryIconWidth, // width + batteryIconHeight); // height - // Note: the tiles of placeholder and menu applets are manipulated specially - // - menuApplet borrows user tiles - // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles + // Note: the tiles of placeholder and menu applets are manipulated specially + // - menuApplet borrows user tiles + // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles } // Activate or deactivate user applets, to match settings // Called at boot, or after run-time config changes via menu // Note: this method does not instantiate the applets; // this is done in setupNicheGraphics, when passing to InkHUD::addApplet -void InkHUD::WindowManager::createUserApplets() -{ - // Deactivate and remove any no-longer-needed applets - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - Applet *a = inkhud->userApplets.at(i); +void InkHUD::WindowManager::createUserApplets() { + // Deactivate and remove any no-longer-needed applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); - // If the applet is active, but settings say it shouldn't be: - // - run applet's custom deactivation code - // - mark applet as inactive (internally) - if (a->isActive() && !settings->userApplets.active[i]) - a->deactivate(); - } + // If the applet is active, but settings say it shouldn't be: + // - run applet's custom deactivation code + // - mark applet as inactive (internally) + if (a->isActive() && !settings->userApplets.active[i]) + a->deactivate(); + } - // Activate and add any new applets - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + // Activate and add any new applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - // If not activated, but it now should be: - // - run applet's custom activation code - // - mark applet as active (internally) - if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i]) - inkhud->userApplets.at(i)->activate(); - } + // If not activated, but it now should be: + // - run applet's custom activation code + // - mark applet as active (internally) + if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i]) + inkhud->userApplets.at(i)->activate(); + } } // Creates the tiles which will host user applets // The amount of these is controlled by the user, via "layout" option in the InkHUD menu -void InkHUD::WindowManager::createUserTiles() -{ - // Delete any tiles which currently exist - for (Tile *t : userTiles) - delete t; - userTiles.clear(); +void InkHUD::WindowManager::createUserTiles() { + // Delete any tiles which currently exist + for (Tile *t : userTiles) + delete t; + userTiles.clear(); - // Create new tiles - for (uint8_t i = 0; i < settings->userTiles.count; i++) { - Tile *t = new Tile; - userTiles.push_back(t); - } + // Create new tiles + for (uint8_t i = 0; i < settings->userTiles.count; i++) { + Tile *t = new Tile; + userTiles.push_back(t); + } } // Calculate the display region occupied by each tile // This determines how pixels are translated from "relative" applet-space to "absolute" windowmanager-space // The size and position depend on the amount of tiles the user prefers, set by the "layout" option -void InkHUD::WindowManager::placeUserTiles() -{ - for (uint8_t i = 0; i < userTiles.size(); i++) - userTiles.at(i)->setRegion(settings->userTiles.count, i); +void InkHUD::WindowManager::placeUserTiles() { + for (uint8_t i = 0; i < userTiles.size(); i++) + userTiles.at(i)->setRegion(settings->userTiles.count, i); } // Link "foreground" user applets with tiles @@ -534,99 +512,96 @@ void InkHUD::WindowManager::placeUserTiles() // This initial state changes once WindowManager::nextApplet is called. // Performed at startup, or during certain run-time reconfigurations (e.g number of tiles) // This state of "which applets are foreground" is preserved between reboots, but the value needs validating at startup. -void InkHUD::WindowManager::assignUserAppletsToTiles() -{ - // Each user tile - for (uint8_t i = 0; i < userTiles.size(); i++) { - Tile *t = userTiles.at(i); +void InkHUD::WindowManager::assignUserAppletsToTiles() { + // Each user tile + for (uint8_t i = 0; i < userTiles.size(); i++) { + Tile *t = userTiles.at(i); - // Check whether tile can display the previously shown applet again - uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets - bool canRestore = true; - if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds - canRestore = false; - else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated - canRestore = false; - else { // Check that the old applet isn't now shown already on a different tile - for (uint8_t i2 = 0; i2 < i; i2++) { - if (settings->userTiles.displayedUserApplet[i2] == oldIndex) { - canRestore = false; - break; - } - } - } - - // Restore previously shown applet if possible, - // otherwise assign nullptr, which will render specially using placeholderApplet - if (canRestore) { - Applet *a = inkhud->userApplets.at(oldIndex); - t->assignApplet(a); - a->bringToForeground(); - } else { - t->assignApplet(nullptr); - settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet + // Check whether tile can display the previously shown applet again + uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets + bool canRestore = true; + if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds + canRestore = false; + else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated + canRestore = false; + else { // Check that the old applet isn't now shown already on a different tile + for (uint8_t i2 = 0; i2 < i; i2++) { + if (settings->userTiles.displayedUserApplet[i2] == oldIndex) { + canRestore = false; + break; } + } } + + // Restore previously shown applet if possible, + // otherwise assign nullptr, which will render specially using placeholderApplet + if (canRestore) { + Applet *a = inkhud->userApplets.at(oldIndex); + t->assignApplet(a); + a->bringToForeground(); + } else { + t->assignApplet(nullptr); + settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet + } + } } // During layout changes, our focused tile setting can become invalid // This method identifies that situation and corrects for it -void InkHUD::WindowManager::refocusTile() -{ - // Validate "focused tile" setting - // - info: focused tile responds to button presses: applet cycling, menu, etc - // - if number of tiles changed, might now be out of index - if (settings->userTiles.focused >= userTiles.size()) - settings->userTiles.focused = 0; +void InkHUD::WindowManager::refocusTile() { + // Validate "focused tile" setting + // - info: focused tile responds to button presses: applet cycling, menu, etc + // - if number of tiles changed, might now be out of index + if (settings->userTiles.focused >= userTiles.size()) + settings->userTiles.focused = 0; - // Give "focused tile" a valid applet - // - scan for another valid applet, which we can addSubstitution - // - reason: nextApplet() won't cycle if no applet is assigned - Tile *focusedTile = userTiles.at(settings->userTiles.focused); - if (!focusedTile->getAssignedApplet()) { - // Search for available applets - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - Applet *a = inkhud->userApplets.at(i); - if (a->isActive() && !a->isForeground()) { - // Found a suitable applet - // Assign it to the focused tile - focusedTile->assignApplet(a); - a->bringToForeground(); - settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot - break; - } - } + // Give "focused tile" a valid applet + // - scan for another valid applet, which we can addSubstitution + // - reason: nextApplet() won't cycle if no applet is assigned + Tile *focusedTile = userTiles.at(settings->userTiles.focused); + if (!focusedTile->getAssignedApplet()) { + // Search for available applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a->isActive() && !a->isForeground()) { + // Found a suitable applet + // Assign it to the focused tile + focusedTile->assignApplet(a); + a->bringToForeground(); + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot + break; + } } + } } // Seach for any applets which believe they are foreground, but no longer have a valid tile // Tidies up after layout changes at runtime -void InkHUD::WindowManager::findOrphanApplets() -{ - for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) { - Applet *a = inkhud->userApplets.at(ia); +void InkHUD::WindowManager::findOrphanApplets() { + for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) { + Applet *a = inkhud->userApplets.at(ia); - // Applet doesn't believe it is displayed: not orphaned - if (!a->isForeground()) - continue; + // Applet doesn't believe it is displayed: not orphaned + if (!a->isForeground()) + continue; - // Check each tile, to see if anyone claims this applet - bool foundOwner = false; - for (uint8_t it = 0; it < userTiles.size(); it++) { - Tile *t = userTiles.at(it); - // A tile claims this applet: not orphaned - if (t->getAssignedApplet() == a) { - foundOwner = true; - break; - } - } - - // Orphan found - // Tell the applet that no tile is currently displaying it - // This allows the focussed tile to cycle to this applet again by pressing user button - if (!foundOwner) - a->sendToBackground(); + // Check each tile, to see if anyone claims this applet + bool foundOwner = false; + for (uint8_t it = 0; it < userTiles.size(); it++) { + Tile *t = userTiles.at(it); + // A tile claims this applet: not orphaned + if (t->getAssignedApplet() == a) { + foundOwner = true; + break; + } } + + // Orphan found + // Tell the applet that no tile is currently displaying it + // This allows the focussed tile to cycle to this applet again by pressing user button + if (!foundOwner) + a->sendToBackground(); + } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 5def48f8c..dc11d0f5e 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -15,59 +15,57 @@ Responsible for managing which applets are shown, and their sizes / positions #include "./Persistence.h" #include "./Tile.h" -namespace NicheGraphics::InkHUD -{ +namespace NicheGraphics::InkHUD { -class WindowManager -{ - public: - WindowManager(); - void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile); - void begin(); +class WindowManager { +public: + WindowManager(); + void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile); + void begin(); - // - call these to make stuff change + // - call these to make stuff change - void nextTile(); - void prevTile(); - void openMenu(); - void openAlignStick(); - void nextApplet(); - void prevApplet(); - void rotate(); - void toggleBatteryIcon(); + void nextTile(); + void prevTile(); + void openMenu(); + void openAlignStick(); + void nextApplet(); + void prevApplet(); + void rotate(); + void toggleBatteryIcon(); - // - call these to manifest changes already made to the relevant Persistence::Settings values + // - call these to manifest changes already made to the relevant Persistence::Settings values - void changeLayout(); // Change tile layout or count - void changeActivatedApplets(); // Change which applets are activated + void changeLayout(); // Change tile layout or count + void changeActivatedApplets(); // Change which applets are activated - // - called during the rendering operation + // - called during the rendering operation - void autoshow(); // Show a different applet, to display new info - std::vector getEmptyTiles(); // Any user tiles without a valid applet + void autoshow(); // Show a different applet, to display new info + std::vector getEmptyTiles(); // Any user tiles without a valid applet - private: - // Steps for configuring (or reconfiguring) the window manager - // - all steps required at startup - // - various combinations of steps required for on-the-fly reconfiguration (by user, via menu) +private: + // Steps for configuring (or reconfiguring) the window manager + // - all steps required at startup + // - various combinations of steps required for on-the-fly reconfiguration (by user, via menu) - void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile); - void createSystemApplets(); // Instantiate the system applets - void placeSystemTiles(); // Assign manual positions to (most) system applets + void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile); + void createSystemApplets(); // Instantiate the system applets + void placeSystemTiles(); // Assign manual positions to (most) system applets - void createUserApplets(); // Activate user's selected applets - void createUserTiles(); // Instantiate enough tiles for user's selected layout - void assignUserAppletsToTiles(); - void placeUserTiles(); // Automatically place tiles, according to user's layout - void refocusTile(); // Ensure focused tile has a valid applet + void createUserApplets(); // Activate user's selected applets + void createUserTiles(); // Instantiate enough tiles for user's selected layout + void assignUserAppletsToTiles(); + void placeUserTiles(); // Automatically place tiles, according to user's layout + void refocusTile(); // Ensure focused tile has a valid applet - void findOrphanApplets(); // Find any applets left-behind when layout changes + void findOrphanApplets(); // Find any applets left-behind when layout changes - std::vector userTiles; // Tiles which can host user applets + std::vector userTiles; // Tiles which can host user applets - // For convenience - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; + // For convenience + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index bd29f981d..f470e156b 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -8,302 +8,284 @@ using namespace NicheGraphics::Inputs; -TwoButton::TwoButton() : concurrency::OSThread("TwoButton") -{ - // Don't start polling buttons for release immediately - // Assume they are in a "released" state at boot - OSThread::disable(); +TwoButton::TwoButton() : concurrency::OSThread("TwoButton") { + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); #endif - // Explicitly initialize these, just to keep cppcheck quiet.. - buttons[0] = Button(); - buttons[1] = Button(); + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); } // Get access to (or create) the singleton instance of this class // Accessible inside the ISRs, even though we maybe shouldn't -TwoButton *TwoButton::getInstance() -{ - // Instantiate the class the first time this method is called - static TwoButton *const singletonInstance = new TwoButton; +TwoButton *TwoButton::getInstance() { + // Instantiate the class the first time this method is called + static TwoButton *const singletonInstance = new TwoButton; - return singletonInstance; + return singletonInstance; } // Begin receiving button input // We probably need to do this after sleep, as well as at boot -void TwoButton::start() -{ - if (buttons[0].pin != 0xFF) - attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); +void TwoButton::start() { + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); - if (buttons[1].pin != 0xFF) - attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); } // Stop receiving button input, and run custom sleep code // Called before device sleeps. This might be power-off, or just ESP32 light sleep // Some devices will want to attach interrupts here, for the user button to wake from sleep -void TwoButton::stop() -{ - if (buttons[0].pin != 0xFF) - detachInterrupt(buttons[0].pin); +void TwoButton::stop() { + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); - if (buttons[1].pin != 0xFF) - detachInterrupt(buttons[1].pin); + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // This helper method isn't used by the TweButton class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. -uint8_t TwoButton::getUserButtonPin() -{ - uint8_t pin = 0xFF; // Unset +uint8_t TwoButton::getUserButtonPin() { + uint8_t pin = 0xFF; // Unset - // Use default pin for variant, if no better source + // Use default pin for variant, if no better source #ifdef BUTTON_PIN - pin = BUTTON_PIN; + pin = BUTTON_PIN; #endif - // From userPrefs.jsonc, if set + // From userPrefs.jsonc, if set #ifdef USERPREFS_BUTTON_PIN - pin = USERPREFS_BUTTON_PIN; + pin = USERPREFS_BUTTON_PIN; #endif - // From user's override in device settings, if set - if (config.device.button_gpio) - pin = config.device.button_gpio; + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; - return pin; + return pin; } // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp -void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) -{ - // Prevent the same GPIO being assigned to multiple buttons - // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button - for (uint8_t i = 0; i < whichButton; i++) { - if (buttons[i].pin == pin) { - LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); - return; - } +void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; } + } - assert(whichButton < 2); - buttons[whichButton].pin = pin; - buttons[whichButton].activeLogic = LOW; // Unimplemented + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; // Unimplemented - pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } -void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) -{ - assert(whichButton < 2); - buttons[whichButton].debounceLength = debounceMs; - buttons[whichButton].longpressLength = longpressMs; +void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; } // Set what should happen when a button becomes pressed // Use this to implement a "while held" behavior -void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) -{ - assert(whichButton < 2); - buttons[whichButton].onDown = onDown; +void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) { + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; } // Set what should happen when a button becomes unpressed // Use this to implement a "While held" behavior -void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) -{ - assert(whichButton < 2); - buttons[whichButton].onUp = onUp; +void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) { + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; } // Set what should happen when a "short press" event has occurred -void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) -{ - assert(whichButton < 2); - buttons[whichButton].onShortPress = onShortPress; +void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) { + assert(whichButton < 2); + buttons[whichButton].onShortPress = onShortPress; } // Set what should happen when a "long press" event has fired // Note: this will occur while the button is still held -void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) -{ - assert(whichButton < 2); - buttons[whichButton].onLongPress = onLongPress; +void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) { + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; } // Handle the start of a press to the primary button // Wakes our button thread -void TwoButton::isrPrimary() -{ - static volatile bool isrRunning = false; +void TwoButton::isrPrimary() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButton *b = TwoButton::getInstance(); - if (b->buttons[0].state == State::REST) { - b->buttons[0].state = State::IRQ; - b->buttons[0].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButton *b = TwoButton::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } // Handle the start of a press to the secondary button // Wakes our button thread -void TwoButton::isrSecondary() -{ - static volatile bool isrRunning = false; +void TwoButton::isrSecondary() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButton *b = TwoButton::getInstance(); - if (b->buttons[1].state == State::REST) { - b->buttons[1].state = State::IRQ; - b->buttons[1].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButton *b = TwoButton::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } // Concise method to start our button thread // Follows an ISR, listening for button release -void TwoButton::startThread() -{ - if (!OSThread::enabled) { - OSThread::setInterval(10); - OSThread::enabled = true; - } +void TwoButton::startThread() { + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } } // Concise method to stop our button thread // Called when we no longer need to poll for button release -void TwoButton::stopThread() -{ - if (OSThread::enabled) { - OSThread::disable(); - } +void TwoButton::stopThread() { + if (OSThread::enabled) { + OSThread::disable(); + } - // Reset both buttons manually - // Just in case an IRQ fires during the process of resetting the system - // Can occur with super rapid presses? - buttons[0].state = REST; - buttons[1].state = REST; + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; } // Our button thread // Started by an IRQ, on either button // Polls for button releases // Stops when both buttons released -int32_t TwoButton::runOnce() -{ - constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); +int32_t TwoButton::runOnce() { + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); - // Allow either button to request that our thread should continue polling - bool awaitingRelease = false; + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; - // Check both primary and secondary buttons - for (uint8_t i = 0; i < BUTTON_COUNT; i++) { - switch (buttons[i].state) { - // No action: button has not been pressed - case REST: - break; + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; - // New press detected by interrupt - case IRQ: - powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) - buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) - buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled - awaitingRelease = true; // Mark that polling-for-release should continue - break; + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; - // An existing press continues - // Not held long enough to register as longpress - case POLLING_UNFIRED: { - uint32_t length = millis() - buttons[i].irqAtMillis; + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; - // If button released since last thread tick, - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) - buttons[i].state = State::REST; // Mark that the button has reset - if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, - buttons[i].onShortPress(); // Run callback: short press - } + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onShortPress(); // Run callback: short press + } - // If button not yet released - else { - awaitingRelease = true; // Mark that polling-for-release should continue - if (length >= buttons[i].longpressLength) { - // Run callback: long press (once) - // Then continue waiting for release, to rearm - buttons[i].state = State::POLLING_FIRED; - buttons[i].onLongPress(); - } - } - break; - } - - // Button still held, but duration long enough that longpress event already fired - // Just waiting for release - case POLLING_FIRED: - // Release detected - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].state = State::REST; - buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) - } - // Not yet released, keep polling - else - awaitingRelease = true; - break; + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); } + } + break; } - // If both buttons are now released - // we don't need to waste cpu resources polling - // IRQ will restart this thread when we next need it - if (!awaitingRelease) - stopThread(); + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } - // Run this method again, or don't.. - // Use whatever behavior was previously set by stopThread() or startThread() - return OSThread::interval; + // If both buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); + + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int TwoButton::beforeLightSleep(void *unused) -{ - stop(); - return 0; // Indicates success +int TwoButton::beforeLightSleep(void *unused) { + stop(); + return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) -{ - start(); +int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) { + start(); - // Manually trigger the button-down ISR - // - during light sleep, our ISR is disabled - // - if light sleep ends by button press, pretend our own ISR caught it - // - need to manually confirm by reading pin ourselves, to avoid occasional false positives - // (false positive only when using internal pullup resistors?) - if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) - isrPrimary(); + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); - return 0; // Indicates success + return 0; // Indicates success } #endif diff --git a/src/graphics/niche/Inputs/TwoButton.h b/src/graphics/niche/Inputs/TwoButton.h index ae66adf96..89623d82c 100644 --- a/src/graphics/niche/Inputs/TwoButton.h +++ b/src/graphics/niche/Inputs/TwoButton.h @@ -22,81 +22,78 @@ Interrupt driven #include "Observer.h" -namespace NicheGraphics::Inputs -{ +namespace NicheGraphics::Inputs { -class TwoButton : protected concurrency::OSThread -{ +class TwoButton : protected concurrency::OSThread { +public: + typedef std::function Callback; + + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + + static TwoButton *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + void stop(); // Stop handling button input (disconnect ISRs for sleep) + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); + void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + +private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; + + // Contains info about a specific button + // (Array of this struct below) + class Button { public: - typedef std::function Callback; + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + bool activeLogic = LOW; // Active LOW by default. Currently unimplemented. + uint32_t debounceLength = 50; // Minimum length for shortpress, in ms + uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down - static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition - - static TwoButton *getInstance(); // Create or get the singleton instance - void start(); // Start handling button input - void stop(); // Stop handling button input (disconnect ISRs for sleep) - void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); - void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); - void setHandlerDown(uint8_t whichButton, Callback onDown); - void setHandlerUp(uint8_t whichButton, Callback onUp); - void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); - void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); - - // Disconnect and reconnect interrupts for light sleep -#ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); -#endif - - private: - // Internal state of a specific button - enum State { - REST, // Up, no activity - IRQ, // Down detected, not yet handled - POLLING_UNFIRED, // Down handled, polling for release - POLLING_FIRED, // Longpress fired, button still held - }; - - // Contains info about a specific button - // (Array of this struct below) - class Button - { - public: - // Per-button config - uint8_t pin = 0xFF; // 0xFF: unset - bool activeLogic = LOW; // Active LOW by default. Currently unimplemented. - uint32_t debounceLength = 50; // Minimum length for shortpress, in ms - uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms - volatile State state = State::REST; // Internal state - volatile uint32_t irqAtMillis; // millis() when button went down - - // Per-button event callbacks - static void noop(){}; - std::function onDown = noop; - std::function onUp = noop; - std::function onShortPress = noop; - std::function onLongPress = noop; - }; + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onShortPress = noop; + std::function onLongPress = noop; + }; #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = CallbackObserver(this, &TwoButton::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &TwoButton::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &TwoButton::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButton::afterLightSleep); #endif - int32_t runOnce() override; // Timer method. Polls for button release + int32_t runOnce() override; // Timer method. Polls for button release - void startThread(); // Start polling for release - void stopThread(); // Stop polling for release + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release - static void isrPrimary(); // Detect start of press - static void isrSecondary(); // Detect start of press (optional aux button) + static void isrPrimary(); // Detect start of press + static void isrSecondary(); // Detect start of press (optional aux button) - TwoButton(); // Constructor made private: force use of Button::instance() + TwoButton(); // Constructor made private: force use of Button::instance() - // Info about both buttons - Button buttons[2]; + // Info about both buttons + Button buttons[2]; }; }; // namespace NicheGraphics::Inputs diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp index 287fb943f..ac541e383 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.cpp +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -8,514 +8,481 @@ using namespace NicheGraphics::Inputs; -TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") -{ - // Don't start polling buttons for release immediately - // Assume they are in a "released" state at boot - OSThread::disable(); +TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") { + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); #endif - // Explicitly initialize these, just to keep cppcheck quiet.. - buttons[0] = Button(); - buttons[1] = Button(); - joystick[Direction::UP] = SimpleButton(); - joystick[Direction::DOWN] = SimpleButton(); - joystick[Direction::LEFT] = SimpleButton(); - joystick[Direction::RIGHT] = SimpleButton(); + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); + joystick[Direction::UP] = SimpleButton(); + joystick[Direction::DOWN] = SimpleButton(); + joystick[Direction::LEFT] = SimpleButton(); + joystick[Direction::RIGHT] = SimpleButton(); } // Get access to (or create) the singleton instance of this class // Accessible inside the ISRs, even though we maybe shouldn't -TwoButtonExtended *TwoButtonExtended::getInstance() -{ - // Instantiate the class the first time this method is called - static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; +TwoButtonExtended *TwoButtonExtended::getInstance() { + // Instantiate the class the first time this method is called + static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; - return singletonInstance; + return singletonInstance; } // Begin receiving button input // We probably need to do this after sleep, as well as at boot -void TwoButtonExtended::start() -{ - if (buttons[0].pin != 0xFF) - attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); +void TwoButtonExtended::start() { + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); - if (buttons[1].pin != 0xFF) - attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); - if (joystick[Direction::UP].pin != 0xFF) - attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, - joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::UP].pin != 0xFF) + attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, joystickActiveLogic == LOW ? FALLING : RISING); - if (joystick[Direction::DOWN].pin != 0xFF) - attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, - joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::DOWN].pin != 0xFF) + attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, joystickActiveLogic == LOW ? FALLING : RISING); - if (joystick[Direction::LEFT].pin != 0xFF) - attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, - joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::LEFT].pin != 0xFF) + attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, joystickActiveLogic == LOW ? FALLING : RISING); - if (joystick[Direction::RIGHT].pin != 0xFF) - attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, - joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::RIGHT].pin != 0xFF) + attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, joystickActiveLogic == LOW ? FALLING : RISING); } // Stop receiving button input, and run custom sleep code // Called before device sleeps. This might be power-off, or just ESP32 light sleep // Some devices will want to attach interrupts here, for the user button to wake from sleep -void TwoButtonExtended::stop() -{ - if (buttons[0].pin != 0xFF) - detachInterrupt(buttons[0].pin); +void TwoButtonExtended::stop() { + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); - if (buttons[1].pin != 0xFF) - detachInterrupt(buttons[1].pin); + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); - if (joystick[Direction::UP].pin != 0xFF) - detachInterrupt(joystick[Direction::UP].pin); + if (joystick[Direction::UP].pin != 0xFF) + detachInterrupt(joystick[Direction::UP].pin); - if (joystick[Direction::DOWN].pin != 0xFF) - detachInterrupt(joystick[Direction::DOWN].pin); + if (joystick[Direction::DOWN].pin != 0xFF) + detachInterrupt(joystick[Direction::DOWN].pin); - if (joystick[Direction::LEFT].pin != 0xFF) - detachInterrupt(joystick[Direction::LEFT].pin); + if (joystick[Direction::LEFT].pin != 0xFF) + detachInterrupt(joystick[Direction::LEFT].pin); - if (joystick[Direction::RIGHT].pin != 0xFF) - detachInterrupt(joystick[Direction::RIGHT].pin); + if (joystick[Direction::RIGHT].pin != 0xFF) + detachInterrupt(joystick[Direction::RIGHT].pin); } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method. -uint8_t TwoButtonExtended::getUserButtonPin() -{ - uint8_t pin = 0xFF; // Unset +uint8_t TwoButtonExtended::getUserButtonPin() { + uint8_t pin = 0xFF; // Unset - // Use default pin for variant, if no better source + // Use default pin for variant, if no better source #ifdef BUTTON_PIN - pin = BUTTON_PIN; + pin = BUTTON_PIN; #endif - // From userPrefs.jsonc, if set + // From userPrefs.jsonc, if set #ifdef USERPREFS_BUTTON_PIN - pin = USERPREFS_BUTTON_PIN; + pin = USERPREFS_BUTTON_PIN; #endif - // From user's override in device settings, if set - if (config.device.button_gpio) - pin = config.device.button_gpio; + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; - return pin; + return pin; } // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp -void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) -{ - // Prevent the same GPIO being assigned to multiple buttons - // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button - for (uint8_t i = 0; i < whichButton; i++) { - if (buttons[i].pin == pin) { - LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); - return; - } +void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; } + } - assert(whichButton < 2); - buttons[whichButton].pin = pin; - buttons[whichButton].activeLogic = LOW; + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; - pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } // Configures the wiring and logic of the joystick buttons // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp -void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) -{ - if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || - joystick[Direction::RIGHT].pin == rPin) { - LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); - return; - } +void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) { + if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || + joystick[Direction::RIGHT].pin == rPin) { + LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); + return; + } - joystick[Direction::UP].pin = uPin; - joystick[Direction::DOWN].pin = dPin; - joystick[Direction::LEFT].pin = lPin; - joystick[Direction::RIGHT].pin = rPin; - joystickActiveLogic = LOW; + joystick[Direction::UP].pin = uPin; + joystick[Direction::DOWN].pin = dPin; + joystick[Direction::LEFT].pin = lPin; + joystick[Direction::RIGHT].pin = rPin; + joystickActiveLogic = LOW; - pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); - pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); - pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); - pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); } -void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) -{ - assert(whichButton < 2); - buttons[whichButton].debounceLength = debounceMs; - buttons[whichButton].longpressLength = longpressMs; +void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; } -void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) -{ - joystickDebounceLength = debounceMs; -} +void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) { joystickDebounceLength = debounceMs; } // Set what should happen when a button becomes pressed // Use this to implement a "while held" behavior -void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) -{ - assert(whichButton < 2); - buttons[whichButton].onDown = onDown; +void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) { + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; } // Set what should happen when a button becomes unpressed // Use this to implement a "While held" behavior -void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) -{ - assert(whichButton < 2); - buttons[whichButton].onUp = onUp; +void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) { + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; } // Set what should happen when a "short press" event has occurred -void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) -{ - assert(whichButton < 2); - buttons[whichButton].onPress = onPress; +void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) { + assert(whichButton < 2); + buttons[whichButton].onPress = onPress; } // Set what should happen when a "long press" event has fired // Note: this will occur while the button is still held -void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) -{ - assert(whichButton < 2); - buttons[whichButton].onLongPress = onLongPress; +void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) { + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; } // Set what should happen when a joystick button becomes pressed // Use this to implement a "while held" behavior -void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) -{ - joystick[Direction::UP].onDown = uDown; - joystick[Direction::DOWN].onDown = dDown; - joystick[Direction::LEFT].onDown = lDown; - joystick[Direction::RIGHT].onDown = rDown; +void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) { + joystick[Direction::UP].onDown = uDown; + joystick[Direction::DOWN].onDown = dDown; + joystick[Direction::LEFT].onDown = lDown; + joystick[Direction::RIGHT].onDown = rDown; } // Set what should happen when a joystick button becomes unpressed // Use this to implement a "while held" behavior -void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) -{ - joystick[Direction::UP].onUp = uUp; - joystick[Direction::DOWN].onUp = dUp; - joystick[Direction::LEFT].onUp = lUp; - joystick[Direction::RIGHT].onUp = rUp; +void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) { + joystick[Direction::UP].onUp = uUp; + joystick[Direction::DOWN].onUp = dUp; + joystick[Direction::LEFT].onUp = lUp; + joystick[Direction::RIGHT].onUp = rUp; } // Set what should happen when a "press" event has fired // Note: this will occur while the joystick button is still held -void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) -{ - joystick[Direction::UP].onPress = uPress; - joystick[Direction::DOWN].onPress = dPress; - joystick[Direction::LEFT].onPress = lPress; - joystick[Direction::RIGHT].onPress = rPress; +void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) { + joystick[Direction::UP].onPress = uPress; + joystick[Direction::DOWN].onPress = dPress; + joystick[Direction::LEFT].onPress = lPress; + joystick[Direction::RIGHT].onPress = rPress; } // Handle the start of a press to the primary button // Wakes our button thread -void TwoButtonExtended::isrPrimary() -{ - static volatile bool isrRunning = false; +void TwoButtonExtended::isrPrimary() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->buttons[0].state == State::REST) { - b->buttons[0].state = State::IRQ; - b->buttons[0].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } // Handle the start of a press to the secondary button // Wakes our button thread -void TwoButtonExtended::isrSecondary() -{ - static volatile bool isrRunning = false; +void TwoButtonExtended::isrSecondary() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->buttons[1].state == State::REST) { - b->buttons[1].state = State::IRQ; - b->buttons[1].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } // Handle the start of a press to the joystick buttons // Also wakes our button thread -void TwoButtonExtended::isrJoystickUp() -{ - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickUp() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::UP].state == State::REST) { - b->joystick[Direction::UP].state = State::IRQ; - b->joystick[Direction::UP].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::UP].state == State::REST) { + b->joystick[Direction::UP].state = State::IRQ; + b->joystick[Direction::UP].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } -void TwoButtonExtended::isrJoystickDown() -{ - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickDown() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::DOWN].state == State::REST) { - b->joystick[Direction::DOWN].state = State::IRQ; - b->joystick[Direction::DOWN].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::DOWN].state == State::REST) { + b->joystick[Direction::DOWN].state = State::IRQ; + b->joystick[Direction::DOWN].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } -void TwoButtonExtended::isrJoystickLeft() -{ - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickLeft() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::LEFT].state == State::REST) { - b->joystick[Direction::LEFT].state = State::IRQ; - b->joystick[Direction::LEFT].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::LEFT].state == State::REST) { + b->joystick[Direction::LEFT].state = State::IRQ; + b->joystick[Direction::LEFT].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } -void TwoButtonExtended::isrJoystickRight() -{ - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickRight() { + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::RIGHT].state == State::REST) { - b->joystick[Direction::RIGHT].state = State::IRQ; - b->joystick[Direction::RIGHT].irqAtMillis = millis(); - b->startThread(); - } - isrRunning = false; + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::RIGHT].state == State::REST) { + b->joystick[Direction::RIGHT].state = State::IRQ; + b->joystick[Direction::RIGHT].irqAtMillis = millis(); + b->startThread(); } + isrRunning = false; + } } // Concise method to start our button thread // Follows an ISR, listening for button release -void TwoButtonExtended::startThread() -{ - if (!OSThread::enabled) { - OSThread::setInterval(10); - OSThread::enabled = true; - } +void TwoButtonExtended::startThread() { + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } } // Concise method to stop our button thread // Called when we no longer need to poll for button release -void TwoButtonExtended::stopThread() -{ - if (OSThread::enabled) { - OSThread::disable(); - } +void TwoButtonExtended::stopThread() { + if (OSThread::enabled) { + OSThread::disable(); + } - // Reset both buttons manually - // Just in case an IRQ fires during the process of resetting the system - // Can occur with super rapid presses? - buttons[0].state = REST; - buttons[1].state = REST; - joystick[Direction::UP].state = REST; - joystick[Direction::DOWN].state = REST; - joystick[Direction::LEFT].state = REST; - joystick[Direction::RIGHT].state = REST; + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; + joystick[Direction::UP].state = REST; + joystick[Direction::DOWN].state = REST; + joystick[Direction::LEFT].state = REST; + joystick[Direction::RIGHT].state = REST; } // Our button thread // Started by an IRQ, on either button // Polls for button releases // Stops when both buttons released -int32_t TwoButtonExtended::runOnce() -{ - constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); - constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); +int32_t TwoButtonExtended::runOnce() { + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); + constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); - // Allow either button to request that our thread should continue polling - bool awaitingRelease = false; + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; - // Check both primary and secondary buttons - for (uint8_t i = 0; i < BUTTON_COUNT; i++) { - switch (buttons[i].state) { - // No action: button has not been pressed - case REST: - break; + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; - // New press detected by interrupt - case IRQ: - powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) - buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) - buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled - awaitingRelease = true; // Mark that polling-for-release should continue - break; + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; - // An existing press continues - // Not held long enough to register as longpress - case POLLING_UNFIRED: { - uint32_t length = millis() - buttons[i].irqAtMillis; + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; - // If button released since last thread tick, - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) - buttons[i].state = State::REST; // Mark that the button has reset - if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, - buttons[i].onPress(); // Run callback: press - } - // If button not yet released - else { - awaitingRelease = true; // Mark that polling-for-release should continue - if (length >= buttons[i].longpressLength) { - // Run callback: long press (once) - // Then continue waiting for release, to rearm - buttons[i].state = State::POLLING_FIRED; - buttons[i].onLongPress(); - } - } - break; - } - - // Button still held, but duration long enough that longpress event already fired - // Just waiting for release - case POLLING_FIRED: - // Release detected - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].state = State::REST; - buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) - } - // Not yet released, keep polling - else - awaitingRelease = true; - break; + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onPress(); // Run callback: press + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); } + } + break; } - // Check all the joystick directions - for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { - switch (joystick[i].state) { - // No action: button has not been pressed - case REST: - break; + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } - // New press detected by interrupt - case IRQ: - powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) - joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) - joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled - awaitingRelease = true; // Mark that polling-for-release should continue - break; + // Check all the joystick directions + for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { + switch (joystick[i].state) { + // No action: button has not been pressed + case REST: + break; - // An existing press continues - // Not held long enough to register as press - case POLLING_UNFIRED: { - uint32_t length = millis() - joystick[i].irqAtMillis; + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) + joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; - // If button released since last thread tick, - if (digitalRead(joystick[i].pin) != joystickActiveLogic) { - joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) - joystick[i].state = State::REST; // Mark that the button has reset - } - // If button not yet released - else { - awaitingRelease = true; // Mark that polling-for-release should continue - if (length >= joystickDebounceLength) { - // Run callback: long press (once) - // Then continue waiting for release, to rearm - joystick[i].state = State::POLLING_FIRED; - joystick[i].onPress(); - } - } - break; - } - - // Button still held after press - // Just waiting for release - case POLLING_FIRED: - // Release detected - if (digitalRead(joystick[i].pin) != joystickActiveLogic) { - joystick[i].state = State::REST; - joystick[i].onUp(); // Callback: release of hold - } - // Not yet released, keep polling - else - awaitingRelease = true; - break; + // An existing press continues + // Not held long enough to register as press + case POLLING_UNFIRED: { + uint32_t length = millis() - joystick[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) + joystick[i].state = State::REST; // Mark that the button has reset + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= joystickDebounceLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + joystick[i].state = State::POLLING_FIRED; + joystick[i].onPress(); } + } + break; } - // If all buttons are now released - // we don't need to waste cpu resources polling - // IRQ will restart this thread when we next need it - if (!awaitingRelease) - stopThread(); + // Button still held after press + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].state = State::REST; + joystick[i].onUp(); // Callback: release of hold + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } - // Run this method again, or don't.. - // Use whatever behavior was previously set by stopThread() or startThread() - return OSThread::interval; + // If all buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); + + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int TwoButtonExtended::beforeLightSleep(void *unused) -{ - stop(); - return 0; // Indicates success +int TwoButtonExtended::beforeLightSleep(void *unused) { + stop(); + return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) -{ - start(); +int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) { + start(); - // Manually trigger the button-down ISR - // - during light sleep, our ISR is disabled - // - if light sleep ends by button press, pretend our own ISR caught it - // - need to manually confirm by reading pin ourselves, to avoid occasional false positives - // (false positive only when using internal pullup resistors?) - if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) - isrPrimary(); + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); - return 0; // Indicates success + return 0; // Indicates success } #endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h index 23fd78a2a..b47dbad92 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.h +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -30,105 +30,100 @@ Interrupt driven #include "Observer.h" -namespace NicheGraphics::Inputs -{ +namespace NicheGraphics::Inputs { -class TwoButtonExtended : protected concurrency::OSThread -{ +class TwoButtonExtended : protected concurrency::OSThread { +public: + typedef std::function Callback; + + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + + static TwoButtonExtended *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + void stop(); // Stop handling button input (disconnect ISRs for sleep) + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); + void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); + void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setJoystickDebounce(uint32_t debounceMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); + void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); + void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + +private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; + + // Joystick Directions + enum Direction { UP = 0, DOWN, LEFT, RIGHT }; + + // Data used for direction (single-action) buttons + class SimpleButton { public: - typedef std::function Callback; + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down - static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onPress = noop; + }; - static TwoButtonExtended *getInstance(); // Create or get the singleton instance - void start(); // Start handling button input - void stop(); // Stop handling button input (disconnect ISRs for sleep) - void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); - void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); - void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); - void setJoystickDebounce(uint32_t debounceMs); - void setHandlerDown(uint8_t whichButton, Callback onDown); - void setHandlerUp(uint8_t whichButton, Callback onUp); - void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); - void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); - void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); - void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); - void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + // Data used for double-action buttons + class Button : public SimpleButton { + public: + // Per-button extended config + bool activeLogic = LOW; // Active LOW by default. + uint32_t debounceLength = 50; // Minimum length for shortpress in ms + uint32_t longpressLength = 500; // Time until longpress in ms - // Disconnect and reconnect interrupts for light sleep -#ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); -#endif - - private: - // Internal state of a specific button - enum State { - REST, // Up, no activity - IRQ, // Down detected, not yet handled - POLLING_UNFIRED, // Down handled, polling for release - POLLING_FIRED, // Longpress fired, button still held - }; - - // Joystick Directions - enum Direction { UP = 0, DOWN, LEFT, RIGHT }; - - // Data used for direction (single-action) buttons - class SimpleButton - { - public: - // Per-button config - uint8_t pin = 0xFF; // 0xFF: unset - volatile State state = State::REST; // Internal state - volatile uint32_t irqAtMillis; // millis() when button went down - - // Per-button event callbacks - static void noop(){}; - std::function onDown = noop; - std::function onUp = noop; - std::function onPress = noop; - }; - - // Data used for double-action buttons - class Button : public SimpleButton - { - public: - // Per-button extended config - bool activeLogic = LOW; // Active LOW by default. - uint32_t debounceLength = 50; // Minimum length for shortpress in ms - uint32_t longpressLength = 500; // Time until longpress in ms - - // Per-button event callbacks - std::function onLongPress = noop; - }; + // Per-button event callbacks + std::function onLongPress = noop; + }; #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &TwoButtonExtended::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButtonExtended::afterLightSleep); #endif - int32_t runOnce() override; // Timer method. Polls for button release + int32_t runOnce() override; // Timer method. Polls for button release - void startThread(); // Start polling for release - void stopThread(); // Stop polling for release + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release - static void isrPrimary(); // User Button ISR - static void isrSecondary(); // optional aux button or joystick center - static void isrJoystickUp(); - static void isrJoystickDown(); - static void isrJoystickLeft(); - static void isrJoystickRight(); + static void isrPrimary(); // User Button ISR + static void isrSecondary(); // optional aux button or joystick center + static void isrJoystickUp(); + static void isrJoystickDown(); + static void isrJoystickLeft(); + static void isrJoystickRight(); - TwoButtonExtended(); // Constructor made private: force use of Button::instance() + TwoButtonExtended(); // Constructor made private: force use of Button::instance() - // Info about both buttons - Button buttons[2]; - bool joystickActiveLogic = LOW; // Active LOW by default - uint32_t joystickDebounceLength = 50; // time until press in ms - SimpleButton joystick[4]; + // Info about both buttons + Button buttons[2]; + bool joystickActiveLogic = LOW; // Active LOW by default + uint32_t joystickDebounceLength = 50; // time until press in ms + SimpleButton joystick[4]; }; }; // namespace NicheGraphics::Inputs diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp index 50998930d..21665ee59 100644 --- a/src/graphics/niche/Utils/CannedMessageStore.cpp +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -12,152 +12,142 @@ using namespace NicheGraphics; // Location of the file which stores the canned messages on flash static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; -CannedMessageStore::CannedMessageStore() -{ +CannedMessageStore::CannedMessageStore() { #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe(adminModule); #endif - // Load & parse messages from flash - load(); + // Load & parse messages from flash + load(); } // Get access to (or create) the singleton instance of this class -CannedMessageStore *CannedMessageStore::getInstance() -{ - // Instantiate the class the first time this method is called - static CannedMessageStore *const singletonInstance = new CannedMessageStore; +CannedMessageStore *CannedMessageStore::getInstance() { + // Instantiate the class the first time this method is called + static CannedMessageStore *const singletonInstance = new CannedMessageStore; - return singletonInstance; + return singletonInstance; } // Access canned messages by index // Consumer should check CannedMessageStore::size to avoid accessing out of bounds -const std::string &CannedMessageStore::at(uint8_t i) -{ - assert(i < messages.size()); - return messages.at(i); +const std::string &CannedMessageStore::at(uint8_t i) { + assert(i < messages.size()); + return messages.at(i); } // Number of canned message strings available -uint8_t CannedMessageStore::size() -{ - return messages.size(); -} +uint8_t CannedMessageStore::size() { return messages.size(); } // Load canned message data from flash, and parse into the individual strings -void CannedMessageStore::load() -{ - // In case we're reloading - messages.clear(); +void CannedMessageStore::load() { + // In case we're reloading + messages.clear(); - // Attempt to load the bulk canned message data from flash - meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; - LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, - sizeof(meshtastic_CannedMessageModuleConfig), - &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + // Attempt to load the bulk canned message data from flash + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + LoadFileResult result = + nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, sizeof(meshtastic_CannedMessageModuleConfig), + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); - // Abort if nothing to load - if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) - return; + // Abort if nothing to load + if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) + return; - // Split into individual canned messages - // These are concatenated when stored in flash, using '|' as a delimiter - std::string s; - for (char c : cannedMessageModuleConfig.messages) { // Character by character + // Split into individual canned messages + // These are concatenated when stored in flash, using '|' as a delimiter + std::string s; + for (char c : cannedMessageModuleConfig.messages) { // Character by character - // If found end of a string - if (c == '|' || c == '\0') { - // Copy into the vector (if non-empty) - if (!s.empty()) - messages.push_back(s); + // If found end of a string + if (c == '|' || c == '\0') { + // Copy into the vector (if non-empty) + if (!s.empty()) + messages.push_back(s); - // Reset the string builder - s.clear(); + // Reset the string builder + s.clear(); - // End of data, all strings processed - if (c == 0) - break; - } - - // Otherwise, append char (continue building string) - else - s.push_back(c); + // End of data, all strings processed + if (c == 0) + break; } + + // Otherwise, append char (continue building string) + else + s.push_back(c); + } } // Handle incoming admin messages // We get these as an observer of AdminModule // It's our responsibility to handle setting and getting of canned messages via the client API -// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics -int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) -{ - switch (data->request->which_payload_variant) { +// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for +// NicheGraphics +int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) { + switch (data->request->which_payload_variant) { - // Client API changing the canned messages - case meshtastic_AdminMessage_set_canned_message_module_messages_tag: - handleSet(data->request); - *data->result = AdminMessageHandleResult::HANDLED; - break; + // Client API changing the canned messages + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + handleSet(data->request); + *data->result = AdminMessageHandleResult::HANDLED; + break; - // Client API wants to know the current canned messages - case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: - handleGet(data->response); - *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; - break; + // Client API wants to know the current canned messages + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + handleGet(data->response); + *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; - default: - break; - } + default: + break; + } - return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } // Client API changing the canned messages -void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) -{ - // Copy into the correct struct (for writing to flash as protobuf) - meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; - strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, - sizeof(cannedMessageModuleConfig.messages)); +void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) { + // Copy into the correct struct (for writing to flash as protobuf) + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, sizeof(cannedMessageModuleConfig.messages)); - // Ensure the directory exists + // Ensure the directory exists #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - // Write to flash - nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, - &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + // Write to flash + nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg, + &cannedMessageModuleConfig); - // Reload from flash, to update the canned messages in RAM - // (This is a lazy way to handle it) - load(); + // Reload from flash, to update the canned messages in RAM + // (This is a lazy way to handle it) + load(); } // Client API wants to know the current canned messages // We're reconstructing the monolithic canned message string from our copy of the messages in RAM // Lazy, but more convenient that reloading the monolithic string from flash just for this -void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) -{ - // Merge the canned messages back into the delimited format expected - std::string merged; - if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 - merged.reserve(201); - for (std::string &s : messages) { - merged += s; - merged += '|'; - } - merged.pop_back(); // Drop the final delimiter (loop added one too many) +void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) { + // Merge the canned messages back into the delimited format expected + std::string merged; + if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 + merged.reserve(201); + for (std::string &s : messages) { + merged += s; + merged += '|'; } + merged.pop_back(); // Drop the final delimiter (loop added one too many) + } - // Place the data into the response - // This response is scoped to AdminModule::handleReceivedProtobuf - // We were passed reference to it via the observable - response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; - strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); + // Place the data into the response + // This response is scoped to AdminModule::handleReceivedProtobuf + // We were passed reference to it via the observable + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); } #endif \ No newline at end of file diff --git a/src/graphics/niche/Utils/CannedMessageStore.h b/src/graphics/niche/Utils/CannedMessageStore.h index c00e1cf5c..f6715bb3f 100644 --- a/src/graphics/niche/Utils/CannedMessageStore.h +++ b/src/graphics/niche/Utils/CannedMessageStore.h @@ -22,31 +22,29 @@ The necessary interaction with the AdminModule is done as an observer. #include "modules/AdminModule.h" -namespace NicheGraphics -{ +namespace NicheGraphics { -class CannedMessageStore -{ - public: - static CannedMessageStore *getInstance(); // Create or get the singleton instance - const std::string &at(uint8_t i); // Get canned message at index - uint8_t size(); // Get total number of canned messages +class CannedMessageStore { +public: + static CannedMessageStore *getInstance(); // Create or get the singleton instance + const std::string &at(uint8_t i); // Get canned message at index + uint8_t size(); // Get total number of canned messages - int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages - private: - CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() +private: + CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() - void load(); // Load from flash, and parse + void load(); // Load from flash, and parse - void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages - void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages + void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages + void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages - std::vector messages; + std::vector messages; - // Get notified of incoming admin messages, to get / set canned messages - CallbackObserver adminMessageObserver = - CallbackObserver(this, &CannedMessageStore::onAdminMessage); + // Get notified of incoming admin messages, to get / set canned messages + CallbackObserver adminMessageObserver = + CallbackObserver(this, &CannedMessageStore::onAdminMessage); }; }; // namespace NicheGraphics diff --git a/src/graphics/niche/Utils/FlashData.h b/src/graphics/niche/Utils/FlashData.h index 233d0922e..77fc3d89f 100644 --- a/src/graphics/niche/Utils/FlashData.h +++ b/src/graphics/niche/Utils/FlashData.h @@ -16,156 +16,149 @@ Avoid bloating everyone's protobuf code for our one-off UI implementations #include "SPILock.h" #include "SafeFile.h" -namespace NicheGraphics -{ +namespace NicheGraphics { -template class FlashData -{ - private: - static std::string getFilename(const char *label) - { - std::string filename; - filename += "/NicheGraphics"; - filename += "/"; - filename += label; - filename += ".data"; +template class FlashData { +private: + static std::string getFilename(const char *label) { + std::string filename; + filename += "/NicheGraphics"; + filename += "/"; + filename += label; + filename += ".data"; - return filename; - } + return filename; + } - static uint32_t getHash(T *data) - { - uint32_t hash = 0; + static uint32_t getHash(T *data) { + uint32_t hash = 0; - // Sum all bytes of the image buffer together - for (uint32_t i = 0; i < sizeof(T); i++) - hash ^= ((uint8_t *)data)[i] + 1; + // Sum all bytes of the image buffer together + for (uint32_t i = 0; i < sizeof(T); i++) + hash ^= ((uint8_t *)data)[i] + 1; - return hash; - } + return hash; + } - public: - static bool load(T *data, const char *label) - { - // Take firmware's SPI lock - concurrency::LockGuard guard(spiLock); - - // Set false if we run into issues - bool okay = true; - - // Get a filename based on the label - std::string filename = getFilename(label); - -#ifdef FSCom - - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_WARN("'%s' not found. Using default values", filename.c_str()); - okay = false; - return okay; - } - - // Open the file - auto f = FSCom.open(filename.c_str(), FILE_O_READ); - - // If opened, start reading - if (f) { - LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str()); - - // Create an object which will received data from flash - // We read here first, so we can verify the checksum, without committing to overwriting the *data object - // Allows us to retain any defaults that might be set after we declared *data, but before loading settings, - // in case the flash values are corrupt - T flashData; - - // Read the actual data - f.readBytes((char *)&flashData, sizeof(T)); - - // Read the hash - uint32_t savedHash = 0; - f.readBytes((char *)&savedHash, sizeof(savedHash)); - - // Calculate hash of the loaded data, then compare with the saved hash - // If hash looks good, copy the values to the main data object - uint32_t calculatedHash = getHash(&flashData); - if (savedHash != calculatedHash) { - LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str()); - okay = false; - } else - *data = flashData; - - f.close(); - } else { - LOG_ERROR("Could not open / read %s", filename.c_str()); - okay = false; - } -#else - LOG_ERROR("Filesystem not implemented"); - state = LoadFileState::NO_FILESYSTEM; - okay = false; -#endif - return okay; - } - - // Save module's custom data (settings?) to flash. Doesn't use protobufs - // Takes the firmware's SPI lock, in case the files are stored on SD card - // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally. - static void save(T *data, const char *label) - { - // Get a filename based on the label - std::string filename = getFilename(label); - -#ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/NicheGraphics"); - spiLock->unlock(); - - auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. - - LOG_INFO("Saving %s", filename.c_str()); - - // Calculate a hash of the data - uint32_t hash = getHash(data); - - spiLock->lock(); - f.write((uint8_t *)data, sizeof(T)); // Write the actual data - f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash - spiLock->unlock(); - - bool writeSucceeded = f.close(); - - if (!writeSucceeded) { - LOG_ERROR("Can't write data!"); - } -#else - LOG_ERROR("ERROR: Filesystem not implemented\n"); -#endif - } -}; - -// Erase contents of the NicheGraphics data directory -inline void clearFlashData() -{ - - // Take firmware's SPI lock, in case the files are stored on SD card +public: + static bool load(T *data, const char *label) { + // Take firmware's SPI lock concurrency::LockGuard guard(spiLock); + // Set false if we run into issues + bool okay = true; + + // Get a filename based on the label + std::string filename = getFilename(label); + #ifdef FSCom - File dir = FSCom.open("/NicheGraphics"); // Open the directory - File file = dir.openNextFile(); // Attempt to open the first file in the directory - // While the directory still contains files - while (file) { - std::string path = "/NicheGraphics/"; - path += file.name(); - LOG_DEBUG("Erasing %s", path.c_str()); - file.close(); - FSCom.remove(path.c_str()); + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_WARN("'%s' not found. Using default values", filename.c_str()); + okay = false; + return okay; + } - file = dir.openNextFile(); + // Open the file + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + + // If opened, start reading + if (f) { + LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str()); + + // Create an object which will received data from flash + // We read here first, so we can verify the checksum, without committing to overwriting the *data object + // Allows us to retain any defaults that might be set after we declared *data, but before loading settings, + // in case the flash values are corrupt + T flashData; + + // Read the actual data + f.readBytes((char *)&flashData, sizeof(T)); + + // Read the hash + uint32_t savedHash = 0; + f.readBytes((char *)&savedHash, sizeof(savedHash)); + + // Calculate hash of the loaded data, then compare with the saved hash + // If hash looks good, copy the values to the main data object + uint32_t calculatedHash = getHash(&flashData); + if (savedHash != calculatedHash) { + LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str()); + okay = false; + } else + *data = flashData; + + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename.c_str()); + okay = false; + } +#else + LOG_ERROR("Filesystem not implemented"); + state = LoadFileState::NO_FILESYSTEM; + okay = false; +#endif + return okay; + } + + // Save module's custom data (settings?) to flash. Doesn't use protobufs + // Takes the firmware's SPI lock, in case the files are stored on SD card + // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally. + static void save(T *data, const char *label) { + // Get a filename based on the label + std::string filename = getFilename(label); + +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/NicheGraphics"); + spiLock->unlock(); + + auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. + + LOG_INFO("Saving %s", filename.c_str()); + + // Calculate a hash of the data + uint32_t hash = getHash(data); + + spiLock->lock(); + f.write((uint8_t *)data, sizeof(T)); // Write the actual data + f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash + spiLock->unlock(); + + bool writeSucceeded = f.close(); + + if (!writeSucceeded) { + LOG_ERROR("Can't write data!"); } #else LOG_ERROR("ERROR: Filesystem not implemented\n"); +#endif + } +}; + +// Erase contents of the NicheGraphics data directory +inline void clearFlashData() { + + // Take firmware's SPI lock, in case the files are stored on SD card + concurrency::LockGuard guard(spiLock); + +#ifdef FSCom + File dir = FSCom.open("/NicheGraphics"); // Open the directory + File file = dir.openNextFile(); // Attempt to open the first file in the directory + + // While the directory still contains files + while (file) { + std::string path = "/NicheGraphics/"; + path += file.name(); + LOG_DEBUG("Erasing %s", path.c_str()); + file.close(); + FSCom.remove(path.c_str()); + + file = dir.openNextFile(); + } +#else + LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 5654fa02a..c33de3db0 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -18,120 +18,110 @@ DeviceScreen *deviceScreen = nullptr; #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep -CallbackObserver tftSleepObserver = - CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); +CallbackObserver tftSleepObserver = CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); CallbackObserver endSleepObserver = CallbackObserver(deviceScreen, &DeviceScreen::wakeUp); #endif -void tft_task_handler(void *param = nullptr) -{ - while (true) { - spiLock->lock(); - deviceScreen->task_handler(); - spiLock->unlock(); - deviceScreen->sleep(); - } +void tft_task_handler(void *param = nullptr) { + while (true) { + spiLock->lock(); + deviceScreen->task_handler(); + spiLock->unlock(); + deviceScreen->sleep(); + } } -void tftSetup(void) -{ +void tftSetup(void) { #ifndef ARCH_PORTDUINO - deviceScreen = &DeviceScreen::create(); + deviceScreen = &DeviceScreen::create(); + PacketAPI::create(PacketServer::init()); + deviceScreen->init(new PacketClient); +#else + if (portduino_config.displayPanel != no_screen) { + DisplayDriverConfig displayConfig; + static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; + static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; +#if defined(USE_X11) + if (portduino_config.displayPanel == x11) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); + else + displayConfig.device(DisplayDriverConfig::device_t::X11); + } else +#elif defined(USE_FRAMEBUFFER) + if (portduino_config.displayPanel == fb) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = + DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, (uint16_t)portduino_config.displayHeight); + else + displayConfig.device(DisplayDriverConfig::device_t::FB); + } else +#endif + { + displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) + .panel( + DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], + .panel_width = (uint16_t)portduino_config.displayWidth, + .panel_height = (uint16_t)portduino_config.displayHeight, + .rotation = (bool)portduino_config.displayRotate, + .pin_cs = (int16_t)portduino_config.displayCS.pin, + .pin_rst = (int16_t)portduino_config.displayReset.pin, + .offset_x = (uint16_t)portduino_config.displayOffsetX, + .offset_y = (uint16_t)portduino_config.displayOffsetY, + .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, + .invert = portduino_config.displayInvert ? true : false, + .rgb_order = (bool)portduino_config.displayRGBOrder, + .dlen_16bit = portduino_config.displayPanel == ili9486 || portduino_config.displayPanel == ili9488}) + .bus(DisplayDriverConfig::bus_config_t{ + .freq_write = (uint32_t)portduino_config.displayBusFrequency, + .freq_read = 16000000, + .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, .use_lock = true, .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) + .input( + DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, .pointerDevice = portduino_config.pointerDevice}) + .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, + .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, + .invert = (bool)portduino_config.displayBacklightInvert}); + if (portduino_config.touchscreenI2CAddr == -1) { + displayConfig.touch(DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, + .spi{ + .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, + }, + .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); + } else { + displayConfig.touch(DisplayDriverConfig::touch_config_t{ + .type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, + .x_min = 0, + .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth : portduino_config.displayHeight) - 1), + .y_min = 0, + .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight : portduino_config.displayWidth) - 1), + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, + .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); + } + } + deviceScreen = &DeviceScreen::create(&displayConfig); PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); -#else - if (portduino_config.displayPanel != no_screen) { - DisplayDriverConfig displayConfig; - static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", - "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; - static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; -#if defined(USE_X11) - if (portduino_config.displayPanel == x11) { - if (portduino_config.displayWidth && portduino_config.displayHeight) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, - (uint16_t)portduino_config.displayHeight); - else - displayConfig.device(DisplayDriverConfig::device_t::X11); - } else -#elif defined(USE_FRAMEBUFFER) - if (portduino_config.displayPanel == fb) { - if (portduino_config.displayWidth && portduino_config.displayHeight) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, - (uint16_t)portduino_config.displayHeight); - else - displayConfig.device(DisplayDriverConfig::device_t::FB); - } else -#endif - { - displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) - .panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], - .panel_width = (uint16_t)portduino_config.displayWidth, - .panel_height = (uint16_t)portduino_config.displayHeight, - .rotation = (bool)portduino_config.displayRotate, - .pin_cs = (int16_t)portduino_config.displayCS.pin, - .pin_rst = (int16_t)portduino_config.displayReset.pin, - .offset_x = (uint16_t)portduino_config.displayOffsetX, - .offset_y = (uint16_t)portduino_config.displayOffsetY, - .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, - .invert = portduino_config.displayInvert ? true : false, - .rgb_order = (bool)portduino_config.displayRGBOrder, - .dlen_16bit = portduino_config.displayPanel == ili9486 || - portduino_config.displayPanel == ili9488}) - .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency, - .freq_read = 16000000, - .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, - .use_lock = true, - .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) - .input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, - .pointerDevice = portduino_config.pointerDevice}) - .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, - .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, - .invert = (bool)portduino_config.displayBacklightInvert}); - if (portduino_config.touchscreenI2CAddr == -1) { - displayConfig.touch( - DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], - .freq = (uint32_t)portduino_config.touchscreenBusFrequency, - .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, - .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, - .spi{ - .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, - }, - .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); - } else { - displayConfig.touch(DisplayDriverConfig::touch_config_t{ - .type = touch[portduino_config.touchscreenModule], - .freq = (uint32_t)portduino_config.touchscreenBusFrequency, - .x_min = 0, - .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth - : portduino_config.displayHeight) - - 1), - .y_min = 0, - .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight - : portduino_config.displayWidth) - - 1), - .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, - .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, - .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); - } - } - deviceScreen = &DeviceScreen::create(&displayConfig); - PacketAPI::create(PacketServer::init()); - deviceScreen->init(new PacketClient); - } else { - LOG_INFO("Running without TFT display!"); - } + } else { + LOG_INFO("Running without TFT display!"); + } #endif - if (deviceScreen) { + if (deviceScreen) { #ifdef ARCH_ESP32 - tftSleepObserver.observe(¬ifyLightSleep); - endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + tftSleepObserver.observe(¬ifyLightSleep); + endSleepObserver.observe(¬ifyLightSleepEnd); + xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) - std::thread *tft_task = new std::thread([] { tft_task_handler(); }); + std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif - } + } } #endif \ No newline at end of file diff --git a/src/input/BBQ10Keyboard.cpp b/src/input/BBQ10Keyboard.cpp index 8f8399aef..c717b3866 100644 --- a/src/input/BBQ10Keyboard.cpp +++ b/src/input/BBQ10Keyboard.cpp @@ -37,145 +37,119 @@ BBQ10Keyboard::BBQ10Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) {} -void BBQ10Keyboard::begin(uint8_t addr, TwoWire *wire) -{ - m_addr = addr; - m_wire = wire; +void BBQ10Keyboard::begin(uint8_t addr, TwoWire *wire) { + m_addr = addr; + m_wire = wire; - m_wire->begin(); + m_wire->begin(); - reset(); + reset(); } -void BBQ10Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) -{ - m_addr = addr; - m_wire = nullptr; - writeCallback = w; - readCallback = r; - reset(); +void BBQ10Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); } -void BBQ10Keyboard::reset() -{ - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(_REG_RST); - m_wire->endTransmission(); - } - if (writeCallback) { - uint8_t data = 0; - writeCallback(m_addr, _REG_RST, &data, 0); - } - delay(100); - writeRegister(_REG_CFG, readRegister8(_REG_CFG) | CFG_REPORT_MODS); - delay(100); +void BBQ10Keyboard::reset() { + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(_REG_RST); + m_wire->endTransmission(); + } + if (writeCallback) { + uint8_t data = 0; + writeCallback(m_addr, _REG_RST, &data, 0); + } + delay(100); + writeRegister(_REG_CFG, readRegister8(_REG_CFG) | CFG_REPORT_MODS); + delay(100); } -void BBQ10Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const -{ - pinMode(pin, INPUT_PULLUP); - ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); +void BBQ10Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const { + pinMode(pin, INPUT_PULLUP); + ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); } -void BBQ10Keyboard::detachInterrupt(uint8_t pin) const -{ - ::detachInterrupt(pin); -} +void BBQ10Keyboard::detachInterrupt(uint8_t pin) const { ::detachInterrupt(pin); } -void BBQ10Keyboard::clearInterruptStatus() -{ - writeRegister(_REG_INT, 0x00); -} +void BBQ10Keyboard::clearInterruptStatus() { writeRegister(_REG_INT, 0x00); } -uint8_t BBQ10Keyboard::status() const -{ - return readRegister8(_REG_KEY); -} +uint8_t BBQ10Keyboard::status() const { return readRegister8(_REG_KEY); } -uint8_t BBQ10Keyboard::keyCount() const -{ - return status() & KEY_COUNT_MASK; -} +uint8_t BBQ10Keyboard::keyCount() const { return status() & KEY_COUNT_MASK; } -BBQ10Keyboard::KeyEvent BBQ10Keyboard::keyEvent() const -{ - KeyEvent event = {.key = '\0', .state = StateIdle}; - - if (keyCount() == 0) - return event; - - const uint16_t buf = readRegister16(_REG_FIF); - event.key = buf >> 8; - event.state = KeyState(buf & 0xFF); +BBQ10Keyboard::KeyEvent BBQ10Keyboard::keyEvent() const { + KeyEvent event = {.key = '\0', .state = StateIdle}; + if (keyCount() == 0) return event; + + const uint16_t buf = readRegister16(_REG_FIF); + event.key = buf >> 8; + event.state = KeyState(buf & 0xFF); + + return event; } -float BBQ10Keyboard::backlight() const -{ - return readRegister8(_REG_BKL) / 255.0f; +float BBQ10Keyboard::backlight() const { return readRegister8(_REG_BKL) / 255.0f; } + +void BBQ10Keyboard::setBacklight(float value) { writeRegister(_REG_BKL, value * 255); } + +uint8_t BBQ10Keyboard::readRegister8(uint8_t reg) const { + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); + + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) + return 0; + + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; } -void BBQ10Keyboard::setBacklight(float value) -{ - writeRegister(_REG_BKL, value * 255); +uint16_t BBQ10Keyboard::readRegister16(uint8_t reg) const { + uint8_t data[2] = {0}; + // uint8_t low = 0, high = 0; + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); + + m_wire->requestFrom(m_addr, (uint8_t)2); + if (m_wire->available() < 2) + return 0; + data[0] = m_wire->read(); + data[1] = m_wire->read(); + } + if (readCallback) { + readCallback(m_addr, reg, data, 2); + } + return (data[1] << 8) | data[0]; } -uint8_t BBQ10Keyboard::readRegister8(uint8_t reg) const -{ - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); +void BBQ10Keyboard::writeRegister(uint8_t reg, uint8_t value) { + uint8_t data[2]; + data[0] = reg | _WRITE_MASK; + data[1] = value; - m_wire->requestFrom(m_addr, (uint8_t)1); - if (m_wire->available() < 1) - return 0; - - return m_wire->read(); - } - if (readCallback) { - uint8_t data; - readCallback(m_addr, reg, &data, 1); - return data; - } - return 0; -} - -uint16_t BBQ10Keyboard::readRegister16(uint8_t reg) const -{ - uint8_t data[2] = {0}; - // uint8_t low = 0, high = 0; - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); - - m_wire->requestFrom(m_addr, (uint8_t)2); - if (m_wire->available() < 2) - return 0; - data[0] = m_wire->read(); - data[1] = m_wire->read(); - } - if (readCallback) { - readCallback(m_addr, reg, data, 2); - } - return (data[1] << 8) | data[0]; -} - -void BBQ10Keyboard::writeRegister(uint8_t reg, uint8_t value) -{ - uint8_t data[2]; - data[0] = reg | _WRITE_MASK; - data[1] = value; - - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(data, sizeof(uint8_t) * 2); - m_wire->endTransmission(); - } - if (writeCallback) { - writeCallback(m_addr, data[0], &(data[1]), 1); - } + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(data, sizeof(uint8_t) * 2); + m_wire->endTransmission(); + } + if (writeCallback) { + writeCallback(m_addr, data[0], &(data[1]), 1); + } } diff --git a/src/input/BBQ10Keyboard.h b/src/input/BBQ10Keyboard.h index 07d02308f..c99a61299 100644 --- a/src/input/BBQ10Keyboard.h +++ b/src/input/BBQ10Keyboard.h @@ -8,44 +8,43 @@ #define KEY_MOD_SHR (0x1C) #define KEY_MOD_SYM (0x1D) -class BBQ10Keyboard -{ - public: - typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); +class BBQ10Keyboard { +public: + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); - enum KeyState { StateIdle = 0, StatePress, StateLongPress, StateRelease }; + enum KeyState { StateIdle = 0, StatePress, StateLongPress, StateRelease }; - struct KeyEvent { - char key; - KeyState state; - }; + struct KeyEvent { + char key; + KeyState state; + }; - BBQ10Keyboard(); + BBQ10Keyboard(); - void begin(uint8_t addr = BBQ10_KB_ADDR, TwoWire *wire = &Wire); + void begin(uint8_t addr = BBQ10_KB_ADDR, TwoWire *wire = &Wire); - void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = BBQ10_KB_ADDR); + void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = BBQ10_KB_ADDR); - void reset(void); + void reset(void); - void attachInterrupt(uint8_t pin, void (*func)(void)) const; - void detachInterrupt(uint8_t pin) const; - void clearInterruptStatus(void); + void attachInterrupt(uint8_t pin, void (*func)(void)) const; + void detachInterrupt(uint8_t pin) const; + void clearInterruptStatus(void); - uint8_t status(void) const; - uint8_t keyCount(void) const; - KeyEvent keyEvent(void) const; + uint8_t status(void) const; + uint8_t keyCount(void) const; + KeyEvent keyEvent(void) const; - float backlight() const; - void setBacklight(float value); + float backlight() const; + void setBacklight(float value); - uint8_t readRegister8(uint8_t reg) const; - uint16_t readRegister16(uint8_t reg) const; - void writeRegister(uint8_t reg, uint8_t value); + uint8_t readRegister8(uint8_t reg) const; + uint16_t readRegister16(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); - private: - TwoWire *m_wire; - uint8_t m_addr; - i2c_com_fptr_t readCallback; - i2c_com_fptr_t writeCallback; +private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; }; diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 9f53b06f4..200b84118 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -22,307 +22,291 @@ using namespace concurrency; #if HAS_BUTTON #endif -ButtonThread::ButtonThread(const char *name) : OSThread(name) -{ - _originName = name; -} +ButtonThread::ButtonThread(const char *name) : OSThread(name) { _originName = name; } -bool ButtonThread::initButton(const ButtonConfig &config) -{ - if (inputBroker) - inputBroker->registerSource(this); - _longPressTime = config.longPressTime; - _longLongPressTime = config.longLongPressTime; - _pinNum = config.pinNumber; - _activeLow = config.activeLow; - _touchQuirk = config.touchQuirk; - _intRoutine = config.intRoutine; - _longLongPress = config.longLongPress; +bool ButtonThread::initButton(const ButtonConfig &config) { + if (inputBroker) + inputBroker->registerSource(this); + _longPressTime = config.longPressTime; + _longLongPressTime = config.longLongPressTime; + _pinNum = config.pinNumber; + _activeLow = config.activeLow; + _touchQuirk = config.touchQuirk; + _intRoutine = config.intRoutine; + _longLongPress = config.longLongPress; - userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); + userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); - if (config.pullupSense != 0) { - pinMode(config.pinNumber, config.pullupSense); - } + if (config.pullupSense != 0) { + pinMode(config.pinNumber, config.pullupSense); + } - _singlePress = config.singlePress; - userButton.attachClick( + _singlePress = config.singlePress; + userButton.attachClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_PRESSED; + }, + this); + + _longPress = config.longPress; + userButton.attachLongPressStart( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + // if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; + }, + this); + userButton.attachLongPressStop( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + // if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; + }, + this); + + if (config.doublePress != INPUT_BROKER_NONE) { + _doublePress = config.doublePress; + userButton.attachDoubleClick( [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - thread->btnEvent = BUTTON_EVENT_PRESSED; + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; }, this); + } - _longPress = config.longPress; - userButton.attachLongPressStart( + if (config.triplePress != INPUT_BROKER_NONE) { + _triplePress = config.triplePress; + userButton.attachMultiClick( [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - // if (millis() > 30000) // hold off 30s after boot - thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; + ButtonThread *thread = (ButtonThread *)callerThread; + thread->storeClickCount(); + thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; }, this); - userButton.attachLongPressStop( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - // if (millis() > 30000) // hold off 30s after boot - thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; - }, - this); - - if (config.doublePress != INPUT_BROKER_NONE) { - _doublePress = config.doublePress; - userButton.attachDoubleClick( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; - }, - this); - } - - if (config.triplePress != INPUT_BROKER_NONE) { - _triplePress = config.triplePress; - userButton.attachMultiClick( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - thread->storeClickCount(); - thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; - }, - this); - } - if (config.shortLong != INPUT_BROKER_NONE) { - _shortLong = config.shortLong; - } + } + if (config.shortLong != INPUT_BROKER_NONE) { + _shortLong = config.shortLong; + } #ifdef USE_EINK - userButton.setDebounceMs(0); + userButton.setDebounceMs(0); #else - userButton.setDebounceMs(1); + userButton.setDebounceMs(1); #endif - userButton.setPressMs(_longPressTime); + userButton.setPressMs(_longPressTime); - if (screen) { - userButton.setClickMs(20); - } else { - userButton.setClickMs(BUTTON_CLICK_MS); - } - attachButtonInterrupts(); + if (screen) { + userButton.setClickMs(20); + } else { + userButton.setClickMs(BUTTON_CLICK_MS); + } + attachButtonInterrupts(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); #endif - return true; + return true; } -int32_t ButtonThread::runOnce() -{ - // If the button is pressed we suppress CPU sleep until release - canSleep = true; // Assume we should not keep the board awake +int32_t ButtonThread::runOnce() { + // If the button is pressed we suppress CPU sleep until release + canSleep = true; // Assume we should not keep the board awake - // Check for combination timeout - if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { - waitingForLongPress = false; + // Check for combination timeout + if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { + waitingForLongPress = false; + } + + userButton.tick(); + canSleep &= userButton.isIdle(); + + // Check if we should play lead-up sound during long press + // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers + bool buttonCurrentlyPressed = isButtonPressed(_pinNum); + + // Detect start of button press + if (buttonCurrentlyPressed && !buttonWasPressed) { + buttonPressStartTime = millis(); + leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + // Progressive lead-up sound system + if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { + + // Start the progressive sequence if not already active + if (!leadUpSequenceActive) { + leadUpSequenceActive = true; + lastLeadUpNoteTime = millis(); + playNextLeadUpNote(); // Play the first note immediately + } + // Continue playing notes at intervals + else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes + if (playNextLeadUpNote()) { + lastLeadUpNoteTime = millis(); + } else { + leadUpPlayed = true; + } + } + } + + // Reset when button is released + if (!buttonCurrentlyPressed && buttonWasPressed) { + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + buttonWasPressed = buttonCurrentlyPressed; + + // new behavior + if (btnEvent != BUTTON_EVENT_NONE) { + InputEvent evt; + evt.source = _originName; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + evt.inputEvent = _singlePress; + // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types + this->notifyObservers(&evt); + + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); + + break; + } + case BUTTON_EVENT_LONG_PRESSED: { + // Ignore if: TX in progress + // Uncommon T-Echo hardware bug, LoRa TX triggers touch button + if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) + break; + + // Check if this is part of a short-press + long-press combination + if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { + evt.inputEvent = _shortLong; + // evt.kbchar = _shortLong; + this->notifyObservers(&evt); + // Play the combination tune + playComboTune(); + + break; + } + if (_longPress != INPUT_BROKER_NONE) { + // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) + evt.inputEvent = _longPress; + this->notifyObservers(&evt); + } + // Reset combination tracking + waitingForLongPress = false; + + break; } - userButton.tick(); - canSleep &= userButton.isIdle(); + case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected + LOG_INFO("Double press!"); - // Check if we should play lead-up sound during long press - // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers - bool buttonCurrentlyPressed = isButtonPressed(_pinNum); + // Reset combination tracking + waitingForLongPress = false; - // Detect start of button press - if (buttonCurrentlyPressed && !buttonWasPressed) { - buttonPressStartTime = millis(); - leadUpPlayed = false; - leadUpSequenceActive = false; - resetLeadUpSequence(); + evt.inputEvent = _doublePress; + // evt.kbchar = _doublePress; + this->notifyObservers(&evt); + playComboTune(); + + break; } - // Progressive lead-up sound system - if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { + case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present + LOG_INFO("Mulitipress! %hux", multipressClickCount); - // Start the progressive sequence if not already active - if (!leadUpSequenceActive) { - leadUpSequenceActive = true; - lastLeadUpNoteTime = millis(); - playNextLeadUpNote(); // Play the first note immediately - } - // Continue playing notes at intervals - else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes - if (playNextLeadUpNote()) { - lastLeadUpNoteTime = millis(); - } else { - leadUpPlayed = true; - } - } + // Reset combination tracking + waitingForLongPress = false; + + switch (multipressClickCount) { + case 3: + evt.inputEvent = _triplePress; + // evt.kbchar = _triplePress; + this->notifyObservers(&evt); + playComboTune(); + break; + + // No valid multipress action + default: + break; + } // end switch: click count + + break; + } // end multipress event + + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediatedly. + case BUTTON_EVENT_LONG_RELEASED: { + + LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); + if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { + evt.inputEvent = _longLongPress; + this->notifyObservers(&evt); + } + // Reset combination tracking + waitingForLongPress = false; + leadUpPlayed = false; + + break; } - // Reset when button is released - if (!buttonCurrentlyPressed && buttonWasPressed) { - leadUpSequenceActive = false; - resetLeadUpSequence(); + // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG + default: { + break; } - - buttonWasPressed = buttonCurrentlyPressed; - - // new behavior - if (btnEvent != BUTTON_EVENT_NONE) { - InputEvent evt; - evt.source = _originName; - evt.kbchar = 0; - evt.touchX = 0; - evt.touchY = 0; - switch (btnEvent) { - case BUTTON_EVENT_PRESSED: { - // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) - evt.inputEvent = _singlePress; - // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types - this->notifyObservers(&evt); - - // Start tracking for potential combination - waitingForLongPress = true; - shortPressTime = millis(); - - break; - } - case BUTTON_EVENT_LONG_PRESSED: { - // Ignore if: TX in progress - // Uncommon T-Echo hardware bug, LoRa TX triggers touch button - if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) - break; - - // Check if this is part of a short-press + long-press combination - if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && - (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { - evt.inputEvent = _shortLong; - // evt.kbchar = _shortLong; - this->notifyObservers(&evt); - // Play the combination tune - playComboTune(); - - break; - } - if (_longPress != INPUT_BROKER_NONE) { - // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) - evt.inputEvent = _longPress; - this->notifyObservers(&evt); - } - // Reset combination tracking - waitingForLongPress = false; - - break; - } - - case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected - LOG_INFO("Double press!"); - - // Reset combination tracking - waitingForLongPress = false; - - evt.inputEvent = _doublePress; - // evt.kbchar = _doublePress; - this->notifyObservers(&evt); - playComboTune(); - - break; - } - - case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present - LOG_INFO("Mulitipress! %hux", multipressClickCount); - - // Reset combination tracking - waitingForLongPress = false; - - switch (multipressClickCount) { - case 3: - evt.inputEvent = _triplePress; - // evt.kbchar = _triplePress; - this->notifyObservers(&evt); - playComboTune(); - break; - - // No valid multipress action - default: - break; - } // end switch: click count - - break; - } // end multipress event - - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. - case BUTTON_EVENT_LONG_RELEASED: { - - LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); - if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && - (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { - evt.inputEvent = _longLongPress; - this->notifyObservers(&evt); - } - // Reset combination tracking - waitingForLongPress = false; - leadUpPlayed = false; - - break; - } - - // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG - default: { - break; - } - } } - btnEvent = BUTTON_EVENT_NONE; + } + btnEvent = BUTTON_EVENT_NONE; - // only pull when the button is pressed, we get notified via IRQ on a new press - if (!userButton.isIdle() || waitingForLongPress) { - return 50; - } - return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here? + // only pull when the button is pressed, we get notified via IRQ on a new press + if (!userButton.isIdle() || waitingForLongPress) { + return 50; + } + return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here? } /* * Attach (or re-attach) hardware interrupts for buttons * Public method. Used outside class when waking from MCU sleep */ -void ButtonThread::attachButtonInterrupts() -{ - // Interrupt for user button, during normal use. Improves responsiveness. - attachInterrupt(_pinNum, _intRoutine, CHANGE); +void ButtonThread::attachButtonInterrupts() { + // Interrupt for user button, during normal use. Improves responsiveness. + attachInterrupt(_pinNum, _intRoutine, CHANGE); } /* * Detach the "normal" button interrupts. * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep */ -void ButtonThread::detachButtonInterrupts() -{ - detachInterrupt(_pinNum); -} +void ButtonThread::detachButtonInterrupts() { detachInterrupt(_pinNum); } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int ButtonThread::beforeLightSleep(void *unused) -{ - detachButtonInterrupts(); - return 0; // Indicates success +int ButtonThread::beforeLightSleep(void *unused) { + detachButtonInterrupts(); + return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) -{ - attachButtonInterrupts(); - return 0; // Indicates success +int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) { + attachButtonInterrupts(); + return 0; // Indicates success } #endif // Non-static method, runs during callback. Grabs info while still valid -void ButtonThread::storeClickCount() -{ - multipressClickCount = userButton.getNumberClicks(); -} \ No newline at end of file +void ButtonThread::storeClickCount() { multipressClickCount = userButton.getNumberClicks(); } \ No newline at end of file diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index 7de38341c..78d08dce1 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -8,23 +8,23 @@ typedef void (*voidFuncPtr)(void); struct ButtonConfig { - uint8_t pinNumber; - bool activeLow = true; - bool activePullup = true; - uint32_t pullupSense = 0; - voidFuncPtr intRoutine = nullptr; - input_broker_event singlePress = INPUT_BROKER_NONE; - input_broker_event longPress = INPUT_BROKER_NONE; - uint16_t longPressTime = 500; - input_broker_event doublePress = INPUT_BROKER_NONE; - input_broker_event longLongPress = INPUT_BROKER_NONE; - uint16_t longLongPressTime = 3900; - input_broker_event triplePress = INPUT_BROKER_NONE; - input_broker_event shortLong = INPUT_BROKER_NONE; - bool touchQuirk = false; + uint8_t pinNumber; + bool activeLow = true; + bool activePullup = true; + uint32_t pullupSense = 0; + voidFuncPtr intRoutine = nullptr; + input_broker_event singlePress = INPUT_BROKER_NONE; + input_broker_event longPress = INPUT_BROKER_NONE; + uint16_t longPressTime = 500; + input_broker_event doublePress = INPUT_BROKER_NONE; + input_broker_event longLongPress = INPUT_BROKER_NONE; + uint16_t longLongPressTime = 3900; + input_broker_event triplePress = INPUT_BROKER_NONE; + input_broker_event shortLong = INPUT_BROKER_NONE; + bool touchQuirk = false; - // Constructor to set required parameter - explicit ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {} + // Constructor to set required parameter + explicit ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {} }; #ifndef BUTTON_CLICK_MS @@ -43,89 +43,86 @@ struct ButtonConfig { #define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding #endif -class ButtonThread : public Observable, public concurrency::OSThread -{ - public: - const char *_originName; - static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot - bool initButton(const ButtonConfig &config); +class ButtonThread : public Observable, public concurrency::OSThread { +public: + const char *_originName; + static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot + bool initButton(const ButtonConfig &config); - enum ButtonEventType { - BUTTON_EVENT_NONE, - BUTTON_EVENT_PRESSED, - BUTTON_EVENT_PRESSED_SCREEN, - BUTTON_EVENT_DOUBLE_PRESSED, - BUTTON_EVENT_MULTI_PRESSED, - BUTTON_EVENT_LONG_PRESSED, - BUTTON_EVENT_LONG_RELEASED, - BUTTON_EVENT_TOUCH_LONG_PRESSED, - BUTTON_EVENT_COMBO_SHORT_LONG, - }; + enum ButtonEventType { + BUTTON_EVENT_NONE, + BUTTON_EVENT_PRESSED, + BUTTON_EVENT_PRESSED_SCREEN, + BUTTON_EVENT_DOUBLE_PRESSED, + BUTTON_EVENT_MULTI_PRESSED, + BUTTON_EVENT_LONG_PRESSED, + BUTTON_EVENT_LONG_RELEASED, + BUTTON_EVENT_TOUCH_LONG_PRESSED, + BUTTON_EVENT_COMBO_SHORT_LONG, + }; - explicit ButtonThread(const char *name); - int32_t runOnce() override; - OneButton userButton; - void attachButtonInterrupts(); - void detachButtonInterrupts(); - void storeClickCount(); - bool isButtonPressed(int buttonPin) - { - if (_activeLow) - return !digitalRead(buttonPin); // Active low: pressed = LOW - else - return digitalRead(buttonPin); // Most buttons are active low by default - } + explicit ButtonThread(const char *name); + int32_t runOnce() override; + OneButton userButton; + void attachButtonInterrupts(); + void detachButtonInterrupts(); + void storeClickCount(); + bool isButtonPressed(int buttonPin) { + if (_activeLow) + return !digitalRead(buttonPin); // Active low: pressed = LOW + else + return digitalRead(buttonPin); // Most buttons are active low by default + } - // Returns true while this thread's button is physically held down - bool isHeld() { return isButtonPressed(_pinNum); } + // Returns true while this thread's button is physically held down + bool isHeld() { return isButtonPressed(_pinNum); } - // Disconnect and reconnect interrupts for light sleep + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif - private: - input_broker_event _singlePress = INPUT_BROKER_NONE; - input_broker_event _longPress = INPUT_BROKER_NONE; - input_broker_event _longLongPress = INPUT_BROKER_NONE; +private: + input_broker_event _singlePress = INPUT_BROKER_NONE; + input_broker_event _longPress = INPUT_BROKER_NONE; + input_broker_event _longLongPress = INPUT_BROKER_NONE; - input_broker_event _doublePress = INPUT_BROKER_NONE; - input_broker_event _triplePress = INPUT_BROKER_NONE; - input_broker_event _shortLong = INPUT_BROKER_NONE; + input_broker_event _doublePress = INPUT_BROKER_NONE; + input_broker_event _triplePress = INPUT_BROKER_NONE; + input_broker_event _shortLong = INPUT_BROKER_NONE; - voidFuncPtr _intRoutine = nullptr; - uint16_t _longPressTime = 500; - uint16_t _longLongPressTime = 3900; - int _pinNum = 0; - bool _activeLow = true; - bool _touchQuirk = false; + voidFuncPtr _intRoutine = nullptr; + uint16_t _longPressTime = 500; + uint16_t _longLongPressTime = 3900; + int _pinNum = 0; + bool _activeLow = true; + bool _touchQuirk = false; - uint32_t buttonPressStartTime = 0; - bool buttonWasPressed = false; + uint32_t buttonPressStartTime = 0; + bool buttonWasPressed = false; #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &ButtonThread::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &ButtonThread::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &ButtonThread::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &ButtonThread::afterLightSleep); #endif - volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; + volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; - // Store click count during callback, for later use - volatile int multipressClickCount = 0; + // Store click count during callback, for later use + volatile int multipressClickCount = 0; - // Combination tracking state - bool waitingForLongPress = false; - uint32_t shortPressTime = 0; + // Combination tracking state + bool waitingForLongPress = false; + uint32_t shortPressTime = 0; - // Long press lead-up tracking - bool leadUpPlayed = false; - uint32_t lastLeadUpNoteTime = 0; - bool leadUpSequenceActive = false; + // Long press lead-up tracking + bool leadUpPlayed = false; + uint32_t lastLeadUpNoteTime = 0; + bool leadUpSequenceActive = false; - static void wakeOnIrq(int irq, int mode); + static void wakeOnIrq(int irq, int mode); }; extern ButtonThread *buttonThread; diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 01712ad2a..f6dc0e19c 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -21,225 +21,210 @@ static const char inputSourceName[] = "ExpressLRS5Way"; // should match "allow i * Example Fuzz values: {20, 20, 100, 100, 125, 300} now the fuzz for the 1600 * position is 300 instead of 20 */ -void ExpressLRSFiveWay::calcFuzzValues() -{ - for (unsigned int i = 0; i < N_JOY_ADC_VALUES; i++) { - uint16_t closestDist = 0xffff; - uint16_t ival = joyAdcValues[i]; - // Find the closest value to ival - for (unsigned int j = 0; j < N_JOY_ADC_VALUES; j++) { - // Don't compare value with itself - if (j == i) - continue; - uint16_t jval = joyAdcValues[j]; - if (jval < ival && (ival - jval < closestDist)) - closestDist = ival - jval; - if (jval > ival && (jval - ival < closestDist)) - closestDist = jval - ival; - } // for j +void ExpressLRSFiveWay::calcFuzzValues() { + for (unsigned int i = 0; i < N_JOY_ADC_VALUES; i++) { + uint16_t closestDist = 0xffff; + uint16_t ival = joyAdcValues[i]; + // Find the closest value to ival + for (unsigned int j = 0; j < N_JOY_ADC_VALUES; j++) { + // Don't compare value with itself + if (j == i) + continue; + uint16_t jval = joyAdcValues[j]; + if (jval < ival && (ival - jval < closestDist)) + closestDist = ival - jval; + if (jval > ival && (jval - ival < closestDist)) + closestDist = jval - ival; + } // for j - // And the fuzz is half the distance to the closest value - fuzzValues[i] = closestDist / 2; - // DBG("joy%u=%u f=%u, ", i, ival, fuzzValues[i]); - } // for i + // And the fuzz is half the distance to the closest value + fuzzValues[i] = closestDist / 2; + // DBG("joy%u=%u f=%u, ", i, ival, fuzzValues[i]); + } // for i } -int ExpressLRSFiveWay::readKey() -{ - uint16_t value = analogRead(PIN_JOYSTICK); +int ExpressLRSFiveWay::readKey() { + uint16_t value = analogRead(PIN_JOYSTICK); - constexpr uint8_t IDX_TO_INPUT[N_JOY_ADC_VALUES - 1] = {UP, DOWN, LEFT, RIGHT, OK}; - for (unsigned int i = 0; i < N_JOY_ADC_VALUES - 1; ++i) { - if (value < (joyAdcValues[i] + fuzzValues[i]) && value > (joyAdcValues[i] - fuzzValues[i])) - return IDX_TO_INPUT[i]; - } - return NO_PRESS; + constexpr uint8_t IDX_TO_INPUT[N_JOY_ADC_VALUES - 1] = {UP, DOWN, LEFT, RIGHT, OK}; + for (unsigned int i = 0; i < N_JOY_ADC_VALUES - 1; ++i) { + if (value < (joyAdcValues[i] + fuzzValues[i]) && value > (joyAdcValues[i] - fuzzValues[i])) + return IDX_TO_INPUT[i]; + } + return NO_PRESS; } -ExpressLRSFiveWay::ExpressLRSFiveWay() : concurrency::OSThread(inputSourceName) -{ - // ExpressLRS: init values - isLongPressed = false; - keyInProcess = NO_PRESS; - keyDownStart = 0; +ExpressLRSFiveWay::ExpressLRSFiveWay() : concurrency::OSThread(inputSourceName) { + // ExpressLRS: init values + isLongPressed = false; + keyInProcess = NO_PRESS; + keyDownStart = 0; - // Express LRS: calculate the threshold for interpreting ADC values as various buttons - calcFuzzValues(); + // Express LRS: calculate the threshold for interpreting ADC values as various buttons + calcFuzzValues(); - // Meshtastic: register with canned messages - inputBroker->registerSource(this); + // Meshtastic: register with canned messages + inputBroker->registerSource(this); } // ExpressLRS: interpret reading as key events -void ExpressLRSFiveWay::update(int *keyValue, bool *keyLongPressed) -{ - *keyValue = NO_PRESS; +void ExpressLRSFiveWay::update(int *keyValue, bool *keyLongPressed) { + *keyValue = NO_PRESS; - int newKey = readKey(); - if (keyInProcess == NO_PRESS) { - // New key down - if (newKey != NO_PRESS) { - keyDownStart = millis(); - // DBGLN("down=%u", newKey); + int newKey = readKey(); + if (keyInProcess == NO_PRESS) { + // New key down + if (newKey != NO_PRESS) { + keyDownStart = millis(); + // DBGLN("down=%u", newKey); + } + } else { + // if key released + if (newKey == NO_PRESS) { + // DBGLN("up=%u", keyInProcess); + if (!isLongPressed) { + if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_DEBOUNCE_MS)) { + *keyValue = keyInProcess; + *keyLongPressed = false; } - } else { - // if key released - if (newKey == NO_PRESS) { - // DBGLN("up=%u", keyInProcess); - if (!isLongPressed) { - if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_DEBOUNCE_MS)) { - *keyValue = keyInProcess; - *keyLongPressed = false; - } - } - isLongPressed = false; - } - // else if the key has changed while down, reset state for next go-around - else if (newKey != keyInProcess) { - newKey = NO_PRESS; - } - // else still pressing, waiting for long if not already signaled - else if (!isLongPressed) { - if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_LONG_PRESS_MS)) { - *keyValue = keyInProcess; - *keyLongPressed = true; - isLongPressed = true; - } - } - } // if keyInProcess != NO_PRESS + } + isLongPressed = false; + } + // else if the key has changed while down, reset state for next go-around + else if (newKey != keyInProcess) { + newKey = NO_PRESS; + } + // else still pressing, waiting for long if not already signaled + else if (!isLongPressed) { + if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_LONG_PRESS_MS)) { + *keyValue = keyInProcess; + *keyLongPressed = true; + isLongPressed = true; + } + } + } // if keyInProcess != NO_PRESS - keyInProcess = newKey; + keyInProcess = newKey; } // Meshtastic: runs at regular intervals -int32_t ExpressLRSFiveWay::runOnce() -{ - uint32_t now = millis(); +int32_t ExpressLRSFiveWay::runOnce() { + uint32_t now = millis(); - // Dismiss any alert frames after 2 seconds - // Feedback for GPS toggle / adhoc ping - if (alerting && now > alertingSinceMs + 2000) { - alerting = false; - screen->endAlert(); - } + // Dismiss any alert frames after 2 seconds + // Feedback for GPS toggle / adhoc ping + if (alerting && now > alertingSinceMs + 2000) { + alerting = false; + screen->endAlert(); + } - // Get key events from ExpressLRS code - int keyValue; - bool longPressed; - update(&keyValue, &longPressed); + // Get key events from ExpressLRS code + int keyValue; + bool longPressed; + update(&keyValue, &longPressed); - // Do something about this key press - determineAction((KeyType)keyValue, longPressed ? LONG : SHORT); + // Do something about this key press + determineAction((KeyType)keyValue, longPressed ? LONG : SHORT); - // If there has been recent key activity, poll the joystick slightly more frequently - if (now < keyDownStart + (20 * 1000UL)) // Within last 20 seconds - return 100; + // If there has been recent key activity, poll the joystick slightly more frequently + if (now < keyDownStart + (20 * 1000UL)) // Within last 20 seconds + return 100; - // Otherwise, poll slightly less often - // Too many missed pressed if much slower than 250ms - return 250; + // Otherwise, poll slightly less often + // Too many missed pressed if much slower than 250ms + return 250; } // Determine what action to take when a button press is detected // Written verbose for easier remapping by user -void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) -{ - switch (key) { - case LEFT: - if (inCannedMessageMenu()) // If in canned message menu - sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) - else - sendKey(INPUT_BROKER_LEFT); - break; +void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) { + switch (key) { + case LEFT: + if (inCannedMessageMenu()) // If in canned message menu + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) + else + sendKey(INPUT_BROKER_LEFT); + break; - case RIGHT: - if (inCannedMessageMenu()) // If in canned message menu: - sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) - else - sendKey(INPUT_BROKER_RIGHT); - break; + case RIGHT: + if (inCannedMessageMenu()) // If in canned message menu: + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) + else + sendKey(INPUT_BROKER_RIGHT); + break; - case UP: - if (length == LONG) - toggleGPS(); - else - sendKey(INPUT_BROKER_UP); - break; + case UP: + if (length == LONG) + toggleGPS(); + else + sendKey(INPUT_BROKER_UP); + break; - case DOWN: - if (length == LONG) - sendAdhocPing(); - else - sendKey(INPUT_BROKER_DOWN); - break; + case DOWN: + if (length == LONG) + sendAdhocPing(); + else + sendKey(INPUT_BROKER_DOWN); + break; - case OK: - if (length == LONG) - shutdown(); - else - click(); // Use instead of sendKey(OK). Works better when canned message module disabled - break; + case OK: + if (length == LONG) + shutdown(); + else + click(); // Use instead of sendKey(OK). Works better when canned message module disabled + break; - default: - break; - } + default: + break; + } } // Feed input to the canned messages module -void ExpressLRSFiveWay::sendKey(input_broker_event key) -{ - InputEvent e = {}; - e.source = inputSourceName; - e.inputEvent = key; - notifyObservers(&e); +void ExpressLRSFiveWay::sendKey(input_broker_event key) { + InputEvent e = {}; + e.source = inputSourceName; + e.inputEvent = key; + notifyObservers(&e); } // Enable or Disable a connected GPS // Contained as one method for easier remapping of buttons by user -void ExpressLRSFiveWay::toggleGPS() -{ +void ExpressLRSFiveWay::toggleGPS() { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) { - gps->toggleGpsMode(); - screen->startAlert("GPS Toggled"); - alerting = true; - alertingSinceMs = millis(); - } + if (gps != nullptr) { + gps->toggleGpsMode(); + screen->startAlert("GPS Toggled"); + alerting = true; + alertingSinceMs = millis(); + } #endif } // Send either node-info or position, on demand // Contained as one method for easier remapping of buttons by user -void ExpressLRSFiveWay::sendAdhocPing() -{ - service->refreshLocalMeshNode(); - bool sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); +void ExpressLRSFiveWay::sendAdhocPing() { + service->refreshLocalMeshNode(); + bool sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); - // Show custom alert frame, with multi-line centering - screen->startAlert([sentPosition](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - uint16_t x_offset = display->width() / 2; - uint16_t y_offset = 26; // Same constant as the default startAlert frame - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Sent ad-hoc"); - display->drawString(x_offset + x, y_offset + FONT_HEIGHT_MEDIUM + y, sentPosition ? "position" : "nodeinfo"); - }); + // Show custom alert frame, with multi-line centering + screen->startAlert([sentPosition](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + uint16_t x_offset = display->width() / 2; + uint16_t y_offset = 26; // Same constant as the default startAlert frame + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Sent ad-hoc"); + display->drawString(x_offset + x, y_offset + FONT_HEIGHT_MEDIUM + y, sentPosition ? "position" : "nodeinfo"); + }); - alerting = true; - alertingSinceMs = millis(); + alerting = true; + alertingSinceMs = millis(); } // Shutdown the node (enter deep-sleep) // Contained as one method for easier remapping of buttons by user -void ExpressLRSFiveWay::shutdown() -{ - sendKey(INPUT_BROKER_SHUTDOWN); -} +void ExpressLRSFiveWay::shutdown() { sendKey(INPUT_BROKER_SHUTDOWN); } -void ExpressLRSFiveWay::click() -{ - sendKey(INPUT_BROKER_SELECT); -} +void ExpressLRSFiveWay::click() { sendKey(INPUT_BROKER_SELECT); } ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; diff --git a/src/input/ExpressLRSFiveWay.h b/src/input/ExpressLRSFiveWay.h index 7c7f210f8..a9bccc9ad 100644 --- a/src/input/ExpressLRSFiveWay.h +++ b/src/input/ExpressLRSFiveWay.h @@ -28,56 +28,55 @@ #include "GPS.h" // For toggle GPS action #endif -class ExpressLRSFiveWay : public Observable, public concurrency::OSThread -{ - private: - // Number of values in JOY_ADC_VALUES, if defined - // These must be ADC readings for {UP, DOWN, LEFT, RIGHT, ENTER, IDLE} - static constexpr size_t N_JOY_ADC_VALUES = 6; - static constexpr uint32_t KEY_DEBOUNCE_MS = 25; - static constexpr uint32_t KEY_LONG_PRESS_MS = 3000; // How many milliseconds to hold key for a long press +class ExpressLRSFiveWay : public Observable, public concurrency::OSThread { +private: + // Number of values in JOY_ADC_VALUES, if defined + // These must be ADC readings for {UP, DOWN, LEFT, RIGHT, ENTER, IDLE} + static constexpr size_t N_JOY_ADC_VALUES = 6; + static constexpr uint32_t KEY_DEBOUNCE_MS = 25; + static constexpr uint32_t KEY_LONG_PRESS_MS = 3000; // How many milliseconds to hold key for a long press - // This merged an enum used by the ExpressLRS code, with meshtastic canned message values - // Key names are kept simple, to allow user customizaton - typedef enum { - UP = INPUT_BROKER_UP, - DOWN = INPUT_BROKER_DOWN, - LEFT = INPUT_BROKER_LEFT, - RIGHT = INPUT_BROKER_RIGHT, - OK = INPUT_BROKER_SELECT, - CANCEL = INPUT_BROKER_CANCEL, - NO_PRESS = INPUT_BROKER_NONE - } KeyType; + // This merged an enum used by the ExpressLRS code, with meshtastic canned message values + // Key names are kept simple, to allow user customizaton + typedef enum { + UP = INPUT_BROKER_UP, + DOWN = INPUT_BROKER_DOWN, + LEFT = INPUT_BROKER_LEFT, + RIGHT = INPUT_BROKER_RIGHT, + OK = INPUT_BROKER_SELECT, + CANCEL = INPUT_BROKER_CANCEL, + NO_PRESS = INPUT_BROKER_NONE + } KeyType; - typedef enum { SHORT, LONG } PressLength; + typedef enum { SHORT, LONG } PressLength; - // From ExpressLRS - int keyInProcess; - uint32_t keyDownStart; - bool isLongPressed; - const uint16_t joyAdcValues[N_JOY_ADC_VALUES] = {JOYSTICK_ADC_VALS}; - uint16_t fuzzValues[N_JOY_ADC_VALUES]; - void calcFuzzValues(); - int readKey(); - void update(int *keyValue, bool *keyLongPressed); + // From ExpressLRS + int keyInProcess; + uint32_t keyDownStart; + bool isLongPressed; + const uint16_t joyAdcValues[N_JOY_ADC_VALUES] = {JOYSTICK_ADC_VALS}; + uint16_t fuzzValues[N_JOY_ADC_VALUES]; + void calcFuzzValues(); + int readKey(); + void update(int *keyValue, bool *keyLongPressed); - // Meshtastic code - void determineAction(KeyType key, PressLength length); - void sendKey(input_broker_event key); - inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } - int32_t runOnce() override; + // Meshtastic code + void determineAction(KeyType key, PressLength length); + void sendKey(input_broker_event key); + inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } + int32_t runOnce() override; - // Simplified Meshtastic actions, for easier remapping by user - void toggleGPS(); - void sendAdhocPing(); - void shutdown(); - void click(); + // Simplified Meshtastic actions, for easier remapping by user + void toggleGPS(); + void sendAdhocPing(); + void shutdown(); + void click(); - bool alerting = false; // Is the screen showing an alert frame? Feedback for GPS toggle / adhoc ping actions - uint32_t alertingSinceMs = 0; // When did screen begin showing an alert frame? Used to auto-dismiss + bool alerting = false; // Is the screen showing an alert frame? Feedback for GPS toggle / adhoc ping actions + uint32_t alertingSinceMs = 0; // When did screen begin showing an alert frame? Used to auto-dismiss - public: - ExpressLRSFiveWay(); +public: + ExpressLRSFiveWay(); }; extern ExpressLRSFiveWay *expressLRSFiveWayInput; diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp index 87c8a24ae..46f2c08ce 100644 --- a/src/input/HackadayCommunicatorKeyboard.cpp +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -106,112 +106,103 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, {}}; HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), - last_tap(0L), char_idx(0), tap_interval(0) -{ - reset(); + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), + char_idx(0), tap_interval(0) { + reset(); } -void HackadayCommunicatorKeyboard::reset(void) -{ - TCA8418KeyboardBase::reset(); - enableInterrupts(); +void HackadayCommunicatorKeyboard::reset(void) { + TCA8418KeyboardBase::reset(); + enableInterrupts(); } // handle multi-key presses (shift and alt) -void HackadayCommunicatorKeyboard::trigger() -{ - uint8_t count = keyCount(); - if (count == 0) - return; - for (uint8_t i = 0; i < count; ++i) { - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); - uint8_t key = k & 0x7F; - if (k & 0x80) { - pressed(key); - } else { - released(); - state = Idle; - } - } -} - -void HackadayCommunicatorKeyboard::pressed(uint8_t key) -{ - if (state == Init || state == Busy) { - return; - } - - if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { - modifierFlag = 0; - } - - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } - - next_key = row * _TCA8418_COLS + col; - state = Held; - - uint32_t now = millis(); - tap_interval = now - last_tap; - - updateModifierFlag(next_key); - if (isModifierKey(next_key)) { - last_modifier_time = now; - } - - if (tap_interval < 0) { - last_tap = 0; - state = Busy; - return; - } - - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; +void HackadayCommunicatorKeyboard::trigger() { + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); } else { - char_idx += 1; + released(); + state = Idle; } - - last_key = next_key; - last_tap = now; + } } -void HackadayCommunicatorKeyboard::released() -{ - if (state != Held) { - return; - } +void HackadayCommunicatorKeyboard::pressed(uint8_t key) { + if (state == Init || state == Busy) { + return; + } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; - state = Idle; - return; - } + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } - uint32_t now = millis(); - last_tap = now; - if (HackadayCommunicatorTapMod[last_key]) - queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]); - if (isModifierKey(last_key) == false) - modifierFlag = 0; + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; } -void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key) -{ - if (key == modifierRightShiftKey) { - modifierFlag ^= modifierRightShift; - } else if (key == modifierLeftShiftKey) { - modifierFlag ^= modifierLeftShift; - } +void HackadayCommunicatorKeyboard::released() { + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + if (HackadayCommunicatorTapMod[last_key]) + queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; } -bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key) -{ - return (key == modifierRightShiftKey || key == modifierLeftShiftKey); +void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key) { + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierLeftShiftKey) { + modifierFlag ^= modifierLeftShift; + } } +bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierLeftShiftKey); } + #endif \ No newline at end of file diff --git a/src/input/HackadayCommunicatorKeyboard.h b/src/input/HackadayCommunicatorKeyboard.h index 8316bed72..2d862c863 100644 --- a/src/input/HackadayCommunicatorKeyboard.h +++ b/src/input/HackadayCommunicatorKeyboard.h @@ -1,26 +1,25 @@ #include "TCA8418KeyboardBase.h" -class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase -{ - public: - HackadayCommunicatorKeyboard(); - void reset(void); - void trigger(void) override; - virtual ~HackadayCommunicatorKeyboard() {} +class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase { +public: + HackadayCommunicatorKeyboard(); + void reset(void); + void trigger(void) override; + virtual ~HackadayCommunicatorKeyboard() {} - protected: - void pressed(uint8_t key) override; - void released(void) override; +protected: + void pressed(uint8_t key) override; + void released(void) override; - void updateModifierFlag(uint8_t key); - bool isModifierKey(uint8_t key); + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); - private: - uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed - uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; +private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 0aa78e2b8..ca4a330e1 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -5,72 +5,63 @@ InputBroker *inputBroker = nullptr; -InputBroker::InputBroker() -{ +InputBroker::InputBroker() { #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - inputEventQueue = xQueueCreate(5, sizeof(InputEvent)); - pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *)); - xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask); + inputEventQueue = xQueueCreate(5, sizeof(InputEvent)); + pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *)); + xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask); #endif } -void InputBroker::registerSource(Observable *source) -{ - this->inputEventObserver.observe(source); -} +void InputBroker::registerSource(Observable *source) { this->inputEventObserver.observe(source); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) -void InputBroker::requestPollSoon(InputPollable *pollable) -{ - if (xPortInIsrContext() == pdTRUE) { - xQueueSendFromISR(pollSoonQueue, &pollable, NULL); - } else { - xQueueSend(pollSoonQueue, &pollable, 0); - } +void InputBroker::requestPollSoon(InputPollable *pollable) { + if (xPortInIsrContext() == pdTRUE) { + xQueueSendFromISR(pollSoonQueue, &pollable, NULL); + } else { + xQueueSend(pollSoonQueue, &pollable, 0); + } } -void InputBroker::queueInputEvent(const InputEvent *event) -{ - if (xPortInIsrContext() == pdTRUE) { - xQueueSendFromISR(inputEventQueue, event, NULL); - } else { - xQueueSend(inputEventQueue, event, portMAX_DELAY); - } +void InputBroker::queueInputEvent(const InputEvent *event) { + if (xPortInIsrContext() == pdTRUE) { + xQueueSendFromISR(inputEventQueue, event, NULL); + } else { + xQueueSend(inputEventQueue, event, portMAX_DELAY); + } } -void InputBroker::processInputEventQueue() -{ - InputEvent event; - while (xQueueReceive(inputEventQueue, &event, 0)) { - handleInputEvent(&event); - } +void InputBroker::processInputEventQueue() { + InputEvent event; + while (xQueueReceive(inputEventQueue, &event, 0)) { + handleInputEvent(&event); + } } #endif -int InputBroker::handleInputEvent(const InputEvent *event) -{ - powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release +int InputBroker::handleInputEvent(const InputEvent *event) { + powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release - if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && - moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) { - externalNotificationModule->stopNow(); - } + if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && moduleConfig.external_notification.enabled && + externalNotificationModule->nagging()) { + externalNotificationModule->stopNow(); + } - this->notifyObservers(event); - return 0; + this->notifyObservers(event); + return 0; } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) -void InputBroker::pollSoonWorker(void *p) -{ - InputBroker *instance = (InputBroker *)p; - while (true) { - InputPollable *pollable = NULL; - xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY); - if (pollable) { - pollable->pollOnce(); - } +void InputBroker::pollSoonWorker(void *p) { + InputBroker *instance = (InputBroker *)p; + while (true) { + InputPollable *pollable = NULL; + xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY); + if (pollable) { + pollable->pollOnce(); } - vTaskDelete(NULL); + } + vTaskDelete(NULL); } #endif diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index c55d7fa53..486b80b4b 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -10,25 +10,25 @@ #endif enum input_broker_event { - INPUT_BROKER_NONE = 0, - INPUT_BROKER_SELECT = 10, - INPUT_BROKER_SELECT_LONG = 11, - INPUT_BROKER_UP_LONG = 12, - INPUT_BROKER_DOWN_LONG = 13, - INPUT_BROKER_UP = 17, - INPUT_BROKER_DOWN = 18, - INPUT_BROKER_LEFT = 19, - INPUT_BROKER_RIGHT = 20, - INPUT_BROKER_CANCEL = 24, - INPUT_BROKER_BACK = 27, - INPUT_BROKER_USER_PRESS, - INPUT_BROKER_ALT_PRESS, - INPUT_BROKER_ALT_LONG, - INPUT_BROKER_SHUTDOWN = 0x9b, - INPUT_BROKER_GPS_TOGGLE = 0x9e, - INPUT_BROKER_SEND_PING = 0xaf, - INPUT_BROKER_MATRIXKEY = 0xFE, - INPUT_BROKER_ANYKEY = 0xff + INPUT_BROKER_NONE = 0, + INPUT_BROKER_SELECT = 10, + INPUT_BROKER_SELECT_LONG = 11, + INPUT_BROKER_UP_LONG = 12, + INPUT_BROKER_DOWN_LONG = 13, + INPUT_BROKER_UP = 17, + INPUT_BROKER_DOWN = 18, + INPUT_BROKER_LEFT = 19, + INPUT_BROKER_RIGHT = 20, + INPUT_BROKER_CANCEL = 24, + INPUT_BROKER_BACK = 27, + INPUT_BROKER_USER_PRESS, + INPUT_BROKER_ALT_PRESS, + INPUT_BROKER_ALT_LONG, + INPUT_BROKER_SHUTDOWN = 0x9b, + INPUT_BROKER_GPS_TOGGLE = 0x9e, + INPUT_BROKER_SEND_PING = 0xaf, + INPUT_BROKER_MATRIXKEY = 0xFE, + INPUT_BROKER_ANYKEY = 0xff }; @@ -43,44 +43,42 @@ enum input_broker_event { #define INPUT_BROKER_MSG_EMOTE_LIST 0x8F typedef struct _InputEvent { - const char *source; - input_broker_event inputEvent; - unsigned char kbchar; - uint16_t touchX; - uint16_t touchY; + const char *source; + input_broker_event inputEvent; + unsigned char kbchar; + uint16_t touchX; + uint16_t touchY; } InputEvent; -class InputPollable -{ - public: - virtual ~InputPollable() = default; - virtual void pollOnce() = 0; +class InputPollable { +public: + virtual ~InputPollable() = default; + virtual void pollOnce() = 0; }; -class InputBroker : public Observable -{ - CallbackObserver inputEventObserver = - CallbackObserver(this, &InputBroker::handleInputEvent); +class InputBroker : public Observable { + CallbackObserver inputEventObserver = + CallbackObserver(this, &InputBroker::handleInputEvent); - public: - InputBroker(); - void registerSource(Observable *source); - void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } +public: + InputBroker(); + void registerSource(Observable *source); + void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - void requestPollSoon(InputPollable *pollable); - void queueInputEvent(const InputEvent *event); - void processInputEventQueue(); + void requestPollSoon(InputPollable *pollable); + void queueInputEvent(const InputEvent *event); + void processInputEventQueue(); #endif - protected: - int handleInputEvent(const InputEvent *event); +protected: + int handleInputEvent(const InputEvent *event); - private: +private: #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - QueueHandle_t inputEventQueue; - QueueHandle_t pollSoonQueue; - TaskHandle_t pollSoonTask; - static void pollSoonWorker(void *p); + QueueHandle_t inputEventQueue; + QueueHandle_t pollSoonQueue; + TaskHandle_t pollSoonTask; + static void pollSoonWorker(void *p); #endif }; diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index fee7c8ded..d020ec3b6 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -18,172 +18,167 @@ // Inspired by https://github.com/librerpi/rpi-tools/blob/master/keyboard-proxy/main.c which is GPL-v2 -LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name) -{ - this->_originName = name; +LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name) { this->_originName = name; } + +void LinuxInput::deInit() { + if (fd >= 0) + close(fd); } -void LinuxInput::deInit() -{ - if (fd >= 0) - close(fd); -} +int32_t LinuxInput::runOnce() { -int32_t LinuxInput::runOnce() -{ + if (firstTime) { + if (portduino_config.keyboardDevice == "") + return disable(); + fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); + if (fd < 0) + return disable(); + ret = ioctl(fd, EVIOCGRAB, (void *)1); + if (ret != 0) + return disable(); - if (firstTime) { - if (portduino_config.keyboardDevice == "") - return disable(); - fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); - if (fd < 0) - return disable(); - ret = ioctl(fd, EVIOCGRAB, (void *)1); - if (ret != 0) - return disable(); + epollfd = epoll_create1(0); + assert(epollfd >= 0); - epollfd = epoll_create1(0); - assert(epollfd >= 0); + ev.events = EPOLLIN; + ev.data.fd = fd; + if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) { + perror("unable to epoll add"); + return disable(); + } + kb_found = true; + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 0; + } - ev.events = EPOLLIN; - ev.data.fd = fd; - if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) { - perror("unable to epoll add"); - return disable(); + int nfds = epoll_wait(epollfd, events, MAX_EVENTS, 1); + if (nfds < 0) { + printf("%d ", nfds); + perror("epoll_wait failed"); + return disable(); + } else if (nfds == 0) { + return 50; + } + + int keys = 0; + memset(report, 0, 8); + for (int i = 0; i < nfds; i++) { + + struct input_event ev[64]; + int rd = read(events[i].data.fd, ev, sizeof(ev)); + assert(rd > ((signed int)sizeof(struct input_event))); + for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + e.kbchar = 0; + unsigned int type, code; + type = ev[j].type; + code = ev[j].code; + int value = ev[j].value; + // printf("Event: time %ld.%06ld, ", ev[j].time.tv_sec, ev[j].time.tv_usec); + + if (type == EV_KEY) { + uint8_t mod = 0; + + switch (code) { + case KEY_LEFTCTRL: + mod = 0x01; + break; + case KEY_RIGHTCTRL: + mod = 0x10; + break; + case KEY_LEFTSHIFT: + mod = 0x02; + break; + case KEY_RIGHTSHIFT: + mod = 0x20; + break; + case KEY_LEFTALT: + mod = 0x04; + break; + case KEY_RIGHTALT: + mod = 0x40; + break; + case KEY_LEFTMETA: + mod = 0x08; + break; } - kb_found = true; - // This is the first time the OSThread library has called this function, so do port setup - firstTime = 0; - } + if (value == 1) { + switch (code) { + case KEY_LEFTCTRL: + mod = 0x01; + break; + case KEY_RIGHTCTRL: + mod = 0x10; + break; + case KEY_LEFTSHIFT: + mod = 0x02; + break; + case KEY_RIGHTSHIFT: + mod = 0x20; + break; + case KEY_LEFTALT: + mod = 0x04; + break; + case KEY_RIGHTALT: + mod = 0x40; + break; + case KEY_LEFTMETA: + mod = 0x08; + break; + case KEY_ESC: // ESC + e.inputEvent = INPUT_BROKER_CANCEL; + break; + case KEY_BACK: // Back + e.inputEvent = INPUT_BROKER_BACK; + // e.kbchar = key; + break; - int nfds = epoll_wait(epollfd, events, MAX_EVENTS, 1); - if (nfds < 0) { - printf("%d ", nfds); - perror("epoll_wait failed"); - return disable(); - } else if (nfds == 0) { - return 50; - } - - int keys = 0; - memset(report, 0, 8); - for (int i = 0; i < nfds; i++) { - - struct input_event ev[64]; - int rd = read(events[i].data.fd, ev, sizeof(ev)); - assert(rd > ((signed int)sizeof(struct input_event))); - for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; + case KEY_UP: // Up + e.inputEvent = INPUT_BROKER_UP; + break; + case KEY_DOWN: // Down + e.inputEvent = INPUT_BROKER_DOWN; + break; + case KEY_LEFT: // Left + e.inputEvent = INPUT_BROKER_LEFT; + break; + e.kbchar = INPUT_BROKER_LEFT; + case KEY_RIGHT: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + break; e.kbchar = 0; - unsigned int type, code; - type = ev[j].type; - code = ev[j].code; - int value = ev[j].value; - // printf("Event: time %ld.%06ld, ", ev[j].time.tv_sec, ev[j].time.tv_usec); - - if (type == EV_KEY) { - uint8_t mod = 0; - - switch (code) { - case KEY_LEFTCTRL: - mod = 0x01; - break; - case KEY_RIGHTCTRL: - mod = 0x10; - break; - case KEY_LEFTSHIFT: - mod = 0x02; - break; - case KEY_RIGHTSHIFT: - mod = 0x20; - break; - case KEY_LEFTALT: - mod = 0x04; - break; - case KEY_RIGHTALT: - mod = 0x40; - break; - case KEY_LEFTMETA: - mod = 0x08; - break; - } - if (value == 1) { - switch (code) { - case KEY_LEFTCTRL: - mod = 0x01; - break; - case KEY_RIGHTCTRL: - mod = 0x10; - break; - case KEY_LEFTSHIFT: - mod = 0x02; - break; - case KEY_RIGHTSHIFT: - mod = 0x20; - break; - case KEY_LEFTALT: - mod = 0x04; - break; - case KEY_RIGHTALT: - mod = 0x40; - break; - case KEY_LEFTMETA: - mod = 0x08; - break; - case KEY_ESC: // ESC - e.inputEvent = INPUT_BROKER_CANCEL; - break; - case KEY_BACK: // Back - e.inputEvent = INPUT_BROKER_BACK; - // e.kbchar = key; - break; - - case KEY_UP: // Up - e.inputEvent = INPUT_BROKER_UP; - break; - case KEY_DOWN: // Down - e.inputEvent = INPUT_BROKER_DOWN; - break; - case KEY_LEFT: // Left - e.inputEvent = INPUT_BROKER_LEFT; - break; - e.kbchar = INPUT_BROKER_LEFT; - case KEY_RIGHT: // Right - e.inputEvent = INPUT_BROKER_RIGHT; - break; - e.kbchar = 0; - case KEY_ENTER: // Enter - e.inputEvent = INPUT_BROKER_SELECT; - break; - case KEY_POWER: - system("poweroff"); - break; - default: // all other keys - if (keymap[code]) { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = keymap[code]; - } - break; - } - } - if (ev[j].value) { - modifiers |= mod; - } else { - modifiers &= ~mod; - } - report[0] = modifiers; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) - e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. - this->notifyObservers(&e); + case KEY_ENTER: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + break; + case KEY_POWER: + system("poweroff"); + break; + default: // all other keys + if (keymap[code]) { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = keymap[code]; } + break; + } } + if (ev[j].value) { + modifiers |= mod; + } else { + modifiers &= ~mod; + } + report[0] = modifiers; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) + e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. + this->notifyObservers(&e); + } } + } - return 50; // Keyscan every 50msec to avoid key bounce + return 50; // Keyscan every 50msec to avoid key bounce } #endif \ No newline at end of file diff --git a/src/input/LinuxInput.h b/src/input/LinuxInput.h index 43d08493c..606a04c77 100644 --- a/src/input/LinuxInput.h +++ b/src/input/LinuxInput.h @@ -17,49 +17,47 @@ #define MAX_EVENTS 10 -class LinuxInput : public Observable, public concurrency::OSThread -{ - public: - explicit LinuxInput(const char *name); - void deInit(); // Strictly for cleanly "rebooting" the binary on native +class LinuxInput : public Observable, public concurrency::OSThread { +public: + explicit LinuxInput(const char *name); + void deInit(); // Strictly for cleanly "rebooting" the binary on native - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; - private: - const char *_originName; - bool firstTime = 1; - int shift = 0; - char key = 0; - char prevkey = 0; +private: + const char *_originName; + bool firstTime = 1; + int shift = 0; + char key = 0; + char prevkey = 0; - InputEvent eventqueue[50]; // The Linux API will return multiple keypresses at a time. Queue them to not miss any. - int queue_length = 0; - int queue_progress = 0; + InputEvent eventqueue[50]; // The Linux API will return multiple keypresses at a time. Queue them to not miss any. + int queue_length = 0; + int queue_progress = 0; - struct epoll_event events[MAX_EVENTS]; - int fd = -1; - int ret; - uint8_t report[8]; - int epollfd; - struct epoll_event ev; - uint8_t modifiers = 0; - std::map keymap{ - {KEY_A, 'a'}, {KEY_B, 'b'}, {KEY_C, 'c'}, {KEY_D, 'd'}, {KEY_E, 'e'}, - {KEY_F, 'f'}, {KEY_G, 'g'}, {KEY_H, 'h'}, {KEY_I, 'i'}, {KEY_J, 'j'}, - {KEY_K, 'k'}, {KEY_L, 'l'}, {KEY_M, 'm'}, {KEY_N, 'n'}, {KEY_O, 'o'}, - {KEY_P, 'p'}, {KEY_Q, 'q'}, {KEY_R, 'r'}, {KEY_S, 's'}, {KEY_T, 't'}, - {KEY_U, 'u'}, {KEY_V, 'v'}, {KEY_W, 'w'}, {KEY_X, 'x'}, {KEY_Y, 'y'}, - {KEY_Z, 'z'}, {KEY_BACKSPACE, 0x08}, {KEY_SPACE, ' '}, {KEY_1, '1'}, {KEY_2, '2'}, - {KEY_3, '3'}, {KEY_4, '4'}, {KEY_5, '5'}, {KEY_6, '6'}, {KEY_7, '7'}, - {KEY_8, '8'}, {KEY_9, '9'}, {KEY_0, '0'}, {KEY_DOT, '.'}, {KEY_COMMA, ','}, - {KEY_MINUS, '-'}, {KEY_EQUAL, '='}, {KEY_LEFTBRACE, '['}, {KEY_RIGHTBRACE, ']'}, {KEY_BACKSLASH, '\\'}, - {KEY_SEMICOLON, ';'}, {KEY_APOSTROPHE, '\''}, {KEY_SLASH, '/'}, {KEY_TAB, 0x09}}; - std::map uppers{{'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'}, {'f', 'F'}, {'g', 'G'}, {'h', 'H'}, - {'i', 'I'}, {'j', 'J'}, {'k', 'K'}, {'l', 'L'}, {'m', 'M'}, {'n', 'N'}, {'o', 'O'}, {'p', 'P'}, - {'q', 'Q'}, {'r', 'R'}, {'s', 'S'}, {'t', 'T'}, {'u', 'U'}, {'v', 'V'}, {'w', 'W'}, {'x', 'X'}, - {'y', 'Y'}, {'z', 'Z'}, {'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'}, {'6', '^'}, - {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'}, {'.', '>'}, {',', '<'}, {'-', '_'}, {'=', '+'}, - {'[', '{'}, {']', '}'}, {'\\', '|'}, {';', ':'}, {'\'', '"'}, {'/', '?'}}; + struct epoll_event events[MAX_EVENTS]; + int fd = -1; + int ret; + uint8_t report[8]; + int epollfd; + struct epoll_event ev; + uint8_t modifiers = 0; + std::map keymap{{KEY_A, 'a'}, {KEY_B, 'b'}, {KEY_C, 'c'}, {KEY_D, 'd'}, {KEY_E, 'e'}, + {KEY_F, 'f'}, {KEY_G, 'g'}, {KEY_H, 'h'}, {KEY_I, 'i'}, {KEY_J, 'j'}, + {KEY_K, 'k'}, {KEY_L, 'l'}, {KEY_M, 'm'}, {KEY_N, 'n'}, {KEY_O, 'o'}, + {KEY_P, 'p'}, {KEY_Q, 'q'}, {KEY_R, 'r'}, {KEY_S, 's'}, {KEY_T, 't'}, + {KEY_U, 'u'}, {KEY_V, 'v'}, {KEY_W, 'w'}, {KEY_X, 'x'}, {KEY_Y, 'y'}, + {KEY_Z, 'z'}, {KEY_BACKSPACE, 0x08}, {KEY_SPACE, ' '}, {KEY_1, '1'}, {KEY_2, '2'}, + {KEY_3, '3'}, {KEY_4, '4'}, {KEY_5, '5'}, {KEY_6, '6'}, {KEY_7, '7'}, + {KEY_8, '8'}, {KEY_9, '9'}, {KEY_0, '0'}, {KEY_DOT, '.'}, {KEY_COMMA, ','}, + {KEY_MINUS, '-'}, {KEY_EQUAL, '='}, {KEY_LEFTBRACE, '['}, {KEY_RIGHTBRACE, ']'}, {KEY_BACKSLASH, '\\'}, + {KEY_SEMICOLON, ';'}, {KEY_APOSTROPHE, '\''}, {KEY_SLASH, '/'}, {KEY_TAB, 0x09}}; + std::map uppers{{'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'}, {'f', 'F'}, {'g', 'G'}, {'h', 'H'}, + {'i', 'I'}, {'j', 'J'}, {'k', 'K'}, {'l', 'L'}, {'m', 'M'}, {'n', 'N'}, {'o', 'O'}, {'p', 'P'}, + {'q', 'Q'}, {'r', 'R'}, {'s', 'S'}, {'t', 'T'}, {'u', 'U'}, {'v', 'V'}, {'w', 'W'}, {'x', 'X'}, + {'y', 'Y'}, {'z', 'Z'}, {'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'}, {'6', '^'}, + {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'}, {'.', '>'}, {',', '<'}, {'-', '_'}, {'=', '+'}, + {'[', '{'}, {']', '}'}, {'\\', '|'}, {';', ':'}, {'\'', '"'}, {'/', '?'}}; }; #endif \ No newline at end of file diff --git a/src/input/LinuxInputImpl.cpp b/src/input/LinuxInputImpl.cpp index 4ddda1923..96e5260f4 100644 --- a/src/input/LinuxInputImpl.cpp +++ b/src/input/LinuxInputImpl.cpp @@ -7,9 +7,6 @@ LinuxInputImpl *aLinuxInputImpl; LinuxInputImpl::LinuxInputImpl() : LinuxInput("LinuxInput") {} -void LinuxInputImpl::init() -{ - inputBroker->registerSource(this); -} +void LinuxInputImpl::init() { inputBroker->registerSource(this); } #endif \ No newline at end of file diff --git a/src/input/LinuxInputImpl.h b/src/input/LinuxInputImpl.h index e734b0294..ef4ead5ec 100644 --- a/src/input/LinuxInputImpl.h +++ b/src/input/LinuxInputImpl.h @@ -11,11 +11,10 @@ * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class LinuxInputImpl : public LinuxInput -{ - public: - LinuxInputImpl(); - void init(); +class LinuxInputImpl : public LinuxInput { +public: + LinuxInputImpl(); + void init(); }; extern LinuxInputImpl *aLinuxInputImpl; #endif \ No newline at end of file diff --git a/src/input/MPR121Keyboard.cpp b/src/input/MPR121Keyboard.cpp index 9bca6801d..b7858f51a 100644 --- a/src/input/MPR121Keyboard.cpp +++ b/src/input/MPR121Keyboard.cpp @@ -87,347 +87,320 @@ unsigned char MPR121_LongPressMap[12] = {MPR121_ESC, ' ', MPR121_NONE, // Rotated Layout uint8_t MPR121_KeyMap[12] = {2, 5, 8, 11, 1, 4, 7, 10, 0, 3, 6, 9}; -MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) -{ - // LOG_DEBUG("MPR121 @ %02x", m_addr); - state = Init; - last_key = -1; - last_tap = 0L; - char_idx = 0; - queue = ""; +MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) { + // LOG_DEBUG("MPR121 @ %02x", m_addr); + state = Init; + last_key = -1; + last_tap = 0L; + char_idx = 0; + queue = ""; } -void MPR121Keyboard::begin(uint8_t addr, TwoWire *wire) -{ - m_addr = addr; - m_wire = wire; +void MPR121Keyboard::begin(uint8_t addr, TwoWire *wire) { + m_addr = addr; + m_wire = wire; - m_wire->begin(); + m_wire->begin(); - reset(); + reset(); } -void MPR121Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) -{ - m_addr = addr; - m_wire = nullptr; - writeCallback = w; - readCallback = r; - reset(); +void MPR121Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); } -void MPR121Keyboard::reset() -{ - LOG_DEBUG("MPR121 Reset"); - // Trigger a MPR121 Soft Reset - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(_MPR121_REG_SOFT_RESET); - m_wire->endTransmission(); +void MPR121Keyboard::reset() { + LOG_DEBUG("MPR121 Reset"); + // Trigger a MPR121 Soft Reset + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(_MPR121_REG_SOFT_RESET); + m_wire->endTransmission(); + } + if (writeCallback) { + uint8_t data = 0; + writeCallback(m_addr, _MPR121_REG_SOFT_RESET, &data, 0); + } + delay(100); + // Reset Electrode Configuration to 0x00, Stop Mode + writeRegister(_MPR121_REG_ELECTRODE_CONFIG, 0x00); + delay(100); + + LOG_DEBUG("MPR121 Configuring"); + // Set touch release thresholds + for (uint8_t i = 0; i < 12; i++) { + // Set touch threshold + writeRegister(_MPR121_REG_TOUCH_THRESHOLD + (i * 2), 10); + delay(20); + // Set release threshold + writeRegister(_MPR121_REG_RELEASE_THRESHOLD + (i * 2), 5); + delay(20); + } + // Configure filtering and baseline registers + writeRegister(_MPR121_REG_MAX_HALF_DELTA_RISING, 0x05); + delay(20); + writeRegister(_MPR121_REG_MAX_HALF_DELTA_FALLING, 0x01); + delay(20); + writeRegister(_MPR121_REG_NOISE_HALF_DELTA_RISING, 0x01); + delay(20); + writeRegister(_MPR121_REG_NOISE_HALF_DELTA_FALLING, 0x05); + delay(20); + writeRegister(_MPR121_REG_NOISE_HALF_DELTA_TOUCHED, 0x00); + delay(20); + writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_RISING, 0x05); + delay(20); + writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_FALLING, 0x01); + delay(20); + writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_TOUCHED, 0x00); + delay(20); + writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_RISING, 0x00); + delay(20); + writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_FALLING, 0x00); + delay(20); + writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_TOUCHED, 0x00); + delay(20); + writeRegister(_MPR121_REG_AUTOCONF_CTRL0, 0x04); // Auto-config enable + delay(20); + writeRegister(_MPR121_REG_AUTOCONF_CTRL1, 0x00); // Ensure no auto-config interrupt + delay(20); + writeRegister(_MPR121_REG_DEBOUNCE, 0x02); + delay(20); + writeRegister(_MPR121_REG_CONFIG1, 0x20); + delay(20); + writeRegister(_MPR121_REG_CONFIG2, 0x21); + delay(20); + // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels + writeRegister(_MPR121_REG_ELECTRODE_CONFIG, ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); + delay(100); + LOG_DEBUG("MPR121 Run"); + state = Idle; +} + +void MPR121Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const { + pinMode(pin, INPUT_PULLUP); + ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); +} + +void MPR121Keyboard::detachInterrupt(uint8_t pin) const { ::detachInterrupt(pin); } + +uint8_t MPR121Keyboard::status() const { return readRegister16(_MPR121_REG_KEY); } + +uint8_t MPR121Keyboard::keyCount() const { + // Read the key register + uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); + return keyCount(keyRegister); +} + +uint8_t MPR121Keyboard::keyCount(uint16_t value) const { + // Mask the first 12 bits + uint16_t buttonState = value & _KEY_MASK; + + // Count how many bits are set to 1 (i.e., how many buttons are pressed) + uint8_t numButtonsPressed = 0; + for (uint8_t i = 0; i < 12; ++i) { + if (buttonState & (1 << i)) { + numButtonsPressed++; } - if (writeCallback) { - uint8_t data = 0; - writeCallback(m_addr, _MPR121_REG_SOFT_RESET, &data, 0); - } - delay(100); - // Reset Electrode Configuration to 0x00, Stop Mode - writeRegister(_MPR121_REG_ELECTRODE_CONFIG, 0x00); - delay(100); + } - LOG_DEBUG("MPR121 Configuring"); - // Set touch release thresholds - for (uint8_t i = 0; i < 12; i++) { - // Set touch threshold - writeRegister(_MPR121_REG_TOUCH_THRESHOLD + (i * 2), 10); - delay(20); - // Set release threshold - writeRegister(_MPR121_REG_RELEASE_THRESHOLD + (i * 2), 5); - delay(20); - } - // Configure filtering and baseline registers - writeRegister(_MPR121_REG_MAX_HALF_DELTA_RISING, 0x05); - delay(20); - writeRegister(_MPR121_REG_MAX_HALF_DELTA_FALLING, 0x01); - delay(20); - writeRegister(_MPR121_REG_NOISE_HALF_DELTA_RISING, 0x01); - delay(20); - writeRegister(_MPR121_REG_NOISE_HALF_DELTA_FALLING, 0x05); - delay(20); - writeRegister(_MPR121_REG_NOISE_HALF_DELTA_TOUCHED, 0x00); - delay(20); - writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_RISING, 0x05); - delay(20); - writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_FALLING, 0x01); - delay(20); - writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_TOUCHED, 0x00); - delay(20); - writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_RISING, 0x00); - delay(20); - writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_FALLING, 0x00); - delay(20); - writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_TOUCHED, 0x00); - delay(20); - writeRegister(_MPR121_REG_AUTOCONF_CTRL0, 0x04); // Auto-config enable - delay(20); - writeRegister(_MPR121_REG_AUTOCONF_CTRL1, 0x00); // Ensure no auto-config interrupt - delay(20); - writeRegister(_MPR121_REG_DEBOUNCE, 0x02); - delay(20); - writeRegister(_MPR121_REG_CONFIG1, 0x20); - delay(20); - writeRegister(_MPR121_REG_CONFIG2, 0x21); - delay(20); - // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels - writeRegister(_MPR121_REG_ELECTRODE_CONFIG, - ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); - delay(100); - LOG_DEBUG("MPR121 Run"); - state = Idle; + return numButtonsPressed; } -void MPR121Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const -{ - pinMode(pin, INPUT_PULLUP); - ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); +bool MPR121Keyboard::hasEvent() { return queue.length() > 0; } + +void MPR121Keyboard::queueEvent(char next) { + if (next == MPR121_NONE) { + return; + } + queue.concat(next); } -void MPR121Keyboard::detachInterrupt(uint8_t pin) const -{ - ::detachInterrupt(pin); +char MPR121Keyboard::dequeueEvent() { + if (queue.length() < 1) { + return MPR121_NONE; + } + char next = queue.charAt(0); + queue.remove(0, 1); + return next; } -uint8_t MPR121Keyboard::status() const -{ - return readRegister16(_MPR121_REG_KEY); -} - -uint8_t MPR121Keyboard::keyCount() const -{ +void MPR121Keyboard::trigger() { + // Intended to fire in response to an interrupt from the MPR121 or a longpress callback + // Only functional if not in Init state + if (state != Init) { // Read the key register uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); - return keyCount(keyRegister); + uint8_t keysPressed = keyCount(keyRegister); + if (keysPressed == 0) { + // No buttons pressed + if (state == Held) + released(); + state = Idle; + return; + } + if (keysPressed == 1) { + // No buttons pressed + if (state == Held || state == HeldLong) + held(keyRegister); + if (state == Idle) + pressed(keyRegister); + return; + } + if (keysPressed > 1) { + // Multipress + state = Busy; + return; + } + } else { + reset(); + } } -uint8_t MPR121Keyboard::keyCount(uint16_t value) const -{ - // Mask the first 12 bits - uint16_t buttonState = value & _KEY_MASK; - - // Count how many bits are set to 1 (i.e., how many buttons are pressed) - uint8_t numButtonsPressed = 0; - for (uint8_t i = 0; i < 12; ++i) { - if (buttonState & (1 << i)) { - numButtonsPressed++; - } +void MPR121Keyboard::pressed(uint16_t keyRegister) { + if (state == Init || state == Busy) { + return; + } + if (keyCount(keyRegister) != 1) { + LOG_DEBUG("Multipress"); + return; + } else { + LOG_DEBUG("Pressed"); + } + uint16_t buttonState = keyRegister & _KEY_MASK; + uint8_t next_pin = 0; + for (uint8_t i = 0; i < 12; ++i) { + if (buttonState & (1 << i)) { + next_pin = i; } - - return numButtonsPressed; + } + uint8_t next_key = MPR121_KeyMap[next_pin]; + LOG_DEBUG("MPR121 Pin: %i Key: %i", next_pin, next_key); + uint32_t now = millis(); + int32_t tap_interval = now - last_tap; + if (tap_interval < 0) { + // long running, millis has overflowed. + last_tap = 0; + state = Busy; + return; + } + if (next_key != last_key || tap_interval > MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + last_key = next_key; + last_tap = now; + state = Held; + return; } -bool MPR121Keyboard::hasEvent() -{ - return queue.length() > 0; -} - -void MPR121Keyboard::queueEvent(char next) -{ - if (next == MPR121_NONE) { - return; +void MPR121Keyboard::held(uint16_t keyRegister) { + if (state == Init || state == Busy) { + return; + } + if (keyCount(keyRegister) != 1) { + return; + } + LOG_DEBUG("Held"); + uint16_t buttonState = keyRegister & _KEY_MASK; + uint8_t next_key = 0; + for (uint8_t i = 0; i < 12; ++i) { + if (buttonState & (1 << i)) { + next_key = MPR121_KeyMap[i]; } - queue.concat(next); -} - -char MPR121Keyboard::dequeueEvent() -{ - if (queue.length() < 1) { - return MPR121_NONE; - } - char next = queue.charAt(0); - queue.remove(0, 1); - return next; -} - -void MPR121Keyboard::trigger() -{ - // Intended to fire in response to an interrupt from the MPR121 or a longpress callback - // Only functional if not in Init state - if (state != Init) { - // Read the key register - uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); - uint8_t keysPressed = keyCount(keyRegister); - if (keysPressed == 0) { - // No buttons pressed - if (state == Held) - released(); - state = Idle; - return; - } - if (keysPressed == 1) { - // No buttons pressed - if (state == Held || state == HeldLong) - held(keyRegister); - if (state == Idle) - pressed(keyRegister); - return; - } - if (keysPressed > 1) { - // Multipress - state = Busy; - return; - } - } else { - reset(); - } -} - -void MPR121Keyboard::pressed(uint16_t keyRegister) -{ - if (state == Init || state == Busy) { - return; - } - if (keyCount(keyRegister) != 1) { - LOG_DEBUG("Multipress"); - return; - } else { - LOG_DEBUG("Pressed"); - } - uint16_t buttonState = keyRegister & _KEY_MASK; - uint8_t next_pin = 0; - for (uint8_t i = 0; i < 12; ++i) { - if (buttonState & (1 << i)) { - next_pin = i; - } - } - uint8_t next_key = MPR121_KeyMap[next_pin]; - LOG_DEBUG("MPR121 Pin: %i Key: %i", next_pin, next_key); - uint32_t now = millis(); - int32_t tap_interval = now - last_tap; - if (tap_interval < 0) { - // long running, millis has overflowed. - last_tap = 0; - state = Busy; - return; - } - if (next_key != last_key || tap_interval > MULTI_TAP_THRESHOLD) { - char_idx = 0; - } else { - char_idx += 1; - } - last_key = next_key; + } + uint32_t now = millis(); + int32_t held_interval = now - last_tap; + if (held_interval < 0 || next_key != last_key) { + // long running, millis has overflowed, or a key has been switched quickly... + last_tap = 0; + state = Busy; + return; + } + if (held_interval > LONG_PRESS_THRESHOLD) { + // Set state to heldlong, send a longpress, and reset the timer... + state = HeldLong; // heldlong will allow this function to still fire, but prevent a "release" + queueEvent(MPR121_LongPressMap[last_key]); last_tap = now; - state = Held; + LOG_DEBUG("Long Press Key: %i Map: %i", last_key, MPR121_LongPressMap[last_key]); + } + return; +} + +void MPR121Keyboard::released() { + if (state != Held) { return; -} - -void MPR121Keyboard::held(uint16_t keyRegister) -{ - if (state == Init || state == Busy) { - return; - } - if (keyCount(keyRegister) != 1) { - return; - } - LOG_DEBUG("Held"); - uint16_t buttonState = keyRegister & _KEY_MASK; - uint8_t next_key = 0; - for (uint8_t i = 0; i < 12; ++i) { - if (buttonState & (1 << i)) { - next_key = MPR121_KeyMap[i]; - } - } - uint32_t now = millis(); - int32_t held_interval = now - last_tap; - if (held_interval < 0 || next_key != last_key) { - // long running, millis has overflowed, or a key has been switched quickly... - last_tap = 0; - state = Busy; - return; - } - if (held_interval > LONG_PRESS_THRESHOLD) { - // Set state to heldlong, send a longpress, and reset the timer... - state = HeldLong; // heldlong will allow this function to still fire, but prevent a "release" - queueEvent(MPR121_LongPressMap[last_key]); - last_tap = now; - LOG_DEBUG("Long Press Key: %i Map: %i", last_key, MPR121_LongPressMap[last_key]); - } + } + // would clear longpress callback... later. + if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = -1; + state = Idle; return; + } + LOG_DEBUG("Released"); + if (char_idx > 0 && TapMod[last_key] > 1) { + queueEvent(MPR121_BSP); + LOG_DEBUG("Multi Press, Backspace"); + } + queueEvent(MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); + LOG_DEBUG("Key Press: %i Index:%i if %i Map: %i", last_key, char_idx, TapMod[last_key], MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); } -void MPR121Keyboard::released() -{ - if (state != Held) { - return; - } - // would clear longpress callback... later. - if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; - state = Idle; - return; - } - LOG_DEBUG("Released"); - if (char_idx > 0 && TapMod[last_key] > 1) { - queueEvent(MPR121_BSP); - LOG_DEBUG("Multi Press, Backspace"); - } - queueEvent(MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); - LOG_DEBUG("Key Press: %i Index:%i if %i Map: %i", last_key, char_idx, TapMod[last_key], - MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); +uint8_t MPR121Keyboard::readRegister8(uint8_t reg) const { + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); + + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) + return 0; + + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; } -uint8_t MPR121Keyboard::readRegister8(uint8_t reg) const -{ - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); +uint16_t MPR121Keyboard::readRegister16(uint8_t reg) const { + uint8_t data[2] = {0}; + // uint8_t low = 0, high = 0; + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); - m_wire->requestFrom(m_addr, (uint8_t)1); - if (m_wire->available() < 1) - return 0; - - return m_wire->read(); - } - if (readCallback) { - uint8_t data; - readCallback(m_addr, reg, &data, 1); - return data; - } - return 0; + m_wire->requestFrom(m_addr, (uint8_t)2); + if (m_wire->available() < 2) + return 0; + data[0] = m_wire->read(); + data[1] = m_wire->read(); + } + if (readCallback) { + readCallback(m_addr, reg, data, 2); + } + return (data[1] << 8) | data[0]; } -uint16_t MPR121Keyboard::readRegister16(uint8_t reg) const -{ - uint8_t data[2] = {0}; - // uint8_t low = 0, high = 0; - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); +void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value) { + uint8_t data[2]; + data[0] = reg; + data[1] = value; - m_wire->requestFrom(m_addr, (uint8_t)2); - if (m_wire->available() < 2) - return 0; - data[0] = m_wire->read(); - data[1] = m_wire->read(); - } - if (readCallback) { - readCallback(m_addr, reg, data, 2); - } - return (data[1] << 8) | data[0]; -} - -void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value) -{ - uint8_t data[2]; - data[0] = reg; - data[1] = value; - - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(data, sizeof(uint8_t) * 2); - m_wire->endTransmission(); - } - if (writeCallback) { - writeCallback(m_addr, data[0], &(data[1]), 1); - } + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(data, sizeof(uint8_t) * 2); + m_wire->endTransmission(); + } + if (writeCallback) { + writeCallback(m_addr, data[0], &(data[1]), 1); + } } \ No newline at end of file diff --git a/src/input/MPR121Keyboard.h b/src/input/MPR121Keyboard.h index 6349750ce..baa29d6e1 100644 --- a/src/input/MPR121Keyboard.h +++ b/src/input/MPR121Keyboard.h @@ -5,52 +5,51 @@ #include #include -class MPR121Keyboard -{ - public: - typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); +class MPR121Keyboard { +public: + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); - enum MPR121States { Init = 0, Idle, Held, HeldLong, Busy }; + enum MPR121States { Init = 0, Idle, Held, HeldLong, Busy }; - MPR121States state; + MPR121States state; - int8_t last_key; - uint32_t last_tap; - uint8_t char_idx; + int8_t last_key; + uint32_t last_tap; + uint8_t char_idx; - String queue; + String queue; - MPR121Keyboard(); + MPR121Keyboard(); - void begin(uint8_t addr = MPR121_KB_ADDR, TwoWire *wire = &Wire); + void begin(uint8_t addr = MPR121_KB_ADDR, TwoWire *wire = &Wire); - void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = MPR121_KB_ADDR); + void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = MPR121_KB_ADDR); - void reset(void); + void reset(void); - void attachInterrupt(uint8_t pin, void (*func)(void)) const; - void detachInterrupt(uint8_t pin) const; + void attachInterrupt(uint8_t pin, void (*func)(void)) const; + void detachInterrupt(uint8_t pin) const; - void trigger(void); - void pressed(uint16_t value); - void held(uint16_t value); - void released(void); + void trigger(void); + void pressed(uint16_t value); + void held(uint16_t value); + void released(void); - uint8_t status(void) const; - uint8_t keyCount(void) const; - uint8_t keyCount(uint16_t value) const; + uint8_t status(void) const; + uint8_t keyCount(void) const; + uint8_t keyCount(uint16_t value) const; - bool hasEvent(void); - char dequeueEvent(void); - void queueEvent(char); + bool hasEvent(void); + char dequeueEvent(void); + void queueEvent(char); - uint8_t readRegister8(uint8_t reg) const; - uint16_t readRegister16(uint8_t reg) const; - void writeRegister(uint8_t reg, uint8_t value); + uint8_t readRegister8(uint8_t reg) const; + uint16_t readRegister16(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); - private: - TwoWire *m_wire; - uint8_t m_addr; - i2c_com_fptr_t readCallback; - i2c_com_fptr_t writeCallback; +private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; }; \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index cc1222595..dbcaf1814 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -11,131 +11,122 @@ RotaryEncoderImpl *rotaryEncoderImpl; -RotaryEncoderImpl::RotaryEncoderImpl() -{ +RotaryEncoderImpl::RotaryEncoderImpl() { + rotary = nullptr; +#ifdef ARCH_ESP32 + isFirstInit = true; +#endif +} + +RotaryEncoderImpl::~RotaryEncoderImpl() { + LOG_DEBUG("RotaryEncoderImpl destructor"); + detachRotaryEncoderInterrupts(); + + if (rotary != nullptr) { + delete rotary; rotary = nullptr; -#ifdef ARCH_ESP32 - isFirstInit = true; -#endif + } } -RotaryEncoderImpl::~RotaryEncoderImpl() -{ - LOG_DEBUG("RotaryEncoderImpl destructor"); - detachRotaryEncoderInterrupts(); +bool RotaryEncoderImpl::init() { + if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || + moduleConfig.canned_message.inputbroker_pin_b == 0) { + // Input device is disabled. + return false; + } - if (rotary != nullptr) { - delete rotary; - rotary = nullptr; - } -} + eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); -bool RotaryEncoderImpl::init() -{ - if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || - moduleConfig.canned_message.inputbroker_pin_b == 0) { - // Input device is disabled. - return false; - } + if (rotary == nullptr) { + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + } - eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); - eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); - eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); - - if (rotary == nullptr) { - rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, - moduleConfig.canned_message.inputbroker_pin_press); - } - - attachRotaryEncoderInterrupts(); + attachRotaryEncoderInterrupts(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - if (isFirstInit) { - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); - isFirstInit = false; - } + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + if (isFirstInit) { + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + isFirstInit = false; + } #endif - LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, - moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, - eventPressed); - return true; + LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, + moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, eventPressed); + return true; } -void RotaryEncoderImpl::pollOnce() -{ - InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0}; +void RotaryEncoderImpl::pollOnce() { + InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0}; - static uint32_t lastPressed = millis(); - if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { - if (lastPressed + 200 < millis()) { - LOG_DEBUG("Rotary event Press"); - lastPressed = millis(); - e.inputEvent = this->eventPressed; - inputBroker->queueInputEvent(&e); - } + static uint32_t lastPressed = millis(); + if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { + if (lastPressed + 200 < millis()) { + LOG_DEBUG("Rotary event Press"); + lastPressed = millis(); + e.inputEvent = this->eventPressed; + inputBroker->queueInputEvent(&e); } + } - switch (rotary->process()) { - case RotaryEncoder::DIRECTION_CW: - LOG_DEBUG("Rotary event CW"); - e.inputEvent = this->eventCw; - inputBroker->queueInputEvent(&e); - break; - case RotaryEncoder::DIRECTION_CCW: - LOG_DEBUG("Rotary event CCW"); - e.inputEvent = this->eventCcw; - inputBroker->queueInputEvent(&e); - break; - default: - break; - } + switch (rotary->process()) { + case RotaryEncoder::DIRECTION_CW: + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->eventCw; + inputBroker->queueInputEvent(&e); + break; + case RotaryEncoder::DIRECTION_CCW: + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->eventCcw; + inputBroker->queueInputEvent(&e); + break; + default: + break; + } } -void RotaryEncoderImpl::detachRotaryEncoderInterrupts() -{ - LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); - if (interruptInstance == this) { - detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); - detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); - detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); - interruptInstance = nullptr; - } else { - LOG_WARN("RotaryEncoderImpl: interrupts already detached"); - } +void RotaryEncoderImpl::detachRotaryEncoderInterrupts() { + LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); + if (interruptInstance == this) { + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); + interruptInstance = nullptr; + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already detached"); + } } -void RotaryEncoderImpl::attachRotaryEncoderInterrupts() -{ - LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); - if (rotary != nullptr && interruptInstance == nullptr) { - rotary->resetButton(); +void RotaryEncoderImpl::attachRotaryEncoderInterrupts() { + LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); + if (rotary != nullptr && interruptInstance == nullptr) { + rotary->resetButton(); - interruptInstance = this; - auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); - } else { - LOG_WARN("RotaryEncoderImpl: interrupts already attached"); - } + interruptInstance = this; + auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already attached"); + } } #ifdef ARCH_ESP32 -int RotaryEncoderImpl::beforeLightSleep(void *unused) -{ - detachRotaryEncoderInterrupts(); - return 0; // Indicates success; +int RotaryEncoderImpl::beforeLightSleep(void *unused) { + detachRotaryEncoderInterrupts(); + return 0; // Indicates success; } -int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) -{ - attachRotaryEncoderInterrupts(); - return 0; // Indicates success; +int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) { + attachRotaryEncoderInterrupts(); + return 0; // Indicates success; } #endif diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h index ec8a064bd..3a6fdd693 100644 --- a/src/input/RotaryEncoderImpl.h +++ b/src/input/RotaryEncoderImpl.h @@ -8,41 +8,39 @@ class RotaryEncoder; -class RotaryEncoderImpl final : public InputPollable -{ - public: - RotaryEncoderImpl(); - ~RotaryEncoderImpl() override; - bool init(); - virtual void pollOnce() override; - // Disconnect and reconnect interrupts for light sleep +class RotaryEncoderImpl final : public InputPollable { +public: + RotaryEncoderImpl(); + ~RotaryEncoderImpl() override; + bool init(); + virtual void pollOnce() override; + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif - protected: - static RotaryEncoderImpl *interruptInstance; +protected: + static RotaryEncoderImpl *interruptInstance; - input_broker_event eventCw = INPUT_BROKER_NONE; - input_broker_event eventCcw = INPUT_BROKER_NONE; - input_broker_event eventPressed = INPUT_BROKER_NONE; + input_broker_event eventCw = INPUT_BROKER_NONE; + input_broker_event eventCcw = INPUT_BROKER_NONE; + input_broker_event eventPressed = INPUT_BROKER_NONE; - RotaryEncoder *rotary; + RotaryEncoder *rotary; - private: +private: #ifdef ARCH_ESP32 - bool isFirstInit; + bool isFirstInit; #endif - void detachRotaryEncoderInterrupts(); - void attachRotaryEncoderInterrupts(); + void detachRotaryEncoderInterrupts(); + void attachRotaryEncoderInterrupts(); #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); #endif }; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index c315f23d9..8cd050bb7 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -1,129 +1,120 @@ #include "RotaryEncoderInterruptBase.h" #include "configuration.h" -RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concurrency::OSThread(name) -{ - this->_originName = name; -} +RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concurrency::OSThread(name) { this->_originName = name; } void RotaryEncoderInterruptBase::init( - uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, - input_broker_event eventPressed, input_broker_event eventPressedLong, + uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, input_broker_event eventPressed, + input_broker_event eventPressedLong, // std::function onIntA, std::function onIntB, std::function onIntPress) : - void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) -{ - this->_pinA = pinA; - this->_pinB = pinB; - this->_pinPress = pinPress; - this->_eventCw = eventCw; - this->_eventCcw = eventCcw; - this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) { + this->_pinA = pinA; + this->_pinB = pinB; + this->_pinPress = pinPress; + this->_eventCw = eventCw; + this->_eventCcw = eventCcw; + this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; - bool isRAK = false; + bool isRAK = false; #ifdef RAK_4631 - isRAK = true; + isRAK = true; #endif - if (!isRAK || pinPress != 0) { - pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, CHANGE); - } - if (!isRAK || this->_pinA != 0) { - pinMode(this->_pinA, INPUT_PULLUP); - attachInterrupt(this->_pinA, onIntA, CHANGE); - } - if (!isRAK || this->_pinA != 0) { - pinMode(this->_pinB, INPUT_PULLUP); - attachInterrupt(this->_pinB, onIntB, CHANGE); - } + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinA, INPUT_PULLUP); + attachInterrupt(this->_pinA, onIntA, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinB, INPUT_PULLUP); + attachInterrupt(this->_pinB, onIntB, CHANGE); + } - this->rotaryLevelA = digitalRead(this->_pinA); - this->rotaryLevelB = digitalRead(this->_pinB); - LOG_INFO("Rotary initialized (%d, %d, %d)", this->_pinA, this->_pinB, pinPress); + this->rotaryLevelA = digitalRead(this->_pinA); + this->rotaryLevelB = digitalRead(this->_pinB); + LOG_INFO("Rotary initialized (%d, %d, %d)", this->_pinA, this->_pinB, pinPress); } -int32_t RotaryEncoderInterruptBase::runOnce() -{ - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - unsigned long now = millis(); +int32_t RotaryEncoderInterruptBase::runOnce() { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + unsigned long now = millis(); - // Handle press long/short detection - if (this->action == ROTARY_ACTION_PRESSED) { - bool buttonPressed = !digitalRead(_pinPress); - if (!pressDetected && buttonPressed) { - pressDetected = true; - pressStartTime = now; - } - - if (pressDetected) { - uint32_t duration = now - pressStartTime; - if (!buttonPressed) { - // released -> if short press, send short, else already sent long - if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { - lastPressKeyTime = now; - LOG_DEBUG("Rotary event Press short"); - e.inputEvent = this->_eventPressed; - } - pressDetected = false; - pressStartTime = 0; - lastPressLongEventTime = 0; - this->action = ROTARY_ACTION_NONE; - } else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && - lastPressLongEventTime == 0) { - // fire single-shot long press - lastPressLongEventTime = now; - LOG_DEBUG("Rotary event Press long"); - e.inputEvent = this->_eventPressedLong; - } - } - } else if (this->action == ROTARY_ACTION_CW) { - LOG_DEBUG("Rotary event CW"); - e.inputEvent = this->_eventCw; - } else if (this->action == ROTARY_ACTION_CCW) { - LOG_DEBUG("Rotary event CCW"); - e.inputEvent = this->_eventCcw; + // Handle press long/short detection + if (this->action == ROTARY_ACTION_PRESSED) { + bool buttonPressed = !digitalRead(_pinPress); + if (!pressDetected && buttonPressed) { + pressDetected = true; + pressStartTime = now; } - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } - - if (!pressDetected) { + if (pressDetected) { + uint32_t duration = now - pressStartTime; + if (!buttonPressed) { + // released -> if short press, send short, else already sent long + if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + LOG_DEBUG("Rotary event Press short"); + e.inputEvent = this->_eventPressed; + } + pressDetected = false; + pressStartTime = 0; + lastPressLongEventTime = 0; this->action = ROTARY_ACTION_NONE; + } else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) { + // fire single-shot long press + lastPressLongEventTime = now; + LOG_DEBUG("Rotary event Press long"); + e.inputEvent = this->_eventPressedLong; + } } + } else if (this->action == ROTARY_ACTION_CW) { + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->_eventCw; + } else if (this->action == ROTARY_ACTION_CCW) { + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->_eventCcw; + } - return INT32_MAX; + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + + if (!pressDetected) { + this->action = ROTARY_ACTION_NONE; + } + + return INT32_MAX; } -void RotaryEncoderInterruptBase::intPressHandler() -{ - this->action = ROTARY_ACTION_PRESSED; - setIntervalFromNow(20); // start checking for long/short +void RotaryEncoderInterruptBase::intPressHandler() { + this->action = ROTARY_ACTION_PRESSED; + setIntervalFromNow(20); // start checking for long/short } -void RotaryEncoderInterruptBase::intAHandler() -{ - // CW rotation (at least on most common rotary encoders) - int currentLevelA = digitalRead(this->_pinA); - if (this->rotaryLevelA == currentLevelA) { - return; - } - this->rotaryLevelA = currentLevelA; - this->rotaryStateCCW = intHandler(currentLevelA == HIGH, this->rotaryLevelB, ROTARY_ACTION_CCW, this->rotaryStateCCW); +void RotaryEncoderInterruptBase::intAHandler() { + // CW rotation (at least on most common rotary encoders) + int currentLevelA = digitalRead(this->_pinA); + if (this->rotaryLevelA == currentLevelA) { + return; + } + this->rotaryLevelA = currentLevelA; + this->rotaryStateCCW = intHandler(currentLevelA == HIGH, this->rotaryLevelB, ROTARY_ACTION_CCW, this->rotaryStateCCW); } -void RotaryEncoderInterruptBase::intBHandler() -{ - // CW rotation (at least on most common rotary encoders) - int currentLevelB = digitalRead(this->_pinB); - if (this->rotaryLevelB == currentLevelB) { - return; - } - this->rotaryLevelB = currentLevelB; - this->rotaryStateCW = intHandler(currentLevelB == HIGH, this->rotaryLevelA, ROTARY_ACTION_CW, this->rotaryStateCW); +void RotaryEncoderInterruptBase::intBHandler() { + // CW rotation (at least on most common rotary encoders) + int currentLevelB = digitalRead(this->_pinB); + if (this->rotaryLevelB == currentLevelB) { + return; + } + this->rotaryLevelB = currentLevelB; + this->rotaryStateCW = intHandler(currentLevelB == HIGH, this->rotaryLevelA, ROTARY_ACTION_CW, this->rotaryStateCW); } /** @@ -137,21 +128,20 @@ void RotaryEncoderInterruptBase::intBHandler() */ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool actualPinRaising, int otherPinLevel, RotaryEncoderInterruptBaseActionType action, - RotaryEncoderInterruptBaseStateType state) -{ - RotaryEncoderInterruptBaseStateType newState = state; - if (actualPinRaising && (otherPinLevel == LOW)) { - if (state == ROTARY_EVENT_CLEARED) { - newState = ROTARY_EVENT_OCCURRED; - if ((this->action != ROTARY_ACTION_PRESSED) && (this->action != action)) { - this->action = action; - } - } - } else if (!actualPinRaising && (otherPinLevel == HIGH)) { - // Logic to prevent bouncing. - newState = ROTARY_EVENT_CLEARED; + RotaryEncoderInterruptBaseStateType state) { + RotaryEncoderInterruptBaseStateType newState = state; + if (actualPinRaising && (otherPinLevel == LOW)) { + if (state == ROTARY_EVENT_CLEARED) { + newState = ROTARY_EVENT_OCCURRED; + if ((this->action != ROTARY_ACTION_PRESSED) && (this->action != action)) { + this->action = action; + } } - setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! + } else if (!actualPinRaising && (otherPinLevel == HIGH)) { + // Logic to prevent bouncing. + newState = ROTARY_EVENT_CLEARED; + } + setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! - return newState; + return newState; } diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h index 4f9757609..b856d69bd 100644 --- a/src/input/RotaryEncoderInterruptBase.h +++ b/src/input/RotaryEncoderInterruptBase.h @@ -8,47 +8,46 @@ enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_C enum RotaryEncoderInterruptBaseActionType { ROTARY_ACTION_NONE, ROTARY_ACTION_PRESSED, ROTARY_ACTION_CW, ROTARY_ACTION_CCW }; -class RotaryEncoderInterruptBase : public Observable, public concurrency::OSThread -{ - public: - explicit RotaryEncoderInterruptBase(const char *name); - void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, - input_broker_event eventPressed, input_broker_event eventPressedLong, - // std::function onIntA, std::function onIntB, std::function onIntPress); - void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); - void intPressHandler(); - void intAHandler(); - void intBHandler(); +class RotaryEncoderInterruptBase : public Observable, public concurrency::OSThread { +public: + explicit RotaryEncoderInterruptBase(const char *name); + void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, input_broker_event eventPressed, + input_broker_event eventPressedLong, + // std::function onIntA, std::function onIntB, std::function + // onIntPress); + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); + void intPressHandler(); + void intAHandler(); + void intBHandler(); - protected: - virtual int32_t runOnce() override; - RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel, - RotaryEncoderInterruptBaseActionType action, - RotaryEncoderInterruptBaseStateType state); +protected: + virtual int32_t runOnce() override; + RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel, RotaryEncoderInterruptBaseActionType action, + RotaryEncoderInterruptBaseStateType state); - volatile RotaryEncoderInterruptBaseStateType rotaryStateCW = ROTARY_EVENT_CLEARED; - volatile RotaryEncoderInterruptBaseStateType rotaryStateCCW = ROTARY_EVENT_CLEARED; - volatile int rotaryLevelA = LOW; - volatile int rotaryLevelB = LOW; - volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; + volatile RotaryEncoderInterruptBaseStateType rotaryStateCW = ROTARY_EVENT_CLEARED; + volatile RotaryEncoderInterruptBaseStateType rotaryStateCCW = ROTARY_EVENT_CLEARED; + volatile int rotaryLevelA = LOW; + volatile int rotaryLevelB = LOW; + volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; - private: - // pins and events - uint8_t _pinA = 0; - uint8_t _pinB = 0; - uint8_t _pinPress = 0; - input_broker_event _eventCw = INPUT_BROKER_NONE; - input_broker_event _eventCcw = INPUT_BROKER_NONE; - input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; - const char *_originName; +private: + // pins and events + uint8_t _pinA = 0; + uint8_t _pinB = 0; + uint8_t _pinPress = 0; + input_broker_event _eventCw = INPUT_BROKER_NONE; + input_broker_event _eventCcw = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + const char *_originName; - // Long press detection variables - uint32_t pressStartTime = 0; - bool pressDetected = false; - uint32_t lastPressLongEventTime = 0; - unsigned long lastPressKeyTime = 0; - static const uint32_t LONG_PRESS_DURATION = 300; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select - const unsigned long pressDebounceMs = 200; // ms + // Long press detection variables + uint32_t pressStartTime = 0; + bool pressDetected = false; + uint32_t lastPressLongEventTime = 0; + unsigned long lastPressKeyTime = 0; + static const uint32_t LONG_PRESS_DURATION = 300; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select + const unsigned long pressDebounceMs = 200; // ms }; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 1da2ea008..03458ed29 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -6,42 +6,31 @@ RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; RotaryEncoderInterruptImpl1::RotaryEncoderInterruptImpl1() : RotaryEncoderInterruptBase("rotEnc1") {} -bool RotaryEncoderInterruptImpl1::init() -{ - if (!moduleConfig.canned_message.rotary1_enabled) { - // Input device is disabled. - disable(); - return false; - } +bool RotaryEncoderInterruptImpl1::init() { + if (!moduleConfig.canned_message.rotary1_enabled) { + // Input device is disabled. + disable(); + return false; + } - uint8_t pinA = moduleConfig.canned_message.inputbroker_pin_a; - uint8_t pinB = moduleConfig.canned_message.inputbroker_pin_b; - uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); - input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); - input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; + uint8_t pinA = moduleConfig.canned_message.inputbroker_pin_a; + uint8_t pinB = moduleConfig.canned_message.inputbroker_pin_b; + uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; + input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; - // moduleConfig.canned_message.ext_notification_module_output - RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong, - RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, - RotaryEncoderInterruptImpl1::handleIntPressed); - inputBroker->registerSource(this); + // moduleConfig.canned_message.ext_notification_module_output + RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong, RotaryEncoderInterruptImpl1::handleIntA, + RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif - return true; + return true; } -void RotaryEncoderInterruptImpl1::handleIntA() -{ - rotaryEncoderInterruptImpl1->intAHandler(); -} -void RotaryEncoderInterruptImpl1::handleIntB() -{ - rotaryEncoderInterruptImpl1->intBHandler(); -} -void RotaryEncoderInterruptImpl1::handleIntPressed() -{ - rotaryEncoderInterruptImpl1->intPressHandler(); -} \ No newline at end of file +void RotaryEncoderInterruptImpl1::handleIntA() { rotaryEncoderInterruptImpl1->intAHandler(); } +void RotaryEncoderInterruptImpl1::handleIntB() { rotaryEncoderInterruptImpl1->intBHandler(); } +void RotaryEncoderInterruptImpl1::handleIntPressed() { rotaryEncoderInterruptImpl1->intPressHandler(); } \ No newline at end of file diff --git a/src/input/RotaryEncoderInterruptImpl1.h b/src/input/RotaryEncoderInterruptImpl1.h index 22ecba37a..901f29d71 100644 --- a/src/input/RotaryEncoderInterruptImpl1.h +++ b/src/input/RotaryEncoderInterruptImpl1.h @@ -8,14 +8,13 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class RotaryEncoderInterruptImpl1 : public RotaryEncoderInterruptBase -{ - public: - RotaryEncoderInterruptImpl1(); - bool init(); - static void handleIntA(); - static void handleIntB(); - static void handleIntPressed(); +class RotaryEncoderInterruptImpl1 : public RotaryEncoderInterruptBase { +public: + RotaryEncoderInterruptImpl1(); + bool init(); + static void handleIntA(); + static void handleIntB(); + static void handleIntPressed(); }; extern RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; \ No newline at end of file diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp index 0a6e6e974..dce06c342 100644 --- a/src/input/SeesawRotary.cpp +++ b/src/input/SeesawRotary.cpp @@ -6,78 +6,73 @@ using namespace concurrency; SeesawRotary *seesawRotary; -SeesawRotary::SeesawRotary(const char *name) : OSThread(name) -{ - _originName = name; +SeesawRotary::SeesawRotary(const char *name) : OSThread(name) { _originName = name; } + +bool SeesawRotary::init() { + if (inputBroker) + inputBroker->registerSource(this); + + if (!ss.begin(SEESAW_ADDR)) { + return false; + } + // attachButtonInterrupts(); + + uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF); + if (version != 4991) { + LOG_WARN("Wrong firmware loaded? %u", version); + } else { + LOG_INFO("Found Product 4991"); + } + /* + #ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + #endif + */ + ss.pinMode(SS_SWITCH, INPUT_PULLUP); + + // get starting position + encoder_position = ss.getEncoderPosition(); + + ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); + ss.enableEncoderInterrupt(); + canSleep = true; // Assume we should not keep the board awake + + return true; } -bool SeesawRotary::init() -{ - if (inputBroker) - inputBroker->registerSource(this); +int32_t SeesawRotary::runOnce() { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + bool currentlyPressed = !ss.digitalRead(SS_SWITCH); - if (!ss.begin(SEESAW_ADDR)) { - return false; - } - // attachButtonInterrupts(); + if (currentlyPressed && !wasPressed) { + e.inputEvent = INPUT_BROKER_SELECT; + } + wasPressed = currentlyPressed; - uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF); - if (version != 4991) { - LOG_WARN("Wrong firmware loaded? %u", version); + int32_t new_position = ss.getEncoderPosition(); + // did we move arounde? + if (encoder_position != new_position) { + if (encoder_position == 0 && new_position != 1) { + e.inputEvent = INPUT_BROKER_ALT_PRESS; + } else if (new_position == 0 && encoder_position != 1) { + e.inputEvent = INPUT_BROKER_USER_PRESS; + } else if (new_position > encoder_position) { + e.inputEvent = INPUT_BROKER_USER_PRESS; } else { - LOG_INFO("Found Product 4991"); + e.inputEvent = INPUT_BROKER_ALT_PRESS; } - /* - #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); - #endif - */ - ss.pinMode(SS_SWITCH, INPUT_PULLUP); + encoder_position = new_position; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = 0x00; + this->notifyObservers(&e); + } - // get starting position - encoder_position = ss.getEncoderPosition(); - - ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); - ss.enableEncoderInterrupt(); - canSleep = true; // Assume we should not keep the board awake - - return true; -} - -int32_t SeesawRotary::runOnce() -{ - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - bool currentlyPressed = !ss.digitalRead(SS_SWITCH); - - if (currentlyPressed && !wasPressed) { - e.inputEvent = INPUT_BROKER_SELECT; - } - wasPressed = currentlyPressed; - - int32_t new_position = ss.getEncoderPosition(); - // did we move arounde? - if (encoder_position != new_position) { - if (encoder_position == 0 && new_position != 1) { - e.inputEvent = INPUT_BROKER_ALT_PRESS; - } else if (new_position == 0 && encoder_position != 1) { - e.inputEvent = INPUT_BROKER_USER_PRESS; - } else if (new_position > encoder_position) { - e.inputEvent = INPUT_BROKER_USER_PRESS; - } else { - e.inputEvent = INPUT_BROKER_ALT_PRESS; - } - encoder_position = new_position; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - e.source = this->_originName; - e.kbchar = 0x00; - this->notifyObservers(&e); - } - - return 50; + return 50; } #endif \ No newline at end of file diff --git a/src/input/SeesawRotary.h b/src/input/SeesawRotary.h index 3812b130a..6af0bb5ee 100644 --- a/src/input/SeesawRotary.h +++ b/src/input/SeesawRotary.h @@ -11,18 +11,17 @@ #define SEESAW_ADDR 0x36 -class SeesawRotary : public Observable, public concurrency::OSThread -{ - public: - const char *_originName; - bool init(); - SeesawRotary(const char *name); - int32_t runOnce() override; +class SeesawRotary : public Observable, public concurrency::OSThread { +public: + const char *_originName; + bool init(); + SeesawRotary(const char *name); + int32_t runOnce() override; - private: - Adafruit_seesaw ss; - int32_t encoder_position; - bool wasPressed = false; +private: + Adafruit_seesaw ss; + int32_t encoder_position; + bool wasPressed = false; }; extern SeesawRotary *seesawRotary; diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index a5d2c614f..61c7b9ab5 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -24,164 +24,161 @@ unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w', #endif -SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) -{ - this->_originName = name; +SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) { + this->_originName = name; - globalSerialKeyboard = this; + globalSerialKeyboard = this; } -void SerialKeyboard::erase() -{ - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - e.source = this->_originName; - this->notifyObservers(&e); +void SerialKeyboard::erase() { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + e.source = this->_originName; + this->notifyObservers(&e); } -int32_t SerialKeyboard::runOnce() -{ - if (!INPUTBROKER_SERIAL_TYPE) { - // Input device is not requested. - return disable(); +int32_t SerialKeyboard::runOnce() { + if (!INPUTBROKER_SERIAL_TYPE) { + // Input device is not requested. + return disable(); + } + + if (firstTime) { + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 0; + pinMode(KB_LOAD, OUTPUT); + pinMode(KB_CLK, OUTPUT); + pinMode(KB_DATA, INPUT); + digitalWrite(KB_LOAD, HIGH); + digitalWrite(KB_CLK, LOW); + prevKeys = 0b1111111111111111; + LOG_DEBUG("Serial Keyboard setup"); + } + + if (INPUTBROKER_SERIAL_TYPE == 1) { // Chatter V1.0 & V2.0 keypads + // scan for keypresses + // Write pulse to load pin + digitalWrite(KB_LOAD, LOW); + delayMicroseconds(5); + digitalWrite(KB_LOAD, HIGH); + delayMicroseconds(5); + + // Get data from 74HC165 + byte shiftRegister1 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); + byte shiftRegister2 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); + + keys = (shiftRegister1 << 8) + shiftRegister2; + + // Print to serial monitor + // Serial.print (shiftRegister1, BIN); + // Serial.print ("X"); + // Serial.println (shiftRegister2, BIN); + + if (!Throttle::isWithinTimespanMs(lastPressTime, 500)) { + quickPress = 0; } - if (firstTime) { - // This is the first time the OSThread library has called this function, so do port setup - firstTime = 0; - pinMode(KB_LOAD, OUTPUT); - pinMode(KB_CLK, OUTPUT); - pinMode(KB_DATA, INPUT); - digitalWrite(KB_LOAD, HIGH); - digitalWrite(KB_CLK, LOW); - prevKeys = 0b1111111111111111; - LOG_DEBUG("Serial Keyboard setup"); - } + if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once + // but shouldn't be a limitation + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + // SELECT OR SEND OR CANCEL EVENT + if (!(shiftRegister2 & (1 << 3))) { + if (shift > 0) { + e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED + e.kbchar = 0x09; // TAB + shift = 0; // reset shift after TAB + } else { + e.inputEvent = INPUT_BROKER_LEFT; + } + } else if (!(shiftRegister2 & (1 << 2))) { + if (shift > 0) { + e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED + e.kbchar = 0x09; // TAB + shift = 0; // reset shift after TAB + } else { + e.inputEvent = INPUT_BROKER_RIGHT; + } + e.kbchar = 0; + } else if (!(shiftRegister2 & (1 << 1))) { + e.inputEvent = INPUT_BROKER_SELECT; + } else if (!(shiftRegister2 & (1 << 0))) { + e.inputEvent = INPUT_BROKER_CANCEL; + } - if (INPUTBROKER_SERIAL_TYPE == 1) { // Chatter V1.0 & V2.0 keypads - // scan for keypresses - // Write pulse to load pin - digitalWrite(KB_LOAD, LOW); - delayMicroseconds(5); - digitalWrite(KB_LOAD, HIGH); - delayMicroseconds(5); + // TEXT INPUT EVENT + else if (!(shiftRegister1 & (1 << 4))) { + keyPressed = 0; + } else if (!(shiftRegister1 & (1 << 3))) { + keyPressed = 1; + } else if (!(shiftRegister2 & (1 << 4))) { + keyPressed = 2; + } else if (!(shiftRegister1 & (1 << 5))) { + keyPressed = 3; + } else if (!(shiftRegister1 & (1 << 2))) { + keyPressed = 4; + } else if (!(shiftRegister2 & (1 << 5))) { + keyPressed = 5; + } else if (!(shiftRegister1 & (1 << 6))) { + keyPressed = 6; + } else if (!(shiftRegister1 & (1 << 1))) { + keyPressed = 7; + } else if (!(shiftRegister2 & (1 << 6))) { + keyPressed = 8; + } else if (!(shiftRegister1 & (1 << 0))) { + keyPressed = 9; + } + // BACKSPACE or TAB + else if (!(shiftRegister1 & (1 << 7))) { + if (shift == 0 || shift == 2) { // BACKSPACE + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + } else { // shift = 1 => TAB + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x09; + } + } + // SHIFT + else if (!(shiftRegister2 & (1 << 7))) { + keyPressed = 10; + } - // Get data from 74HC165 - byte shiftRegister1 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); - byte shiftRegister2 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); - - keys = (shiftRegister1 << 8) + shiftRegister2; - - // Print to serial monitor - // Serial.print (shiftRegister1, BIN); - // Serial.print ("X"); - // Serial.println (shiftRegister2, BIN); - - if (!Throttle::isWithinTimespanMs(lastPressTime, 500)) { + if (keyPressed < 11) { + if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { + quickPress += 1; + if (quickPress > 3) { quickPress = 0; + } } - - if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but - // shouldn't be a limitation - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - // SELECT OR SEND OR CANCEL EVENT - if (!(shiftRegister2 & (1 << 3))) { - if (shift > 0) { - e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED - e.kbchar = 0x09; // TAB - shift = 0; // reset shift after TAB - } else { - e.inputEvent = INPUT_BROKER_LEFT; - } - } else if (!(shiftRegister2 & (1 << 2))) { - if (shift > 0) { - e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED - e.kbchar = 0x09; // TAB - shift = 0; // reset shift after TAB - } else { - e.inputEvent = INPUT_BROKER_RIGHT; - } - e.kbchar = 0; - } else if (!(shiftRegister2 & (1 << 1))) { - e.inputEvent = INPUT_BROKER_SELECT; - } else if (!(shiftRegister2 & (1 << 0))) { - e.inputEvent = INPUT_BROKER_CANCEL; - } - - // TEXT INPUT EVENT - else if (!(shiftRegister1 & (1 << 4))) { - keyPressed = 0; - } else if (!(shiftRegister1 & (1 << 3))) { - keyPressed = 1; - } else if (!(shiftRegister2 & (1 << 4))) { - keyPressed = 2; - } else if (!(shiftRegister1 & (1 << 5))) { - keyPressed = 3; - } else if (!(shiftRegister1 & (1 << 2))) { - keyPressed = 4; - } else if (!(shiftRegister2 & (1 << 5))) { - keyPressed = 5; - } else if (!(shiftRegister1 & (1 << 6))) { - keyPressed = 6; - } else if (!(shiftRegister1 & (1 << 1))) { - keyPressed = 7; - } else if (!(shiftRegister2 & (1 << 6))) { - keyPressed = 8; - } else if (!(shiftRegister1 & (1 << 0))) { - keyPressed = 9; - } - // BACKSPACE or TAB - else if (!(shiftRegister1 & (1 << 7))) { - if (shift == 0 || shift == 2) { // BACKSPACE - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - } else { // shift = 1 => TAB - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x09; - } - } - // SHIFT - else if (!(shiftRegister2 & (1 << 7))) { - keyPressed = 10; - } - - if (keyPressed < 11) { - if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { - quickPress += 1; - if (quickPress > 3) { - quickPress = 0; - } - } - if (keyPressed != lastKeyPressed) { - quickPress = 0; - } - if (keyPressed < 10) { // if it's a letter - if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { - erase(); - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); - } else { // then it's shift - shift += 1; - if (shift > 2) { - shift = 0; - } - } - lastPressTime = millis(); - lastKeyPressed = keyPressed; - keyPressed = 13; - } - - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } + if (keyPressed != lastKeyPressed) { + quickPress = 0; } - prevKeys = keys; + if (keyPressed < 10) { // if it's a letter + if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { + erase(); + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); + } else { // then it's shift + shift += 1; + if (shift > 2) { + shift = 0; + } + } + lastPressTime = millis(); + lastKeyPressed = keyPressed; + keyPressed = 13; + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } } - return 50; + prevKeys = keys; + } + return 50; } #endif // INPUTBROKER_SERIAL_TYPE \ No newline at end of file diff --git a/src/input/SerialKeyboard.h b/src/input/SerialKeyboard.h index f25eb2630..ebac7d673 100644 --- a/src/input/SerialKeyboard.h +++ b/src/input/SerialKeyboard.h @@ -3,27 +3,26 @@ #include "InputBroker.h" #include "concurrency/OSThread.h" -class SerialKeyboard : public Observable, public concurrency::OSThread -{ - public: - explicit SerialKeyboard(const char *name); +class SerialKeyboard : public Observable, public concurrency::OSThread { +public: + explicit SerialKeyboard(const char *name); - uint8_t getShift() const { return shift; } + uint8_t getShift() const { return shift; } - protected: - virtual int32_t runOnce() override; - void erase(); +protected: + virtual int32_t runOnce() override; + void erase(); - private: - const char *_originName; - bool firstTime = 1; - int prevKeys = 0; - int keys = 0; - int shift = 0; - int keyPressed = 13; - int lastKeyPressed = 13; - int quickPress = 0; - unsigned long lastPressTime = 0; +private: + const char *_originName; + bool firstTime = 1; + int prevKeys = 0; + int keys = 0; + int shift = 0; + int keyPressed = 13; + int lastKeyPressed = 13; + int quickPress = 0; + unsigned long lastPressTime = 0; }; extern SerialKeyboard *globalSerialKeyboard; \ No newline at end of file diff --git a/src/input/SerialKeyboardImpl.cpp b/src/input/SerialKeyboardImpl.cpp index 249b76fe3..e1d42bb19 100644 --- a/src/input/SerialKeyboardImpl.cpp +++ b/src/input/SerialKeyboardImpl.cpp @@ -8,14 +8,13 @@ SerialKeyboardImpl *aSerialKeyboardImpl; SerialKeyboardImpl::SerialKeyboardImpl() : SerialKeyboard("serialKB") {} -void SerialKeyboardImpl::init() -{ - if (!INPUTBROKER_SERIAL_TYPE) { - disable(); - return; - } +void SerialKeyboardImpl::init() { + if (!INPUTBROKER_SERIAL_TYPE) { + disable(); + return; + } - inputBroker->registerSource(this); + inputBroker->registerSource(this); } #endif // INPUTBROKER_SERIAL_TYPE \ No newline at end of file diff --git a/src/input/SerialKeyboardImpl.h b/src/input/SerialKeyboardImpl.h index 7f62aa420..d8908b81b 100644 --- a/src/input/SerialKeyboardImpl.h +++ b/src/input/SerialKeyboardImpl.h @@ -9,11 +9,10 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class SerialKeyboardImpl : public SerialKeyboard -{ - public: - SerialKeyboardImpl(); - void init(); +class SerialKeyboardImpl : public SerialKeyboard { +public: + SerialKeyboardImpl(); + void init(); }; extern SerialKeyboardImpl *aSerialKeyboardImpl; \ No newline at end of file diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index bd8338acf..5c77cab30 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -44,94 +44,88 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = { TCA8418Keyboard::TCA8418Keyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0), - should_backspace(false) -{ + should_backspace(false) {} + +void TCA8418Keyboard::reset() { + TCA8418KeyboardBase::reset(); + + // Set COL9 as GPIO output + writeRegister(TCA8418_REG_GPIO_DIR_3, 0x02); + // Switch off keyboard backlight (COL9 = LOW) + writeRegister(TCA8418_REG_GPIO_DAT_OUT_3, 0x00); } -void TCA8418Keyboard::reset() -{ - TCA8418KeyboardBase::reset(); +void TCA8418Keyboard::pressed(uint8_t key) { + if (state == Init || state == Busy) { + return; + } + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; - // Set COL9 as GPIO output - writeRegister(TCA8418_REG_GPIO_DIR_3, 0x02); - // Switch off keyboard backlight (COL9 = LOW) - writeRegister(TCA8418_REG_GPIO_DAT_OUT_3, 0x00); + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + // Compute key index based on dynamic row/column + next_key = row * _TCA8418_COLS + col; + + // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); + + state = Held; + uint32_t now = millis(); + tap_interval = now - last_tap; + if (tap_interval < 0) { + // Long running, millis has overflowed. + last_tap = 0; + state = Busy; + return; + } + + // Check if the key is the same as the last one or if the time interval has passed + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; // Reset char index if new key or long press + should_backspace = false; // dont backspace on new key + } else { + char_idx += 1; // Cycle through characters if same key pressed + should_backspace = true; // allow backspace on same key + } + + // Store the current key as the last key + last_key = next_key; + last_tap = now; } -void TCA8418Keyboard::pressed(uint8_t key) -{ - if (state == Init || state == Busy) { - return; - } - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; +void TCA8418Keyboard::released() { + if (state != Held) { + return; + } - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } - - // Compute key index based on dynamic row/column - next_key = row * _TCA8418_COLS + col; - - // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); - - state = Held; - uint32_t now = millis(); - tap_interval = now - last_tap; - if (tap_interval < 0) { - // Long running, millis has overflowed. - last_tap = 0; - state = Busy; - return; - } - - // Check if the key is the same as the last one or if the time interval has passed - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; // Reset char index if new key or long press - should_backspace = false; // dont backspace on new key - } else { - char_idx += 1; // Cycle through characters if same key pressed - should_backspace = true; // allow backspace on same key - } - - // Store the current key as the last key - last_key = next_key; - last_tap = now; + if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = -1; + state = Idle; + return; + } + uint32_t now = millis(); + int32_t held_interval = now - last_tap; + last_tap = now; + if (tap_interval < _TCA8418_MULTI_TAP_THRESHOLD && should_backspace) { + queueEvent(BSP); + } + if (held_interval > _TCA8418_LONG_PRESS_THRESHOLD) { + queueEvent(TCA8418LongPressMap[last_key]); + // LOG_DEBUG("Long Press Key: %i Map: %i", last_key, TCA8418LongPressMap[last_key]); + } else { + queueEvent(TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); + // LOG_DEBUG("Key Press: %i Index:%i if %i Map: %c", last_key, char_idx, TCA8418TapMod[last_key], + // TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); + } } -void TCA8418Keyboard::released() -{ - if (state != Held) { - return; - } - - if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; - state = Idle; - return; - } - uint32_t now = millis(); - int32_t held_interval = now - last_tap; - last_tap = now; - if (tap_interval < _TCA8418_MULTI_TAP_THRESHOLD && should_backspace) { - queueEvent(BSP); - } - if (held_interval > _TCA8418_LONG_PRESS_THRESHOLD) { - queueEvent(TCA8418LongPressMap[last_key]); - // LOG_DEBUG("Long Press Key: %i Map: %i", last_key, TCA8418LongPressMap[last_key]); - } else { - queueEvent(TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); - // LOG_DEBUG("Key Press: %i Index:%i if %i Map: %c", last_key, char_idx, TCA8418TapMod[last_key], - // TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); - } -} - -void TCA8418Keyboard::setBacklight(bool on) -{ - if (on) { - digitalWrite(TCA8418_COL9, HIGH); - } else { - digitalWrite(TCA8418_COL9, LOW); - } +void TCA8418Keyboard::setBacklight(bool on) { + if (on) { + digitalWrite(TCA8418_COL9, HIGH); + } else { + digitalWrite(TCA8418_COL9, LOW); + } } diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index b76916643..62b8505e0 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -3,21 +3,20 @@ /** * @brief 3x4 keypad with 3 columns and 4 rows */ -class TCA8418Keyboard : public TCA8418KeyboardBase -{ - public: - TCA8418Keyboard(); - void reset(void) override; - void setBacklight(bool on) override; +class TCA8418Keyboard : public TCA8418KeyboardBase { +public: + TCA8418Keyboard(); + void reset(void) override; + void setBacklight(bool on) override; - protected: - void pressed(uint8_t key) override; - void released(void) override; +protected: + void pressed(uint8_t key) override; + void released(void) override; - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; - bool should_backspace; + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; + bool should_backspace; }; diff --git a/src/input/TCA8418KeyboardBase.cpp b/src/input/TCA8418KeyboardBase.cpp index 00aed9962..7c2d114c8 100644 --- a/src/input/TCA8418KeyboardBase.cpp +++ b/src/input/TCA8418KeyboardBase.cpp @@ -32,346 +32,314 @@ #define _TCA8418_REG_LCK_EC_KLEC_0 0x01 // Key event count bit 0 TCA8418KeyboardBase::TCA8418KeyboardBase(uint8_t rows, uint8_t columns) - : rows(rows), columns(columns), state(Init), queue(""), m_wire(nullptr), m_addr(0), readCallback(nullptr), - writeCallback(nullptr) -{ + : rows(rows), columns(columns), state(Init), queue(""), m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) {} + +void TCA8418KeyboardBase::begin(uint8_t addr, TwoWire *wire) { + m_addr = addr; + m_wire = wire; + m_wire->begin(); + reset(); } -void TCA8418KeyboardBase::begin(uint8_t addr, TwoWire *wire) -{ - m_addr = addr; - m_wire = wire; - m_wire->begin(); - reset(); +void TCA8418KeyboardBase::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); } -void TCA8418KeyboardBase::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) -{ - m_addr = addr; - m_wire = nullptr; - writeCallback = w; - readCallback = r; - reset(); +void TCA8418KeyboardBase::reset() { + LOG_DEBUG("TCA8418 Reset"); + // GPIO + // set default all GIO pins to INPUT + writeRegister(TCA8418_REG_GPIO_DIR_1, 0x00); + writeRegister(TCA8418_REG_GPIO_DIR_2, 0x00); + writeRegister(TCA8418_REG_GPIO_DIR_3, 0x00); + + // add all pins to key events + writeRegister(TCA8418_REG_GPI_EM_1, 0xFF); + writeRegister(TCA8418_REG_GPI_EM_2, 0xFF); + writeRegister(TCA8418_REG_GPI_EM_3, 0xFF); + + // set all pins to FALLING interrupts + writeRegister(TCA8418_REG_GPIO_INT_LVL_1, 0x00); + writeRegister(TCA8418_REG_GPIO_INT_LVL_2, 0x00); + writeRegister(TCA8418_REG_GPIO_INT_LVL_3, 0x00); + + // add all pins to interrupts + writeRegister(TCA8418_REG_GPIO_INT_EN_1, 0xFF); + writeRegister(TCA8418_REG_GPIO_INT_EN_2, 0xFF); + writeRegister(TCA8418_REG_GPIO_INT_EN_3, 0xFF); + + // Set keyboard matrix size + matrix(rows, columns); + enableDebounce(); + flush(); + state = Idle; } -void TCA8418KeyboardBase::reset() -{ - LOG_DEBUG("TCA8418 Reset"); - // GPIO - // set default all GIO pins to INPUT - writeRegister(TCA8418_REG_GPIO_DIR_1, 0x00); - writeRegister(TCA8418_REG_GPIO_DIR_2, 0x00); - writeRegister(TCA8418_REG_GPIO_DIR_3, 0x00); +bool TCA8418KeyboardBase::matrix(uint8_t rows, uint8_t columns) { + if (rows < 1 || rows > 8 || columns < 1 || columns > 10) + return false; - // add all pins to key events - writeRegister(TCA8418_REG_GPI_EM_1, 0xFF); - writeRegister(TCA8418_REG_GPI_EM_2, 0xFF); - writeRegister(TCA8418_REG_GPI_EM_3, 0xFF); + // Setup the keypad matrix. + uint8_t mask = 0x00; + for (int r = 0; r < rows; r++) { + mask <<= 1; + mask |= 1; + } + writeRegister(TCA8418_REG_KP_GPIO_1, mask); - // set all pins to FALLING interrupts - writeRegister(TCA8418_REG_GPIO_INT_LVL_1, 0x00); - writeRegister(TCA8418_REG_GPIO_INT_LVL_2, 0x00); - writeRegister(TCA8418_REG_GPIO_INT_LVL_3, 0x00); + mask = 0x00; + for (int c = 0; c < columns && c < 8; c++) { + mask <<= 1; + mask |= 1; + } + writeRegister(TCA8418_REG_KP_GPIO_2, mask); - // add all pins to interrupts - writeRegister(TCA8418_REG_GPIO_INT_EN_1, 0xFF); - writeRegister(TCA8418_REG_GPIO_INT_EN_2, 0xFF); - writeRegister(TCA8418_REG_GPIO_INT_EN_3, 0xFF); + if (columns > 8) { + if (columns == 9) + mask = 0x01; + else + mask = 0x03; + writeRegister(TCA8418_REG_KP_GPIO_3, mask); + } - // Set keyboard matrix size - matrix(rows, columns); - enableDebounce(); - flush(); - state = Idle; + return true; } -bool TCA8418KeyboardBase::matrix(uint8_t rows, uint8_t columns) -{ - if (rows < 1 || rows > 8 || columns < 1 || columns > 10) - return false; - - // Setup the keypad matrix. - uint8_t mask = 0x00; - for (int r = 0; r < rows; r++) { - mask <<= 1; - mask |= 1; - } - writeRegister(TCA8418_REG_KP_GPIO_1, mask); - - mask = 0x00; - for (int c = 0; c < columns && c < 8; c++) { - mask <<= 1; - mask |= 1; - } - writeRegister(TCA8418_REG_KP_GPIO_2, mask); - - if (columns > 8) { - if (columns == 9) - mask = 0x01; - else - mask = 0x03; - writeRegister(TCA8418_REG_KP_GPIO_3, mask); - } - - return true; +uint8_t TCA8418KeyboardBase::keyCount() const { + uint8_t eventCount = readRegister(TCA8418_REG_KEY_LCK_EC); + eventCount &= 0x0F; // lower 4 bits only + return eventCount; } -uint8_t TCA8418KeyboardBase::keyCount() const -{ - uint8_t eventCount = readRegister(TCA8418_REG_KEY_LCK_EC); - eventCount &= 0x0F; // lower 4 bits only - return eventCount; +bool TCA8418KeyboardBase::hasEvent() const { return queue.length() > 0; } + +void TCA8418KeyboardBase::queueEvent(char next) { + if (next == NONE) { + return; + } + queue.concat(next); } -bool TCA8418KeyboardBase::hasEvent() const -{ - return queue.length() > 0; +char TCA8418KeyboardBase::dequeueEvent() { + if (queue.length() < 1) { + return NONE; + } + char next = queue.charAt(0); + queue.remove(0, 1); + return next; } -void TCA8418KeyboardBase::queueEvent(char next) -{ - if (next == NONE) { - return; - } - queue.concat(next); -} - -char TCA8418KeyboardBase::dequeueEvent() -{ - if (queue.length() < 1) { - return NONE; - } - char next = queue.charAt(0); - queue.remove(0, 1); - return next; -} - -void TCA8418KeyboardBase::trigger() -{ - if (keyCount() == 0) { - return; - } - if (state != Init) { - // Read the key register - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A); - uint8_t key = k & 0x7F; - if (k & 0x80) { - if (state == Idle) - pressed(key); - return; - } else { - if (state == Held) { - released(); - } - state = Idle; - return; - } +void TCA8418KeyboardBase::trigger() { + if (keyCount() == 0) { + return; + } + if (state != Init) { + // Read the key register + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A); + uint8_t key = k & 0x7F; + if (k & 0x80) { + if (state == Idle) + pressed(key); + return; } else { - reset(); + if (state == Held) { + released(); + } + state = Idle; + return; } + } else { + reset(); + } } -void TCA8418KeyboardBase::pressed(uint8_t key) -{ - // must be defined in derived class - LOG_ERROR("pressed() not implemented in derived class"); +void TCA8418KeyboardBase::pressed(uint8_t key) { + // must be defined in derived class + LOG_ERROR("pressed() not implemented in derived class"); } -void TCA8418KeyboardBase::released() -{ - // must be defined in derived class - LOG_ERROR("released() not implemented in derived class"); +void TCA8418KeyboardBase::released() { + // must be defined in derived class + LOG_ERROR("released() not implemented in derived class"); } -uint8_t TCA8418KeyboardBase::flush() -{ - // Flush key events - uint8_t count = 0; - while (readRegister(TCA8418_REG_KEY_EVENT_A) != 0) - count++; +uint8_t TCA8418KeyboardBase::flush() { + // Flush key events + uint8_t count = 0; + while (readRegister(TCA8418_REG_KEY_EVENT_A) != 0) + count++; - // Flush gpio events - readRegister(TCA8418_REG_GPIO_INT_STAT_1); - readRegister(TCA8418_REG_GPIO_INT_STAT_2); - readRegister(TCA8418_REG_GPIO_INT_STAT_3); + // Flush gpio events + readRegister(TCA8418_REG_GPIO_INT_STAT_1); + readRegister(TCA8418_REG_GPIO_INT_STAT_2); + readRegister(TCA8418_REG_GPIO_INT_STAT_3); - // Clear INT_STAT register - writeRegister(TCA8418_REG_INT_STAT, 3); - return count; + // Clear INT_STAT register + writeRegister(TCA8418_REG_INT_STAT, 3); + return count; } -void TCA8418KeyboardBase::clearInt() -{ - writeRegister(TCA8418_REG_INT_STAT, 3); +void TCA8418KeyboardBase::clearInt() { writeRegister(TCA8418_REG_INT_STAT, 3); } + +uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const { + if (pinnum > TCA8418_COL9) + return 0xFF; + + uint8_t reg = TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8; + uint8_t mask = (1 << (pinnum % 8)); + + // Level 0 = low other = high + uint8_t value = readRegister(reg); + if (value & mask) + return HIGH; + return LOW; } -uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const -{ - if (pinnum > TCA8418_COL9) - return 0xFF; +bool TCA8418KeyboardBase::digitalWrite(uint8_t pinnum, uint8_t level) { + if (pinnum > TCA8418_COL9) + return false; - uint8_t reg = TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8; - uint8_t mask = (1 << (pinnum % 8)); + uint8_t reg = TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8; + uint8_t mask = (1 << (pinnum % 8)); - // Level 0 = low other = high - uint8_t value = readRegister(reg); - if (value & mask) - return HIGH; - return LOW; -} - -bool TCA8418KeyboardBase::digitalWrite(uint8_t pinnum, uint8_t level) -{ - if (pinnum > TCA8418_COL9) - return false; - - uint8_t reg = TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8; - uint8_t mask = (1 << (pinnum % 8)); - - // Level 0 = low other = high - uint8_t value = readRegister(reg); - if (level == LOW) - value &= ~mask; - else - value |= mask; - writeRegister(reg, value); - return true; -} - -bool TCA8418KeyboardBase::pinMode(uint8_t pinnum, uint8_t mode) -{ - if (pinnum > TCA8418_COL9) - return false; - - uint8_t idx = pinnum / 8; - uint8_t reg = TCA8418_REG_GPIO_DIR_1 + idx; - uint8_t mask = (1 << (pinnum % 8)); - - // Mode 0 = input 1 = output - uint8_t value = readRegister(reg); - if (mode == OUTPUT) - value |= mask; - else - value &= ~mask; - writeRegister(reg, value); - - // Pullup 0 = enabled 1 = disabled - reg = TCA8418_REG_GPIO_PULL_1 + idx; - value = readRegister(reg); - if (mode == INPUT_PULLUP) - value &= ~mask; - else - value |= mask; - writeRegister(reg, value); - - return true; -} - -bool TCA8418KeyboardBase::pinIRQMode(uint8_t pinnum, uint8_t mode) -{ - if (pinnum > TCA8418_COL9) - return false; - if ((mode != RISING) && (mode != FALLING)) - return false; - - // Mode 0 = falling 1 = rising - uint8_t idx = pinnum / 8; - uint8_t reg = TCA8418_REG_GPIO_INT_LVL_1 + idx; - uint8_t mask = (1 << (pinnum % 8)); - - uint8_t value = readRegister(reg); - if (mode == RISING) - value |= mask; - else - value &= ~mask; - writeRegister(reg, value); - - // Enable interrupt - reg = TCA8418_REG_GPIO_INT_EN_1 + idx; - value = readRegister(reg); + // Level 0 = low other = high + uint8_t value = readRegister(reg); + if (level == LOW) + value &= ~mask; + else value |= mask; - writeRegister(reg, value); - - return true; + writeRegister(reg, value); + return true; } -void TCA8418KeyboardBase::enableInterrupts() -{ - uint8_t value = readRegister(TCA8418_REG_CFG); - value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); - writeRegister(TCA8418_REG_CFG, value); -}; +bool TCA8418KeyboardBase::pinMode(uint8_t pinnum, uint8_t mode) { + if (pinnum > TCA8418_COL9) + return false; -void TCA8418KeyboardBase::disableInterrupts() -{ - uint8_t value = readRegister(TCA8418_REG_CFG); - value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); - writeRegister(TCA8418_REG_CFG, value); -}; + uint8_t idx = pinnum / 8; + uint8_t reg = TCA8418_REG_GPIO_DIR_1 + idx; + uint8_t mask = (1 << (pinnum % 8)); -void TCA8418KeyboardBase::enableMatrixOverflow() -{ - uint8_t value = readRegister(TCA8418_REG_CFG); - value |= _TCA8418_REG_CFG_OVR_FLOW_M; - writeRegister(TCA8418_REG_CFG, value); -}; + // Mode 0 = input 1 = output + uint8_t value = readRegister(reg); + if (mode == OUTPUT) + value |= mask; + else + value &= ~mask; + writeRegister(reg, value); -void TCA8418KeyboardBase::disableMatrixOverflow() -{ - uint8_t value = readRegister(TCA8418_REG_CFG); - value &= ~_TCA8418_REG_CFG_OVR_FLOW_M; - writeRegister(TCA8418_REG_CFG, value); -}; + // Pullup 0 = enabled 1 = disabled + reg = TCA8418_REG_GPIO_PULL_1 + idx; + value = readRegister(reg); + if (mode == INPUT_PULLUP) + value &= ~mask; + else + value |= mask; + writeRegister(reg, value); -void TCA8418KeyboardBase::enableDebounce() -{ - writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0x00); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0x00); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0x00); + return true; } -void TCA8418KeyboardBase::disableDebounce() -{ - writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0xFF); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0xFF); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0xFF); +bool TCA8418KeyboardBase::pinIRQMode(uint8_t pinnum, uint8_t mode) { + if (pinnum > TCA8418_COL9) + return false; + if ((mode != RISING) && (mode != FALLING)) + return false; + + // Mode 0 = falling 1 = rising + uint8_t idx = pinnum / 8; + uint8_t reg = TCA8418_REG_GPIO_INT_LVL_1 + idx; + uint8_t mask = (1 << (pinnum % 8)); + + uint8_t value = readRegister(reg); + if (mode == RISING) + value |= mask; + else + value &= ~mask; + writeRegister(reg, value); + + // Enable interrupt + reg = TCA8418_REG_GPIO_INT_EN_1 + idx; + value = readRegister(reg); + value |= mask; + writeRegister(reg, value); + + return true; +} + +void TCA8418KeyboardBase::enableInterrupts() { + uint8_t value = readRegister(TCA8418_REG_CFG); + value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); + writeRegister(TCA8418_REG_CFG, value); +}; + +void TCA8418KeyboardBase::disableInterrupts() { + uint8_t value = readRegister(TCA8418_REG_CFG); + value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); + writeRegister(TCA8418_REG_CFG, value); +}; + +void TCA8418KeyboardBase::enableMatrixOverflow() { + uint8_t value = readRegister(TCA8418_REG_CFG); + value |= _TCA8418_REG_CFG_OVR_FLOW_M; + writeRegister(TCA8418_REG_CFG, value); +}; + +void TCA8418KeyboardBase::disableMatrixOverflow() { + uint8_t value = readRegister(TCA8418_REG_CFG); + value &= ~_TCA8418_REG_CFG_OVR_FLOW_M; + writeRegister(TCA8418_REG_CFG, value); +}; + +void TCA8418KeyboardBase::enableDebounce() { + writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0x00); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0x00); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0x00); +} + +void TCA8418KeyboardBase::disableDebounce() { + writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0xFF); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0xFF); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0xFF); } void TCA8418KeyboardBase::setBacklight(bool on) {} -uint8_t TCA8418KeyboardBase::readRegister(uint8_t reg) const -{ - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); +uint8_t TCA8418KeyboardBase::readRegister(uint8_t reg) const { + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); - m_wire->requestFrom(m_addr, (uint8_t)1); - if (m_wire->available() < 1) - return 0; + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) + return 0; - return m_wire->read(); - } - if (readCallback) { - uint8_t data; - readCallback(m_addr, reg, &data, 1); - return data; - } - return 0; + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; } -void TCA8418KeyboardBase::writeRegister(uint8_t reg, uint8_t value) -{ - uint8_t data[2]; - data[0] = reg; - data[1] = value; +void TCA8418KeyboardBase::writeRegister(uint8_t reg, uint8_t value) { + uint8_t data[2]; + data[0] = reg; + data[1] = value; - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(data, sizeof(uint8_t) * 2); - m_wire->endTransmission(); - } - if (writeCallback) { - writeCallback(m_addr, data[0], &(data[1]), 1); - } + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(data, sizeof(uint8_t) * 2); + m_wire->endTransmission(); + } + if (writeCallback) { + writeCallback(m_addr, data[0], &(data[1]), 1); + } } \ No newline at end of file diff --git a/src/input/TCA8418KeyboardBase.h b/src/input/TCA8418KeyboardBase.h index 8e509ac7e..82f7a4363 100644 --- a/src/input/TCA8418KeyboardBase.h +++ b/src/input/TCA8418KeyboardBase.h @@ -8,165 +8,164 @@ * and handling key states. It is designed to be extended for specific keyboard implementations. * It supports both I2C communication and function pointers for custom I2C operations. */ -class TCA8418KeyboardBase -{ - public: - enum TCA8418Key : uint8_t { - NONE = 0x00, - BSP = 0x08, - TAB = 0x09, - SELECT = 0x0d, - ESC = 0x1b, - REBOOT = 0x90, - LEFT = 0xb4, - UP = 0xb5, - DOWN = 0xb6, - RIGHT = 0xb7, - BT_TOGGLE = 0xAA, - GPS_TOGGLE = 0x9E, - MUTE_TOGGLE = 0xAC, - SEND_PING = 0xAF, - BL_TOGGLE = 0xAB - }; +class TCA8418KeyboardBase { +public: + enum TCA8418Key : uint8_t { + NONE = 0x00, + BSP = 0x08, + TAB = 0x09, + SELECT = 0x0d, + ESC = 0x1b, + REBOOT = 0x90, + LEFT = 0xb4, + UP = 0xb5, + DOWN = 0xb6, + RIGHT = 0xb7, + BT_TOGGLE = 0xAA, + GPS_TOGGLE = 0x9E, + MUTE_TOGGLE = 0xAC, + SEND_PING = 0xAF, + BL_TOGGLE = 0xAB + }; - typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); - TCA8418KeyboardBase(uint8_t rows, uint8_t columns); + TCA8418KeyboardBase(uint8_t rows, uint8_t columns); - virtual void begin(uint8_t addr = TCA8418_KB_ADDR, TwoWire *wire = &Wire); - virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR); + virtual void begin(uint8_t addr = TCA8418_KB_ADDR, TwoWire *wire = &Wire); + virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR); - virtual void reset(void); - void clearInt(void); + virtual void reset(void); + void clearInt(void); - virtual void trigger(void); + virtual void trigger(void); - virtual void setBacklight(bool on); + virtual void setBacklight(bool on); - // Key events available - virtual bool hasEvent(void) const; - virtual char dequeueEvent(void); + // Key events available + virtual bool hasEvent(void) const; + virtual char dequeueEvent(void); - protected: - enum KeyState { Init, Idle, Held, Busy }; +protected: + enum KeyState { Init, Idle, Held, Busy }; - enum TCA8418Register : uint8_t { - TCA8418_REG_RESERVED = 0x00, - TCA8418_REG_CFG = 0x01, - TCA8418_REG_INT_STAT = 0x02, - TCA8418_REG_KEY_LCK_EC = 0x03, - TCA8418_REG_KEY_EVENT_A = 0x04, - TCA8418_REG_KEY_EVENT_B = 0x05, - TCA8418_REG_KEY_EVENT_C = 0x06, - TCA8418_REG_KEY_EVENT_D = 0x07, - TCA8418_REG_KEY_EVENT_E = 0x08, - TCA8418_REG_KEY_EVENT_F = 0x09, - TCA8418_REG_KEY_EVENT_G = 0x0A, - TCA8418_REG_KEY_EVENT_H = 0x0B, - TCA8418_REG_KEY_EVENT_I = 0x0C, - TCA8418_REG_KEY_EVENT_J = 0x0D, - TCA8418_REG_KP_LCK_TIMER = 0x0E, - TCA8418_REG_UNLOCK_1 = 0x0F, - TCA8418_REG_UNLOCK_2 = 0x10, - TCA8418_REG_GPIO_INT_STAT_1 = 0x11, - TCA8418_REG_GPIO_INT_STAT_2 = 0x12, - TCA8418_REG_GPIO_INT_STAT_3 = 0x13, - TCA8418_REG_GPIO_DAT_STAT_1 = 0x14, - TCA8418_REG_GPIO_DAT_STAT_2 = 0x15, - TCA8418_REG_GPIO_DAT_STAT_3 = 0x16, - TCA8418_REG_GPIO_DAT_OUT_1 = 0x17, - TCA8418_REG_GPIO_DAT_OUT_2 = 0x18, - TCA8418_REG_GPIO_DAT_OUT_3 = 0x19, - TCA8418_REG_GPIO_INT_EN_1 = 0x1A, - TCA8418_REG_GPIO_INT_EN_2 = 0x1B, - TCA8418_REG_GPIO_INT_EN_3 = 0x1C, - TCA8418_REG_KP_GPIO_1 = 0x1D, - TCA8418_REG_KP_GPIO_2 = 0x1E, - TCA8418_REG_KP_GPIO_3 = 0x1F, - TCA8418_REG_GPI_EM_1 = 0x20, - TCA8418_REG_GPI_EM_2 = 0x21, - TCA8418_REG_GPI_EM_3 = 0x22, - TCA8418_REG_GPIO_DIR_1 = 0x23, - TCA8418_REG_GPIO_DIR_2 = 0x24, - TCA8418_REG_GPIO_DIR_3 = 0x25, - TCA8418_REG_GPIO_INT_LVL_1 = 0x26, - TCA8418_REG_GPIO_INT_LVL_2 = 0x27, - TCA8418_REG_GPIO_INT_LVL_3 = 0x28, - TCA8418_REG_DEBOUNCE_DIS_1 = 0x29, - TCA8418_REG_DEBOUNCE_DIS_2 = 0x2A, - TCA8418_REG_DEBOUNCE_DIS_3 = 0x2B, - TCA8418_REG_GPIO_PULL_1 = 0x2C, - TCA8418_REG_GPIO_PULL_2 = 0x2D, - TCA8418_REG_GPIO_PULL_3 = 0x2E - }; + enum TCA8418Register : uint8_t { + TCA8418_REG_RESERVED = 0x00, + TCA8418_REG_CFG = 0x01, + TCA8418_REG_INT_STAT = 0x02, + TCA8418_REG_KEY_LCK_EC = 0x03, + TCA8418_REG_KEY_EVENT_A = 0x04, + TCA8418_REG_KEY_EVENT_B = 0x05, + TCA8418_REG_KEY_EVENT_C = 0x06, + TCA8418_REG_KEY_EVENT_D = 0x07, + TCA8418_REG_KEY_EVENT_E = 0x08, + TCA8418_REG_KEY_EVENT_F = 0x09, + TCA8418_REG_KEY_EVENT_G = 0x0A, + TCA8418_REG_KEY_EVENT_H = 0x0B, + TCA8418_REG_KEY_EVENT_I = 0x0C, + TCA8418_REG_KEY_EVENT_J = 0x0D, + TCA8418_REG_KP_LCK_TIMER = 0x0E, + TCA8418_REG_UNLOCK_1 = 0x0F, + TCA8418_REG_UNLOCK_2 = 0x10, + TCA8418_REG_GPIO_INT_STAT_1 = 0x11, + TCA8418_REG_GPIO_INT_STAT_2 = 0x12, + TCA8418_REG_GPIO_INT_STAT_3 = 0x13, + TCA8418_REG_GPIO_DAT_STAT_1 = 0x14, + TCA8418_REG_GPIO_DAT_STAT_2 = 0x15, + TCA8418_REG_GPIO_DAT_STAT_3 = 0x16, + TCA8418_REG_GPIO_DAT_OUT_1 = 0x17, + TCA8418_REG_GPIO_DAT_OUT_2 = 0x18, + TCA8418_REG_GPIO_DAT_OUT_3 = 0x19, + TCA8418_REG_GPIO_INT_EN_1 = 0x1A, + TCA8418_REG_GPIO_INT_EN_2 = 0x1B, + TCA8418_REG_GPIO_INT_EN_3 = 0x1C, + TCA8418_REG_KP_GPIO_1 = 0x1D, + TCA8418_REG_KP_GPIO_2 = 0x1E, + TCA8418_REG_KP_GPIO_3 = 0x1F, + TCA8418_REG_GPI_EM_1 = 0x20, + TCA8418_REG_GPI_EM_2 = 0x21, + TCA8418_REG_GPI_EM_3 = 0x22, + TCA8418_REG_GPIO_DIR_1 = 0x23, + TCA8418_REG_GPIO_DIR_2 = 0x24, + TCA8418_REG_GPIO_DIR_3 = 0x25, + TCA8418_REG_GPIO_INT_LVL_1 = 0x26, + TCA8418_REG_GPIO_INT_LVL_2 = 0x27, + TCA8418_REG_GPIO_INT_LVL_3 = 0x28, + TCA8418_REG_DEBOUNCE_DIS_1 = 0x29, + TCA8418_REG_DEBOUNCE_DIS_2 = 0x2A, + TCA8418_REG_DEBOUNCE_DIS_3 = 0x2B, + TCA8418_REG_GPIO_PULL_1 = 0x2C, + TCA8418_REG_GPIO_PULL_2 = 0x2D, + TCA8418_REG_GPIO_PULL_3 = 0x2E + }; - // Pin IDs for matrix rows/columns - enum TCA8418PinId : uint8_t { - TCA8418_ROW0, // Pin ID for row 0 - TCA8418_ROW1, // Pin ID for row 1 - TCA8418_ROW2, // Pin ID for row 2 - TCA8418_ROW3, // Pin ID for row 3 - TCA8418_ROW4, // Pin ID for row 4 - TCA8418_ROW5, // Pin ID for row 5 - TCA8418_ROW6, // Pin ID for row 6 - TCA8418_ROW7, // Pin ID for row 7 - TCA8418_COL0, // Pin ID for column 0 - TCA8418_COL1, // Pin ID for column 1 - TCA8418_COL2, // Pin ID for column 2 - TCA8418_COL3, // Pin ID for column 3 - TCA8418_COL4, // Pin ID for column 4 - TCA8418_COL5, // Pin ID for column 5 - TCA8418_COL6, // Pin ID for column 6 - TCA8418_COL7, // Pin ID for column 7 - TCA8418_COL8, // Pin ID for column 8 - TCA8418_COL9 // Pin ID for column 9 - }; + // Pin IDs for matrix rows/columns + enum TCA8418PinId : uint8_t { + TCA8418_ROW0, // Pin ID for row 0 + TCA8418_ROW1, // Pin ID for row 1 + TCA8418_ROW2, // Pin ID for row 2 + TCA8418_ROW3, // Pin ID for row 3 + TCA8418_ROW4, // Pin ID for row 4 + TCA8418_ROW5, // Pin ID for row 5 + TCA8418_ROW6, // Pin ID for row 6 + TCA8418_ROW7, // Pin ID for row 7 + TCA8418_COL0, // Pin ID for column 0 + TCA8418_COL1, // Pin ID for column 1 + TCA8418_COL2, // Pin ID for column 2 + TCA8418_COL3, // Pin ID for column 3 + TCA8418_COL4, // Pin ID for column 4 + TCA8418_COL5, // Pin ID for column 5 + TCA8418_COL6, // Pin ID for column 6 + TCA8418_COL7, // Pin ID for column 7 + TCA8418_COL8, // Pin ID for column 8 + TCA8418_COL9 // Pin ID for column 9 + }; - virtual void pressed(uint8_t key); - virtual void released(void); + virtual void pressed(uint8_t key); + virtual void released(void); - virtual void queueEvent(char); + virtual void queueEvent(char); - virtual ~TCA8418KeyboardBase() {} + virtual ~TCA8418KeyboardBase() {} - protected: - // Set the size of the keypad matrix - // All other rows and columns are set as inputs. - bool matrix(uint8_t rows, uint8_t columns); +protected: + // Set the size of the keypad matrix + // All other rows and columns are set as inputs. + bool matrix(uint8_t rows, uint8_t columns); - uint8_t keyCount(void) const; + uint8_t keyCount(void) const; - // Flush all events in the FIFO buffer + GPIO events. - uint8_t flush(void); + // Flush all events in the FIFO buffer + GPIO events. + uint8_t flush(void); - // debounce keys. - void enableDebounce(); - void disableDebounce(); + // debounce keys. + void enableDebounce(); + void disableDebounce(); - // enable / disable interrupts for matrix and GPI pins - void enableInterrupts(); - void disableInterrupts(); + // enable / disable interrupts for matrix and GPI pins + void enableInterrupts(); + void disableInterrupts(); - // ignore key events when FIFO buffer is full or not. - void enableMatrixOverflow(); - void disableMatrixOverflow(); + // ignore key events when FIFO buffer is full or not. + void enableMatrixOverflow(); + void disableMatrixOverflow(); - uint8_t digitalRead(uint8_t pinnum) const; - bool digitalWrite(uint8_t pinnum, uint8_t level); - bool pinMode(uint8_t pinnum, uint8_t mode); - bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING - uint8_t readRegister(uint8_t reg) const; - void writeRegister(uint8_t reg, uint8_t value); + uint8_t digitalRead(uint8_t pinnum) const; + bool digitalWrite(uint8_t pinnum, uint8_t level); + bool pinMode(uint8_t pinnum, uint8_t mode); + bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING + uint8_t readRegister(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); - protected: - uint8_t rows; - uint8_t columns; - KeyState state; - String queue; +protected: + uint8_t rows; + uint8_t columns; + KeyState state; + String queue; - private: - TwoWire *m_wire; - uint8_t m_addr; - i2c_com_fptr_t readCallback; - i2c_com_fptr_t writeCallback; +private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; }; diff --git a/src/input/TDeckProKeyboard.cpp b/src/input/TDeckProKeyboard.cpp index eeafe4949..62056f2e7 100644 --- a/src/input/TDeckProKeyboard.cpp +++ b/src/input/TDeckProKeyboard.cpp @@ -62,135 +62,123 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = { }; TDeckProKeyboard::TDeckProKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), - last_tap(0L), char_idx(0), tap_interval(0) -{ -} + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), + char_idx(0), tap_interval(0) {} -void TDeckProKeyboard::reset() -{ - TCA8418KeyboardBase::reset(); - pinMode(KB_BL_PIN, OUTPUT); - setBacklight(false); +void TDeckProKeyboard::reset() { + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + setBacklight(false); } // handle multi-key presses (shift and alt) -void TDeckProKeyboard::trigger() -{ - uint8_t count = keyCount(); - if (count == 0) - return; - for (uint8_t i = 0; i < count; ++i) { - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); - uint8_t key = k & 0x7F; - if (k & 0x80) { - pressed(key); - } else { - released(); - state = Idle; - } - } -} - -void TDeckProKeyboard::pressed(uint8_t key) -{ - if (state == Init || state == Busy) { - return; - } - if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { - modifierFlag = 0; - } - - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; - - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } - - next_key = row * _TCA8418_COLS + col; - state = Held; - - uint32_t now = millis(); - tap_interval = now - last_tap; - - updateModifierFlag(next_key); - if (isModifierKey(next_key)) { - last_modifier_time = now; - } - - if (tap_interval < 0) { - last_tap = 0; - state = Busy; - return; - } - - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; +void TDeckProKeyboard::trigger() { + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); } else { - char_idx += 1; + released(); + state = Idle; } - - last_key = next_key; - last_tap = now; + } } -void TDeckProKeyboard::released() -{ - if (state != Held) { - return; - } +void TDeckProKeyboard::pressed(uint8_t key) { + if (state == Init || state == Busy) { + return; + } + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; - state = Idle; - return; - } + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; - uint32_t now = millis(); - last_tap = now; + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } - if (TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]] == Key::BL_TOGGLE) { - toggleBacklight(); - return; - } + next_key = row * _TCA8418_COLS + col; + state = Held; - queueEvent(TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]]); - if (isModifierKey(last_key) == false) - modifierFlag = 0; + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; } -void TDeckProKeyboard::setBacklight(bool on) -{ - if (on) { - digitalWrite(KB_BL_PIN, HIGH); - } else { - digitalWrite(KB_BL_PIN, LOW); - } +void TDeckProKeyboard::released() { + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } + + queueEvent(TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; } -void TDeckProKeyboard::toggleBacklight(void) -{ - digitalWrite(KB_BL_PIN, !digitalRead(KB_BL_PIN)); +void TDeckProKeyboard::setBacklight(bool on) { + if (on) { + digitalWrite(KB_BL_PIN, HIGH); + } else { + digitalWrite(KB_BL_PIN, LOW); + } } -void TDeckProKeyboard::updateModifierFlag(uint8_t key) -{ - if (key == modifierRightShiftKey) { - modifierFlag ^= modifierRightShift; - } else if (key == modifierLeftShiftKey) { - modifierFlag ^= modifierLeftShift; - } else if (key == modifierSymKey) { - modifierFlag ^= modifierSym; - } else if (key == modifierAltKey) { - modifierFlag ^= modifierAlt; - } +void TDeckProKeyboard::toggleBacklight(void) { digitalWrite(KB_BL_PIN, !digitalRead(KB_BL_PIN)); } + +void TDeckProKeyboard::updateModifierFlag(uint8_t key) { + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierLeftShiftKey) { + modifierFlag ^= modifierLeftShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } else if (key == modifierAltKey) { + modifierFlag ^= modifierAlt; + } } -bool TDeckProKeyboard::isModifierKey(uint8_t key) -{ - return (key == modifierRightShiftKey || key == modifierLeftShiftKey || key == modifierAltKey || key == modifierSymKey); +bool TDeckProKeyboard::isModifierKey(uint8_t key) { + return (key == modifierRightShiftKey || key == modifierLeftShiftKey || key == modifierAltKey || key == modifierSymKey); } #endif // T_DECK_PRO \ No newline at end of file diff --git a/src/input/TDeckProKeyboard.h b/src/input/TDeckProKeyboard.h index 617f3f20b..14106c2f8 100644 --- a/src/input/TDeckProKeyboard.h +++ b/src/input/TDeckProKeyboard.h @@ -1,27 +1,26 @@ #include "TCA8418KeyboardBase.h" -class TDeckProKeyboard : public TCA8418KeyboardBase -{ - public: - TDeckProKeyboard(); - void reset(void) override; - void trigger(void) override; - void setBacklight(bool on) override; +class TDeckProKeyboard : public TCA8418KeyboardBase { +public: + TDeckProKeyboard(); + void reset(void) override; + void trigger(void) override; + void setBacklight(bool on) override; - protected: - void pressed(uint8_t key) override; - void released(void) override; +protected: + void pressed(uint8_t key) override; + void released(void) override; - void updateModifierFlag(uint8_t key); - bool isModifierKey(uint8_t key); - void toggleBacklight(void); + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(void); - private: - uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed - uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; +private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp index 9a4fd8679..f04bb04a8 100644 --- a/src/input/TLoraPagerKeyboard.cpp +++ b/src/input/TLoraPagerKeyboard.cpp @@ -29,8 +29,7 @@ constexpr uint8_t modifierSymKey = 21 - 1; constexpr uint8_t modifierSym = 0b0010; // Num chars per key, Modulus for rotating through characters -static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; +static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {'w', 'W', '2'}, @@ -65,168 +64,156 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {' ', 0x00, Key::BL_TOGGLE}}; TLoraPagerKeyboard::TLoraPagerKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), - last_tap(0L), char_idx(0), tap_interval(0) -{ + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), + char_idx(0), tap_interval(0) { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); #else - ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); - ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); + ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); #endif - reset(); + reset(); } -void TLoraPagerKeyboard::reset(void) -{ - TCA8418KeyboardBase::reset(); - pinMode(KB_BL_PIN, OUTPUT); - digitalWrite(KB_BL_PIN, LOW); - setBacklight(false); +void TLoraPagerKeyboard::reset(void) { + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + digitalWrite(KB_BL_PIN, LOW); + setBacklight(false); } // handle multi-key presses (shift and alt) -void TLoraPagerKeyboard::trigger() -{ - uint8_t count = keyCount(); - if (count == 0) - return; - for (uint8_t i = 0; i < count; ++i) { - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); - uint8_t key = k & 0x7F; - if (k & 0x80) { - pressed(key); - } else { - released(); - state = Idle; - } +void TLoraPagerKeyboard::trigger() { + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; } + } } -void TLoraPagerKeyboard::setBacklight(bool on) -{ - uint32_t _brightness = 0; - if (on) - _brightness = brightness; +void TLoraPagerKeyboard::setBacklight(bool on) { + uint32_t _brightness = 0; + if (on) + _brightness = brightness; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - ledcWrite(KB_BL_PIN, _brightness); + ledcWrite(KB_BL_PIN, _brightness); #else - ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness); + ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness); #endif } -void TLoraPagerKeyboard::pressed(uint8_t key) -{ - if (state == Init || state == Busy) { - return; - } - if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { - hapticFeedback(); - } +void TLoraPagerKeyboard::pressed(uint8_t key) { + if (state == Init || state == Busy) { + return; + } + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + hapticFeedback(); + } - if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { - modifierFlag = 0; - } + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } - next_key = row * _TCA8418_COLS + col; - state = Held; + next_key = row * _TCA8418_COLS + col; + state = Held; - uint32_t now = millis(); - tap_interval = now - last_tap; + uint32_t now = millis(); + tap_interval = now - last_tap; - updateModifierFlag(next_key); - if (isModifierKey(next_key)) { - last_modifier_time = now; - } + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } - if (tap_interval < 0) { - last_tap = 0; - state = Busy; - return; - } + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; - } else { - char_idx += 1; - } + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } - last_key = next_key; - last_tap = now; + last_key = next_key; + last_tap = now; } -void TLoraPagerKeyboard::released() -{ - if (state != Held) { - return; - } +void TLoraPagerKeyboard::released() { + if (state != Held) { + return; + } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; - state = Idle; - return; - } + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } - uint32_t now = millis(); - last_tap = now; + uint32_t now = millis(); + last_tap = now; - if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { - toggleBacklight(); - return; - } + if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } - queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); - if (isModifierKey(last_key) == false) - modifierFlag = 0; + queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; } -void TLoraPagerKeyboard::hapticFeedback() -{ - drv.setWaveform(0, 14); // strong buzz 100% - drv.setWaveform(1, 0); // end waveform - drv.go(); +void TLoraPagerKeyboard::hapticFeedback() { + drv.setWaveform(0, 14); // strong buzz 100% + drv.setWaveform(1, 0); // end waveform + drv.go(); } // toggle brightness of the backlight in three steps -void TLoraPagerKeyboard::toggleBacklight(bool off) -{ - if (off) { - brightness = 0; - } else { - if (brightness == 0) { - brightness = 40; - } else if (brightness == 40) { - brightness = 127; - } else if (brightness >= 127) { - brightness = 0; - } +void TLoraPagerKeyboard::toggleBacklight(bool off) { + if (off) { + brightness = 0; + } else { + if (brightness == 0) { + brightness = 40; + } else if (brightness == 40) { + brightness = 127; + } else if (brightness >= 127) { + brightness = 0; } - LOG_DEBUG("Toggle backlight: %d", brightness); + } + LOG_DEBUG("Toggle backlight: %d", brightness); - setBacklight(true); + setBacklight(true); } -void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) -{ - if (key == modifierRightShiftKey) { - modifierFlag ^= modifierRightShift; - } else if (key == modifierSymKey) { - modifierFlag ^= modifierSym; - } +void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) { + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } } -bool TLoraPagerKeyboard::isModifierKey(uint8_t key) -{ - return (key == modifierRightShiftKey || key == modifierSymKey); -} +bool TLoraPagerKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierSymKey); } #endif \ No newline at end of file diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index f04d2ce6a..61333149f 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -1,30 +1,29 @@ #include "TCA8418KeyboardBase.h" -class TLoraPagerKeyboard : public TCA8418KeyboardBase -{ - public: - TLoraPagerKeyboard(); - void reset(void); - void trigger(void) override; - void setBacklight(bool on) override; - virtual ~TLoraPagerKeyboard() {} +class TLoraPagerKeyboard : public TCA8418KeyboardBase { +public: + TLoraPagerKeyboard(); + void reset(void); + void trigger(void) override; + void setBacklight(bool on) override; + virtual ~TLoraPagerKeyboard() {} - protected: - void pressed(uint8_t key) override; - void released(void) override; - void hapticFeedback(void); +protected: + void pressed(uint8_t key) override; + void released(void) override; + void hapticFeedback(void); - void updateModifierFlag(uint8_t key); - bool isModifierKey(uint8_t key); - void toggleBacklight(bool off = false); + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(bool off = false); - private: - uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed - uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; - uint32_t brightness = 0; +private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; + uint32_t brightness = 0; }; diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index c2755980e..d3e353f6b 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -19,141 +19,136 @@ #endif TouchScreenBase::TouchScreenBase(const char *name, uint16_t width, uint16_t height) - : concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0), - _last_y(0), _start(0), _tapped(false), _originName(name) -{ + : concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0), _last_y(0), _start(0), + _tapped(false), _originName(name) {} + +void TouchScreenBase::init(bool hasTouch) { + if (hasTouch) { + LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y); + this->setInterval(100); + } else { + disable(); + this->setInterval(UINT_MAX); + } } -void TouchScreenBase::init(bool hasTouch) -{ - if (hasTouch) { - LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y); - this->setInterval(100); - } else { - disable(); - this->setInterval(UINT_MAX); - } -} +int32_t TouchScreenBase::runOnce() { + TouchEvent e; + e.touchEvent = static_cast(TOUCH_ACTION_NONE); -int32_t TouchScreenBase::runOnce() -{ - TouchEvent e; - e.touchEvent = static_cast(TOUCH_ACTION_NONE); - - // process touch events - int16_t x, y; - bool touched = getTouch(x, y); - if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen - touched = false; + // process touch events + int16_t x, y; + bool touched = getTouch(x, y); + if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen + touched = false; + if (touched) { + this->setInterval(20); + _last_x = x; + _last_y = y; + } + if (touched != _touchedOld) { if (touched) { - this->setInterval(20); - _last_x = x; - _last_y = y; - } - if (touched != _touchedOld) { - if (touched) { - hapticFeedback(); - _state = TOUCH_EVENT_OCCURRED; - _start = millis(); - _first_x = x; - _first_y = y; - } else { - _state = TOUCH_EVENT_CLEARED; - time_t duration = millis() - _start; - x = _last_x; - y = _last_y; - this->setInterval(50); + hapticFeedback(); + _state = TOUCH_EVENT_OCCURRED; + _start = millis(); + _first_x = x; + _first_y = y; + } else { + _state = TOUCH_EVENT_CLEARED; + time_t duration = millis() - _start; + x = _last_x; + y = _last_y; + this->setInterval(50); - // compute distance - int16_t dx = x - _first_x; - int16_t dy = y - _first_y; - uint16_t adx = abs(dx); - uint16_t ady = abs(dy); + // compute distance + int16_t dx = x - _first_x; + int16_t dy = y - _first_y; + uint16_t adx = abs(dx); + uint16_t ady = abs(dy); - // swipe horizontal - if (adx > ady && adx > TOUCH_THRESHOLD_X) { - if (0 > dx) { // swipe right to left - e.touchEvent = static_cast(TOUCH_ACTION_LEFT); - LOG_DEBUG("action SWIPE: right to left"); - } else { // swipe left to right - e.touchEvent = static_cast(TOUCH_ACTION_RIGHT); - LOG_DEBUG("action SWIPE: left to right"); - } - } - // swipe vertical - else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { - if (0 > dy) { // swipe bottom to top - e.touchEvent = static_cast(TOUCH_ACTION_UP); - LOG_DEBUG("action SWIPE: bottom to top"); - } else { // swipe top to bottom - e.touchEvent = static_cast(TOUCH_ACTION_DOWN); - LOG_DEBUG("action SWIPE: top to bottom"); - } - } - // tap - else { - if (duration > 0 && duration < TIME_LONG_PRESS) { - if (_tapped) { - _tapped = false; - } else { - _tapped = true; - } - } else { - _tapped = false; - } - } + // swipe horizontal + if (adx > ady && adx > TOUCH_THRESHOLD_X) { + if (0 > dx) { // swipe right to left + e.touchEvent = static_cast(TOUCH_ACTION_LEFT); + LOG_DEBUG("action SWIPE: right to left"); + } else { // swipe left to right + e.touchEvent = static_cast(TOUCH_ACTION_RIGHT); + LOG_DEBUG("action SWIPE: left to right"); } + } + // swipe vertical + else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { + if (0 > dy) { // swipe bottom to top + e.touchEvent = static_cast(TOUCH_ACTION_UP); + LOG_DEBUG("action SWIPE: bottom to top"); + } else { // swipe top to bottom + e.touchEvent = static_cast(TOUCH_ACTION_DOWN); + LOG_DEBUG("action SWIPE: top to bottom"); + } + } + // tap + else { + if (duration > 0 && duration < TIME_LONG_PRESS) { + if (_tapped) { + _tapped = false; + } else { + _tapped = true; + } + } else { + _tapped = false; + } + } } - _touchedOld = touched; + } + _touchedOld = touched; #if defined RAK14014 - // Speed up the processing speed of the keyboard in virtual keyboard mode - auto state = cannedMessageModule->getRunState(); - if (state == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - if (_tapped) { - _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_TAP); - LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); - } - } else { - if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { - _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_TAP); - LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); - } - } -#else - // fire TAP event when no 2nd tap occured within time + // Speed up the processing speed of the keyboard in virtual keyboard mode + auto state = cannedMessageModule->getRunState(); + if (state == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (_tapped) { - _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_TAP); - LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); } + } else { + if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + } + } +#else + // fire TAP event when no 2nd tap occured within time + if (_tapped) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + } #endif - // fire LONG_PRESS event without the need for release - if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { - // tricky: prevent reoccurring events and another touch event when releasing - _start = millis() + 30000; - e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); - LOG_DEBUG("action LONG PRESS(%d/%d)", _last_x, _last_y); - } + // fire LONG_PRESS event without the need for release + if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { + // tricky: prevent reoccurring events and another touch event when releasing + _start = millis() + 30000; + e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); + LOG_DEBUG("action LONG PRESS(%d/%d)", _last_x, _last_y); + } - if (e.touchEvent != TOUCH_ACTION_NONE) { - e.source = this->_originName; - e.x = _last_x; - e.y = _last_y; - onEvent(e); - } + if (e.touchEvent != TOUCH_ACTION_NONE) { + e.source = this->_originName; + e.x = _last_x; + e.y = _last_y; + onEvent(e); + } - return interval; + return interval; } -void TouchScreenBase::hapticFeedback() -{ +void TouchScreenBase::hapticFeedback() { #ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 0); // end waveform - drv.go(); + drv.setWaveform(0, 75); + drv.setWaveform(1, 0); // end waveform + drv.go(); #endif } diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h index 90314cf02..67502fde6 100644 --- a/src/input/TouchScreenBase.h +++ b/src/input/TouchScreenBase.h @@ -6,50 +6,49 @@ #include "time.h" typedef struct _TouchEvent { - const char *source; - char touchEvent; - uint16_t x; - uint16_t y; + const char *source; + char touchEvent; + uint16_t x; + uint16_t y; } TouchEvent; -class TouchScreenBase : public Observable, public concurrency::OSThread -{ - public: - explicit TouchScreenBase(const char *name, uint16_t width, uint16_t height); - void init(bool hasTouch); +class TouchScreenBase : public Observable, public concurrency::OSThread { +public: + explicit TouchScreenBase(const char *name, uint16_t width, uint16_t height); + void init(bool hasTouch); - protected: - enum TouchScreenBaseStateType { TOUCH_EVENT_OCCURRED, TOUCH_EVENT_CLEARED }; +protected: + enum TouchScreenBaseStateType { TOUCH_EVENT_OCCURRED, TOUCH_EVENT_CLEARED }; - enum TouchScreenBaseEventType { - TOUCH_ACTION_NONE, - TOUCH_ACTION_UP, - TOUCH_ACTION_DOWN, - TOUCH_ACTION_LEFT, - TOUCH_ACTION_RIGHT, - TOUCH_ACTION_TAP, - TOUCH_ACTION_LONG_PRESS - }; + enum TouchScreenBaseEventType { + TOUCH_ACTION_NONE, + TOUCH_ACTION_UP, + TOUCH_ACTION_DOWN, + TOUCH_ACTION_LEFT, + TOUCH_ACTION_RIGHT, + TOUCH_ACTION_TAP, + TOUCH_ACTION_LONG_PRESS + }; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - virtual bool getTouch(int16_t &x, int16_t &y) = 0; - virtual void onEvent(const TouchEvent &event) = 0; + virtual bool getTouch(int16_t &x, int16_t &y) = 0; + virtual void onEvent(const TouchEvent &event) = 0; - volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED; - volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE; - void hapticFeedback(); + volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED; + volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE; + void hapticFeedback(); - protected: - uint16_t _display_width; - uint16_t _display_height; +protected: + uint16_t _display_width; + uint16_t _display_height; - private: - bool _touchedOld = false; // previous touch state - int16_t _first_x, _last_x; // horizontal swipe direction - int16_t _first_y, _last_y; // vertical swipe direction - time_t _start; // for LONG_PRESS - bool _tapped; // for DOUBLE_TAP +private: + bool _touchedOld = false; // previous touch state + int16_t _first_x, _last_x; // horizontal swipe direction + int16_t _first_y, _last_y; // vertical swipe direction + time_t _start; // for LONG_PRESS + bool _tapped; // for DOUBLE_TAP - const char *_originName; + const char *_originName; }; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index 69dcab04e..8369cabb9 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -11,32 +11,26 @@ TouchScreenImpl1 *touchScreenImpl1; TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)) - : TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch) -{ -} + : TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch) {} -void TouchScreenImpl1::init() -{ +void TouchScreenImpl1::init() { #if ARCH_PORTDUINO - if (portduino_config.touchscreenModule) { - TouchScreenBase::init(true); - inputBroker->registerSource(this); - } else { - TouchScreenBase::init(false); - } -#elif !HAS_TOUCHSCREEN - TouchScreenBase::init(false); - return; -#else + if (portduino_config.touchscreenModule) { TouchScreenBase::init(true); inputBroker->registerSource(this); + } else { + TouchScreenBase::init(false); + } +#elif !HAS_TOUCHSCREEN + TouchScreenBase::init(false); + return; +#else + TouchScreenBase::init(true); + inputBroker->registerSource(this); #endif } -bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) -{ - return _getTouch(&x, &y); -} +bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) { return _getTouch(&x, &y); } /** * @brief forward touchscreen event @@ -45,41 +39,40 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) * * The touchscreen events are translated to input events and reversed */ -void TouchScreenImpl1::onEvent(const TouchEvent &event) -{ - InputEvent e = {}; - e.source = event.source; - e.kbchar = 0; - e.touchX = event.x; - e.touchY = event.y; +void TouchScreenImpl1::onEvent(const TouchEvent &event) { + InputEvent e = {}; + e.source = event.source; + e.kbchar = 0; + e.touchX = event.x; + e.touchY = event.y; - switch (event.touchEvent) { - case TOUCH_ACTION_LEFT: { - e.inputEvent = INPUT_BROKER_LEFT; - break; - } - case TOUCH_ACTION_RIGHT: { - e.inputEvent = INPUT_BROKER_RIGHT; - break; - } - case TOUCH_ACTION_UP: { - e.inputEvent = INPUT_BROKER_UP; - break; - } - case TOUCH_ACTION_DOWN: { - e.inputEvent = INPUT_BROKER_DOWN; - break; - } - case TOUCH_ACTION_LONG_PRESS: { - e.inputEvent = INPUT_BROKER_SELECT; - break; - } - case TOUCH_ACTION_TAP: { - e.inputEvent = INPUT_BROKER_USER_PRESS; - break; - } - default: - return; - } - this->notifyObservers(&e); + switch (event.touchEvent) { + case TOUCH_ACTION_LEFT: { + e.inputEvent = INPUT_BROKER_LEFT; + break; + } + case TOUCH_ACTION_RIGHT: { + e.inputEvent = INPUT_BROKER_RIGHT; + break; + } + case TOUCH_ACTION_UP: { + e.inputEvent = INPUT_BROKER_UP; + break; + } + case TOUCH_ACTION_DOWN: { + e.inputEvent = INPUT_BROKER_DOWN; + break; + } + case TOUCH_ACTION_LONG_PRESS: { + e.inputEvent = INPUT_BROKER_SELECT; + break; + } + case TOUCH_ACTION_TAP: { + e.inputEvent = INPUT_BROKER_USER_PRESS; + break; + } + default: + return; + } + this->notifyObservers(&e); } \ No newline at end of file diff --git a/src/input/TouchScreenImpl1.h b/src/input/TouchScreenImpl1.h index 0c5338459..5d55442d9 100644 --- a/src/input/TouchScreenImpl1.h +++ b/src/input/TouchScreenImpl1.h @@ -1,17 +1,16 @@ #pragma once #include "TouchScreenBase.h" -class TouchScreenImpl1 : public TouchScreenBase -{ - public: - TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)); - void init(void); +class TouchScreenImpl1 : public TouchScreenBase { +public: + TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)); + void init(void); - protected: - virtual bool getTouch(int16_t &x, int16_t &y); - virtual void onEvent(const TouchEvent &event); +protected: + virtual bool getTouch(int16_t &x, int16_t &y); + virtual void onEvent(const TouchEvent &event); - bool (*_getTouch)(int16_t *, int16_t *); + bool (*_getTouch)(int16_t *, int16_t *); }; extern TouchScreenImpl1 *touchScreenImpl1; diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index bbd07e199..bf9a82a33 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -4,219 +4,201 @@ extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} -void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, - input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, - input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) -{ - this->_pinDown = pinDown; - this->_pinUp = pinUp; - this->_pinLeft = pinLeft; - this->_pinRight = pinRight; - this->_pinPress = pinPress; - this->_eventDown = eventDown; - this->_eventUp = eventUp; - this->_eventLeft = eventLeft; - this->_eventRight = eventRight; - this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; +void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, + input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, + input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_pinLeft = pinLeft; + this->_pinRight = pinRight; + this->_pinPress = pinPress; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventLeft = eventLeft; + this->_eventRight = eventRight; + this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; - if (pinPress != 255) { - pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, TB_DIRECTION); - } - if (this->_pinDown != 255) { - pinMode(this->_pinDown, INPUT_PULLUP); - attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); - } - if (this->_pinUp != 255) { - pinMode(this->_pinUp, INPUT_PULLUP); - attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); - } - if (this->_pinLeft != 255) { - pinMode(this->_pinLeft, INPUT_PULLUP); - attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); - } - if (this->_pinRight != 255) { - pinMode(this->_pinRight, INPUT_PULLUP); - attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); - } + if (pinPress != 255) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, TB_DIRECTION); + } + if (this->_pinDown != 255) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); + } + if (this->_pinUp != 255) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); + } + if (this->_pinLeft != 255) { + pinMode(this->_pinLeft, INPUT_PULLUP); + attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); + } + if (this->_pinRight != 255) { + pinMode(this->_pinRight, INPUT_PULLUP); + attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); + } - LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, - this->_pinLeft, this->_pinRight, pinPress); + LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, + pinPress); #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif - this->setInterval(100); + this->setInterval(100); } -int32_t TrackballInterruptBase::runOnce() -{ - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; +int32_t TrackballInterruptBase::runOnce() { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = millis() - pressStartTime; - bool buttonStillPressed = false; + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = millis() - pressStartTime; + bool buttonStillPressed = false; #if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); + buttonStillPressed = (this->action == TB_ACTION_PRESSED); #else - buttonStillPressed = !digitalRead(_pinPress); + buttonStillPressed = !digitalRead(_pinPress); #endif - if (!buttonStillPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION) { - // Short press - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastLongPressEventTime = 0; - this->action = TB_ACTION_NONE; - } else if (pressDuration >= LONG_PRESS_DURATION) { - // Long press detected - uint32_t currentTime = millis(); - // Only trigger long press event if enough time has passed since the last one - if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventPressedLong; - lastLongPressEventTime = currentTime; - } - this->action = TB_ACTION_PRESSED_LONG; - } + if (!buttonStillPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION) { + // Short press + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastLongPressEventTime = 0; + this->action = TB_ACTION_NONE; + } else if (pressDuration >= LONG_PRESS_DURATION) { + // Long press detected + uint32_t currentTime = millis(); + // Only trigger long press event if enough time has passed since the last one + if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventPressedLong; + lastLongPressEventTime = currentTime; + } + this->action = TB_ACTION_PRESSED_LONG; + } + } + + if (directionDetected && directionStartTime > 0) { + uint32_t directionDuration = millis() - directionStartTime; + uint8_t directionPressedNow = 0; + directionInterval++; + + if (!digitalRead(_pinUp)) { + directionPressedNow = TB_ACTION_UP; + } else if (!digitalRead(_pinDown)) { + directionPressedNow = TB_ACTION_DOWN; + } else if (!digitalRead(_pinLeft)) { + directionPressedNow = TB_ACTION_LEFT; + } else if (!digitalRead(_pinRight)) { + directionPressedNow = TB_ACTION_RIGHT; } - if (directionDetected && directionStartTime > 0) { - uint32_t directionDuration = millis() - directionStartTime; - uint8_t directionPressedNow = 0; - directionInterval++; + const uint8_t DIRECTION_REPEAT_THRESHOLD = 3; - if (!digitalRead(_pinUp)) { - directionPressedNow = TB_ACTION_UP; - } else if (!digitalRead(_pinDown)) { - directionPressedNow = TB_ACTION_DOWN; - } else if (!digitalRead(_pinLeft)) { - directionPressedNow = TB_ACTION_LEFT; - } else if (!digitalRead(_pinRight)) { - directionPressedNow = TB_ACTION_RIGHT; - } + if (directionPressedNow == TB_ACTION_NONE) { + // Reset state + directionDetected = false; + directionStartTime = 0; + directionInterval = 0; + this->action = TB_ACTION_NONE; + } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) { + // repeat event when long press these direction. + switch (directionPressedNow) { + case TB_ACTION_UP: + e.inputEvent = this->_eventUp; + break; + case TB_ACTION_DOWN: + e.inputEvent = this->_eventDown; + break; + case TB_ACTION_LEFT: + e.inputEvent = this->_eventLeft; + break; + case TB_ACTION_RIGHT: + e.inputEvent = this->_eventRight; + break; + } - const uint8_t DIRECTION_REPEAT_THRESHOLD = 3; - - if (directionPressedNow == TB_ACTION_NONE) { - // Reset state - directionDetected = false; - directionStartTime = 0; - directionInterval = 0; - this->action = TB_ACTION_NONE; - } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) { - // repeat event when long press these direction. - switch (directionPressedNow) { - case TB_ACTION_UP: - e.inputEvent = this->_eventUp; - break; - case TB_ACTION_DOWN: - e.inputEvent = this->_eventDown; - break; - case TB_ACTION_LEFT: - e.inputEvent = this->_eventLeft; - break; - case TB_ACTION_RIGHT: - e.inputEvent = this->_eventRight; - break; - } - - directionInterval = 0; - } + directionInterval = 0; } + } #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { - // LOG_DEBUG("Trackball event UP"); - e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { - // LOG_DEBUG("Trackball event DOWN"); - e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { - // LOG_DEBUG("Trackball event LEFT"); - e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { - // LOG_DEBUG("Trackball event RIGHT"); - e.inputEvent = this->_eventRight; - } + if (this->action == TB_ACTION_PRESSED && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press + } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { + // LOG_DEBUG("Trackball event UP"); + e.inputEvent = this->_eventUp; + } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { + // LOG_DEBUG("Trackball event DOWN"); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { + // LOG_DEBUG("Trackball event LEFT"); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { + // LOG_DEBUG("Trackball event RIGHT"); + e.inputEvent = this->_eventRight; + } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventUp; - // send event first,will automatically trigger every 50ms * 3 after 500ms - } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventRight; - } + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press + } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventUp; + // send event first,will automatically trigger every 50ms * 3 after 500ms + } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventRight; + } #endif - if (e.inputEvent != INPUT_BROKER_NONE) { - e.source = this->_originName; - e.kbchar = 0x00; - this->notifyObservers(&e); + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = 0x00; + this->notifyObservers(&e); + } + + // Only update lastEvent for non-press actions or completed press actions + if (this->action != TB_ACTION_PRESSED || !pressDetected) { + lastEvent = action; + if (!pressDetected) { + this->action = TB_ACTION_NONE; } + } - // Only update lastEvent for non-press actions or completed press actions - if (this->action != TB_ACTION_PRESSED || !pressDetected) { - lastEvent = action; - if (!pressDetected) { - this->action = TB_ACTION_NONE; - } - } - - return 50; // Check more frequently for better long press detection + return 50; // Check more frequently for better long press detection } -void TrackballInterruptBase::intPressHandler() -{ - this->action = TB_ACTION_PRESSED; -} +void TrackballInterruptBase::intPressHandler() { this->action = TB_ACTION_PRESSED; } -void TrackballInterruptBase::intDownHandler() -{ - this->action = TB_ACTION_DOWN; -} +void TrackballInterruptBase::intDownHandler() { this->action = TB_ACTION_DOWN; } -void TrackballInterruptBase::intUpHandler() -{ - this->action = TB_ACTION_UP; -} +void TrackballInterruptBase::intUpHandler() { this->action = TB_ACTION_UP; } -void TrackballInterruptBase::intLeftHandler() -{ - this->action = TB_ACTION_LEFT; -} +void TrackballInterruptBase::intLeftHandler() { this->action = TB_ACTION_LEFT; } -void TrackballInterruptBase::intRightHandler() -{ - this->action = TB_ACTION_RIGHT; -} +void TrackballInterruptBase::intRightHandler() { this->action = TB_ACTION_RIGHT; } diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 67d4ee449..29c124d5d 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -12,59 +12,58 @@ #endif #endif -class TrackballInterruptBase : public Observable, public concurrency::OSThread -{ - public: - explicit TrackballInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, - input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); - void intPressHandler(); - void intDownHandler(); - void intUpHandler(); - void intLeftHandler(); - void intRightHandler(); - uint32_t lastTime = 0; +class TrackballInterruptBase : public Observable, public concurrency::OSThread { +public: + explicit TrackballInterruptBase(const char *name); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, + input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, input_broker_event eventPressed, + input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); + void intLeftHandler(); + void intRightHandler(); + uint32_t lastTime = 0; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - protected: - enum TrackballInterruptBaseActionType { - TB_ACTION_NONE, - TB_ACTION_PRESSED, - TB_ACTION_PRESSED_LONG, - TB_ACTION_UP, - TB_ACTION_DOWN, - TB_ACTION_LEFT, - TB_ACTION_RIGHT - }; - uint8_t _pinDown = 0; - uint8_t _pinUp = 0; - uint8_t _pinLeft = 0; - uint8_t _pinRight = 0; - uint8_t _pinPress = 0; +protected: + enum TrackballInterruptBaseActionType { + TB_ACTION_NONE, + TB_ACTION_PRESSED, + TB_ACTION_PRESSED_LONG, + TB_ACTION_UP, + TB_ACTION_DOWN, + TB_ACTION_LEFT, + TB_ACTION_RIGHT + }; + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + uint8_t _pinLeft = 0; + uint8_t _pinRight = 0; + uint8_t _pinPress = 0; - volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - // Long press detection for press button - uint32_t pressStartTime = 0; - uint32_t directionStartTime = 0; - uint8_t directionInterval = 0; - bool pressDetected = false; - bool directionDetected = false; - uint32_t lastLongPressEventTime = 0; - uint32_t lastDirectionPressEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = 500; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events + // Long press detection for press button + uint32_t pressStartTime = 0; + uint32_t directionStartTime = 0; + uint8_t directionInterval = 0; + bool pressDetected = false; + bool directionDetected = false; + uint32_t lastLongPressEventTime = 0; + uint32_t lastDirectionPressEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = 500; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events - private: - input_broker_event _eventDown = INPUT_BROKER_NONE; - input_broker_event _eventUp = INPUT_BROKER_NONE; - input_broker_event _eventLeft = INPUT_BROKER_NONE; - input_broker_event _eventRight = INPUT_BROKER_NONE; - input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; - const char *_originName; - TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; +private: + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventLeft = INPUT_BROKER_NONE; + input_broker_event _eventRight = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + const char *_originName; + TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 594facdeb..e9e71b7a0 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -6,59 +6,52 @@ TrackballInterruptImpl1 *trackballInterruptImpl1; TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} -void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) -{ - input_broker_event eventDown = INPUT_BROKER_DOWN; - input_broker_event eventUp = INPUT_BROKER_UP; - input_broker_event eventLeft = INPUT_BROKER_LEFT; - input_broker_event eventRight = INPUT_BROKER_RIGHT; - input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; +void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) { + input_broker_event eventDown = INPUT_BROKER_DOWN; + input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventLeft = INPUT_BROKER_LEFT; + input_broker_event eventRight = INPUT_BROKER_RIGHT; + input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; - TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, - TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, - TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); - inputBroker->registerSource(this); + TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, eventPressed, eventPressedLong, + TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, + TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); } -void TrackballInterruptImpl1::handleIntDown() -{ - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intDownHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntDown() { + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intDownHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntUp() -{ - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intUpHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntUp() { + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intUpHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntLeft() -{ - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intLeftHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntLeft() { + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intLeftHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntRight() -{ - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intRightHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntRight() { + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intRightHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntPressed() -{ - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intPressHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntPressed() { + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intPressHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } diff --git a/src/input/TrackballInterruptImpl1.h b/src/input/TrackballInterruptImpl1.h index 4683efa41..0647c71b2 100644 --- a/src/input/TrackballInterruptImpl1.h +++ b/src/input/TrackballInterruptImpl1.h @@ -1,16 +1,15 @@ #pragma once #include "TrackballInterruptBase.h" -class TrackballInterruptImpl1 : public TrackballInterruptBase -{ - public: - TrackballInterruptImpl1(); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); - static void handleIntDown(); - static void handleIntUp(); - static void handleIntLeft(); - static void handleIntRight(); - static void handleIntPressed(); +class TrackballInterruptImpl1 : public TrackballInterruptBase { +public: + TrackballInterruptImpl1(); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); + static void handleIntDown(); + static void handleIntUp(); + static void handleIntLeft(); + static void handleIntRight(); + static void handleIntPressed(); }; extern TrackballInterruptImpl1 *trackballInterruptImpl1; diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index d597c8d8f..a3d74142b 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -1,165 +1,151 @@ #include "UpDownInterruptBase.h" #include "configuration.h" -UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThread(name) -{ - this->_originName = name; -} +UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThread(name) { this->_originName = name; } -void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, - input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong, - input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(), - void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) -{ - this->_pinDown = pinDown; - this->_pinUp = pinUp; - this->_pinPress = pinPress; - this->_eventDown = eventDown; - this->_eventUp = eventUp; - this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; - this->_eventUpLong = eventUpLong; - this->_eventDownLong = eventDownLong; +void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, + input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, + input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), + unsigned long updownDebounceMs) { + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_pinPress = pinPress; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; + this->_eventUpLong = eventUpLong; + this->_eventDownLong = eventDownLong; - // Store debounce configuration passed by caller - this->updownDebounceMs = updownDebounceMs; - bool isRAK = false; + // Store debounce configuration passed by caller + this->updownDebounceMs = updownDebounceMs; + bool isRAK = false; #ifdef RAK_4631 - isRAK = true; + isRAK = true; #endif - if (!isRAK || pinPress != 0) { - pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, FALLING); - } - if (!isRAK || this->_pinDown != 0) { - pinMode(this->_pinDown, INPUT_PULLUP); - attachInterrupt(this->_pinDown, onIntDown, FALLING); - } - if (!isRAK || this->_pinUp != 0) { - pinMode(this->_pinUp, INPUT_PULLUP); - attachInterrupt(this->_pinUp, onIntUp, FALLING); - } + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, FALLING); + } + if (!isRAK || this->_pinDown != 0) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, FALLING); + } + if (!isRAK || this->_pinUp != 0) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, FALLING); + } - LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); + LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); - this->setInterval(20); + this->setInterval(20); } -int32_t UpDownInterruptBase::runOnce() -{ - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - unsigned long now = millis(); +int32_t UpDownInterruptBase::runOnce() { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + unsigned long now = millis(); - // Read all button states once at the beginning - bool pressButtonPressed = !digitalRead(_pinPress); - bool upButtonPressed = !digitalRead(_pinUp); - bool downButtonPressed = !digitalRead(_pinDown); + // Read all button states once at the beginning + bool pressButtonPressed = !digitalRead(_pinPress); + bool upButtonPressed = !digitalRead(_pinUp); + bool downButtonPressed = !digitalRead(_pinDown); - // Handle initial button press detection - only if not already detected - if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) { - pressDetected = true; - pressStartTime = now; - } else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) { - upDetected = true; - upStartTime = now; - } else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) { - downDetected = true; - downStartTime = now; + // Handle initial button press detection - only if not already detected + if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) { + pressDetected = true; + pressStartTime = now; + } else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) { + upDetected = true; + upStartTime = now; + } else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) { + downDetected = true; + downStartTime = now; + } + + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = now - pressStartTime; + + if (!pressButtonPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastPressLongEventTime = 0; + } else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) { + // First long press event only - avoid repeated events causing lag + e.inputEvent = this->_eventPressedLong; + lastPressLongEventTime = now; } + } - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = now - pressStartTime; + // Handle long press detection for up button + if (upDetected && upStartTime > 0) { + uint32_t upDuration = now - upStartTime; - if (!pressButtonPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { - lastPressKeyTime = now; - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastPressLongEventTime = 0; - } else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) { - // First long press event only - avoid repeated events causing lag - e.inputEvent = this->_eventPressedLong; - lastPressLongEventTime = now; - } + if (!upButtonPressed) { + // Button released + if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) { + lastUpKeyTime = now; + e.inputEvent = this->_eventUp; + } + // Reset state + upDetected = false; + upStartTime = 0; + lastUpLongEventTime = 0; + } else if (upDuration >= LONG_PRESS_DURATION) { + // Auto-repeat long press events + if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventUpLong; + lastUpLongEventTime = now; + } } + } - // Handle long press detection for up button - if (upDetected && upStartTime > 0) { - uint32_t upDuration = now - upStartTime; + // Handle long press detection for down button + if (downDetected && downStartTime > 0) { + uint32_t downDuration = now - downStartTime; - if (!upButtonPressed) { - // Button released - if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) { - lastUpKeyTime = now; - e.inputEvent = this->_eventUp; - } - // Reset state - upDetected = false; - upStartTime = 0; - lastUpLongEventTime = 0; - } else if (upDuration >= LONG_PRESS_DURATION) { - // Auto-repeat long press events - if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventUpLong; - lastUpLongEventTime = now; - } - } + if (!downButtonPressed) { + // Button released + if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) { + lastDownKeyTime = now; + e.inputEvent = this->_eventDown; + } + // Reset state + downDetected = false; + downStartTime = 0; + lastDownLongEventTime = 0; + } else if (downDuration >= LONG_PRESS_DURATION) { + // Auto-repeat long press events + if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventDownLong; + lastDownLongEventTime = now; + } } + } - // Handle long press detection for down button - if (downDetected && downStartTime > 0) { - uint32_t downDuration = now - downStartTime; + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = INPUT_BROKER_NONE; + this->notifyObservers(&e); + } - if (!downButtonPressed) { - // Button released - if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) { - lastDownKeyTime = now; - e.inputEvent = this->_eventDown; - } - // Reset state - downDetected = false; - downStartTime = 0; - lastDownLongEventTime = 0; - } else if (downDuration >= LONG_PRESS_DURATION) { - // Auto-repeat long press events - if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventDownLong; - lastDownLongEventTime = now; - } - } - } + if (!pressDetected && !upDetected && !downDetected) { + this->action = UPDOWN_ACTION_NONE; + } - if (e.inputEvent != INPUT_BROKER_NONE) { - e.source = this->_originName; - e.kbchar = INPUT_BROKER_NONE; - this->notifyObservers(&e); - } - - if (!pressDetected && !upDetected && !downDetected) { - this->action = UPDOWN_ACTION_NONE; - } - - return 20; // This will control how the input frequency + return 20; // This will control how the input frequency } -void UpDownInterruptBase::intPressHandler() -{ - this->action = UPDOWN_ACTION_PRESSED; -} +void UpDownInterruptBase::intPressHandler() { this->action = UPDOWN_ACTION_PRESSED; } -void UpDownInterruptBase::intDownHandler() -{ - this->action = UPDOWN_ACTION_DOWN; -} +void UpDownInterruptBase::intDownHandler() { this->action = UPDOWN_ACTION_DOWN; } -void UpDownInterruptBase::intUpHandler() -{ - this->action = UPDOWN_ACTION_UP; -} +void UpDownInterruptBase::intUpHandler() { this->action = UPDOWN_ACTION_UP; } diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h index 2b9d38c83..1da38ecd9 100644 --- a/src/input/UpDownInterruptBase.h +++ b/src/input/UpDownInterruptBase.h @@ -11,61 +11,59 @@ #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 300 #endif -class UpDownInterruptBase : public Observable, public concurrency::OSThread -{ - public: - explicit UpDownInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, - input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, - input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), - unsigned long updownDebounceMs = 50); - void intPressHandler(); - void intDownHandler(); - void intUpHandler(); +class UpDownInterruptBase : public Observable, public concurrency::OSThread { +public: + explicit UpDownInterruptBase(const char *name); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, + input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, input_broker_event eventDownLong, + void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs = 50); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); - int32_t runOnce() override; + int32_t runOnce() override; - protected: - enum UpDownInterruptBaseActionType { - UPDOWN_ACTION_NONE, - UPDOWN_ACTION_PRESSED, - UPDOWN_ACTION_PRESSED_LONG, - UPDOWN_ACTION_UP, - UPDOWN_ACTION_UP_LONG, - UPDOWN_ACTION_DOWN, - UPDOWN_ACTION_DOWN_LONG - }; +protected: + enum UpDownInterruptBaseActionType { + UPDOWN_ACTION_NONE, + UPDOWN_ACTION_PRESSED, + UPDOWN_ACTION_PRESSED_LONG, + UPDOWN_ACTION_UP, + UPDOWN_ACTION_UP_LONG, + UPDOWN_ACTION_DOWN, + UPDOWN_ACTION_DOWN_LONG + }; - volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; + volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; - // Long press detection variables - uint32_t pressStartTime = 0; - uint32_t upStartTime = 0; - uint32_t downStartTime = 0; - bool pressDetected = false; - bool upDetected = false; - bool downDetected = false; - uint32_t lastPressLongEventTime = 0; - uint32_t lastUpLongEventTime = 0; - uint32_t lastDownLongEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION; - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL; + // Long press detection variables + uint32_t pressStartTime = 0; + uint32_t upStartTime = 0; + uint32_t downStartTime = 0; + bool pressDetected = false; + bool upDetected = false; + bool downDetected = false; + uint32_t lastPressLongEventTime = 0; + uint32_t lastUpLongEventTime = 0; + uint32_t lastDownLongEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION; + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL; - private: - uint8_t _pinDown = 0; - uint8_t _pinUp = 0; - uint8_t _pinPress = 0; - input_broker_event _eventDown = INPUT_BROKER_NONE; - input_broker_event _eventUp = INPUT_BROKER_NONE; - input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; - input_broker_event _eventUpLong = INPUT_BROKER_NONE; - input_broker_event _eventDownLong = INPUT_BROKER_NONE; - const char *_originName; +private: + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + uint8_t _pinPress = 0; + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + input_broker_event _eventUpLong = INPUT_BROKER_NONE; + input_broker_event _eventDownLong = INPUT_BROKER_NONE; + const char *_originName; - unsigned long lastUpKeyTime = 0; - unsigned long lastDownKeyTime = 0; - unsigned long lastPressKeyTime = 0; - unsigned long updownDebounceMs = 50; - const unsigned long pressDebounceMs = 200; + unsigned long lastUpKeyTime = 0; + unsigned long lastDownKeyTime = 0; + unsigned long lastPressKeyTime = 0; + unsigned long updownDebounceMs = 50; + const unsigned long pressDebounceMs = 200; }; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 906dcd2a8..99e4d8464 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -6,44 +6,33 @@ UpDownInterruptImpl1 *upDownInterruptImpl1; UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {} -bool UpDownInterruptImpl1::init() -{ +bool UpDownInterruptImpl1::init() { - if (!moduleConfig.canned_message.updown1_enabled) { - // Input device is disabled. - return false; - } + if (!moduleConfig.canned_message.updown1_enabled) { + // Input device is disabled. + return false; + } - uint8_t pinUp = moduleConfig.canned_message.inputbroker_pin_a; - uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; - uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; + uint8_t pinUp = moduleConfig.canned_message.inputbroker_pin_a; + uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; + uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN - input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP - input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; - input_broker_event eventUpLong = INPUT_BROKER_UP_LONG; - input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG; + input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN + input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP + input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; + input_broker_event eventUpLong = INPUT_BROKER_UP_LONG; + input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG; - UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong, - eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, - UpDownInterruptImpl1::handleIntPressed); - inputBroker->registerSource(this); + UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong, eventDownLong, + UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif - return true; + return true; } -void UpDownInterruptImpl1::handleIntDown() -{ - upDownInterruptImpl1->intDownHandler(); -} -void UpDownInterruptImpl1::handleIntUp() -{ - upDownInterruptImpl1->intUpHandler(); -} -void UpDownInterruptImpl1::handleIntPressed() -{ - upDownInterruptImpl1->intPressHandler(); -} \ No newline at end of file +void UpDownInterruptImpl1::handleIntDown() { upDownInterruptImpl1->intDownHandler(); } +void UpDownInterruptImpl1::handleIntUp() { upDownInterruptImpl1->intUpHandler(); } +void UpDownInterruptImpl1::handleIntPressed() { upDownInterruptImpl1->intPressHandler(); } \ No newline at end of file diff --git a/src/input/UpDownInterruptImpl1.h b/src/input/UpDownInterruptImpl1.h index 4739cd2ff..8c12bb87c 100644 --- a/src/input/UpDownInterruptImpl1.h +++ b/src/input/UpDownInterruptImpl1.h @@ -1,14 +1,13 @@ #pragma once #include "UpDownInterruptBase.h" -class UpDownInterruptImpl1 : public UpDownInterruptBase -{ - public: - UpDownInterruptImpl1(); - bool init(); - static void handleIntDown(); - static void handleIntUp(); - static void handleIntPressed(); +class UpDownInterruptImpl1 : public UpDownInterruptBase { +public: + UpDownInterruptImpl1(); + bool init(); + static void handleIntDown(); + static void handleIntUp(); + static void handleIntPressed(); }; extern UpDownInterruptImpl1 *upDownInterruptImpl1; \ No newline at end of file diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index cb03eb4ff..18d58a439 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -7,69 +7,68 @@ CardKbI2cImpl *cardKbI2cImpl; CardKbI2cImpl::CardKbI2cImpl() : KbI2cBase("cardKB") {} -void CardKbI2cImpl::init() -{ +void CardKbI2cImpl::init() { #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) - if (cardkb_found.address == 0x00) { - LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; + if (cardkb_found.address == 0x00) { + LOG_DEBUG("Rescan for I2C keyboard"); + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; #if defined(T_LORA_PAGER) - uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); + uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); #else - uint8_t i2caddr_asize = 5; + uint8_t i2caddr_asize = 5; #endif - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); #endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto kb_info = i2cScanner->firstKeyboard(); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto kb_info = i2cScanner->firstKeyboard(); - if (kb_info.type != ScanI2C::DeviceType::NONE) { - cardkb_found = kb_info.address; - switch (kb_info.type) { - case ScanI2C::DeviceType::RAK14004: - kb_model = 0x02; - break; - case ScanI2C::DeviceType::CARDKB: - kb_model = 0x00; - break; - case ScanI2C::DeviceType::TDECKKB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x10; - break; - case ScanI2C::DeviceType::BBQ10KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x11; - break; - case ScanI2C::DeviceType::MPR121KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x37; - break; - case ScanI2C::DeviceType::TCA8418KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x84; - break; - default: - // use this as default since it's also just zero - LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); - kb_model = 0x00; - } - } - if (cardkb_found.address == 0x00) { - disable(); - return; - } else { - LOG_DEBUG("Keyboard Type: 0x%02x Model: 0x%02x Address: 0x%02x", kb_info.type, kb_model, cardkb_found.address); - } + if (kb_info.type != ScanI2C::DeviceType::NONE) { + cardkb_found = kb_info.address; + switch (kb_info.type) { + case ScanI2C::DeviceType::RAK14004: + kb_model = 0x02; + break; + case ScanI2C::DeviceType::CARDKB: + kb_model = 0x00; + break; + case ScanI2C::DeviceType::TDECKKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x10; + break; + case ScanI2C::DeviceType::BBQ10KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x11; + break; + case ScanI2C::DeviceType::MPR121KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x37; + break; + case ScanI2C::DeviceType::TCA8418KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x84; + break; + default: + // use this as default since it's also just zero + LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); + kb_model = 0x00; + } } -#else if (cardkb_found.address == 0x00) { - disable(); - return; + disable(); + return; + } else { + LOG_DEBUG("Keyboard Type: 0x%02x Model: 0x%02x Address: 0x%02x", kb_info.type, kb_model, cardkb_found.address); } + } +#else + if (cardkb_found.address == 0x00) { + disable(); + return; + } #endif - inputBroker->registerSource(this); - kb_found = true; + inputBroker->registerSource(this); + kb_found = true; } \ No newline at end of file diff --git a/src/input/cardKbI2cImpl.h b/src/input/cardKbI2cImpl.h index 811a0558c..ce6a0758b 100644 --- a/src/input/cardKbI2cImpl.h +++ b/src/input/cardKbI2cImpl.h @@ -8,11 +8,10 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class CardKbI2cImpl : public KbI2cBase -{ - public: - CardKbI2cImpl(); - void init(); +class CardKbI2cImpl : public KbI2cBase { +public: + CardKbI2cImpl(); + void init(); }; extern CardKbI2cImpl *cardKbI2cImpl; \ No newline at end of file diff --git a/src/input/i2cButton.cpp b/src/input/i2cButton.cpp index d874146cd..105f4ff60 100644 --- a/src/input/i2cButton.cpp +++ b/src/input/i2cButton.cpp @@ -31,65 +31,63 @@ extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value); #define PI4IO_REG_IN_STA 0x0F #define PI4IO_REG_CHIP_RESET 0x01 -i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) -{ - _originName = name; - if (inputBroker) - inputBroker->registerSource(this); +i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) { + _originName = name; + if (inputBroker) + inputBroker->registerSource(this); } -int32_t i2cButtonThread::runOnce() -{ - static bool btn1_pressed = false; - static uint32_t press_start_time = 0; - const uint32_t LONG_PRESS_TIME = 1000; - static bool long_press_triggered = false; +int32_t i2cButtonThread::runOnce() { + static bool btn1_pressed = false; + static uint32_t press_start_time = 0; + const uint32_t LONG_PRESS_TIME = 1000; + static bool long_press_triggered = false; - uint8_t in_data; - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); - if (getbit(in_data, 0)) { - uint8_t input_state; - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); + uint8_t in_data; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); + if (getbit(in_data, 0)) { + uint8_t input_state; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); - if (!getbit(input_state, 0)) { - if (!btn1_pressed) { - btn1_pressed = true; - press_start_time = millis(); - long_press_triggered = false; - } - } else { - if (btn1_pressed) { - btn1_pressed = false; - uint32_t press_duration = millis() - press_start_time; - if (long_press_triggered) { - long_press_triggered = false; - return 50; - } - - if (press_duration < LONG_PRESS_TIME) { - InputEvent evt; - evt.source = "UserButton"; - evt.inputEvent = INPUT_BROKER_USER_PRESS; - evt.kbchar = 0; - evt.touchX = 0; - evt.touchY = 0; - this->notifyObservers(&evt); - } - } + if (!getbit(input_state, 0)) { + if (!btn1_pressed) { + btn1_pressed = true; + press_start_time = millis(); + long_press_triggered = false; + } + } else { + if (btn1_pressed) { + btn1_pressed = false; + uint32_t press_duration = millis() - press_start_time; + if (long_press_triggered) { + long_press_triggered = false; + return 50; } - } - if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { - long_press_triggered = true; - InputEvent evt; - evt.source = "UserButton"; - evt.inputEvent = INPUT_BROKER_SELECT; - evt.kbchar = 0; - evt.touchX = 0; - evt.touchY = 0; - this->notifyObservers(&evt); + if (press_duration < LONG_PRESS_TIME) { + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_USER_PRESS; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + } } - return 50; + } + + if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { + long_press_triggered = true; + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_SELECT; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + return 50; } #endif \ No newline at end of file diff --git a/src/input/i2cButton.h b/src/input/i2cButton.h index 1ad908606..821f8f3a6 100644 --- a/src/input/i2cButton.h +++ b/src/input/i2cButton.h @@ -6,12 +6,11 @@ #include "configuration.h" #if defined(M5STACK_UNITC6L) -class i2cButtonThread : public Observable, public concurrency::OSThread -{ - public: - const char *_originName; - explicit i2cButtonThread(const char *name); - int32_t runOnce() override; +class i2cButtonThread : public Observable, public concurrency::OSThread { +public: + const char *_originName; + explicit i2cButtonThread(const char *name); + int32_t runOnce() override; }; extern i2cButtonThread *i2cButton; diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 12d0822f6..e4276caea 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -28,505 +28,502 @@ KbI2cBase::KbI2cBase(const char *name) TCAKeyboard(*(new TCA8418Keyboard())) #endif { - this->_originName = name; + this->_originName = name; } -uint8_t read_from_14004(TwoWire *i2cBus, uint8_t reg, uint8_t *data, uint8_t length) -{ - uint8_t readflag = 0; - i2cBus->beginTransmission(CARDKB_ADDR); - i2cBus->write(reg); - i2cBus->endTransmission(); // stop transmitting - delay(20); - i2cBus->requestFrom(CARDKB_ADDR, (int)length); - int i = 0; - while (i2cBus->available()) // slave may send less than requested - { - data[i++] = i2cBus->read(); // receive a byte as a proper uint8_t - readflag = 1; - } - return readflag; +uint8_t read_from_14004(TwoWire *i2cBus, uint8_t reg, uint8_t *data, uint8_t length) { + uint8_t readflag = 0; + i2cBus->beginTransmission(CARDKB_ADDR); + i2cBus->write(reg); + i2cBus->endTransmission(); // stop transmitting + delay(20); + i2cBus->requestFrom(CARDKB_ADDR, (int)length); + int i = 0; + while (i2cBus->available()) // slave may send less than requested + { + data[i++] = i2cBus->read(); // receive a byte as a proper uint8_t + readflag = 1; + } + return readflag; } -int32_t KbI2cBase::runOnce() -{ - if (!i2cBus) { - switch (cardkb_found.port) { - case ScanI2C::WIRE1: +int32_t KbI2cBase::runOnce() { + if (!i2cBus) { + switch (cardkb_found.port) { + case ScanI2C::WIRE1: #if WIRE_INTERFACES_COUNT == 2 - LOG_DEBUG("Use I2C Bus 1 (the second one)"); - i2cBus = &Wire1; - if (cardkb_found.address == BBQ10_KB_ADDR) { - Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1); - Q10keyboard.setBacklight(0); - } - if (cardkb_found.address == MPR121_KB_ADDR) { - MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1); - } - if (cardkb_found.address == TCA8418_KB_ADDR) { - TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1); - } - break; + LOG_DEBUG("Use I2C Bus 1 (the second one)"); + i2cBus = &Wire1; + if (cardkb_found.address == BBQ10_KB_ADDR) { + Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1); + Q10keyboard.setBacklight(0); + } + if (cardkb_found.address == MPR121_KB_ADDR) { + MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1); + } + if (cardkb_found.address == TCA8418_KB_ADDR) { + TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1); + } + break; #endif - case ScanI2C::WIRE: - LOG_DEBUG("Use I2C Bus 0 (the first one)"); - i2cBus = &Wire; - if (cardkb_found.address == BBQ10_KB_ADDR) { - Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); - Q10keyboard.setBacklight(0); - } - if (cardkb_found.address == MPR121_KB_ADDR) { - MPRkeyboard.begin(MPR121_KB_ADDR, &Wire); - } - if (cardkb_found.address == TCA8418_KB_ADDR) { - TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire); - } - break; - case ScanI2C::NO_I2C: - default: - i2cBus = 0; - } - } - - switch (kb_model) { - case 0x11: { // BB Q10 - int keyCount = Q10keyboard.keyCount(); - while (keyCount--) { - const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); - if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - switch (key.key) { - case 'p': // TAB - case 't': // TAB as well - if (is_sym) { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x09; // TAB Scancode - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 'q': // ESC - if (is_sym) { - e.inputEvent = INPUT_BROKER_CANCEL; - e.kbchar = 0; - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 0x08: // Back - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = key.key; - break; - case 'e': // sym e - if (is_sym) { - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = INPUT_BROKER_UP; - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 'x': // sym x - if (is_sym) { - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0; - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 's': // sym s - if (is_sym) { - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0x00; // tweak for destSelect - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 'f': // sym f - if (is_sym) { - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0x00; // tweak for destSelect - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 0x13: // Code scanner says the SYM key is 0x13 - is_sym = !is_sym; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that - : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active - break; - case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return - e.inputEvent = INPUT_BROKER_SELECT; - break; - case 0x00: // nopress - e.inputEvent = INPUT_BROKER_NONE; - break; - default: // all other keys - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - is_sym = false; // reset sym state after second keypress - break; - } - - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } - } - } - break; - } - case 0x37: { // MPR121 - MPRkeyboard.trigger(); - InputEvent e = {}; - - while (MPRkeyboard.hasEvent()) { - char nextEvent = MPRkeyboard.dequeueEvent(); - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x00; - e.source = this->_originName; - switch (nextEvent) { - case 0x00: // MPR121_NONE - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - case 0x90: // MPR121_REBOOT - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_REBOOT; - break; - case 0xb4: // MPR121_LEFT - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0x00; - break; - case 0xb5: // MPR121_UP - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = 0x00; - break; - case 0xb6: // MPR121_DOWN - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0x00; - break; - case 0xb7: // MPR121_RIGHT - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0x00; - break; - case 0x1b: // MPR121_ESC - e.inputEvent = INPUT_BROKER_CANCEL; - e.kbchar = 0; - break; - case 0x08: // MPR121_BSP - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - break; - case 0x0d: // MPR121_SELECT - e.inputEvent = INPUT_BROKER_SELECT; - e.kbchar = 0x00; - break; - default: - if (nextEvent > 127) { - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = nextEvent; - break; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); - this->notifyObservers(&e); - } - } - break; - } - case 0x84: { // Adafruit TCA8418 - TCAKeyboard.trigger(); - InputEvent e = {}; - while (TCAKeyboard.hasEvent()) { - char nextEvent = TCAKeyboard.dequeueEvent(); - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x00; - e.source = this->_originName; - switch (nextEvent) { - case TCA8418KeyboardBase::NONE: - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::REBOOT: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_REBOOT; - break; - case TCA8418KeyboardBase::LEFT: - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::UP: - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::DOWN: - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::RIGHT: - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::BSP: - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - break; - case TCA8418KeyboardBase::SELECT: - e.inputEvent = INPUT_BROKER_SELECT; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::ESC: - e.inputEvent = INPUT_BROKER_CANCEL; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::GPS_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_GPS_TOGGLE; - break; - case TCA8418KeyboardBase::SEND_PING: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_SEND_PING; - break; - case TCA8418KeyboardBase::MUTE_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; - break; - case TCA8418KeyboardBase::BT_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; - break; - case TCA8418KeyboardBase::BL_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; - break; - case TCA8418KeyboardBase::TAB: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_TAB; - break; - default: - if (nextEvent > 127) { - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = nextEvent; - break; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); - this->notifyObservers(&e); - } - TCAKeyboard.trigger(); - } - TCAKeyboard.clearInt(); - break; - } - case 0x02: { - // RAK14004 - uint8_t rDataBuf[8] = {0}; - uint8_t PrintDataBuf = 0; - if (read_from_14004(i2cBus, 0x01, rDataBuf, 0x04) == 1) { - for (uint8_t aCount = 0; aCount < 0x04; aCount++) { - for (uint8_t bCount = 0; bCount < 0x04; bCount++) { - if (((rDataBuf[aCount] >> bCount) & 0x01) == 0x01) { - PrintDataBuf = aCount * 0x04 + bCount + 1; - } - } - } - } - if (PrintDataBuf != 0) { - LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_MATRIXKEY; - e.source = this->_originName; - e.kbchar = PrintDataBuf; - this->notifyObservers(&e); - } - break; - } - case 0x00: // CARDKB - case 0x10: { // T-DECK - - i2cBus->requestFrom((int)cardkb_found.address, 1); - - if (i2cBus->available()) { - char c = i2cBus->read(); - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - switch (c) { - case 0x71: // This is the button q. If modifier and q pressed, it cancels the input - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_CANCEL; - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x74: // letter t. if modifier and t pressed call 'tab' - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x09; // TAB Scancode - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x6d: // letter m. Modifier makes it mute notifications - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x6f: // letter o(+). Modifier makes screen increase in brightness - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x69: // letter i(-). Modifier makes screen decrease in brightness - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x20: // Space. Send network ping like double press does - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x67: // letter g. toggle gps - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_GPS_TOGGLE; - e.kbchar = INPUT_BROKER_GPS_TOGGLE; - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x1b: // ESC - e.inputEvent = INPUT_BROKER_CANCEL; - break; - case 0x08: // Back - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0; - break; - case 0xb5: // Up - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = 0; - break; - case 0xb6: // Down - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0; - break; - case 0xb4: // Left - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0; - break; - case 0xb7: // Right - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0; - break; - case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) - // toggle moddifiers button. - is_sym = !is_sym; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the - : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active - break; - case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE - e.inputEvent = INPUT_BROKER_GPS_TOGGLE; - e.kbchar = c; - break; - case 0xaf: // fn+space INPUT_BROKER_SEND_PING - e.inputEvent = INPUT_BROKER_SEND_PING; - e.kbchar = c; - break; - case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN - e.inputEvent = INPUT_BROKER_SHUTDOWN; - e.kbchar = c; - break; - - case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT - case 0x91: // fn+t - case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE - case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE - case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST - // just pass those unmodified - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - break; - case 0x0d: // Enter - e.inputEvent = INPUT_BROKER_SELECT; - break; - case 0x00: // nopress - e.inputEvent = INPUT_BROKER_NONE; - break; - default: // all other keys - if (c > 127) { // bogus key value - e.inputEvent = INPUT_BROKER_NONE; - break; - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - is_sym = false; - break; - } - - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } - } - break; - } + case ScanI2C::WIRE: + LOG_DEBUG("Use I2C Bus 0 (the first one)"); + i2cBus = &Wire; + if (cardkb_found.address == BBQ10_KB_ADDR) { + Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); + Q10keyboard.setBacklight(0); + } + if (cardkb_found.address == MPR121_KB_ADDR) { + MPRkeyboard.begin(MPR121_KB_ADDR, &Wire); + } + if (cardkb_found.address == TCA8418_KB_ADDR) { + TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire); + } + break; + case ScanI2C::NO_I2C: default: - LOG_WARN("Unknown kb_model 0x%02x", kb_model); + i2cBus = 0; } - return 300; + } + + switch (kb_model) { + case 0x11: { // BB Q10 + int keyCount = Q10keyboard.keyCount(); + while (keyCount--) { + const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); + if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + switch (key.key) { + case 'p': // TAB + case 't': // TAB as well + if (is_sym) { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x09; // TAB Scancode + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 'q': // ESC + if (is_sym) { + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 0x08: // Back + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = key.key; + break; + case 'e': // sym e + if (is_sym) { + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = INPUT_BROKER_UP; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 'x': // sym x + if (is_sym) { + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 's': // sym s + if (is_sym) { + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; // tweak for destSelect + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 'f': // sym f + if (is_sym) { + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; // tweak for destSelect + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 0x13: // Code scanner says the SYM key is 0x13 + is_sym = !is_sym; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that + : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active + break; + case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return + e.inputEvent = INPUT_BROKER_SELECT; + break; + case 0x00: // nopress + e.inputEvent = INPUT_BROKER_NONE; + break; + default: // all other keys + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + is_sym = false; // reset sym state after second keypress + break; + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + } + break; + } + case 0x37: { // MPR121 + MPRkeyboard.trigger(); + InputEvent e = {}; + + while (MPRkeyboard.hasEvent()) { + char nextEvent = MPRkeyboard.dequeueEvent(); + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x00; + e.source = this->_originName; + switch (nextEvent) { + case 0x00: // MPR121_NONE + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + case 0x90: // MPR121_REBOOT + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_REBOOT; + break; + case 0xb4: // MPR121_LEFT + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; + break; + case 0xb5: // MPR121_UP + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0x00; + break; + case 0xb6: // MPR121_DOWN + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0x00; + break; + case 0xb7: // MPR121_RIGHT + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; + break; + case 0x1b: // MPR121_ESC + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; + break; + case 0x08: // MPR121_BSP + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + break; + case 0x0d: // MPR121_SELECT + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; + break; + default: + if (nextEvent > 127) { + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = nextEvent; + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); + this->notifyObservers(&e); + } + } + break; + } + case 0x84: { // Adafruit TCA8418 + TCAKeyboard.trigger(); + InputEvent e = {}; + while (TCAKeyboard.hasEvent()) { + char nextEvent = TCAKeyboard.dequeueEvent(); + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x00; + e.source = this->_originName; + switch (nextEvent) { + case TCA8418KeyboardBase::NONE: + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::REBOOT: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_REBOOT; + break; + case TCA8418KeyboardBase::LEFT: + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::UP: + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::DOWN: + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::RIGHT: + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::BSP: + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + break; + case TCA8418KeyboardBase::SELECT: + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::ESC: + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::GPS_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_GPS_TOGGLE; + break; + case TCA8418KeyboardBase::SEND_PING: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_SEND_PING; + break; + case TCA8418KeyboardBase::MUTE_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; + break; + case TCA8418KeyboardBase::BT_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; + break; + case TCA8418KeyboardBase::BL_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; + break; + case TCA8418KeyboardBase::TAB: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_TAB; + break; + default: + if (nextEvent > 127) { + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = nextEvent; + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); + this->notifyObservers(&e); + } + TCAKeyboard.trigger(); + } + TCAKeyboard.clearInt(); + break; + } + case 0x02: { + // RAK14004 + uint8_t rDataBuf[8] = {0}; + uint8_t PrintDataBuf = 0; + if (read_from_14004(i2cBus, 0x01, rDataBuf, 0x04) == 1) { + for (uint8_t aCount = 0; aCount < 0x04; aCount++) { + for (uint8_t bCount = 0; bCount < 0x04; bCount++) { + if (((rDataBuf[aCount] >> bCount) & 0x01) == 0x01) { + PrintDataBuf = aCount * 0x04 + bCount + 1; + } + } + } + } + if (PrintDataBuf != 0) { + LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_MATRIXKEY; + e.source = this->_originName; + e.kbchar = PrintDataBuf; + this->notifyObservers(&e); + } + break; + } + case 0x00: // CARDKB + case 0x10: { // T-DECK + + i2cBus->requestFrom((int)cardkb_found.address, 1); + + if (i2cBus->available()) { + char c = i2cBus->read(); + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + switch (c) { + case 0x71: // This is the button q. If modifier and q pressed, it cancels the input + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_CANCEL; + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x74: // letter t. if modifier and t pressed call 'tab' + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x09; // TAB Scancode + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x6d: // letter m. Modifier makes it mute notifications + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x6f: // letter o(+). Modifier makes screen increase in brightness + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x69: // letter i(-). Modifier makes screen decrease in brightness + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x20: // Space. Send network ping like double press does + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x67: // letter g. toggle gps + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = INPUT_BROKER_GPS_TOGGLE; + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x1b: // ESC + e.inputEvent = INPUT_BROKER_CANCEL; + break; + case 0x08: // Back + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; + break; + case 0xb5: // Up + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0; + break; + case 0xb6: // Down + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; + break; + case 0xb4: // Left + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; + break; + case 0xb7: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; + break; + case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) + // toggle moddifiers button. + is_sym = !is_sym; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the + : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active + break; + case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = c; + break; + case 0xaf: // fn+space INPUT_BROKER_SEND_PING + e.inputEvent = INPUT_BROKER_SEND_PING; + e.kbchar = c; + break; + case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN + e.inputEvent = INPUT_BROKER_SHUTDOWN; + e.kbchar = c; + break; + + case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT + case 0x91: // fn+t + case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE + case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE + case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST + // just pass those unmodified + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + break; + case 0x0d: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + break; + case 0x00: // nopress + e.inputEvent = INPUT_BROKER_NONE; + break; + default: // all other keys + if (c > 127) { // bogus key value + e.inputEvent = INPUT_BROKER_NONE; + break; + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + is_sym = false; + break; + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + break; + } + default: + LOG_WARN("Unknown kb_model 0x%02x", kb_model); + } + return 300; } -void KbI2cBase::toggleBacklight(bool on) -{ +void KbI2cBase::toggleBacklight(bool on) { #if defined(T_LORA_PAGER) - TCAKeyboard.setBacklight(on); + TCAKeyboard.setBacklight(on); #endif } diff --git a/src/input/kbI2cBase.h b/src/input/kbI2cBase.h index ae769dff8..2b6af0a7e 100644 --- a/src/input/kbI2cBase.h +++ b/src/input/kbI2cBase.h @@ -8,22 +8,21 @@ class TCA8418KeyboardBase; -class KbI2cBase : public Observable, public concurrency::OSThread -{ - public: - explicit KbI2cBase(const char *name); - void toggleBacklight(bool on); +class KbI2cBase : public Observable, public concurrency::OSThread { +public: + explicit KbI2cBase(const char *name); + void toggleBacklight(bool on); - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; - private: - const char *_originName; +private: + const char *_originName; - TwoWire *i2cBus = 0; + TwoWire *i2cBus = 0; - BBQ10Keyboard Q10keyboard; - MPR121Keyboard MPRkeyboard; - TCA8418KeyboardBase &TCAKeyboard; - bool is_sym = false; + BBQ10Keyboard Q10keyboard; + MPR121Keyboard MPRkeyboard; + TCA8418KeyboardBase &TCAKeyboard; + bool is_sym = false; }; \ No newline at end of file diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp index 18243f3bf..bbd238dfe 100644 --- a/src/input/kbMatrixBase.cpp +++ b/src/input/kbMatrixBase.cpp @@ -30,102 +30,98 @@ unsigned char KeyMap[3][sizeof(keys_rows)][sizeof(keys_cols)] = {{{' ', '.', 'm' {'1', '2', '3', '4', '5', 0x1a}}}; #endif -KbMatrixBase::KbMatrixBase(const char *name) : concurrency::OSThread(name) -{ - this->_originName = name; -} +KbMatrixBase::KbMatrixBase(const char *name) : concurrency::OSThread(name) { this->_originName = name; } -int32_t KbMatrixBase::runOnce() -{ - if (!INPUTBROKER_MATRIX_TYPE) { - // Input device is not requested. - return disable(); +int32_t KbMatrixBase::runOnce() { + if (!INPUTBROKER_MATRIX_TYPE) { + // Input device is not requested. + return disable(); + } + + if (firstTime) { + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 0; + for (byte i = 0; i < sizeof(keys_rows); i++) { + pinMode(keys_rows[i], OUTPUT); + digitalWrite(keys_rows[i], HIGH); + } + for (byte i = 0; i < sizeof(keys_cols); i++) { + pinMode(keys_cols[i], INPUT_PULLUP); + } + } + + key = 0; + + if (INPUTBROKER_MATRIX_TYPE == 1) { + // scan for keypresses + for (byte i = 0; i < sizeof(keys_rows); i++) { + digitalWrite(keys_rows[i], LOW); + for (byte j = 0; j < sizeof(keys_cols); j++) { + if (digitalRead(keys_cols[j]) == LOW) { + key = KeyMap[shift][i][j]; + } + } + digitalWrite(keys_rows[i], HIGH); + } + // debounce + if (key != prevkey) { + if (key != 0) { + LOG_DEBUG("Key 0x%x pressed", key); + // reset shift now that we have a keypress + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + switch (key) { + case 0x1b: // ESC + e.inputEvent = INPUT_BROKER_CANCEL; + break; + case 0x08: // Back + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; + break; + case 0xb5: // Up + e.inputEvent = INPUT_BROKER_UP; + break; + case 0xb6: // Down + e.inputEvent = INPUT_BROKER_DOWN; + break; + case 0xb4: // Left + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; + break; + case 0xb7: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; + break; + case 0x0d: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + break; + case 0x00: // nopress + e.inputEvent = INPUT_BROKER_NONE; + break; + case 0x1a: // Shift + shift++; + if (shift > 2) { + shift = 0; + } + break; + default: // all other keys + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key; + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + prevkey = key; } - if (firstTime) { - // This is the first time the OSThread library has called this function, so do port setup - firstTime = 0; - for (byte i = 0; i < sizeof(keys_rows); i++) { - pinMode(keys_rows[i], OUTPUT); - digitalWrite(keys_rows[i], HIGH); - } - for (byte i = 0; i < sizeof(keys_cols); i++) { - pinMode(keys_cols[i], INPUT_PULLUP); - } - } - - key = 0; - - if (INPUTBROKER_MATRIX_TYPE == 1) { - // scan for keypresses - for (byte i = 0; i < sizeof(keys_rows); i++) { - digitalWrite(keys_rows[i], LOW); - for (byte j = 0; j < sizeof(keys_cols); j++) { - if (digitalRead(keys_cols[j]) == LOW) { - key = KeyMap[shift][i][j]; - } - } - digitalWrite(keys_rows[i], HIGH); - } - // debounce - if (key != prevkey) { - if (key != 0) { - LOG_DEBUG("Key 0x%x pressed", key); - // reset shift now that we have a keypress - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - switch (key) { - case 0x1b: // ESC - e.inputEvent = INPUT_BROKER_CANCEL; - break; - case 0x08: // Back - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0; - break; - case 0xb5: // Up - e.inputEvent = INPUT_BROKER_UP; - break; - case 0xb6: // Down - e.inputEvent = INPUT_BROKER_DOWN; - break; - case 0xb4: // Left - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0; - break; - case 0xb7: // Right - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0; - break; - case 0x0d: // Enter - e.inputEvent = INPUT_BROKER_SELECT; - break; - case 0x00: // nopress - e.inputEvent = INPUT_BROKER_NONE; - break; - case 0x1a: // Shift - shift++; - if (shift > 2) { - shift = 0; - } - break; - default: // all other keys - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key; - break; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } - } - prevkey = key; - } - - } else { - LOG_WARN("Unknown kb_model 0x%02x", INPUTBROKER_MATRIX_TYPE); - return disable(); - } - return 50; // Keyscan every 50msec to avoid key bounce + } else { + LOG_WARN("Unknown kb_model 0x%02x", INPUTBROKER_MATRIX_TYPE); + return disable(); + } + return 50; // Keyscan every 50msec to avoid key bounce } #endif // INPUTBROKER_MATRIX_TYPE \ No newline at end of file diff --git a/src/input/kbMatrixBase.h b/src/input/kbMatrixBase.h index 8259fc07c..873979d13 100644 --- a/src/input/kbMatrixBase.h +++ b/src/input/kbMatrixBase.h @@ -3,18 +3,17 @@ #include "InputBroker.h" #include "concurrency/OSThread.h" -class KbMatrixBase : public Observable, public concurrency::OSThread -{ - public: - explicit KbMatrixBase(const char *name); +class KbMatrixBase : public Observable, public concurrency::OSThread { +public: + explicit KbMatrixBase(const char *name); - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; - private: - const char *_originName; - bool firstTime = 1; - int shift = 0; - char key = 0; - char prevkey = 0; +private: + const char *_originName; + bool firstTime = 1; + int shift = 0; + char key = 0; + char prevkey = 0; }; \ No newline at end of file diff --git a/src/input/kbMatrixImpl.cpp b/src/input/kbMatrixImpl.cpp index 0561b16fe..6b6eb8c69 100644 --- a/src/input/kbMatrixImpl.cpp +++ b/src/input/kbMatrixImpl.cpp @@ -7,14 +7,13 @@ KbMatrixImpl *kbMatrixImpl; KbMatrixImpl::KbMatrixImpl() : KbMatrixBase("matrixKB") {} -void KbMatrixImpl::init() -{ - if (!INPUTBROKER_MATRIX_TYPE) { - disable(); - return; - } +void KbMatrixImpl::init() { + if (!INPUTBROKER_MATRIX_TYPE) { + disable(); + return; + } - inputBroker->registerSource(this); + inputBroker->registerSource(this); } #endif // INPUTBROKER_MATRIX_TYPE \ No newline at end of file diff --git a/src/input/kbMatrixImpl.h b/src/input/kbMatrixImpl.h index ead4a2d57..1d26a93df 100644 --- a/src/input/kbMatrixImpl.h +++ b/src/input/kbMatrixImpl.h @@ -9,11 +9,10 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class KbMatrixImpl : public KbMatrixBase -{ - public: - KbMatrixImpl(); - void init(); +class KbMatrixImpl : public KbMatrixBase { +public: + KbMatrixImpl(); + void init(); }; extern KbMatrixImpl *kbMatrixImpl; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 9ac060d37..1f9e2dbc2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -230,38 +230,36 @@ Router *router = NULL; // Users of router don't care what sort of subclass imple const char *firmware_version = optstr(APP_VERSION_SHORT); -const char *getDeviceName() -{ - uint8_t dmac[6]; +const char *getDeviceName() { + uint8_t dmac[6]; - getMacAddr(dmac); + getMacAddr(dmac); - // Meshtastic_ab3c or Shortname_abcd - static char name[20]; - snprintf(name, sizeof(name), "%02x%02x", dmac[4], dmac[5]); - // if the shortname exists and is NOT the new default of ab3c, use it for BLE name. - if (strcmp(owner.short_name, name) != 0) { - snprintf(name, sizeof(name), "%s_%02x%02x", owner.short_name, dmac[4], dmac[5]); - } else { - snprintf(name, sizeof(name), "Meshtastic_%02x%02x", dmac[4], dmac[5]); - } - return name; + // Meshtastic_ab3c or Shortname_abcd + static char name[20]; + snprintf(name, sizeof(name), "%02x%02x", dmac[4], dmac[5]); + // if the shortname exists and is NOT the new default of ab3c, use it for BLE name. + if (strcmp(owner.short_name, name) != 0) { + snprintf(name, sizeof(name), "%s_%02x%02x", owner.short_name, dmac[4], dmac[5]); + } else { + snprintf(name, sizeof(name), "Meshtastic_%02x%02x", dmac[4], dmac[5]); + } + return name; } -static int32_t ledBlinker() -{ - // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if - // config.device.led_heartbeat_disabled is changed - if (config.device.led_heartbeat_disabled) - return 1000; +static int32_t ledBlinker() { + // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if + // config.device.led_heartbeat_disabled is changed + if (config.device.led_heartbeat_disabled) + return 1000; - static bool ledOn; - ledOn ^= 1; + static bool ledOn; + ledOn ^= 1; - ledBlink.set(ledOn); + ledBlink.set(ledOn); - // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that - return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); + // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that + return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); } uint32_t timeLastPowered = 0; @@ -278,10 +276,7 @@ RadioLibHal *RadioLibHAL = NULL; /** * Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep. */ -__attribute__((weak, noinline)) bool loopCanSleep() -{ - return true; -} +__attribute__((weak, noinline)) bool loopCanSleep() { return true; } // Weak empty variant initialization function. // May be redefined by variant files. @@ -291,228 +286,224 @@ void lateInitVariant() {} /** * Print info as a structured log message (for automated log processing) */ -void printInfo() -{ - LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); -} +void printInfo() { LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); } #ifndef PIO_UNIT_TESTING -void setup() -{ +void setup() { #if defined(R1_NEO) - pinMode(DCDC_EN_HOLD, OUTPUT); - digitalWrite(DCDC_EN_HOLD, HIGH); - pinMode(NRF_ON, OUTPUT); - digitalWrite(NRF_ON, HIGH); + pinMode(DCDC_EN_HOLD, OUTPUT); + digitalWrite(DCDC_EN_HOLD, HIGH); + pinMode(NRF_ON, OUTPUT); + digitalWrite(NRF_ON, HIGH); #endif #if defined(PIN_POWER_EN) - pinMode(PIN_POWER_EN, OUTPUT); - digitalWrite(PIN_POWER_EN, HIGH); + pinMode(PIN_POWER_EN, OUTPUT); + digitalWrite(PIN_POWER_EN, HIGH); #endif #if defined(ELECROW_ThinkNode_M5) - Wire.begin(48, 47); - io.pinMode(PCA_PIN_EINK_EN, OUTPUT); - io.pinMode(PCA_PIN_POWER_EN, OUTPUT); - io.digitalWrite(PCA_PIN_POWER_EN, HIGH); - // io.pinMode(C2_PIN, OUTPUT); + Wire.begin(48, 47); + io.pinMode(PCA_PIN_EINK_EN, OUTPUT); + io.pinMode(PCA_PIN_POWER_EN, OUTPUT); + io.digitalWrite(PCA_PIN_POWER_EN, HIGH); + // io.pinMode(C2_PIN, OUTPUT); #endif #ifdef LED_POWER - pinMode(LED_POWER, OUTPUT); - digitalWrite(LED_POWER, LED_STATE_ON); + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LED_STATE_ON); #endif #ifdef USER_LED - pinMode(USER_LED, OUTPUT); - digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); + pinMode(USER_LED, OUTPUT); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); #endif #ifdef WIFI_LED - pinMode(WIFI_LED, OUTPUT); - digitalWrite(WIFI_LED, LOW); + pinMode(WIFI_LED, OUTPUT); + digitalWrite(WIFI_LED, LOW); #endif #ifdef BLE_LED - pinMode(BLE_LED, OUTPUT); + pinMode(BLE_LED, OUTPUT); #ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); + digitalWrite(BLE_LED, HIGH); #else - digitalWrite(BLE_LED, LOW); + digitalWrite(BLE_LED, LOW); #endif #endif #if defined(T_DECK) - // GPIO10 manages all peripheral power supplies - // Turn on peripheral power immediately after MUC starts. - // If some boards are turned on late, ESP32 will reset due to low voltage. - // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , - // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) - pinMode(KB_POWERON, OUTPUT); - digitalWrite(KB_POWERON, HIGH); - // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus - // We need to initialize all CS pins in advance otherwise there will be SPI communication issues - // e.g. when detecting the SD card - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - delay(100); + // GPIO10 manages all peripheral power supplies + // Turn on peripheral power immediately after MUC starts. + // If some boards are turned on late, ESP32 will reset due to low voltage. + // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , + // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) + pinMode(KB_POWERON, OUTPUT); + digitalWrite(KB_POWERON, HIGH); + // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus + // We need to initialize all CS pins in advance otherwise there will be SPI communication issues + // e.g. when detecting the SD card + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + delay(100); #elif defined(T_DECK_PRO) - pinMode(LORA_EN, OUTPUT); - digitalWrite(LORA_EN, HIGH); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(PIN_EINK_CS, OUTPUT); - digitalWrite(PIN_EINK_CS, HIGH); + pinMode(LORA_EN, OUTPUT); + digitalWrite(LORA_EN, HIGH); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(PIN_EINK_CS, OUTPUT); + digitalWrite(PIN_EINK_CS, HIGH); #elif defined(T_LORA_PAGER) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - pinMode(KB_INT, INPUT_PULLUP); - // io expander - io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); - io.pinMode(EXPANDS_DRV_EN, OUTPUT); - io.digitalWrite(EXPANDS_DRV_EN, HIGH); - io.pinMode(EXPANDS_AMP_EN, OUTPUT); - io.digitalWrite(EXPANDS_AMP_EN, LOW); - io.pinMode(EXPANDS_LORA_EN, OUTPUT); - io.digitalWrite(EXPANDS_LORA_EN, HIGH); - io.pinMode(EXPANDS_GPS_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPS_EN, HIGH); - io.pinMode(EXPANDS_KB_EN, OUTPUT); - io.digitalWrite(EXPANDS_KB_EN, HIGH); - io.pinMode(EXPANDS_SD_EN, OUTPUT); - io.digitalWrite(EXPANDS_SD_EN, HIGH); - io.pinMode(EXPANDS_GPIO_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPIO_EN, HIGH); - io.pinMode(EXPANDS_SD_PULLEN, INPUT); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + pinMode(KB_INT, INPUT_PULLUP); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); #elif defined(HACKADAY_COMMUNICATOR) - pinMode(KB_INT, INPUT); + pinMode(KB_INT, INPUT); #endif - concurrency::hasBeenSetup = true; + concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO - SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); + SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else - SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); + SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif - meshtastic_Config_DisplayConfig_OledType screen_model = - meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; - OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; + meshtastic_Config_DisplayConfig_OledType screen_model = + meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; + OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; #ifdef USE_SEGGER - auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; + auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; #ifdef NRF52840_XXAA - auto buflen = 4096; // this board has a fair amount of ram + auto buflen = 4096; // this board has a fair amount of ram #else - auto buflen = 256; // this board has a fair amount of ram + auto buflen = 256; // this board has a fair amount of ram #endif - SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode); + SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode); #endif #ifdef DEBUG_PORT - consoleInit(); // Set serial baud rate and init our mesh console + consoleInit(); // Set serial baud rate and init our mesh console #endif #ifdef UNPHONE - unphone.printStore(); + unphone.printStore(); #endif #if ARCH_PORTDUINO - RTCQuality ourQuality = RTCQualityDevice; + RTCQuality ourQuality = RTCQualityDevice; - std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); - if (timeCommandResult[0] == '1') { - ourQuality = RTCQualityNTP; - } + std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); + if (timeCommandResult[0] == '1') { + ourQuality = RTCQualityNTP; + } - struct timeval tv; - tv.tv_sec = time(NULL); - tv.tv_usec = 0; - perhapsSetRTC(ourQuality, &tv); + struct timeval tv; + tv.tv_sec = time(NULL); + tv.tv_usec = 0; + perhapsSetRTC(ourQuality, &tv); #endif - powerMonInit(); - serialSinceMsec = millis(); + powerMonInit(); + serialSinceMsec = millis(); - LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); + LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR - // use PSRAM for malloc calls > 256 bytes - heap_caps_malloc_extmem_enable(256); + // use PSRAM for malloc calls > 256 bytes + heap_caps_malloc_extmem_enable(256); #endif #endif #if defined(DEBUG_MUTE) && defined(DEBUG_PORT) - DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); - DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); - DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n"); + DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); + DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); + DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n"); #endif - initDeepSleep(); + initDeepSleep(); #if defined(MODEM_POWER_EN) - pinMode(MODEM_POWER_EN, OUTPUT); - digitalWrite(MODEM_POWER_EN, LOW); + pinMode(MODEM_POWER_EN, OUTPUT); + digitalWrite(MODEM_POWER_EN, LOW); #endif #if defined(MODEM_PWRKEY) - pinMode(MODEM_PWRKEY, OUTPUT); - digitalWrite(MODEM_PWRKEY, LOW); + pinMode(MODEM_PWRKEY, OUTPUT); + digitalWrite(MODEM_PWRKEY, LOW); #endif #if defined(LORA_TCXO_GPIO) - pinMode(LORA_TCXO_GPIO, OUTPUT); - digitalWrite(LORA_TCXO_GPIO, HIGH); + pinMode(LORA_TCXO_GPIO, OUTPUT); + digitalWrite(LORA_TCXO_GPIO, HIGH); #endif #if defined(VEXT_ENABLE) - pinMode(VEXT_ENABLE, OUTPUT); - digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power + pinMode(VEXT_ENABLE, OUTPUT); + digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power #endif #if defined(BIAS_T_ENABLE) - pinMode(BIAS_T_ENABLE, OUTPUT); - digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna + pinMode(BIAS_T_ENABLE, OUTPUT); + digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna #endif #if defined(VTFT_CTRL) - pinMode(VTFT_CTRL, OUTPUT); - digitalWrite(VTFT_CTRL, LOW); + pinMode(VTFT_CTRL, OUTPUT); + digitalWrite(VTFT_CTRL, LOW); #endif #ifdef RESET_OLED - pinMode(RESET_OLED, OUTPUT); - digitalWrite(RESET_OLED, 1); - delay(2); - digitalWrite(RESET_OLED, 0); - delay(10); - digitalWrite(RESET_OLED, 1); + pinMode(RESET_OLED, OUTPUT); + digitalWrite(RESET_OLED, 1); + delay(2); + digitalWrite(RESET_OLED, 0); + delay(10); + digitalWrite(RESET_OLED, 1); #endif #ifdef SENSOR_POWER_CTRL_PIN - pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); - digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); + pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); #endif #ifdef SENSOR_GPS_CONFLICT - bool sensor_detected = false; + bool sensor_detected = false; #endif #ifdef PERIPHERAL_WARMUP_MS - // Some peripherals may require additional time to stabilize after power is connected - // e.g. I2C on Heltec Vision Master - LOG_INFO("Wait for peripherals to stabilize"); - delay(PERIPHERAL_WARMUP_MS); + // Some peripherals may require additional time to stabilize after power is connected + // e.g. I2C on Heltec Vision Master + LOG_INFO("Wait for peripherals to stabilize"); + delay(PERIPHERAL_WARMUP_MS); #endif #ifdef BUTTON_PIN @@ -520,199 +511,200 @@ void setup() #if ESP_ARDUINO_VERSION_MAJOR >= 3 #ifdef BUTTON_NEED_PULLUP - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); #else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN #endif #else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN #ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - delay(10); + gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + delay(10); #endif #ifdef BUTTON_NEED_PULLUP2 - gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); - delay(10); + gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); + delay(10); #endif #endif #endif #endif - initSPI(); + initSPI(); - OSThread::setup(); + OSThread::setup(); #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - // The ThinkNodes have their own blink logic - // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); + // The ThinkNodes have their own blink logic + // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); #else - ledPeriodic = new Periodic("Blink", ledBlinker); + ledPeriodic = new Periodic("Blink", ledBlinker); #endif - fsInit(); + fsInit(); #if !MESHTASTIC_EXCLUDE_I2C #if defined(I2C_SDA1) && defined(ARCH_RP2040) - Wire1.setSDA(I2C_SDA1); - Wire1.setSCL(I2C_SCL1); - Wire1.begin(); + Wire1.setSDA(I2C_SDA1); + Wire1.setSCL(I2C_SCL1); + Wire1.begin(); #elif defined(I2C_SDA1) && !defined(ARCH_RP2040) - Wire1.begin(I2C_SDA1, I2C_SCL1); + Wire1.begin(I2C_SDA1, I2C_SCL1); #elif WIRE_INTERFACES_COUNT == 2 - Wire1.begin(); + Wire1.begin(); #endif #if defined(I2C_SDA) && defined(ARCH_RP2040) - Wire.setSDA(I2C_SDA); - Wire.setSCL(I2C_SCL); - Wire.begin(); + Wire.setSDA(I2C_SDA); + Wire.setSCL(I2C_SCL); + Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) - Wire.begin(I2C_SDA, I2C_SCL); + Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) - if (portduino_config.i2cdev != "") { - LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); - Wire.begin(portduino_config.i2cdev.c_str()); - } else { - LOG_INFO("No I2C device configured, Skip"); - } + if (portduino_config.i2cdev != "") { + LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); + Wire.begin(portduino_config.i2cdev.c_str()); + } else { + LOG_INFO("No I2C device configured, Skip"); + } #elif HAS_WIRE - Wire.begin(); + Wire.begin(); #endif #endif #if defined(M5STACK_UNITC6L) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, 1); - c6l_init(); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, 1); + c6l_init(); #endif #ifdef PIN_LCD_RESET - // FIXME - move this someplace better, LCD is at address 0x3F - pinMode(PIN_LCD_RESET, OUTPUT); - digitalWrite(PIN_LCD_RESET, 0); - delay(1); - digitalWrite(PIN_LCD_RESET, 1); - delay(1); + // FIXME - move this someplace better, LCD is at address 0x3F + pinMode(PIN_LCD_RESET, OUTPUT); + digitalWrite(PIN_LCD_RESET, 0); + delay(1); + digitalWrite(PIN_LCD_RESET, 1); + delay(1); #endif #ifdef AQ_SET_PIN - // RAK-12039 set pin for Air quality sensor. Detectable on I2C after ~3 seconds, so we need to rescan later - pinMode(AQ_SET_PIN, OUTPUT); - digitalWrite(AQ_SET_PIN, HIGH); + // RAK-12039 set pin for Air quality sensor. Detectable on I2C after ~3 seconds, so we need to rescan later + pinMode(AQ_SET_PIN, OUTPUT); + digitalWrite(AQ_SET_PIN, HIGH); #endif - // Currently only the tbeam has a PMU - // PMU initialization needs to be placed before i2c scanning - power = new Power(); - power->setStatusHandler(powerStatus); - powerStatus->observe(&power->newStatus); - power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial configuration + // Currently only the tbeam has a PMU + // PMU initialization needs to be placed before i2c scanning + power = new Power(); + power->setStatusHandler(powerStatus); + powerStatus->observe(&power->newStatus); + power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial + // configuration #if !MESHTASTIC_EXCLUDE_I2C - // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to - // accessories - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); + // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to + // accessories + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if HAS_WIRE - LOG_INFO("Scan for i2c devices"); + LOG_INFO("Scan for i2c devices"); #endif #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif #if defined(I2C_SDA) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #elif defined(ARCH_PORTDUINO) - if (portduino_config.i2cdev != "") { - LOG_INFO("Scan for i2c devices"); - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); - } -#elif HAS_WIRE + if (portduino_config.i2cdev != "") { + LOG_INFO("Scan for i2c devices"); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); + } +#elif HAS_WIRE + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif - auto i2cCount = i2cScanner->countDevices(); - if (i2cCount == 0) { - LOG_INFO("No I2C devices found"); - } else { - LOG_INFO("%i I2C devices found", i2cCount); + auto i2cCount = i2cScanner->countDevices(); + if (i2cCount == 0) { + LOG_INFO("No I2C devices found"); + } else { + LOG_INFO("%i I2C devices found", i2cCount); #ifdef SENSOR_GPS_CONFLICT - sensor_detected = true; + sensor_detected = true; #endif - } + } #ifdef ARCH_ESP32 - // Don't init display if we don't have one or we are waking headless due to a timer event - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { - LOG_DEBUG("suppress screen wake because this is a headless timer wakeup"); - i2cScanner->setSuppressScreen(); - } + // Don't init display if we don't have one or we are waking headless due to a timer event + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { + LOG_DEBUG("suppress screen wake because this is a headless timer wakeup"); + i2cScanner->setSuppressScreen(); + } #endif - auto screenInfo = i2cScanner->firstScreen(); - screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; + auto screenInfo = i2cScanner->firstScreen(); + screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) { - switch (screenInfo.type) { - case ScanI2C::DeviceType::SCREEN_SH1106: - screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SH1106; - break; - case ScanI2C::DeviceType::SCREEN_SSD1306: - screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306; - break; - case ScanI2C::DeviceType::SCREEN_ST7567: - case ScanI2C::DeviceType::SCREEN_UNKNOWN: - default: - screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; - } + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) { + switch (screenInfo.type) { + case ScanI2C::DeviceType::SCREEN_SH1106: + screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SH1106; + break; + case ScanI2C::DeviceType::SCREEN_SSD1306: + screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306; + break; + case ScanI2C::DeviceType::SCREEN_ST7567: + case ScanI2C::DeviceType::SCREEN_UNKNOWN: + default: + screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; } + } #define UPDATE_FROM_SCANNER(FIND_FN) #if defined(USE_VIRTUAL_KEYBOARD) - kb_found = true; + kb_found = true; #endif - auto rtc_info = i2cScanner->firstRTC(); - rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; + auto rtc_info = i2cScanner->firstRTC(); + rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; - auto kb_info = i2cScanner->firstKeyboard(); + auto kb_info = i2cScanner->firstKeyboard(); - if (kb_info.type != ScanI2C::DeviceType::NONE) { - kb_found = true; - cardkb_found = kb_info.address; - switch (kb_info.type) { - case ScanI2C::DeviceType::RAK14004: - kb_model = 0x02; - break; - case ScanI2C::DeviceType::CARDKB: - kb_model = 0x00; - break; - case ScanI2C::DeviceType::TDECKKB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x10; - break; - case ScanI2C::DeviceType::BBQ10KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x11; - break; - case ScanI2C::DeviceType::MPR121KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x37; - break; - case ScanI2C::DeviceType::TCA8418KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x84; - break; - default: - // use this as default since it's also just zero - LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); - kb_model = 0x00; - } + if (kb_info.type != ScanI2C::DeviceType::NONE) { + kb_found = true; + cardkb_found = kb_info.address; + switch (kb_info.type) { + case ScanI2C::DeviceType::RAK14004: + kb_model = 0x02; + break; + case ScanI2C::DeviceType::CARDKB: + kb_model = 0x00; + break; + case ScanI2C::DeviceType::TDECKKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x10; + break; + case ScanI2C::DeviceType::BBQ10KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x11; + break; + case ScanI2C::DeviceType::MPR121KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x37; + break; + case ScanI2C::DeviceType::TCA8418KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x84; + break; + default: + // use this as default since it's also just zero + LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); + kb_model = 0x00; } + } - pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); + pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); - auto aqiInfo = i2cScanner->firstAQI(); - aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE; + auto aqiInfo = i2cScanner->firstAQI(); + aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE; /* * There are a bunch of sensors that have no further logic than to be found and stuffed into the @@ -722,380 +714,379 @@ void setup() // Two supported RGB LED currently #ifdef HAS_RGB_LED - rgb_found = i2cScanner->firstRGBLED(); + rgb_found = i2cScanner->firstRGBLED(); #endif #ifdef HAS_TPS65233 - // TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher - // We are switching it off here since we don't use an LNB. - if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) { - Wire.beginTransmission(TPS65233_ADDR); - Wire.write(0); // Register 0 - Wire.write(128); // Turn off the LNB power, keep I2C Control enabled - Wire.endTransmission(); - Wire.beginTransmission(TPS65233_ADDR); - Wire.write(1); // Register 1 - Wire.write(0); // Turn off Tone Generator 22kHz - Wire.endTransmission(); - } + // TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher + // We are switching it off here since we don't use an LNB. + if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) { + Wire.beginTransmission(TPS65233_ADDR); + Wire.write(0); // Register 0 + Wire.write(128); // Turn off the LNB power, keep I2C Control enabled + Wire.endTransmission(); + Wire.beginTransmission(TPS65233_ADDR); + Wire.write(1); // Register 1 + Wire.write(0); // Turn off Tone Generator 22kHz + Wire.endTransmission(); + } #endif #if !defined(ARCH_STM32WL) - auto acc_info = i2cScanner->firstAccelerometer(); - accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; - LOG_DEBUG("acc_info = %i", acc_info.type); + auto acc_info = i2cScanner->firstAccelerometer(); + accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; + LOG_DEBUG("acc_info = %i", acc_info.type); #endif - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); #endif #ifdef HAS_SDCARD - setupSDCard(); + setupSDCard(); #endif - // LED init + // LED init #ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now #endif - // Hello - printInfo(); + // Hello + printInfo(); #ifdef BUILD_EPOCH - LOG_INFO("Build timestamp: %ld", BUILD_EPOCH); + LOG_INFO("Build timestamp: %ld", BUILD_EPOCH); #endif #ifdef ARCH_ESP32 - esp32Setup(); + esp32Setup(); #endif #ifdef ARCH_NRF52 - nrf52Setup(); + nrf52Setup(); #endif #ifdef ARCH_RP2040 - rp2040Setup(); + rp2040Setup(); #endif - // We do this as early as possible because this loads preferences from flash - // but we need to do this after main cpu init (esp32setup), because we need the random seed set - nodeDB = new NodeDB; + // We do this as early as possible because this loads preferences from flash + // but we need to do this after main cpu init (esp32setup), because we need the random seed set + nodeDB = new NodeDB; #if HAS_TFT - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - tftSetup(); - } + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + tftSetup(); + } #endif - router = new ReliableRouter(); + router = new ReliableRouter(); - // only play start melody when role is not tracker or sensor - if (config.power.is_power_saving == true && - IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR)) - LOG_DEBUG("Tracker/Sensor: Skip start melody"); - else - playStartMelody(); + // only play start melody when role is not tracker or sensor + if (config.power.is_power_saving == true && IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR)) + LOG_DEBUG("Tracker/Sensor: Skip start melody"); + else + playStartMelody(); - // fixed screen override? - if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) - screen_model = config.display.oled; + // fixed screen override? + if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) + screen_model = config.display.oled; #if defined(USE_SH1107) - screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 - screen_geometry = GEOMETRY_128_128; + screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 + screen_geometry = GEOMETRY_128_128; #endif #if defined(USE_SH1107_128_64) - screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 + screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 #endif #if !MESHTASTIC_EXCLUDE_I2C #if !defined(ARCH_STM32WL) - if (acc_info.type != ScanI2C::DeviceType::NONE) { - accelerometerThread = new AccelerometerThread(acc_info.type); - } + if (acc_info.type != ScanI2C::DeviceType::NONE) { + accelerometerThread = new AccelerometerThread(acc_info.type); + } #endif #if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED) - ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE); + ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE); #elif !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) - if (rgb_found.type != ScanI2C::DeviceType::NONE) { - ambientLightingThread = new AmbientLightingThread(rgb_found.type); - } + if (rgb_found.type != ScanI2C::DeviceType::NONE) { + ambientLightingThread = new AmbientLightingThread(rgb_found.type); + } #endif #endif #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - drv.begin(); - drv.selectLibrary(1); - // I2C trigger by sending 'go' command - drv.setMode(DRV2605_MODE_INTTRIG); + drv.begin(); + drv.selectLibrary(1); + // I2C trigger by sending 'go' command + drv.setMode(DRV2605_MODE_INTTRIG); #endif - // Init our SPI controller (must be before screen and lora) + // Init our SPI controller (must be before screen and lora) #ifdef ARCH_RP2040 #ifdef HW_SPI1_DEVICE - SPI1.setSCK(LORA_SCK); - SPI1.setTX(LORA_MOSI); - SPI1.setRX(LORA_MISO); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - SPI1.begin(false); + SPI1.setSCK(LORA_SCK); + SPI1.setTX(LORA_MOSI); + SPI1.setRX(LORA_MISO); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + SPI1.begin(false); #else // HW_SPI1_DEVICE - SPI.setSCK(LORA_SCK); - SPI.setTX(LORA_MOSI); - SPI.setRX(LORA_MISO); - SPI.begin(false); + SPI.setSCK(LORA_SCK); + SPI.setTX(LORA_MOSI); + SPI.setRX(LORA_MISO); + SPI.begin(false); #endif // HW_SPI1_DEVICE #elif ARCH_PORTDUINO - if (portduino_config.lora_spi_dev != "ch341") { - SPI.begin(); - } + if (portduino_config.lora_spi_dev != "ch341") { + SPI.begin(); + } #elif !defined(ARCH_ESP32) // ARCH_RP2040 #if defined(RAK3401) || defined(RAK13302) - pinMode(WB_IO2, OUTPUT); - digitalWrite(WB_IO2, HIGH); - SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); - SPI1.begin(); + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, HIGH); + SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); + SPI1.begin(); #else - SPI.begin(); + SPI.begin(); #endif #else - // ESP32 + // ESP32 #if defined(HW_SPI1_DEVICE) - SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - SPI1.setFrequency(4000000); + SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + SPI1.setFrequency(4000000); #else - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - LOG_DEBUG("SPI.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - SPI.setFrequency(4000000); + SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + LOG_DEBUG("SPI.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + SPI.setFrequency(4000000); #endif #endif - // Initialize the screen first so we can show the logo while we start up everything else. + // Initialize the screen first so we can show the logo while we start up everything else. #if HAS_SCREEN - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || defined(USE_ST7796) || \ + defined(HACKADAY_COMMUNICATOR) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && - config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); - } -#else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); -#endif + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); } +#else + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); +#endif + } #endif // HAS_SCREEN - // setup TZ prior to time actions. + // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ - LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string - if (*config.device.tzdef && config.device.tzdef[0] != 0) { - LOG_DEBUG("Saved TZ: %s ", config.device.tzdef); - setenv("TZ", config.device.tzdef, 1); + LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string + if (*config.device.tzdef && config.device.tzdef[0] != 0) { + LOG_DEBUG("Saved TZ: %s ", config.device.tzdef); + setenv("TZ", config.device.tzdef, 1); + } else { + if (strncmp((const char *)slipstreamTZString, "tzpl", 4) == 0) { + setenv("TZ", "GMT0", 1); } else { - if (strncmp((const char *)slipstreamTZString, "tzpl", 4) == 0) { - setenv("TZ", "GMT0", 1); - } else { - setenv("TZ", (const char *)slipstreamTZString, 1); - strcpy(config.device.tzdef, (const char *)slipstreamTZString); - } + setenv("TZ", (const char *)slipstreamTZString, 1); + strcpy(config.device.tzdef, (const char *)slipstreamTZString); } - tzset(); - LOG_DEBUG("Set Timezone to %s", getenv("TZ")); + } + tzset(); + LOG_DEBUG("Set Timezone to %s", getenv("TZ")); #endif - readFromRTC(); // read the main CPU RTC at first (in case we can't get GPS time) + readFromRTC(); // read the main CPU RTC at first (in case we can't get GPS time) #if !MESHTASTIC_EXCLUDE_GPS - // If we're taking on the repeater role, ignore GPS + // If we're taking on the repeater role, ignore GPS #ifdef SENSOR_GPS_CONFLICT - if (sensor_detected == false) { + if (sensor_detected == false) { #endif - if (HAS_GPS) { - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - gps = GPS::createGps(); - if (gps) { - gpsStatus->observe(&gps->newStatus); - } else { - LOG_DEBUG("Run without GPS"); - } - } + if (HAS_GPS) { + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + gps = GPS::createGps(); + if (gps) { + gpsStatus->observe(&gps->newStatus); + } else { + LOG_DEBUG("Run without GPS"); } -#ifdef SENSOR_GPS_CONFLICT + } } +#ifdef SENSOR_GPS_CONFLICT + } #endif #endif - nodeStatus->observe(&nodeDB->newStatus); + nodeStatus->observe(&nodeDB->newStatus); #ifdef HAS_I2S - LOG_DEBUG("Start audio thread"); - audioThread = new AudioThread(); + LOG_DEBUG("Start audio thread"); + audioThread = new AudioThread(); #endif #ifdef HAS_UDP_MULTICAST - LOG_DEBUG("Start multicast thread"); - udpHandler = new UdpMulticastHandler(); + LOG_DEBUG("Start multicast thread"); + udpHandler = new UdpMulticastHandler(); #ifdef ARCH_PORTDUINO - // FIXME: portduino does not ever call onNetworkConnected so call it here because I don't know what happen if I call - // onNetworkConnected there - if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->start(); - } + // FIXME: portduino does not ever call onNetworkConnected so call it here because I don't know what happen if I call + // onNetworkConnected there + if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->start(); + } #endif #endif - service = new MeshService(); - service->init(); + service = new MeshService(); + service->init(); - // Now that the mesh service is created, create any modules - setupModules(); + // Now that the mesh service is created, create any modules + setupModules(); #if !MESHTASTIC_EXCLUDE_I2C - // Inform modules about I2C devices - ScanI2CCompleted(i2cScanner.get()); - i2cScanner.reset(); + // Inform modules about I2C devices + ScanI2CCompleted(i2cScanner.get()); + i2cScanner.reset(); #endif #if !defined(MESHTASTIC_EXCLUDE_PKI) - // warn the user about a low entropy key - if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) { - LOG_WARN(LOW_ENTROPY_WARNING); - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, LOW_ENTROPY_WARNING); - service->sendClientNotification(cn); - nodeDB->hasWarned = true; - } + // warn the user about a low entropy key + if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) { + LOG_WARN(LOW_ENTROPY_WARNING); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, LOW_ENTROPY_WARNING); + service->sendClientNotification(cn); + nodeDB->hasWarned = true; + } #endif // buttons are now inputBroker, so have to come after setupModules #if HAS_BUTTON - int pullup_sense = 0; + int pullup_sense = 0; #ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did #ifdef BUTTON_SENSE_TYPE - pullup_sense = BUTTON_SENSE_TYPE; + pullup_sense = BUTTON_SENSE_TYPE; #else - pullup_sense = INPUT_PULLUP_SENSE; + pullup_sense = INPUT_PULLUP_SENSE; #endif #endif #if defined(ARCH_PORTDUINO) - if (portduino_config.userButtonPin.enabled) { + if (portduino_config.userButtonPin.enabled) { - LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig config; - config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; - config.activeLow = true; - config.activePullup = true; - config.pullupSense = INPUT_PULLUP; - config.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - config.singlePress = INPUT_BROKER_USER_PRESS; - config.longPress = INPUT_BROKER_SELECT; - UserButtonThread->initButton(config); - } + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); } + } #endif #ifdef BUTTON_PIN_TOUCH - TouchButtonThread = new ButtonThread("BackButton"); - ButtonConfig touchConfig; - touchConfig.pinNumber = BUTTON_PIN_TOUCH; - touchConfig.activeLow = true; - touchConfig.activePullup = true; - touchConfig.pullupSense = pullup_sense; - touchConfig.intRoutine = []() { - TouchButtonThread->userButton.tick(); - TouchButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - touchConfig.singlePress = INPUT_BROKER_NONE; - touchConfig.longPress = INPUT_BROKER_BACK; - TouchButtonThread->initButton(touchConfig); + TouchButtonThread = new ButtonThread("BackButton"); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + TouchButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; + TouchButtonThread->initButton(touchConfig); #endif #if defined(CANCEL_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - CancelButtonThread = new ButtonThread("CancelButton"); - ButtonConfig cancelConfig; - cancelConfig.pinNumber = CANCEL_BUTTON_PIN; - cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; - cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; - cancelConfig.pullupSense = pullup_sense; - cancelConfig.intRoutine = []() { - CancelButtonThread->userButton.tick(); - CancelButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - cancelConfig.singlePress = INPUT_BROKER_CANCEL; - cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; - cancelConfig.longPressTime = 4000; - CancelButtonThread->initButton(cancelConfig); + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + CancelButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); #endif #if defined(ALT_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - BackButtonThread = new ButtonThread("BackButton"); - ButtonConfig backConfig; - backConfig.pinNumber = ALT_BUTTON_PIN; - backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; - backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; - backConfig.pullupSense = pullup_sense; - backConfig.intRoutine = []() { - BackButtonThread->userButton.tick(); - BackButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - backConfig.singlePress = INPUT_BROKER_ALT_PRESS; - backConfig.longPress = INPUT_BROKER_ALT_LONG; - backConfig.longPressTime = 500; - BackButtonThread->initButton(backConfig); + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + BackButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); #endif #if defined(BUTTON_PIN) #if defined(USERPREFS_BUTTON_PIN) - int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; + int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; #else - int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; + int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; #endif #ifndef BUTTON_ACTIVE_LOW #define BUTTON_ACTIVE_LOW true @@ -1104,411 +1095,409 @@ void setup() #define BUTTON_ACTIVE_PULLUP true #endif - // Buttons. Moved here cause we need NodeDB to be initialized - // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig userConfig; - userConfig.pinNumber = (uint8_t)_pinNum; - userConfig.activeLow = BUTTON_ACTIVE_LOW; - userConfig.activePullup = BUTTON_ACTIVE_PULLUP; - userConfig.pullupSense = pullup_sense; - userConfig.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfig.singlePress = INPUT_BROKER_USER_PRESS; - userConfig.longPress = INPUT_BROKER_SELECT; - userConfig.longPressTime = 500; - userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; - UserButtonThread->initButton(userConfig); - } else { - ButtonConfig userConfigNoScreen; - userConfigNoScreen.pinNumber = (uint8_t)_pinNum; - userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; - userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; - userConfigNoScreen.pullupSense = pullup_sense; - userConfigNoScreen.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; - userConfigNoScreen.longPress = INPUT_BROKER_NONE; - userConfigNoScreen.longPressTime = 500; - userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; - userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; - userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; - UserButtonThread->initButton(userConfigNoScreen); - } + // Buttons. Moved here cause we need NodeDB to be initialized + // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_NONE; + userConfigNoScreen.longPressTime = 500; + userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } #endif #endif #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS - // After modules are setup, so we can observe modules - setupNicheGraphics(); + // After modules are setup, so we can observe modules + setupNicheGraphics(); #endif #ifdef LED_PIN - // Turn LED off after boot, if heartbeat by config - if (config.device.led_heartbeat_disabled) - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); + // Turn LED off after boot, if heartbeat by config + if (config.device.led_heartbeat_disabled) + digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); #endif // Do this after service.init (because that clears error_code) #ifdef HAS_PMU - if (!pmu_found) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_AXP192); // Record a hardware fault for missing hardware + if (!pmu_found) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_AXP192); // Record a hardware fault for missing hardware #endif #if !MESHTASTIC_EXCLUDE_I2C // Don't call screen setup until after nodedb is setup (because we need // the current region name) -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(HACKADAY_COMMUNICATOR) - if (screen) - screen->setup(); +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_ST7796) || defined(USE_SPISSD1306) || \ + defined(HACKADAY_COMMUNICATOR) + if (screen) + screen->setup(); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && - config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - screen->setup(); - } + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen->setup(); + } #else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) - screen->setup(); + if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) + screen->setup(); #endif #endif #ifdef ARCH_PORTDUINO - // as one can't use a function pointer to the class constructor: - auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) { - switch (portduino_config.lora_module) { - case use_rf95: - return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); - case use_sx1262: - return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); - case use_sx1268: - return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); - case use_sx1280: - return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); - case use_lr1110: - return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); - case use_lr1120: - return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); - case use_lr1121: - return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); - case use_llcc68: - return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); - case use_simradio: - return (RadioInterface *)new SimRadio; - default: - assert(0); // shouldn't happen - return (RadioInterface *)nullptr; - } - }; - - LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), - portduino_config.lora_spi_dev.c_str()); - if (portduino_config.lora_spi_dev == "ch341") { - RadioLibHAL = ch341Hal; - } else { - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + // as one can't use a function pointer to the class constructor: + auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) { + switch (portduino_config.lora_module) { + case use_rf95: + return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); + case use_sx1262: + return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); + case use_sx1268: + return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); + case use_sx1280: + return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); + case use_lr1110: + return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); + case use_lr1120: + return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); + case use_lr1121: + return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); + case use_llcc68: + return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + case use_simradio: + return (RadioInterface *)new SimRadio; + default: + assert(0); // shouldn't happen + return (RadioInterface *)nullptr; } - rIf = - loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, + }; + + LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), + portduino_config.lora_spi_dev.c_str()); + if (portduino_config.lora_spi_dev == "ch341") { + RadioLibHAL = ch341Hal; + } else { + RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + } + rIf = loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); - if (!rIf->init()) { - LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); - delete rIf; - rIf = NULL; - exit(EXIT_FAILURE); - } else { - LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); - } + if (!rIf->init()) { + LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); + } #elif defined(HW_SPI1_DEVICE) - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); #else // HW_SPI1_DEVICE - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); #endif - // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) + // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) #if defined(USE_STM32WLx) - if (!rIf) { - rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No STM32WL radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("STM32WL init success"); - radioType = STM32WLx_RADIO; - } + if (!rIf) { + rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No STM32WL radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("STM32WL init success"); + radioType = STM32WLx_RADIO; } + } #endif #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); - if (!rIf->init()) { - LOG_WARN("No RF95 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("RF95 init success"); - radioType = RF95_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); + if (!rIf->init()) { + LOG_WARN("No RF95 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("RF95 init success"); + radioType = RF95_RADIO; } + } #endif #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); #ifdef SX126X_DIO3_TCXO_VOLTAGE - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); #endif - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio"); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success"); - rIf = sxIf; - radioType = SX1262_RADIO; - } + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio"); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success"); + rIf = sxIf; + radioType = SX1262_RADIO; } + } #endif #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1262_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1262_RADIO; } + } - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); - radioType = SX1262_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); + radioType = SX1262_RADIO; } + } #endif #if defined(USE_SX1268) #if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1268_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1268_RADIO; } + } #endif - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1268 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success"); - radioType = SX1268_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1268 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success"); + radioType = SX1268_RADIO; } + } #endif #if defined(USE_LLCC68) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No LLCC68 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LLCC68 init success"); - radioType = LLCC68_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No LLCC68 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LLCC68 init success"); + radioType = LLCC68_RADIO; } + } #endif #if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1110 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1110 init success"); - radioType = LR1110_RADIO; - } + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1110 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1110 init success"); + radioType = LR1110_RADIO; } + } #endif #if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1120 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1120 init success"); - radioType = LR1120_RADIO; - } + if (!rIf) { + rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1120 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1120 init success"); + radioType = LR1120_RADIO; } + } #endif #if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1121 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1121 init success"); - radioType = LR1121_RADIO; - } + if (!rIf) { + rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1121 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1121 init success"); + radioType = LR1121_RADIO; } + } #endif #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 - if (!rIf) { - rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1280 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1280 init success"); - radioType = SX1280_RADIO; - } + if (!rIf) { + rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1280 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1280 init success"); + radioType = SX1280_RADIO; } + } #endif - // check if the radio chip matches the selected region - if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { - LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; - nodeDB->saveToDisk(SEGMENT_CONFIG); + // check if the radio chip matches the selected region + if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { + LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + nodeDB->saveToDisk(SEGMENT_CONFIG); - if (!rIf->reconfigure()) { - LOG_WARN("Reconfigure failed, rebooting"); - if (screen) { - screen->showSimpleBanner("Rebooting..."); - } - rebootAtMsec = millis() + 5000; - } + if (!rIf->reconfigure()) { + LOG_WARN("Reconfigure failed, rebooting"); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } + rebootAtMsec = millis() + 5000; } + } - lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) + lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) #if !MESHTASTIC_EXCLUDE_MQTT - mqttInit(); + mqttInit(); #endif #ifdef RF95_FAN_EN - // Ability to disable FAN if PIN has been set with RF95_FAN_EN. - // Make sure LoRa has been started before disabling FAN. - if (config.lora.pa_fan_disabled) - digitalWrite(RF95_FAN_EN, LOW ^ 0); + // Ability to disable FAN if PIN has been set with RF95_FAN_EN. + // Make sure LoRa has been started before disabling FAN. + if (config.lora.pa_fan_disabled) + digitalWrite(RF95_FAN_EN, LOW ^ 0); #endif #ifndef ARCH_PORTDUINO - // Initialize Wifi + // Initialize Wifi #if HAS_WIFI - initWifi(); + initWifi(); #endif #if HAS_ETHERNET - // Initialize Ethernet - initEthernet(); + // Initialize Ethernet + initEthernet(); #endif #endif #if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - // Start web server thread. - webServerThread = new WebServerThread(); + // Start web server thread. + webServerThread = new WebServerThread(); #endif #ifdef ARCH_PORTDUINO #if __has_include() - if (portduino_config.webserverport != -1) { - piwebServerThread = new PiWebServerThread(); - std::atexit([] { delete piwebServerThread; }); - } + if (portduino_config.webserverport != -1) { + piwebServerThread = new PiWebServerThread(); + std::atexit([] { delete piwebServerThread; }); + } #endif - initApiServer(TCPPort); + initApiServer(TCPPort); #endif - // Start airtime logger thread. - airTime = new AirTime(); + // Start airtime logger thread. + airTime = new AirTime(); - if (!rIf) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); - else { - router->addInterface(rIf); + if (!rIf) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); + else { + router->addInterface(rIf); - // Log bit rate to debug output - LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) / - (float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) * - 1000); - } + // Log bit rate to debug output + LOG_DEBUG("LoRA bitrate = %f bytes / sec", + (float(meshtastic_Constants_DATA_PAYLOAD_LEN) / (float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) * 1000); + } - // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values - PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS - powerFSMthread = new PowerFSMThread(); + // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values + PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking + // from SDS + powerFSMthread = new PowerFSMThread(); #if !HAS_TFT - setCPUFast(false); // 80MHz is fine for our slow peripherals + setCPUFast(false); // 80MHz is fine for our slow peripherals #endif #ifdef ARDUINO_ARCH_ESP32 - LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); - LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); + LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); + LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); #endif - // We manually run this to update the NodeStatus - nodeDB->notifyObservers(true); + // We manually run this to update the NodeStatus + nodeDB->notifyObservers(true); } #endif @@ -1519,122 +1508,118 @@ uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to s // This will suppress the current delay and instead try to run ASAP. bool runASAP; -extern meshtastic_DeviceMetadata getDeviceMetadata() -{ - meshtastic_DeviceMetadata deviceMetadata; - strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version)); - deviceMetadata.device_state_version = DEVICESTATE_CUR_VER; - deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN; - deviceMetadata.hasBluetooth = HAS_BLUETOOTH; - deviceMetadata.hasWifi = HAS_WIFI; - deviceMetadata.hasEthernet = HAS_ETHERNET; - deviceMetadata.role = config.device.role; - deviceMetadata.position_flags = config.position.position_flags; - deviceMetadata.hw_model = HW_VENDOR; - deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; - deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; +extern meshtastic_DeviceMetadata getDeviceMetadata() { + meshtastic_DeviceMetadata deviceMetadata; + strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version)); + deviceMetadata.device_state_version = DEVICESTATE_CUR_VER; + deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN; + deviceMetadata.hasBluetooth = HAS_BLUETOOTH; + deviceMetadata.hasWifi = HAS_WIFI; + deviceMetadata.hasEthernet = HAS_ETHERNET; + deviceMetadata.role = config.device.role; + deviceMetadata.position_flags = config.position.position_flags; + deviceMetadata.hw_model = HW_VENDOR; + deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; + deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; #if MESHTASTIC_EXCLUDE_REMOTEHARDWARE - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; #endif #if MESHTASTIC_EXCLUDE_AUDIO - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; #endif // Option to explicitly include canned messages for edge cases, e.g. niche graphics #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif #if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts // We'll have to macro guard against those targets potentially #if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG; #endif // If we don't have any GPIO and we don't have GPS OR we don't want too - no purpose in having serial config #if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; #endif #ifndef ARCH_ESP32 - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; #endif #if !defined(HAS_RGB_LED) && !RAK_4631 - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; #endif // No bluetooth on these targets (yet): // Pico W / 2W may get it at some point // Portduino and ESP32-C6 are excluded because we don't have a working bluetooth stacks integrated yet. #if defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) || defined(CONFIG_IDF_TARGET_ESP32C6) - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG; #endif #if defined(ARCH_NRF52) && !HAS_ETHERNET // nrf52 doesn't have network unless it's a RAK ethernet gateway currently - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on nRF52 + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on nRF52 #elif defined(ARCH_RP2040) && !HAS_WIFI && !HAS_ETHERNET - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on RP2040 + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on RP2040 #endif #if !(MESHTASTIC_EXCLUDE_PKI) - deviceMetadata.hasPKC = true; + deviceMetadata.hasPKC = true; #endif - return deviceMetadata; + return deviceMetadata; } #if !MESHTASTIC_EXCLUDE_I2C void scannerToSensorsMap(const std::unique_ptr &i2cScanner, ScanI2C::DeviceType deviceType, - meshtastic_TelemetrySensorType sensorType) -{ - auto found = i2cScanner->find(deviceType); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[sensorType].first = found.address.address; - nodeTelemetrySensorsMap[sensorType].second = i2cScanner->fetchI2CBus(found.address); - } + meshtastic_TelemetrySensorType sensorType) { + auto found = i2cScanner->find(deviceType); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[sensorType].first = found.address.address; + nodeTelemetrySensorsMap[sensorType].second = i2cScanner->fetchI2CBus(found.address); + } } #endif #ifndef PIO_UNIT_TESTING -void loop() -{ - runASAP = false; +void loop() { + runASAP = false; #ifdef ARCH_ESP32 - esp32Loop(); + esp32Loop(); #endif #ifdef ARCH_NRF52 - nrf52Loop(); + nrf52Loop(); #endif - power->powerCommandsCheck(); + power->powerCommandsCheck(); #ifdef DEBUG_STACK - static uint32_t lastPrint = 0; - if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { - lastPrint = millis(); - meshtastic::printThreadInfo("main"); - } + static uint32_t lastPrint = 0; + if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { + lastPrint = millis(); + meshtastic::printThreadInfo("main"); + } #endif - service->loop(); + service->loop(); #if !MESHTASTIC_EXCLUDE_INPUTBROKER && defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - if (inputBroker) - inputBroker->processInputEventQueue(); + if (inputBroker) + inputBroker->processInputEventQueue(); #endif #if ARCH_PORTDUINO && HAS_TFT - if (screen && portduino_config.displayPanel == x11 && - config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - auto dispdev = screen->getDisplayDevice(); - if (dispdev) - static_cast(dispdev)->sdlLoop(); - } + if (screen && portduino_config.displayPanel == x11 && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + auto dispdev = screen->getDisplayDevice(); + if (dispdev) + static_cast(dispdev)->sdlLoop(); + } #endif - long delayMsec = mainController.runOrDelay(); + long delayMsec = mainController.runOrDelay(); - // We want to sleep as long as possible here - because it saves power - if (!runASAP && loopCanSleep()) { + // We want to sleep as long as possible here - because it saves power + if (!runASAP && loopCanSleep()) { #ifdef DEBUG_LOOP_TIMING - LOG_DEBUG("main loop delay: %d", delayMsec); + LOG_DEBUG("main loop delay: %d", delayMsec); #endif - mainDelay.delay(delayMsec); - } + mainDelay.delay(delayMsec); + } } #endif diff --git a/src/memGet.cpp b/src/memGet.cpp index 14e614014..72f40d470 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -20,20 +20,19 @@ MemGet memGet; * Returns the amount of free heap memory in bytes. * @return uint32_t The amount of free heap memory in bytes. */ -uint32_t MemGet::getFreeHeap() -{ +uint32_t MemGet::getFreeHeap() { #ifdef ARCH_ESP32 - return ESP.getFreeHeap(); + return ESP.getFreeHeap(); #elif defined(ARCH_NRF52) - return dbgHeapFree(); + return dbgHeapFree(); #elif defined(ARCH_RP2040) - return rp2040.getFreeHeap(); + return rp2040.getFreeHeap(); #elif defined(ARCH_STM32WL) - struct mallinfo m = mallinfo(); - return m.fordblks; // Total free space (bytes) + struct mallinfo m = mallinfo(); + return m.fordblks; // Total free space (bytes) #else - // this platform does not have heap management function implemented - return UINT32_MAX; + // this platform does not have heap management function implemented + return UINT32_MAX; #endif } @@ -41,20 +40,19 @@ uint32_t MemGet::getFreeHeap() * Returns the size of the heap memory in bytes. * @return uint32_t The size of the heap memory in bytes. */ -uint32_t MemGet::getHeapSize() -{ +uint32_t MemGet::getHeapSize() { #ifdef ARCH_ESP32 - return ESP.getHeapSize(); + return ESP.getHeapSize(); #elif defined(ARCH_NRF52) - return dbgHeapTotal(); + return dbgHeapTotal(); #elif defined(ARCH_RP2040) - return rp2040.getTotalHeap(); + return rp2040.getTotalHeap(); #elif defined(ARCH_STM32WL) - struct mallinfo m = mallinfo(); - return m.arena; // Non-mmapped space allocated (bytes) + struct mallinfo m = mallinfo(); + return m.arena; // Non-mmapped space allocated (bytes) #else - // this platform does not have heap management function implemented - return UINT32_MAX; + // this platform does not have heap management function implemented + return UINT32_MAX; #endif } @@ -63,14 +61,13 @@ uint32_t MemGet::getHeapSize() * * @return The amount of free psram memory in bytes. */ -uint32_t MemGet::getFreePsram() -{ +uint32_t MemGet::getFreePsram() { #ifdef ARCH_ESP32 - return ESP.getFreePsram(); + return ESP.getFreePsram(); #elif defined(ARCH_PORTDUINO) - return 4194252; + return 4194252; #else - return 0; + return 0; #endif } @@ -79,25 +76,23 @@ uint32_t MemGet::getFreePsram() * * @return uint32_t The size of the PSRAM memory. */ -uint32_t MemGet::getPsramSize() -{ +uint32_t MemGet::getPsramSize() { #ifdef ARCH_ESP32 - return ESP.getPsramSize(); + return ESP.getPsramSize(); #elif defined(ARCH_PORTDUINO) - return 4194252; + return 4194252; #else - return 0; + return 0; #endif } -void displayPercentHeapFree() -{ - uint32_t freeHeap = memGet.getFreeHeap(); - uint32_t totalHeap = memGet.getHeapSize(); - if (totalHeap == 0 || totalHeap == UINT32_MAX) { - LOG_INFO("Heap size unavailable"); - return; - } - int percent = (int)((freeHeap * 100) / totalHeap); - LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); +void displayPercentHeapFree() { + uint32_t freeHeap = memGet.getFreeHeap(); + uint32_t totalHeap = memGet.getHeapSize(); + if (totalHeap == 0 || totalHeap == UINT32_MAX) { + LOG_INFO("Heap size unavailable"); + return; + } + int percent = (int)((freeHeap * 100) / totalHeap); + LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); } \ No newline at end of file diff --git a/src/memGet.h b/src/memGet.h index 130d2ca7e..608f52c5a 100644 --- a/src/memGet.h +++ b/src/memGet.h @@ -4,13 +4,12 @@ #include -class MemGet -{ - public: - uint32_t getFreeHeap(); - uint32_t getHeapSize(); - uint32_t getFreePsram(); - uint32_t getPsramSize(); +class MemGet { +public: + uint32_t getFreeHeap(); + uint32_t getHeapSize(); + uint32_t getFreePsram(); + uint32_t getPsramSize(); }; extern MemGet memGet; diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4dcd94e3b..0834279ca 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -22,386 +22,369 @@ const char *Channels::serialChannel = "serial"; const char *Channels::mqttChannel = "mqtt"; #endif -uint8_t xorHash(const uint8_t *p, size_t len) -{ - uint8_t code = 0; - for (size_t i = 0; i < len; i++) - code ^= p[i]; - return code; +uint8_t xorHash(const uint8_t *p, size_t len) { + uint8_t code = 0; + for (size_t i = 0; i < len; i++) + code ^= p[i]; + return code; } /** Given a channel number, return the (0 to 255) hash for that channel. * The hash is just an xor of the channel name followed by the channel PSK being used for encryption * If no suitable channel could be found, return -1 */ -int16_t Channels::generateHash(ChannelIndex channelNum) -{ - auto k = getKey(channelNum); - if (k.length < 0) - return -1; // invalid - else { - const char *name = getName(channelNum); - uint8_t h = xorHash((const uint8_t *)name, strlen(name)); +int16_t Channels::generateHash(ChannelIndex channelNum) { + auto k = getKey(channelNum); + if (k.length < 0) + return -1; // invalid + else { + const char *name = getName(channelNum); + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); - h ^= xorHash(k.bytes, k.length); + h ^= xorHash(k.bytes, k.length); - return h; - } + return h; + } } /** * Validate a channel, fixing any errors as needed */ -meshtastic_Channel &Channels::fixupChannel(ChannelIndex chIndex) -{ - meshtastic_Channel &ch = getByIndex(chIndex); +meshtastic_Channel &Channels::fixupChannel(ChannelIndex chIndex) { + meshtastic_Channel &ch = getByIndex(chIndex); - ch.index = chIndex; // Preinit the index so it be ready to share with the phone (we'll never change it later) + ch.index = chIndex; // Preinit the index so it be ready to share with the phone (we'll never change it later) - if (!ch.has_settings) { - // No settings! Must disable and skip - ch.role = meshtastic_Channel_Role_DISABLED; - memset(&ch.settings, 0, sizeof(ch.settings)); - ch.has_settings = true; - } else { - meshtastic_ChannelSettings &meshtastic_channelSettings = ch.settings; + if (!ch.has_settings) { + // No settings! Must disable and skip + ch.role = meshtastic_Channel_Role_DISABLED; + memset(&ch.settings, 0, sizeof(ch.settings)); + ch.has_settings = true; + } else { + meshtastic_ChannelSettings &meshtastic_channelSettings = ch.settings; - // Convert the old string "Default" to our new short representation - if (strcmp(meshtastic_channelSettings.name, "Default") == 0) - *meshtastic_channelSettings.name = '\0'; - } + // Convert the old string "Default" to our new short representation + if (strcmp(meshtastic_channelSettings.name, "Default") == 0) + *meshtastic_channelSettings.name = '\0'; + } - hashes[chIndex] = generateHash(chIndex); + hashes[chIndex] = generateHash(chIndex); - return ch; + return ch; } -void Channels::initDefaultLoraConfig() -{ - meshtastic_Config_LoRaConfig &loraConfig = config.lora; +void Channels::initDefaultLoraConfig() { + meshtastic_Config_LoRaConfig &loraConfig = config.lora; - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; // Default to Long Range & Fast - loraConfig.use_preset = true; - loraConfig.tx_power = 0; // default - loraConfig.channel_num = 0; + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; // Default to Long Range & Fast + loraConfig.use_preset = true; + loraConfig.tx_power = 0; // default + loraConfig.channel_num = 0; #ifdef USERPREFS_LORACONFIG_MODEM_PRESET - loraConfig.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; + loraConfig.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; #endif #ifdef USERPREFS_LORACONFIG_CHANNEL_NUM - loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM; + loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM; #endif } -bool Channels::ensureLicensedOperation() -{ - if (!owner.is_licensed) { - return false; +bool Channels::ensureLicensedOperation() { + if (!owner.is_licensed) { + return false; + } + bool hasEncryptionOrAdmin = false; + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + auto channel = channels.getByIndex(i); + if (!channel.has_settings) { + continue; } - bool hasEncryptionOrAdmin = false; - for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { - auto channel = channels.getByIndex(i); - if (!channel.has_settings) { - continue; - } - auto &channelSettings = channel.settings; - if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) { - channel.role = meshtastic_Channel_Role_DISABLED; - channelSettings.psk.bytes[0] = 0; - channelSettings.psk.size = 0; - hasEncryptionOrAdmin = true; - channels.setChannel(channel); + auto &channelSettings = channel.settings; + if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) { + channel.role = meshtastic_Channel_Role_DISABLED; + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + hasEncryptionOrAdmin = true; + channels.setChannel(channel); - } else if (channelSettings.psk.size > 0) { - channelSettings.psk.bytes[0] = 0; - channelSettings.psk.size = 0; - hasEncryptionOrAdmin = true; - channels.setChannel(channel); - } + } else if (channelSettings.psk.size > 0) { + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + hasEncryptionOrAdmin = true; + channels.setChannel(channel); } - return hasEncryptionOrAdmin; + } + return hasEncryptionOrAdmin; } /** * Write a default channel to the specified channel index */ -void Channels::initDefaultChannel(ChannelIndex chIndex) -{ - meshtastic_Channel &ch = getByIndex(chIndex); - meshtastic_ChannelSettings &channelSettings = ch.settings; +void Channels::initDefaultChannel(ChannelIndex chIndex) { + meshtastic_Channel &ch = getByIndex(chIndex); + meshtastic_ChannelSettings &channelSettings = ch.settings; - uint8_t defaultpskIndex = 1; - channelSettings.psk.bytes[0] = defaultpskIndex; - channelSettings.psk.size = 1; - strncpy(channelSettings.name, "", sizeof(channelSettings.name)); - channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel - channelSettings.has_module_settings = true; + uint8_t defaultpskIndex = 1; + channelSettings.psk.bytes[0] = defaultpskIndex; + channelSettings.psk.size = 1; + strncpy(channelSettings.name, "", sizeof(channelSettings.name)); + channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel + channelSettings.has_module_settings = true; - ch.has_settings = true; - ch.role = chIndex == 0 ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; + ch.has_settings = true; + ch.role = chIndex == 0 ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; - switch (chIndex) { - case 0: + switch (chIndex) { + case 0: #ifdef USERPREFS_CHANNEL_0_PSK - static const uint8_t defaultpsk0[] = USERPREFS_CHANNEL_0_PSK; - memcpy(channelSettings.psk.bytes, defaultpsk0, sizeof(defaultpsk0)); - channelSettings.psk.size = sizeof(defaultpsk0); + static const uint8_t defaultpsk0[] = USERPREFS_CHANNEL_0_PSK; + memcpy(channelSettings.psk.bytes, defaultpsk0, sizeof(defaultpsk0)); + channelSettings.psk.size = sizeof(defaultpsk0); #endif #ifdef USERPREFS_CHANNEL_0_NAME - strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_0_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_0_NAME); #endif #ifdef USERPREFS_CHANNEL_0_PRECISION - channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_0_PRECISION; + channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_0_PRECISION; #endif #ifdef USERPREFS_CHANNEL_0_UPLINK_ENABLED - channelSettings.uplink_enabled = USERPREFS_CHANNEL_0_UPLINK_ENABLED; + channelSettings.uplink_enabled = USERPREFS_CHANNEL_0_UPLINK_ENABLED; #endif #ifdef USERPREFS_CHANNEL_0_DOWNLINK_ENABLED - channelSettings.downlink_enabled = USERPREFS_CHANNEL_0_DOWNLINK_ENABLED; + channelSettings.downlink_enabled = USERPREFS_CHANNEL_0_DOWNLINK_ENABLED; #endif - break; - case 1: + break; + case 1: #ifdef USERPREFS_CHANNEL_1_PSK - static const uint8_t defaultpsk1[] = USERPREFS_CHANNEL_1_PSK; - memcpy(channelSettings.psk.bytes, defaultpsk1, sizeof(defaultpsk1)); - channelSettings.psk.size = sizeof(defaultpsk1); + static const uint8_t defaultpsk1[] = USERPREFS_CHANNEL_1_PSK; + memcpy(channelSettings.psk.bytes, defaultpsk1, sizeof(defaultpsk1)); + channelSettings.psk.size = sizeof(defaultpsk1); #endif #ifdef USERPREFS_CHANNEL_1_NAME - strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_1_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_1_NAME); #endif #ifdef USERPREFS_CHANNEL_1_PRECISION - channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_1_PRECISION; + channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_1_PRECISION; #endif #ifdef USERPREFS_CHANNEL_1_UPLINK_ENABLED - channelSettings.uplink_enabled = USERPREFS_CHANNEL_1_UPLINK_ENABLED; + channelSettings.uplink_enabled = USERPREFS_CHANNEL_1_UPLINK_ENABLED; #endif #ifdef USERPREFS_CHANNEL_1_DOWNLINK_ENABLED - channelSettings.downlink_enabled = USERPREFS_CHANNEL_1_DOWNLINK_ENABLED; + channelSettings.downlink_enabled = USERPREFS_CHANNEL_1_DOWNLINK_ENABLED; #endif - break; - case 2: + break; + case 2: #ifdef USERPREFS_CHANNEL_2_PSK - static const uint8_t defaultpsk2[] = USERPREFS_CHANNEL_2_PSK; - memcpy(channelSettings.psk.bytes, defaultpsk2, sizeof(defaultpsk2)); - channelSettings.psk.size = sizeof(defaultpsk2); + static const uint8_t defaultpsk2[] = USERPREFS_CHANNEL_2_PSK; + memcpy(channelSettings.psk.bytes, defaultpsk2, sizeof(defaultpsk2)); + channelSettings.psk.size = sizeof(defaultpsk2); #endif #ifdef USERPREFS_CHANNEL_2_NAME - strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_2_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_2_NAME); #endif #ifdef USERPREFS_CHANNEL_2_PRECISION - channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_2_PRECISION; + channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_2_PRECISION; #endif #ifdef USERPREFS_CHANNEL_2_UPLINK_ENABLED - channelSettings.uplink_enabled = USERPREFS_CHANNEL_2_UPLINK_ENABLED; + channelSettings.uplink_enabled = USERPREFS_CHANNEL_2_UPLINK_ENABLED; #endif #ifdef USERPREFS_CHANNEL_2_DOWNLINK_ENABLED - channelSettings.downlink_enabled = USERPREFS_CHANNEL_2_DOWNLINK_ENABLED; + channelSettings.downlink_enabled = USERPREFS_CHANNEL_2_DOWNLINK_ENABLED; #endif - break; - default: - break; - } + break; + default: + break; + } } -CryptoKey Channels::getKey(ChannelIndex chIndex) -{ - meshtastic_Channel &ch = getByIndex(chIndex); - const meshtastic_ChannelSettings &channelSettings = ch.settings; +CryptoKey Channels::getKey(ChannelIndex chIndex) { + meshtastic_Channel &ch = getByIndex(chIndex); + const meshtastic_ChannelSettings &channelSettings = ch.settings; - CryptoKey k; - memset(k.bytes, 0, sizeof(k.bytes)); // In case the user provided a short key, we want to pad the rest with zeros + CryptoKey k; + memset(k.bytes, 0, sizeof(k.bytes)); // In case the user provided a short key, we want to pad the rest with zeros - if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED) { - k.length = -1; // invalid - } else { - memcpy(k.bytes, channelSettings.psk.bytes, channelSettings.psk.size); - k.length = channelSettings.psk.size; - if (k.length == 0) { - if (ch.role == meshtastic_Channel_Role_SECONDARY) { - LOG_DEBUG("Unset PSK for secondary channel %s. use primary key", ch.settings.name); - k = getKey(primaryIndex); - } else { - LOG_WARN("User disabled encryption"); - } - } else if (k.length == 1) { - // Convert the short single byte variants of psk into variant that can be used more generally + if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED) { + k.length = -1; // invalid + } else { + memcpy(k.bytes, channelSettings.psk.bytes, channelSettings.psk.size); + k.length = channelSettings.psk.size; + if (k.length == 0) { + if (ch.role == meshtastic_Channel_Role_SECONDARY) { + LOG_DEBUG("Unset PSK for secondary channel %s. use primary key", ch.settings.name); + k = getKey(primaryIndex); + } else { + LOG_WARN("User disabled encryption"); + } + } else if (k.length == 1) { + // Convert the short single byte variants of psk into variant that can be used more generally - uint8_t pskIndex = k.bytes[0]; - LOG_DEBUG("Expand short PSK #%d", pskIndex); - if (pskIndex == 0) - k.length = 0; // Turn off encryption - else { - memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); - k.length = sizeof(defaultpsk); - // Bump up the last byte of PSK as needed - uint8_t *last = k.bytes + sizeof(defaultpsk) - 1; - *last = *last + pskIndex - 1; // index of 1 means no change vs defaultPSK - } - } else if (k.length < 16) { - // Error! The user specified only the first few bits of an AES128 key. So by convention we just pad the rest of the - // key with zeros - LOG_WARN("User provided a too short AES128 key - padding"); - k.length = 16; - } else if (k.length < 32 && k.length != 16) { - // Error! The user specified only the first few bits of an AES256 key. So by convention we just pad the rest of the - // key with zeros - LOG_WARN("User provided a too short AES256 key - padding"); - k.length = 32; - } + uint8_t pskIndex = k.bytes[0]; + LOG_DEBUG("Expand short PSK #%d", pskIndex); + if (pskIndex == 0) + k.length = 0; // Turn off encryption + else { + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + // Bump up the last byte of PSK as needed + uint8_t *last = k.bytes + sizeof(defaultpsk) - 1; + *last = *last + pskIndex - 1; // index of 1 means no change vs defaultPSK + } + } else if (k.length < 16) { + // Error! The user specified only the first few bits of an AES128 key. So by convention we just pad the rest of + // the key with zeros + LOG_WARN("User provided a too short AES128 key - padding"); + k.length = 16; + } else if (k.length < 32 && k.length != 16) { + // Error! The user specified only the first few bits of an AES256 key. So by convention we just pad the rest of + // the key with zeros + LOG_WARN("User provided a too short AES256 key - padding"); + k.length = 32; } + } - return k; + return k; } /** Given a channel index, change to use the crypto key specified by that index */ -int16_t Channels::setCrypto(ChannelIndex chIndex) -{ - CryptoKey k = getKey(chIndex); +int16_t Channels::setCrypto(ChannelIndex chIndex) { + CryptoKey k = getKey(chIndex); - if (k.length < 0) - return -1; - else { - // Tell our crypto engine about the psk - crypto->setKey(k); - return getHash(chIndex); - } + if (k.length < 0) + return -1; + else { + // Tell our crypto engine about the psk + crypto->setKey(k); + return getHash(chIndex); + } } -void Channels::initDefaults() -{ - channelFile.channels_count = MAX_NUM_CHANNELS; - for (int i = 0; i < channelFile.channels_count; i++) - fixupChannel(i); - initDefaultLoraConfig(); +void Channels::initDefaults() { + channelFile.channels_count = MAX_NUM_CHANNELS; + for (int i = 0; i < channelFile.channels_count; i++) + fixupChannel(i); + initDefaultLoraConfig(); #ifdef USERPREFS_CHANNELS_TO_WRITE - for (int i = 0; i < USERPREFS_CHANNELS_TO_WRITE; i++) { - initDefaultChannel(i); - } + for (int i = 0; i < USERPREFS_CHANNELS_TO_WRITE; i++) { + initDefaultChannel(i); + } #else - initDefaultChannel(0); + initDefaultChannel(0); #endif } -void Channels::onConfigChanged() -{ - // Make sure the phone hasn't mucked anything up - for (int i = 0; i < channelFile.channels_count; i++) { - const meshtastic_Channel &ch = fixupChannel(i); +void Channels::onConfigChanged() { + // Make sure the phone hasn't mucked anything up + for (int i = 0; i < channelFile.channels_count; i++) { + const meshtastic_Channel &ch = fixupChannel(i); - if (ch.role == meshtastic_Channel_Role_PRIMARY) - primaryIndex = i; - } + if (ch.role == meshtastic_Channel_Role_PRIMARY) + primaryIndex = i; + } #if !MESHTASTIC_EXCLUDE_MQTT - if (channels.anyMqttEnabled() && mqtt && !mqtt->isEnabled()) { - LOG_DEBUG("MQTT is enabled on at least one channel, so set MQTT thread to run immediately"); - mqtt->start(); - } + if (channels.anyMqttEnabled() && mqtt && !mqtt->isEnabled()) { + LOG_DEBUG("MQTT is enabled on at least one channel, so set MQTT thread to run immediately"); + mqtt->start(); + } #endif } -meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex) -{ - // remove this assert cause malformed packets can make our firmware reboot here. - if (chIndex < channelFile.channels_count) { // This should be equal to MAX_NUM_CHANNELS - meshtastic_Channel *ch = channelFile.channels + chIndex; - return *ch; - } else { - LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count); +meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex) { + // remove this assert cause malformed packets can make our firmware reboot here. + if (chIndex < channelFile.channels_count) { // This should be equal to MAX_NUM_CHANNELS + meshtastic_Channel *ch = channelFile.channels + chIndex; + return *ch; + } else { + LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count); - static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel)); - memset(ch, 0, sizeof(meshtastic_Channel)); - // ch.index -1 means we don't know the channel locally and need to look it up by settings.name - // not sure this is handled right everywhere - ch->index = -1; - return *ch; - } + static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel)); + memset(ch, 0, sizeof(meshtastic_Channel)); + // ch.index -1 means we don't know the channel locally and need to look it up by settings.name + // not sure this is handled right everywhere + ch->index = -1; + return *ch; + } } -meshtastic_Channel &Channels::getByName(const char *chName) -{ - for (ChannelIndex i = 0; i < getNumChannels(); i++) { - if (strcasecmp(getGlobalId(i), chName) == 0) { - return channelFile.channels[i]; - } +meshtastic_Channel &Channels::getByName(const char *chName) { + for (ChannelIndex i = 0; i < getNumChannels(); i++) { + if (strcasecmp(getGlobalId(i), chName) == 0) { + return channelFile.channels[i]; } + } - return getByIndex(getPrimaryIndex()); + return getByIndex(getPrimaryIndex()); } -void Channels::setChannel(const meshtastic_Channel &c) -{ - meshtastic_Channel &old = getByIndex(c.index); +void Channels::setChannel(const meshtastic_Channel &c) { + meshtastic_Channel &old = getByIndex(c.index); - // if this is the new primary, demote any existing roles - if (c.role == meshtastic_Channel_Role_PRIMARY) - for (int i = 0; i < getNumChannels(); i++) - if (channelFile.channels[i].role == meshtastic_Channel_Role_PRIMARY) - channelFile.channels[i].role = meshtastic_Channel_Role_SECONDARY; - - old = c; // slam in the new settings/role -} - -bool Channels::anyMqttEnabled() -{ -#if USERPREFS_EVENT_MODE && !MESHTASTIC_EXCLUDE_MQTT - // Don't publish messages on the public MQTT broker if we are in event mode - if (mqtt && mqtt->isUsingDefaultServer() && mqtt->isUsingDefaultRootTopic()) { - return false; - } -#endif + // if this is the new primary, demote any existing roles + if (c.role == meshtastic_Channel_Role_PRIMARY) for (int i = 0; i < getNumChannels(); i++) - if (channelFile.channels[i].role != meshtastic_Channel_Role_DISABLED && channelFile.channels[i].has_settings && - (channelFile.channels[i].settings.downlink_enabled || channelFile.channels[i].settings.uplink_enabled)) - return true; + if (channelFile.channels[i].role == meshtastic_Channel_Role_PRIMARY) + channelFile.channels[i].role = meshtastic_Channel_Role_SECONDARY; - return false; + old = c; // slam in the new settings/role } -const char *Channels::getName(size_t chIndex) -{ - // Convert the short "" representation for Default into a usable string - const meshtastic_ChannelSettings &channelSettings = getByIndex(chIndex).settings; - const char *channelName = channelSettings.name; - if (!*channelName) { // emptystring - // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case - // the app effed up and forgot to set channelSettings.name - if (config.lora.use_preset) { - channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); - } else { - channelName = "Custom"; - } - } +bool Channels::anyMqttEnabled() { +#if USERPREFS_EVENT_MODE && !MESHTASTIC_EXCLUDE_MQTT + // Don't publish messages on the public MQTT broker if we are in event mode + if (mqtt && mqtt->isUsingDefaultServer() && mqtt->isUsingDefaultRootTopic()) { + return false; + } +#endif + for (int i = 0; i < getNumChannels(); i++) + if (channelFile.channels[i].role != meshtastic_Channel_Role_DISABLED && channelFile.channels[i].has_settings && + (channelFile.channels[i].settings.downlink_enabled || channelFile.channels[i].settings.uplink_enabled)) + return true; - return channelName; + return false; } -bool Channels::isDefaultChannel(ChannelIndex chIndex) -{ - const auto &ch = getByIndex(chIndex); - if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { - const char *name = getName(chIndex); - const char *presetName = - DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); - // Check if the name is the default derived from the modem preset - if (strcmp(name, presetName) == 0) - return true; +const char *Channels::getName(size_t chIndex) { + // Convert the short "" representation for Default into a usable string + const meshtastic_ChannelSettings &channelSettings = getByIndex(chIndex).settings; + const char *channelName = channelSettings.name; + if (!*channelName) { // emptystring + // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case + // the app effed up and forgot to set channelSettings.name + if (config.lora.use_preset) { + channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + } else { + channelName = "Custom"; } - return false; + } + + return channelName; } -bool Channels::hasDefaultChannel() -{ - // If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel - if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency) - return false; - // Check if any of the channels are using the default name and PSK - for (size_t i = 0; i < getNumChannels(); i++) { - if (isDefaultChannel(i)) - return true; - } +bool Channels::isDefaultChannel(ChannelIndex chIndex) { + const auto &ch = getByIndex(chIndex); + if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { + const char *name = getName(chIndex); + const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + // Check if the name is the default derived from the modem preset + if (strcmp(name, presetName) == 0) + return true; + } + return false; +} + +bool Channels::hasDefaultChannel() { + // If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default + // channel + if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency) return false; + // Check if any of the channels are using the default name and PSK + for (size_t i = 0; i < getNumChannels(); i++) { + if (isDefaultChannel(i)) + return true; + } + return false; } /** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured) @@ -410,44 +393,40 @@ bool Channels::hasDefaultChannel() * * @return false if the channel hash or channel is invalid */ -bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) -{ - if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) { - // LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex), - // channelHash); - return false; - } else { - LOG_DEBUG("Use channel %d (hash 0x%x)", chIndex, channelHash); - setCrypto(chIndex); - return true; - } +bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) { + if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) { + // LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex), + // channelHash); + return false; + } else { + LOG_DEBUG("Use channel %d (hash 0x%x)", chIndex, channelHash); + setCrypto(chIndex); + return true; + } } -bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) -{ - // Iterate all known presets - for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; - ++preset) { - const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, - config.lora.use_preset); - if (!name) - continue; - if (strcmp(name, "Invalid") == 0) - continue; // skip invalid placeholder - uint8_t h = xorHash((const uint8_t *)name, strlen(name)); - // Expand default PSK alias 1 to actual bytes and xor into hash - uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); - if (tmp == channelHash) { - // Set crypto to defaultpsk and report success - CryptoKey k; - memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); - k.length = sizeof(defaultpsk); - crypto->setKey(k); - LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); - return true; - } +bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) { + // Iterate all known presets + for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, config.lora.use_preset); + if (!name) + continue; + if (strcmp(name, "Invalid") == 0) + continue; // skip invalid placeholder + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); + // Expand default PSK alias 1 to actual bytes and xor into hash + uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); + if (tmp == channelHash) { + // Set crypto to defaultpsk and report success + CryptoKey k; + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + crypto->setKey(k); + LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); + return true; } - return false; + } + return false; } /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) @@ -456,7 +435,4 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) * * @return the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 */ -int16_t Channels::setActiveByIndex(ChannelIndex channelIndex) -{ - return setCrypto(channelIndex); -} \ No newline at end of file +int16_t Channels::setActiveByIndex(ChannelIndex channelIndex) { return setCrypto(channelIndex); } \ No newline at end of file diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index a3cc7791c..518a8e909 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -15,135 +15,132 @@ typedef uint8_t ChannelIndex; typedef uint8_t ChannelHash; /** The container/on device API for working with channels */ -class Channels -{ - /// The index of the primary channel - ChannelIndex primaryIndex = 0; +class Channels { + /// The index of the primary channel + ChannelIndex primaryIndex = 0; - /** The channel index that was requested for sending/receiving. Note: if this channel is a secondary - channel and does not have a PSK, we will use the PSK from the primary channel. If this channel is disabled - no sending or receiving will be allowed */ - ChannelIndex activeChannelIndex = 0; + /** The channel index that was requested for sending/receiving. Note: if this channel is a secondary + channel and does not have a PSK, we will use the PSK from the primary channel. If this channel is disabled + no sending or receiving will be allowed */ + ChannelIndex activeChannelIndex = 0; - /// the precomputed hashes for each of our channels, or -1 for invalid - int16_t hashes[MAX_NUM_CHANNELS] = {}; + /// the precomputed hashes for each of our channels, or -1 for invalid + int16_t hashes[MAX_NUM_CHANNELS] = {}; - public: - Channels() {} +public: + Channels() {} - /// Well known channel names - static const char *adminChannel, *gpioChannel, *serialChannel, *mqttChannel; + /// Well known channel names + static const char *adminChannel, *gpioChannel, *serialChannel, *mqttChannel; - const meshtastic_ChannelSettings &getPrimary() { return getByIndex(getPrimaryIndex()).settings; } + const meshtastic_ChannelSettings &getPrimary() { return getByIndex(getPrimaryIndex()).settings; } - /** Return the Channel for a specified index */ - meshtastic_Channel &getByIndex(ChannelIndex chIndex); + /** Return the Channel for a specified index */ + meshtastic_Channel &getByIndex(ChannelIndex chIndex); - /** Return the Channel for a specified name, return primary if not found. */ - meshtastic_Channel &getByName(const char *chName); + /** Return the Channel for a specified name, return primary if not found. */ + meshtastic_Channel &getByName(const char *chName); - /** Using the index inside the channel, update the specified channel's settings and role. If this channel is being promoted - * to be primary, force all other channels to be secondary. - */ - void setChannel(const meshtastic_Channel &c); + /** Using the index inside the channel, update the specified channel's settings and role. If this channel is being + * promoted to be primary, force all other channels to be secondary. + */ + void setChannel(const meshtastic_Channel &c); - /** Return a human friendly name for this channel (and expand any short strings as needed) - */ - const char *getName(size_t chIndex); + /** Return a human friendly name for this channel (and expand any short strings as needed) + */ + const char *getName(size_t chIndex); - /** - * Return a globally unique channel ID usable with MQTT. - */ - const char *getGlobalId(size_t chIndex) { return getName(chIndex); } // FIXME, not correct + /** + * Return a globally unique channel ID usable with MQTT. + */ + const char *getGlobalId(size_t chIndex) { return getName(chIndex); } // FIXME, not correct - /** The index of the primary channel */ - ChannelIndex getPrimaryIndex() const { return primaryIndex; } + /** The index of the primary channel */ + ChannelIndex getPrimaryIndex() const { return primaryIndex; } - ChannelIndex getNumChannels() { return channelFile.channels_count; } + ChannelIndex getNumChannels() { return channelFile.channels_count; } - /// Called by NodeDB on initial boot when the radio config settings are unset. Set a default single channel config. - void initDefaults(); + /// Called by NodeDB on initial boot when the radio config settings are unset. Set a default single channel config. + void initDefaults(); - /// called when the user has just changed our radio config and we might need to change channel keys - void onConfigChanged(); + /// called when the user has just changed our radio config and we might need to change channel keys + void onConfigChanged(); - /** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured) - * - * This method is called before decoding inbound packets - * - * @return false if the channel hash or channel is invalid - */ - bool decryptForHash(ChannelIndex chIndex, ChannelHash channelHash); + /** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured) + * + * This method is called before decoding inbound packets + * + * @return false if the channel hash or channel is invalid + */ + bool decryptForHash(ChannelIndex chIndex, ChannelHash channelHash); - /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) - * - * This method is called before encoding outbound packets - * - * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 - */ - int16_t setActiveByIndex(ChannelIndex channelIndex); + /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) + * + * This method is called before encoding outbound packets + * + * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 + */ + int16_t setActiveByIndex(ChannelIndex channelIndex); - // Returns true if the channel has the default name and PSK - bool isDefaultChannel(ChannelIndex chIndex); + // Returns true if the channel has the default name and PSK + bool isDefaultChannel(ChannelIndex chIndex); - // Returns true if we can be reached via a channel with the default settings given a region and modem preset - bool hasDefaultChannel(); + // Returns true if we can be reached via a channel with the default settings given a region and modem preset + bool hasDefaultChannel(); - // Returns true if any of our channels have enabled MQTT uplink or downlink - bool anyMqttEnabled(); + // Returns true if any of our channels have enabled MQTT uplink or downlink + bool anyMqttEnabled(); - bool ensureLicensedOperation(); + bool ensureLicensedOperation(); - bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + bool setDefaultPresetCryptoForHash(ChannelHash channelHash); - int16_t getHash(ChannelIndex i) { return hashes[i]; } + int16_t getHash(ChannelIndex i) { return hashes[i]; } - private: - /** Given a channel index, change to use the crypto key specified by that index - * - * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 - */ - int16_t setCrypto(ChannelIndex chIndex); +private: + /** Given a channel index, change to use the crypto key specified by that index + * + * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 + */ + int16_t setCrypto(ChannelIndex chIndex); - /** Return the channel index for the specified channel hash, or -1 for not found */ - int8_t getIndexByHash(ChannelHash channelHash); + /** Return the channel index for the specified channel hash, or -1 for not found */ + int8_t getIndexByHash(ChannelHash channelHash); - /** Given a channel number, return the (0 to 255) hash for that channel - * If no suitable channel could be found, return -1 - * - * called by fixupChannel when a new channel is set - */ - int16_t generateHash(ChannelIndex channelNum); + /** Given a channel number, return the (0 to 255) hash for that channel + * If no suitable channel could be found, return -1 + * + * called by fixupChannel when a new channel is set + */ + int16_t generateHash(ChannelIndex channelNum); - /** - * Validate a channel, fixing any errors as needed - */ - meshtastic_Channel &fixupChannel(ChannelIndex chIndex); + /** + * Validate a channel, fixing any errors as needed + */ + meshtastic_Channel &fixupChannel(ChannelIndex chIndex); - /** - * Writes the default lora config - */ - void initDefaultLoraConfig(); + /** + * Writes the default lora config + */ + void initDefaultLoraConfig(); - /** - * Write default channels defined in UserPrefs - */ - void initDefaultChannel(ChannelIndex chIndex); + /** + * Write default channels defined in UserPrefs + */ + void initDefaultChannel(ChannelIndex chIndex); - /** - * Return the key used for encrypting this channel (if channel is secondary and no key provided, use the primary channel's - * PSK) - */ - CryptoKey getKey(ChannelIndex chIndex); + /** + * Return the key used for encrypting this channel (if channel is secondary and no key provided, use the primary + * channel's PSK) + */ + CryptoKey getKey(ChannelIndex chIndex); }; /// Singleton channel table extern Channels channels; /// 16 bytes of random PSK for our _public_ default channel that all devices power up on (AES128) -static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, - 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}; +static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}; -static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36, - 0xb8, 0x61, 0x21, 0xe1, 0xfb, 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74, - 0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1}; \ No newline at end of file +static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36, 0xb8, 0x61, 0x21, 0xe1, 0xfb, + 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74, 0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1}; \ No newline at end of file diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index 9ca16878d..e739fda39 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -21,20 +21,19 @@ * @param pubKey The destination for the public key. * @param privKey The destination for the private key. */ -void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) -{ - // Mix in any randomness we can, to make key generation stronger. - CryptRNG.begin(optstr(APP_VERSION)); - if (myNodeInfo.device_id.size == 16) { - CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); - } - auto noise = random(); - CryptRNG.stir((uint8_t *)&noise, sizeof(noise)); +void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) { + // Mix in any randomness we can, to make key generation stronger. + CryptRNG.begin(optstr(APP_VERSION)); + if (myNodeInfo.device_id.size == 16) { + CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); + } + auto noise = random(); + CryptRNG.stir((uint8_t *)&noise, sizeof(noise)); - LOG_DEBUG("Generate Curve25519 keypair"); - Curve25519::dh1(public_key, private_key); - memcpy(pubKey, public_key, sizeof(public_key)); - memcpy(privKey, private_key, sizeof(private_key)); + LOG_DEBUG("Generate Curve25519 keypair"); + Curve25519::dh1(public_key, private_key); + memcpy(pubKey, public_key, sizeof(public_key)); + memcpy(privKey, private_key, sizeof(private_key)); } /** @@ -43,28 +42,26 @@ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) * @param pubKey The destination for the public key. * @param privKey The source for the private key. */ -bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) -{ - if (!memfll(privKey, 0, sizeof(private_key))) { - Curve25519::eval(pubKey, privKey, 0); - if (Curve25519::isWeakPoint(pubKey)) { - LOG_ERROR("PKI key generation failed. Specified private key results in a weak"); - memset(pubKey, 0, 32); - return false; - } - memcpy(private_key, privKey, sizeof(private_key)); - memcpy(public_key, pubKey, sizeof(public_key)); - } else { - LOG_WARN("X25519 key generation failed due to blank private key"); - return false; +bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) { + if (!memfll(privKey, 0, sizeof(private_key))) { + Curve25519::eval(pubKey, privKey, 0); + if (Curve25519::isWeakPoint(pubKey)) { + LOG_ERROR("PKI key generation failed. Specified private key results in a weak"); + memset(pubKey, 0, 32); + return false; } - return true; + memcpy(private_key, privKey, sizeof(private_key)); + memcpy(public_key, pubKey, sizeof(public_key)); + } else { + LOG_WARN("X25519 key generation failed due to blank private key"); + return false; + } + return true; } #endif -void CryptoEngine::clearKeys() -{ - memset(public_key, 0, sizeof(public_key)); - memset(private_key, 0, sizeof(private_key)); +void CryptoEngine::clearKeys() { + memset(public_key, 0, sizeof(public_key)); + memset(private_key, 0, sizeof(private_key)); } /** @@ -79,33 +76,32 @@ void CryptoEngine::clearKeys() * @param bytes Buffer containing plaintext input. * @param bytesOut Output buffer to be populated with encrypted ciphertext. */ -bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, - uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) -{ - uint8_t *auth; - long extraNonceTmp = random(); - auth = bytesOut + numBytes; - memcpy((uint8_t *)(auth + 8), &extraNonceTmp, - sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; - LOG_DEBUG("Random nonce value: %d", extraNonceTmp); - if (remotePublic.size == 0) { - LOG_DEBUG("Node %d or their public_key not found", toNode); - return false; - } - if (!setDHPublicKey(remotePublic.bytes)) { - return false; - } - hash(shared_key, 32); - initNonce(fromNode, packetNum, extraNonceTmp); +bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) { + uint8_t *auth; + long extraNonceTmp = random(); + auth = bytesOut + numBytes; + memcpy((uint8_t *)(auth + 8), &extraNonceTmp, + sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; + LOG_DEBUG("Random nonce value: %d", extraNonceTmp); + if (remotePublic.size == 0) { + LOG_DEBUG("Node %d or their public_key not found", toNode); + return false; + } + if (!setDHPublicKey(remotePublic.bytes)) { + return false; + } + hash(shared_key, 32); + initNonce(fromNode, packetNum, extraNonceTmp); - // Calculate the shared secret with the destination node and encrypt - printBytes("Attempt encrypt with nonce: ", nonce, 13); - printBytes("Attempt encrypt with shared_key starting with: ", shared_key, 8); - aes_ccm_ae(shared_key, 32, nonce, 8, bytes, numBytes, nullptr, 0, bytesOut, - auth); // this can write up to 15 bytes longer than numbytes past bytesOut - memcpy((uint8_t *)(auth + 8), &extraNonceTmp, - sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; - return true; + // Calculate the shared secret with the destination node and encrypt + printBytes("Attempt encrypt with nonce: ", nonce, 13); + printBytes("Attempt encrypt with shared_key starting with: ", shared_key, 8); + aes_ccm_ae(shared_key, 32, nonce, 8, bytes, numBytes, nullptr, 0, bytesOut, + auth); // this can write up to 15 bytes longer than numbytes past bytesOut + memcpy((uint8_t *)(auth + 8), &extraNonceTmp, + sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; + return true; } /** @@ -119,36 +115,32 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas * @param bytes Buffer containing ciphertext input. * @param bytesOut Output buffer to be populated with decrypted plaintext. */ -bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, - size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) -{ - const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? - uint32_t extraNonce; // pointer was not really used - memcpy(&extraNonce, auth + 8, - sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : (uint32_t *)(auth + 8); - LOG_INFO("Random nonce value: %d", extraNonce); +bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, + const uint8_t *bytes, uint8_t *bytesOut) { + const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? + uint32_t extraNonce; // pointer was not really used + memcpy(&extraNonce, auth + 8, + sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : (uint32_t *)(auth + 8); + LOG_INFO("Random nonce value: %d", extraNonce); - if (remotePublic.size == 0) { - LOG_DEBUG("Node or its public key not found in database"); - return false; - } + if (remotePublic.size == 0) { + LOG_DEBUG("Node or its public key not found in database"); + return false; + } - // Calculate the shared secret with the sending node and decrypt - if (!setDHPublicKey(remotePublic.bytes)) { - return false; - } - hash(shared_key, 32); + // Calculate the shared secret with the sending node and decrypt + if (!setDHPublicKey(remotePublic.bytes)) { + return false; + } + hash(shared_key, 32); - initNonce(fromNode, packetNum, extraNonce); - printBytes("Attempt decrypt with nonce: ", nonce, 13); - printBytes("Attempt decrypt with shared_key starting with: ", shared_key, 8); - return aes_ccm_ad(shared_key, 32, nonce, 8, bytes, numBytes - 12, nullptr, 0, auth, bytesOut); + initNonce(fromNode, packetNum, extraNonce); + printBytes("Attempt decrypt with nonce: ", nonce, 13); + printBytes("Attempt decrypt with shared_key starting with: ", shared_key, 8); + return aes_ccm_ad(shared_key, 32, nonce, 8, bytes, numBytes - 12, nullptr, 0, auth, bytesOut); } -void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) -{ - memcpy(private_key, _private_key, 32); -} +void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) { memcpy(private_key, _private_key, 32); } /** * Hash arbitrary data using SHA256. @@ -156,58 +148,51 @@ void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) * @param bytes * @param numBytes */ -void CryptoEngine::hash(uint8_t *bytes, size_t numBytes) -{ - SHA256 hash; - size_t posn; - uint8_t size = numBytes; - uint8_t inc = 16; - hash.reset(); - for (posn = 0; posn < size; posn += inc) { - size_t len = size - posn; - if (len > inc) - len = inc; - hash.update(bytes + posn, len); - } - hash.finalize(bytes, 32); +void CryptoEngine::hash(uint8_t *bytes, size_t numBytes) { + SHA256 hash; + size_t posn; + uint8_t size = numBytes; + uint8_t inc = 16; + hash.reset(); + for (posn = 0; posn < size; posn += inc) { + size_t len = size - posn; + if (len > inc) + len = inc; + hash.update(bytes + posn, len); + } + hash.finalize(bytes, 32); } -void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len) -{ - delete aes; - aes = nullptr; - if (key_len != 0) { - aes = new AESSmall256(); - aes->setKey(key_bytes, key_len); - } +void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len) { + delete aes; + aes = nullptr; + if (key_len != 0) { + aes = new AESSmall256(); + aes->setKey(key_bytes, key_len); + } } -void CryptoEngine::aesEncrypt(uint8_t *in, uint8_t *out) -{ - aes->encryptBlock(out, in); -} +void CryptoEngine::aesEncrypt(uint8_t *in, uint8_t *out) { aes->encryptBlock(out, in); } -bool CryptoEngine::setDHPublicKey(uint8_t *pubKey) -{ - uint8_t local_priv[32]; - memcpy(shared_key, pubKey, 32); - memcpy(local_priv, private_key, 32); - // Calculate the shared secret with the specified node's public key and our private key - // This includes an internal weak key check, which among other things looks for an all 0 public key and shared key. - if (!Curve25519::dh2(shared_key, local_priv)) { - LOG_WARN("Curve25519DH step 2 failed!"); - return false; - } - return true; +bool CryptoEngine::setDHPublicKey(uint8_t *pubKey) { + uint8_t local_priv[32]; + memcpy(shared_key, pubKey, 32); + memcpy(local_priv, private_key, 32); + // Calculate the shared secret with the specified node's public key and our private key + // This includes an internal weak key check, which among other things looks for an all 0 public key and shared key. + if (!Curve25519::dh2(shared_key, local_priv)) { + LOG_WARN("Curve25519DH step 2 failed!"); + return false; + } + return true; } #endif concurrency::Lock *cryptLock; -void CryptoEngine::setKey(const CryptoKey &k) -{ - LOG_DEBUG("Use AES%d key!", k.length * 8); - key = k; +void CryptoEngine::setKey(const CryptoKey &k) { + LOG_DEBUG("Use AES%d key!", k.length * 8); + key = k; } /** @@ -215,56 +200,52 @@ void CryptoEngine::setKey(const CryptoKey &k) * * @param bytes is updated in place */ -void CryptoEngine::encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) -{ - if (key.length > 0) { - initNonce(fromNode, packetId); - if (numBytes <= MAX_BLOCKSIZE) { - encryptAESCtr(key, nonce, numBytes, bytes); - } else { - LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); - } +void CryptoEngine::encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) { + if (key.length > 0) { + initNonce(fromNode, packetId); + if (numBytes <= MAX_BLOCKSIZE) { + encryptAESCtr(key, nonce, numBytes, bytes); + } else { + LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); } + } } -void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) -{ - // For CTR, the implementation is the same - encryptPacket(fromNode, packetId, numBytes, bytes); +void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) { + // For CTR, the implementation is the same + encryptPacket(fromNode, packetId, numBytes, bytes); } // Generic implementation of AES-CTR encryption. -void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) -{ - delete ctr; - ctr = nullptr; - if (_key.length == 16) - ctr = new CTR(); - else - ctr = new CTR(); - ctr->setKey(_key.bytes, _key.length); - static uint8_t scratch[MAX_BLOCKSIZE]; - memcpy(scratch, bytes, numBytes); - memset(scratch + numBytes, 0, - sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) +void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) { + delete ctr; + ctr = nullptr; + if (_key.length == 16) + ctr = new CTR(); + else + ctr = new CTR(); + ctr->setKey(_key.bytes, _key.length); + static uint8_t scratch[MAX_BLOCKSIZE]; + memcpy(scratch, bytes, numBytes); + memset(scratch + numBytes, 0, + sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) - ctr->setIV(_nonce, 16); - ctr->setCounterSize(4); - ctr->encrypt(bytes, scratch, numBytes); + ctr->setIV(_nonce, 16); + ctr->setCounterSize(4); + ctr->encrypt(bytes, scratch, numBytes); } /** * Init our 128 bit nonce for a new packet */ -void CryptoEngine::initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce) -{ - memset(nonce, 0, sizeof(nonce)); +void CryptoEngine::initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce) { + memset(nonce, 0, sizeof(nonce)); - // use memcpy to avoid breaking strict-aliasing - memcpy(nonce, &packetId, sizeof(uint64_t)); - memcpy(nonce + sizeof(uint64_t), &fromNode, sizeof(uint32_t)); - if (extraNonce) - memcpy(nonce + sizeof(uint32_t), &extraNonce, sizeof(uint32_t)); + // use memcpy to avoid breaking strict-aliasing + memcpy(nonce, &packetId, sizeof(uint64_t)); + memcpy(nonce + sizeof(uint64_t), &fromNode, sizeof(uint32_t)); + if (extraNonce) + memcpy(nonce + sizeof(uint32_t), &extraNonce, sizeof(uint32_t)); } #ifndef HAS_CUSTOM_CRYPTO_ENGINE CryptoEngine *crypto = new CryptoEngine; diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index 6bbcb3b8a..01ceb5068 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -9,10 +9,10 @@ extern concurrency::Lock *cryptLock; struct CryptoKey { - uint8_t bytes[32]; + uint8_t bytes[32]; - /// # of bytes, or -1 to mean "invalid key - do not use" - int8_t length; + /// # of bytes, or -1 to mean "invalid key - do not use" + int8_t length; }; /** @@ -23,75 +23,74 @@ struct CryptoKey { #define MAX_BLOCKSIZE 256 #define TEST_CURVE25519_FIELD_OPS // Exposes Curve25519::isWeakPoint() for testing keys -class CryptoEngine -{ - public: +class CryptoEngine { +public: #if !(MESHTASTIC_EXCLUDE_PKI) - uint8_t public_key[32] = {0}; + uint8_t public_key[32] = {0}; #endif - virtual ~CryptoEngine() {} + virtual ~CryptoEngine() {} #if !(MESHTASTIC_EXCLUDE_PKI) #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) - virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey); - virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); + virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey); + virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); #endif - void clearKeys(); - void setDHPrivateKey(uint8_t *_private_key); - virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, - uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); - virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, - size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); - virtual bool setDHPublicKey(uint8_t *publicKey); - virtual void hash(uint8_t *bytes, size_t numBytes); + void clearKeys(); + void setDHPrivateKey(uint8_t *_private_key); + virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); + virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, + const uint8_t *bytes, uint8_t *bytesOut); + virtual bool setDHPublicKey(uint8_t *publicKey); + virtual void hash(uint8_t *bytes, size_t numBytes); - virtual void aesSetKey(const uint8_t *key, size_t key_len); + virtual void aesSetKey(const uint8_t *key, size_t key_len); - virtual void aesEncrypt(uint8_t *in, uint8_t *out); - AESSmall256 *aes = NULL; + virtual void aesEncrypt(uint8_t *in, uint8_t *out); + AESSmall256 *aes = NULL; #endif - /** - * Set the key used for encrypt, decrypt. - * - * As a special case: If all bytes are zero, we assume _no encryption_ and send all data in cleartext. - * - * @param numBytes must be 16 (AES128), 32 (AES256) or 0 (no crypt) - * @param bytes a _static_ buffer that will remain valid for the life of this crypto instance (i.e. this class will cache the - * provided pointer) - */ - virtual void setKey(const CryptoKey &k); + /** + * Set the key used for encrypt, decrypt. + * + * As a special case: If all bytes are zero, we assume _no encryption_ and send all data in cleartext. + * + * @param numBytes must be 16 (AES128), 32 (AES256) or 0 (no crypt) + * @param bytes a _static_ buffer that will remain valid for the life of this crypto instance (i.e. this class will + * cache the provided pointer) + */ + virtual void setKey(const CryptoKey &k); - /** - * Encrypt a packet - * - * @param bytes is updated in place - */ - virtual void encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); - virtual void decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); - virtual void encryptAESCtr(CryptoKey key, uint8_t *nonce, size_t numBytes, uint8_t *bytes); + /** + * Encrypt a packet + * + * @param bytes is updated in place + */ + virtual void encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); + virtual void decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); + virtual void encryptAESCtr(CryptoKey key, uint8_t *nonce, size_t numBytes, uint8_t *bytes); #ifndef PIO_UNIT_TESTING - protected: +protected: #endif - /** Our per packet nonce */ - uint8_t nonce[16] = {0}; - CryptoKey key = {}; - CTRCommon *ctr = NULL; + /** Our per packet nonce */ + uint8_t nonce[16] = {0}; + CryptoKey key = {}; + CTRCommon *ctr = NULL; #if !(MESHTASTIC_EXCLUDE_PKI) - uint8_t shared_key[32] = {0}; - uint8_t private_key[32] = {0}; + uint8_t shared_key[32] = {0}; + uint8_t private_key[32] = {0}; #endif - /** - * Init our 128 bit nonce for a new packet - * - * The NONCE is constructed by concatenating (from MSB to LSB): - * a 64 bit packet number (stored in little endian order) - * a 32 bit sending node number (stored in little endian order) - * a 32 bit block counter (starts at zero) - */ - void initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce = 0); + /** + * Init our 128 bit nonce for a new packet + * + * The NONCE is constructed by concatenating (from MSB to LSB): + * a 64 bit packet number (stored in little endian order) + * a 32 bit sending node number (stored in little endian order) + * a 32 bit block counter (starts at zero) + */ + void initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce = 0); }; extern CryptoEngine *crypto; \ No newline at end of file diff --git a/src/mesh/Default.cpp b/src/mesh/Default.cpp index 1bd0340f8..f595aed84 100644 --- a/src/mesh/Default.cpp +++ b/src/mesh/Default.cpp @@ -2,25 +2,22 @@ #include "meshUtils.h" -uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval) -{ - if (configuredInterval > 0) - return configuredInterval * 1000; - return defaultInterval * 1000; +uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval) { + if (configuredInterval > 0) + return configuredInterval * 1000; + return defaultInterval * 1000; } -uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval) -{ - if (configuredInterval > 0) - return configuredInterval * 1000; - return default_broadcast_interval_secs * 1000; +uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval) { + if (configuredInterval > 0) + return configuredInterval * 1000; + return default_broadcast_interval_secs * 1000; } -uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue) -{ - if (configured > 0) - return configured; - return defaultValue; +uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue) { + if (configured > 0) + return configured; + return defaultValue; } /** * Calculates the scaled value of the configured or default value in ms based on the number of online nodes. @@ -35,33 +32,30 @@ uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultVa * @param numOnlineNodes The number of online nodes. * @return The scaled value of the configured or default value. */ -uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) -{ - // If we are a router, we don't scale the value. It's already significantly higher. - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) - return getConfiguredOrDefaultMs(configured, defaultValue); +uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) { + // If we are a router, we don't scale the value. It's already significantly higher. + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) + return getConfiguredOrDefaultMs(configured, defaultValue); - // Additionally if we're a tracker or sensor, we want priority to send position and telemetry - if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER)) - return getConfiguredOrDefaultMs(configured, defaultValue); + // Additionally if we're a tracker or sensor, we want priority to send position and telemetry + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER)) + return getConfiguredOrDefaultMs(configured, defaultValue); - return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); + return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); } -uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue) -{ - // If zero, intervals should be coalesced later by getConfiguredOrDefault... methods - if (configured == 0) - return configured; +uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue) { + // If zero, intervals should be coalesced later by getConfiguredOrDefault... methods + if (configured == 0) + return configured; - return configured < minValue ? minValue : configured; + return configured < minValue ? minValue : configured; } -uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured) -{ +uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured) { #if USERPREFS_EVENT_MODE - return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit; + return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit; #else - return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit; + return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit; #endif } \ No newline at end of file diff --git a/src/mesh/Default.h b/src/mesh/Default.h index a60e3af9b..ec44c7190 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -38,47 +38,43 @@ #define default_mqtt_encryption_enabled true #define default_mqtt_tls_enabled false -#define IF_ROUTER(routerVal, normalVal) \ - ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal)) +#define IF_ROUTER(routerVal, normalVal) ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal)) -class Default -{ - public: - static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval); - static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval); - static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue); - // Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility, - // even though internal node counts use uint16_t (max 65535 nodes) - static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes); - static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured); - static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue); +class Default { +public: + static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval); + static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval); + static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue); + // Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility, + // even though internal node counts use uint16_t (max 65535 nodes) + static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes); + static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured); + static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue); - private: - // Note: Kept as uint32_t to match the public API parameter type - static float congestionScalingCoefficient(uint32_t numOnlineNodes) - { - if (numOnlineNodes <= 40) { - return 1.0; - } else { - float throttlingFactor = 0.075; - if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW) - throttlingFactor = 0.04; - else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) - throttlingFactor = 0.02; - else if (config.lora.use_preset && - IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) - throttlingFactor = 0.01; +private: + // Note: Kept as uint32_t to match the public API parameter type + static float congestionScalingCoefficient(uint32_t numOnlineNodes) { + if (numOnlineNodes <= 40) { + return 1.0; + } else { + float throttlingFactor = 0.075; + if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW) + throttlingFactor = 0.04; + else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) + throttlingFactor = 0.02; + else if (config.lora.use_preset && + IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) + throttlingFactor = 0.01; #if USERPREFS_EVENT_MODE - // If we are in event mode, scale down the throttling factor - throttlingFactor = 0.04; + // If we are in event mode, scale down the throttling factor + throttlingFactor = 0.04; #endif - // Scaling up traffic based on number of nodes over 40 - int nodesOverForty = (numOnlineNodes - 40); - return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) - } + // Scaling up traffic based on number of nodes over 40 + int nodesOverForty = (numOnlineNodes - 40); + return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) } + } }; diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index b7459abe0..2e98b015e 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -15,139 +15,126 @@ FloodingRouter::FloodingRouter() {} * later free() the packet to pool. This routine is not allowed to stall. * If the txmit queue is full it might return an error */ -ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p) -{ - // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) - p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us - wasSeenRecently(p); // FIXME, move this to a sniffSent method +ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p) { + // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us + wasSeenRecently(p); // FIXME, move this to a sniffSent method - return Router::send(p); + return Router::send(p); } -bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) -{ - bool wasUpgraded = false; - bool seenRecently = - wasSeenRecently(p, true, nullptr, nullptr, &wasUpgraded); // Updates history; returns false when an upgrade is detected +bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { + bool wasUpgraded = false; + bool seenRecently = wasSeenRecently(p, true, nullptr, nullptr, + &wasUpgraded); // Updates history; returns false when an upgrade is detected - // Handle hop_limit upgrade scenario for rebroadcasters - if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { - return true; // we handled it, so stop processing + // Handle hop_limit upgrade scenario for rebroadcasters + if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { + return true; // we handled it, so stop processing + } + + if (seenRecently) { + printPacket("Ignore dupe incoming msg", p); + rxDupe++; + + /* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, + e.g., when the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */ + bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; + if (isRepeated) { + LOG_DEBUG("Repeated reliable tx"); + // Check if it's still in the Tx queue, if not, we have to relay it again + if (!findInTxQueue(p->from, p->id)) { + reprocessPacket(p); + perhapsRebroadcast(p); + } + } else { + perhapsCancelDupe(p); } - if (seenRecently) { - printPacket("Ignore dupe incoming msg", p); - rxDupe++; + return true; + } - /* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, e.g., when - the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */ - bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; - if (isRepeated) { - LOG_DEBUG("Repeated reliable tx"); - // Check if it's still in the Tx queue, if not, we have to relay it again - if (!findInTxQueue(p->from, p->id)) { - reprocessPacket(p); - perhapsRebroadcast(p); - } - } else { - perhapsCancelDupe(p); - } - - return true; - } - - return Router::shouldFilterReceived(p); + return Router::shouldFilterReceived(p); } -bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p) -{ - // isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages - if (isRebroadcaster() && iface && p->hop_limit > 0) { - // If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to - // rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead. - uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining - if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) { - LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id, - p->hop_limit, dropThreshold); +bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p) { + // isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages + if (isRebroadcaster() && iface && p->hop_limit > 0) { + // If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to + // rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead. + uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining + if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) { + LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id, p->hop_limit, dropThreshold); - reprocessPacket(p); - perhapsRebroadcast(p); + reprocessPacket(p); + perhapsRebroadcast(p); - rxDupe++; - // We already enqueued the improved copy, so make sure the incoming packet stops here. - return true; - } + rxDupe++; + // We already enqueued the improved copy, so make sure the incoming packet stops here. + return true; } + } - return false; + return false; } -void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) -{ - if (nodeDB) - nodeDB->updateFrom(*p); +void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) { + if (nodeDB) + nodeDB->updateFrom(*p); #if !MESHTASTIC_EXCLUDE_TRACEROUTE - if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) - traceRouteModule->processUpgradedPacket(*p); + if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) + traceRouteModule->processUpgradedPacket(*p); #endif } -bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) -{ - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - // ROUTER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), - // even if we've heard another station rebroadcast it already. - return false; - } +bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) { + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + // ROUTER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return false; + } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - // CLIENT_BASE: if the packet is from or to a favorited node, - // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), - // even if we've heard another station rebroadcast it already. - return !nodeDB->isFromOrToFavoritedNode(*p); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // CLIENT_BASE: if the packet is from or to a favorited node, + // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return !nodeDB->isFromOrToFavoritedNode(*p); + } - // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. - return true; + // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. + return true; } -void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) -{ - if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { - // cancel rebroadcast of this message *if* there was already one, unless we're a router! - // But only LoRa packets should be able to trigger this. - if (Router::cancelSending(p->from, p->id)) - txRelayCanceled++; - } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { - iface->clampToLateRebroadcastWindow(getFrom(p), p->id); - } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && - nodeDB->isFromOrToFavoritedNode(*p)) { - iface->clampToLateRebroadcastWindow(getFrom(p), p->id); - } +void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { + // cancel rebroadcast of this message *if* there was already one, unless we're a router! + // But only LoRa packets should be able to trigger this. + if (Router::cancelSending(p->from, p->id)) + txRelayCanceled++; + } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && nodeDB->isFromOrToFavoritedNode(*p)) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } } -bool FloodingRouter::isRebroadcaster() -{ - return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE && - config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; +bool FloodingRouter::isRebroadcaster() { + return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE && + config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; } -void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) -{ - bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && - (p->decoded.request_id != 0 || p->decoded.reply_id != 0); - if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) { - // do not flood direct message that is ACKed or replied to - LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast"); - Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM - } +void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { + bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0 || p->decoded.reply_id != 0); + if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) { + // do not flood direct message that is ACKed or replied to + LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast"); + Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM + } - perhapsRebroadcast(p); + perhapsRebroadcast(p); - // handle the packet as normal - Router::sniffReceived(p, c); + // handle the packet as normal + Router::sniffReceived(p, c); } diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index e8a2e9685..d3b9199ad 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -25,54 +25,53 @@ Any entries in recentBroadcasts that are older than X seconds (longer than the max time a flood can take) will be discarded. */ -class FloodingRouter : public Router -{ - public: - /** - * Constructor - * - */ - FloodingRouter(); +class FloodingRouter : public Router { +public: + /** + * Constructor + * + */ + FloodingRouter(); - /** - * Send a packet on a suitable interface. This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + /** + * Send a packet on a suitable interface. This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - protected: - /** - * Should this incoming filter be dropped? - * - * Called immediately on reception, before any further processing. - * @return true to abandon the packet - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; +protected: + /** + * Should this incoming filter be dropped? + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; - /** - * Look for broadcasts we need to rebroadcast - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + /** + * Look for broadcasts we need to rebroadcast + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /* Check if we should rebroadcast this packet, and do so if needed */ - virtual bool perhapsRebroadcast(const meshtastic_MeshPacket *p) = 0; + /* Check if we should rebroadcast this packet, and do so if needed */ + virtual bool perhapsRebroadcast(const meshtastic_MeshPacket *p) = 0; - /* Check if we should handle an upgraded packet (with higher hop_limit) - * @return true if we handled it (so stop processing) - */ - bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p); + /* Check if we should handle an upgraded packet (with higher hop_limit) + * @return true if we handled it (so stop processing) + */ + bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p); - /* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */ - void reprocessPacket(const meshtastic_MeshPacket *p); + /* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */ + void reprocessPacket(const meshtastic_MeshPacket *p); - // Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of - // the same packet - bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); + // Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of + // the same packet + bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); - /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ - void perhapsCancelDupe(const meshtastic_MeshPacket *p); + /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ + void perhapsCancelDupe(const meshtastic_MeshPacket *p); - // Return true if we are a rebroadcaster - bool isRebroadcaster(); + // Return true if we are a rebroadcaster + bool isRebroadcaster(); }; \ No newline at end of file diff --git a/src/mesh/LLCC68Interface.cpp b/src/mesh/LLCC68Interface.cpp index d92ea5450..7cc0ec4d7 100644 --- a/src/mesh/LLCC68Interface.cpp +++ b/src/mesh/LLCC68Interface.cpp @@ -3,9 +3,6 @@ #include "configuration.h" #include "error.h" -LLCC68Interface::LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) -{ -} +LLCC68Interface::LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) {} #endif \ No newline at end of file diff --git a/src/mesh/LLCC68Interface.h b/src/mesh/LLCC68Interface.h index 1cd23e92b..fb7cfccbb 100644 --- a/src/mesh/LLCC68Interface.h +++ b/src/mesh/LLCC68Interface.h @@ -6,14 +6,11 @@ * Our adapter for LLCC68 radios * https://www.semtech.com/products/wireless-rf/lora-core/llcc68 * ⚠️⚠️⚠️ - * Be aware that LLCC68 does not support Spreading Factor 12 (SF12) and will not work on the "LongSlow" and "VLongSlow" channels. - * You must change the channel if you get `Critical Error #3` with this module. - * ⚠️⚠️⚠️ + * Be aware that LLCC68 does not support Spreading Factor 12 (SF12) and will not work on the "LongSlow" and "VLongSlow" + * channels. You must change the channel if you get `Critical Error #3` with this module. ⚠️⚠️⚠️ */ -class LLCC68Interface : public SX126xInterface -{ - public: - LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +class LLCC68Interface : public SX126xInterface { +public: + LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/LR1110Interface.cpp b/src/mesh/LR1110Interface.cpp index 5dbd3ff38..a1cfb0c2d 100644 --- a/src/mesh/LR1110Interface.cpp +++ b/src/mesh/LR1110Interface.cpp @@ -4,9 +4,6 @@ #include "configuration.h" #include "error.h" -LR1110Interface::LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : LR11x0Interface(hal, cs, irq, rst, busy) -{ -} +LR1110Interface::LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : LR11x0Interface(hal, cs, irq, rst, busy) {} #endif \ No newline at end of file diff --git a/src/mesh/LR1110Interface.h b/src/mesh/LR1110Interface.h index 2a2e6e861..6edf76d57 100644 --- a/src/mesh/LR1110Interface.h +++ b/src/mesh/LR1110Interface.h @@ -5,10 +5,8 @@ /** * Our adapter for LR1110 radios */ -class LR1110Interface : public LR11x0Interface -{ - public: - LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +class LR1110Interface : public LR11x0Interface { +public: + LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/LR1120Interface.cpp b/src/mesh/LR1120Interface.cpp index a17ac87ef..08c38f79c 100644 --- a/src/mesh/LR1120Interface.cpp +++ b/src/mesh/LR1120Interface.cpp @@ -4,14 +4,8 @@ #include "configuration.h" #include "error.h" -LR1120Interface::LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : LR11x0Interface(hal, cs, irq, rst, busy) -{ -} +LR1120Interface::LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : LR11x0Interface(hal, cs, irq, rst, busy) {} -bool LR1120Interface::wideLora() -{ - return true; -} +bool LR1120Interface::wideLora() { return true; } #endif \ No newline at end of file diff --git a/src/mesh/LR1120Interface.h b/src/mesh/LR1120Interface.h index d81a480a9..0e143bbf7 100644 --- a/src/mesh/LR1120Interface.h +++ b/src/mesh/LR1120Interface.h @@ -5,11 +5,9 @@ /** * Our adapter for LR1120 wideband radios */ -class LR1120Interface : public LR11x0Interface -{ - public: - LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); - bool wideLora() override; +class LR1120Interface : public LR11x0Interface { +public: + LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); + bool wideLora() override; }; #endif \ No newline at end of file diff --git a/src/mesh/LR1121Interface.cpp b/src/mesh/LR1121Interface.cpp index 29bd07d08..5d86cfeca 100644 --- a/src/mesh/LR1121Interface.cpp +++ b/src/mesh/LR1121Interface.cpp @@ -3,14 +3,8 @@ #include "configuration.h" #include "error.h" -LR1121Interface::LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : LR11x0Interface(hal, cs, irq, rst, busy) -{ -} +LR1121Interface::LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : LR11x0Interface(hal, cs, irq, rst, busy) {} -bool LR1121Interface::wideLora() -{ - return true; -} +bool LR1121Interface::wideLora() { return true; } #endif \ No newline at end of file diff --git a/src/mesh/LR1121Interface.h b/src/mesh/LR1121Interface.h index ebc5b59a9..1c15b4285 100644 --- a/src/mesh/LR1121Interface.h +++ b/src/mesh/LR1121Interface.h @@ -6,11 +6,9 @@ /** * Our adapter for LR1121 wideband radios */ -class LR1121Interface : public LR11x0Interface -{ - public: - LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); - bool wideLora() override; +class LR1121Interface : public LR11x0Interface { +public: + LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); + bool wideLora() override; }; #endif \ No newline at end of file diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index af6dd92e9..03b9c858a 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -18,8 +18,8 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { }; #endif -// Particular boards might define a different max power based on what their hardware can do, default to max power output if not -// specified (may be dangerous if using external PA and LR11x0 power config forgotten) +// Particular boards might define a different max power based on what their hardware can do, default to max power output +// if not specified (may be dangerous if using external PA and LR11x0 power config forgotten) #if ARCH_PORTDUINO #define LR1110_MAX_POWER portduino_config.lr1110_max_power #endif @@ -39,271 +39,254 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { template LR11x0Interface::LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) -{ - LOG_WARN("LR11x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { + LOG_WARN("LR11x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -template bool LR11x0Interface::init() -{ +template bool LR11x0Interface::init() { #ifdef LR11X0_POWER_EN - pinMode(LR11X0_POWER_EN, OUTPUT); - digitalWrite(LR11X0_POWER_EN, HIGH); + pinMode(LR11X0_POWER_EN, OUTPUT); + digitalWrite(LR11X0_POWER_EN, HIGH); #endif #if ARCH_PORTDUINO - float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; // FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE #elif !defined(LR11X0_DIO3_TCXO_VOLTAGE) - float tcxoVoltage = - 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per - // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 - // (DIO3 is free to be used as an IRQ) - LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); + float tcxoVoltage = + 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per + // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 + // (DIO3 is free to be used as an IRQ) + LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); #else - float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE; - LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE); - // (DIO3 is not free to be used as an IRQ) + float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE; + LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE); + // (DIO3 is not free to be used as an IRQ) #endif - RadioLibInterface::init(); + RadioLibInterface::init(); - limitPower(LR1110_MAX_POWER); + limitPower(LR1110_MAX_POWER); - if ((power > LR1120_MAX_POWER) && - (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range - power = LR1120_MAX_POWER; - preambleLength = 12; // 12 is the default for operation above 2GHz - } + if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range + power = LR1120_MAX_POWER; + preambleLength = 12; // 12 is the default for operation above 2GHz + } #ifdef LR11X0_RF_SWITCH_SUBGHZ - pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); - digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); - LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); + pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); + digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); + LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif #ifdef LR11X0_RF_SWITCH_2_4GHZ - pinMode(LR11X0_RF_SWITCH_2_4GHZ, OUTPUT); - digitalWrite(LR11X0_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); - LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); + pinMode(LR11X0_RF_SWITCH_2_4GHZ, OUTPUT); + digitalWrite(LR11X0_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); + LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); - // \todo Display actual typename of the adapter, not just `LR11x0` - LOG_INFO("LR11x0 init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) - return false; + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + // \todo Display actual typename of the adapter, not just `LR11x0` + LOG_INFO("LR11x0 init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) + return false; - LR11x0VersionInfo_t version; - res = lora.getVersionInfo(&version); - if (res == RADIOLIB_ERR_NONE) - LOG_DEBUG("LR11x0 Device %d, HW %d, FW %d.%d, WiFi %d.%d, GNSS %d.%d", version.device, version.hardware, version.fwMajor, - version.fwMinor, version.fwMajorWiFi, version.fwMinorWiFi, version.fwGNSS, version.almanacGNSS); + LR11x0VersionInfo_t version; + res = lora.getVersionInfo(&version); + if (res == RADIOLIB_ERR_NONE) + LOG_DEBUG("LR11x0 Device %d, HW %d, FW %d.%d, WiFi %d.%d, GNSS %d.%d", version.device, version.hardware, version.fwMajor, version.fwMinor, + version.fwMajorWiFi, version.fwMinorWiFi, version.fwGNSS, version.almanacGNSS); - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); - if (res == RADIOLIB_ERR_NONE) - res = lora.setCRC(2); + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); - // FIXME: May want to set depending on a definition, currently all LR1110 variant files use the DC-DC regulator option - if (res == RADIOLIB_ERR_NONE) - res = lora.setRegulatorDCDC(); + // FIXME: May want to set depending on a definition, currently all LR1110 variant files use the DC-DC regulator option + if (res == RADIOLIB_ERR_NONE) + res = lora.setRegulatorDCDC(); #ifdef LR11X0_DIO_AS_RF_SWITCH - bool dioAsRfSwitch = true; + bool dioAsRfSwitch = true; #elif defined(ARCH_PORTDUINO) - bool dioAsRfSwitch = portduino_config.has_rfswitch_table; + bool dioAsRfSwitch = portduino_config.has_rfswitch_table; #else - bool dioAsRfSwitch = false; + bool dioAsRfSwitch = false; #endif - if (dioAsRfSwitch) { - lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); - LOG_DEBUG("Set DIO RF switch"); + if (dioAsRfSwitch) { + lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); + LOG_DEBUG("Set DIO RF switch"); + } + + if (res == RADIOLIB_ERR_NONE) { + if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate + res = lora.setRxBoostedGainMode(true); + LOG_INFO("Set RX gain to boosted mode; result: %d", res); + } else { + res = lora.setRxBoostedGainMode(false); + LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); } + } - if (res == RADIOLIB_ERR_NONE) { - if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate - res = lora.setRxBoostedGainMode(true); - LOG_INFO("Set RX gain to boosted mode; result: %d", res); - } else { - res = lora.setRxBoostedGainMode(false); - LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); - } - } + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving - - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -template bool LR11x0Interface::reconfigure() -{ - RadioLibInterface::reconfigure(); +template bool LR11x0Interface::reconfigure() { + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f)); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f)); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setSyncWord(syncWord); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setSyncWord(syncWord); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setPreambleLength(preambleLength); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setPreambleLength(preambleLength); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > LR1110_MAX_POWER) // This chip has lower power limits than some - power = LR1110_MAX_POWER; - if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit - power = LR1120_MAX_POWER; + if (power > LR1110_MAX_POWER) // This chip has lower power limits than some + power = LR1110_MAX_POWER; + if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit + power = LR1120_MAX_POWER; - err = lora.setOutputPower(power); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setOutputPower(power); + assert(err == RADIOLIB_ERR_NONE); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() -{ - lora.clearIrqAction(); -} +template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() { lora.clearIrqAction(); } -template void LR11x0Interface::setStandby() -{ - checkNotification(); // handle any pending interrupts before we force standby +template void LR11x0Interface::setStandby() { + checkNotification(); // handle any pending interrupts before we force standby - int err = lora.standby(); + int err = lora.standby(); - if (err != RADIOLIB_ERR_NONE) { - LOG_DEBUG("LR11x0 standby failed with error %d", err); - } + if (err != RADIOLIB_ERR_NONE) { + LOG_DEBUG("LR11x0 standby failed with error %d", err); + } - assert(err == RADIOLIB_ERR_NONE); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = false; // If we were receiving, not any more - activeReceiveStart = 0; - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** * Add SNR data to received messages */ -template void LR11x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) -{ - // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); - mp->rx_snr = lora.getSNR(); - mp->rx_rssi = lround(lora.getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); +template void LR11x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) { + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ -template void LR11x0Interface::configHardwareForSend() -{ - RadioLibInterface::configHardwareForSend(); -} +template void LR11x0Interface::configHardwareForSend() { RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY -template void LR11x0Interface::startReceive() -{ +template void LR11x0Interface::startReceive() { #ifdef SLEEP_ONLY - sleep(); + sleep(); #else - setStandby(); + setStandby(); - lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. + lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. - // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. - int err = - lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); - if (err) - LOG_ERROR("StartReceive error: %d", err); - assert(err == RADIOLIB_ERR_NONE); + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + int err = lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + if (err) + LOG_ERROR("StartReceive error: %d", err); + assert(err == RADIOLIB_ERR_NONE); - RadioLibInterface::startReceive(); + RadioLibInterface::startReceive(); - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register + // bits + enableInterrupt(isrRxLevel0); #endif } /** Is the channel currently active? */ -template bool LR11x0Interface::isChannelActive() -{ - // check if we can detect a LoRa preamble on the current channel - ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, - .detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, - .detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, - .exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, - .timeout = 0, - .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, - .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; - int16_t result; +template bool LR11x0Interface::isChannelActive() { + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; - setStandby(); - result = lora.scanChannel(cfg); - if (result == RADIOLIB_LORA_DETECTED) - return true; + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; - assert(result != RADIOLIB_ERR_WRONG_MODEM); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - return false; + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -template bool LR11x0Interface::isActivelyReceiving() -{ - // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet - // received and handled the interrupt for reading the packet/handling errors. - return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID, - RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); +template bool LR11x0Interface::isActivelyReceiving() { + // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet + // received and handled the interrupt for reading the packet/handling errors. + return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID, RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); } -template bool LR11x0Interface::sleep() -{ - // \todo Display actual typename of the adapter, not just `LR11x0` - LOG_DEBUG("LR11x0 entering sleep mode"); - setStandby(); // Stop any pending operations +template bool LR11x0Interface::sleep() { + // \todo Display actual typename of the adapter, not just `LR11x0` + LOG_DEBUG("LR11x0 entering sleep mode"); + setStandby(); // Stop any pending operations - // turn off TCXO if it was powered - lora.setTCXO(0); + // turn off TCXO if it was powered + lora.setTCXO(0); - // put chipset into sleep mode (we've already disabled interrupts by now) - bool keepConfig = false; - lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = false; + lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed #ifdef LR11X0_POWER_EN - digitalWrite(LR11X0_POWER_EN, LOW); + digitalWrite(LR11X0_POWER_EN, LOW); #endif - return true; + return true; } #endif diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 840184bbf..b94c0077a 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -6,66 +6,64 @@ * \brief Adapter for LR11x0 radio family. Implements common logic for child classes. * \tparam T RadioLib module type for LR11x0: SX1262, SX1268. */ -template class LR11x0Interface : public RadioLibInterface -{ - public: - LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +template class LR11x0Interface : public RadioLibInterface { +public: + LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - bool isIRQPending() override { return lora.getIrqFlags() != 0; } + bool isIRQPending() override { return lora.getIrqFlags() != 0; } - protected: - /** - * Specific module instance - */ - T lora; +protected: + /** + * Specific module instance + */ + T lora; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; #endif \ No newline at end of file diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index eb5ac5109..ad85685e1 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -8,154 +8,139 @@ #include "PointerQueue.h" #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP -template class Allocator -{ +template class Allocator { - public: - Allocator() : deleter([this](T *p) { this->release(p); }) {} - virtual ~Allocator() {} +public: + Allocator() : deleter([this](T *p) { this->release(p); }) {} + virtual ~Allocator() {} - /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available - /// Note: this method is safe to call from regular OR ISR code - T *allocZeroed() - { - T *p = allocZeroed(0); - if (!p) { - LOG_WARN("Failed to allocate zeroed memory"); - } - return p; + /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available + /// Note: this method is safe to call from regular OR ISR code + T *allocZeroed() { + T *p = allocZeroed(0); + if (!p) { + LOG_WARN("Failed to allocate zeroed memory"); + } + return p; + } + + /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you + /// probably don't want this version). + T *allocZeroed(TickType_t maxWait) { + T *p = alloc(maxWait); + + if (p) + memset(p, 0, sizeof(T)); + return p; + } + + /// Return a queable object which is a copy of some other object + T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { + T *p = alloc(maxWait); + if (!p) { + LOG_WARN("Failed to allocate memory for copy"); + return nullptr; } - /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably - /// don't want this version). - T *allocZeroed(TickType_t maxWait) - { - T *p = alloc(maxWait); + *p = src; + return p; + } - if (p) - memset(p, 0, sizeof(T)); - return p; - } + /// Variations of the above methods that return std::unique_ptr instead of raw pointers. + using UniqueAllocation = std::unique_ptr &>; + /// Return a queable object which has been prefilled with zeros. + /// std::unique_ptr wrapped variant of allocZeroed(). + UniqueAllocation allocUniqueZeroed() { return UniqueAllocation(allocZeroed(), deleter); } + /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you + /// probably don't want this version). std::unique_ptr wrapped variant of allocZeroed(TickType_t maxWait). + UniqueAllocation allocUniqueZeroed(TickType_t maxWait) { return UniqueAllocation(allocZeroed(maxWait), deleter); } + /// Return a queable object which is a copy of some other object + /// std::unique_ptr wrapped variant of allocCopy(const T &src, TickType_t maxWait). + UniqueAllocation allocUniqueCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { return UniqueAllocation(allocCopy(src, maxWait), deleter); } - /// Return a queable object which is a copy of some other object - T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) - { - T *p = alloc(maxWait); - if (!p) { - LOG_WARN("Failed to allocate memory for copy"); - return nullptr; - } + /// Return a buffer for use by others + virtual void release(T *p) = 0; - *p = src; - return p; - } +protected: + // Alloc some storage + virtual T *alloc(TickType_t maxWait) = 0; - /// Variations of the above methods that return std::unique_ptr instead of raw pointers. - using UniqueAllocation = std::unique_ptr &>; - /// Return a queable object which has been prefilled with zeros. - /// std::unique_ptr wrapped variant of allocZeroed(). - UniqueAllocation allocUniqueZeroed() { return UniqueAllocation(allocZeroed(), deleter); } - /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably - /// don't want this version). - /// std::unique_ptr wrapped variant of allocZeroed(TickType_t maxWait). - UniqueAllocation allocUniqueZeroed(TickType_t maxWait) { return UniqueAllocation(allocZeroed(maxWait), deleter); } - /// Return a queable object which is a copy of some other object - /// std::unique_ptr wrapped variant of allocCopy(const T &src, TickType_t maxWait). - UniqueAllocation allocUniqueCopy(const T &src, TickType_t maxWait = portMAX_DELAY) - { - return UniqueAllocation(allocCopy(src, maxWait), deleter); - } - - /// Return a buffer for use by others - virtual void release(T *p) = 0; - - protected: - // Alloc some storage - virtual T *alloc(TickType_t maxWait) = 0; - - private: - // std::unique_ptr Deleter function; calls release(). - const std::function deleter; +private: + // std::unique_ptr Deleter function; calls release(). + const std::function deleter; }; /** * An allocator that just uses regular free/malloc */ -template class MemoryDynamic : public Allocator -{ - public: - /// Return a buffer for use by others - virtual void release(T *p) override - { - if (p == nullptr) - return; +template class MemoryDynamic : public Allocator { +public: + /// Return a buffer for use by others + virtual void release(T *p) override { + if (p == nullptr) + return; - LOG_HEAP("Freeing 0x%x", p); + LOG_HEAP("Freeing 0x%x", p); - free(p); - } + free(p); + } - protected: - // Alloc some storage - virtual T *alloc(TickType_t maxWait) override - { - T *p = (T *)malloc(sizeof(T)); - assert(p); - return p; - } +protected: + // Alloc some storage + virtual T *alloc(TickType_t maxWait) override { + T *p = (T *)malloc(sizeof(T)); + assert(p); + return p; + } }; /** * A static memory pool that uses a fixed buffer instead of heap allocation */ -template class MemoryPool : public Allocator -{ - private: - T pool[MaxSize]; - bool used[MaxSize]; +template class MemoryPool : public Allocator { +private: + T pool[MaxSize]; + bool used[MaxSize]; - public: - MemoryPool() : pool{}, used{} - { - // Arrays are now zero-initialized by member initializer list - // pool array: all elements are default-constructed (zero for POD types) - // used array: all elements are false (zero-initialized) +public: + MemoryPool() : pool{}, used{} { + // Arrays are now zero-initialized by member initializer list + // pool array: all elements are default-constructed (zero for POD types) + // used array: all elements are false (zero-initialized) + } + + /// Return a buffer for use by others + virtual void release(T *p) override { + if (!p) { + LOG_DEBUG("Failed to release memory, pointer is null"); + return; } - /// Return a buffer for use by others - virtual void release(T *p) override - { - if (!p) { - LOG_DEBUG("Failed to release memory, pointer is null"); - return; - } + // Find the index of this pointer in our pool + int index = p - pool; + if (index >= 0 && index < MaxSize) { + assert(used[index]); // Should be marked as used + used[index] = false; + LOG_HEAP("Released static pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from our pool!", p); + } + } - // Find the index of this pointer in our pool - int index = p - pool; - if (index >= 0 && index < MaxSize) { - assert(used[index]); // Should be marked as used - used[index] = false; - LOG_HEAP("Released static pool item %d at 0x%x", index, p); - } else { - LOG_WARN("Pointer 0x%x not from our pool!", p); - } +protected: + // Alloc some storage from our static pool + virtual T *alloc(TickType_t maxWait) override { + // Find first free slot + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } } - protected: - // Alloc some storage from our static pool - virtual T *alloc(TickType_t maxWait) override - { - // Find first free slot - for (int i = 0; i < MaxSize; i++) { - if (!used[i]) { - used[i] = true; - LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); - return &pool[i]; - } - } - - // No free slots available - return nullptr instead of asserting - LOG_WARN("No free slots available in static memory pool!"); - return nullptr; - } + // No free slots available - return nullptr instead of asserting + LOG_WARN("No free slots available in static memory pool!"); + return nullptr; + } }; diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 83b64a873..a4c0e797a 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -18,297 +18,278 @@ uint8_t MeshModule::numPeriodicModules = 0; */ meshtastic_MeshPacket *MeshModule::currentReply; -MeshModule::MeshModule(const char *_name) : name(_name) -{ - // Can't trust static initializer order, so we check each time - if (!modules) - modules = new std::vector(); +MeshModule::MeshModule(const char *_name) : name(_name) { + // Can't trust static initializer order, so we check each time + if (!modules) + modules = new std::vector(); - modules->push_back(this); + modules->push_back(this); } void MeshModule::setup() {} -MeshModule::~MeshModule() -{ - auto it = std::find(modules->begin(), modules->end(), this); - assert(it != modules->end()); - modules->erase(it); +MeshModule::~MeshModule() { + auto it = std::find(modules->begin(), modules->end(), this); + assert(it != modules->end()); + modules->erase(it); } // ⚠️ **Only call once** to set the initial delay before a module starts broadcasting periodically -int32_t MeshModule::setStartDelay() -{ - int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS; - numPeriodicModules++; +int32_t MeshModule::setStartDelay() { + int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS; + numPeriodicModules++; - return startDelay; + return startDelay; } -meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, - uint8_t hopLimit) -{ - meshtastic_Routing c = meshtastic_Routing_init_default; +meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { + meshtastic_Routing c = meshtastic_Routing_init_default; - c.error_reason = err; - c.which_variant = meshtastic_Routing_error_reason_tag; + c.error_reason = err; + c.which_variant = meshtastic_Routing_error_reason_tag; - // Now that we have moded sendAckNak up one level into the class hierarchy we can no longer assume we are a RoutingModule - // So we manually call pb_encode_to_bytes and specify routing port number - // auto p = allocDataProtobuf(c); - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_ROUTING_APP; - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Routing_msg, &c); + // Now that we have moded sendAckNak up one level into the class hierarchy we can no longer assume we are a + // RoutingModule So we manually call pb_encode_to_bytes and specify routing port number auto p = allocDataProtobuf(c); + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_ROUTING_APP; + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Routing_msg, &c); - p->priority = meshtastic_MeshPacket_Priority_ACK; + p->priority = meshtastic_MeshPacket_Priority_ACK; - p->hop_limit = hopLimit; // Flood ACK back to original sender - p->to = to; - p->decoded.request_id = idFrom; - p->channel = chIndex; - if (err != meshtastic_Routing_Error_NONE) - LOG_WARN("Alloc an err=%d,to=0x%x,idFrom=0x%x,id=0x%x", err, to, idFrom, p->id); + p->hop_limit = hopLimit; // Flood ACK back to original sender + p->to = to; + p->decoded.request_id = idFrom; + p->channel = chIndex; + if (err != meshtastic_Routing_Error_NONE) + LOG_WARN("Alloc an err=%d,to=0x%x,idFrom=0x%x,id=0x%x", err, to, idFrom, p->id); - return p; + return p; } -meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p) -{ - // If the original packet couldn't be decoded, use the primary channel - uint8_t channelIndex = - p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? p->channel : channels.getPrimaryIndex(); - auto r = allocAckNak(err, getFrom(p), p->id, channelIndex); +meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p) { + // If the original packet couldn't be decoded, use the primary channel + uint8_t channelIndex = p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? p->channel : channels.getPrimaryIndex(); + auto r = allocAckNak(err, getFrom(p), p->id, channelIndex); - setReplyTo(r, *p); + setReplyTo(r, *p); - return r; + return r; } -void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) -{ - // LOG_DEBUG("In call modules"); - bool moduleFound = false; +void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) { + // LOG_DEBUG("In call modules"); + bool moduleFound = false; - // We now allow **encrypted** packets to pass through the modules - bool isDecoded = mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag; + // We now allow **encrypted** packets to pass through the modules + bool isDecoded = mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag; - currentReply = NULL; // No reply yet + currentReply = NULL; // No reply yet - bool ignoreRequest = false; // No module asked to ignore the request yet + bool ignoreRequest = false; // No module asked to ignore the request yet - // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets - auto ourNodeNum = nodeDB->getNodeNum(); - bool toUs = isBroadcast(mp.to) || isToUs(&mp); + // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets + auto ourNodeNum = nodeDB->getNodeNum(); + bool toUs = isBroadcast(mp.to) || isToUs(&mp); - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; - pi.currentRequest = ∓ + pi.currentRequest = ∓ - /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) - bool wantsPacket = (isDecoded || pi.encryptedOk) && (pi.isPromiscuous || toUs) && pi.wantPacket(&mp); + /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) + bool wantsPacket = (isDecoded || pi.encryptedOk) && (pi.isPromiscuous || toUs) && pi.wantPacket(&mp); - if ((src == RX_SRC_LOCAL) && !(pi.loopbackOk)) { - // new case, monitor separately for now, then FIXME merge above - wantsPacket = false; - } - - assert(!pi.myReply); // If it is !null it means we have a bug, because it should have been sent the previous time - - if (wantsPacket) { - LOG_DEBUG("Module '%s' wantsPacket=%d", pi.name, wantsPacket); - - moduleFound = true; - - /// received channel (or NULL if not decoded) - meshtastic_Channel *ch = isDecoded ? &channels.getByIndex(mp.channel) : NULL; - - /// Is the channel this packet arrived on acceptable? (security check) - /// Note: we can't know channel names for encrypted packets, so those are NEVER sent to boundChannel modules - - /// Also: if a packet comes in on the local PC interface, we don't check for bound channels, because it is TRUSTED and - /// it needs to to be able to fetch the initial admin packets without yet knowing any channels. - - bool rxChannelOk = !pi.boundChannel || (mp.from == 0) || (ch && strcasecmp(ch->settings.name, pi.boundChannel) == 0); - - if (!rxChannelOk) { - // no one should have already replied! - assert(!currentReply); - - if (isDecoded && mp.decoded.want_response) { - printPacket("packet on wrong channel, returning error", &mp); - currentReply = pi.allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); - } else - printPacket("packet on wrong channel, but can't respond", &mp); - } else { - ProcessMessage handled = pi.handleReceived(mp); - - pi.alterReceived(mp); - - // Possibly send replies (but only if the message was directed to us specifically, i.e. not for promiscious - // sniffing) also: we only let the one module send a reply, once that happens, remaining modules are not - // considered - - // NOTE: we send a reply *even if the (non broadcast) request was from us* which is unfortunate but necessary - // because currently when the phone sends things, it sends things using the local node ID as the from address. A - // better solution (FIXME) would be to let phones have their own distinct addresses and we 'route' to them like - // any other node. - if (isDecoded && mp.decoded.want_response && toUs && (!isFromUs(&mp) || isToUs(&mp)) && !currentReply) { - pi.sendResponse(mp); - ignoreRequest = ignoreRequest || pi.ignoreRequest; // If at least one module asks it, we may ignore a request - LOG_INFO("Asked module '%s' to send a response", pi.name); - } else { - LOG_DEBUG("Module '%s' considered", pi.name); - } - - // If the requester didn't ask for a response we might need to discard unused replies to prevent memory leaks - if (pi.myReply) { - LOG_DEBUG("Discard an unneeded response"); - packetPool.release(pi.myReply); - pi.myReply = NULL; - } - - if (handled == ProcessMessage::STOP) { - LOG_DEBUG("Module '%s' handled and skipped other processing", pi.name); - break; - } - } - } - - pi.currentRequest = NULL; + if ((src == RX_SRC_LOCAL) && !(pi.loopbackOk)) { + // new case, monitor separately for now, then FIXME merge above + wantsPacket = false; } - if (isDecoded && mp.decoded.want_response && toUs) { - if (currentReply) { - printPacket("Send response", currentReply); - service->sendToMesh(currentReply); - currentReply = NULL; - } else if (mp.from != ourNodeNum && !ignoreRequest) { - // Note: if the message started with the local node or a module asked to ignore the request, we don't want to send a - // no response reply + assert(!pi.myReply); // If it is !null it means we have a bug, because it should have been sent the previous time - // No one wanted to reply to this request, tell the requster that happened - LOG_DEBUG("No one responded, send a nak"); + if (wantsPacket) { + LOG_DEBUG("Module '%s' wantsPacket=%d", pi.name, wantsPacket); - // SECURITY NOTE! I considered sending back a different error code if we didn't find the psk (i.e. !isDecoded) - // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" vs - // bad. - routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, - routingModule->getHopLimitForResponse(mp)); + moduleFound = true; + + /// received channel (or NULL if not decoded) + meshtastic_Channel *ch = isDecoded ? &channels.getByIndex(mp.channel) : NULL; + + /// Is the channel this packet arrived on acceptable? (security check) + /// Note: we can't know channel names for encrypted packets, so those are NEVER sent to boundChannel modules + + /// Also: if a packet comes in on the local PC interface, we don't check for bound channels, because it is TRUSTED + /// and it needs to to be able to fetch the initial admin packets without yet knowing any channels. + + bool rxChannelOk = !pi.boundChannel || (mp.from == 0) || (ch && strcasecmp(ch->settings.name, pi.boundChannel) == 0); + + if (!rxChannelOk) { + // no one should have already replied! + assert(!currentReply); + + if (isDecoded && mp.decoded.want_response) { + printPacket("packet on wrong channel, returning error", &mp); + currentReply = pi.allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + } else + printPacket("packet on wrong channel, but can't respond", &mp); + } else { + ProcessMessage handled = pi.handleReceived(mp); + + pi.alterReceived(mp); + + // Possibly send replies (but only if the message was directed to us specifically, i.e. not for promiscious + // sniffing) also: we only let the one module send a reply, once that happens, remaining modules are not + // considered + + // NOTE: we send a reply *even if the (non broadcast) request was from us* which is unfortunate but necessary + // because currently when the phone sends things, it sends things using the local node ID as the from address. A + // better solution (FIXME) would be to let phones have their own distinct addresses and we 'route' to them like + // any other node. + if (isDecoded && mp.decoded.want_response && toUs && (!isFromUs(&mp) || isToUs(&mp)) && !currentReply) { + pi.sendResponse(mp); + ignoreRequest = ignoreRequest || pi.ignoreRequest; // If at least one module asks it, we may ignore a request + LOG_INFO("Asked module '%s' to send a response", pi.name); + } else { + LOG_DEBUG("Module '%s' considered", pi.name); } + + // If the requester didn't ask for a response we might need to discard unused replies to prevent memory leaks + if (pi.myReply) { + LOG_DEBUG("Discard an unneeded response"); + packetPool.release(pi.myReply); + pi.myReply = NULL; + } + + if (handled == ProcessMessage::STOP) { + LOG_DEBUG("Module '%s' handled and skipped other processing", pi.name); + break; + } + } } - if (!moduleFound && isDecoded) { - LOG_DEBUG("No modules interested in portnum=%d, src=%s", mp.decoded.portnum, (src == RX_SRC_LOCAL) ? "LOCAL" : "REMOTE"); + pi.currentRequest = NULL; + } + + if (isDecoded && mp.decoded.want_response && toUs) { + if (currentReply) { + printPacket("Send response", currentReply); + service->sendToMesh(currentReply); + currentReply = NULL; + } else if (mp.from != ourNodeNum && !ignoreRequest) { + // Note: if the message started with the local node or a module asked to ignore the request, we don't want to send + // a no response reply + + // No one wanted to reply to this request, tell the requster that happened + LOG_DEBUG("No one responded, send a nak"); + + // SECURITY NOTE! I considered sending back a different error code if we didn't find the psk (i.e. !isDecoded) + // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" + // vs bad. + routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, routingModule->getHopLimitForResponse(mp)); } + } + + if (!moduleFound && isDecoded) { + LOG_DEBUG("No modules interested in portnum=%d, src=%s", mp.decoded.portnum, (src == RX_SRC_LOCAL) ? "LOCAL" : "REMOTE"); + } } -meshtastic_MeshPacket *MeshModule::allocReply() -{ - auto r = myReply; - myReply = NULL; // Only use each reply once - return r; +meshtastic_MeshPacket *MeshModule::allocReply() { + auto r = myReply; + myReply = NULL; // Only use each reply once + return r; } /** Messages can be received that have the want_response bit set. If set, this callback will be invoked * so that subclasses can (optionally) send a response back to the original sender. Implementing this method * is optional */ -void MeshModule::sendResponse(const meshtastic_MeshPacket &req) -{ - auto r = allocReply(); - if (r) { - setReplyTo(r, req); - currentReply = r; - } else { - // Ignore - this is now expected behavior for routing module (because it ignores some replies) - // LOG_WARN("Client requested response but this module did not provide"); - } +void MeshModule::sendResponse(const meshtastic_MeshPacket &req) { + auto r = allocReply(); + if (r) { + setReplyTo(r, req); + currentReply = r; + } else { + // Ignore - this is now expected behavior for routing module (because it ignores some replies) + // LOG_WARN("Client requested response but this module did not provide"); + } } /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet * This ensures that if the request packet was sent reliably, the reply is sent that way as well. */ -void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) -{ - assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now - p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 - p->channel = to.channel; // Use the same channel that the request came in on - p->hop_limit = routingModule->getHopLimitForResponse(to); +void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) { + assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now + p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 + p->channel = to.channel; // Use the same channel that the request came in on + p->hop_limit = routingModule->getHopLimitForResponse(to); - // No need for an ack if we are just delivering locally (it just generates an ignored ack) - p->want_ack = (to.from != 0) ? to.want_ack : false; - if (p->priority == meshtastic_MeshPacket_Priority_UNSET) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - p->decoded.request_id = to.id; + // No need for an ack if we are just delivering locally (it just generates an ignored ack) + p->want_ack = (to.from != 0) ? to.want_ack : false; + if (p->priority == meshtastic_MeshPacket_Priority_UNSET) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + p->decoded.request_id = to.id; } -std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) -{ - std::vector modulesWithUIFrames; +std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) { + std::vector modulesWithUIFrames; - // Fill with nullptr up to startIndex - modulesWithUIFrames.resize(startIndex, nullptr); + // Fill with nullptr up to startIndex + modulesWithUIFrames.resize(startIndex, nullptr); - if (modules) { - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; - if (pi.wantUIFrame()) { - LOG_DEBUG("%s wants a UI Frame", pi.name); - modulesWithUIFrames.push_back(&pi); - } - } + if (modules) { + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + if (pi.wantUIFrame()) { + LOG_DEBUG("%s wants a UI Frame", pi.name); + modulesWithUIFrames.push_back(&pi); + } } - return modulesWithUIFrames; + } + return modulesWithUIFrames; } -void MeshModule::observeUIEvents(Observer *observer) -{ - if (modules) { - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; - Observable *observable = pi.getUIFrameObservable(); - if (observable != NULL) { - LOG_DEBUG("%s wants a UI Frame", pi.name); - observer->observe(observable); - } - } +void MeshModule::observeUIEvents(Observer *observer) { + if (modules) { + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + Observable *observable = pi.getUIFrameObservable(); + if (observable != NULL) { + LOG_DEBUG("%s wants a UI Frame", pi.name); + observer->observe(observable); + } } + } } -AdminMessageHandleResult MeshModule::handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) -{ - AdminMessageHandleResult handled = AdminMessageHandleResult::NOT_HANDLED; - if (modules) { - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; - AdminMessageHandleResult h = pi.handleAdminMessageForModule(mp, request, response); - if (h == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { - // In case we have a response it always has priority. - LOG_DEBUG("Reply prepared by module '%s' of variant: %d", pi.name, response->which_payload_variant); - handled = h; - } else if ((handled != AdminMessageHandleResult::HANDLED_WITH_RESPONSE) && (h == AdminMessageHandleResult::HANDLED)) { - // In case the message is handled it should be populated, but will not overwrite - // a result with response. - handled = h; - } - } +AdminMessageHandleResult MeshModule::handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + AdminMessageHandleResult handled = AdminMessageHandleResult::NOT_HANDLED; + if (modules) { + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + AdminMessageHandleResult h = pi.handleAdminMessageForModule(mp, request, response); + if (h == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + // In case we have a response it always has priority. + LOG_DEBUG("Reply prepared by module '%s' of variant: %d", pi.name, response->which_payload_variant); + handled = h; + } else if ((handled != AdminMessageHandleResult::HANDLED_WITH_RESPONSE) && (h == AdminMessageHandleResult::HANDLED)) { + // In case the message is handled it should be populated, but will not overwrite + // a result with response. + handled = h; + } } - return handled; + } + return handled; } #if HAS_SCREEN // Would our module like its frame to be focused after Screen::setFrames has regenerated the list of frames? // Only considered if setFrames is triggered by a UIFrameEvent -bool MeshModule::isRequestingFocus() -{ - if (_requestingFocus) { - _requestingFocus = false; // Consume the request - return true; - } else - return false; +bool MeshModule::isRequestingFocus() { + if (_requestingFocus) { + _requestingFocus = false; // Consume the request + return true; + } else + return false; } #endif \ No newline at end of file diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index 63f401d18..4fe04d1c7 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -19,8 +19,8 @@ * Use ProcessMessage::STOP to stop further message processing. */ enum class ProcessMessage { - CONTINUE = 0, - STOP = 1, + CONTINUE = 0, + STOP = 1, }; /** @@ -30,197 +30,193 @@ enum class ProcessMessage { * should be returned. */ enum class AdminMessageHandleResult { - NOT_HANDLED = 0, - HANDLED = 1, - HANDLED_WITH_RESPONSE = 2, + NOT_HANDLED = 0, + HANDLED = 1, + HANDLED_WITH_RESPONSE = 2, }; /* * This struct is used by Screen to figure out whether screen frame should be updated. */ struct UIFrameEvent { - // What do we actually want to happen? - enum Action { - REDRAW_ONLY, // Don't change which frames are show, just redraw, asap - REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() - REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout - SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen - } action = REDRAW_ONLY; + // What do we actually want to happen? + enum Action { + REDRAW_ONLY, // Don't change which frames are show, just redraw, asap + REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() + REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout + SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen + } action = REDRAW_ONLY; - // We might want to pass additional data inside this struct at some point + // We might want to pass additional data inside this struct at some point }; /** A baseclass for any mesh "module". * * A module allows you to add new features to meshtastic device code, without needing to know messaging details. * - * A key concept for this is that your module should use a particular "portnum" for each message type you want to receive - * and handle. + * A key concept for this is that your module should use a particular "portnum" for each message type you want to + * receive and handle. * * Internally we use modules to implement the core meshtastic text messaging and gps position sharing features. You * can use these classes as examples for how to write your own custom module. See here: (FIXME) */ -class MeshModule -{ - static std::vector *modules; +class MeshModule { + static std::vector *modules; - public: - /** Constructor - * name is for debugging output - */ - MeshModule(const char *_name); +public: + /** Constructor + * name is for debugging output + */ + MeshModule(const char *_name); - virtual ~MeshModule(); + virtual ~MeshModule(); - /** For use only by MeshService - */ - static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); + /** For use only by MeshService + */ + static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); - static std::vector GetMeshModulesWithUIFrames(int startIndex); - static void observeUIEvents(Observer *observer); - static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response); + static std::vector GetMeshModulesWithUIFrames(int startIndex); + static void observeUIEvents(Observer *observer); + static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response); #if HAS_SCREEN - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } - virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset - virtual bool interceptingKeyboardInput() { return false; } // Can screen use keyboard for nav, or is module handling input? + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } + virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset + virtual bool interceptingKeyboardInput() { return false; } // Can screen use keyboard for nav, or is module handling input? #endif - protected: - const char *name; +protected: + const char *name; - /** Most modules only care about packets that are destined for their node (i.e. broadcasts or has their node as the specific - recipient) But some plugs might want to 'sniff' packets that are merely being routed (passing through the current node). Those - modules can set this to true and their handleReceived() will be called for every packet. - */ - bool isPromiscuous = false; + /** Most modules only care about packets that are destined for their node (i.e. broadcasts or has their node as the + specific recipient) But some plugs might want to 'sniff' packets that are merely being routed (passing through the + current node). Those modules can set this to true and their handleReceived() will be called for every packet. + */ + bool isPromiscuous = false; - /** Also receive a copy of LOCALLY GENERATED messages - most modules should leave - * this setting disabled - see issue #877 */ - bool loopbackOk = false; + /** Also receive a copy of LOCALLY GENERATED messages - most modules should leave + * this setting disabled - see issue #877 */ + bool loopbackOk = false; - /** Most modules only understand decrypted packets. For modules that also want to see encrypted packets, they should set this - * flag */ - bool encryptedOk = false; + /** Most modules only understand decrypted packets. For modules that also want to see encrypted packets, they should + * set this flag */ + bool encryptedOk = false; - /* We allow modules to ignore a request without sending an error if they have a specific reason for it. */ - bool ignoreRequest = false; + /* We allow modules to ignore a request without sending an error if they have a specific reason for it. */ + bool ignoreRequest = false; - /** If a bound channel name is set, we will only accept received packets that come in on that channel. - * A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface - * are allowed on any channel (this lets the local user do anything). - * - * We will send responses on the same channel that the request arrived on. - */ - const char *boundChannel = NULL; + /** If a bound channel name is set, we will only accept received packets that come in on that channel. + * A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface + * are allowed on any channel (this lets the local user do anything). + * + * We will send responses on the same channel that the request arrived on. + */ + const char *boundChannel = NULL; - /** - * If this module is currently handling a request currentRequest will be preset - * to the packet with the request. This is mostly useful for reply handlers. - * - * Note: this can be static because we are guaranteed to be processing only one - * plumodulegin at a time. - */ - static const meshtastic_MeshPacket *currentRequest; + /** + * If this module is currently handling a request currentRequest will be preset + * to the packet with the request. This is mostly useful for reply handlers. + * + * Note: this can be static because we are guaranteed to be processing only one + * plumodulegin at a time. + */ + static const meshtastic_MeshPacket *currentRequest; - // We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time - static uint8_t numPeriodicModules; + // We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time + static uint8_t numPeriodicModules; - // Set the start delay for module that broadcasts periodically - int32_t setStartDelay(); + // Set the start delay for module that broadcasts periodically + int32_t setStartDelay(); - /** - * If your handler wants to send a response, simply set currentReply and it will be sent at the end of response handling. - */ - meshtastic_MeshPacket *myReply = NULL; + /** + * If your handler wants to send a response, simply set currentReply and it will be sent at the end of response + * handling. + */ + meshtastic_MeshPacket *myReply = NULL; - /** - * Initialize your module. This setup function is called once after all hardware and mesh protocol layers have - * been initialized - */ - virtual void setup(); + /** + * Initialize your module. This setup function is called once after all hardware and mesh protocol layers have + * been initialized + */ + virtual void setup(); - /** - * @return true if you want to receive the specified portnum - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) = 0; + /** + * @return true if you want to receive the specified portnum + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) = 0; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) { return ProcessMessage::CONTINUE; } + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) { return ProcessMessage::CONTINUE; } - /** Called to change a particular incoming message - This allows the module to change the message before it is passed through the rest of the call-chain. - */ - virtual void alterReceived(meshtastic_MeshPacket &mp) {} + /** Called to change a particular incoming message + This allows the module to change the message before it is passed through the rest of the call-chain. + */ + virtual void alterReceived(meshtastic_MeshPacket &mp) {} - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. - * - * Note: most implementers don't need to override this, instead: If while handling a request you have a reply, just set - * the protected reply field in this instance. - * */ - virtual meshtastic_MeshPacket *allocReply(); + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. + * + * Note: most implementers don't need to override this, instead: If while handling a request you have a reply, just + * set the protected reply field in this instance. + * */ + virtual meshtastic_MeshPacket *allocReply(); - /*** - * @return true if you want to be alloced a UI screen frame - */ - virtual bool wantUIFrame() { return false; } - virtual Observable *getUIFrameObservable() { return NULL; } + /*** + * @return true if you want to be alloced a UI screen frame + */ + virtual bool wantUIFrame() { return false; } + virtual Observable *getUIFrameObservable() { return NULL; } - meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, - uint8_t hopLimit = 0); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0); - /// Send an error response for the specified packet. - meshtastic_MeshPacket *allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p); + /// Send an error response for the specified packet. + meshtastic_MeshPacket *allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p); - /** - * @brief An admin message arrived to AdminModule. Module was asked whether it want to handle the request. - * - * @param mp The mesh packet arrived. - * @param request The AdminMessage request extracted from the packet. - * @param response The prepared response - * @return AdminMessageHandleResult - * HANDLED if message was handled - * HANDLED_WITH_RESPONSE if a response is also prepared and to be sent. - */ - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) - { - return AdminMessageHandleResult::NOT_HANDLED; - }; + /** + * @brief An admin message arrived to AdminModule. Module was asked whether it want to handle the request. + * + * @param mp The mesh packet arrived. + * @param request The AdminMessage request extracted from the packet. + * @param response The prepared response + * @return AdminMessageHandleResult + * HANDLED if message was handled + * HANDLED_WITH_RESPONSE if a response is also prepared and to be sent. + */ + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + return AdminMessageHandleResult::NOT_HANDLED; + }; #if HAS_SCREEN - /** Request that our module's screen frame be focused when Screen::setFrames runs - * Only considered if Screen::setFrames is triggered via a UIFrameEvent - * - * Having this as a separate call, instead of part of the UIFrameEvent, allows the module to delay decision - * until drawFrame() is called. This required less restructuring. - */ - bool _requestingFocus = false; - void requestFocus() { _requestingFocus = true; } + /** Request that our module's screen frame be focused when Screen::setFrames runs + * Only considered if Screen::setFrames is triggered via a UIFrameEvent + * + * Having this as a separate call, instead of part of the UIFrameEvent, allows the module to delay decision + * until drawFrame() is called. This required less restructuring. + */ + bool _requestingFocus = false; + void requestFocus() { _requestingFocus = true; } #else - void requestFocus(){}; // No-op + void requestFocus(){}; // No-op #endif - private: - /** - * If any of the current chain of modules has already sent a reply, it will be here. This is useful to allow - * the RoutingModule to avoid sending redundant acks - */ - static meshtastic_MeshPacket *currentReply; +private: + /** + * If any of the current chain of modules has already sent a reply, it will be here. This is useful to allow + * the RoutingModule to avoid sending redundant acks + */ + static meshtastic_MeshPacket *currentReply; - friend class ReliableRouter; + friend class ReliableRouter; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. This method calls allocReply() - * to generate the reply message, and if !NULL that message will be delivered to whoever sent req - */ - void sendResponse(const meshtastic_MeshPacket &req); + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. This method calls allocReply() + * to generate the reply message, and if !NULL that message will be delivered to whoever sent req + */ + void sendResponse(const meshtastic_MeshPacket &req); }; /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index cbea85c62..8ce629c6b 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -6,184 +6,168 @@ #include /// @return the priority of the specified packet -inline uint32_t getPriority(const meshtastic_MeshPacket *p) -{ - auto pri = p->priority; - return pri; +inline uint32_t getPriority(const meshtastic_MeshPacket *p) { + auto pri = p->priority; + return pri; } /// @return "true" if "p1" is ordered before "p2" -bool CompareMeshPacketFunc(const meshtastic_MeshPacket *p1, const meshtastic_MeshPacket *p2) -{ - assert(p1 && p2); +bool CompareMeshPacketFunc(const meshtastic_MeshPacket *p1, const meshtastic_MeshPacket *p2) { + assert(p1 && p2); - // If one packet is in the late transmit window, prefer the other one - if ((bool)p1->tx_after != (bool)p2->tx_after) { - return !p1->tx_after; - } + // If one packet is in the late transmit window, prefer the other one + if ((bool)p1->tx_after != (bool)p2->tx_after) { + return !p1->tx_after; + } - auto p1p = getPriority(p1), p2p = getPriority(p2); - // If priorities differ, use that - // for equal priorities, prefer packets already on mesh. - return (p1p != p2p) ? (p1p > p2p) : (!isFromUs(p1) && isFromUs(p2)); + auto p1p = getPriority(p1), p2p = getPriority(p2); + // If priorities differ, use that + // for equal priorities, prefer packets already on mesh. + return (p1p != p2p) ? (p1p > p2p) : (!isFromUs(p1) && isFromUs(p2)); } MeshPacketQueue::MeshPacketQueue(size_t _maxLen) : maxLen(_maxLen) {} -bool MeshPacketQueue::empty() -{ - return queue.empty(); -} +bool MeshPacketQueue::empty() { return queue.empty(); } /** * Some clients might not properly set priority, therefore we fix it here. */ -void fixPriority(meshtastic_MeshPacket *p) -{ - // We might receive acks from other nodes (and since generated remotely, they won't have priority assigned. Check for that - // and fix it - if (p->priority == meshtastic_MeshPacket_Priority_UNSET) { - // if a reliable message give a bit higher default priority - p->priority = (p->want_ack ? meshtastic_MeshPacket_Priority_RELIABLE : meshtastic_MeshPacket_Priority_DEFAULT); - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - // if acks/naks give very high priority - if (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) { - p->priority = meshtastic_MeshPacket_Priority_ACK; - // if text or admin, give high priority - } else if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || - p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { - p->priority = meshtastic_MeshPacket_Priority_HIGH; - // if it is a response, give higher priority to let it arrive early and stop the request being relayed - } else if (p->decoded.request_id != 0) { - p->priority = meshtastic_MeshPacket_Priority_RESPONSE; - // Also if we want a response, give a bit higher priority - } else if (p->decoded.want_response) { - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - } - } +void fixPriority(meshtastic_MeshPacket *p) { + // We might receive acks from other nodes (and since generated remotely, they won't have priority assigned. Check for + // that and fix it + if (p->priority == meshtastic_MeshPacket_Priority_UNSET) { + // if a reliable message give a bit higher default priority + p->priority = (p->want_ack ? meshtastic_MeshPacket_Priority_RELIABLE : meshtastic_MeshPacket_Priority_DEFAULT); + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + // if acks/naks give very high priority + if (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) { + p->priority = meshtastic_MeshPacket_Priority_ACK; + // if text or admin, give high priority + } else if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { + p->priority = meshtastic_MeshPacket_Priority_HIGH; + // if it is a response, give higher priority to let it arrive early and stop the request being relayed + } else if (p->decoded.request_id != 0) { + p->priority = meshtastic_MeshPacket_Priority_RESPONSE; + // Also if we want a response, give a bit higher priority + } else if (p->decoded.want_response) { + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + } } + } } /** enqueue a packet, return false if full */ -bool MeshPacketQueue::enqueue(meshtastic_MeshPacket *p, bool *dropped) -{ - // no space - try to replace a lower priority packet in the queue - if (queue.size() >= maxLen) { - bool replaced = replaceLowerPriorityPacket(p); - if (!replaced) { - LOG_WARN("TX queue is full, and there is no lower-priority packet available to evict in favour of 0x%08x", p->id); - } - if (dropped) { - *dropped = true; - } - return replaced; +bool MeshPacketQueue::enqueue(meshtastic_MeshPacket *p, bool *dropped) { + // no space - try to replace a lower priority packet in the queue + if (queue.size() >= maxLen) { + bool replaced = replaceLowerPriorityPacket(p); + if (!replaced) { + LOG_WARN("TX queue is full, and there is no lower-priority packet available to evict in favour of 0x%08x", p->id); } - if (dropped) { - *dropped = false; + *dropped = true; } + return replaced; + } - // Find the correct position using upper_bound to maintain a stable order - auto it = std::upper_bound(queue.begin(), queue.end(), p, CompareMeshPacketFunc); - queue.insert(it, p); // Insert packet at the found position - return true; + if (dropped) { + *dropped = false; + } + + // Find the correct position using upper_bound to maintain a stable order + auto it = std::upper_bound(queue.begin(), queue.end(), p, CompareMeshPacketFunc); + queue.insert(it, p); // Insert packet at the found position + return true; } -meshtastic_MeshPacket *MeshPacketQueue::dequeue() -{ - if (empty()) { - return NULL; - } +meshtastic_MeshPacket *MeshPacketQueue::dequeue() { + if (empty()) { + return NULL; + } - auto *p = queue.front(); - queue.erase(queue.begin()); // Remove the highest-priority packet - return p; + auto *p = queue.front(); + queue.erase(queue.begin()); // Remove the highest-priority packet + return p; } -meshtastic_MeshPacket *MeshPacketQueue::getFront() -{ - if (empty()) { - return NULL; - } +meshtastic_MeshPacket *MeshPacketQueue::getFront() { + if (empty()) { + return NULL; + } - auto *p = queue.front(); - return p; + auto *p = queue.front(); + return p; } /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ -meshtastic_MeshPacket *MeshPacketQueue::getPacketFromQueue(NodeNum from, PacketId id) -{ - for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); - if (getFrom(p) == from && p->id == id) { - return p; - } +meshtastic_MeshPacket *MeshPacketQueue::getPacketFromQueue(NodeNum from, PacketId id) { + for (auto it = queue.begin(); it != queue.end(); it++) { + auto p = (*it); + if (getFrom(p) == from && p->id == id) { + return p; } + } - return NULL; + return NULL; } -/** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found */ -meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late, uint8_t hop_limit_lt) -{ - for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); - if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after)) && - (!hop_limit_lt || p->hop_limit < hop_limit_lt)) { - queue.erase(it); - return p; - } +/** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found + */ +meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late, uint8_t hop_limit_lt) { + for (auto it = queue.begin(); it != queue.end(); it++) { + auto p = (*it); + if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after)) && + (!hop_limit_lt || p->hop_limit < hop_limit_lt)) { + queue.erase(it); + return p; } + } - return NULL; + return NULL; } /* Attempt to find a packet from this queue. Return true if it was found. */ -bool MeshPacketQueue::find(const NodeNum from, const PacketId id) -{ - return getPacketFromQueue(from, id) != NULL; -} +bool MeshPacketQueue::find(const NodeNum from, const PacketId id) { return getPacketFromQueue(from, id) != NULL; } /** * Attempt to find a lower-priority packet in the queue and replace it with the provided one. * @return True if the replacement succeeded, false otherwise */ -bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) -{ +bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) { - if (queue.empty()) { - return false; // No packets to replace + if (queue.empty()) { + return false; // No packets to replace + } + + // Check if the packet at the back has a lower priority than the new packet + auto *backPacket = queue.back(); + if (!backPacket->tx_after && backPacket->priority < p->priority) { + LOG_WARN("Dropping packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", backPacket->id, p->id); + // Remove the back packet + queue.pop_back(); + packetPool.release(backPacket); + // Insert the new packet in the correct order + enqueue(p); + return true; + } + + if (backPacket->tx_after) { + // Check if there's a non-late packet with lower priority + auto it = queue.end(); + auto refPacket = *--it; + for (; refPacket->tx_after && it != queue.begin(); refPacket = *--it) + ; + if (!refPacket->tx_after && refPacket->priority < p->priority) { + LOG_WARN("Dropping non-late packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", refPacket->id, p->id); + queue.erase(it); + packetPool.release(refPacket); + // Insert the new packet in the correct order + enqueue(p); + return true; } + } - // Check if the packet at the back has a lower priority than the new packet - auto *backPacket = queue.back(); - if (!backPacket->tx_after && backPacket->priority < p->priority) { - LOG_WARN("Dropping packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", backPacket->id, p->id); - // Remove the back packet - queue.pop_back(); - packetPool.release(backPacket); - // Insert the new packet in the correct order - enqueue(p); - return true; - } - - if (backPacket->tx_after) { - // Check if there's a non-late packet with lower priority - auto it = queue.end(); - auto refPacket = *--it; - for (; refPacket->tx_after && it != queue.begin(); refPacket = *--it) - ; - if (!refPacket->tx_after && refPacket->priority < p->priority) { - LOG_WARN("Dropping non-late packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", - refPacket->id, p->id); - queue.erase(it); - packetPool.release(refPacket); - // Insert the new packet in the correct order - enqueue(p); - return true; - } - } - - // If the back packet's priority is not lower, no replacement occurs - return false; + // If the back packet's priority is not lower, no replacement occurs + return false; } \ No newline at end of file diff --git a/src/mesh/MeshPacketQueue.h b/src/mesh/MeshPacketQueue.h index 3d3902c1e..4de595e7c 100644 --- a/src/mesh/MeshPacketQueue.h +++ b/src/mesh/MeshPacketQueue.h @@ -7,43 +7,42 @@ /** * A priority queue of packets */ -class MeshPacketQueue -{ - size_t maxLen; - std::vector queue; +class MeshPacketQueue { + size_t maxLen; + std::vector queue; - /** Replace a lower priority package in the queue with 'mp' (provided there are lower pri packages). Return true if replaced. - */ - bool replaceLowerPriorityPacket(meshtastic_MeshPacket *mp); + /** Replace a lower priority package in the queue with 'mp' (provided there are lower pri packages). Return true if + * replaced. + */ + bool replaceLowerPriorityPacket(meshtastic_MeshPacket *mp); - public: - explicit MeshPacketQueue(size_t _maxLen); +public: + explicit MeshPacketQueue(size_t _maxLen); - /** enqueue a packet, return false if full - * @param dropped Optional pointer to a bool that will be set to true if a packet was dropped - */ - bool enqueue(meshtastic_MeshPacket *p, bool *dropped = nullptr); + /** enqueue a packet, return false if full + * @param dropped Optional pointer to a bool that will be set to true if a packet was dropped + */ + bool enqueue(meshtastic_MeshPacket *p, bool *dropped = nullptr); - /** return true if the queue is empty */ - bool empty(); + /** return true if the queue is empty */ + bool empty(); - /** return amount of free packets in Queue */ - size_t getFree() { return maxLen - queue.size(); } + /** return amount of free packets in Queue */ + size_t getFree() { return maxLen - queue.size(); } - /** return total size of the Queue */ - size_t getMaxLen() { return maxLen; } + /** return total size of the Queue */ + size_t getMaxLen() { return maxLen; } - meshtastic_MeshPacket *dequeue(); + meshtastic_MeshPacket *dequeue(); - meshtastic_MeshPacket *getFront(); + meshtastic_MeshPacket *getFront(); - /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ - meshtastic_MeshPacket *getPacketFromQueue(NodeNum from, PacketId id); + /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ + meshtastic_MeshPacket *getPacketFromQueue(NodeNum from, PacketId id); - /** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ - meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true, - uint8_t hop_limit_lt = 0); + /** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ + meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true, uint8_t hop_limit_lt = 0); - /* Attempt to find a packet from this queue. Return true if it was found. */ - bool find(const NodeNum from, const PacketId id); + /* Attempt to find a packet from this queue. Return true if it was found. */ + bool find(const NodeNum from, const PacketId id); }; \ No newline at end of file diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index f2514eea1..ce03fce12 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -7,16 +7,16 @@ // Map from old region names to new region enums struct RegionInfo { - meshtastic_Config_LoRaConfig_RegionCode code; - float freqStart; - float freqEnd; - float dutyCycle; - float spacing; - uint8_t powerLimit; // Or zero for not set - bool audioPermitted; - bool freqSwitching; - bool wideLora; - const char *name; // EU433 etc + meshtastic_Config_LoRaConfig_RegionCode code; + float freqStart; + float freqEnd; + float dutyCycle; + float spacing; + uint8_t powerLimit; // Or zero for not set + bool audioPermitted; + bool freqSwitching; + bool wideLora; + const char *name; // EU433 etc }; extern const RegionInfo regions[]; diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index c1b3839bb..1e01a1ae0 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -28,22 +28,22 @@ #endif /* -receivedPacketQueue - this is a queue of messages we've received from the mesh, which we are keeping to deliver to the phone. -It is implemented with a FreeRTos queue (wrapped with a little RTQueue class) of pointers to MeshPacket protobufs (which were -alloced with new). After a packet ptr is removed from the queue and processed it should be deleted. (eventually we should move -sent packets into a 'sentToPhone' queue of packets we can delete just as soon as we are sure the phone has acked those packets - -when the phone writes to FromNum) +receivedPacketQueue - this is a queue of messages we've received from the mesh, which we are keeping to deliver to the +phone. It is implemented with a FreeRTos queue (wrapped with a little RTQueue class) of pointers to MeshPacket protobufs +(which were alloced with new). After a packet ptr is removed from the queue and processed it should be deleted. +(eventually we should move sent packets into a 'sentToPhone' queue of packets we can delete just as soon as we are sure +the phone has acked those packets - when the phone writes to FromNum) -mesh - an instance of Mesh class. Which manages the interface to the mesh radio library, reception of packets from other nodes, -arbitrating to select a node number and keeping the current nodedb. +mesh - an instance of Mesh class. Which manages the interface to the mesh radio library, reception of packets from +other nodes, arbitrating to select a node number and keeping the current nodedb. */ /* Broadcast when a newly powered mesh node wants to find a node num it can use The algorithm is as follows: -* when a node starts up, it broadcasts their user and the normal flow is for all other nodes to reply with their User as well (so -the new node can build its node db) +* when a node starts up, it broadcasts their user and the normal flow is for all other nodes to reply with their User as +well (so the new node can build its node db) */ MeshService *service; @@ -67,398 +67,372 @@ Allocator &queueStatusPool = staticQueueStatusPool; MeshService::MeshService() #ifdef ARCH_PORTDUINO - : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), - toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) + : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), + toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) #endif { - lastQueueStatus = {0, 0, 16, 0}; + lastQueueStatus = {0, 0, 16, 0}; } -void MeshService::init() -{ +void MeshService::init() { #if HAS_GPS - if (gps) - gpsObserver.observe(&gps->newStatus); + if (gps) + gpsObserver.observe(&gps->newStatus); #endif } -int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) -{ - powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping +int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) { + powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping - nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio - bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; - if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { - LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); - // ignore our request for its NodeInfo - } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && - nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { - if (airTime->isTxAllowedChannelUtil(true)) { - const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); - if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { - LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); - } else { - LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); - nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel); - } - } else { - LOG_DEBUG("Skip sending NodeInfo > 25%% ch. util"); - } + nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio + bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && + mp->decoded.request_id > 0) { + LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); + // ignore our request for its NodeInfo + } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && nodeInfoModule && + !isPreferredRebroadcaster && !nodeDB->isFull()) { + if (airTime->isTxAllowedChannelUtil(true)) { + const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); + if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { + LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); + } else { + LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); + nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel); + } + } else { + LOG_DEBUG("Skip sending NodeInfo > 25%% ch. util"); } + } - printPacket("Forwarding to phone", mp); - sendToPhone(packetPool.allocCopy(*mp)); + printPacket("Forwarding to phone", mp); + sendToPhone(packetPool.allocCopy(*mp)); - return 0; + return 0; } /// Do idle processing (mostly processing messages which have been queued from the radio) -void MeshService::loop() -{ - if (lastQueueStatus.free == 0) { // check if there is now free space in TX queue - meshtastic_QueueStatus qs = router->getQueueStatus(); - if (qs.free != lastQueueStatus.free) - (void)sendQueueStatusToPhone(qs, 0, 0); - } - if (oldFromNum != fromNum) { // We don't want to generate extra notifies for multiple new packets - int result = fromNumChanged.notifyObservers(fromNum); - if (result == 0) // If any observer returns non-zero, we will try again - oldFromNum = fromNum; - } +void MeshService::loop() { + if (lastQueueStatus.free == 0) { // check if there is now free space in TX queue + meshtastic_QueueStatus qs = router->getQueueStatus(); + if (qs.free != lastQueueStatus.free) + (void)sendQueueStatusToPhone(qs, 0, 0); + } + if (oldFromNum != fromNum) { // We don't want to generate extra notifies for multiple new packets + int result = fromNumChanged.notifyObservers(fromNum); + if (result == 0) // If any observer returns non-zero, we will try again + oldFromNum = fromNum; + } } /// The radioConfig object just changed, call this to force the hw to change to the new settings -void MeshService::reloadConfig(int saveWhat) -{ - // If we can successfully set this radio to these settings, save them to disk +void MeshService::reloadConfig(int saveWhat) { + // If we can successfully set this radio to these settings, save them to disk - // This will also update the region as needed - nodeDB->resetRadioConfig(); // Don't let the phone send us fatally bad settings + // This will also update the region as needed + nodeDB->resetRadioConfig(); // Don't let the phone send us fatally bad settings - configChanged.notifyObservers(NULL); // This will cause radio hardware to change freqs etc - nodeDB->saveToDisk(saveWhat); + configChanged.notifyObservers(NULL); // This will cause radio hardware to change freqs etc + nodeDB->saveToDisk(saveWhat); } /// The owner User record just got updated, update our node DB and broadcast the info into the mesh -void MeshService::reloadOwner(bool shouldSave) -{ - // LOG_DEBUG("reloadOwner()"); - // update our local data directly - nodeDB->updateUser(nodeDB->getNodeNum(), owner); - assert(nodeInfoModule); - // update everyone else and save to disk - if (nodeInfoModule && shouldSave) { - nodeInfoModule->sendOurNodeInfo(); - } +void MeshService::reloadOwner(bool shouldSave) { + // LOG_DEBUG("reloadOwner()"); + // update our local data directly + nodeDB->updateUser(nodeDB->getNodeNum(), owner); + assert(nodeInfoModule); + // update everyone else and save to disk + if (nodeInfoModule && shouldSave) { + nodeInfoModule->sendOurNodeInfo(); + } } // search the queue for a request id and return the matching nodenum -NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id) -{ - NodeNum nodenum = 0; - for (int i = 0; i < toPhoneQueue.numUsed(); i++) { - meshtastic_MeshPacket *p = toPhoneQueue.dequeuePtr(0); - if (p->id == request_id) { - nodenum = p->to; - // make sure to continue this to make one full loop - } - // put it right back on the queue - toPhoneQueue.enqueue(p, 0); +NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id) { + NodeNum nodenum = 0; + for (int i = 0; i < toPhoneQueue.numUsed(); i++) { + meshtastic_MeshPacket *p = toPhoneQueue.dequeuePtr(0); + if (p->id == request_id) { + nodenum = p->to; + // make sure to continue this to make one full loop } - return nodenum; + // put it right back on the queue + toPhoneQueue.enqueue(p, 0); + } + return nodenum; } /** * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) - * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep a - * reference + * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can + * not keep a reference */ -void MeshService::handleToRadio(meshtastic_MeshPacket &p) -{ +void MeshService::handleToRadio(meshtastic_MeshPacket &p) { #if defined(ARCH_PORTDUINO) - if (SimRadio::instance && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) { - // Simulates device received a packet via the LoRa chip - SimRadio::instance->unpackAndReceive(p); - return; - } + if (SimRadio::instance && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) { + // Simulates device received a packet via the LoRa chip + SimRadio::instance->unpackAndReceive(p); + return; + } #endif - p.from = 0; // We don't let clients assign nodenums to their sent messages - p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages - p.relay_node = NO_RELAY_NODE; // We don't let clients assign relay_node to their sent messages + p.from = 0; // We don't let clients assign nodenums to their sent messages + p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages + p.relay_node = NO_RELAY_NODE; // We don't let clients assign relay_node to their sent messages - if (p.id == 0) - p.id = generatePacketId(); // If the phone didn't supply one, then pick one + if (p.id == 0) + p.id = generatePacketId(); // If the phone didn't supply one, then pick one - p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone + p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone - IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && - p.to != NODENUM_BROADCAST && p.to != 0) // DM only - { - perhapsDecode(&p); - const StoredMessage &sm = messageStore.addFromPacket(p); - graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI - }) - // Send the packet into the mesh + IF_SCREEN( + if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST && p.to != 0) // DM only + { + perhapsDecode(&p); + const StoredMessage &sm = messageStore.addFromPacket(p); + graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI + }) + // Send the packet into the mesh + DEBUG_HEAP_BEFORE; + auto a = packetPool.allocCopy(p); + DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); + sendToMesh(a, RX_SRC_USER); + + bool loopback = false; // if true send any packet the phone sends back itself (for testing) + if (loopback) { + // no need to copy anymore because handle from radio assumes it should _not_ delete + // packetPool.allocCopy(r.variant.packet); + handleFromRadio(&p); + // handleFromRadio will tell the phone a new packet arrived + } +} + +/** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could + * cancel */ +bool MeshService::cancelSending(PacketId id) { return router->cancelSending(nodeDB->getNodeNum(), id); } + +ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id) { + meshtastic_QueueStatus *copied = queueStatusPool.allocCopy(qs); + + copied->res = res; + copied->mesh_packet_id = mesh_packet_id; + + if (toPhoneQueueStatusQueue.numFree() == 0) { + LOG_INFO("tophone queue status queue is full, discard oldest"); + meshtastic_QueueStatus *d = toPhoneQueueStatusQueue.dequeuePtr(0); + if (d) + releaseQueueStatusToPool(d); + } + + lastQueueStatus = *copied; + + res = toPhoneQueueStatusQueue.enqueue(copied, 0); + fromNum++; + + return res ? ERRNO_OK : ERRNO_UNKNOWN; +} + +void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPhone) { + uint32_t mesh_packet_id = p->id; + nodeDB->updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...) + + // Note: We might return !OK if our fifo was full, at that point the only option we have is to drop it + ErrorCode res = router->sendLocal(p, src); + + /* NOTE(pboldin): Prepare and send QueueStatus message to the phone as a + * high-priority message. */ + meshtastic_QueueStatus qs = router->getQueueStatus(); + ErrorCode r = sendQueueStatusToPhone(qs, res, mesh_packet_id); + if (r != ERRNO_OK) { + LOG_DEBUG("Can't send status to phone"); + } + + if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent DEBUG_HEAP_BEFORE; - auto a = packetPool.allocCopy(p); - DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); - sendToMesh(a, RX_SRC_USER); + auto a = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); - bool loopback = false; // if true send any packet the phone sends back itself (for testing) - if (loopback) { - // no need to copy anymore because handle from radio assumes it should _not_ delete - // packetPool.allocCopy(r.variant.packet); - handleFromRadio(&p); - // handleFromRadio will tell the phone a new packet arrived - } + sendToPhone(a); + } + + // Router may ask us to release the packet if it wasn't sent + if (res == ERRNO_SHOULD_RELEASE) { + releaseToPool(p); + } } -/** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could cancel */ -bool MeshService::cancelSending(PacketId id) -{ - return router->cancelSending(nodeDB->getNodeNum(), id); -} +bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); -ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id) -{ - meshtastic_QueueStatus *copied = queueStatusPool.allocCopy(qs); + assert(node); - copied->res = res; - copied->mesh_packet_id = mesh_packet_id; - - if (toPhoneQueueStatusQueue.numFree() == 0) { - LOG_INFO("tophone queue status queue is full, discard oldest"); - meshtastic_QueueStatus *d = toPhoneQueueStatusQueue.dequeuePtr(0); - if (d) - releaseQueueStatusToPool(d); - } - - lastQueueStatus = *copied; - - res = toPhoneQueueStatusQueue.enqueue(copied, 0); - fromNum++; - - return res ? ERRNO_OK : ERRNO_UNKNOWN; -} - -void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPhone) -{ - uint32_t mesh_packet_id = p->id; - nodeDB->updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...) - - // Note: We might return !OK if our fifo was full, at that point the only option we have is to drop it - ErrorCode res = router->sendLocal(p, src); - - /* NOTE(pboldin): Prepare and send QueueStatus message to the phone as a - * high-priority message. */ - meshtastic_QueueStatus qs = router->getQueueStatus(); - ErrorCode r = sendQueueStatusToPhone(qs, res, mesh_packet_id); - if (r != ERRNO_OK) { - LOG_DEBUG("Can't send status to phone"); - } - - if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent - DEBUG_HEAP_BEFORE; - auto a = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); - - sendToPhone(a); - } - - // Router may ask us to release the packet if it wasn't sent - if (res == ERRNO_SHOULD_RELEASE) { - releaseToPool(p); - } -} - -bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) -{ - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - - assert(node); - - if (nodeDB->hasValidPosition(node)) { + if (nodeDB->hasValidPosition(node)) { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS - if (positionModule) { - if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { - LOG_DEBUG("Skip position ping; no fresh position since boot"); - return false; - } - LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); - positionModule->sendOurPosition(dest, wantReplies, node->channel); - return true; - } - } else { -#endif - if (nodeInfoModule) { - LOG_INFO("Send nodeinfo ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); - nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel); - } + if (positionModule) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position ping; no fresh position since boot"); + return false; + } + LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); + positionModule->sendOurPosition(dest, wantReplies, node->channel); + return true; } - return false; + } else { +#endif + if (nodeInfoModule) { + LOG_INFO("Send nodeinfo ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); + nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel); + } + } + return false; } -void MeshService::sendToPhone(meshtastic_MeshPacket *p) -{ - perhapsDecode(p); +void MeshService::sendToPhone(meshtastic_MeshPacket *p) { + perhapsDecode(p); #ifdef ARCH_ESP32 #if !MESHTASTIC_EXCLUDE_STOREFORWARD - if (moduleConfig.store_forward.enabled && storeForwardModule->isServer() && - p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - releaseToPool(p); // Copy is already stored in StoreForward history - fromNum++; // Notify observers for packet from radio - return; - } + if (moduleConfig.store_forward.enabled && storeForwardModule->isServer() && p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { + releaseToPool(p); // Copy is already stored in StoreForward history + fromNum++; // Notify observers for packet from radio + return; + } #endif #endif - if (toPhoneQueue.numFree() == 0) { - if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || - p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { - LOG_WARN("ToPhone queue is full, discard oldest"); - meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); - if (d) - releaseToPool(d); - } else { - LOG_WARN("ToPhone queue is full, drop packet"); - releaseToPool(p); - fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets - return; - } - } - - if (toPhoneQueue.enqueue(p, 0) == false) { - LOG_CRIT("Failed to queue a packet into toPhoneQueue!"); - abort(); - } - fromNum++; -} - -void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) -{ - LOG_DEBUG("Send mqtt message on topic '%s' to client for proxy", m->topic); - if (toPhoneMqttProxyQueue.numFree() == 0) { - LOG_WARN("MqttClientProxyMessagePool queue is full, discard oldest"); - meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); - if (d) - releaseMqttClientProxyMessageToPool(d); - } - - if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) { - LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!"); - abort(); - } - fromNum++; -} - -void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp) -{ - if (!mp) { - LOG_WARN("Cannot send routing error response: null packet"); - return; - } - - // Use the routing module to send the error response - if (routingModule) { - routingModule->sendAckNak(error, mp->from, mp->id, mp->channel); + if (toPhoneQueue.numFree() == 0) { + if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { + LOG_WARN("ToPhone queue is full, discard oldest"); + meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); + if (d) + releaseToPool(d); } else { - LOG_ERROR("Cannot send routing error response: no routing module"); + LOG_WARN("ToPhone queue is full, drop packet"); + releaseToPool(p); + fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets + return; } + } + + if (toPhoneQueue.enqueue(p, 0) == false) { + LOG_CRIT("Failed to queue a packet into toPhoneQueue!"); + abort(); + } + fromNum++; } -void MeshService::sendClientNotification(meshtastic_ClientNotification *n) -{ - LOG_DEBUG("Send client notification to phone"); - if (toPhoneClientNotificationQueue.numFree() == 0) { - LOG_WARN("ClientNotification queue is full, discard oldest"); - meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0); - if (d) - releaseClientNotificationToPool(d); - } +void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) { + LOG_DEBUG("Send mqtt message on topic '%s' to client for proxy", m->topic); + if (toPhoneMqttProxyQueue.numFree() == 0) { + LOG_WARN("MqttClientProxyMessagePool queue is full, discard oldest"); + meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); + if (d) + releaseMqttClientProxyMessageToPool(d); + } - if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) { - LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!"); - abort(); - } - fromNum++; + if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) { + LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!"); + abort(); + } + fromNum++; } -meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() -{ - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - assert(node); +void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp) { + if (!mp) { + LOG_WARN("Cannot send routing error response: null packet"); + return; + } - // We might not have a position yet for our local node, in that case, at least try to send the time - if (!node->has_position) { - memset(&node->position, 0, sizeof(node->position)); - node->has_position = true; - } + // Use the routing module to send the error response + if (routingModule) { + routingModule->sendAckNak(error, mp->from, mp->id, mp->channel); + } else { + LOG_ERROR("Cannot send routing error response: no routing module"); + } +} - meshtastic_PositionLite &position = node->position; +void MeshService::sendClientNotification(meshtastic_ClientNotification *n) { + LOG_DEBUG("Send client notification to phone"); + if (toPhoneClientNotificationQueue.numFree() == 0) { + LOG_WARN("ClientNotification queue is full, discard oldest"); + meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0); + if (d) + releaseClientNotificationToPool(d); + } - // Update our local node info with our time (even if we don't decide to update anyone else) - node->last_heard = - getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid + if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) { + LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!"); + abort(); + } + fromNum++; +} - position.time = getValidTime(RTCQualityFromNet); +meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + assert(node); - if (powerStatus->getHasBattery() == 1) { - updateBatteryLevel(powerStatus->getBatteryChargePercent()); - } + // We might not have a position yet for our local node, in that case, at least try to send the time + if (!node->has_position) { + memset(&node->position, 0, sizeof(node->position)); + node->has_position = true; + } - return node; + meshtastic_PositionLite &position = node->position; + + // Update our local node info with our time (even if we don't decide to update anyone else) + node->last_heard = getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid + + position.time = getValidTime(RTCQualityFromNet); + + if (powerStatus->getHasBattery() == 1) { + updateBatteryLevel(powerStatus->getBatteryChargePercent()); + } + + return node; } #if HAS_GPS -int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus) -{ - // Update our local node info with our position (even if we don't decide to update anyone else) - const meshtastic_NodeInfoLite *node = refreshLocalMeshNode(); - meshtastic_Position pos = meshtastic_Position_init_default; +int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus) { + // Update our local node info with our position (even if we don't decide to update anyone else) + const meshtastic_NodeInfoLite *node = refreshLocalMeshNode(); + meshtastic_Position pos = meshtastic_Position_init_default; - if (newStatus->getHasLock()) { - // load data from GPS object, will add timestamp + battery further down - pos = gps->p; - } else { - // The GPS has lost lock + if (newStatus->getHasLock()) { + // load data from GPS object, will add timestamp + battery further down + pos = gps->p; + } else { + // The GPS has lost lock #ifdef GPS_DEBUG - LOG_DEBUG("onGPSchanged() - lost validLocation"); + LOG_DEBUG("onGPSchanged() - lost validLocation"); #endif - } - // Used fixed position if configured regardless of GPS lock - if (config.position.fixed_position) { - LOG_WARN("Use fixed position"); - pos = TypeConversions::ConvertToPosition(node->position); - } + } + // Used fixed position if configured regardless of GPS lock + if (config.position.fixed_position) { + LOG_WARN("Use fixed position"); + pos = TypeConversions::ConvertToPosition(node->position); + } - // Add a fresh timestamp - pos.time = getValidTime(RTCQualityFromNet); + // Add a fresh timestamp + pos.time = getValidTime(RTCQualityFromNet); - // In debug logs, identify position by @timestamp:stage (stage 4 = nodeDB) - LOG_DEBUG("onGPSChanged() pos@%x time=%u lat=%d lon=%d alt=%d", pos.timestamp, pos.time, pos.latitude_i, pos.longitude_i, - pos.altitude); + // In debug logs, identify position by @timestamp:stage (stage 4 = nodeDB) + LOG_DEBUG("onGPSChanged() pos@%x time=%u lat=%d lon=%d alt=%d", pos.timestamp, pos.time, pos.latitude_i, pos.longitude_i, pos.altitude); - // Update our current position in the local DB - nodeDB->updatePosition(nodeDB->getNodeNum(), pos, RX_SRC_LOCAL); + // Update our current position in the local DB + nodeDB->updatePosition(nodeDB->getNodeNum(), pos, RX_SRC_LOCAL); - return 0; + return 0; } #endif -bool MeshService::isToPhoneQueueEmpty() -{ - return toPhoneQueue.isEmpty(); -} - -uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp) -{ - uint32_t now = getTime(); - - uint32_t last_seen = mp->rx_time; - int delta = (int)(now - last_seen); - if (delta < 0) // our clock must be slightly off still - not set from GPS yet - delta = 0; - - return delta; +bool MeshService::isToPhoneQueueEmpty() { return toPhoneQueue.isEmpty(); } + +uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp) { + uint32_t now = getTime(); + + uint32_t last_seen = mp->rx_time; + int delta = (int)(now - last_seen); + if (delta < 0) // our clock must be slightly off still - not set from GPS yet + delta = 0; + + return delta; } diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 71fb544a0..2ad33388d 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -32,174 +32,171 @@ extern Allocator &clientNotificationPool; * Top level app for this service. keeps the mesh, the radio config and the queue of received packets. * */ -class MeshService -{ +class MeshService { #if HAS_GPS - CallbackObserver gpsObserver = - CallbackObserver(this, &MeshService::onGPSChanged); + CallbackObserver gpsObserver = + CallbackObserver(this, &MeshService::onGPSChanged); #endif - /// received packets waiting for the phone to process them - /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure - /// we never hang because android hasn't been there in a while - /// FIXME - save this to flash on deep sleep + /// received packets waiting for the phone to process them + /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure + /// we never hang because android hasn't been there in a while + /// FIXME - save this to flash on deep sleep #ifdef ARCH_PORTDUINO - PointerQueue toPhoneQueue; + PointerQueue toPhoneQueue; #else - StaticPointerQueue toPhoneQueue; + StaticPointerQueue toPhoneQueue; #endif - // keep list of QueueStatus packets to be send to the phone + // keep list of QueueStatus packets to be send to the phone #ifdef ARCH_PORTDUINO - PointerQueue toPhoneQueueStatusQueue; + PointerQueue toPhoneQueueStatusQueue; #else - StaticPointerQueue toPhoneQueueStatusQueue; + StaticPointerQueue toPhoneQueueStatusQueue; #endif - // keep list of MqttClientProxyMessages to be send to the client for delivery + // keep list of MqttClientProxyMessages to be send to the client for delivery #ifdef ARCH_PORTDUINO - PointerQueue toPhoneMqttProxyQueue; + PointerQueue toPhoneMqttProxyQueue; #else - StaticPointerQueue toPhoneMqttProxyQueue; + StaticPointerQueue toPhoneMqttProxyQueue; #endif - // keep list of ClientNotifications to be send to the client (phone) + // keep list of ClientNotifications to be send to the client (phone) #ifdef ARCH_PORTDUINO - PointerQueue toPhoneClientNotificationQueue; + PointerQueue toPhoneClientNotificationQueue; #else - StaticPointerQueue toPhoneClientNotificationQueue; + StaticPointerQueue toPhoneClientNotificationQueue; #endif - // This holds the last QueueStatus send - meshtastic_QueueStatus lastQueueStatus; + // This holds the last QueueStatus send + meshtastic_QueueStatus lastQueueStatus; - /// The current nonce for the newest packet which has been queued for the phone - uint32_t fromNum = 0; + /// The current nonce for the newest packet which has been queued for the phone + uint32_t fromNum = 0; - /// Updated in loop() to detect when fromNum changes - uint32_t oldFromNum = 0; + /// Updated in loop() to detect when fromNum changes + uint32_t oldFromNum = 0; - public: - enum APIState { - STATE_DISCONNECTED, // Initial state, no API is connected - STATE_BLE, - STATE_WIFI, - STATE_SERIAL, - STATE_PACKET, - STATE_HTTP, - STATE_ETH - }; +public: + enum APIState { + STATE_DISCONNECTED, // Initial state, no API is connected + STATE_BLE, + STATE_WIFI, + STATE_SERIAL, + STATE_PACKET, + STATE_HTTP, + STATE_ETH + }; - APIState api_state = STATE_DISCONNECTED; + APIState api_state = STATE_DISCONNECTED; - static bool isTextPayload(const meshtastic_MeshPacket *p) - { - if (moduleConfig.range_test.enabled && p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { - return true; - } - return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || - p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || - p->decoded.portnum == meshtastic_PortNum_ALERT_APP; + static bool isTextPayload(const meshtastic_MeshPacket *p) { + if (moduleConfig.range_test.enabled && p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { + return true; } - /// Called when some new packets have arrived from one of the radios - Observable fromNumChanged; + return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || + p->decoded.portnum == meshtastic_PortNum_ALERT_APP; + } + /// Called when some new packets have arrived from one of the radios + Observable fromNumChanged; - /// Called when radio config has changed (radios should observe this and set their hardware as required) - Observable configChanged; + /// Called when radio config has changed (radios should observe this and set their hardware as required) + Observable configChanged; - MeshService(); + MeshService(); - void init(); + void init(); - /// Do idle processing (mostly processing messages which have been queued from the radio) - void loop(); + /// Do idle processing (mostly processing messages which have been queued from the radio) + void loop(); - /// Return the next packet destined to the phone. FIXME, somehow use fromNum to allow the phone to retry the - /// last few packets if needs to. - meshtastic_MeshPacket *getForPhone() { return toPhoneQueue.dequeuePtr(0); } + /// Return the next packet destined to the phone. FIXME, somehow use fromNum to allow the phone to retry the + /// last few packets if needs to. + meshtastic_MeshPacket *getForPhone() { return toPhoneQueue.dequeuePtr(0); } - /// Allows the bluetooth handler to free packets after they have been sent - void releaseToPool(meshtastic_MeshPacket *p) { packetPool.release(p); } + /// Allows the bluetooth handler to free packets after they have been sent + void releaseToPool(meshtastic_MeshPacket *p) { packetPool.release(p); } - /// Return the next QueueStatus packet destined to the phone. - meshtastic_QueueStatus *getQueueStatusForPhone() { return toPhoneQueueStatusQueue.dequeuePtr(0); } + /// Return the next QueueStatus packet destined to the phone. + meshtastic_QueueStatus *getQueueStatusForPhone() { return toPhoneQueueStatusQueue.dequeuePtr(0); } - /// Return the next MqttClientProxyMessage packet destined to the phone. - meshtastic_MqttClientProxyMessage *getMqttClientProxyMessageForPhone() { return toPhoneMqttProxyQueue.dequeuePtr(0); } + /// Return the next MqttClientProxyMessage packet destined to the phone. + meshtastic_MqttClientProxyMessage *getMqttClientProxyMessageForPhone() { return toPhoneMqttProxyQueue.dequeuePtr(0); } - /// Return the next ClientNotification packet destined to the phone. - meshtastic_ClientNotification *getClientNotificationForPhone() { return toPhoneClientNotificationQueue.dequeuePtr(0); } + /// Return the next ClientNotification packet destined to the phone. + meshtastic_ClientNotification *getClientNotificationForPhone() { return toPhoneClientNotificationQueue.dequeuePtr(0); } - // search the queue for a request id and return the matching nodenum - NodeNum getNodenumFromRequestId(uint32_t request_id); + // search the queue for a request id and return the matching nodenum + NodeNum getNodenumFromRequestId(uint32_t request_id); - // Release QueueStatus packet to pool - void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); } + // Release QueueStatus packet to pool + void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); } - // Release MqttClientProxyMessage packet to pool - void releaseMqttClientProxyMessageToPool(meshtastic_MqttClientProxyMessage *p) { mqttClientProxyMessagePool.release(p); } + // Release MqttClientProxyMessage packet to pool + void releaseMqttClientProxyMessageToPool(meshtastic_MqttClientProxyMessage *p) { mqttClientProxyMessagePool.release(p); } - /// Release the next ClientNotification packet to pool. - void releaseClientNotificationToPool(meshtastic_ClientNotification *p) { clientNotificationPool.release(p); } + /// Release the next ClientNotification packet to pool. + void releaseClientNotificationToPool(meshtastic_ClientNotification *p) { clientNotificationPool.release(p); } - /** - * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) - * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep - * a reference - */ - void handleToRadio(meshtastic_MeshPacket &p); + /** + * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) + * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can + * not keep a reference + */ + void handleToRadio(meshtastic_MeshPacket &p); - /** The radioConfig object just changed, call this to force the hw to change to the new settings - * @return true if client devices should be sent a new set of radio configs - */ - void reloadConfig(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + /** The radioConfig object just changed, call this to force the hw to change to the new settings + * @return true if client devices should be sent a new set of radio configs + */ + void reloadConfig(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); - /// The owner User record just got updated, update our node DB and broadcast the info into the mesh - void reloadOwner(bool shouldSave = true); + /// The owner User record just got updated, update our node DB and broadcast the info into the mesh + void reloadOwner(bool shouldSave = true); - /// Called when the user wakes up our GUI, normally sends our latest location to the mesh (if we have it), otherwise at least - /// sends our nodeinfo - /// returns true if we sent a position - bool trySendPosition(NodeNum dest, bool wantReplies = false); + /// Called when the user wakes up our GUI, normally sends our latest location to the mesh (if we have it), otherwise + /// at least sends our nodeinfo returns true if we sent a position + bool trySendPosition(NodeNum dest, bool wantReplies = false); - /// Send a packet into the mesh - note p must have been allocated from packetPool. We will return it to that pool after - /// sending. This is the ONLY function you should use for sending messages into the mesh, because it also updates the nodedb - /// cache - void sendToMesh(meshtastic_MeshPacket *p, RxSource src = RX_SRC_LOCAL, bool ccToPhone = false); + /// Send a packet into the mesh - note p must have been allocated from packetPool. We will return it to that pool + /// after sending. This is the ONLY function you should use for sending messages into the mesh, because it also + /// updates the nodedb cache + void sendToMesh(meshtastic_MeshPacket *p, RxSource src = RX_SRC_LOCAL, bool ccToPhone = false); - /** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could cancel */ - bool cancelSending(PacketId id); + /** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could + * cancel */ + bool cancelSending(PacketId id); - /// Pull the latest power and time info into my nodeinfo - meshtastic_NodeInfoLite *refreshLocalMeshNode(); + /// Pull the latest power and time info into my nodeinfo + meshtastic_NodeInfoLite *refreshLocalMeshNode(); - /// Send a packet to the phone - void sendToPhone(meshtastic_MeshPacket *p); + /// Send a packet to the phone + void sendToPhone(meshtastic_MeshPacket *p); - /// Send an MQTT message to the phone for client proxying - virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); + /// Send an MQTT message to the phone for client proxying + virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); - /// Send a ClientNotification to the phone - virtual void sendClientNotification(meshtastic_ClientNotification *cn); + /// Send a ClientNotification to the phone + virtual void sendClientNotification(meshtastic_ClientNotification *cn); - /// Send an error response to the phone - void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); + /// Send an error response to the phone + void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); - bool isToPhoneQueueEmpty(); + bool isToPhoneQueueEmpty(); - ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id); + ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id); - uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp); + uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp); - private: +private: #if HAS_GPS - /// Called when our gps position has changed - updates nodedb and sends Location message out into the mesh - /// returns 0 to allow further processing - int onGPSChanged(const meshtastic::GPSStatus *arg); + /// Called when our gps position has changed - updates nodedb and sends Location message out into the mesh + /// returns 0 to allow further processing + int onGPSChanged(const meshtastic::GPSStatus *arg); #endif - /// Handle a packet that just arrived from the radio. This method does _not_ free the provided packet. If it - /// needs to keep the packet around it makes a copy - int handleFromRadio(const meshtastic_MeshPacket *p); - friend class RoutingModule; + /// Handle a packet that just arrived from the radio. This method does _not_ free the provided packet. If it + /// needs to keep the packet around it makes a copy + int handleFromRadio(const meshtastic_MeshPacket *p); + friend class RoutingModule; }; extern MeshService *service; diff --git a/src/mesh/MeshTypes.h b/src/mesh/MeshTypes.h index 680926d3c..0112f1149 100644 --- a/src/mesh/MeshTypes.h +++ b/src/mesh/MeshTypes.h @@ -10,8 +10,9 @@ typedef uint32_t NodeNum; typedef uint32_t PacketId; // A packet sequence number #define NODENUM_BROADCAST UINT32_MAX -#define NODENUM_BROADCAST_NO_LORA \ - 1 // Reserved to only deliver packets over high speed (non-lora) transports, such as MQTT or BLE mesh (not yet implemented) +#define NODENUM_BROADCAST_NO_LORA \ + 1 // Reserved to only deliver packets over high speed (non-lora) transports, such as MQTT or BLE mesh (not yet + // implemented) #define ERRNO_OK 0 #define ERRNO_NO_INTERFACES 33 #define ERRNO_UNKNOWN 32 // pick something that doesn't conflict with RH_ROUTER_ERROR_UNABLE_TO_DELIVER @@ -23,17 +24,17 @@ typedef uint32_t PacketId; // A packet sequence number * Source of a received message */ enum RxSource { - RX_SRC_LOCAL, // message was generated locally - RX_SRC_RADIO, // message was received from radio mesh - RX_SRC_USER // message was received from end-user device + RX_SRC_LOCAL, // message was generated locally + RX_SRC_RADIO, // message was received from radio mesh + RX_SRC_USER // message was received from end-user device }; /** * the max number of hops a message can pass through, used as the default max for hop_limit in MeshPacket. * - * We reserve 3 bits in the header so this could be up to 7, but given the high range of lora and typical usecases, keeping - * maxhops to 3 should be fine for a while. This also serves to prevent routing/flooding attempts to be attempted for - * too long. + * We reserve 3 bits in the header so this could be up to 7, but given the high range of lora and typical usecases, + *keeping maxhops to 3 should be fine for a while. This also serves to prevent routing/flooding attempts to be + *attempted for too long. **/ #define HOP_MAX 7 @@ -52,8 +53,8 @@ extern Allocator &packetPool; using UniquePacketPoolPacket = Allocator::UniqueAllocation; /** - * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on - * the local node. If from is zero this function returns our node number instead + * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they + * originated on the local node. If from is zero this function returns our node number instead */ NodeNum getFrom(const meshtastic_MeshPacket *p); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 5230e5b85..c26e7f6fa 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -8,331 +8,315 @@ NextHopRouter::NextHopRouter() {} -PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions) -{ - packet = p; - this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send +PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions) { + packet = p; + this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send } /** * Send a packet */ -ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) -{ - // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) - p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us - wasSeenRecently(p); // FIXME, move this to a sniffSent method +ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) { + // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us + wasSeenRecently(p); // FIXME, move this to a sniffSent method - p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop - LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); + p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop + LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); - // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is - // not 0 or want_ack is set, start retransmissions - if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) - startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet + // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop + // limit is not 0 or want_ack is set, start retransmissions + if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) + startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet - return Router::send(p); + return Router::send(p); } -bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) -{ - bool wasFallback = false; - bool weWereNextHop = false; - bool wasUpgraded = false; - bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop, - &wasUpgraded); // Updates history; returns false when an upgrade is detected +bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { + bool wasFallback = false; + bool weWereNextHop = false; + bool wasUpgraded = false; + bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop, + &wasUpgraded); // Updates history; returns false when an upgrade is detected - // Handle hop_limit upgrade scenario for rebroadcasters - if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { - return true; // we handled it, so stop processing + // Handle hop_limit upgrade scenario for rebroadcasters + if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { + return true; // we handled it, so stop processing + } + + if (seenRecently) { + printPacket("Ignore dupe incoming msg", p); + + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + rxDupe++; + stopRetransmission(p->from, p->id); } - if (seenRecently) { - printPacket("Ignore dupe incoming msg", p); - - if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { - rxDupe++; - stopRetransmission(p->from, p->id); + // If it was a fallback to flooding, try to relay again + if (wasFallback) { + LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node); + // Check if it's still in the Tx queue, if not, we have to relay it again + if (!findInTxQueue(p->from, p->id)) { + reprocessPacket(p); + perhapsRebroadcast(p); + } + } else { + bool isRepeated = getHopsAway(*p) == 0; + // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again + if (isRepeated) { + if (!findInTxQueue(p->from, p->id)) { + reprocessPacket(p); + if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) { + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } } - - // If it was a fallback to flooding, try to relay again - if (wasFallback) { - LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node); - // Check if it's still in the Tx queue, if not, we have to relay it again - if (!findInTxQueue(p->from, p->id)) { - reprocessPacket(p); - perhapsRebroadcast(p); - } - } else { - bool isRepeated = getHopsAway(*p) == 0; - // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again - if (isRepeated) { - if (!findInTxQueue(p->from, p->id)) { - reprocessPacket(p); - if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) { - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } - } - } else if (!weWereNextHop) { - perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay - } - } - return true; + } else if (!weWereNextHop) { + perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay + } } + return true; + } - return Router::shouldFilterReceived(p); + return Router::shouldFilterReceived(p); } -void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) -{ - NodeNum ourNodeNum = getNodeNum(); - uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum); - bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && - (p->decoded.request_id != 0 || p->decoded.reply_id != 0); - if (isAckorReply) { - // Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from" - // is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the - // destination - if (p->from != 0) { - meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); - if (origTx) { - // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came - // directly from the destination - bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); - bool weWereSoleRelayer = false; - bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); - if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { - if (origTx->next_hop != p->relay_node) { // Not already set - LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, - p->relay_node, wasAlreadyRelayer, weWereSoleRelayer); - origTx->next_hop = p->relay_node; - } - } - } - } - if (!isToUs(p)) { - Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM - // stop retransmission for the original packet - stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id +void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { + NodeNum ourNodeNum = getNodeNum(); + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum); + bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0 || p->decoded.reply_id != 0); + if (isAckorReply) { + // Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if + // "from" is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to + // the destination + if (p->from != 0) { + meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); + if (origTx) { + // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came + // directly from the destination + bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); + bool weWereSoleRelayer = false; + bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); + if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { + if (origTx->next_hop != p->relay_node) { // Not already set + LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, p->relay_node, wasAlreadyRelayer, + weWereSoleRelayer); + origTx->next_hop = p->relay_node; + } } + } } + if (!isToUs(p)) { + Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM + // stop retransmission for the original packet + stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id + } + } - perhapsRebroadcast(p); + perhapsRebroadcast(p); - // handle the packet as normal - Router::sniffReceived(p, c); + // handle the packet as normal + Router::sniffReceived(p, c); } /* Check if we should be rebroadcasting this packet if so, do so. */ -bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) -{ - if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { - if (p->id != 0) { - if (isRebroadcaster()) { - if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { - meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it - LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); +bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) { + if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { + if (p->id != 0) { + if (isRebroadcaster()) { + if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { + meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it + LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); - // Use shared logic to determine if hop_limit should be decremented - if (shouldDecrementHopLimit(p)) { - tosend->hop_limit--; // bump down the hop count - } else { - LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); - } + // Use shared logic to determine if hop_limit should be decremented + if (shouldDecrementHopLimit(p)) { + tosend->hop_limit--; // bump down the hop count + } else { + LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); + } #if USERPREFS_EVENT_MODE - if (tosend->hop_limit > 2) { - // if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away. - tosend->hop_start -= (tosend->hop_limit - 2); - tosend->hop_limit = 2; - } + if (tosend->hop_limit > 2) { + // if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away. + tosend->hop_start -= (tosend->hop_limit - 2); + tosend->hop_limit = 2; + } #endif - if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { - FloodingRouter::send(tosend); - } else { - NextHopRouter::send(tosend); - } + if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { + FloodingRouter::send(tosend); + } else { + NextHopRouter::send(tosend); + } - return true; - } - } else { - LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); - } - } else { - LOG_DEBUG("Ignore 0 id broadcast"); + return true; } + } else { + LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); + } + } else { + LOG_DEBUG("Ignore 0 id broadcast"); } + } - return false; + return false; } /** * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ -uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) -{ - if (isBroadcast(to)) - return NO_NEXT_HOP_PREFERENCE; - - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); - if (node && node->next_hop) { - // We are careful not to return the relay node as the next hop - if (node->next_hop != relay_node) { - // LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop); - return node->next_hop; - } else - LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); - } +uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) { + if (isBroadcast(to)) return NO_NEXT_HOP_PREFERENCE; + + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); + if (node && node->next_hop) { + // We are careful not to return the relay node as the next hop + if (node->next_hop != relay_node) { + // LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop); + return node->next_hop; + } else + LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); + } + return NO_NEXT_HOP_PREFERENCE; } -PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) -{ - auto old = pending.find(key); // If we have an old record, someone messed up because id got reused - if (old != pending.end()) { - return &old->second; - } else - return NULL; +PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) { + auto old = pending.find(key); // If we have an old record, someone messed up because id got reused + if (old != pending.end()) { + return &old->second; + } else + return NULL; } /** * Stop any retransmissions we are doing of the specified node/packet ID pair */ -bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) -{ - auto key = GlobalPacketId(from, id); - return stopRetransmission(key); +bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) { + auto key = GlobalPacketId(from, id); + return stopRetransmission(key); } -bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) -{ - // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) +bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) { + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) - // Return false for roles like ROUTER, ROUTER_LATE which should always transmit the packet at least once. + // Return false for roles like ROUTER, ROUTER_LATE which should always transmit the packet at least once. - return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe + return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe } -bool NextHopRouter::stopRetransmission(GlobalPacketId key) -{ - auto old = findPendingPacket(key); - if (old) { - auto p = old->packet; - /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue - to avoid canceling a transmission if it was ACKed super fast via MQTT */ - if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { - // We only cancel it if we are the original sender or if we're not a router(_late) - if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { - // remove the 'original' (identified by originator and packet->id) from the txqueue and free it - cancelSending(getFrom(p), p->id); - } - } +bool NextHopRouter::stopRetransmission(GlobalPacketId key) { + auto old = findPendingPacket(key); + if (old) { + auto p = old->packet; + /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue + to avoid canceling a transmission if it was ACKed super fast via MQTT */ + if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { + // We only cancel it if we are the original sender or if we're not a router(_late) + if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { + // remove the 'original' (identified by originator and packet->id) from the txqueue and free it + cancelSending(getFrom(p), p->id); + } + } - // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it - // doesn't get scheduled again. (This is the core of stopRetransmission.) - auto numErased = pending.erase(key); - assert(numErased == 1); + // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it + // doesn't get scheduled again. (This is the core of stopRetransmission.) + auto numErased = pending.erase(key); + assert(numErased == 1); - // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the - // call to startRetransmission. - packetPool.release(p); + // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the + // call to startRetransmission. + packetPool.release(p); - return true; - } else - return false; + return true; + } else + return false; } /** * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. */ -PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx) -{ - auto id = GlobalPacketId(p); - auto rec = PendingPacket(p, numReTx); +PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx) { + auto id = GlobalPacketId(p); + auto rec = PendingPacket(p, numReTx); - stopRetransmission(getFrom(p), p->id); + stopRetransmission(getFrom(p), p->id); - setNextTx(&rec); - pending[id] = rec; + setNextTx(&rec); + pending[id] = rec; - return &pending[id]; + return &pending[id]; } /** * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) */ -int32_t NextHopRouter::doRetransmissions() -{ - uint32_t now = millis(); - int32_t d = INT32_MAX; +int32_t NextHopRouter::doRetransmissions() { + uint32_t now = millis(); + int32_t d = INT32_MAX; - // FIXME, we should use a better datastructure rather than walking through this map. - // for(auto el: pending) { - for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { - ++nextIt; // we use this odd pattern because we might be deleting it... - auto &p = it->second; + // FIXME, we should use a better datastructure rather than walking through this map. + // for(auto el: pending) { + for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { + ++nextIt; // we use this odd pattern because we might be deleting it... + auto &p = it->second; - bool stillValid = true; // assume we'll keep this record around + bool stillValid = true; // assume we'll keep this record around - // FIXME, handle 51 day rolloever here!!! - if (p.nextTxMsec <= now) { - if (p.numRetransmissions == 0) { - if (isFromUs(p.packet)) { - LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, - p.packet->id); - sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); - } - // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived - stopRetransmission(it->first); - stillValid = false; // just deleted it - } else { - LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, - p.packet->id, p.numRetransmissions); + // FIXME, handle 51 day rolloever here!!! + if (p.nextTxMsec <= now) { + if (p.numRetransmissions == 0) { + if (isFromUs(p.packet)) { + LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, p.packet->id); + sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); + } + // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived + stopRetransmission(it->first); + stillValid = false; // just deleted it + } else { + LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, p.packet->id, p.numRetransmissions); - if (!isBroadcast(p.packet->to)) { - if (p.numRetransmissions == 1) { - // Last retransmission, reset next_hop (fallback to FloodingRouter) - p.packet->next_hop = NO_NEXT_HOP_PREFERENCE; - // Also reset it in the nodeDB - meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); - if (sentTo) { - LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to); - sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; - } - FloodingRouter::send(packetPool.allocCopy(*p.packet)); - } else { - NextHopRouter::send(packetPool.allocCopy(*p.packet)); - } - } else { - // Note: we call the superclass version because we don't want to have our version of send() add a new - // retransmission record - FloodingRouter::send(packetPool.allocCopy(*p.packet)); - } - - // Queue again - --p.numRetransmissions; - setNextTx(&p); + if (!isBroadcast(p.packet->to)) { + if (p.numRetransmissions == 1) { + // Last retransmission, reset next_hop (fallback to FloodingRouter) + p.packet->next_hop = NO_NEXT_HOP_PREFERENCE; + // Also reset it in the nodeDB + meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); + if (sentTo) { + LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to); + sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; } + FloodingRouter::send(packetPool.allocCopy(*p.packet)); + } else { + NextHopRouter::send(packetPool.allocCopy(*p.packet)); + } + } else { + // Note: we call the superclass version because we don't want to have our version of send() add a new + // retransmission record + FloodingRouter::send(packetPool.allocCopy(*p.packet)); } - if (stillValid) { - // Update our desired sleep delay - int32_t t = p.nextTxMsec - now; - - d = min(t, d); - } + // Queue again + --p.numRetransmissions; + setNextTx(&p); + } } - return d; + if (stillValid) { + // Update our desired sleep delay + int32_t t = p.nextTxMsec - now; + + d = min(t, d); + } + } + + return d; } -void NextHopRouter::setNextTx(PendingPacket *pending) -{ - assert(iface); - auto d = iface->getRetransmissionMsec(pending->packet); - pending->nextTxMsec = millis() + d; - LOG_DEBUG("Setting next retransmission in %u msecs: ", d); - printPacket("", pending->packet); - setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time +void NextHopRouter::setNextTx(PendingPacket *pending) { + assert(iface); + auto d = iface->getRetransmissionMsec(pending->packet); + pending->nextTxMsec = millis() + d; + LOG_DEBUG("Setting next retransmission in %u msecs: ", d); + printPacket("", pending->packet); + setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time } diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index c1df3596b..0fbffaa5f 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -8,147 +8,143 @@ * to that message */ struct GlobalPacketId { - NodeNum node; - PacketId id; + NodeNum node; + PacketId id; - bool operator==(const GlobalPacketId &p) const { return node == p.node && id == p.id; } + bool operator==(const GlobalPacketId &p) const { return node == p.node && id == p.id; } - explicit GlobalPacketId(const meshtastic_MeshPacket *p) - { - node = getFrom(p); - id = p->id; - } + explicit GlobalPacketId(const meshtastic_MeshPacket *p) { + node = getFrom(p); + id = p->id; + } - GlobalPacketId(NodeNum _from, PacketId _id) - { - node = _from; - id = _id; - } + GlobalPacketId(NodeNum _from, PacketId _id) { + node = _from; + id = _id; + } }; /** * A packet queued for retransmission */ struct PendingPacket { - meshtastic_MeshPacket *packet; + meshtastic_MeshPacket *packet; - /** The next time we should try to retransmit this packet */ - uint32_t nextTxMsec = 0; + /** The next time we should try to retransmit this packet */ + uint32_t nextTxMsec = 0; - /** Starts at NUM_RETRANSMISSIONS -1 and counts down. Once zero it will be removed from the list */ - uint8_t numRetransmissions = 0; + /** Starts at NUM_RETRANSMISSIONS -1 and counts down. Once zero it will be removed from the list */ + uint8_t numRetransmissions = 0; - PendingPacket() {} - explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions); + PendingPacket() {} + explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions); }; -class GlobalPacketIdHashFunction -{ - public: - size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } +class GlobalPacketIdHashFunction { +public: + size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } }; /* Router for direct messages, which only relays if it is the next hop for a packet. The next hop is set by the current - relayer of a packet, which bases this on information from a previous successful delivery to the destination via flooding. - Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered back to us via a node - that also relayed the original packet, we use that node as next hop for the destination from then on. This makes sure that only - when there’s a two-way connection, we assign a next hop. Both the ReliableRouter and NextHopRouter will do retransmissions (the - NextHopRouter only 1 time). For the final retry, if no one actually relayed the packet, it will reset the next hop in order to - fall back to the FloodingRouter again. Note that thus also intermediate hops will do a single retransmission if the intended - next-hop didn’t relay, in order to fix changes in the middle of the route. + relayer of a packet, which bases this on information from a previous successful delivery to the destination via + flooding. Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered + back to us via a node that also relayed the original packet, we use that node as next hop for the destination from + then on. This makes sure that only when there’s a two-way connection, we assign a next hop. Both the ReliableRouter + and NextHopRouter will do retransmissions (the NextHopRouter only 1 time). For the final retry, if no one actually + relayed the packet, it will reset the next hop in order to fall back to the FloodingRouter again. Note that thus also + intermediate hops will do a single retransmission if the intended next-hop didn’t relay, in order to fix changes in + the middle of the route. */ -class NextHopRouter : public FloodingRouter -{ - public: - /** - * Constructor - * - */ - NextHopRouter(); +class NextHopRouter : public FloodingRouter { +public: + /** + * Constructor + * + */ + NextHopRouter(); - /** - * Send a packet - * @return an error code - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + /** + * Send a packet + * @return an error code + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - /** Do our retransmission handling */ - virtual int32_t runOnce() override - { - // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation - doRetransmissions(); + /** Do our retransmission handling */ + virtual int32_t runOnce() override { + // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation + doRetransmissions(); - int32_t r = FloodingRouter::runOnce(); + int32_t r = FloodingRouter::runOnce(); - // Also after calling runOnce there might be new packets to retransmit - auto d = doRetransmissions(); - return min(d, r); - } + // Also after calling runOnce there might be new packets to retransmit + auto d = doRetransmissions(); + return min(d, r); + } - // The number of retransmissions intermediate nodes will do (actually 1 less than this) - constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2; - // The number of retransmissions the original sender will do - constexpr static uint8_t NUM_RELIABLE_RETX = 3; + // The number of retransmissions intermediate nodes will do (actually 1 less than this) + constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2; + // The number of retransmissions the original sender will do + constexpr static uint8_t NUM_RELIABLE_RETX = 3; - protected: - /** - * Pending retransmissions - */ - std::unordered_map pending; +protected: + /** + * Pending retransmissions + */ + std::unordered_map pending; - /** - * Should this incoming filter be dropped? - * - * Called immediately on reception, before any further processing. - * @return true to abandon the packet - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + /** + * Should this incoming filter be dropped? + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; - /** - * Look for packets we need to relay - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + /** + * Look for packets we need to relay + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /** - * Try to find the pending packet record for this ID (or NULL if not found) - */ - PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } - PendingPacket *findPendingPacket(GlobalPacketId p); + /** + * Try to find the pending packet record for this ID (or NULL if not found) + */ + PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } + PendingPacket *findPendingPacket(GlobalPacketId p); - /** - * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. - */ - PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); + /** + * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. + */ + PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); - // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) - bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); - /** - * Stop any retransmissions we are doing of the specified node/packet ID pair - * - * @return true if we found and removed a transmission with this ID - */ - bool stopRetransmission(NodeNum from, PacketId id); - bool stopRetransmission(GlobalPacketId p); + /** + * Stop any retransmissions we are doing of the specified node/packet ID pair + * + * @return true if we found and removed a transmission with this ID + */ + bool stopRetransmission(NodeNum from, PacketId id); + bool stopRetransmission(GlobalPacketId p); - /** - * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) - * - * @return the number of msecs until our next retransmission or MAXINT if none scheduled - */ - int32_t doRetransmissions(); + /** + * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) + * + * @return the number of msecs until our next retransmission or MAXINT if none scheduled + */ + int32_t doRetransmissions(); - void setNextTx(PendingPacket *pending); + void setNextTx(PendingPacket *pending); - private: - /** - * Get the next hop for a destination, given the relay node - * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) - */ - uint8_t getNextHop(NodeNum to, uint8_t relay_node); +private: + /** + * Get the next hop for a destination, given the relay node + * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) + */ + uint8_t getNextHop(NodeNum to, uint8_t relay_node); - /** Check if we should be rebroadcasting this packet if so, do so. - * @return true if we did rebroadcast */ - bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override; + /** Check if we should be rebroadcasting this packet if so, do so. + * @return true if we did rebroadcast */ + bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override; }; \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3c408f01f..d6ad61fad 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -79,94 +79,89 @@ static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2; #ifdef HELTEC_MESH_NODE_T114 -uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) -{ - uint32_t ret = 0; - uint8_t SDAPIN = mosi; - pinMode(SDAPIN, INPUT_PULLUP); - digitalWrite(dc, HIGH); - for (int i = 0; i < dummy; i++) { // any dummy clocks - digitalWrite(sck, HIGH); - delay(1); - digitalWrite(sck, LOW); - delay(1); - } - for (int i = 0; i < bits; i++) { // read results - ret <<= 1; - delay(1); - if (digitalRead(SDAPIN)) - ret |= 1; - ; - digitalWrite(sck, HIGH); - delay(1); - digitalWrite(sck, LOW); - } - return ret; +uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { + uint32_t ret = 0; + uint8_t SDAPIN = mosi; + pinMode(SDAPIN, INPUT_PULLUP); + digitalWrite(dc, HIGH); + for (int i = 0; i < dummy; i++) { // any dummy clocks + digitalWrite(sck, HIGH); + delay(1); + digitalWrite(sck, LOW); + delay(1); + } + for (int i = 0; i < bits; i++) { // read results + ret <<= 1; + delay(1); + if (digitalRead(SDAPIN)) + ret |= 1; + ; + digitalWrite(sck, HIGH); + delay(1); + digitalWrite(sck, LOW); + } + return ret; } -void write9(uint8_t val, uint8_t dc_val, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) -{ - pinMode(mosi, OUTPUT); - digitalWrite(dc, dc_val); - for (int i = 0; i < 8; i++) { // send command - digitalWrite(mosi, (val & 0x80) != 0); - delay(1); - digitalWrite(sck, HIGH); - delay(1); - digitalWrite(sck, LOW); - val <<= 1; - } +void write9(uint8_t val, uint8_t dc_val, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { + pinMode(mosi, OUTPUT); + digitalWrite(dc, dc_val); + for (int i = 0; i < 8; i++) { // send command + digitalWrite(mosi, (val & 0x80) != 0); + delay(1); + digitalWrite(sck, HIGH); + delay(1); + digitalWrite(sck, LOW); + val <<= 1; + } } -uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) -{ - digitalWrite(cs, LOW); - write9(cmd, 0, cs, sck, mosi, dc, rst); - uint32_t ret = read8(bits, dummy, cs, sck, mosi, dc, rst); - digitalWrite(cs, HIGH); - return ret; +uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { + digitalWrite(cs, LOW); + write9(cmd, 0, cs, sck, mosi, dc, rst); + uint32_t ret = read8(bits, dummy, cs, sck, mosi, dc, rst); + digitalWrite(cs, HIGH); + return ret; } -uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) -{ - pinMode(cs, OUTPUT); - digitalWrite(cs, HIGH); - pinMode(cs, OUTPUT); - pinMode(sck, OUTPUT); - pinMode(mosi, OUTPUT); - pinMode(dc, OUTPUT); - pinMode(rst, OUTPUT); - digitalWrite(rst, LOW); // Hardware Reset - delay(10); - digitalWrite(rst, HIGH); - delay(10); +uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { + pinMode(cs, OUTPUT); + digitalWrite(cs, HIGH); + pinMode(cs, OUTPUT); + pinMode(sck, OUTPUT); + pinMode(mosi, OUTPUT); + pinMode(dc, OUTPUT); + pinMode(rst, OUTPUT); + digitalWrite(rst, LOW); // Hardware Reset + delay(10); + digitalWrite(rst, HIGH); + delay(10); - uint32_t ID = 0; - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice - return ID; + uint32_t ID = 0; + ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); + ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice + return ID; } #endif -bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) -{ - if (ostream) { - std::vector const *vec = (std::vector *)field->pData; - for (auto item : *vec) { - if (!pb_encode_tag_for_field(ostream, field)) - return false; - pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); - } +bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) { + if (ostream) { + std::vector const *vec = (std::vector *)field->pData; + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); } - if (istream) { - meshtastic_NodeInfoLite node; // this gets good data - std::vector *vec = (std::vector *)field->pData; + } + if (istream) { + meshtastic_NodeInfoLite node; // this gets good data + std::vector *vec = (std::vector *)field->pData; - if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) - vec->push_back(node); - } - return true; + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) + vec->push_back(node); + } + return true; } /** The current change # for radio settings. Starts at 0 on boot and any time the radio settings @@ -191,917 +186,884 @@ uint32_t error_address = 0; static uint8_t ourMacAddr[6]; -NodeDB::NodeDB() -{ - LOG_INFO("Init NodeDB"); - loadFromDisk(); - cleanupMeshDB(); +NodeDB::NodeDB() { + LOG_INFO("Init NodeDB"); + loadFromDisk(); + cleanupMeshDB(); - uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); - uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)); - uint32_t configCRC = crc32Buffer(&config, sizeof(config)); - uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); + uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); + uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)); + uint32_t configCRC = crc32Buffer(&config, sizeof(config)); + uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); - int saveWhat = 0; - // Get device unique id + int saveWhat = 0; + // Get device unique id #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) - uint32_t unique_id[4]; - // ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series - // This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us - esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8); - if (err == ESP_OK) { - memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id)); - myNodeInfo.device_id.size = 16; - } else { - LOG_WARN("Failed to read unique id from efuse"); - } -#elif defined(ARCH_NRF52) - // Nordic applies a FIPS compliant Random ID to each chip at the factory - // We concatenate the device address to the Random ID to create a unique ID for now - // This will likely utilize a crypto module in the future - uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0]; - uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; - memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start)); - memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); + uint32_t unique_id[4]; + // ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series + // This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us + esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8); + if (err == ESP_OK) { + memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id)); myNodeInfo.device_id.size = 16; - // Uncomment below to print the device id + } else { + LOG_WARN("Failed to read unique id from efuse"); + } +#elif defined(ARCH_NRF52) + // Nordic applies a FIPS compliant Random ID to each chip at the factory + // We concatenate the device address to the Random ID to create a unique ID for now + // This will likely utilize a crypto module in the future + uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0]; + uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; + memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start)); + memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); + myNodeInfo.device_id.size = 16; + // Uncomment below to print the device id #elif ARCH_PORTDUINO - if (portduino_config.has_device_id) { - memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); - myNodeInfo.device_id.size = 16; - } + if (portduino_config.has_device_id) { + memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); + myNodeInfo.device_id.size = 16; + } #else - // FIXME - implement for other platforms + // FIXME - implement for other platforms #endif - // if (myNodeInfo.device_id.size == 16) { - // std::string deviceIdHex; - // for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) { - // char buf[3]; - // snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]); - // deviceIdHex += buf; - // } - // LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str()); - // } + // if (myNodeInfo.device_id.size == 16) { + // std::string deviceIdHex; + // for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) { + // char buf[3]; + // snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]); + // deviceIdHex += buf; + // } + // LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str()); + // } - // likewise - we always want the app requirements to come from the running appload - myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00 - // Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we won't - // keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid conflicts) - pickNewNodeNum(); + // likewise - we always want the app requirements to come from the running appload + myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00 + // Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we + // won't keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid + // conflicts) + pickNewNodeNum(); - // Set our board type so we can share it with others - owner.hw_model = HW_VENDOR; - // Ensure user (nodeinfo) role is set to whatever we're configured to - owner.role = config.device.role; - // Ensure macaddr is set to our macaddr as it will be copied in our info below - memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); - // Ensure owner.id is always derived from the node number - snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); + // Set our board type so we can share it with others + owner.hw_model = HW_VENDOR; + // Ensure user (nodeinfo) role is set to whatever we're configured to + owner.role = config.device.role; + // Ensure macaddr is set to our macaddr as it will be copied in our info below + memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); + // Ensure owner.id is always derived from the node number + snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); - if (!config.has_security) { - config.has_security = true; - config.security = meshtastic_Config_SecurityConfig_init_default; - config.security.serial_enabled = config.device.serial_enabled; - config.security.is_managed = config.device.is_managed; - } + if (!config.has_security) { + config.has_security = true; + config.security = meshtastic_Config_SecurityConfig_init_default; + config.security.serial_enabled = config.device.serial_enabled; + config.security.is_managed = config.device.is_managed; + } #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - bool keygenSuccess = false; - keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key); - if (config.security.private_key.size == 32 && !keyIsLowEntropy) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + bool keygenSuccess = false; + keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key); + if (config.security.private_key.size == 32 && !keyIsLowEntropy) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } #elif !(MESHTASTIC_EXCLUDE_PKI) - // Calculate Curve25519 public and private keys - if (config.security.private_key.size == 32 && config.security.public_key.size == 32) { - owner.public_key.size = config.security.public_key.size; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); - crypto->setDHPrivateKey(config.security.private_key.bytes); - } + // Calculate Curve25519 public and private keys + if (config.security.private_key.size == 32 && config.security.public_key.size == 32) { + owner.public_key.size = config.security.public_key.size; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); + crypto->setDHPrivateKey(config.security.private_key.bytes); + } #endif - // Include our owner in the node db under our nodenum - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); - info->user = TypeConversions::ConvertToUserLite(owner); - info->has_user = true; + // Include our owner in the node db under our nodenum + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); + info->user = TypeConversions::ConvertToUserLite(owner); + info->has_user = true; - // If node database has not been saved for the first time, save it now + // If node database has not been saved for the first time, save it now #ifdef FSCom - if (!FSCom.exists(nodeDatabaseFileName)) { - saveNodeDatabaseToDisk(); - } + if (!FSCom.exists(nodeDatabaseFileName)) { + saveNodeDatabaseToDisk(); + } #endif #ifdef ARCH_ESP32 - Preferences preferences; - preferences.begin("meshtastic", false); - myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0); - preferences.end(); - LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count); + Preferences preferences; + preferences.begin("meshtastic", false); + myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0); + preferences.end(); + LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count); #endif - resetRadioConfig(); // If bogus settings got saved, then fix them - // nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes); + resetRadioConfig(); // If bogus settings got saved, then fix them + // nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes); - // Uncomment below to always enable UDP broadcasts - // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; + // Uncomment below to always enable UDP broadcasts + // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; - // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value - // of 30 minutes or more - if (channels.isDefaultChannel(channels.getPrimaryIndex())) { - LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); - moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue( - moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.environment_update_interval = Default::getConfiguredOrMinimumValue( - moduleConfig.telemetry.environment_update_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.air_quality_interval = Default::getConfiguredOrMinimumValue( - moduleConfig.telemetry.air_quality_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.power_update_interval = Default::getConfiguredOrMinimumValue( - moduleConfig.telemetry.power_update_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue( - moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); - } - // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched - if (config.device.node_info_broadcast_secs > MAX_INTERVAL) - config.device.node_info_broadcast_secs = MAX_INTERVAL; - if (config.position.position_broadcast_secs > MAX_INTERVAL) - config.position.position_broadcast_secs = MAX_INTERVAL; - if (config.position.gps_update_interval > MAX_INTERVAL) - config.position.gps_update_interval = MAX_INTERVAL; - if (config.position.gps_attempt_time > MAX_INTERVAL) - config.position.gps_attempt_time = MAX_INTERVAL; - if (config.position.position_flags > MAX_INTERVAL) - config.position.position_flags = MAX_INTERVAL; - if (config.position.rx_gpio > MAX_INTERVAL) - config.position.rx_gpio = MAX_INTERVAL; - if (config.position.tx_gpio > MAX_INTERVAL) - config.position.tx_gpio = MAX_INTERVAL; - if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL) - config.position.broadcast_smart_minimum_distance = MAX_INTERVAL; - if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL) - config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL; - if (config.position.gps_en_gpio > MAX_INTERVAL) - config.position.gps_en_gpio = MAX_INTERVAL; - if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) - moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) - moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) - moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) - moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) - moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; + // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum + // value of 30 minutes or more + if (channels.isDefaultChannel(channels.getPrimaryIndex())) { + LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); + moduleConfig.telemetry.device_update_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.environment_update_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.environment_update_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.air_quality_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.air_quality_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.power_update_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.power_update_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.health_update_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); + } + // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched + if (config.device.node_info_broadcast_secs > MAX_INTERVAL) + config.device.node_info_broadcast_secs = MAX_INTERVAL; + if (config.position.position_broadcast_secs > MAX_INTERVAL) + config.position.position_broadcast_secs = MAX_INTERVAL; + if (config.position.gps_update_interval > MAX_INTERVAL) + config.position.gps_update_interval = MAX_INTERVAL; + if (config.position.gps_attempt_time > MAX_INTERVAL) + config.position.gps_attempt_time = MAX_INTERVAL; + if (config.position.position_flags > MAX_INTERVAL) + config.position.position_flags = MAX_INTERVAL; + if (config.position.rx_gpio > MAX_INTERVAL) + config.position.rx_gpio = MAX_INTERVAL; + if (config.position.tx_gpio > MAX_INTERVAL) + config.position.tx_gpio = MAX_INTERVAL; + if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL) + config.position.broadcast_smart_minimum_distance = MAX_INTERVAL; + if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL) + config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL; + if (config.position.gps_en_gpio > MAX_INTERVAL) + config.position.gps_en_gpio = MAX_INTERVAL; + if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; - if (moduleConfig.mqtt.has_map_report_settings && - moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { - moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; - } + if (moduleConfig.mqtt.has_map_report_settings && moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { + moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; + } - // Ensure that the neighbor info update interval is coerced to the minimum - moduleConfig.neighbor_info.update_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs); + // Ensure that the neighbor info update interval is coerced to the minimum + moduleConfig.neighbor_info.update_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs); - // Don't let licensed users to rebroadcast encrypted packets - if (owner.is_licensed) { - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - } + // Don't let licensed users to rebroadcast encrypted packets + if (owner.is_licensed) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + } #if !HAS_TFT - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - // On a device without MUI, this display mode makes no sense, and will break logic. - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - config.bluetooth.enabled = true; - } + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // On a device without MUI, this display mode makes no sense, and will break logic. + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + config.bluetooth.enabled = true; + } #endif - if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) - saveWhat |= SEGMENT_DEVICESTATE; - if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) - saveWhat |= SEGMENT_NODEDATABASE; - if (configCRC != crc32Buffer(&config, sizeof(config))) - saveWhat |= SEGMENT_CONFIG; - if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile))) - saveWhat |= SEGMENT_CHANNELS; + if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) + saveWhat |= SEGMENT_DEVICESTATE; + if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) + saveWhat |= SEGMENT_NODEDATABASE; + if (configCRC != crc32Buffer(&config, sizeof(config))) + saveWhat |= SEGMENT_CONFIG; + if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile))) + saveWhat |= SEGMENT_CHANNELS; - if (config.position.gps_enabled) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - config.position.gps_enabled = 0; - } + if (config.position.gps_enabled) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + config.position.gps_enabled = 0; + } #ifdef USERPREFS_FIRMWARE_EDITION - myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; + myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; #endif #ifdef USERPREFS_FIXED_GPS - if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. - meshtastic_Position fixedGPS = meshtastic_Position_init_default; + if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. + meshtastic_Position fixedGPS = meshtastic_Position_init_default; #ifdef USERPREFS_FIXED_GPS_LAT - fixedGPS.latitude_i = (int32_t)(USERPREFS_FIXED_GPS_LAT * 1e7); - fixedGPS.has_latitude_i = true; + fixedGPS.latitude_i = (int32_t)(USERPREFS_FIXED_GPS_LAT * 1e7); + fixedGPS.has_latitude_i = true; #endif #ifdef USERPREFS_FIXED_GPS_LON - fixedGPS.longitude_i = (int32_t)(USERPREFS_FIXED_GPS_LON * 1e7); - fixedGPS.has_longitude_i = true; + fixedGPS.longitude_i = (int32_t)(USERPREFS_FIXED_GPS_LON * 1e7); + fixedGPS.has_longitude_i = true; #endif #ifdef USERPREFS_FIXED_GPS_ALT - fixedGPS.altitude = USERPREFS_FIXED_GPS_ALT; - fixedGPS.has_altitude = true; + fixedGPS.altitude = USERPREFS_FIXED_GPS_ALT; + fixedGPS.has_altitude = true; #endif #if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON) - fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; - config.has_position = true; - info->has_position = true; - info->position = TypeConversions::ConvertToPositionLite(fixedGPS); - nodeDB->setLocalPosition(fixedGPS); - config.position.fixed_position = true; + fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; + config.has_position = true; + info->has_position = true; + info->position = TypeConversions::ConvertToPositionLite(fixedGPS); + nodeDB->setLocalPosition(fixedGPS); + config.position.fixed_position = true; #endif - } + } #endif - sortMeshDB(); - saveToDisk(saveWhat); + sortMeshDB(); + saveToDisk(saveWhat); } /** - * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on - * the local node. If from is zero this function returns our node number instead + * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they + * originated on the local node. If from is zero this function returns our node number instead */ -NodeNum getFrom(const meshtastic_MeshPacket *p) -{ - return (p->from == 0) ? nodeDB->getNodeNum() : p->from; -} +NodeNum getFrom(const meshtastic_MeshPacket *p) { return (p->from == 0) ? nodeDB->getNodeNum() : p->from; } // Returns true if the packet originated from the local node -bool isFromUs(const meshtastic_MeshPacket *p) -{ - return p->from == 0 || p->from == nodeDB->getNodeNum(); -} +bool isFromUs(const meshtastic_MeshPacket *p) { return p->from == 0 || p->from == nodeDB->getNodeNum(); } // Returns true if the packet is destined to us -bool isToUs(const meshtastic_MeshPacket *p) -{ - return p->to == nodeDB->getNodeNum(); +bool isToUs(const meshtastic_MeshPacket *p) { return p->to == nodeDB->getNodeNum(); } + +bool isBroadcast(uint32_t dest) { return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; } + +void NodeDB::resetRadioConfig(bool is_fresh_install) { + if (is_fresh_install) { + radioGeneration++; + } + + if (channelFile.channels_count != MAX_NUM_CHANNELS) { + LOG_INFO("Set default channel and radio preferences!"); + + channels.initDefaults(); + } + + channels.onConfigChanged(); + + // Update the global myRegion + initRegion(); } -bool isBroadcast(uint32_t dest) -{ - return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; -} - -void NodeDB::resetRadioConfig(bool is_fresh_install) -{ - if (is_fresh_install) { - radioGeneration++; - } - - if (channelFile.channels_count != MAX_NUM_CHANNELS) { - LOG_INFO("Set default channel and radio preferences!"); - - channels.initDefaults(); - } - - channels.onConfigChanged(); - - // Update the global myRegion - initRegion(); -} - -bool NodeDB::factoryReset(bool eraseBleBonds) -{ - LOG_INFO("Perform factory reset!"); - // first, remove the "/prefs" (this removes most prefs) - spiLock->lock(); - rmDir("/prefs"); // this uses spilock internally... +bool NodeDB::factoryReset(bool eraseBleBonds) { + LOG_INFO("Perform factory reset!"); + // first, remove the "/prefs" (this removes most prefs) + spiLock->lock(); + rmDir("/prefs"); // this uses spilock internally... #ifdef FSCom - if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) { - LOG_ERROR("Could not remove rangetest.csv file"); - } + if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) { + LOG_ERROR("Could not remove rangetest.csv file"); + } #endif - spiLock->unlock(); - // second, install default state (this will deal with the duplicate mac address issue) - installDefaultNodeDatabase(); - installDefaultDeviceState(); - installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds - installDefaultModuleConfig(); - installDefaultChannels(); - // third, write everything to disk - saveToDisk(); - if (eraseBleBonds) { - LOG_INFO("Erase BLE bonds"); + spiLock->unlock(); + // second, install default state (this will deal with the duplicate mac address issue) + installDefaultNodeDatabase(); + installDefaultDeviceState(); + installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds + installDefaultModuleConfig(); + installDefaultChannels(); + // third, write everything to disk + saveToDisk(); + if (eraseBleBonds) { + LOG_INFO("Erase BLE bonds"); #ifdef ARCH_ESP32 - // This will erase what's in NVS including ssl keys, persistent variables and ble pairing - nvs_flash_erase(); + // This will erase what's in NVS including ssl keys, persistent variables and ble pairing + nvs_flash_erase(); #endif #ifdef ARCH_NRF52 - LOG_INFO("Clear bluetooth bonds!"); - bond_print_list(BLE_GAP_ROLE_PERIPH); - bond_print_list(BLE_GAP_ROLE_CENTRAL); - Bluefruit.Periph.clearBonds(); - Bluefruit.Central.clearBonds(); + LOG_INFO("Clear bluetooth bonds!"); + bond_print_list(BLE_GAP_ROLE_PERIPH); + bond_print_list(BLE_GAP_ROLE_CENTRAL); + Bluefruit.Periph.clearBonds(); + Bluefruit.Central.clearBonds(); #endif - } - return true; + } + return true; } -void NodeDB::installDefaultNodeDatabase() -{ - LOG_DEBUG("Install default NodeDatabase"); - nodeDatabase.version = DEVICESTATE_CUR_VER; - nodeDatabase.nodes = std::vector(MAX_NUM_NODES); - numMeshNodes = 0; - meshNodes = &nodeDatabase.nodes; +void NodeDB::installDefaultNodeDatabase() { + LOG_DEBUG("Install default NodeDatabase"); + nodeDatabase.version = DEVICESTATE_CUR_VER; + nodeDatabase.nodes = std::vector(MAX_NUM_NODES); + numMeshNodes = 0; + meshNodes = &nodeDatabase.nodes; } -void NodeDB::installDefaultConfig(bool preserveKey = false) -{ - uint8_t private_key_temp[32]; - bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size > 0; - if (shouldPreserveKey) { - memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size); - } - LOG_INFO("Install default LocalConfig"); - memset(&config, 0, sizeof(meshtastic_LocalConfig)); - config.version = DEVICESTATE_CUR_VER; - config.has_device = true; - config.has_display = true; - config.has_lora = true; - config.has_position = true; - config.has_power = true; - config.has_network = true; - config.has_bluetooth = (HAS_BLUETOOTH ? true : false); - config.has_security = true; - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; +void NodeDB::installDefaultConfig(bool preserveKey = false) { + uint8_t private_key_temp[32]; + bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size > 0; + if (shouldPreserveKey) { + memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size); + } + LOG_INFO("Install default LocalConfig"); + memset(&config, 0, sizeof(meshtastic_LocalConfig)); + config.version = DEVICESTATE_CUR_VER; + config.has_device = true; + config.has_display = true; + config.has_lora = true; + config.has_position = true; + config.has_power = true; + config.has_network = true; + config.has_bluetooth = (HAS_BLUETOOTH ? true : false); + config.has_security = true; + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - config.lora.sx126x_rx_boosted_gain = true; - config.lora.tx_enabled = - true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) - config.lora.override_duty_cycle = false; - config.lora.config_ok_to_mqtt = false; + config.lora.sx126x_rx_boosted_gain = true; + config.lora.tx_enabled = true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) + config.lora.override_duty_cycle = false; + config.lora.config_ok_to_mqtt = false; #if HAS_TFT // For the devices that support MUI, default to that - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif #ifdef USERPREFS_CONFIG_DEVICE_ROLE - // Restrict ROUTER*, LOST AND FOUND roles for security reasons - if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { - LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - } else { - config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; - } + // Restrict ROUTER*, LOST AND FOUND roles for security reasons + if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, + meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { + LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else { + config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; + } #else - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. #endif #ifdef USERPREFS_CONFIG_LORA_REGION - config.lora.region = USERPREFS_CONFIG_LORA_REGION; + config.lora.region = USERPREFS_CONFIG_LORA_REGION; #else - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; #endif #ifdef USERPREFS_LORACONFIG_MODEM_PRESET - config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; + config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; #else - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; #endif - config.lora.hop_limit = HOP_RELIABLE; + config.lora.hop_limit = HOP_RELIABLE; #ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT - config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT; + config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT; #else - config.lora.ignore_mqtt = false; + config.lora.ignore_mqtt = false; #endif - // Initialize admin_key_count to zero - byte numAdminKeys = 0; + // Initialize admin_key_count to zero + byte numAdminKeys = 0; #ifdef USERPREFS_USE_ADMIN_KEY_0 - // Check if USERPREFS_ADMIN_KEY_0 is non-empty - if (sizeof(userprefs_admin_key_0) > 0) { - memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); - config.security.admin_key[0].size = 32; - numAdminKeys++; - } + // Check if USERPREFS_ADMIN_KEY_0 is non-empty + if (sizeof(userprefs_admin_key_0) > 0) { + memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); + config.security.admin_key[0].size = 32; + numAdminKeys++; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 - // Check if USERPREFS_ADMIN_KEY_1 is non-empty - if (sizeof(userprefs_admin_key_1) > 0) { - memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); - config.security.admin_key[1].size = 32; - numAdminKeys++; - } + // Check if USERPREFS_ADMIN_KEY_1 is non-empty + if (sizeof(userprefs_admin_key_1) > 0) { + memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); + config.security.admin_key[1].size = 32; + numAdminKeys++; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 - // Check if USERPREFS_ADMIN_KEY_2 is non-empty - if (sizeof(userprefs_admin_key_2) > 0) { - memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); - config.security.admin_key[2].size = 32; - numAdminKeys++; - } + // Check if USERPREFS_ADMIN_KEY_2 is non-empty + if (sizeof(userprefs_admin_key_2) > 0) { + memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); + config.security.admin_key[2].size = 32; + numAdminKeys++; + } #endif - config.security.admin_key_count = numAdminKeys; + config.security.admin_key_count = numAdminKeys; - if (shouldPreserveKey) { - config.security.private_key.size = 32; - memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size); - printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size); - } else { - config.security.private_key.size = 0; - } - config.security.public_key.size = 0; + if (shouldPreserveKey) { + config.security.private_key.size = 32; + memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size); + printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size); + } else { + config.security.private_key.size = 0; + } + config.security.public_key.size = 0; #ifdef PIN_GPS_EN - config.position.gps_en_gpio = PIN_GPS_EN; + config.position.gps_en_gpio = PIN_GPS_EN; #endif #if defined(USERPREFS_CONFIG_GPS_MODE) - config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; + config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; #elif !HAS_GPS || GPS_DEFAULT_NOT_PRESENT - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; #elif !defined(GPS_RX_PIN) - if (config.position.rx_gpio == 0) - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; - else - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + if (config.position.rx_gpio == 0) + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; + else + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; #else - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; #endif #ifdef USERPREFS_CONFIG_SMART_POSITION_ENABLED - config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED; + config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED; #else - config.position.position_broadcast_smart_enabled = true; + config.position.position_broadcast_smart_enabled = true; #endif - config.position.broadcast_smart_minimum_distance = 100; - config.position.broadcast_smart_minimum_interval_secs = 30; - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) - config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; - config.security.serial_enabled = true; - config.security.admin_channel_enabled = false; - resetRadioConfig(true); // This also triggers NodeInfo/Position requests since we're fresh - strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); + config.position.broadcast_smart_minimum_distance = 100; + config.position.broadcast_smart_minimum_interval_secs = 30; + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) + config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; + config.security.serial_enabled = true; + config.security.admin_channel_enabled = false; + resetRadioConfig(true); // This also triggers NodeInfo/Position requests since we're fresh + strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); -#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \ - defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \ +#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \ + defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \ HAS_TFT - // switch BT off by default; use TFT programming mode or hotkey to enable - config.bluetooth.enabled = false; + // switch BT off by default; use TFT programming mode or hotkey to enable + config.bluetooth.enabled = false; #else - // default to bluetooth capability of platform as default - config.bluetooth.enabled = true; + // default to bluetooth capability of platform as default + config.bluetooth.enabled = true; #endif - config.bluetooth.fixed_pin = defaultBLEPin; + config.bluetooth.fixed_pin = defaultBLEPin; -#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || \ - defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) - bool hasScreen = true; +#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || defined(HX8357_CS) || \ + defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || defined(USE_ST7796) || \ + defined(HACKADAY_COMMUNICATOR) + bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 - uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); - if (st7789_id == 0xFFFFFF) { - hasScreen = false; - } + uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); + if (st7789_id == 0xFFFFFF) { + hasScreen = false; + } #endif #elif ARCH_PORTDUINO - bool hasScreen = false; - if (portduino_config.displayPanel) - hasScreen = true; - else - hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; + bool hasScreen = false; + if (portduino_config.displayPanel) + hasScreen = true; + else + hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #elif MESHTASTIC_INCLUDE_NICHE_GRAPHICS // See "src/graphics/niche" - bool hasScreen = true; // Use random pin for Bluetooth pairing + bool hasScreen = true; // Use random pin for Bluetooth pairing #else - bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; + bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #endif #ifdef USERPREFS_FIXED_BLUETOOTH - config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH; - config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH; + config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; #else - config.bluetooth.mode = hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN - : meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + config.bluetooth.mode = + hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN : meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; #endif - // for backward compat, default position flags are ALT+MSL - config.position.position_flags = - (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL | - meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | - meshtastic_Config_PositionConfig_PositionFlags_DOP | meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW); + // for backward compat, default position flags are ALT+MSL + config.position.position_flags = + (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL | + meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | + meshtastic_Config_PositionConfig_PositionFlags_DOP | meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW); // Set default value for 'Mesh via UDP' #if HAS_UDP_MULTICAST #ifdef USERPREFS_NETWORK_ENABLED_PROTOCOLS - config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS; + config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS; #else - config.network.enabled_protocols = 0; + config.network.enabled_protocols = 0; #endif #endif #ifdef USERPREFS_NETWORK_WIFI_ENABLED - config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED; + config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED; #endif #ifdef USERPREFS_NETWORK_WIFI_SSID - strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid)); + strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid)); #endif #ifdef USERPREFS_NETWORK_WIFI_PSK - strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk)); + strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk)); #endif #if defined(USERPREFS_NETWORK_IPV6_ENABLED) - config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED; + config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED; #else - config.network.ipv6_enabled = default_network_ipv6_enabled; + config.network.ipv6_enabled = default_network_ipv6_enabled; #endif #ifdef DISPLAY_FLIP_SCREEN - config.display.flip_screen = true; + config.display.flip_screen = true; #endif #ifdef RAK4630 - config.display.wake_on_tap_or_motion = true; + config.display.wake_on_tap_or_motion = true; #endif #if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) - config.display.screen_on_secs = 30; - config.display.wake_on_tap_or_motion = true; + config.display.screen_on_secs = 30; + config.display.wake_on_tap_or_motion = true; #endif #ifdef COMPASS_ORIENTATION - config.display.compass_orientation = COMPASS_ORIENTATION; + config.display.compass_orientation = COMPASS_ORIENTATION; #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::isUpdated()) { - WiFiOTA::recoverConfig(&config.network); - } + if (WiFiOTA::isUpdated()) { + WiFiOTA::recoverConfig(&config.network); + } #endif #ifdef USERPREFS_CONFIG_DEVICE_ROLE - // Apply role-specific defaults when role is set via user preferences - installRoleDefaults(config.device.role); + // Apply role-specific defaults when role is set via user preferences + installRoleDefaults(config.device.role); #endif - initConfigIntervals(); + initConfigIntervals(); } -void NodeDB::initConfigIntervals() -{ +void NodeDB::initConfigIntervals() { #ifdef USERPREFS_CONFIG_GPS_UPDATE_INTERVAL - config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL; + config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL; #else - config.position.gps_update_interval = default_gps_update_interval; + config.position.gps_update_interval = default_gps_update_interval; #endif #ifdef USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL - config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL; + config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL; #else - config.position.position_broadcast_secs = default_broadcast_interval_secs; + config.position.position_broadcast_secs = default_broadcast_interval_secs; #endif - config.power.ls_secs = default_ls_secs; - config.power.min_wake_secs = default_min_wake_secs; - config.power.sds_secs = default_sds_secs; - config.power.wait_bluetooth_secs = default_wait_bluetooth_secs; + config.power.ls_secs = default_ls_secs; + config.power.min_wake_secs = default_min_wake_secs; + config.power.sds_secs = default_sds_secs; + config.power.wait_bluetooth_secs = default_wait_bluetooth_secs; - config.display.screen_on_secs = default_screen_on_secs; + config.display.screen_on_secs = default_screen_on_secs; #if defined(USE_POWERSAVE) - config.power.is_power_saving = true; - config.display.screen_on_secs = 30; - config.power.wait_bluetooth_secs = 30; + config.power.is_power_saving = true; + config.display.screen_on_secs = 30; + config.power.wait_bluetooth_secs = 30; #endif } -void NodeDB::installDefaultModuleConfig() -{ - LOG_INFO("Install default ModuleConfig"); - memset(&moduleConfig, 0, sizeof(meshtastic_ModuleConfig)); +void NodeDB::installDefaultModuleConfig() { + LOG_INFO("Install default ModuleConfig"); + memset(&moduleConfig, 0, sizeof(meshtastic_ModuleConfig)); - moduleConfig.version = DEVICESTATE_CUR_VER; - moduleConfig.has_mqtt = true; - moduleConfig.has_range_test = true; - moduleConfig.has_serial = true; - moduleConfig.has_store_forward = true; - moduleConfig.has_telemetry = true; - moduleConfig.has_external_notification = true; + moduleConfig.version = DEVICESTATE_CUR_VER; + moduleConfig.has_mqtt = true; + moduleConfig.has_range_test = true; + moduleConfig.has_serial = true; + moduleConfig.has_store_forward = true; + moduleConfig.has_telemetry = true; + moduleConfig.has_external_notification = true; #if defined(PIN_BUZZER) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output_buzzer = PIN_BUZZER; - moduleConfig.external_notification.use_pwm = true; - moduleConfig.external_notification.alert_message_buzzer = true; - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output_buzzer = PIN_BUZZER; + moduleConfig.external_notification.use_pwm = true; + moduleConfig.external_notification.alert_message_buzzer = true; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #if defined(PIN_VIBRATION) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output_vibra = PIN_VIBRATION; - moduleConfig.external_notification.alert_message_vibra = true; - moduleConfig.external_notification.output_ms = 500; - moduleConfig.external_notification.nag_timeout = 2; + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output_vibra = PIN_VIBRATION; + moduleConfig.external_notification.alert_message_vibra = true; + moduleConfig.external_notification.output_ms = 500; + moduleConfig.external_notification.nag_timeout = 2; #endif -#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ - defined(ELECROW_ThinkNode_M6) - // Default to PIN_LED2 for external notification output (LED color depends on device variant) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = PIN_LED2; +#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || defined(ELECROW_ThinkNode_M6) + // Default to PIN_LED2 for external notification output (LED color depends on device variant) + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output = PIN_LED2; #if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) - moduleConfig.external_notification.active = false; + moduleConfig.external_notification.active = false; #else - moduleConfig.external_notification.active = true; + moduleConfig.external_notification.active = true; #endif - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; + moduleConfig.external_notification.alert_message = true; + moduleConfig.external_notification.output_ms = 1000; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #ifdef HAS_I2S - // Don't worry about the other settings for T-Watch, we'll also use the DRV2056 behavior for notifications - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.use_i2s_as_buzzer = true; - moduleConfig.external_notification.alert_message_buzzer = true; + // Don't worry about the other settings for T-Watch, we'll also use the DRV2056 behavior for notifications + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.use_i2s_as_buzzer = true; + moduleConfig.external_notification.alert_message_buzzer = true; #if HAS_TFT - if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) - moduleConfig.external_notification.nag_timeout = 0; + if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) + moduleConfig.external_notification.nag_timeout = 0; #else - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #endif #ifdef NANO_G2_ULTRA - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 100; - moduleConfig.external_notification.active = true; + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.alert_message = true; + moduleConfig.external_notification.output_ms = 100; + moduleConfig.external_notification.active = true; #endif #ifdef ELECROW_ThinkNode_M1 - // Default to Elecrow USER_LED (blue) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = USER_LED; - moduleConfig.external_notification.active = true; - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = 60; + // Default to Elecrow USER_LED (blue) + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output = USER_LED; + moduleConfig.external_notification.active = true; + moduleConfig.external_notification.alert_message = true; + moduleConfig.external_notification.output_ms = 1000; + moduleConfig.external_notification.nag_timeout = 60; #endif #ifdef T_LORA_PAGER - moduleConfig.canned_message.updown1_enabled = true; - moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; - moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; - moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; - moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); - moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); - moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; + moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; + moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; + moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); + moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); + moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif - moduleConfig.has_canned_message = true; + moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT - moduleConfig.mqtt.enabled = true; + moduleConfig.mqtt.enabled = true; #endif #ifdef USERPREFS_MQTT_ADDRESS - strncpy(moduleConfig.mqtt.address, USERPREFS_MQTT_ADDRESS, sizeof(moduleConfig.mqtt.address)); + strncpy(moduleConfig.mqtt.address, USERPREFS_MQTT_ADDRESS, sizeof(moduleConfig.mqtt.address)); #else - strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address)); + strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address)); #endif #ifdef USERPREFS_MQTT_USERNAME - strncpy(moduleConfig.mqtt.username, USERPREFS_MQTT_USERNAME, sizeof(moduleConfig.mqtt.username)); + strncpy(moduleConfig.mqtt.username, USERPREFS_MQTT_USERNAME, sizeof(moduleConfig.mqtt.username)); #else - strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username)); + strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username)); #endif #ifdef USERPREFS_MQTT_PASSWORD - strncpy(moduleConfig.mqtt.password, USERPREFS_MQTT_PASSWORD, sizeof(moduleConfig.mqtt.password)); + strncpy(moduleConfig.mqtt.password, USERPREFS_MQTT_PASSWORD, sizeof(moduleConfig.mqtt.password)); #else - strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password)); + strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password)); #endif #ifdef USERPREFS_MQTT_ROOT_TOPIC - strncpy(moduleConfig.mqtt.root, USERPREFS_MQTT_ROOT_TOPIC, sizeof(moduleConfig.mqtt.root)); + strncpy(moduleConfig.mqtt.root, USERPREFS_MQTT_ROOT_TOPIC, sizeof(moduleConfig.mqtt.root)); #else - strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root)); + strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root)); #endif #ifdef USERPREFS_MQTT_ENCRYPTION_ENABLED - moduleConfig.mqtt.encryption_enabled = USERPREFS_MQTT_ENCRYPTION_ENABLED; + moduleConfig.mqtt.encryption_enabled = USERPREFS_MQTT_ENCRYPTION_ENABLED; #else - moduleConfig.mqtt.encryption_enabled = default_mqtt_encryption_enabled; + moduleConfig.mqtt.encryption_enabled = default_mqtt_encryption_enabled; #endif #ifdef USERPREFS_MQTT_TLS_ENABLED - moduleConfig.mqtt.tls_enabled = USERPREFS_MQTT_TLS_ENABLED; + moduleConfig.mqtt.tls_enabled = USERPREFS_MQTT_TLS_ENABLED; #else - moduleConfig.mqtt.tls_enabled = default_mqtt_tls_enabled; + moduleConfig.mqtt.tls_enabled = default_mqtt_tls_enabled; #endif - moduleConfig.has_neighbor_info = true; - moduleConfig.neighbor_info.enabled = false; + moduleConfig.has_neighbor_info = true; + moduleConfig.neighbor_info.enabled = false; - moduleConfig.has_detection_sensor = true; - moduleConfig.detection_sensor.enabled = false; - moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; - moduleConfig.detection_sensor.minimum_broadcast_secs = 45; + moduleConfig.has_detection_sensor = true; + moduleConfig.detection_sensor.enabled = false; + moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; + moduleConfig.detection_sensor.minimum_broadcast_secs = 45; - moduleConfig.has_ambient_lighting = true; - moduleConfig.ambient_lighting.current = 10; - // Default to a color based on our node number - moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; - moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; - moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; + moduleConfig.has_ambient_lighting = true; + moduleConfig.ambient_lighting.current = 10; + // Default to a color based on our node number + moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; + moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; + moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; + initModuleConfigIntervals(); +} + +void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) { + if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + initConfigIntervals(); initModuleConfigIntervals(); -} - -void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) -{ - if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) { - initConfigIntervals(); - initModuleConfigIntervals(); - moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - moduleConfig.telemetry.device_update_interval = ONE_DAY; - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; - moduleConfig.telemetry.environment_measurement_enabled = true; - moduleConfig.telemetry.environment_update_interval = 300; - } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { - config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = 300; // Every 5 minutes - } else if (role == meshtastic_Config_DeviceConfig_Role_TAK) { - config.device.node_info_broadcast_secs = ONE_DAY; - config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = ONE_DAY; - // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) - config.position.position_flags = - (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | - meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); - moduleConfig.telemetry.device_update_interval = ONE_DAY; - } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; - } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - config.device.node_info_broadcast_secs = ONE_DAY; - config.position.position_broadcast_smart_enabled = true; - config.position.position_broadcast_secs = 3 * 60; // Every 3 minutes - config.position.broadcast_smart_minimum_distance = 20; - config.position.broadcast_smart_minimum_interval_secs = 15; - // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) - config.position.position_flags = - (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | - meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); - moduleConfig.telemetry.device_update_interval = ONE_DAY; - } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - config.device.node_info_broadcast_secs = MAX_INTERVAL; - config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = MAX_INTERVAL; - moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; - moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; - moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; - moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; - moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; - } -} - -void NodeDB::initModuleConfigIntervals() -{ - // Zero out telemetry intervals so that they coalesce to defaults in Default.h -#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL - moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; -#else + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + moduleConfig.telemetry.device_update_interval = ONE_DAY; + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; + moduleConfig.telemetry.environment_measurement_enabled = true; + moduleConfig.telemetry.environment_update_interval = 300; + } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = 300; // Every 5 minutes + } else if (role == meshtastic_Config_DeviceConfig_Role_TAK) { + config.device.node_info_broadcast_secs = ONE_DAY; + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = ONE_DAY; + // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) + config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | + meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); + moduleConfig.telemetry.device_update_interval = ONE_DAY; + } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; + } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + config.device.node_info_broadcast_secs = ONE_DAY; + config.position.position_broadcast_smart_enabled = true; + config.position.position_broadcast_secs = 3 * 60; // Every 3 minutes + config.position.broadcast_smart_minimum_distance = 20; + config.position.broadcast_smart_minimum_interval_secs = 15; + // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) + config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | + meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); + moduleConfig.telemetry.device_update_interval = ONE_DAY; + } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + config.device.node_info_broadcast_secs = MAX_INTERVAL; + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = MAX_INTERVAL; + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; -#endif - moduleConfig.telemetry.environment_update_interval = 0; - moduleConfig.telemetry.air_quality_interval = 0; - moduleConfig.telemetry.power_update_interval = 0; - moduleConfig.telemetry.health_update_interval = 0; - moduleConfig.neighbor_info.update_interval = 0; - moduleConfig.paxcounter.paxcounter_update_interval = 0; + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; + } } -void NodeDB::installDefaultChannels() -{ - LOG_INFO("Install default ChannelFile"); - memset(&channelFile, 0, sizeof(meshtastic_ChannelFile)); - channelFile.version = DEVICESTATE_CUR_VER; -} - -void NodeDB::resetNodes(bool keepFavorites) -{ - if (!config.position.fixed_position) - clearLocalPosition(); - numMeshNodes = 1; - if (keepFavorites) { - LOG_INFO("Clearing node database - preserving favorites"); - for (size_t i = 0; i < meshNodes->size(); i++) { - meshtastic_NodeInfoLite &node = meshNodes->at(i); - if (i > 0 && !node.is_favorite) { - node = meshtastic_NodeInfoLite(); - } else { - numMeshNodes += 1; - } - }; - } else { - LOG_INFO("Clearing node database - removing favorites"); - std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); - } - devicestate.has_rx_text_message = false; - devicestate.has_rx_waypoint = false; - saveNodeDatabaseToDisk(); - saveDeviceStateToDisk(); - if (neighborInfoModule && moduleConfig.neighbor_info.enabled) - neighborInfoModule->resetNeighbors(); -} - -void NodeDB::removeNodeByNum(NodeNum nodeNum) -{ - int newPos = 0, removed = 0; - for (int i = 0; i < numMeshNodes; i++) { - if (meshNodes->at(i).num != nodeNum) - meshNodes->at(newPos++) = meshNodes->at(i); - else - removed++; - } - numMeshNodes -= removed; - std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, - meshtastic_NodeInfoLite()); - LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); - saveNodeDatabaseToDisk(); -} - -void NodeDB::clearLocalPosition() -{ - meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); - node->position.latitude_i = 0; - node->position.longitude_i = 0; - node->position.altitude = 0; - node->position.time = 0; - setLocalPosition(meshtastic_Position_init_default); - localPositionUpdatedSinceBoot = false; -} - -void NodeDB::cleanupMeshDB() -{ - int newPos = 0, removed = 0; - for (int i = 0; i < numMeshNodes; i++) { - if (meshNodes->at(i).has_user) { - if (meshNodes->at(i).user.public_key.size > 0) { - if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { - meshNodes->at(i).user.public_key.size = 0; - } - } - if (newPos != i) - meshNodes->at(newPos++) = meshNodes->at(i); - else - newPos++; - } else { - removed++; - } - } - numMeshNodes -= removed; - std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, - meshtastic_NodeInfoLite()); - LOG_DEBUG("cleanupMeshDB purged %d entries", removed); -} - -void NodeDB::installDefaultDeviceState() -{ - LOG_INFO("Install default DeviceState"); - // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); - - // init our devicestate with valid flags so protobuf writing/reading will work - devicestate.has_my_node = true; - devicestate.has_owner = true; - devicestate.version = DEVICESTATE_CUR_VER; - devicestate.receive_queue_count = 0; // Not yet implemented FIXME - devicestate.has_rx_waypoint = false; - devicestate.has_rx_text_message = false; - - generatePacketId(); // FIXME - ugly way to init current_packet_id; - - // Set default owner name - pickNewNodeNum(); // based on macaddr now -#ifdef USERPREFS_CONFIG_OWNER_LONG_NAME - snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME); +void NodeDB::initModuleConfigIntervals() { + // Zero out telemetry intervals so that they coalesce to defaults in Default.h +#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL + moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; #else - snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff); + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; +#endif + moduleConfig.telemetry.environment_update_interval = 0; + moduleConfig.telemetry.air_quality_interval = 0; + moduleConfig.telemetry.power_update_interval = 0; + moduleConfig.telemetry.health_update_interval = 0; + moduleConfig.neighbor_info.update_interval = 0; + moduleConfig.paxcounter.paxcounter_update_interval = 0; +} + +void NodeDB::installDefaultChannels() { + LOG_INFO("Install default ChannelFile"); + memset(&channelFile, 0, sizeof(meshtastic_ChannelFile)); + channelFile.version = DEVICESTATE_CUR_VER; +} + +void NodeDB::resetNodes(bool keepFavorites) { + if (!config.position.fixed_position) + clearLocalPosition(); + numMeshNodes = 1; + if (keepFavorites) { + LOG_INFO("Clearing node database - preserving favorites"); + for (size_t i = 0; i < meshNodes->size(); i++) { + meshtastic_NodeInfoLite &node = meshNodes->at(i); + if (i > 0 && !node.is_favorite) { + node = meshtastic_NodeInfoLite(); + } else { + numMeshNodes += 1; + } + }; + } else { + LOG_INFO("Clearing node database - removing favorites"); + std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); + } + devicestate.has_rx_text_message = false; + devicestate.has_rx_waypoint = false; + saveNodeDatabaseToDisk(); + saveDeviceStateToDisk(); + if (neighborInfoModule && moduleConfig.neighbor_info.enabled) + neighborInfoModule->resetNeighbors(); +} + +void NodeDB::removeNodeByNum(NodeNum nodeNum) { + int newPos = 0, removed = 0; + for (int i = 0; i < numMeshNodes; i++) { + if (meshNodes->at(i).num != nodeNum) + meshNodes->at(newPos++) = meshNodes->at(i); + else + removed++; + } + numMeshNodes -= removed; + std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, meshtastic_NodeInfoLite()); + LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); + saveNodeDatabaseToDisk(); +} + +void NodeDB::clearLocalPosition() { + meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); + node->position.latitude_i = 0; + node->position.longitude_i = 0; + node->position.altitude = 0; + node->position.time = 0; + setLocalPosition(meshtastic_Position_init_default); + localPositionUpdatedSinceBoot = false; +} + +void NodeDB::cleanupMeshDB() { + int newPos = 0, removed = 0; + for (int i = 0; i < numMeshNodes; i++) { + if (meshNodes->at(i).has_user) { + if (meshNodes->at(i).user.public_key.size > 0) { + if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { + meshNodes->at(i).user.public_key.size = 0; + } + } + if (newPos != i) + meshNodes->at(newPos++) = meshNodes->at(i); + else + newPos++; + } else { + removed++; + } + } + numMeshNodes -= removed; + std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, meshtastic_NodeInfoLite()); + LOG_DEBUG("cleanupMeshDB purged %d entries", removed); +} + +void NodeDB::installDefaultDeviceState() { + LOG_INFO("Install default DeviceState"); + // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); + + // init our devicestate with valid flags so protobuf writing/reading will work + devicestate.has_my_node = true; + devicestate.has_owner = true; + devicestate.version = DEVICESTATE_CUR_VER; + devicestate.receive_queue_count = 0; // Not yet implemented FIXME + devicestate.has_rx_waypoint = false; + devicestate.has_rx_text_message = false; + + generatePacketId(); // FIXME - ugly way to init current_packet_id; + + // Set default owner name + pickNewNodeNum(); // based on macaddr now +#ifdef USERPREFS_CONFIG_OWNER_LONG_NAME + snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME); +#else + snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff); #endif #ifdef USERPREFS_CONFIG_OWNER_SHORT_NAME - snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME); + snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME); #else - snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff); + snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff); #endif - snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum - memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); - owner.has_is_unmessagable = true; - owner.is_unmessagable = false; + snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum + memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); + owner.has_is_unmessagable = true; + owner.is_unmessagable = false; } // We reserve a few nodenums for future use @@ -1110,477 +1072,456 @@ void NodeDB::installDefaultDeviceState() /** * get our starting (provisional) nodenum from flash. */ -void NodeDB::pickNewNodeNum() -{ - NodeNum nodeNum = myNodeInfo.my_node_num; - getMacAddr(ourMacAddr); // Make sure ourMacAddr is set - if (nodeNum == 0) { - // Pick an initial nodenum based on the macaddr - nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; - } +void NodeDB::pickNewNodeNum() { + NodeNum nodeNum = myNodeInfo.my_node_num; + getMacAddr(ourMacAddr); // Make sure ourMacAddr is set + if (nodeNum == 0) { + // Pick an initial nodenum based on the macaddr + nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; + } - meshtastic_NodeInfoLite *found; - while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) || - (nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) { - NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice - if (found) - LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so " - "trying for 0x%x", - nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate); - nodeNum = candidate; - } - LOG_DEBUG("Use nodenum 0x%x ", nodeNum); + meshtastic_NodeInfoLite *found; + while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) || + (nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) { + NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice + if (found) + LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so " + "trying for 0x%x", + nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate); + nodeNum = candidate; + } + LOG_DEBUG("Use nodenum 0x%x ", nodeNum); - myNodeInfo.my_node_num = nodeNum; + myNodeInfo.my_node_num = nodeNum; } /** Load a protobuf from a file, return LoadFileResult */ -LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, - void *dest_struct) -{ - LoadFileResult state = LoadFileResult::OTHER_FAILURE; +LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct) { + LoadFileResult state = LoadFileResult::OTHER_FAILURE; #ifdef FSCom - concurrency::LockGuard g(spiLock); + concurrency::LockGuard g(spiLock); - auto f = FSCom.open(filename, FILE_O_READ); + auto f = FSCom.open(filename, FILE_O_READ); - if (f) { - LOG_INFO("Load %s", filename); - pb_istream_t stream = {&readcb, &f, protoSize}; - if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object - memset(dest_struct, 0, objSize); - if (!pb_decode(&stream, fields, dest_struct)) { - LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); - state = LoadFileResult::DECODE_FAILED; - } else { - LOG_INFO("Loaded %s successfully", filename); - state = LoadFileResult::LOAD_SUCCESS; - } - f.close(); + if (f) { + LOG_INFO("Load %s", filename); + pb_istream_t stream = {&readcb, &f, protoSize}; + if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object + memset(dest_struct, 0, objSize); + if (!pb_decode(&stream, fields, dest_struct)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); + state = LoadFileResult::DECODE_FAILED; } else { - LOG_ERROR("Could not open / read %s", filename); + LOG_INFO("Loaded %s successfully", filename); + state = LoadFileResult::LOAD_SUCCESS; } + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename); + } #else - LOG_ERROR("ERROR: Filesystem not implemented"); - state = LoadFileResult::NO_FILESYSTEM; + LOG_ERROR("ERROR: Filesystem not implemented"); + state = LoadFileResult::NO_FILESYSTEM; #endif - return state; + return state; } -void NodeDB::loadFromDisk() -{ - // Mark the current device state as completely unusable, so that if we fail reading the entire file from - // disk we will still factoryReset to restore things. - devicestate.version = 0; +void NodeDB::loadFromDisk() { + // Mark the current device state as completely unusable, so that if we fail reading the entire file from + // disk we will still factoryReset to restore things. + devicestate.version = 0; - meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero; + meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero; #ifdef ARCH_ESP32 - spiLock->lock(); - // If the legacy deviceState exists, start over with a factory reset - if (FSCom.exists("/static/static")) - rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release - spiLock->unlock(); + spiLock->lock(); + // If the legacy deviceState exists, start over with a factory reset + if (FSCom.exists("/static/static")) + rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release + spiLock->unlock(); #endif #ifdef FSCom #ifdef FACTORY_INSTALL - spiLock->lock(); - if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) { - LOG_WARN("Factory Install Reset!"); - FSCom.format(); - FSCom.mkdir("/prefs"); - File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE); - if (f2) { - f2.flush(); - f2.close(); - } + spiLock->lock(); + if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) { + LOG_WARN("Factory Install Reset!"); + FSCom.format(); + FSCom.mkdir("/prefs"); + File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE); + if (f2) { + f2.flush(); + f2.close(); } + } + spiLock->unlock(); +#endif + spiLock->lock(); + if (FSCom.exists(legacyPrefFileName)) { spiLock->unlock(); -#endif + LOG_WARN("Legacy prefs version found, factory resetting"); + if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config) == + LoadFileResult::LOAD_SUCCESS && + config.has_security && config.security.private_key.size > 0) { + LOG_DEBUG("Saving backup of security config and keys"); + backupSecurity = config.security; + } spiLock->lock(); - if (FSCom.exists(legacyPrefFileName)) { - spiLock->unlock(); - LOG_WARN("Legacy prefs version found, factory resetting"); - if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, - &config) == LoadFileResult::LOAD_SUCCESS && - config.has_security && config.security.private_key.size > 0) { - LOG_DEBUG("Saving backup of security config and keys"); - backupSecurity = config.security; - } - spiLock->lock(); - rmDir("/prefs"); - spiLock->unlock(); - } else { - spiLock->unlock(); - } + rmDir("/prefs"); + spiLock->unlock(); + } else { + spiLock->unlock(); + } #endif - auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase), - &meshtastic_NodeDatabase_msg, &nodeDatabase); - if (nodeDatabase.version < DEVICESTATE_MIN_VER) { - LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); - installDefaultNodeDatabase(); + auto state = + loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase), &meshtastic_NodeDatabase_msg, &nodeDatabase); + if (nodeDatabase.version < DEVICESTATE_MIN_VER) { + LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); + installDefaultNodeDatabase(); + } else { + meshNodes = &nodeDatabase.nodes; + numMeshNodes = nodeDatabase.nodes.size(); + LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); + } + + if (numMeshNodes > MAX_NUM_NODES) { + LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); + numMeshNodes = MAX_NUM_NODES; + } + meshNodes->resize(MAX_NUM_NODES); + + // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM + state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), &meshtastic_DeviceState_msg, &devicestate); + + // See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786 + // It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most + // of our critical config may still be valid (in the other files - loaded next). Also, if we did fail on reading we + // probably failed on the enormous (and non critical) nodeDB. So DO NOT install default device state. if (state != + // LoadFileResult::LOAD_SUCCESS) { + // installDefaultDeviceState(); // Our in RAM copy might now be corrupt + //} else { + if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { + LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); + installDefaultDeviceState(); + } else { + LOG_INFO("Loaded saved devicestate version %d", devicestate.version); + } + + state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config); + if (state != LoadFileResult::LOAD_SUCCESS) { + installDefaultConfig(); // Our in RAM copy might now be corrupt + } else { + if (config.version < DEVICESTATE_MIN_VER) { + LOG_WARN("config %d is old, discard", config.version); + installDefaultConfig(true); } else { - meshNodes = &nodeDatabase.nodes; - numMeshNodes = nodeDatabase.nodes.size(); - LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); + LOG_INFO("Loaded saved config version %d", config.version); } + } + if (backupSecurity.private_key.size > 0) { + LOG_DEBUG("Restoring backup of security config"); + config.security = backupSecurity; + saveToDisk(SEGMENT_CONFIG); + } - if (numMeshNodes > MAX_NUM_NODES) { - LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); - numMeshNodes = MAX_NUM_NODES; - } - meshNodes->resize(MAX_NUM_NODES); - - // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM - state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), - &meshtastic_DeviceState_msg, &devicestate); - - // See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786 - // It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most of our - // critical config may still be valid (in the other files - loaded next). - // Also, if we did fail on reading we probably failed on the enormous (and non critical) nodeDB. So DO NOT install default - // device state. - // if (state != LoadFileResult::LOAD_SUCCESS) { - // installDefaultDeviceState(); // Our in RAM copy might now be corrupt - //} else { - if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { - LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); - installDefaultDeviceState(); - } else { - LOG_INFO("Loaded saved devicestate version %d", devicestate.version); - } - - state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, - &config); - if (state != LoadFileResult::LOAD_SUCCESS) { - installDefaultConfig(); // Our in RAM copy might now be corrupt - } else { - if (config.version < DEVICESTATE_MIN_VER) { - LOG_WARN("config %d is old, discard", config.version); - installDefaultConfig(true); - } else { - LOG_INFO("Loaded saved config version %d", config.version); - } - } - if (backupSecurity.private_key.size > 0) { - LOG_DEBUG("Restoring backup of security config"); - config.security = backupSecurity; - saveToDisk(SEGMENT_CONFIG); - } - - // Make sure we load hard coded admin keys even when the configuration file has none. - // Initialize admin_key_count to zero - byte numAdminKeys = 0; + // Make sure we load hard coded admin keys even when the configuration file has none. + // Initialize admin_key_count to zero + byte numAdminKeys = 0; #if defined(USERPREFS_USE_ADMIN_KEY_0) || defined(USERPREFS_USE_ADMIN_KEY_1) || defined(USERPREFS_USE_ADMIN_KEY_2) - uint16_t sum = 0; + uint16_t sum = 0; #endif #ifdef USERPREFS_USE_ADMIN_KEY_0 - for (uint8_t b = 0; b < 32; b++) { - sum += config.security.admin_key[0].bytes[b]; - } - if (sum == 0) { - numAdminKeys += 1; - LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences."); - memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); - config.security.admin_key[0].size = 32; - } + for (uint8_t b = 0; b < 32; b++) { + sum += config.security.admin_key[0].bytes[b]; + } + if (sum == 0) { + numAdminKeys += 1; + LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences."); + memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); + config.security.admin_key[0].size = 32; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 - sum = 0; - for (uint8_t b = 0; b < 32; b++) { - sum += config.security.admin_key[1].bytes[b]; - } - if (sum == 0) { - numAdminKeys += 1; - LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences."); - memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); - config.security.admin_key[1].size = 32; - } + sum = 0; + for (uint8_t b = 0; b < 32; b++) { + sum += config.security.admin_key[1].bytes[b]; + } + if (sum == 0) { + numAdminKeys += 1; + LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences."); + memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); + config.security.admin_key[1].size = 32; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 - sum = 0; - for (uint8_t b = 0; b < 32; b++) { - sum += config.security.admin_key[2].bytes[b]; - } - if (sum == 0) { - numAdminKeys += 1; - LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences."); - memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); - config.security.admin_key[2].size = 32; - } + sum = 0; + for (uint8_t b = 0; b < 32; b++) { + sum += config.security.admin_key[2].bytes[b]; + } + if (sum == 0) { + numAdminKeys += 1; + LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences."); + memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); + config.security.admin_key[2].size = 32; + } #endif - if (numAdminKeys > 0) { - LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys); - config.security.admin_key_count = numAdminKeys; - saveToDisk(SEGMENT_CONFIG); - } + if (numAdminKeys > 0) { + LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys); + config.security.admin_key_count = numAdminKeys; + saveToDisk(SEGMENT_CONFIG); + } - state = loadProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, sizeof(meshtastic_LocalModuleConfig), - &meshtastic_LocalModuleConfig_msg, &moduleConfig); - if (state != LoadFileResult::LOAD_SUCCESS) { - installDefaultModuleConfig(); // Our in RAM copy might now be corrupt + state = loadProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, sizeof(meshtastic_LocalModuleConfig), &meshtastic_LocalModuleConfig_msg, + &moduleConfig); + if (state != LoadFileResult::LOAD_SUCCESS) { + installDefaultModuleConfig(); // Our in RAM copy might now be corrupt + } else { + if (moduleConfig.version < DEVICESTATE_MIN_VER) { + LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version); + installDefaultModuleConfig(); } else { - if (moduleConfig.version < DEVICESTATE_MIN_VER) { - LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version); - installDefaultModuleConfig(); - } else { - LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version); - } + LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version); } + } - state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg, - &channelFile); - if (state != LoadFileResult::LOAD_SUCCESS) { - installDefaultChannels(); // Our in RAM copy might now be corrupt + state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg, &channelFile); + if (state != LoadFileResult::LOAD_SUCCESS) { + installDefaultChannels(); // Our in RAM copy might now be corrupt + } else { + if (channelFile.version < DEVICESTATE_MIN_VER) { + LOG_WARN("channelFile %d is old, discard", channelFile.version); + installDefaultChannels(); } else { - if (channelFile.version < DEVICESTATE_MIN_VER) { - LOG_WARN("channelFile %d is old, discard", channelFile.version); - installDefaultChannels(); - } else { - LOG_INFO("Loaded saved channelFile version %d", channelFile.version); - } + LOG_INFO("Loaded saved channelFile version %d", channelFile.version); } + } - state = loadProto(uiconfigFileName, meshtastic_DeviceUIConfig_size, sizeof(meshtastic_DeviceUIConfig), - &meshtastic_DeviceUIConfig_msg, &uiconfig); - if (state == LoadFileResult::LOAD_SUCCESS) { - LOG_INFO("Loaded UIConfig"); - } + state = loadProto(uiconfigFileName, meshtastic_DeviceUIConfig_size, sizeof(meshtastic_DeviceUIConfig), &meshtastic_DeviceUIConfig_msg, &uiconfig); + if (state == LoadFileResult::LOAD_SUCCESS) { + LOG_INFO("Loaded UIConfig"); + } - // 2.4.X - configuration migration to update new default intervals - if (moduleConfig.version < 23) { - LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version); - moduleConfig.version = DEVICESTATE_CUR_VER; - if (moduleConfig.telemetry.device_update_interval == 900) - moduleConfig.telemetry.device_update_interval = 0; - if (moduleConfig.telemetry.environment_update_interval == 900) - moduleConfig.telemetry.environment_update_interval = 0; - if (moduleConfig.telemetry.air_quality_interval == 900) - moduleConfig.telemetry.air_quality_interval = 0; - if (moduleConfig.telemetry.power_update_interval == 900) - moduleConfig.telemetry.power_update_interval = 0; - if (moduleConfig.neighbor_info.update_interval == 900) - moduleConfig.neighbor_info.update_interval = 0; - if (moduleConfig.paxcounter.paxcounter_update_interval == 900) - moduleConfig.paxcounter.paxcounter_update_interval = 0; + // 2.4.X - configuration migration to update new default intervals + if (moduleConfig.version < 23) { + LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version); + moduleConfig.version = DEVICESTATE_CUR_VER; + if (moduleConfig.telemetry.device_update_interval == 900) + moduleConfig.telemetry.device_update_interval = 0; + if (moduleConfig.telemetry.environment_update_interval == 900) + moduleConfig.telemetry.environment_update_interval = 0; + if (moduleConfig.telemetry.air_quality_interval == 900) + moduleConfig.telemetry.air_quality_interval = 0; + if (moduleConfig.telemetry.power_update_interval == 900) + moduleConfig.telemetry.power_update_interval = 0; + if (moduleConfig.neighbor_info.update_interval == 900) + moduleConfig.neighbor_info.update_interval = 0; + if (moduleConfig.paxcounter.paxcounter_update_interval == 900) + moduleConfig.paxcounter.paxcounter_update_interval = 0; - saveToDisk(SEGMENT_MODULECONFIG); - } + saveToDisk(SEGMENT_MODULECONFIG); + } #if ARCH_PORTDUINO - // set any config overrides - if (portduino_config.has_configDisplayMode) { - config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; - } + // set any config overrides + if (portduino_config.has_configDisplayMode) { + config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; + } #endif } /** Save a protobuf from a file, return true for success */ -bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, - bool fullAtomic) -{ - bool okay = false; +bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic) { + bool okay = false; #ifdef FSCom - auto f = SafeFile(filename, fullAtomic); + auto f = SafeFile(filename, fullAtomic); - LOG_INFO("Save %s", filename); - pb_ostream_t stream = {&writecb, static_cast(&f), protoSize}; + LOG_INFO("Save %s", filename); + pb_ostream_t stream = {&writecb, static_cast(&f), protoSize}; - if (!pb_encode(&stream, fields, dest_struct)) { - LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); - } else { - okay = true; - } + if (!pb_encode(&stream, fields, dest_struct)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + } else { + okay = true; + } - bool writeSucceeded = f.close(); + bool writeSucceeded = f.close(); - if (!okay || !writeSucceeded) { - LOG_ERROR("Can't write prefs!"); - } + if (!okay || !writeSucceeded) { + LOG_ERROR("Can't write prefs!"); + } #else - LOG_ERROR("ERROR: Filesystem not implemented"); + LOG_ERROR("ERROR: Filesystem not implemented"); #endif - return okay; + return okay; } -bool NodeDB::saveChannelsToDisk() -{ +bool NodeDB::saveChannelsToDisk() { #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile); + return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile); } -bool NodeDB::saveDeviceStateToDisk() -{ +bool NodeDB::saveDeviceStateToDisk() { #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - // Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB - // Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this - return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true); + // Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB + // Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of + // this + return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true); } -bool NodeDB::saveNodeDatabaseToDisk() -{ +bool NodeDB::saveNodeDatabaseToDisk() { #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - size_t nodeDatabaseSize; - pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); - return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); + size_t nodeDatabaseSize; + pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); + return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); } -bool NodeDB::saveToDiskNoRetry(int saveWhat) -{ - bool success = true; +bool NodeDB::saveToDiskNoRetry(int saveWhat) { + bool success = true; #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - if (saveWhat & SEGMENT_CONFIG) { - config.has_device = true; - config.has_display = true; - config.has_lora = true; - config.has_position = true; - config.has_power = true; - config.has_network = true; - config.has_bluetooth = true; - config.has_security = true; + if (saveWhat & SEGMENT_CONFIG) { + config.has_device = true; + config.has_display = true; + config.has_lora = true; + config.has_position = true; + config.has_power = true; + config.has_network = true; + config.has_bluetooth = true; + config.has_security = true; - success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config); - } + success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config); + } - if (saveWhat & SEGMENT_MODULECONFIG) { - moduleConfig.has_canned_message = true; - moduleConfig.has_external_notification = true; - moduleConfig.has_mqtt = true; - moduleConfig.has_range_test = true; - moduleConfig.has_serial = true; - moduleConfig.has_store_forward = true; - moduleConfig.has_telemetry = true; - moduleConfig.has_neighbor_info = true; - moduleConfig.has_detection_sensor = true; - moduleConfig.has_ambient_lighting = true; - moduleConfig.has_audio = true; - moduleConfig.has_paxcounter = true; + if (saveWhat & SEGMENT_MODULECONFIG) { + moduleConfig.has_canned_message = true; + moduleConfig.has_external_notification = true; + moduleConfig.has_mqtt = true; + moduleConfig.has_range_test = true; + moduleConfig.has_serial = true; + moduleConfig.has_store_forward = true; + moduleConfig.has_telemetry = true; + moduleConfig.has_neighbor_info = true; + moduleConfig.has_detection_sensor = true; + moduleConfig.has_ambient_lighting = true; + moduleConfig.has_audio = true; + moduleConfig.has_paxcounter = true; - success &= - saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); - } + success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); + } - if (saveWhat & SEGMENT_CHANNELS) { - success &= saveChannelsToDisk(); - } + if (saveWhat & SEGMENT_CHANNELS) { + success &= saveChannelsToDisk(); + } - if (saveWhat & SEGMENT_DEVICESTATE) { - success &= saveDeviceStateToDisk(); - } + if (saveWhat & SEGMENT_DEVICESTATE) { + success &= saveDeviceStateToDisk(); + } - if (saveWhat & SEGMENT_NODEDATABASE) { - success &= saveNodeDatabaseToDisk(); - } + if (saveWhat & SEGMENT_NODEDATABASE) { + success &= saveNodeDatabaseToDisk(); + } - return success; + return success; } -bool NodeDB::saveToDisk(int saveWhat) -{ - LOG_DEBUG("Save to disk %d", saveWhat); - bool success = saveToDiskNoRetry(saveWhat); +bool NodeDB::saveToDisk(int saveWhat) { + LOG_DEBUG("Save to disk %d", saveWhat); + bool success = saveToDiskNoRetry(saveWhat); - if (!success) { - LOG_ERROR("Failed to save to disk, retrying"); + if (!success) { + LOG_ERROR("Failed to save to disk, retrying"); #ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion - spiLock->lock(); - FSCom.format(); - spiLock->unlock(); + spiLock->lock(); + FSCom.format(); + spiLock->unlock(); #endif - success = saveToDiskNoRetry(saveWhat); + success = saveToDiskNoRetry(saveWhat); - RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE - : meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); - } + RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE + : meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + } - return success; + return success; } -const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex) -{ - if (readIndex < numMeshNodes) - return &meshNodes->at(readIndex++); - else - return NULL; +const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex) { + if (readIndex < numMeshNodes) + return &meshNodes->at(readIndex++); + else + return NULL; } /// Given a node, return how many seconds in the past (vs now) that we last heard from it -uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n) -{ - uint32_t now = getTime(); +uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n) { + uint32_t now = getTime(); - int delta = (int)(now - n->last_heard); - if (delta < 0) // our clock must be slightly off still - not set from GPS yet - delta = 0; + int delta = (int)(now - n->last_heard); + if (delta < 0) // our clock must be slightly off still - not set from GPS yet + delta = 0; - return delta; + return delta; } -uint32_t sinceReceived(const meshtastic_MeshPacket *p) -{ - uint32_t now = getTime(); +uint32_t sinceReceived(const meshtastic_MeshPacket *p) { + uint32_t now = getTime(); - int delta = (int)(now - p->rx_time); - if (delta < 0) // our clock must be slightly off still - not set from GPS yet - delta = 0; + int delta = (int)(now - p->rx_time); + if (delta < 0) // our clock must be slightly off still - not set from GPS yet + delta = 0; - return delta; + return delta; } -int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) -{ - // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a - // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware - // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as - // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns - // defaultIfUnknown when hop_start is 0. - if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) - return defaultIfUnknown; // Cannot reliably determine the number of hops. +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) { + // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a + // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware + // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as + // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns + // defaultIfUnknown when hop_start is 0. + if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) + return defaultIfUnknown; // Cannot reliably determine the number of hops. - // Guard against invalid values. - if (p.hop_start < p.hop_limit) - return defaultIfUnknown; + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return defaultIfUnknown; - return p.hop_start - p.hop_limit; + return p.hop_start - p.hop_limit; } #define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline -size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) -{ - size_t numseen = 0; +size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) { + size_t numseen = 0; - // FIXME this implementation is kinda expensive - for (int i = 0; i < numMeshNodes; i++) { - if (localOnly && meshNodes->at(i).via_mqtt) - continue; - if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) - numseen++; - } + // FIXME this implementation is kinda expensive + for (int i = 0; i < numMeshNodes; i++) { + if (localOnly && meshNodes->at(i).via_mqtt) + continue; + if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) + numseen++; + } - return numseen; + return numseen; } #include "MeshModule.h" @@ -1588,575 +1529,546 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) /** Update position info for this node based on received position data */ -void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src) -{ - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - if (!info) { - return; - } +void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src) { + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); + if (!info) { + return; + } - if (src == RX_SRC_LOCAL) { - // Local packet, fully authoritative - LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i, - p.altitude); + if (src == RX_SRC_LOCAL) { + // Local packet, fully authoritative + LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i, p.altitude); - setLocalPosition(p); - info->position = TypeConversions::ConvertToPositionLite(p); - } else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) { - // FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO - // (stop-gap fix for issue #900) - LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time); - info->position.time = p.time; - } else { - // Be careful to only update fields that have been set by the REMOTE sender - // A lot of position reports don't have time populated. In that case, be careful to not blow away the time we - // recorded based on the packet rxTime - // - // FIXME perhaps handle RX_SRC_USER separately? - LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i); + setLocalPosition(p); + info->position = TypeConversions::ConvertToPositionLite(p); + } else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) { + // FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO + // (stop-gap fix for issue #900) + LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time); + info->position.time = p.time; + } else { + // Be careful to only update fields that have been set by the REMOTE sender + // A lot of position reports don't have time populated. In that case, be careful to not blow away the time we + // recorded based on the packet rxTime + // + // FIXME perhaps handle RX_SRC_USER separately? + LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i); - // First, back up fields that we want to protect from overwrite - uint32_t tmp_time = info->position.time; + // First, back up fields that we want to protect from overwrite + uint32_t tmp_time = info->position.time; - // Next, update atomically - info->position = TypeConversions::ConvertToPositionLite(p); + // Next, update atomically + info->position = TypeConversions::ConvertToPositionLite(p); - // Last, restore any fields that may have been overwritten - if (!info->position.time) - info->position.time = tmp_time; - } - info->has_position = true; - updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed + // Last, restore any fields that may have been overwritten + if (!info->position.time) + info->position.time = tmp_time; + } + info->has_position = true; + updateGUIforNode = info; + notifyObservers(true); // Force an update whether or not our node counts have changed } /** Update telemetry info for this node based on received metrics * We only care about device telemetry here */ -void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src) -{ - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - // Environment metrics should never go to NodeDb but we'll safegaurd anyway - if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) { - return; - } +void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src) { + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); + // Environment metrics should never go to NodeDb but we'll safegaurd anyway + if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) { + return; + } - if (src == RX_SRC_LOCAL) { - // Local packet, fully authoritative - LOG_DEBUG("updateTelemetry LOCAL"); - } else { - LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId); - } - info->device_metrics = t.variant.device_metrics; - info->has_device_metrics = true; - updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed + if (src == RX_SRC_LOCAL) { + // Local packet, fully authoritative + LOG_DEBUG("updateTelemetry LOCAL"); + } else { + LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId); + } + info->device_metrics = t.variant.device_metrics; + info->has_device_metrics = true; + updateGUIforNode = info; + notifyObservers(true); // Force an update whether or not our node counts have changed } /** * Update the node database with a new contact */ -void NodeDB::addFromContact(meshtastic_SharedContact contact) -{ - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num); - if (!info || !contact.has_user) { - return; +void NodeDB::addFromContact(meshtastic_SharedContact contact) { + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num); + if (!info || !contact.has_user) { + return; + } + // If the local node has this node marked as manually verified + // and the client does not, do not allow the client to update the + // saved public key. + if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { + if (contact.user.public_key.size != info->user.public_key.size || + memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { + return; } - // If the local node has this node marked as manually verified - // and the client does not, do not allow the client to update the - // saved public key. - if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { - if (contact.user.public_key.size != info->user.public_key.size || - memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { - return; - } - } - info->num = contact.node_num; - info->has_user = true; - info->user = TypeConversions::ConvertToUserLite(contact.user); - if (contact.should_ignore) { - // If should_ignore is set, - // we need to clear the public key and other cruft, in addition to setting the node as ignored - info->is_ignored = true; - info->is_favorite = false; - info->has_device_metrics = false; - info->has_position = false; - info->user.public_key.size = 0; - info->user.public_key.bytes[0] = 0; + } + info->num = contact.node_num; + info->has_user = true; + info->user = TypeConversions::ConvertToUserLite(contact.user); + if (contact.should_ignore) { + // If should_ignore is set, + // we need to clear the public key and other cruft, in addition to setting the node as ignored + info->is_ignored = true; + info->is_favorite = false; + info->has_device_metrics = false; + info->has_position = false; + info->user.public_key.size = 0; + info->user.public_key.bytes[0] = 0; + } else { + /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database + * with public keys than the radio holds). However, we don't want to update last_heard just because we sent someone + * a DM! + */ + + /* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed + * nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we + * set the new node as a favorite, and we leave last_heard alone (even if it's zero). + */ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it + // without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to + // add contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll + // set last_heard to now, so that the add_contact node doesn't immediately get evicted. + info->last_heard = getTime(); } else { - /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with - * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! - */ - - /* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed - * nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we set the - * new node as a favorite, and we leave last_heard alone (even if it's zero). - */ - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it - // without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add - // contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set - // last_heard to now, so that the add_contact node doesn't immediately get evicted. - info->last_heard = getTime(); - } else { - // Normal case: set is_favorite to prevent expiration. - // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). - info->is_favorite = true; - } - - // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified - if (contact.manually_verified) { - info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - } - // Mark the node's key as manually verified to indicate trustworthiness. - updateGUIforNode = info; - sortMeshDB(); - notifyObservers(true); // Force an update whether or not our node counts have changed + // Normal case: set is_favorite to prevent expiration. + // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). + info->is_favorite = true; } - saveNodeDatabaseToDisk(); + + // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually + // verified + if (contact.manually_verified) { + info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + // Mark the node's key as manually verified to indicate trustworthiness. + updateGUIforNode = info; + sortMeshDB(); + notifyObservers(true); // Force an update whether or not our node counts have changed + } + saveNodeDatabaseToDisk(); } /** Update user info and channel for this node based on received user data */ -bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex) -{ - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - if (!info) { - return false; - } +bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex) { + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); + if (!info) { + return false; + } #if !(MESHTASTIC_EXCLUDE_PKI) - if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { - printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); + if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { + printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); - // Alert the user if a remote node is advertising public key that matches our own - if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) { - if (!duplicateWarned) { - duplicateWarned = true; - char warning[] = - "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " - "to regenerate your public keys."; - LOG_WARN(warning, p.long_name); - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, warning, p.long_name); - service->sendClientNotification(cn); - } - return false; - } + // Alert the user if a remote node is advertising public key that matches our own + if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) { + if (!duplicateWarned) { + duplicateWarned = true; + char warning[] = "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " + "to regenerate your public keys."; + LOG_WARN(warning, p.long_name); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, warning, p.long_name); + service->sendClientNotification(cn); + } + return false; } - if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one - // if the key doesn't match, don't update nodeDB at all. - if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { - LOG_WARN("Public Key mismatch, dropping NodeInfo"); - return false; - } - LOG_INFO("Public Key set for node, not updating!"); - } else if (p.public_key.size == 32) { - LOG_INFO("Update Node Pubkey!"); + } + if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one + // if the key doesn't match, don't update nodeDB at all. + if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { + LOG_WARN("Public Key mismatch, dropping NodeInfo"); + return false; } + LOG_INFO("Public Key set for node, not updating!"); + } else if (p.public_key.size == 32) { + LOG_INFO("Update Node Pubkey!"); + } #endif - // Always ensure user.id is derived from nodeId, regardless of what was received - snprintf(p.id, sizeof(p.id), "!%08x", nodeId); + // Always ensure user.id is derived from nodeId, regardless of what was received + snprintf(p.id, sizeof(p.id), "!%08x", nodeId); - // Both of info->user and p start as filled with zero so I think this is okay - auto lite = TypeConversions::ConvertToUserLite(p); - bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); + // Both of info->user and p start as filled with zero so I think this is okay + auto lite = TypeConversions::ConvertToUserLite(p); + bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); - info->user = lite; - if (info->user.public_key.size == 32) { - printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32); + info->user = lite; + if (info->user.public_key.size == 32) { + printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32); + } + if (nodeId != getNodeNum()) + info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) + LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, info->channel); + info->has_user = true; + + if (changed) { + updateGUIforNode = info; + notifyObservers(true); // Force an update whether or not our node counts have changed + + // We just changed something about a User, + // store our DB unless we just did so less than a minute ago + + if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) { + saveToDisk(SEGMENT_NODEDATABASE); + lastNodeDbSave = millis(); + } else { + LOG_DEBUG("Defer NodeDB saveToDisk for now"); } - if (nodeId != getNodeNum()) - info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) - LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, - info->channel); - info->has_user = true; + } - if (changed) { - updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed - - // We just changed something about a User, - // store our DB unless we just did so less than a minute ago - - if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) { - saveToDisk(SEGMENT_NODEDATABASE); - lastNodeDbSave = millis(); - } else { - LOG_DEBUG("Defer NodeDB saveToDisk for now"); - } - } - - return changed; + return changed; } /// given a subpacket sniffed from the network, update our DB state /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw -void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) -{ - if (mp.from == getNodeNum()) { - LOG_DEBUG("Ignore update from self"); - return; - } - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { - LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); +void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { + if (mp.from == getNodeNum()) { + LOG_DEBUG("Ignore update from self"); + return; + } + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { + LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getFrom(&mp)); - if (!info) { - return; - } - - if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard - info->last_heard = mp.rx_time; - - if (mp.rx_snr) - info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. - - info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT - - // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway - const int8_t hopsAway = getHopsAway(mp); - if (hopsAway >= 0) { - info->has_hops_away = true; - info->hops_away = hopsAway; - } - sortMeshDB(); - } -} - -void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) -{ - meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); - if (lite && lite->is_favorite != is_favorite) { - lite->is_favorite = is_favorite; - sortMeshDB(); - saveNodeDatabaseToDisk(); - } -} - -bool NodeDB::isFavorite(uint32_t nodeId) -{ - // returns true if nodeId is_favorite; false if not or not found - - // NODENUM_BROADCAST will never be in the DB - if (nodeId == NODENUM_BROADCAST) - return false; - - meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); - - if (lite) { - return lite->is_favorite; - } - return false; -} - -bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) -{ - // This method is logically equivalent to: - // return isFavorite(p.from) || isFavorite(p.to); - // but is more efficient by: - // 1. doing only one pass through the database, instead of two - // 2. exiting early when a favorite is found, or if both from and to have been seen - - if (p.to == NODENUM_BROADCAST) - return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from - - meshtastic_NodeInfoLite *lite = NULL; - - bool seenFrom = false; - bool seenTo = false; - - for (int i = 0; i < numMeshNodes; i++) { - lite = &meshNodes->at(i); - - if (lite->num == p.from) { - if (lite->is_favorite) - return true; - - seenFrom = true; - } - - if (lite->num == p.to) { - if (lite->is_favorite) - return true; - - seenTo = true; - } - - if (seenFrom && seenTo) - return false; // we've seen both, and neither is a favorite, so we can stop searching early - - // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching - // all favorited nodes first. - } - - return false; -} - -void NodeDB::pause_sort(bool paused) -{ - sortingIsPaused = paused; -} - -void NodeDB::sortMeshDB() -{ - if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { - lastSort = millis(); - bool changed = true; - while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing - changed = false; - for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 - if (meshNodes->at(i - 1).num == getNodeNum()) { - // noop - } else if (meshNodes->at(i).num == - getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there - // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash - std::swap(meshNodes->at(i), meshNodes->at(i - 1)); - changed = true; - } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { - std::swap(meshNodes->at(i), meshNodes->at(i - 1)); - changed = true; - } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { - // noop - } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { - std::swap(meshNodes->at(i), meshNodes->at(i - 1)); - changed = true; - } - } - } - LOG_INFO("Sort took %u milliseconds", millis() - lastSort); - } -} - -uint8_t NodeDB::getMeshNodeChannel(NodeNum n) -{ - const meshtastic_NodeInfoLite *info = getMeshNode(n); + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getFrom(&mp)); if (!info) { - return 0; // defaults to PRIMARY + return; } - return info->channel; + + if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard + info->last_heard = mp.rx_time; + + if (mp.rx_snr) + info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. + + info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT + + // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway + const int8_t hopsAway = getHopsAway(mp); + if (hopsAway >= 0) { + info->has_hops_away = true; + info->hops_away = hopsAway; + } + sortMeshDB(); + } } -std::string NodeDB::getNodeId() const -{ - char nodeId[16]; - snprintf(nodeId, sizeof(nodeId), "!%08x", myNodeInfo.my_node_num); - return std::string(nodeId); +void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) { + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + if (lite && lite->is_favorite != is_favorite) { + lite->is_favorite = is_favorite; + sortMeshDB(); + saveNodeDatabaseToDisk(); + } +} + +bool NodeDB::isFavorite(uint32_t nodeId) { + // returns true if nodeId is_favorite; false if not or not found + + // NODENUM_BROADCAST will never be in the DB + if (nodeId == NODENUM_BROADCAST) + return false; + + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + + if (lite) { + return lite->is_favorite; + } + return false; +} + +bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) { + // This method is logically equivalent to: + // return isFavorite(p.from) || isFavorite(p.to); + // but is more efficient by: + // 1. doing only one pass through the database, instead of two + // 2. exiting early when a favorite is found, or if both from and to have been seen + + if (p.to == NODENUM_BROADCAST) + return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from + + meshtastic_NodeInfoLite *lite = NULL; + + bool seenFrom = false; + bool seenTo = false; + + for (int i = 0; i < numMeshNodes; i++) { + lite = &meshNodes->at(i); + + if (lite->num == p.from) { + if (lite->is_favorite) + return true; + + seenFrom = true; + } + + if (lite->num == p.to) { + if (lite->is_favorite) + return true; + + seenTo = true; + } + + if (seenFrom && seenTo) + return false; // we've seen both, and neither is a favorite, so we can stop searching early + + // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after + // searching all favorited nodes first. + } + + return false; +} + +void NodeDB::pause_sort(bool paused) { sortingIsPaused = paused; } + +void NodeDB::sortMeshDB() { + if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { + lastSort = millis(); + bool changed = true; + while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing + changed = false; + for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 + if (meshNodes->at(i - 1).num == getNodeNum()) { + // noop + } else if (meshNodes->at(i).num == getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there + // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { + // noop + } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } + } + } + LOG_INFO("Sort took %u milliseconds", millis() - lastSort); + } +} + +uint8_t NodeDB::getMeshNodeChannel(NodeNum n) { + const meshtastic_NodeInfoLite *info = getMeshNode(n); + if (!info) { + return 0; // defaults to PRIMARY + } + return info->channel; +} + +std::string NodeDB::getNodeId() const { + char nodeId[16]; + snprintf(nodeId, sizeof(nodeId), "!%08x", myNodeInfo.my_node_num); + return std::string(nodeId); } /// Find a node in our DB, return null for missing /// NOTE: This function might be called from an ISR -meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n) -{ - for (int i = 0; i < numMeshNodes; i++) - if (meshNodes->at(i).num == n) - return &meshNodes->at(i); +meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n) { + for (int i = 0; i < numMeshNodes; i++) + if (meshNodes->at(i).num == n) + return &meshNodes->at(i); - return NULL; + return NULL; } // returns true if the maximum number of nodes is reached or we are running low on memory -bool NodeDB::isFull() -{ - return (numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP); -} +bool NodeDB::isFull() { return (numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP); } /// Find a node in our DB, create an empty NodeInfo if missing -meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) -{ - meshtastic_NodeInfoLite *lite = getMeshNode(n); +meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) { + meshtastic_NodeInfoLite *lite = getMeshNode(n); - if (!lite) { - if (isFull()) { - LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes, - memGet.getFreeHeap()); - // look for oldest node and erase it - uint32_t oldest = UINT32_MAX; - uint32_t oldestBoring = UINT32_MAX; - int oldestIndex = -1; - int oldestBoringIndex = -1; - for (int i = 1; i < numMeshNodes; i++) { - // Simply the oldest non-favorite, non-ignored, non-verified node - if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && - !(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && - meshNodes->at(i).last_heard < oldest) { - oldest = meshNodes->at(i).last_heard; - oldestIndex = i; - } - // The oldest "boring" node - if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && - meshNodes->at(i).last_heard < oldestBoring) { - oldestBoring = meshNodes->at(i).last_heard; - oldestBoringIndex = i; - } - } - // if we found a "boring" node, evict it - if (oldestBoringIndex != -1) { - oldestIndex = oldestBoringIndex; - } - - if (oldestIndex != -1) { - // Shove the remaining nodes down the chain - for (int i = oldestIndex; i < numMeshNodes - 1; i++) { - meshNodes->at(i) = meshNodes->at(i + 1); - } - (numMeshNodes)--; - } + if (!lite) { + if (isFull()) { + LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes, memGet.getFreeHeap()); + // look for oldest node and erase it + uint32_t oldest = UINT32_MAX; + uint32_t oldestBoring = UINT32_MAX; + int oldestIndex = -1; + int oldestBoringIndex = -1; + for (int i = 1; i < numMeshNodes; i++) { + // Simply the oldest non-favorite, non-ignored, non-verified node + if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && + !(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && meshNodes->at(i).last_heard < oldest) { + oldest = meshNodes->at(i).last_heard; + oldestIndex = i; } - // add the node at the end - lite = &meshNodes->at((numMeshNodes)++); + // The oldest "boring" node + if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && + meshNodes->at(i).last_heard < oldestBoring) { + oldestBoring = meshNodes->at(i).last_heard; + oldestBoringIndex = i; + } + } + // if we found a "boring" node, evict it + if (oldestBoringIndex != -1) { + oldestIndex = oldestBoringIndex; + } - // everything is missing except the nodenum - memset(lite, 0, sizeof(*lite)); - lite->num = n; - LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); + if (oldestIndex != -1) { + // Shove the remaining nodes down the chain + for (int i = oldestIndex; i < numMeshNodes - 1; i++) { + meshNodes->at(i) = meshNodes->at(i + 1); + } + (numMeshNodes)--; + } } + // add the node at the end + lite = &meshNodes->at((numMeshNodes)++); - return lite; + // everything is missing except the nodenum + memset(lite, 0, sizeof(*lite)); + lite->num = n; + LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); + } + + return lite; } /// Sometimes we will have Position objects that only have a time, so check for /// valid lat/lon -bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) -{ - return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); +bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) { + return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); } /// If we have a node / user and they report is_licensed = true /// we consider them licensed -UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) -{ - meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); - if (!info || !info->has_user) { - return UserLicenseStatus::NotKnown; - } - return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; +UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) { + meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); + if (!info || !info->has_user) { + return UserLicenseStatus::NotKnown; + } + return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; } #if !defined(MESHTASTIC_EXCLUDE_PKI) -bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest) -{ - if (keyToTest.size == 32) { - uint8_t keyHash[32] = {0}; - memcpy(keyHash, keyToTest.bytes, keyToTest.size); - crypto->hash(keyHash, 32); - for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { - if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) { - return true; - } - } +bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest) { + if (keyToTest.size == 32) { + uint8_t keyHash[32] = {0}; + memcpy(keyHash, keyToTest.bytes, keyToTest.size); + crypto->hash(keyHash, 32); + for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { + if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) { + return true; + } } - return false; + } + return false; } #endif -bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) -{ - bool success = false; - lastBackupAttempt = millis(); +bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { + bool success = false; + lastBackupAttempt = millis(); #ifdef FSCom - if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { - meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; - backup.version = DEVICESTATE_CUR_VER; - backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); - backup.has_config = true; - backup.config = config; - backup.has_module_config = true; - backup.module_config = moduleConfig; - backup.has_channels = true; - backup.channels = channelFile; - backup.has_owner = true; - backup.owner = owner; + if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { + meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; + backup.version = DEVICESTATE_CUR_VER; + backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); + backup.has_config = true; + backup.config = config; + backup.has_module_config = true; + backup.module_config = moduleConfig; + backup.has_channels = true; + backup.channels = channelFile; + backup.has_owner = true; + backup.owner = owner; - size_t backupSize; - pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup); + size_t backupSize; + pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup); - spiLock->lock(); - FSCom.mkdir("/backups"); - spiLock->unlock(); - success = saveProto(backupFileName, backupSize, &meshtastic_BackupPreferences_msg, &backup); + spiLock->lock(); + FSCom.mkdir("/backups"); + spiLock->unlock(); + success = saveProto(backupFileName, backupSize, &meshtastic_BackupPreferences_msg, &backup); - if (success) { - LOG_INFO("Saved backup preferences"); - } else { - LOG_ERROR("Failed to save backup preferences to file"); - } - } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support + if (success) { + LOG_INFO("Saved backup preferences"); + } else { + LOG_ERROR("Failed to save backup preferences to file"); } + } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { + // TODO: After more mainline SD card support + } #endif - return success; + return success; } -bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat) -{ - bool success = false; +bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat) { + bool success = false; #ifdef FSCom - if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { - spiLock->lock(); - if (!FSCom.exists(backupFileName)) { - spiLock->unlock(); - LOG_WARN("Could not restore. No backup file found"); - return false; - } else { - spiLock->unlock(); - } - meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; - success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences), - &meshtastic_BackupPreferences_msg, &backup); - if (success) { - if (restoreWhat & SEGMENT_CONFIG) { - config = backup.config; - LOG_DEBUG("Restored config"); - } - if (restoreWhat & SEGMENT_MODULECONFIG) { - moduleConfig = backup.module_config; - LOG_DEBUG("Restored module config"); - } - if (restoreWhat & SEGMENT_DEVICESTATE) { - devicestate.owner = backup.owner; - LOG_DEBUG("Restored device state"); - } - if (restoreWhat & SEGMENT_CHANNELS) { - channelFile = backup.channels; - LOG_DEBUG("Restored channels"); - } - - success = saveToDisk(restoreWhat); - if (success) { - LOG_INFO("Restored preferences from backup"); - } else { - LOG_ERROR("Failed to save restored preferences to flash"); - } - } else { - LOG_ERROR("Failed to restore preferences from backup file"); - } - } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support + if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { + spiLock->lock(); + if (!FSCom.exists(backupFileName)) { + spiLock->unlock(); + LOG_WARN("Could not restore. No backup file found"); + return false; + } else { + spiLock->unlock(); } - return success; + meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; + success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences), &meshtastic_BackupPreferences_msg, + &backup); + if (success) { + if (restoreWhat & SEGMENT_CONFIG) { + config = backup.config; + LOG_DEBUG("Restored config"); + } + if (restoreWhat & SEGMENT_MODULECONFIG) { + moduleConfig = backup.module_config; + LOG_DEBUG("Restored module config"); + } + if (restoreWhat & SEGMENT_DEVICESTATE) { + devicestate.owner = backup.owner; + LOG_DEBUG("Restored device state"); + } + if (restoreWhat & SEGMENT_CHANNELS) { + channelFile = backup.channels; + LOG_DEBUG("Restored channels"); + } + + success = saveToDisk(restoreWhat); + if (success) { + LOG_INFO("Restored preferences from backup"); + } else { + LOG_ERROR("Failed to save restored preferences to flash"); + } + } else { + LOG_ERROR("Failed to restore preferences from backup file"); + } + } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { + // TODO: After more mainline SD card support + } + return success; #endif } /// Record an error that should be reported via analytics -void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) -{ - if (filename) { - LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); - } else { - LOG_ERROR("NOTE! Record critical error %d, address=0x%lx", code, address); - } +void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) { + if (filename) { + LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); + } else { + LOG_ERROR("NOTE! Record critical error %d, address=0x%lx", code, address); + } - // Record error to DB - error_code = code; - error_address = address; + // Record error to DB + error_code = code; + error_address = address; - // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened + // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened #ifdef ARCH_PORTDUINO - LOG_ERROR("A critical failure occurred, portduino is exiting"); - exit(2); + LOG_ERROR("A critical failure occurred, portduino is exiting"); + exit(2); #endif } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 817e31617..33f8c0aa2 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -20,55 +20,54 @@ #if !defined(MESHTASTIC_EXCLUDE_PKI) // E3B0C442 is the blank hash -static const uint8_t LOW_ENTROPY_HASHES[][32] = { - {0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, 0x08, 0x4f, 0x39, 0x63, 0xea, - 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b}, - {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, 0x64, 0xa3, 0xa6, 0xfe, 0x71, - 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9}, - {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, 0x45, 0x5e, 0x2e, 0xbd, 0x16, - 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f}, - {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, 0x91, 0x18, 0x5a, 0x62, 0xe1, - 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d}, - {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, 0x25, 0x64, 0x54, 0x6a, 0x6b, - 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58}, - {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, 0xd9, 0xd1, 0x5d, 0xc4, 0x68, - 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f}, - {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, 0x8e, 0x98, 0x9d, 0xc2, 0x3e, - 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54}, - {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, 0x2b, 0x3a, 0xe2, 0x43, 0x42, - 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49}, - {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, 0xf9, 0x55, 0xcf, 0x30, 0xa0, - 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70}, - {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, 0x6c, 0x0a, 0x9b, 0x75, 0xa4, - 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b}, - {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, 0x64, 0x82, 0xf0, 0x4d, 0x1b, - 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49}, - {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, 0xa3, 0xc3, 0xe1, 0xcf, 0x97, - 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e}, - {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, 0xff, 0x58, 0x17, 0x91, 0xaa, - 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98}, - {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, 0xb4, 0x13, 0xd2, 0x01, 0x2f, - 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0}, - {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, 0x03, 0xe5, 0x80, 0x59, 0xd3, - 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64}, - {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, 0x2D, 0x3C, 0xDF, 0xB1, 0x22, - 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF}, - {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, 0x26, 0x60, 0xF4, 0x6D, 0xCE, - 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B}, - {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, 0x89, 0x36, 0x40, 0xC9, 0x99, - 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5}, - {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, 0xA4, 0xC2, 0x19, 0xA3, 0x83, - 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60}, - {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, 0x89, 0x9D, 0x0E, 0x24, 0xA1, - 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4}, - {0x91, 0x70, 0xb4, 0x7c, 0xfb, 0xff, 0xa0, 0x59, 0x6a, 0x25, 0x1c, 0xa9, 0x9e, 0xe9, 0x43, 0x81, - 0x5d, 0x74, 0xb1, 0xb1, 0x09, 0x28, 0x00, 0x4a, 0xaf, 0xe3, 0xfc, 0xa9, 0x4e, 0x27, 0x76, 0x4c}, - {0x85, 0xfe, 0x7c, 0xec, 0xb6, 0x78, 0x74, 0xc3, 0xec, 0xe1, 0x32, 0x7f, 0xb0, 0xb7, 0x02, 0x74, - 0xf9, 0x23, 0xd8, 0xe7, 0xfa, 0x14, 0xe6, 0xee, 0x66, 0x44, 0xb1, 0x8c, 0xa5, 0x2f, 0x7e, 0xd2}, - {0x8e, 0x66, 0x65, 0x7b, 0x3b, 0x6f, 0x7e, 0xcc, 0x57, 0xb4, 0x57, 0xea, 0xcc, 0x83, 0xf5, 0xaa, - 0xf7, 0x65, 0xa3, 0xce, 0x93, 0x72, 0x13, 0xc1, 0xb6, 0x46, 0x7b, 0x29, 0x45, 0xb5, 0xc8, 0x93}, - {0xcc, 0x11, 0xfb, 0x1a, 0xab, 0xa1, 0x31, 0x87, 0x6a, 0xc6, 0xde, 0x88, 0x87, 0xa9, 0xb9, 0x59, - 0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}}; +static const uint8_t LOW_ENTROPY_HASHES[][32] = {{0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, 0x08, 0x4f, 0x39, 0x63, 0xea, + 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b}, + {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, 0x64, 0xa3, 0xa6, 0xfe, 0x71, + 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9}, + {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, 0x45, 0x5e, 0x2e, 0xbd, 0x16, + 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f}, + {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, 0x91, 0x18, 0x5a, 0x62, 0xe1, + 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d}, + {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, 0x25, 0x64, 0x54, 0x6a, 0x6b, + 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58}, + {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, 0xd9, 0xd1, 0x5d, 0xc4, 0x68, + 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f}, + {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, 0x8e, 0x98, 0x9d, 0xc2, 0x3e, + 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54}, + {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, 0x2b, 0x3a, 0xe2, 0x43, 0x42, + 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49}, + {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, 0xf9, 0x55, 0xcf, 0x30, 0xa0, + 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70}, + {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, 0x6c, 0x0a, 0x9b, 0x75, 0xa4, + 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b}, + {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, 0x64, 0x82, 0xf0, 0x4d, 0x1b, + 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49}, + {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, 0xa3, 0xc3, 0xe1, 0xcf, 0x97, + 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e}, + {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, 0xff, 0x58, 0x17, 0x91, 0xaa, + 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98}, + {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, 0xb4, 0x13, 0xd2, 0x01, 0x2f, + 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0}, + {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, 0x03, 0xe5, 0x80, 0x59, 0xd3, + 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64}, + {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, 0x2D, 0x3C, 0xDF, 0xB1, 0x22, + 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF}, + {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, 0x26, 0x60, 0xF4, 0x6D, 0xCE, + 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B}, + {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, 0x89, 0x36, 0x40, 0xC9, 0x99, + 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5}, + {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, 0xA4, 0xC2, 0x19, 0xA3, 0x83, + 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60}, + {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, 0x89, 0x9D, 0x0E, 0x24, 0xA1, + 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4}, + {0x91, 0x70, 0xb4, 0x7c, 0xfb, 0xff, 0xa0, 0x59, 0x6a, 0x25, 0x1c, 0xa9, 0x9e, 0xe9, 0x43, 0x81, + 0x5d, 0x74, 0xb1, 0xb1, 0x09, 0x28, 0x00, 0x4a, 0xaf, 0xe3, 0xfc, 0xa9, 0x4e, 0x27, 0x76, 0x4c}, + {0x85, 0xfe, 0x7c, 0xec, 0xb6, 0x78, 0x74, 0xc3, 0xec, 0xe1, 0x32, 0x7f, 0xb0, 0xb7, 0x02, 0x74, + 0xf9, 0x23, 0xd8, 0xe7, 0xfa, 0x14, 0xe6, 0xee, 0x66, 0x44, 0xb1, 0x8c, 0xa5, 0x2f, 0x7e, 0xd2}, + {0x8e, 0x66, 0x65, 0x7b, 0x3b, 0x6f, 0x7e, 0xcc, 0x57, 0xb4, 0x57, 0xea, 0xcc, 0x83, 0xf5, 0xaa, + 0xf7, 0x65, 0xa3, 0xce, 0x93, 0x72, 0x13, 0xc1, 0xb6, 0x46, 0x7b, 0x29, 0x45, 0xb5, 0xc8, 0x93}, + {0xcc, 0x11, 0xfb, 0x1a, 0xab, 0xa1, 0x31, 0x87, 0x6a, 0xc6, 0xde, 0x88, 0x87, 0xa9, 0xb9, 0x59, + 0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}}; static const char LOW_ENTROPY_WARNING[] = "Compromised keys were detected and regenerated."; #endif /* @@ -115,233 +114,224 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p); int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); enum LoadFileResult { - // Successfully opened the file - LOAD_SUCCESS = 1, - // File does not exist - NOT_FOUND = 2, - // Device does not have a filesystem - NO_FILESYSTEM = 3, - // File exists, but could not decode protobufs - DECODE_FAILED = 4, - // File exists, but open failed for some reason - OTHER_FAILURE = 5 + // Successfully opened the file + LOAD_SUCCESS = 1, + // File does not exist + NOT_FOUND = 2, + // Device does not have a filesystem + NO_FILESYSTEM = 3, + // File exists, but could not decode protobufs + DECODE_FAILED = 4, + // File exists, but open failed for some reason + OTHER_FAILURE = 5 }; enum UserLicenseStatus { NotKnown, NotLicensed, Licensed }; -class NodeDB -{ - // NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt +class NodeDB { + // NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt - // A NodeInfo for every node we've seen - // Eventually use a smarter datastructure - // HashMap nodes; - // Note: these two references just point into our static array we serialize to/from disk + // A NodeInfo for every node we've seen + // Eventually use a smarter datastructure + // HashMap nodes; + // Note: these two references just point into our static array we serialize to/from disk - public: - std::vector *meshNodes; - bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled - meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI - Observable newStatus; - pb_size_t numMeshNodes; +public: + std::vector *meshNodes; + bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled + meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI + Observable newStatus; + pb_size_t numMeshNodes; - bool keyIsLowEntropy = false; - bool hasWarned = false; + bool keyIsLowEntropy = false; + bool hasWarned = false; - /// don't do mesh based algorithm for node id assignment (initially) - /// instead just store in flash - possibly even in the initial alpha release do this hack - NodeDB(); + /// don't do mesh based algorithm for node id assignment (initially) + /// instead just store in flash - possibly even in the initial alpha release do this hack + NodeDB(); - /// write to flash - /// @return true if the save was successful - bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | - SEGMENT_NODEDATABASE); + /// write to flash + /// @return true if the save was successful + bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); - /** Reinit radio config if needed, because either: - * a) sometimes a buggy android app might send us bogus settings or - * b) the client set factory_reset - * - * @param factory_reset if true, reset all settings to factory defaults - * @param is_fresh_install set to true after a fresh install, to trigger NodeInfo/Position requests - * @return true if the config was completely reset, in that case, we should send it back to the client - */ - void resetRadioConfig(bool is_fresh_install = false); + /** Reinit radio config if needed, because either: + * a) sometimes a buggy android app might send us bogus settings or + * b) the client set factory_reset + * + * @param factory_reset if true, reset all settings to factory defaults + * @param is_fresh_install set to true after a fresh install, to trigger NodeInfo/Position requests + * @return true if the config was completely reset, in that case, we should send it back to the client + */ + void resetRadioConfig(bool is_fresh_install = false); - /// given a subpacket sniffed from the network, update our DB state - /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw - void updateFrom(const meshtastic_MeshPacket &p); + /// given a subpacket sniffed from the network, update our DB state + /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw + void updateFrom(const meshtastic_MeshPacket &p); - void addFromContact(const meshtastic_SharedContact); + void addFromContact(const meshtastic_SharedContact); - /** Update position info for this node based on received position data - */ - void updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src = RX_SRC_RADIO); + /** Update position info for this node based on received position data + */ + void updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src = RX_SRC_RADIO); - /** Update telemetry info for this node based on received metrics - */ - void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO); + /** Update telemetry info for this node based on received metrics + */ + void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO); - /** Update user info and channel for this node based on received user data - */ - bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); + /** Update user info and channel for this node based on received user data + */ + bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); - /* - * Sets a node either favorite or unfavorite - */ - void set_favorite(bool is_favorite, uint32_t nodeId); + /* + * Sets a node either favorite or unfavorite + */ + void set_favorite(bool is_favorite, uint32_t nodeId); - /* - * Returns true if the node is in the NodeDB and marked as favorite - */ - bool isFavorite(uint32_t nodeId); + /* + * Returns true if the node is in the NodeDB and marked as favorite + */ + bool isFavorite(uint32_t nodeId); - /* - * Returns true if p->from or p->to is a favorited node - */ - bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); + /* + * Returns true if p->from or p->to is a favorited node + */ + bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); - /** - * Other functions like the node picker can request a pause in the node sorting - */ - void pause_sort(bool paused); + /** + * Other functions like the node picker can request a pause in the node sorting + */ + void pause_sort(bool paused); - /// @return our node number - NodeNum getNodeNum() { return myNodeInfo.my_node_num; } + /// @return our node number + NodeNum getNodeNum() { return myNodeInfo.my_node_num; } - /// @return our node ID as a string in the format "!xxxxxxxx" - std::string getNodeId() const; + /// @return our node ID as a string in the format "!xxxxxxxx" + std::string getNodeId() const; - // @return last byte of a NodeNum, 0xFF if it ended at 0x00 - uint8_t getLastByteOfNodeNum(NodeNum num) { return (uint8_t)((num & 0xFF) ? (num & 0xFF) : 0xFF); } + // @return last byte of a NodeNum, 0xFF if it ended at 0x00 + uint8_t getLastByteOfNodeNum(NodeNum num) { return (uint8_t)((num & 0xFF) ? (num & 0xFF) : 0xFF); } - /// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay for use - // bool handleWantNodeNum(NodeNum n); + /// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay + /// for use + // bool handleWantNodeNum(NodeNum n); - /* void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea - and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum message. - the unconfigured node num would only be used while initially joining the mesh so low odds of conflicting (especially if we - randomly select from a small number of nodenums which can be used temporarily for this operation). figure out what the lower - level mesh sw does if it does conflict? would it be better for people who are replying with denynode num to just broadcast - their denial?) - */ + /* void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea + and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum + message. the unconfigured node num would only be used while initially joining the mesh so low odds of conflicting + (especially if we randomly select from a small number of nodenums which can be used temporarily for this operation). + figure out what the lower level mesh sw does if it does conflict? would it be better for people who are replying with + denynode num to just broadcast their denial?) + */ - // get channel channel index we heard a nodeNum on, defaults to 0 if not found - uint8_t getMeshNodeChannel(NodeNum n); + // get channel channel index we heard a nodeNum on, defaults to 0 if not found + uint8_t getMeshNodeChannel(NodeNum n); - /* Return the number of nodes we've heard from recently (within the last 2 hrs?) - * @param localOnly if true, ignore nodes heard via MQTT - */ - size_t getNumOnlineMeshNodes(bool localOnly = false); + /* Return the number of nodes we've heard from recently (within the last 2 hrs?) + * @param localOnly if true, ignore nodes heard via MQTT + */ + size_t getNumOnlineMeshNodes(bool localOnly = false); - void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false), - removeNodeByNum(NodeNum nodeNum); + void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false), removeNodeByNum(NodeNum nodeNum); - bool factoryReset(bool eraseBleBonds = false); + bool factoryReset(bool eraseBleBonds = false); - LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, - void *dest_struct); - bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, - bool fullAtomic = true); + LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct); + bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic = true); - void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role); + void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role); - const meshtastic_NodeInfoLite *readNextMeshNode(uint32_t &readIndex); + const meshtastic_NodeInfoLite *readNextMeshNode(uint32_t &readIndex); - meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x) - { - assert(x < numMeshNodes); - return &meshNodes->at(x); + meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x) { + assert(x < numMeshNodes); + return &meshNodes->at(x); + } + + virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); + size_t getNumMeshNodes() { return numMeshNodes; } + + UserLicenseStatus getLicenseStatus(uint32_t nodeNum); + + size_t getMaxNodesAllocatedSize() { + meshtastic_NodeDatabase emptyNodeDatabase; + emptyNodeDatabase.version = DEVICESTATE_CUR_VER; + size_t nodeDatabaseSize; + pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); + return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); + } + + // returns true if the maximum number of nodes is reached or we are running low on memory + bool isFull(); + + void clearLocalPosition(); + + void setLocalPosition(meshtastic_Position position, bool timeOnly = false) { + if (timeOnly) { + LOG_DEBUG("Set local position time only: time=%u timestamp=%u", position.time, position.timestamp); + localPosition.time = position.time; + localPosition.timestamp = position.timestamp > 0 ? position.timestamp : position.time; + return; } - - virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); - size_t getNumMeshNodes() { return numMeshNodes; } - - UserLicenseStatus getLicenseStatus(uint32_t nodeNum); - - size_t getMaxNodesAllocatedSize() - { - meshtastic_NodeDatabase emptyNodeDatabase; - emptyNodeDatabase.version = DEVICESTATE_CUR_VER; - size_t nodeDatabaseSize; - pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); - return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); + LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, position.time, position.timestamp); + localPosition = position; + if (position.latitude_i != 0 || position.longitude_i != 0) { + localPositionUpdatedSinceBoot = true; } + } - // returns true if the maximum number of nodes is reached or we are running low on memory - bool isFull(); - - void clearLocalPosition(); - - void setLocalPosition(meshtastic_Position position, bool timeOnly = false) - { - if (timeOnly) { - LOG_DEBUG("Set local position time only: time=%u timestamp=%u", position.time, position.timestamp); - localPosition.time = position.time; - localPosition.timestamp = position.timestamp > 0 ? position.timestamp : position.time; - return; - } - LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, - position.time, position.timestamp); - localPosition = position; - if (position.latitude_i != 0 || position.longitude_i != 0) { - localPositionUpdatedSinceBoot = true; - } - } - - bool hasValidPosition(const meshtastic_NodeInfoLite *n); - bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } + bool hasValidPosition(const meshtastic_NodeInfoLite *n); + bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } #if !defined(MESHTASTIC_EXCLUDE_PKI) - bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); + bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); #endif - bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); - bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, - int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); + bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, + int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); - /// Notify observers of changes to the DB - void notifyObservers(bool forceUpdate = false) - { - // Notify observers of the current node state - const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate); - newStatus.notifyObservers(&status); - } + /// Notify observers of changes to the DB + void notifyObservers(bool forceUpdate = false) { + // Notify observers of the current node state + const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate); + newStatus.notifyObservers(&status); + } - private: - bool duplicateWarned = false; - bool localPositionUpdatedSinceBoot = false; - uint32_t lastNodeDbSave = 0; // when we last saved our db to flash - uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually - uint32_t lastSort = 0; // When last sorted the nodeDB - /// Find a node in our DB, create an empty NodeInfoLite if missing - meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); +private: + bool duplicateWarned = false; + bool localPositionUpdatedSinceBoot = false; + uint32_t lastNodeDbSave = 0; // when we last saved our db to flash + uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually + uint32_t lastSort = 0; // When last sorted the nodeDB + /// Find a node in our DB, create an empty NodeInfoLite if missing + meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); - /* - * Internal boolean to track sorting paused - */ - bool sortingIsPaused = false; + /* + * Internal boolean to track sorting paused + */ + bool sortingIsPaused = false; - /// pick a provisional nodenum we hope no one is using - void pickNewNodeNum(); + /// pick a provisional nodenum we hope no one is using + void pickNewNodeNum(); - /// read our db from flash - void loadFromDisk(); + /// read our db from flash + void loadFromDisk(); - /// purge db entries without user info - void cleanupMeshDB(); + /// purge db entries without user info + void cleanupMeshDB(); - /// Reinit device state from scratch (not loading from disk) - void installDefaultDeviceState(), installDefaultNodeDatabase(), installDefaultChannels(), - installDefaultConfig(bool preserveKey), installDefaultModuleConfig(); + /// Reinit device state from scratch (not loading from disk) + void installDefaultDeviceState(), installDefaultNodeDatabase(), installDefaultChannels(), installDefaultConfig(bool preserveKey), + installDefaultModuleConfig(); - /// write to flash - /// @return true if the save was successful - bool saveToDiskNoRetry(int saveWhat); + /// write to flash + /// @return true if the save was successful + bool saveToDiskNoRetry(int saveWhat); - bool saveChannelsToDisk(); - bool saveDeviceStateToDisk(); - bool saveNodeDatabaseToDisk(); - void sortMeshDB(); + bool saveChannelsToDisk(); + bool saveDeviceStateToDisk(); + bool saveNodeDatabaseToDisk(); + void sortMeshDB(); }; extern NodeDB *nodeDB; @@ -379,9 +369,9 @@ extern uint32_t error_address; #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) -#define Module_Config_size \ - (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ - ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \ - ModuleConfig_TelemetryConfig_size + ModuleConfig_size) +#define Module_Config_size \ + (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ + ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + ModuleConfig_TelemetryConfig_size + \ + ModuleConfig_size) // Please do not remove this comment, it makes trunk and compiler happy at the same time. diff --git a/src/mesh/PacketCache.cpp b/src/mesh/PacketCache.cpp index 0edf81741..e8306760e 100644 --- a/src/mesh/PacketCache.cpp +++ b/src/mesh/PacketCache.cpp @@ -6,248 +6,233 @@ PacketCache packetCache{}; /** * Allocate a new cache entry and copy the packet header and payload into it */ -PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata) -{ - size_t payload_size = - (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size; - PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size + - (preserveMetadata ? sizeof(PacketCacheMetadata) : 0)); - if (!e) { - LOG_ERROR("Unable to allocate memory for packet cache entry"); - return NULL; - } +PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata) { + size_t payload_size = (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size; + PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size + (preserveMetadata ? sizeof(PacketCacheMetadata) : 0)); + if (!e) { + LOG_ERROR("Unable to allocate memory for packet cache entry"); + return NULL; + } - *e = {}; - e->header.from = p->from; - e->header.to = p->to; - e->header.id = p->id; - e->header.channel = p->channel; - e->header.next_hop = p->next_hop; - e->header.relay_node = p->relay_node; - e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | - (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) | - ((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK); + *e = {}; + e->header.from = p->from; + e->header.to = p->to; + e->header.id = p->id; + e->header.channel = p->channel; + e->header.next_hop = p->next_hop; + e->header.relay_node = p->relay_node; + e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | + (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) | ((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK); - PacketCacheMetadata m{}; + PacketCacheMetadata m{}; + if (preserveMetadata) { + e->has_metadata = true; + m.rx_rssi = (uint8_t)(p->rx_rssi + 200); + m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f); + m.rx_time = p->rx_time; + m.transport_mechanism = p->transport_mechanism; + m.priority = p->priority; + } + if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + e->encrypted = true; + e->payload_len = p->encrypted.size; + memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size); + } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + e->encrypted = false; if (preserveMetadata) { - e->has_metadata = true; - m.rx_rssi = (uint8_t)(p->rx_rssi + 200); - m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f); - m.rx_time = p->rx_time; - m.transport_mechanism = p->transport_mechanism; - m.priority = p->priority; + m.portnum = p->decoded.portnum; + m.want_response = p->decoded.want_response; + m.emoji = p->decoded.emoji; + m.bitfield = p->decoded.bitfield; + if (p->decoded.reply_id) + m.reply_id = p->decoded.reply_id; + else if (p->decoded.request_id) + m.request_id = p->decoded.request_id; } - if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { - e->encrypted = true; - e->payload_len = p->encrypted.size; - memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size); - } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - e->encrypted = false; - if (preserveMetadata) { - m.portnum = p->decoded.portnum; - m.want_response = p->decoded.want_response; - m.emoji = p->decoded.emoji; - m.bitfield = p->decoded.bitfield; - if (p->decoded.reply_id) - m.reply_id = p->decoded.reply_id; - else if (p->decoded.request_id) - m.request_id = p->decoded.request_id; - } - e->payload_len = p->decoded.payload.size; - memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size); - } else { - LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant); - free(e); - return NULL; - } - if (preserveMetadata) - memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m)); + e->payload_len = p->decoded.payload.size; + memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size); + } else { + LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant); + free(e); + return NULL; + } + if (preserveMetadata) + memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m)); - size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); - insert(e); - return e; + size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); + insert(e); + return e; }; /** * Dump a list of packets into the provided buffer */ -void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries) -{ - unsigned char *pos = (unsigned char *)dest; - for (size_t i = 0; i < num_entries; i++) { - size_t entry_len = - sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0); - memcpy(pos, entries[i], entry_len); - pos += entry_len; - } +void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries) { + unsigned char *pos = (unsigned char *)dest; + for (size_t i = 0; i < num_entries; i++) { + size_t entry_len = sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0); + memcpy(pos, entries[i], entry_len); + pos += entry_len; + } } /** * Calculate the length of buffer needed to dump the specified entries */ -size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries) -{ - size_t total_size = 0; - for (size_t i = 0; i < num_entries; i++) { - total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len; - if (entries[i]->has_metadata) - total_size += sizeof(PacketCacheMetadata); - } - return total_size; +size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries) { + size_t total_size = 0; + for (size_t i = 0; i < num_entries; i++) { + total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len; + if (entries[i]->has_metadata) + total_size += sizeof(PacketCacheMetadata); + } + return total_size; } /** * Find a packet in the cache */ -PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id) -{ - uint16_t h = PACKET_HASH(from, id); - PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; - while (e) { - if (e->header.from == from && e->header.id == id) - return e; - e = e->next; - } - return NULL; +PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id) { + uint16_t h = PACKET_HASH(from, id); + PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; + while (e) { + if (e->header.from == from && e->header.id == id) + return e; + e = e->next; + } + return NULL; } /** * Find a packet in the cache by its hash */ -PacketCacheEntry *PacketCache::find(PacketHash h) -{ - PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; - while (e) { - if (PACKET_HASH(e->header.from, e->header.id) == h) - return e; - e = e->next; - } - return NULL; +PacketCacheEntry *PacketCache::find(PacketHash h) { + PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; + while (e) { + if (PACKET_HASH(e->header.from, e->header.id) == h) + return e; + e = e->next; + } + return NULL; } /** * Load a list of packets from the provided buffer */ -bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries) -{ - memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries); - unsigned char *pos = (unsigned char *)src; - for (size_t i = 0; i < num_entries; i++) { - PacketCacheEntry e{}; - memcpy(&e, pos, sizeof(PacketCacheEntry)); - size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0); - entries[i] = (PacketCacheEntry *)malloc(entry_len); - size += entry_len; - if (!entries[i]) { - LOG_ERROR("Unable to allocate memory for packet cache entry"); - for (size_t j = 0; j < i; j++) { - size -= sizeof(PacketCacheEntry) + entries[j]->payload_len + - (entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0); - free(entries[j]); - entries[j] = NULL; - } - return false; - } - memcpy(entries[i], pos, entry_len); - pos += entry_len; +bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries) { + memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries); + unsigned char *pos = (unsigned char *)src; + for (size_t i = 0; i < num_entries; i++) { + PacketCacheEntry e{}; + memcpy(&e, pos, sizeof(PacketCacheEntry)); + size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0); + entries[i] = (PacketCacheEntry *)malloc(entry_len); + size += entry_len; + if (!entries[i]) { + LOG_ERROR("Unable to allocate memory for packet cache entry"); + for (size_t j = 0; j < i; j++) { + size -= sizeof(PacketCacheEntry) + entries[j]->payload_len + (entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0); + free(entries[j]); + entries[j] = NULL; + } + return false; } - for (size_t i = 0; i < num_entries; i++) - insert(entries[i]); - return true; + memcpy(entries[i], pos, entry_len); + pos += entry_len; + } + for (size_t i = 0; i < num_entries; i++) + insert(entries[i]); + return true; } /** * Copy the cached packet into the provided MeshPacket structure */ -void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p) -{ - if (!e || !p) - return; +void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p) { + if (!e || !p) + return; - *p = {}; - p->from = e->header.from; - p->to = e->header.to; - p->id = e->header.id; - p->channel = e->header.channel; - p->next_hop = e->header.next_hop; - p->relay_node = e->header.relay_node; - p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; - p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK); - p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK); - p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; - p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag; + *p = {}; + p->from = e->header.from; + p->to = e->header.to; + p->id = e->header.id; + p->channel = e->header.channel; + p->next_hop = e->header.next_hop; + p->relay_node = e->header.relay_node; + p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; + p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK); + p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK); + p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; + p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag; - unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry); - PacketCacheMetadata m{}; + unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry); + PacketCacheMetadata m{}; + if (e->has_metadata) { + memcpy(&m, (payload + e->payload_len), sizeof(m)); + p->rx_rssi = ((int)m.rx_rssi) - 200; + p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f; + p->rx_time = m.rx_time; + p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism; + p->priority = (meshtastic_MeshPacket_Priority)m.priority; + } + if (e->encrypted) { + memcpy(p->encrypted.bytes, payload, e->payload_len); + p->encrypted.size = e->payload_len; + } else { + memcpy(p->decoded.payload.bytes, payload, e->payload_len); + p->decoded.payload.size = e->payload_len; if (e->has_metadata) { - memcpy(&m, (payload + e->payload_len), sizeof(m)); - p->rx_rssi = ((int)m.rx_rssi) - 200; - p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f; - p->rx_time = m.rx_time; - p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism; - p->priority = (meshtastic_MeshPacket_Priority)m.priority; - } - if (e->encrypted) { - memcpy(p->encrypted.bytes, payload, e->payload_len); - p->encrypted.size = e->payload_len; - } else { - memcpy(p->decoded.payload.bytes, payload, e->payload_len); - p->decoded.payload.size = e->payload_len; - if (e->has_metadata) { - // Decrypted-only metadata - p->decoded.portnum = (meshtastic_PortNum)m.portnum; - p->decoded.want_response = m.want_response; - p->decoded.emoji = m.emoji; - p->decoded.bitfield = m.bitfield; - if (m.reply_id) - p->decoded.reply_id = m.reply_id; - else if (m.request_id) - p->decoded.request_id = m.request_id; - } + // Decrypted-only metadata + p->decoded.portnum = (meshtastic_PortNum)m.portnum; + p->decoded.want_response = m.want_response; + p->decoded.emoji = m.emoji; + p->decoded.bitfield = m.bitfield; + if (m.reply_id) + p->decoded.reply_id = m.reply_id; + else if (m.request_id) + p->decoded.request_id = m.request_id; } + } } /** * Release a cache entry */ -void PacketCache::release(PacketCacheEntry *e) -{ - if (!e) - return; - remove(e); - size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); - free(e); +void PacketCache::release(PacketCacheEntry *e) { + if (!e) + return; + remove(e); + size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); + free(e); } /** * Insert a new entry into the hash table */ -void PacketCache::insert(PacketCacheEntry *e) -{ - assert(e); - PacketHash h = PACKET_HASH(e->header.from, e->header.id); - PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; - e->next = *target; - *target = e; - num_entries++; +void PacketCache::insert(PacketCacheEntry *e) { + assert(e); + PacketHash h = PACKET_HASH(e->header.from, e->header.id); + PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; + e->next = *target; + *target = e; + num_entries++; } /** * Remove an entry from the hash table */ -void PacketCache::remove(PacketCacheEntry *e) -{ - assert(e); - PacketHash h = PACKET_HASH(e->header.from, e->header.id); - PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; - while (*target) { - if (*target == e) { - *target = e->next; - e->next = NULL; - num_entries--; - break; - } else { - target = &(*target)->next; - } +void PacketCache::remove(PacketCacheEntry *e) { + assert(e); + PacketHash h = PACKET_HASH(e->header.from, e->header.id); + PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; + while (*target) { + if (*target == e) { + *target = e->next; + e->next = NULL; + num_entries--; + break; + } else { + target = &(*target)->next; } + } } \ No newline at end of file diff --git a/src/mesh/PacketCache.h b/src/mesh/PacketCache.h index 85660922b..89c2dcfa6 100644 --- a/src/mesh/PacketCache.h +++ b/src/mesh/PacketCache.h @@ -8,68 +8,67 @@ typedef uint16_t PacketHash; #define PACKET_CACHE_BUCKET(h) (((h >> 12) ^ (h >> 6) ^ h) & 0x3F) // Fold hash down to 6-bit bucket index typedef struct PacketCacheEntry { - PacketCacheEntry *next; - PacketHeader header; - uint16_t payload_len = 0; - union { - uint16_t bitfield; - struct { - uint8_t encrypted : 1; // Payload is encrypted - uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata - uint8_t : 6; // Reserved for future use - uint8_t : 8; // Reserved for future use - }; + PacketCacheEntry *next; + PacketHeader header; + uint16_t payload_len = 0; + union { + uint16_t bitfield; + struct { + uint8_t encrypted : 1; // Payload is encrypted + uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata + uint8_t : 6; // Reserved for future use + uint8_t : 8; // Reserved for future use }; + }; } PacketCacheEntry; typedef struct PacketCacheMetadata { - PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {} - union { - uint32_t _bitfield; - struct { - uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum - uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response - uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji - uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated) - uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200) - uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f) - }; - }; - union { - uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id - uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id - }; - uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time - uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism + PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {} + union { + uint32_t _bitfield; struct { - uint8_t _bitfield2; - union { - uint8_t priority : 7; // meshtastic_MeshPacket::priority - uint8_t reserved : 1; // Reserved for future use - }; + uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum + uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response + uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji + uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated) + uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200) + uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f) }; + }; + union { + uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id + uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id + }; + uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time + uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism + struct { + uint8_t _bitfield2; + union { + uint8_t priority : 7; // meshtastic_MeshPacket::priority + uint8_t reserved : 1; // Reserved for future use + }; + }; } PacketCacheMetadata; -class PacketCache -{ - public: - PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata); - static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries); - size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries); - PacketCacheEntry *find(NodeNum from, PacketId id); - PacketCacheEntry *find(PacketHash h); - bool load(void *src, PacketCacheEntry **entries, size_t num_entries); - size_t getNumEntries() { return num_entries; } - size_t getSize() { return size; } - void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p); - void release(PacketCacheEntry *e); +class PacketCache { +public: + PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata); + static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries); + size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries); + PacketCacheEntry *find(NodeNum from, PacketId id); + PacketCacheEntry *find(PacketHash h); + bool load(void *src, PacketCacheEntry **entries, size_t num_entries); + size_t getNumEntries() { return num_entries; } + size_t getSize() { return size; } + void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p); + void release(PacketCacheEntry *e); - private: - PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{}; - size_t num_entries = 0; - size_t size = 0; - void insert(PacketCacheEntry *e); - void remove(PacketCacheEntry *e); +private: + PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{}; + size_t num_entries = 0; + size_t size = 0; + void insert(PacketCacheEntry *e); + void remove(PacketCacheEntry *e); }; extern PacketCache packetCache; \ No newline at end of file diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index b4af707ae..e3dfd2e44 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -7,454 +7,425 @@ #endif #include "Throttle.h" -#define PACKETHISTORY_MAX \ - max((u_int32_t)(MAX_NUM_NODES * 2.0), \ - (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 +#define PACKETHISTORY_MAX \ + max((u_int32_t)(MAX_NUM_NODES * 2.0), (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 #define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min #define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging #define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots -PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members +PacketHistory::PacketHistory(uint32_t size) + : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members { - if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense - LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); - size = PACKETHISTORY_MAX; // Use default size if invalid - } + if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense + LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); + size = PACKETHISTORY_MAX; // Use default size if invalid + } - // Allocate memory for the recent packets array - recentPacketsCapacity = size; - recentPackets = new PacketRecord[recentPacketsCapacity]; - if (!recentPackets) { // No logging here, console/log probably uninitialized yet. - LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, - sizeof(PacketRecord) * recentPacketsCapacity); - recentPacketsCapacity = 0; // mark allocation fail - return; // return early - } + // Allocate memory for the recent packets array + recentPacketsCapacity = size; + recentPackets = new PacketRecord[recentPacketsCapacity]; + if (!recentPackets) { // No logging here, console/log probably uninitialized yet. + LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, sizeof(PacketRecord) * recentPacketsCapacity); + recentPacketsCapacity = 0; // mark allocation fail + return; // return early + } - // Initialize the recent packets array to zero - memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); + // Initialize the recent packets array to zero + memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); } -PacketHistory::~PacketHistory() -{ - recentPacketsCapacity = 0; - delete[] recentPackets; - recentPackets = NULL; +PacketHistory::~PacketHistory() { + recentPacketsCapacity = 0; + delete[] recentPackets; + recentPackets = NULL; } /** Update recentPackets and return true if we have already seen this packet */ -bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop, - bool *wasUpgraded) -{ - if (!initOk()) { - LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); - return false; - } +bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop, bool *wasUpgraded) { + if (!initOk()) { + LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); + return false; + } - if (p->id == 0) { + if (p->id == 0) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); + LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); #endif - return false; // Not a floodable message ID, so we don't care - } + return false; // Not a floodable message ID, so we don't care + } - PacketRecord r; - memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero + PacketRecord r; + memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero - // Save basic info from checked packet - r.id = p->id; - r.sender = getFrom(p); // If 0 then use our ID - r.next_hop = p->next_hop; - setHighestHopLimit(r, p->hop_limit); - bool weWillRelay = false; - uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); - if (p->relay_node == ourRelayID) { // If the relay_node is us, store it - weWillRelay = true; - setOurTxHopLimit(r, p->hop_limit); - r.relayed_by[0] = p->relay_node; - } + // Save basic info from checked packet + r.id = p->id; + r.sender = getFrom(p); // If 0 then use our ID + r.next_hop = p->next_hop; + setHighestHopLimit(r, p->hop_limit); + bool weWillRelay = false; + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); + if (p->relay_node == ourRelayID) { // If the relay_node is us, store it + weWillRelay = true; + setOurTxHopLimit(r, p->hop_limit); + r.relayed_by[0] = p->relay_node; + } - r.rxTimeMsec = millis(); // - if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special - r.rxTimeMsec = 1; + r.rxTimeMsec = millis(); // + if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special + r.rxTimeMsec = 1; #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d wWNH?%d", - r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, - weWereNextHop ? *weWereNextHop : -1); + LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d " + "wWNH?%d", + r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, + weWereNextHop ? *weWereNextHop : -1); #endif - PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array - bool seenRecently = (found != NULL); // If found -> the packet was seen recently + PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array + bool seenRecently = (found != NULL); // If found -> the packet was seen recently - // Check for hop_limit upgrade scenario - if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) { - LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit, - p->hop_limit); - *wasUpgraded = true; - } else if (wasUpgraded) { - *wasUpgraded = false; // Initialize to false if not an upgrade + // Check for hop_limit upgrade scenario + if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) { + LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit, p->hop_limit); + *wasUpgraded = true; + } else if (wasUpgraded) { + *wasUpgraded = false; // Initialize to false if not an upgrade + } + + if (seenRecently) { + if (wasFallback) { + // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed + // already before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need + // to handle it now. + if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && found->next_hop != ourRelayID && + p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && !wasRelayer(ourRelayID, *found) && + !wasRelayer(found->next_hop, + *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", p->from, p->id, p->next_hop, + p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif + *wasFallback = true; + } else { + // debug log only +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", p->from, p->id, p->next_hop, + p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif + } } - if (seenRecently) { - if (wasFallback) { - // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already - // before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle - // it now. - if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && - found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && - !wasRelayer(ourRelayID, *found) && - !wasRelayer( - found->next_hop, - *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet + // Check if we were the next hop for this packet + if (weWereNextHop) { + *weWereNextHop = (found->next_hop == ourRelayID); #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", - p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", p->from, p->id, p->next_hop, + p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); #endif - *wasFallback = true; - } else { - // debug log only + } + } + + if (withUpdate) { + if (found != NULL) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", - p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", found->sender, found->id, + found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], millis() - found->rxTimeMsec); #endif - } + // Only update the relayer if it heard us directly (meaning hopLimit is decreased by 1) + uint8_t startIdx = weWillRelay ? 1 : 0; + if (!weWillRelay) { + bool weWereRelayer = wasRelayer(ourRelayID, *found); + // We were a relayer and the packet came in with a hop limit that is one less than when we sent it out + if (weWereRelayer && (p->hop_limit == getOurTxHopLimit(*found) || p->hop_limit == getOurTxHopLimit(*found) - 1)) { + r.relayed_by[0] = p->relay_node; + startIdx = 1; // Start copying existing relayers from index 1 + } + // keep the original ourTxHopLimit + setOurTxHopLimit(r, getOurTxHopLimit(*found)); + } + + // Preserve the highest hop_limit we've ever seen for this packet so upgrades aren't lost when a later copy has + // fewer hops remaining. + if (getHighestHopLimit(*found) > getHighestHopLimit(r)) + setHighestHopLimit(r, getHighestHopLimit(*found)); + + // Add the existing relayed_by to the new record, avoiding duplicates + for (uint8_t i = 0; i < (NUM_RELAYERS - startIdx); i++) { + if (found->relayed_by[i] == 0) + continue; + + bool exists = false; + for (uint8_t j = 0; j < NUM_RELAYERS; j++) { + if (r.relayed_by[j] == found->relayed_by[i]) { + exists = true; + break; + } } - // Check if we were the next hop for this packet - if (weWereNextHop) { - *weWereNextHop = (found->next_hop == ourRelayID); -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", - p->from, p->id, p->next_hop, p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); -#endif + if (!exists) { + r.relayed_by[i + startIdx] = found->relayed_by[i]; } + } + r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, r.id, r.next_hop, + r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); +#endif + // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this } - - if (withUpdate) { - if (found != NULL) { + insert(r); // Insert or update the packet record in the history + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", - found->sender, found->id, found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], - millis() - found->rxTimeMsec); -#endif - // Only update the relayer if it heard us directly (meaning hopLimit is decreased by 1) - uint8_t startIdx = weWillRelay ? 1 : 0; - if (!weWillRelay) { - bool weWereRelayer = wasRelayer(ourRelayID, *found); - // We were a relayer and the packet came in with a hop limit that is one less than when we sent it out - if (weWereRelayer && (p->hop_limit == getOurTxHopLimit(*found) || p->hop_limit == getOurTxHopLimit(*found) - 1)) { - r.relayed_by[0] = p->relay_node; - startIdx = 1; // Start copying existing relayers from index 1 - } - // keep the original ourTxHopLimit - setOurTxHopLimit(r, getOurTxHopLimit(*found)); - } - - // Preserve the highest hop_limit we've ever seen for this packet so upgrades aren't lost when a later copy has - // fewer hops remaining. - if (getHighestHopLimit(*found) > getHighestHopLimit(r)) - setHighestHopLimit(r, getHighestHopLimit(*found)); - - // Add the existing relayed_by to the new record, avoiding duplicates - for (uint8_t i = 0; i < (NUM_RELAYERS - startIdx); i++) { - if (found->relayed_by[i] == 0) - continue; - - bool exists = false; - for (uint8_t j = 0; j < NUM_RELAYERS; j++) { - if (r.relayed_by[j] == found->relayed_by[i]) { - exists = true; - break; - } - } - - if (!exists) { - r.relayed_by[i + startIdx] = found->relayed_by[i]; - } - } - r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, - r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); -#endif - // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this - } - insert(r); // Insert or update the packet record in the history - } -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " - "found?%s seenRecently?%s wUpd?%s", - r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, - found ? "YES" : "NO ", seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); + LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " + "found?%s seenRecently?%s wUpd?%s", + r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, found ? "YES" : "NO ", + seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); #endif - return seenRecently; + return seenRecently; } /** Find a packet record in history. * @return pointer to PacketRecord if found, NULL if not found */ -PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) -{ - if (sender == 0 || id == 0) { +PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) { + if (sender == 0 || id == 0) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); + LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); #endif - return NULL; - } + return NULL; + } - PacketRecord *it = NULL; - for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { - if (it->id == id && it->sender == sender) { + PacketRecord *it = NULL; + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == id && it->sender == sender) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, - it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), - it - recentPackets, recentPacketsCapacity); + LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, it->id, it->next_hop, + it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), it - recentPackets, recentPacketsCapacity); #endif - // only the first match is returned, so be careful not to create duplicate entries - return it; // Return pointer to the found record - } + // only the first match is returned, so be careful not to create duplicate entries + return it; // Return pointer to the found record } + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); + LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); #endif - return NULL; // Not found + return NULL; // Not found } /** Insert/Replace oldest PacketRecord in recentPackets. */ -void PacketHistory::insert(const PacketRecord &r) -{ - uint32_t now_millis = millis(); // Should not jump with time changes - uint32_t OldtrxTimeMsec = 0; - PacketRecord *tu = NULL; // Will insert here. - PacketRecord *it = NULL; +void PacketHistory::insert(const PacketRecord &r) { + uint32_t now_millis = millis(); // Should not jump with time changes + uint32_t OldtrxTimeMsec = 0; + PacketRecord *tu = NULL; // Will insert here. + PacketRecord *it = NULL; - // Find a free, matching or oldest used slot in the recentPackets array - for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { - if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty - tu = it; // Remember the free slot + // Find a free, matching or oldest used slot in the recentPackets array + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty + tu = it; // Remember the free slot #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); + LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); #endif - // We have that, Exit the loop - it = (recentPackets + recentPacketsCapacity); - } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert - tu = it; // Remember the matching slot - OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert + tu = it; // Remember the matching slot + OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, - OldtrxTimeMsec); + LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); #endif - // We have that, Exit the loop - it = (recentPackets + recentPacketsCapacity); - } else { - if (it->rxTimeMsec == 0) { - LOG_WARN( - "Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!", - it->sender, it->id, it - recentPackets, recentPacketsCapacity); - } - if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly - OldtrxTimeMsec = now_millis - it->rxTimeMsec; - tu = it; // remember the oldest packet -#if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, - OldtrxTimeMsec); -#endif - } - // keep looking for oldest till entire array is checked - } - } - - if (tu == NULL) { - LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx - // assert(false); // This should never happen, we should always have at least one packet to clear - return; // Return early if we can't update the history - } - -#if VERBOSE_PACKET_HISTORY - if (tu->id == 0 && tu->sender == 0) { - LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); - } else if (tu->id == r.id && tu->sender == r.sender) { - LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, - OldtrxTimeMsec); + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); } else { - LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, - OldtrxTimeMsec); + if (it->rxTimeMsec == 0) { + LOG_WARN("Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never " + "happen!", + it->sender, it->id, it - recentPackets, recentPacketsCapacity); + } + if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly + OldtrxTimeMsec = now_millis - it->rxTimeMsec; + tu = it; // remember the oldest packet +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); +#endif + } + // keep looking for oldest till entire array is checked } + } + + if (tu == NULL) { + LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx + // assert(false); // This should never happen, we should always have at least one packet to clear + return; // Return early if we can't update the history + } + +#if VERBOSE_PACKET_HISTORY + if (tu->id == 0 && tu->sender == 0) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); + } else if (tu->id == r.id && tu->sender == r.sender) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); + } else { + LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); + } #endif - // If we are reusing a slot, we should warn if the packet is too recent + // If we are reusing a slot, we should warn if the packet is too recent #if RECENT_WARN_AGE > 0 - if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { - if (!(tu->id == r.id && tu->sender == r.sender)) { + if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { + if (!(tu->id == r.id && tu->sender == r.sender)) { #if VERBOSE_PACKET_HISTORY - LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, - RECENT_WARN_AGE / 1000); + LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, RECENT_WARN_AGE / 1000); #endif - } else { - // debug only + } else { + // debug only #if VERBOSE_PACKET_HISTORY - LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", - OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000); + LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", OldtrxTimeMsec / 1000., + RECENT_WARN_AGE / 1000); #endif - } } + } #if PACKET_HISTORY_TRACE_AGING - if (tu->rxTimeMsec != 0) { - LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., - (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); - } else { - LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); - } + if (tu->rxTimeMsec != 0) { + LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., + (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); + } else { + LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); + } #endif #endif #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", - tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], - tu->relayed_by[2], tu->rxTimeMsec); + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - recentPackets, + recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2], tu->rxTimeMsec); #endif - if (r.rxTimeMsec == 0) { + if (r.rxTimeMsec == 0) { #if VERBOSE_PACKET_HISTORY - LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); + LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); #endif - return; // Return early if we can't update the history - } + return; // Return early if we can't update the history + } - *tu = r; // store the packet + *tu = r; // store the packet #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", - tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], - tu->relayed_by[2], tu->rxTimeMsec); + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - recentPackets, + recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2], tu->rxTimeMsec); #endif } /* Check if a certain node was a relayer of a packet in the history given an ID and sender * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) -{ - if (!initOk()) { - LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); - return false; - } +bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) { + if (!initOk()) { + LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); + return false; + } - if (relayer == 0) { + if (relayer == 0) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); #endif - return false; - } + return false; + } - const PacketRecord *found = find(sender, id); + const PacketRecord *found = find(sender, id); - if (found == NULL) { + if (found == NULL) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); #endif - return false; - } + return false; + } #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", - found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], - found->relayed_by[2], relayer); + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", found->sender, found->id, + found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif - return wasRelayer(relayer, *found, wasSole); + return wasRelayer(relayer, *found, wasSole); } /* Check if a certain node was a relayer of a packet in the history given iterator * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) -{ - bool found = false; - bool other_present = false; +bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) { + bool found = false; + bool other_present = false; - for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { - if (r.relayed_by[i] == relayer) { - found = true; - } else if (r.relayed_by[i] != 0) { - other_present = true; - } + for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { + if (r.relayed_by[i] == relayer) { + found = true; + } else if (r.relayed_by[i] != 0) { + other_present = true; } + } - if (wasSole) { - *wasSole = (found && !other_present); - } + if (wasSole) { + *wasSole = (found && !other_present); + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], - r.relayed_by[1], r.relayed_by[2], relayer); + LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], r.relayed_by[1], + r.relayed_by[2], relayer); #endif - return found; + return found; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender -void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) -{ - if (!initOk()) { - LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); - return; - } +void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { + if (!initOk()) { + LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); + return; + } - PacketRecord *found = find(sender, id); - if (found == NULL) { + PacketRecord *found = find(sender, id); + if (found == NULL) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); #endif - return; // Nothing to remove - } + return; // Nothing to remove + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, - found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, found->relayed_by[0], + found->relayed_by[1], found->relayed_by[2], relayer); #endif - // nexthop and rxTimeMsec too stay in found entry + // nexthop and rxTimeMsec too stay in found entry - uint8_t j = 0; - uint8_t i = 0; - for (; i < NUM_RELAYERS; i++) { - if (found->relayed_by[i] != relayer) { - found->relayed_by[j] = found->relayed_by[i]; - j++; - } else - found->relayed_by[i] = 0; - } - for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array - found->relayed_by[j] = 0; - } + uint8_t j = 0; + uint8_t i = 0; + for (; i < NUM_RELAYERS; i++) { + if (found->relayed_by[i] != relayer) { + found->relayed_by[j] = found->relayed_by[i]; + j++; + } else + found->relayed_by[i] = 0; + } + for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array + found->relayed_by[j] = 0; + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, - found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, found->id, + found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); #endif } // Getters and setters for hop limit fields packed in hop_limit -inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r) -{ - return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; +inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r) { return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; } + +inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) { + r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); } -inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) -{ - r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); -} +inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) { return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; } -inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) -{ - return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; -} - -inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) -{ - r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); +inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) { + r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); } \ No newline at end of file diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 5fbad2dc9..038d5e31b 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -11,68 +11,68 @@ /** * This is a mixin that adds a record of past packets we have seen */ -class PacketHistory -{ - private: - struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. - NodeNum sender; - PacketId id; - uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty - uint8_t next_hop; // The next hop asked for this packet - uint8_t hop_limit; // bit 0-2: Highest hop limit observed for this packet, - // bit 3-5: our hop limit when we first transmitted it - uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet - }; // 4B + 4B + 4B + 1B + 1B + 6B = 20B +class PacketHistory { +private: + struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. + NodeNum sender; + PacketId id; + uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty + uint8_t next_hop; // The next hop asked for this packet + uint8_t hop_limit; // bit 0-2: Highest hop limit observed for this packet, + // bit 3-5: our hop limit when we first transmitted it + uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet + }; // 4B + 4B + 4B + 1B + 1B + 6B = 20B - uint32_t recentPacketsCapacity = - 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. - PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. + uint32_t recentPacketsCapacity = 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. + PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. - /** Find a packet record in history. - * @param sender NodeNum - * @param id PacketId - * @return pointer to PacketRecord if found, NULL if not found */ - PacketRecord *find(NodeNum sender, PacketId id); + /** Find a packet record in history. + * @param sender NodeNum + * @param id PacketId + * @return pointer to PacketRecord if found, NULL if not found */ + PacketRecord *find(NodeNum sender, PacketId id); - /** Insert/Replace oldest PacketRecord in mx_recentPackets. - * @param r PacketRecord to insert or replace */ - void insert(const PacketRecord &r); // Insert or replace a packet record in the history + /** Insert/Replace oldest PacketRecord in mx_recentPackets. + * @param r PacketRecord to insert or replace */ + void insert(const PacketRecord &r); // Insert or replace a packet record in the history - /* Check if a certain node was a relayer of a packet in the history given iterator - * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet - * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); + /* Check if a certain node was a relayer of a packet in the history given iterator + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); - uint8_t getHighestHopLimit(PacketRecord &r); - void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); - uint8_t getOurTxHopLimit(PacketRecord &r); - void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); + uint8_t getHighestHopLimit(PacketRecord &r); + void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); + uint8_t getOurTxHopLimit(PacketRecord &r); + void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); - PacketHistory(const PacketHistory &); // non construction-copyable - PacketHistory &operator=(const PacketHistory &); // non copyable - public: - explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX - ~PacketHistory(); + PacketHistory(const PacketHistory &); // non construction-copyable + PacketHistory &operator=(const PacketHistory &); // non copyable +public: + explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX + ~PacketHistory(); - /** - * Update recentBroadcasts and return true if we have already seen this packet - * - * @param withUpdate if true and not found we add an entry to recentPackets - * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if so - * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true if so - * @param wasUpgraded if not nullptr, will be set to true if this packet has better hop_limit than previously seen - */ - bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr, - bool *weWereNextHop = nullptr, bool *wasUpgraded = nullptr); + /** + * Update recentBroadcasts and return true if we have already seen this packet + * + * @param withUpdate if true and not found we add an entry to recentPackets + * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if + * so + * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true + * if so + * @param wasUpgraded if not nullptr, will be set to true if this packet has better hop_limit than previously seen + */ + bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr, bool *weWereNextHop = nullptr, + bool *wasUpgraded = nullptr); - /* Check if a certain node was a relayer of a packet in the history given an ID and sender - * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet - * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); + /* Check if a certain node was a relayer of a packet in the history given an ID and sender + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); - // Remove a relayer from the list of relayers of a packet in the history given an ID and sender - void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); + // Remove a relayer from the list of relayers of a packet in the history given an ID and sender + void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); - // To check if the PacketHistory was initialized correctly by constructor - bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } + // To check if the PacketHistory was initialized correctly by constructor + bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } }; diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 9050ee89d..47b378136 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -35,171 +35,163 @@ // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; -PhoneAPI::PhoneAPI() -{ - lastContactMsec = millis(); - std::fill(std::begin(recentToRadioPacketIds), std::end(recentToRadioPacketIds), 0); +PhoneAPI::PhoneAPI() { + lastContactMsec = millis(); + std::fill(std::begin(recentToRadioPacketIds), std::end(recentToRadioPacketIds), 0); } -PhoneAPI::~PhoneAPI() -{ - close(); -} +PhoneAPI::~PhoneAPI() { close(); } -void PhoneAPI::handleStartConfig() -{ - // Must be before setting state (because state is how we know !connected) - if (!isConnected()) { - onConnectionChanged(true); - observe(&service->fromNumChanged); +void PhoneAPI::handleStartConfig() { + // Must be before setting state (because state is how we know !connected) + if (!isConnected()) { + onConnectionChanged(true); + observe(&service->fromNumChanged); #ifdef FSCom - observe(&xModem.packetReady); + observe(&xModem.packetReady); #endif - } + } - // Allow subclasses to prepare for high-throughput config traffic - onConfigStart(); + // Allow subclasses to prepare for high-throughput config traffic + onConfigStart(); - // even if we were already connected - restart our state machine - if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { - // If client only wants node info, jump directly to sending nodes - state = STATE_SEND_OWN_NODEINFO; - LOG_INFO("Client only wants node info, skipping other config"); - } else { - state = STATE_SEND_MY_INFO; - } - pauseBluetoothLogging = true; - spiLock->lock(); - filesManifest = getFiles("/", 10); - spiLock->unlock(); - LOG_DEBUG("Got %d files in manifest", filesManifest.size()); + // even if we were already connected - restart our state machine + if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { + // If client only wants node info, jump directly to sending nodes + state = STATE_SEND_OWN_NODEINFO; + LOG_INFO("Client only wants node info, skipping other config"); + } else { + state = STATE_SEND_MY_INFO; + } + pauseBluetoothLogging = true; + spiLock->lock(); + filesManifest = getFiles("/", 10); + spiLock->unlock(); + LOG_DEBUG("Got %d files in manifest", filesManifest.size()); - LOG_INFO("Start API client config millis=%u", millis()); - // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone = {}; - nodeInfoQueue.clear(); - } + LOG_INFO("Start API client config millis=%u", millis()); + // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = {}; + nodeInfoQueue.clear(); + } + resetReadIndex(); +} + +void PhoneAPI::close() { + LOG_DEBUG("PhoneAPI::close()"); + if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_SERIAL && api_type == TYPE_SERIAL) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_PACKET && api_type == TYPE_PACKET) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_HTTP && api_type == TYPE_HTTP) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_ETH && api_type == TYPE_ETH) + service->api_state = service->STATE_DISCONNECTED; + + if (state != STATE_SEND_NOTHING) { + state = STATE_SEND_NOTHING; resetReadIndex(); -} - -void PhoneAPI::close() -{ - LOG_DEBUG("PhoneAPI::close()"); - if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_SERIAL && api_type == TYPE_SERIAL) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_PACKET && api_type == TYPE_PACKET) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_HTTP && api_type == TYPE_HTTP) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_ETH && api_type == TYPE_ETH) - service->api_state = service->STATE_DISCONNECTED; - - if (state != STATE_SEND_NOTHING) { - state = STATE_SEND_NOTHING; - resetReadIndex(); - unobserve(&service->fromNumChanged); + unobserve(&service->fromNumChanged); #ifdef FSCom - unobserve(&xModem.packetReady); + unobserve(&xModem.packetReady); #endif - releasePhonePacket(); // Don't leak phone packets on shutdown - releaseQueueStatusPhonePacket(); - releaseMqttClientProxyPhonePacket(); - releaseClientNotification(); - onConnectionChanged(false); - fromRadioScratch = {}; - toRadioScratch = {}; - // Clear cached node info under lock because NimBLE callbacks can still be draining it. - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone = {}; - nodeInfoQueue.clear(); - } - packetForPhone = NULL; - filesManifest.clear(); - fromRadioNum = 0; - config_nonce = 0; - config_state = 0; - pauseBluetoothLogging = false; - heartbeatReceived = false; + releasePhonePacket(); // Don't leak phone packets on shutdown + releaseQueueStatusPhonePacket(); + releaseMqttClientProxyPhonePacket(); + releaseClientNotification(); + onConnectionChanged(false); + fromRadioScratch = {}; + toRadioScratch = {}; + // Clear cached node info under lock because NimBLE callbacks can still be draining it. + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = {}; + nodeInfoQueue.clear(); } + packetForPhone = NULL; + filesManifest.clear(); + fromRadioNum = 0; + config_nonce = 0; + config_state = 0; + pauseBluetoothLogging = false; + heartbeatReceived = false; + } } -bool PhoneAPI::checkConnectionTimeout() -{ - if (isConnected()) { - bool newContact = checkIsConnected(); - if (!newContact) { - LOG_INFO("Lost phone connection"); - close(); - return true; - } +bool PhoneAPI::checkConnectionTimeout() { + if (isConnected()) { + bool newContact = checkIsConnected(); + if (!newContact) { + LOG_INFO("Lost phone connection"); + close(); + return true; } - return false; + } + return false; } /** * Handle a ToRadio protobuf */ -bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) -{ - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep - lastContactMsec = millis(); +bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) { + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep + lastContactMsec = millis(); - memset(&toRadioScratch, 0, sizeof(toRadioScratch)); - if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) { - switch (toRadioScratch.which_payload_variant) { - case meshtastic_ToRadio_packet_tag: - return handleToRadioPacket(toRadioScratch.packet); - case meshtastic_ToRadio_want_config_id_tag: - config_nonce = toRadioScratch.want_config_id; - LOG_INFO("Client wants config, nonce=%u", config_nonce); - handleStartConfig(); - break; - case meshtastic_ToRadio_disconnect_tag: - LOG_INFO("Disconnect from phone"); - close(); - break; - case meshtastic_ToRadio_xmodemPacket_tag: - LOG_INFO("Got xmodem packet"); + memset(&toRadioScratch, 0, sizeof(toRadioScratch)); + if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) { + switch (toRadioScratch.which_payload_variant) { + case meshtastic_ToRadio_packet_tag: + return handleToRadioPacket(toRadioScratch.packet); + case meshtastic_ToRadio_want_config_id_tag: + config_nonce = toRadioScratch.want_config_id; + LOG_INFO("Client wants config, nonce=%u", config_nonce); + handleStartConfig(); + break; + case meshtastic_ToRadio_disconnect_tag: + LOG_INFO("Disconnect from phone"); + close(); + break; + case meshtastic_ToRadio_xmodemPacket_tag: + LOG_INFO("Got xmodem packet"); #ifdef FSCom - xModem.handlePacket(toRadioScratch.xmodemPacket); + xModem.handlePacket(toRadioScratch.xmodemPacket); #endif - break; + break; #if !MESHTASTIC_EXCLUDE_MQTT - case meshtastic_ToRadio_mqttClientProxyMessage_tag: - LOG_DEBUG("Got MqttClientProxy message"); - if (state != STATE_SEND_PACKETS) { - LOG_WARN("Ignore MqttClientProxy message while completing config handshake"); - break; - } - if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled && - (channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) { - mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage); - } else { - LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting " - "not enabled"); - } - break; + case meshtastic_ToRadio_mqttClientProxyMessage_tag: + LOG_DEBUG("Got MqttClientProxy message"); + if (state != STATE_SEND_PACKETS) { + LOG_WARN("Ignore MqttClientProxy message while completing config handshake"); + break; + } + if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled && + (channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) { + mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage); + } else { + LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting " + "not enabled"); + } + break; #endif - case meshtastic_ToRadio_heartbeat_tag: - LOG_DEBUG("Got client heartbeat"); - heartbeatReceived = true; - break; - default: - // Ignore nop messages - break; - } - } else { - LOG_ERROR("Error: ignore malformed toradio"); + case meshtastic_ToRadio_heartbeat_tag: + LOG_DEBUG("Got client heartbeat"); + heartbeatReceived = true; + break; + default: + // Ignore nop messages + break; } + } else { + LOG_ERROR("Error: ignore malformed toradio"); + } - return false; + return false; } /** @@ -221,624 +213,610 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) STATE_SEND_PACKETS // send packets or debug strings */ -size_t PhoneAPI::getFromRadio(uint8_t *buf) -{ - // Respond to heartbeat by sending queue status - if (heartbeatReceived) { - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; - fromRadioScratch.queueStatus = router->getQueueStatus(); - heartbeatReceived = false; - size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); - LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes); - return numbytes; - } - - if (!available()) { - return 0; - } - // In case we send a FromRadio packet +size_t PhoneAPI::getFromRadio(uint8_t *buf) { + // Respond to heartbeat by sending queue status + if (heartbeatReceived) { memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; + fromRadioScratch.queueStatus = router->getQueueStatus(); + heartbeatReceived = false; + size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); + LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes); + return numbytes; + } - // Advance states as needed - switch (state) { - case STATE_SEND_NOTHING: - LOG_DEBUG("FromRadio=STATE_SEND_NOTHING"); - break; - case STATE_SEND_MY_INFO: - LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO"); - // If the user has specified they don't want our node to share its location, make sure to tell the phone - // app not to send locations on our behalf. - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag; - strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env)); - myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes()); - fromRadioScratch.my_info = myNodeInfo; - state = STATE_SEND_UIDATA; - - service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon. - break; - - case STATE_SEND_UIDATA: - LOG_INFO("getFromRadio=STATE_SEND_UIDATA"); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag; - fromRadioScratch.deviceuiConfig = uiconfig; - state = STATE_SEND_OWN_NODEINFO; - break; - - case STATE_SEND_OWN_NODEINFO: { - LOG_DEBUG("Send My NodeInfo"); - auto us = nodeDB->readNextMeshNode(readIndex); - if (us) { - auto info = TypeConversions::ConvertToNodeInfo(us); - info.has_hops_away = false; - info.is_favorite = true; - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone = info; - } - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; - fromRadioScratch.node_info = info; - // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone.num = 0; - } - } - if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { - // If client only wants node info, jump directly to sending nodes - state = STATE_SEND_OTHER_NODEINFOS; - onNowHasData(0); - } else { - state = STATE_SEND_METADATA; - } - break; - } - - case STATE_SEND_METADATA: - LOG_DEBUG("Send device metadata"); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag; - fromRadioScratch.metadata = getDeviceMetadata(); - state = STATE_SEND_CHANNELS; - break; - - case STATE_SEND_CHANNELS: - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag; - fromRadioScratch.channel = channels.getByIndex(config_state); - config_state++; - // Advance when we have sent all of our Channels - if (config_state >= MAX_NUM_CHANNELS) { - LOG_DEBUG("Send channels %d", config_state); - state = STATE_SEND_CONFIG; - config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1; - } - break; - - case STATE_SEND_CONFIG: - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; - switch (config_state) { - case meshtastic_Config_device_tag: - LOG_DEBUG("Send config: device"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag; - fromRadioScratch.config.payload_variant.device = config.device; - break; - case meshtastic_Config_position_tag: - LOG_DEBUG("Send config: position"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag; - fromRadioScratch.config.payload_variant.position = config.position; - break; - case meshtastic_Config_power_tag: - LOG_DEBUG("Send config: power"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_power_tag; - fromRadioScratch.config.payload_variant.power = config.power; - fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs; - break; - case meshtastic_Config_network_tag: - LOG_DEBUG("Send config: network"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag; - fromRadioScratch.config.payload_variant.network = config.network; - break; - case meshtastic_Config_display_tag: - LOG_DEBUG("Send config: display"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag; - fromRadioScratch.config.payload_variant.display = config.display; - break; - case meshtastic_Config_lora_tag: - LOG_DEBUG("Send config: lora"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag; - fromRadioScratch.config.payload_variant.lora = config.lora; - break; - case meshtastic_Config_bluetooth_tag: - LOG_DEBUG("Send config: bluetooth"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; - fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; - break; - case meshtastic_Config_security_tag: - LOG_DEBUG("Send config: security"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag; - fromRadioScratch.config.payload_variant.security = config.security; - break; - case meshtastic_Config_sessionkey_tag: - LOG_DEBUG("Send config: sessionkey"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_tag; - break; - case meshtastic_Config_device_ui_tag: // NOOP! - fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_ui_tag; - break; - default: - LOG_ERROR("Unknown config type %d", config_state); - } - // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. - // So even if we internally use 0 to represent 'use default' we still need to send the value we are - // using to the app (so that even old phone apps work with new device loads). - - config_state++; - // Advance when we have sent all of our config objects - if (config_state > (_meshtastic_AdminMessage_ConfigType_MAX + 1)) { - state = STATE_SEND_MODULECONFIG; - config_state = _meshtastic_AdminMessage_ModuleConfigType_MIN + 1; - } - break; - - case STATE_SEND_MODULECONFIG: - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; - switch (config_state) { - case meshtastic_ModuleConfig_mqtt_tag: - LOG_DEBUG("Send module config: mqtt"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; - fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt; - break; - case meshtastic_ModuleConfig_serial_tag: - LOG_DEBUG("Send module config: serial"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag; - fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; - break; - case meshtastic_ModuleConfig_external_notification_tag: - LOG_DEBUG("Send module config: ext notification"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; - fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; - break; - case meshtastic_ModuleConfig_store_forward_tag: - LOG_DEBUG("Send module config: store forward"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; - fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; - break; - case meshtastic_ModuleConfig_range_test_tag: - LOG_DEBUG("Send module config: range test"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; - fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; - break; - case meshtastic_ModuleConfig_telemetry_tag: - LOG_DEBUG("Send module config: telemetry"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; - fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry; - break; - case meshtastic_ModuleConfig_canned_message_tag: - LOG_DEBUG("Send module config: canned message"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; - fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; - break; - case meshtastic_ModuleConfig_audio_tag: - LOG_DEBUG("Send module config: audio"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; - fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; - break; - case meshtastic_ModuleConfig_remote_hardware_tag: - LOG_DEBUG("Send module config: remote hardware"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; - fromRadioScratch.moduleConfig.payload_variant.remote_hardware = moduleConfig.remote_hardware; - break; - case meshtastic_ModuleConfig_neighbor_info_tag: - LOG_DEBUG("Send module config: neighbor info"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; - fromRadioScratch.moduleConfig.payload_variant.neighbor_info = moduleConfig.neighbor_info; - break; - case meshtastic_ModuleConfig_detection_sensor_tag: - LOG_DEBUG("Send module config: detection sensor"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; - fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; - break; - case meshtastic_ModuleConfig_ambient_lighting_tag: - LOG_DEBUG("Send module config: ambient lighting"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; - fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; - break; - case meshtastic_ModuleConfig_paxcounter_tag: - LOG_DEBUG("Send module config: paxcounter"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; - fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; - break; - default: - LOG_ERROR("Unknown module config type %d", config_state); - } - - config_state++; - // Advance when we have sent all of our ModuleConfig objects - if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { - // Handle special nonce behaviors: - // - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest - // - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete - if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { - state = STATE_SEND_FILEMANIFEST; - } else { - state = STATE_SEND_OTHER_NODEINFOS; - onNowHasData(0); - } - config_state = 0; - } - break; - - case STATE_SEND_OTHER_NODEINFOS: { - if (readIndex == 2) { // readIndex==2 will be true for the first non-us node - LOG_INFO("Start sending nodeinfos millis=%u", millis()); - } - - meshtastic_NodeInfo infoToSend = {}; - { - concurrency::LockGuard guard(&nodeInfoMutex); - if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { - // Serve the next cached node without re-reading from the DB iterator. - nodeInfoForPhone = nodeInfoQueue.front(); - nodeInfoQueue.pop_front(); - } - infoToSend = nodeInfoForPhone; - if (infoToSend.num != 0) - nodeInfoForPhone = {}; - } - - if (infoToSend.num != 0) { - // Just in case we stored a different user.id in the past, but should never happen going forward - sprintf(infoToSend.user.id, "!%08x", infoToSend.num); - - // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only - // uncomment if you really need to: - // LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, - // nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); - - // Occasional progress logging. (readIndex==2 will be true for the first non-us node) - if (readIndex == 2 || readIndex % 20 == 0) { - LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes()); - } - - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; - fromRadioScratch.node_info = infoToSend; - prefetchNodeInfos(); - } else { - LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoQueue.clear(); - state = STATE_SEND_FILEMANIFEST; - // Go ahead and send that ID right now - return getFromRadio(buf); - } - break; - } - - case STATE_SEND_FILEMANIFEST: { - LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); - // last element - if (config_state == filesManifest.size() || - config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest - config_state = 0; - filesManifest.clear(); - // Skip to complete packet - sendConfigComplete(); - } else { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag; - fromRadioScratch.fileInfo = filesManifest.at(config_state); - LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes); - config_state++; - } - break; - } - - case STATE_SEND_COMPLETE_ID: - sendConfigComplete(); - break; - - case STATE_SEND_PACKETS: - pauseBluetoothLogging = false; - // Do we have a message from the mesh or packet from the local device? - LOG_DEBUG("FromRadio=STATE_SEND_PACKETS"); - if (queueStatusPacketForPhone) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; - fromRadioScratch.queueStatus = *queueStatusPacketForPhone; - releaseQueueStatusPhonePacket(); - } else if (mqttClientProxyMessageForPhone) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag; - fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone; - releaseMqttClientProxyPhonePacket(); - } else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag; - fromRadioScratch.xmodemPacket = xmodemPacketForPhone; - xmodemPacketForPhone = meshtastic_XModem_init_zero; - } else if (clientNotification) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag; - fromRadioScratch.clientNotification = *clientNotification; - releaseClientNotification(); - } else if (packetForPhone) { - printPacket("phone downloaded packet", packetForPhone); - - // Encapsulate as a FromRadio packet - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; - fromRadioScratch.packet = *packetForPhone; - releasePhonePacket(); - } - break; - - default: - LOG_ERROR("getFromRadio unexpected state %d", state); - } - - // Do we have a message from the mesh? - if (fromRadioScratch.which_payload_variant != 0) { - // Encapsulate as a FromRadio packet - size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); - - // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer - // for logging (when we are encapsulating with protobufs) - return numbytes; - } - - LOG_DEBUG("No FromRadio packet available"); + if (!available()) { return 0; -} + } + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); -void PhoneAPI::sendConfigComplete() -{ - LOG_INFO("Config Send Complete millis=%u", millis()); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; - fromRadioScratch.config_complete_id = config_nonce; - config_nonce = 0; - state = STATE_SEND_PACKETS; - if (api_type == TYPE_BLE) { - service->api_state = service->STATE_BLE; - } else if (api_type == TYPE_WIFI) { - service->api_state = service->STATE_WIFI; - } else if (api_type == TYPE_SERIAL) { - service->api_state = service->STATE_SERIAL; - } else if (api_type == TYPE_PACKET) { - service->api_state = service->STATE_PACKET; - } else if (api_type == TYPE_HTTP) { - service->api_state = service->STATE_HTTP; - } else if (api_type == TYPE_ETH) { - service->api_state = service->STATE_ETH; - } + // Advance states as needed + switch (state) { + case STATE_SEND_NOTHING: + LOG_DEBUG("FromRadio=STATE_SEND_NOTHING"); + break; + case STATE_SEND_MY_INFO: + LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO"); + // If the user has specified they don't want our node to share its location, make sure to tell the phone + // app not to send locations on our behalf. + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag; + strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env)); + myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes()); + fromRadioScratch.my_info = myNodeInfo; + state = STATE_SEND_UIDATA; - // Allow subclasses to know we've entered steady-state so they can lower power consumption - onConfigComplete(); + service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon. + break; - pauseBluetoothLogging = false; -} + case STATE_SEND_UIDATA: + LOG_INFO("getFromRadio=STATE_SEND_UIDATA"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag; + fromRadioScratch.deviceuiConfig = uiconfig; + state = STATE_SEND_OWN_NODEINFO; + break; -void PhoneAPI::releasePhonePacket() -{ - if (packetForPhone) { - service->releaseToPool(packetForPhone); // we just copied the bytes, so don't need this buffer anymore - packetForPhone = NULL; - } -} - -void PhoneAPI::releaseQueueStatusPhonePacket() -{ - if (queueStatusPacketForPhone) { - service->releaseQueueStatusToPool(queueStatusPacketForPhone); - queueStatusPacketForPhone = NULL; - } -} - -void PhoneAPI::prefetchNodeInfos() -{ - bool added = false; - // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. - { + case STATE_SEND_OWN_NODEINFO: { + LOG_DEBUG("Send My NodeInfo"); + auto us = nodeDB->readNextMeshNode(readIndex); + if (us) { + auto info = TypeConversions::ConvertToNodeInfo(us); + info.has_hops_away = false; + info.is_favorite = true; + { concurrency::LockGuard guard(&nodeInfoMutex); - while (nodeInfoQueue.size() < kNodePrefetchDepth) { - auto nextNode = nodeDB->readNextMeshNode(readIndex); - if (!nextNode) - break; + nodeInfoForPhone = info; + } + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; + fromRadioScratch.node_info = info; + // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone.num = 0; + } + } + if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { + // If client only wants node info, jump directly to sending nodes + state = STATE_SEND_OTHER_NODEINFOS; + onNowHasData(0); + } else { + state = STATE_SEND_METADATA; + } + break; + } - auto info = TypeConversions::ConvertToNodeInfo(nextNode); - bool isUs = info.num == nodeDB->getNodeNum(); - info.hops_away = isUs ? 0 : info.hops_away; - info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; - info.snr = isUs ? 0 : info.snr; - info.via_mqtt = isUs ? false : info.via_mqtt; - info.is_favorite = info.is_favorite || isUs; - nodeInfoQueue.push_back(info); - added = true; - } + case STATE_SEND_METADATA: + LOG_DEBUG("Send device metadata"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag; + fromRadioScratch.metadata = getDeviceMetadata(); + state = STATE_SEND_CHANNELS; + break; + + case STATE_SEND_CHANNELS: + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag; + fromRadioScratch.channel = channels.getByIndex(config_state); + config_state++; + // Advance when we have sent all of our Channels + if (config_state >= MAX_NUM_CHANNELS) { + LOG_DEBUG("Send channels %d", config_state); + state = STATE_SEND_CONFIG; + config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1; + } + break; + + case STATE_SEND_CONFIG: + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; + switch (config_state) { + case meshtastic_Config_device_tag: + LOG_DEBUG("Send config: device"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag; + fromRadioScratch.config.payload_variant.device = config.device; + break; + case meshtastic_Config_position_tag: + LOG_DEBUG("Send config: position"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag; + fromRadioScratch.config.payload_variant.position = config.position; + break; + case meshtastic_Config_power_tag: + LOG_DEBUG("Send config: power"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_power_tag; + fromRadioScratch.config.payload_variant.power = config.power; + fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs; + break; + case meshtastic_Config_network_tag: + LOG_DEBUG("Send config: network"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag; + fromRadioScratch.config.payload_variant.network = config.network; + break; + case meshtastic_Config_display_tag: + LOG_DEBUG("Send config: display"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag; + fromRadioScratch.config.payload_variant.display = config.display; + break; + case meshtastic_Config_lora_tag: + LOG_DEBUG("Send config: lora"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag; + fromRadioScratch.config.payload_variant.lora = config.lora; + break; + case meshtastic_Config_bluetooth_tag: + LOG_DEBUG("Send config: bluetooth"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; + fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; + break; + case meshtastic_Config_security_tag: + LOG_DEBUG("Send config: security"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag; + fromRadioScratch.config.payload_variant.security = config.security; + break; + case meshtastic_Config_sessionkey_tag: + LOG_DEBUG("Send config: sessionkey"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_tag; + break; + case meshtastic_Config_device_ui_tag: // NOOP! + fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_ui_tag; + break; + default: + LOG_ERROR("Unknown config type %d", config_state); + } + // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. + // So even if we internally use 0 to represent 'use default' we still need to send the value we are + // using to the app (so that even old phone apps work with new device loads). + + config_state++; + // Advance when we have sent all of our config objects + if (config_state > (_meshtastic_AdminMessage_ConfigType_MAX + 1)) { + state = STATE_SEND_MODULECONFIG; + config_state = _meshtastic_AdminMessage_ModuleConfigType_MIN + 1; + } + break; + + case STATE_SEND_MODULECONFIG: + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; + switch (config_state) { + case meshtastic_ModuleConfig_mqtt_tag: + LOG_DEBUG("Send module config: mqtt"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; + fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt; + break; + case meshtastic_ModuleConfig_serial_tag: + LOG_DEBUG("Send module config: serial"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag; + fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + LOG_DEBUG("Send module config: ext notification"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; + fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; + break; + case meshtastic_ModuleConfig_store_forward_tag: + LOG_DEBUG("Send module config: store forward"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; + fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; + break; + case meshtastic_ModuleConfig_range_test_tag: + LOG_DEBUG("Send module config: range test"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; + fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; + break; + case meshtastic_ModuleConfig_telemetry_tag: + LOG_DEBUG("Send module config: telemetry"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; + fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + LOG_DEBUG("Send module config: canned message"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; + fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; + break; + case meshtastic_ModuleConfig_audio_tag: + LOG_DEBUG("Send module config: audio"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; + fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + LOG_DEBUG("Send module config: remote hardware"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; + fromRadioScratch.moduleConfig.payload_variant.remote_hardware = moduleConfig.remote_hardware; + break; + case meshtastic_ModuleConfig_neighbor_info_tag: + LOG_DEBUG("Send module config: neighbor info"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; + fromRadioScratch.moduleConfig.payload_variant.neighbor_info = moduleConfig.neighbor_info; + break; + case meshtastic_ModuleConfig_detection_sensor_tag: + LOG_DEBUG("Send module config: detection sensor"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; + fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; + break; + case meshtastic_ModuleConfig_ambient_lighting_tag: + LOG_DEBUG("Send module config: ambient lighting"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; + fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; + break; + case meshtastic_ModuleConfig_paxcounter_tag: + LOG_DEBUG("Send module config: paxcounter"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; + fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; + break; + default: + LOG_ERROR("Unknown module config type %d", config_state); } - if (added) + config_state++; + // Advance when we have sent all of our ModuleConfig objects + if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { + // Handle special nonce behaviors: + // - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest + // - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete + if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { + state = STATE_SEND_FILEMANIFEST; + } else { + state = STATE_SEND_OTHER_NODEINFOS; onNowHasData(0); + } + config_state = 0; + } + break; + + case STATE_SEND_OTHER_NODEINFOS: { + if (readIndex == 2) { // readIndex==2 will be true for the first non-us node + LOG_INFO("Start sending nodeinfos millis=%u", millis()); + } + + meshtastic_NodeInfo infoToSend = {}; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { + // Serve the next cached node without re-reading from the DB iterator. + nodeInfoForPhone = nodeInfoQueue.front(); + nodeInfoQueue.pop_front(); + } + infoToSend = nodeInfoForPhone; + if (infoToSend.num != 0) + nodeInfoForPhone = {}; + } + + if (infoToSend.num != 0) { + // Just in case we stored a different user.id in the past, but should never happen going forward + sprintf(infoToSend.user.id, "!%08x", infoToSend.num); + + // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so + // only uncomment if you really need to: LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", + // nodeInfoForPhone.num, nodeInfoForPhone.last_heard, nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); + + // Occasional progress logging. (readIndex==2 will be true for the first non-us node) + if (readIndex == 2 || readIndex % 20 == 0) { + LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes()); + } + + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; + fromRadioScratch.node_info = infoToSend; + prefetchNodeInfos(); + } else { + LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoQueue.clear(); + state = STATE_SEND_FILEMANIFEST; + // Go ahead and send that ID right now + return getFromRadio(buf); + } + break; + } + + case STATE_SEND_FILEMANIFEST: { + LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); + // last element + if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest + config_state = 0; + filesManifest.clear(); + // Skip to complete packet + sendConfigComplete(); + } else { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag; + fromRadioScratch.fileInfo = filesManifest.at(config_state); + LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes); + config_state++; + } + break; + } + + case STATE_SEND_COMPLETE_ID: + sendConfigComplete(); + break; + + case STATE_SEND_PACKETS: + pauseBluetoothLogging = false; + // Do we have a message from the mesh or packet from the local device? + LOG_DEBUG("FromRadio=STATE_SEND_PACKETS"); + if (queueStatusPacketForPhone) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; + fromRadioScratch.queueStatus = *queueStatusPacketForPhone; + releaseQueueStatusPhonePacket(); + } else if (mqttClientProxyMessageForPhone) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag; + fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone; + releaseMqttClientProxyPhonePacket(); + } else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag; + fromRadioScratch.xmodemPacket = xmodemPacketForPhone; + xmodemPacketForPhone = meshtastic_XModem_init_zero; + } else if (clientNotification) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag; + fromRadioScratch.clientNotification = *clientNotification; + releaseClientNotification(); + } else if (packetForPhone) { + printPacket("phone downloaded packet", packetForPhone); + + // Encapsulate as a FromRadio packet + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; + fromRadioScratch.packet = *packetForPhone; + releasePhonePacket(); + } + break; + + default: + LOG_ERROR("getFromRadio unexpected state %d", state); + } + + // Do we have a message from the mesh? + if (fromRadioScratch.which_payload_variant != 0) { + // Encapsulate as a FromRadio packet + size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); + + // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer + // for logging (when we are encapsulating with protobufs) + return numbytes; + } + + LOG_DEBUG("No FromRadio packet available"); + return 0; } -void PhoneAPI::releaseMqttClientProxyPhonePacket() -{ - if (mqttClientProxyMessageForPhone) { - service->releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone); - mqttClientProxyMessageForPhone = NULL; - } +void PhoneAPI::sendConfigComplete() { + LOG_INFO("Config Send Complete millis=%u", millis()); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; + fromRadioScratch.config_complete_id = config_nonce; + config_nonce = 0; + state = STATE_SEND_PACKETS; + if (api_type == TYPE_BLE) { + service->api_state = service->STATE_BLE; + } else if (api_type == TYPE_WIFI) { + service->api_state = service->STATE_WIFI; + } else if (api_type == TYPE_SERIAL) { + service->api_state = service->STATE_SERIAL; + } else if (api_type == TYPE_PACKET) { + service->api_state = service->STATE_PACKET; + } else if (api_type == TYPE_HTTP) { + service->api_state = service->STATE_HTTP; + } else if (api_type == TYPE_ETH) { + service->api_state = service->STATE_ETH; + } + + // Allow subclasses to know we've entered steady-state so they can lower power consumption + onConfigComplete(); + + pauseBluetoothLogging = false; } -void PhoneAPI::releaseClientNotification() -{ - if (clientNotification) { - service->releaseClientNotificationToPool(clientNotification); - clientNotification = NULL; +void PhoneAPI::releasePhonePacket() { + if (packetForPhone) { + service->releaseToPool(packetForPhone); // we just copied the bytes, so don't need this buffer anymore + packetForPhone = NULL; + } +} + +void PhoneAPI::releaseQueueStatusPhonePacket() { + if (queueStatusPacketForPhone) { + service->releaseQueueStatusToPool(queueStatusPacketForPhone); + queueStatusPacketForPhone = NULL; + } +} + +void PhoneAPI::prefetchNodeInfos() { + bool added = false; + // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. + { + concurrency::LockGuard guard(&nodeInfoMutex); + while (nodeInfoQueue.size() < kNodePrefetchDepth) { + auto nextNode = nodeDB->readNextMeshNode(readIndex); + if (!nextNode) + break; + + auto info = TypeConversions::ConvertToNodeInfo(nextNode); + bool isUs = info.num == nodeDB->getNodeNum(); + info.hops_away = isUs ? 0 : info.hops_away; + info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; + info.snr = isUs ? 0 : info.snr; + info.via_mqtt = isUs ? false : info.via_mqtt; + info.is_favorite = info.is_favorite || isUs; + nodeInfoQueue.push_back(info); + added = true; } + } + + if (added) + onNowHasData(0); +} + +void PhoneAPI::releaseMqttClientProxyPhonePacket() { + if (mqttClientProxyMessageForPhone) { + service->releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone); + mqttClientProxyMessageForPhone = NULL; + } +} + +void PhoneAPI::releaseClientNotification() { + if (clientNotification) { + service->releaseClientNotificationToPool(clientNotification); + clientNotification = NULL; + } } /** * Return true if we have data available to send to the phone */ -bool PhoneAPI::available() -{ - switch (state) { - case STATE_SEND_NOTHING: - return false; - case STATE_SEND_MY_INFO: - case STATE_SEND_UIDATA: - case STATE_SEND_CHANNELS: - case STATE_SEND_CONFIG: - case STATE_SEND_MODULECONFIG: - case STATE_SEND_METADATA: - case STATE_SEND_OWN_NODEINFO: - case STATE_SEND_FILEMANIFEST: - case STATE_SEND_COMPLETE_ID: - return true; +bool PhoneAPI::available() { + switch (state) { + case STATE_SEND_NOTHING: + return false; + case STATE_SEND_MY_INFO: + case STATE_SEND_UIDATA: + case STATE_SEND_CHANNELS: + case STATE_SEND_CONFIG: + case STATE_SEND_MODULECONFIG: + case STATE_SEND_METADATA: + case STATE_SEND_OWN_NODEINFO: + case STATE_SEND_FILEMANIFEST: + case STATE_SEND_COMPLETE_ID: + return true; - case STATE_SEND_OTHER_NODEINFOS: { - concurrency::LockGuard guard(&nodeInfoMutex); - if (nodeInfoQueue.empty()) { - // Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it. - goto PREFETCH_NODEINFO; - } + case STATE_SEND_OTHER_NODEINFOS: { + concurrency::LockGuard guard(&nodeInfoMutex); + if (nodeInfoQueue.empty()) { + // Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it. + goto PREFETCH_NODEINFO; } - return true; // Always say we have something, because we might need to advance our state machine - PREFETCH_NODEINFO: - prefetchNodeInfos(); - return true; - case STATE_SEND_PACKETS: { - if (!queueStatusPacketForPhone) - queueStatusPacketForPhone = service->getQueueStatusForPhone(); - if (!mqttClientProxyMessageForPhone) - mqttClientProxyMessageForPhone = service->getMqttClientProxyMessageForPhone(); - if (!clientNotification) - clientNotification = service->getClientNotificationForPhone(); - bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone || !!clientNotification; - if (hasPacket) - return true; + } + return true; // Always say we have something, because we might need to advance our state machine + PREFETCH_NODEINFO: + prefetchNodeInfos(); + return true; + case STATE_SEND_PACKETS: { + if (!queueStatusPacketForPhone) + queueStatusPacketForPhone = service->getQueueStatusForPhone(); + if (!mqttClientProxyMessageForPhone) + mqttClientProxyMessageForPhone = service->getMqttClientProxyMessageForPhone(); + if (!clientNotification) + clientNotification = service->getClientNotificationForPhone(); + bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone || !!clientNotification; + if (hasPacket) + return true; #ifdef FSCom - if (xmodemPacketForPhone.control == meshtastic_XModem_Control_NUL) - xmodemPacketForPhone = xModem.getForPhone(); - if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { - xModem.resetForPhone(); - return true; - } + if (xmodemPacketForPhone.control == meshtastic_XModem_Control_NUL) + xmodemPacketForPhone = xModem.getForPhone(); + if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { + xModem.resetForPhone(); + return true; + } #endif #ifdef ARCH_ESP32 #if !MESHTASTIC_EXCLUDE_STOREFORWARD - // Check if StoreForward has packets stored for us. - if (!packetForPhone && storeForwardModule) - packetForPhone = storeForwardModule->getForPhone(); + // Check if StoreForward has packets stored for us. + if (!packetForPhone && storeForwardModule) + packetForPhone = storeForwardModule->getForPhone(); #endif #endif - if (!packetForPhone) - packetForPhone = service->getForPhone(); - hasPacket = !!packetForPhone; - return hasPacket; - } - default: - LOG_ERROR("PhoneAPI::available unexpected state %d", state); - } + if (!packetForPhone) + packetForPhone = service->getForPhone(); + hasPacket = !!packetForPhone; + return hasPacket; + } + default: + LOG_ERROR("PhoneAPI::available unexpected state %d", state); + } - return false; + return false; } -void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message) -{ - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->has_reply_id = true; - cn->reply_id = replyId; - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, message, sizeof(cn->message)); - service->sendClientNotification(cn); +void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message) { + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->has_reply_id = true; + cn->reply_id = replyId; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, message, sizeof(cn->message)); + service->sendClientNotification(cn); } -bool PhoneAPI::wasSeenRecently(uint32_t id) -{ - for (int i = 0; i < 20; i++) { - if (recentToRadioPacketIds[i] == id) { - return true; - } - if (recentToRadioPacketIds[i] == 0) { - recentToRadioPacketIds[i] = id; - return false; - } +bool PhoneAPI::wasSeenRecently(uint32_t id) { + for (int i = 0; i < 20; i++) { + if (recentToRadioPacketIds[i] == id) { + return true; } - // If the array is full, shift all elements to the left and add the new id at the end - memmove(recentToRadioPacketIds, recentToRadioPacketIds + 1, (19) * sizeof(uint32_t)); - recentToRadioPacketIds[19] = id; - return false; + if (recentToRadioPacketIds[i] == 0) { + recentToRadioPacketIds[i] = id; + return false; + } + } + // If the array is full, shift all elements to the left and add the new id at the end + memmove(recentToRadioPacketIds, recentToRadioPacketIds + 1, (19) * sizeof(uint32_t)); + recentToRadioPacketIds[19] = id; + return false; } /** * Handle a packet that the phone wants us to send. It is our responsibility to free the packet to the pool */ -bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) -{ - printPacket("PACKET FROM PHONE", &p); +bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) { + printPacket("PACKET FROM PHONE", &p); #if defined(ARCH_PORTDUINO) - // For use with the simulator, we should not ignore duplicate packets from the phone - if (SimRadio::instance == nullptr) + // For use with the simulator, we should not ignore duplicate packets from the phone + if (SimRadio::instance == nullptr) #endif - if (p.id > 0 && wasSeenRecently(p.id)) { - LOG_DEBUG("Ignore packet from phone, already seen recently"); - return false; - } - - if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] && - Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) { - LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds"); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - return false; - } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - return false; - } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, - meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP) && - lastPortNumToRadio[p.decoded.portnum] && - Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { - // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset - LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - // FIXME: Figure out why this continues to happen - // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); - return false; - } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && - Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { - LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p); - // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds"); - return false; + if (p.id > 0 && wasSeenRecently(p.id)) { + LOG_DEBUG("Ignore packet from phone, already seen recently"); + return false; } - // Upgrade traceroute requests from phone to use reliable delivery, matching TraceRouteModule - if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && !isBroadcast(p.to)) { - // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) - p.want_ack = true; - } + if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) { + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds"); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + return false; + } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_ALERT_APP, + meshtastic_PortNum_TELEMETRY_APP) && + lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { + // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + // FIXME: Figure out why this continues to happen + // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); + return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p); + // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 + // seconds"); + return false; + } - lastPortNumToRadio[p.decoded.portnum] = millis(); - service->handleToRadio(p); - return true; + // Upgrade traceroute requests from phone to use reliable delivery, matching TraceRouteModule + if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && !isBroadcast(p.to)) { + // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) + p.want_ack = true; + } + + lastPortNumToRadio[p.decoded.portnum] = millis(); + service->handleToRadio(p); + return true; } /// If the mesh service tells us fromNum has changed, tell the phone -int PhoneAPI::onNotify(uint32_t newValue) -{ - bool timeout = checkConnectionTimeout(); // a handy place to check if we've heard from the phone (since the BLE version - // doesn't call this from idle) +int PhoneAPI::onNotify(uint32_t newValue) { + bool timeout = checkConnectionTimeout(); // a handy place to check if we've heard from the phone (since the BLE + // version doesn't call this from idle) - if (state == STATE_SEND_PACKETS) { - LOG_INFO("Tell client we have new packets %u", newValue); - onNowHasData(newValue); - } else { - LOG_DEBUG("Client not yet interested in packets (state=%d)", state); - } + if (state == STATE_SEND_PACKETS) { + LOG_INFO("Tell client we have new packets %u", newValue); + onNowHasData(newValue); + } else { + LOG_DEBUG("Client not yet interested in packets (state=%d)", state); + } - return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one + return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one } diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 7f79b5792..9a6edf764 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -32,172 +32,171 @@ * Eventually there should be once instance of this class for each live connection (because it has a bit of state * for that connection) */ -class PhoneAPI - : public Observer // FIXME, we shouldn't be inheriting from Observer, instead use CallbackObserver as a member +class PhoneAPI : public Observer // FIXME, we shouldn't be inheriting from Observer, instead use + // CallbackObserver as a member { - enum State { - STATE_SEND_NOTHING, // Initial state, don't send anything until the client starts asking for config - STATE_SEND_UIDATA, // send stored data for device-ui - STATE_SEND_MY_INFO, // send our my info record - STATE_SEND_OWN_NODEINFO, - STATE_SEND_METADATA, - STATE_SEND_CHANNELS, // Send all channels - STATE_SEND_CONFIG, // Replacement for the old Radioconfig - STATE_SEND_MODULECONFIG, // Send Module specific config - STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client - STATE_SEND_FILEMANIFEST, // Send file manifest - STATE_SEND_COMPLETE_ID, - STATE_SEND_PACKETS // send packets or debug strings - }; + enum State { + STATE_SEND_NOTHING, // Initial state, don't send anything until the client starts asking for config + STATE_SEND_UIDATA, // send stored data for device-ui + STATE_SEND_MY_INFO, // send our my info record + STATE_SEND_OWN_NODEINFO, + STATE_SEND_METADATA, + STATE_SEND_CHANNELS, // Send all channels + STATE_SEND_CONFIG, // Replacement for the old Radioconfig + STATE_SEND_MODULECONFIG, // Send Module specific config + STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client + STATE_SEND_FILEMANIFEST, // Send file manifest + STATE_SEND_COMPLETE_ID, + STATE_SEND_PACKETS // send packets or debug strings + }; - State state = STATE_SEND_NOTHING; + State state = STATE_SEND_NOTHING; - uint8_t config_state = 0; + uint8_t config_state = 0; - // Hashmap of timestamps for last time we received a packet on the API per portnum - std::unordered_map lastPortNumToRadio; - uint32_t recentToRadioPacketIds[20]; // Last 20 ToRadio MeshPacket IDs we have seen + // Hashmap of timestamps for last time we received a packet on the API per portnum + std::unordered_map lastPortNumToRadio; + uint32_t recentToRadioPacketIds[20]; // Last 20 ToRadio MeshPacket IDs we have seen - /** - * Each packet sent to the phone has an incrementing count - */ - uint32_t fromRadioNum = 0; + /** + * Each packet sent to the phone has an incrementing count + */ + uint32_t fromRadioNum = 0; - /// We temporarily keep the packet here between the call to available and getFromRadio. We will free it after the phone - /// downloads it - meshtastic_MeshPacket *packetForPhone = NULL; + /// We temporarily keep the packet here between the call to available and getFromRadio. We will free it after the + /// phone downloads it + meshtastic_MeshPacket *packetForPhone = NULL; - // file transfer packets destined for phone. Push it to the queue then free it. - meshtastic_XModem xmodemPacketForPhone = meshtastic_XModem_init_zero; + // file transfer packets destined for phone. Push it to the queue then free it. + meshtastic_XModem xmodemPacketForPhone = meshtastic_XModem_init_zero; - // Keep QueueStatus packet just as packetForPhone - meshtastic_QueueStatus *queueStatusPacketForPhone = NULL; + // Keep QueueStatus packet just as packetForPhone + meshtastic_QueueStatus *queueStatusPacketForPhone = NULL; - // Keep MqttClientProxyMessage packet just as packetForPhone - meshtastic_MqttClientProxyMessage *mqttClientProxyMessageForPhone = NULL; + // Keep MqttClientProxyMessage packet just as packetForPhone + meshtastic_MqttClientProxyMessage *mqttClientProxyMessageForPhone = NULL; - // Keep ClientNotification packet just as packetForPhone - meshtastic_ClientNotification *clientNotification = NULL; + // Keep ClientNotification packet just as packetForPhone + meshtastic_ClientNotification *clientNotification = NULL; - /// We temporarily keep the nodeInfo here between the call to available and getFromRadio - meshtastic_NodeInfo nodeInfoForPhone = meshtastic_NodeInfo_init_default; - // Prefetched node info entries ready for immediate transmission to the phone. - std::deque nodeInfoQueue; - // Tunable size of the node info cache so we can keep BLE reads non-blocking. - static constexpr size_t kNodePrefetchDepth = 4; - // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. - concurrency::Lock nodeInfoMutex; + /// We temporarily keep the nodeInfo here between the call to available and getFromRadio + meshtastic_NodeInfo nodeInfoForPhone = meshtastic_NodeInfo_init_default; + // Prefetched node info entries ready for immediate transmission to the phone. + std::deque nodeInfoQueue; + // Tunable size of the node info cache so we can keep BLE reads non-blocking. + static constexpr size_t kNodePrefetchDepth = 4; + // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. + concurrency::Lock nodeInfoMutex; - meshtastic_ToRadio toRadioScratch = { - 0}; // this is a static scratch object, any data must be copied elsewhere before returning + meshtastic_ToRadio toRadioScratch = {0}; // this is a static scratch object, any data must be copied elsewhere before returning - /// Use to ensure that clients don't get confused about old messages from the radio - uint32_t config_nonce = 0; - uint32_t readIndex = 0; + /// Use to ensure that clients don't get confused about old messages from the radio + uint32_t config_nonce = 0; + uint32_t readIndex = 0; - std::vector filesManifest = {}; + std::vector filesManifest = {}; - void resetReadIndex() { readIndex = 0; } + void resetReadIndex() { readIndex = 0; } - public: - PhoneAPI(); +public: + PhoneAPI(); - /// Destructor - calls close() - virtual ~PhoneAPI(); + /// Destructor - calls close() + virtual ~PhoneAPI(); - // Call this when the client drops the connection, resets the state to STATE_SEND_NOTHING - // Unregisters our observer. A closed connection **can** be reopened by calling init again. - virtual void close(); + // Call this when the client drops the connection, resets the state to STATE_SEND_NOTHING + // Unregisters our observer. A closed connection **can** be reopened by calling init again. + virtual void close(); - /** - * Handle a ToRadio protobuf - * @return true true if a packet was queued for sending (so that caller can yield) - */ - virtual bool handleToRadio(const uint8_t *buf, size_t len); + /** + * Handle a ToRadio protobuf + * @return true true if a packet was queued for sending (so that caller can yield) + */ + virtual bool handleToRadio(const uint8_t *buf, size_t len); - /** - * Send a (client)notification to the phone - */ - virtual void sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message); + /** + * Send a (client)notification to the phone + */ + virtual void sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message); - /** - * Get the next packet we want to send to the phone - * - * We assume buf is at least FromRadio_size bytes long. - * Returns number of bytes in the FromRadio packet (or 0 if no packet available) - */ - size_t getFromRadio(uint8_t *buf); + /** + * Get the next packet we want to send to the phone + * + * We assume buf is at least FromRadio_size bytes long. + * Returns number of bytes in the FromRadio packet (or 0 if no packet available) + */ + size_t getFromRadio(uint8_t *buf); - void sendConfigComplete(); + void sendConfigComplete(); - /** - * Return true if we have data available to send to the phone - */ - bool available(); + /** + * Return true if we have data available to send to the phone + */ + bool available(); - bool isConnected() { return state != STATE_SEND_NOTHING; } - bool isSendingPackets() { return state == STATE_SEND_PACKETS; } + bool isConnected() { return state != STATE_SEND_NOTHING; } + bool isSendingPackets() { return state == STATE_SEND_PACKETS; } - protected: - /// Our fromradio packet while it is being assembled - meshtastic_FromRadio fromRadioScratch = {}; +protected: + /// Our fromradio packet while it is being assembled + meshtastic_FromRadio fromRadioScratch = {}; - /** the last msec we heard from the client on the other side of this link */ - uint32_t lastContactMsec = 0; + /** the last msec we heard from the client on the other side of this link */ + uint32_t lastContactMsec = 0; - /// Hookable to find out when connection changes - virtual void onConnectionChanged(bool connected) {} + /// Hookable to find out when connection changes + virtual void onConnectionChanged(bool connected) {} - /// If we haven't heard from the other side in a while then say not connected. Returns true if timeout occurred - bool checkConnectionTimeout(); + /// If we haven't heard from the other side in a while then say not connected. Returns true if timeout occurred + bool checkConnectionTimeout(); - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() = 0; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() = 0; - /** - * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) - */ - virtual void onNowHasData(uint32_t fromRadioNum) {} + /** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + virtual void onNowHasData(uint32_t fromRadioNum) {} - /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state - /// (i.e. BLE connection params) - virtual void onConfigStart() {} - virtual void onConfigComplete() {} + /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state + /// (i.e. BLE connection params) + virtual void onConfigStart() {} + virtual void onConfigComplete() {} - /// begin a new connection - void handleStartConfig(); + /// begin a new connection + void handleStartConfig(); - enum APIType { - TYPE_NONE, // Initial state, don't send anything until the client starts asking for config - TYPE_BLE, - TYPE_WIFI, - TYPE_SERIAL, - TYPE_PACKET, - TYPE_HTTP, - TYPE_ETH - }; + enum APIType { + TYPE_NONE, // Initial state, don't send anything until the client starts asking for config + TYPE_BLE, + TYPE_WIFI, + TYPE_SERIAL, + TYPE_PACKET, + TYPE_HTTP, + TYPE_ETH + }; - APIType api_type = TYPE_NONE; + APIType api_type = TYPE_NONE; - private: - void releasePhonePacket(); +private: + void releasePhonePacket(); - void releaseQueueStatusPhonePacket(); + void releaseQueueStatusPhonePacket(); - void prefetchNodeInfos(); + void prefetchNodeInfos(); - void releaseMqttClientProxyPhonePacket(); + void releaseMqttClientProxyPhonePacket(); - void releaseClientNotification(); + void releaseClientNotification(); - bool wasSeenRecently(uint32_t packetId); + bool wasSeenRecently(uint32_t packetId); - /** - * Handle a packet that the phone wants us to send. We can write to it but can not keep a reference to it - * @return true true if a packet was queued for sending - */ - bool handleToRadioPacket(meshtastic_MeshPacket &p); + /** + * Handle a packet that the phone wants us to send. We can write to it but can not keep a reference to it + * @return true true if a packet was queued for sending + */ + bool handleToRadioPacket(meshtastic_MeshPacket &p); - /// If the mesh service tells us fromNum has changed, tell the phone - virtual int onNotify(uint32_t newValue) override; + /// If the mesh service tells us fromNum has changed, tell the phone + virtual int onNotify(uint32_t newValue) override; }; diff --git a/src/mesh/PointerQueue.h b/src/mesh/PointerQueue.h index b45245eb8..14fce8aeb 100644 --- a/src/mesh/PointerQueue.h +++ b/src/mesh/PointerQueue.h @@ -5,26 +5,23 @@ /** * A wrapper for freertos queues that assumes each element is a pointer */ -template class PointerQueue : public TypedQueue -{ - public: - explicit PointerQueue(int maxElements) : TypedQueue(maxElements) {} +template class PointerQueue : public TypedQueue { +public: + explicit PointerQueue(int maxElements) : TypedQueue(maxElements) {} - // returns a ptr or null if the queue was empty - T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) - { - T *p; + // returns a ptr or null if the queue was empty + T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) { + T *p; - return this->dequeue(&p, maxWait) ? p : nullptr; - } + return this->dequeue(&p, maxWait) ? p : nullptr; + } #ifdef HAS_FREE_RTOS - // returns a ptr or null if the queue was empty - T *dequeuePtrFromISR(BaseType_t *higherPriWoken) - { - T *p; + // returns a ptr or null if the queue was empty + T *dequeuePtrFromISR(BaseType_t *higherPriWoken) { + T *p; - return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; - } + return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; + } #endif }; diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 725477eae..5a9f4038b 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -8,116 +8,108 @@ * If you are using protobufs to encode your packets (recommended) you can use this as a baseclass for your module * and avoid a bunch of boilerplate code. */ -template class ProtobufModule : protected SinglePortModule -{ - const pb_msgdesc_t *fields; +template class ProtobufModule : protected SinglePortModule { + const pb_msgdesc_t *fields; - public: - uint16_t numOnlineNodes = 0; - /** Constructor - * name is for debugging output - */ - ProtobufModule(const char *_name, meshtastic_PortNum _ourPortNum, const pb_msgdesc_t *_fields) - : SinglePortModule(_name, _ourPortNum), fields(_fields) - { +public: + uint16_t numOnlineNodes = 0; + /** Constructor + * name is for debugging output + */ + ProtobufModule(const char *_name, meshtastic_PortNum _ourPortNum, const pb_msgdesc_t *_fields) + : SinglePortModule(_name, _ourPortNum), fields(_fields) {} + +protected: + /** + * Handle a received message, the data field in the message is already decoded and is provided + * + * In general decoded will always be !NULL. But in some special applications (where you have handling packets + * for multiple port numbers, decoding will ONLY be attempted for packets where the portnum matches our expected + * ourPortNum. + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, T *decoded) = 0; + + /** Called to make changes to a particular incoming message + */ + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, T *decoded){}; + + /** + * Return a mesh packet which has been preinited with a particular protobuf data payload and port number. + * You can then send this packet (after customizing any of the payload fields you might need) with + * service->sendToMesh() + */ + meshtastic_MeshPacket *allocDataProtobuf(const T &payload) { + // Update our local node info with our position (even if we don't decide to update anyone else) + meshtastic_MeshPacket *p = allocDataPacket(); + + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), fields, &payload); + // LOG_DEBUG("did encode"); + return p; + } + + /** + * Gets the short name from the sender of the mesh packet + * Returns "???" if unknown sender + */ + const char *getSenderShortName(const meshtastic_MeshPacket &mp) { + auto node = nodeDB->getMeshNode(getFrom(&mp)); + const char *sender = (node) ? node->user.short_name : "???"; + return sender; + } + + int handleStatusUpdate(const meshtastic::Status *arg) { + if (arg->getStatusType() == STATUS_TYPE_NODE) { + numOnlineNodes = nodeStatus->getNumOnline(); + } + return 0; + } + +private: + /** Called to handle a particular incoming message + + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override { + // FIXME - we currently update position data in the DB only if the message was a broadcast or destined to us + // it would be better to update even if the message was destined to others. + + auto &p = mp.decoded; + LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); + + T scratch; + T *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding proto module!"); + // if we can't decode it, nobody can process it! + return ProcessMessage::STOP; + } } - protected: - /** - * Handle a received message, the data field in the message is already decoded and is provided - * - * In general decoded will always be !NULL. But in some special applications (where you have handling packets - * for multiple port numbers, decoding will ONLY be attempted for packets where the portnum matches our expected ourPortNum. - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, T *decoded) = 0; + return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; + } - /** Called to make changes to a particular incoming message - */ - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, T *decoded){}; + /** Called to alter a particular incoming message + */ + virtual void alterReceived(meshtastic_MeshPacket &mp) override { + T scratch; + T *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + const meshtastic_Data &p = mp.decoded; + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding proto module!"); + // if we can't decode it, nobody can process it! + return; + } - /** - * Return a mesh packet which has been preinited with a particular protobuf data payload and port number. - * You can then send this packet (after customizing any of the payload fields you might need) with - * service->sendToMesh() - */ - meshtastic_MeshPacket *allocDataProtobuf(const T &payload) - { - // Update our local node info with our position (even if we don't decide to update anyone else) - meshtastic_MeshPacket *p = allocDataPacket(); - - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), fields, &payload); - // LOG_DEBUG("did encode"); - return p; - } - - /** - * Gets the short name from the sender of the mesh packet - * Returns "???" if unknown sender - */ - const char *getSenderShortName(const meshtastic_MeshPacket &mp) - { - auto node = nodeDB->getMeshNode(getFrom(&mp)); - const char *sender = (node) ? node->user.short_name : "???"; - return sender; - } - - int handleStatusUpdate(const meshtastic::Status *arg) - { - if (arg->getStatusType() == STATUS_TYPE_NODE) { - numOnlineNodes = nodeStatus->getNumOnline(); - } - return 0; - } - - private: - /** Called to handle a particular incoming message - - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override - { - // FIXME - we currently update position data in the DB only if the message was a broadcast or destined to us - // it would be better to update even if the message was destined to others. - - auto &p = mp.decoded; - LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); - - T scratch; - T *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding proto module!"); - // if we can't decode it, nobody can process it! - return ProcessMessage::STOP; - } - } - - return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; - } - - /** Called to alter a particular incoming message - */ - virtual void alterReceived(meshtastic_MeshPacket &mp) override - { - T scratch; - T *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { - memset(&scratch, 0, sizeof(scratch)); - const meshtastic_Data &p = mp.decoded; - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding proto module!"); - // if we can't decode it, nobody can process it! - return; - } - - return alterReceivedProtobuf(mp, decoded); - } + return alterReceivedProtobuf(mp, decoded); } + } }; \ No newline at end of file diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index da0039d38..26c8a4c78 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -17,325 +17,307 @@ #endif // if we use 20 we are limited to 1% duty cycle or hw might overheat. For continuous operation set a limit of 17 -// In theory up to 27 dBm is possible, but the modules installed in most radios can cope with a max of 20. So BIG WARNING -// if you set power to something higher than 17 or 20 you might fry your board. +// In theory up to 27 dBm is possible, but the modules installed in most radios can cope with a max of 20. So BIG +// WARNING if you set power to something higher than 17 or 20 you might fry your board. #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) // Structure to hold DAC and DB values typedef struct { - uint8_t dac; - uint8_t db; + uint8_t dac; + uint8_t db; } DACDB; // Interpolation function -DACDB interpolate(uint8_t dbm, uint8_t dbm1, uint8_t dbm2, DACDB val1, DACDB val2) -{ - DACDB result; - double fraction = (double)(dbm - dbm1) / (dbm2 - dbm1); - result.dac = (uint8_t)(val1.dac + fraction * (val2.dac - val1.dac)); - result.db = (uint8_t)(val1.db + fraction * (val2.db - val1.db)); - return result; +DACDB interpolate(uint8_t dbm, uint8_t dbm1, uint8_t dbm2, DACDB val1, DACDB val2) { + DACDB result; + double fraction = (double)(dbm - dbm1) / (dbm2 - dbm1); + result.dac = (uint8_t)(val1.dac + fraction * (val2.dac - val1.dac)); + result.db = (uint8_t)(val1.db + fraction * (val2.db - val1.db)); + return result; } // Function to find the correct DAC and DB values based on dBm using interpolation -DACDB getDACandDB(uint8_t dbm) -{ - // Predefined values - static const struct { - uint8_t dbm; - DACDB values; - } +DACDB getDACandDB(uint8_t dbm) { + // Predefined values + static const struct { + uint8_t dbm; + DACDB values; + } #ifdef RADIOMASTER_900_BANDIT_NANO - dbmToDACDB[] = { - {20, {168, 2}}, // 100mW - {24, {148, 6}}, // 250mW - {27, {128, 9}}, // 500mW - {30, {90, 12}} // 1000mW - }; + dbmToDACDB[] = { + {20, {168, 2}}, // 100mW + {24, {148, 6}}, // 250mW + {27, {128, 9}}, // 500mW + {30, {90, 12}} // 1000mW + }; #endif #ifdef RADIOMASTER_900_BANDIT - dbmToDACDB[] = { - {20, {165, 2}}, // 100mW - {24, {155, 6}}, // 250mW - {27, {142, 9}}, // 500mW - {30, {110, 10}} // 1000mW - }; + dbmToDACDB[] = { + {20, {165, 2}}, // 100mW + {24, {155, 6}}, // 250mW + {27, {142, 9}}, // 500mW + {30, {110, 10}} // 1000mW + }; #endif - const int numValues = sizeof(dbmToDACDB) / sizeof(dbmToDACDB[0]); + const int numValues = sizeof(dbmToDACDB) / sizeof(dbmToDACDB[0]); - // Find the interval dbm falls within and interpolate - for (int i = 0; i < numValues - 1; i++) { - if (dbm >= dbmToDACDB[i].dbm && dbm <= dbmToDACDB[i + 1].dbm) { - return interpolate(dbm, dbmToDACDB[i].dbm, dbmToDACDB[i + 1].dbm, dbmToDACDB[i].values, dbmToDACDB[i + 1].values); - } + // Find the interval dbm falls within and interpolate + for (int i = 0; i < numValues - 1; i++) { + if (dbm >= dbmToDACDB[i].dbm && dbm <= dbmToDACDB[i + 1].dbm) { + return interpolate(dbm, dbmToDACDB[i].dbm, dbmToDACDB[i + 1].dbm, dbmToDACDB[i].values, dbmToDACDB[i + 1].values); } + } - // Return a default value if no match is found and default to 100mW + // Return a default value if no match is found and default to 100mW #ifdef RADIOMASTER_900_BANDIT_NANO - DACDB defaultValue = {168, 2}; + DACDB defaultValue = {168, 2}; #endif #ifdef RADIOMASTER_900_BANDIT - DACDB defaultValue = {165, 2}; + DACDB defaultValue = {165, 2}; #endif - return defaultValue; + return defaultValue; } #endif -RF95Interface::RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy) -{ - LOG_DEBUG("RF95Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); +RF95Interface::RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : RadioLibInterface(hal, cs, irq, rst, busy) { + LOG_DEBUG("RF95Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /** Some boards require GPIO control of tx vs rx paths */ -void RF95Interface::setTransmitEnable(bool txon) -{ +void RF95Interface::setTransmitEnable(bool txon) { #ifdef RF95_TXEN - digitalWrite(RF95_TXEN, txon ? 1 : 0); + digitalWrite(RF95_TXEN, txon ? 1 : 0); #elif ARCH_PORTDUINO - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); - } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); + } #endif #ifdef RF95_RXEN - digitalWrite(RF95_RXEN, txon ? 0 : 1); + digitalWrite(RF95_RXEN, txon ? 0 : 1); #elif ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); + } #endif } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -bool RF95Interface::init() -{ - RadioLibInterface::init(); +bool RF95Interface::init() { + RadioLibInterface::init(); #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) - // DAC and DB values based on dBm using interpolation - DACDB dacDbValues = getDACandDB(power); - int8_t powerDAC = dacDbValues.dac; - power = dacDbValues.db; + // DAC and DB values based on dBm using interpolation + DACDB dacDbValues = getDACandDB(power); + int8_t powerDAC = dacDbValues.dac; + power = dacDbValues.db; #endif - limitPower(RF95_MAX_POWER); + limitPower(RF95_MAX_POWER); - iface = lora = new RadioLibRF95(&module); + iface = lora = new RadioLibRF95(&module); #ifdef RF95_TCXO - pinMode(RF95_TCXO, OUTPUT); - digitalWrite(RF95_TCXO, 1); + pinMode(RF95_TCXO, OUTPUT); + digitalWrite(RF95_TCXO, 1); #endif - // enable PA + // enable PA #ifdef RF95_PA_EN #if defined(RF95_PA_DAC_EN) #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) - // Use calculated DAC value - dacWrite(RF95_PA_EN, powerDAC); + // Use calculated DAC value + dacWrite(RF95_PA_EN, powerDAC); #else - // Use Value set in /*/variant.h - dacWrite(RF95_PA_EN, RF95_PA_LEVEL); + // Use Value set in /*/variant.h + dacWrite(RF95_PA_EN, RF95_PA_LEVEL); #endif #endif #endif - /* - #define RF95_TXEN (22) // If defined, this pin should be set high prior to transmit (controls an external analog switch) - #define RF95_RXEN (23) // If defined, this pin should be set high prior to receive (controls an external analog switch) - */ + /* + #define RF95_TXEN (22) // If defined, this pin should be set high prior to transmit (controls an external analog + switch) #define RF95_RXEN (23) // If defined, this pin should be set high prior to receive (controls an external + analog switch) + */ #ifdef RF95_TXEN - pinMode(RF95_TXEN, OUTPUT); - digitalWrite(RF95_TXEN, 0); + pinMode(RF95_TXEN, OUTPUT); + digitalWrite(RF95_TXEN, 0); #endif #ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, 1); + pinMode(RF95_FAN_EN, OUTPUT); + digitalWrite(RF95_FAN_EN, 1); #endif #ifdef RF95_RXEN - pinMode(RF95_RXEN, OUTPUT); - digitalWrite(RF95_RXEN, 1); + pinMode(RF95_RXEN, OUTPUT); + digitalWrite(RF95_RXEN, 1); #endif #if ARCH_PORTDUINO - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_txen_pin.pin, 0); - } - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_rxen_pin.pin, 0); - } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, 0); + } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, 0); + } #endif - setTransmitEnable(false); + setTransmitEnable(false); - int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); - LOG_INFO("RF95 init result %d", res); - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); + LOG_INFO("RF95 init result %d", res); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) - LOG_INFO("DAC output set to %d", powerDAC); + LOG_INFO("DAC output set to %d", powerDAC); #endif - if (res == RADIOLIB_ERR_NONE) - res = lora->setCRC(RADIOLIB_SX126X_LORA_CRC_ON); + if (res == RADIOLIB_ERR_NONE) + res = lora->setCRC(RADIOLIB_SX126X_LORA_CRC_ON); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -void INTERRUPT_ATTR RF95Interface::disableInterrupt() -{ - lora->clearDio0Action(); -} +void INTERRUPT_ATTR RF95Interface::disableInterrupt() { lora->clearDio0Action(); } -bool RF95Interface::reconfigure() -{ - RadioLibInterface::reconfigure(); +bool RF95Interface::reconfigure() { + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora->setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora->setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora->setBandwidth(bw); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora->setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora->setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora->setSyncWord(syncWord); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 setSyncWord %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora->setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 setSyncWord %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora->setCurrentLimit(currentLimit); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 setCurrentLimit %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora->setCurrentLimit(currentLimit); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 setCurrentLimit %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora->setPreambleLength(preambleLength); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 setPreambleLength %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora->setPreambleLength(preambleLength); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 setPreambleLength %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora->setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora->setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > RF95_MAX_POWER) // This chip has lower power limits than some - power = RF95_MAX_POWER; + if (power > RF95_MAX_POWER) // This chip has lower power limits than some + power = RF95_MAX_POWER; #ifdef USE_RF95_RFO - err = lora->setOutputPower(power, true); + err = lora->setOutputPower(power, true); #else - err = lora->setOutputPower(power); + err = lora->setOutputPower(power); #endif - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } /** * Add SNR data to received messages */ -void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) -{ - mp->rx_snr = lora->getSNR(); - mp->rx_rssi = lround(lora->getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError()); +void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) { + mp->rx_snr = lora->getSNR(); + mp->rx_rssi = lround(lora->getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError()); } -void RF95Interface::setStandby() -{ - int err = lora->standby(); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 standby %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); +void RF95Interface::setStandby() { + int err = lora->standby(); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = false; // If we were receiving, not any more - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** We override to turn on transmitter power as needed. */ -void RF95Interface::configHardwareForSend() -{ - setTransmitEnable(true); +void RF95Interface::configHardwareForSend() { + setTransmitEnable(true); - RadioLibInterface::configHardwareForSend(); + RadioLibInterface::configHardwareForSend(); } -void RF95Interface::startReceive() -{ - setTransmitEnable(false); - setStandby(); - int err = lora->startReceive(); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 startReceive %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); +void RF95Interface::startReceive() { + setTransmitEnable(false); + setStandby(); + int err = lora->startReceive(); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 startReceive %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = true; + isReceiving = true; - // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); } -bool RF95Interface::isChannelActive() -{ - // check if we can detect a LoRa preamble on the current channel - int16_t result; - setTransmitEnable(false); - setStandby(); // needed for smooth transition - result = lora->scanChannel(); +bool RF95Interface::isChannelActive() { + // check if we can detect a LoRa preamble on the current channel + int16_t result; + setTransmitEnable(false); + setStandby(); // needed for smooth transition + result = lora->scanChannel(); - if (result == RADIOLIB_PREAMBLE_DETECTED) { - // LOG_DEBUG("Channel is busy!"); - return true; - } - if (result != RADIOLIB_CHANNEL_FREE) - LOG_ERROR("RF95 isChannelActive %s%d", radioLibErr, result); - assert(result != RADIOLIB_ERR_WRONG_MODEM); + if (result == RADIOLIB_PREAMBLE_DETECTED) { + // LOG_DEBUG("Channel is busy!"); + return true; + } + if (result != RADIOLIB_CHANNEL_FREE) + LOG_ERROR("RF95 isChannelActive %s%d", radioLibErr, result); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - // LOG_DEBUG("Channel is free!"); - return false; + // LOG_DEBUG("Channel is free!"); + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -bool RF95Interface::isActivelyReceiving() -{ - return lora->isReceiving(); -} +bool RF95Interface::isActivelyReceiving() { return lora->isReceiving(); } -bool RF95Interface::sleep() -{ - // put chipset into sleep mode - setStandby(); // First cancel any active receiving/sending - lora->sleep(); +bool RF95Interface::sleep() { + // put chipset into sleep mode + setStandby(); // First cancel any active receiving/sending + lora->sleep(); #ifdef RF95_FAN_EN - digitalWrite(RF95_FAN_EN, 0); + digitalWrite(RF95_FAN_EN, 0); #endif - return true; + return true; } #endif \ No newline at end of file diff --git a/src/mesh/RF95Interface.h b/src/mesh/RF95Interface.h index ffd8ae008..b63b9a441 100644 --- a/src/mesh/RF95Interface.h +++ b/src/mesh/RF95Interface.h @@ -7,68 +7,66 @@ /** * Our new not radiohead adapter for RF95 style radios */ -class RF95Interface : public RadioLibInterface -{ - RadioLibRF95 *lora = NULL; // Either a RFM95 or RFM96 depending on what was stuffed on this board +class RF95Interface : public RadioLibInterface { + RadioLibRF95 *lora = NULL; // Either a RFM95 or RFM96 depending on what was stuffed on this board - public: - RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +public: + RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - // TODO: Verify that this irq flag works with RFM95 / SX1276 radios the way it used to - bool isIRQPending() override { return lora->getIRQFlags() & RADIOLIB_SX127X_MASK_IRQ_FLAG_VALID_HEADER; } + // TODO: Verify that this irq flag works with RFM95 / SX1276 radios the way it used to + bool isIRQPending() override { return lora->getIRQFlags() & RADIOLIB_SX127X_MASK_IRQ_FLAG_VALID_HEADER; } - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - protected: - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; +protected: + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora->setDio0Action(callback, RISING); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora->setDio0Action(callback, RISING); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(*lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(*lora, pl, received); } - private: - /** Some boards require GPIO control of tx vs rx paths */ - void setTransmitEnable(bool txon); +private: + /** Some boards require GPIO control of tx vs rx paths */ + void setTransmitEnable(bool txon); }; #endif diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 5ee513e89..c326a3a68 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -13,16 +13,13 @@ #include // Calculate 2^n without calling pow() -uint32_t pow_of_2(uint32_t n) -{ - return 1 << n; -} +uint32_t pow_of_2(uint32_t n) { return 1 << n; } -#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ - { \ - meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \ - frequency_switching, wide_lora, #name \ - } +#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ + { \ + meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, \ + wide_lora, #name \ + } const RegionInfo regions[] = { /* @@ -171,8 +168,7 @@ const RegionInfo regions[] = { 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ - RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), - RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), + RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), /* Nepal @@ -205,19 +201,18 @@ bool RadioInterface::uses_default_frequency_slot = true; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; -void initRegion() -{ - const RegionInfo *r = regions; +void initRegion() { + const RegionInfo *r = regions; #ifdef REGULATORY_LORA_REGIONCODE - for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != REGULATORY_LORA_REGIONCODE; r++) - ; - LOG_INFO("Wanted region %d, regulatory override to %s", config.lora.region, r->name); + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != REGULATORY_LORA_REGIONCODE; r++) + ; + LOG_INFO("Wanted region %d, regulatory override to %s", config.lora.region, r->name); #else - for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != config.lora.region; r++) - ; - LOG_INFO("Wanted region %d, using %s", config.lora.region, r->name); + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != config.lora.region; r++) + ; + LOG_INFO("Wanted region %d, using %s", config.lora.region, r->name); #endif - myRegion = r; + myRegion = r; } /** @@ -231,186 +226,172 @@ The band is from 902 to 928 MHz. It mentions channel number and its respective c separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. */ -uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) -{ - uint32_t pl = 0; - if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { - pl = p->encrypted.size + sizeof(PacketHeader); - } else { - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - pl = numbytes + sizeof(PacketHeader); - } - return getPacketTime(pl, received); +uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) { + uint32_t pl = 0; + if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + pl = p->encrypted.size + sizeof(PacketHeader); + } else { + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + pl = numbytes + sizeof(PacketHeader); + } + return getPacketTime(pl, received); } /** The delay to use for retransmitting dropped packets */ -uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) -{ - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); - // Make sure enough time has elapsed for this packet to be sent and an ACK is received. - // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); - float channelUtil = airTime->channelUtilizationPercent(); - uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); - // Assuming we pick max. of CWsize and there will be a client with SNR at half the range - return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec + - PROCESSING_TIME_MSEC; +uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) { + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); + // Make sure enough time has elapsed for this packet to be sent and an ACK is received. + // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); + float channelUtil = airTime->channelUtilizationPercent(); + uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); + // Assuming we pick max. of CWsize and there will be a client with SNR at half the range + return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec + PROCESSING_TIME_MSEC; } /** The delay to use when we want to send something */ -uint32_t RadioInterface::getTxDelayMsec() -{ - /** We wait a random multiple of 'slotTimes' (see definition in header file) in order to avoid collisions. - The pool to take a random multiple from is the contention window (CW), which size depends on the - current channel utilization. */ - float channelUtil = airTime->channelUtilizationPercent(); - uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); - // LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize); - return random(0, pow_of_2(CWsize)) * slotTimeMsec; +uint32_t RadioInterface::getTxDelayMsec() { + /** We wait a random multiple of 'slotTimes' (see definition in header file) in order to avoid collisions. + The pool to take a random multiple from is the contention window (CW), which size depends on the + current channel utilization. */ + float channelUtil = airTime->channelUtilizationPercent(); + uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); + // LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize); + return random(0, pow_of_2(CWsize)) * slotTimeMsec; } /** The CW size to use when calculating SNR_based delays */ -uint8_t RadioInterface::getCWsize(float snr) -{ - // The minimum value for a LoRa SNR - const int32_t SNR_MIN = -20; +uint8_t RadioInterface::getCWsize(float snr) { + // The minimum value for a LoRa SNR + const int32_t SNR_MIN = -20; - // The maximum value for a LoRa SNR - const int32_t SNR_MAX = 10; + // The maximum value for a LoRa SNR + const int32_t SNR_MAX = 10; - return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); + return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); } /** The worst-case SNR_based packet delay */ -uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) -{ - uint8_t CWsize = getCWsize(snr); - // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) - return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; +uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) { + uint8_t CWsize = getCWsize(snr); + // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) + return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } /** Returns true if we should rebroadcast early like a ROUTER */ -bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) -{ - // If we are a ROUTER, we always rebroadcast early - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { - return true; - } +bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) { + // If we are a ROUTER, we always rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + return true; + } - return false; + return false; } /** The delay to use when we want to flood a message */ -uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) -{ - // high SNR = large CW size (Long Delay) - // low SNR = small CW size (Short Delay) - float snr = p->rx_snr; - uint32_t delay = 0; - uint8_t CWsize = getCWsize(snr); - // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); - if (shouldRebroadcastEarlyLikeRouter(p)) { - delay = random(0, 2 * CWsize) * slotTimeMsec; - LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); - } else { - // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) - delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec; - LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); - } +uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) { + // high SNR = large CW size (Long Delay) + // low SNR = small CW size (Short Delay) + float snr = p->rx_snr; + uint32_t delay = 0; + uint8_t CWsize = getCWsize(snr); + // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); + if (shouldRebroadcastEarlyLikeRouter(p)) { + delay = random(0, 2 * CWsize) * slotTimeMsec; + LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); + } else { + // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) + delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec; + LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); + } - return delay; + return delay; } -void printPacket(const char *prefix, const meshtastic_MeshPacket *p) -{ +void printPacket(const char *prefix, const meshtastic_MeshPacket *p) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - std::string out = - DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, - p->from, p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel); - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - auto &s = p->decoded; + std::string out = DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, p->from, + p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel); + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + auto &s = p->decoded; - out += DEBUG_PORT.mt_sprintf(" Portnum=%d", s.portnum); + out += DEBUG_PORT.mt_sprintf(" Portnum=%d", s.portnum); - if (s.want_response) - out += DEBUG_PORT.mt_sprintf(" WANTRESP"); + if (s.want_response) + out += DEBUG_PORT.mt_sprintf(" WANTRESP"); - if (p->pki_encrypted) - out += DEBUG_PORT.mt_sprintf(" PKI"); + if (p->pki_encrypted) + out += DEBUG_PORT.mt_sprintf(" PKI"); - if (s.source != 0) - out += DEBUG_PORT.mt_sprintf(" source=%08x", s.source); + if (s.source != 0) + out += DEBUG_PORT.mt_sprintf(" source=%08x", s.source); - if (s.dest != 0) - out += DEBUG_PORT.mt_sprintf(" dest=%08x", s.dest); + if (s.dest != 0) + out += DEBUG_PORT.mt_sprintf(" dest=%08x", s.dest); - if (s.request_id) - out += DEBUG_PORT.mt_sprintf(" requestId=%0x", s.request_id); + if (s.request_id) + out += DEBUG_PORT.mt_sprintf(" requestId=%0x", s.request_id); - /* now inside Data and therefore kinda opaque - if (s.which_ackVariant == SubPacket_success_id_tag) - out += DEBUG_PORT.mt_sprintf(" successId=%08x", s.ackVariant.success_id); - else if (s.which_ackVariant == SubPacket_fail_id_tag) - out += DEBUG_PORT.mt_sprintf(" failId=%08x", s.ackVariant.fail_id); */ - } else { - out += " encrypted"; - out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader)); - } + /* now inside Data and therefore kinda opaque + if (s.which_ackVariant == SubPacket_success_id_tag) + out += DEBUG_PORT.mt_sprintf(" successId=%08x", s.ackVariant.success_id); + else if (s.which_ackVariant == SubPacket_fail_id_tag) + out += DEBUG_PORT.mt_sprintf(" failId=%08x", s.ackVariant.fail_id); */ + } else { + out += " encrypted"; + out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader)); + } - if (p->rx_time != 0) - out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time); - if (p->rx_snr != 0.0) - out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr); - if (p->rx_rssi != 0) - out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi); - if (p->via_mqtt != 0) - out += DEBUG_PORT.mt_sprintf(" via MQTT"); - if (p->hop_start != 0) - out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start); - if (p->next_hop != 0) - out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop); - if (p->relay_node != 0) - out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node); - if (p->priority != 0) - out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); + if (p->rx_time != 0) + out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time); + if (p->rx_snr != 0.0) + out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr); + if (p->rx_rssi != 0) + out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi); + if (p->via_mqtt != 0) + out += DEBUG_PORT.mt_sprintf(" via MQTT"); + if (p->hop_start != 0) + out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start); + if (p->next_hop != 0) + out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop); + if (p->relay_node != 0) + out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node); + if (p->priority != 0) + out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); - out += ")"; - LOG_DEBUG("%s", out.c_str()); + out += ")"; + LOG_DEBUG("%s", out.c_str()); #endif } -RadioInterface::RadioInterface() -{ - assert(sizeof(PacketHeader) == MESHTASTIC_HEADER_LENGTH); // make sure the compiler did what we expected +RadioInterface::RadioInterface() { + assert(sizeof(PacketHeader) == MESHTASTIC_HEADER_LENGTH); // make sure the compiler did what we expected } -bool RadioInterface::reconfigure() -{ - applyModemConfig(); - return true; +bool RadioInterface::reconfigure() { + applyModemConfig(); + return true; } -bool RadioInterface::init() -{ - LOG_INFO("Start meshradio init"); +bool RadioInterface::init() { + LOG_INFO("Start meshradio init"); - configChangedObserver.observe(&service->configChanged); - preflightSleepObserver.observe(&preflightSleep); - notifyDeepSleepObserver.observe(¬ifyDeepSleep); + configChangedObserver.observe(&service->configChanged); + preflightSleepObserver.observe(&preflightSleep); + notifyDeepSleepObserver.observe(¬ifyDeepSleep); - // we now expect interfaces to operate in promiscuous mode - // radioIf.setThisAddress(nodeDB->getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at - // constructor time. + // we now expect interfaces to operate in promiscuous mode + // radioIf.setThisAddress(nodeDB->getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at + // constructor time. - applyModemConfig(); + applyModemConfig(); - return true; + return true; } -int RadioInterface::notifyDeepSleepCb(void *unused) -{ - sleep(); - return 0; +int RadioInterface::notifyDeepSleepCb(void *unused) { + sleep(); + return 0; } /** hash a string into an integer @@ -418,215 +399,198 @@ int RadioInterface::notifyDeepSleepCb(void *unused) * djb2 by Dan Bernstein. * http://www.cse.yorku.ca/~oz/hash.html */ -uint32_t hash(const char *str) -{ - uint32_t hash = 5381; - int c; +uint32_t hash(const char *str) { + uint32_t hash = 5381; + int c; - while ((c = *str++) != 0) - hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */ + while ((c = *str++) != 0) + hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */ - return hash; + return hash; } /** * Save our frequency for later reuse. */ -void RadioInterface::saveFreq(float freq) -{ - savedFreq = freq; -} +void RadioInterface::saveFreq(float freq) { savedFreq = freq; } /** * Save our channel for later reuse. */ -void RadioInterface::saveChannelNum(uint32_t channel_num) -{ - savedChannelNum = channel_num; -} +void RadioInterface::saveChannelNum(uint32_t channel_num) { savedChannelNum = channel_num; } /** * Save our frequency for later reuse. */ -float RadioInterface::getFreq() -{ - return savedFreq; -} +float RadioInterface::getFreq() { return savedFreq; } /** * Save our channel for later reuse. */ -uint32_t RadioInterface::getChannelNum() -{ - return savedChannelNum; -} +uint32_t RadioInterface::getChannelNum() { return savedChannelNum; } /** * Pull our channel settings etc... from protobufs to the dumb interface settings */ -void RadioInterface::applyModemConfig() -{ - // Set up default configuration - // No Sync Words in LORA mode - meshtastic_Config_LoRaConfig &loraConfig = config.lora; - bool validConfig = false; // We need to check for a valid configuration - while (!validConfig) { - if (loraConfig.use_preset) { +void RadioInterface::applyModemConfig() { + // Set up default configuration + // No Sync Words in LORA mode + meshtastic_Config_LoRaConfig &loraConfig = config.lora; + bool validConfig = false; // We need to check for a valid configuration + while (!validConfig) { + if (loraConfig.use_preset) { - switch (loraConfig.modem_preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 8; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 9; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 10; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 8; - sf = 11; - break; - default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 12; - break; - } - if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { - cr = loraConfig.coding_rate; - LOG_INFO("Using custom Coding Rate %u", cr); - } - } else { - sf = loraConfig.spread_factor; - cr = loraConfig.coding_rate; - bw = loraConfig.bandwidth; + switch (loraConfig.modem_preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 9; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 10; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + cr = 8; + sf = 11; + break; + default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + bw = (myRegion->wideLora) ? 406.25 : 125; + cr = 8; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + bw = (myRegion->wideLora) ? 406.25 : 125; + cr = 8; + sf = 12; + break; + } + if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { + cr = loraConfig.coding_rate; + LOG_INFO("Using custom Coding Rate %u", cr); + } + } else { + sf = loraConfig.spread_factor; + cr = loraConfig.coding_rate; + bw = loraConfig.bandwidth; - if (bw == 31) // This parameter is not an integer - bw = 31.25; - if (bw == 62) // Fix for 62.5Khz bandwidth - bw = 62.5; - if (bw == 200) - bw = 203.125; - if (bw == 400) - bw = 406.25; - if (bw == 800) - bw = 812.5; - if (bw == 1600) - bw = 1625.0; - } - - if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { - const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; - const float requestedBwKHz = bw; - const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset - const char *presetName = - DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); - - char err_string[160]; - if (isWideRequest) { - snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", - myRegion->name, presetName); - } else { - snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", - myRegion->name, regionSpanKHz, requestedBwKHz); - } - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - snprintf(cn->message, sizeof(cn->message), "%s", err_string); - service->sendClientNotification(cn); - - // Set to default modem preset - loraConfig.use_preset = true; - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - } else { - validConfig = true; - } + if (bw == 31) // This parameter is not an integer + bw = 31.25; + if (bw == 62) // Fix for 62.5Khz bandwidth + bw = 62.5; + if (bw == 200) + bw = 203.125; + if (bw == 400) + bw = 406.25; + if (bw == 800) + bw = 812.5; + if (bw == 1600) + bw = 1625.0; } - power = loraConfig.tx_power; + if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { + const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; + const float requestedBwKHz = bw; + const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset + const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); - if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) - power = myRegion->powerLimit; + char err_string[160]; + if (isWideRequest) { + snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", myRegion->name, + presetName); + } else { + snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", myRegion->name, + regionSpanKHz, requestedBwKHz); + } + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power == 0) - power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults - // to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this - // variable set to 0 don't have a valid power limit) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + snprintf(cn->message, sizeof(cn->message), "%s", err_string); + service->sendClientNotification(cn); - // Set final tx_power back onto config - loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger - - // Calculate the number of channels - uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000))); - - // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name - const char *channelName = channels.getName(channels.getPrimaryIndex()); - // channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1) - uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels; - - // Check if we use the default frequency slot - RadioInterface::uses_default_frequency_slot = - channel_num == - hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; - - // Old frequency selection formula - // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); - - // New frequency selection formula - float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000)); - - // override if we have a verbatim frequency - if (loraConfig.override_frequency) { - freq = loraConfig.override_frequency; - channel_num = -1; + // Set to default modem preset + loraConfig.use_preset = true; + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + } else { + validConfig = true; } + } - saveChannelNum(channel_num); - saveFreq(freq + loraConfig.frequency_offset); + power = loraConfig.tx_power; - slotTimeMsec = computeSlotTimeMsec(); - preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); + if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) + power = myRegion->powerLimit; - LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); - LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, - channel_num, power); - LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd, - myRegion->freqEnd - myRegion->freqStart); - LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw); - LOG_INFO("channel_num: %d", channel_num + 1); - LOG_INFO("frequency: %f", getFreq()); - LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); + if (power == 0) + power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults + // to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this + // variable set to 0 don't have a valid power limit) + + // Set final tx_power back onto config + loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger + + // Calculate the number of channels + uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000))); + + // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name + const char *channelName = channels.getName(channels.getPrimaryIndex()); + // channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1) + uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels; + + // Check if we use the default frequency slot + RadioInterface::uses_default_frequency_slot = + channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; + + // Old frequency selection formula + // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); + + // New frequency selection formula + float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000)); + + // override if we have a verbatim frequency + if (loraConfig.override_frequency) { + freq = loraConfig.override_frequency; + channel_num = -1; + } + + saveChannelNum(channel_num); + saveFreq(freq + loraConfig.frequency_offset); + + slotTimeMsec = computeSlotTimeMsec(); + preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); + + LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); + LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, channel_num, power); + LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd, + myRegion->freqEnd - myRegion->freqStart); + LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw); + LOG_INFO("channel_num: %d", channel_num + 1); + LOG_INFO("frequency: %f", getFreq()); + LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); } /** Slottime is the time to detect a transmission has started, consisting of: @@ -634,99 +598,93 @@ void RadioInterface::applyModemConfig() - roundtrip air propagation time (assuming max. 30km between nodes); - Tx/Rx turnaround time (maximum of SX126x and SX127x); - MAC processing time (measured on T-beam) */ -uint32_t RadioInterface::computeSlotTimeMsec() -{ - float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds - float symbolTime = pow_of_2(sf) / bw; // in milliseconds +uint32_t RadioInterface::computeSlotTimeMsec() { + float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds + float symbolTime = pow_of_2(sf) / bw; // in milliseconds - if (myRegion->wideLora) { - // CAD duration derived from AN1200.22 of SX1280 - return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime; - } else { - // CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol - return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime; - } + if (myRegion->wideLora) { + // CAD duration derived from AN1200.22 of SX1280 + return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime; + } else { + // CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol + return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime; + } } /** * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it */ -void RadioInterface::limitPower(int8_t loraMaxPower) -{ - uint8_t maxPower = 255; // No limit +void RadioInterface::limitPower(int8_t loraMaxPower) { + uint8_t maxPower = 255; // No limit - if (myRegion->powerLimit) - maxPower = myRegion->powerLimit; + if (myRegion->powerLimit) + maxPower = myRegion->powerLimit; - if ((power > maxPower) && !devicestate.owner.is_licensed) { - LOG_INFO("Lower transmit power because of regulatory limits"); - power = maxPower; - } + if ((power > maxPower) && !devicestate.owner.is_licensed) { + LOG_INFO("Lower transmit power because of regulatory limits"); + power = maxPower; + } #ifndef NUM_PA_POINTS - if (TX_GAIN_LORA > 0 && !devicestate.owner.is_licensed) { - LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA); - power -= TX_GAIN_LORA; - } + if (TX_GAIN_LORA > 0 && !devicestate.owner.is_licensed) { + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA); + power -= TX_GAIN_LORA; + } #else - if (!devicestate.owner.is_licensed) { - // we have an array of PA gain values. Find the highest power setting that works. - const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; - for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) { - if (((radio_dbm + tx_gain[radio_dbm]) > power) || - ((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { - // we've exceeded the power limit, or hit the max we can do - LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); - power -= tx_gain[radio_dbm]; - break; - } - } + if (!devicestate.owner.is_licensed) { + // we have an array of PA gain values. Find the highest power setting that works. + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; + for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) { + if (((radio_dbm + tx_gain[radio_dbm]) > power) || ((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { + // we've exceeded the power limit, or hit the max we can do + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); + power -= tx_gain[radio_dbm]; + break; + } } + } #endif - if (power > loraMaxPower) // Clamp power to maximum defined level - power = loraMaxPower; + if (power > loraMaxPower) // Clamp power to maximum defined level + power = loraMaxPower; - LOG_INFO("Final Tx power: %d dBm", power); + LOG_INFO("Final Tx power: %d dBm", power); } -void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) -{ - if (router) { - p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; - router->enqueueReceivedMessage(p); - } +void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) { + if (router) { + p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; + router->enqueueReceivedMessage(p); + } } /*** * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of payload bytes to send */ -size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) -{ - assert(!sendingPacket); +size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) { + assert(!sendingPacket); - // LOG_DEBUG("Send queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); - assert(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag); // It should have already been encoded by now + // LOG_DEBUG("Send queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); + assert(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag); // It should have already been encoded by now - radioBuffer.header.from = p->from; - radioBuffer.header.to = p->to; - radioBuffer.header.id = p->id; - radioBuffer.header.channel = p->channel; - radioBuffer.header.next_hop = p->next_hop; - radioBuffer.header.relay_node = p->relay_node; - if (p->hop_limit > HOP_MAX) { - LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE); - p->hop_limit = HOP_RELIABLE; - } - radioBuffer.header.flags = - p->hop_limit | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0); - radioBuffer.header.flags |= (p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK; + radioBuffer.header.from = p->from; + radioBuffer.header.to = p->to; + radioBuffer.header.id = p->id; + radioBuffer.header.channel = p->channel; + radioBuffer.header.next_hop = p->next_hop; + radioBuffer.header.relay_node = p->relay_node; + if (p->hop_limit > HOP_MAX) { + LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE); + p->hop_limit = HOP_RELIABLE; + } + radioBuffer.header.flags = p->hop_limit | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0); + radioBuffer.header.flags |= (p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK; - // if the sender nodenum is zero, that means uninitialized - assert(radioBuffer.header.from); - assert(p->encrypted.size <= sizeof(radioBuffer.payload)); - memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); + // if the sender nodenum is zero, that means uninitialized + assert(radioBuffer.header.from); + assert(p->encrypted.size <= sizeof(radioBuffer.payload)); + memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); - sendingPacket = p; - return p->encrypted.size + sizeof(PacketHeader); + sendingPacket = p; + return p->encrypted.size + sizeof(PacketHeader); } diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 6049a11cc..5e429580e 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -24,25 +24,25 @@ * with the old radiohead implementation. */ typedef struct { - NodeNum to, from; // can be 1 byte or four bytes + NodeNum to, from; // can be 1 byte or four bytes - PacketId id; // can be 1 byte or 4 bytes + PacketId id; // can be 1 byte or 4 bytes - /** - * Usage of flags: - * - * The bottom three bits of flags are use to store hop_limit when sent over the wire. - **/ - uint8_t flags; + /** + * Usage of flags: + * + * The bottom three bits of flags are use to store hop_limit when sent over the wire. + **/ + uint8_t flags; - /** The channel hash - used as a hint for the decoder to limit which channels we consider */ - uint8_t channel; + /** The channel hash - used as a hint for the decoder to limit which channels we consider */ + uint8_t channel; - // Last byte of the NodeNum of the next-hop for this packet - uint8_t next_hop; + // Last byte of the NodeNum of the next-hop for this packet + uint8_t next_hop; - // Last byte of the NodeNum of the node that will relay/relayed this packet - uint8_t relay_node; + // Last byte of the NodeNum of the node that will relay/relayed this packet + uint8_t relay_node; } PacketHeader; /** @@ -51,11 +51,11 @@ typedef struct { * It makes the use of its data easier, and avoids manipulating pointers (and potential non aligned accesses) */ typedef struct { - /** The header, as defined just before */ - PacketHeader header; + /** The header, as defined just before */ + PacketHeader header; - /** The payload, of maximum length minus the header, aligned just to be sure */ - uint8_t payload[MAX_LORA_PAYLOAD_LEN + 1 - sizeof(PacketHeader)] __attribute__((__aligned__)); + /** The payload, of maximum length minus the header, aligned just to be sure */ + uint8_t payload[MAX_LORA_PAYLOAD_LEN + 1 - sizeof(PacketHeader)] __attribute__((__aligned__)); } RadioBuffer; @@ -64,210 +64,204 @@ typedef struct { * * This defines the SOLE API for talking to radios (because soon we will have alternate radio implementations) */ -class RadioInterface -{ - friend class MeshRadio; // for debugging we let that class touch pool +class RadioInterface { + friend class MeshRadio; // for debugging we let that class touch pool - CallbackObserver configChangedObserver = - CallbackObserver(this, &RadioInterface::reloadConfig); + CallbackObserver configChangedObserver = CallbackObserver(this, &RadioInterface::reloadConfig); - CallbackObserver preflightSleepObserver = - CallbackObserver(this, &RadioInterface::preflightSleepCb); + CallbackObserver preflightSleepObserver = CallbackObserver(this, &RadioInterface::preflightSleepCb); - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &RadioInterface::notifyDeepSleepCb); + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &RadioInterface::notifyDeepSleepCb); - protected: - bool disabled = false; +protected: + bool disabled = false; - float bw = 125; - uint8_t sf = 9; - uint8_t cr = 5; + float bw = 125; + uint8_t sf = 9; + uint8_t cr = 5; - const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 - const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 - uint32_t slotTimeMsec = computeSlotTimeMsec(); - uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving - uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast - const uint32_t PROCESSING_TIME_MSEC = - 4500; // time to construct, process and construct a packet again (empirically determined) - const uint8_t CWmin = 3; // minimum CWsize - const uint8_t CWmax = 8; // maximum CWsize + const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 + const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 + uint32_t slotTimeMsec = computeSlotTimeMsec(); + uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast + const uint32_t PROCESSING_TIME_MSEC = 4500; // time to construct, process and construct a packet again (empirically determined) + const uint8_t CWmin = 3; // minimum CWsize + const uint8_t CWmax = 8; // maximum CWsize - meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending - uint32_t lastTxStart = 0L; + meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending + uint32_t lastTxStart = 0L; - uint32_t computeSlotTimeMsec(); + uint32_t computeSlotTimeMsec(); - /** - * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need - * */ - RadioBuffer radioBuffer __attribute__((__aligned__)); - /** - * Enqueue a received packet for the registered receiver - */ - void deliverToReceiver(meshtastic_MeshPacket *p); + /** + * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need + * */ + RadioBuffer radioBuffer __attribute__((__aligned__)); + /** + * Enqueue a received packet for the registered receiver + */ + void deliverToReceiver(meshtastic_MeshPacket *p); - public: - /** pool is the pool we will alloc our rx packets from - */ - RadioInterface(); +public: + /** pool is the pool we will alloc our rx packets from + */ + RadioInterface(); - virtual ~RadioInterface() {} + virtual ~RadioInterface() {} - /** - * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) - * - * This method must be used before putting the CPU into deep or light sleep. - */ - virtual bool canSleep() { return true; } + /** + * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) + * + * This method must be used before putting the CPU into deep or light sleep. + */ + virtual bool canSleep() { return true; } - virtual bool wideLora() { return false; } + virtual bool wideLora() { return false; } - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() { return true; } + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() { return true; } - /// Disable this interface (while disabled, no packets can be sent or received) - void disable() - { - disabled = true; - sleep(); - } + /// Disable this interface (while disabled, no packets can be sent or received) + void disable() { + disabled = true; + sleep(); + } - /** - * Send a packet (possibly by enquing in a private fifo). This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) = 0; + /** + * Send a packet (possibly by enquing in a private fifo). This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) = 0; - /** Return TX queue status */ - virtual meshtastic_QueueStatus getQueueStatus() - { - meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; - return qs; - } + /** Return TX queue status */ + virtual meshtastic_QueueStatus getQueueStatus() { + meshtastic_QueueStatus qs; + qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; + return qs; + } - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - virtual bool cancelSending(NodeNum from, PacketId id) { return false; } + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) { return false; } - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; } + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; } - // methods from radiohead + // methods from radiohead - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init(); + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init(); - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure(); + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure(); - /** The delay to use for retransmitting dropped packets */ - uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); + /** The delay to use for retransmitting dropped packets */ + uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); - /** The delay to use when we want to send something */ - uint32_t getTxDelayMsec(); + /** The delay to use when we want to send something */ + uint32_t getTxDelayMsec(); - /** The CW to use when calculating SNR_based delays */ - uint8_t getCWsize(float snr); + /** The CW to use when calculating SNR_based delays */ + uint8_t getCWsize(float snr); - /** The worst-case SNR_based packet delay */ - uint32_t getTxDelayMsecWeightedWorst(float snr); + /** The worst-case SNR_based packet delay */ + uint32_t getTxDelayMsecWeightedWorst(float snr); - /** Returns true if we should rebroadcast early like a ROUTER */ - bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + /** Returns true if we should rebroadcast early like a ROUTER */ + bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); - /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); + /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ + uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); - /** If the packet is not already in the late rebroadcast window, move it there */ - virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } + /** If the packet is not already in the late rebroadcast window, move it there */ + virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } - /** - * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version - * @return Whether a pending packet was removed - */ - virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; } + /** + * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better + * version + * @return Whether a pending packet was removed + */ + virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; } - /** - * Calculate airtime per - * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf - * section 4 - * - * @return num msecs for the packet - */ - uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); - virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; + /** + * Calculate airtime per + * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf + * section 4 + * + * @return num msecs for the packet + */ + uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); + virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; - /** - * Get the channel we saved. - */ - uint32_t getChannelNum(); + /** + * Get the channel we saved. + */ + uint32_t getChannelNum(); - /** - * Get the frequency we saved. - */ - virtual float getFreq(); + /** + * Get the frequency we saved. + */ + virtual float getFreq(); - /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers - virtual bool isIRQPending() { return false; } + /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers + virtual bool isIRQPending() { return false; } - // Whether we use the default frequency slot given our LoRa config (region and modem preset) - static bool uses_default_frequency_slot; + // Whether we use the default frequency slot given our LoRa config (region and modem preset) + static bool uses_default_frequency_slot; - protected: - int8_t power = 17; // Set by applyModemConfig() +protected: + int8_t power = 17; // Set by applyModemConfig() - float savedFreq; - uint32_t savedChannelNum; + float savedFreq; + uint32_t savedChannelNum; - /*** - * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the - * PacketHeader & payload). - * - * Used as the first step of - */ - size_t beginSending(meshtastic_MeshPacket *p); + /*** + * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the + * PacketHeader & payload). + * + * Used as the first step of + */ + size_t beginSending(meshtastic_MeshPacket *p); - /** - * Some regulatory regions limit xmit power. - * This function should be called by subclasses after setting their desired power. It might lower it - */ - void limitPower(int8_t MAX_POWER); + /** + * Some regulatory regions limit xmit power. + * This function should be called by subclasses after setting their desired power. It might lower it + */ + void limitPower(int8_t MAX_POWER); - /** - * Save the frequency we selected for later reuse. - */ - virtual void saveFreq(float savedFreq); + /** + * Save the frequency we selected for later reuse. + */ + virtual void saveFreq(float savedFreq); - /** - * Save the channel we selected for later reuse. - */ - virtual void saveChannelNum(uint32_t savedChannelNum); + /** + * Save the channel we selected for later reuse. + */ + virtual void saveChannelNum(uint32_t savedChannelNum); - private: - /** - * Convert our modemConfig enum into wf, sf, etc... - * - * These parameters will be pull from the channelSettings global - */ - void applyModemConfig(); +private: + /** + * Convert our modemConfig enum into wf, sf, etc... + * + * These parameters will be pull from the channelSettings global + */ + void applyModemConfig(); - /// Return 0 if sleep is okay - int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; } + /// Return 0 if sleep is okay + int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; } - int notifyDeepSleepCb(void *unused = NULL); + int notifyDeepSleepCb(void *unused = NULL); - int reloadConfig(void *unused) - { - reconfigure(); - return 0; - } + int reloadConfig(void *unused) { + reconfigure(); + return 0; + } }; /// Debug printing for packets diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 80e51b8bc..46afb1738 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -15,34 +15,28 @@ #include "PortduinoGlue.h" #include "meshUtils.h" #endif -void LockingArduinoHal::spiBeginTransaction() -{ - spiLock->lock(); +void LockingArduinoHal::spiBeginTransaction() { + spiLock->lock(); - ArduinoHal::spiBeginTransaction(); + ArduinoHal::spiBeginTransaction(); } -void LockingArduinoHal::spiEndTransaction() -{ - ArduinoHal::spiEndTransaction(); +void LockingArduinoHal::spiEndTransaction() { + ArduinoHal::spiEndTransaction(); - spiLock->unlock(); + spiLock->unlock(); } #if ARCH_PORTDUINO -void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) -{ - spi->transfer(out, in, len); -} +void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) { spi->transfer(out, in, len); } #endif RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *_iface) - : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) -{ - instance = this; + : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) { + instance = this; #if defined(ARCH_STM32WL) && defined(USE_SX1262) - module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); - module.setCb_digitalRead(stm32wl_emulate_digitalRead); + module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); + module.setCb_digitalRead(stm32wl_emulate_digitalRead); #endif } @@ -53,198 +47,179 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x) #endif -void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause) -{ - instance->disableInterrupt(); +void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause) { + instance->disableInterrupt(); - BaseType_t xHigherPriorityTaskWoken; - instance->notifyFromISR(&xHigherPriorityTaskWoken, cause, true); + BaseType_t xHigherPriorityTaskWoken; + instance->notifyFromISR(&xHigherPriorityTaskWoken, cause, true); - /* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE. - The macro used to do this is dependent on the port and may be called - portEND_SWITCHING_ISR. */ - YIELD_FROM_ISR(xHigherPriorityTaskWoken); + /* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE. + The macro used to do this is dependent on the port and may be called + portEND_SWITCHING_ISR. */ + YIELD_FROM_ISR(xHigherPriorityTaskWoken); } -void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() -{ - isrLevel0Common(ISR_RX); -} +void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() { isrLevel0Common(ISR_RX); } -void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() -{ - isrLevel0Common(ISR_TX); -} +void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() { isrLevel0Common(ISR_TX); } /** Our ISR code currently needs this to find our active instance */ RadioLibInterface *RadioLibInterface::instance; /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -bool RadioLibInterface::canSendImmediately() -{ - // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). - // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, - // we almost certainly guarantee no one outside will like the packet we are sending. - bool busyTx = sendingPacket != NULL; - bool busyRx = isReceiving && isActivelyReceiving(); +bool RadioLibInterface::canSendImmediately() { + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + bool busyTx = sendingPacket != NULL; + bool busyRx = isReceiving && isActivelyReceiving(); - if (busyTx || busyRx) { - if (busyTx) { - LOG_WARN("Can not send yet, busyTx"); - } - // If we've been trying to send the same packet more than one minute and we haven't gotten a - // TX IRQ from the radio, the radio is probably broken. - if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) { - LOG_ERROR("Hardware Failure! busyTx for more than 60s"); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED); - // reboot in 5 seconds when this condition occurs. - rebootAtMsec = lastTxStart + 65000; - } - if (busyRx) { - LOG_WARN("Can not send yet, busyRx"); - } - return false; - } else - return true; + if (busyTx || busyRx) { + if (busyTx) { + LOG_WARN("Can not send yet, busyTx"); + } + // If we've been trying to send the same packet more than one minute and we haven't gotten a + // TX IRQ from the radio, the radio is probably broken. + if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) { + LOG_ERROR("Hardware Failure! busyTx for more than 60s"); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED); + // reboot in 5 seconds when this condition occurs. + rebootAtMsec = lastTxStart + 65000; + } + if (busyRx) { + LOG_WARN("Can not send yet, busyRx"); + } + return false; + } else + return true; } -bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag) -{ - bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag)); - // Handle false detections - if (detected) { - if (!activeReceiveStart) { - activeReceiveStart = millis(); - } else if (!Throttle::isWithinTimespanMs(activeReceiveStart, 2 * preambleTimeMsec)) { - if (!(irq & syncWordHeaderValidFlag)) { - // The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag - activeReceiveStart = 0; - LOG_DEBUG("Ignore false preamble detection"); - return false; - } else { - uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); - if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) { - // We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag - activeReceiveStart = 0; - LOG_DEBUG("Ignore false header detection"); - return false; - } - } +bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag) { + bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag)); + // Handle false detections + if (detected) { + if (!activeReceiveStart) { + activeReceiveStart = millis(); + } else if (!Throttle::isWithinTimespanMs(activeReceiveStart, 2 * preambleTimeMsec)) { + if (!(irq & syncWordHeaderValidFlag)) { + // The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag + activeReceiveStart = 0; + LOG_DEBUG("Ignore false preamble detection"); + return false; + } else { + uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); + if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) { + // We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag + activeReceiveStart = 0; + LOG_DEBUG("Ignore false header detection"); + return false; } + } } - return detected; + } + return detected; } /// Send a packet (possibly by enquing in a private fifo). This routine will /// later free() the packet to pool. This routine is not allowed to stall because it is called from /// bluetooth comms code. If the txmit queue is empty it might return an error -ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p) -{ +ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p) { #ifndef DISABLE_WELCOME_UNSET - if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - if (disabled || !config.lora.tx_enabled) { - LOG_WARN("send - !config.lora.tx_enabled"); - packetPool.release(p); - return ERRNO_DISABLED; - } - - } else { - LOG_WARN("send - lora tx disabled: Region unset"); - packetPool.release(p); - return ERRNO_DISABLED; - } - -#else - + if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { if (disabled || !config.lora.tx_enabled) { - LOG_WARN("send - !config.lora.tx_enabled"); - packetPool.release(p); - return ERRNO_DISABLED; + LOG_WARN("send - !config.lora.tx_enabled"); + packetPool.release(p); + return ERRNO_DISABLED; } -#endif - - if (p->to == NODENUM_BROADCAST_NO_LORA) { - LOG_DEBUG("Drop no-LoRa pkt"); - return ERRNO_SHOULD_RELEASE; - } - - // Sometimes when testing it is useful to be able to never turn on the xmitter -#ifndef LORA_DISABLE_SENDING - printPacket("enqueue for send", p); - - LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); - bool dropped = false; - ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; - - if (dropped) { - txDrop++; - } - - if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks - packetPool.release(p); - return res; - } - - // set (random) transmit delay to let others reconfigure their radio, - // to avoid collisions and implement timing-based flooding - setTransmitDelay(); - - return res; -#else + } else { + LOG_WARN("send - lora tx disabled: Region unset"); packetPool.release(p); return ERRNO_DISABLED; + } + +#else + + if (disabled || !config.lora.tx_enabled) { + LOG_WARN("send - !config.lora.tx_enabled"); + packetPool.release(p); + return ERRNO_DISABLED; + } + +#endif + + if (p->to == NODENUM_BROADCAST_NO_LORA) { + LOG_DEBUG("Drop no-LoRa pkt"); + return ERRNO_SHOULD_RELEASE; + } + + // Sometimes when testing it is useful to be able to never turn on the xmitter +#ifndef LORA_DISABLE_SENDING + printPacket("enqueue for send", p); + + LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); + bool dropped = false; + ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; + + if (dropped) { + txDrop++; + } + + if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks + packetPool.release(p); + return res; + } + + // set (random) transmit delay to let others reconfigure their radio, + // to avoid collisions and implement timing-based flooding + setTransmitDelay(); + + return res; +#else + packetPool.release(p); + return ERRNO_DISABLED; #endif } -meshtastic_QueueStatus RadioLibInterface::getQueueStatus() -{ - meshtastic_QueueStatus qs; +meshtastic_QueueStatus RadioLibInterface::getQueueStatus() { + meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = 0; - qs.free = txQueue.getFree(); - qs.maxlen = txQueue.getMaxLen(); + qs.res = qs.mesh_packet_id = 0; + qs.free = txQueue.getFree(); + qs.maxlen = txQueue.getMaxLen(); - return qs; + return qs; } -bool RadioLibInterface::canSleep() -{ - bool res = txQueue.empty(); - if (!res) { // only print debug messages if we are vetoing sleep - LOG_DEBUG("Radio wait to sleep, txEmpty=%d", res); - } - return res; +bool RadioLibInterface::canSleep() { + bool res = txQueue.empty(); + if (!res) { // only print debug messages if we are vetoing sleep + LOG_DEBUG("Radio wait to sleep, txEmpty=%d", res); + } + return res; } /** Allow other firmware components to ask whether we are currently sending a packet Initially implemented to protect T-Echo's capacitive touch button from spurious presses during tx */ -bool RadioLibInterface::isSending() -{ - return sendingPacket != NULL; -} +bool RadioLibInterface::isSending() { return sendingPacket != NULL; } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ -bool RadioLibInterface::cancelSending(NodeNum from, PacketId id) -{ - auto p = txQueue.remove(from, id); - if (p) - packetPool.release(p); // free the packet we just removed +bool RadioLibInterface::cancelSending(NodeNum from, PacketId id) { + auto p = txQueue.remove(from, id); + if (p) + packetPool.release(p); // free the packet we just removed - bool result = (p != NULL); - LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); - return result; + bool result = (p != NULL); + LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); + return result; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ -bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) -{ - return txQueue.find(from, id); -} +bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } /** radio helper thread callback. We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of @@ -253,137 +228,132 @@ The CW size is determined by setTransmitDelay() and depends either on the curren of a flooding message. After this, we perform channel activity detection (CAD) and reset the transmit delay if it is currently active. */ -void RadioLibInterface::onNotify(uint32_t notification) -{ - switch (notification) { - case ISR_TX: - handleTransmitInterrupt(); - startReceive(); - setTransmitDelay(); - break; - case ISR_RX: - handleReceiveInterrupt(); - startReceive(); - setTransmitDelay(); - break; - case TRANSMIT_DELAY_COMPLETED: +void RadioLibInterface::onNotify(uint32_t notification) { + switch (notification) { + case ISR_TX: + handleTransmitInterrupt(); + startReceive(); + setTransmitDelay(); + break; + case ISR_RX: + handleReceiveInterrupt(); + startReceive(); + setTransmitDelay(); + break; + case TRANSMIT_DELAY_COMPLETED: - // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread - // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? - if (!txQueue.empty()) { - if (!canSendImmediately()) { - setTransmitDelay(); // currently Rx/Tx-ing: reset random delay - } else { - meshtastic_MeshPacket *txp = txQueue.getFront(); - assert(txp); - long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; - if (delay_remaining > 0) { - // There's still some delay pending on this packet, so resume waiting for it to elapse - notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); - } else { - if (isChannelActive()) { // check if there is currently a LoRa packet on the channel - startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again - setTransmitDelay(); - } else { - // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and - // actual transmission as short as possible - txp = txQueue.dequeue(); - assert(txp); - startSend(txp); - LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); - } - } - } + // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread + // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? + if (!txQueue.empty()) { + if (!canSendImmediately()) { + setTransmitDelay(); // currently Rx/Tx-ing: reset random delay + } else { + meshtastic_MeshPacket *txp = txQueue.getFront(); + assert(txp); + long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; + if (delay_remaining > 0) { + // There's still some delay pending on this packet, so resume waiting for it to elapse + notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); } else { - // Do nothing, because the queue is empty + if (isChannelActive()) { // check if there is currently a LoRa packet on the channel + startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again + setTransmitDelay(); + } else { + // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and + // actual transmission as short as possible + txp = txQueue.dequeue(); + assert(txp); + startSend(txp); + LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); + } } - break; - default: - assert(0); // We expected to receive a valid notification from the ISR - } -} - -void RadioLibInterface::setTransmitDelay() -{ - meshtastic_MeshPacket *p = txQueue.getFront(); - if (!p) { - return; // noop if there's nothing in the queue - } - - // We want all sending/receiving to be done by our daemon thread. - // We use a delay here because this packet might have been sent in response to a packet we just received. - // So we want to make sure the other side has had a chance to reconfigure its radio. - - if (p->tx_after) { - unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); - unsigned long now = millis(); - p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); - notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); - } else if (p->rx_snr == 0 && p->rx_rssi == 0) { - /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. - * This assumption is valid because of the offset generated by the radio to account for the noise - * floor. - */ - startTransmitTimer(true); + } } else { - // If there is a SNR, start a timer scaled based on that SNR. - LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p); + // Do nothing, because the queue is empty } + break; + default: + assert(0); // We expected to receive a valid notification from the ISR + } } -void RadioLibInterface::startTransmitTimer(bool withDelay) -{ - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable - } +void RadioLibInterface::setTransmitDelay() { + meshtastic_MeshPacket *p = txQueue.getFront(); + if (!p) { + return; // noop if there's nothing in the queue + } + + // We want all sending/receiving to be done by our daemon thread. + // We use a delay here because this packet might have been sent in response to a packet we just received. + // So we want to make sure the other side has had a chance to reconfigure its radio. + + if (p->tx_after) { + unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); + unsigned long now = millis(); + p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); + notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); + } else if (p->rx_snr == 0 && p->rx_rssi == 0) { + /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. + * This assumption is valid because of the offset generated by the radio to account for the noise + * floor. + */ + startTransmitTimer(true); + } else { + // If there is a SNR, start a timer scaled based on that SNR. + LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); + startTransmitTimerRebroadcast(p); + } } -void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) -{ - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delay = getTxDelayMsecWeighted(p); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable - } +void RadioLibInterface::startTransmitTimer(bool withDelay) { + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + } +} + +void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delay = getTxDelayMsecWeighted(p); + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + } } /** * If the packet is not already in the late rebroadcast window, move it there */ -void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) -{ - // Look for non-late packets only, so we don't do this twice! - meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); - if (p) { - p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); - bool dropped = false; - if (txQueue.enqueue(p, &dropped)) { - LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); - } else { - packetPool.release(p); - } - if (dropped) { - txDrop++; - } +void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) { + // Look for non-late packets only, so we don't do this twice! + meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); + if (p) { + p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); + bool dropped = false; + if (txQueue.enqueue(p, &dropped)) { + LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); + } else { + packetPool.release(p); } + if (dropped) { + txDrop++; + } + } } /** - * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version + * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better + * version * @return Whether a pending packet was removed */ -bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) -{ - meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt); - if (p) { - LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit); - packetPool.release(p); - return true; - } - return false; +bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { + meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt); + if (p) { + LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit); + packetPool.release(p); + return true; + } + return false; } /** @@ -391,176 +361,166 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_ */ // void RadioLibInterface::removePending -void RadioLibInterface::handleTransmitInterrupt() -{ - // This can be null if we forced the device to enter standby mode. In that case - // ignore the transmit interrupt - if (sendingPacket) - completeSending(); - powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now +void RadioLibInterface::handleTransmitInterrupt() { + // This can be null if we forced the device to enter standby mode. In that case + // ignore the transmit interrupt + if (sendingPacket) + completeSending(); + powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now } -void RadioLibInterface::completeSending() -{ - // We are careful to clear sending packet before calling printPacket because - // that can take a long time - auto p = sendingPacket; - sendingPacket = NULL; +void RadioLibInterface::completeSending() { + // We are careful to clear sending packet before calling printPacket because + // that can take a long time + auto p = sendingPacket; + sendingPacket = NULL; - if (p) { - // Packet has been sent, count it toward our TX airtime utilization. - uint32_t xmitMsec = getPacketTime(p); - airTime->logAirtime(TX_LOG, xmitMsec); + if (p) { + // Packet has been sent, count it toward our TX airtime utilization. + uint32_t xmitMsec = getPacketTime(p); + airTime->logAirtime(TX_LOG, xmitMsec); - txGood++; - if (!isFromUs(p)) - txRelay++; - printPacket("Completed sending", p); + txGood++; + if (!isFromUs(p)) + txRelay++; + printPacket("Completed sending", p); - // We are done sending that packet, release it - packetPool.release(p); - } + // We are done sending that packet, release it + packetPool.release(p); + } } -void RadioLibInterface::handleReceiveInterrupt() -{ - // when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race - // Condition? - if (!isReceiving) { - LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen"); - return; - } +void RadioLibInterface::handleReceiveInterrupt() { + // when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race + // Condition? + if (!isReceiving) { + LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen"); + return; + } - isReceiving = false; + isReceiving = false; - // read the number of actually received bytes - size_t length = iface->getPacketLength(); + // read the number of actually received bytes + size_t length = iface->getPacketLength(); - uint32_t rxMsec = getPacketTime(length, true); + uint32_t rxMsec = getPacketTime(length, true); #ifndef DISABLE_WELCOME_UNSET - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - LOG_WARN("lora rx disabled: Region unset"); - airTime->logAirtime(RX_ALL_LOG, rxMsec); - return; - } + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + LOG_WARN("lora rx disabled: Region unset"); + airTime->logAirtime(RX_ALL_LOG, rxMsec); + return; + } #endif - int state = iface->readData((uint8_t *)&radioBuffer, length); + int state = iface->readData((uint8_t *)&radioBuffer, length); #if ARCH_PORTDUINO - if (portduino_config.logoutputlevel == level_trace) { - printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); - } + if (portduino_config.logoutputlevel == level_trace) { + printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); + } #endif - if (state != RADIOLIB_ERR_NONE) { - LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, - radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags); - rxBad++; + if (state != RADIOLIB_ERR_NONE) { + LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, radioBuffer.header.to, + radioBuffer.header.from, radioBuffer.header.flags); + rxBad++; - airTime->logAirtime(RX_ALL_LOG, rxMsec); + airTime->logAirtime(RX_ALL_LOG, rxMsec); + } else { + // Skip the 4 headers that are at the beginning of the rxBuf + int32_t payloadLen = length - sizeof(PacketHeader); + + // check for short packets + if (payloadLen < 0) { + LOG_WARN("Ignore received packet too short"); + rxBad++; + airTime->logAirtime(RX_ALL_LOG, rxMsec); } else { - // Skip the 4 headers that are at the beginning of the rxBuf - int32_t payloadLen = length - sizeof(PacketHeader); + rxGood++; + // altered packet with "from == 0" can do Remote Node Administration without permission + if (radioBuffer.header.from == 0) { + LOG_WARN("Ignore received packet without sender"); + return; + } - // check for short packets - if (payloadLen < 0) { - LOG_WARN("Ignore received packet too short"); - rxBad++; - airTime->logAirtime(RX_ALL_LOG, rxMsec); - } else { - rxGood++; - // altered packet with "from == 0" can do Remote Node Administration without permission - if (radioBuffer.header.from == 0) { - LOG_WARN("Ignore received packet without sender"); - return; - } + // Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous). + // This allows the router and other apps on our node to sniff packets (usually routing) between other + // nodes. + meshtastic_MeshPacket *mp = packetPool.allocZeroed(); - // Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous). - // This allows the router and other apps on our node to sniff packets (usually routing) between other - // nodes. - meshtastic_MeshPacket *mp = packetPool.allocZeroed(); + // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto + mp->from = radioBuffer.header.from; + mp->to = radioBuffer.header.to; + mp->id = radioBuffer.header.id; + mp->channel = radioBuffer.header.channel; + assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code + mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; + mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; + mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); + mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); + // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) + mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; + mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; - // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto - mp->from = radioBuffer.header.from; - mp->to = radioBuffer.header.to; - mp->id = radioBuffer.header.id; - mp->channel = radioBuffer.header.channel; - assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code - mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; - mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; - mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); - mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); - // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) - mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; - mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; + addReceiveMetadata(mp); - addReceiveMetadata(mp); + mp->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point + assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes)); + memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen); + mp->encrypted.size = payloadLen; - mp->which_payload_variant = - meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point - assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes)); - memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen); - mp->encrypted.size = payloadLen; + printPacket("Lora RX", mp); - printPacket("Lora RX", mp); + airTime->logAirtime(RX_LOG, rxMsec); - airTime->logAirtime(RX_LOG, rxMsec); - - deliverToReceiver(mp); - } + deliverToReceiver(mp); } + } } -void RadioLibInterface::startReceive() -{ - isReceiving = true; - powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); +void RadioLibInterface::startReceive() { + isReceiving = true; + powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } -void RadioLibInterface::configHardwareForSend() -{ - powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); -} +void RadioLibInterface::configHardwareForSend() { powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); } -void RadioLibInterface::setStandby() -{ - // neither sending nor receiving - powerMon->clearState(meshtastic_PowerMon_State_Lora_RXOn); - powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); +void RadioLibInterface::setStandby() { + // neither sending nor receiving + powerMon->clearState(meshtastic_PowerMon_State_Lora_RXOn); + powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); } /** start an immediate transmit */ -bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) -{ - /* NOTE: Minimize the actions before startTransmit() to keep the time between - channel scan and actual transmit as low as possible to avoid collisions. */ - if (disabled || !config.lora.tx_enabled) { - LOG_WARN("Drop Tx packet because LoRa Tx disabled"); - packetPool.release(txp); - return false; +bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) { + /* NOTE: Minimize the actions before startTransmit() to keep the time between + channel scan and actual transmit as low as possible to avoid collisions. */ + if (disabled || !config.lora.tx_enabled) { + LOG_WARN("Drop Tx packet because LoRa Tx disabled"); + packetPool.release(txp); + return false; + } else { + configHardwareForSend(); // must be after setStandby + + size_t numbytes = beginSending(txp); + + int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes); + if (res != RADIOLIB_ERR_NONE) { + LOG_ERROR("startTransmit failed, error=%d", res); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG); + + // This send failed, but make sure to 'complete' it properly + completeSending(); + powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now + startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode) } else { - configHardwareForSend(); // must be after setStandby - - size_t numbytes = beginSending(txp); - - int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes); - if (res != RADIOLIB_ERR_NONE) { - LOG_ERROR("startTransmit failed, error=%d", res); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG); - - // This send failed, but make sure to 'complete' it properly - completeSending(); - powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now - startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode) - } else { - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register - // bits - enableInterrupt(isrTxLevel0); - lastTxStart = millis(); - printPacket("Started Tx", txp); - } - - return res == RADIOLIB_ERR_NONE; + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register + // bits + enableInterrupt(isrTxLevel0); + lastTxStart = millis(); + printPacket("Started Tx", txp); } + + return res == RADIOLIB_ERR_NONE; + } } \ No newline at end of file diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 833c88710..7b1802d0f 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -22,15 +22,14 @@ /** * We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access */ -class LockingArduinoHal : public ArduinoHal -{ - public: - LockingArduinoHal(SPIClass &spi, SPISettings spiSettings) : ArduinoHal(spi, spiSettings){}; +class LockingArduinoHal : public ArduinoHal { +public: + LockingArduinoHal(SPIClass &spi, SPISettings spiSettings) : ArduinoHal(spi, spiSettings){}; - void spiBeginTransaction() override; - void spiEndTransaction() override; + void spiBeginTransaction() override; + void spiEndTransaction() override; #if ARCH_PORTDUINO - void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override; + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override; #endif }; @@ -39,229 +38,225 @@ class LockingArduinoHal : public ArduinoHal /** * A wrapper for the RadioLib STM32WLx_Module class, that doesn't connect any pins as they are virtual */ -class STM32WLx_ModuleWrapper : public STM32WLx_Module -{ - public: - STM32WLx_ModuleWrapper(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : STM32WLx_Module(){}; +class STM32WLx_ModuleWrapper : public STM32WLx_Module { +public: + STM32WLx_ModuleWrapper(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : STM32WLx_Module(){}; }; #endif -class RadioLibInterface : public RadioInterface, protected concurrency::NotifiedWorkerThread -{ - /// Used as our notification from the ISR - enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; +class RadioLibInterface : public RadioInterface, protected concurrency::NotifiedWorkerThread { + /// Used as our notification from the ISR + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; - /** - * Raw ISR handler that just calls our polymorphic method - */ - static void isrTxLevel0(), isrLevel0Common(PendingISR code); + /** + * Raw ISR handler that just calls our polymorphic method + */ + static void isrTxLevel0(), isrLevel0Common(PendingISR code); - MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); - protected: - ModemType_t modemType = RADIOLIB_MODEM_LORA; - DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } - PacketConfig_t getPacketConfig() const - { - return {.lora = {.preambleLength = preambleLength, - .implicitHeader = false, - .crcEnabled = true, - // We use auto LDRO, meaning it is enabled if the symbol time is >= 16msec - .ldrOptimize = (1 << sf) / bw >= 16}}; - } +protected: + ModemType_t modemType = RADIOLIB_MODEM_LORA; + DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } + PacketConfig_t getPacketConfig() const { + return {.lora = {.preambleLength = preambleLength, + .implicitHeader = false, + .crcEnabled = true, + // We use auto LDRO, meaning it is enabled if the symbol time is >= 16msec + .ldrOptimize = (1 << sf) / bw >= 16}}; + } - /** - * We use a meshtastic sync word, but hashed with the Channel name. For releases before 1.2 we used 0x12 (or for very old - * loads 0x14) Note: do not use 0x34 - that is reserved for lorawan - * - * We now use 0x2b (so that someday we can possibly use NOT 2b - because that would be funny pun). We will be staying with - * this code for a long time. - */ - const uint8_t syncWord = 0x2b; + /** + * We use a meshtastic sync word, but hashed with the Channel name. For releases before 1.2 we used 0x12 (or for very + * old loads 0x14) Note: do not use 0x34 - that is reserved for lorawan + * + * We now use 0x2b (so that someday we can possibly use NOT 2b - because that would be funny pun). We will be staying + * with this code for a long time. + */ + const uint8_t syncWord = 0x2b; - float currentLimit = 100; // 100mA OCP - Should be acceptable for RFM95/SX127x chipset. + float currentLimit = 100; // 100mA OCP - Should be acceptable for RFM95/SX127x chipset. #if !defined(USE_STM32WLx) - Module module; // The HW interface to the radio + Module module; // The HW interface to the radio #else - STM32WLx_ModuleWrapper module; + STM32WLx_ModuleWrapper module; #endif - /** - * provides lowest common denominator RadioLib API - */ - PhysicalLayer *iface; + /** + * provides lowest common denominator RadioLib API + */ + PhysicalLayer *iface; - /// are _trying_ to receive a packet currently (note - we might just be waiting for one) - bool isReceiving = false; + /// are _trying_ to receive a packet currently (note - we might just be waiting for one) + bool isReceiving = false; - public: - /** Our ISR code currently needs this to find our active instance - */ - static RadioLibInterface *instance; +public: + /** Our ISR code currently needs this to find our active instance + */ + static RadioLibInterface *instance; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() = 0; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() = 0; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*)()) = 0; + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*)()) = 0; - /** - * Debugging counts - */ - uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; - uint16_t txDrop = 0; + /** + * Debugging counts + */ + uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; + uint16_t txDrop = 0; - public: - RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL); +public: + RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, + PhysicalLayer *iface = NULL); - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - /** - * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) - * - * This method must be used before putting the CPU into deep or light sleep. - */ - virtual bool canSleep() override; + /** + * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) + * + * This method must be used before putting the CPU into deep or light sleep. + */ + virtual bool canSleep() override; - /** - * Start waiting to receive a message - * - * External functions can call this method to wake the device from sleep. - * Subclasses must override and call this base method - */ - virtual void startReceive(); + /** + * Start waiting to receive a message + * + * External functions can call this method to wake the device from sleep. + * Subclasses must override and call this base method + */ + virtual void startReceive(); - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() = 0; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() = 0; - /** are we actively receiving a packet (only called during receiving state) - * This method is only public to facilitate debugging. Do not call. - */ - virtual bool isActivelyReceiving() = 0; + /** are we actively receiving a packet (only called during receiving state) + * This method is only public to facilitate debugging. Do not call. + */ + virtual bool isActivelyReceiving() = 0; - /** Are we are currently sending a packet? - * This method is public, intending to expose this information to other firmware components - */ - virtual bool isSending(); + /** Are we are currently sending a packet? + * This method is public, intending to expose this information to other firmware components + */ + virtual bool isSending(); - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - virtual bool cancelSending(NodeNum from, PacketId id) override; + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) override; - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - virtual bool findInTxQueue(NodeNum from, PacketId id) override; + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) override; - private: - /** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually - * doing the transmit */ - void setTransmitDelay(); +private: + /** if we have something waiting to send, start a short (random) timer so we can come check for collision before + * actually doing the transmit */ + void setTransmitDelay(); - /** - * random timer with certain min. and max. settings - * @return Timestamp after which the packet may be sent - */ - void startTransmitTimer(bool withDelay = true); + /** + * random timer with certain min. and max. settings + * @return Timestamp after which the packet may be sent + */ + void startTransmitTimer(bool withDelay = true); - /** - * timer scaled to SNR of to be flooded packet - * @return Timestamp after which the packet may be sent - */ - void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); + /** + * timer scaled to SNR of to be flooded packet + * @return Timestamp after which the packet may be sent + */ + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); - void handleTransmitInterrupt(); - void handleReceiveInterrupt(); + void handleTransmitInterrupt(); + void handleReceiveInterrupt(); - static void timerCallback(void *p1, uint32_t p2); + static void timerCallback(void *p1, uint32_t p2); - virtual void onNotify(uint32_t notification) override; + virtual void onNotify(uint32_t notification) override; - /** start an immediate transmit - * This method is virtual so subclasses can hook as needed, subclasses should not call directly - * @return true if packet was sent - */ - virtual bool startSend(meshtastic_MeshPacket *txp); + /** start an immediate transmit + * This method is virtual so subclasses can hook as needed, subclasses should not call directly + * @return true if packet was sent + */ + virtual bool startSend(meshtastic_MeshPacket *txp); - meshtastic_QueueStatus getQueueStatus(); + meshtastic_QueueStatus getQueueStatus(); - protected: - uint32_t activeReceiveStart = 0; +protected: + uint32_t activeReceiveStart = 0; - bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag); + bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag); - /** Do any hardware setup needed on entry into send configuration for the radio. - * Subclasses can customize, but must also call this base method */ - virtual void configHardwareForSend(); + /** Do any hardware setup needed on entry into send configuration for the radio. + * Subclasses can customize, but must also call this base method */ + virtual void configHardwareForSend(); - /** Could we send right now (i.e. either not actively receiving or transmitting)? */ - virtual bool canSendImmediately(); + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately(); - /** - * Raw ISR handler that just calls our polymorphic method - */ - static void isrRxLevel0(); + /** + * Raw ISR handler that just calls our polymorphic method + */ + static void isrRxLevel0(); - /** - * If a send was in progress finish it and return the buffer to the pool */ - void completeSending(); + /** + * If a send was in progress finish it and return the buffer to the pool */ + void completeSending(); - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) = 0; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) = 0; - /** - * Subclasses must override, implement and then call into this base class implementation - */ - virtual void setStandby(); + /** + * Subclasses must override, implement and then call into this base class implementation + */ + virtual void setStandby(); - /** - * Derive packet time either for a received (using header info) or a transmitted packet - */ - template uint32_t computePacketTime(T &lora, uint32_t pl, bool received) - { - if (received) { - // First get the actual coding rate and CRC status from the received packet - uint8_t rxCR; - bool hasCRC; - lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC); - // Go from raw header value to denominator - if (rxCR < 5) { - rxCR += 4; - } else if (rxCR == 7) { - rxCR = 8; - } + /** + * Derive packet time either for a received (using header info) or a transmitted packet + */ + template uint32_t computePacketTime(T &lora, uint32_t pl, bool received) { + if (received) { + // First get the actual coding rate and CRC status from the received packet + uint8_t rxCR; + bool hasCRC; + lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC); + // Go from raw header value to denominator + if (rxCR < 5) { + rxCR += 4; + } else if (rxCR == 7) { + rxCR = 8; + } - // Received packet configuration must be the same as configured, except for coding rate and CRC - DataRate_t dr = getDataRate(); - dr.lora.codingRate = rxCR; + // Received packet configuration must be the same as configured, except for coding rate and CRC + DataRate_t dr = getDataRate(); + dr.lora.codingRate = rxCR; - PacketConfig_t pc = getPacketConfig(); - pc.lora.crcEnabled = hasCRC; + PacketConfig_t pc = getPacketConfig(); + pc.lora.crcEnabled = hasCRC; - return lora.calculateTimeOnAir(modemType, dr, pc, pl) / 1000; - } - - return lora.getTimeOnAir(pl) / 1000; + return lora.calculateTimeOnAir(modemType, dr, pc, pl) / 1000; } - const char *radioLibErr = "RadioLib err="; + return lora.getTimeOnAir(pl) / 1000; + } - /** - * If the packet is not already in the late rebroadcast window, move it there - */ - void clampToLateRebroadcastWindow(NodeNum from, PacketId id); + const char *radioLibErr = "RadioLib err="; - /** - * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version - * @return Whether a pending packet was removed - */ + /** + * If the packet is not already in the late rebroadcast window, move it there + */ + void clampToLateRebroadcastWindow(NodeNum from, PacketId id); - bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; + /** + * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better + * version + * @return Whether a pending packet was removed + */ + + bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; }; diff --git a/src/mesh/RadioLibRF95.cpp b/src/mesh/RadioLibRF95.cpp index a34c0605f..d22daab41 100644 --- a/src/mesh/RadioLibRF95.cpp +++ b/src/mesh/RadioLibRF95.cpp @@ -7,60 +7,57 @@ RadioLibRF95::RadioLibRF95(Module *mod) : SX1278(mod) {} -int16_t RadioLibRF95::begin(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t syncWord, int8_t power, uint16_t preambleLength, - uint8_t gain) -{ - // execute common part - uint8_t rf95versions[2] = {0x12, 0x11}; - int16_t state = SX127x::begin(rf95versions, sizeof(rf95versions), syncWord, preambleLength); - RADIOLIB_ASSERT(state); +int16_t RadioLibRF95::begin(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t syncWord, int8_t power, uint16_t preambleLength, uint8_t gain) { + // execute common part + uint8_t rf95versions[2] = {0x12, 0x11}; + int16_t state = SX127x::begin(rf95versions, sizeof(rf95versions), syncWord, preambleLength); + RADIOLIB_ASSERT(state); - // current limit was removed from module' ctor - // override default value (60 mA) - state = setCurrentLimit(currentLimit); - LOG_DEBUG("Current limit set to %f", currentLimit); - LOG_DEBUG("Current limit set result %d", state); + // current limit was removed from module' ctor + // override default value (60 mA) + state = setCurrentLimit(currentLimit); + LOG_DEBUG("Current limit set to %f", currentLimit); + LOG_DEBUG("Current limit set result %d", state); - // configure settings not accessible by API - // state = config(); - RADIOLIB_ASSERT(state); + // configure settings not accessible by API + // state = config(); + RADIOLIB_ASSERT(state); #ifdef RF95_TCXO - state = _mod->SPIsetRegValue(RADIOLIB_SX127X_REG_TCXO, 0x10 | _mod->SPIgetRegValue(RADIOLIB_SX127X_REG_TCXO)); - RADIOLIB_ASSERT(state); + state = _mod->SPIsetRegValue(RADIOLIB_SX127X_REG_TCXO, 0x10 | _mod->SPIgetRegValue(RADIOLIB_SX127X_REG_TCXO)); + RADIOLIB_ASSERT(state); #endif - // configure publicly accessible settings - state = setFrequency(freq); - RADIOLIB_ASSERT(state); + // configure publicly accessible settings + state = setFrequency(freq); + RADIOLIB_ASSERT(state); - state = setBandwidth(bw); - RADIOLIB_ASSERT(state); + state = setBandwidth(bw); + RADIOLIB_ASSERT(state); - state = setSpreadingFactor(sf); - RADIOLIB_ASSERT(state); + state = setSpreadingFactor(sf); + RADIOLIB_ASSERT(state); - state = setCodingRate(cr); - RADIOLIB_ASSERT(state); + state = setCodingRate(cr); + RADIOLIB_ASSERT(state); #ifdef USE_RF95_RFO - state = setOutputPower(power, true); + state = setOutputPower(power, true); #else - state = setOutputPower(power); + state = setOutputPower(power); #endif - RADIOLIB_ASSERT(state); + RADIOLIB_ASSERT(state); - state = setGain(gain); + state = setGain(gain); - return (state); + return (state); } -int16_t RadioLibRF95::setFrequency(float freq) -{ - // RADIOLIB_CHECK_RANGE(freq, 862.0, 1020.0, ERR_INVALID_FREQUENCY); +int16_t RadioLibRF95::setFrequency(float freq) { + // RADIOLIB_CHECK_RANGE(freq, 862.0, 1020.0, ERR_INVALID_FREQUENCY); - // set frequency - return (SX127x::setFrequencyRaw(freq)); + // set frequency + return (SX127x::setFrequencyRaw(freq)); } #define RH_RF95_MODEM_STATUS_CLEAR 0x10 @@ -69,18 +66,15 @@ int16_t RadioLibRF95::setFrequency(float freq) #define RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED 0x02 #define RH_RF95_MODEM_STATUS_SIGNAL_DETECTED 0x01 -bool RadioLibRF95::isReceiving() -{ - // 0x0b == Look for header info valid, signal synchronized or signal detected - uint8_t reg = readReg(RADIOLIB_SX127X_REG_MODEM_STAT); - // Serial.printf("reg %x", reg); - return (reg & (RH_RF95_MODEM_STATUS_SIGNAL_DETECTED | RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED | - RH_RF95_MODEM_STATUS_HEADER_INFO_VALID)) != 0; +bool RadioLibRF95::isReceiving() { + // 0x0b == Look for header info valid, signal synchronized or signal detected + uint8_t reg = readReg(RADIOLIB_SX127X_REG_MODEM_STAT); + // Serial.printf("reg %x", reg); + return (reg & (RH_RF95_MODEM_STATUS_SIGNAL_DETECTED | RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED | RH_RF95_MODEM_STATUS_HEADER_INFO_VALID)) != 0; } -uint8_t RadioLibRF95::readReg(uint8_t addr) -{ - Module *mod = this->getMod(); - return mod->SPIreadRegister(addr); +uint8_t RadioLibRF95::readReg(uint8_t addr) { + Module *mod = this->getMod(); + return mod->SPIreadRegister(addr); } #endif \ No newline at end of file diff --git a/src/mesh/RadioLibRF95.h b/src/mesh/RadioLibRF95.h index 916a33234..c4bcafa5e 100644 --- a/src/mesh/RadioLibRF95.h +++ b/src/mesh/RadioLibRF95.h @@ -7,67 +7,66 @@ \brief Derived class for %RFM95 modules. Overrides some methods from SX1278 due to different parameter ranges. */ -class RadioLibRF95 : public SX1278 -{ - public: - // constructor +class RadioLibRF95 : public SX1278 { +public: + // constructor - /*! - \brief Default constructor. Called from Arduino sketch when creating new LoRa instance. + /*! + \brief Default constructor. Called from Arduino sketch when creating new LoRa instance. - \param mod Instance of Module that will be used to communicate with the %LoRa chip. - */ - explicit RadioLibRF95(Module *mod); + \param mod Instance of Module that will be used to communicate with the %LoRa chip. + */ + explicit RadioLibRF95(Module *mod); - // basic methods + // basic methods - /*! - \brief %LoRa modem initialization method. Must be called at least once from Arduino sketch to initialize the module. + /*! + \brief %LoRa modem initialization method. Must be called at least once from Arduino sketch to initialize the module. - \param freq Carrier frequency in MHz. Allowed values range from 868.0 MHz to 915.0 MHz. + \param freq Carrier frequency in MHz. Allowed values range from 868.0 MHz to 915.0 MHz. - \param bw %LoRa link bandwidth in kHz. Allowed values are 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250 and 500 kHz. + \param bw %LoRa link bandwidth in kHz. Allowed values are 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250 and 500 kHz. - \param sf %LoRa link spreading factor. Allowed values range from 6 to 12. + \param sf %LoRa link spreading factor. Allowed values range from 6 to 12. - \param cr %LoRa link coding rate denominator. Allowed values range from 5 to 8. + \param cr %LoRa link coding rate denominator. Allowed values range from 5 to 8. - \param syncWord %LoRa sync word. Can be used to distinguish different networks. Note that value 0x34 is reserved for LoRaWAN - networks. + \param syncWord %LoRa sync word. Can be used to distinguish different networks. Note that value 0x34 is reserved for + LoRaWAN networks. - \param power Transmission output power in dBm. Allowed values range from 2 to 17 dBm. + \param power Transmission output power in dBm. Allowed values range from 2 to 17 dBm. - \param preambleLength Length of %LoRa transmission preamble in symbols. The actual preamble length is 4.25 symbols longer - than the set number. Allowed values range from 6 to 65535. + \param preambleLength Length of %LoRa transmission preamble in symbols. The actual preamble length is 4.25 symbols + longer than the set number. Allowed values range from 6 to 65535. - \param gain Gain of receiver LNA (low-noise amplifier). Can be set to any integer in range 1 to 6 where 1 is the highest - gain. Set to 0 to enable automatic gain control (recommended). + \param gain Gain of receiver LNA (low-noise amplifier). Can be set to any integer in range 1 to 6 where 1 is the + highest gain. Set to 0 to enable automatic gain control (recommended). - \returns \ref status_codes - */ - int16_t begin(float freq = 915.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, - uint8_t syncWord = RADIOLIB_SX127X_SYNC_WORD, int8_t power = 17, uint16_t preambleLength = 8, uint8_t gain = 0); + \returns \ref status_codes + */ + int16_t begin(float freq = 915.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, uint8_t syncWord = RADIOLIB_SX127X_SYNC_WORD, int8_t power = 17, + uint16_t preambleLength = 8, uint8_t gain = 0); - // configuration methods + // configuration methods - /*! - \brief Sets carrier frequency. Allowed values range from 868.0 MHz to 915.0 MHz. + /*! + \brief Sets carrier frequency. Allowed values range from 868.0 MHz to 915.0 MHz. - \param freq Carrier frequency to be set in MHz. + \param freq Carrier frequency to be set in MHz. - \returns \ref status_codes - */ - int16_t setFrequency(float freq); + \returns \ref status_codes + */ + int16_t setFrequency(float freq); - // Return true if we are actively receiving a message currently - bool isReceiving(); + // Return true if we are actively receiving a message currently + bool isReceiving(); - /// For debugging - uint8_t readReg(uint8_t addr); + /// For debugging + uint8_t readReg(uint8_t addr); - protected: - // since default current limit for SX126x/127x in updated RadioLib is 60mA - // use the previous value - float currentLimit = 100; +protected: + // since default current limit for SX126x/127x in updated RadioLib is 60mA + // use the previous value + float currentLimit = 100; }; #endif \ No newline at end of file diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 2b9b17183..ed0b1a48b 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -14,78 +14,76 @@ * If the message is want_ack, then add it to a list of packets to retransmit. * If we run out of retransmissions, send a nak packet towards the original client to indicate failure. */ -ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) -{ - if (p->want_ack) { - // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our - // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop - // counts and we want this message to get through the whole mesh, so use the default. - if (p->hop_limit == 0) { - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - } - DEBUG_HEAP_BEFORE; - auto copy = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("ReliableRouter::send", copy); - - startRetransmission(copy, NUM_RELIABLE_RETX); +ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) { + if (p->want_ack) { + // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives + // our message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference + // on hop counts and we want this message to get through the whole mesh, so use the default. + if (p->hop_limit == 0) { + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); } + DEBUG_HEAP_BEFORE; + auto copy = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("ReliableRouter::send", copy); - /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an - (implicit) ACK. Otherwise, we might retransmit too early. - */ - for (auto i = pending.begin(); i != pending.end(); i++) { - if (i->first.id != p->id) { - i->second.nextTxMsec += iface->getPacketTime(p); - } + startRetransmission(copy, NUM_RELIABLE_RETX); + } + + /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot + receive an (implicit) ACK. Otherwise, we might retransmit too early. + */ + for (auto i = pending.begin(); i != pending.end(); i++) { + if (i->first.id != p->id) { + i->second.nextTxMsec += iface->getPacketTime(p); } + } - return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p); + return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p); } -bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) -{ - // Note: do not use getFrom() here, because we want to ignore messages sent from phone - if (p->from == getNodeNum()) { - printPacket("Rx someone rebroadcasting for us", p); +bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { + // Note: do not use getFrom() here, because we want to ignore messages sent from phone + if (p->from == getNodeNum()) { + printPacket("Rx someone rebroadcasting for us", p); - // We are seeing someone rebroadcast one of our broadcast attempts. - // If this is the first time we saw this, cancel any retransmissions we have queued up and generate an internal ack for - // the original sending process. + // We are seeing someone rebroadcast one of our broadcast attempts. + // If this is the first time we saw this, cancel any retransmissions we have queued up and generate an internal ack + // for the original sending process. - // This "optimization", does save lots of airtime. For DMs, you also get a real ACK back - // from the intended recipient. - auto key = GlobalPacketId(getFrom(p), p->id); - auto old = findPendingPacket(key); - if (old) { - LOG_DEBUG("Generate implicit ack"); - // NOTE: we do NOT check p->wantAck here because p is the INCOMING rebroadcast and that packet is not expected to be - // marked as wantAck - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); + // This "optimization", does save lots of airtime. For DMs, you also get a real ACK back + // from the intended recipient. + auto key = GlobalPacketId(getFrom(p), p->id); + auto old = findPendingPacket(key); + if (old) { + LOG_DEBUG("Generate implicit ack"); + // NOTE: we do NOT check p->wantAck here because p is the INCOMING rebroadcast and that packet is not expected to + // be marked as wantAck + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); - // Only stop retransmissions if the rebroadcast came via LoRa - if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { - stopRetransmission(key); - } - } else { - LOG_DEBUG("Didn't find pending packet"); - } + // Only stop retransmissions if the rebroadcast came via LoRa + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + stopRetransmission(key); + } + } else { + LOG_DEBUG("Didn't find pending packet"); } + } - /* At this point we have already deleted the pending retransmission if this packet was an (implicit) ACK to it. - Now for all other pending retransmissions, we have to add the airtime of this received packet to the retransmission timer, - because while receiving this packet, we could not have received an (implicit) ACK for it. - If we don't add this, we will likely retransmit too early. - */ - for (auto i = pending.begin(); i != pending.end(); i++) { - i->second.nextTxMsec += iface->getPacketTime(p, true); - } + /* At this point we have already deleted the pending retransmission if this packet was an (implicit) ACK to it. + Now for all other pending retransmissions, we have to add the airtime of this received packet to the retransmission + timer, because while receiving this packet, we could not have received an (implicit) ACK for it. If we don't add + this, we will likely retransmit too early. + */ + for (auto i = pending.begin(); i != pending.end(); i++) { + i->second.nextTxMsec += iface->getPacketTime(p, true); + } - return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p); + return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p); } /** - * If we receive a want_ack packet (do not check for wasSeenRecently), send back an ack (this might generate multiple ack sends in - * case the our first ack gets lost) + * If we receive a want_ack packet (do not check for wasSeenRecently), send back an ack (this might generate multiple + * ack sends in case the our first ack gets lost) * * If we receive an ack packet (do check wasSeenRecently), clear out any retransmissions and * forward the ack to the application layer. @@ -95,106 +93,100 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) * * Otherwise, let superclass handle it. */ -void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) -{ - if (isToUs(p)) { // ignore ack/nak/want_ack packets that are not address to us (we only handle 0 hop reliability) - if (!MeshModule::currentReply) { - if (p->want_ack) { - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - /* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received - an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to - make sure the other side stops retransmitting. */ +void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { + if (isToUs(p)) { // ignore ack/nak/want_ack packets that are not address to us (we only handle 0 hop reliability) + if (!MeshModule::currentReply) { + if (p->want_ack) { + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + /* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received + an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to + make sure the other side stops retransmitting. */ - if (shouldSuccessAckWithWantAck(p)) { - // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we - // do that unconditionally. - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(*p), true); - } else if (!p->decoded.request_id && !p->decoded.reply_id) { - // If it's not an ACK or a reply, send an ACK. - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(*p)); - } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { - // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender - // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to - // stop the immediate relayer's retransmissions. - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } - } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && - (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { - LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); - sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(*p)); - } else { - // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded - sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(*p)); - } - } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { - // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } + if (shouldSuccessAckWithWantAck(p)) { + // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we + // do that unconditionally. + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, routingModule->getHopLimitForResponse(*p), true); + } else if (!p->decoded.request_id && !p->decoded.reply_id) { + // If it's not an ACK or a reply, send an ACK. + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, routingModule->getHopLimitForResponse(*p)); + } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { + // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender + // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to + // stop the immediate relayer's retransmissions. + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } + } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && + (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { + LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); + sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), + routingModule->getHopLimitForResponse(*p)); } else { - LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); - } - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c && - c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { - if (owner.public_key.size == 32) { - LOG_INFO("PKI decrypt failure, send a NodeInfo"); - nodeInfoModule->sendOurNodeInfo(p->from, false, p->channel, true); - } - } - // We consider an ack to be either a !routing packet with a request ID or a routing packet with !error - PacketId ackId = ((c && c->error_reason == meshtastic_Routing_Error_NONE) || !c) ? p->decoded.request_id : 0; - - // A nak is a routing packt that has an error code - PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; - - // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records - if ((ackId || nakId) && - // Implicit ACKs from MQTT should not stop retransmissions - !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { - LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); - if (ackId) { - stopRetransmission(p->to, ackId); - } else { - stopRetransmission(p->to, nakId); - } + // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded + sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), routingModule->getHopLimitForResponse(*p)); } + } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { + // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } + } else { + LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); } + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c && c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { + if (owner.public_key.size == 32) { + LOG_INFO("PKI decrypt failure, send a NodeInfo"); + nodeInfoModule->sendOurNodeInfo(p->from, false, p->channel, true); + } + } + // We consider an ack to be either a !routing packet with a request ID or a routing packet with !error + PacketId ackId = ((c && c->error_reason == meshtastic_Routing_Error_NONE) || !c) ? p->decoded.request_id : 0; - // handle the packet as normal - isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c); + // A nak is a routing packt that has an error code + PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; + + // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission + // records + if ((ackId || nakId) && + // Implicit ACKs from MQTT should not stop retransmissions + !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { + LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); + if (ackId) { + stopRetransmission(p->to, ackId); + } else { + stopRetransmission(p->to, nakId); + } + } + } + + // handle the packet as normal + isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c); } /** * If we ACK this packet, should we set want_ack=true on the ACK for reliable delivery of the ACK packet? */ -bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p) -{ - // Don't ACK-with-want-ACK outgoing packets - if (isFromUs(p)) - return false; - - // Only ACK-with-want-ACK if the original packet asked for want_ack - if (!p->want_ack) - return false; - - // Only ACK-with-want-ACK packets to us (not broadcast) - if (!isToUs(p)) - return false; - - // Special case for text message DMs: - bool isTextMessage = - (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && - IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP); - - if (isTextMessage) { - // If it's a non-broadcast text message, and the original asked for want_ack, - // let's send an ACK that is itself want_ack to improve reliability of confirming delivery back to the sender. - // This should include all DMs regardless of whether or not reply_id is set. - return true; - } - +bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p) { + // Don't ACK-with-want-ACK outgoing packets + if (isFromUs(p)) return false; + + // Only ACK-with-want-ACK if the original packet asked for want_ack + if (!p->want_ack) + return false; + + // Only ACK-with-want-ACK packets to us (not broadcast) + if (!isToUs(p)) + return false; + + // Special case for text message DMs: + bool isTextMessage = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && + IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP); + + if (isTextMessage) { + // If it's a non-broadcast text message, and the original asked for want_ack, + // let's send an ACK that is itself want_ack to improve reliability of confirming delivery back to the sender. + // This should include all DMs regardless of whether or not reply_id is set. + return true; + } + + return false; } \ No newline at end of file diff --git a/src/mesh/ReliableRouter.h b/src/mesh/ReliableRouter.h index 33121de6b..bf567551d 100644 --- a/src/mesh/ReliableRouter.h +++ b/src/mesh/ReliableRouter.h @@ -5,36 +5,35 @@ /** * This is a mixin that extends Router with the ability to do (one hop only) reliable message sends. */ -class ReliableRouter : public NextHopRouter -{ - public: - /** - * Constructor - * - */ - // ReliableRouter(); +class ReliableRouter : public NextHopRouter { +public: + /** + * Constructor + * + */ + // ReliableRouter(); - /** - * Send a packet on a suitable interface. This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + /** + * Send a packet on a suitable interface. This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - protected: - /** - * Look for acks/naks or someone retransmitting us - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; +protected: + /** + * Look for acks/naks or someone retransmitting us + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /** - * We hook this method so we can see packets before FloodingRouter says they should be discarded - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + /** + * We hook this method so we can see packets before FloodingRouter says they should be discarded + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; - private: - /** - * Should this packet be ACKed with a want_ack for reliable delivery? - */ - bool shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p); +private: + /** + * Should this packet be ACKed with a want_ack for reliable delivery? + */ + bool shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p); }; \ No newline at end of file diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 6b197f3eb..f840cfca1 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -23,34 +23,33 @@ #include "serialization/MeshPacketSerializer.h" #endif -#define MAX_RX_FROMRADIO \ - 4 // max number of packets destined to our queue, we dispatch packets quickly so it doesn't need to be big +#define MAX_RX_FROMRADIO 4 // max number of packets destined to our queue, we dispatch packets quickly so it doesn't need to be big // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment #ifdef ARCH_PORTDUINO // Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes -#define MAX_PACKETS \ - (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ - 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +#define MAX_PACKETS \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM) -// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically. -// For now, make it dynamic again. -#define MAX_PACKETS \ - (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ - 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this +// statically. For now, make it dynamic again. +#define MAX_PACKETS \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants -#define MAX_PACKETS_STATIC \ - (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ - 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +#define MAX_PACKETS_STATIC \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryPool staticPool; Allocator &packetPool = staticPool; @@ -63,220 +62,204 @@ static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); * * Currently we only allow one interface, that may change in the future */ -Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRADIO) -{ - // This is called pre main(), don't touch anything here, the following code is not safe +Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRADIO) { + // This is called pre main(), don't touch anything here, the following code is not safe - /* LOG_DEBUG("Size of NodeInfo %d", sizeof(NodeInfo)); - LOG_DEBUG("Size of SubPacket %d", sizeof(SubPacket)); - LOG_DEBUG("Size of MeshPacket %d", sizeof(MeshPacket)); */ + /* LOG_DEBUG("Size of NodeInfo %d", sizeof(NodeInfo)); + LOG_DEBUG("Size of SubPacket %d", sizeof(SubPacket)); + LOG_DEBUG("Size of MeshPacket %d", sizeof(MeshPacket)); */ - fromRadioQueue.setReader(this); + fromRadioQueue.setReader(this); - // init Lockguard for crypt operations - assert(!cryptLock); - cryptLock = new concurrency::Lock(); + // init Lockguard for crypt operations + assert(!cryptLock); + cryptLock = new concurrency::Lock(); } -bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) -{ - // First hop MUST always decrement to prevent retry issues - if (getHopsAway(*p) == 0) { - return true; // Always decrement on first hop - } +bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) { + // First hop MUST always decrement to prevent retry issues + if (getHopsAway(*p) == 0) { + return true; // Always decrement on first hop + } - // Check if both local device and previous relay are routers (including CLIENT_BASE) - bool localIsRouter = - IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, - meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); + // Check if both local device and previous relay are routers (including CLIENT_BASE) + bool localIsRouter = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); - // If local device isn't a router, always decrement - if (!localIsRouter) { - return true; - } - - // For subsequent hops, check if previous relay is a favorite router - // Optimized search for favorite routers with matching last byte - // Check ordering optimized for IoT devices (cheapest checks first) - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node) - continue; - - // Check 1: is_favorite (cheapest - single bool) - if (!node->is_favorite) - continue; - - // Check 2: has_user (cheap - single bool) - if (!node->has_user) - continue; - - // Check 3: role check (moderate cost - multiple comparisons) - if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { - continue; - } - - // Check 4: last byte extraction and comparison (most expensive) - if (nodeDB->getLastByteOfNodeNum(node->num) == p->relay_node) { - // Found a favorite router match - LOG_DEBUG("Identified favorite relay router 0x%x from last byte 0x%x", node->num, p->relay_node); - return false; // Don't decrement hop_limit - } - } - - // No favorite router match found, decrement hop_limit + // If local device isn't a router, always decrement + if (!localIsRouter) { return true; + } + + // For subsequent hops, check if previous relay is a favorite router + // Optimized search for favorite routers with matching last byte + // Check ordering optimized for IoT devices (cheapest checks first) + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node) + continue; + + // Check 1: is_favorite (cheapest - single bool) + if (!node->is_favorite) + continue; + + // Check 2: has_user (cheap - single bool) + if (!node->has_user) + continue; + + // Check 3: role check (moderate cost - multiple comparisons) + if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + continue; + } + + // Check 4: last byte extraction and comparison (most expensive) + if (nodeDB->getLastByteOfNodeNum(node->num) == p->relay_node) { + // Found a favorite router match + LOG_DEBUG("Identified favorite relay router 0x%x from last byte 0x%x", node->num, p->relay_node); + return false; // Don't decrement hop_limit + } + } + + // No favorite router match found, decrement hop_limit + return true; } /** * do idle processing * Mostly looking in our incoming rxPacket queue and calling handleReceived. */ -int32_t Router::runOnce() -{ - meshtastic_MeshPacket *mp; - while ((mp = fromRadioQueue.dequeuePtr(0)) != NULL) { - // printPacket("handle fromRadioQ", mp); - perhapsHandleReceived(mp); - } +int32_t Router::runOnce() { + meshtastic_MeshPacket *mp; + while ((mp = fromRadioQueue.dequeuePtr(0)) != NULL) { + // printPacket("handle fromRadioQ", mp); + perhapsHandleReceived(mp); + } - // LOG_DEBUG("Sleep forever!"); - return INT32_MAX; // Wait a long time - until we get woken for the message queue + // LOG_DEBUG("Sleep forever!"); + return INT32_MAX; // Wait a long time - until we get woken for the message queue } /** - * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for - * freeing the packet + * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible + * for freeing the packet */ -void Router::enqueueReceivedMessage(meshtastic_MeshPacket *p) -{ - // Try enqueue until successful - while (!fromRadioQueue.enqueue(p, 0)) { - meshtastic_MeshPacket *old_p; - old_p = fromRadioQueue.dequeuePtr(0); // Dequeue and discard the oldest packet - if (old_p) { - printPacket("fromRadioQ full, drop oldest!", old_p); - packetPool.release(old_p); - } +void Router::enqueueReceivedMessage(meshtastic_MeshPacket *p) { + // Try enqueue until successful + while (!fromRadioQueue.enqueue(p, 0)) { + meshtastic_MeshPacket *old_p; + old_p = fromRadioQueue.dequeuePtr(0); // Dequeue and discard the oldest packet + if (old_p) { + printPacket("fromRadioQ full, drop oldest!", old_p); + packetPool.release(old_p); } - // Nasty hack because our threading is primitive. interfaces shouldn't need to know about routers FIXME - setReceivedMessage(); + } + // Nasty hack because our threading is primitive. interfaces shouldn't need to know about routers FIXME + setReceivedMessage(); } /// Generate a unique packet id // FIXME, move this someplace better -PacketId generatePacketId() -{ - static uint32_t rollingPacketId; // Note: trying to keep this in noinit didn't help for working across reboots - static bool didInit = false; +PacketId generatePacketId() { + static uint32_t rollingPacketId; // Note: trying to keep this in noinit didn't help for working across reboots + static bool didInit = false; - if (!didInit) { - didInit = true; + if (!didInit) { + didInit = true; - // pick a random initial sequence number at boot (to prevent repeated reboots always starting at 0) - // Note: we mask the high order bit to ensure that we never pass a 'negative' number to random - rollingPacketId = random(UINT32_MAX & 0x7fffffff); - LOG_DEBUG("Initial packet id %u", rollingPacketId); - } + // pick a random initial sequence number at boot (to prevent repeated reboots always starting at 0) + // Note: we mask the high order bit to ensure that we never pass a 'negative' number to random + rollingPacketId = random(UINT32_MAX & 0x7fffffff); + LOG_DEBUG("Initial packet id %u", rollingPacketId); + } - rollingPacketId++; + rollingPacketId++; - rollingPacketId &= ID_COUNTER_MASK; // Mask out the top 22 bits - PacketId id = rollingPacketId | random(UINT32_MAX & 0x7fffffff) << 10; // top 22 bits - LOG_DEBUG("Partially randomized packet id %u", id); - return id; + rollingPacketId &= ID_COUNTER_MASK; // Mask out the top 22 bits + PacketId id = rollingPacketId | random(UINT32_MAX & 0x7fffffff) << 10; // top 22 bits + LOG_DEBUG("Partially randomized packet id %u", id); + return id; } -meshtastic_MeshPacket *Router::allocForSending() -{ - meshtastic_MeshPacket *p = packetPool.allocZeroed(); +meshtastic_MeshPacket *Router::allocForSending() { + meshtastic_MeshPacket *p = packetPool.allocZeroed(); - p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // Assume payload is decoded at start. - p->from = nodeDB->getNodeNum(); - p->to = NODENUM_BROADCAST; - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - p->id = generatePacketId(); - p->rx_time = - getValidTime(RTCQualityFromNet); // Just in case we process the packet locally - make sure it has a valid timestamp + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // Assume payload is decoded at start. + p->from = nodeDB->getNodeNum(); + p->to = NODENUM_BROADCAST; + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + p->id = generatePacketId(); + p->rx_time = getValidTime(RTCQualityFromNet); // Just in case we process the packet locally - make sure it has a valid timestamp - return p; + return p; } /** * Send an ack or a nak packet back towards whoever sent idFrom */ -void Router::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, - bool ackWantsAck) -{ - routingModule->sendAckNak(err, to, idFrom, chIndex, hopLimit, ackWantsAck); +void Router::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, bool ackWantsAck) { + routingModule->sendAckNak(err, to, idFrom, chIndex, hopLimit, ackWantsAck); } -void Router::abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p) -{ - LOG_ERROR("Error=%d, return NAK and drop packet", err); - sendAckNak(err, getFrom(p), p->id, p->channel); - packetPool.release(p); +void Router::abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p) { + LOG_ERROR("Error=%d, return NAK and drop packet", err); + sendAckNak(err, getFrom(p), p->id, p->channel); + packetPool.release(p); } -void Router::setReceivedMessage() -{ - // LOG_DEBUG("set interval to ASAP"); - setInterval(0); // Run ASAP, so we can figure out our correct sleep time - runASAP = true; +void Router::setReceivedMessage() { + // LOG_DEBUG("set interval to ASAP"); + setInterval(0); // Run ASAP, so we can figure out our correct sleep time + runASAP = true; } -meshtastic_QueueStatus Router::getQueueStatus() -{ - if (!iface) { - meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; - return qs; - } else - return iface->getQueueStatus(); +meshtastic_QueueStatus Router::getQueueStatus() { + if (!iface) { + meshtastic_QueueStatus qs; + qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; + return qs; + } else + return iface->getQueueStatus(); } -ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) -{ - if (p->to == 0) { - LOG_ERROR("Packet received with to: of 0!"); +ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) { + if (p->to == 0) { + LOG_ERROR("Packet received with to: of 0!"); + } + // No need to deliver externally if the destination is the local node + if (isToUs(p)) { + printPacket("Enqueued local", p); + enqueueReceivedMessage(p); + return ERRNO_OK; + } else if (!iface) { + // We must be sending to remote nodes also, fail if no interface found + abortSendAndNak(meshtastic_Routing_Error_NO_INTERFACE, p); + + return ERRNO_NO_INTERFACES; + } else { + // If we are sending a broadcast, we also treat it as if we just received it ourself + // this allows local apps (and PCs) to see broadcasts sourced locally + if (isBroadcast(p->to)) { + handleReceived(p, src); } - // No need to deliver externally if the destination is the local node - if (isToUs(p)) { - printPacket("Enqueued local", p); - enqueueReceivedMessage(p); - return ERRNO_OK; - } else if (!iface) { - // We must be sending to remote nodes also, fail if no interface found - abortSendAndNak(meshtastic_Routing_Error_NO_INTERFACE, p); - return ERRNO_NO_INTERFACES; - } else { - // If we are sending a broadcast, we also treat it as if we just received it ourself - // this allows local apps (and PCs) to see broadcasts sourced locally - if (isBroadcast(p->to)) { - handleReceived(p, src); - } - - // don't override if a channel was requested and no need to set it when PKI is enforced - if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) { - meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to); - if (node) { - p->channel = node->channel; - LOG_DEBUG("localSend to channel %d", p->channel); - } - } - - return send(p); + // don't override if a channel was requested and no need to set it when PKI is enforced + if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) { + meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to); + if (node) { + p->channel = node->channel; + LOG_DEBUG("localSend to channel %d", p->channel); + } } + + return send(p); + } } /** * Send a packet on a suitable interface. */ -ErrorCode Router::rawSend(meshtastic_MeshPacket *p) -{ - assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) - return iface->send(p); +ErrorCode Router::rawSend(meshtastic_MeshPacket *p) { + assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) + return iface->send(p); } /** @@ -284,534 +267,512 @@ ErrorCode Router::rawSend(meshtastic_MeshPacket *p) * later free() the packet to pool. This routine is not allowed to stall. * If the txmit queue is full it might return an error. */ -ErrorCode Router::send(meshtastic_MeshPacket *p) -{ - if (isToUs(p)) { - LOG_ERROR("BUG! send() called with packet destined for local node!"); +ErrorCode Router::send(meshtastic_MeshPacket *p) { + if (isToUs(p)) { + LOG_ERROR("BUG! send() called with packet destined for local node!"); + packetPool.release(p); + return meshtastic_Routing_Error_BAD_REQUEST; + } // should have already been handled by sendLocal + + // Abort sending if we are violating the duty cycle + if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { + float hourlyTxPercent = airTime->utilizationTXPercent(); + if (hourlyTxPercent > myRegion->dutyCycle) { + uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); + + LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->has_reply_id = true; + cn->reply_id = p->id; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); + service->sendClientNotification(cn); + + meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; + if (isFromUs(p)) { // only send NAK to API, not to the mesh + abortSendAndNak(err, p); + } else { packetPool.release(p); - return meshtastic_Routing_Error_BAD_REQUEST; - } // should have already been handled by sendLocal - - // Abort sending if we are violating the duty cycle - if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { - float hourlyTxPercent = airTime->utilizationTXPercent(); - if (hourlyTxPercent > myRegion->dutyCycle) { - uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); - - LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); - - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->has_reply_id = true; - cn->reply_id = p->id; - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); - service->sendClientNotification(cn); - - meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; - if (isFromUs(p)) { // only send NAK to API, not to the mesh - abortSendAndNak(err, p); - } else { - packetPool.release(p); - } - return err; - } + } + return err; } + } - // PacketId nakId = p->decoded.which_ackVariant == SubPacket_fail_id_tag ? p->decoded.ackVariant.fail_id : 0; - // assert(!nakId); // I don't think we ever send 0hop naks over the wire (other than to the phone), test that assumption with - // assert + // PacketId nakId = p->decoded.which_ackVariant == SubPacket_fail_id_tag ? p->decoded.ackVariant.fail_id : 0; + // assert(!nakId); // I don't think we ever send 0hop naks over the wire (other than to the phone), test that + // assumption with assert - // Never set the want_ack flag on broadcast packets sent over the air. - if (isBroadcast(p->to)) - p->want_ack = false; + // Never set the want_ack flag on broadcast packets sent over the air. + if (isBroadcast(p->to)) + p->want_ack = false; - // Up until this point we might have been using 0 for the from address (if it started with the phone), but when we send over - // the lora we need to make sure we have replaced it with our local address - p->from = getFrom(p); + // Up until this point we might have been using 0 for the from address (if it started with the phone), but when we + // send over the lora we need to make sure we have replaced it with our local address + p->from = getFrom(p); - p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us - // If we are the original transmitter, set the hop limit with which we start - if (isFromUs(p)) - p->hop_start = p->hop_limit; + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us + // If we are the original transmitter, set the hop limit with which we start + if (isFromUs(p)) + p->hop_start = p->hop_limit; - // If the packet hasn't yet been encrypted, do so now (it might already be encrypted if we are just forwarding it) + // If the packet hasn't yet been encrypted, do so now (it might already be encrypted if we are just forwarding it) - if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag || - p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) { - return meshtastic_Routing_Error_BAD_REQUEST; + if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag || p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) { + return meshtastic_Routing_Error_BAD_REQUEST; + } + + fixPriority(p); // Before encryption, fix the priority if it's unset + + // If the packet is not yet encrypted, do so now + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it + + DEBUG_HEAP_BEFORE; + meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("Router::send", p_decoded); + + auto encodeResult = perhapsEncode(p); + if (encodeResult != meshtastic_Routing_Error_NONE) { + packetPool.release(p_decoded); + p->channel = 0; // Reset the channel to 0, so we don't use the failing hash again + abortSendAndNak(encodeResult, p); + return encodeResult; // FIXME - this isn't a valid ErrorCode } - - fixPriority(p); // Before encryption, fix the priority if it's unset - - // If the packet is not yet encrypted, do so now - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it - - DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("Router::send", p_decoded); - - auto encodeResult = perhapsEncode(p); - if (encodeResult != meshtastic_Routing_Error_NONE) { - packetPool.release(p_decoded); - p->channel = 0; // Reset the channel to 0, so we don't use the failing hash again - abortSendAndNak(encodeResult, p); - return encodeResult; // FIXME - this isn't a valid ErrorCode - } #if !MESHTASTIC_EXCLUDE_MQTT - // Only publish to MQTT if we're the original transmitter of the packet - if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { - mqtt->onSend(*p, *p_decoded, chIndex); - } -#endif - packetPool.release(p_decoded); + // Only publish to MQTT if we're the original transmitter of the packet + if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { + mqtt->onSend(*p, *p_decoded, chIndex); } +#endif + packetPool.release(p_decoded); + } #if HAS_UDP_MULTICAST - if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->onSend(const_cast(p)); - } + if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->onSend(const_cast(p)); + } #endif - assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) - return iface->send(p); + assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) + return iface->send(p); } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ -bool Router::cancelSending(NodeNum from, PacketId id) -{ - if (iface && iface->cancelSending(from, id)) { - // We are not a relayer of this packet anymore - removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); - return true; - } - return false; +bool Router::cancelSending(NodeNum from, PacketId id) { + if (iface && iface->cancelSending(from, id)) { + // We are not a relayer of this packet anymore + removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); + return true; + } + return false; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ -bool Router::findInTxQueue(NodeNum from, PacketId id) -{ - return iface->findInTxQueue(from, id); -} +bool Router::findInTxQueue(NodeNum from, PacketId id) { return iface->findInTxQueue(from, id); } /** * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to * update routing tables etc... based on what we overhear (even for messages not destined to our node) */ -void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) -{ - // FIXME, update nodedb here for any packet that passes through us +void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { + // FIXME, update nodedb here for any packet that passes through us } -DecodeState perhapsDecode(meshtastic_MeshPacket *p) -{ - concurrency::LockGuard g(cryptLock); +DecodeState perhapsDecode(meshtastic_MeshPacket *p) { + concurrency::LockGuard g(cryptLock); - if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && - (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { - LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); + if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && + (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { + LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); + return DecodeState::DECODE_FAILURE; + } + + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) + return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return + + size_t rawSize = p->encrypted.size; + if (rawSize > sizeof(bytes)) { + LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize); + return DecodeState::DECODE_FATAL; + } + bool decrypted = false; + ChannelIndex chIndex = 0; +#if !(MESHTASTIC_EXCLUDE_PKI) + // Attempt PKI decryption first + if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && + nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && + rawSize > MESHTASTIC_PKC_OVERHEAD) { + LOG_DEBUG("Attempt PKI decryption"); + + if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, bytes)) { + LOG_INFO("PKI Decryption worked!"); + + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + rawSize -= MESHTASTIC_PKC_OVERHEAD; + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { + decrypted = true; + LOG_INFO("Packet decrypted using PKI!"); + p->pki_encrypted = true; + memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); + p->public_key.size = 32; + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded + } else { + LOG_ERROR("PKC Decrypted, but pb_decode failed!"); return DecodeState::DECODE_FAILURE; - } - - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) - return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return - - size_t rawSize = p->encrypted.size; - if (rawSize > sizeof(bytes)) { - LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize); - return DecodeState::DECODE_FATAL; - } - bool decrypted = false; - ChannelIndex chIndex = 0; -#if !(MESHTASTIC_EXCLUDE_PKI) - // Attempt PKI decryption first - if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && - nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && - rawSize > MESHTASTIC_PKC_OVERHEAD) { - LOG_DEBUG("Attempt PKI decryption"); - - if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, - bytes)) { - LOG_INFO("PKI Decryption worked!"); - - meshtastic_Data decodedtmp; - memset(&decodedtmp, 0, sizeof(decodedtmp)); - rawSize -= MESHTASTIC_PKC_OVERHEAD; - if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && - decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { - decrypted = true; - LOG_INFO("Packet decrypted using PKI!"); - p->pki_encrypted = true; - memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); - p->public_key.size = 32; - p->decoded = decodedtmp; - p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded - } else { - LOG_ERROR("PKC Decrypted, but pb_decode failed!"); - return DecodeState::DECODE_FAILURE; - } - } else { - LOG_WARN("PKC decrypt attempted but failed!"); - } - } -#endif - - // assert(p->which_payloadVariant == MeshPacket_encrypted_tag); - if (!decrypted) { - // Try to find a channel that works with this hash - for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) { - // Try to use this hash/channel pair - if (channels.decryptForHash(chIndex, p->channel)) { - // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a - // fresh copy for each decrypt attempt. - memcpy(bytes, p->encrypted.bytes, rawSize); - // Try to decrypt the packet if we can - crypto->decrypt(p->from, p->id, rawSize, bytes); - - // printBytes("plaintext", bytes, p->encrypted.size); - - // Take those raw bytes and convert them back into a well structured protobuf we can understand - meshtastic_Data decodedtmp; - memset(&decodedtmp, 0, sizeof(decodedtmp)); - if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) { - LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); - } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { - LOG_ERROR("Invalid portnum (bad psk?)!"); -#if !(MESHTASTIC_EXCLUDE_PKI) - } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - LOG_WARN("Rejecting legacy DM"); - return DecodeState::DECODE_FAILURE; -#endif - } else { - p->decoded = decodedtmp; - p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded - decrypted = true; - break; - } - } - } - } - - if (decrypted) { - // parsing was successful - p->channel = chIndex; // change to store the index instead of the hash - if (p->decoded.has_bitfield) - p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK; - - /* Not actually ever used. - // Decompress if needed. jm - if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) { - // Decompress the payload - char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - int decompressed_len; - - memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size); - - decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out); - - // LOG_DEBUG("**Decompressed length - %d ", decompressed_len); - - memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len); - - // Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - } */ - - printPacket("decoded message", p); -#if ENABLE_JSON_LOGGING - LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); -#elif ARCH_PORTDUINO - if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { - LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); - } else if (portduino_config.JSONFilename != "") { - if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { - JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; - } - } -#endif - return DecodeState::DECODE_SUCCESS; + } } else { - LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); - return DecodeState::DECODE_FAILURE; + LOG_WARN("PKC decrypt attempted but failed!"); } + } +#endif + + // assert(p->which_payloadVariant == MeshPacket_encrypted_tag); + if (!decrypted) { + // Try to find a channel that works with this hash + for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) { + // Try to use this hash/channel pair + if (channels.decryptForHash(chIndex, p->channel)) { + // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a + // fresh copy for each decrypt attempt. + memcpy(bytes, p->encrypted.bytes, rawSize); + // Try to decrypt the packet if we can + crypto->decrypt(p->from, p->id, rawSize, bytes); + + // printBytes("plaintext", bytes, p->encrypted.size); + + // Take those raw bytes and convert them back into a well structured protobuf we can understand + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) { + LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); + } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { + LOG_ERROR("Invalid portnum (bad psk?)!"); +#if !(MESHTASTIC_EXCLUDE_PKI) + } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { + LOG_WARN("Rejecting legacy DM"); + return DecodeState::DECODE_FAILURE; +#endif + } else { + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded + decrypted = true; + break; + } + } + } + } + + if (decrypted) { + // parsing was successful + p->channel = chIndex; // change to store the index instead of the hash + if (p->decoded.has_bitfield) + p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK; + + /* Not actually ever used. + // Decompress if needed. jm + if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) { + // Decompress the payload + char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + int decompressed_len; + + memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size); + + decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out); + + // LOG_DEBUG("**Decompressed length - %d ", decompressed_len); + + memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len); + + // Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + } */ + + printPacket("decoded message", p); +#if ENABLE_JSON_LOGGING + LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); +#elif ARCH_PORTDUINO + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { + LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); + } else if (portduino_config.JSONFilename != "") { + if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { + JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; + } + } +#endif + return DecodeState::DECODE_SUCCESS; + } else { + LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); + return DecodeState::DECODE_FAILURE; + } } /** Return 0 for success or a Routing_Error code for failure */ -meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) -{ - concurrency::LockGuard g(cryptLock); +meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) { + concurrency::LockGuard g(cryptLock); - int16_t hash; + int16_t hash; - // If the packet is not yet encrypted, do so now - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - if (isFromUs(p)) { - p->decoded.has_bitfield = true; - p->decoded.bitfield |= (config.lora.config_ok_to_mqtt << BITFIELD_OK_TO_MQTT_SHIFT); - p->decoded.bitfield |= (p->decoded.want_response << BITFIELD_WANT_RESPONSE_SHIFT); - } - - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - - /* Not actually used, so save the cycles - // TODO: Allow modules to opt into compression. - if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - - char original_payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; - memcpy(original_payload, p->decoded.payload.bytes, p->decoded.payload.size); - - char compressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; - - int compressed_len; - compressed_len = unishox2_compress_simple(original_payload, p->decoded.payload.size, compressed_out); - - LOG_DEBUG("Original length - %d ", p->decoded.payload.size); - LOG_DEBUG("Compressed length - %d ", compressed_len); - LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes); - - // If the compressed length is greater than or equal to the original size, don't use the compressed form - if (compressed_len >= p->decoded.payload.size) { - - LOG_DEBUG("Not using compressing message"); - // Set the uncompressed payload variant anyway. Shouldn't hurt? - // p->decoded.which_payloadVariant = Data_payload_tag; - - // Otherwise we use the compressor - } else { - LOG_DEBUG("Use compressed message"); - // Copy the compressed data into the meshpacket - - p->decoded.payload.size = compressed_len; - memcpy(p->decoded.payload.bytes, compressed_out, compressed_len); - - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP; - } - } */ - - if (numbytes + MESHTASTIC_HEADER_LENGTH > MAX_LORA_PAYLOAD_LEN) - return meshtastic_Routing_Error_TOO_LARGE; - - // printBytes("plaintext", bytes, numbytes); - - ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it - -#if !(MESHTASTIC_EXCLUDE_PKI) - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); - // We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node - // is not in the local nodedb - // First, only PKC encrypt packets we are originating - if (isFromUs(p) && -#if ARCH_PORTDUINO - // Sim radio via the cli flag skips PKC - !portduino_config.force_simradio && -#endif - // Don't use PKC with Ham mode - !owner.is_licensed && - // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested - !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || - strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && - // Check for valid keys and single node destination - config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && - // Check for a known public key for the destination - (node->user.public_key.size == 32) && - // Some portnums either make no sense to send with PKC - p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && - p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { - LOG_DEBUG("Use PKI!"); - if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) - return meshtastic_Routing_Error_TOO_LARGE; - if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && - memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { - LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, - *node->user.public_key.bytes); - return meshtastic_Routing_Error_PKI_FAILED; - } - crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); - numbytes += MESHTASTIC_PKC_OVERHEAD; - p->channel = 0; - p->pki_encrypted = true; - } else { - if (p->pki_encrypted == true) { - // Client specifically requested PKI encryption - return meshtastic_Routing_Error_PKI_FAILED; - } - hash = channels.setActiveByIndex(chIndex); - - // Now that we are encrypting the packet channel should be the hash (no longer the index) - p->channel = hash; - if (hash < 0) { - // No suitable channel could be found for - return meshtastic_Routing_Error_NO_CHANNEL; - } - crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); - memcpy(p->encrypted.bytes, bytes, numbytes); - } -#else - if (p->pki_encrypted == true) { - // Client specifically requested PKI encryption - return meshtastic_Routing_Error_PKI_FAILED; - } - hash = channels.setActiveByIndex(chIndex); - - // Now that we are encrypting the packet channel should be the hash (no longer the index) - p->channel = hash; - if (hash < 0) { - // No suitable channel could be found for - return meshtastic_Routing_Error_NO_CHANNEL; - } - crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); - memcpy(p->encrypted.bytes, bytes, numbytes); -#endif - - // Copy back into the packet and set the variant type - p->encrypted.size = numbytes; - p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + // If the packet is not yet encrypted, do so now + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (isFromUs(p)) { + p->decoded.has_bitfield = true; + p->decoded.bitfield |= (config.lora.config_ok_to_mqtt << BITFIELD_OK_TO_MQTT_SHIFT); + p->decoded.bitfield |= (p->decoded.want_response << BITFIELD_WANT_RESPONSE_SHIFT); } - return meshtastic_Routing_Error_NONE; + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + + /* Not actually used, so save the cycles + // TODO: Allow modules to opt into compression. + if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { + + char original_payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; + memcpy(original_payload, p->decoded.payload.bytes, p->decoded.payload.size); + + char compressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; + + int compressed_len; + compressed_len = unishox2_compress_simple(original_payload, p->decoded.payload.size, compressed_out); + + LOG_DEBUG("Original length - %d ", p->decoded.payload.size); + LOG_DEBUG("Compressed length - %d ", compressed_len); + LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes); + + // If the compressed length is greater than or equal to the original size, don't use the compressed form + if (compressed_len >= p->decoded.payload.size) { + + LOG_DEBUG("Not using compressing message"); + // Set the uncompressed payload variant anyway. Shouldn't hurt? + // p->decoded.which_payloadVariant = Data_payload_tag; + + // Otherwise we use the compressor + } else { + LOG_DEBUG("Use compressed message"); + // Copy the compressed data into the meshpacket + + p->decoded.payload.size = compressed_len; + memcpy(p->decoded.payload.bytes, compressed_out, compressed_len); + + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP; + } + } */ + + if (numbytes + MESHTASTIC_HEADER_LENGTH > MAX_LORA_PAYLOAD_LEN) + return meshtastic_Routing_Error_TOO_LARGE; + + // printBytes("plaintext", bytes, numbytes); + + ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it + +#if !(MESHTASTIC_EXCLUDE_PKI) + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); + // We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the + // node is not in the local nodedb First, only PKC encrypt packets we are originating + if (isFromUs(p) && +#if ARCH_PORTDUINO + // Sim radio via the cli flag skips PKC + !portduino_config.force_simradio && +#endif + // Don't use PKC with Ham mode + !owner.is_licensed && + // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested + !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || + strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && + // Check for valid keys and single node destination + config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && + // Check for a known public key for the destination + (node->user.public_key.size == 32) && + // Some portnums either make no sense to send with PKC + p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && + p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { + LOG_DEBUG("Use PKI!"); + if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) + return meshtastic_Routing_Error_TOO_LARGE; + if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { + LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, *node->user.public_key.bytes); + return meshtastic_Routing_Error_PKI_FAILED; + } + crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); + numbytes += MESHTASTIC_PKC_OVERHEAD; + p->channel = 0; + p->pki_encrypted = true; + } else { + if (p->pki_encrypted == true) { + // Client specifically requested PKI encryption + return meshtastic_Routing_Error_PKI_FAILED; + } + hash = channels.setActiveByIndex(chIndex); + + // Now that we are encrypting the packet channel should be the hash (no longer the index) + p->channel = hash; + if (hash < 0) { + // No suitable channel could be found for + return meshtastic_Routing_Error_NO_CHANNEL; + } + crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); + memcpy(p->encrypted.bytes, bytes, numbytes); + } +#else + if (p->pki_encrypted == true) { + // Client specifically requested PKI encryption + return meshtastic_Routing_Error_PKI_FAILED; + } + hash = channels.setActiveByIndex(chIndex); + + // Now that we are encrypting the packet channel should be the hash (no longer the index) + p->channel = hash; + if (hash < 0) { + // No suitable channel could be found for + return meshtastic_Routing_Error_NO_CHANNEL; + } + crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); + memcpy(p->encrypted.bytes, bytes, numbytes); +#endif + + // Copy back into the packet and set the variant type + p->encrypted.size = numbytes; + p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + } + + return meshtastic_Routing_Error_NONE; } -NodeNum Router::getNodeNum() -{ - return nodeDB->getNodeNum(); -} +NodeNum Router::getNodeNum() { return nodeDB->getNodeNum(); } /** * Handle any packet that is received by an interface on this node. * Note: some packets may merely being passed through this node and will be forwarded elsewhere. */ -void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) -{ - bool skipHandle = false; - // Also, we should set the time from the ISR and it should have msec level resolution - p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone +void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) { + bool skipHandle = false; + // Also, we should set the time from the ISR and it should have msec level resolution + p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone - // Store a copy of encrypted packet for MQTT - DEBUG_HEAP_BEFORE; - p_encrypted = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); + // Store a copy of encrypted packet for MQTT + DEBUG_HEAP_BEFORE; + p_encrypted = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); - // Take those raw bytes and convert them back into a well structured protobuf we can understand - auto decodedState = perhapsDecode(p); - if (decodedState == DecodeState::DECODE_FATAL) { - // Fatal decoding error, we can't do anything with this packet - LOG_WARN("Fatal decode error, dropping packet"); - cancelSending(p->from, p->id); - skipHandle = true; - } else if (decodedState == DecodeState::DECODE_SUCCESS) { - // parsing was successful, queue for our recipient - if (src == RX_SRC_LOCAL) - printPacket("handleReceived(LOCAL)", p); - else if (src == RX_SRC_USER) - printPacket("handleReceived(USER)", p); - else - printPacket("handleReceived(REMOTE)", p); + // Take those raw bytes and convert them back into a well structured protobuf we can understand + auto decodedState = perhapsDecode(p); + if (decodedState == DecodeState::DECODE_FATAL) { + // Fatal decoding error, we can't do anything with this packet + LOG_WARN("Fatal decode error, dropping packet"); + cancelSending(p->from, p->id); + skipHandle = true; + } else if (decodedState == DecodeState::DECODE_SUCCESS) { + // parsing was successful, queue for our recipient + if (src == RX_SRC_LOCAL) + printPacket("handleReceived(LOCAL)", p); + else if (src == RX_SRC_USER) + printPacket("handleReceived(USER)", p); + else + printPacket("handleReceived(REMOTE)", p); - // Neighbor info module is disabled, ignore expensive neighbor info packets - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP && - (!moduleConfig.has_neighbor_info || !moduleConfig.neighbor_info.enabled)) { - LOG_DEBUG("Neighbor info module is disabled, ignore neighbor packet"); - cancelSending(p->from, p->id); - skipHandle = true; - } - - bool shouldIgnoreNonstandardPorts = - config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; -#if USERPREFS_EVENT_MODE - shouldIgnoreNonstandardPorts = true; -#endif - if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP, - meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, - meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, - meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, - meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, - meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { - LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); - cancelSending(p->from, p->id); - skipHandle = true; - } - } else { - printPacket("packet decoding failed or skipped (no PSK?)", p); + // Neighbor info module is disabled, ignore expensive neighbor info packets + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP && + (!moduleConfig.has_neighbor_info || !moduleConfig.neighbor_info.enabled)) { + LOG_DEBUG("Neighbor info module is disabled, ignore neighbor packet"); + cancelSending(p->from, p->id); + skipHandle = true; } - // call modules here - // If this could be a spoofed packet, don't let the modules see it. - if (!skipHandle) { - MeshModule::callModules(*p, src); + bool shouldIgnoreNonstandardPorts = config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; +#if USERPREFS_EVENT_MODE + shouldIgnoreNonstandardPorts = true; +#endif + if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP, + meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, meshtastic_PortNum_TELEMETRY_APP, + meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_KEY_VERIFICATION_APP, + meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { + LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); + cancelSending(p->from, p->id); + skipHandle = true; + } + } else { + printPacket("packet decoding failed or skipped (no PSK?)", p); + } + + // call modules here + // If this could be a spoofed packet, don't let the modules see it. + if (!skipHandle) { + MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT - if (p_encrypted == nullptr) { - LOG_WARN("p_encrypted is null, skipping MQTT publish"); - } else { - // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not - // to us (because we would be able to decrypt it) - if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && - !isBroadcast(p->to) && !isToUs(p)) - p_encrypted->pki_encrypted = true; - // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet - if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && - !isFromUs(p) && mqtt) - mqtt->onSend(*p_encrypted, *p, p->channel); - } -#endif + if (p_encrypted == nullptr) { + LOG_WARN("p_encrypted is null, skipping MQTT publish"); + } else { + // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM + // not to us (because we would be able to decrypt it) + if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && !isBroadcast(p->to) && + !isToUs(p)) + p_encrypted->pki_encrypted = true; + // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the + // packet + if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && !isFromUs(p) && mqtt) + mqtt->onSend(*p_encrypted, *p, p->channel); } +#endif + } - packetPool.release(p_encrypted); // Release the encrypted packet - p_encrypted = nullptr; + packetPool.release(p_encrypted); // Release the encrypted packet + p_encrypted = nullptr; } -void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) -{ +void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) { #if ENABLE_JSON_LOGGING - // Even ignored packets get logged in the trace + // Even ignored packets get logged in the trace + p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone + LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); +#elif ARCH_PORTDUINO + // Even ignored packets get logged in the trace + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); -#elif ARCH_PORTDUINO - // Even ignored packets get logged in the trace - if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { - p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone - LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); - } + } #endif - // assert(radioConfig.has_preferences); - if (is_in_repeated(config.lora.ignore_incoming, p->from)) { - LOG_DEBUG("Ignore msg, 0x%x is in our ignore list", p->from); - packetPool.release(p); - return; - } - - meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); - if (node != NULL && node->is_ignored) { - LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); - packetPool.release(p); - return; - } - - if (p->from == NODENUM_BROADCAST) { - LOG_DEBUG("Ignore msg from broadcast address"); - packetPool.release(p); - return; - } - - if (config.lora.ignore_mqtt && p->via_mqtt) { - LOG_DEBUG("Msg came in via MQTT from 0x%x", p->from); - packetPool.release(p); - return; - } - - if (shouldFilterReceived(p)) { - LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from); - packetPool.release(p); - return; - } - - // Note: we avoid calling shouldFilterReceived if we are supposed to ignore certain nodes - because some overrides might - // cache/learn of the existence of nodes (i.e. FloodRouter) that they should not - handleReceived(p); + // assert(radioConfig.has_preferences); + if (is_in_repeated(config.lora.ignore_incoming, p->from)) { + LOG_DEBUG("Ignore msg, 0x%x is in our ignore list", p->from); packetPool.release(p); + return; + } + + meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); + if (node != NULL && node->is_ignored) { + LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); + packetPool.release(p); + return; + } + + if (p->from == NODENUM_BROADCAST) { + LOG_DEBUG("Ignore msg from broadcast address"); + packetPool.release(p); + return; + } + + if (config.lora.ignore_mqtt && p->via_mqtt) { + LOG_DEBUG("Msg came in via MQTT from 0x%x", p->from); + packetPool.release(p); + return; + } + + if (shouldFilterReceived(p)) { + LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from); + packetPool.release(p); + return; + } + + // Note: we avoid calling shouldFilterReceived if we are supposed to ignore certain nodes - because some overrides + // might cache/learn of the existence of nodes (i.e. FloodRouter) that they should not + handleReceived(p); + packetPool.release(p); } diff --git a/src/mesh/Router.h b/src/mesh/Router.h index dbe6f4f39..954a3a3bc 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -12,148 +12,147 @@ /** * A mesh aware router that supports multiple interfaces. */ -class Router : protected concurrency::OSThread, protected PacketHistory -{ - private: - /// Packets which have just arrived from the radio, ready to be processed by this service and possibly - /// forwarded to the phone. - PointerQueue fromRadioQueue; +class Router : protected concurrency::OSThread, protected PacketHistory { +private: + /// Packets which have just arrived from the radio, ready to be processed by this service and possibly + /// forwarded to the phone. + PointerQueue fromRadioQueue; - protected: - RadioInterface *iface = NULL; +protected: + RadioInterface *iface = NULL; - public: - /** - * Constructor - * - */ - Router(); +public: + /** + * Constructor + * + */ + Router(); - /** - * Currently we only allow one interface, that may change in the future - */ - void addInterface(RadioInterface *_iface) { iface = _iface; } + /** + * Currently we only allow one interface, that may change in the future + */ + void addInterface(RadioInterface *_iface) { iface = _iface; } - /** - * do idle processing - * Mostly looking in our incoming rxPacket queue and calling handleReceived. - */ - virtual int32_t runOnce() override; + /** + * do idle processing + * Mostly looking in our incoming rxPacket queue and calling handleReceived. + */ + virtual int32_t runOnce() override; - /** - * Works like send, but if we are sending to the local node, we directly put the message in the receive queue. - * This is the primary method used for sending packets, because it handles both the remote and local cases. - * - * NOTE: This method will free the provided packet (even if we return an error code) - */ - ErrorCode sendLocal(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); + /** + * Works like send, but if we are sending to the local node, we directly put the message in the receive queue. + * This is the primary method used for sending packets, because it handles both the remote and local cases. + * + * NOTE: This method will free the provided packet (even if we return an error code) + */ + ErrorCode sendLocal(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - bool cancelSending(NodeNum from, PacketId id); + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + bool cancelSending(NodeNum from, PacketId id); - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - bool findInTxQueue(NodeNum from, PacketId id); + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + bool findInTxQueue(NodeNum from, PacketId id); - /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. - * The returned packet is guaranteed to have a unique packet ID already assigned - */ - meshtastic_MeshPacket *allocForSending(); + /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. + * The returned packet is guaranteed to have a unique packet ID already assigned + */ + meshtastic_MeshPacket *allocForSending(); - /** Return Underlying interface's TX queue status */ - meshtastic_QueueStatus getQueueStatus(); + /** Return Underlying interface's TX queue status */ + meshtastic_QueueStatus getQueueStatus(); - /** - * @return our local nodenum */ - NodeNum getNodeNum(); + /** + * @return our local nodenum */ + NodeNum getNodeNum(); - /** Wake up the router thread ASAP, because we just queued a message for it. - * FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this queue' - */ - void setReceivedMessage(); + /** Wake up the router thread ASAP, because we just queued a message for it. + * FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this + * queue' + */ + void setReceivedMessage(); - /** - * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for - * freeing the packet - */ - virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p); + /** + * RadioInterface calls this to queue up packets that have been received from the radio. The router is now + * responsible for freeing the packet + */ + virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p); - /** - * Send a packet on a suitable interface. This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - * - * NOTE: This method will free the provided packet (even if we return an error code) - */ - virtual ErrorCode send(meshtastic_MeshPacket *p); - virtual ErrorCode rawSend(meshtastic_MeshPacket *p); + /** + * Send a packet on a suitable interface. This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + * + * NOTE: This method will free the provided packet (even if we return an error code) + */ + virtual ErrorCode send(meshtastic_MeshPacket *p); + virtual ErrorCode rawSend(meshtastic_MeshPacket *p); - /* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it - before us */ - uint32_t rxDupe = 0, txRelayCanceled = 0; + /* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone + did it before us */ + uint32_t rxDupe = 0, txRelayCanceled = 0; - // pointer to the encrypted packet - meshtastic_MeshPacket *p_encrypted = nullptr; + // pointer to the encrypted packet + meshtastic_MeshPacket *p_encrypted = nullptr; - protected: - friend class RoutingModule; +protected: + friend class RoutingModule; - /** - * Should this incoming filter be dropped? - * - * FIXME, move this into the new RoutingModule and do the filtering there using the regular module logic - * - * Called immediately on reception, before any further processing. - * @return true to abandon the packet - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; } + /** + * Should this incoming filter be dropped? + * + * FIXME, move this into the new RoutingModule and do the filtering there using the regular module logic + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; } - /** - * Determine if hop_limit should be decremented for a relay operation. - * Returns false (preserve hop_limit) only if all conditions are met: - * - It's NOT the first hop (first hop must always decrement) - * - Local device is a ROUTER, ROUTER_LATE, or CLIENT_BASE - * - Previous relay is a favorite ROUTER, ROUTER_LATE, or CLIENT_BASE - * - * @param p The packet being relayed - * @return true if hop_limit should be decremented, false to preserve it - */ - bool shouldDecrementHopLimit(const meshtastic_MeshPacket *p); + /** + * Determine if hop_limit should be decremented for a relay operation. + * Returns false (preserve hop_limit) only if all conditions are met: + * - It's NOT the first hop (first hop must always decrement) + * - Local device is a ROUTER, ROUTER_LATE, or CLIENT_BASE + * - Previous relay is a favorite ROUTER, ROUTER_LATE, or CLIENT_BASE + * + * @param p The packet being relayed + * @return true if hop_limit should be decremented, false to preserve it + */ + bool shouldDecrementHopLimit(const meshtastic_MeshPacket *p); - /** - * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to - * update routing tables etc... based on what we overhear (even for messages not destined to our node) - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c); + /** + * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to + * update routing tables etc... based on what we overhear (even for messages not destined to our node) + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c); - /** - * Send an ack or a nak packet back towards whoever sent idFrom - */ - void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, - bool ackWantsAck = false); + /** + * Send an ack or a nak packet back towards whoever sent idFrom + */ + void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); - private: - /** - * Called from loop() - * Handle any packet that is received by an interface on this node. - * Note: some packets may merely being passed through this node and will be forwarded elsewhere. - * - * Note: this packet will never be called for messages sent/generated by this node. - * Note: this method will free the provided packet. - */ - void perhapsHandleReceived(meshtastic_MeshPacket *p); +private: + /** + * Called from loop() + * Handle any packet that is received by an interface on this node. + * Note: some packets may merely being passed through this node and will be forwarded elsewhere. + * + * Note: this packet will never be called for messages sent/generated by this node. + * Note: this method will free the provided packet. + */ + void perhapsHandleReceived(meshtastic_MeshPacket *p); - /** - * Called from perhapsHandleReceived() - allows subclass message delivery behavior. - * Handle any packet that is received by an interface on this node. - * Note: some packets may merely being passed through this node and will be forwarded elsewhere. - * - * Note: this packet will never be called for messages sent/generated by this node. - * Note: this method will free the provided packet. - */ - void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); + /** + * Called from perhapsHandleReceived() - allows subclass message delivery behavior. + * Handle any packet that is received by an interface on this node. + * Note: some packets may merely being passed through this node and will be forwarded elsewhere. + * + * Note: this packet will never be called for messages sent/generated by this node. + * Note: this method will free the provided packet. + */ + void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); - /** Frees the provided packet, and generates a NAK indicating the specifed error while sending */ - void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p); + /** Frees the provided packet, and generates a NAK indicating the specifed error while sending */ + void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p); }; enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_FATAL }; diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index f6e4b3512..c7b37fd47 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -8,37 +8,34 @@ #define STM32WLx_MAX_POWER 22 #endif -STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, - RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) -{ -} +STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) {} -bool STM32WLE5JCInterface::init() -{ - RadioLibInterface::init(); +bool STM32WLE5JCInterface::init() { + RadioLibInterface::init(); // https://github.com/Seeed-Studio/LoRaWan-E5-Node/blob/main/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c #if (!defined(_VARIANT_RAK3172_)) - setTCXOVoltage(1.7); + setTCXOVoltage(1.7); #endif - lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); + lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); - limitPower(STM32WLx_MAX_POWER); + limitPower(STM32WLx_MAX_POWER); - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); - LOG_INFO("STM32WLx init result %d", res); + LOG_INFO("STM32WLx init result %d", res); - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } #endif // ARCH_STM32WL diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h index ee935375e..0ba49f4d2 100644 --- a/src/mesh/STM32WLE5JCInterface.h +++ b/src/mesh/STM32WLE5JCInterface.h @@ -7,13 +7,11 @@ /** * Our adapter for STM32WLE5JC radios */ -class STM32WLE5JCInterface : public SX126xInterface -{ - public: - STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +class STM32WLE5JCInterface : public SX126xInterface { +public: + STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - virtual bool init() override; + virtual bool init() override; }; #endif // ARCH_STM32WL \ No newline at end of file diff --git a/src/mesh/SX1262Interface.cpp b/src/mesh/SX1262Interface.cpp index 4c0dea00b..7eff8a8c3 100644 --- a/src/mesh/SX1262Interface.cpp +++ b/src/mesh/SX1262Interface.cpp @@ -3,9 +3,6 @@ #include "configuration.h" #include "error.h" -SX1262Interface::SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) -{ -} +SX1262Interface::SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) {} #endif \ No newline at end of file diff --git a/src/mesh/SX1262Interface.h b/src/mesh/SX1262Interface.h index 6e4616c8b..cd8d425f2 100644 --- a/src/mesh/SX1262Interface.h +++ b/src/mesh/SX1262Interface.h @@ -6,10 +6,8 @@ /** * Our adapter for SX1262 radios */ -class SX1262Interface : public SX126xInterface -{ - public: - SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +class SX1262Interface : public SX126xInterface { +public: + SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/SX1268Interface.cpp b/src/mesh/SX1268Interface.cpp index fe6e9af89..edc8633c9 100644 --- a/src/mesh/SX1268Interface.cpp +++ b/src/mesh/SX1268Interface.cpp @@ -3,18 +3,14 @@ #include "configuration.h" #include "error.h" -SX1268Interface::SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) -{ -} +SX1268Interface::SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) {} -float SX1268Interface::getFreq() -{ - // Set frequency to default of EU_433 if outside of allowed range (e.g. when region is UNSET) - if (savedFreq < 410 || savedFreq > 810) - return 433.125f; - else - return savedFreq; +float SX1268Interface::getFreq() { + // Set frequency to default of EU_433 if outside of allowed range (e.g. when region is UNSET) + if (savedFreq < 410 || savedFreq > 810) + return 433.125f; + else + return savedFreq; } #endif \ No newline at end of file diff --git a/src/mesh/SX1268Interface.h b/src/mesh/SX1268Interface.h index cc6dd3534..4c7eaa3d8 100644 --- a/src/mesh/SX1268Interface.h +++ b/src/mesh/SX1268Interface.h @@ -6,12 +6,10 @@ /** * Our adapter for SX1268 radios */ -class SX1268Interface : public SX126xInterface -{ - public: - virtual float getFreq() override; +class SX1268Interface : public SX126xInterface { +public: + virtual float getFreq() override; - SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); + SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index e1f07a32b..5b30dfc49 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -9,8 +9,8 @@ #include "Throttle.h" -// Particular boards might define a different max power based on what their hardware can do, default to max power output if not -// specified (may be dangerous if using external PA and SX126x power config forgotten) +// Particular boards might define a different max power based on what their hardware can do, default to max power output +// if not specified (may be dangerous if using external PA and SX126x power config forgotten) #if ARCH_PORTDUINO #define SX126X_MAX_POWER portduino_config.sx126x_max_power #endif @@ -21,135 +21,133 @@ template SX126xInterface::SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) -{ - LOG_DEBUG("SX126xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { + LOG_DEBUG("SX126xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -template bool SX126xInterface::init() -{ +template bool SX126xInterface::init() { -// Typically, the RF switch on SX126x boards is controlled by two signals, which are negations of each other (switched RFIO -// paths). The negation is usually performed in hardware, or (suboptimal design) TXEN and RXEN are the two inputs to this style of -// RF switch. On some boards, there is no hardware negation between CTRL and ¬CTRL, but CTRL is internally connected to DIO2, and -// DIO2's switching is done by the SX126X itself, so the MCU can't control ¬CTRL at exactly the same time. One solution would be -// to set ¬CTRL as SX126X_TXEN or SX126X_RXEN, but they may already be used for another purpose, such as controlling another -// PA/LNA. Keeping ¬CTRL high seems to work, as long CTRL=1, ¬CTRL=1 has the opposite and stable RF path effect as CTRL=0 and -// ¬CTRL=1, this depends on the RF switch, but it seems this usually works. Better hardware design, which is done most the time, -// means this workaround is not necessary. -#ifdef SX126X_ANT_SW // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly - // used and not part of the 'default' set of pin definitions. - digitalWrite(SX126X_ANT_SW, HIGH); - pinMode(SX126X_ANT_SW, OUTPUT); +// Typically, the RF switch on SX126x boards is controlled by two signals, which are negations of each other (switched +// RFIO paths). The negation is usually performed in hardware, or (suboptimal design) TXEN and RXEN are the two inputs +// to this style of RF switch. On some boards, there is no hardware negation between CTRL and ¬CTRL, but CTRL is +// internally connected to DIO2, and DIO2's switching is done by the SX126X itself, so the MCU can't control ¬CTRL at +// exactly the same time. One solution would be to set ¬CTRL as SX126X_TXEN or SX126X_RXEN, but they may already be used +// for another purpose, such as controlling another PA/LNA. Keeping ¬CTRL high seems to work, as long CTRL=1, ¬CTRL=1 +// has the opposite and stable RF path effect as CTRL=0 and ¬CTRL=1, this depends on the RF switch, but it seems this +// usually works. Better hardware design, which is done most the time, means this workaround is not necessary. +#ifdef SX126X_ANT_SW // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not + // commonly used and not part of the 'default' set of pin definitions. + digitalWrite(SX126X_ANT_SW, HIGH); + pinMode(SX126X_ANT_SW, OUTPUT); #endif -#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly - // used and not part of the 'default' set of pin definitions. - digitalWrite(SX126X_POWER_EN, HIGH); - pinMode(SX126X_POWER_EN, OUTPUT); +#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not + // commonly used and not part of the 'default' set of pin definitions. + digitalWrite(SX126X_POWER_EN, HIGH); + pinMode(SX126X_POWER_EN, OUTPUT); #endif #if defined(USE_GC1109_PA) - pinMode(LORA_PA_POWER, OUTPUT); - digitalWrite(LORA_PA_POWER, HIGH); + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); - pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, LOW); - pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); + pinMode(LORA_PA_EN, OUTPUT); + digitalWrite(LORA_PA_EN, LOW); + pinMode(LORA_PA_TX_EN, OUTPUT); + digitalWrite(LORA_PA_TX_EN, LOW); #endif #if ARCH_PORTDUINO - tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; - if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); - pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); - } + tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; + if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); + pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); + } #endif - if (tcxoVoltage == 0.0) - LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); - else - LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); - setTransmitEnable(false); - // FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option - bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC? + if (tcxoVoltage == 0.0) + LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); + else + LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); + setTransmitEnable(false); + // FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option + bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC? - RadioLibInterface::init(); + RadioLibInterface::init(); - limitPower(SX126X_MAX_POWER); - // Make sure we reach the minimum power supported to turn the chip on (-9dBm) - if (power < -9) - power = -9; + limitPower(SX126X_MAX_POWER); + // Make sure we reach the minimum power supported to turn the chip on (-9dBm) + if (power < -9) + power = -9; - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); - // \todo Display actual typename of the adapter, not just `SX126x` - LOG_INFO("SX126x init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) - return false; + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); + // \todo Display actual typename of the adapter, not just `SX126x` + LOG_INFO("SX126x init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); - // Overriding current limit - // (https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.cpp#L85) using - // value in SX126xInterface.h (currently 140 mA) It may or may not be necessary, depending on how RadioLib functions, from - // SX1261/2 datasheet: OCP after setting DeviceSel with SetPaConfig(): SX1261 - 60 mA, SX1262 - 140 mA For the SX1268 the IC - // defaults to 140mA no matter the set power level, but RadioLib set it lower, this would need further checking Default values - // are: SX1262, SX1268: 0x38 (140 mA), SX1261: 0x18 (60 mA) - // FIXME: Not ideal to increase SX1261 current limit above 60mA as it can only transmit max 15dBm, should probably only do it - // if using SX1262 or SX1268 - res = lora.setCurrentLimit(currentLimit); - LOG_DEBUG("Current limit set to %f", currentLimit); - LOG_DEBUG("Current limit set result %d", res); + // Overriding current limit + // (https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.cpp#L85) + // using value in SX126xInterface.h (currently 140 mA) It may or may not be necessary, depending on how RadioLib + // functions, from SX1261/2 datasheet: OCP after setting DeviceSel with SetPaConfig(): SX1261 - 60 mA, SX1262 - 140 mA + // For the SX1268 the IC defaults to 140mA no matter the set power level, but RadioLib set it lower, this would need + // further checking Default values are: SX1262, SX1268: 0x38 (140 mA), SX1261: 0x18 (60 mA) + // FIXME: Not ideal to increase SX1261 current limit above 60mA as it can only transmit max 15dBm, should probably + // only do it if using SX1262 or SX1268 + res = lora.setCurrentLimit(currentLimit); + LOG_DEBUG("Current limit set to %f", currentLimit); + LOG_DEBUG("Current limit set result %d", res); - if (res == RADIOLIB_ERR_NONE) { + if (res == RADIOLIB_ERR_NONE) { #ifdef SX126X_DIO2_AS_RF_SWITCH - bool dio2AsRfSwitch = true; + bool dio2AsRfSwitch = true; #elif defined(ARCH_PORTDUINO) - bool dio2AsRfSwitch = false; - if (portduino_config.dio2_as_rf_switch) { - dio2AsRfSwitch = true; - } + bool dio2AsRfSwitch = false; + if (portduino_config.dio2_as_rf_switch) { + dio2AsRfSwitch = true; + } #else - bool dio2AsRfSwitch = false; + bool dio2AsRfSwitch = false; #endif - res = lora.setDio2AsRfSwitch(dio2AsRfSwitch); - LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res); - } + res = lora.setDio2AsRfSwitch(dio2AsRfSwitch); + LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res); + } -// If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as it has -// no effect +// If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as +// it has no effect #if ARCH_PORTDUINO - if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, - portduino_config.lora_txen_pin.pin); - lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); - } + if (res == RADIOLIB_ERR_NONE) { + LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, + portduino_config.lora_txen_pin.pin); + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); + } #else #ifndef SX126X_RXEN #define SX126X_RXEN RADIOLIB_NC - LOG_DEBUG("SX126X_RXEN not defined, defaulting to RADIOLIB_NC"); + LOG_DEBUG("SX126X_RXEN not defined, defaulting to RADIOLIB_NC"); #endif #ifndef SX126X_TXEN #define SX126X_TXEN RADIOLIB_NC - LOG_DEBUG("SX126X_TXEN not defined, defaulting to RADIOLIB_NC"); + LOG_DEBUG("SX126X_TXEN not defined, defaulting to RADIOLIB_NC"); #endif - if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN); - lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); - } + if (res == RADIOLIB_ERR_NONE) { + LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN); + lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); + } #endif - if (config.lora.sx126x_rx_boosted_gain) { - uint16_t result = lora.setRxBoostedGainMode(true); - LOG_INFO("Set RX gain to boosted mode; result: %d", result); - } else { - uint16_t result = lora.setRxBoostedGainMode(false); - LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); - } + if (config.lora.sx126x_rx_boosted_gain) { + uint16_t result = lora.setRxBoostedGainMode(true); + LOG_INFO("Set RX gain to boosted mode; result: %d", result); + } else { + uint16_t result = lora.setRxBoostedGainMode(false); + LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); + } #if 0 // Read/write a register we are not using (only used for FSK mode) to test SPI comms @@ -175,203 +173,192 @@ template bool SX126xInterface::init() // If we got this far register accesses (and therefore SPI comms) are good #endif - if (res == RADIOLIB_ERR_NONE) - res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON); + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -template bool SX126xInterface::reconfigure() -{ - RadioLibInterface::reconfigure(); +template bool SX126xInterface::reconfigure() { + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setBandwidth(bw); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setSyncWord(syncWord); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setSyncWord %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setSyncWord %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setCurrentLimit(currentLimit); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setCurrentLimit %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setCurrentLimit(currentLimit); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setCurrentLimit %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setPreambleLength(preambleLength); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setPreambleLength %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setPreambleLength(preambleLength); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setPreambleLength %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX126X_MAX_POWER) // This chip has lower power limits than some - power = SX126X_MAX_POWER; + if (power > SX126X_MAX_POWER) // This chip has lower power limits than some + power = SX126X_MAX_POWER; - err = lora.setOutputPower(power); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setOutputPower(power); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() -{ - lora.clearDio1Action(); -} +template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() { lora.clearDio1Action(); } -template void SX126xInterface::setStandby() -{ - checkNotification(); // handle any pending interrupts before we force standby +template void SX126xInterface::setStandby() { + checkNotification(); // handle any pending interrupts before we force standby - int err = lora.standby(); + int err = lora.standby(); - if (err != RADIOLIB_ERR_NONE) - LOG_DEBUG("SX126x standby %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) + LOG_DEBUG("SX126x standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = false; // If we were receiving, not any more - activeReceiveStart = 0; - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** * Add SNR data to received messages */ -template void SX126xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) -{ - // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); - mp->rx_snr = lora.getSNR(); - mp->rx_rssi = lround(lora.getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); +template void SX126xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) { + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ -template void SX126xInterface::configHardwareForSend() -{ - setTransmitEnable(true); - RadioLibInterface::configHardwareForSend(); +template void SX126xInterface::configHardwareForSend() { + setTransmitEnable(true); + RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY -template void SX126xInterface::startReceive() -{ +template void SX126xInterface::startReceive() { #ifdef SLEEP_ONLY - sleep(); + sleep(); #else - setTransmitEnable(false); - setStandby(); + setTransmitEnable(false); + setStandby(); - // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. - int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - RadioLibInterface::startReceive(); + RadioLibInterface::startReceive(); - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register + // bits + enableInterrupt(isrRxLevel0); #endif } /** Is the channel currently active? */ -template bool SX126xInterface::isChannelActive() -{ - // check if we can detect a LoRa preamble on the current channel - ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, - .detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, - .detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, - .exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, - .timeout = 0, - .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, - .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; - int16_t result; - setTransmitEnable(false); - setStandby(); - result = lora.scanChannel(cfg); - if (result == RADIOLIB_LORA_DETECTED) - return true; - if (result != RADIOLIB_CHANNEL_FREE) - LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); - assert(result != RADIOLIB_ERR_WRONG_MODEM); +template bool SX126xInterface::isChannelActive() { + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; + setTransmitEnable(false); + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; + if (result != RADIOLIB_CHANNEL_FREE) + LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - return false; + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -template bool SX126xInterface::isActivelyReceiving() -{ - // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet - // received and handled the interrupt for reading the packet/handling errors. - return receiveDetected(lora.getIrqFlags(), RADIOLIB_SX126X_IRQ_HEADER_VALID, RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); +template bool SX126xInterface::isActivelyReceiving() { + // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet + // received and handled the interrupt for reading the packet/handling errors. + return receiveDetected(lora.getIrqFlags(), RADIOLIB_SX126X_IRQ_HEADER_VALID, RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); } -template bool SX126xInterface::sleep() -{ - // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet - // \todo Display actual typename of the adapter, not just `SX126x` - LOG_DEBUG("SX126x entering sleep mode"); // (FIXME, don't keep config) - setStandby(); // Stop any pending operations +template bool SX126xInterface::sleep() { + // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet + // \todo Display actual typename of the adapter, not just `SX126x` + LOG_DEBUG("SX126x entering sleep mode"); // (FIXME, don't keep config) + setStandby(); // Stop any pending operations - // turn off TCXO if it was powered - // FIXME - this isn't correct - // lora.setTCXO(0); + // turn off TCXO if it was powered + // FIXME - this isn't correct + // lora.setTCXO(0); - // put chipset into sleep mode (we've already disabled interrupts by now) - bool keepConfig = true; - lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = true; + lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed #ifdef SX126X_POWER_EN - digitalWrite(SX126X_POWER_EN, LOW); + digitalWrite(SX126X_POWER_EN, LOW); #endif #if defined(USE_GC1109_PA) - /* - * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. - * // digitalWrite(LORA_PA_POWER, LOW); - */ - digitalWrite(LORA_PA_EN, LOW); - digitalWrite(LORA_PA_TX_EN, LOW); + /* + * Do not switch the power on and off frequently. + * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. + * // digitalWrite(LORA_PA_POWER, LOW); + */ + digitalWrite(LORA_PA_EN, LOW); + digitalWrite(LORA_PA_TX_EN, LOW); #endif - return true; + return true; } /** Some boards require GPIO control of tx vs rx paths */ -template void SX126xInterface::setTransmitEnable(bool txon) -{ +template void SX126xInterface::setTransmitEnable(bool txon) { #if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); - digitalWrite(LORA_PA_EN, HIGH); - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); + digitalWrite(LORA_PA_POWER, HIGH); + digitalWrite(LORA_PA_EN, HIGH); + digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); #endif } diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index b8f16ac6d..e47f28e38 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -7,75 +7,73 @@ * \brief Adapter for SX126x radio family. Implements common logic for child classes. * \tparam T RadioLib module type for SX126x: SX1262, SX1268. */ -template class SX126xInterface : public RadioLibInterface -{ - public: - SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +template class SX126xInterface : public RadioLibInterface { +public: + SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - bool isIRQPending() override { return lora.getIrqFlags() != 0; } + bool isIRQPending() override { return lora.getIrqFlags() != 0; } - void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } + void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } - protected: - float currentLimit = 140; // Higher OCP limit for SX126x PA - float tcxoVoltage = 0.0; +protected: + float currentLimit = 140; // Higher OCP limit for SX126x PA + float tcxoVoltage = 0.0; - /** - * Specific module instance - */ - T lora; + /** + * Specific module instance + */ + T lora; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } - private: - /** Some boards require GPIO control of tx vs rx paths */ - void setTransmitEnable(bool txon); +private: + /** Some boards require GPIO control of tx vs rx paths */ + void setTransmitEnable(bool txon); }; #endif \ No newline at end of file diff --git a/src/mesh/SX1280Interface.cpp b/src/mesh/SX1280Interface.cpp index 9e0d42122..5e0436b3c 100644 --- a/src/mesh/SX1280Interface.cpp +++ b/src/mesh/SX1280Interface.cpp @@ -3,9 +3,6 @@ #include "configuration.h" #include "error.h" -SX1280Interface::SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : SX128xInterface(hal, cs, irq, rst, busy) -{ -} +SX1280Interface::SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : SX128xInterface(hal, cs, irq, rst, busy) {} #endif \ No newline at end of file diff --git a/src/mesh/SX1280Interface.h b/src/mesh/SX1280Interface.h index 534dd8084..c0c39626e 100644 --- a/src/mesh/SX1280Interface.h +++ b/src/mesh/SX1280Interface.h @@ -6,10 +6,8 @@ * Our adapter for SX1280 radios */ -class SX1280Interface : public SX128xInterface -{ - public: - SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +class SX1280Interface : public SX128xInterface { +public: + SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); }; #endif diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 80872af07..9b5285168 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -20,307 +20,291 @@ template SX128xInterface::SX128xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) -{ - LOG_DEBUG("SX128xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { + LOG_DEBUG("SX128xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -template bool SX128xInterface::init() -{ +template bool SX128xInterface::init() { #ifdef SX128X_POWER_EN - pinMode(SX128X_POWER_EN, OUTPUT); - digitalWrite(SX128X_POWER_EN, HIGH); + pinMode(SX128X_POWER_EN, OUTPUT); + digitalWrite(SX128X_POWER_EN, HIGH); #endif #ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, 1); + pinMode(RF95_FAN_EN, OUTPUT); + digitalWrite(RF95_FAN_EN, 1); #endif #if ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output - } - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output + } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output + } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // set not rx or tx mode - pinMode(SX128X_RXEN, OUTPUT); - digitalWrite(SX128X_RXEN, LOW); // Set low before becoming an output + pinMode(SX128X_RXEN, OUTPUT); + digitalWrite(SX128X_RXEN, LOW); // Set low before becoming an output #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) - pinMode(SX128X_TXEN, OUTPUT); - digitalWrite(SX128X_TXEN, LOW); + pinMode(SX128X_TXEN, OUTPUT); + digitalWrite(SX128X_TXEN, LOW); #endif #endif - RadioLibInterface::init(); + RadioLibInterface::init(); - limitPower(SX128X_MAX_POWER); + limitPower(SX128X_MAX_POWER); - preambleLength = 12; // 12 is the default for this chip, 32 does not RX at all + preambleLength = 12; // 12 is the default for this chip, 32 does not RX at all - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); - // \todo Display actual typename of the adapter, not just `SX128x` - LOG_INFO("SX128x init result %d", res); + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); + // \todo Display actual typename of the adapter, not just `SX128x` + LOG_INFO("SX128x init result %d", res); - if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { - LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; - nodeDB->saveToDisk(SEGMENT_CONFIG); - delay(2000); + if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { + LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + nodeDB->saveToDisk(SEGMENT_CONFIG); + delay(2000); #if defined(ARCH_ESP32) - ESP.restart(); + ESP.restart(); #elif defined(ARCH_NRF52) - NVIC_SystemReset(); + NVIC_SystemReset(); #else - LOG_ERROR("FIXME implement reboot for this platform. Skip for now"); + LOG_ERROR("FIXME implement reboot for this platform. Skip for now"); #endif - } + } - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) && defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) - if (res == RADIOLIB_ERR_NONE) { - lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN); - } + if (res == RADIOLIB_ERR_NONE) { + lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN); + } #elif ARCH_PORTDUINO - if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && - portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); - } + if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); + } #endif - if (res == RADIOLIB_ERR_NONE) - res = lora.setCRC(2); + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -template bool SX128xInterface::reconfigure() -{ - RadioLibInterface::reconfigure(); +template bool SX128xInterface::reconfigure() { + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setBandwidth(bw); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setSyncWord(syncWord); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X setSyncWord %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X setSyncWord %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setPreambleLength(preambleLength); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X setPreambleLength %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setPreambleLength(preambleLength); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X setPreambleLength %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX128X_MAX_POWER) // This chip has lower power limits than some - power = SX128X_MAX_POWER; + if (power > SX128X_MAX_POWER) // This chip has lower power limits than some + power = SX128X_MAX_POWER; - err = lora.setOutputPower(power); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X setOutputPower %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setOutputPower(power); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X setOutputPower %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() -{ - lora.clearDio1Action(); -} +template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() { lora.clearDio1Action(); } -template bool SX128xInterface::wideLora() -{ - return true; -} +template bool SX128xInterface::wideLora() { return true; } -template void SX128xInterface::setStandby() -{ - checkNotification(); // handle any pending interrupts before we force standby +template void SX128xInterface::setStandby() { + checkNotification(); // handle any pending interrupts before we force standby - int err = lora.standby(); + int err = lora.standby(); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128x standby %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128x standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); #if ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); - } - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, LOW); - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); + } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); + } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn off RX and TX power - digitalWrite(SX128X_RXEN, LOW); + digitalWrite(SX128X_RXEN, LOW); #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) - digitalWrite(SX128X_TXEN, LOW); + digitalWrite(SX128X_TXEN, LOW); #endif #endif - isReceiving = false; // If we were receiving, not any more - activeReceiveStart = 0; - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** * Add SNR data to received messages */ -template void SX128xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) -{ - // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); - mp->rx_snr = lora.getSNR(); - mp->rx_rssi = lround(lora.getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); +template void SX128xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) { + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ -template void SX128xInterface::configHardwareForSend() -{ +template void SX128xInterface::configHardwareForSend() { #if ARCH_PORTDUINO - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); - } - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); - } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); + } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); + } #else #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn on TX power / off RX power - digitalWrite(SX128X_TXEN, HIGH); + digitalWrite(SX128X_TXEN, HIGH); #endif #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) - digitalWrite(SX128X_RXEN, LOW); + digitalWrite(SX128X_RXEN, LOW); #endif #endif - RadioLibInterface::configHardwareForSend(); + RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY -template void SX128xInterface::startReceive() -{ +template void SX128xInterface::startReceive() { #ifdef SLEEP_ONLY - sleep(); + sleep(); #else - setStandby(); + setStandby(); #if ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); - } - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, LOW); - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); + } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); + } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn on RX power / off TX power - digitalWrite(SX128X_RXEN, HIGH); + digitalWrite(SX128X_RXEN, HIGH); #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) - digitalWrite(SX128X_TXEN, LOW); + digitalWrite(SX128X_TXEN, LOW); #endif #endif - int err = lora.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); + int err = lora.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X startReceive %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X startReceive %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - RadioLibInterface::startReceive(); + RadioLibInterface::startReceive(); - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register + // bits + enableInterrupt(isrRxLevel0); #endif } /** Is the channel currently active? */ -template bool SX128xInterface::isChannelActive() -{ - // check if we can detect a LoRa preamble on the current channel - ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD_24GHZ, - .detPeak = 0, - .detMin = 0, - .exitMode = 0, - .timeout = 0, - .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, - .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; - int16_t result; +template bool SX128xInterface::isChannelActive() { + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD_24GHZ, + .detPeak = 0, + .detMin = 0, + .exitMode = 0, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; - setStandby(); - result = lora.scanChannel(cfg); - if (result == RADIOLIB_LORA_DETECTED) - return true; - if (result != RADIOLIB_CHANNEL_FREE) - LOG_ERROR("SX128X scanChannel %s%d", radioLibErr, result); - assert(result != RADIOLIB_ERR_WRONG_MODEM); + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; + if (result != RADIOLIB_CHANNEL_FREE) + LOG_ERROR("SX128X scanChannel %s%d", radioLibErr, result); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - return false; + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -template bool SX128xInterface::isActivelyReceiving() -{ - return receiveDetected(lora.getIrqStatus(), RADIOLIB_SX128X_IRQ_HEADER_VALID, RADIOLIB_SX128X_IRQ_PREAMBLE_DETECTED); +template bool SX128xInterface::isActivelyReceiving() { + return receiveDetected(lora.getIrqStatus(), RADIOLIB_SX128X_IRQ_HEADER_VALID, RADIOLIB_SX128X_IRQ_PREAMBLE_DETECTED); } -template bool SX128xInterface::sleep() -{ - // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet - // \todo Display actual typename of the adapter, not just `SX128x` - LOG_DEBUG("SX128x entering sleep mode"); // (FIXME, don't keep config) - setStandby(); // Stop any pending operations +template bool SX128xInterface::sleep() { + // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet + // \todo Display actual typename of the adapter, not just `SX128x` + LOG_DEBUG("SX128x entering sleep mode"); // (FIXME, don't keep config) + setStandby(); // Stop any pending operations - // turn off TCXO if it was powered - // FIXME - this isn't correct - // lora.setTCXO(0); + // turn off TCXO if it was powered + // FIXME - this isn't correct + // lora.setTCXO(0); - // put chipset into sleep mode (we've already disabled interrupts by now) - bool keepConfig = true; - lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = true; + lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed #ifdef SX128X_POWER_EN - digitalWrite(SX128X_POWER_EN, LOW); + digitalWrite(SX128X_POWER_EN, LOW); #endif - return true; + return true; } #endif \ No newline at end of file diff --git a/src/mesh/SX128xInterface.h b/src/mesh/SX128xInterface.h index acdcbbb27..7a3246f9c 100644 --- a/src/mesh/SX128xInterface.h +++ b/src/mesh/SX128xInterface.h @@ -6,67 +6,65 @@ * \brief Adapter for SX128x radio family. Implements common logic for child classes. * \tparam T RadioLib module type for SX128x: SX1280. */ -template class SX128xInterface : public RadioLibInterface -{ - public: - SX128xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy); +template class SX128xInterface : public RadioLibInterface { +public: + SX128xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - virtual bool wideLora() override; + virtual bool wideLora() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - bool isIRQPending() override { return lora.getIrqFlags() != 0; } + bool isIRQPending() override { return lora.getIrqFlags() != 0; } - protected: - /** - * Specific module instance - */ - T lora; +protected: + /** + * Specific module instance + */ + T lora; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; diff --git a/src/mesh/SinglePortModule.h b/src/mesh/SinglePortModule.h index e43de09d1..19de88abf 100644 --- a/src/mesh/SinglePortModule.h +++ b/src/mesh/SinglePortModule.h @@ -6,34 +6,32 @@ * Most modules are only interested in sending/receiving one particular portnum. This baseclass simplifies that common * case. */ -class SinglePortModule : public MeshModule -{ - protected: - meshtastic_PortNum ourPortNum; +class SinglePortModule : public MeshModule { +protected: + meshtastic_PortNum ourPortNum; - public: - /** Constructor - * name is for debugging output - */ - SinglePortModule(const char *_name, meshtastic_PortNum _ourPortNum) : MeshModule(_name), ourPortNum(_ourPortNum) {} +public: + /** Constructor + * name is for debugging output + */ + SinglePortModule(const char *_name, meshtastic_PortNum _ourPortNum) : MeshModule(_name), ourPortNum(_ourPortNum) {} - protected: - /** - * @return true if you want to receive the specified portnum - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } +protected: + /** + * @return true if you want to receive the specified portnum + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } - /** - * Return a mesh packet which has been preinited as a data packet with a particular port number. - * You can then send this packet (after customizing any of the payload fields you might need) with - * service->sendToMesh() - */ - meshtastic_MeshPacket *allocDataPacket() - { - // Update our local node info with our position (even if we don't decide to update anyone else) - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = ourPortNum; + /** + * Return a mesh packet which has been preinited as a data packet with a particular port number. + * You can then send this packet (after customizing any of the payload fields you might need) with + * service->sendToMesh() + */ + meshtastic_MeshPacket *allocDataPacket() { + // Update our local node info with our position (even if we don't decide to update anyone else) + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = ourPortNum; - return p; - } + return p; + } }; \ No newline at end of file diff --git a/src/mesh/StaticPointerQueue.h b/src/mesh/StaticPointerQueue.h index 398ee450c..2a748dc10 100644 --- a/src/mesh/StaticPointerQueue.h +++ b/src/mesh/StaticPointerQueue.h @@ -9,69 +9,64 @@ * This provides the same interface as PointerQueue but uses a statically allocated * buffer instead of dynamic allocation. */ -template class StaticPointerQueue -{ - static_assert(MaxElements > 0, "MaxElements must be greater than 0"); +template class StaticPointerQueue { + static_assert(MaxElements > 0, "MaxElements must be greater than 0"); - T *buffer[MaxElements]; - int head = 0; - int tail = 0; - int count = 0; - concurrency::OSThread *reader = nullptr; + T *buffer[MaxElements]; + int head = 0; + int tail = 0; + int count = 0; + concurrency::OSThread *reader = nullptr; - public: - StaticPointerQueue() - { - // Initialize all buffer elements to nullptr to silence warnings and ensure clean state - for (int i = 0; i < MaxElements; i++) { - buffer[i] = nullptr; - } +public: + StaticPointerQueue() { + // Initialize all buffer elements to nullptr to silence warnings and ensure clean state + for (int i = 0; i < MaxElements; i++) { + buffer[i] = nullptr; + } + } + + int numFree() const { return MaxElements - count; } + + bool isEmpty() const { return count == 0; } + + int numUsed() const { return count; } + + bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) { + if (count >= MaxElements) { + return false; // Queue is full } - int numFree() const { return MaxElements - count; } - - bool isEmpty() const { return count == 0; } - - int numUsed() const { return count; } - - bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) - { - if (count >= MaxElements) { - return false; // Queue is full - } - - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interrupt(); - } - - buffer[tail] = x; - tail = (tail + 1) % MaxElements; - count++; - return true; + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); } - bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) - { - if (count == 0) { - return false; // Queue is empty - } + buffer[tail] = x; + tail = (tail + 1) % MaxElements; + count++; + return true; + } - *p = buffer[head]; - head = (head + 1) % MaxElements; - count--; - return true; + bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) { + if (count == 0) { + return false; // Queue is empty } - // returns a ptr or null if the queue was empty - T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) - { - T *p; - return dequeue(&p, maxWait) ? p : nullptr; - } + *p = buffer[head]; + head = (head + 1) % MaxElements; + count--; + return true; + } - void setReader(concurrency::OSThread *t) { reader = t; } + // returns a ptr or null if the queue was empty + T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) { + T *p; + return dequeue(&p, maxWait) ? p : nullptr; + } - // For compatibility with PointerQueue interface - int getMaxLen() const { return MaxElements; } + void setReader(concurrency::OSThread *t) { reader = t; } + + // For compatibility with PointerQueue interface + int getMaxLen() const { return MaxElements; } }; diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 20026767e..2578d83bc 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -8,220 +8,211 @@ #define START2 0xc3 #define HEADER_LEN 4 -int32_t StreamAPI::runOncePart() -{ - auto result = readStream(); - writeStream(); - checkConnectionTimeout(); - return result; +int32_t StreamAPI::runOncePart() { + auto result = readStream(); + writeStream(); + checkConnectionTimeout(); + return result; } -int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) -{ - auto result = readStream(buf, bufLen); - writeStream(); - checkConnectionTimeout(); - return result; +int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) { + auto result = readStream(buf, bufLen); + writeStream(); + checkConnectionTimeout(); + return result; } /** * Read any rx chars from the link and call handleRecStream */ -int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) -{ - if (bufLen < 1) { - // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time - bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); - return recentRx ? 5 : 250; - } else { - handleRecStream(buf, bufLen); - // we had bytes available this time, so assume we might have them next time also - lastRxMsec = millis(); - return 0; - } +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) { + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a + // long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } } /** * call getFromRadio() and deliver encapsulated packets to the Stream */ -void StreamAPI::writeStream() -{ - if (canWrite) { - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - emitTxBuffer(len); - } while (len); - } +void StreamAPI::writeStream() { + if (canWrite) { + uint32_t len; + do { + // Send every packet we can + len = getFromRadio(txBuf + HEADER_LEN); + emitTxBuffer(len); + } while (len); + } } -int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) -{ - uint16_t index = 0; - while (bufLen > index) { // Currently we never want to block - int cInt = buf[index++]; - if (cInt < 0) - break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit - // arduino +int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) { + uint16_t index = 0; + while (bufLen > index) { // Currently we never want to block + int cInt = buf[index++]; + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino - uint8_t c = (uint8_t)cInt; + uint8_t c = (uint8_t)cInt; - // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload - size_t ptr = rxPtr; + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; - rxPtr++; // assume we will probably advance the rxPtr - rxBuf[ptr] = c; // store all bytes (including framing) + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) - // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); - if (ptr == 0) { // looking for START1 - if (c != START1) - rxPtr = 0; // failed to find framing - } else if (ptr == 1) { // looking for START2 - if (c != START2) - rxPtr = 0; // failed to find framing - } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing - uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing - // console->printf("len %d\n", len); + // console->printf("len %d\n", len); - if (ptr == HEADER_LEN - 1) { - // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid - // protobuf also) - if (len > MAX_TO_FROM_RADIO_SIZE) - rxPtr = 0; // length is bogus, restart search for framing - } + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } - if (rxPtr != 0) // Is packet still considered 'good'? - if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? - rxPtr = 0; // start over again on the next packet + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet - // If we didn't just fail the packet and we now have the right # of bytes, parse it - handleToRadio(rxBuf + HEADER_LEN, len); - } + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); } } - return 0; + } + return 0; } /** * Read any rx chars from the link and call handleToRadio */ -int32_t StreamAPI::readStream() -{ - if (!stream->available()) { - // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time - bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); - return recentRx ? 5 : 250; - } else { - while (stream->available()) { // Currently we never want to block - int cInt = stream->read(); - if (cInt < 0) - break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit - // arduino +int32_t StreamAPI::readStream() { + if (!stream->available()) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a + // long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + while (stream->available()) { // Currently we never want to block + int cInt = stream->read(); + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino - uint8_t c = (uint8_t)cInt; + uint8_t c = (uint8_t)cInt; - // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload - size_t ptr = rxPtr; + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; - rxPtr++; // assume we will probably advance the rxPtr - rxBuf[ptr] = c; // store all bytes (including framing) + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) - // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); - if (ptr == 0) { // looking for START1 - if (c != START1) - rxPtr = 0; // failed to find framing - } else if (ptr == 1) { // looking for START2 - if (c != START2) - rxPtr = 0; // failed to find framing - } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing - uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing - // console->printf("len %d\n", len); + // console->printf("len %d\n", len); - if (ptr == HEADER_LEN - 1) { - // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid - // protobuf also) - if (len > MAX_TO_FROM_RADIO_SIZE) - rxPtr = 0; // length is bogus, restart search for framing - } - - if (rxPtr != 0) // Is packet still considered 'good'? - if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? - rxPtr = 0; // start over again on the next packet - - // If we didn't just fail the packet and we now have the right # of bytes, parse it - handleToRadio(rxBuf + HEADER_LEN, len); - } - } + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing } - // we had bytes available this time, so assume we might have them next time also - lastRxMsec = millis(); - return 0; + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } } + + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } } /** * Send the current txBuffer over our stream */ -void StreamAPI::emitTxBuffer(size_t len) -{ - if (len != 0) { - txBuf[0] = START1; - txBuf[1] = START2; - txBuf[2] = (len >> 8) & 0xff; - txBuf[3] = len & 0xff; +void StreamAPI::emitTxBuffer(size_t len) { + if (len != 0) { + txBuf[0] = START1; + txBuf[1] = START2; + txBuf[2] = (len >> 8) & 0xff; + txBuf[3] = len & 0xff; - auto totalLen = len + HEADER_LEN; - stream->write(txBuf, totalLen); - stream->flush(); - } + auto totalLen = len + HEADER_LEN; + stream->write(txBuf, totalLen); + stream->flush(); + } } -void StreamAPI::emitRebooted() -{ - // In case we send a FromRadio packet - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_rebooted_tag; - fromRadioScratch.rebooted = true; +void StreamAPI::emitRebooted() { + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_rebooted_tag; + fromRadioScratch.rebooted = true; - // LOG_DEBUG("Emitting reboot packet for serial shell"); - emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); + // LOG_DEBUG("Emitting reboot packet for serial shell"); + emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); } -void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg) -{ - // In case we send a FromRadio packet - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag; - fromRadioScratch.log_record.level = level; +void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg) { + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag; + fromRadioScratch.log_record.level = level; - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - fromRadioScratch.log_record.time = rtc_sec; - strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1); + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + fromRadioScratch.log_record.time = rtc_sec; + strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1); - auto num_printed = - vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg); - if (num_printed > 0 && fromRadioScratch.log_record.message[num_printed - 1] == - '\n') // Strip any ending newline, because we have records for framing instead. - fromRadioScratch.log_record.message[num_printed - 1] = '\0'; - emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); + auto num_printed = vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg); + if (num_printed > 0 && + fromRadioScratch.log_record.message[num_printed - 1] == '\n') // Strip any ending newline, because we have records for framing instead. + fromRadioScratch.log_record.message[num_printed - 1] = '\0'; + emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); } /// Hookable to find out when connection changes -void StreamAPI::onConnectionChanged(bool connected) -{ - // FIXME do reference counting instead +void StreamAPI::onConnectionChanged(bool connected) { + // FIXME do reference counting instead - if (connected) { // To prevent user confusion, turn off bluetooth while using the serial port api - powerFSM.trigger(EVENT_SERIAL_CONNECTED); - } else { - // FIXME, we get no notice of serial going away, we should instead automatically generate this event if we haven't - // received a packet in a while - powerFSM.trigger(EVENT_SERIAL_DISCONNECTED); - } + if (connected) { // To prevent user confusion, turn off bluetooth while using the serial port api + powerFSM.trigger(EVENT_SERIAL_CONNECTED); + } else { + // FIXME, we get no notice of serial going away, we should instead automatically generate this event if we haven't + // received a packet in a while + powerFSM.trigger(EVENT_SERIAL_DISCONNECTED); + } } \ No newline at end of file diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 4ca2c197f..c5c76d9d6 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -14,79 +14,80 @@ * * ## Wire encoding -When sending protobuf packets over serial or TCP each packet is preceded by uint32 sent in network byte order (big endian). -The upper 16 bits must be 0x94C3. The lower 16 bits are packet length (this encoding gives room to eventually allow quite large -packets). +When sending protobuf packets over serial or TCP each packet is preceded by uint32 sent in network byte order (big +endian). The upper 16 bits must be 0x94C3. The lower 16 bits are packet length (this encoding gives room to eventually +allow quite large packets). -Implementations validate length against the maximum possible size of a BLE packet (our lowest common denominator) of 512 bytes. If -the length provided is larger than that we assume the packet is corrupted and begin again looking for 0x4403 framing. +Implementations validate length against the maximum possible size of a BLE packet (our lowest common denominator) of 512 +bytes. If the length provided is larger than that we assume the packet is corrupted and begin again looking for 0x4403 +framing. -The packets flowing towards the device are ToRadio protobufs, the packets flowing from the device are FromRadio protobufs. -The 0x94C3 marker can be used as framing to (eventually) resync if packets are corrupted over the wire. +The packets flowing towards the device are ToRadio protobufs, the packets flowing from the device are FromRadio +protobufs. The 0x94C3 marker can be used as framing to (eventually) resync if packets are corrupted over the wire. -Note: the 0x94C3 framing was chosen to prevent confusion with the 7 bit ascii character set. It also doesn't collide with any -valid utf8 encoding. This makes it a bit easier to start a device outputting regular debug output on its serial port and then only -after it has received a valid packet from the PC, turn off unencoded debug printing and switch to this packet encoding. +Note: the 0x94C3 framing was chosen to prevent confusion with the 7 bit ascii character set. It also doesn't collide +with any valid utf8 encoding. This makes it a bit easier to start a device outputting regular debug output on its serial +port and then only after it has received a valid packet from the PC, turn off unencoded debug printing and switch to +this packet encoding. */ -class StreamAPI : public PhoneAPI -{ - /** - * The stream we read/write from - */ - Stream *stream; +class StreamAPI : public PhoneAPI { + /** + * The stream we read/write from + */ + Stream *stream; - uint8_t rxBuf[MAX_STREAM_BUF_SIZE] = {0}; - size_t rxPtr = 0; + uint8_t rxBuf[MAX_STREAM_BUF_SIZE] = {0}; + size_t rxPtr = 0; - /// time of last rx, used, to slow down our polling if we haven't heard from anyone - uint32_t lastRxMsec = 0; + /// time of last rx, used, to slow down our polling if we haven't heard from anyone + uint32_t lastRxMsec = 0; - public: - StreamAPI(Stream *_stream) : stream(_stream) {} +public: + StreamAPI(Stream *_stream) : stream(_stream) {} - /** - * Currently we require frequent invocation from loop() to check for arrived serial packets and to send new packets to the - * phone. - */ - virtual int32_t runOncePart(); - virtual int32_t runOncePart(char *buf, uint16_t bufLen); + /** + * Currently we require frequent invocation from loop() to check for arrived serial packets and to send new packets to + * the phone. + */ + virtual int32_t runOncePart(); + virtual int32_t runOncePart(char *buf, uint16_t bufLen); - private: - /** - * Read any rx chars from the link and call handleToRadio - */ - int32_t readStream(); - int32_t readStream(char *buf, uint16_t bufLen); - int32_t handleRecStream(char *buf, uint16_t bufLen); +private: + /** + * Read any rx chars from the link and call handleToRadio + */ + int32_t readStream(); + int32_t readStream(char *buf, uint16_t bufLen); + int32_t handleRecStream(char *buf, uint16_t bufLen); - /** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ - void writeStream(); + /** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ + void writeStream(); - protected: - /** - * Send a FromRadio.rebooted = true packet to the phone - */ - void emitRebooted(); +protected: + /** + * Send a FromRadio.rebooted = true packet to the phone + */ + void emitRebooted(); - virtual void onConnectionChanged(bool connected) override; + virtual void onConnectionChanged(bool connected) override; - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override = 0; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override = 0; - /** - * Send the current txBuffer over our stream - */ - void emitTxBuffer(size_t len); + /** + * Send the current txBuffer over our stream + */ + void emitTxBuffer(size_t len); - /// Are we allowed to write packets to our output stream (subclasses can turn this off - i.e. SerialConsole) - bool canWrite = true; + /// Are we allowed to write packets to our output stream (subclasses can turn this off - i.e. SerialConsole) + bool canWrite = true; - /// Subclasses can use this scratch buffer if they wish - uint8_t txBuf[MAX_STREAM_BUF_SIZE] = {0}; + /// Subclasses can use this scratch buffer if they wish + uint8_t txBuf[MAX_STREAM_BUF_SIZE] = {0}; - /// Low level function to emit a protobuf encapsulated log record - void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg); + /// Low level function to emit a protobuf encapsulated log record + void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg); }; \ No newline at end of file diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index f278cc843..170b7ba46 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -7,29 +7,25 @@ /// @param throttleFunc Function to execute if the execution is not deferred /// @param onDefer Default to NULL, execute the function if the execution is deferred /// @return true if the function was executed, false if it was deferred -bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*throttleFunc)(void), void (*onDefer)(void)) -{ - if (*lastExecutionMs == 0) { - *lastExecutionMs = millis(); - throttleFunc(); - return true; - } - uint32_t now = millis(); +bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*throttleFunc)(void), void (*onDefer)(void)) { + if (*lastExecutionMs == 0) { + *lastExecutionMs = millis(); + throttleFunc(); + return true; + } + uint32_t now = millis(); - if ((now - *lastExecutionMs) >= minumumIntervalMs) { - throttleFunc(); - *lastExecutionMs = now; - return true; - } else if (onDefer != NULL) { - onDefer(); - } - return false; + if ((now - *lastExecutionMs) >= minumumIntervalMs) { + throttleFunc(); + *lastExecutionMs = now; + return true; + } else if (onDefer != NULL) { + onDefer(); + } + return false; } /// @brief Check if the last execution time is within the interval /// @param lastExecutionMs The last execution time in milliseconds /// @param timeSpanMs The interval in milliseconds of the timespan -bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) -{ - return (millis() - lastExecutionMs) < timeSpanMs; -} \ No newline at end of file +bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) { return (millis() - lastExecutionMs) < timeSpanMs; } \ No newline at end of file diff --git a/src/mesh/Throttle.h b/src/mesh/Throttle.h index 8b4bb5d30..7a42d971f 100644 --- a/src/mesh/Throttle.h +++ b/src/mesh/Throttle.h @@ -2,9 +2,8 @@ #include #include -class Throttle -{ - public: - static bool execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*func)(void), void (*onDefer)(void) = NULL); - static bool isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t intervalMs); +class Throttle { +public: + static bool execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*func)(void), void (*onDefer)(void) = NULL); + static bool isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t intervalMs); }; \ No newline at end of file diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index 17cd92851..6e0d06003 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -2,111 +2,106 @@ #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h" -meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) -{ - meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default; +meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) { + meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default; - info.num = lite->num; - info.snr = lite->snr; - info.last_heard = lite->last_heard; - info.channel = lite->channel; - info.via_mqtt = lite->via_mqtt; - info.is_favorite = lite->is_favorite; - info.is_ignored = lite->is_ignored; - info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + info.num = lite->num; + info.snr = lite->snr; + info.last_heard = lite->last_heard; + info.channel = lite->channel; + info.via_mqtt = lite->via_mqtt; + info.is_favorite = lite->is_favorite; + info.is_ignored = lite->is_ignored; + info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - if (lite->has_hops_away) { - info.has_hops_away = true; - info.hops_away = lite->hops_away; - } + if (lite->has_hops_away) { + info.has_hops_away = true; + info.hops_away = lite->hops_away; + } - if (lite->has_position) { - info.has_position = true; - if (lite->position.latitude_i != 0) - info.position.has_latitude_i = true; - info.position.latitude_i = lite->position.latitude_i; - if (lite->position.longitude_i != 0) - info.position.has_longitude_i = true; - info.position.longitude_i = lite->position.longitude_i; - if (lite->position.altitude != 0) - info.position.has_altitude = true; - info.position.altitude = lite->position.altitude; - info.position.location_source = lite->position.location_source; - info.position.time = lite->position.time; - } - if (lite->has_user) { - info.has_user = true; - info.user = ConvertToUser(lite->num, lite->user); - } - if (lite->has_device_metrics) { - info.has_device_metrics = true; - info.device_metrics = lite->device_metrics; - } - return info; + if (lite->has_position) { + info.has_position = true; + if (lite->position.latitude_i != 0) + info.position.has_latitude_i = true; + info.position.latitude_i = lite->position.latitude_i; + if (lite->position.longitude_i != 0) + info.position.has_longitude_i = true; + info.position.longitude_i = lite->position.longitude_i; + if (lite->position.altitude != 0) + info.position.has_altitude = true; + info.position.altitude = lite->position.altitude; + info.position.location_source = lite->position.location_source; + info.position.time = lite->position.time; + } + if (lite->has_user) { + info.has_user = true; + info.user = ConvertToUser(lite->num, lite->user); + } + if (lite->has_device_metrics) { + info.has_device_metrics = true; + info.device_metrics = lite->device_metrics; + } + return info; } -meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position) -{ - meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; - lite.latitude_i = position.latitude_i; - lite.longitude_i = position.longitude_i; - lite.altitude = position.altitude; - lite.location_source = position.location_source; - lite.time = position.time; +meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position) { + meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; + lite.latitude_i = position.latitude_i; + lite.longitude_i = position.longitude_i; + lite.altitude = position.altitude; + lite.location_source = position.location_source; + lite.time = position.time; - return lite; + return lite; } -meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite lite) -{ - meshtastic_Position position = meshtastic_Position_init_default; - if (lite.latitude_i != 0) - position.has_latitude_i = true; - position.latitude_i = lite.latitude_i; - if (lite.longitude_i != 0) - position.has_longitude_i = true; - position.longitude_i = lite.longitude_i; - if (lite.altitude != 0) - position.has_altitude = true; - position.altitude = lite.altitude; - position.location_source = lite.location_source; - position.time = lite.time; +meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite lite) { + meshtastic_Position position = meshtastic_Position_init_default; + if (lite.latitude_i != 0) + position.has_latitude_i = true; + position.latitude_i = lite.latitude_i; + if (lite.longitude_i != 0) + position.has_longitude_i = true; + position.longitude_i = lite.longitude_i; + if (lite.altitude != 0) + position.has_altitude = true; + position.altitude = lite.altitude; + position.location_source = lite.location_source; + position.time = lite.time; - return position; + return position; } -meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) -{ - meshtastic_UserLite lite = meshtastic_UserLite_init_default; +meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) { + meshtastic_UserLite lite = meshtastic_UserLite_init_default; - strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); - strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); - lite.hw_model = user.hw_model; - lite.role = user.role; - lite.is_licensed = user.is_licensed; - memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); - memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes)); - lite.public_key.size = user.public_key.size; - lite.has_is_unmessagable = user.has_is_unmessagable; - lite.is_unmessagable = user.is_unmessagable; - return lite; + strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); + strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); + lite.hw_model = user.hw_model; + lite.role = user.role; + lite.is_licensed = user.is_licensed; + memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); + memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes)); + lite.public_key.size = user.public_key.size; + lite.has_is_unmessagable = user.has_is_unmessagable; + lite.is_unmessagable = user.is_unmessagable; + return lite; } -meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) -{ - meshtastic_User user = meshtastic_User_init_default; +meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) { + meshtastic_User user = meshtastic_User_init_default; - snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); - strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); - strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); - user.hw_model = lite.hw_model; - user.role = lite.role; - user.is_licensed = lite.is_licensed; - memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); - memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); - user.public_key.size = lite.public_key.size; - user.has_is_unmessagable = lite.has_is_unmessagable; - user.is_unmessagable = lite.is_unmessagable; + snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); + strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); + strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); + user.hw_model = lite.hw_model; + user.role = lite.role; + user.is_licensed = lite.is_licensed; + memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); + memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); + user.public_key.size = lite.public_key.size; + user.has_is_unmessagable = lite.has_is_unmessagable; + user.is_unmessagable = lite.is_unmessagable; - return user; + return user; } \ No newline at end of file diff --git a/src/mesh/TypeConversions.h b/src/mesh/TypeConversions.h index 19e471f98..bdf347e94 100644 --- a/src/mesh/TypeConversions.h +++ b/src/mesh/TypeConversions.h @@ -4,12 +4,11 @@ #pragma once #include "NodeDB.h" -class TypeConversions -{ - public: - static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite); - static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position); - static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); - static meshtastic_UserLite ConvertToUserLite(meshtastic_User user); - static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite); +class TypeConversions { +public: + static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite); + static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position); + static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); + static meshtastic_UserLite ConvertToUserLite(meshtastic_User user); + static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite); }; diff --git a/src/mesh/TypedQueue.h b/src/mesh/TypedQueue.h index 47d7200a5..ddbb4297c 100644 --- a/src/mesh/TypedQueue.h +++ b/src/mesh/TypedQueue.h @@ -12,54 +12,51 @@ * A wrapper for freertos queues. Note: each element object should be small * and POD (Plain Old Data type) as elements are memcpied by value. */ -template class TypedQueue -{ - static_assert(std::is_standard_layout::value, "T must be standard layout"); - QueueHandle_t h; - concurrency::OSThread *reader = NULL; +template class TypedQueue { + static_assert(std::is_standard_layout::value, "T must be standard layout"); + QueueHandle_t h; + concurrency::OSThread *reader = NULL; - public: - explicit TypedQueue(int maxElements) : h(xQueueCreate(maxElements, sizeof(T))) { assert(h); } +public: + explicit TypedQueue(int maxElements) : h(xQueueCreate(maxElements, sizeof(T))) { assert(h); } - ~TypedQueue() { vQueueDelete(h); } + ~TypedQueue() { vQueueDelete(h); } - int numFree() { return uxQueueSpacesAvailable(h); } + int numFree() { return uxQueueSpacesAvailable(h); } - bool isEmpty() { return uxQueueMessagesWaiting(h) == 0; } + bool isEmpty() { return uxQueueMessagesWaiting(h) == 0; } - int numUsed() { return uxQueueMessagesWaiting(h); } + int numUsed() { return uxQueueMessagesWaiting(h); } - /** euqueue a packet. Also, maxWait used to default to portMAX_DELAY, but we now want to callers to THINK about what blocking - * they want */ - bool enqueue(T x, TickType_t maxWait) - { - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interrupt(); - } - return xQueueSendToBack(h, &x, maxWait) == pdTRUE; + /** euqueue a packet. Also, maxWait used to default to portMAX_DELAY, but we now want to callers to THINK about what + * blocking they want */ + bool enqueue(T x, TickType_t maxWait) { + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); } + return xQueueSendToBack(h, &x, maxWait) == pdTRUE; + } - bool enqueueFromISR(T x, BaseType_t *higherPriWoken) - { - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interruptFromISR(higherPriWoken); - } - return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; + bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interruptFromISR(higherPriWoken); } + return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; + } - bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) { return xQueueReceive(h, p, maxWait) == pdTRUE; } + bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) { return xQueueReceive(h, p, maxWait) == pdTRUE; } - bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } + bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } - /** - * Set a thread that is reading from this queue - * If a message is pushed to this queue that thread will be scheduled to run ASAP. - * - * Note: thread will not be automatically enabled, just have its interval set to 0 - */ - void setReader(concurrency::OSThread *t) { reader = t; } + /** + * Set a thread that is reading from this queue + * If a message is pushed to this queue that thread will be scheduled to run ASAP. + * + * Note: thread will not be automatically enabled, just have its interval set to 0 + */ + void setReader(concurrency::OSThread *t) { reader = t; } }; #else @@ -70,55 +67,52 @@ template class TypedQueue * A wrapper for freertos queues. Note: each element object should be small * and POD (Plain Old Data type) as elements are memcpied by value. */ -template class TypedQueue -{ - std::queue q; - concurrency::OSThread *reader = NULL; - int maxElements; +template class TypedQueue { + std::queue q; + concurrency::OSThread *reader = NULL; + int maxElements; - public: - explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {} +public: + explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {} - int numFree() - { - if (maxElements <= 0) - return 1; // Always claim 1 free, because we can grow to any size - return maxElements - numUsed(); + int numFree() { + if (maxElements <= 0) + return 1; // Always claim 1 free, because we can grow to any size + return maxElements - numUsed(); + } + + bool isEmpty() { return q.empty(); } + + int numUsed() { return q.size(); } + + bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) { + if (numFree() <= 0) + return false; + + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); } - bool isEmpty() { return q.empty(); } + q.push(x); + return true; + } - int numUsed() { return q.size(); } + // bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { return xQueueSendToBackFromISR(h, &x, higherPriWoken) == + // pdTRUE; } - bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) - { - if (numFree() <= 0) - return false; - - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interrupt(); - } - - q.push(x); - return true; + bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) { + if (isEmpty()) + return false; + else { + *p = q.front(); + q.pop(); + return true; } + } - // bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; } + // bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } - bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) - { - if (isEmpty()) - return false; - else { - *p = q.front(); - q.pop(); - return true; - } - } - - // bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } - - void setReader(concurrency::OSThread *t) { reader = t; } + void setReader(concurrency::OSThread *t) { reader = t; } }; #endif \ No newline at end of file diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp index 420d80e9a..6c22a2ae7 100644 --- a/src/mesh/aes-ccm.cpp +++ b/src/mesh/aes-ccm.cpp @@ -18,163 +18,151 @@ * @param len Number of bytes to compare * @return 0 if arrays are equal, -1 if different or if inputs are invalid */ -static int constant_time_compare(const void *a_, const void *b_, size_t len) -{ - /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ - const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; - const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; - if (len == 0) - return 0; - if (a == NULL || b == NULL) - return -1; - size_t i; - volatile uint8_t d = 0U; - for (i = 0U; i < len; i++) { - d |= (a[i] ^ b[i]); - } - /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ - return (1 & ((d - 1) >> 8)) - 1; -} - -static void WPA_PUT_BE16(uint8_t *a, uint16_t val) -{ - a[0] = val >> 8; - a[1] = val & 0xff; -} - -static void xor_aes_block(uint8_t *dst, const uint8_t *src) -{ - for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) { - dst[i] ^= src[i]; - } -} -static void aes_ccm_auth_start(size_t M, size_t L, const uint8_t *nonce, const uint8_t *aad, size_t aad_len, size_t plain_len, - uint8_t *x) -{ - uint8_t aad_buf[2 * AES_BLOCK_SIZE]; - uint8_t b[AES_BLOCK_SIZE]; - /* Authentication */ - /* B_0: Flags | Nonce N | l(m) */ - b[0] = aad_len ? 0x40 : 0 /* Adata */; - b[0] |= (((M - 2) / 2) /* M' */ << 3); - b[0] |= (L - 1) /* L' */; - memcpy(&b[1], nonce, 15 - L); - WPA_PUT_BE16(&b[AES_BLOCK_SIZE - L], plain_len); - crypto->aesEncrypt(b, x); /* X_1 = E(K, B_0) */ - if (!aad_len) - return; - WPA_PUT_BE16(aad_buf, aad_len); - memcpy(aad_buf + 2, aad, aad_len); - memset(aad_buf + 2 + aad_len, 0, sizeof(aad_buf) - 2 - aad_len); - xor_aes_block(aad_buf, x); - crypto->aesEncrypt(aad_buf, x); /* X_2 = E(K, X_1 XOR B_1) */ - if (aad_len > AES_BLOCK_SIZE - 2) { - xor_aes_block(&aad_buf[AES_BLOCK_SIZE], x); - /* X_3 = E(K, X_2 XOR B_2) */ - crypto->aesEncrypt(&aad_buf[AES_BLOCK_SIZE], x); - } -} -static void aes_ccm_auth(const uint8_t *data, size_t len, uint8_t *x) -{ - size_t last = len % AES_BLOCK_SIZE; - size_t i; - for (i = 0; i < len / AES_BLOCK_SIZE; i++) { - /* X_i+1 = E(K, X_i XOR B_i) */ - xor_aes_block(x, data); - data += AES_BLOCK_SIZE; - crypto->aesEncrypt(x, x); - } - if (last) { - /* XOR zero-padded last block */ - for (i = 0; i < last; i++) - x[i] ^= *data++; - crypto->aesEncrypt(x, x); - } -} -static void aes_ccm_encr_start(size_t L, const uint8_t *nonce, uint8_t *a) -{ - /* A_i = Flags | Nonce N | Counter i */ - a[0] = L - 1; /* Flags = L' */ - memcpy(&a[1], nonce, 15 - L); -} -static void aes_ccm_encr(size_t L, const uint8_t *in, size_t len, uint8_t *out, uint8_t *a) -{ - size_t last = len % AES_BLOCK_SIZE; - size_t i; - /* crypt = msg XOR (S_1 | S_2 | ... | S_n) */ - for (i = 1; i <= len / AES_BLOCK_SIZE; i++) { - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); - /* S_i = E(K, A_i) */ - crypto->aesEncrypt(a, out); - xor_aes_block(out, in); - out += AES_BLOCK_SIZE; - in += AES_BLOCK_SIZE; - } - if (last) { - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); - crypto->aesEncrypt(a, out); - /* XOR zero-padded last block */ - for (i = 0; i < last; i++) - *out++ ^= *in++; - } -} -static void aes_ccm_encr_auth(size_t M, const uint8_t *x, uint8_t *a, uint8_t *auth) -{ - size_t i; - uint8_t tmp[AES_BLOCK_SIZE]; - /* U = T XOR S_0; S_0 = E(K, A_0) */ - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); - crypto->aesEncrypt(a, tmp); - for (i = 0; i < M; i++) - auth[i] = x[i] ^ tmp[i]; -} -static void aes_ccm_decr_auth(size_t M, uint8_t *a, const uint8_t *auth, uint8_t *t) -{ - size_t i; - uint8_t tmp[AES_BLOCK_SIZE]; - /* U = T XOR S_0; S_0 = E(K, A_0) */ - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); - crypto->aesEncrypt(a, tmp); - for (i = 0; i < M; i++) - t[i] = auth[i] ^ tmp[i]; -} -/* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ -int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, - const uint8_t *aad, size_t aad_len, uint8_t *crypt, uint8_t *auth) -{ - const size_t L = 2; - uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; - if (aad_len > 30 || M > AES_BLOCK_SIZE) - return -1; - crypto->aesSetKey(key, key_len); - aes_ccm_auth_start(M, L, nonce, aad, aad_len, plain_len, x); - aes_ccm_auth(plain, plain_len, x); - /* Encryption */ - aes_ccm_encr_start(L, nonce, a); - aes_ccm_encr(L, plain, plain_len, crypt, a); - aes_ccm_encr_auth(M, x, a, auth); +static int constant_time_compare(const void *a_, const void *b_, size_t len) { + /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ + const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; + const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; + if (len == 0) return 0; + if (a == NULL || b == NULL) + return -1; + size_t i; + volatile uint8_t d = 0U; + for (i = 0U; i < len; i++) { + d |= (a[i] ^ b[i]); + } + /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ + return (1 & ((d - 1) >> 8)) - 1; +} + +static void WPA_PUT_BE16(uint8_t *a, uint16_t val) { + a[0] = val >> 8; + a[1] = val & 0xff; +} + +static void xor_aes_block(uint8_t *dst, const uint8_t *src) { + for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) { + dst[i] ^= src[i]; + } +} +static void aes_ccm_auth_start(size_t M, size_t L, const uint8_t *nonce, const uint8_t *aad, size_t aad_len, size_t plain_len, uint8_t *x) { + uint8_t aad_buf[2 * AES_BLOCK_SIZE]; + uint8_t b[AES_BLOCK_SIZE]; + /* Authentication */ + /* B_0: Flags | Nonce N | l(m) */ + b[0] = aad_len ? 0x40 : 0 /* Adata */; + b[0] |= (((M - 2) / 2) /* M' */ << 3); + b[0] |= (L - 1) /* L' */; + memcpy(&b[1], nonce, 15 - L); + WPA_PUT_BE16(&b[AES_BLOCK_SIZE - L], plain_len); + crypto->aesEncrypt(b, x); /* X_1 = E(K, B_0) */ + if (!aad_len) + return; + WPA_PUT_BE16(aad_buf, aad_len); + memcpy(aad_buf + 2, aad, aad_len); + memset(aad_buf + 2 + aad_len, 0, sizeof(aad_buf) - 2 - aad_len); + xor_aes_block(aad_buf, x); + crypto->aesEncrypt(aad_buf, x); /* X_2 = E(K, X_1 XOR B_1) */ + if (aad_len > AES_BLOCK_SIZE - 2) { + xor_aes_block(&aad_buf[AES_BLOCK_SIZE], x); + /* X_3 = E(K, X_2 XOR B_2) */ + crypto->aesEncrypt(&aad_buf[AES_BLOCK_SIZE], x); + } +} +static void aes_ccm_auth(const uint8_t *data, size_t len, uint8_t *x) { + size_t last = len % AES_BLOCK_SIZE; + size_t i; + for (i = 0; i < len / AES_BLOCK_SIZE; i++) { + /* X_i+1 = E(K, X_i XOR B_i) */ + xor_aes_block(x, data); + data += AES_BLOCK_SIZE; + crypto->aesEncrypt(x, x); + } + if (last) { + /* XOR zero-padded last block */ + for (i = 0; i < last; i++) + x[i] ^= *data++; + crypto->aesEncrypt(x, x); + } +} +static void aes_ccm_encr_start(size_t L, const uint8_t *nonce, uint8_t *a) { + /* A_i = Flags | Nonce N | Counter i */ + a[0] = L - 1; /* Flags = L' */ + memcpy(&a[1], nonce, 15 - L); +} +static void aes_ccm_encr(size_t L, const uint8_t *in, size_t len, uint8_t *out, uint8_t *a) { + size_t last = len % AES_BLOCK_SIZE; + size_t i; + /* crypt = msg XOR (S_1 | S_2 | ... | S_n) */ + for (i = 1; i <= len / AES_BLOCK_SIZE; i++) { + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); + /* S_i = E(K, A_i) */ + crypto->aesEncrypt(a, out); + xor_aes_block(out, in); + out += AES_BLOCK_SIZE; + in += AES_BLOCK_SIZE; + } + if (last) { + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); + crypto->aesEncrypt(a, out); + /* XOR zero-padded last block */ + for (i = 0; i < last; i++) + *out++ ^= *in++; + } +} +static void aes_ccm_encr_auth(size_t M, const uint8_t *x, uint8_t *a, uint8_t *auth) { + size_t i; + uint8_t tmp[AES_BLOCK_SIZE]; + /* U = T XOR S_0; S_0 = E(K, A_0) */ + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); + crypto->aesEncrypt(a, tmp); + for (i = 0; i < M; i++) + auth[i] = x[i] ^ tmp[i]; +} +static void aes_ccm_decr_auth(size_t M, uint8_t *a, const uint8_t *auth, uint8_t *t) { + size_t i; + uint8_t tmp[AES_BLOCK_SIZE]; + /* U = T XOR S_0; S_0 = E(K, A_0) */ + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); + crypto->aesEncrypt(a, tmp); + for (i = 0; i < M; i++) + t[i] = auth[i] ^ tmp[i]; } /* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ -bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, - const uint8_t *aad, size_t aad_len, const uint8_t *auth, uint8_t *plain) -{ - const size_t L = 2; - uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; - uint8_t t[AES_BLOCK_SIZE]; - if (aad_len > 30 || M > AES_BLOCK_SIZE) - return false; - crypto->aesSetKey(key, key_len); - /* Decryption */ - aes_ccm_encr_start(L, nonce, a); - aes_ccm_decr_auth(M, a, auth, t); - /* plaintext = msg XOR (S_1 | S_2 | ... | S_n) */ - aes_ccm_encr(L, crypt, crypt_len, plain, a); - aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); - aes_ccm_auth(plain, crypt_len, x); - if (constant_time_compare(x, t, M) != 0) { - return false; - } - return true; +int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, const uint8_t *aad, + size_t aad_len, uint8_t *crypt, uint8_t *auth) { + const size_t L = 2; + uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; + if (aad_len > 30 || M > AES_BLOCK_SIZE) + return -1; + crypto->aesSetKey(key, key_len); + aes_ccm_auth_start(M, L, nonce, aad, aad_len, plain_len, x); + aes_ccm_auth(plain, plain_len, x); + /* Encryption */ + aes_ccm_encr_start(L, nonce, a); + aes_ccm_encr(L, plain, plain_len, crypt, a); + aes_ccm_encr_auth(M, x, a, auth); + return 0; +} +/* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ +bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, const uint8_t *aad, + size_t aad_len, const uint8_t *auth, uint8_t *plain) { + const size_t L = 2; + uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; + uint8_t t[AES_BLOCK_SIZE]; + if (aad_len > 30 || M > AES_BLOCK_SIZE) + return false; + crypto->aesSetKey(key, key_len); + /* Decryption */ + aes_ccm_encr_start(L, nonce, a); + aes_ccm_decr_auth(M, a, auth, t); + /* plaintext = msg XOR (S_1 | S_2 | ... | S_n) */ + aes_ccm_encr(L, crypt, crypt_len, plain, a); + aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); + aes_ccm_auth(plain, crypt_len, x); + if (constant_time_compare(x, t, M) != 0) { + return false; + } + return true; } #endif \ No newline at end of file diff --git a/src/mesh/aes-ccm.h b/src/mesh/aes-ccm.h index 6b8edcde4..5027853b7 100644 --- a/src/mesh/aes-ccm.h +++ b/src/mesh/aes-ccm.h @@ -2,9 +2,9 @@ #include "CryptoEngine.h" #if !MESHTASTIC_EXCLUDE_PKI -int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, - const uint8_t *aad, size_t aad_len, uint8_t *crypt, uint8_t *auth); +int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, const uint8_t *aad, + size_t aad_len, uint8_t *crypt, uint8_t *auth); -bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, - const uint8_t *aad, size_t aad_len, const uint8_t *auth, uint8_t *plain); +bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, const uint8_t *aad, + size_t aad_len, const uint8_t *auth, uint8_t *plain); #endif \ No newline at end of file diff --git a/src/mesh/api/PacketAPI.cpp b/src/mesh/api/PacketAPI.cpp index f4d5de540..b92acd5eb 100644 --- a/src/mesh/api/PacketAPI.cpp +++ b/src/mesh/api/PacketAPI.cpp @@ -8,124 +8,116 @@ PacketAPI *packetAPI = nullptr; -PacketAPI *PacketAPI::create(PacketServer *_server) -{ - if (!packetAPI) { - packetAPI = new PacketAPI(_server); - } - return packetAPI; +PacketAPI *PacketAPI::create(PacketServer *_server) { + if (!packetAPI) { + packetAPI = new PacketAPI(_server); + } + return packetAPI; } -PacketAPI::PacketAPI(PacketServer *_server) - : concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server) -{ - api_type = TYPE_PACKET; +PacketAPI::PacketAPI(PacketServer *_server) : concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server) { + api_type = TYPE_PACKET; } -int32_t PacketAPI::runOnce() -{ - bool success = false; +int32_t PacketAPI::runOnce() { + bool success = false; #ifndef ARCH_PORTDUINO - if (config.bluetooth.enabled) { - if (!programmingMode) { - // in programmingMode we don't send any packets to the client except this one notify - programmingMode = true; - success = notifyProgrammingMode(); - } - } else + if (config.bluetooth.enabled) { + if (!programmingMode) { + // in programmingMode we don't send any packets to the client except this one notify + programmingMode = true; + success = notifyProgrammingMode(); + } + } else #endif - { - success = sendPacket(); - } - success |= receivePacket(); - return success ? 10 : 50; + { + success = sendPacket(); + } + success |= receivePacket(); + return success ? 10 : 50; } -bool PacketAPI::receivePacket(void) -{ - bool data_received = false; - while (server->hasData()) { - isConnected = true; - data_received = true; +bool PacketAPI::receivePacket(void) { + bool data_received = false; + while (server->hasData()) { + isConnected = true; + data_received = true; - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); - lastContactMsec = millis(); + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + lastContactMsec = millis(); - meshtastic_ToRadio *mr; - auto p = server->receivePacket()->move(); - int id = p->getPacketId(); - LOG_DEBUG("Received packet id=%u", id); - mr = (meshtastic_ToRadio *)&static_cast *>(p.get())->getData(); + meshtastic_ToRadio *mr; + auto p = server->receivePacket()->move(); + int id = p->getPacketId(); + LOG_DEBUG("Received packet id=%u", id); + mr = (meshtastic_ToRadio *)&static_cast *>(p.get())->getData(); - switch (mr->which_payload_variant) { - case meshtastic_ToRadio_packet_tag: { - meshtastic_MeshPacket *mp = &mr->packet; - mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API; - printPacket("PACKET FROM QUEUE", mp); - service->handleToRadio(*mp); - break; - } - case meshtastic_ToRadio_want_config_id_tag: { - uint32_t config_nonce = mr->want_config_id; - LOG_INFO("Screen wants config, nonce=%u", config_nonce); - handleStartConfig(); - break; - } - case meshtastic_ToRadio_heartbeat_tag: - if (mr->heartbeat.nonce == 1) { - if (nodeInfoModule) { - LOG_INFO("Broadcasting nodeinfo ping"); - nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true); - } - } else { - LOG_DEBUG("Got client heartbeat"); - } - break; - default: - LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant); - break; - } + switch (mr->which_payload_variant) { + case meshtastic_ToRadio_packet_tag: { + meshtastic_MeshPacket *mp = &mr->packet; + mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API; + printPacket("PACKET FROM QUEUE", mp); + service->handleToRadio(*mp); + break; } - return data_received; + case meshtastic_ToRadio_want_config_id_tag: { + uint32_t config_nonce = mr->want_config_id; + LOG_INFO("Screen wants config, nonce=%u", config_nonce); + handleStartConfig(); + break; + } + case meshtastic_ToRadio_heartbeat_tag: + if (mr->heartbeat.nonce == 1) { + if (nodeInfoModule) { + LOG_INFO("Broadcasting nodeinfo ping"); + nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true); + } + } else { + LOG_DEBUG("Got client heartbeat"); + } + break; + default: + LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant); + break; + } + } + return data_received; } -bool PacketAPI::sendPacket(void) -{ - if (server->available()) { - // fill dummy buffer; we don't use it, we directly send the fromRadio structure - uint32_t len = getFromRadio(txBuf); - if (len != 0) { - static uint32_t id = 0; - fromRadioScratch.id = ++id; - bool result = server->sendPacket(DataPacket(id, fromRadioScratch)); - if (!result) { - LOG_ERROR("send queue full"); - } - return result; - } +bool PacketAPI::sendPacket(void) { + if (server->available()) { + // fill dummy buffer; we don't use it, we directly send the fromRadio structure + uint32_t len = getFromRadio(txBuf); + if (len != 0) { + static uint32_t id = 0; + fromRadioScratch.id = ++id; + bool result = server->sendPacket(DataPacket(id, fromRadioScratch)); + if (!result) { + LOG_ERROR("send queue full"); + } + return result; } - return false; + } + return false; } -bool PacketAPI::notifyProgrammingMode(void) -{ - // tell the client we are in programming mode by sending only the bluetooth config state - LOG_INFO("force client into programmingMode"); - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.id = nodeDB->getNodeNum(); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; - fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; - fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; - return server->sendPacket(DataPacket(0, fromRadioScratch)); +bool PacketAPI::notifyProgrammingMode(void) { + // tell the client we are in programming mode by sending only the bluetooth config state + LOG_INFO("force client into programmingMode"); + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.id = nodeDB->getNodeNum(); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; + fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; + fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; + return server->sendPacket(DataPacket(0, fromRadioScratch)); } /** * return true if we got (once!) contact from our client and the server send queue is not full */ -bool PacketAPI::checkIsConnected() -{ - isConnected |= server->hasData(); - return isConnected && server->available(); +bool PacketAPI::checkIsConnected() { + isConnected |= server->hasData(); + return isConnected && server->available(); } #endif \ No newline at end of file diff --git a/src/mesh/api/PacketAPI.h b/src/mesh/api/PacketAPI.h index fc08ab209..a2593d59f 100644 --- a/src/mesh/api/PacketAPI.h +++ b/src/mesh/api/PacketAPI.h @@ -9,30 +9,29 @@ * between two tasks running on CPU0 and CPU1, respectively. * */ -class PacketAPI : public PhoneAPI, public concurrency::OSThread -{ - public: - static PacketAPI *create(PacketServer *_server); - virtual ~PacketAPI(){}; - virtual int32_t runOnce(); +class PacketAPI : public PhoneAPI, public concurrency::OSThread { +public: + static PacketAPI *create(PacketServer *_server); + virtual ~PacketAPI(){}; + virtual int32_t runOnce(); - protected: - PacketAPI(PacketServer *_server); - // Check the current underlying physical queue to see if the client is fetching packets - bool checkIsConnected() override; +protected: + PacketAPI(PacketServer *_server); + // Check the current underlying physical queue to see if the client is fetching packets + bool checkIsConnected() override; - void onNowHasData(uint32_t fromRadioNum) override {} - void onConnectionChanged(bool connected) override {} + void onNowHasData(uint32_t fromRadioNum) override {} + void onConnectionChanged(bool connected) override {} - private: - bool receivePacket(void); - bool sendPacket(void); - bool notifyProgrammingMode(void); +private: + bool receivePacket(void); + bool sendPacket(void); + bool notifyProgrammingMode(void); - bool isConnected; - bool programmingMode; - PacketServer *server; - uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI + bool isConnected; + bool programmingMode; + PacketServer *server; + uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI }; extern PacketAPI *packetAPI; \ No newline at end of file diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index 1a506421c..087e7e19d 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -2,82 +2,68 @@ #include "configuration.h" #include -template -ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) -{ - LOG_INFO("Incoming API connection"); +template ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) { + LOG_INFO("Incoming API connection"); } -template ServerAPI::~ServerAPI() -{ - client.stop(); -} +template ServerAPI::~ServerAPI() { client.stop(); } -template void ServerAPI::close() -{ - client.stop(); // drop tcp connection - StreamAPI::close(); +template void ServerAPI::close() { + client.stop(); // drop tcp connection + StreamAPI::close(); } /// Check the current underlying physical link to see if the client is currently connected -template bool ServerAPI::checkIsConnected() -{ - return client.connected(); -} +template bool ServerAPI::checkIsConnected() { return client.connected(); } -template int32_t ServerAPI::runOnce() -{ - if (client.connected()) { - return StreamAPI::runOncePart(); - } else { - LOG_INFO("Client dropped connection, suspend API service"); - enabled = false; // we no longer need to run - return 0; - } +template int32_t ServerAPI::runOnce() { + if (client.connected()) { + return StreamAPI::runOncePart(); + } else { + LOG_INFO("Client dropped connection, suspend API service"); + enabled = false; // we no longer need to run + return 0; + } } template APIServerPort::APIServerPort(int port) : U(port), concurrency::OSThread("ApiServer") {} -template void APIServerPort::init() -{ - U::begin(); -} +template void APIServerPort::init() { U::begin(); } -template int32_t APIServerPort::runOnce() -{ +template int32_t APIServerPort::runOnce() { #ifdef ARCH_ESP32 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - auto client = U::accept(); + auto client = U::accept(); #else - auto client = U::available(); + auto client = U::available(); #endif #elif defined(ARCH_RP2040) - auto client = U::accept(); + auto client = U::accept(); #else - auto client = U::available(); + auto client = U::available(); #endif - if (client) { - // Close any previous connection (see FIXME in header file) - if (openAPI) { + if (client) { + // Close any previous connection (see FIXME in header file) + if (openAPI) { #if RAK_4631 - // RAK13800 Ethernet requests periodically take more time - // This backoff addresses most cases keeping max wait < 1s - // Reconnections are delayed by full wait time - if (waitTime < 400) { - waitTime *= 2; - LOG_INFO("Previous TCP connection still open, try again in %dms", waitTime); - return waitTime; - } + // RAK13800 Ethernet requests periodically take more time + // This backoff addresses most cases keeping max wait < 1s + // Reconnections are delayed by full wait time + if (waitTime < 400) { + waitTime *= 2; + LOG_INFO("Previous TCP connection still open, try again in %dms", waitTime); + return waitTime; + } #endif - LOG_INFO("Force close previous TCP connection"); - delete openAPI; - } - - openAPI = new T(client); + LOG_INFO("Force close previous TCP connection"); + delete openAPI; } + openAPI = new T(client); + } + #if RAK_4631 - waitTime = 100; + waitTime = 100; #endif - return 100; // only check occasionally for incoming connections + return 100; // only check occasionally for incoming connections } \ No newline at end of file diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h index 111314476..ec28651f6 100644 --- a/src/mesh/api/ServerAPI.h +++ b/src/mesh/api/ServerAPI.h @@ -8,51 +8,49 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -template class ServerAPI : public StreamAPI, private concurrency::OSThread -{ - private: - T client; +template class ServerAPI : public StreamAPI, private concurrency::OSThread { +private: + T client; - public: - explicit ServerAPI(T &_client); +public: + explicit ServerAPI(T &_client); - virtual ~ServerAPI(); + virtual ~ServerAPI(); - /// override close to also shutdown the TCP link - virtual void close(); + /// override close to also shutdown the TCP link + virtual void close(); - protected: - /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to - /// stay in the POWERED state to prevent disabling wifi) - virtual void onConnectionChanged(bool connected) override {} +protected: + /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the + /// board to stay in the POWERED state to prevent disabling wifi) + virtual void onConnectionChanged(bool connected) override {} - virtual int32_t runOnce() override; // Check for dropped client connections + virtual int32_t runOnce() override; // Check for dropped client connections - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; }; /** * Listens for incoming connections and does accepts and creates instances of ServerAPI as needed */ -template class APIServerPort : public U, private concurrency::OSThread -{ - /** The currently open port - * - * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this class to - * delegate to the worker. Once coroutines are implemented we can relax this restriction. - */ - T *openAPI = NULL; +template class APIServerPort : public U, private concurrency::OSThread { + /** The currently open port + * + * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this + * class to delegate to the worker. Once coroutines are implemented we can relax this restriction. + */ + T *openAPI = NULL; #if defined(RAK_4631) || defined(RAK11310) - // Track wait time for RAK13800 Ethernet requests - int32_t waitTime = 100; + // Track wait time for RAK13800 Ethernet requests + int32_t waitTime = 100; #endif - public: - explicit APIServerPort(int port); +public: + explicit APIServerPort(int port); - void init(); + void init(); - protected: - int32_t runOnce() override; +protected: + int32_t runOnce() override; }; diff --git a/src/mesh/api/WiFiServerAPI.cpp b/src/mesh/api/WiFiServerAPI.cpp index 4d729f5c7..4bf6417cf 100644 --- a/src/mesh/api/WiFiServerAPI.cpp +++ b/src/mesh/api/WiFiServerAPI.cpp @@ -6,27 +6,24 @@ static WiFiServerPort *apiPort; -void initApiServer(int port) -{ - // Start API server on port 4403 - if (!apiPort) { - apiPort = new WiFiServerPort(port); - LOG_INFO("API server listen on TCP port %d", port); - apiPort->init(); - } +void initApiServer(int port) { + // Start API server on port 4403 + if (!apiPort) { + apiPort = new WiFiServerPort(port); + LOG_INFO("API server listen on TCP port %d", port); + apiPort->init(); + } } -void deInitApiServer() -{ - if (apiPort) { - delete apiPort; - apiPort = nullptr; - } +void deInitApiServer() { + if (apiPort) { + delete apiPort; + apiPort = nullptr; + } } -WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client) -{ - api_type = TYPE_WIFI; - LOG_INFO("Incoming wifi connection"); +WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client) { + api_type = TYPE_WIFI; + LOG_INFO("Incoming wifi connection"); } WiFiServerPort::WiFiServerPort(int port) : APIServerPort(port) {} diff --git a/src/mesh/api/WiFiServerAPI.h b/src/mesh/api/WiFiServerAPI.h index 5f2019983..eea3e7559 100644 --- a/src/mesh/api/WiFiServerAPI.h +++ b/src/mesh/api/WiFiServerAPI.h @@ -12,19 +12,17 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -class WiFiServerAPI : public ServerAPI -{ - public: - explicit WiFiServerAPI(WiFiClient &_client); +class WiFiServerAPI : public ServerAPI { +public: + explicit WiFiServerAPI(WiFiClient &_client); }; /** * Listens for incoming connections and does accepts and creates instances of WiFiServerAPI as needed */ -class WiFiServerPort : public APIServerPort -{ - public: - explicit WiFiServerPort(int port); +class WiFiServerPort : public APIServerPort { +public: + explicit WiFiServerPort(int port); }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp index 10ff06df2..42c0bae69 100644 --- a/src/mesh/api/ethServerAPI.cpp +++ b/src/mesh/api/ethServerAPI.cpp @@ -7,20 +7,18 @@ static ethServerPort *apiPort; -void initApiServer(int port) -{ - // Start API server on port 4403 - if (!apiPort) { - apiPort = new ethServerPort(port); - LOG_INFO("API server listening on TCP port %d", port); - apiPort->init(); - } +void initApiServer(int port) { + // Start API server on port 4403 + if (!apiPort) { + apiPort = new ethServerPort(port); + LOG_INFO("API server listening on TCP port %d", port); + apiPort->init(); + } } -ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) -{ - LOG_INFO("Incoming ethernet connection"); - api_type = TYPE_ETH; +ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) { + LOG_INFO("Incoming ethernet connection"); + api_type = TYPE_ETH; } ethServerPort::ethServerPort(int port) : APIServerPort(port) {} diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index c616c87be..262da80c5 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -8,19 +8,17 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -class ethServerAPI : public ServerAPI -{ - public: - explicit ethServerAPI(EthernetClient &_client); +class ethServerAPI : public ServerAPI { +public: + explicit ethServerAPI(EthernetClient &_client); }; /** * Listens for incoming connections and does accepts and creates instances of EthernetServerAPI as needed */ -class ethServerPort : public APIServerPort -{ - public: - explicit ethServerPort(int port); +class ethServerPort : public APIServerPort { +public: + explicit ethServerPort(int port); }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); diff --git a/src/mesh/compression/unishox2.cpp b/src/mesh/compression/unishox2.cpp index 9fc012a76..dd81d81d5 100644 --- a/src/mesh/compression/unishox2.cpp +++ b/src/mesh/compression/unishox2.cpp @@ -48,14 +48,12 @@ const char *USX_TEMPLATES[] = {"tfff-of-tfTtf:rf:rf.fffZ", "tfff-of-tf", "(fff) /// possible horizontal sets and states enum { USX_ALPHA = 0, USX_SYM, USX_NUM, USX_DICT, USX_DELTA, USX_NUM_TEMP }; -/// This 2D array has the characters for the sets USX_ALPHA, USX_SYM and USX_NUM. Where a character cannot fit into a uint8_t, 0 -/// is used and handled in code. -uint8_t usx_sets[][28] = {{0, ' ', 'e', 't', 'a', 'o', 'i', 'n', 's', 'r', 'l', 'c', 'd', 'h', - 'u', 'p', 'm', 'b', 'g', 'w', 'f', 'y', 'v', 'k', 'q', 'j', 'x', 'z'}, - {'"', '{', '}', '_', '<', '>', ':', '\n', 0, '[', ']', '\\', ';', '\'', - '\t', '@', '*', '&', '?', '!', '^', '|', '\r', '~', '`', 0, 0, 0}, - {0, ',', '.', '0', '1', '9', '2', '5', '-', '/', '3', '4', '6', '7', - '8', '(', ')', ' ', '=', '+', '$', '%', '#', 0, 0, 0, 0, 0}}; +/// This 2D array has the characters for the sets USX_ALPHA, USX_SYM and USX_NUM. Where a character cannot fit into a +/// uint8_t, 0 is used and handled in code. +uint8_t usx_sets[][28] = { + {0, ' ', 'e', 't', 'a', 'o', 'i', 'n', 's', 'r', 'l', 'c', 'd', 'h', 'u', 'p', 'm', 'b', 'g', 'w', 'f', 'y', 'v', 'k', 'q', 'j', 'x', 'z'}, + {'"', '{', '}', '_', '<', '>', ':', '\n', 0, '[', ']', '\\', ';', '\'', '\t', '@', '*', '&', '?', '!', '^', '|', '\r', '~', '`', 0, 0, 0}, + {0, ',', '.', '0', '1', '9', '2', '5', '-', '/', '3', '4', '6', '7', '8', '(', ')', ' ', '=', '+', '$', '%', '#', 0, 0, 0, 0, 0}}; /// Stores position of letter in usx_sets. /// First 3 bits - position in usx_hcodes @@ -69,8 +67,8 @@ uint8_t usx_vcodes[] = {0x00, 0x40, 0x60, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0x /// Length of each veritical code uint8_t usx_vcode_lens[] = {2, 3, 3, 4, 4, 4, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}; -/// Vertical Codes and Set number for frequent sequences in sets USX_SYM and USX_NUM. First 3 bits indicate set (USX_SYM/USX_NUM) -/// and rest are vcode positions +/// Vertical Codes and Set number for frequent sequences in sets USX_SYM and USX_NUM. First 3 bits indicate set +/// (USX_SYM/USX_NUM) and rest are vcode positions uint8_t usx_freq_codes[] = {(1 << 5) + 25, (1 << 5) + 26, (1 << 5) + 27, (2 << 5) + 23, (2 << 5) + 24, (2 << 5) + 25}; /// Not used @@ -125,22 +123,21 @@ uint8_t is_inited = 0; /// Fills the usx_code_94 94 letter array based on sets of characters at usx_sets \n /// For each element in usx_code_94, first 3 msb bits is set (USX_ALPHA / USX_SYM / USX_NUM) \n /// and the rest 5 bits indicate the vertical position in the corresponding set -void init_coder() -{ - if (is_inited) - return; - memset(usx_code_94, '\0', sizeof(usx_code_94)); - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 28; j++) { - uint8_t c = usx_sets[i][j]; - if (c > 32) { - usx_code_94[c - USX_OFFSET_94] = (i << 5) + j; - if (c >= 'a' && c <= 'z') - usx_code_94[c - USX_OFFSET_94 - ('a' - 'A')] = (i << 5) + j; - } - } +void init_coder() { + if (is_inited) + return; + memset(usx_code_94, '\0', sizeof(usx_code_94)); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 28; j++) { + uint8_t c = usx_sets[i][j]; + if (c > 32) { + usx_code_94[c - USX_OFFSET_94] = (i << 5) + j; + if (c >= 'a' && c <= 'z') + usx_code_94[c - USX_OFFSET_94 - ('a' - 'A')] = (i << 5) + j; + } } - is_inited = 1; + } + is_inited = 1; } /// Mask for retrieving each code to be encoded according to its length @@ -149,84 +146,80 @@ unsigned int usx_mask[] = {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF}; /// Appends specified number of bits to the output (out) \n /// If maximum limit (olen) is reached, -1 is returned \n /// Otherwise clen bits in code are appended to out starting with MSB -int append_bits(char *out, int olen, int ol, uint8_t code, int clen) -{ +int append_bits(char *out, int olen, int ol, uint8_t code, int clen) { - // printf("%d,%x,%d,%d\n", ol, code, clen, state); + // printf("%d,%x,%d,%d\n", ol, code, clen, state); - while (clen > 0) { - int oidx; - unsigned char a_byte; + while (clen > 0) { + int oidx; + unsigned char a_byte; - uint8_t cur_bit = ol % 8; - uint8_t blen = clen; - a_byte = code & usx_mask[blen - 1]; - a_byte >>= cur_bit; - if (blen + cur_bit > 8) - blen = (8 - cur_bit); - oidx = ol / 8; - if (oidx < 0 || olen <= oidx) - return -1; - if (cur_bit == 0) - out[oidx] = a_byte; - else - out[oidx] |= a_byte; - code <<= blen; - ol += blen; - clen -= blen; - } - return ol; + uint8_t cur_bit = ol % 8; + uint8_t blen = clen; + a_byte = code & usx_mask[blen - 1]; + a_byte >>= cur_bit; + if (blen + cur_bit > 8) + blen = (8 - cur_bit); + oidx = ol / 8; + if (oidx < 0 || olen <= oidx) + return -1; + if (cur_bit == 0) + out[oidx] = a_byte; + else + out[oidx] |= a_byte; + code <<= blen; + ol += blen; + clen -= blen; + } + return ol; } /// This is a safe call to append_bits() making sure it does not write past olen -#define SAFE_APPEND_BITS(exp) \ - do { \ - const int newidx = (exp); \ - if (newidx < 0) \ - return newidx; \ - } while (0) +#define SAFE_APPEND_BITS(exp) \ + do { \ + const int newidx = (exp); \ + if (newidx < 0) \ + return newidx; \ + } while (0) /// Appends switch code to out depending on the state (USX_DELTA or other) -int append_switch_code(char *out, int olen, int ol, uint8_t state) -{ - if (state == USX_DELTA) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SW_CODE, UNI_STATE_SW_CODE_LEN)); - } else - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, SW_CODE, SW_CODE_LEN)); - return ol; +int append_switch_code(char *out, int olen, int ol, uint8_t state) { + if (state == USX_DELTA) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SW_CODE, UNI_STATE_SW_CODE_LEN)); + } else + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, SW_CODE, SW_CODE_LEN)); + return ol; } /// Appends given horizontal and veritical code bits to out -int append_code(char *out, int olen, int ol, uint8_t code, uint8_t *state, const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[]) -{ - uint8_t hcode = code >> 5; - uint8_t vcode = code & 0x1F; - if (!usx_hcode_lens[hcode] && hcode != USX_ALPHA) - return ol; - switch (hcode) { - case USX_ALPHA: - if (*state != USX_ALPHA) { - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - *state = USX_ALPHA; - } - break; - case USX_SYM: - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_SYM], usx_hcode_lens[USX_SYM])); - break; - case USX_NUM: - if (*state != USX_NUM) { - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); - if (usx_sets[hcode][vcode] >= '0' && usx_sets[hcode][vcode] <= '9') - *state = USX_NUM; - } - } - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[vcode], usx_vcode_lens[vcode])); +int append_code(char *out, int olen, int ol, uint8_t code, uint8_t *state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { + uint8_t hcode = code >> 5; + uint8_t vcode = code & 0x1F; + if (!usx_hcode_lens[hcode] && hcode != USX_ALPHA) return ol; + switch (hcode) { + case USX_ALPHA: + if (*state != USX_ALPHA) { + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + *state = USX_ALPHA; + } + break; + case USX_SYM: + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_SYM], usx_hcode_lens[USX_SYM])); + break; + case USX_NUM: + if (*state != USX_NUM) { + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); + if (usx_sets[hcode][vcode] >= '0' && usx_sets[hcode][vcode] <= '9') + *state = USX_NUM; + } + } + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[vcode], usx_vcode_lens[vcode])); + return ol; } /// Length of bits used to represent count for each level @@ -236,22 +229,21 @@ const int32_t count_adder[5] = {4, 20, 148, 2196, 67732}; /// Codes used to specify the level that the count belongs to const uint8_t count_codes[] = {0x01, 0x82, 0xC3, 0xE4, 0xF4}; /// Encodes given count to out -int encodeCount(char *out, int olen, int ol, int count) -{ - // First five bits are code and Last three bits of codes represent length - for (int i = 0; i < 5; i++) { - if (count < count_adder[i]) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); - uint16_t count16 = (count - (i ? count_adder[i - 1] : 0)) << (16 - count_bit_lens[i]); - if (count_bit_lens[i] > 8) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 & 0xFF, count_bit_lens[i] - 8)); - } else - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, count_bit_lens[i])); - return ol; - } +int encodeCount(char *out, int olen, int ol, int count) { + // First five bits are code and Last three bits of codes represent length + for (int i = 0; i < 5; i++) { + if (count < count_adder[i]) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); + uint16_t count16 = (count - (i ? count_adder[i - 1] : 0)) << (16 - count_bit_lens[i]); + if (count_bit_lens[i] > 8) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 & 0xFF, count_bit_lens[i] - 8)); + } else + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, count_bit_lens[i])); + return ol; } - return ol; + } + return ol; } /// Length of bits used to represent delta code for each level @@ -260,78 +252,75 @@ const uint8_t uni_bit_len[5] = {6, 12, 14, 16, 21}; const int32_t uni_adder[5] = {0, 64, 4160, 20544, 86080}; /// Encodes the unicode code point given by code to out. prev_code is used to calculate the delta -int encodeUnicode(char *out, int olen, int ol, int32_t code, int32_t prev_code) -{ - // First five bits are code and Last three bits of codes represent length - // const uint8_t codes[8] = {0x00, 0x42, 0x83, 0xA3, 0xC3, 0xE4, 0xF5, 0xFD}; - const uint8_t codes[6] = {0x01, 0x82, 0xC3, 0xE4, 0xF5, 0xFD}; - int32_t till = 0; - int32_t diff = code - prev_code; - if (diff < 0) - diff = -diff; - // printf("%ld, ", code); - // printf("Diff: %d\n", diff); - for (int i = 0; i < 5; i++) { - till += (1 << uni_bit_len[i]); - if (diff < till) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (codes[i] & 0xF8), codes[i] & 0x07)); - // if (diff) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, prev_code > code ? 0x80 : 0, 1)); - int32_t val = diff - uni_adder[i]; - // printf("Val: %d\n", val); - if (uni_bit_len[i] > 16) { - val <<= (24 - uni_bit_len[i]); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 16, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (val >> 8) & 0xFF, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 16)); - } else if (uni_bit_len[i] > 8) { - val <<= (16 - uni_bit_len[i]); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 8, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 8)); - } else { - val <<= (8 - uni_bit_len[i]); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i])); - } - return ol; - } +int encodeUnicode(char *out, int olen, int ol, int32_t code, int32_t prev_code) { + // First five bits are code and Last three bits of codes represent length + // const uint8_t codes[8] = {0x00, 0x42, 0x83, 0xA3, 0xC3, 0xE4, 0xF5, 0xFD}; + const uint8_t codes[6] = {0x01, 0x82, 0xC3, 0xE4, 0xF5, 0xFD}; + int32_t till = 0; + int32_t diff = code - prev_code; + if (diff < 0) + diff = -diff; + // printf("%ld, ", code); + // printf("Diff: %d\n", diff); + for (int i = 0; i < 5; i++) { + till += (1 << uni_bit_len[i]); + if (diff < till) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (codes[i] & 0xF8), codes[i] & 0x07)); + // if (diff) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, prev_code > code ? 0x80 : 0, 1)); + int32_t val = diff - uni_adder[i]; + // printf("Val: %d\n", val); + if (uni_bit_len[i] > 16) { + val <<= (24 - uni_bit_len[i]); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 16, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (val >> 8) & 0xFF, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 16)); + } else if (uni_bit_len[i] > 8) { + val <<= (16 - uni_bit_len[i]); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 8, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 8)); + } else { + val <<= (8 - uni_bit_len[i]); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i])); + } + return ol; } - return ol; + } + return ol; } /// Reads UTF-8 character from in. Also returns the number of bytes occupied by the UTF-8 character in utf8len -int32_t readUTF8(const char *in, int len, int l, int *utf8len) -{ - int32_t ret = 0; - if (l < (len - 1) && (in[l] & 0xE0) == 0xC0 && (in[l + 1] & 0xC0) == 0x80) { - *utf8len = 2; - ret = (in[l] & 0x1F); - ret <<= 6; - ret += (in[l + 1] & 0x3F); - if (ret < 0x80) - ret = 0; - } else if (l < (len - 2) && (in[l] & 0xF0) == 0xE0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80) { - *utf8len = 3; - ret = (in[l] & 0x0F); - ret <<= 6; - ret += (in[l + 1] & 0x3F); - ret <<= 6; - ret += (in[l + 2] & 0x3F); - if (ret < 0x0800) - ret = 0; - } else if (l < (len - 3) && (in[l] & 0xF8) == 0xF0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80 && - (in[l + 3] & 0xC0) == 0x80) { - *utf8len = 4; - ret = (in[l] & 0x07); - ret <<= 6; - ret += (in[l + 1] & 0x3F); - ret <<= 6; - ret += (in[l + 2] & 0x3F); - ret <<= 6; - ret += (in[l + 3] & 0x3F); - if (ret < 0x10000) - ret = 0; - } - return ret; +int32_t readUTF8(const char *in, int len, int l, int *utf8len) { + int32_t ret = 0; + if (l < (len - 1) && (in[l] & 0xE0) == 0xC0 && (in[l + 1] & 0xC0) == 0x80) { + *utf8len = 2; + ret = (in[l] & 0x1F); + ret <<= 6; + ret += (in[l + 1] & 0x3F); + if (ret < 0x80) + ret = 0; + } else if (l < (len - 2) && (in[l] & 0xF0) == 0xE0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80) { + *utf8len = 3; + ret = (in[l] & 0x0F); + ret <<= 6; + ret += (in[l + 1] & 0x3F); + ret <<= 6; + ret += (in[l + 2] & 0x3F); + if (ret < 0x0800) + ret = 0; + } else if (l < (len - 3) && (in[l] & 0xF8) == 0xF0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80 && (in[l + 3] & 0xC0) == 0x80) { + *utf8len = 4; + ret = (in[l] & 0x07); + ret <<= 6; + ret += (in[l + 1] & 0x3F); + ret <<= 6; + ret += (in[l + 2] & 0x3F); + ret <<= 6; + ret += (in[l + 3] & 0x3F); + if (ret < 0x10000) + ret = 0; + } + return ret; } /// Finds the longest matching sequence from the beginning of the string. \n @@ -340,41 +329,40 @@ int32_t readUTF8(const char *in, int len, int l, int *utf8len) /// This is a crude implementation that is not optimized. Assuming only short strings \n /// are encoded, this is not much of an issue. int matchOccurance(const char *in, int len, int l, char *out, int olen, int *ol, const uint8_t *state, const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[]) -{ - int j, k; - int longest_dist = 0; - int longest_len = 0; - for (j = l - NICE_LEN; j >= 0; j--) { - for (k = l; k < len && j + k - l < l; k++) { - if (in[k] != in[j + k - l]) - break; - } - while ((((unsigned char)in[k]) >> 6) == 2) - k--; // Skip partial UTF-8 matches - // if ((in[k - 1] >> 3) == 0x1E || (in[k - 1] >> 4) == 0x0E || (in[k - 1] >> 5) == 0x06) - // k--; - if ((k - l) > (NICE_LEN - 1)) { - int match_len = k - l - NICE_LEN; - int match_dist = l - j - NICE_LEN + 1; - if (match_len > longest_len) { - longest_len = match_len; - longest_dist = match_dist; - } - } + const uint8_t usx_hcode_lens[]) { + int j, k; + int longest_dist = 0; + int longest_len = 0; + for (j = l - NICE_LEN; j >= 0; j--) { + for (k = l; k < len && j + k - l < l; k++) { + if (in[k] != in[j + k - l]) + break; } - if (longest_len) { - SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); - SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); - // printf("Len:%d / Dist:%d/%.*s\n", longest_len, longest_dist, longest_len + NICE_LEN, in + l - longest_dist - NICE_LEN + - // 1); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_len)); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_dist)); - l += (longest_len + NICE_LEN); - l--; - return l; + while ((((unsigned char)in[k]) >> 6) == 2) + k--; // Skip partial UTF-8 matches + // if ((in[k - 1] >> 3) == 0x1E || (in[k - 1] >> 4) == 0x0E || (in[k - 1] >> 5) == 0x06) + // k--; + if ((k - l) > (NICE_LEN - 1)) { + int match_len = k - l - NICE_LEN; + int match_dist = l - j - NICE_LEN + 1; + if (match_len > longest_len) { + longest_len = match_len; + longest_dist = match_dist; + } } - return -l; + } + if (longest_len) { + SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); + SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); + // printf("Len:%d / Dist:%d/%.*s\n", longest_len, longest_dist, longest_len + NICE_LEN, in + l - longest_dist - + // NICE_LEN + 1); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_len)); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_dist)); + l += (longest_len + NICE_LEN); + l--; + return l; + } + return -l; } /// This is used only when encoding a string array @@ -384,75 +372,73 @@ int matchOccurance(const char *in, int len, int l, char *out, int olen, int *ol, /// This is a crude implementation that is not optimized. Assuming only short strings \n /// are encoded, this is not much of an issue. int matchLine(const char *in, int len, int l, char *out, int olen, int *ol, struct us_lnk_lst *prev_lines, const uint8_t *state, - const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) -{ - int last_ol = *ol; - int last_len = 0; - int last_dist = 0; - int last_ctx = 0; - int line_ctr = 0; - int j = 0; - do { - int i, k; - int line_len = (int)strlen(prev_lines->data); - int limit = (line_ctr == 0 ? l : line_len); - for (; j < limit; j++) { - for (i = l, k = j; k < line_len && i < len; k++, i++) { - if (prev_lines->data[k] != in[i]) - break; - } - while ((((unsigned char)prev_lines->data[k]) >> 6) == 2) - k--; // Skip partial UTF-8 matches - if ((k - j) >= NICE_LEN) { - if (last_len) { - if (j > last_dist) - continue; - // int saving = ((k - j) - last_len) + (last_dist - j) + (last_ctx - line_ctr); - // if (saving < 0) { - // //printf("No savng: %d\n", saving); - // continue; - // } - *ol = last_ol; - } - last_len = (k - j); - last_dist = j; - last_ctx = line_ctr; - SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); - SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_len - NICE_LEN)); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_dist)); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_ctx)); - /* - if ((*ol - last_ol) > (last_len * 4)) { - last_len = 0; - *ol = last_ol; - }*/ - // printf("Len: %d, Dist: %d, Line: %d\n", last_len, last_dist, last_ctx); - j += last_len; - } + const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { + int last_ol = *ol; + int last_len = 0; + int last_dist = 0; + int last_ctx = 0; + int line_ctr = 0; + int j = 0; + do { + int i, k; + int line_len = (int)strlen(prev_lines->data); + int limit = (line_ctr == 0 ? l : line_len); + for (; j < limit; j++) { + for (i = l, k = j; k < line_len && i < len; k++, i++) { + if (prev_lines->data[k] != in[i]) + break; + } + while ((((unsigned char)prev_lines->data[k]) >> 6) == 2) + k--; // Skip partial UTF-8 matches + if ((k - j) >= NICE_LEN) { + if (last_len) { + if (j > last_dist) + continue; + // int saving = ((k - j) - last_len) + (last_dist - j) + (last_ctx - line_ctr); + // if (saving < 0) { + // //printf("No savng: %d\n", saving); + // continue; + // } + *ol = last_ol; } - line_ctr++; - prev_lines = prev_lines->previous; - } while (prev_lines && prev_lines->data != NULL); - if (last_len) { - l += last_len; - l--; - return l; + last_len = (k - j); + last_dist = j; + last_ctx = line_ctr; + SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); + SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_len - NICE_LEN)); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_dist)); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_ctx)); + /* + if ((*ol - last_ol) > (last_len * 4)) { + last_len = 0; + *ol = last_ol; + }*/ + // printf("Len: %d, Dist: %d, Line: %d\n", last_len, last_dist, last_ctx); + j += last_len; + } } - return -l; + line_ctr++; + prev_lines = prev_lines->previous; + } while (prev_lines && prev_lines->data != NULL); + if (last_len) { + l += last_len; + l--; + return l; + } + return -l; } /// Returns 4 bit code assuming ch falls between '0' to '9', \n /// 'A' to 'F' or 'a' to 'f' -uint8_t getBaseCode(char ch) -{ - if (ch >= '0' && ch <= '9') - return (ch - '0') << 4; - else if (ch >= 'A' && ch <= 'F') - return (ch - 'A' + 10) << 4; - else if (ch >= 'a' && ch <= 'f') - return (ch - 'a' + 10) << 4; - return 0; +uint8_t getBaseCode(char ch) { + if (ch >= '0' && ch <= '9') + return (ch - '0') << 4; + else if (ch >= 'A' && ch <= 'F') + return (ch - 'A' + 10) << 4; + else if (ch >= 'a' && ch <= 'f') + return (ch - 'a' + 10) << 4; + return 0; } /// Enum indicating nibble type - USX_NIB_NUM means ch is a number '0' to '9', \n @@ -461,440 +447,415 @@ uint8_t getBaseCode(char ch) enum { USX_NIB_NUM = 0, USX_NIB_HEX_LOWER, USX_NIB_HEX_UPPER, USX_NIB_NOT }; /// Gets 4 bit code assuming ch falls between '0' to '9', \n /// 'A' to 'F' or 'a' to 'f' -char getNibbleType(char ch) -{ - if (ch >= '0' && ch <= '9') - return USX_NIB_NUM; - else if (ch >= 'a' && ch <= 'f') - return USX_NIB_HEX_LOWER; - else if (ch >= 'A' && ch <= 'F') - return USX_NIB_HEX_UPPER; - return USX_NIB_NOT; +char getNibbleType(char ch) { + if (ch >= '0' && ch <= '9') + return USX_NIB_NUM; + else if (ch >= 'a' && ch <= 'f') + return USX_NIB_HEX_LOWER; + else if (ch >= 'A' && ch <= 'F') + return USX_NIB_HEX_UPPER; + return USX_NIB_NOT; } /// Starts coding of nibble sets -int append_nibble_escape(char *out, int olen, int ol, uint8_t state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) -{ - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, 0, 2)); - return ol; +int append_nibble_escape(char *out, int olen, int ol, uint8_t state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, 0, 2)); + return ol; } /// Returns minimum value of two longs -long min_of(long c, long i) -{ - return c > i ? i : c; -} +long min_of(long c, long i) { return c > i ? i : c; } -/// Appends the terminator code depending on the state, preset and whether full terminator needs to be encoded to out or not \n -int append_final_bits(char *const out, const int olen, int ol, const uint8_t state, const uint8_t is_all_upper, - const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) -{ - if (usx_hcode_lens[USX_ALPHA]) { - if (USX_NUM != state) { - // for num state, append TERM_CODE directly - // for other state, switch to Num Set first - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); - } - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[TERM_CODE & 0x1F], usx_vcode_lens[TERM_CODE & 0x1F])); - } else { - // preset 1, terminate at 2 or 3 SW_CODE, i.e., 4 or 6 continuous 0 bits - // see discussion: https://github.com/siara-cc/Unishox/issues/19#issuecomment-922435580 - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, TERM_BYTE_PRESET_1, - is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER)); +/// Appends the terminator code depending on the state, preset and whether full terminator needs to be encoded to out or +/// not \n +int append_final_bits(char *const out, const int olen, int ol, const uint8_t state, const uint8_t is_all_upper, const uint8_t usx_hcodes[], + const uint8_t usx_hcode_lens[]) { + if (usx_hcode_lens[USX_ALPHA]) { + if (USX_NUM != state) { + // for num state, append TERM_CODE directly + // for other state, switch to Num Set first + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); } + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[TERM_CODE & 0x1F], usx_vcode_lens[TERM_CODE & 0x1F])); + } else { + // preset 1, terminate at 2 or 3 SW_CODE, i.e., 4 or 6 continuous 0 bits + // see discussion: https://github.com/siara-cc/Unishox/issues/19#issuecomment-922435580 + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, TERM_BYTE_PRESET_1, is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER)); + } - // fill uint8_t with the last bit - SAFE_APPEND_BITS( - ol = append_bits(out, olen, ol, (ol == 0 || out[(ol - 1) / 8] << ((ol - 1) & 7) >= 0) ? 0 : 0xFF, (8 - ol % 8) & 7)); + // fill uint8_t with the last bit + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (ol == 0 || out[(ol - 1) / 8] << ((ol - 1) & 7) >= 0) ? 0 : 0xFF, (8 - ol % 8) & 7)); - return ol; + return ol; } /// Macro used in the main compress function so that if the output len exceeds given maximum length (olen) it can exit -#define SAFE_APPEND_BITS2(olen, exp) \ - do { \ - const int newidx = (exp); \ - const int __olen = (olen); \ - if (newidx < 0) \ - return __olen >= 0 ? __olen + 1 : (1 - __olen) * 4; \ - } while (0) +#define SAFE_APPEND_BITS2(olen, exp) \ + do { \ + const int newidx = (exp); \ + const int __olen = (olen); \ + if (newidx < 0) \ + return __olen >= 0 ? __olen + 1 : (1 - __olen) * 4; \ + } while (0) // Main API function. See unishox2.h for documentation int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], - struct us_lnk_lst *prev_lines) -{ + const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines) { - uint8_t state; + uint8_t state; - int l, ll, ol; - char c_in, c_next; - int prev_uni; - uint8_t is_upper, is_all_upper; + int l, ll, ol; + char c_in, c_next; + int prev_uni; + uint8_t is_upper, is_all_upper; #if (UNISHOX_API_OUT_AND_LEN(0, 1)) == 0 - const int olen = INT_MAX - 1; - const int rawolen = olen; - const uint8_t need_full_term_codes = 0; + const int olen = INT_MAX - 1; + const int rawolen = olen; + const uint8_t need_full_term_codes = 0; #else - const int rawolen = olen; - uint8_t need_full_term_codes = 0; - if (olen < 0) { - need_full_term_codes = 1; - olen *= -1; - } + const int rawolen = olen; + uint8_t need_full_term_codes = 0; + if (olen < 0) { + need_full_term_codes = 1; + olen *= -1; + } #endif - init_coder(); - ol = 0; - prev_uni = 0; - state = USX_ALPHA; - is_all_upper = 0; - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNISHOX_MAGIC_BITS, UNISHOX_MAGIC_BIT_LEN)); // magic bit(s) - for (l = 0; l < len; l++) { + init_coder(); + ol = 0; + prev_uni = 0; + state = USX_ALPHA; + is_all_upper = 0; + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNISHOX_MAGIC_BITS, UNISHOX_MAGIC_BIT_LEN)); // magic bit(s) + for (l = 0; l < len; l++) { - if (usx_hcode_lens[USX_DICT] && l < (len - NICE_LEN + 1)) { - if (prev_lines) { - l = matchLine(in, len, l, out, olen, &ol, prev_lines, &state, usx_hcodes, usx_hcode_lens); - if (l > 0) { - continue; - } else if (l < 0 && ol < 0) { - return olen + 1; - } - l = -l; - } else { - l = matchOccurance(in, len, l, out, olen, &ol, &state, usx_hcodes, usx_hcode_lens); - if (l > 0) { - continue; - } else if (l < 0 && ol < 0) { - return olen + 1; - } - l = -l; - } + if (usx_hcode_lens[USX_DICT] && l < (len - NICE_LEN + 1)) { + if (prev_lines) { + l = matchLine(in, len, l, out, olen, &ol, prev_lines, &state, usx_hcodes, usx_hcode_lens); + if (l > 0) { + continue; + } else if (l < 0 && ol < 0) { + return olen + 1; } - - c_in = in[l]; - if (l && len > 4 && l < (len - 4) && usx_hcode_lens[USX_NUM]) { - if (c_in == in[l - 1] && c_in == in[l + 1] && c_in == in[l + 2] && c_in == in[l + 3]) { - int rpt_count = l + 4; - while (rpt_count < len && in[rpt_count] == c_in) - rpt_count++; - rpt_count -= l; - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, RPT_CODE, &state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rpt_count - 4)); - l += rpt_count; - l--; - continue; - } + l = -l; + } else { + l = matchOccurance(in, len, l, out, olen, &ol, &state, usx_hcodes, usx_hcode_lens); + if (l > 0) { + continue; + } else if (l < 0 && ol < 0) { + return olen + 1; } + l = -l; + } + } - if (l <= (len - 36) && usx_hcode_lens[USX_NUM]) { - if (in[l + 8] == '-' && in[l + 13] == '-' && in[l + 18] == '-' && in[l + 23] == '-') { - char hex_type = USX_NIB_NUM; - int uid_pos = l; - for (; uid_pos < l + 36; uid_pos++) { - char c_uid = in[uid_pos]; - if (c_uid == '-' && (uid_pos == 8 || uid_pos == 13 || uid_pos == 18 || uid_pos == 23)) - continue; - char nib_type = getNibbleType(c_uid); - if (nib_type == USX_NIB_NOT) - break; - if (nib_type != USX_NIB_NUM) { - if (hex_type != USX_NIB_NUM && hex_type != nib_type) - break; - hex_type = nib_type; - } - } - if (uid_pos == l + 36) { - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0xC0 : 0xF0), - (hex_type == USX_NIB_HEX_LOWER ? 3 : 5))); - for (uid_pos = l; uid_pos < l + 36; uid_pos++) { - char c_uid = in[uid_pos]; - if (c_uid != '-') - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(c_uid), 4)); - } - // printf("GUID:\n"); - l += 35; - continue; - } - } + c_in = in[l]; + if (l && len > 4 && l < (len - 4) && usx_hcode_lens[USX_NUM]) { + if (c_in == in[l - 1] && c_in == in[l + 1] && c_in == in[l + 2] && c_in == in[l + 3]) { + int rpt_count = l + 4; + while (rpt_count < len && in[rpt_count] == c_in) + rpt_count++; + rpt_count -= l; + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, RPT_CODE, &state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rpt_count - 4)); + l += rpt_count; + l--; + continue; + } + } + + if (l <= (len - 36) && usx_hcode_lens[USX_NUM]) { + if (in[l + 8] == '-' && in[l + 13] == '-' && in[l + 18] == '-' && in[l + 23] == '-') { + char hex_type = USX_NIB_NUM; + int uid_pos = l; + for (; uid_pos < l + 36; uid_pos++) { + char c_uid = in[uid_pos]; + if (c_uid == '-' && (uid_pos == 8 || uid_pos == 13 || uid_pos == 18 || uid_pos == 23)) + continue; + char nib_type = getNibbleType(c_uid); + if (nib_type == USX_NIB_NOT) + break; + if (nib_type != USX_NIB_NUM) { + if (hex_type != USX_NIB_NUM && hex_type != nib_type) + break; + hex_type = nib_type; + } } - - if (l < (len - 5) && usx_hcode_lens[USX_NUM]) { - char hex_type = USX_NIB_NUM; - int hex_len = 0; - do { - char nib_type = getNibbleType(in[l + hex_len]); - if (nib_type == USX_NIB_NOT) - break; - if (nib_type != USX_NIB_NUM) { - if (hex_type != USX_NIB_NUM && hex_type != nib_type) - break; - hex_type = nib_type; - } - hex_len++; - } while (l + hex_len < len); - if (hex_len > 10 && hex_type == USX_NIB_NUM) - hex_type = USX_NIB_HEX_LOWER; - if ((hex_type == USX_NIB_HEX_LOWER || hex_type == USX_NIB_HEX_UPPER) && hex_len > 3) { - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0x80 : 0xE0), - (hex_type == USX_NIB_HEX_LOWER ? 2 : 4))); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, hex_len)); - do { - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l++]), 4)); - } while (--hex_len); - l--; - continue; - } + if (uid_pos == l + 36) { + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, + ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0xC0 : 0xF0), (hex_type == USX_NIB_HEX_LOWER ? 3 : 5))); + for (uid_pos = l; uid_pos < l + 36; uid_pos++) { + char c_uid = in[uid_pos]; + if (c_uid != '-') + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(c_uid), 4)); + } + // printf("GUID:\n"); + l += 35; + continue; } + } + } - if (usx_templates != NULL) { - int i; - for (i = 0; i < 5; i++) { - if (usx_templates[i]) { - int rem = (int)strlen(usx_templates[i]); - int j = 0; - for (; j < rem && l + j < len; j++) { - char c_t = usx_templates[i][j]; - c_in = in[l + j]; - if (c_t == 'f' || c_t == 'F') { - if (getNibbleType(c_in) != (c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER) && - getNibbleType(c_in) != USX_NIB_NUM) { - break; - } - } else if (c_t == 'r' || c_t == 't' || c_t == 'o') { - if (c_in < '0' || c_in > (c_t == 'r' ? '7' : (c_t == 't' ? '3' : '1'))) - break; - } else if (c_t != c_in) - break; - } - if (((float)j / rem) > 0.66) { - // printf("%s\n", usx_templates[i]); - rem = rem - j; - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0, 1)); - SAFE_APPEND_BITS2(rawolen, - ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rem)); - for (int k = 0; k < j; k++) { - char c_t = usx_templates[i][k]; - if (c_t == 'f' || c_t == 'F') - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l + k]), 4)); - else if (c_t == 'r' || c_t == 't' || c_t == 'o') { - c_t = (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (in[l + k] - '0') << (8 - c_t), c_t)); - } - } - l += j; - l--; - break; - } - } - } - if (i < 5) - continue; + if (l < (len - 5) && usx_hcode_lens[USX_NUM]) { + char hex_type = USX_NIB_NUM; + int hex_len = 0; + do { + char nib_type = getNibbleType(in[l + hex_len]); + if (nib_type == USX_NIB_NOT) + break; + if (nib_type != USX_NIB_NUM) { + if (hex_type != USX_NIB_NUM && hex_type != nib_type) + break; + hex_type = nib_type; } + hex_len++; + } while (l + hex_len < len); + if (hex_len > 10 && hex_type == USX_NIB_NUM) + hex_type = USX_NIB_HEX_LOWER; + if ((hex_type == USX_NIB_HEX_LOWER || hex_type == USX_NIB_HEX_UPPER) && hex_len > 3) { + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, + ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0x80 : 0xE0), (hex_type == USX_NIB_HEX_LOWER ? 2 : 4))); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, hex_len)); + do { + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l++]), 4)); + } while (--hex_len); + l--; + continue; + } + } - if (usx_freq_seq != NULL) { - int i; - for (i = 0; i < 6; i++) { - int seq_len = (int)strlen(usx_freq_seq[i]); - if (len - seq_len >= 0 && l <= len - seq_len) { - if (memcmp(usx_freq_seq[i], in + l, seq_len) == 0 && usx_hcode_lens[usx_freq_codes[i] >> 5]) { - SAFE_APPEND_BITS2(rawolen, - ol = append_code(out, olen, ol, usx_freq_codes[i], &state, usx_hcodes, usx_hcode_lens)); - l += seq_len; - l--; - break; - } - } + if (usx_templates != NULL) { + int i; + for (i = 0; i < 5; i++) { + if (usx_templates[i]) { + int rem = (int)strlen(usx_templates[i]); + int j = 0; + for (; j < rem && l + j < len; j++) { + char c_t = usx_templates[i][j]; + c_in = in[l + j]; + if (c_t == 'f' || c_t == 'F') { + if (getNibbleType(c_in) != (c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER) && getNibbleType(c_in) != USX_NIB_NUM) { + break; + } + } else if (c_t == 'r' || c_t == 't' || c_t == 'o') { + if (c_in < '0' || c_in > (c_t == 'r' ? '7' : (c_t == 't' ? '3' : '1'))) + break; + } else if (c_t != c_in) + break; + } + if (((float)j / rem) > 0.66) { + // printf("%s\n", usx_templates[i]); + rem = rem - j; + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0, 1)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rem)); + for (int k = 0; k < j; k++) { + char c_t = usx_templates[i][k]; + if (c_t == 'f' || c_t == 'F') + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l + k]), 4)); + else if (c_t == 'r' || c_t == 't' || c_t == 'o') { + c_t = (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (in[l + k] - '0') << (8 - c_t), c_t)); + } } - if (i < 6) - continue; + l += j; + l--; + break; + } } + } + if (i < 5) + continue; + } - c_in = in[l]; - - is_upper = 0; - if (c_in >= 'A' && c_in <= 'Z') - is_upper = 1; - else { - if (is_all_upper) { - is_all_upper = 0; - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - state = USX_ALPHA; - } + if (usx_freq_seq != NULL) { + int i; + for (i = 0; i < 6; i++) { + int seq_len = (int)strlen(usx_freq_seq[i]); + if (len - seq_len >= 0 && l <= len - seq_len) { + if (memcmp(usx_freq_seq[i], in + l, seq_len) == 0 && usx_hcode_lens[usx_freq_codes[i] >> 5]) { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, usx_freq_codes[i], &state, usx_hcodes, usx_hcode_lens)); + l += seq_len; + l--; + break; + } } - if (is_upper && !is_all_upper) { - if (state == USX_NUM) { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - state = USX_ALPHA; + } + if (i < 6) + continue; + } + + c_in = in[l]; + + is_upper = 0; + if (c_in >= 'A' && c_in <= 'Z') + is_upper = 1; + else { + if (is_all_upper) { + is_all_upper = 0; + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + state = USX_ALPHA; + } + } + if (is_upper && !is_all_upper) { + if (state == USX_NUM) { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + state = USX_ALPHA; + } + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + if (state == USX_DELTA) { + state = USX_ALPHA; + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + } + } + c_next = 0; + if (l + 1 < len) + c_next = in[l + 1]; + + if (c_in >= 32 && c_in <= 126) { + if (is_upper && !is_all_upper) { + for (ll = l + 4; ll >= l && ll < len; ll--) { + if (in[ll] < 'A' || in[ll] > 'Z') + break; + } + if (ll == l - 1) { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + state = USX_ALPHA; + is_all_upper = 1; + } + } + if (state == USX_DELTA && (c_in == ' ' || c_in == '.' || c_in == ',')) { + uint8_t spl_code = (c_in == ',' ? 0xC0 : (c_in == '.' ? 0xE0 : (c_in == ' ' ? 0 : 0xFF))); + if (spl_code != 0xFF) { + uint8_t spl_code_len = (c_in == ',' ? 3 : (c_in == '.' ? 4 : (c_in == ' ' ? 1 : 4))); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, spl_code, spl_code_len)); + continue; + } + } + c_in -= 32; + if (is_all_upper && is_upper) + c_in += 32; + if (c_in == 0) { + if (state == USX_NUM) + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[NUM_SPC_CODE & 0x1F], usx_vcode_lens[NUM_SPC_CODE & 0x1F])); + else + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); + } else { + c_in--; + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, usx_code_94[(int)c_in], &state, usx_hcodes, usx_hcode_lens)); + } + } else if (c_in == 13 && c_next == 10) { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CRLF_CODE, &state, usx_hcodes, usx_hcode_lens)); + l++; + } else if (c_in == 10) { + if (state == USX_DELTA) { + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF0, 4)); + } else + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, LF_CODE, &state, usx_hcodes, usx_hcode_lens)); + } else if (c_in == 13) { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CR_CODE, &state, usx_hcodes, usx_hcode_lens)); + } else if (c_in == '\t') { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, TAB_CODE, &state, usx_hcodes, usx_hcode_lens)); + } else { + int utf8len; + int32_t uni = readUTF8(in, len, l, &utf8len); + if (uni) { + l += utf8len; + if (state != USX_DELTA) { + int32_t uni2 = readUTF8(in, len, l, &utf8len); + if (uni2) { + if (state != USX_ALPHA) { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); } SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - if (state == USX_DELTA) { - state = USX_ALPHA; - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - } + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); // code for space (' ') + state = USX_DELTA; + } else { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_DELTA], usx_hcode_lens[USX_DELTA])); + } } - c_next = 0; - if (l + 1 < len) - c_next = in[l + 1]; - - if (c_in >= 32 && c_in <= 126) { - if (is_upper && !is_all_upper) { - for (ll = l + 4; ll >= l && ll < len; ll--) { - if (in[ll] < 'A' || in[ll] > 'Z') - break; - } - if (ll == l - 1) { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - state = USX_ALPHA; - is_all_upper = 1; - } - } - if (state == USX_DELTA && (c_in == ' ' || c_in == '.' || c_in == ',')) { - uint8_t spl_code = (c_in == ',' ? 0xC0 : (c_in == '.' ? 0xE0 : (c_in == ' ' ? 0 : 0xFF))); - if (spl_code != 0xFF) { - uint8_t spl_code_len = (c_in == ',' ? 3 : (c_in == '.' ? 4 : (c_in == ' ' ? 1 : 4))); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, spl_code, spl_code_len)); - continue; - } - } - c_in -= 32; - if (is_all_upper && is_upper) - c_in += 32; - if (c_in == 0) { - if (state == USX_NUM) - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[NUM_SPC_CODE & 0x1F], - usx_vcode_lens[NUM_SPC_CODE & 0x1F])); - else - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); - } else { - c_in--; - SAFE_APPEND_BITS2(rawolen, - ol = append_code(out, olen, ol, usx_code_94[(int)c_in], &state, usx_hcodes, usx_hcode_lens)); - } - } else if (c_in == 13 && c_next == 10) { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CRLF_CODE, &state, usx_hcodes, usx_hcode_lens)); - l++; - } else if (c_in == 10) { - if (state == USX_DELTA) { - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF0, 4)); - } else - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, LF_CODE, &state, usx_hcodes, usx_hcode_lens)); - } else if (c_in == 13) { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CR_CODE, &state, usx_hcodes, usx_hcode_lens)); - } else if (c_in == '\t') { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, TAB_CODE, &state, usx_hcodes, usx_hcode_lens)); - } else { - int utf8len; - int32_t uni = readUTF8(in, len, l, &utf8len); - if (uni) { - l += utf8len; - if (state != USX_DELTA) { - int32_t uni2 = readUTF8(in, len, l, &utf8len); - if (uni2) { - if (state != USX_ALPHA) { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, - ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - } - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, - ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - SAFE_APPEND_BITS2( - rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); // code for space (' ') - state = USX_DELTA; - } else { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, - ol = append_bits(out, olen, ol, usx_hcodes[USX_DELTA], usx_hcode_lens[USX_DELTA])); - } - } - SAFE_APPEND_BITS2(rawolen, ol = encodeUnicode(out, olen, ol, uni, prev_uni)); - // printf("%d:%d:%d\n", l, utf8len, uni); - prev_uni = uni; - l--; - } else { - int bin_count = 1; - for (int bi = l + 1; bi < len; bi++) { - char c_bi = in[bi]; - // if (c_bi > 0x1F && c_bi != 0x7F) - // break; - if (readUTF8(in, len, bi, &utf8len)) - break; - if (bi < (len - 4) && c_bi == in[bi - 1] && c_bi == in[bi + 1] && c_bi == in[bi + 2] && c_bi == in[bi + 3]) - break; - bin_count++; - } - // printf("Bin:%d:%d:%x:%d\n", l, (unsigned char) c_in, (unsigned char) c_in, bin_count); - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF8, 5)); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, bin_count)); - do { - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, in[l++], 8)); - } while (--bin_count); - l--; - } + SAFE_APPEND_BITS2(rawolen, ol = encodeUnicode(out, olen, ol, uni, prev_uni)); + // printf("%d:%d:%d\n", l, utf8len, uni); + prev_uni = uni; + l--; + } else { + int bin_count = 1; + for (int bi = l + 1; bi < len; bi++) { + char c_bi = in[bi]; + // if (c_bi > 0x1F && c_bi != 0x7F) + // break; + if (readUTF8(in, len, bi, &utf8len)) + break; + if (bi < (len - 4) && c_bi == in[bi - 1] && c_bi == in[bi + 1] && c_bi == in[bi + 2] && c_bi == in[bi + 3]) + break; + bin_count++; } + // printf("Bin:%d:%d:%x:%d\n", l, (unsigned char) c_in, (unsigned char) c_in, bin_count); + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF8, 5)); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, bin_count)); + do { + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, in[l++], 8)); + } while (--bin_count); + l--; + } } + } - if (need_full_term_codes) { - const int orig_ol = ol; - SAFE_APPEND_BITS2(rawolen, ol = append_final_bits(out, olen, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens)); - return (ol / 8) * 4 + (((ol - orig_ol) / 8) & 3); - } else { - const int rst = (ol + 7) / 8; - append_final_bits(out, rst, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens); - return rst; - } + if (need_full_term_codes) { + const int orig_ol = ol; + SAFE_APPEND_BITS2(rawolen, ol = append_final_bits(out, olen, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens)); + return (ol / 8) * 4 + (((ol - orig_ol) / 8) & 3); + } else { + const int rst = (ol + 7) / 8; + append_final_bits(out, rst, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens); + return rst; + } } // Main API function. See unishox2.h for documentation int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) -{ - return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, - usx_templates, NULL); + const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) { + return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, usx_templates, NULL); } // Main API function. See unishox2.h for documentation -int unishox2_compress_simple(const char *in, int len, char *out) -{ - return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, - USX_FREQ_SEQ_DFLT, USX_TEMPLATES, NULL); +int unishox2_compress_simple(const char *in, int len, char *out) { + return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_DFLT, + USX_TEMPLATES, NULL); } // Reads one bit from in -int readBit(const char *in, int bit_no) -{ - return in[bit_no >> 3] & (0x80 >> (bit_no % 8)); -} +int readBit(const char *in, int bit_no) { return in[bit_no >> 3] & (0x80 >> (bit_no % 8)); } // Reads next 8 bits, if available -int read8bitCode(const char *in, int len, int bit_no) -{ - int bit_pos = bit_no & 0x07; - int char_pos = bit_no >> 3; - len >>= 3; - uint8_t code = (((uint8_t)in[char_pos]) << bit_pos); - char_pos++; - if (char_pos < len) { - code |= ((uint8_t)in[char_pos]) >> (8 - bit_pos); - } else - code |= (0xFF >> (8 - bit_pos)); - return code; +int read8bitCode(const char *in, int len, int bit_no) { + int bit_pos = bit_no & 0x07; + int char_pos = bit_no >> 3; + len >>= 3; + uint8_t code = (((uint8_t)in[char_pos]) << bit_pos); + char_pos++; + if (char_pos < len) { + code |= ((uint8_t)in[char_pos]) >> (8 - bit_pos); + } else + code |= (0xFF >> (8 - bit_pos)); + return code; } /// The list of veritical codes is split into 5 sections. Used by readVCodeIdx() @@ -926,22 +887,21 @@ uint8_t usx_vcode_lookup[36] = {(1 << 5) + 0, (1 << 5) + 0, (2 << 5) + 1, (2 /// Decoder is designed for using less memory, not speed. \n /// Returns the veritical code index or 99 if match could not be found. \n /// Also updates bit_no_p with how many ever bits used by the vertical code. -int readVCodeIdx(const char *in, int len, int *bit_no_p) -{ - if (*bit_no_p < len) { - uint8_t code = read8bitCode(in, len, *bit_no_p); - int i = 0; - do { - if (code <= usx_vsections[i]) { - uint8_t vcode = usx_vcode_lookup[usx_vsection_pos[i] + ((code & usx_vsection_mask[i]) >> usx_vsection_shift[i])]; - (*bit_no_p) += ((vcode >> 5) + 1); - if (*bit_no_p > len) - return 99; - return vcode & 0x1F; - } - } while (++i < SECTION_COUNT); - } - return 99; +int readVCodeIdx(const char *in, int len, int *bit_no_p) { + if (*bit_no_p < len) { + uint8_t code = read8bitCode(in, len, *bit_no_p); + int i = 0; + do { + if (code <= usx_vsections[i]) { + uint8_t vcode = usx_vcode_lookup[usx_vsection_pos[i] + ((code & usx_vsection_mask[i]) >> usx_vsection_shift[i])]; + (*bit_no_p) += ((vcode >> 5) + 1); + if (*bit_no_p > len) + return 99; + return vcode & 0x1F; + } + } while (++i < SECTION_COUNT); + } + return 99; } /// Mask for retrieving each code to be decoded according to its length \n @@ -951,482 +911,467 @@ uint8_t len_masks[] = {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF}; /// depending on the hcodes defined using usx_hcodes and usx_hcode_lens \n /// Returns the horizontal code index or 99 if match could not be found. \n /// Also updates bit_no_p with how many ever bits used by the horizontal code. -int readHCodeIdx(const char *in, int len, int *bit_no_p, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) -{ - if (!usx_hcode_lens[USX_ALPHA]) - return USX_ALPHA; - if (*bit_no_p < len) { - uint8_t code = read8bitCode(in, len, *bit_no_p); - for (int code_pos = 0; code_pos < 5; code_pos++) { - if (usx_hcode_lens[code_pos] && (code & len_masks[usx_hcode_lens[code_pos] - 1]) == usx_hcodes[code_pos]) { - *bit_no_p += usx_hcode_lens[code_pos]; - return code_pos; - } - } +int readHCodeIdx(const char *in, int len, int *bit_no_p, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { + if (!usx_hcode_lens[USX_ALPHA]) + return USX_ALPHA; + if (*bit_no_p < len) { + uint8_t code = read8bitCode(in, len, *bit_no_p); + for (int code_pos = 0; code_pos < 5; code_pos++) { + if (usx_hcode_lens[code_pos] && (code & len_masks[usx_hcode_lens[code_pos] - 1]) == usx_hcodes[code_pos]) { + *bit_no_p += usx_hcode_lens[code_pos]; + return code_pos; + } } - return 99; + } + return 99; } // TODO: Last value check.. Also len check in readBit /// Returns the position of step code (0, 10, 110, etc.) encountered in the stream -int getStepCodeIdx(const char *in, int len, int *bit_no_p, int limit) -{ - int idx = 0; - while (*bit_no_p < len && readBit(in, *bit_no_p)) { - idx++; - (*bit_no_p)++; - if (idx == limit) - return idx; - } - if (*bit_no_p >= len) - return 99; +int getStepCodeIdx(const char *in, int len, int *bit_no_p, int limit) { + int idx = 0; + while (*bit_no_p < len && readBit(in, *bit_no_p)) { + idx++; (*bit_no_p)++; - return idx; + if (idx == limit) + return idx; + } + if (*bit_no_p >= len) + return 99; + (*bit_no_p)++; + return idx; } /// Reads specified number of bits and builds the corresponding integer -int32_t getNumFromBits(const char *in, int len, int bit_no, int count) -{ - int32_t ret = 0; - while (count-- && bit_no < len) { - ret += (readBit(in, bit_no) ? 1 << count : 0); - bit_no++; - } - return count < 0 ? ret : -1; +int32_t getNumFromBits(const char *in, int len, int bit_no, int count) { + int32_t ret = 0; + while (count-- && bit_no < len) { + ret += (readBit(in, bit_no) ? 1 << count : 0); + bit_no++; + } + return count < 0 ? ret : -1; } /// Decodes the count from the given bit stream at in. Also updates bit_no_p -int32_t readCount(const char *in, int *bit_no_p, int len) -{ - int idx = getStepCodeIdx(in, len, bit_no_p, 4); - if (idx == 99) - return -1; - if (*bit_no_p + count_bit_lens[idx] - 1 >= len) - return -1; - int32_t count = getNumFromBits(in, len, *bit_no_p, count_bit_lens[idx]) + (idx ? count_adder[idx - 1] : 0); - (*bit_no_p) += count_bit_lens[idx]; - return count; +int32_t readCount(const char *in, int *bit_no_p, int len) { + int idx = getStepCodeIdx(in, len, bit_no_p, 4); + if (idx == 99) + return -1; + if (*bit_no_p + count_bit_lens[idx] - 1 >= len) + return -1; + int32_t count = getNumFromBits(in, len, *bit_no_p, count_bit_lens[idx]) + (idx ? count_adder[idx - 1] : 0); + (*bit_no_p) += count_bit_lens[idx]; + return count; } /// Decodes the Unicode codepoint from the given bit stream at in. Also updates bit_no_p \n /// When the step code is 5, reads the next step code to find out the special code. -int32_t readUnicode(const char *in, int *bit_no_p, int len) -{ - int idx = getStepCodeIdx(in, len, bit_no_p, 5); - if (idx == 99) - return 0x7FFFFF00 + 99; - if (idx == 5) { - idx = getStepCodeIdx(in, len, bit_no_p, 4); - return 0x7FFFFF00 + idx; - } - if (idx >= 0) { - int sign = (*bit_no_p < len ? readBit(in, *bit_no_p) : 0); - (*bit_no_p)++; - if (*bit_no_p + uni_bit_len[idx] - 1 >= len) - return 0x7FFFFF00 + 99; - int32_t count = getNumFromBits(in, len, *bit_no_p, uni_bit_len[idx]); - count += uni_adder[idx]; - (*bit_no_p) += uni_bit_len[idx]; - // printf("Sign: %d, Val:%d", sign, count); - return sign ? -count : count; - } - return 0; +int32_t readUnicode(const char *in, int *bit_no_p, int len) { + int idx = getStepCodeIdx(in, len, bit_no_p, 5); + if (idx == 99) + return 0x7FFFFF00 + 99; + if (idx == 5) { + idx = getStepCodeIdx(in, len, bit_no_p, 4); + return 0x7FFFFF00 + idx; + } + if (idx >= 0) { + int sign = (*bit_no_p < len ? readBit(in, *bit_no_p) : 0); + (*bit_no_p)++; + if (*bit_no_p + uni_bit_len[idx] - 1 >= len) + return 0x7FFFFF00 + 99; + int32_t count = getNumFromBits(in, len, *bit_no_p, uni_bit_len[idx]); + count += uni_adder[idx]; + (*bit_no_p) += uni_bit_len[idx]; + // printf("Sign: %d, Val:%d", sign, count); + return sign ? -count : count; + } + return 0; } /// Macro to ensure that the decoder does not append more than olen bytes to out -#define DEC_OUTPUT_CHAR(out, olen, ol, c) \ - do { \ - char *const obuf = (out); \ - const int oidx = (ol); \ - const int limit = (olen); \ - if (limit <= oidx) \ - return limit + 1; \ - else if (oidx < 0) \ - return 0; \ - else \ - obuf[oidx] = (c); \ - } while (0) +#define DEC_OUTPUT_CHAR(out, olen, ol, c) \ + do { \ + char *const obuf = (out); \ + const int oidx = (ol); \ + const int limit = (olen); \ + if (limit <= oidx) \ + return limit + 1; \ + else if (oidx < 0) \ + return 0; \ + else \ + obuf[oidx] = (c); \ + } while (0) /// Macro to ensure that the decoder does not append more than olen bytes to out -#define DEC_OUTPUT_CHARS(olen, exp) \ - do { \ - const int newidx = (exp); \ - const int limit = (olen); \ - if (newidx > limit) \ - return limit + 1; \ - } while (0) +#define DEC_OUTPUT_CHARS(olen, exp) \ + do { \ + const int newidx = (exp); \ + const int limit = (olen); \ + if (newidx > limit) \ + return limit + 1; \ + } while (0) /// Write given unicode code point to out as a UTF-8 sequence -int writeUTF8(char *out, int olen, int ol, int uni) -{ - if (uni < (1 << 11)) { - DEC_OUTPUT_CHAR(out, olen, ol++, 0xC0 + (uni >> 6)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); - } else if (uni < (1 << 16)) { - DEC_OUTPUT_CHAR(out, olen, ol++, 0xE0 + (uni >> 12)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); - } else { - DEC_OUTPUT_CHAR(out, olen, ol++, 0xF0 + (uni >> 18)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 12) & 0x3F)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); - } - return ol; +int writeUTF8(char *out, int olen, int ol, int uni) { + if (uni < (1 << 11)) { + DEC_OUTPUT_CHAR(out, olen, ol++, 0xC0 + (uni >> 6)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); + } else if (uni < (1 << 16)) { + DEC_OUTPUT_CHAR(out, olen, ol++, 0xE0 + (uni >> 12)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); + } else { + DEC_OUTPUT_CHAR(out, olen, ol++, 0xF0 + (uni >> 18)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 12) & 0x3F)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); + } + return ol; } /// Decode repeating sequence and appends to out -int decodeRepeat(const char *in, int len, char *out, int olen, int ol, int *bit_no, struct us_lnk_lst *prev_lines) -{ - if (prev_lines) { - int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; - if (dict_len < NICE_LEN) - return -1; - int32_t dist = readCount(in, bit_no, len); - if (dist < 0) - return -1; - int32_t ctx = readCount(in, bit_no, len); - if (ctx < 0) - return -1; - struct us_lnk_lst *cur_line = prev_lines; - const int left = olen - ol; - while (ctx-- && cur_line) - cur_line = cur_line->previous; - if (cur_line == NULL) - return -1; - if (left <= 0) - return olen + 1; - if ((size_t)dist >= strlen(cur_line->data)) - return -1; - memmove(out + ol, cur_line->data + dist, min_of(left, dict_len)); - if (left < dict_len) - return olen + 1; - ol += dict_len; - } else { - int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; - if (dict_len < NICE_LEN) - return -1; - int32_t dist = readCount(in, bit_no, len) + NICE_LEN - 1; - if (dist < NICE_LEN - 1) - return -1; - const int32_t left = olen - ol; - // printf("Decode len: %d, dist: %d\n", dict_len - NICE_LEN, dist - NICE_LEN + 1); - if (left <= 0) - return olen + 1; - if (ol - dist < 0) - return -1; - memmove(out + ol, out + ol - dist, min_of(left, dict_len)); - if (left < dict_len) - return olen + 1; - ol += dict_len; - } - return ol; +int decodeRepeat(const char *in, int len, char *out, int olen, int ol, int *bit_no, struct us_lnk_lst *prev_lines) { + if (prev_lines) { + int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; + if (dict_len < NICE_LEN) + return -1; + int32_t dist = readCount(in, bit_no, len); + if (dist < 0) + return -1; + int32_t ctx = readCount(in, bit_no, len); + if (ctx < 0) + return -1; + struct us_lnk_lst *cur_line = prev_lines; + const int left = olen - ol; + while (ctx-- && cur_line) + cur_line = cur_line->previous; + if (cur_line == NULL) + return -1; + if (left <= 0) + return olen + 1; + if ((size_t)dist >= strlen(cur_line->data)) + return -1; + memmove(out + ol, cur_line->data + dist, min_of(left, dict_len)); + if (left < dict_len) + return olen + 1; + ol += dict_len; + } else { + int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; + if (dict_len < NICE_LEN) + return -1; + int32_t dist = readCount(in, bit_no, len) + NICE_LEN - 1; + if (dist < NICE_LEN - 1) + return -1; + const int32_t left = olen - ol; + // printf("Decode len: %d, dist: %d\n", dict_len - NICE_LEN, dist - NICE_LEN + 1); + if (left <= 0) + return olen + 1; + if (ol - dist < 0) + return -1; + memmove(out + ol, out + ol - dist, min_of(left, dict_len)); + if (left < dict_len) + return olen + 1; + ol += dict_len; + } + return ol; } /// Returns hex character corresponding to the 4 bit nibble -char getHexChar(int32_t nibble, int hex_type) -{ - if (nibble >= 0 && nibble <= 9) - return '0' + nibble; - else if (hex_type < USX_NIB_HEX_UPPER) - return 'a' + nibble - 10; - return 'A' + nibble - 10; +char getHexChar(int32_t nibble, int hex_type) { + if (nibble >= 0 && nibble <= 9) + return '0' + nibble; + else if (hex_type < USX_NIB_HEX_UPPER) + return 'a' + nibble - 10; + return 'A' + nibble - 10; } // Main API function. See unishox2.h for documentation int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], - struct us_lnk_lst *prev_lines) -{ + struct us_lnk_lst *prev_lines) { - int dstate; - int bit_no; - int h, v; - uint8_t is_all_upper; + int dstate; + int bit_no; + int h, v; + uint8_t is_all_upper; #if (UNISHOX_API_OUT_AND_LEN(0, 1)) == 0 - const int olen = INT_MAX - 1; + const int olen = INT_MAX - 1; #endif - init_coder(); - int ol = 0; - bit_no = UNISHOX_MAGIC_BIT_LEN; // ignore the magic bit - dstate = h = USX_ALPHA; - is_all_upper = 0; + init_coder(); + int ol = 0; + bit_no = UNISHOX_MAGIC_BIT_LEN; // ignore the magic bit + dstate = h = USX_ALPHA; + is_all_upper = 0; - int prev_uni = 0; + int prev_uni = 0; - len <<= 3; - while (bit_no < len) { - int orig_bit_no = bit_no; - if (dstate == USX_DELTA || h == USX_DELTA) { - if (dstate != USX_DELTA) - h = dstate; - int32_t delta = readUnicode(in, &bit_no, len); - if ((delta >> 8) == 0x7FFFFF) { - int spl_code_idx = delta & 0x000000FF; - if (spl_code_idx == 99) - break; - switch (spl_code_idx) { - case 0: - DEC_OUTPUT_CHAR(out, olen, ol++, ' '); - continue; - case 1: - h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); - if (h == 99) { - bit_no = len; - continue; - } - if (h == USX_DELTA || h == USX_ALPHA) { - dstate = h; - continue; - } - if (h == USX_DICT) { - int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); - if (rpt_ret < 0) - return ol; // if we break here it will only break out of switch - DEC_OUTPUT_CHARS(olen, ol = rpt_ret); - h = dstate; - continue; - } - break; - case 2: - DEC_OUTPUT_CHAR(out, olen, ol++, ','); - continue; - case 3: - DEC_OUTPUT_CHAR(out, olen, ol++, '.'); - continue; - case 4: - DEC_OUTPUT_CHAR(out, olen, ol++, 10); - continue; - } - } else { - prev_uni += delta; - DEC_OUTPUT_CHARS(olen, ol = writeUTF8(out, olen, ol, prev_uni)); - // printf("%ld, ", prev_uni); - } - if (dstate == USX_DELTA && h == USX_DELTA) - continue; - } else + len <<= 3; + while (bit_no < len) { + int orig_bit_no = bit_no; + if (dstate == USX_DELTA || h == USX_DELTA) { + if (dstate != USX_DELTA) + h = dstate; + int32_t delta = readUnicode(in, &bit_no, len); + if ((delta >> 8) == 0x7FFFFF) { + int spl_code_idx = delta & 0x000000FF; + if (spl_code_idx == 99) + break; + switch (spl_code_idx) { + case 0: + DEC_OUTPUT_CHAR(out, olen, ol++, ' '); + continue; + case 1: + h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); + if (h == 99) { + bit_no = len; + continue; + } + if (h == USX_DELTA || h == USX_ALPHA) { + dstate = h; + continue; + } + if (h == USX_DICT) { + int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); + if (rpt_ret < 0) + return ol; // if we break here it will only break out of switch + DEC_OUTPUT_CHARS(olen, ol = rpt_ret); h = dstate; - char c = 0; - uint8_t is_upper = is_all_upper; - v = readVCodeIdx(in, len, &bit_no); - if (v == 99 || h == 99) { + continue; + } + break; + case 2: + DEC_OUTPUT_CHAR(out, olen, ol++, ','); + continue; + case 3: + DEC_OUTPUT_CHAR(out, olen, ol++, '.'); + continue; + case 4: + DEC_OUTPUT_CHAR(out, olen, ol++, 10); + continue; + } + } else { + prev_uni += delta; + DEC_OUTPUT_CHARS(olen, ol = writeUTF8(out, olen, ol, prev_uni)); + // printf("%ld, ", prev_uni); + } + if (dstate == USX_DELTA && h == USX_DELTA) + continue; + } else + h = dstate; + char c = 0; + uint8_t is_upper = is_all_upper; + v = readVCodeIdx(in, len, &bit_no); + if (v == 99 || h == 99) { + bit_no = orig_bit_no; + break; + } + if (v == 0 && h != USX_SYM) { + if (bit_no >= len) + break; + if (h != USX_NUM || dstate != USX_DELTA) { + h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); + if (h == 99 || bit_no >= len) { + bit_no = orig_bit_no; + break; + } + } + if (h == USX_ALPHA) { + if (dstate == USX_ALPHA) { + if (!usx_hcode_lens[USX_ALPHA] && + TERM_BYTE_PRESET_1 == (read8bitCode(in, len, bit_no - SW_CODE_LEN) & + (0xFF << (8 - (is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER))))) + break; // Terminator for preset 1 + if (is_all_upper) { + is_upper = is_all_upper = 0; + continue; + } + v = readVCodeIdx(in, len, &bit_no); + if (v == 99) { bit_no = orig_bit_no; break; - } - if (v == 0 && h != USX_SYM) { - if (bit_no >= len) - break; - if (h != USX_NUM || dstate != USX_DELTA) { - h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); - if (h == 99 || bit_no >= len) { - bit_no = orig_bit_no; - break; - } + } + if (v == 0) { + h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); + if (h == 99) { + bit_no = orig_bit_no; + break; } if (h == USX_ALPHA) { - if (dstate == USX_ALPHA) { - if (!usx_hcode_lens[USX_ALPHA] && - TERM_BYTE_PRESET_1 == - (read8bitCode(in, len, bit_no - SW_CODE_LEN) & - (0xFF << (8 - (is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER))))) - break; // Terminator for preset 1 - if (is_all_upper) { - is_upper = is_all_upper = 0; - continue; - } - v = readVCodeIdx(in, len, &bit_no); - if (v == 99) { - bit_no = orig_bit_no; - break; - } - if (v == 0) { - h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); - if (h == 99) { - bit_no = orig_bit_no; - break; - } - if (h == USX_ALPHA) { - is_all_upper = 1; - continue; - } - } - is_upper = 1; - } else { - dstate = USX_ALPHA; - continue; - } - } else if (h == USX_DICT) { - int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); - if (rpt_ret < 0) - break; - DEC_OUTPUT_CHARS(olen, ol = rpt_ret); - continue; - } else if (h == USX_DELTA) { - // printf("Sign: %d, bitno: %d\n", sign, bit_no); - // printf("Code: %d\n", prev_uni); - // printf("BitNo: %d\n", bit_no); - continue; - } else { - if (h != USX_NUM || dstate != USX_DELTA) - v = readVCodeIdx(in, len, &bit_no); - if (v == 99) { - bit_no = orig_bit_no; - break; - } - if (h == USX_NUM && v == 0) { - int idx = getStepCodeIdx(in, len, &bit_no, 5); - if (idx == 99) - break; - if (idx == 0) { - idx = getStepCodeIdx(in, len, &bit_no, 4); - if (idx >= 5) - break; - int32_t rem = readCount(in, &bit_no, len); - if (rem < 0) - break; - if (usx_templates[idx] == NULL) - break; - size_t tlen = strlen(usx_templates[idx]); - if ((size_t)rem > tlen) - break; - rem = tlen - rem; - int eof = 0; - for (int j = 0; j < rem; j++) { - char c_t = usx_templates[idx][j]; - if (c_t == 'f' || c_t == 'r' || c_t == 't' || c_t == 'o' || c_t == 'F') { - char nibble_len = (c_t == 'f' || c_t == 'F' ? 4 : (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1))); - const int32_t raw_char = getNumFromBits(in, len, bit_no, nibble_len); - if (raw_char < 0) { - eof = 1; - break; - } - DEC_OUTPUT_CHAR(out, olen, ol++, - getHexChar((char)raw_char, c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); - bit_no += nibble_len; - } else - DEC_OUTPUT_CHAR(out, olen, ol++, c_t); - } - if (eof) - break; // reach input eof - } else if (idx == 5) { - int32_t bin_count = readCount(in, &bit_no, len); - if (bin_count < 0) - break; - if (bin_count == 0) // invalid encoding - break; - do { - const int32_t raw_char = getNumFromBits(in, len, bit_no, 8); - if (raw_char < 0) - break; - DEC_OUTPUT_CHAR(out, olen, ol++, (char)raw_char); - bit_no += 8; - } while (--bin_count); - if (bin_count > 0) - break; // reach input eof - } else { - int32_t nibble_count = 0; - if (idx == 2 || idx == 4) - nibble_count = 32; - else { - nibble_count = readCount(in, &bit_no, len); - if (nibble_count < 0) - break; - if (nibble_count == 0) // invalid encoding - break; - } - do { - int32_t nibble = getNumFromBits(in, len, bit_no, 4); - if (nibble < 0) - break; - DEC_OUTPUT_CHAR(out, olen, ol++, getHexChar(nibble, idx < 3 ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); - if ((idx == 2 || idx == 4) && - (nibble_count == 25 || nibble_count == 21 || nibble_count == 17 || nibble_count == 13)) - DEC_OUTPUT_CHAR(out, olen, ol++, '-'); - bit_no += 4; - } while (--nibble_count); - if (nibble_count > 0) - break; // reach input eof - } - if (dstate == USX_DELTA) - h = USX_DELTA; - continue; - } + is_all_upper = 1; + continue; } - } - if (is_upper && v == 1) { - h = dstate = USX_DELTA; // continuous delta coding - continue; - } - if (h < 3 && v < 28) - c = usx_sets[h][v]; - if (c >= 'a' && c <= 'z') { - dstate = USX_ALPHA; - if (is_upper) - c -= 32; + } + is_upper = 1; } else { - if (c >= '0' && c <= '9') { - dstate = USX_NUM; - } else if (c == 0) { - if (v == 8) { - DEC_OUTPUT_CHAR(out, olen, ol++, '\r'); - DEC_OUTPUT_CHAR(out, olen, ol++, '\n'); - } else if (h == USX_NUM && v == 26) { - int32_t count = readCount(in, &bit_no, len); - if (count < 0) - break; - count += 4; - if (ol <= 0) - return 0; // invalid encoding - char rpt_c = out[ol - 1]; - while (count--) - DEC_OUTPUT_CHAR(out, olen, ol++, rpt_c); - } else if (h == USX_SYM && v > 24) { - v -= 25; - const int freqlen = (int)strlen(usx_freq_seq[v]); - const int left = olen - ol; - if (left <= 0) - return olen + 1; - memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); - if (left < freqlen) - return olen + 1; - ol += freqlen; - } else if (h == USX_NUM && v > 22 && v < 26) { - v -= (23 - 3); - const int freqlen = (int)strlen(usx_freq_seq[v]); - const int left = olen - ol; - if (left <= 0) - return olen + 1; - memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); - if (left < freqlen) - return olen + 1; - ol += freqlen; - } else - break; // Terminator - if (dstate == USX_DELTA) - h = USX_DELTA; - continue; - } + dstate = USX_ALPHA; + continue; } - if (dstate == USX_DELTA) + } else if (h == USX_DICT) { + int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); + if (rpt_ret < 0) + break; + DEC_OUTPUT_CHARS(olen, ol = rpt_ret); + continue; + } else if (h == USX_DELTA) { + // printf("Sign: %d, bitno: %d\n", sign, bit_no); + // printf("Code: %d\n", prev_uni); + // printf("BitNo: %d\n", bit_no); + continue; + } else { + if (h != USX_NUM || dstate != USX_DELTA) + v = readVCodeIdx(in, len, &bit_no); + if (v == 99) { + bit_no = orig_bit_no; + break; + } + if (h == USX_NUM && v == 0) { + int idx = getStepCodeIdx(in, len, &bit_no, 5); + if (idx == 99) + break; + if (idx == 0) { + idx = getStepCodeIdx(in, len, &bit_no, 4); + if (idx >= 5) + break; + int32_t rem = readCount(in, &bit_no, len); + if (rem < 0) + break; + if (usx_templates[idx] == NULL) + break; + size_t tlen = strlen(usx_templates[idx]); + if ((size_t)rem > tlen) + break; + rem = tlen - rem; + int eof = 0; + for (int j = 0; j < rem; j++) { + char c_t = usx_templates[idx][j]; + if (c_t == 'f' || c_t == 'r' || c_t == 't' || c_t == 'o' || c_t == 'F') { + char nibble_len = (c_t == 'f' || c_t == 'F' ? 4 : (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1))); + const int32_t raw_char = getNumFromBits(in, len, bit_no, nibble_len); + if (raw_char < 0) { + eof = 1; + break; + } + DEC_OUTPUT_CHAR(out, olen, ol++, getHexChar((char)raw_char, c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); + bit_no += nibble_len; + } else + DEC_OUTPUT_CHAR(out, olen, ol++, c_t); + } + if (eof) + break; // reach input eof + } else if (idx == 5) { + int32_t bin_count = readCount(in, &bit_no, len); + if (bin_count < 0) + break; + if (bin_count == 0) // invalid encoding + break; + do { + const int32_t raw_char = getNumFromBits(in, len, bit_no, 8); + if (raw_char < 0) + break; + DEC_OUTPUT_CHAR(out, olen, ol++, (char)raw_char); + bit_no += 8; + } while (--bin_count); + if (bin_count > 0) + break; // reach input eof + } else { + int32_t nibble_count = 0; + if (idx == 2 || idx == 4) + nibble_count = 32; + else { + nibble_count = readCount(in, &bit_no, len); + if (nibble_count < 0) + break; + if (nibble_count == 0) // invalid encoding + break; + } + do { + int32_t nibble = getNumFromBits(in, len, bit_no, 4); + if (nibble < 0) + break; + DEC_OUTPUT_CHAR(out, olen, ol++, getHexChar(nibble, idx < 3 ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); + if ((idx == 2 || idx == 4) && (nibble_count == 25 || nibble_count == 21 || nibble_count == 17 || nibble_count == 13)) + DEC_OUTPUT_CHAR(out, olen, ol++, '-'); + bit_no += 4; + } while (--nibble_count); + if (nibble_count > 0) + break; // reach input eof + } + if (dstate == USX_DELTA) h = USX_DELTA; - DEC_OUTPUT_CHAR(out, olen, ol++, c); + continue; + } + } } + if (is_upper && v == 1) { + h = dstate = USX_DELTA; // continuous delta coding + continue; + } + if (h < 3 && v < 28) + c = usx_sets[h][v]; + if (c >= 'a' && c <= 'z') { + dstate = USX_ALPHA; + if (is_upper) + c -= 32; + } else { + if (c >= '0' && c <= '9') { + dstate = USX_NUM; + } else if (c == 0) { + if (v == 8) { + DEC_OUTPUT_CHAR(out, olen, ol++, '\r'); + DEC_OUTPUT_CHAR(out, olen, ol++, '\n'); + } else if (h == USX_NUM && v == 26) { + int32_t count = readCount(in, &bit_no, len); + if (count < 0) + break; + count += 4; + if (ol <= 0) + return 0; // invalid encoding + char rpt_c = out[ol - 1]; + while (count--) + DEC_OUTPUT_CHAR(out, olen, ol++, rpt_c); + } else if (h == USX_SYM && v > 24) { + v -= 25; + const int freqlen = (int)strlen(usx_freq_seq[v]); + const int left = olen - ol; + if (left <= 0) + return olen + 1; + memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); + if (left < freqlen) + return olen + 1; + ol += freqlen; + } else if (h == USX_NUM && v > 22 && v < 26) { + v -= (23 - 3); + const int freqlen = (int)strlen(usx_freq_seq[v]); + const int left = olen - ol; + if (left <= 0) + return olen + 1; + memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); + if (left < freqlen) + return olen + 1; + ol += freqlen; + } else + break; // Terminator + if (dstate == USX_DELTA) + h = USX_DELTA; + continue; + } + } + if (dstate == USX_DELTA) + h = USX_DELTA; + DEC_OUTPUT_CHAR(out, olen, ol++, c); + } - return ol; + return ol; } // Main API function. See unishox2.h for documentation int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) -{ - return unishox2_decompress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, - usx_templates, NULL); + const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) { + return unishox2_decompress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, usx_templates, NULL); } // Main API function. See unishox2.h for documentation -int unishox2_decompress_simple(const char *in, int len, char *out) -{ - return unishox2_decompress(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_PSET_DFLT); +int unishox2_decompress_simple(const char *in, int len, char *out) { + return unishox2_decompress(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_PSET_DFLT); } \ No newline at end of file diff --git a/src/mesh/compression/unishox2.h b/src/mesh/compression/unishox2.h index 823128f02..3e37e7a79 100644 --- a/src/mesh/compression/unishox2.h +++ b/src/mesh/compression/unishox2.h @@ -59,139 +59,79 @@ // enum {USX_ALPHA = 0, USX_SYM, USX_NUM, USX_DICT, USX_DELTA}; -/// Default Horizontal codes. When composition of text is know beforehand, the other hcodes in this section can be used to achieve -/// more compression. -#define USX_HCODES_DFLT \ - (const unsigned char[]) \ - { \ - 0x00, 0x40, 0x80, 0xC0, 0xE0 \ - } +/// Default Horizontal codes. When composition of text is know beforehand, the other hcodes in this section can be used +/// to achieve more compression. +#define USX_HCODES_DFLT \ + (const unsigned char[]) { 0x00, 0x40, 0x80, 0xC0, 0xE0 } /// Length of each default hcode -#define USX_HCODE_LENS_DFLT \ - (const unsigned char[]) \ - { \ - 2, 2, 2, 3, 3 \ - } +#define USX_HCODE_LENS_DFLT \ + (const unsigned char[]) { 2, 2, 2, 3, 3 } /// Horizontal codes preset for English Alphabet content only -#define USX_HCODES_ALPHA_ONLY \ - (const unsigned char[]) \ - { \ - 0x00, 0x00, 0x00, 0x00, 0x00 \ - } +#define USX_HCODES_ALPHA_ONLY \ + (const unsigned char[]) { 0x00, 0x00, 0x00, 0x00, 0x00 } /// Length of each Alpha only hcode -#define USX_HCODE_LENS_ALPHA_ONLY \ - (const unsigned char[]) \ - { \ - 0, 0, 0, 0, 0 \ - } +#define USX_HCODE_LENS_ALPHA_ONLY \ + (const unsigned char[]) { 0, 0, 0, 0, 0 } /// Horizontal codes preset for Alpha Numeric content only -#define USX_HCODES_ALPHA_NUM_ONLY \ - (const unsigned char[]) \ - { \ - 0x00, 0x00, 0x80, 0x00, 0x00 \ - } +#define USX_HCODES_ALPHA_NUM_ONLY \ + (const unsigned char[]) { 0x00, 0x00, 0x80, 0x00, 0x00 } /// Length of each Alpha numeric hcode -#define USX_HCODE_LENS_ALPHA_NUM_ONLY \ - (const unsigned char[]) \ - { \ - 1, 0, 1, 0, 0 \ - } +#define USX_HCODE_LENS_ALPHA_NUM_ONLY \ + (const unsigned char[]) { 1, 0, 1, 0, 0 } /// Horizontal codes preset for Alpha Numeric and Symbol content only -#define USX_HCODES_ALPHA_NUM_SYM_ONLY \ - (const unsigned char[]) \ - { \ - 0x00, 0x80, 0xC0, 0x00, 0x00 \ - } +#define USX_HCODES_ALPHA_NUM_SYM_ONLY \ + (const unsigned char[]) { 0x00, 0x80, 0xC0, 0x00, 0x00 } /// Length of each Alpha numeric and symbol hcodes -#define USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY \ - (const unsigned char[]) \ - { \ - 1, 2, 2, 0, 0 \ - } +#define USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY \ + (const unsigned char[]) { 1, 2, 2, 0, 0 } /// Horizontal codes preset favouring Alphabet content -#define USX_HCODES_FAVOR_ALPHA \ - (const unsigned char[]) \ - { \ - 0x00, 0x80, 0xA0, 0xC0, 0xE0 \ - } +#define USX_HCODES_FAVOR_ALPHA \ + (const unsigned char[]) { 0x00, 0x80, 0xA0, 0xC0, 0xE0 } /// Length of each hcode favouring Alpha content -#define USX_HCODE_LENS_FAVOR_ALPHA \ - (const unsigned char[]) \ - { \ - 1, 3, 3, 3, 3 \ - } +#define USX_HCODE_LENS_FAVOR_ALPHA \ + (const unsigned char[]) { 1, 3, 3, 3, 3 } /// Horizontal codes preset favouring repeating sequences -#define USX_HCODES_FAVOR_DICT \ - (const unsigned char[]) \ - { \ - 0x00, 0x40, 0xC0, 0x80, 0xE0 \ - } +#define USX_HCODES_FAVOR_DICT \ + (const unsigned char[]) { 0x00, 0x40, 0xC0, 0x80, 0xE0 } /// Length of each hcode favouring repeating sequences -#define USX_HCODE_LENS_FAVOR_DICT \ - (const unsigned char[]) \ - { \ - 2, 2, 3, 2, 3 \ - } +#define USX_HCODE_LENS_FAVOR_DICT \ + (const unsigned char[]) { 2, 2, 3, 2, 3 } /// Horizontal codes preset favouring symbols -#define USX_HCODES_FAVOR_SYM \ - (const unsigned char[]) \ - { \ - 0x80, 0x00, 0xA0, 0xC0, 0xE0 \ - } +#define USX_HCODES_FAVOR_SYM \ + (const unsigned char[]) { 0x80, 0x00, 0xA0, 0xC0, 0xE0 } /// Length of each hcode favouring symbols -#define USX_HCODE_LENS_FAVOR_SYM \ - (const unsigned char[]) \ - { \ - 3, 1, 3, 3, 3 \ - } +#define USX_HCODE_LENS_FAVOR_SYM \ + (const unsigned char[]) { 3, 1, 3, 3, 3 } // #define USX_HCODES_FAVOR_UMLAUT {0x00, 0x40, 0xE0, 0xC0, 0x80} // #define USX_HCODE_LENS_FAVOR_UMLAUT {2, 2, 3, 3, 2} /// Horizontal codes preset favouring umlaut letters -#define USX_HCODES_FAVOR_UMLAUT \ - (const unsigned char[]) \ - { \ - 0x80, 0xA0, 0xC0, 0xE0, 0x00 \ - } +#define USX_HCODES_FAVOR_UMLAUT \ + (const unsigned char[]) { 0x80, 0xA0, 0xC0, 0xE0, 0x00 } /// Length of each hcode favouring umlaut letters -#define USX_HCODE_LENS_FAVOR_UMLAUT \ - (const unsigned char[]) \ - { \ - 3, 3, 3, 3, 1 \ - } +#define USX_HCODE_LENS_FAVOR_UMLAUT \ + (const unsigned char[]) { 3, 3, 3, 3, 1 } /// Horizontal codes preset for no repeating sequences -#define USX_HCODES_NO_DICT \ - (const unsigned char[]) \ - { \ - 0x00, 0x40, 0x80, 0x00, 0xC0 \ - } +#define USX_HCODES_NO_DICT \ + (const unsigned char[]) { 0x00, 0x40, 0x80, 0x00, 0xC0 } /// Length of each hcode for no repeating sequences -#define USX_HCODE_LENS_NO_DICT \ - (const unsigned char[]) \ - { \ - 2, 2, 2, 0, 2 \ - } +#define USX_HCODE_LENS_NO_DICT \ + (const unsigned char[]) { 2, 2, 2, 0, 2 } /// Horizontal codes preset for no Unicode characters -#define USX_HCODES_NO_UNI \ - (const unsigned char[]) \ - { \ - 0x00, 0x40, 0x80, 0xC0, 0x00 \ - } +#define USX_HCODES_NO_UNI \ + (const unsigned char[]) { 0x00, 0x40, 0x80, 0xC0, 0x00 } /// Length of each hcode for no Unicode characters -#define USX_HCODE_LENS_NO_UNI \ - (const unsigned char[]) \ - { \ - 2, 2, 2, 2, 0 \ - } +#define USX_HCODE_LENS_NO_UNI \ + (const unsigned char[]) { 2, 2, 2, 2, 0 } extern const char *USX_FREQ_SEQ_DFLT[]; extern const char *USX_FREQ_SEQ_TXT[]; @@ -201,19 +141,17 @@ extern const char *USX_FREQ_SEQ_HTML[]; extern const char *USX_FREQ_SEQ_XML[]; extern const char *USX_TEMPLATES[]; -/// Default preset parameter set. When composition of text is know beforehand, the other parameter sets in this section can be -/// used to achieve more compression. +/// Default preset parameter set. When composition of text is know beforehand, the other parameter sets in this section +/// can be used to achieve more compression. #define USX_PSET_DFLT USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES /// Preset parameter set for English Alphabet only content #define USX_PSET_ALPHA_ONLY USX_HCODES_ALPHA_ONLY, USX_HCODE_LENS_ALPHA_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES /// Preset parameter set for Alpha numeric content #define USX_PSET_ALPHA_NUM_ONLY USX_HCODES_ALPHA_NUM_ONLY, USX_HCODE_LENS_ALPHA_NUM_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES /// Preset parameter set for Alpha numeric and symbol content -#define USX_PSET_ALPHA_NUM_SYM_ONLY \ - USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +#define USX_PSET_ALPHA_NUM_SYM_ONLY USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES /// Preset parameter set for Alpha numeric symbol content having predominantly text -#define USX_PSET_ALPHA_NUM_SYM_ONLY_TXT \ - USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +#define USX_PSET_ALPHA_NUM_SYM_ONLY_TXT USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES /// Preset parameter set favouring Alphabet content #define USX_PSET_FAVOR_ALPHA USX_HCODES_FAVOR_ALPHA, USX_HCODE_LENS_FAVOR_ALPHA, USX_FREQ_SEQ_TXT, USX_TEMPLATES /// Preset parameter set favouring repeating sequences @@ -244,8 +182,8 @@ extern const char *USX_TEMPLATES[]; * This is passed as a parameter to the unishox2_decompress_lines() function */ struct us_lnk_lst { - char *data; - struct us_lnk_lst *previous; + char *data; + struct us_lnk_lst *previous; }; /** @@ -294,9 +232,8 @@ extern int unishox2_decompress_simple(const char *in, int len, char *out); * @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ -extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), - const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], - const char *usx_templates[]); +extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], + const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]); /** * Comprehensive API for de-compressing a string * @@ -313,9 +250,8 @@ extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(ch * @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ -extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), - const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], - const char *usx_templates[]); +extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], + const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]); /** * More Comprehensive API for compressing array of strings * @@ -326,9 +262,9 @@ extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN( * and stored in a compressed array of bytes for use as a constant in other programs \n * where each element of the array can be decompressed and used at runtime. */ -extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), - const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], - const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines); +extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], + const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], + struct us_lnk_lst *prev_lines); /** * More Comprehensive API for de-compressing array of strings \n * This function is not be used in conjuction with unishox2_compress_lines() @@ -340,8 +276,8 @@ extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_ * routine which takes this compressed array as parameter and index to be \n * decompressed. */ -extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), - const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], - const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines); +extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], + const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], + struct us_lnk_lst *prev_lines); #endif diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 2b4f63512..49a49a3fb 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -29,172 +29,166 @@ using namespace concurrency; static Periodic *ethEvent; -static int32_t reconnectETH() -{ - if (config.network.eth_enabled) { - Ethernet.maintain(); - if (!ethStartupComplete) { - // Start web server - LOG_INFO("Start Ethernet network services"); +static int32_t reconnectETH() { + if (config.network.eth_enabled) { + Ethernet.maintain(); + if (!ethStartupComplete) { + // Start web server + LOG_INFO("Start Ethernet network services"); #ifndef DISABLE_NTP - LOG_INFO("Start NTP time client"); - timeClient.begin(); - timeClient.setUpdateInterval(60 * 60); // Update once an hour + LOG_INFO("Start NTP time client"); + timeClient.begin(); + timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif - if (config.network.rsyslog_server[0]) { - LOG_INFO("Start Syslog client"); - // Defaults - int serverPort = 514; - const char *serverAddr = config.network.rsyslog_server; - String server = String(serverAddr); - int delimIndex = server.indexOf(':'); - if (delimIndex > 0) { - String port = server.substring(delimIndex + 1, server.length()); - server[delimIndex] = 0; - serverPort = port.toInt(); - serverAddr = server.c_str(); - } - syslog.server(serverAddr, serverPort); - syslog.deviceHostname(getDeviceName()); - syslog.appName("Meshtastic"); - syslog.defaultPriority(LOGLEVEL_USER); - syslog.enable(); - } + if (config.network.rsyslog_server[0]) { + LOG_INFO("Start Syslog client"); + // Defaults + int serverPort = 514; + const char *serverAddr = config.network.rsyslog_server; + String server = String(serverAddr); + int delimIndex = server.indexOf(':'); + if (delimIndex > 0) { + String port = server.substring(delimIndex + 1, server.length()); + server[delimIndex] = 0; + serverPort = port.toInt(); + serverAddr = server.c_str(); + } + syslog.server(serverAddr, serverPort); + syslog.deviceHostname(getDeviceName()); + syslog.appName("Meshtastic"); + syslog.defaultPriority(LOGLEVEL_USER); + syslog.enable(); + } #if !MESHTASTIC_EXCLUDE_SOCKETAPI - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - initApiServer(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif #if HAS_UDP_MULTICAST - if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->start(); - } + if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->start(); + } #endif - ethStartupComplete = true; - } + ethStartupComplete = true; } + } #ifndef DISABLE_NTP - if (isEthernetAvailable() && (ntp_renew < millis())) { + if (isEthernetAvailable() && (ntp_renew < millis())) { - LOG_INFO("Update NTP time from %s", config.network.ntp_server); - if (timeClient.update()) { - LOG_DEBUG("NTP Request Success - Set RTCQualityNTP if needed"); + LOG_INFO("Update NTP time from %s", config.network.ntp_server); + if (timeClient.update()) { + LOG_DEBUG("NTP Request Success - Set RTCQualityNTP if needed"); - struct timeval tv; - tv.tv_sec = timeClient.getEpochTime(); - tv.tv_usec = 0; + struct timeval tv; + tv.tv_sec = timeClient.getEpochTime(); + tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + perhapsSetRTC(RTCQualityNTP, &tv); - ntp_renew = millis() + 43200 * 1000; // success, refresh every 12 hours - } else { - LOG_ERROR("NTP Update failed"); - ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes - } + ntp_renew = millis() + 43200 * 1000; // success, refresh every 12 hours + } else { + LOG_ERROR("NTP Update failed"); + ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes } + } #endif - return 5000; // every 5 seconds + return 5000; // every 5 seconds } // Startup Ethernet -bool initEthernet() -{ - if (config.network.eth_enabled) { +bool initEthernet() { + if (config.network.eth_enabled) { #ifdef PIN_ETH_POWER_EN - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. - delay(100); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. + delay(100); #endif #ifdef PIN_ETHERNET_RESET - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. - delay(100); - digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. #endif #ifdef RAK11310 // Initialize the SPI port - ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); - ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); - ETH_SPI_PORT.setRX(PIN_SPI0_MISO); - ETH_SPI_PORT.begin(); + ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); + ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); + ETH_SPI_PORT.setRX(PIN_SPI0_MISO); + ETH_SPI_PORT.begin(); #endif - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); - uint8_t mac[6]; + uint8_t mac[6]; - int status = 0; + int status = 0; - // createSSLCert(); + // createSSLCert(); - getMacAddr(mac); // FIXME use the BLE MAC for now... - mac[0] &= 0xfe; // Make sure this is not a multicast MAC + getMacAddr(mac); // FIXME use the BLE MAC for now... + mac[0] &= 0xfe; // Make sure this is not a multicast MAC - if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { - LOG_INFO("Start Ethernet DHCP"); - status = Ethernet.begin(mac); - } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { - LOG_INFO("Start Ethernet Static"); - Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, - config.network.ipv4_config.subnet); - status = 1; - } else { - LOG_INFO("Ethernet Disabled"); - return false; - } - - if (status == 0) { - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - LOG_ERROR("Ethernet shield was not found"); - return false; - } else if (Ethernet.linkStatus() == LinkOFF) { - LOG_ERROR("Ethernet cable is not connected"); - return false; - } else { - LOG_ERROR("Unknown Ethernet error"); - return false; - } - } else { - LOG_INFO("Local IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], - Ethernet.localIP()[3]); - LOG_INFO("Subnet Mask %u.%u.%u.%u", Ethernet.subnetMask()[0], Ethernet.subnetMask()[1], Ethernet.subnetMask()[2], - Ethernet.subnetMask()[3]); - LOG_INFO("Gateway IP %u.%u.%u.%u", Ethernet.gatewayIP()[0], Ethernet.gatewayIP()[1], Ethernet.gatewayIP()[2], - Ethernet.gatewayIP()[3]); - LOG_INFO("DNS Server IP %u.%u.%u.%u", Ethernet.dnsServerIP()[0], Ethernet.dnsServerIP()[1], Ethernet.dnsServerIP()[2], - Ethernet.dnsServerIP()[3]); - } - - ethEvent = new Periodic("ethConnect", reconnectETH); - - return true; + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { + LOG_INFO("Start Ethernet DHCP"); + status = Ethernet.begin(mac); + } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { + LOG_INFO("Start Ethernet Static"); + Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, + config.network.ipv4_config.subnet); + status = 1; } else { - LOG_INFO("Not using Ethernet"); - return false; + LOG_INFO("Ethernet Disabled"); + return false; } + + if (status == 0) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + LOG_ERROR("Ethernet shield was not found"); + return false; + } else if (Ethernet.linkStatus() == LinkOFF) { + LOG_ERROR("Ethernet cable is not connected"); + return false; + } else { + LOG_ERROR("Unknown Ethernet error"); + return false; + } + } else { + LOG_INFO("Local IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]); + LOG_INFO("Subnet Mask %u.%u.%u.%u", Ethernet.subnetMask()[0], Ethernet.subnetMask()[1], Ethernet.subnetMask()[2], Ethernet.subnetMask()[3]); + LOG_INFO("Gateway IP %u.%u.%u.%u", Ethernet.gatewayIP()[0], Ethernet.gatewayIP()[1], Ethernet.gatewayIP()[2], Ethernet.gatewayIP()[3]); + LOG_INFO("DNS Server IP %u.%u.%u.%u", Ethernet.dnsServerIP()[0], Ethernet.dnsServerIP()[1], Ethernet.dnsServerIP()[2], + Ethernet.dnsServerIP()[3]); + } + + ethEvent = new Periodic("ethConnect", reconnectETH); + + return true; + } else { + LOG_INFO("Not using Ethernet"); + return false; + } } -bool isEthernetAvailable() -{ +bool isEthernetAvailable() { - if (!config.network.eth_enabled) { - syslog.disable(); - return false; - } else if (Ethernet.hardwareStatus() == EthernetNoHardware) { - syslog.disable(); - return false; - } else if (Ethernet.linkStatus() == LinkOFF) { - syslog.disable(); - return false; - } else { - return true; - } + if (!config.network.eth_enabled) { + syslog.disable(); + return false; + } else if (Ethernet.hardwareStatus() == EthernetNoHardware) { + syslog.disable(); + return false; + } else if (Ethernet.linkStatus() == LinkOFF) { + syslog.disable(); + return false; + } else { + return true; + } } #endif diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 7b7ebb595..f6ec9d88b 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -67,927 +67,903 @@ char const *contentTypes[][2] = {{".txt", "text/plain"}, {".html", "text/htm // Our API to handle messages to and from the radio. HttpAPI webAPI; -void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) -{ +void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) { - // For every resource available on the server, we need to create a ResourceNode - // The ResourceNode links URL and HTTP method to a handler function + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function - ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); - ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); - ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio); - ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); + ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio); + ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); - // ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); - // ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot); + // ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); + // ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot); - ResourceNode *nodeAdmin = new ResourceNode("/admin", "GET", &handleAdmin); - // ResourceNode *nodeAdminSettings = new ResourceNode("/admin/settings", "GET", &handleAdminSettings); - // ResourceNode *nodeAdminSettingsApply = new ResourceNode("/admin/settings/apply", "POST", &handleAdminSettingsApply); - // ResourceNode *nodeAdminFs = new ResourceNode("/admin/fs", "GET", &handleFs); - // ResourceNode *nodeUpdateFs = new ResourceNode("/admin/fs/update", "POST", &handleUpdateFs); - // ResourceNode *nodeDeleteFs = new ResourceNode("/admin/fs/delete", "GET", &handleDeleteFsContent); + ResourceNode *nodeAdmin = new ResourceNode("/admin", "GET", &handleAdmin); + // ResourceNode *nodeAdminSettings = new ResourceNode("/admin/settings", "GET", &handleAdminSettings); + // ResourceNode *nodeAdminSettingsApply = new ResourceNode("/admin/settings/apply", "POST", + // &handleAdminSettingsApply); ResourceNode *nodeAdminFs = new ResourceNode("/admin/fs", "GET", &handleFs); + // ResourceNode *nodeUpdateFs = new ResourceNode("/admin/fs/update", "POST", &handleUpdateFs); + // ResourceNode *nodeDeleteFs = new ResourceNode("/admin/fs/delete", "GET", &handleDeleteFsContent); - ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart); - ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); + ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart); + ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); - ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); - ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); - ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); - ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); - ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); - ResourceNode *nodeJsonDelete = new ResourceNode("/json/fs/delete/static", "DELETE", &handleFsDeleteStatic); + ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); + ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); + ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); + ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); + ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); + ResourceNode *nodeJsonDelete = new ResourceNode("/json/fs/delete/static", "DELETE", &handleFsDeleteStatic); - ResourceNode *nodeRoot = new ResourceNode("/*", "GET", &handleStatic); + ResourceNode *nodeRoot = new ResourceNode("/*", "GET", &handleStatic); - // Secure nodes - secureServer->registerNode(nodeAPIv1ToRadioOptions); - secureServer->registerNode(nodeAPIv1ToRadio); - secureServer->registerNode(nodeAPIv1FromRadioOptions); - secureServer->registerNode(nodeAPIv1FromRadio); - // secureServer->registerNode(nodeHotspotApple); - // secureServer->registerNode(nodeHotspotAndroid); - secureServer->registerNode(nodeRestart); - secureServer->registerNode(nodeFormUpload); - secureServer->registerNode(nodeJsonScanNetworks); - secureServer->registerNode(nodeJsonBlinkLED); - secureServer->registerNode(nodeJsonFsBrowseStatic); - secureServer->registerNode(nodeJsonDelete); - secureServer->registerNode(nodeJsonReport); - secureServer->registerNode(nodeJsonNodes); - // secureServer->registerNode(nodeUpdateFs); - // secureServer->registerNode(nodeDeleteFs); - secureServer->registerNode(nodeAdmin); - // secureServer->registerNode(nodeAdminFs); - // secureServer->registerNode(nodeAdminSettings); - // secureServer->registerNode(nodeAdminSettingsApply); - secureServer->registerNode(nodeRoot); // This has to be last + // Secure nodes + secureServer->registerNode(nodeAPIv1ToRadioOptions); + secureServer->registerNode(nodeAPIv1ToRadio); + secureServer->registerNode(nodeAPIv1FromRadioOptions); + secureServer->registerNode(nodeAPIv1FromRadio); + // secureServer->registerNode(nodeHotspotApple); + // secureServer->registerNode(nodeHotspotAndroid); + secureServer->registerNode(nodeRestart); + secureServer->registerNode(nodeFormUpload); + secureServer->registerNode(nodeJsonScanNetworks); + secureServer->registerNode(nodeJsonBlinkLED); + secureServer->registerNode(nodeJsonFsBrowseStatic); + secureServer->registerNode(nodeJsonDelete); + secureServer->registerNode(nodeJsonReport); + secureServer->registerNode(nodeJsonNodes); + // secureServer->registerNode(nodeUpdateFs); + // secureServer->registerNode(nodeDeleteFs); + secureServer->registerNode(nodeAdmin); + // secureServer->registerNode(nodeAdminFs); + // secureServer->registerNode(nodeAdminSettings); + // secureServer->registerNode(nodeAdminSettingsApply); + secureServer->registerNode(nodeRoot); // This has to be last - // Insecure nodes - insecureServer->registerNode(nodeAPIv1ToRadioOptions); - insecureServer->registerNode(nodeAPIv1ToRadio); - insecureServer->registerNode(nodeAPIv1FromRadioOptions); - insecureServer->registerNode(nodeAPIv1FromRadio); - // insecureServer->registerNode(nodeHotspotApple); - // insecureServer->registerNode(nodeHotspotAndroid); - insecureServer->registerNode(nodeRestart); - insecureServer->registerNode(nodeFormUpload); - insecureServer->registerNode(nodeJsonScanNetworks); - insecureServer->registerNode(nodeJsonBlinkLED); - insecureServer->registerNode(nodeJsonFsBrowseStatic); - insecureServer->registerNode(nodeJsonDelete); - insecureServer->registerNode(nodeJsonReport); - // insecureServer->registerNode(nodeUpdateFs); - // insecureServer->registerNode(nodeDeleteFs); - insecureServer->registerNode(nodeAdmin); - // insecureServer->registerNode(nodeAdminFs); - // insecureServer->registerNode(nodeAdminSettings); - // insecureServer->registerNode(nodeAdminSettingsApply); - insecureServer->registerNode(nodeRoot); // This has to be last + // Insecure nodes + insecureServer->registerNode(nodeAPIv1ToRadioOptions); + insecureServer->registerNode(nodeAPIv1ToRadio); + insecureServer->registerNode(nodeAPIv1FromRadioOptions); + insecureServer->registerNode(nodeAPIv1FromRadio); + // insecureServer->registerNode(nodeHotspotApple); + // insecureServer->registerNode(nodeHotspotAndroid); + insecureServer->registerNode(nodeRestart); + insecureServer->registerNode(nodeFormUpload); + insecureServer->registerNode(nodeJsonScanNetworks); + insecureServer->registerNode(nodeJsonBlinkLED); + insecureServer->registerNode(nodeJsonFsBrowseStatic); + insecureServer->registerNode(nodeJsonDelete); + insecureServer->registerNode(nodeJsonReport); + // insecureServer->registerNode(nodeUpdateFs); + // insecureServer->registerNode(nodeDeleteFs); + insecureServer->registerNode(nodeAdmin); + // insecureServer->registerNode(nodeAdminFs); + // insecureServer->registerNode(nodeAdminSettings); + // insecureServer->registerNode(nodeAdminSettingsApply); + insecureServer->registerNode(nodeRoot); // This has to be last } -void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) -{ - if (webServerThread) - webServerThread->markActivity(); +void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) { + if (webServerThread) + webServerThread->markActivity(); - LOG_DEBUG("webAPI handleAPIv1FromRadio"); + LOG_DEBUG("webAPI handleAPIv1FromRadio"); - /* - For documentation, see: - https://meshtastic.org/docs/development/device/http-api - https://meshtastic.org/docs/development/device/client-api - */ + /* + For documentation, see: + https://meshtastic.org/docs/development/device/http-api + https://meshtastic.org/docs/development/device/client-api + */ - // Get access to the parameters - ResourceParameters *params = req->getParams(); + // Get access to the parameters + ResourceParameters *params = req->getParams(); - // std::string paramAll = "all"; - std::string valueAll; + // std::string paramAll = "all"; + std::string valueAll; - // Status code is 200 OK by default. - res->setHeader("Content-Type", "application/x-protobuf"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + // Status code is 200 OK by default. + res->setHeader("Content-Type", "application/x-protobuf"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (req->getMethod() == "OPTIONS") { - res->setStatusCode(204); // Success with no content - // res->print(""); @todo remove - return; - } + if (req->getMethod() == "OPTIONS") { + res->setStatusCode(204); // Success with no content + // res->print(""); @todo remove + return; + } - uint8_t txBuf[MAX_STREAM_BUF_SIZE]; - uint32_t len = 1; + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; - if (params->getQueryParameter("all", valueAll)) { + if (params->getQueryParameter("all", valueAll)) { - // If all is true, return all the buffers we have available - // to us at this point in time. - if (valueAll == "true") { - while (len) { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); - } - - // Otherwise, just return one protobuf - } else { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); - } - - // the param "all" was not specified. Return just one protobuf - } else { + // If all is true, return all the buffers we have available + // to us at this point in time. + if (valueAll == "true") { + while (len) { len = webAPI.getFromRadio(txBuf); res->write(txBuf, len); + } + + // Otherwise, just return one protobuf + } else { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); } - LOG_DEBUG("webAPI handleAPIv1FromRadio, len %d", len); + // the param "all" was not specified. Return just one protobuf + } else { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); + } + + LOG_DEBUG("webAPI handleAPIv1FromRadio, len %d", len); } -void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) -{ - LOG_DEBUG("webAPI handleAPIv1ToRadio"); +void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) { + LOG_DEBUG("webAPI handleAPIv1ToRadio"); - /* - For documentation, see: - https://meshtastic.org/docs/development/device/http-api - https://meshtastic.org/docs/development/device/client-api - */ + /* + For documentation, see: + https://meshtastic.org/docs/development/device/http-api + https://meshtastic.org/docs/development/device/client-api + */ - res->setHeader("Content-Type", "application/x-protobuf"); - res->setHeader("Access-Control-Allow-Headers", "Content-Type"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "PUT, OPTIONS"); - res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + res->setHeader("Content-Type", "application/x-protobuf"); + res->setHeader("Access-Control-Allow-Headers", "Content-Type"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "PUT, OPTIONS"); + res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (req->getMethod() == "OPTIONS") { - res->setStatusCode(204); // Success with no content - // res->print(""); @todo remove - return; - } + if (req->getMethod() == "OPTIONS") { + res->setStatusCode(204); // Success with no content + // res->print(""); @todo remove + return; + } - byte buffer[MAX_TO_FROM_RADIO_SIZE]; - size_t s = req->readBytes(buffer, MAX_TO_FROM_RADIO_SIZE); + byte buffer[MAX_TO_FROM_RADIO_SIZE]; + size_t s = req->readBytes(buffer, MAX_TO_FROM_RADIO_SIZE); - LOG_DEBUG("Received %d bytes from PUT request", s); - webAPI.handleToRadio(buffer, s); + LOG_DEBUG("Received %d bytes from PUT request", s); + webAPI.handleToRadio(buffer, s); - res->write(buffer, s); - LOG_DEBUG("webAPI handleAPIv1ToRadio"); + res->write(buffer, s); + LOG_DEBUG("webAPI handleAPIv1ToRadio"); } -void htmlDeleteDir(const char *dirname) -{ +void htmlDeleteDir(const char *dirname) { - File root = FSCom.open(dirname); - if (!root) { - return; - } - if (!root.isDirectory()) { - return; - } + File root = FSCom.open(dirname); + if (!root) { + return; + } + if (!root.isDirectory()) { + return; + } - File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - htmlDeleteDir(file.name()); - file.flush(); - file.close(); - } else { - String fileName = String(file.name()); - file.flush(); - file.close(); - LOG_DEBUG(" %s", fileName.c_str()); - FSCom.remove(fileName); - } - file = root.openNextFile(); + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + htmlDeleteDir(file.name()); + file.flush(); + file.close(); + } else { + String fileName = String(file.name()); + file.flush(); + file.close(); + LOG_DEBUG(" %s", fileName.c_str()); + FSCom.remove(fileName); } - root.flush(); - root.close(); + file = root.openNextFile(); + } + root.flush(); + root.close(); } -JSONArray htmlListDir(const char *dirname, uint8_t levels) -{ - File root = FSCom.open(dirname, FILE_O_READ); - JSONArray fileList; - if (!root) { - return fileList; - } - if (!root.isDirectory()) { - return fileList; - } - - // iterate over the file list - File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { -#ifdef ARCH_ESP32 - fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1))); -#else - fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1))); -#endif - file.close(); - } - } else { - JSONObject thisFileMap; - thisFileMap["size"] = new JSONValue((int)file.size()); -#ifdef ARCH_ESP32 - String fileName = String(file.path()).substring(1); - thisFileMap["name"] = new JSONValue(fileName.c_str()); -#else - String fileName = String(file.name()).substring(1); - thisFileMap["name"] = new JSONValue(fileName.c_str()); -#endif - String tempName = String(file.name()).substring(1); - if (tempName.endsWith(".gz")) { -#ifdef ARCH_ESP32 - String modifiedFile = String(file.path()).substring(1); -#else - String modifiedFile = String(file.name()).substring(1); -#endif - modifiedFile.remove((modifiedFile.length() - 3), 3); - thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str()); - } - fileList.push_back(new JSONValue(thisFileMap)); - } - file.close(); - file = root.openNextFile(); - } - root.close(); +JSONArray htmlListDir(const char *dirname, uint8_t levels) { + File root = FSCom.open(dirname, FILE_O_READ); + JSONArray fileList; + if (!root) { return fileList; + } + if (!root.isDirectory()) { + return fileList; + } + + // iterate over the file list + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { +#ifdef ARCH_ESP32 + fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1))); +#else + fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1))); +#endif + file.close(); + } + } else { + JSONObject thisFileMap; + thisFileMap["size"] = new JSONValue((int)file.size()); +#ifdef ARCH_ESP32 + String fileName = String(file.path()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); +#else + String fileName = String(file.name()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); +#endif + String tempName = String(file.name()).substring(1); + if (tempName.endsWith(".gz")) { +#ifdef ARCH_ESP32 + String modifiedFile = String(file.path()).substring(1); +#else + String modifiedFile = String(file.name()).substring(1); +#endif + modifiedFile.remove((modifiedFile.length() - 3), 3); + thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str()); + } + fileList.push_back(new JSONValue(thisFileMap)); + } + file.close(); + file = root.openNextFile(); + } + root.close(); + return fileList; } -void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) -{ +void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + + concurrency::LockGuard g(spiLock); + auto fileList = htmlListDir("/static", 10); + + // create json output structure + JSONObject filesystemObj; + filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes()); + filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes()); + filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes())); + + JSONObject jsonObjInner; + jsonObjInner["files"] = new JSONValue(fileList); + jsonObjInner["filesystem"] = new JSONValue(filesystemObj); + + JSONObject jsonObjOuter; + jsonObjOuter["data"] = new JSONValue(jsonObjInner); + jsonObjOuter["status"] = new JSONValue("ok"); + + JSONValue *value = new JSONValue(jsonObjOuter); + + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + + delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } +} + +void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) { + ResourceParameters *params = req->getParams(); + std::string paramValDelete; + + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "DELETE"); + + if (params->getQueryParameter("delete", paramValDelete)) { + std::string pathDelete = "/" + paramValDelete; + concurrency::LockGuard g(spiLock); + if (FSCom.remove(pathDelete.c_str())) { + + LOG_INFO("%s", pathDelete.c_str()); + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("ok"); + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; + return; + } else { + + LOG_INFO("%s", pathDelete.c_str()); + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("Error"); + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; + return; + } + } +} + +void handleStatic(HTTPRequest *req, HTTPResponse *res) { + if (webServerThread) + webServerThread->markActivity(); + + // Get access to the parameters + ResourceParameters *params = req->getParams(); + + std::string parameter1; + // Print the first parameter value + if (params->getPathParameter(0, parameter1)) { + + std::string filename = "/static/" + parameter1; + std::string filenameGzip = "/static/" + parameter1 + ".gz"; + + // Try to open the file + File file; + + bool has_set_content_type = false; + + if (filename == "/static/") { + filename = "/static/index.html"; + filenameGzip = "/static/index.html.gz"; + } + + concurrency::LockGuard g(spiLock); + + if (FSCom.exists(filename.c_str())) { + file = FSCom.open(filename.c_str()); + if (!file.available()) { + LOG_WARN("File not available - %s", filename.c_str()); + } + } else if (FSCom.exists(filenameGzip.c_str())) { + file = FSCom.open(filenameGzip.c_str()); + res->setHeader("Content-Encoding", "gzip"); + if (!file.available()) { + LOG_WARN("File not available - %s", filenameGzip.c_str()); + } + } else { + has_set_content_type = true; + filenameGzip = "/static/index.html.gz"; + file = FSCom.open(filenameGzip.c_str()); + res->setHeader("Content-Type", "text/html"); + if (!file.available()) { + + LOG_WARN("File not available - %s", filenameGzip.c_str()); + res->println("Web server is running.

The content you are looking for can't be found. Please see: FAQ.

admin"); + + return; + } else { + res->setHeader("Content-Encoding", "gzip"); + } + } + + res->setHeader("Content-Length", httpsserver::intToString(file.size())); + + // Content-Type is guessed using the definition of the contentTypes-table defined above + int cTypeIdx = 0; + do { + if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) { + res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); + has_set_content_type = true; + break; + } + cTypeIdx += 1; + } while (strlen(contentTypes[cTypeIdx][0]) > 0); + + if (!has_set_content_type) { + // Set a default content type + res->setHeader("Content-Type", "application/octet-stream"); + } + + // Read the file and write it to the HTTP response body + size_t length = 0; + do { + char buffer[256]; + length = file.read((uint8_t *)buffer, 256); + std::string bufferString(buffer, length); + res->write((uint8_t *)bufferString.c_str(), bufferString.size()); + } while (length > 0); + + file.close(); + + return; + } else { + LOG_ERROR("This should not have happened"); + res->println("ERROR: This should not have happened"); + } +} + +void handleFormUpload(HTTPRequest *req, HTTPResponse *res) { + + LOG_DEBUG("Form Upload - Disable keep-alive"); + res->setHeader("Connection", "close"); + + // First, we need to check the encoding of the form that we have received. + // The browser will set the Content-Type request header, so we can use it for that purpose. + // Then we select the body parser based on the encoding. + // Actually we do this only for documentary purposes, we know the form is going + // to be multipart/form-data. + LOG_DEBUG("Form Upload - Creating body parser reference"); + HTTPBodyParser *parser; + std::string contentType = req->getHeader("Content-Type"); + + // The content type may have additional properties after a semicolon, for example: + // Content-Type: text/html;charset=utf-8 + // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs + // As we're interested only in the actual mime _type_, we strip everything after the + // first semicolon, if one exists: + size_t semicolonPos = contentType.find(";"); + if (semicolonPos != std::string::npos) { + contentType.resize(semicolonPos); + } + + // Now, we can decide based on the content type: + if (contentType == "multipart/form-data") { + LOG_DEBUG("Form Upload - multipart/form-data"); + parser = new HTTPMultipartBodyParser(req); + } else { + LOG_DEBUG("Unknown POST Content-Type: %s", contentType.c_str()); + return; + } + + res->println("File " + "Upload

File Upload

"); + + // We iterate over the fields. Any field with a filename is uploaded. + // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's + // fields only a single time. The reason for this is that it allows you to handle large requests + // which would not fit into memory. + bool didwrite = false; + + // parser->nextField() will move the parser to the next field in the request body (field meaning a + // form field, if you take the HTML perspective). After the last field has been processed, nextField() + // returns false and the while loop ends. + while (parser->nextField()) { + // For Multipart data, each field has three properties: + // The name ("name" value of the tag) + // The filename (If it was a , this is the filename on the machine of the + // user uploading it) + // The mime type (It is determined by the client. So do not trust this value and blindly start + // parsing files only if the type matches) + std::string name = parser->getFieldName(); + std::string filename = parser->getFieldFilename(); + std::string mimeType = parser->getFieldMimeType(); + // We log all three values, so that you can observe the upload on the serial monitor: + LOG_DEBUG("handleFormUpload: field name='%s', filename='%s', mimetype='%s'", name.c_str(), filename.c_str(), mimeType.c_str()); + + // Double check that it is what we expect + if (name != "file") { + LOG_DEBUG("Skip unexpected field"); + res->println("

No file found.

"); + return; + } + + // Double check that it is what we expect + if (filename == "") { + LOG_DEBUG("Skip unexpected field"); + res->println("

No file found.

"); + return; + } + + // You should check file name validity and all that, but we skip that to make the core + // concepts of the body parser functionality easier to understand. + std::string pathname = "/static/" + filename; + + concurrency::LockGuard g(spiLock); + // Create a new file to stream the data into + File file = FSCom.open(pathname.c_str(), FILE_O_WRITE); + size_t fileLength = 0; + didwrite = true; + + // With endOfField you can check whether the end of field has been reached or if there's + // still data pending. With multipart bodies, you cannot know the field size in advance. + while (!parser->endOfField()) { + esp_task_wdt_reset(); + + byte buf[512]; + size_t readLength = parser->read(buf, 512); + // LOG_DEBUG("readLength - %i", readLength); + + // Abort the transfer if there is less than 50k space left on the filesystem. + if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { + file.flush(); + file.close(); + res->println("

Write aborted! Reserving 50k on filesystem.

"); + + // enableLoopWDT(); + + delete parser; + return; + } + + // if (readLength) { + file.write(buf, readLength); + fileLength += readLength; + LOG_DEBUG("File Length %i", fileLength); + //} + } + // enableLoopWDT(); + + file.flush(); + file.close(); + + res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); + } + if (!didwrite) { + res->println("

Did not write any file

"); + } + res->println(""); + delete parser; +} + +void handleReport(HTTPRequest *req, HTTPResponse *res) { + ResourceParameters *params = req->getParams(); + std::string content; + + if (!params->getQueryParameter("content", content)) { + content = "json"; + } + + if (content == "json") { res->setHeader("Content-Type", "application/json"); res->setHeader("Access-Control-Allow-Origin", "*"); res->setHeader("Access-Control-Allow-Methods", "GET"); + } else { + res->setHeader("Content-Type", "text/html"); + res->println("
");
+  }
 
-    concurrency::LockGuard g(spiLock);
-    auto fileList = htmlListDir("/static", 10);
-
-    // create json output structure
-    JSONObject filesystemObj;
-    filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes());
-    filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes());
-    filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
-
-    JSONObject jsonObjInner;
-    jsonObjInner["files"] = new JSONValue(fileList);
-    jsonObjInner["filesystem"] = new JSONValue(filesystemObj);
-
-    JSONObject jsonObjOuter;
-    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
-    jsonObjOuter["status"] = new JSONValue("ok");
-
-    JSONValue *value = new JSONValue(jsonObjOuter);
-
-    std::string jsonString = value->Stringify();
-    res->print(jsonString.c_str());
-
-    delete value;
-
-    // Clean up the fileList to prevent memory leak
-    for (auto *val : fileList) {
-        delete val;
+  // Helper lambda to create JSON array and clean up memory properly
+  auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
+    JSONArray tempArray;
+    for (int i = 0; i < count; i++) {
+      tempArray.push_back(new JSONValue((int)logArray[i]));
     }
+    JSONValue *result = new JSONValue(tempArray);
+    // Note: Don't delete tempArray elements here - JSONValue now owns them
+    return result;
+  };
+
+  // data->airtime->tx_log
+  uint32_t *logArray;
+  logArray = airTime->airtimeReport(TX_LOG);
+  JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
+
+  // data->airtime->rx_log
+  logArray = airTime->airtimeReport(RX_LOG);
+  JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
+
+  // data->airtime->rx_all_log
+  logArray = airTime->airtimeReport(RX_ALL_LOG);
+  JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
+
+  // data->airtime
+  JSONObject jsonObjAirtime;
+  jsonObjAirtime["tx_log"] = txLogJsonValue;
+  jsonObjAirtime["rx_log"] = rxLogJsonValue;
+  jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
+  jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
+  jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
+  jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
+  jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
+  jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
+
+  // data->wifi
+  JSONObject jsonObjWifi;
+  jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
+  String wifiIPString = WiFi.localIP().toString();
+  std::string wifiIP = wifiIPString.c_str();
+  jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
+
+  // data->memory
+  JSONObject jsonObjMemory;
+  jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
+  jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
+  jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
+  jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
+  spiLock->lock();
+  jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
+  jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
+  jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
+  spiLock->unlock();
+
+  // data->power
+  JSONObject jsonObjPower;
+  jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
+  jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
+  jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
+  jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
+  jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
+
+  // data->device
+  JSONObject jsonObjDevice;
+  jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
+
+  // data->radio
+  JSONObject jsonObjRadio;
+  jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
+  jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
+
+  // collect data to inner data object
+  JSONObject jsonObjInner;
+  jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
+  jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
+  jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
+  jsonObjInner["power"] = new JSONValue(jsonObjPower);
+  jsonObjInner["device"] = new JSONValue(jsonObjDevice);
+  jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
+
+  // create json output structure
+  JSONObject jsonObjOuter;
+  jsonObjOuter["data"] = new JSONValue(jsonObjInner);
+  jsonObjOuter["status"] = new JSONValue("ok");
+  // serialize and write it to the stream
+  JSONValue *value = new JSONValue(jsonObjOuter);
+  std::string jsonString = value->Stringify();
+  res->print(jsonString.c_str());
+  delete value;
 }
 
-void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
-{
-    ResourceParameters *params = req->getParams();
-    std::string paramValDelete;
+void handleNodes(HTTPRequest *req, HTTPResponse *res) {
+  ResourceParameters *params = req->getParams();
+  std::string content;
 
+  if (!params->getQueryParameter("content", content)) {
+    content = "json";
+  }
+
+  if (content == "json") {
     res->setHeader("Content-Type", "application/json");
     res->setHeader("Access-Control-Allow-Origin", "*");
-    res->setHeader("Access-Control-Allow-Methods", "DELETE");
+    res->setHeader("Access-Control-Allow-Methods", "GET");
+  } else {
+    res->setHeader("Content-Type", "text/html");
+    res->println("
");
+  }
 
-    if (params->getQueryParameter("delete", paramValDelete)) {
-        std::string pathDelete = "/" + paramValDelete;
-        concurrency::LockGuard g(spiLock);
-        if (FSCom.remove(pathDelete.c_str())) {
+  JSONArray nodesArray;
 
-            LOG_INFO("%s", pathDelete.c_str());
-            JSONObject jsonObjOuter;
-            jsonObjOuter["status"] = new JSONValue("ok");
-            JSONValue *value = new JSONValue(jsonObjOuter);
-            std::string jsonString = value->Stringify();
-            res->print(jsonString.c_str());
-            delete value;
-            return;
-        } else {
+  uint32_t readIndex = 0;
+  const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
+  while (tempNodeInfo != NULL) {
+    if (tempNodeInfo->has_user) {
+      JSONObject node;
 
-            LOG_INFO("%s", pathDelete.c_str());
-            JSONObject jsonObjOuter;
-            jsonObjOuter["status"] = new JSONValue("Error");
-            JSONValue *value = new JSONValue(jsonObjOuter);
-            std::string jsonString = value->Stringify();
-            res->print(jsonString.c_str());
-            delete value;
-            return;
-        }
-    }
-}
-
-void handleStatic(HTTPRequest *req, HTTPResponse *res)
-{
-    if (webServerThread)
-        webServerThread->markActivity();
-
-    // Get access to the parameters
-    ResourceParameters *params = req->getParams();
-
-    std::string parameter1;
-    // Print the first parameter value
-    if (params->getPathParameter(0, parameter1)) {
-
-        std::string filename = "/static/" + parameter1;
-        std::string filenameGzip = "/static/" + parameter1 + ".gz";
-
-        // Try to open the file
-        File file;
-
-        bool has_set_content_type = false;
-
-        if (filename == "/static/") {
-            filename = "/static/index.html";
-            filenameGzip = "/static/index.html.gz";
-        }
-
-        concurrency::LockGuard g(spiLock);
-
-        if (FSCom.exists(filename.c_str())) {
-            file = FSCom.open(filename.c_str());
-            if (!file.available()) {
-                LOG_WARN("File not available - %s", filename.c_str());
-            }
-        } else if (FSCom.exists(filenameGzip.c_str())) {
-            file = FSCom.open(filenameGzip.c_str());
-            res->setHeader("Content-Encoding", "gzip");
-            if (!file.available()) {
-                LOG_WARN("File not available - %s", filenameGzip.c_str());
-            }
-        } else {
-            has_set_content_type = true;
-            filenameGzip = "/static/index.html.gz";
-            file = FSCom.open(filenameGzip.c_str());
-            res->setHeader("Content-Type", "text/html");
-            if (!file.available()) {
-
-                LOG_WARN("File not available - %s", filenameGzip.c_str());
-                res->println("Web server is running.

The content you are looking for can't be found. Please see: FAQ.

admin"); - - return; - } else { - res->setHeader("Content-Encoding", "gzip"); - } - } - - res->setHeader("Content-Length", httpsserver::intToString(file.size())); - - // Content-Type is guessed using the definition of the contentTypes-table defined above - int cTypeIdx = 0; - do { - if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) { - res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); - has_set_content_type = true; - break; - } - cTypeIdx += 1; - } while (strlen(contentTypes[cTypeIdx][0]) > 0); - - if (!has_set_content_type) { - // Set a default content type - res->setHeader("Content-Type", "application/octet-stream"); - } - - // Read the file and write it to the HTTP response body - size_t length = 0; - do { - char buffer[256]; - length = file.read((uint8_t *)buffer, 256); - std::string bufferString(buffer, length); - res->write((uint8_t *)bufferString.c_str(), bufferString.size()); - } while (length > 0); - - file.close(); - - return; - } else { - LOG_ERROR("This should not have happened"); - res->println("ERROR: This should not have happened"); - } -} - -void handleFormUpload(HTTPRequest *req, HTTPResponse *res) -{ - - LOG_DEBUG("Form Upload - Disable keep-alive"); - res->setHeader("Connection", "close"); - - // First, we need to check the encoding of the form that we have received. - // The browser will set the Content-Type request header, so we can use it for that purpose. - // Then we select the body parser based on the encoding. - // Actually we do this only for documentary purposes, we know the form is going - // to be multipart/form-data. - LOG_DEBUG("Form Upload - Creating body parser reference"); - HTTPBodyParser *parser; - std::string contentType = req->getHeader("Content-Type"); - - // The content type may have additional properties after a semicolon, for example: - // Content-Type: text/html;charset=utf-8 - // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs - // As we're interested only in the actual mime _type_, we strip everything after the - // first semicolon, if one exists: - size_t semicolonPos = contentType.find(";"); - if (semicolonPos != std::string::npos) { - contentType.resize(semicolonPos); - } - - // Now, we can decide based on the content type: - if (contentType == "multipart/form-data") { - LOG_DEBUG("Form Upload - multipart/form-data"); - parser = new HTTPMultipartBodyParser(req); - } else { - LOG_DEBUG("Unknown POST Content-Type: %s", contentType.c_str()); - return; - } - - res->println("File " - "Upload

File Upload

"); - - // We iterate over the fields. Any field with a filename is uploaded. - // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's - // fields only a single time. The reason for this is that it allows you to handle large requests - // which would not fit into memory. - bool didwrite = false; - - // parser->nextField() will move the parser to the next field in the request body (field meaning a - // form field, if you take the HTML perspective). After the last field has been processed, nextField() - // returns false and the while loop ends. - while (parser->nextField()) { - // For Multipart data, each field has three properties: - // The name ("name" value of the tag) - // The filename (If it was a , this is the filename on the machine of the - // user uploading it) - // The mime type (It is determined by the client. So do not trust this value and blindly start - // parsing files only if the type matches) - std::string name = parser->getFieldName(); - std::string filename = parser->getFieldFilename(); - std::string mimeType = parser->getFieldMimeType(); - // We log all three values, so that you can observe the upload on the serial monitor: - LOG_DEBUG("handleFormUpload: field name='%s', filename='%s', mimetype='%s'", name.c_str(), filename.c_str(), - mimeType.c_str()); - - // Double check that it is what we expect - if (name != "file") { - LOG_DEBUG("Skip unexpected field"); - res->println("

No file found.

"); - return; - } - - // Double check that it is what we expect - if (filename == "") { - LOG_DEBUG("Skip unexpected field"); - res->println("

No file found.

"); - return; - } - - // You should check file name validity and all that, but we skip that to make the core - // concepts of the body parser functionality easier to understand. - std::string pathname = "/static/" + filename; - - concurrency::LockGuard g(spiLock); - // Create a new file to stream the data into - File file = FSCom.open(pathname.c_str(), FILE_O_WRITE); - size_t fileLength = 0; - didwrite = true; - - // With endOfField you can check whether the end of field has been reached or if there's - // still data pending. With multipart bodies, you cannot know the field size in advance. - while (!parser->endOfField()) { - esp_task_wdt_reset(); - - byte buf[512]; - size_t readLength = parser->read(buf, 512); - // LOG_DEBUG("readLength - %i", readLength); - - // Abort the transfer if there is less than 50k space left on the filesystem. - if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { - file.flush(); - file.close(); - res->println("

Write aborted! Reserving 50k on filesystem.

"); - - // enableLoopWDT(); - - delete parser; - return; - } - - // if (readLength) { - file.write(buf, readLength); - fileLength += readLength; - LOG_DEBUG("File Length %i", fileLength); - //} - } - // enableLoopWDT(); - - file.flush(); - file.close(); - - res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); - } - if (!didwrite) { - res->println("

Did not write any file

"); - } - res->println(""); - delete parser; -} - -void handleReport(HTTPRequest *req, HTTPResponse *res) -{ - ResourceParameters *params = req->getParams(); - std::string content; - - if (!params->getQueryParameter("content", content)) { - content = "json"; - } - - if (content == "json") { - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - } else { - res->setHeader("Content-Type", "text/html"); - res->println("
");
-    }
-
-    // Helper lambda to create JSON array and clean up memory properly
-    auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
-        JSONArray tempArray;
-        for (int i = 0; i < count; i++) {
-            tempArray.push_back(new JSONValue((int)logArray[i]));
-        }
-        JSONValue *result = new JSONValue(tempArray);
-        // Note: Don't delete tempArray elements here - JSONValue now owns them
-        return result;
-    };
-
-    // data->airtime->tx_log
-    uint32_t *logArray;
-    logArray = airTime->airtimeReport(TX_LOG);
-    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
-
-    // data->airtime->rx_log
-    logArray = airTime->airtimeReport(RX_LOG);
-    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
-
-    // data->airtime->rx_all_log
-    logArray = airTime->airtimeReport(RX_ALL_LOG);
-    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
-
-    // data->airtime
-    JSONObject jsonObjAirtime;
-    jsonObjAirtime["tx_log"] = txLogJsonValue;
-    jsonObjAirtime["rx_log"] = rxLogJsonValue;
-    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
-    jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
-    jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
-    jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
-    jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
-    jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
-
-    // data->wifi
-    JSONObject jsonObjWifi;
-    jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
-    String wifiIPString = WiFi.localIP().toString();
-    std::string wifiIP = wifiIPString.c_str();
-    jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
-
-    // data->memory
-    JSONObject jsonObjMemory;
-    jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
-    jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
-    jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
-    jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
-    spiLock->lock();
-    jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
-    jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
-    jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
-    spiLock->unlock();
-
-    // data->power
-    JSONObject jsonObjPower;
-    jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
-    jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
-    jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
-    jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
-    jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
-
-    // data->device
-    JSONObject jsonObjDevice;
-    jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
-
-    // data->radio
-    JSONObject jsonObjRadio;
-    jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
-    jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
-
-    // collect data to inner data object
-    JSONObject jsonObjInner;
-    jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
-    jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
-    jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
-    jsonObjInner["power"] = new JSONValue(jsonObjPower);
-    jsonObjInner["device"] = new JSONValue(jsonObjDevice);
-    jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
-
-    // create json output structure
-    JSONObject jsonObjOuter;
-    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
-    jsonObjOuter["status"] = new JSONValue("ok");
-    // serialize and write it to the stream
-    JSONValue *value = new JSONValue(jsonObjOuter);
-    std::string jsonString = value->Stringify();
-    res->print(jsonString.c_str());
-    delete value;
-}
-
-void handleNodes(HTTPRequest *req, HTTPResponse *res)
-{
-    ResourceParameters *params = req->getParams();
-    std::string content;
-
-    if (!params->getQueryParameter("content", content)) {
-        content = "json";
-    }
-
-    if (content == "json") {
-        res->setHeader("Content-Type", "application/json");
-        res->setHeader("Access-Control-Allow-Origin", "*");
-        res->setHeader("Access-Control-Allow-Methods", "GET");
-    } else {
-        res->setHeader("Content-Type", "text/html");
-        res->println("
");
-    }
-
-    JSONArray nodesArray;
-
-    uint32_t readIndex = 0;
-    const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
-    while (tempNodeInfo != NULL) {
-        if (tempNodeInfo->has_user) {
-            JSONObject node;
-
-            char id[16];
-            snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
-
-            node["id"] = new JSONValue(id);
-            node["snr"] = new JSONValue(tempNodeInfo->snr);
-            node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
-            node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
-            node["position"] = new JSONValue();
-
-            if (nodeDB->hasValidPosition(tempNodeInfo)) {
-                JSONObject position;
-                position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
-                position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
-                position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
-                node["position"] = new JSONValue(position);
-            }
-
-            node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
-            node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
-            char macStr[18];
-            snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
-                     tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3],
-                     tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
-            node["mac_address"] = new JSONValue(macStr);
-            node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
-
-            nodesArray.push_back(new JSONValue(node));
-        }
-        tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
-    }
-
-    // collect data to inner data object
-    JSONObject jsonObjInner;
-    jsonObjInner["nodes"] = new JSONValue(nodesArray);
-
-    // create json output structure
-    JSONObject jsonObjOuter;
-    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
-    jsonObjOuter["status"] = new JSONValue("ok");
-    // serialize and write it to the stream
-    JSONValue *value = new JSONValue(jsonObjOuter);
-    std::string jsonString = value->Stringify();
-    res->print(jsonString.c_str());
-    delete value;
-
-    // Clean up the nodesArray to prevent memory leak
-    for (auto *val : nodesArray) {
-        delete val;
+      char id[16];
+      snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
+
+      node["id"] = new JSONValue(id);
+      node["snr"] = new JSONValue(tempNodeInfo->snr);
+      node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
+      node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
+      node["position"] = new JSONValue();
+
+      if (nodeDB->hasValidPosition(tempNodeInfo)) {
+        JSONObject position;
+        position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
+        position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
+        position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
+        node["position"] = new JSONValue(position);
+      }
+
+      node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
+      node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
+      char macStr[18];
+      snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0], tempNodeInfo->user.macaddr[1],
+               tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3], tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
+      node["mac_address"] = new JSONValue(macStr);
+      node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
+
+      nodesArray.push_back(new JSONValue(node));
     }
+    tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
+  }
+
+  // collect data to inner data object
+  JSONObject jsonObjInner;
+  jsonObjInner["nodes"] = new JSONValue(nodesArray);
+
+  // create json output structure
+  JSONObject jsonObjOuter;
+  jsonObjOuter["data"] = new JSONValue(jsonObjInner);
+  jsonObjOuter["status"] = new JSONValue("ok");
+  // serialize and write it to the stream
+  JSONValue *value = new JSONValue(jsonObjOuter);
+  std::string jsonString = value->Stringify();
+  res->print(jsonString.c_str());
+  delete value;
+
+  // Clean up the nodesArray to prevent memory leak
+  for (auto *val : nodesArray) {
+    delete val;
+  }
 }
 
 /*
     This supports the Apple Captive Network Assistant (CNA) Portal
 */
-void handleHotspot(HTTPRequest *req, HTTPResponse *res)
-{
-    LOG_INFO("Hotspot Request");
+void handleHotspot(HTTPRequest *req, HTTPResponse *res) {
+  LOG_INFO("Hotspot Request");
 
-    /*
-        If we don't do a redirect, be sure to return a "Success" message
-        otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
-    */
+  /*
+      If we don't do a redirect, be sure to return a "Success" message
+      otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
+  */
 
-    // Status code is 200 OK by default.
-    // We want to deliver a simple HTML page, so we send a corresponding content type:
-    res->setHeader("Content-Type", "text/html");
-    res->setHeader("Access-Control-Allow-Origin", "*");
-    res->setHeader("Access-Control-Allow-Methods", "GET");
+  // Status code is 200 OK by default.
+  // We want to deliver a simple HTML page, so we send a corresponding content type:
+  res->setHeader("Content-Type", "text/html");
+  res->setHeader("Access-Control-Allow-Origin", "*");
+  res->setHeader("Access-Control-Allow-Methods", "GET");
 
-    // res->println("");
-    res->println("");
+  // res->println("");
+  res->println("");
 }
 
-void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res)
-{
-    res->setHeader("Content-Type", "text/html");
-    res->setHeader("Access-Control-Allow-Origin", "*");
-    res->setHeader("Access-Control-Allow-Methods", "GET");
+void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res) {
+  res->setHeader("Content-Type", "text/html");
+  res->setHeader("Access-Control-Allow-Origin", "*");
+  res->setHeader("Access-Control-Allow-Methods", "GET");
 
-    res->println("

Meshtastic

"); - res->println("Delete Content in /static/*"); + res->println("

Meshtastic

"); + res->println("Delete Content in /static/*"); - LOG_INFO("Delete files from /static/* : "); + LOG_INFO("Delete files from /static/* : "); - concurrency::LockGuard g(spiLock); - htmlDeleteDir("/static"); + concurrency::LockGuard g(spiLock); + htmlDeleteDir("/static"); - res->println("


Back to admin"); + res->println("


Back to admin"); } -void handleAdmin(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleAdmin(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - // res->println("Settings
"); - // res->println("Manage Web Content
"); - res->println("Device Report
"); + res->println("

Meshtastic

"); + // res->println("Settings
"); + // res->println("Manage Web Content
"); + res->println("Device Report
"); } -void handleAdminSettings(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleAdminSettings(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - res->println("This isn't done."); - res->println("
"); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println( - ""); - res->println("
Set?Settingcurrent valuenew value
WiFi SSIDfalse
WiFi Passwordfalse
Smart Position Updatefalse
"); - res->println(""); - res->println(""); - res->println(""); - res->println("


Back to admin"); + res->println("

Meshtastic

"); + res->println("This isn't done."); + res->println(""); + res->println("
"); + res->println(""); + res->println(""); + res->println(""); + res->println(""); + res->println("
Set?Settingcurrent valuenew value
WiFi SSIDfalse
WiFi Passwordfalse
Smart Position Updatefalse
"); + res->println(""); + res->println(""); + res->println(""); + res->println("


Back to admin"); } -void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "POST"); - res->println("

Meshtastic

"); - res->println( - "Settings Applied. "); +void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "POST"); + res->println("

Meshtastic

"); + res->println("Settings Applied. "); - res->println("Settings Applied. Please wait."); + res->println("Settings Applied. Please wait."); } -void handleFs(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleFs(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - res->println("Delete Web Content

Be patient!"); - res->println("


Back to admin"); + res->println("

Meshtastic

"); + res->println("Delete Web Content

Be patient!"); + res->println("


Back to admin"); } -void handleRestart(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleRestart(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - res->println("Restarting"); + res->println("

Meshtastic

"); + res->println("Restarting"); - LOG_DEBUG("Restarted on HTTP(s) Request"); - webServerThread->requestRestart = (millis() / 1000) + 5; + LOG_DEBUG("Restarted on HTTP(s) Request"); + webServerThread->requestRestart = (millis() / 1000) + 5; } -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "POST"); +void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "POST"); - ResourceParameters *params = req->getParams(); - std::string blink_target; + ResourceParameters *params = req->getParams(); + std::string blink_target; - if (!params->getQueryParameter("blink_target", blink_target)) { - // if no blink_target was supplied in the URL parameters of the - // POST request, then assume we should blink the LED - blink_target = "LED"; + if (!params->getQueryParameter("blink_target", blink_target)) { + // if no blink_target was supplied in the URL parameters of the + // POST request, then assume we should blink the LED + blink_target = "LED"; + } + + if (blink_target == "LED") { + uint8_t count = 10; + while (count > 0) { + ledBlink.set(true); + delay(50); + ledBlink.set(false); + delay(50); + count = count - 1; } - - if (blink_target == "LED") { - uint8_t count = 10; - while (count > 0) { - ledBlink.set(true); - delay(50); - ledBlink.set(false); - delay(50); - count = count - 1; - } - } else { + } else { #if HAS_SCREEN - if (screen) - screen->blink(); + if (screen) + screen->blink(); #endif - } + } - JSONObject jsonObjOuter; - jsonObjOuter["status"] = new JSONValue("ok"); - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("ok"); + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; } -void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - // res->setHeader("Content-Type", "text/html"); +void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) { + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + // res->setHeader("Content-Type", "text/html"); - int n = WiFi.scanNetworks(); + int n = WiFi.scanNetworks(); - // build list of network objects - JSONArray networkObjs; - if (n > 0) { - for (int i = 0; i < n; ++i) { - char ssidArray[50]; - String ssidString = String(WiFi.SSID(i)); - ssidString.replace("\"", "\\\""); - ssidString.toCharArray(ssidArray, 50); + // build list of network objects + JSONArray networkObjs; + if (n > 0) { + for (int i = 0; i < n; ++i) { + char ssidArray[50]; + String ssidString = String(WiFi.SSID(i)); + ssidString.replace("\"", "\\\""); + ssidString.toCharArray(ssidArray, 50); - if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) { - JSONObject thisNetwork; - thisNetwork["ssid"] = new JSONValue(ssidArray); - thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i))); - networkObjs.push_back(new JSONValue(thisNetwork)); - } - // Yield some cpu cycles to IP stack. - // This is important in case the list is large and it takes us time to return - // to the main loop. - yield(); - } + if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) { + JSONObject thisNetwork; + thisNetwork["ssid"] = new JSONValue(ssidArray); + thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i))); + networkObjs.push_back(new JSONValue(thisNetwork)); + } + // Yield some cpu cycles to IP stack. + // This is important in case the list is large and it takes us time to return + // to the main loop. + yield(); } + } - // build output structure - JSONObject jsonObjOuter; - jsonObjOuter["data"] = new JSONValue(networkObjs); - jsonObjOuter["status"] = new JSONValue("ok"); + // build output structure + JSONObject jsonObjOuter; + jsonObjOuter["data"] = new JSONValue(networkObjs); + jsonObjOuter["status"] = new JSONValue("ok"); - // serialize and write it to the stream - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; - // Clean up the networkObjs to prevent memory leak - for (auto *val : networkObjs) { - delete val; - } + // Clean up the networkObjs to prevent memory leak + for (auto *val : networkObjs) { + delete val; + } } #endif \ No newline at end of file diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h index 91cad3359..1b4c84a6a 100644 --- a/src/mesh/http/ContentHandler.h +++ b/src/mesh/http/ContentHandler.h @@ -22,16 +22,15 @@ void handleAdminSettings(HTTPRequest *req, HTTPResponse *res); void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res); // Interface to the PhoneAPI to access the protobufs with messages -class HttpAPI : public PhoneAPI -{ +class HttpAPI : public PhoneAPI { - public: - HttpAPI() { api_type = TYPE_HTTP; } +public: + HttpAPI() { api_type = TYPE_HTTP; } - private: - // Nothing here yet +private: + // Nothing here yet - protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this +protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; \ No newline at end of file diff --git a/src/mesh/http/ContentHelper.cpp b/src/mesh/http/ContentHelper.cpp index 8f283932b..bdd2eefb8 100644 --- a/src/mesh/http/ContentHelper.cpp +++ b/src/mesh/http/ContentHelper.cpp @@ -2,13 +2,12 @@ // #include // #include "main.h" -void replaceAll(std::string &str, const std::string &from, const std::string &to) -{ - if (from.empty()) - return; - size_t start_pos = 0; - while ((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' - } +void replaceAll(std::string &str, const std::string &from, const std::string &to) { + if (from.empty()) + return; + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } } diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 3a264fa5a..15be54f56 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -62,21 +62,19 @@ static HTTPServer *insecureServer; volatile bool isWebServerReady; volatile bool isCertReady; -static void handleWebResponse() -{ - if (isWifiAvailable()) { +static void handleWebResponse() { + if (isWifiAvailable()) { - if (isWebServerReady) { - if (secureServer) - secureServer->loop(); - insecureServer->loop(); - } + if (isWebServerReady) { + if (secureServer) + secureServer->loop(); + insecureServer->loop(); } + } } -static void taskCreateCert(void *parameter) -{ - prefs.begin("MeshtasticHTTPS", false); +static void taskCreateCert(void *parameter) { + prefs.begin("MeshtasticHTTPS", false); #if 0 // Delete the saved certs (used in debugging) @@ -86,165 +84,156 @@ static void taskCreateCert(void *parameter) prefs.remove("cert"); #endif - LOG_INFO("Checking if we have a saved SSL Certificate"); + LOG_INFO("Checking if we have a saved SSL Certificate"); - size_t pkLen = prefs.getBytesLength("PK"); - size_t certLen = prefs.getBytesLength("cert"); + size_t pkLen = prefs.getBytesLength("PK"); + size_t certLen = prefs.getBytesLength("cert"); - if (pkLen && certLen) { - LOG_INFO("Existing SSL Certificate found!"); + if (pkLen && certLen) { + LOG_INFO("Existing SSL Certificate found!"); - uint8_t *pkBuffer = new uint8_t[pkLen]; - prefs.getBytes("PK", pkBuffer, pkLen); + uint8_t *pkBuffer = new uint8_t[pkLen]; + prefs.getBytes("PK", pkBuffer, pkLen); - uint8_t *certBuffer = new uint8_t[certLen]; - prefs.getBytes("cert", certBuffer, certLen); + uint8_t *certBuffer = new uint8_t[certLen]; + prefs.getBytes("cert", certBuffer, certLen); - cert = new SSLCert(certBuffer, certLen, pkBuffer, pkLen); + cert = new SSLCert(certBuffer, certLen, pkBuffer, pkLen); - LOG_DEBUG("Retrieved Private Key: %d Bytes", cert->getPKLength()); - LOG_DEBUG("Retrieved Certificate: %d Bytes", cert->getCertLength()); + LOG_DEBUG("Retrieved Private Key: %d Bytes", cert->getPKLength()); + LOG_DEBUG("Retrieved Certificate: %d Bytes", cert->getCertLength()); + } else { + + LOG_INFO("Creating the certificate. This may take a while. Please wait"); + yield(); + cert = new SSLCert(); + yield(); + int createCertResult = createSelfSignedCert(*cert, KEYSIZE_2048, "CN=meshtastic.local,O=Meshtastic,C=US", "20190101000000", "20300101000000"); + yield(); + + if (createCertResult != 0) { + LOG_ERROR("Creating the certificate failed"); } else { + LOG_INFO("Creating the certificate was successful"); - LOG_INFO("Creating the certificate. This may take a while. Please wait"); - yield(); - cert = new SSLCert(); - yield(); - int createCertResult = createSelfSignedCert(*cert, KEYSIZE_2048, "CN=meshtastic.local,O=Meshtastic,C=US", - "20190101000000", "20300101000000"); - yield(); + LOG_DEBUG("Created Private Key: %d Bytes", cert->getPKLength()); - if (createCertResult != 0) { - LOG_ERROR("Creating the certificate failed"); - } else { - LOG_INFO("Creating the certificate was successful"); + LOG_DEBUG("Created Certificate: %d Bytes", cert->getCertLength()); - LOG_DEBUG("Created Private Key: %d Bytes", cert->getPKLength()); - - LOG_DEBUG("Created Certificate: %d Bytes", cert->getCertLength()); - - prefs.putBytes("PK", (uint8_t *)cert->getPKData(), cert->getPKLength()); - prefs.putBytes("cert", (uint8_t *)cert->getCertData(), cert->getCertLength()); - } + prefs.putBytes("PK", (uint8_t *)cert->getPKData(), cert->getPKLength()); + prefs.putBytes("cert", (uint8_t *)cert->getCertData(), cert->getCertLength()); } + } - isCertReady = true; + isCertReady = true; - // Must delete self, can't just fall out - vTaskDelete(NULL); + // Must delete self, can't just fall out + vTaskDelete(NULL); } -void createSSLCert() -{ - if (isWifiAvailable() && !isCertReady) { - bool runLoop = false; +void createSSLCert() { + if (isWifiAvailable() && !isCertReady) { + bool runLoop = false; - // Create a new process just to handle creating the cert. - // This is a workaround for Bug: https://github.com/fhessel/esp32_https_server/issues/48 - // jm@casler.org (Oct 2020) - xTaskCreate(taskCreateCert, /* Task function. */ - "createCert", /* String with name of task. */ - // 16384, /* Stack size in bytes. */ - 8192, /* Stack size in bytes. */ - NULL, /* Parameter passed as input of the task */ - 16, /* Priority of the task. */ - NULL); /* Task handle. */ + // Create a new process just to handle creating the cert. + // This is a workaround for Bug: https://github.com/fhessel/esp32_https_server/issues/48 + // jm@casler.org (Oct 2020) + xTaskCreate(taskCreateCert, /* Task function. */ + "createCert", /* String with name of task. */ + // 16384, /* Stack size in bytes. */ + 8192, /* Stack size in bytes. */ + NULL, /* Parameter passed as input of the task */ + 16, /* Priority of the task. */ + NULL); /* Task handle. */ - LOG_DEBUG("Waiting for SSL Cert to be generated"); - while (!isCertReady) { - if ((millis() / 500) % 2) { - if (runLoop) { - LOG_DEBUG("."); + LOG_DEBUG("Waiting for SSL Cert to be generated"); + while (!isCertReady) { + if ((millis() / 500) % 2) { + if (runLoop) { + LOG_DEBUG("."); - yield(); - esp_task_wdt_reset(); + yield(); + esp_task_wdt_reset(); #if HAS_SCREEN - if (millis() / 1000 >= 3) { - if (screen) - screen->setSSLFrames(); - } + if (millis() / 1000 >= 3) { + if (screen) + screen->setSSLFrames(); + } #endif - } - runLoop = false; - } else { - runLoop = true; - } } - LOG_INFO("SSL Cert Ready!"); + runLoop = false; + } else { + runLoop = true; + } } + LOG_INFO("SSL Cert Ready!"); + } } WebServerThread *webServerThread; -WebServerThread::WebServerThread() : concurrency::OSThread("WebServer") -{ - if (!config.network.wifi_enabled && !config.network.eth_enabled) { - disable(); - } - lastActivityTime = millis(); +WebServerThread::WebServerThread() : concurrency::OSThread("WebServer") { + if (!config.network.wifi_enabled && !config.network.eth_enabled) { + disable(); + } + lastActivityTime = millis(); } -void WebServerThread::markActivity() -{ - lastActivityTime = millis(); +void WebServerThread::markActivity() { lastActivityTime = millis(); } + +int32_t WebServerThread::getAdaptiveInterval() { + uint32_t currentTime = millis(); + uint32_t timeSinceActivity; + + if (currentTime >= lastActivityTime) { + timeSinceActivity = currentTime - lastActivityTime; + } else { + timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1; + } + + if (timeSinceActivity < ACTIVE_THRESHOLD_MS) { + return ACTIVE_INTERVAL_MS; + } else if (timeSinceActivity < MEDIUM_THRESHOLD_MS) { + return MEDIUM_INTERVAL_MS; + } else { + return IDLE_INTERVAL_MS; + } } -int32_t WebServerThread::getAdaptiveInterval() -{ - uint32_t currentTime = millis(); - uint32_t timeSinceActivity; +int32_t WebServerThread::runOnce() { + if (!config.network.wifi_enabled && !config.network.eth_enabled) { + disable(); + } - if (currentTime >= lastActivityTime) { - timeSinceActivity = currentTime - lastActivityTime; - } else { - timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1; - } + handleWebResponse(); - if (timeSinceActivity < ACTIVE_THRESHOLD_MS) { - return ACTIVE_INTERVAL_MS; - } else if (timeSinceActivity < MEDIUM_THRESHOLD_MS) { - return MEDIUM_INTERVAL_MS; - } else { - return IDLE_INTERVAL_MS; - } + if (requestRestart && (millis() / 1000) > requestRestart) { + ESP.restart(); + } + + return getAdaptiveInterval(); } -int32_t WebServerThread::runOnce() -{ - if (!config.network.wifi_enabled && !config.network.eth_enabled) { - disable(); - } +void initWebServer() { + LOG_DEBUG("Init Web Server"); - handleWebResponse(); + // We can now use the new certificate to setup our server as usual. + secureServer = new HTTPSServer(cert); + insecureServer = new HTTPServer(); - if (requestRestart && (millis() / 1000) > requestRestart) { - ESP.restart(); - } + registerHandlers(insecureServer, secureServer); - return getAdaptiveInterval(); -} - -void initWebServer() -{ - LOG_DEBUG("Init Web Server"); - - // We can now use the new certificate to setup our server as usual. - secureServer = new HTTPSServer(cert); - insecureServer = new HTTPServer(); - - registerHandlers(insecureServer, secureServer); - - if (secureServer) { - LOG_INFO("Start Secure Web Server"); - secureServer->start(); - } - LOG_INFO("Start Insecure Web Server"); - insecureServer->start(); - if (insecureServer->isRunning()) { - LOG_INFO("Web Servers Ready! :-) "); - isWebServerReady = true; - } else { - LOG_ERROR("Web Servers Failed! ;-( "); - } + if (secureServer) { + LOG_INFO("Start Secure Web Server"); + secureServer->start(); + } + LOG_INFO("Start Insecure Web Server"); + insecureServer->start(); + if (insecureServer->isRunning()) { + LOG_INFO("Web Servers Ready! :-) "); + isWebServerReady = true; + } else { + LOG_ERROR("Web Servers Failed! ;-( "); + } } #endif diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index e7a29a5a7..fa50a86db 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -8,19 +8,18 @@ void initWebServer(); void createSSLCert(); -class WebServerThread : private concurrency::OSThread -{ - private: - uint32_t lastActivityTime = 0; +class WebServerThread : private concurrency::OSThread { +private: + uint32_t lastActivityTime = 0; - public: - WebServerThread(); - uint32_t requestRestart = 0; - void markActivity(); +public: + WebServerThread(); + uint32_t requestRestart = 0; + void markActivity(); - protected: - virtual int32_t runOnce() override; - int32_t getAdaptiveInterval(); +protected: + virtual int32_t runOnce() override; + int32_t getAdaptiveInterval(); }; extern WebServerThread *webServerThread; diff --git a/src/mesh/mesh-pb-constants.cpp b/src/mesh/mesh-pb-constants.cpp index a8f4fd6d8..cb79de5c3 100644 --- a/src/mesh/mesh-pb-constants.cpp +++ b/src/mesh/mesh-pb-constants.cpp @@ -9,67 +9,62 @@ /// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic /// returns the encoded packet size -size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct) -{ - pb_ostream_t stream = pb_ostream_from_buffer(destbuf, destbufsize); - if (!pb_encode(&stream, fields, src_struct)) { - LOG_ERROR("Panic: can't encode protobuf reason='%s'", PB_GET_ERROR(&stream)); - return 0; - } else { - return stream.bytes_written; - } +size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct) { + pb_ostream_t stream = pb_ostream_from_buffer(destbuf, destbufsize); + if (!pb_encode(&stream, fields, src_struct)) { + LOG_ERROR("Panic: can't encode protobuf reason='%s'", PB_GET_ERROR(&stream)); + return 0; + } else { + return stream.bytes_written; + } } /// helper function for decoding a record as a protobuf, we will return false if the decoding failed -bool pb_decode_from_bytes(const uint8_t *srcbuf, size_t srcbufsize, const pb_msgdesc_t *fields, void *dest_struct) -{ - pb_istream_t stream = pb_istream_from_buffer(srcbuf, srcbufsize); - if (!pb_decode(&stream, fields, dest_struct)) { - LOG_ERROR("Can't decode protobuf reason='%s', pb_msgdesc %p", PB_GET_ERROR(&stream), fields); - return false; - } else { - return true; - } +bool pb_decode_from_bytes(const uint8_t *srcbuf, size_t srcbufsize, const pb_msgdesc_t *fields, void *dest_struct) { + pb_istream_t stream = pb_istream_from_buffer(srcbuf, srcbufsize); + if (!pb_decode(&stream, fields, dest_struct)) { + LOG_ERROR("Can't decode protobuf reason='%s', pb_msgdesc %p", PB_GET_ERROR(&stream), fields); + return false; + } else { + return true; + } } #ifdef FSCom /// Read from an Arduino File -bool readcb(pb_istream_t *stream, uint8_t *buf, size_t count) -{ - bool status = false; - File *file = (File *)stream->state; +bool readcb(pb_istream_t *stream, uint8_t *buf, size_t count) { + bool status = false; + File *file = (File *)stream->state; - if (buf == NULL) { - while (count-- && file->read() != EOF) - ; - return count == 0; - } + if (buf == NULL) { + while (count-- && file->read() != EOF) + ; + return count == 0; + } - status = (file->read(buf, count) == (int)count); + status = (file->read(buf, count) == (int)count); - if (file->available() == 0) - stream->bytes_left = 0; + if (file->available() == 0) + stream->bytes_left = 0; - return status; + return status; } /// Write to an arduino file -bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count) -{ - spiLock->lock(); - auto file = (Print *)stream->state; - // LOG_DEBUG("writing %d bytes to protobuf file", count); - bool status = file->write(buf, count) == count; - spiLock->unlock(); - return status; +bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count) { + spiLock->lock(); + auto file = (Print *)stream->state; + // LOG_DEBUG("writing %d bytes to protobuf file", count); + bool status = file->write(buf, count) == count; + spiLock->unlock(); + return status; } #endif -bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count) -{ - for (pb_size_t i = 0; i < count; i++) - if (array[i] == n) - return true; +bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count) { + for (pb_size_t i = 0; i < count; i++) + if (array[i] == n) + return true; - return false; + return false; } \ No newline at end of file diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index e4f65aa28..f07f6a228 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -11,9 +11,11 @@ // Tricky macro to let you find the sizeof a type member #define member_size(type, member) sizeof(((type *)0)->member) -/// max number of packets which can be waiting for delivery to android - note, this value comes from mesh.options protobuf -// FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in -// RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) +/// max number of packets which can be waiting for delivery to android - note, this value comes from mesh.options +/// protobuf +// FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big +// array in RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, +// receive_queue[0])) #ifndef MAX_RX_TOPHONE #if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) #define MAX_RX_TOPHONE 8 @@ -49,16 +51,15 @@ static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increas #define MAX_NUM_NODES 80 #elif defined(CONFIG_IDF_TARGET_ESP32S3) #include "Esp.h" -static inline int get_max_num_nodes() -{ - uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Convert Bytes to MB - if (flash_size >= 15) { - return 250; - } else if (flash_size >= 7) { - return 200; - } else { - return 100; - } +static inline int get_max_num_nodes() { + uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Convert Bytes to MB + if (flash_size >= 15) { + return 250; + } else if (flash_size >= 7) { + return 200; + } else { + return 100; + } } #define MAX_NUM_NODES get_max_num_nodes() #else diff --git a/src/mesh/raspihttp/PiWebServer.cpp b/src/mesh/raspihttp/PiWebServer.cpp index 3e9dbe8c2..e34e4c285 100644 --- a/src/mesh/raspihttp/PiWebServer.cpp +++ b/src/mesh/raspihttp/PiWebServer.cpp @@ -90,130 +90,126 @@ PiWebServerThread *piwebServerThread; /** * Return the filename extension */ -const char *get_filename_ext(const char *path) -{ - const char *dot = strrchr(path, '.'); - if (!dot || dot == path) - return "*"; - if (strchr(dot, '?') != NULL) { - //*strchr(dot, '?') = '\0'; - const char *empty = "\0"; - return empty; - } - return dot; +const char *get_filename_ext(const char *path) { + const char *dot = strrchr(path, '.'); + if (!dot || dot == path) + return "*"; + if (strchr(dot, '?') != NULL) { + //*strchr(dot, '?') = '\0'; + const char *empty = "\0"; + return empty; + } + return dot; } /** * Streaming callback function to ease sending large files */ -static ssize_t callback_static_file_stream(void *cls, uint64_t pos, char *buf, size_t max) -{ - (void)(pos); - if (cls != NULL) { - return fread(buf, 1, max, (FILE *)cls); - } else { - return U_STREAM_END; - } +static ssize_t callback_static_file_stream(void *cls, uint64_t pos, char *buf, size_t max) { + (void)(pos); + if (cls != NULL) { + return fread(buf, 1, max, (FILE *)cls); + } else { + return U_STREAM_END; + } } /** * Cleanup FILE* structure when streaming is complete */ -static void callback_static_file_stream_free(void *cls) -{ - if (cls != NULL) { - fclose((FILE *)cls); - } +static void callback_static_file_stream_free(void *cls) { + if (cls != NULL) { + fclose((FILE *)cls); + } } /** * static file callback endpoint that delivers the content for WebServer calls */ -int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data) -{ - size_t length; - FILE *f; - char *file_requested, *file_path, *url_dup_save, *real_path = NULL; - const char *content_type; +int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data) { + size_t length; + FILE *f; + char *file_requested, *file_path, *url_dup_save, *real_path = NULL; + const char *content_type; - /* - * Comment this if statement if you don't access static files url from root dir, like /app - */ - if (request->callback_position > 0) { - return U_CALLBACK_CONTINUE; - } else if (user_data != NULL && (configWeb.files_path != NULL)) { - file_requested = o_strdup(request->http_url); - url_dup_save = file_requested; + /* + * Comment this if statement if you don't access static files url from root dir, like /app + */ + if (request->callback_position > 0) { + return U_CALLBACK_CONTINUE; + } else if (user_data != NULL && (configWeb.files_path != NULL)) { + file_requested = o_strdup(request->http_url); + url_dup_save = file_requested; - while (file_requested[0] == '/') { - file_requested++; - } - file_requested += o_strlen(configWeb.url_prefix); - while (file_requested[0] == '/') { - file_requested++; - } - - if (strchr(file_requested, '#') != NULL) { - *strchr(file_requested, '#') = '\0'; - } - - if (strchr(file_requested, '?') != NULL) { - *strchr(file_requested, '?') = '\0'; - } - - if (file_requested == NULL || o_strlen(file_requested) == 0 || 0 == o_strcmp("/", file_requested)) { - o_free(url_dup_save); - url_dup_save = file_requested = o_strdup("index.html"); - } - - file_path = msprintf("%s/%s", configWeb.files_path, file_requested); - real_path = realpath(file_path, NULL); - if (0 == o_strncmp(configWeb.files_path, real_path, o_strlen(configWeb.files_path))) { - if (access(file_path, F_OK) != -1) { - f = fopen(file_path, "rb"); - if (f) { - fseek(f, 0, SEEK_END); - length = ftell(f); - fseek(f, 0, SEEK_SET); - - content_type = u_map_get_case(&configWeb.mime_types, get_filename_ext(file_requested)); - if (content_type == NULL) { - content_type = u_map_get(&configWeb.mime_types, "*"); - LOG_DEBUG("Static File Server - Unknown mime type for extension %s ", get_filename_ext(file_requested)); - } - u_map_put(response->map_header, "Content-Type", content_type); - u_map_copy_into(response->map_header, &configWeb.map_header); - - if (ulfius_set_stream_response(response, 200, callback_static_file_stream, callback_static_file_stream_free, - length, STATIC_FILE_CHUNK, f) != U_OK) { - LOG_DEBUG("callback_static_file - Error ulfius_set_stream_response"); - } - } - } else { - if (configWeb.redirect_on_404 == NULL) { - ulfius_set_string_body_response(response, 404, "File not found"); - } else { - ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); - response->status = 302; - } - } - } else { - if (configWeb.redirect_on_404 == NULL) { - ulfius_set_string_body_response(response, 404, "File not found"); - } else { - ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); - response->status = 302; - } - } - - o_free(file_path); - o_free(url_dup_save); - free(real_path); // realpath uses malloc - return U_CALLBACK_CONTINUE; - } else { - LOG_DEBUG("Static File Server - Error, user_data is NULL or inconsistent"); - return U_CALLBACK_ERROR; + while (file_requested[0] == '/') { + file_requested++; } + file_requested += o_strlen(configWeb.url_prefix); + while (file_requested[0] == '/') { + file_requested++; + } + + if (strchr(file_requested, '#') != NULL) { + *strchr(file_requested, '#') = '\0'; + } + + if (strchr(file_requested, '?') != NULL) { + *strchr(file_requested, '?') = '\0'; + } + + if (file_requested == NULL || o_strlen(file_requested) == 0 || 0 == o_strcmp("/", file_requested)) { + o_free(url_dup_save); + url_dup_save = file_requested = o_strdup("index.html"); + } + + file_path = msprintf("%s/%s", configWeb.files_path, file_requested); + real_path = realpath(file_path, NULL); + if (0 == o_strncmp(configWeb.files_path, real_path, o_strlen(configWeb.files_path))) { + if (access(file_path, F_OK) != -1) { + f = fopen(file_path, "rb"); + if (f) { + fseek(f, 0, SEEK_END); + length = ftell(f); + fseek(f, 0, SEEK_SET); + + content_type = u_map_get_case(&configWeb.mime_types, get_filename_ext(file_requested)); + if (content_type == NULL) { + content_type = u_map_get(&configWeb.mime_types, "*"); + LOG_DEBUG("Static File Server - Unknown mime type for extension %s ", get_filename_ext(file_requested)); + } + u_map_put(response->map_header, "Content-Type", content_type); + u_map_copy_into(response->map_header, &configWeb.map_header); + + if (ulfius_set_stream_response(response, 200, callback_static_file_stream, callback_static_file_stream_free, length, STATIC_FILE_CHUNK, + f) != U_OK) { + LOG_DEBUG("callback_static_file - Error ulfius_set_stream_response"); + } + } + } else { + if (configWeb.redirect_on_404 == NULL) { + ulfius_set_string_body_response(response, 404, "File not found"); + } else { + ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); + response->status = 302; + } + } + } else { + if (configWeb.redirect_on_404 == NULL) { + ulfius_set_string_body_response(response, 404, "File not found"); + } else { + ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); + response->status = 302; + } + } + + o_free(file_path); + o_free(url_dup_save); + free(real_path); // realpath uses malloc + return U_CALLBACK_CONTINUE; + } else { + LOG_DEBUG("Static File Server - Error, user_data is NULL or inconsistent"); + return U_CALLBACK_ERROR; + } } static void handleWebResponse() {} @@ -222,316 +218,305 @@ static void handleWebResponse() {} * Adapt the radioapi to the Webservice handleAPIv1ToRadio * Trigger : WebGui(SAVE)->WebServcice->phoneApi */ -int handleAPIv1ToRadio(const struct _u_request *req, struct _u_response *res, void *user_data) -{ - LOG_DEBUG("handleAPIv1ToRadio web -> radio "); +int handleAPIv1ToRadio(const struct _u_request *req, struct _u_response *res, void *user_data) { + LOG_DEBUG("handleAPIv1ToRadio web -> radio "); - ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Headers", "Content-Type"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "PUT, OPTIONS"); - ulfius_add_header_to_response(res, "X-Protobuf-Schema", - "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Headers", "Content-Type"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "PUT, OPTIONS"); + ulfius_add_header_to_response(res, "X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (strcmp(req->http_verb, "OPTIONS") == 0) { - ulfius_set_response_properties(res, U_OPT_STATUS, 204); - return U_CALLBACK_COMPLETE; - } - - byte buffer[MAX_TO_FROM_RADIO_SIZE]; - size_t s = req->binary_body_length; - - memcpy(buffer, req->binary_body, MAX_TO_FROM_RADIO_SIZE); - - // FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread - - portduinoVFS->mountpoint(configWeb.rootPath); - - LOG_DEBUG("Received %d bytes from PUT request", s); - static_cast(user_data)->handleToRadio(buffer, s); - LOG_DEBUG("end web->radio "); + if (strcmp(req->http_verb, "OPTIONS") == 0) { + ulfius_set_response_properties(res, U_OPT_STATUS, 204); return U_CALLBACK_COMPLETE; + } + + byte buffer[MAX_TO_FROM_RADIO_SIZE]; + size_t s = req->binary_body_length; + + memcpy(buffer, req->binary_body, MAX_TO_FROM_RADIO_SIZE); + + // FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread + + portduinoVFS->mountpoint(configWeb.rootPath); + + LOG_DEBUG("Received %d bytes from PUT request", s); + static_cast(user_data)->handleToRadio(buffer, s); + LOG_DEBUG("end web->radio "); + return U_CALLBACK_COMPLETE; } /* * Adapt the radioapi to the Webservice handleAPIv1FromRadio * Trigger : WebGui(POLL)->handleAPIv1FromRadio->phoneapi->Meshtastic(Radio) events */ -int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data) -{ +int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data) { - // LOG_DEBUG("handleAPIv1FromRadio radio -> web"); - std::string valueAll; + // LOG_DEBUG("handleAPIv1FromRadio radio -> web"); + std::string valueAll; - // Status code is 200 OK by default. - ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "GET"); - ulfius_add_header_to_response(res, "X-Protobuf-Schema", - "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + // Status code is 200 OK by default. + ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "GET"); + ulfius_add_header_to_response(res, "X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (strcmp(req->http_verb, "OPTIONS") == 0) { - ulfius_set_response_properties(res, U_OPT_STATUS, 204); - return U_CALLBACK_COMPLETE; - } - - uint8_t txBuf[MAX_STREAM_BUF_SIZE]; - uint32_t len = 1; - - if (valueAll == "true") { - while (len) { - len = static_cast(user_data)->getFromRadio(txBuf); - ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len); - const char *tmpa = (const char *)txBuf; - ulfius_set_string_body_response(res, 200, tmpa); - // LOG_DEBUG("\n----webAPI response all:----"); - // LOG_DEBUG(tmpa); - // LOG_DEBUG(""); - } - // Otherwise, just return one protobuf - } else { - len = static_cast(user_data)->getFromRadio(txBuf); - const char *tmpa = (const char *)txBuf; - ulfius_set_binary_body_response(res, 200, tmpa, len); - // LOG_DEBUG("\n----webAPI response:"); - // LOG_DEBUG(tmpa); - // LOG_DEBUG(""); - } - - // LOG_DEBUG("end radio->web", len); + if (strcmp(req->http_verb, "OPTIONS") == 0) { + ulfius_set_response_properties(res, U_OPT_STATUS, 204); return U_CALLBACK_COMPLETE; + } + + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; + + if (valueAll == "true") { + while (len) { + len = static_cast(user_data)->getFromRadio(txBuf); + ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len); + const char *tmpa = (const char *)txBuf; + ulfius_set_string_body_response(res, 200, tmpa); + // LOG_DEBUG("\n----webAPI response all:----"); + // LOG_DEBUG(tmpa); + // LOG_DEBUG(""); + } + // Otherwise, just return one protobuf + } else { + len = static_cast(user_data)->getFromRadio(txBuf); + const char *tmpa = (const char *)txBuf; + ulfius_set_binary_body_response(res, 200, tmpa, len); + // LOG_DEBUG("\n----webAPI response:"); + // LOG_DEBUG(tmpa); + // LOG_DEBUG(""); + } + + // LOG_DEBUG("end radio->web", len); + return U_CALLBACK_COMPLETE; } /* OpenSSL RSA Key Gen */ -int generate_rsa_key(EVP_PKEY **pkey) -{ - EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); - if (!pkey_ctx) - return -1; - if (EVP_PKEY_keygen_init(pkey_ctx) <= 0) - return -1; - if (EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0) - return -1; - if (EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) - return -1; - EVP_PKEY_CTX_free(pkey_ctx); - return 0; // SUCCESS +int generate_rsa_key(EVP_PKEY **pkey) { + EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (!pkey_ctx) + return -1; + if (EVP_PKEY_keygen_init(pkey_ctx) <= 0) + return -1; + if (EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0) + return -1; + if (EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) + return -1; + EVP_PKEY_CTX_free(pkey_ctx); + return 0; // SUCCESS } -int generate_self_signed_x509(EVP_PKEY *pkey, X509 **x509) -{ - *x509 = X509_new(); - if (!*x509) - return -1; - if (X509_set_version(*x509, 2) != 1) - return -1; - ASN1_INTEGER_set(X509_get_serialNumber(*x509), 1); - X509_gmtime_adj(X509_get_notBefore(*x509), 0); - X509_gmtime_adj(X509_get_notAfter(*x509), 31536000L); // 1 YEAR ACCESS +int generate_self_signed_x509(EVP_PKEY *pkey, X509 **x509) { + *x509 = X509_new(); + if (!*x509) + return -1; + if (X509_set_version(*x509, 2) != 1) + return -1; + ASN1_INTEGER_set(X509_get_serialNumber(*x509), 1); + X509_gmtime_adj(X509_get_notBefore(*x509), 0); + X509_gmtime_adj(X509_get_notAfter(*x509), 31536000L); // 1 YEAR ACCESS - X509_set_pubkey(*x509, pkey); + X509_set_pubkey(*x509, pkey); - // SET Subject Name - X509_NAME *name = X509_get_subject_name(*x509); - X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"DE", -1, -1, 0); - X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Meshtastic", -1, -1, 0); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"meshtastic.local", -1, -1, 0); - // Selfsigned, Issuer = Subject - X509_set_issuer_name(*x509, name); + // SET Subject Name + X509_NAME *name = X509_get_subject_name(*x509); + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"DE", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Meshtastic", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"meshtastic.local", -1, -1, 0); + // Selfsigned, Issuer = Subject + X509_set_issuer_name(*x509, name); - // Certificate signed with our privte key - if (X509_sign(*x509, pkey, EVP_sha256()) <= 0) - return -1; + // Certificate signed with our privte key + if (X509_sign(*x509, pkey, EVP_sha256()) <= 0) + return -1; - return 0; + return 0; } -char *read_file_into_string(const char *filename) -{ - FILE *file = fopen(filename, "rb"); - if (file == NULL) { - LOG_ERROR("Error reading File : %s ", filename); - return NULL; - } +char *read_file_into_string(const char *filename) { + FILE *file = fopen(filename, "rb"); + if (file == NULL) { + LOG_ERROR("Error reading File : %s ", filename); + return NULL; + } - // Size of file - fseek(file, 0, SEEK_END); - long filesize = ftell(file); - rewind(file); + // Size of file + fseek(file, 0, SEEK_END); + long filesize = ftell(file); + rewind(file); - // reserve mem for file + 1 byte - char *buffer = (char *)malloc(filesize + 1); - if (buffer == NULL) { - LOG_ERROR("Malloc of mem failed for file : %s ", filename); - fclose(file); - return NULL; - } - - // read content - size_t readSize = fread(buffer, 1, filesize, file); - if (readSize != filesize) { - LOG_ERROR("Error reading file into buffer"); - free(buffer); - fclose(file); - return NULL; - } - - // add terminator sign at the end - buffer[filesize] = '\0'; + // reserve mem for file + 1 byte + char *buffer = (char *)malloc(filesize + 1); + if (buffer == NULL) { + LOG_ERROR("Malloc of mem failed for file : %s ", filename); fclose(file); - return buffer; // return pointer + return NULL; + } + + // read content + size_t readSize = fread(buffer, 1, filesize, file); + if (readSize != filesize) { + LOG_ERROR("Error reading file into buffer"); + free(buffer); + fclose(file); + return NULL; + } + + // add terminator sign at the end + buffer[filesize] = '\0'; + fclose(file); + return buffer; // return pointer } -int PiWebServerThread::CheckSSLandLoad() -{ - // read certificate - cert_pem = read_file_into_string(CERT_PATH); - if (cert_pem == NULL) { - LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing"); - return 1; - } - // read private key - key_pem = read_file_into_string(KEY_PATH); - if (key_pem == NULL) { - LOG_ERROR("ERROR file private_key can't be loaded or is missing"); - return 2; - } +int PiWebServerThread::CheckSSLandLoad() { + // read certificate + cert_pem = read_file_into_string(CERT_PATH); + if (cert_pem == NULL) { + LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing"); + return 1; + } + // read private key + key_pem = read_file_into_string(KEY_PATH); + if (key_pem == NULL) { + LOG_ERROR("ERROR file private_key can't be loaded or is missing"); + return 2; + } - return 0; + return 0; } -int PiWebServerThread::CreateSSLCertificate() -{ +int PiWebServerThread::CreateSSLCertificate() { - EVP_PKEY *pkey = NULL; - X509 *x509 = NULL; + EVP_PKEY *pkey = NULL; + X509 *x509 = NULL; - if (generate_rsa_key(&pkey) != 0) { - LOG_ERROR("Error generating RSA-Key"); - return 1; - } + if (generate_rsa_key(&pkey) != 0) { + LOG_ERROR("Error generating RSA-Key"); + return 1; + } - if (generate_self_signed_x509(pkey, &x509) != 0) { - LOG_ERROR("Error generating X509-Cert"); - return 2; - } + if (generate_self_signed_x509(pkey, &x509) != 0) { + LOG_ERROR("Error generating X509-Cert"); + return 2; + } - // Open file to write private key file - FILE *pkey_file = fopen(KEY_PATH, "wb"); - if (!pkey_file) { - LOG_ERROR("Error opening private key file"); - return 3; - } - // write private key file - PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL); - fclose(pkey_file); + // Open file to write private key file + FILE *pkey_file = fopen(KEY_PATH, "wb"); + if (!pkey_file) { + LOG_ERROR("Error opening private key file"); + return 3; + } + // write private key file + PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL); + fclose(pkey_file); - // open Certificate file - FILE *x509_file = fopen(CERT_PATH, "wb"); - if (!x509_file) { - LOG_ERROR("Error opening cert"); - return 4; - } - // write certificate - PEM_write_X509(x509_file, x509); - fclose(x509_file); + // open Certificate file + FILE *x509_file = fopen(CERT_PATH, "wb"); + if (!x509_file) { + LOG_ERROR("Error opening cert"); + return 4; + } + // write certificate + PEM_write_X509(x509_file, x509); + fclose(x509_file); - EVP_PKEY_free(pkey); - LOG_INFO("Create SSL Key %s successful", KEY_PATH); - X509_free(x509); - LOG_INFO("Create SSL Cert %s successful", CERT_PATH); - return 0; + EVP_PKEY_free(pkey); + LOG_INFO("Create SSL Key %s successful", KEY_PATH); + X509_free(x509); + LOG_INFO("Create SSL Cert %s successful", CERT_PATH); + return 0; } void initWebServer() {} -PiWebServerThread::PiWebServerThread() -{ - int ret, retssl, webservport; +PiWebServerThread::PiWebServerThread() { + int ret, retssl, webservport; + if (CheckSSLandLoad() != 0) { + CreateSSLCertificate(); if (CheckSSLandLoad() != 0) { - CreateSSLCertificate(); - if (CheckSSLandLoad() != 0) { - LOG_ERROR("Major Error Gen & Read SSL Certificate"); - } + LOG_ERROR("Major Error Gen & Read SSL Certificate"); } + } - if (portduino_config.webserverport != 0) { - webservport = portduino_config.webserverport; - LOG_INFO("Use webserver port from yaml config %i ", webservport); + if (portduino_config.webserverport != 0) { + webservport = portduino_config.webserverport; + LOG_INFO("Use webserver port from yaml config %i ", webservport); + } else { + LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443"); + webservport = 9443; + } + + // Web Content Service Instance + if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) { + LOG_ERROR("Webserver couldn't be started, abort execution"); + } else { + + LOG_INFO("Webserver started"); + u_map_init(&configWeb.mime_types); + u_map_put(&configWeb.mime_types, "*", "application/octet-stream"); + u_map_put(&configWeb.mime_types, ".html", "text/html"); + u_map_put(&configWeb.mime_types, ".htm", "text/html"); + u_map_put(&configWeb.mime_types, ".tsx", "application/javascript"); + u_map_put(&configWeb.mime_types, ".ts", "application/javascript"); + u_map_put(&configWeb.mime_types, ".css", "text/css"); + u_map_put(&configWeb.mime_types, ".js", "application/javascript"); + u_map_put(&configWeb.mime_types, ".json", "application/json"); + u_map_put(&configWeb.mime_types, ".png", "image/png"); + u_map_put(&configWeb.mime_types, ".gif", "image/gif"); + u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg"); + u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg"); + u_map_put(&configWeb.mime_types, ".ttf", "font/ttf"); + u_map_put(&configWeb.mime_types, ".woff", "font/woff"); + u_map_put(&configWeb.mime_types, ".ico", "image/x-icon"); + u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml"); + + webrootpath = portduino_config.webserver_root_path; + + configWeb.files_path = (char *)webrootpath.c_str(); + configWeb.url_prefix = ""; + configWeb.rootPath = strdup(portduinoVFS->mountpoint()); + + u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*"); + // Maximum body size sent by the client is 1 Kb + instanceWeb.max_post_body_size = 1024; + ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); + ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); + ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); + ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); + + // Add callback function to all endpoints for the Web Server + ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb); + + // thats for serving without SSL + // retssl = ulfius_start_framework(&instanceWeb); + + // thats for serving with SSL + retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem); + + if (retssl == U_OK) { + LOG_INFO("Web Server framework started on port: %i ", webservport); + LOG_INFO("Web Server root %s", (char *)webrootpath.c_str()); } else { - LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443"); - webservport = 9443; - } - - // Web Content Service Instance - if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) { - LOG_ERROR("Webserver couldn't be started, abort execution"); - } else { - - LOG_INFO("Webserver started"); - u_map_init(&configWeb.mime_types); - u_map_put(&configWeb.mime_types, "*", "application/octet-stream"); - u_map_put(&configWeb.mime_types, ".html", "text/html"); - u_map_put(&configWeb.mime_types, ".htm", "text/html"); - u_map_put(&configWeb.mime_types, ".tsx", "application/javascript"); - u_map_put(&configWeb.mime_types, ".ts", "application/javascript"); - u_map_put(&configWeb.mime_types, ".css", "text/css"); - u_map_put(&configWeb.mime_types, ".js", "application/javascript"); - u_map_put(&configWeb.mime_types, ".json", "application/json"); - u_map_put(&configWeb.mime_types, ".png", "image/png"); - u_map_put(&configWeb.mime_types, ".gif", "image/gif"); - u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg"); - u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg"); - u_map_put(&configWeb.mime_types, ".ttf", "font/ttf"); - u_map_put(&configWeb.mime_types, ".woff", "font/woff"); - u_map_put(&configWeb.mime_types, ".ico", "image/x-icon"); - u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml"); - - webrootpath = portduino_config.webserver_root_path; - - configWeb.files_path = (char *)webrootpath.c_str(); - configWeb.url_prefix = ""; - configWeb.rootPath = strdup(portduinoVFS->mountpoint()); - - u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*"); - // Maximum body size sent by the client is 1 Kb - instanceWeb.max_post_body_size = 1024; - ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); - ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); - ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); - ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); - - // Add callback function to all endpoints for the Web Server - ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb); - - // thats for serving without SSL - // retssl = ulfius_start_framework(&instanceWeb); - - // thats for serving with SSL - retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem); - - if (retssl == U_OK) { - LOG_INFO("Web Server framework started on port: %i ", webservport); - LOG_INFO("Web Server root %s", (char *)webrootpath.c_str()); - } else { - LOG_ERROR("Error starting Web Server framework, error number: %d", retssl); - } + LOG_ERROR("Error starting Web Server framework, error number: %d", retssl); } + } } -PiWebServerThread::~PiWebServerThread() -{ - u_map_clean(&configWeb.mime_types); +PiWebServerThread::~PiWebServerThread() { + u_map_clean(&configWeb.mime_types); - ulfius_stop_framework(&instanceWeb); - ulfius_clean_instance(&instanceWeb); - free(configWeb.rootPath); - free(key_pem); - free(cert_pem); - LOG_INFO("End framework"); + ulfius_stop_framework(&instanceWeb); + ulfius_clean_instance(&instanceWeb); + free(configWeb.rootPath); + free(key_pem); + free(cert_pem); + LOG_INFO("End framework"); } #endif diff --git a/src/mesh/raspihttp/PiWebServer.h b/src/mesh/raspihttp/PiWebServer.h index 5a4adedaa..039d27319 100644 --- a/src/mesh/raspihttp/PiWebServer.h +++ b/src/mesh/raspihttp/PiWebServer.h @@ -15,44 +15,42 @@ int callback_static_file(const struct _u_request *request, struct _u_response *r const char *get_filename_ext(const char *path); struct _file_config { - char *files_path; - char *url_prefix; - struct _u_map mime_types; - struct _u_map map_header; - char *redirect_on_404; - char *rootPath; + char *files_path; + char *url_prefix; + struct _u_map mime_types; + struct _u_map map_header; + char *redirect_on_404; + char *rootPath; }; -class HttpAPI : public PhoneAPI -{ +class HttpAPI : public PhoneAPI { - public: - HttpAPI() { api_type = TYPE_HTTP; } +public: + HttpAPI() { api_type = TYPE_HTTP; } - private: - // Nothing here yet +private: + // Nothing here yet - protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this +protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; -class PiWebServerThread -{ - private: - char *key_pem = NULL; - char *cert_pem = NULL; - // struct _u_map mime_types; - std::string webrootpath; - HttpAPI webAPI; +class PiWebServerThread { +private: + char *key_pem = NULL; + char *cert_pem = NULL; + // struct _u_map mime_types; + std::string webrootpath; + HttpAPI webAPI; - public: - PiWebServerThread(); - ~PiWebServerThread(); - int CreateSSLCertificate(); - int CheckSSLandLoad(); - uint32_t requestRestart = 0; - struct _u_instance instanceWeb; +public: + PiWebServerThread(); + ~PiWebServerThread(); + int CreateSSLCertificate(); + int CheckSSLandLoad(); + uint32_t requestRestart = 0; + struct _u_instance instanceWeb; }; extern PiWebServerThread *piwebServerThread; diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 2df8686a3..ab3ca48a9 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -19,78 +19,73 @@ #define UDP_MULTICAST_DEFAUL_PORT 4403 // Default port for UDP multicast is same as TCP api server -class UdpMulticastHandler final -{ - public: - UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); } +class UdpMulticastHandler final { +public: + UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); } - void start() - { - if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { + void start() { + if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) - LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], - UDP_MULTICAST_DEFAUL_PORT); + LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], UDP_MULTICAST_DEFAUL_PORT); #else - LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); + LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); #endif - udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); - } else { - LOG_DEBUG("Failed to listen on UDP"); - } + udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); + } else { + LOG_DEBUG("Failed to listen on UDP"); } + } - void onReceive(AsyncUDPPacket packet) - { - size_t packetLength = packet.length(); + void onReceive(AsyncUDPPacket packet) { + size_t packetLength = packet.length(); #if defined(ARCH_NRF52) - IPAddress ip = packet.remoteIP(); - LOG_DEBUG("UDP broadcast from: %u.%u.%u.%u, len=%u", ip[0], ip[1], ip[2], ip[3], packetLength); + IPAddress ip = packet.remoteIP(); + LOG_DEBUG("UDP broadcast from: %u.%u.%u.%u, len=%u", ip[0], ip[1], ip[2], ip[3], packetLength); #elif !defined(ARCH_PORTDUINO) - // FIXME(PORTDUINO): arduino lacks IPAddress::toString() - LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); + // FIXME(PORTDUINO): arduino lacks IPAddress::toString() + LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif - meshtastic_MeshPacket mp; - LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); - bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); - if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { - mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; - mp.pki_encrypted = false; - mp.public_key.size = 0; - memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); - UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); - // Unset received SNR/RSSI - p->rx_snr = 0; - p->rx_rssi = 0; - router->enqueueReceivedMessage(p.release()); - } + meshtastic_MeshPacket mp; + LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); + bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); + if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; + mp.pki_encrypted = false; + mp.public_key.size = 0; + memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); + UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); + // Unset received SNR/RSSI + p->rx_snr = 0; + p->rx_rssi = 0; + router->enqueueReceivedMessage(p.release()); } + } - bool onSend(const meshtastic_MeshPacket *mp) - { - if (!mp || !udp) { - return false; - } + bool onSend(const meshtastic_MeshPacket *mp) { + if (!mp || !udp) { + return false; + } #if defined(ARCH_NRF52) - if (!isEthernetAvailable()) { - return false; - } -#elif !defined(ARCH_PORTDUINO) - if (WiFi.status() != WL_CONNECTED) { - return false; - } -#endif - if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { - LOG_ERROR("Attempt to send UDP sourced packet over UDP"); - } - LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); - uint8_t buffer[meshtastic_MeshPacket_size]; - size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); - udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); - return true; + if (!isEthernetAvailable()) { + return false; } +#elif !defined(ARCH_PORTDUINO) + if (WiFi.status() != WL_CONNECTED) { + return false; + } +#endif + if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { + LOG_ERROR("Attempt to send UDP sourced packet over UDP"); + } + LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); + uint8_t buffer[meshtastic_MeshPacket_size]; + size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); + udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); + return true; + } - private: - IPAddress udpIpAddress; - AsyncUDP udp; +private: + IPAddress udpIpAddress; + AsyncUDP udp; }; #endif // HAS_UDP_MULTICAST \ No newline at end of file diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 45944872e..b88ba53f4 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -64,273 +64,266 @@ Periodic *wifiReconnect; #ifdef USE_WS5500 // Startup Ethernet -bool initEthernet() -{ - if ((config.network.eth_enabled) && (ETH.begin(ETH_PHY_W5500, 1, ETH_CS_PIN, ETH_INT_PIN, ETH_RST_PIN, SPI3_HOST, - ETH_SCLK_PIN, ETH_MISO_PIN, ETH_MOSI_PIN))) { - WiFi.onEvent(WiFiEvent); +bool initEthernet() { + if ((config.network.eth_enabled) && + (ETH.begin(ETH_PHY_W5500, 1, ETH_CS_PIN, ETH_INT_PIN, ETH_RST_PIN, SPI3_HOST, ETH_SCLK_PIN, ETH_MISO_PIN, ETH_MOSI_PIN))) { + WiFi.onEvent(WiFiEvent); #if !MESHTASTIC_EXCLUDE_WEBSERVER - createSSLCert(); // For WebServer + createSSLCert(); // For WebServer #endif - return true; - } + return true; + } - return false; + return false; } #endif -static void onNetworkConnected() -{ - if (!APStartupComplete) { - // Start web server - LOG_INFO("Start network services"); +static void onNetworkConnected() { + if (!APStartupComplete) { + // Start web server + LOG_INFO("Start network services"); - // start mdns - if (!MDNS.begin("Meshtastic")) { - LOG_ERROR("Error setting up mDNS responder!"); - } else { - LOG_INFO("mDNS Host: Meshtastic.local"); - MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT); + // start mdns + if (!MDNS.begin("Meshtastic")) { + LOG_ERROR("Error setting up mDNS responder!"); + } else { + LOG_INFO("mDNS Host: Meshtastic.local"); + MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT); // ESPmDNS (ESP32) and SimpleMDNS (RP2040) have slightly different APIs for adding TXT records #ifdef ARCH_ESP32 - MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name)); - MDNS.addServiceTxt("meshtastic", "tcp", "id", String(nodeDB->getNodeId().c_str())); - MDNS.addServiceTxt("meshtastic", "tcp", "pio_env", optstr(APP_ENV)); - // ESP32 prints obtained IP address in WiFiEvent + MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name)); + MDNS.addServiceTxt("meshtastic", "tcp", "id", String(nodeDB->getNodeId().c_str())); + MDNS.addServiceTxt("meshtastic", "tcp", "pio_env", optstr(APP_ENV)); + // ESP32 prints obtained IP address in WiFiEvent #elif defined(ARCH_RP2040) - MDNS.addServiceTxt("meshtastic", "shortname", owner.short_name); - MDNS.addServiceTxt("meshtastic", "id", nodeDB->getNodeId().c_str()); - MDNS.addServiceTxt("meshtastic", "pio_env", optstr(APP_ENV)); - LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); + MDNS.addServiceTxt("meshtastic", "shortname", owner.short_name); + MDNS.addServiceTxt("meshtastic", "id", nodeDB->getNodeId().c_str()); + MDNS.addServiceTxt("meshtastic", "pio_env", optstr(APP_ENV)); + LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); #endif - } + } #ifndef DISABLE_NTP - LOG_INFO("Start NTP time client"); - timeClient.begin(); - timeClient.setUpdateInterval(60 * 60); // Update once an hour + LOG_INFO("Start NTP time client"); + timeClient.begin(); + timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif - if (config.network.rsyslog_server[0]) { - LOG_INFO("Start Syslog client"); - // Defaults - int serverPort = 514; - const char *serverAddr = config.network.rsyslog_server; - String server = String(serverAddr); - int delimIndex = server.indexOf(':'); - if (delimIndex > 0) { - String port = server.substring(delimIndex + 1, server.length()); - server[delimIndex] = 0; - serverPort = port.toInt(); - serverAddr = server.c_str(); - } - syslog.server(serverAddr, serverPort); - syslog.deviceHostname(getDeviceName()); - syslog.appName("Meshtastic"); - syslog.defaultPriority(LOGLEVEL_USER); - syslog.enable(); - } + if (config.network.rsyslog_server[0]) { + LOG_INFO("Start Syslog client"); + // Defaults + int serverPort = 514; + const char *serverAddr = config.network.rsyslog_server; + String server = String(serverAddr); + int delimIndex = server.indexOf(':'); + if (delimIndex > 0) { + String port = server.substring(delimIndex + 1, server.length()); + server[delimIndex] = 0; + serverPort = port.toInt(); + serverAddr = server.c_str(); + } + syslog.server(serverAddr, serverPort); + syslog.deviceHostname(getDeviceName()); + syslog.appName("Meshtastic"); + syslog.defaultPriority(LOGLEVEL_USER); + syslog.enable(); + } #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - initWebServer(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initWebServer(); + } #endif #if !MESHTASTIC_EXCLUDE_SOCKETAPI - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - initApiServer(); - } -#endif - APStartupComplete = true; + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); } +#endif + APStartupComplete = true; + } #if HAS_UDP_MULTICAST - if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->start(); - } + if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->start(); + } #endif } -static int32_t reconnectWiFi() -{ - const char *wifiName = config.network.wifi_ssid; - const char *wifiPsw = config.network.wifi_psk; +static int32_t reconnectWiFi() { + const char *wifiName = config.network.wifi_ssid; + const char *wifiPsw = config.network.wifi_psk; - if (config.network.wifi_enabled && needReconnect) { + if (config.network.wifi_enabled && needReconnect) { - if (!*wifiPsw) // Treat empty password as no password - wifiPsw = NULL; + if (!*wifiPsw) // Treat empty password as no password + wifiPsw = NULL; - needReconnect = false; - isReconnecting = true; + needReconnect = false; + isReconnecting = true; - // Make sure we clear old connection credentials + // Make sure we clear old connection credentials #ifdef ARCH_ESP32 - WiFi.disconnect(false, true); + WiFi.disconnect(false, true); #elif defined(ARCH_RP2040) - WiFi.disconnect(false); + WiFi.disconnect(false); #endif - LOG_INFO("Reconnecting to WiFi access point %s", wifiName); + LOG_INFO("Reconnecting to WiFi access point %s", wifiName); - // Start the non-blocking wait for 5 seconds - wifiReconnectStartMillis = millis(); - wifiReconnectPending = true; - // Do not attempt to connect yet, wait for the next invocation - return 5000; // Schedule next check soon - } + // Start the non-blocking wait for 5 seconds + wifiReconnectStartMillis = millis(); + wifiReconnectPending = true; + // Do not attempt to connect yet, wait for the next invocation + return 5000; // Schedule next check soon + } - // Check if we are ready to proceed with the WiFi connection after the 5s wait - if (wifiReconnectPending) { - if (millis() - wifiReconnectStartMillis >= 5000) { - if (!WiFi.isConnected()) { + // Check if we are ready to proceed with the WiFi connection after the 5s wait + if (wifiReconnectPending) { + if (millis() - wifiReconnectStartMillis >= 5000) { + if (!WiFi.isConnected()) { #ifdef CONFIG_IDF_TARGET_ESP32C3 - WiFi.mode(WIFI_MODE_NULL); - WiFi.useStaticBuffers(true); - WiFi.mode(WIFI_STA); + WiFi.mode(WIFI_MODE_NULL); + WiFi.useStaticBuffers(true); + WiFi.mode(WIFI_STA); #endif - WiFi.begin(wifiName, wifiPsw); - } - isReconnecting = false; - wifiReconnectPending = false; - } else { - // Still waiting for 5s to elapse - return 100; // Check again soon - } + WiFi.begin(wifiName, wifiPsw); + } + isReconnecting = false; + wifiReconnectPending = false; + } else { + // Still waiting for 5s to elapse + return 100; // Check again soon } + } #ifndef DISABLE_NTP - if (WiFi.isConnected() && (!Throttle::isWithinTimespanMs(lastrun_ntp, 43200000) || (lastrun_ntp == 0))) { // every 12 hours - LOG_DEBUG("Update NTP time from %s", config.network.ntp_server); - if (timeClient.update()) { - LOG_DEBUG("NTP Request Success - Setting RTCQualityNTP if needed"); + if (WiFi.isConnected() && (!Throttle::isWithinTimespanMs(lastrun_ntp, 43200000) || (lastrun_ntp == 0))) { // every 12 hours + LOG_DEBUG("Update NTP time from %s", config.network.ntp_server); + if (timeClient.update()) { + LOG_DEBUG("NTP Request Success - Setting RTCQualityNTP if needed"); - struct timeval tv; - tv.tv_sec = timeClient.getEpochTime(); - tv.tv_usec = 0; + struct timeval tv; + tv.tv_sec = timeClient.getEpochTime(); + tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); - lastrun_ntp = millis(); - } else { - LOG_DEBUG("NTP Update failed"); - } - } -#endif - - if (config.network.wifi_enabled && !WiFi.isConnected()) { -#ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) - needReconnect = APStartupComplete; -#endif - return 1000; // check once per second + perhapsSetRTC(RTCQualityNTP, &tv); + lastrun_ntp = millis(); } else { -#ifdef ARCH_RP2040 - onNetworkConnected(); // will only do anything once -#endif - return 300000; // every 5 minutes + LOG_DEBUG("NTP Update failed"); } + } +#endif + + if (config.network.wifi_enabled && !WiFi.isConnected()) { +#ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) + needReconnect = APStartupComplete; +#endif + return 1000; // check once per second + } else { +#ifdef ARCH_RP2040 + onNetworkConnected(); // will only do anything once +#endif + return 300000; // every 5 minutes + } } -bool isWifiAvailable() -{ +bool isWifiAvailable() { - if (config.network.wifi_enabled && (config.network.wifi_ssid[0])) { - return true; + if (config.network.wifi_enabled && (config.network.wifi_ssid[0])) { + return true; #ifdef USE_WS5500 - } else if (config.network.eth_enabled) { - return true; + } else if (config.network.eth_enabled) { + return true; #endif #ifndef ARCH_PORTDUINO - } else if (WiFi.status() == WL_CONNECTED) { - // it's likely we have wifi now, but user intends to turn it off in config! - return true; + } else if (WiFi.status() == WL_CONNECTED) { + // it's likely we have wifi now, but user intends to turn it off in config! + return true; #endif - } else { - return false; - } + } else { + return false; + } } // Disable WiFi -void deinitWifi() -{ - LOG_INFO("WiFi deinit"); +void deinitWifi() { + LOG_INFO("WiFi deinit"); - if (isWifiAvailable()) { + if (isWifiAvailable()) { #ifdef ARCH_ESP32 - WiFi.disconnect(true, false); + WiFi.disconnect(true, false); #elif defined(ARCH_RP2040) - WiFi.disconnect(true); + WiFi.disconnect(true); #endif - WiFi.mode(WIFI_OFF); - LOG_INFO("WiFi Turned Off"); - // WiFi.printDiag(Serial); - } + WiFi.mode(WIFI_OFF); + LOG_INFO("WiFi Turned Off"); + // WiFi.printDiag(Serial); + } } // Startup WiFi -bool initWifi() -{ - if (config.network.wifi_enabled && config.network.wifi_ssid[0]) { +bool initWifi() { + if (config.network.wifi_enabled && config.network.wifi_ssid[0]) { - const char *wifiName = config.network.wifi_ssid; - const char *wifiPsw = config.network.wifi_psk; + const char *wifiName = config.network.wifi_ssid; + const char *wifiPsw = config.network.wifi_psk; #ifndef ARCH_RP2040 #if !MESHTASTIC_EXCLUDE_WEBSERVER - createSSLCert(); // For WebServer + createSSLCert(); // For WebServer #endif - WiFi.persistent(false); // Disable flash storage for WiFi credentials + WiFi.persistent(false); // Disable flash storage for WiFi credentials #endif - if (!*wifiPsw) // Treat empty password as no password - wifiPsw = NULL; + if (!*wifiPsw) // Treat empty password as no password + wifiPsw = NULL; - if (*wifiName) { - uint8_t dmac[6]; - getMacAddr(dmac); - snprintf(ourHost, sizeof(ourHost), "Meshtastic-%02x%02x", dmac[4], dmac[5]); + if (*wifiName) { + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(ourHost, sizeof(ourHost), "Meshtastic-%02x%02x", dmac[4], dmac[5]); - WiFi.mode(WIFI_STA); - WiFi.setHostname(ourHost); + WiFi.mode(WIFI_STA); + WiFi.setHostname(ourHost); - if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC && - config.network.ipv4_config.ip != 0) { + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC && config.network.ipv4_config.ip != 0) { #ifdef ARCH_ESP32 - WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet, - config.network.ipv4_config.dns); + WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet, + config.network.ipv4_config.dns); #elif defined(ARCH_RP2040) - WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, - config.network.ipv4_config.subnet); + WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, + config.network.ipv4_config.subnet); #endif - } + } #ifdef ARCH_ESP32 - WiFi.onEvent(WiFiEvent); - WiFi.setAutoReconnect(true); - WiFi.setSleep(false); + WiFi.onEvent(WiFiEvent); + WiFi.setAutoReconnect(true); + WiFi.setSleep(false); - // This is needed to improve performance. - esp_wifi_set_ps(WIFI_PS_NONE); // Disable radio power saving + // This is needed to improve performance. + esp_wifi_set_ps(WIFI_PS_NONE); // Disable radio power saving - WiFi.onEvent( - [](WiFiEvent_t event, WiFiEventInfo_t info) { - LOG_WARN("WiFi lost connection. Reason: %d", info.wifi_sta_disconnected.reason); + WiFi.onEvent( + [](WiFiEvent_t event, WiFiEventInfo_t info) { + LOG_WARN("WiFi lost connection. Reason: %d", info.wifi_sta_disconnected.reason); - /* - If we are disconnected from the AP for some reason, - save the error code. + /* + If we are disconnected from the AP for some reason, + save the error code. - For a reference to the codes: - https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - */ - wifiDisconnectReason = info.wifi_sta_disconnected.reason; - }, - WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + For a reference to the codes: + https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + */ + wifiDisconnectReason = info.wifi_sta_disconnected.reason; + }, + WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); #endif - LOG_DEBUG("JOINING WIFI soon: ssid=%s", wifiName); - wifiReconnect = new Periodic("WifiConnect", reconnectWiFi); - } - return true; - } else { - LOG_INFO("Not using WIFI"); - return false; + LOG_DEBUG("JOINING WIFI soon: ssid=%s", wifiName); + wifiReconnect = new Periodic("WifiConnect", reconnectWiFi); } + return true; + } else { + LOG_INFO("Not using WIFI"); + return false; + } } #ifdef ARCH_ESP32 @@ -339,208 +332,203 @@ bool initWifi() // Licensed under the GNU Lesser General Public License v2.1 // https://github.com/espressif/arduino-esp32/blob/1f038677eb2eaf5e9ca6b6074486803c15468bed/libraries/WiFi/src/WiFiSTA.cpp#L755 esp_netif_t *get_esp_interface_netif(esp_interface_t interface); -IPv6Address GlobalIPv6() -{ - esp_ip6_addr_t addr; - if (WiFiGenericClass::getMode() == WIFI_MODE_NULL) { - return IPv6Address(); - } - if (esp_netif_get_ip6_global(get_esp_interface_netif(ESP_IF_WIFI_STA), &addr)) { - return IPv6Address(); - } - return IPv6Address(addr.addr); +IPv6Address GlobalIPv6() { + esp_ip6_addr_t addr; + if (WiFiGenericClass::getMode() == WIFI_MODE_NULL) { + return IPv6Address(); + } + if (esp_netif_get_ip6_global(get_esp_interface_netif(ESP_IF_WIFI_STA), &addr)) { + return IPv6Address(); + } + return IPv6Address(addr.addr); } #endif // Called by the Espressif SDK to -static void WiFiEvent(WiFiEvent_t event) -{ - LOG_DEBUG("Network-Event %d: ", event); +static void WiFiEvent(WiFiEvent_t event) { + LOG_DEBUG("Network-Event %d: ", event); - switch (event) { - case ARDUINO_EVENT_WIFI_READY: - LOG_INFO("WiFi interface ready"); - break; - case ARDUINO_EVENT_WIFI_SCAN_DONE: - LOG_INFO("Completed scan for access points"); - break; - case ARDUINO_EVENT_WIFI_STA_START: - LOG_INFO("WiFi station started"); - break; - case ARDUINO_EVENT_WIFI_STA_STOP: - LOG_INFO("WiFi station stopped"); - syslog.disable(); - break; - case ARDUINO_EVENT_WIFI_STA_CONNECTED: - LOG_INFO("Connected to access point"); - if (config.network.ipv6_enabled) { + switch (event) { + case ARDUINO_EVENT_WIFI_READY: + LOG_INFO("WiFi interface ready"); + break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + LOG_INFO("Completed scan for access points"); + break; + case ARDUINO_EVENT_WIFI_STA_START: + LOG_INFO("WiFi station started"); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + LOG_INFO("WiFi station stopped"); + syslog.disable(); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + LOG_INFO("Connected to access point"); + if (config.network.ipv6_enabled) { #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - if (!WiFi.enableIPv6()) { - LOG_WARN("Failed to enable IPv6"); - } + if (!WiFi.enableIPv6()) { + LOG_WARN("Failed to enable IPv6"); + } #else - if (!WiFi.enableIpV6()) { - LOG_WARN("Failed to enable IPv6"); - } + if (!WiFi.enableIpV6()) { + LOG_WARN("Failed to enable IPv6"); + } #endif - } -#ifdef WIFI_LED - digitalWrite(WIFI_LED, HIGH); -#endif - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - LOG_INFO("Disconnected from WiFi access point"); -#ifdef WIFI_LED - digitalWrite(WIFI_LED, LOW); -#endif - if (!isReconnecting) { - WiFi.disconnect(false, true); - syslog.disable(); - needReconnect = true; - wifiReconnect->setIntervalFromNow(1000); - } - break; - case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: - LOG_INFO("Authentication mode of access point has changed"); - break; - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); - onNetworkConnected(); - break; - case ARDUINO_EVENT_WIFI_STA_GOT_IP6: -#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - LOG_INFO("Obtained Local IP6 address: %s", WiFi.linkLocalIPv6().toString().c_str()); - LOG_INFO("Obtained GlobalIP6 address: %s", WiFi.globalIPv6().toString().c_str()); -#else - LOG_INFO("Obtained Local IP6 address: %s", WiFi.localIPv6().toString().c_str()); - LOG_INFO("Obtained GlobalIP6 address: %s", GlobalIPv6().toString().c_str()); -#endif - break; - case ARDUINO_EVENT_WIFI_STA_LOST_IP: - LOG_INFO("Lost IP address and IP address is reset to 0"); - if (!isReconnecting) { - WiFi.disconnect(false, true); - syslog.disable(); - needReconnect = true; - wifiReconnect->setIntervalFromNow(1000); - } - break; - case ARDUINO_EVENT_WPS_ER_SUCCESS: - LOG_INFO("WiFi Protected Setup (WPS): succeeded in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_FAILED: - LOG_INFO("WiFi Protected Setup (WPS): failed in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_TIMEOUT: - LOG_INFO("WiFi Protected Setup (WPS): timeout in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_PIN: - LOG_INFO("WiFi Protected Setup (WPS): pin code in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_PBC_OVERLAP: - LOG_INFO("WiFi Protected Setup (WPS): push button overlap in enrollee mode"); - break; - case ARDUINO_EVENT_WIFI_AP_START: - LOG_INFO("WiFi access point started"); -#ifdef WIFI_LED - digitalWrite(WIFI_LED, HIGH); -#endif - break; - case ARDUINO_EVENT_WIFI_AP_STOP: - LOG_INFO("WiFi access point stopped"); -#ifdef WIFI_LED - digitalWrite(WIFI_LED, LOW); -#endif - break; - case ARDUINO_EVENT_WIFI_AP_STACONNECTED: - LOG_INFO("Client connected"); - break; - case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: - LOG_INFO("Client disconnected"); - break; - case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: - LOG_INFO("Assigned IP address to client"); - break; - case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: - LOG_INFO("Received probe request"); - break; - case ARDUINO_EVENT_WIFI_AP_GOT_IP6: - LOG_INFO("IPv6 is preferred"); - break; - case ARDUINO_EVENT_WIFI_FTM_REPORT: - LOG_INFO("Fast Transition Management report"); - break; - case ARDUINO_EVENT_ETH_START: - LOG_INFO("Ethernet started"); - break; - case ARDUINO_EVENT_ETH_STOP: - syslog.disable(); - LOG_INFO("Ethernet stopped"); - break; - case ARDUINO_EVENT_ETH_CONNECTED: - LOG_INFO("Ethernet connected"); - break; - case ARDUINO_EVENT_ETH_DISCONNECTED: - syslog.disable(); - LOG_INFO("Ethernet disconnected"); - break; - case ARDUINO_EVENT_ETH_GOT_IP: -#ifdef USE_WS5500 - LOG_INFO("Obtained IP address: %s, %u Mbps, %s", ETH.localIP().toString().c_str(), ETH.linkSpeed(), - ETH.fullDuplex() ? "FULL_DUPLEX" : "HALF_DUPLEX"); - onNetworkConnected(); -#endif - break; - case ARDUINO_EVENT_ETH_GOT_IP6: -#ifdef USE_WS5500 -#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - LOG_INFO("Obtained Local IP6 address: %s", ETH.linkLocalIPv6().toString().c_str()); - LOG_INFO("Obtained GlobalIP6 address: %s", ETH.globalIPv6().toString().c_str()); -#else - LOG_INFO("Obtained IP6 address: %s", ETH.localIPv6().toString().c_str()); -#endif -#endif - break; - case ARDUINO_EVENT_SC_SCAN_DONE: - LOG_INFO("SmartConfig: Scan done"); - break; - case ARDUINO_EVENT_SC_FOUND_CHANNEL: - LOG_INFO("SmartConfig: Found channel"); - break; - case ARDUINO_EVENT_SC_GOT_SSID_PSWD: - LOG_INFO("SmartConfig: Got SSID and password"); - break; - case ARDUINO_EVENT_SC_SEND_ACK_DONE: - LOG_INFO("SmartConfig: Send ACK done"); - break; - case ARDUINO_EVENT_PROV_INIT: - LOG_INFO("Provision Init"); - break; - case ARDUINO_EVENT_PROV_DEINIT: - LOG_INFO("Provision Stopped"); - break; - case ARDUINO_EVENT_PROV_START: - LOG_INFO("Provision Started"); - break; - case ARDUINO_EVENT_PROV_END: - LOG_INFO("Provision End"); - break; - case ARDUINO_EVENT_PROV_CRED_RECV: - LOG_INFO("Provision Credentials received"); - break; - case ARDUINO_EVENT_PROV_CRED_FAIL: - LOG_INFO("Provision Credentials failed"); - break; - case ARDUINO_EVENT_PROV_CRED_SUCCESS: - LOG_INFO("Provision Credentials success"); - break; - default: - break; } +#ifdef WIFI_LED + digitalWrite(WIFI_LED, HIGH); +#endif + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + LOG_INFO("Disconnected from WiFi access point"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, LOW); +#endif + if (!isReconnecting) { + WiFi.disconnect(false, true); + syslog.disable(); + needReconnect = true; + wifiReconnect->setIntervalFromNow(1000); + } + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + LOG_INFO("Authentication mode of access point has changed"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); + onNetworkConnected(); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP6: +#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) + LOG_INFO("Obtained Local IP6 address: %s", WiFi.linkLocalIPv6().toString().c_str()); + LOG_INFO("Obtained GlobalIP6 address: %s", WiFi.globalIPv6().toString().c_str()); +#else + LOG_INFO("Obtained Local IP6 address: %s", WiFi.localIPv6().toString().c_str()); + LOG_INFO("Obtained GlobalIP6 address: %s", GlobalIPv6().toString().c_str()); +#endif + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + LOG_INFO("Lost IP address and IP address is reset to 0"); + if (!isReconnecting) { + WiFi.disconnect(false, true); + syslog.disable(); + needReconnect = true; + wifiReconnect->setIntervalFromNow(1000); + } + break; + case ARDUINO_EVENT_WPS_ER_SUCCESS: + LOG_INFO("WiFi Protected Setup (WPS): succeeded in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_FAILED: + LOG_INFO("WiFi Protected Setup (WPS): failed in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_TIMEOUT: + LOG_INFO("WiFi Protected Setup (WPS): timeout in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PIN: + LOG_INFO("WiFi Protected Setup (WPS): pin code in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PBC_OVERLAP: + LOG_INFO("WiFi Protected Setup (WPS): push button overlap in enrollee mode"); + break; + case ARDUINO_EVENT_WIFI_AP_START: + LOG_INFO("WiFi access point started"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, HIGH); +#endif + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + LOG_INFO("WiFi access point stopped"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, LOW); +#endif + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + LOG_INFO("Client connected"); + break; + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + LOG_INFO("Client disconnected"); + break; + case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: + LOG_INFO("Assigned IP address to client"); + break; + case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: + LOG_INFO("Received probe request"); + break; + case ARDUINO_EVENT_WIFI_AP_GOT_IP6: + LOG_INFO("IPv6 is preferred"); + break; + case ARDUINO_EVENT_WIFI_FTM_REPORT: + LOG_INFO("Fast Transition Management report"); + break; + case ARDUINO_EVENT_ETH_START: + LOG_INFO("Ethernet started"); + break; + case ARDUINO_EVENT_ETH_STOP: + syslog.disable(); + LOG_INFO("Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + LOG_INFO("Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + syslog.disable(); + LOG_INFO("Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: +#ifdef USE_WS5500 + LOG_INFO("Obtained IP address: %s, %u Mbps, %s", ETH.localIP().toString().c_str(), ETH.linkSpeed(), + ETH.fullDuplex() ? "FULL_DUPLEX" : "HALF_DUPLEX"); + onNetworkConnected(); +#endif + break; + case ARDUINO_EVENT_ETH_GOT_IP6: +#ifdef USE_WS5500 +#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) + LOG_INFO("Obtained Local IP6 address: %s", ETH.linkLocalIPv6().toString().c_str()); + LOG_INFO("Obtained GlobalIP6 address: %s", ETH.globalIPv6().toString().c_str()); +#else + LOG_INFO("Obtained IP6 address: %s", ETH.localIPv6().toString().c_str()); +#endif +#endif + break; + case ARDUINO_EVENT_SC_SCAN_DONE: + LOG_INFO("SmartConfig: Scan done"); + break; + case ARDUINO_EVENT_SC_FOUND_CHANNEL: + LOG_INFO("SmartConfig: Found channel"); + break; + case ARDUINO_EVENT_SC_GOT_SSID_PSWD: + LOG_INFO("SmartConfig: Got SSID and password"); + break; + case ARDUINO_EVENT_SC_SEND_ACK_DONE: + LOG_INFO("SmartConfig: Send ACK done"); + break; + case ARDUINO_EVENT_PROV_INIT: + LOG_INFO("Provision Init"); + break; + case ARDUINO_EVENT_PROV_DEINIT: + LOG_INFO("Provision Stopped"); + break; + case ARDUINO_EVENT_PROV_START: + LOG_INFO("Provision Started"); + break; + case ARDUINO_EVENT_PROV_END: + LOG_INFO("Provision End"); + break; + case ARDUINO_EVENT_PROV_CRED_RECV: + LOG_INFO("Provision Credentials received"); + break; + case ARDUINO_EVENT_PROV_CRED_FAIL: + LOG_INFO("Provision Credentials failed"); + break; + case ARDUINO_EVENT_PROV_CRED_SUCCESS: + LOG_INFO("Provision Credentials success"); + break; + default: + break; + } } #endif -uint8_t getWifiDisconnectReason() -{ - return wifiDisconnectReason; -} +uint8_t getWifiDisconnectReason() { return wifiDisconnectReason; } #endif // HAS_WIFI diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp index ea2ba641b..4e711d5cf 100644 --- a/src/meshUtils.cpp +++ b/src/meshUtils.cpp @@ -37,73 +37,68 @@ * SUCH DAMAGE. * */ -char *strnstr(const char *s, const char *find, size_t slen) -{ - char c; - if ((c = *find++) != '\0') { - char sc; - size_t len; +char *strnstr(const char *s, const char *find, size_t slen) { + char c; + if ((c = *find++) != '\0') { + char sc; + size_t len; - len = strlen(find); - do { - do { - if (slen-- < 1 || (sc = *s++) == '\0') - return (NULL); - } while (sc != c); - if (len > slen) - return (NULL); - } while (strncmp(s, find, len) != 0); - s--; + len = strlen(find); + do { + do { + if (slen-- < 1 || (sc = *s++) == '\0') + return (NULL); + } while (sc != c); + if (len > slen) + return (NULL); + } while (strncmp(s, find, len) != 0); + s--; + } + return ((char *)s); +} + +void printBytes(const char *label, const uint8_t *p, size_t numbytes) { + int labelSize = strlen(label); + char *messageBuffer = new char[labelSize + (numbytes * 3) + 2]; + strncpy(messageBuffer, label, labelSize); + for (size_t i = 0; i < numbytes; i++) + snprintf(messageBuffer + labelSize + i * 3, 4, " %02x", p[i]); + strcpy(messageBuffer + labelSize + numbytes * 3, "\n"); + LOG_DEBUG(messageBuffer); + delete[] messageBuffer; +} + +bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes) { + for (uint8_t i = 0; i < numbytes; i++) { + if (mem[i] != find) + return false; + } + return true; +} + +bool isOneOf(int item, int count, ...) { + va_list args; + va_start(args, count); + bool found = false; + for (int i = 0; i < count; ++i) { + if (item == va_arg(args, int)) { + found = true; + break; } - return ((char *)s); + } + va_end(args); + return found; } -void printBytes(const char *label, const uint8_t *p, size_t numbytes) -{ - int labelSize = strlen(label); - char *messageBuffer = new char[labelSize + (numbytes * 3) + 2]; - strncpy(messageBuffer, label, labelSize); - for (size_t i = 0; i < numbytes; i++) - snprintf(messageBuffer + labelSize + i * 3, 4, " %02x", p[i]); - strcpy(messageBuffer + labelSize + numbytes * 3, "\n"); - LOG_DEBUG(messageBuffer); - delete[] messageBuffer; -} - -bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes) -{ - for (uint8_t i = 0; i < numbytes; i++) { - if (mem[i] != find) - return false; - } - return true; -} - -bool isOneOf(int item, int count, ...) -{ - va_list args; - va_start(args, count); - bool found = false; - for (int i = 0; i < count; ++i) { - if (item == va_arg(args, int)) { - found = true; - break; - } - } - va_end(args); - return found; -} - -const std::string vformat(const char *const zcFormat, ...) -{ - va_list vaArgs; - va_start(vaArgs, zcFormat); - va_list vaArgsCopy; - va_copy(vaArgsCopy, vaArgs); - const int iLen = std::vsnprintf(NULL, 0, zcFormat, vaArgsCopy); - va_end(vaArgsCopy); - std::vector zc(iLen + 1); - std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs); - va_end(vaArgs); - return std::string(zc.data(), iLen); +const std::string vformat(const char *const zcFormat, ...) { + va_list vaArgs; + va_start(vaArgs, zcFormat); + va_list vaArgsCopy; + va_copy(vaArgsCopy, vaArgs); + const int iLen = std::vsnprintf(NULL, 0, zcFormat, vaArgsCopy); + va_end(vaArgsCopy); + std::vector zc(iLen + 1); + std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs); + va_end(vaArgs); + return std::string(zc.data(), iLen); } \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index 9fcf6f8a8..7503bda67 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -6,16 +6,13 @@ #include /// C++ v17+ clamp function, limits a given value to a range defined by lo and hi -template constexpr const T &clamp(const T &v, const T &lo, const T &hi) -{ - return (v < lo) ? lo : (hi < v) ? hi : v; -} +template constexpr const T &clamp(const T &v, const T &lo, const T &hi) { return (v < lo) ? lo : (hi < v) ? hi : v; } #if HAS_SCREEN -#define IF_SCREEN(X) \ - if (screen) { \ - X; \ - } +#define IF_SCREEN(X) \ + if (screen) { \ + X; \ + } #else #define IF_SCREEN(...) #endif diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 5f0c27fff..44806e42c 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -43,25 +43,23 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #include "SerialModule.h" #endif AdminModule *adminModule; bool hasOpenEditTransaction; -/// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' word instead. -/// Also, to make setting work correctly, if someone tries to set a string to this reserved value we assume they don't really want -/// a change. +/// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' +/// word instead. Also, to make setting work correctly, if someone tries to set a string to this reserved value we +/// assume they don't really want a change. static const char *secretReserved = "sekrit"; /// If buf is the reserved secret word, replace the buffer with currentVal -static void writeSecret(char *buf, size_t bufsz, const char *currentVal) -{ - if (strcmp(buf, secretReserved) == 0) { - strncpy(buf, currentVal, bufsz); - } +static void writeSecret(char *buf, size_t bufsz, const char *currentVal) { + if (strcmp(buf, secretReserved) == 0) { + strncpy(buf, currentVal, bufsz); + } } /** @@ -71,1417 +69,1372 @@ static void writeSecret(char *buf, size_t bufsz, const char *currentVal) * @param r Decoded AdminMessage * @return bool */ -bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) -{ - // if handled == false, then let others look at this message also if they want - bool handled = false; - assert(r); - bool fromOthers = !isFromUs(&mp); - if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { - return handled; +bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) { + // if handled == false, then let others look at this message also if they want + bool handled = false; + assert(r); + bool fromOthers = !isFromUs(&mp); + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + return handled; + } + meshtastic_Channel *ch = &channels.getByIndex(mp.channel); + // Could tighten this up further by tracking the last public_key we went an AdminMessage request to + // and only allowing responses from that remote. + if (messageIsResponse(r)) { + LOG_DEBUG("Allow admin response message"); + } else if (mp.from == 0) { + if (config.security.is_managed) { + LOG_INFO("Ignore local admin payload because is_managed"); + return handled; } - meshtastic_Channel *ch = &channels.getByIndex(mp.channel); - // Could tighten this up further by tracking the last public_key we went an AdminMessage request to - // and only allowing responses from that remote. - if (messageIsResponse(r)) { - LOG_DEBUG("Allow admin response message"); - } else if (mp.from == 0) { - if (config.security.is_managed) { - LOG_INFO("Ignore local admin payload because is_managed"); - return handled; - } - } else if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) { - if (!config.security.admin_channel_enabled) { - LOG_INFO("Ignore admin channel, legacy admin is disabled"); - myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); - return handled; - } - } else if (mp.pki_encrypted) { - if ((config.security.admin_key[0].size == 32 && - memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || - (config.security.admin_key[1].size == 32 && - memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || - (config.security.admin_key[2].size == 32 && - memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { - LOG_INFO("PKC admin payload with authorized sender key"); + } else if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) { + if (!config.security.admin_channel_enabled) { + LOG_INFO("Ignore admin channel, legacy admin is disabled"); + myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + return handled; + } + } else if (mp.pki_encrypted) { + if ((config.security.admin_key[0].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || + (config.security.admin_key[1].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || + (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { + LOG_INFO("PKC admin payload with authorized sender key"); - // Automatically favorite the node that is using the admin key - auto remoteNode = nodeDB->getMeshNode(mp.from); - if (remoteNode && !remoteNode->is_favorite) { - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it - // without the user doing so deliberately. - LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); - } else { - LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); - remoteNode->is_favorite = true; - } - } + // Automatically favorite the node that is using the admin key + auto remoteNode = nodeDB->getMeshNode(mp.from); + if (remoteNode && !remoteNode->is_favorite) { + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it + // without the user doing so deliberately. + LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); } else { - myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); - LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); - return handled; + LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); + remoteNode->is_favorite = true; } + } } else { - LOG_INFO("Ignore unauthorized admin payload %i", r->which_payload_variant); - myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); - return handled; + myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); + LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); + return handled; } + } else { + LOG_INFO("Ignore unauthorized admin payload %i", r->which_payload_variant); + myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + return handled; + } - LOG_INFO("Handle admin payload %i", r->which_payload_variant); + LOG_INFO("Handle admin payload %i", r->which_payload_variant); - // all of the get and set messages, including those for other modules, flow through here first. - // any message that changes state, we want to check the passkey for - if (mp.from != 0 && !messageIsRequest(r) && !messageIsResponse(r)) { - if (!checkPassKey(r)) { - LOG_WARN("Admin message without session_key!"); - myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY, &mp); - return handled; - } + // all of the get and set messages, including those for other modules, flow through here first. + // any message that changes state, we want to check the passkey for + if (mp.from != 0 && !messageIsRequest(r) && !messageIsResponse(r)) { + if (!checkPassKey(r)) { + LOG_WARN("Admin message without session_key!"); + myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY, &mp); + return handled; } - switch (r->which_payload_variant) { + } + switch (r->which_payload_variant) { - /** - * Getters - */ - case meshtastic_AdminMessage_get_owner_request_tag: - LOG_DEBUG("Client got owner"); - handleGetOwner(mp); - break; + /** + * Getters + */ + case meshtastic_AdminMessage_get_owner_request_tag: + LOG_DEBUG("Client got owner"); + handleGetOwner(mp); + break; - case meshtastic_AdminMessage_get_config_request_tag: - LOG_DEBUG("Client got config"); - handleGetConfig(mp, r->get_config_request); - break; + case meshtastic_AdminMessage_get_config_request_tag: + LOG_DEBUG("Client got config"); + handleGetConfig(mp, r->get_config_request); + break; - case meshtastic_AdminMessage_get_module_config_request_tag: - LOG_DEBUG("Client got module config"); - handleGetModuleConfig(mp, r->get_module_config_request); - break; + case meshtastic_AdminMessage_get_module_config_request_tag: + LOG_DEBUG("Client got module config"); + handleGetModuleConfig(mp, r->get_module_config_request); + break; - case meshtastic_AdminMessage_get_channel_request_tag: { - uint32_t i = r->get_channel_request - 1; - LOG_DEBUG("Client got channel %u", i); - if (i >= MAX_NUM_CHANNELS) - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - else - handleGetChannel(mp, i); + case meshtastic_AdminMessage_get_channel_request_tag: { + uint32_t i = r->get_channel_request - 1; + LOG_DEBUG("Client got channel %u", i); + if (i >= MAX_NUM_CHANNELS) + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + else + handleGetChannel(mp, i); + break; + } + + /** + * Setters + */ + case meshtastic_AdminMessage_set_owner_tag: + LOG_DEBUG("Client set owner"); + // Validate names + if (*r->set_owner.long_name) { + const char *start = r->set_owner.long_name; + // Skip all whitespace (space, tab, newline, etc) + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); break; + } } - - /** - * Setters - */ - case meshtastic_AdminMessage_set_owner_tag: - LOG_DEBUG("Client set owner"); - // Validate names - if (*r->set_owner.long_name) { - const char *start = r->set_owner.long_name; - // Skip all whitespace (space, tab, newline, etc) - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - break; - } - } - if (*r->set_owner.short_name) { - const char *start = r->set_owner.short_name; - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - break; - } - } - handleSetOwner(r->set_owner); - break; - - case meshtastic_AdminMessage_set_config_tag: - LOG_DEBUG("Client set config"); - handleSetConfig(r->set_config); - break; - - case meshtastic_AdminMessage_set_module_config_tag: - LOG_DEBUG("Client set module config"); - if (!handleSetModuleConfig(r->set_module_config)) { - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - } - break; - - case meshtastic_AdminMessage_set_channel_tag: - LOG_DEBUG("Client set channel %d", r->set_channel.index); - if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - else - handleSetChannel(r->set_channel); - break; - case meshtastic_AdminMessage_set_ham_mode_tag: - LOG_DEBUG("Client set ham mode"); - handleSetHamMode(r->set_ham_mode); - break; - case meshtastic_AdminMessage_get_ui_config_request_tag: { - LOG_DEBUG("Client is getting device-ui config"); - handleGetDeviceUIConfig(mp); - handled = true; + if (*r->set_owner.short_name) { + const char *start = r->set_owner.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); break; + } } + handleSetOwner(r->set_owner); + break; - /** - * Other - */ - case meshtastic_AdminMessage_reboot_seconds_tag: { - reboot(r->reboot_seconds); - break; + case meshtastic_AdminMessage_set_config_tag: + LOG_DEBUG("Client set config"); + handleSetConfig(r->set_config); + break; + + case meshtastic_AdminMessage_set_module_config_tag: + LOG_DEBUG("Client set module config"); + if (!handleSetModuleConfig(r->set_module_config)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } - case meshtastic_AdminMessage_reboot_ota_seconds_tag: { - int32_t s = r->reboot_ota_seconds; + break; + + case meshtastic_AdminMessage_set_channel_tag: + LOG_DEBUG("Client set channel %d", r->set_channel.index); + if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + else + handleSetChannel(r->set_channel); + break; + case meshtastic_AdminMessage_set_ham_mode_tag: + LOG_DEBUG("Client set ham mode"); + handleSetHamMode(r->set_ham_mode); + break; + case meshtastic_AdminMessage_get_ui_config_request_tag: { + LOG_DEBUG("Client is getting device-ui config"); + handleGetDeviceUIConfig(mp); + handled = true; + break; + } + + /** + * Other + */ + case meshtastic_AdminMessage_reboot_seconds_tag: { + reboot(r->reboot_seconds); + break; + } + case meshtastic_AdminMessage_reboot_ota_seconds_tag: { + int32_t s = r->reboot_ota_seconds; #if defined(ARCH_ESP32) #if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (!BleOta::getOtaAppVersion().isEmpty()) { - if (screen) - screen->startFirmwareUpdateScreen(); - BleOta::switchToOtaApp(); - LOG_INFO("Rebooting to BLE OTA"); - } + if (!BleOta::getOtaAppVersion().isEmpty()) { + if (screen) + screen->startFirmwareUpdateScreen(); + BleOta::switchToOtaApp(); + LOG_INFO("Rebooting to BLE OTA"); + } #endif #if !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::trySwitchToOTA()) { - if (screen) - screen->startFirmwareUpdateScreen(); - WiFiOTA::saveConfig(&config.network); - LOG_INFO("Rebooting to WiFi OTA"); - } + if (WiFiOTA::trySwitchToOTA()) { + if (screen) + screen->startFirmwareUpdateScreen(); + WiFiOTA::saveConfig(&config.network); + LOG_INFO("Rebooting to WiFi OTA"); + } #endif #endif - LOG_INFO("Reboot in %d seconds", s); - rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); - break; + LOG_INFO("Reboot in %d seconds", s); + rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); + break; + } + case meshtastic_AdminMessage_shutdown_seconds_tag: { + int32_t s = r->shutdown_seconds; + LOG_INFO("Shutdown in %d seconds", s); + shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000); + break; + } + case meshtastic_AdminMessage_get_device_metadata_request_tag: { + LOG_INFO("Client got device metadata"); + handleGetDeviceMetadata(mp); + break; + } + case meshtastic_AdminMessage_factory_reset_config_tag: { + disableBluetooth(); + LOG_INFO("Initiate factory config reset"); + nodeDB->factoryReset(); + LOG_INFO("Factory config reset finished, rebooting soon"); + reboot(DEFAULT_REBOOT_SECONDS); + break; + } + case meshtastic_AdminMessage_factory_reset_device_tag: { + disableBluetooth(); + LOG_INFO("Initiate full factory reset"); + nodeDB->factoryReset(true); + reboot(DEFAULT_REBOOT_SECONDS); + break; + } + case meshtastic_AdminMessage_nodedb_reset_tag: { + disableBluetooth(); + LOG_INFO("Initiate node-db reset"); + // CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a + // favorited node, so ensure that their favorites are kept on reset + bool rolePreference = isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); + nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset); + reboot(DEFAULT_REBOOT_SECONDS); + break; + } + case meshtastic_AdminMessage_store_ui_config_tag: { + LOG_INFO("Storing device-ui config"); + handleStoreDeviceUIConfig(r->store_ui_config); + handled = true; + break; + } + case meshtastic_AdminMessage_begin_edit_settings_tag: { + LOG_INFO("Begin transaction for editing settings"); + hasOpenEditTransaction = true; + break; + } + case meshtastic_AdminMessage_commit_edit_settings_tag: { + disableBluetooth(); + LOG_INFO("Commit transaction for edited settings"); + hasOpenEditTransaction = false; + saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); + break; + } + case meshtastic_AdminMessage_get_device_connection_status_request_tag: { + LOG_INFO("Client got device connection status"); + handleGetDeviceConnectionStatus(mp); + break; + } + case meshtastic_AdminMessage_get_module_config_response_tag: { + LOG_INFO("Client received a get_module_config response"); + if (fromOthers && r->get_module_config_response.which_payload_variant == meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) { + handleGetModuleConfigResponse(mp, r); } - case meshtastic_AdminMessage_shutdown_seconds_tag: { - int32_t s = r->shutdown_seconds; - LOG_INFO("Shutdown in %d seconds", s); - shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000); - break; + break; + } + case meshtastic_AdminMessage_remove_by_nodenum_tag: { + LOG_INFO("Client received remove_nodenum command"); + nodeDB->removeNodeByNum(r->remove_by_nodenum); + break; + } + case meshtastic_AdminMessage_add_contact_tag: { + LOG_INFO("Client received add_contact command"); + nodeDB->addFromContact(r->add_contact); + break; + } + case meshtastic_AdminMessage_set_favorite_node_tag: { + LOG_INFO("Client received set_favorite_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); + if (node != NULL) { + node->is_favorite = true; + saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } - case meshtastic_AdminMessage_get_device_metadata_request_tag: { - LOG_INFO("Client got device metadata"); - handleGetDeviceMetadata(mp); - break; + break; + } + case meshtastic_AdminMessage_remove_favorite_node_tag: { + LOG_INFO("Client received remove_favorite_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); + if (node != NULL) { + node->is_favorite = false; + saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } - case meshtastic_AdminMessage_factory_reset_config_tag: { - disableBluetooth(); - LOG_INFO("Initiate factory config reset"); - nodeDB->factoryReset(); - LOG_INFO("Factory config reset finished, rebooting soon"); - reboot(DEFAULT_REBOOT_SECONDS); - break; + break; + } + case meshtastic_AdminMessage_set_ignored_node_tag: { + LOG_INFO("Client received set_ignored_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); + if (node != NULL) { + node->is_ignored = true; + node->has_device_metrics = false; + node->has_position = false; + node->user.public_key.size = 0; + node->user.public_key.bytes[0] = 0; + saveChanges(SEGMENT_NODEDATABASE, false); } - case meshtastic_AdminMessage_factory_reset_device_tag: { - disableBluetooth(); - LOG_INFO("Initiate full factory reset"); - nodeDB->factoryReset(true); - reboot(DEFAULT_REBOOT_SECONDS); - break; + break; + } + case meshtastic_AdminMessage_remove_ignored_node_tag: { + LOG_INFO("Client received remove_ignored_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); + if (node != NULL) { + node->is_ignored = false; + saveChanges(SEGMENT_NODEDATABASE, false); } - case meshtastic_AdminMessage_nodedb_reset_tag: { - disableBluetooth(); - LOG_INFO("Initiate node-db reset"); - // CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a - // favorited node, so ensure that their favorites are kept on reset - bool rolePreference = - isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE, - meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); - nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset); - reboot(DEFAULT_REBOOT_SECONDS); - break; - } - case meshtastic_AdminMessage_store_ui_config_tag: { - LOG_INFO("Storing device-ui config"); - handleStoreDeviceUIConfig(r->store_ui_config); - handled = true; - break; - } - case meshtastic_AdminMessage_begin_edit_settings_tag: { - LOG_INFO("Begin transaction for editing settings"); - hasOpenEditTransaction = true; - break; - } - case meshtastic_AdminMessage_commit_edit_settings_tag: { - disableBluetooth(); - LOG_INFO("Commit transaction for edited settings"); - hasOpenEditTransaction = false; - saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); - break; - } - case meshtastic_AdminMessage_get_device_connection_status_request_tag: { - LOG_INFO("Client got device connection status"); - handleGetDeviceConnectionStatus(mp); - break; - } - case meshtastic_AdminMessage_get_module_config_response_tag: { - LOG_INFO("Client received a get_module_config response"); - if (fromOthers && r->get_module_config_response.which_payload_variant == - meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) { - handleGetModuleConfigResponse(mp, r); - } - break; - } - case meshtastic_AdminMessage_remove_by_nodenum_tag: { - LOG_INFO("Client received remove_nodenum command"); - nodeDB->removeNodeByNum(r->remove_by_nodenum); - break; - } - case meshtastic_AdminMessage_add_contact_tag: { - LOG_INFO("Client received add_contact command"); - nodeDB->addFromContact(r->add_contact); - break; - } - case meshtastic_AdminMessage_set_favorite_node_tag: { - LOG_INFO("Client received set_favorite_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); - if (node != NULL) { - node->is_favorite = true; - saveChanges(SEGMENT_NODEDATABASE, false); - if (screen) - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens - } - break; - } - case meshtastic_AdminMessage_remove_favorite_node_tag: { - LOG_INFO("Client received remove_favorite_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); - if (node != NULL) { - node->is_favorite = false; - saveChanges(SEGMENT_NODEDATABASE, false); - if (screen) - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens - } - break; - } - case meshtastic_AdminMessage_set_ignored_node_tag: { - LOG_INFO("Client received set_ignored_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); - if (node != NULL) { - node->is_ignored = true; - node->has_device_metrics = false; - node->has_position = false; - node->user.public_key.size = 0; - node->user.public_key.bytes[0] = 0; - saveChanges(SEGMENT_NODEDATABASE, false); - } - break; - } - case meshtastic_AdminMessage_remove_ignored_node_tag: { - LOG_INFO("Client received remove_ignored_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); - if (node != NULL) { - node->is_ignored = false; - saveChanges(SEGMENT_NODEDATABASE, false); - } - break; - } - case meshtastic_AdminMessage_set_fixed_position_tag: { - LOG_INFO("Client received set_fixed_position command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - node->has_position = true; - node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); - nodeDB->setLocalPosition(r->set_fixed_position); - config.position.fixed_position = true; - saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + break; + } + case meshtastic_AdminMessage_set_fixed_position_tag: { + LOG_INFO("Client received set_fixed_position command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + node->has_position = true; + node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); + nodeDB->setLocalPosition(r->set_fixed_position); + config.position.fixed_position = true; + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); #if !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) - gps->enable(); - // Send our new fixed position to the mesh for good measure - positionModule->sendOurPosition(); + if (gps != nullptr) + gps->enable(); + // Send our new fixed position to the mesh for good measure + positionModule->sendOurPosition(); #endif - break; - } - case meshtastic_AdminMessage_remove_fixed_position_tag: { - LOG_INFO("Client received remove_fixed_position command"); - nodeDB->clearLocalPosition(); - config.position.fixed_position = false; - saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); - break; - } - case meshtastic_AdminMessage_set_time_only_tag: { - LOG_INFO("Client received set_time_only command"); - struct timeval tv; - tv.tv_sec = r->set_time_only; - tv.tv_usec = 0; + break; + } + case meshtastic_AdminMessage_remove_fixed_position_tag: { + LOG_INFO("Client received remove_fixed_position command"); + nodeDB->clearLocalPosition(); + config.position.fixed_position = false; + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + break; + } + case meshtastic_AdminMessage_set_time_only_tag: { + LOG_INFO("Client received set_time_only command"); + struct timeval tv; + tv.tv_sec = r->set_time_only; + tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv, false); - break; - } - case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { - LOG_INFO("Client requesting to enter DFU mode"); + perhapsSetRTC(RTCQualityNTP, &tv, false); + break; + } + case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { + LOG_INFO("Client requesting to enter DFU mode"); #if HAS_SCREEN - IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); + IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); #endif #if defined(ARCH_NRF52) || defined(ARCH_RP2040) - enterDfuMode(); + enterDfuMode(); #endif - break; - } - case meshtastic_AdminMessage_delete_file_request_tag: { - LOG_DEBUG("Client requesting to delete file: %s", r->delete_file_request); + break; + } + case meshtastic_AdminMessage_delete_file_request_tag: { + LOG_DEBUG("Client requesting to delete file: %s", r->delete_file_request); #ifdef FSCom - spiLock->lock(); - if (FSCom.remove(r->delete_file_request)) { - LOG_DEBUG("Successfully deleted file"); - } else { - LOG_DEBUG("Failed to delete file"); - } - spiLock->unlock(); + spiLock->lock(); + if (FSCom.remove(r->delete_file_request)) { + LOG_DEBUG("Successfully deleted file"); + } else { + LOG_DEBUG("Failed to delete file"); + } + spiLock->unlock(); #endif - break; + break; + } + case meshtastic_AdminMessage_backup_preferences_tag: { + LOG_INFO("Client requesting to backup preferences"); + if (nodeDB->backupPreferences(r->backup_preferences)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); + } else { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } - case meshtastic_AdminMessage_backup_preferences_tag: { - LOG_INFO("Client requesting to backup preferences"); - if (nodeDB->backupPreferences(r->backup_preferences)) { - myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); - } else { - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - } - break; + break; + } + case meshtastic_AdminMessage_restore_preferences_tag: { + LOG_INFO("Client requesting to restore preferences"); + if (nodeDB->restorePreferences(r->backup_preferences, SEGMENT_DEVICESTATE | SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_CHANNELS)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); + LOG_DEBUG("Rebooting after successful restore of preferences"); + reboot(1000); + disableBluetooth(); + } else { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } - case meshtastic_AdminMessage_restore_preferences_tag: { - LOG_INFO("Client requesting to restore preferences"); - if (nodeDB->restorePreferences(r->backup_preferences, - SEGMENT_DEVICESTATE | SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_CHANNELS)) { - myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); - LOG_DEBUG("Rebooting after successful restore of preferences"); - reboot(1000); - disableBluetooth(); - } else { - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - } - break; - } - case meshtastic_AdminMessage_remove_backup_preferences_tag: { - LOG_INFO("Client requesting to remove backup preferences"); + break; + } + case meshtastic_AdminMessage_remove_backup_preferences_tag: { + LOG_INFO("Client requesting to remove backup preferences"); #ifdef FSCom - if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_FLASH) { - spiLock->lock(); - FSCom.remove(backupFileName); - spiLock->unlock(); - } else if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support - LOG_ERROR("SD backup removal not implemented yet"); - } + if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_FLASH) { + spiLock->lock(); + FSCom.remove(backupFileName); + spiLock->unlock(); + } else if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_SD) { + // TODO: After more mainline SD card support + LOG_ERROR("SD backup removal not implemented yet"); + } #endif - break; - } - case meshtastic_AdminMessage_send_input_event_tag: { - LOG_INFO("Client requesting to send input event"); - handleSendInputEvent(r->send_input_event); - break; - } + break; + } + case meshtastic_AdminMessage_send_input_event_tag: { + LOG_INFO("Client requesting to send input event"); + handleSendInputEvent(r->send_input_event); + break; + } #ifdef ARCH_PORTDUINO - case meshtastic_AdminMessage_exit_simulator_tag: - LOG_INFO("Exiting simulator"); - exit(0); - break; + case meshtastic_AdminMessage_exit_simulator_tag: + LOG_INFO("Exiting simulator"); + exit(0); + break; #endif - default: - meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - AdminMessageHandleResult handleResult = MeshModule::handleAdminMessageForAllModules(mp, r, &res); + default: + meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; + AdminMessageHandleResult handleResult = MeshModule::handleAdminMessageForAllModules(mp, r, &res); - if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { - setPassKey(&res); - myReply = allocDataProtobuf(res); - } else if (mp.decoded.want_response) { - LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); - } else if (handleResult != AdminMessageHandleResult::HANDLED) { - // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages - LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); - } - break; + if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&res); + myReply = allocDataProtobuf(res); + } else if (mp.decoded.want_response) { + LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); + } else if (handleResult != AdminMessageHandleResult::HANDLED) { + // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages + LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); } + break; + } - // Allow any observers (e.g. the UI) to handle/respond - AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; - meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; - AdminModule_ObserverData observerData = { - .request = r, - .response = &observerResponse, - .result = &observerResult, - }; + // Allow any observers (e.g. the UI) to handle/respond + AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; + meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; + AdminModule_ObserverData observerData = { + .request = r, + .response = &observerResponse, + .result = &observerResult, + }; - notifyObservers(&observerData); + notifyObservers(&observerData); - if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { - setPassKey(&observerResponse); - myReply = allocDataProtobuf(observerResponse); - LOG_DEBUG("Observer responded to admin message"); - } else if (observerResult == AdminMessageHandleResult::HANDLED) { - LOG_DEBUG("Observer handled admin message"); - } + if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&observerResponse); + myReply = allocDataProtobuf(observerResponse); + LOG_DEBUG("Observer responded to admin message"); + } else if (observerResult == AdminMessageHandleResult::HANDLED) { + LOG_DEBUG("Observer handled admin message"); + } - // If asked for a response and it is not yet set, generate an 'ACK' response - if (mp.decoded.want_response && !myReply) { - myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); - } - if (mp.pki_encrypted && myReply) { - myReply->pki_encrypted = true; - } - return handled; + // If asked for a response and it is not yet set, generate an 'ACK' response + if (mp.decoded.want_response && !myReply) { + myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); + } + if (mp.pki_encrypted && myReply) { + myReply->pki_encrypted = true; + } + return handled; } -void AdminModule::handleGetModuleConfigResponse(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) -{ - // Skip if it's disabled or no pins are exposed - if (!r->get_module_config_response.payload_variant.remote_hardware.enabled || - r->get_module_config_response.payload_variant.remote_hardware.available_pins_count == 0) { - LOG_DEBUG("Remote hardware module disabled or no available_pins. Skip"); - return; +void AdminModule::handleGetModuleConfigResponse(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) { + // Skip if it's disabled or no pins are exposed + if (!r->get_module_config_response.payload_variant.remote_hardware.enabled || + r->get_module_config_response.payload_variant.remote_hardware.available_pins_count == 0) { + LOG_DEBUG("Remote hardware module disabled or no available_pins. Skip"); + return; + } + for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { + if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { + continue; } - for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { - if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { - continue; - } - for (uint8_t j = 0; j < r->get_module_config_response.payload_variant.remote_hardware.available_pins_count; j++) { - auto availablePin = r->get_module_config_response.payload_variant.remote_hardware.available_pins[j]; - if (i < devicestate.node_remote_hardware_pins_count) { - devicestate.node_remote_hardware_pins[i].node_num = mp.from; - devicestate.node_remote_hardware_pins[i].pin = availablePin; - } - i++; - } + for (uint8_t j = 0; j < r->get_module_config_response.payload_variant.remote_hardware.available_pins_count; j++) { + auto availablePin = r->get_module_config_response.payload_variant.remote_hardware.available_pins[j]; + if (i < devicestate.node_remote_hardware_pins_count) { + devicestate.node_remote_hardware_pins[i].node_num = mp.from; + devicestate.node_remote_hardware_pins[i].pin = availablePin; + } + i++; } + } } /** * Setter methods */ -void AdminModule::handleSetOwner(const meshtastic_User &o) -{ - int changed = 0; +void AdminModule::handleSetOwner(const meshtastic_User &o) { + int changed = 0; - if (*o.long_name) { - changed |= strcmp(owner.long_name, o.long_name); - strncpy(owner.long_name, o.long_name, sizeof(owner.long_name)); - } - if (*o.short_name) { - changed |= strcmp(owner.short_name, o.short_name); - strncpy(owner.short_name, o.short_name, sizeof(owner.short_name)); - } - snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum()); + if (*o.long_name) { + changed |= strcmp(owner.long_name, o.long_name); + strncpy(owner.long_name, o.long_name, sizeof(owner.long_name)); + } + if (*o.short_name) { + changed |= strcmp(owner.short_name, o.short_name); + strncpy(owner.short_name, o.short_name, sizeof(owner.short_name)); + } + snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum()); - if (owner.is_licensed != o.is_licensed) { - changed = 1; - owner.is_licensed = o.is_licensed; - if (channels.ensureLicensedOperation()) { - sendWarning(licensedModeMessage); - } - } - if (owner.has_is_unmessagable != o.has_is_unmessagable || - (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { - changed = 1; - owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; - owner.is_unmessagable = o.is_unmessagable; + if (owner.is_licensed != o.is_licensed) { + changed = 1; + owner.is_licensed = o.is_licensed; + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); } + } + if (owner.has_is_unmessagable != o.has_is_unmessagable || (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { + changed = 1; + owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; + owner.is_unmessagable = o.is_unmessagable; + } - if (changed) { // If nothing really changed, don't broadcast on the network or write to flash - service->reloadOwner(!hasOpenEditTransaction); - saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE); - } + if (changed) { // If nothing really changed, don't broadcast on the network or write to flash + service->reloadOwner(!hasOpenEditTransaction); + saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE); + } } -void AdminModule::handleSetConfig(const meshtastic_Config &c) -{ - auto changes = SEGMENT_CONFIG; - auto existingRole = config.device.role; - bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); - bool requiresReboot = true; +void AdminModule::handleSetConfig(const meshtastic_Config &c) { + auto changes = SEGMENT_CONFIG; + auto existingRole = config.device.role; + bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); + bool requiresReboot = true; - switch (c.which_payload_variant) { - case meshtastic_Config_device_tag: - LOG_INFO("Set config: Device"); - config.has_device = true; + switch (c.which_payload_variant) { + case meshtastic_Config_device_tag: + LOG_INFO("Set config: Device"); + config.has_device = true; #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && - accelerometerThread->enabled == false) { - config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; - accelerometerThread->enabled = true; - accelerometerThread->start(); - } + if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && + accelerometerThread->enabled == false) { + config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; + accelerometerThread->enabled = true; + accelerometerThread->start(); + } #endif #ifdef LED_PIN - // Turn LED off if heartbeat by config - if (c.payload_variant.device.led_heartbeat_disabled) { - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); - } + // Turn LED off if heartbeat by config + if (c.payload_variant.device.led_heartbeat_disabled) { + digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); + } #endif - if (config.device.button_gpio == c.payload_variant.device.button_gpio && - config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && - config.device.role == c.payload_variant.device.role && - config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) { - requiresReboot = false; - } - config.device = c.payload_variant.device; - if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - const char *warning = "Rebroadcast mode can't be set to NONE for a router"; - LOG_WARN(warning); - sendWarning(warning); - } - // If we're setting router role for the first time, install its intervals - if (existingRole != c.payload_variant.device.role) { - nodeDB->installRoleDefaults(c.payload_variant.device.role); - changes |= SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE; // Some role defaults affect owner - } - if (config.device.node_info_broadcast_secs < min_node_info_broadcast_secs) { - LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d", min_node_info_broadcast_secs); - config.device.node_info_broadcast_secs = min_node_info_broadcast_secs; - } - // Router Client and Repeater deprecated; Set it to client - if (IS_ONE_OF(c.payload_variant.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT, - meshtastic_Config_DeviceConfig_Role_REPEATER)) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - if (moduleConfig.store_forward.enabled && !moduleConfig.store_forward.is_server) { - moduleConfig.store_forward.is_server = true; - changes |= SEGMENT_MODULECONFIG; - requiresReboot = true; - } - } + if (config.device.button_gpio == c.payload_variant.device.button_gpio && config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && + config.device.role == c.payload_variant.device.role && config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) { + requiresReboot = false; + } + config.device = c.payload_variant.device; + if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; + const char *warning = "Rebroadcast mode can't be set to NONE for a router"; + LOG_WARN(warning); + sendWarning(warning); + } + // If we're setting router role for the first time, install its intervals + if (existingRole != c.payload_variant.device.role) { + nodeDB->installRoleDefaults(c.payload_variant.device.role); + changes |= SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE; // Some role defaults affect owner + } + if (config.device.node_info_broadcast_secs < min_node_info_broadcast_secs) { + LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d", min_node_info_broadcast_secs); + config.device.node_info_broadcast_secs = min_node_info_broadcast_secs; + } + // Router Client and Repeater deprecated; Set it to client + if (IS_ONE_OF(c.payload_variant.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT, meshtastic_Config_DeviceConfig_Role_REPEATER)) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + if (moduleConfig.store_forward.enabled && !moduleConfig.store_forward.is_server) { + moduleConfig.store_forward.is_server = true; + changes |= SEGMENT_MODULECONFIG; + requiresReboot = true; + } + } #if USERPREFS_EVENT_MODE - // If we're in event mode, nobody is a Router or Router Late - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - } + // If we're in event mode, nobody is a Router or Router Late + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } #endif - break; - case meshtastic_Config_position_tag: - LOG_INFO("Set config: Position"); - config.has_position = true; - // If we have turned off the GPS (disabled or not present) and we're not using fixed position, - // clear the stored position since it may not get updated - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && - c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && - config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) { - nodeDB->clearLocalPosition(); - saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); - } - config.position = c.payload_variant.position; + break; + case meshtastic_Config_position_tag: + LOG_INFO("Set config: Position"); + config.has_position = true; + // If we have turned off the GPS (disabled or not present) and we're not using fixed position, + // clear the stored position since it may not get updated + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && config.position.fixed_position == false && + c.payload_variant.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + } + config.position = c.payload_variant.position; - // Save nodedb as well in case we got a fixed position packet - break; - case meshtastic_Config_power_tag: - LOG_INFO("Set config: Power"); - config.has_power = true; - // Really just the adc override is the only thing that can change without a reboot - if (config.power.device_battery_ina_address == c.payload_variant.power.device_battery_ina_address && - config.power.is_power_saving == c.payload_variant.power.is_power_saving && - config.power.ls_secs == c.payload_variant.power.ls_secs && - config.power.min_wake_secs == c.payload_variant.power.min_wake_secs && - config.power.on_battery_shutdown_after_secs == c.payload_variant.power.on_battery_shutdown_after_secs && - config.power.sds_secs == c.payload_variant.power.sds_secs && - config.power.wait_bluetooth_secs == c.payload_variant.power.wait_bluetooth_secs) { - requiresReboot = false; - } - config.power = c.payload_variant.power; - if (c.payload_variant.power.on_battery_shutdown_after_secs > 0 && - c.payload_variant.power.on_battery_shutdown_after_secs < 30) { - LOG_WARN("Tried to set on_battery_shutdown_after_secs too low, set to min 30 seconds"); - config.power.on_battery_shutdown_after_secs = 30; - } - break; - case meshtastic_Config_network_tag: - LOG_INFO("Set config: WiFi"); - config.has_network = true; - config.network = c.payload_variant.network; - break; - case meshtastic_Config_display_tag: - LOG_INFO("Set config: Display"); - config.has_display = true; - if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && - config.display.flip_screen == c.payload_variant.display.flip_screen && - config.display.oled == c.payload_variant.display.oled && - config.display.displaymode == c.payload_variant.display.displaymode) { - requiresReboot = false; - } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && - c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - config.bluetooth.enabled = false; - } + // Save nodedb as well in case we got a fixed position packet + break; + case meshtastic_Config_power_tag: + LOG_INFO("Set config: Power"); + config.has_power = true; + // Really just the adc override is the only thing that can change without a reboot + if (config.power.device_battery_ina_address == c.payload_variant.power.device_battery_ina_address && + config.power.is_power_saving == c.payload_variant.power.is_power_saving && config.power.ls_secs == c.payload_variant.power.ls_secs && + config.power.min_wake_secs == c.payload_variant.power.min_wake_secs && + config.power.on_battery_shutdown_after_secs == c.payload_variant.power.on_battery_shutdown_after_secs && + config.power.sds_secs == c.payload_variant.power.sds_secs && + config.power.wait_bluetooth_secs == c.payload_variant.power.wait_bluetooth_secs) { + requiresReboot = false; + } + config.power = c.payload_variant.power; + if (c.payload_variant.power.on_battery_shutdown_after_secs > 0 && c.payload_variant.power.on_battery_shutdown_after_secs < 30) { + LOG_WARN("Tried to set on_battery_shutdown_after_secs too low, set to min 30 seconds"); + config.power.on_battery_shutdown_after_secs = 30; + } + break; + case meshtastic_Config_network_tag: + LOG_INFO("Set config: WiFi"); + config.has_network = true; + config.network = c.payload_variant.network; + break; + case meshtastic_Config_display_tag: + LOG_INFO("Set config: Display"); + config.has_display = true; + if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && + config.display.flip_screen == c.payload_variant.display.flip_screen && config.display.oled == c.payload_variant.display.oled && + config.display.displaymode == c.payload_variant.display.displaymode) { + requiresReboot = false; + } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && + c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + config.bluetooth.enabled = false; + } #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && - accelerometerThread->enabled == false) { - config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; - accelerometerThread->enabled = true; - accelerometerThread->start(); - } + if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && + accelerometerThread->enabled == false) { + config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; + accelerometerThread->enabled = true; + accelerometerThread->start(); + } #endif - config.display = c.payload_variant.display; - break; + config.display = c.payload_variant.display; + break; - case meshtastic_Config_lora_tag: { - // Wrap the entire case in a block to scope variables and avoid crossing initialization - auto oldLoraConfig = config.lora; - auto validatedLora = c.payload_variant.lora; + case meshtastic_Config_lora_tag: { + // Wrap the entire case in a block to scope variables and avoid crossing initialization + auto oldLoraConfig = config.lora; + auto validatedLora = c.payload_variant.lora; - LOG_INFO("Set config: LoRa"); - config.has_lora = true; + LOG_INFO("Set config: LoRa"); + config.has_lora = true; - if (validatedLora.coding_rate < 4 || validatedLora.coding_rate > 8) { - LOG_WARN("Invalid coding_rate %d, setting to 5", validatedLora.coding_rate); - validatedLora.coding_rate = 5; - } + if (validatedLora.coding_rate < 4 || validatedLora.coding_rate > 8) { + LOG_WARN("Invalid coding_rate %d, setting to 5", validatedLora.coding_rate); + validatedLora.coding_rate = 5; + } - if (validatedLora.spread_factor < 7 || validatedLora.spread_factor > 12) { - LOG_WARN("Invalid spread_factor %d, setting to 11", validatedLora.spread_factor); - validatedLora.spread_factor = 11; - } + if (validatedLora.spread_factor < 7 || validatedLora.spread_factor > 12) { + LOG_WARN("Invalid spread_factor %d, setting to 11", validatedLora.spread_factor); + validatedLora.spread_factor = 11; + } - if (validatedLora.bandwidth == 0) { - int originalBandwidth = validatedLora.bandwidth; - validatedLora.bandwidth = myRegion->wideLora ? 800 : 250; - LOG_WARN("Invalid bandwidth %d, setting to default", originalBandwidth); - } + if (validatedLora.bandwidth == 0) { + int originalBandwidth = validatedLora.bandwidth; + validatedLora.bandwidth = myRegion->wideLora ? 800 : 250; + LOG_WARN("Invalid bandwidth %d, setting to default", originalBandwidth); + } - // If no lora radio parameters change, don't need to reboot - if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && - oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && - oldLoraConfig.spread_factor == validatedLora.spread_factor && - oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power && - oldLoraConfig.frequency_offset == validatedLora.frequency_offset && - oldLoraConfig.override_frequency == validatedLora.override_frequency && - oldLoraConfig.channel_num == validatedLora.channel_num && - oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { - requiresReboot = false; - } + // If no lora radio parameters change, don't need to reboot + if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && + oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && + oldLoraConfig.spread_factor == validatedLora.spread_factor && oldLoraConfig.coding_rate == validatedLora.coding_rate && + oldLoraConfig.tx_power == validatedLora.tx_power && oldLoraConfig.frequency_offset == validatedLora.frequency_offset && + oldLoraConfig.override_frequency == validatedLora.override_frequency && oldLoraConfig.channel_num == validatedLora.channel_num && + oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { + requiresReboot = false; + } #if defined(ARCH_PORTDUINO) - // If running on portduino and using SimRadio, do not require reboot - if (SimRadio::instance) { - requiresReboot = false; - } + // If running on portduino and using SimRadio, do not require reboot + if (SimRadio::instance) { + requiresReboot = false; + } #endif #ifdef RF95_FAN_EN - // Turn PA off if disabled by config - if (c.payload_variant.lora.pa_fan_disabled) { - digitalWrite(RF95_FAN_EN, LOW ^ 0); - } else { - digitalWrite(RF95_FAN_EN, HIGH ^ 0); - } + // Turn PA off if disabled by config + if (c.payload_variant.lora.pa_fan_disabled) { + digitalWrite(RF95_FAN_EN, LOW ^ 0); + } else { + digitalWrite(RF95_FAN_EN, HIGH ^ 0); + } #endif - config.lora = validatedLora; - // If we're setting region for the first time, init the region and regenerate the keys - if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + config.lora = validatedLora; + // If we're setting region for the first time, init the region and regenerate the keys + if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } #endif - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - // Compare the entire string, we are sure of the length as a topic has never been set - if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } - } - if (config.lora.region != myRegion->code) { - // Region has changed so check whether there is a regulatory one we should be using instead. - // Additionally as a side-effect, assume a new value under myRegion - initRegion(); - - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default root is in use, so subscribe to the appropriate MQTT topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - } - - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } - break; + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + // Compare the entire string, we are sure of the length as a topic has never been set + if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } } - case meshtastic_Config_bluetooth_tag: - LOG_INFO("Set config: Bluetooth"); - config.has_bluetooth = true; - config.bluetooth = c.payload_variant.bluetooth; - break; - case meshtastic_Config_security_tag: - LOG_INFO("Set config: Security"); - config.security = c.payload_variant.security; + if (config.lora.region != myRegion->code) { + // Region has changed so check whether there is a regulatory one we should be using instead. + // Additionally as a side-effect, assume a new value under myRegion + initRegion(); + + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + // Default root is in use, so subscribe to the appropriate MQTT topic for this region + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + } + + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } + break; + } + case meshtastic_Config_bluetooth_tag: + LOG_INFO("Set config: Bluetooth"); + config.has_bluetooth = true; + config.bluetooth = c.payload_variant.bluetooth; + break; + case meshtastic_Config_security_tag: + LOG_INFO("Set config: Security"); + config.security = c.payload_variant.security; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI) - // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode - if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - if (config.security.private_key.size != 32) { - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (config.security.private_key.size != 32) { + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - } else { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - config.security.public_key.size = 32; - } - } + } else { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + config.security.public_key.size = 32; } + } + } #endif - owner.public_key.size = config.security.public_key.size; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); + owner.public_key.size = config.security.public_key.size; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); #if !MESHTASTIC_EXCLUDE_PKI - crypto->setDHPrivateKey(config.security.private_key.bytes); + crypto->setDHPrivateKey(config.security.private_key.bytes); #endif - if (config.security.is_managed && !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 || - config.security.admin_key[2].size == 32)) { - config.security.is_managed = false; - const char *warning = "You must provide at least one admin public key to enable managed mode"; - LOG_WARN(warning); - sendWarning(warning); - } - - if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled && - config.security.serial_enabled == c.payload_variant.security.serial_enabled) - requiresReboot = false; - - break; - case meshtastic_Config_device_ui_tag: - // NOOP! This is handled by handleStoreDeviceUIConfig - break; - } - if (requiresReboot && !hasOpenEditTransaction) { - disableBluetooth(); + if (config.security.is_managed && + !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 || config.security.admin_key[2].size == 32)) { + config.security.is_managed = false; + const char *warning = "You must provide at least one admin public key to enable managed mode"; + LOG_WARN(warning); + sendWarning(warning); } - saveChanges(changes, requiresReboot); + if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled && + config.security.serial_enabled == c.payload_variant.security.serial_enabled) + requiresReboot = false; + + break; + case meshtastic_Config_device_ui_tag: + // NOOP! This is handled by handleStoreDeviceUIConfig + break; + } + if (requiresReboot && !hasOpenEditTransaction) { + disableBluetooth(); + } + + saveChanges(changes, requiresReboot); } -bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) -{ - // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth - // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && - !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { - disableBluetooth(); - } +bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { + // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth + // Otherwise, disable Bluetooth to prevent the phone from interfering with the config + if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { + disableBluetooth(); + } - switch (c.which_payload_variant) { - case meshtastic_ModuleConfig_mqtt_tag: + switch (c.which_payload_variant) { + case meshtastic_ModuleConfig_mqtt_tag: #if MESHTASTIC_EXCLUDE_MQTT - LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config"); - return false; + LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config"); + return false; #else - LOG_INFO("Set module config: MQTT"); - if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { - return false; - } - // Disable Bluetooth to prevent interference during MQTT configuration - disableBluetooth(); - moduleConfig.has_mqtt = true; - moduleConfig.mqtt = c.payload_variant.mqtt; -#endif - break; - case meshtastic_ModuleConfig_serial_tag: - LOG_INFO("Set module config: Serial"); -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) - if (!SerialModule::isValidConfig(c.payload_variant.serial)) { - LOG_ERROR("Invalid serial config"); - return false; - } - disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration -#endif - moduleConfig.has_serial = true; - moduleConfig.serial = c.payload_variant.serial; - break; - case meshtastic_ModuleConfig_external_notification_tag: - LOG_INFO("Set module config: External Notification"); - moduleConfig.has_external_notification = true; - moduleConfig.external_notification = c.payload_variant.external_notification; - break; - case meshtastic_ModuleConfig_store_forward_tag: - LOG_INFO("Set module config: Store & Forward"); - moduleConfig.has_store_forward = true; - moduleConfig.store_forward = c.payload_variant.store_forward; - break; - case meshtastic_ModuleConfig_range_test_tag: - LOG_INFO("Set module config: Range Test"); - moduleConfig.has_range_test = true; - moduleConfig.range_test = c.payload_variant.range_test; - break; - case meshtastic_ModuleConfig_telemetry_tag: - LOG_INFO("Set module config: Telemetry"); - moduleConfig.has_telemetry = true; - moduleConfig.telemetry = c.payload_variant.telemetry; - break; - case meshtastic_ModuleConfig_canned_message_tag: - LOG_INFO("Set module config: Canned Message"); - moduleConfig.has_canned_message = true; - moduleConfig.canned_message = c.payload_variant.canned_message; - break; - case meshtastic_ModuleConfig_audio_tag: - LOG_INFO("Set module config: Audio"); - moduleConfig.has_audio = true; - moduleConfig.audio = c.payload_variant.audio; - break; - case meshtastic_ModuleConfig_remote_hardware_tag: - LOG_INFO("Set module config: Remote Hardware"); - moduleConfig.has_remote_hardware = true; - moduleConfig.remote_hardware = c.payload_variant.remote_hardware; - break; - case meshtastic_ModuleConfig_neighbor_info_tag: - LOG_INFO("Set module config: Neighbor Info"); - moduleConfig.has_neighbor_info = true; - if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) { - LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs); - moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs; - } - moduleConfig.neighbor_info = c.payload_variant.neighbor_info; - break; - case meshtastic_ModuleConfig_detection_sensor_tag: - LOG_INFO("Set module config: Detection Sensor"); - moduleConfig.has_detection_sensor = true; - moduleConfig.detection_sensor = c.payload_variant.detection_sensor; - break; - case meshtastic_ModuleConfig_ambient_lighting_tag: - LOG_INFO("Set module config: Ambient Lighting"); - moduleConfig.has_ambient_lighting = true; - moduleConfig.ambient_lighting = c.payload_variant.ambient_lighting; - break; - case meshtastic_ModuleConfig_paxcounter_tag: - LOG_INFO("Set module config: Paxcounter"); - moduleConfig.has_paxcounter = true; - moduleConfig.paxcounter = c.payload_variant.paxcounter; - break; + LOG_INFO("Set module config: MQTT"); + if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { + return false; } - saveChanges(SEGMENT_MODULECONFIG); - return true; + // Disable Bluetooth to prevent interference during MQTT configuration + disableBluetooth(); + moduleConfig.has_mqtt = true; + moduleConfig.mqtt = c.payload_variant.mqtt; +#endif + break; + case meshtastic_ModuleConfig_serial_tag: + LOG_INFO("Set module config: Serial"); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if (!SerialModule::isValidConfig(c.payload_variant.serial)) { + LOG_ERROR("Invalid serial config"); + return false; + } + disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration +#endif + moduleConfig.has_serial = true; + moduleConfig.serial = c.payload_variant.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + LOG_INFO("Set module config: External Notification"); + moduleConfig.has_external_notification = true; + moduleConfig.external_notification = c.payload_variant.external_notification; + break; + case meshtastic_ModuleConfig_store_forward_tag: + LOG_INFO("Set module config: Store & Forward"); + moduleConfig.has_store_forward = true; + moduleConfig.store_forward = c.payload_variant.store_forward; + break; + case meshtastic_ModuleConfig_range_test_tag: + LOG_INFO("Set module config: Range Test"); + moduleConfig.has_range_test = true; + moduleConfig.range_test = c.payload_variant.range_test; + break; + case meshtastic_ModuleConfig_telemetry_tag: + LOG_INFO("Set module config: Telemetry"); + moduleConfig.has_telemetry = true; + moduleConfig.telemetry = c.payload_variant.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + LOG_INFO("Set module config: Canned Message"); + moduleConfig.has_canned_message = true; + moduleConfig.canned_message = c.payload_variant.canned_message; + break; + case meshtastic_ModuleConfig_audio_tag: + LOG_INFO("Set module config: Audio"); + moduleConfig.has_audio = true; + moduleConfig.audio = c.payload_variant.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + LOG_INFO("Set module config: Remote Hardware"); + moduleConfig.has_remote_hardware = true; + moduleConfig.remote_hardware = c.payload_variant.remote_hardware; + break; + case meshtastic_ModuleConfig_neighbor_info_tag: + LOG_INFO("Set module config: Neighbor Info"); + moduleConfig.has_neighbor_info = true; + if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) { + LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs); + moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs; + } + moduleConfig.neighbor_info = c.payload_variant.neighbor_info; + break; + case meshtastic_ModuleConfig_detection_sensor_tag: + LOG_INFO("Set module config: Detection Sensor"); + moduleConfig.has_detection_sensor = true; + moduleConfig.detection_sensor = c.payload_variant.detection_sensor; + break; + case meshtastic_ModuleConfig_ambient_lighting_tag: + LOG_INFO("Set module config: Ambient Lighting"); + moduleConfig.has_ambient_lighting = true; + moduleConfig.ambient_lighting = c.payload_variant.ambient_lighting; + break; + case meshtastic_ModuleConfig_paxcounter_tag: + LOG_INFO("Set module config: Paxcounter"); + moduleConfig.has_paxcounter = true; + moduleConfig.paxcounter = c.payload_variant.paxcounter; + break; + } + saveChanges(SEGMENT_MODULECONFIG); + return true; } -void AdminModule::handleSetChannel(const meshtastic_Channel &cc) -{ - channels.setChannel(cc); - if (channels.ensureLicensedOperation()) { - sendWarning(licensedModeMessage); - } - channels.onConfigChanged(); // tell the radios about this change - saveChanges(SEGMENT_CHANNELS, false); +void AdminModule::handleSetChannel(const meshtastic_Channel &cc) { + channels.setChannel(cc); + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } + channels.onConfigChanged(); // tell the radios about this change + saveChanges(SEGMENT_CHANNELS, false); } /** * Getters */ -void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) -{ - if (req.decoded.want_response) { - // We create the reply here - meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - res.get_owner_response = owner; - - res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; - setPassKey(&res); - myReply = allocDataProtobuf(res); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } - } -} - -void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32_t configType) -{ +void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) { + if (req.decoded.want_response) { + // We create the reply here meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; + res.get_owner_response = owner; - if (req.decoded.want_response) { - switch (configType) { - case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG: - LOG_INFO("Get config: Device"); - res.get_config_response.which_payload_variant = meshtastic_Config_device_tag; - res.get_config_response.payload_variant.device = config.device; - break; - case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG: - LOG_INFO("Get config: Position"); - res.get_config_response.which_payload_variant = meshtastic_Config_position_tag; - res.get_config_response.payload_variant.position = config.position; - break; - case meshtastic_AdminMessage_ConfigType_POWER_CONFIG: - LOG_INFO("Get config: Power"); - res.get_config_response.which_payload_variant = meshtastic_Config_power_tag; - res.get_config_response.payload_variant.power = config.power; - break; - case meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG: - LOG_INFO("Get config: Network"); - res.get_config_response.which_payload_variant = meshtastic_Config_network_tag; - res.get_config_response.payload_variant.network = config.network; - writeSecret(res.get_config_response.payload_variant.network.wifi_psk, - sizeof(res.get_config_response.payload_variant.network.wifi_psk), config.network.wifi_psk); - break; - case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG: - LOG_INFO("Get config: Display"); - res.get_config_response.which_payload_variant = meshtastic_Config_display_tag; - res.get_config_response.payload_variant.display = config.display; - break; - case meshtastic_AdminMessage_ConfigType_LORA_CONFIG: - LOG_INFO("Get config: LoRa"); - res.get_config_response.which_payload_variant = meshtastic_Config_lora_tag; - res.get_config_response.payload_variant.lora = config.lora; - break; - case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG: - LOG_INFO("Get config: Bluetooth"); - res.get_config_response.which_payload_variant = meshtastic_Config_bluetooth_tag; - res.get_config_response.payload_variant.bluetooth = config.bluetooth; - break; - case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG: - LOG_INFO("Get config: Security"); - res.get_config_response.which_payload_variant = meshtastic_Config_security_tag; - res.get_config_response.payload_variant.security = config.security; - break; - case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG: - LOG_INFO("Get config: Sessionkey"); - res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_tag; - break; - case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG: - // NOOP! This is handled by handleGetDeviceUIConfig - res.get_config_response.which_payload_variant = meshtastic_Config_device_ui_tag; - break; - } - // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. - // So even if we internally use 0 to represent 'use default' we still need to send the value we are - // using to the app (so that even old phone apps work with new device loads). - // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally - // private and useful for users to know current provisioning) - // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = - // Config_ModuleConfig_telemetry_tag; - res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; - setPassKey(&res); - myReply = allocDataProtobuf(res); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } - } -} - -void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const uint32_t configType) -{ - meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - - if (req.decoded.want_response) { - switch (configType) { - case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: - LOG_INFO("Get module config: MQTT"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; - res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt; - break; - case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: - LOG_INFO("Get module config: Serial"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag; - res.get_module_config_response.payload_variant.serial = moduleConfig.serial; - break; - case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: - LOG_INFO("Get module config: External Notification"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; - res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification; - break; - case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: - LOG_INFO("Get module config: Store & Forward"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; - res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward; - break; - case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: - LOG_INFO("Get module config: Range Test"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; - res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test; - break; - case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: - LOG_INFO("Get module config: Telemetry"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; - res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry; - break; - case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: - LOG_INFO("Get module config: Canned Message"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; - res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; - break; - case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: - LOG_INFO("Get module config: Audio"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; - res.get_module_config_response.payload_variant.audio = moduleConfig.audio; - break; - case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: - LOG_INFO("Get module config: Remote Hardware"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; - res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware; - break; - case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: - LOG_INFO("Get module config: Neighbor Info"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; - res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; - break; - case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: - LOG_INFO("Get module config: Detection Sensor"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; - res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor; - break; - case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: - LOG_INFO("Get module config: Ambient Lighting"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; - res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; - break; - case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: - LOG_INFO("Get module config: Paxcounter"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; - res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; - break; - } - - // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. - // So even if we internally use 0 to represent 'use default' we still need to send the value we are - // using to the app (so that even old phone apps work with new device loads). - // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally - // private and useful for users to know current provisioning) - // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = - // Config_ModuleConfig_telemetry_tag; - res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; - setPassKey(&res); - myReply = allocDataProtobuf(res); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } - } -} - -void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req) -{ - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.which_payload_variant = meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag; - for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { - if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { - continue; - } - r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i] = devicestate.node_remote_hardware_pins[i]; - } - for (uint8_t i = 0; i < moduleConfig.remote_hardware.available_pins_count; i++) { - if (!moduleConfig.remote_hardware.available_pins[i].gpio_pin) { - continue; - } - meshtastic_NodeRemoteHardwarePin nodePin = meshtastic_NodeRemoteHardwarePin_init_default; - nodePin.node_num = nodeDB->getNodeNum(); - nodePin.pin = moduleConfig.remote_hardware.available_pins[i]; - r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i + 12] = nodePin; - } - setPassKey(&r); - myReply = allocDataProtobuf(r); + res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; + setPassKey(&res); + myReply = allocDataProtobuf(res); if (req.pki_encrypted) { - myReply->pki_encrypted = true; + myReply->pki_encrypted = true; } + } } -void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) -{ - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.get_device_metadata_response = getDeviceMetadata(); - r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; - setPassKey(&r); - myReply = allocDataProtobuf(r); +void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32_t configType) { + meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; + + if (req.decoded.want_response) { + switch (configType) { + case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG: + LOG_INFO("Get config: Device"); + res.get_config_response.which_payload_variant = meshtastic_Config_device_tag; + res.get_config_response.payload_variant.device = config.device; + break; + case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG: + LOG_INFO("Get config: Position"); + res.get_config_response.which_payload_variant = meshtastic_Config_position_tag; + res.get_config_response.payload_variant.position = config.position; + break; + case meshtastic_AdminMessage_ConfigType_POWER_CONFIG: + LOG_INFO("Get config: Power"); + res.get_config_response.which_payload_variant = meshtastic_Config_power_tag; + res.get_config_response.payload_variant.power = config.power; + break; + case meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG: + LOG_INFO("Get config: Network"); + res.get_config_response.which_payload_variant = meshtastic_Config_network_tag; + res.get_config_response.payload_variant.network = config.network; + writeSecret(res.get_config_response.payload_variant.network.wifi_psk, sizeof(res.get_config_response.payload_variant.network.wifi_psk), + config.network.wifi_psk); + break; + case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG: + LOG_INFO("Get config: Display"); + res.get_config_response.which_payload_variant = meshtastic_Config_display_tag; + res.get_config_response.payload_variant.display = config.display; + break; + case meshtastic_AdminMessage_ConfigType_LORA_CONFIG: + LOG_INFO("Get config: LoRa"); + res.get_config_response.which_payload_variant = meshtastic_Config_lora_tag; + res.get_config_response.payload_variant.lora = config.lora; + break; + case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG: + LOG_INFO("Get config: Bluetooth"); + res.get_config_response.which_payload_variant = meshtastic_Config_bluetooth_tag; + res.get_config_response.payload_variant.bluetooth = config.bluetooth; + break; + case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG: + LOG_INFO("Get config: Security"); + res.get_config_response.which_payload_variant = meshtastic_Config_security_tag; + res.get_config_response.payload_variant.security = config.security; + break; + case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG: + LOG_INFO("Get config: Sessionkey"); + res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_tag; + break; + case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG: + // NOOP! This is handled by handleGetDeviceUIConfig + res.get_config_response.which_payload_variant = meshtastic_Config_device_ui_tag; + break; + } + // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. + // So even if we internally use 0 to represent 'use default' we still need to send the value we are + // using to the app (so that even old phone apps work with new device loads). + // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; + res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; + setPassKey(&res); + myReply = allocDataProtobuf(res); if (req.pki_encrypted) { - myReply->pki_encrypted = true; + myReply->pki_encrypted = true; } + } } -void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) -{ - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; +void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const uint32_t configType) { + meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - meshtastic_DeviceConnectionStatus conn = meshtastic_DeviceConnectionStatus_init_zero; + if (req.decoded.want_response) { + switch (configType) { + case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: + LOG_INFO("Get module config: MQTT"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; + res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt; + break; + case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: + LOG_INFO("Get module config: Serial"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag; + res.get_module_config_response.payload_variant.serial = moduleConfig.serial; + break; + case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: + LOG_INFO("Get module config: External Notification"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; + res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification; + break; + case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: + LOG_INFO("Get module config: Store & Forward"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; + res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward; + break; + case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: + LOG_INFO("Get module config: Range Test"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; + res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test; + break; + case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: + LOG_INFO("Get module config: Telemetry"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; + res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry; + break; + case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: + LOG_INFO("Get module config: Canned Message"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; + res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; + break; + case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: + LOG_INFO("Get module config: Audio"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; + res.get_module_config_response.payload_variant.audio = moduleConfig.audio; + break; + case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: + LOG_INFO("Get module config: Remote Hardware"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; + res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware; + break; + case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: + LOG_INFO("Get module config: Neighbor Info"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; + res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; + break; + case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: + LOG_INFO("Get module config: Detection Sensor"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; + res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor; + break; + case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: + LOG_INFO("Get module config: Ambient Lighting"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; + res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; + break; + case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: + LOG_INFO("Get module config: Paxcounter"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; + res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; + break; + } + + // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. + // So even if we internally use 0 to represent 'use default' we still need to send the value we are + // using to the app (so that even old phone apps work with new device loads). + // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; + res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; + setPassKey(&res); + myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } + } +} + +void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req) { + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.which_payload_variant = meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag; + for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { + if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { + continue; + } + r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i] = devicestate.node_remote_hardware_pins[i]; + } + for (uint8_t i = 0; i < moduleConfig.remote_hardware.available_pins_count; i++) { + if (!moduleConfig.remote_hardware.available_pins[i].gpio_pin) { + continue; + } + meshtastic_NodeRemoteHardwarePin nodePin = meshtastic_NodeRemoteHardwarePin_init_default; + nodePin.node_num = nodeDB->getNodeNum(); + nodePin.pin = moduleConfig.remote_hardware.available_pins[i]; + r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i + 12] = nodePin; + } + setPassKey(&r); + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } +} + +void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) { + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.get_device_metadata_response = getDeviceMetadata(); + r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; + setPassKey(&r); + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } +} + +void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) { + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + + meshtastic_DeviceConnectionStatus conn = meshtastic_DeviceConnectionStatus_init_zero; #if HAS_WIFI - conn.has_wifi = true; - conn.wifi.has_status = true; + conn.has_wifi = true; + conn.wifi.has_status = true; #ifdef ARCH_PORTDUINO - conn.wifi.status.is_connected = true; + conn.wifi.status.is_connected = true; #else - conn.wifi.status.is_connected = WiFi.status() == WL_CONNECTED; + conn.wifi.status.is_connected = WiFi.status() == WL_CONNECTED; #endif - strncpy(conn.wifi.ssid, config.network.wifi_ssid, 33); - if (conn.wifi.status.is_connected) { - conn.wifi.rssi = WiFi.RSSI(); - conn.wifi.status.ip_address = WiFi.localIP(); + strncpy(conn.wifi.ssid, config.network.wifi_ssid, 33); + if (conn.wifi.status.is_connected) { + conn.wifi.rssi = WiFi.RSSI(); + conn.wifi.status.ip_address = WiFi.localIP(); #ifndef MESHTASTIC_EXCLUDE_MQTT - conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); + conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); #endif - conn.wifi.status.is_syslog_connected = false; // FIXME wire this up - } + conn.wifi.status.is_syslog_connected = false; // FIXME wire this up + } #endif #if HAS_ETHERNET && !defined(USE_WS5500) - conn.has_ethernet = true; - conn.ethernet.has_status = true; - if (Ethernet.linkStatus() == LinkON) { - conn.ethernet.status.is_connected = true; - conn.ethernet.status.ip_address = Ethernet.localIP(); + conn.has_ethernet = true; + conn.ethernet.has_status = true; + if (Ethernet.linkStatus() == LinkON) { + conn.ethernet.status.is_connected = true; + conn.ethernet.status.ip_address = Ethernet.localIP(); #if !MESHTASTIC_EXCLUDE_MQTT - conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); + conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); #endif - conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up - } else { - conn.ethernet.status.is_connected = false; - } + conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up + } else { + conn.ethernet.status.is_connected = false; + } #endif #if HAS_BLUETOOTH - conn.has_bluetooth = true; - conn.bluetooth.pin = config.bluetooth.fixed_pin; + conn.has_bluetooth = true; + conn.bluetooth.pin = config.bluetooth.fixed_pin; #ifdef ARCH_ESP32 - if (config.bluetooth.enabled && nimbleBluetooth) { - conn.bluetooth.is_connected = nimbleBluetooth->isConnected(); - conn.bluetooth.rssi = nimbleBluetooth->getRssi(); - } + if (config.bluetooth.enabled && nimbleBluetooth) { + conn.bluetooth.is_connected = nimbleBluetooth->isConnected(); + conn.bluetooth.rssi = nimbleBluetooth->getRssi(); + } #elif defined(ARCH_NRF52) - if (config.bluetooth.enabled && nrf52Bluetooth) { - conn.bluetooth.is_connected = nrf52Bluetooth->isConnected(); - } + if (config.bluetooth.enabled && nrf52Bluetooth) { + conn.bluetooth.is_connected = nrf52Bluetooth->isConnected(); + } #endif #endif - conn.has_serial = true; // No serial-less devices + conn.has_serial = true; // No serial-less devices #if !MESHTASTIC_EXCLUDE_POWER_FSM - conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; + conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; #else - conn.serial.is_connected = powerFSM.getState(); + conn.serial.is_connected = powerFSM.getState(); #endif - conn.serial.baud = SERIAL_BAUD; + conn.serial.baud = SERIAL_BAUD; - r.get_device_connection_status_response = conn; - r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; + r.get_device_connection_status_response = conn; + r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; + setPassKey(&r); + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } +} + +void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) { + if (req.decoded.want_response) { + // We create the reply here + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.get_channel_response = channels.getByIndex(channelIndex); + r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); if (req.pki_encrypted) { - myReply->pki_encrypted = true; + myReply->pki_encrypted = true; } + } } -void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) -{ - if (req.decoded.want_response) { - // We create the reply here - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.get_channel_response = channels.getByIndex(channelIndex); - r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; - setPassKey(&r); - myReply = allocDataProtobuf(r); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } +void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) { + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; + r.get_ui_config_response = uiconfig; + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } +} + +void AdminModule::reboot(int32_t seconds) { + LOG_INFO("Reboot in %d seconds", seconds); + if (screen) + screen->showSimpleBanner("Rebooting...", 0); // stays on screen + rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); +} + +void AdminModule::saveChanges(int saveWhat, bool shouldReboot) { + if (!hasOpenEditTransaction) { + LOG_INFO("Save changes to disk"); + service->reloadConfig(saveWhat); // Calls saveToDisk among other things + } else { + LOG_INFO("Delay save of changes to disk until the open transaction is committed"); + } + if (shouldReboot && !hasOpenEditTransaction) { + reboot(DEFAULT_REBOOT_SECONDS); + } +} + +void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg) { + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg); +} + +void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { + // Validate ham parameters before setting since this would bypass validation in the owner struct + if (*p.call_sign) { + const char *start = p.call_sign; + // Skip all whitespace + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); + return; } -} - -void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) -{ - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; - r.get_ui_config_response = uiconfig; - myReply = allocDataProtobuf(r); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; + } + if (*p.short_name) { + const char *start = p.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); + return; } + } + + // Set call sign and override lora limitations for licensed use + strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); + strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); + owner.is_licensed = true; + config.lora.override_duty_cycle = true; + config.lora.tx_power = p.tx_power; + config.lora.override_frequency = p.frequency; + // Set node info broadcast interval to 10 minutes + // For FCC minimum call-sign announcement + config.device.node_info_broadcast_secs = 600; + + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + // Remove PSK of primary channel for plaintext amateur usage + + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } + channels.onConfigChanged(); + + service->reloadOwner(false); + saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); } -void AdminModule::reboot(int32_t seconds) -{ - LOG_INFO("Reboot in %d seconds", seconds); - if (screen) - screen->showSimpleBanner("Rebooting...", 0); // stays on screen - rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); +AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg) { + // restrict to the admin channel for rx + // boundChannel = Channels::adminChannel; } -void AdminModule::saveChanges(int saveWhat, bool shouldReboot) -{ - if (!hasOpenEditTransaction) { - LOG_INFO("Save changes to disk"); - service->reloadConfig(saveWhat); // Calls saveToDisk among other things - } else { - LOG_INFO("Delay save of changes to disk until the open transaction is committed"); - } - if (shouldReboot && !hasOpenEditTransaction) { - reboot(DEFAULT_REBOOT_SECONDS); +void AdminModule::setPassKey(meshtastic_AdminMessage *res) { + if (session_time == 0 || millis() / 1000 > session_time + 150) { + for (int i = 0; i < 8; i++) { + session_passkey[i] = random(); } + session_time = millis() / 1000; + } + memcpy(res->session_passkey.bytes, session_passkey, 8); + res->session_passkey.size = 8; + printBytes("Set admin key to ", res->session_passkey.bytes, 8); + // if halfway to session_expire, regenerate session_passkey, reset the timeout + // set the key in the packet } -void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg) -{ - nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg); +bool AdminModule::checkPassKey(meshtastic_AdminMessage *res) { // check that the key in the packet is still valid + printBytes("Incoming session key: ", res->session_passkey.bytes, 8); + printBytes("Expected session key: ", session_passkey, 8); + return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 && memcmp(res->session_passkey.bytes, session_passkey, 8) == 0); } -void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) -{ - // Validate ham parameters before setting since this would bypass validation in the owner struct - if (*p.call_sign) { - const char *start = p.call_sign; - // Skip all whitespace - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); - return; - } - } - if (*p.short_name) { - const char *start = p.short_name; - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); - return; - } - } - - // Set call sign and override lora limitations for licensed use - strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); - strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); - owner.is_licensed = true; - config.lora.override_duty_cycle = true; - config.lora.tx_power = p.tx_power; - config.lora.override_frequency = p.frequency; - // Set node info broadcast interval to 10 minutes - // For FCC minimum call-sign announcement - config.device.node_info_broadcast_secs = 600; - - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - // Remove PSK of primary channel for plaintext amateur usage - - if (channels.ensureLicensedOperation()) { - sendWarning(licensedModeMessage); - } - channels.onConfigChanged(); - - service->reloadOwner(false); - saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); +bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r) { + if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_owner_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_config_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_response_tag) + return true; + else + return false; } -AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg) -{ - // restrict to the admin channel for rx - // boundChannel = Channels::adminChannel; +bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) { + if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_owner_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_config_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag) + return true; + else + return false; } -void AdminModule::setPassKey(meshtastic_AdminMessage *res) -{ - if (session_time == 0 || millis() / 1000 > session_time + 150) { - for (int i = 0; i < 8; i++) { - session_passkey[i] = random(); - } - session_time = millis() / 1000; - } - memcpy(res->session_passkey.bytes, session_passkey, 8); - res->session_passkey.size = 8; - printBytes("Set admin key to ", res->session_passkey.bytes, 8); - // if halfway to session_expire, regenerate session_passkey, reset the timeout - // set the key in the packet -} +void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) { + LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, inputEvent.kb_char, + inputEvent.touch_x, inputEvent.touch_y); -bool AdminModule::checkPassKey(meshtastic_AdminMessage *res) -{ // check that the key in the packet is still valid - printBytes("Incoming session key: ", res->session_passkey.bytes, 8); - printBytes("Expected session key: ", session_passkey, 8); - return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 && - memcmp(res->session_passkey.bytes, session_passkey, 8) == 0); -} + // Create InputEvent for injection + InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, + .kbchar = (unsigned char)inputEvent.kb_char, + .touchX = inputEvent.touch_x, + .touchY = inputEvent.touch_y}; -bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r) -{ - if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_owner_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_config_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_response_tag) - return true; - else - return false; -} + // Log the event being injected + LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, + (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); -bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) -{ - if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_owner_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_config_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag) - return true; - else - return false; -} - -void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) -{ - LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, - inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y); - - // Create InputEvent for injection - InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, - .kbchar = (unsigned char)inputEvent.kb_char, - .touchX = inputEvent.touch_x, - .touchY = inputEvent.touch_y}; - - // Log the event being injected - LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, - (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); - - // Wake the device if asleep - powerFSM.trigger(EVENT_INPUT); + // Wake the device if asleep + powerFSM.trigger(EVENT_INPUT); #if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) - // Inject the event through InputBroker - if (inputBroker) { - inputBroker->injectInputEvent(&event); - } else { - LOG_ERROR("InputBroker not available for event injection"); - } + // Inject the event through InputBroker + if (inputBroker) { + inputBroker->injectInputEvent(&event); + } else { + LOG_ERROR("InputBroker not available for event injection"); + } #endif } -void AdminModule::sendWarning(const char *message) -{ - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, message, sizeof(cn->message)); - service->sendClientNotification(cn); +void AdminModule::sendWarning(const char *message) { + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, message, sizeof(cn->message)); + service->sendClientNotification(cn); } -void disableBluetooth() -{ +void disableBluetooth() { #if HAS_BLUETOOTH #ifdef ARCH_ESP32 - if (nimbleBluetooth) - nimbleBluetooth->deinit(); + if (nimbleBluetooth) + nimbleBluetooth->deinit(); #elif defined(ARCH_NRF52) - if (nrf52Bluetooth) - nrf52Bluetooth->shutdown(); + if (nrf52Bluetooth) + nrf52Bluetooth->shutdown(); #endif #endif } diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 867751f49..b73eb4035 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -10,72 +10,70 @@ * Datatype passed to Observers by AdminModule, to allow external handling of admin messages */ struct AdminModule_ObserverData { - const meshtastic_AdminMessage *request; - meshtastic_AdminMessage *response; - AdminMessageHandleResult *result; + const meshtastic_AdminMessage *request; + meshtastic_AdminMessage *response; + AdminMessageHandleResult *result; }; /** * Admin module for admin messages */ -class AdminModule : public ProtobufModule, public Observable -{ - public: - /** Constructor - * name is for debugging output - */ - AdminModule(); +class AdminModule : public ProtobufModule, public Observable { +public: + /** Constructor + * name is for debugging output + */ + AdminModule(); - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *p) override; - private: - bool hasOpenEditTransaction = false; +private: + bool hasOpenEditTransaction = false; - uint8_t session_passkey[8] = {0}; - uint session_time = 0; + uint8_t session_passkey[8] = {0}; + uint session_time = 0; - void saveChanges(int saveWhat, bool shouldReboot = true); + void saveChanges(int saveWhat, bool shouldReboot = true); - /** - * Getters - */ - void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p); - void handleGetOwner(const meshtastic_MeshPacket &req); - void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType); - void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType); - void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex); - void handleGetDeviceMetadata(const meshtastic_MeshPacket &req); - void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req); - void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req); - void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req); - /** - * Setters - */ - void handleSetOwner(const meshtastic_User &o); - void handleSetChannel(const meshtastic_Channel &cc); - void handleSetConfig(const meshtastic_Config &c); - bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); - void handleSetChannel(); - void handleSetHamMode(const meshtastic_HamParameters &req); - void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); - void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); - void reboot(int32_t seconds); + /** + * Getters + */ + void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p); + void handleGetOwner(const meshtastic_MeshPacket &req); + void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType); + void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType); + void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex); + void handleGetDeviceMetadata(const meshtastic_MeshPacket &req); + void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req); + void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req); + void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req); + /** + * Setters + */ + void handleSetOwner(const meshtastic_User &o); + void handleSetChannel(const meshtastic_Channel &cc); + void handleSetConfig(const meshtastic_Config &c); + bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); + void handleSetChannel(); + void handleSetHamMode(const meshtastic_HamParameters &req); + void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); + void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); + void reboot(int32_t seconds); - void setPassKey(meshtastic_AdminMessage *res); - bool checkPassKey(meshtastic_AdminMessage *res); + void setPassKey(meshtastic_AdminMessage *res); + bool checkPassKey(meshtastic_AdminMessage *res); - bool messageIsResponse(const meshtastic_AdminMessage *r); - bool messageIsRequest(const meshtastic_AdminMessage *r); - void sendWarning(const char *message); + bool messageIsResponse(const meshtastic_AdminMessage *r); + bool messageIsRequest(const meshtastic_AdminMessage *r); + void sendWarning(const char *message); }; -static constexpr const char *licensedModeMessage = - "Licensed mode activated, removing admin channel and encryption from all channels"; +static constexpr const char *licensedModeMessage = "Licensed mode activated, removing admin channel and encryption from all channels"; extern AdminModule *adminModule; diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp index a51ef54c3..b77e40a78 100644 --- a/src/modules/AtakPluginModule.cpp +++ b/src/modules/AtakPluginModule.cpp @@ -11,188 +11,172 @@ AtakPluginModule *atakPluginModule; AtakPluginModule::AtakPluginModule() - : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin") -{ - ourPortNum = meshtastic_PortNum_ATAK_PLUGIN; + : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin") { + ourPortNum = meshtastic_PortNum_ATAK_PLUGIN; } /* Encompasses the full construction and sending packet to mesh Will be used for broadcast. */ -int32_t AtakPluginModule::runOnce() -{ - return default_broadcast_interval_secs; +int32_t AtakPluginModule::runOnce() { return default_broadcast_interval_secs; } + +bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r) { return false; } + +meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t) { + meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero; + if (t->has_group) { + clone.has_group = true; + clone.group = t->group; + } + if (t->has_status) { + clone.has_status = true; + clone.status = t->status; + } + if (t->has_contact) { + clone.has_contact = true; + clone.contact = {0}; + } + + if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_pli_tag; + clone.payload_variant.pli = t->payload_variant.pli; + } else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_chat_tag; + clone.payload_variant.chat = {0}; + } else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_detail_tag; + clone.payload_variant.detail.size = t->payload_variant.detail.size; + memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size); + } + + return clone; } -bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r) -{ - return false; -} - -meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t) -{ - meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero; - if (t->has_group) { - clone.has_group = true; - clone.group = t->group; - } - if (t->has_status) { - clone.has_status = true; - clone.status = t->status; - } +void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) { + // From Phone (EUD) + if (mp.from == 0) { + LOG_DEBUG("Received uncompressed TAK payload from phone: %d bytes", mp.decoded.payload.size); + // Compress for LoRA transport + auto compressed = cloneTAKPacketData(t); + compressed.is_compressed = true; if (t->has_contact) { - clone.has_contact = true; - clone.contact = {0}; + auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign, + sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed callsign: %d bytes", length); + length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), compressed.contact.device_callsign, + sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed device_callsign: %d bytes", length); + } + if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { + auto length = + unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), compressed.payload_variant.chat.message, + sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed chat message: %d bytes", length); + + if (t->payload_variant.chat.has_to) { + compressed.payload_variant.chat.has_to = true; + length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), compressed.payload_variant.chat.to, + sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed chat to: %d bytes", length); + } + + if (t->payload_variant.chat.has_to_callsign) { + compressed.payload_variant.chat.has_to_callsign = true; + length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), + compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1, + USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed chat to_callsign: %d bytes", length); + } + } + mp.decoded.payload.size = + pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), meshtastic_TAKPacket_fields, &compressed); + LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size); + } else { + if (!t->is_compressed) { + // Not compressed. Something is wrong + LOG_WARN("Received uncompressed TAKPacket over radio! Skip"); + return; } - if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) { - clone.which_payload_variant = meshtastic_TAKPacket_pli_tag; - clone.payload_variant.pli = t->payload_variant.pli; - } else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { - clone.which_payload_variant = meshtastic_TAKPacket_chat_tag; - clone.payload_variant.chat = {0}; - } else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) { - clone.which_payload_variant = meshtastic_TAKPacket_detail_tag; - clone.payload_variant.detail.size = t->payload_variant.detail.size; - memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size); + // Decompress for Phone (EUD) + auto uncompressed = cloneTAKPacketData(t); + uncompressed.is_compressed = false; + if (t->has_contact) { + auto length = unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign, + sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow contact.callsign. Bailing out"); + return; + } + LOG_DEBUG("Decompressed callsign: %d bytes", length); + + length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), uncompressed.contact.device_callsign, + sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); + return; + } + LOG_DEBUG("Decompressed device_callsign: %d bytes", length); } + if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { + auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), + uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, + USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow chat.message. Bailing out"); + return; + } + LOG_DEBUG("Decompressed chat message: %d bytes", length); - return clone; -} - -void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) -{ - // From Phone (EUD) - if (mp.from == 0) { - LOG_DEBUG("Received uncompressed TAK payload from phone: %d bytes", mp.decoded.payload.size); - // Compress for LoRA transport - auto compressed = cloneTAKPacketData(t); - compressed.is_compressed = true; - if (t->has_contact) { - auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign, - sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed callsign: %d bytes", length); - length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), - compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, - USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed device_callsign: %d bytes", length); + if (t->payload_variant.chat.has_to) { + uncompressed.payload_variant.chat.has_to = true; + length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), uncompressed.payload_variant.chat.to, + sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow chat.to. Bailing out"); + return; } - if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - compressed.payload_variant.chat.message, - sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed chat message: %d bytes", length); + LOG_DEBUG("Decompressed chat to: %d bytes", length); + } - if (t->payload_variant.chat.has_to) { - compressed.payload_variant.chat.has_to = true; - length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), - compressed.payload_variant.chat.to, - sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed chat to: %d bytes", length); - } - - if (t->payload_variant.chat.has_to_callsign) { - compressed.payload_variant.chat.has_to_callsign = true; - length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - compressed.payload_variant.chat.to_callsign, - sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed chat to_callsign: %d bytes", length); - } + if (t->payload_variant.chat.has_to_callsign) { + uncompressed.payload_variant.chat.has_to_callsign = true; + length = unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), + uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, + USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); + return; } - mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), - meshtastic_TAKPacket_fields, &compressed); - LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size); - } else { - if (!t->is_compressed) { - // Not compressed. Something is wrong - LOG_WARN("Received uncompressed TAKPacket over radio! Skip"); - return; - } - - // Decompress for Phone (EUD) - auto uncompressed = cloneTAKPacketData(t); - uncompressed.is_compressed = false; - if (t->has_contact) { - auto length = - unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign, - sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow contact.callsign. Bailing out"); - return; - } - LOG_DEBUG("Decompressed callsign: %d bytes", length); - - length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), - uncompressed.contact.device_callsign, - sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); - return; - } - LOG_DEBUG("Decompressed device_callsign: %d bytes", length); - } - if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - uncompressed.payload_variant.chat.message, - sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow chat.message. Bailing out"); - return; - } - LOG_DEBUG("Decompressed chat message: %d bytes", length); - - if (t->payload_variant.chat.has_to) { - uncompressed.payload_variant.chat.has_to = true; - length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), - uncompressed.payload_variant.chat.to, - sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow chat.to. Bailing out"); - return; - } - LOG_DEBUG("Decompressed chat to: %d bytes", length); - } - - if (t->payload_variant.chat.has_to_callsign) { - uncompressed.payload_variant.chat.has_to_callsign = true; - length = - unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - uncompressed.payload_variant.chat.to_callsign, - sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); - return; - } - LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length); - } - } - auto decompressedCopy = packetPool.allocCopy(mp); - decompressedCopy->decoded.payload.size = - pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload), - meshtastic_TAKPacket_fields, &uncompressed); - - service->sendToPhone(decompressedCopy); + LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length); + } } - return; + auto decompressedCopy = packetPool.allocCopy(mp); + decompressedCopy->decoded.payload.size = pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload), + meshtastic_TAKPacket_fields, &uncompressed); + + service->sendToPhone(decompressedCopy); + } + return; } \ No newline at end of file diff --git a/src/modules/AtakPluginModule.h b/src/modules/AtakPluginModule.h index 0470a3b32..35c9cb172 100644 --- a/src/modules/AtakPluginModule.h +++ b/src/modules/AtakPluginModule.h @@ -5,22 +5,21 @@ /** * Waypoint message handling for meshtastic */ -class AtakPluginModule : public ProtobufModule, private concurrency::OSThread -{ - public: - /** Constructor - * name is for debugging output - */ - AtakPluginModule(); +class AtakPluginModule : public ProtobufModule, private concurrency::OSThread { +public: + /** Constructor + * name is for debugging output + */ + AtakPluginModule(); - protected: - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; - /* Does our periodic broadcast */ - int32_t runOnce() override; +protected: + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; + /* Does our periodic broadcast */ + int32_t runOnce() override; - private: - meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t); +private: + meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t); }; extern AtakPluginModule *atakPluginModule; \ No newline at end of file diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 8d1ba6346..43e80677e 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -46,72 +46,69 @@ extern MessageStore messageStore; #define INACTIVATE_AFTER_MS 20000 // Tokenize a message string into emote/text segments -static std::vector> tokenizeMessageWithEmotes(const char *msg) -{ - std::vector> tokens; - int msgLen = strlen(msg); - int pos = 0; - while (pos < msgLen) { - const graphics::Emote *foundEmote = nullptr; - int foundLen = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - int labelLen = strlen(label); - if (labelLen == 0) - continue; - if (strncmp(msg + pos, label, labelLen) == 0) { - if (!foundEmote || labelLen > foundLen) { - foundEmote = &graphics::emotes[j]; - foundLen = labelLen; - } - } - } - if (foundEmote) { - tokens.emplace_back(true, String(foundEmote->label)); - pos += foundLen; - } else { - // Find next emote - int nextEmote = msgLen; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *found = strstr(msg + pos, label); - if (found && (found - msg) < nextEmote) { - nextEmote = found - msg; - } - } - int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); - if (textLen > 0) { - tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); - pos += textLen; - } else { - break; - } +static std::vector> tokenizeMessageWithEmotes(const char *msg) { + std::vector> tokens; + int msgLen = strlen(msg); + int pos = 0; + while (pos < msgLen) { + const graphics::Emote *foundEmote = nullptr; + int foundLen = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) + continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; } + } } - return tokens; + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; + } + } + } + return tokens; } // Render a single emote token centered vertically on a row -static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) -{ - const graphics::Emote *emote = nullptr; - for (int j = 0; j < graphics::numEmotes; j++) { - if (label == graphics::emotes[j].label) { - emote = &graphics::emotes[j]; - break; - } - } - if (emote) { - int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote - display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; // spacing between tokens +static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) { + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (label == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote + display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; // spacing between tokens + } } -namespace graphics -{ +namespace graphics { extern int bannerSignalBars; } extern ScanI2C::DeviceAddress cardkb_found; @@ -126,89 +123,80 @@ meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; CannedMessageModule *cannedMessageModule; -CannedMessageModule::CannedMessageModule() - : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") -{ - this->loadProtoForModule(); - if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && - !CANNED_MESSAGE_MODULE_ENABLE) { - LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); - this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; - disable(); - } else { - LOG_INFO("CannedMessageModule is enabled"); - moduleConfig.canned_message.enabled = true; - this->inputObserver.observe(inputBroker); - } +CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { + this->loadProtoForModule(); + if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && !CANNED_MESSAGE_MODULE_ENABLE) { + LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); + this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; + disable(); + } else { + LOG_INFO("CannedMessageModule is enabled"); + moduleConfig.canned_message.enabled = true; + this->inputObserver.observe(inputBroker); + } } -void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) -{ - // Do NOT override explicit broadcast replies - // Only reuse lastDest in LaunchRepeatDestination() +void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) { + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() - dest = newDest; - channel = newChannel; + dest = newDest; + channel = newChannel; - lastDest = dest; - lastChannel = channel; - lastDestSet = true; + lastDest = dest; + lastChannel = channel; + lastDestSet = true; - // Upon activation, highlight "[Select Destination]" - int selectDestination = 0; - for (int i = 0; i < messagesCount; ++i) { - if (strcmp(messages[i], "[Select Destination]") == 0) { - selectDestination = i; - break; - } + // Upon activation, highlight "[Select Destination]" + int selectDestination = 0; + for (int i = 0; i < messagesCount; ++i) { + if (strcmp(messages[i], "[Select Destination]") == 0) { + selectDestination = i; + break; } - currentMessageIndex = selectDestination; + } + currentMessageIndex = selectDestination; - // This triggers the canned message list - runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); + // This triggers the canned message list + runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); - LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); + LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); } -void CannedMessageModule::LaunchRepeatDestination() -{ - if (!lastDestSet) { - LaunchWithDestination(NODENUM_BROADCAST, 0); - } else { - LaunchWithDestination(lastDest, lastChannel); - } +void CannedMessageModule::LaunchRepeatDestination() { + if (!lastDestSet) { + LaunchWithDestination(NODENUM_BROADCAST, 0); + } else { + LaunchWithDestination(lastDest, lastChannel); + } } -void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) -{ - // Do NOT override explicit broadcast replies - // Only reuse lastDest in LaunchRepeatDestination() +void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) { + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() - dest = newDest; - channel = newChannel; + dest = newDest; + channel = newChannel; - lastDest = dest; - lastChannel = channel; - lastDestSet = true; + lastDest = dest; + lastChannel = channel; + lastDestSet = true; - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); - LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); + LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); } static bool returnToCannedList = false; -bool hasKeyForNode(const meshtastic_NodeInfoLite *node) -{ - return node && node->has_user && node->user.public_key.size > 0; -} +bool hasKeyForNode(const meshtastic_NodeInfoLite *node) { return node && node->has_user && node->user.public_key.size > 0; } /** * @brief Items in array this->messages will be set to be pointing on the right * starting points of the string this->messageStore @@ -216,2154 +204,2084 @@ bool hasKeyForNode(const meshtastic_NodeInfoLite *node) * @return int Returns the number of messages found. */ -int CannedMessageModule::splitConfiguredMessages() -{ - int i = 0; +int CannedMessageModule::splitConfiguredMessages() { + int i = 0; - String canned_messages = cannedMessageModuleConfig.messages; + String canned_messages = cannedMessageModuleConfig.messages; - // Copy all message parts into the buffer - strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); + // Copy all message parts into the buffer + strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); - // Temporary array to allow for insertion - const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; - int tempCount = 0; - // Insert at position 0 (top) - tempMessages[tempCount++] = "[Select Destination]"; + // Temporary array to allow for insertion + const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; + int tempCount = 0; + // Insert at position 0 (top) + tempMessages[tempCount++] = "[Select Destination]"; #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a touch screen virtual keyboard - tempMessages[tempCount++] = "[-- Free Text --]"; + // Add a "Free Text" entry at the top if using a touch screen virtual keyboard + tempMessages[tempCount++] = "[-- Free Text --]"; #else - if (osk_found && screen) { - tempMessages[tempCount++] = "[-- Free Text --]"; - } + if (osk_found && screen) { + tempMessages[tempCount++] = "[-- Free Text --]"; + } #endif - // First message always starts at buffer start - tempMessages[tempCount++] = this->messageBuffer; - int upTo = strlen(this->messageBuffer) - 1; + // First message always starts at buffer start + tempMessages[tempCount++] = this->messageBuffer; + int upTo = strlen(this->messageBuffer) - 1; - // Walk buffer, splitting on '|' - while (i < upTo) { - if (this->messageBuffer[i] == '|') { - this->messageBuffer[i] = '\0'; // End previous message - if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) - break; - tempMessages[tempCount++] = (this->messageBuffer + i + 1); - } - i += 1; + // Walk buffer, splitting on '|' + while (i < upTo) { + if (this->messageBuffer[i] == '|') { + this->messageBuffer[i] = '\0'; // End previous message + if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) + break; + tempMessages[tempCount++] = (this->messageBuffer + i + 1); } + i += 1; + } - // Add [Exit] as the last entry - tempMessages[tempCount++] = "[Exit]"; + // Add [Exit] as the last entry + tempMessages[tempCount++] = "[Exit]"; - // Copy to the member array - for (int k = 0; k < tempCount; ++k) { - this->messages[k] = (char *)tempMessages[k]; - } - this->messagesCount = tempCount; + // Copy to the member array + for (int k = 0; k < tempCount; ++k) { + this->messages[k] = (char *)tempMessages[k]; + } + this->messagesCount = tempCount; - return this->messagesCount; + return this->messagesCount; } -void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) -{ - if (graphics::currentResolution == graphics::ScreenResolution::High) { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } +void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { + if (graphics::currentResolution == graphics::ScreenResolution::High) { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); } else { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } + display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); } + } else { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); + } + } } -void CannedMessageModule::resetSearch() -{ - int previousDestIndex = destIndex; +void CannedMessageModule::resetSearch() { + int previousDestIndex = destIndex; - searchQuery = ""; - updateDestinationSelectionList(); + searchQuery = ""; + updateDestinationSelectionList(); - // Adjust scrollIndex so previousDestIndex is still visible - int totalEntries = activeChannelIndices.size() + filteredNodes.size(); - this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; - if (this->visibleRows < 1) - this->visibleRows = 1; - int maxScrollIndex = std::max(0, totalEntries - visibleRows); - scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); + // Adjust scrollIndex so previousDestIndex is still visible + int totalEntries = activeChannelIndices.size() + filteredNodes.size(); + this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; + if (this->visibleRows < 1) + this->visibleRows = 1; + int maxScrollIndex = std::max(0, totalEntries - visibleRows); + scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); - lastUpdateMillis = millis(); - requestFocus(); + lastUpdateMillis = millis(); + requestFocus(); } -void CannedMessageModule::updateDestinationSelectionList() -{ - static size_t lastNumMeshNodes = 0; - static String lastSearchQuery = ""; +void CannedMessageModule::updateDestinationSelectionList() { + static size_t lastNumMeshNodes = 0; + static String lastSearchQuery = ""; - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - bool nodesChanged = (numMeshNodes != lastNumMeshNodes); - lastNumMeshNodes = numMeshNodes; + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + bool nodesChanged = (numMeshNodes != lastNumMeshNodes); + lastNumMeshNodes = numMeshNodes; - // Early exit if nothing changed - if (searchQuery == lastSearchQuery && !nodesChanged) - return; - lastSearchQuery = searchQuery; - needsUpdate = false; + // Early exit if nothing changed + if (searchQuery == lastSearchQuery && !nodesChanged) + return; + lastSearchQuery = searchQuery; + needsUpdate = false; - this->filteredNodes.clear(); - this->activeChannelIndices.clear(); + this->filteredNodes.clear(); + this->activeChannelIndices.clear(); - NodeNum myNodeNum = nodeDB->getNodeNum(); - String lowerSearchQuery = searchQuery; - lowerSearchQuery.toLowerCase(); + NodeNum myNodeNum = nodeDB->getNodeNum(); + String lowerSearchQuery = searchQuery; + lowerSearchQuery.toLowerCase(); - // Preallocate space to reduce reallocation - this->filteredNodes.reserve(numMeshNodes); + // Preallocate space to reduce reallocation + this->filteredNodes.reserve(numMeshNodes); - for (size_t i = 0; i < numMeshNodes; ++i) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) - continue; + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) + continue; - const String &nodeName = node->user.long_name; + const String &nodeName = node->user.long_name; - if (searchQuery.length() == 0) { - this->filteredNodes.push_back({node, sinceLastSeen(node)}); - } else { - // Avoid unnecessary lowercase conversion if already matched - String lowerNodeName = nodeName; - lowerNodeName.toLowerCase(); + if (searchQuery.length() == 0) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } else { + // Avoid unnecessary lowercase conversion if already matched + String lowerNodeName = nodeName; + lowerNodeName.toLowerCase(); - if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { - this->filteredNodes.push_back({node, sinceLastSeen(node)}); - } - } + if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } } + } - meshtastic_MeshPacket *p = allocDataPacket(); - p->pki_encrypted = true; - p->channel = 0; + meshtastic_MeshPacket *p = allocDataPacket(); + p->pki_encrypted = true; + p->channel = 0; - // Populate active channels - std::vector seenChannels; - seenChannels.reserve(channels.getNumChannels()); - for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { - String name = channels.getName(i); - if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { - this->activeChannelIndices.push_back(i); - seenChannels.push_back(name); - } + // Populate active channels + std::vector seenChannels; + seenChannels.reserve(channels.getNumChannels()); + for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { + String name = channels.getName(i); + if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { + this->activeChannelIndices.push_back(i); + seenChannels.push_back(name); } + } - scrollIndex = 0; // Show first result at the top - destIndex = 0; // Highlight the first entry - if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - LOG_INFO("Nodes changed, forcing UI refresh."); - screen->forceDisplay(); - } + scrollIndex = 0; // Show first result at the top + destIndex = 0; // Highlight the first entry + if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + LOG_INFO("Nodes changed, forcing UI refresh."); + screen->forceDisplay(); + } } // Returns true if character input is currently allowed (used for search/freetext states) -bool CannedMessageModule::isCharInputAllowed() const -{ - return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; +bool CannedMessageModule::isCharInputAllowed() const { + return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; } /** * Main input event dispatcher for CannedMessageModule. * Routes keyboard/button/touch input to the correct handler based on the current runState. * Only one handler (per state) processes each event, eliminating redundancy. */ -int CannedMessageModule::handleInputEvent(const InputEvent *event) -{ - // Block ALL input if an alert banner is active - if (screen && screen->isOverlayBannerShowing()) { - return 0; - } - - // Tab key: Always allow switching between canned/destination screens - if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) - return 1; - - // Matrix keypad: If matrix key, trigger action select for canned message - if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - payload = INPUT_BROKER_MATRIXKEY; - currentMessageIndex = event->kbchar - 1; - lastTouchMillis = millis(); - requestFocus(); - return 1; - } - - // Always normalize navigation/select buttons for further handlers - bool isUp = isUpEvent(event); - bool isDown = isDownEvent(event); - bool isSelect = isSelectEvent(event); - - // Route event to handler for current UI state (no double-handling) - switch (runState) { - // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace - case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: - if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) - return 1; - return 0; // prevent fall-through to selector input - - // Free text input mode: Handles character input, cancel, backspace, select, etc. - case CANNED_MESSAGE_RUN_STATE_FREETEXT: - return handleFreeTextInput(event); // All allowed input for this state - - // Virtual keyboard mode: Show virtual keyboard and handle input - - // If sending, block all input except global/system (handled above) - case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: - return 1; - - // If sending, block all input except global/system (handled above) - case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: - return handleEmotePickerInput(event); - - case CANNED_MESSAGE_RUN_STATE_INACTIVE: - if (event->inputEvent == INPUT_BROKER_ALT_LONG) { - LaunchWithDestination(NODENUM_BROADCAST); - return 1; - } - // Printable char (ASCII) opens free text compose - if (event->kbchar >= 32 && event->kbchar <= 126) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - // Immediately process the input in the new state (freetext) - return handleFreeTextInput(event); - } - return 0; - break; - - // (Other states can be added here as needed) - default: - break; - } - - // If no state handler above processed the event, let the message selector try to handle it - // (Handles up/down/select on canned message list, exit/return) - if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) - return 1; - - // Default: event not handled by canned message system, allow others to process +int CannedMessageModule::handleInputEvent(const InputEvent *event) { + // Block ALL input if an alert banner is active + if (screen && screen->isOverlayBannerShowing()) { return 0; -} + } -bool CannedMessageModule::isUpEvent(const InputEvent *event) -{ - return event->inputEvent == INPUT_BROKER_UP || - ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); -} -bool CannedMessageModule::isDownEvent(const InputEvent *event) -{ - return event->inputEvent == INPUT_BROKER_DOWN || - ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); -} -bool CannedMessageModule::isSelectEvent(const InputEvent *event) -{ - return event->inputEvent == INPUT_BROKER_SELECT; -} + // Tab key: Always allow switching between canned/destination screens + if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) + return 1; -bool CannedMessageModule::handleTabSwitch(const InputEvent *event) -{ - if (event->kbchar != 0x09) - return false; - - runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - - destIndex = 0; - scrollIndex = 0; - // RESTORE THIS! - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) - updateDestinationSelectionList(); + // Matrix keypad: If matrix key, trigger action select for canned message + if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + payload = INPUT_BROKER_MATRIXKEY; + currentMessageIndex = event->kbchar - 1; + lastTouchMillis = millis(); requestFocus(); + return 1; + } + // Always normalize navigation/select buttons for further handlers + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + + // Route event to handler for current UI state (no double-handling) + switch (runState) { + // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace + case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: + if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) + return 1; + return 0; // prevent fall-through to selector input + + // Free text input mode: Handles character input, cancel, backspace, select, etc. + case CANNED_MESSAGE_RUN_STATE_FREETEXT: + return handleFreeTextInput(event); // All allowed input for this state + + // Virtual keyboard mode: Show virtual keyboard and handle input + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: + return 1; + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: + return handleEmotePickerInput(event); + + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + if (event->inputEvent == INPUT_BROKER_ALT_LONG) { + LaunchWithDestination(NODENUM_BROADCAST); + return 1; + } + // Printable char (ASCII) opens free text compose + if (event->kbchar >= 32 && event->kbchar <= 126) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Immediately process the input in the new state (freetext) + return handleFreeTextInput(event); + } + return 0; + break; + + // (Other states can be added here as needed) + default: + break; + } + + // If no state handler above processed the event, let the message selector try to handle it + // (Handles up/down/select on canned message list, exit/return) + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) + return 1; + + // Default: event not handled by canned message system, allow others to process + return 0; +} + +bool CannedMessageModule::isUpEvent(const InputEvent *event) { + return event->inputEvent == INPUT_BROKER_UP || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); +} +bool CannedMessageModule::isDownEvent(const InputEvent *event) { + return event->inputEvent == INPUT_BROKER_DOWN || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); +} +bool CannedMessageModule::isSelectEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_SELECT; } + +bool CannedMessageModule::handleTabSwitch(const InputEvent *event) { + if (event->kbchar != 0x09) + return false; + + runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + + destIndex = 0; + scrollIndex = 0; + // RESTORE THIS! + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + updateDestinationSelectionList(); + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; +} + +int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { + // Override isDown and isSelect ONLY for destination selector behavior + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && + event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { + this->searchQuery += (char)event->kbchar; + needsUpdate = true; + if ((millis() - lastFilterUpdate) > filterDebounceMs) { + runOnce(); // update filter immediately + lastFilterUpdate = millis(); + } + return 1; + } + + size_t numMeshNodes = filteredNodes.size(); + int totalEntries = numMeshNodes + activeChannelIndices.size(); + int columns = 1; + int totalRows = totalEntries; + int maxScrollIndex = std::max(0, totalRows - visibleRows); + scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); + + // Handle backspace + if (event->inputEvent == INPUT_BROKER_BACK) { + if (searchQuery.length() > 0) { + searchQuery.remove(searchQuery.length() - 1); + needsUpdate = true; + runOnce(); + } + if (searchQuery.length() == 0) { + resetSearch(); + needsUpdate = false; + } + return 1; + } + + if (isUp) { + if (destIndex > 0) { + destIndex--; + } else if (totalEntries > 0) { + destIndex = totalEntries - 1; + } + + if ((destIndex / columns) < scrollIndex) + scrollIndex = destIndex / columns; + else if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(true); + return 1; + } + + if (isDown) { + if (destIndex + 1 < totalEntries) { + destIndex++; + } else if (totalEntries > 0) { + destIndex = 0; + scrollIndex = 0; + } + + if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(true); + return 1; + } + + // SELECT + if (isSelect) { + if (destIndex < static_cast(activeChannelIndices.size())) { + dest = NODENUM_BROADCAST; + channel = activeChannelIndices[destIndex]; + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + } else { + int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); + if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { + const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; + if (selectedNode) { + dest = selectedNode->num; + channel = selectedNode->channel; + // Already saves here, but for clarity, also: + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + } + } + } + + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + screen->forceDisplay(true); + return 1; + } + + // CANCEL + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + searchQuery = ""; + + // UIFrameEvent e; + // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + // notifyObservers(&e); + screen->forceDisplay(true); + return 1; + } + + return 0; +} + +bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { + // Override isDown and isSelect ONLY for canned message list behavior + if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + return false; + + // Handle Cancel key: go inactive, clear UI state + if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; -} + } -int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) -{ - // Override isDown and isSelect ONLY for destination selector behavior - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - if (event->inputEvent == INPUT_BROKER_USER_PRESS) { - isDown = true; - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - isSelect = true; - } + bool handled = false; + + // Handle up/down navigation + if (isUp && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + handled = true; + } else if (isDown && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + handled = true; + } else if (isSelect) { + const char *current = messages[currentMessageIndex]; + + // [Select Destination] triggers destination selection UI + if (strcmp(current, "[Select Destination]") == 0) { + returnToCannedList = true; + runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + destIndex = 0; + scrollIndex = 0; + updateDestinationSelectionList(); // Make sure list is fresh + screen->forceDisplay(); + return true; } - if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && - event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { - this->searchQuery += (char)event->kbchar; - needsUpdate = true; - if ((millis() - lastFilterUpdate) > filterDebounceMs) { - runOnce(); // update filter immediately - lastFilterUpdate = millis(); - } - return 1; + // [Exit] returns to the main/inactive screen + if (strcmp(current, "[Exit]") == 0) { + // Set runState to inactive so we return to main UI + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + currentMessageIndex = -1; + + // Notify UI to regenerate frame set and redraw + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; } - size_t numMeshNodes = filteredNodes.size(); - int totalEntries = numMeshNodes + activeChannelIndices.size(); - int columns = 1; - int totalRows = totalEntries; - int maxScrollIndex = std::max(0, totalRows - visibleRows); - scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); - - // Handle backspace - if (event->inputEvent == INPUT_BROKER_BACK) { - if (searchQuery.length() > 0) { - searchQuery.remove(searchQuery.length() - 1); - needsUpdate = true; - runOnce(); - } - if (searchQuery.length() == 0) { - resetSearch(); - needsUpdate = false; - } - return 1; + // [Free Text] triggers the free text input (virtual keyboard) +#if defined(USE_VIRTUAL_KEYBOARD) + if (strcmp(current, "[-- Free Text --]") == 0) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return true; } - - if (isUp) { - if (destIndex > 0) { - destIndex--; - } else if (totalEntries > 0) { - destIndex = totalEntries - 1; - } - - if ((destIndex / columns) < scrollIndex) - scrollIndex = destIndex / columns; - else if ((destIndex / columns) >= (scrollIndex + visibleRows)) - scrollIndex = (destIndex / columns) - visibleRows + 1; - - screen->forceDisplay(true); - return 1; - } - - if (isDown) { - if (destIndex + 1 < totalEntries) { - destIndex++; - } else if (totalEntries > 0) { - destIndex = 0; - scrollIndex = 0; - } - - if ((destIndex / columns) >= (scrollIndex + visibleRows)) - scrollIndex = (destIndex / columns) - visibleRows + 1; - - screen->forceDisplay(true); - return 1; - } - - // SELECT - if (isSelect) { - if (destIndex < static_cast(activeChannelIndices.size())) { - dest = NODENUM_BROADCAST; - channel = activeChannelIndices[destIndex]; - lastDest = dest; - lastChannel = channel; - lastDestSet = true; +#else + if (strcmp(current, "[-- Free Text --]") == 0) { + if (osk_found && screen) { + char headerBuffer[64]; + if (this->dest == NODENUM_BROADCAST) { + snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); } else { - int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); - if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { - const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; - if (selectedNode) { - dest = selectedNode->num; - channel = selectedNode->channel; - // Already saves here, but for clarity, also: - lastDest = dest; - lastChannel = channel; - lastDestSet = true; - } - } + snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); } - - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; - returnToCannedList = false; - screen->forceDisplay(true); - return 1; - } - - // CANCEL - if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; - returnToCannedList = false; - searchQuery = ""; - - // UIFrameEvent e; - // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - // notifyObservers(&e); - screen->forceDisplay(true); - return 1; - } - - return 0; -} - -bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) -{ - // Override isDown and isSelect ONLY for canned message list behavior - if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - if (event->inputEvent == INPUT_BROKER_USER_PRESS) { - isDown = true; - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - isSelect = true; - } - } - - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) - return false; - - // Handle Cancel key: go inactive, clear UI state - if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && - (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; - payload = 0; - currentMessageIndex = -1; - - // Notify UI that we want to redraw/close this screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return true; - } - - bool handled = false; - - // Handle up/down navigation - if (isUp && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - handled = true; - } else if (isDown && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - handled = true; - } else if (isSelect) { - const char *current = messages[currentMessageIndex]; - - // [Select Destination] triggers destination selection UI - if (strcmp(current, "[Select Destination]") == 0) { - returnToCannedList = true; - runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - destIndex = 0; - scrollIndex = 0; - updateDestinationSelectionList(); // Make sure list is fresh - screen->forceDisplay(); - return true; - } - - // [Exit] returns to the main/inactive screen - if (strcmp(current, "[Exit]") == 0) { - // Set runState to inactive so we return to main UI - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { + if (!text.empty()) { + this->freetext = text.c_str(); + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; currentMessageIndex = -1; - // Notify UI to regenerate frame set and redraw UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); + this->notifyObservers(&e); screen->forceDisplay(); - return true; - } - // [Free Text] triggers the free text input (virtual keyboard) -#if defined(USE_VIRTUAL_KEYBOARD) - if (strcmp(current, "[-- Free Text --]") == 0) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + setIntervalFromNow(500); + return; + } else { + // Don't delete virtual keyboard immediately - it might still be executing + // Instead, just clear the callback and reset banner to stop input processing + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + + // Return to inactive state + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Force display update to show normal screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return true; - } -#else - if (strcmp(current, "[-- Free Text --]") == 0) { - if (osk_found && screen) { - char headerBuffer[64]; - if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); - } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); - } - screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { - if (!text.empty()) { - this->freetext = text.c_str(); - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; - currentMessageIndex = -1; + this->notifyObservers(&e); + screen->forceDisplay(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); + // Schedule cleanup for next loop iteration to ensure safe deletion + setIntervalFromNow(50); + return; + } + }); - setIntervalFromNow(500); - return; - } else { - // Don't delete virtual keyboard immediately - it might still be executing - // Instead, just clear the callback and reset banner to stop input processing - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - - // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Force display update to show normal screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - // Schedule cleanup for next loop iteration to ensure safe deletion - setIntervalFromNow(50); - return; - } - }); - - return true; - } - } + return true; + } + } #endif - // Normal canned message selection - if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { - } else { + // Normal canned message selection + if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + } else { #if CANNED_MESSAGE_ADD_CONFIRMATION - const int savedIndex = currentMessageIndex; - graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { - this->currentMessageIndex = savedIndex; - this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - this->setIntervalFromNow(0); - }); + const int savedIndex = currentMessageIndex; + graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { + this->currentMessageIndex = savedIndex; + this->payload = this->runState; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->setIntervalFromNow(0); + }); #else - payload = runState; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + payload = runState; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; #endif - // Do not immediately set runState; wait for confirmation - handled = true; - } + // Do not immediately set runState; wait for confirmation + handled = true; } + } - if (handled) { - requestFocus(); - if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) - setIntervalFromNow(0); - else - runOnce(); - } + if (handled) { + requestFocus(); + if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) + setIntervalFromNow(0); + else + runOnce(); + } - return handled; + return handled; } -bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) -{ - // Always process only if in FREETEXT mode - if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) - return false; +bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) { + // Always process only if in FREETEXT mode + if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) + return false; #if defined(USE_VIRTUAL_KEYBOARD) - // Cancel (dismiss freetext screen) - if (event->inputEvent == INPUT_BROKER_LEFT) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; - payload = 0; - currentMessageIndex = -1; + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_LEFT) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; - // Notify UI that we want to redraw/close this screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return true; + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + // Touch input (virtual keyboard) handling + // Only handle if touch coordinates present (CardKB won't set these) + if (event->touchX != 0 || event->touchY != 0) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + bool valid = false; + + if (keyTapped == "⇧") { + highlight = -1; + payload = 0x00; + shift = !shift; + valid = true; + } else if (keyTapped == "⌫") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = 0x08; + shift = false; + valid = true; + } else if (keyTapped == "123" || keyTapped == "ABC") { + highlight = -1; + payload = 0x00; + charSet = (charSet == 0 ? 1 : 0); + valid = true; + } else if (keyTapped == " ") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = keyTapped[0]; + shift = false; + valid = true; } - // Touch input (virtual keyboard) handling - // Only handle if touch coordinates present (CardKB won't set these) - if (event->touchX != 0 || event->touchY != 0) { - String keyTapped = keyForCoordinates(event->touchX, event->touchY); - bool valid = false; - - if (keyTapped == "⇧") { - highlight = -1; - payload = 0x00; - shift = !shift; - valid = true; - } else if (keyTapped == "⌫") { + // Touch enter/submit + else if (keyTapped == "↵") { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + shift = false; + valid = true; + } else if (!(keyTapped == "")) { #ifndef RAK14014 - highlight = keyTapped[0]; + highlight = keyTapped[0]; #endif - payload = 0x08; - shift = false; - valid = true; - } else if (keyTapped == "123" || keyTapped == "ABC") { - highlight = -1; - payload = 0x00; - charSet = (charSet == 0 ? 1 : 0); - valid = true; - } else if (keyTapped == " ") { -#ifndef RAK14014 - highlight = keyTapped[0]; -#endif - payload = keyTapped[0]; - shift = false; - valid = true; - } - // Touch enter/submit - else if (keyTapped == "↵") { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! - payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - currentMessageIndex = -1; - shift = false; - valid = true; - } else if (!(keyTapped == "")) { -#ifndef RAK14014 - highlight = keyTapped[0]; -#endif - payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); - shift = false; - valid = true; - } - - if (valid) { - lastTouchMillis = millis(); - runOnce(); - payload = 0; - return true; // STOP: We handled a VKB touch - } + payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); + shift = false; + valid = true; } + + if (valid) { + lastTouchMillis = millis(); + runOnce(); + payload = 0; + return true; // STOP: We handled a VKB touch + } + } #endif // USE_VIRTUAL_KEYBOARD - // All hardware keys fall through to here (CardKB, physical, etc.) + // All hardware keys fall through to here (CardKB, physical, etc.) - if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { - runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; - requestFocus(); - screen->forceDisplay(); - return true; - } - // Confirm select (Enter) - bool isSelect = isSelectEvent(event); - if (isSelect) { - LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, - freetext.c_str()); - if (dest == 0) - dest = NODENUM_BROADCAST; - // Defensive: If channel isn't valid, pick the first available channel - if (channel >= channels.getNumChannels()) - channel = 0; - - payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - currentMessageIndex = -1; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - lastTouchMillis = millis(); - runOnce(); - return true; - } - - // Backspace - if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { - payload = 0x08; - lastTouchMillis = millis(); - requestFocus(); - runOnce(); - return true; - } - - // Move cursor left - if (event->inputEvent == INPUT_BROKER_LEFT) { - payload = INPUT_BROKER_LEFT; - lastTouchMillis = millis(); - requestFocus(); - runOnce(); - return true; - } - // Move cursor right - if (event->inputEvent == INPUT_BROKER_RIGHT) { - payload = INPUT_BROKER_RIGHT; - lastTouchMillis = millis(); - requestFocus(); - runOnce(); - return true; - } - - // Cancel (dismiss freetext screen) - if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || - (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; - payload = 0; - currentMessageIndex = -1; - - // Notify UI that we want to redraw/close this screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return true; - } - - // Tab (switch destination) - if (event->kbchar == INPUT_BROKER_MSG_TAB) { - return handleTabSwitch(event); // Reuse tab logic - } - - // Printable ASCII (add char to draft) - if (event->kbchar >= 32 && event->kbchar <= 126) { - payload = event->kbchar; - lastTouchMillis = millis(); - runOnce(); - return true; - } - - return false; -} - -int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) -{ - int numEmotes = graphics::numEmotes; - - // Override isDown and isSelect ONLY for emote picker behavior - bool isUp = isUpEvent(event); - bool isDown = isDownEvent(event); - bool isSelect = isSelectEvent(event); - if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { - if (event->inputEvent == INPUT_BROKER_USER_PRESS) { - isDown = true; - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - isSelect = true; - } - } - - // Scroll emote list - if (isUp && emotePickerIndex > 0) { - emotePickerIndex--; - screen->forceDisplay(); - return 1; - } - if (isDown && emotePickerIndex < numEmotes - 1) { - emotePickerIndex++; - screen->forceDisplay(); - return 1; - } - - // Select emote: insert into freetext at cursor and return to freetext - if (isSelect) { - String label = graphics::emotes[emotePickerIndex].label; - String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" - if (cursor == freetext.length()) { - freetext += emoteInsert; - } else { - freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); - } - cursor += emoteInsert.length(); - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - screen->forceDisplay(); - return 1; - } - - // Cancel returns to freetext - if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - screen->forceDisplay(); - return 1; - } - - return 0; -} - -void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) -{ - lastDest = dest; - lastChannel = channel; - lastDestSet = true; - - meshtastic_MeshPacket *p = allocDataPacket(); - p->to = dest; - p->channel = channel; - p->want_ack = true; - p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num - - this->lastSentNode = dest; - this->incoming = dest; - - // Manually find the node by number to check PKI capability - meshtastic_NodeInfoLite *node = nullptr; - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < numMeshNodes; ++i) { - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num == dest) { - node = n; - break; - } - } - - NodeNum myNodeNum = nodeDB->getNodeNum(); - if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { - p->pki_encrypted = true; - p->channel = 0; // force PKI - } - - // Track this packet’s request ID for matching ACKs - this->lastRequestId = p->id; - - // Copy payload - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - - if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size++] = 7; - p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; - } - - this->waitingForAck = true; - - // Send to mesh (PKI-encrypted if conditions above matched) - service->sendToMesh(p, RX_SRC_LOCAL, true); - - // Show banner immediately - if (screen) { - graphics::BannerOverlayOptions opts; - opts.message = "Sending..."; - opts.durationMs = 2000; - screen->showOverlayBanner(opts); - } - - // Save outgoing message - StoredMessage sm; - - // Always use our local time, consistent with other paths - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; - sm.isBootRelative = (nowSecs == 0); - - sm.sender = nodeDB->getNodeNum(); // us - sm.channelIndex = channel; - size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); - sm.textOffset = MessageStore::storeText(message, len); - sm.textLength = len; - - // Classify broadcast vs DM - if (dest == NODENUM_BROADCAST) { - sm.dest = NODENUM_BROADCAST; - sm.type = MessageType::BROADCAST; - } else { - sm.dest = dest; - sm.type = MessageType::DM_TO_US; - // Only add as favorite if our role is NOT CLIENT_BASE - if (config.device.role != 12) { - LOG_INFO("Proactively adding %x as favorite node", dest); - nodeDB->set_favorite(true, dest); - } else { - LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); - } - } - sm.ackStatus = AckStatus::NONE; - - messageStore.addLiveMessage(std::move(sm)); - - // Auto-switch thread view on outgoing message - if (sm.type == MessageType::BROADCAST) { - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); - } else { - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); - } - - playComboTune(); - - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; - this->payload = wantReplies ? 1 : 0; + if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { + runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; requestFocus(); + screen->forceDisplay(); + return true; + } + // Confirm select (Enter) + bool isSelect = isSelectEvent(event); + if (isSelect) { + LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, freetext.c_str()); + if (dest == 0) + dest = NODENUM_BROADCAST; + // Defensive: If channel isn't valid, pick the first available channel + if (channel >= channels.getNumChannels()) + channel = 0; - // Tell Screen to switch to TextMessage frame via UIFrameEvent + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Backspace + if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { + payload = 0x08; + lastTouchMillis = millis(); + requestFocus(); + runOnce(); + return true; + } + + // Move cursor left + if (event->inputEvent == INPUT_BROKER_LEFT) { + payload = INPUT_BROKER_LEFT; + lastTouchMillis = millis(); + requestFocus(); + runOnce(); + return true; + } + // Move cursor right + if (event->inputEvent == INPUT_BROKER_RIGHT) { + payload = INPUT_BROKER_RIGHT; + lastTouchMillis = millis(); + requestFocus(); + runOnce(); + return true; + } + + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || + (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen UIFrameEvent e; - e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + // Tab (switch destination) + if (event->kbchar == INPUT_BROKER_MSG_TAB) { + return handleTabSwitch(event); // Reuse tab logic + } + + // Printable ASCII (add char to draft) + if (event->kbchar >= 32 && event->kbchar <= 126) { + payload = event->kbchar; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + return false; } -int32_t CannedMessageModule::runOnce() -{ - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { - updateDestinationSelectionList(); - needsUpdate = false; +int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) { + int numEmotes = graphics::numEmotes; + + // Override isDown and isSelect ONLY for emote picker behavior + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + // Scroll emote list + if (isUp && emotePickerIndex > 0) { + emotePickerIndex--; + screen->forceDisplay(); + return 1; + } + if (isDown && emotePickerIndex < numEmotes - 1) { + emotePickerIndex++; + screen->forceDisplay(); + return 1; + } + + // Select emote: insert into freetext at cursor and return to freetext + if (isSelect) { + String label = graphics::emotes[emotePickerIndex].label; + String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" + if (cursor == freetext.length()) { + freetext += emoteInsert; + } else { + freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); + } + cursor += emoteInsert.length(); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + + // Cancel returns to freetext + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + + return 0; +} + +void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) { + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num + + this->lastSentNode = dest; + this->incoming = dest; + + // Manually find the node by number to check PKI capability + meshtastic_NodeInfoLite *node = nullptr; + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num == dest) { + node = n; + break; + } + } + + NodeNum myNodeNum = nodeDB->getNodeNum(); + if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { + p->pki_encrypted = true; + p->channel = 0; // force PKI + } + + // Track this packet’s request ID for matching ACKs + this->lastRequestId = p->id; + + // Copy payload + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; + } + + this->waitingForAck = true; + + // Send to mesh (PKI-encrypted if conditions above matched) + service->sendToMesh(p, RX_SRC_LOCAL, true); + + // Show banner immediately + if (screen) { + graphics::BannerOverlayOptions opts; + opts.message = "Sending..."; + opts.durationMs = 2000; + screen->showOverlayBanner(opts); + } + + // Save outgoing message + StoredMessage sm; + + // Always use our local time, consistent with other paths + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; + sm.isBootRelative = (nowSecs == 0); + + sm.sender = nodeDB->getNodeNum(); // us + sm.channelIndex = channel; + size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); + sm.textOffset = MessageStore::storeText(message, len); + sm.textLength = len; + + // Classify broadcast vs DM + if (dest == NODENUM_BROADCAST) { + sm.dest = NODENUM_BROADCAST; + sm.type = MessageType::BROADCAST; + } else { + sm.dest = dest; + sm.type = MessageType::DM_TO_US; + // Only add as favorite if our role is NOT CLIENT_BASE + if (config.device.role != 12) { + LOG_INFO("Proactively adding %x as favorite node", dest); + nodeDB->set_favorite(true, dest); + } else { + LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); + } + } + sm.ackStatus = AckStatus::NONE; + + messageStore.addLiveMessage(std::move(sm)); + + // Auto-switch thread view on outgoing message + if (sm.type == MessageType::BROADCAST) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); + } else { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); + } + + playComboTune(); + + this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + this->payload = wantReplies ? 1 : 0; + requestFocus(); + + // Tell Screen to switch to TextMessage frame via UIFrameEvent + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + notifyObservers(&e); +} + +int32_t CannedMessageModule::runOnce() { + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { + updateDestinationSelectionList(); + needsUpdate = false; + } + + // If we're in node selection, do nothing except keep alive + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + return INACTIVATE_AFTER_MS; + } + + // Normal module disable/idle handling + if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + // Clean up virtual keyboard if needed when going inactive + if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { + LOG_INFO("Performing delayed virtual keyboard cleanup"); + graphics::OnScreenKeyboardModule::instance().stop(false); } - // If we're in node selection, do nothing except keep alive - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - return INACTIVATE_AFTER_MS; - } + return INT32_MAX; + } - // Normal module disable/idle handling - if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { - // Clean up virtual keyboard if needed when going inactive - if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { - LOG_INFO("Performing delayed virtual keyboard cleanup"); - graphics::OnScreenKeyboardModule::instance().stop(false); - } + // Handle delayed virtual keyboard message sending + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + // Virtual keyboard message sending case - text was not empty + if (this->freetext.length() > 0) { + LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); + sendText(this->dest, this->channel, this->freetext.c_str(), true); - return INT32_MAX; - } + // Clean up virtual keyboard after sending + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard after message send"); + graphics::OnScreenKeyboardModule::instance().stop(false); + graphics::NotificationRenderer::resetBanner(); + } - // Handle delayed virtual keyboard message sending - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - // Virtual keyboard message sending case - text was not empty - if (this->freetext.length() > 0) { - LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); - sendText(this->dest, this->channel, this->freetext.c_str(), true); - - // Clean up virtual keyboard after sending - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard after message send"); - graphics::OnScreenKeyboardModule::instance().stop(false); - graphics::NotificationRenderer::resetBanner(); - } - - // Clear payload to indicate virtual keyboard processing is complete - // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds - this->payload = 0; - } else { - // Empty message, just go inactive - LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - return 2000; + // Clear payload to indicate virtual keyboard processing is complete + // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds + this->payload = 0; + } else { + // Empty message, just go inactive + LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && - this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || - (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || - (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - } - // Handle SENDING_ACTIVE state transition after virtual keyboard message - else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - return INT32_MAX; - } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && - !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { - // Reset module on inactivity - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); + return 2000; + } - // Clean up virtual keyboard if it exists during timeout - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - graphics::OnScreenKeyboardModule::instance().stop(false); - graphics::NotificationRenderer::resetBanner(); - } - - this->notifyObservers(&e); - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == 0) { - // [Exit] button pressed - return to inactive state - LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - if (this->freetext.length() > 0) { - sendText(this->dest, this->channel, this->freetext.c_str(), true); - - // Clean up state but *don’t* deactivate yet - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Tell Screen to jump straight to the TextMessage frame - UIFrameEvent e; - e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; - this->notifyObservers(&e); - - // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - return INT32_MAX; // don’t fall back into canned list - } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - } else { - if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - return INT32_MAX; - } - if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { - if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { - return INT32_MAX; - } else { - sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); - - // Clean up state - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Tell Screen to jump straight to the TextMessage frame - UIFrameEvent e; - e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; - this->notifyObservers(&e); - - // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - return INT32_MAX; // don’t fall back into canned list - } - } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - } - // fallback clean-up if nothing above returned - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - - // Immediately stop, don’t linger on canned screen - return INT32_MAX; - } - // Highlight [Select Destination] initially when entering the message list - else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, - // not when coming back from a sent message. - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - int selectDestination = 0; - for (int i = 0; i < this->messagesCount; ++i) { - if (strcmp(this->messages[i], "[Select Destination]") == 0) { - selectDestination = i; - break; - } - } - this->currentMessageIndex = selectDestination; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { - if (this->messagesCount > 0) { - this->currentMessageIndex = getPrevIndex(); - this->freetext = ""; - this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { - if (this->messagesCount > 0) { - this->currentMessageIndex = this->getNextIndex(); - this->freetext = ""; - this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - switch (this->payload) { - case INPUT_BROKER_LEFT: - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { - this->cursor--; - } - break; - case INPUT_BROKER_RIGHT: - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { - this->cursor++; - } - break; - default: - break; - } - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - switch (this->payload) { - case 0x08: // backspace - if (this->freetext.length() > 0) { - if (this->cursor > 0) { - if (this->cursor == this->freetext.length()) { - this->freetext = this->freetext.substring(0, this->freetext.length() - 1); - } else { - this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); - } - this->cursor--; - } - } else { - } - break; - case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler - return 0; - case INPUT_BROKER_LEFT: - case INPUT_BROKER_RIGHT: - break; - default: - // Only insert ASCII printable characters (32–126) - if (this->payload >= 32 && this->payload <= 126) { - requestFocus(); - if (this->cursor == this->freetext.length()) { - this->freetext += (char)this->payload; - } else { - this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + - this->freetext.substring(this->cursor); - } - this->cursor++; - const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); - if (this->freetext.length() > maxChars) { - this->cursor = maxChars; - this->freetext = this->freetext.substring(0, maxChars); - } - } - break; - } - } - this->lastTouchMillis = millis(); - this->notifyObservers(&e); - return INACTIVATE_AFTER_MS; - } - - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - this->lastTouchMillis = millis(); - this->notifyObservers(&e); - return INACTIVATE_AFTER_MS; - } + UIFrameEvent e; + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); + } + // Handle SENDING_ACTIVE state transition after virtual keyboard message + else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; return INT32_MAX; -} + } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && + !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { + // Reset module on inactivity + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; -const char *CannedMessageModule::getCurrentMessage() -{ - return this->messages[this->currentMessageIndex]; -} -const char *CannedMessageModule::getPrevMessage() -{ - return this->messages[this->getPrevIndex()]; -} -const char *CannedMessageModule::getNextMessage() -{ - return this->messages[this->getNextIndex()]; -} -const char *CannedMessageModule::getMessageByIndex(int index) -{ - return (index >= 0 && index < this->messagesCount) ? this->messages[index] : ""; -} - -const char *CannedMessageModule::getNodeName(NodeNum node) -{ - if (node == NODENUM_BROADCAST) - return "Broadcast"; - - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info && info->has_user && strlen(info->user.long_name) > 0) { - return info->user.long_name; + // Clean up virtual keyboard if it exists during timeout + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard due to module timeout"); + graphics::OnScreenKeyboardModule::instance().stop(false); + graphics::NotificationRenderer::resetBanner(); } - static char fallback[12]; - snprintf(fallback, sizeof(fallback), "0x%08x", node); - return fallback; + this->notifyObservers(&e); + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { + if (this->payload == 0) { + // [Exit] button pressed - return to inactive state + LOG_INFO("Processing [Exit] action - returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->freetext.length() > 0) { + sendText(this->dest, this->channel, this->freetext.c_str(), true); + + // Clean up state but *don’t* deactivate yet + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list + } else { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + } else { + if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + return INT32_MAX; + } + if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { + if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { + return INT32_MAX; + } else { + sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); + + // Clean up state + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list + } + } else { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + } + // fallback clean-up if nothing above returned + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + + // Immediately stop, don’t linger on canned screen + return INT32_MAX; + } + // Highlight [Select Destination] initially when entering the message list + else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { + // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, + // not when coming back from a sent message. + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + int selectDestination = 0; + for (int i = 0; i < this->messagesCount; ++i) { + if (strcmp(this->messages[i], "[Select Destination]") == 0) { + selectDestination = i; + break; + } + } + this->currentMessageIndex = selectDestination; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { + if (this->messagesCount > 0) { + this->currentMessageIndex = getPrevIndex(); + this->freetext = ""; + this->cursor = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { + if (this->messagesCount > 0) { + this->currentMessageIndex = this->getNextIndex(); + this->freetext = ""; + this->cursor = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + switch (this->payload) { + case INPUT_BROKER_LEFT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { + this->cursor--; + } + break; + case INPUT_BROKER_RIGHT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { + this->cursor++; + } + break; + default: + break; + } + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + switch (this->payload) { + case 0x08: // backspace + if (this->freetext.length() > 0) { + if (this->cursor > 0) { + if (this->cursor == this->freetext.length()) { + this->freetext = this->freetext.substring(0, this->freetext.length() - 1); + } else { + this->freetext = this->freetext.substring(0, this->cursor - 1) + this->freetext.substring(this->cursor, this->freetext.length()); + } + this->cursor--; + } + } else { + } + break; + case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler + return 0; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: + break; + default: + // Only insert ASCII printable characters (32–126) + if (this->payload >= 32 && this->payload <= 126) { + requestFocus(); + if (this->cursor == this->freetext.length()) { + this->freetext += (char)this->payload; + } else { + this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + this->freetext.substring(this->cursor); + } + this->cursor++; + const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); + if (this->freetext.length() > maxChars) { + this->cursor = maxChars; + this->freetext = this->freetext.substring(0, maxChars); + } + } + break; + } + } + this->lastTouchMillis = millis(); + this->notifyObservers(&e); + return INACTIVATE_AFTER_MS; + } + + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + this->lastTouchMillis = millis(); + this->notifyObservers(&e); + return INACTIVATE_AFTER_MS; + } + return INT32_MAX; } -bool CannedMessageModule::shouldDraw() -{ - // Only allow drawing when we're in an interactive UI state. - return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || - this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || - this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); +const char *CannedMessageModule::getCurrentMessage() { return this->messages[this->currentMessageIndex]; } +const char *CannedMessageModule::getPrevMessage() { return this->messages[this->getPrevIndex()]; } +const char *CannedMessageModule::getNextMessage() { return this->messages[this->getNextIndex()]; } +const char *CannedMessageModule::getMessageByIndex(int index) { return (index >= 0 && index < this->messagesCount) ? this->messages[index] : ""; } + +const char *CannedMessageModule::getNodeName(NodeNum node) { + if (node == NODENUM_BROADCAST) + return "Broadcast"; + + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user && strlen(info->user.long_name) > 0) { + return info->user.long_name; + } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; +} + +bool CannedMessageModule::shouldDraw() { + // Only allow drawing when we're in an interactive UI state. + return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); } // Has the user defined any canned messages? // Expose publicly whether canned message module is ready for use -bool CannedMessageModule::hasMessages() -{ - return (this->messagesCount > 0); +bool CannedMessageModule::hasMessages() { return (this->messagesCount > 0); } + +int CannedMessageModule::getNextIndex() { + if (this->currentMessageIndex >= (this->messagesCount - 1)) { + return 0; + } else { + return this->currentMessageIndex + 1; + } } -int CannedMessageModule::getNextIndex() -{ - if (this->currentMessageIndex >= (this->messagesCount - 1)) { - return 0; - } else { - return this->currentMessageIndex + 1; - } -} - -int CannedMessageModule::getPrevIndex() -{ - if (this->currentMessageIndex <= 0) { - return this->messagesCount - 1; - } else { - return this->currentMessageIndex - 1; - } +int CannedMessageModule::getPrevIndex() { + if (this->currentMessageIndex <= 0) { + return this->messagesCount - 1; + } else { + return this->currentMessageIndex - 1; + } } #if defined(USE_VIRTUAL_KEYBOARD) -String CannedMessageModule::keyForCoordinates(uint x, uint y) -{ - int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; +String CannedMessageModule::keyForCoordinates(uint x, uint y) { + int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; - for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { - int innerSize = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; + for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { + int innerSize = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; - for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { - Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; - if (x > letter.rectX && x < (letter.rectX + letter.rectWidth) && y > letter.rectY && - y < (letter.rectY + letter.rectHeight)) { - return letter.character; - } - } + if (x > letter.rectX && x < (letter.rectX + letter.rectWidth) && y > letter.rectY && y < (letter.rectY + letter.rectHeight)) { + return letter.character; + } } + } - return ""; + return ""; } -void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; +void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; - int xOffset = 0; + int xOffset = 0; - int yOffset = 56; + int yOffset = 56; - display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + display->setFont(FONT_SMALL); - display->setColor(OLEDDISPLAY_COLOR::WHITE); + display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawStringMaxWidth(0, 0, display->getWidth(), - cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); + display->drawStringMaxWidth(0, 0, display->getWidth(), + cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); - display->setFont(FONT_MEDIUM); + display->setFont(FONT_MEDIUM); - int cellHeight = round((display->height() - 64) / outerSize); + int cellHeight = round((display->height() - 64) / outerSize); - int yCorrection = 8; + int yCorrection = 8; - for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { - yOffset += outerIndex > 0 ? cellHeight : 0; + for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { + yOffset += outerIndex > 0 ? cellHeight : 0; - int innerSizeBound = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; + int innerSizeBound = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; - int innerSize = 0; + int innerSize = 0; - for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) { - if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") { - innerSize++; - } - } + for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) { + if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") { + innerSize++; + } + } - int cellWidth = display->width() / innerSize; + int cellWidth = display->width() / innerSize; - for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { - xOffset += innerIndex > 0 ? cellWidth : 0; + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + xOffset += innerIndex > 0 ? cellWidth : 0; - Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; - Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight}; + Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight}; #ifdef RAK14014 // Optimize the touch range of the virtual keyboard in the bottom row - if (outerIndex == outerSize - 1) { - updatedLetter.rectHeight = 240 - yOffset; - } + if (outerIndex == outerSize - 1) { + updatedLetter.rectHeight = 240 - yOffset; + } #endif - this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter; + this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter; - float characterOffset = ((cellWidth / 2) - (letter.width / 2)); + float characterOffset = ((cellWidth / 2) - (letter.width / 2)); - if (letter.character == "⇧") { - if (this->shift) { - display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + if (letter.character == "⇧") { + if (this->shift) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - display->setColor(OLEDDISPLAY_COLOR::BLACK); + display->setColor(OLEDDISPLAY_COLOR::BLACK); - drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - display->setColor(OLEDDISPLAY_COLOR::WHITE); - } else { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + display->setColor(OLEDDISPLAY_COLOR::WHITE); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - } - } else if (letter.character == "⌫") { - if (this->highlight == letter.character[0]) { - display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - - display->setColor(OLEDDISPLAY_COLOR::BLACK); - - drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - setIntervalFromNow(0); - } else { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - - drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - } - } else if (letter.character == "↵") { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - - drawEnterIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.7); - } else { - if (this->highlight == letter.character[0]) { - display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - - display->setColor(OLEDDISPLAY_COLOR::BLACK); - - display->drawString(xOffset + characterOffset, yOffset + yCorrection, - letter.character == " " ? "space" : letter.character); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - setIntervalFromNow(0); - } else { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - - display->drawString(xOffset + characterOffset, yOffset + yCorrection, - letter.character == " " ? "space" : letter.character); - } - } + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); } + } else if (letter.character == "⌫") { + if (this->highlight == letter.character[0]) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - xOffset = 0; + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + setIntervalFromNow(0); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + } + } else if (letter.character == "↵") { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawEnterIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.7); + } else { + if (this->highlight == letter.character[0]) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + display->drawString(xOffset + characterOffset, yOffset + yCorrection, letter.character == " " ? "space" : letter.character); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + setIntervalFromNow(0); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + display->drawString(xOffset + characterOffset, yOffset + yCorrection, letter.character == " " ? "space" : letter.character); + } + } } - this->highlight = 0x00; + xOffset = 0; + } + + this->highlight = 0x00; } -void CannedMessageModule::drawShiftIcon(OLEDDisplay *display, int x, int y, float scale) -{ - PointStruct shiftIcon[10] = {{8, 0}, {15, 7}, {15, 8}, {12, 8}, {12, 12}, {4, 12}, {4, 8}, {1, 8}, {1, 7}, {8, 0}}; +void CannedMessageModule::drawShiftIcon(OLEDDisplay *display, int x, int y, float scale) { + PointStruct shiftIcon[10] = {{8, 0}, {15, 7}, {15, 8}, {12, 8}, {12, 12}, {4, 12}, {4, 8}, {1, 8}, {1, 7}, {8, 0}}; - int size = 10; + int size = 10; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (shiftIcon[i].x * scale); - int y0 = y + (shiftIcon[i].y * scale); - int x1 = x + (shiftIcon[i + 1].x * scale); - int y1 = y + (shiftIcon[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (shiftIcon[i].x * scale); + int y0 = y + (shiftIcon[i].y * scale); + int x1 = x + (shiftIcon[i + 1].x * scale); + int y1 = y + (shiftIcon[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } } -void CannedMessageModule::drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale) -{ - PointStruct backspaceIcon[6] = {{0, 7}, {5, 2}, {15, 2}, {15, 12}, {5, 12}, {0, 7}}; +void CannedMessageModule::drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale) { + PointStruct backspaceIcon[6] = {{0, 7}, {5, 2}, {15, 2}, {15, 12}, {5, 12}, {0, 7}}; - int size = 6; + int size = 6; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (backspaceIcon[i].x * scale); - int y0 = y + (backspaceIcon[i].y * scale); - int x1 = x + (backspaceIcon[i + 1].x * scale); - int y1 = y + (backspaceIcon[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (backspaceIcon[i].x * scale); + int y0 = y + (backspaceIcon[i].y * scale); + int x1 = x + (backspaceIcon[i + 1].x * scale); + int y1 = y + (backspaceIcon[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } - PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 4}}; + PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 4}}; - size = 4; + size = 4; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (backspaceIconX[i].x * scale); - int y0 = y + (backspaceIconX[i].y * scale); - int x1 = x + (backspaceIconX[i + 1].x * scale); - int y1 = y + (backspaceIconX[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (backspaceIconX[i].x * scale); + int y0 = y + (backspaceIconX[i].y * scale); + int x1 = x + (backspaceIconX[i + 1].x * scale); + int y1 = y + (backspaceIconX[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } } -void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, float scale) -{ - PointStruct enterIcon[6] = {{0, 7}, {4, 3}, {4, 11}, {0, 7}, {15, 7}, {15, 0}}; +void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, float scale) { + PointStruct enterIcon[6] = {{0, 7}, {4, 3}, {4, 11}, {0, 7}, {15, 7}, {15, 0}}; - int size = 6; + int size = 6; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (enterIcon[i].x * scale); - int y0 = y + (enterIcon[i].y * scale); - int x1 = x + (enterIcon[i + 1].x * scale); - int y1 = y + (enterIcon[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (enterIcon[i].x * scale); + int y0 = y + (enterIcon[i].y * scale); + int x1 = x + (enterIcon[i + 1].x * scale); + int y1 = y + (enterIcon[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } } #endif // Indicate to screen class that module is handling keyboard input specially (at certain times) // This prevents the left & right keys being used for nav. between screen frames during text entry. -bool CannedMessageModule::interceptingKeyboardInput() -{ - switch (runState) { - case CANNED_MESSAGE_RUN_STATE_DISABLED: - case CANNED_MESSAGE_RUN_STATE_INACTIVE: - return false; - default: - return true; - } +bool CannedMessageModule::interceptingKeyboardInput() { + switch (runState) { + case CANNED_MESSAGE_RUN_STATE_DISABLED: + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + return false; + default: + return true; + } } // Draw the node/channel selection screen -void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ +void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + requestFocus(); + display->setColor(WHITE); // Always draw cleanly + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // Header + int titleY = 2; + String titleText = "Select Destination"; + titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // List Items + int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); + int numActiveChannels = this->activeChannelIndices.size(); + int totalEntries = numActiveChannels + this->filteredNodes.size(); + int columns = 1; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); + if (this->visibleRows < 1) + this->visibleRows = 1; + + // Clamp scrolling + if (scrollIndex > totalEntries / columns) + scrollIndex = totalEntries / columns; + if (scrollIndex < 0) + scrollIndex = 0; + + for (int row = 0; row < visibleRows; row++) { + int itemIndex = scrollIndex + row; + if (itemIndex >= totalEntries) + break; + + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + char entryText[64] = ""; + + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && node->user.long_name) { + strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); + entryText[sizeof(entryText) - 1] = '\0'; + } + int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - + ((node && node->is_favorite) ? 10 : 0); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(entryText); + while (entryText[0] && display->getStringWidth(entryText) > availWidth) { + entryText[strlen(entryText) - 1] = '\0'; + } + if (strlen(entryText) < origLen) { + strcat(entryText, "..."); + } + + // Prepend "* " if this is a favorite + if (node && node->is_favorite) { + size_t len = strlen(entryText); + if (len + 2 < sizeof(entryText)) { + memmove(entryText + 2, entryText, len + 1); + entryText[0] = '*'; + entryText[1] = ' '; + } + } + if (node) { + if (display->getWidth() <= 64) { + snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); + } + } + } + } + + if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) + strcpy(entryText, "?"); + + // Highlight background (if selected) + if (itemIndex == destIndex) { + int scrollPadding = 8; // Reserve space for scrollbar + display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); + } + + // Draw entry text + display->drawString(xOffset + 2, yOffset, entryText); + display->setColor(WHITE); + + // Draw key icon (after highlight) + /* + if (itemIndex >= numActiveChannels) { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && hasKeyForNode(node)) { + int iconX = display->getWidth() - key_symbol_width - 15; + int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; + + if (itemIndex == destIndex) { + display->setColor(INVERSE); + } else { + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); + } + } + } + */ + } + + // Scrollbar + if (totalEntries > visibleRows) { + int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); + int totalScrollable = totalEntries; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); + int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; + int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; + display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); + } +} + +void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height + const int headerMargin = 2; // Extra pixels below header + const int labelGap = 6; + const int bitmapGapX = 4; + + // Find max emote height (assume all same, or precalculated) + int maxEmoteHeight = 0; + for (int i = 0; i < graphics::numEmotes; ++i) + if (graphics::emotes[i].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[i].height; + + const int rowHeight = maxEmoteHeight + 2; + + // Place header at top, then compute start of emote list + int headerY = y; + int listTop = headerY + headerFontHeight + headerMargin; + + int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; + int numEmotes = graphics::numEmotes; + + // keep member variable in sync + this->visibleRows = _visibleRows; + + // Clamp highlight index + if (emotePickerIndex < 0) + emotePickerIndex = 0; + if (emotePickerIndex >= numEmotes) + emotePickerIndex = numEmotes - 1; + + // Determine which emote is at the top + int topIndex = emotePickerIndex - _visibleRows / 2; + if (topIndex < 0) + topIndex = 0; + if (topIndex > numEmotes - _visibleRows) + topIndex = std::max(0, numEmotes - _visibleRows); + + // Draw header/title + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, headerY, "Select Emote"); + + // Draw emote rows + display->setTextAlignment(TEXT_ALIGN_LEFT); + + for (int vis = 0; vis < _visibleRows; ++vis) { + int emoteIdx = topIndex + vis; + if (emoteIdx >= numEmotes) + break; + const graphics::Emote &emote = graphics::emotes[emoteIdx]; + int rowY = listTop + vis * rowHeight; + + // Draw highlight box 2px taller than emote (1px margin above and below) + if (emoteIdx == emotePickerIndex) { + display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); + display->setColor(BLACK); + } + + // Emote bitmap (left), 1px margin from highlight bar top + int emoteY = rowY + 1; + display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + + // Emote label (right of bitmap) + display->setFont(FONT_MEDIUM); + int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); + display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + + if (emoteIdx == emotePickerIndex) + display->setColor(WHITE); + } + + // Draw scrollbar if needed + if (numEmotes > _visibleRows) { + int scrollbarHeight = _visibleRows * rowHeight; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); + int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); + int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; + display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); + } +} + +void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + this->displayHeight = display->getHeight(); // Store display height for later use + char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // Never draw if state is outside our UI modes + if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { + return; // bail if not in a UI state that should render + } + + // Emote Picker Screen + if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here + return; + } + + // Destination Selection + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + drawDestinationSelectionScreen(display, state, x, y); + return; + } + + // Disabled Screen + if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); + return; + } + + // Free Text Input Screen + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { requestFocus(); - display->setColor(WHITE); // Always draw cleanly +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + EInkDynamicDisplay *einkDisplay = static_cast(display); + einkDisplay->enableUnlimitedFastMode(); +#endif +#if defined(USE_VIRTUAL_KEYBOARD) + drawKeyboard(display, state, 0, 0); +#else display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // Header - int titleY = 2; - String titleText = "Select Destination"; - titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, titleY, titleText); + // Draw node/channel header at the top + drawHeader(display, x, y, buffer); + + // Char count right-aligned + if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); + snprintf(buffer, sizeof(buffer), "%d left", charsLeft); + display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); + } + +#if INPUTBROKER_SERIAL_TYPE == 1 + // Chatter Modifier key mode label (right side) + { + uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0; + const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#"; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const int16_t th = FONT_HEIGHT_SMALL; + const int16_t tw = display->getStringWidth(label); + const int16_t padX = 3; + const int16_t padY = 2; + const int16_t r = 3; + + const int16_t bw = tw + padX * 2; + const int16_t bh = th + padY * 2; + + const int16_t bx = x + display->getWidth() - bw - 2; + const int16_t by = y + display->getHeight() - bh - 2; + + display->setColor(WHITE); + display->fillRect(bx + r, by, bw - r * 2, bh); + display->fillRect(bx, by + r, r, bh - r * 2); + display->fillRect(bx + bw - r, by + r, r, bh - r * 2); + display->fillCircle(bx + r, by + r, r); + display->fillCircle(bx + bw - r - 1, by + r, r); + display->fillCircle(bx + r, by + bh - r - 1, r); + display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); + + display->setColor(BLACK); + display->drawString(bx + padX, by + padY, label); + } + + // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + ◄”) + { + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const char *label = "Dest: Shift + "; + int16_t labelW = display->getStringWidth(label); + + // triangle size visually matches glyph height, not full line height + const int triH = FONT_HEIGHT_SMALL - 3; + const int triW = triH * 0.7; + + const int16_t padX = 3; + const int16_t padY = 2; + const int16_t r = 3; + + const int16_t bw = labelW + triW + padX * 2 + 2; + const int16_t bh = FONT_HEIGHT_SMALL + padY * 2; + + const int16_t bx = x + 2; + const int16_t by = y + display->getHeight() - bh - 2; + + // Rounded white box + display->setColor(WHITE); + display->fillRect(bx + r, by, bw - (r * 2), bh); + display->fillRect(bx, by + r, r, bh - (r * 2)); + display->fillRect(bx + bw - r, by + r, r, bh - (r * 2)); + display->fillCircle(bx + r, by + r, r); + display->fillCircle(bx + bw - r - 1, by + r, r); + display->fillCircle(bx + r, by + bh - r - 1, r); + display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); + + // Draw text + display->setColor(BLACK); + display->drawString(bx + padX, by + padY, label); + + // Perfectly center triangle on text baseline + int16_t tx = bx + padX + labelW; + int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering + + // ◄ Left-pointing triangle + display->fillTriangle(tx + triW, ty, // top-right + tx, ty + triH / 2, // left center + tx + triW, ty + triH // bottom-right + ); + } +#endif + // Draw Free Text input with multi-emote support and proper line wrapping + display->setColor(WHITE); + { + int inputY = 0 + y + FONT_HEIGHT_SMALL; + String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); + + // Tokenize input into (isEmote, token) pairs + const char *msg = msgWithCursor.c_str(); + std::vector> tokens = tokenizeMessageWithEmotes(msg); + + // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) + std::vector>> lines; + std::vector> currentLine; + int lineWidth = 0; + int maxWidth = display->getWidth(); + for (auto &token : tokens) { + if (token.first) { + // Emote + int tokenWidth = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + tokenWidth = graphics::emotes[j].width + 2; + break; + } + } + if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back(token); + lineWidth += tokenWidth; + } else { + // Text: split by words and wrap inside word if needed + String text = token.second; + int pos = 0; + while (pos < static_cast(text.length())) { + // Find next space (or end) + int spacePos = text.indexOf(' ', pos); + int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space + String word = text.substring(pos, endPos); + int wordWidth = display->getStringWidth(word); + + if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + // If word itself too big, split by character + if (wordWidth > maxWidth) { + uint16_t charPos = 0; + while (charPos < word.length()) { + String oneChar = word.substring(charPos, charPos + 1); + int charWidth = display->getStringWidth(oneChar); + if (lineWidth + charWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back({false, oneChar}); + lineWidth += charWidth; + charPos++; + } + } else { + currentLine.push_back({false, word}); + lineWidth += wordWidth; + } + pos = endPos; + } + } + } + if (!currentLine.empty()) + lines.push_back(currentLine); + + // Draw lines with emotes + int rowHeight = FONT_HEIGHT_SMALL; + int yLine = inputY; + for (auto &line : lines) { + int nextX = x; + for (const auto &token : line) { + if (token.first) { + // Emote rendering centralized in helper + renderEmote(display, nextX, yLine, rowHeight, token.second); + } else { + display->drawString(nextX, yLine, token.second); + nextX += display->getStringWidth(token.second); + } + } + yLine += rowHeight; + } + } +#endif + return; + } + + // Canned Messages List + if (this->messagesCount > 0) { display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - // List Items - int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); - int numActiveChannels = this->activeChannelIndices.size(); - int totalEntries = numActiveChannels + this->filteredNodes.size(); - int columns = 1; - this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); - if (this->visibleRows < 1) - this->visibleRows = 1; + // Precompute per-row heights based on emotes (centered if present) + const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; - // Clamp scrolling - if (scrollIndex > totalEntries / columns) - scrollIndex = totalEntries / columns; - if (scrollIndex < 0) - scrollIndex = 0; + int topMsg; + std::vector rowHeights; + int _visibleRows; - for (int row = 0; row < visibleRows; row++) { - int itemIndex = scrollIndex + row; - if (itemIndex >= totalEntries) - break; + // Draw header (To: ...) + drawHeader(display, x, y, buffer); - int xOffset = 0; - int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - char entryText[64] = ""; + // Shift message list upward by 3 pixels to reduce spacing between header and first message + const int listYOffset = y + FONT_HEIGHT_SMALL - 3; + _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; - // Draw Channels First - if (itemIndex < numActiveChannels) { - uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); + // Figure out which messages are visible and their needed heights + topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) ? currentMessageIndex - _visibleRows + 2 : 0; + int countRows = std::min(messagesCount, _visibleRows); + + // Build per-row max height based on all emotes in line + for (int i = 0; i < countRows; i++) { + const char *msg = getMessageByIndex(topMsg + i); + int maxEmoteHeight = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *search = msg; + while ((search = strstr(search, label))) { + if (graphics::emotes[j].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[j].height; + search += strlen(label); // Advance past this emote } - // Then Draw Nodes - else { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && node->user.long_name) { - strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); - entryText[sizeof(entryText) - 1] = '\0'; - } - int availWidth = display->getWidth() - - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - - ((node && node->is_favorite) ? 10 : 0); - if (availWidth < 0) - availWidth = 0; + } + rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); + } - size_t origLen = strlen(entryText); - while (entryText[0] && display->getStringWidth(entryText) > availWidth) { - entryText[strlen(entryText) - 1] = '\0'; - } - if (strlen(entryText) < origLen) { - strcat(entryText, "..."); - } + // Draw all message rows with multi-emote support + int yCursor = listYOffset; + for (int vis = 0; vis < countRows; vis++) { + int msgIdx = topMsg + vis; + int lineY = yCursor; + const char *msg = getMessageByIndex(msgIdx); + int rowHeight = rowHeights[vis]; + bool _highlight = (msgIdx == currentMessageIndex); - // Prepend "* " if this is a favorite - if (node && node->is_favorite) { - size_t len = strlen(entryText); - if (len + 2 < sizeof(entryText)) { - memmove(entryText + 2, entryText, len + 1); - entryText[0] = '*'; - entryText[1] = ' '; - } - } - if (node) { - if (display->getWidth() <= 64) { - snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); - } - } - } + // Multi-emote tokenization + std::vector> tokens = tokenizeMessageWithEmotes(msg); + + // Vertically center based on rowHeight + int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; + +#ifdef USE_EINK + int nextX = x + (_highlight ? 12 : 0); + if (_highlight) + display->drawString(x + 0, lineY + textYOffset, ">"); +#else + int scrollPadding = 8; + if (_highlight) { + display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); + display->setColor(BLACK); + } + int nextX = x + (_highlight ? 2 : 0); +#endif + + // Draw all tokens left to right + for (const auto &token : tokens) { + if (token.first) { + // Emote rendering centralized in helper + renderEmote(display, nextX, lineY, rowHeight, token.second); + } else { + // Text + display->drawString(nextX, lineY + textYOffset, token.second); + nextX += display->getStringWidth(token.second); } - - if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) - strcpy(entryText, "?"); - - // Highlight background (if selected) - if (itemIndex == destIndex) { - int scrollPadding = 8; // Reserve space for scrollbar - display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); - display->setColor(BLACK); - } - - // Draw entry text - display->drawString(xOffset + 2, yOffset, entryText); + } +#ifndef USE_EINK + if (_highlight) display->setColor(WHITE); +#endif - // Draw key icon (after highlight) - /* - if (itemIndex >= numActiveChannels) { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && hasKeyForNode(node)) { - int iconX = display->getWidth() - key_symbol_width - 15; - int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; - - if (itemIndex == destIndex) { - display->setColor(INVERSE); - } else { - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); - } - } - } - */ + yCursor += rowHeight; } // Scrollbar - if (totalEntries > visibleRows) { - int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); - int totalScrollable = totalEntries; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); - int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; - int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; - display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); - } -} - -void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height - const int headerMargin = 2; // Extra pixels below header - const int labelGap = 6; - const int bitmapGapX = 4; - - // Find max emote height (assume all same, or precalculated) - int maxEmoteHeight = 0; - for (int i = 0; i < graphics::numEmotes; ++i) - if (graphics::emotes[i].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[i].height; - - const int rowHeight = maxEmoteHeight + 2; - - // Place header at top, then compute start of emote list - int headerY = y; - int listTop = headerY + headerFontHeight + headerMargin; - - int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; - int numEmotes = graphics::numEmotes; - - // keep member variable in sync - this->visibleRows = _visibleRows; - - // Clamp highlight index - if (emotePickerIndex < 0) - emotePickerIndex = 0; - if (emotePickerIndex >= numEmotes) - emotePickerIndex = numEmotes - 1; - - // Determine which emote is at the top - int topIndex = emotePickerIndex - _visibleRows / 2; - if (topIndex < 0) - topIndex = 0; - if (topIndex > numEmotes - _visibleRows) - topIndex = std::max(0, numEmotes - _visibleRows); - - // Draw header/title - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, headerY, "Select Emote"); - - // Draw emote rows - display->setTextAlignment(TEXT_ALIGN_LEFT); - - for (int vis = 0; vis < _visibleRows; ++vis) { - int emoteIdx = topIndex + vis; - if (emoteIdx >= numEmotes) - break; - const graphics::Emote &emote = graphics::emotes[emoteIdx]; - int rowY = listTop + vis * rowHeight; - - // Draw highlight box 2px taller than emote (1px margin above and below) - if (emoteIdx == emotePickerIndex) { - display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); - display->setColor(BLACK); - } - - // Emote bitmap (left), 1px margin from highlight bar top - int emoteY = rowY + 1; - display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); - - // Emote label (right of bitmap) - display->setFont(FONT_MEDIUM); - int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); - display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); - - if (emoteIdx == emotePickerIndex) - display->setColor(WHITE); - } - - // Draw scrollbar if needed - if (numEmotes > _visibleRows) { - int scrollbarHeight = _visibleRows * rowHeight; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); - int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); - int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; - display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); - } -} - -void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - this->displayHeight = display->getHeight(); // Store display height for later use - char buffer[50]; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // Never draw if state is outside our UI modes - if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { - return; // bail if not in a UI state that should render - } - - // Emote Picker Screen - if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { - drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here - return; - } - - // Destination Selection - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - drawDestinationSelectionScreen(display, state, x, y); - return; - } - - // Disabled Screen - if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); - return; - } - - // Free Text Input Screen - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - requestFocus(); -#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) - EInkDynamicDisplay *einkDisplay = static_cast(display); - einkDisplay->enableUnlimitedFastMode(); -#endif -#if defined(USE_VIRTUAL_KEYBOARD) - drawKeyboard(display, state, 0, 0); -#else - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // Draw node/channel header at the top - drawHeader(display, x, y, buffer); - - // Char count right-aligned - if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - uint16_t charsLeft = - meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); - snprintf(buffer, sizeof(buffer), "%d left", charsLeft); - display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); - } - -#if INPUTBROKER_SERIAL_TYPE == 1 - // Chatter Modifier key mode label (right side) - { - uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0; - const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#"; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - const int16_t th = FONT_HEIGHT_SMALL; - const int16_t tw = display->getStringWidth(label); - const int16_t padX = 3; - const int16_t padY = 2; - const int16_t r = 3; - - const int16_t bw = tw + padX * 2; - const int16_t bh = th + padY * 2; - - const int16_t bx = x + display->getWidth() - bw - 2; - const int16_t by = y + display->getHeight() - bh - 2; - - display->setColor(WHITE); - display->fillRect(bx + r, by, bw - r * 2, bh); - display->fillRect(bx, by + r, r, bh - r * 2); - display->fillRect(bx + bw - r, by + r, r, bh - r * 2); - display->fillCircle(bx + r, by + r, r); - display->fillCircle(bx + bw - r - 1, by + r, r); - display->fillCircle(bx + r, by + bh - r - 1, r); - display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); - - display->setColor(BLACK); - display->drawString(bx + padX, by + padY, label); - } - - // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + ◄”) - { - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - const char *label = "Dest: Shift + "; - int16_t labelW = display->getStringWidth(label); - - // triangle size visually matches glyph height, not full line height - const int triH = FONT_HEIGHT_SMALL - 3; - const int triW = triH * 0.7; - - const int16_t padX = 3; - const int16_t padY = 2; - const int16_t r = 3; - - const int16_t bw = labelW + triW + padX * 2 + 2; - const int16_t bh = FONT_HEIGHT_SMALL + padY * 2; - - const int16_t bx = x + 2; - const int16_t by = y + display->getHeight() - bh - 2; - - // Rounded white box - display->setColor(WHITE); - display->fillRect(bx + r, by, bw - (r * 2), bh); - display->fillRect(bx, by + r, r, bh - (r * 2)); - display->fillRect(bx + bw - r, by + r, r, bh - (r * 2)); - display->fillCircle(bx + r, by + r, r); - display->fillCircle(bx + bw - r - 1, by + r, r); - display->fillCircle(bx + r, by + bh - r - 1, r); - display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); - - // Draw text - display->setColor(BLACK); - display->drawString(bx + padX, by + padY, label); - - // Perfectly center triangle on text baseline - int16_t tx = bx + padX + labelW; - int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering - - // ◄ Left-pointing triangle - display->fillTriangle(tx + triW, ty, // top-right - tx, ty + triH / 2, // left center - tx + triW, ty + triH // bottom-right - ); - } -#endif - // Draw Free Text input with multi-emote support and proper line wrapping - display->setColor(WHITE); - { - int inputY = 0 + y + FONT_HEIGHT_SMALL; - String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); - - // Tokenize input into (isEmote, token) pairs - const char *msg = msgWithCursor.c_str(); - std::vector> tokens = tokenizeMessageWithEmotes(msg); - - // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) - std::vector>> lines; - std::vector> currentLine; - int lineWidth = 0; - int maxWidth = display->getWidth(); - for (auto &token : tokens) { - if (token.first) { - // Emote - int tokenWidth = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - tokenWidth = graphics::emotes[j].width + 2; - break; - } - } - if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back(token); - lineWidth += tokenWidth; - } else { - // Text: split by words and wrap inside word if needed - String text = token.second; - int pos = 0; - while (pos < static_cast(text.length())) { - // Find next space (or end) - int spacePos = text.indexOf(' ', pos); - int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space - String word = text.substring(pos, endPos); - int wordWidth = display->getStringWidth(word); - - if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - // If word itself too big, split by character - if (wordWidth > maxWidth) { - uint16_t charPos = 0; - while (charPos < word.length()) { - String oneChar = word.substring(charPos, charPos + 1); - int charWidth = display->getStringWidth(oneChar); - if (lineWidth + charWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back({false, oneChar}); - lineWidth += charWidth; - charPos++; - } - } else { - currentLine.push_back({false, word}); - lineWidth += wordWidth; - } - pos = endPos; - } - } - } - if (!currentLine.empty()) - lines.push_back(currentLine); - - // Draw lines with emotes - int rowHeight = FONT_HEIGHT_SMALL; - int yLine = inputY; - for (auto &line : lines) { - int nextX = x; - for (const auto &token : line) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, yLine, rowHeight, token.second); - } else { - display->drawString(nextX, yLine, token.second); - nextX += display->getStringWidth(token.second); - } - } - yLine += rowHeight; - } - } -#endif - return; - } - - // Canned Messages List - if (this->messagesCount > 0) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // Precompute per-row heights based on emotes (centered if present) - const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; - - int topMsg; - std::vector rowHeights; - int _visibleRows; - - // Draw header (To: ...) - drawHeader(display, x, y, buffer); - - // Shift message list upward by 3 pixels to reduce spacing between header and first message - const int listYOffset = y + FONT_HEIGHT_SMALL - 3; - _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; - - // Figure out which messages are visible and their needed heights - topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) - ? currentMessageIndex - _visibleRows + 2 - : 0; - int countRows = std::min(messagesCount, _visibleRows); - - // Build per-row max height based on all emotes in line - for (int i = 0; i < countRows; i++) { - const char *msg = getMessageByIndex(topMsg + i); - int maxEmoteHeight = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *search = msg; - while ((search = strstr(search, label))) { - if (graphics::emotes[j].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[j].height; - search += strlen(label); // Advance past this emote - } - } - rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); - } - - // Draw all message rows with multi-emote support - int yCursor = listYOffset; - for (int vis = 0; vis < countRows; vis++) { - int msgIdx = topMsg + vis; - int lineY = yCursor; - const char *msg = getMessageByIndex(msgIdx); - int rowHeight = rowHeights[vis]; - bool _highlight = (msgIdx == currentMessageIndex); - - // Multi-emote tokenization - std::vector> tokens = tokenizeMessageWithEmotes(msg); - - // Vertically center based on rowHeight - int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; - -#ifdef USE_EINK - int nextX = x + (_highlight ? 12 : 0); - if (_highlight) - display->drawString(x + 0, lineY + textYOffset, ">"); -#else - int scrollPadding = 8; - if (_highlight) { - display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); - display->setColor(BLACK); - } - int nextX = x + (_highlight ? 2 : 0); -#endif - - // Draw all tokens left to right - for (const auto &token : tokens) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, lineY, rowHeight, token.second); - } else { - // Text - display->drawString(nextX, lineY + textYOffset, token.second); - nextX += display->getStringWidth(token.second); - } - } -#ifndef USE_EINK - if (_highlight) - display->setColor(WHITE); -#endif - - yCursor += rowHeight; - } - - // Scrollbar - if (messagesCount > _visibleRows) { - int scrollHeight = display->getHeight() - listYOffset; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); - int barHeight = (scrollHeight * _visibleRows) / messagesCount; - int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; - display->fillRect(scrollTrackX, scrollPos, 4, barHeight); - } + if (messagesCount > _visibleRows) { + int scrollHeight = display->getHeight() - listYOffset; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); + int barHeight = (scrollHeight * _visibleRows) / messagesCount; + int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; + display->fillRect(scrollTrackX, scrollPos, 4, barHeight); } + } } // Return SNR limit based on modem preset -static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) -{ - switch (preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: - return -6.0f; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - return -5.5f; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - return -4.5f; - default: - return -6.0f; - } +static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) { + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return -6.0f; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return -5.5f; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return -4.5f; + default: + return -6.0f; + } } // Return Good/Fair/Bad label and set 1–5 bars based on SNR and RSSI -static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) -{ - // 5-bar logic: strength inside Good/Fair/Bad category - if (snr > snrLimit && rssi > -10) { - bars = 5; // very strong good - return "Good"; - } else if (snr > snrLimit && rssi > -20) { - bars = 4; // normal good - return "Good"; - } else if (snr > 0 && rssi > -50) { - bars = 3; // weaker good (on edge of fair) - return "Good"; - } else if (snr > -10 && rssi > -100) { - bars = 2; // fair - return "Fair"; - } else { - bars = 1; // bad - return "Bad"; - } +static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) { + // 5-bar logic: strength inside Good/Fair/Bad category + if (snr > snrLimit && rssi > -10) { + bars = 5; // very strong good + return "Good"; + } else if (snr > snrLimit && rssi > -20) { + bars = 4; // normal good + return "Good"; + } else if (snr > 0 && rssi > -50) { + bars = 3; // weaker good (on edge of fair) + return "Good"; + } else if (snr > -10 && rssi > -100) { + bars = 2; // fair + return "Fair"; + } else { + bars = 1; // bad + return "Bad"; + } } -ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) -{ - // Only process routing ACK/NACK packets that are responses to our own outbound - if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && - mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet - { - if (mp.decoded.request_id != 0) { - // Decode the routing response - meshtastic_Routing decoded = meshtastic_Routing_init_default; - pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); +ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { + // Only process routing ACK/NACK packets that are responses to our own outbound + if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && + mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet + { + if (mp.decoded.request_id != 0) { + // Decode the routing response + meshtastic_Routing decoded = meshtastic_Routing_init_default; + pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - // Determine ACK/NACK status - bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); - bool isFromDest = (mp.from == this->lastSentNode); - bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + // Determine ACK/NACK status + bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); + bool isFromDest = (mp.from == this->lastSentNode); + bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); - // Identify the responding node - if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { - this->incoming = mp.from; // relayed by another node - } else { - this->incoming = this->lastSentNode; // direct reply - } + // Identify the responding node + if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { + this->incoming = mp.from; // relayed by another node + } else { + this->incoming = this->lastSentNode; // direct reply + } - // Final ACK/NACK logic - if (wasBroadcast) { - // Any ACK counts for broadcast - this->ack = isAck; - waitingForAck = false; - } else if (isFromDest) { - // Only ACK from destination counts as final - this->ack = isAck; - waitingForAck = false; - } else if (isAck) { - // Relay ACK → mark as RELAYED, still no final ACK - this->ack = false; - waitingForAck = false; - } else { - // Explicit failure - this->ack = false; - waitingForAck = false; - } + // Final ACK/NACK logic + if (wasBroadcast) { + // Any ACK counts for broadcast + this->ack = isAck; + waitingForAck = false; + } else if (isFromDest) { + // Only ACK from destination counts as final + this->ack = isAck; + waitingForAck = false; + } else if (isAck) { + // Relay ACK → mark as RELAYED, still no final ACK + this->ack = false; + waitingForAck = false; + } else { + // Explicit failure + this->ack = false; + waitingForAck = false; + } - // Update last sent StoredMessage with ACK/NACK/RELAYED result - if (!messageStore.getMessages().empty()) { - StoredMessage &last = const_cast(messageStore.getMessages().back()); - if (last.sender == nodeDB->getNodeNum()) { // only update our own messages - if (wasBroadcast && isAck) { - last.ackStatus = AckStatus::ACKED; - } else if (isFromDest && isAck) { - last.ackStatus = AckStatus::ACKED; - } else if (!isFromDest && isAck) { - last.ackStatus = AckStatus::RELAYED; - } else { - last.ackStatus = AckStatus::NACKED; - } - } - } - - // Capture radio metrics - this->lastRxRssi = mp.rx_rssi; - this->lastRxSnr = mp.rx_snr; - - // Show overlay banner - if (screen) { - auto *display = screen->getDisplayDevice(); - graphics::BannerOverlayOptions opts; - static char buf[128]; - - const char *channelName = channels.getName(this->channel); - const char *src = getNodeName(this->incoming); - char nodeName[48]; - strncpy(nodeName, src, sizeof(nodeName) - 1); - nodeName[sizeof(nodeName) - 1] = '\0'; - - int availWidth = - display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(nodeName); - while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { - nodeName[strlen(nodeName) - 1] = '\0'; - } - if (strlen(nodeName) < origLen) { - strcat(nodeName, "..."); - } - - // Calculate signal quality and bars based on preset, SNR, and RSSI - float snrLimit = getSnrLimit(config.lora.modem_preset); - int bars = 0; - const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); - - if (this->ack) { - if (this->lastSentNode == NODENUM_BROADCAST) { - snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", - (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); - } else { - snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", - (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); - } - } else if (isAck && !isFromDest) { - // Relay ACK banner - snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", - (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); - } else { - if (this->lastSentNode == NODENUM_BROADCAST) { - snprintf(buf, sizeof(buf), "Message failed to\n#%s", - (channelName && channelName[0]) ? channelName : "unknown"); - } else { - snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); - } - } - - opts.message = buf; - opts.durationMs = 3000; - graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw - screen->showOverlayBanner(opts); // this triggers drawNotificationBox() - } + // Update last sent StoredMessage with ACK/NACK/RELAYED result + if (!messageStore.getMessages().empty()) { + StoredMessage &last = const_cast(messageStore.getMessages().back()); + if (last.sender == nodeDB->getNodeNum()) { // only update our own messages + if (wasBroadcast && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (isFromDest && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (!isFromDest && isAck) { + last.ackStatus = AckStatus::RELAYED; + } else { + last.ackStatus = AckStatus::NACKED; + } } - } + } - return ProcessMessage::CONTINUE; + // Capture radio metrics + this->lastRxRssi = mp.rx_rssi; + this->lastRxSnr = mp.rx_snr; + + // Show overlay banner + if (screen) { + auto *display = screen->getDisplayDevice(); + graphics::BannerOverlayOptions opts; + static char buf[128]; + + const char *channelName = channels.getName(this->channel); + const char *src = getNodeName(this->incoming); + char nodeName[48]; + strncpy(nodeName, src, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; + + int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(nodeName); + while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { + nodeName[strlen(nodeName) - 1] = '\0'; + } + if (strlen(nodeName) < origLen) { + strcat(nodeName, "..."); + } + + // Calculate signal quality and bars based on preset, SNR, and RSSI + float snrLimit = getSnrLimit(config.lora.modem_preset); + int bars = 0; + const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); + + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); + } else { + snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } + } else if (isAck && !isFromDest) { + // Relay ACK banner + snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", (nodeName && nodeName[0]) ? nodeName : "unknown", + qualityLabel); + } else { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message failed to\n#%s", (channelName && channelName[0]) ? channelName : "unknown"); + } else { + snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); + } + } + + opts.message = buf; + opts.durationMs = 3000; + graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw + screen->showOverlayBanner(opts); // this triggers drawNotificationBox() + } + } + } + + return ProcessMessage::CONTINUE; } -void CannedMessageModule::loadProtoForModule() -{ - if (nodeDB->loadProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, - sizeof(meshtastic_CannedMessageModuleConfig), &meshtastic_CannedMessageModuleConfig_msg, - &cannedMessageModuleConfig) != LoadFileResult::LOAD_SUCCESS) { - installDefaultCannedMessageModuleConfig(); - } +void CannedMessageModule::loadProtoForModule() { + if (nodeDB->loadProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, sizeof(meshtastic_CannedMessageModuleConfig), + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig) != LoadFileResult::LOAD_SUCCESS) { + installDefaultCannedMessageModuleConfig(); + } } /** * @brief Save the module config to file. @@ -2371,28 +2289,26 @@ void CannedMessageModule::loadProtoForModule() * @return true On success. * @return false On error. */ -bool CannedMessageModule::saveProtoForModule() -{ - bool okay = true; +bool CannedMessageModule::saveProtoForModule() { + bool okay = true; #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, - &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg, + &cannedMessageModuleConfig); - return okay; + return okay; } /** * @brief Fill configuration with default values. */ -void CannedMessageModule::installDefaultCannedMessageModuleConfig() -{ - strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); +void CannedMessageModule::installDefaultCannedMessageModuleConfig() { + strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); } /** @@ -2404,65 +2320,59 @@ void CannedMessageModule::installDefaultCannedMessageModuleConfig() * @return AdminMessageHandleResult HANDLED if message was handled * HANDLED_WITH_RESULT if a result is also prepared. */ -AdminMessageHandleResult CannedMessageModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) -{ - AdminMessageHandleResult result; +AdminMessageHandleResult CannedMessageModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + AdminMessageHandleResult result; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: - LOG_DEBUG("Client getting radio canned messages"); - this->handleGetCannedMessageModuleMessages(mp, response); - result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; - break; + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + LOG_DEBUG("Client getting radio canned messages"); + this->handleGetCannedMessageModuleMessages(mp, response); + result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; - case meshtastic_AdminMessage_set_canned_message_module_messages_tag: - LOG_DEBUG("Client getting radio canned messages"); - this->handleSetCannedMessageModuleMessages(request->set_canned_message_module_messages); - result = AdminMessageHandleResult::HANDLED; - break; + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + LOG_DEBUG("Client getting radio canned messages"); + this->handleSetCannedMessageModuleMessages(request->set_canned_message_module_messages); + result = AdminMessageHandleResult::HANDLED; + break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; - } + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } - return result; + return result; } -void CannedMessageModule::handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, - meshtastic_AdminMessage *response) -{ - LOG_DEBUG("*** handleGetCannedMessageModuleMessages"); - if (req.decoded.want_response) { - response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; - strncpy(response->get_canned_message_module_messages_response, cannedMessageModuleConfig.messages, - sizeof(response->get_canned_message_module_messages_response)); - } // Don't send anything if not instructed to. Better than asserting. +void CannedMessageModule::handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) { + LOG_DEBUG("*** handleGetCannedMessageModuleMessages"); + if (req.decoded.want_response) { + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, cannedMessageModuleConfig.messages, + sizeof(response->get_canned_message_module_messages_response)); + } // Don't send anything if not instructed to. Better than asserting. } -void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_msg) -{ - int changed = 0; +void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_msg) { + int changed = 0; - if (*from_msg) { - changed |= strcmp(cannedMessageModuleConfig.messages, from_msg); - strncpy(cannedMessageModuleConfig.messages, from_msg, sizeof(cannedMessageModuleConfig.messages)); - LOG_DEBUG("*** from_msg.text:%s", from_msg); - } + if (*from_msg) { + changed |= strcmp(cannedMessageModuleConfig.messages, from_msg); + strncpy(cannedMessageModuleConfig.messages, from_msg, sizeof(cannedMessageModuleConfig.messages)); + LOG_DEBUG("*** from_msg.text:%s", from_msg); + } - if (changed) { - this->saveProtoForModule(); - if (splitConfiguredMessages()) { - moduleConfig.canned_message.enabled = true; - } + if (changed) { + this->saveProtoForModule(); + if (splitConfiguredMessages()) { + moduleConfig.canned_message.enabled = true; } + } } -String CannedMessageModule::drawWithCursor(String text, int cursor) -{ - String result = text.substring(0, cursor) + "_" + text.substring(cursor); - return result; +String CannedMessageModule::drawWithCursor(String text, int cursor) { + String result = text.substring(0, cursor) + "_" + text.substring(cursor); + return result; } #endif diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 3d7c09d87..c25bc31d7 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -8,18 +8,18 @@ // ============================ enum cannedMessageModuleRunState { - CANNED_MESSAGE_RUN_STATE_DISABLED, - CANNED_MESSAGE_RUN_STATE_INACTIVE, - CANNED_MESSAGE_RUN_STATE_ACTIVE, - CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, - CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, - CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, - CANNED_MESSAGE_RUN_STATE_ACTION_UP, - CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, - CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, - CANNED_MESSAGE_RUN_STATE_FREETEXT, - CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, - CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER + CANNED_MESSAGE_RUN_STATE_DISABLED, + CANNED_MESSAGE_RUN_STATE_INACTIVE, + CANNED_MESSAGE_RUN_STATE_ACTIVE, + CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, + CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, + CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, + CANNED_MESSAGE_RUN_STATE_ACTION_UP, + CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, + CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, + CANNED_MESSAGE_RUN_STATE_FREETEXT, + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, + CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; @@ -36,233 +36,226 @@ enum CannedMessageModuleIconType { shift, backspace, space, enter }; // ============================ struct Letter { - String character; - float width; - int rectX; - int rectY; - int rectWidth; - int rectHeight; + String character; + float width; + int rectX; + int rectY; + int rectWidth; + int rectHeight; }; struct NodeEntry { - meshtastic_NodeInfoLite *node; - uint32_t lastHeard; + meshtastic_NodeInfoLite *node; + uint32_t lastHeard; }; // ============================ // Main Class // ============================ -class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread -{ - public: - CannedMessageModule(); +class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread { +public: + CannedMessageModule(); - void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); - void LaunchRepeatDestination(); - void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); + void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); + void LaunchRepeatDestination(); + void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); - // === Emote Picker navigation === - int emotePickerIndex = 0; // Tracks currently selected emote in the picker + // === Emote Picker navigation === + int emotePickerIndex = 0; // Tracks currently selected emote in the picker - // === Message navigation === - const char *getCurrentMessage(); - const char *getPrevMessage(); - const char *getNextMessage(); - const char *getMessageByIndex(int index); - const char *getNodeName(NodeNum node); + // === Message navigation === + const char *getCurrentMessage(); + const char *getPrevMessage(); + const char *getNextMessage(); + const char *getMessageByIndex(int index); + const char *getNodeName(NodeNum node); - // === State/UI === - bool shouldDraw(); - bool hasMessages(); - void resetSearch(); - void updateDestinationSelectionList(); - void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - bool isCharInputAllowed() const; - String drawWithCursor(String text, int cursor); + // === State/UI === + bool shouldDraw(); + bool hasMessages(); + void resetSearch(); + void updateDestinationSelectionList(); + void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + bool isCharInputAllowed() const; + String drawWithCursor(String text, int cursor); - // === Emote Picker === - int handleEmotePickerInput(const InputEvent *event); - void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // === Emote Picker === + int handleEmotePickerInput(const InputEvent *event); + void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - // === Admin Handlers === - void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); - void handleSetCannedMessageModuleMessages(const char *from_msg); + // === Admin Handlers === + void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); + void handleSetCannedMessageModuleMessages(const char *from_msg); #ifdef RAK14014 - cannedMessageModuleRunState getRunState() const { return runState; } + cannedMessageModuleRunState getRunState() const { return runState; } #endif - // === Packet Interest Filter === - virtual bool wantPacket(const meshtastic_MeshPacket *p) override - { - if (p->rx_rssi != 0) - lastRxRssi = p->rx_rssi; - if (p->rx_snr > 0) - lastRxSnr = p->rx_snr; - return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; - } + // === Packet Interest Filter === + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { + if (p->rx_rssi != 0) + lastRxRssi = p->rx_rssi; + if (p->rx_snr > 0) + lastRxSnr = p->rx_snr; + return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; + } - protected: - // === Thread Entry Point === - virtual int32_t runOnce() override; +protected: + // === Thread Entry Point === + virtual int32_t runOnce() override; - // === Transmission === - void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); - void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); - int splitConfiguredMessages(); - int getNextIndex(); - int getPrevIndex(); + // === Transmission === + void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); + void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); + int splitConfiguredMessages(); + int getNextIndex(); + int getPrevIndex(); #if defined(USE_VIRTUAL_KEYBOARD) - void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - String keyForCoordinates(uint x, uint y); - void drawShiftIcon(OLEDDisplay *display, int x, int y, float scale = 1); - void drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale = 1); - void drawEnterIcon(OLEDDisplay *display, int x, int y, float scale = 1); + void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + String keyForCoordinates(uint x, uint y); + void drawShiftIcon(OLEDDisplay *display, int x, int y, float scale = 1); + void drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale = 1); + void drawEnterIcon(OLEDDisplay *display, int x, int y, float scale = 1); #endif - // === Input Handling === - int handleInputEvent(const InputEvent *event); - virtual bool wantUIFrame() override { return shouldDraw(); } - virtual Observable *getUIFrameObservable() override { return this; } - virtual bool interceptingKeyboardInput() override; - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + // === Input Handling === + int handleInputEvent(const InputEvent *event); + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } + virtual bool interceptingKeyboardInput() override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - void loadProtoForModule(); - bool saveProtoForModule(); - void installDefaultCannedMessageModuleConfig(); + void loadProtoForModule(); + bool saveProtoForModule(); + void installDefaultCannedMessageModuleConfig(); - private: - // === Input Observers === - CallbackObserver inputObserver = - CallbackObserver(this, &CannedMessageModule::handleInputEvent); +private: + // === Input Observers === + CallbackObserver inputObserver = + CallbackObserver(this, &CannedMessageModule::handleInputEvent); - // === Display and UI === - int displayHeight = 64; - int destIndex = 0; - int scrollIndex = 0; - int visibleRows = 0; - bool needsUpdate = true; - unsigned long lastUpdateMillis = 0; - String searchQuery; - String freetext; + // === Display and UI === + int displayHeight = 64; + int destIndex = 0; + int scrollIndex = 0; + int visibleRows = 0; + bool needsUpdate = true; + unsigned long lastUpdateMillis = 0; + String searchQuery; + String freetext; - // === Message Storage === - char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; - char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; - int messagesCount = 0; - int currentMessageIndex = -1; + // === Message Storage === + char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; + char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; + int messagesCount = 0; + int currentMessageIndex = -1; - // === Routing & Acknowledgment === - NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) - NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received - NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) - ChannelIndex channel = 0; // Channel index used when sending a message + // === Routing & Acknowledgment === + NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) + NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received + NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) + ChannelIndex channel = 0; // Channel index used when sending a message - bool ack = false; // True = ACK received, False = NACK or failed - bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets - float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) - int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) - uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet + bool ack = false; // True = ACK received, False = NACK or failed + bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets + float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) + int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) + uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet - // === State Tracking === - cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - char highlight = 0x00; - char payload = 0x00; - unsigned int cursor = 0; - unsigned long lastTouchMillis = 0; - uint32_t lastFilterUpdate = 0; - static constexpr uint32_t filterDebounceMs = 30; - std::vector activeChannelIndices; - std::vector filteredNodes; + // === State Tracking === + cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + char highlight = 0x00; + char payload = 0x00; + unsigned int cursor = 0; + unsigned long lastTouchMillis = 0; + uint32_t lastFilterUpdate = 0; + static constexpr uint32_t filterDebounceMs = 30; + std::vector activeChannelIndices; + std::vector filteredNodes; #if defined(USE_VIRTUAL_KEYBOARD) - bool shift = false; - int charSet = 0; // 0=ABC, 1=123 + bool shift = false; + int charSet = 0; // 0=ABC, 1=123 #endif - bool isUpEvent(const InputEvent *event); - bool isDownEvent(const InputEvent *event); - bool isSelectEvent(const InputEvent *event); - bool handleTabSwitch(const InputEvent *event); - int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); - bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); - bool handleFreeTextInput(const InputEvent *event); + bool isUpEvent(const InputEvent *event); + bool isDownEvent(const InputEvent *event); + bool isSelectEvent(const InputEvent *event); + bool handleTabSwitch(const InputEvent *event); + int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleFreeTextInput(const InputEvent *event); #if defined(USE_VIRTUAL_KEYBOARD) - Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, - {"W", 22, 0, 0, 0, 0}, - {"E", 17, 0, 0, 0, 0}, - {"R", 16.5, 0, 0, 0, 0}, - {"T", 14, 0, 0, 0, 0}, - {"Y", 15, 0, 0, 0, 0}, - {"U", 16.5, 0, 0, 0, 0}, - {"I", 5, 0, 0, 0, 0}, - {"O", 19.5, 0, 0, 0, 0}, - {"P", 15.5, 0, 0, 0, 0}}, - {{"A", 14, 0, 0, 0, 0}, - {"S", 15, 0, 0, 0, 0}, - {"D", 16.5, 0, 0, 0, 0}, - {"F", 15, 0, 0, 0, 0}, - {"G", 17, 0, 0, 0, 0}, - {"H", 15.5, 0, 0, 0, 0}, - {"J", 12, 0, 0, 0, 0}, - {"K", 15.5, 0, 0, 0, 0}, - {"L", 14, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}}, - {{"⇧", 20, 0, 0, 0, 0}, - {"Z", 14, 0, 0, 0, 0}, - {"X", 14.5, 0, 0, 0, 0}, - {"C", 15.5, 0, 0, 0, 0}, - {"V", 13.5, 0, 0, 0, 0}, - {"B", 15, 0, 0, 0, 0}, - {"N", 15, 0, 0, 0, 0}, - {"M", 17, 0, 0, 0, 0}, - {"⌫", 20, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}}, - {{"123", 42, 0, 0, 0, 0}, - {" ", 64, 0, 0, 0, 0}, - {"↵", 36, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}}}, - {{{"1", 12, 0, 0, 0, 0}, - {"2", 13.5, 0, 0, 0, 0}, - {"3", 12.5, 0, 0, 0, 0}, - {"4", 14, 0, 0, 0, 0}, - {"5", 14, 0, 0, 0, 0}, - {"6", 14, 0, 0, 0, 0}, - {"7", 13.5, 0, 0, 0, 0}, - {"8", 14, 0, 0, 0, 0}, - {"9", 14, 0, 0, 0, 0}, - {"0", 14, 0, 0, 0, 0}}, - {{"-", 8, 0, 0, 0, 0}, - {"/", 8, 0, 0, 0, 0}, - {":", 4.5, 0, 0, 0, 0}, - {";", 4.5, 0, 0, 0, 0}, - {"(", 7, 0, 0, 0, 0}, - {")", 6.5, 0, 0, 0, 0}, - {"$", 12.5, 0, 0, 0, 0}, - {"&", 15, 0, 0, 0, 0}, - {"@", 21.5, 0, 0, 0, 0}, - {"\"", 8, 0, 0, 0, 0}}, - {{".", 8, 0, 0, 0, 0}, - {",", 8, 0, 0, 0, 0}, - {"?", 10, 0, 0, 0, 0}, - {"!", 10, 0, 0, 0, 0}, - {"'", 10, 0, 0, 0, 0}, - {"⌫", 20, 0, 0, 0, 0}}, - {{"ABC", 50, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}}}}; + Letter keyboard[2][4][10] = { + {{{"Q", 20, 0, 0, 0, 0}, + {"W", 22, 0, 0, 0, 0}, + {"E", 17, 0, 0, 0, 0}, + {"R", 16.5, 0, 0, 0, 0}, + {"T", 14, 0, 0, 0, 0}, + {"Y", 15, 0, 0, 0, 0}, + {"U", 16.5, 0, 0, 0, 0}, + {"I", 5, 0, 0, 0, 0}, + {"O", 19.5, 0, 0, 0, 0}, + {"P", 15.5, 0, 0, 0, 0}}, + {{"A", 14, 0, 0, 0, 0}, + {"S", 15, 0, 0, 0, 0}, + {"D", 16.5, 0, 0, 0, 0}, + {"F", 15, 0, 0, 0, 0}, + {"G", 17, 0, 0, 0, 0}, + {"H", 15.5, 0, 0, 0, 0}, + {"J", 12, 0, 0, 0, 0}, + {"K", 15.5, 0, 0, 0, 0}, + {"L", 14, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}}, + {{"⇧", 20, 0, 0, 0, 0}, + {"Z", 14, 0, 0, 0, 0}, + {"X", 14.5, 0, 0, 0, 0}, + {"C", 15.5, 0, 0, 0, 0}, + {"V", 13.5, 0, 0, 0, 0}, + {"B", 15, 0, 0, 0, 0}, + {"N", 15, 0, 0, 0, 0}, + {"M", 17, 0, 0, 0, 0}, + {"⌫", 20, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}}, + {{"123", 42, 0, 0, 0, 0}, + {" ", 64, 0, 0, 0, 0}, + {"↵", 36, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}}}, + {{{"1", 12, 0, 0, 0, 0}, + {"2", 13.5, 0, 0, 0, 0}, + {"3", 12.5, 0, 0, 0, 0}, + {"4", 14, 0, 0, 0, 0}, + {"5", 14, 0, 0, 0, 0}, + {"6", 14, 0, 0, 0, 0}, + {"7", 13.5, 0, 0, 0, 0}, + {"8", 14, 0, 0, 0, 0}, + {"9", 14, 0, 0, 0, 0}, + {"0", 14, 0, 0, 0, 0}}, + {{"-", 8, 0, 0, 0, 0}, + {"/", 8, 0, 0, 0, 0}, + {":", 4.5, 0, 0, 0, 0}, + {";", 4.5, 0, 0, 0, 0}, + {"(", 7, 0, 0, 0, 0}, + {")", 6.5, 0, 0, 0, 0}, + {"$", 12.5, 0, 0, 0, 0}, + {"&", 15, 0, 0, 0, 0}, + {"@", 21.5, 0, 0, 0, 0}, + {"\"", 8, 0, 0, 0, 0}}, + {{".", 8, 0, 0, 0, 0}, {",", 8, 0, 0, 0, 0}, {"?", 10, 0, 0, 0, 0}, {"!", 10, 0, 0, 0, 0}, {"'", 10, 0, 0, 0, 0}, {"⌫", 20, 0, 0, 0, 0}}, + {{"ABC", 50, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}}}}; #endif }; diff --git a/src/modules/DetectionSensorModule.cpp b/src/modules/DetectionSensorModule.cpp index ca682b772..32d214dc9 100644 --- a/src/modules/DetectionSensorModule.cpp +++ b/src/modules/DetectionSensorModule.cpp @@ -12,29 +12,26 @@ DetectionSensorModule *detectionSensorModule; #define DELAYED_INTERVAL 1000 typedef enum { - DetectionSensorVerdictDetected, - DetectionSensorVerdictSendState, - DetectionSensorVerdictNoop, + DetectionSensorVerdictDetected, + DetectionSensorVerdictSendState, + DetectionSensorVerdictNoop, } DetectionSensorTriggerVerdict; typedef DetectionSensorTriggerVerdict (*DetectionSensorTriggerHandler)(bool prev, bool current); -static DetectionSensorTriggerVerdict detection_trigger_logic_level(bool prev, bool current) -{ - return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; +static DetectionSensorTriggerVerdict detection_trigger_logic_level(bool prev, bool current) { + return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; } -static DetectionSensorTriggerVerdict detection_trigger_single_edge(bool prev, bool current) -{ - return (!prev && current) ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; +static DetectionSensorTriggerVerdict detection_trigger_single_edge(bool prev, bool current) { + return (!prev && current) ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; } -static DetectionSensorTriggerVerdict detection_trigger_either_edge(bool prev, bool current) -{ - if (prev == current) { - return DetectionSensorVerdictNoop; - } - return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictSendState; +static DetectionSensorTriggerVerdict detection_trigger_either_edge(bool prev, bool current) { + if (prev == current) { + return DetectionSensorVerdictNoop; + } + return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictSendState; } const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX + 1] = { @@ -46,119 +43,113 @@ const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_Det [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge, }; -int32_t DetectionSensorModule::runOnce() -{ - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ - // moduleConfig.detection_sensor.enabled = true; - // moduleConfig.detection_sensor.monitor_pin = 10; // WisBlock PIR IO6 - // moduleConfig.detection_sensor.monitor_pin = 21; // WisBlock RAK12013 Radar IO6 - // moduleConfig.detection_sensor.minimum_broadcast_secs = 30; - // moduleConfig.detection_sensor.state_broadcast_secs = 120; - // moduleConfig.detection_sensor.detection_trigger_type = - // meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; - // strcpy(moduleConfig.detection_sensor.name, "Motion"); +int32_t DetectionSensorModule::runOnce() { + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ + // moduleConfig.detection_sensor.enabled = true; + // moduleConfig.detection_sensor.monitor_pin = 10; // WisBlock PIR IO6 + // moduleConfig.detection_sensor.monitor_pin = 21; // WisBlock RAK12013 Radar IO6 + // moduleConfig.detection_sensor.minimum_broadcast_secs = 30; + // moduleConfig.detection_sensor.state_broadcast_secs = 120; + // moduleConfig.detection_sensor.detection_trigger_type = + // meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; + // strcpy(moduleConfig.detection_sensor.name, "Motion"); - if (moduleConfig.detection_sensor.enabled == false) - return disable(); + if (moduleConfig.detection_sensor.enabled == false) + return disable(); - if (firstTime) { + if (firstTime) { #ifdef DETECTION_SENSOR_EN - pinMode(DETECTION_SENSOR_EN, OUTPUT); - digitalWrite(DETECTION_SENSOR_EN, HIGH); + pinMode(DETECTION_SENSOR_EN, OUTPUT); + digitalWrite(DETECTION_SENSOR_EN, HIGH); #endif - // This is the first time the OSThread library has called this function, so do some setup - firstTime = false; - if (moduleConfig.detection_sensor.monitor_pin > 0) { - pinMode(moduleConfig.detection_sensor.monitor_pin, moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT); - } else { - LOG_WARN("Detection Sensor Module: Set to enabled but no monitor pin is set. Disable module"); - return disable(); - } - LOG_INFO("Detection Sensor Module: init"); - - return setStartDelay(); + // This is the first time the OSThread library has called this function, so do some setup + firstTime = false; + if (moduleConfig.detection_sensor.monitor_pin > 0) { + pinMode(moduleConfig.detection_sensor.monitor_pin, moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT); + } else { + LOG_WARN("Detection Sensor Module: Set to enabled but no monitor pin is set. Disable module"); + return disable(); } + LOG_INFO("Detection Sensor Module: init"); - // LOG_DEBUG("Detection Sensor Module: Current pin state: %i", digitalRead(moduleConfig.detection_sensor.monitor_pin)); + return setStartDelay(); + } - if (!Throttle::isWithinTimespanMs(lastSentToMesh, - Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { - bool isDetected = hasDetectionEvent(); - DetectionSensorTriggerVerdict verdict = - handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected); - wasDetected = isDetected; - switch (verdict) { - case DetectionSensorVerdictDetected: - sendDetectionMessage(); - return DELAYED_INTERVAL; - case DetectionSensorVerdictSendState: - sendCurrentStateMessage(isDetected); - return DELAYED_INTERVAL; - case DetectionSensorVerdictNoop: - break; - } + // LOG_DEBUG("Detection Sensor Module: Current pin state: %i", + // digitalRead(moduleConfig.detection_sensor.monitor_pin)); + + if (!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { + bool isDetected = hasDetectionEvent(); + DetectionSensorTriggerVerdict verdict = handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected); + wasDetected = isDetected; + switch (verdict) { + case DetectionSensorVerdictDetected: + sendDetectionMessage(); + return DELAYED_INTERVAL; + case DetectionSensorVerdictSendState: + sendCurrentStateMessage(isDetected); + return DELAYED_INTERVAL; + case DetectionSensorVerdictNoop: + break; } - // Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort - // of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only broadcast state - // change detections. - if (moduleConfig.detection_sensor.state_broadcast_secs > 0 && - !Throttle::isWithinTimespanMs(lastSentToMesh, - Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs, - default_telemetry_broadcast_interval_secs))) { - sendCurrentStateMessage(hasDetectionEvent()); - return DELAYED_INTERVAL; - } - return GPIO_POLLING_INTERVAL; + } + // Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort + // of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only + // broadcast state change detections. + if (moduleConfig.detection_sensor.state_broadcast_secs > 0 && + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs, + default_telemetry_broadcast_interval_secs))) { + sendCurrentStateMessage(hasDetectionEvent()); + return DELAYED_INTERVAL; + } + return GPIO_POLLING_INTERVAL; } -void DetectionSensorModule::sendDetectionMessage() -{ - LOG_DEBUG("Detected event observed. Send message"); - char *message = new char[40]; - sprintf(message, "%s detected", moduleConfig.detection_sensor.name); - meshtastic_MeshPacket *p = allocDataPacket(); - p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character - p->decoded.payload.size++; - } - lastSentToMesh = millis(); - if (!channels.isDefaultChannel(0)) { - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh(p); - } else - LOG_ERROR("Message not allow on Public channel"); - delete[] message; +void DetectionSensorModule::sendDetectionMessage() { + LOG_DEBUG("Detected event observed. Send message"); + char *message = new char[40]; + sprintf(message, "%s detected", moduleConfig.detection_sensor.name); + meshtastic_MeshPacket *p = allocDataPacket(); + p->want_ack = false; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character + p->decoded.payload.size++; + } + lastSentToMesh = millis(); + if (!channels.isDefaultChannel(0)) { + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + service->sendToMesh(p); + } else + LOG_ERROR("Message not allow on Public channel"); + delete[] message; } -void DetectionSensorModule::sendCurrentStateMessage(bool state) -{ - char *message = new char[40]; - sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state); - meshtastic_MeshPacket *p = allocDataPacket(); - p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - lastSentToMesh = millis(); - if (!channels.isDefaultChannel(0)) { - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh(p); - } else - LOG_ERROR("Message not allow on Public channel"); - delete[] message; +void DetectionSensorModule::sendCurrentStateMessage(bool state) { + char *message = new char[40]; + sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state); + meshtastic_MeshPacket *p = allocDataPacket(); + p->want_ack = false; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + lastSentToMesh = millis(); + if (!channels.isDefaultChannel(0)) { + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + service->sendToMesh(p); + } else + LOG_ERROR("Message not allow on Public channel"); + delete[] message; } -bool DetectionSensorModule::hasDetectionEvent() -{ - bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin); - // LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState); - return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState; +bool DetectionSensorModule::hasDetectionEvent() { + bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin); + // LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState); + return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState; } \ No newline at end of file diff --git a/src/modules/DetectionSensorModule.h b/src/modules/DetectionSensorModule.h index 3ba10d329..d288ca1f4 100644 --- a/src/modules/DetectionSensorModule.h +++ b/src/modules/DetectionSensorModule.h @@ -1,23 +1,20 @@ #pragma once #include "SinglePortModule.h" -class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread -{ - public: - DetectionSensorModule() : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensor") - { - } +class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread { +public: + DetectionSensorModule() : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensor") {} - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; - private: - bool firstTime = true; - uint32_t lastSentToMesh = 0; - bool wasDetected = false; - void sendDetectionMessage(); - void sendCurrentStateMessage(bool state); - bool hasDetectionEvent(); +private: + bool firstTime = true; + uint32_t lastSentToMesh = 0; + bool wasDetected = false; + void sendDetectionMessage(); + void sendCurrentStateMessage(bool state); + bool hasDetectionEvent(); }; extern DetectionSensorModule *detectionSensorModule; \ No newline at end of file diff --git a/src/modules/DropzoneModule.cpp b/src/modules/DropzoneModule.cpp index 4b8f2fec8..c06ad8106 100644 --- a/src/modules/DropzoneModule.cpp +++ b/src/modules/DropzoneModule.cpp @@ -16,78 +16,75 @@ DropzoneModule *dropzoneModule; -int32_t DropzoneModule::runOnce() -{ - // Send on a 5 second delay from receiving the matching request - if (startSendConditions != 0 && (startSendConditions + 5000U) < millis()) { - service->sendToMesh(sendConditions(), RX_SRC_LOCAL); - startSendConditions = 0; - } - // Run every second to check if we need to send conditions - return 1000; +int32_t DropzoneModule::runOnce() { + // Send on a 5 second delay from receiving the matching request + if (startSendConditions != 0 && (startSendConditions + 5000U) < millis()) { + service->sendToMesh(sendConditions(), RX_SRC_LOCAL); + startSendConditions = 0; + } + // Run every second to check if we need to send conditions + return 1000; } -ProcessMessage DropzoneModule::handleReceived(const meshtastic_MeshPacket &mp) -{ - auto &p = mp.decoded; - char matchCompare[54]; - auto incomingMessage = reinterpret_cast(p.payload.bytes); - sprintf(matchCompare, "%s conditions", owner.short_name); - if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { - LOG_DEBUG("Received dropzone conditions request"); - startSendConditions = millis(); - } +ProcessMessage DropzoneModule::handleReceived(const meshtastic_MeshPacket &mp) { + auto &p = mp.decoded; + char matchCompare[54]; + auto incomingMessage = reinterpret_cast(p.payload.bytes); + sprintf(matchCompare, "%s conditions", owner.short_name); + if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { + LOG_DEBUG("Received dropzone conditions request"); + startSendConditions = millis(); + } - sprintf(matchCompare, "%s conditions", owner.long_name); - if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { - LOG_DEBUG("Received dropzone conditions request"); - startSendConditions = millis(); - } - return ProcessMessage::CONTINUE; + sprintf(matchCompare, "%s conditions", owner.long_name); + if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { + LOG_DEBUG("Received dropzone conditions request"); + startSendConditions = millis(); + } + return ProcessMessage::CONTINUE; } -meshtastic_MeshPacket *DropzoneModule::sendConditions() -{ - char replyStr[200]; - /* - CLOSED @ {HH:MM:SS}z - Wind 2 kts @ 125° - 29.25 inHg 72°C - */ - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - int hour = 0, min = 0, sec = 0; - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; +meshtastic_MeshPacket *DropzoneModule::sendConditions() { + char replyStr[200]; + /* + CLOSED @ {HH:MM:SS}z + Wind 2 kts @ 125° + 29.25 inHg 72°C + */ + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + int hour = 0, min = 0, sec = 0; + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - graphics::decomposeTime(rtc_sec, hour, min, sec); - } + graphics::decomposeTime(rtc_sec, hour, min, sec); + } - // Check if the dropzone is open or closed by reading the analog pin - // If pin is connected to GND (below 100 should be lower than floating voltage), - // the dropzone is open - auto dropzoneStatus = analogRead(A1) < 100 ? "OPEN" : "CLOSED"; - auto reply = allocDataPacket(); + // Check if the dropzone is open or closed by reading the analog pin + // If pin is connected to GND (below 100 should be lower than floating voltage), + // the dropzone is open + auto dropzoneStatus = analogRead(A1) < 100 ? "OPEN" : "CLOSED"; + auto reply = allocDataPacket(); - auto node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (sensor.hasSensor()) { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - sensor.getMetrics(&telemetry); - auto windSpeed = UnitConversions::MetersPerSecondToKnots(telemetry.variant.environment_metrics.wind_speed); - auto windDirection = telemetry.variant.environment_metrics.wind_direction; - auto temp = telemetry.variant.environment_metrics.temperature; - auto baro = UnitConversions::HectoPascalToInchesOfMercury(telemetry.variant.environment_metrics.barometric_pressure); - sprintf(replyStr, "%s @ %02d:%02d:%02dz\nWind %.2f kts @ %d°\nBaro %.2f inHg %.2f°C", dropzoneStatus, hour, min, sec, - windSpeed, windDirection, baro, temp); - } else { - LOG_ERROR("No sensor found"); - sprintf(replyStr, "%s @ %02d:%02d:%02d\nNo sensor found", dropzoneStatus, hour, min, sec); - } - LOG_DEBUG("Conditions reply: %s", replyStr); - reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply - memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); + auto node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (sensor.hasSensor()) { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + sensor.getMetrics(&telemetry); + auto windSpeed = UnitConversions::MetersPerSecondToKnots(telemetry.variant.environment_metrics.wind_speed); + auto windDirection = telemetry.variant.environment_metrics.wind_direction; + auto temp = telemetry.variant.environment_metrics.temperature; + auto baro = UnitConversions::HectoPascalToInchesOfMercury(telemetry.variant.environment_metrics.barometric_pressure); + sprintf(replyStr, "%s @ %02d:%02d:%02dz\nWind %.2f kts @ %d°\nBaro %.2f inHg %.2f°C", dropzoneStatus, hour, min, sec, windSpeed, windDirection, + baro, temp); + } else { + LOG_ERROR("No sensor found"); + sprintf(replyStr, "%s @ %02d:%02d:%02d\nNo sensor found", dropzoneStatus, hour, min, sec); + } + LOG_DEBUG("Conditions reply: %s", replyStr); + reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply + memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); - return reply; + return reply; } #endif \ No newline at end of file diff --git a/src/modules/DropzoneModule.h b/src/modules/DropzoneModule.h index 9e79ab447..7d8f258d3 100644 --- a/src/modules/DropzoneModule.h +++ b/src/modules/DropzoneModule.h @@ -7,30 +7,28 @@ * An example module that replies to a message with the current conditions * and status at the dropzone when it receives a text message mentioning it's name followed by "conditions" */ -class DropzoneModule : public SinglePortModule, private concurrency::OSThread -{ - DFRobotLarkSensor sensor; +class DropzoneModule : public SinglePortModule, private concurrency::OSThread { + DFRobotLarkSensor sensor; - public: - /** Constructor - * name is for debugging output - */ - DropzoneModule() : SinglePortModule("dropzone", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("Dropzone") - { - // Set up the analog pin for reading the dropzone status - pinMode(PIN_A1, INPUT); - } +public: + /** Constructor + * name is for debugging output + */ + DropzoneModule() : SinglePortModule("dropzone", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("Dropzone") { + // Set up the analog pin for reading the dropzone status + pinMode(PIN_A1, INPUT); + } - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - protected: - /** Called to handle a particular incoming message - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; +protected: + /** Called to handle a particular incoming message + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - private: - meshtastic_MeshPacket *sendConditions(); - uint32_t startSendConditions = 0; +private: + meshtastic_MeshPacket *sendConditions(); + uint32_t startSendConditions = 0; }; extern DropzoneModule *dropzoneModule; diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 6d52a3e46..8be9681e9 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -2,10 +2,10 @@ * @file ExternalNotificationModule.cpp * @brief Implementation of the ExternalNotificationModule class. * - * This file contains the implementation of the ExternalNotificationModule class, which is responsible for handling external - * notifications such as vibration, buzzer, and LED lights. The class provides methods to turn on and off the external - * notification outputs and to play ringtones using PWM buzzer. It also includes default configurations and a runOnce() method to - * handle the module's behavior. + * This file contains the implementation of the ExternalNotificationModule class, which is responsible for handling + * external notifications such as vibration, buzzer, and LED lights. The class provides methods to turn on and off the + * external notification outputs and to play ringtones using PWM buzzer. It also includes default configurations and a + * runOnce() method to handle the module's behavior. * * Documentation: * https://meshtastic.org/docs/configuration/module/external-notification @@ -83,140 +83,133 @@ uint32_t externalTurnedOn[3] = {}; static const char *rtttlConfigFile = "/prefs/ringtone.proto"; -int32_t ExternalNotificationModule::runOnce() -{ - if (!moduleConfig.external_notification.enabled) { - return INT32_MAX; // we don't need this thread here... - } else { - uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS; - bool isRtttlPlaying = rtttl::isPlaying(); +int32_t ExternalNotificationModule::runOnce() { + if (!moduleConfig.external_notification.enabled) { + return INT32_MAX; // we don't need this thread here... + } else { + uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS; + bool isRtttlPlaying = rtttl::isPlaying(); #ifdef HAS_I2S - // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop - isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying(); + // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop + isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying(); #endif - if ((nagCycleCutoff < millis()) && !isRtttlPlaying) { - // Turn off external notification immediately when timeout is reached, regardless of song state - nagCycleCutoff = UINT32_MAX; - ExternalNotificationModule::stopNow(); - isNagging = false; - return INT32_MAX; // save cycles till we're needed again - } + if ((nagCycleCutoff < millis()) && !isRtttlPlaying) { + // Turn off external notification immediately when timeout is reached, regardless of song state + nagCycleCutoff = UINT32_MAX; + ExternalNotificationModule::stopNow(); + isNagging = false; + return INT32_MAX; // save cycles till we're needed again + } - // If the output is turned on, turn it back off after the given period of time. - if (isNagging) { - delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms - : EXT_NOTIFICATION_MODULE_OUTPUT_MS); - if (externalTurnedOn[0] + delay < millis()) { - setExternalState(0, !getExternal(0)); - } - if (externalTurnedOn[1] + delay < millis()) { - setExternalState(1, !getExternal(1)); - } - // Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL) - if (!moduleConfig.external_notification.use_pwm && externalTurnedOn[2] + delay < millis()) { - LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms, - millis()); - setExternalState(2, !getExternal(2)); - } + // If the output is turned on, turn it back off after the given period of time. + if (isNagging) { + delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms : EXT_NOTIFICATION_MODULE_OUTPUT_MS); + if (externalTurnedOn[0] + delay < millis()) { + setExternalState(0, !getExternal(0)); + } + if (externalTurnedOn[1] + delay < millis()) { + setExternalState(1, !getExternal(1)); + } + // Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL) + if (!moduleConfig.external_notification.use_pwm && externalTurnedOn[2] + delay < millis()) { + LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms, millis()); + setExternalState(2, !getExternal(2)); + } #if defined(HAS_RGB_LED) - red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7 - green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 - blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 - white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; + red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7 + green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 + blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 + white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; #ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } #endif #ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } + if (rgb_found.type == ScanI2C::LP5562) { + rgbw.setColor(red, green, blue, white); + } #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); + analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic + analogWrite(RGBLED_GREEN, 255 - green); + analogWrite(RGBLED_BLUE, 255 - blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); + analogWrite(RGBLED_RED, red); + analogWrite(RGBLED_GREEN, green); + analogWrite(RGBLED_BLUE, blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); #endif #ifdef UNPHONE - unphone.rgb(red, green, blue); + unphone.rgb(red, green, blue); #endif - if (ascending) { // fade in - brightnessIndex++; - if (brightnessIndex == (sizeof(brightnessValues) - 1)) { - ascending = false; - } - } else { - brightnessIndex--; // fade out - } - if (brightnessIndex == 0) { - ascending = true; - colorState++; // next color - if (colorState > 7) { - colorState = 1; - } - } - // we need fast updates for the color change - delay = EXT_NOTIFICATION_FAST_THREAD_MS; + if (ascending) { // fade in + brightnessIndex++; + if (brightnessIndex == (sizeof(brightnessValues) - 1)) { + ascending = false; + } + } else { + brightnessIndex--; // fade out + } + if (brightnessIndex == 0) { + ascending = true; + colorState++; // next color + if (colorState > 7) { + colorState = 1; + } + } + // we need fast updates for the color change + delay = EXT_NOTIFICATION_FAST_THREAD_MS; #endif #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - drv.go(); + drv.go(); #endif - } - - // Play RTTTL over i2s audio interface if enabled as buzzer -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - if (audioThread->isPlaying()) { - // Continue playing - } else if (isNagging && (nagCycleCutoff >= millis())) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } - // we need fast updates to play the RTTTL - delay = EXT_NOTIFICATION_FAST_THREAD_MS; - } -#endif - // now let the PWM buzzer play - if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { - if (rtttl::isPlaying()) { - rtttl::play(); - } else if (isNagging && (nagCycleCutoff >= millis())) { - // start the song again if we have time left - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - // we need fast updates to play the RTTTL - delay = EXT_NOTIFICATION_FAST_THREAD_MS; - } - - return delay; } + + // Play RTTTL over i2s audio interface if enabled as buzzer +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + if (audioThread->isPlaying()) { + // Continue playing + } else if (isNagging && (nagCycleCutoff >= millis())) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } + // we need fast updates to play the RTTTL + delay = EXT_NOTIFICATION_FAST_THREAD_MS; + } +#endif + // now let the PWM buzzer play + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { + if (rtttl::isPlaying()) { + rtttl::play(); + } else if (isNagging && (nagCycleCutoff >= millis())) { + // start the song again if we have time left + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } + // we need fast updates to play the RTTTL + delay = EXT_NOTIFICATION_FAST_THREAD_MS; + } + + return delay; + } } /** * Based on buzzer mode, return true if we can buzz. */ -bool ExternalNotificationModule::canBuzz() -{ - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && - config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { - return true; - } - return false; +bool ExternalNotificationModule::canBuzz() { + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + return true; + } + return false; } -bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) -{ - return MeshService::isTextPayload(p); -} +bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } /** * Sets the external notification for the specified index. @@ -224,365 +217,349 @@ bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) * @param index The index of the external notification to change state. * @param on Whether we are turning things on (true) or off (false). */ -void ExternalNotificationModule::setExternalState(uint8_t index, bool on) -{ - externalCurrentState[index] = on; - externalTurnedOn[index] = millis(); +void ExternalNotificationModule::setExternalState(uint8_t index, bool on) { + externalCurrentState[index] = on; + externalTurnedOn[index] = millis(); - switch (index) { - case 1: + switch (index) { + case 1: #ifdef UNPHONE - unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander + unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander #endif - if (moduleConfig.external_notification.output_vibra) - digitalWrite(moduleConfig.external_notification.output_vibra, on); - break; - case 2: - // Only control buzzer pin digitally if not using PWM mode - if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm) - digitalWrite(moduleConfig.external_notification.output_buzzer, on); - break; - default: - if (output > 0) - digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); - break; - } + if (moduleConfig.external_notification.output_vibra) + digitalWrite(moduleConfig.external_notification.output_vibra, on); + break; + case 2: + // Only control buzzer pin digitally if not using PWM mode + if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm) + digitalWrite(moduleConfig.external_notification.output_buzzer, on); + break; + default: + if (output > 0) + digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); + break; + } #if defined(HAS_RGB_LED) - if (!on) { - red = 0; - green = 0; - blue = 0; - white = 0; - } + if (!on) { + red = 0; + green = 0; + blue = 0; + white = 0; + } #endif #ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } #endif #ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } + if (rgb_found.type == ScanI2C::LP5562) { + rgbw.setColor(red, green, blue, white); + } #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); + analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic + analogWrite(RGBLED_GREEN, 255 - green); + analogWrite(RGBLED_BLUE, 255 - blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); + analogWrite(RGBLED_RED, red); + analogWrite(RGBLED_GREEN, green); + analogWrite(RGBLED_BLUE, blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); #endif #ifdef UNPHONE - unphone.rgb(red, green, blue); + unphone.rgb(red, green, blue); #endif #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - if (on) { - drv.go(); - } else { - drv.stop(); - } + if (on) { + drv.go(); + } else { + drv.stop(); + } #endif } -bool ExternalNotificationModule::getExternal(uint8_t index) -{ - return externalCurrentState[index]; -} +bool ExternalNotificationModule::getExternal(uint8_t index) { return externalCurrentState[index]; } // Allow other firmware components to determine whether a notification is ongoing -bool ExternalNotificationModule::nagging() -{ - return isNagging; -} +bool ExternalNotificationModule::nagging() { return isNagging; } -void ExternalNotificationModule::stopNow() -{ - LOG_INFO("Turning off external notification: "); - LOG_INFO("Stop RTTTL playback"); - rtttl::stop(); +void ExternalNotificationModule::stopNow() { + LOG_INFO("Turning off external notification: "); + LOG_INFO("Stop RTTTL playback"); + rtttl::stop(); #ifdef HAS_I2S - LOG_INFO("Stop audioThread playback"); - audioThread->stop(); + LOG_INFO("Stop audioThread playback"); + audioThread->stop(); #endif - // Turn off all outputs - LOG_INFO("Turning off setExternalStates"); - for (int i = 0; i < 3; i++) { - setExternalState(i, false); - externalTurnedOn[i] = 0; - } - setIntervalFromNow(0); + // Turn off all outputs + LOG_INFO("Turning off setExternalStates"); + for (int i = 0; i < 3; i++) { + setExternalState(i, false); + externalTurnedOn[i] = 0; + } + setIntervalFromNow(0); #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - drv.stop(); + drv.stop(); #endif - // Prevent the state machine from immediately re-triggering outputs after a manual stop. - isNagging = false; - nagCycleCutoff = UINT32_MAX; + // Prevent the state machine from immediately re-triggering outputs after a manual stop. + isNagging = false; + nagCycleCutoff = UINT32_MAX; #ifdef HAS_I2S - // GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library - // T-Deck uses GPIO0 as trackball button, so restore the mode + // GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library + // T-Deck uses GPIO0 as trackball button, so restore the mode #if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0) - pinMode(0, INPUT); + pinMode(0, INPUT); #endif #endif } ExternalNotificationModule::ExternalNotificationModule() - : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), - concurrency::OSThread("ExternalNotification") -{ - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("ExternalNotification") { + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.external_notification.alert_message = true; - // moduleConfig.external_notification.alert_message_buzzer = true; - // moduleConfig.external_notification.alert_message_vibra = true; - // moduleConfig.external_notification.use_i2s_as_buzzer = true; + // moduleConfig.external_notification.alert_message = true; + // moduleConfig.external_notification.alert_message_buzzer = true; + // moduleConfig.external_notification.alert_message_vibra = true; + // moduleConfig.external_notification.use_i2s_as_buzzer = true; - // moduleConfig.external_notification.active = true; - // moduleConfig.external_notification.alert_bell = 1; - // moduleConfig.external_notification.output_ms = 1000; - // moduleConfig.external_notification.output = 4; // RAK4631 IO4 - // moduleConfig.external_notification.output_buzzer = 10; // RAK4631 IO6 - // moduleConfig.external_notification.output_vibra = 28; // RAK4631 IO7 - // moduleConfig.external_notification.nag_timeout = 300; + // moduleConfig.external_notification.active = true; + // moduleConfig.external_notification.alert_bell = 1; + // moduleConfig.external_notification.output_ms = 1000; + // moduleConfig.external_notification.output = 4; // RAK4631 IO4 + // moduleConfig.external_notification.output_buzzer = 10; // RAK4631 IO6 + // moduleConfig.external_notification.output_vibra = 28; // RAK4631 IO7 + // moduleConfig.external_notification.nag_timeout = 300; - // T-Watch / T-Deck i2s audio as buzzer: - // moduleConfig.external_notification.enabled = true; - // moduleConfig.external_notification.nag_timeout = 300; - // moduleConfig.external_notification.output_ms = 1000; - // moduleConfig.external_notification.use_i2s_as_buzzer = true; - // moduleConfig.external_notification.alert_message_buzzer = true; + // T-Watch / T-Deck i2s audio as buzzer: + // moduleConfig.external_notification.enabled = true; + // moduleConfig.external_notification.nag_timeout = 300; + // moduleConfig.external_notification.output_ms = 1000; + // moduleConfig.external_notification.use_i2s_as_buzzer = true; + // moduleConfig.external_notification.alert_message_buzzer = true; - if (moduleConfig.external_notification.enabled) { + if (moduleConfig.external_notification.enabled) { #if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) - if (inputBroker) // put our callback in the inputObserver list - inputObserver.observe(inputBroker); + if (inputBroker) // put our callback in the inputObserver list + inputObserver.observe(inputBroker); #endif - if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), - &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { - memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); - // The default ringtone is always loaded from userPrefs.jsonc - strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone)); - } + if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != + LoadFileResult::LOAD_SUCCESS) { + memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); + // The default ringtone is always loaded from userPrefs.jsonc + strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone)); + } - LOG_INFO("Init External Notification Module"); + LOG_INFO("Init External Notification Module"); - output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output - : EXT_NOTIFICATION_MODULE_OUTPUT; + output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output : EXT_NOTIFICATION_MODULE_OUTPUT; - // Set the direction of a pin - if (output > 0) { - LOG_INFO("Use Pin %i in digital mode", output); - pinMode(output, OUTPUT); - } - setExternalState(0, false); - externalTurnedOn[0] = 0; - if (moduleConfig.external_notification.output_vibra) { - LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra); - pinMode(moduleConfig.external_notification.output_vibra, OUTPUT); - setExternalState(1, false); - externalTurnedOn[1] = 0; - } - if (moduleConfig.external_notification.output_buzzer && canBuzz()) { - if (!moduleConfig.external_notification.use_pwm) { - LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); - pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); - setExternalState(2, false); - externalTurnedOn[2] = 0; - } else { - config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER; - // in PWM Mode we force the buzzer pin if it is set - LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); - } - } + // Set the direction of a pin + if (output > 0) { + LOG_INFO("Use Pin %i in digital mode", output); + pinMode(output, OUTPUT); + } + setExternalState(0, false); + externalTurnedOn[0] = 0; + if (moduleConfig.external_notification.output_vibra) { + LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra); + pinMode(moduleConfig.external_notification.output_vibra, OUTPUT); + setExternalState(1, false); + externalTurnedOn[1] = 0; + } + if (moduleConfig.external_notification.output_buzzer && canBuzz()) { + if (!moduleConfig.external_notification.use_pwm) { + LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); + pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); + setExternalState(2, false); + externalTurnedOn[2] = 0; + } else { + config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER; + // in PWM Mode we force the buzzer pin if it is set + LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); + } + } #ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.begin(); - rgb.setCurrent(10); - } + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.begin(); + rgb.setCurrent(10); + } #endif #ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.begin(); - rgbw.setCurrent(20); - } + if (rgb_found.type == ScanI2C::LP5562) { + rgbw.begin(); + rgbw.setCurrent(20); + } #endif #ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); + pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins + pinMode(RGBLED_GREEN, OUTPUT); + pinMode(RGBLED_BLUE, OUTPUT); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed - analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off - analogWrite(RGBLED_BLUE, 255); + analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed + analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off + analogWrite(RGBLED_BLUE, 255); #endif #ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); + pixels.begin(); // Initialise the pixel(s) + pixels.clear(); // Set all pixel colors to 'off' + pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - } else { - LOG_INFO("External Notification Module Disabled"); - disable(); - } + } else { + LOG_INFO("External Notification Module Disabled"); + disable(); + } } -ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) -{ - if (moduleConfig.external_notification.enabled && !isSilenced) { +ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) { + if (moduleConfig.external_notification.enabled && !isSilenced) { #ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); + drv.setWaveform(0, 75); + drv.setWaveform(1, 56); + drv.setWaveform(2, 0); + drv.go(); #endif - if (!isFromUs(&mp)) { - // Check if the message contains a bell character. Don't do this loop for every pin, just once. - auto &p = mp.decoded; - bool containsBell = false; - for (size_t i = 0; i < p.payload.size; i++) { - if (p.payload.bytes[i] == ASCII_BELL) { - containsBell = true; - } - } - - meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); - if (moduleConfig.external_notification.alert_bell) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_bell_vibra) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); - isNagging = true; - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_message && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - - if (moduleConfig.external_notification.alert_message_vibra && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - - if (moduleConfig.external_notification.alert_message_buzzer && - (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (!isBroadcast(mp.to) && isToUs(&mp))) { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us - isNagging = true; -#ifdef T_LORA_PAGER - if (canBuzz()) { - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); - } -#endif - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } else { - // Don't beep if buzzer mode is "direct messages only" and it is no direct message - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); - } - } - setIntervalFromNow(0); // run once so we know if we should do something + if (!isFromUs(&mp)) { + // Check if the message contains a bell character. Don't do this loop for every pin, just once. + auto &p = mp.decoded; + bool containsBell = false; + for (size_t i = 0; i < p.payload.size; i++) { + if (p.payload.bytes[i] == ASCII_BELL) { + containsBell = true; } - } else { - LOG_INFO("External Notification Module Disabled or muted"); - } + } - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); + if (moduleConfig.external_notification.alert_bell) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell"); + isNagging = true; + setExternalState(0, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + } + + if (moduleConfig.external_notification.alert_bell_vibra) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); + isNagging = true; + setExternalState(1, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + } + + if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); + isNagging = true; + if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { + setExternalState(2, true); + } else { +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } else +#endif + if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } + } + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + } + + if (moduleConfig.external_notification.alert_message && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + LOG_INFO("externalNotificationModule - Notification Module"); + isNagging = true; + setExternalState(0, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + + if (moduleConfig.external_notification.alert_message_vibra && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); + isNagging = true; + setExternalState(1, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + + if (moduleConfig.external_notification.alert_message_buzzer && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || (!isBroadcast(mp.to) && isToUs(&mp))) { + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us + isNagging = true; +#ifdef T_LORA_PAGER + if (canBuzz()) { + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); + } +#endif + if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { + setExternalState(2, true); + } else { +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } else +#endif + if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } + } + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } else { + // Don't beep if buzzer mode is "direct messages only" and it is no direct message + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + } + } + setIntervalFromNow(0); // run once so we know if we should do something + } + } else { + LOG_INFO("External Notification Module Disabled or muted"); + } + + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** @@ -594,61 +571,56 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP * @return AdminMessageHandleResult HANDLED if message was handled * HANDLED_WITH_RESULT if a result is also prepared. */ -AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) -{ - AdminMessageHandleResult result; +AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + AdminMessageHandleResult result; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_get_ringtone_request_tag: - LOG_INFO("Client getting ringtone"); - this->handleGetRingtone(mp, response); - result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; - break; + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_get_ringtone_request_tag: + LOG_INFO("Client getting ringtone"); + this->handleGetRingtone(mp, response); + result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; - case meshtastic_AdminMessage_set_ringtone_message_tag: - LOG_INFO("Client setting ringtone"); - this->handleSetRingtone(request->set_canned_message_module_messages); - result = AdminMessageHandleResult::HANDLED; - break; + case meshtastic_AdminMessage_set_ringtone_message_tag: + LOG_INFO("Client setting ringtone"); + this->handleSetRingtone(request->set_canned_message_module_messages); + result = AdminMessageHandleResult::HANDLED; + break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; - } + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } - return result; + return result; } -void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) -{ - LOG_INFO("*** handleGetRingtone"); - if (req.decoded.want_response) { - response->which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag; - strncpy(response->get_ringtone_response, rtttlConfig.ringtone, sizeof(response->get_ringtone_response)); - } // Don't send anything if not instructed to. Better than asserting. +void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) { + LOG_INFO("*** handleGetRingtone"); + if (req.decoded.want_response) { + response->which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag; + strncpy(response->get_ringtone_response, rtttlConfig.ringtone, sizeof(response->get_ringtone_response)); + } // Don't send anything if not instructed to. Better than asserting. } -void ExternalNotificationModule::handleSetRingtone(const char *from_msg) -{ - int changed = 0; +void ExternalNotificationModule::handleSetRingtone(const char *from_msg) { + int changed = 0; - if (*from_msg) { - changed |= strcmp(rtttlConfig.ringtone, from_msg); - strncpy(rtttlConfig.ringtone, from_msg, sizeof(rtttlConfig.ringtone)); - LOG_INFO("*** from_msg.text:%s", from_msg); - } + if (*from_msg) { + changed |= strcmp(rtttlConfig.ringtone, from_msg); + strncpy(rtttlConfig.ringtone, from_msg, sizeof(rtttlConfig.ringtone)); + LOG_INFO("*** from_msg.text:%s", from_msg); + } - if (changed) { - nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); - } + if (changed) { + nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); + } } -int ExternalNotificationModule::handleInputEvent(const InputEvent *event) -{ - if (nagCycleCutoff != UINT32_MAX) { - stopNow(); - return 1; - } - return 0; +int ExternalNotificationModule::handleInputEvent(const InputEvent *event) { + if (nagCycleCutoff != UINT32_MAX) { + stopNow(); + return 1; + } + return 0; } \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index f667f7be9..f490dff91 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -9,15 +9,14 @@ #include #else // Noop class for portduino. -class rtttl -{ - public: - explicit rtttl() {} - static bool isPlaying() { return false; } - static void play() {} - static void begin(byte a, const char *b){}; - static void stop() {} - static bool done() { return true; } +class rtttl { +public: + explicit rtttl() {} + static bool isPlaying() { return false; } + static void play() {} + static void begin(byte a, const char *b){}; + static void stop() {} + static bool done() { return true; } }; #endif #include @@ -27,51 +26,49 @@ class rtttl * Radio interface for ExternalNotificationModule * */ -class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread -{ - CallbackObserver inputObserver = - CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); - uint32_t output = 0; +class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread { + CallbackObserver inputObserver = + CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); + uint32_t output = 0; - public: - ExternalNotificationModule(); +public: + ExternalNotificationModule(); - int handleInputEvent(const InputEvent *arg); + int handleInputEvent(const InputEvent *arg); - uint32_t nagCycleCutoff = 1; + uint32_t nagCycleCutoff = 1; - void setExternalState(uint8_t index = 0, bool on = false); - bool getExternal(uint8_t index = 0); + void setExternalState(uint8_t index = 0, bool on = false); + bool getExternal(uint8_t index = 0); - void setMute(bool mute) { isSilenced = mute; } - bool getMute() { return isSilenced; } + void setMute(bool mute) { isSilenced = mute; } + bool getMute() { return isSilenced; } - bool canBuzz(); - bool nagging(); + bool canBuzz(); + bool nagging(); - void stopNow(); + void stopNow(); - void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); - void handleSetRingtone(const char *from_msg); + void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); + void handleSetRingtone(const char *from_msg); - protected: - /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; +protected: + /** Called to handle a particular incoming message + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - virtual bool wantPacket(const meshtastic_MeshPacket *p) override; + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; - bool isNagging = false; + bool isNagging = false; - bool isSilenced = false; + bool isSilenced = false; - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; }; extern ExternalNotificationModule *externalNotificationModule; \ No newline at end of file diff --git a/src/modules/GenericThreadModule.cpp b/src/modules/GenericThreadModule.cpp index eb92566bd..3c811b583 100644 --- a/src/modules/GenericThreadModule.cpp +++ b/src/modules/GenericThreadModule.cpp @@ -10,19 +10,18 @@ GenericThreadModule *genericThreadModule; GenericThreadModule::GenericThreadModule() : concurrency::OSThread("GenericThreadModule") {} -int32_t GenericThreadModule::runOnce() -{ +int32_t GenericThreadModule::runOnce() { - bool enabled = true; - if (!enabled) - return disable(); + bool enabled = true; + if (!enabled) + return disable(); - if (firstTime) { - // do something the first time we run - firstTime = 0; - LOG_INFO("first time GenericThread running"); - } + if (firstTime) { + // do something the first time we run + firstTime = 0; + LOG_INFO("first time GenericThread running"); + } - LOG_INFO("GenericThread executing"); - return (my_interval); + LOG_INFO("GenericThread executing"); + return (my_interval); } diff --git a/src/modules/GenericThreadModule.h b/src/modules/GenericThreadModule.h index 05f7946bb..6ef7c4b29 100644 --- a/src/modules/GenericThreadModule.h +++ b/src/modules/GenericThreadModule.h @@ -6,16 +6,15 @@ #include #include -class GenericThreadModule : private concurrency::OSThread -{ - bool firstTime = 1; +class GenericThreadModule : private concurrency::OSThread { + bool firstTime = 1; - public: - GenericThreadModule(); +public: + GenericThreadModule(); - protected: - unsigned int my_interval = 10000; // interval in millisconds - virtual int32_t runOnce() override; +protected: + unsigned int my_interval = 10000; // interval in millisconds + virtual int32_t runOnce() override; }; extern GenericThreadModule *genericThreadModule; diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 3b8225763..1356919a6 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -11,303 +11,284 @@ KeyVerificationModule *keyVerificationModule; KeyVerificationModule::KeyVerificationModule() - : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) -{ - ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; + : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) { + ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; } -AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) -{ - updateState(); - if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { - LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); +AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + updateState(); + if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { + LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); - if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && - currentState == KEY_VERIFICATION_IDLE) { - sendInitialRequest(request->key_verification.remote_nodenum); + if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && + currentState == KEY_VERIFICATION_IDLE) { + sendInitialRequest(request->key_verification.remote_nodenum); - } else if (request->key_verification.message_type == - meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && - request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && - request->key_verification.nonce == currentNonce) { - processSecurityNumber(request->key_verification.security_number); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && + request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && + request->key_verification.nonce == currentNonce) { + processSecurityNumber(request->key_verification.security_number); - } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && - request->key_verification.nonce == currentNonce) { - auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - resetToIdle(); - } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { - resetToIdle(); - } - return AdminMessageHandleResult::HANDLED; + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && + request->key_verification.nonce == currentNonce) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + resetToIdle(); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { + resetToIdle(); } - return AdminMessageHandleResult::NOT_HANDLED; + return AdminMessageHandleResult::HANDLED; + } + return AdminMessageHandleResult::NOT_HANDLED; } -bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) -{ - updateState(); - if (mp.pki_encrypted == false) { - return false; - } - if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply() - return false; - } - if (currentState == KEY_VERIFICATION_IDLE) { - return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() - } - - if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && - r->hash1.size == 0) { - memcpy(hash2, r->hash2.bytes, 32); - IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void { - keyVerificationModule->processSecurityNumber(number_picked); - });) - - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Enter Security Number for Key Verification"); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; - cn->payload_variant.key_verification_number_request.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, - sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); - service->sendClientNotification(cn); - LOG_INFO("Received hash2"); - currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; - return true; - - } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { - if (memcmp(hash1, r->hash1.bytes, 32) == 0) { - memset(message, 0, sizeof(message)); - sprintf(message, "Verification: \n"); - generateVerificationCode(message + 15); - LOG_INFO("Hash1 matches!"); - static const char *optionsArray[] = {"Reject", "Accept"}; - // Don't try to put the array definition in the macro. Does not work with curly braces. - IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; - options.optionsArrayPtr = optionsArray; options.optionsCount = 2; - options.notificationType = graphics::notificationTypeEnum::selection_picker; - options.bannerCallback = - [=](int selected) { - if (selected == 1) { - auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - } - }; - screen->showOverlayBanner(options);) - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; - cn->payload_variant.key_verification_final.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, - sizeof(cn->payload_variant.key_verification_final.remote_longname)); - cn->payload_variant.key_verification_final.isSender = false; - service->sendClientNotification(cn); - - currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; - return true; - } - } +bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) { + updateState(); + if (mp.pki_encrypted == false) { return false; -} + } + if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply() + return false; + } + if (currentState == KEY_VERIFICATION_IDLE) { + return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() + } -bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) -{ - LOG_DEBUG("keyVerification start"); - // generate nonce - updateState(); - if (currentState != KEY_VERIFICATION_IDLE) { - IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) - return false; - } - currentNonce = random(); - currentNonceTimestamp = getTime(); - currentRemoteNode = remoteNode; - meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; - KeyVerification.nonce = currentNonce; - KeyVerification.hash2.size = 0; - KeyVerification.hash1.size = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); - p->to = remoteNode; - p->channel = 0; - p->pki_encrypted = true; - p->decoded.want_response = true; - p->priority = meshtastic_MeshPacket_Priority_HIGH; - service->sendToMesh(p, RX_SRC_LOCAL, true); + if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && r->hash1.size == 0) { + memcpy(hash2, r->hash2.bytes, 32); + IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, + [](int number_picked) -> void { keyVerificationModule->processSecurityNumber(number_picked); });) - currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Enter Security Number for Key Verification"); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; + cn->payload_variant.key_verification_number_request.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + service->sendClientNotification(cn); + LOG_INFO("Received hash2"); + currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; return true; + + } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { + if (memcmp(hash1, r->hash1.bytes, 32) == 0) { + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); + LOG_INFO("Hash1 matches!"); + static const char *optionsArray[] = {"Reject", "Accept"}; + // Don't try to put the array definition in the macro. Does not work with curly braces. + IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; options.optionsArrayPtr = optionsArray; + options.optionsCount = 2; options.notificationType = graphics::notificationTypeEnum::selection_picker; + options.bannerCallback = + [=](int selected) { + if (selected == 1) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + }; + screen->showOverlayBanner(options);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = false; + service->sendClientNotification(cn); + + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; + return true; + } + } + return false; } -meshtastic_MeshPacket *KeyVerificationModule::allocReply() -{ - SHA256 hash; - NodeNum ourNodeNum = nodeDB->getNodeNum(); - updateState(); - if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period - LOG_WARN("Key Verification requested, but already in a request"); - return nullptr; - } else if (!currentRequest->pki_encrypted) { - LOG_WARN("Key Verification requested, but not in a PKI packet"); - return nullptr; - } - currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; +bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) { + LOG_DEBUG("keyVerification start"); + // generate nonce + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { + IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) + return false; + } + currentNonce = random(); + currentNonceTimestamp = getTime(); + currentRemoteNode = remoteNode; + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 0; + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = remoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_KeyVerification scratch; - meshtastic_KeyVerification response; - meshtastic_MeshPacket *responsePacket = nullptr; - pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); - - currentNonce = scratch.nonce; - response.nonce = scratch.nonce; - currentRemoteNode = req.from; - currentNonceTimestamp = getTime(); - currentSecurityNumber = random(1, 999999); - - // generate hash1 - hash.reset(); - hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); - hash.update(&ourNodeNum, sizeof(ourNodeNum)); - hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); - hash.update(owner.public_key.bytes, owner.public_key.size); - hash.finalize(hash1, 32); - - // generate hash2 - hash.reset(); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(hash1, 32); - hash.finalize(hash2, 32); - response.hash1.size = 0; - response.hash2.size = 32; - memcpy(response.hash2.bytes, hash2, 32); - - responsePacket = allocDataProtobuf(response); - - responsePacket->pki_encrypted = true; - IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); - screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);) - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, - currentSecurityNumber % 1000); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; - cn->payload_variant.key_verification_number_inform.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, - sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); - cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; - service->sendClientNotification(cn); - LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); - return responsePacket; + currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; + return true; } -void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) -{ - SHA256 hash; - NodeNum ourNodeNum = nodeDB->getNodeNum(); - uint8_t scratch_hash[32] = {0}; - LOG_WARN("received security number: %u", incomingNumber); - meshtastic_NodeInfoLite *remoteNodePtr = nullptr; - remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { - currentState = KEY_VERIFICATION_IDLE; - return; // should we throw an error here? - } - LOG_WARN("hashing "); - // calculate hash1 - hash.reset(); - hash.update(&incomingNumber, sizeof(incomingNumber)); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(&ourNodeNum, sizeof(ourNodeNum)); - hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); - hash.update(owner.public_key.bytes, owner.public_key.size); +meshtastic_MeshPacket *KeyVerificationModule::allocReply() { + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period + LOG_WARN("Key Verification requested, but already in a request"); + return nullptr; + } else if (!currentRequest->pki_encrypted) { + LOG_WARN("Key Verification requested, but not in a PKI packet"); + return nullptr; + } + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; - hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); - hash.finalize(hash1, 32); + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_KeyVerification scratch; + meshtastic_KeyVerification response; + meshtastic_MeshPacket *responsePacket = nullptr; + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); - hash.reset(); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(hash1, 32); - hash.finalize(scratch_hash, 32); + currentNonce = scratch.nonce; + response.nonce = scratch.nonce; + currentRemoteNode = req.from; + currentNonceTimestamp = getTime(); + currentSecurityNumber = random(1, 999999); - if (memcmp(scratch_hash, hash2, 32) != 0) { - LOG_WARN("Hash2 did not match"); - return; // should probably throw an error of some sort - } - currentSecurityNumber = incomingNumber; + // generate hash1 + hash.reset(); + hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); + hash.update(owner.public_key.bytes, owner.public_key.size); + hash.finalize(hash1, 32); - meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; - KeyVerification.nonce = currentNonce; - KeyVerification.hash2.size = 0; - KeyVerification.hash1.size = 32; - memcpy(KeyVerification.hash1.bytes, hash1, 32); - meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); - p->to = currentRemoteNode; - p->channel = 0; - p->pki_encrypted = true; - p->decoded.want_response = true; - p->priority = meshtastic_MeshPacket_Priority_HIGH; - service->sendToMesh(p, RX_SRC_LOCAL, true); - currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; - IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; - cn->payload_variant.key_verification_final.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, - sizeof(cn->payload_variant.key_verification_final.remote_longname)); - cn->payload_variant.key_verification_final.isSender = true; - service->sendClientNotification(cn); - LOG_INFO(message); + // generate hash2 + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(hash2, 32); + response.hash1.size = 0; + response.hash2.size = 32; + memcpy(response.hash2.bytes, hash2, 32); - return; + responsePacket = allocDataProtobuf(response); + + responsePacket->pki_encrypted = true; + IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); + screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; + cn->payload_variant.key_verification_number_inform.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); + cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; + service->sendClientNotification(cn); + LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); + return responsePacket; } -void KeyVerificationModule::updateState() -{ - if (currentState != KEY_VERIFICATION_IDLE) { - // check for the 60 second timeout - if (currentNonceTimestamp < getTime() - 60) { - resetToIdle(); - } else { - currentNonceTimestamp = getTime(); - } - } -} - -void KeyVerificationModule::resetToIdle() -{ - memset(hash1, 0, 32); - memset(hash2, 0, 32); - currentNonce = 0; - currentNonceTimestamp = 0; - currentSecurityNumber = 0; - currentRemoteNode = 0; +void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) { + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + uint8_t scratch_hash[32] = {0}; + LOG_WARN("received security number: %u", incomingNumber); + meshtastic_NodeInfoLite *remoteNodePtr = nullptr; + remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { currentState = KEY_VERIFICATION_IDLE; + return; // should we throw an error here? + } + LOG_WARN("hashing "); + // calculate hash1 + hash.reset(); + hash.update(&incomingNumber, sizeof(incomingNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(owner.public_key.bytes, owner.public_key.size); + + hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); + hash.finalize(hash1, 32); + + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(scratch_hash, 32); + + if (memcmp(scratch_hash, hash2, 32) != 0) { + LOG_WARN("Hash2 did not match"); + return; // should probably throw an error of some sort + } + currentSecurityNumber = incomingNumber; + + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 32; + memcpy(KeyVerification.hash1.bytes, hash1, 32); + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = currentRemoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; + IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = true; + service->sendClientNotification(cn); + LOG_INFO(message); + + return; } -void KeyVerificationModule::generateVerificationCode(char *readableCode) -{ - for (int i = 0; i < 4; i++) { - // drop the two highest significance bits, then encode as a base64 - readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. - } - readableCode[4] = ' '; - for (int i = 5; i < 9; i++) { - // drop the two highest significance bits, then encode as a base64 - readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. +void KeyVerificationModule::updateState() { + if (currentState != KEY_VERIFICATION_IDLE) { + // check for the 60 second timeout + if (currentNonceTimestamp < getTime() - 60) { + resetToIdle(); + } else { + currentNonceTimestamp = getTime(); } + } +} + +void KeyVerificationModule::resetToIdle() { + memset(hash1, 0, 32); + memset(hash2, 0, 32); + currentNonce = 0; + currentNonceTimestamp = 0; + currentSecurityNumber = 0; + currentRemoteNode = 0; + currentState = KEY_VERIFICATION_IDLE; +} + +void KeyVerificationModule::generateVerificationCode(char *readableCode) { + for (int i = 0; i < 4; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } + readableCode[4] = ' '; + for (int i = 5; i < 9; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } } #endif \ No newline at end of file diff --git a/src/modules/KeyVerificationModule.h b/src/modules/KeyVerificationModule.h index d5dba01d7..e9bfb9e1d 100644 --- a/src/modules/KeyVerificationModule.h +++ b/src/modules/KeyVerificationModule.h @@ -4,62 +4,62 @@ #include "SinglePortModule.h" enum KeyVerificationState { - KEY_VERIFICATION_IDLE, - KEY_VERIFICATION_SENDER_HAS_INITIATED, - KEY_VERIFICATION_SENDER_AWAITING_NUMBER, - KEY_VERIFICATION_SENDER_AWAITING_USER, - KEY_VERIFICATION_RECEIVER_AWAITING_USER, - KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, + KEY_VERIFICATION_IDLE, + KEY_VERIFICATION_SENDER_HAS_INITIATED, + KEY_VERIFICATION_SENDER_AWAITING_NUMBER, + KEY_VERIFICATION_SENDER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, }; class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // { - // CallbackObserver nodeStatusObserver = - // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); + // CallbackObserver nodeStatusObserver = + // CallbackObserver(this, + // &KeyVerificationModule::handleStatusUpdate); - public: - KeyVerificationModule(); - /* : concurrency::OSThread("KeyVerification"), - ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) - { - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent - }*/ - virtual bool wantUIFrame() { return false; }; - bool sendInitialRequest(NodeNum remoteNode); - void generateVerificationCode(char *); // fills char with the user readable verification code - uint32_t getCurrentRemoteNode() { return currentRemoteNode; } +public: + KeyVerificationModule(); + /* : concurrency::OSThread("KeyVerification"), + ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) + { + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + }*/ + virtual bool wantUIFrame() { return false; }; + bool sendInitialRequest(NodeNum remoteNode); + void generateVerificationCode(char *); // fills char with the user readable verification code + uint32_t getCurrentRemoteNode() { return currentRemoteNode; } - protected: - /* Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); - // virtual meshtastic_MeshPacket *allocReply() override; +protected: + /* Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); + // virtual meshtastic_MeshPacket *allocReply() override; - // rather than add to the craziness that is the admin module, just handle those requests here. - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; - /* - * Send our Telemetry into the mesh - */ - bool sendMetrics(); - virtual meshtastic_MeshPacket *allocReply() override; + // rather than add to the craziness that is the admin module, just handle those requests here. + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + /* + * Send our Telemetry into the mesh + */ + bool sendMetrics(); + virtual meshtastic_MeshPacket *allocReply() override; - private: - uint64_t currentNonce = 0; - uint32_t currentNonceTimestamp = 0; - NodeNum currentRemoteNode = 0; - uint32_t currentSecurityNumber = 0; - KeyVerificationState currentState = KEY_VERIFICATION_IDLE; - uint8_t hash1[32] = {0}; // - uint8_t hash2[32] = {0}; // - char message[40] = {0}; +private: + uint64_t currentNonce = 0; + uint32_t currentNonceTimestamp = 0; + NodeNum currentRemoteNode = 0; + uint32_t currentSecurityNumber = 0; + KeyVerificationState currentState = KEY_VERIFICATION_IDLE; + uint8_t hash1[32] = {0}; // + uint8_t hash2[32] = {0}; // + char message[40] = {0}; - void processSecurityNumber(uint32_t); - void updateState(); // check the timeouts and maybe reset the state to idle - void resetToIdle(); // Zero out module state + void processSecurityNumber(uint32_t); + void updateState(); // check the timeouts and maybe reset the state to idle + void resetToIdle(); // Zero out module state }; extern KeyVerificationModule *keyVerificationModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 63392f7e4..b95399120 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -112,200 +112,195 @@ /** * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) */ -void setupModules() -{ +void setupModules() { #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - inputBroker = new InputBroker(); - systemCommandsModule = new SystemCommandsModule(); - buzzerFeedbackThread = new BuzzerFeedbackThread(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + inputBroker = new InputBroker(); + systemCommandsModule = new SystemCommandsModule(); + buzzerFeedbackThread = new BuzzerFeedbackThread(); + } #endif #if defined(LED_CHARGE) || defined(LED_PAIRING) - statusLEDModule = new StatusLEDModule(); + statusLEDModule = new StatusLEDModule(); #endif #if !MESHTASTIC_EXCLUDE_ADMIN - adminModule = new AdminModule(); + adminModule = new AdminModule(); #endif #if !MESHTASTIC_EXCLUDE_NODEINFO - nodeInfoModule = new NodeInfoModule(); + nodeInfoModule = new NodeInfoModule(); #endif #if !MESHTASTIC_EXCLUDE_GPS - positionModule = new PositionModule(); + positionModule = new PositionModule(); #endif #if !MESHTASTIC_EXCLUDE_WAYPOINT - waypointModule = new WaypointModule(); + waypointModule = new WaypointModule(); #endif #if !MESHTASTIC_EXCLUDE_TEXTMESSAGE - textMessageModule = new TextMessageModule(); + textMessageModule = new TextMessageModule(); #endif #if !MESHTASTIC_EXCLUDE_TRACEROUTE - traceRouteModule = new TraceRouteModule(); + traceRouteModule = new TraceRouteModule(); #endif #if !MESHTASTIC_EXCLUDE_NEIGHBORINFO - if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) { - neighborInfoModule = new NeighborInfoModule(); - } + if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) { + neighborInfoModule = new NeighborInfoModule(); + } #endif #if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR - if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) { - detectionSensorModule = new DetectionSensorModule(); - } + if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) { + detectionSensorModule = new DetectionSensorModule(); + } #endif #if !MESHTASTIC_EXCLUDE_ATAK - if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { - atakPluginModule = new AtakPluginModule(); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + atakPluginModule = new AtakPluginModule(); + } #endif #if !MESHTASTIC_EXCLUDE_PKI - keyVerificationModule = new KeyVerificationModule(); + keyVerificationModule = new KeyVerificationModule(); #endif #if !MESHTASTIC_EXCLUDE_DROPZONE - dropzoneModule = new DropzoneModule(); + dropzoneModule = new DropzoneModule(); #endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE - new GenericThreadModule(); + new GenericThreadModule(); #endif - // Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance - // to a global variable. + // Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance + // to a global variable. #if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE - new RemoteHardwareModule(); + new RemoteHardwareModule(); #endif #if !MESHTASTIC_EXCLUDE_POWERSTRESS - new PowerStressModule(); + new PowerStressModule(); #endif - // Example: Put your module here - // new ReplyModule(); + // Example: Put your module here + // new ReplyModule(); #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(T_LORA_PAGER) - // use a special FSM based rotary encoder version for T-LoRa Pager - rotaryEncoderImpl = new RotaryEncoderImpl(); - if (!rotaryEncoderImpl->init()) { - delete rotaryEncoderImpl; - rotaryEncoderImpl = nullptr; - } + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } #elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } #else - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } #endif - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); #if defined(M5STACK_UNITC6L) - i2cButton = new i2cButtonThread("i2cButtonThread"); + i2cButton = new i2cButtonThread("i2cButtonThread"); #endif #ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE #ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE - } + } #endif // HAS_BUTTON #if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { - seesawRotary = new SeesawRotary("SeesawRotary"); - if (!seesawRotary->init()) { - delete seesawRotary; - seesawRotary = nullptr; - } - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; } + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } #endif #if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); + } #endif #ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE - expressLRSFiveWayInput = new ExpressLRSFiveWay(); + expressLRSFiveWayInput = new ExpressLRSFiveWay(); #endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - cannedMessageModule = new CannedMessageModule(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + cannedMessageModule = new CannedMessageModule(); + } #endif #if ARCH_PORTDUINO - new HostMetricsModule(); + new HostMetricsModule(); #endif #if HAS_TELEMETRY - new DeviceTelemetryModule(); + new DeviceTelemetryModule(); #endif #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (moduleConfig.has_telemetry && - (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { - new EnvironmentTelemetryModule(); - } + if (moduleConfig.has_telemetry && (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { + new EnvironmentTelemetryModule(); + } #if __has_include("Adafruit_PM25AQI.h") - if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { - new AirQualityTelemetryModule(); - } + if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { + new AirQualityTelemetryModule(); + } #endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { - new HealthTelemetryModule(); - } + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { + new HealthTelemetryModule(); + } #endif #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (moduleConfig.has_telemetry && - (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) { - new PowerTelemetryModule(); - } + if (moduleConfig.has_telemetry && (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) { + new PowerTelemetryModule(); + } #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ - !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL - if (moduleConfig.has_serial && moduleConfig.serial.enabled && - config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - new SerialModule(); - } + if (moduleConfig.has_serial && moduleConfig.serial.enabled && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + new SerialModule(); + } #endif #endif #ifdef ARCH_ESP32 - // Only run on an esp32 based device. + // Only run on an esp32 based device. #if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO - audioModule = new AudioModule(); + audioModule = new AudioModule(); #endif #if !MESHTASTIC_EXCLUDE_PAXCOUNTER - if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) { - paxcounterModule = new PaxcounterModule(); - } + if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) { + paxcounterModule = new PaxcounterModule(); + } #endif #endif #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) #if !MESHTASTIC_EXCLUDE_STOREFORWARD - if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) { - storeForwardModule = new StoreForwardModule(); - } + if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) { + storeForwardModule = new StoreForwardModule(); + } #endif #endif #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION - externalNotificationModule = new ExternalNotificationModule(); + externalNotificationModule = new ExternalNotificationModule(); #endif #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS - if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) - new RangeTestModule(); + if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) + new RangeTestModule(); #endif - // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra - // acks - routingModule = new RoutingModule(); + // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending + // extra acks + routingModule = new RoutingModule(); } diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index 2cd8ec5ed..794c0d6d7 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -12,45 +12,39 @@ Prints a single neighbor info packet and associated neighbors Uses LOG_DEBUG, which equates to Console.log NOTE: For debugging only */ -void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np) -{ - LOG_DEBUG("%s NEIGHBORINFO PACKET from Node 0x%x to Node 0x%x (last sent by 0x%x)", header, np->node_id, nodeDB->getNodeNum(), - np->last_sent_by_id); - LOG_DEBUG("Packet contains %d neighbors", np->neighbors_count); - for (int i = 0; i < np->neighbors_count; i++) { - LOG_DEBUG("Neighbor %d: node_id=0x%x, snr=%.2f", i, np->neighbors[i].node_id, np->neighbors[i].snr); - } +void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np) { + LOG_DEBUG("%s NEIGHBORINFO PACKET from Node 0x%x to Node 0x%x (last sent by 0x%x)", header, np->node_id, nodeDB->getNodeNum(), np->last_sent_by_id); + LOG_DEBUG("Packet contains %d neighbors", np->neighbors_count); + for (int i = 0; i < np->neighbors_count; i++) { + LOG_DEBUG("Neighbor %d: node_id=0x%x, snr=%.2f", i, np->neighbors[i].node_id, np->neighbors[i].snr); + } } /* Prints the nodeDB neighbors NOTE: for debugging only */ -void NeighborInfoModule::printNodeDBNeighbors() -{ - LOG_DEBUG("Our NodeDB contains %d neighbors", neighbors.size()); - for (size_t i = 0; i < neighbors.size(); i++) { - LOG_DEBUG("Node %d: node_id=0x%x, snr=%.2f", i, neighbors[i].node_id, neighbors[i].snr); - } +void NeighborInfoModule::printNodeDBNeighbors() { + LOG_DEBUG("Our NodeDB contains %d neighbors", neighbors.size()); + for (size_t i = 0; i < neighbors.size(); i++) { + LOG_DEBUG("Node %d: node_id=0x%x, snr=%.2f", i, neighbors[i].node_id, neighbors[i].snr); + } } /* Send our initial owner announcement 35 seconds after we start (to give * network time to setup) */ NeighborInfoModule::NeighborInfoModule() - : ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg), - concurrency::OSThread("NeighborInfo") -{ - ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + : ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg), concurrency::OSThread("NeighborInfo") { + ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); - if (moduleConfig.neighbor_info.enabled) { - isPromiscuous = true; // Update neighbors from all packets - setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, - default_telemetry_broadcast_interval_secs)); - } else { - LOG_DEBUG("NeighborInfoModule is disabled"); - disable(); - } + if (moduleConfig.neighbor_info.enabled) { + isPromiscuous = true; // Update neighbors from all packets + setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs)); + } else { + LOG_DEBUG("NeighborInfoModule is disabled"); + disable(); + } } /* @@ -58,198 +52,183 @@ Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time Assumes that the neighborInfo packet has been allocated @returns the number of entries collected */ -uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo) -{ - NodeNum my_node_id = nodeDB->getNodeNum(); - neighborInfo->node_id = my_node_id; - neighborInfo->last_sent_by_id = my_node_id; - neighborInfo->node_broadcast_interval_secs = - Default::getConfiguredOrDefault(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs); +uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo) { + NodeNum my_node_id = nodeDB->getNodeNum(); + neighborInfo->node_id = my_node_id; + neighborInfo->last_sent_by_id = my_node_id; + neighborInfo->node_broadcast_interval_secs = + Default::getConfiguredOrDefault(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs); - cleanUpNeighbors(); + cleanUpNeighbors(); - for (auto nbr : neighbors) { - if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) { - neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id; - neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr; - // Note: we don't set the last_rx_time and node_broadcast_intervals_secs - // here, because we don't want to send this over the mesh - neighborInfo->neighbors_count++; - } + for (auto nbr : neighbors) { + if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) { + neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id; + neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr; + // Note: we don't set the last_rx_time and node_broadcast_intervals_secs + // here, because we don't want to send this over the mesh + neighborInfo->neighbors_count++; } - printNodeDBNeighbors(); - return neighborInfo->neighbors_count; + } + printNodeDBNeighbors(); + return neighborInfo->neighbors_count; } /* Remove neighbors from the database that we haven't heard from in a while */ -void NeighborInfoModule::cleanUpNeighbors() -{ - uint32_t now = getTime(); - NodeNum my_node_id = nodeDB->getNodeNum(); - for (auto it = neighbors.rbegin(); it != neighbors.rend();) { - // We will remove a neighbor if we haven't heard from them in twice the - // broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is - // seconds since 1970 - if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) { - LOG_DEBUG("Remove neighbor with node ID 0x%x", it->node_id); - it = std::vector::reverse_iterator( - neighbors.erase(std::next(it).base())); // Erase the element and update the iterator - } else { - ++it; - } +void NeighborInfoModule::cleanUpNeighbors() { + uint32_t now = getTime(); + NodeNum my_node_id = nodeDB->getNodeNum(); + for (auto it = neighbors.rbegin(); it != neighbors.rend();) { + // We will remove a neighbor if we haven't heard from them in twice the + // broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is + // seconds since 1970 + if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) { + LOG_DEBUG("Remove neighbor with node ID 0x%x", it->node_id); + it = std::vector::reverse_iterator(neighbors.erase(std::next(it).base())); // Erase the element and update the iterator + } else { + ++it; } + } } /* Send neighbor info to the mesh */ -void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) -{ - meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; - collectNeighborInfo(&neighborInfo); - // only send neighbours if we have some to send - if (neighborInfo.neighbors_count > 0) { - meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); - p->to = dest; - p->decoded.want_response = wantReplies; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - printNeighborInfo("SENDING", &neighborInfo); - service->sendToMesh(p, RX_SRC_LOCAL, true); - } +void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) { + meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; + collectNeighborInfo(&neighborInfo); + // only send neighbours if we have some to send + if (neighborInfo.neighbors_count > 0) { + meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); + p->to = dest; + p->decoded.want_response = wantReplies; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + printNeighborInfo("SENDING", &neighborInfo); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } } /* Encompasses the full construction and sending packet to mesh Will be used for broadcast. */ -int32_t NeighborInfoModule::runOnce() -{ - if (moduleConfig.neighbor_info.transmit_over_lora && - (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) && - airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { - sendNeighborInfo(NODENUM_BROADCAST, false); - } else { - sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false); - } - return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs); +int32_t NeighborInfoModule::runOnce() { + if (moduleConfig.neighbor_info.transmit_over_lora && + (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) && + airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { + sendNeighborInfo(NODENUM_BROADCAST, false); + } else { + sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false); + } + return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs); } -meshtastic_MeshPacket *NeighborInfoModule::allocReply() -{ - LOG_INFO("NeighborInfoRequested."); - if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { - LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return nullptr; - } +meshtastic_MeshPacket *NeighborInfoModule::allocReply() { + LOG_INFO("NeighborInfoRequested."); + if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return nullptr; + } - meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; - collectNeighborInfo(&neighborInfo); + meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; + collectNeighborInfo(&neighborInfo); - meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo); + meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo); - if (reply) { - lastSentReply = millis(); // Track when we sent this reply - } - return reply; + if (reply) { + lastSentReply = millis(); // Track when we sent this reply + } + return reply; } /* Collect a received neighbor info packet from another node Pass it to an upper client; do not persist this data on the mesh */ -bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np) -{ - LOG_DEBUG("NeighborInfo: handleReceivedProtobuf"); - if (np) { - printNeighborInfo("RECEIVED", np); - // Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0 - if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) { - LOG_DEBUG(" Updating neighbours"); - updateNeighbors(mp, np); - } else { - LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); - } - } else if (getHopsAway(mp) == 0) { - LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); - // If the hopLimit is the same as hopStart, then it is a neighbor - getOrCreateNeighbor(mp.from, mp.from, 0, - mp.rx_snr); // Set the broadcast interval to 0, as we don't know it +bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np) { + LOG_DEBUG("NeighborInfo: handleReceivedProtobuf"); + if (np) { + printNeighborInfo("RECEIVED", np); + // Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0 + if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) { + LOG_DEBUG(" Updating neighbours"); + updateNeighbors(mp, np); + } else { + LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); } - // Allow others to handle this packet - return false; + } else if (getHopsAway(mp) == 0) { + LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); + // If the hopLimit is the same as hopStart, then it is a neighbor + getOrCreateNeighbor(mp.from, mp.from, 0, + mp.rx_snr); // Set the broadcast interval to 0, as we don't know it + } + // Allow others to handle this packet + return false; } /* Copy the content of a current NeighborInfo packet into a new one and update the last_sent_by_id to our NodeNum */ -void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) -{ - n->last_sent_by_id = nodeDB->getNodeNum(); +void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) { + n->last_sent_by_id = nodeDB->getNodeNum(); - // Set updated last_sent_by_id to the payload of the to be flooded packet - p.decoded.payload.size = - pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, n); + // Set updated last_sent_by_id to the payload of the to be flooded packet + p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, n); } -void NeighborInfoModule::resetNeighbors() -{ - neighbors.clear(); +void NeighborInfoModule::resetNeighbors() { neighbors.clear(); } + +void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np) { + LOG_DEBUG("updateNeighbors"); + // The last sent ID will be 0 if the packet is from the phone, which we don't + // count as an edge. So we assume that if it's zero, then this packet is from + // our node. + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { + getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr); + } } -void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np) -{ - LOG_DEBUG("updateNeighbors"); - // The last sent ID will be 0 if the packet is from the phone, which we don't - // count as an edge. So we assume that if it's zero, then this packet is from - // our node. - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { - getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr); +meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr) { + // our node and the phone are the same node (not neighbors) + if (n == 0) { + n = nodeDB->getNodeNum(); + } + // look for one in the existing list + for (size_t i = 0; i < neighbors.size(); i++) { + if (neighbors[i].node_id == n) { + // if found, update it + neighbors[i].snr = snr; + neighbors[i].last_rx_time = getTime(); + // Only if this is the original sender, the broadcast interval corresponds + // to it + if (originalSender == n && node_broadcast_interval_secs != 0) + neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs; + return &neighbors[i]; } -} - -meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n, - uint32_t node_broadcast_interval_secs, float snr) -{ - // our node and the phone are the same node (not neighbors) - if (n == 0) { - n = nodeDB->getNodeNum(); - } - // look for one in the existing list - for (size_t i = 0; i < neighbors.size(); i++) { - if (neighbors[i].node_id == n) { - // if found, update it - neighbors[i].snr = snr; - neighbors[i].last_rx_time = getTime(); - // Only if this is the original sender, the broadcast interval corresponds - // to it - if (originalSender == n && node_broadcast_interval_secs != 0) - neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs; - return &neighbors[i]; - } - } - // otherwise, allocate one and assign data to it - - meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero; - new_nbr.node_id = n; - new_nbr.snr = snr; - new_nbr.last_rx_time = getTime(); - // Only if this is the original sender, the broadcast interval corresponds to - // it - if (originalSender == n && node_broadcast_interval_secs != 0) - new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs; - else // Assume the same broadcast interval as us for the neighbor if we don't - // know it - new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval; - - if (neighbors.size() < MAX_NUM_NEIGHBORS) { - neighbors.push_back(new_nbr); - } else { - // If we have too many neighbors, replace the oldest one - LOG_WARN("Neighbor DB is full, replace oldest neighbor"); - neighbors.erase(neighbors.begin()); - neighbors.push_back(new_nbr); - } - return &neighbors.back(); + } + // otherwise, allocate one and assign data to it + + meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero; + new_nbr.node_id = n; + new_nbr.snr = snr; + new_nbr.last_rx_time = getTime(); + // Only if this is the original sender, the broadcast interval corresponds to + // it + if (originalSender == n && node_broadcast_interval_secs != 0) + new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs; + else // Assume the same broadcast interval as us for the neighbor if we don't + // know it + new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval; + + if (neighbors.size() < MAX_NUM_NEIGHBORS) { + neighbors.push_back(new_nbr); + } else { + // If we have too many neighbors, replace the oldest one + LOG_WARN("Neighbor DB is full, replace oldest neighbor"); + neighbors.erase(neighbors.begin()); + neighbors.push_back(new_nbr); + } + return &neighbors.back(); } diff --git a/src/modules/NeighborInfoModule.h b/src/modules/NeighborInfoModule.h index abb530329..0771f892b 100644 --- a/src/modules/NeighborInfoModule.h +++ b/src/modules/NeighborInfoModule.h @@ -5,73 +5,72 @@ /* * Neighborinfo module for sending info on each node's 0-hop neighbors to the mesh */ -class NeighborInfoModule : public ProtobufModule, private concurrency::OSThread -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &NeighborInfoModule::handleStatusUpdate); +class NeighborInfoModule : public ProtobufModule, private concurrency::OSThread { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &NeighborInfoModule::handleStatusUpdate); - std::vector neighbors; + std::vector neighbors; - public: - /* - * Expose the constructor - */ - NeighborInfoModule(); +public: + /* + * Expose the constructor + */ + NeighborInfoModule(); - /* Reset neighbor info after clearing nodeDB*/ - void resetNeighbors(); + /* Reset neighbor info after clearing nodeDB*/ + void resetNeighbors(); - protected: - /* - * Called to handle a particular incoming message - * @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *nb) override; +protected: + /* + * Called to handle a particular incoming message + * @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *nb) override; - /* Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /* Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /* - * Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time - * @return the number of entries collected - */ - uint32_t collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo); + /* + * Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time + * @return the number of entries collected + */ + uint32_t collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo); - /* - Remove neighbors from the database that we haven't heard from in a while - */ - void cleanUpNeighbors(); + /* + Remove neighbors from the database that we haven't heard from in a while + */ + void cleanUpNeighbors(); - /* Allocate a new NeighborInfo packet */ - meshtastic_NeighborInfo *allocateNeighborInfoPacket(); + /* Allocate a new NeighborInfo packet */ + meshtastic_NeighborInfo *allocateNeighborInfoPacket(); - // Find a neighbor in our DB, create an empty neighbor if missing - meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr); + // Find a neighbor in our DB, create an empty neighbor if missing + meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr); - /* - * Send info on our node's neighbors into the mesh - */ - void sendNeighborInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /* + * Send info on our node's neighbors into the mesh + */ + void sendNeighborInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - /* update neighbors with subpacket sniffed from network */ - void updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np); + /* update neighbors with subpacket sniffed from network */ + void updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np); - /* update a NeighborInfo packet with our NodeNum as last_sent_by_id */ - void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) override; + /* update a NeighborInfo packet with our NodeNum as last_sent_by_id */ + void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) override; - /* Does our periodic broadcast */ - int32_t runOnce() override; + /* Does our periodic broadcast */ + int32_t runOnce() override; - /* Override wantPacket to say we want to see all packets when enabled, not just those for our port number. - Exception is when the packet came via MQTT */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return enabled && !p->via_mqtt; } + /* Override wantPacket to say we want to see all packets when enabled, not just those for our port number. + Exception is when the packet came via MQTT */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return enabled && !p->via_mqtt; } - /* These are for debugging only */ - void printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np); - void printNodeDBNeighbors(); + /* These are for debugging only */ + void printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np); + void printNodeDBNeighbors(); - private: - uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) +private: + uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) }; extern NeighborInfoModule *neighborInfoModule; \ No newline at end of file diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 7db8b66cc..019e5a522 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -17,190 +17,182 @@ NodeInfoModule *nodeInfoModule; static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS; -bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) -{ - suppressReplyForCurrentRequest = false; +bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { + suppressReplyForCurrentRequest = false; - if (mp.from == nodeDB->getNodeNum()) { - LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); - return false; + if (mp.from == nodeDB->getNodeNum()) { + LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); + return false; + } + + auto p = *pptr; + + if (mp.decoded.want_response) { + const NodeNum sender = getFrom(&mp); + const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); + auto it = lastNodeInfoSeen.find(sender); + if (it != lastNodeInfoSeen.end()) { + uint32_t sinceLast = now >= it->second ? now - it->second : 0; + if (sinceLast < NodeInfoReplySuppressSeconds) { + suppressReplyForCurrentRequest = true; + } } - - auto p = *pptr; - - if (mp.decoded.want_response) { - const NodeNum sender = getFrom(&mp); - const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); - auto it = lastNodeInfoSeen.find(sender); - if (it != lastNodeInfoSeen.end()) { - uint32_t sinceLast = now >= it->second ? now - it->second : 0; - if (sinceLast < NodeInfoReplySuppressSeconds) { - suppressReplyForCurrentRequest = true; - } - } - lastNodeInfoSeen[sender] = now; - pruneLastNodeInfoCache(); - } - - if (p.is_licensed != owner.is_licensed) { - LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); - return true; - } - - // Coerce user.id to be derived from the node number - snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); - - bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); - - bool wasBroadcast = isBroadcast(mp.to); - - // LOG_DEBUG("did encode"); - // if user has changed while packet was not for us, inform phone - if (hasChanged && !wasBroadcast && !isToUs(&mp)) { - auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis - - // Re-encode the user protobuf, as we have stripped out the user.id - packetCopy->decoded.payload.size = pb_encode_to_bytes( - packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p); - - service->sendToPhone(packetCopy); - } - + lastNodeInfoSeen[sender] = now; pruneLastNodeInfoCache(); + } - // LOG_DEBUG("did handleReceived"); - return false; // Let others look at this message also if they want + if (p.is_licensed != owner.is_licensed) { + LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); + return true; + } + + // Coerce user.id to be derived from the node number + snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); + + bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); + + bool wasBroadcast = isBroadcast(mp.to); + + // LOG_DEBUG("did encode"); + // if user has changed while packet was not for us, inform phone + if (hasChanged && !wasBroadcast && !isToUs(&mp)) { + auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis + + // Re-encode the user protobuf, as we have stripped out the user.id + packetCopy->decoded.payload.size = + pb_encode_to_bytes(packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p); + + service->sendToPhone(packetCopy); + } + + pruneLastNodeInfoCache(); + + // LOG_DEBUG("did handleReceived"); + return false; // Let others look at this message also if they want } -void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) -{ - // Coerce user.id to be derived from the node number - snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp)); +void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) { + // Coerce user.id to be derived from the node number + snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp)); - // Re-encode the altered protobuf back into the packet - mp.decoded.payload.size = - pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p); + // Re-encode the altered protobuf back into the packet + mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p); } -void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout) -{ - // cancel any not yet sent (now stale) position packets - if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) - service->cancelSending(prevPacketId); - shorterTimeout = _shorterTimeout; - DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p = allocReply(); - DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); +void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout) { + // cancel any not yet sent (now stale) position packets + if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) + service->cancelSending(prevPacketId); + shorterTimeout = _shorterTimeout; + DEBUG_HEAP_BEFORE; + meshtastic_MeshPacket *p = allocReply(); + DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); - if (p) { // Check whether we didn't ignore it - p->to = dest; - bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && - config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - wantReplies; + if (p) { // Check whether we didn't ignore it + p->to = dest; + bool requestWantResponse = + (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + wantReplies; - p->decoded.want_response = requestWantResponse; - if (_shorterTimeout) - p->priority = meshtastic_MeshPacket_Priority_DEFAULT; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - if (channel > 0) { - LOG_DEBUG("Send ourNodeInfo to channel %d", channel); - p->channel = channel; - } - - prevPacketId = p->id; - - service->sendToMesh(p); - shorterTimeout = false; + p->decoded.want_response = requestWantResponse; + if (_shorterTimeout) + p->priority = meshtastic_MeshPacket_Priority_DEFAULT; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + if (channel > 0) { + LOG_DEBUG("Send ourNodeInfo to channel %d", channel); + p->channel = channel; } + + prevPacketId = p->id; + + service->sendToMesh(p); + shorterTimeout = false; + } } -meshtastic_MeshPacket *NodeInfoModule::allocReply() -{ - if (suppressReplyForCurrentRequest) { - LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); - ignoreRequest = true; - suppressReplyForCurrentRequest = false; - return NULL; +meshtastic_MeshPacket *NodeInfoModule::allocReply() { + if (suppressReplyForCurrentRequest) { + LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); + ignoreRequest = true; + suppressReplyForCurrentRequest = false; + return NULL; + } + + if (!airTime->isTxAllowedChannelUtil(false)) { + ignoreRequest = true; // Mark it as ignored for MeshModule + LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); + return NULL; + } + // If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway. + if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) { + LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return NULL; + } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { + LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return NULL; + } else { + ignoreRequest = false; // Don't ignore requests anymore + meshtastic_User &u = owner; + + // Strip the public key if the user is licensed + if (u.is_licensed && u.public_key.size > 0) { + u.public_key.bytes[0] = 0; + u.public_key.size = 0; } - if (!airTime->isTxAllowedChannelUtil(false)) { - ignoreRequest = true; // Mark it as ignored for MeshModule - LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); - return NULL; - } - // If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway. - if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) { - LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return NULL; - } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { - LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return NULL; + // FIXME: Clear the user.id field since it should be derived from node number on the receiving end + // u.id[0] = '\0'; + + // Ensure our user.id is derived correctly + strcpy(u.id, nodeDB->getNodeId().c_str()); + + LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); + lastSentToMesh = millis(); + return allocDataProtobuf(u); + } +} + +void NodeInfoModule::pruneLastNodeInfoCache() { + if (!nodeDB || !nodeDB->meshNodes) + return; + + const size_t maxEntries = nodeDB->meshNodes->size(); + + for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { + if (!nodeDB->getMeshNode(it->first)) { + it = lastNodeInfoSeen.erase(it); } else { - ignoreRequest = false; // Don't ignore requests anymore - meshtastic_User &u = owner; - - // Strip the public key if the user is licensed - if (u.is_licensed && u.public_key.size > 0) { - u.public_key.bytes[0] = 0; - u.public_key.size = 0; - } - - // FIXME: Clear the user.id field since it should be derived from node number on the receiving end - // u.id[0] = '\0'; - - // Ensure our user.id is derived correctly - strcpy(u.id, nodeDB->getNodeId().c_str()); - - LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); - lastSentToMesh = millis(); - return allocDataProtobuf(u); + ++it; } -} + } -void NodeInfoModule::pruneLastNodeInfoCache() -{ - if (!nodeDB || !nodeDB->meshNodes) - return; - - const size_t maxEntries = nodeDB->meshNodes->size(); - - for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { - if (!nodeDB->getMeshNode(it->first)) { - it = lastNodeInfoSeen.erase(it); - } else { - ++it; - } - } - - while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { - auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), - [](const std::pair &lhs, - const std::pair &rhs) { return lhs.second < rhs.second; }); - lastNodeInfoSeen.erase(oldestIt); - } + while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { + auto oldestIt = std::min_element( + lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), + [](const std::pair &lhs, const std::pair &rhs) { return lhs.second < rhs.second; }); + lastNodeInfoSeen.erase(oldestIt); + } } NodeInfoModule::NodeInfoModule() - : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") -{ - isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others + : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") { + isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others - setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds - // after we start (to give network time to setup) + setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds + // after we start (to give network time to setup) } -int32_t NodeInfoModule::runOnce() -{ - // If we changed channels, ask everyone else for their latest info - bool requestReplies = currentGeneration != radioGeneration; - currentGeneration = radioGeneration; +int32_t NodeInfoModule::runOnce() { + // If we changed channels, ask everyone else for their latest info + bool requestReplies = currentGeneration != radioGeneration; + currentGeneration = radioGeneration; - if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { - LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies); - sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies) - } - return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); + if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies); + sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies) + } + return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); } \ No newline at end of file diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index d16fbeac2..4e9b7e0b2 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -5,49 +5,47 @@ /** * NodeInfo module for sending/receiving NodeInfos into the mesh */ -class NodeInfoModule : public ProtobufModule, private concurrency::OSThread -{ - /// The id of the last packet we sent, to allow us to cancel it if we make something fresher - PacketId prevPacketId = 0; +class NodeInfoModule : public ProtobufModule, private concurrency::OSThread { + /// The id of the last packet we sent, to allow us to cancel it if we make something fresher + PacketId prevPacketId = 0; - uint32_t currentGeneration = 0; + uint32_t currentGeneration = 0; - public: - /** Constructor - * name is for debugging output - */ - NodeInfoModule(); +public: + /** Constructor + * name is for debugging output + */ + NodeInfoModule(); - /** - * Send our NodeInfo into the mesh - */ - void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, - bool _shorterTimeout = false); + /** + * Send our NodeInfo into the mesh + */ + void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, bool _shorterTimeout = false); - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *p) override; - /** Called to alter received User protobuf */ - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) override; + /** Called to alter received User protobuf */ + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) override; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /** Does our periodic broadcast */ - virtual int32_t runOnce() override; + /** Does our periodic broadcast */ + virtual int32_t runOnce() override; - private: - uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh - bool shorterTimeout = false; - bool suppressReplyForCurrentRequest = false; - std::map lastNodeInfoSeen; +private: + uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh + bool shorterTimeout = false; + bool suppressReplyForCurrentRequest = false; + std::map lastNodeInfoSeen; - void pruneLastNodeInfoCache(); + void pruneLastNodeInfoCache(); }; extern NodeInfoModule *nodeInfoModule; diff --git a/src/modules/OnScreenKeyboardModule.cpp b/src/modules/OnScreenKeyboardModule.cpp index e75d926bf..a67457072 100644 --- a/src/modules/OnScreenKeyboardModule.cpp +++ b/src/modules/OnScreenKeyboardModule.cpp @@ -9,262 +9,244 @@ #include #include -namespace graphics -{ +namespace graphics { -OnScreenKeyboardModule &OnScreenKeyboardModule::instance() -{ - static OnScreenKeyboardModule inst; - return inst; +OnScreenKeyboardModule &OnScreenKeyboardModule::instance() { + static OnScreenKeyboardModule inst; + return inst; } -OnScreenKeyboardModule::~OnScreenKeyboardModule() -{ - if (keyboard) { - delete keyboard; - keyboard = nullptr; +OnScreenKeyboardModule::~OnScreenKeyboardModule() { + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } +} + +void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs, std::function cb) { + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + keyboard = new VirtualKeyboard(); + callback = cb; + if (header) + keyboard->setHeader(header); + if (initialText) + keyboard->setInputText(initialText); + + // Route VK submission/cancel events back into the module + keyboard->setCallback([this](const std::string &text) { + if (text.empty()) { + this->onCancel(); + } else { + this->onSubmit(text); } + }); + + // Maintain legacy compatibility hooks + NotificationRenderer::virtualKeyboard = keyboard; + NotificationRenderer::textInputCallback = callback; } -void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs, - std::function cb) -{ - if (keyboard) { - delete keyboard; - keyboard = nullptr; - } - keyboard = new VirtualKeyboard(); - callback = cb; - if (header) - keyboard->setHeader(header); - if (initialText) - keyboard->setInputText(initialText); - - // Route VK submission/cancel events back into the module - keyboard->setCallback([this](const std::string &text) { - if (text.empty()) { - this->onCancel(); - } else { - this->onSubmit(text); - } - }); - - // Maintain legacy compatibility hooks - NotificationRenderer::virtualKeyboard = keyboard; - NotificationRenderer::textInputCallback = callback; +void OnScreenKeyboardModule::stop(bool callEmptyCallback) { + auto cb = callback; + callback = nullptr; + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + // Keep NotificationRenderer legacy pointers in sync + NotificationRenderer::virtualKeyboard = nullptr; + NotificationRenderer::textInputCallback = nullptr; + clearPopup(); + if (callEmptyCallback && cb) + cb(""); } -void OnScreenKeyboardModule::stop(bool callEmptyCallback) -{ - auto cb = callback; - callback = nullptr; - if (keyboard) { - delete keyboard; - keyboard = nullptr; - } - // Keep NotificationRenderer legacy pointers in sync - NotificationRenderer::virtualKeyboard = nullptr; - NotificationRenderer::textInputCallback = nullptr; - clearPopup(); - if (callEmptyCallback && cb) - cb(""); +void OnScreenKeyboardModule::handleInput(const InputEvent &event) { + if (!keyboard) + return; + + if (processVirtualKeyboardInput(event, keyboard)) + return; + + if (event.inputEvent == INPUT_BROKER_CANCEL) + onCancel(); } -void OnScreenKeyboardModule::handleInput(const InputEvent &event) -{ - if (!keyboard) - return; +bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard) { + if (!targetKeyboard) + return false; - if (processVirtualKeyboardInput(event, keyboard)) - return; - - if (event.inputEvent == INPUT_BROKER_CANCEL) - onCancel(); -} - -bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard) -{ - if (!targetKeyboard) - return false; - - switch (event.inputEvent) { - case INPUT_BROKER_UP: - case INPUT_BROKER_UP_LONG: - targetKeyboard->moveCursorUp(); - return true; - case INPUT_BROKER_DOWN: - case INPUT_BROKER_DOWN_LONG: - targetKeyboard->moveCursorDown(); - return true; - case INPUT_BROKER_LEFT: - case INPUT_BROKER_ALT_PRESS: - targetKeyboard->moveCursorLeft(); - return true; - case INPUT_BROKER_RIGHT: - case INPUT_BROKER_USER_PRESS: - targetKeyboard->moveCursorRight(); - return true; - case INPUT_BROKER_SELECT: - targetKeyboard->handlePress(); - return true; - case INPUT_BROKER_SELECT_LONG: - targetKeyboard->handleLongPress(); - return true; - default: - return false; - } -} - -bool OnScreenKeyboardModule::draw(OLEDDisplay *display) -{ - if (!keyboard) - return false; - - // Timeout - if (keyboard->isTimedOut()) { - onCancel(); - return false; - } - - // Clear full screen behind keyboard - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - keyboard->draw(display, 0, 0); - - // Draw popup overlay if needed - drawPopup(display); + switch (event.inputEvent) { + case INPUT_BROKER_UP: + case INPUT_BROKER_UP_LONG: + targetKeyboard->moveCursorUp(); return true; + case INPUT_BROKER_DOWN: + case INPUT_BROKER_DOWN_LONG: + targetKeyboard->moveCursorDown(); + return true; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_ALT_PRESS: + targetKeyboard->moveCursorLeft(); + return true; + case INPUT_BROKER_RIGHT: + case INPUT_BROKER_USER_PRESS: + targetKeyboard->moveCursorRight(); + return true; + case INPUT_BROKER_SELECT: + targetKeyboard->handlePress(); + return true; + case INPUT_BROKER_SELECT_LONG: + targetKeyboard->handleLongPress(); + return true; + default: + return false; + } } -void OnScreenKeyboardModule::onSubmit(const std::string &text) -{ - auto cb = callback; - stop(false); - if (cb) - cb(text); +bool OnScreenKeyboardModule::draw(OLEDDisplay *display) { + if (!keyboard) + return false; + + // Timeout + if (keyboard->isTimedOut()) { + onCancel(); + return false; + } + + // Clear full screen behind keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + keyboard->draw(display, 0, 0); + + // Draw popup overlay if needed + drawPopup(display); + return true; } -void OnScreenKeyboardModule::onCancel() -{ - stop(true); +void OnScreenKeyboardModule::onSubmit(const std::string &text) { + auto cb = callback; + stop(false); + if (cb) + cb(text); } -void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) -{ - if (!title || !content) - return; - strncpy(popupTitle, title, sizeof(popupTitle) - 1); - popupTitle[sizeof(popupTitle) - 1] = '\0'; - strncpy(popupMessage, content, sizeof(popupMessage) - 1); - popupMessage[sizeof(popupMessage) - 1] = '\0'; - popupUntil = millis() + durationMs; - popupVisible = true; +void OnScreenKeyboardModule::onCancel() { stop(true); } + +void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) { + if (!title || !content) + return; + strncpy(popupTitle, title, sizeof(popupTitle) - 1); + popupTitle[sizeof(popupTitle) - 1] = '\0'; + strncpy(popupMessage, content, sizeof(popupMessage) - 1); + popupMessage[sizeof(popupMessage) - 1] = '\0'; + popupUntil = millis() + durationMs; + popupVisible = true; } -void OnScreenKeyboardModule::clearPopup() -{ - popupTitle[0] = '\0'; - popupMessage[0] = '\0'; - popupUntil = 0; +void OnScreenKeyboardModule::clearPopup() { + popupTitle[0] = '\0'; + popupMessage[0] = '\0'; + popupUntil = 0; + popupVisible = false; +} + +void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display) { + // Only render the popup overlay (without drawing the keyboard) + drawPopup(display); +} + +void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) { + if (!popupVisible) + return; + if (millis() > popupUntil || popupMessage[0] == '\0') { popupVisible = false; -} + return; + } -void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display) -{ - // Only render the popup overlay (without drawing the keyboard) - drawPopup(display); -} + // Build lines and leverage NotificationRenderer inverted box drawing for consistent style + constexpr uint16_t maxContentLines = 3; + const bool hasTitle = popupTitle[0] != '\0'; -void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) -{ - if (!popupVisible) - return; - if (millis() > popupUntil || popupMessage[0] == '\0') { - popupVisible = false; - return; - } + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const uint16_t maxWrapWidth = display->width() - 40; - // Build lines and leverage NotificationRenderer inverted box drawing for consistent style - constexpr uint16_t maxContentLines = 3; - const bool hasTitle = popupTitle[0] != '\0'; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const uint16_t maxWrapWidth = display->width() - 40; - - auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { - std::vector wrapped; - std::string current; - std::string word; - const char *p = text; - while (*p && wrapped.size() < maxContentLines) { - while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { - if (*p == '\n') { - if (!current.empty()) { - wrapped.push_back(current); - current.clear(); - if (wrapped.size() >= maxContentLines) - break; - } - } - ++p; - } - if (!*p || wrapped.size() >= maxContentLines) - break; - word.clear(); - while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') - word += *p++; - if (word.empty()) - continue; - std::string test = current.empty() ? word : (current + " " + word); - uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); - if (w <= availableWidth) - current = test; - else { - if (!current.empty()) { - wrapped.push_back(current); - current = word; - if (wrapped.size() >= maxContentLines) - break; - } else { - current = word; - while (current.size() > 1 && - display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) - current.pop_back(); - } - } - } - if (!current.empty() && wrapped.size() < maxContentLines) + auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { + std::vector wrapped; + std::string current; + std::string word; + const char *p = text; + while (*p && wrapped.size() < maxContentLines) { + while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { + if (*p == '\n') { + if (!current.empty()) { wrapped.push_back(current); - return wrapped; - }; - - std::vector allLines; - if (hasTitle) - allLines.emplace_back(popupTitle); - - char buf[sizeof(popupMessage)]; - strncpy(buf, popupMessage, sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - char *paragraph = strtok(buf, "\n"); - while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { - auto wrapped = wrapText(paragraph, maxWrapWidth); - for (const auto &ln : wrapped) { - if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) - break; - allLines.push_back(ln); + current.clear(); + if (wrapped.size() >= maxContentLines) + break; + } } - paragraph = strtok(nullptr, "\n"); + ++p; + } + if (!*p || wrapped.size() >= maxContentLines) + break; + word.clear(); + while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') + word += *p++; + if (word.empty()) + continue; + std::string test = current.empty() ? word : (current + " " + word); + uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); + if (w <= availableWidth) + current = test; + else { + if (!current.empty()) { + wrapped.push_back(current); + current = word; + if (wrapped.size() >= maxContentLines) + break; + } else { + current = word; + while (current.size() > 1 && display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) + current.pop_back(); + } + } } + if (!current.empty() && wrapped.size() < maxContentLines) + wrapped.push_back(current); + return wrapped; + }; - std::vector ptrs; - for (const auto &ln : allLines) - ptrs.push_back(ln.c_str()); - ptrs.push_back(nullptr); + std::vector allLines; + if (hasTitle) + allLines.emplace_back(popupTitle); - // Use the standard notification box drawing from NotificationRenderer - NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); + char buf[sizeof(popupMessage)]; + strncpy(buf, popupMessage, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + char *paragraph = strtok(buf, "\n"); + while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { + auto wrapped = wrapText(paragraph, maxWrapWidth); + for (const auto &ln : wrapped) { + if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) + break; + allLines.push_back(ln); + } + paragraph = strtok(nullptr, "\n"); + } + + std::vector ptrs; + for (const auto &ln : allLines) + ptrs.push_back(ln.c_str()); + ptrs.push_back(nullptr); + + // Use the standard notification box drawing from NotificationRenderer + NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); } } // namespace graphics diff --git a/src/modules/OnScreenKeyboardModule.h b/src/modules/OnScreenKeyboardModule.h index f86b71ec3..edc2b6da9 100644 --- a/src/modules/OnScreenKeyboardModule.h +++ b/src/modules/OnScreenKeyboardModule.h @@ -9,45 +9,42 @@ #include #include -namespace graphics -{ -class OnScreenKeyboardModule -{ - public: - static OnScreenKeyboardModule &instance(); +namespace graphics { +class OnScreenKeyboardModule { +public: + static OnScreenKeyboardModule &instance(); - void start(const char *header, const char *initialText, uint32_t durationMs, - std::function callback); + void start(const char *header, const char *initialText, uint32_t durationMs, std::function callback); - void stop(bool callEmptyCallback); + void stop(bool callEmptyCallback); - void handleInput(const InputEvent &event); - static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); - bool draw(OLEDDisplay *display); + void handleInput(const InputEvent &event); + static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); + bool draw(OLEDDisplay *display); - void showPopup(const char *title, const char *content, uint32_t durationMs); - void clearPopup(); - // Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard) - void drawPopupOverlay(OLEDDisplay *display); + void showPopup(const char *title, const char *content, uint32_t durationMs); + void clearPopup(); + // Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard) + void drawPopupOverlay(OLEDDisplay *display); - private: - OnScreenKeyboardModule() = default; - ~OnScreenKeyboardModule(); - OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; - OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; +private: + OnScreenKeyboardModule() = default; + ~OnScreenKeyboardModule(); + OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; + OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; - void onSubmit(const std::string &text); - void onCancel(); + void onSubmit(const std::string &text); + void onCancel(); - void drawPopup(OLEDDisplay *display); + void drawPopup(OLEDDisplay *display); - VirtualKeyboard *keyboard = nullptr; - std::function callback; + VirtualKeyboard *keyboard = nullptr; + std::function callback; - char popupTitle[64] = {0}; - char popupMessage[256] = {0}; - uint32_t popupUntil = 0; - bool popupVisible = false; + char popupTitle[64] = {0}; + char popupMessage[256] = {0}; + uint32_t popupUntil = 0; + bool popupVisible = false; }; } // namespace graphics diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index f7116e701..947ac46ff 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -21,543 +21,512 @@ PositionModule *positionModule; PositionModule::PositionModule() - : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread("Position") -{ - precision = 0; // safe starting value - isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread("Position") { + precision = 0; // safe starting value + isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others + nodeStatusObserver.observe(&nodeStatus->onNewStatus); - if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && - config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { - setIntervalFromNow(setStartDelay()); - } + if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + setIntervalFromNow(setStartDelay()); + } - // Power saving trackers should clear their position on startup to avoid waking up and sending a stale position - if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && - config.power.is_power_saving) { - LOG_DEBUG("Clear position on startup for sleepy tracker (ー。ー) zzz"); - nodeDB->clearLocalPosition(); - } + // Power saving trackers should clear their position on startup to avoid waking up and sending a stale position + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && + config.power.is_power_saving) { + LOG_DEBUG("Clear position on startup for sleepy tracker (ー。ー) zzz"); + nodeDB->clearLocalPosition(); + } } -bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr) -{ - auto p = *pptr; +bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr) { + auto p = *pptr; - const auto transport = mp.transport_mechanism; - if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, - meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { - LOG_WARN("Ignoring packet supposedly from us over external transport"); - return true; - } + const auto transport = mp.transport_mechanism; + if (isFromUs(&mp) && + !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { + LOG_WARN("Ignoring packet supposedly from us over external transport"); + return true; + } - // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) - // to set fixed location, EUD-GPS location or just the time (see also issue #900) - bool isLocal = false; - if (isFromUs(&mp)) { - isLocal = true; - if (config.position.fixed_position) { - LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true"); + // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) + // to set fixed location, EUD-GPS location or just the time (see also issue #900) + bool isLocal = false; + if (isFromUs(&mp)) { + isLocal = true; + if (config.position.fixed_position) { + LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true"); #ifdef T_WATCH_S3 - // Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the - // client device here - if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { - trySetRtc(p, isLocal, true); - } + // Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the + // client device here + if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + trySetRtc(p, isLocal, true); + } #endif - nodeDB->setLocalPosition(p, true); - return false; - } else { - LOG_DEBUG("Incoming update from MYSELF"); - nodeDB->setLocalPosition(p); - } - } - - // Log packet size and data fields - LOG_DEBUG("POSITION node=%08x l=%d lat=%d lon=%d msl=%d hae=%d geo=%d pdop=%d hdop=%d vdop=%d siv=%d fxq=%d fxt=%d pts=%d " - "time=%d", - getFrom(&mp), mp.decoded.payload.size, p.latitude_i, p.longitude_i, p.altitude, p.altitude_hae, - p.altitude_geoidal_separation, p.PDOP, p.HDOP, p.VDOP, p.sats_in_view, p.fix_quality, p.fix_type, p.timestamp, - p.time); - - if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { - bool force = false; - -#ifdef T_WATCH_S3 - // The T-Watch appears to "pause" its RTC when shut down, such that the time it reads upon powering on is the same as when - // it was shut down. So we need to force the update here, since otherwise RTC::perhapsSetRTC will ignore it because it - // will always be an equivalent or lesser RTCQuality (RTCQualityNTP or RTCQualityNet). - force = true; -#endif - // Set from phone RTC Quality to RTCQualityNTP since it should be approximately so - trySetRtc(p, isLocal, force); - } - - nodeDB->updatePosition(getFrom(&mp), p); - if (channels.getByIndex(mp.channel).settings.has_module_settings) { - precision = channels.getByIndex(mp.channel).settings.module_settings.position_precision; - } else if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { - precision = 32; + nodeDB->setLocalPosition(p, true); + return false; } else { - precision = 0; + LOG_DEBUG("Incoming update from MYSELF"); + nodeDB->setLocalPosition(p); } + } - return false; // Let others look at this message also if they want -} + // Log packet size and data fields + LOG_DEBUG("POSITION node=%08x l=%d lat=%d lon=%d msl=%d hae=%d geo=%d pdop=%d hdop=%d vdop=%d siv=%d fxq=%d fxt=%d pts=%d " + "time=%d", + getFrom(&mp), mp.decoded.payload.size, p.latitude_i, p.longitude_i, p.altitude, p.altitude_hae, p.altitude_geoidal_separation, p.PDOP, + p.HDOP, p.VDOP, p.sats_in_view, p.fix_quality, p.fix_type, p.timestamp, p.time); -void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) -{ - // Phone position packets need to be truncated to the channel precision - if (isFromUs(&mp) && (precision < 32 && precision > 0)) { - LOG_DEBUG("Truncate phone position to channel precision %i", precision); - p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision)); - p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision)); + if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + bool force = false; - // We want the imprecise position to be the middle of the possible location, not - p->latitude_i += (1 << (31 - precision)); - p->longitude_i += (1 << (31 - precision)); - - mp.decoded.payload.size = - pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p); - } -} - -void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate) -{ - if (hasQualityTimesource() && !isLocal) { - LOG_DEBUG("Ignore time from mesh because we have a GPS, RTC, or Phone/NTP time source in the past day"); - return; - } - if (!isLocal && p.location_source < meshtastic_Position_LocSource_LOC_INTERNAL) { - LOG_DEBUG("Ignore time from mesh because it has a unknown or manual source"); - return; - } - struct timeval tv; - uint32_t secs = p.time; - - tv.tv_sec = secs; - tv.tv_usec = 0; - - perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate); -} - -bool PositionModule::hasQualityTimesource() -{ - bool setFromPhoneOrNtpToday = - lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL); -#if MESHTASTIC_EXCLUDE_GPS - bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address); -#else - bool hasGpsOrRtc = hasGPS() || (rtc_found.address != ScanI2C::ADDRESS_NONE.address); +#ifdef T_WATCH_S3 + // The T-Watch appears to "pause" its RTC when shut down, such that the time it reads upon powering on is the same + // as when it was shut down. So we need to force the update here, since otherwise RTC::perhapsSetRTC will ignore it + // because it will always be an equivalent or lesser RTCQuality (RTCQualityNTP or RTCQualityNet). + force = true; #endif - return hasGpsOrRtc || setFromPhoneOrNtpToday; + // Set from phone RTC Quality to RTCQualityNTP since it should be approximately so + trySetRtc(p, isLocal, force); + } + + nodeDB->updatePosition(getFrom(&mp), p); + if (channels.getByIndex(mp.channel).settings.has_module_settings) { + precision = channels.getByIndex(mp.channel).settings.module_settings.position_precision; + } else if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + precision = 32; + } else { + precision = 0; + } + + return false; // Let others look at this message also if they want } -bool PositionModule::hasGPS() -{ +void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) { + // Phone position packets need to be truncated to the channel precision + if (isFromUs(&mp) && (precision < 32 && precision > 0)) { + LOG_DEBUG("Truncate phone position to channel precision %i", precision); + p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision)); + p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision)); + + // We want the imprecise position to be the middle of the possible location, not + p->latitude_i += (1 << (31 - precision)); + p->longitude_i += (1 << (31 - precision)); + + mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p); + } +} + +void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate) { + if (hasQualityTimesource() && !isLocal) { + LOG_DEBUG("Ignore time from mesh because we have a GPS, RTC, or Phone/NTP time source in the past day"); + return; + } + if (!isLocal && p.location_source < meshtastic_Position_LocSource_LOC_INTERNAL) { + LOG_DEBUG("Ignore time from mesh because it has a unknown or manual source"); + return; + } + struct timeval tv; + uint32_t secs = p.time; + + tv.tv_sec = secs; + tv.tv_usec = 0; + + perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate); +} + +bool PositionModule::hasQualityTimesource() { + bool setFromPhoneOrNtpToday = lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL); #if MESHTASTIC_EXCLUDE_GPS - return false; + bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address); #else - return gps && gps->isConnected(); + bool hasGpsOrRtc = hasGPS() || (rtc_found.address != ScanI2C::ADDRESS_NONE.address); +#endif + return hasGpsOrRtc || setFromPhoneOrNtpToday; +} + +bool PositionModule::hasGPS() { +#if MESHTASTIC_EXCLUDE_GPS + return false; +#else + return gps && gps->isConnected(); #endif } // Allocate a packet with our position data if we have one -meshtastic_MeshPacket *PositionModule::allocPositionPacket() -{ - if (precision == 0) { - LOG_DEBUG("Skip location send because precision is set to 0!"); - return nullptr; - } +meshtastic_MeshPacket *PositionModule::allocPositionPacket() { + if (precision == 0) { + LOG_DEBUG("Skip location send because precision is set to 0!"); + return nullptr; + } - meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position - assert(node->has_position); + meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position + assert(node->has_position); - // configuration of POSITION packet - // consider making this a function argument? - uint32_t pos_flags = config.position.position_flags; + // configuration of POSITION packet + // consider making this a function argument? + uint32_t pos_flags = config.position.position_flags; - // Populate a Position struct with ONLY the requested fields - meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure - // if localPosition is totally empty, put our last saved position (lite) in there - if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position)); - } - localPosition.seq_number++; + // Populate a Position struct with ONLY the requested fields + meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure + // if localPosition is totally empty, put our last saved position (lite) in there + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position)); + } + localPosition.seq_number++; - if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - LOG_WARN("Skip position send because lat/lon are zero!"); - return nullptr; - } + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + LOG_WARN("Skip position send because lat/lon are zero!"); + return nullptr; + } - // lat/lon are unconditionally included - IF AVAILABLE! - LOG_DEBUG("Send location with precision %i", precision); - if (precision < 32 && precision > 0) { - p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision)); - p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision)); + // lat/lon are unconditionally included - IF AVAILABLE! + LOG_DEBUG("Send location with precision %i", precision); + if (precision < 32 && precision > 0) { + p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision)); + p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision)); - // We want the imprecise position to be the middle of the possible location, not - p.latitude_i += (1 << (31 - precision)); - p.longitude_i += (1 << (31 - precision)); + // We want the imprecise position to be the middle of the possible location, not + p.latitude_i += (1 << (31 - precision)); + p.longitude_i += (1 << (31 - precision)); + } else { + p.latitude_i = localPosition.latitude_i; + p.longitude_i = localPosition.longitude_i; + } + p.precision_bits = precision; + p.has_latitude_i = true; + p.has_longitude_i = true; + // Always use NTP / GPS time if available + if (getValidTime(RTCQualityNTP) > 0) { + p.time = getValidTime(RTCQualityNTP); + } else if (rtc_found.address != ScanI2C::ADDRESS_NONE.address) { + LOG_INFO("Use RTC time for position"); + p.time = getValidTime(RTCQualityDevice); + } else if (getRTCQuality() < RTCQualityNTP) { + LOG_INFO("Strip low RTCQuality (%d) time from position", getRTCQuality()); + p.time = 0; + } + + if (config.position.fixed_position) { + p.location_source = meshtastic_Position_LocSource_LOC_MANUAL; + } else { + p.location_source = localPosition.location_source; + } + + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE) { + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL) { + p.altitude = localPosition.altitude; + p.has_altitude = true; } else { - p.latitude_i = localPosition.latitude_i; - p.longitude_i = localPosition.longitude_i; - } - p.precision_bits = precision; - p.has_latitude_i = true; - p.has_longitude_i = true; - // Always use NTP / GPS time if available - if (getValidTime(RTCQualityNTP) > 0) { - p.time = getValidTime(RTCQualityNTP); - } else if (rtc_found.address != ScanI2C::ADDRESS_NONE.address) { - LOG_INFO("Use RTC time for position"); - p.time = getValidTime(RTCQualityDevice); - } else if (getRTCQuality() < RTCQualityNTP) { - LOG_INFO("Strip low RTCQuality (%d) time from position", getRTCQuality()); - p.time = 0; + p.altitude_hae = localPosition.altitude_hae; + p.has_altitude_hae = true; } - if (config.position.fixed_position) { - p.location_source = meshtastic_Position_LocSource_LOC_MANUAL; - } else { - p.location_source = localPosition.location_source; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_GEOIDAL_SEPARATION) { + p.altitude_geoidal_separation = localPosition.altitude_geoidal_separation; + p.has_altitude_geoidal_separation = true; } + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE) { - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL) { - p.altitude = localPosition.altitude; - p.has_altitude = true; - } else { - p.altitude_hae = localPosition.altitude_hae; - p.has_altitude_hae = true; - } + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_DOP) { + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HVDOP) { + p.HDOP = localPosition.HDOP; + p.VDOP = localPosition.VDOP; + } else + p.PDOP = localPosition.PDOP; + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_GEOIDAL_SEPARATION) { - p.altitude_geoidal_separation = localPosition.altitude_geoidal_separation; - p.has_altitude_geoidal_separation = true; - } - } + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW) + p.sats_in_view = localPosition.sats_in_view; - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_DOP) { - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HVDOP) { - p.HDOP = localPosition.HDOP; - p.VDOP = localPosition.VDOP; - } else - p.PDOP = localPosition.PDOP; - } + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP) + p.timestamp = localPosition.timestamp; - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW) - p.sats_in_view = localPosition.sats_in_view; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO) + p.seq_number = localPosition.seq_number; - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP) - p.timestamp = localPosition.timestamp; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HEADING) { + p.ground_track = localPosition.ground_track; + p.has_ground_track = true; + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO) - p.seq_number = localPosition.seq_number; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SPEED) { + p.ground_speed = localPosition.ground_speed; + p.has_ground_speed = true; + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HEADING) { - p.ground_track = localPosition.ground_track; - p.has_ground_track = true; - } - - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SPEED) { - p.ground_speed = localPosition.ground_speed; - p.has_ground_speed = true; - } - - LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); + LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); #ifndef MESHTASTIC_EXCLUDE_ATAK - // TAK Tracker devices should send their position in a TAK packet over the ATAK port - if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) - return allocAtakPli(); + // TAK Tracker devices should send their position in a TAK packet over the ATAK port + if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) + return allocAtakPli(); #endif - return allocDataProtobuf(p); + return allocDataProtobuf(p); } -meshtastic_MeshPacket *PositionModule::allocReply() -{ - if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && - Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { - LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return nullptr; - } +meshtastic_MeshPacket *PositionModule::allocReply() { + if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && + Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return nullptr; + } - meshtastic_MeshPacket *reply = allocPositionPacket(); - if (reply) { - lastSentReply = millis(); // Track when we sent this reply - } - return reply; + meshtastic_MeshPacket *reply = allocPositionPacket(); + if (reply) { + lastSentReply = millis(); // Track when we sent this reply + } + return reply; } -meshtastic_MeshPacket *PositionModule::allocAtakPli() -{ - LOG_INFO("Send TAK PLI packet"); - meshtastic_MeshPacket *mp = allocDataPacket(); - mp->decoded.portnum = meshtastic_PortNum_ATAK_PLUGIN; +meshtastic_MeshPacket *PositionModule::allocAtakPli() { + LOG_INFO("Send TAK PLI packet"); + meshtastic_MeshPacket *mp = allocDataPacket(); + mp->decoded.portnum = meshtastic_PortNum_ATAK_PLUGIN; - meshtastic_TAKPacket takPacket = {.is_compressed = true, - .has_contact = true, - .contact = meshtastic_Contact_init_default, - .has_group = true, - .group = {meshtastic_MemberRole_TeamMember, meshtastic_Team_Cyan}, - .has_status = true, - .status = - { - .battery = powerStatus->getBatteryChargePercent(), - }, - .which_payload_variant = meshtastic_TAKPacket_pli_tag, - .payload_variant = {.pli = { - .latitude_i = localPosition.latitude_i, - .longitude_i = localPosition.longitude_i, - .altitude = localPosition.altitude_hae, - .speed = localPosition.ground_speed, - .course = static_cast(localPosition.ground_track), - }}}; + meshtastic_TAKPacket takPacket = {.is_compressed = true, + .has_contact = true, + .contact = meshtastic_Contact_init_default, + .has_group = true, + .group = {meshtastic_MemberRole_TeamMember, meshtastic_Team_Cyan}, + .has_status = true, + .status = + { + .battery = powerStatus->getBatteryChargePercent(), + }, + .which_payload_variant = meshtastic_TAKPacket_pli_tag, + .payload_variant = {.pli = { + .latitude_i = localPosition.latitude_i, + .longitude_i = localPosition.longitude_i, + .altitude = localPosition.altitude_hae, + .speed = localPosition.ground_speed, + .course = static_cast(localPosition.ground_track), + }}}; - auto length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.device_callsign, - sizeof(takPacket.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); - LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes", owner.long_name, strlen(owner.long_name)); - LOG_DEBUG("Compressed device_callsign '%s' - %d bytes", takPacket.contact.device_callsign, length); - length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.callsign, - sizeof(takPacket.contact.callsign) - 1, USX_PSET_DFLT, NULL); - mp->decoded.payload.size = - pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_TAKPacket_msg, &takPacket); - return mp; + auto length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.device_callsign, + sizeof(takPacket.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes", owner.long_name, strlen(owner.long_name)); + LOG_DEBUG("Compressed device_callsign '%s' - %d bytes", takPacket.contact.device_callsign, length); + length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.callsign, sizeof(takPacket.contact.callsign) - 1, + USX_PSET_DFLT, NULL); + mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_TAKPacket_msg, &takPacket); + return mp; } -void PositionModule::sendOurPosition() -{ - bool requestReplies = currentGeneration != radioGeneration; - currentGeneration = radioGeneration; +void PositionModule::sendOurPosition() { + bool requestReplies = currentGeneration != radioGeneration; + currentGeneration = radioGeneration; - // If we changed channels, ask everyone else for their latest info - LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); - for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { - if (channels.getByIndex(channelNum).settings.has_module_settings && - channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { - sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); - return; - } + // If we changed channels, ask everyone else for their latest info + LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); + for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { + if (channels.getByIndex(channelNum).settings.has_module_settings && + channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { + sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); + return; } + } } -void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) -{ - if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { - LOG_DEBUG("Skip position send; no fresh position since boot"); - return; - } +void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position send; no fresh position since boot"); + return; + } - // cancel any not yet sent (now stale) position packets - if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) - service->cancelSending(prevPacketId); + // cancel any not yet sent (now stale) position packets + if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) + service->cancelSending(prevPacketId); - // Set's the class precision value for this particular packet - if (channels.getByIndex(channel).settings.has_module_settings) { - precision = channels.getByIndex(channel).settings.module_settings.position_precision; - } + // Set's the class precision value for this particular packet + if (channels.getByIndex(channel).settings.has_module_settings) { + precision = channels.getByIndex(channel).settings.module_settings.position_precision; + } - meshtastic_MeshPacket *p = allocPositionPacket(); - if (p == nullptr) { - LOG_DEBUG("allocPositionPacket returned a nullptr"); - return; - } + meshtastic_MeshPacket *p = allocPositionPacket(); + if (p == nullptr) { + LOG_DEBUG("allocPositionPacket returned a nullptr"); + return; + } - p->to = dest; - p->decoded.want_response = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ? false : wantReplies; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - prevPacketId = p->id; + p->to = dest; + p->decoded.want_response = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ? false : wantReplies; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + prevPacketId = p->id; - if (channel > 0) - p->channel = channel; + if (channel > 0) + p->channel = channel; - service->sendToMesh(p, RX_SRC_LOCAL, true); + service->sendToMesh(p, RX_SRC_LOCAL, true); - if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && - config.power.is_power_saving) { - meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); - notification->level = meshtastic_LogRecord_Level_INFO; - notification->time = getValidTime(RTCQualityFromNet); - sprintf(notification->message, "Sending position and sleeping for %us interval in a moment", - Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) / - 1000U); - service->sendClientNotification(notification); - sleepOnNextExecution = true; - LOG_DEBUG("Start next execution in 5s, then sleep"); - setIntervalFromNow(FIVE_SECONDS_MS); - } + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && + config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending position and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) / 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } #define RUNONCE_INTERVAL 5000; -int32_t PositionModule::runOnce() -{ - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs); - LOG_DEBUG("Sleep for %ims, then awaking to send position again", nightyNightMs); - doDeepSleep(nightyNightMs, false, false); - } +int32_t PositionModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs); + LOG_DEBUG("Sleep for %ims, then awaking to send position again", nightyNightMs); + doDeepSleep(nightyNightMs, false, false); + } - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (node == nullptr) - return RUNONCE_INTERVAL; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (node == nullptr) + return RUNONCE_INTERVAL; - // We limit our GPS broadcasts to a max rate - uint32_t now = millis(); - uint32_t intervalMs = Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs, - default_broadcast_interval_secs, numOnlineNodes); - uint32_t msSinceLastSend = now - lastGpsSend; - // Only send packets if the channel util. is less than 25% utilized or we're a tracker with less than 40% utilized. - if (!airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && - config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) { - return RUNONCE_INTERVAL; - } + // We limit our GPS broadcasts to a max rate + uint32_t now = millis(); + uint32_t intervalMs = + Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs, default_broadcast_interval_secs, numOnlineNodes); + uint32_t msSinceLastSend = now - lastGpsSend; + // Only send packets if the channel util. is less than 25% utilized or we're a tracker with less than 40% utilized. + if (!airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) { + return RUNONCE_INTERVAL; + } - bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); + bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); - if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { - if (waitingForFreshPosition) { + if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { + if (waitingForFreshPosition) { #ifdef GPS_DEBUG - LOG_DEBUG("Skip initial position send; no fresh position since boot"); + LOG_DEBUG("Skip initial position send; no fresh position since boot"); #endif - } else if (nodeDB->hasValidPosition(node)) { - lastGpsSend = now; + } else if (nodeDB->hasValidPosition(node)) { + lastGpsSend = now; - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; + lastGpsLatitude = node->position.latitude_i; + lastGpsLongitude = node->position.longitude_i; - sendOurPosition(); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { - sendLostAndFoundText(); - } - } - } else if (config.position.position_broadcast_smart_enabled) { - const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position - - if (nodeDB->hasValidPosition(node2)) { - // The minimum time (in seconds) that would pass before we are able to send a new position packet. - - auto smartPosition = getDistanceTraveledSinceLastSend(node->position); - msSinceLastSend = now - lastGpsSend; - - if (smartPosition.hasTraveledOverThreshold && - Throttle::execute( - &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { - - LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " - "minTimeInterval=%ims)", - localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, - msSinceLastSend, minimumTimeThreshold); - - // Set the current coords as our last ones, after we've compared distance with current and decided to send - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; - } - } + sendOurPosition(); + if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { + sendLostAndFoundText(); + } } + } else if (config.position.position_broadcast_smart_enabled) { + const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position - return RUNONCE_INTERVAL; // to save power only wake for our callback occasionally + if (nodeDB->hasValidPosition(node2)) { + // The minimum time (in seconds) that would pass before we are able to send a new position packet. + + auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + msSinceLastSend = now - lastGpsSend; + + if (smartPosition.hasTraveledOverThreshold && Throttle::execute( + &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, + []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + + LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " + "minTimeInterval=%ims)", + localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, minimumTimeThreshold); + + // Set the current coords as our last ones, after we've compared distance with current and decided to send + lastGpsLatitude = node->position.latitude_i; + lastGpsLongitude = node->position.longitude_i; + } + } + } + + return RUNONCE_INTERVAL; // to save power only wake for our callback occasionally } -void PositionModule::sendLostAndFoundText() -{ - meshtastic_MeshPacket *p = allocDataPacket(); - p->to = NODENUM_BROADCAST; - char *message = new char[60]; - sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7)); - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); +void PositionModule::sendLostAndFoundText() { + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = NODENUM_BROADCAST; + char *message = new char[60]; + sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7)); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->want_ack = false; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - service->sendToMesh(p, RX_SRC_LOCAL, true); - delete[] message; + service->sendToMesh(p, RX_SRC_LOCAL, true); + delete[] message; } // Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision -static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) -{ - if (precisionBits > 0 && precisionBits < 32) { - // Build mask for top 'precisionBits' bits of a 32-bit unsigned field - const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); - // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but - // the bitmask logic used previously operated as unsigned—preserve that behavior by - // casting to uint32_t for masking, then back to int32_t. - uint32_t lat_u = static_cast(inLat) & mask; - uint32_t lon_u = static_cast(inLon) & mask; +static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) { + if (precisionBits > 0 && precisionBits < 32) { + // Build mask for top 'precisionBits' bits of a 32-bit unsigned field + const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); + // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but + // the bitmask logic used previously operated as unsigned—preserve that behavior by + // casting to uint32_t for masking, then back to int32_t. + uint32_t lat_u = static_cast(inLat) & mask; + uint32_t lon_u = static_cast(inLon) & mask; - // Add the "center of cell" offset used elsewhere: - // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. - uint32_t center_offset = (1u << (31 - precisionBits)); - lat_u += center_offset; - lon_u += center_offset; + // Add the "center of cell" offset used elsewhere: + // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. + uint32_t center_offset = (1u << (31 - precisionBits)); + lat_u += center_offset; + lon_u += center_offset; - outLat = static_cast(lat_u); - outLon = static_cast(lon_u); - } else { - // full precision: return input unchanged - outLat = inLat; - outLon = inLon; - } + outLat = static_cast(lat_u); + outLon = static_cast(lon_u); + } else { + // full precision: return input unchanged + outLat = inLat; + outLon = inLon; + } } -struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) -{ - const uint32_t distanceTravelThreshold = - Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); +struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) { + const uint32_t distanceTravelThreshold = Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); - int32_t lastLatImprecise, lastLonImprecise; - int32_t currentLatImprecise, currentLonImprecise; + int32_t lastLatImprecise, lastLonImprecise; + int32_t currentLatImprecise, currentLonImprecise; - computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); - computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, - currentLonImprecise); + computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); + computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, currentLonImprecise); - float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, - currentLonImprecise * 1e-7); + float distMeters = + GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, currentLonImprecise * 1e-7); - float distanceTraveled = fabsf(distMeters); + float distanceTraveled = fabsf(distMeters); - return SmartPosition{.distanceTraveled = distanceTraveled, - .distanceThreshold = distanceTravelThreshold, - .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; + return SmartPosition{.distanceTraveled = distanceTraveled, + .distanceThreshold = distanceTravelThreshold, + .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; } -void PositionModule::handleNewPosition() -{ - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position - // We limit our GPS broadcasts to a max rate - if (nodeDB->hasValidPosition(node2)) { - auto smartPosition = getDistanceTraveledSinceLastSend(node->position); - uint32_t msSinceLastSend = millis() - lastGpsSend; - if (smartPosition.hasTraveledOverThreshold && - Throttle::execute( - &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { - LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " - "minTimeInterval=%ims)", - localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, - minimumTimeThreshold); +void PositionModule::handleNewPosition() { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position + // We limit our GPS broadcasts to a max rate + if (nodeDB->hasValidPosition(node2)) { + auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + uint32_t msSinceLastSend = millis() - lastGpsSend; + if (smartPosition.hasTraveledOverThreshold && Throttle::execute( + &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, + []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " + "minTimeInterval=%ims)", + localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, minimumTimeThreshold); - // Set the current coords as our last ones, after we've compared distance with current and decided to send - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; - } + // Set the current coords as our last ones, after we've compared distance with current and decided to send + lastGpsLatitude = node->position.latitude_i; + lastGpsLongitude = node->position.longitude_i; } + } } #endif \ No newline at end of file diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 32e499531..4efd6a58a 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -6,80 +6,78 @@ /** * Position module for sending/receiving positions into the mesh */ -class PositionModule : public ProtobufModule, private concurrency::OSThread -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &PositionModule::handleStatusUpdate); +class PositionModule : public ProtobufModule, private concurrency::OSThread { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &PositionModule::handleStatusUpdate); - /// The id of the last packet we sent, to allow us to cancel it if we make something fresher - PacketId prevPacketId = 0; + /// The id of the last packet we sent, to allow us to cancel it if we make something fresher + PacketId prevPacketId = 0; - /// We limit our GPS broadcasts to a max rate - uint32_t lastGpsSend = 0; + /// We limit our GPS broadcasts to a max rate + uint32_t lastGpsSend = 0; - // Store the latest good lat / long - int32_t lastGpsLatitude = 0; - int32_t lastGpsLongitude = 0; + // Store the latest good lat / long + int32_t lastGpsLatitude = 0; + int32_t lastGpsLongitude = 0; - /// We force a rebroadcast if the radio settings change - uint32_t currentGeneration = 0; + /// We force a rebroadcast if the radio settings change + uint32_t currentGeneration = 0; - public: - /** Constructor - * name is for debugging output - */ - PositionModule(); +public: + /** Constructor + * name is for debugging output + */ + PositionModule(); - /** - * Send our position into the mesh - */ - void sendOurPosition(NodeNum dest, bool wantReplies = false, uint8_t channel = 0); - void sendOurPosition(); + /** + * Send our position into the mesh + */ + void sendOurPosition(NodeNum dest, bool wantReplies = false, uint8_t channel = 0); + void sendOurPosition(); - void handleNewPosition(); + void handleNewPosition(); - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *p) override; - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) override; + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) override; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /** Does our periodic broadcast */ - virtual int32_t runOnce() override; + /** Does our periodic broadcast */ + virtual int32_t runOnce() override; - private: - meshtastic_MeshPacket *allocPositionPacket(); - struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); - meshtastic_MeshPacket *allocAtakPli(); - void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); - uint32_t precision; - void sendLostAndFoundText(); - bool hasQualityTimesource(); - bool hasGPS(); - uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) +private: + meshtastic_MeshPacket *allocPositionPacket(); + struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); + meshtastic_MeshPacket *allocAtakPli(); + void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); + uint32_t precision; + void sendLostAndFoundText(); + bool hasQualityTimesource(); + bool hasGPS(); + uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) #if USERPREFS_EVENT_MODE - // In event mode we want to prevent excessive position broadcasts - // we set the minimum interval to 5m - const uint32_t minimumTimeThreshold = - max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); + // In event mode we want to prevent excessive position broadcasts + // we set the minimum interval to 5m + const uint32_t minimumTimeThreshold = + max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); #else - const uint32_t minimumTimeThreshold = - Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); + const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); #endif }; struct SmartPosition { - float distanceTraveled; - uint32_t distanceThreshold; - bool hasTraveledOverThreshold; + float distanceTraveled; + uint32_t distanceThreshold; + bool hasTraveledOverThreshold; }; extern PositionModule *positionModule; \ No newline at end of file diff --git a/src/modules/PowerStressModule.cpp b/src/modules/PowerStressModule.cpp index d487fe6fc..1ee11ace5 100644 --- a/src/modules/PowerStressModule.cpp +++ b/src/modules/PowerStressModule.cpp @@ -14,121 +14,115 @@ extern void printInfo(); PowerStressModule::PowerStressModule() - : ProtobufModule("powerstress", meshtastic_PortNum_POWERSTRESS_APP, &meshtastic_PowerStressMessage_msg), - concurrency::OSThread("PowerStress") -{ + : ProtobufModule("powerstress", meshtastic_PortNum_POWERSTRESS_APP, &meshtastic_PowerStressMessage_msg), concurrency::OSThread("PowerStress") {} + +bool PowerStressModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_PowerStressMessage *pptr) { + // We only respond to messages if powermon debugging is already on + if (config.power.powermon_enables) { + auto p = *pptr; + LOG_INFO("Received PowerStress cmd=%d", p.cmd); + + // Some commands we can handle immediately, anything else gets deferred to be handled by our thread + switch (p.cmd) { + case meshtastic_PowerStressMessage_Opcode_UNSET: + LOG_ERROR("PowerStress operation unset"); + break; + + case meshtastic_PowerStressMessage_Opcode_PRINT_INFO: + printInfo(); + + // Now that we know we are actually doing power stress testing, go ahead and turn on all enables (so the log is + // fully detailed) + powerMon->force_enabled = true; + break; + + default: + if (currentMessage.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) + LOG_ERROR("PowerStress operation %d already in progress! Can't start new command", currentMessage.cmd); + else + currentMessage = p; // copy for use by thread (the message provided to us will be getting freed) + break; + } + } + return true; } -bool PowerStressModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_PowerStressMessage *pptr) -{ - // We only respond to messages if powermon debugging is already on - if (config.power.powermon_enables) { - auto p = *pptr; - LOG_INFO("Received PowerStress cmd=%d", p.cmd); +int32_t PowerStressModule::runOnce() { + if (!config.power.powermon_enables) { + // Powermon not enabled - stop using CPU/stop this thread + return disable(); + } - // Some commands we can handle immediately, anything else gets deferred to be handled by our thread - switch (p.cmd) { - case meshtastic_PowerStressMessage_Opcode_UNSET: - LOG_ERROR("PowerStress operation unset"); - break; + int32_t sleep_msec = 10; // when not active check for new messages every 10ms - case meshtastic_PowerStressMessage_Opcode_PRINT_INFO: - printInfo(); + auto &p = currentMessage; - // Now that we know we are actually doing power stress testing, go ahead and turn on all enables (so the log is fully - // detailed) - powerMon->force_enabled = true; - break; + if (isRunningCommand) { + // Done with the previous command - our sleep must have finished + p.cmd = meshtastic_PowerStressMessage_Opcode_UNSET; + p.num_seconds = 0; + isRunningCommand = false; + LOG_INFO("S:PS:%u", p.cmd); + } else { + if (p.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) { + sleep_msec = (int32_t)(p.num_seconds * 1000); + isRunningCommand = !!sleep_msec; // if the command wants us to sleep, make sure to mark that we have something running + LOG_INFO("S:PS:%u", + p.cmd); // Emit a structured log saying we are starting a powerstress state (to make it easier to parse later) - default: - if (currentMessage.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) - LOG_ERROR("PowerStress operation %d already in progress! Can't start new command", currentMessage.cmd); - else - currentMessage = p; // copy for use by thread (the message provided to us will be getting freed) - break; - } + switch (p.cmd) { + case meshtastic_PowerStressMessage_Opcode_LED_ON: + ledForceOn.set(true); + break; + case meshtastic_PowerStressMessage_Opcode_LED_OFF: + ledForceOn.set(false); + break; + case meshtastic_PowerStressMessage_Opcode_GPS_ON: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_GPS_OFF: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_LORA_OFF: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_LORA_RX: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_LORA_TX: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_SCREEN_OFF: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_SCREEN_ON: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_BT_OFF: + setBluetoothEnable(false); + break; + case meshtastic_PowerStressMessage_Opcode_BT_ON: + setBluetoothEnable(true); + break; + case meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP: + doDeepSleep(sleep_msec, true, true); + break; + case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: { + uint32_t start_msec = millis(); + while (Throttle::isWithinTimespanMs(start_msec, sleep_msec)) + ; // Don't let CPU idle at all + sleep_msec = 0; // we already slept + break; + } + case meshtastic_PowerStressMessage_Opcode_CPU_IDLE: + // FIXME - implement + break; + default: + LOG_ERROR("PowerStress operation %d not yet implemented!", p.cmd); + sleep_msec = 0; // Don't do whatever sleep was requested... + break; + } } - return true; -} - -int32_t PowerStressModule::runOnce() -{ - if (!config.power.powermon_enables) { - // Powermon not enabled - stop using CPU/stop this thread - return disable(); - } - - int32_t sleep_msec = 10; // when not active check for new messages every 10ms - - auto &p = currentMessage; - - if (isRunningCommand) { - // Done with the previous command - our sleep must have finished - p.cmd = meshtastic_PowerStressMessage_Opcode_UNSET; - p.num_seconds = 0; - isRunningCommand = false; - LOG_INFO("S:PS:%u", p.cmd); - } else { - if (p.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) { - sleep_msec = (int32_t)(p.num_seconds * 1000); - isRunningCommand = !!sleep_msec; // if the command wants us to sleep, make sure to mark that we have something running - LOG_INFO( - "S:PS:%u", - p.cmd); // Emit a structured log saying we are starting a powerstress state (to make it easier to parse later) - - switch (p.cmd) { - case meshtastic_PowerStressMessage_Opcode_LED_ON: - ledForceOn.set(true); - break; - case meshtastic_PowerStressMessage_Opcode_LED_OFF: - ledForceOn.set(false); - break; - case meshtastic_PowerStressMessage_Opcode_GPS_ON: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_GPS_OFF: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_LORA_OFF: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_LORA_RX: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_LORA_TX: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_SCREEN_OFF: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_SCREEN_ON: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_BT_OFF: - setBluetoothEnable(false); - break; - case meshtastic_PowerStressMessage_Opcode_BT_ON: - setBluetoothEnable(true); - break; - case meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP: - doDeepSleep(sleep_msec, true, true); - break; - case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: { - uint32_t start_msec = millis(); - while (Throttle::isWithinTimespanMs(start_msec, sleep_msec)) - ; // Don't let CPU idle at all - sleep_msec = 0; // we already slept - break; - } - case meshtastic_PowerStressMessage_Opcode_CPU_IDLE: - // FIXME - implement - break; - default: - LOG_ERROR("PowerStress operation %d not yet implemented!", p.cmd); - sleep_msec = 0; // Don't do whatever sleep was requested... - break; - } - } - } - return sleep_msec; + } + return sleep_msec; } \ No newline at end of file diff --git a/src/modules/PowerStressModule.h b/src/modules/PowerStressModule.h index 2d449f690..b27b225ba 100644 --- a/src/modules/PowerStressModule.h +++ b/src/modules/PowerStressModule.h @@ -6,33 +6,32 @@ /** * A module that provides easy low-level remote access to device hardware. */ -class PowerStressModule : public ProtobufModule, private concurrency::OSThread -{ - meshtastic_PowerStressMessage currentMessage = meshtastic_PowerStressMessage_init_default; - bool isRunningCommand = false; +class PowerStressModule : public ProtobufModule, private concurrency::OSThread { + meshtastic_PowerStressMessage currentMessage = meshtastic_PowerStressMessage_init_default; + bool isRunningCommand = false; - public: - /** Constructor - * name is for debugging output - */ - PowerStressModule(); +public: + /** Constructor + * name is for debugging output + */ + PowerStressModule(); - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_PowerStressMessage *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_PowerStressMessage *p) override; - /** - * Periodically read the gpios we have been asked to WATCH, if they have changed, - * broadcast a message with the change information. - * - * The method that will be called each time our thread gets a chance to run - * - * Returns desired period for next invocation (or RUN_SAME for no change) - */ - virtual int32_t runOnce() override; + /** + * Periodically read the gpios we have been asked to WATCH, if they have changed, + * broadcast a message with the change information. + * + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce() override; }; extern PowerStressModule powerStressModule; \ No newline at end of file diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp index 026b3028d..4b12c004e 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -29,80 +29,79 @@ RangeTestModule::RangeTestModule() : concurrency::OSThread("RangeTest") {} uint32_t packetSequence = 0; -int32_t RangeTestModule::runOnce() -{ +int32_t RangeTestModule::runOnce() { #if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.range_test.enabled = 1; - // moduleConfig.range_test.sender = 30; - // moduleConfig.range_test.save = 1; - // moduleConfig.range_test.clear_on_reboot = 1; + // moduleConfig.range_test.enabled = 1; + // moduleConfig.range_test.sender = 30; + // moduleConfig.range_test.save = 1; + // moduleConfig.range_test.clear_on_reboot = 1; - // Fixed position is useful when testing indoors. - // config.position.fixed_position = 1; + // Fixed position is useful when testing indoors. + // config.position.fixed_position = 1; - uint32_t senderHeartbeat = moduleConfig.range_test.sender * 1000; - if (moduleConfig.range_test.enabled) { + uint32_t senderHeartbeat = moduleConfig.range_test.sender * 1000; + if (moduleConfig.range_test.enabled) { - if (firstTime) { - rangeTestModuleRadio = new RangeTestModuleRadio(); + if (firstTime) { + rangeTestModuleRadio = new RangeTestModuleRadio(); - firstTime = 0; + firstTime = 0; - if (moduleConfig.range_test.clear_on_reboot) { - // User wants to delete previous range test(s) - LOG_INFO("Range Test Module - Clearing out previous test file"); - rangeTestModuleRadio->removeFile(); - } - if (moduleConfig.range_test.sender) { - LOG_INFO("Init Range Test Module -- Sender"); - started = millis(); // make a note of when we started - return (5000); // Sending first message 5 seconds after initialization. - } else { - LOG_INFO("Init Range Test Module -- Receiver"); - return disable(); - // This thread does not need to run as a receiver - } - } else { - - if (moduleConfig.range_test.sender) { - // If sender - LOG_INFO("Range Test Module - Sending heartbeat every %d ms", (senderHeartbeat)); - - LOG_INFO("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); - LOG_INFO("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); - LOG_INFO("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); - LOG_INFO("gpsStatus->getDOP() %d", gpsStatus->getDOP()); - LOG_INFO("fixed_position() %d", config.position.fixed_position); - - // Only send packets if the channel is less than 25% utilized. - if (airTime->isTxAllowedChannelUtil(true)) { - rangeTestModuleRadio->sendPayload(); - } - - // If we have been running for more than 8 hours, turn module back off - if (!Throttle::isWithinTimespanMs(started, 28800000)) { - LOG_INFO("Range Test Module - Disable after 8 hours"); - return disable(); - } else { - return (senderHeartbeat); - } - } else { - return disable(); - // This thread does not need to run as a receiver - } - } + if (moduleConfig.range_test.clear_on_reboot) { + // User wants to delete previous range test(s) + LOG_INFO("Range Test Module - Clearing out previous test file"); + rangeTestModuleRadio->removeFile(); + } + if (moduleConfig.range_test.sender) { + LOG_INFO("Init Range Test Module -- Sender"); + started = millis(); // make a note of when we started + return (5000); // Sending first message 5 seconds after initialization. + } else { + LOG_INFO("Init Range Test Module -- Receiver"); + return disable(); + // This thread does not need to run as a receiver + } } else { - LOG_INFO("Range Test Module - Disabled"); + + if (moduleConfig.range_test.sender) { + // If sender + LOG_INFO("Range Test Module - Sending heartbeat every %d ms", (senderHeartbeat)); + + LOG_INFO("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); + LOG_INFO("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); + LOG_INFO("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); + LOG_INFO("gpsStatus->getDOP() %d", gpsStatus->getDOP()); + LOG_INFO("fixed_position() %d", config.position.fixed_position); + + // Only send packets if the channel is less than 25% utilized. + if (airTime->isTxAllowedChannelUtil(true)) { + rangeTestModuleRadio->sendPayload(); + } + + // If we have been running for more than 8 hours, turn module back off + if (!Throttle::isWithinTimespanMs(started, 28800000)) { + LOG_INFO("Range Test Module - Disable after 8 hours"); + return disable(); + } else { + return (senderHeartbeat); + } + } else { + return disable(); + // This thread does not need to run as a receiver + } } + } else { + LOG_INFO("Range Test Module - Disabled"); + } #endif - return disable(); + return disable(); } /** @@ -111,233 +110,229 @@ int32_t RangeTestModule::runOnce() * @param dest The destination node number. * @param wantReplies Whether or not to request replies from the destination node. */ -void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies) -{ - meshtastic_MeshPacket *p = allocDataPacket(); - p->to = dest; - p->decoded.want_response = wantReplies; - p->hop_limit = 0; - p->want_ack = false; +void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies) { + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = dest; + p->decoded.want_response = wantReplies; + p->hop_limit = 0; + p->want_ack = false; - packetSequence++; + packetSequence++; - static char heartbeatString[MAX_LORA_PAYLOAD_LEN + 1]; - snprintf(heartbeatString, sizeof(heartbeatString), "seq %u", packetSequence); + static char heartbeatString[MAX_LORA_PAYLOAD_LEN + 1]; + snprintf(heartbeatString, sizeof(heartbeatString), "seq %u", packetSequence); - p->decoded.payload.size = strlen(heartbeatString); // You must specify how many bytes are in the reply - memcpy(p->decoded.payload.bytes, heartbeatString, p->decoded.payload.size); + p->decoded.payload.size = strlen(heartbeatString); // You must specify how many bytes are in the reply + memcpy(p->decoded.payload.bytes, heartbeatString, p->decoded.payload.size); - service->sendToMesh(p); + service->sendToMesh(p); - // TODO: Handle this better. We want to keep the phone awake otherwise it stops sending. - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + // TODO: Handle this better. We want to keep the phone awake otherwise it stops sending. + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); } -ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) -{ +ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) - if (moduleConfig.range_test.enabled) { + if (moduleConfig.range_test.enabled) { - /* - auto &p = mp.decoded; - LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", - LOG_INFO.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); - */ - - if (!isFromUs(&mp)) { - if (moduleConfig.range_test.save) { - appendFile(mp); - } - - /* - NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); - - LOG_DEBUG("-----------------------------------------"); - LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); - LOG_DEBUG("p.payload.size %d", p.payload.size); - LOG_DEBUG("---- Received Packet:"); - LOG_DEBUG("mp.from %d", mp.from); - LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); - LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi); - LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); - LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); - LOG_DEBUG("n->user.long_name %s", n->user.long_name); - LOG_DEBUG("n->user.short_name %s", n->user.short_name); - LOG_DEBUG("n->has_position %d", n->has_position); - LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); - LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); - LOG_DEBUG("---- Current device location information:"); - LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); - LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); - LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); - LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); - LOG_DEBUG("-----------------------------------------"); - */ - } - } else { - LOG_INFO("Range Test Module Disabled"); - } - -#endif - - return ProcessMessage::CONTINUE; // Let others look at this message also if they want -} - -bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) -{ -#ifdef ARCH_ESP32 - auto &p = mp.decoded; - - meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); /* - LOG_DEBUG("-----------------------------------------"); - LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); - LOG_DEBUG("p.payload.size %d", p.payload.size); - LOG_DEBUG("---- Received Packet:"); - LOG_DEBUG("mp.from %d", mp.from); - LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); - LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); - LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); - LOG_DEBUG("n->user.long_name %s", n->user.long_name); - LOG_DEBUG("n->user.short_name %s", n->user.short_name); - LOG_DEBUG("n->has_position %d", n->has_position); - LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); - LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); - LOG_DEBUG("---- Current device location information:"); - LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); - LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); - LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); - LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); - LOG_DEBUG("-----------------------------------------"); + auto &p = mp.decoded; + LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", + LOG_INFO.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); */ - concurrency::LockGuard g(spiLock); - if (!FSBegin()) { - LOG_DEBUG("An Error has occurred while mounting the filesystem"); - return 0; + + if (!isFromUs(&mp)) { + if (moduleConfig.range_test.save) { + appendFile(mp); + } + + /* + NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); + + LOG_DEBUG("-----------------------------------------"); + LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); + LOG_DEBUG("p.payload.size %d", p.payload.size); + LOG_DEBUG("---- Received Packet:"); + LOG_DEBUG("mp.from %d", mp.from); + LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); + LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi); + LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); + LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); + LOG_DEBUG("n->user.long_name %s", n->user.long_name); + LOG_DEBUG("n->user.short_name %s", n->user.short_name); + LOG_DEBUG("n->has_position %d", n->has_position); + LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); + LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); + LOG_DEBUG("---- Current device location information:"); + LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); + LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); + LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); + LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); + LOG_DEBUG("-----------------------------------------"); + */ + } + } else { + LOG_INFO("Range Test Module Disabled"); + } + +#endif + + return ProcessMessage::CONTINUE; // Let others look at this message also if they want +} + +bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) { +#ifdef ARCH_ESP32 + auto &p = mp.decoded; + + meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); + /* + LOG_DEBUG("-----------------------------------------"); + LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); + LOG_DEBUG("p.payload.size %d", p.payload.size); + LOG_DEBUG("---- Received Packet:"); + LOG_DEBUG("mp.from %d", mp.from); + LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); + LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); + LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); + LOG_DEBUG("n->user.long_name %s", n->user.long_name); + LOG_DEBUG("n->user.short_name %s", n->user.short_name); + LOG_DEBUG("n->has_position %d", n->has_position); + LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); + LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); + LOG_DEBUG("---- Current device location information:"); + LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); + LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); + LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); + LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); + LOG_DEBUG("-----------------------------------------"); + */ + concurrency::LockGuard g(spiLock); + if (!FSBegin()) { + LOG_DEBUG("An Error has occurred while mounting the filesystem"); + return 0; + } + + if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { + LOG_DEBUG("Filesystem doesn't have enough free space. Aborting write"); + return 0; + } + + FSCom.mkdir("/static"); + + // If the file doesn't exist, write the header. + if (!FSCom.exists("/static/rangetest.csv")) { + //--------- Write to file + File fileToWrite = FSCom.open("/static/rangetest.csv", FILE_WRITE); + + if (!fileToWrite) { + LOG_ERROR("There was an error opening the file for writing"); + return 0; } - if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { - LOG_DEBUG("Filesystem doesn't have enough free space. Aborting write"); - return 0; - } - - FSCom.mkdir("/static"); - - // If the file doesn't exist, write the header. - if (!FSCom.exists("/static/rangetest.csv")) { - //--------- Write to file - File fileToWrite = FSCom.open("/static/rangetest.csv", FILE_WRITE); - - if (!fileToWrite) { - LOG_ERROR("There was an error opening the file for writing"); - return 0; - } - - // Print the CSV header - if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx " - "snr,distance,hop limit,payload,rx rssi")) { - LOG_INFO("File was written"); - } else { - LOG_ERROR("File write failed"); - } - fileToWrite.flush(); - fileToWrite.close(); - } - - //--------- Append content to file - File fileToAppend = FSCom.open("/static/rangetest.csv", FILE_APPEND); - - if (!fileToAppend) { - LOG_ERROR("There was an error opening the file for appending"); - return 0; - } - - struct timeval tv; - if (!gettimeofday(&tv, NULL)) { - long hms = tv.tv_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - fileToAppend.printf("%02d:%02d:%02d,", hour, min, sec); // Time + // Print the CSV header + if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx " + "snr,distance,hop limit,payload,rx rssi")) { + LOG_INFO("File was written"); } else { - fileToAppend.printf("??:??:??,"); // Time + LOG_ERROR("File write failed"); } + fileToWrite.flush(); + fileToWrite.close(); + } - fileToAppend.printf("%d,", getFrom(&mp)); // From - fileToAppend.printf("%s,", n->user.long_name); // Long Name - fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat - fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long - if (gpsStatus->getIsConnected() || config.position.fixed_position) { - fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat - fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long - fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude - } else { - // When the phone API is in use, the node info will be updated with position - meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); - fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat - fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long - fileToAppend.printf("%d,", us->position.altitude); // RX Altitude - } + //--------- Append content to file + File fileToAppend = FSCom.open("/static/rangetest.csv", FILE_APPEND); - fileToAppend.printf("%f,", mp.rx_snr); // RX SNR + if (!fileToAppend) { + LOG_ERROR("There was an error opening the file for appending"); + return 0; + } - if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { - float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, - gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7); - fileToAppend.printf("%f,", distance); // Distance in meters - } else { - fileToAppend.printf("0,"); - } + struct timeval tv; + if (!gettimeofday(&tv, NULL)) { + long hms = tv.tv_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - // TODO: If quotes are found in the payload, it has to be escaped. - fileToAppend.printf("\"%s\"\n", p.payload.bytes); - fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI + fileToAppend.printf("%02d:%02d:%02d,", hour, min, sec); // Time + } else { + fileToAppend.printf("??:??:??,"); // Time + } - fileToAppend.flush(); - fileToAppend.close(); + fileToAppend.printf("%d,", getFrom(&mp)); // From + fileToAppend.printf("%s,", n->user.long_name); // Long Name + fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat + fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long + if (gpsStatus->getIsConnected() || config.position.fixed_position) { + fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat + fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long + fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude + } else { + // When the phone API is in use, the node info will be updated with position + meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); + fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat + fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long + fileToAppend.printf("%d,", us->position.altitude); // RX Altitude + } - return 1; + fileToAppend.printf("%f,", mp.rx_snr); // RX SNR + + if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { + float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, gpsStatus->getLatitude() * 1e-7, + gpsStatus->getLongitude() * 1e-7); + fileToAppend.printf("%f,", distance); // Distance in meters + } else { + fileToAppend.printf("0,"); + } + + fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit + + // TODO: If quotes are found in the payload, it has to be escaped. + fileToAppend.printf("\"%s\"\n", p.payload.bytes); + fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI + + fileToAppend.flush(); + fileToAppend.close(); + + return 1; #else - LOG_ERROR("Failed to store range test results - feature only available for ESP32"); + LOG_ERROR("Failed to store range test results - feature only available for ESP32"); - return 0; + return 0; #endif } -bool RangeTestModuleRadio::removeFile() -{ +bool RangeTestModuleRadio::removeFile() { #ifdef ARCH_ESP32 - if (!FSBegin()) { - LOG_DEBUG("An Error has occurred while mounting the filesystem"); - return 0; - } - - if (!FSCom.exists("/static/rangetest.csv")) { - LOG_DEBUG("No range tests found."); - return 0; - } - - LOG_INFO("Deleting previous range test."); - bool result = FSCom.remove("/static/rangetest.csv"); - - if (!result) { - LOG_ERROR("Failed to delete range test."); - return 0; - } - LOG_INFO("Range test removed."); - - return 1; -#else - LOG_ERROR("Failed to remove range test results - feature only available for ESP32"); - + if (!FSBegin()) { + LOG_DEBUG("An Error has occurred while mounting the filesystem"); return 0; + } + + if (!FSCom.exists("/static/rangetest.csv")) { + LOG_DEBUG("No range tests found."); + return 0; + } + + LOG_INFO("Deleting previous range test."); + bool result = FSCom.remove("/static/rangetest.csv"); + + if (!result) { + LOG_ERROR("Failed to delete range test."); + return 0; + } + LOG_INFO("Range test removed."); + + return 1; +#else + LOG_ERROR("Failed to remove range test results - feature only available for ESP32"); + + return 0; #endif } \ No newline at end of file diff --git a/src/modules/RangeTestModule.h b/src/modules/RangeTestModule.h index 0512e70a8..4ae39b179 100644 --- a/src/modules/RangeTestModule.h +++ b/src/modules/RangeTestModule.h @@ -6,16 +6,15 @@ #include #include -class RangeTestModule : private concurrency::OSThread -{ - bool firstTime = 1; - unsigned long started = 0; +class RangeTestModule : private concurrency::OSThread { + bool firstTime = 1; + unsigned long started = 0; - public: - RangeTestModule(); +public: + RangeTestModule(); - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; }; extern RangeTestModule *rangeTestModule; @@ -24,38 +23,36 @@ extern RangeTestModule *rangeTestModule; * Radio interface for RangeTestModule * */ -class RangeTestModuleRadio : public SinglePortModule -{ - uint32_t lastRxID = 0; +class RangeTestModuleRadio : public SinglePortModule { + uint32_t lastRxID = 0; - public: - RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_RANGE_TEST_APP) - { - loopbackOk = true; // Allow locally generated messages to loop back to the client - } +public: + RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_RANGE_TEST_APP) { + loopbackOk = true; // Allow locally generated messages to loop back to the client + } - /** - * Send our payload into the mesh - */ - void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - /** - * Append range test data to the file on the Filesystem - */ - bool appendFile(const meshtastic_MeshPacket &mp); + /** + * Append range test data to the file on the Filesystem + */ + bool appendFile(const meshtastic_MeshPacket &mp); - /** - * Cleanup range test data from filesystem - */ - bool removeFile(); + /** + * Cleanup range test data from filesystem + */ + bool removeFile(); - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; extern RangeTestModuleRadio *rangeTestModuleRadio; diff --git a/src/modules/RemoteHardwareModule.cpp b/src/modules/RemoteHardwareModule.cpp index 04cfeb651..82510440a 100644 --- a/src/modules/RemoteHardwareModule.cpp +++ b/src/modules/RemoteHardwareModule.cpp @@ -16,146 +16,139 @@ #define WATCH_INTERVAL_MSEC (30 * 1000) // Tests for access to read from or write to a specified GPIO pin -static bool pinAccessAllowed(uint64_t mask, uint8_t pin) -{ - // If undefined pin access is allowed, don't check the pin and just return true - if (moduleConfig.remote_hardware.allow_undefined_pin_access) { - return true; - } +static bool pinAccessAllowed(uint64_t mask, uint8_t pin) { + // If undefined pin access is allowed, don't check the pin and just return true + if (moduleConfig.remote_hardware.allow_undefined_pin_access) { + return true; + } - // Test to see if the pin is in the list of allowed pins and return true if found - if (mask & (1ULL << pin)) { - return true; - } + // Test to see if the pin is in the list of allowed pins and return true if found + if (mask & (1ULL << pin)) { + return true; + } - return false; + return false; } /// Set pin modes for every set bit in a mask -static void pinModes(uint64_t mask, uint8_t mode, uint64_t maskAvailable) -{ - for (uint64_t i = 0; i < NUM_GPIOS; i++) { - if (mask & (1ULL << i)) { - if (pinAccessAllowed(maskAvailable, i)) { - pinMode(i, mode); - } - } +static void pinModes(uint64_t mask, uint8_t mode, uint64_t maskAvailable) { + for (uint64_t i = 0; i < NUM_GPIOS; i++) { + if (mask & (1ULL << i)) { + if (pinAccessAllowed(maskAvailable, i)) { + pinMode(i, mode); + } } + } } /// Read all the pins mentioned in a mask -static uint64_t digitalReads(uint64_t mask, uint64_t maskAvailable) -{ - uint64_t res = 0; +static uint64_t digitalReads(uint64_t mask, uint64_t maskAvailable) { + uint64_t res = 0; - pinModes(mask, INPUT_PULLUP, maskAvailable); + pinModes(mask, INPUT_PULLUP, maskAvailable); - for (uint64_t i = 0; i < NUM_GPIOS; i++) { - uint64_t m = 1ULL << i; - if (mask & m && pinAccessAllowed(maskAvailable, i)) { - if (digitalRead(i)) { - res |= m; - } - } + for (uint64_t i = 0; i < NUM_GPIOS; i++) { + uint64_t m = 1ULL << i; + if (mask & m && pinAccessAllowed(maskAvailable, i)) { + if (digitalRead(i)) { + res |= m; + } } + } - return res; + return res; } RemoteHardwareModule::RemoteHardwareModule() : ProtobufModule("remotehardware", meshtastic_PortNum_REMOTE_HARDWARE_APP, &meshtastic_HardwareMessage_msg), - concurrency::OSThread("RemoteHardware") -{ - // restrict to the gpio channel for rx - boundChannel = Channels::gpioChannel; + concurrency::OSThread("RemoteHardware") { + // restrict to the gpio channel for rx + boundChannel = Channels::gpioChannel; - // Pull available pin allowlist from config and build a bitmask out of it for fast comparisons later - for (uint8_t i = 0; i < 4; i++) { - availablePins += 1ULL << moduleConfig.remote_hardware.available_pins[i].gpio_pin; - } + // Pull available pin allowlist from config and build a bitmask out of it for fast comparisons later + for (uint8_t i = 0; i < 4; i++) { + availablePins += 1ULL << moduleConfig.remote_hardware.available_pins[i].gpio_pin; + } } -bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_HardwareMessage *pptr) -{ - if (moduleConfig.remote_hardware.enabled) { - auto p = *pptr; - LOG_INFO("Received RemoteHardware type=%d", p.type); +bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_HardwareMessage *pptr) { + if (moduleConfig.remote_hardware.enabled) { + auto p = *pptr; + LOG_INFO("Received RemoteHardware type=%d", p.type); - switch (p.type) { - case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { - pinModes(p.gpio_mask, OUTPUT, availablePins); - for (uint8_t i = 0; i < NUM_GPIOS; i++) { - uint64_t mask = 1ULL << i; - if (p.gpio_mask & mask && pinAccessAllowed(availablePins, i)) { - digitalWrite(i, (p.gpio_value & mask) ? 1 : 0); - } - } - - break; + switch (p.type) { + case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { + pinModes(p.gpio_mask, OUTPUT, availablePins); + for (uint8_t i = 0; i < NUM_GPIOS; i++) { + uint64_t mask = 1ULL << i; + if (p.gpio_mask & mask && pinAccessAllowed(availablePins, i)) { + digitalWrite(i, (p.gpio_value & mask) ? 1 : 0); } + } - case meshtastic_HardwareMessage_Type_READ_GPIOS: { - uint64_t res = digitalReads(p.gpio_mask, availablePins); - - // Send the reply - meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; - r.type = meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY; - r.gpio_value = res; - r.gpio_mask = p.gpio_mask; - meshtastic_MeshPacket *p2 = allocDataProtobuf(r); - setReplyTo(p2, req); - myReply = p2; - break; - } - - case meshtastic_HardwareMessage_Type_WATCH_GPIOS: { - watchGpios = p.gpio_mask; - lastWatchMsec = 0; // Force a new publish soon - previousWatch = - ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) - enabled = true; // Let our thread run at least once - setInterval(2000); // Set a new interval so we'll run soon - LOG_INFO("Now watching GPIOs 0x%llx", watchGpios); - break; - } - - case meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY: - case meshtastic_HardwareMessage_Type_GPIOS_CHANGED: - break; // Ignore - we might see our own replies - - default: - LOG_ERROR("Hardware operation %d not yet implemented! FIXME", p.type); - break; - } + break; } - return false; + case meshtastic_HardwareMessage_Type_READ_GPIOS: { + uint64_t res = digitalReads(p.gpio_mask, availablePins); + + // Send the reply + meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; + r.type = meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY; + r.gpio_value = res; + r.gpio_mask = p.gpio_mask; + meshtastic_MeshPacket *p2 = allocDataProtobuf(r); + setReplyTo(p2, req); + myReply = p2; + break; + } + + case meshtastic_HardwareMessage_Type_WATCH_GPIOS: { + watchGpios = p.gpio_mask; + lastWatchMsec = 0; // Force a new publish soon + previousWatch = ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) + enabled = true; // Let our thread run at least once + setInterval(2000); // Set a new interval so we'll run soon + LOG_INFO("Now watching GPIOs 0x%llx", watchGpios); + break; + } + + case meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY: + case meshtastic_HardwareMessage_Type_GPIOS_CHANGED: + break; // Ignore - we might see our own replies + + default: + LOG_ERROR("Hardware operation %d not yet implemented! FIXME", p.type); + break; + } + } + + return false; } -int32_t RemoteHardwareModule::runOnce() -{ - if (moduleConfig.remote_hardware.enabled && watchGpios) { +int32_t RemoteHardwareModule::runOnce() { + if (moduleConfig.remote_hardware.enabled && watchGpios) { - if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) { - uint64_t curVal = digitalReads(watchGpios, availablePins); - lastWatchMsec = millis(); + if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) { + uint64_t curVal = digitalReads(watchGpios, availablePins); + lastWatchMsec = millis(); - if (curVal != previousWatch) { - previousWatch = curVal; - LOG_INFO("Broadcast GPIOS 0x%llx changed!", curVal); + if (curVal != previousWatch) { + previousWatch = curVal; + LOG_INFO("Broadcast GPIOS 0x%llx changed!", curVal); - // Something changed! Tell the world with a broadcast message - meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; - r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED; - r.gpio_value = curVal; - meshtastic_MeshPacket *p = allocDataProtobuf(r); - service->sendToMesh(p); - } - } - } else { - // No longer watching anything - stop using CPU - return disable(); + // Something changed! Tell the world with a broadcast message + meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; + r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED; + r.gpio_value = curVal; + meshtastic_MeshPacket *p = allocDataProtobuf(r); + service->sendToMesh(p); + } } + } else { + // No longer watching anything - stop using CPU + return disable(); + } - return 2000; // Poll our GPIOs every 2000ms + return 2000; // Poll our GPIOs every 2000ms } \ No newline at end of file diff --git a/src/modules/RemoteHardwareModule.h b/src/modules/RemoteHardwareModule.h index 4dc31d405..7e77949ab 100644 --- a/src/modules/RemoteHardwareModule.h +++ b/src/modules/RemoteHardwareModule.h @@ -6,42 +6,41 @@ /** * A module that provides easy low-level remote access to device hardware. */ -class RemoteHardwareModule : public ProtobufModule, private concurrency::OSThread -{ - /// The current set of GPIOs we've been asked to watch for changes - uint64_t watchGpios = 0; +class RemoteHardwareModule : public ProtobufModule, private concurrency::OSThread { + /// The current set of GPIOs we've been asked to watch for changes + uint64_t watchGpios = 0; - /// The previously read value of watched pins - uint64_t previousWatch = 0; + /// The previously read value of watched pins + uint64_t previousWatch = 0; - /// The timestamp of our last watch event (we throttle watches to 1 change every 30 seconds) - uint32_t lastWatchMsec = 0; + /// The timestamp of our last watch event (we throttle watches to 1 change every 30 seconds) + uint32_t lastWatchMsec = 0; - /// A bitmask of GPIOs that are exposed to the mesh if undefined access is not enabled - uint64_t availablePins = 0; + /// A bitmask of GPIOs that are exposed to the mesh if undefined access is not enabled + uint64_t availablePins = 0; - public: - /** Constructor - * name is for debugging output - */ - RemoteHardwareModule(); +public: + /** Constructor + * name is for debugging output + */ + RemoteHardwareModule(); - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_HardwareMessage *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_HardwareMessage *p) override; - /** - * Periodically read the gpios we have been asked to WATCH, if they have changed, - * broadcast a message with the change information. - * - * The method that will be called each time our thread gets a chance to run - * - * Returns desired period for next invocation (or RUN_SAME for no change) - */ - virtual int32_t runOnce() override; + /** + * Periodically read the gpios we have been asked to WATCH, if they have changed, + * broadcast a message with the change information. + * + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce() override; }; extern RemoteHardwareModule remoteHardwareModule; diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp index 434441d49..2add898c5 100644 --- a/src/modules/ReplyModule.cpp +++ b/src/modules/ReplyModule.cpp @@ -5,20 +5,19 @@ #include -meshtastic_MeshPacket *ReplyModule::allocReply() -{ - assert(currentRequest); // should always be !NULL +meshtastic_MeshPacket *ReplyModule::allocReply() { + assert(currentRequest); // should always be !NULL #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - auto req = *currentRequest; - auto &p = req.decoded; - // The incoming message is in p.payload - LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); + auto req = *currentRequest; + auto &p = req.decoded; + // The incoming message is in p.payload + LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); #endif - const char *replyStr = "Message Received"; - auto reply = allocDataPacket(); // Allocate a packet for sending - reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply - memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); + const char *replyStr = "Message Received"; + auto reply = allocDataPacket(); // Allocate a packet for sending + reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply + memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); - return reply; + return reply; } diff --git a/src/modules/ReplyModule.h b/src/modules/ReplyModule.h index 86d4172ed..4f6e2146a 100644 --- a/src/modules/ReplyModule.h +++ b/src/modules/ReplyModule.h @@ -4,17 +4,16 @@ /** * A simple example module that just replies with "Message received" to any message it receives. */ -class ReplyModule : public SinglePortModule -{ - public: - /** Constructor - * name is for debugging output - */ - ReplyModule() : SinglePortModule("reply", meshtastic_PortNum_REPLY_APP) {} +class ReplyModule : public SinglePortModule { +public: + /** Constructor + * name is for debugging output + */ + ReplyModule() : SinglePortModule("reply", meshtastic_PortNum_REPLY_APP) {} - protected: - /** For reply module we do all of our processing in the (normally optional) - * want_replies handling - */ - virtual meshtastic_MeshPacket *allocReply() override; +protected: + /** For reply module we do all of our processing in the (normally optional) + * want_replies handling + */ + virtual meshtastic_MeshPacket *allocReply() override; }; diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index e9e1fc786..5d9ea9a63 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -8,84 +8,76 @@ RoutingModule *routingModule; -bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *r) -{ - bool maybePKI = mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && mp.channel == 0 && !isBroadcast(mp.to); - // Beginning of logic whether to drop the packet based on Rebroadcast mode - if (mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && - (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY || - config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { - if (!maybePKI) - return false; - if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && - (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) - return false; - } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { - // Don't let licensed users to rebroadcast packets from unlicensed users - // If we know they are in-fact unlicensed - LOG_DEBUG("Packet from unlicensed user, ignoring packet"); - return false; - } +bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *r) { + bool maybePKI = mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && mp.channel == 0 && !isBroadcast(mp.to); + // Beginning of logic whether to drop the packet based on Rebroadcast mode + if (mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && + (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY || + config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { + if (!maybePKI) + return false; + if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && + (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) + return false; + } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { + // Don't let licensed users to rebroadcast packets from unlicensed users + // If we know they are in-fact unlicensed + LOG_DEBUG("Packet from unlicensed user, ignoring packet"); + return false; + } - printPacket("Routing sniffing", &mp); - router->sniffReceived(&mp, r); + printPacket("Routing sniffing", &mp); + router->sniffReceived(&mp, r); - // FIXME - move this to a non promsicious PhoneAPI module? - // Note: we are careful not to send back packets that started with the phone back to the phone - if ((isBroadcast(mp.to) || isToUs(&mp)) && (mp.from != 0)) { - printPacket("Delivering rx packet", &mp); - service->handleFromRadio(&mp); - } + // FIXME - move this to a non promsicious PhoneAPI module? + // Note: we are careful not to send back packets that started with the phone back to the phone + if ((isBroadcast(mp.to) || isToUs(&mp)) && (mp.from != 0)) { + printPacket("Delivering rx packet", &mp); + service->handleFromRadio(&mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -meshtastic_MeshPacket *RoutingModule::allocReply() -{ - assert(currentRequest); +meshtastic_MeshPacket *RoutingModule::allocReply() { + assert(currentRequest); - return NULL; + return NULL; } -void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, - bool ackWantsAck) -{ - auto p = allocAckNak(err, to, idFrom, chIndex, hopLimit); +void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, bool ackWantsAck) { + auto p = allocAckNak(err, to, idFrom, chIndex, hopLimit); - // Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably - p->want_ack = ackWantsAck; + // Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably + p->want_ack = ackWantsAck; - router->sendLocal(p); // we sometimes send directly to the local node + router->sendLocal(p); // we sometimes send directly to the local node } -uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) -{ - const int8_t hopsUsed = getHopsAway(mp); - if (hopsUsed >= 0) { - if (hopsUsed > (int32_t)(config.lora.hop_limit)) { +uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { + const int8_t hopsUsed = getHopsAway(mp); + if (hopsUsed >= 0) { + if (hopsUsed > (int32_t)(config.lora.hop_limit)) { // In event mode, we never want to send packets with more than our default 3 hops. -#if !(EVENTMODE) // This falls through to the default. - return hopsUsed; // If the request used more hops than the limit, use the same amount of hops +#if !(EVENTMODE) // This falls through to the default. + return hopsUsed; // If the request used more hops than the limit, use the same amount of hops #endif - } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { - return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different - } + } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { + return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different } - return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit + } + return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } -meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, - uint8_t hopLimit) -{ - return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); +meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { + return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); } -RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) -{ - isPromiscuous = true; +RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { + isPromiscuous = true; - // moved the RebroadcastMode logic into handleReceivedProtobuf - // LocalOnly requires either the from or to to be a known node - // knownOnly specifically requires the from to be a known node. - encryptedOk = true; + // moved the RebroadcastMode logic into handleReceivedProtobuf + // LocalOnly requires either the from or to to be a known node + // knownOnly specifically requires the from to be a known node. + encryptedOk = true; } \ No newline at end of file diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index 2ac42f447..2b0283c61 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -5,38 +5,36 @@ /** * Routing module for router control messages */ -class RoutingModule : public ProtobufModule -{ - public: - /** Constructor - * name is for debugging output - */ - RoutingModule(); +class RoutingModule : public ProtobufModule { +public: + /** Constructor + * name is for debugging output + */ + RoutingModule(); - virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, - bool ackWantsAck = false); + virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, + bool ackWantsAck = false); - meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, - uint8_t hopLimit = 0); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0); - // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response - uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); + // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response + uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); - protected: - friend class Router; +protected: + friend class Router; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *p) override; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /// Override wantPacket to say we want to see all packets, not just those for our port number - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } + /// Override wantPacket to say we want to see all packets, not just those for our port number + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } }; extern RoutingModule *routingModule; \ No newline at end of file diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 719e342b1..57f2593b1 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -49,8 +49,8 @@ #include "meshSolarApp.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ - !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) #define RX_BUFFER 256 #define TIMEOUT 250 @@ -63,68 +63,55 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || \ - defined(MUZI_BASE) -SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} +#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M5) || \ + defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) +SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL) -SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} +SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } static Print *serialPrint = &Serial1; #else -SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} +SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } static Print *serialPrint = &Serial2; #endif char serialBytes[512]; size_t serialPayloadSize; -bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) -{ - if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { - const char *warning = - "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; - LOG_ERROR(warning); +bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) { + if (config.override_console_serial_port && + !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; + LOG_ERROR(warning); #if !IS_RUNNING_TESTS - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - cn->time = getValidTime(RTCQualityFromNet); - snprintf(cn->message, sizeof(cn->message), "%s", warning); - service->sendClientNotification(cn); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), "%s", warning); + service->sendClientNotification(cn); #endif - return false; - } + return false; + } - return true; + return true; } -SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") -{ - switch (moduleConfig.serial.mode) { - case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG: - ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP; - break; - case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA: - case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO: - ourPortNum = meshtastic_PortNum_POSITION_APP; - break; - default: - ourPortNum = meshtastic_PortNum_SERIAL_APP; - // restrict to the serial channel for rx - boundChannel = Channels::serialChannel; - break; - } +SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { + switch (moduleConfig.serial.mode) { + case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG: + ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP; + break; + case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA: + case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO: + ourPortNum = meshtastic_PortNum_POSITION_APP; + break; + default: + ourPortNum = meshtastic_PortNum_SERIAL_APP; + // restrict to the serial channel for rx + boundChannel = Channels::serialChannel; + break; + } } /** @@ -132,172 +119,169 @@ SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") * * @return true if the serial connection is established, false otherwise. * - * For the serial2 port we can't really detect if any client is on the other side, so instead just look for recent messages + * For the serial2 port we can't really detect if any client is on the other side, so instead just look for recent + * messages */ -bool SerialModule::checkIsConnected() -{ - return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); -} +bool SerialModule::checkIsConnected() { return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); } -int32_t SerialModule::runOnce() -{ - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ +int32_t SerialModule::runOnce() { + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.serial.enabled = true; - // moduleConfig.serial.rxd = 35; - // moduleConfig.serial.txd = 15; - // moduleConfig.serial.override_console_serial_port = true; - // moduleConfig.serial.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; - // moduleConfig.serial.timeout = 1000; - // moduleConfig.serial.echo = 1; + // moduleConfig.serial.enabled = true; + // moduleConfig.serial.rxd = 35; + // moduleConfig.serial.txd = 15; + // moduleConfig.serial.override_console_serial_port = true; + // moduleConfig.serial.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + // moduleConfig.serial.timeout = 1000; + // moduleConfig.serial.echo = 1; - if (!moduleConfig.serial.enabled) - return disable(); + if (!moduleConfig.serial.enabled) + return disable(); - if (moduleConfig.serial.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) { - if (firstTime) { - // Interface with the serial peripheral from in here. - LOG_INFO("Init serial peripheral interface"); + if (moduleConfig.serial.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) { + if (firstTime) { + // Interface with the serial peripheral from in here. + LOG_INFO("Init serial peripheral interface"); - uint32_t baud = getBaudRate(); + uint32_t baud = getBaudRate(); - if (moduleConfig.serial.override_console_serial_port) { + if (moduleConfig.serial.override_console_serial_port) { #ifdef RP2040_SLOW_CLOCK - Serial2.flush(); - serialPrint = &Serial2; + Serial2.flush(); + serialPrint = &Serial2; #else - Serial.flush(); - serialPrint = &Serial; + Serial.flush(); + serialPrint = &Serial; #endif - // Give it a chance to flush out 💩 - delay(10); - } + // Give it a chance to flush out 💩 + delay(10); + } #if defined(CONFIG_IDF_TARGET_ESP32C6) - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { - Serial1.setRxBufferSize(RX_BUFFER); - Serial1.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); - } else { - Serial.begin(baud); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); - } + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + Serial1.setRxBufferSize(RX_BUFFER); + Serial1.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); + } else { + Serial.begin(baud); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + } #elif defined(ARCH_STM32WL) #ifndef RAK3172 - HardwareSerial *serialInstance = &Serial2; + HardwareSerial *serialInstance = &Serial2; #else - HardwareSerial *serialInstance = &Serial1; + HardwareSerial *serialInstance = &Serial1; #endif - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { - serialInstance->setTx(moduleConfig.serial.txd); - serialInstance->setRx(moduleConfig.serial.rxd); - } - serialInstance->begin(baud); - serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + serialInstance->setTx(moduleConfig.serial.txd); + serialInstance->setRx(moduleConfig.serial.rxd); + } + serialInstance->begin(baud); + serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #elif defined(ARCH_ESP32) - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { - Serial2.setRxBufferSize(RX_BUFFER); - Serial2.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); - } else { - Serial.begin(baud); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); - } -#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + Serial2.setRxBufferSize(RX_BUFFER); + Serial2.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); + } else { + Serial.begin(baud); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + } +#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ + !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 - Serial2.setFIFOSize(RX_BUFFER); - Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd); + Serial2.setFIFOSize(RX_BUFFER); + Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd); #else - Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd); + Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd); #endif - Serial2.begin(baud, SERIAL_8N1); - Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); - } else { + Serial2.begin(baud, SERIAL_8N1); + Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + } else { #ifdef RP2040_SLOW_CLOCK - Serial2.begin(baud, SERIAL_8N1); - Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + Serial2.begin(baud, SERIAL_8N1); + Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #else - Serial.begin(baud, SERIAL_8N1); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + Serial.begin(baud, SERIAL_8N1); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #endif - } + } #else - Serial.begin(baud, SERIAL_8N1); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + Serial.begin(baud, SERIAL_8N1); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #endif - serialModuleRadio = new SerialModuleRadio(); + serialModuleRadio = new SerialModuleRadio(); - firstTime = 0; + firstTime = 0; - // in API mode send rebooted sequence - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { - emitRebooted(); - } - } else { - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { - return runOncePart(); - } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) { - // in NMEA mode send out GGA every 2 seconds, Don't read from Port - if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) { - lastNmeaTime = millis(); - printGGA(outbuf, sizeof(outbuf), localPosition); - serialPrint->printf("%s", outbuf); - } - } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) { - if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) { - lastNmeaTime = millis(); - uint32_t readIndex = 0; - const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); - while (tempNodeInfo != NULL) { - if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { - printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); - serialPrint->printf("%s", outbuf); - } - tempNodeInfo = nodeDB->readNextMeshNode(readIndex); - } - } + // in API mode send rebooted sequence + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { + emitRebooted(); + } + } else { + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { + return runOncePart(); + } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) { + // in NMEA mode send out GGA every 2 seconds, Don't read from Port + if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) { + lastNmeaTime = millis(); + printGGA(outbuf, sizeof(outbuf), localPosition); + serialPrint->printf("%s", outbuf); + } + } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) { + if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) { + lastNmeaTime = millis(); + uint32_t readIndex = 0; + const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); + while (tempNodeInfo != NULL) { + if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { + printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); + serialPrint->printf("%s", outbuf); } + tempNodeInfo = nodeDB->readNextMeshNode(readIndex); + } + } + } -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) - else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { - processWXSerial(); +#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ + !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { + processWXSerial(); - } + } #if defined(HELTEC_MESH_SOLAR) - else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { - serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); - // If the parsing fails, the following parsing will be performed. - if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { - return runOncePart(serialBytes, serialPayloadSize); - } - } + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); + // If the parsing fails, the following parsing will be performed. + if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { + return runOncePart(serialBytes, serialPayloadSize); + } + } #endif - else { + else { #if defined(CONFIG_IDF_TARGET_ESP32C6) - while (Serial1.available()) { - serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + while (Serial1.available()) { + serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #else #ifndef RAK3172 - HardwareSerial *serialInstance = &Serial2; + HardwareSerial *serialInstance = &Serial2; #else - HardwareSerial *serialInstance = &Serial1; + HardwareSerial *serialInstance = &Serial1; #endif - while (serialInstance->available()) { - serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); -#endif - serialModuleRadio->sendPayload(); - } - } + while (serialInstance->available()) { + serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #endif + serialModuleRadio->sendPayload(); } - return (10); - } else { - return disable(); + } +#endif } + return (10); + } else { + return disable(); + } } /** @@ -309,21 +293,19 @@ int32_t SerialModule::runOnce() * * @throws None */ -void SerialModule::sendTelemetry(meshtastic_Telemetry m) -{ - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Telemetry_msg, &m); - p->to = NODENUM_BROADCAST; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) { - p->want_ack = true; - p->priority = meshtastic_MeshPacket_Priority_HIGH; - } else { - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - } - service->sendToMesh(p, RX_SRC_LOCAL, true); +void SerialModule::sendTelemetry(meshtastic_Telemetry m) { + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Telemetry_msg, &m); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) { + p->want_ack = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + } else { + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + } + service->sendToMesh(p, RX_SRC_LOCAL, true); } /** @@ -331,11 +313,10 @@ void SerialModule::sendTelemetry(meshtastic_Telemetry m) * * @return A pointer to the newly allocated mesh packet. */ -meshtastic_MeshPacket *SerialModuleRadio::allocReply() -{ - auto reply = allocDataPacket(); // Allocate a packet for sending +meshtastic_MeshPacket *SerialModuleRadio::allocReply() { + auto reply = allocDataPacket(); // Allocate a packet for sending - return reply; + return reply; } /** @@ -344,22 +325,21 @@ meshtastic_MeshPacket *SerialModuleRadio::allocReply() * @param dest The destination node number. * @param wantReplies Whether or not to request replies from the destination node. */ -void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies) -{ - const meshtastic_Channel *ch = (boundChannel != NULL) ? &channels.getByName(boundChannel) : NULL; - meshtastic_MeshPacket *p = allocReply(); - p->to = dest; - if (ch != NULL) { - p->channel = ch->index; - } - p->decoded.want_response = wantReplies; +void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies) { + const meshtastic_Channel *ch = (boundChannel != NULL) ? &channels.getByName(boundChannel) : NULL; + meshtastic_MeshPacket *p = allocReply(); + p->to = dest; + if (ch != NULL) { + p->channel = ch->index; + } + p->decoded.want_response = wantReplies; - p->want_ack = ACK; + p->want_ack = ACK; - p->decoded.payload.size = serialPayloadSize; // You must specify how many bytes are in the reply - memcpy(p->decoded.payload.bytes, serialBytes, p->decoded.payload.size); + p->decoded.payload.size = serialPayloadSize; // You must specify how many bytes are in the reply + memcpy(p->decoded.payload.bytes, serialBytes, p->decoded.payload.size); - service->sendToMesh(p); + service->sendToMesh(p); } /** @@ -368,66 +348,65 @@ void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies) * @param mp The received mesh packet. * @return The processed message. */ -ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) -{ - if (moduleConfig.serial.enabled) { - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { - // in API mode we don't care about stuff from radio. - return ProcessMessage::CONTINUE; - } - - auto &p = mp.decoded; - // LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", - // nodeDB->getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); - - if (isFromUs(&mp)) { - - /* - * If moduleConfig.serial.echo is true, then echo the packets that are sent out - * back to the TX of the serial interface. - */ - if (moduleConfig.serial.echo) { - - // For some reason, we get the packet back twice when we send out of the radio. - // TODO: need to find out why. - if (lastRxID != mp.id) { - lastRxID = mp.id; - // LOG_DEBUG("* * Message came this device"); - // serialPrint->println("* * Message came this device"); - serialPrint->printf("%s", p.payload.bytes); - } - } - } else { - - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT || - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) { - serialPrint->write(p.payload.bytes, p.payload.size); - } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - const char *sender = (node && node->has_user) ? node->user.short_name : "???"; - serialPrint->println(); - serialPrint->printf("%s: %s", sender, p.payload.bytes); - serialPrint->println(); - } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA || - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && - HAS_GPS) { - // Decode the Payload some more - meshtastic_Position scratch; - meshtastic_Position *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) { - decoded = &scratch; - } - // send position packet as WPL to the serial port - printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); - serialPrint->printf("%s", outbuf); - } - } - } +ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) { + if (moduleConfig.serial.enabled) { + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { + // in API mode we don't care about stuff from radio. + return ProcessMessage::CONTINUE; } - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + + auto &p = mp.decoded; + // LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", + // nodeDB->getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); + + if (isFromUs(&mp)) { + + /* + * If moduleConfig.serial.echo is true, then echo the packets that are sent out + * back to the TX of the serial interface. + */ + if (moduleConfig.serial.echo) { + + // For some reason, we get the packet back twice when we send out of the radio. + // TODO: need to find out why. + if (lastRxID != mp.id) { + lastRxID = mp.id; + // LOG_DEBUG("* * Message came this device"); + // serialPrint->println("* * Message came this device"); + serialPrint->printf("%s", p.payload.bytes); + } + } + } else { + + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT || + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) { + serialPrint->write(p.payload.bytes, p.payload.size); + } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + const char *sender = (node && node->has_user) ? node->user.short_name : "???"; + serialPrint->println(); + serialPrint->printf("%s: %s", sender, p.payload.bytes); + serialPrint->println(); + } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA || + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && + HAS_GPS) { + // Decode the Payload some more + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + } + // send position packet as WPL to the serial port + printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); + serialPrint->printf("%s", outbuf); + } + } + } + } + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** @@ -435,46 +414,45 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp * * @return uint32_t The baud rate of the serial module. */ -uint32_t SerialModule::getBaudRate() -{ - if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110) { - return 110; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300) { - return 300; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600) { - return 600; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200) { - return 1200; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400) { - return 2400; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800) { - return 4800; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600) { - return 9600; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200) { - return 19200; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400) { - return 38400; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600) { - return 57600; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200) { - return 115200; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400) { - return 230400; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800) { - return 460800; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000) { - return 576000; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600) { - return 921600; - } - return BAUD; +uint32_t SerialModule::getBaudRate() { + if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110) { + return 110; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300) { + return 300; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600) { + return 600; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200) { + return 1200; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400) { + return 2400; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800) { + return 4800; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600) { + return 9600; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200) { + return 19200; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400) { + return 38400; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600) { + return 57600; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200) { + return 115200; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400) { + return 230400; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800) { + return 460800; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000) { + return 576000; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600) { + return 921600; + } + return BAUD; } // Add this structure to help with parsing WindGust = 24.4 serial lines. struct ParsedLine { - char name[64]; - char value[128]; + char name[64]; + char value[128]; }; /** @@ -482,50 +460,49 @@ struct ParsedLine { * @param line Input line to parse * @return ParsedLine containing name and value, or empty strings if parse failed */ -ParsedLine parseLine(const char *line) -{ - ParsedLine result = {"", ""}; - - // Find equals sign - const char *equals = strchr(line, '='); - if (!equals) { - return result; - } - - // Extract name by copying substring - char nameBuf[64]; // Temporary buffer - size_t nameLen = equals - line; - if (nameLen >= sizeof(nameBuf)) { - nameLen = sizeof(nameBuf) - 1; - } - strncpy(nameBuf, line, nameLen); - nameBuf[nameLen] = '\0'; - - // Trim whitespace from name - char *nameStart = nameBuf; - while (*nameStart && isspace(*nameStart)) - nameStart++; - char *nameEnd = nameStart + strlen(nameStart) - 1; - while (nameEnd > nameStart && isspace(*nameEnd)) - *nameEnd-- = '\0'; - - // Copy trimmed name - strncpy(result.name, nameStart, sizeof(result.name) - 1); - result.name[sizeof(result.name) - 1] = '\0'; - - // Extract value part (after equals) - const char *valueStart = equals + 1; - while (*valueStart && isspace(*valueStart)) - valueStart++; - strncpy(result.value, valueStart, sizeof(result.value) - 1); - result.value[sizeof(result.value) - 1] = '\0'; - - // Trim trailing whitespace from value - char *valueEnd = result.value + strlen(result.value) - 1; - while (valueEnd > result.value && isspace(*valueEnd)) - *valueEnd-- = '\0'; +ParsedLine parseLine(const char *line) { + ParsedLine result = {"", ""}; + // Find equals sign + const char *equals = strchr(line, '='); + if (!equals) { return result; + } + + // Extract name by copying substring + char nameBuf[64]; // Temporary buffer + size_t nameLen = equals - line; + if (nameLen >= sizeof(nameBuf)) { + nameLen = sizeof(nameBuf) - 1; + } + strncpy(nameBuf, line, nameLen); + nameBuf[nameLen] = '\0'; + + // Trim whitespace from name + char *nameStart = nameBuf; + while (*nameStart && isspace(*nameStart)) + nameStart++; + char *nameEnd = nameStart + strlen(nameStart) - 1; + while (nameEnd > nameStart && isspace(*nameEnd)) + *nameEnd-- = '\0'; + + // Copy trimmed name + strncpy(result.name, nameStart, sizeof(result.name) - 1); + result.name[sizeof(result.name) - 1] = '\0'; + + // Extract value part (after equals) + const char *valueStart = equals + 1; + while (*valueStart && isspace(*valueStart)) + valueStart++; + strncpy(result.value, valueStart, sizeof(result.value) - 1); + result.value[sizeof(result.value) - 1] = '\0'; + + // Trim trailing whitespace from value + char *valueEnd = result.value + strlen(result.value) - 1; + while (valueEnd > result.value && isspace(*valueEnd)) + *valueEnd-- = '\0'; + + return result; } /** @@ -534,188 +511,185 @@ ParsedLine parseLine(const char *line) * * @return void */ -void SerialModule::processWXSerial() -{ -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ - !defined(ARCH_STM32WL) && !defined(MUZI_BASE) - static unsigned int lastAveraged = 0; - static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. - static double dir_sum_sin = 0; - static double dir_sum_cos = 0; - static float velSum = 0; - static float gust = 0; - static float lull = -1; - static int velCount = 0; - static int dirCount = 0; - static char windDir[4] = "xxx"; // Assuming windDir is 3 characters long + null terminator - static char windVel[5] = "xx.x"; // Assuming windVel is 4 characters long + null terminator - static char windGust[5] = "xx.x"; // Assuming windGust is 4 characters long + null terminator - static char batVoltage[5] = "0.0V"; - static char capVoltage[5] = "0.0V"; - static char temperature[5] = "00.0"; - static float batVoltageF = 0; - static float capVoltageF = 0; - static float temperatureF = 0; +void SerialModule::processWXSerial() { +#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && \ + !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && \ + !defined(MUZI_BASE) + static unsigned int lastAveraged = 0; + static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. + static double dir_sum_sin = 0; + static double dir_sum_cos = 0; + static float velSum = 0; + static float gust = 0; + static float lull = -1; + static int velCount = 0; + static int dirCount = 0; + static char windDir[4] = "xxx"; // Assuming windDir is 3 characters long + null terminator + static char windVel[5] = "xx.x"; // Assuming windVel is 4 characters long + null terminator + static char windGust[5] = "xx.x"; // Assuming windGust is 4 characters long + null terminator + static char batVoltage[5] = "0.0V"; + static char capVoltage[5] = "0.0V"; + static char temperature[5] = "00.0"; + static float batVoltageF = 0; + static float capVoltageF = 0; + static float temperatureF = 0; - static char rainStr[] = "5780860000"; - static int rainSum = 0; - static float rain = 0; - bool gotwind = false; + static char rainStr[] = "5780860000"; + static int rainSum = 0; + static float rain = 0; + bool gotwind = false; - while (Serial2.available()) { - // clear serialBytes buffer - memset(serialBytes, '\0', sizeof(serialBytes)); - // memset(formattedString, '\0', sizeof(formattedString)); - serialPayloadSize = Serial2.readBytes(serialBytes, 512); - // check for a strings we care about - // example output of serial data fields from the WS85 - // WindDir = 79 - // WindSpeed = 0.5 - // WindGust = 0.6 - // GXTS04Temp = 24.4 - // Temperature = 23.4 // WS80 + while (Serial2.available()) { + // clear serialBytes buffer + memset(serialBytes, '\0', sizeof(serialBytes)); + // memset(formattedString, '\0', sizeof(formattedString)); + serialPayloadSize = Serial2.readBytes(serialBytes, 512); + // check for a strings we care about + // example output of serial data fields from the WS85 + // WindDir = 79 + // WindSpeed = 0.5 + // WindGust = 0.6 + // GXTS04Temp = 24.4 + // Temperature = 23.4 // WS80 - // RainIntSum = 0 - // Rain = 0.0 - if (serialPayloadSize > 0) { - // Define variables for line processing - int lineStart = 0; - int lineEnd = -1; + // RainIntSum = 0 + // Rain = 0.0 + if (serialPayloadSize > 0) { + // Define variables for line processing + int lineStart = 0; + int lineEnd = -1; - // Process each byte in the received data - for (size_t i = 0; i < serialPayloadSize; i++) { - // go until we hit the end of line and then process the line - if (serialBytes[i] == '\n') { - lineEnd = i; - // Extract the current line - char line[meshtastic_Constants_DATA_PAYLOAD_LEN]; - memset(line, '\0', sizeof(line)); - if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) { - memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); + // Process each byte in the received data + for (size_t i = 0; i < serialPayloadSize; i++) { + // go until we hit the end of line and then process the line + if (serialBytes[i] == '\n') { + lineEnd = i; + // Extract the current line + char line[meshtastic_Constants_DATA_PAYLOAD_LEN]; + memset(line, '\0', sizeof(line)); + if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) { + memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); - ParsedLine parsed = parseLine(line); - if (strlen(parsed.name) > 0) { - if (strcmp(parsed.name, "WindDir") == 0) { - strlcpy(windDir, parsed.value, sizeof(windDir)); - double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); - dir_sum_sin += sin(radians); - dir_sum_cos += cos(radians); - dirCount++; - gotwind = true; - } else if (strcmp(parsed.name, "WindSpeed") == 0) { - strlcpy(windVel, parsed.value, sizeof(windVel)); - float newv = strtof(windVel, nullptr); - velSum += newv; - velCount++; - if (newv < lull || lull == -1) { - lull = newv; - } - gotwind = true; - } else if (strcmp(parsed.name, "WindGust") == 0) { - strlcpy(windGust, parsed.value, sizeof(windGust)); - float newg = strtof(windGust, nullptr); - if (newg > gust) { - gust = newg; - } - gotwind = true; - } else if (strcmp(parsed.name, "BatVoltage") == 0) { - strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); - batVoltageF = strtof(batVoltage, nullptr); - break; // last possible data we want so break - } else if (strcmp(parsed.name, "CapVoltage") == 0) { - strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); - capVoltageF = strtof(capVoltage, nullptr); - } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { - strlcpy(temperature, parsed.value, sizeof(temperature)); - temperatureF = strtof(temperature, nullptr); - } else if (strcmp(parsed.name, "RainIntSum") == 0) { - strlcpy(rainStr, parsed.value, sizeof(rainStr)); - rainSum = int(strtof(rainStr, nullptr)); - } else if (strcmp(parsed.name, "Rain") == 0) { - strlcpy(rainStr, parsed.value, sizeof(rainStr)); - rain = strtof(rainStr, nullptr); - } - } - - // Update lineStart for the next line - lineStart = lineEnd + 1; - } + ParsedLine parsed = parseLine(line); + if (strlen(parsed.name) > 0) { + if (strcmp(parsed.name, "WindDir") == 0) { + strlcpy(windDir, parsed.value, sizeof(windDir)); + double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); + dir_sum_sin += sin(radians); + dir_sum_cos += cos(radians); + dirCount++; + gotwind = true; + } else if (strcmp(parsed.name, "WindSpeed") == 0) { + strlcpy(windVel, parsed.value, sizeof(windVel)); + float newv = strtof(windVel, nullptr); + velSum += newv; + velCount++; + if (newv < lull || lull == -1) { + lull = newv; } + gotwind = true; + } else if (strcmp(parsed.name, "WindGust") == 0) { + strlcpy(windGust, parsed.value, sizeof(windGust)); + float newg = strtof(windGust, nullptr); + if (newg > gust) { + gust = newg; + } + gotwind = true; + } else if (strcmp(parsed.name, "BatVoltage") == 0) { + strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); + batVoltageF = strtof(batVoltage, nullptr); + break; // last possible data we want so break + } else if (strcmp(parsed.name, "CapVoltage") == 0) { + strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); + capVoltageF = strtof(capVoltage, nullptr); + } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { + strlcpy(temperature, parsed.value, sizeof(temperature)); + temperatureF = strtof(temperature, nullptr); + } else if (strcmp(parsed.name, "RainIntSum") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); + rainSum = int(strtof(rainStr, nullptr)); + } else if (strcmp(parsed.name, "Rain") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); + rain = strtof(rainStr, nullptr); + } } - break; - // clear the input buffer - while (Serial2.available() > 0) { - Serial2.read(); // Read and discard the bytes in the input buffer - } + + // Update lineStart for the next line + lineStart = lineEnd + 1; + } } + } + break; + // clear the input buffer + while (Serial2.available() > 0) { + Serial2.read(); // Read and discard the bytes in the input buffer + } } - if (gotwind) { + } + if (gotwind) { - LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), - strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum); + LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr), + batVoltageF, capVoltageF, temperatureF, rain, rainSum); + } + if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { + // calculate averages and send to the mesh + float velAvg = 1.0 * velSum / velCount; + + double avgSin = dir_sum_sin / dirCount; + double avgCos = dir_sum_cos / dirCount; + + double avgRadians = atan2(avgSin, avgCos); + float dirAvg = GeoCoord::toDegrees(avgRadians); + + if (dirAvg < 0) { + dirAvg += 360.0; } - if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { - // calculate averages and send to the mesh - float velAvg = 1.0 * velSum / velCount; + lastAveraged = millis(); - double avgSin = dir_sum_sin / dirCount; - double avgCos = dir_sum_cos / dirCount; + // make a telemetry packet with the data + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_environment_metrics_tag; - double avgRadians = atan2(avgSin, avgCos); - float dirAvg = GeoCoord::toDegrees(avgRadians); + m.variant.environment_metrics.wind_speed = velAvg; + m.variant.environment_metrics.has_wind_speed = true; - if (dirAvg < 0) { - dirAvg += 360.0; - } - lastAveraged = millis(); + m.variant.environment_metrics.wind_direction = dirAvg; + m.variant.environment_metrics.has_wind_direction = true; - // make a telemetry packet with the data - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_environment_metrics_tag; + m.variant.environment_metrics.temperature = temperatureF; + m.variant.environment_metrics.has_temperature = true; - m.variant.environment_metrics.wind_speed = velAvg; - m.variant.environment_metrics.has_wind_speed = true; + m.variant.environment_metrics.voltage = capVoltageF > batVoltageF ? capVoltageF : batVoltageF; // send the larger of the two voltage values. + m.variant.environment_metrics.has_voltage = true; - m.variant.environment_metrics.wind_direction = dirAvg; - m.variant.environment_metrics.has_wind_direction = true; + m.variant.environment_metrics.wind_gust = gust; + m.variant.environment_metrics.has_wind_gust = true; - m.variant.environment_metrics.temperature = temperatureF; - m.variant.environment_metrics.has_temperature = true; + m.variant.environment_metrics.rainfall_24h = rainSum; + m.variant.environment_metrics.has_rainfall_24h = true; - m.variant.environment_metrics.voltage = - capVoltageF > batVoltageF ? capVoltageF : batVoltageF; // send the larger of the two voltage values. - m.variant.environment_metrics.has_voltage = true; + // not sure if this value is actually the 1hr sum so needs to do some testing + m.variant.environment_metrics.rainfall_1h = rain; + m.variant.environment_metrics.has_rainfall_1h = true; - m.variant.environment_metrics.wind_gust = gust; - m.variant.environment_metrics.has_wind_gust = true; + if (lull == -1) + lull = 0; + m.variant.environment_metrics.wind_lull = lull; + m.variant.environment_metrics.has_wind_lull = true; - m.variant.environment_metrics.rainfall_24h = rainSum; - m.variant.environment_metrics.has_rainfall_24h = true; + LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f", m.variant.environment_metrics.wind_speed, + m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust, + m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature); - // not sure if this value is actually the 1hr sum so needs to do some testing - m.variant.environment_metrics.rainfall_1h = rain; - m.variant.environment_metrics.has_rainfall_1h = true; + sendTelemetry(m); - if (lull == -1) - lull = 0; - m.variant.environment_metrics.wind_lull = lull; - m.variant.environment_metrics.has_wind_lull = true; - - LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f", - m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction, - m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust, - m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature); - - sendTelemetry(m); - - // reset counters and gust/lull - velSum = velCount = dirCount = 0; - dir_sum_sin = dir_sum_cos = 0; - gust = 0; - lull = -1; - } + // reset counters and gust/lull + velSum = velCount = dirCount = 0; + dir_sum_sin = dir_sum_cos = 0; + gust = 0; + lull = -1; + } #endif - return; + return; } #endif \ No newline at end of file diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index dbe4f75db..7dbd0e3c2 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -8,30 +8,29 @@ #include #include -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ - !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) -class SerialModule : public StreamAPI, private concurrency::OSThread -{ - bool firstTime = 1; - unsigned long lastNmeaTime = millis(); - char outbuf[90] = ""; +class SerialModule : public StreamAPI, private concurrency::OSThread { + bool firstTime = 1; + unsigned long lastNmeaTime = millis(); + char outbuf[90] = ""; - public: - SerialModule(); +public: + SerialModule(); - static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); + static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; - private: - uint32_t getBaudRate(); - void sendTelemetry(meshtastic_Telemetry m); - void processWXSerial(); +private: + uint32_t getBaudRate(); + void sendTelemetry(meshtastic_Telemetry m); + void processWXSerial(); }; extern SerialModule *serialModule; @@ -40,41 +39,39 @@ extern SerialModule *serialModule; * Radio interface for SerialModule * */ -class SerialModuleRadio : public MeshModule -{ - uint32_t lastRxID = 0; - char outbuf[90] = ""; +class SerialModuleRadio : public MeshModule { + uint32_t lastRxID = 0; + char outbuf[90] = ""; - public: - SerialModuleRadio(); +public: + SerialModuleRadio(); - /** - * Send our payload into the mesh - */ - void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - protected: - virtual meshtastic_MeshPacket *allocReply() override; +protected: + virtual meshtastic_MeshPacket *allocReply() override; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - meshtastic_PortNum ourPortNum; + meshtastic_PortNum ourPortNum; - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } - meshtastic_MeshPacket *allocDataPacket() - { - // Update our local node info with our position (even if we don't decide to update anyone else) - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = ourPortNum; + meshtastic_MeshPacket *allocDataPacket() { + // Update our local node info with our position (even if we don't decide to update anyone else) + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = ourPortNum; - return p; - } + return p; + } }; extern SerialModuleRadio *serialModuleRadio; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index 8738c16ca..4ea48d097 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -9,107 +9,104 @@ It reflects charging, charged, discharging, and Bluetooth connection states usin */ StatusLEDModule *statusLEDModule; -StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") -{ - bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); - powerStatusObserver.observe(&powerStatus->onNewStatus); +StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { + bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); + powerStatusObserver.observe(&powerStatus->onNewStatus); } -int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) -{ - switch (arg->getStatusType()) { - case STATUS_TYPE_POWER: { - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; - if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { - power_state = charging; - if (powerStatus->getBatteryChargePercent() >= 100) { - power_state = charged; - } - } else { - if (powerStatus->getBatteryChargePercent() > 5) { - power_state = discharging; - } else { - power_state = critical; - } - } - break; +int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) { + switch (arg->getStatusType()) { + case STATUS_TYPE_POWER: { + meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; + if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { + power_state = charging; + if (powerStatus->getBatteryChargePercent() >= 100) { + power_state = charged; + } + } else { + if (powerStatus->getBatteryChargePercent() > 5) { + power_state = discharging; + } else { + power_state = critical; + } + } + break; + } + case STATUS_TYPE_BLUETOOTH: { + meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg; + switch (bluetoothStatus->getConnectionState()) { + case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { + ble_state = unpaired; + PAIRING_LED_starttime = millis(); + break; + } + case meshtastic::BluetoothStatus::ConnectionState::PAIRING: { + ble_state = pairing; + PAIRING_LED_starttime = millis(); + break; + } + case meshtastic::BluetoothStatus::ConnectionState::CONNECTED: { + ble_state = connected; + PAIRING_LED_starttime = millis(); + break; + } } - case STATUS_TYPE_BLUETOOTH: { - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg; - switch (bluetoothStatus->getConnectionState()) { - case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { - ble_state = unpaired; - PAIRING_LED_starttime = millis(); - break; - } - case meshtastic::BluetoothStatus::ConnectionState::PAIRING: { - ble_state = pairing; - PAIRING_LED_starttime = millis(); - break; - } - case meshtastic::BluetoothStatus::ConnectionState::CONNECTED: { - ble_state = connected; - PAIRING_LED_starttime = millis(); - break; - } - } - break; - } - } - return 0; + break; + } + } + return 0; }; -int32_t StatusLEDModule::runOnce() -{ - my_interval = 1000; - - if (power_state == charging) { - CHARGE_LED_state = !CHARGE_LED_state; - } else if (power_state == charged) { - CHARGE_LED_state = LED_STATE_ON; - } else if (power_state == critical) { - if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { - doing_fast_blink = true; - POWER_LED_starttime = millis(); - } - if (doing_fast_blink) { - PAIRING_LED_state = LED_STATE_OFF; - CHARGE_LED_state = !CHARGE_LED_state; - my_interval = 250; - if (POWER_LED_starttime + 2000 < millis()) { - doing_fast_blink = false; - } - } else { - CHARGE_LED_state = LED_STATE_OFF; - } +int32_t StatusLEDModule::runOnce() { + my_interval = 1000; + if (power_state == charging) { + CHARGE_LED_state = !CHARGE_LED_state; + } else if (power_state == charged) { + CHARGE_LED_state = LED_STATE_ON; + } else if (power_state == critical) { + if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { + doing_fast_blink = true; + POWER_LED_starttime = millis(); + } + if (doing_fast_blink) { + PAIRING_LED_state = LED_STATE_OFF; + CHARGE_LED_state = !CHARGE_LED_state; + my_interval = 250; + if (POWER_LED_starttime + 2000 < millis()) { + doing_fast_blink = false; + } } else { - CHARGE_LED_state = LED_STATE_OFF; + CHARGE_LED_state = LED_STATE_OFF; } - if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { - PAIRING_LED_state = LED_STATE_OFF; - } else if (ble_state == unpaired) { - if (slowTrack) { - PAIRING_LED_state = !PAIRING_LED_state; - slowTrack = false; - } else { - slowTrack = true; - } - } else if (ble_state == pairing) { - PAIRING_LED_state = !PAIRING_LED_state; + } else { + CHARGE_LED_state = LED_STATE_OFF; + } + + if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { + PAIRING_LED_state = LED_STATE_OFF; + } else if (ble_state == unpaired) { + if (slowTrack) { + PAIRING_LED_state = !PAIRING_LED_state; + slowTrack = false; } else { - PAIRING_LED_state = LED_STATE_ON; + slowTrack = true; } + } else if (ble_state == pairing) { + PAIRING_LED_state = !PAIRING_LED_state; + } else { + PAIRING_LED_state = LED_STATE_ON; + } #ifdef LED_CHARGE - digitalWrite(LED_CHARGE, CHARGE_LED_state); + digitalWrite(LED_CHARGE, CHARGE_LED_state); #endif - // digitalWrite(green_LED_PIN, LED_STATE_OFF); + // digitalWrite(green_LED_PIN, LED_STATE_OFF); #ifdef LED_PAIRING - digitalWrite(LED_PAIRING, PAIRING_LED_state); + digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif - return (my_interval); + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index d90ff718c..63594657c 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -8,39 +8,38 @@ #include #include -class StatusLEDModule : private concurrency::OSThread -{ - bool slowTrack = false; +class StatusLEDModule : private concurrency::OSThread { + bool slowTrack = false; - public: - StatusLEDModule(); +public: + StatusLEDModule(); - int handleStatusUpdate(const meshtastic::Status *); + int handleStatusUpdate(const meshtastic::Status *); - protected: - unsigned int my_interval = 1000; // interval in millisconds - virtual int32_t runOnce() override; +protected: + unsigned int my_interval = 1000; // interval in millisconds + virtual int32_t runOnce() override; - CallbackObserver bluetoothStatusObserver = - CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); - CallbackObserver powerStatusObserver = - CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver bluetoothStatusObserver = + CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver powerStatusObserver = + CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); - private: - bool CHARGE_LED_state = LED_STATE_OFF; - bool PAIRING_LED_state = LED_STATE_OFF; +private: + bool CHARGE_LED_state = LED_STATE_OFF; + bool PAIRING_LED_state = LED_STATE_OFF; - uint32_t PAIRING_LED_starttime = 0; - uint32_t POWER_LED_starttime = 0; - bool doing_fast_blink = false; + uint32_t PAIRING_LED_starttime = 0; + uint32_t POWER_LED_starttime = 0; + bool doing_fast_blink = false; - enum PowerState { discharging, charging, charged, critical }; + enum PowerState { discharging, charging, charged, critical }; - PowerState power_state = discharging; + PowerState power_state = discharging; - enum BLEState { unpaired, pairing, connected }; + enum BLEState { unpaired, pairing, connected }; - BLEState ble_state = unpaired; + BLEState ble_state = unpaired; }; extern StatusLEDModule *statusLEDModule; diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index b8a710bf5..01aff66ae 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -2,12 +2,13 @@ * @file StoreForwardModule.cpp * @brief Implementation of the StoreForwardModule class. * - * This file contains the implementation of the StoreForwardModule class, which is responsible for managing the store and forward - * functionality of the Meshtastic device. The class provides methods for sending and receiving messages, as well as managing the - * message history queue. It also initializes and manages the data structures used for storing the message history. + * This file contains the implementation of the StoreForwardModule class, which is responsible for managing the store + * and forward functionality of the Meshtastic device. The class provides methods for sending and receiving messages, as + * well as managing the message history queue. It also initializes and manages the data structures used for storing the + * message history. * - * The StoreForwardModule class is used by the MeshService class to provide store and forward functionality to the Meshtastic - * device. + * The StoreForwardModule class is used by the MeshService class to provide store and forward functionality to the + * Meshtastic device. * * @author Jm Casler * @date [Insert Date] @@ -30,65 +31,59 @@ StoreForwardModule *storeForwardModule; -int32_t StoreForwardModule::runOnce() -{ +int32_t StoreForwardModule::runOnce() { #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) - if (moduleConfig.store_forward.enabled && is_server) { - // Send out the message queue. - if (this->busy) { - // Only send packets if the channel is less than 25% utilized and until historyReturnMax - if (airTime->isTxAllowedChannelUtil(true) && this->requestCount < this->historyReturnMax) { - if (!storeForwardModule->sendPayload(this->busyTo, this->last_time)) { - this->requestCount = 0; - this->busy = false; - } - } - } else if (this->heartbeat && (!Throttle::isWithinTimespanMs(lastHeartbeat, heartbeatInterval * 1000)) && - airTime->isTxAllowedChannelUtil(true)) { - lastHeartbeat = millis(); - LOG_INFO("Send heartbeat"); - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT; - sf.which_variant = meshtastic_StoreAndForward_heartbeat_tag; - sf.variant.heartbeat.period = heartbeatInterval; - sf.variant.heartbeat.secondary = 0; // TODO we always have one primary router for now - storeForwardModule->sendMessage(NODENUM_BROADCAST, sf); + if (moduleConfig.store_forward.enabled && is_server) { + // Send out the message queue. + if (this->busy) { + // Only send packets if the channel is less than 25% utilized and until historyReturnMax + if (airTime->isTxAllowedChannelUtil(true) && this->requestCount < this->historyReturnMax) { + if (!storeForwardModule->sendPayload(this->busyTo, this->last_time)) { + this->requestCount = 0; + this->busy = false; } - return (this->packetTimeMax); + } + } else if (this->heartbeat && (!Throttle::isWithinTimespanMs(lastHeartbeat, heartbeatInterval * 1000)) && airTime->isTxAllowedChannelUtil(true)) { + lastHeartbeat = millis(); + LOG_INFO("Send heartbeat"); + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT; + sf.which_variant = meshtastic_StoreAndForward_heartbeat_tag; + sf.variant.heartbeat.period = heartbeatInterval; + sf.variant.heartbeat.secondary = 0; // TODO we always have one primary router for now + storeForwardModule->sendMessage(NODENUM_BROADCAST, sf); } + return (this->packetTimeMax); + } #endif - return disable(); + return disable(); } /** * Populates the PSRAM with data to be sent later when a device is out of range. */ -void StoreForwardModule::populatePSRAM() -{ - /* - For PSRAM usage, see: - https://learn.upesy.com/en/programmation/psram.html#psram-tab - */ +void StoreForwardModule::populatePSRAM() { + /* + For PSRAM usage, see: + https://learn.upesy.com/en/programmation/psram.html#psram-tab + */ - LOG_DEBUG("Before PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), - memGet.getPsramSize()); + LOG_DEBUG("Before PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), memGet.getPsramSize()); - /* Use a maximum of 3/4 the available PSRAM unless otherwise specified. - Note: This needs to be done after every thing that would use PSRAM - */ - uint32_t numberOfPackets = - (this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct))); - this->records = numberOfPackets; + /* Use a maximum of 3/4 the available PSRAM unless otherwise specified. + Note: This needs to be done after every thing that would use PSRAM + */ + uint32_t numberOfPackets = (this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct))); + this->records = numberOfPackets; #if defined(ARCH_ESP32) - this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); + this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); #elif defined(ARCH_PORTDUINO) - this->packetHistory = static_cast(calloc(numberOfPackets, sizeof(PacketHistoryStruct))); + this->packetHistory = static_cast(calloc(numberOfPackets, sizeof(PacketHistoryStruct))); #endif - LOG_DEBUG("After PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), - memGet.getPsramSize()); - LOG_DEBUG("numberOfPackets for packetHistory - %u", numberOfPackets); + LOG_DEBUG("After PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), memGet.getPsramSize()); + LOG_DEBUG("numberOfPackets for packetHistory - %u", numberOfPackets); } /** @@ -97,28 +92,27 @@ void StoreForwardModule::populatePSRAM() * @param sAgo The number of seconds ago from which to start sending messages. * @param to The recipient ID to send the messages to. */ -void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) -{ - this->last_time = getTime() < secAgo ? 0 : getTime() - secAgo; - uint32_t queueSize = getNumAvailablePackets(to, last_time); - if (queueSize > this->historyReturnMax) - queueSize = this->historyReturnMax; +void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) { + this->last_time = getTime() < secAgo ? 0 : getTime() - secAgo; + uint32_t queueSize = getNumAvailablePackets(to, last_time); + if (queueSize > this->historyReturnMax) + queueSize = this->historyReturnMax; - if (queueSize) { - LOG_INFO("S&F - Send %u message(s)", queueSize); - this->busy = true; // runOnce() will pickup the next steps once busy = true. - this->busyTo = to; - } else { - LOG_INFO("S&F - No history"); - } - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY; - sf.which_variant = meshtastic_StoreAndForward_history_tag; - sf.variant.history.history_messages = queueSize; - sf.variant.history.window = secAgo * 1000; - sf.variant.history.last_request = lastRequest[to]; - storeForwardModule->sendMessage(to, sf); - setIntervalFromNow(this->packetTimeMax); // Delay start of sending payloads + if (queueSize) { + LOG_INFO("S&F - Send %u message(s)", queueSize); + this->busy = true; // runOnce() will pickup the next steps once busy = true. + this->busyTo = to; + } else { + LOG_INFO("S&F - No history"); + } + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY; + sf.which_variant = meshtastic_StoreAndForward_history_tag; + sf.variant.history.history_messages = queueSize; + sf.variant.history.window = secAgo * 1000; + sf.variant.history.last_request = lastRequest[to]; + storeForwardModule->sendMessage(to, sf); + setIntervalFromNow(this->packetTimeMax); // Delay start of sending payloads } /** @@ -128,22 +122,20 @@ void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) * @param last_time The relative time to start counting messages from. * @return The number of available packets in the message history. */ -uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time) -{ - uint32_t count = 0; - if (lastRequest.find(dest) == lastRequest.end()) { - lastRequest.emplace(dest, 0); +uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time) { + uint32_t count = 0; + if (lastRequest.find(dest) == lastRequest.end()) { + lastRequest.emplace(dest, 0); + } + for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { + if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { + // Client is only interested in packets not from itself and only in broadcast packets or packets towards it. + if (this->packetHistory[i].from != dest && (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { + count++; + } } - for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { - if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { - // Client is only interested in packets not from itself and only in broadcast packets or packets towards it. - if (this->packetHistory[i].from != dest && - (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { - count++; - } - } - } - return count; + } + return count; } /** @@ -151,30 +143,29 @@ uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_ * * @return A pointer to the allocated mesh packet or nullptr if none is available. */ -meshtastic_MeshPacket *StoreForwardModule::getForPhone() -{ - if (moduleConfig.store_forward.enabled && is_server) { - NodeNum to = nodeDB->getNodeNum(); - if (!this->busy) { - // Get number of packets we're going to send in this loop - uint32_t histSize = getNumAvailablePackets(to, 0); // No time limit - if (histSize) { - this->busy = true; - this->busyTo = to; - } else { - return nullptr; - } - } - - // We're busy with sending to us until no payload is available anymore - if (this->busy && this->busyTo == to) { - meshtastic_MeshPacket *p = preparePayload(to, 0, true); // No time limit - if (!p) // No more messages to send - this->busy = false; - return p; - } +meshtastic_MeshPacket *StoreForwardModule::getForPhone() { + if (moduleConfig.store_forward.enabled && is_server) { + NodeNum to = nodeDB->getNodeNum(); + if (!this->busy) { + // Get number of packets we're going to send in this loop + uint32_t histSize = getNumAvailablePackets(to, 0); // No time limit + if (histSize) { + this->busy = true; + this->busyTo = to; + } else { + return nullptr; + } } - return nullptr; + + // We're busy with sending to us until no payload is available anymore + if (this->busy && this->busyTo == to) { + meshtastic_MeshPacket *p = preparePayload(to, 0, true); // No time limit + if (!p) // No more messages to send + this->busy = false; + return p; + } + } + return nullptr; } /** @@ -182,35 +173,34 @@ meshtastic_MeshPacket *StoreForwardModule::getForPhone() * * @param mp The mesh packet to add to the history buffer. */ -void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) -{ - const auto &p = mp.decoded; +void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) { + const auto &p = mp.decoded; - if (this->packetHistoryTotalCount == this->records) { - LOG_WARN("S&F - PSRAM Full. Starting overwrite"); - this->packetHistoryTotalCount = 0; - for (auto &i : lastRequest) { - i.second = 0; // Clear the last request index for each client device - } + if (this->packetHistoryTotalCount == this->records) { + LOG_WARN("S&F - PSRAM Full. Starting overwrite"); + this->packetHistoryTotalCount = 0; + for (auto &i : lastRequest) { + i.second = 0; // Clear the last request index for each client device } + } - this->packetHistory[this->packetHistoryTotalCount].time = getTime(); - this->packetHistory[this->packetHistoryTotalCount].to = mp.to; - this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel; - this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp); - this->packetHistory[this->packetHistoryTotalCount].id = mp.id; - this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; - this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; - this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; - this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; - this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; - this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start; - this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; - this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; - this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; - memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + this->packetHistory[this->packetHistoryTotalCount].time = getTime(); + this->packetHistory[this->packetHistoryTotalCount].to = mp.to; + this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel; + this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp); + this->packetHistory[this->packetHistoryTotalCount].id = mp.id; + this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; + this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; + this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; + this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; + this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; + this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start; + this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; + this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; + this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; + memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); - this->packetHistoryTotalCount++; + this->packetHistoryTotalCount++; } /** @@ -220,16 +210,15 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) * @param last_time The relative time to start sending messages from. * @return True if a packet was successfully sent, false otherwise. */ -bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time) -{ - meshtastic_MeshPacket *p = preparePayload(dest, last_time); - if (p) { - LOG_INFO("Send S&F Payload"); - service->sendToMesh(p); - this->requestCount++; - return true; - } - return false; +bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time) { + meshtastic_MeshPacket *p = preparePayload(dest, last_time); + if (p) { + LOG_INFO("Send S&F Payload"); + service->sendToMesh(p); + this->requestCount++; + return true; + } + return false; } /** @@ -239,62 +228,60 @@ bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time) * @param last_time The relative time to start sending messages from. * @return A pointer to the prepared mesh packet or nullptr if none is available. */ -meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t last_time, bool local) -{ - for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { - if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { - /* Copy the messages that were received by the server in the last msAgo - to the packetHistoryTXQueue structure. - Client not interested in packets from itself and only in broadcast packets or packets towards it. */ - if (this->packetHistory[i].from != dest && - (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { +meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t last_time, bool local) { + for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { + if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { + /* Copy the messages that were received by the server in the last msAgo + to the packetHistoryTXQueue structure. + Client not interested in packets from itself and only in broadcast packets or packets towards it. */ + if (this->packetHistory[i].from != dest && (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { - meshtastic_MeshPacket *p = allocDataPacket(); + meshtastic_MeshPacket *p = allocDataPacket(); - p->to = local ? this->packetHistory[i].to : dest; // PhoneAPI can handle original `to` - p->from = this->packetHistory[i].from; - p->id = this->packetHistory[i].id; - p->channel = this->packetHistory[i].channel; - p->decoded.reply_id = this->packetHistory[i].reply_id; - p->rx_time = this->packetHistory[i].time; - p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; - p->rx_rssi = this->packetHistory[i].rx_rssi; - p->rx_snr = this->packetHistory[i].rx_snr; - p->hop_start = this->packetHistory[i].hop_start; - p->hop_limit = this->packetHistory[i].hop_limit; - p->via_mqtt = this->packetHistory[i].via_mqtt; - p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism; + p->to = local ? this->packetHistory[i].to : dest; // PhoneAPI can handle original `to` + p->from = this->packetHistory[i].from; + p->id = this->packetHistory[i].id; + p->channel = this->packetHistory[i].channel; + p->decoded.reply_id = this->packetHistory[i].reply_id; + p->rx_time = this->packetHistory[i].time; + p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; + p->rx_rssi = this->packetHistory[i].rx_rssi; + p->rx_snr = this->packetHistory[i].rx_snr; + p->hop_start = this->packetHistory[i].hop_start; + p->hop_limit = this->packetHistory[i].hop_limit; + p->via_mqtt = this->packetHistory[i].via_mqtt; + p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism; - // Let's assume that if the server received the S&F request that the client is in range. - // TODO: Make this configurable. - p->want_ack = false; + // Let's assume that if the server received the S&F request that the client is in range. + // TODO: Make this configurable. + p->want_ack = false; - if (local) { // PhoneAPI gets normal TEXT_MESSAGE_APP - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - memcpy(p->decoded.payload.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); - p->decoded.payload.size = this->packetHistory[i].payload_size; - } else { - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.which_variant = meshtastic_StoreAndForward_text_tag; - sf.variant.text.size = this->packetHistory[i].payload_size; - memcpy(sf.variant.text.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); - if (this->packetHistory[i].to == NODENUM_BROADCAST) { - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST; - } else { - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT; - } + if (local) { // PhoneAPI gets normal TEXT_MESSAGE_APP + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + memcpy(p->decoded.payload.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); + p->decoded.payload.size = this->packetHistory[i].payload_size; + } else { + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.which_variant = meshtastic_StoreAndForward_text_tag; + sf.variant.text.size = this->packetHistory[i].payload_size; + memcpy(sf.variant.text.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); + if (this->packetHistory[i].to == NODENUM_BROADCAST) { + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST; + } else { + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT; + } - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), - &meshtastic_StoreAndForward_msg, &sf); - } - - lastRequest[dest] = i + 1; // Update the last request index for the client device - - return p; - } + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_StoreAndForward_msg, &sf); } + + lastRequest[dest] = i + 1; // Update the last request index for the client device + + return p; + } } - return nullptr; + } + return nullptr; } /** @@ -303,20 +290,19 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t * @param dest The destination node number. * @param payload The message payload to be sent. */ -void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload) -{ - meshtastic_MeshPacket *p = allocDataProtobuf(payload); +void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload) { + meshtastic_MeshPacket *p = allocDataProtobuf(payload); - p->to = dest; + p->to = dest; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // Let's assume that if the server received the S&F request that the client is in range. - // TODO: Make this configurable. - p->want_ack = false; - p->decoded.want_response = false; + // Let's assume that if the server received the S&F request that the client is in range. + // TODO: Make this configurable. + p->want_ack = false; + p->decoded.want_response = false; - service->sendToMesh(p); + service->sendToMesh(p); } /** @@ -325,12 +311,11 @@ void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForw * @param dest The destination node number. * @param rr The store-and-forward request/response message to send. */ -void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr) -{ - // Craft an empty response, save some bytes in flash - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = rr; - storeForwardModule->sendMessage(dest, sf); +void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr) { + // Craft an empty response, save some bytes in flash + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.rr = rr; + storeForwardModule->sendMessage(dest, sf); } /** @@ -339,27 +324,26 @@ void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_Re * @param dest The destination node number. * @param want_response True if the original message requested a response, false otherwise. */ -void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response) -{ - meshtastic_MeshPacket *pr = allocDataPacket(); - pr->to = dest; - pr->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - pr->want_ack = false; - pr->decoded.want_response = false; - pr->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - const char *str; - if (this->busy) { - str = "S&F - Busy. Try again shortly."; - } else { - str = "S&F not permitted on the public channel."; - } - LOG_WARN("%s", str); - memcpy(pr->decoded.payload.bytes, str, strlen(str)); - pr->decoded.payload.size = strlen(str); - if (want_response) { - ignoreRequest = true; // This text message counts as response. - } - service->sendToMesh(pr); +void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response) { + meshtastic_MeshPacket *pr = allocDataPacket(); + pr->to = dest; + pr->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + pr->want_ack = false; + pr->decoded.want_response = false; + pr->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + const char *str; + if (this->busy) { + str = "S&F - Busy. Try again shortly."; + } else { + str = "S&F not permitted on the public channel."; + } + LOG_WARN("%s", str); + memcpy(pr->decoded.payload.bytes, str, strlen(str)); + pr->decoded.payload.size = strlen(str); + if (want_response) { + ignoreRequest = true; // This text message counts as response. + } + service->sendToMesh(pr); } /** @@ -367,24 +351,23 @@ void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response) * * @param to The node ID to send the statistics to. */ -void StoreForwardModule::statsSend(uint32_t to) -{ - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; +void StoreForwardModule::statsSend(uint32_t to) { + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS; - sf.which_variant = meshtastic_StoreAndForward_stats_tag; - sf.variant.stats.messages_total = this->records; - sf.variant.stats.messages_saved = this->packetHistoryTotalCount; - sf.variant.stats.messages_max = this->records; - sf.variant.stats.up_time = millis() / 1000; - sf.variant.stats.requests = this->requests; - sf.variant.stats.requests_history = this->requests_history; - sf.variant.stats.heartbeat = this->heartbeat; - sf.variant.stats.return_max = this->historyReturnMax; - sf.variant.stats.return_window = this->historyReturnWindow; + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS; + sf.which_variant = meshtastic_StoreAndForward_stats_tag; + sf.variant.stats.messages_total = this->records; + sf.variant.stats.messages_saved = this->packetHistoryTotalCount; + sf.variant.stats.messages_max = this->records; + sf.variant.stats.up_time = millis() / 1000; + sf.variant.stats.requests = this->requests; + sf.variant.stats.requests_history = this->requests_history; + sf.variant.stats.heartbeat = this->heartbeat; + sf.variant.stats.return_max = this->historyReturnMax; + sf.variant.stats.return_window = this->historyReturnWindow; - LOG_DEBUG("Send S&F Stats"); - storeForwardModule->sendMessage(to, sf); + LOG_DEBUG("Send S&F Stats"); + storeForwardModule->sendMessage(to, sf); } /** @@ -393,46 +376,45 @@ void StoreForwardModule::statsSend(uint32_t to) * @param mp The received mesh packet. * @return A `ProcessMessage` indicating whether the packet was successfully handled. */ -ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &mp) -{ +ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) - if (moduleConfig.store_forward.enabled) { + if (moduleConfig.store_forward.enabled) { - if ((mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) && is_server) { - auto &p = mp.decoded; - if (isToUs(&mp) && (p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 0x00)) { - LOG_DEBUG("Legacy Request to send"); + if ((mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) && is_server) { + auto &p = mp.decoded; + if (isToUs(&mp) && (p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 0x00)) { + LOG_DEBUG("Legacy Request to send"); - // Send the last 60 minutes of messages. - if (this->busy || channels.isDefaultChannel(mp.channel)) { - sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); - } else { - storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); - } - } else { - storeForwardModule->historyAdd(mp); - LOG_INFO("S&F stored. Message history contains %u records now", this->packetHistoryTotalCount); - } - } else if (!isFromUs(&mp) && mp.decoded.portnum == meshtastic_PortNum_STORE_FORWARD_APP) { - auto &p = mp.decoded; - meshtastic_StoreAndForward scratch; - meshtastic_StoreAndForward *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_StoreAndForward_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding proto module!"); - // if we can't decode it, nobody can process it! - return ProcessMessage::STOP; - } - return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; - } - } // all others are irrelevant - } + // Send the last 60 minutes of messages. + if (this->busy || channels.isDefaultChannel(mp.channel)) { + sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); + } else { + storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); + } + } else { + storeForwardModule->historyAdd(mp); + LOG_INFO("S&F stored. Message history contains %u records now", this->packetHistoryTotalCount); + } + } else if (!isFromUs(&mp) && mp.decoded.portnum == meshtastic_PortNum_STORE_FORWARD_APP) { + auto &p = mp.decoded; + meshtastic_StoreAndForward scratch; + meshtastic_StoreAndForward *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_StoreAndForward_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding proto module!"); + // if we can't decode it, nobody can process it! + return ProcessMessage::STOP; + } + return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; + } + } // all others are irrelevant + } #endif - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** @@ -442,197 +424,194 @@ ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &m * @param p A pointer to the StoreAndForward object. * @return True if the message was successfully handled, false otherwise. */ -bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p) -{ - if (!moduleConfig.store_forward.enabled) { - // If this module is not enabled in any capacity, don't handle the packet, and allow other modules to consume - return false; +bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p) { + if (!moduleConfig.store_forward.enabled) { + // If this module is not enabled in any capacity, don't handle the packet, and allow other modules to consume + return false; + } + + requests++; + + switch (p->rr) { + case meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR: + case meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT: + if (is_server) { + // stop sending stuff, the client wants to abort or has another error + if ((this->busy) && (this->busyTo == getFrom(&mp))) { + LOG_ERROR("Client in ERROR or ABORT requested"); + this->requestCount = 0; + this->busy = false; + } } + break; - requests++; - - switch (p->rr) { - case meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR: - case meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT: - if (is_server) { - // stop sending stuff, the client wants to abort or has another error - if ((this->busy) && (this->busyTo == getFrom(&mp))) { - LOG_ERROR("Client in ERROR or ABORT requested"); - this->requestCount = 0; - this->busy = false; - } + case meshtastic_StoreAndForward_RequestResponse_CLIENT_HISTORY: + if (is_server) { + requests_history++; + LOG_INFO("Client Request to send HISTORY"); + // Send the last 60 minutes of messages. + if (this->busy || channels.isDefaultChannel(mp.channel)) { + sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); + } else { + if ((p->which_variant == meshtastic_StoreAndForward_history_tag) && (p->variant.history.window > 0)) { + // window is in minutes + storeForwardModule->historySend(p->variant.history.window * 60, getFrom(&mp)); + } else { + storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); // defaults to 4 hours } - break; - - case meshtastic_StoreAndForward_RequestResponse_CLIENT_HISTORY: - if (is_server) { - requests_history++; - LOG_INFO("Client Request to send HISTORY"); - // Send the last 60 minutes of messages. - if (this->busy || channels.isDefaultChannel(mp.channel)) { - sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); - } else { - if ((p->which_variant == meshtastic_StoreAndForward_history_tag) && (p->variant.history.window > 0)) { - // window is in minutes - storeForwardModule->historySend(p->variant.history.window * 60, getFrom(&mp)); - } else { - storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); // defaults to 4 hours - } - } - } - break; - - case meshtastic_StoreAndForward_RequestResponse_CLIENT_PING: - if (is_server) { - // respond with a ROUTER PONG - storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG); - } - break; - - case meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG: - if (is_server) { - // NodeDB is already updated - } - break; - - case meshtastic_StoreAndForward_RequestResponse_CLIENT_STATS: - if (is_server) { - LOG_INFO("Client Request to send STATS"); - if (this->busy) { - storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY); - LOG_INFO("S&F - Busy. Try again shortly"); - } else { - storeForwardModule->statsSend(getFrom(&mp)); - } - } - break; - - case meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR: - case meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY: - if (is_client) { - LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY"); - // retry in messages_saved * packetTimeMax ms - retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax * - (meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); - } - break; - - case meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG: - // A router responded, this is equal to receiving a heartbeat - case meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT: - if (is_client) { - // register heartbeat and interval - if (p->which_variant == meshtastic_StoreAndForward_heartbeat_tag) { - heartbeatInterval = p->variant.heartbeat.period; - } - lastHeartbeat = millis(); - LOG_INFO("StoreAndForward Heartbeat received"); - } - break; - - case meshtastic_StoreAndForward_RequestResponse_ROUTER_PING: - if (is_client) { - // respond with a CLIENT PONG - storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG); - } - break; - - case meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS: - if (is_client) { - LOG_DEBUG("Router Response STATS"); - // These fields only have informational purpose on a client. Fill them to consume later. - if (p->which_variant == meshtastic_StoreAndForward_stats_tag) { - this->records = p->variant.stats.messages_max; - this->requests = p->variant.stats.requests; - this->requests_history = p->variant.stats.requests_history; - this->heartbeat = p->variant.stats.heartbeat; - this->historyReturnMax = p->variant.stats.return_max; - this->historyReturnWindow = p->variant.stats.return_window; - } - } - break; - - case meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY: - if (is_client) { - // These fields only have informational purpose on a client. Fill them to consume later. - if (p->which_variant == meshtastic_StoreAndForward_history_tag) { - this->historyReturnWindow = p->variant.history.window / 60000; - LOG_INFO("Router Response HISTORY - Sending %d messages from last %d minutes", - p->variant.history.history_messages, this->historyReturnWindow); - } - } - break; - - default: - break; // no need to do anything + } } - return false; // RoutingModule sends it to the phone + break; + + case meshtastic_StoreAndForward_RequestResponse_CLIENT_PING: + if (is_server) { + // respond with a ROUTER PONG + storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG); + } + break; + + case meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG: + if (is_server) { + // NodeDB is already updated + } + break; + + case meshtastic_StoreAndForward_RequestResponse_CLIENT_STATS: + if (is_server) { + LOG_INFO("Client Request to send STATS"); + if (this->busy) { + storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY); + LOG_INFO("S&F - Busy. Try again shortly"); + } else { + storeForwardModule->statsSend(getFrom(&mp)); + } + } + break; + + case meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR: + case meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY: + if (is_client) { + LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY"); + // retry in messages_saved * packetTimeMax ms + retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax * + (meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); + } + break; + + case meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG: + // A router responded, this is equal to receiving a heartbeat + case meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT: + if (is_client) { + // register heartbeat and interval + if (p->which_variant == meshtastic_StoreAndForward_heartbeat_tag) { + heartbeatInterval = p->variant.heartbeat.period; + } + lastHeartbeat = millis(); + LOG_INFO("StoreAndForward Heartbeat received"); + } + break; + + case meshtastic_StoreAndForward_RequestResponse_ROUTER_PING: + if (is_client) { + // respond with a CLIENT PONG + storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG); + } + break; + + case meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS: + if (is_client) { + LOG_DEBUG("Router Response STATS"); + // These fields only have informational purpose on a client. Fill them to consume later. + if (p->which_variant == meshtastic_StoreAndForward_stats_tag) { + this->records = p->variant.stats.messages_max; + this->requests = p->variant.stats.requests; + this->requests_history = p->variant.stats.requests_history; + this->heartbeat = p->variant.stats.heartbeat; + this->historyReturnMax = p->variant.stats.return_max; + this->historyReturnWindow = p->variant.stats.return_window; + } + } + break; + + case meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY: + if (is_client) { + // These fields only have informational purpose on a client. Fill them to consume later. + if (p->which_variant == meshtastic_StoreAndForward_history_tag) { + this->historyReturnWindow = p->variant.history.window / 60000; + LOG_INFO("Router Response HISTORY - Sending %d messages from last %d minutes", p->variant.history.history_messages, + this->historyReturnWindow); + } + } + break; + + default: + break; // no need to do anything + } + return false; // RoutingModule sends it to the phone } StoreForwardModule::StoreForwardModule() - : concurrency::OSThread("StoreForward"), - ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg) -{ + : concurrency::OSThread("StoreForward"), ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg) { #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) - isPromiscuous = true; // Brown chicken brown cow + isPromiscuous = true; // Brown chicken brown cow - if (StoreForward_Dev) { - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + if (StoreForward_Dev) { + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - moduleConfig.store_forward.enabled = 1; - } + moduleConfig.store_forward.enabled = 1; + } - if (moduleConfig.store_forward.enabled) { + if (moduleConfig.store_forward.enabled) { - // Router - if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) { - LOG_INFO("Init Store & Forward Module in Server mode"); - if (memGet.getPsramSize() > 0) { - if (memGet.getFreePsram() >= 1024 * 1024) { + // Router + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) { + LOG_INFO("Init Store & Forward Module in Server mode"); + if (memGet.getPsramSize() > 0) { + if (memGet.getFreePsram() >= 1024 * 1024) { - // Do the startup here + // Do the startup here - // Maximum number of records to return. - if (moduleConfig.store_forward.history_return_max) - this->historyReturnMax = moduleConfig.store_forward.history_return_max; + // Maximum number of records to return. + if (moduleConfig.store_forward.history_return_max) + this->historyReturnMax = moduleConfig.store_forward.history_return_max; - // Maximum time window for records to return (in minutes) - if (moduleConfig.store_forward.history_return_window) - this->historyReturnWindow = moduleConfig.store_forward.history_return_window; + // Maximum time window for records to return (in minutes) + if (moduleConfig.store_forward.history_return_window) + this->historyReturnWindow = moduleConfig.store_forward.history_return_window; - // Maximum number of records to store in memory - if (moduleConfig.store_forward.records) - this->records = moduleConfig.store_forward.records; + // Maximum number of records to store in memory + if (moduleConfig.store_forward.records) + this->records = moduleConfig.store_forward.records; - // send heartbeat advertising? - if (moduleConfig.store_forward.heartbeat) - this->heartbeat = moduleConfig.store_forward.heartbeat; - else - this->heartbeat = false; + // send heartbeat advertising? + if (moduleConfig.store_forward.heartbeat) + this->heartbeat = moduleConfig.store_forward.heartbeat; + else + this->heartbeat = false; - // Popupate PSRAM with our data structures. - this->populatePSRAM(); - is_server = true; - } else { - LOG_INFO("."); - LOG_INFO("S&F: not enough PSRAM free, Disable"); - } - } else { - LOG_INFO("S&F: device doesn't have PSRAM, Disable"); - } - - // Client + // Popupate PSRAM with our data structures. + this->populatePSRAM(); + is_server = true; } else { - is_client = true; - LOG_INFO("Init Store & Forward Module in Client mode"); + LOG_INFO("."); + LOG_INFO("S&F: not enough PSRAM free, Disable"); } + } else { + LOG_INFO("S&F: device doesn't have PSRAM, Disable"); + } + + // Client } else { - disable(); + is_client = true; + LOG_INFO("Init Store & Forward Module in Client mode"); } + } else { + disable(); + } #endif } diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h index 148568e1b..32e939a12 100644 --- a/src/modules/StoreForwardModule.h +++ b/src/modules/StoreForwardModule.h @@ -10,108 +10,106 @@ #include struct PacketHistoryStruct { - uint32_t time; - uint32_t to; - uint32_t from; - uint32_t id; - uint8_t channel; - uint32_t reply_id; - bool emoji; - uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; - pb_size_t payload_size; - int32_t rx_rssi; - float rx_snr; - uint8_t hop_start; - uint8_t hop_limit; - bool via_mqtt; - uint8_t transport_mechanism; + uint32_t time; + uint32_t to; + uint32_t from; + uint32_t id; + uint8_t channel; + uint32_t reply_id; + bool emoji; + uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; + pb_size_t payload_size; + int32_t rx_rssi; + float rx_snr; + uint8_t hop_start; + uint8_t hop_limit; + bool via_mqtt; + uint8_t transport_mechanism; }; -class StoreForwardModule : private concurrency::OSThread, public ProtobufModule -{ - bool busy = 0; - uint32_t busyTo = 0; - char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; +class StoreForwardModule : private concurrency::OSThread, public ProtobufModule { + bool busy = 0; + uint32_t busyTo = 0; + char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; - PacketHistoryStruct *packetHistory = 0; - uint32_t packetHistoryTotalCount = 0; - uint32_t last_time = 0; - uint32_t requestCount = 0; + PacketHistoryStruct *packetHistory = 0; + uint32_t packetHistoryTotalCount = 0; + uint32_t last_time = 0; + uint32_t requestCount = 0; - uint32_t packetTimeMax = 5000; // Interval between sending history packets as a server. + uint32_t packetTimeMax = 5000; // Interval between sending history packets as a server. - bool is_client = false; - bool is_server = false; + bool is_client = false; + bool is_server = false; - // Unordered_map stores the last request for each nodeNum (`to` field) - std::unordered_map lastRequest; + // Unordered_map stores the last request for each nodeNum (`to` field) + std::unordered_map lastRequest; - public: - StoreForwardModule(); +public: + StoreForwardModule(); - unsigned long lastHeartbeat = 0; - uint32_t heartbeatInterval = 900; + unsigned long lastHeartbeat = 0; + uint32_t heartbeatInterval = 900; - /** - Update our local reference of when we last saw that node. - @return 0 if we have never seen that node before otherwise return the last time we saw the node. - */ - void historyAdd(const meshtastic_MeshPacket &mp); - void statsSend(uint32_t to); - void historySend(uint32_t secAgo, uint32_t to); - uint32_t getNumAvailablePackets(NodeNum dest, uint32_t last_time); + /** + Update our local reference of when we last saw that node. + @return 0 if we have never seen that node before otherwise return the last time we saw the node. + */ + void historyAdd(const meshtastic_MeshPacket &mp); + void statsSend(uint32_t to); + void historySend(uint32_t secAgo, uint32_t to); + uint32_t getNumAvailablePackets(NodeNum dest, uint32_t last_time); - /** - * Send our payload into the mesh - */ - bool sendPayload(NodeNum dest = NODENUM_BROADCAST, uint32_t packetHistory_index = 0); - meshtastic_MeshPacket *preparePayload(NodeNum dest, uint32_t packetHistory_index, bool local = false); - void sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload); - void sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr); - void sendErrorTextMessage(NodeNum dest, bool want_response); - meshtastic_MeshPacket *getForPhone(); - // Returns true if we are configured as server AND we could allocate PSRAM. - bool isServer() { return is_server; } + /** + * Send our payload into the mesh + */ + bool sendPayload(NodeNum dest = NODENUM_BROADCAST, uint32_t packetHistory_index = 0); + meshtastic_MeshPacket *preparePayload(NodeNum dest, uint32_t packetHistory_index, bool local = false); + void sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload); + void sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr); + void sendErrorTextMessage(NodeNum dest, bool want_response); + meshtastic_MeshPacket *getForPhone(); + // Returns true if we are configured as server AND we could allocate PSRAM. + bool isServer() { return is_server; } - /* - -Override the wantPacket method. - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override - { - switch (p->decoded.portnum) { - case meshtastic_PortNum_TEXT_MESSAGE_APP: - case meshtastic_PortNum_STORE_FORWARD_APP: - return true; - default: - return false; - } + /* + -Override the wantPacket method. + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { + switch (p->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: + case meshtastic_PortNum_STORE_FORWARD_APP: + return true; + default: + return false; } + } - private: - void populatePSRAM(); +private: + void populatePSRAM(); - // S&F Defaults - uint32_t historyReturnMax = 25; // Return maximum of 25 records by default. - uint32_t historyReturnWindow = 240; // Return history of last 4 hours by default. - uint32_t records = 0; // Calculated - bool heartbeat = false; // No heartbeat. + // S&F Defaults + uint32_t historyReturnMax = 25; // Return maximum of 25 records by default. + uint32_t historyReturnWindow = 240; // Return history of last 4 hours by default. + uint32_t records = 0; // Calculated + bool heartbeat = false; // No heartbeat. - // stats - uint32_t requests = 0; // Number of times any client sent a request to the S&F. - uint32_t requests_history = 0; // Number of times the history was requested. + // stats + uint32_t requests = 0; // Number of times any client sent a request to the S&F. + uint32_t requests_history = 0; // Number of times the history was requested. - uint32_t retry_delay = 0; // If server is busy, retry after this delay (in ms). + uint32_t retry_delay = 0; // If server is busy, retry after this delay (in ms). - protected: - virtual int32_t runOnce() override; +protected: + virtual int32_t runOnce() override; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); }; extern StoreForwardModule *storeForwardModule; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 51543eab6..54a269631 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -18,107 +18,103 @@ SystemCommandsModule *systemCommandsModule; -SystemCommandsModule::SystemCommandsModule() -{ - if (inputBroker) - inputObserver.observe(inputBroker); +SystemCommandsModule::SystemCommandsModule() { + if (inputBroker) + inputObserver.observe(inputBroker); } -int SystemCommandsModule::handleInputEvent(const InputEvent *event) -{ - LOG_INPUT("SystemCommands Input event %u! kb %u", event->inputEvent, event->kbchar); - // System commands (all others fall through) - switch (event->kbchar) { - // Fn key symbols - case INPUT_BROKER_MSG_FN_SYMBOL_ON: - case INPUT_BROKER_MSG_FN_SYMBOL_OFF: - return 0; - // Brightness - case INPUT_BROKER_MSG_BRIGHTNESS_UP: - IF_SCREEN(screen->increaseBrightness()); - LOG_DEBUG("Increase Screen Brightness"); - return 0; - case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: - IF_SCREEN(screen->decreaseBrightness()); - LOG_DEBUG("Decrease Screen Brightness"); - return 0; - // Mute - case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled && externalNotificationModule) { - externalNotificationModule->setMute(externalNotificationModule->getMute()); - IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); screen->showSimpleBanner( - externalNotificationModule->getMute() ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) - } - return 0; - // Bluetooth - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: - config.bluetooth.enabled = !config.bluetooth.enabled; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); +int SystemCommandsModule::handleInputEvent(const InputEvent *event) { + LOG_INPUT("SystemCommands Input event %u! kb %u", event->inputEvent, event->kbchar); + // System commands (all others fall through) + switch (event->kbchar) { + // Fn key symbols + case INPUT_BROKER_MSG_FN_SYMBOL_ON: + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: + return 0; + // Brightness + case INPUT_BROKER_MSG_BRIGHTNESS_UP: + IF_SCREEN(screen->increaseBrightness()); + LOG_DEBUG("Increase Screen Brightness"); + return 0; + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: + IF_SCREEN(screen->decreaseBrightness()); + LOG_DEBUG("Decrease Screen Brightness"); + return 0; + // Mute + case INPUT_BROKER_MSG_MUTE_TOGGLE: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); + screen->showSimpleBanner(externalNotificationModule->getMute() ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + } + return 0; + // Bluetooth + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: + config.bluetooth.enabled = !config.bluetooth.enabled; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); #if defined(ARDUINO_ARCH_NRF52) - if (!config.bluetooth.enabled) { - disableBluetooth(); - IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; - } else { - IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; + } else { + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } #else - if (!config.bluetooth.enabled) { - disableBluetooth(); - IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); - } else { - IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } #endif - return 0; - case INPUT_BROKER_MSG_REBOOT: - IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); - nodeDB->saveToDisk(); + return 0; + case INPUT_BROKER_MSG_REBOOT: + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); #if HAS_SCREEN - messageStore.saveToFlash(); + messageStore.saveToFlash(); #endif - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - return true; - } + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; + } - switch (event->inputEvent) { - // GPS - case INPUT_BROKER_GPS_TOGGLE: + switch (event->inputEvent) { + // GPS + case INPUT_BROKER_GPS_TOGGLE: #if !MESHTASTIC_EXCLUDE_GPS - if (gps) { - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && - config.position.fixed_position == false) { - nodeDB->clearLocalPosition(); - nodeDB->saveToDisk(); - } - gps->toggleGpsMode(); - const char *msg = - (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; - IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);) - } -#endif - return true; - // Mesh ping - case INPUT_BROKER_SEND_PING: - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); - } else { - IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); - } - return true; - // Power control - case INPUT_BROKER_SHUTDOWN: - shutdownAtMsec = millis(); - return true; - - default: - // No other input events handled here - break; + if (gps) { + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && config.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + nodeDB->saveToDisk(); + } + gps->toggleGpsMode(); + const char *msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; + IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);) } - return false; +#endif + return true; + // Mesh ping + case INPUT_BROKER_SEND_PING: + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } + return true; + // Power control + case INPUT_BROKER_SHUTDOWN: + shutdownAtMsec = millis(); + return true; + + default: + // No other input events handled here + break; + } + return false; } diff --git a/src/modules/SystemCommandsModule.h b/src/modules/SystemCommandsModule.h index 44910f443..9810ee3a0 100644 --- a/src/modules/SystemCommandsModule.h +++ b/src/modules/SystemCommandsModule.h @@ -6,14 +6,13 @@ #include #include -class SystemCommandsModule -{ - CallbackObserver inputObserver = - CallbackObserver(this, &SystemCommandsModule::handleInputEvent); +class SystemCommandsModule { + CallbackObserver inputObserver = + CallbackObserver(this, &SystemCommandsModule::handleInputEvent); - public: - SystemCommandsModule(); - int handleInputEvent(const InputEvent *event); +public: + SystemCommandsModule(); + int handleInputEvent(const InputEvent *event); }; extern SystemCommandsModule *systemCommandsModule; \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 21a563b9d..8ea0dc97a 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -21,216 +21,206 @@ #define PMSA003I_WARMUP_MS 30000 #endif -int32_t AirQualityTelemetryModule::runOnce() -{ - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ +int32_t AirQualityTelemetryModule::runOnce() { + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.telemetry.air_quality_enabled = 1; + // moduleConfig.telemetry.air_quality_enabled = 1; - if (!(moduleConfig.telemetry.air_quality_enabled)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } + if (!(moduleConfig.telemetry.air_quality_enabled)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); + } - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = false; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = false; - if (moduleConfig.telemetry.air_quality_enabled) { - LOG_INFO("Air quality Telemetry: init"); + if (moduleConfig.telemetry.air_quality_enabled) { + LOG_INFO("Air quality Telemetry: init"); #ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); + // put the sensor to sleep on startup + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); + digitalWrite(PMSA003I_ENABLE_PIN, LOW); #endif /* PMSA003I_ENABLE_PIN */ - if (!aqi.begin_I2C()) { + if (!aqi.begin_I2C()) { #ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); + LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); + // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. + uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; + uint8_t i2caddr_asize = 1; + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); #endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif - return disable(); - } - return setStartDelay(); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = i2cScanner->fetchI2CBus(found.address); + return setStartDelay(); } +#endif return disable(); - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) - return disable(); - - switch (state) { -#ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - - return PMSA003I_WARMUP_MS; -#endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - } - -#ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -#endif /* PMSA003I_ENABLE_PIN */ - - return sendToPhoneIntervalMs; - default: - return disable(); - } + } + return setStartDelay(); } + return disable(); + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.air_quality_enabled) + return disable(); + + switch (state) { +#ifdef PMSA003I_ENABLE_PIN + case State::IDLE: + // sensor is in standby; fire it up and sleep + LOG_DEBUG("runOnce(): state = idle"); + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + + return PMSA003I_WARMUP_MS; +#endif /* PMSA003I_ENABLE_PIN */ + case State::ACTIVE: + // sensor is already warmed up; grab telemetry and send it + LOG_DEBUG("runOnce(): state = active"); + + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (service->isToPhoneQueueEmpty()) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + } + +#ifdef PMSA003I_ENABLE_PIN + // put sensor back to sleep + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +#endif /* PMSA003I_ENABLE_PIN */ + + return sendToPhoneIntervalMs; + default: + return disable(); + } + } } -bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) -{ - if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { +bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { + if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, - t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, - t->variant.air_quality_metrics.pm100_standard); + LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, t->variant.air_quality_metrics.pm10_standard, + t->variant.air_quality_metrics.pm25_standard, t->variant.air_quality_metrics.pm100_standard); - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, - t->variant.air_quality_metrics.pm100_environmental); + LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", + t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, + t->variant.air_quality_metrics.pm100_environmental); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) -{ - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; - - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; - - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, - m->variant.air_quality_metrics.pm100_environmental); - - return true; -} - -meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() -{ - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding AirQualityTelemetry module!"); - return NULL; - } - // Check for a request for air quality metrics - if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getAirQualityTelemetry(&m)) { - LOG_INFO("Air quality telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } - } - return NULL; -} - -bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) -{ - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getAirQualityTelemetry(&m)) { - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); - - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); - } - return true; - } - +bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { + if (!aqi.read(&data)) { + LOG_WARN("Skip send measurements. Could not read AQIn"); return false; + } + + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; + m->variant.air_quality_metrics.has_pm10_standard = true; + m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; + m->variant.air_quality_metrics.has_pm25_standard = true; + m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; + m->variant.air_quality_metrics.has_pm100_standard = true; + m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + + m->variant.air_quality_metrics.has_pm10_environmental = true; + m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; + m->variant.air_quality_metrics.has_pm25_environmental = true; + m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; + m->variant.air_quality_metrics.has_pm100_environmental = true; + m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + + LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, + m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); + + LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", m->variant.air_quality_metrics.pm10_environmental, + m->variant.air_quality_metrics.pm25_environmental, m->variant.air_quality_metrics.pm100_environmental); + + return true; +} + +meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() { + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding AirQualityTelemetry module!"); + return NULL; + } + // Check for a request for air quality metrics + if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getAirQualityTelemetry(&m)) { + LOG_INFO("Air quality telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } + } + return NULL; +} + +bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getAirQualityTelemetry(&m)) { + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); + + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } + return true; + } + + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee686..7da80f73f 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -8,60 +8,57 @@ #include "NodeDB.h" #include "ProtobufModule.h" -class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, - &AirQualityTelemetryModule::handleStatusUpdate); +class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &AirQualityTelemetryModule::handleStatusUpdate); - public: - AirQualityTelemetryModule() - : concurrency::OSThread("AirQualityTelemetry"), - ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) - { - lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); +public: + AirQualityTelemetryModule() + : concurrency::OSThread("AirQualityTelemetry"), + ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + lastMeasurementPacket = nullptr; + setIntervalFromNow(10 * 1000); + aqi = Adafruit_PM25AQI(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); #ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; + // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking + // a reading + state = State::IDLE; #else - state = State::ACTIVE; + state = State::ACTIVE; #endif - } + } - protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Air Quality data - @return true if it contains valid data - */ - bool getAirQualityTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); +protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Air Quality data + @return true if it contains valid data + */ + bool getAirQualityTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; +private: + enum State { + IDLE = 0, + ACTIVE = 1, + }; - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; - bool firstTime = true; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; + State state; + Adafruit_PM25AQI aqi; + PM25_AQI_Data data = {0}; + bool firstTime = true; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 066b9361d..1418b92e9 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -16,175 +16,162 @@ #define MAGIC_USB_BATTERY_LEVEL 101 -int32_t DeviceTelemetryModule::runOnce() -{ - refreshUptime(); - bool isImpoliteRole = - IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER); - if (((lastSentToMesh == 0) || - ((uptimeLastMs - lastSentToMesh) >= - Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && - config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && - moduleConfig.telemetry.device_telemetry_enabled) { - sendTelemetry(); - lastSentToMesh = uptimeLastMs; - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - if (lastSentStatsToPhone == 0 || (uptimeLastMs - lastSentStatsToPhone) >= sendStatsToPhoneIntervalMs) { - sendLocalStatsToPhone(); - lastSentStatsToPhone = uptimeLastMs; - } +int32_t DeviceTelemetryModule::runOnce() { + refreshUptime(); + bool isImpoliteRole = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER); + if (((lastSentToMesh == 0) || + ((uptimeLastMs - lastSentToMesh) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && + config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && moduleConfig.telemetry.device_telemetry_enabled) { + sendTelemetry(); + lastSentToMesh = uptimeLastMs; + } else if (service->isToPhoneQueueEmpty()) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + if (lastSentStatsToPhone == 0 || (uptimeLastMs - lastSentStatsToPhone) >= sendStatsToPhoneIntervalMs) { + sendLocalStatsToPhone(); + lastSentStatsToPhone = uptimeLastMs; } - return sendToPhoneIntervalMs; + } + return sendToPhoneIntervalMs; } -bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) -{ - if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { +bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { + if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender, - t->variant.device_metrics.air_util_tx, t->variant.device_metrics.channel_utilization, - t->variant.device_metrics.battery_level, t->variant.device_metrics.voltage); + LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender, + t->variant.device_metrics.air_util_tx, t->variant.device_metrics.channel_utilization, t->variant.device_metrics.battery_level, + t->variant.device_metrics.voltage); #endif - nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); - } - return false; // Let others look at this message also if they want + nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); + } + return false; // Let others look at this message also if they want } -meshtastic_MeshPacket *DeviceTelemetryModule::allocReply() -{ - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding DeviceTelemetry module!"); - return NULL; - } - // Check for a request for device metrics - if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - LOG_INFO("Device telemetry reply to request"); - return allocDataProtobuf(getDeviceTelemetry()); - } else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) { - LOG_INFO("Device telemetry reply w/ LocalStats to request"); - return allocDataProtobuf(getLocalStatsTelemetry()); - } - } - return NULL; -} - -meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry() -{ - meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; - t.which_variant = meshtastic_Telemetry_device_metrics_tag; - t.time = getTime(); - t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero; - t.variant.device_metrics.has_air_util_tx = true; - t.variant.device_metrics.has_battery_level = true; - t.variant.device_metrics.has_channel_utilization = true; - t.variant.device_metrics.has_voltage = true; - t.variant.device_metrics.has_uptime_seconds = true; - - t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent(); - t.variant.device_metrics.battery_level = (!powerStatus->getHasBattery() || powerStatus->getIsCharging()) - ? MAGIC_USB_BATTERY_LEVEL - : powerStatus->getBatteryChargePercent(); - t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent(); - t.variant.device_metrics.voltage = powerStatus->getBatteryVoltageMv() / 1000.0; - t.variant.device_metrics.uptime_seconds = getUptimeSeconds(); - - return t; -} - -meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() -{ - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.which_variant = meshtastic_Telemetry_local_stats_tag; - telemetry.variant.local_stats = meshtastic_LocalStats_init_zero; - telemetry.time = getTime(); - telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds(); - telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent(); - telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent(); - telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; - telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); - if (RadioLibInterface::instance) { - telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood; - telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad; - telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad; - telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay; - telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop; - } -#ifdef ARCH_PORTDUINO - if (SimRadio::instance) { - telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; - telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; - telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; - telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay; - telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop; - } -#else - telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize(); - telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap(); -#endif - if (router) { - telemetry.variant.local_stats.num_rx_dupe = router->rxDupe; - telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled; - } - - LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", - telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization, - telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes, - telemetry.variant.local_stats.num_total_nodes); - - LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx, - telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad); - - return telemetry; -} - -void DeviceTelemetryModule::sendLocalStatsToPhone() -{ - meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry()); - p->to = NODENUM_BROADCAST; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - - service->sendToPhone(p); -} - -bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) -{ - meshtastic_Telemetry telemetry = getDeviceTelemetry(); - LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i", - telemetry.variant.device_metrics.air_util_tx, telemetry.variant.device_metrics.channel_utilization, - telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage, - telemetry.variant.device_metrics.uptime_seconds); - - DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); - DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); - - p->to = dest; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - - nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); +meshtastic_MeshPacket *DeviceTelemetryModule::allocReply() { + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + LOG_ERROR("Error decoding DeviceTelemetry module!"); + return NULL; } - return true; + // Check for a request for device metrics + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + LOG_INFO("Device telemetry reply to request"); + return allocDataProtobuf(getDeviceTelemetry()); + } else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) { + LOG_INFO("Device telemetry reply w/ LocalStats to request"); + return allocDataProtobuf(getLocalStatsTelemetry()); + } + } + return NULL; +} + +meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry() { + meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; + t.which_variant = meshtastic_Telemetry_device_metrics_tag; + t.time = getTime(); + t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero; + t.variant.device_metrics.has_air_util_tx = true; + t.variant.device_metrics.has_battery_level = true; + t.variant.device_metrics.has_channel_utilization = true; + t.variant.device_metrics.has_voltage = true; + t.variant.device_metrics.has_uptime_seconds = true; + + t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent(); + t.variant.device_metrics.battery_level = + (!powerStatus->getHasBattery() || powerStatus->getIsCharging()) ? MAGIC_USB_BATTERY_LEVEL : powerStatus->getBatteryChargePercent(); + t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent(); + t.variant.device_metrics.voltage = powerStatus->getBatteryVoltageMv() / 1000.0; + t.variant.device_metrics.uptime_seconds = getUptimeSeconds(); + + return t; +} + +meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.which_variant = meshtastic_Telemetry_local_stats_tag; + telemetry.variant.local_stats = meshtastic_LocalStats_init_zero; + telemetry.time = getTime(); + telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds(); + telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent(); + telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent(); + telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; + telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); + if (RadioLibInterface::instance) { + telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood; + telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad; + telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad; + telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay; + telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop; + } +#ifdef ARCH_PORTDUINO + if (SimRadio::instance) { + telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; + telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; + telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; + telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay; + telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop; + } +#else + telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize(); + telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap(); +#endif + if (router) { + telemetry.variant.local_stats.num_rx_dupe = router->rxDupe; + telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled; + } + + LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", + telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization, telemetry.variant.local_stats.air_util_tx, + telemetry.variant.local_stats.num_online_nodes, telemetry.variant.local_stats.num_total_nodes); + + LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx, + telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad); + + return telemetry; +} + +void DeviceTelemetryModule::sendLocalStatsToPhone() { + meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry()); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + + service->sendToPhone(p); +} + +bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { + meshtastic_Telemetry telemetry = getDeviceTelemetry(); + LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i", telemetry.variant.device_metrics.air_util_tx, + telemetry.variant.device_metrics.channel_utilization, telemetry.variant.device_metrics.battery_level, + telemetry.variant.device_metrics.voltage, telemetry.variant.device_metrics.uptime_seconds); + + DEBUG_HEAP_BEFORE; + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); + + p->to = dest; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + + nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } + return true; } \ No newline at end of file diff --git a/src/modules/Telemetry/DeviceTelemetry.h b/src/modules/Telemetry/DeviceTelemetry.h index a1d55a596..58fdd8f7e 100644 --- a/src/modules/Telemetry/DeviceTelemetry.h +++ b/src/modules/Telemetry/DeviceTelemetry.h @@ -5,61 +5,57 @@ #include #include -class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); +class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); - public: - DeviceTelemetryModule() - : concurrency::OSThread("DeviceTelemetry"), - ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) - { - uptimeWrapCount = 0; - uptimeLastMs = millis(); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent - } - virtual bool wantUIFrame() { return false; } +public: + DeviceTelemetryModule() + : concurrency::OSThread("DeviceTelemetry"), ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + uptimeWrapCount = 0; + uptimeLastMs = millis(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + } + virtual bool wantUIFrame() { return false; } - protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual meshtastic_MeshPacket *allocReply() override; - virtual int32_t runOnce() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool phoneOnly = false); +protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual meshtastic_MeshPacket *allocReply() override; + virtual int32_t runOnce() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool phoneOnly = false); - /** - * Get the uptime in seconds - * Loses some accuracy after 49 days, but that's fine - */ - uint32_t getUptimeSeconds() { return (0xFFFFFFFF / 1000) * uptimeWrapCount + (uptimeLastMs / 1000); } + /** + * Get the uptime in seconds + * Loses some accuracy after 49 days, but that's fine + */ + uint32_t getUptimeSeconds() { return (0xFFFFFFFF / 1000) * uptimeWrapCount + (uptimeLastMs / 1000); } - private: - meshtastic_Telemetry getDeviceTelemetry(); - meshtastic_Telemetry getLocalStatsTelemetry(); +private: + meshtastic_Telemetry getDeviceTelemetry(); + meshtastic_Telemetry getLocalStatsTelemetry(); - void sendLocalStatsToPhone(); - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes - uint32_t lastSentStatsToPhone = 0; - uint32_t lastSentToMesh = 0; + void sendLocalStatsToPhone(); + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes + uint32_t lastSentStatsToPhone = 0; + uint32_t lastSentToMesh = 0; - void refreshUptime() - { - auto now = millis(); - // If we wrapped around (~49 days), increment the wrap count - if (now < uptimeLastMs) - uptimeWrapCount++; + void refreshUptime() { + auto now = millis(); + // If we wrapped around (~49 days), increment the wrap count + if (now < uptimeLastMs) + uptimeWrapCount++; - uptimeLastMs = now; - } + uptimeLastMs = now; + } - uint32_t uptimeWrapCount; - uint32_t uptimeLastMs; + uint32_t uptimeWrapCount; + uint32_t uptimeLastMs; }; \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 41062662b..85fc2ade2 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -28,10 +28,8 @@ #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" -namespace graphics -{ -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, - bool show_date); +namespace graphics { +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); } #if __has_include() #include "Sensor/AHT10.h" @@ -148,575 +146,556 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c static std::forward_list sensors; -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) { + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); #if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } } -void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) -{ - if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { - return; - } - LOG_INFO("Environment Telemetry adding I2C devices..."); +void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { + if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { + return; + } + LOG_INFO("Environment Telemetry adding I2C devices..."); - // order by priority of metrics/values (low top, high bottom) + // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #ifdef T1000X_SENSOR_EN - // Not a real I2C device - addSensor(i2cScanner, ScanI2C::DeviceType::NONE); + // Not a real I2C device + addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #else #ifdef SENSECAP_INDICATOR - // Not a real I2C device, uses UART - addSensor(i2cScanner, ScanI2C::DeviceType::NONE); + // Not a real I2C device, uses UART + addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #endif - addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); - addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); + addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); + addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); #endif #endif #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); + addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); + addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); + addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BMP_085); + addSensor(i2cScanner, ScanI2C::DeviceType::BMP_085); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BME_280); + addSensor(i2cScanner, ScanI2C::DeviceType::BME_280); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); + addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); + addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BMP_280); + addSensor(i2cScanner, ScanI2C::DeviceType::BMP_280); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::DPS310); + addSensor(i2cScanner, ScanI2C::DeviceType::DPS310); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); + addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); + addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); + addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); + addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); #endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 - addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); + addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::VEML7700); + addSensor(i2cScanner, ScanI2C::DeviceType::VEML7700); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::TSL2591); + addSensor(i2cScanner, ScanI2C::DeviceType::TSL2591); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); + addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); + addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); + addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); + addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::PCT2075); + addSensor(i2cScanner, ScanI2C::DeviceType::PCT2075); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::TSL2561); + addSensor(i2cScanner, ScanI2C::DeviceType::TSL2561); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::NAU7802); + addSensor(i2cScanner, ScanI2C::DeviceType::NAU7802); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); + addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); #endif #endif } -int32_t EnvironmentTelemetryModule::runOnce() -{ - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, - default_telemetry_broadcast_interval_secs); - LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); - doDeepSleep(nightyNightMs, true, false); - } +int32_t EnvironmentTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } - uint32_t result = UINT32_MAX; - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + uint32_t result = UINT32_MAX; + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.telemetry.environment_measurement_enabled = 1; - // moduleConfig.telemetry.environment_screen_enabled = 1; - // moduleConfig.telemetry.environment_update_interval = 15; + // moduleConfig.telemetry.environment_measurement_enabled = 1; + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.environment_update_interval = 15; - if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || - ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } + if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || + ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); + } - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = 0; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = 0; - if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { - LOG_INFO("Environment Telemetry: init"); + if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { + LOG_INFO("Environment Telemetry: init"); - // check if we have at least one sensor - if (!sensors.empty()) { - result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } + // check if we have at least one sensor + if (!sensors.empty()) { + result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } #ifdef T1000X_SENSOR_EN #elif !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL - if (ina219Sensor.hasSensor()) - result = ina219Sensor.runOnce(); - if (ina260Sensor.hasSensor()) - result = ina260Sensor.runOnce(); - if (ina3221Sensor.hasSensor()) - result = ina3221Sensor.runOnce(); - if (max17048Sensor.hasSensor()) - result = max17048Sensor.runOnce(); - // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the - // sensormap here. + if (ina219Sensor.hasSensor()) + result = ina219Sensor.runOnce(); + if (ina260Sensor.hasSensor()) + result = ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor()) + result = ina3221Sensor.runOnce(); + if (max17048Sensor.hasSensor()) + result = max17048Sensor.runOnce(); + // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the + // sensormap here. #ifdef HAS_RAKPROT - result = rak9154Sensor.runOnce(); + result = rak9154Sensor.runOnce(); #endif #endif - } - // it's possible to have this module enabled, only for displaying values on the screen. - // therefore, we should only enable the sensor loop if measurement is also enabled - return result == UINT32_MAX ? disable() : setStartDelay(); - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { - return disable(); - } - - for (TelemetrySensor *sensor : sensors) { - uint32_t delay = sensor->runOnce(); - if (delay < result) { - result = delay; - } - } - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.environment_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - lastSentToPhone = millis(); - } } - return min(sendToPhoneIntervalMs, result); + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { + return disable(); + } + + for (TelemetrySensor *sensor : sensors) { + uint32_t delay = sensor->runOnce(); + if (delay < result) { + result = delay; + } + } + + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + } + return min(sendToPhoneIntervalMs, result); } -bool EnvironmentTelemetryModule::wantUIFrame() -{ - return moduleConfig.telemetry.environment_screen_enabled; -} +bool EnvironmentTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.environment_screen_enabled; } #if HAS_SCREEN -void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // === Setup display === - display->clear(); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - int line = 1; +void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; - // === Set Title - const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; + // === Set Title + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === Row spacing setup === - const int rowHeight = FONT_HEIGHT_SMALL - 4; - int currentY = graphics::getTextPositions(display)[line++]; + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; - // === Show "No Telemetry" if no data available === - if (!lastMeasurementPacket) { - display->drawString(x, currentY, "No Telemetry"); - return; + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // Decode the telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + const auto &m = telemetry.variant.environment_metrics; + + // Check if any telemetry field has valid data + bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || m.current != 0 || + m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; + + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_temperature) { + String tempStr = moduleConfig.telemetry.environment_display_fahrenheit + ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" + : "Tmp: " + String(m.temperature, 1) + "°C"; + entries.push_back(tempStr); + } + if (m.has_relative_humidity) + entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); + if (m.barometric_pressure != 0) + entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + const char *bannerMsg = nullptr; // Default: no banner + + if (m.iaq <= 25) + aqi += " (Excellent)"; + else if (m.iaq <= 50) + aqi += " (Good)"; + else if (m.iaq <= 100) + aqi += " (Moderate)"; + else if (m.iaq <= 150) + aqi += " (Poor)"; + else if (m.iaq <= 200) { + aqi += " (Unhealthy)"; + bannerMsg = "Unhealthy IAQ"; + } else if (m.iaq <= 300) { + aqi += " (Very Unhealthy)"; + bannerMsg = "Very Unhealthy IAQ"; + } else { + aqi += " (Hazardous)"; + bannerMsg = "Hazardous IAQ"; } - // Decode the telemetry message from the latest received packet - const meshtastic_Data &p = lastMeasurementPacket->decoded; - meshtastic_Telemetry telemetry; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { - display->drawString(x, currentY, "No Telemetry"); - return; + entries.push_back(aqi); + + // === IAQ alert logic === + static uint32_t lastAlertTime = 0; + uint32_t now = millis(); + + bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); + bool isCooldownOver = (now - lastAlertTime > 60000); + + if (isOwnTelemetry && bannerMsg && isCooldownOver) { + LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); + screen->showSimpleBanner(bannerMsg, 3000); + + // Only buzz if IAQ is over 200 + if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); + } + + lastAlertTime = now; + } + } + if (m.voltage != 0 || m.current != 0) + entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); + if (m.lux != 0) + entries.push_back("Light: " + String(m.lux, 0) + "lx"); + if (m.white_lux != 0) + entries.push_back("White: " + String(m.white_lux, 0) + "lx"); + if (m.weight != 0) + entries.push_back("Weight: " + String(m.weight, 0) + "kg"); + if (m.distance != 0) + entries.push_back("Level: " + String(m.distance, 0) + "mm"); + if (m.radiation != 0) + entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); } - const auto &m = telemetry.variant.environment_metrics; - - // Check if any telemetry field has valid data - bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || - m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; - - if (!hasAny) { - display->drawString(x, currentY, "No Telemetry"); - return; - } - - // === First line: Show sender name + time since received (left), and first metric (right) === - const char *sender = getSenderShortName(*lastMeasurementPacket); - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - String agoStr = (agoSecs > 864000) ? "?" - : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" - : (agoSecs > 60) ? String(agoSecs / 60) + "m" - : String(agoSecs) + "s"; - - String leftStr = String(sender) + " (" + agoStr + ")"; - display->drawString(x, currentY, leftStr); // Left side: who and when - - // === Collect sensor readings as label strings (no icons) === - std::vector entries; - - if (m.has_temperature) { - String tempStr = moduleConfig.telemetry.environment_display_fahrenheit - ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" - : "Tmp: " + String(m.temperature, 1) + "°C"; - entries.push_back(tempStr); - } - if (m.has_relative_humidity) - entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); - if (m.barometric_pressure != 0) - entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); - if (m.iaq != 0) { - String aqi = "IAQ: " + String(m.iaq); - const char *bannerMsg = nullptr; // Default: no banner - - if (m.iaq <= 25) - aqi += " (Excellent)"; - else if (m.iaq <= 50) - aqi += " (Good)"; - else if (m.iaq <= 100) - aqi += " (Moderate)"; - else if (m.iaq <= 150) - aqi += " (Poor)"; - else if (m.iaq <= 200) { - aqi += " (Unhealthy)"; - bannerMsg = "Unhealthy IAQ"; - } else if (m.iaq <= 300) { - aqi += " (Very Unhealthy)"; - bannerMsg = "Very Unhealthy IAQ"; - } else { - aqi += " (Hazardous)"; - bannerMsg = "Hazardous IAQ"; - } - - entries.push_back(aqi); - - // === IAQ alert logic === - static uint32_t lastAlertTime = 0; - uint32_t now = millis(); - - bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); - bool isCooldownOver = (now - lastAlertTime > 60000); - - if (isOwnTelemetry && bannerMsg && isCooldownOver) { - LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); - screen->showSimpleBanner(bannerMsg, 3000); - - // Only buzz if IAQ is over 200 - if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { - playLongBeep(); - } - - lastAlertTime = now; - } - } - if (m.voltage != 0 || m.current != 0) - entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); - if (m.lux != 0) - entries.push_back("Light: " + String(m.lux, 0) + "lx"); - if (m.white_lux != 0) - entries.push_back("White: " + String(m.white_lux, 0) + "lx"); - if (m.weight != 0) - entries.push_back("Weight: " + String(m.weight, 0) + "kg"); - if (m.distance != 0) - entries.push_back("Level: " + String(m.distance, 0) + "mm"); - if (m.radiation != 0) - entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); - - // === Show first available metric on top-right of first line === - if (!entries.empty()) { - String valueStr = entries.front(); - int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); - display->drawString(rightX, currentY, valueStr); - entries.erase(entries.begin()); // Remove from queue - } - - // === Advance to next line for remaining telemetry entries === currentY += rowHeight; - - // === Draw remaining entries in 2-column format (left and right) === - for (size_t i = 0; i < entries.size(); i += 2) { - // Left column - display->drawString(x, currentY, entries[i]); - - // Right column if it exists - if (i + 1 < entries.size()) { - int rightX = SCREEN_WIDTH / 2; - display->drawString(rightX, currentY, entries[i + 1]); - } - - currentY += rowHeight; - } - graphics::drawCommonFooter(display, x, y); + } + graphics::drawCommonFooter(display, x, y); } #endif -bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) -{ - if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { +bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { + if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " - "temperature=%f", - sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, - t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, - t->variant.environment_metrics.temperature); - LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, - t->variant.environment_metrics.voltage, t->variant.environment_metrics.iaq, - t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, - t->variant.environment_metrics.white_lux); + LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " + "temperature=%f", + sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, + t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, + t->variant.environment_metrics.temperature); + LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, t->variant.environment_metrics.voltage, + t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, + t->variant.environment_metrics.white_lux); - LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender, - t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction, - t->variant.environment_metrics.weight); + LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender, t->variant.environment_metrics.wind_speed, + t->variant.environment_metrics.wind_direction, t->variant.environment_metrics.weight); - LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); + LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) -{ - bool valid = true; - bool hasSensor = false; - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_environment_metrics_tag; - m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; +bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) { + bool valid = true; + bool hasSensor = false; + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_environment_metrics_tag; + m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; - for (TelemetrySensor *sensor : sensors) { - valid = valid && sensor->getMetrics(m); - hasSensor = true; - } + for (TelemetrySensor *sensor : sensors) { + valid = valid && sensor->getMetrics(m); + hasSensor = true; + } #ifndef T1000X_SENSOR_EN - if (ina219Sensor.hasSensor()) { - valid = valid && ina219Sensor.getMetrics(m); - hasSensor = true; - } - if (ina260Sensor.hasSensor()) { - valid = valid && ina260Sensor.getMetrics(m); - hasSensor = true; - } - if (ina3221Sensor.hasSensor()) { - valid = valid && ina3221Sensor.getMetrics(m); - hasSensor = true; - } - if (max17048Sensor.hasSensor()) { - valid = valid && max17048Sensor.getMetrics(m); - hasSensor = true; - } + if (ina219Sensor.hasSensor()) { + valid = valid && ina219Sensor.getMetrics(m); + hasSensor = true; + } + if (ina260Sensor.hasSensor()) { + valid = valid && ina260Sensor.getMetrics(m); + hasSensor = true; + } + if (ina3221Sensor.hasSensor()) { + valid = valid && ina3221Sensor.getMetrics(m); + hasSensor = true; + } + if (max17048Sensor.hasSensor()) { + valid = valid && max17048Sensor.getMetrics(m); + hasSensor = true; + } #endif #ifdef HAS_RAKPROT - valid = valid && rak9154Sensor.getMetrics(m); - hasSensor = true; + valid = valid && rak9154Sensor.getMetrics(m); + hasSensor = true; #endif - return valid && hasSensor; + return valid && hasSensor; } -meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() -{ - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding EnvironmentTelemetry module!"); - return NULL; - } - // Check for a request for environment metrics - if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getEnvironmentTelemetry(&m)) { - LOG_INFO("Environment telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } +meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() { + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding EnvironmentTelemetry module!"); + return NULL; } - return NULL; -} - -bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) -{ - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_environment_metrics_tag; - m.time = getTime(); - - if (getEnvironmentTelemetry(&m)) { - LOG_INFO("Send: barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, temperature=%f", - m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.current, - m.variant.environment_metrics.gas_resistance, m.variant.environment_metrics.relative_humidity, - m.variant.environment_metrics.temperature); - LOG_INFO("Send: voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage, - m.variant.environment_metrics.iaq, m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); - - LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed, - m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight); - - LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); - - LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, - m.variant.environment_metrics.soil_moisture); - - sensor_read_error_count = 0; - - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); - - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); - - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); - notification->level = meshtastic_LogRecord_Level_INFO; - notification->time = getValidTime(RTCQualityFromNet); - sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", - Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, - default_telemetry_broadcast_interval_secs) / - 1000U); - service->sendClientNotification(notification); - sleepOnNextExecution = true; - LOG_DEBUG("Start next execution in 5s, then sleep"); - setIntervalFromNow(FIVE_SECONDS_MS); - } - } - return true; + // Check for a request for environment metrics + if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getEnvironmentTelemetry(&m)) { + LOG_INFO("Environment telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } } - return false; + } + return NULL; } -AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) -{ - AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; +bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_environment_metrics_tag; + m.time = getTime(); + + if (getEnvironmentTelemetry(&m)) { + LOG_INFO("Send: barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, temperature=%f", + m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.current, m.variant.environment_metrics.gas_resistance, + m.variant.environment_metrics.relative_humidity, m.variant.environment_metrics.temperature); + LOG_INFO("Send: voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage, m.variant.environment_metrics.iaq, + m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); + + LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed, + m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight); + + LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); + + LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, + m.variant.environment_metrics.soil_moisture); + + sensor_read_error_count = 0; + + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); + + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } + } + return true; + } + return false; +} + +AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL - for (TelemetrySensor *sensor : sensors) { - result = sensor->handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } + for (TelemetrySensor *sensor : sensors) { + result = sensor->handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } - if (ina219Sensor.hasSensor()) { - result = ina219Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } - if (ina260Sensor.hasSensor()) { - result = ina260Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } - if (ina3221Sensor.hasSensor()) { - result = ina3221Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } - if (max17048Sensor.hasSensor()) { - result = max17048Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } + if (ina219Sensor.hasSensor()) { + result = ina219Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (ina260Sensor.hasSensor()) { + result = ina260Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (ina3221Sensor.hasSensor()) { + result = ina3221Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (max17048Sensor.hasSensor()) { + result = max17048Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } #endif - return result; + return result; } #endif diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 6e4ce82e7..d8e15ae02 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -15,59 +15,53 @@ #include #include -class EnvironmentTelemetryModule : private concurrency::OSThread, - public ScanI2CConsumer, - public ProtobufModule -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, - &EnvironmentTelemetryModule::handleStatusUpdate); +class EnvironmentTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, public ProtobufModule { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &EnvironmentTelemetryModule::handleStatusUpdate); - public: - EnvironmentTelemetryModule() - : concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(), - ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) - { - lastMeasurementPacket = nullptr; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(10 * 1000); - } - virtual bool wantUIFrame() override; +public: + EnvironmentTelemetryModule() + : concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(), + ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + lastMeasurementPacket = nullptr; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(10 * 1000); + } + virtual bool wantUIFrame() override; #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Environment telemetry data - @return true if it contains valid data - */ - bool getEnvironmentTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); +protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Environment telemetry data + @return true if it contains valid data + */ + bool getEnvironmentTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; - void i2cScanFinished(ScanI2C *i2cScanner); + void i2cScanFinished(ScanI2C *i2cScanner); - private: - bool firstTime = 1; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; - uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; +private: + bool firstTime = 1; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; + uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 215e49c7a..db37d1ad5 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -33,226 +33,214 @@ MLX90614Sensor mlx90614Sensor; #endif #include -int32_t HealthTelemetryModule::runOnce() -{ - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval, - default_telemetry_broadcast_interval_secs); - LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); - doDeepSleep(nightyNightMs, true, false); +int32_t HealthTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval, default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } + + uint32_t result = UINT32_MAX; + + if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); + } + + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = false; + + if (moduleConfig.telemetry.health_measurement_enabled) { + LOG_INFO("Health Telemetry: init"); + // Initialize sensors + if (mlx90614Sensor.hasSensor()) + result = mlx90614Sensor.runOnce(); + if (max30102Sensor.hasSensor()) + result = max30102Sensor.runOnce(); + } + return result == UINT32_MAX ? disable() : setStartDelay(); + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.health_measurement_enabled) { + return disable(); } - uint32_t result = UINT32_MAX; - - if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.health_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); } - - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = false; - - if (moduleConfig.telemetry.health_measurement_enabled) { - LOG_INFO("Health Telemetry: init"); - // Initialize sensors - if (mlx90614Sensor.hasSensor()) - result = mlx90614Sensor.runOnce(); - if (max30102Sensor.hasSensor()) - result = max30102Sensor.runOnce(); - } - return result == UINT32_MAX ? disable() : setStartDelay(); - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.health_measurement_enabled) { - return disable(); - } - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.health_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - lastSentToPhone = millis(); - } - } - return min(sendToPhoneIntervalMs, result); + } + return min(sendToPhoneIntervalMs, result); } -bool HealthTelemetryModule::wantUIFrame() -{ - return moduleConfig.telemetry.health_screen_enabled; +bool HealthTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.health_screen_enabled; } + +void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (lastMeasurementPacket == nullptr) { + // If there's no valid packet, display "Health" + display->drawString(x, y, "Health"); + display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + return; + } + + // Decode the last measurement packet + meshtastic_Telemetry lastMeasurement; + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + const char *lastSender = getSenderShortName(*lastMeasurementPacket); + + const meshtastic_Data &p = lastMeasurementPacket->decoded; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { + display->drawString(x, y, "Measurement Error"); + LOG_ERROR("Unable to decode last packet"); + return; + } + + // Display "Health From: ..." on its own + char headerStr[64]; + snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); + display->drawString(x, y, headerStr); + + char last_temp[16]; + if (moduleConfig.telemetry.environment_display_fahrenheit) { + snprintf(last_temp, sizeof(last_temp), "%.0f°F", UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); + } else { + snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature); + } + + // Continue with the remaining details + char tempStr[32]; + snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); + display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); + if (lastMeasurement.variant.health_metrics.has_heart_bpm) { + char heartStr[32]; + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); + } + if (lastMeasurement.variant.health_metrics.has_spO2) { + char spo2Str[32]; + snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); + } } -void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - if (lastMeasurementPacket == nullptr) { - // If there's no valid packet, display "Health" - display->drawString(x, y, "Health"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); - return; - } - - // Decode the last measurement packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); - - const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); - return; - } - - // Display "Health From: ..." on its own - char headerStr[64]; - snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); - display->drawString(x, y, headerStr); - - char last_temp[16]; - if (moduleConfig.telemetry.environment_display_fahrenheit) { - snprintf(last_temp, sizeof(last_temp), "%.0f°F", - UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); - } else { - snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature); - } - - // Continue with the remaining details - char tempStr[32]; - snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); - display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); - if (lastMeasurement.variant.health_metrics.has_heart_bpm) { - char heartStr[32]; - snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); - display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); - } - if (lastMeasurement.variant.health_metrics.has_spO2) { - char spo2Str[32]; - snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); - display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); - } -} - -bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) -{ - if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { +bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { + if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature, - t->variant.health_metrics.heart_bpm, t->variant.health_metrics.spO2); + LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature, + t->variant.health_metrics.heart_bpm, t->variant.health_metrics.spO2); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) -{ - bool valid = true; - bool hasSensor = false; - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_health_metrics_tag; - m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; +bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) { + bool valid = true; + bool hasSensor = false; + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_health_metrics_tag; + m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; - if (max30102Sensor.hasSensor()) { - valid = valid && max30102Sensor.getMetrics(m); - hasSensor = true; - } - if (mlx90614Sensor.hasSensor()) { - valid = valid && mlx90614Sensor.getMetrics(m); - hasSensor = true; - } + if (max30102Sensor.hasSensor()) { + valid = valid && max30102Sensor.getMetrics(m); + hasSensor = true; + } + if (mlx90614Sensor.hasSensor()) { + valid = valid && mlx90614Sensor.getMetrics(m); + hasSensor = true; + } - return valid && hasSensor; + return valid && hasSensor; } -meshtastic_MeshPacket *HealthTelemetryModule::allocReply() -{ - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding HealthTelemetry module!"); - return NULL; - } - // Check for a request for health metrics - if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getHealthTelemetry(&m)) { - LOG_INFO("Health telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } +meshtastic_MeshPacket *HealthTelemetryModule::allocReply() { + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding HealthTelemetry module!"); + return NULL; } - return NULL; + // Check for a request for health metrics + if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getHealthTelemetry(&m)) { + LOG_INFO("Health telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } + } + return NULL; } -bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) -{ - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_health_metrics_tag; - m.time = getTime(); - if (getHealthTelemetry(&m)) { - LOG_INFO("Send: temperature=%f, heart_bpm=%d, spO2=%d", m.variant.health_metrics.temperature, - m.variant.health_metrics.heart_bpm, m.variant.health_metrics.spO2); +bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_health_metrics_tag; + m.time = getTime(); + if (getHealthTelemetry(&m)) { + LOG_INFO("Send: temperature=%f, heart_bpm=%d, spO2=%d", m.variant.health_metrics.temperature, m.variant.health_metrics.heart_bpm, + m.variant.health_metrics.spO2); - sensor_read_error_count = 0; + sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - LOG_DEBUG("Start next execution in 5s, then sleep"); - sleepOnNextExecution = true; - setIntervalFromNow(5000); - } - } - return true; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + LOG_DEBUG("Start next execution in 5s, then sleep"); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } } - return false; + return true; + } + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HealthTelemetry.h b/src/modules/Telemetry/HealthTelemetry.h index 01e4c2372..ffe95f36d 100644 --- a/src/modules/Telemetry/HealthTelemetry.h +++ b/src/modules/Telemetry/HealthTelemetry.h @@ -9,52 +9,49 @@ #include #include -class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); +class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); - public: - HealthTelemetryModule() - : concurrency::OSThread("HealthTelemetry"), - ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) - { - lastMeasurementPacket = nullptr; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(10 * 1000); - } +public: + HealthTelemetryModule() + : concurrency::OSThread("HealthTelemetry"), ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + lastMeasurementPacket = nullptr; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(10 * 1000); + } #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - virtual bool wantUIFrame() override; + virtual bool wantUIFrame() override; - protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Health telemetry data - @return true if it contains valid data - */ - bool getHealthTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); +protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Health telemetry data + @return true if it contains valid data + */ + bool getHealthTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - private: - bool firstTime = 1; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; - uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; +private: + bool firstTime = 1; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; + uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp index 577132006..cd736797b 100644 --- a/src/modules/Telemetry/HostMetrics.cpp +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -6,37 +6,34 @@ #include #endif -int32_t HostMetricsModule::runOnce() -{ +int32_t HostMetricsModule::runOnce() { #if ARCH_PORTDUINO - if (portduino_config.hostMetrics_interval == 0) { - return disable(); - } else { - sendMetrics(); - return 60 * 1000 * portduino_config.hostMetrics_interval; - } -#else + if (portduino_config.hostMetrics_interval == 0) { return disable(); + } else { + sendMetrics(); + return 60 * 1000 * portduino_config.hostMetrics_interval; + } +#else + return disable(); #endif } -bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) -{ - if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { +bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { + if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); - if (t->variant.host_metrics.has_user_string) - t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; + const char *sender = getSenderShortName(mp); + if (t->variant.host_metrics.has_user_string) + t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; - LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, - t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, - t->variant.host_metrics.freemem_bytes, static_cast(t->variant.host_metrics.load1) / 100, - static_cast(t->variant.host_metrics.load5) / 100, - static_cast(t->variant.host_metrics.load15) / 100); - // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); + LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, + t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, t->variant.host_metrics.freemem_bytes, + static_cast(t->variant.host_metrics.load1) / 100, static_cast(t->variant.host_metrics.load5) / 100, + static_cast(t->variant.host_metrics.load15) / 100); + // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); #endif - } - return false; // Let others look at this message also if they want + } + return false; // Let others look at this message also if they want } /* @@ -65,75 +62,72 @@ meshtastic_MeshPacket *HostMetricsModule::allocReply() */ #if ARCH_PORTDUINO -meshtastic_Telemetry HostMetricsModule::getHostMetrics() -{ - std::string file_line; - meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; - t.which_variant = meshtastic_Telemetry_host_metrics_tag; - t.variant.host_metrics = meshtastic_HostMetrics_init_zero; +meshtastic_Telemetry HostMetricsModule::getHostMetrics() { + std::string file_line; + meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; + t.which_variant = meshtastic_Telemetry_host_metrics_tag; + t.variant.host_metrics = meshtastic_HostMetrics_init_zero; - if (access("/proc/uptime", R_OK) == 0) { - std::ifstream proc_uptime("/proc/uptime"); - if (proc_uptime.is_open()) { - std::getline(proc_uptime, file_line, ' '); - proc_uptime.close(); - t.variant.host_metrics.uptime_seconds = stoul(file_line); - } + if (access("/proc/uptime", R_OK) == 0) { + std::ifstream proc_uptime("/proc/uptime"); + if (proc_uptime.is_open()) { + std::getline(proc_uptime, file_line, ' '); + proc_uptime.close(); + t.variant.host_metrics.uptime_seconds = stoul(file_line); } + } - std::filesystem::space_info root = std::filesystem::space("/"); - t.variant.host_metrics.diskfree1_bytes = root.available; + std::filesystem::space_info root = std::filesystem::space("/"); + t.variant.host_metrics.diskfree1_bytes = root.available; - if (access("/proc/meminfo", R_OK) == 0) { - std::ifstream proc_meminfo("/proc/meminfo"); - if (proc_meminfo.is_open()) { - do { - std::getline(proc_meminfo, file_line); - } while (file_line.find("MemAvailable") == std::string::npos); - proc_meminfo.close(); - t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024; - } + if (access("/proc/meminfo", R_OK) == 0) { + std::ifstream proc_meminfo("/proc/meminfo"); + if (proc_meminfo.is_open()) { + do { + std::getline(proc_meminfo, file_line); + } while (file_line.find("MemAvailable") == std::string::npos); + proc_meminfo.close(); + t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024; } - if (access("/proc/loadavg", R_OK) == 0) { - std::ifstream proc_loadavg("/proc/loadavg"); - if (proc_loadavg.is_open()) { - std::getline(proc_loadavg, file_line, ' '); - t.variant.host_metrics.load1 = stof(file_line) * 100; - std::getline(proc_loadavg, file_line, ' '); - t.variant.host_metrics.load5 = stof(file_line) * 100; - std::getline(proc_loadavg, file_line, ' '); - t.variant.host_metrics.load15 = stof(file_line) * 100; - proc_loadavg.close(); - } + } + if (access("/proc/loadavg", R_OK) == 0) { + std::ifstream proc_loadavg("/proc/loadavg"); + if (proc_loadavg.is_open()) { + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load1 = stof(file_line) * 100; + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load5 = stof(file_line) * 100; + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load15 = stof(file_line) * 100; + proc_loadavg.close(); } - if (portduino_config.hostMetrics_user_command != "") { - std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str()); - if (userCommandResult.length() > 1) { - strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); - t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; - t.variant.host_metrics.has_user_string = true; - } + } + if (portduino_config.hostMetrics_user_command != "") { + std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str()); + if (userCommandResult.length() > 1) { + strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); + t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; + t.variant.host_metrics.has_user_string = true; } - return t; + } + return t; } -bool HostMetricsModule::sendMetrics() -{ - meshtastic_Telemetry telemetry = getHostMetrics(); - LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", - telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes, - telemetry.variant.host_metrics.freemem_bytes, static_cast(telemetry.variant.host_metrics.load1) / 100, - static_cast(telemetry.variant.host_metrics.load5) / 100, - static_cast(telemetry.variant.host_metrics.load15) / 100); - // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); +bool HostMetricsModule::sendMetrics() { + meshtastic_Telemetry telemetry = getHostMetrics(); + LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", telemetry.variant.host_metrics.uptime_seconds, + telemetry.variant.host_metrics.diskfree1_bytes, telemetry.variant.host_metrics.freemem_bytes, + static_cast(telemetry.variant.host_metrics.load1) / 100, static_cast(telemetry.variant.host_metrics.load5) / 100, + static_cast(telemetry.variant.host_metrics.load15) / 100); + // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); - meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); - p->to = NODENUM_BROADCAST; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - p->channel = portduino_config.hostMetrics_channel; - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); - return true; + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->channel = portduino_config.hostMetrics_channel; + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + return true; } #endif diff --git a/src/modules/Telemetry/HostMetrics.h b/src/modules/Telemetry/HostMetrics.h index 99ee631c1..e737d171c 100644 --- a/src/modules/Telemetry/HostMetrics.h +++ b/src/modules/Telemetry/HostMetrics.h @@ -2,39 +2,36 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "ProtobufModule.h" -class HostMetricsModule : private concurrency::OSThread, public ProtobufModule -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &HostMetricsModule::handleStatusUpdate); +class HostMetricsModule : private concurrency::OSThread, public ProtobufModule { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &HostMetricsModule::handleStatusUpdate); - public: - HostMetricsModule() - : concurrency::OSThread("HostMetrics"), - ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) - { - uptimeWrapCount = 0; - uptimeLastMs = millis(); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent - } - virtual bool wantUIFrame() { return false; } +public: + HostMetricsModule() + : concurrency::OSThread("HostMetrics"), ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + uptimeWrapCount = 0; + uptimeLastMs = millis(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + } + virtual bool wantUIFrame() { return false; } - protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - // virtual meshtastic_MeshPacket *allocReply() override; - virtual int32_t runOnce() override; - /** - * Send our Telemetry into the mesh - */ - bool sendMetrics(); +protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + // virtual meshtastic_MeshPacket *allocReply() override; + virtual int32_t runOnce() override; + /** + * Send our Telemetry into the mesh + */ + bool sendMetrics(); - private: - meshtastic_Telemetry getHostMetrics(); +private: + meshtastic_Telemetry getHostMetrics(); - uint32_t lastSentToMesh = 0; - uint32_t uptimeWrapCount; - uint32_t uptimeLastMs; + uint32_t lastSentToMesh = 0; + uint32_t uptimeWrapCount; + uint32_t uptimeLastMs; }; \ No newline at end of file diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 9047c7cd4..11813b9c7 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -22,268 +22,255 @@ #include "graphics/ScreenFonts.h" #include -namespace graphics -{ -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, - bool show_date); +namespace graphics { +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); } -int32_t PowerTelemetryModule::runOnce() -{ - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval, - default_telemetry_broadcast_interval_secs); - LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); - doDeepSleep(nightyNightMs, true, false); - } +int32_t PowerTelemetryModule::runOnce() { + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.telemetry.power_measurement_enabled = 1; - // moduleConfig.telemetry.power_screen_enabled = 1; - // moduleConfig.telemetry.power_update_interval = 45; + // moduleConfig.telemetry.power_measurement_enabled = 1; + // moduleConfig.telemetry.power_screen_enabled = 1; + // moduleConfig.telemetry.power_update_interval = 45; - if (!(moduleConfig.telemetry.power_measurement_enabled)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } + if (!(moduleConfig.telemetry.power_measurement_enabled)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); + } - uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes); + uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.power_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes); - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = 0; - uint32_t result = UINT32_MAX; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = 0; + uint32_t result = UINT32_MAX; #if HAS_TELEMETRY - if (moduleConfig.telemetry.power_measurement_enabled) { - LOG_INFO("Power Telemetry: init"); - // If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again, - // but we need to set the result to != UINT32_MAX to avoid it being disabled - if (ina219Sensor.hasSensor()) - result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce(); - if (ina226Sensor.hasSensor()) - result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce(); - if (ina260Sensor.hasSensor()) - result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce(); - if (ina3221Sensor.hasSensor()) - result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce(); - if (max17048Sensor.hasSensor()) - result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); - } - - // it's possible to have this module enabled, only for displaying values on the screen. - // therefore, we should only enable the sensor loop if measurement is also enabled - return result == UINT32_MAX ? disable() : setStartDelay(); -#else - return disable(); -#endif - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.power_measurement_enabled) - return disable(); - - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - lastSentToPhone = millis(); - } + if (moduleConfig.telemetry.power_measurement_enabled) { + LOG_INFO("Power Telemetry: init"); + // If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again, + // but we need to set the result to != UINT32_MAX to avoid it being disabled + if (ina219Sensor.hasSensor()) + result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce(); + if (ina226Sensor.hasSensor()) + result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce(); + if (ina260Sensor.hasSensor()) + result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor()) + result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce(); + if (max17048Sensor.hasSensor()) + result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); } - return min(sendToPhoneIntervalMs, sendToMeshIntervalMs); + + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); +#else + return disable(); +#endif + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.power_measurement_enabled) + return disable(); + + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + } + return min(sendToPhoneIntervalMs, sendToMeshIntervalMs); } -bool PowerTelemetryModule::wantUIFrame() -{ - return moduleConfig.telemetry.power_screen_enabled; -} +bool PowerTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.power_screen_enabled; } #if HAS_SCREEN -void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; + // === Set Title + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - if (lastMeasurementPacket == nullptr) { - // In case of no valid packet, display "Power Telemetry", "No measurement" - display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); - return; - } + if (lastMeasurementPacket == nullptr) { + // In case of no valid packet, display "Power Telemetry", "No measurement" + display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); + return; + } - // Decode the last power packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); + // Decode the last power packet + meshtastic_Telemetry lastMeasurement; + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + const char *lastSender = getSenderShortName(*lastMeasurementPacket); - const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); - return; - } + const meshtastic_Data &p = lastMeasurementPacket->decoded; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { + display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); + LOG_ERROR("Unable to decode last packet"); + return; + } - // Display "Pow. From: ..." - char fromStr[64]; - snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); - display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); + // Display "Pow. From: ..." + char fromStr[64]; + snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); + display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); - // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags - const auto &m = lastMeasurement.variant.power_metrics; - int lineY = textSecondLine; + // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags + const auto &m = lastMeasurement.variant.power_metrics; + int lineY = textSecondLine; - auto drawLine = [&](const char *label, float voltage, float current) { - char lineStr[64]; - snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); - display->drawString(x, lineY, lineStr); - lineY += _fontHeight(FONT_SMALL); - }; + auto drawLine = [&](const char *label, float voltage, float current) { + char lineStr[64]; + snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); + display->drawString(x, lineY, lineStr); + lineY += _fontHeight(FONT_SMALL); + }; - if (m.has_ch1_voltage || m.has_ch1_current) { - drawLine("Ch1", m.ch1_voltage, m.ch1_current); - } - if (m.has_ch2_voltage || m.has_ch2_current) { - drawLine("Ch2", m.ch2_voltage, m.ch2_current); - } - if (m.has_ch3_voltage || m.has_ch3_current) { - drawLine("Ch3", m.ch3_voltage, m.ch3_current); - } - graphics::drawCommonFooter(display, x, y); + if (m.has_ch1_voltage || m.has_ch1_current) { + drawLine("Ch1", m.ch1_voltage, m.ch1_current); + } + if (m.has_ch2_voltage || m.has_ch2_current) { + drawLine("Ch2", m.ch2_voltage, m.ch2_current); + } + if (m.has_ch3_voltage || m.has_ch3_current) { + drawLine("Ch3", m.ch3_voltage, m.ch3_current); + } + graphics::drawCommonFooter(display, x, y); } #endif -bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) -{ - if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { +bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { + if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, " - "ch3_voltage=%.1f, ch3_current=%.1f", - sender, t->variant.power_metrics.ch1_voltage, t->variant.power_metrics.ch1_current, - t->variant.power_metrics.ch2_voltage, t->variant.power_metrics.ch2_current, t->variant.power_metrics.ch3_voltage, - t->variant.power_metrics.ch3_current); + LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, " + "ch3_voltage=%.1f, ch3_current=%.1f", + sender, t->variant.power_metrics.ch1_voltage, t->variant.power_metrics.ch1_current, t->variant.power_metrics.ch2_voltage, + t->variant.power_metrics.ch2_current, t->variant.power_metrics.ch3_voltage, t->variant.power_metrics.ch3_current); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m) -{ - bool valid = false; - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_power_metrics_tag; +bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m) { + bool valid = false; + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_power_metrics_tag; - m->variant.power_metrics = meshtastic_PowerMetrics_init_zero; + m->variant.power_metrics = meshtastic_PowerMetrics_init_zero; #if HAS_TELEMETRY - if (ina219Sensor.hasSensor()) - valid = ina219Sensor.getMetrics(m); - if (ina226Sensor.hasSensor()) - valid = ina226Sensor.getMetrics(m); - if (ina260Sensor.hasSensor()) - valid = ina260Sensor.getMetrics(m); - if (ina3221Sensor.hasSensor()) - valid = ina3221Sensor.getMetrics(m); - if (max17048Sensor.hasSensor()) - valid = max17048Sensor.getMetrics(m); + if (ina219Sensor.hasSensor()) + valid = ina219Sensor.getMetrics(m); + if (ina226Sensor.hasSensor()) + valid = ina226Sensor.getMetrics(m); + if (ina260Sensor.hasSensor()) + valid = ina260Sensor.getMetrics(m); + if (ina3221Sensor.hasSensor()) + valid = ina3221Sensor.getMetrics(m); + if (max17048Sensor.hasSensor()) + valid = max17048Sensor.getMetrics(m); #endif - return valid; + return valid; } -meshtastic_MeshPacket *PowerTelemetryModule::allocReply() -{ - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding PowerTelemetry module!"); - return NULL; - } - // Check for a request for power metrics - if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getPowerTelemetry(&m)) { - LOG_INFO("Power telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } +meshtastic_MeshPacket *PowerTelemetryModule::allocReply() { + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding PowerTelemetry module!"); + return NULL; } + // Check for a request for power metrics + if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getPowerTelemetry(&m)) { + LOG_INFO("Power telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } + } - return NULL; + return NULL; } -bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) -{ - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_power_metrics_tag; - m.time = getTime(); - if (getPowerTelemetry(&m)) { - LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, " - "ch3_voltage=%f, ch3_current=%f", - m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage, - m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current); +bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_power_metrics_tag; + m.time = getTime(); + if (getPowerTelemetry(&m)) { + LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, " + "ch3_voltage=%f, ch3_current=%f", + m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage, + m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current); - sensor_read_error_count = 0; + sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - LOG_DEBUG("Start next execution in 5s then sleep"); - sleepOnNextExecution = true; - setIntervalFromNow(5000); - } - } - return true; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + LOG_DEBUG("Start next execution in 5s then sleep"); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } } - return false; + return true; + } + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/PowerTelemetry.h b/src/modules/Telemetry/PowerTelemetry.h index b9ec6edc1..cb88ce05b 100644 --- a/src/modules/Telemetry/PowerTelemetry.h +++ b/src/modules/Telemetry/PowerTelemetry.h @@ -10,50 +10,47 @@ #include #include -class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule -{ - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); +class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule { + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); - public: - PowerTelemetryModule() - : concurrency::OSThread("PowerTelemetry"), - ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) - { - lastMeasurementPacket = nullptr; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(10 * 1000); - } - virtual bool wantUIFrame() override; +public: + PowerTelemetryModule() + : concurrency::OSThread("PowerTelemetry"), ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + lastMeasurementPacket = nullptr; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(10 * 1000); + } + virtual bool wantUIFrame() override; #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Power telemetry data - @return true if it contains valid data - */ - bool getPowerTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); +protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Power telemetry data + @return true if it contains valid data + */ + bool getPowerTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - private: - bool firstTime = 1; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; - uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; +private: + bool firstTime = 1; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; + uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/AHT10.cpp b/src/modules/Telemetry/Sensor/AHT10.cpp index c38fd2a92..5f6cbb22b 100644 --- a/src/modules/Telemetry/Sensor/AHT10.cpp +++ b/src/modules/Telemetry/Sensor/AHT10.cpp @@ -15,35 +15,33 @@ AHT10Sensor::AHT10Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_AHT10, "AHT10") {} -bool AHT10Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - aht10 = Adafruit_AHTX0(); - status = aht10.begin(bus, 0, dev->address.address); +bool AHT10Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + aht10 = Adafruit_AHTX0(); + status = aht10.begin(bus, 0, dev->address.address); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - LOG_DEBUG("AHT10 getMetrics"); +bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement) { + LOG_DEBUG("AHT10 getMetrics"); - sensors_event_t humidity, temp; - aht10.getEvent(&humidity, &temp); + sensors_event_t humidity, temp; + aht10.getEvent(&humidity, &temp); - // prefer other sensors like bmp280, bmp3xx - if (!measurement->variant.environment_metrics.has_temperature) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = temp.temperature + AHT10_TEMP_OFFSET; - } + // prefer other sensors like bmp280, bmp3xx + if (!measurement->variant.environment_metrics.has_temperature) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = temp.temperature + AHT10_TEMP_OFFSET; + } - if (!measurement->variant.environment_metrics.has_relative_humidity) { - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - } + if (!measurement->variant.environment_metrics.has_relative_humidity) { + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; + } - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/AHT10.h b/src/modules/Telemetry/Sensor/AHT10.h index f85f04aa0..bf17d344b 100644 --- a/src/modules/Telemetry/Sensor/AHT10.h +++ b/src/modules/Telemetry/Sensor/AHT10.h @@ -14,15 +14,14 @@ #include "TelemetrySensor.h" #include -class AHT10Sensor : public TelemetrySensor -{ - private: - Adafruit_AHTX0 aht10; +class AHT10Sensor : public TelemetrySensor { +private: + Adafruit_AHTX0 aht10; - public: - AHT10Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + AHT10Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/BH1750Sensor.cpp b/src/modules/Telemetry/Sensor/BH1750Sensor.cpp index b8790dcd5..38efe6a66 100644 --- a/src/modules/Telemetry/Sensor/BH1750Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BH1750Sensor.cpp @@ -13,42 +13,40 @@ BH1750Sensor::BH1750Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BH1750, "BH1750") {} -bool BH1750Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s with mode %d", sensorName, BH1750_SENSOR_MODE); +bool BH1750Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s with mode %d", sensorName, BH1750_SENSOR_MODE); - bh1750 = BH1750_WE(bus, dev->address.address); - status = bh1750.init(); - if (!status) { - return status; - } - - bh1750.setMode(BH1750_SENSOR_MODE); - - initI2CSensor(); + bh1750 = BH1750_WE(bus, dev->address.address); + status = bh1750.init(); + if (!status) { return status; + } + + bh1750.setMode(BH1750_SENSOR_MODE); + + initI2CSensor(); + return status; } -bool BH1750Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ +bool BH1750Sensor::getMetrics(meshtastic_Telemetry *measurement) { - /* An OTH and OTH_2 measurement takes ~120 ms. I suggest to wait - 140 ms to be on the safe side. - An OTL measurement takes about 16 ms. I suggest to wait 20 ms - to be on the safe side. */ - if (BH1750_SENSOR_MODE == BH1750Mode::OTH || BH1750_SENSOR_MODE == BH1750Mode::OTH_2) { - bh1750.setMode(BH1750_SENSOR_MODE); - delay(140); // wait for measurement to be completed - } else if (BH1750_SENSOR_MODE == BH1750Mode::OTL) { - bh1750.setMode(BH1750_SENSOR_MODE); - delay(20); - } + /* An OTH and OTH_2 measurement takes ~120 ms. I suggest to wait + 140 ms to be on the safe side. + An OTL measurement takes about 16 ms. I suggest to wait 20 ms + to be on the safe side. */ + if (BH1750_SENSOR_MODE == BH1750Mode::OTH || BH1750_SENSOR_MODE == BH1750Mode::OTH_2) { + bh1750.setMode(BH1750_SENSOR_MODE); + delay(140); // wait for measurement to be completed + } else if (BH1750_SENSOR_MODE == BH1750Mode::OTL) { + bh1750.setMode(BH1750_SENSOR_MODE); + delay(20); + } - measurement->variant.environment_metrics.has_lux = true; - float lightIntensity = bh1750.getLux(); + measurement->variant.environment_metrics.has_lux = true; + float lightIntensity = bh1750.getLux(); - measurement->variant.environment_metrics.lux = lightIntensity; - return true; + measurement->variant.environment_metrics.lux = lightIntensity; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/BH1750Sensor.h b/src/modules/Telemetry/Sensor/BH1750Sensor.h index d9a4ded95..3df5c9ca9 100644 --- a/src/modules/Telemetry/Sensor/BH1750Sensor.h +++ b/src/modules/Telemetry/Sensor/BH1750Sensor.h @@ -7,15 +7,14 @@ #include "TelemetrySensor.h" #include -class BH1750Sensor : public TelemetrySensor -{ - private: - BH1750_WE bh1750; +class BH1750Sensor : public TelemetrySensor { +private: + BH1750_WE bh1750; - public: - BH1750Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + BH1750Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/BME280Sensor.cpp b/src/modules/Telemetry/Sensor/BME280Sensor.cpp index 779b2e603..a793d89b5 100644 --- a/src/modules/Telemetry/Sensor/BME280Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME280Sensor.cpp @@ -10,36 +10,34 @@ BME280Sensor::BME280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME280, "BME280") {} -bool BME280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = bme280.begin(dev->address.address, bus); - if (!status) { - return status; - } - - bme280.setSampling(Adafruit_BME280::MODE_FORCED, - Adafruit_BME280::SAMPLING_X1, // Temp. oversampling - Adafruit_BME280::SAMPLING_X1, // Pressure oversampling - Adafruit_BME280::SAMPLING_X1, // Humidity oversampling - Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); - - initI2CSensor(); +bool BME280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = bme280.begin(dev->address.address, bus); + if (!status) { return status; + } + + bme280.setSampling(Adafruit_BME280::MODE_FORCED, + Adafruit_BME280::SAMPLING_X1, // Temp. oversampling + Adafruit_BME280::SAMPLING_X1, // Pressure oversampling + Adafruit_BME280::SAMPLING_X1, // Humidity oversampling + Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); + + initI2CSensor(); + return status; } -bool BME280Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool BME280Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - LOG_DEBUG("BME280 getMetrics"); - bme280.takeForcedMeasurement(); - measurement->variant.environment_metrics.temperature = bme280.readTemperature(); - measurement->variant.environment_metrics.relative_humidity = bme280.readHumidity(); - measurement->variant.environment_metrics.barometric_pressure = bme280.readPressure() / 100.0F; + LOG_DEBUG("BME280 getMetrics"); + bme280.takeForcedMeasurement(); + measurement->variant.environment_metrics.temperature = bme280.readTemperature(); + measurement->variant.environment_metrics.relative_humidity = bme280.readHumidity(); + measurement->variant.environment_metrics.barometric_pressure = bme280.readPressure() / 100.0F; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME280Sensor.h b/src/modules/Telemetry/Sensor/BME280Sensor.h index fadae46cd..81a7f5139 100644 --- a/src/modules/Telemetry/Sensor/BME280Sensor.h +++ b/src/modules/Telemetry/Sensor/BME280Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class BME280Sensor : public TelemetrySensor -{ - private: - Adafruit_BME280 bme280; +class BME280Sensor : public TelemetrySensor { +private: + Adafruit_BME280 bme280; - public: - BME280Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + BME280Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 95f3dc5f0..6f871c8e2 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -10,139 +10,132 @@ BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} -int32_t BME680Sensor::runOnce() -{ - if (!bme680.run()) { - checkStatus("runTrigger"); +int32_t BME680Sensor::runOnce() { + if (!bme680.run()) { + checkStatus("runTrigger"); + } + return 35; +} + +bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + status = 0; + if (!bme680.begin(dev->address.address, *bus)) + checkStatus("begin"); + + if (bme680.status == BSEC_OK) { + status = 1; + if (!bme680.setConfig(bsec_config)) { + checkStatus("setConfig"); + status = 0; } - return 35; -} - -bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - status = 0; - if (!bme680.begin(dev->address.address, *bus)) - checkStatus("begin"); - - if (bme680.status == BSEC_OK) { - status = 1; - if (!bme680.setConfig(bsec_config)) { - checkStatus("setConfig"); - status = 0; - } - loadState(); - if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) { - checkStatus("updateSubscription"); - status = 0; - } - LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major, - bme680.version.minor, bme680.version.major_bugfix, bme680.version.minor_bugfix); + loadState(); + if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) { + checkStatus("updateSubscription"); + status = 0; } + LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major, bme680.version.minor, + bme680.version.major_bugfix, bme680.version.minor_bugfix); + } - if (status == 0) - LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); + if (status == 0) + LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) - return false; +bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) { + if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) + return false; - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.has_gas_resistance = true; - measurement->variant.environment_metrics.has_iaq = true; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.has_gas_resistance = true; + measurement->variant.environment_metrics.has_iaq = true; - measurement->variant.environment_metrics.temperature = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE).signal; - measurement->variant.environment_metrics.relative_humidity = - bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY).signal; - measurement->variant.environment_metrics.barometric_pressure = bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal; - measurement->variant.environment_metrics.gas_resistance = bme680.getData(BSEC_OUTPUT_RAW_GAS).signal / 1000.0; - // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) - measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; - updateState(); - return true; + measurement->variant.environment_metrics.temperature = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE).signal; + measurement->variant.environment_metrics.relative_humidity = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY).signal; + measurement->variant.environment_metrics.barometric_pressure = bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal; + measurement->variant.environment_metrics.gas_resistance = bme680.getData(BSEC_OUTPUT_RAW_GAS).signal / 1000.0; + // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) + measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; + updateState(); + return true; } -void BME680Sensor::loadState() -{ +void BME680Sensor::loadState() { #ifdef FSCom - spiLock->lock(); - auto file = FSCom.open(bsecConfigFileName, FILE_O_READ); + spiLock->lock(); + auto file = FSCom.open(bsecConfigFileName, FILE_O_READ); + if (file) { + file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + bme680.setState(bsecState); + LOG_INFO("%s state read from %s", sensorName, bsecConfigFileName); + } else { + LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName); + } + spiLock->unlock(); +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif +} + +void BME680Sensor::updateState() { +#ifdef FSCom + spiLock->lock(); + bool update = false; + if (stateUpdateCounter == 0) { + /* First state update when IAQ accuracy is >= 3 */ + accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy; + if (accuracy >= 2) { + LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy); + update = true; + stateUpdateCounter++; + } else { + LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy); + } + } else { + /* Update every STATE_SAVE_PERIOD minutes */ + if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) { + LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000); + update = true; + stateUpdateCounter++; + } + } + + if (update) { + bme680.getState(bsecState); + if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) { + LOG_WARN("Can't remove old state file"); + } + auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE); if (file) { - file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.close(); - bme680.setState(bsecState); - LOG_INFO("%s state read from %s", sensorName, bsecConfigFileName); + LOG_INFO("%s state write to %s", sensorName, bsecConfigFileName); + file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.flush(); + file.close(); } else { - LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName); + LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName); } - spiLock->unlock(); + } + spiLock->unlock(); #else - LOG_ERROR("ERROR: Filesystem not implemented"); + LOG_ERROR("ERROR: Filesystem not implemented"); #endif } -void BME680Sensor::updateState() -{ -#ifdef FSCom - spiLock->lock(); - bool update = false; - if (stateUpdateCounter == 0) { - /* First state update when IAQ accuracy is >= 3 */ - accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy; - if (accuracy >= 2) { - LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy); - update = true; - stateUpdateCounter++; - } else { - LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy); - } - } else { - /* Update every STATE_SAVE_PERIOD minutes */ - if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) { - LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000); - update = true; - stateUpdateCounter++; - } - } +void BME680Sensor::checkStatus(const char *functionName) { + if (bme680.status < BSEC_OK) + LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); + else if (bme680.status > BSEC_OK) + LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); - if (update) { - bme680.getState(bsecState); - if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) { - LOG_WARN("Can't remove old state file"); - } - auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE); - if (file) { - LOG_INFO("%s state write to %s", sensorName, bsecConfigFileName); - file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.flush(); - file.close(); - } else { - LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName); - } - } - spiLock->unlock(); -#else - LOG_ERROR("ERROR: Filesystem not implemented"); -#endif -} - -void BME680Sensor::checkStatus(const char *functionName) -{ - if (bme680.status < BSEC_OK) - LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); - else if (bme680.status > BSEC_OK) - LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); - - if (bme680.sensor.status < BME68X_OK) - LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); - else if (bme680.sensor.status > BME68X_OK) - LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); + if (bme680.sensor.status < BME68X_OK) + LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); + else if (bme680.sensor.status > BME68X_OK) + LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index f4ead95f7..10202cfdd 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -12,34 +12,33 @@ const uint8_t bsec_config[] = { #include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" }; -class BME680Sensor : public TelemetrySensor -{ - private: - Bsec2 bme680; +class BME680Sensor : public TelemetrySensor { +private: + Bsec2 bme680; - protected: - const char *bsecConfigFileName = "/prefs/bsec.dat"; - uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; - uint8_t accuracy = 0; - uint16_t stateUpdateCounter = 0; - bsecSensor sensorList[9] = {BSEC_OUTPUT_IAQ, - BSEC_OUTPUT_RAW_TEMPERATURE, - BSEC_OUTPUT_RAW_PRESSURE, - BSEC_OUTPUT_RAW_HUMIDITY, - BSEC_OUTPUT_RAW_GAS, - BSEC_OUTPUT_STABILIZATION_STATUS, - BSEC_OUTPUT_RUN_IN_STATUS, - BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, - BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; - void loadState(); - void updateState(); - void checkStatus(const char *functionName); +protected: + const char *bsecConfigFileName = "/prefs/bsec.dat"; + uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; + uint8_t accuracy = 0; + uint16_t stateUpdateCounter = 0; + bsecSensor sensorList[9] = {BSEC_OUTPUT_IAQ, + BSEC_OUTPUT_RAW_TEMPERATURE, + BSEC_OUTPUT_RAW_PRESSURE, + BSEC_OUTPUT_RAW_HUMIDITY, + BSEC_OUTPUT_RAW_GAS, + BSEC_OUTPUT_STABILIZATION_STATUS, + BSEC_OUTPUT_RUN_IN_STATUS, + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; + void loadState(); + void updateState(); + void checkStatus(const char *functionName); - public: - BME680Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + BME680Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP085Sensor.cpp b/src/modules/Telemetry/Sensor/BMP085Sensor.cpp index 1fb2ecc28..f049b9124 100644 --- a/src/modules/Telemetry/Sensor/BMP085Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BMP085Sensor.cpp @@ -10,27 +10,25 @@ BMP085Sensor::BMP085Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP085, "BMP085") {} -bool BMP085Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool BMP085Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - bmp085 = Adafruit_BMP085(); - status = bmp085.begin(dev->address.address, bus); + bmp085 = Adafruit_BMP085(); + status = bmp085.begin(dev->address.address, bus); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - LOG_DEBUG("BMP085 getMetrics"); - measurement->variant.environment_metrics.temperature = bmp085.readTemperature(); - measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F; + LOG_DEBUG("BMP085 getMetrics"); + measurement->variant.environment_metrics.temperature = bmp085.readTemperature(); + measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP085Sensor.h b/src/modules/Telemetry/Sensor/BMP085Sensor.h index 12ccf35a1..446d76da2 100644 --- a/src/modules/Telemetry/Sensor/BMP085Sensor.h +++ b/src/modules/Telemetry/Sensor/BMP085Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class BMP085Sensor : public TelemetrySensor -{ - private: - Adafruit_BMP085 bmp085; +class BMP085Sensor : public TelemetrySensor { +private: + Adafruit_BMP085 bmp085; - public: - BMP085Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + BMP085Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP280Sensor.cpp b/src/modules/Telemetry/Sensor/BMP280Sensor.cpp index 2b7407c43..45b9f92c3 100644 --- a/src/modules/Telemetry/Sensor/BMP280Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BMP280Sensor.cpp @@ -10,36 +10,34 @@ BMP280Sensor::BMP280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP280, "BMP280") {} -bool BMP280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool BMP280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - bmp280 = Adafruit_BMP280(bus); - status = bmp280.begin(dev->address.address); - if (!status) { - return status; - } - - bmp280.setSampling(Adafruit_BMP280::MODE_FORCED, - Adafruit_BMP280::SAMPLING_X1, // Temp. oversampling - Adafruit_BMP280::SAMPLING_X1, // Pressure oversampling - Adafruit_BMP280::FILTER_OFF, Adafruit_BMP280::STANDBY_MS_1000); - - initI2CSensor(); + bmp280 = Adafruit_BMP280(bus); + status = bmp280.begin(dev->address.address); + if (!status) { return status; + } + + bmp280.setSampling(Adafruit_BMP280::MODE_FORCED, + Adafruit_BMP280::SAMPLING_X1, // Temp. oversampling + Adafruit_BMP280::SAMPLING_X1, // Pressure oversampling + Adafruit_BMP280::FILTER_OFF, Adafruit_BMP280::STANDBY_MS_1000); + + initI2CSensor(); + return status; } -bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - LOG_DEBUG("BMP280 getMetrics"); - bmp280.takeForcedMeasurement(); - measurement->variant.environment_metrics.temperature = bmp280.readTemperature(); - measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F; + LOG_DEBUG("BMP280 getMetrics"); + bmp280.takeForcedMeasurement(); + measurement->variant.environment_metrics.temperature = bmp280.readTemperature(); + measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP280Sensor.h b/src/modules/Telemetry/Sensor/BMP280Sensor.h index 2199fc0cd..5686fa690 100644 --- a/src/modules/Telemetry/Sensor/BMP280Sensor.h +++ b/src/modules/Telemetry/Sensor/BMP280Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class BMP280Sensor : public TelemetrySensor -{ - private: - Adafruit_BMP280 bmp280; +class BMP280Sensor : public TelemetrySensor { +private: + Adafruit_BMP280 bmp280; - public: - BMP280Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + BMP280Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp b/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp index ac80732bf..587a4fa09 100644 --- a/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp +++ b/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp @@ -6,65 +6,61 @@ BMP3XXSensor::BMP3XXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP3XX, "BMP3XX") {} -bool BMP3XXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool BMP3XXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - // Get a singleton instance and initialise the bmp3xx - if (bmp3xx == nullptr) { - bmp3xx = BMP3XXSingleton::GetInstance(); - } - status = bmp3xx->begin_I2C(dev->address.address, bus); - if (!status) { - return status; - } - - // set up oversampling and filter initialization - bmp3xx->setTemperatureOversampling(BMP3_OVERSAMPLING_4X); - bmp3xx->setPressureOversampling(BMP3_OVERSAMPLING_8X); - bmp3xx->setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3); - bmp3xx->setOutputDataRate(BMP3_ODR_25_HZ); - - // take a couple of initial readings to settle the sensor filters - for (int i = 0; i < 3; i++) { - bmp3xx->performReading(); - } - initI2CSensor(); + // Get a singleton instance and initialise the bmp3xx + if (bmp3xx == nullptr) { + bmp3xx = BMP3XXSingleton::GetInstance(); + } + status = bmp3xx->begin_I2C(dev->address.address, bus); + if (!status) { return status; + } + + // set up oversampling and filter initialization + bmp3xx->setTemperatureOversampling(BMP3_OVERSAMPLING_4X); + bmp3xx->setPressureOversampling(BMP3_OVERSAMPLING_8X); + bmp3xx->setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3); + bmp3xx->setOutputDataRate(BMP3_ODR_25_HZ); + + // take a couple of initial readings to settle the sensor filters + for (int i = 0; i < 3; i++) { + bmp3xx->performReading(); + } + initI2CSensor(); + return status; } -bool BMP3XXSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - if (bmp3xx == nullptr) { - bmp3xx = BMP3XXSingleton::GetInstance(); - } - if ((int)measurement->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - bmp3xx->performReading(); +bool BMP3XXSensor::getMetrics(meshtastic_Telemetry *measurement) { + if (bmp3xx == nullptr) { + bmp3xx = BMP3XXSingleton::GetInstance(); + } + if ((int)measurement->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + bmp3xx->performReading(); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.has_relative_humidity = false; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.has_relative_humidity = false; - measurement->variant.environment_metrics.temperature = static_cast(bmp3xx->temperature); - measurement->variant.environment_metrics.barometric_pressure = static_cast(bmp3xx->pressure) / 100.0F; - measurement->variant.environment_metrics.relative_humidity = 0.0f; + measurement->variant.environment_metrics.temperature = static_cast(bmp3xx->temperature); + measurement->variant.environment_metrics.barometric_pressure = static_cast(bmp3xx->pressure) / 100.0F; + measurement->variant.environment_metrics.relative_humidity = 0.0f; - LOG_DEBUG("BMP3XX getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant, - measurement->variant.environment_metrics.temperature, - measurement->variant.environment_metrics.barometric_pressure); - } else { - LOG_DEBUG("BMP3XX getMetrics id: %i", measurement->which_variant); - } - return true; + LOG_DEBUG("BMP3XX getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant, measurement->variant.environment_metrics.temperature, + measurement->variant.environment_metrics.barometric_pressure); + } else { + LOG_DEBUG("BMP3XX getMetrics id: %i", measurement->which_variant); + } + return true; } // Get a singleton wrapper for an Adafruit_bmp3xx -BMP3XXSingleton *BMP3XXSingleton::GetInstance() -{ - if (pinstance == nullptr) { - pinstance = new BMP3XXSingleton(); - } - return pinstance; +BMP3XXSingleton *BMP3XXSingleton::GetInstance() { + if (pinstance == nullptr) { + pinstance = new BMP3XXSingleton(); + } + return pinstance; } BMP3XXSingleton::BMP3XXSingleton() {} @@ -73,16 +69,15 @@ BMP3XXSingleton::~BMP3XXSingleton() {} BMP3XXSingleton *BMP3XXSingleton::pinstance{nullptr}; -bool BMP3XXSingleton::performReading() -{ - bool result = Adafruit_BMP3XX::performReading(); - if (result) { - double atmospheric = this->pressure / 100.0; - altitudeAmslMetres = 44330.0 * (1.0 - pow(atmospheric / SEAL_LEVEL_HPA, 0.1903)); - } else { - altitudeAmslMetres = 0.0; - } - return result; +bool BMP3XXSingleton::performReading() { + bool result = Adafruit_BMP3XX::performReading(); + if (result) { + double atmospheric = this->pressure / 100.0; + altitudeAmslMetres = 44330.0 * (1.0 - pow(atmospheric / SEAL_LEVEL_HPA, 0.1903)); + } else { + altitudeAmslMetres = 0.0; + } + return result; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP3XXSensor.h b/src/modules/Telemetry/Sensor/BMP3XXSensor.h index 7ce14d9db..b65a31ea3 100644 --- a/src/modules/Telemetry/Sensor/BMP3XXSensor.h +++ b/src/modules/Telemetry/Sensor/BMP3XXSensor.h @@ -12,42 +12,40 @@ #include // Singleton wrapper for the Adafruit_BMP3XX class -class BMP3XXSingleton : public Adafruit_BMP3XX -{ - private: - static BMP3XXSingleton *pinstance; +class BMP3XXSingleton : public Adafruit_BMP3XX { +private: + static BMP3XXSingleton *pinstance; - protected: - BMP3XXSingleton(); - ~BMP3XXSingleton(); +protected: + BMP3XXSingleton(); + ~BMP3XXSingleton(); - public: - // Create a singleton instance (not thread safe) - static BMP3XXSingleton *GetInstance(); +public: + // Create a singleton instance (not thread safe) + static BMP3XXSingleton *GetInstance(); - // Singletons should not be cloneable. - BMP3XXSingleton(BMP3XXSingleton &other) = delete; + // Singletons should not be cloneable. + BMP3XXSingleton(BMP3XXSingleton &other) = delete; - // Singletons should not be assignable. - void operator=(const BMP3XXSingleton &) = delete; + // Singletons should not be assignable. + void operator=(const BMP3XXSingleton &) = delete; - // Performs a full reading of all sensors in the BMP3XX. Assigns - // the internal temperature, pressure and altitudeAmsl variables - bool performReading(); + // Performs a full reading of all sensors in the BMP3XX. Assigns + // the internal temperature, pressure and altitudeAmsl variables + bool performReading(); - // Altitude in metres above mean sea level, assigned after calling performReading() - double altitudeAmslMetres = 0.0f; + // Altitude in metres above mean sea level, assigned after calling performReading() + double altitudeAmslMetres = 0.0f; }; -class BMP3XXSensor : public TelemetrySensor -{ - protected: - BMP3XXSingleton *bmp3xx = nullptr; +class BMP3XXSensor : public TelemetrySensor { +protected: + BMP3XXSingleton *bmp3xx = nullptr; - public: - BMP3XXSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + BMP3XXSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp b/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp index e7b191398..c1aa2c5fe 100644 --- a/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp +++ b/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp @@ -14,55 +14,51 @@ CGRadSensSensor::CGRadSensSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RADSENS, "RadSens") {} -bool CGRadSensSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - // Initialize the sensor following the same pattern as RCWL9620Sensor - LOG_INFO("Init sensor: %s", sensorName); - status = true; - begin(bus, dev->address.address); - initI2CSensor(); - return status; +bool CGRadSensSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + // Initialize the sensor following the same pattern as RCWL9620Sensor + LOG_INFO("Init sensor: %s", sensorName); + status = true; + begin(bus, dev->address.address); + initI2CSensor(); + return status; } -void CGRadSensSensor::begin(TwoWire *wire, uint8_t addr) -{ - // Store the Wire and address to the sensor following the same pattern as RCWL9620Sensor - _wire = wire; - _addr = addr; - _wire->begin(); +void CGRadSensSensor::begin(TwoWire *wire, uint8_t addr) { + // Store the Wire and address to the sensor following the same pattern as RCWL9620Sensor + _wire = wire; + _addr = addr; + _wire->begin(); } -float CGRadSensSensor::getStaticRadiation() -{ - // Read a register, following the same pattern as the RCWL9620Sensor - _wire->beginTransmission(_addr); // Transfer data to addr. - _wire->write(0x06); // Radiation intensity (static period T = 500 sec) - if (_wire->endTransmission() == 0) { - if (_wire->requestFrom(_addr, (uint8_t)3)) { - ; // Request 3 bytes - uint32_t data = _wire->read(); - data <<= 8; - data |= _wire->read(); - data <<= 8; - data |= _wire->read(); +float CGRadSensSensor::getStaticRadiation() { + // Read a register, following the same pattern as the RCWL9620Sensor + _wire->beginTransmission(_addr); // Transfer data to addr. + _wire->write(0x06); // Radiation intensity (static period T = 500 sec) + if (_wire->endTransmission() == 0) { + if (_wire->requestFrom(_addr, (uint8_t)3)) { + ; // Request 3 bytes + uint32_t data = _wire->read(); + data <<= 8; + data |= _wire->read(); + data <<= 8; + data |= _wire->read(); - // As per the data sheet for the RadSens - // Register 0x06 contains the reading in 0.1 * μR / h - float microRadPerHr = float(data) / 10.0; - return microRadPerHr; - } + // As per the data sheet for the RadSens + // Register 0x06 contains the reading in 0.1 * μR / h + float microRadPerHr = float(data) / 10.0; + return microRadPerHr; } - return -1.0; + } + return -1.0; } -bool CGRadSensSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - // Store the meansurement in the the appropriate fields of the protobuf - measurement->variant.environment_metrics.has_radiation = true; +bool CGRadSensSensor::getMetrics(meshtastic_Telemetry *measurement) { + // Store the meansurement in the the appropriate fields of the protobuf + measurement->variant.environment_metrics.has_radiation = true; - LOG_DEBUG("CGRADSENS getMetrics"); - measurement->variant.environment_metrics.radiation = getStaticRadiation(); + LOG_DEBUG("CGRADSENS getMetrics"); + measurement->variant.environment_metrics.radiation = getStaticRadiation(); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/CGRadSensSensor.h b/src/modules/Telemetry/Sensor/CGRadSensSensor.h index c677e8899..da5d36078 100644 --- a/src/modules/Telemetry/Sensor/CGRadSensSensor.h +++ b/src/modules/Telemetry/Sensor/CGRadSensSensor.h @@ -10,20 +10,19 @@ #include "TelemetrySensor.h" #include -class CGRadSensSensor : public TelemetrySensor -{ - private: - uint8_t _addr = 0x66; - TwoWire *_wire = &Wire; +class CGRadSensSensor : public TelemetrySensor { +private: + uint8_t _addr = 0x66; + TwoWire *_wire = &Wire; - protected: - void begin(TwoWire *wire = &Wire, uint8_t addr = 0x66); - float getStaticRadiation(); +protected: + void begin(TwoWire *wire = &Wire, uint8_t addr = 0x66); + float getStaticRadiation(); - public: - CGRadSensSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + CGRadSensSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/CurrentSensor.h b/src/modules/Telemetry/Sensor/CurrentSensor.h index 9827a9aa4..500485fa8 100644 --- a/src/modules/Telemetry/Sensor/CurrentSensor.h +++ b/src/modules/Telemetry/Sensor/CurrentSensor.h @@ -4,10 +4,9 @@ #pragma once -class CurrentSensor -{ - public: - virtual int16_t getCurrentMa() = 0; +class CurrentSensor { +public: + virtual int16_t getCurrentMa() = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp index 101b01f8f..b85dfd11b 100644 --- a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp +++ b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp @@ -10,46 +10,43 @@ DFRobotGravitySensor::DFRobotGravitySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_RAIN, "DFROBOT_RAIN") {} -DFRobotGravitySensor::~DFRobotGravitySensor() -{ - if (gravity) { +DFRobotGravitySensor::~DFRobotGravitySensor() { + if (gravity) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - delete gravity; + delete gravity; #pragma GCC diagnostic pop - gravity = nullptr; - } + gravity = nullptr; + } } -bool DFRobotGravitySensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool DFRobotGravitySensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - gravity = new DFRobot_RainfallSensor_I2C(bus); - status = gravity->begin(); + gravity = new DFRobot_RainfallSensor_I2C(bus); + status = gravity->begin(); - LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity->vid, gravity->pid, gravity->getFirmwareVersion().c_str()); + LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity->vid, gravity->pid, gravity->getFirmwareVersion().c_str()); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement) -{ - if (!gravity) { - LOG_ERROR("DFRobotGravitySensor not initialized"); - return false; - } +bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement) { + if (!gravity) { + LOG_ERROR("DFRobotGravitySensor not initialized"); + return false; + } - measurement->variant.environment_metrics.has_rainfall_1h = true; - measurement->variant.environment_metrics.has_rainfall_24h = true; + measurement->variant.environment_metrics.has_rainfall_1h = true; + measurement->variant.environment_metrics.has_rainfall_24h = true; - measurement->variant.environment_metrics.rainfall_1h = gravity->getRainfall(1); - measurement->variant.environment_metrics.rainfall_24h = gravity->getRainfall(24); + measurement->variant.environment_metrics.rainfall_1h = gravity->getRainfall(1); + measurement->variant.environment_metrics.rainfall_24h = gravity->getRainfall(24); - LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h); - LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h); - return true; + LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h); + LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h); + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h index 2b4890e30..ceada9cfc 100644 --- a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h +++ b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h @@ -11,16 +11,15 @@ #include #include -class DFRobotGravitySensor : public TelemetrySensor -{ - private: - DFRobot_RainfallSensor_I2C *gravity = nullptr; +class DFRobotGravitySensor : public TelemetrySensor { +private: + DFRobot_RainfallSensor_I2C *gravity = nullptr; - public: - DFRobotGravitySensor(); - ~DFRobotGravitySensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + DFRobotGravitySensor(); + ~DFRobotGravitySensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp index 2c2aeed6d..8af35e656 100644 --- a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp +++ b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp @@ -11,44 +11,42 @@ DFRobotLarkSensor::DFRobotLarkSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_LARK, "DFROBOT_LARK") {} -bool DFRobotLarkSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - lark = DFRobot_LarkWeatherStation_I2C(dev->address.address, bus); +bool DFRobotLarkSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + lark = DFRobot_LarkWeatherStation_I2C(dev->address.address, bus); - if (lark.begin() == 0) // DFRobotLarkSensor init - { - LOG_DEBUG("DFRobotLarkSensor Init Succeed"); - status = true; - } else { - LOG_ERROR("DFRobotLarkSensor Init Failed"); - status = false; - } - initI2CSensor(); - return status; + if (lark.begin() == 0) // DFRobotLarkSensor init + { + LOG_DEBUG("DFRobotLarkSensor Init Succeed"); + status = true; + } else { + LOG_ERROR("DFRobotLarkSensor Init Failed"); + status = false; + } + initI2CSensor(); + return status; } -bool DFRobotLarkSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.has_wind_speed = true; - measurement->variant.environment_metrics.has_wind_direction = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool DFRobotLarkSensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_wind_speed = true; + measurement->variant.environment_metrics.has_wind_direction = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.temperature = lark.getValue("Temp").toFloat(); - measurement->variant.environment_metrics.relative_humidity = lark.getValue("Humi").toFloat(); - measurement->variant.environment_metrics.wind_speed = lark.getValue("Speed").toFloat(); - measurement->variant.environment_metrics.wind_direction = GeoCoord::bearingToDegrees(lark.getValue("Dir").c_str()); - measurement->variant.environment_metrics.barometric_pressure = lark.getValue("Pressure").toFloat(); + measurement->variant.environment_metrics.temperature = lark.getValue("Temp").toFloat(); + measurement->variant.environment_metrics.relative_humidity = lark.getValue("Humi").toFloat(); + measurement->variant.environment_metrics.wind_speed = lark.getValue("Speed").toFloat(); + measurement->variant.environment_metrics.wind_direction = GeoCoord::bearingToDegrees(lark.getValue("Dir").c_str()); + measurement->variant.environment_metrics.barometric_pressure = lark.getValue("Pressure").toFloat(); - LOG_INFO("Temperature: %f", measurement->variant.environment_metrics.temperature); - LOG_INFO("Humidity: %f", measurement->variant.environment_metrics.relative_humidity); - LOG_INFO("Wind Speed: %f", measurement->variant.environment_metrics.wind_speed); - LOG_INFO("Wind Direction: %d", measurement->variant.environment_metrics.wind_direction); - LOG_INFO("Barometric Pressure: %f", measurement->variant.environment_metrics.barometric_pressure); + LOG_INFO("Temperature: %f", measurement->variant.environment_metrics.temperature); + LOG_INFO("Humidity: %f", measurement->variant.environment_metrics.relative_humidity); + LOG_INFO("Wind Speed: %f", measurement->variant.environment_metrics.wind_speed); + LOG_INFO("Wind Direction: %d", measurement->variant.environment_metrics.wind_direction); + LOG_INFO("Barometric Pressure: %f", measurement->variant.environment_metrics.barometric_pressure); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h index f3e4661a1..c1c1611a1 100644 --- a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h +++ b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h @@ -11,15 +11,14 @@ #include #include -class DFRobotLarkSensor : public TelemetrySensor -{ - private: - DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C(); +class DFRobotLarkSensor : public TelemetrySensor { +private: + DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C(); - public: - DFRobotLarkSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + DFRobotLarkSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/DPS310Sensor.cpp b/src/modules/Telemetry/Sensor/DPS310Sensor.cpp index 19e54aa4b..3ef67d794 100644 --- a/src/modules/Telemetry/Sensor/DPS310Sensor.cpp +++ b/src/modules/Telemetry/Sensor/DPS310Sensor.cpp @@ -9,36 +9,34 @@ DPS310Sensor::DPS310Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DPS310, "DPS310") {} -bool DPS310Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = dps310.begin_I2C(dev->address.address, bus); - if (!status) { - return status; - } - - dps310.configurePressure(DPS310_1HZ, DPS310_4SAMPLES); - dps310.configureTemperature(DPS310_1HZ, DPS310_4SAMPLES); - dps310.setMode(DPS310_CONT_PRESTEMP); - - initI2CSensor(); +bool DPS310Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = dps310.begin_I2C(dev->address.address, bus); + if (!status) { return status; + } + + dps310.configurePressure(DPS310_1HZ, DPS310_4SAMPLES); + dps310.configureTemperature(DPS310_1HZ, DPS310_4SAMPLES); + dps310.setMode(DPS310_CONT_PRESTEMP); + + initI2CSensor(); + return status; } -bool DPS310Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - sensors_event_t temp, press; +bool DPS310Sensor::getMetrics(meshtastic_Telemetry *measurement) { + sensors_event_t temp, press; - if (!dps310.getEvents(&temp, &press)) { - LOG_DEBUG("DPS310 getEvents no data"); - return false; - } + if (!dps310.getEvents(&temp, &press)) { + LOG_DEBUG("DPS310 getEvents no data"); + return false; + } - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.barometric_pressure = press.pressure; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.barometric_pressure = press.pressure; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DPS310Sensor.h b/src/modules/Telemetry/Sensor/DPS310Sensor.h index 4de8b2d1a..a62393011 100644 --- a/src/modules/Telemetry/Sensor/DPS310Sensor.h +++ b/src/modules/Telemetry/Sensor/DPS310Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class DPS310Sensor : public TelemetrySensor -{ - private: - Adafruit_DPS310 dps310; +class DPS310Sensor : public TelemetrySensor { +private: + Adafruit_DPS310 dps310; - public: - DPS310Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + DPS310Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA219Sensor.cpp b/src/modules/Telemetry/Sensor/INA219Sensor.cpp index d94afbc7c..12e2e9d88 100644 --- a/src/modules/Telemetry/Sensor/INA219Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA219Sensor.cpp @@ -13,41 +13,33 @@ INA219Sensor::INA219Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA219, "INA219") {} -int32_t INA219Sensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - if (!ina219.success()) { - ina219 = Adafruit_INA219(nodeTelemetrySensorsMap[sensorType].first); - status = ina219.begin(nodeTelemetrySensorsMap[sensorType].second); - } else { - status = ina219.success(); - } - return initI2CSensor(); +int32_t INA219Sensor::runOnce() { + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + if (!ina219.success()) { + ina219 = Adafruit_INA219(nodeTelemetrySensorsMap[sensorType].first); + status = ina219.begin(nodeTelemetrySensorsMap[sensorType].second); + } else { + status = ina219.success(); + } + return initI2CSensor(); } void INA219Sensor::setup() {} -bool INA219Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; +bool INA219Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - measurement->variant.environment_metrics.voltage = ina219.getBusVoltage_V(); - measurement->variant.environment_metrics.current = ina219.getCurrent_mA() * INA219_MULTIPLIER; - return true; + measurement->variant.environment_metrics.voltage = ina219.getBusVoltage_V(); + measurement->variant.environment_metrics.current = ina219.getCurrent_mA() * INA219_MULTIPLIER; + return true; } -uint16_t INA219Sensor::getBusVoltageMv() -{ - return lround(ina219.getBusVoltage_V() * 1000); -} +uint16_t INA219Sensor::getBusVoltageMv() { return lround(ina219.getBusVoltage_V() * 1000); } -int16_t INA219Sensor::getCurrentMa() -{ - return lround(ina219.getCurrent_mA()); -} +int16_t INA219Sensor::getCurrentMa() { return lround(ina219.getCurrent_mA()); } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA219Sensor.h b/src/modules/Telemetry/Sensor/INA219Sensor.h index 908366ce6..c35a9a5ab 100644 --- a/src/modules/Telemetry/Sensor/INA219Sensor.h +++ b/src/modules/Telemetry/Sensor/INA219Sensor.h @@ -8,20 +8,19 @@ #include "VoltageSensor.h" #include -class INA219Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor -{ - private: - Adafruit_INA219 ina219; +class INA219Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { +private: + Adafruit_INA219 ina219; - protected: - virtual void setup() override; +protected: + virtual void setup() override; - public: - INA219Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; +public: + INA219Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.cpp b/src/modules/Telemetry/Sensor/INA226Sensor.cpp index 6fa35598f..9cd74d921 100644 --- a/src/modules/Telemetry/Sensor/INA226Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA226Sensor.cpp @@ -9,76 +9,65 @@ INA226Sensor::INA226Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA226, "INA226") {} -int32_t INA226Sensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t INA226Sensor::runOnce() { + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first); + begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first); - if (!status) { - status = ina226.begin(); - } - return initI2CSensor(); + if (!status) { + status = ina226.begin(); + } + return initI2CSensor(); } void INA226Sensor::setup() {} -void INA226Sensor::begin(TwoWire *wire, uint8_t addr) -{ - _wire = wire; - _addr = addr; - ina226 = INA226(_addr, _wire); - _wire->begin(); - ina226.setMaxCurrentShunt(0.8, 0.100); +void INA226Sensor::begin(TwoWire *wire, uint8_t addr) { + _wire = wire; + _addr = addr; + ina226 = INA226(_addr, _wire); + _wire->begin(); + ina226.setMaxCurrentShunt(0.8, 0.100); } -bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - switch (measurement->which_variant) { - case meshtastic_Telemetry_environment_metrics_tag: - return getEnvironmentMetrics(measurement); +bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement) { + switch (measurement->which_variant) { + case meshtastic_Telemetry_environment_metrics_tag: + return getEnvironmentMetrics(measurement); - case meshtastic_Telemetry_power_metrics_tag: - return getPowerMetrics(measurement); - } + case meshtastic_Telemetry_power_metrics_tag: + return getPowerMetrics(measurement); + } - // unsupported metric - return false; + // unsupported metric + return false; } -bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; +bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - measurement->variant.environment_metrics.voltage = ina226.getBusVoltage(); - measurement->variant.environment_metrics.current = ina226.getCurrent_mA(); + measurement->variant.environment_metrics.voltage = ina226.getBusVoltage(); + measurement->variant.environment_metrics.current = ina226.getCurrent_mA(); - return true; + return true; } -bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.power_metrics.has_ch1_voltage = true; - measurement->variant.power_metrics.has_ch1_current = true; +bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.power_metrics.has_ch1_voltage = true; + measurement->variant.power_metrics.has_ch1_current = true; - measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage(); - measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA(); + measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage(); + measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA(); - return true; + return true; } -uint16_t INA226Sensor::getBusVoltageMv() -{ - return lround(ina226.getBusVoltage() * 1000); -} +uint16_t INA226Sensor::getBusVoltageMv() { return lround(ina226.getBusVoltage() * 1000); } -int16_t INA226Sensor::getCurrentMa() -{ - return lround(ina226.getCurrent_mA()); -} +int16_t INA226Sensor::getCurrentMa() { return lround(ina226.getCurrent_mA()); } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.h b/src/modules/Telemetry/Sensor/INA226Sensor.h index 51435550e..91972ba6f 100644 --- a/src/modules/Telemetry/Sensor/INA226Sensor.h +++ b/src/modules/Telemetry/Sensor/INA226Sensor.h @@ -8,26 +8,25 @@ #include "VoltageSensor.h" #include -class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor -{ - private: - uint8_t _addr = INA_ADDR; - TwoWire *_wire = &Wire; - INA226 ina226 = INA226(_addr, _wire); +class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { +private: + uint8_t _addr = INA_ADDR; + TwoWire *_wire = &Wire; + INA226 ina226 = INA226(_addr, _wire); - bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); - bool getPowerMetrics(meshtastic_Telemetry *measurement); + bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); + bool getPowerMetrics(meshtastic_Telemetry *measurement); - protected: - virtual void setup() override; - void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR); +protected: + virtual void setup() override; + void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR); - public: - INA226Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; +public: + INA226Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA260Sensor.cpp b/src/modules/Telemetry/Sensor/INA260Sensor.cpp index 9d9a99c00..f3b836799 100644 --- a/src/modules/Telemetry/Sensor/INA260Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA260Sensor.cpp @@ -9,35 +9,30 @@ INA260Sensor::INA260Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA260, "INA260") {} -int32_t INA260Sensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t INA260Sensor::runOnce() { + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - if (!status) { - status = ina260.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); - } - return initI2CSensor(); + if (!status) { + status = ina260.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + } + return initI2CSensor(); } void INA260Sensor::setup() {} -bool INA260Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; +bool INA260Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - // mV conversion to V - measurement->variant.environment_metrics.voltage = ina260.readBusVoltage() / 1000; - measurement->variant.environment_metrics.current = ina260.readCurrent(); - return true; + // mV conversion to V + measurement->variant.environment_metrics.voltage = ina260.readBusVoltage() / 1000; + measurement->variant.environment_metrics.current = ina260.readCurrent(); + return true; } -uint16_t INA260Sensor::getBusVoltageMv() -{ - return lround(ina260.readBusVoltage()); -} +uint16_t INA260Sensor::getBusVoltageMv() { return lround(ina260.readBusVoltage()); } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA260Sensor.h b/src/modules/Telemetry/Sensor/INA260Sensor.h index ea71c24e0..f1741b08e 100644 --- a/src/modules/Telemetry/Sensor/INA260Sensor.h +++ b/src/modules/Telemetry/Sensor/INA260Sensor.h @@ -7,19 +7,18 @@ #include "VoltageSensor.h" #include -class INA260Sensor : public TelemetrySensor, VoltageSensor -{ - private: - Adafruit_INA260 ina260 = Adafruit_INA260(); +class INA260Sensor : public TelemetrySensor, VoltageSensor { +private: + Adafruit_INA260 ina260 = Adafruit_INA260(); - protected: - virtual void setup() override; +protected: + virtual void setup() override; - public: - INA260Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; +public: + INA260Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp index 78081132a..dca62dce2 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp @@ -9,102 +9,90 @@ INA3221Sensor::INA3221Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA3221, "INA3221"){}; -int32_t INA3221Sensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - if (!status) { - ina3221.begin(nodeTelemetrySensorsMap[sensorType].second); - ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors - status = true; - } else { - status = true; - } - return initI2CSensor(); +int32_t INA3221Sensor::runOnce() { + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + if (!status) { + ina3221.begin(nodeTelemetrySensorsMap[sensorType].second); + ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors + status = true; + } else { + status = true; + } + return initI2CSensor(); }; void INA3221Sensor::setup() {} -struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch) -{ - struct _INA3221Measurement measurement; +struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch) { + struct _INA3221Measurement measurement; - measurement.voltage = ina3221.getVoltage(ch); - measurement.current = ina3221.getCurrent(ch); + measurement.voltage = ina3221.getVoltage(ch); + measurement.current = ina3221.getCurrent(ch); - return measurement; + return measurement; } -struct _INA3221Measurements INA3221Sensor::getMeasurements() -{ - struct _INA3221Measurements measurements; +struct _INA3221Measurements INA3221Sensor::getMeasurements() { + struct _INA3221Measurements measurements; - // INA3221 has 3 channels starting from 0 - for (int i = 0; i < 3; i++) { - measurements.measurements[i] = getMeasurement((ina3221_ch_t)i); - } + // INA3221 has 3 channels starting from 0 + for (int i = 0; i < 3; i++) { + measurements.measurements[i] = getMeasurement((ina3221_ch_t)i); + } - return measurements; + return measurements; } -bool INA3221Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - switch (measurement->which_variant) { - case meshtastic_Telemetry_environment_metrics_tag: - return getEnvironmentMetrics(measurement); +bool INA3221Sensor::getMetrics(meshtastic_Telemetry *measurement) { + switch (measurement->which_variant) { + case meshtastic_Telemetry_environment_metrics_tag: + return getEnvironmentMetrics(measurement); - case meshtastic_Telemetry_power_metrics_tag: - return getPowerMetrics(measurement); - } + case meshtastic_Telemetry_power_metrics_tag: + return getPowerMetrics(measurement); + } - // unsupported metric - return false; + // unsupported metric + return false; } -bool INA3221Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) -{ - struct _INA3221Measurement m = getMeasurement(ENV_CH); +bool INA3221Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) { + struct _INA3221Measurement m = getMeasurement(ENV_CH); - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - measurement->variant.environment_metrics.voltage = m.voltage; - measurement->variant.environment_metrics.current = m.current; + measurement->variant.environment_metrics.voltage = m.voltage; + measurement->variant.environment_metrics.current = m.current; - return true; + return true; } -bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) -{ - struct _INA3221Measurements m = getMeasurements(); +bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) { + struct _INA3221Measurements m = getMeasurements(); - measurement->variant.power_metrics.has_ch1_voltage = true; - measurement->variant.power_metrics.has_ch1_current = true; - measurement->variant.power_metrics.has_ch2_voltage = true; - measurement->variant.power_metrics.has_ch2_current = true; - measurement->variant.power_metrics.has_ch3_voltage = true; - measurement->variant.power_metrics.has_ch3_current = true; + measurement->variant.power_metrics.has_ch1_voltage = true; + measurement->variant.power_metrics.has_ch1_current = true; + measurement->variant.power_metrics.has_ch2_voltage = true; + measurement->variant.power_metrics.has_ch2_current = true; + measurement->variant.power_metrics.has_ch3_voltage = true; + measurement->variant.power_metrics.has_ch3_current = true; - measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage; - measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current; - measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage; - measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current; - measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage; - measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current; + measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage; + measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current; + measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage; + measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current; + measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage; + measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current; - return true; + return true; } -uint16_t INA3221Sensor::getBusVoltageMv() -{ - return lround(ina3221.getVoltage(BAT_CH) * 1000); -} +uint16_t INA3221Sensor::getBusVoltageMv() { return lround(ina3221.getVoltage(BAT_CH) * 1000); } -int16_t INA3221Sensor::getCurrentMa() -{ - return lround(ina3221.getCurrent(BAT_CH)); -} +int16_t INA3221Sensor::getCurrentMa() { return lround(ina3221.getCurrent(BAT_CH)); } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.h b/src/modules/Telemetry/Sensor/INA3221Sensor.h index 0581f92f6..5afc6c0dd 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.h +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.h @@ -16,45 +16,44 @@ #define INA3221_BAT_CH INA3221_CH1 #endif -class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor -{ - private: - INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); +class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { +private: + INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); - // channel to report voltage/current for environment metrics - static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; + // channel to report voltage/current for environment metrics + static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; - // channel to report battery voltage for device_battery_ina_address - static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; + // channel to report battery voltage for device_battery_ina_address + static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; - // get a single measurement for a channel - struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); + // get a single measurement for a channel + struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); - // get all measurements for all channels - struct _INA3221Measurements getMeasurements(); + // get all measurements for all channels + struct _INA3221Measurements getMeasurements(); - bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); - bool getPowerMetrics(meshtastic_Telemetry *measurement); + bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); + bool getPowerMetrics(meshtastic_Telemetry *measurement); - protected: - void setup() override; +protected: + void setup() override; - public: - INA3221Sensor(); - int32_t runOnce() override; - bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; +public: + INA3221Sensor(); + int32_t runOnce() override; + bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; struct _INA3221Measurement { - float voltage; - float current; + float voltage; + float current; }; struct _INA3221Measurements { - // INA3221 has 3 channels - struct _INA3221Measurement measurements[3]; + // INA3221 has 3 channels + struct _INA3221Measurement measurements[3]; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/IndicatorSensor.cpp b/src/modules/Telemetry/Sensor/IndicatorSensor.cpp index 26a4bc5fc..f6896bcb6 100644 --- a/src/modules/Telemetry/Sensor/IndicatorSensor.cpp +++ b/src/modules/Telemetry/Sensor/IndicatorSensor.cpp @@ -19,148 +19,144 @@ uint8_t data[SENSOR_BUF_SIZE]; // decode #define ACK_PKT_PARA "ACK" enum sensor_pkt_type { - PKT_TYPE_ACK = 0x00, // uin32_t - PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t - PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time - PKT_TYPE_CMD_BEEP_OFF = 0xA2, - PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t - PKT_TYPE_CMD_POWER_ON = 0xA4, - PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float - PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float - PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float - PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float - PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float - PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float + PKT_TYPE_ACK = 0x00, // uin32_t + PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t + PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time + PKT_TYPE_CMD_BEEP_OFF = 0xA2, + PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t + PKT_TYPE_CMD_POWER_ON = 0xA4, + PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float + PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float + PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float + PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float + PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float + PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float }; -static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len) -{ - uint8_t send_buf[32] = {0}; - uint8_t send_data[32] = {0}; - - if (len > 31) { - return -1; - } - - uint8_t index = 1; - - send_data[0] = cmd; - - if (len > 0 && p_data != NULL) { - memcpy(&send_data[1], p_data, len); - index += len; - } - cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index); - - // LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd); - - if (ret.status == COBS_ENCODE_OK) { - return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1); - } +static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len) { + uint8_t send_buf[32] = {0}; + uint8_t send_data[32] = {0}; + if (len > 31) { return -1; + } + + uint8_t index = 1; + + send_data[0] = cmd; + + if (len > 0 && p_data != NULL) { + memcpy(&send_data[1], p_data, len); + index += len; + } + cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index); + + // LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd); + + if (ret.status == COBS_ENCODE_OK) { + return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1); + } + + return -1; } -bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("%s: init", sensorName); - setup(); - return true; +bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("%s: init", sensorName); + setup(); + return true; } -void IndicatorSensor::setup() -{ - uart_config_t uart_config = { - .baud_rate = SENSOR_BAUD_RATE, - .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, - .stop_bits = UART_STOP_BITS_1, - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .source_clk = UART_SCLK_APB, - }; - int intr_alloc_flags = 0; - char buffer[11]; +void IndicatorSensor::setup() { + uart_config_t uart_config = { + .baud_rate = SENSOR_BAUD_RATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_APB, + }; + int intr_alloc_flags = 0; + char buffer[11]; - uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags); - uart_param_config(SENSOR_PORT_NUM, &uart_config); - uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0); - // measure and send only once every minute, for the phone API - const char *interval = ultoa(60000, buffer, 10); - cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1); + uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags); + uart_param_config(SENSOR_PORT_NUM, &uart_config); + uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0); + // measure and send only once every minute, for the phone API + const char *interval = ultoa(60000, buffer, 10); + cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1); } -bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - cobs_decode_result ret; - int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS); +bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement) { + cobs_decode_result ret; + int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS); - float value = 0.0; - uint8_t *p_buf_start = buf; - uint8_t *p_buf_end = buf; - if (len > 0) { - while (p_buf_start < (buf + len)) { - p_buf_end = p_buf_start; - while (p_buf_end < (buf + len)) { - if (*p_buf_end == 0x00) { - break; - } - p_buf_end++; - } - // decode buf - memset(data, 0, sizeof(data)); - ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start); - - // LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]); - - if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) { - - value = 0.0; - uint8_t pkt_type = data[0]; - switch (pkt_type) { - case PKT_TYPE_SENSOR_SCD41_CO2: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("CO2: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - break; - } - - case PKT_TYPE_SENSOR_AHT20_TEMP: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Temp: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = value; - break; - } - - case PKT_TYPE_SENSOR_AHT20_HUMIDITY: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Humidity: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.relative_humidity = value; - break; - } - - case PKT_TYPE_SENSOR_TVOC_INDEX: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Tvoc: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_iaq = true; - measurement->variant.environment_metrics.iaq = value; - break; - } - default: - break; - } - } - - p_buf_start = p_buf_end + 1; // next message + float value = 0.0; + uint8_t *p_buf_start = buf; + uint8_t *p_buf_end = buf; + if (len > 0) { + while (p_buf_start < (buf + len)) { + p_buf_end = p_buf_start; + while (p_buf_end < (buf + len)) { + if (*p_buf_end == 0x00) { + break; } - return true; + p_buf_end++; + } + // decode buf + memset(data, 0, sizeof(data)); + ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start); + + // LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]); + + if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) { + + value = 0.0; + uint8_t pkt_type = data[0]; + switch (pkt_type) { + case PKT_TYPE_SENSOR_SCD41_CO2: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("CO2: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + break; + } + + case PKT_TYPE_SENSOR_AHT20_TEMP: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("Temp: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = value; + break; + } + + case PKT_TYPE_SENSOR_AHT20_HUMIDITY: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("Humidity: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.relative_humidity = value; + break; + } + + case PKT_TYPE_SENSOR_TVOC_INDEX: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("Tvoc: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + measurement->variant.environment_metrics.has_iaq = true; + measurement->variant.environment_metrics.iaq = value; + break; + } + default: + break; + } + } + + p_buf_start = p_buf_end + 1; // next message } - return false; + return true; + } + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/IndicatorSensor.h b/src/modules/Telemetry/Sensor/IndicatorSensor.h index 22a0d9c83..2d29715ce 100644 --- a/src/modules/Telemetry/Sensor/IndicatorSensor.h +++ b/src/modules/Telemetry/Sensor/IndicatorSensor.h @@ -5,15 +5,14 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -class IndicatorSensor : public TelemetrySensor -{ - public: - IndicatorSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +class IndicatorSensor : public TelemetrySensor { +public: + IndicatorSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; - private: - void setup(); +private: + void setup(); }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp b/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp index 4ed78dcb0..23a46f964 100644 --- a/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp +++ b/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp @@ -10,32 +10,30 @@ LPS22HBSensor::LPS22HBSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LPS22, "LPS22HB") {} -bool LPS22HBSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = lps22hb.begin_I2C(dev->address.address, bus); - if (!status) { - return status; - } - lps22hb.setDataRate(LPS22_RATE_10_HZ); - - initI2CSensor(); +bool LPS22HBSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = lps22hb.begin_I2C(dev->address.address, bus); + if (!status) { return status; + } + lps22hb.setDataRate(LPS22_RATE_10_HZ); + + initI2CSensor(); + return status; } -bool LPS22HBSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool LPS22HBSensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - sensors_event_t temp; - sensors_event_t pressure; - lps22hb.getEvent(&pressure, &temp); + sensors_event_t temp; + sensors_event_t pressure; + lps22hb.getEvent(&pressure, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.barometric_pressure = pressure.pressure; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.barometric_pressure = pressure.pressure; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LPS22HBSensor.h b/src/modules/Telemetry/Sensor/LPS22HBSensor.h index 90b006fa2..043c10068 100644 --- a/src/modules/Telemetry/Sensor/LPS22HBSensor.h +++ b/src/modules/Telemetry/Sensor/LPS22HBSensor.h @@ -7,15 +7,14 @@ #include #include -class LPS22HBSensor : public TelemetrySensor -{ - private: - Adafruit_LPS22 lps22hb; +class LPS22HBSensor : public TelemetrySensor { +private: + Adafruit_LPS22 lps22hb; - public: - LPS22HBSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + LPS22HBSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp b/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp index cb7290fee..143b9b4d4 100644 --- a/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp +++ b/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp @@ -9,65 +9,60 @@ LTR390UVSensor::LTR390UVSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LTR390UV, "LTR390UV") {} -bool LTR390UVSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool LTR390UVSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - status = ltr390uv.begin(bus); - if (!status) { - return status; - } - - ltr390uv.setMode(LTR390_MODE_UVS); - ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default - ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default - - initI2CSensor(); + status = ltr390uv.begin(bus); + if (!status) { return status; + } + + ltr390uv.setMode(LTR390_MODE_UVS); + ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default + ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default + + initI2CSensor(); + return status; } -bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - LOG_DEBUG("LTR390UV getMetrics"); +bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement) { + LOG_DEBUG("LTR390UV getMetrics"); - // Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes. - if (ltr390uv.newDataAvailable()) { - measurement->variant.environment_metrics.has_lux = true; - measurement->variant.environment_metrics.has_uv_lux = true; + // Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes. + if (ltr390uv.newDataAvailable()) { + measurement->variant.environment_metrics.has_lux = true; + measurement->variant.environment_metrics.has_uv_lux = true; - if (ltr390uv.getMode() == LTR390_MODE_ALS) { - lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution - LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading); + if (ltr390uv.getMode() == LTR390_MODE_ALS) { + lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution + LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading); - measurement->variant.environment_metrics.lux = lastLuxReading; - measurement->variant.environment_metrics.uv_lux = lastUVReading; + measurement->variant.environment_metrics.lux = lastLuxReading; + measurement->variant.environment_metrics.uv_lux = lastUVReading; - ltr390uv.setGain( - LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain - ltr390uv.setMode(LTR390_MODE_UVS); + ltr390uv.setGain(LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain + ltr390uv.setMode(LTR390_MODE_UVS); - return true; + return true; - } else if (ltr390uv.getMode() == LTR390_MODE_UVS) { - lastUVReading = ltr390uv.readUVS() / - 2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution - LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading); + } else if (ltr390uv.getMode() == LTR390_MODE_UVS) { + lastUVReading = ltr390uv.readUVS() / 2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution + LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading); - measurement->variant.environment_metrics.lux = lastLuxReading; - measurement->variant.environment_metrics.uv_lux = lastUVReading; + measurement->variant.environment_metrics.lux = lastLuxReading; + measurement->variant.environment_metrics.uv_lux = lastUVReading; - ltr390uv.setGain( - LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it - ltr390uv.setMode(LTR390_MODE_ALS); + ltr390uv.setGain(LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it + ltr390uv.setMode(LTR390_MODE_ALS); - return true; - } + return true; } + } - // In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false - measurement->variant.environment_metrics.has_lux = false; - measurement->variant.environment_metrics.has_uv_lux = false; + // In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false + measurement->variant.environment_metrics.has_lux = false; + measurement->variant.environment_metrics.has_uv_lux = false; - return false; + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LTR390UVSensor.h b/src/modules/Telemetry/Sensor/LTR390UVSensor.h index e12d17274..a93eaf7d4 100644 --- a/src/modules/Telemetry/Sensor/LTR390UVSensor.h +++ b/src/modules/Telemetry/Sensor/LTR390UVSensor.h @@ -6,17 +6,16 @@ #include "TelemetrySensor.h" #include -class LTR390UVSensor : public TelemetrySensor -{ - private: - Adafruit_LTR390 ltr390uv = Adafruit_LTR390(); - float lastLuxReading = 0; - float lastUVReading = 0; +class LTR390UVSensor : public TelemetrySensor { +private: + Adafruit_LTR390 ltr390uv = Adafruit_LTR390(); + float lastLuxReading = 0; + float lastUVReading = 0; - public: - LTR390UVSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + LTR390UVSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp index 1a6792d3a..da2bc2569 100644 --- a/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp @@ -2,12 +2,11 @@ #if !MESHTASTIC_EXCLUDE_I2C && __has_include() -MAX17048Singleton *MAX17048Singleton::GetInstance() -{ - if (pinstance == nullptr) { - pinstance = new MAX17048Singleton(); - } - return pinstance; +MAX17048Singleton *MAX17048Singleton::GetInstance() { + if (pinstance == nullptr) { + pinstance = new MAX17048Singleton(); + } + return pinstance; } MAX17048Singleton::MAX17048Singleton() {} @@ -16,160 +15,147 @@ MAX17048Singleton::~MAX17048Singleton() {} MAX17048Singleton *MAX17048Singleton::pinstance{nullptr}; -bool MAX17048Singleton::runOnce(TwoWire *theWire) -{ - initialized = begin(theWire); - LOG_DEBUG("%s::runOnce %s", sensorStr, initialized ? "began ok" : "begin failed"); - return initialized; +bool MAX17048Singleton::runOnce(TwoWire *theWire) { + initialized = begin(theWire); + LOG_DEBUG("%s::runOnce %s", sensorStr, initialized ? "began ok" : "begin failed"); + return initialized; } -bool MAX17048Singleton::isBatteryCharging() -{ - float volts = cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("%s::isBatteryCharging not connected", sensorStr); - return 0; - } +bool MAX17048Singleton::isBatteryCharging() { + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::isBatteryCharging not connected", sensorStr); + return 0; + } - MAX17048ChargeSample sample; - sample.chargeRate = chargeRate(); // charge/discharge rate in percent/hr - sample.cellPercent = cellPercent(); // state of charge in percent 0 to 100 - chargeSamples.push(sample); // save a sample into a fifo buffer + MAX17048ChargeSample sample; + sample.chargeRate = chargeRate(); // charge/discharge rate in percent/hr + sample.cellPercent = cellPercent(); // state of charge in percent 0 to 100 + chargeSamples.push(sample); // save a sample into a fifo buffer - // Keep the fifo buffer trimmed - while (chargeSamples.size() > MAX17048_CHARGING_SAMPLES) - chargeSamples.pop(); + // Keep the fifo buffer trimmed + while (chargeSamples.size() > MAX17048_CHARGING_SAMPLES) + chargeSamples.pop(); - // Based on the past n samples, is the lipo charging, discharging or idle - if (chargeSamples.front().chargeRate > MAX17048_CHARGING_MINIMUM_RATE && - chargeSamples.back().chargeRate > MAX17048_CHARGING_MINIMUM_RATE) { - if (chargeSamples.front().cellPercent > chargeSamples.back().cellPercent) - chargeState = MAX17048ChargeState::EXPORT; - else if (chargeSamples.front().cellPercent < chargeSamples.back().cellPercent) - chargeState = MAX17048ChargeState::IMPORT; - else - chargeState = MAX17048ChargeState::IDLE; - } else { - chargeState = MAX17048ChargeState::IDLE; - } + // Based on the past n samples, is the lipo charging, discharging or idle + if (chargeSamples.front().chargeRate > MAX17048_CHARGING_MINIMUM_RATE && chargeSamples.back().chargeRate > MAX17048_CHARGING_MINIMUM_RATE) { + if (chargeSamples.front().cellPercent > chargeSamples.back().cellPercent) + chargeState = MAX17048ChargeState::EXPORT; + else if (chargeSamples.front().cellPercent < chargeSamples.back().cellPercent) + chargeState = MAX17048ChargeState::IMPORT; + else + chargeState = MAX17048ChargeState::IDLE; + } else { + chargeState = MAX17048ChargeState::IDLE; + } - LOG_DEBUG("%s::isBatteryCharging %s volts: %.3f soc: %.3f rate: %.3f", sensorStr, chargeLabels[chargeState], volts, - sample.cellPercent, sample.chargeRate); - return chargeState == MAX17048ChargeState::IMPORT; + LOG_DEBUG("%s::isBatteryCharging %s volts: %.3f soc: %.3f rate: %.3f", sensorStr, chargeLabels[chargeState], volts, sample.cellPercent, + sample.chargeRate); + return chargeState == MAX17048ChargeState::IMPORT; } -uint16_t MAX17048Singleton::getBusVoltageMv() -{ - float volts = cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("%s::getBusVoltageMv is not connected", sensorStr); - return 0; - } - LOG_DEBUG("%s::getBusVoltageMv %.3fmV", sensorStr, volts); - return (uint16_t)(volts * 1000.0f); +uint16_t MAX17048Singleton::getBusVoltageMv() { + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::getBusVoltageMv is not connected", sensorStr); + return 0; + } + LOG_DEBUG("%s::getBusVoltageMv %.3fmV", sensorStr, volts); + return (uint16_t)(volts * 1000.0f); } -uint8_t MAX17048Singleton::getBusBatteryPercent() -{ - float soc = cellPercent(); - LOG_DEBUG("%s::getBusBatteryPercent %.1f%%", sensorStr, soc); - return clamp(static_cast(round(soc)), static_cast(0), static_cast(100)); +uint8_t MAX17048Singleton::getBusBatteryPercent() { + float soc = cellPercent(); + LOG_DEBUG("%s::getBusBatteryPercent %.1f%%", sensorStr, soc); + return clamp(static_cast(round(soc)), static_cast(0), static_cast(100)); } -uint16_t MAX17048Singleton::getTimeToGoSecs() -{ - float rate = chargeRate(); // charge/discharge rate in percent/hr - float soc = cellPercent(); // state of charge in percent 0 to 100 - soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% - float ttg = ((100.0f - soc) / rate) * 3600.0f; // calculate seconds to charge/discharge - LOG_DEBUG("%s::getTimeToGoSecs %.0f seconds", sensorStr, ttg); - return (uint16_t)ttg; +uint16_t MAX17048Singleton::getTimeToGoSecs() { + float rate = chargeRate(); // charge/discharge rate in percent/hr + float soc = cellPercent(); // state of charge in percent 0 to 100 + soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% + float ttg = ((100.0f - soc) / rate) * 3600.0f; // calculate seconds to charge/discharge + LOG_DEBUG("%s::getTimeToGoSecs %.0f seconds", sensorStr, ttg); + return (uint16_t)ttg; } -bool MAX17048Singleton::isBatteryConnected() -{ - float volts = cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("%s::isBatteryConnected is not connected", sensorStr); - return false; - } +bool MAX17048Singleton::isBatteryConnected() { + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::isBatteryConnected is not connected", sensorStr); + return false; + } - // if a valid voltage is returned, then battery must be connected + // if a valid voltage is returned, then battery must be connected + return true; +} + +bool MAX17048Singleton::isExternallyPowered() { + float volts = cellVoltage(); + if (isnan(volts)) { + // if the battery is not connected then there must be external power + LOG_DEBUG("%s::isExternallyPowered battery is", sensorStr); return true; -} - -bool MAX17048Singleton::isExternallyPowered() -{ - float volts = cellVoltage(); - if (isnan(volts)) { - // if the battery is not connected then there must be external power - LOG_DEBUG("%s::isExternallyPowered battery is", sensorStr); - return true; - } - // if the bus voltage is over MAX17048_BUS_POWER_VOLTS, then the external power - // is assumed to be connected - LOG_DEBUG("%s::isExternallyPowered %s connected", sensorStr, volts >= MAX17048_BUS_POWER_VOLTS ? "is" : "is not"); - return volts >= MAX17048_BUS_POWER_VOLTS; + } + // if the bus voltage is over MAX17048_BUS_POWER_VOLTS, then the external power + // is assumed to be connected + LOG_DEBUG("%s::isExternallyPowered %s connected", sensorStr, volts >= MAX17048_BUS_POWER_VOLTS ? "is" : "is not"); + return volts >= MAX17048_BUS_POWER_VOLTS; } #if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY)) MAX17048Sensor::MAX17048Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX17048, "MAX17048") {} -int32_t MAX17048Sensor::runOnce() -{ - if (isInitialized()) { - LOG_INFO("Init sensor: %s is already initialised", sensorName); - return true; - } +int32_t MAX17048Sensor::runOnce() { + if (isInitialized()) { + LOG_INFO("Init sensor: %s is already initialised", sensorName); + return true; + } - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - // Get a singleton instance and initialise the max17048 - if (max17048 == nullptr) { - max17048 = MAX17048Singleton::GetInstance(); - } - status = max17048->runOnce(nodeTelemetrySensorsMap[sensorType].second); - return initI2CSensor(); + // Get a singleton instance and initialise the max17048 + if (max17048 == nullptr) { + max17048 = MAX17048Singleton::GetInstance(); + } + status = max17048->runOnce(nodeTelemetrySensorsMap[sensorType].second); + return initI2CSensor(); } void MAX17048Sensor::setup() {} -bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - LOG_DEBUG("MAX17048 getMetrics id: %i", measurement->which_variant); +bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement) { + LOG_DEBUG("MAX17048 getMetrics id: %i", measurement->which_variant); - float volts = max17048->cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("MAX17048 getMetrics battery is not connected"); - return false; - } + float volts = max17048->cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("MAX17048 getMetrics battery is not connected"); + return false; + } - float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr - float soc = max17048->cellPercent(); // state of charge in percent 0 to 100 - soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% - float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge + float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr + float soc = max17048->cellPercent(); // state of charge in percent 0 to 100 + soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% + float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge - LOG_DEBUG("MAX17048 getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours", volts, soc, ttg); - if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) { - measurement->variant.power_metrics.has_ch1_voltage = true; - measurement->variant.power_metrics.ch1_voltage = volts; - } else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) { - measurement->variant.device_metrics.has_battery_level = true; - measurement->variant.device_metrics.has_voltage = true; - measurement->variant.device_metrics.battery_level = static_cast(round(soc)); - measurement->variant.device_metrics.voltage = volts; - } - return true; + LOG_DEBUG("MAX17048 getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours", volts, soc, ttg); + if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) { + measurement->variant.power_metrics.has_ch1_voltage = true; + measurement->variant.power_metrics.ch1_voltage = volts; + } else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) { + measurement->variant.device_metrics.has_battery_level = true; + measurement->variant.device_metrics.has_voltage = true; + measurement->variant.device_metrics.battery_level = static_cast(round(soc)); + measurement->variant.device_metrics.voltage = volts; + } + return true; } -uint16_t MAX17048Sensor::getBusVoltageMv() -{ - return max17048->getBusVoltageMv(); -}; +uint16_t MAX17048Sensor::getBusVoltageMv() { return max17048->getBusVoltageMv(); }; #endif diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.h b/src/modules/Telemetry/Sensor/MAX17048Sensor.h index d27169406..82b32c448 100644 --- a/src/modules/Telemetry/Sensor/MAX17048Sensor.h +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.h @@ -25,83 +25,81 @@ #include struct MAX17048ChargeSample { - float cellPercent; - float chargeRate; + float cellPercent; + float chargeRate; }; enum MAX17048ChargeState { IDLE, EXPORT, IMPORT }; // Singleton wrapper for the Adafruit_MAX17048 class -class MAX17048Singleton : public Adafruit_MAX17048 -{ - private: - static MAX17048Singleton *pinstance; - bool initialized = false; - std::queue chargeSamples; - MAX17048ChargeState chargeState = IDLE; - const String chargeLabels[3] = {F("idle"), F("export"), F("import")}; - const char *sensorStr = "MAX17048Sensor"; +class MAX17048Singleton : public Adafruit_MAX17048 { +private: + static MAX17048Singleton *pinstance; + bool initialized = false; + std::queue chargeSamples; + MAX17048ChargeState chargeState = IDLE; + const String chargeLabels[3] = {F("idle"), F("export"), F("import")}; + const char *sensorStr = "MAX17048Sensor"; - protected: - MAX17048Singleton(); - ~MAX17048Singleton(); +protected: + MAX17048Singleton(); + ~MAX17048Singleton(); - public: - // Create a singleton instance (not thread safe) - static MAX17048Singleton *GetInstance(); +public: + // Create a singleton instance (not thread safe) + static MAX17048Singleton *GetInstance(); - // Singletons should not be cloneable. - MAX17048Singleton(MAX17048Singleton &other) = delete; + // Singletons should not be cloneable. + MAX17048Singleton(MAX17048Singleton &other) = delete; - // Singletons should not be assignable. - void operator=(const MAX17048Singleton &) = delete; + // Singletons should not be assignable. + void operator=(const MAX17048Singleton &) = delete; - // Initialise the sensor (not thread safe) - virtual bool runOnce(TwoWire *theWire = &Wire); + // Initialise the sensor (not thread safe) + virtual bool runOnce(TwoWire *theWire = &Wire); - // Get the current bus voltage - uint16_t getBusVoltageMv(); + // Get the current bus voltage + uint16_t getBusVoltageMv(); - // Get the state of charge in percent 0 to 100 - uint8_t getBusBatteryPercent(); + // Get the state of charge in percent 0 to 100 + uint8_t getBusBatteryPercent(); - // Calculate the seconds to charge/discharge - uint16_t getTimeToGoSecs(); + // Calculate the seconds to charge/discharge + uint16_t getTimeToGoSecs(); - // Returns true if the battery sensor has started - inline virtual bool isInitialised() { return initialized; }; + // Returns true if the battery sensor has started + inline virtual bool isInitialised() { return initialized; }; - // Returns true if the battery is currently on charge (not thread safe) - bool isBatteryCharging(); + // Returns true if the battery is currently on charge (not thread safe) + bool isBatteryCharging(); - // Returns true if a battery is actually connected - bool isBatteryConnected(); + // Returns true if a battery is actually connected + bool isBatteryConnected(); - // Returns true if there is bus or external power connected - bool isExternallyPowered(); + // Returns true if there is bus or external power connected + bool isExternallyPowered(); }; #if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY)) -class MAX17048Sensor : public TelemetrySensor, VoltageSensor -{ - private: - MAX17048Singleton *max17048 = nullptr; +class MAX17048Sensor : public TelemetrySensor, VoltageSensor { +private: + MAX17048Singleton *max17048 = nullptr; - protected: - virtual void setup() override; +protected: + virtual void setup() override; - public: - MAX17048Sensor(); +public: + MAX17048Sensor(); - // Initialise the sensor - virtual int32_t runOnce() override; + // Initialise the sensor + virtual int32_t runOnce() override; - // Get the current bus voltage and state of charge - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + // Get the current bus voltage and state of charge + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - // Get the current bus voltage - virtual uint16_t getBusVoltageMv() override; + // Get the current bus voltage + virtual uint16_t getBusVoltageMv() override; }; #endif diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp index ceca4be5e..4df0418c2 100644 --- a/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp @@ -9,75 +9,71 @@ MAX30102Sensor::MAX30102Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX30102, "MAX30102") {} -int32_t MAX30102Sensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t MAX30102Sensor::runOnce() { + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - if (max30102.begin(*nodeTelemetrySensorsMap[sensorType].second, _speed, nodeTelemetrySensorsMap[sensorType].first) == - true) // MAX30102 init - { - byte brightness = 60; // 0=Off to 255=50mA - byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32 - byte leds = 2; // 1 = Red only, 2 = Red + IR - byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200 - int pulseWidth = 411; // 69, 118, 215, 411 - int adcRange = 4096; // 2048, 4096, 8192, 16384 + if (max30102.begin(*nodeTelemetrySensorsMap[sensorType].second, _speed, nodeTelemetrySensorsMap[sensorType].first) == true) // MAX30102 init + { + byte brightness = 60; // 0=Off to 255=50mA + byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32 + byte leds = 2; // 1 = Red only, 2 = Red + IR + byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200 + int pulseWidth = 411; // 69, 118, 215, 411 + int adcRange = 4096; // 2048, 4096, 8192, 16384 - max30102.enableDIETEMPRDY(); // Enable the temperature ready interrupt - max30102.setup(brightness, sampleAverage, leds, sampleRate, pulseWidth, adcRange); - LOG_DEBUG("MAX30102 Init Succeed"); - status = true; - } else { - LOG_ERROR("MAX30102 Init Failed"); - status = false; - } - return initI2CSensor(); + max30102.enableDIETEMPRDY(); // Enable the temperature ready interrupt + max30102.setup(brightness, sampleAverage, leds, sampleRate, pulseWidth, adcRange); + LOG_DEBUG("MAX30102 Init Succeed"); + status = true; + } else { + LOG_ERROR("MAX30102 Init Failed"); + status = false; + } + return initI2CSensor(); } void MAX30102Sensor::setup() {} -bool MAX30102Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - uint32_t ir_buff[MAX30102_BUFFER_LEN]; - uint32_t red_buff[MAX30102_BUFFER_LEN]; - int32_t spo2; - int8_t spo2_valid; - int32_t heart_rate; - int8_t heart_rate_valid; - float temp = max30102.readTemperature(); +bool MAX30102Sensor::getMetrics(meshtastic_Telemetry *measurement) { + uint32_t ir_buff[MAX30102_BUFFER_LEN]; + uint32_t red_buff[MAX30102_BUFFER_LEN]; + int32_t spo2; + int8_t spo2_valid; + int32_t heart_rate; + int8_t heart_rate_valid; + float temp = max30102.readTemperature(); - measurement->variant.environment_metrics.temperature = temp; - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.health_metrics.temperature = temp; - measurement->variant.health_metrics.has_temperature = true; - for (byte i = 0; i < MAX30102_BUFFER_LEN; i++) { - while (max30102.available() == false) - max30102.check(); + measurement->variant.environment_metrics.temperature = temp; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.health_metrics.temperature = temp; + measurement->variant.health_metrics.has_temperature = true; + for (byte i = 0; i < MAX30102_BUFFER_LEN; i++) { + while (max30102.available() == false) + max30102.check(); - red_buff[i] = max30102.getRed(); - ir_buff[i] = max30102.getIR(); - max30102.nextSample(); - } + red_buff[i] = max30102.getRed(); + ir_buff[i] = max30102.getIR(); + max30102.nextSample(); + } - maxim_heart_rate_and_oxygen_saturation(ir_buff, MAX30102_BUFFER_LEN, red_buff, &spo2, &spo2_valid, &heart_rate, - &heart_rate_valid); - LOG_DEBUG("heart_rate=%d(%d), sp02=%d(%d)", heart_rate, heart_rate_valid, spo2, spo2_valid); - if (heart_rate_valid) { - measurement->variant.health_metrics.has_heart_bpm = true; - measurement->variant.health_metrics.heart_bpm = heart_rate; - } else { - measurement->variant.health_metrics.has_heart_bpm = false; - } - if (spo2_valid) { - measurement->variant.health_metrics.has_spO2 = true; - measurement->variant.health_metrics.spO2 = spo2; - } else { - measurement->variant.health_metrics.has_spO2 = true; - } - return true; + maxim_heart_rate_and_oxygen_saturation(ir_buff, MAX30102_BUFFER_LEN, red_buff, &spo2, &spo2_valid, &heart_rate, &heart_rate_valid); + LOG_DEBUG("heart_rate=%d(%d), sp02=%d(%d)", heart_rate, heart_rate_valid, spo2, spo2_valid); + if (heart_rate_valid) { + measurement->variant.health_metrics.has_heart_bpm = true; + measurement->variant.health_metrics.heart_bpm = heart_rate; + } else { + measurement->variant.health_metrics.has_heart_bpm = false; + } + if (spo2_valid) { + measurement->variant.health_metrics.has_spO2 = true; + measurement->variant.health_metrics.spO2 = spo2; + } else { + measurement->variant.health_metrics.has_spO2 = true; + } + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.h b/src/modules/Telemetry/Sensor/MAX30102Sensor.h index 9981d4006..9a6b8b5b4 100644 --- a/src/modules/Telemetry/Sensor/MAX30102Sensor.h +++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.h @@ -8,19 +8,18 @@ #define MAX30102_BUFFER_LEN 100 -class MAX30102Sensor : public TelemetrySensor -{ - private: - MAX30105 max30102 = MAX30105(); - uint32_t _speed = 200000UL; +class MAX30102Sensor : public TelemetrySensor { +private: + MAX30105 max30102 = MAX30105(); + uint32_t _speed = 200000UL; - protected: - virtual void setup() override; +protected: + virtual void setup() override; - public: - MAX30102Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +public: + MAX30102Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp b/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp index c93d6a927..8fe342882 100644 --- a/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp @@ -9,26 +9,24 @@ MCP9808Sensor::MCP9808Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MCP9808, "MCP9808") {} -bool MCP9808Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = mcp9808.begin(dev->address.address, bus); - if (!status) { - return status; - } - mcp9808.setResolution(2); - - initI2CSensor(); +bool MCP9808Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = mcp9808.begin(dev->address.address, bus); + if (!status) { return status; + } + mcp9808.setResolution(2); + + initI2CSensor(); + return status; } -bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; +bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; - LOG_DEBUG("MCP9808 getMetrics"); - measurement->variant.environment_metrics.temperature = mcp9808.readTempC(); - return true; + LOG_DEBUG("MCP9808 getMetrics"); + measurement->variant.environment_metrics.temperature = mcp9808.readTempC(); + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MCP9808Sensor.h b/src/modules/Telemetry/Sensor/MCP9808Sensor.h index cef7a48c2..2e90a88e5 100644 --- a/src/modules/Telemetry/Sensor/MCP9808Sensor.h +++ b/src/modules/Telemetry/Sensor/MCP9808Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class MCP9808Sensor : public TelemetrySensor -{ - private: - Adafruit_MCP9808 mcp9808; +class MCP9808Sensor : public TelemetrySensor { +private: + Adafruit_MCP9808 mcp9808; - public: - MCP9808Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + MCP9808Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp index 9661b59c2..5a1abe336 100644 --- a/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp @@ -7,38 +7,36 @@ #include "TelemetrySensor.h" MLX90614Sensor::MLX90614Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90614, "MLX90614") {} -int32_t MLX90614Sensor::runOnce() -{ - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t MLX90614Sensor::runOnce() { + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second) == true) // MLX90614 init - { - LOG_DEBUG("MLX90614 emissivity: %f", mlx.readEmissivity()); - if (fabs(MLX90614_EMISSIVITY - mlx.readEmissivity()) > 0.001) { - mlx.writeEmissivity(MLX90614_EMISSIVITY); - LOG_INFO("MLX90614 emissivity updated. In case of weird data, power cycle"); - } - LOG_DEBUG("MLX90614 Init Succeed"); - status = true; - } else { - LOG_ERROR("MLX90614 Init Failed"); - status = false; + if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second) == true) // MLX90614 init + { + LOG_DEBUG("MLX90614 emissivity: %f", mlx.readEmissivity()); + if (fabs(MLX90614_EMISSIVITY - mlx.readEmissivity()) > 0.001) { + mlx.writeEmissivity(MLX90614_EMISSIVITY); + LOG_INFO("MLX90614 emissivity updated. In case of weird data, power cycle"); } - return initI2CSensor(); + LOG_DEBUG("MLX90614 Init Succeed"); + status = true; + } else { + LOG_ERROR("MLX90614 Init Failed"); + status = false; + } + return initI2CSensor(); } void MLX90614Sensor::setup() {} -bool MLX90614Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.temperature = mlx.readAmbientTempC(); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.health_metrics.temperature = mlx.readObjectTempC(); - measurement->variant.health_metrics.has_temperature = true; - return true; +bool MLX90614Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.temperature = mlx.readAmbientTempC(); + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.health_metrics.temperature = mlx.readObjectTempC(); + measurement->variant.health_metrics.has_temperature = true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.h b/src/modules/Telemetry/Sensor/MLX90614Sensor.h index c2571027e..a667d08d7 100644 --- a/src/modules/Telemetry/Sensor/MLX90614Sensor.h +++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.h @@ -7,18 +7,17 @@ #define MLX90614_EMISSIVITY 0.98 // human skin -class MLX90614Sensor : public TelemetrySensor -{ - private: - Adafruit_MLX90614 mlx = Adafruit_MLX90614(); +class MLX90614Sensor : public TelemetrySensor { +private: + Adafruit_MLX90614 mlx = Adafruit_MLX90614(); - protected: - virtual void setup() override; +protected: + virtual void setup() override; - public: - MLX90614Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +public: + MLX90614Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp b/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp index eb84edffc..b87f4a098 100644 --- a/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp @@ -8,29 +8,27 @@ MLX90632Sensor::MLX90632Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90632, "MLX90632") {} -bool MLX90632Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool MLX90632Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - MLX90632::status returnError; - if (mlx.begin(dev->address.address, *bus, returnError) == true) // MLX90632 init - { - LOG_DEBUG("MLX90632 Init Succeed"); - status = true; - } else { - LOG_ERROR("MLX90632 Init Failed"); - status = false; - } - initI2CSensor(); - return status; + MLX90632::status returnError; + if (mlx.begin(dev->address.address, *bus, returnError) == true) // MLX90632 init + { + LOG_DEBUG("MLX90632 Init Succeed"); + status = true; + } else { + LOG_ERROR("MLX90632 Init Failed"); + status = false; + } + initI2CSensor(); + return status; } -bool MLX90632Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = mlx.getObjectTemp(); // Get the object temperature in Fahrenheit +bool MLX90632Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = mlx.getObjectTemp(); // Get the object temperature in Fahrenheit - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MLX90632Sensor.h b/src/modules/Telemetry/Sensor/MLX90632Sensor.h index 566db8319..e6eeaf38e 100644 --- a/src/modules/Telemetry/Sensor/MLX90632Sensor.h +++ b/src/modules/Telemetry/Sensor/MLX90632Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class MLX90632Sensor : public TelemetrySensor -{ - private: - MLX90632 mlx = MLX90632(); +class MLX90632Sensor : public TelemetrySensor { +private: + MLX90632 mlx = MLX90632(); - public: - MLX90632Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + MLX90632Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp index e67b78145..fda5c3554 100644 --- a/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp +++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp @@ -16,128 +16,121 @@ meshtastic_Nau7802Config nau7802config = meshtastic_Nau7802Config_init_zero; NAU7802Sensor::NAU7802Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_NAU7802, "NAU7802") {} -bool NAU7802Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = nau7802.begin(*bus); - if (!status) { - return status; - } - nau7802.setSampleRate(NAU7802_SPS_320); - if (!loadCalibrationData()) { - LOG_ERROR("Failed to load calibration data"); - } - nau7802.calibrateAFE(); - LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); - initI2CSensor(); +bool NAU7802Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = nau7802.begin(*bus); + if (!status) { return status; + } + nau7802.setSampleRate(NAU7802_SPS_320); + if (!loadCalibrationData()) { + LOG_ERROR("Failed to load calibration data"); + } + nau7802.calibrateAFE(); + LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); + initI2CSensor(); + return status; } -bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - LOG_DEBUG("NAU7802 getMetrics"); - nau7802.powerUp(); - // Wait for the sensor to become ready for one second max - uint32_t start = millis(); - while (!nau7802.available()) { - delay(100); - if (!Throttle::isWithinTimespanMs(start, 1000)) { - nau7802.powerDown(); - return false; - } +bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement) { + LOG_DEBUG("NAU7802 getMetrics"); + nau7802.powerUp(); + // Wait for the sensor to become ready for one second max + uint32_t start = millis(); + while (!nau7802.available()) { + delay(100); + if (!Throttle::isWithinTimespanMs(start, 1000)) { + nau7802.powerDown(); + return false; } - measurement->variant.environment_metrics.has_weight = true; - // Check if we have correct calibration values after powerup - LOG_DEBUG("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); - measurement->variant.environment_metrics.weight = nau7802.getWeight() / 1000; // sample is in kg - nau7802.powerDown(); - return true; + } + measurement->variant.environment_metrics.has_weight = true; + // Check if we have correct calibration values after powerup + LOG_DEBUG("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); + measurement->variant.environment_metrics.weight = nau7802.getWeight() / 1000; // sample is in kg + nau7802.powerDown(); + return true; } -void NAU7802Sensor::calibrate(float weight) -{ - nau7802.calculateCalibrationFactor(weight * 1000, 64); // internal sample is in grams - if (!saveCalibrationData()) { - LOG_WARN("Failed to save calibration data"); - } - LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); +void NAU7802Sensor::calibrate(float weight) { + nau7802.calculateCalibrationFactor(weight * 1000, 64); // internal sample is in grams + if (!saveCalibrationData()) { + LOG_WARN("Failed to save calibration data"); + } + LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); } AdminMessageHandleResult NAU7802Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) -{ - AdminMessageHandleResult result; + meshtastic_AdminMessage *response) { + AdminMessageHandleResult result; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_set_scale_tag: - if (request->set_scale == 0) { - this->tare(); - LOG_DEBUG("Client requested to tare scale"); - } else { - this->calibrate(request->set_scale); - LOG_DEBUG("Client requested to calibrate to %d kg", request->set_scale); - } - result = AdminMessageHandleResult::HANDLED; - break; - - default: - result = AdminMessageHandleResult::NOT_HANDLED; - } - - return result; -} - -void NAU7802Sensor::tare() -{ - nau7802.calculateZeroOffset(64); - if (!saveCalibrationData()) { - LOG_WARN("Failed to save calibration data"); - } - LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); -} - -bool NAU7802Sensor::saveCalibrationData() -{ - auto file = SafeFile(nau7802ConfigFileName); - nau7802config.zeroOffset = nau7802.getZeroOffset(); - nau7802config.calibrationFactor = nau7802.getCalibrationFactor(); - bool okay = false; - - LOG_INFO("%s state write to %s", sensorName, nau7802ConfigFileName); - pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_Nau7802Config_size}; - - if (!pb_encode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { - LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_set_scale_tag: + if (request->set_scale == 0) { + this->tare(); + LOG_DEBUG("Client requested to tare scale"); } else { - okay = true; + this->calibrate(request->set_scale); + LOG_DEBUG("Client requested to calibrate to %d kg", request->set_scale); } - // Note: SafeFile::close() already acquires the lock and releases it internally - okay &= file.close(); + result = AdminMessageHandleResult::HANDLED; + break; - return okay; + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + return result; } -bool NAU7802Sensor::loadCalibrationData() -{ - spiLock->lock(); - auto file = FSCom.open(nau7802ConfigFileName, FILE_O_READ); - bool okay = false; - if (file) { - LOG_INFO("%s state read from %s", sensorName, nau7802ConfigFileName); - pb_istream_t stream = {&readcb, &file, meshtastic_Nau7802Config_size}; - if (!pb_decode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { - LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); - } else { - nau7802.setZeroOffset(nau7802config.zeroOffset); - nau7802.setCalibrationFactor(nau7802config.calibrationFactor); - okay = true; - } - file.close(); +void NAU7802Sensor::tare() { + nau7802.calculateZeroOffset(64); + if (!saveCalibrationData()) { + LOG_WARN("Failed to save calibration data"); + } + LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); +} + +bool NAU7802Sensor::saveCalibrationData() { + auto file = SafeFile(nau7802ConfigFileName); + nau7802config.zeroOffset = nau7802.getZeroOffset(); + nau7802config.calibrationFactor = nau7802.getCalibrationFactor(); + bool okay = false; + + LOG_INFO("%s state write to %s", sensorName, nau7802ConfigFileName); + pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_Nau7802Config_size}; + + if (!pb_encode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + } else { + okay = true; + } + // Note: SafeFile::close() already acquires the lock and releases it internally + okay &= file.close(); + + return okay; +} + +bool NAU7802Sensor::loadCalibrationData() { + spiLock->lock(); + auto file = FSCom.open(nau7802ConfigFileName, FILE_O_READ); + bool okay = false; + if (file) { + LOG_INFO("%s state read from %s", sensorName, nau7802ConfigFileName); + pb_istream_t stream = {&readcb, &file, meshtastic_Nau7802Config_size}; + if (!pb_decode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); } else { - LOG_INFO("No %s state found (File: %s)", sensorName, nau7802ConfigFileName); + nau7802.setZeroOffset(nau7802config.zeroOffset); + nau7802.setCalibrationFactor(nau7802config.calibrationFactor); + okay = true; } - spiLock->unlock(); - return okay; + file.close(); + } else { + LOG_INFO("No %s state found (File: %s)", sensorName, nau7802ConfigFileName); + } + spiLock->unlock(); + return okay; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/NAU7802Sensor.h b/src/modules/Telemetry/Sensor/NAU7802Sensor.h index a45e9a78a..83b50e230 100644 --- a/src/modules/Telemetry/Sensor/NAU7802Sensor.h +++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.h @@ -7,24 +7,23 @@ #include "TelemetrySensor.h" #include -class NAU7802Sensor : public TelemetrySensor -{ - private: - NAU7802 nau7802; +class NAU7802Sensor : public TelemetrySensor { +private: + NAU7802 nau7802; - protected: - const char *nau7802ConfigFileName = "/prefs/nau7802.dat"; - bool saveCalibrationData(); - bool loadCalibrationData(); +protected: + const char *nau7802ConfigFileName = "/prefs/nau7802.dat"; + bool saveCalibrationData(); + bool loadCalibrationData(); - public: - NAU7802Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; - void tare(); - void calibrate(float weight); - AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; +public: + NAU7802Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + void tare(); + void calibrate(float weight); + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp b/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp index 3407f2f0f..b62f6375d 100644 --- a/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp +++ b/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp @@ -9,41 +9,39 @@ OPT3001Sensor::OPT3001Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_OPT3001, "OPT3001") {} -bool OPT3001Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - auto errorCode = opt3001.begin(dev->address.address); - status = errorCode == NO_ERROR; - if (!status) { - return status; - } - - OPT3001_Config newConfig; - - newConfig.RangeNumber = 0b1100; - newConfig.ConvertionTime = 0b0; - newConfig.Latch = 0b1; - newConfig.ModeOfConversionOperation = 0b11; - - OPT3001_ErrorCode errorConfig = opt3001.writeConfig(newConfig); - if (errorConfig != NO_ERROR) { - LOG_ERROR("OPT3001 configuration error #%d", errorConfig); - } - status = errorConfig == NO_ERROR; - - initI2CSensor(); +bool OPT3001Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + auto errorCode = opt3001.begin(dev->address.address); + status = errorCode == NO_ERROR; + if (!status) { return status; + } + + OPT3001_Config newConfig; + + newConfig.RangeNumber = 0b1100; + newConfig.ConvertionTime = 0b0; + newConfig.Latch = 0b1; + newConfig.ModeOfConversionOperation = 0b11; + + OPT3001_ErrorCode errorConfig = opt3001.writeConfig(newConfig); + if (errorConfig != NO_ERROR) { + LOG_ERROR("OPT3001 configuration error #%d", errorConfig); + } + status = errorConfig == NO_ERROR; + + initI2CSensor(); + return status; } -bool OPT3001Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_lux = true; - OPT3001 result = opt3001.readResult(); +bool OPT3001Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_lux = true; + OPT3001 result = opt3001.readResult(); - measurement->variant.environment_metrics.lux = result.lux; - LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); + measurement->variant.environment_metrics.lux = result.lux; + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/OPT3001Sensor.h b/src/modules/Telemetry/Sensor/OPT3001Sensor.h index c8a140b51..6885d5b02 100644 --- a/src/modules/Telemetry/Sensor/OPT3001Sensor.h +++ b/src/modules/Telemetry/Sensor/OPT3001Sensor.h @@ -7,18 +7,17 @@ #include "TelemetrySensor.h" #include -class OPT3001Sensor : public TelemetrySensor -{ - private: - ClosedCube_OPT3001 opt3001; +class OPT3001Sensor : public TelemetrySensor { +private: + ClosedCube_OPT3001 opt3001; - public: - OPT3001Sensor(); +public: + OPT3001Sensor(); #if WIRE_INTERFACES_COUNT > 1 - virtual bool onlyWire1() { return true; } + virtual bool onlyWire1() { return true; } #endif - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp b/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp index 189317bf2..ad8eb013a 100644 --- a/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp +++ b/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp @@ -9,21 +9,19 @@ PCT2075Sensor::PCT2075Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PCT2075, "PCT2075") {} -bool PCT2075Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = pct2075.begin(dev->address.address, bus); +bool PCT2075Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = pct2075.begin(dev->address.address, bus); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool PCT2075Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = pct2075.getTemperature(); +bool PCT2075Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = pct2075.getTemperature(); - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/PCT2075Sensor.h b/src/modules/Telemetry/Sensor/PCT2075Sensor.h index 55f9423d4..864391f21 100644 --- a/src/modules/Telemetry/Sensor/PCT2075Sensor.h +++ b/src/modules/Telemetry/Sensor/PCT2075Sensor.h @@ -7,15 +7,14 @@ #include "TelemetrySensor.h" #include -class PCT2075Sensor : public TelemetrySensor -{ - private: - Adafruit_PCT2075 pct2075; +class PCT2075Sensor : public TelemetrySensor { +private: + Adafruit_PCT2075 pct2075; - public: - PCT2075Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + PCT2075Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp index ff0628cc3..3061f698f 100644 --- a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp @@ -6,105 +6,102 @@ RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {} -bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - // TODO:: check for up to 2 additional sensors and start them if present. - sensor.set_sensor_addr(RAK120351_ADDR); - delay(100); - sensor.begin(dev->address.address); +bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + // TODO:: check for up to 2 additional sensors and start them if present. + sensor.set_sensor_addr(RAK120351_ADDR); + delay(100); + sensor.begin(dev->address.address); - // Get sensor firmware version - uint8_t data = 0; - sensor.get_sensor_version(&data); - if (data != 0) { - LOG_INFO("Init sensor: %s", sensorName); - LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); - status = true; - sensor.sensor_sleep(); - } else { - // If we reach here, it means the sensor did not initialize correctly. - LOG_INFO("Init sensor: %s", sensorName); - LOG_ERROR("RAK12035Sensor Init Failed"); - status = false; - } - if (!status) { - return status; - } - setup(); - - initI2CSensor(); + // Get sensor firmware version + uint8_t data = 0; + sensor.get_sensor_version(&data); + if (data != 0) { + LOG_INFO("Init sensor: %s", sensorName); + LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); + status = true; + sensor.sensor_sleep(); + } else { + // If we reach here, it means the sensor did not initialize correctly. + LOG_INFO("Init sensor: %s", sensorName); + LOG_ERROR("RAK12035Sensor Init Failed"); + status = false; + } + if (!status) { return status; + } + setup(); + + initI2CSensor(); + return status; } -void RAK12035Sensor::setup() -{ - // Set the calibration values - // Reading the saved calibration values from the sensor. - // TODO:: Check for and run calibration check for up to 2 additional sensors if present. - uint16_t zero_val = 0; - uint16_t hundred_val = 0; - uint16_t default_zero_val = 550; - uint16_t default_hundred_val = 420; - sensor.sensor_on(); - delay(200); - sensor.get_dry_cal(&zero_val); - sensor.get_wet_cal(&hundred_val); - delay(200); - if (zero_val == 0 || zero_val <= hundred_val) { - LOG_INFO("Dry calibration value is %d", zero_val); - LOG_INFO("Wet calibration value is %d", hundred_val); - LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " - "https://github.com/RAKWireless/RAK12035_SoilMoisture."); - LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); - sensor.set_dry_cal(default_zero_val); - sensor.get_dry_cal(&zero_val); - LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); - } - if (hundred_val == 0 || hundred_val >= zero_val) { - LOG_INFO("Dry calibration value is %d", zero_val); - LOG_INFO("Wet calibration value is %d", hundred_val); - LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " - "https://github.com/RAKWireless/RAK12035_SoilMoisture."); - LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); - sensor.set_wet_cal(default_hundred_val); - sensor.get_wet_cal(&hundred_val); - LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); - } - sensor.sensor_sleep(); - delay(200); +void RAK12035Sensor::setup() { + // Set the calibration values + // Reading the saved calibration values from the sensor. + // TODO:: Check for and run calibration check for up to 2 additional sensors if present. + uint16_t zero_val = 0; + uint16_t hundred_val = 0; + uint16_t default_zero_val = 550; + uint16_t default_hundred_val = 420; + sensor.sensor_on(); + delay(200); + sensor.get_dry_cal(&zero_val); + sensor.get_wet_cal(&hundred_val); + delay(200); + if (zero_val == 0 || zero_val <= hundred_val) { LOG_INFO("Dry calibration value is %d", zero_val); LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); + sensor.set_dry_cal(default_zero_val); + sensor.get_dry_cal(&zero_val); + LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); + } + if (hundred_val == 0 || hundred_val >= zero_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); + sensor.set_wet_cal(default_hundred_val); + sensor.get_wet_cal(&hundred_val); + LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); + } + sensor.sensor_sleep(); + delay(200); + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); } -bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - // TODO:: read and send metrics for up to 2 additional soil monitors if present. - // -- how to do this.. this could get a little complex.. - // ie - 1> we combine them into an average and send that, 2> we send them as separate metrics - // ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting in the - // device ui and an additional proto for that? - measurement->variant.environment_metrics.has_soil_temperature = true; - measurement->variant.environment_metrics.has_soil_moisture = true; +bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) { + // TODO:: read and send metrics for up to 2 additional soil monitors if present. + // -- how to do this.. this could get a little complex.. + // ie - 1> we combine them into an average and send that, 2> we send them as separate metrics + // ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting + // in the device ui and an additional proto for that? + measurement->variant.environment_metrics.has_soil_temperature = true; + measurement->variant.environment_metrics.has_soil_moisture = true; - uint8_t moisture = 0; - uint16_t temp = 0; - bool success = false; + uint8_t moisture = 0; + uint16_t temp = 0; + bool success = false; - sensor.sensor_on(); - delay(200); - success = sensor.get_sensor_moisture(&moisture); - delay(200); - success &= sensor.get_sensor_temperature(&temp); - delay(200); - sensor.sensor_sleep(); + sensor.sensor_on(); + delay(200); + success = sensor.get_sensor_moisture(&moisture); + delay(200); + success &= sensor.get_sensor_temperature(&temp); + delay(200); + sensor.sensor_sleep(); - if (success == false) { - LOG_ERROR("Failed to read sensor data"); - return false; - } - measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); - measurement->variant.environment_metrics.soil_moisture = moisture; + if (success == false) { + LOG_ERROR("Failed to read sensor data"); + return false; + } + measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); + measurement->variant.environment_metrics.soil_moisture = moisture; - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.h b/src/modules/Telemetry/Sensor/RAK12035Sensor.h index 6a38d2eb3..6a6df27f3 100644 --- a/src/modules/Telemetry/Sensor/RAK12035Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.h @@ -12,18 +12,17 @@ #include "TelemetrySensor.h" #include -class RAK12035Sensor : public TelemetrySensor -{ - private: - RAK12035 sensor; - void setup(); +class RAK12035Sensor : public TelemetrySensor { +private: + RAK12035 sensor; + void setup(); - public: - RAK12035Sensor(); +public: + RAK12035Sensor(); #if WIRE_INTERFACES_COUNT > 1 - virtual bool onlyWire1() { return true; } + virtual bool onlyWire1() { return true; } #endif - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp b/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp index ad3925f08..2cfd3081f 100644 --- a/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp @@ -28,182 +28,162 @@ static uint8_t provision = 0; extern RAK9154Sensor rak9154Sensor; -static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len) -{ - switch (eid) { - case SNHUBAPI_EVT_RECV_REQ: - case SNHUBAPI_EVT_RECV_RSP: - break; +static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len) { + switch (eid) { + case SNHUBAPI_EVT_RECV_REQ: + case SNHUBAPI_EVT_RECV_RSP: + break; - case SNHUBAPI_EVT_QSEND: - mySerial.write(msg, len); - break; + case SNHUBAPI_EVT_QSEND: + mySerial.write(msg, len); + break; - case SNHUBAPI_EVT_ADD_SID: - // LOG_INFO("+ADD:SID:[%02x]", msg[0]); - break; + case SNHUBAPI_EVT_ADD_SID: + // LOG_INFO("+ADD:SID:[%02x]", msg[0]); + break; - case SNHUBAPI_EVT_ADD_PID: - // LOG_INFO("+ADD:PID:[%02x]", msg[0]); + case SNHUBAPI_EVT_ADD_PID: + // LOG_INFO("+ADD:PID:[%02x]", msg[0]); #ifdef BOOT_DATA_REQ - provision = msg[0]; + provision = msg[0]; #endif - break; + break; - case SNHUBAPI_EVT_GET_INTV: - break; + case SNHUBAPI_EVT_GET_INTV: + break; - case SNHUBAPI_EVT_GET_ENABLE: - break; + case SNHUBAPI_EVT_GET_ENABLE: + break; - case SNHUBAPI_EVT_SDATA_REQ: - - // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); - // for( uint16_t i=1; i 100) { - dc_prec = 100; - } - break; - case RAK_IPSO_DC_CURRENT: - dc_cur = (msg[2] << 8) + msg[1]; - break; - case RAK_IPSO_DC_VOLTAGE: - dc_vol = (msg[2] << 8) + msg[1]; - dc_vol *= 10; - break; - default: - break; - } - rak9154Sensor.setLastRead(millis()); - - break; - case SNHUBAPI_EVT_REPORT: - - // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); - // for( uint16_t i=1; i 100) { - dc_prec = 100; - } - break; - case RAK_IPSO_DC_CURRENT: - dc_cur = (msg[1] << 8) + msg[2]; - break; - case RAK_IPSO_DC_VOLTAGE: - dc_vol = (msg[1] << 8) + msg[2]; - dc_vol *= 10; - break; - default: - break; - } - rak9154Sensor.setLastRead(millis()); - - break; - - case SNHUBAPI_EVT_CHKSUM_ERR: - LOG_INFO("+ERR:CHKSUM"); - break; - - case SNHUBAPI_EVT_SEQ_ERR: - LOG_INFO("+ERR:SEQUCE"); - break; + case SNHUBAPI_EVT_SDATA_REQ: + // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); + // for( uint16_t i=1; i 100) { + dc_prec = 100; + } + break; + case RAK_IPSO_DC_CURRENT: + dc_cur = (msg[2] << 8) + msg[1]; + break; + case RAK_IPSO_DC_VOLTAGE: + dc_vol = (msg[2] << 8) + msg[1]; + dc_vol *= 10; + break; default: - break; + break; } -} + rak9154Sensor.setLastRead(millis()); -static int32_t onewireHandle() -{ - if (provision != 0) { - RakSNHub_Protocl_API.get.data(provision); - provision = 0; + break; + case SNHUBAPI_EVT_REPORT: + + // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); + // for( uint16_t i=1; i 100) { + dc_prec = 100; + } + break; + case RAK_IPSO_DC_CURRENT: + dc_cur = (msg[1] << 8) + msg[2]; + break; + case RAK_IPSO_DC_VOLTAGE: + dc_vol = (msg[1] << 8) + msg[2]; + dc_vol *= 10; + break; + default: + break; } + rak9154Sensor.setLastRead(millis()); - while (mySerial.available()) { - char a = mySerial.read(); - buff[bufflen++] = a; - delay(2); // continue data, timeout=2ms - } + break; - if (bufflen != 0) { - RakSNHub_Protocl_API.process((uint8_t *)buff, bufflen); - bufflen = 0; - } + case SNHUBAPI_EVT_CHKSUM_ERR: + LOG_INFO("+ERR:CHKSUM"); + break; - return 50; + case SNHUBAPI_EVT_SEQ_ERR: + LOG_INFO("+ERR:SEQUCE"); + break; + + default: + break; + } } -int32_t RAK9154Sensor::runOnce() -{ - if (!rak9154Sensor.isInitialized()) { - onewirePeriodic = new Periodic("onewireHandle", onewireHandle); +static int32_t onewireHandle() { + if (provision != 0) { + RakSNHub_Protocl_API.get.data(provision); + provision = 0; + } - mySerial.begin(9600); + while (mySerial.available()) { + char a = mySerial.read(); + buff[bufflen++] = a; + delay(2); // continue data, timeout=2ms + } - RakSNHub_Protocl_API.init(onewire_evt); + if (bufflen != 0) { + RakSNHub_Protocl_API.process((uint8_t *)buff, bufflen); + bufflen = 0; + } - status = true; - initialized = true; - } - - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + return 50; } -void RAK9154Sensor::setup() -{ - // Set up oversampling and filter initialization +int32_t RAK9154Sensor::runOnce() { + if (!rak9154Sensor.isInitialized()) { + onewirePeriodic = new Periodic("onewireHandle", onewireHandle); + + mySerial.begin(9600); + + RakSNHub_Protocl_API.init(onewire_evt); + + status = true; + initialized = true; + } + + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } -bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - if (getBusVoltageMv() > 0) { - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; - - measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000; - measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000; - return true; - } else { - return false; - } +void RAK9154Sensor::setup() { + // Set up oversampling and filter initialization } -uint16_t RAK9154Sensor::getBusVoltageMv() -{ - return dc_vol; +bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) { + if (getBusVoltageMv() > 0) { + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; + + measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000; + measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000; + return true; + } else { + return false; + } } -int16_t RAK9154Sensor::getCurrentMa() -{ - return dc_cur; -} +uint16_t RAK9154Sensor::getBusVoltageMv() { return dc_vol; } -int RAK9154Sensor::getBusBatteryPercent() -{ - return (int)dc_prec; -} +int16_t RAK9154Sensor::getCurrentMa() { return dc_cur; } -bool RAK9154Sensor::isCharging() -{ - return (dc_cur > 0) ? true : false; -} -void RAK9154Sensor::setLastRead(uint32_t lastRead) -{ - this->lastRead = lastRead; -} +int RAK9154Sensor::getBusBatteryPercent() { return (int)dc_prec; } + +bool RAK9154Sensor::isCharging() { return (dc_cur > 0) ? true : false; } +void RAK9154Sensor::setLastRead(uint32_t lastRead) { this->lastRead = lastRead; } #endif // HAS_RAKPROT diff --git a/src/modules/Telemetry/Sensor/RAK9154Sensor.h b/src/modules/Telemetry/Sensor/RAK9154Sensor.h index c96139f9c..9c01a8961 100644 --- a/src/modules/Telemetry/Sensor/RAK9154Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.h @@ -9,22 +9,21 @@ #include "TelemetrySensor.h" #include "VoltageSensor.h" -class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor -{ - private: - protected: - virtual void setup() override; - uint32_t lastRead = 0; +class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { +private: +protected: + virtual void setup() override; + uint32_t lastRead = 0; - public: - RAK9154Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; - int getBusBatteryPercent(); - bool isCharging(); - void setLastRead(uint32_t lastRead); +public: + RAK9154Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; + int getBusBatteryPercent(); + bool isCharging(); + void setLastRead(uint32_t lastRead); }; #endif // _RAK9154SENSOR_H #endif // HAS_RAKPROT \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp index 3dbd06e8d..73dcb20c5 100644 --- a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp @@ -8,70 +8,66 @@ RCWL9620Sensor::RCWL9620Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RCWL9620, "RCWL9620") {} -bool RCWL9620Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = 1; - begin(bus, dev->address.address); - initI2CSensor(); - return status; +bool RCWL9620Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = 1; + begin(bus, dev->address.address); + initI2CSensor(); + return status; } -bool RCWL9620Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_distance = true; - LOG_DEBUG("RCWL9620 getMetrics"); - measurement->variant.environment_metrics.distance = getDistance(); - return true; +bool RCWL9620Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_distance = true; + LOG_DEBUG("RCWL9620 getMetrics"); + measurement->variant.environment_metrics.distance = getDistance(); + return true; } -void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) -{ - _wire = wire; - _addr = addr; - _sda = sda; - _scl = scl; - _speed = speed; - _wire->begin(); +void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) { + _wire = wire; + _addr = addr; + _sda = sda; + _scl = scl; + _speed = speed; + _wire->begin(); } -float RCWL9620Sensor::getDistance() -{ - uint32_t data = 0; - uint8_t b1 = 0, b2 = 0, b3 = 0; +float RCWL9620Sensor::getDistance() { + uint32_t data = 0; + uint8_t b1 = 0, b2 = 0, b3 = 0; - LOG_DEBUG("[RCWL9620] Start measure command"); + LOG_DEBUG("[RCWL9620] Start measure command"); - _wire->beginTransmission(_addr); - _wire->write(0x01); // À tester aussi sans cette ligne si besoin - uint8_t result = _wire->endTransmission(); - LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); - delay(100); // délai pour laisser le capteur répondre + _wire->beginTransmission(_addr); + _wire->write(0x01); // À tester aussi sans cette ligne si besoin + uint8_t result = _wire->endTransmission(); + LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); + delay(100); // délai pour laisser le capteur répondre - LOG_DEBUG("[RCWL9620] Read i2c data:"); - _wire->requestFrom(_addr, (uint8_t)3); + LOG_DEBUG("[RCWL9620] Read i2c data:"); + _wire->requestFrom(_addr, (uint8_t)3); - if (_wire->available() < 3) { - LOG_DEBUG("[RCWL9620] less than 3 octets !"); - return 0.0; - } + if (_wire->available() < 3) { + LOG_DEBUG("[RCWL9620] less than 3 octets !"); + return 0.0; + } - b1 = _wire->read(); - b2 = _wire->read(); - b3 = _wire->read(); + b1 = _wire->read(); + b2 = _wire->read(); + b3 = _wire->read(); - data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; + data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; - float Distance = float(data) / 1000.0; + float Distance = float(data) / 1000.0; - LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); - LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); + LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); + LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); - if (Distance > 4500.00) { - return 4500.00; - } else { - return Distance; - } + if (Distance > 4500.00) { + return 4500.00; + } else { + return Distance; + } } #endif diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.h b/src/modules/Telemetry/Sensor/RCWL9620Sensor.h index 408db3633..7ccc7a6da 100644 --- a/src/modules/Telemetry/Sensor/RCWL9620Sensor.h +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.h @@ -6,23 +6,22 @@ #include "TelemetrySensor.h" #include -class RCWL9620Sensor : public TelemetrySensor -{ - private: - uint8_t _addr = 0x57; - TwoWire *_wire = &Wire; - uint8_t _scl = -1; - uint8_t _sda = -1; - uint32_t _speed = 200000UL; +class RCWL9620Sensor : public TelemetrySensor { +private: + uint8_t _addr = 0x57; + TwoWire *_wire = &Wire; + uint8_t _scl = -1; + uint8_t _sda = -1; + uint32_t _speed = 200000UL; - protected: - void begin(TwoWire *wire = &Wire, uint8_t addr = 0x57, uint8_t sda = -1, uint8_t scl = -1, uint32_t speed = 200000UL); - float getDistance(); +protected: + void begin(TwoWire *wire = &Wire, uint8_t addr = 0x57, uint8_t sda = -1, uint8_t scl = -1, uint32_t speed = 200000UL); + float getDistance(); - public: - RCWL9620Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + RCWL9620Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp index 67a36933d..78143af5d 100644 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp +++ b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp @@ -9,23 +9,21 @@ SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {} -bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - sht31 = Adafruit_SHT31(bus); - status = sht31.begin(dev->address.address); - initI2CSensor(); - return status; +bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + sht31 = Adafruit_SHT31(bus); + status = sht31.begin(dev->address.address); + initI2CSensor(); + return status; } -bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.temperature = sht31.readTemperature(); - measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity(); +bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.temperature = sht31.readTemperature(); + measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity(); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.h b/src/modules/Telemetry/Sensor/SHT31Sensor.h index ecb7d63a6..97e1cdda3 100644 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.h +++ b/src/modules/Telemetry/Sensor/SHT31Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class SHT31Sensor : public TelemetrySensor -{ - private: - Adafruit_SHT31 sht31; +class SHT31Sensor : public TelemetrySensor { +private: + Adafruit_SHT31 sht31; - public: - SHT31Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + SHT31Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp index b11795d97..95795da5e 100644 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp @@ -9,40 +9,38 @@ SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {} -bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - uint32_t serialNumber = 0; + uint32_t serialNumber = 0; - status = sht4x.begin(bus); - if (!status) { - return status; - } - - serialNumber = sht4x.readSerial(); - if (serialNumber != 0) { - LOG_DEBUG("serialNumber : %x", serialNumber); - status = 1; - } else { - LOG_DEBUG("Error trying to execute readSerial(): "); - status = 0; - } - - initI2CSensor(); + status = sht4x.begin(bus); + if (!status) { return status; + } + + serialNumber = sht4x.readSerial(); + if (serialNumber != 0) { + LOG_DEBUG("serialNumber : %x", serialNumber); + status = 1; + } else { + LOG_DEBUG("Error trying to execute readSerial(): "); + status = 0; + } + + initI2CSensor(); + return status; } -bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; +bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; - sensors_event_t humidity, temp; - sht4x.getEvent(&humidity, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - return true; + sensors_event_t humidity, temp; + sht4x.getEvent(&humidity, &temp); + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.h b/src/modules/Telemetry/Sensor/SHT4XSensor.h index 7311d2366..8bb819e9c 100644 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.h +++ b/src/modules/Telemetry/Sensor/SHT4XSensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class SHT4XSensor : public TelemetrySensor -{ - private: - Adafruit_SHT4x sht4x = Adafruit_SHT4x(); +class SHT4XSensor : public TelemetrySensor { +private: + Adafruit_SHT4x sht4x = Adafruit_SHT4x(); - public: - SHT4XSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + SHT4XSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp index fdab0b266..b2961a845 100644 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp +++ b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp @@ -9,27 +9,25 @@ SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {} -bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = shtc3.begin(bus); +bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = shtc3.begin(bus); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; +bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; - sensors_event_t humidity, temp; - shtc3.getEvent(&humidity, &temp); + sensors_event_t humidity, temp; + shtc3.getEvent(&humidity, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.h b/src/modules/Telemetry/Sensor/SHTC3Sensor.h index 51cee18f7..a6aaa9905 100644 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.h +++ b/src/modules/Telemetry/Sensor/SHTC3Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class SHTC3Sensor : public TelemetrySensor -{ - private: - Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); +class SHTC3Sensor : public TelemetrySensor { +private: + Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); - public: - SHTC3Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + SHTC3Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/T1000xSensor.cpp b/src/modules/Telemetry/Sensor/T1000xSensor.cpp index b123450ec..0fba20b4f 100644 --- a/src/modules/Telemetry/Sensor/T1000xSensor.cpp +++ b/src/modules/Telemetry/Sensor/T1000xSensor.cpp @@ -17,95 +17,89 @@ // ntc res table uint32_t ntc_res2[136] = { - 113347, 107565, 102116, 96978, 92132, 87559, 83242, 79166, 75316, 71677, 68237, 64991, 61919, 59011, 56258, 53650, 51178, - 48835, 46613, 44506, 42506, 40600, 38791, 37073, 35442, 33892, 32420, 31020, 29689, 28423, 27219, 26076, 24988, 23951, - 22963, 22021, 21123, 20267, 19450, 18670, 17926, 17214, 16534, 15886, 15266, 14674, 14108, 13566, 13049, 12554, 12081, - 11628, 11195, 10780, 10382, 10000, 9634, 9284, 8947, 8624, 8315, 8018, 7734, 7461, 7199, 6948, 6707, 6475, - 6253, 6039, 5834, 5636, 5445, 5262, 5086, 4917, 4754, 4597, 4446, 4301, 4161, 4026, 3896, 3771, 3651, - 3535, 3423, 3315, 3211, 3111, 3014, 2922, 2834, 2748, 2666, 2586, 2509, 2435, 2364, 2294, 2228, 2163, - 2100, 2040, 1981, 1925, 1870, 1817, 1766, 1716, 1669, 1622, 1578, 1535, 1493, 1452, 1413, 1375, 1338, - 1303, 1268, 1234, 1202, 1170, 1139, 1110, 1081, 1053, 1026, 999, 974, 949, 925, 902, 880, 858, + 113347, 107565, 102116, 96978, 92132, 87559, 83242, 79166, 75316, 71677, 68237, 64991, 61919, 59011, 56258, 53650, 51178, 48835, 46613, 44506, + 42506, 40600, 38791, 37073, 35442, 33892, 32420, 31020, 29689, 28423, 27219, 26076, 24988, 23951, 22963, 22021, 21123, 20267, 19450, 18670, + 17926, 17214, 16534, 15886, 15266, 14674, 14108, 13566, 13049, 12554, 12081, 11628, 11195, 10780, 10382, 10000, 9634, 9284, 8947, 8624, + 8315, 8018, 7734, 7461, 7199, 6948, 6707, 6475, 6253, 6039, 5834, 5636, 5445, 5262, 5086, 4917, 4754, 4597, 4446, 4301, + 4161, 4026, 3896, 3771, 3651, 3535, 3423, 3315, 3211, 3111, 3014, 2922, 2834, 2748, 2666, 2586, 2509, 2435, 2364, 2294, + 2228, 2163, 2100, 2040, 1981, 1925, 1870, 1817, 1766, 1716, 1669, 1622, 1578, 1535, 1493, 1452, 1413, 1375, 1338, 1303, + 1268, 1234, 1202, 1170, 1139, 1110, 1081, 1053, 1026, 999, 974, 949, 925, 902, 880, 858, }; int8_t ntc_temp2[136] = { - -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, - -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, - 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, - 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, + -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, }; T1000xSensor::T1000xSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "T1000x") {} -bool T1000xSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - return true; +bool T1000xSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + return true; } -float T1000xSensor::getLux() -{ - uint32_t lux_vot = 0; - float lux_level = 0; +float T1000xSensor::getLux() { + uint32_t lux_vot = 0; + float lux_level = 0; - for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - lux_vot += analogRead(T1000X_LUX_PIN); - } - lux_vot = lux_vot / T1000X_SENSE_SAMPLES; - lux_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * lux_vot; + for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { + lux_vot += analogRead(T1000X_LUX_PIN); + } + lux_vot = lux_vot / T1000X_SENSE_SAMPLES; + lux_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * lux_vot; - if (lux_vot <= 80) - lux_level = 0; - else if (lux_vot >= 2480) - lux_level = 100; - else - lux_level = 100 * (lux_vot - 80) / T1000X_LIGHT_REF_VCC; + if (lux_vot <= 80) + lux_level = 0; + else if (lux_vot >= 2480) + lux_level = 100; + else + lux_level = 100 * (lux_vot - 80) / T1000X_LIGHT_REF_VCC; - return lux_level; + return lux_level; } -float T1000xSensor::getTemp() -{ - uint32_t vcc_vot = 0, ntc_vot = 0; +float T1000xSensor::getTemp() { + uint32_t vcc_vot = 0, ntc_vot = 0; - uint8_t u8i = 0; - float Vout = 0, Rt = 0, temp = 0; - float Temp = 0; + uint8_t u8i = 0; + float Vout = 0, Rt = 0, temp = 0; + float Temp = 0; - for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - vcc_vot += analogRead(T1000X_VCC_PIN); + for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { + vcc_vot += analogRead(T1000X_VCC_PIN); + } + vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; + vcc_vot = 2 * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + + for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { + ntc_vot += analogRead(T1000X_NTC_PIN); + } + ntc_vot = ntc_vot / T1000X_SENSE_SAMPLES; + ntc_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * ntc_vot; + + Vout = ntc_vot; + Rt = (HEATER_NTC_RP * vcc_vot) / Vout - HEATER_NTC_RP; + for (u8i = 0; u8i < 135; u8i++) { + if (Rt >= ntc_res2[u8i]) { + break; } - vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; - vcc_vot = 2 * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + } + temp = ntc_temp2[u8i - 1] + 1 * (ntc_res2[u8i - 1] - Rt) / (float)(ntc_res2[u8i - 1] - ntc_res2[u8i]); + Temp = (temp * 100 + 5) / 100; // half adjust - for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - ntc_vot += analogRead(T1000X_NTC_PIN); - } - ntc_vot = ntc_vot / T1000X_SENSE_SAMPLES; - ntc_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * ntc_vot; - - Vout = ntc_vot; - Rt = (HEATER_NTC_RP * vcc_vot) / Vout - HEATER_NTC_RP; - for (u8i = 0; u8i < 135; u8i++) { - if (Rt >= ntc_res2[u8i]) { - break; - } - } - temp = ntc_temp2[u8i - 1] + 1 * (ntc_res2[u8i - 1] - Rt) / (float)(ntc_res2[u8i - 1] - ntc_res2[u8i]); - Temp = (temp * 100 + 5) / 100; // half adjust - - return Temp; + return Temp; } -bool T1000xSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_lux = true; +bool T1000xSensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_lux = true; - measurement->variant.environment_metrics.temperature = getTemp(); - measurement->variant.environment_metrics.lux = getLux(); - return true; + measurement->variant.environment_metrics.temperature = getTemp(); + measurement->variant.environment_metrics.lux = getLux(); + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/T1000xSensor.h b/src/modules/Telemetry/Sensor/T1000xSensor.h index b840a2d88..1b5929e0f 100644 --- a/src/modules/Telemetry/Sensor/T1000xSensor.h +++ b/src/modules/Telemetry/Sensor/T1000xSensor.h @@ -5,14 +5,13 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -class T1000xSensor : public TelemetrySensor -{ - public: - T1000xSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; - virtual float getLux(); - virtual float getTemp(); +class T1000xSensor : public TelemetrySensor { +public: + T1000xSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual float getLux(); + virtual float getTemp(); }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp index 4e02af642..e643d85ee 100644 --- a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp +++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp @@ -9,30 +9,28 @@ TSL2561Sensor::TSL2561Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL2561, "TSL2561") {} -bool TSL2561Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); +bool TSL2561Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); - status = tsl.begin(bus); - if (!status) { - return status; - } - tsl.setGain(TSL2561_GAIN_1X); - tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS); - - initI2CSensor(); + status = tsl.begin(bus); + if (!status) { return status; + } + tsl.setGain(TSL2561_GAIN_1X); + tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS); + + initI2CSensor(); + return status; } -bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_lux = true; - sensors_event_t event; - tsl.getEvent(&event); - measurement->variant.environment_metrics.lux = event.light; - LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); +bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_lux = true; + sensors_event_t event; + tsl.getEvent(&event); + measurement->variant.environment_metrics.lux = event.light; + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.h b/src/modules/Telemetry/Sensor/TSL2561Sensor.h index abf5a8f73..667c7e970 100644 --- a/src/modules/Telemetry/Sensor/TSL2561Sensor.h +++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.h @@ -6,15 +6,14 @@ #include "TelemetrySensor.h" #include -class TSL2561Sensor : public TelemetrySensor -{ - private: - // The magic number is a sensor id, the actual value doesn't matter - Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345); +class TSL2561Sensor : public TelemetrySensor { +private: + // The magic number is a sensor id, the actual value doesn't matter + Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345); - public: - TSL2561Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + TSL2561Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp index 0899d4470..c7198b74e 100644 --- a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp @@ -10,32 +10,30 @@ TSL2591Sensor::TSL2591Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL25911FN, "TSL2591") {} -bool TSL2591Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = tsl.begin(bus); - if (!status) { - return status; - } - tsl.setGain(TSL2591_GAIN_LOW); // 1x gain - tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); - - initI2CSensor(); +bool TSL2591Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = tsl.begin(bus); + if (!status) { return status; + } + tsl.setGain(TSL2591_GAIN_LOW); // 1x gain + tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); + + initI2CSensor(); + return status; } -bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_lux = true; - uint32_t lum = tsl.getFullLuminosity(); - uint16_t ir, full; - ir = lum >> 16; - full = lum & 0xFFFF; +bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_lux = true; + uint32_t lum = tsl.getFullLuminosity(); + uint16_t ir, full; + ir = lum >> 16; + full = lum & 0xFFFF; - measurement->variant.environment_metrics.lux = tsl.calculateLux(full, ir); - LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); + measurement->variant.environment_metrics.lux = tsl.calculateLux(full, ir); + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.h b/src/modules/Telemetry/Sensor/TSL2591Sensor.h index 1ac430a03..df968da72 100644 --- a/src/modules/Telemetry/Sensor/TSL2591Sensor.h +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.h @@ -6,14 +6,13 @@ #include "TelemetrySensor.h" #include -class TSL2591Sensor : public TelemetrySensor -{ - private: - Adafruit_TSL2591 tsl; +class TSL2591Sensor : public TelemetrySensor { +private: + Adafruit_TSL2591 tsl; - public: - TSL2591Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + TSL2591Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e61808..e9790564a 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -16,59 +16,55 @@ class TwoWire; #define DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000 extern std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1]; -class TelemetrySensor -{ - protected: - TelemetrySensor(meshtastic_TelemetrySensorType sensorType, const char *sensorName) - { - this->sensorName = sensorName; - this->sensorType = sensorType; - this->status = 0; +class TelemetrySensor { +protected: + TelemetrySensor(meshtastic_TelemetrySensorType sensorType, const char *sensorName) { + this->sensorName = sensorName; + this->sensorType = sensorType; + this->status = 0; + } + + const char *sensorName; + meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; + unsigned status; + bool initialized = false; + + int32_t initI2CSensor() { + if (!status) { + LOG_WARN("Can't connect to detected %s sensor. Remove from nodeTelemetrySensorsMap", sensorName); + nodeTelemetrySensorsMap[sensorType].first = 0; + } else { + LOG_INFO("Opened %s sensor on i2c bus", sensorName); + setup(); } + initialized = true; + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - const char *sensorName; - meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; - unsigned status; - bool initialized = false; + // TODO: check is setup used at all? + virtual void setup() {} - int32_t initI2CSensor() - { - if (!status) { - LOG_WARN("Can't connect to detected %s sensor. Remove from nodeTelemetrySensorsMap", sensorName); - nodeTelemetrySensorsMap[sensorType].first = 0; - } else { - LOG_INFO("Opened %s sensor on i2c bus", sensorName); - setup(); - } - initialized = true; - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +public: + virtual ~TelemetrySensor() {} - // TODO: check is setup used at all? - virtual void setup() {} + virtual AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { + return AdminMessageHandleResult::NOT_HANDLED; + } - public: - virtual ~TelemetrySensor() {} - - virtual AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) - { - return AdminMessageHandleResult::NOT_HANDLED; - } - - // TODO: delete after migration - bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // TODO: delete after migration + bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } #if WIRE_INTERFACES_COUNT > 1 - // Set to true if Implementation only works first I2C port (Wire) - virtual bool onlyWire1() { return false; } + // Set to true if Implementation only works first I2C port (Wire) + virtual bool onlyWire1() { return false; } #endif - virtual int32_t runOnce() { return INT32_MAX; } - virtual bool isInitialized() { return initialized; } - virtual bool isRunning() { return status > 0; } + virtual int32_t runOnce() { return INT32_MAX; } + virtual bool isInitialized() { return initialized; } + virtual bool isRunning() { return status > 0; } - virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; + virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp b/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp index c89463be5..c09bf691a 100644 --- a/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp +++ b/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp @@ -11,20 +11,19 @@ VEML7700Sensor::VEML7700Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_VEML7700, "VEML7700") {} -bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = veml7700.begin(bus); - if (!status) { - return status; - } - - veml7700.setLowThreshold(10000); - veml7700.setHighThreshold(20000); - veml7700.interruptEnable(true); - - initI2CSensor(); +bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { + LOG_INFO("Init sensor: %s", sensorName); + status = veml7700.begin(bus); + if (!status) { return status; + } + + veml7700.setLowThreshold(10000); + veml7700.setHighThreshold(20000); + veml7700.interruptEnable(true); + + initI2CSensor(); + return status; } /*! @@ -33,35 +32,29 @@ bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) * @param corrected if true, apply non-linear correction * @return lux value */ -float VEML7700Sensor::computeLux(uint16_t rawALS, bool corrected) -{ - float lux = getResolution() * rawALS; - if (corrected) - lux = (((6.0135e-13 * lux - 9.3924e-9) * lux + 8.1488e-5) * lux + 1.0023) * lux; - return lux; +float VEML7700Sensor::computeLux(uint16_t rawALS, bool corrected) { + float lux = getResolution() * rawALS; + if (corrected) + lux = (((6.0135e-13 * lux - 9.3924e-9) * lux + 8.1488e-5) * lux + 1.0023) * lux; + return lux; } /*! * @brief Determines resolution for current gain and integration time * settings. */ -float VEML7700Sensor::getResolution(void) -{ - return MAX_RES * (IT_MAX / veml7700.getIntegrationTimeValue()) * (GAIN_MAX / veml7700.getGainValue()); -} +float VEML7700Sensor::getResolution(void) { return MAX_RES * (IT_MAX / veml7700.getIntegrationTimeValue()) * (GAIN_MAX / veml7700.getGainValue()); } -bool VEML7700Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_lux = true; - measurement->variant.environment_metrics.has_white_lux = true; +bool VEML7700Sensor::getMetrics(meshtastic_Telemetry *measurement) { + measurement->variant.environment_metrics.has_lux = true; + measurement->variant.environment_metrics.has_white_lux = true; - int16_t white; - measurement->variant.environment_metrics.lux = veml7700.readLux(VEML_LUX_AUTO); - white = veml7700.readWhite(true); - measurement->variant.environment_metrics.white_lux = computeLux(white, white > 100); - LOG_INFO("white lux %f, als lux %f", measurement->variant.environment_metrics.white_lux, - measurement->variant.environment_metrics.lux); + int16_t white; + measurement->variant.environment_metrics.lux = veml7700.readLux(VEML_LUX_AUTO); + white = veml7700.readWhite(true); + measurement->variant.environment_metrics.white_lux = computeLux(white, white > 100); + LOG_INFO("white lux %f, als lux %f", measurement->variant.environment_metrics.white_lux, measurement->variant.environment_metrics.lux); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VEML7700Sensor.h b/src/modules/Telemetry/Sensor/VEML7700Sensor.h index 92883df08..75dd6827e 100644 --- a/src/modules/Telemetry/Sensor/VEML7700Sensor.h +++ b/src/modules/Telemetry/Sensor/VEML7700Sensor.h @@ -6,19 +6,18 @@ #include "TelemetrySensor.h" #include -class VEML7700Sensor : public TelemetrySensor -{ - private: - const float MAX_RES = 0.0036; - const float GAIN_MAX = 2; - const float IT_MAX = 800; - Adafruit_VEML7700 veml7700; - float computeLux(uint16_t rawALS, bool corrected); - float getResolution(void); +class VEML7700Sensor : public TelemetrySensor { +private: + const float MAX_RES = 0.0036; + const float GAIN_MAX = 2; + const float IT_MAX = 800; + Adafruit_VEML7700 veml7700; + float computeLux(uint16_t rawALS, bool corrected); + float getResolution(void); - public: - VEML7700Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +public: + VEML7700Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VoltageSensor.h b/src/modules/Telemetry/Sensor/VoltageSensor.h index 767ffd246..17565a214 100644 --- a/src/modules/Telemetry/Sensor/VoltageSensor.h +++ b/src/modules/Telemetry/Sensor/VoltageSensor.h @@ -4,10 +4,9 @@ #pragma once -class VoltageSensor -{ - public: - virtual uint16_t getBusVoltageMv() = 0; +class VoltageSensor { +public: + virtual uint16_t getBusVoltageMv() = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/nullSensor.cpp b/src/modules/Telemetry/Sensor/nullSensor.cpp index 1d545186a..30af8d09b 100644 --- a/src/modules/Telemetry/Sensor/nullSensor.cpp +++ b/src/modules/Telemetry/Sensor/nullSensor.cpp @@ -9,26 +9,14 @@ NullSensor::NullSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "nullSensor") {} -int32_t NullSensor::runOnce() -{ - return INT32_MAX; -} +int32_t NullSensor::runOnce() { return INT32_MAX; } void NullSensor::setup() {} -bool NullSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - return false; -} +bool NullSensor::getMetrics(meshtastic_Telemetry *measurement) { return false; } -uint16_t NullSensor::getBusVoltageMv() -{ - return 0; -} +uint16_t NullSensor::getBusVoltageMv() { return 0; } -int16_t NullSensor::getCurrentMa() -{ - return 0; -} +int16_t NullSensor::getCurrentMa() { return 0; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/nullSensor.h b/src/modules/Telemetry/Sensor/nullSensor.h index a400acf97..26f9bc567 100644 --- a/src/modules/Telemetry/Sensor/nullSensor.h +++ b/src/modules/Telemetry/Sensor/nullSensor.h @@ -9,20 +9,19 @@ #include "TelemetrySensor.h" #include "VoltageSensor.h" -class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor -{ +class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor { - protected: - virtual void setup() override; +protected: + virtual void setup() override; - public: - NullSensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - int32_t runTrigger() { return 0; } +public: + NullSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + int32_t runTrigger() { return 0; } - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/UnitConversions.cpp b/src/modules/Telemetry/UnitConversions.cpp index fff1ee3d2..7bee372df 100644 --- a/src/modules/Telemetry/UnitConversions.cpp +++ b/src/modules/Telemetry/UnitConversions.cpp @@ -1,21 +1,9 @@ #include "UnitConversions.h" -float UnitConversions::CelsiusToFahrenheit(float celsius) -{ - return (celsius * 9) / 5 + 32; -} +float UnitConversions::CelsiusToFahrenheit(float celsius) { return (celsius * 9) / 5 + 32; } -float UnitConversions::MetersPerSecondToKnots(float metersPerSecond) -{ - return metersPerSecond * 1.94384; -} +float UnitConversions::MetersPerSecondToKnots(float metersPerSecond) { return metersPerSecond * 1.94384; } -float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond) -{ - return metersPerSecond * 2.23694; -} +float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond) { return metersPerSecond * 2.23694; } -float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal) -{ - return hectoPascal * 0.029529983071445; -} +float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal) { return hectoPascal * 0.029529983071445; } diff --git a/src/modules/Telemetry/UnitConversions.h b/src/modules/Telemetry/UnitConversions.h index 638476315..4fa7ca143 100644 --- a/src/modules/Telemetry/UnitConversions.h +++ b/src/modules/Telemetry/UnitConversions.h @@ -1,10 +1,9 @@ #pragma once -class UnitConversions -{ - public: - static float CelsiusToFahrenheit(float celsius); - static float MetersPerSecondToKnots(float metersPerSecond); - static float MetersPerSecondToMilesPerHour(float metersPerSecond); - static float HectoPascalToInchesOfMercury(float hectoPascal); +class UnitConversions { +public: + static float CelsiusToFahrenheit(float celsius); + static float MetersPerSecondToKnots(float metersPerSecond); + static float MetersPerSecondToMilesPerHour(float metersPerSecond); + static float HectoPascalToInchesOfMercury(float hectoPascal); }; diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index 7f889e087..1d07435b0 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -11,39 +11,35 @@ #include "main.h" TextMessageModule *textMessageModule; -ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) -{ +ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - auto &p = mp.decoded; - LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); + auto &p = mp.decoded; + LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif - // We only store/display messages destined for us. - devicestate.rx_text_message = mp; - devicestate.has_rx_text_message = true; - IF_SCREEN( - // Guard against running in MeshtasticUI or with no screen - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - // Store in the central message history - const StoredMessage &sm = messageStore.addFromPacket(mp); + // We only store/display messages destined for us. + devicestate.rx_text_message = mp; + devicestate.has_rx_text_message = true; + IF_SCREEN( + // Guard against running in MeshtasticUI or with no screen + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // Store in the central message history + const StoredMessage &sm = messageStore.addFromPacket(mp); - // Pass message to renderer (banner + thread switching + scroll reset) - // Use the global Screen singleton to retrieve the current OLED display - auto *display = screen ? screen->getDisplayDevice() : nullptr; - graphics::MessageRenderer::handleNewMessage(display, sm, mp); - }) - // Only trigger screen wake if configuration allows it - if (shouldWakeOnReceivedMessage()) { - powerFSM.trigger(EVENT_RECEIVED_MSG); - } + // Pass message to renderer (banner + thread switching + scroll reset) + // Use the global Screen singleton to retrieve the current OLED display + auto *display = screen ? screen->getDisplayDevice() : nullptr; + graphics::MessageRenderer::handleNewMessage(display, sm, mp); + }) + // Only trigger screen wake if configuration allows it + if (shouldWakeOnReceivedMessage()) { + powerFSM.trigger(EVENT_RECEIVED_MSG); + } - // Notify any observers (e.g. external modules that care about packets) - notifyObservers(&mp); + // Notify any observers (e.g. external modules that care about packets) + notifyObservers(&mp); - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } -bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) -{ - return MeshService::isTextPayload(p); -} +bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } diff --git a/src/modules/TextMessageModule.h b/src/modules/TextMessageModule.h index e719f1abc..791ceb88a 100644 --- a/src/modules/TextMessageModule.h +++ b/src/modules/TextMessageModule.h @@ -11,22 +11,21 @@ * * Rendering of messages on screen is no longer done here. */ -class TextMessageModule : public SinglePortModule, public Observable -{ - public: - /** Constructor - * name is for debugging output - */ - TextMessageModule() : SinglePortModule("text", meshtastic_PortNum_TEXT_MESSAGE_APP) {} +class TextMessageModule : public SinglePortModule, public Observable { +public: + /** Constructor + * name is for debugging output + */ + TextMessageModule() : SinglePortModule("text", meshtastic_PortNum_TEXT_MESSAGE_APP) {} - protected: - /** Called to handle a particular incoming message - * - * @return ProcessMessage::STOP if you've guaranteed you've handled this - * message and no other handlers should be considered for it. - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - virtual bool wantPacket(const meshtastic_MeshPacket *p) override; +protected: + /** Called to handle a particular incoming message + * + * @return ProcessMessage::STOP if you've guaranteed you've handled this + * message and no other handlers should be considered for it. + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; }; extern TextMessageModule *textMessageModule; \ No newline at end of file diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 41dc02cd1..57f9ebd8a 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -12,887 +12,863 @@ extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; -void TraceRouteModule::setResultText(const String &text) -{ - resultText = text; - resultLines.clear(); - resultLinesDirty = true; +void TraceRouteModule::setResultText(const String &text) { + resultText = text; + resultLines.clear(); + resultLinesDirty = true; } -void TraceRouteModule::clearResultLines() -{ - resultLines.clear(); - resultLinesDirty = false; +void TraceRouteModule::clearResultLines() { + resultLines.clear(); + resultLinesDirty = false; } #if HAS_SCREEN -void TraceRouteModule::rebuildResultLines(OLEDDisplay *display) -{ - if (!display) { - resultLinesDirty = false; - return; - } - - resultLines.clear(); - - if (resultText.length() == 0) { - resultLinesDirty = false; - return; - } - - int maxWidth = display->getWidth() - 4; - if (maxWidth <= 0) { - resultLinesDirty = false; - return; - } - - int start = 0; - int textLength = resultText.length(); - - while (start <= textLength) { - int newlinePos = resultText.indexOf('\n', start); - String segment; - - if (newlinePos != -1) { - segment = resultText.substring(start, newlinePos); - start = newlinePos + 1; - } else { - segment = resultText.substring(start); - start = textLength + 1; - } - - if (segment.length() == 0) { - resultLines.push_back(""); - continue; - } - - if (display->getStringWidth(segment) <= maxWidth) { - resultLines.push_back(segment); - continue; - } - - String remaining = segment; - - while (remaining.length() > 0) { - String tempLine = ""; - int lastGoodBreak = -1; - bool lineComplete = false; - - for (int i = 0; i < static_cast(remaining.length()); i++) { - char ch = remaining.charAt(i); - String testLine = tempLine + ch; - - if (display->getStringWidth(testLine) > maxWidth) { - if (lastGoodBreak >= 0) { - resultLines.push_back(remaining.substring(0, lastGoodBreak + 1)); - remaining = remaining.substring(lastGoodBreak + 1); - lineComplete = true; - break; - } else if (tempLine.length() > 0) { - resultLines.push_back(tempLine); - remaining = remaining.substring(i); - lineComplete = true; - break; - } else { - resultLines.push_back(String(ch)); - remaining = remaining.substring(i + 1); - lineComplete = true; - break; - } - } else { - tempLine = testLine; - if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') { - lastGoodBreak = i; - } - } - } - - if (!lineComplete) { - if (tempLine.length() > 0) { - resultLines.push_back(tempLine); - } - break; - } - } - } - +void TraceRouteModule::rebuildResultLines(OLEDDisplay *display) { + if (!display) { resultLinesDirty = false; -} -#endif + return; + } -bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) -{ - // We only alter the packet in alterReceivedProtobuf() - return false; // let it be handled by RoutingModule -} + resultLines.clear(); -void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) -{ - const meshtastic_Data &incoming = p.decoded; + if (resultText.length() == 0) { + resultLinesDirty = false; + return; + } - // Update next-hops using returned route - if (incoming.request_id) { - updateNextHops(p, r); - } + int maxWidth = display->getWidth() - 4; + if (maxWidth <= 0) { + resultLinesDirty = false; + return; + } - // Insert unknown hops if necessary - insertUnknownHops(p, r, !incoming.request_id); + int start = 0; + int textLength = resultText.length(); - // Append ID and SNR. If the last hop is to us, we only need to append the SNR - appendMyIDandSNR(r, p.rx_snr, !incoming.request_id, isToUs(&p)); - if (!incoming.request_id) - printRoute(r, p.from, p.to, true); - else - printRoute(r, p.to, p.from, false); + while (start <= textLength) { + int newlinePos = resultText.indexOf('\n', start); + String segment; - // Set updated route to the payload of the to be flooded packet - p.decoded.payload.size = - pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); - - if (tracingNode != 0) { - // check isResponseFromTarget - bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); - bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); - - // Check if this is a trace route response containing our target node - bool containsTargetNode = false; - for (uint8_t i = 0; i < r->route_count; i++) { - if (r->route[i] == tracingNode) { - containsTargetNode = true; - break; - } - } - for (uint8_t i = 0; i < r->route_back_count; i++) { - if (r->route_back[i] == tracingNode) { - containsTargetNode = true; - break; - } - } - - // Check if this response contains a complete route to our target - bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) || - (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); - - LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, - p.from, p.to, incoming.request_id); - LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", - isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute); - - if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { - LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); - - LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); - for (int i = 0; i < r->snr_towards_count; i++) { - LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); - } - for (int i = 0; i < r->snr_back_count; i++) { - LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); - } - - String result = ""; - - // Show request path (from initiator to target) - if (r->route_count > 0) { - result += getNodeName(nodeDB->getNodeNum()); - for (uint8_t i = 0; i < r->route_count; i++) { - result += " > "; - const char *name = getNodeName(r->route[i]); - float snr = - (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; - result += name; - if (snr != 0.0f) { - result += "("; - result += String(snr, 1); - result += "dB)"; - } - } - result += " > "; - result += getNodeName(tracingNode); - if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { - result += "("; - result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); - result += "dB)"; - } - result += "\n"; - } else { - // Direct connection (no intermediate hops) - result += getNodeName(nodeDB->getNodeNum()); - result += " > "; - result += getNodeName(tracingNode); - if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { - result += "("; - result += String((float)r->snr_towards[0] / 4.0f, 1); - result += "dB)"; - } - result += "\n"; - } - - // Show response path (from target back to initiator) - if (r->route_back_count > 0) { - result += getNodeName(tracingNode); - for (int8_t i = r->route_back_count - 1; i >= 0; i--) { - result += " > "; - const char *name = getNodeName(r->route_back[i]); - float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; - result += name; - if (snr != 0.0f) { - result += "("; - result += String(snr, 1); - result += "dB)"; - } - } - // add initiator node - result += " > "; - result += getNodeName(nodeDB->getNodeNum()); - if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { - result += "("; - result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); - result += "dB)"; - } - } else { - // Direct return path (no intermediate hops) - result += getNodeName(tracingNode); - result += " > "; - result += getNodeName(nodeDB->getNodeNum()); - if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { - result += "("; - result += String((float)r->snr_back[0] / 4.0f, 1); - result += "dB)"; - } - } - - LOG_INFO("Trace route result: %s", result.c_str()); - handleTraceRouteResult(result); - } - } -} - -void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) -{ - // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D - // Similarly, if we are C, we can set D as next-hop for D - // If we are A, we can set B as next-hop for B, C and D - - // First check if we were the original sender or in the original route - int8_t nextHopIndex = -1; - if (isToUs(&p)) { - nextHopIndex = 0; // We are the original sender, next hop is first in route + if (newlinePos != -1) { + segment = resultText.substring(start, newlinePos); + start = newlinePos + 1; } else { - // Check if we are in the original route - for (uint8_t i = 0; i < r->route_count; i++) { - if (r->route[i] == nodeDB->getNodeNum()) { - nextHopIndex = i + 1; // Next hop is the one after us - break; - } - } + segment = resultText.substring(start); + start = textLength + 1; } - // If we are in the original route, update the next hops - if (nextHopIndex != -1) { - // For every node after us, we can set the next-hop to the first node after us - NodeNum nextHop; - if (nextHopIndex == r->route_count) { - nextHop = p.from; // We are the last in the route, next hop is destination + if (segment.length() == 0) { + resultLines.push_back(""); + continue; + } + + if (display->getStringWidth(segment) <= maxWidth) { + resultLines.push_back(segment); + continue; + } + + String remaining = segment; + + while (remaining.length() > 0) { + String tempLine = ""; + int lastGoodBreak = -1; + bool lineComplete = false; + + for (int i = 0; i < static_cast(remaining.length()); i++) { + char ch = remaining.charAt(i); + String testLine = tempLine + ch; + + if (display->getStringWidth(testLine) > maxWidth) { + if (lastGoodBreak >= 0) { + resultLines.push_back(remaining.substring(0, lastGoodBreak + 1)); + remaining = remaining.substring(lastGoodBreak + 1); + lineComplete = true; + break; + } else if (tempLine.length() > 0) { + resultLines.push_back(tempLine); + remaining = remaining.substring(i); + lineComplete = true; + break; + } else { + resultLines.push_back(String(ch)); + remaining = remaining.substring(i + 1); + lineComplete = true; + break; + } } else { - nextHop = r->route[nextHopIndex]; + tempLine = testLine; + if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') { + lastGoodBreak = i; + } } + } - if (nextHop == NODENUM_BROADCAST) { - return; + if (!lineComplete) { + if (tempLine.length() > 0) { + resultLines.push_back(tempLine); } - uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop); - - // For the rest of the nodes in the route, set their next-hop - // Note: if we are the last in the route, this loop will not run - for (int8_t i = nextHopIndex; i < r->route_count; i++) { - NodeNum targetNode = r->route[i]; - maybeSetNextHop(targetNode, nextHopByte); - } - - // Also set next-hop for the destination node - maybeSetNextHop(p.from, nextHopByte); + break; + } } + } + + resultLinesDirty = false; +} +#endif + +bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) { + // We only alter the packet in alterReceivedProtobuf() + return false; // let it be handled by RoutingModule } -void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte) -{ - if (target == NODENUM_BROADCAST) - return; +void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { + const meshtastic_Data &incoming = p.decoded; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target); - if (node && node->next_hop != nextHopByte) { - LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte); - node->next_hop = nextHopByte; - } -} + // Update next-hops using returned route + if (incoming.request_id) { + updateNextHops(p, r); + } -void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp) -{ - if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP) - return; + // Insert unknown hops if necessary + insertUnknownHops(p, r, !incoming.request_id); - meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero; - if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &decoded)) - return; + // Append ID and SNR. If the last hop is to us, we only need to append the SNR + appendMyIDandSNR(r, p.rx_snr, !incoming.request_id, isToUs(&p)); + if (!incoming.request_id) + printRoute(r, p.from, p.to, true); + else + printRoute(r, p.to, p.from, false); - handleReceivedProtobuf(mp, &decoded); - // Intentionally modify the packet in-place so downstream relays see our updates. - alterReceivedProtobuf(const_cast(mp), &decoded); -} + // Set updated route to the payload of the to be flooded packet + p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); -void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) -{ - pb_size_t *route_count; - uint32_t *route; - pb_size_t *snr_count; - int8_t *snr_list; + if (tracingNode != 0) { + // check isResponseFromTarget + bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); + bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); - // Pick the correct route array and SNR list - if (isTowardsDestination) { - route_count = &r->route_count; - route = r->route; - snr_count = &r->snr_towards_count; - snr_list = r->snr_towards; - } else { - route_count = &r->route_back_count; - route = r->route_back; - snr_count = &r->snr_back_count; - snr_list = r->snr_back; - } - - // Only insert unknown hops if hop_start is valid - const int8_t hopsTaken = getHopsAway(p); - if (hopsTaken >= 0) { - int8_t diff = hopsTaken - *route_count; - for (int8_t i = 0; i < diff; i++) { - if (*route_count < ROUTE_SIZE) { - route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop - *route_count += 1; - } - } - // Add unknown SNR values if necessary - diff = *route_count - *snr_count; - for (int8_t i = 0; i < diff; i++) { - if (*snr_count < ROUTE_SIZE) { - snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR - *snr_count += 1; - } - } - } -} - -void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, float snr, bool isTowardsDestination, bool SNRonly) -{ - pb_size_t *route_count; - uint32_t *route; - pb_size_t *snr_count; - int8_t *snr_list; - - // Pick the correct route array and SNR list - if (isTowardsDestination) { - route_count = &updated->route_count; - route = updated->route; - snr_count = &updated->snr_towards_count; - snr_list = updated->snr_towards; - } else { - route_count = &updated->route_back_count; - route = updated->route_back; - snr_count = &updated->snr_back_count; - snr_list = updated->snr_back; - } - - if (*snr_count < ROUTE_SIZE) { - snr_list[*snr_count] = (int8_t)(snr * 4); // Convert SNR to 1 byte - *snr_count += 1; - } - if (SNRonly) - return; - - // Length of route array can normally not be exceeded due to the max. hop_limit of 7 - if (*route_count < ROUTE_SIZE) { - route[*route_count] = myNodeInfo.my_node_num; - *route_count += 1; - } else { - LOG_WARN("Route exceeded maximum hop limit!"); // Are you bridging networks? - } -} - -void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) -{ -#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - std::string route = "Route traced:\n"; - route += vformat("0x%x --> ", origin); + // Check if this is a trace route response containing our target node + bool containsTargetNode = false; for (uint8_t i = 0; i < r->route_count; i++) { - if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) - route += vformat("0x%x (%.2fdB) --> ", r->route[i], (float)r->snr_towards[i] / 4); - else - route += vformat("0x%x (?dB) --> ", r->route[i]); + if (r->route[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + for (uint8_t i = 0; i < r->route_back_count; i++) { + if (r->route_back[i] == tracingNode) { + containsTargetNode = true; + break; + } } - // If we are the destination, or it has already reached the destination, print it - if (dest == nodeDB->getNodeNum() || !isTowardsDestination) { - if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) - route += vformat("0x%x (%.2fdB)", dest, (float)r->snr_towards[r->snr_towards_count - 1] / 4); - else - route += vformat("0x%x (?dB)", dest); - } else - route += "..."; + // Check if this response contains a complete route to our target + bool hasCompleteRoute = + (r->route_count > 0 && r->route_back_count > 0) || (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); - // If there's a route back (or we are the destination as then the route is complete), print it - if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) { - route += "\n"; - if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum()) - route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin); - else - route += "..."; + LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, p.from, p.to, + incoming.request_id); + LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", isResponseFromTarget, + isRequestToUs, containsTargetNode, hasCompleteRoute); - for (int8_t i = r->route_back_count - 1; i >= 0; i--) { - if (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) - route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[i] / 4, r->route_back[i]); - else - route += vformat("(?dB) 0x%x <-- ", r->route_back[i]); + if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { + LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); + + LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); + for (int i = 0; i < r->snr_towards_count; i++) { + LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); + } + for (int i = 0; i < r->snr_back_count; i++) { + LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); + } + + String result = ""; + + // Show request path (from initiator to target) + if (r->route_count > 0) { + result += getNodeName(nodeDB->getNodeNum()); + for (uint8_t i = 0; i < r->route_count; i++) { + result += " > "; + const char *name = getNodeName(r->route[i]); + float snr = (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } } - route += vformat("0x%x", dest); + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } else { + // Direct connection (no intermediate hops) + result += getNodeName(nodeDB->getNodeNum()); + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[0] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } + + // Show response path (from target back to initiator) + if (r->route_back_count > 0) { + result += getNodeName(tracingNode); + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + result += " > "; + const char *name = getNodeName(r->route_back[i]); + float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + // add initiator node + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); + result += "dB)"; + } + } else { + // Direct return path (no intermediate hops) + result += getNodeName(tracingNode); + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[0] / 4.0f, 1); + result += "dB)"; + } + } + + LOG_INFO("Trace route result: %s", result.c_str()); + handleTraceRouteResult(result); } - LOG_INFO(route.c_str()); + } +} + +void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { + // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D + // Similarly, if we are C, we can set D as next-hop for D + // If we are A, we can set B as next-hop for B, C and D + + // First check if we were the original sender or in the original route + int8_t nextHopIndex = -1; + if (isToUs(&p)) { + nextHopIndex = 0; // We are the original sender, next hop is first in route + } else { + // Check if we are in the original route + for (uint8_t i = 0; i < r->route_count; i++) { + if (r->route[i] == nodeDB->getNodeNum()) { + nextHopIndex = i + 1; // Next hop is the one after us + break; + } + } + } + + // If we are in the original route, update the next hops + if (nextHopIndex != -1) { + // For every node after us, we can set the next-hop to the first node after us + NodeNum nextHop; + if (nextHopIndex == r->route_count) { + nextHop = p.from; // We are the last in the route, next hop is destination + } else { + nextHop = r->route[nextHopIndex]; + } + + if (nextHop == NODENUM_BROADCAST) { + return; + } + uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop); + + // For the rest of the nodes in the route, set their next-hop + // Note: if we are the last in the route, this loop will not run + for (int8_t i = nextHopIndex; i < r->route_count; i++) { + NodeNum targetNode = r->route[i]; + maybeSetNextHop(targetNode, nextHopByte); + } + + // Also set next-hop for the destination node + maybeSetNextHop(p.from, nextHopByte); + } +} + +void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte) { + if (target == NODENUM_BROADCAST) + return; + + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target); + if (node && node->next_hop != nextHopByte) { + LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte); + node->next_hop = nextHopByte; + } +} + +void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp) { + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP) + return; + + meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero; + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &decoded)) + return; + + handleReceivedProtobuf(mp, &decoded); + // Intentionally modify the packet in-place so downstream relays see our updates. + alterReceivedProtobuf(const_cast(mp), &decoded); +} + +void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) { + pb_size_t *route_count; + uint32_t *route; + pb_size_t *snr_count; + int8_t *snr_list; + + // Pick the correct route array and SNR list + if (isTowardsDestination) { + route_count = &r->route_count; + route = r->route; + snr_count = &r->snr_towards_count; + snr_list = r->snr_towards; + } else { + route_count = &r->route_back_count; + route = r->route_back; + snr_count = &r->snr_back_count; + snr_list = r->snr_back; + } + + // Only insert unknown hops if hop_start is valid + const int8_t hopsTaken = getHopsAway(p); + if (hopsTaken >= 0) { + int8_t diff = hopsTaken - *route_count; + for (int8_t i = 0; i < diff; i++) { + if (*route_count < ROUTE_SIZE) { + route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop + *route_count += 1; + } + } + // Add unknown SNR values if necessary + diff = *route_count - *snr_count; + for (int8_t i = 0; i < diff; i++) { + if (*snr_count < ROUTE_SIZE) { + snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR + *snr_count += 1; + } + } + } +} + +void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, float snr, bool isTowardsDestination, bool SNRonly) { + pb_size_t *route_count; + uint32_t *route; + pb_size_t *snr_count; + int8_t *snr_list; + + // Pick the correct route array and SNR list + if (isTowardsDestination) { + route_count = &updated->route_count; + route = updated->route; + snr_count = &updated->snr_towards_count; + snr_list = updated->snr_towards; + } else { + route_count = &updated->route_back_count; + route = updated->route_back; + snr_count = &updated->snr_back_count; + snr_list = updated->snr_back; + } + + if (*snr_count < ROUTE_SIZE) { + snr_list[*snr_count] = (int8_t)(snr * 4); // Convert SNR to 1 byte + *snr_count += 1; + } + if (SNRonly) + return; + + // Length of route array can normally not be exceeded due to the max. hop_limit of 7 + if (*route_count < ROUTE_SIZE) { + route[*route_count] = myNodeInfo.my_node_num; + *route_count += 1; + } else { + LOG_WARN("Route exceeded maximum hop limit!"); // Are you bridging networks? + } +} + +void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) { +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) + std::string route = "Route traced:\n"; + route += vformat("0x%x --> ", origin); + for (uint8_t i = 0; i < r->route_count; i++) { + if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) + route += vformat("0x%x (%.2fdB) --> ", r->route[i], (float)r->snr_towards[i] / 4); + else + route += vformat("0x%x (?dB) --> ", r->route[i]); + } + // If we are the destination, or it has already reached the destination, print it + if (dest == nodeDB->getNodeNum() || !isTowardsDestination) { + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) + route += vformat("0x%x (%.2fdB)", dest, (float)r->snr_towards[r->snr_towards_count - 1] / 4); + + else + route += vformat("0x%x (?dB)", dest); + } else + route += "..."; + + // If there's a route back (or we are the destination as then the route is complete), print it + if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) { + route += "\n"; + if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum()) + route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin); + else + route += "..."; + + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + if (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) + route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[i] / 4, r->route_back[i]); + else + route += vformat("(?dB) 0x%x <-- ", r->route_back[i]); + } + route += vformat("0x%x", dest); + } + LOG_INFO(route.c_str()); #endif } -meshtastic_MeshPacket *TraceRouteModule::allocReply() -{ - assert(currentRequest); +meshtastic_MeshPacket *TraceRouteModule::allocReply() { + assert(currentRequest); - // Ignore multi-hop broadcast requests - if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { - ignoreRequest = true; - return NULL; - } + // Ignore multi-hop broadcast requests + if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { + ignoreRequest = true; + return NULL; + } - // Copy the payload of the current request - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_RouteDiscovery scratch; - meshtastic_RouteDiscovery *updated = NULL; - memset(&scratch, 0, sizeof(scratch)); - pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_RouteDiscovery_msg, &scratch); - updated = &scratch; + // Copy the payload of the current request + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *updated = NULL; + memset(&scratch, 0, sizeof(scratch)); + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_RouteDiscovery_msg, &scratch); + updated = &scratch; - // Create a MeshPacket with this payload and set it as the reply - meshtastic_MeshPacket *reply = allocDataProtobuf(*updated); + // Create a MeshPacket with this payload and set it as the reply + meshtastic_MeshPacket *reply = allocDataProtobuf(*updated); - return reply; + return reply; } TraceRouteModule::TraceRouteModule() - : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") -{ - ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; - isPromiscuous = true; // We need to update the route even if it is not destined to us + : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") { + ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; + isPromiscuous = true; // We need to update the route even if it is not destined to us } -const char *TraceRouteModule::getNodeName(NodeNum node) -{ - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info && info->has_user) { - if (strlen(info->user.short_name) > 0) { - return info->user.short_name; - } - if (strlen(info->user.long_name) > 0) { - return info->user.long_name; - } +const char *TraceRouteModule::getNodeName(NodeNum node) { + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user) { + if (strlen(info->user.short_name) > 0) { + return info->user.short_name; } + if (strlen(info->user.long_name) > 0) { + return info->user.long_name; + } + } - static char fallback[12]; - snprintf(fallback, sizeof(fallback), "0x%08x", node); - return fallback; + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; } -bool TraceRouteModule::startTraceRoute(NodeNum node) -{ - LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); - unsigned long now = millis(); +bool TraceRouteModule::startTraceRoute(NodeNum node) { + LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); + unsigned long now = millis(); - if (node == 0 || node == NODENUM_BROADCAST) { - LOG_ERROR("Invalid node number for trace route: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Invalid node"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return false; - } - - if (node == nodeDB->getNodeNum()) { - LOG_ERROR("Cannot trace route to self: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Cannot trace self"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return false; - } - - if (!initialized) { - lastTraceRouteTime = 0; - initialized = true; - LOG_INFO("TraceRoute initialized for first time"); - } - - if (runState == TRACEROUTE_STATE_TRACKING) { - LOG_INFO("TraceRoute already in progress"); - return false; - } - - if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { - // Cooldown - unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; - bannerText = String("Wait for ") + String(wait) + String("s"); - runState = TRACEROUTE_STATE_COOLDOWN; - resultText = ""; - clearResultLines(); - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); - return false; - } - - tracingNode = node; - lastTraceRouteTime = now; - runState = TRACEROUTE_STATE_TRACKING; - resultText = ""; - clearResultLines(); - bannerText = String("Tracing ") + getNodeName(node); - - LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); - - // 请求焦点,然后触发UI更新事件 - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - // 设置定时器来处理超时检查 - setIntervalFromNow(1000); // 每秒检查一次状态 - - meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; - LOG_INFO("Creating RouteDiscovery protobuf..."); - - // Allocate a packet directly from router like the reference code - meshtastic_MeshPacket *p = router->allocForSending(); - if (p) { - // Set destination and port - p->to = node; - p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; - p->decoded.want_response = true; - - // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) - p->want_ack = true; - - // Manually encode the RouteDiscovery payload - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); - - LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, - p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); - LOG_INFO("About to call service->sendToMesh..."); - - if (service) { - LOG_INFO("MeshService is available, sending packet..."); - service->sendToMesh(p, RX_SRC_USER); - LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); - } else { - LOG_ERROR("MeshService is NULL!"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Service unavailable"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e2; - e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e2); - return false; - } - } else { - LOG_ERROR("Failed to allocate TraceRoute packet from router"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Failed to send"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e2; - e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e2); - return false; - } - return true; -} - -void TraceRouteModule::launch(NodeNum node) -{ - if (node == 0 || node == NODENUM_BROADCAST) { - LOG_ERROR("Invalid node number for trace route: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Invalid node"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return; - } - - if (node == nodeDB->getNodeNum()) { - LOG_ERROR("Cannot trace route to self: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Cannot trace self"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return; - } - - if (!initialized) { - lastTraceRouteTime = 0; - initialized = true; - LOG_INFO("TraceRoute initialized for first time"); - } - - unsigned long now = millis(); - if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { - unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; - bannerText = String("Wait for ") + String(wait) + String("s"); - runState = TRACEROUTE_STATE_COOLDOWN; - resultText = ""; - clearResultLines(); - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); - return; - } - - runState = TRACEROUTE_STATE_TRACKING; - tracingNode = node; - lastTraceRouteTime = now; - resultText = ""; - clearResultLines(); - bannerText = String("Tracing ") + getNodeName(node); - - requestFocus(); - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - setIntervalFromNow(1000); - - meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; - LOG_INFO("Creating RouteDiscovery protobuf..."); - - meshtastic_MeshPacket *p = router->allocForSending(); - if (p) { - p->to = node; - p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; - p->decoded.want_response = true; - - // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) - p->want_ack = true; - - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); - - LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, - p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); - - if (service) { - service->sendToMesh(p, RX_SRC_USER); - LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); - } else { - LOG_ERROR("MeshService is NULL!"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Service unavailable"); - resultShowTime = millis(); - tracingNode = 0; - } - } else { - LOG_ERROR("Failed to allocate TraceRoute packet from router"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Failed to send"); - resultShowTime = millis(); - tracingNode = 0; - } -} - -void TraceRouteModule::handleTraceRouteResult(const String &result) -{ - setResultText(result); + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; + setResultText("Invalid node"); resultShowTime = millis(); tracingNode = 0; - LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } - setIntervalFromNow(1000); + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Cannot trace self"); + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + LOG_INFO("TraceRoute already in progress"); + return false; + } + + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + // Cooldown + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + resultText = ""; + clearResultLines(); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return false; + } + + tracingNode = node; + lastTraceRouteTime = now; + runState = TRACEROUTE_STATE_TRACKING; + resultText = ""; + clearResultLines(); + bannerText = String("Tracing ") + getNodeName(node); + + LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); + + // 请求焦点,然后触发UI更新事件 + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + // 设置定时器来处理超时检查 + setIntervalFromNow(1000); // 每秒检查一次状态 + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + // Allocate a packet directly from router like the reference code + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + // Set destination and port + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) + p->want_ack = true; + + // Manually encode the RouteDiscovery payload + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum, + p->decoded.want_response, p->decoded.payload.size); + LOG_INFO("About to call service->sendToMesh..."); + + if (service) { + LOG_INFO("MeshService is available, sending packet..."); + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Service unavailable"); + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Failed to send"); + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + return true; +} + +void TraceRouteModule::launch(NodeNum node) { + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Invalid node"); + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Cannot trace self"); + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + unsigned long now = millis(); + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + resultText = ""; + clearResultLines(); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return; + } + + runState = TRACEROUTE_STATE_TRACKING; + tracingNode = node; + lastTraceRouteTime = now; + resultText = ""; + clearResultLines(); + bannerText = String("Tracing ") + getNodeName(node); + + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(1000); + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) + p->want_ack = true; + + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum, + p->decoded.want_response, p->decoded.payload.size); + + if (service) { + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Service unavailable"); + resultShowTime = millis(); + tracingNode = 0; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Failed to send"); + resultShowTime = millis(); + tracingNode = 0; + } +} + +void TraceRouteModule::handleTraceRouteResult(const String &result) { + setResultText(result); + runState = TRACEROUTE_STATE_RESULT; + resultShowTime = millis(); + tracingNode = 0; + + LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); + + setIntervalFromNow(1000); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); +} + +bool TraceRouteModule::shouldDraw() { + bool draw = (runState != TRACEROUTE_STATE_IDLE); + static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; + if (runState != lastLoggedState) { + LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); + lastLoggedState = runState; + } + return draw; +} +#if HAS_SCREEN +void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + + if (runState == TRACEROUTE_STATE_TRACKING) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + + } else if (runState == TRACEROUTE_STATE_RESULT) { + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + display->drawString(x, y, "Route Result"); + + int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (resultText.length() > 0) { + if (resultLinesDirty) { + rebuildResultLines(display); + } + + int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing + for (size_t i = 0; i < resultLines.size(); i++) { + int lineY = contentStartY + (i * lineHeight); + if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { + display->drawString(x + 2, lineY, resultLines[i]); + } + } + } + + } else if (runState == TRACEROUTE_STATE_COOLDOWN) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + } +} +#endif // HAS_SCREEN +int32_t TraceRouteModule::runOnce() { + unsigned long now = millis(); + + if (runState == TRACEROUTE_STATE_IDLE) { + return INT32_MAX; + } + + // Check for tracking timeout + if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { + LOG_INFO("TraceRoute timeout, no response received"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("No response received"); + resultShowTime = now; + tracingNode = 0; requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); - LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); -} - -bool TraceRouteModule::shouldDraw() -{ - bool draw = (runState != TRACEROUTE_STATE_IDLE); - static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; - if (runState != lastLoggedState) { - LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); - lastLoggedState = runState; - } - return draw; -} -#if HAS_SCREEN -void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); - - display->setTextAlignment(TEXT_ALIGN_CENTER); - - if (runState == TRACEROUTE_STATE_TRACKING) { - display->setFont(FONT_MEDIUM); - int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); - display->drawString(display->getWidth() / 2 + x, centerY, bannerText); - - } else if (runState == TRACEROUTE_STATE_RESULT) { - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - display->drawString(x, y, "Route Result"); - - int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - if (resultText.length() > 0) { - if (resultLinesDirty) { - rebuildResultLines(display); - } - - int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing - for (size_t i = 0; i < resultLines.size(); i++) { - int lineY = contentStartY + (i * lineHeight); - if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { - display->drawString(x + 2, lineY, resultLines[i]); - } - } - } - - } else if (runState == TRACEROUTE_STATE_COOLDOWN) { - display->setFont(FONT_MEDIUM); - int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); - display->drawString(display->getWidth() / 2 + x, centerY, bannerText); - } -} -#endif // HAS_SCREEN -int32_t TraceRouteModule::runOnce() -{ - unsigned long now = millis(); - - if (runState == TRACEROUTE_STATE_IDLE) { - return INT32_MAX; - } - - // Check for tracking timeout - if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { - LOG_INFO("TraceRoute timeout, no response received"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("No response received"); - resultShowTime = now; - tracingNode = 0; - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - setIntervalFromNow(resultDisplayMs); - return resultDisplayMs; - } - - // Update cooldown display every second - if (runState == TRACEROUTE_STATE_COOLDOWN) { - unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; - if (wait > 0) { - String newBannerText = String("Wait for ") + String(wait) + String("s"); - bannerText = newBannerText; - LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); - - // Force flash UI - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - if (screen) { - screen->forceDisplay(); - } - - return 1000; - } else { - // Cooldown finished - LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); - runState = TRACEROUTE_STATE_IDLE; - resultText = ""; - clearResultLines(); - bannerText = ""; - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return INT32_MAX; - } - } - - if (runState == TRACEROUTE_STATE_RESULT) { - if (now - resultShowTime >= resultDisplayMs) { - LOG_INFO("TraceRoute result display timeout, returning to IDLE"); - runState = TRACEROUTE_STATE_IDLE; - resultText = ""; - clearResultLines(); - bannerText = ""; - tracingNode = 0; - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return INT32_MAX; - } else { - return 1000; - } - } - - if (runState == TRACEROUTE_STATE_TRACKING) { - return 1000; - } - - return INT32_MAX; + setIntervalFromNow(resultDisplayMs); + return resultDisplayMs; + } + + // Update cooldown display every second + if (runState == TRACEROUTE_STATE_COOLDOWN) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + if (wait > 0) { + String newBannerText = String("Wait for ") + String(wait) + String("s"); + bannerText = newBannerText; + LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); + + // Force flash UI + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + if (screen) { + screen->forceDisplay(); + } + + return 1000; + } else { + // Cooldown finished + LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + clearResultLines(); + bannerText = ""; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } + } + + if (runState == TRACEROUTE_STATE_RESULT) { + if (now - resultShowTime >= resultDisplayMs) { + LOG_INFO("TraceRoute result display timeout, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + clearResultLines(); + bannerText = ""; + tracingNode = 0; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } else { + return 1000; + } + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + return 1000; + } + + return INT32_MAX; } diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index a40ed7733..edde36638 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -16,74 +16,71 @@ */ enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN }; -class TraceRouteModule : public ProtobufModule, - public Observable, - private concurrency::OSThread -{ - public: - TraceRouteModule(); +class TraceRouteModule : public ProtobufModule, public Observable, private concurrency::OSThread { +public: + TraceRouteModule(); - bool startTraceRoute(NodeNum node); - void launch(NodeNum node); - void handleTraceRouteResult(const String &result); - bool shouldDraw(); + bool startTraceRoute(NodeNum node); + void launch(NodeNum node); + void handleTraceRouteResult(const String &result); + bool shouldDraw(); #if HAS_SCREEN - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - const char *getNodeName(NodeNum node); + const char *getNodeName(NodeNum node); - virtual bool wantUIFrame() override { return shouldDraw(); } - virtual Observable *getUIFrameObservable() override { return this; } + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } - void processUpgradedPacket(const meshtastic_MeshPacket &mp); + void processUpgradedPacket(const meshtastic_MeshPacket &mp); - protected: - bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; +protected: + bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; - virtual meshtastic_MeshPacket *allocReply() override; + virtual meshtastic_MeshPacket *allocReply() override; - /* Called before rebroadcasting a RouteDiscovery payload in order to update - the route array containing the IDs of nodes this packet went through */ - void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; + /* Called before rebroadcasting a RouteDiscovery payload in order to update + the route array containing the IDs of nodes this packet went through */ + void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - private: - void setResultText(const String &text); - void clearResultLines(); +private: + void setResultText(const String &text); + void clearResultLines(); #if HAS_SCREEN - void rebuildResultLines(OLEDDisplay *display); + void rebuildResultLines(OLEDDisplay *display); #endif - // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit - void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); + // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit + void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); - // Call to add your ID to the route array of a RouteDiscovery message - void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); + // Call to add your ID to the route array of a RouteDiscovery message + void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); - // Update next-hops in the routing table based on the returned route - void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); + // Update next-hops in the routing table based on the returned route + void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); - // Helper to update next-hop for a single node - void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); + // Helper to update next-hop for a single node + void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); - /* Call to print the route array of a RouteDiscovery message. - Set origin to where the request came from. - Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ - void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); + /* Call to print the route array of a RouteDiscovery message. + Set origin to where the request came from. + Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ + void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); - TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; - unsigned long lastTraceRouteTime = 0; - unsigned long resultShowTime = 0; - unsigned long cooldownMs = 30000; - unsigned long resultDisplayMs = 10000; - unsigned long trackingTimeoutMs = 10000; - String bannerText; - String resultText; - std::vector resultLines; - bool resultLinesDirty = false; - NodeNum tracingNode = 0; - bool initialized = false; + TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; + unsigned long lastTraceRouteTime = 0; + unsigned long resultShowTime = 0; + unsigned long cooldownMs = 30000; + unsigned long resultDisplayMs = 10000; + unsigned long trackingTimeoutMs = 10000; + String bannerText; + String resultText; + std::vector resultLines; + bool resultLinesDirty = false; + NodeNum tracingNode = 0; + bool initialized = false; }; extern TraceRouteModule *traceRouteModule; diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 4db80ba18..7abe59762 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -15,164 +15,152 @@ WaypointModule *waypointModule; -static inline float degToRad(float deg) -{ - return deg * PI / 180.0f; -} -static inline float radToDeg(float rad) -{ - return rad * 180.0f / PI; -} +static inline float degToRad(float deg) { return deg * PI / 180.0f; } +static inline float radToDeg(float rad) { return rad * 180.0f / PI; } -ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) -{ +ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - auto &p = mp.decoded; - LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); + auto &p = mp.decoded; + LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif - // We only store/display messages destined for us. - // Keep a copy of the most recent text message. - devicestate.rx_waypoint = mp; - devicestate.has_rx_waypoint = true; + // We only store/display messages destined for us. + // Keep a copy of the most recent text message. + devicestate.rx_waypoint = mp; + devicestate.has_rx_waypoint = true; - powerFSM.trigger(EVENT_RECEIVED_MSG); + powerFSM.trigger(EVENT_RECEIVED_MSG); #if HAS_SCREEN - UIFrameEvent e; + UIFrameEvent e; - // New or updated waypoint: focus on this frame next time Screen::setFrames runs - if (shouldDraw()) { - requestFocus(); - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - } + // New or updated waypoint: focus on this frame next time Screen::setFrames runs + if (shouldDraw()) { + requestFocus(); + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + } - // Deleting an old waypoint: remove the frame quietly, don't change frame position if possible - else - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND; + // Deleting an old waypoint: remove the frame quietly, don't change frame position if possible + else + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND; - notifyObservers(&e); + notifyObservers(&e); #endif - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } #if HAS_SCREEN -bool WaypointModule::shouldDraw() -{ +bool WaypointModule::shouldDraw() { #if !MESHTASTIC_EXCLUDE_WAYPOINT - if (!screen || !devicestate.has_rx_waypoint) - return false; - - meshtastic_Waypoint wp{}; // <- replaces memset - if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, - &meshtastic_Waypoint_msg, &wp)) { - return wp.expire > getTime(); - } - return false; // no LOG_ERROR, no flag writes -#else + if (!screen || !devicestate.has_rx_waypoint) return false; + + meshtastic_Waypoint wp{}; // <- replaces memset + if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, &meshtastic_Waypoint_msg, + &wp)) { + return wp.expire > getTime(); + } + return false; // no LOG_ERROR, no flag writes +#else + return false; #endif } /// Draw the last waypoint we received -void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - if (!screen) - return; - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (!screen) + return; + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "Waypoint"; + // === Set Title + const char *titleStr = "Waypoint"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - const int w = display->getWidth(); - const int h = display->getHeight(); + const int w = display->getWidth(); + const int h = display->getHeight(); - // Decode the waypoint - const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; - meshtastic_Waypoint wp{}; - if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { - devicestate.has_rx_waypoint = false; - return; + // Decode the waypoint + const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; + meshtastic_Waypoint wp{}; + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { + devicestate.has_rx_waypoint = false; + return; + } + + // Get timestamp info. Will pass as a field to drawColumns + char lastStr[20]; + getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); + + // Will contain distance information, passed as a field to drawColumns + char distStr[20]; + + // Get our node, to use our own position + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + // Dimensions / co-ordinates for the compass/circle + const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); + const int16_t compassX = x + w - (compassDiam / 2) - 5; + const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) + ? y + h / 2 + : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; + + // If our node has a position: + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { + const meshtastic_PositionLite &op = ourNode->position; + float myHeading; + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = 0; + } else { + if (screen->hasHeading()) + myHeading = degToRad(screen->getHeading()); + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); - // Get timestamp info. Will pass as a field to drawColumns - char lastStr[20]; - getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); + // Compass bearing to waypoint + float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); + // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly + // If the top of the compass is not a static north we need adjust bearingToOther based on heading + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) + bearingToOther -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - // Will contain distance information, passed as a field to drawColumns - char distStr[20]; + float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; + bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); - // Get our node, to use our own position - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - - // Dimensions / co-ordinates for the compass/circle - const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); - const int16_t compassX = x + w - (compassDiam / 2) - 5; - const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - ? y + h / 2 - : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; - - // If our node has a position: - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { - const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; - } else { - if (screen->hasHeading()) - myHeading = degToRad(screen->getHeading()); - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); - - // Compass bearing to waypoint - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearingToOther -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); - - // Distance to Waypoint - float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - float feet = d * METERS_TO_FEET; - snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", - feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); - } else { - snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, - bearingToOtherDegrees); - } + // Distance to Waypoint + float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + float feet = d * METERS_TO_FEET; + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); + } else { + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, bearingToOtherDegrees); } + } - else { - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + else { + display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); - // ? in the distance field - snprintf(distStr, sizeof(distStr), "? %s ?°", - (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); - } + // ? in the distance field + snprintf(distStr, sizeof(distStr), "? %s ?°", (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); + } - // Draw compass circle - display->drawCircle(compassX, compassY, compassDiam / 2); + // Draw compass circle + display->drawCircle(compassX, compassY, compassDiam / 2); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! - display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); - display->drawString(0, graphics::getTextPositions(display)[line++], distStr); + display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! + display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); + display->drawString(0, graphics::getTextPositions(display)[line++], distStr); } #endif diff --git a/src/modules/WaypointModule.h b/src/modules/WaypointModule.h index 4c9c7b86b..f56d9d0c4 100644 --- a/src/modules/WaypointModule.h +++ b/src/modules/WaypointModule.h @@ -5,29 +5,28 @@ /** * Waypoint message handling for meshtastic */ -class WaypointModule : public SinglePortModule, public Observable -{ - public: - /** Constructor - * name is for debugging output - */ - WaypointModule() : SinglePortModule("waypoint", meshtastic_PortNum_WAYPOINT_APP) {} +class WaypointModule : public SinglePortModule, public Observable { +public: + /** Constructor + * name is for debugging output + */ + WaypointModule() : SinglePortModule("waypoint", meshtastic_PortNum_WAYPOINT_APP) {} #if HAS_SCREEN - bool shouldDraw(); + bool shouldDraw(); #endif - protected: - /** Called to handle a particular incoming message +protected: + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for - it - */ + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + considered for it + */ - virtual Observable *getUIFrameObservable() override { return this; } + virtual Observable *getUIFrameObservable() override { return this; } #if HAS_SCREEN - virtual bool wantUIFrame() override { return this->shouldDraw(); } - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual bool wantUIFrame() override { return this->shouldDraw(); } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; extern WaypointModule *waypointModule; \ No newline at end of file diff --git a/src/modules/esp32/AudioModule.cpp b/src/modules/esp32/AudioModule.cpp index 77cc94359..de4e89e9f 100644 --- a/src/modules/esp32/AudioModule.cpp +++ b/src/modules/esp32/AudioModule.cpp @@ -9,8 +9,8 @@ /* AudioModule - A interface to send raw codec2 audio data over the mesh network. Based on the example code from the ESP32_codec2 project. - https://github.com/deulis/ESP32_Codec2 + A interface to send raw codec2 audio data over the mesh network. Based on the example code from the ESP32_codec2 + project. https://github.com/deulis/ESP32_Codec2 Codec 2 is a low-bitrate speech audio codec (speech coding) that is patent free and open source develop by David Grant Rowe. @@ -19,8 +19,8 @@ Basic Usage: 1) Enable the module by setting audio.codec2_enabled to 1. 2) Set the pins for the I2S interface. Recommended on TLora is I2S_WS 13/I2S_SD 15/I2S_SIN 2/I2S_SCK 14 - 3) Set audio.bitrate to the desired codec2 rate (CODEC2_3200, CODEC2_2400, CODEC2_1600, CODEC2_1400, CODEC2_1300, - CODEC2_1200, CODEC2_700, CODEC2_700B) + 3) Set audio.bitrate to the desired codec2 rate (CODEC2_3200, CODEC2_2400, CODEC2_1600, CODEC2_1400, + CODEC2_1300, CODEC2_1200, CODEC2_700, CODEC2_700B) KNOWN PROBLEMS * Half Duplex @@ -41,250 +41,238 @@ AudioModule *audioModule; #include "graphics/ScreenFonts.h" -void run_codec2(void *parameter) -{ - // 4 bytes of header in each frame hex c0 de c2 plus the bitrate - memcpy(audioModule->tx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)); +void run_codec2(void *parameter) { + // 4 bytes of header in each frame hex c0 de c2 plus the bitrate + memcpy(audioModule->tx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)); - LOG_INFO("Start codec2 task"); + LOG_INFO("Start codec2 task"); - while (true) { - uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(10000)); + while (true) { + uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(10000)); - if (tcount != 0) { - if (audioModule->radio_state == RadioState::tx) { - for (int i = 0; i < audioModule->adc_buffer_size; i++) - audioModule->speech[i] = (int16_t)hp_filter.Update((float)audioModule->speech[i]); + if (tcount != 0) { + if (audioModule->radio_state == RadioState::tx) { + for (int i = 0; i < audioModule->adc_buffer_size; i++) + audioModule->speech[i] = (int16_t)hp_filter.Update((float)audioModule->speech[i]); - codec2_encode(audioModule->codec2, audioModule->tx_encode_frame + audioModule->tx_encode_frame_index, - audioModule->speech); - audioModule->tx_encode_frame_index += audioModule->encode_codec_size; + codec2_encode(audioModule->codec2, audioModule->tx_encode_frame + audioModule->tx_encode_frame_index, audioModule->speech); + audioModule->tx_encode_frame_index += audioModule->encode_codec_size; - if (audioModule->tx_encode_frame_index == (audioModule->encode_frame_size + sizeof(audioModule->tx_header))) { - LOG_INFO("Send %d codec2 bytes", audioModule->encode_frame_size); - audioModule->sendPayload(); - audioModule->tx_encode_frame_index = sizeof(audioModule->tx_header); - } - } - if (audioModule->radio_state == RadioState::rx) { - size_t bytesOut = 0; - if (memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)) == 0) { - for (int i = 4; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) { - codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); - i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut, - pdMS_TO_TICKS(500)); - } - } else { - // if the buffer header does not match our own codec, make a temp decoding setup. - CODEC2 *tmp_codec2 = codec2_create(audioModule->rx_encode_frame[3]); - codec2_set_lpc_post_filter(tmp_codec2, 1, 0, 0.8, 0.2); - int tmp_encode_codec_size = (codec2_bits_per_frame(tmp_codec2) + 7) / 8; - int tmp_adc_buffer_size = codec2_samples_per_frame(tmp_codec2); - for (int i = 4; i < audioModule->rx_encode_frame_index; i += tmp_encode_codec_size) { - codec2_decode(tmp_codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); - i2s_write(I2S_PORT, &audioModule->output_buffer, tmp_adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); - } - codec2_destroy(tmp_codec2); - } - } + if (audioModule->tx_encode_frame_index == (audioModule->encode_frame_size + sizeof(audioModule->tx_header))) { + LOG_INFO("Send %d codec2 bytes", audioModule->encode_frame_size); + audioModule->sendPayload(); + audioModule->tx_encode_frame_index = sizeof(audioModule->tx_header); } - } -} - -AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_APP), concurrency::OSThread("Audio") -{ - // moduleConfig.audio.codec2_enabled = true; - // moduleConfig.audio.i2s_ws = 13; - // moduleConfig.audio.i2s_sd = 15; - // moduleConfig.audio.i2s_din = 22; - // moduleConfig.audio.i2s_sck = 14; - // moduleConfig.audio.ptt_pin = 39; - - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); - tx_header.mode = (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1; - codec2_set_lpc_post_filter(codec2, 1, 0, 0.8, 0.2); - encode_codec_size = (codec2_bits_per_frame(codec2) + 7) / 8; - encode_frame_num = (meshtastic_Constants_DATA_PAYLOAD_LEN - sizeof(tx_header)) / encode_codec_size; - encode_frame_size = encode_frame_num * encode_codec_size; // max 233 bytes + 4 header bytes - adc_buffer_size = codec2_samples_per_frame(codec2); - LOG_INFO("Use %d frames of %d bytes for a total payload length of %d bytes", encode_frame_num, encode_codec_size, - encode_frame_size); - xTaskCreate(&run_codec2, "codec2_task", 30000, NULL, 5, &codec2HandlerTask); - } else { - disable(); - } -} - -void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - char buffer[50]; - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawStringf(0 + x, 0 + y, buffer, "Codec2 Mode %d Audio", - (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - display->setColor(WHITE); - display->setFont(FONT_LARGE); - display->setTextAlignment(TEXT_ALIGN_CENTER); - switch (radio_state) { - case RadioState::tx: - display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "PTT"); - break; - default: - display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "Receive"); - break; - } -} - -int32_t AudioModule::runOnce() -{ - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - esp_err_t res; - if (firstTime) { - // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC - LOG_INFO("Init I2S SD: %d DIN: %d WS: %d SCK: %d", moduleConfig.audio.i2s_sd, moduleConfig.audio.i2s_din, - moduleConfig.audio.i2s_ws, moduleConfig.audio.i2s_sck); - i2s_config_t i2s_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | (moduleConfig.audio.i2s_sd ? I2S_MODE_RX : 0) | - (moduleConfig.audio.i2s_din ? I2S_MODE_TX : 0)), - .sample_rate = 8000, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, - .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), - .intr_alloc_flags = 0, - .dma_buf_count = 8, - .dma_buf_len = adc_buffer_size, // 320 * 2 bytes - .use_apll = false, - .tx_desc_auto_clear = true, - .fixed_mclk = 0}; - res = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); - if (res != ESP_OK) { - LOG_ERROR("Failed to install I2S driver: %d", res); - } - - const i2s_pin_config_t pin_config = { - .bck_io_num = moduleConfig.audio.i2s_sck, - .ws_io_num = moduleConfig.audio.i2s_ws, - .data_out_num = moduleConfig.audio.i2s_din ? moduleConfig.audio.i2s_din : I2S_PIN_NO_CHANGE, - .data_in_num = moduleConfig.audio.i2s_sd ? moduleConfig.audio.i2s_sd : I2S_PIN_NO_CHANGE}; - res = i2s_set_pin(I2S_PORT, &pin_config); - if (res != ESP_OK) { - LOG_ERROR("Failed to set I2S pin config: %d", res); - } - - res = i2s_start(I2S_PORT); - if (res != ESP_OK) { - LOG_ERROR("Failed to start I2S: %d", res); - } - - radio_state = RadioState::rx; - - // Configure PTT input - LOG_INFO("Init PTT on Pin %u", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); - pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT); - - firstTime = false; + } + if (audioModule->radio_state == RadioState::rx) { + size_t bytesOut = 0; + if (memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)) == 0) { + for (int i = 4; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) { + codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); + i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); + } } else { - UIFrameEvent e; - // Check if PTT is pressed. TODO hook that into Onebutton/Interrupt drive. - if (digitalRead(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN) == HIGH) { - if (radio_state == RadioState::rx) { - LOG_INFO("PTT pressed, switching to TX"); - radio_state = RadioState::tx; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - } - } else { - if (radio_state == RadioState::tx) { - LOG_INFO("PTT released, switching to RX"); - if (tx_encode_frame_index > sizeof(tx_header)) { - // Send the incomplete frame - LOG_INFO("Send %d codec2 bytes (incomplete)", tx_encode_frame_index); - sendPayload(); - } - tx_encode_frame_index = sizeof(tx_header); - radio_state = RadioState::rx; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - } - } - if (radio_state == RadioState::tx) { - // Get I2S data from the microphone and place in data buffer - size_t bytesIn = 0; - res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn, - pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. - - if (res == ESP_OK) { - adc_buffer_index += bytesIn; - if (adc_buffer_index == adc_buffer_size) { - adc_buffer_index = 0; - memcpy((void *)speech, (void *)adc_buffer, 2 * adc_buffer_size); - // Notify run_codec2 task that the buffer is ready. - radio_state = RadioState::tx; - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) - YIELD_FROM_ISR(xHigherPriorityTaskWoken); - } - } - } + // if the buffer header does not match our own codec, make a temp decoding setup. + CODEC2 *tmp_codec2 = codec2_create(audioModule->rx_encode_frame[3]); + codec2_set_lpc_post_filter(tmp_codec2, 1, 0, 0.8, 0.2); + int tmp_encode_codec_size = (codec2_bits_per_frame(tmp_codec2) + 7) / 8; + int tmp_adc_buffer_size = codec2_samples_per_frame(tmp_codec2); + for (int i = 4; i < audioModule->rx_encode_frame_index; i += tmp_encode_codec_size) { + codec2_decode(tmp_codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); + i2s_write(I2S_PORT, &audioModule->output_buffer, tmp_adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); + } + codec2_destroy(tmp_codec2); } - return 100; + } + } + } +} + +AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_APP), concurrency::OSThread("Audio") { + // moduleConfig.audio.codec2_enabled = true; + // moduleConfig.audio.i2s_ws = 13; + // moduleConfig.audio.i2s_sd = 15; + // moduleConfig.audio.i2s_din = 22; + // moduleConfig.audio.i2s_sck = 14; + // moduleConfig.audio.ptt_pin = 39; + + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); + tx_header.mode = (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1; + codec2_set_lpc_post_filter(codec2, 1, 0, 0.8, 0.2); + encode_codec_size = (codec2_bits_per_frame(codec2) + 7) / 8; + encode_frame_num = (meshtastic_Constants_DATA_PAYLOAD_LEN - sizeof(tx_header)) / encode_codec_size; + encode_frame_size = encode_frame_num * encode_codec_size; // max 233 bytes + 4 header bytes + adc_buffer_size = codec2_samples_per_frame(codec2); + LOG_INFO("Use %d frames of %d bytes for a total payload length of %d bytes", encode_frame_num, encode_codec_size, encode_frame_size); + xTaskCreate(&run_codec2, "codec2_task", 30000, NULL, 5, &codec2HandlerTask); + } else { + disable(); + } +} + +void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + char buffer[50]; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawStringf(0 + x, 0 + y, buffer, "Codec2 Mode %d Audio", + (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + display->setColor(WHITE); + display->setFont(FONT_LARGE); + display->setTextAlignment(TEXT_ALIGN_CENTER); + switch (radio_state) { + case RadioState::tx: + display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "PTT"); + break; + default: + display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "Receive"); + break; + } +} + +int32_t AudioModule::runOnce() { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + esp_err_t res; + if (firstTime) { + // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC + LOG_INFO("Init I2S SD: %d DIN: %d WS: %d SCK: %d", moduleConfig.audio.i2s_sd, moduleConfig.audio.i2s_din, moduleConfig.audio.i2s_ws, + moduleConfig.audio.i2s_sck); + i2s_config_t i2s_config = { + .mode = (i2s_mode_t)(I2S_MODE_MASTER | (moduleConfig.audio.i2s_sd ? I2S_MODE_RX : 0) | (moduleConfig.audio.i2s_din ? I2S_MODE_TX : 0)), + .sample_rate = 8000, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, + .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), + .intr_alloc_flags = 0, + .dma_buf_count = 8, + .dma_buf_len = adc_buffer_size, // 320 * 2 bytes + .use_apll = false, + .tx_desc_auto_clear = true, + .fixed_mclk = 0}; + res = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); + if (res != ESP_OK) { + LOG_ERROR("Failed to install I2S driver: %d", res); + } + + const i2s_pin_config_t pin_config = {.bck_io_num = moduleConfig.audio.i2s_sck, + .ws_io_num = moduleConfig.audio.i2s_ws, + .data_out_num = moduleConfig.audio.i2s_din ? moduleConfig.audio.i2s_din : I2S_PIN_NO_CHANGE, + .data_in_num = moduleConfig.audio.i2s_sd ? moduleConfig.audio.i2s_sd : I2S_PIN_NO_CHANGE}; + res = i2s_set_pin(I2S_PORT, &pin_config); + if (res != ESP_OK) { + LOG_ERROR("Failed to set I2S pin config: %d", res); + } + + res = i2s_start(I2S_PORT); + if (res != ESP_OK) { + LOG_ERROR("Failed to start I2S: %d", res); + } + + radio_state = RadioState::rx; + + // Configure PTT input + LOG_INFO("Init PTT on Pin %u", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); + pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT); + + firstTime = false; } else { - return disable(); - } -} + UIFrameEvent e; + // Check if PTT is pressed. TODO hook that into Onebutton/Interrupt drive. + if (digitalRead(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN) == HIGH) { + if (radio_state == RadioState::rx) { + LOG_INFO("PTT pressed, switching to TX"); + radio_state = RadioState::tx; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->notifyObservers(&e); + } + } else { + if (radio_state == RadioState::tx) { + LOG_INFO("PTT released, switching to RX"); + if (tx_encode_frame_index > sizeof(tx_header)) { + // Send the incomplete frame + LOG_INFO("Send %d codec2 bytes (incomplete)", tx_encode_frame_index); + sendPayload(); + } + tx_encode_frame_index = sizeof(tx_header); + radio_state = RadioState::rx; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->notifyObservers(&e); + } + } + if (radio_state == RadioState::tx) { + // Get I2S data from the microphone and place in data buffer + size_t bytesIn = 0; + res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn, + pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. -meshtastic_MeshPacket *AudioModule::allocReply() -{ - auto reply = allocDataPacket(); - return reply; -} - -bool AudioModule::shouldDraw() -{ - if (!moduleConfig.audio.codec2_enabled) { - return false; - } - return (radio_state == RadioState::tx); -} - -void AudioModule::sendPayload(NodeNum dest, bool wantReplies) -{ - meshtastic_MeshPacket *p = allocReply(); - p->to = dest; - p->decoded.want_response = wantReplies; - - p->want_ack = false; // Audio is shoot&forget. No need to wait for ACKs. - p->priority = meshtastic_MeshPacket_Priority_MAX; // Audio is important, because realtime - - p->decoded.payload.size = tx_encode_frame_index; - memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); - - service->sendToMesh(p); -} - -ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) -{ - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - auto &p = mp.decoded; - if (!isFromUs(&mp)) { - memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); - radio_state = RadioState::rx; - rx_encode_frame_index = p.payload.size; + if (res == ESP_OK) { + adc_buffer_index += bytesIn; + if (adc_buffer_index == adc_buffer_size) { + adc_buffer_index = 0; + memcpy((void *)speech, (void *)adc_buffer, 2 * adc_buffer_size); // Notify run_codec2 task that the buffer is ready. + radio_state = RadioState::tx; BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); if (xHigherPriorityTaskWoken == pdTRUE) - YIELD_FROM_ISR(xHigherPriorityTaskWoken); + YIELD_FROM_ISR(xHigherPriorityTaskWoken); + } } + } } + return 100; + } else { + return disable(); + } +} - return ProcessMessage::CONTINUE; +meshtastic_MeshPacket *AudioModule::allocReply() { + auto reply = allocDataPacket(); + return reply; +} + +bool AudioModule::shouldDraw() { + if (!moduleConfig.audio.codec2_enabled) { + return false; + } + return (radio_state == RadioState::tx); +} + +void AudioModule::sendPayload(NodeNum dest, bool wantReplies) { + meshtastic_MeshPacket *p = allocReply(); + p->to = dest; + p->decoded.want_response = wantReplies; + + p->want_ack = false; // Audio is shoot&forget. No need to wait for ACKs. + p->priority = meshtastic_MeshPacket_Priority_MAX; // Audio is important, because realtime + + p->decoded.payload.size = tx_encode_frame_index; + memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); + + service->sendToMesh(p); +} + +ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + auto &p = mp.decoded; + if (!isFromUs(&mp)) { + memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); + radio_state = RadioState::rx; + rx_encode_frame_index = p.payload.size; + // Notify run_codec2 task that the buffer is ready. + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + YIELD_FROM_ISR(xHigherPriorityTaskWoken); + } + } + + return ProcessMessage::CONTINUE; } #endif \ No newline at end of file diff --git a/src/modules/esp32/AudioModule.h b/src/modules/esp32/AudioModule.h index b6762706a..fb134bcaa 100644 --- a/src/modules/esp32/AudioModule.h +++ b/src/modules/esp32/AudioModule.h @@ -18,8 +18,8 @@ enum RadioState { standby, rx, tx }; const char c2_magic[3] = {0xc0, 0xde, 0xc2}; // Magic number for codec2 header struct c2_header { - char magic[3]; - char mode; + char magic[3]; + char mode; }; #define ADC_BUFFER_SIZE_MAX 320 @@ -30,56 +30,55 @@ struct c2_header { #define AUDIO_MODULE_RX_BUFFER 128 #define AUDIO_MODULE_MODE meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700 -class AudioModule : public SinglePortModule, public Observable, private concurrency::OSThread -{ - public: - unsigned char rx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - unsigned char tx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - c2_header tx_header = {}; - int16_t speech[ADC_BUFFER_SIZE_MAX] = {}; - int16_t output_buffer[ADC_BUFFER_SIZE_MAX] = {}; - uint16_t adc_buffer[ADC_BUFFER_SIZE_MAX] = {}; - int adc_buffer_size = 0; - uint16_t adc_buffer_index = 0; - int tx_encode_frame_index = sizeof(c2_header); // leave room for header - int rx_encode_frame_index = 0; - int encode_codec_size = 0; - int encode_frame_size = 0; - volatile RadioState radio_state = RadioState::rx; +class AudioModule : public SinglePortModule, public Observable, private concurrency::OSThread { +public: + unsigned char rx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + unsigned char tx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + c2_header tx_header = {}; + int16_t speech[ADC_BUFFER_SIZE_MAX] = {}; + int16_t output_buffer[ADC_BUFFER_SIZE_MAX] = {}; + uint16_t adc_buffer[ADC_BUFFER_SIZE_MAX] = {}; + int adc_buffer_size = 0; + uint16_t adc_buffer_index = 0; + int tx_encode_frame_index = sizeof(c2_header); // leave room for header + int rx_encode_frame_index = 0; + int encode_codec_size = 0; + int encode_frame_size = 0; + volatile RadioState radio_state = RadioState::rx; - struct CODEC2 *codec2 = NULL; - // int16_t sample; + struct CODEC2 *codec2 = NULL; + // int16_t sample; - AudioModule(); + AudioModule(); - bool shouldDraw(); + bool shouldDraw(); - /** - * Send our payload into the mesh - */ - void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - protected: - int encode_frame_num = 0; - bool firstTime = true; +protected: + int encode_frame_num = 0; + bool firstTime = true; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - virtual meshtastic_MeshPacket *allocReply() override; + virtual meshtastic_MeshPacket *allocReply() override; - virtual bool wantUIFrame() override { return this->shouldDraw(); } - virtual Observable *getUIFrameObservable() override { return this; } + virtual bool wantUIFrame() override { return this->shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - /** Called to handle a particular incoming message - * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered - * for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + /** Called to handle a particular incoming message + * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be + * considered for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; extern AudioModule *audioModule; diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp index 9c25177bc..9c91cd2a5 100644 --- a/src/modules/esp32/PaxcounterModule.cpp +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -15,20 +15,16 @@ PaxcounterModule *paxcounterModule; * We only clear our sent flag here, since this function is called from another thread, so we * cannot send to the mesh directly. */ -void PaxcounterModule::handlePaxCounterReportRequest() -{ - // The libpax library already updated our data structure, just before invoking this callback. - LOG_INFO("PaxcounterModule: libpax reported new data: wifi=%d; ble=%d; uptime=%lu", - paxcounterModule->count_from_libpax.wifi_count, paxcounterModule->count_from_libpax.ble_count, millis() / 1000); - paxcounterModule->reportedDataSent = false; - paxcounterModule->setIntervalFromNow(0); +void PaxcounterModule::handlePaxCounterReportRequest() { + // The libpax library already updated our data structure, just before invoking this callback. + LOG_INFO("PaxcounterModule: libpax reported new data: wifi=%d; ble=%d; uptime=%lu", paxcounterModule->count_from_libpax.wifi_count, + paxcounterModule->count_from_libpax.ble_count, millis() / 1000); + paxcounterModule->reportedDataSent = false; + paxcounterModule->setIntervalFromNow(0); } PaxcounterModule::PaxcounterModule() - : concurrency::OSThread("Paxcounter"), - ProtobufModule("paxcounter", meshtastic_PortNum_PAXCOUNTER_APP, &meshtastic_Paxcount_msg) -{ -} + : concurrency::OSThread("Paxcounter"), ProtobufModule("paxcounter", meshtastic_PortNum_PAXCOUNTER_APP, &meshtastic_Paxcount_msg) {} /** * Send the Pax information to the mesh if we got new data from libpax. @@ -37,79 +33,72 @@ PaxcounterModule::PaxcounterModule() * @param dest - destination node (usually NODENUM_BROADCAST) * @return false if sending is unnecessary, true if information was sent */ -bool PaxcounterModule::sendInfo(NodeNum dest) -{ - if (paxcounterModule->reportedDataSent) - return false; +bool PaxcounterModule::sendInfo(NodeNum dest) { + if (paxcounterModule->reportedDataSent) + return false; - LOG_INFO("PaxcounterModule: send pax info wifi=%d; ble=%d; uptime=%lu", count_from_libpax.wifi_count, - count_from_libpax.ble_count, millis() / 1000); + LOG_INFO("PaxcounterModule: send pax info wifi=%d; ble=%d; uptime=%lu", count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); - meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; - pl.wifi = count_from_libpax.wifi_count; - pl.ble = count_from_libpax.ble_count; - pl.uptime = millis() / 1000; + meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; + pl.wifi = count_from_libpax.wifi_count; + pl.ble = count_from_libpax.ble_count; + pl.uptime = millis() / 1000; - meshtastic_MeshPacket *p = allocDataProtobuf(pl); - p->to = dest; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + meshtastic_MeshPacket *p = allocDataProtobuf(pl); + p->to = dest; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - service->sendToMesh(p, RX_SRC_LOCAL, true); + service->sendToMesh(p, RX_SRC_LOCAL, true); - paxcounterModule->reportedDataSent = true; + paxcounterModule->reportedDataSent = true; - return true; + return true; } -bool PaxcounterModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) -{ - return false; // Let others look at this message also if they want. We don't do anything with received packets. +bool PaxcounterModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) { + return false; // Let others look at this message also if they want. We don't do anything with received packets. } -meshtastic_MeshPacket *PaxcounterModule::allocReply() -{ - meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; - pl.wifi = count_from_libpax.wifi_count; - pl.ble = count_from_libpax.ble_count; - pl.uptime = millis() / 1000; - return allocDataProtobuf(pl); +meshtastic_MeshPacket *PaxcounterModule::allocReply() { + meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; + pl.wifi = count_from_libpax.wifi_count; + pl.ble = count_from_libpax.ble_count; + pl.uptime = millis() / 1000; + return allocDataProtobuf(pl); } -int32_t PaxcounterModule::runOnce() -{ - if (isActive()) { - if (firstTime) { - firstTime = false; - LOG_DEBUG("Paxcounter starting up with interval of %d seconds", - Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, - default_telemetry_broadcast_interval_secs)); - struct libpax_config_t configuration; - libpax_default_config(&configuration); +int32_t PaxcounterModule::runOnce() { + if (isActive()) { + if (firstTime) { + firstTime = false; + LOG_DEBUG("Paxcounter starting up with interval of %d seconds", + Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs)); + struct libpax_config_t configuration; + libpax_default_config(&configuration); - configuration.blecounter = 1; - configuration.blescantime = 0; // infinite - configuration.wificounter = 1; - configuration.wifi_channel_map = WIFI_CHANNEL_ALL; - configuration.wifi_channel_switch_interval = 50; - configuration.wifi_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.wifi_threshold, -80); - configuration.ble_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.ble_threshold, -80); - libpax_update_config(&configuration); + configuration.blecounter = 1; + configuration.blescantime = 0; // infinite + configuration.wificounter = 1; + configuration.wifi_channel_map = WIFI_CHANNEL_ALL; + configuration.wifi_channel_switch_interval = 50; + configuration.wifi_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.wifi_threshold, -80); + configuration.ble_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.ble_threshold, -80); + libpax_update_config(&configuration); - // internal processing initialization - libpax_counter_init(handlePaxCounterReportRequest, &count_from_libpax, - Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, - default_telemetry_broadcast_interval_secs), - 0); - libpax_counter_start(); - } else { - sendInfo(NODENUM_BROADCAST); - } - return Default::getConfiguredOrDefaultMsScaled(moduleConfig.paxcounter.paxcounter_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes); + // internal processing initialization + libpax_counter_init( + handlePaxCounterReportRequest, &count_from_libpax, + Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs), 0); + libpax_counter_start(); } else { - return disable(); + sendInfo(NODENUM_BROADCAST); } + return Default::getConfiguredOrDefaultMsScaled(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs, + numOnlineNodes); + } else { + return disable(); + } } #if HAS_SCREEN @@ -117,31 +106,29 @@ int32_t PaxcounterModule::runOnce() #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" -void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "Pax"; + // === Set Title + const char *titleStr = "Pax"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - char buffer[50]; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - libpax_counter_count(&count_from_libpax); + libpax_counter_count(&count_from_libpax); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, - "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, - millis() / 1000); - graphics::drawCommonFooter(display, x, y); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, "WiFi: %d\nBLE: %d\nUptime: %ds", + count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); + graphics::drawCommonFooter(display, x, y); } #endif // HAS_SCREEN diff --git a/src/modules/esp32/PaxcounterModule.h b/src/modules/esp32/PaxcounterModule.h index ebd6e7191..92097de1f 100644 --- a/src/modules/esp32/PaxcounterModule.h +++ b/src/modules/esp32/PaxcounterModule.h @@ -11,26 +11,25 @@ * Wrapper module for the estimate passenger (PAX) count library (https://github.com/dbinfrago/libpax) which * implements the core functionality of the ESP32 Paxcounter project (https://github.com/cyberman54/ESP32-Paxcounter) */ -class PaxcounterModule : private concurrency::OSThread, public ProtobufModule -{ - bool firstTime = true; - bool reportedDataSent = true; +class PaxcounterModule : private concurrency::OSThread, public ProtobufModule { + bool firstTime = true; + bool reportedDataSent = true; - static void handlePaxCounterReportRequest(); + static void handlePaxCounterReportRequest(); - public: - PaxcounterModule(); +public: + PaxcounterModule(); - protected: - struct count_payload_t count_from_libpax = {0, 0, 0}; - virtual int32_t runOnce() override; - bool sendInfo(NodeNum dest = NODENUM_BROADCAST); - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) override; - virtual meshtastic_MeshPacket *allocReply() override; - bool isActive() { return moduleConfig.paxcounter.enabled && !config.bluetooth.enabled && !config.network.wifi_enabled; } +protected: + struct count_payload_t count_from_libpax = {0, 0, 0}; + virtual int32_t runOnce() override; + bool sendInfo(NodeNum dest = NODENUM_BROADCAST); + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) override; + virtual meshtastic_MeshPacket *allocReply() override; + bool isActive() { return moduleConfig.paxcounter.enabled && !config.bluetooth.enabled && !config.network.wifi_enabled; } #if HAS_SCREEN - virtual bool wantUIFrame() override { return isActive(); } - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual bool wantUIFrame() override { return isActive(); } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif }; diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index f08ee00f9..6644e0624 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -26,139 +26,127 @@ extern ScanI2C::DeviceAddress accelerometer_found; -class AccelerometerThread : public concurrency::OSThread -{ - private: - MotionSensor *sensor = nullptr; - bool isInitialised = false; +class AccelerometerThread : public concurrency::OSThread { +private: + MotionSensor *sensor = nullptr; + bool isInitialised = false; - public: - explicit AccelerometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Accelerometer") - { - device = foundDevice; - init(); +public: + explicit AccelerometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Accelerometer") { + device = foundDevice; + init(); + } + + explicit AccelerometerThread(ScanI2C::DeviceType type) : AccelerometerThread(ScanI2C::FoundDevice{type, accelerometer_found}) {} + + void start() { + init(); + setIntervalFromNow(0); + }; + + void calibrate(uint16_t forSeconds) { + if (sensor) { + sensor->calibrate(forSeconds); + } + } + +protected: + int32_t runOnce() override { + // Assume we should not keep the board awake + canSleep = true; + + if (isInitialised) + return sensor->runOnce(); + + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + +private: + ScanI2C::FoundDevice device; + + void init() { + if (isInitialised) + return; + + if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) { + LOG_DEBUG("AccelerometerThread Disable due to no sensors found"); + disable(); + return; } - explicit AccelerometerThread(ScanI2C::DeviceType type) : AccelerometerThread(ScanI2C::FoundDevice{type, accelerometer_found}) - { - } - - void start() - { - init(); - setIntervalFromNow(0); - }; - - void calibrate(uint16_t forSeconds) - { - if (sensor) { - sensor->calibrate(forSeconds); - } - } - - protected: - int32_t runOnce() override - { - // Assume we should not keep the board awake - canSleep = true; - - if (isInitialised) - return sensor->runOnce(); - - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - - private: - ScanI2C::FoundDevice device; - - void init() - { - if (isInitialised) - return; - - if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) { - LOG_DEBUG("AccelerometerThread Disable due to no sensors found"); - disable(); - return; - } - - switch (device.type) { + switch (device.type) { #ifdef HAS_BMA423 - case ScanI2C::DeviceType::BMA423: - sensor = new BMA423Sensor(device); - break; + case ScanI2C::DeviceType::BMA423: + sensor = new BMA423Sensor(device); + break; #endif - case ScanI2C::DeviceType::MPU6050: - sensor = new MPU6050Sensor(device); - break; - case ScanI2C::DeviceType::BMX160: - sensor = new BMX160Sensor(device); - break; - case ScanI2C::DeviceType::LIS3DH: - sensor = new LIS3DHSensor(device); - break; - case ScanI2C::DeviceType::LSM6DS3: - sensor = new LSM6DS3Sensor(device); - break; + case ScanI2C::DeviceType::MPU6050: + sensor = new MPU6050Sensor(device); + break; + case ScanI2C::DeviceType::BMX160: + sensor = new BMX160Sensor(device); + break; + case ScanI2C::DeviceType::LIS3DH: + sensor = new LIS3DHSensor(device); + break; + case ScanI2C::DeviceType::LSM6DS3: + sensor = new LSM6DS3Sensor(device); + break; #ifdef HAS_STK8XXX - case ScanI2C::DeviceType::STK8BAXX: - sensor = new STK8XXXSensor(device); - break; + case ScanI2C::DeviceType::STK8BAXX: + sensor = new STK8XXXSensor(device); + break; #endif - case ScanI2C::DeviceType::ICM20948: - sensor = new ICM20948Sensor(device); - break; - case ScanI2C::DeviceType::BMM150: - sensor = new BMM150Sensor(device); - break; + case ScanI2C::DeviceType::ICM20948: + sensor = new ICM20948Sensor(device); + break; + case ScanI2C::DeviceType::BMM150: + sensor = new BMM150Sensor(device); + break; #ifdef HAS_QMA6100P - case ScanI2C::DeviceType::QMA6100P: - sensor = new QMA6100PSensor(device); - break; + case ScanI2C::DeviceType::QMA6100P: + sensor = new QMA6100PSensor(device); + break; #endif - default: - disable(); - return; - } - - isInitialised = sensor->init(); - if (!isInitialised) { - clean(); - } - LOG_DEBUG("AccelerometerThread::init %s", isInitialised ? "ok" : "failed"); + default: + disable(); + return; } - // Copy constructor (not implemented / included to avoid cppcheck warnings) - AccelerometerThread(const AccelerometerThread &other) : OSThread::OSThread("Accelerometer") { this->copy(other); } - - // Destructor (included to avoid cppcheck warnings) - virtual ~AccelerometerThread() { clean(); } - - // Copy assignment (not implemented / included to avoid cppcheck warnings) - AccelerometerThread &operator=(const AccelerometerThread &other) - { - this->copy(other); - return *this; + isInitialised = sensor->init(); + if (!isInitialised) { + clean(); } + LOG_DEBUG("AccelerometerThread::init %s", isInitialised ? "ok" : "failed"); + } - // Take a very shallow copy (does not copy OSThread state nor the sensor object) - // If for some reason this is ever used, make sure to call init() after any copy - void copy(const AccelerometerThread &other) - { - if (this != &other) { - clean(); - this->device = ScanI2C::FoundDevice(other.device.type, - ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address)); - } - } + // Copy constructor (not implemented / included to avoid cppcheck warnings) + AccelerometerThread(const AccelerometerThread &other) : OSThread::OSThread("Accelerometer") { this->copy(other); } - // Cleanup resources - void clean() - { - isInitialised = false; - delete sensor; - sensor = nullptr; + // Destructor (included to avoid cppcheck warnings) + virtual ~AccelerometerThread() { clean(); } + + // Copy assignment (not implemented / included to avoid cppcheck warnings) + AccelerometerThread &operator=(const AccelerometerThread &other) { + this->copy(other); + return *this; + } + + // Take a very shallow copy (does not copy OSThread state nor the sensor object) + // If for some reason this is ever used, make sure to call init() after any copy + void copy(const AccelerometerThread &other) { + if (this != &other) { + clean(); + this->device = ScanI2C::FoundDevice(other.device.type, ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address)); } + } + + // Cleanup resources + void clean() { + isInitialised = false; + delete sensor; + sensor = nullptr; + } }; #endif diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp index 5111dae32..ed61553bc 100755 --- a/src/motion/BMA423Sensor.cpp +++ b/src/motion/BMA423Sensor.cpp @@ -4,57 +4,55 @@ BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool BMA423Sensor::init() -{ - if (sensor.begin(Wire, deviceAddress())) { - sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); - sensor.enableAccelerometer(); - sensor.configInterrupt(); +bool BMA423Sensor::init() { + if (sensor.begin(Wire, deviceAddress())) { + sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); + sensor.enableAccelerometer(); + sensor.configInterrupt(); #ifdef BMA423_INT - pinMode(BMA4XX_INT, INPUT); - attachInterrupt( - BMA4XX_INT, - [] { - // Set interrupt to set irq value to true - BMA_IRQ = true; - }, - RISING); // Select the interrupt mode according to the actual circuit + pinMode(BMA4XX_INT, INPUT); + attachInterrupt( + BMA4XX_INT, + [] { + // Set interrupt to set irq value to true + BMA_IRQ = true; + }, + RISING); // Select the interrupt mode according to the actual circuit #endif #ifdef T_WATCH_S3 - // Need to raise the wrist function, need to set the correct axis - sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); + // Need to raise the wrist function, need to set the correct axis + sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); #else - sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); + sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); #endif - // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); - sensor.enableFeature(sensor.FEATURE_TILT, true); - sensor.enableFeature(sensor.FEATURE_WAKEUP, true); - // sensor.resetPedometer(); + // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); + sensor.enableFeature(sensor.FEATURE_TILT, true); + sensor.enableFeature(sensor.FEATURE_WAKEUP, true); + // sensor.resetPedometer(); - // Turn on feature interrupt - sensor.enablePedometerIRQ(); - sensor.enableTiltIRQ(); + // Turn on feature interrupt + sensor.enablePedometerIRQ(); + sensor.enableTiltIRQ(); - // It corresponds to isDoubleClick interrupt - sensor.enableWakeupIRQ(); - LOG_DEBUG("BMA423 init ok"); - return true; - } - LOG_DEBUG("BMA423 init failed"); - return false; + // It corresponds to isDoubleClick interrupt + sensor.enableWakeupIRQ(); + LOG_DEBUG("BMA423 init ok"); + return true; + } + LOG_DEBUG("BMA423 init failed"); + return false; } -int32_t BMA423Sensor::runOnce() -{ - if (sensor.readIrqStatus()) { - if (sensor.isTilt() || sensor.isDoubleTap()) { - wakeScreen(); - return 500; - } +int32_t BMA423Sensor::runOnce() { + if (sensor.readIrqStatus()) { + if (sensor.isTilt() || sensor.isDoubleTap()) { + wakeScreen(); + return 500; } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif \ No newline at end of file diff --git a/src/motion/BMA423Sensor.h b/src/motion/BMA423Sensor.h index b9d7b4aa0..28bfa3fd5 100755 --- a/src/motion/BMA423Sensor.h +++ b/src/motion/BMA423Sensor.h @@ -9,16 +9,15 @@ #include #include -class BMA423Sensor : public MotionSensor -{ - private: - SensorBMA423 sensor; - volatile bool BMA_IRQ = false; +class BMA423Sensor : public MotionSensor { +private: + SensorBMA423 sensor; + volatile bool BMA_IRQ = false; - public: - explicit BMA423Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; +public: + explicit BMA423Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp index 4b3a1215c..080918114 100644 --- a/src/motion/BMM150Sensor.cpp +++ b/src/motion/BMM150Sensor.cpp @@ -12,39 +12,37 @@ volatile static bool BMM150_IRQ = false; BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool BMM150Sensor::init() -{ - // Initialise the sensor - sensor = BMM150Singleton::GetInstance(device); - return sensor->init(device); +bool BMM150Sensor::init() { + // Initialise the sensor + sensor = BMM150Singleton::GetInstance(device); + return sensor->init(device); } -int32_t BMM150Sensor::runOnce() -{ +int32_t BMM150Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - float heading = sensor->getCompassDegree(); + float heading = sensor->getCompassDegree(); - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } - if (screen) - screen->setHeading(heading); + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); #endif - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } // ---------------------------------------------------------------------- @@ -52,17 +50,16 @@ int32_t BMM150Sensor::runOnce() // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun BMM_150_I2C -BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) -{ +BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) { #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) - TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - TwoWire &bus = Wire; // fallback if only one I2C interface + TwoWire &bus = Wire; // fallback if only one I2C interface #endif - if (pinstance == nullptr) { - pinstance = new BMM150Singleton(&bus, device.address.address); - } - return pinstance; + if (pinstance == nullptr) { + pinstance = new BMM150Singleton(&bus, device.address.address); + } + return pinstance; } BMM150Singleton::~BMM150Singleton() {} @@ -71,23 +68,22 @@ BMM150Singleton *BMM150Singleton::pinstance{nullptr}; // Initialise the BMM150 Sensor // https://github.com/DFRobot/DFRobot_BMM150/blob/master/examples/getGeomagneticData/getGeomagneticData.ino -bool BMM150Singleton::init(ScanI2C::FoundDevice device) -{ +bool BMM150Singleton::init(ScanI2C::FoundDevice device) { - // startup - LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); - uint8_t status = begin(); - if (status != 0) { - LOG_DEBUG("BMM150 init error %u", status); - return false; - } + // startup + LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); + uint8_t status = begin(); + if (status != 0) { + LOG_DEBUG("BMM150 init error %u", status); + return false; + } - // SW reset to make sure the device starts in a known state - setOperationMode(BMM150_POWERMODE_NORMAL); - setPresetMode(BMM150_PRESETMODE_LOWPOWER); - setRate(BMM150_DATA_RATE_02HZ); - setMeasurementXYZ(); - return true; + // SW reset to make sure the device starts in a known state + setOperationMode(BMM150_POWERMODE_NORMAL); + setPresetMode(BMM150_PRESETMODE_LOWPOWER); + setRate(BMM150_DATA_RATE_02HZ); + setMeasurementXYZ(); + return true; } #endif \ No newline at end of file diff --git a/src/motion/BMM150Sensor.h b/src/motion/BMM150Sensor.h index 879045400..36d7a29f8 100644 --- a/src/motion/BMM150Sensor.h +++ b/src/motion/BMM150Sensor.h @@ -13,43 +13,41 @@ extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper -class BMM150Singleton : public DFRobot_BMM150_I2C -{ - private: - static BMM150Singleton *pinstance; +class BMM150Singleton : public DFRobot_BMM150_I2C { +private: + static BMM150Singleton *pinstance; - protected: - BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} - ~BMM150Singleton(); +protected: + BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} + ~BMM150Singleton(); - public: - // Create a singleton instance (not thread safe) - static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); +public: + // Create a singleton instance (not thread safe) + static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); - // Singletons should not be cloneable. - BMM150Singleton(BMM150Singleton &other) = delete; + // Singletons should not be cloneable. + BMM150Singleton(BMM150Singleton &other) = delete; - // Singletons should not be assignable. - void operator=(const BMM150Singleton &) = delete; + // Singletons should not be assignable. + void operator=(const BMM150Singleton &) = delete; - // Initialise the motion sensor singleton for normal operation - bool init(ScanI2C::FoundDevice device); + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); }; -class BMM150Sensor : public MotionSensor -{ - private: - BMM150Singleton *sensor = nullptr; - bool showingScreen = false; +class BMM150Sensor : public MotionSensor { +private: + BMM150Singleton *sensor = nullptr; + bool showingScreen = false; - public: - explicit BMM150Sensor(ScanI2C::FoundDevice foundDevice); +public: + explicit BMM150Sensor(ScanI2C::FoundDevice foundDevice); - // Initialise the motion sensor - virtual bool init() override; + // Initialise the motion sensor + virtual bool init() override; - // Called each time our sensor gets a chance to run - virtual int32_t runOnce() override; + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index 5888c20be..c576abf2d 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -11,123 +11,120 @@ BMX160Sensor::BMX160Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::Mot extern graphics::Screen *screen; #endif -bool BMX160Sensor::init() -{ - if (sensor.begin()) { - // set output data rate - sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ); - LOG_DEBUG("BMX160 init ok"); - return true; - } - LOG_DEBUG("BMX160 init failed"); - return false; +bool BMX160Sensor::init() { + if (sensor.begin()) { + // set output data rate + sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ); + LOG_DEBUG("BMX160 init ok"); + return true; + } + LOG_DEBUG("BMX160 init failed"); + return false; } -int32_t BMX160Sensor::runOnce() -{ +int32_t BMX160Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) - sBmx160SensorData_t magAccel; - sBmx160SensorData_t gAccel; + sBmx160SensorData_t magAccel; + sBmx160SensorData_t gAccel; - /* Get a new sensor event */ - sensor.getAllData(&magAccel, NULL, &gAccel); + /* Get a new sensor event */ + sensor.getAllData(&magAccel, NULL, &gAccel); - if (doCalibration) { + if (doCalibration) { - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); - } - - if (magAccel.x > highestX) - highestX = magAccel.x; - if (magAccel.x < lowestX) - lowestX = magAccel.x; - if (magAccel.y > highestY) - highestY = magAccel.y; - if (magAccel.y < lowestY) - lowestY = magAccel.y; - if (magAccel.z > highestZ) - highestZ = magAccel.z; - if (magAccel.z < lowestZ) - lowestZ = magAccel.z; - - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); - } - - // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, - // lowestY, highestY, lowestZ, highestZ); + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration + showingScreen = true; + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } - int highestRealX = highestX - (highestX + lowestX) / 2; + if (magAccel.x > highestX) + highestX = magAccel.x; + if (magAccel.x < lowestX) + lowestX = magAccel.x; + if (magAccel.y > highestY) + highestY = magAccel.y; + if (magAccel.y < lowestY) + lowestY = magAccel.y; + if (magAccel.z > highestZ) + highestZ = magAccel.z; + if (magAccel.z < lowestZ) + lowestZ = magAccel.z; - magAccel.x -= (highestX + lowestX) / 2; - magAccel.y -= (highestY + lowestY) / 2; - magAccel.z -= (highestZ + lowestZ) / 2; - FusionVector ga, ma; - ga.axis.x = -gAccel.x; // default location for the BMX160 is on the rear of the board - ga.axis.y = -gAccel.y; - ga.axis.z = gAccel.z; - ma.axis.x = -magAccel.x; - ma.axis.y = -magAccel.y; - ma.axis.z = magAccel.z * 3; - - // If we're set to one of the inverted positions - if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { - ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); - ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); + uint32_t now = millis(); + if (now > endCalibrationAt) { + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + if (screen) + screen->endAlert(); } - float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); + // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, + // highestX, lowestY, highestY, lowestZ, highestZ); + } - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } - if (screen) - screen->setHeading(heading); + int highestRealX = highestX - (highestX + lowestX) / 2; + + magAccel.x -= (highestX + lowestX) / 2; + magAccel.y -= (highestY + lowestY) / 2; + magAccel.z -= (highestZ + lowestZ) / 2; + FusionVector ga, ma; + ga.axis.x = -gAccel.x; // default location for the BMX160 is on the rear of the board + ga.axis.y = -gAccel.y; + ga.axis.z = gAccel.z; + ma.axis.x = -magAccel.x; + ma.axis.y = -magAccel.y; + ma.axis.z = magAccel.z * 3; + + // If we're set to one of the inverted positions + if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { + ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); + ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); + } + + float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); + + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); #endif - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } -void BMX160Sensor::calibrate(uint16_t forSeconds) -{ +void BMX160Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) - sBmx160SensorData_t magAccel; - sBmx160SensorData_t gAccel; - LOG_DEBUG("BMX160 calibration started for %is", forSeconds); - sensor.getAllData(&magAccel, NULL, &gAccel); - highestX = magAccel.x, lowestX = magAccel.x; - highestY = magAccel.y, lowestY = magAccel.y; - highestZ = magAccel.z, lowestZ = magAccel.z; + sBmx160SensorData_t magAccel; + sBmx160SensorData_t gAccel; + LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + sensor.getAllData(&magAccel, NULL, &gAccel); + highestX = magAccel.x, lowestX = magAccel.x; + highestY = magAccel.y, lowestY = magAccel.y; + highestZ = magAccel.z, lowestZ = magAccel.z; - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + doCalibration = true; + uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided + endCalibrationAt = millis() + calibrateFor; + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index ddca5767c..3a3886756 100755 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -12,27 +12,25 @@ #include "Fusion/Fusion.h" #include -class BMX160Sensor : public MotionSensor -{ - private: - RAK_BMX160 sensor; - bool showingScreen = false; - float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; +class BMX160Sensor : public MotionSensor { +private: + RAK_BMX160 sensor; + bool showingScreen = false; + float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; - public: - explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; - virtual void calibrate(uint16_t forSeconds) override; +public: + explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; }; #else // Stub -class BMX160Sensor : public MotionSensor -{ - public: - explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); +class BMX160Sensor : public MotionSensor { +public: + explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); }; #endif diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 9455eafe0..208edeac6 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -11,186 +11,180 @@ extern graphics::Screen *screen; volatile static bool ICM20948_IRQ = false; // Interrupt service routine -void ICM20948SetInterrupt() -{ - ICM20948_IRQ = true; -} +void ICM20948SetInterrupt() { ICM20948_IRQ = true; } ICM20948Sensor::ICM20948Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool ICM20948Sensor::init() -{ - // Initialise the sensor - sensor = ICM20948Singleton::GetInstance(); - if (!sensor->init(device)) - return false; +bool ICM20948Sensor::init() { + // Initialise the sensor + sensor = ICM20948Singleton::GetInstance(); + if (!sensor->init(device)) + return false; - // Enable simple Wake on Motion - return sensor->setWakeOnMotion(); + // Enable simple Wake on Motion + return sensor->setWakeOnMotion(); } #ifdef ICM_20948_INT_PIN -int32_t ICM20948Sensor::runOnce() -{ - // Wake on motion using hardware interrupts - this is the most efficient way to check for motion - if (ICM20948_IRQ) { - ICM20948_IRQ = false; - sensor->clearInterrupts(); - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t ICM20948Sensor::runOnce() { + // Wake on motion using hardware interrupts - this is the most efficient way to check for motion + if (ICM20948_IRQ) { + ICM20948_IRQ = false; + sensor->clearInterrupts(); + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #else -int32_t ICM20948Sensor::runOnce() -{ +int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN #if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze - if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { - if (!isAsleep) { - LOG_DEBUG("sleeping IMU"); - sensor->sleep(true); - isAsleep = true; - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - if (isAsleep) { - sensor->sleep(false); - isAsleep = false; - } -#endif - - float magX = 0, magY = 0, magZ = 0; - if (sensor->dataReady()) { - sensor->getAGMT(); - magX = sensor->agmt.mag.axes.x; - magY = sensor->agmt.mag.axes.y; - magZ = sensor->agmt.mag.axes.z; - } - - if (doCalibration) { - - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); - } - - if (magX > highestX) - highestX = magX; - if (magX < lowestX) - lowestX = magX; - if (magY > highestY) - highestY = magY; - if (magY < lowestY) - lowestY = magY; - if (magZ > highestZ) - highestZ = magZ; - if (magZ < lowestZ) - lowestZ = magZ; - - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); - } - - // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, - // lowestY, highestY, lowestZ, highestZ); - } - - magX -= (highestX + lowestX) / 2; - magY -= (highestY + lowestY) / 2; - magZ -= (highestZ + lowestZ) / 2; - FusionVector ga, ma; - ga.axis.x = (sensor->agmt.acc.axes.x); - ga.axis.y = -(sensor->agmt.acc.axes.y); - ga.axis.z = -(sensor->agmt.acc.axes.z); - ma.axis.x = magX; - ma.axis.y = magY; - ma.axis.z = magZ; - - // If we're set to one of the inverted positions - if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { - ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); - ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); - } - - float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } - if (screen) - screen->setHeading(heading); -#endif - - // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) - auto status = sensor->setBank(0); - if (sensor->status != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 isWakeOnMotion failed to set bank - %s", sensor->statusString()); - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - - ICM_20948_INT_STATUS_t int_stat; - status = sensor->read(AGB0_REG_INT_STATUS, (uint8_t *)&int_stat, sizeof(ICM_20948_INT_STATUS_t)); - if (status != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 isWakeOnMotion failed to read interrupts - %s", sensor->statusString()); - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - - if (int_stat.WOM_INT != 0) { - // Wake up! - wakeScreen(); + if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { + if (!isAsleep) { + LOG_DEBUG("sleeping IMU"); + sensor->sleep(true); + isAsleep = true; } return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + if (isAsleep) { + sensor->sleep(false); + isAsleep = false; + } +#endif + + float magX = 0, magY = 0, magZ = 0; + if (sensor->dataReady()) { + sensor->getAGMT(); + magX = sensor->agmt.mag.axes.x; + magY = sensor->agmt.mag.axes.y; + magZ = sensor->agmt.mag.axes.z; + } + + if (doCalibration) { + + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration + showingScreen = true; + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); + } + + if (magX > highestX) + highestX = magX; + if (magX < lowestX) + lowestX = magX; + if (magY > highestY) + highestY = magY; + if (magY < lowestY) + lowestY = magY; + if (magZ > highestZ) + highestZ = magZ; + if (magZ < lowestZ) + lowestZ = magZ; + + uint32_t now = millis(); + if (now > endCalibrationAt) { + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + if (screen) + screen->endAlert(); + } + + // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, + // highestX, + // lowestY, highestY, lowestZ, highestZ); + } + + magX -= (highestX + lowestX) / 2; + magY -= (highestY + lowestY) / 2; + magZ -= (highestZ + lowestZ) / 2; + FusionVector ga, ma; + ga.axis.x = (sensor->agmt.acc.axes.x); + ga.axis.y = -(sensor->agmt.acc.axes.y); + ga.axis.z = -(sensor->agmt.acc.axes.z); + ma.axis.x = magX; + ma.axis.y = magY; + ma.axis.z = magZ; + + // If we're set to one of the inverted positions + if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { + ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); + ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); + } + + float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); + + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); +#endif + + // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) + auto status = sensor->setBank(0); + if (sensor->status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 isWakeOnMotion failed to set bank - %s", sensor->statusString()); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + ICM_20948_INT_STATUS_t int_stat; + status = sensor->read(AGB0_REG_INT_STATUS, (uint8_t *)&int_stat, sizeof(ICM_20948_INT_STATUS_t)); + if (status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 isWakeOnMotion failed to read interrupts - %s", sensor->statusString()); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + if (int_stat.WOM_INT != 0) { + // Wake up! + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif -void ICM20948Sensor::calibrate(uint16_t forSeconds) -{ +void ICM20948Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", - highestX, lowestX, highestY, lowestY, highestZ, lowestZ); - LOG_DEBUG("BMX160 calibration started for %is", forSeconds); - if (sensor->dataReady()) { - sensor->getAGMT(); - highestX = sensor->agmt.mag.axes.x; - lowestX = sensor->agmt.mag.axes.x; - highestY = sensor->agmt.mag.axes.y; - lowestY = sensor->agmt.mag.axes.y; - highestZ = sensor->agmt.mag.axes.z; - lowestZ = sensor->agmt.mag.axes.z; - } else { - highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; - } + LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", highestX, lowestX, + highestY, lowestY, highestZ, lowestZ); + LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + if (sensor->dataReady()) { + sensor->getAGMT(); + highestX = sensor->agmt.mag.axes.x; + lowestX = sensor->agmt.mag.axes.x; + highestY = sensor->agmt.mag.axes.y; + lowestY = sensor->agmt.mag.axes.y; + highestZ = sensor->agmt.mag.axes.z; + lowestZ = sensor->agmt.mag.axes.z; + } else { + highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + } - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + doCalibration = true; + uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided + endCalibrationAt = millis() + calibrateFor; + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } // ---------------------------------------------------------------------- @@ -198,12 +192,11 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds) // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun ICM_20948_I2C -ICM20948Singleton *ICM20948Singleton::GetInstance() -{ - if (pinstance == nullptr) { - pinstance = new ICM20948Singleton(); - } - return pinstance; +ICM20948Singleton *ICM20948Singleton::GetInstance() { + if (pinstance == nullptr) { + pinstance = new ICM20948Singleton(); + } + return pinstance; } ICM20948Singleton::ICM20948Singleton() {} @@ -213,114 +206,109 @@ ICM20948Singleton::~ICM20948Singleton() {} ICM20948Singleton *ICM20948Singleton::pinstance{nullptr}; // Initialise the ICM20948 Sensor -bool ICM20948Singleton::init(ScanI2C::FoundDevice device) -{ +bool ICM20948Singleton::init(ScanI2C::FoundDevice device) { #ifdef ICM_20948_DEBUG - // Set ICM_20948_DEBUG to enable helpful debug messages on Serial - enableDebugging(); + // Set ICM_20948_DEBUG to enable helpful debug messages on Serial + enableDebugging(); #endif - // startup + // startup #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) - TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - TwoWire &bus = Wire; // fallback if only one I2C interface + TwoWire &bus = Wire; // fallback if only one I2C interface #endif - bool bAddr = (device.address.address == 0x69); - delay(100); + bool bAddr = (device.address.address == 0x69); + delay(100); - LOG_DEBUG("ICM20948 begin on addr 0x%02X (port=%d, bAddr=%d)", device.address.address, device.address.port, bAddr); + LOG_DEBUG("ICM20948 begin on addr 0x%02X (port=%d, bAddr=%d)", device.address.address, device.address.port, bAddr); - ICM_20948_Status_e status = begin(bus, bAddr); - if (status != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init begin - %s", statusString()); - return false; - } + ICM_20948_Status_e status = begin(bus, bAddr); + if (status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init begin - %s", statusString()); + return false; + } - // SW reset to make sure the device starts in a known state - if (swReset() != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init reset - %s", statusString()); - return false; - } - delay(200); + // SW reset to make sure the device starts in a known state + if (swReset() != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init reset - %s", statusString()); + return false; + } + delay(200); - // Now wake the sensor up - if (sleep(false) != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init wake - %s", statusString()); - return false; - } + // Now wake the sensor up + if (sleep(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init wake - %s", statusString()); + return false; + } - if (lowPower(false) != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init high power - %s", statusString()); - return false; - } + if (lowPower(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init high power - %s", statusString()); + return false; + } - if (startupMagnetometer(false) != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init magnetometer - %s", statusString()); - return false; - } + if (startupMagnetometer(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init magnetometer - %s", statusString()); + return false; + } #ifdef ICM_20948_INT_PIN - // Active low - cfgIntActiveLow(true); - LOG_DEBUG("ICM20948 init set cfgIntActiveLow - %s", statusString()); + // Active low + cfgIntActiveLow(true); + LOG_DEBUG("ICM20948 init set cfgIntActiveLow - %s", statusString()); - // Push-pull - cfgIntOpenDrain(false); - LOG_DEBUG("ICM20948 init set cfgIntOpenDrain - %s", statusString()); + // Push-pull + cfgIntOpenDrain(false); + LOG_DEBUG("ICM20948 init set cfgIntOpenDrain - %s", statusString()); - // If enabled, *ANY* read will clear the INT_STATUS register. - cfgIntAnyReadToClear(true); - LOG_DEBUG("ICM20948 init set cfgIntAnyReadToClear - %s", statusString()); + // If enabled, *ANY* read will clear the INT_STATUS register. + cfgIntAnyReadToClear(true); + LOG_DEBUG("ICM20948 init set cfgIntAnyReadToClear - %s", statusString()); - // Latch the interrupt until cleared - cfgIntLatch(true); - LOG_DEBUG("ICM20948 init set cfgIntLatch - %s", statusString()); + // Latch the interrupt until cleared + cfgIntLatch(true); + LOG_DEBUG("ICM20948 init set cfgIntLatch - %s", statusString()); - // Set up an interrupt pin with an internal pullup for active low - pinMode(ICM_20948_INT_PIN, INPUT_PULLUP); + // Set up an interrupt pin with an internal pullup for active low + pinMode(ICM_20948_INT_PIN, INPUT_PULLUP); - // Set up an interrupt service routine - attachInterrupt(ICM_20948_INT_PIN, ICM20948SetInterrupt, FALLING); + // Set up an interrupt service routine + attachInterrupt(ICM_20948_INT_PIN, ICM20948SetInterrupt, FALLING); #endif - return true; + return true; } #ifdef ICM_20948_DMP_IS_ENABLED // Stub -bool ICM20948Sensor::initDMP() -{ +bool ICM20948Sensor::initDMP() { return false; } + +#endif + +bool ICM20948Singleton::setWakeOnMotion() { + // Set WoM threshold in milli G's + auto status = WOMThreshold(ICM_20948_WOM_THRESHOLD); + if (status != ICM_20948_Stat_Ok) return false; -} - -#endif - -bool ICM20948Singleton::setWakeOnMotion() -{ - // Set WoM threshold in milli G's - auto status = WOMThreshold(ICM_20948_WOM_THRESHOLD); - if (status != ICM_20948_Stat_Ok) - return false; - - // Enable WoM Logic mode 1 = Compare the current sample with the previous sample - status = WOMLogic(true, 1); - LOG_DEBUG("ICM20948 init set WOMLogic - %s", statusString()); - if (status != ICM_20948_Stat_Ok) - return false; - - // Enable interrupts on WakeOnMotion - status = intEnableWOM(true); - LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); - return status == ICM_20948_Stat_Ok; - - // Clear any current interrupts - ICM20948_IRQ = false; - clearInterrupts(); - return true; + + // Enable WoM Logic mode 1 = Compare the current sample with the previous sample + status = WOMLogic(true, 1); + LOG_DEBUG("ICM20948 init set WOMLogic - %s", statusString()); + if (status != ICM_20948_Stat_Ok) + return false; + + // Enable interrupts on WakeOnMotion + status = intEnableWOM(true); + LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); + return status == ICM_20948_Stat_Ok; + + // Clear any current interrupts + ICM20948_IRQ = false; + clearInterrupts(); + return true; } #endif diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index a9b7b69d0..dc6906adb 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -46,59 +46,56 @@ extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper for the Sparkfun ICM_20948_I2C class -class ICM20948Singleton : public ICM_20948_I2C -{ - private: - static ICM20948Singleton *pinstance; +class ICM20948Singleton : public ICM_20948_I2C { +private: + static ICM20948Singleton *pinstance; - protected: - ICM20948Singleton(); - ~ICM20948Singleton(); +protected: + ICM20948Singleton(); + ~ICM20948Singleton(); - public: - // Create a singleton instance (not thread safe) - static ICM20948Singleton *GetInstance(); +public: + // Create a singleton instance (not thread safe) + static ICM20948Singleton *GetInstance(); - // Singletons should not be cloneable. - ICM20948Singleton(ICM20948Singleton &other) = delete; + // Singletons should not be cloneable. + ICM20948Singleton(ICM20948Singleton &other) = delete; - // Singletons should not be assignable. - void operator=(const ICM20948Singleton &) = delete; + // Singletons should not be assignable. + void operator=(const ICM20948Singleton &) = delete; - // Initialise the motion sensor singleton for normal operation - bool init(ScanI2C::FoundDevice device); + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); - // Enable Wake on Motion interrupts (sensor must be initialised first) - bool setWakeOnMotion(); + // Enable Wake on Motion interrupts (sensor must be initialised first) + bool setWakeOnMotion(); #ifdef ICM_20948_DMP_IS_ENABLED - // Initialise the motion sensor singleton for digital motion processing - bool initDMP(); + // Initialise the motion sensor singleton for digital motion processing + bool initDMP(); #endif }; -class ICM20948Sensor : public MotionSensor -{ - private: - ICM20948Singleton *sensor = nullptr; - bool showingScreen = false; +class ICM20948Sensor : public MotionSensor { +private: + ICM20948Singleton *sensor = nullptr; + bool showingScreen = false; #ifdef MUZI_BASE - bool isAsleep = false; - float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, - lowestZ = 98.000000; + bool isAsleep = false; + float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else - float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; #endif - public: - explicit ICM20948Sensor(ScanI2C::FoundDevice foundDevice); +public: + explicit ICM20948Sensor(ScanI2C::FoundDevice foundDevice); - // Initialise the motion sensor - virtual bool init() override; + // Initialise the motion sensor + virtual bool init() override; - // Called each time our sensor gets a chance to run - virtual int32_t runOnce() override; - virtual void calibrate(uint16_t forSeconds) override; + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; }; #endif diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp index 903cc92f7..bd8325699 100755 --- a/src/motion/LIS3DHSensor.cpp +++ b/src/motion/LIS3DHSensor.cpp @@ -5,33 +5,31 @@ LIS3DHSensor::LIS3DHSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool LIS3DHSensor::init() -{ - if (sensor.begin(deviceAddress())) { - sensor.setRange(LIS3DH_RANGE_2_G); - // Adjust threshold, higher numbers are less sensitive - sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS); - LOG_DEBUG("LIS3DH init ok"); - return true; - } - LOG_DEBUG("LIS3DH init failed"); - return false; +bool LIS3DHSensor::init() { + if (sensor.begin(deviceAddress())) { + sensor.setRange(LIS3DH_RANGE_2_G); + // Adjust threshold, higher numbers are less sensitive + sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS); + LOG_DEBUG("LIS3DH init ok"); + return true; + } + LOG_DEBUG("LIS3DH init failed"); + return false; } -int32_t LIS3DHSensor::runOnce() -{ - if (sensor.getClick() > 0) { - uint8_t click = sensor.getClick(); - if (!config.device.double_tap_as_button_press && config.display.wake_on_tap_or_motion) { - wakeScreen(); - } - - if (config.device.double_tap_as_button_press && (click & 0x20)) { - buttonPress(); - return 500; - } +int32_t LIS3DHSensor::runOnce() { + if (sensor.getClick() > 0) { + uint8_t click = sensor.getClick(); + if (!config.device.double_tap_as_button_press && config.display.wake_on_tap_or_motion) { + wakeScreen(); } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + + if (config.device.double_tap_as_button_press && (click & 0x20)) { + buttonPress(); + return 500; + } + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif diff --git a/src/motion/LIS3DHSensor.h b/src/motion/LIS3DHSensor.h index 924b193e2..8d9567b51 100755 --- a/src/motion/LIS3DHSensor.h +++ b/src/motion/LIS3DHSensor.h @@ -8,15 +8,14 @@ #include -class LIS3DHSensor : public MotionSensor -{ - private: - Adafruit_LIS3DH sensor; +class LIS3DHSensor : public MotionSensor { +private: + Adafruit_LIS3DH sensor; - public: - explicit LIS3DHSensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; +public: + explicit LIS3DHSensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/LSM6DS3Sensor.cpp b/src/motion/LSM6DS3Sensor.cpp index 7e2d7dfcd..bf29f6721 100755 --- a/src/motion/LSM6DS3Sensor.cpp +++ b/src/motion/LSM6DS3Sensor.cpp @@ -5,30 +5,28 @@ LSM6DS3Sensor::LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool LSM6DS3Sensor::init() -{ - if (sensor.begin_I2C(deviceAddress())) { +bool LSM6DS3Sensor::init() { + if (sensor.begin_I2C(deviceAddress())) { - // Default threshold of 2G, less sensitive options are 4, 8 or 16G - sensor.setAccelRange(LSM6DS_ACCEL_RANGE_2_G); + // Default threshold of 2G, less sensitive options are 4, 8 or 16G + sensor.setAccelRange(LSM6DS_ACCEL_RANGE_2_G); - // Duration is number of occurrences needed to trigger, higher threshold is less sensitive - sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DS3_WAKE_THRESH); + // Duration is number of occurrences needed to trigger, higher threshold is less sensitive + sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DS3_WAKE_THRESH); - LOG_DEBUG("LSM6DS3 init ok"); - return true; - } - LOG_DEBUG("LSM6DS3 init failed"); - return false; + LOG_DEBUG("LSM6DS3 init ok"); + return true; + } + LOG_DEBUG("LSM6DS3 init failed"); + return false; } -int32_t LSM6DS3Sensor::runOnce() -{ - if (sensor.shake()) { - wakeScreen(); - return 500; - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t LSM6DS3Sensor::runOnce() { + if (sensor.shake()) { + wakeScreen(); + return 500; + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif \ No newline at end of file diff --git a/src/motion/LSM6DS3Sensor.h b/src/motion/LSM6DS3Sensor.h index 8bf885149..9f58b6ffc 100755 --- a/src/motion/LSM6DS3Sensor.h +++ b/src/motion/LSM6DS3Sensor.h @@ -12,15 +12,14 @@ #include -class LSM6DS3Sensor : public MotionSensor -{ - private: - Adafruit_LSM6DS3TRC sensor; +class LSM6DS3Sensor : public MotionSensor { +private: + Adafruit_LSM6DS3TRC sensor; - public: - explicit LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; +public: + explicit LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/MPU6050Sensor.cpp b/src/motion/MPU6050Sensor.cpp index 5d4f7bfdb..2d9a34984 100755 --- a/src/motion/MPU6050Sensor.cpp +++ b/src/motion/MPU6050Sensor.cpp @@ -4,28 +4,26 @@ MPU6050Sensor::MPU6050Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool MPU6050Sensor::init() -{ - if (sensor.begin(deviceAddress())) { - // setup motion detection - sensor.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); - sensor.setMotionDetectionThreshold(1); - sensor.setMotionDetectionDuration(20); - sensor.setInterruptPinLatch(true); // Keep it latched. Will turn off when reinitialized. - sensor.setInterruptPinPolarity(true); - LOG_DEBUG("MPU6050 init ok"); - return true; - } - LOG_DEBUG("MPU6050 init failed"); - return false; +bool MPU6050Sensor::init() { + if (sensor.begin(deviceAddress())) { + // setup motion detection + sensor.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); + sensor.setMotionDetectionThreshold(1); + sensor.setMotionDetectionDuration(20); + sensor.setInterruptPinLatch(true); // Keep it latched. Will turn off when reinitialized. + sensor.setInterruptPinPolarity(true); + LOG_DEBUG("MPU6050 init ok"); + return true; + } + LOG_DEBUG("MPU6050 init failed"); + return false; } -int32_t MPU6050Sensor::runOnce() -{ - if (sensor.getMotionInterruptStatus()) { - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t MPU6050Sensor::runOnce() { + if (sensor.getMotionInterruptStatus()) { + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif \ No newline at end of file diff --git a/src/motion/MPU6050Sensor.h b/src/motion/MPU6050Sensor.h index 2bca72b34..b4dd1f385 100755 --- a/src/motion/MPU6050Sensor.h +++ b/src/motion/MPU6050Sensor.h @@ -8,15 +8,14 @@ #include -class MPU6050Sensor : public MotionSensor -{ - private: - Adafruit_MPU6050 sensor; +class MPU6050Sensor : public MotionSensor { +private: + Adafruit_MPU6050 sensor; - public: - explicit MPU6050Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; +public: + explicit MPU6050Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index d0bfe4e2c..dd2159919 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -8,76 +8,63 @@ char timeRemainingBuffer[12]; // screen is defined in main.cpp extern graphics::Screen *screen; -MotionSensor::MotionSensor(ScanI2C::FoundDevice foundDevice) -{ - device.address.address = foundDevice.address.address; - device.address.port = foundDevice.address.port; - device.type = foundDevice.type; - LOG_DEBUG("Motion MotionSensor port: %s address: 0x%x type: %d", devicePort() == ScanI2C::I2CPort::WIRE1 ? "Wire1" : "Wire", - (uint8_t)deviceAddress(), deviceType()); +MotionSensor::MotionSensor(ScanI2C::FoundDevice foundDevice) { + device.address.address = foundDevice.address.address; + device.address.port = foundDevice.address.port; + device.type = foundDevice.type; + LOG_DEBUG("Motion MotionSensor port: %s address: 0x%x type: %d", devicePort() == ScanI2C::I2CPort::WIRE1 ? "Wire1" : "Wire", + (uint8_t)deviceAddress(), deviceType()); } -ScanI2C::DeviceType MotionSensor::deviceType() -{ - return device.type; -} +ScanI2C::DeviceType MotionSensor::deviceType() { return device.type; } -uint8_t MotionSensor::deviceAddress() -{ - return device.address.address; -} +uint8_t MotionSensor::deviceAddress() { return device.address.address; } -ScanI2C::I2CPort MotionSensor::devicePort() -{ - return device.address.port; -} +ScanI2C::I2CPort MotionSensor::devicePort() { return device.address.port; } #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - if (screen == nullptr) - return; - // int x_offset = display->width() / 2; - // int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - display->drawString(x, y, "Calibrating\nCompass"); +void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (screen == nullptr) + return; + // int x_offset = display->width() / 2; + // int y_offset = display->height() <= 80 ? 0 : 32; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + display->drawString(x, y, "Calibrating\nCompass"); - uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; - sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); - display->setFont(FONT_SMALL); - display->drawString(x, y + 40, timeRemainingBuffer); + uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; + sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); + display->setFont(FONT_SMALL); + display->drawString(x, y + 40, timeRemainingBuffer); - int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); - // coordinates for the center of the compass/circle - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + display->getHeight() / 2; - } else { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; - } - display->drawCircle(compassX, compassY, compassDiam / 2); - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); + // coordinates for the center of the compass/circle + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + display->getHeight() / 2; + } else { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; + } + display->drawCircle(compassX, compassY, compassDiam / 2); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } #endif #if !MESHTASTIC_EXCLUDE_POWER_FSM -void MotionSensor::wakeScreen() -{ - if (powerFSM.getState() == &stateDARK) { - LOG_DEBUG("Motion wakeScreen detected"); - if (config.display.wake_on_tap_or_motion) - powerFSM.trigger(EVENT_INPUT); - } +void MotionSensor::wakeScreen() { + if (powerFSM.getState() == &stateDARK) { + LOG_DEBUG("Motion wakeScreen detected"); + if (config.display.wake_on_tap_or_motion) + powerFSM.trigger(EVENT_INPUT); + } } -void MotionSensor::buttonPress() -{ - LOG_DEBUG("Motion buttonPress detected"); - powerFSM.trigger(EVENT_PRESS); +void MotionSensor::buttonPress() { + LOG_DEBUG("Motion buttonPress detected"); + powerFSM.trigger(EVENT_PRESS); } #else diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 8eb3bf95b..2f0eca2c5 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -17,48 +17,47 @@ #include "Wire.h" // Base class for motion processing -class MotionSensor -{ - public: - explicit MotionSensor(ScanI2C::FoundDevice foundDevice); - virtual ~MotionSensor(){}; +class MotionSensor { +public: + explicit MotionSensor(ScanI2C::FoundDevice foundDevice); + virtual ~MotionSensor(){}; - // Get the device type - ScanI2C::DeviceType deviceType(); + // Get the device type + ScanI2C::DeviceType deviceType(); - // Get the device address - uint8_t deviceAddress(); + // Get the device address + uint8_t deviceAddress(); - // Get the device port - ScanI2C::I2CPort devicePort(); + // Get the device port + ScanI2C::I2CPort devicePort(); - // Initialise the motion sensor - inline virtual bool init() { return false; }; + // Initialise the motion sensor + inline virtual bool init() { return false; }; - // The method that will be called each time our sensor gets a chance to run - // Returns the desired period for next invocation (or RUN_SAME for no change) - // Refer to /src/concurrency/OSThread.h for more information - inline virtual int32_t runOnce() { return MOTION_SENSOR_CHECK_INTERVAL_MS; }; + // The method that will be called each time our sensor gets a chance to run + // Returns the desired period for next invocation (or RUN_SAME for no change) + // Refer to /src/concurrency/OSThread.h for more information + inline virtual int32_t runOnce() { return MOTION_SENSOR_CHECK_INTERVAL_MS; }; - virtual void calibrate(uint16_t forSeconds){}; + virtual void calibrate(uint16_t forSeconds){}; - protected: - // Turn on the screen when a tap or motion is detected - virtual void wakeScreen(); +protected: + // Turn on the screen when a tap or motion is detected + virtual void wakeScreen(); - // Register a button press when a double-tap is detected - virtual void buttonPress(); + // Register a button press when a double-tap is detected + virtual void buttonPress(); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - // draw an OLED frame (currently only used by the RAK4631 BMX160 sensor) - static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // draw an OLED frame (currently only used by the RAK4631 BMX160 sensor) + static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif - ScanI2C::FoundDevice device; + ScanI2C::FoundDevice device; - // Do calibration if true - bool doCalibration = false; - uint32_t endCalibrationAt = 0; + // Do calibration if true + bool doCalibration = false; + uint32_t endCalibrationAt = 0; }; #endif diff --git a/src/motion/QMA6100PSensor.cpp b/src/motion/QMA6100PSensor.cpp index a04837e80..acf93e660 100644 --- a/src/motion/QMA6100PSensor.cpp +++ b/src/motion/QMA6100PSensor.cpp @@ -6,53 +6,47 @@ volatile static bool QMA6100P_IRQ = false; // Interrupt service routine -void QMA6100PSetInterrupt() -{ - QMA6100P_IRQ = true; -} +void QMA6100PSetInterrupt() { QMA6100P_IRQ = true; } QMA6100PSensor::QMA6100PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool QMA6100PSensor::init() -{ - // Initialise the sensor - sensor = QMA6100PSingleton::GetInstance(); - if (!sensor->init(device)) - return false; +bool QMA6100PSensor::init() { + // Initialise the sensor + sensor = QMA6100PSingleton::GetInstance(); + if (!sensor->init(device)) + return false; - // Enable simple Wake on Motion - return sensor->setWakeOnMotion(); + // Enable simple Wake on Motion + return sensor->setWakeOnMotion(); } #ifdef QMA_6100P_INT_PIN -int32_t QMA6100PSensor::runOnce() -{ - // Wake on motion using hardware interrupts - this is the most efficient way to check for motion - if (QMA6100P_IRQ) { - QMA6100P_IRQ = false; - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t QMA6100PSensor::runOnce() { + // Wake on motion using hardware interrupts - this is the most efficient way to check for motion + if (QMA6100P_IRQ) { + QMA6100P_IRQ = false; + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #else -int32_t QMA6100PSensor::runOnce() -{ - // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) +int32_t QMA6100PSensor::runOnce() { + // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) - uint8_t tempVal; - if (!sensor->readRegisterRegion(SFE_QMA6100P_INT_ST0, &tempVal, 1)) { - LOG_DEBUG("QMA6100PS isWakeOnMotion failed to read interrupts"); - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - - if ((tempVal & 7) != 0) { - // Wake up! - wakeScreen(); - } + uint8_t tempVal; + if (!sensor->readRegisterRegion(SFE_QMA6100P_INT_ST0, &tempVal, 1)) { + LOG_DEBUG("QMA6100PS isWakeOnMotion failed to read interrupts"); return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + if ((tempVal & 7) != 0) { + // Wake up! + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif @@ -62,12 +56,11 @@ int32_t QMA6100PSensor::runOnce() // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun QMA_6100P_I2C -QMA6100PSingleton *QMA6100PSingleton::GetInstance() -{ - if (pinstance == nullptr) { - pinstance = new QMA6100PSingleton(); - } - return pinstance; +QMA6100PSingleton *QMA6100PSingleton::GetInstance() { + if (pinstance == nullptr) { + pinstance = new QMA6100PSingleton(); + } + return pinstance; } QMA6100PSingleton::QMA6100PSingleton() {} @@ -77,107 +70,105 @@ QMA6100PSingleton::~QMA6100PSingleton() {} QMA6100PSingleton *QMA6100PSingleton::pinstance{nullptr}; // Initialise the QMA6100P Sensor -bool QMA6100PSingleton::init(ScanI2C::FoundDevice device) -{ +bool QMA6100PSingleton::init(ScanI2C::FoundDevice device) { // startup #ifdef Wire1 - bool status = begin(device.address.address, device.address.port == ScanI2C::I2CPort::WIRE1 ? &Wire1 : &Wire); + bool status = begin(device.address.address, device.address.port == ScanI2C::I2CPort::WIRE1 ? &Wire1 : &Wire); #else - // check chip id - bool status = begin(device.address.address, &Wire); + // check chip id + bool status = begin(device.address.address, &Wire); #endif - if (status != true) { - LOG_WARN("QMA6100P init begin failed"); - return false; - } - delay(20); - // SW reset to make sure the device starts in a known state - if (softwareReset() != true) { - LOG_WARN("QMA6100P init reset failed"); - return false; - } - delay(20); - // Set range - if (!setRange(QMA_6100P_MPU_ACCEL_SCALE)) { - LOG_WARN("QMA6100P init range failed"); - return false; - } - // set active mode - if (!enableAccel()) { - LOG_WARN("ERROR QMA6100P active mode set failed"); - } - // set calibrateoffsets - if (!calibrateOffsets()) { - LOG_WARN("ERROR QMA6100P calibration failed"); - } + if (status != true) { + LOG_WARN("QMA6100P init begin failed"); + return false; + } + delay(20); + // SW reset to make sure the device starts in a known state + if (softwareReset() != true) { + LOG_WARN("QMA6100P init reset failed"); + return false; + } + delay(20); + // Set range + if (!setRange(QMA_6100P_MPU_ACCEL_SCALE)) { + LOG_WARN("QMA6100P init range failed"); + return false; + } + // set active mode + if (!enableAccel()) { + LOG_WARN("ERROR QMA6100P active mode set failed"); + } + // set calibrateoffsets + if (!calibrateOffsets()) { + LOG_WARN("ERROR QMA6100P calibration failed"); + } #ifdef QMA_6100P_INT_PIN - // Active low & Open Drain - uint8_t tempVal; - if (!readRegisterRegion(SFE_QMA6100P_INTPINT_CONF, &tempVal, 1)) { - LOG_WARN("QMA6100P init failed to read interrupt pin config"); - return false; - } + // Active low & Open Drain + uint8_t tempVal; + if (!readRegisterRegion(SFE_QMA6100P_INTPINT_CONF, &tempVal, 1)) { + LOG_WARN("QMA6100P init failed to read interrupt pin config"); + return false; + } - tempVal |= 0b00000010; // Active low & Open Drain + tempVal |= 0b00000010; // Active low & Open Drain - if (!writeRegisterByte(SFE_QMA6100P_INTPINT_CONF, tempVal)) { - LOG_WARN("QMA6100P init failed to write interrupt pin config"); - return false; - } + if (!writeRegisterByte(SFE_QMA6100P_INTPINT_CONF, tempVal)) { + LOG_WARN("QMA6100P init failed to write interrupt pin config"); + return false; + } - // Latch until cleared, all reads clear the latch - if (!readRegisterRegion(SFE_QMA6100P_INT_CFG, &tempVal, 1)) { - LOG_WARN("QMA6100P init failed to read interrupt config"); - return false; - } + // Latch until cleared, all reads clear the latch + if (!readRegisterRegion(SFE_QMA6100P_INT_CFG, &tempVal, 1)) { + LOG_WARN("QMA6100P init failed to read interrupt config"); + return false; + } - tempVal |= 0b10000001; // Latch until cleared, INT_RD_CLR1 + tempVal |= 0b10000001; // Latch until cleared, INT_RD_CLR1 - if (!writeRegisterByte(SFE_QMA6100P_INT_CFG, tempVal)) { - LOG_WARN("QMA6100P init failed to write interrupt config"); - return false; - } - // Set up an interrupt pin with an internal pullup for active low - pinMode(QMA_6100P_INT_PIN, INPUT_PULLUP); + if (!writeRegisterByte(SFE_QMA6100P_INT_CFG, tempVal)) { + LOG_WARN("QMA6100P init failed to write interrupt config"); + return false; + } + // Set up an interrupt pin with an internal pullup for active low + pinMode(QMA_6100P_INT_PIN, INPUT_PULLUP); - // Set up an interrupt service routine - attachInterrupt(QMA_6100P_INT_PIN, QMA6100PSetInterrupt, FALLING); + // Set up an interrupt service routine + attachInterrupt(QMA_6100P_INT_PIN, QMA6100PSetInterrupt, FALLING); #endif - return true; + return true; } -bool QMA6100PSingleton::setWakeOnMotion() -{ - // Enable 'Any Motion' interrupt - if (!writeRegisterByte(SFE_QMA6100P_INT_EN2, 0b00000111)) { - LOG_WARN("QMA6100P :setWakeOnMotion failed to write interrupt enable"); - return false; - } +bool QMA6100PSingleton::setWakeOnMotion() { + // Enable 'Any Motion' interrupt + if (!writeRegisterByte(SFE_QMA6100P_INT_EN2, 0b00000111)) { + LOG_WARN("QMA6100P :setWakeOnMotion failed to write interrupt enable"); + return false; + } - // Set 'Significant Motion' interrupt map to INT1 - uint8_t tempVal; + // Set 'Significant Motion' interrupt map to INT1 + uint8_t tempVal; - if (!readRegisterRegion(SFE_QMA6100P_INT_MAP1, &tempVal, 1)) { - LOG_WARN("QMA6100P setWakeOnMotion failed to read interrupt map"); - return false; - } + if (!readRegisterRegion(SFE_QMA6100P_INT_MAP1, &tempVal, 1)) { + LOG_WARN("QMA6100P setWakeOnMotion failed to read interrupt map"); + return false; + } - sfe_qma6100p_int_map1_bitfield_t int_map1; - int_map1.all = tempVal; - int_map1.bits.int1_any_mot = 1; // any motion interrupt to INT1 - tempVal = int_map1.all; + sfe_qma6100p_int_map1_bitfield_t int_map1; + int_map1.all = tempVal; + int_map1.bits.int1_any_mot = 1; // any motion interrupt to INT1 + tempVal = int_map1.all; - if (!writeRegisterByte(SFE_QMA6100P_INT_MAP1, tempVal)) { - LOG_WARN("QMA6100P setWakeOnMotion failed to write interrupt map"); - return false; - } + if (!writeRegisterByte(SFE_QMA6100P_INT_MAP1, tempVal)) { + LOG_WARN("QMA6100P setWakeOnMotion failed to write interrupt map"); + return false; + } - // Clear any current interrupts - QMA6100P_IRQ = false; - return true; + // Clear any current interrupts + QMA6100P_IRQ = false; + return true; } #endif diff --git a/src/motion/QMA6100PSensor.h b/src/motion/QMA6100PSensor.h index 72e716ef9..61d33d4e4 100644 --- a/src/motion/QMA6100PSensor.h +++ b/src/motion/QMA6100PSensor.h @@ -17,45 +17,43 @@ extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper for the Sparkfun QMA_6100P_I2C class -class QMA6100PSingleton : public QMA6100P -{ - private: - static QMA6100PSingleton *pinstance; +class QMA6100PSingleton : public QMA6100P { +private: + static QMA6100PSingleton *pinstance; - protected: - QMA6100PSingleton(); - ~QMA6100PSingleton(); +protected: + QMA6100PSingleton(); + ~QMA6100PSingleton(); - public: - // Create a singleton instance (not thread safe) - static QMA6100PSingleton *GetInstance(); +public: + // Create a singleton instance (not thread safe) + static QMA6100PSingleton *GetInstance(); - // Singletons should not be cloneable. - QMA6100PSingleton(QMA6100PSingleton &other) = delete; + // Singletons should not be cloneable. + QMA6100PSingleton(QMA6100PSingleton &other) = delete; - // Singletons should not be assignable. - void operator=(const QMA6100PSingleton &) = delete; + // Singletons should not be assignable. + void operator=(const QMA6100PSingleton &) = delete; - // Initialise the motion sensor singleton for normal operation - bool init(ScanI2C::FoundDevice device); + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); - // Enable Wake on Motion interrupts (sensor must be initialised first) - bool setWakeOnMotion(); + // Enable Wake on Motion interrupts (sensor must be initialised first) + bool setWakeOnMotion(); }; -class QMA6100PSensor : public MotionSensor -{ - private: - QMA6100PSingleton *sensor = nullptr; +class QMA6100PSensor : public MotionSensor { +private: + QMA6100PSingleton *sensor = nullptr; - public: - explicit QMA6100PSensor(ScanI2C::FoundDevice foundDevice); +public: + explicit QMA6100PSensor(ScanI2C::FoundDevice foundDevice); - // Initialise the motion sensor - virtual bool init() override; + // Initialise the motion sensor + virtual bool init() override; - // Called each time our sensor gets a chance to run - virtual int32_t runOnce() override; + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/STK8XXXSensor.cpp b/src/motion/STK8XXXSensor.cpp index d27a1e88d..8fb717fc4 100755 --- a/src/motion/STK8XXXSensor.cpp +++ b/src/motion/STK8XXXSensor.cpp @@ -8,31 +8,29 @@ STK8XXXSensor::STK8XXXSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::M volatile static bool STK_IRQ; -bool STK8XXXSensor::init() -{ - if (sensor.STK8xxx_Initialization(STK8xxx_VAL_RANGE_2G)) { - STK_IRQ = false; - sensor.STK8xxx_Anymotion_init(); - pinMode(STK8XXX_INT, INPUT_PULLUP); - attachInterrupt( - digitalPinToInterrupt(STK8XXX_INT), [] { STK_IRQ = true; }, RISING); +bool STK8XXXSensor::init() { + if (sensor.STK8xxx_Initialization(STK8xxx_VAL_RANGE_2G)) { + STK_IRQ = false; + sensor.STK8xxx_Anymotion_init(); + pinMode(STK8XXX_INT, INPUT_PULLUP); + attachInterrupt( + digitalPinToInterrupt(STK8XXX_INT), [] { STK_IRQ = true; }, RISING); - LOG_DEBUG("STK8XXX init ok"); - return true; - } - LOG_DEBUG("STK8XXX init failed"); - return false; + LOG_DEBUG("STK8XXX init ok"); + return true; + } + LOG_DEBUG("STK8XXX init failed"); + return false; } -int32_t STK8XXXSensor::runOnce() -{ - if (STK_IRQ) { - STK_IRQ = false; - if (config.display.wake_on_tap_or_motion) { - wakeScreen(); - } +int32_t STK8XXXSensor::runOnce() { + if (STK_IRQ) { + STK_IRQ = false; + if (config.display.wake_on_tap_or_motion) { + wakeScreen(); } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif diff --git a/src/motion/STK8XXXSensor.h b/src/motion/STK8XXXSensor.h index f54bc7707..47abf98dd 100755 --- a/src/motion/STK8XXXSensor.h +++ b/src/motion/STK8XXXSensor.h @@ -10,24 +10,22 @@ #include -class STK8XXXSensor : public MotionSensor -{ - private: - STK8xxx sensor; +class STK8XXXSensor : public MotionSensor { +private: + STK8xxx sensor; - public: - explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; +public: + explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #else // Stub -class STK8XXXSensor : public MotionSensor -{ - public: - explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); +class STK8XXXSensor : public MotionSensor { +public: + explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); }; #endif diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 7c33f0360..376be2956 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -43,8 +43,7 @@ MQTT *mqtt; -namespace -{ +namespace { constexpr int reconnectMax = 5; // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets @@ -53,856 +52,814 @@ static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for cha static bool isMqttServerAddressPrivate = false; static bool isConnected = false; -inline void onReceiveProto(char *topic, byte *payload, size_t length) -{ - const DecodedServiceEnvelope e(payload, length); - if (!e.validDecode || e.channel_id == NULL || e.gateway_id == NULL || e.packet == NULL) { - LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); - return; - } +inline void onReceiveProto(char *topic, byte *payload, size_t length) { + const DecodedServiceEnvelope e(payload, length); + if (!e.validDecode || e.channel_id == NULL || e.gateway_id == NULL || e.packet == NULL) { + LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); + return; + } - const meshtastic_Channel &ch = channels.getByName(e.channel_id); - // Find channel by channel_id and check downlink_enabled - if (!(strcmp(e.channel_id, "PKI") == 0 || - (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { - return; - } + const meshtastic_Channel &ch = channels.getByName(e.channel_id); + // Find channel by channel_id and check downlink_enabled + if (!(strcmp(e.channel_id, "PKI") == 0 || (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { + return; + } - bool anyChannelHasDownlink = false; - size_t numChan = channels.getNumChannels(); - for (size_t i = 0; i < numChan; ++i) { - const auto &c = channels.getByIndex(i); - if (c.settings.downlink_enabled) { - anyChannelHasDownlink = true; - break; - } + bool anyChannelHasDownlink = false; + size_t numChan = channels.getNumChannels(); + for (size_t i = 0; i < numChan; ++i) { + const auto &c = channels.getByIndex(i); + if (c.settings.downlink_enabled) { + anyChannelHasDownlink = true; + break; } + } - if (strcmp(e.channel_id, "PKI") == 0 && !anyChannelHasDownlink) { - return; - } - // Generate node ID from nodenum for comparison - std::string nodeId = nodeDB->getNodeId(); - if (strcmp(e.gateway_id, nodeId.c_str()) == 0) { - // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. - // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node - // receives it when we get our own packet back. Then we'll stop our retransmissions. - if (isFromUs(e.packet)) { - auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); - pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; - router->sendLocal(pAck); - } else { - LOG_INFO("Ignore downlink message we originally sent"); - } - return; - } + if (strcmp(e.channel_id, "PKI") == 0 && !anyChannelHasDownlink) { + return; + } + // Generate node ID from nodenum for comparison + std::string nodeId = nodeDB->getNodeId(); + if (strcmp(e.gateway_id, nodeId.c_str()) == 0) { + // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. + // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node + // receives it when we get our own packet back. Then we'll stop our retransmissions. if (isFromUs(e.packet)) { - LOG_INFO("Ignore downlink message we originally sent"); - return; + auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); + pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + router->sendLocal(pAck); + } else { + LOG_INFO("Ignore downlink message we originally sent"); } + return; + } + if (isFromUs(e.packet)) { + LOG_INFO("Ignore downlink message we originally sent"); + return; + } - LOG_INFO("Received MQTT topic %s, len=%u", topic, length); - if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) { - LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start); - return; + LOG_INFO("Received MQTT topic %s, len=%u", topic, length); + if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) { + LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start); + return; + } + + UniquePacketPoolPacket p = packetPool.allocUniqueZeroed(); + p->from = e.packet->from; + p->to = e.packet->to; + p->id = e.packet->id; + p->channel = e.packet->channel; + p->hop_limit = e.packet->hop_limit; + p->hop_start = e.packet->hop_start; + p->want_ack = e.packet->want_ack; + p->via_mqtt = true; // Mark that the packet was received via MQTT + p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + p->which_payload_variant = e.packet->which_payload_variant; + memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted))); + + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (moduleConfig.mqtt.encryption_enabled) { + LOG_INFO("Ignore decoded message on MQTT, encryption is enabled"); + return; } - - UniquePacketPoolPacket p = packetPool.allocUniqueZeroed(); - p->from = e.packet->from; - p->to = e.packet->to; - p->id = e.packet->id; - p->channel = e.packet->channel; - p->hop_limit = e.packet->hop_limit; - p->hop_start = e.packet->hop_start; - p->want_ack = e.packet->want_ack; - p->via_mqtt = true; // Mark that the packet was received via MQTT - p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; - p->which_payload_variant = e.packet->which_payload_variant; - memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted))); - - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - if (moduleConfig.mqtt.encryption_enabled) { - LOG_INFO("Ignore decoded message on MQTT, encryption is enabled"); - return; - } - if (p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { - LOG_INFO("Ignore decoded admin packet"); - return; - } - p->channel = ch.index; + if (p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { + LOG_INFO("Ignore decoded admin packet"); + return; } + p->channel = ch.index; + } - // PKI messages get accepted even if we can't decrypt - if (router && p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && strcmp(e.channel_id, "PKI") == 0) { - const meshtastic_NodeInfoLite *tx = nodeDB->getMeshNode(getFrom(p.get())); - const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); - // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's - // likely they discovered each other via a channel we have downlink enabled for - if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) - router->enqueueReceivedMessage(p.release()); - } else if (router && - perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key - router->enqueueReceivedMessage(p.release()); + // PKI messages get accepted even if we can't decrypt + if (router && p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && strcmp(e.channel_id, "PKI") == 0) { + const meshtastic_NodeInfoLite *tx = nodeDB->getMeshNode(getFrom(p.get())); + const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); + // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's + // likely they discovered each other via a channel we have downlink enabled for + if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) + router->enqueueReceivedMessage(p.release()); + } else if (router && perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key + router->enqueueReceivedMessage(p.release()); } #if !defined(ARCH_NRF52) || NRF52_USE_JSON // returns true if this is a valid JSON envelope which we accept on downlink -inline bool isValidJsonEnvelope(JSONObject &json) -{ - // Generate node ID from nodenum for comparison - std::string nodeId = nodeDB->getNodeId(); - // if "sender" is provided, avoid processing packets we uplinked - return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) && - (json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number - (json.find("from") != json.end()) && json["from"]->IsNumber() && - (json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us - (json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type - (json.find("payload") != json.end()); // should have a payload +inline bool isValidJsonEnvelope(JSONObject &json) { + // Generate node ID from nodenum for comparison + std::string nodeId = nodeDB->getNodeId(); + // if "sender" is provided, avoid processing packets we uplinked + return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) && + (json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number + (json.find("from") != json.end()) && json["from"]->IsNumber() && + (json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us + (json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type + (json.find("payload") != json.end()); // should have a payload } -inline void onReceiveJson(byte *payload, size_t length) -{ - char payloadStr[length + 1]; - memcpy(payloadStr, payload, length); - payloadStr[length] = 0; // null terminated string - std::unique_ptr json_value(JSON::Parse(payloadStr)); - if (json_value == nullptr) { - LOG_ERROR("JSON received payload on MQTT but not a valid JSON"); - return; - } +inline void onReceiveJson(byte *payload, size_t length) { + char payloadStr[length + 1]; + memcpy(payloadStr, payload, length); + payloadStr[length] = 0; // null terminated string + std::unique_ptr json_value(JSON::Parse(payloadStr)); + if (json_value == nullptr) { + LOG_ERROR("JSON received payload on MQTT but not a valid JSON"); + return; + } - JSONObject json; - json = json_value->AsObject(); + JSONObject json; + json = json_value->AsObject(); - if (!isValidJsonEnvelope(json)) { - LOG_ERROR("JSON received payload on MQTT but not a valid envelope"); - return; - } + if (!isValidJsonEnvelope(json)) { + LOG_ERROR("JSON received payload on MQTT but not a valid envelope"); + return; + } - // this is a valid envelope - if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) { - std::string jsonPayloadStr = json["payload"]->AsString(); - LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length()); + // this is a valid envelope + if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) { + std::string jsonPayloadStr = json["payload"]->AsString(); + LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length()); - // construct protobuf data packet using TEXT_MESSAGE, send it to the mesh - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - if (json.find("channel") != json.end() && json["channel"]->IsNumber() && - (json["channel"]->AsNumber() < channels.getNumChannels())) - p->channel = json["channel"]->AsNumber(); - if (json.find("to") != json.end() && json["to"]->IsNumber()) - p->to = json["to"]->AsNumber(); - if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) - p->hop_limit = json["hopLimit"]->AsNumber(); - if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) { - memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length()); - p->decoded.payload.size = jsonPayloadStr.length(); - service->sendToMesh(p, RX_SRC_LOCAL); - } else { - LOG_WARN("Received MQTT json payload too long, drop"); - } - } else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) { - // invent the "sendposition" type for a valid envelope - JSONObject posit; - posit = json["payload"]->AsObject(); // get nested JSON Position - meshtastic_Position pos = meshtastic_Position_init_default; - if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber()) - pos.latitude_i = posit["latitude_i"]->AsNumber(); - if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber()) - pos.longitude_i = posit["longitude_i"]->AsNumber(); - if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber()) - pos.altitude = posit["altitude"]->AsNumber(); - if (posit.find("time") != posit.end() && posit["time"]->IsNumber()) - pos.time = posit["time"]->AsNumber(); - - // construct protobuf data packet using POSITION, send it to the mesh - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_POSITION_APP; - if (json.find("channel") != json.end() && json["channel"]->IsNumber() && - (json["channel"]->AsNumber() < channels.getNumChannels())) - p->channel = json["channel"]->AsNumber(); - if (json.find("to") != json.end() && json["to"]->IsNumber()) - p->to = json["to"]->AsNumber(); - if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) - p->hop_limit = json["hopLimit"]->AsNumber(); - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg, - &pos); // make the Data protobuf from position - service->sendToMesh(p, RX_SRC_LOCAL); + // construct protobuf data packet using TEXT_MESSAGE, send it to the mesh + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + if (json.find("channel") != json.end() && json["channel"]->IsNumber() && (json["channel"]->AsNumber() < channels.getNumChannels())) + p->channel = json["channel"]->AsNumber(); + if (json.find("to") != json.end() && json["to"]->IsNumber()) + p->to = json["to"]->AsNumber(); + if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) + p->hop_limit = json["hopLimit"]->AsNumber(); + if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) { + memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length()); + p->decoded.payload.size = jsonPayloadStr.length(); + service->sendToMesh(p, RX_SRC_LOCAL); } else { - LOG_DEBUG("JSON ignore downlink message with unsupported type"); + LOG_WARN("Received MQTT json payload too long, drop"); } + } else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) { + // invent the "sendposition" type for a valid envelope + JSONObject posit; + posit = json["payload"]->AsObject(); // get nested JSON Position + meshtastic_Position pos = meshtastic_Position_init_default; + if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber()) + pos.latitude_i = posit["latitude_i"]->AsNumber(); + if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber()) + pos.longitude_i = posit["longitude_i"]->AsNumber(); + if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber()) + pos.altitude = posit["altitude"]->AsNumber(); + if (posit.find("time") != posit.end() && posit["time"]->IsNumber()) + pos.time = posit["time"]->AsNumber(); + + // construct protobuf data packet using POSITION, send it to the mesh + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_POSITION_APP; + if (json.find("channel") != json.end() && json["channel"]->IsNumber() && (json["channel"]->AsNumber() < channels.getNumChannels())) + p->channel = json["channel"]->AsNumber(); + if (json.find("to") != json.end() && json["to"]->IsNumber()) + p->to = json["to"]->AsNumber(); + if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) + p->hop_limit = json["hopLimit"]->AsNumber(); + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg, + &pos); // make the Data protobuf from position + service->sendToMesh(p, RX_SRC_LOCAL); + } else { + LOG_DEBUG("JSON ignore downlink message with unsupported type"); + } } #endif /// Determines if the given IPAddress is a private IPv4 address, i.e. not routable on the public internet. -bool isPrivateIpAddress(const IPAddress &ip) -{ - constexpr struct { - uint32_t network; - uint32_t mask; - } privateCidrRanges[] = { - {.network = 192u << 24 | 168 << 16, .mask = 0xffff0000}, // 192.168.0.0/16 - {.network = 172u << 24 | 16 << 16, .mask = 0xfff00000}, // 172.16.0.0/12 - {.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16 - {.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8 - {.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32 - {.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10 - }; - const uint32_t addr = ntohl(ip); - for (const auto &cidrRange : privateCidrRanges) { - if (cidrRange.network == (addr & cidrRange.mask)) { - LOG_INFO("MQTT server on a private IP"); - return true; - } +bool isPrivateIpAddress(const IPAddress &ip) { + constexpr struct { + uint32_t network; + uint32_t mask; + } privateCidrRanges[] = { + {.network = 192u << 24 | 168 << 16, .mask = 0xffff0000}, // 192.168.0.0/16 + {.network = 172u << 24 | 16 << 16, .mask = 0xfff00000}, // 172.16.0.0/12 + {.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16 + {.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8 + {.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32 + {.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10 + }; + const uint32_t addr = ntohl(ip); + for (const auto &cidrRange : privateCidrRanges) { + if (cidrRange.network == (addr & cidrRange.mask)) { + LOG_INFO("MQTT server on a private IP"); + return true; } - return false; + } + return false; } // Separate a [:] string. Returns a pair containing the parsed host and port. If the port is // not present in the input string, or is invalid, the value of the `port` argument will be returned. -std::pair parseHostAndPort(String server, uint16_t port = 0) -{ - const int delimIndex = server.indexOf(':'); - if (delimIndex > 0) { - const long parsedPort = server.substring(delimIndex + 1, server.length()).toInt(); - if (parsedPort < 1 || parsedPort > UINT16_MAX) { - LOG_WARN("Invalid MQTT port %d: %s", parsedPort, server.c_str()); - } else { - port = parsedPort; - } - server[delimIndex] = 0; +std::pair parseHostAndPort(String server, uint16_t port = 0) { + const int delimIndex = server.indexOf(':'); + if (delimIndex > 0) { + const long parsedPort = server.substring(delimIndex + 1, server.length()).toInt(); + if (parsedPort < 1 || parsedPort > UINT16_MAX) { + LOG_WARN("Invalid MQTT port %d: %s", parsedPort, server.c_str()); + } else { + port = parsedPort; } - return std::make_pair(std::move(server), port); + server[delimIndex] = 0; + } + return std::make_pair(std::move(server), port); } -bool isDefaultServer(const String &host) -{ - return host.length() == 0 || host == default_mqtt_address; -} +bool isDefaultServer(const String &host) { return host.length() == 0 || host == default_mqtt_address; } -bool isDefaultRootTopic(const String &root) -{ - return root.length() == 0 || root == default_mqtt_root; -} +bool isDefaultRootTopic(const String &root) { return root.length() == 0 || root == default_mqtt_root; } struct PubSubConfig { - explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config) - { - if (*config.address) { - serverAddr = config.address; - mqttUsername = config.username; - mqttPassword = config.password; - } - if (config.tls_enabled) { - serverPort = 8883; - } - std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); + explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { + if (*config.address) { + serverAddr = config.address; + mqttUsername = config.username; + mqttPassword = config.password; } + if (config.tls_enabled) { + serverPort = 8883; + } + std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); + } - // Defaults - static constexpr uint16_t defaultPort = 1883; - static constexpr uint16_t defaultPortTls = 8883; + // Defaults + static constexpr uint16_t defaultPort = 1883; + static constexpr uint16_t defaultPortTls = 8883; - uint16_t serverPort = defaultPort; - String serverAddr = default_mqtt_address; - const char *mqttUsername = default_mqtt_username; - const char *mqttPassword = default_mqtt_password; + uint16_t serverPort = defaultPort; + String serverAddr = default_mqtt_address; + const char *mqttUsername = default_mqtt_username; + const char *mqttPassword = default_mqtt_password; }; #if HAS_NETWORKING -bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client) -{ - pubSub.setBufferSize(1024, 1024); - pubSub.setClient(client); - pubSub.setServer(config.serverAddr.c_str(), config.serverPort); +bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client) { + pubSub.setBufferSize(1024, 1024); + pubSub.setClient(client); + pubSub.setServer(config.serverAddr.c_str(), config.serverPort); - LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), - config.serverPort, config.mqttUsername, config.mqttPassword); + LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), config.serverPort, + config.mqttUsername, config.mqttPassword); - // Generate node ID from nodenum for client identification - std::string nodeId = nodeDB->getNodeId(); - const bool connected = pubSub.connect(nodeId.c_str(), config.mqttUsername, config.mqttPassword); - if (connected) { - isConnected = true; - LOG_INFO("MQTT connected"); - } else { - isConnected = false; - LOG_WARN("Failed to connect to MQTT server"); - } - return connected; + // Generate node ID from nodenum for client identification + std::string nodeId = nodeDB->getNodeId(); + const bool connected = pubSub.connect(nodeId.c_str(), config.mqttUsername, config.mqttPassword); + if (connected) { + isConnected = true; + LOG_INFO("MQTT connected"); + } else { + isConnected = false; + LOG_WARN("Failed to connect to MQTT server"); + } + return connected; } #endif -inline bool isConnectedToNetwork() -{ +inline bool isConnectedToNetwork() { #ifdef USE_WS5500 - if (ETH.connected()) - return true; + if (ETH.connected()) + return true; #endif #if HAS_WIFI - return WiFi.isConnected(); + return WiFi.isConnected(); #elif HAS_ETHERNET - return Ethernet.linkStatus() == LinkON; + return Ethernet.linkStatus() == LinkON; #else - return false; + return false; #endif } /** return true if we have a channel that wants uplink/downlink or map reporting is enabled */ -bool wantsLink() -{ - const bool hasChannelorMapReport = - moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); - return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork()); +bool wantsLink() { + const bool hasChannelorMapReport = moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); + return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork()); } } // namespace -void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) -{ - mqtt->onReceive(topic, payload, length); +void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) { mqtt->onReceive(topic, payload, length); } + +void MQTT::onClientProxyReceive(meshtastic_MqttClientProxyMessage msg) { + onReceive(msg.topic, msg.payload_variant.data.bytes, msg.payload_variant.data.size); } -void MQTT::onClientProxyReceive(meshtastic_MqttClientProxyMessage msg) -{ - onReceive(msg.topic, msg.payload_variant.data.bytes, msg.payload_variant.data.size); -} +void MQTT::onReceive(char *topic, byte *payload, size_t length) { + if (length == 0) { + LOG_WARN("Empty MQTT payload received, topic %s!", topic); + return; + } -void MQTT::onReceive(char *topic, byte *payload, size_t length) -{ - if (length == 0) { - LOG_WARN("Empty MQTT payload received, topic %s!", topic); - return; - } - - // check if this is a json payload message by comparing the topic start - if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) { + // check if this is a json payload message by comparing the topic start + if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) { #if !defined(ARCH_NRF52) || NRF52_USE_JSON - // parse the channel name from the topic string - // the topic has been checked above for having jsonTopic prefix, so just move past it - char *channelName = topic + jsonTopic.length(); - // if another "/" was added, parse string up to that character - channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; - // We allow downlink JSON packets only on a channel named "mqtt" - const meshtastic_Channel &sendChannel = channels.getByName(channelName); - if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && - sendChannel.settings.downlink_enabled)) { - LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); - return; - } - onReceiveJson(payload, length); -#endif - return; + // parse the channel name from the topic string + // the topic has been checked above for having jsonTopic prefix, so just move past it + char *channelName = topic + jsonTopic.length(); + // if another "/" was added, parse string up to that character + channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; + // We allow downlink JSON packets only on a channel named "mqtt" + const meshtastic_Channel &sendChannel = channels.getByName(channelName); + if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && + sendChannel.settings.downlink_enabled)) { + LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); + return; } + onReceiveJson(payload, length); +#endif + return; + } - onReceiveProto(topic, payload, length); + onReceiveProto(topic, payload, length); } -void mqttInit() -{ - new MQTT(); -} +void mqttInit() { new MQTT(); } #if HAS_NETWORKING MQTT::MQTT() : MQTT(std::unique_ptr(new MQTTClient())) {} MQTT::MQTT(std::unique_ptr _mqttClient) : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE), mqttClient(std::move(_mqttClient)), pubSub(*mqttClient) #else -MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) +MQTT::MQTT() + : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) #endif { - if (moduleConfig.mqtt.enabled) { - LOG_DEBUG("Init MQTT"); + if (moduleConfig.mqtt.enabled) { + LOG_DEBUG("Init MQTT"); - assert(!mqtt); - mqtt = this; + assert(!mqtt); + mqtt = this; - if (*moduleConfig.mqtt.root) { - cryptTopic = moduleConfig.mqtt.root + cryptTopic; - jsonTopic = moduleConfig.mqtt.root + jsonTopic; - mapTopic = moduleConfig.mqtt.root + mapTopic; - isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root); - } else { - cryptTopic = "msh" + cryptTopic; - jsonTopic = "msh" + jsonTopic; - mapTopic = "msh" + mapTopic; - isConfiguredForDefaultRootTopic = true; - } - - if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) { - map_position_precision = Default::getConfiguredOrDefault(moduleConfig.mqtt.map_report_settings.position_precision, - default_map_position_precision); - map_publish_interval_msecs = Default::getConfiguredOrDefaultMs( - moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); - } - - String host = parseHostAndPort(moduleConfig.mqtt.address).first; - isConfiguredForDefaultServer = isDefaultServer(host); - IPAddress ip; - isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); - -#if HAS_NETWORKING - if (!moduleConfig.mqtt.proxy_to_client_enabled) - pubSub.setCallback(mqttCallback); -#endif - - if (moduleConfig.mqtt.proxy_to_client_enabled) { - LOG_INFO("MQTT configured to use client proxy"); - enabled = true; - runASAP = true; - reconnectCount = 0; - publishNodeInfo(); - } - // preflightSleepObserver.observe(&preflightSleep); + if (*moduleConfig.mqtt.root) { + cryptTopic = moduleConfig.mqtt.root + cryptTopic; + jsonTopic = moduleConfig.mqtt.root + jsonTopic; + mapTopic = moduleConfig.mqtt.root + mapTopic; + isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root); } else { - disable(); + cryptTopic = "msh" + cryptTopic; + jsonTopic = "msh" + jsonTopic; + mapTopic = "msh" + mapTopic; + isConfiguredForDefaultRootTopic = true; } + + if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) { + map_position_precision = + Default::getConfiguredOrDefault(moduleConfig.mqtt.map_report_settings.position_precision, default_map_position_precision); + map_publish_interval_msecs = + Default::getConfiguredOrDefaultMs(moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); + } + + String host = parseHostAndPort(moduleConfig.mqtt.address).first; + isConfiguredForDefaultServer = isDefaultServer(host); + IPAddress ip; + isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); + +#if HAS_NETWORKING + if (!moduleConfig.mqtt.proxy_to_client_enabled) + pubSub.setCallback(mqttCallback); +#endif + + if (moduleConfig.mqtt.proxy_to_client_enabled) { + LOG_INFO("MQTT configured to use client proxy"); + enabled = true; + runASAP = true; + reconnectCount = 0; + publishNodeInfo(); + } + // preflightSleepObserver.observe(&preflightSleep); + } else { + disable(); + } } -bool MQTT::isConnectedDirectly() -{ +bool MQTT::isConnectedDirectly() { #if HAS_NETWORKING - return pubSub.connected(); + return pubSub.connected(); #else - return false; + return false; #endif } -bool MQTT::publish(const char *topic, const char *payload, bool retained) -{ +bool MQTT::publish(const char *topic, const char *payload, bool retained) { + if (moduleConfig.mqtt.proxy_to_client_enabled) { + meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); + msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; + strcpy(msg->topic, topic); + strcpy(msg->payload_variant.text, payload); + msg->retained = retained; + service->sendMqttMessageToClientProxy(msg); + return true; + } +#if HAS_NETWORKING + else if (isConnectedDirectly()) { + return pubSub.publish(topic, payload, retained); + } +#endif + return false; +} + +bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, bool retained) { + if (moduleConfig.mqtt.proxy_to_client_enabled) { + meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); + msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; + strlcpy(msg->topic, topic, sizeof(msg->topic)); + if (length > sizeof(msg->payload_variant.data.bytes)) + length = sizeof(msg->payload_variant.data.bytes); + msg->payload_variant.data.size = length; + memcpy(msg->payload_variant.data.bytes, payload, length); + msg->retained = retained; + service->sendMqttMessageToClientProxy(msg); + return true; + } +#if HAS_NETWORKING + else if (isConnectedDirectly()) { + return pubSub.publish(topic, payload, length, retained); + } +#endif + return false; +} + +void MQTT::reconnect() { + isConnected = false; + if (wantsLink()) { if (moduleConfig.mqtt.proxy_to_client_enabled) { - meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); - msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; - strcpy(msg->topic, topic); - strcpy(msg->payload_variant.text, payload); - msg->retained = retained; - service->sendMqttMessageToClientProxy(msg); - return true; + LOG_INFO("MQTT connect via client proxy instead"); + enabled = true; + runASAP = true; + reconnectCount = 0; + + publishNodeInfo(); + return; // Don't try to connect directly to the server } #if HAS_NETWORKING - else if (isConnectedDirectly()) { - return pubSub.publish(topic, payload, retained); - } -#endif - return false; -} - -bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, bool retained) -{ - if (moduleConfig.mqtt.proxy_to_client_enabled) { - meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); - msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - strlcpy(msg->topic, topic, sizeof(msg->topic)); - if (length > sizeof(msg->payload_variant.data.bytes)) - length = sizeof(msg->payload_variant.data.bytes); - msg->payload_variant.data.size = length; - memcpy(msg->payload_variant.data.bytes, payload, length); - msg->retained = retained; - service->sendMqttMessageToClientProxy(msg); - return true; - } -#if HAS_NETWORKING - else if (isConnectedDirectly()) { - return pubSub.publish(topic, payload, length, retained); - } -#endif - return false; -} - -void MQTT::reconnect() -{ - isConnected = false; - if (wantsLink()) { - if (moduleConfig.mqtt.proxy_to_client_enabled) { - LOG_INFO("MQTT connect via client proxy instead"); - enabled = true; - runASAP = true; - reconnectCount = 0; - - publishNodeInfo(); - return; // Don't try to connect directly to the server - } -#if HAS_NETWORKING - const PubSubConfig ps_config(moduleConfig.mqtt); - MQTTClient *clientConnection = mqttClient.get(); + const PubSubConfig ps_config(moduleConfig.mqtt); + MQTTClient *clientConnection = mqttClient.get(); #if MQTT_SUPPORTS_TLS - if (moduleConfig.mqtt.tls_enabled) { - mqttClientTLS.setInsecure(); - LOG_INFO("Use TLS-encrypted session"); - clientConnection = &mqttClientTLS; - } else { - LOG_INFO("Use non-TLS-encrypted session"); - } + if (moduleConfig.mqtt.tls_enabled) { + mqttClientTLS.setInsecure(); + LOG_INFO("Use TLS-encrypted session"); + clientConnection = &mqttClientTLS; + } else { + LOG_INFO("Use non-TLS-encrypted session"); + } #endif - if (connectPubSub(ps_config, pubSub, *clientConnection)) { - enabled = true; // Start running background process again - runASAP = true; - reconnectCount = 0; - isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP()); - isConnected = true; - publishNodeInfo(); - sendSubscriptions(); - } else { + if (connectPubSub(ps_config, pubSub, *clientConnection)) { + enabled = true; // Start running background process again + runASAP = true; + reconnectCount = 0; + isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP()); + isConnected = true; + publishNodeInfo(); + sendSubscriptions(); + } else { #if HAS_WIFI && !defined(ARCH_PORTDUINO) - reconnectCount++; - LOG_ERROR("Failed to contact MQTT server directly (%d/%d)", reconnectCount, reconnectMax); - if (reconnectCount >= reconnectMax) { - needReconnect = true; - wifiReconnect->setIntervalFromNow(0); - reconnectCount = 0; - } -#endif - } + reconnectCount++; + LOG_ERROR("Failed to contact MQTT server directly (%d/%d)", reconnectCount, reconnectMax); + if (reconnectCount >= reconnectMax) { + needReconnect = true; + wifiReconnect->setIntervalFromNow(0); + reconnectCount = 0; + } #endif } +#endif + } } -void MQTT::sendSubscriptions() -{ +void MQTT::sendSubscriptions() { #if HAS_NETWORKING - bool hasDownlink = false; - size_t numChan = channels.getNumChannels(); - for (size_t i = 0; i < numChan; i++) { - const auto &ch = channels.getByIndex(i); - if (ch.settings.downlink_enabled) { - hasDownlink = true; - std::string topic = cryptTopic + channels.getGlobalId(i) + "/+"; - LOG_INFO("Subscribe to %s", topic.c_str()); - pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right? -#if !defined(ARCH_NRF52) || \ - defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ### - if (moduleConfig.mqtt.json_enabled == true) { - std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+"; - LOG_INFO("Subscribe to %s", topicDecoded.c_str()); - pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right? - } + bool hasDownlink = false; + size_t numChan = channels.getNumChannels(); + for (size_t i = 0; i < numChan; i++) { + const auto &ch = channels.getByIndex(i); + if (ch.settings.downlink_enabled) { + hasDownlink = true; + std::string topic = cryptTopic + channels.getGlobalId(i) + "/+"; + LOG_INFO("Subscribe to %s", topic.c_str()); + pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right? +#if !defined(ARCH_NRF52) || defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ### + if (moduleConfig.mqtt.json_enabled == true) { + std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+"; + LOG_INFO("Subscribe to %s", topicDecoded.c_str()); + pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right? + } #endif // ARCH_NRF52 NRF52_USE_JSON - } } + } #if !MESHTASTIC_EXCLUDE_PKI - if (hasDownlink) { - std::string topic = cryptTopic + "PKI/+"; - LOG_INFO("Subscribe to %s", topic.c_str()); - pubSub.subscribe(topic.c_str(), 1); - } + if (hasDownlink) { + std::string topic = cryptTopic + "PKI/+"; + LOG_INFO("Subscribe to %s", topic.c_str()); + pubSub.subscribe(topic.c_str(), 1); + } #endif #endif } -int32_t MQTT::runOnce() -{ - if (!moduleConfig.mqtt.enabled || !(moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled())) - return disable(); - bool wantConnection = wantsLink(); +int32_t MQTT::runOnce() { + if (!moduleConfig.mqtt.enabled || !(moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled())) + return disable(); + bool wantConnection = wantsLink(); - perhapsReportToMap(); + perhapsReportToMap(); - // If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact server - if (moduleConfig.mqtt.proxy_to_client_enabled) { + // If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact + // server + if (moduleConfig.mqtt.proxy_to_client_enabled) { + publishQueuedMessages(); + return 200; + } +#if HAS_NETWORKING + else if (!pubSub.loop()) { + if (!wantConnection) + return 5000; // If we don't want connection now, check again in 5 secs + else { + reconnect(); + // If we succeeded, empty the queue one by one and start reading rapidly, else try again in 30 seconds (TCP + // connections are EXPENSIVE so try rarely) + if (isConnectedDirectly()) { publishQueuedMessages(); return 200; + } else + return 30000; + } + } else { + // we are connected to server, check often for new requests on the TCP port + if (!wantConnection) { + LOG_INFO("MQTT link not needed, drop"); + pubSub.disconnect(); } -#if HAS_NETWORKING - else if (!pubSub.loop()) { - if (!wantConnection) - return 5000; // If we don't want connection now, check again in 5 secs - else { - reconnect(); - // If we succeeded, empty the queue one by one and start reading rapidly, else try again in 30 seconds (TCP - // connections are EXPENSIVE so try rarely) - if (isConnectedDirectly()) { - publishQueuedMessages(); - return 200; - } else - return 30000; - } - } else { - // we are connected to server, check often for new requests on the TCP port - if (!wantConnection) { - LOG_INFO("MQTT link not needed, drop"); - pubSub.disconnect(); - } - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) - return 20; - } + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) + return 20; + } #else - // No networking available, return default interval - return 30000; + // No networking available, return default interval + return 30000; #endif } -bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client) -{ - const PubSubConfig parsed(config); +bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client) { + const PubSubConfig parsed(config); - if (config.enabled && !config.proxy_to_client_enabled) { + if (config.enabled && !config.proxy_to_client_enabled) { #if HAS_NETWORKING - std::unique_ptr clientConnection; - if (config.tls_enabled) { + std::unique_ptr clientConnection; + if (config.tls_enabled) { #if MQTT_SUPPORTS_TLS - MQTTClientTLS *tlsClient = new MQTTClientTLS; - clientConnection.reset(tlsClient); - tlsClient->setInsecure(); + MQTTClientTLS *tlsClient = new MQTTClientTLS; + clientConnection.reset(tlsClient); + tlsClient->setInsecure(); #else - LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); - return false; + LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); + return false; #endif - } else { - clientConnection.reset(new MQTTClient); - } - std::unique_ptr pubSub(new PubSubClient); - if (isConnectedToNetwork()) { - return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); - } -#else - const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; - LOG_ERROR(warning); -#if !IS_RUNNING_TESTS - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, warning, sizeof(cn->message) - 1); - cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination - service->sendClientNotification(cn); -#endif - return false; -#endif - } - - const bool defaultServer = isDefaultServer(parsed.serverAddr); - if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) { - const char *warning = "Invalid MQTT config: default server address must not have a port specified"; - LOG_ERROR(warning); -#if !IS_RUNNING_TESTS - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, warning, sizeof(cn->message) - 1); - cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination - service->sendClientNotification(cn); -#endif - return false; - } - return true; -} - -void MQTT::publishNodeInfo() -{ - // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) -} -void MQTT::publishQueuedMessages() -{ - if (mqttQueue.isEmpty()) - return; - - if (!moduleConfig.mqtt.proxy_to_client_enabled && !isConnected) - return; - - LOG_DEBUG("Publish enqueued MQTT message"); - const std::unique_ptr entry(mqttQueue.dequeuePtr(0)); - LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size()); - publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false); - -#if !defined(ARCH_NRF52) || \ - defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### - if (!moduleConfig.mqtt.json_enabled) - return; - - // handle json topic - const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size()); - if (!env.validDecode || env.packet == NULL || env.channel_id == NULL) - return; - - auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet); - if (jsonString.length() == 0) - return; - - // Generate node ID from nodenum for topic - std::string nodeId = nodeDB->getNodeId(); - - std::string topicJson; - if (env.packet->pki_encrypted) { - topicJson = jsonTopic + "PKI/" + nodeId; } else { - topicJson = jsonTopic + env.channel_id + "/" + nodeId; + clientConnection.reset(new MQTTClient); } + std::unique_ptr pubSub(new PubSubClient); + if (isConnectedToNetwork()) { + return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + } +#else + const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif + return false; +#endif + } + + const bool defaultServer = isDefaultServer(parsed.serverAddr); + if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) { + const char *warning = "Invalid MQTT config: default server address must not have a port specified"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif + return false; + } + return true; +} + +void MQTT::publishNodeInfo() { + // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) +} +void MQTT::publishQueuedMessages() { + if (mqttQueue.isEmpty()) + return; + + if (!moduleConfig.mqtt.proxy_to_client_enabled && !isConnected) + return; + + LOG_DEBUG("Publish enqueued MQTT message"); + const std::unique_ptr entry(mqttQueue.dequeuePtr(0)); + LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size()); + publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false); + +#if !defined(ARCH_NRF52) || defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### + if (!moduleConfig.mqtt.json_enabled) + return; + + // handle json topic + const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size()); + if (!env.validDecode || env.packet == NULL || env.channel_id == NULL) + return; + + auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet); + if (jsonString.length() == 0) + return; + + // Generate node ID from nodenum for topic + std::string nodeId = nodeDB->getNodeId(); + + std::string topicJson; + if (env.packet->pki_encrypted) { + topicJson = jsonTopic + "PKI/" + nodeId; + } else { + topicJson = jsonTopic + env.channel_id + "/" + nodeId; + } + LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); + publish(topicJson.c_str(), jsonString.c_str(), false); +#endif // ARCH_NRF52 NRF52_USE_JSON +} + +void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex) { + if (mp_encrypted.via_mqtt) + return; // Don't send messages that came from MQTT back into MQTT + bool uplinkEnabled = false; + for (int i = 0; i <= 7; i++) { + if (channels.getByIndex(i).settings.uplink_enabled) + uplinkEnabled = true; + } + if (!uplinkEnabled) + return; // no channels have an uplink enabled + auto &ch = channels.getByIndex(chIndex); + + // mp_decoded will not be decoded when it's PKI encrypted and not directed to us + if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield + bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); + // check for the lowest bit of the data bitfield set false, and the use of one of the default keys. + if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink && + (ch.settings.psk.size < 2 || (ch.settings.psk.size == 16 && memcmp(ch.settings.psk.bytes, defaultpsk, 16)) || + (ch.settings.psk.size == 32 && memcmp(ch.settings.psk.bytes, eventpsk, 32)))) { + LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); + return; + } + + if (isConfiguredForDefaultServer && + (mp_decoded.decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP || mp_decoded.decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP)) { + LOG_DEBUG("MQTT onSend - Ignoring range test or detection sensor message on public mqtt"); + return; + } + } + // Either encrypted packet (we couldn't decrypt) is marked as pki_encrypted, or we could decode the PKI encrypted + // packet + bool isPKIEncrypted = mp_encrypted.pki_encrypted || mp_decoded.pki_encrypted; + // If it was to a channel, check uplink enabled, else must be pki_encrypted + if (!(ch.settings.uplink_enabled || isPKIEncrypted)) + return; + const char *channelId = isPKIEncrypted ? "PKI" : channels.getGlobalId(chIndex); + + LOG_DEBUG("MQTT onSend - Publish "); + const meshtastic_MeshPacket *p; + if (moduleConfig.mqtt.encryption_enabled) { + p = &mp_encrypted; + LOG_DEBUG("encrypted message"); + } else if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + p = &mp_decoded; + LOG_DEBUG("portnum %i message", mp_decoded.decoded.portnum); + } else { + LOG_DEBUG("nothing, pkt not decrypted"); + return; // Don't upload a still-encrypted PKI packet if not encryption_enabled + } + + // Generate node ID from nodenum for service envelope + std::string nodeId = nodeDB->getNodeId(); + + const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), + .channel_id = const_cast(channelId), + .gateway_id = const_cast(nodeId.c_str())}; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); + std::string topic = cryptTopic + channelId + "/" + nodeId; + + if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { + LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes); + publish(topic.c_str(), bytes, numBytes, false); + +#if !defined(ARCH_NRF52) || defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### + if (!moduleConfig.mqtt.json_enabled) + return; + // handle json topic + auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded); + if (jsonString.length() == 0) + return; + // Generate node ID from nodenum for JSON topic + std::string nodeIdForJson = nodeDB->getNodeId(); + std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson; LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); publish(topicJson.c_str(), jsonString.c_str(), false); #endif // ARCH_NRF52 NRF52_USE_JSON + } else { + LOG_INFO("MQTT not connected, queue packet"); + QueueEntry *entry; + if (mqttQueue.numFree() == 0) { + LOG_WARN("MQTT queue is full, discard oldest"); + entry = mqttQueue.dequeuePtr(0); + } else { + entry = new QueueEntry; + } + entry->topic = std::move(topic); + entry->envBytes.assign(bytes, numBytes); + if (mqttQueue.enqueue(entry, 0) == false) { + LOG_CRIT("Failed to add a message to mqttQueue!"); + abort(); + } + } } -void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex) -{ - if (mp_encrypted.via_mqtt) - return; // Don't send messages that came from MQTT back into MQTT - bool uplinkEnabled = false; - for (int i = 0; i <= 7; i++) { - if (channels.getByIndex(i).settings.uplink_enabled) - uplinkEnabled = true; - } - if (!uplinkEnabled) - return; // no channels have an uplink enabled - auto &ch = channels.getByIndex(chIndex); +void MQTT::perhapsReportToMap() { + if (!moduleConfig.mqtt.map_reporting_enabled || !moduleConfig.mqtt.map_report_settings.should_report_location || + !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly())) + return; - // mp_decoded will not be decoded when it's PKI encrypted and not directed to us - if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield - bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); - // check for the lowest bit of the data bitfield set false, and the use of one of the default keys. - if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink && - (ch.settings.psk.size < 2 || (ch.settings.psk.size == 16 && memcmp(ch.settings.psk.bytes, defaultpsk, 16)) || - (ch.settings.psk.size == 32 && memcmp(ch.settings.psk.bytes, eventpsk, 32)))) { - LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); - return; - } + // Coerce the map position precision to be within the valid range + // This removes obtusely large radius and privacy problematic ones from the map + if (map_position_precision < 12 || map_position_precision > 15) { + LOG_WARN("MQTT Map report position precision %u is out of range, using default %u", map_position_precision, default_map_position_precision); + map_position_precision = default_map_position_precision; + } - if (isConfiguredForDefaultServer && (mp_decoded.decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP || - mp_decoded.decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP)) { - LOG_DEBUG("MQTT onSend - Ignoring range test or detection sensor message on public mqtt"); - return; - } - } - // Either encrypted packet (we couldn't decrypt) is marked as pki_encrypted, or we could decode the PKI encrypted packet - bool isPKIEncrypted = mp_encrypted.pki_encrypted || mp_decoded.pki_encrypted; - // If it was to a channel, check uplink enabled, else must be pki_encrypted - if (!(ch.settings.uplink_enabled || isPKIEncrypted)) - return; - const char *channelId = isPKIEncrypted ? "PKI" : channels.getGlobalId(chIndex); + if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) + return; - LOG_DEBUG("MQTT onSend - Publish "); - const meshtastic_MeshPacket *p; - if (moduleConfig.mqtt.encryption_enabled) { - p = &mp_encrypted; - LOG_DEBUG("encrypted message"); - } else if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - p = &mp_decoded; - LOG_DEBUG("portnum %i message", mp_decoded.decoded.portnum); - } else { - LOG_DEBUG("nothing, pkt not decrypted"); - return; // Don't upload a still-encrypted PKI packet if not encryption_enabled - } - - // Generate node ID from nodenum for service envelope - std::string nodeId = nodeDB->getNodeId(); - - const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), - .channel_id = const_cast(channelId), - .gateway_id = const_cast(nodeId.c_str())}; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); - std::string topic = cryptTopic + channelId + "/" + nodeId; - - if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { - LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes); - publish(topic.c_str(), bytes, numBytes, false); - -#if !defined(ARCH_NRF52) || \ - defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### - if (!moduleConfig.mqtt.json_enabled) - return; - // handle json topic - auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded); - if (jsonString.length() == 0) - return; - // Generate node ID from nodenum for JSON topic - std::string nodeIdForJson = nodeDB->getNodeId(); - std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson; - LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); - publish(topicJson.c_str(), jsonString.c_str(), false); -#endif // ARCH_NRF52 NRF52_USE_JSON - } else { - LOG_INFO("MQTT not connected, queue packet"); - QueueEntry *entry; - if (mqttQueue.numFree() == 0) { - LOG_WARN("MQTT queue is full, discard oldest"); - entry = mqttQueue.dequeuePtr(0); - } else { - entry = new QueueEntry; - } - entry->topic = std::move(topic); - entry->envBytes.assign(bytes, numBytes); - if (mqttQueue.enqueue(entry, 0) == false) { - LOG_CRIT("Failed to add a message to mqttQueue!"); - abort(); - } - } -} - -void MQTT::perhapsReportToMap() -{ - if (!moduleConfig.mqtt.map_reporting_enabled || !moduleConfig.mqtt.map_report_settings.should_report_location || - !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly())) - return; - - // Coerce the map position precision to be within the valid range - // This removes obtusely large radius and privacy problematic ones from the map - if (map_position_precision < 12 || map_position_precision > 15) { - LOG_WARN("MQTT Map report position precision %u is out of range, using default %u", map_position_precision, - default_map_position_precision); - map_position_precision = default_map_position_precision; - } - - if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) - return; - - if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - last_report_to_map = millis(); - LOG_WARN("MQTT Map report enabled, but no position available"); - return; - } - - // Allocate MeshPacket and fill it - meshtastic_MeshPacket *mp = packetPool.allocZeroed(); - mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag; - mp->from = nodeDB->getNodeNum(); - mp->to = NODENUM_BROADCAST; - mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP; - - // Fill MapReport message - meshtastic_MapReport mapReport = meshtastic_MapReport_init_default; - memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name)); - memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name)); - mapReport.role = config.device.role; - mapReport.hw_model = owner.hw_model; - strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version)); - mapReport.region = config.lora.region; - mapReport.modem_preset = config.lora.modem_preset; - mapReport.has_default_channel = channels.hasDefaultChannel(); - mapReport.has_opted_report_location = true; - - // Set position with precision (same as in PositionModule) - mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision)); - mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision)); - mapReport.latitude_i += (1 << (31 - map_position_precision)); - mapReport.longitude_i += (1 << (31 - map_position_precision)); - - mapReport.altitude = localPosition.altitude; - mapReport.position_precision = map_position_precision; - - mapReport.num_online_local_nodes = nodeDB->getNumOnlineMeshNodes(true); - - // Encode MapReport message into the MeshPacket - mp->decoded.payload.size = - pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_MapReport_msg, &mapReport); - - // Generate node ID from nodenum for service envelope - std::string nodeId = nodeDB->getNodeId(); - - // Encode the MeshPacket into a binary ServiceEnvelope and publish - const meshtastic_ServiceEnvelope se = { - .packet = mp, - .channel_id = (char *)channels.getGlobalId(channels.getPrimaryIndex()), // Use primary channel as the channel_id - .gateway_id = const_cast(nodeId.c_str())}; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &se); - - LOG_INFO("MQTT Publish map report to %s", mapTopic.c_str()); - publish(mapTopic.c_str(), bytes, numBytes, false); - - // Release the allocated memory for MeshPacket - packetPool.release(mp); - - // Update the last report time + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { last_report_to_map = millis(); + LOG_WARN("MQTT Map report enabled, but no position available"); + return; + } + + // Allocate MeshPacket and fill it + meshtastic_MeshPacket *mp = packetPool.allocZeroed(); + mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag; + mp->from = nodeDB->getNodeNum(); + mp->to = NODENUM_BROADCAST; + mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP; + + // Fill MapReport message + meshtastic_MapReport mapReport = meshtastic_MapReport_init_default; + memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name)); + memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name)); + mapReport.role = config.device.role; + mapReport.hw_model = owner.hw_model; + strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version)); + mapReport.region = config.lora.region; + mapReport.modem_preset = config.lora.modem_preset; + mapReport.has_default_channel = channels.hasDefaultChannel(); + mapReport.has_opted_report_location = true; + + // Set position with precision (same as in PositionModule) + mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision)); + mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision)); + mapReport.latitude_i += (1 << (31 - map_position_precision)); + mapReport.longitude_i += (1 << (31 - map_position_precision)); + + mapReport.altitude = localPosition.altitude; + mapReport.position_precision = map_position_precision; + + mapReport.num_online_local_nodes = nodeDB->getNumOnlineMeshNodes(true); + + // Encode MapReport message into the MeshPacket + mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_MapReport_msg, &mapReport); + + // Generate node ID from nodenum for service envelope + std::string nodeId = nodeDB->getNodeId(); + + // Encode the MeshPacket into a binary ServiceEnvelope and publish + const meshtastic_ServiceEnvelope se = {.packet = mp, + .channel_id = + (char *)channels.getGlobalId(channels.getPrimaryIndex()), // Use primary channel as the channel_id + .gateway_id = const_cast(nodeId.c_str())}; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &se); + + LOG_INFO("MQTT Publish map report to %s", mapTopic.c_str()); + publish(mapTopic.c_str(), bytes, numBytes, false); + + // Release the allocated memory for MeshPacket + packetPool.release(mp); + + // Update the last report time + last_report_to_map = millis(); } diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index 7d5715602..6c42ad299 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -27,115 +27,114 @@ #define MAX_MQTT_QUEUE 16 /** - * Our wrapper/singleton for sending/receiving MQTT "udp" packets. This object isolates the MQTT protocol implementation from - * the two components that use it: MQTTPlugin and MQTTSimInterface. + * Our wrapper/singleton for sending/receiving MQTT "udp" packets. This object isolates the MQTT protocol + * implementation from the two components that use it: MQTTPlugin and MQTTSimInterface. */ -class MQTT : private concurrency::OSThread -{ - public: - MQTT(); +class MQTT : private concurrency::OSThread { +public: + MQTT(); - /** - * Publish a packet on the global MQTT server. - * @param mp_encrypted the encrypted packet to publish - * @param mp_decoded the decrypted packet to publish - * @param chIndex the index of the channel for this message - * - * Note: for messages we are forwarding on the mesh that we can't find the channel for (because we don't have the keys), we - * can not forward those messages to the cloud - because no way to find a global channel ID. - */ - void onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex); + /** + * Publish a packet on the global MQTT server. + * @param mp_encrypted the encrypted packet to publish + * @param mp_decoded the decrypted packet to publish + * @param chIndex the index of the channel for this message + * + * Note: for messages we are forwarding on the mesh that we can't find the channel for (because we don't have the + * keys), we can not forward those messages to the cloud - because no way to find a global channel ID. + */ + void onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex); - bool isConnectedDirectly(); + bool isConnectedDirectly(); - bool publish(const char *topic, const char *payload, bool retained); + bool publish(const char *topic, const char *payload, bool retained); - bool publish(const char *topic, const uint8_t *payload, size_t length, const bool retained); + bool publish(const char *topic, const uint8_t *payload, size_t length, const bool retained); - void onClientProxyReceive(meshtastic_MqttClientProxyMessage msg); + void onClientProxyReceive(meshtastic_MqttClientProxyMessage msg); - bool isEnabled() { return this->enabled; }; + bool isEnabled() { return this->enabled; }; - void start() { setIntervalFromNow(0); }; + void start() { setIntervalFromNow(0); }; - bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } - bool isUsingDefaultRootTopic() { return isConfiguredForDefaultRootTopic; } + bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } + bool isUsingDefaultRootTopic() { return isConfiguredForDefaultRootTopic; } - /// Validate the meshtastic_ModuleConfig_MQTTConfig. - static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); } + /// Validate the meshtastic_ModuleConfig_MQTTConfig. + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); } - protected: - struct QueueEntry { - std::string topic; - std::basic_string envBytes; // binary/pb_encode_to_bytes ServiceEnvelope - }; - PointerQueue mqttQueue; +protected: + struct QueueEntry { + std::string topic; + std::basic_string envBytes; // binary/pb_encode_to_bytes ServiceEnvelope + }; + PointerQueue mqttQueue; - int reconnectCount = 0; - bool isConfiguredForDefaultServer = true; - bool isConfiguredForDefaultRootTopic = true; + int reconnectCount = 0; + bool isConfiguredForDefaultServer = true; + bool isConfiguredForDefaultRootTopic = true; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; #ifndef PIO_UNIT_TESTING - private: +private: #endif #if HAS_WIFI - using MQTTClient = WiFiClient; + using MQTTClient = WiFiClient; #if __has_include() - using MQTTClientTLS = WiFiClientSecure; + using MQTTClientTLS = WiFiClientSecure; #define MQTT_SUPPORTS_TLS 1 #endif #elif HAS_ETHERNET - using MQTTClient = EthernetClient; + using MQTTClient = EthernetClient; #else - using MQTTClient = void; + using MQTTClient = void; #endif #if HAS_NETWORKING - std::unique_ptr mqttClient; + std::unique_ptr mqttClient; #if MQTT_SUPPORTS_TLS - MQTTClientTLS mqttClientTLS; + MQTTClientTLS mqttClientTLS; #endif - PubSubClient pubSub; - explicit MQTT(std::unique_ptr mqttClient); + PubSubClient pubSub; + explicit MQTT(std::unique_ptr mqttClient); #endif - std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID - std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID - std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages + std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID + std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID + std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages - // For map reporting (only applies when enabled) - const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m - uint32_t last_report_to_map = 0; - uint32_t map_position_precision = default_map_position_precision; - uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000; + // For map reporting (only applies when enabled) + const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m + uint32_t last_report_to_map = 0; + uint32_t map_position_precision = default_map_position_precision; + uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000; - /** Attempt to connect to server if necessary - */ - void reconnect(); + /** Attempt to connect to server if necessary + */ + void reconnect(); - /** Tell the server what subscriptions we want (based on channels.downlink_enabled) - */ - void sendSubscriptions(); + /** Tell the server what subscriptions we want (based on channels.downlink_enabled) + */ + void sendSubscriptions(); - /// Callback for direct mqtt subscription messages - static void mqttCallback(char *topic, byte *payload, unsigned int length); + /// Callback for direct mqtt subscription messages + static void mqttCallback(char *topic, byte *payload, unsigned int length); - static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client); + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client); - /// Called when a new publish arrives from the MQTT server - void onReceive(char *topic, byte *payload, size_t length); + /// Called when a new publish arrives from the MQTT server + void onReceive(char *topic, byte *payload, size_t length); - void publishQueuedMessages(); + void publishQueuedMessages(); - void publishNodeInfo(); + void publishNodeInfo(); - // Check if we should report unencrypted information about our node for consumption by a map - void perhapsReportToMap(); + // Check if we should report unencrypted information about our node for consumption by a map + void perhapsReportToMap(); - /// Return 0 if sleep is okay, veto sleep if we are connected to pubsub server - // int preflightSleepCb(void *unused = NULL) { return pubSub.connected() ? 1 : 0; } + /// Return 0 if sleep is okay, veto sleep if we are connected to pubsub server + // int preflightSleepCb(void *unused = NULL) { return pubSub.connected() ? 1 : 0; } }; void mqttInit(); diff --git a/src/mqtt/ServiceEnvelope.cpp b/src/mqtt/ServiceEnvelope.cpp index ee55f22f6..1bfccc0ad 100644 --- a/src/mqtt/ServiceEnvelope.cpp +++ b/src/mqtt/ServiceEnvelope.cpp @@ -4,20 +4,16 @@ DecodedServiceEnvelope::DecodedServiceEnvelope(const uint8_t *payload, size_t length) : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_default), - validDecode(pb_decode_from_bytes(payload, length, &meshtastic_ServiceEnvelope_msg, this)) -{ -} + validDecode(pb_decode_from_bytes(payload, length, &meshtastic_ServiceEnvelope_msg, this)) {} DecodedServiceEnvelope::DecodedServiceEnvelope(DecodedServiceEnvelope &&other) - : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_zero), validDecode(other.validDecode) -{ - std::swap(packet, other.packet); - std::swap(channel_id, other.channel_id); - std::swap(gateway_id, other.gateway_id); + : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_zero), validDecode(other.validDecode) { + std::swap(packet, other.packet); + std::swap(channel_id, other.channel_id); + std::swap(gateway_id, other.gateway_id); } -DecodedServiceEnvelope::~DecodedServiceEnvelope() -{ - if (validDecode) - pb_release(&meshtastic_ServiceEnvelope_msg, this); +DecodedServiceEnvelope::~DecodedServiceEnvelope() { + if (validDecode) + pb_release(&meshtastic_ServiceEnvelope_msg, this); } \ No newline at end of file diff --git a/src/mqtt/ServiceEnvelope.h b/src/mqtt/ServiceEnvelope.h index 6ab0695db..d403de254 100644 --- a/src/mqtt/ServiceEnvelope.h +++ b/src/mqtt/ServiceEnvelope.h @@ -4,10 +4,10 @@ // meshtastic_ServiceEnvelope that automatically releases dynamically allocated memory when it goes out of scope. struct DecodedServiceEnvelope : public meshtastic_ServiceEnvelope { - DecodedServiceEnvelope(const uint8_t *payload, size_t length); - DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete; - DecodedServiceEnvelope(DecodedServiceEnvelope &&); - ~DecodedServiceEnvelope(); - // Clients must check that this is true before using. - const bool validDecode; + DecodedServiceEnvelope(const uint8_t *payload, size_t length); + DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete; + DecodedServiceEnvelope(DecodedServiceEnvelope &&); + ~DecodedServiceEnvelope(); + // Clients must check that this is true before using. + const bool validDecode; }; \ No newline at end of file diff --git a/src/network-stubs.cpp b/src/network-stubs.cpp index 1737579d5..7073f2a5c 100644 --- a/src/network-stubs.cpp +++ b/src/network-stubs.cpp @@ -2,30 +2,18 @@ #if (HAS_WIFI == 0) -bool initWifi() -{ - return false; -} +bool initWifi() { return false; } void deinitWifi() {} -bool isWifiAvailable() -{ - return false; -} +bool isWifiAvailable() { return false; } #endif #if (HAS_ETHERNET == 0) -bool initEthernet() -{ - return false; -} +bool initEthernet() { return false; } -bool isEthernetAvailable() -{ - return false; -} +bool isEthernetAvailable() { return false; } #endif diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 3b98eca3d..743c57396 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -28,8 +28,7 @@ #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) -namespace -{ +namespace { constexpr uint16_t kPreferredBleMtu = 517; constexpr uint16_t kPreferredBleTxOctets = 251; constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8; @@ -52,339 +51,331 @@ NimBLEServer *bleServer; static bool passkeyShowing; static std::atomic nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection" -class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread -{ +class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { + /* + CAUTION: There's a lot going on here and lots of room to break things. + + This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the + onRead and onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI). + + The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't + have to know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking + OSThread system, where locking isn't something that anyone has to worry about too much! :) + + We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and + handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry + about being run concurrently, which would make everything else much much much more complicated. + + PHONE -> RADIO: + - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue. + - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, + and calls handleToRadio **in main task**. + + RADIO -> PHONE: + - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. + (unless there's already a packet waiting in toPhoneQueue) + - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main + task** to get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the + onReadCallbackIsWaitingForData flag. + - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds + toPhoneMutex, pops the packet from toPhoneQueue, and returns it to NimBLE. + + MUTEXES: + - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize + - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize + + ATOMICS: + - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or + onDisconnect). + - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect). + - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue + (or onDisconnect). + + PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from + getFromRadio. + + BLE CONNECTION PARAMS: + - During config, we request a high-throughput, low-latency BLE connection for speed. + - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life. + + MEMORY MANAGEMENT: + - We keep packets on the stack and do not allocate heap. + - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks. + - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory + management. + + NOTIFY IS BROKEN: + - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards + compatible. + + ZERO-SIZE READS: + - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead + until we have data. + - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do + reads until they get a 0-byte response. + + CROSS-TASK WAKEUP: + - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data, + - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping. + - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause. + */ + +public: + BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { api_type = TYPE_BLE; } + + /* Packets from phone (BLE onWrite callback) */ + std::mutex fromPhoneMutex; + std::atomic fromPhoneQueueSize{0}; + // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. + std::array fromPhoneQueue{}; + + /* Packets to phone (BLE onRead callback) */ + std::mutex toPhoneMutex; + std::atomic toPhoneQueueSize{0}; + // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. + std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{}; + std::array toPhoneQueueByteSizes{}; + // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our + // main task's runOnce. It's only set by onRead, and only cleared by runOnce. + std::atomic onReadCallbackIsWaitingForData{false}; + + /* Statistics/logging helpers */ + std::atomic readCount{0}; + std::atomic notifyCount{0}; + std::atomic writeCount{0}; + +protected: + virtual int32_t runOnce() override { + while (runOnceHasWorkToDo()) { + /* + PROCESS fromPhoneQueue BEFORE toPhoneQueue: + + In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the + same time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally + ok to service either the reads or writes first. + + However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and + they expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS + back to another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config + only.) + + So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous" + write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this + is what the client wants! + */ + + // PHONE -> RADIO: + runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio + + // RADIO -> PHONE: + runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead + } + + // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback + return INT32_MAX; + } + + virtual void onConfigStart() override { + LOG_INFO("BLE onConfigStart"); + + // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) + if (bleServer && isConnected()) { + uint16_t conn_handle = nimbleBluetoothConnHandle.load(); + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + requestHighThroughputConnection(conn_handle); + } + } + } + + virtual void onConfigComplete() override { + LOG_INFO("BLE onConfigComplete"); + + // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete + if (bleServer && isConnected()) { + uint16_t conn_handle = nimbleBluetoothConnHandle.load(); + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + requestLowerPowerConnection(conn_handle); + } + } + } + + bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); } + + bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); } + + bool runOnceToPhoneCanPreloadNextPacket() { /* - CAUTION: There's a lot going on here and lots of room to break things. - - This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and - onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI). - - The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to - know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where - locking isn't something that anyone has to worry about too much! :) - - We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and - handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about - being run concurrently, which would make everything else much much much more complicated. - - PHONE -> RADIO: - - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue. - - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls - handleToRadio **in main task**. - - RADIO -> PHONE: - - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless - there's already a packet waiting in toPhoneQueue) - - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to - get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the - onReadCallbackIsWaitingForData flag. - - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex, - pops the packet from toPhoneQueue, and returns it to NimBLE. - - MUTEXES: - - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize - - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize - - ATOMICS: - - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect). - - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect). - - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or - onDisconnect). - - PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio. - - BLE CONNECTION PARAMS: - - During config, we request a high-throughput, low-latency BLE connection for speed. - - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life. - - MEMORY MANAGEMENT: - - We keep packets on the stack and do not allocate heap. - - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks. - - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management. - - NOTIFY IS BROKEN: - - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible. - - ZERO-SIZE READS: - - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we - have data. - - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads - until they get a 0-byte response. - - CROSS-TASK WAKEUP: - - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data, - - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping. - - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause. - */ - - public: - BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { api_type = TYPE_BLE; } - - /* Packets from phone (BLE onWrite callback) */ - std::mutex fromPhoneMutex; - std::atomic fromPhoneQueueSize{0}; - // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. - std::array fromPhoneQueue{}; - - /* Packets to phone (BLE onRead callback) */ - std::mutex toPhoneMutex; - std::atomic toPhoneQueueSize{0}; - // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. - std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{}; - std::array toPhoneQueueByteSizes{}; - // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main - // task's runOnce. It's only set by onRead, and only cleared by runOnce. - std::atomic onReadCallbackIsWaitingForData{false}; - - /* Statistics/logging helpers */ - std::atomic readCount{0}; - std::atomic notifyCount{0}; - std::atomic writeCount{0}; - - protected: - virtual int32_t runOnce() override - { - while (runOnceHasWorkToDo()) { - /* - PROCESS fromPhoneQueue BEFORE toPhoneQueue: - - In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same - time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to - service either the reads or writes first. - - However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they - expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to - another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.) - - So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous" - write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is - what the client wants! - */ - - // PHONE -> RADIO: - runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio - - // RADIO -> PHONE: - runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead - } - - // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback - return INT32_MAX; - } - - virtual void onConfigStart() override - { - LOG_INFO("BLE onConfigStart"); - - // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) - if (bleServer && isConnected()) { - uint16_t conn_handle = nimbleBluetoothConnHandle.load(); - if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { - requestHighThroughputConnection(conn_handle); - } - } - } - - virtual void onConfigComplete() override - { - LOG_INFO("BLE onConfigComplete"); - - // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete - if (bleServer && isConnected()) { - uint16_t conn_handle = nimbleBluetoothConnHandle.load(); - if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { - requestLowerPowerConnection(conn_handle); - } - } - } - - bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); } - - bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); } - - bool runOnceToPhoneCanPreloadNextPacket() - { - /* - * PRELOADING getFromRadio RESPONSES: - * - * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call - * getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet - * forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where - * the client might disconnect before completing the read. - * - * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into - * toPhoneQueue because the client will just reconnect after a disconnect, losing nothing. - */ - - if (!isConnected()) { - return false; - } else if (isSendingPackets()) { - // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio. - return false; - } else { - // In other states, we can preload as long as there's space in the toPhoneQueue. - return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE; - } - } - - void runOnceHandleToPhoneQueue() - { - // Stack buffer for getFromRadio packet - uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; - size_t numBytes = 0; - - if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) { - numBytes = getFromRadio(fromRadioBytes); - - if (numBytes == 0) { - /* - Client expected a read, but we have nothing to send. - - In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond - notifies regularly, to make sure they have nothing else to read. - - In other states, this is fine **so long as we've already processed pending onWrites first**, because the client - may requesting wantConfig and immediately doing a read. - */ - } else { - // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. - if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { - // Note: the comparison above is safe without a mutex because we are the only method that *increases* - // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.) - - { // scope for toPhoneMutex mutex - std::lock_guard guard(toPhoneMutex); - size_t storeAtIndex = toPhoneQueueSize.load(); - memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes); - toPhoneQueueByteSizes[storeAtIndex] = numBytes; - toPhoneQueueSize++; - } -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes, - toPhoneQueueSize.load()); -#endif - } else { - // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full! - LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes); - } - } - - // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. - onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push - } - } - - bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } - - void runOnceHandleFromPhoneQueue() - { - // Handle packets we received from onWrite from the phone. - if (fromPhoneQueueSize > 0) { - // Note: the comparison above is safe without a mutex because we are the only method that *decreases* - // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.) - - LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load()); - - // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop. - NimBLEAttValue val; - { // scope for fromPhoneMutex mutex - std::lock_guard guard(fromPhoneMutex); - val = fromPhoneQueue[0]; - - // Shift the rest of the queue down - for (uint8_t i = 1; i < fromPhoneQueueSize; i++) { - fromPhoneQueue[i - 1] = fromPhoneQueue[i]; - } - - // Safe decrement due to onDisconnect - if (fromPhoneQueueSize > 0) - fromPhoneQueueSize--; - } - - handleToRadio(val.data(), val.length()); - } - } - - /** - * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + * PRELOADING getFromRadio RESPONSES: + * + * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we + * call getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that + * packet forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time + * window where the client might disconnect before completing the read. + * + * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload + * packets into toPhoneQueue because the client will just reconnect after a disconnect, losing nothing. */ - virtual void onNowHasData(uint32_t fromRadioNum) - { - PhoneAPI::onNowHasData(fromRadioNum); - int currentNotifyCount = notifyCount.fetch_add(1); + if (!isConnected()) { + return false; + } else if (isSendingPackets()) { + // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio. + return false; + } else { + // In other states, we can preload as long as there's space in the toPhoneQueue. + return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE; + } + } - uint8_t cc = bleServer->getConnectedCount(); + void runOnceHandleToPhoneQueue() { + // Stack buffer for getFromRadio packet + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; + size_t numBytes = 0; + + if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) { + numBytes = getFromRadio(fromRadioBytes); + + if (numBytes == 0) { + /* + Client expected a read, but we have nothing to send. + + In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond + notifies regularly, to make sure they have nothing else to read. + + In other states, this is fine **so long as we've already processed pending onWrites first**, because the + client may requesting wantConfig and immediately doing a read. + */ + } else { + // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. + if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { + // Note: the comparison above is safe without a mutex because we are the only method that *increases* + // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.) + + { // scope for toPhoneMutex mutex + std::lock_guard guard(toPhoneMutex); + size_t storeAtIndex = toPhoneQueueSize.load(); + memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes); + toPhoneQueueByteSizes[storeAtIndex] = numBytes; + toPhoneQueueSize++; + } +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes, toPhoneQueueSize.load()); +#endif + } else { + // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full! + LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes); + } + } + + // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. + onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push + } + } + + bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } + + void runOnceHandleFromPhoneQueue() { + // Handle packets we received from onWrite from the phone. + if (fromPhoneQueueSize > 0) { + // Note: the comparison above is safe without a mutex because we are the only method that *decreases* + // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.) + + LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load()); + + // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop. + NimBLEAttValue val; + { // scope for fromPhoneMutex mutex + std::lock_guard guard(fromPhoneMutex); + val = fromPhoneQueue[0]; + + // Shift the rest of the queue down + for (uint8_t i = 1; i < fromPhoneQueueSize; i++) { + fromPhoneQueue[i - 1] = fromPhoneQueue[i]; + } + + // Safe decrement due to onDisconnect + if (fromPhoneQueueSize > 0) + fromPhoneQueueSize--; + } + + handleToRadio(val.data(), val.length()); + } + } + + /** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + virtual void onNowHasData(uint32_t fromRadioNum) { + PhoneAPI::onNowHasData(fromRadioNum); + + int currentNotifyCount = notifyCount.fetch_add(1); + + uint8_t cc = bleServer->getConnectedCount(); #ifdef DEBUG_NIMBLE_NOTIFY - // This logging slows things down when there are lots of packets going to the phone, like initial connection: - LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); + // This logging slows things down when there are lots of packets going to the phone, like initial connection: + LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); #endif - uint8_t val[4]; - put_le32(val, fromRadioNum); + uint8_t val[4]; + put_le32(val, fromRadioNum); - fromNumCharacteristic->setValue(val, sizeof(val)); + fromNumCharacteristic->setValue(val, sizeof(val)); #ifdef NIMBLE_TWO - // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be - // notify(). - fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); + // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be + // notify(). + fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); #else - fromNumCharacteristic->notify(); + fromNumCharacteristic->notify(); #endif - } + } - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } - void requestHighThroughputConnection(uint16_t conn_handle) - { - /* Request a lower-latency, higher-throughput BLE connection. + void requestHighThroughputConnection(uint16_t conn_handle) { + /* Request a lower-latency, higher-throughput BLE connection. - This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to - a slower mode. + This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then + switch to a slower mode. - See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS - constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple - recommendations.) + See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS + constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the + Apple recommendations.) - Selected settings: - minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client - supports it.) - maxInterval (units of 1.25ms): 15ms = 12 - latency: 0 (don't allow peripheral to skip any connection events) - timeout (units of 10ms): 6 seconds = 600 (supervision timeout) + Selected settings: + minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the + client supports it.) maxInterval (units of 1.25ms): 15ms = 12 latency: 0 (don't allow peripheral to skip any + connection events) timeout (units of 10ms): 6 seconds = 600 (supervision timeout) - These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at - setup. Not worth adjusting much. - */ - LOG_INFO("BLE requestHighThroughputConnection"); - bleServer->updateConnParams(conn_handle, 6, 12, 0, 600); - } + These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds + at setup. Not worth adjusting much. + */ + LOG_INFO("BLE requestHighThroughputConnection"); + bleServer->updateConnParams(conn_handle, 6, 12, 0, 600); + } - void requestLowerPowerConnection(uint16_t conn_handle) - { - /* Request a lower power consumption (but higher latency, lower throughput) BLE connection. + void requestLowerPowerConnection(uint16_t conn_handle) { + /* Request a lower power consumption (but higher latency, lower throughput) BLE connection. - This is suitable for steady-state operation after initial setup is complete. + This is suitable for steady-state operation after initial setup is complete. - See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS - constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple - recommendations.) + See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS + constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the + Apple recommendations.) - Selected settings: - minInterval (units of 1.25ms): 30ms = 24 - maxInterval (units of 1.25ms): 50ms = 40 - latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power) - timeout (units of 10ms): 6 seconds = 600 (supervision timeout) + Selected settings: + minInterval (units of 1.25ms): 30ms = 24 + maxInterval (units of 1.25ms): 50ms = 40 + latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power) + timeout (units of 10ms): 6 seconds = 600 (supervision timeout) - There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets - per second. - */ - LOG_INFO("BLE requestLowerPowerConnection"); - bleServer->updateConnParams(conn_handle, 24, 40, 2, 600); - } + There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 + packets per second. + */ + LOG_INFO("BLE requestLowerPowerConnection"); + bleServer->updateConnParams(conn_handle, 24, 40, 2, 600); + } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; @@ -395,597 +386,569 @@ static BluetoothPhoneAPI *bluetoothPhoneAPI; // Last ToRadio value received from the phone static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; -class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks -{ +class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { #ifdef NIMBLE_TWO - virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) + virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) #else - virtual void onWrite(NimBLECharacteristic *pCharacteristic) + virtual void onWrite(NimBLECharacteristic *pCharacteristic) #endif - { - // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. - // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls. + { + // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's + // runOnce. Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent + // onWrite calls. - int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1); + int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1); #ifdef DEBUG_NIMBLE_ON_WRITE_TIMING - int startMillis = millis(); - LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis); + int startMillis = millis(); + LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis); #endif - auto val = pCharacteristic->getValue(); + auto val = pCharacteristic->getValue(); - if (memcmp(lastToRadio, val.data(), val.length()) != 0) { - if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) { - // Note: the comparison above is safe without a mutex because we are the only method that *increases* - // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.) - memcpy(lastToRadio, val.data(), val.length()); + if (memcmp(lastToRadio, val.data(), val.length()) != 0) { + if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) { + // Note: the comparison above is safe without a mutex because we are the only method that *increases* + // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.) + memcpy(lastToRadio, val.data(), val.length()); - { // scope for fromPhoneMutex mutex - // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible. - std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); - bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val; - bluetoothPhoneAPI->fromPhoneQueueSize++; - } + { // scope for fromPhoneMutex mutex + // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible. + std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); + bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val; + bluetoothPhoneAPI->fromPhoneQueueSize++; + } - // After releasing the mutex, schedule immediate processing of the new packet. - bluetoothPhoneAPI->setIntervalFromNow(0); - concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + // After releasing the mutex, schedule immediate processing of the new packet. + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping #ifdef DEBUG_NIMBLE_ON_WRITE_TIMING - int finishMillis = millis(); - LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount, - finishMillis - startMillis, val.length()); -#endif - } else { - LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length()); - } - } else { - LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length()); - } - } -}; - -class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks -{ -#ifdef NIMBLE_TWO - virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) -#else - virtual void onRead(NimBLECharacteristic *pCharacteristic) -#endif - { - // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. - - int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); - int tries = 0; - int startMillis = millis(); - -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis); -#endif - - // Is there a packet ready to go, or do we have to ask the main task to get one for us? - if (bluetoothPhoneAPI->toPhoneQueueSize > 0) { - // Note: the comparison above is safe without a mutex because we are the only method that *decreases* - // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.) - - // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData. -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount); -#endif - } else { - // Tell the main task that we'd like a packet. - bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true; - - // Wait for the main task to produce a packet for us, up to about 20 seconds. - // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer - // doing various setup tasks. - while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) { - // Schedule the main task runOnce to run ASAP. - bluetoothPhoneAPI->setIntervalFromNow(0); - concurrency::mainDelay.interrupt(); // wake up main loop if sleeping - - if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) { - // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran - // already -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount, - millis() - startMillis, tries); -#endif - break; - } - - // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back. - // No harm in polling pretty frequently. - delay(tries < 20 ? 1 : 5); - tries++; - - if (tries == 4000) { - LOG_WARN( - "BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response", - currentReadCount, millis() - startMillis, tries); - } - } - } - - // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. - uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet - size_t numBytes = 0; - { // scope for toPhoneMutex mutex - std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); - size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load(); - if (toPhoneQueueSize > 0) { - // Copy from the front of the toPhoneQueue - memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]); - numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0]; - - // Shift the rest of the queue down - for (uint8_t i = 1; i < toPhoneQueueSize; i++) { - memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(), - bluetoothPhoneAPI->toPhoneQueueByteSizes[i]); - // The above line is similar to: - // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i] - // but is usually faster because it doesn't have to copy all the trailing bytes beyond - // toPhoneQueueByteSizes[i]. - // - // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic - // memory allocations and frees across FreeRTOS tasks. - - bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i]; - } - - // Safe decrement due to onDisconnect - if (bluetoothPhoneAPI->toPhoneQueueSize > 0) - bluetoothPhoneAPI->toPhoneQueueSize--; - } else { - // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0. - } - } - -#ifdef DEBUG_NIMBLE_ON_READ_TIMING int finishMillis = millis(); - LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount, - finishMillis - startMillis, tries, numBytes); + LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount, finishMillis - startMillis, val.length()); #endif - - pCharacteristic->setValue(fromRadioBytes, numBytes); - - // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue. - if (numBytes != 0) { - bluetoothPhoneAPI->setIntervalFromNow(0); - concurrency::mainDelay.interrupt(); // wake up main loop if sleeping - } + } else { + LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length()); + } + } else { + LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length()); } + } }; -class NimbleBluetoothServerCallback : public NimBLEServerCallbacks -{ +class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { #ifdef NIMBLE_TWO - public: - NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } - - private: - NimbleBluetooth *ble; - - virtual uint32_t onPassKeyDisplay() + virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) #else - virtual uint32_t onPassKeyRequest() + virtual void onRead(NimBLECharacteristic *pCharacteristic) #endif - { - uint32_t passkey = config.bluetooth.fixed_pin; + { + // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's + // runOnce. - if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { - LOG_INFO("Use random passkey"); - // This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits - passkey = random(100000, 999999); + int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); + int tries = 0; + int startMillis = millis(); + +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis); +#endif + + // Is there a packet ready to go, or do we have to ask the main task to get one for us? + if (bluetoothPhoneAPI->toPhoneQueueSize > 0) { + // Note: the comparison above is safe without a mutex because we are the only method that *decreases* + // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.) + + // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData. +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount); +#endif + } else { + // Tell the main task that we'd like a packet. + bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true; + + // Wait for the main task to produce a packet for us, up to about 20 seconds. + // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for + // longer doing various setup tasks. + while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) { + // Schedule the main task runOnce to run ASAP. + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + + if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) { + // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran + // already +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount, millis() - startMillis, tries); +#endif + break; } - LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); - powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); - bluetoothStatus->updateStatus(&newStatus); + // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back. + // No harm in polling pretty frequently. + delay(tries < 20 ? 1 : 5); + tries++; + + if (tries == 4000) { + LOG_WARN("BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response", currentReadCount, + millis() - startMillis, tries); + } + } + } + + // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet + size_t numBytes = 0; + { // scope for toPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); + size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load(); + if (toPhoneQueueSize > 0) { + // Copy from the front of the toPhoneQueue + memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]); + numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0]; + + // Shift the rest of the queue down + for (uint8_t i = 1; i < toPhoneQueueSize; i++) { + memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(), + bluetoothPhoneAPI->toPhoneQueueByteSizes[i]); + // The above line is similar to: + // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i] + // but is usually faster because it doesn't have to copy all the trailing bytes beyond + // toPhoneQueueByteSizes[i]. + // + // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic + // memory allocations and frees across FreeRTOS tasks. + + bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i]; + } + + // Safe decrement due to onDisconnect + if (bluetoothPhoneAPI->toPhoneQueueSize > 0) + bluetoothPhoneAPI->toPhoneQueueSize--; + } else { + // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0. + } + } + +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + int finishMillis = millis(); + LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount, finishMillis - startMillis, tries, + numBytes); +#endif + + pCharacteristic->setValue(fromRadioBytes, numBytes); + + // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue. + if (numBytes != 0) { + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + } + } +}; + +class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { +#ifdef NIMBLE_TWO +public: + NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } + +private: + NimbleBluetooth *ble; + + virtual uint32_t onPassKeyDisplay() +#else + virtual uint32_t onPassKeyRequest() +#endif + { + uint32_t passkey = config.bluetooth.fixed_pin; + + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { + LOG_INFO("Use random passkey"); + // This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits + passkey = random(100000, 999999); + } + LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); + + powerFSM.trigger(EVENT_BLUETOOTH_PAIR); + meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); + bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (screen) { - screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", passkey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 12; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", passkey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); #if !defined(M5STACK_UNITC6L) - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); #endif - display->setFont(FONT_LARGE); - char pin[8]; - snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + char pin[8]; + snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - char deviceName[64]; - snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); - } + display->setFont(FONT_SMALL); + char deviceName[64]; + snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif - passkeyShowing = true; + passkeyShowing = true; - return passkey; + return passkey; + } + +#ifdef NIMBLE_TWO + virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) +#else + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) +#endif + { + LOG_INFO("BLE authentication complete"); + + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); + + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + if (passkeyShowing) { + passkeyShowing = false; + if (screen) + screen->endAlert(); } + // Store the connection handle for future use #ifdef NIMBLE_TWO - virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) + nimbleBluetoothConnHandle = connInfo.getConnHandle(); #else - virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) + nimbleBluetoothConnHandle = desc->conn_handle; #endif - { - LOG_INFO("BLE authentication complete"); - - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newStatus); - - // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (passkeyShowing) { - passkeyShowing = false; - if (screen) - screen->endAlert(); - } - - // Store the connection handle for future use -#ifdef NIMBLE_TWO - nimbleBluetoothConnHandle = connInfo.getConnHandle(); -#else - nimbleBluetoothConnHandle = desc->conn_handle; -#endif - } + } #ifdef NIMBLE_TWO - virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) - { - LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); + virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) { + LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); - const uint16_t connHandle = connInfo.getConnHandle(); + const uint16_t connHandle = connInfo.getConnHandle(); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) - int phyResult = - ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY); - if (phyResult == 0) { - LOG_INFO("BLE conn %u requested 2M PHY", connHandle); - } else { - LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult); - } -#endif - - int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs); - if (dataLenResult == 0) { - LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets); - } else { - LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult); - } - - LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); - pServer->updateConnParams(connHandle, 6, 12, 0, 200); + int phyResult = ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY); + if (phyResult == 0) { + LOG_INFO("BLE conn %u requested 2M PHY", connHandle); + } else { + LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult); } #endif -#ifdef NIMBLE_TWO - virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) - { - LOG_INFO("BLE disconnect reason: %d", reason); -#else - virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) - { - LOG_INFO("BLE disconnect"); -#endif -#ifdef NIMBLE_TWO - if (ble->isDeInit) - return; -#endif - - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newStatus); - - if (bluetoothPhoneAPI) { - bluetoothPhoneAPI->close(); - - { // scope for fromPhoneMutex mutex - std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); - bluetoothPhoneAPI->fromPhoneQueueSize = 0; - } - - bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; - { // scope for toPhoneMutex mutex - std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); - bluetoothPhoneAPI->toPhoneQueueSize = 0; - } - - bluetoothPhoneAPI->readCount = 0; - bluetoothPhoneAPI->notifyCount = 0; - bluetoothPhoneAPI->writeCount = 0; - } - - // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection - memset(lastToRadio, 0, sizeof(lastToRadio)); - - nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection" - -#ifdef NIMBLE_TWO - // Restart Advertising - ble->startAdvertising(); -#else - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - if (!pAdvertising->start(0)) { - if (pAdvertising->isAdvertising()) { - LOG_DEBUG("BLE advertising already running"); - } else { - LOG_ERROR("BLE failed to restart advertising"); - } - } -#endif + int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs); + if (dataLenResult == 0) { + LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets); + } else { + LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult); } + + LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); + pServer->updateConnParams(connHandle, 6, 12, 0, 200); + } +#endif + +#ifdef NIMBLE_TWO + virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) { + LOG_INFO("BLE disconnect reason: %d", reason); +#else + virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) { + LOG_INFO("BLE disconnect"); +#endif +#ifdef NIMBLE_TWO + if (ble->isDeInit) + return; +#endif + + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); + + if (bluetoothPhoneAPI) { + bluetoothPhoneAPI->close(); + + { // scope for fromPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); + bluetoothPhoneAPI->fromPhoneQueueSize = 0; + } + + bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; + { // scope for toPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); + bluetoothPhoneAPI->toPhoneQueueSize = 0; + } + + bluetoothPhoneAPI->readCount = 0; + bluetoothPhoneAPI->notifyCount = 0; + bluetoothPhoneAPI->writeCount = 0; + } + + // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection + memset(lastToRadio, 0, sizeof(lastToRadio)); + + nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection" + +#ifdef NIMBLE_TWO + // Restart Advertising + ble->startAdvertising(); +#else + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + if (!pAdvertising->start(0)) { + if (pAdvertising->isAdvertising()) { + LOG_DEBUG("BLE advertising already running"); + } else { + LOG_ERROR("BLE failed to restart advertising"); + } + } +#endif + } }; static NimbleBluetoothToRadioCallback *toRadioCallbacks; static NimbleBluetoothFromRadioCallback *fromRadioCallbacks; -void NimbleBluetooth::shutdown() -{ - // No measurable power saving for ESP32 during light-sleep(?) +void NimbleBluetooth::shutdown() { + // No measurable power saving for ESP32 during light-sleep(?) #ifndef ARCH_ESP32 - // Shutdown bluetooth for minimum power draw - LOG_INFO("Disable bluetooth"); - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->reset(); - pAdvertising->stop(); + // Shutdown bluetooth for minimum power draw + LOG_INFO("Disable bluetooth"); + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->reset(); + pAdvertising->stop(); #endif } // Proper shutdown for ESP32. Needs reboot to reverse. -void NimbleBluetooth::deinit() -{ +void NimbleBluetooth::deinit() { #ifdef ARCH_ESP32 - LOG_INFO("Disable bluetooth until reboot"); - isDeInit = true; + LOG_INFO("Disable bluetooth until reboot"); + isDeInit = true; #ifdef BLE_LED #ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); + digitalWrite(BLE_LED, HIGH); #else - digitalWrite(BLE_LED, LOW); + digitalWrite(BLE_LED, LOW); #endif #endif #ifndef NIMBLE_TWO - NimBLEDevice::deinit(); + NimBLEDevice::deinit(); #endif #endif } // Has initial setup been completed -bool NimbleBluetooth::isActive() -{ - return bleServer; -} +bool NimbleBluetooth::isActive() { return bleServer; } -bool NimbleBluetooth::isConnected() -{ - return bleServer->getConnectedCount() > 0; -} +bool NimbleBluetooth::isConnected() { return bleServer->getConnectedCount() > 0; } -int NimbleBluetooth::getRssi() -{ +int NimbleBluetooth::getRssi() { #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) - if (!bleServer || !isConnected()) { - return 0; // No active BLE connection + if (!bleServer || !isConnected()) { + return 0; // No active BLE connection + } + + uint16_t connHandle = nimbleBluetoothConnHandle.load(); + + if (connHandle == BLE_HS_CONN_HANDLE_NONE) { + const auto peers = bleServer->getPeerDevices(); + if (!peers.empty()) { + connHandle = peers.front(); + nimbleBluetoothConnHandle = connHandle; } + } - uint16_t connHandle = nimbleBluetoothConnHandle.load(); + if (connHandle == BLE_HS_CONN_HANDLE_NONE) { + return 0; // Connection handle not available yet + } - if (connHandle == BLE_HS_CONN_HANDLE_NONE) { - const auto peers = bleServer->getPeerDevices(); - if (!peers.empty()) { - connHandle = peers.front(); - nimbleBluetoothConnHandle = connHandle; - } - } + int8_t rssi = 0; + const int rc = ble_gap_conn_rssi(connHandle, &rssi); - if (connHandle == BLE_HS_CONN_HANDLE_NONE) { - return 0; // Connection handle not available yet - } - - int8_t rssi = 0; - const int rc = ble_gap_conn_rssi(connHandle, &rssi); - - if (rc == 0) { - return rssi; - } - LOG_DEBUG("BLE RSSI read failed, rc=%d", rc); + if (rc == 0) { + return rssi; + } + LOG_DEBUG("BLE RSSI read failed, rc=%d", rc); #endif - return 0; + return 0; } -void NimbleBluetooth::setup() -{ - // Uncomment for testing - // NimbleBluetooth::clearBonds(); +void NimbleBluetooth::setup() { + // Uncomment for testing + // NimbleBluetooth::clearBonds(); - LOG_INFO("Init the NimBLE bluetooth module"); + LOG_INFO("Init the NimBLE bluetooth module"); - NimBLEDevice::init(getDeviceName()); - NimBLEDevice::setPower(ESP_PWR_LVL_P9); + NimBLEDevice::init(getDeviceName()); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) - int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); - if (mtuResult == 0) { - LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu); - } else { - LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult); - } + int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); + if (mtuResult == 0) { + LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu); + } else { + LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult); + } - int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK); - if (phyResult == 0) { - LOG_INFO("BLE default PHY preference set to 2M"); - } else { - LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult); - } + int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK); + if (phyResult == 0) { + LOG_INFO("BLE default PHY preference set to 2M"); + } else { + LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult); + } - int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs); - if (dataLenResult == 0) { - LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets); - } else { - LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs, - dataLenResult); - } + int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs); + if (dataLenResult == 0) { + LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets); + } else { + LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs, dataLenResult); + } #endif - if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC); - NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); - NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); - NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); - } - bleServer = NimBLEDevice::createServer(); + if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC); + NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); + NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); + NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); + } + bleServer = NimBLEDevice::createServer(); #ifdef NIMBLE_TWO - NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); #else - NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); #endif - bleServer->setCallbacks(serverCallbacks, true); - setupService(); - startAdvertising(); + bleServer->setCallbacks(serverCallbacks, true); + setupService(); + startAdvertising(); } -void NimbleBluetooth::setupService() -{ - NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID); - NimBLECharacteristic *ToRadioCharacteristic; - NimBLECharacteristic *FromRadioCharacteristic; - // Define the characteristics that the app is looking for - if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); - // Allow notifications so phones can stream FromRadio without polling. - FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); - fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); - logRadioCharacteristic = - bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); - } else { - ToRadioCharacteristic = bleService->createCharacteristic( - TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); - FromRadioCharacteristic = bleService->createCharacteristic( - FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); - fromNumCharacteristic = - bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | - NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); - logRadioCharacteristic = bleService->createCharacteristic( - LOGRADIO_UUID, - NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U); - } - bluetoothPhoneAPI = new BluetoothPhoneAPI(); +void NimbleBluetooth::setupService() { + NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID); + NimBLECharacteristic *ToRadioCharacteristic; + NimBLECharacteristic *FromRadioCharacteristic; + // Define the characteristics that the app is looking for + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); + // Allow notifications so phones can stream FromRadio without polling. + FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); + fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); + logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); + } else { + ToRadioCharacteristic = + bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); + FromRadioCharacteristic = + bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); + fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | + NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); + logRadioCharacteristic = bleService->createCharacteristic( + LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U); + } + bluetoothPhoneAPI = new BluetoothPhoneAPI(); - toRadioCallbacks = new NimbleBluetoothToRadioCallback(); - ToRadioCharacteristic->setCallbacks(toRadioCallbacks); + toRadioCallbacks = new NimbleBluetoothToRadioCallback(); + ToRadioCharacteristic->setCallbacks(toRadioCallbacks); - fromRadioCallbacks = new NimbleBluetoothFromRadioCallback(); - FromRadioCharacteristic->setCallbacks(fromRadioCallbacks); + fromRadioCallbacks = new NimbleBluetoothFromRadioCallback(); + FromRadioCharacteristic->setCallbacks(fromRadioCallbacks); - bleService->start(); + bleService->start(); - // Setup the battery service - NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service - BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) - (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); + // Setup the battery service + NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service + BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) + (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); #ifdef NIMBLE_TWO - NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); + NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); #else - NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); + NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); #endif - batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); - batteryLevelDescriptor->setNamespace(1); - batteryLevelDescriptor->setUnit(0x27ad); + batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); + batteryLevelDescriptor->setNamespace(1); + batteryLevelDescriptor->setUnit(0x27ad); - batteryService->start(); + batteryService->start(); } -void NimbleBluetooth::startAdvertising() -{ +void NimbleBluetooth::startAdvertising() { #ifdef NIMBLE_TWO - NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - NimBLEExtAdvertisement legacyAdvertising; + NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + NimBLEExtAdvertisement legacyAdvertising; - legacyAdvertising.setLegacyAdvertising(true); - legacyAdvertising.setScannable(true); - legacyAdvertising.setConnectable(true); - legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); - if (powerStatus->getHasBattery() == 1) { - legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); - } - legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); - legacyAdvertising.setMinInterval(500); - legacyAdvertising.setMaxInterval(1000); + legacyAdvertising.setLegacyAdvertising(true); + legacyAdvertising.setScannable(true); + legacyAdvertising.setConnectable(true); + legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); + if (powerStatus->getHasBattery() == 1) { + legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); + } + legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); + legacyAdvertising.setMinInterval(500); + legacyAdvertising.setMaxInterval(1000); - NimBLEExtAdvertisement legacyScanResponse; - legacyScanResponse.setLegacyAdvertising(true); - legacyScanResponse.setConnectable(true); - legacyScanResponse.setName(getDeviceName()); + NimBLEExtAdvertisement legacyScanResponse; + legacyScanResponse.setLegacyAdvertising(true); + legacyScanResponse.setConnectable(true); + legacyScanResponse.setName(getDeviceName()); - if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { - LOG_ERROR("BLE failed to set legacyAdvertising"); - } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { - LOG_ERROR("BLE failed to set legacyScanResponse"); - } else if (!pAdvertising->start(0, 0, 0)) { - LOG_ERROR("BLE failed to start legacyAdvertising"); - } + if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { + LOG_ERROR("BLE failed to set legacyAdvertising"); + } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { + LOG_ERROR("BLE failed to set legacyScanResponse"); + } else if (!pAdvertising->start(0, 0, 0)) { + LOG_ERROR("BLE failed to start legacyAdvertising"); + } #else - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->reset(); - pAdvertising->addServiceUUID(MESH_SERVICE_UUID); - pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service - pAdvertising->start(0); + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->reset(); + pAdvertising->addServiceUUID(MESH_SERVICE_UUID); + pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service + pAdvertising->start(0); #endif } /// Given a level between 0-100, update the BLE attribute -void updateBatteryLevel(uint8_t level) -{ - if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { - BatteryCharacteristic->setValue(&level, 1); +void updateBatteryLevel(uint8_t level) { + if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { + BatteryCharacteristic->setValue(&level, 1); #ifdef NIMBLE_TWO - BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); + BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); #else - BatteryCharacteristic->notify(); + BatteryCharacteristic->notify(); #endif - } + } } -void NimbleBluetooth::clearBonds() -{ - LOG_INFO("Clearing bluetooth bonds!"); - NimBLEDevice::deleteAllBonds(); +void NimbleBluetooth::clearBonds() { + LOG_INFO("Clearing bluetooth bonds!"); + NimBLEDevice::deleteAllBonds(); } -void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) -{ - if (!bleServer || !isConnected() || length > 512) { - return; - } +void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) { + if (!bleServer || !isConnected() || length > 512) { + return; + } #ifdef NIMBLE_TWO - logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); + logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); #else - logRadioCharacteristic->notify(logMessage, length, true); + logRadioCharacteristic->notify(logMessage, length, true); #endif } -void clearNVS() -{ - NimBLEDevice::deleteAllBonds(); +void clearNVS() { + NimBLEDevice::deleteAllBonds(); #ifdef ARCH_ESP32 - ESP.restart(); + ESP.restart(); #endif } #endif diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index 458fa4a67..f485619c4 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -1,26 +1,25 @@ #pragma once #include "BluetoothCommon.h" -class NimbleBluetooth : BluetoothApi -{ - public: - void setup(); - void shutdown(); - void deinit(); - void clearBonds(); - bool isActive(); - bool isConnected(); - int getRssi(); - void sendLog(const uint8_t *logMessage, size_t length); +class NimbleBluetooth : BluetoothApi { +public: + void setup(); + void shutdown(); + void deinit(); + void clearBonds(); + bool isActive(); + bool isConnected(); + int getRssi(); + void sendLog(const uint8_t *logMessage, size_t length); #if defined(NIMBLE_TWO) - void startAdvertising(); + void startAdvertising(); #endif - bool isDeInit = false; + bool isDeInit = false; - private: - void setupService(); +private: + void setupService(); #if !defined(NIMBLE_TWO) - void startAdvertising(); + void startAdvertising(); #endif }; diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp index 698336f69..2414a9671 100644 --- a/src/platform/esp32/BleOta.cpp +++ b/src/platform/esp32/BleOta.cpp @@ -4,43 +4,40 @@ static const String MESHTASTIC_OTA_APP_PROJECT_NAME("Meshtastic-OTA"); -const esp_partition_t *BleOta::findEspOtaAppPartition() -{ - const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); +const esp_partition_t *BleOta::findEspOtaAppPartition() { + const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + esp_app_desc_t app_desc; + esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } + if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) { + part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); + ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + } - if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) { - return part; - } else { - return nullptr; - } + if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) { + return part; + } else { + return nullptr; + } } -String BleOta::getOtaAppVersion() -{ - const esp_partition_t *part = findEspOtaAppPartition(); - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - String version; - if (ret == ESP_OK) { - version = app_desc.version; - } - return version; +String BleOta::getOtaAppVersion() { + const esp_partition_t *part = findEspOtaAppPartition(); + esp_app_desc_t app_desc; + esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + String version; + if (ret == ESP_OK) { + version = app_desc.version; + } + return version; } -bool BleOta::switchToOtaApp() -{ - bool success = false; - const esp_partition_t *part = findEspOtaAppPartition(); - if (part) { - success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); - } - return success; +bool BleOta::switchToOtaApp() { + bool success = false; + const esp_partition_t *part = findEspOtaAppPartition(); + if (part) { + success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); + } + return success; } \ No newline at end of file diff --git a/src/platform/esp32/BleOta.h b/src/platform/esp32/BleOta.h index f4c510920..6857ab51c 100644 --- a/src/platform/esp32/BleOta.h +++ b/src/platform/esp32/BleOta.h @@ -4,17 +4,16 @@ #include #include -class BleOta -{ - public: - explicit BleOta(){}; +class BleOta { +public: + explicit BleOta(){}; - static String getOtaAppVersion(); - static bool switchToOtaApp(); + static String getOtaAppVersion(); + static bool switchToOtaApp(); - private: - String mUserAgent; - static const esp_partition_t *findEspOtaAppPartition(); +private: + String mUserAgent; + static const esp_partition_t *findEspOtaAppPartition(); }; #endif // BLEOTA_H \ No newline at end of file diff --git a/src/platform/esp32/ESP32CryptoEngine.cpp b/src/platform/esp32/ESP32CryptoEngine.cpp index b554a3de4..43c73c41f 100644 --- a/src/platform/esp32/ESP32CryptoEngine.cpp +++ b/src/platform/esp32/ESP32CryptoEngine.cpp @@ -3,39 +3,37 @@ #include "mbedtls/aes.h" -class ESP32CryptoEngine : public CryptoEngine -{ +class ESP32CryptoEngine : public CryptoEngine { - mbedtls_aes_context aes; + mbedtls_aes_context aes; - public: - ESP32CryptoEngine() { mbedtls_aes_init(&aes); } +public: + ESP32CryptoEngine() { mbedtls_aes_init(&aes); } - ~ESP32CryptoEngine() { mbedtls_aes_free(&aes); } + ~ESP32CryptoEngine() { mbedtls_aes_free(&aes); } - /** - * Encrypt a packet - * - * @param bytes is updated in place - * TODO: return bool, and handle graciously when something fails - */ - virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override - { - if (_key.length > 0) { - if (numBytes <= MAX_BLOCKSIZE) { - mbedtls_aes_setkey_enc(&aes, _key.bytes, _key.length * 8); - static uint8_t scratch[MAX_BLOCKSIZE]; - uint8_t stream_block[16]; - size_t nc_off = 0; - memcpy(scratch, bytes, numBytes); - memset(scratch + numBytes, 0, - sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) - mbedtls_aes_crypt_ctr(&aes, numBytes, &nc_off, _nonce, stream_block, scratch, bytes); - } else { - LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); - } - } + /** + * Encrypt a packet + * + * @param bytes is updated in place + * TODO: return bool, and handle graciously when something fails + */ + virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override { + if (_key.length > 0) { + if (numBytes <= MAX_BLOCKSIZE) { + mbedtls_aes_setkey_enc(&aes, _key.bytes, _key.length * 8); + static uint8_t scratch[MAX_BLOCKSIZE]; + uint8_t stream_block[16]; + size_t nc_off = 0; + memcpy(scratch, bytes, numBytes); + memset(scratch + numBytes, 0, + sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) + mbedtls_aes_crypt_ctr(&aes, numBytes, &nc_off, _nonce, stream_block, scratch, bytes); + } else { + LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); + } } + } }; CryptoEngine *crypto = new ESP32CryptoEngine(); \ No newline at end of file diff --git a/src/platform/esp32/SimpleAllocator.cpp b/src/platform/esp32/SimpleAllocator.cpp index d482a3f1c..27d38bbbc 100644 --- a/src/platform/esp32/SimpleAllocator.cpp +++ b/src/platform/esp32/SimpleAllocator.cpp @@ -2,27 +2,17 @@ #include "assert.h" #include "configuration.h" -SimpleAllocator::SimpleAllocator() -{ - reset(); +SimpleAllocator::SimpleAllocator() { reset(); } + +void *SimpleAllocator::alloc(size_t size) { + assert(nextFree + size <= sizeof(bytes)); + void *res = &bytes[nextFree]; + nextFree += size; + LOG_DEBUG("Total simple allocs %u", nextFree); + + return res; } -void *SimpleAllocator::alloc(size_t size) -{ - assert(nextFree + size <= sizeof(bytes)); - void *res = &bytes[nextFree]; - nextFree += size; - LOG_DEBUG("Total simple allocs %u", nextFree); +void SimpleAllocator::reset() { nextFree = 0; } - return res; -} - -void SimpleAllocator::reset() -{ - nextFree = 0; -} - -void *operator new(size_t size, SimpleAllocator &p) -{ - return p.alloc(size); -} +void *operator new(size_t size, SimpleAllocator &p) { return p.alloc(size); } diff --git a/src/platform/esp32/SimpleAllocator.h b/src/platform/esp32/SimpleAllocator.h index eaf4ee710..7bb8da116 100644 --- a/src/platform/esp32/SimpleAllocator.h +++ b/src/platform/esp32/SimpleAllocator.h @@ -12,21 +12,20 @@ * Currently the only usecase for this class is the ESP32 bluetooth stack, where once we've called deinit(false) * we are sure all those bluetooth objects no longer exist, and we'll need to recreate them when we restart bluetooth */ -class SimpleAllocator -{ - uint8_t bytes[POOL_SIZE] = {}; +class SimpleAllocator { + uint8_t bytes[POOL_SIZE] = {}; - uint32_t nextFree = 0; + uint32_t nextFree = 0; - public: - SimpleAllocator(); +public: + SimpleAllocator(); - void *alloc(size_t size); + void *alloc(size_t size); - /** If you are _sure_ no outstanding references to blocks in this buffer still exist, you can call - * reset() to start from scratch. - * */ - void reset(); + /** If you are _sure_ no outstanding references to blocks in this buffer still exist, you can call + * reset() to start from scratch. + * */ + void reset(); }; void *operator new(size_t size, SimpleAllocator &p); @@ -35,9 +34,8 @@ void *operator new(size_t size, SimpleAllocator &p); * Temporarily makes the specified Allocator be used for _all_ allocations. Useful when calling library routines * that don't know about pools */ -class AllocatorScope -{ - public: - explicit AllocatorScope(SimpleAllocator &a); - ~AllocatorScope(); +class AllocatorScope { +public: + explicit AllocatorScope(SimpleAllocator &a); + ~AllocatorScope(); }; diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/WiFiOTA.cpp index 4cf157b4c..d6177fb7f 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/WiFiOTA.cpp @@ -3,90 +3,77 @@ #include #include -namespace WiFiOTA -{ +namespace WiFiOTA { static const char *nvsNamespace = "ota-wifi"; static const char *appProjectName = "OTA-WiFi"; static bool updated = false; -bool isUpdated() -{ - return updated; -} +bool isUpdated() { return updated; } -void initialize() -{ - Preferences prefs; - prefs.begin(nvsNamespace); - if (prefs.getBool("updated")) { - LOG_INFO("First boot after OTA update"); - updated = true; - prefs.putBool("updated", false); - } - prefs.end(); -} - -void recoverConfig(meshtastic_Config_NetworkConfig *network) -{ - LOG_INFO("Recovering WiFi settings after OTA update"); - - Preferences prefs; - prefs.begin(nvsNamespace, true); - String ssid = prefs.getString("ssid"); - String psk = prefs.getString("psk"); - prefs.end(); - - network->wifi_enabled = true; - strncpy(network->wifi_ssid, ssid.c_str(), sizeof(network->wifi_ssid)); - strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); -} - -void saveConfig(meshtastic_Config_NetworkConfig *network) -{ - LOG_INFO("Saving WiFi settings for upcoming OTA update"); - - Preferences prefs; - prefs.begin(nvsNamespace); - prefs.putString("ssid", network->wifi_ssid); - prefs.putString("psk", network->wifi_psk); +void initialize() { + Preferences prefs; + prefs.begin(nvsNamespace); + if (prefs.getBool("updated")) { + LOG_INFO("First boot after OTA update"); + updated = true; prefs.putBool("updated", false); - prefs.end(); + } + prefs.end(); } -const esp_partition_t *getAppPartition() -{ - return esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); +void recoverConfig(meshtastic_Config_NetworkConfig *network) { + LOG_INFO("Recovering WiFi settings after OTA update"); + + Preferences prefs; + prefs.begin(nvsNamespace, true); + String ssid = prefs.getString("ssid"); + String psk = prefs.getString("psk"); + prefs.end(); + + network->wifi_enabled = true; + strncpy(network->wifi_ssid, ssid.c_str(), sizeof(network->wifi_ssid)); + strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); } -bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) -{ - if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) - return false; - if (strcmp(app_desc->project_name, appProjectName) != 0) - return false; - return true; +void saveConfig(meshtastic_Config_NetworkConfig *network) { + LOG_INFO("Saving WiFi settings for upcoming OTA update"); + + Preferences prefs; + prefs.begin(nvsNamespace); + prefs.putString("ssid", network->wifi_ssid); + prefs.putString("psk", network->wifi_psk); + prefs.putBool("updated", false); + prefs.end(); } -bool trySwitchToOTA() -{ - const esp_partition_t *part = getAppPartition(); - esp_app_desc_t app_desc; - if (!getAppDesc(part, &app_desc)) - return false; - if (esp_ota_set_boot_partition(part) != ESP_OK) - return false; - return true; +const esp_partition_t *getAppPartition() { return esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); } + +bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) { + if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) + return false; + if (strcmp(app_desc->project_name, appProjectName) != 0) + return false; + return true; } -const char *getVersion() -{ - const esp_partition_t *part = getAppPartition(); - static esp_app_desc_t app_desc; - if (!getAppDesc(part, &app_desc)) - return ""; - return app_desc.version; +bool trySwitchToOTA() { + const esp_partition_t *part = getAppPartition(); + esp_app_desc_t app_desc; + if (!getAppDesc(part, &app_desc)) + return false; + if (esp_ota_set_boot_partition(part) != ESP_OK) + return false; + return true; +} + +const char *getVersion() { + const esp_partition_t *part = getAppPartition(); + static esp_app_desc_t app_desc; + if (!getAppDesc(part, &app_desc)) + return ""; + return app_desc.version; } } // namespace WiFiOTA diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h index 5a7ee348a..753dbc9c8 100644 --- a/src/platform/esp32/WiFiOTA.h +++ b/src/platform/esp32/WiFiOTA.h @@ -4,8 +4,7 @@ #include "mesh-pb-constants.h" #include -namespace WiFiOTA -{ +namespace WiFiOTA { void initialize(); bool isUpdated(); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 085692f96..0158627fb 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -212,8 +212,8 @@ // ----------------------------------------------------------------------------- // If an SPI-related pin used by the LoRa module isn't defined, use the conventional pin number for it. -// FIXME: these pins should really be defined in each variant.h file to prevent breakages if the defaults change, currently many -// ESP32 variants don't define these pins in their variant.h file. +// FIXME: these pins should really be defined in each variant.h file to prevent breakages if the defaults change, +// currently many ESP32 variants don't define these pins in their variant.h file. #ifndef LORA_SCK #define LORA_SCK 5 #endif diff --git a/src/platform/esp32/iram-quirk.c b/src/platform/esp32/iram-quirk.c index 813842138..31283ae56 100644 --- a/src/platform/esp32/iram-quirk.c +++ b/src/platform/esp32/iram-quirk.c @@ -8,10 +8,7 @@ #define IRAM_SECTION section(".iram1.stub") -IRAM_ATTR esp_err_t stub_probe(esp_flash_t *chip, uint32_t flash_id) -{ - return ESP_ERR_NOT_FOUND; -} +IRAM_ATTR esp_err_t stub_probe(esp_flash_t *chip, uint32_t flash_id) { return ESP_ERR_NOT_FOUND; } const spi_flash_chip_t stub_flash_chip __attribute__((IRAM_SECTION)) = { .name = "stub", diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 760964119..67c636063 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -26,143 +26,138 @@ #include #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH -void setBluetoothEnable(bool enable) -{ +void setBluetoothEnable(bool enable) { #ifdef USE_WS5500 - if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false)) + if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false)) #elif HAS_WIFI - if (!isWifiAvailable() && config.bluetooth.enabled == true) + if (!isWifiAvailable() && config.bluetooth.enabled == true) #else - if (config.bluetooth.enabled == true) + if (config.bluetooth.enabled == true) #endif - { - if (!nimbleBluetooth) { - nimbleBluetooth = new NimbleBluetooth(); - } - if (enable && !nimbleBluetooth->isActive()) { - powerMon->setState(meshtastic_PowerMon_State_BT_On); - nimbleBluetooth->setup(); - } - // For ESP32, no way to recover from bluetooth shutdown without reboot - // BLE advertising automatically stops when MCU enters light-sleep(?) - // For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse + { + if (!nimbleBluetooth) { + nimbleBluetooth = new NimbleBluetooth(); } + if (enable && !nimbleBluetooth->isActive()) { + powerMon->setState(meshtastic_PowerMon_State_BT_On); + nimbleBluetooth->setup(); + } + // For ESP32, no way to recover from bluetooth shutdown without reboot + // BLE advertising automatically stops when MCU enters light-sleep(?) + // For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse + } } #else void setBluetoothEnable(bool enable) {} void updateBatteryLevel(uint8_t level) {} #endif -void getMacAddr(uint8_t *dmac) -{ +void getMacAddr(uint8_t *dmac) { #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_SOC_IEEE802154_SUPPORTED) - auto res = esp_base_mac_addr_get(dmac); - assert(res == ESP_OK); + auto res = esp_base_mac_addr_get(dmac); + assert(res == ESP_OK); #else - auto res = esp_efuse_mac_get_default(dmac); - assert(res == ESP_OK); + auto res = esp_efuse_mac_get_default(dmac); + assert(res == ESP_OK); #endif } #if HAS_32768HZ #define CALIBRATE_ONE(cali_clk) calibrate_one(cali_clk, #cali_clk) -static uint32_t calibrate_one(rtc_cal_sel_t cal_clk, const char *name) -{ - const uint32_t cal_count = 1000; - // const float factor = (1 << 19) * 1000.0f; unused var? - uint32_t cali_val; - for (int i = 0; i < 5; ++i) { - cali_val = rtc_clk_cal(cal_clk, cal_count); - } - return cali_val; +static uint32_t calibrate_one(rtc_cal_sel_t cal_clk, const char *name) { + const uint32_t cal_count = 1000; + // const float factor = (1 << 19) * 1000.0f; unused var? + uint32_t cali_val; + for (int i = 0; i < 5; ++i) { + cali_val = rtc_clk_cal(cal_clk, cal_count); + } + return cali_val; } -void enableSlowCLK() -{ - rtc_clk_32k_enable(true); +void enableSlowCLK() { + rtc_clk_32k_enable(true); - CALIBRATE_ONE(RTC_CAL_RTC_MUX); - uint32_t cal_32k = CALIBRATE_ONE(RTC_CAL_32K_XTAL); + CALIBRATE_ONE(RTC_CAL_RTC_MUX); + uint32_t cal_32k = CALIBRATE_ONE(RTC_CAL_32K_XTAL); - if (cal_32k == 0) { - LOG_DEBUG("32k XTAL OSC has not started up"); - } else { - rtc_clk_slow_freq_set(RTC_SLOW_FREQ_32K_XTAL); - LOG_DEBUG("Switch RTC Source to 32.768kHz succeeded, using 32k XTAL"); - CALIBRATE_ONE(RTC_CAL_RTC_MUX); - CALIBRATE_ONE(RTC_CAL_32K_XTAL); - } + if (cal_32k == 0) { + LOG_DEBUG("32k XTAL OSC has not started up"); + } else { + rtc_clk_slow_freq_set(RTC_SLOW_FREQ_32K_XTAL); + LOG_DEBUG("Switch RTC Source to 32.768kHz succeeded, using 32k XTAL"); CALIBRATE_ONE(RTC_CAL_RTC_MUX); CALIBRATE_ONE(RTC_CAL_32K_XTAL); - if (rtc_clk_slow_freq_get() != RTC_SLOW_FREQ_32K_XTAL) { - LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768kHz !!! "); - return; - } + } + CALIBRATE_ONE(RTC_CAL_RTC_MUX); + CALIBRATE_ONE(RTC_CAL_32K_XTAL); + if (rtc_clk_slow_freq_get() != RTC_SLOW_FREQ_32K_XTAL) { + LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768kHz !!! "); + return; + } } #endif -void esp32Setup() -{ - /* We explicitly don't want to do call randomSeed, - // as that triggers the esp32 core to use a less secure pseudorandom function. - uint32_t seed = esp_random(); - LOG_DEBUG("Set random seed %u", seed); - randomSeed(seed); - */ +void esp32Setup() { + /* We explicitly don't want to do call randomSeed, + // as that triggers the esp32 core to use a less secure pseudorandom function. + uint32_t seed = esp_random(); + LOG_DEBUG("Set random seed %u", seed); + randomSeed(seed); + */ #ifdef ADC_V - pinMode(ADC_V, INPUT); + pinMode(ADC_V, INPUT); #endif - LOG_DEBUG("Total heap: %d", ESP.getHeapSize()); - LOG_DEBUG("Free heap: %d", ESP.getFreeHeap()); - LOG_DEBUG("Total PSRAM: %d", ESP.getPsramSize()); - LOG_DEBUG("Free PSRAM: %d", ESP.getFreePsram()); + LOG_DEBUG("Total heap: %d", ESP.getHeapSize()); + LOG_DEBUG("Free heap: %d", ESP.getFreeHeap()); + LOG_DEBUG("Total PSRAM: %d", ESP.getPsramSize()); + LOG_DEBUG("Free PSRAM: %d", ESP.getFreePsram()); - nvs_stats_t nvs_stats; - auto res = nvs_get_stats(NULL, &nvs_stats); - assert(res == ESP_OK); - LOG_DEBUG("NVS: UsedEntries %d, FreeEntries %d, AllEntries %d, NameSpaces %d", nvs_stats.used_entries, nvs_stats.free_entries, - nvs_stats.total_entries, nvs_stats.namespace_count); + nvs_stats_t nvs_stats; + auto res = nvs_get_stats(NULL, &nvs_stats); + assert(res == ESP_OK); + LOG_DEBUG("NVS: UsedEntries %d, FreeEntries %d, AllEntries %d, NameSpaces %d", nvs_stats.used_entries, nvs_stats.free_entries, + nvs_stats.total_entries, nvs_stats.namespace_count); - LOG_DEBUG("Setup Preferences in Flash Storage"); + LOG_DEBUG("Setup Preferences in Flash Storage"); - // Create object to store our persistent data - Preferences preferences; - preferences.begin("meshtastic", false); + // Create object to store our persistent data + Preferences preferences; + preferences.begin("meshtastic", false); - uint32_t rebootCounter = preferences.getUInt("rebootCounter", 0); - rebootCounter++; - preferences.putUInt("rebootCounter", rebootCounter); - // store firmware version and hwrevision for access from OTA firmware - String fwrev = preferences.getString("firmwareVersion", ""); - if (fwrev.compareTo(optstr(APP_VERSION)) != 0) - preferences.putString("firmwareVersion", optstr(APP_VERSION)); - uint8_t hwven = preferences.getUInt("hwVendor", 0); - if (hwven != HW_VENDOR) - preferences.putUInt("hwVendor", HW_VENDOR); - preferences.end(); - LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); + uint32_t rebootCounter = preferences.getUInt("rebootCounter", 0); + rebootCounter++; + preferences.putUInt("rebootCounter", rebootCounter); + // store firmware version and hwrevision for access from OTA firmware + String fwrev = preferences.getString("firmwareVersion", ""); + if (fwrev.compareTo(optstr(APP_VERSION)) != 0) + preferences.putString("firmwareVersion", optstr(APP_VERSION)); + uint8_t hwven = preferences.getUInt("hwVendor", 0); + if (hwven != HW_VENDOR) + preferences.putUInt("hwVendor", HW_VENDOR); + preferences.end(); + LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); #if !MESHTASTIC_EXCLUDE_BLUETOOTH - String BLEOTA = BleOta::getOtaAppVersion(); - if (BLEOTA.isEmpty()) { - LOG_INFO("No BLE OTA firmware available"); - } else { - LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); - } + String BLEOTA = BleOta::getOtaAppVersion(); + if (BLEOTA.isEmpty()) { + LOG_INFO("No BLE OTA firmware available"); + } else { + LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); + } #endif #if !MESHTASTIC_EXCLUDE_WIFI - String version = WiFiOTA::getVersion(); - if (version.isEmpty()) { - LOG_INFO("No WiFi OTA firmware available"); - } else { - LOG_INFO("WiFi OTA firmware version %s", version.c_str()); - } - WiFiOTA::initialize(); + String version = WiFiOTA::getVersion(); + if (version.isEmpty()) { + LOG_INFO("No WiFi OTA firmware available"); + } else { + LOG_INFO("WiFi OTA firmware version %s", version.c_str()); + } + WiFiOTA::initialize(); #endif - // enableModemSleep(); + // enableModemSleep(); // Since we are turning on watchdogs rather late in the release schedule, we really don't want to catch any // false positives. The wait-to-sleep timeout for shutting down radios is 30 secs, so pick 45 for now. @@ -170,98 +165,96 @@ void esp32Setup() #define APP_WATCHDOG_SECS 90 #ifdef CONFIG_IDF_TARGET_ESP32C6 - esp_task_wdt_config_t *wdt_config = (esp_task_wdt_config_t *)malloc(sizeof(esp_task_wdt_config_t)); - wdt_config->timeout_ms = APP_WATCHDOG_SECS * 1000; - wdt_config->trigger_panic = true; - res = esp_task_wdt_init(wdt_config); - assert(res == ESP_OK); + esp_task_wdt_config_t *wdt_config = (esp_task_wdt_config_t *)malloc(sizeof(esp_task_wdt_config_t)); + wdt_config->timeout_ms = APP_WATCHDOG_SECS * 1000; + wdt_config->trigger_panic = true; + res = esp_task_wdt_init(wdt_config); + assert(res == ESP_OK); #else - res = esp_task_wdt_init(APP_WATCHDOG_SECS, true); - assert(res == ESP_OK); + res = esp_task_wdt_init(APP_WATCHDOG_SECS, true); + assert(res == ESP_OK); #endif - res = esp_task_wdt_add(NULL); - assert(res == ESP_OK); + res = esp_task_wdt_add(NULL); + assert(res == ESP_OK); #if HAS_32768HZ - enableSlowCLK(); + enableSlowCLK(); #endif } /// loop code specific to ESP32 targets -void esp32Loop() -{ - esp_task_wdt_reset(); // service our app level watchdog +void esp32Loop() { + esp_task_wdt_reset(); // service our app level watchdog - // for debug printing - // radio.radioIf.canSleep(); + // for debug printing + // radio.radioIf.canSleep(); } -void cpuDeepSleep(uint32_t msecToWake) -{ - /* - Some ESP32 IOs have internal pullups or pulldowns, which are enabled by default. - If an external circuit drives this pin in deep sleep mode, current consumption may - increase due to current flowing through these pullups and pulldowns. +void cpuDeepSleep(uint32_t msecToWake) { + /* + Some ESP32 IOs have internal pullups or pulldowns, which are enabled by default. + If an external circuit drives this pin in deep sleep mode, current consumption may + increase due to current flowing through these pullups and pulldowns. - To isolate a pin, preventing extra current draw, call rtc_gpio_isolate() function. - For example, on ESP32-WROVER module, GPIO12 is pulled up externally. - GPIO12 also has an internal pulldown in the ESP32 chip. This means that in deep sleep, - some current will flow through these external and internal resistors, increasing deep - sleep current above the minimal possible value. + To isolate a pin, preventing extra current draw, call rtc_gpio_isolate() function. + For example, on ESP32-WROVER module, GPIO12 is pulled up externally. + GPIO12 also has an internal pulldown in the ESP32 chip. This means that in deep sleep, + some current will flow through these external and internal resistors, increasing deep + sleep current above the minimal possible value. - Note: we don't isolate pins that are used for the LORA, LED, i2c, or ST7735 Display for the Chatter2, spi or the wake - button(s), maybe we should not include any other GPIOs... - */ + Note: we don't isolate pins that are used for the LORA, LED, i2c, or ST7735 Display for the Chatter2, spi or the wake + button(s), maybe we should not include any other GPIOs... + */ #if SOC_RTCIO_HOLD_SUPPORTED - static const uint8_t rtcGpios[] = { + static const uint8_t rtcGpios[] = { #ifndef HELTEC_VISION_MASTER_E213 - // For this variant, >20mA leaks through the display if pin 2 held - // Todo: check if it's safe to remove this pin for all variants - 2, + // For this variant, >20mA leaks through the display if pin 2 held + // Todo: check if it's safe to remove this pin for all variants + 2, #endif #ifndef USE_JTAG - 13, + 13, #endif - 34, 35, 37}; + 34, 35, 37}; - for (int i = 0; i < sizeof(rtcGpios); i++) - rtc_gpio_isolate((gpio_num_t)rtcGpios[i]); + for (int i = 0; i < sizeof(rtcGpios); i++) + rtc_gpio_isolate((gpio_num_t)rtcGpios[i]); #endif - // FIXME, disable internal rtc pullups/pulldowns on the non isolated pins. for inputs that we aren't using - // to detect wake and in normal operation the external part drives them hard. + // FIXME, disable internal rtc pullups/pulldowns on the non isolated pins. for inputs that we aren't using + // to detect wake and in normal operation the external part drives them hard. #ifdef BUTTON_PIN - // Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39. + // Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39. #if SOC_RTCIO_HOLD_SUPPORTED && SOC_PM_SUPPORT_EXT_WAKEUP - uint64_t gpioMask = (1ULL << (config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + uint64_t gpioMask = (1ULL << (config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); #endif #ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)BUTTON_PIN); + gpio_pullup_en((gpio_num_t)BUTTON_PIN); #endif - // Not needed because both of the current boards have external pullups - // FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons (instead - // of just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN); + // Not needed because both of the current boards have external pullups + // FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons + // (instead of just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN); #ifdef ESP32S3_WAKE_TYPE - esp_sleep_enable_ext1_wakeup(gpioMask, ESP32S3_WAKE_TYPE); + esp_sleep_enable_ext1_wakeup(gpioMask, ESP32S3_WAKE_TYPE); #else #if SOC_PM_SUPPORT_EXT_WAKEUP #ifdef CONFIG_IDF_TARGET_ESP32 - // ESP_EXT1_WAKEUP_ALL_LOW has been deprecated since esp-idf v5.4 for any other target. - esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW); + // ESP_EXT1_WAKEUP_ALL_LOW has been deprecated since esp-idf v5.4 for any other target. + esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW); #else - esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ANY_LOW); + esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ANY_LOW); #endif #endif #endif // #end ESP32S3_WAKE_TYPE #endif - // We want RTC peripherals to stay on - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + // We want RTC peripherals to stay on + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs - esp_deep_sleep_start(); // TBD mA sleep current (battery) + esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs + esp_deep_sleep_start(); // TBD mA sleep current (battery) } diff --git a/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp b/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp index 12960e2d9..73ad6c541 100644 --- a/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp +++ b/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp @@ -7,34 +7,33 @@ #include "graphics/TFTDisplay.h" // Heltec tracker specific init -void lateInitVariant() -{ - // LOG_DEBUG("Heltec tracker initVariant"); +void lateInitVariant() { + // LOG_DEBUG("Heltec tracker initVariant"); #ifndef MESHTASTIC_EXCLUDE_GPS - GpioVirtPin *virtGpsEnable = gps ? gps->enablePin : new GpioVirtPin(); + GpioVirtPin *virtGpsEnable = gps ? gps->enablePin : new GpioVirtPin(); #else - GpioVirtPin *virtGpsEnable = new GpioVirtPin(); + GpioVirtPin *virtGpsEnable = new GpioVirtPin(); #endif #ifndef MESHTASTIC_EXCLUDE_SCREEN - // On this board we are actually using the backlightEnable signal to already be controlling a physical enable to the - // display controller. But we'd _ALSO_ like to have that signal drive a virtual GPIO. So nest it as needed. - GpioVirtPin *virtScreenEnable = new GpioVirtPin(); - if (TFTDisplay::backlightEnable) { - GpioPin *physScreenEnable = TFTDisplay::backlightEnable; - GpioPin *splitter = new GpioSplitter(virtScreenEnable, physScreenEnable); - TFTDisplay::backlightEnable = splitter; + // On this board we are actually using the backlightEnable signal to already be controlling a physical enable to the + // display controller. But we'd _ALSO_ like to have that signal drive a virtual GPIO. So nest it as needed. + GpioVirtPin *virtScreenEnable = new GpioVirtPin(); + if (TFTDisplay::backlightEnable) { + GpioPin *physScreenEnable = TFTDisplay::backlightEnable; + GpioPin *splitter = new GpioSplitter(virtScreenEnable, physScreenEnable); + TFTDisplay::backlightEnable = splitter; - // Assume screen is initially powered - splitter->set(true); - } + // Assume screen is initially powered + splitter->set(true); + } #endif #if defined(VEXT_ENABLE) && (!defined(MESHTASTIC_EXCLUDE_GPS) || !defined(MESHTASTIC_EXCLUDE_SCREEN)) - // If either the GPS or the screen is on, turn on the external power regulator - GpioPin *hwEnable = new GpioHwPin(VEXT_ENABLE); - new GpioBinaryTransformer(virtGpsEnable, virtScreenEnable, hwEnable, GpioBinaryTransformer::Or); + // If either the GPS or the screen is on, turn on the external power regulator + GpioPin *hwEnable = new GpioHwPin(VEXT_ENABLE); + new GpioBinaryTransformer(virtGpsEnable, virtScreenEnable, hwEnable, GpioBinaryTransformer::Or); #endif } diff --git a/src/platform/extra_variants/t_deck_pro/variant.cpp b/src/platform/extra_variants/t_deck_pro/variant.cpp index eae9335ce..7fe84025b 100644 --- a/src/platform/extra_variants/t_deck_pro/variant.cpp +++ b/src/platform/extra_variants/t_deck_pro/variant.cpp @@ -8,21 +8,19 @@ CSE_CST328 tsPanel = CSE_CST328(EINK_WIDTH, EINK_HEIGHT, &Wire, CST328_PIN_RST, CST328_PIN_INT); -bool readTouch(int16_t *x, int16_t *y) -{ - if (tsPanel.getTouches()) { - *x = tsPanel.getPoint(0).x; - *y = tsPanel.getPoint(0).y; - return true; - } - return false; +bool readTouch(int16_t *x, int16_t *y) { + if (tsPanel.getTouches()) { + *x = tsPanel.getPoint(0).x; + *y = tsPanel.getPoint(0).y; + return true; + } + return false; } // T-Deck Pro specific init -void lateInitVariant() -{ - tsPanel.begin(); - touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch); - touchScreenImpl1->init(); +void lateInitVariant() { + tsPanel.begin(); + touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch); + touchScreenImpl1->init(); } #endif \ No newline at end of file diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp index ea5773d30..fb8e4d1e6 100644 --- a/src/platform/extra_variants/t_lora_pager/variant.cpp +++ b/src/platform/extra_variants/t_lora_pager/variant.cpp @@ -8,20 +8,19 @@ DriverPins PinsAudioBoardES8311; AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); // TLora Pager specific init -void lateInitVariant() -{ - // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); - // I2C: function, scl, sda - PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); - // I2S: function, mclk, bck, ws, data_out, data_in - PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); +void lateInitVariant() { + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); - // configure codec - CodecConfig cfg; - cfg.input_device = ADC_INPUT_LINE1; - cfg.output_device = DAC_OUTPUT_ALL; - cfg.i2s.bits = BIT_LENGTH_16BITS; - cfg.i2s.rate = RATE_44K; - board.begin(cfg); + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); } #endif \ No newline at end of file diff --git a/src/platform/extra_variants/tbeam_displayshield/variant.cpp b/src/platform/extra_variants/tbeam_displayshield/variant.cpp index 7beac2293..cf08698f8 100644 --- a/src/platform/extra_variants/tbeam_displayshield/variant.cpp +++ b/src/platform/extra_variants/tbeam_displayshield/variant.cpp @@ -10,34 +10,32 @@ TouchDrvCSTXXX tsPanel; static constexpr uint8_t PossibleAddresses[2] = {CST328_ADDR, CST226SE_ADDR_ALT}; uint8_t i2cAddress = 0; -bool readTouch(int16_t *x, int16_t *y) -{ - int16_t x_array[1], y_array[1]; - uint8_t touched = tsPanel.getPoint(x_array, y_array, 1); - if (touched > 0) { - *y = x_array[0]; - *x = (TFT_WIDTH - y_array[0]); - // Check bounds - if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) { - return false; - } - return true; // Valid touch detected +bool readTouch(int16_t *x, int16_t *y) { + int16_t x_array[1], y_array[1]; + uint8_t touched = tsPanel.getPoint(x_array, y_array, 1); + if (touched > 0) { + *y = x_array[0]; + *x = (TFT_WIDTH - y_array[0]); + // Check bounds + if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) { + return false; } - return false; // No valid touch data + return true; // Valid touch detected + } + return false; // No valid touch data } -void lateInitVariant() -{ - tsPanel.setTouchDrvModel(TouchDrv_CST226); - for (uint8_t addr : PossibleAddresses) { - if (tsPanel.begin(Wire, addr, I2C_SDA, I2C_SCL)) { - i2cAddress = addr; - LOG_DEBUG("CST226SE init OK at address 0x%02X", addr); - touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch); - touchScreenImpl1->init(); - return; - } +void lateInitVariant() { + tsPanel.setTouchDrvModel(TouchDrv_CST226); + for (uint8_t addr : PossibleAddresses) { + if (tsPanel.begin(Wire, addr, I2C_SDA, I2C_SCL)) { + i2cAddress = addr; + LOG_DEBUG("CST226SE init OK at address 0x%02X", addr); + touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch); + touchScreenImpl1->init(); + return; } - LOG_ERROR("CST226SE init failed at all known addresses"); + } + LOG_ERROR("CST226SE init failed at all known addresses"); } #endif diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp index 836fb1307..a119b6278 100644 --- a/src/platform/nrf52/AsyncUDP.cpp +++ b/src/platform/nrf52/AsyncUDP.cpp @@ -4,70 +4,48 @@ AsyncUDP::AsyncUDP() : OSThread("AsyncUDP"), localPort(0) {} -bool AsyncUDP::listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl) -{ - if (!isMulticast(multicastIP)) - return false; - localPort = port; - udp.beginMulticast(multicastIP, port); - return true; +bool AsyncUDP::listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl) { + if (!isMulticast(multicastIP)) + return false; + localPort = port; + udp.beginMulticast(multicastIP, port); + return true; } -size_t AsyncUDP::write(uint8_t b) -{ - return udp.write(&b, 1); -} +size_t AsyncUDP::write(uint8_t b) { return udp.write(&b, 1); } -size_t AsyncUDP::write(const uint8_t *data, size_t len) -{ - return udp.write(data, len); -} +size_t AsyncUDP::write(const uint8_t *data, size_t len) { return udp.write(data, len); } -void AsyncUDP::onPacket(const std::function &callback) -{ - _onPacket = callback; -} +void AsyncUDP::onPacket(const std::function &callback) { _onPacket = callback; } -bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port) -{ - if (!udp.beginPacket(ip, port)) - return false; - udp.write(data, len); - return udp.endPacket(); +bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port) { + if (!udp.beginPacket(ip, port)) + return false; + udp.write(data, len); + return udp.endPacket(); } // AsyncUDPPacket -AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort()) -{ - if (_udp.available() > 0) { - _readLength = _udp.read(_buffer, sizeof(_buffer)); - } else { - _readLength = 0; - } +AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort()) { + if (_udp.available() > 0) { + _readLength = _udp.read(_buffer, sizeof(_buffer)); + } else { + _readLength = 0; + } } -IPAddress AsyncUDPPacket::remoteIP() -{ - return _remoteIP; -} +IPAddress AsyncUDPPacket::remoteIP() { return _remoteIP; } -uint16_t AsyncUDPPacket::length() -{ - return _readLength; -} +uint16_t AsyncUDPPacket::length() { return _readLength; } -const uint8_t *AsyncUDPPacket::data() -{ - return _buffer; -} +const uint8_t *AsyncUDPPacket::data() { return _buffer; } -int32_t AsyncUDP::runOnce() -{ - if (_onPacket && udp.parsePacket() > 0) { - AsyncUDPPacket packet(udp); - _onPacket(packet); - } - return 5; // check every 5ms +int32_t AsyncUDP::runOnce() { + if (_onPacket && udp.parsePacket() > 0) { + AsyncUDPPacket packet(udp); + _onPacket(packet); + } + return 5; // check every 5ms } #endif // HAS_ETHERNET \ No newline at end of file diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h index e2b406ba9..b710c7d59 100644 --- a/src/platform/nrf52/AsyncUDP.h +++ b/src/platform/nrf52/AsyncUDP.h @@ -14,49 +14,44 @@ class AsyncUDPPacket; -class AsyncUDP : public Print, private concurrency::OSThread -{ - public: - AsyncUDP(); - explicit operator bool() const { return localPort != 0; } +class AsyncUDP : public Print, private concurrency::OSThread { +public: + AsyncUDP(); + explicit operator bool() const { return localPort != 0; } - bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); - bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); + bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); + bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); - size_t write(uint8_t b) override; - size_t write(const uint8_t *data, size_t len) override; - void onPacket(const std::function &callback); + size_t write(uint8_t b) override; + size_t write(const uint8_t *data, size_t len) override; + void onPacket(const std::function &callback); - private: - EthernetUDP udp; - uint16_t localPort; - std::function _onPacket; - virtual int32_t runOnce() override; +private: + EthernetUDP udp; + uint16_t localPort; + std::function _onPacket; + virtual int32_t runOnce() override; }; -class AsyncUDPPacket -{ - public: - AsyncUDPPacket(EthernetUDP &source); +class AsyncUDPPacket { +public: + AsyncUDPPacket(EthernetUDP &source); - IPAddress remoteIP(); - uint16_t length(); - const uint8_t *data(); + IPAddress remoteIP(); + uint16_t length(); + const uint8_t *data(); - private: - EthernetUDP &_udp; - IPAddress _remoteIP; - uint16_t _remotePort; - size_t _readLength = 0; +private: + EthernetUDP &_udp; + IPAddress _remoteIP; + uint16_t _remotePort; + size_t _readLength = 0; - static constexpr size_t BUF_SIZE = 512; - uint8_t _buffer[BUF_SIZE]; + static constexpr size_t BUF_SIZE = 512; + uint8_t _buffer[BUF_SIZE]; }; -inline bool isMulticast(const IPAddress &ip) -{ - return (ip[0] & 0xF0) == 0xE0; -} +inline bool isMulticast(const IPAddress &ip) { return (ip[0] & 0xF0) == 0xE0; } #endif // HAS_ETHERNET diff --git a/src/platform/nrf52/BLEDfuScure.cpp b/src/platform/nrf52/BLEDfuScure.cpp index 82cb8905a..79d2e7c53 100644 --- a/src/platform/nrf52/BLEDfuScure.cpp +++ b/src/platform/nrf52/BLEDfuScure.cpp @@ -41,103 +41,99 @@ const uint16_t UUID16_SVC_DFU_OTA = 0xFE59; -const uint8_t UUID128_CHR_DFU_CONTROL[16] = {0x50, 0xEA, 0xDA, 0x30, 0x88, 0x83, 0xB8, 0x9F, - 0x60, 0x4F, 0x15, 0xF3, 0x03, 0x00, 0xC9, 0x8E}; +const uint8_t UUID128_CHR_DFU_CONTROL[16] = {0x50, 0xEA, 0xDA, 0x30, 0x88, 0x83, 0xB8, 0x9F, 0x60, 0x4F, 0x15, 0xF3, 0x03, 0x00, 0xC9, 0x8E}; extern "C" void bootloader_util_app_start(uint32_t start_addr); -static uint16_t crc16(const uint8_t *data_p, uint8_t length) -{ - uint16_t crc = 0xFFFF; +static uint16_t crc16(const uint8_t *data_p, uint8_t length) { + uint16_t crc = 0xFFFF; - while (length--) { - uint8_t x = crc >> 8 ^ *data_p++; - x ^= x >> 4; - crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x << 5)) ^ ((uint16_t)x); - } - return crc; + while (length--) { + uint8_t x = crc >> 8 ^ *data_p++; + x ^= x >> 4; + crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x << 5)) ^ ((uint16_t)x); + } + return crc; } -static void bledfu_control_wr_authorize_cb(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_write_t *request) -{ - if ((request->handle == chr->handles().value_handle) && (request->op != BLE_GATTS_OP_PREP_WRITE_REQ) && - (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_NOW) && (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL)) { - BLEConnection *conn = Bluefruit.Connection(conn_hdl); +static void bledfu_control_wr_authorize_cb(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_write_t *request) { + if ((request->handle == chr->handles().value_handle) && (request->op != BLE_GATTS_OP_PREP_WRITE_REQ) && + (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_NOW) && (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL)) { + BLEConnection *conn = Bluefruit.Connection(conn_hdl); - ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_WRITE}; + ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_WRITE}; - if (!chr->indicateEnabled(conn_hdl)) { - reply.params.write.gatt_status = BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR; - sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); - return; - } - - reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; - sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); - - enum { START_DFU = 1 }; - if (request->data[0] == START_DFU) { - // Peer data information so that bootloader could re-connect after reboot - typedef struct { - ble_gap_addr_t addr; - ble_gap_irk_t irk; - ble_gap_enc_key_t enc_key; - uint8_t sys_attr[8]; - uint16_t crc16; - } peer_data_t; - - VERIFY_STATIC(offsetof(peer_data_t, crc16) == 60); - - /* Save Peer data - * Peer data address is defined in bootloader linker @0x20007F80 - * - If bonded : save Security information - * - Otherwise : save Address for direct advertising - * - * TODO may force bonded only for security reason - */ - peer_data_t *peer_data = (peer_data_t *)(0x20007F80UL); - varclr(peer_data); - - // Get CCCD - uint16_t sysattr_len = sizeof(peer_data->sys_attr); - sd_ble_gatts_sys_attr_get(conn_hdl, peer_data->sys_attr, &sysattr_len, BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS); - - // Get Bond Data or using Address if not bonded - peer_data->addr = conn->getPeerAddr(); - - if (conn->secured()) { - bond_keys_t bkeys; - if (conn->loadBondKey(&bkeys)) { - peer_data->addr = bkeys.peer_id.id_addr_info; - peer_data->irk = bkeys.peer_id.id_info; - peer_data->enc_key = bkeys.own_enc; - } - } - - // Calculate crc - peer_data->crc16 = crc16((uint8_t *)peer_data, offsetof(peer_data_t, crc16)); - - // Initiate DFU Sequence and reboot into DFU OTA mode - Bluefruit.Advertising.restartOnDisconnect(false); - conn->disconnect(); - - NRF_POWER->GPREGRET = 0xB1; - NVIC_SystemReset(); - } + if (!chr->indicateEnabled(conn_hdl)) { + reply.params.write.gatt_status = BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR; + sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); + return; } + + reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; + sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); + + enum { START_DFU = 1 }; + if (request->data[0] == START_DFU) { + // Peer data information so that bootloader could re-connect after reboot + typedef struct { + ble_gap_addr_t addr; + ble_gap_irk_t irk; + ble_gap_enc_key_t enc_key; + uint8_t sys_attr[8]; + uint16_t crc16; + } peer_data_t; + + VERIFY_STATIC(offsetof(peer_data_t, crc16) == 60); + + /* Save Peer data + * Peer data address is defined in bootloader linker @0x20007F80 + * - If bonded : save Security information + * - Otherwise : save Address for direct advertising + * + * TODO may force bonded only for security reason + */ + peer_data_t *peer_data = (peer_data_t *)(0x20007F80UL); + varclr(peer_data); + + // Get CCCD + uint16_t sysattr_len = sizeof(peer_data->sys_attr); + sd_ble_gatts_sys_attr_get(conn_hdl, peer_data->sys_attr, &sysattr_len, BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS); + + // Get Bond Data or using Address if not bonded + peer_data->addr = conn->getPeerAddr(); + + if (conn->secured()) { + bond_keys_t bkeys; + if (conn->loadBondKey(&bkeys)) { + peer_data->addr = bkeys.peer_id.id_addr_info; + peer_data->irk = bkeys.peer_id.id_info; + peer_data->enc_key = bkeys.own_enc; + } + } + + // Calculate crc + peer_data->crc16 = crc16((uint8_t *)peer_data, offsetof(peer_data_t, crc16)); + + // Initiate DFU Sequence and reboot into DFU OTA mode + Bluefruit.Advertising.restartOnDisconnect(false); + conn->disconnect(); + + NRF_POWER->GPREGRET = 0xB1; + NVIC_SystemReset(); + } + } } BLEDfuSecure::BLEDfuSecure(void) : BLEService(UUID16_SVC_DFU_OTA), _chr_control(UUID128_CHR_DFU_CONTROL) {} -err_t BLEDfuSecure::begin(void) -{ - // Invoke base class begin() - VERIFY_STATUS(BLEService::begin()); +err_t BLEDfuSecure::begin(void) { + // Invoke base class begin() + VERIFY_STATUS(BLEService::begin()); - _chr_control.setProperties(CHR_PROPS_WRITE | CHR_PROPS_INDICATE); - _chr_control.setMaxLen(23); - _chr_control.setWriteAuthorizeCallback(bledfu_control_wr_authorize_cb); - VERIFY_STATUS(_chr_control.begin()); + _chr_control.setProperties(CHR_PROPS_WRITE | CHR_PROPS_INDICATE); + _chr_control.setMaxLen(23); + _chr_control.setWriteAuthorizeCallback(bledfu_control_wr_authorize_cb); + VERIFY_STATUS(_chr_control.begin()); - return ERROR_NONE; + return ERROR_NONE; } diff --git a/src/platform/nrf52/BLEDfuSecure.h b/src/platform/nrf52/BLEDfuSecure.h index bd5d910e8..7fea607a3 100644 --- a/src/platform/nrf52/BLEDfuSecure.h +++ b/src/platform/nrf52/BLEDfuSecure.h @@ -41,15 +41,14 @@ #include "BLECharacteristic.h" #include "BLEService.h" -class BLEDfuSecure : public BLEService -{ - protected: - BLECharacteristic _chr_control; +class BLEDfuSecure : public BLEService { +protected: + BLECharacteristic _chr_control; - public: - BLEDfuSecure(void); +public: + BLEDfuSecure(void); - virtual err_t begin(void); + virtual err_t begin(void); }; #endif /* BLEDFUSECURE_H_ */ diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 4f7fb4776..797d40f57 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -19,12 +19,12 @@ static BLEBas blebas; // BAS (Battery Service) helper class instance #ifndef BLE_DFU_SECURE static BLEDfu bledfu; // DFU software update helper service #else -static BLEDfuSecure bledfusecure; // DFU software update helper service +static BLEDfuSecure bledfusecure; // DFU software update helper service #endif -// This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be in -// process at once -// static uint8_t trBytes[_max(_max(_max(_max(ToRadio_size, RadioConfig_size), User_size), MyNodeInfo_size), FromRadio_size)]; +// This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be +// in process at once static uint8_t trBytes[_max(_max(_max(_max(ToRadio_size, RadioConfig_size), User_size), +// MyNodeInfo_size), FromRadio_size)]; static uint8_t fromRadioBytes[meshtastic_FromRadio_size]; static uint8_t toRadioBytes[meshtastic_ToRadio_size]; @@ -33,403 +33,372 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; static uint16_t connectionHandle; -class BluetoothPhoneAPI : public PhoneAPI -{ - /** - * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) - */ - virtual void onNowHasData(uint32_t fromRadioNum) override - { - PhoneAPI::onNowHasData(fromRadioNum); +class BluetoothPhoneAPI : public PhoneAPI { + /** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + virtual void onNowHasData(uint32_t fromRadioNum) override { + PhoneAPI::onNowHasData(fromRadioNum); - LOG_INFO("BLE notify fromNum"); - fromNum.notify32(fromRadioNum); - } + LOG_INFO("BLE notify fromNum"); + fromNum.notify32(fromRadioNum); + } - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return Bluefruit.connected(connectionHandle); } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return Bluefruit.connected(connectionHandle); } - public: - BluetoothPhoneAPI() { api_type = TYPE_BLE; } +public: + BluetoothPhoneAPI() { api_type = TYPE_BLE; } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; -void onConnect(uint16_t conn_handle) -{ - // Get the reference to current connection - BLEConnection *connection = Bluefruit.Connection(conn_handle); - connectionHandle = conn_handle; - char central_name[32] = {0}; - connection->getPeerName(central_name, sizeof(central_name)); - LOG_INFO("BLE Connected to %s", central_name); +void onConnect(uint16_t conn_handle) { + // Get the reference to current connection + BLEConnection *connection = Bluefruit.Connection(conn_handle); + connectionHandle = conn_handle; + char central_name[32] = {0}; + connection->getPeerName(central_name, sizeof(central_name)); + LOG_INFO("BLE Connected to %s", central_name); - // Notify UI (or any other interested firmware components) - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newStatus); + // Notify UI (or any other interested firmware components) + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped * @param conn_handle connection where this event happens * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ -void onDisconnect(uint16_t conn_handle, uint8_t reason) -{ - LOG_INFO("BLE Disconnected, reason = 0x%x", reason); - if (bluetoothPhoneAPI) { - bluetoothPhoneAPI->close(); - } +void onDisconnect(uint16_t conn_handle, uint8_t reason) { + LOG_INFO("BLE Disconnected, reason = 0x%x", reason); + if (bluetoothPhoneAPI) { + bluetoothPhoneAPI->close(); + } - // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection - memset(lastToRadio, 0, sizeof(lastToRadio)); + // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection + memset(lastToRadio, 0, sizeof(lastToRadio)); - // Notify UI (or any other interested firmware components) - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newStatus); + // Notify UI (or any other interested firmware components) + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); } -void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) -{ - // Display the raw request packet - LOG_INFO("CCCD Updated: %u", cccd_value); - // Check the characteristic this CCCD update is associated with in case - // this handler is used for multiple CCCD records. +void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { + // Display the raw request packet + LOG_INFO("CCCD Updated: %u", cccd_value); + // Check the characteristic this CCCD update is associated with in case + // this handler is used for multiple CCCD records. - // According to the GATT spec: cccd value = 0x0001 means notifications are enabled - // and cccd value = 0x0002 means indications are enabled + // According to the GATT spec: cccd value = 0x0001 means notifications are enabled + // and cccd value = 0x0002 means indications are enabled - if (chr->uuid == fromNum.uuid || chr->uuid == logRadio.uuid) { - auto result = cccd_value == 2 ? chr->indicateEnabled(conn_hdl) : chr->notifyEnabled(conn_hdl); - if (result) { - LOG_INFO("Notify/Indicate enabled"); - } else { - LOG_INFO("Notify/Indicate disabled"); - } + if (chr->uuid == fromNum.uuid || chr->uuid == logRadio.uuid) { + auto result = cccd_value == 2 ? chr->indicateEnabled(conn_hdl) : chr->notifyEnabled(conn_hdl); + if (result) { + LOG_INFO("Notify/Indicate enabled"); + } else { + LOG_INFO("Notify/Indicate disabled"); } + } } -void startAdv(void) -{ - // Advertising packet - Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); - // IncludeService UUID - // Bluefruit.ScanResponse.addService(meshBleService); - Bluefruit.ScanResponse.addTxPower(); - Bluefruit.ScanResponse.addName(); - // Include Name - // Bluefruit.Advertising.addName(); - Bluefruit.Advertising.addService(meshBleService); - /* Start Advertising - * - Enable auto advertising if disconnected - * - Interval: fast mode = 20 ms, slow mode = 152.5 ms - * - Timeout for fast mode is 30 seconds - * - Start(timeout) with timeout = 0 will advertise forever (until connected) - * - * For recommended advertising interval - * https://developer.apple.com/library/content/qa/qa1931/_index.html - */ - Bluefruit.Advertising.restartOnDisconnect(true); - Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms - Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode - Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds. FIXME, we should stop advertising after X +void startAdv(void) { + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + // IncludeService UUID + // Bluefruit.ScanResponse.addService(meshBleService); + Bluefruit.ScanResponse.addTxPower(); + Bluefruit.ScanResponse.addName(); + // Include Name + // Bluefruit.Advertising.addName(); + Bluefruit.Advertising.addService(meshBleService); + /* Start Advertising + * - Enable auto advertising if disconnected + * - Interval: fast mode = 20 ms, slow mode = 152.5 ms + * - Timeout for fast mode is 30 seconds + * - Start(timeout) with timeout = 0 will advertise forever (until connected) + * + * For recommended advertising interval + * https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds. FIXME, we should stop advertising after X } // Just ack that the caller is allowed to read -static void authorizeRead(uint16_t conn_hdl) -{ - ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; - reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; - sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); +static void authorizeRead(uint16_t conn_hdl) { + ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; + reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; + sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); } /** * client is starting read, pull the bytes from our API class */ -void onFromRadioAuthorize(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_read_t *request) -{ - if (request->offset == 0) { - // If the read is long, we will get multiple authorize invocations - we only populate data on the first - size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); - // Someone is going to read our value as soon as this callback returns. So fill it with the next message in the queue - // or make empty if the queue is empty - fromRadio.write(fromRadioBytes, numBytes); - } else { - // LOG_INFO("Ignore successor read"); - } - authorizeRead(conn_hdl); +void onFromRadioAuthorize(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_read_t *request) { + if (request->offset == 0) { + // If the read is long, we will get multiple authorize invocations - we only populate data on the first + size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); + // Someone is going to read our value as soon as this callback returns. So fill it with the next message in the + // queue or make empty if the queue is empty + fromRadio.write(fromRadioBytes, numBytes); + } else { + // LOG_INFO("Ignore successor read"); + } + authorizeRead(conn_hdl); } -void onToRadioWrite(uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) -{ - LOG_INFO("toRadioWriteCb data %p, len %u", data, len); - if (memcmp(lastToRadio, data, len) != 0) { - LOG_DEBUG("New ToRadio packet"); - memcpy(lastToRadio, data, len); - bluetoothPhoneAPI->handleToRadio(data, len); - } else { - LOG_DEBUG("Drop dup ToRadio packet we just saw"); - } +void onToRadioWrite(uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) { + LOG_INFO("toRadioWriteCb data %p, len %u", data, len); + if (memcmp(lastToRadio, data, len) != 0) { + LOG_DEBUG("New ToRadio packet"); + memcpy(lastToRadio, data, len); + bluetoothPhoneAPI->handleToRadio(data, len); + } else { + LOG_DEBUG("Drop dup ToRadio packet we just saw"); + } } -void setupMeshService(void) -{ - bluetoothPhoneAPI = new BluetoothPhoneAPI(); - meshBleService.begin(); - // Note: You must call .begin() on the BLEService before calling .begin() on - // any characteristic(s) within that service definition.. Calling .begin() on - // a BLECharacteristic will cause it to be added to the last BLEService that - // was 'begin()'ed! - auto secMode = - config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN ? SECMODE_OPEN : SECMODE_ENC_NO_MITM; - fromNum.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); - fromNum.setPermission(secMode, SECMODE_NO_ACCESS); // FIXME, secure this!!! - fromNum.setFixedLen( - 0); // Variable len (either 0 or 4) FIXME consider changing protocol so it is fixed 4 byte len, where 0 means empty - fromNum.setMaxLen(4); - fromNum.setCccdWriteCallback(onCccd); // Optionally capture CCCD updates - // We don't yet need to hook the fromNum auth callback - // fromNum.setReadAuthorizeCallback(fromNumAuthorizeCb); - fromNum.write32(0); // Provide default fromNum of 0 - fromNum.begin(); +void setupMeshService(void) { + bluetoothPhoneAPI = new BluetoothPhoneAPI(); + meshBleService.begin(); + // Note: You must call .begin() on the BLEService before calling .begin() on + // any characteristic(s) within that service definition.. Calling .begin() on + // a BLECharacteristic will cause it to be added to the last BLEService that + // was 'begin()'ed! + auto secMode = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN ? SECMODE_OPEN : SECMODE_ENC_NO_MITM; + fromNum.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); + fromNum.setPermission(secMode, SECMODE_NO_ACCESS); // FIXME, secure this!!! + fromNum.setFixedLen(0); // Variable len (either 0 or 4) FIXME consider changing protocol so it is fixed 4 byte len, + // where 0 means empty + fromNum.setMaxLen(4); + fromNum.setCccdWriteCallback(onCccd); // Optionally capture CCCD updates + // We don't yet need to hook the fromNum auth callback + // fromNum.setReadAuthorizeCallback(fromNumAuthorizeCb); + fromNum.write32(0); // Provide default fromNum of 0 + fromNum.begin(); - fromRadio.setProperties(CHR_PROPS_READ); - fromRadio.setPermission(secMode, SECMODE_NO_ACCESS); - fromRadio.setMaxLen(sizeof(fromRadioBytes)); - fromRadio.setReadAuthorizeCallback( - onFromRadioAuthorize, - false); // We don't call this callback via the adafruit queue, because we can safely run in the BLE context - fromRadio.setBuffer(fromRadioBytes, sizeof(fromRadioBytes)); // we preallocate our fromradio buffer so we won't waste space - // for two copies - fromRadio.begin(); + fromRadio.setProperties(CHR_PROPS_READ); + fromRadio.setPermission(secMode, SECMODE_NO_ACCESS); + fromRadio.setMaxLen(sizeof(fromRadioBytes)); + fromRadio.setReadAuthorizeCallback(onFromRadioAuthorize, + false); // We don't call this callback via the adafruit queue, because we can safely run in the BLE context + fromRadio.setBuffer(fromRadioBytes, + sizeof(fromRadioBytes)); // we preallocate our fromradio buffer so we won't waste space + // for two copies + fromRadio.begin(); - toRadio.setProperties(CHR_PROPS_WRITE); - toRadio.setPermission(secMode, secMode); // FIXME secure this! - toRadio.setFixedLen(0); - toRadio.setMaxLen(512); - toRadio.setBuffer(toRadioBytes, sizeof(toRadioBytes)); - // We don't call this callback via the adafruit queue, because we can safely run in the BLE context - toRadio.setWriteCallback(onToRadioWrite, false); - toRadio.begin(); + toRadio.setProperties(CHR_PROPS_WRITE); + toRadio.setPermission(secMode, secMode); // FIXME secure this! + toRadio.setFixedLen(0); + toRadio.setMaxLen(512); + toRadio.setBuffer(toRadioBytes, sizeof(toRadioBytes)); + // We don't call this callback via the adafruit queue, because we can safely run in the BLE context + toRadio.setWriteCallback(onToRadioWrite, false); + toRadio.begin(); - logRadio.setProperties(CHR_PROPS_INDICATE | CHR_PROPS_NOTIFY | CHR_PROPS_READ); - logRadio.setPermission(secMode, SECMODE_NO_ACCESS); - logRadio.setMaxLen(512); - logRadio.setCccdWriteCallback(onCccd); - logRadio.write32(0); - logRadio.begin(); + logRadio.setProperties(CHR_PROPS_INDICATE | CHR_PROPS_NOTIFY | CHR_PROPS_READ); + logRadio.setPermission(secMode, SECMODE_NO_ACCESS); + logRadio.setMaxLen(512); + logRadio.setCccdWriteCallback(onCccd); + logRadio.write32(0); + logRadio.begin(); } static uint32_t configuredPasskey; -void NRF52Bluetooth::shutdown() -{ - // Shutdown bluetooth for minimum power draw - LOG_INFO("Disable NRF52 bluetooth"); - Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset) - disconnect(); - Bluefruit.Advertising.stop(); +void NRF52Bluetooth::shutdown() { + // Shutdown bluetooth for minimum power draw + LOG_INFO("Disable NRF52 bluetooth"); + Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset) + disconnect(); + Bluefruit.Advertising.stop(); } -void NRF52Bluetooth::startDisabled() -{ - // Setup Bluetooth - nrf52Bluetooth->setup(); - // Shutdown bluetooth for minimum power draw - Bluefruit.Advertising.stop(); - Bluefruit.setTxPower(-40); // Minimum power - LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)"); +void NRF52Bluetooth::startDisabled() { + // Setup Bluetooth + nrf52Bluetooth->setup(); + // Shutdown bluetooth for minimum power draw + Bluefruit.Advertising.stop(); + Bluefruit.setTxPower(-40); // Minimum power + LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)"); } -bool NRF52Bluetooth::isConnected() -{ - return Bluefruit.connected(connectionHandle); +bool NRF52Bluetooth::isConnected() { return Bluefruit.connected(connectionHandle); } +int NRF52Bluetooth::getRssi() { + return 0; // FIXME figure out where to source this } -int NRF52Bluetooth::getRssi() -{ - return 0; // FIXME figure out where to source this -} -void NRF52Bluetooth::setup() -{ - // Initialise the Bluefruit module - LOG_INFO("Init the Bluefruit nRF52 module"); - Bluefruit.autoConnLed(false); - Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); - Bluefruit.begin(); - // Clear existing data. - Bluefruit.Advertising.stop(); - Bluefruit.Advertising.clearData(); - Bluefruit.ScanResponse.clearData(); - if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN - ? config.bluetooth.fixed_pin - : random(100000, 999999); - auto pinString = std::to_string(configuredPasskey); - LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); - Bluefruit.Security.setPIN(pinString.c_str()); - Bluefruit.Security.setIOCaps(true, false, false); - Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); - Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); - Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); - meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - } else { - Bluefruit.Security.setIOCaps(false, false, false); - meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); - } - // Set the advertised device name (keep it short!) - Bluefruit.setName(getDeviceName()); - // Set the connect/disconnect callback handlers - Bluefruit.Periph.setConnectCallback(onConnect); - Bluefruit.Periph.setDisconnectCallback(onDisconnect); +void NRF52Bluetooth::setup() { + // Initialise the Bluefruit module + LOG_INFO("Init the Bluefruit nRF52 module"); + Bluefruit.autoConnLed(false); + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.begin(); + // Clear existing data. + Bluefruit.Advertising.stop(); + Bluefruit.Advertising.clearData(); + Bluefruit.ScanResponse.clearData(); + if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + configuredPasskey = + config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN ? config.bluetooth.fixed_pin : random(100000, 999999); + auto pinString = std::to_string(configuredPasskey); + LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); + Bluefruit.Security.setPIN(pinString.c_str()); + Bluefruit.Security.setIOCaps(true, false, false); + Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); + Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); + Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); + meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + } else { + Bluefruit.Security.setIOCaps(false, false, false); + meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); + } + // Set the advertised device name (keep it short!) + Bluefruit.setName(getDeviceName()); + // Set the connect/disconnect callback handlers + Bluefruit.Periph.setConnectCallback(onConnect); + Bluefruit.Periph.setDisconnectCallback(onDisconnect); #ifndef BLE_DFU_SECURE - bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - bledfu.begin(); // Install the DFU helper + bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + bledfu.begin(); // Install the DFU helper #else - bledfusecure.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // add by WayenWeng - bledfusecure.begin(); // Install the DFU helper + bledfusecure.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // add by WayenWeng + bledfusecure.begin(); // Install the DFU helper #endif - // Configure and Start the Device Information Service - LOG_INFO("Init the Device Information Service"); - bledis.setModel(optstr(HW_VERSION)); - bledis.setFirmwareRev(optstr(APP_VERSION)); - bledis.begin(); - // Start the BLE Battery Service and set it to 100% - LOG_INFO("Init the Battery Service"); - blebas.begin(); - blebas.write(0); // Unknown battery level for now - // Setup the Heart Rate Monitor service using - // BLEService and BLECharacteristic classes - LOG_INFO("Init the Mesh bluetooth service"); - setupMeshService(); - // Setup the advertising packet(s) - LOG_INFO("Set up the advertising payload(s)"); - startAdv(); - LOG_INFO("Advertise"); + // Configure and Start the Device Information Service + LOG_INFO("Init the Device Information Service"); + bledis.setModel(optstr(HW_VERSION)); + bledis.setFirmwareRev(optstr(APP_VERSION)); + bledis.begin(); + // Start the BLE Battery Service and set it to 100% + LOG_INFO("Init the Battery Service"); + blebas.begin(); + blebas.write(0); // Unknown battery level for now + // Setup the Heart Rate Monitor service using + // BLEService and BLECharacteristic classes + LOG_INFO("Init the Mesh bluetooth service"); + setupMeshService(); + // Setup the advertising packet(s) + LOG_INFO("Set up the advertising payload(s)"); + startAdv(); + LOG_INFO("Advertise"); } -void NRF52Bluetooth::resumeAdvertising() -{ - Bluefruit.Advertising.restartOnDisconnect(true); - Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms - Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode - Bluefruit.Advertising.start(0); +void NRF52Bluetooth::resumeAdvertising() { + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); } /// Given a level between 0-100, update the BLE attribute -void updateBatteryLevel(uint8_t level) -{ - blebas.write(level); +void updateBatteryLevel(uint8_t level) { blebas.write(level); } +void NRF52Bluetooth::clearBonds() { + LOG_INFO("Clear bluetooth bonds!"); + bond_print_list(BLE_GAP_ROLE_PERIPH); + bond_print_list(BLE_GAP_ROLE_CENTRAL); + Bluefruit.Periph.clearBonds(); + Bluefruit.Central.clearBonds(); } -void NRF52Bluetooth::clearBonds() -{ - LOG_INFO("Clear bluetooth bonds!"); - bond_print_list(BLE_GAP_ROLE_PERIPH); - bond_print_list(BLE_GAP_ROLE_CENTRAL); - Bluefruit.Periph.clearBonds(); - Bluefruit.Central.clearBonds(); -} -void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) -{ - LOG_INFO("BLE connection secured"); -} -bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) -{ - char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; - char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; - LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); - powerFSM.trigger(EVENT_BLUETOOTH_PAIR); +void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) { LOG_INFO("BLE connection secured"); } +bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { + char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; + char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; + LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); + powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - // Get passkey as string - // Note: possible leading zeros - std::string textkey; - for (uint8_t i = 0; i < 6; i++) - textkey += (char)passkey[i]; + // Get passkey as string + // Note: possible leading zeros + std::string textkey; + for (uint8_t i = 0; i < 6; i++) + textkey += (char)passkey[i]; - // Notify UI (or other components) of pairing event and passkey - meshtastic::BluetoothStatus newStatus(textkey); - bluetoothStatus->updateStatus(&newStatus); + // Notify UI (or other components) of pairing event and passkey + meshtastic::BluetoothStatus newStatus(textkey); + bluetoothStatus->updateStatus(&newStatus); -#if HAS_SCREEN && \ - !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (screen) { - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 12; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); +#if HAS_SCREEN && !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and + // observe bluetoothStatus + if (screen) { + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); - } + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif - if (match_request) { - uint32_t start_time = millis(); - while (millis() < start_time + 30000) { - if (!Bluefruit.connected(conn_handle)) - break; - } + if (match_request) { + uint32_t start_time = millis(); + while (millis() < start_time + 30000) { + if (!Bluefruit.connected(conn_handle)) + break; } - LOG_INFO("BLE passkey pair: match_request=%i", match_request); - return true; + } + LOG_INFO("BLE passkey pair: match_request=%i", match_request); + return true; } // Actively refuse new BLE pairings -// After clearing bonds (at factory reset), clients seem initially able to attempt to re-pair, even with advertising disabled. -// On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any connection attempts. -bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) -{ - NRF52Bluetooth::disconnect(); - return false; +// After clearing bonds (at factory reset), clients seem initially able to attempt to re-pair, even with advertising +// disabled. On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any +// connection attempts. +bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { + NRF52Bluetooth::disconnect(); + return false; } // Disconnect any BLE connections -void NRF52Bluetooth::disconnect() -{ - uint8_t connection_num = Bluefruit.connected(); - if (connection_num) { - // Close all connections. We're only expecting one. - for (uint8_t i = 0; i < connection_num; i++) - Bluefruit.disconnect(i); +void NRF52Bluetooth::disconnect() { + uint8_t connection_num = Bluefruit.connected(); + if (connection_num) { + // Close all connections. We're only expecting one. + for (uint8_t i = 0; i < connection_num; i++) + Bluefruit.disconnect(i); - // Wait for disconnection - while (Bluefruit.connected()) - yield(); + // Wait for disconnection + while (Bluefruit.connected()) + yield(); - LOG_INFO("Ended BLE connection"); - } + LOG_INFO("Ended BLE connection"); + } } -void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) -{ - if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { - LOG_INFO("BLE pair success"); - meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newConnectedStatus); - } else { - LOG_INFO("BLE pair failed"); - // Notify UI (or any other interested firmware components) - meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newDisconnectedStatus); - } +void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) { + if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { + LOG_INFO("BLE pair success"); + meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newConnectedStatus); + } else { + LOG_INFO("BLE pair failed"); + // Notify UI (or any other interested firmware components) + meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newDisconnectedStatus); + } - // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (screen) { - screen->endAlert(); - } + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + if (screen) { + screen->endAlert(); + } } -void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) -{ - if (!isConnected() || length > 512) - return; - if (logRadio.indicateEnabled()) - logRadio.indicate(logMessage, (uint16_t)length); - else - logRadio.notify(logMessage, (uint16_t)length); +void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) { + if (!isConnected() || length > 512) + return; + if (logRadio.indicateEnabled()) + logRadio.indicate(logMessage, (uint16_t)length); + else + logRadio.notify(logMessage, (uint16_t)length); } \ No newline at end of file diff --git a/src/platform/nrf52/NRF52Bluetooth.h b/src/platform/nrf52/NRF52Bluetooth.h index 630ab05bc..8896729c9 100644 --- a/src/platform/nrf52/NRF52Bluetooth.h +++ b/src/platform/nrf52/NRF52Bluetooth.h @@ -3,23 +3,22 @@ #include "BluetoothCommon.h" #include -class NRF52Bluetooth : BluetoothApi -{ - public: - void setup(); - void shutdown(); - void startDisabled(); - void resumeAdvertising(); - void clearBonds(); - bool isConnected(); - int getRssi(); - void sendLog(const uint8_t *logMessage, size_t length); +class NRF52Bluetooth : BluetoothApi { +public: + void setup(); + void shutdown(); + void startDisabled(); + void resumeAdvertising(); + void clearBonds(); + bool isConnected(); + int getRssi(); + void sendLog(const uint8_t *logMessage, size_t length); - private: - static void onConnectionSecured(uint16_t conn_handle); - static bool onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); - static void onPairingCompleted(uint16_t conn_handle, uint8_t auth_status); +private: + static void onConnectionSecured(uint16_t conn_handle); + static bool onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); + static void onPairingCompleted(uint16_t conn_handle, uint8_t auth_status); - static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); - static void disconnect(); + static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); + static void disconnect(); }; \ No newline at end of file diff --git a/src/platform/nrf52/NRF52CryptoEngine.cpp b/src/platform/nrf52/NRF52CryptoEngine.cpp index 5de13c58b..2788d3f66 100644 --- a/src/platform/nrf52/NRF52CryptoEngine.cpp +++ b/src/platform/nrf52/NRF52CryptoEngine.cpp @@ -2,31 +2,29 @@ #include "aes-256/tiny-aes.h" #include "configuration.h" #include -class NRF52CryptoEngine : public CryptoEngine -{ - public: - NRF52CryptoEngine() {} +class NRF52CryptoEngine : public CryptoEngine { +public: + NRF52CryptoEngine() {} - ~NRF52CryptoEngine() {} + ~NRF52CryptoEngine() {} - virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override - { - if (_key.length > 16) { - AES_ctx ctx; - AES_init_ctx_iv(&ctx, _key.bytes, _nonce); - AES_CTR_xcrypt_buffer(&ctx, bytes, numBytes); - } else if (_key.length > 0) { - nRFCrypto.begin(); - nRFCrypto_AES ctx; - uint8_t myLen = ctx.blockLen(numBytes); - char encBuf[myLen] = {0}; - ctx.begin(); - ctx.Process((char *)bytes, numBytes, _nonce, _key.bytes, _key.length, encBuf, ctx.encryptFlag, ctx.ctrMode); - ctx.end(); - nRFCrypto.end(); - memcpy(bytes, encBuf, numBytes); - } + virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override { + if (_key.length > 16) { + AES_ctx ctx; + AES_init_ctx_iv(&ctx, _key.bytes, _nonce); + AES_CTR_xcrypt_buffer(&ctx, bytes, numBytes); + } else if (_key.length > 0) { + nRFCrypto.begin(); + nRFCrypto_AES ctx; + uint8_t myLen = ctx.blockLen(numBytes); + char encBuf[myLen] = {0}; + ctx.begin(); + ctx.Process((char *)bytes, numBytes, _nonce, _key.bytes, _key.length, encBuf, ctx.encryptFlag, ctx.ctrMode); + ctx.end(); + nRFCrypto.end(); + memcpy(bytes, encBuf, numBytes); } + } }; CryptoEngine *crypto = new NRF52CryptoEngine(); \ No newline at end of file diff --git a/src/platform/nrf52/aes-256/tiny-aes.cpp b/src/platform/nrf52/aes-256/tiny-aes.cpp index 20a7344e1..bc3e7c6a7 100644 --- a/src/platform/nrf52/aes-256/tiny-aes.cpp +++ b/src/platform/nrf52/aes-256/tiny-aes.cpp @@ -19,198 +19,179 @@ typedef uint8_t state_t[4][4]; static const uint8_t sbox[256] = { // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, - 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, - 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, - 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, - 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, - 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, - 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, - 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, - 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, - 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, - 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, - 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, + 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, + 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, + 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, + 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, + 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16}; static const uint8_t Rcon[11] = {0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36}; #define getSBoxValue(num) (sbox[(num)]) -static void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key) -{ - uint8_t tempa[4]; +static void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key) { + uint8_t tempa[4]; - for (unsigned i = 0; i < Nk; ++i) { - RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; - RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; - RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; - RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + for (unsigned i = 0; i < Nk; ++i) { + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + } + + for (unsigned i = Nk; i < Nb * (Nr + 1); ++i) { + unsigned k = (i - 1) * 4; + tempa[0] = RoundKey[k + 0]; + tempa[1] = RoundKey[k + 1]; + tempa[2] = RoundKey[k + 2]; + tempa[3] = RoundKey[k + 3]; + + if (i % Nk == 0) { + const uint8_t u8tmp = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = u8tmp; + + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + + tempa[0] = tempa[0] ^ Rcon[i / Nk]; } - for (unsigned i = Nk; i < Nb * (Nr + 1); ++i) { - unsigned k = (i - 1) * 4; - tempa[0] = RoundKey[k + 0]; - tempa[1] = RoundKey[k + 1]; - tempa[2] = RoundKey[k + 2]; - tempa[3] = RoundKey[k + 3]; + if (i % Nk == 4) { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } - if (i % Nk == 0) { - const uint8_t u8tmp = tempa[0]; - tempa[0] = tempa[1]; - tempa[1] = tempa[2]; - tempa[2] = tempa[3]; - tempa[3] = u8tmp; + unsigned j = i * 4; + k = (i - Nk) * 4; + RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; + RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; + RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; + RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; + } +} - tempa[0] = getSBoxValue(tempa[0]); - tempa[1] = getSBoxValue(tempa[1]); - tempa[2] = getSBoxValue(tempa[2]); - tempa[3] = getSBoxValue(tempa[3]); +void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key) { KeyExpansion(ctx->RoundKey, key); } +void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8_t *iv) { + KeyExpansion(ctx->RoundKey, key); + memcpy(ctx->Iv, iv, AES_BLOCKLEN); +} +void AES_ctx_set_iv(struct AES_ctx *ctx, const uint8_t *iv) { memcpy(ctx->Iv, iv, AES_BLOCKLEN); } - tempa[0] = tempa[0] ^ Rcon[i / Nk]; +static void AddRoundKey(uint8_t round, state_t *state, const uint8_t *RoundKey) { + for (uint8_t i = 0; i < 4; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; + } + } +} + +static void SubBytes(state_t *state) { + for (uint8_t i = 0; i < 4; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + (*state)[j][i] = getSBoxValue((*state)[j][i]); + } + } +} + +static void ShiftRows(state_t *state) { + uint8_t temp = (*state)[0][1]; + (*state)[0][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[3][1]; + (*state)[3][1] = temp; + + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[3][3]; + (*state)[3][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[1][3]; + (*state)[1][3] = temp; +} + +static uint8_t xtime(uint8_t x) { return ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); } + +static void MixColumns(state_t *state) { + for (uint8_t i = 0; i < 4; ++i) { + uint8_t t = (*state)[i][0]; + uint8_t Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3]; + uint8_t Tm = (*state)[i][0] ^ (*state)[i][1]; + Tm = xtime(Tm); + (*state)[i][0] ^= Tm ^ Tmp; + Tm = (*state)[i][1] ^ (*state)[i][2]; + Tm = xtime(Tm); + (*state)[i][1] ^= Tm ^ Tmp; + Tm = (*state)[i][2] ^ (*state)[i][3]; + Tm = xtime(Tm); + (*state)[i][2] ^= Tm ^ Tmp; + Tm = (*state)[i][3] ^ t; + Tm = xtime(Tm); + (*state)[i][3] ^= Tm ^ Tmp; + } +} + +#define Multiply(x, y) \ + (((y & 1) * x) ^ ((y >> 1 & 1) * xtime(x)) ^ ((y >> 2 & 1) * xtime(xtime(x))) ^ ((y >> 3 & 1) * xtime(xtime(xtime(x)))) ^ \ + ((y >> 4 & 1) * xtime(xtime(xtime(xtime(x)))))) + +static void Cipher(state_t *state, const uint8_t *RoundKey) { + uint8_t round = 0; + + AddRoundKey(0, state, RoundKey); + + for (round = 1;; ++round) { + SubBytes(state); + ShiftRows(state); + if (round == Nr) { + break; + } + MixColumns(state); + AddRoundKey(round, state, RoundKey); + } + AddRoundKey(Nr, state, RoundKey); +} + +void AES_CTR_xcrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length) { + uint8_t buffer[AES_BLOCKLEN]; + + size_t i; + int bi; + for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) { + if (bi == AES_BLOCKLEN) { + + memcpy(buffer, ctx->Iv, AES_BLOCKLEN); + Cipher((state_t *)buffer, ctx->RoundKey); + + for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) { + if (ctx->Iv[bi] == 255) { + ctx->Iv[bi] = 0; + continue; } - - if (i % Nk == 4) { - tempa[0] = getSBoxValue(tempa[0]); - tempa[1] = getSBoxValue(tempa[1]); - tempa[2] = getSBoxValue(tempa[2]); - tempa[3] = getSBoxValue(tempa[3]); - } - - unsigned j = i * 4; - k = (i - Nk) * 4; - RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; - RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; - RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; - RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; - } -} - -void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key) -{ - KeyExpansion(ctx->RoundKey, key); -} -void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8_t *iv) -{ - KeyExpansion(ctx->RoundKey, key); - memcpy(ctx->Iv, iv, AES_BLOCKLEN); -} -void AES_ctx_set_iv(struct AES_ctx *ctx, const uint8_t *iv) -{ - memcpy(ctx->Iv, iv, AES_BLOCKLEN); -} - -static void AddRoundKey(uint8_t round, state_t *state, const uint8_t *RoundKey) -{ - for (uint8_t i = 0; i < 4; ++i) { - for (uint8_t j = 0; j < 4; ++j) { - (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; - } - } -} - -static void SubBytes(state_t *state) -{ - for (uint8_t i = 0; i < 4; ++i) { - for (uint8_t j = 0; j < 4; ++j) { - (*state)[j][i] = getSBoxValue((*state)[j][i]); - } - } -} - -static void ShiftRows(state_t *state) -{ - uint8_t temp = (*state)[0][1]; - (*state)[0][1] = (*state)[1][1]; - (*state)[1][1] = (*state)[2][1]; - (*state)[2][1] = (*state)[3][1]; - (*state)[3][1] = temp; - - temp = (*state)[0][2]; - (*state)[0][2] = (*state)[2][2]; - (*state)[2][2] = temp; - - temp = (*state)[1][2]; - (*state)[1][2] = (*state)[3][2]; - (*state)[3][2] = temp; - - temp = (*state)[0][3]; - (*state)[0][3] = (*state)[3][3]; - (*state)[3][3] = (*state)[2][3]; - (*state)[2][3] = (*state)[1][3]; - (*state)[1][3] = temp; -} - -static uint8_t xtime(uint8_t x) -{ - return ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); -} - -static void MixColumns(state_t *state) -{ - for (uint8_t i = 0; i < 4; ++i) { - uint8_t t = (*state)[i][0]; - uint8_t Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3]; - uint8_t Tm = (*state)[i][0] ^ (*state)[i][1]; - Tm = xtime(Tm); - (*state)[i][0] ^= Tm ^ Tmp; - Tm = (*state)[i][1] ^ (*state)[i][2]; - Tm = xtime(Tm); - (*state)[i][1] ^= Tm ^ Tmp; - Tm = (*state)[i][2] ^ (*state)[i][3]; - Tm = xtime(Tm); - (*state)[i][2] ^= Tm ^ Tmp; - Tm = (*state)[i][3] ^ t; - Tm = xtime(Tm); - (*state)[i][3] ^= Tm ^ Tmp; - } -} - -#define Multiply(x, y) \ - (((y & 1) * x) ^ ((y >> 1 & 1) * xtime(x)) ^ ((y >> 2 & 1) * xtime(xtime(x))) ^ ((y >> 3 & 1) * xtime(xtime(xtime(x)))) ^ \ - ((y >> 4 & 1) * xtime(xtime(xtime(xtime(x)))))) - -static void Cipher(state_t *state, const uint8_t *RoundKey) -{ - uint8_t round = 0; - - AddRoundKey(0, state, RoundKey); - - for (round = 1;; ++round) { - SubBytes(state); - ShiftRows(state); - if (round == Nr) { - break; - } - MixColumns(state); - AddRoundKey(round, state, RoundKey); - } - AddRoundKey(Nr, state, RoundKey); -} - -void AES_CTR_xcrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length) -{ - uint8_t buffer[AES_BLOCKLEN]; - - size_t i; - int bi; - for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) { - if (bi == AES_BLOCKLEN) { - - memcpy(buffer, ctx->Iv, AES_BLOCKLEN); - Cipher((state_t *)buffer, ctx->RoundKey); - - for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) { - if (ctx->Iv[bi] == 255) { - ctx->Iv[bi] = 0; - continue; - } - ctx->Iv[bi] += 1; - break; - } - bi = 0; - } - - buf[i] = (buf[i] ^ buffer[bi]); + ctx->Iv[bi] += 1; + break; + } + bi = 0; } + + buf[i] = (buf[i] ^ buffer[bi]); + } } diff --git a/src/platform/nrf52/aes-256/tiny-aes.h b/src/platform/nrf52/aes-256/tiny-aes.h index ef78e0aeb..080f8996b 100644 --- a/src/platform/nrf52/aes-256/tiny-aes.h +++ b/src/platform/nrf52/aes-256/tiny-aes.h @@ -9,8 +9,8 @@ #define AES_keyExpSize 240 struct AES_ctx { - uint8_t RoundKey[AES_keyExpSize]; - uint8_t Iv[AES_BLOCKLEN]; + uint8_t RoundKey[AES_keyExpSize]; + uint8_t Iv[AES_BLOCKLEN]; }; void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key); diff --git a/src/platform/nrf52/alloc.cpp b/src/platform/nrf52/alloc.cpp index 0a610bbe3..af4eb62ec 100644 --- a/src/platform/nrf52/alloc.cpp +++ b/src/platform/nrf52/alloc.cpp @@ -7,26 +7,18 @@ * Custom new/delete to panic if out out memory */ -void *operator new(size_t size) -{ - auto p = rtos_malloc(size); - assert(p); - return p; +void *operator new(size_t size) { + auto p = rtos_malloc(size); + assert(p); + return p; } -void *operator new[](size_t size) -{ - auto p = rtos_malloc(size); - assert(p); - return p; +void *operator new[](size_t size) { + auto p = rtos_malloc(size); + assert(p); + return p; } -void operator delete(void *ptr) -{ - rtos_free(ptr); -} +void operator delete(void *ptr) { rtos_free(ptr); } -void operator delete[](void *ptr) -{ - rtos_free(ptr); -} \ No newline at end of file +void operator delete[](void *ptr) { rtos_free(ptr); } \ No newline at end of file diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index d4699cd8c..7412182ab 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -155,8 +155,8 @@ // Debug printing to segger console #define SEGGER_MSG(...) SEGGER_RTT_printf(SEGGER_STDOUT_CH, __VA_ARGS__) -// If we are not on a NRF52840 (which has built in USB-ACM serial support) and we don't have serial pins hooked up, then we MUST -// use SEGGER for debug output +// If we are not on a NRF52840 (which has built in USB-ACM serial support) and we don't have serial pins hooked up, then +// we MUST use SEGGER for debug output #if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) // No serial ports on this board - ONLY use segger in memory console #define USE_SEGGER diff --git a/src/platform/nrf52/hardfault.cpp b/src/platform/nrf52/hardfault.cpp index 7b7a718b8..e6eb2b97b 100644 --- a/src/platform/nrf52/hardfault.cpp +++ b/src/platform/nrf52/hardfault.cpp @@ -1,94 +1,90 @@ #include "configuration.h" #include -// Based on reading/modifying https://blog.feabhas.com/2013/02/developing-a-generic-hard-fault-handler-for-arm-cortex-m3cortex-m4/ +// Based on reading/modifying +// https://blog.feabhas.com/2013/02/developing-a-generic-hard-fault-handler-for-arm-cortex-m3cortex-m4/ enum { r0, r1, r2, r3, r12, lr, pc, psr }; -// we can't use the regular LOG_DEBUG for these crash dumps because it depends on threading still being running. Instead use the -// segger in memory tool +// we can't use the regular LOG_DEBUG for these crash dumps because it depends on threading still being running. Instead +// use the segger in memory tool #define FAULT_MSG(...) SEGGER_MSG(__VA_ARGS__) // Per http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/Cihcfefj.html -static void printUsageErrorMsg(uint32_t cfsr) -{ - FAULT_MSG("Usage fault: "); - cfsr >>= SCB_CFSR_USGFAULTSR_Pos; // right shift to lsb - if ((cfsr & (1 << 9)) != 0) - FAULT_MSG("Divide by zero\n"); - else if ((cfsr & (1 << 8)) != 0) - FAULT_MSG("Unaligned\n"); - else if ((cfsr & (1 << 1)) != 0) - FAULT_MSG("Invalid state\n"); - else if ((cfsr & (1 << 0)) != 0) - FAULT_MSG("Invalid instruction\n"); - else - FAULT_MSG("FIXME add to printUsageErrorMsg!\n"); +static void printUsageErrorMsg(uint32_t cfsr) { + FAULT_MSG("Usage fault: "); + cfsr >>= SCB_CFSR_USGFAULTSR_Pos; // right shift to lsb + if ((cfsr & (1 << 9)) != 0) + FAULT_MSG("Divide by zero\n"); + else if ((cfsr & (1 << 8)) != 0) + FAULT_MSG("Unaligned\n"); + else if ((cfsr & (1 << 1)) != 0) + FAULT_MSG("Invalid state\n"); + else if ((cfsr & (1 << 0)) != 0) + FAULT_MSG("Invalid instruction\n"); + else + FAULT_MSG("FIXME add to printUsageErrorMsg!\n"); } -static void printBusErrorMsg(uint32_t cfsr) -{ - FAULT_MSG("Bus fault: "); - cfsr >>= SCB_CFSR_BUSFAULTSR_Pos; // right shift to lsb - if ((cfsr & (1 << 0)) != 0) - FAULT_MSG("Instruction bus error\n"); - if ((cfsr & (1 << 1)) != 0) - FAULT_MSG("Precise data bus error\n"); - if ((cfsr & (1 << 2)) != 0) - FAULT_MSG("Imprecise data bus error\n"); +static void printBusErrorMsg(uint32_t cfsr) { + FAULT_MSG("Bus fault: "); + cfsr >>= SCB_CFSR_BUSFAULTSR_Pos; // right shift to lsb + if ((cfsr & (1 << 0)) != 0) + FAULT_MSG("Instruction bus error\n"); + if ((cfsr & (1 << 1)) != 0) + FAULT_MSG("Precise data bus error\n"); + if ((cfsr & (1 << 2)) != 0) + FAULT_MSG("Imprecise data bus error\n"); } -static void printMemErrorMsg(uint32_t cfsr) -{ - FAULT_MSG("Memory fault: "); - cfsr >>= SCB_CFSR_MEMFAULTSR_Pos; // right shift to lsb - if ((cfsr & (1 << 0)) != 0) - FAULT_MSG("Instruction access violation\n"); - if ((cfsr & (1 << 1)) != 0) - FAULT_MSG("Data access violation\n"); +static void printMemErrorMsg(uint32_t cfsr) { + FAULT_MSG("Memory fault: "); + cfsr >>= SCB_CFSR_MEMFAULTSR_Pos; // right shift to lsb + if ((cfsr & (1 << 0)) != 0) + FAULT_MSG("Instruction access violation\n"); + if ((cfsr & (1 << 1)) != 0) + FAULT_MSG("Data access violation\n"); } -extern "C" void HardFault_Impl(uint32_t stack[]) -{ - FAULT_MSG("Hard Fault occurred! SCB->HFSR = 0x%08lx\n", SCB->HFSR); +extern "C" void HardFault_Impl(uint32_t stack[]) { + FAULT_MSG("Hard Fault occurred! SCB->HFSR = 0x%08lx\n", SCB->HFSR); - if ((SCB->HFSR & SCB_HFSR_FORCED_Msk) != 0) { - FAULT_MSG("Forced Hard Fault: SCB->CFSR = 0x%08lx\n", SCB->CFSR); + if ((SCB->HFSR & SCB_HFSR_FORCED_Msk) != 0) { + FAULT_MSG("Forced Hard Fault: SCB->CFSR = 0x%08lx\n", SCB->CFSR); - if ((SCB->CFSR & SCB_CFSR_USGFAULTSR_Msk) != 0) { - printUsageErrorMsg(SCB->CFSR); - } - if ((SCB->CFSR & SCB_CFSR_BUSFAULTSR_Msk) != 0) { - printBusErrorMsg(SCB->CFSR); - } - if ((SCB->CFSR & SCB_CFSR_MEMFAULTSR_Msk) != 0) { - printMemErrorMsg(SCB->CFSR); - } - - FAULT_MSG("r0 = 0x%08lx\n", stack[r0]); - FAULT_MSG("r1 = 0x%08lx\n", stack[r1]); - FAULT_MSG("r2 = 0x%08lx\n", stack[r2]); - FAULT_MSG("r3 = 0x%08lx\n", stack[r3]); - FAULT_MSG("r12 = 0x%08lx\n", stack[r12]); - FAULT_MSG("lr = 0x%08lx\n", stack[lr]); - FAULT_MSG("pc = 0x%08lx\n", stack[pc]); - FAULT_MSG("psr = 0x%08lx\n", stack[psr]); + if ((SCB->CFSR & SCB_CFSR_USGFAULTSR_Msk) != 0) { + printUsageErrorMsg(SCB->CFSR); + } + if ((SCB->CFSR & SCB_CFSR_BUSFAULTSR_Msk) != 0) { + printBusErrorMsg(SCB->CFSR); + } + if ((SCB->CFSR & SCB_CFSR_MEMFAULTSR_Msk) != 0) { + printMemErrorMsg(SCB->CFSR); } - FAULT_MSG("Done with fault report - Waiting to reboot\n"); - asm volatile("bkpt #01"); // Enter the debugger if one is connected + FAULT_MSG("r0 = 0x%08lx\n", stack[r0]); + FAULT_MSG("r1 = 0x%08lx\n", stack[r1]); + FAULT_MSG("r2 = 0x%08lx\n", stack[r2]); + FAULT_MSG("r3 = 0x%08lx\n", stack[r3]); + FAULT_MSG("r12 = 0x%08lx\n", stack[r12]); + FAULT_MSG("lr = 0x%08lx\n", stack[lr]); + FAULT_MSG("pc = 0x%08lx\n", stack[pc]); + FAULT_MSG("psr = 0x%08lx\n", stack[psr]); + } - // Don't spin, so that the debugger will let the user step to next instruction - // while (1) ; + FAULT_MSG("Done with fault report - Waiting to reboot\n"); + asm volatile("bkpt #01"); // Enter the debugger if one is connected + + // Don't spin, so that the debugger will let the user step to next instruction + // while (1) ; } #ifndef INC_FREERTOS_H // This is a generic cortex M entrypoint that doesn't assume freertos -extern "C" void HardFault_Handler(void) -{ - asm volatile(" mrs r0,msp\n" - " b HardFault_Impl \n"); +extern "C" void HardFault_Handler(void) { + asm volatile(" mrs r0,msp\n" + " b HardFault_Impl \n"); } #elif !defined(ARCH_NRF52) @@ -98,15 +94,14 @@ extern "C" void HardFault_Handler(void) __attribute__((naked)); /* The fault handler implementation calls a function called prvGetRegistersFromStack(). */ -extern "C" void HardFault_Handler(void) -{ - __asm volatile(" tst lr, #4 \n" - " ite eq \n" - " mrseq r0, msp \n" - " mrsne r0, psp \n" - " ldr r1, [r0, #24] \n" - " ldr r2, handler2_address_const \n" - " bx r2 \n" - " handler2_address_const: .word HardFault_Impl \n"); +extern "C" void HardFault_Handler(void) { + __asm volatile(" tst lr, #4 \n" + " ite eq \n" + " mrseq r0, msp \n" + " mrsne r0, psp \n" + " ldr r1, [r0, #24] \n" + " ldr r2, handler2_address_const \n" + " bx r2 \n" + " handler2_address_const: .word HardFault_Impl \n"); } #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 472107229..77ca022c7 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -38,101 +38,96 @@ void variant_shutdown() {} static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; -static inline void debugger_break(void) -{ - __asm volatile("bkpt #0x01\n\t" - "mov pc, lr\n\t"); +static inline void debugger_break(void) { + __asm volatile("bkpt #0x01\n\t" + "mov pc, lr\n\t"); } -bool loopCanSleep() -{ - // turn off sleep only while connected via USB - // return true; - return !Serial; // the bool operator on the nrf52 serial class returns true if connected to a PC currently - // return !(TinyUSBDevice.mounted() && !TinyUSBDevice.suspended()); +bool loopCanSleep() { + // turn off sleep only while connected via USB + // return true; + return !Serial; // the bool operator on the nrf52 serial class returns true if connected to a PC currently + // return !(TinyUSBDevice.mounted() && !TinyUSBDevice.suspended()); } // handle standard gcc assert failures -void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) -{ - LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); - // debugger_break(); FIXME doesn't work, possibly not for segger - // Reboot cpu - NVIC_SystemReset(); +void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) { + LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); + // debugger_break(); FIXME doesn't work, possibly not for segger + // Reboot cpu + NVIC_SystemReset(); } -void getMacAddr(uint8_t *dmac) -{ - const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR; - dmac[5] = src[0]; - dmac[4] = src[1]; - dmac[3] = src[2]; - dmac[2] = src[3]; - dmac[1] = src[4]; - dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack +void getMacAddr(uint8_t *dmac) { + const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR; + dmac[5] = src[0]; + dmac[4] = src[1]; + dmac[3] = src[2]; + dmac[2] = src[3]; + dmac[1] = src[4]; + dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack } -static void initBrownout() -{ - auto vccthresh = POWER_POFCON_THRESHOLD_V24; +static void initBrownout() { + auto vccthresh = POWER_POFCON_THRESHOLD_V24; - auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); - assert(err_code == NRF_SUCCESS); + auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); + assert(err_code == NRF_SUCCESS); - err_code = sd_power_pof_threshold_set(vccthresh); - assert(err_code == NRF_SUCCESS); + err_code = sd_power_pof_threshold_set(vccthresh); + assert(err_code == NRF_SUCCESS); - // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice + // We don't bother with setting up brownout if soft device is disabled - because during production we always use + // softdevice } // This is a public global so that the debugger can set it to false automatically from our gdbinit bool useSoftDevice = true; // Set to false for easier debugging #if !MESHTASTIC_EXCLUDE_BLUETOOTH -void setBluetoothEnable(bool enable) -{ - // For debugging use: don't use bluetooth - if (!useSoftDevice) { - if (enable) - LOG_INFO("Disable NRF52 BLUETOOTH WHILE DEBUGGING"); - return; - } +void setBluetoothEnable(bool enable) { + // For debugging use: don't use bluetooth + if (!useSoftDevice) { + if (enable) + LOG_INFO("Disable NRF52 BLUETOOTH WHILE DEBUGGING"); + return; + } - // If user disabled bluetooth: init then disable advertising & reduce power - // Workaround. Avoid issue where device hangs several days after boot.. - // Allegedly, no significant increase in power consumption - if (!config.bluetooth.enabled) { - static bool initialized = false; - if (!initialized) { - nrf52Bluetooth = new NRF52Bluetooth(); - nrf52Bluetooth->startDisabled(); - initBrownout(); - initialized = true; - } - return; + // If user disabled bluetooth: init then disable advertising & reduce power + // Workaround. Avoid issue where device hangs several days after boot.. + // Allegedly, no significant increase in power consumption + if (!config.bluetooth.enabled) { + static bool initialized = false; + if (!initialized) { + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->startDisabled(); + initBrownout(); + initialized = true; } + return; + } - if (enable) { - powerMon->setState(meshtastic_PowerMon_State_BT_On); + if (enable) { + powerMon->setState(meshtastic_PowerMon_State_BT_On); - // If not yet set-up - if (!nrf52Bluetooth) { - LOG_DEBUG("Init NRF52 Bluetooth"); - nrf52Bluetooth = new NRF52Bluetooth(); - nrf52Bluetooth->setup(); + // If not yet set-up + if (!nrf52Bluetooth) { + LOG_DEBUG("Init NRF52 Bluetooth"); + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->setup(); - // We delay brownout init until after BLE because BLE starts soft device - initBrownout(); - } - // Already setup, apparently - else - nrf52Bluetooth->resumeAdvertising(); - } - // Disable (if previously set-up) - else if (nrf52Bluetooth) { - powerMon->clearState(meshtastic_PowerMon_State_BT_On); - nrf52Bluetooth->shutdown(); + // We delay brownout init until after BLE because BLE starts soft device + initBrownout(); } + // Already setup, apparently + else + nrf52Bluetooth->resumeAdvertising(); + } + // Disable (if previously set-up) + else if (nrf52Bluetooth) { + powerMon->clearState(meshtastic_PowerMon_State_BT_On); + nrf52Bluetooth->shutdown(); + } } #else #warning NRF52 "Bluetooth disable" workaround does not apply to builds with MESHTASTIC_EXCLUDE_BLUETOOTH @@ -141,97 +136,90 @@ void setBluetoothEnable(bool enable) {} /** * Override printf to use the SEGGER output library (note - this does not effect the printf method on the debug console) */ -int printf(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - auto res = SEGGER_RTT_vprintf(0, fmt, &args); - va_end(args); - return res; +int printf(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + auto res = SEGGER_RTT_vprintf(0, fmt, &args); + va_end(args); + return res; } -namespace -{ +namespace { constexpr uint8_t NRF52_MAGIC_LFS_IS_CORRUPT = 0xF5; constexpr uint32_t MULTIPLE_CORRUPTION_DELAY_MILLIS = 20 * 60 * 1000; static unsigned long millis_until_formatting_again = 0; // Report the critical error from loop(), giving a chance for the screen to be initialized first. -inline void reportLittleFSCorruptionOnce() -{ - static bool report_corruption = !!millis_until_formatting_again; - if (report_corruption) { - report_corruption = false; - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); - } +inline void reportLittleFSCorruptionOnce() { + static bool report_corruption = !!millis_until_formatting_again; + if (report_corruption) { + report_corruption = false; + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + } } } // namespace -void preFSBegin() -{ - // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET - // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. - if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) - return; - NRF_POWER->GPREGRET = 0; - millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS; - InternalFS.format(); - LOG_INFO("LittleFS format complete; restoring default settings"); +void preFSBegin() { + // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET + // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. + if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) + return; + NRF_POWER->GPREGRET = 0; + millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS; + InternalFS.format(); + LOG_INFO("LittleFS format complete; restoring default settings"); } -extern "C" void lfs_assert(const char *reason) -{ - LOG_ERROR("LittleFS corruption detected: %s", reason); - if (millis_until_formatting_again > millis()) { - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); - const long millis_remain = millis_until_formatting_again - millis(); - LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000); - delay(millis_remain); - } - LOG_INFO("Rebooting to format LittleFS"); - delay(500); // Give the serial port a bit of time to output that last message. - // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set - // NRF_POWER->GPREGRET directly. - if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { - NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; - } - NVIC_SystemReset(); +extern "C" void lfs_assert(const char *reason) { + LOG_ERROR("LittleFS corruption detected: %s", reason); + if (millis_until_formatting_again > millis()) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + const long millis_remain = millis_until_formatting_again - millis(); + LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000); + delay(millis_remain); + } + LOG_INFO("Rebooting to format LittleFS"); + delay(500); // Give the serial port a bit of time to output that last message. + // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) + // then set NRF_POWER->GPREGRET directly. + if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { + NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + } + NVIC_SystemReset(); } -void checkSDEvents() -{ - if (useSoftDevice) { - uint32_t evt; - while (NRF_SUCCESS == sd_evt_get(&evt)) { - switch (evt) { - case NRF_EVT_POWER_FAILURE_WARNING: - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); - break; +void checkSDEvents() { + if (useSoftDevice) { + uint32_t evt; + while (NRF_SUCCESS == sd_evt_get(&evt)) { + switch (evt) { + case NRF_EVT_POWER_FAILURE_WARNING: + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); + break; - default: - LOG_DEBUG("Unexpected SDevt %d", evt); - break; - } - } - } else { - if (NRF_POWER->EVENTS_POFWARN) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); + default: + LOG_DEBUG("Unexpected SDevt %d", evt); + break; + } } + } else { + if (NRF_POWER->EVENTS_POFWARN) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); + } } -void nrf52Loop() -{ - { - static bool watchdog_running = false; - if (!watchdog_running) { - nrfx_wdt_enable(&nrfx_wdt); - watchdog_running = true; - } +void nrf52Loop() { + { + static bool watchdog_running = false; + if (!watchdog_running) { + nrfx_wdt_enable(&nrfx_wdt); + watchdog_running = true; } - nrfx_wdt_channel_feed(&nrfx_wdt, nrfx_wdt_channel_id_nrf52_main); + } + nrfx_wdt_channel_feed(&nrfx_wdt, nrfx_wdt_channel_id_nrf52_main); - checkSDEvents(); - reportLittleFSCorruptionOnce(); + checkSDEvents(); + reportLittleFSCorruptionOnce(); } #ifdef USE_SEMIHOSTING @@ -247,249 +235,243 @@ bool wantSemihost; /** * Turn on semihosting if the ICE debugger wants it. */ -void nrf52InitSemiHosting() -{ - if (wantSemihost) { - static SemihostingStream semiStream; - // We must dynamically alloc because the constructor does semihost operations which - // would crash any load not talking to a debugger - semiStream.open(); - semiStream.println("Semihosting starts!"); - // Redirect our serial output to instead go via the ICE port - console->setDestination(&semiStream); - } +void nrf52InitSemiHosting() { + if (wantSemihost) { + static SemihostingStream semiStream; + // We must dynamically alloc because the constructor does semihost operations which + // would crash any load not talking to a debugger + semiStream.open(); + semiStream.println("Semihosting starts!"); + // Redirect our serial output to instead go via the ICE port + console->setDestination(&semiStream); + } } #endif -void nrf52Setup() -{ +void nrf52Setup() { #ifdef ADC_V - pinMode(ADC_V, INPUT); + pinMode(ADC_V, INPUT); #endif - uint32_t why = NRF_POWER->RESETREAS; - // per - // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpower.html - LOG_DEBUG("Reset reason: 0x%x", why); + uint32_t why = NRF_POWER->RESETREAS; + // per + // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpower.html + LOG_DEBUG("Reset reason: 0x%x", why); #ifdef USE_SEMIHOSTING - nrf52InitSemiHosting(); + nrf52InitSemiHosting(); #endif - // Per - // https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/monitor-mode-debugging-with-j-link-and-gdbeclipse - // This is the recommended setting for Monitor Mode Debugging - NVIC_SetPriority(DebugMonitor_IRQn, 6UL); + // Per + // https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/monitor-mode-debugging-with-j-link-and-gdbeclipse + // This is the recommended setting for Monitor Mode Debugging + NVIC_SetPriority(DebugMonitor_IRQn, 6UL); #ifdef BQ25703A_ADDR - auto *bq = new BQ25713(); - if (!bq->setup()) - LOG_ERROR("ERROR! Charge controller init failed"); + auto *bq = new BQ25713(); + if (!bq->setup()) + LOG_ERROR("ERROR! Charge controller init failed"); #endif - // Init random seed - union seedParts { - uint32_t seed32; - uint8_t seed8[4]; - } seed; - nRFCrypto.begin(); - nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8)); - LOG_DEBUG("Set random seed %u", seed.seed32); - randomSeed(seed.seed32); - nRFCrypto.end(); + // Init random seed + union seedParts { + uint32_t seed32; + uint8_t seed8[4]; + } seed; + nRFCrypto.begin(); + nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8)); + LOG_DEBUG("Set random seed %u", seed.seed32); + randomSeed(seed.seed32); + nRFCrypto.end(); - // Set up nrfx watchdog. Do not enable the watchdog yet (we do that - // the first time through the main loop), so that other threads can - // allocate their own wdt channel to protect themselves from hangs. - nrfx_wdt_config_t wdt0_config = { - .behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000, - // Note: Not using wdt interrupts. - // .interrupt_priority = NRFX_WDT_DEFAULT_CONFIG_IRQ_PRIORITY - }; - nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, - nullptr // Watchdog event handler, not used, we just reset. - ); - assert(r == NRFX_SUCCESS); + // Set up nrfx watchdog. Do not enable the watchdog yet (we do that + // the first time through the main loop), so that other threads can + // allocate their own wdt channel to protect themselves from hangs. + nrfx_wdt_config_t wdt0_config = { + .behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000, + // Note: Not using wdt interrupts. + // .interrupt_priority = NRFX_WDT_DEFAULT_CONFIG_IRQ_PRIORITY + }; + nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, + nullptr // Watchdog event handler, not used, we just reset. + ); + assert(r == NRFX_SUCCESS); - r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main); - assert(r == NRFX_SUCCESS); + r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main); + assert(r == NRFX_SUCCESS); } -void cpuDeepSleep(uint32_t msecToWake) -{ - // FIXME, configure RTC or button press to wake us - // FIXME, power down SPI, I2C, RAMs +void cpuDeepSleep(uint32_t msecToWake) { + // FIXME, configure RTC or button press to wake us + // FIXME, power down SPI, I2C, RAMs #if HAS_WIRE - Wire.end(); + Wire.end(); #endif - SPI.end(); + SPI.end(); #if SPI_INTERFACES_COUNT > 1 - SPI1.end(); + SPI1.end(); #endif - if (Serial) // Another check in case of disabled default serial, does nothing bad - Serial.end(); // This may cause crashes as debug messages continue to flow. + if (Serial) // Another check in case of disabled default serial, does nothing bad + Serial.end(); // This may cause crashes as debug messages continue to flow. - // This causes troubles with waking up on nrf52 (on pro-micro in particular): - // we have no Serial1 in use on nrf52, check Serial and GPS modules. + // This causes troubles with waking up on nrf52 (on pro-micro in particular): + // we have no Serial1 in use on nrf52, check Serial and GPS modules. #ifdef PIN_SERIAL1_RX - if (Serial1) // A straightforward solution to the wake from deepsleep problem - Serial1.end(); + if (Serial1) // A straightforward solution to the wake from deepsleep problem + Serial1.end(); #endif #ifdef TTGO_T_ECHO - // To power off the T-Echo, the display must be set - // as an input pin; otherwise, there will be leakage current. - pinMode(PIN_EINK_CS, INPUT); - pinMode(PIN_EINK_DC, INPUT); - pinMode(PIN_EINK_RES, INPUT); - pinMode(PIN_EINK_BUSY, INPUT); + // To power off the T-Echo, the display must be set + // as an input pin; otherwise, there will be leakage current. + pinMode(PIN_EINK_CS, INPUT); + pinMode(PIN_EINK_DC, INPUT); + pinMode(PIN_EINK_RES, INPUT); + pinMode(PIN_EINK_BUSY, INPUT); #endif - setBluetoothEnable(false); + setBluetoothEnable(false); #ifdef RAK4630 #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, LOW); + digitalWrite(PIN_3V3_EN, LOW); #endif #ifdef AQ_SET_PIN - // RAK-12039 set pin for Air quality sensor - digitalWrite(AQ_SET_PIN, LOW); + // RAK-12039 set pin for Air quality sensor + digitalWrite(AQ_SET_PIN, LOW); #endif #ifdef RAK14014 - // GPIO restores input status, otherwise there will be leakage current - nrf_gpio_cfg_default(TFT_BL); - nrf_gpio_cfg_default(TFT_DC); - nrf_gpio_cfg_default(TFT_CS); - nrf_gpio_cfg_default(TFT_SCLK); - nrf_gpio_cfg_default(TFT_MOSI); - nrf_gpio_cfg_default(TFT_MISO); - nrf_gpio_cfg_default(SCREEN_TOUCH_INT); - nrf_gpio_cfg_default(WB_I2C1_SCL); - nrf_gpio_cfg_default(WB_I2C1_SDA); + // GPIO restores input status, otherwise there will be leakage current + nrf_gpio_cfg_default(TFT_BL); + nrf_gpio_cfg_default(TFT_DC); + nrf_gpio_cfg_default(TFT_CS); + nrf_gpio_cfg_default(TFT_SCLK); + nrf_gpio_cfg_default(TFT_MOSI); + nrf_gpio_cfg_default(TFT_MISO); + nrf_gpio_cfg_default(SCREEN_TOUCH_INT); + nrf_gpio_cfg_default(WB_I2C1_SCL); + nrf_gpio_cfg_default(WB_I2C1_SDA); - // nrf_gpio_cfg_default(WB_I2C2_SCL); - // nrf_gpio_cfg_default(WB_I2C2_SDA); + // nrf_gpio_cfg_default(WB_I2C2_SCL); + // nrf_gpio_cfg_default(WB_I2C2_SDA); #endif #endif #ifdef MESHLINK #ifdef PIN_WD_EN - digitalWrite(PIN_WD_EN, LOW); + digitalWrite(PIN_WD_EN, LOW); #endif #endif #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR) - nrf_gpio_cfg_default(PIN_GPS_PPS); - detachInterrupt(PIN_GPS_PPS); - detachInterrupt(PIN_BUTTON1); + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); #endif #ifdef ELECROW_ThinkNode_M1 - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - pinMode(pin, OUTPUT); + for (int pin = 0; pin < 48; pin++) { + if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || pin == PIN_BUTTON1 || + pin == PIN_BUTTON2) { + continue; } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - digitalWrite(pin, LOW); + pinMode(pin, OUTPUT); + } + for (int pin = 0; pin < 48; pin++) { + if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || pin == PIN_BUTTON1 || + pin == PIN_BUTTON2) { + continue; } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - NRF_GPIO->DIRCLR = (1 << pin); + digitalWrite(pin, LOW); + } + for (int pin = 0; pin < 48; pin++) { + if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || pin == PIN_BUTTON1 || + pin == PIN_BUTTON2) { + continue; } + NRF_GPIO->DIRCLR = (1 << pin); + } #endif - variant_shutdown(); + variant_shutdown(); - // Sleepy trackers or sensors can low power "sleep" - // Don't enter this if we're sleeping portMAX_DELAY, since that's a shutdown event - if (msecToWake != portMAX_DELAY && - (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR) && - config.power.is_power_saving == true)) { - sd_power_mode_set(NRF_POWER_MODE_LOWPWR); - delay(msecToWake); - NVIC_SystemReset(); - } else { - // Resume on user button press - // https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738 - constexpr uint32_t DFU_MAGIC_SKIP = 0x6d; - sd_power_gpregret_clr(0, 0xFF); // Clear the register before setting a new values in it for stability reasons - sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP + // Sleepy trackers or sensors can low power "sleep" + // Don't enter this if we're sleeping portMAX_DELAY, since that's a shutdown event + if (msecToWake != portMAX_DELAY && (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR) && + config.power.is_power_saving == true)) { + sd_power_mode_set(NRF_POWER_MODE_LOWPWR); + delay(msecToWake); + NVIC_SystemReset(); + } else { + // Resume on user button press + // https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738 + constexpr uint32_t DFU_MAGIC_SKIP = 0x6d; + sd_power_gpregret_clr(0, 0xFF); // Clear the register before setting a new values in it for stability reasons + sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP - // FIXME, use system off mode with ram retention for key state? - // FIXME, use non-init RAM per - // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled + // FIXME, use system off mode with ram retention for key state? + // FIXME, use non-init RAM per + // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled #ifdef ELECROW_ThinkNode_M1 - nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); - nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); + nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); #endif #ifdef PROMICRO_DIY_TCXO - nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge - nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep + nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge + nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep #endif #ifdef BATTERY_LPCOMP_INPUT - // Wake up if power rises again - nrf_lpcomp_config_t c; - c.reference = BATTERY_LPCOMP_THRESHOLD; - c.detection = NRF_LPCOMP_DETECT_UP; - c.hyst = NRF_LPCOMP_HYST_NOHYST; - nrf_lpcomp_configure(NRF_LPCOMP, &c); - nrf_lpcomp_input_select(NRF_LPCOMP, BATTERY_LPCOMP_INPUT); - nrf_lpcomp_enable(NRF_LPCOMP); + // Wake up if power rises again + nrf_lpcomp_config_t c; + c.reference = BATTERY_LPCOMP_THRESHOLD; + c.detection = NRF_LPCOMP_DETECT_UP; + c.hyst = NRF_LPCOMP_HYST_NOHYST; + nrf_lpcomp_configure(NRF_LPCOMP, &c); + nrf_lpcomp_input_select(NRF_LPCOMP, BATTERY_LPCOMP_INPUT); + nrf_lpcomp_enable(NRF_LPCOMP); - battery_adcEnable(); + battery_adcEnable(); - nrf_lpcomp_task_trigger(NRF_LPCOMP, NRF_LPCOMP_TASK_START); - while (!nrf_lpcomp_event_check(NRF_LPCOMP, NRF_LPCOMP_EVENT_READY)) - ; + nrf_lpcomp_task_trigger(NRF_LPCOMP, NRF_LPCOMP_TASK_START); + while (!nrf_lpcomp_event_check(NRF_LPCOMP, NRF_LPCOMP_EVENT_READY)) + ; #endif - auto ok = sd_power_system_off(); - if (ok != NRF_SUCCESS) { - LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!"); - NRF_POWER->SYSTEMOFF = 1; - } + auto ok = sd_power_system_off(); + if (ok != NRF_SUCCESS) { + LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!"); + NRF_POWER->SYSTEMOFF = 1; } + } - // The following code should not be run, because we are off - while (1) { - delay(5000); - LOG_DEBUG("."); - } + // The following code should not be run, because we are off + while (1) { + delay(5000); + LOG_DEBUG("."); + } } -void clearBonds() -{ - if (!nrf52Bluetooth) { - nrf52Bluetooth = new NRF52Bluetooth(); - nrf52Bluetooth->setup(); - } - nrf52Bluetooth->clearBonds(); +void clearBonds() { + if (!nrf52Bluetooth) { + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->setup(); + } + nrf52Bluetooth->clearBonds(); } -void enterDfuMode() -{ +void enterDfuMode() { // SDK kit does not have native USB like almost all other NRF52 boards #ifdef NRF_USE_SERIAL_DFU - enterSerialDfu(); + enterSerialDfu(); #else - enterUf2Dfu(); + enterUf2Dfu(); #endif } diff --git a/src/platform/nrf52/softdevice/ble.h b/src/platform/nrf52/softdevice/ble.h index 177b436ad..6d4be74ce 100644 --- a/src/platform/nrf52/softdevice/ble.h +++ b/src/platform/nrf52/softdevice/ble.h @@ -70,26 +70,26 @@ extern "C" { * @brief Common API SVC numbers. */ enum BLE_COMMON_SVCS { - SD_BLE_ENABLE = BLE_SVC_BASE, /**< Enable and initialize the BLE stack */ - SD_BLE_EVT_GET, /**< Get an event from the pending events queue. */ - SD_BLE_UUID_VS_ADD, /**< Add a Vendor Specific base UUID. */ - SD_BLE_UUID_DECODE, /**< Decode UUID bytes. */ - SD_BLE_UUID_ENCODE, /**< Encode UUID bytes. */ - SD_BLE_VERSION_GET, /**< Get the local version information (company ID, Link Layer Version, Link Layer Subversion). */ - SD_BLE_USER_MEM_REPLY, /**< User Memory Reply. */ - SD_BLE_OPT_SET, /**< Set a BLE option. */ - SD_BLE_OPT_GET, /**< Get a BLE option. */ - SD_BLE_CFG_SET, /**< Add a configuration to the BLE stack. */ - SD_BLE_UUID_VS_REMOVE, /**< Remove a Vendor Specific base UUID. */ + SD_BLE_ENABLE = BLE_SVC_BASE, /**< Enable and initialize the BLE stack */ + SD_BLE_EVT_GET, /**< Get an event from the pending events queue. */ + SD_BLE_UUID_VS_ADD, /**< Add a Vendor Specific base UUID. */ + SD_BLE_UUID_DECODE, /**< Decode UUID bytes. */ + SD_BLE_UUID_ENCODE, /**< Encode UUID bytes. */ + SD_BLE_VERSION_GET, /**< Get the local version information (company ID, Link Layer Version, Link Layer Subversion). */ + SD_BLE_USER_MEM_REPLY, /**< User Memory Reply. */ + SD_BLE_OPT_SET, /**< Set a BLE option. */ + SD_BLE_OPT_GET, /**< Get a BLE option. */ + SD_BLE_CFG_SET, /**< Add a configuration to the BLE stack. */ + SD_BLE_UUID_VS_REMOVE, /**< Remove a Vendor Specific base UUID. */ }; /** * @brief BLE Module Independent Event IDs. */ enum BLE_COMMON_EVTS { - BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. See @ref ble_evt_user_mem_request_t - \n Reply with @ref sd_ble_user_mem_reply. */ - BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. See @ref ble_evt_user_mem_release_t */ + BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. See @ref ble_evt_user_mem_request_t + \n Reply with @ref sd_ble_user_mem_reply. */ + BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. See @ref ble_evt_user_mem_release_t */ }; /**@brief BLE Connection Configuration IDs. @@ -97,11 +97,11 @@ enum BLE_COMMON_EVTS { * IDs that uniquely identify a connection configuration. */ enum BLE_CONN_CFGS { - BLE_CONN_CFG_GAP = BLE_CONN_CFG_BASE + 0, /**< BLE GAP specific connection configuration. */ - BLE_CONN_CFG_GATTC = BLE_CONN_CFG_BASE + 1, /**< BLE GATTC specific connection configuration. */ - BLE_CONN_CFG_GATTS = BLE_CONN_CFG_BASE + 2, /**< BLE GATTS specific connection configuration. */ - BLE_CONN_CFG_GATT = BLE_CONN_CFG_BASE + 3, /**< BLE GATT specific connection configuration. */ - BLE_CONN_CFG_L2CAP = BLE_CONN_CFG_BASE + 4, /**< BLE L2CAP specific connection configuration. */ + BLE_CONN_CFG_GAP = BLE_CONN_CFG_BASE + 0, /**< BLE GAP specific connection configuration. */ + BLE_CONN_CFG_GATTC = BLE_CONN_CFG_BASE + 1, /**< BLE GATTC specific connection configuration. */ + BLE_CONN_CFG_GATTS = BLE_CONN_CFG_BASE + 2, /**< BLE GATTS specific connection configuration. */ + BLE_CONN_CFG_GATT = BLE_CONN_CFG_BASE + 3, /**< BLE GATT specific connection configuration. */ + BLE_CONN_CFG_L2CAP = BLE_CONN_CFG_BASE + 4, /**< BLE L2CAP specific connection configuration. */ }; /**@brief BLE Common Configuration IDs. @@ -109,16 +109,16 @@ enum BLE_CONN_CFGS { * IDs that uniquely identify a common configuration. */ enum BLE_COMMON_CFGS { - BLE_COMMON_CFG_VS_UUID = BLE_CFG_BASE, /**< Vendor specific base UUID configuration */ + BLE_COMMON_CFG_VS_UUID = BLE_CFG_BASE, /**< Vendor specific base UUID configuration */ }; /**@brief Common Option IDs. * IDs that uniquely identify a common option. */ enum BLE_COMMON_OPTS { - BLE_COMMON_OPT_PA_LNA = BLE_OPT_BASE + 0, /**< PA and LNA options */ - BLE_COMMON_OPT_CONN_EVT_EXT = BLE_OPT_BASE + 1, /**< Extended connection events option */ - BLE_COMMON_OPT_EXTENDED_RC_CAL = BLE_OPT_BASE + 2, /**< Extended RC calibration option */ + BLE_COMMON_OPT_PA_LNA = BLE_OPT_BASE + 0, /**< PA and LNA options */ + BLE_COMMON_OPT_CONN_EVT_EXT = BLE_OPT_BASE + 1, /**< Extended connection events option */ + BLE_COMMON_OPT_EXTENDED_RC_CAL = BLE_OPT_BASE + 2, /**< Extended RC calibration option */ }; /** @} */ @@ -136,10 +136,11 @@ enum BLE_COMMON_OPTS { /** @brief Maximum possible length for BLE Events. * @note The highest value used for @ref ble_gatt_conn_cfg_t::att_mtu in any connection configuration shall be used as a - * parameter. If that value has not been configured for any connections then @ref BLE_GATT_ATT_MTU_DEFAULT must be used instead. + * parameter. If that value has not been configured for any connections then @ref BLE_GATT_ATT_MTU_DEFAULT must be used + * instead. */ -#define BLE_EVT_LEN_MAX(ATT_MTU) \ - (offsetof(ble_evt_t, evt.gattc_evt.params.prim_srvc_disc_rsp.services) + ((ATT_MTU)-1) / 4 * sizeof(ble_gattc_service_t)) +#define BLE_EVT_LEN_MAX(ATT_MTU) \ + (offsetof(ble_evt_t, evt.gattc_evt.params.prim_srvc_disc_rsp.services) + ((ATT_MTU)-1) / 4 * sizeof(ble_gattc_service_t)) /** @defgroup BLE_USER_MEM_TYPES User Memory Types * @{ */ @@ -168,67 +169,68 @@ enum BLE_COMMON_OPTS { /**@brief User Memory Block. */ typedef struct { - uint8_t *p_mem; /**< Pointer to the start of the user memory block. */ - uint16_t len; /**< Length in bytes of the user memory block. */ + uint8_t *p_mem; /**< Pointer to the start of the user memory block. */ + uint16_t len; /**< Length in bytes of the user memory block. */ } ble_user_mem_block_t; /**@brief Event structure for @ref BLE_EVT_USER_MEM_REQUEST. */ typedef struct { - uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ + uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ } ble_evt_user_mem_request_t; /**@brief Event structure for @ref BLE_EVT_USER_MEM_RELEASE. */ typedef struct { - uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ - ble_user_mem_block_t mem_block; /**< User memory block */ + uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ + ble_user_mem_block_t mem_block; /**< User memory block */ } ble_evt_user_mem_release_t; /**@brief Event structure for events not associated with a specific function module. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which this event occurred. */ - union { - ble_evt_user_mem_request_t user_mem_request; /**< User Memory Request Event Parameters. */ - ble_evt_user_mem_release_t user_mem_release; /**< User Memory Release Event Parameters. */ - } params; /**< Event parameter union. */ + uint16_t conn_handle; /**< Connection Handle on which this event occurred. */ + union { + ble_evt_user_mem_request_t user_mem_request; /**< User Memory Request Event Parameters. */ + ble_evt_user_mem_release_t user_mem_release; /**< User Memory Release Event Parameters. */ + } params; /**< Event parameter union. */ } ble_common_evt_t; /**@brief BLE Event header. */ typedef struct { - uint16_t evt_id; /**< Value from a BLE__EVT series. */ - uint16_t evt_len; /**< Length in octets including this header. */ + uint16_t evt_id; /**< Value from a BLE__EVT series. */ + uint16_t evt_len; /**< Length in octets including this header. */ } ble_evt_hdr_t; /**@brief Common BLE Event type, wrapping the module specific event reports. */ typedef struct { - ble_evt_hdr_t header; /**< Event header. */ - union { - ble_common_evt_t common_evt; /**< Common Event, evt_id in BLE_EVT_* series. */ - ble_gap_evt_t gap_evt; /**< GAP originated event, evt_id in BLE_GAP_EVT_* series. */ - ble_gattc_evt_t gattc_evt; /**< GATT client originated event, evt_id in BLE_GATTC_EVT* series. */ - ble_gatts_evt_t gatts_evt; /**< GATT server originated event, evt_id in BLE_GATTS_EVT* series. */ - ble_l2cap_evt_t l2cap_evt; /**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series. */ - } evt; /**< Event union. */ + ble_evt_hdr_t header; /**< Event header. */ + union { + ble_common_evt_t common_evt; /**< Common Event, evt_id in BLE_EVT_* series. */ + ble_gap_evt_t gap_evt; /**< GAP originated event, evt_id in BLE_GAP_EVT_* series. */ + ble_gattc_evt_t gattc_evt; /**< GATT client originated event, evt_id in BLE_GATTC_EVT* series. */ + ble_gatts_evt_t gatts_evt; /**< GATT server originated event, evt_id in BLE_GATTS_EVT* series. */ + ble_l2cap_evt_t l2cap_evt; /**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series. */ + } evt; /**< Event union. */ } ble_evt_t; /** * @brief Version Information. */ typedef struct { - uint8_t version_number; /**< Link Layer Version number. See - https://www.bluetooth.org/en-us/specification/assigned-numbers/link-layer for assigned values. */ - uint16_t company_id; /**< Company ID, Nordic Semiconductor's company ID is 89 (0x0059) - (https://www.bluetooth.org/apps/content/Default.aspx?doc_id=49708). */ - uint16_t - subversion_number; /**< Link Layer Sub Version number, corresponds to the SoftDevice Config ID or Firmware ID (FWID). */ + uint8_t version_number; /**< Link Layer Version number. See + https://www.bluetooth.org/en-us/specification/assigned-numbers/link-layer for assigned + values. */ + uint16_t company_id; /**< Company ID, Nordic Semiconductor's company ID is 89 (0x0059) + (https://www.bluetooth.org/apps/content/Default.aspx?doc_id=49708). */ + uint16_t subversion_number; /**< Link Layer Sub Version number, corresponds to the SoftDevice Config ID or Firmware ID + (FWID). */ } ble_version_t; /** * @brief Configuration parameters for the PA and LNA. */ typedef struct { - uint8_t enable : 1; /**< Enable toggling for this amplifier */ - uint8_t active_high : 1; /**< Set the pin to be active high */ - uint8_t gpio_pin : 6; /**< The GPIO pin to toggle for this amplifier */ + uint8_t enable : 1; /**< Enable toggling for this amplifier */ + uint8_t active_high : 1; /**< Set the pin to be active high */ + uint8_t gpio_pin : 6; /**< The GPIO pin to toggle for this amplifier */ } ble_pa_lna_cfg_t; /** @@ -241,16 +243,16 @@ typedef struct { * by the application and should be regarded as reserved as long as any PA/LNA toggling is enabled. * * @note @ref sd_ble_opt_get is not supported for this option. - * @note Setting this option while the radio is in use (i.e. any of the roles are active) may have undefined consequences - * and must be avoided by the application. + * @note Setting this option while the radio is in use (i.e. any of the roles are active) may have undefined + * consequences and must be avoided by the application. */ typedef struct { - ble_pa_lna_cfg_t pa_cfg; /**< Power Amplifier configuration */ - ble_pa_lna_cfg_t lna_cfg; /**< Low Noise Amplifier configuration */ + ble_pa_lna_cfg_t pa_cfg; /**< Power Amplifier configuration */ + ble_pa_lna_cfg_t lna_cfg; /**< Low Noise Amplifier configuration */ - uint8_t ppi_ch_id_set; /**< PPI channel used for radio pin setting */ - uint8_t ppi_ch_id_clr; /**< PPI channel used for radio pin clearing */ - uint8_t gpiote_ch_id; /**< GPIOTE channel used for radio pin toggling */ + uint8_t ppi_ch_id_set; /**< PPI channel used for radio pin setting */ + uint8_t ppi_ch_id_clr; /**< PPI channel used for radio pin clearing */ + uint8_t gpiote_ch_id; /**< GPIOTE channel used for radio pin toggling */ } ble_common_opt_pa_lna_t; /** @@ -258,25 +260,27 @@ typedef struct { * * When enabled the SoftDevice will dynamically extend the connection event when possible. * - * The connection event length is controlled by the connection configuration as set by @ref ble_gap_conn_cfg_t::event_length. - * The connection event can be extended if there is time to send another packet pair before the start of the next connection - * interval, and if there are no conflicts with other BLE roles requesting radio time. + * The connection event length is controlled by the connection configuration as set by @ref + * ble_gap_conn_cfg_t::event_length. The connection event can be extended if there is time to send another packet pair + * before the start of the next connection interval, and if there are no conflicts with other BLE roles requesting radio + * time. * * @note @ref sd_ble_opt_get is not supported for this option. */ typedef struct { - uint8_t enable : 1; /**< Enable extended BLE connection events, disabled by default. */ + uint8_t enable : 1; /**< Enable extended BLE connection events, disabled by default. */ } ble_common_opt_conn_evt_ext_t; /** * @brief Enable/disable extended RC calibration. * - * If extended RC calibration is enabled and the internal RC oscillator (@ref NRF_CLOCK_LF_SRC_RC) is used as the SoftDevice - * LFCLK source, the SoftDevice as a peripheral will by default try to increase the receive window if two consecutive packets - * are not received. If it turns out that the packets were not received due to clock drift, the RC calibration is started. - * This calibration comes in addition to the periodic calibration that is configured by @ref sd_softdevice_enable(). When - * using only peripheral connections, the periodic calibration can therefore be configured with a much longer interval as the - * peripheral will be able to detect and adjust automatically to clock drift, and calibrate on demand. + * If extended RC calibration is enabled and the internal RC oscillator (@ref NRF_CLOCK_LF_SRC_RC) is used as the + * SoftDevice LFCLK source, the SoftDevice as a peripheral will by default try to increase the receive window if two + * consecutive packets are not received. If it turns out that the packets were not received due to clock drift, the RC + * calibration is started. This calibration comes in addition to the periodic calibration that is configured by @ref + * sd_softdevice_enable(). When using only peripheral connections, the periodic calibration can therefore be configured + * with a much longer interval as the peripheral will be able to detect and adjust automatically to clock drift, and + * calibrate on demand. * * If extended RC calibration is disabled and the internal RC oscillator is used as the SoftDevice LFCLK source, the * RC oscillator is calibrated periodically as configured by @ref sd_softdevice_enable(). @@ -284,28 +288,29 @@ typedef struct { * @note @ref sd_ble_opt_get is not supported for this option. */ typedef struct { - uint8_t enable : 1; /**< Enable extended RC calibration, enabled by default. */ + uint8_t enable : 1; /**< Enable extended RC calibration, enabled by default. */ } ble_common_opt_extended_rc_cal_t; /**@brief Option structure for common options. */ typedef union { - ble_common_opt_pa_lna_t pa_lna; /**< Parameters for controlling PA and LNA pin toggling. */ - ble_common_opt_conn_evt_ext_t conn_evt_ext; /**< Parameters for enabling extended connection events. */ - ble_common_opt_extended_rc_cal_t extended_rc_cal; /**< Parameters for enabling extended RC calibration. */ + ble_common_opt_pa_lna_t pa_lna; /**< Parameters for controlling PA and LNA pin toggling. */ + ble_common_opt_conn_evt_ext_t conn_evt_ext; /**< Parameters for enabling extended connection events. */ + ble_common_opt_extended_rc_cal_t extended_rc_cal; /**< Parameters for enabling extended RC calibration. */ } ble_common_opt_t; /**@brief Common BLE Option type, wrapping the module specific options. */ typedef union { - ble_common_opt_t common_opt; /**< COMMON options, opt_id in @ref BLE_COMMON_OPTS series. */ - ble_gap_opt_t gap_opt; /**< GAP option, opt_id in @ref BLE_GAP_OPTS series. */ - ble_gattc_opt_t gattc_opt; /**< GATTC option, opt_id in @ref BLE_GATTC_OPTS series. */ + ble_common_opt_t common_opt; /**< COMMON options, opt_id in @ref BLE_COMMON_OPTS series. */ + ble_gap_opt_t gap_opt; /**< GAP option, opt_id in @ref BLE_GAP_OPTS series. */ + ble_gattc_opt_t gattc_opt; /**< GATTC option, opt_id in @ref BLE_GATTC_OPTS series. */ } ble_opt_t; /**@brief BLE connection configuration type, wrapping the module specific configurations, set with * @ref sd_ble_cfg_set. * * @note Connection configurations don't have to be set. - * In the case that no configurations has been set, or fewer connection configurations has been set than enabled connections, + * In the case that no configurations has been set, or fewer connection configurations has been set than enabled + connections, * the default connection configuration will be automatically added for the remaining connections. * When creating connections with the default configuration, @ref BLE_CONN_CFG_TAG_DEFAULT should be used in * place of @ref ble_conn_cfg_t::conn_cfg_tag. @@ -319,17 +324,18 @@ typedef union { */ typedef struct { - uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the - @ref sd_ble_gap_adv_start() and @ref sd_ble_gap_connect() calls - to select this configuration when creating a connection. - Must be different for all connection configurations added and not @ref BLE_CONN_CFG_TAG_DEFAULT. */ - union { - ble_gap_conn_cfg_t gap_conn_cfg; /**< GAP connection configuration, cfg_id is @ref BLE_CONN_CFG_GAP. */ - ble_gattc_conn_cfg_t gattc_conn_cfg; /**< GATTC connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTC. */ - ble_gatts_conn_cfg_t gatts_conn_cfg; /**< GATTS connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTS. */ - ble_gatt_conn_cfg_t gatt_conn_cfg; /**< GATT connection configuration, cfg_id is @ref BLE_CONN_CFG_GATT. */ - ble_l2cap_conn_cfg_t l2cap_conn_cfg; /**< L2CAP connection configuration, cfg_id is @ref BLE_CONN_CFG_L2CAP. */ - } params; /**< Connection configuration union. */ + uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the + @ref sd_ble_gap_adv_start() and @ref sd_ble_gap_connect() calls + to select this configuration when creating a connection. + Must be different for all connection configurations added and not @ref + BLE_CONN_CFG_TAG_DEFAULT. */ + union { + ble_gap_conn_cfg_t gap_conn_cfg; /**< GAP connection configuration, cfg_id is @ref BLE_CONN_CFG_GAP. */ + ble_gattc_conn_cfg_t gattc_conn_cfg; /**< GATTC connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTC. */ + ble_gatts_conn_cfg_t gatts_conn_cfg; /**< GATTS connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTS. */ + ble_gatt_conn_cfg_t gatt_conn_cfg; /**< GATT connection configuration, cfg_id is @ref BLE_CONN_CFG_GATT. */ + ble_l2cap_conn_cfg_t l2cap_conn_cfg; /**< L2CAP connection configuration, cfg_id is @ref BLE_CONN_CFG_L2CAP. */ + } params; /**< Connection configuration union. */ } ble_conn_cfg_t; /** @@ -338,22 +344,22 @@ typedef struct { * @retval ::NRF_ERROR_INVALID_PARAM Too many UUIDs configured. */ typedef struct { - uint8_t vs_uuid_count; /**< Number of 128-bit Vendor Specific base UUID bases to allocate memory for. - Default value is @ref BLE_UUID_VS_COUNT_DEFAULT. Maximum value is - @ref BLE_UUID_VS_COUNT_MAX. */ + uint8_t vs_uuid_count; /**< Number of 128-bit Vendor Specific base UUID bases to allocate memory for. + Default value is @ref BLE_UUID_VS_COUNT_DEFAULT. Maximum value is + @ref BLE_UUID_VS_COUNT_MAX. */ } ble_common_cfg_vs_uuid_t; /**@brief Common BLE Configuration type, wrapping the common configurations. */ typedef union { - ble_common_cfg_vs_uuid_t vs_uuid_cfg; /**< Vendor Specific base UUID configuration, cfg_id is @ref BLE_COMMON_CFG_VS_UUID. */ + ble_common_cfg_vs_uuid_t vs_uuid_cfg; /**< Vendor Specific base UUID configuration, cfg_id is @ref BLE_COMMON_CFG_VS_UUID. */ } ble_common_cfg_t; /**@brief BLE Configuration type, wrapping the module specific configurations. */ typedef union { - ble_conn_cfg_t conn_cfg; /**< Connection specific configurations, cfg_id in @ref BLE_CONN_CFGS series. */ - ble_common_cfg_t common_cfg; /**< Global common configurations, cfg_id in @ref BLE_COMMON_CFGS series. */ - ble_gap_cfg_t gap_cfg; /**< Global GAP configurations, cfg_id in @ref BLE_GAP_CFGS series. */ - ble_gatts_cfg_t gatts_cfg; /**< Global GATTS configuration, cfg_id in @ref BLE_GATTS_CFGS series. */ + ble_conn_cfg_t conn_cfg; /**< Connection specific configurations, cfg_id in @ref BLE_CONN_CFGS series. */ + ble_common_cfg_t common_cfg; /**< Global common configurations, cfg_id in @ref BLE_COMMON_CFGS series. */ + ble_gap_cfg_t gap_cfg; /**< Global GAP configurations, cfg_id in @ref BLE_GAP_CFGS series. */ + ble_gatts_cfg_t gatts_cfg; /**< Global GATTS configuration, cfg_id in @ref BLE_GATTS_CFGS series. */ } ble_cfg_t; /** @} */ @@ -402,11 +408,12 @@ typedef union { * @retval ::NRF_ERROR_INVALID_ADDR Invalid or not sufficiently aligned pointer supplied. * @retval ::NRF_ERROR_NO_MEM One or more of the following is true: * - The amount of memory assigned to the SoftDevice by *p_app_ram_base is not - * large enough to fit this configuration's memory requirement. Check *p_app_ram_base - * and set the start address of the application RAM region accordingly. + * large enough to fit this configuration's memory requirement. Check + * *p_app_ram_base and set the start address of the application RAM region accordingly. * - Dynamic part of the SoftDevice RAM region is larger then 64 kB which * is currently not supported. - * @retval ::NRF_ERROR_RESOURCES The total number of L2CAP Channels configured using @ref sd_ble_cfg_set is too large. + * @retval ::NRF_ERROR_RESOURCES The total number of L2CAP Channels configured using @ref sd_ble_cfg_set is too + * large. */ SVCALL(SD_BLE_ENABLE, uint32_t, sd_ble_enable(uint32_t *p_app_ram_base)); @@ -461,11 +468,12 @@ SVCALL(SD_BLE_CFG_SET, uint32_t, sd_ble_cfg_set(uint32_t cfg_id, ble_cfg_t const * every time SD_EVT_IRQn is raised to ensure that all available events are pulled from the BLE stack. Failure to do so * could potentially leave events in the internal queue without the application being aware of this fact. * - * Sizing the p_dest buffer is equally important, since the application needs to provide all the memory necessary for the event to - * be copied into application memory. If the buffer provided is not large enough to fit the entire contents of the event, + * Sizing the p_dest buffer is equally important, since the application needs to provide all the memory necessary for + * the event to be copied into application memory. If the buffer provided is not large enough to fit the entire contents + * of the event, * @ref NRF_ERROR_DATA_SIZE will be returned and the application can then call again with a larger buffer size. - * The maximum possible event length is defined by @ref BLE_EVT_LEN_MAX. The application may also "peek" the event length - * by providing p_dest as a NULL pointer and inspecting the value of *p_len upon return: + * The maximum possible event length is defined by @ref BLE_EVT_LEN_MAX. The application may also "peek" the event + * length by providing p_dest as a NULL pointer and inspecting the value of *p_len upon return: * * \code * uint16_t len; @@ -504,8 +512,8 @@ SVCALL(SD_BLE_EVT_GET, uint32_t, sd_ble_evt_get(uint8_t *p_dest, uint16_t *p_len * * @param[in] p_vs_uuid Pointer to a 16-octet (128-bit) little endian Vendor Specific base UUID disregarding * bytes 12 and 13. - * @param[out] p_uuid_type Pointer to a uint8_t where the type field in @ref ble_uuid_t corresponding to this UUID will be - * stored. + * @param[out] p_uuid_type Pointer to a uint8_t where the type field in @ref ble_uuid_t corresponding to this UUID will + * be stored. * * @retval ::NRF_SUCCESS Successfully added the Vendor Specific base UUID. * @retval ::NRF_ERROR_INVALID_ADDR If p_vs_uuid or p_uuid_type is NULL or invalid. @@ -518,14 +526,13 @@ SVCALL(SD_BLE_UUID_VS_ADD, uint32_t, sd_ble_uuid_vs_add(ble_uuid128_t const *p_v * @details This call removes a Vendor Specific base UUID. This function allows * the application to reuse memory allocated for Vendor Specific base UUIDs. * - * @note Currently this function can only be called with a p_uuid_type set to @ref BLE_UUID_TYPE_UNKNOWN or the last added UUID - * type. + * @note Currently this function can only be called with a p_uuid_type set to @ref BLE_UUID_TYPE_UNKNOWN or the last + * added UUID type. * - * @param[inout] p_uuid_type Pointer to a uint8_t where its value matches the UUID type in @ref ble_uuid_t::type to be removed. - * If the type is set to @ref BLE_UUID_TYPE_UNKNOWN, or the pointer is NULL, the last Vendor Specific - * base UUID will be removed. If the function returns successfully, the UUID type that was removed will - * be written back to @p p_uuid_type. If function returns with a failure, it contains the last type that - * is in use by the ATT Server. + * @param[inout] p_uuid_type Pointer to a uint8_t where its value matches the UUID type in @ref ble_uuid_t::type to be + * removed. If the type is set to @ref BLE_UUID_TYPE_UNKNOWN, or the pointer is NULL, the last Vendor Specific base UUID + * will be removed. If the function returns successfully, the UUID type that was removed will be written back to @p + * p_uuid_type. If function returns with a failure, it contains the last type that is in use by the ATT Server. * * @retval ::NRF_SUCCESS Successfully removed the Vendor Specific base UUID. * @retval ::NRF_ERROR_INVALID_ADDR If p_uuid_type is invalid. @@ -556,8 +563,8 @@ SVCALL(SD_BLE_UUID_DECODE, uint32_t, sd_ble_uuid_decode(uint8_t uuid_le_len, uin /** @brief Encode a @ref ble_uuid_t structure into little endian raw UUID bytes (16-bit or 128-bit). * - * @note The pointer to the destination buffer p_uuid_le may be NULL, in which case only the validity and size of p_uuid is - * computed. + * @note The pointer to the destination buffer p_uuid_le may be NULL, in which case only the validity and size of p_uuid + * is computed. * * @param[in] p_uuid Pointer to a @ref ble_uuid_t structure that will be encoded into bytes. * @param[out] p_uuid_le_len Pointer to a uint8_t that will be filled with the encoded length (2 or 16 bytes). diff --git a/src/platform/nrf52/softdevice/ble_err.h b/src/platform/nrf52/softdevice/ble_err.h index d20f6d141..bbd7571f1 100644 --- a/src/platform/nrf52/softdevice/ble_err.h +++ b/src/platform/nrf52/softdevice/ble_err.h @@ -67,8 +67,8 @@ extern "C" { #define BLE_ERROR_INVALID_ATTR_HANDLE (NRF_ERROR_STK_BASE_NUM + 0x003) /**< Invalid attribute handle. */ #define BLE_ERROR_INVALID_ADV_HANDLE (NRF_ERROR_STK_BASE_NUM + 0x004) /**< Invalid advertising handle. */ #define BLE_ERROR_INVALID_ROLE (NRF_ERROR_STK_BASE_NUM + 0x005) /**< Invalid role. */ -#define BLE_ERROR_BLOCKED_BY_OTHER_LINKS \ - (NRF_ERROR_STK_BASE_NUM + 0x006) /**< The attempt to change link settings failed due to the scheduling of other links. */ +#define BLE_ERROR_BLOCKED_BY_OTHER_LINKS \ + (NRF_ERROR_STK_BASE_NUM + 0x006) /**< The attempt to change link settings failed due to the scheduling of other links. */ /** @} */ /** @defgroup BLE_ERROR_SUBRANGES Module specific error code subranges diff --git a/src/platform/nrf52/softdevice/ble_gap.h b/src/platform/nrf52/softdevice/ble_gap.h index 8ebdfa82b..e2c7de30d 100644 --- a/src/platform/nrf52/softdevice/ble_gap.h +++ b/src/platform/nrf52/softdevice/ble_gap.h @@ -63,129 +63,108 @@ extern "C" { /**@brief GAP API SVC numbers. */ enum BLE_GAP_SVCS { - SD_BLE_GAP_ADDR_SET = BLE_GAP_SVC_BASE, /**< Set own Bluetooth Address. */ - SD_BLE_GAP_ADDR_GET = BLE_GAP_SVC_BASE + 1, /**< Get own Bluetooth Address. */ - SD_BLE_GAP_WHITELIST_SET = BLE_GAP_SVC_BASE + 2, /**< Set active whitelist. */ - SD_BLE_GAP_DEVICE_IDENTITIES_SET = BLE_GAP_SVC_BASE + 3, /**< Set device identity list. */ - SD_BLE_GAP_PRIVACY_SET = BLE_GAP_SVC_BASE + 4, /**< Set Privacy settings*/ - SD_BLE_GAP_PRIVACY_GET = BLE_GAP_SVC_BASE + 5, /**< Get Privacy settings*/ - SD_BLE_GAP_ADV_SET_CONFIGURE = BLE_GAP_SVC_BASE + 6, /**< Configure an advertising set. */ - SD_BLE_GAP_ADV_START = BLE_GAP_SVC_BASE + 7, /**< Start Advertising. */ - SD_BLE_GAP_ADV_STOP = BLE_GAP_SVC_BASE + 8, /**< Stop Advertising. */ - SD_BLE_GAP_CONN_PARAM_UPDATE = BLE_GAP_SVC_BASE + 9, /**< Connection Parameter Update. */ - SD_BLE_GAP_DISCONNECT = BLE_GAP_SVC_BASE + 10, /**< Disconnect. */ - SD_BLE_GAP_TX_POWER_SET = BLE_GAP_SVC_BASE + 11, /**< Set TX Power. */ - SD_BLE_GAP_APPEARANCE_SET = BLE_GAP_SVC_BASE + 12, /**< Set Appearance. */ - SD_BLE_GAP_APPEARANCE_GET = BLE_GAP_SVC_BASE + 13, /**< Get Appearance. */ - SD_BLE_GAP_PPCP_SET = BLE_GAP_SVC_BASE + 14, /**< Set PPCP. */ - SD_BLE_GAP_PPCP_GET = BLE_GAP_SVC_BASE + 15, /**< Get PPCP. */ - SD_BLE_GAP_DEVICE_NAME_SET = BLE_GAP_SVC_BASE + 16, /**< Set Device Name. */ - SD_BLE_GAP_DEVICE_NAME_GET = BLE_GAP_SVC_BASE + 17, /**< Get Device Name. */ - SD_BLE_GAP_AUTHENTICATE = BLE_GAP_SVC_BASE + 18, /**< Initiate Pairing/Bonding. */ - SD_BLE_GAP_SEC_PARAMS_REPLY = BLE_GAP_SVC_BASE + 19, /**< Reply with Security Parameters. */ - SD_BLE_GAP_AUTH_KEY_REPLY = BLE_GAP_SVC_BASE + 20, /**< Reply with an authentication key. */ - SD_BLE_GAP_LESC_DHKEY_REPLY = BLE_GAP_SVC_BASE + 21, /**< Reply with an LE Secure Connections DHKey. */ - SD_BLE_GAP_KEYPRESS_NOTIFY = BLE_GAP_SVC_BASE + 22, /**< Notify of a keypress during an authentication procedure. */ - SD_BLE_GAP_LESC_OOB_DATA_GET = BLE_GAP_SVC_BASE + 23, /**< Get the local LE Secure Connections OOB data. */ - SD_BLE_GAP_LESC_OOB_DATA_SET = BLE_GAP_SVC_BASE + 24, /**< Set the remote LE Secure Connections OOB data. */ - SD_BLE_GAP_ENCRYPT = BLE_GAP_SVC_BASE + 25, /**< Initiate encryption procedure. */ - SD_BLE_GAP_SEC_INFO_REPLY = BLE_GAP_SVC_BASE + 26, /**< Reply with Security Information. */ - SD_BLE_GAP_CONN_SEC_GET = BLE_GAP_SVC_BASE + 27, /**< Obtain connection security level. */ - SD_BLE_GAP_RSSI_START = BLE_GAP_SVC_BASE + 28, /**< Start reporting of changes in RSSI. */ - SD_BLE_GAP_RSSI_STOP = BLE_GAP_SVC_BASE + 29, /**< Stop reporting of changes in RSSI. */ - SD_BLE_GAP_SCAN_START = BLE_GAP_SVC_BASE + 30, /**< Start Scanning. */ - SD_BLE_GAP_SCAN_STOP = BLE_GAP_SVC_BASE + 31, /**< Stop Scanning. */ - SD_BLE_GAP_CONNECT = BLE_GAP_SVC_BASE + 32, /**< Connect. */ - SD_BLE_GAP_CONNECT_CANCEL = BLE_GAP_SVC_BASE + 33, /**< Cancel ongoing connection procedure. */ - SD_BLE_GAP_RSSI_GET = BLE_GAP_SVC_BASE + 34, /**< Get the last RSSI sample. */ - SD_BLE_GAP_PHY_UPDATE = BLE_GAP_SVC_BASE + 35, /**< Initiate or respond to a PHY Update Procedure. */ - SD_BLE_GAP_DATA_LENGTH_UPDATE = BLE_GAP_SVC_BASE + 36, /**< Initiate or respond to a Data Length Update Procedure. */ - SD_BLE_GAP_QOS_CHANNEL_SURVEY_START = BLE_GAP_SVC_BASE + 37, /**< Start Quality of Service (QoS) channel survey module. */ - SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP = BLE_GAP_SVC_BASE + 38, /**< Stop Quality of Service (QoS) channel survey module. */ - SD_BLE_GAP_ADV_ADDR_GET = BLE_GAP_SVC_BASE + 39, /**< Get the Address used on air while Advertising. */ - SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET = BLE_GAP_SVC_BASE + 40, /**< Get the next connection event counter. */ - SD_BLE_GAP_CONN_EVT_TRIGGER_START = BLE_GAP_SVC_BASE + 41, /** Start triggering a given task on connection event start. */ - SD_BLE_GAP_CONN_EVT_TRIGGER_STOP = - BLE_GAP_SVC_BASE + 42, /** Stop triggering the task configured using @ref sd_ble_gap_conn_evt_trigger_start. */ + SD_BLE_GAP_ADDR_SET = BLE_GAP_SVC_BASE, /**< Set own Bluetooth Address. */ + SD_BLE_GAP_ADDR_GET = BLE_GAP_SVC_BASE + 1, /**< Get own Bluetooth Address. */ + SD_BLE_GAP_WHITELIST_SET = BLE_GAP_SVC_BASE + 2, /**< Set active whitelist. */ + SD_BLE_GAP_DEVICE_IDENTITIES_SET = BLE_GAP_SVC_BASE + 3, /**< Set device identity list. */ + SD_BLE_GAP_PRIVACY_SET = BLE_GAP_SVC_BASE + 4, /**< Set Privacy settings*/ + SD_BLE_GAP_PRIVACY_GET = BLE_GAP_SVC_BASE + 5, /**< Get Privacy settings*/ + SD_BLE_GAP_ADV_SET_CONFIGURE = BLE_GAP_SVC_BASE + 6, /**< Configure an advertising set. */ + SD_BLE_GAP_ADV_START = BLE_GAP_SVC_BASE + 7, /**< Start Advertising. */ + SD_BLE_GAP_ADV_STOP = BLE_GAP_SVC_BASE + 8, /**< Stop Advertising. */ + SD_BLE_GAP_CONN_PARAM_UPDATE = BLE_GAP_SVC_BASE + 9, /**< Connection Parameter Update. */ + SD_BLE_GAP_DISCONNECT = BLE_GAP_SVC_BASE + 10, /**< Disconnect. */ + SD_BLE_GAP_TX_POWER_SET = BLE_GAP_SVC_BASE + 11, /**< Set TX Power. */ + SD_BLE_GAP_APPEARANCE_SET = BLE_GAP_SVC_BASE + 12, /**< Set Appearance. */ + SD_BLE_GAP_APPEARANCE_GET = BLE_GAP_SVC_BASE + 13, /**< Get Appearance. */ + SD_BLE_GAP_PPCP_SET = BLE_GAP_SVC_BASE + 14, /**< Set PPCP. */ + SD_BLE_GAP_PPCP_GET = BLE_GAP_SVC_BASE + 15, /**< Get PPCP. */ + SD_BLE_GAP_DEVICE_NAME_SET = BLE_GAP_SVC_BASE + 16, /**< Set Device Name. */ + SD_BLE_GAP_DEVICE_NAME_GET = BLE_GAP_SVC_BASE + 17, /**< Get Device Name. */ + SD_BLE_GAP_AUTHENTICATE = BLE_GAP_SVC_BASE + 18, /**< Initiate Pairing/Bonding. */ + SD_BLE_GAP_SEC_PARAMS_REPLY = BLE_GAP_SVC_BASE + 19, /**< Reply with Security Parameters. */ + SD_BLE_GAP_AUTH_KEY_REPLY = BLE_GAP_SVC_BASE + 20, /**< Reply with an authentication key. */ + SD_BLE_GAP_LESC_DHKEY_REPLY = BLE_GAP_SVC_BASE + 21, /**< Reply with an LE Secure Connections DHKey. */ + SD_BLE_GAP_KEYPRESS_NOTIFY = BLE_GAP_SVC_BASE + 22, /**< Notify of a keypress during an authentication procedure. */ + SD_BLE_GAP_LESC_OOB_DATA_GET = BLE_GAP_SVC_BASE + 23, /**< Get the local LE Secure Connections OOB data. */ + SD_BLE_GAP_LESC_OOB_DATA_SET = BLE_GAP_SVC_BASE + 24, /**< Set the remote LE Secure Connections OOB data. */ + SD_BLE_GAP_ENCRYPT = BLE_GAP_SVC_BASE + 25, /**< Initiate encryption procedure. */ + SD_BLE_GAP_SEC_INFO_REPLY = BLE_GAP_SVC_BASE + 26, /**< Reply with Security Information. */ + SD_BLE_GAP_CONN_SEC_GET = BLE_GAP_SVC_BASE + 27, /**< Obtain connection security level. */ + SD_BLE_GAP_RSSI_START = BLE_GAP_SVC_BASE + 28, /**< Start reporting of changes in RSSI. */ + SD_BLE_GAP_RSSI_STOP = BLE_GAP_SVC_BASE + 29, /**< Stop reporting of changes in RSSI. */ + SD_BLE_GAP_SCAN_START = BLE_GAP_SVC_BASE + 30, /**< Start Scanning. */ + SD_BLE_GAP_SCAN_STOP = BLE_GAP_SVC_BASE + 31, /**< Stop Scanning. */ + SD_BLE_GAP_CONNECT = BLE_GAP_SVC_BASE + 32, /**< Connect. */ + SD_BLE_GAP_CONNECT_CANCEL = BLE_GAP_SVC_BASE + 33, /**< Cancel ongoing connection procedure. */ + SD_BLE_GAP_RSSI_GET = BLE_GAP_SVC_BASE + 34, /**< Get the last RSSI sample. */ + SD_BLE_GAP_PHY_UPDATE = BLE_GAP_SVC_BASE + 35, /**< Initiate or respond to a PHY Update Procedure. */ + SD_BLE_GAP_DATA_LENGTH_UPDATE = BLE_GAP_SVC_BASE + 36, /**< Initiate or respond to a Data Length Update Procedure. */ + SD_BLE_GAP_QOS_CHANNEL_SURVEY_START = BLE_GAP_SVC_BASE + 37, /**< Start Quality of Service (QoS) channel survey module. */ + SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP = BLE_GAP_SVC_BASE + 38, /**< Stop Quality of Service (QoS) channel survey module. */ + SD_BLE_GAP_ADV_ADDR_GET = BLE_GAP_SVC_BASE + 39, /**< Get the Address used on air while Advertising. */ + SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET = BLE_GAP_SVC_BASE + 40, /**< Get the next connection event counter. */ + SD_BLE_GAP_CONN_EVT_TRIGGER_START = BLE_GAP_SVC_BASE + 41, /** Start triggering a given task on connection event start. */ + SD_BLE_GAP_CONN_EVT_TRIGGER_STOP = BLE_GAP_SVC_BASE + 42, /** Stop triggering the task configured using @ref sd_ble_gap_conn_evt_trigger_start. */ }; /**@brief GAP Event IDs. * IDs that uniquely identify an event coming from the stack to the application. */ enum BLE_GAP_EVTS { - BLE_GAP_EVT_CONNECTED = - BLE_GAP_EVT_BASE, /**< Connected to peer. \n See @ref ble_gap_evt_connected_t */ - BLE_GAP_EVT_DISCONNECTED = - BLE_GAP_EVT_BASE + 1, /**< Disconnected from peer. \n See @ref ble_gap_evt_disconnected_t. */ - BLE_GAP_EVT_CONN_PARAM_UPDATE = - BLE_GAP_EVT_BASE + 2, /**< Connection Parameters updated. \n See @ref ble_gap_evt_conn_param_update_t. */ - BLE_GAP_EVT_SEC_PARAMS_REQUEST = - BLE_GAP_EVT_BASE + 3, /**< Request to provide security parameters. \n Reply with @ref sd_ble_gap_sec_params_reply. - \n See @ref ble_gap_evt_sec_params_request_t. */ - BLE_GAP_EVT_SEC_INFO_REQUEST = - BLE_GAP_EVT_BASE + 4, /**< Request to provide security information. \n Reply with @ref sd_ble_gap_sec_info_reply. - \n See @ref ble_gap_evt_sec_info_request_t. */ - BLE_GAP_EVT_PASSKEY_DISPLAY = - BLE_GAP_EVT_BASE + 5, /**< Request to display a passkey to the user. \n In LESC Numeric Comparison, reply with @ref - sd_ble_gap_auth_key_reply. \n See @ref ble_gap_evt_passkey_display_t. */ - BLE_GAP_EVT_KEY_PRESSED = - BLE_GAP_EVT_BASE + 6, /**< Notification of a keypress on the remote device.\n See @ref ble_gap_evt_key_pressed_t */ - BLE_GAP_EVT_AUTH_KEY_REQUEST = - BLE_GAP_EVT_BASE + 7, /**< Request to provide an authentication key. \n Reply with @ref sd_ble_gap_auth_key_reply. - \n See @ref ble_gap_evt_auth_key_request_t. */ - BLE_GAP_EVT_LESC_DHKEY_REQUEST = - BLE_GAP_EVT_BASE + 8, /**< Request to calculate an LE Secure Connections DHKey. \n Reply with @ref - sd_ble_gap_lesc_dhkey_reply. \n See @ref ble_gap_evt_lesc_dhkey_request_t */ - BLE_GAP_EVT_AUTH_STATUS = - BLE_GAP_EVT_BASE + 9, /**< Authentication procedure completed with status. \n See @ref ble_gap_evt_auth_status_t. */ - BLE_GAP_EVT_CONN_SEC_UPDATE = - BLE_GAP_EVT_BASE + 10, /**< Connection security updated. \n See @ref ble_gap_evt_conn_sec_update_t. */ - BLE_GAP_EVT_TIMEOUT = - BLE_GAP_EVT_BASE + 11, /**< Timeout expired. \n See @ref ble_gap_evt_timeout_t. */ - BLE_GAP_EVT_RSSI_CHANGED = - BLE_GAP_EVT_BASE + 12, /**< RSSI report. \n See @ref ble_gap_evt_rssi_changed_t. */ - BLE_GAP_EVT_ADV_REPORT = - BLE_GAP_EVT_BASE + 13, /**< Advertising report. \n See @ref ble_gap_evt_adv_report_t. */ - BLE_GAP_EVT_SEC_REQUEST = - BLE_GAP_EVT_BASE + 14, /**< Security Request. \n Reply with @ref sd_ble_gap_authenticate -\n or with @ref sd_ble_gap_encrypt if required security information is available -. \n See @ref ble_gap_evt_sec_request_t. */ - BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST = - BLE_GAP_EVT_BASE + 15, /**< Connection Parameter Update Request. \n Reply with @ref - sd_ble_gap_conn_param_update. \n See @ref ble_gap_evt_conn_param_update_request_t. */ - BLE_GAP_EVT_SCAN_REQ_REPORT = - BLE_GAP_EVT_BASE + 16, /**< Scan request report. \n See @ref ble_gap_evt_scan_req_report_t. */ - BLE_GAP_EVT_PHY_UPDATE_REQUEST = - BLE_GAP_EVT_BASE + 17, /**< PHY Update Request. \n Reply with @ref sd_ble_gap_phy_update. \n - See @ref ble_gap_evt_phy_update_request_t. */ - BLE_GAP_EVT_PHY_UPDATE = - BLE_GAP_EVT_BASE + 18, /**< PHY Update Procedure is complete. \n See @ref ble_gap_evt_phy_update_t. */ - BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST = - BLE_GAP_EVT_BASE + 19, /**< Data Length Update Request. \n Reply with @ref - sd_ble_gap_data_length_update. \n See @ref ble_gap_evt_data_length_update_request_t. */ - BLE_GAP_EVT_DATA_LENGTH_UPDATE = - BLE_GAP_EVT_BASE + - 20, /**< LL Data Channel PDU payload length updated. \n See @ref ble_gap_evt_data_length_update_t. */ - BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT = - BLE_GAP_EVT_BASE + - 21, /**< Channel survey report. \n See @ref ble_gap_evt_qos_channel_survey_report_t. */ - BLE_GAP_EVT_ADV_SET_TERMINATED = - BLE_GAP_EVT_BASE + - 22, /**< Advertising set terminated. \n See @ref ble_gap_evt_adv_set_terminated_t. */ + BLE_GAP_EVT_CONNECTED = BLE_GAP_EVT_BASE, /**< Connected to peer. \n See @ref ble_gap_evt_connected_t */ + BLE_GAP_EVT_DISCONNECTED = BLE_GAP_EVT_BASE + 1, /**< Disconnected from peer. \n See @ref ble_gap_evt_disconnected_t. */ + BLE_GAP_EVT_CONN_PARAM_UPDATE = + BLE_GAP_EVT_BASE + 2, /**< Connection Parameters updated. \n See @ref ble_gap_evt_conn_param_update_t. */ + BLE_GAP_EVT_SEC_PARAMS_REQUEST = BLE_GAP_EVT_BASE + 3, /**< Request to provide security parameters. \n Reply with @ref + sd_ble_gap_sec_params_reply. \n See @ref ble_gap_evt_sec_params_request_t. */ + BLE_GAP_EVT_SEC_INFO_REQUEST = BLE_GAP_EVT_BASE + 4, /**< Request to provide security information. \n Reply with @ref + sd_ble_gap_sec_info_reply. \n See @ref ble_gap_evt_sec_info_request_t. */ + BLE_GAP_EVT_PASSKEY_DISPLAY = BLE_GAP_EVT_BASE + 5, /**< Request to display a passkey to the user. \n In LESC Numeric Comparison, reply + with @ref sd_ble_gap_auth_key_reply. \n See @ref ble_gap_evt_passkey_display_t. */ + BLE_GAP_EVT_KEY_PRESSED = BLE_GAP_EVT_BASE + 6, /**< Notification of a keypress on the remote device.\n See @ref ble_gap_evt_key_pressed_t */ + BLE_GAP_EVT_AUTH_KEY_REQUEST = BLE_GAP_EVT_BASE + 7, /**< Request to provide an authentication key. \n Reply with @ref + sd_ble_gap_auth_key_reply. \n See @ref ble_gap_evt_auth_key_request_t. */ + BLE_GAP_EVT_LESC_DHKEY_REQUEST = BLE_GAP_EVT_BASE + 8, /**< Request to calculate an LE Secure Connections DHKey. \n Reply with @ref + sd_ble_gap_lesc_dhkey_reply. \n See @ref ble_gap_evt_lesc_dhkey_request_t */ + BLE_GAP_EVT_AUTH_STATUS = BLE_GAP_EVT_BASE + 9, /**< Authentication procedure completed with status. \n See @ref ble_gap_evt_auth_status_t. */ + BLE_GAP_EVT_CONN_SEC_UPDATE = + BLE_GAP_EVT_BASE + 10, /**< Connection security updated. \n See @ref ble_gap_evt_conn_sec_update_t. */ + BLE_GAP_EVT_TIMEOUT = BLE_GAP_EVT_BASE + 11, /**< Timeout expired. \n See @ref ble_gap_evt_timeout_t. */ + BLE_GAP_EVT_RSSI_CHANGED = BLE_GAP_EVT_BASE + 12, /**< RSSI report. \n See @ref ble_gap_evt_rssi_changed_t. */ + BLE_GAP_EVT_ADV_REPORT = BLE_GAP_EVT_BASE + 13, /**< Advertising report. \n See @ref ble_gap_evt_adv_report_t. */ + BLE_GAP_EVT_SEC_REQUEST = BLE_GAP_EVT_BASE + 14, /**< Security Request. \n Reply with + @ref sd_ble_gap_authenticate \n or with @ref sd_ble_gap_encrypt if required security information is + available . \n See @ref ble_gap_evt_sec_request_t. */ + BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST = + BLE_GAP_EVT_BASE + 15, /**< Connection Parameter Update Request. \n Reply with @ref + sd_ble_gap_conn_param_update. \n See @ref ble_gap_evt_conn_param_update_request_t. */ + BLE_GAP_EVT_SCAN_REQ_REPORT = + BLE_GAP_EVT_BASE + 16, /**< Scan request report. \n See @ref ble_gap_evt_scan_req_report_t. */ + BLE_GAP_EVT_PHY_UPDATE_REQUEST = BLE_GAP_EVT_BASE + 17, /**< PHY Update Request. \n Reply with @ref + sd_ble_gap_phy_update. \n See @ref ble_gap_evt_phy_update_request_t. */ + BLE_GAP_EVT_PHY_UPDATE = BLE_GAP_EVT_BASE + 18, /**< PHY Update Procedure is complete. \n See @ref ble_gap_evt_phy_update_t. */ + BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST = + BLE_GAP_EVT_BASE + 19, /**< Data Length Update Request. \n Reply with @ref + sd_ble_gap_data_length_update. \n See @ref ble_gap_evt_data_length_update_request_t. */ + BLE_GAP_EVT_DATA_LENGTH_UPDATE = + BLE_GAP_EVT_BASE + 20, /**< LL Data Channel PDU payload length updated. \n See @ref ble_gap_evt_data_length_update_t. */ + BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT = + BLE_GAP_EVT_BASE + 21, /**< Channel survey report. \n See @ref ble_gap_evt_qos_channel_survey_report_t. */ + BLE_GAP_EVT_ADV_SET_TERMINATED = + BLE_GAP_EVT_BASE + 22, /**< Advertising set terminated. \n See @ref ble_gap_evt_adv_set_terminated_t. */ }; /**@brief GAP Option IDs. * IDs that uniquely identify a GAP option. */ enum BLE_GAP_OPTS { - BLE_GAP_OPT_CH_MAP = BLE_GAP_OPT_BASE, /**< Channel Map. @ref ble_gap_opt_ch_map_t */ - BLE_GAP_OPT_LOCAL_CONN_LATENCY = BLE_GAP_OPT_BASE + 1, /**< Local connection latency. @ref ble_gap_opt_local_conn_latency_t */ - BLE_GAP_OPT_PASSKEY = BLE_GAP_OPT_BASE + 2, /**< Set passkey. @ref ble_gap_opt_passkey_t */ - BLE_GAP_OPT_COMPAT_MODE_1 = BLE_GAP_OPT_BASE + 3, /**< Compatibility mode. @ref ble_gap_opt_compat_mode_1_t */ - BLE_GAP_OPT_AUTH_PAYLOAD_TIMEOUT = - BLE_GAP_OPT_BASE + 4, /**< Set Authenticated payload timeout. @ref ble_gap_opt_auth_payload_timeout_t */ - BLE_GAP_OPT_SLAVE_LATENCY_DISABLE = - BLE_GAP_OPT_BASE + 5, /**< Disable slave latency. @ref ble_gap_opt_slave_latency_disable_t */ + BLE_GAP_OPT_CH_MAP = BLE_GAP_OPT_BASE, /**< Channel Map. @ref ble_gap_opt_ch_map_t */ + BLE_GAP_OPT_LOCAL_CONN_LATENCY = BLE_GAP_OPT_BASE + 1, /**< Local connection latency. @ref ble_gap_opt_local_conn_latency_t */ + BLE_GAP_OPT_PASSKEY = BLE_GAP_OPT_BASE + 2, /**< Set passkey. @ref ble_gap_opt_passkey_t */ + BLE_GAP_OPT_COMPAT_MODE_1 = BLE_GAP_OPT_BASE + 3, /**< Compatibility mode. @ref ble_gap_opt_compat_mode_1_t */ + BLE_GAP_OPT_AUTH_PAYLOAD_TIMEOUT = BLE_GAP_OPT_BASE + 4, /**< Set Authenticated payload timeout. @ref ble_gap_opt_auth_payload_timeout_t */ + BLE_GAP_OPT_SLAVE_LATENCY_DISABLE = BLE_GAP_OPT_BASE + 5, /**< Disable slave latency. @ref ble_gap_opt_slave_latency_disable_t */ }; /**@brief GAP Configuration IDs. @@ -193,20 +172,20 @@ enum BLE_GAP_OPTS { * IDs that uniquely identify a GAP configuration. */ enum BLE_GAP_CFGS { - BLE_GAP_CFG_ROLE_COUNT = BLE_GAP_CFG_BASE, /**< Role count configuration. */ - BLE_GAP_CFG_DEVICE_NAME = BLE_GAP_CFG_BASE + 1, /**< Device name configuration. */ - BLE_GAP_CFG_PPCP_INCL_CONFIG = BLE_GAP_CFG_BASE + 2, /**< Peripheral Preferred Connection Parameters characteristic - inclusion configuration. */ - BLE_GAP_CFG_CAR_INCL_CONFIG = BLE_GAP_CFG_BASE + 3, /**< Central Address Resolution characteristic - inclusion configuration. */ + BLE_GAP_CFG_ROLE_COUNT = BLE_GAP_CFG_BASE, /**< Role count configuration. */ + BLE_GAP_CFG_DEVICE_NAME = BLE_GAP_CFG_BASE + 1, /**< Device name configuration. */ + BLE_GAP_CFG_PPCP_INCL_CONFIG = BLE_GAP_CFG_BASE + 2, /**< Peripheral Preferred Connection Parameters characteristic + inclusion configuration. */ + BLE_GAP_CFG_CAR_INCL_CONFIG = BLE_GAP_CFG_BASE + 3, /**< Central Address Resolution characteristic + inclusion configuration. */ }; /**@brief GAP TX Power roles. */ enum BLE_GAP_TX_POWER_ROLES { - BLE_GAP_TX_POWER_ROLE_ADV = 1, /**< Advertiser role. */ - BLE_GAP_TX_POWER_ROLE_SCAN_INIT = 2, /**< Scanner and initiator role. */ - BLE_GAP_TX_POWER_ROLE_CONN = 3, /**< Connection role. */ + BLE_GAP_TX_POWER_ROLE_ADV = 1, /**< Advertiser role. */ + BLE_GAP_TX_POWER_ROLE_SCAN_INIT = 2, /**< Scanner and initiator role. */ + BLE_GAP_TX_POWER_ROLE_CONN = 3, /**< Connection role. */ }; /** @} */ @@ -216,18 +195,16 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_ERRORS_GAP SVC return values specific to GAP * @{ */ -#define BLE_ERROR_GAP_UUID_LIST_MISMATCH \ - (NRF_GAP_ERR_BASE + 0x000) /**< UUID list does not contain an integral number of UUIDs. */ -#define BLE_ERROR_GAP_DISCOVERABLE_WITH_WHITELIST \ - (NRF_GAP_ERR_BASE + 0x001) /**< Use of Whitelist not permitted with discoverable advertising. */ -#define BLE_ERROR_GAP_INVALID_BLE_ADDR \ - (NRF_GAP_ERR_BASE + 0x002) /**< The upper two bits of the address do not correspond to the specified address type. */ -#define BLE_ERROR_GAP_WHITELIST_IN_USE \ - (NRF_GAP_ERR_BASE + 0x003) /**< Attempt to modify the whitelist while already in use by another operation. */ -#define BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE \ - (NRF_GAP_ERR_BASE + 0x004) /**< Attempt to modify the device identity list while already in use by another operation. */ -#define BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE \ - (NRF_GAP_ERR_BASE + 0x005) /**< The device identity list contains entries with duplicate identity addresses. */ +#define BLE_ERROR_GAP_UUID_LIST_MISMATCH (NRF_GAP_ERR_BASE + 0x000) /**< UUID list does not contain an integral number of UUIDs. */ +#define BLE_ERROR_GAP_DISCOVERABLE_WITH_WHITELIST (NRF_GAP_ERR_BASE + 0x001) /**< Use of Whitelist not permitted with discoverable advertising. */ +#define BLE_ERROR_GAP_INVALID_BLE_ADDR \ + (NRF_GAP_ERR_BASE + 0x002) /**< The upper two bits of the address do not correspond to the specified address type. \ + */ +#define BLE_ERROR_GAP_WHITELIST_IN_USE (NRF_GAP_ERR_BASE + 0x003) /**< Attempt to modify the whitelist while already in use by another operation. */ +#define BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE \ + (NRF_GAP_ERR_BASE + 0x004) /**< Attempt to modify the device identity list while already in use by another operation. */ +#define BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE \ + (NRF_GAP_ERR_BASE + 0x005) /**< The device identity list contains entries with duplicate identity addresses. */ /**@} */ /**@defgroup BLE_GAP_ROLES GAP Roles @@ -250,9 +227,9 @@ enum BLE_GAP_TX_POWER_ROLES { #define BLE_GAP_ADDR_TYPE_RANDOM_STATIC 0x01 /**< Random static (identity) address. */ #define BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE 0x02 /**< Random private resolvable address. */ #define BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE 0x03 /**< Random private non-resolvable address. */ -#define BLE_GAP_ADDR_TYPE_ANONYMOUS \ - 0x7F /**< An advertiser may advertise without its address. \ - This type of advertising is called anonymous. */ +#define BLE_GAP_ADDR_TYPE_ANONYMOUS \ + 0x7F /**< An advertiser may advertise without its address. \ + This type of advertising is called anonymous. */ /**@} */ /**@brief The default interval in seconds at which a private address is refreshed. */ @@ -267,10 +244,10 @@ enum BLE_GAP_TX_POWER_ROLES { * @{ */ #define BLE_GAP_PRIVACY_MODE_OFF 0x00 /**< Device will send and accept its identity address for its own address. */ #define BLE_GAP_PRIVACY_MODE_DEVICE_PRIVACY 0x01 /**< Device will send and accept only private addresses for its own address. */ -#define BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY \ - 0x02 /**< Device will send and accept only private addresses for its own address, \ - and will not accept a peer using identity address as sender address when \ - the peer IRK is exchanged, non-zero and added to the identity list. */ +#define BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY \ + 0x02 /**< Device will send and accept only private addresses for its own address, \ + and will not accept a peer using identity address as sender address when \ + the peer IRK is exchanged, non-zero and added to the identity list. */ /**@} */ /** @brief Invalid power level. */ @@ -287,14 +264,13 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_ADV_SET_DATA_SIZES Advertising data sizes. * @{ */ -#define BLE_GAP_ADV_SET_DATA_SIZE_MAX \ - (31) /**< Maximum data length for an advertising set. \ - If more advertising data is required, use extended advertising instead. */ -#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED \ - (255) /**< Maximum supported data length for an extended advertising set. */ +#define BLE_GAP_ADV_SET_DATA_SIZE_MAX \ + (31) /**< Maximum data length for an advertising set. \ + If more advertising data is required, use extended advertising instead. */ +#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED (255) /**< Maximum supported data length for an extended advertising set. */ -#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_CONNECTABLE_MAX_SUPPORTED \ - (238) /**< Maximum supported data length for an extended connectable advertising set. */ +#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_CONNECTABLE_MAX_SUPPORTED \ + (238) /**< Maximum supported data length for an extended connectable advertising set. */ /**@}. */ /** @brief Set ID not available in advertising report. */ @@ -352,12 +328,10 @@ enum BLE_GAP_TX_POWER_ROLES { #define BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED (0x04) /**< BR/EDR not supported. */ #define BLE_GAP_ADV_FLAG_LE_BR_EDR_CONTROLLER (0x08) /**< Simultaneous LE and BR/EDR, Controller. */ #define BLE_GAP_ADV_FLAG_LE_BR_EDR_HOST (0x10) /**< Simultaneous LE and BR/EDR, Host. */ -#define BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE \ - (BLE_GAP_ADV_FLAG_LE_LIMITED_DISC_MODE | \ - BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE Limited Discoverable Mode, BR/EDR not supported. */ -#define BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE \ - (BLE_GAP_ADV_FLAG_LE_GENERAL_DISC_MODE | \ - BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE General Discoverable Mode, BR/EDR not supported. */ +#define BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE \ + (BLE_GAP_ADV_FLAG_LE_LIMITED_DISC_MODE | BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE Limited Discoverable Mode, BR/EDR not supported. */ +#define BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE \ + (BLE_GAP_ADV_FLAG_LE_GENERAL_DISC_MODE | BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE General Discoverable Mode, BR/EDR not supported. */ /**@} */ /**@defgroup BLE_GAP_ADV_INTERVALS GAP Advertising interval max and min @@ -390,21 +364,21 @@ enum BLE_GAP_TX_POWER_ROLES { * If ble_gap_scan_params_t::extended is set to 0, @ref BLE_GAP_SCAN_BUFFER_MIN is the minimum scan buffer length. * else the minimum scan buffer size is @ref BLE_GAP_SCAN_BUFFER_EXTENDED_MIN. * @{ */ -#define BLE_GAP_SCAN_BUFFER_MIN \ - (31) /**< Minimum data length for an \ - advertising set. */ -#define BLE_GAP_SCAN_BUFFER_MAX \ - (31) /**< Maximum data length for an \ - advertising set. */ -#define BLE_GAP_SCAN_BUFFER_EXTENDED_MIN \ - (255) /**< Minimum data length for an \ - extended advertising set. */ -#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX \ - (1650) /**< Maximum data length for an \ - extended advertising set. */ -#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED \ - (255) /**< Maximum supported data length for \ - an extended advertising set. */ +#define BLE_GAP_SCAN_BUFFER_MIN \ + (31) /**< Minimum data length for an \ + advertising set. */ +#define BLE_GAP_SCAN_BUFFER_MAX \ + (31) /**< Maximum data length for an \ + advertising set. */ +#define BLE_GAP_SCAN_BUFFER_EXTENDED_MIN \ + (255) /**< Minimum data length for an \ + extended advertising set. */ +#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX \ + (1650) /**< Maximum data length for an \ + extended advertising set. */ +#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED \ + (255) /**< Maximum supported data length for \ + an extended advertising set. */ /** @} */ /**@defgroup BLE_GAP_ADV_TYPES GAP Advertising types @@ -418,44 +392,44 @@ enum BLE_GAP_TX_POWER_ROLES { * scan response data. * * @{ */ -#define BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED \ - 0x01 /**< Connectable and scannable undirected \ - advertising events. */ -#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE \ - 0x02 /**< Connectable non-scannable directed advertising \ - events. Advertising interval is less that 3.75 ms. \ - Use this type for fast reconnections. \ - @note Advertising data is not supported. */ -#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED \ - 0x03 /**< Connectable non-scannable directed advertising \ - events. \ - @note Advertising data is not supported. */ -#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ - 0x04 /**< Non-connectable scannable undirected \ - advertising events. */ -#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ - 0x05 /**< Non-connectable non-scannable undirected \ - advertising events. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_UNDIRECTED \ - 0x06 /**< Connectable non-scannable undirected advertising \ - events using extended advertising PDUs. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_DIRECTED \ - 0x07 /**< Connectable non-scannable directed advertising \ - events using extended advertising PDUs. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ - 0x08 /**< Non-connectable scannable undirected advertising \ - events using extended advertising PDUs. \ - @note Only scan response data is supported. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_DIRECTED \ - 0x09 /**< Non-connectable scannable directed advertising \ - events using extended advertising PDUs. \ - @note Only scan response data is supported. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ - 0x0A /**< Non-connectable non-scannable undirected advertising \ - events using extended advertising PDUs. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED \ - 0x0B /**< Non-connectable non-scannable directed advertising \ - events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED \ + 0x01 /**< Connectable and scannable undirected \ + advertising events. */ +#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE \ + 0x02 /**< Connectable non-scannable directed advertising \ + events. Advertising interval is less that 3.75 ms. \ + Use this type for fast reconnections. \ + @note Advertising data is not supported. */ +#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED \ + 0x03 /**< Connectable non-scannable directed advertising \ + events. \ + @note Advertising data is not supported. */ +#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ + 0x04 /**< Non-connectable scannable undirected \ + advertising events. */ +#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ + 0x05 /**< Non-connectable non-scannable undirected \ + advertising events. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_UNDIRECTED \ + 0x06 /**< Connectable non-scannable undirected advertising \ + events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_DIRECTED \ + 0x07 /**< Connectable non-scannable directed advertising \ + events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ + 0x08 /**< Non-connectable scannable undirected advertising \ + events using extended advertising PDUs. \ + @note Only scan response data is supported. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_DIRECTED \ + 0x09 /**< Non-connectable scannable directed advertising \ + events using extended advertising PDUs. \ + @note Only scan response data is supported. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ + 0x0A /**< Non-connectable non-scannable undirected advertising \ + events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED \ + 0x0B /**< Non-connectable non-scannable directed advertising \ + events using extended advertising PDUs. */ /**@} */ /**@defgroup BLE_GAP_ADV_FILTER_POLICIES GAP Advertising filter policies @@ -469,50 +443,50 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_ADV_DATA_STATUS GAP Advertising data status * @{ */ #define BLE_GAP_ADV_DATA_STATUS_COMPLETE 0x00 /**< All data in the advertising event have been received. */ -#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA \ - 0x01 /**< More data to be received. \ - @note This value will only be used if \ - @ref ble_gap_scan_params_t::report_incomplete_evts and \ - @ref ble_gap_adv_report_type_t::extended_pdu are set to true. */ -#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED \ - 0x02 /**< Incomplete data. Buffer size insufficient to receive more. \ - @note This value will only be used if \ - @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ -#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MISSED \ - 0x03 /**< Failed to receive the remaining data. \ - @note This value will only be used if \ - @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ +#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA \ + 0x01 /**< More data to be received. \ + @note This value will only be used if \ + @ref ble_gap_scan_params_t::report_incomplete_evts and \ + @ref ble_gap_adv_report_type_t::extended_pdu are set to true. */ +#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED \ + 0x02 /**< Incomplete data. Buffer size insufficient to receive more. \ + @note This value will only be used if \ + @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ +#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MISSED \ + 0x03 /**< Failed to receive the remaining data. \ + @note This value will only be used if \ + @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ /**@} */ /**@defgroup BLE_GAP_SCAN_FILTER_POLICIES GAP Scanner filter policies * @{ */ -#define BLE_GAP_SCAN_FP_ACCEPT_ALL \ - 0x00 /**< Accept all advertising packets except directed advertising packets \ - not addressed to this device. */ -#define BLE_GAP_SCAN_FP_WHITELIST \ - 0x01 /**< Accept advertising packets from devices in the whitelist except directed \ - packets not addressed to this device. */ -#define BLE_GAP_SCAN_FP_ALL_NOT_RESOLVED_DIRECTED \ - 0x02 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_ACCEPT_ALL. \ - In addition, accept directed advertising packets, where the advertiser's \ - address is a resolvable private address that cannot be resolved. */ -#define BLE_GAP_SCAN_FP_WHITELIST_NOT_RESOLVED_DIRECTED \ - 0x03 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_WHITELIST. \ - In addition, accept directed advertising packets, where the advertiser's \ - address is a resolvable private address that cannot be resolved. */ +#define BLE_GAP_SCAN_FP_ACCEPT_ALL \ + 0x00 /**< Accept all advertising packets except directed advertising packets \ + not addressed to this device. */ +#define BLE_GAP_SCAN_FP_WHITELIST \ + 0x01 /**< Accept advertising packets from devices in the whitelist except directed \ + packets not addressed to this device. */ +#define BLE_GAP_SCAN_FP_ALL_NOT_RESOLVED_DIRECTED \ + 0x02 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_ACCEPT_ALL. \ + In addition, accept directed advertising packets, where the advertiser's \ + address is a resolvable private address that cannot be resolved. */ +#define BLE_GAP_SCAN_FP_WHITELIST_NOT_RESOLVED_DIRECTED \ + 0x03 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_WHITELIST. \ + In addition, accept directed advertising packets, where the advertiser's \ + address is a resolvable private address that cannot be resolved. */ /**@} */ /**@defgroup BLE_GAP_ADV_TIMEOUT_VALUES GAP Advertising timeout values in 10 ms units * @{ */ -#define BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX \ - (128) /**< Maximum high duty advertising time in 10 ms units. Corresponds to 1.28 s. \ - */ -#define BLE_GAP_ADV_TIMEOUT_LIMITED_MAX \ - (18000) /**< Maximum advertising time in 10 ms units corresponding to TGAP(lim_adv_timeout) = 180 s in limited discoverable \ - mode. */ -#define BLE_GAP_ADV_TIMEOUT_GENERAL_UNLIMITED \ - (0) /**< Unlimited advertising in general discoverable mode. \ - For high duty cycle advertising, this corresponds to @ref BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX. */ +#define BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX \ + (128) /**< Maximum high duty advertising time in 10 ms units. Corresponds to 1.28 s. \ + */ +#define BLE_GAP_ADV_TIMEOUT_LIMITED_MAX \ + (18000) /**< Maximum advertising time in 10 ms units corresponding to TGAP(lim_adv_timeout) = 180 s in limited \ + discoverable mode. */ +#define BLE_GAP_ADV_TIMEOUT_GENERAL_UNLIMITED \ + (0) /**< Unlimited advertising in general discoverable mode. \ + For high duty cycle advertising, this corresponds to @ref BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX. */ /**@} */ /**@defgroup BLE_GAP_DISC_MODES GAP Discovery modes @@ -581,18 +555,16 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_CP_LIMITS GAP Connection Parameters Limits * @{ */ #define BLE_GAP_CP_MIN_CONN_INTVL_NONE 0xFFFF /**< No new minimum connection interval specified in connect parameters. */ -#define BLE_GAP_CP_MIN_CONN_INTVL_MIN \ - 0x0006 /**< Lowest minimum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ -#define BLE_GAP_CP_MIN_CONN_INTVL_MAX \ - 0x0C80 /**< Highest minimum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ +#define BLE_GAP_CP_MIN_CONN_INTVL_MIN 0x0006 /**< Lowest minimum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ +#define BLE_GAP_CP_MIN_CONN_INTVL_MAX \ + 0x0C80 /**< Highest minimum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ */ #define BLE_GAP_CP_MAX_CONN_INTVL_NONE 0xFFFF /**< No new maximum connection interval specified in connect parameters. */ -#define BLE_GAP_CP_MAX_CONN_INTVL_MIN \ - 0x0006 /**< Lowest maximum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ -#define BLE_GAP_CP_MAX_CONN_INTVL_MAX \ - 0x0C80 /**< Highest maximum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ - */ -#define BLE_GAP_CP_SLAVE_LATENCY_MAX 0x01F3 /**< Highest slave latency permitted, in connection events. */ +#define BLE_GAP_CP_MAX_CONN_INTVL_MIN 0x0006 /**< Lowest maximum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ +#define BLE_GAP_CP_MAX_CONN_INTVL_MAX \ + 0x0C80 /**< Highest maximum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ + */ +#define BLE_GAP_CP_SLAVE_LATENCY_MAX 0x01F3 /**< Highest slave latency permitted, in connection events. */ #define BLE_GAP_CP_CONN_SUP_TIMEOUT_NONE 0xFFFF /**< No new supervision timeout specified in connect parameters. */ #define BLE_GAP_CP_CONN_SUP_TIMEOUT_MIN 0x000A /**< Lowest supervision timeout permitted, in units of 10 ms, i.e. 100 ms. */ #define BLE_GAP_CP_CONN_SUP_TIMEOUT_MAX 0x0C80 /**< Highest supervision timeout permitted, in units of 10 ms, i.e. 32 s. */ @@ -626,47 +598,47 @@ enum BLE_GAP_TX_POWER_ROLES { * See @ref ble_gap_conn_sec_mode_t. * @{ */ /**@brief Set sec_mode pointed to by ptr to have no access rights.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(ptr) \ - do { \ - (ptr)->sm = 0; \ - (ptr)->lv = 0; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(ptr) \ + do { \ + (ptr)->sm = 0; \ + (ptr)->lv = 0; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require no protection, open link.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_OPEN(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 1; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_OPEN(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 1; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require encryption, but no MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_ENC_NO_MITM(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 2; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_ENC_NO_MITM(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 2; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require encryption and MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_ENC_WITH_MITM(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 3; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_ENC_WITH_MITM(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 3; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require LESC encryption and MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_LESC_ENC_WITH_MITM(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 4; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_LESC_ENC_WITH_MITM(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 4; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require signing or encryption, no MITM protection needed.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_NO_MITM(ptr) \ - do { \ - (ptr)->sm = 2; \ - (ptr)->lv = 1; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_NO_MITM(ptr) \ + do { \ + (ptr)->sm = 2; \ + (ptr)->lv = 1; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require signing or encryption with MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_WITH_MITM(ptr) \ - do { \ - (ptr)->sm = 2; \ - (ptr)->lv = 2; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_WITH_MITM(ptr) \ + do { \ + (ptr)->sm = 2; \ + (ptr)->lv = 2; \ + } while (0) /**@} */ /**@brief GAP Security Random Number Length. */ @@ -702,12 +674,12 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_ROLE_COUNT GAP concurrent connection count defines. * @{ */ -#define BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT (1) /**< Default maximum number of connections concurrently acting as peripherals. */ -#define BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT (3) /**< Default maximum number of connections concurrently acting as centrals. */ -#define BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT \ - (1) /**< Default number of SMP instances shared between all connections acting as centrals. */ -#define BLE_GAP_ROLE_COUNT_COMBINED_MAX \ - (20) /**< Maximum supported number of concurrent connections in the peripheral and central roles combined. */ +#define BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT (1) /**< Default maximum number of connections concurrently acting as peripherals. */ +#define BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT (3) /**< Default maximum number of connections concurrently acting as centrals. */ +#define BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT (1) /**< Default number of SMP instances shared between all connections acting as centrals. */ +#define BLE_GAP_ROLE_COUNT_COMBINED_MAX \ + (20) /**< Maximum supported number of concurrent connections in the peripheral and central roles combined. \ + */ /**@} */ @@ -741,17 +713,17 @@ enum BLE_GAP_TX_POWER_ROLES { * @{ */ #define BLE_GAP_CHAR_INCL_CONFIG_INCLUDE (0) /**< Include the characteristic in the Attribute Table */ -#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITH_SPACE \ - (1) /**< Do not include the characteristic in the Attribute table. \ - The SoftDevice will reserve the attribute handles \ - which are otherwise used for this characteristic. \ - By reserving the attribute handles it will be possible \ - to upgrade the SoftDevice without changing handle of the \ - Service Changed characteristic. */ -#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITHOUT_SPACE \ - (2) /**< Do not include the characteristic in the Attribute table. \ - The SoftDevice will not reserve the attribute handles \ - which are otherwise used for this characteristic. */ +#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITH_SPACE \ + (1) /**< Do not include the characteristic in the Attribute table. \ + The SoftDevice will reserve the attribute handles \ + which are otherwise used for this characteristic. \ + By reserving the attribute handles it will be possible \ + to upgrade the SoftDevice without changing handle of the \ + Service Changed characteristic. */ +#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITHOUT_SPACE \ + (2) /**< Do not include the characteristic in the Attribute table. \ + The SoftDevice will not reserve the attribute handles \ + which are otherwise used for this characteristic. */ /**@} */ /** @defgroup BLE_GAP_CHAR_INCL_CONFIG_DEFAULTS Characteristic inclusion default values @@ -762,19 +734,19 @@ enum BLE_GAP_TX_POWER_ROLES { /** @defgroup BLE_GAP_SLAVE_LATENCY Slave latency configuration options * @{ */ -#define BLE_GAP_SLAVE_LATENCY_ENABLE \ - (0) /**< Slave latency is enabled. When slave latency is enabled, \ - the slave will wake up every time it has data to send, \ - and/or every slave latency number of connection events. */ -#define BLE_GAP_SLAVE_LATENCY_DISABLE \ - (1) /**< Disable slave latency. The slave will wake up every connection event \ - regardless of the requested slave latency. \ - This option consumes the most power. */ -#define BLE_GAP_SLAVE_LATENCY_WAIT_FOR_ACK \ - (2) /**< The slave will wake up every connection event if it has not received \ - an ACK from the master for at least slave latency events. This \ - configuration may increase the power consumption in environments \ - with a lot of radio activity. */ +#define BLE_GAP_SLAVE_LATENCY_ENABLE \ + (0) /**< Slave latency is enabled. When slave latency is enabled, \ + the slave will wake up every time it has data to send, \ + and/or every slave latency number of connection events. */ +#define BLE_GAP_SLAVE_LATENCY_DISABLE \ + (1) /**< Disable slave latency. The slave will wake up every connection event \ + regardless of the requested slave latency. \ + This option consumes the most power. */ +#define BLE_GAP_SLAVE_LATENCY_WAIT_FOR_ACK \ + (2) /**< The slave will wake up every connection event if it has not received \ + an ACK from the master for at least slave latency events. This \ + configuration may increase the power consumption in environments \ + with a lot of radio activity. */ /**@} */ /**@addtogroup BLE_GAP_STRUCTURES Structures @@ -782,44 +754,44 @@ enum BLE_GAP_TX_POWER_ROLES { /**@brief Advertising event properties. */ typedef struct { - uint8_t type; /**< Advertising type. See @ref BLE_GAP_ADV_TYPES. */ - uint8_t anonymous : 1; /**< Omit advertiser's address from all PDUs. - @note Anonymous advertising is only available for - @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED and - @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED. */ - uint8_t include_tx_power : 1; /**< This feature is not supported on this SoftDevice. */ + uint8_t type; /**< Advertising type. See @ref BLE_GAP_ADV_TYPES. */ + uint8_t anonymous : 1; /**< Omit advertiser's address from all PDUs. + @note Anonymous advertising is only available for + @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED and + @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED. */ + uint8_t include_tx_power : 1; /**< This feature is not supported on this SoftDevice. */ } ble_gap_adv_properties_t; /**@brief Advertising report type. */ typedef struct { - uint16_t connectable : 1; /**< Connectable advertising event type. */ - uint16_t scannable : 1; /**< Scannable advertising event type. */ - uint16_t directed : 1; /**< Directed advertising event type. */ - uint16_t scan_response : 1; /**< Received a scan response. */ - uint16_t extended_pdu : 1; /**< Received an extended advertising set. */ - uint16_t status : 2; /**< Data status. See @ref BLE_GAP_ADV_DATA_STATUS. */ - uint16_t reserved : 9; /**< Reserved for future use. */ + uint16_t connectable : 1; /**< Connectable advertising event type. */ + uint16_t scannable : 1; /**< Scannable advertising event type. */ + uint16_t directed : 1; /**< Directed advertising event type. */ + uint16_t scan_response : 1; /**< Received a scan response. */ + uint16_t extended_pdu : 1; /**< Received an extended advertising set. */ + uint16_t status : 2; /**< Data status. See @ref BLE_GAP_ADV_DATA_STATUS. */ + uint16_t reserved : 9; /**< Reserved for future use. */ } ble_gap_adv_report_type_t; /**@brief Advertising Auxiliary Pointer. */ typedef struct { - uint16_t aux_offset; /**< Time offset from the beginning of advertising packet to the auxiliary packet in 100 us units. */ - uint8_t aux_phy; /**< Indicates the PHY on which the auxiliary advertising packet is sent. See @ref BLE_GAP_PHYS. */ + uint16_t aux_offset; /**< Time offset from the beginning of advertising packet to the auxiliary packet in 100 us units. */ + uint8_t aux_phy; /**< Indicates the PHY on which the auxiliary advertising packet is sent. See @ref BLE_GAP_PHYS. */ } ble_gap_aux_pointer_t; /**@brief Bluetooth Low Energy address. */ typedef struct { - uint8_t - addr_id_peer : 1; /**< Only valid for peer addresses. - This bit is set by the SoftDevice to indicate whether the address has been resolved from - a Resolvable Private Address (when the peer is using privacy). - If set to 1, @ref addr and @ref addr_type refer to the identity address of the resolved address. - - This bit is ignored when a variable of type @ref ble_gap_addr_t is used as input to API functions. - */ - uint8_t addr_type : 7; /**< See @ref BLE_GAP_ADDR_TYPES. */ - uint8_t addr[BLE_GAP_ADDR_LEN]; /**< 48-bit address, LSB format. - @ref addr is not used if @ref addr_type is @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. */ + uint8_t addr_id_peer : 1; /**< Only valid for peer addresses. + This bit is set by the SoftDevice to indicate whether the address has been resolved + from a Resolvable Private Address (when the peer is using privacy). If set to + 1, @ref addr and @ref addr_type refer to the identity address of the resolved address. + + This bit is ignored when a variable of type @ref ble_gap_addr_t is used as input to API + functions. + */ + uint8_t addr_type : 7; /**< See @ref BLE_GAP_ADDR_TYPES. */ + uint8_t addr[BLE_GAP_ADDR_LEN]; /**< 48-bit address, LSB format. + @ref addr is not used if @ref addr_type is @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. */ } ble_gap_addr_t; /**@brief GAP connection parameters. @@ -834,38 +806,37 @@ typedef struct { * (1 + Conn_Latency) * Conn_Interval_Max * 2, where Conn_Interval_Max is given in milliseconds. */ typedef struct { - uint16_t min_conn_interval; /**< Minimum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ - uint16_t max_conn_interval; /**< Maximum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ - uint16_t slave_latency; /**< Slave Latency in number of connection events, see @ref BLE_GAP_CP_LIMITS.*/ - uint16_t conn_sup_timeout; /**< Connection Supervision Timeout in 10 ms units, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t min_conn_interval; /**< Minimum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t max_conn_interval; /**< Maximum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t slave_latency; /**< Slave Latency in number of connection events, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t conn_sup_timeout; /**< Connection Supervision Timeout in 10 ms units, see @ref BLE_GAP_CP_LIMITS.*/ } ble_gap_conn_params_t; /**@brief GAP connection security modes. * - * Security Mode 0 Level 0: No access permissions at all (this level is not defined by the Bluetooth Core specification).\n - * Security Mode 1 Level 1: No security is needed (aka open link).\n - * Security Mode 1 Level 2: Encrypted link required, MITM protection not necessary.\n - * Security Mode 1 Level 3: MITM protected encrypted link required.\n - * Security Mode 1 Level 4: LESC MITM protected encrypted link using a 128-bit strength encryption key required.\n - * Security Mode 2 Level 1: Signing or encryption required, MITM protection not necessary.\n - * Security Mode 2 Level 2: MITM protected signing required, unless link is MITM protected encrypted.\n + * Security Mode 0 Level 0: No access permissions at all (this level is not defined by the Bluetooth Core + * specification).\n Security Mode 1 Level 1: No security is needed (aka open link).\n Security Mode 1 Level 2: + * Encrypted link required, MITM protection not necessary.\n Security Mode 1 Level 3: MITM protected encrypted link + * required.\n Security Mode 1 Level 4: LESC MITM protected encrypted link using a 128-bit strength encryption key + * required.\n Security Mode 2 Level 1: Signing or encryption required, MITM protection not necessary.\n Security Mode 2 + * Level 2: MITM protected signing required, unless link is MITM protected encrypted.\n */ typedef struct { - uint8_t sm : 4; /**< Security Mode (1 or 2), 0 for no permissions at all. */ - uint8_t lv : 4; /**< Level (1, 2, 3 or 4), 0 for no permissions at all. */ + uint8_t sm : 4; /**< Security Mode (1 or 2), 0 for no permissions at all. */ + uint8_t lv : 4; /**< Level (1, 2, 3 or 4), 0 for no permissions at all. */ } ble_gap_conn_sec_mode_t; /**@brief GAP connection security status.*/ typedef struct { - ble_gap_conn_sec_mode_t sec_mode; /**< Currently active security mode for this connection.*/ - uint8_t - encr_key_size; /**< Length of currently active encryption key, 7 to 16 octets (only applicable for bonding procedures). */ + ble_gap_conn_sec_mode_t sec_mode; /**< Currently active security mode for this connection.*/ + uint8_t encr_key_size; /**< Length of currently active encryption key, 7 to 16 octets (only applicable for bonding + procedures). */ } ble_gap_conn_sec_t; /**@brief Identity Resolving Key. */ typedef struct { - uint8_t irk[BLE_GAP_SEC_KEY_LEN]; /**< Array containing IRK. */ + uint8_t irk[BLE_GAP_SEC_KEY_LEN]; /**< Array containing IRK. */ } ble_gap_irk_t; /**@brief Channel mask (40 bits). @@ -877,68 +848,68 @@ typedef uint8_t ble_gap_ch_mask_t[5]; /**@brief GAP advertising parameters. */ typedef struct { - ble_gap_adv_properties_t properties; /**< The properties of the advertising events. */ - ble_gap_addr_t const *p_peer_addr; /**< Address of a known peer. - @note ble_gap_addr_t::addr_type cannot be - @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. - - When privacy is enabled and the local device uses - @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE addresses, - the device identity list is searched for a matching entry. If - the local IRK for that device identity is set, the local IRK - for that device will be used to generate the advertiser address - field in the advertising packet. - - If @ref ble_gap_adv_properties_t::type is directed, this must be - set to the targeted scanner or initiator. If the peer address is - in the device identity list, the peer IRK for that device will be - used to generate @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE - target addresses used in the advertising event PDUs. */ - uint32_t interval; /**< Advertising interval in 625 us units. @sa BLE_GAP_ADV_INTERVALS. - @note If @ref ble_gap_adv_properties_t::type is set to - @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE - advertising, this parameter is ignored. */ - uint16_t duration; /**< Advertising duration in 10 ms units. When timeout is reached, - an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised. - @sa BLE_GAP_ADV_TIMEOUT_VALUES. - @note The SoftDevice will always complete at least one advertising - event even if the duration is set too low. */ - uint8_t max_adv_evts; /**< Maximum advertising events that shall be sent prior to disabling - advertising. Setting the value to 0 disables the limitation. When - the count of advertising events specified by this parameter - (if not 0) is reached, advertising will be automatically stopped - and an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised - @note If @ref ble_gap_adv_properties_t::type is set to - @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE, - this parameter is ignored. */ - ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. - At least one of the primary channels, that is channel index 37-39, must be used. - Masking away secondary advertising channels is not supported. */ - uint8_t filter_policy; /**< Filter Policy. @sa BLE_GAP_ADV_FILTER_POLICIES. */ - uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising channel packets - are transmitted. If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS - will be used. - Valid values are @ref BLE_GAP_PHY_1MBPS and @ref BLE_GAP_PHY_CODED. - @note The primary_phy shall indicate @ref BLE_GAP_PHY_1MBPS if - @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ - uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising channel packets - are transmitted. - If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS will be used. - Valid values are - @ref BLE_GAP_PHY_1MBPS, @ref BLE_GAP_PHY_2MBPS, and @ref BLE_GAP_PHY_CODED. - If @ref ble_gap_adv_properties_t::type is an extended advertising type - and connectable, this is the PHY that will be used to establish a - connection and send AUX_ADV_IND packets on. - @note This parameter will be ignored when - @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ - uint8_t set_id : 4; /**< The advertising set identifier distinguishes this advertising set from other - advertising sets transmitted by this and other devices. - @note This parameter will be ignored when - @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ - uint8_t scan_req_notification : 1; /**< Enable scan request notifications for this advertising set. When a - scan request is received and the scanner address is allowed - by the filter policy, @ref BLE_GAP_EVT_SCAN_REQ_REPORT is raised. - @note This parameter will be ignored when - @ref ble_gap_adv_properties_t::type is a non-scannable - advertising type. */ + ble_gap_adv_properties_t properties; /**< The properties of the advertising events. */ + ble_gap_addr_t const *p_peer_addr; /**< Address of a known peer. + @note ble_gap_addr_t::addr_type cannot be + @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. + - When privacy is enabled and the local device uses + @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE addresses, + the device identity list is searched for a matching entry. If + the local IRK for that device identity is set, the local IRK + for that device will be used to generate the advertiser address + field in the advertising packet. + - If @ref ble_gap_adv_properties_t::type is directed, this must be + set to the targeted scanner or initiator. If the peer address is + in the device identity list, the peer IRK for that device will be + used to generate @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE + target addresses used in the advertising event PDUs. */ + uint32_t interval; /**< Advertising interval in 625 us units. @sa BLE_GAP_ADV_INTERVALS. + @note If @ref ble_gap_adv_properties_t::type is set to + @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE + advertising, this parameter is ignored. */ + uint16_t duration; /**< Advertising duration in 10 ms units. When timeout is reached, + an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised. + @sa BLE_GAP_ADV_TIMEOUT_VALUES. + @note The SoftDevice will always complete at least one advertising + event even if the duration is set too low. */ + uint8_t max_adv_evts; /**< Maximum advertising events that shall be sent prior to disabling + advertising. Setting the value to 0 disables the limitation. When + the count of advertising events specified by this parameter + (if not 0) is reached, advertising will be automatically stopped + and an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised + @note If @ref ble_gap_adv_properties_t::type is set to + @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE, + this parameter is ignored. */ + ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. + At least one of the primary channels, that is channel index 37-39, must be used. + Masking away secondary advertising channels is not supported. */ + uint8_t filter_policy; /**< Filter Policy. @sa BLE_GAP_ADV_FILTER_POLICIES. */ + uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising channel packets + are transmitted. If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS + will be used. + Valid values are @ref BLE_GAP_PHY_1MBPS and @ref BLE_GAP_PHY_CODED. + @note The primary_phy shall indicate @ref BLE_GAP_PHY_1MBPS if + @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ + uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising channel packets + are transmitted. + If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS will be used. + Valid values are + @ref BLE_GAP_PHY_1MBPS, @ref BLE_GAP_PHY_2MBPS, and @ref BLE_GAP_PHY_CODED. + If @ref ble_gap_adv_properties_t::type is an extended advertising type + and connectable, this is the PHY that will be used to establish a + connection and send AUX_ADV_IND packets on. + @note This parameter will be ignored when + @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ + uint8_t set_id : 4; /**< The advertising set identifier distinguishes this advertising set from other + advertising sets transmitted by this and other devices. + @note This parameter will be ignored when + @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ + uint8_t scan_req_notification : 1; /**< Enable scan request notifications for this advertising set. When a + scan request is received and the scanner address is allowed + by the filter policy, @ref BLE_GAP_EVT_SCAN_REQ_REPORT is raised. + @note This parameter will be ignored when + @ref ble_gap_adv_properties_t::type is a non-scannable + advertising type. */ } ble_gap_adv_params_t; /**@brief GAP advertising data buffers. @@ -952,64 +923,64 @@ typedef struct { * - Advertising data is changed. * To update advertising data while advertising, provide new buffers to @ref sd_ble_gap_adv_set_configure. */ typedef struct { - ble_data_t adv_data; /**< Advertising data. - @note - Advertising data can only be specified for a @ref ble_gap_adv_properties_t::type - that is allowed to contain advertising data. */ - ble_data_t scan_rsp_data; /**< Scan response data. - @note - Scan response data can only be specified for a @ref ble_gap_adv_properties_t::type - that is scannable. */ + ble_data_t adv_data; /**< Advertising data. + @note + Advertising data can only be specified for a @ref ble_gap_adv_properties_t::type + that is allowed to contain advertising data. */ + ble_data_t scan_rsp_data; /**< Scan response data. + @note + Scan response data can only be specified for a @ref ble_gap_adv_properties_t::type + that is scannable. */ } ble_gap_adv_data_t; /**@brief GAP scanning parameters. */ typedef struct { - uint8_t extended : 1; /**< If 1, the scanner will accept extended advertising packets. - If set to 0, the scanner will not receive advertising packets - on secondary advertising channels, and will not be able - to receive long advertising PDUs. */ - uint8_t report_incomplete_evts : 1; /**< If 1, events of type @ref ble_gap_evt_adv_report_t may have - @ref ble_gap_adv_report_type_t::status set to - @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. - This parameter is ignored when used with @ref sd_ble_gap_connect - @note This may be used to abort receiving more packets from an extended - advertising event, and is only available for extended - scanning, see @ref sd_ble_gap_scan_start. - @note This feature is not supported by this SoftDevice. */ - uint8_t active : 1; /**< If 1, perform active scanning by sending scan requests. - This parameter is ignored when used with @ref sd_ble_gap_connect. */ - uint8_t filter_policy : 2; /**< Scanning filter policy. @sa BLE_GAP_SCAN_FILTER_POLICIES. - @note Only @ref BLE_GAP_SCAN_FP_ACCEPT_ALL and - @ref BLE_GAP_SCAN_FP_WHITELIST are valid when used with - @ref sd_ble_gap_connect */ - uint8_t scan_phys; /**< Bitfield of PHYs to scan on. If set to @ref BLE_GAP_PHY_AUTO, - scan_phys will default to @ref BLE_GAP_PHY_1MBPS. - - If @ref ble_gap_scan_params_t::extended is set to 0, the only - supported PHY is @ref BLE_GAP_PHY_1MBPS. - - When used with @ref sd_ble_gap_scan_start, - the bitfield indicates the PHYs the scanner will use for scanning - on primary advertising channels. The scanner will accept - @ref BLE_GAP_PHYS_SUPPORTED as secondary advertising channel PHYs. - - When used with @ref sd_ble_gap_connect, the bitfield indicates - the PHYs the initiator will use for scanning on primary advertising - channels. The initiator will accept connections initiated on either - of the @ref BLE_GAP_PHYS_SUPPORTED PHYs. - If scan_phys contains @ref BLE_GAP_PHY_1MBPS and/or @ref BLE_GAP_PHY_2MBPS, - the primary scan PHY is @ref BLE_GAP_PHY_1MBPS. - If scan_phys also contains @ref BLE_GAP_PHY_CODED, the primary scan - PHY will also contain @ref BLE_GAP_PHY_CODED. If the only scan PHY is - @ref BLE_GAP_PHY_CODED, the primary scan PHY is - @ref BLE_GAP_PHY_CODED only. */ - uint16_t interval; /**< Scan interval in 625 us units. @sa BLE_GAP_SCAN_INTERVALS. */ - uint16_t window; /**< Scan window in 625 us units. @sa BLE_GAP_SCAN_WINDOW. - If scan_phys contains both @ref BLE_GAP_PHY_1MBPS and - @ref BLE_GAP_PHY_CODED interval shall be larger than or - equal to twice the scan window. */ - uint16_t timeout; /**< Scan timeout in 10 ms units. @sa BLE_GAP_SCAN_TIMEOUT. */ - ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. - At least one of the primary channels, that is channel index 37-39, must be - set to 0. - Masking away secondary channels is not supported. */ + uint8_t extended : 1; /**< If 1, the scanner will accept extended advertising packets. + If set to 0, the scanner will not receive advertising packets + on secondary advertising channels, and will not be able + to receive long advertising PDUs. */ + uint8_t report_incomplete_evts : 1; /**< If 1, events of type @ref ble_gap_evt_adv_report_t may have + @ref ble_gap_adv_report_type_t::status set to + @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. + This parameter is ignored when used with @ref sd_ble_gap_connect + @note This may be used to abort receiving more packets from an extended + advertising event, and is only available for extended + scanning, see @ref sd_ble_gap_scan_start. + @note This feature is not supported by this SoftDevice. */ + uint8_t active : 1; /**< If 1, perform active scanning by sending scan requests. + This parameter is ignored when used with @ref sd_ble_gap_connect. */ + uint8_t filter_policy : 2; /**< Scanning filter policy. @sa BLE_GAP_SCAN_FILTER_POLICIES. + @note Only @ref BLE_GAP_SCAN_FP_ACCEPT_ALL and + @ref BLE_GAP_SCAN_FP_WHITELIST are valid when used with + @ref sd_ble_gap_connect */ + uint8_t scan_phys; /**< Bitfield of PHYs to scan on. If set to @ref BLE_GAP_PHY_AUTO, + scan_phys will default to @ref BLE_GAP_PHY_1MBPS. + - If @ref ble_gap_scan_params_t::extended is set to 0, the only + supported PHY is @ref BLE_GAP_PHY_1MBPS. + - When used with @ref sd_ble_gap_scan_start, + the bitfield indicates the PHYs the scanner will use for scanning + on primary advertising channels. The scanner will accept + @ref BLE_GAP_PHYS_SUPPORTED as secondary advertising channel PHYs. + - When used with @ref sd_ble_gap_connect, the bitfield indicates + the PHYs the initiator will use for scanning on primary advertising + channels. The initiator will accept connections initiated on either + of the @ref BLE_GAP_PHYS_SUPPORTED PHYs. + If scan_phys contains @ref BLE_GAP_PHY_1MBPS and/or @ref BLE_GAP_PHY_2MBPS, + the primary scan PHY is @ref BLE_GAP_PHY_1MBPS. + If scan_phys also contains @ref BLE_GAP_PHY_CODED, the primary scan + PHY will also contain @ref BLE_GAP_PHY_CODED. If the only scan PHY is + @ref BLE_GAP_PHY_CODED, the primary scan PHY is + @ref BLE_GAP_PHY_CODED only. */ + uint16_t interval; /**< Scan interval in 625 us units. @sa BLE_GAP_SCAN_INTERVALS. */ + uint16_t window; /**< Scan window in 625 us units. @sa BLE_GAP_SCAN_WINDOW. + If scan_phys contains both @ref BLE_GAP_PHY_1MBPS and + @ref BLE_GAP_PHY_CODED interval shall be larger than or + equal to twice the scan window. */ + uint16_t timeout; /**< Scan timeout in 10 ms units. @sa BLE_GAP_SCAN_TIMEOUT. */ + ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. + At least one of the primary channels, that is channel index 37-39, must be + set to 0. + Masking away secondary channels is not supported. */ } ble_gap_scan_params_t; /**@brief Privacy. @@ -1019,33 +990,34 @@ typedef struct { * that is automatically refreshed at a specified interval. * * If a device still wants to be recognized by other peers, it needs to share it's Identity Resolving Key (IRK). - * With this key, a device can generate a random private address that can only be recognized by peers in possession of that - * key, and devices can establish connections without revealing their real identities. + * With this key, a device can generate a random private address that can only be recognized by peers in + * possession of that key, and devices can establish connections without revealing their real identities. * * Both network privacy (@ref BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY) and device privacy (@ref * BLE_GAP_PRIVACY_MODE_DEVICE_PRIVACY) are supported. * * @note If the device IRK is updated, the new IRK becomes the one to be distributed in all * bonding procedures performed after @ref sd_ble_gap_privacy_set returns. - * The IRK distributed during bonding procedure is the device IRK that is active when @ref sd_ble_gap_sec_params_reply is - * called. + * The IRK distributed during bonding procedure is the device IRK that is active when @ref + * sd_ble_gap_sec_params_reply is called. */ typedef struct { - uint8_t privacy_mode; /**< Privacy mode, see @ref BLE_GAP_PRIVACY_MODES. Default is @ref BLE_GAP_PRIVACY_MODE_OFF. */ - uint8_t private_addr_type; /**< The private address type must be either @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE or - @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE. */ - uint16_t private_addr_cycle_s; /**< Private address cycle interval in seconds. Providing an address cycle value of 0 will use - the default value defined by @ref BLE_GAP_DEFAULT_PRIVATE_ADDR_CYCLE_INTERVAL_S. */ - ble_gap_irk_t - *p_device_irk; /**< When used as input, pointer to IRK structure that will be used as the default IRK. If NULL, the device - default IRK will be used. When used as output, pointer to IRK structure where the current default IRK - will be written to. If NULL, this argument is ignored. By default, the default IRK is used to generate - random private resolvable addresses for the local device unless instructed otherwise. */ + uint8_t privacy_mode; /**< Privacy mode, see @ref BLE_GAP_PRIVACY_MODES. Default is @ref BLE_GAP_PRIVACY_MODE_OFF. */ + uint8_t private_addr_type; /**< The private address type must be either @ref + BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE or + @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE. */ + uint16_t private_addr_cycle_s; /**< Private address cycle interval in seconds. Providing an address cycle value of 0 will + use the default value defined by @ref BLE_GAP_DEFAULT_PRIVATE_ADDR_CYCLE_INTERVAL_S. */ + ble_gap_irk_t *p_device_irk; /**< When used as input, pointer to IRK structure that will be used as the default IRK. + If NULL, the device default IRK will be used. When used as output, pointer to IRK + structure where the current default IRK will be written to. If NULL, this argument is + ignored. By default, the default IRK is used to generate random private resolvable + addresses for the local device unless instructed otherwise. */ } ble_gap_privacy_params_t; /**@brief PHY preferences for TX and RX - * @note tx_phys and rx_phys are bit fields. Multiple bits can be set in them to indicate multiple preferred PHYs for each - * direction. + * @note tx_phys and rx_phys are bit fields. Multiple bits can be set in them to indicate multiple preferred PHYs for + * each direction. * @code * p_gap_phys->tx_phys = BLE_GAP_PHY_1MBPS | BLE_GAP_PHY_2MBPS; * p_gap_phys->rx_phys = BLE_GAP_PHY_1MBPS | BLE_GAP_PHY_2MBPS; @@ -1053,258 +1025,258 @@ typedef struct { * */ typedef struct { - uint8_t tx_phys; /**< Preferred transmit PHYs, see @ref BLE_GAP_PHYS. */ - uint8_t rx_phys; /**< Preferred receive PHYs, see @ref BLE_GAP_PHYS. */ + uint8_t tx_phys; /**< Preferred transmit PHYs, see @ref BLE_GAP_PHYS. */ + uint8_t rx_phys; /**< Preferred receive PHYs, see @ref BLE_GAP_PHYS. */ } ble_gap_phys_t; /** @brief Keys that can be exchanged during a bonding procedure. */ typedef struct { - uint8_t enc : 1; /**< Long Term Key and Master Identification. */ - uint8_t id : 1; /**< Identity Resolving Key and Identity Address Information. */ - uint8_t sign : 1; /**< Connection Signature Resolving Key. */ - uint8_t link : 1; /**< Derive the Link Key from the LTK. */ + uint8_t enc : 1; /**< Long Term Key and Master Identification. */ + uint8_t id : 1; /**< Identity Resolving Key and Identity Address Information. */ + uint8_t sign : 1; /**< Connection Signature Resolving Key. */ + uint8_t link : 1; /**< Derive the Link Key from the LTK. */ } ble_gap_sec_kdist_t; /**@brief GAP security parameters. */ typedef struct { - uint8_t bond : 1; /**< Perform bonding. */ - uint8_t mitm : 1; /**< Enable Man In The Middle protection. */ - uint8_t lesc : 1; /**< Enable LE Secure Connection pairing. */ - uint8_t keypress : 1; /**< Enable generation of keypress notifications. */ - uint8_t io_caps : 3; /**< IO capabilities, see @ref BLE_GAP_IO_CAPS. */ - uint8_t oob : 1; /**< The OOB data flag. - - In LE legacy pairing, this flag is set if a device has out of band authentication data. - The OOB method is used if both of the devices have out of band authentication data. - - In LE Secure Connections pairing, this flag is set if a device has the peer device's out of band - authentication data. The OOB method is used if at least one device has the peer device's OOB data - available. */ - uint8_t - min_key_size; /**< Minimum encryption key size in octets between 7 and 16. If 0 then not applicable in this instance. */ - uint8_t max_key_size; /**< Maximum encryption key size in octets between min_key_size and 16. */ - ble_gap_sec_kdist_t kdist_own; /**< Key distribution bitmap: keys that the local device will distribute. */ - ble_gap_sec_kdist_t kdist_peer; /**< Key distribution bitmap: keys that the remote device will distribute. */ + uint8_t bond : 1; /**< Perform bonding. */ + uint8_t mitm : 1; /**< Enable Man In The Middle protection. */ + uint8_t lesc : 1; /**< Enable LE Secure Connection pairing. */ + uint8_t keypress : 1; /**< Enable generation of keypress notifications. */ + uint8_t io_caps : 3; /**< IO capabilities, see @ref BLE_GAP_IO_CAPS. */ + uint8_t oob : 1; /**< The OOB data flag. + - In LE legacy pairing, this flag is set if a device has out of band authentication data. + The OOB method is used if both of the devices have out of band authentication data. + - In LE Secure Connections pairing, this flag is set if a device has the peer device's out of + band authentication data. The OOB method is used if at least one device has the peer + device's OOB data available. */ + uint8_t min_key_size; /**< Minimum encryption key size in octets between 7 and 16. If 0 then not applicable in this + instance. */ + uint8_t max_key_size; /**< Maximum encryption key size in octets between min_key_size and 16. */ + ble_gap_sec_kdist_t kdist_own; /**< Key distribution bitmap: keys that the local device will distribute. */ + ble_gap_sec_kdist_t kdist_peer; /**< Key distribution bitmap: keys that the remote device will distribute. */ } ble_gap_sec_params_t; /**@brief GAP Encryption Information. */ typedef struct { - uint8_t ltk[BLE_GAP_SEC_KEY_LEN]; /**< Long Term Key. */ - uint8_t lesc : 1; /**< Key generated using LE Secure Connections. */ - uint8_t auth : 1; /**< Authenticated Key. */ - uint8_t ltk_len : 6; /**< LTK length in octets. */ + uint8_t ltk[BLE_GAP_SEC_KEY_LEN]; /**< Long Term Key. */ + uint8_t lesc : 1; /**< Key generated using LE Secure Connections. */ + uint8_t auth : 1; /**< Authenticated Key. */ + uint8_t ltk_len : 6; /**< LTK length in octets. */ } ble_gap_enc_info_t; /**@brief GAP Master Identification. */ typedef struct { - uint16_t ediv; /**< Encrypted Diversifier. */ - uint8_t rand[BLE_GAP_SEC_RAND_LEN]; /**< Random Number. */ + uint16_t ediv; /**< Encrypted Diversifier. */ + uint8_t rand[BLE_GAP_SEC_RAND_LEN]; /**< Random Number. */ } ble_gap_master_id_t; /**@brief GAP Signing Information. */ typedef struct { - uint8_t csrk[BLE_GAP_SEC_KEY_LEN]; /**< Connection Signature Resolving Key. */ + uint8_t csrk[BLE_GAP_SEC_KEY_LEN]; /**< Connection Signature Resolving Key. */ } ble_gap_sign_info_t; /**@brief GAP LE Secure Connections P-256 Public Key. */ typedef struct { - uint8_t pk[BLE_GAP_LESC_P256_PK_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman P-256 Public Key. Stored in the - standard SMP protocol format: {X,Y} both in little-endian. */ + uint8_t pk[BLE_GAP_LESC_P256_PK_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman P-256 Public Key. + Stored in the standard SMP protocol format: {X,Y} both in little-endian. */ } ble_gap_lesc_p256_pk_t; /**@brief GAP LE Secure Connections DHKey. */ typedef struct { - uint8_t key[BLE_GAP_LESC_DHKEY_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman Key. Stored in little-endian. */ + uint8_t key[BLE_GAP_LESC_DHKEY_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman Key. Stored in + little-endian. */ } ble_gap_lesc_dhkey_t; /**@brief GAP LE Secure Connections OOB data. */ typedef struct { - ble_gap_addr_t addr; /**< Bluetooth address of the device. */ - uint8_t r[BLE_GAP_SEC_KEY_LEN]; /**< Random Number. */ - uint8_t c[BLE_GAP_SEC_KEY_LEN]; /**< Confirm Value. */ + ble_gap_addr_t addr; /**< Bluetooth address of the device. */ + uint8_t r[BLE_GAP_SEC_KEY_LEN]; /**< Random Number. */ + uint8_t c[BLE_GAP_SEC_KEY_LEN]; /**< Confirm Value. */ } ble_gap_lesc_oob_data_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONNECTED. */ typedef struct { - ble_gap_addr_t - peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref - ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's identity address. */ - uint8_t role; /**< BLE role for this connection, see @ref BLE_GAP_ROLES */ - ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ - uint8_t adv_handle; /**< Advertising handle in which advertising has ended. - This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ - ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated - advertising set. The advertising buffers provided in - @ref sd_ble_gap_adv_set_configure are now released. - This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref + ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's + identity address. */ + uint8_t role; /**< BLE role for this connection, see @ref BLE_GAP_ROLES */ + ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ + uint8_t adv_handle; /**< Advertising handle in which advertising has ended. + This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ + ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated + advertising set. The advertising buffers provided in + @ref sd_ble_gap_adv_set_configure are now released. + This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ } ble_gap_evt_connected_t; /**@brief Event structure for @ref BLE_GAP_EVT_DISCONNECTED. */ typedef struct { - uint8_t reason; /**< HCI error code, see @ref BLE_HCI_STATUS_CODES. */ + uint8_t reason; /**< HCI error code, see @ref BLE_HCI_STATUS_CODES. */ } ble_gap_evt_disconnected_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONN_PARAM_UPDATE. */ typedef struct { - ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ + ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ } ble_gap_evt_conn_param_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_PHY_UPDATE_REQUEST. */ typedef struct { - ble_gap_phys_t peer_preferred_phys; /**< The PHYs the peer prefers to use. */ + ble_gap_phys_t peer_preferred_phys; /**< The PHYs the peer prefers to use. */ } ble_gap_evt_phy_update_request_t; /**@brief Event Structure for @ref BLE_GAP_EVT_PHY_UPDATE. */ typedef struct { - uint8_t status; /**< Status of the procedure, see @ref BLE_HCI_STATUS_CODES.*/ - uint8_t tx_phy; /**< TX PHY for this connection, see @ref BLE_GAP_PHYS. */ - uint8_t rx_phy; /**< RX PHY for this connection, see @ref BLE_GAP_PHYS. */ + uint8_t status; /**< Status of the procedure, see @ref BLE_HCI_STATUS_CODES.*/ + uint8_t tx_phy; /**< TX PHY for this connection, see @ref BLE_GAP_PHYS. */ + uint8_t rx_phy; /**< RX PHY for this connection, see @ref BLE_GAP_PHYS. */ } ble_gap_evt_phy_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST. */ typedef struct { - ble_gap_sec_params_t peer_params; /**< Initiator Security Parameters. */ + ble_gap_sec_params_t peer_params; /**< Initiator Security Parameters. */ } ble_gap_evt_sec_params_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_SEC_INFO_REQUEST. */ typedef struct { - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. */ - ble_gap_master_id_t master_id; /**< Master Identification for LTK lookup. */ - uint8_t enc_info : 1; /**< If 1, Encryption Information required. */ - uint8_t id_info : 1; /**< If 1, Identity Information required. */ - uint8_t sign_info : 1; /**< If 1, Signing Information required. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. */ + ble_gap_master_id_t master_id; /**< Master Identification for LTK lookup. */ + uint8_t enc_info : 1; /**< If 1, Encryption Information required. */ + uint8_t id_info : 1; /**< If 1, Identity Information required. */ + uint8_t sign_info : 1; /**< If 1, Signing Information required. */ } ble_gap_evt_sec_info_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_PASSKEY_DISPLAY. */ typedef struct { - uint8_t passkey[BLE_GAP_PASSKEY_LEN]; /**< 6-digit passkey in ASCII ('0'-'9' digits only). */ - uint8_t match_request : 1; /**< If 1 requires the application to report the match using @ref sd_ble_gap_auth_key_reply - with either @ref BLE_GAP_AUTH_KEY_TYPE_NONE if there is no match or - @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY if there is a match. */ + uint8_t passkey[BLE_GAP_PASSKEY_LEN]; /**< 6-digit passkey in ASCII ('0'-'9' digits only). */ + uint8_t match_request : 1; /**< If 1 requires the application to report the match using @ref sd_ble_gap_auth_key_reply + with either @ref BLE_GAP_AUTH_KEY_TYPE_NONE if there is no match or + @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY if there is a match. */ } ble_gap_evt_passkey_display_t; /**@brief Event structure for @ref BLE_GAP_EVT_KEY_PRESSED. */ typedef struct { - uint8_t kp_not; /**< Keypress notification type, see @ref BLE_GAP_KP_NOT_TYPES. */ + uint8_t kp_not; /**< Keypress notification type, see @ref BLE_GAP_KP_NOT_TYPES. */ } ble_gap_evt_key_pressed_t; /**@brief Event structure for @ref BLE_GAP_EVT_AUTH_KEY_REQUEST. */ typedef struct { - uint8_t key_type; /**< See @ref BLE_GAP_AUTH_KEY_TYPES. */ + uint8_t key_type; /**< See @ref BLE_GAP_AUTH_KEY_TYPES. */ } ble_gap_evt_auth_key_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST. */ typedef struct { - ble_gap_lesc_p256_pk_t - *p_pk_peer; /**< LE Secure Connections remote P-256 Public Key. This will point to the application-supplied memory - inside the keyset during the call to @ref sd_ble_gap_sec_params_reply. */ - uint8_t oobd_req : 1; /**< LESC OOB data required. A call to @ref sd_ble_gap_lesc_oob_data_set is required to complete the - procedure. */ + ble_gap_lesc_p256_pk_t *p_pk_peer; /**< LE Secure Connections remote P-256 Public Key. This will point to the application-supplied memory + inside the keyset during the call to @ref sd_ble_gap_sec_params_reply. */ + uint8_t oobd_req : 1; /**< LESC OOB data required. A call to @ref sd_ble_gap_lesc_oob_data_set is required to complete + the procedure. */ } ble_gap_evt_lesc_dhkey_request_t; /**@brief Security levels supported. * @note See Bluetooth Specification Version 4.2 Volume 3, Part C, Chapter 10, Section 10.2.1. */ typedef struct { - uint8_t lv1 : 1; /**< If 1: Level 1 is supported. */ - uint8_t lv2 : 1; /**< If 1: Level 2 is supported. */ - uint8_t lv3 : 1; /**< If 1: Level 3 is supported. */ - uint8_t lv4 : 1; /**< If 1: Level 4 is supported. */ + uint8_t lv1 : 1; /**< If 1: Level 1 is supported. */ + uint8_t lv2 : 1; /**< If 1: Level 2 is supported. */ + uint8_t lv3 : 1; /**< If 1: Level 3 is supported. */ + uint8_t lv4 : 1; /**< If 1: Level 4 is supported. */ } ble_gap_sec_levels_t; /**@brief Encryption Key. */ typedef struct { - ble_gap_enc_info_t enc_info; /**< Encryption Information. */ - ble_gap_master_id_t master_id; /**< Master Identification. */ + ble_gap_enc_info_t enc_info; /**< Encryption Information. */ + ble_gap_master_id_t master_id; /**< Master Identification. */ } ble_gap_enc_key_t; /**@brief Identity Key. */ typedef struct { - ble_gap_irk_t id_info; /**< Identity Resolving Key. */ - ble_gap_addr_t id_addr_info; /**< Identity Address. */ + ble_gap_irk_t id_info; /**< Identity Resolving Key. */ + ble_gap_addr_t id_addr_info; /**< Identity Address. */ } ble_gap_id_key_t; /**@brief Security Keys. */ typedef struct { - ble_gap_enc_key_t *p_enc_key; /**< Encryption Key, or NULL. */ - ble_gap_id_key_t *p_id_key; /**< Identity Key, or NULL. */ - ble_gap_sign_info_t *p_sign_key; /**< Signing Key, or NULL. */ - ble_gap_lesc_p256_pk_t *p_pk; /**< LE Secure Connections P-256 Public Key. When in debug mode the application must use the - value defined in the Core Bluetooth Specification v4.2 Vol.3, Part H, Section 2.3.5.6.1 */ + ble_gap_enc_key_t *p_enc_key; /**< Encryption Key, or NULL. */ + ble_gap_id_key_t *p_id_key; /**< Identity Key, or NULL. */ + ble_gap_sign_info_t *p_sign_key; /**< Signing Key, or NULL. */ + ble_gap_lesc_p256_pk_t *p_pk; /**< LE Secure Connections P-256 Public Key. When in debug mode the application must use the + value defined in the Core Bluetooth Specification v4.2 Vol.3, Part H, Section 2.3.5.6.1 */ } ble_gap_sec_keys_t; /**@brief Security key set for both local and peer keys. */ typedef struct { - ble_gap_sec_keys_t keys_own; /**< Keys distributed by the local device. For LE Secure Connections the encryption key will be - generated locally and will always be stored if bonding. */ - ble_gap_sec_keys_t - keys_peer; /**< Keys distributed by the remote device. For LE Secure Connections, p_enc_key must always be NULL. */ + ble_gap_sec_keys_t keys_own; /**< Keys distributed by the local device. For LE Secure Connections the encryption key + will be generated locally and will always be stored if bonding. */ + ble_gap_sec_keys_t keys_peer; /**< Keys distributed by the remote device. For LE Secure Connections, p_enc_key must + always be NULL. */ } ble_gap_sec_keyset_t; /**@brief Data Length Update Procedure parameters. */ typedef struct { - uint16_t max_tx_octets; /**< Maximum number of payload octets that a Controller supports for transmission of a single Link - Layer Data Channel PDU. */ - uint16_t max_rx_octets; /**< Maximum number of payload octets that a Controller supports for reception of a single Link Layer - Data Channel PDU. */ - uint16_t max_tx_time_us; /**< Maximum time, in microseconds, that a Controller supports for transmission of a single Link - Layer Data Channel PDU. */ - uint16_t max_rx_time_us; /**< Maximum time, in microseconds, that a Controller supports for reception of a single Link Layer - Data Channel PDU. */ + uint16_t max_tx_octets; /**< Maximum number of payload octets that a Controller supports for transmission of a single + Link Layer Data Channel PDU. */ + uint16_t max_rx_octets; /**< Maximum number of payload octets that a Controller supports for reception of a single + Link Layer Data Channel PDU. */ + uint16_t max_tx_time_us; /**< Maximum time, in microseconds, that a Controller supports for transmission of a single + Link Layer Data Channel PDU. */ + uint16_t max_rx_time_us; /**< Maximum time, in microseconds, that a Controller supports for reception of a single Link + Layer Data Channel PDU. */ } ble_gap_data_length_params_t; /**@brief Data Length Update Procedure local limitation. */ typedef struct { - uint16_t tx_payload_limited_octets; /**< If > 0, the requested TX packet length is too long by this many octets. */ - uint16_t rx_payload_limited_octets; /**< If > 0, the requested RX packet length is too long by this many octets. */ - uint16_t tx_rx_time_limited_us; /**< If > 0, the requested combination of TX and RX packet lengths is too long by this many - microseconds. */ + uint16_t tx_payload_limited_octets; /**< If > 0, the requested TX packet length is too long by this many octets. */ + uint16_t rx_payload_limited_octets; /**< If > 0, the requested RX packet length is too long by this many octets. */ + uint16_t tx_rx_time_limited_us; /**< If > 0, the requested combination of TX and RX packet lengths is too long by this + many microseconds. */ } ble_gap_data_length_limitation_t; /**@brief Event structure for @ref BLE_GAP_EVT_AUTH_STATUS. */ typedef struct { - uint8_t auth_status; /**< Authentication status, see @ref BLE_GAP_SEC_STATUS. */ - uint8_t error_src : 2; /**< On error, source that caused the failure, see @ref BLE_GAP_SEC_STATUS_SOURCES. */ - uint8_t bonded : 1; /**< Procedure resulted in a bond. */ - uint8_t lesc : 1; /**< Procedure resulted in a LE Secure Connection. */ - ble_gap_sec_levels_t sm1_levels; /**< Levels supported in Security Mode 1. */ - ble_gap_sec_levels_t sm2_levels; /**< Levels supported in Security Mode 2. */ - ble_gap_sec_kdist_t kdist_own; /**< Bitmap stating which keys were exchanged (distributed) by the local device. If bonding - with LE Secure Connections, the enc bit will be always set. */ - ble_gap_sec_kdist_t kdist_peer; /**< Bitmap stating which keys were exchanged (distributed) by the remote device. If bonding - with LE Secure Connections, the enc bit will never be set. */ + uint8_t auth_status; /**< Authentication status, see @ref BLE_GAP_SEC_STATUS. */ + uint8_t error_src : 2; /**< On error, source that caused the failure, see @ref BLE_GAP_SEC_STATUS_SOURCES. */ + uint8_t bonded : 1; /**< Procedure resulted in a bond. */ + uint8_t lesc : 1; /**< Procedure resulted in a LE Secure Connection. */ + ble_gap_sec_levels_t sm1_levels; /**< Levels supported in Security Mode 1. */ + ble_gap_sec_levels_t sm2_levels; /**< Levels supported in Security Mode 2. */ + ble_gap_sec_kdist_t kdist_own; /**< Bitmap stating which keys were exchanged (distributed) by the local device. If + bonding with LE Secure Connections, the enc bit will be always set. */ + ble_gap_sec_kdist_t kdist_peer; /**< Bitmap stating which keys were exchanged (distributed) by the remote device. If + bonding with LE Secure Connections, the enc bit will never be set. */ } ble_gap_evt_auth_status_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONN_SEC_UPDATE. */ typedef struct { - ble_gap_conn_sec_t conn_sec; /**< Connection security level. */ + ble_gap_conn_sec_t conn_sec; /**< Connection security level. */ } ble_gap_evt_conn_sec_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_TIMEOUT. */ typedef struct { - uint8_t src; /**< Source of timeout event, see @ref BLE_GAP_TIMEOUT_SOURCES. */ - union { - ble_data_t adv_report_buffer; /**< If source is set to @ref BLE_GAP_TIMEOUT_SRC_SCAN, the released - scan buffer is contained in this field. */ - } params; /**< Event Parameters. */ + uint8_t src; /**< Source of timeout event, see @ref BLE_GAP_TIMEOUT_SOURCES. */ + union { + ble_data_t adv_report_buffer; /**< If source is set to @ref BLE_GAP_TIMEOUT_SRC_SCAN, the released + scan buffer is contained in this field. */ + } params; /**< Event Parameters. */ } ble_gap_evt_timeout_t; /**@brief Event structure for @ref BLE_GAP_EVT_RSSI_CHANGED. */ typedef struct { - int8_t rssi; /**< Received Signal Strength Indication in dBm. - @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature - measurement. */ - uint8_t ch_index; /**< Data Channel Index on which the Signal Strength is measured (0-36). */ + int8_t rssi; /**< Received Signal Strength Indication in dBm. + @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature + measurement. */ + uint8_t ch_index; /**< Data Channel Index on which the Signal Strength is measured (0-36). */ } ble_gap_evt_rssi_changed_t; /**@brief Event structure for @ref BLE_GAP_EVT_ADV_SET_TERMINATED */ typedef struct { - uint8_t reason; /**< Reason for why the advertising set terminated. See - @ref BLE_GAP_EVT_ADV_SET_TERMINATED_REASON. */ - uint8_t adv_handle; /**< Advertising handle in which advertising has ended. */ - uint8_t num_completed_adv_events; /**< If @ref ble_gap_adv_params_t::max_adv_evts was not set to 0, - this field indicates the number of completed advertising events. */ - ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated - advertising set. The advertising buffers provided in - @ref sd_ble_gap_adv_set_configure are now released. */ + uint8_t reason; /**< Reason for why the advertising set terminated. See + @ref BLE_GAP_EVT_ADV_SET_TERMINATED_REASON. */ + uint8_t adv_handle; /**< Advertising handle in which advertising has ended. */ + uint8_t num_completed_adv_events; /**< If @ref ble_gap_adv_params_t::max_adv_evts was not set to 0, + this field indicates the number of completed advertising events. */ + ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated + advertising set. The advertising buffers provided in + @ref sd_ble_gap_adv_set_configure are now released. */ } ble_gap_evt_adv_set_terminated_t; /**@brief Event structure for @ref BLE_GAP_EVT_ADV_REPORT. @@ -1316,72 +1288,72 @@ typedef struct { * scanning will be paused. To continue scanning, call @ref sd_ble_gap_scan_start. */ typedef struct { - ble_gap_adv_report_type_t type; /**< Advertising report type. See @ref ble_gap_adv_report_type_t. */ - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr is resolved: - @ref ble_gap_addr_t::addr_id_peer is set to 1 and the address is the - peer's identity address. */ - ble_gap_addr_t direct_addr; /**< Contains the target address of the advertising event if - @ref ble_gap_adv_report_type_t::directed is set to 1. If the - SoftDevice was able to resolve the address, - @ref ble_gap_addr_t::addr_id_peer is set to 1 and the direct_addr - contains the local identity address. If the target address of the - advertising event is @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE, - and the SoftDevice was unable to resolve it, the application may try - to resolve this address to find out if the advertising event was - directed to us. */ - uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising packet was received. - See @ref BLE_GAP_PHYS. */ - uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising packet was received. - See @ref BLE_GAP_PHYS. This field is set to @ref BLE_GAP_PHY_NOT_SET if no packets - were received on a secondary advertising channel. */ - int8_t tx_power; /**< TX Power reported by the advertiser in the last packet header received. - This field is set to @ref BLE_GAP_POWER_LEVEL_INVALID if the - last received packet did not contain the Tx Power field. - @note TX Power is only included in extended advertising packets. */ - int8_t rssi; /**< Received Signal Strength Indication in dBm of the last packet received. - @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature - measurement. */ - uint8_t ch_index; /**< Channel Index on which the last advertising packet is received (0-39). */ - uint8_t set_id; /**< Set ID of the received advertising data. Set ID is not present - if set to @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ - uint16_t data_id : 12; /**< The advertising data ID of the received advertising data. Data ID - is not present if @ref ble_gap_evt_adv_report_t::set_id is set to - @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ - ble_data_t data; /**< Received advertising or scan response data. If - @ref ble_gap_adv_report_type_t::status is not set to - @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the data buffer provided - in @ref sd_ble_gap_scan_start is now released. */ - ble_gap_aux_pointer_t aux_pointer; /**< The offset and PHY of the next advertising packet in this extended advertising - event. @note This field is only set if @ref ble_gap_adv_report_type_t::status - is set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. */ + ble_gap_adv_report_type_t type; /**< Advertising report type. See @ref ble_gap_adv_report_type_t. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr is resolved: + @ref ble_gap_addr_t::addr_id_peer is set to 1 and the address is the + peer's identity address. */ + ble_gap_addr_t direct_addr; /**< Contains the target address of the advertising event if + @ref ble_gap_adv_report_type_t::directed is set to 1. If the + SoftDevice was able to resolve the address, + @ref ble_gap_addr_t::addr_id_peer is set to 1 and the direct_addr + contains the local identity address. If the target address of the + advertising event is @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE, + and the SoftDevice was unable to resolve it, the application may try + to resolve this address to find out if the advertising event was + directed to us. */ + uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising packet was received. + See @ref BLE_GAP_PHYS. */ + uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising packet was received. + See @ref BLE_GAP_PHYS. This field is set to @ref BLE_GAP_PHY_NOT_SET if no packets + were received on a secondary advertising channel. */ + int8_t tx_power; /**< TX Power reported by the advertiser in the last packet header received. + This field is set to @ref BLE_GAP_POWER_LEVEL_INVALID if the + last received packet did not contain the Tx Power field. + @note TX Power is only included in extended advertising packets. */ + int8_t rssi; /**< Received Signal Strength Indication in dBm of the last packet received. + @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature + measurement. */ + uint8_t ch_index; /**< Channel Index on which the last advertising packet is received (0-39). */ + uint8_t set_id; /**< Set ID of the received advertising data. Set ID is not present + if set to @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ + uint16_t data_id : 12; /**< The advertising data ID of the received advertising data. Data ID + is not present if @ref ble_gap_evt_adv_report_t::set_id is set to + @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ + ble_data_t data; /**< Received advertising or scan response data. If + @ref ble_gap_adv_report_type_t::status is not set to + @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the data buffer provided + in @ref sd_ble_gap_scan_start is now released. */ + ble_gap_aux_pointer_t aux_pointer; /**< The offset and PHY of the next advertising packet in this extended advertising + event. @note This field is only set if @ref ble_gap_adv_report_type_t::status + is set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. */ } ble_gap_evt_adv_report_t; /**@brief Event structure for @ref BLE_GAP_EVT_SEC_REQUEST. */ typedef struct { - uint8_t bond : 1; /**< Perform bonding. */ - uint8_t mitm : 1; /**< Man In The Middle protection requested. */ - uint8_t lesc : 1; /**< LE Secure Connections requested. */ - uint8_t keypress : 1; /**< Generation of keypress notifications requested. */ + uint8_t bond : 1; /**< Perform bonding. */ + uint8_t mitm : 1; /**< Man In The Middle protection requested. */ + uint8_t lesc : 1; /**< LE Secure Connections requested. */ + uint8_t keypress : 1; /**< Generation of keypress notifications requested. */ } ble_gap_evt_sec_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST. */ typedef struct { - ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ + ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ } ble_gap_evt_conn_param_update_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_SCAN_REQ_REPORT. */ typedef struct { - uint8_t adv_handle; /**< Advertising handle for the advertising set which received the Scan Request */ - int8_t rssi; /**< Received Signal Strength Indication in dBm. - @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature - measurement. */ - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref - ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's identity address. */ + uint8_t adv_handle; /**< Advertising handle for the advertising set which received the Scan Request */ + int8_t rssi; /**< Received Signal Strength Indication in dBm. + @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature + measurement. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref + ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's identity address. */ } ble_gap_evt_scan_req_report_t; /**@brief Event structure for @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST. */ typedef struct { - ble_gap_data_length_params_t peer_params; /**< Peer data length parameters. */ + ble_gap_data_length_params_t peer_params; /**< Peer data length parameters. */ } ble_gap_evt_data_length_update_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE. @@ -1389,48 +1361,47 @@ typedef struct { * @note This event may also be raised after a PHY Update procedure. */ typedef struct { - ble_gap_data_length_params_t effective_params; /**< The effective data length parameters. */ + ble_gap_data_length_params_t effective_params; /**< The effective data length parameters. */ } ble_gap_evt_data_length_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT. */ typedef struct { - int8_t - channel_energy[BLE_GAP_CHANNEL_COUNT]; /**< The measured energy on the Bluetooth Low Energy - channels, in dBm, indexed by Channel Index. - If no measurement is available for the given channel, channel_energy is set to - @ref BLE_GAP_POWER_LEVEL_INVALID. */ + int8_t channel_energy[BLE_GAP_CHANNEL_COUNT]; /**< The measured energy on the Bluetooth Low Energy + channels, in dBm, indexed by Channel Index. + If no measurement is available for the given channel, + channel_energy is set to + @ref BLE_GAP_POWER_LEVEL_INVALID. */ } ble_gap_evt_qos_channel_survey_report_t; /**@brief GAP event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which event occurred. */ - union /**< union alternative identified by evt_id in enclosing struct. */ - { - ble_gap_evt_connected_t connected; /**< Connected Event Parameters. */ - ble_gap_evt_disconnected_t disconnected; /**< Disconnected Event Parameters. */ - ble_gap_evt_conn_param_update_t conn_param_update; /**< Connection Parameter Update Parameters. */ - ble_gap_evt_sec_params_request_t sec_params_request; /**< Security Parameters Request Event Parameters. */ - ble_gap_evt_sec_info_request_t sec_info_request; /**< Security Information Request Event Parameters. */ - ble_gap_evt_passkey_display_t passkey_display; /**< Passkey Display Event Parameters. */ - ble_gap_evt_key_pressed_t key_pressed; /**< Key Pressed Event Parameters. */ - ble_gap_evt_auth_key_request_t auth_key_request; /**< Authentication Key Request Event Parameters. */ - ble_gap_evt_lesc_dhkey_request_t lesc_dhkey_request; /**< LE Secure Connections DHKey calculation request. */ - ble_gap_evt_auth_status_t auth_status; /**< Authentication Status Event Parameters. */ - ble_gap_evt_conn_sec_update_t conn_sec_update; /**< Connection Security Update Event Parameters. */ - ble_gap_evt_timeout_t timeout; /**< Timeout Event Parameters. */ - ble_gap_evt_rssi_changed_t rssi_changed; /**< RSSI Event Parameters. */ - ble_gap_evt_adv_report_t adv_report; /**< Advertising Report Event Parameters. */ - ble_gap_evt_adv_set_terminated_t adv_set_terminated; /**< Advertising Set Terminated Event Parameters. */ - ble_gap_evt_sec_request_t sec_request; /**< Security Request Event Parameters. */ - ble_gap_evt_conn_param_update_request_t conn_param_update_request; /**< Connection Parameter Update Parameters. */ - ble_gap_evt_scan_req_report_t scan_req_report; /**< Scan Request Report Parameters. */ - ble_gap_evt_phy_update_request_t phy_update_request; /**< PHY Update Request Event Parameters. */ - ble_gap_evt_phy_update_t phy_update; /**< PHY Update Parameters. */ - ble_gap_evt_data_length_update_request_t data_length_update_request; /**< Data Length Update Request Event Parameters. */ - ble_gap_evt_data_length_update_t data_length_update; /**< Data Length Update Event Parameters. */ - ble_gap_evt_qos_channel_survey_report_t - qos_channel_survey_report; /**< Quality of Service (QoS) Channel Survey Report Parameters. */ - } params; /**< Event Parameters. */ + uint16_t conn_handle; /**< Connection Handle on which event occurred. */ + union /**< union alternative identified by evt_id in enclosing struct. */ + { + ble_gap_evt_connected_t connected; /**< Connected Event Parameters. */ + ble_gap_evt_disconnected_t disconnected; /**< Disconnected Event Parameters. */ + ble_gap_evt_conn_param_update_t conn_param_update; /**< Connection Parameter Update Parameters. */ + ble_gap_evt_sec_params_request_t sec_params_request; /**< Security Parameters Request Event Parameters. */ + ble_gap_evt_sec_info_request_t sec_info_request; /**< Security Information Request Event Parameters. */ + ble_gap_evt_passkey_display_t passkey_display; /**< Passkey Display Event Parameters. */ + ble_gap_evt_key_pressed_t key_pressed; /**< Key Pressed Event Parameters. */ + ble_gap_evt_auth_key_request_t auth_key_request; /**< Authentication Key Request Event Parameters. */ + ble_gap_evt_lesc_dhkey_request_t lesc_dhkey_request; /**< LE Secure Connections DHKey calculation request. */ + ble_gap_evt_auth_status_t auth_status; /**< Authentication Status Event Parameters. */ + ble_gap_evt_conn_sec_update_t conn_sec_update; /**< Connection Security Update Event Parameters. */ + ble_gap_evt_timeout_t timeout; /**< Timeout Event Parameters. */ + ble_gap_evt_rssi_changed_t rssi_changed; /**< RSSI Event Parameters. */ + ble_gap_evt_adv_report_t adv_report; /**< Advertising Report Event Parameters. */ + ble_gap_evt_adv_set_terminated_t adv_set_terminated; /**< Advertising Set Terminated Event Parameters. */ + ble_gap_evt_sec_request_t sec_request; /**< Security Request Event Parameters. */ + ble_gap_evt_conn_param_update_request_t conn_param_update_request; /**< Connection Parameter Update Parameters. */ + ble_gap_evt_scan_req_report_t scan_req_report; /**< Scan Request Report Parameters. */ + ble_gap_evt_phy_update_request_t phy_update_request; /**< PHY Update Request Event Parameters. */ + ble_gap_evt_phy_update_t phy_update; /**< PHY Update Parameters. */ + ble_gap_evt_data_length_update_request_t data_length_update_request; /**< Data Length Update Request Event Parameters. */ + ble_gap_evt_data_length_update_t data_length_update; /**< Data Length Update Event Parameters. */ + ble_gap_evt_qos_channel_survey_report_t qos_channel_survey_report; /**< Quality of Service (QoS) Channel Survey Report Parameters. */ + } params; /**< Event Parameters. */ } ble_gap_evt_t; /** @@ -1438,17 +1409,18 @@ typedef struct { * * @retval ::NRF_ERROR_CONN_COUNT The connection count for the connection configurations is zero. * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: - * - The sum of conn_count for all connection configurations combined exceeds UINT8_MAX. + * - The sum of conn_count for all connection configurations combined exceeds + * UINT8_MAX. * - The event length is smaller than @ref BLE_GAP_EVENT_LENGTH_MIN. */ typedef struct { - uint8_t conn_count; /**< The number of concurrent connections the application can create with this configuration. - The default and minimum value is @ref BLE_GAP_CONN_COUNT_DEFAULT. */ - uint16_t event_length; /**< The time set aside for this connection on every connection interval in 1.25 ms units. - The default value is @ref BLE_GAP_EVENT_LENGTH_DEFAULT, the minimum value is @ref - BLE_GAP_EVENT_LENGTH_MIN. The event length and the connection interval are the primary parameters - for setting the throughput of a connection. - See the SoftDevice Specification for details on throughput. */ + uint8_t conn_count; /**< The number of concurrent connections the application can create with this configuration. + The default and minimum value is @ref BLE_GAP_CONN_COUNT_DEFAULT. */ + uint16_t event_length; /**< The time set aside for this connection on every connection interval in 1.25 ms units. + The default value is @ref BLE_GAP_EVENT_LENGTH_DEFAULT, the minimum value is @ref + BLE_GAP_EVENT_LENGTH_MIN. The event length and the connection interval are the primary parameters + for setting the throughput of a connection. + See the SoftDevice Specification for details on throughput. */ } ble_gap_conn_cfg_t; /** @@ -1464,15 +1436,17 @@ typedef struct { * @ref BLE_GAP_ADV_SET_COUNT_MAX. */ typedef struct { - uint8_t adv_set_count; /**< Maximum number of advertising sets. Default value is @ref BLE_GAP_ADV_SET_COUNT_DEFAULT. */ - uint8_t periph_role_count; /**< Maximum number of connections concurrently acting as a peripheral. Default value is @ref - BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT. */ - uint8_t central_role_count; /**< Maximum number of connections concurrently acting as a central. Default value is @ref - BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT. */ - uint8_t central_sec_count; /**< Number of SMP instances shared between all connections acting as a central. Default value is - @ref BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT. */ - uint8_t qos_channel_survey_role_available : 1; /**< If set, the Quality of Service (QoS) channel survey module is available to - the application using @ref sd_ble_gap_qos_channel_survey_start. */ + uint8_t adv_set_count; /**< Maximum number of advertising sets. Default value is @ref BLE_GAP_ADV_SET_COUNT_DEFAULT. */ + uint8_t periph_role_count; /**< Maximum number of connections concurrently acting as a peripheral. Default value is + @ref BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT. */ + uint8_t central_role_count; /**< Maximum number of connections concurrently acting as a central. Default value is @ref + BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT. */ + uint8_t central_sec_count; /**< Number of SMP instances shared between all connections acting as a central. Default + value is + @ref BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT. */ + uint8_t qos_channel_survey_role_available : 1; /**< If set, the Quality of Service (QoS) channel survey module is + available to the application using @ref + sd_ble_gap_qos_channel_survey_start. */ } ble_gap_cfg_role_count_t; /** @@ -1499,38 +1473,39 @@ typedef struct { * - Invalid device name location (vloc). * - Invalid device name security mode. * @retval ::NRF_ERROR_INVALID_LENGTH One or more of the following is true: - * - The device name length is invalid (must be between 0 and @ref BLE_GAP_DEVNAME_MAX_LEN). + * - The device name length is invalid (must be between 0 and @ref + * BLE_GAP_DEVNAME_MAX_LEN). * - The device name length is too long for the given Attribute Table. * @retval ::NRF_ERROR_NOT_SUPPORTED Device name security mode is not supported. */ typedef struct { - ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ - uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ - uint8_t *p_value; /**< Pointer to where the value (device name) is stored or will be stored. */ - uint16_t current_len; /**< Current length in bytes of the memory pointed to by p_value.*/ - uint16_t max_len; /**< Maximum length in bytes of the memory pointed to by p_value.*/ + ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ + uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ + uint8_t *p_value; /**< Pointer to where the value (device name) is stored or will be stored. */ + uint16_t current_len; /**< Current length in bytes of the memory pointed to by p_value.*/ + uint16_t max_len; /**< Maximum length in bytes of the memory pointed to by p_value.*/ } ble_gap_cfg_device_name_t; /**@brief Peripheral Preferred Connection Parameters include configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t include_cfg; /**< Inclusion configuration of the Peripheral Preferred Connection Parameters characteristic. - See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_PPCP_INCL_CONFIG_DEFAULT. */ + uint8_t include_cfg; /**< Inclusion configuration of the Peripheral Preferred Connection Parameters characteristic. + See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_PPCP_INCL_CONFIG_DEFAULT. */ } ble_gap_cfg_ppcp_incl_cfg_t; /**@brief Central Address Resolution include configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t include_cfg; /**< Inclusion configuration of the Central Address Resolution characteristic. - See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_CAR_INCL_CONFIG_DEFAULT. */ + uint8_t include_cfg; /**< Inclusion configuration of the Central Address Resolution characteristic. + See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_CAR_INCL_CONFIG_DEFAULT. */ } ble_gap_cfg_car_incl_cfg_t; /**@brief Configuration structure for GAP configurations. */ typedef union { - ble_gap_cfg_role_count_t role_count_cfg; /**< Role count configuration, cfg_id is @ref BLE_GAP_CFG_ROLE_COUNT. */ - ble_gap_cfg_device_name_t device_name_cfg; /**< Device name configuration, cfg_id is @ref BLE_GAP_CFG_DEVICE_NAME. */ - ble_gap_cfg_ppcp_incl_cfg_t ppcp_include_cfg; /**< Peripheral Preferred Connection Parameters characteristic include - configuration, cfg_id is @ref BLE_GAP_CFG_PPCP_INCL_CONFIG. */ - ble_gap_cfg_car_incl_cfg_t car_include_cfg; /**< Central Address Resolution characteristic include configuration, - cfg_id is @ref BLE_GAP_CFG_CAR_INCL_CONFIG. */ + ble_gap_cfg_role_count_t role_count_cfg; /**< Role count configuration, cfg_id is @ref BLE_GAP_CFG_ROLE_COUNT. */ + ble_gap_cfg_device_name_t device_name_cfg; /**< Device name configuration, cfg_id is @ref BLE_GAP_CFG_DEVICE_NAME. */ + ble_gap_cfg_ppcp_incl_cfg_t ppcp_include_cfg; /**< Peripheral Preferred Connection Parameters characteristic include + configuration, cfg_id is @ref BLE_GAP_CFG_PPCP_INCL_CONFIG. */ + ble_gap_cfg_car_incl_cfg_t car_include_cfg; /**< Central Address Resolution characteristic include configuration, + cfg_id is @ref BLE_GAP_CFG_CAR_INCL_CONFIG. */ } ble_gap_cfg_t; /**@brief Channel Map option. @@ -1557,8 +1532,8 @@ typedef union { * */ typedef struct { - uint16_t conn_handle; /**< Connection Handle (only applicable for get) */ - uint8_t ch_map[5]; /**< Channel Map (37-bit). */ + uint16_t conn_handle; /**< Connection Handle (only applicable for get) */ + uint8_t ch_map[5]; /**< Channel Map (37-bit). */ } ble_gap_opt_ch_map_t; /**@brief Local connection latency option. @@ -1584,10 +1559,10 @@ typedef struct { * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle */ - uint16_t requested_latency; /**< Requested local connection latency. */ - uint16_t *p_actual_latency; /**< Pointer to storage for the actual local connection latency (can be set to NULL to skip return - value). */ + uint16_t conn_handle; /**< Connection Handle */ + uint16_t requested_latency; /**< Requested local connection latency. */ + uint16_t *p_actual_latency; /**< Pointer to storage for the actual local connection latency (can be set to NULL to + skip return value). */ } ble_gap_opt_local_conn_latency_t; /**@brief Disable slave latency @@ -1603,8 +1578,8 @@ typedef struct { * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle */ - uint8_t disable; /**< For allowed values see @ref BLE_GAP_SLAVE_LATENCY */ + uint16_t conn_handle; /**< Connection Handle */ + uint8_t disable; /**< For allowed values see @ref BLE_GAP_SLAVE_LATENCY */ } ble_gap_opt_slave_latency_disable_t; /**@brief Passkey Option. @@ -1623,8 +1598,8 @@ typedef struct { * */ typedef struct { - uint8_t const *p_passkey; /**< Pointer to 6-digit ASCII string (digit 0..9 only, no NULL termination) passkey to be used - during pairing. If this is NULL, the SoftDevice will generate a random passkey if required.*/ + uint8_t const *p_passkey; /**< Pointer to 6-digit ASCII string (digit 0..9 only, no NULL termination) passkey to be used + during pairing. If this is NULL, the SoftDevice will generate a random passkey if required.*/ } ble_gap_opt_passkey_t; /**@brief Compatibility mode 1 option. @@ -1643,7 +1618,7 @@ typedef struct { * @retval ::NRF_ERROR_INVALID_STATE When connection creation is ongoing while mode 1 is set. */ typedef struct { - uint8_t enable : 1; /**< Enable compatibility mode 1.*/ + uint8_t enable : 1; /**< Enable compatibility mode 1.*/ } ble_gap_opt_compat_mode_1_t; /**@brief Authenticated payload timeout option. @@ -1666,32 +1641,32 @@ typedef struct { * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle */ - uint16_t auth_payload_timeout; /**< Requested timeout in 10 ms unit, see @ref BLE_GAP_AUTH_PAYLOAD_TIMEOUT. */ + uint16_t conn_handle; /**< Connection Handle */ + uint16_t auth_payload_timeout; /**< Requested timeout in 10 ms unit, see @ref BLE_GAP_AUTH_PAYLOAD_TIMEOUT. */ } ble_gap_opt_auth_payload_timeout_t; /**@brief Option structure for GAP options. */ typedef union { - ble_gap_opt_ch_map_t ch_map; /**< Parameters for the Channel Map option. */ - ble_gap_opt_local_conn_latency_t local_conn_latency; /**< Parameters for the Local connection latency option */ - ble_gap_opt_passkey_t passkey; /**< Parameters for the Passkey option.*/ - ble_gap_opt_compat_mode_1_t compat_mode_1; /**< Parameters for the compatibility mode 1 option.*/ - ble_gap_opt_auth_payload_timeout_t auth_payload_timeout; /**< Parameters for the authenticated payload timeout option.*/ - ble_gap_opt_slave_latency_disable_t slave_latency_disable; /**< Parameters for the Disable slave latency option */ + ble_gap_opt_ch_map_t ch_map; /**< Parameters for the Channel Map option. */ + ble_gap_opt_local_conn_latency_t local_conn_latency; /**< Parameters for the Local connection latency option */ + ble_gap_opt_passkey_t passkey; /**< Parameters for the Passkey option.*/ + ble_gap_opt_compat_mode_1_t compat_mode_1; /**< Parameters for the compatibility mode 1 option.*/ + ble_gap_opt_auth_payload_timeout_t auth_payload_timeout; /**< Parameters for the authenticated payload timeout option.*/ + ble_gap_opt_slave_latency_disable_t slave_latency_disable; /**< Parameters for the Disable slave latency option */ } ble_gap_opt_t; /**@brief Connection event triggering parameters. */ typedef struct { - uint8_t ppi_ch_id; /**< PPI channel to use. This channel should be regarded as reserved until - connection event PPI task triggering is stopped. - The PPI channel ID can not be one of the PPI channels reserved by - the SoftDevice. See @ref NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK. */ - uint32_t task_endpoint; /**< Task Endpoint to trigger. */ - uint16_t conn_evt_counter_start; /**< The connection event on which the task triggering should start. */ - uint16_t period_in_events; /**< Trigger period. Valid range is [1, 32767]. - If the device is in slave role and slave latency is enabled, - this parameter should be set to a multiple of (slave latency + 1) - to ensure low power operation. */ + uint8_t ppi_ch_id; /**< PPI channel to use. This channel should be regarded as reserved until + connection event PPI task triggering is stopped. + The PPI channel ID can not be one of the PPI channels reserved by + the SoftDevice. See @ref NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK. */ + uint32_t task_endpoint; /**< Task Endpoint to trigger. */ + uint16_t conn_evt_counter_start; /**< The connection event on which the task triggering should start. */ + uint16_t period_in_events; /**< Trigger period. Valid range is [1, 32767]. + If the device is in slave role and slave latency is enabled, + this parameter should be set to a multiple of (slave latency + 1) + to ensure low power operation. */ } ble_gap_conn_event_trigger_t; /**@} */ @@ -1731,7 +1706,8 @@ SVCALL(SD_BLE_GAP_ADDR_SET, uint32_t, sd_ble_gap_addr_set(ble_gap_addr_t const * /**@brief Get local Bluetooth identity address. * * @note This will always return the identity address irrespective of the privacy settings, - * i.e. the address type will always be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref BLE_GAP_ADDR_TYPE_RANDOM_STATIC. + * i.e. the address type will always be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref + * BLE_GAP_ADDR_TYPE_RANDOM_STATIC. * * @param[out] p_addr Pointer to address structure to be filled in. * @@ -1788,10 +1764,10 @@ SVCALL(SD_BLE_GAP_WHITELIST_SET, uint32_t, sd_ble_gap_whitelist_set(ble_gap_addr * @note Only one device identity list can be used at a time and the list is shared between the BLE roles. * The device identity list cannot be set if a BLE role is using the list. * - * @param[in] pp_id_keys Pointer to an array of peer identity addresses and peer IRKs, if NULL the device identity list will - * be cleared. - * @param[in] pp_local_irks Pointer to an array of local IRKs. Each entry in the array maps to the entry in pp_id_keys at the - * same index. To fill in the list with the currently set device IRK for all peers, set to NULL. + * @param[in] pp_id_keys Pointer to an array of peer identity addresses and peer IRKs, if NULL the device identity + * list will be cleared. + * @param[in] pp_local_irks Pointer to an array of local IRKs. Each entry in the array maps to the entry in pp_id_keys + * at the same index. To fill in the list with the currently set device IRK for all peers, set to NULL. * @param[in] len Length of the device identity list, maximum @ref BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT. * * @mscs @@ -1807,15 +1783,14 @@ SVCALL(SD_BLE_GAP_WHITELIST_SET, uint32_t, sd_ble_gap_whitelist_set(ble_gap_addr * @retval ::NRF_ERROR_INVALID_ADDR The device identity list (or one of its entries) provided is invalid. * This code may be returned if the local IRK list also has an invalid entry. * @retval ::BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE The device identity list is in use and cannot be set or cleared. - * @retval ::BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE The device identity list contains multiple entries with the same identity - * address. + * @retval ::BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE The device identity list contains multiple entries with the same + * identity address. * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid address type is supplied. * @retval ::NRF_ERROR_DATA_SIZE The given device identity list size invalid (zero or too large); this can * only return when pp_id_keys is not NULL. */ SVCALL(SD_BLE_GAP_DEVICE_IDENTITIES_SET, uint32_t, - sd_ble_gap_device_identities_set(ble_gap_id_key_t const *const *pp_id_keys, ble_gap_irk_t const *const *pp_local_irks, - uint8_t len)); + sd_ble_gap_device_identities_set(ble_gap_id_key_t const *const *pp_id_keys, ble_gap_irk_t const *const *pp_local_irks, uint8_t len)); /**@brief Set privacy settings. * @@ -1846,8 +1821,9 @@ SVCALL(SD_BLE_GAP_PRIVACY_SET, uint32_t, sd_ble_gap_privacy_set(ble_gap_privacy_ /**@brief Get privacy settings. * - * @note ::ble_gap_privacy_params_t::p_device_irk must be initialized to NULL or a valid address before this function is called. - * If it is initialized to a valid address, the address pointed to will contain the current device IRK on return. + * @note ::ble_gap_privacy_params_t::p_device_irk must be initialized to NULL or a valid address before this function is + * called. If it is initialized to a valid address, the address pointed to will contain the current device IRK on + * return. * * @param[in,out] p_privacy_params Privacy settings. * @@ -1860,8 +1836,8 @@ SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_ /**@brief Configure an advertising set. Set, clear or update advertising and scan response data. * * @note The format of the advertising data will be checked by this call to ensure interoperability. - * Limitations imposed by this API call to the data provided include having a flags data type in the scan response data and - * duplicating the local name in the advertising data and scan response data. + * Limitations imposed by this API call to the data provided include having a flags data type in the scan + * response data and duplicating the local name in the advertising data and scan response data. * * @note In order to update advertising data while advertising, new advertising buffers must be provided. * @@ -1871,26 +1847,28 @@ SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_ * @endmscs * * @param[in,out] p_adv_handle Provide a pointer to a handle containing @ref - * BLE_GAP_ADV_SET_HANDLE_NOT_SET to configure a new advertising set. On success, a new handle is then returned through the - * pointer. Provide a pointer to an existing advertising handle to configure an existing advertising set. - * @param[in] p_adv_data Advertising data. If set to NULL, no advertising data will be used. See + * BLE_GAP_ADV_SET_HANDLE_NOT_SET to configure a new advertising set. On success, a new handle is then returned through + * the pointer. Provide a pointer to an existing advertising handle to configure an existing advertising set. + * @param[in] p_adv_data Advertising data. If set to NULL, no advertising data will be + * used. See * @ref ble_gap_adv_data_t. - * @param[in] p_adv_params Advertising parameters. When this function is used to update advertising - * data while advertising, this parameter must be NULL. See @ref ble_gap_adv_params_t. + * @param[in] p_adv_params Advertising parameters. When this function is used to update + * advertising data while advertising, this parameter must be NULL. See @ref ble_gap_adv_params_t. * * @retval ::NRF_SUCCESS Advertising set successfully configured. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied: * - Invalid advertising data configuration specified. See @ref * ble_gap_adv_data_t. - * - Invalid configuration of p_adv_params. See @ref ble_gap_adv_params_t. + * - Invalid configuration of p_adv_params. See @ref + * ble_gap_adv_params_t. * - Use of whitelist requested but whitelist has not been set, * see @ref sd_ble_gap_whitelist_set. * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR ble_gap_adv_params_t::p_peer_addr is invalid. * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: - * - It is invalid to provide non-NULL advertising set parameters while - * advertising. - * - It is invalid to provide the same data buffers while advertising. To - * update advertising data, provide new advertising buffers. + * - It is invalid to provide non-NULL advertising set parameters + * while advertising. + * - It is invalid to provide the same data buffers while + * advertising. To update advertising data, provide new advertising buffers. * @retval ::BLE_ERROR_GAP_DISCOVERABLE_WITH_WHITELIST Discoverable mode and whitelist incompatible. * @retval ::BLE_ERROR_INVALID_ADV_HANDLE The provided advertising handle was not found. Use @ref * BLE_GAP_ADV_SET_HANDLE_NOT_SET to configure a new advertising handle. @@ -1900,13 +1878,12 @@ SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_ * specification given in Bluetooth Specification Version 5.0, Volume 3, Part C, Chapter 11. * @retval ::NRF_ERROR_INVALID_LENGTH Invalid data length(s) supplied. * @retval ::NRF_ERROR_NOT_SUPPORTED Unsupported data length or advertising parameter configuration. - * @retval ::NRF_ERROR_NO_MEM Not enough memory to configure a new advertising handle. Update an - * existing advertising handle instead. + * @retval ::NRF_ERROR_NO_MEM Not enough memory to configure a new advertising handle. Update + * an existing advertising handle instead. * @retval ::BLE_ERROR_GAP_UUID_LIST_MISMATCH Invalid UUID list supplied. */ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, - sd_ble_gap_adv_set_configure(uint8_t *p_adv_handle, ble_gap_adv_data_t const *p_adv_data, - ble_gap_adv_params_t const *p_adv_params)); + sd_ble_gap_adv_set_configure(uint8_t *p_adv_handle, ble_gap_adv_data_t const *p_adv_data, ble_gap_adv_params_t const *p_adv_params)); /**@brief Start advertising (GAP Discoverable, Connectable modes, Broadcast Procedure). * @@ -1930,7 +1907,8 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, * * @param[in] adv_handle Advertising handle to advertise on, received from @ref sd_ble_gap_adv_set_configure. * @param[in] conn_cfg_tag Tag identifying a configuration set by @ref sd_ble_cfg_set or - * @ref BLE_CONN_CFG_TAG_DEFAULT to use the default connection configuration. For non-connectable + * @ref BLE_CONN_CFG_TAG_DEFAULT to use the default connection configuration. For + non-connectable * advertising, this is ignored. * * @retval ::NRF_SUCCESS The BLE stack has started advertising. @@ -1938,7 +1916,8 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, * @retval ::NRF_ERROR_CONN_COUNT The limit of available connections for this connection configuration * tag has been reached; connectable advertiser cannot be started. * To increase the number of available connections, - * use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or @ref BLE_CONN_CFG_GAP. + * use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or @ref + BLE_CONN_CFG_GAP. * @retval ::BLE_ERROR_INVALID_ADV_HANDLE Advertising handle not found. Configure a new adveriting handle with @ref sd_ble_gap_adv_set_configure. * @retval ::NRF_ERROR_NOT_FOUND conn_cfg_tag not found. @@ -1947,16 +1926,21 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, * - Use of whitelist requested but whitelist has not been set, see @ref sd_ble_gap_whitelist_set. * @retval ::NRF_ERROR_RESOURCES Either: - * - adv_handle is configured with connectable advertising, but the event_length parameter - * associated with conn_cfg_tag is too small to be able to establish a connection on - * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event length. - * - Not enough BLE role slots available. - Stop one or more currently active roles (Central, Peripheral, Broadcaster or Observer) - and try again. - * - p_adv_params is configured with connectable advertising, but the event_length + * - adv_handle is configured with connectable advertising, but the event_length parameter - * associated with conn_cfg_tag is too small to be able to establish a connection on - * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event length. + * associated with conn_cfg_tag is too small to be able to establish a + connection on + * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event + length. + * - Not enough BLE role slots available. + Stop one or more currently active roles (Central, Peripheral, Broadcaster or + Observer) and try again. + * - p_adv_params is configured with connectable advertising, but the + event_length parameter + * associated with conn_cfg_tag is too small to be able to establish a + connection on + * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event + length. */ SVCALL(SD_BLE_GAP_ADV_START, uint32_t, sd_ble_gap_adv_start(uint8_t adv_handle, uint8_t conn_cfg_tag)); @@ -1982,8 +1966,8 @@ SVCALL(SD_BLE_GAP_ADV_STOP, uint32_t, sd_ble_gap_adv_stop(uint8_t adv_handle)); * the central to perform the procedure. In both cases, and regardless of success or failure, the application * will be informed of the result with a @ref BLE_GAP_EVT_CONN_PARAM_UPDATE event. * - * @details This function can be used as a central both to reply to a @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST or to start the - * procedure unrequested. + * @details This function can be used as a central both to reply to a @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST or to + * start the procedure unrequested. * * @events * @event{@ref BLE_GAP_EVT_CONN_PARAM_UPDATE, Result of the connection parameter update procedure.} @@ -2011,8 +1995,7 @@ SVCALL(SD_BLE_GAP_ADV_STOP, uint32_t, sd_ble_gap_adv_stop(uint8_t adv_handle)); * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. */ -SVCALL(SD_BLE_GAP_CONN_PARAM_UPDATE, uint32_t, - sd_ble_gap_conn_param_update(uint16_t conn_handle, ble_gap_conn_params_t const *p_conn_params)); +SVCALL(SD_BLE_GAP_CONN_PARAM_UPDATE, uint32_t, sd_ble_gap_conn_param_update(uint16_t conn_handle, ble_gap_conn_params_t const *p_conn_params)); /**@brief Disconnect (GAP Link Termination). * @@ -2044,8 +2027,8 @@ SVCALL(SD_BLE_GAP_DISCONNECT, uint32_t, sd_ble_gap_disconnect(uint16_t conn_hand * possible roles. * @param[in] handle The handle parameter is interpreted depending on role: * - If role is @ref BLE_GAP_TX_POWER_ROLE_CONN, this value is the specific connection handle. - * - If role is @ref BLE_GAP_TX_POWER_ROLE_ADV, the advertising set identified with the advertising handle, - * will use the specified transmit power, and include it in the advertising packet headers if + * - If role is @ref BLE_GAP_TX_POWER_ROLE_ADV, the advertising set identified with the advertising + * handle, will use the specified transmit power, and include it in the advertising packet headers if * @ref ble_gap_adv_properties_t::include_tx_power set. * - For all other roles handle is ignored. * @param[in] tx_power Radio transmit power in dBm (see note for accepted values). @@ -2112,8 +2095,8 @@ SVCALL(SD_BLE_GAP_PPCP_GET, uint32_t, sd_ble_gap_ppcp_get(ble_gap_conn_params_t * * @param[in] p_write_perm Write permissions for the Device Name characteristic, see @ref ble_gap_conn_sec_mode_t. * @param[in] p_dev_name Pointer to a UTF-8 encoded, non NULL-terminated string. - * @param[in] len Length of the UTF-8, non NULL-terminated string pointed to by p_dev_name in octets (must be smaller or - * equal than @ref BLE_GAP_DEVNAME_MAX_LEN). + * @param[in] len Length of the UTF-8, non NULL-terminated string pointed to by p_dev_name in octets (must be + * smaller or equal than @ref BLE_GAP_DEVNAME_MAX_LEN). * * @retval ::NRF_SUCCESS GAP device name and permissions set successfully. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2131,8 +2114,8 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_SET, uint32_t, * and not the number of bytes actually returned in p_dev_name. * The application may use this information to allocate a suitable buffer size. * - * @param[out] p_dev_name Pointer to an empty buffer where the UTF-8 non NULL-terminated string will be placed. Set to - * NULL to obtain the complete device name length. + * @param[out] p_dev_name Pointer to an empty buffer where the UTF-8 non NULL-terminated string will be + * placed. Set to NULL to obtain the complete device name length. * @param[in,out] p_len Length of the buffer pointed by p_dev_name, complete device name length on output. * * @retval ::NRF_SUCCESS GAP device name retrieved successfully. @@ -2176,9 +2159,9 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t * @endmscs * * @param[in] conn_handle Connection handle. - * @param[in] p_sec_params Pointer to the @ref ble_gap_sec_params_t structure with the security parameters to be used during the - * pairing or bonding procedure. In the peripheral role, only the bond, mitm, lesc and keypress fields of this structure are used. - * In the central role, this pointer may be NULL to reject a Security Request. + * @param[in] p_sec_params Pointer to the @ref ble_gap_sec_params_t structure with the security parameters to be used + * during the pairing or bonding procedure. In the peripheral role, only the bond, mitm, lesc and keypress fields of + * this structure are used. In the central role, this pointer may be NULL to reject a Security Request. * * @retval ::NRF_SUCCESS Successfully initiated authentication procedure. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2186,8 +2169,8 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: * - No link has been established. * - An encryption is already executing or queued. - * @retval ::NRF_ERROR_NO_MEM The maximum number of authentication procedures that can run in parallel for the given role is - * reached. + * @retval ::NRF_ERROR_NO_MEM The maximum number of authentication procedures that can run in parallel for the given + * role is reached. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::NRF_ERROR_NOT_SUPPORTED Setting of sign or link fields in @ref ble_gap_sec_kdist_t not supported. * Distribution of own Identity Information is only supported if the Central @@ -2196,15 +2179,14 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t * See @ref ble_gap_cfg_car_incl_cfg_t and @ref ble_gap_cfg_role_count_t. * @retval ::NRF_ERROR_TIMEOUT A SMP timeout has occurred, and further SMP operations on this link is prohibited. */ -SVCALL(SD_BLE_GAP_AUTHENTICATE, uint32_t, - sd_ble_gap_authenticate(uint16_t conn_handle, ble_gap_sec_params_t const *p_sec_params)); +SVCALL(SD_BLE_GAP_AUTHENTICATE, uint32_t, sd_ble_gap_authenticate(uint16_t conn_handle, ble_gap_sec_params_t const *p_sec_params)); /**@brief Reply with GAP security parameters. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST, calling it at other times will result in - * an @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected - * parameters. + * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST, calling it at other times will + * result in an @ref NRF_ERROR_INVALID_STATE. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with + * corrected parameters. * * @events * @event{This function is used during authentication procedures, see the list of events in the documentation of @ref @@ -2240,15 +2222,16 @@ SVCALL(SD_BLE_GAP_AUTHENTICATE, uint32_t, * * @param[in] conn_handle Connection handle. * @param[in] sec_status Security status, see @ref BLE_GAP_SEC_STATUS. - * @param[in] p_sec_params Pointer to a @ref ble_gap_sec_params_t security parameters structure. In the central role this must be - * set to NULL, as the parameters have already been provided during a previous call to @ref sd_ble_gap_authenticate. - * @param[in,out] p_sec_keyset Pointer to a @ref ble_gap_sec_keyset_t security keyset structure. Any keys generated and/or - * distributed as a result of the ongoing security procedure will be stored into the memory referenced by the pointers inside this - * structure. The keys will be stored and available to the application upon reception of a @ref BLE_GAP_EVT_AUTH_STATUS event. - * Note that the SoftDevice expects the application to provide memory for storing the - * peer's keys. So it must be ensured that the relevant pointers inside this structure are not NULL. The - * pointers to the local key can, however, be NULL, in which case, the local key data will not be available to the application - * upon reception of the + * @param[in] p_sec_params Pointer to a @ref ble_gap_sec_params_t security parameters structure. In the central role + * this must be set to NULL, as the parameters have already been provided during a previous call to @ref + * sd_ble_gap_authenticate. + * @param[in,out] p_sec_keyset Pointer to a @ref ble_gap_sec_keyset_t security keyset structure. Any keys generated + * and/or distributed as a result of the ongoing security procedure will be stored into the memory referenced by the + * pointers inside this structure. The keys will be stored and available to the application upon reception of a @ref + * BLE_GAP_EVT_AUTH_STATUS event. Note that the SoftDevice expects the application to provide memory for storing the + * peer's keys. So it must be ensured that the relevant pointers inside this structure are not + * NULL. The pointers to the local key can, however, be NULL, in which case, the local key data will not be available to + * the application upon reception of the * @ref BLE_GAP_EVT_AUTH_STATUS event. * * @retval ::NRF_SUCCESS Successfully accepted security parameter from the application. @@ -2269,10 +2252,10 @@ SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, /**@brief Reply with an authentication key. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_AUTH_KEY_REQUEST or a @ref BLE_GAP_EVT_PASSKEY_DISPLAY, - * calling it at other times will result in an @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected - * parameters. + * @details This function is only used to reply to a @ref BLE_GAP_EVT_AUTH_KEY_REQUEST or a @ref + * BLE_GAP_EVT_PASSKEY_DISPLAY, calling it at other times will result in an @ref NRF_ERROR_INVALID_STATE. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with + * corrected parameters. * * @events * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref @@ -2291,9 +2274,9 @@ SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, * @param[in] conn_handle Connection handle. * @param[in] key_type See @ref BLE_GAP_AUTH_KEY_TYPES. * @param[in] p_key If key type is @ref BLE_GAP_AUTH_KEY_TYPE_NONE, then NULL. - * If key type is @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY, then a 6-byte ASCII string (digit 0..9 only, no NULL - * termination) or NULL when confirming LE Secure Connections Numeric Comparison. If key type is @ref BLE_GAP_AUTH_KEY_TYPE_OOB, - * then a 16-byte OOB key value in little-endian format. + * If key type is @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY, then a 6-byte ASCII string (digit 0..9 only, no + * NULL termination) or NULL when confirming LE Secure Connections Numeric Comparison. If key type is @ref + * BLE_GAP_AUTH_KEY_TYPE_OOB, then a 16-byte OOB key value in little-endian format. * * @retval ::NRF_SUCCESS Authentication key successfully set. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2301,15 +2284,14 @@ SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, * @retval ::NRF_ERROR_INVALID_STATE Authentication key has not been requested. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ -SVCALL(SD_BLE_GAP_AUTH_KEY_REPLY, uint32_t, - sd_ble_gap_auth_key_reply(uint16_t conn_handle, uint8_t key_type, uint8_t const *p_key)); +SVCALL(SD_BLE_GAP_AUTH_KEY_REPLY, uint32_t, sd_ble_gap_auth_key_reply(uint16_t conn_handle, uint8_t key_type, uint8_t const *p_key)); /**@brief Reply with an LE Secure connections DHKey. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST, calling it at other times will result in - * an @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected - * parameters. + * @details This function is only used to reply to a @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST, calling it at other times will + * result in an @ref NRF_ERROR_INVALID_STATE. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with + * corrected parameters. * * @events * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref @@ -2340,8 +2322,7 @@ SVCALL(SD_BLE_GAP_AUTH_KEY_REPLY, uint32_t, * - The application has not pulled a @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST event. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ -SVCALL(SD_BLE_GAP_LESC_DHKEY_REPLY, uint32_t, - sd_ble_gap_lesc_dhkey_reply(uint16_t conn_handle, ble_gap_lesc_dhkey_t const *p_dhkey)); +SVCALL(SD_BLE_GAP_LESC_DHKEY_REPLY, uint32_t, sd_ble_gap_lesc_dhkey_reply(uint16_t conn_handle, ble_gap_lesc_dhkey_t const *p_dhkey)); /**@brief Notify the peer of a local keypress. * @@ -2366,16 +2347,17 @@ SVCALL(SD_BLE_GAP_KEYPRESS_NOTIFY, uint32_t, sd_ble_gap_keypress_notify(uint16_t /**@brief Generate a set of OOB data to send to a peer out of band. * - * @note The @ref ble_gap_addr_t included in the OOB data returned will be the currently active one (or, if a connection has - * already been established, the one used during connection setup). The application may manually overwrite it with an updated - * value. + * @note The @ref ble_gap_addr_t included in the OOB data returned will be the currently active one (or, if a + * connection has already been established, the one used during connection setup). The application may manually + * overwrite it with an updated value. * * @mscs * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_OOB_MSC} * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_OOB_MSC} * @endmscs * - * @param[in] conn_handle Connection handle. Can be @ref BLE_CONN_HANDLE_INVALID if a BLE connection has not been established yet. + * @param[in] conn_handle Connection handle. Can be @ref BLE_CONN_HANDLE_INVALID if a BLE connection has not been + * established yet. * @param[in] p_pk_own LE Secure Connections local P-256 Public Key. * @param[out] p_oobd_own The OOB data to be sent out of band to a peer. * @@ -2384,14 +2366,13 @@ SVCALL(SD_BLE_GAP_KEYPRESS_NOTIFY, uint32_t, sd_ble_gap_keypress_notify(uint16_t * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_GET, uint32_t, - sd_ble_gap_lesc_oob_data_get(uint16_t conn_handle, ble_gap_lesc_p256_pk_t const *p_pk_own, - ble_gap_lesc_oob_data_t *p_oobd_own)); + sd_ble_gap_lesc_oob_data_get(uint16_t conn_handle, ble_gap_lesc_p256_pk_t const *p_pk_own, ble_gap_lesc_oob_data_t *p_oobd_own)); /**@brief Provide the OOB data sent/received out of band. * * @note An authentication procedure with OOB selected as an algorithm must be in progress when calling this function. - * @note A @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST event with the oobd_req set to 1 must have been received prior to calling this - * function. + * @note A @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST event with the oobd_req set to 1 must have been received prior to + * calling this function. * * @events * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref @@ -2420,12 +2401,12 @@ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_GET, uint32_t, * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_SET, uint32_t, - sd_ble_gap_lesc_oob_data_set(uint16_t conn_handle, ble_gap_lesc_oob_data_t const *p_oobd_own, - ble_gap_lesc_oob_data_t const *p_oobd_peer)); + sd_ble_gap_lesc_oob_data_set(uint16_t conn_handle, ble_gap_lesc_oob_data_t const *p_oobd_own, ble_gap_lesc_oob_data_t const *p_oobd_peer)); /**@brief Initiate GAP Encryption procedure. * - * @details In the central role, this function will initiate the encryption procedure using the encryption information provided. + * @details In the central role, this function will initiate the encryption procedure using the encryption information + * provided. * * @events * @event{@ref BLE_GAP_EVT_CONN_SEC_UPDATE, The connection security has been updated.} @@ -2447,18 +2428,19 @@ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_SET, uint32_t, * @retval ::NRF_ERROR_INVALID_STATE No link has been established. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::BLE_ERROR_INVALID_ROLE Operation is not supported in the Peripheral role. - * @retval ::NRF_ERROR_BUSY Procedure already in progress or not allowed at this time, wait for pending procedures to complete and - * retry. + * @retval ::NRF_ERROR_BUSY Procedure already in progress or not allowed at this time, wait for pending procedures to + * complete and retry. */ SVCALL(SD_BLE_GAP_ENCRYPT, uint32_t, sd_ble_gap_encrypt(uint16_t conn_handle, ble_gap_master_id_t const *p_master_id, ble_gap_enc_info_t const *p_enc_info)); /**@brief Reply with GAP security information. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_INFO_REQUEST, calling it at other times will result in + * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_INFO_REQUEST, calling it at other times will + * result in * @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected - * parameters. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with + * corrected parameters. * @note Data signing is not yet supported, and p_sign_info must therefore be NULL. * * @mscs @@ -2466,11 +2448,12 @@ SVCALL(SD_BLE_GAP_ENCRYPT, uint32_t, * @endmscs * * @param[in] conn_handle Connection handle. - * @param[in] p_enc_info Pointer to a @ref ble_gap_enc_info_t encryption information structure. May be NULL to signal none is - * available. - * @param[in] p_id_info Pointer to a @ref ble_gap_irk_t identity information structure. May be NULL to signal none is available. - * @param[in] p_sign_info Pointer to a @ref ble_gap_sign_info_t signing information structure. May be NULL to signal none is + * @param[in] p_enc_info Pointer to a @ref ble_gap_enc_info_t encryption information structure. May be NULL to signal + * none is available. + * @param[in] p_id_info Pointer to a @ref ble_gap_irk_t identity information structure. May be NULL to signal none is * available. + * @param[in] p_sign_info Pointer to a @ref ble_gap_sign_info_t signing information structure. May be NULL to signal + * none is available. * * @retval ::NRF_SUCCESS Successfully accepted security information. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. @@ -2512,8 +2495,8 @@ SVCALL(SD_BLE_GAP_CONN_SEC_GET, uint32_t, sd_ble_gap_conn_sec_get(uint16_t conn_ * @endmscs * * @param[in] conn_handle Connection handle. - * @param[in] threshold_dbm Minimum change in dBm before triggering the @ref BLE_GAP_EVT_RSSI_CHANGED event. Events are - * disabled if threshold_dbm equals @ref BLE_GAP_RSSI_THRESHOLD_INVALID. + * @param[in] threshold_dbm Minimum change in dBm before triggering the @ref BLE_GAP_EVT_RSSI_CHANGED event. Events + * are disabled if threshold_dbm equals @ref BLE_GAP_RSSI_THRESHOLD_INVALID. * @param[in] skip_count Number of RSSI samples with a change of threshold_dbm or more before sending a new @ref * BLE_GAP_EVT_RSSI_CHANGED event. * @@ -2543,8 +2526,9 @@ SVCALL(SD_BLE_GAP_RSSI_STOP, uint32_t, sd_ble_gap_rssi_stop(uint16_t conn_handle /**@brief Get the received signal strength for the last connection event. * - * @ref sd_ble_gap_rssi_start must be called to start reporting RSSI before using this function. @ref NRF_ERROR_NOT_FOUND - * will be returned until RSSI was sampled for the first time after calling @ref sd_ble_gap_rssi_start. + * @ref sd_ble_gap_rssi_start must be called to start reporting RSSI before using this function. @ref + * NRF_ERROR_NOT_FOUND will be returned until RSSI was sampled for the first time after calling @ref + * sd_ble_gap_rssi_start. * @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature measurement. * @mscs * @mmsc{@ref BLE_GAP_CENTRAL_RSSI_READ_MSC} @@ -2572,14 +2556,16 @@ SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, * - @ref sd_ble_gap_scan_stop is called. * - @ref sd_ble_gap_connect is called. * - A @ref BLE_GAP_EVT_TIMEOUT with source set to @ref BLE_GAP_TIMEOUT_SRC_SCAN is received. - * - When a @ref BLE_GAP_EVT_ADV_REPORT event is received and @ref ble_gap_adv_report_type_t::status is not set to - * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. In this case scanning is only paused to let the application - * access received data. The application must call this function to continue scanning, or call @ref + * - When a @ref BLE_GAP_EVT_ADV_REPORT event is received and @ref ble_gap_adv_report_type_t::status is not + * set to + * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. In this case scanning is only paused to let the + * application access received data. The application must call this function to continue scanning, or call @ref * sd_ble_gap_scan_stop to stop scanning. * * @note If a @ref BLE_GAP_EVT_ADV_REPORT event is received with @ref ble_gap_adv_report_type_t::status set to - * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the scanner will continue scanning, and the application will - * receive more reports from this advertising event. The following reports will include the old and new received data. + * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the scanner will continue scanning, and the application + * will receive more reports from this advertising event. The following reports will include the old and new received + * data. * * @events * @event{@ref BLE_GAP_EVT_ADV_REPORT, An advertising or scan response packet has been received.} @@ -2597,8 +2583,8 @@ SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, * The memory pointed to should be kept alive until the scanning is stopped. * See @ref BLE_GAP_SCAN_BUFFER_SIZE for minimum and maximum buffer size. * If the scanner receives advertising data larger than can be stored in the buffer, - * a @ref BLE_GAP_EVT_ADV_REPORT will be raised with @ref ble_gap_adv_report_type_t::status - * set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED. + * a @ref BLE_GAP_EVT_ADV_REPORT will be raised with @ref + * ble_gap_adv_report_type_t::status set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED. * * @retval ::NRF_SUCCESS Successfully initiated scanning procedure. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2610,10 +2596,10 @@ SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, * @retval ::NRF_ERROR_NOT_SUPPORTED Unsupported parameters supplied. See @ref ble_gap_scan_params_t. * @retval ::NRF_ERROR_INVALID_LENGTH The provided buffer length is invalid. See @ref BLE_GAP_SCAN_BUFFER_MIN. * @retval ::NRF_ERROR_RESOURCES Not enough BLE role slots available. - * Stop one or more currently active roles (Central, Peripheral or Broadcaster) and try again + * Stop one or more currently active roles (Central, Peripheral or Broadcaster) and try + * again */ -SVCALL(SD_BLE_GAP_SCAN_START, uint32_t, - sd_ble_gap_scan_start(ble_gap_scan_params_t const *p_scan_params, ble_data_t const *p_adv_report_buffer)); +SVCALL(SD_BLE_GAP_SCAN_START, uint32_t, sd_ble_gap_scan_start(ble_gap_scan_params_t const *p_scan_params, ble_data_t const *p_adv_report_buffer)); /**@brief Stop scanning (GAP Discovery procedure, Observer Procedure). * @@ -2661,22 +2647,23 @@ SVCALL(SD_BLE_GAP_SCAN_STOP, uint32_t, sd_ble_gap_scan_stop(void)); * - Peer address was not present in the device identity list, see @ref * sd_ble_gap_device_identities_set. * @retval ::NRF_ERROR_NOT_FOUND conn_cfg_tag not found. - * @retval ::NRF_ERROR_INVALID_STATE The SoftDevice is in an invalid state to perform this operation. This may be due to an - * existing locally initiated connect procedure, which must complete before initiating again. + * @retval ::NRF_ERROR_INVALID_STATE The SoftDevice is in an invalid state to perform this operation. This may be due to + * an existing locally initiated connect procedure, which must complete before initiating again. * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid Peer address. - * @retval ::NRF_ERROR_CONN_COUNT The limit of available connections for this connection configuration tag has been reached. - * To increase the number of available connections, - * use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or @ref BLE_CONN_CFG_GAP. + * @retval ::NRF_ERROR_CONN_COUNT The limit of available connections for this connection configuration tag has been + * reached. To increase the number of available connections, use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or + * @ref BLE_CONN_CFG_GAP. * @retval ::NRF_ERROR_RESOURCES Either: * - Not enough BLE role slots available. - * Stop one or more currently active roles (Central, Peripheral or Observer) and try again. + * Stop one or more currently active roles (Central, Peripheral or Observer) and try + * again. * - The event_length parameter associated with conn_cfg_tag is too small to be able to * establish a connection on the selected @ref ble_gap_scan_params_t::scan_phys. * Use @ref sd_ble_cfg_set to increase the event length. */ SVCALL(SD_BLE_GAP_CONNECT, uint32_t, - sd_ble_gap_connect(ble_gap_addr_t const *p_peer_addr, ble_gap_scan_params_t const *p_scan_params, - ble_gap_conn_params_t const *p_conn_params, uint8_t conn_cfg_tag)); + sd_ble_gap_connect(ble_gap_addr_t const *p_peer_addr, ble_gap_scan_params_t const *p_scan_params, ble_gap_conn_params_t const *p_conn_params, + uint8_t conn_cfg_tag)); /**@brief Cancel a connection establishment. * @@ -2737,11 +2724,13 @@ SVCALL(SD_BLE_GAP_CONNECT_CANCEL, uint32_t, sd_ble_gap_connect_cancel(void)); * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. * @retval ::NRF_ERROR_INVALID_STATE No link has been established. - * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the combination of + * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the + * combination of * @ref ble_gap_phys_t::tx_phys, @ref ble_gap_phys_t::rx_phys, and @ref - * ble_gap_data_length_params_t. The connection event length is configured with @ref BLE_CONN_CFG_GAP using @ref sd_ble_cfg_set. - * @retval ::NRF_ERROR_BUSY Procedure is already in progress or not allowed at this time. Process pending events and wait for the - * pending procedure to complete and retry. + * ble_gap_data_length_params_t. The connection event length is configured with @ref BLE_CONN_CFG_GAP using @ref + * sd_ble_cfg_set. + * @retval ::NRF_ERROR_BUSY Procedure is already in progress or not allowed at this time. Process pending events and + * wait for the pending procedure to complete and retry. * */ SVCALL(SD_BLE_GAP_PHY_UPDATE, uint32_t, sd_ble_gap_phy_update(uint16_t conn_handle, ble_gap_phys_t const *p_gap_phys)); @@ -2775,9 +2764,9 @@ SVCALL(SD_BLE_GAP_PHY_UPDATE, uint32_t, sd_ble_gap_phy_update(uint16_t conn_hand * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameters supplied. * @retval ::NRF_ERROR_NOT_SUPPORTED The requested parameters are not supported by the SoftDevice. Inspect * p_dl_limitation to see which parameter is not supported. - * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the requested - * parameters. Use @ref sd_ble_cfg_set with @ref BLE_CONN_CFG_GAP to increase the connection event length. Inspect p_dl_limitation - * to see where the limitation is. + * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the + * requested parameters. Use @ref sd_ble_cfg_set with @ref BLE_CONN_CFG_GAP to increase the connection event length. + * Inspect p_dl_limitation to see where the limitation is. * @retval ::NRF_ERROR_BUSY Peer has already initiated a Data Length Update Procedure. Process the * pending @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST event to respond. */ @@ -2848,8 +2837,7 @@ SVCALL(SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP, uint32_t, sd_ble_gap_qos_channel_surv * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter supplied. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. */ -SVCALL(SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET, uint32_t, - sd_ble_gap_next_conn_evt_counter_get(uint16_t conn_handle, uint16_t *p_counter)); +SVCALL(SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET, uint32_t, sd_ble_gap_next_conn_evt_counter_get(uint16_t conn_handle, uint16_t *p_counter)); /**@brief Start triggering a given task on connection event start. * diff --git a/src/platform/nrf52/softdevice/ble_gatt.h b/src/platform/nrf52/softdevice/ble_gatt.h index df0d728fc..31f6a026b 100644 --- a/src/platform/nrf52/softdevice/ble_gatt.h +++ b/src/platform/nrf52/softdevice/ble_gatt.h @@ -112,34 +112,34 @@ extern "C" { #define BLE_GATT_STATUS_ATTERR_INSUF_AUTHENTICATION 0x0105 /**< ATT Error: Authenticated link required. */ #define BLE_GATT_STATUS_ATTERR_REQUEST_NOT_SUPPORTED 0x0106 /**< ATT Error: Used in ATT as Request Not Supported. */ #define BLE_GATT_STATUS_ATTERR_INVALID_OFFSET 0x0107 /**< ATT Error: Offset specified was past the end of the attribute. */ -#define BLE_GATT_STATUS_ATTERR_INSUF_AUTHORIZATION 0x0108 /**< ATT Error: Used in ATT as Insufficient Authorization. */ -#define BLE_GATT_STATUS_ATTERR_PREPARE_QUEUE_FULL 0x0109 /**< ATT Error: Used in ATT as Prepare Queue Full. */ -#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_FOUND 0x010A /**< ATT Error: Used in ATT as Attribute not found. */ -#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_LONG \ - 0x010B /**< ATT Error: Attribute cannot be read or written using read/write blob requests. */ +#define BLE_GATT_STATUS_ATTERR_INSUF_AUTHORIZATION \ + 0x0108 /**< ATT Error: Used in ATT as Insufficient Authorization. \ + */ +#define BLE_GATT_STATUS_ATTERR_PREPARE_QUEUE_FULL 0x0109 /**< ATT Error: Used in ATT as Prepare Queue Full. */ +#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_FOUND 0x010A /**< ATT Error: Used in ATT as Attribute not found. */ +#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_LONG 0x010B /**< ATT Error: Attribute cannot be read or written using read/write blob requests. */ #define BLE_GATT_STATUS_ATTERR_INSUF_ENC_KEY_SIZE 0x010C /**< ATT Error: Encryption key size used is insufficient. */ #define BLE_GATT_STATUS_ATTERR_INVALID_ATT_VAL_LENGTH 0x010D /**< ATT Error: Invalid value size. */ #define BLE_GATT_STATUS_ATTERR_UNLIKELY_ERROR 0x010E /**< ATT Error: Very unlikely error. */ #define BLE_GATT_STATUS_ATTERR_INSUF_ENCRYPTION 0x010F /**< ATT Error: Encrypted link required. */ -#define BLE_GATT_STATUS_ATTERR_UNSUPPORTED_GROUP_TYPE \ - 0x0110 /**< ATT Error: Attribute type is not a supported grouping attribute. */ -#define BLE_GATT_STATUS_ATTERR_INSUF_RESOURCES 0x0111 /**< ATT Error: Insufficient resources. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_BEGIN 0x0112 /**< ATT Error: Reserved for Future Use range #1 begin. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_END 0x017F /**< ATT Error: Reserved for Future Use range #1 end. */ -#define BLE_GATT_STATUS_ATTERR_APP_BEGIN 0x0180 /**< ATT Error: Application range begin. */ -#define BLE_GATT_STATUS_ATTERR_APP_END 0x019F /**< ATT Error: Application range end. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_BEGIN 0x01A0 /**< ATT Error: Reserved for Future Use range #2 begin. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_END 0x01DF /**< ATT Error: Reserved for Future Use range #2 end. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_BEGIN 0x01E0 /**< ATT Error: Reserved for Future Use range #3 begin. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_END 0x01FC /**< ATT Error: Reserved for Future Use range #3 end. */ -#define BLE_GATT_STATUS_ATTERR_CPS_WRITE_REQ_REJECTED \ - 0x01FC /**< ATT Common Profile and Service Error: Write request rejected. \ - */ -#define BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR \ - 0x01FD /**< ATT Common Profile and Service Error: Client Characteristic Configuration Descriptor improperly configured. */ -#define BLE_GATT_STATUS_ATTERR_CPS_PROC_ALR_IN_PROG \ - 0x01FE /**< ATT Common Profile and Service Error: Procedure Already in Progress. */ -#define BLE_GATT_STATUS_ATTERR_CPS_OUT_OF_RANGE 0x01FF /**< ATT Common Profile and Service Error: Out Of Range. */ +#define BLE_GATT_STATUS_ATTERR_UNSUPPORTED_GROUP_TYPE 0x0110 /**< ATT Error: Attribute type is not a supported grouping attribute. */ +#define BLE_GATT_STATUS_ATTERR_INSUF_RESOURCES 0x0111 /**< ATT Error: Insufficient resources. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_BEGIN 0x0112 /**< ATT Error: Reserved for Future Use range #1 begin. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_END 0x017F /**< ATT Error: Reserved for Future Use range #1 end. */ +#define BLE_GATT_STATUS_ATTERR_APP_BEGIN 0x0180 /**< ATT Error: Application range begin. */ +#define BLE_GATT_STATUS_ATTERR_APP_END 0x019F /**< ATT Error: Application range end. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_BEGIN 0x01A0 /**< ATT Error: Reserved for Future Use range #2 begin. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_END 0x01DF /**< ATT Error: Reserved for Future Use range #2 end. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_BEGIN 0x01E0 /**< ATT Error: Reserved for Future Use range #3 begin. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_END 0x01FC /**< ATT Error: Reserved for Future Use range #3 end. */ +#define BLE_GATT_STATUS_ATTERR_CPS_WRITE_REQ_REJECTED \ + 0x01FC /**< ATT Common Profile and Service Error: Write request rejected. \ + */ +#define BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR \ + 0x01FD /**< ATT Common Profile and Service Error: Client Characteristic Configuration Descriptor improperly \ + configured. */ +#define BLE_GATT_STATUS_ATTERR_CPS_PROC_ALR_IN_PROG 0x01FE /**< ATT Common Profile and Service Error: Procedure Already in Progress. */ +#define BLE_GATT_STATUS_ATTERR_CPS_OUT_OF_RANGE 0x01FF /**< ATT Common Profile and Service Error: Out Of Range. */ /** @} */ /** @defgroup BLE_GATT_CPF_FORMATS Characteristic Presentation Formats @@ -194,32 +194,32 @@ extern "C" { * @retval ::NRF_ERROR_INVALID_PARAM att_mtu is smaller than @ref BLE_GATT_ATT_MTU_DEFAULT. */ typedef struct { - uint16_t att_mtu; /**< Maximum size of ATT packet the SoftDevice can send or receive. - The default and minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. - @mscs - @mmsc{@ref BLE_GATTC_MTU_EXCHANGE} - @mmsc{@ref BLE_GATTS_MTU_EXCHANGE} - @endmscs - */ + uint16_t att_mtu; /**< Maximum size of ATT packet the SoftDevice can send or receive. + The default and minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. + @mscs + @mmsc{@ref BLE_GATTC_MTU_EXCHANGE} + @mmsc{@ref BLE_GATTS_MTU_EXCHANGE} + @endmscs + */ } ble_gatt_conn_cfg_t; /**@brief GATT Characteristic Properties. */ typedef struct { - /* Standard properties */ - uint8_t broadcast : 1; /**< Broadcasting of the value permitted. */ - uint8_t read : 1; /**< Reading the value permitted. */ - uint8_t write_wo_resp : 1; /**< Writing the value with Write Command permitted. */ - uint8_t write : 1; /**< Writing the value with Write Request permitted. */ - uint8_t notify : 1; /**< Notification of the value permitted. */ - uint8_t indicate : 1; /**< Indications of the value permitted. */ - uint8_t auth_signed_wr : 1; /**< Writing the value with Signed Write Command permitted. */ + /* Standard properties */ + uint8_t broadcast : 1; /**< Broadcasting of the value permitted. */ + uint8_t read : 1; /**< Reading the value permitted. */ + uint8_t write_wo_resp : 1; /**< Writing the value with Write Command permitted. */ + uint8_t write : 1; /**< Writing the value with Write Request permitted. */ + uint8_t notify : 1; /**< Notification of the value permitted. */ + uint8_t indicate : 1; /**< Indications of the value permitted. */ + uint8_t auth_signed_wr : 1; /**< Writing the value with Signed Write Command permitted. */ } ble_gatt_char_props_t; /**@brief GATT Characteristic Extended Properties. */ typedef struct { - /* Extended properties */ - uint8_t reliable_wr : 1; /**< Writing the value with Queued Write operations permitted. */ - uint8_t wr_aux : 1; /**< Writing the Characteristic User Description descriptor permitted. */ + /* Extended properties */ + uint8_t reliable_wr : 1; /**< Writing the value with Queued Write operations permitted. */ + uint8_t wr_aux : 1; /**< Writing the Characteristic User Description descriptor permitted. */ } ble_gatt_char_ext_props_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/ble_gattc.h b/src/platform/nrf52/softdevice/ble_gattc.h index f1df1782c..def6f481e 100644 --- a/src/platform/nrf52/softdevice/ble_gattc.h +++ b/src/platform/nrf52/softdevice/ble_gattc.h @@ -63,53 +63,56 @@ extern "C" { /**@brief GATTC API SVC numbers. */ enum BLE_GATTC_SVCS { - SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER = BLE_GATTC_SVC_BASE, /**< Primary Service Discovery. */ - SD_BLE_GATTC_RELATIONSHIPS_DISCOVER, /**< Relationship Discovery. */ - SD_BLE_GATTC_CHARACTERISTICS_DISCOVER, /**< Characteristic Discovery. */ - SD_BLE_GATTC_DESCRIPTORS_DISCOVER, /**< Characteristic Descriptor Discovery. */ - SD_BLE_GATTC_ATTR_INFO_DISCOVER, /**< Attribute Information Discovery. */ - SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ, /**< Read Characteristic Value by UUID. */ - SD_BLE_GATTC_READ, /**< Generic read. */ - SD_BLE_GATTC_CHAR_VALUES_READ, /**< Read multiple Characteristic Values. */ - SD_BLE_GATTC_WRITE, /**< Generic write. */ - SD_BLE_GATTC_HV_CONFIRM, /**< Handle Value Confirmation. */ - SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. */ + SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER = BLE_GATTC_SVC_BASE, /**< Primary Service Discovery. */ + SD_BLE_GATTC_RELATIONSHIPS_DISCOVER, /**< Relationship Discovery. */ + SD_BLE_GATTC_CHARACTERISTICS_DISCOVER, /**< Characteristic Discovery. */ + SD_BLE_GATTC_DESCRIPTORS_DISCOVER, /**< Characteristic Descriptor Discovery. */ + SD_BLE_GATTC_ATTR_INFO_DISCOVER, /**< Attribute Information Discovery. */ + SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ, /**< Read Characteristic Value by UUID. */ + SD_BLE_GATTC_READ, /**< Generic read. */ + SD_BLE_GATTC_CHAR_VALUES_READ, /**< Read multiple Characteristic Values. */ + SD_BLE_GATTC_WRITE, /**< Generic write. */ + SD_BLE_GATTC_HV_CONFIRM, /**< Handle Value Confirmation. */ + SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. */ }; /** * @brief GATT Client Event IDs. */ enum BLE_GATTC_EVTS { - BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP = BLE_GATTC_EVT_BASE, /**< Primary Service Discovery Response event. \n See @ref - ble_gattc_evt_prim_srvc_disc_rsp_t. */ - BLE_GATTC_EVT_REL_DISC_RSP, /**< Relationship Discovery Response event. \n See @ref ble_gattc_evt_rel_disc_rsp_t. - */ - BLE_GATTC_EVT_CHAR_DISC_RSP, /**< Characteristic Discovery Response event. \n See @ref - ble_gattc_evt_char_disc_rsp_t. */ - BLE_GATTC_EVT_DESC_DISC_RSP, /**< Descriptor Discovery Response event. \n See @ref - ble_gattc_evt_desc_disc_rsp_t. */ - BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, /**< Attribute Information Response event. \n See @ref - ble_gattc_evt_attr_info_disc_rsp_t. */ - BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP, /**< Read By UUID Response event. \n See @ref - ble_gattc_evt_char_val_by_uuid_read_rsp_t. */ - BLE_GATTC_EVT_READ_RSP, /**< Read Response event. \n See @ref ble_gattc_evt_read_rsp_t. */ - BLE_GATTC_EVT_CHAR_VALS_READ_RSP, /**< Read multiple Response event. \n See @ref - ble_gattc_evt_char_vals_read_rsp_t. */ - BLE_GATTC_EVT_WRITE_RSP, /**< Write Response event. \n See @ref ble_gattc_evt_write_rsp_t. */ - BLE_GATTC_EVT_HVX, /**< Handle Value Notification or Indication event. \n Confirm indication with @ref - sd_ble_gattc_hv_confirm. \n See @ref ble_gattc_evt_hvx_t. */ - BLE_GATTC_EVT_EXCHANGE_MTU_RSP, /**< Exchange MTU Response event. \n See @ref - ble_gattc_evt_exchange_mtu_rsp_t. */ - BLE_GATTC_EVT_TIMEOUT, /**< Timeout event. \n See @ref ble_gattc_evt_timeout_t. */ - BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE /**< Write without Response transmission complete. \n See @ref - ble_gattc_evt_write_cmd_tx_complete_t. */ + BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP = BLE_GATTC_EVT_BASE, /**< Primary Service Discovery Response event. \n See + @ref ble_gattc_evt_prim_srvc_disc_rsp_t. */ + BLE_GATTC_EVT_REL_DISC_RSP, /**< Relationship Discovery Response event. \n See @ref + * ble_gattc_evt_rel_disc_rsp_t. + */ + BLE_GATTC_EVT_CHAR_DISC_RSP, /**< Characteristic Discovery Response event. \n See @ref + ble_gattc_evt_char_disc_rsp_t. */ + BLE_GATTC_EVT_DESC_DISC_RSP, /**< Descriptor Discovery Response event. \n See @ref + ble_gattc_evt_desc_disc_rsp_t. */ + BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, /**< Attribute Information Response event. \n See @ref + ble_gattc_evt_attr_info_disc_rsp_t. */ + BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP, /**< Read By UUID Response event. \n See @ref + ble_gattc_evt_char_val_by_uuid_read_rsp_t. */ + BLE_GATTC_EVT_READ_RSP, /**< Read Response event. \n See @ref ble_gattc_evt_read_rsp_t. + */ + BLE_GATTC_EVT_CHAR_VALS_READ_RSP, /**< Read multiple Response event. \n See @ref + ble_gattc_evt_char_vals_read_rsp_t. */ + BLE_GATTC_EVT_WRITE_RSP, /**< Write Response event. \n See @ref + ble_gattc_evt_write_rsp_t. */ + BLE_GATTC_EVT_HVX, /**< Handle Value Notification or Indication event. \n Confirm indication with @ref + sd_ble_gattc_hv_confirm. \n See @ref ble_gattc_evt_hvx_t. */ + BLE_GATTC_EVT_EXCHANGE_MTU_RSP, /**< Exchange MTU Response event. \n See @ref + ble_gattc_evt_exchange_mtu_rsp_t. */ + BLE_GATTC_EVT_TIMEOUT, /**< Timeout event. \n See @ref ble_gattc_evt_timeout_t. */ + BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE /**< Write without Response transmission complete. \n See @ref + ble_gattc_evt_write_cmd_tx_complete_t. */ }; /**@brief GATTC Option IDs. * IDs that uniquely identify a GATTC option. */ enum BLE_GATTC_OPTS { - BLE_GATTC_OPT_UUID_DISC = BLE_GATTC_OPT_BASE, /**< UUID discovery. @ref ble_gattc_opt_uuid_disc_t */ + BLE_GATTC_OPT_UUID_DISC = BLE_GATTC_OPT_BASE, /**< UUID discovery. @ref ble_gattc_opt_uuid_disc_t */ }; /** @} */ @@ -130,8 +133,7 @@ enum BLE_GATTC_OPTS { /** @defgroup BLE_GATTC_DEFAULTS GATT Client defaults * @{ */ -#define BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT \ - 1 /**< Default number of Write without Response that can be queued for transmission. */ +#define BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT 1 /**< Default number of Write without Response that can be queued for transmission. */ /** @} */ /** @} */ @@ -143,208 +145,206 @@ enum BLE_GATTC_OPTS { * @brief BLE GATTC connection configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t write_cmd_tx_queue_size; /**< The guaranteed minimum number of Write without Response that can be queued for - transmission. The default value is @ref BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT */ + uint8_t write_cmd_tx_queue_size; /**< The guaranteed minimum number of Write without Response that can be queued for + transmission. The default value is @ref BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT */ } ble_gattc_conn_cfg_t; /**@brief Operation Handle Range. */ typedef struct { - uint16_t start_handle; /**< Start Handle. */ - uint16_t end_handle; /**< End Handle. */ + uint16_t start_handle; /**< Start Handle. */ + uint16_t end_handle; /**< End Handle. */ } ble_gattc_handle_range_t; /**@brief GATT service. */ typedef struct { - ble_uuid_t uuid; /**< Service UUID. */ - ble_gattc_handle_range_t handle_range; /**< Service Handle Range. */ + ble_uuid_t uuid; /**< Service UUID. */ + ble_gattc_handle_range_t handle_range; /**< Service Handle Range. */ } ble_gattc_service_t; /**@brief GATT include. */ typedef struct { - uint16_t handle; /**< Include Handle. */ - ble_gattc_service_t included_srvc; /**< Handle of the included service. */ + uint16_t handle; /**< Include Handle. */ + ble_gattc_service_t included_srvc; /**< Handle of the included service. */ } ble_gattc_include_t; /**@brief GATT characteristic. */ typedef struct { - ble_uuid_t uuid; /**< Characteristic UUID. */ - ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ - uint8_t char_ext_props : 1; /**< Extended properties present. */ - uint16_t handle_decl; /**< Handle of the Characteristic Declaration. */ - uint16_t handle_value; /**< Handle of the Characteristic Value. */ + ble_uuid_t uuid; /**< Characteristic UUID. */ + ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ + uint8_t char_ext_props : 1; /**< Extended properties present. */ + uint16_t handle_decl; /**< Handle of the Characteristic Declaration. */ + uint16_t handle_value; /**< Handle of the Characteristic Value. */ } ble_gattc_char_t; /**@brief GATT descriptor. */ typedef struct { - uint16_t handle; /**< Descriptor Handle. */ - ble_uuid_t uuid; /**< Descriptor UUID. */ + uint16_t handle; /**< Descriptor Handle. */ + ble_uuid_t uuid; /**< Descriptor UUID. */ } ble_gattc_desc_t; /**@brief Write Parameters. */ typedef struct { - uint8_t write_op; /**< Write Operation to be performed, see @ref BLE_GATT_WRITE_OPS. */ - uint8_t flags; /**< Flags, see @ref BLE_GATT_EXEC_WRITE_FLAGS. */ - uint16_t handle; /**< Handle to the attribute to be written. */ - uint16_t offset; /**< Offset in bytes. @note For WRITE_CMD and WRITE_REQ, offset must be 0. */ - uint16_t len; /**< Length of data in bytes. */ - uint8_t const *p_value; /**< Pointer to the value data. */ + uint8_t write_op; /**< Write Operation to be performed, see @ref BLE_GATT_WRITE_OPS. */ + uint8_t flags; /**< Flags, see @ref BLE_GATT_EXEC_WRITE_FLAGS. */ + uint16_t handle; /**< Handle to the attribute to be written. */ + uint16_t offset; /**< Offset in bytes. @note For WRITE_CMD and WRITE_REQ, offset must be 0. */ + uint16_t len; /**< Length of data in bytes. */ + uint8_t const *p_value; /**< Pointer to the value data. */ } ble_gattc_write_params_t; /**@brief Attribute Information for 16-bit Attribute UUID. */ typedef struct { - uint16_t handle; /**< Attribute handle. */ - ble_uuid_t uuid; /**< 16-bit Attribute UUID. */ + uint16_t handle; /**< Attribute handle. */ + ble_uuid_t uuid; /**< 16-bit Attribute UUID. */ } ble_gattc_attr_info16_t; /**@brief Attribute Information for 128-bit Attribute UUID. */ typedef struct { - uint16_t handle; /**< Attribute handle. */ - ble_uuid128_t uuid; /**< 128-bit Attribute UUID. */ + uint16_t handle; /**< Attribute handle. */ + ble_uuid128_t uuid; /**< 128-bit Attribute UUID. */ } ble_gattc_attr_info128_t; /**@brief Event structure for @ref BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP. */ typedef struct { - uint16_t count; /**< Service count. */ - ble_gattc_service_t services[1]; /**< Service data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use - event structures with variable length array members. */ + uint16_t count; /**< Service count. */ + ble_gattc_service_t services[1]; /**< Service data. @note This is a variable length array. The size of 1 indicated is + only a placeholder for compilation. See @ref sd_ble_evt_get for more information + on how to use event structures with variable length array members. */ } ble_gattc_evt_prim_srvc_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_REL_DISC_RSP. */ typedef struct { - uint16_t count; /**< Include count. */ - ble_gattc_include_t includes[1]; /**< Include data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use - event structures with variable length array members. */ + uint16_t count; /**< Include count. */ + ble_gattc_include_t includes[1]; /**< Include data. @note This is a variable length array. The size of 1 indicated is + only a placeholder for compilation. See @ref sd_ble_evt_get for more information + on how to use event structures with variable length array members. */ } ble_gattc_evt_rel_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_CHAR_DISC_RSP. */ typedef struct { - uint16_t count; /**< Characteristic count. */ - ble_gattc_char_t chars[1]; /**< Characteristic data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event - structures with variable length array members. */ + uint16_t count; /**< Characteristic count. */ + ble_gattc_char_t chars[1]; /**< Characteristic data. @note This is a variable length array. The size of 1 indicated is + only a placeholder for compilation. See @ref sd_ble_evt_get for more information on how + to use event structures with variable length array members. */ } ble_gattc_evt_char_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_DESC_DISC_RSP. */ typedef struct { - uint16_t count; /**< Descriptor count. */ - ble_gattc_desc_t descs[1]; /**< Descriptor data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event - structures with variable length array members. */ + uint16_t count; /**< Descriptor count. */ + ble_gattc_desc_t descs[1]; /**< Descriptor data. @note This is a variable length array. The size of 1 indicated is + only a placeholder for compilation. See @ref sd_ble_evt_get for more information on how + to use event structures with variable length array members. */ } ble_gattc_evt_desc_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_ATTR_INFO_DISC_RSP. */ typedef struct { - uint16_t count; /**< Attribute count. */ - uint8_t format; /**< Attribute information format, see @ref BLE_GATTC_ATTR_INFO_FORMAT. */ - union { - ble_gattc_attr_info16_t attr_info16[1]; /**< Attribute information for 16-bit Attribute UUID. - @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on - how to use event structures with variable length array members. */ - ble_gattc_attr_info128_t attr_info128[1]; /**< Attribute information for 128-bit Attribute UUID. - @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on - how to use event structures with variable length array members. */ - } info; /**< Attribute information union. */ + uint16_t count; /**< Attribute count. */ + uint8_t format; /**< Attribute information format, see @ref BLE_GATTC_ATTR_INFO_FORMAT. */ + union { + ble_gattc_attr_info16_t attr_info16[1]; /**< Attribute information for 16-bit Attribute UUID. + @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on + how to use event structures with variable length array members. */ + ble_gattc_attr_info128_t attr_info128[1]; /**< Attribute information for 128-bit Attribute UUID. + @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on + how to use event structures with variable length array members. */ + } info; /**< Attribute information union. */ } ble_gattc_evt_attr_info_disc_rsp_t; /**@brief GATT read by UUID handle value pair. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - uint8_t *p_value; /**< Pointer to the Attribute Value, length is available in @ref - ble_gattc_evt_char_val_by_uuid_read_rsp_t::value_len. */ + uint16_t handle; /**< Attribute Handle. */ + uint8_t *p_value; /**< Pointer to the Attribute Value, length is available in @ref + ble_gattc_evt_char_val_by_uuid_read_rsp_t::value_len. */ } ble_gattc_handle_value_t; /**@brief Event structure for @ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP. */ typedef struct { - uint16_t count; /**< Handle-Value Pair Count. */ - uint16_t value_len; /**< Length of the value in Handle-Value(s) list. */ - uint8_t handle_value[1]; /**< Handle-Value(s) list. To iterate through the list use @ref - sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter. - @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures with - variable length array members. */ + uint16_t count; /**< Handle-Value Pair Count. */ + uint16_t value_len; /**< Length of the value in Handle-Value(s) list. */ + uint8_t handle_value[1]; /**< Handle-Value(s) list. To iterate through the list use @ref + sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter. + @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures + with variable length array members. */ } ble_gattc_evt_char_val_by_uuid_read_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_READ_RSP. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - uint16_t offset; /**< Offset of the attribute data. */ - uint16_t len; /**< Attribute data length. */ - uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable - length array members. */ + uint16_t handle; /**< Attribute Handle. */ + uint16_t offset; /**< Offset of the attribute data. */ + uint16_t len; /**< Attribute data length. */ + uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event + structures with variable length array members. */ } ble_gattc_evt_read_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_CHAR_VALS_READ_RSP. */ typedef struct { - uint16_t len; /**< Concatenated Attribute values length. */ - uint8_t values[1]; /**< Attribute values. @note This is a variable length array. The size of 1 indicated is only a placeholder - for compilation. See @ref sd_ble_evt_get for more information on how to use event structures with - variable length array members. */ + uint16_t len; /**< Concatenated Attribute values length. */ + uint8_t values[1]; /**< Attribute values. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event + structures with variable length array members. */ } ble_gattc_evt_char_vals_read_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_WRITE_RSP. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - uint8_t write_op; /**< Type of write operation, see @ref BLE_GATT_WRITE_OPS. */ - uint16_t offset; /**< Data offset. */ - uint16_t len; /**< Data length. */ - uint8_t data[1]; /**< Data. @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable - length array members. */ + uint16_t handle; /**< Attribute Handle. */ + uint8_t write_op; /**< Type of write operation, see @ref BLE_GATT_WRITE_OPS. */ + uint16_t offset; /**< Data offset. */ + uint16_t len; /**< Data length. */ + uint8_t data[1]; /**< Data. @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures with + variable length array members. */ } ble_gattc_evt_write_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_HVX. */ typedef struct { - uint16_t handle; /**< Handle to which the HVx operation applies. */ - uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ - uint16_t len; /**< Attribute data length. */ - uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable - length array members. */ + uint16_t handle; /**< Handle to which the HVx operation applies. */ + uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ + uint16_t len; /**< Attribute data length. */ + uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event + structures with variable length array members. */ } ble_gattc_evt_hvx_t; /**@brief Event structure for @ref BLE_GATTC_EVT_EXCHANGE_MTU_RSP. */ typedef struct { - uint16_t server_rx_mtu; /**< Server RX MTU size. */ + uint16_t server_rx_mtu; /**< Server RX MTU size. */ } ble_gattc_evt_exchange_mtu_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_TIMEOUT. */ typedef struct { - uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ + uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ } ble_gattc_evt_timeout_t; /**@brief Event structure for @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE. */ typedef struct { - uint8_t count; /**< Number of write without response transmissions completed. */ + uint8_t count; /**< Number of write without response transmissions completed. */ } ble_gattc_evt_write_cmd_tx_complete_t; /**@brief GATTC event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which event occurred. */ - uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ - uint16_t - error_handle; /**< In case of error: The handle causing the error. In all other cases @ref BLE_GATT_HANDLE_INVALID. */ - union { - ble_gattc_evt_prim_srvc_disc_rsp_t prim_srvc_disc_rsp; /**< Primary Service Discovery Response Event Parameters. */ - ble_gattc_evt_rel_disc_rsp_t rel_disc_rsp; /**< Relationship Discovery Response Event Parameters. */ - ble_gattc_evt_char_disc_rsp_t char_disc_rsp; /**< Characteristic Discovery Response Event Parameters. */ - ble_gattc_evt_desc_disc_rsp_t desc_disc_rsp; /**< Descriptor Discovery Response Event Parameters. */ - ble_gattc_evt_char_val_by_uuid_read_rsp_t - char_val_by_uuid_read_rsp; /**< Characteristic Value Read by UUID Response Event Parameters. */ - ble_gattc_evt_read_rsp_t read_rsp; /**< Read Response Event Parameters. */ - ble_gattc_evt_char_vals_read_rsp_t char_vals_read_rsp; /**< Characteristic Values Read Response Event Parameters. */ - ble_gattc_evt_write_rsp_t write_rsp; /**< Write Response Event Parameters. */ - ble_gattc_evt_hvx_t hvx; /**< Handle Value Notification/Indication Event Parameters. */ - ble_gattc_evt_exchange_mtu_rsp_t exchange_mtu_rsp; /**< Exchange MTU Response Event Parameters. */ - ble_gattc_evt_timeout_t timeout; /**< Timeout Event Parameters. */ - ble_gattc_evt_attr_info_disc_rsp_t attr_info_disc_rsp; /**< Attribute Information Discovery Event Parameters. */ - ble_gattc_evt_write_cmd_tx_complete_t - write_cmd_tx_complete; /**< Write without Response transmission complete Event Parameters. */ - } params; /**< Event Parameters. @note Only valid if @ref gatt_status == @ref BLE_GATT_STATUS_SUCCESS. */ + uint16_t conn_handle; /**< Connection Handle on which event occurred. */ + uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ + uint16_t error_handle; /**< In case of error: The handle causing the error. In all other cases @ref + BLE_GATT_HANDLE_INVALID. */ + union { + ble_gattc_evt_prim_srvc_disc_rsp_t prim_srvc_disc_rsp; /**< Primary Service Discovery Response Event Parameters. */ + ble_gattc_evt_rel_disc_rsp_t rel_disc_rsp; /**< Relationship Discovery Response Event Parameters. */ + ble_gattc_evt_char_disc_rsp_t char_disc_rsp; /**< Characteristic Discovery Response Event Parameters. */ + ble_gattc_evt_desc_disc_rsp_t desc_disc_rsp; /**< Descriptor Discovery Response Event Parameters. */ + ble_gattc_evt_char_val_by_uuid_read_rsp_t char_val_by_uuid_read_rsp; /**< Characteristic Value Read by UUID Response Event Parameters. */ + ble_gattc_evt_read_rsp_t read_rsp; /**< Read Response Event Parameters. */ + ble_gattc_evt_char_vals_read_rsp_t char_vals_read_rsp; /**< Characteristic Values Read Response Event Parameters. */ + ble_gattc_evt_write_rsp_t write_rsp; /**< Write Response Event Parameters. */ + ble_gattc_evt_hvx_t hvx; /**< Handle Value Notification/Indication Event Parameters. */ + ble_gattc_evt_exchange_mtu_rsp_t exchange_mtu_rsp; /**< Exchange MTU Response Event Parameters. */ + ble_gattc_evt_timeout_t timeout; /**< Timeout Event Parameters. */ + ble_gattc_evt_attr_info_disc_rsp_t attr_info_disc_rsp; /**< Attribute Information Discovery Event Parameters. */ + ble_gattc_evt_write_cmd_tx_complete_t write_cmd_tx_complete; /**< Write without Response transmission complete Event Parameters. */ + } params; /**< Event Parameters. @note Only valid if @ref gatt_status == @ref BLE_GATT_STATUS_SUCCESS. */ } ble_gattc_evt_t; /**@brief UUID discovery option. @@ -370,12 +370,13 @@ typedef struct { * */ typedef struct { - uint8_t auto_add_vs_enable : 1; /**< Set to 1 to enable (or 0 to disable) automatic insertion of discovered 128-bit UUIDs. */ + uint8_t auto_add_vs_enable : 1; /**< Set to 1 to enable (or 0 to disable) automatic insertion of discovered 128-bit + UUIDs. */ } ble_gattc_opt_uuid_disc_t; /**@brief Option structure for GATTC options. */ typedef union { - ble_gattc_opt_uuid_disc_t uuid_disc; /**< Parameters for the UUID discovery option. */ + ble_gattc_opt_uuid_disc_t uuid_disc; /**< Parameters for the UUID discovery option. */ } ble_gattc_opt_t; /** @} */ @@ -386,8 +387,8 @@ typedef union { /**@brief Initiate or continue a GATT Primary Service Discovery procedure. * * @details This function initiates or resumes a Primary Service discovery procedure, starting from the supplied handle. - * If the last service has not been reached, this function must be called again with an updated start handle value to - * continue the search. See also @ref ble_gattc_opt_uuid_disc_t. + * If the last service has not been reached, this function must be called again with an updated start handle + * value to continue the search. See also @ref ble_gattc_opt_uuid_disc_t. * * @events * @event{@ref BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP} @@ -414,8 +415,8 @@ SVCALL(SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Relationship Discovery procedure. * - * @details This function initiates or resumes the Find Included Services sub-procedure. If the last included service has not been - * reached, this must be called again with an updated handle range to continue the search. See also @ref + * @details This function initiates or resumes the Find Included Services sub-procedure. If the last included service + * has not been reached, this must be called again with an updated handle range to continue the search. See also @ref * ble_gattc_opt_uuid_disc_t. * * @events @@ -443,8 +444,8 @@ SVCALL(SD_BLE_GATTC_RELATIONSHIPS_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Characteristic Discovery procedure. * - * @details This function initiates or resumes a Characteristic discovery procedure. If the last Characteristic has not been - * reached, this must be called again with an updated handle range to continue the discovery. See also @ref + * @details This function initiates or resumes a Characteristic discovery procedure. If the last Characteristic has not + * been reached, this must be called again with an updated handle range to continue the discovery. See also @ref * ble_gattc_opt_uuid_disc_t. * * @events @@ -471,8 +472,8 @@ SVCALL(SD_BLE_GATTC_CHARACTERISTICS_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Characteristic Descriptor Discovery procedure. * - * @details This function initiates or resumes a Characteristic Descriptor discovery procedure. If the last Descriptor has not - * been reached, this must be called again with an updated handle range to continue the discovery. See also @ref + * @details This function initiates or resumes a Characteristic Descriptor discovery procedure. If the last Descriptor + * has not been reached, this must be called again with an updated handle range to continue the discovery. See also @ref * ble_gattc_opt_uuid_disc_t. * * @events @@ -499,8 +500,8 @@ SVCALL(SD_BLE_GATTC_DESCRIPTORS_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Read using Characteristic UUID procedure. * - * @details This function initiates or resumes a Read using Characteristic UUID procedure. If the last Characteristic has not been - * reached, this must be called again with an updated handle range to continue the discovery. + * @details This function initiates or resumes a Read using Characteristic UUID procedure. If the last Characteristic + * has not been reached, this must be called again with an updated handle range to continue the discovery. * * @events * @event{@ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP} @@ -523,14 +524,13 @@ SVCALL(SD_BLE_GATTC_DESCRIPTORS_DISCOVER, uint32_t, * reestablishing the connection. */ SVCALL(SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ, uint32_t, - sd_ble_gattc_char_value_by_uuid_read(uint16_t conn_handle, ble_uuid_t const *p_uuid, - ble_gattc_handle_range_t const *p_handle_range)); + sd_ble_gattc_char_value_by_uuid_read(uint16_t conn_handle, ble_uuid_t const *p_uuid, ble_gattc_handle_range_t const *p_handle_range)); /**@brief Initiate or continue a GATT Read (Long) Characteristic or Descriptor procedure. * - * @details This function initiates or resumes a GATT Read (Long) Characteristic or Descriptor procedure. If the Characteristic or - * Descriptor to be read is longer than ATT_MTU - 1, this function must be called multiple times with appropriate offset to read - * the complete value. + * @details This function initiates or resumes a GATT Read (Long) Characteristic or Descriptor procedure. If the + * Characteristic or Descriptor to be read is longer than ATT_MTU - 1, this function must be called multiple times with + * appropriate offset to read the complete value. * * @events * @event{@ref BLE_GATTC_EVT_READ_RSP} @@ -580,8 +580,8 @@ SVCALL(SD_BLE_GATTC_READ, uint32_t, sd_ble_gattc_read(uint16_t conn_handle, uint SVCALL(SD_BLE_GATTC_CHAR_VALUES_READ, uint32_t, sd_ble_gattc_char_values_read(uint16_t conn_handle, uint16_t const *p_handles, uint16_t handle_count)); -/**@brief Perform a Write (Characteristic Value or Descriptor, with or without response, signed or not, long or reliable) - * procedure. +/**@brief Perform a Write (Characteristic Value or Descriptor, with or without response, signed or not, long or + * reliable) procedure. * * @details This function can perform all write procedures described in GATT. * @@ -591,17 +591,17 @@ SVCALL(SD_BLE_GATTC_CHAR_VALUES_READ, uint32_t, * A @ref BLE_GATTC_EVT_WRITE_RSP event will be issued as soon as the write response arrives from the peer. * * @note The number of Write without Response that can be queued is configured by @ref - * ble_gattc_conn_cfg_t::write_cmd_tx_queue_size When the queue is full, the function call will return @ref NRF_ERROR_RESOURCES. - * A @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event will be issued as soon as the transmission of the write without - * response is complete. + * ble_gattc_conn_cfg_t::write_cmd_tx_queue_size When the queue is full, the function call will return @ref + * NRF_ERROR_RESOURCES. A @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event will be issued as soon as the transmission of + * the write without response is complete. * - * @note The application can keep track of the available queue element count for writes without responses by following the - * procedure below: + * @note The application can keep track of the available queue element count for writes without responses by + * following the procedure below: * - Store initial queue element count in a variable. - * - Decrement the variable, which stores the currently available queue element count, by one when a call to this - * function returns @ref NRF_SUCCESS. - * - Increment the variable, which stores the current available queue element count, by the count variable in @ref - * BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event. + * - Decrement the variable, which stores the currently available queue element count, by one when a call to + * this function returns @ref NRF_SUCCESS. + * - Increment the variable, which stores the current available queue element count, by the count variable in + * @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event. * * @events * @event{@ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE, Write without response transmission complete.} @@ -624,8 +624,8 @@ SVCALL(SD_BLE_GATTC_CHAR_VALUES_READ, uint32_t, * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied. - * @retval ::NRF_ERROR_BUSY For write with response, procedure already in progress. Wait for a @ref BLE_GATTC_EVT_WRITE_RSP event - * and retry. + * @retval ::NRF_ERROR_BUSY For write with response, procedure already in progress. Wait for a @ref + * BLE_GATTC_EVT_WRITE_RSP event and retry. * @retval ::NRF_ERROR_RESOURCES Too many writes without responses queued. * Wait for a @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event and retry. * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without @@ -654,7 +654,8 @@ SVCALL(SD_BLE_GATTC_HV_CONFIRM, uint32_t, sd_ble_gattc_hv_confirm(uint16_t conn_ /**@brief Discovers information about a range of attributes on a GATT server. * * @events - * @event{@ref BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, Generated when information about a range of attributes has been received.} + * @event{@ref BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, Generated when information about a range of attributes has been + * received.} * @endevents * * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. @@ -692,7 +693,8 @@ SVCALL(SD_BLE_GATTC_ATTR_INFO_DISCOVER, uint32_t, * - The minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. * - The maximum value is @ref ble_gatt_conn_cfg_t::att_mtu in the connection configuration used for this connection. - * - The value must be equal to Server RX MTU size given in @ref sd_ble_gatts_exchange_mtu_reply + * - The value must be equal to Server RX MTU size given in @ref + sd_ble_gatts_exchange_mtu_reply * if an ATT_MTU exchange has already been performed in the other direction. * * @retval ::NRF_SUCCESS Successfully sent request to the server. @@ -703,8 +705,7 @@ SVCALL(SD_BLE_GATTC_ATTR_INFO_DISCOVER, uint32_t, * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without reestablishing the connection. */ -SVCALL(SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, uint32_t, - sd_ble_gattc_exchange_mtu_request(uint16_t conn_handle, uint16_t client_rx_mtu)); +SVCALL(SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, uint32_t, sd_ble_gattc_exchange_mtu_request(uint16_t conn_handle, uint16_t client_rx_mtu)); /**@brief Iterate through Handle-Value(s) list in @ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP event. * @@ -729,27 +730,24 @@ SVCALL(SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, uint32_t, * @retval ::NRF_SUCCESS Successfully retrieved the next Handle-Value pair. * @retval ::NRF_ERROR_NOT_FOUND No more Handle-Value pairs available in the list. */ -__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, - ble_gattc_handle_value_t *p_iter); +__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, ble_gattc_handle_value_t *p_iter); /** @} */ #ifndef SUPPRESS_INLINE_IMPLEMENTATION -__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, - ble_gattc_handle_value_t *p_iter) -{ - uint32_t value_len = p_gattc_evt->params.char_val_by_uuid_read_rsp.value_len; - uint8_t *p_first = p_gattc_evt->params.char_val_by_uuid_read_rsp.handle_value; - uint8_t *p_next = p_iter->p_value ? p_iter->p_value + value_len : p_first; +__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, ble_gattc_handle_value_t *p_iter) { + uint32_t value_len = p_gattc_evt->params.char_val_by_uuid_read_rsp.value_len; + uint8_t *p_first = p_gattc_evt->params.char_val_by_uuid_read_rsp.handle_value; + uint8_t *p_next = p_iter->p_value ? p_iter->p_value + value_len : p_first; - if ((p_next - p_first) / (sizeof(uint16_t) + value_len) < p_gattc_evt->params.char_val_by_uuid_read_rsp.count) { - p_iter->handle = (uint16_t)p_next[1] << 8 | p_next[0]; - p_iter->p_value = p_next + sizeof(uint16_t); - return NRF_SUCCESS; - } else { - return NRF_ERROR_NOT_FOUND; - } + if ((p_next - p_first) / (sizeof(uint16_t) + value_len) < p_gattc_evt->params.char_val_by_uuid_read_rsp.count) { + p_iter->handle = (uint16_t)p_next[1] << 8 | p_next[0]; + p_iter->p_value = p_next + sizeof(uint16_t); + return NRF_SUCCESS; + } else { + return NRF_ERROR_NOT_FOUND; + } } #endif /* SUPPRESS_INLINE_IMPLEMENTATION */ diff --git a/src/platform/nrf52/softdevice/ble_gatts.h b/src/platform/nrf52/softdevice/ble_gatts.h index dc94957cd..e46b654cd 100644 --- a/src/platform/nrf52/softdevice/ble_gatts.h +++ b/src/platform/nrf52/softdevice/ble_gatts.h @@ -66,45 +66,51 @@ extern "C" { * @brief GATTS API SVC numbers. */ enum BLE_GATTS_SVCS { - SD_BLE_GATTS_SERVICE_ADD = BLE_GATTS_SVC_BASE, /**< Add a service. */ - SD_BLE_GATTS_INCLUDE_ADD, /**< Add an included service. */ - SD_BLE_GATTS_CHARACTERISTIC_ADD, /**< Add a characteristic. */ - SD_BLE_GATTS_DESCRIPTOR_ADD, /**< Add a generic attribute. */ - SD_BLE_GATTS_VALUE_SET, /**< Set an attribute value. */ - SD_BLE_GATTS_VALUE_GET, /**< Get an attribute value. */ - SD_BLE_GATTS_HVX, /**< Handle Value Notification or Indication. */ - SD_BLE_GATTS_SERVICE_CHANGED, /**< Perform a Service Changed Indication to one or more peers. */ - SD_BLE_GATTS_RW_AUTHORIZE_REPLY, /**< Reply to an authorization request for a read or write operation on one or more - attributes. */ - SD_BLE_GATTS_SYS_ATTR_SET, /**< Set the persistent system attributes for a connection. */ - SD_BLE_GATTS_SYS_ATTR_GET, /**< Retrieve the persistent system attributes. */ - SD_BLE_GATTS_INITIAL_USER_HANDLE_GET, /**< Retrieve the first valid user handle. */ - SD_BLE_GATTS_ATTR_GET, /**< Retrieve the UUID and/or metadata of an attribute. */ - SD_BLE_GATTS_EXCHANGE_MTU_REPLY /**< Reply to Exchange MTU Request. */ + SD_BLE_GATTS_SERVICE_ADD = BLE_GATTS_SVC_BASE, /**< Add a service. */ + SD_BLE_GATTS_INCLUDE_ADD, /**< Add an included service. */ + SD_BLE_GATTS_CHARACTERISTIC_ADD, /**< Add a characteristic. */ + SD_BLE_GATTS_DESCRIPTOR_ADD, /**< Add a generic attribute. */ + SD_BLE_GATTS_VALUE_SET, /**< Set an attribute value. */ + SD_BLE_GATTS_VALUE_GET, /**< Get an attribute value. */ + SD_BLE_GATTS_HVX, /**< Handle Value Notification or Indication. */ + SD_BLE_GATTS_SERVICE_CHANGED, /**< Perform a Service Changed Indication to one or more peers. */ + SD_BLE_GATTS_RW_AUTHORIZE_REPLY, /**< Reply to an authorization request for a read or write operation on one or more + attributes. */ + SD_BLE_GATTS_SYS_ATTR_SET, /**< Set the persistent system attributes for a connection. */ + SD_BLE_GATTS_SYS_ATTR_GET, /**< Retrieve the persistent system attributes. */ + SD_BLE_GATTS_INITIAL_USER_HANDLE_GET, /**< Retrieve the first valid user handle. */ + SD_BLE_GATTS_ATTR_GET, /**< Retrieve the UUID and/or metadata of an attribute. */ + SD_BLE_GATTS_EXCHANGE_MTU_REPLY /**< Reply to Exchange MTU Request. */ }; /** * @brief GATT Server Event IDs. */ enum BLE_GATTS_EVTS { - BLE_GATTS_EVT_WRITE = BLE_GATTS_EVT_BASE, /**< Write operation performed. \n See - @ref ble_gatts_evt_write_t. */ - BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST, /**< Read/Write Authorization request. \n Reply with - @ref sd_ble_gatts_rw_authorize_reply. \n See @ref ble_gatts_evt_rw_authorize_request_t. - */ - BLE_GATTS_EVT_SYS_ATTR_MISSING, /**< A persistent system attribute access is pending. \n Respond with @ref - sd_ble_gatts_sys_attr_set. \n See @ref ble_gatts_evt_sys_attr_missing_t. */ - BLE_GATTS_EVT_HVC, /**< Handle Value Confirmation. \n See @ref ble_gatts_evt_hvc_t. - */ - BLE_GATTS_EVT_SC_CONFIRM, /**< Service Changed Confirmation. \n No additional event - structure applies. */ - BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. \n Reply with - @ref sd_ble_gatts_exchange_mtu_reply. \n See @ref ble_gatts_evt_exchange_mtu_request_t. - */ - BLE_GATTS_EVT_TIMEOUT, /**< Peer failed to respond to an ATT request in time. \n See @ref - ble_gatts_evt_timeout_t. */ - BLE_GATTS_EVT_HVN_TX_COMPLETE /**< Handle Value Notification transmission complete. \n See @ref - ble_gatts_evt_hvn_tx_complete_t. */ + BLE_GATTS_EVT_WRITE = BLE_GATTS_EVT_BASE, /**< Write operation performed. \n See + @ref ble_gatts_evt_write_t. */ + BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST, /**< Read/Write Authorization request. \n Reply + with + @ref sd_ble_gatts_rw_authorize_reply. \n See @ref + ble_gatts_evt_rw_authorize_request_t. + */ + BLE_GATTS_EVT_SYS_ATTR_MISSING, /**< A persistent system attribute access is pending. \n Respond + with @ref sd_ble_gatts_sys_attr_set. \n See @ref + ble_gatts_evt_sys_attr_missing_t. */ + BLE_GATTS_EVT_HVC, /**< Handle Value Confirmation. \n See @ref + * ble_gatts_evt_hvc_t. + */ + BLE_GATTS_EVT_SC_CONFIRM, /**< Service Changed Confirmation. \n No additional + event structure applies. */ + BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. \n Reply + with + @ref sd_ble_gatts_exchange_mtu_reply. \n See @ref + ble_gatts_evt_exchange_mtu_request_t. + */ + BLE_GATTS_EVT_TIMEOUT, /**< Peer failed to respond to an ATT request in time. \n See @ref + ble_gatts_evt_timeout_t. */ + BLE_GATTS_EVT_HVN_TX_COMPLETE /**< Handle Value Notification transmission complete. \n See @ref + ble_gatts_evt_hvn_tx_complete_t. */ }; /**@brief GATTS Configuration IDs. @@ -112,9 +118,9 @@ enum BLE_GATTS_EVTS { * IDs that uniquely identify a GATTS configuration. */ enum BLE_GATTS_CFGS { - BLE_GATTS_CFG_SERVICE_CHANGED = BLE_GATTS_CFG_BASE, /**< Service changed configuration. */ - BLE_GATTS_CFG_ATTR_TAB_SIZE, /**< Attribute table size configuration. */ - BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM, /**< Service changed CCCD permission configuration. */ + BLE_GATTS_CFG_SERVICE_CHANGED = BLE_GATTS_CFG_BASE, /**< Service changed configuration. */ + BLE_GATTS_CFG_ATTR_TAB_SIZE, /**< Attribute table size configuration. */ + BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM, /**< Service changed CCCD permission configuration. */ }; /** @} */ @@ -168,10 +174,10 @@ enum BLE_GATTS_CFGS { * @{ */ #define BLE_GATTS_VLOC_INVALID 0x00 /**< Invalid Location. */ #define BLE_GATTS_VLOC_STACK 0x01 /**< Attribute Value is located in stack memory, no user memory is required. */ -#define BLE_GATTS_VLOC_USER \ - 0x02 /**< Attribute Value is located in user memory. This requires the user to maintain a valid buffer through the lifetime \ - of the attribute, since the stack will read and write directly to the memory using the pointer provided in the APIs. \ - There are no alignment requirements for the buffer. */ +#define BLE_GATTS_VLOC_USER \ + 0x02 /**< Attribute Value is located in user memory. This requires the user to maintain a valid buffer through the \ + lifetime of the attribute, since the stack will read and write directly to the memory using the pointer \ + provided in the APIs. There are no alignment requirements for the buffer. */ /** @} */ /** @defgroup BLE_GATTS_AUTHORIZE_TYPES GATT Server Authorization Types @@ -190,8 +196,7 @@ enum BLE_GATTS_CFGS { /** @defgroup BLE_GATTS_SERVICE_CHANGED Service Changed Inclusion Values * @{ */ -#define BLE_GATTS_SERVICE_CHANGED_DEFAULT \ - (1) /**< Default is to include the Service Changed characteristic in the Attribute Table. */ +#define BLE_GATTS_SERVICE_CHANGED_DEFAULT (1) /**< Default is to include the Service Changed characteristic in the Attribute Table. */ /** @} */ /** @defgroup BLE_GATTS_ATTR_TAB_SIZE Attribute Table size @@ -204,8 +209,7 @@ enum BLE_GATTS_CFGS { /** @defgroup BLE_GATTS_DEFAULTS GATT Server defaults * @{ */ -#define BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT \ - 1 /**< Default number of Handle Value Notifications that can be queued for transmission. */ +#define BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT 1 /**< Default number of Handle Value Notifications that can be queued for transmission. */ /** @} */ /** @} */ @@ -217,115 +221,115 @@ enum BLE_GATTS_CFGS { * @brief BLE GATTS connection configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t hvn_tx_queue_size; /**< Minimum guaranteed number of Handle Value Notifications that can be queued for transmission. - The default value is @ref BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT */ + uint8_t hvn_tx_queue_size; /**< Minimum guaranteed number of Handle Value Notifications that can be queued for + transmission. The default value is @ref BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT */ } ble_gatts_conn_cfg_t; /**@brief Attribute metadata. */ typedef struct { - ble_gap_conn_sec_mode_t read_perm; /**< Read permissions. */ - ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ - uint8_t vlen : 1; /**< Variable length attribute. */ - uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ - uint8_t rd_auth : 1; /**< Read authorization and value will be requested from the application on every read operation. */ - uint8_t wr_auth : 1; /**< Write authorization will be requested from the application on every Write Request operation (but not - Write Command). */ + ble_gap_conn_sec_mode_t read_perm; /**< Read permissions. */ + ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ + uint8_t vlen : 1; /**< Variable length attribute. */ + uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ + uint8_t rd_auth : 1; /**< Read authorization and value will be requested from the application on every read operation. */ + uint8_t wr_auth : 1; /**< Write authorization will be requested from the application on every Write Request operation + (but not Write Command). */ } ble_gatts_attr_md_t; /**@brief GATT Attribute. */ typedef struct { - ble_uuid_t const *p_uuid; /**< Pointer to the attribute UUID. */ - ble_gatts_attr_md_t const *p_attr_md; /**< Pointer to the attribute metadata structure. */ - uint16_t init_len; /**< Initial attribute value length in bytes. */ - uint16_t init_offs; /**< Initial attribute value offset in bytes. If different from zero, the first init_offs bytes of the - attribute value will be left uninitialized. */ - uint16_t max_len; /**< Maximum attribute value length in bytes, see @ref BLE_GATTS_ATTR_LENS_MAX for maximum values. */ - uint8_t *p_value; /**< Pointer to the attribute data. Please note that if the @ref BLE_GATTS_VLOC_USER value location is - selected in the attribute metadata, this will have to point to a buffer that remains valid through the - lifetime of the attribute. This excludes usage of automatic variables that may go out of scope or any - other temporary location. The stack may access that memory directly without the application's - knowledge. For writable characteristics, this value must not be a location in flash memory.*/ + ble_uuid_t const *p_uuid; /**< Pointer to the attribute UUID. */ + ble_gatts_attr_md_t const *p_attr_md; /**< Pointer to the attribute metadata structure. */ + uint16_t init_len; /**< Initial attribute value length in bytes. */ + uint16_t init_offs; /**< Initial attribute value offset in bytes. If different from zero, the first init_offs bytes of + the attribute value will be left uninitialized. */ + uint16_t max_len; /**< Maximum attribute value length in bytes, see @ref BLE_GATTS_ATTR_LENS_MAX for maximum values. */ + uint8_t *p_value; /**< Pointer to the attribute data. Please note that if the @ref BLE_GATTS_VLOC_USER value location is + selected in the attribute metadata, this will have to point to a buffer that remains valid through + the lifetime of the attribute. This excludes usage of automatic variables that may go out of scope or + any other temporary location. The stack may access that memory directly without the application's + knowledge. For writable characteristics, this value must not be a location in flash memory.*/ } ble_gatts_attr_t; /**@brief GATT Attribute Value. */ typedef struct { - uint16_t len; /**< Length in bytes to be written or read. Length in bytes written or read after successful return.*/ - uint16_t offset; /**< Attribute value offset. */ - uint8_t *p_value; /**< Pointer to where value is stored or will be stored. - If value is stored in user memory, only the attribute length is updated when p_value == NULL. - Set to NULL when reading to obtain the complete length of the attribute value */ + uint16_t len; /**< Length in bytes to be written or read. Length in bytes written or read after successful return.*/ + uint16_t offset; /**< Attribute value offset. */ + uint8_t *p_value; /**< Pointer to where value is stored or will be stored. + If value is stored in user memory, only the attribute length is updated when p_value == NULL. + Set to NULL when reading to obtain the complete length of the attribute value */ } ble_gatts_value_t; /**@brief GATT Characteristic Presentation Format. */ typedef struct { - uint8_t format; /**< Format of the value, see @ref BLE_GATT_CPF_FORMATS. */ - int8_t exponent; /**< Exponent for integer data types. */ - uint16_t unit; /**< Unit from Bluetooth Assigned Numbers. */ - uint8_t name_space; /**< Namespace from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ - uint16_t desc; /**< Namespace description from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ + uint8_t format; /**< Format of the value, see @ref BLE_GATT_CPF_FORMATS. */ + int8_t exponent; /**< Exponent for integer data types. */ + uint16_t unit; /**< Unit from Bluetooth Assigned Numbers. */ + uint8_t name_space; /**< Namespace from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ + uint16_t desc; /**< Namespace description from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ } ble_gatts_char_pf_t; /**@brief GATT Characteristic metadata. */ typedef struct { - ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ - ble_gatt_char_ext_props_t char_ext_props; /**< Characteristic Extended Properties. */ - uint8_t const * - p_char_user_desc; /**< Pointer to a UTF-8 encoded string (non-NULL terminated), NULL if the descriptor is not required. */ - uint16_t char_user_desc_max_size; /**< The maximum size in bytes of the user description descriptor. */ - uint16_t char_user_desc_size; /**< The size of the user description, must be smaller or equal to char_user_desc_max_size. */ - ble_gatts_char_pf_t const - *p_char_pf; /**< Pointer to a presentation format structure or NULL if the CPF descriptor is not required. */ - ble_gatts_attr_md_t const - *p_user_desc_md; /**< Attribute metadata for the User Description descriptor, or NULL for default values. */ - ble_gatts_attr_md_t const - *p_cccd_md; /**< Attribute metadata for the Client Characteristic Configuration Descriptor, or NULL for default values. */ - ble_gatts_attr_md_t const - *p_sccd_md; /**< Attribute metadata for the Server Characteristic Configuration Descriptor, or NULL for default values. */ + ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ + ble_gatt_char_ext_props_t char_ext_props; /**< Characteristic Extended Properties. */ + uint8_t const *p_char_user_desc; /**< Pointer to a UTF-8 encoded string (non-NULL terminated), NULL if the descriptor + is not required. */ + uint16_t char_user_desc_max_size; /**< The maximum size in bytes of the user description descriptor. */ + uint16_t char_user_desc_size; /**< The size of the user description, must be smaller or equal to + char_user_desc_max_size. */ + ble_gatts_char_pf_t const *p_char_pf; /**< Pointer to a presentation format structure or NULL if the CPF descriptor is not required. */ + ble_gatts_attr_md_t const *p_user_desc_md; /**< Attribute metadata for the User Description descriptor, or NULL for default values. */ + ble_gatts_attr_md_t const *p_cccd_md; /**< Attribute metadata for the Client Characteristic Configuration Descriptor, + or NULL for default values. */ + ble_gatts_attr_md_t const *p_sccd_md; /**< Attribute metadata for the Server Characteristic Configuration Descriptor, + or NULL for default values. */ } ble_gatts_char_md_t; /**@brief GATT Characteristic Definition Handles. */ typedef struct { - uint16_t value_handle; /**< Handle to the characteristic value. */ - uint16_t user_desc_handle; /**< Handle to the User Description descriptor, or @ref BLE_GATT_HANDLE_INVALID if not present. */ - uint16_t cccd_handle; /**< Handle to the Client Characteristic Configuration Descriptor, or @ref BLE_GATT_HANDLE_INVALID if - not present. */ - uint16_t sccd_handle; /**< Handle to the Server Characteristic Configuration Descriptor, or @ref BLE_GATT_HANDLE_INVALID if - not present. */ + uint16_t value_handle; /**< Handle to the characteristic value. */ + uint16_t user_desc_handle; /**< Handle to the User Description descriptor, or @ref BLE_GATT_HANDLE_INVALID if not + present. */ + uint16_t cccd_handle; /**< Handle to the Client Characteristic Configuration Descriptor, or @ref + BLE_GATT_HANDLE_INVALID if not present. */ + uint16_t sccd_handle; /**< Handle to the Server Characteristic Configuration Descriptor, or @ref + BLE_GATT_HANDLE_INVALID if not present. */ } ble_gatts_char_handles_t; /**@brief GATT HVx parameters. */ typedef struct { - uint16_t handle; /**< Characteristic Value Handle. */ - uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ - uint16_t offset; /**< Offset within the attribute value. */ - uint16_t *p_len; /**< Length in bytes to be written, length in bytes written after return. */ - uint8_t const *p_data; /**< Actual data content, use NULL to use the current attribute value. */ + uint16_t handle; /**< Characteristic Value Handle. */ + uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ + uint16_t offset; /**< Offset within the attribute value. */ + uint16_t *p_len; /**< Length in bytes to be written, length in bytes written after return. */ + uint8_t const *p_data; /**< Actual data content, use NULL to use the current attribute value. */ } ble_gatts_hvx_params_t; /**@brief GATT Authorization parameters. */ typedef struct { - uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ - uint8_t update : 1; /**< If set, data supplied in p_data will be used to update the attribute value. - Please note that for @ref BLE_GATTS_AUTHORIZE_TYPE_WRITE operations this bit must always be set, - as the data to be written needs to be stored and later provided by the application. */ - uint16_t offset; /**< Offset of the attribute value being updated. */ - uint16_t len; /**< Length in bytes of the value in p_data pointer, see @ref BLE_GATTS_ATTR_LENS_MAX. */ - uint8_t const *p_data; /**< Pointer to new value used to update the attribute value. */ + uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ + uint8_t update : 1; /**< If set, data supplied in p_data will be used to update the attribute value. + Please note that for @ref BLE_GATTS_AUTHORIZE_TYPE_WRITE operations this bit must always be + set, as the data to be written needs to be stored and later provided by the application. */ + uint16_t offset; /**< Offset of the attribute value being updated. */ + uint16_t len; /**< Length in bytes of the value in p_data pointer, see @ref BLE_GATTS_ATTR_LENS_MAX. */ + uint8_t const *p_data; /**< Pointer to new value used to update the attribute value. */ } ble_gatts_authorize_params_t; /**@brief GATT Read or Write Authorize Reply parameters. */ typedef struct { - uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ - union { - ble_gatts_authorize_params_t read; /**< Read authorization parameters. */ - ble_gatts_authorize_params_t write; /**< Write authorization parameters. */ - } params; /**< Reply Parameters. */ + uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ + union { + ble_gatts_authorize_params_t read; /**< Read authorization parameters. */ + ble_gatts_authorize_params_t write; /**< Write authorization parameters. */ + } params; /**< Reply Parameters. */ } ble_gatts_rw_authorize_reply_params_t; /**@brief Service Changed Inclusion configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t service_changed : 1; /**< If 1, include the Service Changed characteristic in the Attribute Table. Default is @ref - BLE_GATTS_SERVICE_CHANGED_DEFAULT. */ + uint8_t service_changed : 1; /**< If 1, include the Service Changed characteristic in the Attribute Table. Default is + @ref BLE_GATTS_SERVICE_CHANGED_DEFAULT. */ } ble_gatts_cfg_service_changed_t; /**@brief Service Changed CCCD permission configuration parameters, set with @ref sd_ble_cfg_set. @@ -334,16 +338,16 @@ typedef struct { * * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: * - @ref ble_gatts_attr_md_t::write_perm is out of range. - * - @ref ble_gatts_attr_md_t::write_perm is @ref BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS, that is - * not allowed by the Bluetooth Specification. - * - wrong @ref ble_gatts_attr_md_t::read_perm, only @ref BLE_GAP_CONN_SEC_MODE_SET_OPEN is - * allowed by the Bluetooth Specification. + * - @ref ble_gatts_attr_md_t::write_perm is @ref BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS, + * that is not allowed by the Bluetooth Specification. + * - wrong @ref ble_gatts_attr_md_t::read_perm, only @ref + * BLE_GAP_CONN_SEC_MODE_SET_OPEN is allowed by the Bluetooth Specification. * - wrong @ref ble_gatts_attr_md_t::vloc, only @ref BLE_GATTS_VLOC_STACK is allowed. * @retval ::NRF_ERROR_NOT_SUPPORTED Security Mode 2 not supported */ typedef struct { - ble_gatts_attr_md_t - perm; /**< Permission for Service Changed CCCD. Default is @ref BLE_GAP_CONN_SEC_MODE_SET_OPEN, no authorization. */ + ble_gatts_attr_md_t perm; /**< Permission for Service Changed CCCD. Default is @ref BLE_GAP_CONN_SEC_MODE_SET_OPEN, no + authorization. */ } ble_gatts_cfg_service_changed_cccd_perm_t; /**@brief Attribute table size configuration parameters, set with @ref sd_ble_cfg_set. @@ -354,86 +358,85 @@ typedef struct { * - The specified Attribute Table size is not a multiple of 4. */ typedef struct { - uint32_t attr_tab_size; /**< Attribute table size. Default is @ref BLE_GATTS_ATTR_TAB_SIZE_DEFAULT, minimum is @ref - BLE_GATTS_ATTR_TAB_SIZE_MIN. */ + uint32_t attr_tab_size; /**< Attribute table size. Default is @ref BLE_GATTS_ATTR_TAB_SIZE_DEFAULT, minimum is @ref + BLE_GATTS_ATTR_TAB_SIZE_MIN. */ } ble_gatts_cfg_attr_tab_size_t; /**@brief Config structure for GATTS configurations. */ typedef union { - ble_gatts_cfg_service_changed_t - service_changed; /**< Include service changed characteristic, cfg_id is @ref BLE_GATTS_CFG_SERVICE_CHANGED. */ - ble_gatts_cfg_service_changed_cccd_perm_t service_changed_cccd_perm; /**< Service changed CCCD permission, cfg_id is @ref - BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM. */ - ble_gatts_cfg_attr_tab_size_t attr_tab_size; /**< Attribute table size, cfg_id is @ref BLE_GATTS_CFG_ATTR_TAB_SIZE. */ + ble_gatts_cfg_service_changed_t service_changed; /**< Include service changed characteristic, cfg_id is @ref BLE_GATTS_CFG_SERVICE_CHANGED. */ + ble_gatts_cfg_service_changed_cccd_perm_t service_changed_cccd_perm; /**< Service changed CCCD permission, cfg_id is @ref + BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM. */ + ble_gatts_cfg_attr_tab_size_t attr_tab_size; /**< Attribute table size, cfg_id is @ref BLE_GATTS_CFG_ATTR_TAB_SIZE. */ } ble_gatts_cfg_t; /**@brief Event structure for @ref BLE_GATTS_EVT_WRITE. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - ble_uuid_t uuid; /**< Attribute UUID. */ - uint8_t op; /**< Type of write operation, see @ref BLE_GATTS_OPS. */ - uint8_t auth_required; /**< Writing operation deferred due to authorization requirement. Application may use @ref - sd_ble_gatts_value_set to finalize the writing operation. */ - uint16_t offset; /**< Offset for the write operation. */ - uint16_t len; /**< Length of the received data. */ - uint8_t data[1]; /**< Received data. @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable - length array members. */ + uint16_t handle; /**< Attribute Handle. */ + ble_uuid_t uuid; /**< Attribute UUID. */ + uint8_t op; /**< Type of write operation, see @ref BLE_GATTS_OPS. */ + uint8_t auth_required; /**< Writing operation deferred due to authorization requirement. Application may use @ref + sd_ble_gatts_value_set to finalize the writing operation. */ + uint16_t offset; /**< Offset for the write operation. */ + uint16_t len; /**< Length of the received data. */ + uint8_t data[1]; /**< Received data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use + event structures with variable length array members. */ } ble_gatts_evt_write_t; /**@brief Event substructure for authorized read requests, see @ref ble_gatts_evt_rw_authorize_request_t. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - ble_uuid_t uuid; /**< Attribute UUID. */ - uint16_t offset; /**< Offset for the read operation. */ + uint16_t handle; /**< Attribute Handle. */ + ble_uuid_t uuid; /**< Attribute UUID. */ + uint16_t offset; /**< Offset for the read operation. */ } ble_gatts_evt_read_t; /**@brief Event structure for @ref BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST. */ typedef struct { - uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ - union { - ble_gatts_evt_read_t read; /**< Attribute Read Parameters. */ - ble_gatts_evt_write_t write; /**< Attribute Write Parameters. */ - } request; /**< Request Parameters. */ + uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ + union { + ble_gatts_evt_read_t read; /**< Attribute Read Parameters. */ + ble_gatts_evt_write_t write; /**< Attribute Write Parameters. */ + } request; /**< Request Parameters. */ } ble_gatts_evt_rw_authorize_request_t; /**@brief Event structure for @ref BLE_GATTS_EVT_SYS_ATTR_MISSING. */ typedef struct { - uint8_t hint; /**< Hint (currently unused). */ + uint8_t hint; /**< Hint (currently unused). */ } ble_gatts_evt_sys_attr_missing_t; /**@brief Event structure for @ref BLE_GATTS_EVT_HVC. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ + uint16_t handle; /**< Attribute Handle. */ } ble_gatts_evt_hvc_t; /**@brief Event structure for @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST. */ typedef struct { - uint16_t client_rx_mtu; /**< Client RX MTU size. */ + uint16_t client_rx_mtu; /**< Client RX MTU size. */ } ble_gatts_evt_exchange_mtu_request_t; /**@brief Event structure for @ref BLE_GATTS_EVT_TIMEOUT. */ typedef struct { - uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ + uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ } ble_gatts_evt_timeout_t; /**@brief Event structure for @ref BLE_GATTS_EVT_HVN_TX_COMPLETE. */ typedef struct { - uint8_t count; /**< Number of notification transmissions completed. */ + uint8_t count; /**< Number of notification transmissions completed. */ } ble_gatts_evt_hvn_tx_complete_t; /**@brief GATTS event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which the event occurred. */ - union { - ble_gatts_evt_write_t write; /**< Write Event Parameters. */ - ble_gatts_evt_rw_authorize_request_t authorize_request; /**< Read or Write Authorize Request Parameters. */ - ble_gatts_evt_sys_attr_missing_t sys_attr_missing; /**< System attributes missing. */ - ble_gatts_evt_hvc_t hvc; /**< Handle Value Confirmation Event Parameters. */ - ble_gatts_evt_exchange_mtu_request_t exchange_mtu_request; /**< Exchange MTU Request Event Parameters. */ - ble_gatts_evt_timeout_t timeout; /**< Timeout Event. */ - ble_gatts_evt_hvn_tx_complete_t hvn_tx_complete; /**< Handle Value Notification transmission complete Event Parameters. */ - } params; /**< Event Parameters. */ + uint16_t conn_handle; /**< Connection Handle on which the event occurred. */ + union { + ble_gatts_evt_write_t write; /**< Write Event Parameters. */ + ble_gatts_evt_rw_authorize_request_t authorize_request; /**< Read or Write Authorize Request Parameters. */ + ble_gatts_evt_sys_attr_missing_t sys_attr_missing; /**< System attributes missing. */ + ble_gatts_evt_hvc_t hvc; /**< Handle Value Confirmation Event Parameters. */ + ble_gatts_evt_exchange_mtu_request_t exchange_mtu_request; /**< Exchange MTU Request Event Parameters. */ + ble_gatts_evt_timeout_t timeout; /**< Timeout Event. */ + ble_gatts_evt_hvn_tx_complete_t hvn_tx_complete; /**< Handle Value Notification transmission complete Event Parameters. */ + } params; /**< Event Parameters. */ } ble_gatts_evt_t; /** @} */ @@ -443,8 +446,9 @@ typedef struct { /**@brief Add a service declaration to the Attribute Table. * - * @note Secondary Services are only relevant in the context of the entity that references them, it is therefore forbidden to - * add a secondary service declaration that is not referenced by another service later in the Attribute Table. + * @note Secondary Services are only relevant in the context of the entity that references them, it is therefore + * forbidden to add a secondary service declaration that is not referenced by another service later in the Attribute + * Table. * * @mscs * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} @@ -456,7 +460,8 @@ typedef struct { * * @retval ::NRF_SUCCESS Successfully added a service declaration. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. - * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, Vendor Specific UUIDs need to be present in the table. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, Vendor Specific UUIDs need to be present in the + * table. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. */ @@ -464,8 +469,8 @@ SVCALL(SD_BLE_GATTS_SERVICE_ADD, uint32_t, sd_ble_gatts_service_add(uint8_t type /**@brief Add an include declaration to the Attribute Table. * - * @note It is currently only possible to add an include declaration to the last added service (i.e. only sequential population is - * supported at this time). + * @note It is currently only possible to add an include declaration to the last added service (i.e. only sequential + * population is supported at this time). * * @note The included service must already be present in the Attribute Table prior to this call. * @@ -473,42 +478,42 @@ SVCALL(SD_BLE_GATTS_SERVICE_ADD, uint32_t, sd_ble_gatts_service_add(uint8_t type * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} * @endmscs * - * @param[in] service_handle Handle of the service where the included service is to be placed, if @ref BLE_GATT_HANDLE_INVALID - * is used, it will be placed sequentially. + * @param[in] service_handle Handle of the service where the included service is to be placed, if @ref + * BLE_GATT_HANDLE_INVALID is used, it will be placed sequentially. * @param[in] inc_srvc_handle Handle of the included service. * @param[out] p_include_handle Pointer to a 16-bit word where the assigned handle will be stored. * * @retval ::NRF_SUCCESS Successfully added an include declaration. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. - * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, handle values need to match previously added services. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, handle values need to match previously added + * services. * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation, a service context is required. * @retval ::NRF_ERROR_NOT_SUPPORTED Feature is not supported, service_handle must be that of the last added service. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, self inclusions are not allowed. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. */ -SVCALL(SD_BLE_GATTS_INCLUDE_ADD, uint32_t, - sd_ble_gatts_include_add(uint16_t service_handle, uint16_t inc_srvc_handle, uint16_t *p_include_handle)); +SVCALL(SD_BLE_GATTS_INCLUDE_ADD, uint32_t, sd_ble_gatts_include_add(uint16_t service_handle, uint16_t inc_srvc_handle, uint16_t *p_include_handle)); -/**@brief Add a characteristic declaration, a characteristic value declaration and optional characteristic descriptor declarations - * to the Attribute Table. +/**@brief Add a characteristic declaration, a characteristic value declaration and optional characteristic descriptor + * declarations to the Attribute Table. * - * @note It is currently only possible to add a characteristic to the last added service (i.e. only sequential population is - * supported at this time). + * @note It is currently only possible to add a characteristic to the last added service (i.e. only sequential + * population is supported at this time). * - * @note Several restrictions apply to the parameters, such as matching permissions between the user description descriptor and - * the writable auxiliaries bits, readable (no security) and writable (selectable) CCCDs and SCCDs and valid presentation format - * values. + * @note Several restrictions apply to the parameters, such as matching permissions between the user description + * descriptor and the writable auxiliaries bits, readable (no security) and writable (selectable) CCCDs and SCCDs and + * valid presentation format values. * - * @note If no metadata is provided for the optional descriptors, their permissions will be derived from the characteristic - * permissions. + * @note If no metadata is provided for the optional descriptors, their permissions will be derived from the + * characteristic permissions. * * @mscs * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} * @endmscs * - * @param[in] service_handle Handle of the service where the characteristic is to be placed, if @ref BLE_GATT_HANDLE_INVALID is - * used, it will be placed sequentially. + * @param[in] service_handle Handle of the service where the characteristic is to be placed, if @ref + * BLE_GATT_HANDLE_INVALID is used, it will be placed sequentially. * @param[in] p_char_md Characteristic metadata. * @param[in] p_attr_char_value Pointer to the attribute structure corresponding to the characteristic value. * @param[out] p_handles Pointer to the structure where the assigned handles will be stored. @@ -520,37 +525,38 @@ SVCALL(SD_BLE_GATTS_INCLUDE_ADD, uint32_t, * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation, a service context is required. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. - * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref BLE_GATTS_ATTR_LENS_MAX. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref + * BLE_GATTS_ATTR_LENS_MAX. */ SVCALL(SD_BLE_GATTS_CHARACTERISTIC_ADD, uint32_t, - sd_ble_gatts_characteristic_add(uint16_t service_handle, ble_gatts_char_md_t const *p_char_md, - ble_gatts_attr_t const *p_attr_char_value, ble_gatts_char_handles_t *p_handles)); + sd_ble_gatts_characteristic_add(uint16_t service_handle, ble_gatts_char_md_t const *p_char_md, ble_gatts_attr_t const *p_attr_char_value, + ble_gatts_char_handles_t *p_handles)); /**@brief Add a descriptor to the Attribute Table. * - * @note It is currently only possible to add a descriptor to the last added characteristic (i.e. only sequential population is - * supported at this time). + * @note It is currently only possible to add a descriptor to the last added characteristic (i.e. only sequential + * population is supported at this time). * * @mscs * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} * @endmscs * - * @param[in] char_handle Handle of the characteristic where the descriptor is to be placed, if @ref BLE_GATT_HANDLE_INVALID is - * used, it will be placed sequentially. + * @param[in] char_handle Handle of the characteristic where the descriptor is to be placed, if @ref + * BLE_GATT_HANDLE_INVALID is used, it will be placed sequentially. * @param[in] p_attr Pointer to the attribute structure. * @param[out] p_handle Pointer to a 16-bit word where the assigned handle will be stored. * * @retval ::NRF_SUCCESS Successfully added a descriptor. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. - * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, characteristic handle, Vendor Specific UUIDs, lengths, and - * permissions need to adhere to the constraints. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, characteristic handle, Vendor Specific UUIDs, + * lengths, and permissions need to adhere to the constraints. * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation, a characteristic context is required. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. - * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref BLE_GATTS_ATTR_LENS_MAX. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref + * BLE_GATTS_ATTR_LENS_MAX. */ -SVCALL(SD_BLE_GATTS_DESCRIPTOR_ADD, uint32_t, - sd_ble_gatts_descriptor_add(uint16_t char_handle, ble_gatts_attr_t const *p_attr, uint16_t *p_handle)); +SVCALL(SD_BLE_GATTS_DESCRIPTOR_ADD, uint32_t, sd_ble_gatts_descriptor_add(uint16_t char_handle, ble_gatts_attr_t const *p_attr, uint16_t *p_handle)); /**@brief Set the value of a given attribute. * @@ -570,11 +576,11 @@ SVCALL(SD_BLE_GATTS_DESCRIPTOR_ADD, uint32_t, * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. * @retval ::NRF_ERROR_FORBIDDEN Forbidden handle supplied, certain attributes are not modifiable by the application. - * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref BLE_GATTS_ATTR_LENS_MAX. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref + * BLE_GATTS_ATTR_LENS_MAX. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied on a system attribute. */ -SVCALL(SD_BLE_GATTS_VALUE_SET, uint32_t, - sd_ble_gatts_value_set(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); +SVCALL(SD_BLE_GATTS_VALUE_SET, uint32_t, sd_ble_gatts_value_set(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); /**@brief Get the value of a given attribute. * @@ -596,21 +602,21 @@ SVCALL(SD_BLE_GATTS_VALUE_SET, uint32_t, * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. * @retval ::NRF_ERROR_INVALID_PARAM Invalid attribute offset supplied. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied on a system attribute. - * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them to a known - * value. + * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them + * to a known value. */ -SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, - sd_ble_gatts_value_get(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); +SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, sd_ble_gatts_value_get(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); /**@brief Notify or Indicate an attribute value. * - * @details This function checks for the relevant Client Characteristic Configuration descriptor value to verify that the relevant - * operation (notification or indication) has been enabled by the client. It is also able to update the attribute value before - * issuing the PDU, so that the application can atomically perform a value update and a server initiated transaction with a single - * API call. + * @details This function checks for the relevant Client Characteristic Configuration descriptor value to verify that + * the relevant operation (notification or indication) has been enabled by the client. It is also able to update the + * attribute value before issuing the PDU, so that the application can atomically perform a value update and a server + * initiated transaction with a single API call. * - * @note The local attribute value may be updated even if an outgoing packet is not sent to the peer due to an error during - * execution. The Attribute Table has been updated if one of the following error codes is returned: @ref NRF_ERROR_INVALID_STATE, + * @note The local attribute value may be updated even if an outgoing packet is not sent to the peer due to an error + * during execution. The Attribute Table has been updated if one of the following error codes is returned: @ref + * NRF_ERROR_INVALID_STATE, * @ref NRF_ERROR_BUSY, * @ref NRF_ERROR_FORBIDDEN, @ref BLE_ERROR_GATTS_SYS_ATTR_MISSING and @ref NRF_ERROR_RESOURCES. * The caller can check whether the value has been updated by looking at the contents of *(@ref @@ -622,16 +628,17 @@ SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, * A @ref BLE_GATTS_EVT_HVC event will be issued as soon as the confirmation arrives from the peer. * * @note The number of Handle Value Notifications that can be queued is configured by @ref - * ble_gatts_conn_cfg_t::hvn_tx_queue_size When the queue is full, the function call will return @ref NRF_ERROR_RESOURCES. A @ref - * BLE_GATTS_EVT_HVN_TX_COMPLETE event will be issued as soon as the transmission of the notification is complete. + * ble_gatts_conn_cfg_t::hvn_tx_queue_size When the queue is full, the function call will return @ref + * NRF_ERROR_RESOURCES. A @ref BLE_GATTS_EVT_HVN_TX_COMPLETE event will be issued as soon as the transmission of the + * notification is complete. * - * @note The application can keep track of the available queue element count for notifications by following the procedure - * below: + * @note The application can keep track of the available queue element count for notifications by following the + * procedure below: * - Store initial queue element count in a variable. - * - Decrement the variable, which stores the currently available queue element count, by one when a call to this - * function returns @ref NRF_SUCCESS. - * - Increment the variable, which stores the current available queue element count, by the count variable in @ref - * BLE_GATTS_EVT_HVN_TX_COMPLETE event. + * - Decrement the variable, which stores the currently available queue element count, by one when a call to + * this function returns @ref NRF_SUCCESS. + * - Increment the variable, which stores the current available queue element count, by the count variable in + * @ref BLE_GATTS_EVT_HVN_TX_COMPLETE event. * * @events * @event{@ref BLE_GATTS_EVT_HVN_TX_COMPLETE, Notification transmission complete.} @@ -652,8 +659,8 @@ SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, * is updated, @ref ble_gatts_hvx_params_t::p_len is updated by the SoftDevice to * contain the number of actual bytes written, else it will be set to 0. * - * @retval ::NRF_SUCCESS Successfully queued a notification or indication for transmission, and optionally updated the attribute - * value. + * @retval ::NRF_SUCCESS Successfully queued a notification or indication for transmission, and optionally updated the + * attribute value. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. * @retval ::NRF_ERROR_INVALID_STATE One or more of the following is true: * - Invalid Connection State @@ -661,18 +668,18 @@ SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, * - An ATT_MTU exchange is ongoing * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. - * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied. Only attributes added directly by the application - * are available to notify and indicate. - * @retval ::BLE_ERROR_GATTS_INVALID_ATTR_TYPE Invalid attribute type(s) supplied, only characteristic values may be notified and - * indicated. + * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied. Only attributes added directly by the + * application are available to notify and indicate. + * @retval ::BLE_ERROR_GATTS_INVALID_ATTR_TYPE Invalid attribute type(s) supplied, only characteristic values may be + * notified and indicated. * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. - * @retval ::NRF_ERROR_FORBIDDEN The connection's current security level is lower than the one required by the write permissions - * of the CCCD associated with this characteristic. + * @retval ::NRF_ERROR_FORBIDDEN The connection's current security level is lower than the one required by the write + * permissions of the CCCD associated with this characteristic. * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied. - * @retval ::NRF_ERROR_BUSY For @ref BLE_GATT_HVX_INDICATION Procedure already in progress. Wait for a @ref BLE_GATTS_EVT_HVC - * event and retry. - * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them to a known - * value. + * @retval ::NRF_ERROR_BUSY For @ref BLE_GATT_HVX_INDICATION Procedure already in progress. Wait for a @ref + * BLE_GATTS_EVT_HVC event and retry. + * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them + * to a known value. * @retval ::NRF_ERROR_RESOURCES Too many notifications queued. * Wait for a @ref BLE_GATTS_EVT_HVN_TX_COMPLETE event and retry. * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without @@ -682,9 +689,9 @@ SVCALL(SD_BLE_GATTS_HVX, uint32_t, sd_ble_gatts_hvx(uint16_t conn_handle, ble_ga /**@brief Indicate the Service Changed attribute value. * - * @details This call will send a Handle Value Indication to one or more peers connected to inform them that the Attribute - * Table layout has changed. As soon as the peer has confirmed the indication, a @ref BLE_GATTS_EVT_SC_CONFIRM event will - * be issued. + * @details This call will send a Handle Value Indication to one or more peers connected to inform them that the + * Attribute Table layout has changed. As soon as the peer has confirmed the indication, a @ref BLE_GATTS_EVT_SC_CONFIRM + * event will be issued. * * @note Some of the restrictions and limitations that apply to @ref sd_ble_gatts_hvx also apply here. * @@ -709,20 +716,20 @@ SVCALL(SD_BLE_GATTS_HVX, uint32_t, sd_ble_gatts_hvx(uint16_t conn_handle, ble_ga * - Notifications and/or indications not enabled in the CCCD * - An ATT_MTU exchange is ongoing * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. - * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied, handles must be in the range populated by the - * application. + * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied, handles must be in the range populated + * by the application. * @retval ::NRF_ERROR_BUSY Procedure already in progress. - * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them to a known - * value. + * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them + * to a known value. * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without * reestablishing the connection. */ -SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, - sd_ble_gatts_service_changed(uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle)); +SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, sd_ble_gatts_service_changed(uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle)); /**@brief Respond to a Read/Write authorization request. * - * @note This call should only be used as a response to a @ref BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST event issued to the application. + * @note This call should only be used as a response to a @ref BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST event issued to the + * application. * * @mscs * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_AUTH_MSC} @@ -741,8 +748,8 @@ SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, * to a @ref BLE_GATTS_AUTHORIZE_TYPE_READ event if @ref ble_gatts_authorize_params_t::update * is set to 0. * - * @retval ::NRF_SUCCESS Successfully queued a response to the peer, and in the case of a write operation, Attribute - * Table updated. + * @retval ::NRF_SUCCESS Successfully queued a response to the peer, and in the case of a write operation, + * Attribute Table updated. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -754,8 +761,7 @@ SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, * reestablishing the connection. */ SVCALL(SD_BLE_GATTS_RW_AUTHORIZE_REPLY, uint32_t, - sd_ble_gatts_rw_authorize_reply(uint16_t conn_handle, - ble_gatts_rw_authorize_reply_params_t const *p_rw_authorize_reply_params)); + sd_ble_gatts_rw_authorize_reply(uint16_t conn_handle, ble_gatts_rw_authorize_reply_params_t const *p_rw_authorize_reply_params)); /**@brief Update persistent system attribute information. * @@ -770,16 +776,18 @@ SVCALL(SD_BLE_GATTS_RW_AUTHORIZE_REPLY, uint32_t, * If the pointer is NULL, the system attribute info is initialized, assuming that * the application does not have any previously saved system attribute data for this device. * - * @note The state of persistent system attributes is reset upon connection establishment and then remembered for its duration. + * @note The state of persistent system attributes is reset upon connection establishment and then remembered for its + * duration. * - * @note If this call returns with an error code different from @ref NRF_SUCCESS, the storage of persistent system attributes may - * have been completed only partially. This means that the state of the attribute table is undefined, and the application should - * either provide a new set of attributes using this same call or reset the SoftDevice to return to a known state. + * @note If this call returns with an error code different from @ref NRF_SUCCESS, the storage of persistent system + * attributes may have been completed only partially. This means that the state of the attribute table is undefined, and + * the application should either provide a new set of attributes using this same call or reset the SoftDevice to return + * to a known state. * - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included in system - * services will be modified. - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included in user - * services will be modified. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included + * in system services will be modified. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included + * in user services will be modified. * * @mscs * @mmsc{@ref BLE_GATTS_HVX_SYS_ATTRS_MISSING_MSC} @@ -807,27 +815,29 @@ SVCALL(SD_BLE_GATTS_SYS_ATTR_SET, uint32_t, /**@brief Retrieve persistent system attribute information from the stack. * * @details This call is used to retrieve information about values to be stored persistently by the application - * during the lifetime of a connection or after it has been terminated. When a new connection is established with the - * same bonded device, the system attribute information retrieved with this function should be restored using using @ref - * sd_ble_gatts_sys_attr_set. If retrieved after disconnection, the data should be read before a new connection established. The - * connection handle for the previous, now disconnected, connection will remain valid until a new one is created to allow this API - * call to refer to it. Connection handles belonging to active connections can be used as well, but care should be taken since the - * system attributes may be written to at any time by the peer during a connection's lifetime. + * during the lifetime of a connection or after it has been terminated. When a new connection is established + * with the same bonded device, the system attribute information retrieved with this function should be restored using + * using @ref sd_ble_gatts_sys_attr_set. If retrieved after disconnection, the data should be read before a new + * connection established. The connection handle for the previous, now disconnected, connection will remain valid until + * a new one is created to allow this API call to refer to it. Connection handles belonging to active connections can be + * used as well, but care should be taken since the system attributes may be written to at any time by the peer during a + * connection's lifetime. * - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included in system - * services will be returned. - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included in user - * services will be returned. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included + * in system services will be returned. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included + * in user services will be returned. * * @mscs * @mmsc{@ref BLE_GATTS_SYS_ATTRS_BONDED_PEER_MSC} * @endmscs * * @param[in] conn_handle Connection handle of the recently terminated connection. - * @param[out] p_sys_attr_data Pointer to a buffer where updated information about system attributes will be filled in. The - * format of the data is described in @ref BLE_GATTS_SYS_ATTRS_FORMAT. NULL can be provided to obtain the length of the data. - * @param[in,out] p_len Size of application buffer if p_sys_attr_data is not NULL. Unconditionally updated to actual - * length of system attribute data. + * @param[out] p_sys_attr_data Pointer to a buffer where updated information about system attributes will be filled + * in. The format of the data is described in @ref BLE_GATTS_SYS_ATTRS_FORMAT. NULL can be provided to obtain the length + * of the data. + * @param[in,out] p_len Size of application buffer if p_sys_attr_data is not NULL. Unconditionally updated + * to actual length of system attribute data. * @param[in] flags Optional additional flags, see @ref BLE_GATTS_SYS_ATTR_FLAGS * * @retval ::NRF_SUCCESS Successfully retrieved the system attribute information. @@ -881,8 +891,8 @@ SVCALL(SD_BLE_GATTS_ATTR_GET, uint32_t, sd_ble_gatts_attr_get(uint16_t handle, b * - The minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. * - The maximum value is @ref ble_gatt_conn_cfg_t::att_mtu in the connection configuration * used for this connection. - * - The value must be equal to Client RX MTU size given in @ref sd_ble_gattc_exchange_mtu_request - * if an ATT_MTU exchange has already been performed in the other direction. + * - The value must be equal to Client RX MTU size given in @ref + * sd_ble_gattc_exchange_mtu_request if an ATT_MTU exchange has already been performed in the other direction. * * @retval ::NRF_SUCCESS Successfully sent response to the client. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. diff --git a/src/platform/nrf52/softdevice/ble_hci.h b/src/platform/nrf52/softdevice/ble_hci.h index 27f85d52e..2feb661be 100644 --- a/src/platform/nrf52/softdevice/ble_hci.h +++ b/src/platform/nrf52/softdevice/ble_hci.h @@ -71,8 +71,8 @@ extern "C" { 0x11 Unsupported Feature or Parameter Value*/ #define BLE_HCI_STATUS_CODE_INVALID_BTLE_COMMAND_PARAMETERS 0x12 /**< Invalid BLE Command Parameters. */ #define BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION 0x13 /**< Remote User Terminated Connection. */ -#define BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES \ - 0x14 /**< Remote Device Terminated Connection due to low \ +#define BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES \ + 0x14 /**< Remote Device Terminated Connection due to low \ resources.*/ #define BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_POWER_OFF 0x15 /**< Remote Device Terminated Connection due to power off. */ #define BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION 0x16 /**< Local Host Terminated Connection. */ diff --git a/src/platform/nrf52/softdevice/ble_l2cap.h b/src/platform/nrf52/softdevice/ble_l2cap.h index 5f4bd277d..6685dc22e 100644 --- a/src/platform/nrf52/softdevice/ble_l2cap.h +++ b/src/platform/nrf52/softdevice/ble_l2cap.h @@ -83,32 +83,32 @@ extern "C" { /**@brief L2CAP API SVC numbers. */ enum BLE_L2CAP_SVCS { - SD_BLE_L2CAP_CH_SETUP = BLE_L2CAP_SVC_BASE + 0, /**< Set up an L2CAP channel. */ - SD_BLE_L2CAP_CH_RELEASE = BLE_L2CAP_SVC_BASE + 1, /**< Release an L2CAP channel. */ - SD_BLE_L2CAP_CH_RX = BLE_L2CAP_SVC_BASE + 2, /**< Receive an SDU on an L2CAP channel. */ - SD_BLE_L2CAP_CH_TX = BLE_L2CAP_SVC_BASE + 3, /**< Transmit an SDU on an L2CAP channel. */ - SD_BLE_L2CAP_CH_FLOW_CONTROL = BLE_L2CAP_SVC_BASE + 4, /**< Advanced SDU reception flow control. */ + SD_BLE_L2CAP_CH_SETUP = BLE_L2CAP_SVC_BASE + 0, /**< Set up an L2CAP channel. */ + SD_BLE_L2CAP_CH_RELEASE = BLE_L2CAP_SVC_BASE + 1, /**< Release an L2CAP channel. */ + SD_BLE_L2CAP_CH_RX = BLE_L2CAP_SVC_BASE + 2, /**< Receive an SDU on an L2CAP channel. */ + SD_BLE_L2CAP_CH_TX = BLE_L2CAP_SVC_BASE + 3, /**< Transmit an SDU on an L2CAP channel. */ + SD_BLE_L2CAP_CH_FLOW_CONTROL = BLE_L2CAP_SVC_BASE + 4, /**< Advanced SDU reception flow control. */ }; /**@brief L2CAP Event IDs. */ enum BLE_L2CAP_EVTS { - BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event. - \n Reply with @ref sd_ble_l2cap_ch_setup. - \n See @ref ble_l2cap_evt_ch_setup_request_t. */ - BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event. - \n See @ref ble_l2cap_evt_ch_setup_refused_t. */ - BLE_L2CAP_EVT_CH_SETUP = BLE_L2CAP_EVT_BASE + 2, /**< L2CAP Channel Setup Completed event. - \n See @ref ble_l2cap_evt_ch_setup_t. */ - BLE_L2CAP_EVT_CH_RELEASED = BLE_L2CAP_EVT_BASE + 3, /**< L2CAP Channel Released event. - \n No additional event structure applies. */ - BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED = BLE_L2CAP_EVT_BASE + 4, /**< L2CAP Channel SDU data buffer released event. - \n See @ref ble_l2cap_evt_ch_sdu_buf_released_t. */ - BLE_L2CAP_EVT_CH_CREDIT = BLE_L2CAP_EVT_BASE + 5, /**< L2CAP Channel Credit received. - \n See @ref ble_l2cap_evt_ch_credit_t. */ - BLE_L2CAP_EVT_CH_RX = BLE_L2CAP_EVT_BASE + 6, /**< L2CAP Channel SDU received. - \n See @ref ble_l2cap_evt_ch_rx_t. */ - BLE_L2CAP_EVT_CH_TX = BLE_L2CAP_EVT_BASE + 7, /**< L2CAP Channel SDU transmitted. - \n See @ref ble_l2cap_evt_ch_tx_t. */ + BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event. + \n Reply with @ref sd_ble_l2cap_ch_setup. + \n See @ref ble_l2cap_evt_ch_setup_request_t. */ + BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event. + \n See @ref ble_l2cap_evt_ch_setup_refused_t. */ + BLE_L2CAP_EVT_CH_SETUP = BLE_L2CAP_EVT_BASE + 2, /**< L2CAP Channel Setup Completed event. + \n See @ref ble_l2cap_evt_ch_setup_t. */ + BLE_L2CAP_EVT_CH_RELEASED = BLE_L2CAP_EVT_BASE + 3, /**< L2CAP Channel Released event. + \n No additional event structure applies. */ + BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED = BLE_L2CAP_EVT_BASE + 4, /**< L2CAP Channel SDU data buffer released event. + \n See @ref ble_l2cap_evt_ch_sdu_buf_released_t. */ + BLE_L2CAP_EVT_CH_CREDIT = BLE_L2CAP_EVT_BASE + 5, /**< L2CAP Channel Credit received. + \n See @ref ble_l2cap_evt_ch_credit_t. */ + BLE_L2CAP_EVT_CH_RX = BLE_L2CAP_EVT_BASE + 6, /**< L2CAP Channel SDU received. + \n See @ref ble_l2cap_evt_ch_rx_t. */ + BLE_L2CAP_EVT_CH_TX = BLE_L2CAP_EVT_BASE + 7, /**< L2CAP Channel SDU transmitted. + \n See @ref ble_l2cap_evt_ch_tx_t. */ }; /** @} */ @@ -149,9 +149,8 @@ enum BLE_L2CAP_EVTS { #define BLE_L2CAP_CH_STATUS_CODE_INVALID_SCID (0x0009) /**< Invalid Source CID. */ #define BLE_L2CAP_CH_STATUS_CODE_SCID_ALLOCATED (0x000A) /**< Source CID already allocated. */ #define BLE_L2CAP_CH_STATUS_CODE_UNACCEPTABLE_PARAMS (0x000B) /**< Unacceptable parameters. */ -#define BLE_L2CAP_CH_STATUS_CODE_NOT_UNDERSTOOD \ - (0x8000) /**< Command Reject received instead of LE Credit Based Connection Response. */ -#define BLE_L2CAP_CH_STATUS_CODE_TIMEOUT (0xC000) /**< Operation timed out. */ +#define BLE_L2CAP_CH_STATUS_CODE_NOT_UNDERSTOOD (0x8000) /**< Command Reject received instead of LE Credit Based Connection Response. */ +#define BLE_L2CAP_CH_STATUS_CODE_TIMEOUT (0xC000) /**< Operation timed out. */ /** @} */ /** @} */ @@ -172,120 +171,120 @@ enum BLE_L2CAP_EVTS { * @retval ::NRF_ERROR_NO_MEM rx_mps or tx_mps is set too high. */ typedef struct { - uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall - be able to receive on L2CAP channels on connections with this - configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ - uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall - be able to transmit on L2CAP channels on connections with this - configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ - uint8_t rx_queue_size; /**< Number of SDU data buffers that can be queued for reception per - L2CAP channel. The minimum value is one. */ - uint8_t tx_queue_size; /**< Number of SDU data buffers that can be queued for transmission - per L2CAP channel. The minimum value is one. */ - uint8_t ch_count; /**< Number of L2CAP channels the application can create per connection - with this configuration. The default value is zero, the maximum - value is @ref BLE_L2CAP_CH_COUNT_MAX. - @note if this parameter is set to zero, all other parameters in - @ref ble_l2cap_conn_cfg_t are ignored. */ + uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall + be able to receive on L2CAP channels on connections with this + configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ + uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall + be able to transmit on L2CAP channels on connections with this + configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ + uint8_t rx_queue_size; /**< Number of SDU data buffers that can be queued for reception per + L2CAP channel. The minimum value is one. */ + uint8_t tx_queue_size; /**< Number of SDU data buffers that can be queued for transmission + per L2CAP channel. The minimum value is one. */ + uint8_t ch_count; /**< Number of L2CAP channels the application can create per connection + with this configuration. The default value is zero, the maximum + value is @ref BLE_L2CAP_CH_COUNT_MAX. + @note if this parameter is set to zero, all other parameters in + @ref ble_l2cap_conn_cfg_t are ignored. */ } ble_l2cap_conn_cfg_t; /**@brief L2CAP channel RX parameters. */ typedef struct { - uint16_t rx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP shall be able to - receive on this L2CAP channel. - - Must be equal to or greater than @ref BLE_L2CAP_MTU_MIN. */ - uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall be - able to receive on this L2CAP channel. - - Must be equal to or greater than @ref BLE_L2CAP_MPS_MIN. - - Must be equal to or less than @ref ble_l2cap_conn_cfg_t::rx_mps. */ - ble_data_t sdu_buf; /**< SDU data buffer for reception. - - If @ref ble_data_t::p_data is non-NULL, initial credits are - issued to the peer. - - If @ref ble_data_t::p_data is NULL, no initial credits are - issued to the peer. */ + uint16_t rx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP shall be able to + receive on this L2CAP channel. + - Must be equal to or greater than @ref BLE_L2CAP_MTU_MIN. */ + uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall be + able to receive on this L2CAP channel. + - Must be equal to or greater than @ref BLE_L2CAP_MPS_MIN. + - Must be equal to or less than @ref ble_l2cap_conn_cfg_t::rx_mps. */ + ble_data_t sdu_buf; /**< SDU data buffer for reception. + - If @ref ble_data_t::p_data is non-NULL, initial credits are + issued to the peer. + - If @ref ble_data_t::p_data is NULL, no initial credits are + issued to the peer. */ } ble_l2cap_ch_rx_params_t; /**@brief L2CAP channel setup parameters. */ typedef struct { - ble_l2cap_ch_rx_params_t rx_params; /**< L2CAP channel RX parameters. */ - uint16_t le_psm; /**< LE Protocol/Service Multiplexer. Used when requesting - setup of an L2CAP channel, ignored otherwise. */ - uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES. - Used when replying to a setup request of an L2CAP - channel, ignored otherwise. */ + ble_l2cap_ch_rx_params_t rx_params; /**< L2CAP channel RX parameters. */ + uint16_t le_psm; /**< LE Protocol/Service Multiplexer. Used when requesting + setup of an L2CAP channel, ignored otherwise. */ + uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES. + Used when replying to a setup request of an L2CAP + channel, ignored otherwise. */ } ble_l2cap_ch_setup_params_t; /**@brief L2CAP channel TX parameters. */ typedef struct { - uint16_t tx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP is able to - transmit on this L2CAP channel. */ - uint16_t peer_mps; /**< The maximum L2CAP PDU payload size, in bytes, that the peer is - able to receive on this L2CAP channel. */ - uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP is able - to transmit on this L2CAP channel. This is effective tx_mps, - selected by the SoftDevice as - MIN( @ref ble_l2cap_ch_tx_params_t::peer_mps, @ref ble_l2cap_conn_cfg_t::tx_mps ) */ - uint16_t credits; /**< Initial credits given by the peer. */ + uint16_t tx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP is able to + transmit on this L2CAP channel. */ + uint16_t peer_mps; /**< The maximum L2CAP PDU payload size, in bytes, that the peer is + able to receive on this L2CAP channel. */ + uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP is able + to transmit on this L2CAP channel. This is effective tx_mps, + selected by the SoftDevice as + MIN( @ref ble_l2cap_ch_tx_params_t::peer_mps, @ref ble_l2cap_conn_cfg_t::tx_mps ) */ + uint16_t credits; /**< Initial credits given by the peer. */ } ble_l2cap_ch_tx_params_t; /**@brief L2CAP Channel Setup Request event. */ typedef struct { - ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ - uint16_t le_psm; /**< LE Protocol/Service Multiplexer. */ + ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ + uint16_t le_psm; /**< LE Protocol/Service Multiplexer. */ } ble_l2cap_evt_ch_setup_request_t; /**@brief L2CAP Channel Setup Refused event. */ typedef struct { - uint8_t source; /**< Source, see @ref BLE_L2CAP_CH_SETUP_REFUSED_SRCS */ - uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES */ + uint8_t source; /**< Source, see @ref BLE_L2CAP_CH_SETUP_REFUSED_SRCS */ + uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES */ } ble_l2cap_evt_ch_setup_refused_t; /**@brief L2CAP Channel Setup Completed event. */ typedef struct { - ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ + ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ } ble_l2cap_evt_ch_setup_t; /**@brief L2CAP Channel SDU Data Buffer Released event. */ typedef struct { - ble_data_t sdu_buf; /**< Returned reception or transmission SDU data buffer. The SoftDevice - returns SDU data buffers supplied by the application, which have - not yet been returned previously via a @ref BLE_L2CAP_EVT_CH_RX or - @ref BLE_L2CAP_EVT_CH_TX event. */ + ble_data_t sdu_buf; /**< Returned reception or transmission SDU data buffer. The SoftDevice + returns SDU data buffers supplied by the application, which have + not yet been returned previously via a @ref BLE_L2CAP_EVT_CH_RX or + @ref BLE_L2CAP_EVT_CH_TX event. */ } ble_l2cap_evt_ch_sdu_buf_released_t; /**@brief L2CAP Channel Credit received event. */ typedef struct { - uint16_t credits; /**< Additional credits given by the peer. */ + uint16_t credits; /**< Additional credits given by the peer. */ } ble_l2cap_evt_ch_credit_t; /**@brief L2CAP Channel received SDU event. */ typedef struct { - uint16_t sdu_len; /**< Total SDU length, in bytes. */ - ble_data_t sdu_buf; /**< SDU data buffer. - @note If there is not enough space in the buffer - (sdu_buf.len < sdu_len) then the rest of the SDU will be - silently discarded by the SoftDevice. */ + uint16_t sdu_len; /**< Total SDU length, in bytes. */ + ble_data_t sdu_buf; /**< SDU data buffer. + @note If there is not enough space in the buffer + (sdu_buf.len < sdu_len) then the rest of the SDU will be + silently discarded by the SoftDevice. */ } ble_l2cap_evt_ch_rx_t; /**@brief L2CAP Channel transmitted SDU event. */ typedef struct { - ble_data_t sdu_buf; /**< SDU data buffer. */ + ble_data_t sdu_buf; /**< SDU data buffer. */ } ble_l2cap_evt_ch_tx_t; /**@brief L2CAP event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which the event occured. */ - uint16_t local_cid; /**< Local Channel ID of the L2CAP channel, or - @ref BLE_L2CAP_CID_INVALID if not present. */ - union { - ble_l2cap_evt_ch_setup_request_t ch_setup_request; /**< L2CAP Channel Setup Request Event Parameters. */ - ble_l2cap_evt_ch_setup_refused_t ch_setup_refused; /**< L2CAP Channel Setup Refused Event Parameters. */ - ble_l2cap_evt_ch_setup_t ch_setup; /**< L2CAP Channel Setup Completed Event Parameters. */ - ble_l2cap_evt_ch_sdu_buf_released_t ch_sdu_buf_released; /**< L2CAP Channel SDU Data Buffer Released Event Parameters. */ - ble_l2cap_evt_ch_credit_t credit; /**< L2CAP Channel Credit Received Event Parameters. */ - ble_l2cap_evt_ch_rx_t rx; /**< L2CAP Channel SDU Received Event Parameters. */ - ble_l2cap_evt_ch_tx_t tx; /**< L2CAP Channel SDU Transmitted Event Parameters. */ - } params; /**< Event Parameters. */ + uint16_t conn_handle; /**< Connection Handle on which the event occured. */ + uint16_t local_cid; /**< Local Channel ID of the L2CAP channel, or + @ref BLE_L2CAP_CID_INVALID if not present. */ + union { + ble_l2cap_evt_ch_setup_request_t ch_setup_request; /**< L2CAP Channel Setup Request Event Parameters. */ + ble_l2cap_evt_ch_setup_refused_t ch_setup_refused; /**< L2CAP Channel Setup Refused Event Parameters. */ + ble_l2cap_evt_ch_setup_t ch_setup; /**< L2CAP Channel Setup Completed Event Parameters. */ + ble_l2cap_evt_ch_sdu_buf_released_t ch_sdu_buf_released; /**< L2CAP Channel SDU Data Buffer Released Event Parameters. */ + ble_l2cap_evt_ch_credit_t credit; /**< L2CAP Channel Credit Received Event Parameters. */ + ble_l2cap_evt_ch_rx_t rx; /**< L2CAP Channel SDU Received Event Parameters. */ + ble_l2cap_evt_ch_tx_t tx; /**< L2CAP Channel SDU Transmitted Event Parameters. */ + } params; /**< Event Parameters. */ } ble_l2cap_evt_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/ble_types.h b/src/platform/nrf52/softdevice/ble_types.h index db3656cfd..f64503779 100644 --- a/src/platform/nrf52/softdevice/ble_types.h +++ b/src/platform/nrf52/softdevice/ble_types.h @@ -101,79 +101,77 @@ extern "C" { * @note Retrieved from * http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml * @{ */ -#define BLE_APPEARANCE_UNKNOWN 0 /**< Unknown. */ -#define BLE_APPEARANCE_GENERIC_PHONE 64 /**< Generic Phone. */ -#define BLE_APPEARANCE_GENERIC_COMPUTER 128 /**< Generic Computer. */ -#define BLE_APPEARANCE_GENERIC_WATCH 192 /**< Generic Watch. */ -#define BLE_APPEARANCE_WATCH_SPORTS_WATCH 193 /**< Watch: Sports Watch. */ -#define BLE_APPEARANCE_GENERIC_CLOCK 256 /**< Generic Clock. */ -#define BLE_APPEARANCE_GENERIC_DISPLAY 320 /**< Generic Display. */ -#define BLE_APPEARANCE_GENERIC_REMOTE_CONTROL 384 /**< Generic Remote Control. */ -#define BLE_APPEARANCE_GENERIC_EYE_GLASSES 448 /**< Generic Eye-glasses. */ -#define BLE_APPEARANCE_GENERIC_TAG 512 /**< Generic Tag. */ -#define BLE_APPEARANCE_GENERIC_KEYRING 576 /**< Generic Keyring. */ -#define BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 640 /**< Generic Media Player. */ -#define BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 704 /**< Generic Barcode Scanner. */ -#define BLE_APPEARANCE_GENERIC_THERMOMETER 768 /**< Generic Thermometer. */ -#define BLE_APPEARANCE_THERMOMETER_EAR 769 /**< Thermometer: Ear. */ -#define BLE_APPEARANCE_GENERIC_HEART_RATE_SENSOR 832 /**< Generic Heart rate Sensor. */ -#define BLE_APPEARANCE_HEART_RATE_SENSOR_HEART_RATE_BELT 833 /**< Heart Rate Sensor: Heart Rate Belt. */ -#define BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 896 /**< Generic Blood Pressure. */ -#define BLE_APPEARANCE_BLOOD_PRESSURE_ARM 897 /**< Blood Pressure: Arm. */ -#define BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 898 /**< Blood Pressure: Wrist. */ -#define BLE_APPEARANCE_GENERIC_HID 960 /**< Human Interface Device (HID). */ -#define BLE_APPEARANCE_HID_KEYBOARD 961 /**< Keyboard (HID Subtype). */ -#define BLE_APPEARANCE_HID_MOUSE 962 /**< Mouse (HID Subtype). */ -#define BLE_APPEARANCE_HID_JOYSTICK 963 /**< Joystick (HID Subtype). */ -#define BLE_APPEARANCE_HID_GAMEPAD 964 /**< Gamepad (HID Subtype). */ -#define BLE_APPEARANCE_HID_DIGITIZERSUBTYPE 965 /**< Digitizer Tablet (HID Subtype). */ -#define BLE_APPEARANCE_HID_CARD_READER 966 /**< Card Reader (HID Subtype). */ -#define BLE_APPEARANCE_HID_DIGITAL_PEN 967 /**< Digital Pen (HID Subtype). */ -#define BLE_APPEARANCE_HID_BARCODE 968 /**< Barcode Scanner (HID Subtype). */ -#define BLE_APPEARANCE_GENERIC_GLUCOSE_METER 1024 /**< Generic Glucose Meter. */ -#define BLE_APPEARANCE_GENERIC_RUNNING_WALKING_SENSOR 1088 /**< Generic Running Walking Sensor. */ -#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_IN_SHOE 1089 /**< Running Walking Sensor: In-Shoe. */ -#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_SHOE 1090 /**< Running Walking Sensor: On-Shoe. */ -#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_HIP 1091 /**< Running Walking Sensor: On-Hip. */ -#define BLE_APPEARANCE_GENERIC_CYCLING 1152 /**< Generic Cycling. */ -#define BLE_APPEARANCE_CYCLING_CYCLING_COMPUTER 1153 /**< Cycling: Cycling Computer. */ -#define BLE_APPEARANCE_CYCLING_SPEED_SENSOR 1154 /**< Cycling: Speed Sensor. */ -#define BLE_APPEARANCE_CYCLING_CADENCE_SENSOR 1155 /**< Cycling: Cadence Sensor. */ -#define BLE_APPEARANCE_CYCLING_POWER_SENSOR 1156 /**< Cycling: Power Sensor. */ -#define BLE_APPEARANCE_CYCLING_SPEED_CADENCE_SENSOR 1157 /**< Cycling: Speed and Cadence Sensor. */ -#define BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 3136 /**< Generic Pulse Oximeter. */ -#define BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 3137 /**< Fingertip (Pulse Oximeter subtype). */ -#define BLE_APPEARANCE_PULSE_OXIMETER_WRIST_WORN 3138 /**< Wrist Worn(Pulse Oximeter subtype). */ -#define BLE_APPEARANCE_GENERIC_WEIGHT_SCALE 3200 /**< Generic Weight Scale. */ -#define BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS_ACT 5184 /**< Generic Outdoor Sports Activity. */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_DISP 5185 /**< Location Display Device (Outdoor Sports Activity subtype). */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_DISP \ - 5186 /**< Location and Navigation Display Device (Outdoor Sports Activity subtype). */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_POD 5187 /**< Location Pod (Outdoor Sports Activity subtype). */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_POD \ - 5188 /**< Location and Navigation Pod (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_UNKNOWN 0 /**< Unknown. */ +#define BLE_APPEARANCE_GENERIC_PHONE 64 /**< Generic Phone. */ +#define BLE_APPEARANCE_GENERIC_COMPUTER 128 /**< Generic Computer. */ +#define BLE_APPEARANCE_GENERIC_WATCH 192 /**< Generic Watch. */ +#define BLE_APPEARANCE_WATCH_SPORTS_WATCH 193 /**< Watch: Sports Watch. */ +#define BLE_APPEARANCE_GENERIC_CLOCK 256 /**< Generic Clock. */ +#define BLE_APPEARANCE_GENERIC_DISPLAY 320 /**< Generic Display. */ +#define BLE_APPEARANCE_GENERIC_REMOTE_CONTROL 384 /**< Generic Remote Control. */ +#define BLE_APPEARANCE_GENERIC_EYE_GLASSES 448 /**< Generic Eye-glasses. */ +#define BLE_APPEARANCE_GENERIC_TAG 512 /**< Generic Tag. */ +#define BLE_APPEARANCE_GENERIC_KEYRING 576 /**< Generic Keyring. */ +#define BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 640 /**< Generic Media Player. */ +#define BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 704 /**< Generic Barcode Scanner. */ +#define BLE_APPEARANCE_GENERIC_THERMOMETER 768 /**< Generic Thermometer. */ +#define BLE_APPEARANCE_THERMOMETER_EAR 769 /**< Thermometer: Ear. */ +#define BLE_APPEARANCE_GENERIC_HEART_RATE_SENSOR 832 /**< Generic Heart rate Sensor. */ +#define BLE_APPEARANCE_HEART_RATE_SENSOR_HEART_RATE_BELT 833 /**< Heart Rate Sensor: Heart Rate Belt. */ +#define BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 896 /**< Generic Blood Pressure. */ +#define BLE_APPEARANCE_BLOOD_PRESSURE_ARM 897 /**< Blood Pressure: Arm. */ +#define BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 898 /**< Blood Pressure: Wrist. */ +#define BLE_APPEARANCE_GENERIC_HID 960 /**< Human Interface Device (HID). */ +#define BLE_APPEARANCE_HID_KEYBOARD 961 /**< Keyboard (HID Subtype). */ +#define BLE_APPEARANCE_HID_MOUSE 962 /**< Mouse (HID Subtype). */ +#define BLE_APPEARANCE_HID_JOYSTICK 963 /**< Joystick (HID Subtype). */ +#define BLE_APPEARANCE_HID_GAMEPAD 964 /**< Gamepad (HID Subtype). */ +#define BLE_APPEARANCE_HID_DIGITIZERSUBTYPE 965 /**< Digitizer Tablet (HID Subtype). */ +#define BLE_APPEARANCE_HID_CARD_READER 966 /**< Card Reader (HID Subtype). */ +#define BLE_APPEARANCE_HID_DIGITAL_PEN 967 /**< Digital Pen (HID Subtype). */ +#define BLE_APPEARANCE_HID_BARCODE 968 /**< Barcode Scanner (HID Subtype). */ +#define BLE_APPEARANCE_GENERIC_GLUCOSE_METER 1024 /**< Generic Glucose Meter. */ +#define BLE_APPEARANCE_GENERIC_RUNNING_WALKING_SENSOR 1088 /**< Generic Running Walking Sensor. */ +#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_IN_SHOE 1089 /**< Running Walking Sensor: In-Shoe. */ +#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_SHOE 1090 /**< Running Walking Sensor: On-Shoe. */ +#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_HIP 1091 /**< Running Walking Sensor: On-Hip. */ +#define BLE_APPEARANCE_GENERIC_CYCLING 1152 /**< Generic Cycling. */ +#define BLE_APPEARANCE_CYCLING_CYCLING_COMPUTER 1153 /**< Cycling: Cycling Computer. */ +#define BLE_APPEARANCE_CYCLING_SPEED_SENSOR 1154 /**< Cycling: Speed Sensor. */ +#define BLE_APPEARANCE_CYCLING_CADENCE_SENSOR 1155 /**< Cycling: Cadence Sensor. */ +#define BLE_APPEARANCE_CYCLING_POWER_SENSOR 1156 /**< Cycling: Power Sensor. */ +#define BLE_APPEARANCE_CYCLING_SPEED_CADENCE_SENSOR 1157 /**< Cycling: Speed and Cadence Sensor. */ +#define BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 3136 /**< Generic Pulse Oximeter. */ +#define BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 3137 /**< Fingertip (Pulse Oximeter subtype). */ +#define BLE_APPEARANCE_PULSE_OXIMETER_WRIST_WORN 3138 /**< Wrist Worn(Pulse Oximeter subtype). */ +#define BLE_APPEARANCE_GENERIC_WEIGHT_SCALE 3200 /**< Generic Weight Scale. */ +#define BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS_ACT 5184 /**< Generic Outdoor Sports Activity. */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_DISP 5185 /**< Location Display Device (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_DISP 5186 /**< Location and Navigation Display Device (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_POD 5187 /**< Location Pod (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_POD 5188 /**< Location and Navigation Pod (Outdoor Sports Activity subtype). */ /** @} */ /** @brief Set .type and .uuid fields of ble_uuid_struct to specified UUID value. */ -#define BLE_UUID_BLE_ASSIGN(instance, value) \ - do { \ - instance.type = BLE_UUID_TYPE_BLE; \ - instance.uuid = value; \ - } while (0) +#define BLE_UUID_BLE_ASSIGN(instance, value) \ + do { \ + instance.type = BLE_UUID_TYPE_BLE; \ + instance.uuid = value; \ + } while (0) /** @brief Copy type and uuid members from src to dst ble_uuid_t pointer. Both pointers must be valid/non-null. */ -#define BLE_UUID_COPY_PTR(dst, src) \ - do { \ - (dst)->type = (src)->type; \ - (dst)->uuid = (src)->uuid; \ - } while (0) +#define BLE_UUID_COPY_PTR(dst, src) \ + do { \ + (dst)->type = (src)->type; \ + (dst)->uuid = (src)->uuid; \ + } while (0) /** @brief Copy type and uuid members from src to dst ble_uuid_t struct. */ -#define BLE_UUID_COPY_INST(dst, src) \ - do { \ - (dst).type = (src).type; \ - (dst).uuid = (src).uuid; \ - } while (0) +#define BLE_UUID_COPY_INST(dst, src) \ + do { \ + (dst).type = (src).type; \ + (dst).uuid = (src).uuid; \ + } while (0) /** @brief Compare for equality both type and uuid members of two (valid, non-null) ble_uuid_t pointers. */ #define BLE_UUID_EQ(p_uuid1, p_uuid2) (((p_uuid1)->type == (p_uuid2)->type) && ((p_uuid1)->uuid == (p_uuid2)->uuid)) @@ -188,20 +186,20 @@ extern "C" { /** @brief 128 bit UUID values. */ typedef struct { - uint8_t uuid128[16]; /**< Little-Endian UUID bytes. */ + uint8_t uuid128[16]; /**< Little-Endian UUID bytes. */ } ble_uuid128_t; /** @brief Bluetooth Low Energy UUID type, encapsulates both 16-bit and 128-bit UUIDs. */ typedef struct { - uint16_t uuid; /**< 16-bit UUID value or octets 12-13 of 128-bit UUID. */ - uint8_t - type; /**< UUID type, see @ref BLE_UUID_TYPES. If type is @ref BLE_UUID_TYPE_UNKNOWN, the value of uuid is undefined. */ + uint16_t uuid; /**< 16-bit UUID value or octets 12-13 of 128-bit UUID. */ + uint8_t type; /**< UUID type, see @ref BLE_UUID_TYPES. If type is @ref BLE_UUID_TYPE_UNKNOWN, the value of uuid is + undefined. */ } ble_uuid_t; /**@brief Data structure. */ typedef struct { - uint8_t *p_data; /**< Pointer to the data buffer provided to/from the application. */ - uint16_t len; /**< Length of the data buffer, in bytes. */ + uint8_t *p_data; /**< Pointer to the data buffer provided to/from the application. */ + uint16_t len; /**< Length of the data buffer, in bytes. */ } ble_data_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h b/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h index 4e0bd752a..baf7f7189 100644 --- a/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h +++ b/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h @@ -86,21 +86,21 @@ This is the offset where the first byte of the SoftDevice hex file is written. * /**@brief nRF Master Boot Record API SVC numbers. */ enum NRF_MBR_SVCS { - SD_MBR_COMMAND = MBR_SVC_BASE, /**< ::sd_mbr_command */ + SD_MBR_COMMAND = MBR_SVC_BASE, /**< ::sd_mbr_command */ }; /**@brief Possible values for ::sd_mbr_command_t.command */ enum NRF_MBR_COMMANDS { - SD_MBR_COMMAND_COPY_BL, /**< Copy a new BootLoader. @see ::sd_mbr_command_copy_bl_t*/ - SD_MBR_COMMAND_COPY_SD, /**< Copy a new SoftDevice. @see ::sd_mbr_command_copy_sd_t*/ - SD_MBR_COMMAND_INIT_SD, /**< Initialize forwarding interrupts to SD, and run reset function in SD. Does not require any - parameters in ::sd_mbr_command_t params.*/ - SD_MBR_COMMAND_COMPARE, /**< This command works like memcmp. @see ::sd_mbr_command_compare_t*/ - SD_MBR_COMMAND_VECTOR_TABLE_BASE_SET, /**< Change the address the MBR starts after a reset. @see - ::sd_mbr_command_vector_table_base_set_t*/ - SD_MBR_COMMAND_RESERVED, - SD_MBR_COMMAND_IRQ_FORWARD_ADDRESS_SET, /**< Start forwarding all interrupts to this address. @see - ::sd_mbr_command_irq_forward_address_set_t*/ + SD_MBR_COMMAND_COPY_BL, /**< Copy a new BootLoader. @see ::sd_mbr_command_copy_bl_t*/ + SD_MBR_COMMAND_COPY_SD, /**< Copy a new SoftDevice. @see ::sd_mbr_command_copy_sd_t*/ + SD_MBR_COMMAND_INIT_SD, /**< Initialize forwarding interrupts to SD, and run reset function in SD. Does not require + any parameters in ::sd_mbr_command_t params.*/ + SD_MBR_COMMAND_COMPARE, /**< This command works like memcmp. @see ::sd_mbr_command_compare_t*/ + SD_MBR_COMMAND_VECTOR_TABLE_BASE_SET, /**< Change the address the MBR starts after a reset. @see + ::sd_mbr_command_vector_table_base_set_t*/ + SD_MBR_COMMAND_RESERVED, + SD_MBR_COMMAND_IRQ_FORWARD_ADDRESS_SET, /**< Start forwarding all interrupts to this address. @see + ::sd_mbr_command_irq_forward_address_set_t*/ }; /** @} */ @@ -117,12 +117,13 @@ enum NRF_MBR_COMMANDS { * The user of this function is responsible for setting the BPROT registers. * * @retval ::NRF_SUCCESS indicates that the contents of the memory blocks where copied correctly. - * @retval ::NRF_ERROR_INTERNAL indicates that the contents of the memory blocks where not verified correctly after copying. + * @retval ::NRF_ERROR_INTERNAL indicates that the contents of the memory blocks where not verified correctly after + * copying. */ typedef struct { - uint32_t *src; /**< Pointer to the source of data to be copied.*/ - uint32_t *dst; /**< Pointer to the destination where the content is to be copied.*/ - uint32_t len; /**< Number of 32 bit words to copy. Must be a multiple of @ref MBR_PAGE_SIZE_IN_WORDS words.*/ + uint32_t *src; /**< Pointer to the source of data to be copied.*/ + uint32_t *dst; /**< Pointer to the destination where the content is to be copied.*/ + uint32_t len; /**< Number of 32 bit words to copy. Must be a multiple of @ref MBR_PAGE_SIZE_IN_WORDS words.*/ } sd_mbr_command_copy_sd_t; /**@brief This command works like memcmp, but takes the length in words. @@ -131,9 +132,9 @@ typedef struct { * @retval ::NRF_ERROR_NULL indicates that the contents of the memory blocks are not equal. */ typedef struct { - uint32_t *ptr1; /**< Pointer to block of memory. */ - uint32_t *ptr2; /**< Pointer to block of memory. */ - uint32_t len; /**< Number of 32 bit words to compare.*/ + uint32_t *ptr1; /**< Pointer to block of memory. */ + uint32_t *ptr2; /**< Pointer to block of memory. */ + uint32_t len; /**< Number of 32 bit words to compare.*/ } sd_mbr_command_compare_t; /**@brief This command copies a new BootLoader. @@ -159,8 +160,8 @@ typedef struct { * @retval ::NRF_ERROR_NO_MEM No MBR parameter page is provided. See @ref sd_mbr_command. */ typedef struct { - uint32_t *bl_src; /**< Pointer to the source of the bootloader to be be copied.*/ - uint32_t bl_len; /**< Number of 32 bit words to copy for BootLoader. */ + uint32_t *bl_src; /**< Pointer to the source of the bootloader to be be copied.*/ + uint32_t bl_len; /**< Number of 32 bit words to copy for BootLoader. */ } sd_mbr_command_copy_bl_t; /**@brief Change the address the MBR starts after a reset @@ -186,7 +187,7 @@ typedef struct { * @retval ::NRF_ERROR_NO_MEM No MBR parameter page is provided. See @ref sd_mbr_command. */ typedef struct { - uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ + uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ } sd_mbr_command_vector_table_base_set_t; /**@brief Sets the base address of the interrupt vector table for interrupts forwarded from the MBR @@ -197,7 +198,7 @@ typedef struct { * @retval ::NRF_SUCCESS */ typedef struct { - uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ + uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ } sd_mbr_command_irq_forward_address_set_t; /**@brief Input structure containing data used when calling ::sd_mbr_command @@ -207,14 +208,14 @@ typedef struct { * @ref SD_MBR_COMMAND_INIT_SD is set, it is not necessary to set any values under params. */ typedef struct { - uint32_t command; /**< Type of command to be issued. See @ref NRF_MBR_COMMANDS. */ - union { - sd_mbr_command_copy_sd_t copy_sd; /**< Parameters for copy SoftDevice.*/ - sd_mbr_command_compare_t compare; /**< Parameters for verify.*/ - sd_mbr_command_copy_bl_t copy_bl; /**< Parameters for copy BootLoader. Requires parameter page. */ - sd_mbr_command_vector_table_base_set_t base_set; /**< Parameters for vector table base set. Requires parameter page.*/ - sd_mbr_command_irq_forward_address_set_t irq_forward_address_set; /**< Parameters for irq forward address set*/ - } params; /**< Command parameters. */ + uint32_t command; /**< Type of command to be issued. See @ref NRF_MBR_COMMANDS. */ + union { + sd_mbr_command_copy_sd_t copy_sd; /**< Parameters for copy SoftDevice.*/ + sd_mbr_command_compare_t compare; /**< Parameters for verify.*/ + sd_mbr_command_copy_bl_t copy_bl; /**< Parameters for copy BootLoader. Requires parameter page. */ + sd_mbr_command_vector_table_base_set_t base_set; /**< Parameters for vector table base set. Requires parameter page.*/ + sd_mbr_command_irq_forward_address_set_t irq_forward_address_set; /**< Parameters for irq forward address set*/ + } params; /**< Command parameters. */ } sd_mbr_command_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/nrf_error_sdm.h b/src/platform/nrf52/softdevice/nrf_error_sdm.h index 2fd621057..a074b2cc0 100644 --- a/src/platform/nrf52/softdevice/nrf_error_sdm.h +++ b/src/platform/nrf52/softdevice/nrf_error_sdm.h @@ -56,11 +56,10 @@ extern "C" { #endif #define NRF_ERROR_SDM_LFCLK_SOURCE_UNKNOWN (NRF_ERROR_SDM_BASE_NUM + 0) ///< Unknown LFCLK source. -#define NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION \ - (NRF_ERROR_SDM_BASE_NUM + 1) ///< Incorrect interrupt configuration (can be caused by using illegal priority levels, or having - ///< enabled SoftDevice interrupts). -#define NRF_ERROR_SDM_INCORRECT_CLENR0 \ - (NRF_ERROR_SDM_BASE_NUM + 2) ///< Incorrect CLENR0 (can be caused by erroneous SoftDevice flashing). +#define NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION \ + (NRF_ERROR_SDM_BASE_NUM + 1) ///< Incorrect interrupt configuration (can be caused by using illegal priority levels, + ///< or having enabled SoftDevice interrupts). +#define NRF_ERROR_SDM_INCORRECT_CLENR0 (NRF_ERROR_SDM_BASE_NUM + 2) ///< Incorrect CLENR0 (can be caused by erroneous SoftDevice flashing). #ifdef __cplusplus } diff --git a/src/platform/nrf52/softdevice/nrf_nvic.h b/src/platform/nrf52/softdevice/nrf_nvic.h index d4ab204d9..ec6557e0e 100644 --- a/src/platform/nrf52/softdevice/nrf_nvic.h +++ b/src/platform/nrf52/softdevice/nrf_nvic.h @@ -71,27 +71,26 @@ extern "C" { /**@defgroup NRF_NVIC_ISER_DEFINES SoftDevice NVIC internal definitions * @{ */ -#define __NRF_NVIC_NVMC_IRQn \ - (30) /**< The peripheral ID of the NVMC. IRQ numbers are used to identify peripherals, but the NVMC doesn't have an IRQ \ - number in the MDK. */ +#define __NRF_NVIC_NVMC_IRQn \ + (30) /**< The peripheral ID of the NVMC. IRQ numbers are used to identify peripherals, but the NVMC doesn't have an \ + IRQ number in the MDK. */ #define __NRF_NVIC_ISER_COUNT (2) /**< The number of ISER/ICER registers in the NVIC that are used. */ /**@brief Interrupt priority levels used by the SoftDevice. */ -#define __NRF_NVIC_SD_IRQ_PRIOS \ - ((uint8_t)((1U << 0) /**< Priority level high .*/ \ - | (1U << 1) /**< Priority level medium. */ \ - | (1U << 4) /**< Priority level low. */ \ - )) +#define __NRF_NVIC_SD_IRQ_PRIOS \ + ((uint8_t)((1U << 0) /**< Priority level high .*/ \ + | (1U << 1) /**< Priority level medium. */ \ + | (1U << 4) /**< Priority level low. */ \ + )) /**@brief Interrupt priority levels available to the application. */ #define __NRF_NVIC_APP_IRQ_PRIOS ((uint8_t)~__NRF_NVIC_SD_IRQ_PRIOS) /**@brief Interrupts used by the SoftDevice, with IRQn in the range 0-31. */ -#define __NRF_NVIC_SD_IRQS_0 \ - ((uint32_t)((1U << POWER_CLOCK_IRQn) | (1U << RADIO_IRQn) | (1U << RTC0_IRQn) | (1U << TIMER0_IRQn) | (1U << RNG_IRQn) | \ - (1U << ECB_IRQn) | (1U << CCM_AAR_IRQn) | (1U << TEMP_IRQn) | (1U << __NRF_NVIC_NVMC_IRQn) | \ - (1U << (uint32_t)SWI5_IRQn))) +#define __NRF_NVIC_SD_IRQS_0 \ + ((uint32_t)((1U << POWER_CLOCK_IRQn) | (1U << RADIO_IRQn) | (1U << RTC0_IRQn) | (1U << TIMER0_IRQn) | (1U << RNG_IRQn) | (1U << ECB_IRQn) | \ + (1U << CCM_AAR_IRQn) | (1U << TEMP_IRQn) | (1U << __NRF_NVIC_NVMC_IRQn) | (1U << (uint32_t)SWI5_IRQn))) /**@brief Interrupts used by the SoftDevice, with IRQn in the range 32-63. */ #define __NRF_NVIC_SD_IRQS_1 ((uint32_t)0) @@ -111,8 +110,8 @@ extern "C" { /**@brief Type representing the state struct for the SoftDevice NVIC module. */ typedef struct { - uint32_t volatile __irq_masks[__NRF_NVIC_ISER_COUNT]; /**< IRQs enabled by the application in the NVIC. */ - uint32_t volatile __cr_flag; /**< Non-zero if already in a critical region */ + uint32_t volatile __irq_masks[__NRF_NVIC_ISER_COUNT]; /**< IRQs enabled by the application in the NVIC. */ + uint32_t volatile __cr_flag; /**< Non-zero if already in a critical region */ } nrf_nvic_state_t; /**@brief Variable keeping the state for the SoftDevice NVIC module. This must be declared in an @@ -162,7 +161,8 @@ __STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority) * * @retval ::NRF_SUCCESS The interrupt was enabled. * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE The interrupt is not available for the application. - * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt has a priority not available for the application. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt has a priority not available for the + * application. */ __STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn); @@ -226,7 +226,8 @@ __STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn); * * @retval ::NRF_SUCCESS The interrupt and priority level is available for the application. * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE IRQn is not available for the application. - * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt priority is not available for the application. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt priority is not available for the + * application. */ __STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority); @@ -253,8 +254,8 @@ __STATIC_INLINE uint32_t sd_nvic_SystemReset(void); /**@brief Enter critical region. * * @post Application interrupts will be disabled. - * @note sd_nvic_critical_region_enter() and ::sd_nvic_critical_region_exit() must be called in matching pairs inside each - * execution context + * @note sd_nvic_critical_region_enter() and ::sd_nvic_critical_region_exit() must be called in matching pairs inside + * each execution context * @sa sd_nvic_critical_region_exit * * @param[out] p_is_nested_critical_region If 1, the application is now in a nested critical region. @@ -280,162 +281,145 @@ __STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical #ifndef SUPPRESS_INLINE_IMPLEMENTATION -__STATIC_INLINE int __sd_nvic_irq_disable(void) -{ - int pm = __get_PRIMASK(); - __disable_irq(); - return pm; +__STATIC_INLINE int __sd_nvic_irq_disable(void) { + int pm = __get_PRIMASK(); + __disable_irq(); + return pm; } -__STATIC_INLINE void __sd_nvic_irq_enable(void) -{ - __enable_irq(); -} +__STATIC_INLINE void __sd_nvic_irq_enable(void) { __enable_irq(); } -__STATIC_INLINE uint32_t __sd_nvic_app_accessible_irq(IRQn_Type IRQn) -{ - if (IRQn < 32) { - return ((1UL << IRQn) & __NRF_NVIC_APP_IRQS_0) != 0; - } else if (IRQn < 64) { - return ((1UL << (IRQn - 32)) & __NRF_NVIC_APP_IRQS_1) != 0; - } else { - return 1; - } -} - -__STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority) -{ - if ((priority >= (1 << __NVIC_PRIO_BITS)) || (((1 << priority) & __NRF_NVIC_APP_IRQ_PRIOS) == 0)) { - return 0; - } +__STATIC_INLINE uint32_t __sd_nvic_app_accessible_irq(IRQn_Type IRQn) { + if (IRQn < 32) { + return ((1UL << IRQn) & __NRF_NVIC_APP_IRQS_0) != 0; + } else if (IRQn < 64) { + return ((1UL << (IRQn - 32)) & __NRF_NVIC_APP_IRQS_1) != 0; + } else { return 1; + } } -__STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn) -{ - if (!__sd_nvic_app_accessible_irq(IRQn)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } - if (!__sd_nvic_is_app_accessible_priority(NVIC_GetPriority(IRQn))) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; - } +__STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority) { + if ((priority >= (1 << __NVIC_PRIO_BITS)) || (((1 << priority) & __NRF_NVIC_APP_IRQ_PRIOS) == 0)) { + return 0; + } + return 1; +} - if (nrf_nvic_state.__cr_flag) { - nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] |= - (uint32_t)(1 << ((uint32_t)((int32_t)IRQn) & (uint32_t)0x1F)); - } else { - NVIC_EnableIRQ(IRQn); - } +__STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn) { + if (!__sd_nvic_app_accessible_irq(IRQn)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } + if (!__sd_nvic_is_app_accessible_priority(NVIC_GetPriority(IRQn))) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; + } + + if (nrf_nvic_state.__cr_flag) { + nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] |= (uint32_t)(1 << ((uint32_t)((int32_t)IRQn) & (uint32_t)0x1F)); + } else { + NVIC_EnableIRQ(IRQn); + } + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_DisableIRQ(IRQn_Type IRQn) { + if (!__sd_nvic_app_accessible_irq(IRQn)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } + + if (nrf_nvic_state.__cr_flag) { + nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] &= ~(1UL << ((uint32_t)(IRQn)&0x1F)); + } else { + NVIC_DisableIRQ(IRQn); + } + + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_GetPendingIRQ(IRQn_Type IRQn, uint32_t *p_pending_irq) { + if (__sd_nvic_app_accessible_irq(IRQn)) { + *p_pending_irq = NVIC_GetPendingIRQ(IRQn); return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } } -__STATIC_INLINE uint32_t sd_nvic_DisableIRQ(IRQn_Type IRQn) -{ - if (!__sd_nvic_app_accessible_irq(IRQn)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } - - if (nrf_nvic_state.__cr_flag) { - nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] &= ~(1UL << ((uint32_t)(IRQn)&0x1F)); - } else { - NVIC_DisableIRQ(IRQn); - } - +__STATIC_INLINE uint32_t sd_nvic_SetPendingIRQ(IRQn_Type IRQn) { + if (__sd_nvic_app_accessible_irq(IRQn)) { + NVIC_SetPendingIRQ(IRQn); return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } } -__STATIC_INLINE uint32_t sd_nvic_GetPendingIRQ(IRQn_Type IRQn, uint32_t *p_pending_irq) -{ - if (__sd_nvic_app_accessible_irq(IRQn)) { - *p_pending_irq = NVIC_GetPendingIRQ(IRQn); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_SetPendingIRQ(IRQn_Type IRQn) -{ - if (__sd_nvic_app_accessible_irq(IRQn)) { - NVIC_SetPendingIRQ(IRQn); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn) -{ - if (__sd_nvic_app_accessible_irq(IRQn)) { - NVIC_ClearPendingIRQ(IRQn); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority) -{ - if (!__sd_nvic_app_accessible_irq(IRQn)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } - - if (!__sd_nvic_is_app_accessible_priority(priority)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; - } - - NVIC_SetPriority(IRQn, (uint32_t)priority); +__STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn) { + if (__sd_nvic_app_accessible_irq(IRQn)) { + NVIC_ClearPendingIRQ(IRQn); return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } } -__STATIC_INLINE uint32_t sd_nvic_GetPriority(IRQn_Type IRQn, uint32_t *p_priority) -{ - if (__sd_nvic_app_accessible_irq(IRQn)) { - *p_priority = (NVIC_GetPriority(IRQn) & 0xFF); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } +__STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority) { + if (!__sd_nvic_app_accessible_irq(IRQn)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } + + if (!__sd_nvic_is_app_accessible_priority(priority)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; + } + + NVIC_SetPriority(IRQn, (uint32_t)priority); + return NRF_SUCCESS; } -__STATIC_INLINE uint32_t sd_nvic_SystemReset(void) -{ - NVIC_SystemReset(); - return NRF_ERROR_SOC_NVIC_SHOULD_NOT_RETURN; +__STATIC_INLINE uint32_t sd_nvic_GetPriority(IRQn_Type IRQn, uint32_t *p_priority) { + if (__sd_nvic_app_accessible_irq(IRQn)) { + *p_priority = (NVIC_GetPriority(IRQn) & 0xFF); + return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } } -__STATIC_INLINE uint32_t sd_nvic_critical_region_enter(uint8_t *p_is_nested_critical_region) -{ +__STATIC_INLINE uint32_t sd_nvic_SystemReset(void) { + NVIC_SystemReset(); + return NRF_ERROR_SOC_NVIC_SHOULD_NOT_RETURN; +} + +__STATIC_INLINE uint32_t sd_nvic_critical_region_enter(uint8_t *p_is_nested_critical_region) { + int was_masked = __sd_nvic_irq_disable(); + if (!nrf_nvic_state.__cr_flag) { + nrf_nvic_state.__cr_flag = 1; + nrf_nvic_state.__irq_masks[0] = (NVIC->ICER[0] & __NRF_NVIC_APP_IRQS_0); + NVIC->ICER[0] = __NRF_NVIC_APP_IRQS_0; + nrf_nvic_state.__irq_masks[1] = (NVIC->ICER[1] & __NRF_NVIC_APP_IRQS_1); + NVIC->ICER[1] = __NRF_NVIC_APP_IRQS_1; + *p_is_nested_critical_region = 0; + } else { + *p_is_nested_critical_region = 1; + } + if (!was_masked) { + __sd_nvic_irq_enable(); + } + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical_region) { + if (nrf_nvic_state.__cr_flag && (is_nested_critical_region == 0)) { int was_masked = __sd_nvic_irq_disable(); - if (!nrf_nvic_state.__cr_flag) { - nrf_nvic_state.__cr_flag = 1; - nrf_nvic_state.__irq_masks[0] = (NVIC->ICER[0] & __NRF_NVIC_APP_IRQS_0); - NVIC->ICER[0] = __NRF_NVIC_APP_IRQS_0; - nrf_nvic_state.__irq_masks[1] = (NVIC->ICER[1] & __NRF_NVIC_APP_IRQS_1); - NVIC->ICER[1] = __NRF_NVIC_APP_IRQS_1; - *p_is_nested_critical_region = 0; - } else { - *p_is_nested_critical_region = 1; - } + NVIC->ISER[0] = nrf_nvic_state.__irq_masks[0]; + NVIC->ISER[1] = nrf_nvic_state.__irq_masks[1]; + nrf_nvic_state.__cr_flag = 0; if (!was_masked) { - __sd_nvic_irq_enable(); + __sd_nvic_irq_enable(); } - return NRF_SUCCESS; -} + } -__STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical_region) -{ - if (nrf_nvic_state.__cr_flag && (is_nested_critical_region == 0)) { - int was_masked = __sd_nvic_irq_disable(); - NVIC->ISER[0] = nrf_nvic_state.__irq_masks[0]; - NVIC->ISER[1] = nrf_nvic_state.__irq_masks[1]; - nrf_nvic_state.__cr_flag = 0; - if (!was_masked) { - __sd_nvic_irq_enable(); - } - } - - return NRF_SUCCESS; + return NRF_SUCCESS; } #endif /* SUPPRESS_INLINE_IMPLEMENTATION */ diff --git a/src/platform/nrf52/softdevice/nrf_sdm.h b/src/platform/nrf52/softdevice/nrf_sdm.h index 02bf135b4..369141063 100644 --- a/src/platform/nrf52/softdevice/nrf_sdm.h +++ b/src/platform/nrf52/softdevice/nrf_sdm.h @@ -151,26 +151,23 @@ the start of the SoftDevice (without MBR)*/ /** @brief Defines a macro for retrieving the actual SoftDevice ID from a given base address. Use * @ref MBR_SIZE as the argument when the SoftDevice is installed just above the MBR (the * usual case). */ -#define SD_ID_GET(baseaddr) \ - ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) \ - ? (*((uint32_t *)((baseaddr) + SD_ID_OFFSET))) \ - : SDM_INFO_FIELD_INVALID) +#define SD_ID_GET(baseaddr) \ + ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) ? (*((uint32_t *)((baseaddr) + SD_ID_OFFSET))) \ + : SDM_INFO_FIELD_INVALID) /** @brief Defines a macro for retrieving the actual SoftDevice version from a given base address. * Use @ref MBR_SIZE as the argument when the SoftDevice is installed just above the MBR * (the usual case). */ -#define SD_VERSION_GET(baseaddr) \ - ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) \ - ? (*((uint32_t *)((baseaddr) + SD_VERSION_OFFSET))) \ - : SDM_INFO_FIELD_INVALID) +#define SD_VERSION_GET(baseaddr) \ + ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) ? (*((uint32_t *)((baseaddr) + SD_VERSION_OFFSET))) \ + : SDM_INFO_FIELD_INVALID) /** @brief Defines a macro for retrieving the address of SoftDevice unique str based on a given base address. * Use @ref MBR_SIZE as the argument when the SoftDevice is installed just above the MBR * (the usual case). */ -#define SD_UNIQUE_STR_ADDR_GET(baseaddr) \ - ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_UNIQUE_STR_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) \ - ? (((uint8_t *)((baseaddr) + SD_UNIQUE_STR_OFFSET))) \ - : SDM_INFO_FIELD_INVALID) +#define SD_UNIQUE_STR_ADDR_GET(baseaddr) \ + ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_UNIQUE_STR_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) ? (((uint8_t *)((baseaddr) + SD_UNIQUE_STR_OFFSET))) \ + : SDM_INFO_FIELD_INVALID) /**@defgroup NRF_FAULT_ID_RANGES Fault ID ranges * @{ */ @@ -180,14 +177,13 @@ the start of the SoftDevice (without MBR)*/ /**@defgroup NRF_FAULT_IDS Fault ID types * @{ */ -#define NRF_FAULT_ID_SD_ASSERT \ - (NRF_FAULT_ID_SD_RANGE_START + 1) /**< SoftDevice assertion. The info parameter is reserved for future used. */ -#define NRF_FAULT_ID_APP_MEMACC \ - (NRF_FAULT_ID_APP_RANGE_START + 1) /**< Application invalid memory access. The info parameter will contain 0x00000000, \ - in case of SoftDevice RAM access violation. In case of SoftDevice peripheral \ - register violation the info parameter will contain the sub-region number of \ - PREGION[0], on whose address range the disallowed write access caused the \ - memory access fault. */ +#define NRF_FAULT_ID_SD_ASSERT (NRF_FAULT_ID_SD_RANGE_START + 1) /**< SoftDevice assertion. The info parameter is reserved for future used. */ +#define NRF_FAULT_ID_APP_MEMACC \ + (NRF_FAULT_ID_APP_RANGE_START + 1) /**< Application invalid memory access. The info parameter will contain \ + 0x00000000, in case of SoftDevice RAM access violation. In case of SoftDevice \ + peripheral register violation the info parameter will contain the sub-region \ + number of PREGION[0], on whose address range the disallowed write access \ + caused the memory access fault. */ /**@} */ /** @} */ @@ -197,11 +193,11 @@ the start of the SoftDevice (without MBR)*/ /**@brief nRF SoftDevice Manager API SVC numbers. */ enum NRF_SD_SVCS { - SD_SOFTDEVICE_ENABLE = SDM_SVC_BASE, /**< ::sd_softdevice_enable */ - SD_SOFTDEVICE_DISABLE, /**< ::sd_softdevice_disable */ - SD_SOFTDEVICE_IS_ENABLED, /**< ::sd_softdevice_is_enabled */ - SD_SOFTDEVICE_VECTOR_TABLE_BASE_SET, /**< ::sd_softdevice_vector_table_base_set */ - SVC_SDM_LAST /**< Placeholder for last SDM SVC */ + SD_SOFTDEVICE_ENABLE = SDM_SVC_BASE, /**< ::sd_softdevice_enable */ + SD_SOFTDEVICE_DISABLE, /**< ::sd_softdevice_disable */ + SD_SOFTDEVICE_IS_ENABLED, /**< ::sd_softdevice_is_enabled */ + SD_SOFTDEVICE_VECTOR_TABLE_BASE_SET, /**< ::sd_softdevice_vector_table_base_set */ + SVC_SDM_LAST /**< Placeholder for last SDM SVC */ }; /** @} */ @@ -243,34 +239,34 @@ enum NRF_SD_SVCS { /**@brief Type representing LFCLK oscillator source. */ typedef struct { - uint8_t source; /**< LF oscillator clock source, see @ref NRF_CLOCK_LF_SRC. */ - uint8_t rc_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: Calibration timer interval in 1/4 second - units (nRF52: 1-32). - @note To avoid excessive clock drift, 0.5 degrees Celsius is the - maximum temperature change allowed in one calibration timer - interval. The interval should be selected to ensure this. + uint8_t source; /**< LF oscillator clock source, see @ref NRF_CLOCK_LF_SRC. */ + uint8_t rc_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: Calibration timer interval in 1/4 second + units (nRF52: 1-32). + @note To avoid excessive clock drift, 0.5 degrees Celsius is the + maximum temperature change allowed in one calibration timer + interval. The interval should be selected to ensure this. - @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. */ - uint8_t rc_temp_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: How often (in number of calibration - intervals) the RC oscillator shall be calibrated if the temperature - hasn't changed. - 0: Always calibrate even if the temperature hasn't changed. - 1: Only calibrate if the temperature has changed (legacy - nRF51 only). - 2-33: Check the temperature and only calibrate if it has changed, - however calibration will take place every rc_temp_ctiv - intervals in any case. + @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. */ + uint8_t rc_temp_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: How often (in number of calibration + intervals) the RC oscillator shall be calibrated if the temperature + hasn't changed. + 0: Always calibrate even if the temperature hasn't changed. + 1: Only calibrate if the temperature has changed (legacy - nRF51 only). + 2-33: Check the temperature and only calibrate if it has changed, + however calibration will take place every rc_temp_ctiv + intervals in any case. - @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. + @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. - @note For nRF52, the application must ensure calibration at least once - every 8 seconds to ensure +/-500 ppm clock stability. The - recommended configuration for ::NRF_CLOCK_LF_SRC_RC on nRF52 is - rc_ctiv=16 and rc_temp_ctiv=2. This will ensure calibration at - least once every 8 seconds and for temperature changes of 0.5 - degrees Celsius every 4 seconds. See the Product Specification - for the nRF52 device being used for more information.*/ - uint8_t accuracy; /**< External clock accuracy used in the LL to compute timing - windows, see @ref NRF_CLOCK_LF_ACCURACY.*/ + @note For nRF52, the application must ensure calibration at least once + every 8 seconds to ensure +/-500 ppm clock stability. The + recommended configuration for ::NRF_CLOCK_LF_SRC_RC on nRF52 is + rc_ctiv=16 and rc_temp_ctiv=2. This will ensure calibration at + least once every 8 seconds and for temperature changes of 0.5 + degrees Celsius every 4 seconds. See the Product Specification + for the nRF52 device being used for more information.*/ + uint8_t accuracy; /**< External clock accuracy used in the LL to compute timing + windows, see @ref NRF_CLOCK_LF_ACCURACY.*/ } nrf_clock_lf_cfg_t; /**@brief Fault Handler type. @@ -290,9 +286,9 @@ typedef struct { * @param[in] pc The program counter of the instruction that triggered the fault. * @param[in] info Optional additional information regarding the fault. Refer to each Fault identifier for details. * - * @note When id is set to @ref NRF_FAULT_ID_APP_MEMACC, pc will contain the address of the instruction being executed at the time - * when the fault is detected by the CPU. The CPU program counter may have advanced up to 2 instructions (no branching) after the - * one that triggered the fault. + * @note When id is set to @ref NRF_FAULT_ID_APP_MEMACC, pc will contain the address of the instruction being executed + * at the time when the fault is detected by the CPU. The CPU program counter may have advanced up to 2 instructions (no + * branching) after the one that triggered the fault. */ typedef void (*nrf_fault_handler_t)(uint32_t id, uint32_t pc, uint32_t info); @@ -320,20 +316,20 @@ typedef void (*nrf_fault_handler_t)(uint32_t id, uint32_t pc, uint32_t info); * * @param p_clock_lf_cfg Low frequency clock source and accuracy. If NULL the clock will be configured as an RC source with rc_ctiv = 16 and .rc_temp_ctiv = 2 - In the case of XTAL source, the PPM accuracy of the chosen clock source must be greater than or equal to - the actual characteristics of your XTAL clock. + In the case of XTAL source, the PPM accuracy of the chosen clock source must be greater than or + equal to the actual characteristics of your XTAL clock. * @param fault_handler Callback to be invoked in case of fault, cannot be NULL. * * @retval ::NRF_SUCCESS * @retval ::NRF_ERROR_INVALID_ADDR Invalid or NULL pointer supplied. - * @retval ::NRF_ERROR_INVALID_STATE SoftDevice is already enabled, and the clock source and fault handler cannot be updated. - * @retval ::NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION SoftDevice interrupt is already enabled, or an enabled interrupt has - an illegal priority level. + * @retval ::NRF_ERROR_INVALID_STATE SoftDevice is already enabled, and the clock source and fault handler cannot be + updated. + * @retval ::NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION SoftDevice interrupt is already enabled, or an enabled + interrupt has an illegal priority level. * @retval ::NRF_ERROR_SDM_LFCLK_SOURCE_UNKNOWN Unknown low frequency clock source selected. * @retval ::NRF_ERROR_INVALID_PARAM Invalid clock source configuration supplied in p_clock_lf_cfg. */ -SVCALL(SD_SOFTDEVICE_ENABLE, uint32_t, - sd_softdevice_enable(nrf_clock_lf_cfg_t const *p_clock_lf_cfg, nrf_fault_handler_t fault_handler)); +SVCALL(SD_SOFTDEVICE_ENABLE, uint32_t, sd_softdevice_enable(nrf_clock_lf_cfg_t const *p_clock_lf_cfg, nrf_fault_handler_t fault_handler)); /**@brief Disables the SoftDevice and by extension the protocol stack. * diff --git a/src/platform/nrf52/softdevice/nrf_soc.h b/src/platform/nrf52/softdevice/nrf_soc.h index c649ca836..4782aba58 100644 --- a/src/platform/nrf52/softdevice/nrf_soc.h +++ b/src/platform/nrf52/softdevice/nrf_soc.h @@ -81,33 +81,32 @@ extern "C" { #define SOC_ECB_CIPHERTEXT_LENGTH (SOC_ECB_CLEARTEXT_LENGTH) /**< ECB ciphertext length. */ #define SD_EVT_IRQn (SWI2_IRQn) /**< SoftDevice Event IRQ number. Used for both protocol events and SoC events. */ -#define SD_EVT_IRQHandler \ - (SWI2_IRQHandler) /**< SoftDevice Event IRQ handler. Used for both protocol events and SoC events. \ +#define SD_EVT_IRQHandler \ + (SWI2_IRQHandler) /**< SoftDevice Event IRQ handler. Used for both protocol events and SoC events. \ The default interrupt priority for this handler is set to 6 */ #define RADIO_NOTIFICATION_IRQn (SWI1_IRQn) /**< The radio notification IRQ number. */ -#define RADIO_NOTIFICATION_IRQHandler \ - (SWI1_IRQHandler) /**< The radio notification IRQ handler. \ +#define RADIO_NOTIFICATION_IRQHandler \ + (SWI1_IRQHandler) /**< The radio notification IRQ handler. \ The default interrupt priority for this handler is set to 6 */ #define NRF_RADIO_LENGTH_MIN_US (100) /**< The shortest allowed radio timeslot, in microseconds. */ #define NRF_RADIO_LENGTH_MAX_US (100000) /**< The longest allowed radio timeslot, in microseconds. */ -#define NRF_RADIO_DISTANCE_MAX_US \ - (128000000UL - 1UL) /**< The longest timeslot distance, in microseconds, allowed for the distance parameter (see @ref \ - nrf_radio_request_normal_t) in the request. */ +#define NRF_RADIO_DISTANCE_MAX_US \ + (128000000UL - 1UL) /**< The longest timeslot distance, in microseconds, allowed for the distance parameter (see \ + @ref nrf_radio_request_normal_t) in the request. */ -#define NRF_RADIO_EARLIEST_TIMEOUT_MAX_US \ - (128000000UL - 1UL) /**< The longest timeout, in microseconds, allowed when requesting the earliest possible timeslot. */ +#define NRF_RADIO_EARLIEST_TIMEOUT_MAX_US \ + (128000000UL - 1UL) /**< The longest timeout, in microseconds, allowed when requesting the earliest possible timeslot. */ -#define NRF_RADIO_START_JITTER_US \ - (2) /**< The maximum jitter in @ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START relative to the requested start time. */ +#define NRF_RADIO_START_JITTER_US (2) /**< The maximum jitter in @ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START relative to the requested start time. */ /**@brief Mask of PPI channels reserved by the SoftDevice when the SoftDevice is disabled. */ #define NRF_SOC_SD_PPI_CHANNELS_SD_DISABLED_MSK ((uint32_t)(0)) /**@brief Mask of PPI channels reserved by the SoftDevice when the SoftDevice is enabled. */ -#define NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK \ - ((uint32_t)((1U << 17) | (1U << 18) | (1U << 19) | (1U << 20) | (1U << 21) | (1U << 22) | (1U << 23) | (1U << 24) | \ - (1U << 25) | (1U << 26) | (1U << 27) | (1U << 28) | (1U << 29) | (1U << 30) | (1U << 31))) +#define NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK \ + ((uint32_t)((1U << 17) | (1U << 18) | (1U << 19) | (1U << 20) | (1U << 21) | (1U << 22) | (1U << 23) | (1U << 24) | (1U << 25) | (1U << 26) | \ + (1U << 27) | (1U << 28) | (1U << 29) | (1U << 30) | (1U << 31))) /**@brief Mask of PPI groups reserved by the SoftDevice when the SoftDevice is disabled. */ #define NRF_SOC_SD_PPI_GROUPS_SD_DISABLED_MSK ((uint32_t)(0)) @@ -122,55 +121,55 @@ extern "C" { /**@brief The SVC numbers used by the SVC functions in the SoC library. */ enum NRF_SOC_SVCS { - SD_PPI_CHANNEL_ENABLE_GET = SOC_SVC_BASE, - SD_PPI_CHANNEL_ENABLE_SET = SOC_SVC_BASE + 1, - SD_PPI_CHANNEL_ENABLE_CLR = SOC_SVC_BASE + 2, - SD_PPI_CHANNEL_ASSIGN = SOC_SVC_BASE + 3, - SD_PPI_GROUP_TASK_ENABLE = SOC_SVC_BASE + 4, - SD_PPI_GROUP_TASK_DISABLE = SOC_SVC_BASE + 5, - SD_PPI_GROUP_ASSIGN = SOC_SVC_BASE + 6, - SD_PPI_GROUP_GET = SOC_SVC_BASE + 7, - SD_FLASH_PAGE_ERASE = SOC_SVC_BASE + 8, - SD_FLASH_WRITE = SOC_SVC_BASE + 9, - SD_PROTECTED_REGISTER_WRITE = SOC_SVC_BASE + 11, - SD_MUTEX_NEW = SOC_SVC_BASE_NOT_AVAILABLE, - SD_MUTEX_ACQUIRE = SOC_SVC_BASE_NOT_AVAILABLE + 1, - SD_MUTEX_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 2, - SD_RAND_APPLICATION_POOL_CAPACITY_GET = SOC_SVC_BASE_NOT_AVAILABLE + 3, - SD_RAND_APPLICATION_BYTES_AVAILABLE_GET = SOC_SVC_BASE_NOT_AVAILABLE + 4, - SD_RAND_APPLICATION_VECTOR_GET = SOC_SVC_BASE_NOT_AVAILABLE + 5, - SD_POWER_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 6, - SD_POWER_SYSTEM_OFF = SOC_SVC_BASE_NOT_AVAILABLE + 7, - SD_POWER_RESET_REASON_GET = SOC_SVC_BASE_NOT_AVAILABLE + 8, - SD_POWER_RESET_REASON_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 9, - SD_POWER_POF_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 10, - SD_POWER_POF_THRESHOLD_SET = SOC_SVC_BASE_NOT_AVAILABLE + 11, - SD_POWER_POF_THRESHOLDVDDH_SET = SOC_SVC_BASE_NOT_AVAILABLE + 12, - SD_POWER_RAM_POWER_SET = SOC_SVC_BASE_NOT_AVAILABLE + 13, - SD_POWER_RAM_POWER_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 14, - SD_POWER_RAM_POWER_GET = SOC_SVC_BASE_NOT_AVAILABLE + 15, - SD_POWER_GPREGRET_SET = SOC_SVC_BASE_NOT_AVAILABLE + 16, - SD_POWER_GPREGRET_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 17, - SD_POWER_GPREGRET_GET = SOC_SVC_BASE_NOT_AVAILABLE + 18, - SD_POWER_DCDC_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 19, - SD_POWER_DCDC0_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 20, - SD_APP_EVT_WAIT = SOC_SVC_BASE_NOT_AVAILABLE + 21, - SD_CLOCK_HFCLK_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 22, - SD_CLOCK_HFCLK_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 23, - SD_CLOCK_HFCLK_IS_RUNNING = SOC_SVC_BASE_NOT_AVAILABLE + 24, - SD_RADIO_NOTIFICATION_CFG_SET = SOC_SVC_BASE_NOT_AVAILABLE + 25, - SD_ECB_BLOCK_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 26, - SD_ECB_BLOCKS_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 27, - SD_RADIO_SESSION_OPEN = SOC_SVC_BASE_NOT_AVAILABLE + 28, - SD_RADIO_SESSION_CLOSE = SOC_SVC_BASE_NOT_AVAILABLE + 29, - SD_RADIO_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 30, - SD_EVT_GET = SOC_SVC_BASE_NOT_AVAILABLE + 31, - SD_TEMP_GET = SOC_SVC_BASE_NOT_AVAILABLE + 32, - SD_POWER_USBPWRRDY_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 33, - SD_POWER_USBDETECTED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 34, - SD_POWER_USBREMOVED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 35, - SD_POWER_USBREGSTATUS_GET = SOC_SVC_BASE_NOT_AVAILABLE + 36, - SVC_SOC_LAST = SOC_SVC_BASE_NOT_AVAILABLE + 37 + SD_PPI_CHANNEL_ENABLE_GET = SOC_SVC_BASE, + SD_PPI_CHANNEL_ENABLE_SET = SOC_SVC_BASE + 1, + SD_PPI_CHANNEL_ENABLE_CLR = SOC_SVC_BASE + 2, + SD_PPI_CHANNEL_ASSIGN = SOC_SVC_BASE + 3, + SD_PPI_GROUP_TASK_ENABLE = SOC_SVC_BASE + 4, + SD_PPI_GROUP_TASK_DISABLE = SOC_SVC_BASE + 5, + SD_PPI_GROUP_ASSIGN = SOC_SVC_BASE + 6, + SD_PPI_GROUP_GET = SOC_SVC_BASE + 7, + SD_FLASH_PAGE_ERASE = SOC_SVC_BASE + 8, + SD_FLASH_WRITE = SOC_SVC_BASE + 9, + SD_PROTECTED_REGISTER_WRITE = SOC_SVC_BASE + 11, + SD_MUTEX_NEW = SOC_SVC_BASE_NOT_AVAILABLE, + SD_MUTEX_ACQUIRE = SOC_SVC_BASE_NOT_AVAILABLE + 1, + SD_MUTEX_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 2, + SD_RAND_APPLICATION_POOL_CAPACITY_GET = SOC_SVC_BASE_NOT_AVAILABLE + 3, + SD_RAND_APPLICATION_BYTES_AVAILABLE_GET = SOC_SVC_BASE_NOT_AVAILABLE + 4, + SD_RAND_APPLICATION_VECTOR_GET = SOC_SVC_BASE_NOT_AVAILABLE + 5, + SD_POWER_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 6, + SD_POWER_SYSTEM_OFF = SOC_SVC_BASE_NOT_AVAILABLE + 7, + SD_POWER_RESET_REASON_GET = SOC_SVC_BASE_NOT_AVAILABLE + 8, + SD_POWER_RESET_REASON_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 9, + SD_POWER_POF_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 10, + SD_POWER_POF_THRESHOLD_SET = SOC_SVC_BASE_NOT_AVAILABLE + 11, + SD_POWER_POF_THRESHOLDVDDH_SET = SOC_SVC_BASE_NOT_AVAILABLE + 12, + SD_POWER_RAM_POWER_SET = SOC_SVC_BASE_NOT_AVAILABLE + 13, + SD_POWER_RAM_POWER_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 14, + SD_POWER_RAM_POWER_GET = SOC_SVC_BASE_NOT_AVAILABLE + 15, + SD_POWER_GPREGRET_SET = SOC_SVC_BASE_NOT_AVAILABLE + 16, + SD_POWER_GPREGRET_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 17, + SD_POWER_GPREGRET_GET = SOC_SVC_BASE_NOT_AVAILABLE + 18, + SD_POWER_DCDC_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 19, + SD_POWER_DCDC0_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 20, + SD_APP_EVT_WAIT = SOC_SVC_BASE_NOT_AVAILABLE + 21, + SD_CLOCK_HFCLK_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 22, + SD_CLOCK_HFCLK_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 23, + SD_CLOCK_HFCLK_IS_RUNNING = SOC_SVC_BASE_NOT_AVAILABLE + 24, + SD_RADIO_NOTIFICATION_CFG_SET = SOC_SVC_BASE_NOT_AVAILABLE + 25, + SD_ECB_BLOCK_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 26, + SD_ECB_BLOCKS_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 27, + SD_RADIO_SESSION_OPEN = SOC_SVC_BASE_NOT_AVAILABLE + 28, + SD_RADIO_SESSION_CLOSE = SOC_SVC_BASE_NOT_AVAILABLE + 29, + SD_RADIO_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 30, + SD_EVT_GET = SOC_SVC_BASE_NOT_AVAILABLE + 31, + SD_TEMP_GET = SOC_SVC_BASE_NOT_AVAILABLE + 32, + SD_POWER_USBPWRRDY_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 33, + SD_POWER_USBDETECTED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 34, + SD_POWER_USBREMOVED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 35, + SD_POWER_USBREGSTATUS_GET = SOC_SVC_BASE_NOT_AVAILABLE + 36, + SVC_SOC_LAST = SOC_SVC_BASE_NOT_AVAILABLE + 37 }; /**@brief Possible values of a ::nrf_mutex_t. */ @@ -178,79 +177,80 @@ enum NRF_MUTEX_VALUES { NRF_MUTEX_FREE, NRF_MUTEX_TAKEN }; /**@brief Power modes. */ enum NRF_POWER_MODES { - NRF_POWER_MODE_CONSTLAT, /**< Constant latency mode. See power management in the reference manual. */ - NRF_POWER_MODE_LOWPWR /**< Low power mode. See power management in the reference manual. */ + NRF_POWER_MODE_CONSTLAT, /**< Constant latency mode. See power management in the reference manual. */ + NRF_POWER_MODE_LOWPWR /**< Low power mode. See power management in the reference manual. */ }; /**@brief Power failure thresholds */ enum NRF_POWER_THRESHOLDS { - NRF_POWER_THRESHOLD_V17 = 4UL, /**< 1.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V18, /**< 1.8 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V19, /**< 1.9 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V20, /**< 2.0 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V21, /**< 2.1 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V22, /**< 2.2 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V23, /**< 2.3 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V24, /**< 2.4 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V25, /**< 2.5 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V26, /**< 2.6 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V27, /**< 2.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V28 /**< 2.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V17 = 4UL, /**< 1.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V18, /**< 1.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V19, /**< 1.9 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V20, /**< 2.0 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V21, /**< 2.1 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V22, /**< 2.2 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V23, /**< 2.3 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V24, /**< 2.4 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V25, /**< 2.5 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V26, /**< 2.6 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V27, /**< 2.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V28 /**< 2.8 Volts power failure threshold. */ }; /**@brief Power failure thresholds for high voltage */ enum NRF_POWER_THRESHOLDVDDHS { - NRF_POWER_THRESHOLDVDDH_V27, /**< 2.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V28, /**< 2.8 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V29, /**< 2.9 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V30, /**< 3.0 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V31, /**< 3.1 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V32, /**< 3.2 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V33, /**< 3.3 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V34, /**< 3.4 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V35, /**< 3.5 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V36, /**< 3.6 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V37, /**< 3.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V38, /**< 3.8 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V39, /**< 3.9 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V40, /**< 4.0 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V41, /**< 4.1 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V42 /**< 4.2 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V27, /**< 2.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V28, /**< 2.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V29, /**< 2.9 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V30, /**< 3.0 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V31, /**< 3.1 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V32, /**< 3.2 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V33, /**< 3.3 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V34, /**< 3.4 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V35, /**< 3.5 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V36, /**< 3.6 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V37, /**< 3.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V38, /**< 3.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V39, /**< 3.9 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V40, /**< 4.0 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V41, /**< 4.1 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V42 /**< 4.2 Volts power failure threshold. */ }; /**@brief DC/DC converter modes. */ enum NRF_POWER_DCDC_MODES { - NRF_POWER_DCDC_DISABLE, /**< The DCDC is disabled. */ - NRF_POWER_DCDC_ENABLE /**< The DCDC is enabled. */ + NRF_POWER_DCDC_DISABLE, /**< The DCDC is disabled. */ + NRF_POWER_DCDC_ENABLE /**< The DCDC is enabled. */ }; /**@brief Radio notification distances. */ enum NRF_RADIO_NOTIFICATION_DISTANCES { - NRF_RADIO_NOTIFICATION_DISTANCE_NONE = 0, /**< The event does not have a notification. */ - NRF_RADIO_NOTIFICATION_DISTANCE_800US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_1740US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_2680US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_3620US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_4560US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_5500US /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_NONE = 0, /**< The event does not have a notification. */ + NRF_RADIO_NOTIFICATION_DISTANCE_800US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_1740US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_2680US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_3620US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_4560US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_5500US /**< The distance from the active notification to start of radio activity. */ }; /**@brief Radio notification types. */ enum NRF_RADIO_NOTIFICATION_TYPES { - NRF_RADIO_NOTIFICATION_TYPE_NONE = 0, /**< The event does not have a radio notification signal. */ - NRF_RADIO_NOTIFICATION_TYPE_INT_ON_ACTIVE, /**< Using interrupt for notification when the radio will be enabled. */ - NRF_RADIO_NOTIFICATION_TYPE_INT_ON_INACTIVE, /**< Using interrupt for notification when the radio has been disabled. */ - NRF_RADIO_NOTIFICATION_TYPE_INT_ON_BOTH, /**< Using interrupt for notification both when the radio will be enabled and - disabled. */ + NRF_RADIO_NOTIFICATION_TYPE_NONE = 0, /**< The event does not have a radio notification signal. */ + NRF_RADIO_NOTIFICATION_TYPE_INT_ON_ACTIVE, /**< Using interrupt for notification when the radio will be enabled. */ + NRF_RADIO_NOTIFICATION_TYPE_INT_ON_INACTIVE, /**< Using interrupt for notification when the radio has been disabled. + */ + NRF_RADIO_NOTIFICATION_TYPE_INT_ON_BOTH, /**< Using interrupt for notification both when the radio will be enabled and + disabled. */ }; /**@brief The Radio signal callback types. */ enum NRF_RADIO_CALLBACK_SIGNAL_TYPE { - NRF_RADIO_CALLBACK_SIGNAL_TYPE_START, /**< This signal indicates the start of the radio timeslot. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_TIMER0, /**< This signal indicates the NRF_TIMER0 interrupt. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_RADIO, /**< This signal indicates the NRF_RADIO interrupt. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_FAILED, /**< This signal indicates extend action failed. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_SUCCEEDED /**< This signal indicates extend action succeeded. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_START, /**< This signal indicates the start of the radio timeslot. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_TIMER0, /**< This signal indicates the NRF_TIMER0 interrupt. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_RADIO, /**< This signal indicates the NRF_RADIO interrupt. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_FAILED, /**< This signal indicates extend action failed. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_SUCCEEDED /**< This signal indicates extend action succeeded. */ }; /**@brief The actions requested by the signal callback. @@ -259,63 +259,66 @@ enum NRF_RADIO_CALLBACK_SIGNAL_TYPE { * returned. */ enum NRF_RADIO_SIGNAL_CALLBACK_ACTION { - NRF_RADIO_SIGNAL_CALLBACK_ACTION_NONE, /**< Return without action. */ - NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND, /**< Request an extension of the current - timeslot. Maximum execution time for this action: - @ref NRF_RADIO_MAX_EXTENSION_PROCESSING_TIME_US. - This action must be started at least - @ref NRF_RADIO_MIN_EXTENSION_MARGIN_US before - the end of the timeslot. */ - NRF_RADIO_SIGNAL_CALLBACK_ACTION_END, /**< End the current radio timeslot. */ - NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END /**< Request a new radio timeslot and end the current timeslot. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_NONE, /**< Return without action. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND, /**< Request an extension of the current + timeslot. Maximum execution time for this action: + @ref NRF_RADIO_MAX_EXTENSION_PROCESSING_TIME_US. + This action must be started at least + @ref NRF_RADIO_MIN_EXTENSION_MARGIN_US before + the end of the timeslot. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_END, /**< End the current radio timeslot. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END /**< Request a new radio timeslot and end the current timeslot. */ }; /**@brief Radio timeslot high frequency clock source configuration. */ enum NRF_RADIO_HFCLK_CFG { - NRF_RADIO_HFCLK_CFG_XTAL_GUARANTEED, /**< The SoftDevice will guarantee that the high frequency clock source is the - external crystal for the whole duration of the timeslot. This should be the - preferred option for events that use the radio or require high timing accuracy. - @note The SoftDevice will automatically turn on and off the external crystal, - at the beginning and end of the timeslot, respectively. The crystal may also - intentionally be left running after the timeslot, in cases where it is needed - by the SoftDevice shortly after the end of the timeslot. */ - NRF_RADIO_HFCLK_CFG_NO_GUARANTEE /**< This configuration allows for earlier and tighter scheduling of timeslots. - The RC oscillator may be the clock source in part or for the whole duration of the - timeslot. The RC oscillator's accuracy must therefore be taken into consideration. - @note If the application will use the radio peripheral in timeslots with this - configuration, it must make sure that the crystal is running and stable before - starting the radio. */ + NRF_RADIO_HFCLK_CFG_XTAL_GUARANTEED, /**< The SoftDevice will guarantee that the high frequency clock source is the + external crystal for the whole duration of the timeslot. This should be the + preferred option for events that use the radio or require high timing + accuracy. + @note The SoftDevice will automatically turn on and off the external crystal, + at the beginning and end of the timeslot, respectively. The crystal may also + intentionally be left running after the timeslot, in cases where it is needed + by the SoftDevice shortly after the end of the timeslot. */ + NRF_RADIO_HFCLK_CFG_NO_GUARANTEE /**< This configuration allows for earlier and tighter scheduling of timeslots. + The RC oscillator may be the clock source in part or for the whole duration of + the timeslot. The RC oscillator's accuracy must therefore be taken into + consideration. + @note If the application will use the radio peripheral in timeslots with this + configuration, it must make sure that the crystal is running and stable before + starting the radio. */ }; /**@brief Radio timeslot priorities. */ enum NRF_RADIO_PRIORITY { - NRF_RADIO_PRIORITY_HIGH, /**< High (equal priority as the normal connection priority of the SoftDevice stack(s)). */ - NRF_RADIO_PRIORITY_NORMAL, /**< Normal (equal priority as the priority of secondary activities of the SoftDevice stack(s)). */ + NRF_RADIO_PRIORITY_HIGH, /**< High (equal priority as the normal connection priority of the SoftDevice stack(s)). */ + NRF_RADIO_PRIORITY_NORMAL, /**< Normal (equal priority as the priority of secondary activities of the SoftDevice + stack(s)). */ }; /**@brief Radio timeslot request type. */ enum NRF_RADIO_REQUEST_TYPE { - NRF_RADIO_REQ_TYPE_EARLIEST, /**< Request radio timeslot as early as possible. This should always be used for the first - request in a session. */ - NRF_RADIO_REQ_TYPE_NORMAL /**< Normal radio timeslot request. */ + NRF_RADIO_REQ_TYPE_EARLIEST, /**< Request radio timeslot as early as possible. This should always be used for the + first request in a session. */ + NRF_RADIO_REQ_TYPE_NORMAL /**< Normal radio timeslot request. */ }; /**@brief SoC Events. */ enum NRF_SOC_EVTS { - NRF_EVT_HFCLKSTARTED, /**< Event indicating that the HFCLK has started. */ - NRF_EVT_POWER_FAILURE_WARNING, /**< Event indicating that a power failure warning has occurred. */ - NRF_EVT_FLASH_OPERATION_SUCCESS, /**< Event indicating that the ongoing flash operation has completed successfully. */ - NRF_EVT_FLASH_OPERATION_ERROR, /**< Event indicating that the ongoing flash operation has timed out with an error. */ - NRF_EVT_RADIO_BLOCKED, /**< Event indicating that a radio timeslot was blocked. */ - NRF_EVT_RADIO_CANCELED, /**< Event indicating that a radio timeslot was canceled by SoftDevice. */ - NRF_EVT_RADIO_SIGNAL_CALLBACK_INVALID_RETURN, /**< Event indicating that a radio timeslot signal callback handler return was - invalid. */ - NRF_EVT_RADIO_SESSION_IDLE, /**< Event indicating that a radio timeslot session is idle. */ - NRF_EVT_RADIO_SESSION_CLOSED, /**< Event indicating that a radio timeslot session is closed. */ - NRF_EVT_POWER_USB_POWER_READY, /**< Event indicating that a USB 3.3 V supply is ready. */ - NRF_EVT_POWER_USB_DETECTED, /**< Event indicating that voltage supply is detected on VBUS. */ - NRF_EVT_POWER_USB_REMOVED, /**< Event indicating that voltage supply is removed from VBUS. */ - NRF_EVT_NUMBER_OF_EVTS + NRF_EVT_HFCLKSTARTED, /**< Event indicating that the HFCLK has started. */ + NRF_EVT_POWER_FAILURE_WARNING, /**< Event indicating that a power failure warning has occurred. */ + NRF_EVT_FLASH_OPERATION_SUCCESS, /**< Event indicating that the ongoing flash operation has completed successfully. */ + NRF_EVT_FLASH_OPERATION_ERROR, /**< Event indicating that the ongoing flash operation has timed out with an error. */ + NRF_EVT_RADIO_BLOCKED, /**< Event indicating that a radio timeslot was blocked. */ + NRF_EVT_RADIO_CANCELED, /**< Event indicating that a radio timeslot was canceled by SoftDevice. */ + NRF_EVT_RADIO_SIGNAL_CALLBACK_INVALID_RETURN, /**< Event indicating that a radio timeslot signal callback handler + return was invalid. */ + NRF_EVT_RADIO_SESSION_IDLE, /**< Event indicating that a radio timeslot session is idle. */ + NRF_EVT_RADIO_SESSION_CLOSED, /**< Event indicating that a radio timeslot session is closed. */ + NRF_EVT_POWER_USB_POWER_READY, /**< Event indicating that a USB 3.3 V supply is ready. */ + NRF_EVT_POWER_USB_DETECTED, /**< Event indicating that voltage supply is detected on VBUS. */ + NRF_EVT_POWER_USB_REMOVED, /**< Event indicating that voltage supply is removed from VBUS. */ + NRF_EVT_NUMBER_OF_EVTS }; /**@} */ @@ -330,44 +333,44 @@ typedef volatile uint8_t nrf_mutex_t; /**@brief Parameters for a request for a timeslot as early as possible. */ typedef struct { - uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ - uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ - uint32_t length_us; /**< The radio timeslot length (in the range 100 to 100,000] microseconds). */ - uint32_t timeout_us; /**< Longest acceptable delay until the start of the requested timeslot (up to @ref - NRF_RADIO_EARLIEST_TIMEOUT_MAX_US microseconds). */ + uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ + uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ + uint32_t length_us; /**< The radio timeslot length (in the range 100 to 100,000] microseconds). */ + uint32_t timeout_us; /**< Longest acceptable delay until the start of the requested timeslot (up to @ref + NRF_RADIO_EARLIEST_TIMEOUT_MAX_US microseconds). */ } nrf_radio_request_earliest_t; /**@brief Parameters for a normal radio timeslot request. */ typedef struct { - uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ - uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ - uint32_t distance_us; /**< Distance from the start of the previous radio timeslot (up to @ref NRF_RADIO_DISTANCE_MAX_US - microseconds). */ - uint32_t length_us; /**< The radio timeslot length (in the range [100..100,000] microseconds). */ + uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ + uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ + uint32_t distance_us; /**< Distance from the start of the previous radio timeslot (up to @ref + NRF_RADIO_DISTANCE_MAX_US microseconds). */ + uint32_t length_us; /**< The radio timeslot length (in the range [100..100,000] microseconds). */ } nrf_radio_request_normal_t; /**@brief Radio timeslot request parameters. */ typedef struct { - uint8_t request_type; /**< Type of request, see @ref NRF_RADIO_REQUEST_TYPE. */ - union { - nrf_radio_request_earliest_t earliest; /**< Parameters for requesting a radio timeslot as early as possible. */ - nrf_radio_request_normal_t normal; /**< Parameters for requesting a normal radio timeslot. */ - } params; /**< Parameter union. */ + uint8_t request_type; /**< Type of request, see @ref NRF_RADIO_REQUEST_TYPE. */ + union { + nrf_radio_request_earliest_t earliest; /**< Parameters for requesting a radio timeslot as early as possible. */ + nrf_radio_request_normal_t normal; /**< Parameters for requesting a normal radio timeslot. */ + } params; /**< Parameter union. */ } nrf_radio_request_t; /**@brief Return parameters of the radio timeslot signal callback. */ typedef struct { - uint8_t callback_action; /**< The action requested by the application when returning from the signal callback, see @ref - NRF_RADIO_SIGNAL_CALLBACK_ACTION. */ - union { - struct { - nrf_radio_request_t *p_next; /**< The request parameters for the next radio timeslot. */ - } request; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END. */ - struct { - uint32_t length_us; /**< Requested extension of the radio timeslot duration (microseconds) (for minimum time see @ref - NRF_RADIO_MINIMUM_TIMESLOT_LENGTH_EXTENSION_TIME_US). */ - } extend; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND. */ - } params; /**< Parameter union. */ + uint8_t callback_action; /**< The action requested by the application when returning from the signal callback, see + @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION. */ + union { + struct { + nrf_radio_request_t *p_next; /**< The request parameters for the next radio timeslot. */ + } request; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END. */ + struct { + uint32_t length_us; /**< Requested extension of the radio timeslot duration (microseconds) (for minimum time see + @ref NRF_RADIO_MINIMUM_TIMESLOT_LENGTH_EXTENSION_TIME_US). */ + } extend; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND. */ + } params; /**< Parameter union. */ } nrf_radio_signal_callback_return_param_t; /**@brief The radio timeslot signal callback type. @@ -391,17 +394,17 @@ typedef uint8_t soc_ecb_ciphertext_t[SOC_ECB_CIPHERTEXT_LENGTH]; /**< Ciphertext /**@brief AES ECB data structure */ typedef struct { - soc_ecb_key_t key; /**< Encryption key. */ - soc_ecb_cleartext_t cleartext; /**< Cleartext data. */ - soc_ecb_ciphertext_t ciphertext; /**< Ciphertext data. */ + soc_ecb_key_t key; /**< Encryption key. */ + soc_ecb_cleartext_t cleartext; /**< Cleartext data. */ + soc_ecb_ciphertext_t ciphertext; /**< Ciphertext data. */ } nrf_ecb_hal_data_t; /**@brief AES ECB block. Used to provide multiple blocks in a single call to @ref sd_ecb_blocks_encrypt.*/ typedef struct { - soc_ecb_key_t const *p_key; /**< Pointer to the Encryption key. */ - soc_ecb_cleartext_t const *p_cleartext; /**< Pointer to the Cleartext data. */ - soc_ecb_ciphertext_t *p_ciphertext; /**< Pointer to the Ciphertext data. */ + soc_ecb_key_t const *p_key; /**< Pointer to the Encryption key. */ + soc_ecb_cleartext_t const *p_cleartext; /**< Pointer to the Cleartext data. */ + soc_ecb_ciphertext_t *p_ciphertext; /**< Pointer to the Ciphertext data. */ } nrf_ecb_hal_data_block_t; /**@} */ @@ -456,8 +459,8 @@ SVCALL(SD_RAND_APPLICATION_BYTES_AVAILABLE_GET, uint32_t, sd_rand_application_by * @param[in] length Number of bytes to take from pool and place in p_buff. * * @retval ::NRF_SUCCESS The requested bytes were written to p_buff. - * @retval ::NRF_ERROR_SOC_RAND_NOT_ENOUGH_VALUES No bytes were written to the buffer, because there were not enough bytes - * available. + * @retval ::NRF_ERROR_SOC_RAND_NOT_ENOUGH_VALUES No bytes were written to the buffer, because there were not enough + * bytes available. */ SVCALL(SD_RAND_APPLICATION_VECTOR_GET, uint32_t, sd_rand_application_vector_get(uint8_t *p_buff, uint8_t length)); diff --git a/src/platform/nrf52/softdevice/nrf_svc.h b/src/platform/nrf52/softdevice/nrf_svc.h index 1de44656f..534fa00ac 100644 --- a/src/platform/nrf52/softdevice/nrf_svc.h +++ b/src/platform/nrf52/softdevice/nrf_svc.h @@ -68,23 +68,22 @@ extern "C" { #else #define GCC_CAST_CPP #endif -#define SVCALL(number, return_type, signature) \ - _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wreturn-type\"") __attribute__((naked)) \ - __attribute__((unused)) static return_type signature \ - { \ - __asm("svc %0\n" \ - "bx r14" \ - : \ - : "I"(GCC_CAST_CPP number) \ - : "r0"); \ - } \ - _Pragma("GCC diagnostic pop") +#define SVCALL(number, return_type, signature) \ + _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wreturn-type\"") __attribute__((naked)) \ + __attribute__((unused)) static return_type signature { \ + __asm("svc %0\n" \ + "bx r14" \ + : \ + : "I"(GCC_CAST_CPP number) \ + : "r0"); \ + } \ + _Pragma("GCC diagnostic pop") #elif defined(__ICCARM__) #define PRAGMA(x) _Pragma(#x) -#define SVCALL(number, return_type, signature) \ - PRAGMA(swi_number = (number)) \ - __swi return_type signature; +#define SVCALL(number, return_type, signature) \ + PRAGMA(swi_number = (number)) \ + __swi return_type signature; #else #define SVCALL(number, return_type, signature) return_type signature #endif diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 4722a66e5..4d73faf66 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -41,832 +41,803 @@ const char *argp_program_version = optstr(APP_VERSION); char stdoutBuffer[512]; // FIXME - move setBluetoothEnable into a HALPlatform class -void setBluetoothEnable(bool enable) -{ - // not needed +void setBluetoothEnable(bool enable) { + // not needed } -void cpuDeepSleep(uint32_t msecs) -{ - notImplemented("cpuDeepSleep"); -} +void cpuDeepSleep(uint32_t msecs) { notImplemented("cpuDeepSleep"); } void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel"); int TCPPort = SERVER_API_DEFAULT_PORT; -static error_t parse_opt(int key, char *arg, struct argp_state *state) -{ - switch (key) { - case 'p': - if (sscanf(arg, "%d", &TCPPort) < 1) - return ARGP_ERR_UNKNOWN; - else - printf("Using config file %d\n", TCPPort); - break; - case 'c': - configPath = arg; - break; - case 's': - portduino_config.force_simradio = true; - break; - case 'h': - optionMac = arg; - break; - case 'v': - verboseEnabled = true; - break; - case 'y': - yamlOnly = true; - break; - case ARGP_KEY_ARG: - return 0; - default: - return ARGP_ERR_UNKNOWN; - } +static error_t parse_opt(int key, char *arg, struct argp_state *state) { + switch (key) { + case 'p': + if (sscanf(arg, "%d", &TCPPort) < 1) + return ARGP_ERR_UNKNOWN; + else + printf("Using config file %d\n", TCPPort); + break; + case 'c': + configPath = arg; + break; + case 's': + portduino_config.force_simradio = true; + break; + case 'h': + optionMac = arg; + break; + case 'v': + verboseEnabled = true; + break; + case 'y': + yamlOnly = true; + break; + case ARGP_KEY_ARG: return 0; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; } -void portduinoCustomInit() -{ - static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, - {"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."}, - {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"}, - {"sim", 's', 0, 0, "Run in Simulated radio mode"}, - {"verbose", 'v', 0, 0, "Set log level to full debug"}, - {"output-yaml", 'y', 0, 0, "Output config yaml and exit"}, - {0}}; - static void *childArguments; - static char doc[] = "Meshtastic native build."; - static char args_doc[] = "..."; - static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; - const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0}; - portduinoAddArguments(child, childArguments); +void portduinoCustomInit() { + static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, + {"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."}, + {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"}, + {"sim", 's', 0, 0, "Run in Simulated radio mode"}, + {"verbose", 'v', 0, 0, "Set log level to full debug"}, + {"output-yaml", 'y', 0, 0, "Output config yaml and exit"}, + {0}}; + static void *childArguments; + static char doc[] = "Meshtastic native build."; + static char args_doc[] = "..."; + static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; + const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0}; + portduinoAddArguments(child, childArguments); } -void getMacAddr(uint8_t *dmac) -{ - // We should store this value, and short-circuit all this if it's already been set. - if (optionMac != nullptr && strlen(optionMac) > 0) { - if (strlen(optionMac) >= 12) { - MAC_from_string(optionMac, dmac); - } else { - uint32_t hwId = {0}; - sscanf(optionMac, "%u", &hwId); - dmac[0] = 0x80; - dmac[1] = 0; - dmac[2] = hwId >> 24; - dmac[3] = hwId >> 16; - dmac[4] = hwId >> 8; - dmac[5] = hwId & 0xff; - } - } else if (portduino_config.mac_address.length() > 11) { - MAC_from_string(portduino_config.mac_address, dmac); - exit; +void getMacAddr(uint8_t *dmac) { + // We should store this value, and short-circuit all this if it's already been set. + if (optionMac != nullptr && strlen(optionMac) > 0) { + if (strlen(optionMac) >= 12) { + MAC_from_string(optionMac, dmac); } else { - - struct hci_dev_info di = {0}; - di.dev_id = 0; - bdaddr_t bdaddr; - int btsock; - btsock = socket(AF_BLUETOOTH, SOCK_RAW, 1); - if (btsock < 0) { // If anything fails, just return with the default value - return; - } - - if (ioctl(btsock, HCIGETDEVINFO, (void *)&di)) { - return; - } - - dmac[0] = di.bdaddr.b[5]; - dmac[1] = di.bdaddr.b[4]; - dmac[2] = di.bdaddr.b[3]; - dmac[3] = di.bdaddr.b[2]; - dmac[4] = di.bdaddr.b[1]; - dmac[5] = di.bdaddr.b[0]; + uint32_t hwId = {0}; + sscanf(optionMac, "%u", &hwId); + dmac[0] = 0x80; + dmac[1] = 0; + dmac[2] = hwId >> 24; + dmac[3] = hwId >> 16; + dmac[4] = hwId >> 8; + dmac[5] = hwId & 0xff; } + } else if (portduino_config.mac_address.length() > 11) { + MAC_from_string(portduino_config.mac_address, dmac); + exit; + } else { + + struct hci_dev_info di = {0}; + di.dev_id = 0; + bdaddr_t bdaddr; + int btsock; + btsock = socket(AF_BLUETOOTH, SOCK_RAW, 1); + if (btsock < 0) { // If anything fails, just return with the default value + return; + } + + if (ioctl(btsock, HCIGETDEVINFO, (void *)&di)) { + return; + } + + dmac[0] = di.bdaddr.b[5]; + dmac[1] = di.bdaddr.b[4]; + dmac[2] = di.bdaddr.b[3]; + dmac[3] = di.bdaddr.b[2]; + dmac[4] = di.bdaddr.b[1]; + dmac[5] = di.bdaddr.b[0]; + } } -std::string cleanupNameForAutoconf(std::string name) -{ - // Convert spaces -> dashes, lowercase +std::string cleanupNameForAutoconf(std::string name) { + // Convert spaces -> dashes, lowercase - std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { - if (c == ' ') { - return '-'; - } - return (char)std::tolower(c); - }); + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { + if (c == ' ') { + return '-'; + } + return (char)std::tolower(c); + }); - return name; + return name; } /** apps run under portduino can optionally define a portduinoSetup() to * use portduino specific init code (such as gpioBind) to setup portduino on their host machine, * before running 'arduino' code. */ -void portduinoSetup() -{ - int max_GPIO = 0; - std::string gpioChipName = "gpiochip"; - portduino_config.displayPanel = no_screen; +void portduinoSetup() { + int max_GPIO = 0; + std::string gpioChipName = "gpiochip"; + portduino_config.displayPanel = no_screen; - // Force stdout to be line buffered - setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer)); + // Force stdout to be line buffered + setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer)); - if (portduino_config.force_simradio == true) { - portduino_config.lora_module = use_simradio; - } else if (configPath != nullptr) { - if (loadConfig(configPath)) { - if (!yamlOnly) - std::cout << "Using " << configPath << " as config file" << std::endl; - } else { - std::cout << "Unable to use " << configPath << " as config file" << std::endl; - exit(EXIT_FAILURE); - } - } else if (access("config.yaml", R_OK) == 0) { - if (loadConfig("config.yaml")) { - if (!yamlOnly) - std::cout << "Using local config.yaml as config file" << std::endl; - } else { - std::cout << "Unable to use local config.yaml as config file" << std::endl; - exit(EXIT_FAILURE); - } - } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) { - if (loadConfig("/etc/meshtasticd/config.yaml")) { - if (!yamlOnly) - std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; - } else { - std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl; - exit(EXIT_FAILURE); - } + if (portduino_config.force_simradio == true) { + portduino_config.lora_module = use_simradio; + } else if (configPath != nullptr) { + if (loadConfig(configPath)) { + if (!yamlOnly) + std::cout << "Using " << configPath << " as config file" << std::endl; } else { - if (!yamlOnly) - std::cout << "No 'config.yaml' found..." << std::endl; - portduino_config.lora_module = use_simradio; + std::cout << "Unable to use " << configPath << " as config file" << std::endl; + exit(EXIT_FAILURE); } - - if (portduino_config.config_directory != "") { - std::string filetype = ".yaml"; - for (const std::filesystem::directory_entry &entry : - std::filesystem::directory_iterator{portduino_config.config_directory}) { - if (ends_with(entry.path().string(), ".yaml")) { - std::cout << "Also using " << entry << " as additional config file" << std::endl; - loadConfig(entry.path().c_str()); - } - } + } else if (access("config.yaml", R_OK) == 0) { + if (loadConfig("config.yaml")) { + if (!yamlOnly) + std::cout << "Using local config.yaml as config file" << std::endl; + } else { + std::cout << "Unable to use local config.yaml as config file" << std::endl; + exit(EXIT_FAILURE); } - - if (yamlOnly) { - std::cout << portduino_config.emit_yaml() << std::endl; - exit(EXIT_SUCCESS); + } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) { + if (loadConfig("/etc/meshtasticd/config.yaml")) { + if (!yamlOnly) + std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; + } else { + std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl; + exit(EXIT_FAILURE); } + } else { + if (!yamlOnly) + std::cout << "No 'config.yaml' found..." << std::endl; + portduino_config.lora_module = use_simradio; + } - if (portduino_config.force_simradio) { - std::cout << "Running in simulated mode." << std::endl; - portduino_config.MaxNodes = 200; // Default to 200 nodes - // Set the random seed equal to TCPPort to have a different seed per instance - randomSeed(TCPPort); - return; + if (portduino_config.config_directory != "") { + std::string filetype = ".yaml"; + for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator{portduino_config.config_directory}) { + if (ends_with(entry.path().string(), ".yaml")) { + std::cout << "Also using " << entry << " as additional config file" << std::endl; + loadConfig(entry.path().c_str()); + } } + } - // If LoRa `Module: auto` (default in config.yaml), - // attempt to auto config based on Product Strings - if (portduino_config.lora_module == use_autoconf) { - bool found_hat = false; - bool found_rak_eeprom = false; - bool found_ch341 = false; - - char hat_vendor[96] = {0}; - char autoconf_product[96] = {0}; - // Try CH341 - try { - std::cout << "autoconf: Looking for CH341 device..." << std::endl; - ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, - portduino_config.lora_usb_pid); - ch341Hal->getProductString(autoconf_product, 95); - delete ch341Hal; - std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; - - found_ch341 = true; - } catch (...) { - std::cout << "autoconf: Could not locate CH341 device" << std::endl; - } - // Try Pi HAT+ - if (strlen(autoconf_product) < 6) { - std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; - if (access("/proc/device-tree/hat/vendor", R_OK) == 0) { - std::ifstream hatVendorFile("/proc/device-tree/hat/vendor"); - if (hatVendorFile.is_open()) { - hatVendorFile.read(hat_vendor, 95); - hatVendorFile.close(); - } - } - if (access("/proc/device-tree/hat/product", R_OK) == 0) { - std::ifstream hatProductFile("/proc/device-tree/hat/product"); - if (hatProductFile.is_open()) { - hatProductFile.read(autoconf_product, 95); - hatProductFile.close(); - } - std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" - << std::endl; - found_hat = true; - } else { - std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; - } - } - // attempt to load autoconf data from an EEPROM on 0x50 - // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 - // :mac address :<16 random unique bytes in hexidecimal> : crc32 - // crc32 is calculated on the eeprom string up to but not including the final colon - if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { - try { - char *mac_start = nullptr; - char *devID_start = nullptr; - char *crc32_start = nullptr; - Wire.begin(); - Wire.beginTransmission(0x50); - Wire.write(0x0); - Wire.write(0x0); - Wire.endTransmission(); - Wire.requestFrom((uint8_t)0x50, (uint8_t)75); - uint8_t i = 0; - delay(100); - std::string autoconf_raw; - while (Wire.available() && i < sizeof(autoconf_product)) { - autoconf_product[i] = Wire.read(); - if (autoconf_product[i] == 0xff) { - autoconf_product[i] = 0x0; - break; - } - autoconf_raw += autoconf_product[i]; - if (autoconf_product[i] == ':') { - autoconf_product[i] = 0x0; - if (mac_start == nullptr) { - mac_start = autoconf_product + i + 1; - } else if (devID_start == nullptr) { - devID_start = autoconf_product + i + 1; - } else if (crc32_start == nullptr) { - crc32_start = autoconf_product + i + 1; - } - } - i++; - } - if (crc32_start != nullptr && strlen(crc32_start) == 8) { - std::string crc32_str(crc32_start); - uint32_t crc32_value = 0; - - // convert crc32 ascii to raw uint32 - for (int j = 0; j < 4; j++) { - crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8; - } - std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl; - - // set the autoconf string to blank and short circuit - if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) { - std::cout << "autoconf: crc32 mismatch, dropping " << std::endl; - autoconf_product[0] = 0x0; - } else { - std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl; - found_rak_eeprom = true; - if (mac_start != nullptr) { - std::cout << "autoconf: Found mac data " << mac_start << std::endl; - if (strlen(mac_start) == 12) - portduino_config.mac_address = std::string(mac_start); - } - if (devID_start != nullptr) { - std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; - if (strlen(devID_start) == 32) { - std::string devID_str(devID_start); - for (int j = 0; j < 16; j++) { - portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16); - } - portduino_config.has_device_id = true; - } - } - } - } else { - std::cout << "autoconf: crc32 missing " << std::endl; - autoconf_product[0] = 0x0; - } - } catch (...) { - std::cout << "autoconf: Could not locate EEPROM" << std::endl; - } - } - // Load the config file based on the product string - if (strlen(autoconf_product) > 0) { - // From configProducts map in PortduinoGlue.h - std::string product_config = ""; - - if (configProducts.find(autoconf_product) != configProducts.end()) { - product_config = configProducts.at(autoconf_product); - } else { - if (found_hat) { - product_config = - cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); - } else if (found_ch341) { - product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); - } - - // Don't try to automatically find config for a device with RAK eeprom. - if (found_rak_eeprom) { - std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl; - exit(EXIT_FAILURE); - } - if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) { - std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")" - << std::endl; - exit(EXIT_FAILURE); - } - } - - if (loadConfig((portduino_config.available_directory + product_config).c_str())) { - std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl; - } else { - std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product - << std::endl; - exit(EXIT_FAILURE); - } - } else { - std::cerr << "autoconf: Could not locate any devices" << std::endl; - exit(EXIT_FAILURE); - } - } - - // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address - uint8_t dmac[6] = {0}; - if (portduino_config.lora_spi_dev == "ch341") { - try { - ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, - portduino_config.lora_usb_pid); - } catch (std::exception &e) { - std::cerr << e.what() << std::endl; - std::cerr << "Could not initialize CH341 device!" << std::endl; - exit(EXIT_FAILURE); - } - char serial[9] = {0}; - ch341Hal->getSerialString(serial, 8); - std::cout << "CH341 Serial " << serial << std::endl; - char product_string[96] = {0}; - ch341Hal->getProductString(product_string, 95); - std::cout << "CH341 Product " << product_string << std::endl; - if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { - uint8_t hash[32] = {0}; - memcpy(hash, serial, 8); - crypto->hash(hash, 8); - dmac[0] = (hash[0] << 4) | 2; - dmac[1] = hash[1]; - dmac[2] = hash[2]; - dmac[3] = hash[3]; - dmac[4] = hash[4]; - dmac[5] = hash[5]; - char macBuf[13] = {0}; - sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); - portduino_config.mac_address = macBuf; - } - } - - getMacAddr(dmac); -#ifndef UNIT_TEST - if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { - std::cout << "*** Blank MAC Address not allowed!" << std::endl; - std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; - exit(EXIT_FAILURE); - } -#endif - printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); - // Rather important to set this, if not running simulated. - randomSeed(time(NULL)); - - std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); - for (auto i : portduino_config.all_pins) { - if (i->enabled && i->pin > max_GPIO) - max_GPIO = i->pin; - } - - gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. - - // Need to bind all the configured GPIO pins so they're not simulated - // TODO: If one of these fails, we should log and terminate - for (auto i : portduino_config.all_pins) { - // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora - // Those GPIO are handled in our usermode driver instead. - if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { - continue; - } - if (i->enabled) { - if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { - printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); - exit(EXIT_FAILURE); - } - } - } - - // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware - if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { - SPI.begin(portduino_config.lora_spi_dev.c_str()); - } - - if (portduino_config.traceFilename != "") { - try { - traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); - } catch (std::ofstream::failure &e) { - std::cout << "*** traceFile Exception " << e.what() << std::endl; - exit(EXIT_FAILURE); - } - if (!traceFile.is_open()) { - std::cout << "*** traceFile open failure" << std::endl; - exit(EXIT_FAILURE); - } - } else if (portduino_config.JSONFilename != "") { - try { - JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); - } catch (std::ofstream::failure &e) { - std::cout << "*** JSONFile Exception " << e.what() << std::endl; - exit(EXIT_FAILURE); - } - if (!JSONFile.is_open()) { - std::cout << "*** JSONFile open failure" << std::endl; - exit(EXIT_FAILURE); - } - } - if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { - portduino_config.logoutputlevel = level_debug; - } + if (yamlOnly) { + std::cout << portduino_config.emit_yaml() << std::endl; + exit(EXIT_SUCCESS); + } + if (portduino_config.force_simradio) { + std::cout << "Running in simulated mode." << std::endl; + portduino_config.MaxNodes = 200; // Default to 200 nodes + // Set the random seed equal to TCPPort to have a different seed per instance + randomSeed(TCPPort); return; + } + + // If LoRa `Module: auto` (default in config.yaml), + // attempt to auto config based on Product Strings + if (portduino_config.lora_module == use_autoconf) { + bool found_hat = false; + bool found_rak_eeprom = false; + bool found_ch341 = false; + + char hat_vendor[96] = {0}; + char autoconf_product[96] = {0}; + // Try CH341 + try { + std::cout << "autoconf: Looking for CH341 device..." << std::endl; + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); + ch341Hal->getProductString(autoconf_product, 95); + delete ch341Hal; + std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; + + found_ch341 = true; + } catch (...) { + std::cout << "autoconf: Could not locate CH341 device" << std::endl; + } + // Try Pi HAT+ + if (strlen(autoconf_product) < 6) { + std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; + if (access("/proc/device-tree/hat/vendor", R_OK) == 0) { + std::ifstream hatVendorFile("/proc/device-tree/hat/vendor"); + if (hatVendorFile.is_open()) { + hatVendorFile.read(hat_vendor, 95); + hatVendorFile.close(); + } + } + if (access("/proc/device-tree/hat/product", R_OK) == 0) { + std::ifstream hatProductFile("/proc/device-tree/hat/product"); + if (hatProductFile.is_open()) { + hatProductFile.read(autoconf_product, 95); + hatProductFile.close(); + } + std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; + found_hat = true; + } else { + std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; + } + } + // attempt to load autoconf data from an EEPROM on 0x50 + // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 + // :mac address :<16 random unique bytes in hexidecimal> : crc32 + // crc32 is calculated on the eeprom string up to but not including the final colon + if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { + try { + char *mac_start = nullptr; + char *devID_start = nullptr; + char *crc32_start = nullptr; + Wire.begin(); + Wire.beginTransmission(0x50); + Wire.write(0x0); + Wire.write(0x0); + Wire.endTransmission(); + Wire.requestFrom((uint8_t)0x50, (uint8_t)75); + uint8_t i = 0; + delay(100); + std::string autoconf_raw; + while (Wire.available() && i < sizeof(autoconf_product)) { + autoconf_product[i] = Wire.read(); + if (autoconf_product[i] == 0xff) { + autoconf_product[i] = 0x0; + break; + } + autoconf_raw += autoconf_product[i]; + if (autoconf_product[i] == ':') { + autoconf_product[i] = 0x0; + if (mac_start == nullptr) { + mac_start = autoconf_product + i + 1; + } else if (devID_start == nullptr) { + devID_start = autoconf_product + i + 1; + } else if (crc32_start == nullptr) { + crc32_start = autoconf_product + i + 1; + } + } + i++; + } + if (crc32_start != nullptr && strlen(crc32_start) == 8) { + std::string crc32_str(crc32_start); + uint32_t crc32_value = 0; + + // convert crc32 ascii to raw uint32 + for (int j = 0; j < 4; j++) { + crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8; + } + std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl; + + // set the autoconf string to blank and short circuit + if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) { + std::cout << "autoconf: crc32 mismatch, dropping " << std::endl; + autoconf_product[0] = 0x0; + } else { + std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl; + found_rak_eeprom = true; + if (mac_start != nullptr) { + std::cout << "autoconf: Found mac data " << mac_start << std::endl; + if (strlen(mac_start) == 12) + portduino_config.mac_address = std::string(mac_start); + } + if (devID_start != nullptr) { + std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; + if (strlen(devID_start) == 32) { + std::string devID_str(devID_start); + for (int j = 0; j < 16; j++) { + portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16); + } + portduino_config.has_device_id = true; + } + } + } + } else { + std::cout << "autoconf: crc32 missing " << std::endl; + autoconf_product[0] = 0x0; + } + } catch (...) { + std::cout << "autoconf: Could not locate EEPROM" << std::endl; + } + } + // Load the config file based on the product string + if (strlen(autoconf_product) > 0) { + // From configProducts map in PortduinoGlue.h + std::string product_config = ""; + + if (configProducts.find(autoconf_product) != configProducts.end()) { + product_config = configProducts.at(autoconf_product); + } else { + if (found_hat) { + product_config = cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); + } else if (found_ch341) { + product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); + } + + // Don't try to automatically find config for a device with RAK eeprom. + if (found_rak_eeprom) { + std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl; + exit(EXIT_FAILURE); + } + if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) { + std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")" << std::endl; + exit(EXIT_FAILURE); + } + } + + if (loadConfig((portduino_config.available_directory + product_config).c_str())) { + std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl; + } else { + std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product << std::endl; + exit(EXIT_FAILURE); + } + } else { + std::cerr << "autoconf: Could not locate any devices" << std::endl; + exit(EXIT_FAILURE); + } + } + + // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address + uint8_t dmac[6] = {0}; + if (portduino_config.lora_spi_dev == "ch341") { + try { + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); + } catch (std::exception &e) { + std::cerr << e.what() << std::endl; + std::cerr << "Could not initialize CH341 device!" << std::endl; + exit(EXIT_FAILURE); + } + char serial[9] = {0}; + ch341Hal->getSerialString(serial, 8); + std::cout << "CH341 Serial " << serial << std::endl; + char product_string[96] = {0}; + ch341Hal->getProductString(product_string, 95); + std::cout << "CH341 Product " << product_string << std::endl; + if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { + uint8_t hash[32] = {0}; + memcpy(hash, serial, 8); + crypto->hash(hash, 8); + dmac[0] = (hash[0] << 4) | 2; + dmac[1] = hash[1]; + dmac[2] = hash[2]; + dmac[3] = hash[3]; + dmac[4] = hash[4]; + dmac[5] = hash[5]; + char macBuf[13] = {0}; + sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); + portduino_config.mac_address = macBuf; + } + } + + getMacAddr(dmac); +#ifndef UNIT_TEST + if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { + std::cout << "*** Blank MAC Address not allowed!" << std::endl; + std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; + exit(EXIT_FAILURE); + } +#endif + printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); + // Rather important to set this, if not running simulated. + randomSeed(time(NULL)); + + std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); + for (auto i : portduino_config.all_pins) { + if (i->enabled && i->pin > max_GPIO) + max_GPIO = i->pin; + } + + gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. + + // Need to bind all the configured GPIO pins so they're not simulated + // TODO: If one of these fails, we should log and terminate + for (auto i : portduino_config.all_pins) { + // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora + // Those GPIO are handled in our usermode driver instead. + if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { + continue; + } + if (i->enabled) { + if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); + exit(EXIT_FAILURE); + } + } + } + + // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware + if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { + SPI.begin(portduino_config.lora_spi_dev.c_str()); + } + + if (portduino_config.traceFilename != "") { + try { + traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** traceFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!traceFile.is_open()) { + std::cout << "*** traceFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } else if (portduino_config.JSONFilename != "") { + try { + JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** JSONFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!JSONFile.is_open()) { + std::cout << "*** JSONFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } + if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { + portduino_config.logoutputlevel = level_debug; + } + + return; } -int initGPIOPin(int pinNum, const std::string gpioChipName, int line) -{ +int initGPIOPin(int pinNum, const std::string gpioChipName, int line) { #ifdef PORTDUINO_LINUX_HARDWARE - std::string gpio_name = "GPIO" + std::to_string(pinNum); - std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl; - try { - GPIOPin *csPin; - csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); - csPin->setSilent(); - gpioBind(csPin); - return ERRNO_OK; - } catch (...) { - const std::type_info *t = abi::__cxa_current_exception_type(); - std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl; - return ERRNO_DISABLED; - } -#else + std::string gpio_name = "GPIO" + std::to_string(pinNum); + std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl; + try { + GPIOPin *csPin; + csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); + csPin->setSilent(); + gpioBind(csPin); return ERRNO_OK; + } catch (...) { + const std::type_info *t = abi::__cxa_current_exception_type(); + std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl; + return ERRNO_DISABLED; + } +#else + return ERRNO_OK; #endif } -bool loadConfig(const char *configPath) -{ - YAML::Node yamlConfig; - try { - yamlConfig = YAML::LoadFile(configPath); - if (yamlConfig["Logging"]) { - if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") { - portduino_config.logoutputlevel = level_trace; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { - portduino_config.logoutputlevel = level_debug; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { - portduino_config.logoutputlevel = level_info; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { - portduino_config.logoutputlevel = level_warn; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { - portduino_config.logoutputlevel = level_error; - } - portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); - portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); - portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); - if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") - portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") - portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") - portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") - portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") - portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") - portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") - portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") - portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") - portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") - portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; +bool loadConfig(const char *configPath) { + YAML::Node yamlConfig; + try { + yamlConfig = YAML::LoadFile(configPath); + if (yamlConfig["Logging"]) { + if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") { + portduino_config.logoutputlevel = level_trace; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { + portduino_config.logoutputlevel = level_debug; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { + portduino_config.logoutputlevel = level_info; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { + portduino_config.logoutputlevel = level_warn; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { + portduino_config.logoutputlevel = level_error; + } + portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); + portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); + if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") + portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") + portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") + portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") + portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") + portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") + portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") + portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") + portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; - if (yamlConfig["Logging"]["AsciiLogs"]) { - // Default is !isatty(1) but can be set explicitly in config.yaml - portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); - portduino_config.ascii_logs_explicit = true; - } - } - if (yamlConfig["Lora"]) { - - if (yamlConfig["Lora"]["Module"]) { - for (auto &loraModule : portduino_config.loraModules) { - if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { - portduino_config.lora_module = loraModule.first; - break; - } - } - } - if (yamlConfig["Lora"]["SX126X_MAX_POWER"]) - portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); - if (yamlConfig["Lora"]["SX128X_MAX_POWER"]) - portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); - if (yamlConfig["Lora"]["LR1110_MAX_POWER"]) - portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); - if (yamlConfig["Lora"]["LR1120_MAX_POWER"]) - portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); - if (yamlConfig["Lora"]["RF95_MAX_POWER"]) - portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); - - if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && - !portduino_config.force_simradio) { - portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); - portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; - if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { - portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true" - } - - // backwards API compatibility and to globally set gpiochip once - portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0); - for (auto this_pin : portduino_config.all_pins) { - if (this_pin->config_section == "Lora") { - readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin); - } - } - } - - portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); - portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); - portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); - portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86); - - portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0"); - if (portduino_config.lora_spi_dev != "ch341") { - portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev; - if (portduino_config.lora_spi_dev.length() == 14) { - int x = portduino_config.lora_spi_dev.at(11) - '0'; - int y = portduino_config.lora_spi_dev.at(13) - '0'; - // Pretty sure this is always true - if (x >= 0 && x < 10 && y >= 0 && y < 10) { - // I believe this bit of weirdness is specifically for the new GUI - portduino_config.lora_spi_dev_int = x + y << 4; - portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int; - portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int; - } - } - } - if (yamlConfig["Lora"]["rfswitch_table"]) { - portduino_config.has_rfswitch_table = true; - portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY; - portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX; - portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX; - portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP; - portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF; - portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS; - portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI; - portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE; - - for (int i = 0; i < 5; i++) { - - // set up the pin array first - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10; - - // now fill in the table - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH") - portduino_config.rfswitch_table[0].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH") - portduino_config.rfswitch_table[1].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH") - portduino_config.rfswitch_table[2].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH") - portduino_config.rfswitch_table[3].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH") - portduino_config.rfswitch_table[4].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH") - portduino_config.rfswitch_table[5].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH") - portduino_config.rfswitch_table[6].values[i] = HIGH; - } - } - } - readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin); - if (yamlConfig["GPS"]) { - std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); - if (serialPath != "") { - Serial1.setPath(serialPath); - portduino_config.has_gps = 1; - } - } - if (yamlConfig["I2C"]) { - portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); - } - if (yamlConfig["Display"]) { - - for (auto &screen_name : portduino_config.screen_names) { - if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) - portduino_config.displayPanel = screen_name.first; - } - portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0); - portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0); - - readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1); - readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1); - readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1); - readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1); - readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1); - - portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false); - portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false); - portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0); - portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0); - portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false); - portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1); - portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false); - portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000); - if (yamlConfig["Display"]["spidev"]) { - portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); - if (portduino_config.display_spi_dev.length() == 14) { - int x = portduino_config.display_spi_dev.at(11) - '0'; - int y = portduino_config.display_spi_dev.at(13) - '0'; - if (x >= 0 && x < 10 && y >= 0 && y < 10) { - portduino_config.display_spi_dev_int = x + y << 4; - portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int; - } - } - } - } - if (yamlConfig["Touchscreen"]) { - if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") - portduino_config.touchscreenModule = xpt2046; - else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") - portduino_config.touchscreenModule = stmpe610; - else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") - portduino_config.touchscreenModule = gt911; - else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") - portduino_config.touchscreenModule = ft5x06; - - readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1); - readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1); - - portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); - portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1); - portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); - if (yamlConfig["Touchscreen"]["spidev"]) { - portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); - if (portduino_config.touchscreen_spi_dev.length() == 14) { - int x = portduino_config.touchscreen_spi_dev.at(11) - '0'; - int y = portduino_config.touchscreen_spi_dev.at(13) - '0'; - if (x >= 0 && x < 10 && y >= 0 && y < 10) { - portduino_config.touchscreen_spi_dev_int = x + y << 4; - } - } - } - } - if (yamlConfig["Input"]) { - portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as(""); - portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as(""); - - readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin); - - if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { - portduino_config.tbDirection = 4; - } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { - portduino_config.tbDirection = 3; - } - } - - if (yamlConfig["Webserver"]) { - portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1); - portduino_config.webserver_root_path = - (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); - portduino_config.webserver_ssl_key_path = - (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); - portduino_config.webserver_ssl_cert_path = - (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); - } - - if (yamlConfig["HostMetrics"]) { - portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0); - portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); - portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); - } - - if (yamlConfig["Config"]) { - if (yamlConfig["Config"]["DisplayMode"]) { - portduino_config.has_configDisplayMode = true; - if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; - } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; - } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - } else { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - } - } - } - - if (yamlConfig["General"]) { - portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200); - portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100); - portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as(""); - portduino_config.available_directory = - (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/"); - if ((yamlConfig["General"]["MACAddress"]).as("") != "" && - (yamlConfig["General"]["MACAddressSource"]).as("") != "") { - std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; - exit(EXIT_FAILURE); - } - portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); - if (portduino_config.mac_address != "") { - portduino_config.mac_address_explicit = true; - } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { - portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as(""); - std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address"); - std::getline(infile, portduino_config.mac_address); - } - - // https://stackoverflow.com/a/20326454 - portduino_config.mac_address.erase( - std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'), - portduino_config.mac_address.end()); - } - } catch (YAML::Exception &e) { - std::cout << "*** Exception " << e.what() << std::endl; - return false; + if (yamlConfig["Logging"]["AsciiLogs"]) { + // Default is !isatty(1) but can be set explicitly in config.yaml + portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); + portduino_config.ascii_logs_explicit = true; + } } - return true; + if (yamlConfig["Lora"]) { + + if (yamlConfig["Lora"]["Module"]) { + for (auto &loraModule : portduino_config.loraModules) { + if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { + portduino_config.lora_module = loraModule.first; + break; + } + } + } + if (yamlConfig["Lora"]["SX126X_MAX_POWER"]) + portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); + if (yamlConfig["Lora"]["SX128X_MAX_POWER"]) + portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); + if (yamlConfig["Lora"]["LR1110_MAX_POWER"]) + portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); + if (yamlConfig["Lora"]["LR1120_MAX_POWER"]) + portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); + if (yamlConfig["Lora"]["RF95_MAX_POWER"]) + portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); + + if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && !portduino_config.force_simradio) { + portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); + portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; + if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { + portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true" + } + + // backwards API compatibility and to globally set gpiochip once + portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0); + for (auto this_pin : portduino_config.all_pins) { + if (this_pin->config_section == "Lora") { + readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin); + } + } + } + + portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); + portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); + portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); + portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86); + + portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0"); + if (portduino_config.lora_spi_dev != "ch341") { + portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev; + if (portduino_config.lora_spi_dev.length() == 14) { + int x = portduino_config.lora_spi_dev.at(11) - '0'; + int y = portduino_config.lora_spi_dev.at(13) - '0'; + // Pretty sure this is always true + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + // I believe this bit of weirdness is specifically for the new GUI + portduino_config.lora_spi_dev_int = x + y << 4; + portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int; + portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int; + } + } + } + if (yamlConfig["Lora"]["rfswitch_table"]) { + portduino_config.has_rfswitch_table = true; + portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY; + portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX; + portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX; + portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP; + portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF; + portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS; + portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI; + portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE; + + for (int i = 0; i < 5; i++) { + + // set up the pin array first + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10; + + // now fill in the table + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH") + portduino_config.rfswitch_table[0].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH") + portduino_config.rfswitch_table[1].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH") + portduino_config.rfswitch_table[2].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH") + portduino_config.rfswitch_table[3].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH") + portduino_config.rfswitch_table[4].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH") + portduino_config.rfswitch_table[5].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH") + portduino_config.rfswitch_table[6].values[i] = HIGH; + } + } + } + readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin); + if (yamlConfig["GPS"]) { + std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); + if (serialPath != "") { + Serial1.setPath(serialPath); + portduino_config.has_gps = 1; + } + } + if (yamlConfig["I2C"]) { + portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); + } + if (yamlConfig["Display"]) { + + for (auto &screen_name : portduino_config.screen_names) { + if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) + portduino_config.displayPanel = screen_name.first; + } + portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0); + portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0); + + readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1); + readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1); + readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1); + readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1); + readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1); + + portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false); + portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false); + portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0); + portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0); + portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false); + portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1); + portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false); + portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000); + if (yamlConfig["Display"]["spidev"]) { + portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); + if (portduino_config.display_spi_dev.length() == 14) { + int x = portduino_config.display_spi_dev.at(11) - '0'; + int y = portduino_config.display_spi_dev.at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + portduino_config.display_spi_dev_int = x + y << 4; + portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int; + } + } + } + } + if (yamlConfig["Touchscreen"]) { + if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") + portduino_config.touchscreenModule = xpt2046; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") + portduino_config.touchscreenModule = stmpe610; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") + portduino_config.touchscreenModule = gt911; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") + portduino_config.touchscreenModule = ft5x06; + + readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1); + readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1); + + portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); + portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1); + portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); + if (yamlConfig["Touchscreen"]["spidev"]) { + portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); + if (portduino_config.touchscreen_spi_dev.length() == 14) { + int x = portduino_config.touchscreen_spi_dev.at(11) - '0'; + int y = portduino_config.touchscreen_spi_dev.at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + portduino_config.touchscreen_spi_dev_int = x + y << 4; + } + } + } + } + if (yamlConfig["Input"]) { + portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as(""); + portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as(""); + + readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin); + + if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { + portduino_config.tbDirection = 4; + } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { + portduino_config.tbDirection = 3; + } + } + + if (yamlConfig["Webserver"]) { + portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1); + portduino_config.webserver_root_path = (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); + portduino_config.webserver_ssl_key_path = (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); + portduino_config.webserver_ssl_cert_path = (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); + } + + if (yamlConfig["HostMetrics"]) { + portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0); + portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); + portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); + } + + if (yamlConfig["Config"]) { + if (yamlConfig["Config"]["DisplayMode"]) { + portduino_config.has_configDisplayMode = true; + if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; + } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + } else { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + } + } + } + + if (yamlConfig["General"]) { + portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200); + portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100); + portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as(""); + portduino_config.available_directory = (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/"); + if ((yamlConfig["General"]["MACAddress"]).as("") != "" && (yamlConfig["General"]["MACAddressSource"]).as("") != "") { + std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; + exit(EXIT_FAILURE); + } + portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); + if (portduino_config.mac_address != "") { + portduino_config.mac_address_explicit = true; + } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { + portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as(""); + std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address"); + std::getline(infile, portduino_config.mac_address); + } + + // https://stackoverflow.com/a/20326454 + portduino_config.mac_address.erase(std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'), + portduino_config.mac_address.end()); + } + } catch (YAML::Exception &e) { + std::cout << "*** Exception " << e.what() << std::endl; + return false; + } + return true; } // https://stackoverflow.com/questions/874134/find-out-if-string-ends-with-another-string-in-c -static bool ends_with(std::string_view str, std::string_view suffix) -{ - return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +static bool ends_with(std::string_view str, std::string_view suffix) { + return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; } -bool MAC_from_string(std::string mac_str, uint8_t *dmac) -{ - mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end()); - if (mac_str.length() == 12) { - dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16); - dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16); - dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16); - dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16); - dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16); - dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16); - return true; - } else { - return false; - } +bool MAC_from_string(std::string mac_str, uint8_t *dmac) { + mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end()); + if (mac_str.length() == 12) { + dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16); + dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16); + dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16); + dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16); + dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16); + dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16); + return true; + } else { + return false; + } } -std::string exec(const char *cmd) -{ // https://stackoverflow.com/a/478960 - std::array buffer; - std::string result; - std::unique_ptr pipe(popen(cmd, "r"), pclose); - if (!pipe) { - throw std::runtime_error("popen() failed!"); - } - while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { - result += buffer.data(); - } - return result; +std::string exec(const char *cmd) { // https://stackoverflow.com/a/478960 + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; } -void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault) -{ - if (sourceNode.IsMap()) { - destPin.enabled = true; - destPin.pin = sourceNode["pin"].as(pinDefault); - destPin.line = sourceNode["line"].as(destPin.pin); - destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip); - } else if (sourceNode) { // backwards API compatibility - destPin.enabled = true; - destPin.pin = sourceNode.as(pinDefault); - destPin.line = destPin.pin; - destPin.gpiochip = portduino_config.lora_default_gpiochip; - } +void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault) { + if (sourceNode.IsMap()) { + destPin.enabled = true; + destPin.pin = sourceNode["pin"].as(pinDefault); + destPin.line = sourceNode["line"].as(destPin.pin); + destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip); + } else if (sourceNode) { // backwards API compatibility + destPin.enabled = true; + destPin.pin = sourceNode.as(pinDefault); + destPin.line = destPin.pin; + destPin.gpiochip = portduino_config.lora_default_gpiochip; + } } diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 9335be90a..46bf30a9f 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -12,38 +12,26 @@ // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} // YAML paths are relative to `meshtastic/available.d` -inline const std::unordered_map configProducts = { - {"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, - {"MESHSTICK", "lora-meshstick-1262.yaml"}, - {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, - {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, - {"POWERPI", "lora-MeshAdv-900M30S.yaml"}, - {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, - {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; +inline const std::unordered_map configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, + {"MESHSTICK", "lora-meshstick-1262.yaml"}, + {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, + {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, + {"POWERPI", "lora-MeshAdv-900M30S.yaml"}, + {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, + {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; enum screen_modules { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; enum touchscreen_modules { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; enum portduino_log_level { level_error, level_warn, level_info, level_debug, level_trace }; -enum lora_module_enum { - use_simradio, - use_autoconf, - use_rf95, - use_sx1262, - use_sx1268, - use_sx1280, - use_lr1110, - use_lr1120, - use_lr1121, - use_llcc68 -}; +enum lora_module_enum { use_simradio, use_autoconf, use_rf95, use_sx1262, use_sx1268, use_sx1280, use_lr1110, use_lr1120, use_lr1121, use_llcc68 }; struct pinMapping { - std::string config_section; - std::string config_name; - int pin = RADIOLIB_NC; - int gpiochip; - int line; - bool enabled = false; + std::string config_section; + std::string config_name; + int pin = RADIOLIB_NC; + int gpiochip; + int line; + bool enabled = false; }; extern std::ofstream traceFile; @@ -59,448 +47,446 @@ void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault std::string exec(const char *cmd); extern struct portduino_config_struct { + // Lora + std::map loraModules = { + {use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, + {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; + + std::map screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"}, {st7735, "ST7735"}, + {st7735s, "ST7735S"}, {st7796, "ST7796"}, {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, + {ili9486, "ILI9486"}, {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}}; + + lora_module_enum lora_module; + bool has_rfswitch_table = false; + uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + Module::RfSwitchMode_t rfswitch_table[8]; + bool force_simradio = false; + bool has_device_id = false; + uint8_t device_id[16] = {0}; + std::string lora_spi_dev = ""; + std::string lora_usb_serial_num = ""; + int lora_spi_dev_int = 0; + int lora_default_gpiochip = 0; + int sx126x_max_power = 22; + int sx128x_max_power = 13; + int lr1110_max_power = 22; + int lr1120_max_power = 13; + int rf95_max_power = 20; + bool dio2_as_rf_switch = false; + int dio3_tcxo_voltage = 0; + int lora_usb_pid = 0x5512; + int lora_usb_vid = 0x1A86; + int spiSpeed = 2000000; + pinMapping lora_cs_pin = {"Lora", "CS"}; + pinMapping lora_irq_pin = {"Lora", "IRQ"}; + pinMapping lora_busy_pin = {"Lora", "Busy"}; + pinMapping lora_reset_pin = {"Lora", "Reset"}; + pinMapping lora_txen_pin = {"Lora", "TXen"}; + pinMapping lora_rxen_pin = {"Lora", "RXen"}; + pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + + // GPS + bool has_gps = false; + + // I2C + std::string i2cdev = ""; + + // Display + std::string display_spi_dev = ""; + int display_spi_dev_int = 0; + int displayBusFrequency = 40000000; + screen_modules displayPanel = no_screen; + int displayWidth = 0; + int displayHeight = 0; + bool displayRGBOrder = false; + bool displayBacklightInvert = false; + bool displayRotate = false; + int displayOffsetRotate = 1; + bool displayInvert = false; + int displayOffsetX = 0; + int displayOffsetY = 0; + pinMapping displayDC = {"Display", "DC"}; + pinMapping displayCS = {"Display", "CS"}; + pinMapping displayBacklight = {"Display", "Backlight"}; + pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"}; + pinMapping displayReset = {"Display", "Reset"}; + + // Touchscreen + std::string touchscreen_spi_dev = ""; + int touchscreen_spi_dev_int = 0; + touchscreen_modules touchscreenModule = no_touchscreen; + int touchscreenI2CAddr = -1; + int touchscreenBusFrequency = 1000000; + int touchscreenRotate = -1; + pinMapping touchscreenCS = {"Touchscreen", "CS"}; + pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"}; + + // Input + std::string keyboardDevice = ""; + std::string pointerDevice = ""; + int tbDirection; + pinMapping userButtonPin = {"Input", "User"}; + pinMapping tbUpPin = {"Input", "TrackballUp"}; + pinMapping tbDownPin = {"Input", "TrackballDown"}; + pinMapping tbLeftPin = {"Input", "TrackballLwft"}; + pinMapping tbRightPin = {"Input", "TrackballRight"}; + pinMapping tbPressPin = {"Input", "TrackballPress"}; + + // Logging + portduino_log_level logoutputlevel = level_debug; + std::string traceFilename; + bool ascii_logs = !isatty(1); + bool ascii_logs_explicit = false; + + std::string JSONFilename; + meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; + + // Webserver + std::string webserver_root_path = ""; + std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; + std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem"; + int webserverport = -1; + + // HostMetrics + std::string hostMetrics_user_command = ""; + int hostMetrics_interval = 0; + int hostMetrics_channel = 0; + + // config + int configDisplayMode = 0; + bool has_configDisplayMode = false; + + // General + std::string mac_address = ""; + bool mac_address_explicit = false; + std::string mac_address_source = ""; + std::string config_directory = ""; + std::string available_directory = "/etc/meshtasticd/available.d/"; + int maxtophone = 100; + int MaxNodes = 200; + + pinMapping *all_pins[20] = {&lora_cs_pin, + &lora_irq_pin, + &lora_busy_pin, + &lora_reset_pin, + &lora_txen_pin, + &lora_rxen_pin, + &lora_sx126x_ant_sw_pin, + &displayDC, + &displayCS, + &displayBacklight, + &displayBacklightPWMChannel, + &displayReset, + &touchscreenCS, + &touchscreenIRQ, + &userButtonPin, + &tbUpPin, + &tbDownPin, + &tbLeftPin, + &tbRightPin, + &tbPressPin}; + + std::string emit_yaml() { + YAML::Emitter out; + out << YAML::BeginMap; + // Lora - std::map loraModules = { - {use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, - {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; + out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; - std::map screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"}, - {st7735, "ST7735"}, {st7735s, "ST7735S"}, {st7796, "ST7796"}, - {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, {ili9486, "ILI9486"}, - {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}}; + for (auto lora_pin : all_pins) { + if (lora_pin->config_section == "Lora" && lora_pin->enabled) { + out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; + out << YAML::Key << "line" << YAML::Value << lora_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip; + out << YAML::EndMap; // User + } + } - lora_module_enum lora_module; - bool has_rfswitch_table = false; - uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; - Module::RfSwitchMode_t rfswitch_table[8]; - bool force_simradio = false; - bool has_device_id = false; - uint8_t device_id[16] = {0}; - std::string lora_spi_dev = ""; - std::string lora_usb_serial_num = ""; - int lora_spi_dev_int = 0; - int lora_default_gpiochip = 0; - int sx126x_max_power = 22; - int sx128x_max_power = 13; - int lr1110_max_power = 22; - int lr1120_max_power = 13; - int rf95_max_power = 20; - bool dio2_as_rf_switch = false; - int dio3_tcxo_voltage = 0; - int lora_usb_pid = 0x5512; - int lora_usb_vid = 0x1A86; - int spiSpeed = 2000000; - pinMapping lora_cs_pin = {"Lora", "CS"}; - pinMapping lora_irq_pin = {"Lora", "IRQ"}; - pinMapping lora_busy_pin = {"Lora", "Busy"}; - pinMapping lora_reset_pin = {"Lora", "Reset"}; - pinMapping lora_txen_pin = {"Lora", "TXen"}; - pinMapping lora_rxen_pin = {"Lora", "RXen"}; - pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + if (sx126x_max_power != 22) + out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power; + if (sx128x_max_power != 13) + out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power; + if (lr1110_max_power != 22) + out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power; + if (lr1120_max_power != 13) + out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; + if (rf95_max_power != 20) + out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; + out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; + if (dio3_tcxo_voltage != 0) + out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; + if (lora_usb_pid != 0x5512) + out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; + if (lora_usb_vid != 0x1A86) + out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; + if (lora_spi_dev != "") + out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; + if (lora_usb_serial_num != "") + out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; + out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; + if (rfswitch_dio_pins[0] != RADIOLIB_NC) { + out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; - // GPS - bool has_gps = false; + out << YAML::Key << "pins"; + out << YAML::Value << YAML::Flow << YAML::BeginSeq; - // I2C - std::string i2cdev = ""; + for (int i = 0; i < 5; i++) { + // set up the pin array first + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5) + out << "DIO5"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6) + out << "DIO6"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7) + out << "DIO7"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8) + out << "DIO8"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10) + out << "DIO10"; + } + out << YAML::EndSeq; + + for (int i = 0; i < 7; i++) { + switch (i) { + case 0: + out << YAML::Key << "MODE_STBY"; + break; + case 1: + out << YAML::Key << "MODE_RX"; + break; + case 2: + out << YAML::Key << "MODE_TX"; + break; + case 3: + out << YAML::Key << "MODE_TX_HP"; + break; + case 4: + out << YAML::Key << "MODE_TX_HF"; + break; + case 5: + out << YAML::Key << "MODE_GNSS"; + break; + case 6: + out << YAML::Key << "MODE_WIFI"; + break; + } + + out << YAML::Value << YAML::Flow << YAML::BeginSeq; + for (int j = 0; j < 5; j++) { + if (rfswitch_table[i].values[j] == HIGH) { + out << "HIGH"; + } else { + out << "LOW"; + } + } + out << YAML::EndSeq; + } + out << YAML::EndMap; // rfswitch_table + } + out << YAML::EndMap; // Lora + + if (i2cdev != "") { + out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; + out << YAML::EndMap; // I2C + } // Display - std::string display_spi_dev = ""; - int display_spi_dev_int = 0; - int displayBusFrequency = 40000000; - screen_modules displayPanel = no_screen; - int displayWidth = 0; - int displayHeight = 0; - bool displayRGBOrder = false; - bool displayBacklightInvert = false; - bool displayRotate = false; - int displayOffsetRotate = 1; - bool displayInvert = false; - int displayOffsetX = 0; - int displayOffsetY = 0; - pinMapping displayDC = {"Display", "DC"}; - pinMapping displayCS = {"Display", "CS"}; - pinMapping displayBacklight = {"Display", "Backlight"}; - pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"}; - pinMapping displayReset = {"Display", "Reset"}; + if (displayPanel != no_screen) { + out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; + for (auto &screen_name : screen_names) { + if (displayPanel == screen_name.first) + out << YAML::Key << "Module" << YAML::Value << screen_name.second; + } + for (auto display_pin : all_pins) { + if (display_pin->config_section == "Display" && display_pin->enabled) { + out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << display_pin->pin; + out << YAML::Key << "line" << YAML::Value << display_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip; + out << YAML::EndMap; + } + } + out << YAML::Key << "spidev" << YAML::Value << display_spi_dev; + out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency; + if (displayWidth) + out << YAML::Key << "Width" << YAML::Value << displayWidth; + if (displayHeight) + out << YAML::Key << "Height" << YAML::Value << displayHeight; + if (displayRGBOrder) + out << YAML::Key << "RGBOrder" << YAML::Value << true; + if (displayBacklightInvert) + out << YAML::Key << "BacklightInvert" << YAML::Value << true; + if (displayRotate) + out << YAML::Key << "Rotate" << YAML::Value << true; + if (displayInvert) + out << YAML::Key << "Invert" << YAML::Value << true; + if (displayOffsetX) + out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX; + if (displayOffsetY) + out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY; + + out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate; + + out << YAML::EndMap; // Display + } // Touchscreen - std::string touchscreen_spi_dev = ""; - int touchscreen_spi_dev_int = 0; - touchscreen_modules touchscreenModule = no_touchscreen; - int touchscreenI2CAddr = -1; - int touchscreenBusFrequency = 1000000; - int touchscreenRotate = -1; - pinMapping touchscreenCS = {"Touchscreen", "CS"}; - pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"}; + if (touchscreen_spi_dev != "") { + out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev; + out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency; + switch (touchscreenModule) { + case xpt2046: + out << YAML::Key << "Module" << YAML::Value << "XPT2046"; + case stmpe610: + out << YAML::Key << "Module" << YAML::Value << "STMPE610"; + case gt911: + out << YAML::Key << "Module" << YAML::Value << "GT911"; + case ft5x06: + out << YAML::Key << "Module" << YAML::Value << "FT5x06"; + } + for (auto touchscreen_pin : all_pins) { + if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { + out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; + out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip; + out << YAML::EndMap; + } + } + if (touchscreenRotate != -1) + out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate; + if (touchscreenI2CAddr != -1) + out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr; + out << YAML::EndMap; // Touchscreen + } // Input - std::string keyboardDevice = ""; - std::string pointerDevice = ""; - int tbDirection; - pinMapping userButtonPin = {"Input", "User"}; - pinMapping tbUpPin = {"Input", "TrackballUp"}; - pinMapping tbDownPin = {"Input", "TrackballDown"}; - pinMapping tbLeftPin = {"Input", "TrackballLwft"}; - pinMapping tbRightPin = {"Input", "TrackballRight"}; - pinMapping tbPressPin = {"Input", "TrackballPress"}; + out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap; + if (keyboardDevice != "") + out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice; + if (pointerDevice != "") + out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; - // Logging - portduino_log_level logoutputlevel = level_debug; - std::string traceFilename; - bool ascii_logs = !isatty(1); - bool ascii_logs_explicit = false; + for (auto input_pin : all_pins) { + if (input_pin->config_section == "Input" && input_pin->enabled) { + out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << input_pin->pin; + out << YAML::Key << "line" << YAML::Value << input_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip; + out << YAML::EndMap; + } + } + if (tbDirection == 3) + out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING"; - std::string JSONFilename; - meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; + out << YAML::EndMap; // Input + + out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "LogLevel" << YAML::Value; + switch (logoutputlevel) { + case level_error: + out << "error"; + break; + case level_warn: + out << "warn"; + break; + case level_info: + out << "info"; + break; + case level_debug: + out << "debug"; + break; + case level_trace: + out << "trace"; + break; + } + if (traceFilename != "") + out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; + if (JSONFilename != "") { + out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; + else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; + else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; + else if (JSONFilter == meshtastic_PortNum_POSITION_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "position"; + else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; + else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; + else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; + else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; + else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; + else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; + } + if (ascii_logs_explicit) { + out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; + } + out << YAML::EndMap; // Logging // Webserver - std::string webserver_root_path = ""; - std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; - std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem"; - int webserverport = -1; + if (webserver_root_path != "") { + out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path; + out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path; + out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path; + out << YAML::Key << "Port" << YAML::Value << webserverport; + out << YAML::EndMap; // Webserver + } // HostMetrics - std::string hostMetrics_user_command = ""; - int hostMetrics_interval = 0; - int hostMetrics_channel = 0; + if (hostMetrics_user_command != "") { + out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command; + out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval; + out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel; + + out << YAML::EndMap; // HostMetrics + } // config - int configDisplayMode = 0; - bool has_configDisplayMode = false; + if (has_configDisplayMode) { + out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; + switch (configDisplayMode) { + case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: + out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: + out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; + break; + } + + out << YAML::EndMap; // Config + } // General - std::string mac_address = ""; - bool mac_address_explicit = false; - std::string mac_address_source = ""; - std::string config_directory = ""; - std::string available_directory = "/etc/meshtasticd/available.d/"; - int maxtophone = 100; - int MaxNodes = 200; - - pinMapping *all_pins[20] = {&lora_cs_pin, - &lora_irq_pin, - &lora_busy_pin, - &lora_reset_pin, - &lora_txen_pin, - &lora_rxen_pin, - &lora_sx126x_ant_sw_pin, - &displayDC, - &displayCS, - &displayBacklight, - &displayBacklightPWMChannel, - &displayReset, - &touchscreenCS, - &touchscreenIRQ, - &userButtonPin, - &tbUpPin, - &tbDownPin, - &tbLeftPin, - &tbRightPin, - &tbPressPin}; - - std::string emit_yaml() - { - YAML::Emitter out; - out << YAML::BeginMap; - - // Lora - out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; - - for (auto lora_pin : all_pins) { - if (lora_pin->config_section == "Lora" && lora_pin->enabled) { - out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; - out << YAML::Key << "line" << YAML::Value << lora_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip; - out << YAML::EndMap; // User - } - } - - if (sx126x_max_power != 22) - out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power; - if (sx128x_max_power != 13) - out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power; - if (lr1110_max_power != 22) - out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power; - if (lr1120_max_power != 13) - out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; - if (rf95_max_power != 20) - out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; - out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; - if (dio3_tcxo_voltage != 0) - out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; - if (lora_usb_pid != 0x5512) - out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; - if (lora_usb_vid != 0x1A86) - out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; - if (lora_spi_dev != "") - out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; - if (lora_usb_serial_num != "") - out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; - out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; - if (rfswitch_dio_pins[0] != RADIOLIB_NC) { - out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; - - out << YAML::Key << "pins"; - out << YAML::Value << YAML::Flow << YAML::BeginSeq; - - for (int i = 0; i < 5; i++) { - // set up the pin array first - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5) - out << "DIO5"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6) - out << "DIO6"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7) - out << "DIO7"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8) - out << "DIO8"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10) - out << "DIO10"; - } - out << YAML::EndSeq; - - for (int i = 0; i < 7; i++) { - switch (i) { - case 0: - out << YAML::Key << "MODE_STBY"; - break; - case 1: - out << YAML::Key << "MODE_RX"; - break; - case 2: - out << YAML::Key << "MODE_TX"; - break; - case 3: - out << YAML::Key << "MODE_TX_HP"; - break; - case 4: - out << YAML::Key << "MODE_TX_HF"; - break; - case 5: - out << YAML::Key << "MODE_GNSS"; - break; - case 6: - out << YAML::Key << "MODE_WIFI"; - break; - } - - out << YAML::Value << YAML::Flow << YAML::BeginSeq; - for (int j = 0; j < 5; j++) { - if (rfswitch_table[i].values[j] == HIGH) { - out << "HIGH"; - } else { - out << "LOW"; - } - } - out << YAML::EndSeq; - } - out << YAML::EndMap; // rfswitch_table - } - out << YAML::EndMap; // Lora - - if (i2cdev != "") { - out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; - out << YAML::EndMap; // I2C - } - - // Display - if (displayPanel != no_screen) { - out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; - for (auto &screen_name : screen_names) { - if (displayPanel == screen_name.first) - out << YAML::Key << "Module" << YAML::Value << screen_name.second; - } - for (auto display_pin : all_pins) { - if (display_pin->config_section == "Display" && display_pin->enabled) { - out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << display_pin->pin; - out << YAML::Key << "line" << YAML::Value << display_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip; - out << YAML::EndMap; - } - } - out << YAML::Key << "spidev" << YAML::Value << display_spi_dev; - out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency; - if (displayWidth) - out << YAML::Key << "Width" << YAML::Value << displayWidth; - if (displayHeight) - out << YAML::Key << "Height" << YAML::Value << displayHeight; - if (displayRGBOrder) - out << YAML::Key << "RGBOrder" << YAML::Value << true; - if (displayBacklightInvert) - out << YAML::Key << "BacklightInvert" << YAML::Value << true; - if (displayRotate) - out << YAML::Key << "Rotate" << YAML::Value << true; - if (displayInvert) - out << YAML::Key << "Invert" << YAML::Value << true; - if (displayOffsetX) - out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX; - if (displayOffsetY) - out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY; - - out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate; - - out << YAML::EndMap; // Display - } - - // Touchscreen - if (touchscreen_spi_dev != "") { - out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev; - out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency; - switch (touchscreenModule) { - case xpt2046: - out << YAML::Key << "Module" << YAML::Value << "XPT2046"; - case stmpe610: - out << YAML::Key << "Module" << YAML::Value << "STMPE610"; - case gt911: - out << YAML::Key << "Module" << YAML::Value << "GT911"; - case ft5x06: - out << YAML::Key << "Module" << YAML::Value << "FT5x06"; - } - for (auto touchscreen_pin : all_pins) { - if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { - out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; - out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip; - out << YAML::EndMap; - } - } - if (touchscreenRotate != -1) - out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate; - if (touchscreenI2CAddr != -1) - out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr; - out << YAML::EndMap; // Touchscreen - } - - // Input - out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap; - if (keyboardDevice != "") - out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice; - if (pointerDevice != "") - out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; - - for (auto input_pin : all_pins) { - if (input_pin->config_section == "Input" && input_pin->enabled) { - out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << input_pin->pin; - out << YAML::Key << "line" << YAML::Value << input_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip; - out << YAML::EndMap; - } - } - if (tbDirection == 3) - out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING"; - - out << YAML::EndMap; // Input - - out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "LogLevel" << YAML::Value; - switch (logoutputlevel) { - case level_error: - out << "error"; - break; - case level_warn: - out << "warn"; - break; - case level_info: - out << "info"; - break; - case level_debug: - out << "debug"; - break; - case level_trace: - out << "trace"; - break; - } - if (traceFilename != "") - out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; - if (JSONFilename != "") { - out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; - if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; - else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; - else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; - else if (JSONFilter == meshtastic_PortNum_POSITION_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "position"; - else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; - else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; - else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; - else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; - else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; - else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; - } - if (ascii_logs_explicit) { - out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; - } - out << YAML::EndMap; // Logging - - // Webserver - if (webserver_root_path != "") { - out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path; - out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path; - out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path; - out << YAML::Key << "Port" << YAML::Value << webserverport; - out << YAML::EndMap; // Webserver - } - - // HostMetrics - if (hostMetrics_user_command != "") { - out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command; - out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval; - out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel; - - out << YAML::EndMap; // HostMetrics - } - - // config - if (has_configDisplayMode) { - out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; - switch (configDisplayMode) { - case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: - out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: - out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; - break; - } - - out << YAML::EndMap; // Config - } - - // General - out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; - if (config_directory != "") - out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; - if (mac_address_explicit) - out << YAML::Key << "MACAddress" << YAML::Value << mac_address; - if (mac_address_source != "") - out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source; - if (available_directory != "") - out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory; - out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone; - out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes; - out << YAML::EndMap; // General - return out.c_str(); - } + out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; + if (config_directory != "") + out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; + if (mac_address_explicit) + out << YAML::Key << "MACAddress" << YAML::Value << mac_address; + if (mac_address_source != "") + out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source; + if (available_directory != "") + out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory; + out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone; + out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes; + out << YAML::EndMap; // General + return out.c_str(); + } } portduino_config; \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 6e7fe24cb..01f4aeea6 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -2,341 +2,311 @@ #include "MeshService.h" #include "Router.h" -SimRadio::SimRadio() : NotifiedWorkerThread("SimRadio") -{ - instance = this; -} +SimRadio::SimRadio() : NotifiedWorkerThread("SimRadio") { instance = this; } SimRadio *SimRadio::instance; -ErrorCode SimRadio::send(meshtastic_MeshPacket *p) -{ - printPacket("enqueuing for send", p); +ErrorCode SimRadio::send(meshtastic_MeshPacket *p) { + printPacket("enqueuing for send", p); - bool dropped = false; - ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; + bool dropped = false; + ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; - if (dropped) { - txDrop++; - } + if (dropped) { + txDrop++; + } - if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks - packetPool.release(p); - return res; - } - - // set (random) transmit delay to let others reconfigure their radio, - // to avoid collisions and implement timing-based flooding - LOG_DEBUG("Set random delay before tx"); - setTransmitDelay(); + if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks + packetPool.release(p); return res; + } + + // set (random) transmit delay to let others reconfigure their radio, + // to avoid collisions and implement timing-based flooding + LOG_DEBUG("Set random delay before tx"); + setTransmitDelay(); + return res; } -void SimRadio::setTransmitDelay() -{ - meshtastic_MeshPacket *p = txQueue.getFront(); - // We want all sending/receiving to be done by our daemon thread. - // We use a delay here because this packet might have been sent in response to a packet we just received. - // So we want to make sure the other side has had a chance to reconfigure its radio. +void SimRadio::setTransmitDelay() { + meshtastic_MeshPacket *p = txQueue.getFront(); + // We want all sending/receiving to be done by our daemon thread. + // We use a delay here because this packet might have been sent in response to a packet we just received. + // So we want to make sure the other side has had a chance to reconfigure its radio. - /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. - * This assumption is valid because of the offset generated by the radio to account for the noise - * floor. - */ - if (p->rx_snr == 0 && p->rx_rssi == 0) { - startTransmitTimer(true); - } else { - // If there is a SNR, start a timer scaled based on that SNR. - LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p); - } + /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. + * This assumption is valid because of the offset generated by the radio to account for the noise + * floor. + */ + if (p->rx_snr == 0 && p->rx_rssi == 0) { + startTransmitTimer(true); + } else { + // If there is a SNR, start a timer scaled based on that SNR. + LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); + startTransmitTimerRebroadcast(p); + } } -void SimRadio::startTransmitTimer(bool withDelay) -{ - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec(); - // LOG_DEBUG("xmit timer %d", delay); - notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); - } +void SimRadio::startTransmitTimer(bool withDelay) { + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec(); + // LOG_DEBUG("xmit timer %d", delay); + notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); + } } -void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) -{ - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delayMsec = getTxDelayMsecWeighted(p); - // LOG_DEBUG("xmit timer %d", delay); - notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); - } +void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = getTxDelayMsecWeighted(p); + // LOG_DEBUG("xmit timer %d", delay); + notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); + } } -void SimRadio::handleTransmitInterrupt() -{ - // This can be null if we forced the device to enter standby mode. In that case - // ignore the transmit interrupt - if (sendingPacket) - completeSending(); +void SimRadio::handleTransmitInterrupt() { + // This can be null if we forced the device to enter standby mode. In that case + // ignore the transmit interrupt + if (sendingPacket) + completeSending(); - isReceiving = true; - if (receivingPacket) // This happens when we don't consider something a collision if we weren't sending long enough - handleReceiveInterrupt(); + isReceiving = true; + if (receivingPacket) // This happens when we don't consider something a collision if we weren't sending long enough + handleReceiveInterrupt(); } -void SimRadio::completeSending() -{ - // We are careful to clear sending packet before calling printPacket because - // that can take a long time - auto p = sendingPacket; - sendingPacket = NULL; +void SimRadio::completeSending() { + // We are careful to clear sending packet before calling printPacket because + // that can take a long time + auto p = sendingPacket; + sendingPacket = NULL; - if (p) { - txGood++; - if (!isFromUs(p)) - txRelay++; - printPacket("Completed sending", p); + if (p) { + txGood++; + if (!isFromUs(p)) + txRelay++; + printPacket("Completed sending", p); - // We are done sending that packet, release it - packetPool.release(p); - // LOG_DEBUG("Done with send"); - } + // We are done sending that packet, release it + packetPool.release(p); + // LOG_DEBUG("Done with send"); + } } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -bool SimRadio::canSendImmediately() -{ - // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). - // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, - // we almost certainly guarantee no one outside will like the packet we are sending. - bool busyTx = sendingPacket != NULL; - bool busyRx = isReceiving && isActivelyReceiving(); +bool SimRadio::canSendImmediately() { + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + bool busyTx = sendingPacket != NULL; + bool busyRx = isReceiving && isActivelyReceiving(); - if (busyTx || busyRx) { - if (busyTx) - LOG_WARN("Can not send yet, busyTx"); - if (busyRx) - LOG_WARN("Can not send yet, busyRx"); - return false; - } else - return true; + if (busyTx || busyRx) { + if (busyTx) + LOG_WARN("Can not send yet, busyTx"); + if (busyRx) + LOG_WARN("Can not send yet, busyRx"); + return false; + } else + return true; } -bool SimRadio::isActivelyReceiving() -{ - return receivingPacket != nullptr; -} +bool SimRadio::isActivelyReceiving() { return receivingPacket != nullptr; } -bool SimRadio::isChannelActive() -{ - return receivingPacket != nullptr; -} +bool SimRadio::isChannelActive() { return receivingPacket != nullptr; } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ -bool SimRadio::cancelSending(NodeNum from, PacketId id) -{ - auto p = txQueue.remove(from, id); - if (p) - packetPool.release(p); // free the packet we just removed +bool SimRadio::cancelSending(NodeNum from, PacketId id) { + auto p = txQueue.remove(from, id); + if (p) + packetPool.release(p); // free the packet we just removed - bool result = (p != NULL); - LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); - return result; + bool result = (p != NULL); + LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); + return result; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ -bool SimRadio::findInTxQueue(NodeNum from, PacketId id) -{ - return txQueue.find(from, id); -} +bool SimRadio::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } -void SimRadio::onNotify(uint32_t notification) -{ - switch (notification) { - case ISR_TX: - handleTransmitInterrupt(); - // LOG_DEBUG("tx complete - starting timer"); - startTransmitTimer(); - break; - case ISR_RX: - handleReceiveInterrupt(); - // LOG_DEBUG("rx complete - starting timer"); - startTransmitTimer(); - break; - case TRANSMIT_DELAY_COMPLETED: - if (receivingPacket) { // This happens when we had a timer pending and we started receiving - handleReceiveInterrupt(); - startTransmitTimer(); - break; - } - LOG_DEBUG("delay done"); - - // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread - // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? - if (!txQueue.empty()) { - if (!canSendImmediately()) { - // LOG_DEBUG("Currently Rx/Tx-ing: set random delay"); - setTransmitDelay(); // currently Rx/Tx-ing: reset random delay - } else { - if (isChannelActive()) { // check if there is currently a LoRa packet on the channel - // LOG_DEBUG("Channel is active: set random delay"); - setTransmitDelay(); // reset random delay - } else { - // Send any outgoing packets we have ready - meshtastic_MeshPacket *txp = txQueue.dequeue(); - assert(txp); - startSend(txp); - // Packet has been sent, count it toward our TX airtime utilization. - uint32_t xmitMsec = RadioInterface::getPacketTime(txp); - airTime->logAirtime(TX_LOG, xmitMsec); - - notifyLater(xmitMsec, ISR_TX, false); // Model the time it is busy sending - } - } - } else { - // LOG_DEBUG("done with txqueue"); - } - break; - default: - assert(0); // We expected to receive a valid notification from the ISR +void SimRadio::onNotify(uint32_t notification) { + switch (notification) { + case ISR_TX: + handleTransmitInterrupt(); + // LOG_DEBUG("tx complete - starting timer"); + startTransmitTimer(); + break; + case ISR_RX: + handleReceiveInterrupt(); + // LOG_DEBUG("rx complete - starting timer"); + startTransmitTimer(); + break; + case TRANSMIT_DELAY_COMPLETED: + if (receivingPacket) { // This happens when we had a timer pending and we started receiving + handleReceiveInterrupt(); + startTransmitTimer(); + break; } + LOG_DEBUG("delay done"); + + // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread + // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? + if (!txQueue.empty()) { + if (!canSendImmediately()) { + // LOG_DEBUG("Currently Rx/Tx-ing: set random delay"); + setTransmitDelay(); // currently Rx/Tx-ing: reset random delay + } else { + if (isChannelActive()) { // check if there is currently a LoRa packet on the channel + // LOG_DEBUG("Channel is active: set random delay"); + setTransmitDelay(); // reset random delay + } else { + // Send any outgoing packets we have ready + meshtastic_MeshPacket *txp = txQueue.dequeue(); + assert(txp); + startSend(txp); + // Packet has been sent, count it toward our TX airtime utilization. + uint32_t xmitMsec = RadioInterface::getPacketTime(txp); + airTime->logAirtime(TX_LOG, xmitMsec); + + notifyLater(xmitMsec, ISR_TX, false); // Model the time it is busy sending + } + } + } else { + // LOG_DEBUG("done with txqueue"); + } + break; + default: + assert(0); // We expected to receive a valid notification from the ISR + } } /** start an immediate transmit */ -void SimRadio::startSend(meshtastic_MeshPacket *txp) -{ - printPacket("Start low level send", txp); - isReceiving = false; - size_t numbytes = beginSending(txp); - meshtastic_MeshPacket *p = packetPool.allocCopy(*txp); - perhapsDecode(p); - meshtastic_Compressed c = meshtastic_Compressed_init_default; - c.portnum = p->decoded.portnum; - // LOG_DEBUG("Send back to simulator with portNum %d", p->decoded.portnum); - if (p->decoded.payload.size <= sizeof(c.data.bytes)) { - memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size); - c.data.size = p->decoded.payload.size; - } else { - LOG_WARN("Payload size larger than compressed message allows! Send empty payload"); - } - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Compressed_msg, &c); - p->decoded.portnum = meshtastic_PortNum_SIMULATOR_APP; +void SimRadio::startSend(meshtastic_MeshPacket *txp) { + printPacket("Start low level send", txp); + isReceiving = false; + size_t numbytes = beginSending(txp); + meshtastic_MeshPacket *p = packetPool.allocCopy(*txp); + perhapsDecode(p); + meshtastic_Compressed c = meshtastic_Compressed_init_default; + c.portnum = p->decoded.portnum; + // LOG_DEBUG("Send back to simulator with portNum %d", p->decoded.portnum); + if (p->decoded.payload.size <= sizeof(c.data.bytes)) { + memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size); + c.data.size = p->decoded.payload.size; + } else { + LOG_WARN("Payload size larger than compressed message allows! Send empty payload"); + } + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Compressed_msg, &c); + p->decoded.portnum = meshtastic_PortNum_SIMULATOR_APP; - service->sendQueueStatusToPhone(router->getQueueStatus(), 0, p->id); - service->sendToPhone(p); // Sending back to simulator - service->loop(); // Process the send immediately + service->sendQueueStatusToPhone(router->getQueueStatus(), 0, p->id); + service->sendToPhone(p); // Sending back to simulator + service->loop(); // Process the send immediately } // Simulates device received a packet via the LoRa chip -void SimRadio::unpackAndReceive(meshtastic_MeshPacket &p) -{ - // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first - meshtastic_Compressed scratch; - meshtastic_Compressed *decoded = NULL; - if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - memset(&scratch, 0, sizeof(scratch)); - p.decoded.payload.size = - pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch); - if (p.decoded.payload.size) { - decoded = &scratch; - // Extract the original payload and replace - memcpy(&p.decoded.payload, &decoded->data, sizeof(decoded->data)); - // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum - p.decoded.portnum = decoded->portnum; - } else - LOG_ERROR("Error decoding proto for simulator message!"); - } - // Let SimRadio receive as if it did via its LoRa chip - startReceive(&p); +void SimRadio::unpackAndReceive(meshtastic_MeshPacket &p) { + // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first + meshtastic_Compressed scratch; + meshtastic_Compressed *decoded = NULL; + if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + memset(&scratch, 0, sizeof(scratch)); + p.decoded.payload.size = pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch); + if (p.decoded.payload.size) { + decoded = &scratch; + // Extract the original payload and replace + memcpy(&p.decoded.payload, &decoded->data, sizeof(decoded->data)); + // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum + p.decoded.portnum = decoded->portnum; + } else + LOG_ERROR("Error decoding proto for simulator message!"); + } + // Let SimRadio receive as if it did via its LoRa chip + startReceive(&p); } -void SimRadio::startReceive(meshtastic_MeshPacket *p) -{ +void SimRadio::startReceive(meshtastic_MeshPacket *p) { #ifdef USERPREFS_SIMRADIO_EMULATE_COLLISIONS - if (isActivelyReceiving()) { - LOG_WARN("Collision detected, dropping current and previous packet!"); - rxBad++; - airTime->logAirtime(RX_ALL_LOG, getPacketTime(receivingPacket, true)); - packetPool.release(receivingPacket); - receivingPacket = nullptr; - return; - } else if (sendingPacket) { - uint32_t airtimeLeft = tillRun(millis()); - if (airtimeLeft <= 0) { - LOG_WARN("Transmitting packet was already done"); - handleTransmitInterrupt(); // Finish sending first - } else if ((interval - airtimeLeft) > preambleTimeMsec) { - // Only if transmitting for longer than preamble there is a collision - // (channel should actually be detected as active otherwise) - LOG_WARN("Collision detected during transmission!"); - return; - } + if (isActivelyReceiving()) { + LOG_WARN("Collision detected, dropping current and previous packet!"); + rxBad++; + airTime->logAirtime(RX_ALL_LOG, getPacketTime(receivingPacket, true)); + packetPool.release(receivingPacket); + receivingPacket = nullptr; + return; + } else if (sendingPacket) { + uint32_t airtimeLeft = tillRun(millis()); + if (airtimeLeft <= 0) { + LOG_WARN("Transmitting packet was already done"); + handleTransmitInterrupt(); // Finish sending first + } else if ((interval - airtimeLeft) > preambleTimeMsec) { + // Only if transmitting for longer than preamble there is a collision + // (channel should actually be detected as active otherwise) + LOG_WARN("Collision detected during transmission!"); + return; } - isReceiving = true; - receivingPacket = packetPool.allocCopy(*p); - uint32_t airtimeMsec = getPacketTime(p, true); - notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving + } + isReceiving = true; + receivingPacket = packetPool.allocCopy(*p); + uint32_t airtimeMsec = getPacketTime(p, true); + notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving #else - isReceiving = true; - receivingPacket = packetPool.allocCopy(*p); - handleReceiveInterrupt(); // Simulate receiving the packet immediately - startTransmitTimer(); + isReceiving = true; + receivingPacket = packetPool.allocCopy(*p); + handleReceiveInterrupt(); // Simulate receiving the packet immediately + startTransmitTimer(); #endif } -meshtastic_QueueStatus SimRadio::getQueueStatus() -{ - meshtastic_QueueStatus qs; +meshtastic_QueueStatus SimRadio::getQueueStatus() { + meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = 0; - qs.free = txQueue.getFree(); - qs.maxlen = txQueue.getMaxLen(); + qs.res = qs.mesh_packet_id = 0; + qs.free = txQueue.getFree(); + qs.maxlen = txQueue.getMaxLen(); - return qs; + return qs; } -void SimRadio::handleReceiveInterrupt() -{ - if (receivingPacket == nullptr) { - return; - } +void SimRadio::handleReceiveInterrupt() { + if (receivingPacket == nullptr) { + return; + } - if (!isReceiving) { - LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode"); - return; - } + if (!isReceiving) { + LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode"); + return; + } - LOG_DEBUG("HANDLE RECEIVE INTERRUPT"); - rxGood++; + LOG_DEBUG("HANDLE RECEIVE INTERRUPT"); + rxGood++; - meshtastic_MeshPacket *mp = packetPool.allocCopy(*receivingPacket); // keep a copy in packetPool - packetPool.release(receivingPacket); // release the original - receivingPacket = nullptr; + meshtastic_MeshPacket *mp = packetPool.allocCopy(*receivingPacket); // keep a copy in packetPool + packetPool.release(receivingPacket); // release the original + receivingPacket = nullptr; - printPacket("Lora RX", mp); + printPacket("Lora RX", mp); - airTime->logAirtime(RX_LOG, RadioInterface::getPacketTime(mp, true)); + airTime->logAirtime(RX_LOG, RadioInterface::getPacketTime(mp, true)); - deliverToReceiver(mp); + deliverToReceiver(mp); } -size_t SimRadio::getPacketLength(meshtastic_MeshPacket *mp) -{ - auto &p = mp->decoded; - return (size_t)p.payload.size + sizeof(PacketHeader); +size_t SimRadio::getPacketLength(meshtastic_MeshPacket *mp) { + auto &p = mp->decoded; + return (size_t)p.payload.size + sizeof(PacketHeader); } -int16_t SimRadio::readData(uint8_t *data, size_t len) -{ - int16_t state = RADIOLIB_ERR_NONE; +int16_t SimRadio::readData(uint8_t *data, size_t len) { + int16_t state = RADIOLIB_ERR_NONE; - if (state == RADIOLIB_ERR_NONE) { - // add null terminator - data[len] = 0; - } + if (state == RADIOLIB_ERR_NONE) { + // add null terminator + data[len] = 0; + } - return state; + return state; } /** @@ -346,20 +316,18 @@ int16_t SimRadio::readData(uint8_t *data, size_t len) * * @return num msecs for the packet */ -uint32_t SimRadio::getPacketTime(uint32_t pl, bool received) -{ - float bandwidthHz = bw * 1000.0f; - bool headDisable = false; // we currently always use the header - float tSym = (1 << sf) / bandwidthHz; +uint32_t SimRadio::getPacketTime(uint32_t pl, bool received) { + float bandwidthHz = bw * 1000.0f; + bool headDisable = false; // we currently always use the header + float tSym = (1 << sf) / bandwidthHz; - bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms + bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms - float tPreamble = (preambleLength + 4.25f) * tSym; - float numPayloadSym = - 8 + max(ceilf(((8.0f * pl - 4 * sf + 28 + 16 - 20 * headDisable) / (4 * (sf - 2 * lowDataOptEn))) * cr), 0.0f); - float tPayload = numPayloadSym * tSym; - float tPacket = tPreamble + tPayload; + float tPreamble = (preambleLength + 4.25f) * tSym; + float numPayloadSym = 8 + max(ceilf(((8.0f * pl - 4 * sf + 28 + 16 - 20 * headDisable) / (4 * (sf - 2 * lowDataOptEn))) * cr), 0.0f); + float tPayload = numPayloadSym * tSym; + float tPacket = tPreamble + tPayload; - uint32_t msecs = tPacket * 1000; - return msecs; + uint32_t msecs = tPacket * 1000; + return msecs; } \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index 6f80989da..856f76f49 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -7,90 +7,89 @@ #include -class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThread -{ - enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; +class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThread { + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; - MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); - public: - SimRadio(); +public: + SimRadio(); - /** MeshService needs this to find our active instance - */ - static SimRadio *instance; + /** MeshService needs this to find our active instance + */ + static SimRadio *instance; - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive(); + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive(); - /** are we actively receiving a packet (only called during receiving state) - * This method is only public to facilitate debugging. Do not call. - */ - virtual bool isActivelyReceiving(); + /** are we actively receiving a packet (only called during receiving state) + * This method is only public to facilitate debugging. Do not call. + */ + virtual bool isActivelyReceiving(); - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - virtual bool cancelSending(NodeNum from, PacketId id) override; + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) override; - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - virtual bool findInTxQueue(NodeNum from, PacketId id) override; + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) override; - /** - * Start waiting to receive a message - * - * External functions can call this method to wake the device from sleep. - */ - virtual void startReceive(meshtastic_MeshPacket *p); + /** + * Start waiting to receive a message + * + * External functions can call this method to wake the device from sleep. + */ + virtual void startReceive(meshtastic_MeshPacket *p); - meshtastic_QueueStatus getQueueStatus() override; + meshtastic_QueueStatus getQueueStatus() override; - // Convert Compressed_msg to normal msg and receive it - void unpackAndReceive(meshtastic_MeshPacket &p); + // Convert Compressed_msg to normal msg and receive it + void unpackAndReceive(meshtastic_MeshPacket &p); - /** - * Debugging counts - */ - uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; - uint16_t txDrop = 0; + /** + * Debugging counts + */ + uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; + uint16_t txDrop = 0; - protected: - /// are _trying_ to receive a packet currently (note - we might just be waiting for one) - bool isReceiving = true; +protected: + /// are _trying_ to receive a packet currently (note - we might just be waiting for one) + bool isReceiving = true; - private: - void setTransmitDelay(); +private: + void setTransmitDelay(); - /** random timer with certain min. and max. settings */ - void startTransmitTimer(bool withDelay = true); + /** random timer with certain min. and max. settings */ + void startTransmitTimer(bool withDelay = true); - /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); + /** timer scaled to SNR of to be flooded packet */ + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); - void handleTransmitInterrupt(); - void handleReceiveInterrupt(); + void handleTransmitInterrupt(); + void handleReceiveInterrupt(); - void onNotify(uint32_t notification); + void onNotify(uint32_t notification); - // start an immediate transmit - virtual void startSend(meshtastic_MeshPacket *txp); + // start an immediate transmit + virtual void startSend(meshtastic_MeshPacket *txp); - // derive packet length - size_t getPacketLength(meshtastic_MeshPacket *p); + // derive packet length + size_t getPacketLength(meshtastic_MeshPacket *p); - int16_t readData(uint8_t *str, size_t len); + int16_t readData(uint8_t *str, size_t len); - meshtastic_MeshPacket *receivingPacket = nullptr; // The packet we are currently receiving + meshtastic_MeshPacket *receivingPacket = nullptr; // The packet we are currently receiving - protected: - /** Could we send right now (i.e. either not actively receiving or transmitting)? */ - virtual bool canSendImmediately(); +protected: + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately(); - /** - * If a send was in progress finish it and return the buffer to the pool */ - void completeSending(); + /** + * If a send was in progress finish it and return the buffer to the pool */ + void completeSending(); - virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; + virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; }; extern SimRadio *simRadio; \ No newline at end of file diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index ce2a5cfd3..02b8895dd 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -23,146 +23,132 @@ // the HAL must inherit from the base RadioLibHal class // and implement all of its virtual methods -class Ch341Hal : public RadioLibHal -{ - public: - // default constructor - initializes the base HAL and any needed private members - explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, - uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) - : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) - { - if (serial != "") { - strncpy(pinedio.serial_number, serial.c_str(), 8); - pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1); - } - // LOG_INFO("USB Serial: %s", pinedio.serial_number); +class Ch341Hal : public RadioLibHal { +public: + // default constructor - initializes the base HAL and any needed private members + explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, uint32_t spiSpeed = 2000000, + uint8_t spiDevice = 0, uint8_t gpioDevice = 0) + : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) { + if (serial != "") { + strncpy(pinedio.serial_number, serial.c_str(), 8); + pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1); + } + // LOG_INFO("USB Serial: %s", pinedio.serial_number); - // There is no vendor with 0x0 -> so check - if (vid != 0x0) { - pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid); - pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid); - } - int32_t ret = pinedio_init(&pinedio, NULL); - if (ret != 0) { - std::string s = "Could not open SPI: "; - throw(s + std::to_string(ret)); - } - - pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); - pinedio_set_pin_mode(&pinedio, 3, true); - pinedio_set_pin_mode(&pinedio, 5, true); + // There is no vendor with 0x0 -> so check + if (vid != 0x0) { + pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid); + pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid); + } + int32_t ret = pinedio_init(&pinedio, NULL); + if (ret != 0) { + std::string s = "Could not open SPI: "; + throw(s + std::to_string(ret)); } - ~Ch341Hal() { pinedio_deinit(&pinedio); } + pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); + pinedio_set_pin_mode(&pinedio, 3, true); + pinedio_set_pin_mode(&pinedio, 5, true); + } - void getSerialString(char *_serial, size_t len) - { - len = len > 8 ? 8 : len; - strncpy(_serial, pinedio.serial_number, len); + ~Ch341Hal() { pinedio_deinit(&pinedio); } + + void getSerialString(char *_serial, size_t len) { + len = len > 8 ? 8 : len; + strncpy(_serial, pinedio.serial_number, len); + } + + void getProductString(char *_product_string, size_t len) { + len = len > 95 ? 95 : len; + strncpy(_product_string, pinedio.product_string, len); + } + + void init() override {} + void term() override {} + + // GPIO-related methods (pinMode, digitalWrite etc.) should check + // RADIOLIB_NC as an alias for non-connected pins + void pinMode(uint32_t pin, uint32_t mode) override { + if (pin == RADIOLIB_NC) { + return; } + pinedio_set_pin_mode(&pinedio, pin, mode); + } - void getProductString(char *_product_string, size_t len) - { - len = len > 95 ? 95 : len; - strncpy(_product_string, pinedio.product_string, len); + void digitalWrite(uint32_t pin, uint32_t value) override { + if (pin == RADIOLIB_NC) { + return; } + pinedio_digital_write(&pinedio, pin, value); + } - void init() override {} - void term() override {} - - // GPIO-related methods (pinMode, digitalWrite etc.) should check - // RADIOLIB_NC as an alias for non-connected pins - void pinMode(uint32_t pin, uint32_t mode) override - { - if (pin == RADIOLIB_NC) { - return; - } - pinedio_set_pin_mode(&pinedio, pin, mode); + uint32_t digitalRead(uint32_t pin) override { + if (pin == RADIOLIB_NC) { + return 0; } + return pinedio_digital_read(&pinedio, pin); + } - void digitalWrite(uint32_t pin, uint32_t value) override - { - if (pin == RADIOLIB_NC) { - return; - } - pinedio_digital_write(&pinedio, pin, value); + void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { + if (interruptNum == RADIOLIB_NC) { + return; } + // LOG_DEBUG("Attach interrupt to pin %d", interruptNum); + pinedio_attach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum, (pinedio_int_mode)mode, interruptCb); + } - uint32_t digitalRead(uint32_t pin) override - { - if (pin == RADIOLIB_NC) { - return 0; - } - return pinedio_digital_read(&pinedio, pin); + void detachInterrupt(uint32_t interruptNum) override { + if (interruptNum == RADIOLIB_NC) { + return; } + // LOG_DEBUG("Detach interrupt from pin %d", interruptNum); + pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum); + } - void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override - { - if (interruptNum == RADIOLIB_NC) { - return; - } - // LOG_DEBUG("Attach interrupt to pin %d", interruptNum); - pinedio_attach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum, (pinedio_int_mode)mode, interruptCb); + void delay(unsigned long ms) override { delayMicroseconds(ms * 1000); } + + void delayMicroseconds(unsigned long us) override { + if (us == 0) { + sched_yield(); + return; } + usleep(us); + } - void detachInterrupt(uint32_t interruptNum) override - { - if (interruptNum == RADIOLIB_NC) { - return; - } - // LOG_DEBUG("Detach interrupt from pin %d", interruptNum); - pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum); + void yield() override { sched_yield(); } + + unsigned long millis() override { + struct timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL); + } + + unsigned long micros() override { + struct timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000000ULL) + tv.tv_usec; + } + + long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override { + std::cerr << "pulseIn for pin " << pin << "is not supported!" << std::endl; + return 0; + } + + void spiBegin() {} + void spiBeginTransaction() {} + + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) { + int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); + if (ret < 0) { + std::cerr << "Could not perform SPI transfer: " << ret << std::endl; } + } - void delay(unsigned long ms) override { delayMicroseconds(ms * 1000); } + void spiEndTransaction() {} + void spiEnd() {} - void delayMicroseconds(unsigned long us) override - { - if (us == 0) { - sched_yield(); - return; - } - usleep(us); - } - - void yield() override { sched_yield(); } - - unsigned long millis() override - { - struct timeval tv; - gettimeofday(&tv, NULL); - return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL); - } - - unsigned long micros() override - { - struct timeval tv; - gettimeofday(&tv, NULL); - return (tv.tv_sec * 1000000ULL) + tv.tv_usec; - } - - long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override - { - std::cerr << "pulseIn for pin " << pin << "is not supported!" << std::endl; - return 0; - } - - void spiBegin() {} - void spiBeginTransaction() {} - - void spiTransfer(uint8_t *out, size_t len, uint8_t *in) - { - int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); - if (ret < 0) { - std::cerr << "Could not perform SPI transfer: " << ret << std::endl; - } - } - - void spiEndTransaction() {} - void spiEnd() {} - - private: - pinedio_inst pinedio = {0}; +private: + pinedio_inst pinedio = {0}; }; #endif diff --git a/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h b/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h index e1e014f33..256f37128 100644 --- a/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h +++ b/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h @@ -19,11 +19,11 @@ extern "C" { * * Ring Oscillator (ROSC) API * - * A Ring Oscillator is an on-chip oscillator that requires no external crystal. Instead, the output is generated from a series of - * inverters that are chained together to create a feedback loop. RP2040 boots from the ring oscillator initially, meaning the - * first stages of the bootrom, including booting from SPI flash, will be clocked by the ring oscillator. If your design has a - * crystal oscillator, you’ll likely want to switch to this as your reference clock as soon as possible, because the frequency is - * more accurate than the ring oscillator. + * A Ring Oscillator is an on-chip oscillator that requires no external crystal. Instead, the output is generated from a + * series of inverters that are chained together to create a feedback loop. RP2040 boots from the ring oscillator + * initially, meaning the first stages of the bootrom, including booting from SPI flash, will be clocked by the ring + * oscillator. If your design has a crystal oscillator, you’ll likely want to switch to this as your reference clock as + * soon as possible, because the frequency is more accurate than the ring oscillator. */ /*! \brief Set frequency of the Ring Oscillator @@ -68,22 +68,15 @@ uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz); void rosc_set_div(uint32_t div); -inline static void rosc_clear_bad_write(void) -{ - hw_clear_bits(&rosc_hw->status, ROSC_STATUS_BADWRITE_BITS); -} +inline static void rosc_clear_bad_write(void) { hw_clear_bits(&rosc_hw->status, ROSC_STATUS_BADWRITE_BITS); } -inline static bool rosc_write_okay(void) -{ - return !(rosc_hw->status & ROSC_STATUS_BADWRITE_BITS); -} +inline static bool rosc_write_okay(void) { return !(rosc_hw->status & ROSC_STATUS_BADWRITE_BITS); } -inline static void rosc_write(io_rw_32 *addr, uint32_t value) -{ - rosc_clear_bad_write(); - assert(rosc_write_okay()); - *addr = value; - assert(rosc_write_okay()); +inline static void rosc_write(io_rw_32 *addr, uint32_t value) { + rosc_clear_bad_write(); + assert(rosc_write_okay()); + *addr = value; + assert(rosc_write_okay()); }; #ifdef __cplusplus diff --git a/src/platform/rp2xx0/hardware_rosc/rosc.c b/src/platform/rp2xx0/hardware_rosc/rosc.c index f79929f8d..214edf686 100644 --- a/src/platform/rp2xx0/hardware_rosc/rosc.c +++ b/src/platform/rp2xx0/hardware_rosc/rosc.c @@ -12,59 +12,50 @@ // Given a ROSC delay stage code, return the next-numerically-higher code. // Top result bit is set when called on maximum ROSC code. -uint32_t next_rosc_code(uint32_t code) -{ - return ((code | 0x08888888u) + 1u) & 0xf7777777u; -} +uint32_t next_rosc_code(uint32_t code) { return ((code | 0x08888888u) + 1u) & 0xf7777777u; } -uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz) -{ - // TODO: This could be a lot better - rosc_set_div(1); - for (uint32_t code = 0; code <= 0x77777777u; code = next_rosc_code(code)) { - rosc_set_freq(code); - uint rosc_mhz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC) / 1000; - if ((rosc_mhz >= low_mhz) && (rosc_mhz <= high_mhz)) { - return rosc_mhz; - } +uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz) { + // TODO: This could be a lot better + rosc_set_div(1); + for (uint32_t code = 0; code <= 0x77777777u; code = next_rosc_code(code)) { + rosc_set_freq(code); + uint rosc_mhz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC) / 1000; + if ((rosc_mhz >= low_mhz) && (rosc_mhz <= high_mhz)) { + return rosc_mhz; } - return 0; + } + return 0; } -void rosc_set_div(uint32_t div) -{ - assert(div <= 31 && div >= 1); - rosc_write(&rosc_hw->div, ROSC_DIV_VALUE_PASS + div); +void rosc_set_div(uint32_t div) { + assert(div <= 31 && div >= 1); + rosc_write(&rosc_hw->div, ROSC_DIV_VALUE_PASS + div); } -void rosc_set_freq(uint32_t code) -{ - rosc_write(&rosc_hw->freqa, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code & 0xffffu)); - rosc_write(&rosc_hw->freqb, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code >> 16u)); +void rosc_set_freq(uint32_t code) { + rosc_write(&rosc_hw->freqa, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code & 0xffffu)); + rosc_write(&rosc_hw->freqb, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code >> 16u)); } -void rosc_set_range(uint range) -{ - // Range should use enumvals from the headers and thus have the password correct - rosc_write(&rosc_hw->ctrl, (ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB) | range); +void rosc_set_range(uint range) { + // Range should use enumvals from the headers and thus have the password correct + rosc_write(&rosc_hw->ctrl, (ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB) | range); } -void rosc_disable(void) -{ - uint32_t tmp = rosc_hw->ctrl; - tmp &= (~ROSC_CTRL_ENABLE_BITS); - tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB); - rosc_write(&rosc_hw->ctrl, tmp); - // Wait for stable to go away - while (rosc_hw->status & ROSC_STATUS_STABLE_BITS) - ; +void rosc_disable(void) { + uint32_t tmp = rosc_hw->ctrl; + tmp &= (~ROSC_CTRL_ENABLE_BITS); + tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB); + rosc_write(&rosc_hw->ctrl, tmp); + // Wait for stable to go away + while (rosc_hw->status & ROSC_STATUS_STABLE_BITS) + ; } -void rosc_set_dormant(void) -{ - // WARNING: This stops the rosc until woken up by an irq - rosc_write(&rosc_hw->dormant, ROSC_DORMANT_VALUE_DORMANT); - // Wait for it to become stable once woken up - while (!(rosc_hw->status & ROSC_STATUS_STABLE_BITS)) - ; +void rosc_set_dormant(void) { + // WARNING: This stops the rosc until woken up by an irq + rosc_write(&rosc_hw->dormant, ROSC_DORMANT_VALUE_DORMANT); + // Wait for it to become stable once woken up + while (!(rosc_hw->status & ROSC_STATUS_STABLE_BITS)) + ; } \ No newline at end of file diff --git a/src/platform/rp2xx0/main-rp2xx0.cpp b/src/platform/rp2xx0/main-rp2xx0.cpp index 6c73e385a..1e0bde780 100644 --- a/src/platform/rp2xx0/main-rp2xx0.cpp +++ b/src/platform/rp2xx0/main-rp2xx0.cpp @@ -10,148 +10,133 @@ static bool awake; -static void sleep_callback(void) -{ - awake = true; +static void sleep_callback(void) { awake = true; } + +void epoch_to_datetime(time_t epoch, datetime_t *dt) { + struct tm *tm_info; + + tm_info = gmtime(&epoch); + dt->year = tm_info->tm_year; + dt->month = tm_info->tm_mon + 1; + dt->day = tm_info->tm_mday; + dt->dotw = tm_info->tm_wday; + dt->hour = tm_info->tm_hour; + dt->min = tm_info->tm_min; + dt->sec = tm_info->tm_sec; } -void epoch_to_datetime(time_t epoch, datetime_t *dt) -{ - struct tm *tm_info; - - tm_info = gmtime(&epoch); - dt->year = tm_info->tm_year; - dt->month = tm_info->tm_mon + 1; - dt->day = tm_info->tm_mday; - dt->dotw = tm_info->tm_wday; - dt->hour = tm_info->tm_hour; - dt->min = tm_info->tm_min; - dt->sec = tm_info->tm_sec; +void debug_date(datetime_t t) { + LOG_DEBUG("%d %d %d %d %d %d %d", t.year, t.month, t.day, t.hour, t.min, t.sec, t.dotw); + uart_default_tx_wait_blocking(); } -void debug_date(datetime_t t) -{ - LOG_DEBUG("%d %d %d %d %d %d %d", t.year, t.month, t.day, t.hour, t.min, t.sec, t.dotw); - uart_default_tx_wait_blocking(); -} +void cpuDeepSleep(uint32_t msecs) { -void cpuDeepSleep(uint32_t msecs) -{ + time_t seconds = (time_t)(msecs / 1000); + datetime_t t_init, t_alarm; - time_t seconds = (time_t)(msecs / 1000); - datetime_t t_init, t_alarm; + awake = false; + // Start the RTC + rtc_init(); + epoch_to_datetime(0, &t_init); + rtc_set_datetime(&t_init); + epoch_to_datetime(seconds, &t_alarm); + // debug_date(t_init); + // debug_date(t_alarm); + uart_default_tx_wait_blocking(); + sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); + sleep_goto_sleep_until(&t_alarm, &sleep_callback); - awake = false; - // Start the RTC - rtc_init(); - epoch_to_datetime(0, &t_init); - rtc_set_datetime(&t_init); - epoch_to_datetime(seconds, &t_alarm); - // debug_date(t_init); - // debug_date(t_alarm); - uart_default_tx_wait_blocking(); - sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); - sleep_goto_sleep_until(&t_alarm, &sleep_callback); + // Make sure we don't wake + while (!awake) { + delay(1); + } - // Make sure we don't wake - while (!awake) { - delay(1); - } + /* For now, I don't know how to revert this state + We just reboot in order to get back operational */ + rp2040.reboot(); - /* For now, I don't know how to revert this state - We just reboot in order to get back operational */ - rp2040.reboot(); - - /* Set RP2040 in dormant mode. Will not wake up. */ - // xosc_dormant(); + /* Set RP2040 in dormant mode. Will not wake up. */ + // xosc_dormant(); } #else -void cpuDeepSleep(uint32_t msecs) -{ - /* Set RP2040 in dormant mode. Will not wake up. */ - xosc_dormant(); +void cpuDeepSleep(uint32_t msecs) { + /* Set RP2040 in dormant mode. Will not wake up. */ + xosc_dormant(); } #endif -void setBluetoothEnable(bool enable) -{ - // not needed +void setBluetoothEnable(bool enable) { + // not needed } -void updateBatteryLevel(uint8_t level) -{ - // not needed +void updateBatteryLevel(uint8_t level) { + // not needed } -void getMacAddr(uint8_t *dmac) -{ - pico_unique_board_id_t src; - pico_get_unique_board_id(&src); - dmac[5] = src.id[7]; - dmac[4] = src.id[6]; - dmac[3] = src.id[5]; - dmac[2] = src.id[4]; - dmac[1] = src.id[3]; - dmac[0] = src.id[2]; +void getMacAddr(uint8_t *dmac) { + pico_unique_board_id_t src; + pico_get_unique_board_id(&src); + dmac[5] = src.id[7]; + dmac[4] = src.id[6]; + dmac[3] = src.id[5]; + dmac[2] = src.id[4]; + dmac[1] = src.id[3]; + dmac[0] = src.id[2]; } -void rp2040Setup() -{ - /* Sets a random seed to make sure we get different random numbers on each boot. - Taken from CPU cycle counter and ROSC oscillator, so should be pretty random. - */ - randomSeed(rp2040.hwrand32()); +void rp2040Setup() { + /* Sets a random seed to make sure we get different random numbers on each boot. + Taken from CPU cycle counter and ROSC oscillator, so should be pretty random. + */ + randomSeed(rp2040.hwrand32()); #ifdef RP2040_SLOW_CLOCK - uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY); - uint f_pll_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_USB_CLKSRC_PRIMARY); - uint f_rosc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC); - uint f_clk_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); - uint f_clk_peri = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_PERI); - uint f_clk_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_USB); - uint f_clk_adc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_ADC); - uint f_clk_rtc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_RTC); + uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY); + uint f_pll_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_USB_CLKSRC_PRIMARY); + uint f_rosc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC); + uint f_clk_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); + uint f_clk_peri = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_PERI); + uint f_clk_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_USB); + uint f_clk_adc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_ADC); + uint f_clk_rtc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_RTC); - LOG_INFO("Clock speed:"); - LOG_INFO("pll_sys = %dkHz", f_pll_sys); - LOG_INFO("pll_usb = %dkHz", f_pll_usb); - LOG_INFO("rosc = %dkHz", f_rosc); - LOG_INFO("clk_sys = %dkHz", f_clk_sys); - LOG_INFO("clk_peri = %dkHz", f_clk_peri); - LOG_INFO("clk_usb = %dkHz", f_clk_usb); - LOG_INFO("clk_adc = %dkHz", f_clk_adc); - LOG_INFO("clk_rtc = %dkHz", f_clk_rtc); + LOG_INFO("Clock speed:"); + LOG_INFO("pll_sys = %dkHz", f_pll_sys); + LOG_INFO("pll_usb = %dkHz", f_pll_usb); + LOG_INFO("rosc = %dkHz", f_rosc); + LOG_INFO("clk_sys = %dkHz", f_clk_sys); + LOG_INFO("clk_peri = %dkHz", f_clk_peri); + LOG_INFO("clk_usb = %dkHz", f_clk_usb); + LOG_INFO("clk_adc = %dkHz", f_clk_adc); + LOG_INFO("clk_rtc = %dkHz", f_clk_rtc); #endif } -void enterDfuMode() -{ - reset_usb_boot(0, 0); -} +void enterDfuMode() { reset_usb_boot(0, 0); } /* Init in early boot state. */ #ifdef RP2040_SLOW_CLOCK -void initVariant() -{ - /* Set the system frequency to 18 MHz. */ - set_sys_clock_khz(18 * KHZ, false); - /* The previous line automatically detached clk_peri from clk_sys, and - attached it to pll_usb. We need to attach clk_peri back to system PLL to keep SPI - working at this low speed. - For details see https://github.com/jgromes/RadioLib/discussions/938 - */ - clock_configure(clk_peri, - 0, // No glitchless mux - CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, // System PLL on AUX mux - 18 * MHZ, // Input frequency - 18 * MHZ // Output (must be same as no divider) - ); - /* Run also ADC on lower clk_sys. */ - clock_configure(clk_adc, 0, CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 18 * MHZ, 18 * MHZ); - /* Run RTC from XOSC since USB clock is off */ - clock_configure(clk_rtc, 0, CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC, 12 * MHZ, 47 * KHZ); - /* Turn off USB PLL */ - pll_deinit(pll_usb); +void initVariant() { + /* Set the system frequency to 18 MHz. */ + set_sys_clock_khz(18 * KHZ, false); + /* The previous line automatically detached clk_peri from clk_sys, and + attached it to pll_usb. We need to attach clk_peri back to system PLL to keep SPI + working at this low speed. + For details see https://github.com/jgromes/RadioLib/discussions/938 + */ + clock_configure(clk_peri, + 0, // No glitchless mux + CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, // System PLL on AUX mux + 18 * MHZ, // Input frequency + 18 * MHZ // Output (must be same as no divider) + ); + /* Run also ADC on lower clk_sys. */ + clock_configure(clk_adc, 0, CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 18 * MHZ, 18 * MHZ); + /* Run RTC from XOSC since USB clock is off */ + clock_configure(clk_rtc, 0, CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC, 12 * MHZ, 47 * KHZ); + /* Turn off USB PLL */ + pll_deinit(pll_usb); } #endif \ No newline at end of file diff --git a/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h b/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h index 17dff2468..e26f67cfe 100644 --- a/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h +++ b/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h @@ -42,18 +42,12 @@ void sleep_run_from_dormant_source(dormant_source_t dormant_source); /*! \brief Set the dormant clock source to be the crystal oscillator * \ingroup hardware_sleep */ -static inline void sleep_run_from_xosc(void) -{ - sleep_run_from_dormant_source(DORMANT_SOURCE_XOSC); -} +static inline void sleep_run_from_xosc(void) { sleep_run_from_dormant_source(DORMANT_SOURCE_XOSC); } /*! \brief Set the dormant clock source to be the ring oscillator * \ingroup hardware_sleep */ -static inline void sleep_run_from_rosc(void) -{ - sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); -} +static inline void sleep_run_from_rosc(void) { sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); } /*! \brief Send system to sleep until the specified time * \ingroup hardware_sleep @@ -83,10 +77,7 @@ void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high); * * \param gpio_pin The pin to provide the wake up */ -static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) -{ - sleep_goto_dormant_until_pin(gpio_pin, true, true); -} +static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) { sleep_goto_dormant_until_pin(gpio_pin, true, true); } /*! \brief Send system to sleep until a high level is detected on GPIO * \ingroup hardware_sleep @@ -95,10 +86,7 @@ static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) * * \param gpio_pin The pin to provide the wake up */ -static inline void sleep_goto_dormant_until_level_high(uint gpio_pin) -{ - sleep_goto_dormant_until_pin(gpio_pin, false, true); -} +static inline void sleep_goto_dormant_until_level_high(uint gpio_pin) { sleep_goto_dormant_until_pin(gpio_pin, false, true); } #ifdef __cplusplus } diff --git a/src/platform/rp2xx0/pico_sleep/sleep.c b/src/platform/rp2xx0/pico_sleep/sleep.c index 65096be85..291b983fa 100644 --- a/src/platform/rp2xx0/pico_sleep/sleep.c +++ b/src/platform/rp2xx0/pico_sleep/sleep.c @@ -33,123 +33,118 @@ static dormant_source_t _dormant_source; -bool dormant_source_valid(dormant_source_t dormant_source) -{ - return (dormant_source == DORMANT_SOURCE_XOSC) || (dormant_source == DORMANT_SOURCE_ROSC); +bool dormant_source_valid(dormant_source_t dormant_source) { + return (dormant_source == DORMANT_SOURCE_XOSC) || (dormant_source == DORMANT_SOURCE_ROSC); } // In order to go into dormant mode we need to be running from a stoppable clock source: // either the xosc or rosc with no PLLs running. This means we disable the USB and ADC clocks // and all PLLs -void sleep_run_from_dormant_source(dormant_source_t dormant_source) -{ - assert(dormant_source_valid(dormant_source)); - _dormant_source = dormant_source; +void sleep_run_from_dormant_source(dormant_source_t dormant_source) { + assert(dormant_source_valid(dormant_source)); + _dormant_source = dormant_source; - // FIXME: Just defining average rosc freq here. - uint src_hz = (dormant_source == DORMANT_SOURCE_XOSC) ? XOSC_HZ : 6.5 * MHZ; - uint clk_ref_src = (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC - : CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH; + // FIXME: Just defining average rosc freq here. + uint src_hz = (dormant_source == DORMANT_SOURCE_XOSC) ? XOSC_HZ : 6.5 * MHZ; + uint clk_ref_src = + (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC : CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH; - // CLK_REF = XOSC or ROSC - clock_configure(clk_ref, clk_ref_src, - 0, // No aux mux - src_hz, src_hz); + // CLK_REF = XOSC or ROSC + clock_configure(clk_ref, clk_ref_src, + 0, // No aux mux + src_hz, src_hz); - // CLK SYS = CLK_REF - clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, - 0, // Using glitchless mux - src_hz, src_hz); + // CLK SYS = CLK_REF + clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, + 0, // Using glitchless mux + src_hz, src_hz); - // CLK USB = 0MHz - clock_stop(clk_usb); + // CLK USB = 0MHz + clock_stop(clk_usb); - // CLK ADC = 0MHz - clock_stop(clk_adc); + // CLK ADC = 0MHz + clock_stop(clk_adc); - // CLK RTC = ideally XOSC (12MHz) / 256 = 46875Hz but could be rosc - uint clk_rtc_src = (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC - : CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_ROSC_CLKSRC_PH; + // CLK RTC = ideally XOSC (12MHz) / 256 = 46875Hz but could be rosc + uint clk_rtc_src = + (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC : CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_ROSC_CLKSRC_PH; - clock_configure(clk_rtc, - 0, // No GLMUX - clk_rtc_src, src_hz, 46875); + clock_configure(clk_rtc, + 0, // No GLMUX + clk_rtc_src, src_hz, 46875); - // CLK PERI = clk_sys. Used as reference clock for Peripherals. No dividers so just select and enable - clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, src_hz, src_hz); + // CLK PERI = clk_sys. Used as reference clock for Peripherals. No dividers so just select and enable + clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, src_hz, src_hz); - pll_deinit(pll_sys); - pll_deinit(pll_usb); + pll_deinit(pll_sys); + pll_deinit(pll_usb); - // Assuming both xosc and rosc are running at the moment - if (dormant_source == DORMANT_SOURCE_XOSC) { - // Can disable rosc - rosc_disable(); - } else { - // Can disable xosc - xosc_disable(); - } + // Assuming both xosc and rosc are running at the moment + if (dormant_source == DORMANT_SOURCE_XOSC) { + // Can disable rosc + rosc_disable(); + } else { + // Can disable xosc + xosc_disable(); + } - // Reconfigure uart with new clocks - /* This dones not work with our current core */ - // setup_default_uart(); + // Reconfigure uart with new clocks + /* This dones not work with our current core */ + // setup_default_uart(); } // Go to sleep until woken up by the RTC -void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback) -{ - // We should have already called the sleep_run_from_dormant_source function - assert(dormant_source_valid(_dormant_source)); +void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback) { + // We should have already called the sleep_run_from_dormant_source function + assert(dormant_source_valid(_dormant_source)); - // Turn off all clocks when in sleep mode except for RTC - clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS; - clocks_hw->sleep_en1 = 0x0; + // Turn off all clocks when in sleep mode except for RTC + clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS; + clocks_hw->sleep_en1 = 0x0; - rtc_set_alarm(t, callback); + rtc_set_alarm(t, callback); - uint save = scb_hw->scr; - // Enable deep sleep at the proc - scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS; + uint save = scb_hw->scr; + // Enable deep sleep at the proc + scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS; - // Go to sleep - __wfi(); + // Go to sleep + __wfi(); } -static void _go_dormant(void) -{ - assert(dormant_source_valid(_dormant_source)); +static void _go_dormant(void) { + assert(dormant_source_valid(_dormant_source)); - if (_dormant_source == DORMANT_SOURCE_XOSC) { - xosc_dormant(); - } else { - rosc_set_dormant(); - } + if (_dormant_source == DORMANT_SOURCE_XOSC) { + xosc_dormant(); + } else { + rosc_set_dormant(); + } } -void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high) -{ - bool low = !high; - bool level = !edge; +void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high) { + bool low = !high; + bool level = !edge; - // Configure the appropriate IRQ at IO bank 0 - assert(gpio_pin < NUM_BANK0_GPIOS); + // Configure the appropriate IRQ at IO bank 0 + assert(gpio_pin < NUM_BANK0_GPIOS); - uint32_t event = 0; + uint32_t event = 0; - if (level && low) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_LOW_BITS; - if (level && high) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_HIGH_BITS; - if (edge && high) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_HIGH_BITS; - if (edge && low) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_LOW_BITS; + if (level && low) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_LOW_BITS; + if (level && high) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_HIGH_BITS; + if (edge && high) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_HIGH_BITS; + if (edge && low) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_LOW_BITS; - gpio_set_dormant_irq_enabled(gpio_pin, event, true); + gpio_set_dormant_irq_enabled(gpio_pin, event, true); - _go_dormant(); - // Execution stops here until woken up + _go_dormant(); + // Execution stops here until woken up - // Clear the irq so we can go back to dormant mode again if we want - gpio_acknowledge_irq(gpio_pin, event); + // Clear the irq so we can go back to dormant mode again if we want + gpio_acknowledge_irq(gpio_pin, event); } \ No newline at end of file diff --git a/src/platform/stm32wl/LittleFS.cpp b/src/platform/stm32wl/LittleFS.cpp index 40f32eca8..3609602ba 100644 --- a/src/platform/stm32wl/LittleFS.cpp +++ b/src/platform/stm32wl/LittleFS.cpp @@ -55,96 +55,92 @@ // LFS Disk IO //--------------------------------------------------------------------+ -static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) -{ - LFS_UNUSED(c); +static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { + LFS_UNUSED(c); - if (!buffer || !size) { - _LFS_DBG("%s Invalid parameter!\r\n", __func__); - return LFS_ERR_INVAL; - } + if (!buffer || !size) { + _LFS_DBG("%s Invalid parameter!\r\n", __func__); + return LFS_ERR_INVAL; + } - lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); - memcpy(buffer, (void *)address, size); + memcpy(buffer, (void *)address, size); - return LFS_ERR_OK; + return LFS_ERR_OK; } // Program a region in a block. The block must have previously // been erased. Negative error codes are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. -static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) -{ - lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); - HAL_StatusTypeDef hal_rc = HAL_OK; - uint32_t dw_count = size / 8; - uint64_t *bufp = (uint64_t *)buffer; +static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); + HAL_StatusTypeDef hal_rc = HAL_OK; + uint32_t dw_count = size / 8; + uint64_t *bufp = (uint64_t *)buffer; - LFS_UNUSED(c); + LFS_UNUSED(c); - _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); - if (HAL_FLASH_Unlock() != HAL_OK) { - return LFS_ERR_IO; + _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); + if (HAL_FLASH_Unlock() != HAL_OK) { + return LFS_ERR_IO; + } + for (uint32_t i = 0; i < dw_count; i++) { + if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { + _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address); + HAL_FLASH_Lock(); + return LFS_ERR_INVAL; } - for (uint32_t i = 0; i < dw_count; i++) { - if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { - _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address); - HAL_FLASH_Lock(); - return LFS_ERR_INVAL; - } - hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); - if (hal_rc != HAL_OK) { - /* Error occurred while writing data in Flash memory. - * User can add here some code to deal with this error. - */ - _LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError()); - } - address += 8; - bufp += 1; - } - if (HAL_FLASH_Lock() != HAL_OK) { - return LFS_ERR_IO; + hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); + if (hal_rc != HAL_OK) { + /* Error occurred while writing data in Flash memory. + * User can add here some code to deal with this error. + */ + _LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError()); } + address += 8; + bufp += 1; + } + if (HAL_FLASH_Lock() != HAL_OK) { + return LFS_ERR_IO; + } - return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO } // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes // are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. -static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) -{ - lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE); - HAL_StatusTypeDef hal_rc; - FLASH_EraseInitTypeDef EraseInitStruct = {.TypeErase = FLASH_TYPEERASE_PAGES, .Page = 0, .NbPages = 1}; - uint32_t PAGEError = 0; +static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) { + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE); + HAL_StatusTypeDef hal_rc; + FLASH_EraseInitTypeDef EraseInitStruct = {.TypeErase = FLASH_TYPEERASE_PAGES, .Page = 0, .NbPages = 1}; + uint32_t PAGEError = 0; - LFS_UNUSED(c); + LFS_UNUSED(c); - if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { - _LFS_DBG("Wanted to erase out of bound of FLASH: 0x%08x.\n", address); - return LFS_ERR_INVAL; - } - /* calculate the absolute page, i.e. what the ST wants */ - EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE; - _LFS_DBG("Erasing block %d at 0x%08x... ", block, address); - HAL_FLASH_Unlock(); - hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); - HAL_FLASH_Lock(); + if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { + _LFS_DBG("Wanted to erase out of bound of FLASH: 0x%08x.\n", address); + return LFS_ERR_INVAL; + } + /* calculate the absolute page, i.e. what the ST wants */ + EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE; + _LFS_DBG("Erasing block %d at 0x%08x... ", block, address); + HAL_FLASH_Unlock(); + hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); + HAL_FLASH_Lock(); - return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO } // Sync the state of the underlying block device. Negative error codes // are propogated to the user. -static int _internal_flash_sync(const struct lfs_config *c) -{ - LFS_UNUSED(c); - // write function performs no caching. No need for sync. +static int _internal_flash_sync(const struct lfs_config *c) { + LFS_UNUSED(c); + // write function performs no caching. No need for sync. - return LFS_ERR_OK; + return LFS_ERR_OK; } static struct lfs_config _InternalFSConfig = {.context = NULL, @@ -173,26 +169,25 @@ LittleFS InternalFS; LittleFS::LittleFS(void) : STM32_LittleFS(&_InternalFSConfig) {} -bool LittleFS::begin(void) -{ - if (FLASH_BASE >= LFS_FLASH_ADDR_BASE) { - /* There is not enough space on this device for a filesystem. */ - return false; - } - // failed to mount, erase all pages then format and mount again - if (!STM32_LittleFS::begin()) { - // Erase all pages of internal flash region for Filesystem. - for (uint32_t addr = LFS_FLASH_ADDR_BASE; addr < (LFS_FLASH_ADDR_END + 1); addr += STM32WL_PAGE_SIZE) { - _internal_flash_erase(&_InternalFSConfig, (addr - LFS_FLASH_ADDR_BASE) / STM32WL_PAGE_SIZE); - } - - // lfs format - this->format(); - - // mount again if still failed, give up - if (!STM32_LittleFS::begin()) - return false; +bool LittleFS::begin(void) { + if (FLASH_BASE >= LFS_FLASH_ADDR_BASE) { + /* There is not enough space on this device for a filesystem. */ + return false; + } + // failed to mount, erase all pages then format and mount again + if (!STM32_LittleFS::begin()) { + // Erase all pages of internal flash region for Filesystem. + for (uint32_t addr = LFS_FLASH_ADDR_BASE; addr < (LFS_FLASH_ADDR_END + 1); addr += STM32WL_PAGE_SIZE) { + _internal_flash_erase(&_InternalFSConfig, (addr - LFS_FLASH_ADDR_BASE) / STM32WL_PAGE_SIZE); } - return true; + // lfs format + this->format(); + + // mount again if still failed, give up + if (!STM32_LittleFS::begin()) + return false; + } + + return true; } diff --git a/src/platform/stm32wl/LittleFS.h b/src/platform/stm32wl/LittleFS.h index 6c3c47f91..9c823fb1f 100644 --- a/src/platform/stm32wl/LittleFS.h +++ b/src/platform/stm32wl/LittleFS.h @@ -27,13 +27,12 @@ #include "STM32_LittleFS.h" -class LittleFS : public STM32_LittleFS -{ - public: - LittleFS(void); +class LittleFS : public STM32_LittleFS { +public: + LittleFS(void); - // overwrite to also perform low level format (sector erase of whole flash region) - bool begin(void); + // overwrite to also perform low level format (sector erase of whole flash region) + bool begin(void); }; extern LittleFS InternalFS; diff --git a/src/platform/stm32wl/STM32_LittleFS.cpp b/src/platform/stm32wl/STM32_LittleFS.cpp index 97e79e61e..23d09b62c 100644 --- a/src/platform/stm32wl/STM32_LittleFS.cpp +++ b/src/platform/stm32wl/STM32_LittleFS.cpp @@ -37,11 +37,10 @@ using namespace STM32_LittleFS_Namespace; STM32_LittleFS::STM32_LittleFS(void) : STM32_LittleFS(NULL) {} -STM32_LittleFS::STM32_LittleFS(struct lfs_config *cfg) -{ - varclr(&_lfs); - _lfs_cfg = cfg; - _mounted = false; +STM32_LittleFS::STM32_LittleFS(struct lfs_config *cfg) { + varclr(&_lfs); + _lfs_cfg = cfg; + _mounted = false; } STM32_LittleFS::~STM32_LittleFS() {} @@ -49,235 +48,224 @@ STM32_LittleFS::~STM32_LittleFS() {} // Initialize and mount the file system // Return true if mounted successfully else probably corrupted. // User should format the disk and try again -bool STM32_LittleFS::begin(struct lfs_config *cfg) -{ - _lockFS(); +bool STM32_LittleFS::begin(struct lfs_config *cfg) { + _lockFS(); - bool ret; - // not a loop, just an quick way to short-circuit on error - do { - if (_mounted) { - ret = true; - break; - } - if (cfg) { - _lfs_cfg = cfg; - } - if (nullptr == _lfs_cfg) { - ret = false; - break; - } - // actually attempt to mount, and log error if one occurs - int err = lfs_mount(&_lfs, _lfs_cfg); - PRINT_LFS_ERR(err); - _mounted = (err == LFS_ERR_OK); - ret = _mounted; - } while (0); + bool ret; + // not a loop, just an quick way to short-circuit on error + do { + if (_mounted) { + ret = true; + break; + } + if (cfg) { + _lfs_cfg = cfg; + } + if (nullptr == _lfs_cfg) { + ret = false; + break; + } + // actually attempt to mount, and log error if one occurs + int err = lfs_mount(&_lfs, _lfs_cfg); + PRINT_LFS_ERR(err); + _mounted = (err == LFS_ERR_OK); + ret = _mounted; + } while (0); - _unlockFS(); - return ret; + _unlockFS(); + return ret; } // Tear down and unmount file system -void STM32_LittleFS::end(void) -{ - _lockFS(); +void STM32_LittleFS::end(void) { + _lockFS(); - if (_mounted) { - _mounted = false; - int err = lfs_unmount(&_lfs); - PRINT_LFS_ERR(err); - (void)err; - } + if (_mounted) { + _mounted = false; + int err = lfs_unmount(&_lfs); + PRINT_LFS_ERR(err); + (void)err; + } - _unlockFS(); + _unlockFS(); } -bool STM32_LittleFS::format(void) -{ - _lockFS(); +bool STM32_LittleFS::format(void) { + _lockFS(); - int err = LFS_ERR_OK; - bool attemptMount = _mounted; - // not a loop, just an quick way to short-circuit on error - do { - // if already mounted: umount first -> format -> remount - if (_mounted) { - _mounted = false; - err = lfs_unmount(&_lfs); - if (LFS_ERR_OK != err) { - PRINT_LFS_ERR(err); - break; - } - } - err = lfs_format(&_lfs, _lfs_cfg); - if (LFS_ERR_OK != err) { - PRINT_LFS_ERR(err); - break; - } + int err = LFS_ERR_OK; + bool attemptMount = _mounted; + // not a loop, just an quick way to short-circuit on error + do { + // if already mounted: umount first -> format -> remount + if (_mounted) { + _mounted = false; + err = lfs_unmount(&_lfs); + if (LFS_ERR_OK != err) { + PRINT_LFS_ERR(err); + break; + } + } + err = lfs_format(&_lfs, _lfs_cfg); + if (LFS_ERR_OK != err) { + PRINT_LFS_ERR(err); + break; + } - if (attemptMount) { - err = lfs_mount(&_lfs, _lfs_cfg); - if (LFS_ERR_OK != err) { - PRINT_LFS_ERR(err); - break; - } - _mounted = true; - } - // success! - } while (0); + if (attemptMount) { + err = lfs_mount(&_lfs, _lfs_cfg); + if (LFS_ERR_OK != err) { + PRINT_LFS_ERR(err); + break; + } + _mounted = true; + } + // success! + } while (0); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Open a file or folder -STM32_LittleFS_Namespace::File STM32_LittleFS::open(char const *filepath, uint8_t mode) -{ - // No lock is required here ... the File() object will synchronize with the mutex provided - return STM32_LittleFS_Namespace::File(filepath, mode, *this); +STM32_LittleFS_Namespace::File STM32_LittleFS::open(char const *filepath, uint8_t mode) { + // No lock is required here ... the File() object will synchronize with the mutex provided + return STM32_LittleFS_Namespace::File(filepath, mode, *this); } // Check if file or folder exists -bool STM32_LittleFS::exists(char const *filepath) -{ - struct lfs_info info; - _lockFS(); +bool STM32_LittleFS::exists(char const *filepath) { + struct lfs_info info; + _lockFS(); - bool ret = (0 == lfs_stat(&_lfs, filepath, &info)); + bool ret = (0 == lfs_stat(&_lfs, filepath, &info)); - _unlockFS(); - return ret; + _unlockFS(); + return ret; } // Create a directory, create intermediate parent if needed -bool STM32_LittleFS::mkdir(char const *filepath) -{ - bool ret = true; - const char *slash = filepath; - if (slash[0] == '/') - slash++; // skip root '/' +bool STM32_LittleFS::mkdir(char const *filepath) { + bool ret = true; + const char *slash = filepath; + if (slash[0] == '/') + slash++; // skip root '/' - _lockFS(); + _lockFS(); - // make intermediate parent directory(ies) - while (NULL != (slash = strchr(slash, '/'))) { - char parent[slash - filepath + 1] = {0}; - memcpy(parent, filepath, slash - filepath); + // make intermediate parent directory(ies) + while (NULL != (slash = strchr(slash, '/'))) { + char parent[slash - filepath + 1] = {0}; + memcpy(parent, filepath, slash - filepath); - int rc = lfs_mkdir(&_lfs, parent); - if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { - PRINT_LFS_ERR(rc); - ret = false; - break; - } - slash++; + int rc = lfs_mkdir(&_lfs, parent); + if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { + PRINT_LFS_ERR(rc); + ret = false; + break; } - // make the final requested directory - if (ret) { - int rc = lfs_mkdir(&_lfs, filepath); - if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { - PRINT_LFS_ERR(rc); - ret = false; - } + slash++; + } + // make the final requested directory + if (ret) { + int rc = lfs_mkdir(&_lfs, filepath); + if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { + PRINT_LFS_ERR(rc); + ret = false; } + } - _unlockFS(); - return ret; + _unlockFS(); + return ret; } // Remove a file -bool STM32_LittleFS::remove(char const *filepath) -{ - _lockFS(); +bool STM32_LittleFS::remove(char const *filepath) { + _lockFS(); - int err = lfs_remove(&_lfs, filepath); - PRINT_LFS_ERR(err); + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Rename a file -bool STM32_LittleFS::rename(char const *oldfilepath, char const *newfilepath) -{ - _lockFS(); +bool STM32_LittleFS::rename(char const *oldfilepath, char const *newfilepath) { + _lockFS(); - int err = lfs_rename(&_lfs, oldfilepath, newfilepath); - PRINT_LFS_ERR(err); + int err = lfs_rename(&_lfs, oldfilepath, newfilepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Remove a folder -bool STM32_LittleFS::rmdir(char const *filepath) -{ - _lockFS(); +bool STM32_LittleFS::rmdir(char const *filepath) { + _lockFS(); - int err = lfs_remove(&_lfs, filepath); - PRINT_LFS_ERR(err); + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Remove a folder recursively -bool STM32_LittleFS::rmdir_r(char const *filepath) -{ - /* lfs is modified to remove non-empty folder, - According to below issue, comment these 2 line won't corrupt filesystem - at least when using LFS v1. If moving to LFS v2, see tracked issue - to see if issues (such as the orphans in threaded linked list) are resolved. - https://github.com/ARMmbed/littlefs/issues/43 - */ - _lockFS(); +bool STM32_LittleFS::rmdir_r(char const *filepath) { + /* lfs is modified to remove non-empty folder, + According to below issue, comment these 2 line won't corrupt filesystem + at least when using LFS v1. If moving to LFS v2, see tracked issue + to see if issues (such as the orphans in threaded linked list) are resolved. + https://github.com/ARMmbed/littlefs/issues/43 + */ + _lockFS(); - int err = lfs_remove(&_lfs, filepath); - PRINT_LFS_ERR(err); + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } //------------- Debug -------------// #if CFG_DEBUG -const char *dbg_strerr_lfs(int32_t err) -{ - switch (err) { - case LFS_ERR_OK: - return "LFS_ERR_OK"; - case LFS_ERR_IO: - return "LFS_ERR_IO"; - case LFS_ERR_CORRUPT: - return "LFS_ERR_CORRUPT"; - case LFS_ERR_NOENT: - return "LFS_ERR_NOENT"; - case LFS_ERR_EXIST: - return "LFS_ERR_EXIST"; - case LFS_ERR_NOTDIR: - return "LFS_ERR_NOTDIR"; - case LFS_ERR_ISDIR: - return "LFS_ERR_ISDIR"; - case LFS_ERR_NOTEMPTY: - return "LFS_ERR_NOTEMPTY"; - case LFS_ERR_BADF: - return "LFS_ERR_BADF"; - case LFS_ERR_INVAL: - return "LFS_ERR_INVAL"; - case LFS_ERR_NOSPC: - return "LFS_ERR_NOSPC"; - case LFS_ERR_NOMEM: - return "LFS_ERR_NOMEM"; +const char *dbg_strerr_lfs(int32_t err) { + switch (err) { + case LFS_ERR_OK: + return "LFS_ERR_OK"; + case LFS_ERR_IO: + return "LFS_ERR_IO"; + case LFS_ERR_CORRUPT: + return "LFS_ERR_CORRUPT"; + case LFS_ERR_NOENT: + return "LFS_ERR_NOENT"; + case LFS_ERR_EXIST: + return "LFS_ERR_EXIST"; + case LFS_ERR_NOTDIR: + return "LFS_ERR_NOTDIR"; + case LFS_ERR_ISDIR: + return "LFS_ERR_ISDIR"; + case LFS_ERR_NOTEMPTY: + return "LFS_ERR_NOTEMPTY"; + case LFS_ERR_BADF: + return "LFS_ERR_BADF"; + case LFS_ERR_INVAL: + return "LFS_ERR_INVAL"; + case LFS_ERR_NOSPC: + return "LFS_ERR_NOSPC"; + case LFS_ERR_NOMEM: + return "LFS_ERR_NOMEM"; - default: - static char errcode[10]; - sprintf(errcode, "%ld", err); - return errcode; - } + default: + static char errcode[10]; + sprintf(errcode, "%ld", err); + return errcode; + } - return NULL; + return NULL; } #endif diff --git a/src/platform/stm32wl/STM32_LittleFS.h b/src/platform/stm32wl/STM32_LittleFS.h index 9460ffa81..2ebf9db3d 100644 --- a/src/platform/stm32wl/STM32_LittleFS.h +++ b/src/platform/stm32wl/STM32_LittleFS.h @@ -33,60 +33,57 @@ #include "STM32_LittleFS_File.h" #include "littlefs/lfs.h" -class STM32_LittleFS -{ - public: - STM32_LittleFS(void); - explicit STM32_LittleFS(struct lfs_config *cfg); - virtual ~STM32_LittleFS(); +class STM32_LittleFS { +public: + STM32_LittleFS(void); + explicit STM32_LittleFS(struct lfs_config *cfg); + virtual ~STM32_LittleFS(); - bool begin(struct lfs_config *cfg = NULL); - void end(void); + bool begin(struct lfs_config *cfg = NULL); + void end(void); - // Open the specified file/directory with the supplied mode (e.g. read or - // write, etc). Returns a File object for interacting with the file. - // Note that currently only one file can be open at a time. - STM32_LittleFS_Namespace::File open(char const *filename, uint8_t mode = STM32_LittleFS_Namespace::FILE_O_READ); + // Open the specified file/directory with the supplied mode (e.g. read or + // write, etc). Returns a File object for interacting with the file. + // Note that currently only one file can be open at a time. + STM32_LittleFS_Namespace::File open(char const *filename, uint8_t mode = STM32_LittleFS_Namespace::FILE_O_READ); - // Methods to determine if the requested file path exists. - bool exists(char const *filepath); + // Methods to determine if the requested file path exists. + bool exists(char const *filepath); - // Create the requested directory hierarchy--if intermediate directories - // do not exist they will be created. - bool mkdir(char const *filepath); + // Create the requested directory hierarchy--if intermediate directories + // do not exist they will be created. + bool mkdir(char const *filepath); - // Delete the file. - bool remove(char const *filepath); + // Delete the file. + bool remove(char const *filepath); - // Rename the file. - bool rename(char const *oldfilepath, char const *newfilepath); + // Rename the file. + bool rename(char const *oldfilepath, char const *newfilepath); - // Delete a folder (must be empty) - bool rmdir(char const *filepath); + // Delete a folder (must be empty) + bool rmdir(char const *filepath); - // Delete a folder (recursively) - bool rmdir_r(char const *filepath); + // Delete a folder (recursively) + bool rmdir_r(char const *filepath); - // format file system - bool format(void); + // format file system + bool format(void); - /*------------------------------------------------------------------*/ - /* INTERNAL USAGE ONLY - * Although declare as public, it is meant to be invoked by internal - * code. User should not call these directly - *------------------------------------------------------------------*/ - lfs_t *_getFS(void) { return &_lfs; } - void _lockFS(void) - { /* no-op */ - } - void _unlockFS(void) - { /* no-op */ - } + /*------------------------------------------------------------------*/ + /* INTERNAL USAGE ONLY + * Although declare as public, it is meant to be invoked by internal + * code. User should not call these directly + *------------------------------------------------------------------*/ + lfs_t *_getFS(void) { return &_lfs; } + void _lockFS(void) { /* no-op */ + } + void _unlockFS(void) { /* no-op */ + } - protected: - bool _mounted; - struct lfs_config *_lfs_cfg; - lfs_t _lfs; +protected: + bool _mounted; + struct lfs_config *_lfs_cfg; + lfs_t _lfs; }; #if !CFG_DEBUG @@ -94,12 +91,12 @@ class STM32_LittleFS #define PRINT_LFS_ERR(_err) #else #define VERIFY_LFS(...) _GET_3RD_ARG(__VA_ARGS__, VERIFY_ERR_2ARGS, VERIFY_ERR_1ARGS)(__VA_ARGS__, dbg_strerr_lfs) -#define PRINT_LFS_ERR(_err) \ - do { \ - if (_err) { \ - printf("%s:%d, LFS error: %d\n", __FILE__, __LINE__, _err); \ - } \ - } while (0) // LFS_ERR are of type int, VERIFY_MESS expects long_int +#define PRINT_LFS_ERR(_err) \ + do { \ + if (_err) { \ + printf("%s:%d, LFS error: %d\n", __FILE__, __LINE__, _err); \ + } \ + } while (0) // LFS_ERR are of type int, VERIFY_MESS expects long_int const char *dbg_strerr_lfs(int32_t err); #endif diff --git a/src/platform/stm32wl/STM32_LittleFS_File.cpp b/src/platform/stm32wl/STM32_LittleFS_File.cpp index 349187a02..20d8e6b81 100644 --- a/src/platform/stm32wl/STM32_LittleFS_File.cpp +++ b/src/platform/stm32wl/STM32_LittleFS_File.cpp @@ -34,360 +34,324 @@ using namespace STM32_LittleFS_Namespace; -File::File(STM32_LittleFS &fs) -{ - _fs = &fs; - _is_dir = false; - _name[0] = 0; - _name[LFS_NAME_MAX] = 0; - _dir_path = NULL; +File::File(STM32_LittleFS &fs) { + _fs = &fs; + _is_dir = false; + _name[0] = 0; + _name[LFS_NAME_MAX] = 0; + _dir_path = NULL; - _dir = NULL; - _file = NULL; + _dir = NULL; + _file = NULL; } -File::File(char const *filename, uint8_t mode, STM32_LittleFS &fs) : File(fs) -{ - // public constructor calls public API open(), which will obtain the mutex - this->open(filename, mode); +File::File(char const *filename, uint8_t mode, STM32_LittleFS &fs) : File(fs) { + // public constructor calls public API open(), which will obtain the mutex + this->open(filename, mode); } -bool File::_open_file(char const *filepath, uint8_t mode) -{ - int flags = (mode == FILE_O_READ) ? LFS_O_RDONLY : (mode == FILE_O_WRITE) ? (LFS_O_RDWR | LFS_O_CREAT) : 0; +bool File::_open_file(char const *filepath, uint8_t mode) { + int flags = (mode == FILE_O_READ) ? LFS_O_RDONLY : (mode == FILE_O_WRITE) ? (LFS_O_RDWR | LFS_O_CREAT) : 0; - if (flags) { - _file = (lfs_file_t *)rtos_malloc(sizeof(lfs_file_t)); - if (!_file) - return false; + if (flags) { + _file = (lfs_file_t *)rtos_malloc(sizeof(lfs_file_t)); + if (!_file) + return false; - int rc = lfs_file_open(_fs->_getFS(), _file, filepath, flags); - - if (rc) { - // failed to open - PRINT_LFS_ERR(rc); - // free memory - rtos_free(_file); - _file = NULL; - return false; - } - - // move to end of file - if (mode == FILE_O_WRITE) - lfs_file_seek(_fs->_getFS(), _file, 0, LFS_SEEK_END); - - _is_dir = false; - } - - return true; -} - -bool File::_open_dir(char const *filepath) -{ - _dir = (lfs_dir_t *)rtos_malloc(sizeof(lfs_dir_t)); - if (!_dir) - return false; - - int rc = lfs_dir_open(_fs->_getFS(), _dir, filepath); + int rc = lfs_file_open(_fs->_getFS(), _file, filepath, flags); if (rc) { - // failed to open - PRINT_LFS_ERR(rc); - // free memory - rtos_free(_dir); - _dir = NULL; - return false; + // failed to open + PRINT_LFS_ERR(rc); + // free memory + rtos_free(_file); + _file = NULL; + return false; } - _is_dir = true; + // move to end of file + if (mode == FILE_O_WRITE) + lfs_file_seek(_fs->_getFS(), _file, 0, LFS_SEEK_END); - _dir_path = (char *)rtos_malloc(strlen(filepath) + 1); - strcpy(_dir_path, filepath); + _is_dir = false; + } - return true; + return true; } -bool File::open(char const *filepath, uint8_t mode) -{ - bool ret = false; - _fs->_lockFS(); +bool File::_open_dir(char const *filepath) { + _dir = (lfs_dir_t *)rtos_malloc(sizeof(lfs_dir_t)); + if (!_dir) + return false; - ret = this->_open(filepath, mode); + int rc = lfs_dir_open(_fs->_getFS(), _dir, filepath); - _fs->_unlockFS(); - return ret; + if (rc) { + // failed to open + PRINT_LFS_ERR(rc); + // free memory + rtos_free(_dir); + _dir = NULL; + return false; + } + + _is_dir = true; + + _dir_path = (char *)rtos_malloc(strlen(filepath) + 1); + strcpy(_dir_path, filepath); + + return true; } -bool File::_open(char const *filepath, uint8_t mode) -{ - bool ret = false; +bool File::open(char const *filepath, uint8_t mode) { + bool ret = false; + _fs->_lockFS(); - // close if currently opened - if (this->isOpen()) - _close(); + ret = this->_open(filepath, mode); - struct lfs_info info; - int rc = lfs_stat(_fs->_getFS(), filepath, &info); + _fs->_unlockFS(); + return ret; +} - if (LFS_ERR_OK == rc) { - // file existed, open file or directory accordingly - ret = (info.type == LFS_TYPE_REG) ? _open_file(filepath, mode) : _open_dir(filepath); - } else if (LFS_ERR_NOENT == rc) { - // file not existed, only proceed with FILE_O_WRITE mode - if (mode == FILE_O_WRITE) - ret = _open_file(filepath, mode); +bool File::_open(char const *filepath, uint8_t mode) { + bool ret = false; + + // close if currently opened + if (this->isOpen()) + _close(); + + struct lfs_info info; + int rc = lfs_stat(_fs->_getFS(), filepath, &info); + + if (LFS_ERR_OK == rc) { + // file existed, open file or directory accordingly + ret = (info.type == LFS_TYPE_REG) ? _open_file(filepath, mode) : _open_dir(filepath); + } else if (LFS_ERR_NOENT == rc) { + // file not existed, only proceed with FILE_O_WRITE mode + if (mode == FILE_O_WRITE) + ret = _open_file(filepath, mode); + } else { + PRINT_LFS_ERR(rc); + } + + // save bare file name + if (ret) { + char const *splash = strrchr(filepath, '/'); + strncpy(_name, splash ? (splash + 1) : filepath, LFS_NAME_MAX); + } + return ret; +} + +size_t File::write(uint8_t ch) { return write(&ch, 1); } + +size_t File::write(uint8_t const *buf, size_t size) { + lfs_ssize_t wrcount = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); + if (wrcount < 0) { + wrcount = 0; + } + } + + _fs->_unlockFS(); + return wrcount; +} + +int File::read(void) { + // this thin wrapper relies on called function to synchronize + int ret = -1; + uint8_t ch; + if (read(&ch, 1) > 0) { + ret = static_cast(ch); + } + return ret; +} + +int File::read(void *buf, uint16_t nbyte) { + int ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_read(_fs->_getFS(), _file, buf, nbyte); + } + + _fs->_unlockFS(); + return ret; +} + +int File::peek(void) { + int ret = -1; + _fs->_lockFS(); + + if (!this->_is_dir) { + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + uint8_t ch = 0; + if (lfs_file_read(_fs->_getFS(), _file, &ch, 1) > 0) { + ret = static_cast(ch); + } + (void)lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET); + } + + _fs->_unlockFS(); + return ret; +} + +int File::available(void) { + int ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + uint32_t file_size = lfs_file_size(_fs->_getFS(), _file); + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + ret = file_size - pos; + } + + _fs->_unlockFS(); + return ret; +} + +bool File::seek(uint32_t pos) { + bool ret = false; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET) >= 0; + } + + _fs->_unlockFS(); + return ret; +} + +uint32_t File::position(void) { + uint32_t ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_tell(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return ret; +} + +uint32_t File::size(void) { + uint32_t ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_size(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return ret; +} + +bool File::truncate(uint32_t pos) { + int32_t ret = LFS_ERR_ISDIR; + _fs->_lockFS(); + if (!this->_is_dir) { + ret = lfs_file_truncate(_fs->_getFS(), _file, pos); + } + _fs->_unlockFS(); + return (ret == 0); +} + +bool File::truncate(void) { + int32_t ret = LFS_ERR_ISDIR; + _fs->_lockFS(); + if (!this->_is_dir) { + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + ret = lfs_file_truncate(_fs->_getFS(), _file, pos); + } + _fs->_unlockFS(); + return (ret == 0); +} + +void File::flush(void) { + _fs->_lockFS(); + + if (!this->_is_dir) { + lfs_file_sync(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return; +} + +void File::close(void) { + _fs->_lockFS(); + this->_close(); + _fs->_unlockFS(); +} + +void File::_close(void) { + if (this->isOpen()) { + if (this->_is_dir) { + lfs_dir_close(_fs->_getFS(), _dir); + rtos_free(_dir); + _dir = NULL; + + if (this->_dir_path) + rtos_free(_dir_path); + _dir_path = NULL; } else { - PRINT_LFS_ERR(rc); + lfs_file_close(this->_fs->_getFS(), _file); + rtos_free(_file); + _file = NULL; } - - // save bare file name - if (ret) { - char const *splash = strrchr(filepath, '/'); - strncpy(_name, splash ? (splash + 1) : filepath, LFS_NAME_MAX); - } - return ret; + } } -size_t File::write(uint8_t ch) -{ - return write(&ch, 1); -} +File::operator bool(void) { return isOpen(); } -size_t File::write(uint8_t const *buf, size_t size) -{ - lfs_ssize_t wrcount = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); - if (wrcount < 0) { - wrcount = 0; - } - } - - _fs->_unlockFS(); - return wrcount; -} - -int File::read(void) -{ - // this thin wrapper relies on called function to synchronize - int ret = -1; - uint8_t ch; - if (read(&ch, 1) > 0) { - ret = static_cast(ch); - } - return ret; -} - -int File::read(void *buf, uint16_t nbyte) -{ - int ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_read(_fs->_getFS(), _file, buf, nbyte); - } - - _fs->_unlockFS(); - return ret; -} - -int File::peek(void) -{ - int ret = -1; - _fs->_lockFS(); - - if (!this->_is_dir) { - uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); - uint8_t ch = 0; - if (lfs_file_read(_fs->_getFS(), _file, &ch, 1) > 0) { - ret = static_cast(ch); - } - (void)lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET); - } - - _fs->_unlockFS(); - return ret; -} - -int File::available(void) -{ - int ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - uint32_t file_size = lfs_file_size(_fs->_getFS(), _file); - uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); - ret = file_size - pos; - } - - _fs->_unlockFS(); - return ret; -} - -bool File::seek(uint32_t pos) -{ - bool ret = false; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET) >= 0; - } - - _fs->_unlockFS(); - return ret; -} - -uint32_t File::position(void) -{ - uint32_t ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_tell(_fs->_getFS(), _file); - } - - _fs->_unlockFS(); - return ret; -} - -uint32_t File::size(void) -{ - uint32_t ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_size(_fs->_getFS(), _file); - } - - _fs->_unlockFS(); - return ret; -} - -bool File::truncate(uint32_t pos) -{ - int32_t ret = LFS_ERR_ISDIR; - _fs->_lockFS(); - if (!this->_is_dir) { - ret = lfs_file_truncate(_fs->_getFS(), _file, pos); - } - _fs->_unlockFS(); - return (ret == 0); -} - -bool File::truncate(void) -{ - int32_t ret = LFS_ERR_ISDIR; - _fs->_lockFS(); - if (!this->_is_dir) { - uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); - ret = lfs_file_truncate(_fs->_getFS(), _file, pos); - } - _fs->_unlockFS(); - return (ret == 0); -} - -void File::flush(void) -{ - _fs->_lockFS(); - - if (!this->_is_dir) { - lfs_file_sync(_fs->_getFS(), _file); - } - - _fs->_unlockFS(); - return; -} - -void File::close(void) -{ - _fs->_lockFS(); - this->_close(); - _fs->_unlockFS(); -} - -void File::_close(void) -{ - if (this->isOpen()) { - if (this->_is_dir) { - lfs_dir_close(_fs->_getFS(), _dir); - rtos_free(_dir); - _dir = NULL; - - if (this->_dir_path) - rtos_free(_dir_path); - _dir_path = NULL; - } else { - lfs_file_close(this->_fs->_getFS(), _file); - rtos_free(_file); - _file = NULL; - } - } -} - -File::operator bool(void) -{ - return isOpen(); -} - -bool File::isOpen(void) -{ - return (_file != NULL) || (_dir != NULL); -} +bool File::isOpen(void) { return (_file != NULL) || (_dir != NULL); } // WARNING -- although marked as `const`, the values pointed // to may change. For example, if the same File // object has `open()` called with a different // file or directory name, this same pointer will // suddenly (unexpectedly?) have different values. -char const *File::name(void) -{ - return this->_name; -} +char const *File::name(void) { return this->_name; } -bool File::isDirectory(void) -{ - return this->_is_dir; -} +bool File::isDirectory(void) { return this->_is_dir; } -File File::openNextFile(uint8_t mode) -{ - _fs->_lockFS(); +File File::openNextFile(uint8_t mode) { + _fs->_lockFS(); - File ret(*_fs); - if (this->_is_dir) { - struct lfs_info info; - int rc; + File ret(*_fs); + if (this->_is_dir) { + struct lfs_info info; + int rc; - // lfs_dir_read returns 0 when reaching end of directory, 1 if found an entry - // Skip the "." and ".." entries ... - do { - rc = lfs_dir_read(_fs->_getFS(), _dir, &info); - } while (rc == 1 && (!strcmp(".", info.name) || !strcmp("..", info.name))); + // lfs_dir_read returns 0 when reaching end of directory, 1 if found an entry + // Skip the "." and ".." entries ... + do { + rc = lfs_dir_read(_fs->_getFS(), _dir, &info); + } while (rc == 1 && (!strcmp(".", info.name) || !strcmp("..", info.name))); - if (rc == 1) { - // string cat name with current folder - char filepath[strlen(_dir_path) + 1 + strlen(info.name) + 1]; // potential for significant stack usage - strcpy(filepath, _dir_path); - if (!(_dir_path[0] == '/' && _dir_path[1] == 0)) - strcat(filepath, "/"); // only add '/' if cwd is not root - strcat(filepath, info.name); + if (rc == 1) { + // string cat name with current folder + char filepath[strlen(_dir_path) + 1 + strlen(info.name) + 1]; // potential for significant stack usage + strcpy(filepath, _dir_path); + if (!(_dir_path[0] == '/' && _dir_path[1] == 0)) + strcat(filepath, "/"); // only add '/' if cwd is not root + strcat(filepath, info.name); - (void)ret._open(filepath, mode); // return value is ignored ... caller is expected to check isOpened() - } else if (rc < 0) { - PRINT_LFS_ERR(rc); - } + (void)ret._open(filepath, mode); // return value is ignored ... caller is expected to check isOpened() + } else if (rc < 0) { + PRINT_LFS_ERR(rc); } - _fs->_unlockFS(); - return ret; + } + _fs->_unlockFS(); + return ret; } -void File::rewindDirectory(void) -{ - _fs->_lockFS(); - if (this->_is_dir) { - lfs_dir_rewind(_fs->_getFS(), _dir); - } - _fs->_unlockFS(); +void File::rewindDirectory(void) { + _fs->_lockFS(); + if (this->_is_dir) { + lfs_dir_rewind(_fs->_getFS(), _dir); + } + _fs->_unlockFS(); } diff --git a/src/platform/stm32wl/STM32_LittleFS_File.h b/src/platform/stm32wl/STM32_LittleFS_File.h index 2b48b02e0..48b7cfb4b 100644 --- a/src/platform/stm32wl/STM32_LittleFS_File.h +++ b/src/platform/stm32wl/STM32_LittleFS_File.h @@ -30,77 +30,74 @@ // Forward declaration class STM32_LittleFS; -namespace STM32_LittleFS_Namespace -{ +namespace STM32_LittleFS_Namespace { // avoid conflict with other FileSystem FILE_READ/FILE_WRITE enum { - FILE_O_READ = 0, - FILE_O_WRITE = 1, + FILE_O_READ = 0, + FILE_O_WRITE = 1, }; -class File : public Stream -{ - public: - explicit File(STM32_LittleFS &fs); - File(char const *filename, uint8_t mode, STM32_LittleFS &fs); +class File : public Stream { +public: + explicit File(STM32_LittleFS &fs); + File(char const *filename, uint8_t mode, STM32_LittleFS &fs); - public: - bool open(char const *filename, uint8_t mode); +public: + bool open(char const *filename, uint8_t mode); - //------------- Stream API -------------// - virtual size_t write(uint8_t ch); - virtual size_t write(uint8_t const *buf, size_t size); - size_t write(const char *str) - { - if (str == NULL) - return 0; - return write((const uint8_t *)str, strlen(str)); - } - size_t write(const char *buffer, size_t size) { return write((const uint8_t *)buffer, size); } + //------------- Stream API -------------// + virtual size_t write(uint8_t ch); + virtual size_t write(uint8_t const *buf, size_t size); + size_t write(const char *str) { + if (str == NULL) + return 0; + return write((const uint8_t *)str, strlen(str)); + } + size_t write(const char *buffer, size_t size) { return write((const uint8_t *)buffer, size); } - virtual int read(void); - int read(void *buf, uint16_t nbyte); + virtual int read(void); + int read(void *buf, uint16_t nbyte); - virtual int peek(void); - virtual int available(void); - virtual void flush(void); + virtual int peek(void); + virtual int available(void); + virtual void flush(void); - bool seek(uint32_t pos); - uint32_t position(void); - uint32_t size(void); + bool seek(uint32_t pos); + uint32_t position(void); + uint32_t size(void); - bool truncate(uint32_t pos); - bool truncate(void); + bool truncate(uint32_t pos); + bool truncate(void); - void close(void); + void close(void); - operator bool(void); + operator bool(void); - bool isOpen(void); - char const *name(void); + bool isOpen(void); + char const *name(void); - bool isDirectory(void); - File openNextFile(uint8_t mode = FILE_O_READ); - void rewindDirectory(void); + bool isDirectory(void); + File openNextFile(uint8_t mode = FILE_O_READ); + void rewindDirectory(void); - private: - STM32_LittleFS *_fs; +private: + STM32_LittleFS *_fs; - bool _is_dir; + bool _is_dir; - union { - lfs_file_t *_file; - lfs_dir_t *_dir; - }; + union { + lfs_file_t *_file; + lfs_dir_t *_dir; + }; - char *_dir_path; - char _name[LFS_NAME_MAX + 1]; + char *_dir_path; + char _name[LFS_NAME_MAX + 1]; - bool _open(char const *filepath, uint8_t mode); - bool _open_file(char const *filepath, uint8_t mode); - bool _open_dir(char const *filepath); - void _close(void); + bool _open(char const *filepath, uint8_t mode); + bool _open_file(char const *filepath, uint8_t mode); + bool _open_dir(char const *filepath); + void _close(void); }; } // namespace STM32_LittleFS_Namespace diff --git a/src/platform/stm32wl/architecture.h b/src/platform/stm32wl/architecture.h index e131a0a32..cb6b6e15d 100644 --- a/src/platform/stm32wl/architecture.h +++ b/src/platform/stm32wl/architecture.h @@ -34,6 +34,6 @@ #define SX126X_BUSY 1004 #if !defined(DEBUG_MUTE) && !defined(PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF) -#error \ +#error \ "You MUST enable PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF if debug prints are enabled. printf will print uninitialized garbage instead of floats." #endif \ No newline at end of file diff --git a/src/platform/stm32wl/littlefs/lfs.c b/src/platform/stm32wl/littlefs/lfs.c index 5dc4c7669..022070377 100644 --- a/src/platform/stm32wl/littlefs/lfs.c +++ b/src/platform/stm32wl/littlefs/lfs.c @@ -10,239 +10,224 @@ #include /// Caching block device operations /// -static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, - void *buffer, lfs_size_t size) -{ - uint8_t *data = buffer; - LFS_ASSERT(block < lfs->cfg->block_count); +static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, void *buffer, + lfs_size_t size) { + uint8_t *data = buffer; + LFS_ASSERT(block < lfs->cfg->block_count); - while (size > 0) { - if (pcache && block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { - // is already in pcache? - lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); - memcpy(data, &pcache->buffer[off - pcache->off], diff); + while (size > 0) { + if (pcache && block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { + // is already in pcache? + lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); + memcpy(data, &pcache->buffer[off - pcache->off], diff); - data += diff; - off += diff; - size -= diff; - continue; - } + data += diff; + off += diff; + size -= diff; + continue; + } - if (block == rcache->block && off >= rcache->off && off < rcache->off + lfs->cfg->read_size) { - // is already in rcache? - lfs_size_t diff = lfs_min(size, lfs->cfg->read_size - (off - rcache->off)); - memcpy(data, &rcache->buffer[off - rcache->off], diff); + if (block == rcache->block && off >= rcache->off && off < rcache->off + lfs->cfg->read_size) { + // is already in rcache? + lfs_size_t diff = lfs_min(size, lfs->cfg->read_size - (off - rcache->off)); + memcpy(data, &rcache->buffer[off - rcache->off], diff); - data += diff; - off += diff; - size -= diff; - continue; - } + data += diff; + off += diff; + size -= diff; + continue; + } - if (off % lfs->cfg->read_size == 0 && size >= lfs->cfg->read_size) { - // bypass cache? - lfs_size_t diff = size - (size % lfs->cfg->read_size); - int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); - if (err) { - return err; - } + if (off % lfs->cfg->read_size == 0 && size >= lfs->cfg->read_size) { + // bypass cache? + lfs_size_t diff = size - (size % lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); + if (err) { + return err; + } - data += diff; - off += diff; - size -= diff; - continue; - } + data += diff; + off += diff; + size -= diff; + continue; + } - // load to cache, first condition can no longer fail - rcache->block = block; - rcache->off = off - (off % lfs->cfg->read_size); - int err = lfs->cfg->read(lfs->cfg, rcache->block, rcache->off, rcache->buffer, lfs->cfg->read_size); + // load to cache, first condition can no longer fail + rcache->block = block; + rcache->off = off - (off % lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, rcache->block, rcache->off, rcache->buffer, lfs->cfg->read_size); + if (err) { + return err; + } + } + + return 0; +} + +static int lfs_cache_cmp(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, const void *buffer, + lfs_size_t size) { + const uint8_t *data = buffer; + + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); + if (err) { + return err; + } + + if (c != data[i]) { + return false; + } + } + + return true; +} + +static int lfs_cache_crc(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, lfs_size_t size, + uint32_t *crc) { + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); + if (err) { + return err; + } + + lfs_crc(crc, &c, 1); + } + + return 0; +} + +static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { + // do not zero, cheaper if cache is readonly or only going to be + // written with identical data (during relocates) + (void)lfs; + rcache->block = 0xffffffff; +} + +static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) { + // zero to avoid information leak + memset(pcache->buffer, 0xff, lfs->cfg->prog_size); + pcache->block = 0xffffffff; +} + +static int lfs_cache_flush(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache) { + if (pcache->block != 0xffffffff) { + int err = lfs->cfg->prog(lfs->cfg, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); + if (err) { + return err; + } + + if (rcache) { + int res = lfs_cache_cmp(lfs, rcache, NULL, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); + if (res < 0) { + return res; + } + + if (!res) { + return LFS_ERR_CORRUPT; + } + } + + lfs_cache_zero(lfs, pcache); + } + + return 0; +} + +static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_block_t block, lfs_off_t off, const void *buffer, + lfs_size_t size) { + const uint8_t *data = buffer; + LFS_ASSERT(block < lfs->cfg->block_count); + + while (size > 0) { + if (block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { + // is already in pcache? + lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); + memcpy(&pcache->buffer[off - pcache->off], data, diff); + + data += diff; + off += diff; + size -= diff; + + if (off % lfs->cfg->prog_size == 0) { + // eagerly flush out pcache if we fill up + int err = lfs_cache_flush(lfs, pcache, rcache); if (err) { - return err; + return err; } + } + + continue; } - return 0; -} + // pcache must have been flushed, either by programming and + // entire block or manually flushing the pcache + LFS_ASSERT(pcache->block == 0xffffffff); -static int lfs_cache_cmp(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, - const void *buffer, lfs_size_t size) -{ - const uint8_t *data = buffer; + if (off % lfs->cfg->prog_size == 0 && size >= lfs->cfg->prog_size) { + // bypass pcache? + lfs_size_t diff = size - (size % lfs->cfg->prog_size); + int err = lfs->cfg->prog(lfs->cfg, block, off, data, diff); + if (err) { + return err; + } - for (lfs_off_t i = 0; i < size; i++) { - uint8_t c; - int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); - if (err) { - return err; + if (rcache) { + int res = lfs_cache_cmp(lfs, rcache, NULL, block, off, data, diff); + if (res < 0) { + return res; } - if (c != data[i]) { - return false; + if (!res) { + return LFS_ERR_CORRUPT; } + } + + data += diff; + off += diff; + size -= diff; + continue; } - return true; -} + // prepare pcache, first condition can no longer fail + pcache->block = block; + pcache->off = off - (off % lfs->cfg->prog_size); + } -static int lfs_cache_crc(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, - lfs_size_t size, uint32_t *crc) -{ - for (lfs_off_t i = 0; i < size; i++) { - uint8_t c; - int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); - if (err) { - return err; - } - - lfs_crc(crc, &c, 1); - } - - return 0; -} - -static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) -{ - // do not zero, cheaper if cache is readonly or only going to be - // written with identical data (during relocates) - (void)lfs; - rcache->block = 0xffffffff; -} - -static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) -{ - // zero to avoid information leak - memset(pcache->buffer, 0xff, lfs->cfg->prog_size); - pcache->block = 0xffffffff; -} - -static int lfs_cache_flush(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache) -{ - if (pcache->block != 0xffffffff) { - int err = lfs->cfg->prog(lfs->cfg, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); - if (err) { - return err; - } - - if (rcache) { - int res = lfs_cache_cmp(lfs, rcache, NULL, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); - if (res < 0) { - return res; - } - - if (!res) { - return LFS_ERR_CORRUPT; - } - } - - lfs_cache_zero(lfs, pcache); - } - - return 0; -} - -static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_block_t block, lfs_off_t off, - const void *buffer, lfs_size_t size) -{ - const uint8_t *data = buffer; - LFS_ASSERT(block < lfs->cfg->block_count); - - while (size > 0) { - if (block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { - // is already in pcache? - lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); - memcpy(&pcache->buffer[off - pcache->off], data, diff); - - data += diff; - off += diff; - size -= diff; - - if (off % lfs->cfg->prog_size == 0) { - // eagerly flush out pcache if we fill up - int err = lfs_cache_flush(lfs, pcache, rcache); - if (err) { - return err; - } - } - - continue; - } - - // pcache must have been flushed, either by programming and - // entire block or manually flushing the pcache - LFS_ASSERT(pcache->block == 0xffffffff); - - if (off % lfs->cfg->prog_size == 0 && size >= lfs->cfg->prog_size) { - // bypass pcache? - lfs_size_t diff = size - (size % lfs->cfg->prog_size); - int err = lfs->cfg->prog(lfs->cfg, block, off, data, diff); - if (err) { - return err; - } - - if (rcache) { - int res = lfs_cache_cmp(lfs, rcache, NULL, block, off, data, diff); - if (res < 0) { - return res; - } - - if (!res) { - return LFS_ERR_CORRUPT; - } - } - - data += diff; - off += diff; - size -= diff; - continue; - } - - // prepare pcache, first condition can no longer fail - pcache->block = block; - pcache->off = off - (off % lfs->cfg->prog_size); - } - - return 0; + return 0; } /// General lfs block device operations /// -static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) -{ - // if we ever do more than writes to alternating pairs, - // this may need to consider pcache - return lfs_cache_read(lfs, &lfs->rcache, NULL, block, off, buffer, size); +static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { + // if we ever do more than writes to alternating pairs, + // this may need to consider pcache + return lfs_cache_read(lfs, &lfs->rcache, NULL, block, off, buffer, size); } -static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) -{ - return lfs_cache_prog(lfs, &lfs->pcache, NULL, block, off, buffer, size); +static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { + return lfs_cache_prog(lfs, &lfs->pcache, NULL, block, off, buffer, size); } -static int lfs_bd_cmp(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) -{ - return lfs_cache_cmp(lfs, &lfs->rcache, NULL, block, off, buffer, size); +static int lfs_bd_cmp(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { + return lfs_cache_cmp(lfs, &lfs->rcache, NULL, block, off, buffer, size); } -static int lfs_bd_crc(lfs_t *lfs, lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) -{ - return lfs_cache_crc(lfs, &lfs->rcache, NULL, block, off, size, crc); +static int lfs_bd_crc(lfs_t *lfs, lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) { + return lfs_cache_crc(lfs, &lfs->rcache, NULL, block, off, size, crc); } -static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) -{ - return lfs->cfg->erase(lfs->cfg, block); -} +static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { return lfs->cfg->erase(lfs->cfg, block); } -static int lfs_bd_sync(lfs_t *lfs) -{ - lfs_cache_drop(lfs, &lfs->rcache); +static int lfs_bd_sync(lfs_t *lfs) { + lfs_cache_drop(lfs, &lfs->rcache); - int err = lfs_cache_flush(lfs, &lfs->pcache, NULL); - if (err) { - return err; - } + int err = lfs_cache_flush(lfs, &lfs->pcache, NULL); + if (err) { + return err; + } - return lfs->cfg->sync(lfs->cfg); + return lfs->cfg->sync(lfs->cfg); } /// Internal operations predeclared here /// @@ -254,2272 +239,2199 @@ static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_bloc int lfs_deorphan(lfs_t *lfs); /// Block allocator /// -static int lfs_alloc_lookahead(void *p, lfs_block_t block) -{ - lfs_t *lfs = p; +static int lfs_alloc_lookahead(void *p, lfs_block_t block) { + lfs_t *lfs = p; - lfs_block_t off = ((block - lfs->free.off) + lfs->cfg->block_count) % lfs->cfg->block_count; + lfs_block_t off = ((block - lfs->free.off) + lfs->cfg->block_count) % lfs->cfg->block_count; - if (off < lfs->free.size) { - lfs->free.buffer[off / 32] |= 1U << (off % 32); + if (off < lfs->free.size) { + lfs->free.buffer[off / 32] |= 1U << (off % 32); + } + + return 0; +} + +static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { + while (true) { + while (lfs->free.i != lfs->free.size) { + lfs_block_t off = lfs->free.i; + lfs->free.i += 1; + lfs->free.ack -= 1; + + if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { + // found a free block + *block = (lfs->free.off + off) % lfs->cfg->block_count; + + // eagerly find next off so an alloc ack can + // discredit old lookahead blocks + while (lfs->free.i != lfs->free.size && (lfs->free.buffer[lfs->free.i / 32] & (1U << (lfs->free.i % 32)))) { + lfs->free.i += 1; + lfs->free.ack -= 1; + } + + return 0; + } } - return 0; -} - -static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) -{ - while (true) { - while (lfs->free.i != lfs->free.size) { - lfs_block_t off = lfs->free.i; - lfs->free.i += 1; - lfs->free.ack -= 1; - - if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { - // found a free block - *block = (lfs->free.off + off) % lfs->cfg->block_count; - - // eagerly find next off so an alloc ack can - // discredit old lookahead blocks - while (lfs->free.i != lfs->free.size && (lfs->free.buffer[lfs->free.i / 32] & (1U << (lfs->free.i % 32)))) { - lfs->free.i += 1; - lfs->free.ack -= 1; - } - - return 0; - } - } - - // check if we have looked at all blocks since last ack - if (lfs->free.ack == 0) { - LFS_WARN("No more free space %" PRIu32, lfs->free.i + lfs->free.off); - return LFS_ERR_NOSPC; - } - - lfs->free.off = (lfs->free.off + lfs->free.size) % lfs->cfg->block_count; - lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack); - lfs->free.i = 0; - - // find mask of free blocks from tree - memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); - int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs); - if (err) { - return err; - } + // check if we have looked at all blocks since last ack + if (lfs->free.ack == 0) { + LFS_WARN("No more free space %" PRIu32, lfs->free.i + lfs->free.off); + return LFS_ERR_NOSPC; } + + lfs->free.off = (lfs->free.off + lfs->free.size) % lfs->cfg->block_count; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack); + lfs->free.i = 0; + + // find mask of free blocks from tree + memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); + int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs); + if (err) { + return err; + } + } } -static void lfs_alloc_ack(lfs_t *lfs) -{ - lfs->free.ack = lfs->cfg->block_count; -} +static void lfs_alloc_ack(lfs_t *lfs) { lfs->free.ack = lfs->cfg->block_count; } /// Endian swapping functions /// -static void lfs_dir_fromle32(struct lfs_disk_dir *d) -{ - d->rev = lfs_fromle32(d->rev); - d->size = lfs_fromle32(d->size); - d->tail[0] = lfs_fromle32(d->tail[0]); - d->tail[1] = lfs_fromle32(d->tail[1]); +static void lfs_dir_fromle32(struct lfs_disk_dir *d) { + d->rev = lfs_fromle32(d->rev); + d->size = lfs_fromle32(d->size); + d->tail[0] = lfs_fromle32(d->tail[0]); + d->tail[1] = lfs_fromle32(d->tail[1]); } -static void lfs_dir_tole32(struct lfs_disk_dir *d) -{ - d->rev = lfs_tole32(d->rev); - d->size = lfs_tole32(d->size); - d->tail[0] = lfs_tole32(d->tail[0]); - d->tail[1] = lfs_tole32(d->tail[1]); +static void lfs_dir_tole32(struct lfs_disk_dir *d) { + d->rev = lfs_tole32(d->rev); + d->size = lfs_tole32(d->size); + d->tail[0] = lfs_tole32(d->tail[0]); + d->tail[1] = lfs_tole32(d->tail[1]); } -static void lfs_entry_fromle32(struct lfs_disk_entry *d) -{ - d->u.dir[0] = lfs_fromle32(d->u.dir[0]); - d->u.dir[1] = lfs_fromle32(d->u.dir[1]); +static void lfs_entry_fromle32(struct lfs_disk_entry *d) { + d->u.dir[0] = lfs_fromle32(d->u.dir[0]); + d->u.dir[1] = lfs_fromle32(d->u.dir[1]); } -static void lfs_entry_tole32(struct lfs_disk_entry *d) -{ - d->u.dir[0] = lfs_tole32(d->u.dir[0]); - d->u.dir[1] = lfs_tole32(d->u.dir[1]); +static void lfs_entry_tole32(struct lfs_disk_entry *d) { + d->u.dir[0] = lfs_tole32(d->u.dir[0]); + d->u.dir[1] = lfs_tole32(d->u.dir[1]); } -static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) -{ - d->root[0] = lfs_fromle32(d->root[0]); - d->root[1] = lfs_fromle32(d->root[1]); - d->block_size = lfs_fromle32(d->block_size); - d->block_count = lfs_fromle32(d->block_count); - d->version = lfs_fromle32(d->version); +static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) { + d->root[0] = lfs_fromle32(d->root[0]); + d->root[1] = lfs_fromle32(d->root[1]); + d->block_size = lfs_fromle32(d->block_size); + d->block_count = lfs_fromle32(d->block_count); + d->version = lfs_fromle32(d->version); } -static void lfs_superblock_tole32(struct lfs_disk_superblock *d) -{ - d->root[0] = lfs_tole32(d->root[0]); - d->root[1] = lfs_tole32(d->root[1]); - d->block_size = lfs_tole32(d->block_size); - d->block_count = lfs_tole32(d->block_count); - d->version = lfs_tole32(d->version); +static void lfs_superblock_tole32(struct lfs_disk_superblock *d) { + d->root[0] = lfs_tole32(d->root[0]); + d->root[1] = lfs_tole32(d->root[1]); + d->block_size = lfs_tole32(d->block_size); + d->block_count = lfs_tole32(d->block_count); + d->version = lfs_tole32(d->version); } /// Metadata pair and directory operations /// -static inline void lfs_pairswap(lfs_block_t pair[2]) -{ - lfs_block_t t = pair[0]; - pair[0] = pair[1]; - pair[1] = t; +static inline void lfs_pairswap(lfs_block_t pair[2]) { + lfs_block_t t = pair[0]; + pair[0] = pair[1]; + pair[1] = t; } -static inline bool lfs_pairisnull(const lfs_block_t pair[2]) -{ - return pair[0] == 0xffffffff || pair[1] == 0xffffffff; +static inline bool lfs_pairisnull(const lfs_block_t pair[2]) { return pair[0] == 0xffffffff || pair[1] == 0xffffffff; } + +static inline int lfs_paircmp(const lfs_block_t paira[2], const lfs_block_t pairb[2]) { + return !(paira[0] == pairb[0] || paira[1] == pairb[1] || paira[0] == pairb[1] || paira[1] == pairb[0]); } -static inline int lfs_paircmp(const lfs_block_t paira[2], const lfs_block_t pairb[2]) -{ - return !(paira[0] == pairb[0] || paira[1] == pairb[1] || paira[0] == pairb[1] || paira[1] == pairb[0]); +static inline bool lfs_pairsync(const lfs_block_t paira[2], const lfs_block_t pairb[2]) { + return (paira[0] == pairb[0] && paira[1] == pairb[1]) || (paira[0] == pairb[1] && paira[1] == pairb[0]); } -static inline bool lfs_pairsync(const lfs_block_t paira[2], const lfs_block_t pairb[2]) -{ - return (paira[0] == pairb[0] && paira[1] == pairb[1]) || (paira[0] == pairb[1] && paira[1] == pairb[0]); +static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) { return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; } + +static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) { + // allocate pair of dir blocks + for (int i = 0; i < 2; i++) { + int err = lfs_alloc(lfs, &dir->pair[i]); + if (err) { + return err; + } + } + + // rather than clobbering one of the blocks we just pretend + // the revision may be valid + int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + if (err != LFS_ERR_CORRUPT) { + dir->d.rev = lfs_fromle32(dir->d.rev); + } + + // set defaults + dir->d.rev += 1; + dir->d.size = sizeof(dir->d) + 4; + dir->d.tail[0] = 0xffffffff; + dir->d.tail[1] = 0xffffffff; + dir->off = sizeof(dir->d); + + // don't write out yet, let caller take care of that + return 0; } -static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) -{ - return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; -} +static int lfs_dir_fetch(lfs_t *lfs, lfs_dir_t *dir, const lfs_block_t pair[2]) { + // copy out pair, otherwise may be aliasing dir + const lfs_block_t tpair[2] = {pair[0], pair[1]}; + bool valid = false; -static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) -{ - // allocate pair of dir blocks - for (int i = 0; i < 2; i++) { - int err = lfs_alloc(lfs, &dir->pair[i]); - if (err) { - return err; - } + // check both blocks for the most recent revision + for (int i = 0; i < 2; i++) { + struct lfs_disk_dir test; + int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); + lfs_dir_fromle32(&test); + if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } + return err; } - // rather than clobbering one of the blocks we just pretend - // the revision may be valid - int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); - if (err && err != LFS_ERR_CORRUPT) { - return err; + if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { + continue; } - if (err != LFS_ERR_CORRUPT) { - dir->d.rev = lfs_fromle32(dir->d.rev); + if ((0x7fffffff & test.size) < sizeof(test) + 4 || (0x7fffffff & test.size) > lfs->cfg->block_size) { + continue; } - // set defaults - dir->d.rev += 1; - dir->d.size = sizeof(dir->d) + 4; - dir->d.tail[0] = 0xffffffff; - dir->d.tail[1] = 0xffffffff; + uint32_t crc = 0xffffffff; + lfs_dir_tole32(&test); + lfs_crc(&crc, &test, sizeof(test)); + lfs_dir_fromle32(&test); + err = lfs_bd_crc(lfs, tpair[i], sizeof(test), (0x7fffffff & test.size) - sizeof(test), &crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } + return err; + } + + if (crc != 0) { + continue; + } + + valid = true; + + // setup dir in case it's valid + dir->pair[0] = tpair[(i + 0) % 2]; + dir->pair[1] = tpair[(i + 1) % 2]; dir->off = sizeof(dir->d); + dir->d = test; + } - // don't write out yet, let caller take care of that - return 0; -} + if (!valid) { + LFS_ERROR("Corrupted dir pair at %" PRIu32 " %" PRIu32, tpair[0], tpair[1]); + return LFS_ERR_CORRUPT; + } -static int lfs_dir_fetch(lfs_t *lfs, lfs_dir_t *dir, const lfs_block_t pair[2]) -{ - // copy out pair, otherwise may be aliasing dir - const lfs_block_t tpair[2] = {pair[0], pair[1]}; - bool valid = false; - - // check both blocks for the most recent revision - for (int i = 0; i < 2; i++) { - struct lfs_disk_dir test; - int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); - lfs_dir_fromle32(&test); - if (err) { - if (err == LFS_ERR_CORRUPT) { - continue; - } - return err; - } - - if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { - continue; - } - - if ((0x7fffffff & test.size) < sizeof(test) + 4 || (0x7fffffff & test.size) > lfs->cfg->block_size) { - continue; - } - - uint32_t crc = 0xffffffff; - lfs_dir_tole32(&test); - lfs_crc(&crc, &test, sizeof(test)); - lfs_dir_fromle32(&test); - err = lfs_bd_crc(lfs, tpair[i], sizeof(test), (0x7fffffff & test.size) - sizeof(test), &crc); - if (err) { - if (err == LFS_ERR_CORRUPT) { - continue; - } - return err; - } - - if (crc != 0) { - continue; - } - - valid = true; - - // setup dir in case it's valid - dir->pair[0] = tpair[(i + 0) % 2]; - dir->pair[1] = tpair[(i + 1) % 2]; - dir->off = sizeof(dir->d); - dir->d = test; - } - - if (!valid) { - LFS_ERROR("Corrupted dir pair at %" PRIu32 " %" PRIu32, tpair[0], tpair[1]); - return LFS_ERR_CORRUPT; - } - - return 0; + return 0; } struct lfs_region { - lfs_off_t oldoff; - lfs_size_t oldlen; - const void *newdata; - lfs_size_t newlen; + lfs_off_t oldoff; + lfs_size_t oldlen; + const void *newdata; + lfs_size_t newlen; }; -static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, const struct lfs_region *regions, int count) -{ - // increment revision count - dir->d.rev += 1; +static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, const struct lfs_region *regions, int count) { + // increment revision count + dir->d.rev += 1; - // keep pairs in order such that pair[0] is most recent - lfs_pairswap(dir->pair); - for (int i = 0; i < count; i++) { - dir->d.size += regions[i].newlen - regions[i].oldlen; + // keep pairs in order such that pair[0] is most recent + lfs_pairswap(dir->pair); + for (int i = 0; i < count; i++) { + dir->d.size += regions[i].newlen - regions[i].oldlen; + } + + const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]}; + bool relocated = false; + + while (true) { + + int err = lfs_bd_erase(lfs, dir->pair[0]); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; } - const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]}; - bool relocated = false; + uint32_t crc = 0xffffffff; + lfs_dir_tole32(&dir->d); + lfs_crc(&crc, &dir->d, sizeof(dir->d)); + err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); + lfs_dir_fromle32(&dir->d); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } - while (true) { - - int err = lfs_bd_erase(lfs, dir->pair[0]); + int i = 0; + lfs_off_t oldoff = sizeof(dir->d); + lfs_off_t newoff = sizeof(dir->d); + while (newoff < (0x7fffffff & dir->d.size) - 4) { + if (i < count && regions[i].oldoff == oldoff) { + lfs_crc(&crc, regions[i].newdata, regions[i].newlen); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, regions[i].newdata, regions[i].newlen); if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - uint32_t crc = 0xffffffff; - lfs_dir_tole32(&dir->d); - lfs_crc(&crc, &dir->d, sizeof(dir->d)); - err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); - lfs_dir_fromle32(&dir->d); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - int i = 0; - lfs_off_t oldoff = sizeof(dir->d); - lfs_off_t newoff = sizeof(dir->d); - while (newoff < (0x7fffffff & dir->d.size) - 4) { - if (i < count && regions[i].oldoff == oldoff) { - lfs_crc(&crc, regions[i].newdata, regions[i].newlen); - err = lfs_bd_prog(lfs, dir->pair[0], newoff, regions[i].newdata, regions[i].newlen); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - oldoff += regions[i].oldlen; - newoff += regions[i].newlen; - i += 1; - } else { - uint8_t data; - err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); - if (err) { - return err; - } - - lfs_crc(&crc, &data, 1); - err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - oldoff += 1; - newoff += 1; - } - } - - crc = lfs_tole32(crc); - err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); - crc = lfs_fromle32(crc); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - err = lfs_bd_sync(lfs); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // successful commit, check checksum to make sure - uint32_t ncrc = 0xffffffff; - err = lfs_bd_crc(lfs, dir->pair[0], 0, (0x7fffffff & dir->d.size) - 4, &ncrc); - if (err) { - return err; - } - - if (ncrc != crc) { + if (err == LFS_ERR_CORRUPT) { goto relocate; + } + return err; } - break; - relocate: - // commit was corrupted - LFS_DEBUG("Bad block at %" PRIu32, dir->pair[0]); - - // drop caches and prepare to relocate block - relocated = true; - lfs_cache_drop(lfs, &lfs->pcache); - - // can't relocate superblock, filesystem is now frozen - if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) { - LFS_WARN("Superblock %" PRIu32 " has become unwritable", oldpair[0]); - return LFS_ERR_CORRUPT; - } - - // relocate half of pair - err = lfs_alloc(lfs, &dir->pair[0]); + oldoff += regions[i].oldlen; + newoff += regions[i].newlen; + i += 1; + } else { + uint8_t data; + err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); if (err) { - return err; + return err; } - } - if (relocated) { - // update references if we relocated - LFS_DEBUG("Relocating %" PRIu32 " %" PRIu32 " to %" PRIu32 " %" PRIu32, oldpair[0], oldpair[1], dir->pair[0], - dir->pair[1]); - int err = lfs_relocate(lfs, oldpair, dir->pair); + lfs_crc(&crc, &data, 1); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1); if (err) { - return err; + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; } + + oldoff += 1; + newoff += 1; + } } - // shift over any directories that are affected - for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { - if (lfs_paircmp(d->pair, dir->pair) == 0) { - d->pair[0] = dir->pair[0]; - d->pair[1] = dir->pair[1]; - } + crc = lfs_tole32(crc); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); + crc = lfs_fromle32(crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; } - return 0; + err = lfs_bd_sync(lfs); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // successful commit, check checksum to make sure + uint32_t ncrc = 0xffffffff; + err = lfs_bd_crc(lfs, dir->pair[0], 0, (0x7fffffff & dir->d.size) - 4, &ncrc); + if (err) { + return err; + } + + if (ncrc != crc) { + goto relocate; + } + + break; + relocate: + // commit was corrupted + LFS_DEBUG("Bad block at %" PRIu32, dir->pair[0]); + + // drop caches and prepare to relocate block + relocated = true; + lfs_cache_drop(lfs, &lfs->pcache); + + // can't relocate superblock, filesystem is now frozen + if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) { + LFS_WARN("Superblock %" PRIu32 " has become unwritable", oldpair[0]); + return LFS_ERR_CORRUPT; + } + + // relocate half of pair + err = lfs_alloc(lfs, &dir->pair[0]); + if (err) { + return err; + } + } + + if (relocated) { + // update references if we relocated + LFS_DEBUG("Relocating %" PRIu32 " %" PRIu32 " to %" PRIu32 " %" PRIu32, oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]); + int err = lfs_relocate(lfs, oldpair, dir->pair); + if (err) { + return err; + } + } + + // shift over any directories that are affected + for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { + if (lfs_paircmp(d->pair, dir->pair) == 0) { + d->pair[0] = dir->pair[0]; + d->pair[1] = dir->pair[1]; + } + } + + return 0; } -static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) -{ - lfs_entry_tole32(&entry->d); - int err = lfs_dir_commit(lfs, dir, - (struct lfs_region[]){{entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, - {entry->off + sizeof(entry->d), entry->d.nlen, data, entry->d.nlen}}, - data ? 2 : 1); - lfs_entry_fromle32(&entry->d); +static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) { + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit(lfs, dir, + (struct lfs_region[]){{entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, + {entry->off + sizeof(entry->d), entry->d.nlen, data, entry->d.nlen}}, + data ? 2 : 1); + lfs_entry_fromle32(&entry->d); + return err; +} + +static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) { + // check if we fit, if top bit is set we do not and move on + while (true) { + if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { + entry->off = dir->d.size - 4; + + lfs_entry_tole32(&entry->d); + int err = + lfs_dir_commit(lfs, dir, (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); + lfs_entry_fromle32(&entry->d); + return err; + } + + // we need to allocate a new dir block + if (!(0x80000000 & dir->d.size)) { + lfs_dir_t olddir = *dir; + int err = lfs_dir_alloc(lfs, dir); + if (err) { + return err; + } + + dir->d.tail[0] = olddir.d.tail[0]; + dir->d.tail[1] = olddir.d.tail[1]; + entry->off = dir->d.size - 4; + lfs_entry_tole32(&entry->d); + err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); + lfs_entry_fromle32(&entry->d); + if (err) { + return err; + } + + olddir.d.size |= 0x80000000; + olddir.d.tail[0] = dir->pair[0]; + olddir.d.tail[1] = dir->pair[1]; + return lfs_dir_commit(lfs, &olddir, NULL, 0); + } + + int err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + } +} + +static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { + // check if we should just drop the directory block + if ((dir->d.size & 0x7fffffff) == sizeof(dir->d) + 4 + lfs_entry_size(entry)) { + lfs_dir_t pdir; + int res = lfs_pred(lfs, dir->pair, &pdir); + if (res < 0) { + return res; + } + + if (pdir.d.size & 0x80000000) { + pdir.d.size &= dir->d.size | 0x7fffffff; + pdir.d.tail[0] = dir->d.tail[0]; + pdir.d.tail[1] = dir->d.tail[1]; + return lfs_dir_commit(lfs, &pdir, NULL, 0); + } + } + + // shift out the entry + int err = lfs_dir_commit(lfs, dir, + (struct lfs_region[]){ + {entry->off, lfs_entry_size(entry), NULL, 0}, + }, + 1); + if (err) { return err; + } + + // shift over any files/directories that are affected + for (lfs_file_t *f = lfs->files; f; f = f->next) { + if (lfs_paircmp(f->pair, dir->pair) == 0) { + if (f->poff == entry->off) { + f->pair[0] = 0xffffffff; + f->pair[1] = 0xffffffff; + } else if (f->poff > entry->off) { + f->poff -= lfs_entry_size(entry); + } + } + } + + for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { + if (lfs_paircmp(d->pair, dir->pair) == 0) { + if (d->off > entry->off) { + d->off -= lfs_entry_size(entry); + d->pos -= lfs_entry_size(entry); + } + } + } + + return 0; } -static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) -{ - // check if we fit, if top bit is set we do not and move on - while (true) { - if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { - entry->off = dir->d.size - 4; - - lfs_entry_tole32(&entry->d); - int err = lfs_dir_commit( - lfs, dir, - (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); - lfs_entry_fromle32(&entry->d); - return err; - } - - // we need to allocate a new dir block - if (!(0x80000000 & dir->d.size)) { - lfs_dir_t olddir = *dir; - int err = lfs_dir_alloc(lfs, dir); - if (err) { - return err; - } - - dir->d.tail[0] = olddir.d.tail[0]; - dir->d.tail[1] = olddir.d.tail[1]; - entry->off = dir->d.size - 4; - lfs_entry_tole32(&entry->d); - err = lfs_dir_commit( - lfs, dir, - (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); - lfs_entry_fromle32(&entry->d); - if (err) { - return err; - } - - olddir.d.size |= 0x80000000; - olddir.d.tail[0] = dir->pair[0]; - olddir.d.tail[1] = dir->pair[1]; - return lfs_dir_commit(lfs, &olddir, NULL, 0); - } - - int err = lfs_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } - } -} - -static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) -{ - // check if we should just drop the directory block - if ((dir->d.size & 0x7fffffff) == sizeof(dir->d) + 4 + lfs_entry_size(entry)) { - lfs_dir_t pdir; - int res = lfs_pred(lfs, dir->pair, &pdir); - if (res < 0) { - return res; - } - - if (pdir.d.size & 0x80000000) { - pdir.d.size &= dir->d.size | 0x7fffffff; - pdir.d.tail[0] = dir->d.tail[0]; - pdir.d.tail[1] = dir->d.tail[1]; - return lfs_dir_commit(lfs, &pdir, NULL, 0); - } +static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { + while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size) - 4) { + if (!(0x80000000 & dir->d.size)) { + entry->off = dir->off; + return LFS_ERR_NOENT; } - // shift out the entry - int err = lfs_dir_commit(lfs, dir, - (struct lfs_region[]){ - {entry->off, lfs_entry_size(entry), NULL, 0}, - }, - 1); + int err = lfs_dir_fetch(lfs, dir, dir->d.tail); if (err) { - return err; + return err; } - // shift over any files/directories that are affected - for (lfs_file_t *f = lfs->files; f; f = f->next) { - if (lfs_paircmp(f->pair, dir->pair) == 0) { - if (f->poff == entry->off) { - f->pair[0] = 0xffffffff; - f->pair[1] = 0xffffffff; - } else if (f->poff > entry->off) { - f->poff -= lfs_entry_size(entry); - } - } - } + dir->off = sizeof(dir->d); + dir->pos += sizeof(dir->d) + 4; + } - for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { - if (lfs_paircmp(d->pair, dir->pair) == 0) { - if (d->off > entry->off) { - d->off -= lfs_entry_size(entry); - d->pos -= lfs_entry_size(entry); - } - } - } + int err = lfs_bd_read(lfs, dir->pair[0], dir->off, &entry->d, sizeof(entry->d)); + lfs_entry_fromle32(&entry->d); + if (err) { + return err; + } - return 0; + entry->off = dir->off; + dir->off += lfs_entry_size(entry); + dir->pos += lfs_entry_size(entry); + return 0; } -static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) -{ - while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size) - 4) { - if (!(0x80000000 & dir->d.size)) { - entry->off = dir->off; - return LFS_ERR_NOENT; - } +static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const char **path) { + const char *pathname = *path; + size_t pathlen; + entry->d.type = LFS_TYPE_DIR; + entry->d.elen = sizeof(entry->d) - 4; + entry->d.alen = 0; + entry->d.nlen = 0; + entry->d.u.dir[0] = lfs->root[0]; + entry->d.u.dir[1] = lfs->root[1]; - int err = lfs_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } + while (true) { + nextname: + // skip slashes + pathname += strspn(pathname, "/"); + pathlen = strcspn(pathname, "/"); - dir->off = sizeof(dir->d); - dir->pos += sizeof(dir->d) + 4; + // skip '.' and root '..' + if ((pathlen == 1 && memcmp(pathname, ".", 1) == 0) || (pathlen == 2 && memcmp(pathname, "..", 2) == 0)) { + pathname += pathlen; + goto nextname; } - int err = lfs_bd_read(lfs, dir->pair[0], dir->off, &entry->d, sizeof(entry->d)); - lfs_entry_fromle32(&entry->d); - if (err) { - return err; - } - - entry->off = dir->off; - dir->off += lfs_entry_size(entry); - dir->pos += lfs_entry_size(entry); - return 0; -} - -static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const char **path) -{ - const char *pathname = *path; - size_t pathlen; - entry->d.type = LFS_TYPE_DIR; - entry->d.elen = sizeof(entry->d) - 4; - entry->d.alen = 0; - entry->d.nlen = 0; - entry->d.u.dir[0] = lfs->root[0]; - entry->d.u.dir[1] = lfs->root[1]; - + // skip if matched by '..' in name + const char *suffix = pathname + pathlen; + size_t sufflen; + int depth = 1; while (true) { - nextname: - // skip slashes - pathname += strspn(pathname, "/"); - pathlen = strcspn(pathname, "/"); + suffix += strspn(suffix, "/"); + sufflen = strcspn(suffix, "/"); + if (sufflen == 0) { + break; + } - // skip '.' and root '..' - if ((pathlen == 1 && memcmp(pathname, ".", 1) == 0) || (pathlen == 2 && memcmp(pathname, "..", 2) == 0)) { - pathname += pathlen; - goto nextname; + if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { + depth -= 1; + if (depth == 0) { + pathname = suffix + sufflen; + goto nextname; } + } else { + depth += 1; + } - // skip if matched by '..' in name - const char *suffix = pathname + pathlen; - size_t sufflen; - int depth = 1; - while (true) { - suffix += strspn(suffix, "/"); - sufflen = strcspn(suffix, "/"); - if (sufflen == 0) { - break; - } - - if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { - depth -= 1; - if (depth == 0) { - pathname = suffix + sufflen; - goto nextname; - } - } else { - depth += 1; - } - - suffix += sufflen; - } - - // found path - if (pathname[0] == '\0') { - return 0; - } - - // update what we've found - *path = pathname; - - // continue on if we hit a directory - if (entry->d.type != LFS_TYPE_DIR) { - return LFS_ERR_NOTDIR; - } - - int err = lfs_dir_fetch(lfs, dir, entry->d.u.dir); - if (err) { - return err; - } - - // find entry matching name - while (true) { - err = lfs_dir_next(lfs, dir, entry); - if (err) { - return err; - } - - if (((0x7f & entry->d.type) != LFS_TYPE_REG && (0x7f & entry->d.type) != LFS_TYPE_DIR) || entry->d.nlen != pathlen) { - continue; - } - - int res = lfs_bd_cmp(lfs, dir->pair[0], entry->off + 4 + entry->d.elen + entry->d.alen, pathname, pathlen); - if (res < 0) { - return res; - } - - // found match - if (res) { - break; - } - } - - // check that entry has not been moved - if (entry->d.type & 0x80) { - int moved = lfs_moved(lfs, &entry->d.u); - if (moved) { - return (moved < 0) ? moved : LFS_ERR_NOENT; - } - - entry->d.type &= ~0x80; - } - - // to next name - pathname += pathlen; + suffix += sufflen; } + + // found path + if (pathname[0] == '\0') { + return 0; + } + + // update what we've found + *path = pathname; + + // continue on if we hit a directory + if (entry->d.type != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + int err = lfs_dir_fetch(lfs, dir, entry->d.u.dir); + if (err) { + return err; + } + + // find entry matching name + while (true) { + err = lfs_dir_next(lfs, dir, entry); + if (err) { + return err; + } + + if (((0x7f & entry->d.type) != LFS_TYPE_REG && (0x7f & entry->d.type) != LFS_TYPE_DIR) || entry->d.nlen != pathlen) { + continue; + } + + int res = lfs_bd_cmp(lfs, dir->pair[0], entry->off + 4 + entry->d.elen + entry->d.alen, pathname, pathlen); + if (res < 0) { + return res; + } + + // found match + if (res) { + break; + } + } + + // check that entry has not been moved + if (entry->d.type & 0x80) { + int moved = lfs_moved(lfs, &entry->d.u); + if (moved) { + return (moved < 0) ? moved : LFS_ERR_NOENT; + } + + entry->d.type &= ~0x80; + } + + // to next name + pathname += pathlen; + } } /// Top level directory operations /// -int lfs_mkdir(lfs_t *lfs, const char *path) -{ - // deorphan if we haven't yet, needed at most once after poweron - if (!lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - // fetch parent directory - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); - if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) { - return err ? err : LFS_ERR_EXIST; - } - - // build up new directory - lfs_alloc_ack(lfs); - - lfs_dir_t dir; - err = lfs_dir_alloc(lfs, &dir); +int lfs_mkdir(lfs_t *lfs, const char *path) { + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); if (err) { - return err; + return err; } - dir.d.tail[0] = cwd.d.tail[0]; - dir.d.tail[1] = cwd.d.tail[1]; + } - err = lfs_dir_commit(lfs, &dir, NULL, 0); - if (err) { - return err; - } + // fetch parent directory + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) { + return err ? err : LFS_ERR_EXIST; + } - entry.d.type = LFS_TYPE_DIR; - entry.d.elen = sizeof(entry.d) - 4; - entry.d.alen = 0; - entry.d.nlen = strlen(path); - entry.d.u.dir[0] = dir.pair[0]; - entry.d.u.dir[1] = dir.pair[1]; + // build up new directory + lfs_alloc_ack(lfs); - cwd.d.tail[0] = dir.pair[0]; - cwd.d.tail[1] = dir.pair[1]; + lfs_dir_t dir; + err = lfs_dir_alloc(lfs, &dir); + if (err) { + return err; + } + dir.d.tail[0] = cwd.d.tail[0]; + dir.d.tail[1] = cwd.d.tail[1]; - err = lfs_dir_append(lfs, &cwd, &entry, path); - if (err) { - return err; - } + err = lfs_dir_commit(lfs, &dir, NULL, 0); + if (err) { + return err; + } - lfs_alloc_ack(lfs); - return 0; + entry.d.type = LFS_TYPE_DIR; + entry.d.elen = sizeof(entry.d) - 4; + entry.d.alen = 0; + entry.d.nlen = strlen(path); + entry.d.u.dir[0] = dir.pair[0]; + entry.d.u.dir[1] = dir.pair[1]; + + cwd.d.tail[0] = dir.pair[0]; + cwd.d.tail[1] = dir.pair[1]; + + err = lfs_dir_append(lfs, &cwd, &entry, path); + if (err) { + return err; + } + + lfs_alloc_ack(lfs); + return 0; } -int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) -{ - dir->pair[0] = lfs->root[0]; - dir->pair[1] = lfs->root[1]; +int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { + dir->pair[0] = lfs->root[0]; + dir->pair[1] = lfs->root[1]; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, dir, &entry, &path); - if (err) { - return err; - } else if (entry.d.type != LFS_TYPE_DIR) { - return LFS_ERR_NOTDIR; - } + lfs_entry_t entry; + int err = lfs_dir_find(lfs, dir, &entry, &path); + if (err) { + return err; + } else if (entry.d.type != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } - err = lfs_dir_fetch(lfs, dir, entry.d.u.dir); - if (err) { - return err; - } + err = lfs_dir_fetch(lfs, dir, entry.d.u.dir); + if (err) { + return err; + } - // setup head dir - // special offset for '.' and '..' - dir->head[0] = dir->pair[0]; - dir->head[1] = dir->pair[1]; - dir->pos = sizeof(dir->d) - 2; - dir->off = sizeof(dir->d); + // setup head dir + // special offset for '.' and '..' + dir->head[0] = dir->pair[0]; + dir->head[1] = dir->pair[1]; + dir->pos = sizeof(dir->d) - 2; + dir->off = sizeof(dir->d); - // add to list of directories - dir->next = lfs->dirs; - lfs->dirs = dir; + // add to list of directories + dir->next = lfs->dirs; + lfs->dirs = dir; - return 0; + return 0; } -int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) -{ - // remove from list of directories - for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) { - if (*p == dir) { - *p = dir->next; - break; - } +int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { + // remove from list of directories + for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) { + if (*p == dir) { + *p = dir->next; + break; } + } - return 0; + return 0; } -int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) -{ - memset(info, 0, sizeof(*info)); - - // special offset for '.' and '..' - if (dir->pos == sizeof(dir->d) - 2) { - info->type = LFS_TYPE_DIR; - strcpy(info->name, "."); - dir->pos += 1; - return 1; - } else if (dir->pos == sizeof(dir->d) - 1) { - info->type = LFS_TYPE_DIR; - strcpy(info->name, ".."); - dir->pos += 1; - return 1; - } - - lfs_entry_t entry; - while (true) { - int err = lfs_dir_next(lfs, dir, &entry); - if (err) { - return (err == LFS_ERR_NOENT) ? 0 : err; - } - - if ((0x7f & entry.d.type) != LFS_TYPE_REG && (0x7f & entry.d.type) != LFS_TYPE_DIR) { - continue; - } - - // check that entry has not been moved - if (entry.d.type & 0x80) { - int moved = lfs_moved(lfs, &entry.d.u); - if (moved < 0) { - return moved; - } - - if (moved) { - continue; - } - - entry.d.type &= ~0x80; - } - - break; - } - - info->type = entry.d.type; - if (info->type == LFS_TYPE_REG) { - info->size = entry.d.u.file.size; - } - - int err = lfs_bd_read(lfs, dir->pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); - if (err) { - return err; - } +int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { + memset(info, 0, sizeof(*info)); + // special offset for '.' and '..' + if (dir->pos == sizeof(dir->d) - 2) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, "."); + dir->pos += 1; return 1; + } else if (dir->pos == sizeof(dir->d) - 1) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, ".."); + dir->pos += 1; + return 1; + } + + lfs_entry_t entry; + while (true) { + int err = lfs_dir_next(lfs, dir, &entry); + if (err) { + return (err == LFS_ERR_NOENT) ? 0 : err; + } + + if ((0x7f & entry.d.type) != LFS_TYPE_REG && (0x7f & entry.d.type) != LFS_TYPE_DIR) { + continue; + } + + // check that entry has not been moved + if (entry.d.type & 0x80) { + int moved = lfs_moved(lfs, &entry.d.u); + if (moved < 0) { + return moved; + } + + if (moved) { + continue; + } + + entry.d.type &= ~0x80; + } + + break; + } + + info->type = entry.d.type; + if (info->type == LFS_TYPE_REG) { + info->size = entry.d.u.file.size; + } + + int err = lfs_bd_read(lfs, dir->pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); + if (err) { + return err; + } + + return 1; } -int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) -{ - // simply walk from head dir - int err = lfs_dir_rewind(lfs, dir); +int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { + // simply walk from head dir + int err = lfs_dir_rewind(lfs, dir); + if (err) { + return err; + } + dir->pos = off; + + while (off > (0x7fffffff & dir->d.size)) { + off -= 0x7fffffff & dir->d.size; + if (!(0x80000000 & dir->d.size)) { + return LFS_ERR_INVAL; + } + + err = lfs_dir_fetch(lfs, dir, dir->d.tail); if (err) { - return err; + return err; } - dir->pos = off; + } - while (off > (0x7fffffff & dir->d.size)) { - off -= 0x7fffffff & dir->d.size; - if (!(0x80000000 & dir->d.size)) { - return LFS_ERR_INVAL; - } - - err = lfs_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } - } - - dir->off = off; - return 0; + dir->off = off; + return 0; } -int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) -{ - // reload the head dir - int err = lfs_dir_fetch(lfs, dir, dir->head); - if (err) { - return err; - } +int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) { + // reload the head dir + int err = lfs_dir_fetch(lfs, dir, dir->head); + if (err) { + return err; + } - dir->pair[0] = dir->head[0]; - dir->pair[1] = dir->head[1]; - dir->pos = sizeof(dir->d) - 2; - dir->off = sizeof(dir->d); - return 0; + dir->pair[0] = dir->head[0]; + dir->pair[1] = dir->head[1]; + dir->pos = sizeof(dir->d) - 2; + dir->off = sizeof(dir->d); + return 0; } /// File index list operations /// -static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) -{ - lfs_off_t size = *off; - lfs_off_t b = lfs->cfg->block_size - 2 * 4; - lfs_off_t i = size / b; - if (i == 0) { - return 0; - } - - i = (size - 4 * (lfs_popc(i - 1) + 2)) / b; - *off = size - b * i - 4 * lfs_popc(i); - return i; -} - -static int lfs_ctz_find(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, - lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) -{ - if (size == 0) { - *block = 0xffffffff; - *off = 0; - return 0; - } - - lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); - lfs_off_t target = lfs_ctz_index(lfs, &pos); - - while (current > target) { - lfs_size_t skip = lfs_min(lfs_npw2(current - target + 1) - 1, lfs_ctz(current)); - - int err = lfs_cache_read(lfs, rcache, pcache, head, 4 * skip, &head, 4); - head = lfs_fromle32(head); - if (err) { - return err; - } - - LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); - current -= 1 << skip; - } - - *block = head; - *off = pos; +static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { + lfs_off_t size = *off; + lfs_off_t b = lfs->cfg->block_size - 2 * 4; + lfs_off_t i = size / b; + if (i == 0) { return 0; + } + + i = (size - 4 * (lfs_popc(i - 1) + 2)) / b; + *off = size - b * i - 4 * lfs_popc(i); + return i; } -static int lfs_ctz_extend(lfs_t *lfs, lfs_cache_t *rcache, lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, - lfs_block_t *block, lfs_off_t *off) -{ - while (true) { - // go ahead and grab a block - lfs_block_t nblock; - int err = lfs_alloc(lfs, &nblock); - if (err) { - return err; - } - LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); - - if (true) { - err = lfs_bd_erase(lfs, nblock); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - if (size == 0) { - *block = nblock; - *off = 0; - return 0; - } - - size -= 1; - lfs_off_t index = lfs_ctz_index(lfs, &size); - size += 1; - - // just copy out the last block if it is incomplete - if (size != lfs->cfg->block_size) { - for (lfs_off_t i = 0; i < size; i++) { - uint8_t data; - err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1); - if (err) { - return err; - } - - err = lfs_cache_prog(lfs, pcache, rcache, nblock, i, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - *block = nblock; - *off = size; - return 0; - } - - // append block - index += 1; - lfs_size_t skips = lfs_ctz(index) + 1; - - for (lfs_off_t i = 0; i < skips; i++) { - head = lfs_tole32(head); - err = lfs_cache_prog(lfs, pcache, rcache, nblock, 4 * i, &head, 4); - head = lfs_fromle32(head); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - if (i != skips - 1) { - err = lfs_cache_read(lfs, rcache, NULL, head, 4 * i, &head, 4); - head = lfs_fromle32(head); - if (err) { - return err; - } - } - - LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); - } - - *block = nblock; - *off = 4 * skips; - return 0; - } - - relocate: - LFS_DEBUG("Bad block at %" PRIu32, nblock); - - // just clear cache and try a new block - lfs_cache_drop(lfs, &lfs->pcache); - } -} - -static int lfs_ctz_traverse(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, - int (*cb)(void *, lfs_block_t), void *data) -{ - if (size == 0) { - return 0; - } - - lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); - - while (true) { - int err = cb(data, head); - if (err) { - return err; - } - - if (index == 0) { - return 0; - } - - lfs_block_t heads[2]; - int count = 2 - (index & 1); - err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count * 4); - heads[0] = lfs_fromle32(heads[0]); - heads[1] = lfs_fromle32(heads[1]); - if (err) { - return err; - } - - for (int i = 0; i < count - 1; i++) { - err = cb(data, heads[i]); - if (err) { - return err; - } - } - - head = heads[count - 1]; - index -= count; - } -} - -/// Top level file operations /// -int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, const char *path, int flags, const struct lfs_file_config *cfg) -{ - // deorphan if we haven't yet, needed at most once after poweron - if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - // allocate entry for file if it doesn't exist - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); - if (err && (err != LFS_ERR_NOENT || strchr(path, '/') != NULL)) { - return err; - } - - if (err == LFS_ERR_NOENT) { - if (!(flags & LFS_O_CREAT)) { - return LFS_ERR_NOENT; - } - - // create entry to remember name - entry.d.type = LFS_TYPE_REG; - entry.d.elen = sizeof(entry.d) - 4; - entry.d.alen = 0; - entry.d.nlen = strlen(path); - entry.d.u.file.head = 0xffffffff; - entry.d.u.file.size = 0; - err = lfs_dir_append(lfs, &cwd, &entry, path); - if (err) { - return err; - } - } else if (entry.d.type == LFS_TYPE_DIR) { - return LFS_ERR_ISDIR; - } else if (flags & LFS_O_EXCL) { - return LFS_ERR_EXIST; - } - - // setup file struct - file->cfg = cfg; - file->pair[0] = cwd.pair[0]; - file->pair[1] = cwd.pair[1]; - file->poff = entry.off; - file->head = entry.d.u.file.head; - file->size = entry.d.u.file.size; - file->flags = flags; - file->pos = 0; - - if (flags & LFS_O_TRUNC) { - if (file->size != 0) { - file->flags |= LFS_F_DIRTY; - } - file->head = 0xffffffff; - file->size = 0; - } - - // allocate buffer if needed - file->cache.block = 0xffffffff; - if (file->cfg && file->cfg->buffer) { - file->cache.buffer = file->cfg->buffer; - } else if (lfs->cfg->file_buffer) { - if (lfs->files) { - // already in use - return LFS_ERR_NOMEM; - } - file->cache.buffer = lfs->cfg->file_buffer; - } else if ((file->flags & 3) == LFS_O_RDONLY) { - file->cache.buffer = lfs_malloc(lfs->cfg->read_size); - if (!file->cache.buffer) { - return LFS_ERR_NOMEM; - } - } else { - file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); - if (!file->cache.buffer) { - return LFS_ERR_NOMEM; - } - } - - // zero to avoid information leak - lfs_cache_drop(lfs, &file->cache); - if ((file->flags & 3) != LFS_O_RDONLY) { - lfs_cache_zero(lfs, &file->cache); - } - - // add to list of files - file->next = lfs->files; - lfs->files = file; - +static int lfs_ctz_find(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, lfs_size_t pos, + lfs_block_t *block, lfs_off_t *off) { + if (size == 0) { + *block = 0xffffffff; + *off = 0; return 0; -} + } -int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) -{ - return lfs_file_opencfg(lfs, file, path, flags, NULL); -} + lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); + lfs_off_t target = lfs_ctz_index(lfs, &pos); -int lfs_file_close(lfs_t *lfs, lfs_file_t *file) -{ - int err = lfs_file_sync(lfs, file); + while (current > target) { + lfs_size_t skip = lfs_min(lfs_npw2(current - target + 1) - 1, lfs_ctz(current)); - // remove from list of files - for (lfs_file_t **p = &lfs->files; *p; p = &(*p)->next) { - if (*p == file) { - *p = file->next; - break; - } + int err = lfs_cache_read(lfs, rcache, pcache, head, 4 * skip, &head, 4); + head = lfs_fromle32(head); + if (err) { + return err; } - // clean up memory - if (!(file->cfg && file->cfg->buffer) && !lfs->cfg->file_buffer) { - lfs_free(file->cache.buffer); - } + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); + current -= 1 << skip; + } - return err; + *block = head; + *off = pos; + return 0; } -static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) -{ -relocate: - LFS_DEBUG("Bad block at %" PRIu32, file->block); - - // just relocate what exists into new block +static int lfs_ctz_extend(lfs_t *lfs, lfs_cache_t *rcache, lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, lfs_block_t *block, + lfs_off_t *off) { + while (true) { + // go ahead and grab a block lfs_block_t nblock; int err = lfs_alloc(lfs, &nblock); if (err) { - return err; + return err; } + LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); - err = lfs_bd_erase(lfs, nblock); - if (err) { + if (true) { + err = lfs_bd_erase(lfs, nblock); + if (err) { if (err == LFS_ERR_CORRUPT) { - goto relocate; + goto relocate; } return err; - } + } - // either read from dirty cache or disk - for (lfs_off_t i = 0; i < file->off; i++) { - uint8_t data; - err = lfs_cache_read(lfs, &lfs->rcache, &file->cache, file->block, i, &data, 1); - if (err) { - return err; - } - - err = lfs_cache_prog(lfs, &lfs->pcache, &lfs->rcache, nblock, i, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - // copy over new state of file - memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size); - file->cache.block = lfs->pcache.block; - file->cache.off = lfs->pcache.off; - lfs_cache_zero(lfs, &lfs->pcache); - - file->block = nblock; - return 0; -} - -static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) -{ - if (file->flags & LFS_F_READING) { - // just drop read cache - lfs_cache_drop(lfs, &file->cache); - file->flags &= ~LFS_F_READING; - } - - if (file->flags & LFS_F_WRITING) { - lfs_off_t pos = file->pos; - - // copy over anything after current branch - lfs_file_t orig = { - .head = file->head, - .size = file->size, - .flags = LFS_O_RDONLY, - .pos = file->pos, - .cache = lfs->rcache, - }; - lfs_cache_drop(lfs, &lfs->rcache); - - while (file->pos < file->size) { - // copy over a byte at a time, leave it up to caching - // to make this efficient - uint8_t data; - lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1); - if (res < 0) { - return res; - } - - res = lfs_file_write(lfs, file, &data, 1); - if (res < 0) { - return res; - } - - // keep our reference to the rcache in sync - if (lfs->rcache.block != 0xffffffff) { - lfs_cache_drop(lfs, &orig.cache); - lfs_cache_drop(lfs, &lfs->rcache); - } - } - - // write out what we have - while (true) { - int err = lfs_cache_flush(lfs, &file->cache, &lfs->rcache); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - break; - relocate: - err = lfs_file_relocate(lfs, file); - if (err) { - return err; - } - } - - // actual file updates - file->head = file->block; - file->size = file->pos; - file->flags &= ~LFS_F_WRITING; - file->flags |= LFS_F_DIRTY; - - file->pos = pos; - } - - return 0; -} - -int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) -{ - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - - if ((file->flags & LFS_F_DIRTY) && !(file->flags & LFS_F_ERRED) && !lfs_pairisnull(file->pair)) { - // update dir entry - lfs_dir_t cwd; - err = lfs_dir_fetch(lfs, &cwd, file->pair); - if (err) { - return err; - } - - lfs_entry_t entry = {.off = file->poff}; - err = lfs_bd_read(lfs, cwd.pair[0], entry.off, &entry.d, sizeof(entry.d)); - lfs_entry_fromle32(&entry.d); - if (err) { - return err; - } - - LFS_ASSERT(entry.d.type == LFS_TYPE_REG); - entry.d.u.file.head = file->head; - entry.d.u.file.size = file->size; - - err = lfs_dir_update(lfs, &cwd, &entry, NULL); - if (err) { - return err; - } - - file->flags &= ~LFS_F_DIRTY; - } - - return 0; -} - -lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size) -{ - uint8_t *data = buffer; - lfs_size_t nsize = size; - - if ((file->flags & 3) == LFS_O_WRONLY) { - return LFS_ERR_BADF; - } - - if (file->flags & LFS_F_WRITING) { - // flush out any writes - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - } - - if (file->pos >= file->size) { - // eof if past end + if (size == 0) { + *block = nblock; + *off = 0; return 0; - } + } - size = lfs_min(size, file->size - file->pos); - nsize = size; + size -= 1; + lfs_off_t index = lfs_ctz_index(lfs, &size); + size += 1; - while (nsize > 0) { - // check if we need a new block - if (!(file->flags & LFS_F_READING) || file->off == lfs->cfg->block_size) { - int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos, &file->block, &file->off); - if (err) { - return err; - } - - file->flags |= LFS_F_READING; - } - - // read as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); - int err = lfs_cache_read(lfs, &file->cache, NULL, file->block, file->off, data, diff); - if (err) { + // just copy out the last block if it is incomplete + if (size != lfs->cfg->block_size) { + for (lfs_off_t i = 0; i < size; i++) { + uint8_t data; + err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1); + if (err) { return err; + } + + err = lfs_cache_prog(lfs, pcache, rcache, nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } } - file->pos += diff; - file->off += diff; - data += diff; - nsize -= diff; + *block = nblock; + *off = size; + return 0; + } + + // append block + index += 1; + lfs_size_t skips = lfs_ctz(index) + 1; + + for (lfs_off_t i = 0; i < skips; i++) { + head = lfs_tole32(head); + err = lfs_cache_prog(lfs, pcache, rcache, nblock, 4 * i, &head, 4); + head = lfs_fromle32(head); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (i != skips - 1) { + err = lfs_cache_read(lfs, rcache, NULL, head, 4 * i, &head, 4); + head = lfs_fromle32(head); + if (err) { + return err; + } + } + + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); + } + + *block = nblock; + *off = 4 * skips; + return 0; } - return size; + relocate: + LFS_DEBUG("Bad block at %" PRIu32, nblock); + + // just clear cache and try a new block + lfs_cache_drop(lfs, &lfs->pcache); + } } -lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size) -{ - const uint8_t *data = buffer; - lfs_size_t nsize = size; +static int lfs_ctz_traverse(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, + int (*cb)(void *, lfs_block_t), void *data) { + if (size == 0) { + return 0; + } - if ((file->flags & 3) == LFS_O_RDONLY) { - return LFS_ERR_BADF; + lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); + + while (true) { + int err = cb(data, head); + if (err) { + return err; } - if (file->flags & LFS_F_READING) { - // drop any reads - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } + if (index == 0) { + return 0; } - if ((file->flags & LFS_O_APPEND) && file->pos < file->size) { - file->pos = file->size; + lfs_block_t heads[2]; + int count = 2 - (index & 1); + err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count * 4); + heads[0] = lfs_fromle32(heads[0]); + heads[1] = lfs_fromle32(heads[1]); + if (err) { + return err; } - if (!(file->flags & LFS_F_WRITING) && file->pos > file->size) { - // fill with zeros - lfs_off_t pos = file->pos; - file->pos = file->size; - - while (file->pos < pos) { - lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); - if (res < 0) { - return res; - } - } + for (int i = 0; i < count - 1; i++) { + err = cb(data, heads[i]); + if (err) { + return err; + } } - while (nsize > 0) { - // check if we need a new block - if (!(file->flags & LFS_F_WRITING) || file->off == lfs->cfg->block_size) { - if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { - // find out which block we're extending from - int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos - 1, &file->block, &file->off); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - - // mark cache as dirty since we may have read data into it - lfs_cache_zero(lfs, &file->cache); - } - - // extend file with new blocks - lfs_alloc_ack(lfs); - int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache, file->block, file->pos, &file->block, &file->off); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - - file->flags |= LFS_F_WRITING; - } - - // program as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); - while (true) { - int err = lfs_cache_prog(lfs, &file->cache, &lfs->rcache, file->block, file->off, data, diff); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - file->flags |= LFS_F_ERRED; - return err; - } - - break; - relocate: - err = lfs_file_relocate(lfs, file); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - } - - file->pos += diff; - file->off += diff; - data += diff; - nsize -= diff; - - lfs_alloc_ack(lfs); - } - - file->flags &= ~LFS_F_ERRED; - return size; + head = heads[count - 1]; + index -= count; + } } -lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence) -{ - // write out everything beforehand, may be noop if rdonly +/// Top level file operations /// +int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, const char *path, int flags, const struct lfs_file_config *cfg) { + // deorphan if we haven't yet, needed at most once after poweron + if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // allocate entry for file if it doesn't exist + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err && (err != LFS_ERR_NOENT || strchr(path, '/') != NULL)) { + return err; + } + + if (err == LFS_ERR_NOENT) { + if (!(flags & LFS_O_CREAT)) { + return LFS_ERR_NOENT; + } + + // create entry to remember name + entry.d.type = LFS_TYPE_REG; + entry.d.elen = sizeof(entry.d) - 4; + entry.d.alen = 0; + entry.d.nlen = strlen(path); + entry.d.u.file.head = 0xffffffff; + entry.d.u.file.size = 0; + err = lfs_dir_append(lfs, &cwd, &entry, path); + if (err) { + return err; + } + } else if (entry.d.type == LFS_TYPE_DIR) { + return LFS_ERR_ISDIR; + } else if (flags & LFS_O_EXCL) { + return LFS_ERR_EXIST; + } + + // setup file struct + file->cfg = cfg; + file->pair[0] = cwd.pair[0]; + file->pair[1] = cwd.pair[1]; + file->poff = entry.off; + file->head = entry.d.u.file.head; + file->size = entry.d.u.file.size; + file->flags = flags; + file->pos = 0; + + if (flags & LFS_O_TRUNC) { + if (file->size != 0) { + file->flags |= LFS_F_DIRTY; + } + file->head = 0xffffffff; + file->size = 0; + } + + // allocate buffer if needed + file->cache.block = 0xffffffff; + if (file->cfg && file->cfg->buffer) { + file->cache.buffer = file->cfg->buffer; + } else if (lfs->cfg->file_buffer) { + if (lfs->files) { + // already in use + return LFS_ERR_NOMEM; + } + file->cache.buffer = lfs->cfg->file_buffer; + } else if ((file->flags & 3) == LFS_O_RDONLY) { + file->cache.buffer = lfs_malloc(lfs->cfg->read_size); + if (!file->cache.buffer) { + return LFS_ERR_NOMEM; + } + } else { + file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); + if (!file->cache.buffer) { + return LFS_ERR_NOMEM; + } + } + + // zero to avoid information leak + lfs_cache_drop(lfs, &file->cache); + if ((file->flags & 3) != LFS_O_RDONLY) { + lfs_cache_zero(lfs, &file->cache); + } + + // add to list of files + file->next = lfs->files; + lfs->files = file; + + return 0; +} + +int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) { return lfs_file_opencfg(lfs, file, path, flags, NULL); } + +int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { + int err = lfs_file_sync(lfs, file); + + // remove from list of files + for (lfs_file_t **p = &lfs->files; *p; p = &(*p)->next) { + if (*p == file) { + *p = file->next; + break; + } + } + + // clean up memory + if (!(file->cfg && file->cfg->buffer) && !lfs->cfg->file_buffer) { + lfs_free(file->cache.buffer); + } + + return err; +} + +static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) { +relocate: + LFS_DEBUG("Bad block at %" PRIu32, file->block); + + // just relocate what exists into new block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // either read from dirty cache or disk + for (lfs_off_t i = 0; i < file->off; i++) { + uint8_t data; + err = lfs_cache_read(lfs, &lfs->rcache, &file->cache, file->block, i, &data, 1); + if (err) { + return err; + } + + err = lfs_cache_prog(lfs, &lfs->pcache, &lfs->rcache, nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // copy over new state of file + memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size); + file->cache.block = lfs->pcache.block; + file->cache.off = lfs->pcache.off; + lfs_cache_zero(lfs, &lfs->pcache); + + file->block = nblock; + return 0; +} + +static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) { + if (file->flags & LFS_F_READING) { + // just drop read cache + lfs_cache_drop(lfs, &file->cache); + file->flags &= ~LFS_F_READING; + } + + if (file->flags & LFS_F_WRITING) { + lfs_off_t pos = file->pos; + + // copy over anything after current branch + lfs_file_t orig = { + .head = file->head, + .size = file->size, + .flags = LFS_O_RDONLY, + .pos = file->pos, + .cache = lfs->rcache, + }; + lfs_cache_drop(lfs, &lfs->rcache); + + while (file->pos < file->size) { + // copy over a byte at a time, leave it up to caching + // to make this efficient + uint8_t data; + lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1); + if (res < 0) { + return res; + } + + res = lfs_file_write(lfs, file, &data, 1); + if (res < 0) { + return res; + } + + // keep our reference to the rcache in sync + if (lfs->rcache.block != 0xffffffff) { + lfs_cache_drop(lfs, &orig.cache); + lfs_cache_drop(lfs, &lfs->rcache); + } + } + + // write out what we have + while (true) { + int err = lfs_cache_flush(lfs, &file->cache, &lfs->rcache); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + break; + relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + return err; + } + } + + // actual file updates + file->head = file->block; + file->size = file->pos; + file->flags &= ~LFS_F_WRITING; + file->flags |= LFS_F_DIRTY; + + file->pos = pos; + } + + return 0; +} + +int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + if ((file->flags & LFS_F_DIRTY) && !(file->flags & LFS_F_ERRED) && !lfs_pairisnull(file->pair)) { + // update dir entry + lfs_dir_t cwd; + err = lfs_dir_fetch(lfs, &cwd, file->pair); + if (err) { + return err; + } + + lfs_entry_t entry = {.off = file->poff}; + err = lfs_bd_read(lfs, cwd.pair[0], entry.off, &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); + if (err) { + return err; + } + + LFS_ASSERT(entry.d.type == LFS_TYPE_REG); + entry.d.u.file.head = file->head; + entry.d.u.file.size = file->size; + + err = lfs_dir_update(lfs, &cwd, &entry, NULL); + if (err) { + return err; + } + + file->flags &= ~LFS_F_DIRTY; + } + + return 0; +} + +lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size) { + uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & 3) == LFS_O_WRONLY) { + return LFS_ERR_BADF; + } + + if (file->flags & LFS_F_WRITING) { + // flush out any writes int err = lfs_file_flush(lfs, file); if (err) { + return err; + } + } + + if (file->pos >= file->size) { + // eof if past end + return 0; + } + + size = lfs_min(size, file->size - file->pos); + nsize = size; + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_READING) || file->off == lfs->cfg->block_size) { + int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos, &file->block, &file->off); + if (err) { return err; + } + + file->flags |= LFS_F_READING; } - // update pos - if (whence == LFS_SEEK_SET) { - file->pos = off; - } else if (whence == LFS_SEEK_CUR) { - if (off < 0 && (lfs_off_t)-off > file->pos) { - return LFS_ERR_INVAL; - } - - file->pos = file->pos + off; - } else if (whence == LFS_SEEK_END) { - if (off < 0 && (lfs_off_t)-off > file->size) { - return LFS_ERR_INVAL; - } - - file->pos = file->size + off; + // read as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + int err = lfs_cache_read(lfs, &file->cache, NULL, file->block, file->off, data, diff); + if (err) { + return err; } - return file->pos; + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + } + + return size; } -int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) -{ - if ((file->flags & 3) == LFS_O_RDONLY) { - return LFS_ERR_BADF; +lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + if (file->flags & LFS_F_READING) { + // drop any reads + int err = lfs_file_flush(lfs, file); + if (err) { + return err; } + } - lfs_off_t oldsize = lfs_file_size(lfs, file); - if (size < oldsize) { - // need to flush since directly changing metadata - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } + if ((file->flags & LFS_O_APPEND) && file->pos < file->size) { + file->pos = file->size; + } - // lookup new head in ctz skip list - err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, size, &file->head, &(lfs_off_t){0}); - if (err) { - return err; - } + if (!(file->flags & LFS_F_WRITING) && file->pos > file->size) { + // fill with zeros + lfs_off_t pos = file->pos; + file->pos = file->size; - file->size = size; - file->flags |= LFS_F_DIRTY; - } else if (size > oldsize) { - lfs_off_t pos = file->pos; - - // flush+seek if not already at end - if (file->pos != oldsize) { - int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); - if (err < 0) { - return err; - } - } - - // fill with zeros - while (file->pos < size) { - lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); - if (res < 0) { - return res; - } - } - - // restore pos - int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); - if (err < 0) { - return err; - } - } - - return 0; -} - -lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t const *file) -{ - (void)lfs; - return file->pos; -} - -int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) -{ - lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET); - if (res < 0) { + while (file->pos < pos) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { return res; + } + } + } + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_WRITING) || file->off == lfs->cfg->block_size) { + if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { + // find out which block we're extending from + int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos - 1, &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + // mark cache as dirty since we may have read data into it + lfs_cache_zero(lfs, &file->cache); + } + + // extend file with new blocks + lfs_alloc_ack(lfs); + int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache, file->block, file->pos, &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + file->flags |= LFS_F_WRITING; } - return 0; + // program as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + while (true) { + int err = lfs_cache_prog(lfs, &file->cache, &lfs->rcache, file->block, file->off, data, diff); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + file->flags |= LFS_F_ERRED; + return err; + } + + break; + relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + + lfs_alloc_ack(lfs); + } + + file->flags &= ~LFS_F_ERRED; + return size; } -lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) -{ - (void)lfs; - if (file->flags & LFS_F_WRITING) { - return lfs_max(file->pos, file->size); - } else { - return file->size; +lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence) { + // write out everything beforehand, may be noop if rdonly + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // update pos + if (whence == LFS_SEEK_SET) { + file->pos = off; + } else if (whence == LFS_SEEK_CUR) { + if (off < 0 && (lfs_off_t)-off > file->pos) { + return LFS_ERR_INVAL; } + + file->pos = file->pos + off; + } else if (whence == LFS_SEEK_END) { + if (off < 0 && (lfs_off_t)-off > file->size) { + return LFS_ERR_INVAL; + } + + file->pos = file->size + off; + } + + return file->pos; +} + +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + lfs_off_t oldsize = lfs_file_size(lfs, file); + if (size < oldsize) { + // need to flush since directly changing metadata + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // lookup new head in ctz skip list + err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, size, &file->head, &(lfs_off_t){0}); + if (err) { + return err; + } + + file->size = size; + file->flags |= LFS_F_DIRTY; + } else if (size > oldsize) { + lfs_off_t pos = file->pos; + + // flush+seek if not already at end + if (file->pos != oldsize) { + int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); + if (err < 0) { + return err; + } + } + + // fill with zeros + while (file->pos < size) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + + // restore pos + int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); + if (err < 0) { + return err; + } + } + + return 0; +} + +lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t const *file) { + (void)lfs; + return file->pos; +} + +int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { + lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET); + if (res < 0) { + return res; + } + + return 0; +} + +lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { + (void)lfs; + if (file->flags & LFS_F_WRITING) { + return lfs_max(file->pos, file->size); + } else { + return file->size; + } } /// General fs operations /// -int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) -{ - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); +int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err) { + return err; + } + + memset(info, 0, sizeof(*info)); + info->type = entry.d.type; + if (info->type == LFS_TYPE_REG) { + info->size = entry.d.u.file.size; + } + + if (lfs_paircmp(entry.d.u.dir, lfs->root) == 0) { + strcpy(info->name, "/"); + } else { + err = lfs_bd_read(lfs, cwd.pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); if (err) { - return err; + return err; } + } - memset(info, 0, sizeof(*info)); - info->type = entry.d.type; - if (info->type == LFS_TYPE_REG) { - info->size = entry.d.u.file.size; - } - - if (lfs_paircmp(entry.d.u.dir, lfs->root) == 0) { - strcpy(info->name, "/"); - } else { - err = lfs_bd_read(lfs, cwd.pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); - if (err) { - return err; - } - } - - return 0; + return 0; } -int lfs_remove(lfs_t *lfs, const char *path) -{ - // deorphan if we haven't yet, needed at most once after poweron - if (!lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); +int lfs_remove(lfs_t *lfs, const char *path) { + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); if (err) { - return err; + return err; } + } - lfs_dir_t dir; - if (entry.d.type == LFS_TYPE_DIR) { - // must be empty before removal, checking size - // without masking top bit checks for any case where - // dir is not empty - err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); - if (err) { - return err; - } /* else if (dir.d.size != sizeof(dir.d)+4) { - return LFS_ERR_NOTEMPTY; - } allow to remove non-empty folder, - According to below issue, comment these 2 line won't corrupt filesystem - https://github.com/ARMmbed/littlefs/issues/43 */ - } + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err) { + return err; + } - // remove the entry - err = lfs_dir_remove(lfs, &cwd, &entry); + lfs_dir_t dir; + if (entry.d.type == LFS_TYPE_DIR) { + // must be empty before removal, checking size + // without masking top bit checks for any case where + // dir is not empty + err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); if (err) { - return err; + return err; + } /* else if (dir.d.size != sizeof(dir.d)+4) { + return LFS_ERR_NOTEMPTY; + } allow to remove non-empty folder, + According to below issue, comment these 2 line won't corrupt filesystem + https://github.com/ARMmbed/littlefs/issues/43 */ + } + + // remove the entry + err = lfs_dir_remove(lfs, &cwd, &entry); + if (err) { + return err; + } + + // if we were a directory, find pred, replace tail + if (entry.d.type == LFS_TYPE_DIR) { + int res = lfs_pred(lfs, dir.pair, &cwd); + if (res < 0) { + return res; } - // if we were a directory, find pred, replace tail - if (entry.d.type == LFS_TYPE_DIR) { - int res = lfs_pred(lfs, dir.pair, &cwd); - if (res < 0) { - return res; - } + LFS_ASSERT(res); // must have pred + cwd.d.tail[0] = dir.d.tail[0]; + cwd.d.tail[1] = dir.d.tail[1]; - LFS_ASSERT(res); // must have pred - cwd.d.tail[0] = dir.d.tail[0]; - cwd.d.tail[1] = dir.d.tail[1]; - - err = lfs_dir_commit(lfs, &cwd, NULL, 0); - if (err) { - return err; - } + err = lfs_dir_commit(lfs, &cwd, NULL, 0); + if (err) { + return err; } + } - return 0; + return 0; } -int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) -{ - // deorphan if we haven't yet, needed at most once after poweron - if (!lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - // find old entry - lfs_dir_t oldcwd; - lfs_entry_t oldentry; - int err = lfs_dir_find(lfs, &oldcwd, &oldentry, &oldpath); +int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); if (err) { - return err; + return err; } + } - // allocate new entry - lfs_dir_t newcwd; - lfs_entry_t preventry; - err = lfs_dir_find(lfs, &newcwd, &preventry, &newpath); - if (err && (err != LFS_ERR_NOENT || strchr(newpath, '/') != NULL)) { - return err; - } + // find old entry + lfs_dir_t oldcwd; + lfs_entry_t oldentry; + int err = lfs_dir_find(lfs, &oldcwd, &oldentry, &oldpath); + if (err) { + return err; + } - bool prevexists = (err != LFS_ERR_NOENT); - bool samepair = (lfs_paircmp(oldcwd.pair, newcwd.pair) == 0); + // allocate new entry + lfs_dir_t newcwd; + lfs_entry_t preventry; + err = lfs_dir_find(lfs, &newcwd, &preventry, &newpath); + if (err && (err != LFS_ERR_NOENT || strchr(newpath, '/') != NULL)) { + return err; + } - // must have same type - if (prevexists && preventry.d.type != oldentry.d.type) { - return LFS_ERR_ISDIR; - } + bool prevexists = (err != LFS_ERR_NOENT); + bool samepair = (lfs_paircmp(oldcwd.pair, newcwd.pair) == 0); - lfs_dir_t dir; - if (prevexists && preventry.d.type == LFS_TYPE_DIR) { - // must be empty before removal, checking size - // without masking top bit checks for any case where - // dir is not empty - err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); - if (err) { - return err; - } else if (dir.d.size != sizeof(dir.d) + 4) { - return LFS_ERR_NOTEMPTY; - } - } + // must have same type + if (prevexists && preventry.d.type != oldentry.d.type) { + return LFS_ERR_ISDIR; + } - // mark as moving - oldentry.d.type |= 0x80; - err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL); + lfs_dir_t dir; + if (prevexists && preventry.d.type == LFS_TYPE_DIR) { + // must be empty before removal, checking size + // without masking top bit checks for any case where + // dir is not empty + err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); if (err) { - return err; + return err; + } else if (dir.d.size != sizeof(dir.d) + 4) { + return LFS_ERR_NOTEMPTY; } + } - // update pair if newcwd == oldcwd - if (samepair) { - newcwd = oldcwd; - } + // mark as moving + oldentry.d.type |= 0x80; + err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL); + if (err) { + return err; + } - // move to new location - lfs_entry_t newentry = preventry; - newentry.d = oldentry.d; - newentry.d.type &= ~0x80; - newentry.d.nlen = strlen(newpath); + // update pair if newcwd == oldcwd + if (samepair) { + newcwd = oldcwd; + } - if (prevexists) { - err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); - if (err) { - return err; - } - } else { - err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); - if (err) { - return err; - } - } + // move to new location + lfs_entry_t newentry = preventry; + newentry.d = oldentry.d; + newentry.d.type &= ~0x80; + newentry.d.nlen = strlen(newpath); - // update pair if newcwd == oldcwd - if (samepair) { - oldcwd = newcwd; - } - - // remove old entry - err = lfs_dir_remove(lfs, &oldcwd, &oldentry); + if (prevexists) { + err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); if (err) { - return err; + return err; + } + } else { + err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); + if (err) { + return err; + } + } + + // update pair if newcwd == oldcwd + if (samepair) { + oldcwd = newcwd; + } + + // remove old entry + err = lfs_dir_remove(lfs, &oldcwd, &oldentry); + if (err) { + return err; + } + + // if we were a directory, find pred, replace tail + if (prevexists && preventry.d.type == LFS_TYPE_DIR) { + int res = lfs_pred(lfs, dir.pair, &newcwd); + if (res < 0) { + return res; } - // if we were a directory, find pred, replace tail - if (prevexists && preventry.d.type == LFS_TYPE_DIR) { - int res = lfs_pred(lfs, dir.pair, &newcwd); - if (res < 0) { - return res; - } + LFS_ASSERT(res); // must have pred + newcwd.d.tail[0] = dir.d.tail[0]; + newcwd.d.tail[1] = dir.d.tail[1]; - LFS_ASSERT(res); // must have pred - newcwd.d.tail[0] = dir.d.tail[0]; - newcwd.d.tail[1] = dir.d.tail[1]; - - err = lfs_dir_commit(lfs, &newcwd, NULL, 0); - if (err) { - return err; - } + err = lfs_dir_commit(lfs, &newcwd, NULL, 0); + if (err) { + return err; } + } - return 0; + return 0; } /// Filesystem operations /// -static void lfs_deinit(lfs_t *lfs) -{ - // free allocated memory - if (!lfs->cfg->read_buffer) { - lfs_free(lfs->rcache.buffer); - } +static void lfs_deinit(lfs_t *lfs) { + // free allocated memory + if (!lfs->cfg->read_buffer) { + lfs_free(lfs->rcache.buffer); + } - if (!lfs->cfg->prog_buffer) { - lfs_free(lfs->pcache.buffer); - } + if (!lfs->cfg->prog_buffer) { + lfs_free(lfs->pcache.buffer); + } - if (!lfs->cfg->lookahead_buffer) { - lfs_free(lfs->free.buffer); - } + if (!lfs->cfg->lookahead_buffer) { + lfs_free(lfs->free.buffer); + } } -static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) -{ - lfs->cfg = cfg; +static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { + lfs->cfg = cfg; - // setup read cache - if (lfs->cfg->read_buffer) { - lfs->rcache.buffer = lfs->cfg->read_buffer; - } else { - lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); - if (!lfs->rcache.buffer) { - goto cleanup; - } + // setup read cache + if (lfs->cfg->read_buffer) { + lfs->rcache.buffer = lfs->cfg->read_buffer; + } else { + lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); + if (!lfs->rcache.buffer) { + goto cleanup; + } + } + + // setup program cache + if (lfs->cfg->prog_buffer) { + lfs->pcache.buffer = lfs->cfg->prog_buffer; + } else { + lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); + if (!lfs->pcache.buffer) { + goto cleanup; + } + } + + // zero to avoid information leaks + lfs_cache_zero(lfs, &lfs->pcache); + lfs_cache_drop(lfs, &lfs->rcache); + + // setup lookahead, round down to nearest 32-bits + LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); + LFS_ASSERT(lfs->cfg->lookahead > 0); + if (lfs->cfg->lookahead_buffer) { + lfs->free.buffer = lfs->cfg->lookahead_buffer; + } else { + lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead / 8); + if (!lfs->free.buffer) { + goto cleanup; + } + } + + // check that program and read sizes are multiples of the block size + LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); + LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->prog_size == 0); + + // check that the block size is large enough to fit ctz pointers + LFS_ASSERT(4 * lfs_npw2(0xffffffff / (lfs->cfg->block_size - 2 * 4)) <= lfs->cfg->block_size); + + // setup default state + lfs->root[0] = 0xffffffff; + lfs->root[1] = 0xffffffff; + lfs->files = NULL; + lfs->dirs = NULL; + lfs->deorphaned = false; + + return 0; + +cleanup: + lfs_deinit(lfs); + return LFS_ERR_NOMEM; +} + +int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { + int err = 0; + if (true) { + err = lfs_init(lfs, cfg); + if (err) { + return err; } - // setup program cache - if (lfs->cfg->prog_buffer) { - lfs->pcache.buffer = lfs->cfg->prog_buffer; - } else { - lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); - if (!lfs->pcache.buffer) { - goto cleanup; - } + // create free lookahead + memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); + lfs->free.off = 0; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); + lfs->free.i = 0; + lfs_alloc_ack(lfs); + + // create superblock dir + lfs_dir_t superdir; + err = lfs_dir_alloc(lfs, &superdir); + if (err) { + goto cleanup; } - // zero to avoid information leaks - lfs_cache_zero(lfs, &lfs->pcache); - lfs_cache_drop(lfs, &lfs->rcache); - - // setup lookahead, round down to nearest 32-bits - LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); - LFS_ASSERT(lfs->cfg->lookahead > 0); - if (lfs->cfg->lookahead_buffer) { - lfs->free.buffer = lfs->cfg->lookahead_buffer; - } else { - lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead / 8); - if (!lfs->free.buffer) { - goto cleanup; - } + // write root directory + lfs_dir_t root; + err = lfs_dir_alloc(lfs, &root); + if (err) { + goto cleanup; } - // check that program and read sizes are multiples of the block size - LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); - LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->prog_size == 0); + err = lfs_dir_commit(lfs, &root, NULL, 0); + if (err) { + goto cleanup; + } - // check that the block size is large enough to fit ctz pointers - LFS_ASSERT(4 * lfs_npw2(0xffffffff / (lfs->cfg->block_size - 2 * 4)) <= lfs->cfg->block_size); + lfs->root[0] = root.pair[0]; + lfs->root[1] = root.pair[1]; - // setup default state - lfs->root[0] = 0xffffffff; - lfs->root[1] = 0xffffffff; - lfs->files = NULL; - lfs->dirs = NULL; - lfs->deorphaned = false; + // write superblocks + lfs_superblock_t superblock = { + .off = sizeof(superdir.d), + .d.type = LFS_TYPE_SUPERBLOCK, + .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, + .d.nlen = sizeof(superblock.d.magic), + .d.version = LFS_DISK_VERSION, + .d.magic = {"littlefs"}, + .d.block_size = lfs->cfg->block_size, + .d.block_count = lfs->cfg->block_count, + .d.root = {lfs->root[0], lfs->root[1]}, + }; + superdir.d.tail[0] = root.pair[0]; + superdir.d.tail[1] = root.pair[1]; + superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; + + // write both pairs to be safe + lfs_superblock_tole32(&superblock.d); + bool valid = false; + for (int i = 0; i < 2; i++) { + err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){{sizeof(superdir.d), sizeof(superblock.d), &superblock.d, sizeof(superblock.d)}}, 1); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + valid = valid || !err; + } + + if (!valid) { + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + // sanity check that fetch works + err = lfs_dir_fetch(lfs, &superdir, (const lfs_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + + lfs_alloc_ack(lfs); + } + +cleanup: + lfs_deinit(lfs); + return err; +} + +int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { + int err = 0; + if (true) { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + // setup free lookahead + lfs->free.off = 0; + lfs->free.size = 0; + lfs->free.i = 0; + lfs_alloc_ack(lfs); + + // load superblock + lfs_dir_t dir; + lfs_superblock_t superblock; + err = lfs_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + if (!err) { + err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), &superblock.d, sizeof(superblock.d)); + lfs_superblock_fromle32(&superblock.d); + if (err) { + goto cleanup; + } + + lfs->root[0] = superblock.d.root[0]; + lfs->root[1] = superblock.d.root[1]; + } + + if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { + LFS_ERROR("Invalid superblock at %d %d", 0, 1); + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + uint16_t major_version = (0xffff & (superblock.d.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); + if ((major_version != LFS_DISK_VERSION_MAJOR || minor_version > LFS_DISK_VERSION_MINOR)) { + LFS_ERROR("Invalid version %d.%d", major_version, minor_version); + err = LFS_ERR_INVAL; + goto cleanup; + } return 0; - -cleanup: - lfs_deinit(lfs); - return LFS_ERR_NOMEM; -} - -int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) -{ - int err = 0; - if (true) { - err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - // create free lookahead - memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); - lfs->free.off = 0; - lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); - lfs->free.i = 0; - lfs_alloc_ack(lfs); - - // create superblock dir - lfs_dir_t superdir; - err = lfs_dir_alloc(lfs, &superdir); - if (err) { - goto cleanup; - } - - // write root directory - lfs_dir_t root; - err = lfs_dir_alloc(lfs, &root); - if (err) { - goto cleanup; - } - - err = lfs_dir_commit(lfs, &root, NULL, 0); - if (err) { - goto cleanup; - } - - lfs->root[0] = root.pair[0]; - lfs->root[1] = root.pair[1]; - - // write superblocks - lfs_superblock_t superblock = { - .off = sizeof(superdir.d), - .d.type = LFS_TYPE_SUPERBLOCK, - .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, - .d.nlen = sizeof(superblock.d.magic), - .d.version = LFS_DISK_VERSION, - .d.magic = {"littlefs"}, - .d.block_size = lfs->cfg->block_size, - .d.block_count = lfs->cfg->block_count, - .d.root = {lfs->root[0], lfs->root[1]}, - }; - superdir.d.tail[0] = root.pair[0]; - superdir.d.tail[1] = root.pair[1]; - superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; - - // write both pairs to be safe - lfs_superblock_tole32(&superblock.d); - bool valid = false; - for (int i = 0; i < 2; i++) { - err = lfs_dir_commit( - lfs, &superdir, - (struct lfs_region[]){{sizeof(superdir.d), sizeof(superblock.d), &superblock.d, sizeof(superblock.d)}}, 1); - if (err && err != LFS_ERR_CORRUPT) { - goto cleanup; - } - - valid = valid || !err; - } - - if (!valid) { - err = LFS_ERR_CORRUPT; - goto cleanup; - } - - // sanity check that fetch works - err = lfs_dir_fetch(lfs, &superdir, (const lfs_block_t[2]){0, 1}); - if (err) { - goto cleanup; - } - - lfs_alloc_ack(lfs); - } - -cleanup: - lfs_deinit(lfs); - return err; -} - -int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) -{ - int err = 0; - if (true) { - err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - // setup free lookahead - lfs->free.off = 0; - lfs->free.size = 0; - lfs->free.i = 0; - lfs_alloc_ack(lfs); - - // load superblock - lfs_dir_t dir; - lfs_superblock_t superblock; - err = lfs_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); - if (err && err != LFS_ERR_CORRUPT) { - goto cleanup; - } - - if (!err) { - err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), &superblock.d, sizeof(superblock.d)); - lfs_superblock_fromle32(&superblock.d); - if (err) { - goto cleanup; - } - - lfs->root[0] = superblock.d.root[0]; - lfs->root[1] = superblock.d.root[1]; - } - - if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { - LFS_ERROR("Invalid superblock at %d %d", 0, 1); - err = LFS_ERR_CORRUPT; - goto cleanup; - } - - uint16_t major_version = (0xffff & (superblock.d.version >> 16)); - uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); - if ((major_version != LFS_DISK_VERSION_MAJOR || minor_version > LFS_DISK_VERSION_MINOR)) { - LFS_ERROR("Invalid version %d.%d", major_version, minor_version); - err = LFS_ERR_INVAL; - goto cleanup; - } - - return 0; - } + } cleanup: - lfs_deinit(lfs); - return err; + lfs_deinit(lfs); + return err; } -int lfs_unmount(lfs_t *lfs) -{ - lfs_deinit(lfs); - return 0; +int lfs_unmount(lfs_t *lfs) { + lfs_deinit(lfs); + return 0; } /// Littlefs specific operations /// -int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) -{ - if (lfs_pairisnull(lfs->root)) { - return 0; +int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // iterate over metadata pairs + lfs_dir_t dir; + lfs_entry_t entry; + lfs_block_t cwd[2] = {0, 1}; + + while (true) { + for (int i = 0; i < 2; i++) { + int err = cb(data, cwd[i]); + if (err) { + return err; + } } - // iterate over metadata pairs - lfs_dir_t dir; - lfs_entry_t entry; - lfs_block_t cwd[2] = {0, 1}; + int err = lfs_dir_fetch(lfs, &dir, cwd); + if (err) { + return err; + } + + // iterate over contents + while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size) - 4) { + err = lfs_bd_read(lfs, dir.pair[0], dir.off, &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); + if (err) { + return err; + } + + dir.off += lfs_entry_size(&entry); + if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { + err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, entry.d.u.file.head, entry.d.u.file.size, cb, data); + if (err) { + return err; + } + } + } + + cwd[0] = dir.d.tail[0]; + cwd[1] = dir.d.tail[1]; + + if (lfs_pairisnull(cwd)) { + break; + } + } + + // iterate over any open files + for (lfs_file_t *f = lfs->files; f; f = f->next) { + if (f->flags & LFS_F_DIRTY) { + int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->head, f->size, cb, data); + if (err) { + return err; + } + } + + if (f->flags & LFS_F_WRITING) { + int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->block, f->pos, cb, data); + if (err) { + return err; + } + } + } + + return 0; +} + +static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // iterate over all directory directory entries + int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } + + while (!lfs_pairisnull(pdir->d.tail)) { + if (lfs_paircmp(pdir->d.tail, dir) == 0) { + return true; + } + + err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); + if (err) { + return err; + } + } + + return false; +} + +static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *parent, lfs_entry_t *entry) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + parent->d.tail[0] = 0; + parent->d.tail[1] = 1; + + // iterate over all directory directory entries + while (!lfs_pairisnull(parent->d.tail)) { + int err = lfs_dir_fetch(lfs, parent, parent->d.tail); + if (err) { + return err; + } while (true) { - for (int i = 0; i < 2; i++) { - int err = cb(data, cwd[i]); - if (err) { - return err; - } - } - - int err = lfs_dir_fetch(lfs, &dir, cwd); - if (err) { - return err; - } - - // iterate over contents - while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size) - 4) { - err = lfs_bd_read(lfs, dir.pair[0], dir.off, &entry.d, sizeof(entry.d)); - lfs_entry_fromle32(&entry.d); - if (err) { - return err; - } - - dir.off += lfs_entry_size(&entry); - if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { - err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, entry.d.u.file.head, entry.d.u.file.size, cb, data); - if (err) { - return err; - } - } - } - - cwd[0] = dir.d.tail[0]; - cwd[1] = dir.d.tail[1]; - - if (lfs_pairisnull(cwd)) { - break; - } - } - - // iterate over any open files - for (lfs_file_t *f = lfs->files; f; f = f->next) { - if (f->flags & LFS_F_DIRTY) { - int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->head, f->size, cb, data); - if (err) { - return err; - } - } - - if (f->flags & LFS_F_WRITING) { - int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->block, f->pos, cb, data); - if (err) { - return err; - } - } - } - - return 0; -} - -static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) -{ - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - // iterate over all directory directory entries - int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1}); - if (err) { + err = lfs_dir_next(lfs, parent, entry); + if (err && err != LFS_ERR_NOENT) { return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) && lfs_paircmp(entry->d.u.dir, dir) == 0) { + return true; + } } + } - while (!lfs_pairisnull(pdir->d.tail)) { - if (lfs_paircmp(pdir->d.tail, dir) == 0) { - return true; - } - - err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); - if (err) { - return err; - } - } - - return false; + return false; } -static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *parent, lfs_entry_t *entry) -{ - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - parent->d.tail[0] = 0; - parent->d.tail[1] = 1; - - // iterate over all directory directory entries - while (!lfs_pairisnull(parent->d.tail)) { - int err = lfs_dir_fetch(lfs, parent, parent->d.tail); - if (err) { - return err; - } - - while (true) { - err = lfs_dir_next(lfs, parent, entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) && lfs_paircmp(entry->d.u.dir, dir) == 0) { - return true; - } - } - } - - return false; -} - -static int lfs_moved(lfs_t *lfs, const void *e) -{ - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - // skip superblock - lfs_dir_t cwd; - int err = lfs_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); - if (err) { - return err; - } - - // iterate over all directory directory entries - lfs_entry_t entry; - while (!lfs_pairisnull(cwd.d.tail)) { - err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); - if (err) { - return err; - } - - while (true) { - err = lfs_dir_next(lfs, &cwd, &entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - if (!(0x80 & entry.d.type) && memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { - return true; - } - } - } - - return false; -} - -static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) -{ - // find parent - lfs_dir_t parent; - lfs_entry_t entry; - int res = lfs_parent(lfs, oldpair, &parent, &entry); - if (res < 0) { - return res; - } - - if (res) { - // update disk, this creates a desync - entry.d.u.dir[0] = newpair[0]; - entry.d.u.dir[1] = newpair[1]; - - int err = lfs_dir_update(lfs, &parent, &entry, NULL); - if (err) { - return err; - } - - // update internal root - if (lfs_paircmp(oldpair, lfs->root) == 0) { - LFS_DEBUG("Relocating root %" PRIu32 " %" PRIu32, newpair[0], newpair[1]); - lfs->root[0] = newpair[0]; - lfs->root[1] = newpair[1]; - } - - // clean up bad block, which should now be a desync - return lfs_deorphan(lfs); - } - - // find pred - res = lfs_pred(lfs, oldpair, &parent); - if (res < 0) { - return res; - } - - if (res) { - // just replace bad pair, no desync can occur - parent.d.tail[0] = newpair[0]; - parent.d.tail[1] = newpair[1]; - - return lfs_dir_commit(lfs, &parent, NULL, 0); - } - - // couldn't find dir, must be new +static int lfs_moved(lfs_t *lfs, const void *e) { + if (lfs_pairisnull(lfs->root)) { return 0; -} + } -int lfs_deorphan(lfs_t *lfs) -{ - lfs->deorphaned = true; + // skip superblock + lfs_dir_t cwd; + int err = lfs_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } - if (lfs_pairisnull(lfs->root)) { - return 0; + // iterate over all directory directory entries + lfs_entry_t entry; + while (!lfs_pairisnull(cwd.d.tail)) { + err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; } - lfs_dir_t pdir = {.d.size = 0x80000000}; - lfs_dir_t cwd = {.d.tail[0] = 0, .d.tail[1] = 1}; + while (true) { + err = lfs_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } - // iterate over all directory directory entries - for (lfs_size_t i = 0; i < lfs->cfg->block_count; i++) { - if (lfs_pairisnull(cwd.d.tail)) { - return 0; - } + if (err == LFS_ERR_NOENT) { + break; + } - int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (!(0x80 & entry.d.type) && memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { + return true; + } + } + } + + return false; +} + +static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) { + // find parent + lfs_dir_t parent; + lfs_entry_t entry; + int res = lfs_parent(lfs, oldpair, &parent, &entry); + if (res < 0) { + return res; + } + + if (res) { + // update disk, this creates a desync + entry.d.u.dir[0] = newpair[0]; + entry.d.u.dir[1] = newpair[1]; + + int err = lfs_dir_update(lfs, &parent, &entry, NULL); + if (err) { + return err; + } + + // update internal root + if (lfs_paircmp(oldpair, lfs->root) == 0) { + LFS_DEBUG("Relocating root %" PRIu32 " %" PRIu32, newpair[0], newpair[1]); + lfs->root[0] = newpair[0]; + lfs->root[1] = newpair[1]; + } + + // clean up bad block, which should now be a desync + return lfs_deorphan(lfs); + } + + // find pred + res = lfs_pred(lfs, oldpair, &parent); + if (res < 0) { + return res; + } + + if (res) { + // just replace bad pair, no desync can occur + parent.d.tail[0] = newpair[0]; + parent.d.tail[1] = newpair[1]; + + return lfs_dir_commit(lfs, &parent, NULL, 0); + } + + // couldn't find dir, must be new + return 0; +} + +int lfs_deorphan(lfs_t *lfs) { + lfs->deorphaned = true; + + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + lfs_dir_t pdir = {.d.size = 0x80000000}; + lfs_dir_t cwd = {.d.tail[0] = 0, .d.tail[1] = 1}; + + // iterate over all directory directory entries + for (lfs_size_t i = 0; i < lfs->cfg->block_count; i++) { + if (lfs_pairisnull(cwd.d.tail)) { + return 0; + } + + int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; + } + + // check head blocks for orphans + if (!(0x80000000 & pdir.d.size)) { + // check if we have a parent + lfs_dir_t parent; + lfs_entry_t entry; + int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry); + if (res < 0) { + return res; + } + + if (!res) { + // we are an orphan + LFS_DEBUG("Found orphan %" PRIu32 " %" PRIu32, pdir.d.tail[0], pdir.d.tail[1]); + + pdir.d.tail[0] = cwd.d.tail[0]; + pdir.d.tail[1] = cwd.d.tail[1]; + + err = lfs_dir_commit(lfs, &pdir, NULL, 0); if (err) { - return err; + return err; } - // check head blocks for orphans - if (!(0x80000000 & pdir.d.size)) { - // check if we have a parent - lfs_dir_t parent; - lfs_entry_t entry; - int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry); - if (res < 0) { - return res; - } + return 0; + } - if (!res) { - // we are an orphan - LFS_DEBUG("Found orphan %" PRIu32 " %" PRIu32, pdir.d.tail[0], pdir.d.tail[1]); + if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) { + // we have desynced + LFS_DEBUG("Found desync %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - pdir.d.tail[0] = cwd.d.tail[0]; - pdir.d.tail[1] = cwd.d.tail[1]; + pdir.d.tail[0] = entry.d.u.dir[0]; + pdir.d.tail[1] = entry.d.u.dir[1]; - err = lfs_dir_commit(lfs, &pdir, NULL, 0); - if (err) { - return err; - } - - return 0; - } - - if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) { - // we have desynced - LFS_DEBUG("Found desync %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - - pdir.d.tail[0] = entry.d.u.dir[0]; - pdir.d.tail[1] = entry.d.u.dir[1]; - - err = lfs_dir_commit(lfs, &pdir, NULL, 0); - if (err) { - return err; - } - - return 0; - } + err = lfs_dir_commit(lfs, &pdir, NULL, 0); + if (err) { + return err; } - // check entries for moves - lfs_entry_t entry; - while (true) { - err = lfs_dir_next(lfs, &cwd, &entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - // found moved entry - if (entry.d.type & 0x80) { - int moved = lfs_moved(lfs, &entry.d.u); - if (moved < 0) { - return moved; - } - - if (moved) { - LFS_DEBUG("Found move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - err = lfs_dir_remove(lfs, &cwd, &entry); - if (err) { - return err; - } - } else { - LFS_DEBUG("Found partial move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - entry.d.type &= ~0x80; - err = lfs_dir_update(lfs, &cwd, &entry, NULL); - if (err) { - return err; - } - } - } - } - - memcpy(&pdir, &cwd, sizeof(pdir)); + return 0; + } } - // If we reached here, we have more directory pairs than blocks in the - // filesystem... So something must be horribly wrong - return LFS_ERR_CORRUPT; + // check entries for moves + lfs_entry_t entry; + while (true) { + err = lfs_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + // found moved entry + if (entry.d.type & 0x80) { + int moved = lfs_moved(lfs, &entry.d.u); + if (moved < 0) { + return moved; + } + + if (moved) { + LFS_DEBUG("Found move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); + err = lfs_dir_remove(lfs, &cwd, &entry); + if (err) { + return err; + } + } else { + LFS_DEBUG("Found partial move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); + entry.d.type &= ~0x80; + err = lfs_dir_update(lfs, &cwd, &entry, NULL); + if (err) { + return err; + } + } + } + } + + memcpy(&pdir, &cwd, sizeof(pdir)); + } + + // If we reached here, we have more directory pairs than blocks in the + // filesystem... So something must be horribly wrong + return LFS_ERR_CORRUPT; } diff --git a/src/platform/stm32wl/littlefs/lfs.h b/src/platform/stm32wl/littlefs/lfs.h index c6ed1d622..5e6f619e7 100644 --- a/src/platform/stm32wl/littlefs/lfs.h +++ b/src/platform/stm32wl/littlefs/lfs.h @@ -49,230 +49,230 @@ typedef uint32_t lfs_block_t; // Possible error codes, these are negative to allow // valid positive return values enum lfs_error { - LFS_ERR_OK = 0, // No error - LFS_ERR_IO = -5, // Error during device operation - LFS_ERR_CORRUPT = -52, // Corrupted - LFS_ERR_NOENT = -2, // No directory entry - LFS_ERR_EXIST = -17, // Entry already exists - LFS_ERR_NOTDIR = -20, // Entry is not a dir - LFS_ERR_ISDIR = -21, // Entry is a dir - LFS_ERR_NOTEMPTY = -39, // Dir is not empty - LFS_ERR_BADF = -9, // Bad file number - LFS_ERR_INVAL = -22, // Invalid parameter - LFS_ERR_NOSPC = -28, // No space left on device - LFS_ERR_NOMEM = -12, // No more memory available + LFS_ERR_OK = 0, // No error + LFS_ERR_IO = -5, // Error during device operation + LFS_ERR_CORRUPT = -52, // Corrupted + LFS_ERR_NOENT = -2, // No directory entry + LFS_ERR_EXIST = -17, // Entry already exists + LFS_ERR_NOTDIR = -20, // Entry is not a dir + LFS_ERR_ISDIR = -21, // Entry is a dir + LFS_ERR_NOTEMPTY = -39, // Dir is not empty + LFS_ERR_BADF = -9, // Bad file number + LFS_ERR_INVAL = -22, // Invalid parameter + LFS_ERR_NOSPC = -28, // No space left on device + LFS_ERR_NOMEM = -12, // No more memory available }; // File types enum lfs_type { - LFS_TYPE_REG = 0x11, - LFS_TYPE_DIR = 0x22, - LFS_TYPE_SUPERBLOCK = 0x2e, + LFS_TYPE_REG = 0x11, + LFS_TYPE_DIR = 0x22, + LFS_TYPE_SUPERBLOCK = 0x2e, }; // File open flags enum lfs_open_flags { - // open flags - LFS_O_RDONLY = 1, // Open a file as read only - LFS_O_WRONLY = 2, // Open a file as write only - LFS_O_RDWR = 3, // Open a file as read and write - LFS_O_CREAT = 0x0100, // Create a file if it does not exist - LFS_O_EXCL = 0x0200, // Fail if a file already exists - LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size - LFS_O_APPEND = 0x0800, // Move to end of file on every write + // open flags + LFS_O_RDONLY = 1, // Open a file as read only + LFS_O_WRONLY = 2, // Open a file as write only + LFS_O_RDWR = 3, // Open a file as read and write + LFS_O_CREAT = 0x0100, // Create a file if it does not exist + LFS_O_EXCL = 0x0200, // Fail if a file already exists + LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size + LFS_O_APPEND = 0x0800, // Move to end of file on every write - // internally used flags - LFS_F_DIRTY = 0x10000, // File does not match storage - LFS_F_WRITING = 0x20000, // File has been written since last flush - LFS_F_READING = 0x40000, // File has been read since last flush - LFS_F_ERRED = 0x80000, // An error occured during write + // internally used flags + LFS_F_DIRTY = 0x10000, // File does not match storage + LFS_F_WRITING = 0x20000, // File has been written since last flush + LFS_F_READING = 0x40000, // File has been read since last flush + LFS_F_ERRED = 0x80000, // An error occured during write }; // File seek flags enum lfs_whence_flags { - LFS_SEEK_SET = 0, // Seek relative to an absolute position - LFS_SEEK_CUR = 1, // Seek relative to the current file position - LFS_SEEK_END = 2, // Seek relative to the end of the file + LFS_SEEK_SET = 0, // Seek relative to an absolute position + LFS_SEEK_CUR = 1, // Seek relative to the current file position + LFS_SEEK_END = 2, // Seek relative to the end of the file }; // Configuration provided during initialization of the littlefs struct lfs_config { - // Opaque user provided context that can be used to pass - // information to the block device operations - void *context; + // Opaque user provided context that can be used to pass + // information to the block device operations + void *context; - // Read a region in a block. Negative error codes are propogated - // to the user. - int (*read)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size); + // Read a region in a block. Negative error codes are propogated + // to the user. + int (*read)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size); - // Program a region in a block. The block must have previously - // been erased. Negative error codes are propogated to the user. - // May return LFS_ERR_CORRUPT if the block should be considered bad. - int (*prog)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size); + // Program a region in a block. The block must have previously + // been erased. Negative error codes are propogated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*prog)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size); - // Erase a block. A block must be erased before being programmed. - // The state of an erased block is undefined. Negative error codes - // are propogated to the user. - // May return LFS_ERR_CORRUPT if the block should be considered bad. - int (*erase)(const struct lfs_config *c, lfs_block_t block); + // Erase a block. A block must be erased before being programmed. + // The state of an erased block is undefined. Negative error codes + // are propogated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*erase)(const struct lfs_config *c, lfs_block_t block); - // Sync the state of the underlying block device. Negative error codes - // are propogated to the user. - int (*sync)(const struct lfs_config *c); + // Sync the state of the underlying block device. Negative error codes + // are propogated to the user. + int (*sync)(const struct lfs_config *c); - // Minimum size of a block read. This determines the size of read buffers. - // This may be larger than the physical read size to improve performance - // by caching more of the block device. - lfs_size_t read_size; + // Minimum size of a block read. This determines the size of read buffers. + // This may be larger than the physical read size to improve performance + // by caching more of the block device. + lfs_size_t read_size; - // Minimum size of a block program. This determines the size of program - // buffers. This may be larger than the physical program size to improve - // performance by caching more of the block device. - // Must be a multiple of the read size. - lfs_size_t prog_size; + // Minimum size of a block program. This determines the size of program + // buffers. This may be larger than the physical program size to improve + // performance by caching more of the block device. + // Must be a multiple of the read size. + lfs_size_t prog_size; - // Size of an erasable block. This does not impact ram consumption and - // may be larger than the physical erase size. However, this should be - // kept small as each file currently takes up an entire block. - // Must be a multiple of the program size. - lfs_size_t block_size; + // Size of an erasable block. This does not impact ram consumption and + // may be larger than the physical erase size. However, this should be + // kept small as each file currently takes up an entire block. + // Must be a multiple of the program size. + lfs_size_t block_size; - // Number of erasable blocks on the device. - lfs_size_t block_count; + // Number of erasable blocks on the device. + lfs_size_t block_count; - // Number of blocks to lookahead during block allocation. A larger - // lookahead reduces the number of passes required to allocate a block. - // The lookahead buffer requires only 1 bit per block so it can be quite - // large with little ram impact. Should be a multiple of 32. - lfs_size_t lookahead; + // Number of blocks to lookahead during block allocation. A larger + // lookahead reduces the number of passes required to allocate a block. + // The lookahead buffer requires only 1 bit per block so it can be quite + // large with little ram impact. Should be a multiple of 32. + lfs_size_t lookahead; - // Optional, statically allocated read buffer. Must be read sized. - void *read_buffer; + // Optional, statically allocated read buffer. Must be read sized. + void *read_buffer; - // Optional, statically allocated program buffer. Must be program sized. - void *prog_buffer; + // Optional, statically allocated program buffer. Must be program sized. + void *prog_buffer; - // Optional, statically allocated lookahead buffer. Must be 1 bit per - // lookahead block. - void *lookahead_buffer; + // Optional, statically allocated lookahead buffer. Must be 1 bit per + // lookahead block. + void *lookahead_buffer; - // Optional, statically allocated buffer for files. Must be program sized. - // If enabled, only one file may be opened at a time. - void *file_buffer; + // Optional, statically allocated buffer for files. Must be program sized. + // If enabled, only one file may be opened at a time. + void *file_buffer; }; // Optional configuration provided during lfs_file_opencfg struct lfs_file_config { - // Optional, statically allocated buffer for files. Must be program sized. - // If NULL, malloc will be used by default. - void *buffer; + // Optional, statically allocated buffer for files. Must be program sized. + // If NULL, malloc will be used by default. + void *buffer; }; // File info structure struct lfs_info { - // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR - uint8_t type; + // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR + uint8_t type; - // Size of the file, only valid for REG files - lfs_size_t size; + // Size of the file, only valid for REG files + lfs_size_t size; - // Name of the file stored as a null-terminated string - char name[LFS_NAME_MAX + 1]; + // Name of the file stored as a null-terminated string + char name[LFS_NAME_MAX + 1]; }; /// littlefs data structures /// typedef struct lfs_entry { - lfs_off_t off; + lfs_off_t off; - struct lfs_disk_entry { - uint8_t type; - uint8_t elen; - uint8_t alen; - uint8_t nlen; - union { - struct { - lfs_block_t head; - lfs_size_t size; - } file; - lfs_block_t dir[2]; - } u; - } d; + struct lfs_disk_entry { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + union { + struct { + lfs_block_t head; + lfs_size_t size; + } file; + lfs_block_t dir[2]; + } u; + } d; } lfs_entry_t; typedef struct lfs_cache { - lfs_block_t block; - lfs_off_t off; - uint8_t *buffer; + lfs_block_t block; + lfs_off_t off; + uint8_t *buffer; } lfs_cache_t; typedef struct lfs_file { - struct lfs_file *next; - lfs_block_t pair[2]; - lfs_off_t poff; + struct lfs_file *next; + lfs_block_t pair[2]; + lfs_off_t poff; - lfs_block_t head; - lfs_size_t size; + lfs_block_t head; + lfs_size_t size; - const struct lfs_file_config *cfg; - uint32_t flags; - lfs_off_t pos; - lfs_block_t block; - lfs_off_t off; - lfs_cache_t cache; + const struct lfs_file_config *cfg; + uint32_t flags; + lfs_off_t pos; + lfs_block_t block; + lfs_off_t off; + lfs_cache_t cache; } lfs_file_t; typedef struct lfs_dir { - struct lfs_dir *next; - lfs_block_t pair[2]; - lfs_off_t off; + struct lfs_dir *next; + lfs_block_t pair[2]; + lfs_off_t off; - lfs_block_t head[2]; - lfs_off_t pos; + lfs_block_t head[2]; + lfs_off_t pos; - struct lfs_disk_dir { - uint32_t rev; - lfs_size_t size; - lfs_block_t tail[2]; - } d; + struct lfs_disk_dir { + uint32_t rev; + lfs_size_t size; + lfs_block_t tail[2]; + } d; } lfs_dir_t; typedef struct lfs_superblock { - lfs_off_t off; + lfs_off_t off; - struct lfs_disk_superblock { - uint8_t type; - uint8_t elen; - uint8_t alen; - uint8_t nlen; - lfs_block_t root[2]; - uint32_t block_size; - uint32_t block_count; - uint32_t version; - char magic[8]; - } d; + struct lfs_disk_superblock { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + lfs_block_t root[2]; + uint32_t block_size; + uint32_t block_count; + uint32_t version; + char magic[8]; + } d; } lfs_superblock_t; typedef struct lfs_free { - lfs_block_t off; - lfs_block_t size; - lfs_block_t i; - lfs_block_t ack; - uint32_t *buffer; + lfs_block_t off; + lfs_block_t size; + lfs_block_t i; + lfs_block_t ack; + uint32_t *buffer; } lfs_free_t; // The littlefs type typedef struct lfs { - const struct lfs_config *cfg; + const struct lfs_config *cfg; - lfs_block_t root[2]; - lfs_file_t *files; - lfs_dir_t *dirs; + lfs_block_t root[2]; + lfs_file_t *files; + lfs_dir_t *dirs; - lfs_cache_t rcache; - lfs_cache_t pcache; + lfs_cache_t rcache; + lfs_cache_t pcache; - lfs_free_t free; - bool deorphaned; + lfs_free_t free; + bool deorphaned; } lfs_t; /// Filesystem functions /// diff --git a/src/platform/stm32wl/littlefs/lfs_util.c b/src/platform/stm32wl/littlefs/lfs_util.c index 0b352c51f..8c820ce04 100644 --- a/src/platform/stm32wl/littlefs/lfs_util.c +++ b/src/platform/stm32wl/littlefs/lfs_util.c @@ -10,19 +10,18 @@ #ifndef LFS_CONFIG // Software CRC implementation with small lookup table -void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) -{ - static const uint32_t rtable[16] = { - 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, - 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, - }; +void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) { + static const uint32_t rtable[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, + }; - const uint8_t *data = buffer; + const uint8_t *data = buffer; - for (size_t i = 0; i < size; i++) { - *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf]; - *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf]; - } + for (size_t i = 0; i < size; i++) { + *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf]; + *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf]; + } } #endif diff --git a/src/platform/stm32wl/littlefs/lfs_util.h b/src/platform/stm32wl/littlefs/lfs_util.h index 5c8469f88..6ab69766a 100644 --- a/src/platform/stm32wl/littlefs/lfs_util.h +++ b/src/platform/stm32wl/littlefs/lfs_util.h @@ -80,114 +80,96 @@ extern "C" { // expensive basic C implementation for debugging purposes // Min/max functions for unsigned 32-bit numbers -static inline uint32_t lfs_max(uint32_t a, uint32_t b) -{ - return (a > b) ? a : b; -} +static inline uint32_t lfs_max(uint32_t a, uint32_t b) { return (a > b) ? a : b; } -static inline uint32_t lfs_min(uint32_t a, uint32_t b) -{ - return (a < b) ? a : b; -} +static inline uint32_t lfs_min(uint32_t a, uint32_t b) { return (a < b) ? a : b; } // Find the next smallest power of 2 less than or equal to a -static inline uint32_t lfs_npw2(uint32_t a) -{ +static inline uint32_t lfs_npw2(uint32_t a) { #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return 32 - __builtin_clz(a - 1); + return 32 - __builtin_clz(a - 1); #else - uint32_t r = 0; - uint32_t s; - a -= 1; - s = (a > 0xffff) << 4; - a >>= s; - r |= s; - s = (a > 0xff) << 3; - a >>= s; - r |= s; - s = (a > 0xf) << 2; - a >>= s; - r |= s; - s = (a > 0x3) << 1; - a >>= s; - r |= s; - return (r | (a >> 1)) + 1; + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; + a >>= s; + r |= s; + s = (a > 0xff) << 3; + a >>= s; + r |= s; + s = (a > 0xf) << 2; + a >>= s; + r |= s; + s = (a > 0x3) << 1; + a >>= s; + r |= s; + return (r | (a >> 1)) + 1; #endif } // Count the number of trailing binary zeros in a // lfs_ctz(0) may be undefined -static inline uint32_t lfs_ctz(uint32_t a) -{ +static inline uint32_t lfs_ctz(uint32_t a) { #if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) - return __builtin_ctz(a); + return __builtin_ctz(a); #else - return lfs_npw2((a & -a) + 1) - 1; + return lfs_npw2((a & -a) + 1) - 1; #endif } // Count the number of binary ones in a -static inline uint32_t lfs_popc(uint32_t a) -{ +static inline uint32_t lfs_popc(uint32_t a) { #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return __builtin_popcount(a); + return __builtin_popcount(a); #else - a = a - ((a >> 1) & 0x55555555); - a = (a & 0x33333333) + ((a >> 2) & 0x33333333); - return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; #endif } // Find the sequence comparison of a and b, this is the distance // between a and b ignoring overflow -static inline int lfs_scmp(uint32_t a, uint32_t b) -{ - return (int)(unsigned)(a - b); -} +static inline int lfs_scmp(uint32_t a, uint32_t b) { return (int)(unsigned)(a - b); } // Convert from 32-bit little-endian to native order -static inline uint32_t lfs_fromle32(uint32_t a) -{ -#if !defined(LFS_NO_INTRINSICS) && ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || \ - (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ - (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) - return a; -#elif !defined(LFS_NO_INTRINSICS) && \ - ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ +static inline uint32_t lfs_fromle32(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && \ + ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return a; +#elif !defined(LFS_NO_INTRINSICS) && \ + ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) - return __builtin_bswap32(a); + return __builtin_bswap32(a); #else - return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); + return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); #endif } // Convert to 32-bit little-endian from native order -static inline uint32_t lfs_tole32(uint32_t a) -{ - return lfs_fromle32(a); -} +static inline uint32_t lfs_tole32(uint32_t a) { return lfs_fromle32(a); } // Calculate CRC-32 with polynomial = 0x04c11db7 void lfs_crc(uint32_t *crc, const void *buffer, size_t size); // Allocate memory, only used if buffers are not provided to littlefs -static inline void *lfs_malloc(size_t size) -{ +static inline void *lfs_malloc(size_t size) { #ifndef LFS_NO_MALLOC - return malloc(size); + return malloc(size); #else - (void)size; - return NULL; + (void)size; + return NULL; #endif } // Deallocate memory, only used if buffers are not provided to littlefs -static inline void lfs_free(void *p) -{ +static inline void lfs_free(void *p) { #ifndef LFS_NO_MALLOC - free(p); + free(p); #else - (void)p; + (void)p; #endif } diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index e841f8f29..2af57286f 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -9,20 +9,19 @@ void playStartMelody() {} void updateBatteryLevel(uint8_t level) {} -void getMacAddr(uint8_t *dmac) -{ - // https://flit.github.io/2020/06/06/mcu-unique-id-survey.html - const uint32_t uid0 = HAL_GetUIDw0(); // X/Y coordinate on wafer - const uint32_t uid1 = HAL_GetUIDw1(); // [31:8] Lot number (23:0), [7:0] Wafer number - const uint32_t uid2 = HAL_GetUIDw2(); // Lot number (55:24) +void getMacAddr(uint8_t *dmac) { + // https://flit.github.io/2020/06/06/mcu-unique-id-survey.html + const uint32_t uid0 = HAL_GetUIDw0(); // X/Y coordinate on wafer + const uint32_t uid1 = HAL_GetUIDw1(); // [31:8] Lot number (23:0), [7:0] Wafer number + const uint32_t uid2 = HAL_GetUIDw2(); // Lot number (55:24) - // Need to go from 96-bit to 48-bit unique ID - dmac[5] = (uint8_t)uid0; - dmac[4] = (uint8_t)(uid0 >> 16); - dmac[3] = (uint8_t)uid1; - dmac[2] = (uint8_t)(uid1 >> 8); - dmac[1] = (uint8_t)uid2; - dmac[0] = (uint8_t)(uid2 >> 8); + // Need to go from 96-bit to 48-bit unique ID + dmac[5] = (uint8_t)uid0; + dmac[4] = (uint8_t)(uid0 >> 16); + dmac[3] = (uint8_t)uid1; + dmac[2] = (uint8_t)(uid1 >> 8); + dmac[1] = (uint8_t)uid2; + dmac[0] = (uint8_t)(uid2 >> 8); } void cpuDeepSleep(uint32_t msecToWake) {} @@ -30,27 +29,20 @@ void cpuDeepSleep(uint32_t msecToWake) {} // Hacks to force more code and data out. // By default __assert_func uses fiprintf which pulls in stdio. -extern "C" void __wrap___assert_func(const char *, int, const char *, const char *) -{ - while (true) - ; - return; +extern "C" void __wrap___assert_func(const char *, int, const char *, const char *) { + while (true) + ; + return; } // By default strerror has a lot of strings we probably don't use. Make it return an empty string instead. char empty = 0; -extern "C" char *__wrap_strerror(int) -{ - return ∅ -} +extern "C" char *__wrap_strerror(int) { return ∅ } #ifdef MESHTASTIC_EXCLUDE_TZ struct _reent; -// Even if you don't use timezones, mktime will try to set the timezone anyway with _tzset_unlocked(), which pulls in scanf and -// friends. The timezone is initialized to UTC by default. -extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) -{ - return; -} +// Even if you don't use timezones, mktime will try to set the timezone anyway with _tzset_unlocked(), which pulls in +// scanf and friends. The timezone is initialized to UTC by default. +extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) { return; } #endif \ No newline at end of file diff --git a/src/power.h b/src/power.h index c826d98b4..710d21131 100644 --- a/src/power.h +++ b/src/power.h @@ -93,43 +93,42 @@ extern RAK9154Sensor rak9154Sensor; extern XPowersLibInterface *PMU; #endif -class Power : private concurrency::OSThread -{ +class Power : private concurrency::OSThread { - public: - Observable newStatus; +public: + Observable newStatus; - Power(); + Power(); - void powerCommandsCheck(); - void readPowerStatus(); - virtual bool setup(); - virtual int32_t runOnce() override; - void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } - const uint16_t OCV[11] = {OCV_ARRAY}; + void powerCommandsCheck(); + void readPowerStatus(); + virtual bool setup(); + virtual int32_t runOnce() override; + void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } + const uint16_t OCV[11] = {OCV_ARRAY}; - protected: - meshtastic::PowerStatus *statusHandler; +protected: + meshtastic::PowerStatus *statusHandler; - /// Setup a xpowers chip axp192/axp2101, return true if found - bool axpChipInit(); - /// Setup a simple ADC input based battery sensor - bool analogInit(); - /// Setup a Lipo battery level sensor - bool lipoInit(); - /// Setup a Lipo charger - bool lipoChargerInit(); - /// Setup a meshSolar battery sensor - bool meshSolarInit(); + /// Setup a xpowers chip axp192/axp2101, return true if found + bool axpChipInit(); + /// Setup a simple ADC input based battery sensor + bool analogInit(); + /// Setup a Lipo battery level sensor + bool lipoInit(); + /// Setup a Lipo charger + bool lipoChargerInit(); + /// Setup a meshSolar battery sensor + bool meshSolarInit(); - private: - void shutdown(); - void reboot(); - // open circuit voltage lookup table - uint8_t low_voltage_counter; - uint32_t lastLogTime = 0; +private: + void shutdown(); + void reboot(); + // open circuit voltage lookup table + uint8_t low_voltage_counter; + uint32_t lastLogTime = 0; #ifdef DEBUG_HEAP - uint32_t lastheap; + uint32_t lastheap; #endif }; diff --git a/src/serialization/JSON.cpp b/src/serialization/JSON.cpp index 42e615108..fc19aa720 100644 --- a/src/serialization/JSON.cpp +++ b/src/serialization/JSON.cpp @@ -41,25 +41,24 @@ JSON::JSON() {} * * @return JSONValue* Returns a JSON Value representing the root, or NULL on error */ -JSONValue *JSON::Parse(const char *data) -{ - // Skip any preceding whitespace, end of data = no JSON = fail - if (!SkipWhitespace(&data)) - return NULL; +JSONValue *JSON::Parse(const char *data) { + // Skip any preceding whitespace, end of data = no JSON = fail + if (!SkipWhitespace(&data)) + return NULL; - // We need the start of a value here now... - JSONValue *value = JSONValue::Parse(&data); - if (value == NULL) - return NULL; + // We need the start of a value here now... + JSONValue *value = JSONValue::Parse(&data); + if (value == NULL) + return NULL; - // Can be white space now and should be at the end of the string then... - if (SkipWhitespace(&data)) { - delete value; - return NULL; - } + // Can be white space now and should be at the end of the string then... + if (SkipWhitespace(&data)) { + delete value; + return NULL; + } - // We're now at the end of the string - return value; + // We're now at the end of the string + return value; } /** @@ -71,12 +70,11 @@ JSONValue *JSON::Parse(const char *data) * * @return std::string Returns a JSON encoded string representation of the given value */ -std::string JSON::Stringify(const JSONValue *value) -{ - if (value != NULL) - return value->Stringify(); - else - return ""; +std::string JSON::Stringify(const JSONValue *value) { + if (value != NULL) + return value->Stringify(); + else + return ""; } /** @@ -88,12 +86,11 @@ std::string JSON::Stringify(const JSONValue *value) * * @return bool Returns true if there is more data, or false if the end of the text was reached */ -bool JSON::SkipWhitespace(const char **data) -{ - while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n')) - (*data)++; +bool JSON::SkipWhitespace(const char **data) { + while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n')) + (*data)++; - return **data != 0; + return **data != 0; } /** @@ -107,102 +104,101 @@ bool JSON::SkipWhitespace(const char **data) * * @return bool Returns true on success, false on failure */ -bool JSON::ExtractString(const char **data, std::string &str) -{ - str = ""; +bool JSON::ExtractString(const char **data, std::string &str) { + str = ""; - while (**data != 0) { - // Save the char so we can change it if need be - char next_char = **data; + while (**data != 0) { + // Save the char so we can change it if need be + char next_char = **data; - // Escaping something? - if (next_char == '\\') { - // Move over the escape char - (*data)++; + // Escaping something? + if (next_char == '\\') { + // Move over the escape char + (*data)++; - // Deal with the escaped char - switch (**data) { - case '"': - next_char = '"'; - break; - case '\\': - next_char = '\\'; - break; - case '/': - next_char = '/'; - break; - case 'b': - next_char = '\b'; - break; - case 'f': - next_char = '\f'; - break; - case 'n': - next_char = '\n'; - break; - case 'r': - next_char = '\r'; - break; - case 't': - next_char = '\t'; - break; - case 'u': { - // We need 5 chars (4 hex + the 'u') or its not valid - if (!simplejson_csnlen(*data, 5)) - return false; + // Deal with the escaped char + switch (**data) { + case '"': + next_char = '"'; + break; + case '\\': + next_char = '\\'; + break; + case '/': + next_char = '/'; + break; + case 'b': + next_char = '\b'; + break; + case 'f': + next_char = '\f'; + break; + case 'n': + next_char = '\n'; + break; + case 'r': + next_char = '\r'; + break; + case 't': + next_char = '\t'; + break; + case 'u': { + // We need 5 chars (4 hex + the 'u') or its not valid + if (!simplejson_csnlen(*data, 5)) + return false; - // Deal with the chars - next_char = 0; - for (int i = 0; i < 4; i++) { - // Do it first to move off the 'u' and leave us on the - // final hex digit as we move on by one later on - (*data)++; + // Deal with the chars + next_char = 0; + for (int i = 0; i < 4; i++) { + // Do it first to move off the 'u' and leave us on the + // final hex digit as we move on by one later on + (*data)++; - next_char <<= 4; + next_char <<= 4; - // Parse the hex digit - if (**data >= '0' && **data <= '9') - next_char |= (**data - '0'); - else if (**data >= 'A' && **data <= 'F') - next_char |= (10 + (**data - 'A')); - else if (**data >= 'a' && **data <= 'f') - next_char |= (10 + (**data - 'a')); - else { - // Invalid hex digit = invalid JSON - return false; - } - } - break; - } - - // By the spec, only the above cases are allowed - default: - return false; - } - } - - // End of the string? - else if (next_char == '"') { - (*data)++; - str.shrink_to_fit(); // Remove unused capacity - return true; - } - - // Disallowed char? - else if (next_char < ' ' && next_char != '\t') { - // SPEC Violation: Allow tabs due to real world cases + // Parse the hex digit + if (**data >= '0' && **data <= '9') + next_char |= (**data - '0'); + else if (**data >= 'A' && **data <= 'F') + next_char |= (10 + (**data - 'A')); + else if (**data >= 'a' && **data <= 'f') + next_char |= (10 + (**data - 'a')); + else { + // Invalid hex digit = invalid JSON return false; + } } + break; + } - // Add the next char - str += next_char; - - // Move on - (*data)++; + // By the spec, only the above cases are allowed + default: + return false; + } } - // If we're here, the string ended incorrectly - return false; + // End of the string? + else if (next_char == '"') { + (*data)++; + str.shrink_to_fit(); // Remove unused capacity + return true; + } + + // Disallowed char? + else if (next_char < ' ' && next_char != '\t') { + // SPEC Violation: Allow tabs due to real world cases + return false; + } + + // Add the next char + str += next_char; + + // Move on + (*data)++; + } + + // If we're here, the string ended incorrectly + return false; } /** @@ -214,13 +210,12 @@ bool JSON::ExtractString(const char **data, std::string &str) * * @return double Returns the double value of the number found */ -double JSON::ParseInt(const char **data) -{ - double integer = 0; - while (**data != 0 && **data >= '0' && **data <= '9') - integer = integer * 10 + (*(*data)++ - '0'); +double JSON::ParseInt(const char **data) { + double integer = 0; + while (**data != 0 && **data >= '0' && **data <= '9') + integer = integer * 10 + (*(*data)++ - '0'); - return integer; + return integer; } /** @@ -232,14 +227,13 @@ double JSON::ParseInt(const char **data) * * @return double Returns the double value of the decimal found */ -double JSON::ParseDecimal(const char **data) -{ - double decimal = 0.0; - double factor = 0.1; - while (**data != 0 && **data >= '0' && **data <= '9') { - int digit = (*(*data)++ - '0'); - decimal = decimal + digit * factor; - factor *= 0.1; - } - return decimal; +double JSON::ParseDecimal(const char **data) { + double decimal = 0.0; + double factor = 0.1; + while (**data != 0 && **data >= '0' && **data <= '9') { + int digit = (*(*data)++ - '0'); + decimal = decimal + digit * factor; + factor *= 0.1; + } + return decimal; } diff --git a/src/serialization/JSON.h b/src/serialization/JSON.h index 33f34e684..896948d7f 100644 --- a/src/serialization/JSON.h +++ b/src/serialization/JSON.h @@ -31,18 +31,17 @@ #include // Simple function to check a string 's' has at least 'n' characters -static inline bool simplejson_csnlen(const char *s, size_t n) -{ - if (s == 0) - return false; +static inline bool simplejson_csnlen(const char *s, size_t n) { + if (s == 0) + return false; - const char *save = s; - while (n-- > 0) { - if (*(save++) == 0) - return false; - } + const char *save = s; + while (n-- > 0) { + if (*(save++) == 0) + return false; + } - return true; + return true; } // Custom types @@ -52,22 +51,21 @@ typedef std::map JSONObject; #include "JSONValue.h" -class JSON -{ - friend class JSONValue; +class JSON { + friend class JSONValue; - public: - static JSONValue *Parse(const char *data); - static std::string Stringify(const JSONValue *value); +public: + static JSONValue *Parse(const char *data); + static std::string Stringify(const JSONValue *value); - protected: - static bool SkipWhitespace(const char **data); - static bool ExtractString(const char **data, std::string &str); - static double ParseInt(const char **data); - static double ParseDecimal(const char **data); +protected: + static bool SkipWhitespace(const char **data); + static bool ExtractString(const char **data, std::string &str); + static double ParseInt(const char **data); + static double ParseDecimal(const char **data); - private: - JSON(); +private: + JSON(); }; #endif diff --git a/src/serialization/JSONValue.cpp b/src/serialization/JSONValue.cpp index 20cd90373..723784177 100644 --- a/src/serialization/JSONValue.cpp +++ b/src/serialization/JSONValue.cpp @@ -33,20 +33,20 @@ #include "JSONValue.h" // Macros to free an array/object -#define FREE_ARRAY(x) \ - { \ - JSONArray::iterator iter; \ - for (iter = x.begin(); iter != x.end(); ++iter) { \ - delete *iter; \ - } \ - } -#define FREE_OBJECT(x) \ - { \ - JSONObject::iterator iter; \ - for (iter = x.begin(); iter != x.end(); ++iter) { \ - delete (*iter).second; \ - } \ - } +#define FREE_ARRAY(x) \ + { \ + JSONArray::iterator iter; \ + for (iter = x.begin(); iter != x.end(); ++iter) { \ + delete *iter; \ + } \ + } +#define FREE_OBJECT(x) \ + { \ + JSONObject::iterator iter; \ + for (iter = x.begin(); iter != x.end(); ++iter) { \ + delete (*iter).second; \ + } \ + } /** * Parses a JSON encoded value to a JSONValue object @@ -57,234 +57,233 @@ * * @return JSONValue* Returns a pointer to a JSONValue object on success, NULL on error */ -JSONValue *JSONValue::Parse(const char **data) -{ - // Is it a string? - if (**data == '"') { - std::string str; - if (!JSON::ExtractString(&(++(*data)), str)) - return NULL; - else - return new JSONValue(str); +JSONValue *JSONValue::Parse(const char **data) { + // Is it a string? + if (**data == '"') { + std::string str; + if (!JSON::ExtractString(&(++(*data)), str)) + return NULL; + else + return new JSONValue(str); + } + + // Is it a boolean? + else if ((simplejson_csnlen(*data, 4) && strncasecmp(*data, "true", 4) == 0) || + (simplejson_csnlen(*data, 5) && strncasecmp(*data, "false", 5) == 0)) { + bool value = strncasecmp(*data, "true", 4) == 0; + (*data) += value ? 4 : 5; + return new JSONValue(value); + } + + // Is it a null? + else if (simplejson_csnlen(*data, 4) && strncasecmp(*data, "null", 4) == 0) { + (*data) += 4; + return new JSONValue(); + } + + // Is it a number? + else if (**data == '-' || (**data >= '0' && **data <= '9')) { + // Negative? + bool neg = **data == '-'; + if (neg) + (*data)++; + + double number = 0.0; + + // Parse the whole part of the number - only if it wasn't 0 + if (**data == '0') + (*data)++; + else if (**data >= '1' && **data <= '9') + number = JSON::ParseInt(data); + else + return NULL; + + // Could be a decimal now... + if (**data == '.') { + (*data)++; + + // Not get any digits? + if (!(**data >= '0' && **data <= '9')) + return NULL; + + // Find the decimal and sort the decimal place out + // Use ParseDecimal as ParseInt won't work with decimals less than 0.1 + // thanks to Javier Abadia for the report & fix + double decimal = JSON::ParseDecimal(data); + + // Save the number + number += decimal; } - // Is it a boolean? - else if ((simplejson_csnlen(*data, 4) && strncasecmp(*data, "true", 4) == 0) || - (simplejson_csnlen(*data, 5) && strncasecmp(*data, "false", 5) == 0)) { - bool value = strncasecmp(*data, "true", 4) == 0; - (*data) += value ? 4 : 5; - return new JSONValue(value); - } - - // Is it a null? - else if (simplejson_csnlen(*data, 4) && strncasecmp(*data, "null", 4) == 0) { - (*data) += 4; - return new JSONValue(); - } - - // Is it a number? - else if (**data == '-' || (**data >= '0' && **data <= '9')) { - // Negative? - bool neg = **data == '-'; - if (neg) - (*data)++; - - double number = 0.0; - - // Parse the whole part of the number - only if it wasn't 0 - if (**data == '0') - (*data)++; - else if (**data >= '1' && **data <= '9') - number = JSON::ParseInt(data); - else - return NULL; - - // Could be a decimal now... - if (**data == '.') { - (*data)++; - - // Not get any digits? - if (!(**data >= '0' && **data <= '9')) - return NULL; - - // Find the decimal and sort the decimal place out - // Use ParseDecimal as ParseInt won't work with decimals less than 0.1 - // thanks to Javier Abadia for the report & fix - double decimal = JSON::ParseDecimal(data); - - // Save the number - number += decimal; - } - - // Could be an exponent now... - if (**data == 'E' || **data == 'e') { - (*data)++; - - // Check signage of expo - bool neg_expo = false; - if (**data == '-' || **data == '+') { - neg_expo = **data == '-'; - (*data)++; - } - - // Not get any digits? - if (!(**data >= '0' && **data <= '9')) - return NULL; - - // Sort the expo out - double expo = JSON::ParseInt(data); - for (double i = 0.0; i < expo; i++) - number = neg_expo ? (number / 10.0) : (number * 10.0); - } - - // Was it neg? - if (neg) - number *= -1; - - return new JSONValue(number); - } - - // An object? - else if (**data == '{') { - JSONObject object; + // Could be an exponent now... + if (**data == 'E' || **data == 'e') { + (*data)++; + // Check signage of expo + bool neg_expo = false; + if (**data == '-' || **data == '+') { + neg_expo = **data == '-'; (*data)++; + } - while (**data != 0) { - // Whitespace at the start? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } + // Not get any digits? + if (!(**data >= '0' && **data <= '9')) + return NULL; - // Special case - empty object - if (object.size() == 0 && **data == '}') { - (*data)++; - return new JSONValue(object); - } + // Sort the expo out + double expo = JSON::ParseInt(data); + for (double i = 0.0; i < expo; i++) + number = neg_expo ? (number / 10.0) : (number * 10.0); + } - // We want a string now... - std::string name; - if (!JSON::ExtractString(&(++(*data)), name)) { - FREE_OBJECT(object); - return NULL; - } + // Was it neg? + if (neg) + number *= -1; - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } + return new JSONValue(number); + } - // Need a : now - if (*((*data)++) != ':') { - FREE_OBJECT(object); - return NULL; - } + // An object? + else if (**data == '{') { + JSONObject object; - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } + (*data)++; - // The value is here - JSONValue *value = Parse(data); - if (value == NULL) { - FREE_OBJECT(object); - return NULL; - } - - // Add the name:value - if (object.find(name) != object.end()) - delete object[name]; - object[name] = value; - - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } - - // End of object? - if (**data == '}') { - (*data)++; - return new JSONValue(object); - } - - // Want a , now - if (**data != ',') { - FREE_OBJECT(object); - return NULL; - } - - (*data)++; - } - - // Only here if we ran out of data + while (**data != 0) { + // Whitespace at the start? + if (!JSON::SkipWhitespace(data)) { FREE_OBJECT(object); return NULL; + } + + // Special case - empty object + if (object.size() == 0 && **data == '}') { + (*data)++; + return new JSONValue(object); + } + + // We want a string now... + std::string name; + if (!JSON::ExtractString(&(++(*data)), name)) { + FREE_OBJECT(object); + return NULL; + } + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // Need a : now + if (*((*data)++) != ':') { + FREE_OBJECT(object); + return NULL; + } + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // The value is here + JSONValue *value = Parse(data); + if (value == NULL) { + FREE_OBJECT(object); + return NULL; + } + + // Add the name:value + if (object.find(name) != object.end()) + delete object[name]; + object[name] = value; + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // End of object? + if (**data == '}') { + (*data)++; + return new JSONValue(object); + } + + // Want a , now + if (**data != ',') { + FREE_OBJECT(object); + return NULL; + } + + (*data)++; } - // An array? - else if (**data == '[') { - JSONArray array; + // Only here if we ran out of data + FREE_OBJECT(object); + return NULL; + } - (*data)++; + // An array? + else if (**data == '[') { + JSONArray array; - while (**data != 0) { - // Whitespace at the start? - if (!JSON::SkipWhitespace(data)) { - FREE_ARRAY(array); - return NULL; - } + (*data)++; - // Special case - empty array - if (array.size() == 0 && **data == ']') { - (*data)++; - return new JSONValue(array); - } - - // Get the value - JSONValue *value = Parse(data); - if (value == NULL) { - FREE_ARRAY(array); - return NULL; - } - - // Add the value - array.push_back(value); - - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_ARRAY(array); - return NULL; - } - - // End of array? - if (**data == ']') { - (*data)++; - return new JSONValue(array); - } - - // Want a , now - if (**data != ',') { - FREE_ARRAY(array); - return NULL; - } - - (*data)++; - } - - // Only here if we ran out of data + while (**data != 0) { + // Whitespace at the start? + if (!JSON::SkipWhitespace(data)) { FREE_ARRAY(array); return NULL; + } + + // Special case - empty array + if (array.size() == 0 && **data == ']') { + (*data)++; + return new JSONValue(array); + } + + // Get the value + JSONValue *value = Parse(data); + if (value == NULL) { + FREE_ARRAY(array); + return NULL; + } + + // Add the value + array.push_back(value); + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_ARRAY(array); + return NULL; + } + + // End of array? + if (**data == ']') { + (*data)++; + return new JSONValue(array); + } + + // Want a , now + if (**data != ',') { + FREE_ARRAY(array); + return NULL; + } + + (*data)++; } - // Ran out of possibilities, it's bad! - else { - return NULL; - } + // Only here if we ran out of data + FREE_ARRAY(array); + return NULL; + } + + // Ran out of possibilities, it's bad! + else { + return NULL; + } } /** @@ -292,10 +291,7 @@ JSONValue *JSONValue::Parse(const char **data) * * @access public */ -JSONValue::JSONValue(/*NULL*/) -{ - type = JSONType_Null; -} +JSONValue::JSONValue(/*NULL*/) { type = JSONType_Null; } /** * Basic constructor for creating a JSON Value of type String @@ -304,10 +300,9 @@ JSONValue::JSONValue(/*NULL*/) * * @param char* m_char_value The string to use as the value */ -JSONValue::JSONValue(const char *m_char_value) -{ - type = JSONType_String; - string_value = new std::string(std::string(m_char_value)); +JSONValue::JSONValue(const char *m_char_value) { + type = JSONType_String; + string_value = new std::string(std::string(m_char_value)); } /** @@ -317,10 +312,9 @@ JSONValue::JSONValue(const char *m_char_value) * * @param std::string m_string_value The string to use as the value */ -JSONValue::JSONValue(const std::string &m_string_value) -{ - type = JSONType_String; - string_value = new std::string(m_string_value); +JSONValue::JSONValue(const std::string &m_string_value) { + type = JSONType_String; + string_value = new std::string(m_string_value); } /** @@ -330,10 +324,9 @@ JSONValue::JSONValue(const std::string &m_string_value) * * @param bool m_bool_value The bool to use as the value */ -JSONValue::JSONValue(bool m_bool_value) -{ - type = JSONType_Bool; - bool_value = m_bool_value; +JSONValue::JSONValue(bool m_bool_value) { + type = JSONType_Bool; + bool_value = m_bool_value; } /** @@ -343,10 +336,9 @@ JSONValue::JSONValue(bool m_bool_value) * * @param double m_number_value The number to use as the value */ -JSONValue::JSONValue(double m_number_value) -{ - type = JSONType_Number; - number_value = m_number_value; +JSONValue::JSONValue(double m_number_value) { + type = JSONType_Number; + number_value = m_number_value; } /** @@ -356,10 +348,9 @@ JSONValue::JSONValue(double m_number_value) * * @param int m_integer_value The number to use as the value */ -JSONValue::JSONValue(int m_integer_value) -{ - type = JSONType_Number; - number_value = (double)m_integer_value; +JSONValue::JSONValue(int m_integer_value) { + type = JSONType_Number; + number_value = (double)m_integer_value; } /** @@ -369,10 +360,9 @@ JSONValue::JSONValue(int m_integer_value) * * @param unsigned int m_integer_value The number to use as the value */ -JSONValue::JSONValue(unsigned int m_integer_value) -{ - type = JSONType_Number; - number_value = (double)m_integer_value; +JSONValue::JSONValue(unsigned int m_integer_value) { + type = JSONType_Number; + number_value = (double)m_integer_value; } /** @@ -382,10 +372,9 @@ JSONValue::JSONValue(unsigned int m_integer_value) * * @param JSONArray m_array_value The JSONArray to use as the value */ -JSONValue::JSONValue(const JSONArray &m_array_value) -{ - type = JSONType_Array; - array_value = new JSONArray(m_array_value); +JSONValue::JSONValue(const JSONArray &m_array_value) { + type = JSONType_Array; + array_value = new JSONArray(m_array_value); } /** @@ -395,10 +384,9 @@ JSONValue::JSONValue(const JSONArray &m_array_value) * * @param JSONObject m_object_value The JSONObject to use as the value */ -JSONValue::JSONValue(const JSONObject &m_object_value) -{ - type = JSONType_Object; - object_value = new JSONObject(m_object_value); +JSONValue::JSONValue(const JSONObject &m_object_value) { + type = JSONType_Object; + object_value = new JSONObject(m_object_value); } /** @@ -408,47 +396,46 @@ JSONValue::JSONValue(const JSONObject &m_object_value) * * @param JSONValue m_source The source JSONValue that is being copied */ -JSONValue::JSONValue(const JSONValue &m_source) -{ - type = m_source.type; +JSONValue::JSONValue(const JSONValue &m_source) { + type = m_source.type; - switch (type) { - case JSONType_String: - string_value = new std::string(*m_source.string_value); - break; + switch (type) { + case JSONType_String: + string_value = new std::string(*m_source.string_value); + break; - case JSONType_Bool: - bool_value = m_source.bool_value; - break; + case JSONType_Bool: + bool_value = m_source.bool_value; + break; - case JSONType_Number: - number_value = m_source.number_value; - break; + case JSONType_Number: + number_value = m_source.number_value; + break; - case JSONType_Array: { - JSONArray source_array = *m_source.array_value; - JSONArray::iterator iter; - array_value = new JSONArray(); - for (iter = source_array.begin(); iter != source_array.end(); ++iter) - array_value->push_back(new JSONValue(**iter)); - break; + case JSONType_Array: { + JSONArray source_array = *m_source.array_value; + JSONArray::iterator iter; + array_value = new JSONArray(); + for (iter = source_array.begin(); iter != source_array.end(); ++iter) + array_value->push_back(new JSONValue(**iter)); + break; + } + + case JSONType_Object: { + JSONObject source_object = *m_source.object_value; + object_value = new JSONObject(); + JSONObject::iterator iter; + for (iter = source_object.begin(); iter != source_object.end(); ++iter) { + std::string name = (*iter).first; + (*object_value)[name] = new JSONValue(*((*iter).second)); } + break; + } - case JSONType_Object: { - JSONObject source_object = *m_source.object_value; - object_value = new JSONObject(); - JSONObject::iterator iter; - for (iter = source_object.begin(); iter != source_object.end(); ++iter) { - std::string name = (*iter).first; - (*object_value)[name] = new JSONValue(*((*iter).second)); - } - break; - } - - case JSONType_Null: - // Nothing to do. - break; - } + case JSONType_Null: + // Nothing to do. + break; + } } /** @@ -457,22 +444,21 @@ JSONValue::JSONValue(const JSONValue &m_source) * * @access public */ -JSONValue::~JSONValue() -{ - if (type == JSONType_Array) { - JSONArray::iterator iter; - for (iter = array_value->begin(); iter != array_value->end(); ++iter) - delete *iter; - delete array_value; - } else if (type == JSONType_Object) { - JSONObject::iterator iter; - for (iter = object_value->begin(); iter != object_value->end(); ++iter) { - delete (*iter).second; - } - delete object_value; - } else if (type == JSONType_String) { - delete string_value; +JSONValue::~JSONValue() { + if (type == JSONType_Array) { + JSONArray::iterator iter; + for (iter = array_value->begin(); iter != array_value->end(); ++iter) + delete *iter; + delete array_value; + } else if (type == JSONType_Object) { + JSONObject::iterator iter; + for (iter = object_value->begin(); iter != object_value->end(); ++iter) { + delete (*iter).second; } + delete object_value; + } else if (type == JSONType_String) { + delete string_value; + } } /** @@ -482,10 +468,7 @@ JSONValue::~JSONValue() * * @return bool Returns true if it is a NULL value, false otherwise */ -bool JSONValue::IsNull() const -{ - return type == JSONType_Null; -} +bool JSONValue::IsNull() const { return type == JSONType_Null; } /** * Checks if the value is a String @@ -494,10 +477,7 @@ bool JSONValue::IsNull() const * * @return bool Returns true if it is a String value, false otherwise */ -bool JSONValue::IsString() const -{ - return type == JSONType_String; -} +bool JSONValue::IsString() const { return type == JSONType_String; } /** * Checks if the value is a Bool @@ -506,10 +486,7 @@ bool JSONValue::IsString() const * * @return bool Returns true if it is a Bool value, false otherwise */ -bool JSONValue::IsBool() const -{ - return type == JSONType_Bool; -} +bool JSONValue::IsBool() const { return type == JSONType_Bool; } /** * Checks if the value is a Number @@ -518,10 +495,7 @@ bool JSONValue::IsBool() const * * @return bool Returns true if it is a Number value, false otherwise */ -bool JSONValue::IsNumber() const -{ - return type == JSONType_Number; -} +bool JSONValue::IsNumber() const { return type == JSONType_Number; } /** * Checks if the value is an Array @@ -530,10 +504,7 @@ bool JSONValue::IsNumber() const * * @return bool Returns true if it is an Array value, false otherwise */ -bool JSONValue::IsArray() const -{ - return type == JSONType_Array; -} +bool JSONValue::IsArray() const { return type == JSONType_Array; } /** * Checks if the value is an Object @@ -542,10 +513,7 @@ bool JSONValue::IsArray() const * * @return bool Returns true if it is an Object value, false otherwise */ -bool JSONValue::IsObject() const -{ - return type == JSONType_Object; -} +bool JSONValue::IsObject() const { return type == JSONType_Object; } /** * Retrieves the String value of this JSONValue @@ -555,10 +523,7 @@ bool JSONValue::IsObject() const * * @return std::string Returns the string value */ -const std::string &JSONValue::AsString() const -{ - return (*string_value); -} +const std::string &JSONValue::AsString() const { return (*string_value); } /** * Retrieves the Bool value of this JSONValue @@ -568,10 +533,7 @@ const std::string &JSONValue::AsString() const * * @return bool Returns the bool value */ -bool JSONValue::AsBool() const -{ - return bool_value; -} +bool JSONValue::AsBool() const { return bool_value; } /** * Retrieves the Number value of this JSONValue @@ -581,10 +543,7 @@ bool JSONValue::AsBool() const * * @return double Returns the number value */ -double JSONValue::AsNumber() const -{ - return number_value; -} +double JSONValue::AsNumber() const { return number_value; } /** * Retrieves the Array value of this JSONValue @@ -594,10 +553,7 @@ double JSONValue::AsNumber() const * * @return JSONArray Returns the array value */ -const JSONArray &JSONValue::AsArray() const -{ - return (*array_value); -} +const JSONArray &JSONValue::AsArray() const { return (*array_value); } /** * Retrieves the Object value of this JSONValue @@ -607,10 +563,7 @@ const JSONArray &JSONValue::AsArray() const * * @return JSONObject Returns the object value */ -const JSONObject &JSONValue::AsObject() const -{ - return (*object_value); -} +const JSONObject &JSONValue::AsObject() const { return (*object_value); } /** * Retrieves the number of children of this JSONValue. @@ -621,16 +574,15 @@ const JSONObject &JSONValue::AsObject() const * * @return The number of children. */ -std::size_t JSONValue::CountChildren() const -{ - switch (type) { - case JSONType_Array: - return array_value->size(); - case JSONType_Object: - return object_value->size(); - default: - return 0; - } +std::size_t JSONValue::CountChildren() const { + switch (type) { + case JSONType_Array: + return array_value->size(); + case JSONType_Object: + return object_value->size(); + default: + return 0; + } } /** @@ -641,13 +593,12 @@ std::size_t JSONValue::CountChildren() const * * @return bool Returns true if the array has a value at the given index. */ -bool JSONValue::HasChild(std::size_t index) const -{ - if (type == JSONType_Array) { - return index < array_value->size(); - } else { - return false; - } +bool JSONValue::HasChild(std::size_t index) const { + if (type == JSONType_Array) { + return index < array_value->size(); + } else { + return false; + } } /** @@ -659,13 +610,12 @@ bool JSONValue::HasChild(std::size_t index) const * @return JSONValue* Returns JSONValue at the given index or NULL * if it doesn't exist. */ -JSONValue *JSONValue::Child(std::size_t index) -{ - if (index < array_value->size()) { - return (*array_value)[index]; - } else { - return NULL; - } +JSONValue *JSONValue::Child(std::size_t index) { + if (index < array_value->size()) { + return (*array_value)[index]; + } else { + return NULL; + } } /** @@ -676,13 +626,12 @@ JSONValue *JSONValue::Child(std::size_t index) * * @return bool Returns true if the object has a value at the given key. */ -bool JSONValue::HasChild(const char *name) const -{ - if (type == JSONType_Object) { - return object_value->find(name) != object_value->end(); - } else { - return false; - } +bool JSONValue::HasChild(const char *name) const { + if (type == JSONType_Object) { + return object_value->find(name) != object_value->end(); + } else { + return false; + } } /** @@ -694,14 +643,13 @@ bool JSONValue::HasChild(const char *name) const * @return JSONValue* Returns JSONValue for the given key in the object * or NULL if it doesn't exist. */ -JSONValue *JSONValue::Child(const char *name) -{ - JSONObject::const_iterator it = object_value->find(name); - if (it != object_value->end()) { - return it->second; - } else { - return NULL; - } +JSONValue *JSONValue::Child(const char *name) { + JSONObject::const_iterator it = object_value->find(name); + if (it != object_value->end()) { + return it->second; + } else { + return NULL; + } } /** @@ -712,20 +660,19 @@ JSONValue *JSONValue::Child(const char *name) * * @return std::vector A vector containing the keys. */ -std::vector JSONValue::ObjectKeys() const -{ - std::vector keys; +std::vector JSONValue::ObjectKeys() const { + std::vector keys; - if (type == JSONType_Object) { - JSONObject::const_iterator iter = object_value->begin(); - while (iter != object_value->end()) { - keys.push_back(iter->first); + if (type == JSONType_Object) { + JSONObject::const_iterator iter = object_value->begin(); + while (iter != object_value->end()) { + keys.push_back(iter->first); - ++iter; - } + ++iter; } + } - return keys; + return keys; } /** @@ -737,10 +684,9 @@ std::vector JSONValue::ObjectKeys() const * * @return std::string Returns the JSON string */ -std::string JSONValue::Stringify(bool const prettyprint) const -{ - size_t const indentDepth = prettyprint ? 1 : 0; - return StringifyImpl(indentDepth); +std::string JSONValue::Stringify(bool const prettyprint) const { + size_t const indentDepth = prettyprint ? 1 : 0; + return StringifyImpl(indentDepth); } /** @@ -752,70 +698,69 @@ std::string JSONValue::Stringify(bool const prettyprint) const * * @return std::string Returns the JSON string */ -std::string JSONValue::StringifyImpl(size_t const indentDepth) const -{ - std::string ret_string; - size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0; - std::string const indentStr = Indent(indentDepth); - std::string const indentStr1 = Indent(indentDepth1); +std::string JSONValue::StringifyImpl(size_t const indentDepth) const { + std::string ret_string; + size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0; + std::string const indentStr = Indent(indentDepth); + std::string const indentStr1 = Indent(indentDepth1); - switch (type) { - case JSONType_Null: - ret_string = "null"; - break; + switch (type) { + case JSONType_Null: + ret_string = "null"; + break; - case JSONType_String: - ret_string = StringifyString(*string_value); - break; + case JSONType_String: + ret_string = StringifyString(*string_value); + break; - case JSONType_Bool: - ret_string = bool_value ? "true" : "false"; - break; + case JSONType_Bool: + ret_string = bool_value ? "true" : "false"; + break; - case JSONType_Number: { - if (isinf(number_value) || isnan(number_value)) - ret_string = "null"; - else { - std::stringstream ss; - ss.precision(15); - ss << number_value; - ret_string = ss.str(); - } - break; + case JSONType_Number: { + if (isinf(number_value) || isnan(number_value)) + ret_string = "null"; + else { + std::stringstream ss; + ss.precision(15); + ss << number_value; + ret_string = ss.str(); } + break; + } - case JSONType_Array: { - ret_string = indentDepth ? "[\n" + indentStr1 : "["; - JSONArray::const_iterator iter = array_value->begin(); - while (iter != array_value->end()) { - ret_string += (*iter)->StringifyImpl(indentDepth1); + case JSONType_Array: { + ret_string = indentDepth ? "[\n" + indentStr1 : "["; + JSONArray::const_iterator iter = array_value->begin(); + while (iter != array_value->end()) { + ret_string += (*iter)->StringifyImpl(indentDepth1); - // Not at the end - add a separator - if (++iter != array_value->end()) - ret_string += ","; - } - ret_string += indentDepth ? "\n" + indentStr + "]" : "]"; - break; + // Not at the end - add a separator + if (++iter != array_value->end()) + ret_string += ","; } + ret_string += indentDepth ? "\n" + indentStr + "]" : "]"; + break; + } - case JSONType_Object: { - ret_string = indentDepth ? "{\n" + indentStr1 : "{"; - JSONObject::const_iterator iter = object_value->begin(); - while (iter != object_value->end()) { - ret_string += StringifyString((*iter).first); - ret_string += ":"; - ret_string += (*iter).second->StringifyImpl(indentDepth1); + case JSONType_Object: { + ret_string = indentDepth ? "{\n" + indentStr1 : "{"; + JSONObject::const_iterator iter = object_value->begin(); + while (iter != object_value->end()) { + ret_string += StringifyString((*iter).first); + ret_string += ":"; + ret_string += (*iter).second->StringifyImpl(indentDepth1); - // Not at the end - add a separator - if (++iter != object_value->end()) - ret_string += ","; - } - ret_string += indentDepth ? "\n" + indentStr + "}" : "}"; - break; - } + // Not at the end - add a separator + if (++iter != object_value->end()) + ret_string += ","; } + ret_string += indentDepth ? "\n" + indentStr + "}" : "}"; + break; + } + } - return ret_string; + return ret_string; } /** @@ -829,54 +774,53 @@ std::string JSONValue::StringifyImpl(size_t const indentDepth) const * * @return std::string Returns the JSON string */ -std::string JSONValue::StringifyString(const std::string &str) -{ - std::string str_out = "\""; +std::string JSONValue::StringifyString(const std::string &str) { + std::string str_out = "\""; - std::string::const_iterator iter = str.begin(); - while (iter != str.end()) { - char chr = *iter; - - if (chr == '"' || chr == '\\' || chr == '/') { - str_out += '\\'; - str_out += chr; - } else if (chr == '\b') { - str_out += "\\b"; - } else if (chr == '\f') { - str_out += "\\f"; - } else if (chr == '\n') { - str_out += "\\n"; - } else if (chr == '\r') { - str_out += "\\r"; - } else if (chr == '\t') { - str_out += "\\t"; - } else if (chr < 0x20 || chr == 0x7F) { - char buf[7]; - snprintf(buf, sizeof(buf), "\\u%04x", chr); - str_out += buf; - } else if (chr < 0x80) { - str_out += chr; - } else { - str_out += chr; - size_t remain = str.end() - iter - 1; - if ((chr & 0xE0) == 0xC0 && remain >= 1) { - ++iter; - str_out += *iter; - } else if ((chr & 0xF0) == 0xE0 && remain >= 2) { - str_out += *(++iter); - str_out += *(++iter); - } else if ((chr & 0xF8) == 0xF0 && remain >= 3) { - str_out += *(++iter); - str_out += *(++iter); - str_out += *(++iter); - } - } + std::string::const_iterator iter = str.begin(); + while (iter != str.end()) { + char chr = *iter; + if (chr == '"' || chr == '\\' || chr == '/') { + str_out += '\\'; + str_out += chr; + } else if (chr == '\b') { + str_out += "\\b"; + } else if (chr == '\f') { + str_out += "\\f"; + } else if (chr == '\n') { + str_out += "\\n"; + } else if (chr == '\r') { + str_out += "\\r"; + } else if (chr == '\t') { + str_out += "\\t"; + } else if (chr < 0x20 || chr == 0x7F) { + char buf[7]; + snprintf(buf, sizeof(buf), "\\u%04x", chr); + str_out += buf; + } else if (chr < 0x80) { + str_out += chr; + } else { + str_out += chr; + size_t remain = str.end() - iter - 1; + if ((chr & 0xE0) == 0xC0 && remain >= 1) { ++iter; + str_out += *iter; + } else if ((chr & 0xF0) == 0xE0 && remain >= 2) { + str_out += *(++iter); + str_out += *(++iter); + } else if ((chr & 0xF8) == 0xF0 && remain >= 3) { + str_out += *(++iter); + str_out += *(++iter); + str_out += *(++iter); + } } - str_out += "\""; - return str_out; + ++iter; + } + + str_out += "\""; + return str_out; } /** @@ -888,10 +832,9 @@ std::string JSONValue::StringifyString(const std::string &str) * * @return std::string Returns the string */ -std::string JSONValue::Indent(size_t depth) -{ - const size_t indent_step = 2; - depth ? --depth : 0; - std::string indentStr(depth * indent_step, ' '); - return indentStr; +std::string JSONValue::Indent(size_t depth) { + const size_t indent_step = 2; + depth ? --depth : 0; + std::string indentStr(depth * indent_step, ' '); + return indentStr; } \ No newline at end of file diff --git a/src/serialization/JSONValue.h b/src/serialization/JSONValue.h index 16d53e89f..9a947120e 100644 --- a/src/serialization/JSONValue.h +++ b/src/serialization/JSONValue.h @@ -34,62 +34,61 @@ class JSON; enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_Array, JSONType_Object }; -class JSONValue -{ - friend class JSON; +class JSONValue { + friend class JSON; - public: - JSONValue(/*NULL*/); - explicit JSONValue(const char *m_char_value); - explicit JSONValue(const std::string &m_string_value); - explicit JSONValue(bool m_bool_value); - explicit JSONValue(double m_number_value); - explicit JSONValue(int m_integer_value); - explicit JSONValue(unsigned int m_integer_value); - explicit JSONValue(const JSONArray &m_array_value); - explicit JSONValue(const JSONObject &m_object_value); - explicit JSONValue(const JSONValue &m_source); - ~JSONValue(); +public: + JSONValue(/*NULL*/); + explicit JSONValue(const char *m_char_value); + explicit JSONValue(const std::string &m_string_value); + explicit JSONValue(bool m_bool_value); + explicit JSONValue(double m_number_value); + explicit JSONValue(int m_integer_value); + explicit JSONValue(unsigned int m_integer_value); + explicit JSONValue(const JSONArray &m_array_value); + explicit JSONValue(const JSONObject &m_object_value); + explicit JSONValue(const JSONValue &m_source); + ~JSONValue(); - bool IsNull() const; - bool IsString() const; - bool IsBool() const; - bool IsNumber() const; - bool IsArray() const; - bool IsObject() const; + bool IsNull() const; + bool IsString() const; + bool IsBool() const; + bool IsNumber() const; + bool IsArray() const; + bool IsObject() const; - const std::string &AsString() const; - bool AsBool() const; - double AsNumber() const; - const JSONArray &AsArray() const; - const JSONObject &AsObject() const; + const std::string &AsString() const; + bool AsBool() const; + double AsNumber() const; + const JSONArray &AsArray() const; + const JSONObject &AsObject() const; - std::size_t CountChildren() const; - bool HasChild(std::size_t index) const; - JSONValue *Child(std::size_t index); - bool HasChild(const char *name) const; - JSONValue *Child(const char *name); - std::vector ObjectKeys() const; + std::size_t CountChildren() const; + bool HasChild(std::size_t index) const; + JSONValue *Child(std::size_t index); + bool HasChild(const char *name) const; + JSONValue *Child(const char *name); + std::vector ObjectKeys() const; - std::string Stringify(bool const prettyprint = false) const; + std::string Stringify(bool const prettyprint = false) const; - protected: - static JSONValue *Parse(const char **data); +protected: + static JSONValue *Parse(const char **data); - private: - static std::string StringifyString(const std::string &str); - std::string StringifyImpl(size_t const indentDepth) const; - static std::string Indent(size_t depth); +private: + static std::string StringifyString(const std::string &str); + std::string StringifyImpl(size_t const indentDepth) const; + static std::string Indent(size_t depth); - JSONType type; + JSONType type; - union { - bool bool_value; - double number_value; - std::string *string_value; - JSONArray *array_value; - JSONObject *object_value; - }; + union { + bool bool_value; + double number_value; + std::string *string_value; + JSONArray *array_value; + JSONObject *object_value; + }; }; #endif \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index a12972cb0..68f3e3b72 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -15,456 +15,447 @@ static const char *errStr = "Error decoding proto for %s message!"; -std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) -{ - // the created jsonObj is immutable after creation, so - // we need to do the heavy lifting before assembling it. - std::string msgType; - JSONObject jsonObj; +std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) { + // the created jsonObj is immutable after creation, so + // we need to do the heavy lifting before assembling it. + std::string msgType; + JSONObject jsonObj; - if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - JSONObject msgPayload; - switch (mp->decoded.portnum) { - case meshtastic_PortNum_TEXT_MESSAGE_APP: { - msgType = "text"; - // convert bytes to string - if (shouldLog) - LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + JSONObject msgPayload; + switch (mp->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: { + msgType = "text"; + // convert bytes to string + if (shouldLog) + LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - // check if this is a JSON payload - JSONValue *json_value = JSON::Parse(payloadStr); - if (json_value != NULL) { - if (shouldLog) - LOG_INFO("text message payload is of type json"); + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + // check if this is a JSON payload + JSONValue *json_value = JSON::Parse(payloadStr); + if (json_value != NULL) { + if (shouldLog) + LOG_INFO("text message payload is of type json"); - // if it is, then we can just use the json object - jsonObj["payload"] = json_value; - } else { - // if it isn't, then we need to create a json object - // with the string as the value - if (shouldLog) - LOG_INFO("text message payload is of type plaintext"); + // if it is, then we can just use the json object + jsonObj["payload"] = json_value; + } else { + // if it isn't, then we need to create a json object + // with the string as the value + if (shouldLog) + LOG_INFO("text message payload is of type plaintext"); - msgPayload["text"] = new JSONValue(payloadStr); - jsonObj["payload"] = new JSONValue(msgPayload); - } - break; + msgPayload["text"] = new JSONValue(payloadStr); + jsonObj["payload"] = new JSONValue(msgPayload); + } + break; + } + case meshtastic_PortNum_TELEMETRY_APP: { + msgType = "telemetry"; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); + } + msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); + msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); + msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); + msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); + } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + // Avoid sending 0s for sensors that could be 0 + if (decoded->variant.environment_metrics.has_temperature) { + msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); + } + if (decoded->variant.environment_metrics.has_voltage) { + msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); + } + if (decoded->variant.environment_metrics.has_current) { + msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); + } + if (decoded->variant.environment_metrics.has_lux) { + msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); + } + if (decoded->variant.environment_metrics.has_white_lux) { + msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); + } + if (decoded->variant.environment_metrics.has_iaq) { + msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); + } + if (decoded->variant.environment_metrics.has_distance) { + msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance); + } + if (decoded->variant.environment_metrics.has_wind_speed) { + msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); + } + if (decoded->variant.environment_metrics.has_wind_direction) { + msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); + } + if (decoded->variant.environment_metrics.has_wind_gust) { + msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); + } + if (decoded->variant.environment_metrics.has_wind_lull) { + msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); + } + if (decoded->variant.environment_metrics.has_radiation) { + msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); + } + if (decoded->variant.environment_metrics.has_ir_lux) { + msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux); + } + if (decoded->variant.environment_metrics.has_uv_lux) { + msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux); + } + if (decoded->variant.environment_metrics.has_weight) { + msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight); + } + if (decoded->variant.environment_metrics.has_rainfall_1h) { + msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h); + } + if (decoded->variant.environment_metrics.has_rainfall_24h) { + msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h); + } + if (decoded->variant.environment_metrics.has_soil_moisture) { + msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture); + } + if (decoded->variant.environment_metrics.has_soil_temperature) { + msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature); + } + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + } + } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + if (decoded->variant.power_metrics.has_ch1_voltage) { + msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); + } + if (decoded->variant.power_metrics.has_ch1_current) { + msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); + } + if (decoded->variant.power_metrics.has_ch2_current) { + msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); + } + if (decoded->variant.power_metrics.has_ch3_current) { + msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); + } } - case meshtastic_PortNum_TELEMETRY_APP: { - msgType = "telemetry"; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - // If battery is present, encode the battery level value - // TODO - Add a condition to send a code for a non-present value - if (decoded->variant.device_metrics.has_battery_level) { - msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); - } - msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); - msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); - msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); - msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); - } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - // Avoid sending 0s for sensors that could be 0 - if (decoded->variant.environment_metrics.has_temperature) { - msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); - } - if (decoded->variant.environment_metrics.has_relative_humidity) { - msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); - } - if (decoded->variant.environment_metrics.has_barometric_pressure) { - msgPayload["barometric_pressure"] = - new JSONValue(decoded->variant.environment_metrics.barometric_pressure); - } - if (decoded->variant.environment_metrics.has_gas_resistance) { - msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); - } - if (decoded->variant.environment_metrics.has_voltage) { - msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); - } - if (decoded->variant.environment_metrics.has_current) { - msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); - } - if (decoded->variant.environment_metrics.has_lux) { - msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); - } - if (decoded->variant.environment_metrics.has_white_lux) { - msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); - } - if (decoded->variant.environment_metrics.has_iaq) { - msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); - } - if (decoded->variant.environment_metrics.has_distance) { - msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance); - } - if (decoded->variant.environment_metrics.has_wind_speed) { - msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); - } - if (decoded->variant.environment_metrics.has_wind_direction) { - msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); - } - if (decoded->variant.environment_metrics.has_wind_gust) { - msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); - } - if (decoded->variant.environment_metrics.has_wind_lull) { - msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); - } - if (decoded->variant.environment_metrics.has_radiation) { - msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); - } - if (decoded->variant.environment_metrics.has_ir_lux) { - msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux); - } - if (decoded->variant.environment_metrics.has_uv_lux) { - msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux); - } - if (decoded->variant.environment_metrics.has_weight) { - msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight); - } - if (decoded->variant.environment_metrics.has_rainfall_1h) { - msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h); - } - if (decoded->variant.environment_metrics.has_rainfall_24h) { - msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h); - } - if (decoded->variant.environment_metrics.has_soil_moisture) { - msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture); - } - if (decoded->variant.environment_metrics.has_soil_temperature) { - msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature); - } - } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - if (decoded->variant.air_quality_metrics.has_pm10_standard) { - msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); - } - if (decoded->variant.air_quality_metrics.has_pm25_standard) { - msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); - } - if (decoded->variant.air_quality_metrics.has_pm100_standard) { - msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); - } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - msgPayload["pm10_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); - } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - msgPayload["pm25_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); - } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - msgPayload["pm100_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); - } - } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - if (decoded->variant.power_metrics.has_ch1_voltage) { - msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); - } - if (decoded->variant.power_metrics.has_ch1_current) { - msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); - } - if (decoded->variant.power_metrics.has_ch2_voltage) { - msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); - } - if (decoded->variant.power_metrics.has_ch2_current) { - msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); - } - if (decoded->variant.power_metrics.has_ch3_voltage) { - msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); - } - if (decoded->variant.power_metrics.has_ch3_current) { - msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); - } - } - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_NODEINFO_APP: { + msgType = "nodeinfo"; + meshtastic_User scratch; + meshtastic_User *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { + decoded = &scratch; + msgPayload["id"] = new JSONValue(decoded->id); + msgPayload["longname"] = new JSONValue(decoded->long_name); + msgPayload["shortname"] = new JSONValue(decoded->short_name); + msgPayload["hardware"] = new JSONValue(decoded->hw_model); + msgPayload["role"] = new JSONValue((int)decoded->role); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_POSITION_APP: { + msgType = "position"; + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + if ((int)decoded->time) { + msgPayload["time"] = new JSONValue((unsigned int)decoded->time); } - case meshtastic_PortNum_NODEINFO_APP: { - msgType = "nodeinfo"; - meshtastic_User scratch; - meshtastic_User *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { - decoded = &scratch; - msgPayload["id"] = new JSONValue(decoded->id); - msgPayload["longname"] = new JSONValue(decoded->long_name); - msgPayload["shortname"] = new JSONValue(decoded->short_name); - msgPayload["hardware"] = new JSONValue(decoded->hw_model); - msgPayload["role"] = new JSONValue((int)decoded->role); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; + if ((int)decoded->timestamp) { + msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp); } - case meshtastic_PortNum_POSITION_APP: { - msgType = "position"; - meshtastic_Position scratch; - meshtastic_Position *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { - decoded = &scratch; - if ((int)decoded->time) { - msgPayload["time"] = new JSONValue((unsigned int)decoded->time); - } - if ((int)decoded->timestamp) { - msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp); - } - msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); - msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); - if ((int)decoded->altitude) { - msgPayload["altitude"] = new JSONValue((int)decoded->altitude); - } - if ((int)decoded->ground_speed) { - msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed); - } - if (int(decoded->ground_track)) { - msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track); - } - if (int(decoded->sats_in_view)) { - msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view); - } - if ((int)decoded->PDOP) { - msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP); - } - if ((int)decoded->HDOP) { - msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP); - } - if ((int)decoded->VDOP) { - msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP); - } - if ((int)decoded->precision_bits) { - msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits); - } - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; + msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); + msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); + if ((int)decoded->altitude) { + msgPayload["altitude"] = new JSONValue((int)decoded->altitude); } - case meshtastic_PortNum_WAYPOINT_APP: { - msgType = "waypoint"; - meshtastic_Waypoint scratch; - meshtastic_Waypoint *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { - decoded = &scratch; - msgPayload["id"] = new JSONValue((unsigned int)decoded->id); - msgPayload["name"] = new JSONValue(decoded->name); - msgPayload["description"] = new JSONValue(decoded->description); - msgPayload["expire"] = new JSONValue((unsigned int)decoded->expire); - msgPayload["locked_to"] = new JSONValue((unsigned int)decoded->locked_to); - msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); - msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; + if ((int)decoded->ground_speed) { + msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed); } - case meshtastic_PortNum_NEIGHBORINFO_APP: { - msgType = "neighborinfo"; - meshtastic_NeighborInfo scratch; - meshtastic_NeighborInfo *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, - &scratch)) { - decoded = &scratch; - msgPayload["node_id"] = new JSONValue((unsigned int)decoded->node_id); - msgPayload["node_broadcast_interval_secs"] = new JSONValue((unsigned int)decoded->node_broadcast_interval_secs); - msgPayload["last_sent_by_id"] = new JSONValue((unsigned int)decoded->last_sent_by_id); - msgPayload["neighbors_count"] = new JSONValue(decoded->neighbors_count); - JSONArray neighbors; - for (uint8_t i = 0; i < decoded->neighbors_count; i++) { - JSONObject neighborObj; - neighborObj["node_id"] = new JSONValue((unsigned int)decoded->neighbors[i].node_id); - neighborObj["snr"] = new JSONValue((int)decoded->neighbors[i].snr); - neighbors.push_back(new JSONValue(neighborObj)); - } - msgPayload["neighbors"] = new JSONValue(neighbors); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; + if (int(decoded->ground_track)) { + msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track); } - case meshtastic_PortNum_TRACEROUTE_APP: { - if (mp->decoded.request_id) { // Only report the traceroute response - msgType = "traceroute"; - meshtastic_RouteDiscovery scratch; - meshtastic_RouteDiscovery *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, - &scratch)) { - decoded = &scratch; - JSONArray route; // Route this message took - JSONArray routeBack; // Route this message took back - JSONArray snrTowards; // Snr for forward route - JSONArray snrBack; // Snr for reverse route + if (int(decoded->sats_in_view)) { + msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view); + } + if ((int)decoded->PDOP) { + msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP); + } + if ((int)decoded->HDOP) { + msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP); + } + if ((int)decoded->VDOP) { + msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP); + } + if ((int)decoded->precision_bits) { + msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits); + } + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_WAYPOINT_APP: { + msgType = "waypoint"; + meshtastic_Waypoint scratch; + meshtastic_Waypoint *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + decoded = &scratch; + msgPayload["id"] = new JSONValue((unsigned int)decoded->id); + msgPayload["name"] = new JSONValue(decoded->name); + msgPayload["description"] = new JSONValue(decoded->description); + msgPayload["expire"] = new JSONValue((unsigned int)decoded->expire); + msgPayload["locked_to"] = new JSONValue((unsigned int)decoded->locked_to); + msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); + msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_NEIGHBORINFO_APP: { + msgType = "neighborinfo"; + meshtastic_NeighborInfo scratch; + meshtastic_NeighborInfo *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, &scratch)) { + decoded = &scratch; + msgPayload["node_id"] = new JSONValue((unsigned int)decoded->node_id); + msgPayload["node_broadcast_interval_secs"] = new JSONValue((unsigned int)decoded->node_broadcast_interval_secs); + msgPayload["last_sent_by_id"] = new JSONValue((unsigned int)decoded->last_sent_by_id); + msgPayload["neighbors_count"] = new JSONValue(decoded->neighbors_count); + JSONArray neighbors; + for (uint8_t i = 0; i < decoded->neighbors_count; i++) { + JSONObject neighborObj; + neighborObj["node_id"] = new JSONValue((unsigned int)decoded->neighbors[i].node_id); + neighborObj["snr"] = new JSONValue((int)decoded->neighbors[i].snr); + neighbors.push_back(new JSONValue(neighborObj)); + } + msgPayload["neighbors"] = new JSONValue(neighbors); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } + case meshtastic_PortNum_TRACEROUTE_APP: { + if (mp->decoded.request_id) { // Only report the traceroute response + msgType = "traceroute"; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, &scratch)) { + decoded = &scratch; + JSONArray route; // Route this message took + JSONArray routeBack; // Route this message took back + JSONArray snrTowards; // Snr for forward route + JSONArray snrBack; // Snr for reverse route - // Lambda function for adding a long name to the route - auto addToRoute = [](JSONArray *route, NodeNum num) { - char long_name[40] = "Unknown"; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); - bool name_known = node ? node->has_user : false; - if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); - route->push_back(new JSONValue(long_name)); - }; - addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) - for (uint8_t i = 0; i < decoded->route_count; i++) { - addToRoute(&route, decoded->route[i]); - } - addToRoute(&route, mp->from); // Ended at the original destination (source of response) + // Lambda function for adding a long name to the route + auto addToRoute = [](JSONArray *route, NodeNum num) { + char long_name[40] = "Unknown"; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); + bool name_known = node ? node->has_user : false; + if (name_known) + memcpy(long_name, node->user.long_name, sizeof(long_name)); + route->push_back(new JSONValue(long_name)); + }; + addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) + for (uint8_t i = 0; i < decoded->route_count; i++) { + addToRoute(&route, decoded->route[i]); + } + addToRoute(&route, mp->from); // Ended at the original destination (source of response) - addToRoute(&routeBack, mp->from); // Started at the original destination (source of response) - for (uint8_t i = 0; i < decoded->route_back_count; i++) { - addToRoute(&routeBack, decoded->route_back[i]); - } - addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response) + addToRoute(&routeBack, mp->from); // Started at the original destination (source of response) + for (uint8_t i = 0; i < decoded->route_back_count; i++) { + addToRoute(&routeBack, decoded->route_back[i]); + } + addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response) - for (uint8_t i = 0; i < decoded->snr_back_count; i++) { - snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4)); - } + for (uint8_t i = 0; i < decoded->snr_back_count; i++) { + snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4)); + } - for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { - snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4)); - } + for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { + snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4)); + } - msgPayload["route"] = new JSONValue(route); - msgPayload["route_back"] = new JSONValue(routeBack); - msgPayload["snr_back"] = new JSONValue(snrBack); - msgPayload["snr_towards"] = new JSONValue(snrTowards); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - } - break; - } - case meshtastic_PortNum_DETECTION_SENSOR_APP: { - msgType = "detection"; - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - msgPayload["text"] = new JSONValue(payloadStr); - jsonObj["payload"] = new JSONValue(msgPayload); - break; + msgPayload["route"] = new JSONValue(route); + msgPayload["route_back"] = new JSONValue(routeBack); + msgPayload["snr_back"] = new JSONValue(snrBack); + msgPayload["snr_towards"] = new JSONValue(snrTowards); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); } + } + break; + } + case meshtastic_PortNum_DETECTION_SENSOR_APP: { + msgType = "detection"; + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + msgPayload["text"] = new JSONValue(payloadStr); + jsonObj["payload"] = new JSONValue(msgPayload); + break; + } #ifdef ARCH_ESP32 - case meshtastic_PortNum_PAXCOUNTER_APP: { - msgType = "paxcounter"; - meshtastic_Paxcount scratch; - meshtastic_Paxcount *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) { - decoded = &scratch; - msgPayload["wifi_count"] = new JSONValue((unsigned int)decoded->wifi); - msgPayload["ble_count"] = new JSONValue((unsigned int)decoded->ble); - msgPayload["uptime"] = new JSONValue((unsigned int)decoded->uptime); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } + case meshtastic_PortNum_PAXCOUNTER_APP: { + msgType = "paxcounter"; + meshtastic_Paxcount scratch; + meshtastic_Paxcount *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) { + decoded = &scratch; + msgPayload["wifi_count"] = new JSONValue((unsigned int)decoded->wifi); + msgPayload["ble_count"] = new JSONValue((unsigned int)decoded->ble); + msgPayload["uptime"] = new JSONValue((unsigned int)decoded->uptime); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; + } #endif - case meshtastic_PortNum_REMOTE_HARDWARE_APP: { - meshtastic_HardwareMessage scratch; - meshtastic_HardwareMessage *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, - &scratch)) { - decoded = &scratch; - if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { - msgType = "gpios_changed"; - msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { - msgType = "gpios_read_reply"; - msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); - msgPayload["gpio_mask"] = new JSONValue((unsigned int)decoded->gpio_mask); - jsonObj["payload"] = new JSONValue(msgPayload); - } - } else if (shouldLog) { - LOG_ERROR(errStr, "RemoteHardware"); - } - break; + case meshtastic_PortNum_REMOTE_HARDWARE_APP: { + meshtastic_HardwareMessage scratch; + meshtastic_HardwareMessage *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, &scratch)) { + decoded = &scratch; + if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { + msgType = "gpios_changed"; + msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { + msgType = "gpios_read_reply"; + msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); + msgPayload["gpio_mask"] = new JSONValue((unsigned int)decoded->gpio_mask); + jsonObj["payload"] = new JSONValue(msgPayload); } - // add more packet types here if needed - default: - break; - } - } else if (shouldLog) { - LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); + } else if (shouldLog) { + LOG_ERROR(errStr, "RemoteHardware"); + } + break; } - - jsonObj["id"] = new JSONValue((unsigned int)mp->id); - jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); - jsonObj["to"] = new JSONValue((unsigned int)mp->to); - jsonObj["from"] = new JSONValue((unsigned int)mp->from); - jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); - jsonObj["type"] = new JSONValue(msgType.c_str()); - jsonObj["sender"] = new JSONValue(nodeDB->getNodeId().c_str()); - if (mp->rx_rssi != 0) - jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); - if (mp->rx_snr != 0) - jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); - jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); + // add more packet types here if needed + default: + break; } + } else if (shouldLog) { + LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); + } - // serialize and write it to the stream - JSONValue *value = new JSONValue(jsonObj); - std::string jsonStr = value->Stringify(); + jsonObj["id"] = new JSONValue((unsigned int)mp->id); + jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); + jsonObj["to"] = new JSONValue((unsigned int)mp->to); + jsonObj["from"] = new JSONValue((unsigned int)mp->from); + jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); + jsonObj["type"] = new JSONValue(msgType.c_str()); + jsonObj["sender"] = new JSONValue(nodeDB->getNodeId().c_str()); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); + if (mp->rx_snr != 0) + jsonObj["snr"] = new JSONValue((float)mp->rx_snr); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); + jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); + } - if (shouldLog) - LOG_INFO("serialized json message: %s", jsonStr.c_str()); + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObj); + std::string jsonStr = value->Stringify(); - delete value; - return jsonStr; + if (shouldLog) + LOG_INFO("serialized json message: %s", jsonStr.c_str()); + + delete value; + return jsonStr; } -std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) -{ - JSONObject jsonObj; +std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) { + JSONObject jsonObj; - jsonObj["id"] = new JSONValue((unsigned int)mp->id); - jsonObj["time_ms"] = new JSONValue((double)millis()); - jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); - jsonObj["to"] = new JSONValue((unsigned int)mp->to); - jsonObj["from"] = new JSONValue((unsigned int)mp->from); - jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); - jsonObj["want_ack"] = new JSONValue(mp->want_ack); + jsonObj["id"] = new JSONValue((unsigned int)mp->id); + jsonObj["time_ms"] = new JSONValue((double)millis()); + jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); + jsonObj["to"] = new JSONValue((unsigned int)mp->to); + jsonObj["from"] = new JSONValue((unsigned int)mp->from); + jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); + jsonObj["want_ack"] = new JSONValue(mp->want_ack); - if (mp->rx_rssi != 0) - jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); - if (mp->rx_snr != 0) - jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); - jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); - } - jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); - auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); - jsonObj["bytes"] = new JSONValue(encryptedStr.c_str()); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); + if (mp->rx_snr != 0) + jsonObj["snr"] = new JSONValue((float)mp->rx_snr); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); + jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); + } + jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); + auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); + jsonObj["bytes"] = new JSONValue(encryptedStr.c_str()); - // serialize and write it to the stream - JSONValue *value = new JSONValue(jsonObj); - std::string jsonStr = value->Stringify(); + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObj); + std::string jsonStr = value->Stringify(); - delete value; - return jsonStr; + delete value; + return jsonStr; } #endif \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer.h b/src/serialization/MeshPacketSerializer.h index 03860ab35..929b01873 100644 --- a/src/serialization/MeshPacketSerializer.h +++ b/src/serialization/MeshPacketSerializer.h @@ -3,21 +3,19 @@ static const char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; -class MeshPacketSerializer -{ - public: - static std::string JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog = true); - static std::string JsonSerializeEncrypted(const meshtastic_MeshPacket *mp); +class MeshPacketSerializer { +public: + static std::string JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog = true); + static std::string JsonSerializeEncrypted(const meshtastic_MeshPacket *mp); - private: - static std::string bytesToHex(const uint8_t *bytes, int len) - { - std::string result = ""; - for (int i = 0; i < len; ++i) { - char const byte = bytes[i]; - result += hexChars[(byte & 0xF0) >> 4]; - result += hexChars[(byte & 0x0F) >> 0]; - } - return result; +private: + static std::string bytesToHex(const uint8_t *bytes, int len) { + std::string result = ""; + for (int i = 0; i < len; ++i) { + char const byte = bytes[i]; + result += hexChars[(byte & 0xF0) >> 4]; + result += hexChars[(byte & 0x0F) >> 0]; } + return result; + } }; \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 41f505b94..5329a7b09 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -14,399 +14,394 @@ StaticJsonDocument<1024> jsonObj; StaticJsonDocument<1024> arrayObj; -std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) -{ - // the created jsonObj is immutable after creation, so - // we need to do the heavy lifting before assembling it. - std::string msgType; - jsonObj.clear(); - arrayObj.clear(); +std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) { + // the created jsonObj is immutable after creation, so + // we need to do the heavy lifting before assembling it. + std::string msgType; + jsonObj.clear(); + arrayObj.clear(); - if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - switch (mp->decoded.portnum) { - case meshtastic_PortNum_TEXT_MESSAGE_APP: { - msgType = "text"; - // convert bytes to string - if (shouldLog) - LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + switch (mp->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: { + msgType = "text"; + // convert bytes to string + if (shouldLog) + LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - // check if this is a JSON payload - StaticJsonDocument<512> text_doc; - DeserializationError error = deserializeJson(text_doc, payloadStr); - if (error) { - // if it isn't, then we need to create a json object - // with the string as the value - if (shouldLog) - LOG_INFO("text message payload is of type plaintext"); - jsonObj["payload"]["text"] = payloadStr; - } else { - // if it is, then we can just use the json object - if (shouldLog) - LOG_INFO("text message payload is of type json"); - jsonObj["payload"] = text_doc; - } - break; + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + // check if this is a JSON payload + StaticJsonDocument<512> text_doc; + DeserializationError error = deserializeJson(text_doc, payloadStr); + if (error) { + // if it isn't, then we need to create a json object + // with the string as the value + if (shouldLog) + LOG_INFO("text message payload is of type plaintext"); + jsonObj["payload"]["text"] = payloadStr; + } else { + // if it is, then we can just use the json object + if (shouldLog) + LOG_INFO("text message payload is of type json"); + jsonObj["payload"] = text_doc; + } + break; + } + case meshtastic_PortNum_TELEMETRY_APP: { + msgType = "telemetry"; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; + } + jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; + jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; + jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; + jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; + } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + if (decoded->variant.environment_metrics.has_temperature) { + jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; + } + if (decoded->variant.environment_metrics.has_voltage) { + jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; + } + if (decoded->variant.environment_metrics.has_current) { + jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; + } + if (decoded->variant.environment_metrics.has_lux) { + jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; + } + if (decoded->variant.environment_metrics.has_white_lux) { + jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; + } + if (decoded->variant.environment_metrics.has_iaq) { + jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; + } + if (decoded->variant.environment_metrics.has_wind_speed) { + jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; + } + if (decoded->variant.environment_metrics.has_wind_direction) { + jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; + } + if (decoded->variant.environment_metrics.has_wind_gust) { + jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; + } + if (decoded->variant.environment_metrics.has_wind_lull) { + jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; + } + if (decoded->variant.environment_metrics.has_radiation) { + jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; + } + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + } + } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + if (decoded->variant.power_metrics.has_ch1_voltage) { + jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; + } + if (decoded->variant.power_metrics.has_ch1_current) { + jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; + } + if (decoded->variant.power_metrics.has_ch2_current) { + jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; + } + if (decoded->variant.power_metrics.has_ch3_current) { + jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + } } - case meshtastic_PortNum_TELEMETRY_APP: { - msgType = "telemetry"; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - // If battery is present, encode the battery level value - // TODO - Add a condition to send a code for a non-present value - if (decoded->variant.device_metrics.has_battery_level) { - jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; - } - jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; - jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; - jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; - jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; - } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - if (decoded->variant.environment_metrics.has_temperature) { - jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; - } - if (decoded->variant.environment_metrics.has_relative_humidity) { - jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; - } - if (decoded->variant.environment_metrics.has_barometric_pressure) { - jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; - } - if (decoded->variant.environment_metrics.has_gas_resistance) { - jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; - } - if (decoded->variant.environment_metrics.has_voltage) { - jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; - } - if (decoded->variant.environment_metrics.has_current) { - jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; - } - if (decoded->variant.environment_metrics.has_lux) { - jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; - } - if (decoded->variant.environment_metrics.has_white_lux) { - jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; - } - if (decoded->variant.environment_metrics.has_iaq) { - jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; - } - if (decoded->variant.environment_metrics.has_wind_speed) { - jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; - } - if (decoded->variant.environment_metrics.has_wind_direction) { - jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; - } - if (decoded->variant.environment_metrics.has_wind_gust) { - jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; - } - if (decoded->variant.environment_metrics.has_wind_lull) { - jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; - } - if (decoded->variant.environment_metrics.has_radiation) { - jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; - } - } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - if (decoded->variant.air_quality_metrics.has_pm10_standard) { - jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; - } - if (decoded->variant.air_quality_metrics.has_pm25_standard) { - jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; - } - if (decoded->variant.air_quality_metrics.has_pm100_standard) { - jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; - } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; - } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; - } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; - } - } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - if (decoded->variant.power_metrics.has_ch1_voltage) { - jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; - } - if (decoded->variant.power_metrics.has_ch1_current) { - jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; - } - if (decoded->variant.power_metrics.has_ch2_voltage) { - jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; - } - if (decoded->variant.power_metrics.has_ch2_current) { - jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; - } - if (decoded->variant.power_metrics.has_ch3_voltage) { - jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; - } - if (decoded->variant.power_metrics.has_ch3_current) { - jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; - } - } - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for telemetry message!"); - return ""; - } - break; - } - case meshtastic_PortNum_NODEINFO_APP: { - msgType = "nodeinfo"; - meshtastic_User scratch; - meshtastic_User *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { - decoded = &scratch; - jsonObj["payload"]["id"] = decoded->id; - jsonObj["payload"]["longname"] = decoded->long_name; - jsonObj["payload"]["shortname"] = decoded->short_name; - jsonObj["payload"]["hardware"] = decoded->hw_model; - jsonObj["payload"]["role"] = (int)decoded->role; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for nodeinfo message!"); - return ""; - } - break; - } - case meshtastic_PortNum_POSITION_APP: { - msgType = "position"; - meshtastic_Position scratch; - meshtastic_Position *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { - decoded = &scratch; - if ((int)decoded->time) { - jsonObj["payload"]["time"] = (unsigned int)decoded->time; - } - if ((int)decoded->timestamp) { - jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp; - } - jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; - jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; - if ((int)decoded->altitude) { - jsonObj["payload"]["altitude"] = (int)decoded->altitude; - } - if ((int)decoded->ground_speed) { - jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed; - } - if (int(decoded->ground_track)) { - jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track; - } - if (int(decoded->sats_in_view)) { - jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view; - } - if ((int)decoded->PDOP) { - jsonObj["payload"]["PDOP"] = (int)decoded->PDOP; - } - if ((int)decoded->HDOP) { - jsonObj["payload"]["HDOP"] = (int)decoded->HDOP; - } - if ((int)decoded->VDOP) { - jsonObj["payload"]["VDOP"] = (int)decoded->VDOP; - } - if ((int)decoded->precision_bits) { - jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits; - } - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for position message!"); - return ""; - } - break; - } - case meshtastic_PortNum_WAYPOINT_APP: { - msgType = "position"; - meshtastic_Waypoint scratch; - meshtastic_Waypoint *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { - decoded = &scratch; - jsonObj["payload"]["id"] = (unsigned int)decoded->id; - jsonObj["payload"]["name"] = decoded->name; - jsonObj["payload"]["description"] = decoded->description; - jsonObj["payload"]["expire"] = (unsigned int)decoded->expire; - jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to; - jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; - jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for position message!"); - return ""; - } - break; - } - case meshtastic_PortNum_NEIGHBORINFO_APP: { - msgType = "neighborinfo"; - meshtastic_NeighborInfo scratch; - meshtastic_NeighborInfo *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, - &scratch)) { - decoded = &scratch; - jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id; - jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs; - jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id; - jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count; - - JsonObject neighbors_obj = arrayObj.to(); - JsonArray neighbors = neighbors_obj.createNestedArray("neighbors"); - JsonObject neighbors_0 = neighbors.createNestedObject(); - - for (uint8_t i = 0; i < decoded->neighbors_count; i++) { - neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id; - neighbors_0["snr"] = (int)decoded->neighbors[i].snr; - neighbors[i + 1] = neighbors_0; - neighbors_0.clear(); - } - neighbors.remove(0); - jsonObj["payload"]["neighbors"] = neighbors; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for neighborinfo message!"); - return ""; - } - break; - } - case meshtastic_PortNum_TRACEROUTE_APP: { - if (mp->decoded.request_id) { // Only report the traceroute response - msgType = "traceroute"; - meshtastic_RouteDiscovery scratch; - meshtastic_RouteDiscovery *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, - &scratch)) { - decoded = &scratch; - JsonArray route = arrayObj.createNestedArray("route"); - - auto addToRoute = [](JsonArray *route, NodeNum num) { - char long_name[40] = "Unknown"; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); - bool name_known = node ? node->has_user : false; - if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); - route->add(long_name); - }; - - addToRoute(&route, mp->to); // route.add(mp->to); - for (uint8_t i = 0; i < decoded->route_count; i++) { - addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]); - } - addToRoute(&route, - mp->from); // route.add(mp->from); // Ended at the original destination (source of response) - - jsonObj["payload"]["route"] = route; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for traceroute message!"); - return ""; - } - } else { - LOG_WARN("Traceroute response not reported"); - return ""; - } - break; - } - case meshtastic_PortNum_DETECTION_SENSOR_APP: { - msgType = "detection"; - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - jsonObj["payload"]["text"] = payloadStr; - break; - } - case meshtastic_PortNum_REMOTE_HARDWARE_APP: { - meshtastic_HardwareMessage scratch; - meshtastic_HardwareMessage *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, - &scratch)) { - decoded = &scratch; - if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { - msgType = "gpios_changed"; - jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; - } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { - msgType = "gpios_read_reply"; - jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; - jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask; - } - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for RemoteHardware message!"); - return ""; - } - break; - } - // add more packet types here if needed - default: - LOG_WARN("Unsupported packet type %d", mp->decoded.portnum); - return ""; - break; - } - } else if (shouldLog) { - LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for telemetry message!"); return ""; + } + break; } - - jsonObj["id"] = (unsigned int)mp->id; - jsonObj["timestamp"] = (unsigned int)mp->rx_time; - jsonObj["to"] = (unsigned int)mp->to; - jsonObj["from"] = (unsigned int)mp->from; - jsonObj["channel"] = (unsigned int)mp->channel; - jsonObj["type"] = msgType.c_str(); - jsonObj["sender"] = nodeDB->getNodeId().c_str(); - if (mp->rx_rssi != 0) - jsonObj["rssi"] = (int)mp->rx_rssi; - if (mp->rx_snr != 0) - jsonObj["snr"] = (float)mp->rx_snr; - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = (unsigned int)(hopsAway); - jsonObj["hop_start"] = (unsigned int)(mp->hop_start); + case meshtastic_PortNum_NODEINFO_APP: { + msgType = "nodeinfo"; + meshtastic_User scratch; + meshtastic_User *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { + decoded = &scratch; + jsonObj["payload"]["id"] = decoded->id; + jsonObj["payload"]["longname"] = decoded->long_name; + jsonObj["payload"]["shortname"] = decoded->short_name; + jsonObj["payload"]["hardware"] = decoded->hw_model; + jsonObj["payload"]["role"] = (int)decoded->role; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for nodeinfo message!"); + return ""; + } + break; } + case meshtastic_PortNum_POSITION_APP: { + msgType = "position"; + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + if ((int)decoded->time) { + jsonObj["payload"]["time"] = (unsigned int)decoded->time; + } + if ((int)decoded->timestamp) { + jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp; + } + jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; + jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; + if ((int)decoded->altitude) { + jsonObj["payload"]["altitude"] = (int)decoded->altitude; + } + if ((int)decoded->ground_speed) { + jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed; + } + if (int(decoded->ground_track)) { + jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track; + } + if (int(decoded->sats_in_view)) { + jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view; + } + if ((int)decoded->PDOP) { + jsonObj["payload"]["PDOP"] = (int)decoded->PDOP; + } + if ((int)decoded->HDOP) { + jsonObj["payload"]["HDOP"] = (int)decoded->HDOP; + } + if ((int)decoded->VDOP) { + jsonObj["payload"]["VDOP"] = (int)decoded->VDOP; + } + if ((int)decoded->precision_bits) { + jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits; + } + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for position message!"); + return ""; + } + break; + } + case meshtastic_PortNum_WAYPOINT_APP: { + msgType = "position"; + meshtastic_Waypoint scratch; + meshtastic_Waypoint *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + decoded = &scratch; + jsonObj["payload"]["id"] = (unsigned int)decoded->id; + jsonObj["payload"]["name"] = decoded->name; + jsonObj["payload"]["description"] = decoded->description; + jsonObj["payload"]["expire"] = (unsigned int)decoded->expire; + jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to; + jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; + jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for position message!"); + return ""; + } + break; + } + case meshtastic_PortNum_NEIGHBORINFO_APP: { + msgType = "neighborinfo"; + meshtastic_NeighborInfo scratch; + meshtastic_NeighborInfo *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, &scratch)) { + decoded = &scratch; + jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id; + jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs; + jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id; + jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count; - // serialize and write it to the stream + JsonObject neighbors_obj = arrayObj.to(); + JsonArray neighbors = neighbors_obj.createNestedArray("neighbors"); + JsonObject neighbors_0 = neighbors.createNestedObject(); - // Serial.printf("serialized json message: \r"); - // serializeJson(jsonObj, Serial); - // Serial.println(""); + for (uint8_t i = 0; i < decoded->neighbors_count; i++) { + neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id; + neighbors_0["snr"] = (int)decoded->neighbors[i].snr; + neighbors[i + 1] = neighbors_0; + neighbors_0.clear(); + } + neighbors.remove(0); + jsonObj["payload"]["neighbors"] = neighbors; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for neighborinfo message!"); + return ""; + } + break; + } + case meshtastic_PortNum_TRACEROUTE_APP: { + if (mp->decoded.request_id) { // Only report the traceroute response + msgType = "traceroute"; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, &scratch)) { + decoded = &scratch; + JsonArray route = arrayObj.createNestedArray("route"); - std::string jsonStr = ""; - serializeJson(jsonObj, jsonStr); + auto addToRoute = [](JsonArray *route, NodeNum num) { + char long_name[40] = "Unknown"; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); + bool name_known = node ? node->has_user : false; + if (name_known) + memcpy(long_name, node->user.long_name, sizeof(long_name)); + route->add(long_name); + }; - if (shouldLog) - LOG_INFO("serialized json message: %s", jsonStr.c_str()); + addToRoute(&route, mp->to); // route.add(mp->to); + for (uint8_t i = 0; i < decoded->route_count; i++) { + addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]); + } + addToRoute(&route, + mp->from); // route.add(mp->from); // Ended at the original destination (source of response) - return jsonStr; + jsonObj["payload"]["route"] = route; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for traceroute message!"); + return ""; + } + } else { + LOG_WARN("Traceroute response not reported"); + return ""; + } + break; + } + case meshtastic_PortNum_DETECTION_SENSOR_APP: { + msgType = "detection"; + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + jsonObj["payload"]["text"] = payloadStr; + break; + } + case meshtastic_PortNum_REMOTE_HARDWARE_APP: { + meshtastic_HardwareMessage scratch; + meshtastic_HardwareMessage *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, &scratch)) { + decoded = &scratch; + if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { + msgType = "gpios_changed"; + jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; + } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { + msgType = "gpios_read_reply"; + jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; + jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask; + } + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for RemoteHardware message!"); + return ""; + } + break; + } + // add more packet types here if needed + default: + LOG_WARN("Unsupported packet type %d", mp->decoded.portnum); + return ""; + break; + } + } else if (shouldLog) { + LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); + return ""; + } + + jsonObj["id"] = (unsigned int)mp->id; + jsonObj["timestamp"] = (unsigned int)mp->rx_time; + jsonObj["to"] = (unsigned int)mp->to; + jsonObj["from"] = (unsigned int)mp->from; + jsonObj["channel"] = (unsigned int)mp->channel; + jsonObj["type"] = msgType.c_str(); + jsonObj["sender"] = nodeDB->getNodeId().c_str(); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); + jsonObj["hop_start"] = (unsigned int)(mp->hop_start); + } + + // serialize and write it to the stream + + // Serial.printf("serialized json message: \r"); + // serializeJson(jsonObj, Serial); + // Serial.println(""); + + std::string jsonStr = ""; + serializeJson(jsonObj, jsonStr); + + if (shouldLog) + LOG_INFO("serialized json message: %s", jsonStr.c_str()); + + return jsonStr; } -std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) -{ - jsonObj.clear(); - jsonObj["id"] = (unsigned int)mp->id; - jsonObj["time_ms"] = (double)millis(); - jsonObj["timestamp"] = (unsigned int)mp->rx_time; - jsonObj["to"] = (unsigned int)mp->to; - jsonObj["from"] = (unsigned int)mp->from; - jsonObj["channel"] = (unsigned int)mp->channel; - jsonObj["want_ack"] = mp->want_ack; +std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) { + jsonObj.clear(); + jsonObj["id"] = (unsigned int)mp->id; + jsonObj["time_ms"] = (double)millis(); + jsonObj["timestamp"] = (unsigned int)mp->rx_time; + jsonObj["to"] = (unsigned int)mp->to; + jsonObj["from"] = (unsigned int)mp->from; + jsonObj["channel"] = (unsigned int)mp->channel; + jsonObj["want_ack"] = mp->want_ack; - if (mp->rx_rssi != 0) - jsonObj["rssi"] = (int)mp->rx_rssi; - if (mp->rx_snr != 0) - jsonObj["snr"] = (float)mp->rx_snr; - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = (unsigned int)(hopsAway); - jsonObj["hop_start"] = (unsigned int)(mp->hop_start); - } - jsonObj["size"] = (unsigned int)mp->encrypted.size; - auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); - jsonObj["bytes"] = encryptedStr.c_str(); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); + jsonObj["hop_start"] = (unsigned int)(mp->hop_start); + } + jsonObj["size"] = (unsigned int)mp->encrypted.size; + auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); + jsonObj["bytes"] = encryptedStr.c_str(); - // serialize and write it to the stream - std::string jsonStr = ""; - serializeJson(jsonObj, jsonStr); + // serialize and write it to the stream + std::string jsonStr = ""; + serializeJson(jsonObj, jsonStr); - return jsonStr; + return jsonStr; } #endif \ No newline at end of file diff --git a/src/serialization/cobs.cpp b/src/serialization/cobs.cpp index afb868f50..6344419fe 100644 --- a/src/serialization/cobs.cpp +++ b/src/serialization/cobs.cpp @@ -3,127 +3,125 @@ #ifdef SENSECAP_INDICATOR -cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) -{ +cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) { - cobs_encode_result result = {0, COBS_ENCODE_OK}; - - if (!dst_buf_ptr || !src_ptr) { - result.status = COBS_ENCODE_NULL_POINTER; - return result; - } - - const uint8_t *src_read_ptr = src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_code_write_ptr = dst_buf_ptr; - uint8_t *dst_write_ptr = dst_code_write_ptr + 1; - uint8_t search_len = 1; - - if (src_len != 0) { - for (;;) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); - break; - } - - uint8_t src_byte = *src_read_ptr++; - if (src_byte == 0) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - if (src_read_ptr >= src_end_ptr) { - break; - } - } else { - *dst_write_ptr++ = src_byte; - search_len++; - if (src_read_ptr >= src_end_ptr) { - break; - } - if (search_len == 0xFF) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - } - } - } - } - - if (dst_code_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); - dst_write_ptr = dst_buf_end_ptr; - } else { - *dst_code_write_ptr = search_len; - } - - result.out_len = dst_write_ptr - dst_buf_start_ptr; + cobs_encode_result result = {0, COBS_ENCODE_OK}; + if (!dst_buf_ptr || !src_ptr) { + result.status = COBS_ENCODE_NULL_POINTER; return result; + } + + const uint8_t *src_read_ptr = src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_code_write_ptr = dst_buf_ptr; + uint8_t *dst_write_ptr = dst_code_write_ptr + 1; + uint8_t search_len = 1; + + if (src_len != 0) { + for (;;) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); + break; + } + + uint8_t src_byte = *src_read_ptr++; + if (src_byte == 0) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + if (src_read_ptr >= src_end_ptr) { + break; + } + } else { + *dst_write_ptr++ = src_byte; + search_len++; + if (src_read_ptr >= src_end_ptr) { + break; + } + if (search_len == 0xFF) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + } + } + } + } + + if (dst_code_write_ptr >= dst_buf_end_ptr) { + result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); + dst_write_ptr = dst_buf_end_ptr; + } else { + *dst_code_write_ptr = search_len; + } + + result.out_len = dst_write_ptr - dst_buf_start_ptr; + + return result; } -cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) -{ - cobs_decode_result result = {0, COBS_DECODE_OK}; - - if (!dst_buf_ptr || !src_ptr) { - result.status = COBS_DECODE_NULL_POINTER; - return result; - } - - const uint8_t *src_read_ptr = src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = dst_buf_ptr; - const uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_write_ptr = dst_buf_ptr; - - if (src_len != 0) { - for (;;) { - uint8_t len_code = *src_read_ptr++; - if (len_code == 0) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); - break; - } - len_code--; - - size_t remaining_bytes = src_end_ptr - src_read_ptr; - if (len_code > remaining_bytes) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_INPUT_TOO_SHORT); - len_code = remaining_bytes; - } - - remaining_bytes = dst_buf_end_ptr - dst_write_ptr; - if (len_code > remaining_bytes) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); - len_code = remaining_bytes; - } - - for (uint8_t i = len_code; i != 0; i--) { - uint8_t src_byte = *src_read_ptr++; - if (src_byte == 0) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); - } - *dst_write_ptr++ = src_byte; - } - - if (src_read_ptr >= src_end_ptr) { - break; - } - - if (len_code != 0xFE) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); - break; - } - *dst_write_ptr++ = 0; - } - } - } - - result.out_len = dst_write_ptr - dst_buf_start_ptr; +cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) { + cobs_decode_result result = {0, COBS_DECODE_OK}; + if (!dst_buf_ptr || !src_ptr) { + result.status = COBS_DECODE_NULL_POINTER; return result; + } + + const uint8_t *src_read_ptr = src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = dst_buf_ptr; + const uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_write_ptr = dst_buf_ptr; + + if (src_len != 0) { + for (;;) { + uint8_t len_code = *src_read_ptr++; + if (len_code == 0) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); + break; + } + len_code--; + + size_t remaining_bytes = src_end_ptr - src_read_ptr; + if (len_code > remaining_bytes) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_INPUT_TOO_SHORT); + len_code = remaining_bytes; + } + + remaining_bytes = dst_buf_end_ptr - dst_write_ptr; + if (len_code > remaining_bytes) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); + len_code = remaining_bytes; + } + + for (uint8_t i = len_code; i != 0; i--) { + uint8_t src_byte = *src_read_ptr++; + if (src_byte == 0) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); + } + *dst_write_ptr++ = src_byte; + } + + if (src_read_ptr >= src_end_ptr) { + break; + } + + if (len_code != 0xFE) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); + break; + } + *dst_write_ptr++ = 0; + } + } + } + + result.out_len = dst_write_ptr - dst_buf_start_ptr; + + return result; } #endif \ No newline at end of file diff --git a/src/serialization/cobs.h b/src/serialization/cobs.h index f95e61f62..8d3370897 100644 --- a/src/serialization/cobs.h +++ b/src/serialization/cobs.h @@ -12,28 +12,24 @@ #define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN)-1u)) #define COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u) / 254u) -typedef enum { - COBS_ENCODE_OK = 0x00, - COBS_ENCODE_NULL_POINTER = 0x01, - COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 -} cobs_encode_status; +typedef enum { COBS_ENCODE_OK = 0x00, COBS_ENCODE_NULL_POINTER = 0x01, COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 } cobs_encode_status; typedef struct { - size_t out_len; - cobs_encode_status status; + size_t out_len; + cobs_encode_status status; } cobs_encode_result; typedef enum { - COBS_DECODE_OK = 0x00, - COBS_DECODE_NULL_POINTER = 0x01, - COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, - COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, - COBS_DECODE_INPUT_TOO_SHORT = 0x08 + COBS_DECODE_OK = 0x00, + COBS_DECODE_NULL_POINTER = 0x01, + COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, + COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, + COBS_DECODE_INPUT_TOO_SHORT = 0x08 } cobs_decode_status; typedef struct { - size_t out_len; - cobs_decode_status status; + size_t out_len; + cobs_decode_status status; } cobs_decode_result; #ifdef __cplusplus diff --git a/src/sleep.cpp b/src/sleep.cpp index 756582c74..9b6fa4873 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -76,294 +76,288 @@ RTC_DATA_ATTR int bootCount = 0; * We leave CPU at full speed during init, but once loop is called switch to low speed (for a 50% power savings) * */ -void setCPUFast(bool on) -{ +void setCPUFast(bool on) { #if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT - if (isWifiAvailable()) { - /* - * - * There's a newly introduced bug in the espressif framework where WiFi is - * unstable when the frequency is less than 240MHz. - * - * This mostly impacts WiFi AP mode but we'll bump the frequency for - * all WiFi use cases. - * (Added: Dec 23, 2021 by Jm Casler) - */ + if (isWifiAvailable()) { + /* + * + * There's a newly introduced bug in the espressif framework where WiFi is + * unstable when the frequency is less than 240MHz. + * + * This mostly impacts WiFi AP mode but we'll bump the frequency for + * all WiFi use cases. + * (Added: Dec 23, 2021 by Jm Casler) + */ #ifndef CONFIG_IDF_TARGET_ESP32C3 - LOG_DEBUG("Set CPU to 240MHz because WiFi is in use"); - setCpuFrequencyMhz(240); + LOG_DEBUG("Set CPU to 240MHz because WiFi is in use"); + setCpuFrequencyMhz(240); #endif - return; - } + return; + } // The Heltec LORA32 V1 runs at 26 MHz base frequency and doesn't react well to switching to 80 MHz... #if !defined(ARDUINO_HELTEC_WIFI_LORA_32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - setCpuFrequencyMhz(on ? 240 : 80); + setCpuFrequencyMhz(on ? 240 : 80); #endif #endif } // Perform power on init that we do on each wake from deep sleep -void initDeepSleep() -{ +void initDeepSleep() { #ifdef ARCH_ESP32 - bootCount++; - const char *reason; - wakeCause = esp_sleep_get_wakeup_cause(); + bootCount++; + const char *reason; + wakeCause = esp_sleep_get_wakeup_cause(); - switch (wakeCause) { - case ESP_SLEEP_WAKEUP_EXT0: - reason = "ext0 RTC_IO"; - break; - case ESP_SLEEP_WAKEUP_EXT1: - reason = "ext1 RTC_CNTL"; - break; - case ESP_SLEEP_WAKEUP_TIMER: - reason = "timer"; - break; - case ESP_SLEEP_WAKEUP_TOUCHPAD: - reason = "touchpad"; - break; - case ESP_SLEEP_WAKEUP_ULP: - reason = "ULP program"; - break; - default: - reason = "reset"; - break; - } - /* - Not using yet because we are using wake on all buttons being low + switch (wakeCause) { + case ESP_SLEEP_WAKEUP_EXT0: + reason = "ext0 RTC_IO"; + break; + case ESP_SLEEP_WAKEUP_EXT1: + reason = "ext1 RTC_CNTL"; + break; + case ESP_SLEEP_WAKEUP_TIMER: + reason = "timer"; + break; + case ESP_SLEEP_WAKEUP_TOUCHPAD: + reason = "touchpad"; + break; + case ESP_SLEEP_WAKEUP_ULP: + reason = "ULP program"; + break; + default: + reason = "reset"; + break; + } + /* + Not using yet because we are using wake on all buttons being low - wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these buttons is set it was the reason we woke - if (wakeCause == ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all buttons rule for waking' to - support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0]; - */ + wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these buttons is set it was the reason we woke + if (wakeCause == ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all buttons rule for waking' + to support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0]; + */ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - // If we booted because our timer ran out or the user pressed reset, send those as fake events - RESET_REASON hwReason = rtc_get_reset_reason(0); + // If we booted because our timer ran out or the user pressed reset, send those as fake events + RESET_REASON hwReason = rtc_get_reset_reason(0); - if (hwReason == RTCWDT_BROWN_OUT_RESET) - reason = "brownout"; + if (hwReason == RTCWDT_BROWN_OUT_RESET) + reason = "brownout"; - if (hwReason == TG0WDT_SYS_RESET) - reason = "taskWatchdog"; + if (hwReason == TG0WDT_SYS_RESET) + reason = "taskWatchdog"; - if (hwReason == TG1WDT_SYS_RESET) - reason = "intWatchdog"; + if (hwReason == TG1WDT_SYS_RESET) + reason = "intWatchdog"; - LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason); + LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason); #endif #if SOC_RTCIO_HOLD_SUPPORTED - // If waking from sleep, release any and all RTC GPIOs - if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { - LOG_DEBUG("Disable any holds on RTC IO pads"); - for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { - if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) - rtc_gpio_hold_dis((gpio_num_t)i); + // If waking from sleep, release any and all RTC GPIOs + if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { + LOG_DEBUG("Disable any holds on RTC IO pads"); + for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { + if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) + rtc_gpio_hold_dis((gpio_num_t)i); - // ESP32 (original) - else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) - gpio_hold_dis((gpio_num_t)i); - } + // ESP32 (original) + else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) + gpio_hold_dis((gpio_num_t)i); } + } #endif #endif } -bool doPreflightSleep() -{ - if (preflightSleep.notifyObservers(NULL) != 0) - return false; // vetoed - else - return true; +bool doPreflightSleep() { + if (preflightSleep.notifyObservers(NULL) != 0) + return false; // vetoed + else + return true; } /// Tell devices we are going to sleep and wait for them to handle things -static void waitEnterSleep(bool skipPreflight = false) -{ - if (!skipPreflight) { - uint32_t now = millis(); - while (!doPreflightSleep()) { - delay(100); // Kinda yucky - wait until radio says say we can shutdown (finished in process sends/receives) +static void waitEnterSleep(bool skipPreflight = false) { + if (!skipPreflight) { + uint32_t now = millis(); + while (!doPreflightSleep()) { + delay(100); // Kinda yucky - wait until radio says say we can shutdown (finished in process sends/receives) - if (!Throttle::isWithinTimespanMs(now, - THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT); - assert(0); // FIXME - for now we just restart, need to fix bug #167 - break; - } - } + if (!Throttle::isWithinTimespanMs(now, + THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT); + assert(0); // FIXME - for now we just restart, need to fix bug #167 + break; + } } + } - // Code that still needs to be moved into notifyObservers - console->flush(); // send all our characters before we stop cpu clock - setBluetoothEnable(false); // has to be off before calling light sleep + // Code that still needs to be moved into notifyObservers + console->flush(); // send all our characters before we stop cpu clock + setBluetoothEnable(false); // has to be off before calling light sleep } -void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false) -{ - if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { - LOG_INFO("Enter deep sleep forever"); - } else { - LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000); - } +void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false) { + if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { + LOG_INFO("Enter deep sleep forever"); + } else { + LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000); + } - // not using wifi yet, but once we are this is needed to shutoff the radio hw - // esp_wifi_stop(); - waitEnterSleep(skipPreflight); + // not using wifi yet, but once we are this is needed to shutoff the radio hw + // esp_wifi_stop(); + waitEnterSleep(skipPreflight); #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH - // Full shutdown of bluetooth hardware - if (nimbleBluetooth) - nimbleBluetooth->deinit(); + // Full shutdown of bluetooth hardware + if (nimbleBluetooth) + nimbleBluetooth->deinit(); #endif #ifdef ARCH_ESP32 - if (!shouldLoraWake(msecToWake)) - notifyDeepSleep.notifyObservers(NULL); -#else + if (!shouldLoraWake(msecToWake)) notifyDeepSleep.notifyObservers(NULL); +#else + notifyDeepSleep.notifyObservers(NULL); #endif - powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); - if (screen) - screen->doDeepSleep(); // datasheet says this will draw only 10ua + powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); + if (screen) + screen->doDeepSleep(); // datasheet says this will draw only 10ua - if (!skipSaveNodeDb) { - nodeDB->saveToDisk(); - } + if (!skipSaveNodeDb) { + nodeDB->saveToDisk(); + } #ifdef PIN_POWER_EN - digitalWrite(PIN_POWER_EN, LOW); - pinMode(PIN_POWER_EN, INPUT); // power off peripherals - // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); + digitalWrite(PIN_POWER_EN, LOW); + pinMode(PIN_POWER_EN, INPUT); // power off peripherals + // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); #endif #ifdef RAK_WISMESH_TAP_V2 - digitalWrite(SDCARD_CS, LOW); + digitalWrite(SDCARD_CS, LOW); #endif #ifdef TRACKER_T1000_E #ifdef GNSS_AIROHA - digitalWrite(GPS_VRTC_EN, LOW); - digitalWrite(PIN_GPS_RESET, LOW); - digitalWrite(GPS_SLEEP_INT, LOW); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, OUTPUT); - digitalWrite(GPS_RESETB_OUT, LOW); + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(PIN_GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB_OUT, OUTPUT); + digitalWrite(GPS_RESETB_OUT, LOW); #endif #ifdef BUZZER_EN_PIN - digitalWrite(BUZZER_EN_PIN, LOW); + digitalWrite(BUZZER_EN_PIN, LOW); #endif #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, LOW); + digitalWrite(PIN_3V3_EN, LOW); #endif #ifdef PIN_WD_EN - digitalWrite(PIN_WD_EN, LOW); + digitalWrite(PIN_WD_EN, LOW); #endif #endif - ledBlink.set(false); + ledBlink.set(false); #ifdef RESET_OLED - digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power + digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power #endif #if defined(VEXT_ENABLE) - digitalWrite(VEXT_ENABLE, !VEXT_ON_VALUE); // turn on the display power + digitalWrite(VEXT_ENABLE, !VEXT_ON_VALUE); // turn on the display power #endif #ifdef ARCH_ESP32 - if (shouldLoraWake(msecToWake)) { - enableLoraInterrupt(); - } + if (shouldLoraWake(msecToWake)) { + enableLoraInterrupt(); + } #ifdef BUTTON_PIN - // Avoid leakage through button pin - if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) { + // Avoid leakage through button pin + if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) { #ifdef BUTTON_NEED_PULLUP - pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(BUTTON_PIN, INPUT_PULLUP); #else - pinMode(BUTTON_PIN, INPUT); + pinMode(BUTTON_PIN, INPUT); #endif - gpio_hold_en((gpio_num_t)BUTTON_PIN); - } + gpio_hold_en((gpio_num_t)BUTTON_PIN); + } #endif #ifdef SENSECAP_INDICATOR - // Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO + // Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + gpio_hold_en((gpio_num_t)LORA_CS); +#elif defined(ELECROW_PANEL) + // Elecrow panels do not use LORA_CS, do nothing +#else + if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) { + // LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); gpio_hold_en((gpio_num_t)LORA_CS); -#elif defined(ELECROW_PANEL) - // Elecrow panels do not use LORA_CS, do nothing -#else - if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) { - // LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - gpio_hold_en((gpio_num_t)LORA_CS); - } + } #endif #endif #ifdef HAS_PPM - if (PPM) { - LOG_INFO("PMM shutdown"); - console->flush(); - PPM->shutdown(); - } + if (PPM) { + LOG_INFO("PMM shutdown"); + console->flush(); + PPM->shutdown(); + } #endif #ifdef HAS_PMU - if (pmu_found && PMU) { - // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. - // We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost - // wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is - // critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it) - // - // No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would - // leave floating input for the IRQ line - // If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting - // in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets - // all the time. - PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); + if (pmu_found && PMU) { + // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. + // We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost + // wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is + // critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it) + // + // No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would + // leave floating input for the IRQ line + // If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting + // in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets + // all the time. + PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); - uint8_t model = PMU->getChipModel(); - if (model == XPOWERS_AXP2101) { - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - // t-beam v1.2 radio power channel - PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || - HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { - PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel - } - } else if (model == XPOWERS_AXP192) { - // t-beam v1.1 radio power channel - PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel - } - if (msecToWake == portMAX_DELAY) { - LOG_INFO("PMU shutdown"); - console->flush(); - PMU->shutdown(); - } + uint8_t model = PMU->getChipModel(); + if (model == XPOWERS_AXP2101) { + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + // t-beam v1.2 radio power channel + PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { + PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel + } + } else if (model == XPOWERS_AXP192) { + // t-beam v1.1 radio power channel + PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel } + if (msecToWake == portMAX_DELAY) { + LOG_INFO("PMU shutdown"); + console->flush(); + PMU->shutdown(); + } + } #endif #if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA) - // Added by https://github.com/meshtastic/firmware/pull/4418 - // Possibly to support Heltec Capsule Sensor? - Wire.end(); - pinMode(I2C_SDA, ANALOG); - pinMode(I2C_SCL, ANALOG); + // Added by https://github.com/meshtastic/firmware/pull/4418 + // Possibly to support Heltec Capsule Sensor? + Wire.end(); + pinMode(I2C_SDA, ANALOG); + pinMode(I2C_SCL, ANALOG); #endif - console->flush(); - cpuDeepSleep(msecToWake); + console->flush(); + cpuDeepSleep(msecToWake); } #ifdef ARCH_ESP32 @@ -374,131 +368,130 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN */ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default { - // LOG_DEBUG("Enter light sleep"); + // LOG_DEBUG("Enter light sleep"); - // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not entering - // LightSleep. + // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not + // entering LightSleep. #if defined(SENSECAP_INDICATOR) - return ESP_SLEEP_WAKEUP_TIMER; + return ESP_SLEEP_WAKEUP_TIMER; #endif - waitEnterSleep(false); - notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here + waitEnterSleep(false); + notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here - uint64_t sleepUsec = sleepMsec * 1000LL; + uint64_t sleepUsec = sleepMsec * 1000LL; - // NOTE! ESP docs say we must disable bluetooth and wifi before light sleep + // NOTE! ESP docs say we must disable bluetooth and wifi before light sleep - // We want RTC peripherals to stay on - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + // We want RTC peripherals to stay on + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); #if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP) - gpio_pullup_en((gpio_num_t)BUTTON_PIN); + gpio_pullup_en((gpio_num_t)BUTTON_PIN); #endif #ifdef SERIAL0_RX_GPIO - // We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means - // someone started to send something + // We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means + // someone started to send something - // gpio 3 is RXD for serialport 0 on ESP32 - // Send a few Z characters to wake the port + // gpio 3 is RXD for serialport 0 on ESP32 + // Send a few Z characters to wake the port - // this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts) - // So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it - // never tries to go to sleep if the user is using the API - // gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL); + // this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts) + // So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it + // never tries to go to sleep if the user is using the API + // gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL); - // doesn't help - I think the USB-UART chip losing power is pulling the signal low - // gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO); + // doesn't help - I think the USB-UART chip losing power is pulling the signal low + // gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO); - // alas - can only work if using the refclock, which is limited to about 9600 bps - // assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK); - // assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK); + // alas - can only work if using the refclock, which is limited to about 9600 bps + // assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK); + // assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK); #endif #ifdef ROTARY_PRESS - // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup - gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL); + // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup + gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL); #endif #ifdef KB_INT - gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); + gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); #endif #ifdef BUTTON_PIN - gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); - gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); + gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); + gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); #endif #ifdef INPUTDRIVER_ENCODER_BTN - gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); + gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); #endif #if defined(WAKE_ON_TOUCH) - gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); + gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); #endif - enableLoraInterrupt(); + enableLoraInterrupt(); #ifdef PMU_IRQ - // wake due to PMU can happen repeatedly if there is no battery installed or the battery fills - if (pmu_found) - gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq + // wake due to PMU can happen repeatedly if there is no battery installed or the battery fills + if (pmu_found) + gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq #endif - auto res = esp_sleep_enable_gpio_wakeup(); - if (res != ESP_OK) { - LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); - } - assert(res == ESP_OK); - res = esp_sleep_enable_timer_wakeup(sleepUsec); - if (res != ESP_OK) { - LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res); - } - assert(res == ESP_OK); + auto res = esp_sleep_enable_gpio_wakeup(); + if (res != ESP_OK) { + LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); + } + assert(res == ESP_OK); + res = esp_sleep_enable_timer_wakeup(sleepUsec); + if (res != ESP_OK) { + LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res); + } + assert(res == ESP_OK); - console->flush(); - res = esp_light_sleep_start(); - if (res != ESP_OK) { - LOG_ERROR("esp_light_sleep_start result %d", res); - } - // commented out because it's not that crucial; - // if it sporadically happens the node will go into light sleep during the next round - // assert(res == ESP_OK); + console->flush(); + res = esp_light_sleep_start(); + if (res != ESP_OK) { + LOG_ERROR("esp_light_sleep_start result %d", res); + } + // commented out because it's not that crucial; + // if it sporadically happens the node will go into light sleep during the next round + // assert(res == ESP_OK); #ifdef ROTARY_PRESS - gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS); + gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS); #endif #ifdef KB_INT - gpio_wakeup_disable((gpio_num_t)KB_INT); + gpio_wakeup_disable((gpio_num_t)KB_INT); #endif #ifdef BUTTON_PIN - // Disable wake-on-button interrupt. Re-attach normal button-interrupts - gpio_wakeup_disable(pin); + // Disable wake-on-button interrupt. Re-attach normal button-interrupts + gpio_wakeup_disable(pin); #endif #if defined(INPUTDRIVER_ENCODER_BTN) - gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); + gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); #endif #if defined(WAKE_ON_TOUCH) - gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); + gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); #endif #if !defined(SOC_PM_SUPPORT_EXT_WAKEUP) && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - if (radioType != RF95_RADIO) { - gpio_wakeup_disable((gpio_num_t)LORA_DIO1); - } + if (radioType != RF95_RADIO) { + gpio_wakeup_disable((gpio_num_t)LORA_DIO1); + } #endif #if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) - if (radioType == RF95_RADIO) { - gpio_wakeup_disable((gpio_num_t)RF95_IRQ); - } + if (radioType == RF95_RADIO) { + gpio_wakeup_disable((gpio_num_t)RF95_IRQ); + } #endif - esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); - notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here + esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here #ifdef BUTTON_PIN - if (cause == ESP_SLEEP_WAKEUP_GPIO) { - LOG_INFO("Exit light sleep gpio: btn=%d", - !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - } else + if (cause == ESP_SLEEP_WAKEUP_GPIO) { + LOG_INFO("Exit light sleep gpio: btn=%d", !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + } else #endif - { - LOG_INFO("Exit light sleep cause: %d", cause); - } + { + LOG_INFO("Exit light sleep cause: %d", cause); + } - return cause; + return cause; } // not legal on the stock android ESP build @@ -510,73 +503,68 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r * * supposedly according to https://github.com/espressif/arduino-esp32/issues/475 this is already done in arduino */ -void enableModemSleep() -{ +void enableModemSleep() { #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - static esp_pm_config_t esp32_config; // filled with zeros because bss + static esp_pm_config_t esp32_config; // filled with zeros because bss #else - static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss + static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss #endif #if CONFIG_IDF_TARGET_ESP32S3 - esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ; #elif CONFIG_IDF_TARGET_ESP32S2 - esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ; #elif CONFIG_IDF_TARGET_ESP32C6 - esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ; #elif CONFIG_IDF_TARGET_ESP32C3 - esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ; #else - esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ; #endif - esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended - esp32_config.light_sleep_enable = false; - int rv = esp_pm_configure(&esp32_config); - LOG_DEBUG("Sleep request result %x", rv); + esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended + esp32_config.light_sleep_enable = false; + int rv = esp_pm_configure(&esp32_config); + LOG_DEBUG("Sleep request result %x", rv); } -bool shouldLoraWake(uint32_t msecToWake) -{ - return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER); -} +bool shouldLoraWake(uint32_t msecToWake) { return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER); } -void enableLoraInterrupt() -{ - esp_err_t res; +void enableLoraInterrupt() { + esp_err_t res; #if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); - if (res != ESP_OK) { - LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); - } + res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); + if (res != ESP_OK) { + LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); + } #if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC) - res = gpio_pullup_en((gpio_num_t)LORA_RESET); - if (res != ESP_OK) { - LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res); - } + res = gpio_pullup_en((gpio_num_t)LORA_RESET); + if (res != ESP_OK) { + LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res); + } #endif #if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) && !defined(ELECROW_PANEL) - gpio_pullup_en((gpio_num_t)LORA_CS); + gpio_pullup_en((gpio_num_t)LORA_CS); #endif #if defined(USE_GC1109_PA) - gpio_pullup_en((gpio_num_t)LORA_PA_POWER); - gpio_pullup_en((gpio_num_t)LORA_PA_EN); - gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); + gpio_pullup_en((gpio_num_t)LORA_PA_POWER); + gpio_pullup_en((gpio_num_t)LORA_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); #endif - LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); - gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); + LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); + gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); #elif defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - if (radioType != RF95_RADIO) { - LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); - gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); // SX126x/SX128x interrupt, active high - } + if (radioType != RF95_RADIO) { + LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); + gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); // SX126x/SX128x interrupt, active high + } #endif #if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) - if (radioType == RF95_RADIO) { - LOG_INFO("setup RF95_IRQ (GPIO%02d) with wakeup by gpio interrupt", RF95_IRQ); - gpio_wakeup_enable((gpio_num_t)RF95_IRQ, GPIO_INTR_HIGH_LEVEL); // RF95 interrupt, active high - } + if (radioType == RF95_RADIO) { + LOG_INFO("setup RF95_IRQ (GPIO%02d) with wakeup by gpio interrupt", RF95_IRQ); + gpio_wakeup_enable((gpio_num_t)RF95_IRQ, GPIO_INTR_HIGH_LEVEL); // RF95 interrupt, active high + } #endif } #endif diff --git a/src/xmodem.cpp b/src/xmodem.cpp index 1d8c77760..69f1f1963 100644 --- a/src/xmodem.cpp +++ b/src/xmodem.cpp @@ -2,14 +2,14 @@ * @file xmodem.cpp * @brief Implementation of XMODEM protocol for Meshtastic devices. * - * This file contains the implementation of the XMODEM protocol for Meshtastic devices. It is based on the XMODEM implementation - * by Georges Menie (www.menie.org) and has been adapted for protobuf encapsulation. + * This file contains the implementation of the XMODEM protocol for Meshtastic devices. It is based on the XMODEM + * implementation by Georges Menie (www.menie.org) and has been adapted for protobuf encapsulation. * - * The XMODEM protocol is used for reliable transmission of binary data over a serial connection. This implementation supports - * both sending and receiving of data. + * The XMODEM protocol is used for reliable transmission of binary data over a serial connection. This implementation + * supports both sending and receiving of data. * - * The XModemAdapter class provides the main functionality for the protocol, including CRC calculation, packet handling, and - * control signal sending. + * The XModemAdapter class provides the main functionality for the protocol, including CRC calculation, packet handling, + * and control signal sending. * * @copyright Copyright (c) 2001-2019 Georges Menie * @author @@ -64,20 +64,19 @@ XModemAdapter::XModemAdapter() {} * @param length The length of the buffer. * @return The calculated checksum. */ -unsigned short XModemAdapter::crc16_ccitt(const pb_byte_t *buffer, int length) -{ - unsigned short crc16 = 0; - while (length != 0) { - crc16 = (unsigned char)(crc16 >> 8) | (crc16 << 8); - crc16 ^= *buffer; - crc16 ^= (unsigned char)(crc16 & 0xff) >> 4; - crc16 ^= (crc16 << 8) << 4; - crc16 ^= ((crc16 & 0xff) << 4) << 1; - buffer++; - length--; - } +unsigned short XModemAdapter::crc16_ccitt(const pb_byte_t *buffer, int length) { + unsigned short crc16 = 0; + while (length != 0) { + crc16 = (unsigned char)(crc16 >> 8) | (crc16 << 8); + crc16 ^= *buffer; + crc16 ^= (unsigned char)(crc16 & 0xff) >> 4; + crc16 ^= (crc16 << 8) << 4; + crc16 ^= ((crc16 & 0xff) << 4) << 1; + buffer++; + length--; + } - return crc16; + return crc16; } /** @@ -89,190 +88,178 @@ unsigned short XModemAdapter::crc16_ccitt(const pb_byte_t *buffer, int length) * @param tcrc The expected checksum. * @return 1 if the checksums match, 0 otherwise. */ -int XModemAdapter::check(const pb_byte_t *buf, int sz, unsigned short tcrc) -{ - return crc16_ccitt(buf, sz) == tcrc; +int XModemAdapter::check(const pb_byte_t *buf, int sz, unsigned short tcrc) { return crc16_ccitt(buf, sz) == tcrc; } + +void XModemAdapter::sendControl(meshtastic_XModem_Control c) { + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = c; + LOG_DEBUG("XModem: Notify Send control %d", c); + packetReady.notifyObservers(packetno); } -void XModemAdapter::sendControl(meshtastic_XModem_Control c) -{ - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = c; - LOG_DEBUG("XModem: Notify Send control %d", c); - packetReady.notifyObservers(packetno); -} +meshtastic_XModem XModemAdapter::getForPhone() { return xmodemStore; } -meshtastic_XModem XModemAdapter::getForPhone() -{ - return xmodemStore; -} +void XModemAdapter::resetForPhone() { xmodemStore = meshtastic_XModem_init_zero; } -void XModemAdapter::resetForPhone() -{ - xmodemStore = meshtastic_XModem_init_zero; -} +void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket) { + switch (xmodemPacket.control) { + case meshtastic_XModem_Control_SOH: + case meshtastic_XModem_Control_STX: + if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) { + // NULL packet has the destination filename + memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); -void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket) -{ - switch (xmodemPacket.control) { - case meshtastic_XModem_Control_SOH: - case meshtastic_XModem_Control_STX: - if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) { - // NULL packet has the destination filename - memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); - - if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash - spiLock->lock(); - file = FSCom.open(filename, FILE_O_WRITE); - spiLock->unlock(); - if (file) { - sendControl(meshtastic_XModem_Control_ACK); - isReceiving = true; - packetno = 1; - break; - } - sendControl(meshtastic_XModem_Control_NAK); - isReceiving = false; - break; - } else { // Transmit this file from Flash - LOG_INFO("XModem: Transmit file %s", filename); - spiLock->lock(); - file = FSCom.open(filename, FILE_O_READ); - spiLock->unlock(); - if (file) { - packetno = 1; - isTransmitting = true; - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = meshtastic_XModem_Control_SOH; - xmodemStore.seq = packetno; - spiLock->lock(); - xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); - spiLock->unlock(); - xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); - LOG_DEBUG("XModem: STX Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); - if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { - isEOT = true; - // send EOT on next Ack - } - packetReady.notifyObservers(packetno); - break; - } - sendControl(meshtastic_XModem_Control_NAK); - isTransmitting = false; - break; - } - } else { - if (isReceiving) { - // normal file data packet - if ((xmodemPacket.seq == packetno) && - check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) { - // valid packet - spiLock->lock(); - file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); - spiLock->unlock(); - sendControl(meshtastic_XModem_Control_ACK); - packetno++; - break; - } - // invalid packet - sendControl(meshtastic_XModem_Control_NAK); - break; - } else if (isTransmitting) { - // just received something weird. - sendControl(meshtastic_XModem_Control_CAN); - isTransmitting = false; - break; - } - } - break; - case meshtastic_XModem_Control_EOT: - // End of transmission - sendControl(meshtastic_XModem_Control_ACK); + if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash spiLock->lock(); - file.flush(); - file.close(); + file = FSCom.open(filename, FILE_O_WRITE); spiLock->unlock(); + if (file) { + sendControl(meshtastic_XModem_Control_ACK); + isReceiving = true; + packetno = 1; + break; + } + sendControl(meshtastic_XModem_Control_NAK); isReceiving = false; break; - case meshtastic_XModem_Control_CAN: - // Cancel transmission and remove file - sendControl(meshtastic_XModem_Control_ACK); + } else { // Transmit this file from Flash + LOG_INFO("XModem: Transmit file %s", filename); spiLock->lock(); - file.flush(); - file.close(); - - FSCom.remove(filename); + file = FSCom.open(filename, FILE_O_READ); spiLock->unlock(); - isReceiving = false; - break; - case meshtastic_XModem_Control_ACK: - // Acknowledge Send the next packet - if (isTransmitting) { - if (isEOT) { - sendControl(meshtastic_XModem_Control_EOT); - spiLock->lock(); - file.close(); - spiLock->unlock(); - LOG_INFO("XModem: Finished send file %s", filename); - isTransmitting = false; - isEOT = false; - break; - } - retrans = MAXRETRANS; // reset retransmit counter - packetno++; - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = meshtastic_XModem_Control_SOH; - xmodemStore.seq = packetno; - spiLock->lock(); - xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); - spiLock->unlock(); - xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); - LOG_DEBUG("XModem: ACK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); - if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { - isEOT = true; - // send EOT on next Ack - } - packetReady.notifyObservers(packetno); - } else { - // just received something weird. - sendControl(meshtastic_XModem_Control_CAN); + if (file) { + packetno = 1; + isTransmitting = true; + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + spiLock->lock(); + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + spiLock->unlock(); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: STX Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); + if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { + isEOT = true; + // send EOT on next Ack + } + packetReady.notifyObservers(packetno); + break; } + sendControl(meshtastic_XModem_Control_NAK); + isTransmitting = false; break; - case meshtastic_XModem_Control_NAK: - // Negative acknowledge. Send the same buffer again - if (isTransmitting) { - if (--retrans <= 0) { - sendControl(meshtastic_XModem_Control_CAN); - spiLock->lock(); - file.close(); - spiLock->unlock(); - LOG_INFO("XModem: Retransmit timeout, cancel file %s", filename); - isTransmitting = false; - break; - } - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = meshtastic_XModem_Control_SOH; - xmodemStore.seq = packetno; - spiLock->lock(); - file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes)); - - xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); - spiLock->unlock(); - xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); - LOG_DEBUG("XModem: NAK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); - if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { - isEOT = true; - // send EOT on next Ack - } - packetReady.notifyObservers(packetno); - } else { - // just received something weird. - sendControl(meshtastic_XModem_Control_CAN); + } + } else { + if (isReceiving) { + // normal file data packet + if ((xmodemPacket.seq == packetno) && check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) { + // valid packet + spiLock->lock(); + file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); + spiLock->unlock(); + sendControl(meshtastic_XModem_Control_ACK); + packetno++; + break; } + // invalid packet + sendControl(meshtastic_XModem_Control_NAK); break; - default: - // Unknown control character + } else if (isTransmitting) { + // just received something weird. + sendControl(meshtastic_XModem_Control_CAN); + isTransmitting = false; break; + } } + break; + case meshtastic_XModem_Control_EOT: + // End of transmission + sendControl(meshtastic_XModem_Control_ACK); + spiLock->lock(); + file.flush(); + file.close(); + spiLock->unlock(); + isReceiving = false; + break; + case meshtastic_XModem_Control_CAN: + // Cancel transmission and remove file + sendControl(meshtastic_XModem_Control_ACK); + spiLock->lock(); + file.flush(); + file.close(); + + FSCom.remove(filename); + spiLock->unlock(); + isReceiving = false; + break; + case meshtastic_XModem_Control_ACK: + // Acknowledge Send the next packet + if (isTransmitting) { + if (isEOT) { + sendControl(meshtastic_XModem_Control_EOT); + spiLock->lock(); + file.close(); + spiLock->unlock(); + LOG_INFO("XModem: Finished send file %s", filename); + isTransmitting = false; + isEOT = false; + break; + } + retrans = MAXRETRANS; // reset retransmit counter + packetno++; + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + spiLock->lock(); + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + spiLock->unlock(); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: ACK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); + if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { + isEOT = true; + // send EOT on next Ack + } + packetReady.notifyObservers(packetno); + } else { + // just received something weird. + sendControl(meshtastic_XModem_Control_CAN); + } + break; + case meshtastic_XModem_Control_NAK: + // Negative acknowledge. Send the same buffer again + if (isTransmitting) { + if (--retrans <= 0) { + sendControl(meshtastic_XModem_Control_CAN); + spiLock->lock(); + file.close(); + spiLock->unlock(); + LOG_INFO("XModem: Retransmit timeout, cancel file %s", filename); + isTransmitting = false; + break; + } + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + spiLock->lock(); + file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes)); + + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + spiLock->unlock(); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: NAK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); + if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { + isEOT = true; + // send EOT on next Ack + } + packetReady.notifyObservers(packetno); + } else { + // just received something weird. + sendControl(meshtastic_XModem_Control_CAN); + } + break; + default: + // Unknown control character + break; + } } #endif diff --git a/src/xmodem.h b/src/xmodem.h index 4cfcb43e1..cc731caf7 100644 --- a/src/xmodem.h +++ b/src/xmodem.h @@ -40,40 +40,39 @@ #ifdef FSCom -class XModemAdapter -{ - public: - // Called when we put a fragment in the outgoing memory - Observable packetReady; +class XModemAdapter { +public: + // Called when we put a fragment in the outgoing memory + Observable packetReady; - XModemAdapter(); + XModemAdapter(); - void handlePacket(meshtastic_XModem xmodemPacket); - meshtastic_XModem getForPhone(); - void resetForPhone(); + void handlePacket(meshtastic_XModem xmodemPacket); + meshtastic_XModem getForPhone(); + void resetForPhone(); - private: - bool isReceiving = false; - bool isTransmitting = false; - bool isEOT = false; +private: + bool isReceiving = false; + bool isTransmitting = false; + bool isEOT = false; - int retrans = MAXRETRANS; + int retrans = MAXRETRANS; - uint16_t packetno = 0; + uint16_t packetno = 0; #if defined(ARCH_NRF52) || defined(ARCH_STM32WL) - File file = File(FSCom); + File file = File(FSCom); #else - File file; + File file; #endif - char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0}; + char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0}; - protected: - meshtastic_XModem xmodemStore = meshtastic_XModem_init_zero; - unsigned short crc16_ccitt(const pb_byte_t *buffer, int length); - int check(const pb_byte_t *buf, int sz, unsigned short tcrc); - void sendControl(meshtastic_XModem_Control c); +protected: + meshtastic_XModem xmodemStore = meshtastic_XModem_init_zero; + unsigned short crc16_ccitt(const pb_byte_t *buffer, int length); + int check(const pb_byte_t *buf, int sz, unsigned short tcrc); + void sendControl(meshtastic_XModem_Control c); }; extern XModemAdapter xModem; diff --git a/test/TestUtil.cpp b/test/TestUtil.cpp index b470b8ce8..2038a5c0f 100644 --- a/test/TestUtil.cpp +++ b/test/TestUtil.cpp @@ -4,15 +4,14 @@ #include "TestUtil.h" -void initializeTestEnvironment() -{ - concurrency::hasBeenSetup = true; - consoleInit(); +void initializeTestEnvironment() { + concurrency::hasBeenSetup = true; + consoleInit(); #if ARCH_PORTDUINO - struct timeval tv; - tv.tv_sec = time(NULL); - tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + struct timeval tv; + tv.tv_sec = time(NULL); + tv.tv_usec = 0; + perhapsSetRTC(RTCQualityNTP, &tv); #endif - concurrency::OSThread::setup(); + concurrency::OSThread::setup(); } \ No newline at end of file diff --git a/test/test_crypto/test_main.cpp b/test/test_crypto/test_main.cpp index 36dc37b9d..a57ce6a4a 100644 --- a/test/test_crypto/test_main.cpp +++ b/test/test_crypto/test_main.cpp @@ -4,195 +4,186 @@ #include "TestUtil.h" #include -void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0) -{ - if (len) { - memset(result, 0, len); - } - for (unsigned int i = 0; i < hex.length(); i += 2) { - std::string byteString = hex.substr(i, 2); - result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16); - } - return; +void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0) { + if (len) { + memset(result, 0, len); + } + for (unsigned int i = 0; i < hex.length(); i += 2) { + std::string byteString = hex.substr(i, 2); + result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16); + } + return; } -void setUp(void) -{ - // set stuff up here +void setUp(void) { + // set stuff up here } -void tearDown(void) -{ - // clean stuff up here +void tearDown(void) { + // clean stuff up here } -void test_SHA256(void) -{ - uint8_t expected[32]; - uint8_t hash[32] = {0}; +void test_SHA256(void) { + uint8_t expected[32]; + uint8_t hash[32] = {0}; - HexToBytes(expected, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); - crypto->hash(hash, 0); - TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); + HexToBytes(expected, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + crypto->hash(hash, 0); + TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); - HexToBytes(hash, "d3", 32); - HexToBytes(expected, "28969cdfa74a12c82f3bad960b0b000aca2ac329deea5c2328ebc6f2ba9802c1"); - crypto->hash(hash, 1); - TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); + HexToBytes(hash, "d3", 32); + HexToBytes(expected, "28969cdfa74a12c82f3bad960b0b000aca2ac329deea5c2328ebc6f2ba9802c1"); + crypto->hash(hash, 1); + TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); - HexToBytes(hash, "11af", 32); - HexToBytes(expected, "5ca7133fa735326081558ac312c620eeca9970d1e70a4b95533d956f072d1f98"); - crypto->hash(hash, 2); - TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); + HexToBytes(hash, "11af", 32); + HexToBytes(expected, "5ca7133fa735326081558ac312c620eeca9970d1e70a4b95533d956f072d1f98"); + crypto->hash(hash, 2); + TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); } -void test_ECB_AES256(void) -{ - // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf +void test_ECB_AES256(void) { + // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf - uint8_t key[32] = {0}; - uint8_t plain[16] = {0}; - uint8_t result[16] = {0}; - uint8_t expected[16] = {0}; + uint8_t key[32] = {0}; + uint8_t plain[16] = {0}; + uint8_t result[16] = {0}; + uint8_t expected[16] = {0}; - HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4"); + HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4"); - HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A"); - HexToBytes(expected, "F3EED1BDB5D2A03C064B5A7E3DB181F8"); - crypto->aesSetKey(key, 32); - crypto->aesEncrypt(plain, result); // Does 16 bytes at a time - TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); + HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A"); + HexToBytes(expected, "F3EED1BDB5D2A03C064B5A7E3DB181F8"); + crypto->aesSetKey(key, 32); + crypto->aesEncrypt(plain, result); // Does 16 bytes at a time + TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); - HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51"); - HexToBytes(expected, "591CCB10D410ED26DC5BA74A31362870"); - crypto->aesSetKey(key, 32); - crypto->aesEncrypt(plain, result); // Does 16 bytes at a time - TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); + HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51"); + HexToBytes(expected, "591CCB10D410ED26DC5BA74A31362870"); + crypto->aesSetKey(key, 32); + crypto->aesEncrypt(plain, result); // Does 16 bytes at a time + TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); - HexToBytes(plain, "30C81C46A35CE411E5FBC1191A0A52EF"); - HexToBytes(expected, "B6ED21B99CA6F4F9F153E7B1BEAFED1D"); - crypto->aesSetKey(key, 32); - crypto->aesEncrypt(plain, result); // Does 16 bytes at a time - TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); + HexToBytes(plain, "30C81C46A35CE411E5FBC1191A0A52EF"); + HexToBytes(expected, "B6ED21B99CA6F4F9F153E7B1BEAFED1D"); + crypto->aesSetKey(key, 32); + crypto->aesEncrypt(plain, result); // Does 16 bytes at a time + TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); } -void test_DH25519(void) -{ - // test vectors from wycheproof x25519 - // https://github.com/C2SP/wycheproof/blob/master/testvectors/x25519_test.json - uint8_t private_key[32]; - uint8_t public_key[32]; - uint8_t expected_shared[32]; +void test_DH25519(void) { + // test vectors from wycheproof x25519 + // https://github.com/C2SP/wycheproof/blob/master/testvectors/x25519_test.json + uint8_t private_key[32]; + uint8_t public_key[32]; + uint8_t expected_shared[32]; - HexToBytes(public_key, "504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829"); - HexToBytes(private_key, "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475"); - HexToBytes(expected_shared, "436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->setDHPublicKey(public_key)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); + HexToBytes(public_key, "504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829"); + HexToBytes(private_key, "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475"); + HexToBytes(expected_shared, "436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(crypto->setDHPublicKey(public_key)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); - HexToBytes(public_key, "63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733"); - HexToBytes(private_key, "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958"); - HexToBytes(expected_shared, "279df67a7c4611db4708a0e8282b195e5ac0ed6f4b2f292c6fbd0acac30d1332"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->setDHPublicKey(public_key)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); + HexToBytes(public_key, "63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733"); + HexToBytes(private_key, "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958"); + HexToBytes(expected_shared, "279df67a7c4611db4708a0e8282b195e5ac0ed6f4b2f292c6fbd0acac30d1332"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(crypto->setDHPublicKey(public_key)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); - HexToBytes(public_key, "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"); - HexToBytes(private_key, "18630f93598637c35da623a74559cf944374a559114c7937811041fc8605564a"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(!crypto->setDHPublicKey(public_key)); // Weak public key results in 0 shared key + HexToBytes(public_key, "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"); + HexToBytes(private_key, "18630f93598637c35da623a74559cf944374a559114c7937811041fc8605564a"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(!crypto->setDHPublicKey(public_key)); // Weak public key results in 0 shared key - HexToBytes(public_key, "f7e13a1a067d2f4e1061bf9936fde5be6b0c2494a8f809cbac7f290ef719e91c"); - HexToBytes(private_key, "10300724f3bea134eb1575245ef26ff9b8ccd59849cd98ce1a59002fe1d5986c"); - HexToBytes(expected_shared, "24becd5dfed9e9289ba2e15b82b0d54f8e9aacb72f5e4248c58d8d74b451ce76"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->setDHPublicKey(public_key)); - crypto->hash(crypto->shared_key, 32); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); + HexToBytes(public_key, "f7e13a1a067d2f4e1061bf9936fde5be6b0c2494a8f809cbac7f290ef719e91c"); + HexToBytes(private_key, "10300724f3bea134eb1575245ef26ff9b8ccd59849cd98ce1a59002fe1d5986c"); + HexToBytes(expected_shared, "24becd5dfed9e9289ba2e15b82b0d54f8e9aacb72f5e4248c58d8d74b451ce76"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(crypto->setDHPublicKey(public_key)); + crypto->hash(crypto->shared_key, 32); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); } -void test_PKC(void) -{ - uint8_t private_key[32]; - meshtastic_UserLite_public_key_t public_key; - uint8_t expected_shared[32]; - uint8_t expected_decrypted[32]; - uint8_t radioBytes[128] __attribute__((__aligned__)); - uint8_t decrypted[128] __attribute__((__aligned__)); - uint8_t expected_nonce[16]; +void test_PKC(void) { + uint8_t private_key[32]; + meshtastic_UserLite_public_key_t public_key; + uint8_t expected_shared[32]; + uint8_t expected_decrypted[32]; + uint8_t radioBytes[128] __attribute__((__aligned__)); + uint8_t decrypted[128] __attribute__((__aligned__)); + uint8_t expected_nonce[16]; - uint32_t fromNode = 0x0929; - uint64_t packetNum = 0x13b2d662; - HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457"); - public_key.size = 32; - HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277"); - HexToBytes(expected_shared, "777b1545c9d6f9a2"); - HexToBytes(expected_decrypted, "08011204746573744800"); - HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b"); - HexToBytes(expected_nonce, "62d6b213036a792b2909000000"); - crypto->setDHPrivateKey(private_key); + uint32_t fromNode = 0x0929; + uint64_t packetNum = 0x13b2d662; + HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457"); + public_key.size = 32; + HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277"); + HexToBytes(expected_shared, "777b1545c9d6f9a2"); + HexToBytes(expected_decrypted, "08011204746573744800"); + HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b"); + HexToBytes(expected_nonce, "62d6b213036a792b2909000000"); + crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); - TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); - TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); + TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); + TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); + TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); - uint32_t toNode = 0; // Only impacts logging - uint8_t encrypted[128] __attribute__((__aligned__)); - TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); - // The extraNonce is random, so skip checking the nonce and encrypted output here + uint32_t toNode = 0; // Only impacts logging + uint8_t encrypted[128] __attribute__((__aligned__)); + TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); + // The extraNonce is random, so skip checking the nonce and encrypted output here - // Copy the nonce to check it after encryption - memcpy(expected_nonce, crypto->nonce, 16); + // Copy the nonce to check it after encryption + memcpy(expected_nonce, crypto->nonce, 16); - // Decrypt the re-encrypted bytes and check they are the same as what we expect - TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); - TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); - TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); + // Decrypt the re-encrypted bytes and check they are the same as what we expect + TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); + TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); + TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); } -void test_AES_CTR(void) -{ - uint8_t expected[32]; - uint8_t plain[32]; - uint8_t nonce[32]; - CryptoKey k; +void test_AES_CTR(void) { + uint8_t expected[32]; + uint8_t plain[32]; + uint8_t nonce[32]; + CryptoKey k; - // vectors from https://www.rfc-editor.org/rfc/rfc3686#section-6 - k.length = 32; - HexToBytes(k.bytes, "776BEFF2851DB06F4C8A0542C8696F6C6A81AF1EEC96B4D37FC1D689E6C1C104"); - HexToBytes(nonce, "00000060DB5672C97AA8F0B200000001"); - HexToBytes(expected, "145AD01DBF824EC7560863DC71E3E0C0"); - memcpy(plain, "Single block msg", 16); + // vectors from https://www.rfc-editor.org/rfc/rfc3686#section-6 + k.length = 32; + HexToBytes(k.bytes, "776BEFF2851DB06F4C8A0542C8696F6C6A81AF1EEC96B4D37FC1D689E6C1C104"); + HexToBytes(nonce, "00000060DB5672C97AA8F0B200000001"); + HexToBytes(expected, "145AD01DBF824EC7560863DC71E3E0C0"); + memcpy(plain, "Single block msg", 16); - crypto->encryptAESCtr(k, nonce, 16, plain); - TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); + crypto->encryptAESCtr(k, nonce, 16, plain); + TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); - k.length = 16; - memcpy(plain, "Single block msg", 16); - HexToBytes(k.bytes, "AE6852F8121067CC4BF7A5765577F39E"); - HexToBytes(nonce, "00000030000000000000000000000001"); - HexToBytes(expected, "E4095D4FB7A7B3792D6175A3261311B8"); - crypto->encryptAESCtr(k, nonce, 16, plain); - TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); + k.length = 16; + memcpy(plain, "Single block msg", 16); + HexToBytes(k.bytes, "AE6852F8121067CC4BF7A5765577F39E"); + HexToBytes(nonce, "00000030000000000000000000000001"); + HexToBytes(expected, "E4095D4FB7A7B3792D6175A3261311B8"); + crypto->encryptAESCtr(k, nonce, 16, plain); + TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); } -void setup() -{ - // NOTE!!! Wait for >2 secs - // if board doesn't support software reset via Serial.DTR/RTS - delay(10); - delay(2000); +void setup() { + // NOTE!!! Wait for >2 secs + // if board doesn't support software reset via Serial.DTR/RTS + delay(10); + delay(2000); - initializeTestEnvironment(); - UNITY_BEGIN(); // IMPORTANT LINE! - RUN_TEST(test_SHA256); - RUN_TEST(test_ECB_AES256); - RUN_TEST(test_DH25519); - RUN_TEST(test_AES_CTR); - RUN_TEST(test_PKC); - exit(UNITY_END()); // stop unit testing + initializeTestEnvironment(); + UNITY_BEGIN(); // IMPORTANT LINE! + RUN_TEST(test_SHA256); + RUN_TEST(test_ECB_AES256); + RUN_TEST(test_DH25519); + RUN_TEST(test_AES_CTR); + RUN_TEST(test_PKC); + exit(UNITY_END()); // stop unit testing } void loop() {} \ No newline at end of file diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp index 37cfc1626..ef4571576 100644 --- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp +++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp @@ -1,59 +1,54 @@ #include "../test_helpers.h" // Helper function for all encrypted packet assertions -void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet) -{ - // Parse and validate JSON - TEST_ASSERT_TRUE(json.length() > 0); +void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet) { + // Parse and validate JSON + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Assert basic packet fields - TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber()); + // Assert basic packet fields + TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber()); - TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber()); + TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber()); - TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber()); + TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber()); - // Assert encrypted data fields - TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); + // Assert encrypted data fields + TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); - TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber()); + TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber()); - // Assert hex encoding - std::string encrypted_hex = jsonObj["bytes"]->AsString(); - TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length()); + // Assert hex encoding + std::string encrypted_hex = jsonObj["bytes"]->AsString(); + TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length()); - delete root; + delete root; } // Test encrypted packet serialization -void test_encrypted_packet_serialization() -{ - const char *data = "encrypted_payload_data"; - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data), - meshtastic_MeshPacket_encrypted_tag); - std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); +void test_encrypted_packet_serialization() { + const char *data = "encrypted_payload_data"; + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data), + meshtastic_MeshPacket_encrypted_tag); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); - assert_encrypted_packet(json, packet); + assert_encrypted_packet(json, packet); } // Test empty encrypted packet -void test_empty_encrypted_packet() -{ - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag); - std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); +void test_empty_encrypted_packet() { + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); - assert_encrypted_packet(json, packet); + assert_encrypted_packet(json, packet); } diff --git a/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp b/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp index febda9950..aee298169 100644 --- a/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp +++ b/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp @@ -1,51 +1,49 @@ #include "../test_helpers.h" -static size_t encode_user_info(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_User user = meshtastic_User_init_zero; - strcpy(user.short_name, "TEST"); - strcpy(user.long_name, "Test User"); - strcpy(user.id, "!12345678"); - user.hw_model = meshtastic_HardwareModel_HELTEC_V3; +static size_t encode_user_info(uint8_t *buffer, size_t buffer_size) { + meshtastic_User user = meshtastic_User_init_zero; + strcpy(user.short_name, "TEST"); + strcpy(user.long_name, "Test User"); + strcpy(user.id, "!12345678"); + user.hw_model = meshtastic_HardwareModel_HELTEC_V3; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_User_msg, &user); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_User_msg, &user); + return stream.bytes_written; } // Test NODEINFO_APP port -void test_nodeinfo_serialization() -{ - uint8_t buffer[256]; - size_t payload_size = encode_user_info(buffer, sizeof(buffer)); +void test_nodeinfo_serialization() { + uint8_t buffer[256]; + size_t payload_size = encode_user_info(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify user data - TEST_ASSERT_TRUE(payload.find("shortname") != payload.end()); - TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str()); + // Verify user data + TEST_ASSERT_TRUE(payload.find("shortname") != payload.end()); + TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str()); - TEST_ASSERT_TRUE(payload.find("longname") != payload.end()); - TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str()); + TEST_ASSERT_TRUE(payload.find("longname") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/ports/test_position.cpp b/test/test_meshpacket_serializer/ports/test_position.cpp index f0dcc0709..9c8284ccd 100644 --- a/test/test_meshpacket_serializer/ports/test_position.cpp +++ b/test/test_meshpacket_serializer/ports/test_position.cpp @@ -1,57 +1,55 @@ #include "../test_helpers.h" -static size_t encode_position(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_Position position = meshtastic_Position_init_zero; - position.latitude_i = 374208000; // 37.4208 degrees * 1e7 - position.longitude_i = -1221981000; // -122.1981 degrees * 1e7 - position.altitude = 123; - position.time = 1609459200; - position.has_altitude = true; - position.has_latitude_i = true; - position.has_longitude_i = true; +static size_t encode_position(uint8_t *buffer, size_t buffer_size) { + meshtastic_Position position = meshtastic_Position_init_zero; + position.latitude_i = 374208000; // 37.4208 degrees * 1e7 + position.longitude_i = -1221981000; // -122.1981 degrees * 1e7 + position.altitude = 123; + position.time = 1609459200; + position.has_altitude = true; + position.has_latitude_i = true; + position.has_longitude_i = true; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Position_msg, &position); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Position_msg, &position); + return stream.bytes_written; } // Test POSITION_APP port -void test_position_serialization() -{ - uint8_t buffer[256]; - size_t payload_size = encode_position(buffer, sizeof(buffer)); +void test_position_serialization() { + uint8_t buffer[256]; + size_t payload_size = encode_position(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify position data - TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end()); - TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber()); + // Verify position data + TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end()); + TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end()); - TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end()); + TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("altitude") != payload.end()); - TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("altitude") != payload.end()); + TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/ports/test_telemetry.cpp b/test/test_meshpacket_serializer/ports/test_telemetry.cpp index a813aaab5..382692591 100644 --- a/test/test_meshpacket_serializer/ports/test_telemetry.cpp +++ b/test/test_meshpacket_serializer/ports/test_telemetry.cpp @@ -1,528 +1,518 @@ #include "../test_helpers.h" // Helper function to create and encode device metrics -static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag; - telemetry.variant.device_metrics.battery_level = 85; - telemetry.variant.device_metrics.has_battery_level = true; - telemetry.variant.device_metrics.voltage = 3.72f; - telemetry.variant.device_metrics.has_voltage = true; - telemetry.variant.device_metrics.channel_utilization = 15.56f; - telemetry.variant.device_metrics.has_channel_utilization = true; - telemetry.variant.device_metrics.air_util_tx = 8.23f; - telemetry.variant.device_metrics.has_air_util_tx = true; - telemetry.variant.device_metrics.uptime_seconds = 12345; - telemetry.variant.device_metrics.has_uptime_seconds = true; +static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size) { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag; + telemetry.variant.device_metrics.battery_level = 85; + telemetry.variant.device_metrics.has_battery_level = true; + telemetry.variant.device_metrics.voltage = 3.72f; + telemetry.variant.device_metrics.has_voltage = true; + telemetry.variant.device_metrics.channel_utilization = 15.56f; + telemetry.variant.device_metrics.has_channel_utilization = true; + telemetry.variant.device_metrics.air_util_tx = 8.23f; + telemetry.variant.device_metrics.has_air_util_tx = true; + telemetry.variant.device_metrics.uptime_seconds = 12345; + telemetry.variant.device_metrics.has_uptime_seconds = true; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Helper function to create and encode empty environment metrics (no fields set) -static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; +static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size) { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; - // NO fields are set - all has_* flags remain false - // This tests that empty environment metrics don't produce any JSON fields + // NO fields are set - all has_* flags remain false + // This tests that empty environment metrics don't produce any JSON fields - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Helper function to create environment metrics with ALL possible fields set // This function should be updated whenever new fields are added to the protobuf -static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; +static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size) { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; - // Basic environment metrics - telemetry.variant.environment_metrics.temperature = 23.56f; - telemetry.variant.environment_metrics.has_temperature = true; - telemetry.variant.environment_metrics.relative_humidity = 65.43f; - telemetry.variant.environment_metrics.has_relative_humidity = true; - telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; - telemetry.variant.environment_metrics.has_barometric_pressure = true; + // Basic environment metrics + telemetry.variant.environment_metrics.temperature = 23.56f; + telemetry.variant.environment_metrics.has_temperature = true; + telemetry.variant.environment_metrics.relative_humidity = 65.43f; + telemetry.variant.environment_metrics.has_relative_humidity = true; + telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; + telemetry.variant.environment_metrics.has_barometric_pressure = true; - // Gas and air quality - telemetry.variant.environment_metrics.gas_resistance = 50.58f; - telemetry.variant.environment_metrics.has_gas_resistance = true; - telemetry.variant.environment_metrics.iaq = 120; - telemetry.variant.environment_metrics.has_iaq = true; + // Gas and air quality + telemetry.variant.environment_metrics.gas_resistance = 50.58f; + telemetry.variant.environment_metrics.has_gas_resistance = true; + telemetry.variant.environment_metrics.iaq = 120; + telemetry.variant.environment_metrics.has_iaq = true; - // Power measurements - telemetry.variant.environment_metrics.voltage = 3.34f; - telemetry.variant.environment_metrics.has_voltage = true; - telemetry.variant.environment_metrics.current = 0.53f; - telemetry.variant.environment_metrics.has_current = true; + // Power measurements + telemetry.variant.environment_metrics.voltage = 3.34f; + telemetry.variant.environment_metrics.has_voltage = true; + telemetry.variant.environment_metrics.current = 0.53f; + telemetry.variant.environment_metrics.has_current = true; - // Light measurements (ALL 4 types) - telemetry.variant.environment_metrics.lux = 450.12f; - telemetry.variant.environment_metrics.has_lux = true; - telemetry.variant.environment_metrics.white_lux = 380.95f; - telemetry.variant.environment_metrics.has_white_lux = true; - telemetry.variant.environment_metrics.ir_lux = 25.37f; - telemetry.variant.environment_metrics.has_ir_lux = true; - telemetry.variant.environment_metrics.uv_lux = 15.68f; - telemetry.variant.environment_metrics.has_uv_lux = true; + // Light measurements (ALL 4 types) + telemetry.variant.environment_metrics.lux = 450.12f; + telemetry.variant.environment_metrics.has_lux = true; + telemetry.variant.environment_metrics.white_lux = 380.95f; + telemetry.variant.environment_metrics.has_white_lux = true; + telemetry.variant.environment_metrics.ir_lux = 25.37f; + telemetry.variant.environment_metrics.has_ir_lux = true; + telemetry.variant.environment_metrics.uv_lux = 15.68f; + telemetry.variant.environment_metrics.has_uv_lux = true; - // Distance measurement - telemetry.variant.environment_metrics.distance = 150.29f; - telemetry.variant.environment_metrics.has_distance = true; + // Distance measurement + telemetry.variant.environment_metrics.distance = 150.29f; + telemetry.variant.environment_metrics.has_distance = true; - // Wind measurements (ALL 4 types) - telemetry.variant.environment_metrics.wind_direction = 180; - telemetry.variant.environment_metrics.has_wind_direction = true; - telemetry.variant.environment_metrics.wind_speed = 5.52f; - telemetry.variant.environment_metrics.has_wind_speed = true; - telemetry.variant.environment_metrics.wind_gust = 8.24f; - telemetry.variant.environment_metrics.has_wind_gust = true; - telemetry.variant.environment_metrics.wind_lull = 2.13f; - telemetry.variant.environment_metrics.has_wind_lull = true; + // Wind measurements (ALL 4 types) + telemetry.variant.environment_metrics.wind_direction = 180; + telemetry.variant.environment_metrics.has_wind_direction = true; + telemetry.variant.environment_metrics.wind_speed = 5.52f; + telemetry.variant.environment_metrics.has_wind_speed = true; + telemetry.variant.environment_metrics.wind_gust = 8.24f; + telemetry.variant.environment_metrics.has_wind_gust = true; + telemetry.variant.environment_metrics.wind_lull = 2.13f; + telemetry.variant.environment_metrics.has_wind_lull = true; - // Weight measurement - telemetry.variant.environment_metrics.weight = 75.56f; - telemetry.variant.environment_metrics.has_weight = true; + // Weight measurement + telemetry.variant.environment_metrics.weight = 75.56f; + telemetry.variant.environment_metrics.has_weight = true; - // Radiation measurement - telemetry.variant.environment_metrics.radiation = 0.13f; - telemetry.variant.environment_metrics.has_radiation = true; + // Radiation measurement + telemetry.variant.environment_metrics.radiation = 0.13f; + telemetry.variant.environment_metrics.has_radiation = true; - // Rainfall measurements (BOTH types) - telemetry.variant.environment_metrics.rainfall_1h = 2.57f; - telemetry.variant.environment_metrics.has_rainfall_1h = true; - telemetry.variant.environment_metrics.rainfall_24h = 15.89f; - telemetry.variant.environment_metrics.has_rainfall_24h = true; + // Rainfall measurements (BOTH types) + telemetry.variant.environment_metrics.rainfall_1h = 2.57f; + telemetry.variant.environment_metrics.has_rainfall_1h = true; + telemetry.variant.environment_metrics.rainfall_24h = 15.89f; + telemetry.variant.environment_metrics.has_rainfall_24h = true; - // Soil measurements (BOTH types) - telemetry.variant.environment_metrics.soil_moisture = 85; - telemetry.variant.environment_metrics.has_soil_moisture = true; - telemetry.variant.environment_metrics.soil_temperature = 18.54f; - telemetry.variant.environment_metrics.has_soil_temperature = true; + // Soil measurements (BOTH types) + telemetry.variant.environment_metrics.soil_moisture = 85; + telemetry.variant.environment_metrics.has_soil_moisture = true; + telemetry.variant.environment_metrics.soil_temperature = 18.54f; + telemetry.variant.environment_metrics.has_soil_temperature = true; - // IMPORTANT: When new environment fields are added to the protobuf, - // they MUST be added here too, or the coverage test will fail! + // IMPORTANT: When new environment fields are added to the protobuf, + // they MUST be added here too, or the coverage test will fail! - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Helper function to create and encode environment metrics with all current fields -static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; +static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size) { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; - // Basic environment metrics - telemetry.variant.environment_metrics.temperature = 23.56f; - telemetry.variant.environment_metrics.has_temperature = true; - telemetry.variant.environment_metrics.relative_humidity = 65.43f; - telemetry.variant.environment_metrics.has_relative_humidity = true; - telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; - telemetry.variant.environment_metrics.has_barometric_pressure = true; + // Basic environment metrics + telemetry.variant.environment_metrics.temperature = 23.56f; + telemetry.variant.environment_metrics.has_temperature = true; + telemetry.variant.environment_metrics.relative_humidity = 65.43f; + telemetry.variant.environment_metrics.has_relative_humidity = true; + telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; + telemetry.variant.environment_metrics.has_barometric_pressure = true; - // Gas and air quality - telemetry.variant.environment_metrics.gas_resistance = 50.58f; - telemetry.variant.environment_metrics.has_gas_resistance = true; - telemetry.variant.environment_metrics.iaq = 120; - telemetry.variant.environment_metrics.has_iaq = true; + // Gas and air quality + telemetry.variant.environment_metrics.gas_resistance = 50.58f; + telemetry.variant.environment_metrics.has_gas_resistance = true; + telemetry.variant.environment_metrics.iaq = 120; + telemetry.variant.environment_metrics.has_iaq = true; - // Power measurements - telemetry.variant.environment_metrics.voltage = 3.34f; - telemetry.variant.environment_metrics.has_voltage = true; - telemetry.variant.environment_metrics.current = 0.53f; - telemetry.variant.environment_metrics.has_current = true; + // Power measurements + telemetry.variant.environment_metrics.voltage = 3.34f; + telemetry.variant.environment_metrics.has_voltage = true; + telemetry.variant.environment_metrics.current = 0.53f; + telemetry.variant.environment_metrics.has_current = true; - // Light measurements - telemetry.variant.environment_metrics.lux = 450.12f; - telemetry.variant.environment_metrics.has_lux = true; - telemetry.variant.environment_metrics.white_lux = 380.95f; - telemetry.variant.environment_metrics.has_white_lux = true; - telemetry.variant.environment_metrics.ir_lux = 25.37f; - telemetry.variant.environment_metrics.has_ir_lux = true; - telemetry.variant.environment_metrics.uv_lux = 15.68f; - telemetry.variant.environment_metrics.has_uv_lux = true; + // Light measurements + telemetry.variant.environment_metrics.lux = 450.12f; + telemetry.variant.environment_metrics.has_lux = true; + telemetry.variant.environment_metrics.white_lux = 380.95f; + telemetry.variant.environment_metrics.has_white_lux = true; + telemetry.variant.environment_metrics.ir_lux = 25.37f; + telemetry.variant.environment_metrics.has_ir_lux = true; + telemetry.variant.environment_metrics.uv_lux = 15.68f; + telemetry.variant.environment_metrics.has_uv_lux = true; - // Distance measurement - telemetry.variant.environment_metrics.distance = 150.29f; - telemetry.variant.environment_metrics.has_distance = true; + // Distance measurement + telemetry.variant.environment_metrics.distance = 150.29f; + telemetry.variant.environment_metrics.has_distance = true; - // Wind measurements - telemetry.variant.environment_metrics.wind_direction = 180; - telemetry.variant.environment_metrics.has_wind_direction = true; - telemetry.variant.environment_metrics.wind_speed = 5.52f; - telemetry.variant.environment_metrics.has_wind_speed = true; - telemetry.variant.environment_metrics.wind_gust = 8.24f; - telemetry.variant.environment_metrics.has_wind_gust = true; - telemetry.variant.environment_metrics.wind_lull = 2.13f; - telemetry.variant.environment_metrics.has_wind_lull = true; + // Wind measurements + telemetry.variant.environment_metrics.wind_direction = 180; + telemetry.variant.environment_metrics.has_wind_direction = true; + telemetry.variant.environment_metrics.wind_speed = 5.52f; + telemetry.variant.environment_metrics.has_wind_speed = true; + telemetry.variant.environment_metrics.wind_gust = 8.24f; + telemetry.variant.environment_metrics.has_wind_gust = true; + telemetry.variant.environment_metrics.wind_lull = 2.13f; + telemetry.variant.environment_metrics.has_wind_lull = true; - // Weight measurement - telemetry.variant.environment_metrics.weight = 75.56f; - telemetry.variant.environment_metrics.has_weight = true; + // Weight measurement + telemetry.variant.environment_metrics.weight = 75.56f; + telemetry.variant.environment_metrics.has_weight = true; - // Radiation measurement - telemetry.variant.environment_metrics.radiation = 0.13f; - telemetry.variant.environment_metrics.has_radiation = true; + // Radiation measurement + telemetry.variant.environment_metrics.radiation = 0.13f; + telemetry.variant.environment_metrics.has_radiation = true; - // Rainfall measurements - telemetry.variant.environment_metrics.rainfall_1h = 2.57f; - telemetry.variant.environment_metrics.has_rainfall_1h = true; - telemetry.variant.environment_metrics.rainfall_24h = 15.89f; - telemetry.variant.environment_metrics.has_rainfall_24h = true; + // Rainfall measurements + telemetry.variant.environment_metrics.rainfall_1h = 2.57f; + telemetry.variant.environment_metrics.has_rainfall_1h = true; + telemetry.variant.environment_metrics.rainfall_24h = 15.89f; + telemetry.variant.environment_metrics.has_rainfall_24h = true; - // Soil measurements - telemetry.variant.environment_metrics.soil_moisture = 85; - telemetry.variant.environment_metrics.has_soil_moisture = true; - telemetry.variant.environment_metrics.soil_temperature = 18.54f; - telemetry.variant.environment_metrics.has_soil_temperature = true; + // Soil measurements + telemetry.variant.environment_metrics.soil_moisture = 85; + telemetry.variant.environment_metrics.has_soil_moisture = true; + telemetry.variant.environment_metrics.soil_temperature = 18.54f; + telemetry.variant.environment_metrics.has_soil_temperature = true; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Test TELEMETRY_APP port with device metrics -void test_telemetry_device_metrics_serialization() -{ - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer)); +void test_telemetry_device_metrics_serialization() { + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify telemetry data - TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end()); - TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber()); + // Verify telemetry data + TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end()); - TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end()); + TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber()); - // Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision - // We verify the numeric values are correct within tolerance + // Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision + // We verify the numeric values are correct within tolerance - delete root; + delete root; } // Test that telemetry environment metrics are properly serialized -void test_telemetry_environment_metrics_serialization() -{ - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_serialization() { + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Test key fields that should be present in the serializer - TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); + // Test key fields that should be present in the serializer + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); - // Note: JSON serialization may have float precision limitations - // We focus on verifying numeric accuracy rather than exact string formatting + // Note: JSON serialization may have float precision limitations + // We focus on verifying numeric accuracy rather than exact string formatting - delete root; + delete root; } // Test comprehensive environment metrics coverage -void test_telemetry_environment_metrics_comprehensive() -{ - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_comprehensive() { + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Check all 15 originally supported fields - TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); - TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); - TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); - TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); - TEST_ASSERT_TRUE(payload.find("current") != payload.end()); - TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); - TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); - TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); - TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); - TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); + // Check all 15 originally supported fields + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); + TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_TRUE(payload.find("current") != payload.end()); + TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); + TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); + TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); - delete root; + delete root; } // Test for the 7 environment fields that were added to complete coverage -void test_telemetry_environment_metrics_missing_fields() -{ - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_missing_fields() { + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Check the 7 fields that were previously missing - TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); + // Check the 7 fields that were previously missing + TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); - TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); - // Note: JSON float serialization may not preserve exact decimal formatting - // We verify the values are numerically correct within tolerance + // Note: JSON float serialization may not preserve exact decimal formatting + // We verify the values are numerically correct within tolerance - delete root; + delete root; } // Test that ALL environment fields are serialized (canary test for forgotten fields) // This test will FAIL if a new environment field is added to the protobuf but not to the serializer -void test_telemetry_environment_metrics_complete_coverage() -{ - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_complete_coverage() { + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // ✅ ALL 22 environment fields MUST be present and correct - // If this test fails, it means either: - // 1. A new field was added to the protobuf but not to the serializer - // 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated + // ✅ ALL 22 environment fields MUST be present and correct + // If this test fails, it means either: + // 1. A new field was added to the protobuf but not to the serializer + // 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated - // Basic environment (3 fields) - TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber()); + // Basic environment (3 fields) + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber()); - // Gas and air quality (2 fields) - TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); - TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber()); + // Gas and air quality (2 fields) + TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); + TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber()); - // Power measurements (2 fields) - TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("current") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber()); + // Power measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("current") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber()); - // Light measurements (4 fields) - TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); + // Light measurements (4 fields) + TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); - // Distance measurement (1 field) - TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); + // Distance measurement (1 field) + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); - // Wind measurements (4 fields) - TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); - TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber()); + // Wind measurements (4 fields) + TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); + TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber()); - // Weight measurement (1 field) - TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); + // Weight measurement (1 field) + TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); - // Radiation measurement (1 field) - TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber()); + // Radiation measurement (1 field) + TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber()); - // Rainfall measurements (2 fields) - TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); + // Rainfall measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); - // Soil measurements (2 fields) - TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); - TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); + // Soil measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); - // Total: 22 environment fields - // This test ensures 100% coverage of environment metrics + // Total: 22 environment fields + // This test ensures 100% coverage of environment metrics - // Note: JSON float serialization precision may vary due to the underlying library - // The important aspect is that all values are numerically accurate within tolerance + // Note: JSON float serialization precision may vary due to the underlying library + // The important aspect is that all values are numerically accurate within tolerance - delete root; + delete root; } // Test that unset environment fields are not present in JSON -void test_telemetry_environment_metrics_unset_fields() -{ - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_unset_fields() { + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // With completely empty environment metrics, NO fields should be present - // Only basic telemetry fields like "time" might be present + // With completely empty environment metrics, NO fields should be present + // Only basic telemetry fields like "time" might be present - // All 22 environment fields should be absent (none were set) - TEST_ASSERT_TRUE(payload.find("temperature") == payload.end()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end()); - TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end()); - TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end()); - TEST_ASSERT_TRUE(payload.find("iaq") == payload.end()); - TEST_ASSERT_TRUE(payload.find("voltage") == payload.end()); - TEST_ASSERT_TRUE(payload.find("current") == payload.end()); - TEST_ASSERT_TRUE(payload.find("lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("distance") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end()); - TEST_ASSERT_TRUE(payload.find("weight") == payload.end()); - TEST_ASSERT_TRUE(payload.find("radiation") == payload.end()); - TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end()); - TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end()); - TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end()); - TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end()); + // All 22 environment fields should be absent (none were set) + TEST_ASSERT_TRUE(payload.find("temperature") == payload.end()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end()); + TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end()); + TEST_ASSERT_TRUE(payload.find("iaq") == payload.end()); + TEST_ASSERT_TRUE(payload.find("voltage") == payload.end()); + TEST_ASSERT_TRUE(payload.find("current") == payload.end()); + TEST_ASSERT_TRUE(payload.find("lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("distance") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end()); + TEST_ASSERT_TRUE(payload.find("weight") == payload.end()); + TEST_ASSERT_TRUE(payload.find("radiation") == payload.end()); + TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end()); + TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp index 0f3b0bc6d..e3f0c1cc7 100644 --- a/test/test_meshpacket_serializer/ports/test_text_message.cpp +++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp @@ -2,104 +2,97 @@ #include // Helper function to test common packet fields and structure -void verify_text_message_packet_structure(const std::string &json, const char *expected_text) -{ - TEST_ASSERT_TRUE(json.length() > 0); +void verify_text_message_packet_structure(const std::string &json, const char *expected_text) { + TEST_ASSERT_TRUE(json.length() > 0); - // Use smart pointer for automatic memory management - std::unique_ptr root(JSON::Parse(json.c_str())); - TEST_ASSERT_NOT_NULL(root.get()); - TEST_ASSERT_TRUE(root->IsObject()); + // Use smart pointer for automatic memory management + std::unique_ptr root(JSON::Parse(json.c_str())); + TEST_ASSERT_NOT_NULL(root.get()); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check basic packet fields - use helper function to reduce duplication - auto check_field = [&](const char *field, uint32_t expected_value) { - auto it = jsonObj.find(field); - TEST_ASSERT_TRUE(it != jsonObj.end()); - TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); - }; + // Check basic packet fields - use helper function to reduce duplication + auto check_field = [&](const char *field, uint32_t expected_value) { + auto it = jsonObj.find(field); + TEST_ASSERT_TRUE(it != jsonObj.end()); + TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); + }; - check_field("from", 0x11223344); - check_field("to", 0x55667788); - check_field("id", 0x9999); + check_field("from", 0x11223344); + check_field("to", 0x55667788); + check_field("id", 0x9999); - // Check message type - auto type_it = jsonObj.find("type"); - TEST_ASSERT_TRUE(type_it != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); + // Check message type + auto type_it = jsonObj.find("type"); + TEST_ASSERT_TRUE(type_it != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); - // Check payload - auto payload_it = jsonObj.find("payload"); - TEST_ASSERT_TRUE(payload_it != jsonObj.end()); - TEST_ASSERT_TRUE(payload_it->second->IsObject()); + // Check payload + auto payload_it = jsonObj.find("payload"); + TEST_ASSERT_TRUE(payload_it != jsonObj.end()); + TEST_ASSERT_TRUE(payload_it->second->IsObject()); - JSONObject payload = payload_it->second->AsObject(); - auto text_it = payload.find("text"); - TEST_ASSERT_TRUE(text_it != payload.end()); - TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); + JSONObject payload = payload_it->second->AsObject(); + auto text_it = payload.find("text"); + TEST_ASSERT_TRUE(text_it != payload.end()); + TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); - // No need for manual delete with smart pointer + // No need for manual delete with smart pointer } // Test TEXT_MESSAGE_APP port -void test_text_message_serialization() -{ - const char *test_text = "Hello Meshtastic!"; - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); +void test_text_message_serialization() { + const char *test_text = "Hello Meshtastic!"; + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - verify_text_message_packet_structure(json, test_text); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, test_text); } // Test with nullptr to check robustness -void test_text_message_serialization_null() -{ - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); +void test_text_message_serialization_null() { + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - verify_text_message_packet_structure(json, ""); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, ""); } // Test TEXT_MESSAGE_APP port with very long message (boundary testing) -void test_text_message_serialization_long_text() -{ - // Test with actual message size limits - constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit - std::string long_text(MAX_MESSAGE_SIZE, 'A'); +void test_text_message_serialization_long_text() { + // Test with actual message size limits + constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit + std::string long_text(MAX_MESSAGE_SIZE, 'A'); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, - reinterpret_cast(long_text.c_str()), long_text.length()); + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(long_text.c_str()), long_text.length()); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - verify_text_message_packet_structure(json, long_text.c_str()); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, long_text.c_str()); } // Test with message over size limit (should fail) -void test_text_message_serialization_oversized() -{ - constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit - std::string oversized_text(OVERSIZED_MESSAGE, 'B'); +void test_text_message_serialization_oversized() { + constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit + std::string oversized_text(OVERSIZED_MESSAGE, 'B'); - meshtastic_MeshPacket packet = create_test_packet( - meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); - // Should fail or return empty/error - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - // Should only verify first 234 characters for oversized messages - std::string expected_text = oversized_text.substr(0, 234); - verify_text_message_packet_structure(json, expected_text.c_str()); + // Should fail or return empty/error + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + // Should only verify first 234 characters for oversized messages + std::string expected_text = oversized_text.substr(0, 234); + verify_text_message_packet_structure(json, expected_text.c_str()); } // Add test for malformed UTF-8 sequences -void test_text_message_serialization_invalid_utf8() -{ - const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); +void test_text_message_serialization_invalid_utf8() { + const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); - // Should not crash, may produce replacement characters - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + // Should not crash, may produce replacement characters + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); } \ No newline at end of file diff --git a/test/test_meshpacket_serializer/ports/test_waypoint.cpp b/test/test_meshpacket_serializer/ports/test_waypoint.cpp index b7e811d70..d2a549fb2 100644 --- a/test/test_meshpacket_serializer/ports/test_waypoint.cpp +++ b/test/test_meshpacket_serializer/ports/test_waypoint.cpp @@ -1,53 +1,51 @@ #include "../test_helpers.h" -static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size) -{ - meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero; - waypoint.id = 12345; - waypoint.latitude_i = 374208000; - waypoint.longitude_i = -1221981000; - waypoint.expire = 1609459200 + 3600; // 1 hour from now - strcpy(waypoint.name, "Test Point"); - strcpy(waypoint.description, "Test waypoint description"); +static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size) { + meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero; + waypoint.id = 12345; + waypoint.latitude_i = 374208000; + waypoint.longitude_i = -1221981000; + waypoint.expire = 1609459200 + 3600; // 1 hour from now + strcpy(waypoint.name, "Test Point"); + strcpy(waypoint.description, "Test waypoint description"); - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint); + return stream.bytes_written; } // Test WAYPOINT_APP port -void test_waypoint_serialization() -{ - uint8_t buffer[256]; - size_t payload_size = encode_waypoint(buffer, sizeof(buffer)); +void test_waypoint_serialization() { + uint8_t buffer[256]; + size_t payload_size = encode_waypoint(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify waypoint data - TEST_ASSERT_TRUE(payload.find("id") != payload.end()); - TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber()); + // Verify waypoint data + TEST_ASSERT_TRUE(payload.find("id") != payload.end()); + TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("name") != payload.end()); - TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str()); + TEST_ASSERT_TRUE(payload.find("name") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/test_helpers.h b/test/test_meshpacket_serializer/test_helpers.h index 12245b85d..7aa4ac695 100644 --- a/test/test_meshpacket_serializer/test_helpers.h +++ b/test/test_meshpacket_serializer/test_helpers.h @@ -12,38 +12,37 @@ // Helper function to create a test packet with the given port and payload static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size, - int payload_variant = meshtastic_MeshPacket_decoded_tag) -{ - meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + int payload_variant = meshtastic_MeshPacket_decoded_tag) { + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; - packet.id = 0x9999; - packet.from = 0x11223344; - packet.to = 0x55667788; - packet.channel = 0; - packet.hop_limit = 3; - packet.want_ack = false; - packet.priority = meshtastic_MeshPacket_Priority_UNSET; - packet.rx_time = 1609459200; - packet.rx_snr = 10.5f; - packet.hop_start = 3; - packet.rx_rssi = -85; - packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; + packet.id = 0x9999; + packet.from = 0x11223344; + packet.to = 0x55667788; + packet.channel = 0; + packet.hop_limit = 3; + packet.want_ack = false; + packet.priority = meshtastic_MeshPacket_Priority_UNSET; + packet.rx_time = 1609459200; + packet.rx_snr = 10.5f; + packet.hop_start = 3; + packet.rx_rssi = -85; + packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; - // Set decoded variant - packet.which_payload_variant = payload_variant; - packet.decoded.portnum = port; - if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) { - packet.encrypted.size = payload_size; - memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); - } - memcpy(packet.decoded.payload.bytes, payload, payload_size); - packet.decoded.payload.size = payload_size; - packet.decoded.want_response = false; - packet.decoded.dest = 0x55667788; - packet.decoded.source = 0x11223344; - packet.decoded.request_id = 0; - packet.decoded.reply_id = 0; - packet.decoded.emoji = 0; + // Set decoded variant + packet.which_payload_variant = payload_variant; + packet.decoded.portnum = port; + if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) { + packet.encrypted.size = payload_size; + memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); + } + memcpy(packet.decoded.payload.bytes, payload, payload_size); + packet.decoded.payload.size = payload_size; + packet.decoded.want_response = false; + packet.decoded.dest = 0x55667788; + packet.decoded.source = 0x11223344; + packet.decoded.request_id = 0; + packet.decoded.reply_id = 0; + packet.decoded.emoji = 0; - return packet; + return packet; } diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp index 484db8d74..bc4439df7 100644 --- a/test/test_meshpacket_serializer/test_serializer.cpp +++ b/test/test_meshpacket_serializer/test_serializer.cpp @@ -20,42 +20,38 @@ void test_telemetry_environment_metrics_unset_fields(); void test_encrypted_packet_serialization(); void test_empty_encrypted_packet(); -void setup() -{ - UNITY_BEGIN(); +void setup() { + UNITY_BEGIN(); - // Text message tests - RUN_TEST(test_text_message_serialization); - RUN_TEST(test_text_message_serialization_null); - RUN_TEST(test_text_message_serialization_long_text); - RUN_TEST(test_text_message_serialization_oversized); - RUN_TEST(test_text_message_serialization_invalid_utf8); + // Text message tests + RUN_TEST(test_text_message_serialization); + RUN_TEST(test_text_message_serialization_null); + RUN_TEST(test_text_message_serialization_long_text); + RUN_TEST(test_text_message_serialization_oversized); + RUN_TEST(test_text_message_serialization_invalid_utf8); - // Position tests - RUN_TEST(test_position_serialization); + // Position tests + RUN_TEST(test_position_serialization); - // Nodeinfo tests - RUN_TEST(test_nodeinfo_serialization); + // Nodeinfo tests + RUN_TEST(test_nodeinfo_serialization); - // Waypoint tests - RUN_TEST(test_waypoint_serialization); + // Waypoint tests + RUN_TEST(test_waypoint_serialization); - // Telemetry tests - RUN_TEST(test_telemetry_device_metrics_serialization); - RUN_TEST(test_telemetry_environment_metrics_serialization); - RUN_TEST(test_telemetry_environment_metrics_comprehensive); - RUN_TEST(test_telemetry_environment_metrics_missing_fields); - RUN_TEST(test_telemetry_environment_metrics_complete_coverage); - RUN_TEST(test_telemetry_environment_metrics_unset_fields); + // Telemetry tests + RUN_TEST(test_telemetry_device_metrics_serialization); + RUN_TEST(test_telemetry_environment_metrics_serialization); + RUN_TEST(test_telemetry_environment_metrics_comprehensive); + RUN_TEST(test_telemetry_environment_metrics_missing_fields); + RUN_TEST(test_telemetry_environment_metrics_complete_coverage); + RUN_TEST(test_telemetry_environment_metrics_unset_fields); - // Encrypted packet test - RUN_TEST(test_encrypted_packet_serialization); - RUN_TEST(test_empty_encrypted_packet); + // Encrypted packet test + RUN_TEST(test_encrypted_packet_serialization); + RUN_TEST(test_empty_encrypted_packet); - UNITY_END(); + UNITY_END(); } -void loop() -{ - delay(1000); -} +void loop() { delay(1000); } diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index a566dabf7..5c84cee49 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -33,194 +33,175 @@ #define IS_RUNNING_TESTS 0 #endif -namespace -{ +namespace { // Minimal router needed to receive messages from MQTT. -class MockRouter : public Router -{ - public: - ~MockRouter() - { - // cryptLock is created in the constructor for Router. - delete cryptLock; - cryptLock = NULL; - } - void enqueueReceivedMessage(meshtastic_MeshPacket *p) override - { - packets_.emplace_back(*p); - packetPool.release(p); - } - std::list packets_; // Packets received by the Router. +class MockRouter : public Router { +public: + ~MockRouter() { + // cryptLock is created in the constructor for Router. + delete cryptLock; + cryptLock = NULL; + } + void enqueueReceivedMessage(meshtastic_MeshPacket *p) override { + packets_.emplace_back(*p); + packetPool.release(p); + } + std::list packets_; // Packets received by the Router. }; // Minimal MeshService needed to receive messages from MQTT for testing PKI channel. -class MockMeshService : public MeshService -{ - public: - void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override - { - messages_.emplace_back(*m); - releaseMqttClientProxyMessageToPool(m); - } - void sendClientNotification(meshtastic_ClientNotification *n) override - { - notifications_.emplace_back(*n); - releaseClientNotificationToPool(n); - } - std::list messages_; // Messages received from the MeshService. - std::list notifications_; // Notifications received from the MeshService. +class MockMeshService : public MeshService { +public: + void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override { + messages_.emplace_back(*m); + releaseMqttClientProxyMessageToPool(m); + } + void sendClientNotification(meshtastic_ClientNotification *n) override { + notifications_.emplace_back(*n); + releaseClientNotificationToPool(n); + } + std::list messages_; // Messages received from the MeshService. + std::list notifications_; // Notifications received from the MeshService. }; // Minimal NodeDB needed to return values from getMeshNode. -class MockNodeDB : public NodeDB -{ - public: - meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; } - meshtastic_NodeInfoLite emptyNode = {}; +class MockNodeDB : public NodeDB { +public: + meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; } + meshtastic_NodeInfoLite emptyNode = {}; }; // Minimal RoutingModule needed to return values from sendAckNak. -class MockRoutingModule : public RoutingModule -{ - public: - void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, - bool ackWantsAck = false) override - { - ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit); - } - std::list> - ackNacks_; // ackNacks received by the RoutingModule. +class MockRoutingModule : public RoutingModule { +public: + void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, + bool ackWantsAck = false) override { + ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit); + } + std::list> ackNacks_; // ackNacks received by the RoutingModule. }; // A WiFi client used by the MQTT::PubSubClient. Implements a minimal pub/sub server. // There isn't an easy way to mock PubSubClient due to it not having virtual methods, so we mock using // the WiFiClinet that PubSubClient uses. -class MockPubSubServer : public WiFiClient -{ - public: - static constexpr char kTextTopic[] = "TextTopic"; - uint8_t connected() override { return connected_; } - void flush() override {} - IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); } - void stop() override { connected_ = false; } +class MockPubSubServer : public WiFiClient { +public: + static constexpr char kTextTopic[] = "TextTopic"; + uint8_t connected() override { return connected_; } + void flush() override {} + IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); } + void stop() override { connected_ = false; } - int connect(IPAddress ip, uint16_t port) override - { - port_ = port; - if (refuseConnection_) - return 0; - connected_ = true; - return 1; - } - int connect(const char *host, uint16_t port) override - { - host_ = host; - port_ = port; - if (refuseConnection_) - return 0; - connected_ = true; - return 1; + int connect(IPAddress ip, uint16_t port) override { + port_ = port; + if (refuseConnection_) + return 0; + connected_ = true; + return 1; + } + int connect(const char *host, uint16_t port) override { + host_ = host; + port_ = port; + if (refuseConnection_) + return 0; + connected_ = true; + return 1; + } + + int available() override { + if (buffer_.empty()) + return 0; + return buffer_.front().size(); + } + + int read() override { + assert(available()); + std::string &front = buffer_.front(); + char ch = front[0]; + front = front.substr(1, front.size()); + if (front.empty()) + buffer_.pop_front(); + return ch; + } + + size_t write(uint8_t data) override { return write(&data, 1); } + size_t write(const uint8_t *buf, size_t size) override { + command_ += std::string(reinterpret_cast(buf), size); + if (command_.size() < 2) + return size; + const int len = (uint8_t)command_[1] + 2; + if (command_.size() < len) + return size; + handleCommand(command_[0], command_.substr(2, len)); + command_ = command_.substr(len, command_.size()); + return size; + } + + // The pub/sub "server". + // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf + void handleCommand(uint8_t header, std::string_view message) { + switch (header & 0xf0) { + case MQTTCONNECT: + LOG_DEBUG("MQTTCONNECT"); + buffer_.push_back(std::string("\x20\x02\x00\x00", 4)); + break; + + case MQTTSUBSCRIBE: { + LOG_DEBUG("MQTTSUBSCRIBE"); + assert(message.size() >= 5); + message.remove_prefix(2); // skip messageId + + while (message.size() >= 3) { + const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; + message.remove_prefix(2); + + assert(message.size() >= topicSize + 1); + std::string topic(message.data(), topicSize); + message.remove_prefix(topicSize + 1); + + LOG_DEBUG("Subscribed to topic: %s", topic.c_str()); + subscriptions_.insert(std::move(topic)); + } + break; } - int available() override - { - if (buffer_.empty()) - return 0; - return buffer_.front().size(); + case MQTTPINGREQ: + LOG_DEBUG("MQTTPINGREQ"); + buffer_.push_back(std::string("\xd0\x00", 2)); + break; + + case MQTTPUBLISH: { + LOG_DEBUG("MQTTPUBLISH"); + assert(message.size() >= 3); + const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; + message.remove_prefix(2); + + assert(message.size() >= topicSize); + std::string topic(message.data(), topicSize); + message.remove_prefix(topicSize); + + if (topic == kTextTopic) { + published_.emplace_back(std::move(topic), std::string(message.data(), message.size())); + } else { + published_.emplace_back(std::move(topic), DecodedServiceEnvelope(reinterpret_cast(message.data()), message.size())); + } + break; } - - int read() override - { - assert(available()); - std::string &front = buffer_.front(); - char ch = front[0]; - front = front.substr(1, front.size()); - if (front.empty()) - buffer_.pop_front(); - return ch; } + } - size_t write(uint8_t data) override { return write(&data, 1); } - size_t write(const uint8_t *buf, size_t size) override - { - command_ += std::string(reinterpret_cast(buf), size); - if (command_.size() < 2) - return size; - const int len = (uint8_t)command_[1] + 2; - if (command_.size() < len) - return size; - handleCommand(command_[0], command_.substr(2, len)); - command_ = command_.substr(len, command_.size()); - return size; - } - - // The pub/sub "server". - // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf - void handleCommand(uint8_t header, std::string_view message) - { - switch (header & 0xf0) { - case MQTTCONNECT: - LOG_DEBUG("MQTTCONNECT"); - buffer_.push_back(std::string("\x20\x02\x00\x00", 4)); - break; - - case MQTTSUBSCRIBE: { - LOG_DEBUG("MQTTSUBSCRIBE"); - assert(message.size() >= 5); - message.remove_prefix(2); // skip messageId - - while (message.size() >= 3) { - const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; - message.remove_prefix(2); - - assert(message.size() >= topicSize + 1); - std::string topic(message.data(), topicSize); - message.remove_prefix(topicSize + 1); - - LOG_DEBUG("Subscribed to topic: %s", topic.c_str()); - subscriptions_.insert(std::move(topic)); - } - break; - } - - case MQTTPINGREQ: - LOG_DEBUG("MQTTPINGREQ"); - buffer_.push_back(std::string("\xd0\x00", 2)); - break; - - case MQTTPUBLISH: { - LOG_DEBUG("MQTTPUBLISH"); - assert(message.size() >= 3); - const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; - message.remove_prefix(2); - - assert(message.size() >= topicSize); - std::string topic(message.data(), topicSize); - message.remove_prefix(topicSize); - - if (topic == kTextTopic) { - published_.emplace_back(std::move(topic), std::string(message.data(), message.size())); - } else { - published_.emplace_back( - std::move(topic), DecodedServiceEnvelope(reinterpret_cast(message.data()), message.size())); - } - break; - } - } - } - - bool connected_ = false; - bool refuseConnection_ = false; // Simulate a failed connection. - uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server. - std::string host_; // Requested host. - uint16_t port_; // Requested port. - std::list buffer_; // Buffer of messages for the pubSub client to receive. - std::string command_; // Current command received from the pubSub client. - std::set subscriptions_; // Topics that the pubSub client has subscribed to. - std::list>> - published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name and either - // a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope. + bool connected_ = false; + bool refuseConnection_ = false; // Simulate a failed connection. + uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server. + std::string host_; // Requested host. + uint16_t port_; // Requested port. + std::list buffer_; // Buffer of messages for the pubSub client to receive. + std::string command_; // Current command received from the pubSub client. + std::set subscriptions_; // Topics that the pubSub client has subscribed to. + std::list>> + published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name + // and either a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope. }; // Instances of our mocks. @@ -233,71 +214,61 @@ MockRouter *mockRouter; // Keep running the loop until either conditionMet returns true or 4 seconds elapse. // Returns true if conditionMet returns true, returns false on timeout. -bool loopUntil(std::function conditionMet) -{ - long start = millis(); - while (start + 4000 > millis()) { - long delayMsec = concurrency::mainController.runOrDelay(); - if (conditionMet()) - return true; - concurrency::mainDelay.delay(std::min(delayMsec, 5L)); - } - return false; +bool loopUntil(std::function conditionMet) { + long start = millis(); + while (start + 4000 > millis()) { + long delayMsec = concurrency::mainController.runOrDelay(); + if (conditionMet()) + return true; + concurrency::mainDelay.delay(std::min(delayMsec, 5L)); + } + return false; } // Used to access protected/private members of MQTT for unit testing. -class MQTTUnitTest : public MQTT -{ - public: - MQTTUnitTest() : MQTT(std::make_unique()) - { - pubsub = reinterpret_cast(mqttClient.get()); +class MQTTUnitTest : public MQTT { +public: + MQTTUnitTest() : MQTT(std::make_unique()) { pubsub = reinterpret_cast(mqttClient.get()); } + ~MQTTUnitTest() { + // Needed because WiFiClient does not have a virtual destructor. + mqttClient.release(); + delete pubsub; + } + using MQTT::isValidConfig; + using MQTT::reconnect; + int queueSize() { return mqttQueue.numUsed(); } + void reportToMap(std::optional precision = std::nullopt) { + if (precision.has_value()) + map_position_precision = precision.value(); + map_publish_interval_msecs = 0; + perhapsReportToMap(); + } + void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test") { + std::stringstream topic; + topic << "msh/2/e/" << channel << "/!" << gateway; + const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), + .channel_id = const_cast(channel.c_str()), + .gateway_id = const_cast(gateway.c_str())}; + uint8_t bytes[256]; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); + mqttCallback(const_cast(topic.str().c_str()), bytes, numBytes); + } + static void restart() { + if (mqtt != NULL) { + delete mqtt; + mqtt = unitTest = NULL; } - ~MQTTUnitTest() - { - // Needed because WiFiClient does not have a virtual destructor. - mqttClient.release(); - delete pubsub; - } - using MQTT::isValidConfig; - using MQTT::reconnect; - int queueSize() { return mqttQueue.numUsed(); } - void reportToMap(std::optional precision = std::nullopt) - { - if (precision.has_value()) - map_position_precision = precision.value(); - map_publish_interval_msecs = 0; - perhapsReportToMap(); - } - void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test") - { - std::stringstream topic; - topic << "msh/2/e/" << channel << "/!" << gateway; - const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), - .channel_id = const_cast(channel.c_str()), - .gateway_id = const_cast(gateway.c_str())}; - uint8_t bytes[256]; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); - mqttCallback(const_cast(topic.str().c_str()), bytes, numBytes); - } - static void restart() - { - if (mqtt != NULL) { - delete mqtt; - mqtt = unitTest = NULL; - } - mqtt = unitTest = new MQTTUnitTest(); - mqtt->start(); + mqtt = unitTest = new MQTTUnitTest(); + mqtt->start(); - if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { - loopUntil([] { return true; }); // Loop once - return; - } - // Wait for MQTT to subscribe to all topics. - TEST_ASSERT_TRUE(loopUntil( - [] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); + if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { + loopUntil([] { return true; }); // Loop once + return; } - PubSubClient &getPubSub() { return pubSub; } + // Wait for MQTT to subscribe to all topics. + TEST_ASSERT_TRUE(loopUntil([] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); + } + PubSubClient &getPubSub() { return pubSub; } }; // Packets used in unit tests. @@ -318,616 +289,561 @@ const meshtastic_MeshPacket encrypted = { } // namespace // Initialize mocks and configuration before running each test. -void setUp(void) -{ - moduleConfig.mqtt = - meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true}; - moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{ - .publish_interval_secs = 0, .position_precision = 14, .should_report_location = true}; - channelFile.channels[0] = meshtastic_Channel{ - .index = 0, - .has_settings = true, - .settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true}, - .role = meshtastic_Channel_Role_PRIMARY, - }; - channelFile.channels_count = 1; - owner = meshtastic_User{.id = "!12345678"}; - myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic - localPosition = - meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7}; +void setUp(void) { + moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true}; + moduleConfig.mqtt.map_report_settings = + meshtastic_ModuleConfig_MapReportSettings{.publish_interval_secs = 0, .position_precision = 14, .should_report_location = true}; + channelFile.channels[0] = meshtastic_Channel{ + .index = 0, + .has_settings = true, + .settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true}, + .role = meshtastic_Channel_Role_PRIMARY, + }; + channelFile.channels_count = 1; + owner = meshtastic_User{.id = "!12345678"}; + myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic + localPosition = meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7}; - router = mockRouter = new MockRouter(); - service = mockMeshService = new MockMeshService(); - routingModule = mockRoutingModule = new MockRoutingModule(); - MQTTUnitTest::restart(); + router = mockRouter = new MockRouter(); + service = mockMeshService = new MockMeshService(); + routingModule = mockRoutingModule = new MockRoutingModule(); + MQTTUnitTest::restart(); } // Deinitialize all objects created in setUp. -void tearDown(void) -{ - delete unitTest; - mqtt = unitTest = NULL; - delete mockRoutingModule; - routingModule = mockRoutingModule = NULL; - delete mockMeshService; - service = mockMeshService = NULL; - delete mockRouter; - router = mockRouter = NULL; +void tearDown(void) { + delete unitTest; + mqtt = unitTest = NULL; + delete mockRoutingModule; + routingModule = mockRoutingModule = NULL; + delete mockMeshService; + service = mockMeshService = NULL; + delete mockRouter; + router = mockRouter = NULL; } // Test that the decoded MeshPacket is published when encryption_enabled = false. -void test_sendDirectlyConnectedDecoded(void) -{ - mqtt->onSend(encrypted, decoded, 0); +void test_sendDirectlyConnectedDecoded(void) { + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - const DecodedServiceEnvelope &env = std::get(payload); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(decoded.id, env.packet->id); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + const DecodedServiceEnvelope &env = std::get(payload); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Test that the encrypted MeshPacket is published when encryption_enabled = true. -void test_sendDirectlyConnectedEncrypted(void) -{ - moduleConfig.mqtt.encryption_enabled = true; +void test_sendDirectlyConnectedEncrypted(void) { + moduleConfig.mqtt.encryption_enabled = true; - mqtt->onSend(encrypted, decoded, 0); + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - const DecodedServiceEnvelope &env = std::get(payload); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + const DecodedServiceEnvelope &env = std::get(payload); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); } // Verify that the decoded MeshPacket is proxied through the MeshService when encryption_enabled = false. -void test_proxyToMeshServiceDecoded(void) -{ - moduleConfig.mqtt.proxy_to_client_enabled = true; - MQTTUnitTest::restart(); +void test_proxyToMeshServiceDecoded(void) { + moduleConfig.mqtt.proxy_to_client_enabled = true; + MQTTUnitTest::restart(); - mqtt->onSend(encrypted, decoded, 0); + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); - const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(decoded.id, env.packet->id); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); + const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Verify that the encrypted MeshPacket is proxied through the MeshService when encryption_enabled = true. -void test_proxyToMeshServiceEncrypted(void) -{ - moduleConfig.mqtt.proxy_to_client_enabled = true; - moduleConfig.mqtt.encryption_enabled = true; - MQTTUnitTest::restart(); +void test_proxyToMeshServiceEncrypted(void) { + moduleConfig.mqtt.proxy_to_client_enabled = true; + moduleConfig.mqtt.encryption_enabled = true; + MQTTUnitTest::restart(); - mqtt->onSend(encrypted, decoded, 0); + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); - const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); + const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); } // A packet without the OK to MQTT bit set should not be published to a public server. -void test_dontMqttMeOnPublicServer(void) -{ - meshtastic_MeshPacket p = decoded; - p.decoded.bitfield = 0; - p.decoded.has_bitfield = 0; +void test_dontMqttMeOnPublicServer(void) { + meshtastic_MeshPacket p = decoded; + p.decoded.bitfield = 0; + p.decoded.has_bitfield = 0; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); } // A packet without the OK to MQTT bit set should be published to a private server. -void test_okToMqttOnPrivateServer(void) -{ - // Cause a disconnect. - pubsub->connected_ = false; - pubsub->refuseConnection_ = true; - TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); +void test_okToMqttOnPrivateServer(void) { + // Cause a disconnect. + pubsub->connected_ = false; + pubsub->refuseConnection_ = true; + TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); - // Use 127.0.0.1 for the server's IP. - pubsub->ipAddress_ = 0x7f000001; + // Use 127.0.0.1 for the server's IP. + pubsub->ipAddress_ = 0x7f000001; - // Reconnect. - pubsub->refuseConnection_ = false; - TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); })); + // Reconnect. + pubsub->refuseConnection_ = false; + TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); })); - // Send the same packet as test_dontMqttMeOnPublicServer. - meshtastic_MeshPacket p = decoded; - p.decoded.bitfield = 0; - p.decoded.has_bitfield = 0; + // Send the same packet as test_dontMqttMeOnPublicServer. + meshtastic_MeshPacket p = decoded; + p.decoded.bitfield = 0; + p.decoded.has_bitfield = 0; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); } // Range tests messages are not uplinked to the default server. -void test_noRangeTestAppOnDefaultServer(void) -{ - meshtastic_MeshPacket p = decoded; - p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP; +void test_noRangeTestAppOnDefaultServer(void) { + meshtastic_MeshPacket p = decoded; + p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); } // Detection sensor messages are not uplinked to the default server. -void test_noDetectionSensorAppOnDefaultServer(void) -{ - meshtastic_MeshPacket p = decoded; - p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP; +void test_noDetectionSensorAppOnDefaultServer(void) { + meshtastic_MeshPacket p = decoded; + p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); } // Test that a MeshPacket is queued while the MQTT server is disconnected. -void test_sendQueued(void) -{ - // Cause a disconnect. - pubsub->connected_ = false; - pubsub->refuseConnection_ = true; - TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); +void test_sendQueued(void) { + // Cause a disconnect. + pubsub->connected_ = false; + pubsub->refuseConnection_ = true; + TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); - // Send while disconnected. - mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, unitTest->queueSize()); - TEST_ASSERT_TRUE(pubsub->published_.empty()); - TEST_ASSERT_FALSE(unitTest->getPubSub().connected()); + // Send while disconnected. + mqtt->onSend(encrypted, decoded, 0); + TEST_ASSERT_EQUAL(1, unitTest->queueSize()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_FALSE(unitTest->getPubSub().connected()); - // Allow reconnect to happen. Expect to see the packet published now. - pubsub->refuseConnection_ = false; - TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); })); + // Allow reconnect to happen. Expect to see the packet published now. + pubsub->refuseConnection_ = false; + TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); })); - TEST_ASSERT_EQUAL(0, unitTest->queueSize()); - const auto &[topic, payload] = pubsub->published_.front(); - const DecodedServiceEnvelope &env = std::get(payload); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(decoded.id, env.packet->id); + TEST_ASSERT_EQUAL(0, unitTest->queueSize()); + const auto &[topic, payload] = pubsub->published_.front(); + const DecodedServiceEnvelope &env = std::get(payload); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Verify reconnecting with the proxy enabled does not reconnect to a MQTT server. -void test_reconnectProxyDoesNotReconnectMqtt(void) -{ - moduleConfig.mqtt.proxy_to_client_enabled = true; - MQTTUnitTest::restart(); +void test_reconnectProxyDoesNotReconnectMqtt(void) { + moduleConfig.mqtt.proxy_to_client_enabled = true; + MQTTUnitTest::restart(); - unitTest->reconnect(); + unitTest->reconnect(); - TEST_ASSERT_FALSE(pubsub->connected_); + TEST_ASSERT_FALSE(pubsub->connected_); } // Test receiving an empty MeshPacket on a subscribed topic. -void test_receiveEmptyMeshPacket(void) -{ - unitTest->publish(NULL); +void test_receiveEmptyMeshPacket(void) { + unitTest->publish(NULL); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Test receiving a decoded MeshPacket on a subscribed topic. -void test_receiveDecodedProto(void) -{ - unitTest->publish(&decoded); +void test_receiveDecodedProto(void) { + unitTest->publish(&decoded); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(decoded.id, p.id); - TEST_ASSERT_TRUE(p.via_mqtt); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(decoded.id, p.id); + TEST_ASSERT_TRUE(p.via_mqtt); } // Test receiving a decoded MeshPacket from the phone proxy. -void test_receiveDecodedProtoFromProxy(void) -{ - const meshtastic_ServiceEnvelope env = { - .packet = const_cast(&decoded), .channel_id = "test", .gateway_id = "!87654321"}; - meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; - strcat(message.topic, "msh/2/e/test/!87654321"); - message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - message.payload_variant.data.size = pb_encode_to_bytes( - message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env); +void test_receiveDecodedProtoFromProxy(void) { + const meshtastic_ServiceEnvelope env = {.packet = const_cast(&decoded), .channel_id = "test", .gateway_id = "!87654321"}; + meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; + strcat(message.topic, "msh/2/e/test/!87654321"); + message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; + message.payload_variant.data.size = + pb_encode_to_bytes(message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env); - mqtt->onClientProxyReceive(message); + mqtt->onClientProxyReceive(message); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(decoded.id, p.id); - TEST_ASSERT_TRUE(p.via_mqtt); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(decoded.id, p.id); + TEST_ASSERT_TRUE(p.via_mqtt); } // Properly handles the case where the received message is empty. -void test_receiveEmptyDataFromProxy(void) -{ - meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; - message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; +void test_receiveEmptyDataFromProxy(void) { + meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; + message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - mqtt->onClientProxyReceive(message); + mqtt->onClientProxyReceive(message); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Packets should be ignored if downlink is not enabled. -void test_receiveWithoutChannelDownlink(void) -{ - channelFile.channels[0].settings.downlink_enabled = false; +void test_receiveWithoutChannelDownlink(void) { + channelFile.channels[0].settings.downlink_enabled = false; - unitTest->publish(&decoded); + unitTest->publish(&decoded); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Test receiving an encrypted MeshPacket on the PKI topic. -void test_receiveEncryptedPKITopicToUs(void) -{ - meshtastic_MeshPacket e = encrypted; - e.to = myNodeInfo.my_node_num; +void test_receiveEncryptedPKITopicToUs(void) { + meshtastic_MeshPacket e = encrypted; + e.to = myNodeInfo.my_node_num; - unitTest->publish(&e, "!87654321", "PKI"); + unitTest->publish(&e, "!87654321", "PKI"); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(encrypted.id, p.id); - TEST_ASSERT_TRUE(p.via_mqtt); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(encrypted.id, p.id); + TEST_ASSERT_TRUE(p.via_mqtt); } // Should ignore messages published to MQTT by this gateway. -void test_receiveIgnoresOwnPublishedMessages(void) -{ - unitTest->publish(&decoded, nodeDB->getNodeId().c_str()); +void test_receiveIgnoresOwnPublishedMessages(void) { + unitTest->publish(&decoded, nodeDB->getNodeId().c_str()); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Considers receiving one of our packets an acknowledgement of it being sent. -void test_receiveAcksOwnSentMessages(void) -{ - meshtastic_MeshPacket p = decoded; - p.from = myNodeInfo.my_node_num; +void test_receiveAcksOwnSentMessages(void) { + meshtastic_MeshPacket p = decoded; + p.from = myNodeInfo.my_node_num; - unitTest->publish(&p, nodeDB->getNodeId().c_str()); + unitTest->publish(&p, nodeDB->getNodeId().c_str()); - // FIXME: Better assertion for this test - // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); - // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); - // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); - // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); - // TEST_ASSERT_EQUAL(p.id, idFrom); + // FIXME: Better assertion for this test + // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); + // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); + // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); + // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); + // TEST_ASSERT_EQUAL(p.id, idFrom); } // Should ignore our own messages from MQTT that were heard by other nodes. -void test_receiveIgnoresSentMessagesFromOthers(void) -{ - meshtastic_MeshPacket p = decoded; - p.from = myNodeInfo.my_node_num; +void test_receiveIgnoresSentMessagesFromOthers(void) { + meshtastic_MeshPacket p = decoded; + p.from = myNodeInfo.my_node_num; - unitTest->publish(&p); + unitTest->publish(&p); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Decoded MQTT messages should be ignored when encryption is enabled. -void test_receiveIgnoresDecodedWhenEncryptionEnabled(void) -{ - moduleConfig.mqtt.encryption_enabled = true; +void test_receiveIgnoresDecodedWhenEncryptionEnabled(void) { + moduleConfig.mqtt.encryption_enabled = true; - unitTest->publish(&decoded); + unitTest->publish(&decoded); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Non-encrypted messages for the Admin App should be ignored. -void test_receiveIgnoresDecodedAdminApp(void) -{ - meshtastic_MeshPacket p = decoded; - p.decoded.portnum = meshtastic_PortNum_ADMIN_APP; +void test_receiveIgnoresDecodedAdminApp(void) { + meshtastic_MeshPacket p = decoded; + p.decoded.portnum = meshtastic_PortNum_ADMIN_APP; - unitTest->publish(&p); + unitTest->publish(&p); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Only the same fields that are transmitted over LoRa should be set in MQTT messages. -void test_receiveIgnoresUnexpectedFields(void) -{ - meshtastic_MeshPacket input = decoded; - input.rx_snr = 10; - input.rx_rssi = 20; +void test_receiveIgnoresUnexpectedFields(void) { + meshtastic_MeshPacket input = decoded; + input.rx_snr = 10; + input.rx_rssi = 20; - unitTest->publish(&input); + unitTest->publish(&input); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(0, p.rx_snr); - TEST_ASSERT_EQUAL(0, p.rx_rssi); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(0, p.rx_snr); + TEST_ASSERT_EQUAL(0, p.rx_rssi); } // Messages with an invalid hop_limit are ignored. -void test_receiveIgnoresInvalidHopLimit(void) -{ - meshtastic_MeshPacket p = decoded; - p.hop_limit = 10; +void test_receiveIgnoresInvalidHopLimit(void) { + meshtastic_MeshPacket p = decoded; + p.hop_limit = 10; - unitTest->publish(&p); + unitTest->publish(&p); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Publishing to a text channel. -void test_publishTextMessageDirect(void) -{ - TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); +void test_publishTextMessageDirect(void) { + TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - TEST_ASSERT_EQUAL_STRING("payload", std::get(payload).c_str()); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + TEST_ASSERT_EQUAL_STRING("payload", std::get(payload).c_str()); } // Publishing to a text channel via the MQTT client proxy. -void test_publishTextMessageWithProxy(void) -{ - moduleConfig.mqtt.proxy_to_client_enabled = true; +void test_publishTextMessageWithProxy(void) { + moduleConfig.mqtt.proxy_to_client_enabled = true; - TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); + TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant); - TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant); + TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text); } // Helper method to verify the expected latitude/longitude was received. -void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude) -{ - TEST_ASSERT_TRUE(env.validDecode); - const meshtastic_MeshPacket &p = *env.packet; - TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to); - TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant); - TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum); +void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude) { + TEST_ASSERT_TRUE(env.validDecode); + const meshtastic_MeshPacket &p = *env.packet; + TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to); + TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant); + TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum); - meshtastic_MapReport mapReport; - TEST_ASSERT_TRUE( - pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport)); - TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i); - TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i); + meshtastic_MapReport mapReport; + TEST_ASSERT_TRUE(pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport)); + TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i); + TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i); } // Map reporting defaults to an imprecise location. -void test_reportToMapDefaultImprecise(void) -{ - unitTest->reportToMap(); +void test_reportToMapDefaultImprecise(void) { + unitTest->reportToMap(); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str()); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str()); } // Location is sent over the phone proxy. -void test_reportToMapImpreciseProxied(void) -{ - moduleConfig.mqtt.proxy_to_client_enabled = true; - MQTTUnitTest::restart(); +void test_reportToMapImpreciseProxied(void) { + moduleConfig.mqtt.proxy_to_client_enabled = true; + MQTTUnitTest::restart(); - unitTest->reportToMap(/*precision=*/14); + unitTest->reportToMap(/*precision=*/14); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); - const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); + const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); } // isUsingDefaultServer returns true when using the default server. -void test_usingDefaultServer(void) -{ - TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); -} +void test_usingDefaultServer(void) { TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns true when using the default server and a port. -void test_usingDefaultServerWithPort(void) -{ - std::string server = default_mqtt_address; - server += ":1883"; - strcpy(moduleConfig.mqtt.address, server.c_str()); - MQTTUnitTest::restart(); +void test_usingDefaultServerWithPort(void) { + std::string server = default_mqtt_address; + server += ":1883"; + strcpy(moduleConfig.mqtt.address, server.c_str()); + MQTTUnitTest::restart(); - TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); + TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns true when using the default server and invalid port. -void test_usingDefaultServerWithInvalidPort(void) -{ - std::string server = default_mqtt_address; - server += ":invalid"; - strcpy(moduleConfig.mqtt.address, server.c_str()); - MQTTUnitTest::restart(); +void test_usingDefaultServerWithInvalidPort(void) { + std::string server = default_mqtt_address; + server += ":invalid"; + strcpy(moduleConfig.mqtt.address, server.c_str()); + MQTTUnitTest::restart(); - TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); + TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns false when not using the default server. -void test_usingCustomServer(void) -{ - strcpy(moduleConfig.mqtt.address, "custom"); - MQTTUnitTest::restart(); +void test_usingCustomServer(void) { + strcpy(moduleConfig.mqtt.address, "custom"); + MQTTUnitTest::restart(); - TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer()); + TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer()); } // Test that isEnabled returns true the MQTT module is enabled. -void test_enabled(void) -{ - TEST_ASSERT_TRUE(mqtt->isEnabled()); -} +void test_enabled(void) { TEST_ASSERT_TRUE(mqtt->isEnabled()); } // Test that isEnabled returns false the MQTT module not enabled. -void test_disabled(void) -{ - moduleConfig.mqtt.enabled = false; - MQTTUnitTest::restart(); +void test_disabled(void) { + moduleConfig.mqtt.enabled = false; + MQTTUnitTest::restart(); - TEST_ASSERT_FALSE(mqtt->isEnabled()); + TEST_ASSERT_FALSE(mqtt->isEnabled()); } // Subscriptions contain the moduleConfig.mqtt.root prefix. -void test_customMqttRoot(void) -{ - strcpy(moduleConfig.mqtt.root, "custom"); - MQTTUnitTest::restart(); +void test_customMqttRoot(void) { + strcpy(moduleConfig.mqtt.root, "custom"); + MQTTUnitTest::restart(); - TEST_ASSERT_TRUE(loopUntil( - [] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); })); + TEST_ASSERT_TRUE(loopUntil([] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); })); } // Empty configuration is valid. -void test_configEmptyIsValid(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {}; +void test_configEmptyIsValid(void) { + meshtastic_ModuleConfig_MQTTConfig config = {}; - TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Empty 'enabled' configuration is valid. -void test_configEnabledEmptyIsValid(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; - MockPubSubServer client; +void test_configEnabledEmptyIsValid(void) { + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; + MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); - TEST_ASSERT_EQUAL(1883, client.port_); + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(client.connected_); + TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); + TEST_ASSERT_EQUAL(1883, client.port_); } // Configuration with the default server is valid. -void test_configWithDefaultServer(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address}; +void test_configWithDefaultServer(void) { + meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address}; - TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server and port 8888 is invalid. -void test_configWithDefaultServerAndInvalidPort(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"}; +void test_configWithDefaultServerAndInvalidPort(void) { + meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"}; - TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } // isValidConfig connects to a custom host and port. -void test_configCustomHostAndPort(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; - MockPubSubServer client; +void test_configCustomHostAndPort(void) { + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; + MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); - TEST_ASSERT_EQUAL(1234, client.port_); + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(client.connected_); + TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); + TEST_ASSERT_EQUAL(1234, client.port_); } // isValidConfig returns false if a connection cannot be established. -void test_configWithConnectionFailure(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; - MockPubSubServer client; - client.refuseConnection_ = true; +void test_configWithConnectionFailure(void) { + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; + MockPubSubServer client; + client.refuseConnection_ = true; - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); } // isValidConfig returns true when tls_enabled is supported, or false otherwise. -void test_configWithTLSEnabled(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; - MockPubSubServer client; +void test_configWithTLSEnabled(void) { + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; + MockPubSubServer client; #if MQTT_SUPPORTS_TLS - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); #else - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); #endif } -void setup() -{ - initializeTestEnvironment(); - const std::unique_ptr mockNodeDB(new MockNodeDB()); - nodeDB = mockNodeDB.get(); +void setup() { + initializeTestEnvironment(); + const std::unique_ptr mockNodeDB(new MockNodeDB()); + nodeDB = mockNodeDB.get(); - UNITY_BEGIN(); - RUN_TEST(test_sendDirectlyConnectedDecoded); - RUN_TEST(test_sendDirectlyConnectedEncrypted); - RUN_TEST(test_proxyToMeshServiceDecoded); - RUN_TEST(test_proxyToMeshServiceEncrypted); - RUN_TEST(test_dontMqttMeOnPublicServer); - RUN_TEST(test_okToMqttOnPrivateServer); - RUN_TEST(test_noRangeTestAppOnDefaultServer); - RUN_TEST(test_noDetectionSensorAppOnDefaultServer); - RUN_TEST(test_sendQueued); - RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt); - RUN_TEST(test_receiveEmptyMeshPacket); - RUN_TEST(test_receiveDecodedProto); - RUN_TEST(test_receiveDecodedProtoFromProxy); - RUN_TEST(test_receiveEmptyDataFromProxy); - RUN_TEST(test_receiveWithoutChannelDownlink); - RUN_TEST(test_receiveEncryptedPKITopicToUs); - RUN_TEST(test_receiveIgnoresOwnPublishedMessages); - RUN_TEST(test_receiveAcksOwnSentMessages); - RUN_TEST(test_receiveIgnoresSentMessagesFromOthers); - RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled); - RUN_TEST(test_receiveIgnoresDecodedAdminApp); - RUN_TEST(test_receiveIgnoresUnexpectedFields); - RUN_TEST(test_receiveIgnoresInvalidHopLimit); - RUN_TEST(test_publishTextMessageDirect); - RUN_TEST(test_publishTextMessageWithProxy); - RUN_TEST(test_reportToMapDefaultImprecise); - RUN_TEST(test_reportToMapImpreciseProxied); - RUN_TEST(test_usingDefaultServer); - RUN_TEST(test_usingDefaultServerWithPort); - RUN_TEST(test_usingDefaultServerWithInvalidPort); - RUN_TEST(test_usingCustomServer); - RUN_TEST(test_enabled); - RUN_TEST(test_disabled); - RUN_TEST(test_customMqttRoot); - RUN_TEST(test_configEmptyIsValid); - RUN_TEST(test_configEnabledEmptyIsValid); - RUN_TEST(test_configWithDefaultServer); - RUN_TEST(test_configWithDefaultServerAndInvalidPort); - RUN_TEST(test_configCustomHostAndPort); - RUN_TEST(test_configWithConnectionFailure); - RUN_TEST(test_configWithTLSEnabled); - exit(UNITY_END()); + UNITY_BEGIN(); + RUN_TEST(test_sendDirectlyConnectedDecoded); + RUN_TEST(test_sendDirectlyConnectedEncrypted); + RUN_TEST(test_proxyToMeshServiceDecoded); + RUN_TEST(test_proxyToMeshServiceEncrypted); + RUN_TEST(test_dontMqttMeOnPublicServer); + RUN_TEST(test_okToMqttOnPrivateServer); + RUN_TEST(test_noRangeTestAppOnDefaultServer); + RUN_TEST(test_noDetectionSensorAppOnDefaultServer); + RUN_TEST(test_sendQueued); + RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt); + RUN_TEST(test_receiveEmptyMeshPacket); + RUN_TEST(test_receiveDecodedProto); + RUN_TEST(test_receiveDecodedProtoFromProxy); + RUN_TEST(test_receiveEmptyDataFromProxy); + RUN_TEST(test_receiveWithoutChannelDownlink); + RUN_TEST(test_receiveEncryptedPKITopicToUs); + RUN_TEST(test_receiveIgnoresOwnPublishedMessages); + RUN_TEST(test_receiveAcksOwnSentMessages); + RUN_TEST(test_receiveIgnoresSentMessagesFromOthers); + RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled); + RUN_TEST(test_receiveIgnoresDecodedAdminApp); + RUN_TEST(test_receiveIgnoresUnexpectedFields); + RUN_TEST(test_receiveIgnoresInvalidHopLimit); + RUN_TEST(test_publishTextMessageDirect); + RUN_TEST(test_publishTextMessageWithProxy); + RUN_TEST(test_reportToMapDefaultImprecise); + RUN_TEST(test_reportToMapImpreciseProxied); + RUN_TEST(test_usingDefaultServer); + RUN_TEST(test_usingDefaultServerWithPort); + RUN_TEST(test_usingDefaultServerWithInvalidPort); + RUN_TEST(test_usingCustomServer); + RUN_TEST(test_enabled); + RUN_TEST(test_disabled); + RUN_TEST(test_customMqttRoot); + RUN_TEST(test_configEmptyIsValid); + RUN_TEST(test_configEnabledEmptyIsValid); + RUN_TEST(test_configWithDefaultServer); + RUN_TEST(test_configWithDefaultServerAndInvalidPort); + RUN_TEST(test_configCustomHostAndPort); + RUN_TEST(test_configWithConnectionFailure); + RUN_TEST(test_configWithTLSEnabled); + exit(UNITY_END()); } #else -void setup() -{ - initializeTestEnvironment(); - LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient"); - UNITY_BEGIN(); - UNITY_END(); +void setup() { + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient"); + UNITY_BEGIN(); + UNITY_END(); } #endif void loop() {} \ No newline at end of file diff --git a/test/test_serial/SerialModule.cpp b/test/test_serial/SerialModule.cpp index 1bccf04a7..a1da8f25d 100644 --- a/test/test_serial/SerialModule.cpp +++ b/test/test_serial/SerialModule.cpp @@ -11,146 +11,132 @@ #define IS_RUNNING_TESTS 0 #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #include "modules/SerialModule.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) // Test that empty configuration is valid. -void test_serialConfigEmptyIsValid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = {}; +void test_serialConfigEmptyIsValid(void) { + meshtastic_ModuleConfig_SerialConfig config = {}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that basic enabled configuration is valid. -void test_serialConfigEnabledIsValid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; +void test_serialConfigEnabledIsValid(void) { + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and NMEA mode is valid. -void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; +void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) { + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and CalTopo mode is valid. -void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; +void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) { + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and DEFAULT mode is invalid. -void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; +void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) { + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and SIMPLE mode is invalid. -void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; +void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) { + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and TEXTMSG mode is invalid. -void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; +void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) { + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and PROTO mode is invalid. -void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; +void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) { + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that various modes work without override_console_serial_port. -void test_serialConfigVariousModesWithoutOverrideAreValid(void) -{ - meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; +void test_serialConfigVariousModesWithoutOverrideAreValid(void) { + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; - // Test DEFAULT mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test DEFAULT mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test SIMPLE mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test SIMPLE mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test TEXTMSG mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test TEXTMSG mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test PROTO mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test PROTO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test NMEA mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test NMEA mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test CALTOPO mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test CALTOPO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } #endif // Architecture check -void setup() -{ - initializeTestEnvironment(); +void setup() { + initializeTestEnvironment(); -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) - UNITY_BEGIN(); - RUN_TEST(test_serialConfigEmptyIsValid); - RUN_TEST(test_serialConfigEnabledIsValid); - RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); - RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); - RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); - RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); - RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); - RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); - RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); - exit(UNITY_END()); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + UNITY_BEGIN(); + RUN_TEST(test_serialConfigEmptyIsValid); + RUN_TEST(test_serialConfigEnabledIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); + RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); + exit(UNITY_END()); #else - LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); - UNITY_BEGIN(); - UNITY_END(); + LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); + UNITY_BEGIN(); + UNITY_END(); #endif } #else -void setup() -{ - initializeTestEnvironment(); - LOG_WARN("This test requires the ARCH_PORTDUINO variant"); - UNITY_BEGIN(); - UNITY_END(); +void setup() { + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant"); + UNITY_BEGIN(); + UNITY_END(); } #endif void loop() {} diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index 0c1ef6967..0a6ff134a 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -26,9 +26,9 @@ // Status // #define LED_PIN 1 // External notification -// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the -// app/preferences -// #define EXT_NOTIFY_OUT 2 // The GPIO pin that acts as the external notification output (here we connect an LED to it) +// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in +// the app/preferences #define EXT_NOTIFY_OUT 2 // The GPIO pin that acts as the external notification output (here we +// connect an LED to it) // Buzzer #define PIN_BUZZER 19 @@ -73,8 +73,8 @@ #define BATTERY_PIN 34 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO34_CHANNEL -#define ADC_ATTENUATION \ - ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32 +#define ADC_ATTENUATION \ + ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32 // ESP32-S2/C3/S3 are different // lower dB for lower voltage rnage #define ADC_MULTIPLIER 5.0 // VBATT---10k--pin34---2.5K---GND @@ -108,19 +108,20 @@ #define LORA_CS SX126X_CS // FIXME: for some reason both are used in /src -// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an RF95, just -// that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so cause confusion to those -// adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no need to define LORA_DIO0 is not used -// in src (as we are not using RF95) as SX1262 does not have it per SX1262 datasheet, so no need to define -// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other modules -// then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* definitions when -// the RF95 isn't used -#define LORA_DIO1 \ - SX126X_DIO1 // The old name is used in - // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, so - // must also define the old name -// LORA_DIO2 value is never used in src (as we are not using RF95), so no need to define, and if DIO2_AS_RF_SWITCH is set then it -// cannot serve any extra function even if requested to LORA_DIO3 value is never used in src (as we are not using RF95), so no -// need to define, and DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if requested to (from 13.3.2.1 -// DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch or the TCXO, the IRQ will not be -// generated even if it is mapped to the pins.) +// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an +// RF95, just that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so +// cause confusion to those adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no +// need to define LORA_DIO0 is not used in src (as we are not using RF95) as SX1262 does not have it per SX1262 +// datasheet, so no need to define +// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other +// modules then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* +// definitions when the RF95 isn't used +#define LORA_DIO1 \ + SX126X_DIO1 // The old name is used in + // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, + // so must also define the old name +// LORA_DIO2 value is never used in src (as we are not using RF95), so no need to define, and if DIO2_AS_RF_SWITCH is +// set then it cannot serve any extra function even if requested to LORA_DIO3 value is never used in src (as we are not +// using RF95), so no need to define, and DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if +// requested to (from 13.3.2.1 DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch +// or the TCXO, the IRQ will not be generated even if it is mapped to the pins.) diff --git a/variants/esp32/diy/dr-dev/variant.h b/variants/esp32/diy/dr-dev/variant.h index 35b18ee74..8e516062d 100644 --- a/variants/esp32/diy/dr-dev/variant.h +++ b/variants/esp32/diy/dr-dev/variant.h @@ -35,9 +35,9 @@ #define LORA_DIO2 22 // BUSY for SX1262/SX1268 // NOT_A_PIN is treated as RADIOLIB_NC due to how they are defined, best to use RADIOLIB_NC directly #define LORA_TXEN RADIOLIB_NC // Input - RF switch TX control, connecting external MCU IO or DIO2, valid in high level -// E22_TXEN_CONNECTED_TO_DIO2 wasn't defined, so RXEN wasn't controlled. Commented it out to maintain behavior, but shouldn't be. -// Need to comment out defining SX126X_RXEN as LORA_RXEN too -// #define LORA_RXEN 17 // Input - RF switch RX control, connecting external MCU IO, valid in high level +// E22_TXEN_CONNECTED_TO_DIO2 wasn't defined, so RXEN wasn't controlled. Commented it out to maintain behavior, but +// shouldn't be. Need to comment out defining SX126X_RXEN as LORA_RXEN too #define LORA_RXEN 17 // Input - RF switch RX +// control, connecting external MCU IO, valid in high level #undef LORA_CS #define LORA_CS 16 #define SX126X_BUSY 22 diff --git a/variants/esp32/diy/hydra/variant.h b/variants/esp32/diy/hydra/variant.h index e5c10e26b..c82fff55b 100644 --- a/variants/esp32/diy/hydra/variant.h +++ b/variants/esp32/diy/hydra/variant.h @@ -20,8 +20,9 @@ // Radio #define USE_SX1262 // E22-900M30S uses SX1262 #define USE_SX1268 // E22-400M30S uses SX1268 -#define SX126X_MAX_POWER \ - 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA) +#define SX126X_MAX_POWER \ + 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W + // PA) #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V #define SX126X_CS 18 // EBYTE module's NSS pin diff --git a/variants/esp32/heltec_wireless_bridge/variant.h b/variants/esp32/heltec_wireless_bridge/variant.h index 5ad16d0e2..66b69dc91 100644 --- a/variants/esp32/heltec_wireless_bridge/variant.h +++ b/variants/esp32/heltec_wireless_bridge/variant.h @@ -38,4 +38,5 @@ // user button is present on device, but currently untested & unconfigured - couldn't figure out how it's connected -// battery support is present within device, but currently untested & unconfigured - couldn't find reliable information yet +// battery support is present within device, but currently untested & unconfigured - couldn't find reliable information +// yet diff --git a/variants/esp32/nano-g1-explorer/variant.h b/variants/esp32/nano-g1-explorer/variant.h index f3640241a..1431f1f8b 100644 --- a/variants/esp32/nano-g1-explorer/variant.h +++ b/variants/esp32/nano-g1-explorer/variant.h @@ -28,8 +28,8 @@ // Not really an E22 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) #endif #define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/esp32/nano-g1/variant.h b/variants/esp32/nano-g1/variant.h index 2521c3ffe..b53f4019e 100644 --- a/variants/esp32/nano-g1/variant.h +++ b/variants/esp32/nano-g1/variant.h @@ -28,8 +28,8 @@ // Not really an E22 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) #endif // different screen diff --git a/variants/esp32/radiomaster_900_bandit/variant.h b/variants/esp32/radiomaster_900_bandit/variant.h index 0c7417cac..8b6ab4ea2 100644 --- a/variants/esp32/radiomaster_900_bandit/variant.h +++ b/variants/esp32/radiomaster_900_bandit/variant.h @@ -59,10 +59,10 @@ #define NEOPIXEL_DATA 15 // GPIO pin used to send data to the neopixels #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // Type of neopixels in use #define ENABLE_AMBIENTLIGHTING // Turn on Ambient Lighting -// #define BUTTON1_COLOR 0xFF0000 // Background light for Button 1 in HEX RGB Color (RadioMaster Bandit only). -// #define BUTTON1_COLOR_INDEX 0 // NeoPixel Index ID for Button 1 -// #define BUTTON2_COLOR 0x0000FF // Background light for Button 2 in HEX RGB Color (RadioMaster Bandit only). -// #define BUTTON2_COLOR_INDEX 1 // NeoPixel Index ID for Button 2 +// #define BUTTON1_COLOR 0xFF0000 // Background light for Button 1 in HEX RGB Color (RadioMaster Bandit +// only). #define BUTTON1_COLOR_INDEX 0 // NeoPixel Index ID for Button 1 #define BUTTON2_COLOR 0x0000FF +// // Background light for Button 2 in HEX RGB Color (RadioMaster Bandit only). #define BUTTON2_COLOR_INDEX 1 // +// NeoPixel Index ID for Button 2 /* It has 1 x five-way and 2 x normal buttons. diff --git a/variants/esp32/rak11200/variant.h b/variants/esp32/rak11200/variant.h index 01edb8b73..c04cf4dfc 100644 --- a/variants/esp32/rak11200/variant.h +++ b/variants/esp32/rak11200/variant.h @@ -57,8 +57,7 @@ static const uint8_t SCK = 33; #define LORA_RESET WB_IO4 // RST for SX1276, and for SX1262/SX1268 #define LORA_DIO1 WB_IO6 // IRQ for SX1262/SX1268 #define LORA_DIO2 WB_IO5 // BUSY for SX1262/SX1268 -#define LORA_DIO3 \ - RADIOLIB_NC // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled +#define LORA_DIO3 RADIOLIB_NC // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled #undef LORA_SCK #define LORA_SCK SCK diff --git a/variants/esp32/station-g1/variant.h b/variants/esp32/station-g1/variant.h index 6c3a39261..5d3c836a2 100644 --- a/variants/esp32/station-g1/variant.h +++ b/variants/esp32/station-g1/variant.h @@ -26,9 +26,9 @@ #define SX126X_BUSY LORA_DIO2 #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH // Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch -#define SX126X_MAX_POWER \ - 16 // Ensure the PA does not exceed the saturation output power. More - // Info:https://uniteng.com/wiki/doku.php?id=meshtastic:station#rf_design_-_lora_station_edition_g1 +#define SX126X_MAX_POWER \ + 16 // Ensure the PA does not exceed the saturation output power. More + // Info:https://uniteng.com/wiki/doku.php?id=meshtastic:station#rf_design_-_lora_station_edition_g1 #endif #define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index 2d144a888..eaa3a4268 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -11,8 +11,8 @@ #define LED_STATE_ON 0 // State when LED is lit #define LED_PIN 4 // Newer tbeams (1.1) have an extra led on GPIO4 -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if -// not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for +// RF95 and if not found then probe for SX1262 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1268 @@ -31,8 +31,8 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) #endif // Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts diff --git a/variants/esp32/tlora_v2/variant.h b/variants/esp32/tlora_v2/variant.h index 8a7cf89ec..92281e5de 100644 --- a/variants/esp32/tlora_v2/variant.h +++ b/variants/esp32/tlora_v2/variant.h @@ -6,9 +6,9 @@ #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost #define LED_PIN 25 // If defined we will blink this LED -#define BUTTON_PIN \ - 0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire one - // between this pin and ground +#define BUTTON_PIN \ + 0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire + // one between this pin and ground #define BUTTON_NEED_PULLUP #define USE_RF95 diff --git a/variants/esp32/wiphone/variant.h b/variants/esp32/wiphone/variant.h index 619ac622a..c4ce89c28 100644 --- a/variants/esp32/wiphone/variant.h +++ b/variants/esp32/wiphone/variant.h @@ -35,17 +35,15 @@ #define ST7789_CS 5 #define ST7789_RS 26 #define USE_TFTDISPLAY 1 -// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way to control -// it) -// #define ST7789_BL -1 // EXTENDER_PIN(9) +// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way +// to control it) #define ST7789_BL -1 // EXTENDER_PIN(9) #define ST7789_RESET -1 #define ST7789_MISO 19 #define ST7789_BUSY -1 #define ST7789_SPI_HOST SPI3_HOST -// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way to control -// it) -// #define TFT_BL -1 // EXTENDER_PIN(9) +// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way +// to control it) #define TFT_BL -1 // EXTENDER_PIN(9) #define SPI_FREQUENCY 40000000 #define SPI_READ_FREQUENCY 16000000 #define TFT_HEIGHT 240 diff --git a/variants/esp32c6/m5stack_unitc6l/variant.cpp b/variants/esp32c6/m5stack_unitc6l/variant.cpp index 8e26b4ab7..71ff9c027 100644 --- a/variants/esp32c6/m5stack_unitc6l/variant.cpp +++ b/variants/esp32c6/m5stack_unitc6l/variant.cpp @@ -22,53 +22,50 @@ #define reversebit(x, y) x ^= (0x01 << y) #define getbit(x, y) ((x) >> (y)&0x01) -void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) -{ - Wire.beginTransmission(addr); - Wire.write(reg); - Wire.endTransmission(); - Wire.requestFrom(addr, 1); - *value = Wire.read(); +void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.endTransmission(); + Wire.requestFrom(addr, 1); + *value = Wire.read(); } /*******************************************************************/ -void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) -{ - Wire.beginTransmission(addr); - Wire.write(reg); - Wire.write(value); - Wire.endTransmission(); +void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(value); + Wire.endTransmission(); } /*******************************************************************/ -void c6l_init() -{ - // P7 LoRa Reset - // P6 RF Switch - // P5 LNA Enable +void c6l_init() { + // P7 LoRa Reset + // P6 RF Switch + // P5 LNA Enable - printf("pi4io_init\n"); - uint8_t in_data; - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data); - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断 - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0 - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志 + printf("pi4io_init\n"); + uint8_t in_data; + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data); + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断 + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0 + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志 - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data); - setbit(in_data, 6); // HIGH - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data); + setbit(in_data, 6); // HIGH + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data); } diff --git a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h index 5da99667b..029bca872 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h +++ b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h @@ -19,9 +19,9 @@ // Battery voltage monitoring - TODO: test, currently untested, copied from T3S3 variant #define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO1_CHANNEL -#define ADC_MULTIPLIER \ - 2.11 // ratio of voltage divider = 2.0 (R10=1M, R13=1M), plus some undervoltage correction - TODO: this was carried over from - // the T3S3, test to see if the undervoltage correction is needed. +#define ADC_MULTIPLIER \ + 2.11 // ratio of voltage divider = 2.0 (R10=1M, R13=1M), plus some undervoltage correction - TODO: this was carried + // over from the T3S3, test to see if the undervoltage correction is needed. // Display - OLED connected via I2C by the default hardware configuration #define HAS_SCREEN 1 @@ -33,8 +33,9 @@ #define UART_TX 43 #define UART_RX 44 -// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. There are no -// pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently untested +// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. +// There are no pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently +// untested #define I2C_SCL1 21 #define I2C_SDA1 10 @@ -51,13 +52,13 @@ #define SX126X_DIO1 33 #define SX126X_DIO2_AS_RF_SWITCH // All switching is performed with DIO2, it is automatically inverted using circuitry. -// CDEBYTE EoRa-S3 uses an XTAL, thus we do not need DIO3 as TCXO voltage reference. Don't define SX126X_DIO3_TCXO_VOLTAGE for -// simplicity rather than defining it as 0. -#define SX126X_MAX_POWER \ - 22 // E22-900MM22S and E22-400MM22S have a raw SX1262 or SX1268 respsectively, they are rated to output up and including 22 - // dBm out of their SX126x IC. +// CDEBYTE EoRa-S3 uses an XTAL, thus we do not need DIO3 as TCXO voltage reference. Don't define +// SX126X_DIO3_TCXO_VOLTAGE for simplicity rather than defining it as 0. +#define SX126X_MAX_POWER \ + 22 // E22-900MM22S and E22-400MM22S have a raw SX1262 or SX1268 respsectively, they are rated to output up and + // including 22 dBm out of their SX126x IC. -// Compatibility with old variant.h file structure - FIXME: this should be done in the respective radio interface modules to clean -// up all variants. +// Compatibility with old variant.h file structure - FIXME: this should be done in the respective radio interface +// modules to clean up all variants. #define LORA_CS SX126X_CS #define LORA_DIO1 SX126X_DIO1 \ No newline at end of file diff --git a/variants/esp32s3/EBYTE_ESP32-S3/variant.h b/variants/esp32s3/EBYTE_ESP32-S3/variant.h index 80fb26434..fe667e060 100644 --- a/variants/esp32s3/EBYTE_ESP32-S3/variant.h +++ b/variants/esp32s3/EBYTE_ESP32-S3/variant.h @@ -1,30 +1,32 @@ // Supporting information: https://github.com/S5NC/EBYTE_ESP32-S3/ // Originally developed for E22-900M30S with ESP32-S3-WROOM-1-N4 -// NOTE: Uses ESP32-S3-WROOM-1-N4.json in boards folder (via platformio.ini board field), assumes 4 MB (quad SPI) flash, no PSRAM +// NOTE: Uses ESP32-S3-WROOM-1-N4.json in boards folder (via platformio.ini board field), assumes 4 MB (quad SPI) flash, +// no PSRAM // FIXME: implement SX12 module type autodetection and have setup for each case (add E32 support) -// E32 has same pinout except having extra pins. I assume that the GND on it is connected internally to other GNDs so it is not a -// problem to NC the extra GND pins. +// E32 has same pinout except having extra pins. I assume that the GND on it is connected internally to other GNDs so it +// is not a problem to NC the extra GND pins. // For each EBYTE module pin in this section, provide the pin number of the ESP32-S3 you connected it to -// The ESP32-S3 is great because YOU CAN USE PRACTICALLY ANY PINS for the connections, but avoid some pins (such as on the WROOM -// modules the following): strapping pins (except 0 as a user button input as it already has a pulldown resistor in typical -// application schematic) (0, 3, 45, 46), USB-reserved (19, 20), and pins which aren't present on the WROOM-2 module for -// compatiblity as it uses octal SPI, or are likely connected internally in either WROOM version (26-37), and avoid pins whose -// voltages are set by the SPI voltage (47, 48), and pins that don't exist (22-25) You can ALSO set the SPI pins (SX126X_CS, -// SX126X_SCK, SX126X_MISO, SX126X_MOSI) to any pin with the ESP32-S3 due to \ GPIO Matrix / IO MUX / RTC IO MUX \, and also the -// serial pins, but this isn't recommended for Serial0 as the WROOM modules have a 499 Ohm resistor on U0TXD (to reduce harmonics -// but also acting as a sort of protection) +// The ESP32-S3 is great because YOU CAN USE PRACTICALLY ANY PINS for the connections, but avoid some pins (such as on +// the WROOM modules the following): strapping pins (except 0 as a user button input as it already has a pulldown +// resistor in typical application schematic) (0, 3, 45, 46), USB-reserved (19, 20), and pins which aren't present on +// the WROOM-2 module for compatiblity as it uses octal SPI, or are likely connected internally in either WROOM version +// (26-37), and avoid pins whose voltages are set by the SPI voltage (47, 48), and pins that don't exist (22-25) You can +// ALSO set the SPI pins (SX126X_CS, SX126X_SCK, SX126X_MISO, SX126X_MOSI) to any pin with the ESP32-S3 due to \ GPIO +// Matrix / IO MUX / RTC IO MUX \, and also the serial pins, but this isn't recommended for Serial0 as the WROOM modules +// have a 499 Ohm resistor on U0TXD (to reduce harmonics but also acting as a sort of protection) -// We have many free pins on the ESP32-S3-WROOM-X-Y module, perhaps it is best to use one of its pins to control TXEN, and use -// DIO2 as an extra interrupt, but right now Meshtastic does not benefit from having another interrupt pin available. +// We have many free pins on the ESP32-S3-WROOM-X-Y module, perhaps it is best to use one of its pins to control TXEN, +// and use DIO2 as an extra interrupt, but right now Meshtastic does not benefit from having another interrupt pin +// available. -// Adding two 0-ohm links on your PCB design so that you can choose between the two modes for controlling the E22's TXEN would -// enable future software to make the most of an extra available interrupt pin +// Adding two 0-ohm links on your PCB design so that you can choose between the two modes for controlling the E22's TXEN +// would enable future software to make the most of an extra available interrupt pin -// Possible improvement: can add extremely low resistance MOSFET to physically toggle power to E22 module when in full sleep (not -// waiting for interrupt)? +// Possible improvement: can add extremely low resistance MOSFET to physically toggle power to E22 module when in full +// sleep (not waiting for interrupt)? // PA stands for Power Amplifier, used when transmitting to increase output power // LNA stands for Low Noise Amplifier, used when \ listening for / receiving \ data to increase sensitivity @@ -42,57 +44,60 @@ #define SX126X_RESET 40 // EBYTE module's NRST pin #define SX126X_BUSY 41 // EBYTE module's BUSY pin #define SX126X_DIO1 42 // EBYTE module's DIO1 pin -// We don't define a pin for SX126X_DIO2 as Meshtastic doesn't use it as an interrupt output, so it is never connected to an MCU -// pin! Also E22 module datasheets say not to connect it to an MCU pin. -// We don't define a pin for SX126X_DIO3 as Meshtastic doesn't use it as an interrupt output, so it is never connected to an MCU -// pin! Also E22 module datasheets say to use it as the TCXO's reference voltage. -// E32 module (which uses SX1276) may not have ability to set TCXO voltage using a DIO pin. +// We don't define a pin for SX126X_DIO2 as Meshtastic doesn't use it as an interrupt output, so it is never connected +// to an MCU pin! Also E22 module datasheets say not to connect it to an MCU pin. We don't define a pin for SX126X_DIO3 +// as Meshtastic doesn't use it as an interrupt output, so it is never connected to an MCU pin! Also E22 module +// datasheets say to use it as the TCXO's reference voltage. E32 module (which uses SX1276) may not have ability to set +// TCXO voltage using a DIO pin. -// The radio module needs to be told whether to enable RX mode or TX mode. Each radio module takes different actions based on -// these values, but generally the path from the antenna to SX1262 is changed from signal output to signal input. Also, if there -// are LNAs (Low-Noise Amplifiers) or PAs (Power Amplifiers) in the output or input paths, their power is also controlled by -// these pins. You should never have both TXEN and RXEN set high, this can cause problems for some radio modules, and is -// commonly referred to as 'undefined behaviour' in datasheets. For the SX1262, you shouldn't connect DIO2 to the MCU. DIO2 is -// an output only, and can be controlled via SPI instructions, the use for this is to save an MCU pin by using the DIO2 pin to -// control the RF switching mode. +// The radio module needs to be told whether to enable RX mode or TX mode. Each radio module takes different actions +// based on these values, but generally the path from the antenna to SX1262 is changed from signal output to signal +// input. Also, if there are LNAs (Low-Noise Amplifiers) or PAs (Power Amplifiers) in the output or input paths, their +// power is also controlled by these pins. You should never have both TXEN and RXEN set high, this can cause problems +// for some radio modules, and is commonly referred to as 'undefined behaviour' in datasheets. For the SX1262, you +// shouldn't connect DIO2 to the MCU. DIO2 is an output only, and can be controlled via SPI instructions, the use for +// this is to save an MCU pin by using the DIO2 pin to control the RF switching mode. // Choose ONLY ONE option from below, comment in/out the '/*'s and '*/'s // SX126X_TXEN is the E22's [SX1262's] TXEN pin, SX126X_RXEN is the E22's [SX1262's] RXEN pin -// Option 1: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin (more -// expensive option hardware-wise, is the 'most proper' way, removes need for routing one/two traces from MCU to RF switching -// pins), however you can't have E22 in low-power 'sleep' mode (TXEN and RXEN both low cannot be achieved this this option). +// Option 1: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin +// (more expensive option hardware-wise, is the 'most proper' way, removes need for routing one/two traces from MCU to +// RF switching pins), however you can't have E22 in low-power 'sleep' mode (TXEN and RXEN both low cannot be achieved +// this this option). /* #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_TXEN RADIOLIB_NC #define SX126X_RXEN RADIOLIB_NC */ -// Option 2: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to MCU pin (cheaper option hardware-wise, -// removes need for routing another trace from MCU to an RF switching pin). +// Option 2: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to MCU pin (cheaper option +// hardware-wise, removes need for routing another trace from MCU to an RF switching pin). // /* #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_TXEN RADIOLIB_NC #define SX126X_RXEN 10 // */ -// Option 3: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to MCU pin (cheaper option hardware-wise, allows for -// ramping up PA before transmission (add/expand on feature yourself in RadioLib) if PA takes a while to stabilise) -// Don't define DIO2_AS_RF_SWITCH because we only use DIO2 or an MCU pin mutually exclusively to connect to E22's TXEN (to prevent -// a short if they are both connected at the same time (suboptimal PCB design) and there's a slight non-neglibible delay and/or -// voltage difference between DIO2 and TXEN). Can use DIO2 as an IRQ (but not in Meshtastic at the moment). +// Option 3: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to MCU pin (cheaper option hardware-wise, +// allows for ramping up PA before transmission (add/expand on feature yourself in RadioLib) if PA takes a while to +// stabilise) Don't define DIO2_AS_RF_SWITCH because we only use DIO2 or an MCU pin mutually exclusively to connect to +// E22's TXEN (to prevent a short if they are both connected at the same time (suboptimal PCB design) and there's a +// slight non-neglibible delay and/or voltage difference between DIO2 and TXEN). Can use DIO2 as an IRQ (but not in +// Meshtastic at the moment). /* #define SX126X_TXEN 9 #define SX126X_RXEN 10 */ // (NOT RECOMMENDED, if need to ramp up PA before transmission, better to use option 3) -// Option 4: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin (more expensive -// option hardware-wise, allows for ramping up PA before transmission (add/expand on feature yourself in RadioLib) if PA takes -// a while to stabilise, removes need for routing another trace from MCU to an RF switching pin, however may mean if in -// RadioLib you don't tell DIO2 to go high to indicate transmission (so the negated output goes to RXEN to turn the LNA off) -// then you may end up enabling E22's TXEN and RXEN pins at the same time whilst you ramp up the PA which is not ideal, -// changing DIO2's switching advance in RadioLib may not even be possible, may be baked into the SX126x). +// Option 4: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin (more +// expensive option hardware-wise, allows for ramping up PA before transmission (add/expand on feature yourself in +// RadioLib) if PA takes a while to stabilise, removes need for routing another trace from MCU to an RF switching pin, +// however may mean if in RadioLib you don't tell DIO2 to go high to indicate transmission (so the negated output goes +// to RXEN to turn the LNA off) then you may end up enabling E22's TXEN and RXEN pins at the same time whilst you ramp +// up the PA which is not ideal, changing DIO2's switching advance in RadioLib may not even be possible, may be baked +// into the SX126x). /* #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_TXEN 9 @@ -103,8 +108,8 @@ #define LED_PIN 1 #define LED_STATE_ON 1 // State when LED is lit // External notification -// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the -// app/preferences +// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in +// the app/preferences #define EXT_NOTIFY_OUT 2 // The GPIO pin that acts as the external notification output (here we connect an LED to it) // Buzzer #define PIN_BUZZER 11 @@ -119,19 +124,18 @@ // Power // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA) -// Respect local regulations! If your E22-900M30S outputs the advertised 30 dBm and you use a 6 dBi antenna, you are at the -// equivalent of 36 EIRP (Effective Isotropic Radiated Power), which in this case is the limit for non-HAM users in the US (4W -// EIRP, at SPECIFIC frequencies). -// In the EU (and UK), as of now, you are allowed 27 dBm ERP which is 29.15 EIRP. -// https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022D0180 +// Respect local regulations! If your E22-900M30S outputs the advertised 30 dBm and you use a 6 dBi antenna, you are at +// the equivalent of 36 EIRP (Effective Isotropic Radiated Power), which in this case is the limit for non-HAM users in +// the US (4W EIRP, at SPECIFIC frequencies). In the EU (and UK), as of now, you are allowed 27 dBm ERP which is 29.15 +// EIRP. https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022D0180 // https://www.legislation.gov.uk/uksi/1999/930/schedule/6/made -// To respect the 29.15 dBm EIRP (at SPECIFIC frequencies, others are lower) EU limit with a 2.5 dBi gain antenna, consulting -// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/power%20testing.txt, assuming 0.1 dBm insertion loss, output 20 dBm from the -// E22-900M30S's SX1262. It is worth noting that if you are in this situation and don't have a HAM license, you may be better off -// with a lower gain antenna, and output the difference as a higher total power input into the antenna, as your EIRP would be the -// same, but you would get a wider angle of coverage. Also take insertion loss and possibly VSWR into account -// (https://www.everythingrf.com/tech-resources/vswr). Please check regulations yourself and check airtime, usage (for example -// whether you are airborne), frequency, and power laws. +// To respect the 29.15 dBm EIRP (at SPECIFIC frequencies, others are lower) EU limit with a 2.5 dBi gain antenna, +// consulting https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/power%20testing.txt, assuming 0.1 dBm insertion loss, +// output 20 dBm from the E22-900M30S's SX1262. It is worth noting that if you are in this situation and don't have a +// HAM license, you may be better off with a lower gain antenna, and output the difference as a higher total power input +// into the antenna, as your EIRP would be the same, but you would get a wider angle of coverage. Also take insertion +// loss and possibly VSWR into account (https://www.everythingrf.com/tech-resources/vswr). Please check regulations +// yourself and check airtime, usage (for example whether you are airborne), frequency, and power laws. #define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice // Display @@ -157,16 +161,15 @@ #define USE_SX1262 // E22-900M30S, E22-900M22S, and E22-900MM22S (not E220!) use SX1262 #define USE_SX1268 // E22-400M30S, E22-400M33S, E22-400M22S, and E22-400MM22S use SX1268 -// The below isn't needed as we directly define SX126X_TXEN and SX126X_RXEN instead of using proxies E22_TXEN and E22_RXEN +// The below isn't needed as we directly define SX126X_TXEN and SX126X_RXEN instead of using proxies E22_TXEN and +// E22_RXEN /* -// FALLBACK: If somehow E22_TXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid SX126X_TXEN -being defined but having no value #if (!defined(E22_TXEN) || !(0 <= E22_TXEN && E22_TXEN <= 48)) #define E22_TXEN RADIOLIB_NC -#endif -// FALLBACK: If somehow E22_RXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid SX126X_RXEN -being defined but having no value #if (!defined(E22_RXEN) || !(0 <= E22_RXEN && E22_RXEN <= 48)) #define E22_RXEN RADIOLIB_NC -#endif -#define SX126X_TXEN E22_TXEN -#define SX126X_RXEN E22_RXEN +// FALLBACK: If somehow E22_TXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid +SX126X_TXEN being defined but having no value #if (!defined(E22_TXEN) || !(0 <= E22_TXEN && E22_TXEN <= 48)) #define +E22_TXEN RADIOLIB_NC #endif +// FALLBACK: If somehow E22_RXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid +SX126X_RXEN being defined but having no value #if (!defined(E22_RXEN) || !(0 <= E22_RXEN && E22_RXEN <= 48)) #define +E22_RXEN RADIOLIB_NC #endif #define SX126X_TXEN E22_TXEN #define SX126X_RXEN E22_RXEN */ // E22 series TCXO voltage is 1.8V per https://www.ebyte.com/en/pdf-down.aspx?id=781 (source @@ -175,19 +178,20 @@ being defined but having no value #if (!defined(E22_RXEN) || !(0 <= E22_RXEN && #define LORA_CS SX126X_CS // FIXME: for some reason both are used in /src -// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an RF95, just -// that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so cause confusion to those -// adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no need to define LORA_DIO0 is not used -// in src (as we are not using RF95) as SX1262 does not have it per SX1262 datasheet, so no need to define -// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other modules -// then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* definitions when -// the RF95 isn't used -#define LORA_DIO1 \ - SX126X_DIO1 // The old name is used in - // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, so - // must also define the old name -// LORA_DIO2 value is never used in src (as we are not using RF95), so no need to define, and if DIO2_AS_RF_SWITCH is set then it -// cannot serve any extra function even if requested to LORA_DIO3 value is never used in src (as we are not using RF95), so no -// need to define, and DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if requested to (from 13.3.2.1 -// DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch or the TCXO, the IRQ will not be -// generated even if it is mapped to the pins.) \ No newline at end of file +// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an +// RF95, just that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so +// cause confusion to those adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no +// need to define LORA_DIO0 is not used in src (as we are not using RF95) as SX1262 does not have it per SX1262 +// datasheet, so no need to define +// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other +// modules then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* +// definitions when the RF95 isn't used +#define LORA_DIO1 \ + SX126X_DIO1 // The old name is used in + // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, + // so must also define the old name LORA_DIO2 value is never used in src (as we are not using RF95), so no + // need to define, and if DIO2_AS_RF_SWITCH is set then it cannot serve any extra function even if + // requested to LORA_DIO3 value is never used in src (as we are not using RF95), so no need to define, and + // DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if requested to (from 13.3.2.1 + // DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch or the TCXO, + // the IRQ will not be generated even if it is mapped to the pins.) \ No newline at end of file diff --git a/variants/esp32s3/dreamcatcher/rfswitch.h b/variants/esp32s3/dreamcatcher/rfswitch.h index 74edb25d1..0ec4b5c40 100644 --- a/variants/esp32s3/dreamcatcher/rfswitch.h +++ b/variants/esp32s3/dreamcatcher/rfswitch.h @@ -5,8 +5,7 @@ // DIO6 -> RFSW1_V2 // DIO7 -> ANT_CTRL_ON + ESP_IO9/LR_GPS_ANT_DC_EN -> RFI_GPS (Bias-T GPS) -static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, - RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 diff --git a/variants/esp32s3/heltec_vision_master_e213/einkDetect.h b/variants/esp32s3/heltec_vision_master_e213/einkDetect.h index 35140db60..f8fe1c576 100644 --- a/variants/esp32s3/heltec_vision_master_e213/einkDetect.h +++ b/variants/esp32s3/heltec_vision_master_e213/einkDetect.h @@ -3,33 +3,32 @@ #include "configuration.h" enum class EInkDetectionResult : uint8_t { - LCMEN213EFC1 = 0, // Initial version - E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) + LCMEN213EFC1 = 0, // Initial version + E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) }; -EInkDetectionResult detectEInk() -{ - // Test 1: Logic of BUSY pin +EInkDetectionResult detectEInk() { + // Test 1: Logic of BUSY pin - // Determines controller IC manufacturer - // Fitipower: busy when LOW - // Solomon Systech: busy when HIGH + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH - // Force display BUSY by holding reset pin active - pinMode(PIN_EINK_RES, OUTPUT); - digitalWrite(PIN_EINK_RES, LOW); + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); - delay(10); + delay(10); - // Read whether pin is HIGH or LOW while busy - pinMode(PIN_EINK_BUSY, INPUT); - bool busyLogic = digitalRead(PIN_EINK_BUSY); + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); - // Test complete. Release pin - pinMode(PIN_EINK_RES, INPUT); + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); - if (busyLogic == LOW) - return EInkDetectionResult::LCMEN213EFC1; - else // busy HIGH - return EInkDetectionResult::E0213A367; + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; } \ No newline at end of file diff --git a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h index 1b1291424..c7bf319f7 100644 --- a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h @@ -25,93 +25,92 @@ #include "buzz.h" // Button feedback #include "einkDetect.h" // Detect display model at runtime -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // Detect E-Ink Model - // ------------------- + // Detect E-Ink Model + // ------------------- - EInkDetectionResult displayModel = detectEInk(); + EInkDetectionResult displayModel = detectEInk(); - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver; + Drivers::EInk *driver; - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) - driver = new Drivers::LCMEN213EFC1; - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 - driver = new Drivers::E0213A367; + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + driver = new Drivers::E0213A367; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) - inkhud->setDisplayResilience(10, 1.5); - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 - inkhud->setDisplayResilience(15, 3); + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + inkhud->setDisplayResilience(15, 3); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button - buttons->setWiring(1, PIN_BUTTON2); - buttons->setHandlerShortPress(1, [inkhud]() { - inkhud->nextTile(); - playChirp(); - }); + // #1: Aux Button + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playChirp(); + }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h index 61b08c740..d0ea08c12 100644 --- a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h @@ -37,78 +37,77 @@ Different NicheGraphics UIs and different hardware variants will each have their // Button feedback #include "buzz.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::DEPG0290BNS800; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(7, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button - buttons->setWiring(1, PIN_BUTTON2); - buttons->setHandlerShortPress(1, [inkhud]() { - inkhud->nextTile(); - playChirp(); - }); + // #1: Aux Button + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playChirp(); + }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_paper/einkDetect.h b/variants/esp32s3/heltec_wireless_paper/einkDetect.h index 93b3f86e3..594dffc74 100644 --- a/variants/esp32s3/heltec_wireless_paper/einkDetect.h +++ b/variants/esp32s3/heltec_wireless_paper/einkDetect.h @@ -3,33 +3,32 @@ #include "configuration.h" enum class EInkDetectionResult : uint8_t { - LCMEN213EFC1 = 0, // V1.1 - E0213A367 = 1, // V1.1.1, V1.2 + LCMEN213EFC1 = 0, // V1.1 + E0213A367 = 1, // V1.1.1, V1.2 }; -EInkDetectionResult detectEInk() -{ - // Test 1: Logic of BUSY pin +EInkDetectionResult detectEInk() { + // Test 1: Logic of BUSY pin - // Determines controller IC manufacturer - // Fitipower: busy when LOW - // Solomon Systech: busy when HIGH + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH - // Force display BUSY by holding reset pin active - pinMode(PIN_EINK_RES, OUTPUT); - digitalWrite(PIN_EINK_RES, LOW); + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); - delay(10); + delay(10); - // Read whether pin is HIGH or LOW while busy - pinMode(PIN_EINK_BUSY, INPUT); - bool busyLogic = digitalRead(PIN_EINK_BUSY); + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); - // Test complete. Release pin - pinMode(PIN_EINK_RES, INPUT); + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); - if (busyLogic == LOW) - return EInkDetectionResult::LCMEN213EFC1; - else // busy HIGH - return EInkDetectionResult::E0213A367; + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; } \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h index 445b57714..aadd52c91 100644 --- a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h +++ b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h @@ -24,87 +24,86 @@ #include "einkDetect.h" // Detect display model at runtime -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // Detect E-Ink Model - // ------------------- + // Detect E-Ink Model + // ------------------- - EInkDetectionResult displayModel = detectEInk(); + EInkDetectionResult displayModel = detectEInk(); - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver; + Drivers::EInk *driver; - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 - driver = new Drivers::LCMEN213EFC1; - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 - driver = new Drivers::E0213A367; + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + driver = new Drivers::E0213A367; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) - inkhud->setDisplayResilience(10, 1.5); - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 - inkhud->setDisplayResilience(15, 3); + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + inkhud->setDisplayResilience(15, 3); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // No aux button on this board + // No aux button on this board - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_tracker/variant.h b/variants/esp32s3/heltec_wireless_tracker/variant.h index 3b19f5afd..6c5fb1ce3 100644 --- a/variants/esp32s3/heltec_wireless_tracker/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker/variant.h @@ -30,9 +30,8 @@ #define USE_TFTDISPLAY 1 // pin 3 is Vext on v1.1 - HIGH enables LDO for Vext rail which goes to: -// GPS UC6580: GPS V_DET(8), VDD_IO(7), DCDC_IN(21), pulls up RESETN(17), D_SEL(33) and BOOT_MODE(34) through 10kR -// GPS LNA SW7125DE: VCC(4), pulls up SHDN(5) through 10kR -// LED: VDD, LEDA (through diode) +// GPS UC6580: GPS V_DET(8), VDD_IO(7), DCDC_IN(21), pulls up RESETN(17), D_SEL(33) and BOOT_MODE(34) through +// 10kR GPS LNA SW7125DE: VCC(4), pulls up SHDN(5) through 10kR LED: VDD, LEDA (through diode) #define VEXT_ENABLE 3 // active HIGH - powers the GPS, GPS LNA and OLED #define VEXT_ON_VALUE HIGH @@ -51,8 +50,8 @@ #define GPS_TX_PIN 34 #define PIN_GPS_RESET 35 #define PIN_GPS_PPS 36 -// #define PIN_GPS_EN 3 // Uncomment to power off the GPS with triple-click on Tracker v1.1, though we'll also lose the -// display. +// #define PIN_GPS_EN 3 // Uncomment to power off the GPS with triple-click on Tracker v1.1, though we'll also lose +// the display. #define GPS_RESET_MODE LOW #define GPS_UC6580 diff --git a/variants/esp32s3/picomputer-s3/variant.h b/variants/esp32s3/picomputer-s3/variant.h index 275da1b61..b594de899 100644 --- a/variants/esp32s3/picomputer-s3/variant.h +++ b/variants/esp32s3/picomputer-s3/variant.h @@ -56,11 +56,7 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { \ - 44, 47, 17, 15, 13, 41 \ - } -#define KEYS_ROWS \ - { \ - 12, 16, 42, 18, 14, 7 \ - } +#define KEYS_COLS \ + { 44, 47, 17, 15, 13, 41 } +#define KEYS_ROWS \ + { 12, 16, 42, 18, 14, 7 } diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index f946528ae..c22aeb26b 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -13,9 +13,8 @@ // #define BUTTON_NEED_PULLUP -// #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage -// #define ADC_CHANNEL ADC1_GPIO27_CHANNEL -// #define ADC_MULTIPLIER 2 +// #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery +// voltage #define ADC_CHANNEL ADC1_GPIO27_CHANNEL #define ADC_MULTIPLIER 2 // ST7701 TFT LCD #define ST7701_CS (4 | IO_EXPANDER) @@ -45,7 +44,8 @@ #define TOUCH_I2C_PORT 0 #define TOUCH_SLAVE_ADDRESS 0x48 -// in future, we may want to add a buzzer and add all sensors to the indicator via a data protocol for now only GPS is supported +// in future, we may want to add a buzzer and add all sensors to the indicator via a data protocol for now only GPS is +// supported // // Buzzer // #define PIN_BUZZER 19 diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index 36a1310f1..933337f6d 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -90,8 +90,8 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 2.4 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) #define MODEM_POWER_EN 41 #define MODEM_PWRKEY 40 diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index 8d2996131..ca9c588bd 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -107,5 +107,5 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) diff --git a/variants/esp32s3/t-eth-elite/rfswitch.h b/variants/esp32s3/t-eth-elite/rfswitch.h index 589f24767..9332cb747 100644 --- a/variants/esp32s3/t-eth-elite/rfswitch.h +++ b/variants/esp32s3/t-eth-elite/rfswitch.h @@ -4,8 +4,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/esp32s3/t-eth-elite/variant.h b/variants/esp32s3/t-eth-elite/variant.h index b7ac05872..f8e3bf8c5 100644 --- a/variants/esp32s3/t-eth-elite/variant.h +++ b/variants/esp32s3/t-eth-elite/variant.h @@ -17,8 +17,8 @@ #define BUTTON_NEED_PULLUP -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if -// not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for +// RF95 and if not found then probe for SX1262 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1280 diff --git a/variants/esp32s3/tbeam-s3-core/rfswitch.h b/variants/esp32s3/tbeam-s3-core/rfswitch.h index 19080cec6..66621b2fe 100644 --- a/variants/esp32s3/tbeam-s3-core/rfswitch.h +++ b/variants/esp32s3/tbeam-s3-core/rfswitch.h @@ -4,8 +4,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index 1f900fcae..ffba5bbd5 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -11,8 +11,8 @@ #define LED_STATE_ON 0 // State when LED is lit -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if -// not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for +// RF95 and if not found then probe for SX1262 #define USE_SX1262 #define USE_SX1268 #define USE_LR1121 @@ -31,8 +31,8 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) #endif // LR1121 diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 0fba5a305..8dbbf2286 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -4,8 +4,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, - {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file diff --git a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h index 8f5e63653..362ddf4c4 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h +++ b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h @@ -21,69 +21,68 @@ #include "graphics/niche/Drivers/EInk/DEPG0213BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::DEPG0213BNS800; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::DEPG0213BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the driver - inkhud->setDriver(driver); + // Set the driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(15, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(15, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // Setup the main user button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // Setup the main user button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - buttons->start(); + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/tlora_t3s3_v1/rfswitch.h b/variants/esp32s3/tlora_t3s3_v1/rfswitch.h index 19080cec6..66621b2fe 100644 --- a/variants/esp32s3/tlora_t3s3_v1/rfswitch.h +++ b/variants/esp32s3/tlora_t3s3_v1/rfswitch.h @@ -4,8 +4,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file diff --git a/variants/esp32s3/tlora_t3s3_v1/variant.h b/variants/esp32s3/tlora_t3s3_v1/variant.h index babe44a58..800cfbeee 100644 --- a/variants/esp32s3/tlora_t3s3_v1/variant.h +++ b/variants/esp32s3/tlora_t3s3_v1/variant.h @@ -19,8 +19,8 @@ #define BUTTON_NEED_PULLUP -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if -// not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for +// RF95 and if not found then probe for SX1262 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1280 diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index 2287dfe0b..75f046fd5 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -82,12 +82,8 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { \ - 44, 45, 46, 4, 5, 6 \ - } -#define KEYS_ROWS \ - { \ - 26, 37, 17, 16, 15, 7 \ - } +#define KEYS_COLS \ + { 44, 45, 46, 4, 5, 6 } +#define KEYS_ROWS \ + { 26, 37, 17, 16, 15, 7 } // #end keyboard \ No newline at end of file diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index f42a5b19f..4be54e276 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -106,12 +106,8 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { \ - 44, 45, 46, 4, 5, 6 \ - } -#define KEYS_ROWS \ - { \ - 26, 37, 17, 16, 15, 7 \ - } +#define KEYS_COLS \ + { 44, 45, 46, 4, 5, 6 } +#define KEYS_ROWS \ + { 26, 37, 17, 16, 15, 7 } // #end keyboard \ No newline at end of file diff --git a/variants/esp32s3/tracksenger/oled/variant.h b/variants/esp32s3/tracksenger/oled/variant.h index 85cc019c4..7b9a9e9de 100644 --- a/variants/esp32s3/tracksenger/oled/variant.h +++ b/variants/esp32s3/tracksenger/oled/variant.h @@ -83,12 +83,8 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { \ - 44, 45, 46, 4, 5, 6 \ - } -#define KEYS_ROWS \ - { \ - 26, 37, 17, 16, 15, 7 \ - } +#define KEYS_COLS \ + { 44, 45, 46, 4, 5, 6 } +#define KEYS_ROWS \ + { 26, 37, 17, 16, 15, 7 } // #end keyboard \ No newline at end of file diff --git a/variants/esp32s3/unphone/variant.cpp b/variants/esp32s3/unphone/variant.cpp index 7884f82e3..c4811cecb 100644 --- a/variants/esp32s3/unphone/variant.cpp +++ b/variants/esp32s3/unphone/variant.cpp @@ -3,19 +3,18 @@ #include "unPhone.h" unPhone unphone = unPhone("meshtastic_unphone"); -void initVariant() -{ - unphone.begin(); // initialise hardware etc. - unphone.store(unphone.buildTime); - unphone.printWakeupReason(); // what woke us up? (stored, not printed :|) - unphone.checkPowerSwitch(); // if power switch is off, shutdown - unphone.backlight(false); // setup backlight and make sure its off - unphone.expanderPower(true); // enable power to expander / hat / sheild +void initVariant() { + unphone.begin(); // initialise hardware etc. + unphone.store(unphone.buildTime); + unphone.printWakeupReason(); // what woke us up? (stored, not printed :|) + unphone.checkPowerSwitch(); // if power switch is off, shutdown + unphone.backlight(false); // setup backlight and make sure its off + unphone.expanderPower(true); // enable power to expander / hat / sheild - for (int i = 0; i < 3; i++) { // buzz a bit - unphone.vibe(true); - delay(150); - unphone.vibe(false); - delay(150); - } + for (int i = 0; i < 3; i++) { // buzz a bit + unphone.vibe(true); + delay(150); + unphone.vibe(false); + delay(150); + } } \ No newline at end of file diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h index 6f0710d62..8f9322ea7 100644 --- a/variants/esp32s3/unphone/variant.h +++ b/variants/esp32s3/unphone/variant.h @@ -45,10 +45,10 @@ #define USE_POWERSAVE #define SLEEP_TIME 180 -#define HAS_GPS \ - 0 // the unphone doesn't have a gps module by default (though - // GPS featherwing -- https://www.adafruit.com/product/3133 - // -- can be added) +#define HAS_GPS \ + 0 // the unphone doesn't have a gps module by default (though + // GPS featherwing -- https://www.adafruit.com/product/3133 + // -- can be added) #undef GPS_RX_PIN #undef GPS_TX_PIN diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp index 2fc87c718..1226397f1 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp @@ -27,9 +27,8 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); } \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h index f64de9d07..77ddb38a1 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -25,94 +25,93 @@ // Button feedback #include "buzz.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::GDEY0154D67; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - // Todo: observe the display's performance in-person and adjust accordingly. - // Currently set to the values given by Elecrow for EInkDynamicDisplay. - inkhud->setDisplayResilience(10, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + // Todo: observe the display's performance in-person and adjust accordingly. + // Currently set to the values given by Elecrow for EInkDynamicDisplay. + inkhud->setDisplayResilience(10, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - // Setup backlight controller - // Note: button is attached further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + // Setup backlight controller + // Note: button is attached further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // Elecrow diagram: https://www.elecrow.com/download/product/CIL12901M/ThinkNode-M1_User_Manual.pdf + // Elecrow diagram: https://www.elecrow.com/download/product/CIL12901M/ThinkNode-M1_User_Manual.pdf - // #0: Main User Button - // Labeled "Page Turn Button" by manual - buttons->setWiring(0, PIN_BUTTON2); - buttons->setTiming(0, 50, 500); // Todo: confirm 50ms is adequate debounce - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + // Labeled "Page Turn Button" by manual + buttons->setWiring(0, PIN_BUTTON2); + buttons->setTiming(0, 50, 500); // Todo: confirm 50ms is adequate debounce + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button - // Labeled "Function Button" by manual - // Todo: additional features - buttons->setWiring(1, PIN_BUTTON1); - buttons->setTiming(1, 50, 500); // 500ms before latch - buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); - buttons->setHandlerLongPress(1, [backlight]() { - backlight->latch(); - playBoop(); - }); - buttons->setHandlerShortPress(1, [backlight]() { - backlight->off(); - playChirp(); - }); + // #1: Aux Button + // Labeled "Function Button" by manual + // Todo: additional features + buttons->setWiring(1, PIN_BUTTON1); + buttons->setTiming(1, 50, 500); // 500ms before latch + buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); + buttons->setHandlerLongPress(1, [backlight]() { + backlight->latch(); + playBoop(); + }); + buttons->setHandlerShortPress(1, [backlight]() { + backlight->off(); + playChirp(); + }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index cae079b74..4454a1d84 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -30,15 +30,14 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index cde0f49c1..03a7d11bb 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -117,8 +117,8 @@ External serial flash WP25R1635FZUIL0 #define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 20) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching -// #define SX1262_DIO3 (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not -// drive from the main +// #define SX1262_DIO3 (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the +// tcxo, do not drive from the main #define SX126X_BUSY (0 + 17) #define SX126X_RESET (0 + 25) #define SX126X_DIO2_AS_RF_SWITCH diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h b/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h index 77ae9ef73..f1fd32ea0 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h @@ -8,8 +8,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp index 9769e3edd..b42c1273c 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp @@ -31,74 +31,71 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - pinMode(KEY_POWER, OUTPUT); - digitalWrite(KEY_POWER, HIGH); - pinMode(RGB_POWER, OUTPUT); - digitalWrite(RGB_POWER, HIGH); - pinMode(green_LED_PIN, OUTPUT); - digitalWrite(green_LED_PIN, LED_STATE_OFF); - pinMode(LED_BLUE, OUTPUT); - pinMode(PIN_POWER_USB, INPUT); - pinMode(PIN_POWER_DONE, INPUT); - pinMode(PIN_POWER_CHRG, INPUT); - pinMode(BUTTON_PIN, INPUT_PULLUP); - pinMode(EEPROM_POWER, OUTPUT); - digitalWrite(EEPROM_POWER, HIGH); - pinMode(PIN_EN1, OUTPUT); - digitalWrite(PIN_EN1, HIGH); - pinMode(PIN_EN2, OUTPUT); - digitalWrite(PIN_EN2, HIGH); - pinMode(ACC_POWER, OUTPUT); - digitalWrite(ACC_POWER, LOW); - pinMode(DHT_POWER, OUTPUT); - digitalWrite(DHT_POWER, HIGH); - pinMode(Battery_POWER, OUTPUT); - digitalWrite(Battery_POWER, HIGH); - pinMode(GPS_POWER, OUTPUT); - digitalWrite(GPS_POWER, HIGH); +void initVariant() { + pinMode(KEY_POWER, OUTPUT); + digitalWrite(KEY_POWER, HIGH); + pinMode(RGB_POWER, OUTPUT); + digitalWrite(RGB_POWER, HIGH); + pinMode(green_LED_PIN, OUTPUT); + digitalWrite(green_LED_PIN, LED_STATE_OFF); + pinMode(LED_BLUE, OUTPUT); + pinMode(PIN_POWER_USB, INPUT); + pinMode(PIN_POWER_DONE, INPUT); + pinMode(PIN_POWER_CHRG, INPUT); + pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(EEPROM_POWER, OUTPUT); + digitalWrite(EEPROM_POWER, HIGH); + pinMode(PIN_EN1, OUTPUT); + digitalWrite(PIN_EN1, HIGH); + pinMode(PIN_EN2, OUTPUT); + digitalWrite(PIN_EN2, HIGH); + pinMode(ACC_POWER, OUTPUT); + digitalWrite(ACC_POWER, LOW); + pinMode(DHT_POWER, OUTPUT); + digitalWrite(DHT_POWER, HIGH); + pinMode(Battery_POWER, OUTPUT); + digitalWrite(Battery_POWER, HIGH); + pinMode(GPS_POWER, OUTPUT); + digitalWrite(GPS_POWER, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function -void variant_shutdown() -{ - digitalWrite(red_LED_PIN, HIGH); - digitalWrite(green_LED_PIN, HIGH); - digitalWrite(LED_BLUE, HIGH); +void variant_shutdown() { + digitalWrite(red_LED_PIN, HIGH); + digitalWrite(green_LED_PIN, HIGH); + digitalWrite(LED_BLUE, HIGH); - digitalWrite(PIN_EN1, LOW); - digitalWrite(PIN_EN2, LOW); - digitalWrite(EEPROM_POWER, LOW); - digitalWrite(KEY_POWER, LOW); - digitalWrite(DHT_POWER, LOW); - digitalWrite(ACC_POWER, LOW); - digitalWrite(Battery_POWER, LOW); - digitalWrite(GPS_POWER, LOW); + digitalWrite(PIN_EN1, LOW); + digitalWrite(PIN_EN2, LOW); + digitalWrite(EEPROM_POWER, LOW); + digitalWrite(KEY_POWER, LOW); + digitalWrite(DHT_POWER, LOW); + digitalWrite(ACC_POWER, LOW); + digitalWrite(Battery_POWER, LOW); + digitalWrite(GPS_POWER, LOW); - // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. - for (int pin = 0; pin < 48; pin++) { - if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || - pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || - pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || - pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == green_LED_PIN || - pin == red_LED_PIN || pin == LED_BLUE) { - continue; - } - pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); - if (pin >= 32) { - NRF_P1->DIRCLR = (1 << (pin - 32)); - } else { - NRF_GPIO->DIRCLR = (1 << pin); - } + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || pin == ACC_POWER || + pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || + pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || + pin == GPS_RX_PIN || pin == green_LED_PIN || pin == red_LED_PIN || pin == LED_BLUE) { + continue; } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } - nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(BUTTON_PIN, sense1); + nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(BUTTON_PIN, sense1); - nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH; - nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2); + nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH; + nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2); } \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index 9c7b521ef..d271c1c40 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -30,41 +30,38 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - pinMode(LED_CHARGE, OUTPUT); - ledOff(LED_CHARGE); +void initVariant() { + pinMode(LED_CHARGE, OUTPUT); + ledOff(LED_CHARGE); - pinMode(LED_PAIRING, OUTPUT); - ledOff(LED_PAIRING); + pinMode(LED_PAIRING, OUTPUT); + ledOff(LED_PAIRING); - pinMode(VDD_FLASH_EN, OUTPUT); - digitalWrite(VDD_FLASH_EN, HIGH); + pinMode(VDD_FLASH_EN, OUTPUT); + digitalWrite(VDD_FLASH_EN, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function -void variant_shutdown() -{ - // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. - for (int pin = 0; pin < 48; pin++) { - if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || - pin == PIN_SPI_SCK) { - continue; - } - pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); - if (pin >= 32) { - NRF_P1->DIRCLR = (1 << (pin - 32)); - } else { - NRF_GPIO->DIRCLR = (1 << pin); - } +void variant_shutdown() { + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || pin == PIN_SPI_SCK) { + continue; } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } - digitalWrite(PIN_GPS_EN, LOW); - digitalWrite(ADC_CTRL, LOW); - // digitalWrite(RTC_POWER, LOW); + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(ADC_CTRL, LOW); + // digitalWrite(RTC_POWER, LOW); - nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); } diff --git a/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h b/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h index cda6364f5..8f753df83 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h +++ b/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h @@ -8,8 +8,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp index 35dc1d39b..747bb14de 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp @@ -30,11 +30,10 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() { + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h index cda6364f5..8f753df83 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h @@ -8,8 +8,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp index 35dc1d39b..747bb14de 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp @@ -30,11 +30,10 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() { + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp index 8c6bf039c..89f157bfd 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp @@ -27,12 +27,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp index 8c6bf039c..89f157bfd 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp @@ -27,12 +27,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf52840/TWC_mesh_v4/variant.cpp b/variants/nrf52840/TWC_mesh_v4/variant.cpp index b3712346d..cad7c6518 100644 --- a/variants/nrf52840/TWC_mesh_v4/variant.cpp +++ b/variants/nrf52840/TWC_mesh_v4/variant.cpp @@ -27,12 +27,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } \ No newline at end of file diff --git a/variants/nrf52840/canaryone/variant.cpp b/variants/nrf52840/canaryone/variant.cpp index 5967a2a96..5a33c1e9a 100644 --- a/variants/nrf52840/canaryone/variant.cpp +++ b/variants/nrf52840/canaryone/variant.cpp @@ -30,27 +30,26 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LEDs - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LEDs + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); - // Turn on power to the GPS and LoRa - pinMode(PIN_PWR_EN, OUTPUT); - digitalWrite(PIN_PWR_EN, HIGH); + // Turn on power to the GPS and LoRa + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); - // Pull the GPS out of reset - pinMode(GPS_RESET_PIN, OUTPUT); - digitalWrite(GPS_RESET_PIN, HIGH); + // Pull the GPS out of reset + pinMode(GPS_RESET_PIN, OUTPUT); + digitalWrite(GPS_RESET_PIN, HIGH); - // Pull the LoRa out of reset - pinMode(LORA_RF_PWR, OUTPUT); - digitalWrite(LORA_RF_PWR, HIGH); + // Pull the LoRa out of reset + pinMode(LORA_RF_PWR, OUTPUT); + digitalWrite(LORA_RF_PWR, HIGH); } diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 61d1e8df9..2cd3f6c8c 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -148,7 +148,8 @@ static const uint8_t A0 = PIN_A0; #define PIN_SPI_MOSI (GPIO_PORT0 + 22) #define PIN_SPI_SCK (GPIO_PORT0 + 19) -// #define PIN_SPI1_MISO (GPIO_PORT1 + 6) // FIXME not really needed, but for now the SPI code requires something to be defined, +// #define PIN_SPI1_MISO (GPIO_PORT1 + 6) // FIXME not really needed, but for now the SPI code requires something to be +// defined, // pick an used GPIO #define PIN_SPI1_MOSI (GPIO_PORT1 + 8) #define PIN_SPI1_SCK (GPIO_PORT1 + 9) #define PIN_PWR_EN (GPIO_PORT0 + 12) diff --git a/variants/nrf52840/cpp_overrides/lfs_util.h b/variants/nrf52840/cpp_overrides/lfs_util.h index bf5a347b1..52c10ab2e 100644 --- a/variants/nrf52840/cpp_overrides/lfs_util.h +++ b/variants/nrf52840/cpp_overrides/lfs_util.h @@ -5,11 +5,11 @@ * SPDX-License-Identifier: BSD-3-Clause */ -// MESHTASTIC/@geeksville note: This file is copied from the Adafruit nrf52 arduino lib. And we use a special -include in -// nrf52.ini to load it before EVERY file we do this hack because the default definitions for LFS_ASSERT are quite poor and we -// don't want to fork the adafruit lib (again) and send in a PR that they probably won't merge anyways. This file might break if -// they ever update lfs.util on their side, in which case we'll need to update this file to match their new version. The version -// this is a copy from is almost exactly +// MESHTASTIC/@geeksville note: This file is copied from the Adafruit nrf52 arduino lib. And we use a special -include +// in nrf52.ini to load it before EVERY file we do this hack because the default definitions for LFS_ASSERT are quite +// poor and we don't want to fork the adafruit lib (again) and send in a PR that they probably won't merge anyways. This +// file might break if they ever update lfs.util on their side, in which case we'll need to update this file to match +// their new version. The version this is a copy from is almost exactly // https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/c25d93268a3b9c23e9a1ccfcaf9b208beca624ca/libraries/Adafruit_LittleFS/src/littlefs/lfs_util.h #ifndef LFS_UTIL_H @@ -77,9 +77,9 @@ void logLegacy(const char *level, const char *fmt, ...); #define LFS_ASSERT(test) assert(test) #else extern void lfs_assert(const char *reason); -#define LFS_ASSERT(test) \ - if (!(test)) \ - lfs_assert(#test) +#define LFS_ASSERT(test) \ + if (!(test)) \ + lfs_assert(#test) #endif // Builtin functions, these may be replaced by more efficient @@ -87,116 +87,98 @@ extern void lfs_assert(const char *reason); // expensive basic C implementation for debugging purposes // Min/max functions for unsigned 32-bit numbers -static inline uint32_t lfs_max(uint32_t a, uint32_t b) -{ - return (a > b) ? a : b; -} +static inline uint32_t lfs_max(uint32_t a, uint32_t b) { return (a > b) ? a : b; } -static inline uint32_t lfs_min(uint32_t a, uint32_t b) -{ - return (a < b) ? a : b; -} +static inline uint32_t lfs_min(uint32_t a, uint32_t b) { return (a < b) ? a : b; } // Find the next smallest power of 2 less than or equal to a -static inline uint32_t lfs_npw2(uint32_t a) -{ +static inline uint32_t lfs_npw2(uint32_t a) { #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return 32 - __builtin_clz(a - 1); + return 32 - __builtin_clz(a - 1); #else - uint32_t r = 0; - uint32_t s; - a -= 1; - s = (a > 0xffff) << 4; - a >>= s; - r |= s; - s = (a > 0xff) << 3; - a >>= s; - r |= s; - s = (a > 0xf) << 2; - a >>= s; - r |= s; - s = (a > 0x3) << 1; - a >>= s; - r |= s; - return (r | (a >> 1)) + 1; + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; + a >>= s; + r |= s; + s = (a > 0xff) << 3; + a >>= s; + r |= s; + s = (a > 0xf) << 2; + a >>= s; + r |= s; + s = (a > 0x3) << 1; + a >>= s; + r |= s; + return (r | (a >> 1)) + 1; #endif } // Count the number of trailing binary zeros in a // lfs_ctz(0) may be undefined -static inline uint32_t lfs_ctz(uint32_t a) -{ +static inline uint32_t lfs_ctz(uint32_t a) { #if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) - return __builtin_ctz(a); + return __builtin_ctz(a); #else - return lfs_npw2((a & -a) + 1) - 1; + return lfs_npw2((a & -a) + 1) - 1; #endif } // Count the number of binary ones in a -static inline uint32_t lfs_popc(uint32_t a) -{ +static inline uint32_t lfs_popc(uint32_t a) { #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return __builtin_popcount(a); + return __builtin_popcount(a); #else - a = a - ((a >> 1) & 0x55555555); - a = (a & 0x33333333) + ((a >> 2) & 0x33333333); - return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; #endif } // Find the sequence comparison of a and b, this is the distance // between a and b ignoring overflow -static inline int lfs_scmp(uint32_t a, uint32_t b) -{ - return (int)(unsigned)(a - b); -} +static inline int lfs_scmp(uint32_t a, uint32_t b) { return (int)(unsigned)(a - b); } // Convert from 32-bit little-endian to native order -static inline uint32_t lfs_fromle32(uint32_t a) -{ -#if !defined(LFS_NO_INTRINSICS) && ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || \ - (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ - (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) - return a; -#elif !defined(LFS_NO_INTRINSICS) && \ - ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ +static inline uint32_t lfs_fromle32(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && \ + ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return a; +#elif !defined(LFS_NO_INTRINSICS) && \ + ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) - return __builtin_bswap32(a); + return __builtin_bswap32(a); #else - return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); + return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); #endif } // Convert to 32-bit little-endian from native order -static inline uint32_t lfs_tole32(uint32_t a) -{ - return lfs_fromle32(a); -} +static inline uint32_t lfs_tole32(uint32_t a) { return lfs_fromle32(a); } // Calculate CRC-32 with polynomial = 0x04c11db7 void lfs_crc(uint32_t *crc, const void *buffer, size_t size); // Allocate memory, only used if buffers are not provided to littlefs -static inline void *lfs_malloc(size_t size) -{ +static inline void *lfs_malloc(size_t size) { #ifndef LFS_NO_MALLOC - extern void *pvPortMalloc(size_t xWantedSize); - return pvPortMalloc(size); + extern void *pvPortMalloc(size_t xWantedSize); + return pvPortMalloc(size); #else - (void)size; - return NULL; + (void)size; + return NULL; #endif } // Deallocate memory, only used if buffers are not provided to littlefs -static inline void lfs_free(void *p) -{ +static inline void lfs_free(void *p) { #ifndef LFS_NO_MALLOC - extern void vPortFree(void *pv); - vPortFree(p); + extern void vPortFree(void *pv); + vPortFree(p); #else - (void)p; + (void)p; #endif } diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h index 8f30a244f..661ed29e9 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h @@ -29,67 +29,66 @@ #error If not using a DIY preset, display model and resilience must be set manually #endif -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- - SPI.begin(); + // SPI + // ----------------------------- + SPI.begin(); - // Driver - // ----------------------------- + // Driver + // ----------------------------- - // Use E-Ink driver - Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; - driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the driver - inkhud->setDriver(driver); + // Set the driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update. - inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Init settings, and customize defaults - // Values ignored individually if found saved to flash - inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed - inkhud->persistence->settings.userTiles.maxCount = 4; - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); - inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // Setup the main user button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // Setup the main user button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - buttons->start(); + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h index 71508c037..f0994b39c 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -8,8 +8,7 @@ // DIO6 -> RFSW1_V2 // DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. -static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, - RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp index 5869ed1d4..c298b73bb 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp @@ -30,9 +30,8 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); +void initVariant() { + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h index 63af1fe79..8bd65bd57 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h @@ -136,8 +136,8 @@ NRF52 PRO MICRO PIN ASSIGNMENT // SX126X CONFIG #define SX126X_CS (32 + 13) // P1.13 FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 10) // P0.10 IRQ -#define SX126X_DIO2_AS_RF_SWITCH // Note for E22 modules: DIO2 is not attached internally to TXEN for automatic TX/RX switching, - // so it needs connecting externally if it is used in this way +#define SX126X_DIO2_AS_RF_SWITCH // Note for E22 modules: DIO2 is not attached internally to TXEN for automatic TX/RX + // switching, so it needs connecting externally if it is used in this way #define SX126X_BUSY (0 + 29) // P0.29 #define SX126X_RESET (0 + 9) // P0.09 #define SX126X_RXEN (0 + 17) // P0.17 @@ -159,8 +159,8 @@ NRF52 PRO MICRO PIN ASSIGNMENT // #define SX126X_MAX_POWER 8 set this if using a high-power board! /* -On the SX1262, DIO3 sets the voltage for an external TCXO, if one is present. If one is not present, use TCXO_OPTIONAL to try both -settings. +On the SX1262, DIO3 sets the voltage for an external TCXO, if one is present. If one is not present, use TCXO_OPTIONAL +to try both settings. | Mfr | Module | TCXO | RF Switch | Notes | | ------------ | ---------------- | ---- | --------- | ------------------------------------- | diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp index 300f69d0b..2eb468363 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp @@ -54,9 +54,8 @@ const uint32_t g_ADigitalPinMap[] = { 31, // D32 is P0.10 (VBAT) }; -void initVariant() -{ - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); +void initVariant() { + // Set BQ25101 ISET to 100mA instead of 50mA + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); } diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h index 6337ac70c..c33e11654 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h @@ -195,8 +195,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -232,11 +232,9 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_BAUDRATE 9600 diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h index b6be70ff4..6ec89a0fd 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h @@ -30,67 +30,66 @@ #error If not using a DIY preset, display model and resilience must be set manually #endif -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- - SPI1.begin(); + // SPI + // ----------------------------- + SPI1.begin(); - // Driver - // ----------------------------- + // Driver + // ----------------------------- - // Use E-Ink driver - Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the driver - inkhud->setDriver(driver); + // Set the driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update. - inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Init settings, and customize defaults - // Values ignored individually if found saved to flash - inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed - inkhud->persistence->settings.userTiles.maxCount = 4; - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); - inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - buttons->start(); + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp index 85c9f4a72..eb70fe5e9 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp @@ -30,9 +30,8 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); } diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h index 14170d5f3..26f4d364f 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h @@ -1,5 +1,6 @@ // Unlike many other InkHUD variants, this environment does require its own variant.h file -// This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken out +// This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken +// out #ifndef _VARIANT_HELTEC_NRF_ #define _VARIANT_HELTEC_NRF_ @@ -42,8 +43,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular -// GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a +// regular GPIO /* No longer populated on PCB diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp index 85c9f4a72..eb70fe5e9 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp @@ -30,9 +30,8 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); } diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index bad488b35..e352530a6 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -86,8 +86,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular -// GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a +// regular GPIO /* No longer populated on PCB @@ -145,8 +145,7 @@ No longer populated on PCB #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define PIN_SPI1_MISO \ - ST7789_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MISO ST7789_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO #define PIN_SPI1_MOSI ST7789_SDA #define PIN_SPI1_SCK ST7789_SCK diff --git a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h index f8202debb..a8a6c6519 100644 --- a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h @@ -21,70 +21,69 @@ #include "graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h" #include "graphics/niche/Inputs/TwoButton.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::LCMEN2R13ECC1; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::LCMEN2R13ECC1; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(10, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = true; + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_pocket/variant.h b/variants/nrf52840/heltec_mesh_pocket/variant.h index 7ec9b88ea..8133583c2 100644 --- a/variants/nrf52840/heltec_mesh_pocket/variant.h +++ b/variants/nrf52840/heltec_mesh_pocket/variant.h @@ -34,8 +34,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular -// GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a +// regular GPIO /* No longer populated on PCB diff --git a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h index 125f50590..5f6ec3146 100644 --- a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h @@ -21,70 +21,69 @@ #include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Inputs/TwoButton.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::E0213A367; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::E0213A367; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(10, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = true; + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp index c13f006d7..915015cb5 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.cpp +++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp @@ -30,11 +30,10 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); +void initVariant() { + pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); #if defined(PIN_SCREEN_VDD_CTL) - pinMode(PIN_SCREEN_VDD_CTL, OUTPUT); - digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on + pinMode(PIN_SCREEN_VDD_CTL, OUTPUT); + digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on #endif } diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h index 112bcd8b3..0fe7528ba 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.h +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -54,8 +54,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular -// GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a +// regular GPIO /* No longer populated on PCB diff --git a/variants/nrf52840/meshlink/variant.cpp b/variants/nrf52840/meshlink/variant.cpp index 81a5097c4..b0765a547 100644 --- a/variants/nrf52840/meshlink/variant.cpp +++ b/variants/nrf52840/meshlink/variant.cpp @@ -10,14 +10,13 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting - // otherwise it will stay lit for several seconds (could be annoying) +void initVariant() { + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting + // otherwise it will stay lit for several seconds (could be annoying) #ifdef PIN_WD_EN - pinMode(PIN_WD_EN, OUTPUT); - digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot + pinMode(PIN_WD_EN, OUTPUT); + digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot #endif } \ No newline at end of file diff --git a/variants/nrf52840/meshtiny/variant.cpp b/variants/nrf52840/meshtiny/variant.cpp index 2e8b00e4b..c391d4de8 100644 --- a/variants/nrf52840/meshtiny/variant.cpp +++ b/variants/nrf52840/meshtiny/variant.cpp @@ -30,25 +30,24 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); - // Initialize Encoder pins - pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP); - pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP); - pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP); + // Initialize Encoder pins + pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP); + pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP); + pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP); - // Initialize Buzzer pin - pinMode(PIN_BUZZER, OUTPUT); - digitalWrite(PIN_BUZZER, LOW); + // Initialize Buzzer pin + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); } diff --git a/variants/nrf52840/monteops_hw1/variant.cpp b/variants/nrf52840/monteops_hw1/variant.cpp index 75cca1dc3..90a0b7116 100644 --- a/variants/nrf52840/monteops_hw1/variant.cpp +++ b/variants/nrf52840/monteops_hw1/variant.cpp @@ -30,12 +30,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf52840/monteops_hw1/variant.h b/variants/nrf52840/monteops_hw1/variant.h index 97536b169..b5afb2441 100644 --- a/variants/nrf52840/monteops_hw1/variant.h +++ b/variants/nrf52840/monteops_hw1/variant.h @@ -176,8 +176,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG diff --git a/variants/nrf52840/muzi_base/rfswitch.h b/variants/nrf52840/muzi_base/rfswitch.h index 589f24767..9332cb747 100644 --- a/variants/nrf52840/muzi_base/rfswitch.h +++ b/variants/nrf52840/muzi_base/rfswitch.h @@ -4,8 +4,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/muzi_base/variant.cpp b/variants/nrf52840/muzi_base/variant.cpp index da01de974..4d116b288 100644 --- a/variants/nrf52840/muzi_base/variant.cpp +++ b/variants/nrf52840/muzi_base/variant.cpp @@ -57,27 +57,26 @@ const uint32_t g_ADigitalPinMap[] = { 47, }; -void initVariant() -{ - // Initialize the digital pins as inputs or outputs - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, HIGH); +void initVariant() { + // Initialize the digital pins as inputs or outputs + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, HIGH); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, HIGH); - // Initialize LoRa pins - pinMode(SX126X_RESET, OUTPUT); - digitalWrite(SX126X_RESET, HIGH); + // Initialize LoRa pins + pinMode(SX126X_RESET, OUTPUT); + digitalWrite(SX126X_RESET, HIGH); - pinMode(SX126X_CS, OUTPUT); - digitalWrite(SX126X_CS, HIGH); + pinMode(SX126X_CS, OUTPUT); + digitalWrite(SX126X_CS, HIGH); - pinMode(GPS_EN_GPIO, OUTPUT); - digitalWrite(GPS_EN_GPIO, HIGH); // GPS on initially + pinMode(GPS_EN_GPIO, OUTPUT); + digitalWrite(GPS_EN_GPIO, HIGH); // GPS on initially - pinMode(SCREEN_12V_ENABLE, OUTPUT); - digitalWrite(SCREEN_12V_ENABLE, LOW); // + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, LOW); // - pinMode(BATTERY_CHARGING_INV, INPUT); + pinMode(BATTERY_CHARGING_INV, INPUT); } diff --git a/variants/nrf52840/muzi_base/variant.h b/variants/nrf52840/muzi_base/variant.h index 96604c400..cb6f87132 100644 --- a/variants/nrf52840/muzi_base/variant.h +++ b/variants/nrf52840/muzi_base/variant.h @@ -132,37 +132,32 @@ extern "C" { #define USERPREFS_OEM_FONT_SIZE 0 #define USERPREFS_OEM_IMAGE_WIDTH 88 // 11 bytes wide #define USERPREFS_OEM_IMAGE_HEIGHT 47 // 517 bytes total -#define USERPREFS_OEM_IMAGE_DATA \ - { \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, \ - 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0xF7, 0x0F, 0xFF, 0x00, 0xF0, 0xFF, 0x0F, 0xC0, 0xFF, 0x07, 0x78, 0xFF, 0x9F, 0xFF, 0x01, 0xF0, 0xFF, 0x0F, 0xC0, \ - 0xFF, 0x03, 0x78, 0x3F, 0xFE, 0xF3, 0x01, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x03, 0x78, 0x1F, 0xFC, 0xC0, 0x03, 0xF0, \ - 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x78, 0x0F, 0xF8, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x78, \ - 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x78, 0x00, \ - 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x3C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, \ - 0x00, 0x1C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1E, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, \ - 0xE0, 0xFF, 0x0F, 0x00, 0x0F, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, \ - 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x03, 0xC0, 0xFF, \ - 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0xFF, 0x01, 0xE0, 0xFF, 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0x7C, \ - 0x00, 0xF0, 0xFF, 0x07, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ - 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE3, 0xE7, 0xC7, 0x1F, 0xF8, 0x0F, 0xF0, 0xE7, 0xE3, 0x07, 0x7C, 0xC7, 0xE7, \ - 0xC3, 0x0F, 0xE0, 0x07, 0xE0, 0xC7, 0xE1, 0x03, 0x70, 0xC7, 0xC3, 0xE3, 0x87, 0xC1, 0x07, 0xC0, 0xC7, 0xF8, 0xE3, \ - 0x71, 0xC7, 0xC3, 0xE3, 0xE3, 0xC7, 0xC7, 0xC7, 0x47, 0xF8, 0xF3, 0x7F, 0x8F, 0xC3, 0xF1, 0xE3, 0x8F, 0xC7, 0x8F, \ - 0x27, 0xFC, 0xE3, 0x7F, 0x8F, 0x81, 0xF1, 0xF1, 0x8F, 0xC7, 0xCF, 0x07, 0xFE, 0x03, 0x7E, 0x8F, 0x99, 0xF1, 0xF1, \ - 0x8F, 0x07, 0xC0, 0x07, 0xFF, 0x07, 0x78, 0x9F, 0x99, 0xF9, 0xF1, 0x8F, 0x07, 0xE0, 0x07, 0xFE, 0x3F, 0x70, 0x1F, \ - 0x18, 0xF8, 0xF3, 0x8F, 0x07, 0xF0, 0x27, 0xFC, 0xFF, 0x71, 0x3F, 0x18, 0xF8, 0xE3, 0xC7, 0xC7, 0xF1, 0x47, 0xF8, \ - 0xF3, 0x63, 0x3F, 0x3C, 0xFC, 0xC3, 0xC3, 0xC7, 0xE3, 0xC7, 0xF0, 0xE1, 0x71, 0x3F, 0x3C, 0xFC, 0x07, 0xE0, 0xC7, \ - 0xC7, 0xC7, 0xE1, 0x03, 0x70, 0x7F, 0x7E, 0xFE, 0x0F, 0xF0, 0xC7, 0x87, 0xC7, 0xC3, 0x07, 0x78, 0xFF, 0xFF, 0xFF, \ - 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, \ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ - 0xFF, 0xFF, 0x7F \ - } +#define USERPREFS_OEM_IMAGE_DATA \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x3C, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x0F, 0xFF, 0x00, 0xF0, 0xFF, 0x0F, 0xC0, 0xFF, 0x07, 0x78, 0xFF, 0x9F, 0xFF, 0x01, 0xF0, \ + 0xFF, 0x0F, 0xC0, 0xFF, 0x03, 0x78, 0x3F, 0xFE, 0xF3, 0x01, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x03, 0x78, 0x1F, 0xFC, 0xC0, 0x03, 0xF0, 0xFF, \ + 0x0F, 0x00, 0xE0, 0x01, 0x78, 0x0F, 0xF8, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x78, 0x80, 0x03, 0xF0, 0xFF, 0x0F, \ + 0x00, 0x70, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x78, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, \ + 0x3C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1E, \ + 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x0F, 0x00, 0x0F, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x07, 0x80, 0x07, 0x00, \ + 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0x78, \ + 0x07, 0x70, 0x80, 0x03, 0x00, 0xFF, 0x01, 0xE0, 0xFF, 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0x7C, 0x00, 0xF0, 0xFF, 0x07, 0x78, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, \ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, \ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE3, 0xE7, 0xC7, 0x1F, 0xF8, 0x0F, 0xF0, 0xE7, 0xE3, 0x07, 0x7C, 0xC7, 0xE7, 0xC3, 0x0F, 0xE0, \ + 0x07, 0xE0, 0xC7, 0xE1, 0x03, 0x70, 0xC7, 0xC3, 0xE3, 0x87, 0xC1, 0x07, 0xC0, 0xC7, 0xF8, 0xE3, 0x71, 0xC7, 0xC3, 0xE3, 0xE3, 0xC7, 0xC7, \ + 0xC7, 0x47, 0xF8, 0xF3, 0x7F, 0x8F, 0xC3, 0xF1, 0xE3, 0x8F, 0xC7, 0x8F, 0x27, 0xFC, 0xE3, 0x7F, 0x8F, 0x81, 0xF1, 0xF1, 0x8F, 0xC7, 0xCF, \ + 0x07, 0xFE, 0x03, 0x7E, 0x8F, 0x99, 0xF1, 0xF1, 0x8F, 0x07, 0xC0, 0x07, 0xFF, 0x07, 0x78, 0x9F, 0x99, 0xF9, 0xF1, 0x8F, 0x07, 0xE0, 0x07, \ + 0xFE, 0x3F, 0x70, 0x1F, 0x18, 0xF8, 0xF3, 0x8F, 0x07, 0xF0, 0x27, 0xFC, 0xFF, 0x71, 0x3F, 0x18, 0xF8, 0xE3, 0xC7, 0xC7, 0xF1, 0x47, 0xF8, \ + 0xF3, 0x63, 0x3F, 0x3C, 0xFC, 0xC3, 0xC3, 0xC7, 0xE3, 0xC7, 0xF0, 0xE1, 0x71, 0x3F, 0x3C, 0xFC, 0x07, 0xE0, 0xC7, 0xC7, 0xC7, 0xE1, 0x03, \ + 0x70, 0x7F, 0x7E, 0xFE, 0x0F, 0xF0, 0xC7, 0x87, 0xC7, 0xC3, 0x07, 0x78, 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x7E, \ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, \ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F \ + } // QSPI Pins #define PIN_QSPI_SCK (0 + 3) diff --git a/variants/nrf52840/nano-g2-ultra/variant.cpp b/variants/nrf52840/nano-g2-ultra/variant.cpp index ce5d00886..ade2f34c8 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.cpp +++ b/variants/nrf52840/nano-g2-ultra/variant.cpp @@ -30,7 +30,6 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // Nothing need to be inited for now +void initVariant() { + // Nothing need to be inited for now } \ No newline at end of file diff --git a/variants/nrf52840/nano-g2-ultra/variant.h b/variants/nrf52840/nano-g2-ultra/variant.h index d8f41a68c..4753fac6c 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.h +++ b/variants/nrf52840/nano-g2-ultra/variant.h @@ -112,7 +112,8 @@ External serial flash W25Q16JV_IQ #define SX126X_DIO1 (32 + 10) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching // #define SX1262_DIO3 (0 + 21) -// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main CPU? +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main +// CPU? #define SX126X_BUSY (32 + 11) #define SX126X_RESET (32 + 15) // DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 diff --git a/variants/nrf52840/r1-neo/variant.cpp b/variants/nrf52840/r1-neo/variant.cpp index f87c041aa..275173219 100644 --- a/variants/nrf52840/r1-neo/variant.cpp +++ b/variants/nrf52840/r1-neo/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - // pinMode(PIN_3V3_EN, OUTPUT); - // digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + // pinMode(PIN_3V3_EN, OUTPUT); + // digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak2560/variant.cpp b/variants/nrf52840/rak2560/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak2560/variant.cpp +++ b/variants/nrf52840/rak2560/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak2560/variant.h b/variants/nrf52840/rak2560/variant.h index f922e8a61..f40bcc1e4 100644 --- a/variants/nrf52840/rak2560/variant.h +++ b/variants/nrf52840/rak2560/variant.h @@ -184,8 +184,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -215,11 +215,9 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_SERIAL_PORT Serial2 diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak3401_1watt/variant.cpp +++ b/variants/nrf52840/rak3401_1watt/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h index d4bb1a175..4f9d60f4b 100644 --- a/variants/nrf52840/rak3401_1watt/variant.h +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -181,11 +181,9 @@ static const uint8_t SCK = PIN_SPI_SCK; // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak4631/variant.cpp b/variants/nrf52840/rak4631/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak4631/variant.cpp +++ b/variants/nrf52840/rak4631/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631/variant.h b/variants/nrf52840/rak4631/variant.h index 302e531d5..ac3554718 100644 --- a/variants/nrf52840/rak4631/variant.h +++ b/variants/nrf52840/rak4631/variant.h @@ -195,8 +195,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -237,11 +237,9 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak4631_epaper/variant.cpp b/variants/nrf52840/rak4631_epaper/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak4631_epaper/variant.cpp +++ b/variants/nrf52840/rak4631_epaper/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_epaper/variant.h b/variants/nrf52840/rak4631_epaper/variant.h index c1e11bee5..a45a687dd 100644 --- a/variants/nrf52840/rak4631_epaper/variant.h +++ b/variants/nrf52840/rak4631_epaper/variant.h @@ -195,10 +195,9 @@ static const uint8_t SCK = PIN_SPI_SCK; // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h index 1f8257e8e..223b6fe1d 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h @@ -171,12 +171,9 @@ static const uint8_t SCK = PIN_SPI_SCK; // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN -// #define PIN_GPS_PPS (17) // Pulse per second input from the GPS +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS // #define GPS_RX_PIN PIN_SERIAL1_RX // #define GPS_TX_PIN PIN_SERIAL1_TX diff --git a/variants/nrf52840/rak4631_eth_gw/variant.cpp b/variants/nrf52840/rak4631_eth_gw/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.cpp +++ b/variants/nrf52840/rak4631_eth_gw/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_eth_gw/variant.h b/variants/nrf52840/rak4631_eth_gw/variant.h index c8a2f83ae..ab27ca627 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.h +++ b/variants/nrf52840/rak4631_eth_gw/variant.h @@ -192,8 +192,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -224,11 +224,9 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h index 51baf3ada..5758f4bd2 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h @@ -191,8 +191,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -222,11 +222,9 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index e84b60b3b..ceb7e4ff8 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 159cabf07..0523eafe8 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -182,8 +182,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG diff --git a/variants/nrf52840/rak_wismeshtap/variant.cpp b/variants/nrf52840/rak_wismeshtap/variant.cpp index 5a3587982..50282108c 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.cpp +++ b/variants/nrf52840/rak_wismeshtap/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/rak_wismeshtap/variant.h b/variants/nrf52840/rak_wismeshtap/variant.h index a7b9290a5..cbf0f45e1 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.h +++ b/variants/nrf52840/rak_wismeshtap/variant.h @@ -213,8 +213,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the -control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do +the control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -241,11 +241,9 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail -// #define PIN_GPS_RESET (34) -// #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power +// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define +// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/seeed_solar_node/variant.cpp b/variants/nrf52840/seeed_solar_node/variant.cpp index 994e97ff9..3aebcd52e 100644 --- a/variants/nrf52840/seeed_solar_node/variant.cpp +++ b/variants/nrf52840/seeed_solar_node/variant.cpp @@ -84,25 +84,24 @@ const uint32_t g_ADigitalPinMap[] = { }; } -void initVariant() -{ - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); - // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. - pinMode(GPS_EN, OUTPUT); - digitalWrite(GPS_EN, LOW); +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); - // VBAT_ENABLE - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, LOW); + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, LOW); - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, LOW); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, LOW); - pinMode(PIN_LED2, OUTPUT); - // digitalWrite(LED_PIN, LOW); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, OUTPUT); + // digitalWrite(LED_PIN, LOW); - pinMode(GPS_EN, OUTPUT); - digitalWrite(GPS_EN, HIGH); + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/seeed_solar_node/variant.h b/variants/nrf52840/seeed_solar_node/variant.h index b2a1e6dff..3fcf24b70 100644 --- a/variants/nrf52840/seeed_solar_node/variant.h +++ b/variants/nrf52840/seeed_solar_node/variant.h @@ -98,14 +98,15 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX + // power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -#define BAT_READ \ - D19 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO and is - // program pin 32 / or P0.31) +#define BAT_READ \ + D19 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO + // and is program pin 32 / or P0.31) #define BATTERY_SENSE_RESOLUTION_BITS 12 #define ADC_MULTIPLIER 3.3 #define BATTERY_PIN PIN_VBAT // PIN_A7 diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp b/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp index a045b0cf9..09164d1e9 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp @@ -79,18 +79,17 @@ const uint32_t g_ADigitalPinMap[] = { }; } -void initVariant() -{ - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); - // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. - // VBAT_ENABLE - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, HIGH); +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, LOW); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, LOW); - pinMode(PIN_LED2, OUTPUT); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, OUTPUT); } \ No newline at end of file diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.h b/variants/nrf52840/seeed_wio_tracker_L1/variant.h index b62b65161..07251893d 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.h @@ -104,7 +104,8 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX + // power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 98aeb8700..0113639ed 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -21,95 +21,94 @@ #include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" #include "graphics/niche/Inputs/TwoButtonExtended.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - inkhud->setDisplayResilience(15); + // Set how many FAST updates per FULL update + inkhud->setDisplayResilience(15); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + // Customize default settings + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise #if HAS_TRACKBALL - inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick - inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead + inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick + inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead #endif - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component #if HAS_TRACKBALL - // #0: Exit Button - buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); - buttons->setTiming(0, 75, 500); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); + // #0: Exit Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); - // #1: Joystick Center - buttons->setWiring(1, TB_PRESS); - buttons->setTiming(1, 75, 500); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); + // #1: Joystick Center + buttons->setWiring(1, TB_PRESS); + buttons->setTiming(1, 75, 500); + buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); - // Joystick Directions - buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); - buttons->setJoystickDebounce(50); - buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, - [inkhud]() { inkhud->navLeft(); }, [inkhud]() { inkhud->navRight(); }); + // Joystick Directions + buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); + buttons->setJoystickDebounce(50); + buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, [inkhud]() { inkhud->navLeft(); }, + [inkhud]() { inkhud->navRight(); }); #else - // #0: User Button - buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); - buttons->setTiming(0, 75, 500); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: User Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); #endif - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp index bcbe20ea5..a5dc72894 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp @@ -87,17 +87,16 @@ const uint32_t g_ADigitalPinMap[] = { }; } -void initVariant() -{ - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); - // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. - // VBAT_ENABLE - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, HIGH); +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, LOW); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); } \ No newline at end of file diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index ae20f3c36..74bf5378e 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -96,7 +96,8 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX + // power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // EINK diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp index 70cadf5db..be6f20e78 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp @@ -78,19 +78,18 @@ const uint32_t g_ADigitalPinMap[] = { Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -void initVariant() -{ - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); +void initVariant() { + // Set BQ25101 ISET to 100mA instead of 50mA + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); - // LEDs - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); + // LEDs + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/t-echo-lite/variant.cpp b/variants/nrf52840/t-echo-lite/variant.cpp index cae079b74..4454a1d84 100644 --- a/variants/nrf52840/t-echo-lite/variant.cpp +++ b/variants/nrf52840/t-echo-lite/variant.cpp @@ -30,15 +30,14 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/t-echo/nicheGraphics.h b/variants/nrf52840/t-echo/nicheGraphics.h index c89d816b9..78c0414cc 100644 --- a/variants/nrf52840/t-echo/nicheGraphics.h +++ b/variants/nrf52840/t-echo/nicheGraphics.h @@ -28,99 +28,98 @@ // To avoid this, we lockout the button during TX #include "mesh/RadioLibInterface.h" -void setupNicheGraphics() -{ - using namespace NicheGraphics; +void setupNicheGraphics() { + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::GDEY0154D67; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(20, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(20, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it - // Setup backlight controller - // Note: AUX button attached further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + // Setup backlight controller + // Note: AUX button attached further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setTiming(0, 75, 500); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button (Capacitive Touch Button) - // - short: momentary backlight - // - long: latch backlight on - buttons->setWiring(1, PIN_BUTTON_TOUCH); - buttons->setTiming(1, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC + // #1: Aux Button (Capacitive Touch Button) + // - short: momentary backlight + // - long: latch backlight on + buttons->setWiring(1, PIN_BUTTON_TOUCH); + buttons->setTiming(1, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC - buttons->setHandlerDown(1, [inkhud, backlight]() { - // Discard the button press if radio is active - // Rare hardware fault: LoRa activity triggers touch button - if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) - return; + buttons->setHandlerDown(1, [inkhud, backlight]() { + // Discard the button press if radio is active + // Rare hardware fault: LoRa activity triggers touch button + if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) + return; - // Backlight on (while held) - backlight->peek(); + // Backlight on (while held) + backlight->peek(); - // Handler has run, which confirms touch button wasn't removed as part of DIY build. - // No longer need the fallback backlight toggle in menu. - inkhud->persistence->settings.optionalMenuItems.backlight = false; - }); + // Handler has run, which confirms touch button wasn't removed as part of DIY build. + // No longer need the fallback backlight toggle in menu. + inkhud->persistence->settings.optionalMenuItems.backlight = false; + }); - buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); - buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); + buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/t-echo/variant.cpp b/variants/nrf52840/t-echo/variant.cpp index cae079b74..4454a1d84 100644 --- a/variants/nrf52840/t-echo/variant.cpp +++ b/variants/nrf52840/t-echo/variant.cpp @@ -30,15 +30,14 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h index 9244fc6c3..6cf897b38 100644 --- a/variants/nrf52840/t-echo/variant.h +++ b/variants/nrf52840/t-echo/variant.h @@ -133,8 +133,9 @@ External serial flash WP25R1635FZUIL0 #define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 20) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching -#define SX1262_DIO3 \ - (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main +#define SX1262_DIO3 \ + (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from + // the main // CPU? #define SX126X_BUSY (0 + 17) #define SX126X_RESET (0 + 25) @@ -142,8 +143,8 @@ External serial flash WP25R1635FZUIL0 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface -// code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the +// sx1262interface code) // #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) @@ -165,8 +166,7 @@ External serial flash WP25R1635FZUIL0 #define PIN_POWER_EN (0 + 12) // #define PIN_POWER_EN1 (0 + 13) -#define PIN_SPI1_MISO \ - (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MISO (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK diff --git a/variants/nrf52840/tracker-t1000-e/rfswitch.h b/variants/nrf52840/tracker-t1000-e/rfswitch.h index e229d77cf..a0994c63c 100644 --- a/variants/nrf52840/tracker-t1000-e/rfswitch.h +++ b/variants/nrf52840/tracker-t1000-e/rfswitch.h @@ -1,8 +1,8 @@ #include "RadioLib.h" #include "nrf.h" -static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, - RADIOLIB_LR11X0_DIO7, RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, + RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 DIO8 diff --git a/variants/nrf52840/tracker-t1000-e/variant.cpp b/variants/nrf52840/tracker-t1000-e/variant.cpp index 8096705d0..7c4f819e9 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.cpp +++ b/variants/nrf52840/tracker-t1000-e/variant.cpp @@ -30,35 +30,34 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() { + // LED1 & LED2 + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); - pinMode(PIN_3V3_ACC_EN, OUTPUT); - digitalWrite(PIN_3V3_ACC_EN, HIGH); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + digitalWrite(PIN_3V3_ACC_EN, HIGH); - pinMode(BUZZER_EN_PIN, OUTPUT); - digitalWrite(BUZZER_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); + digitalWrite(BUZZER_EN_PIN, HIGH); - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); - digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, INPUT); + pinMode(GPS_RESETB_OUT, INPUT); } \ No newline at end of file diff --git a/variants/nrf52840/wio-sdk-wm1110/rfswitch.h b/variants/nrf52840/wio-sdk-wm1110/rfswitch.h index cda6364f5..8f753df83 100644 --- a/variants/nrf52840/wio-sdk-wm1110/rfswitch.h +++ b/variants/nrf52840/wio-sdk-wm1110/rfswitch.h @@ -8,8 +8,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/wio-sdk-wm1110/variant.cpp b/variants/nrf52840/wio-sdk-wm1110/variant.cpp index 5a3587982..50282108c 100644 --- a/variants/nrf52840/wio-sdk-wm1110/variant.cpp +++ b/variants/nrf52840/wio-sdk-wm1110/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/wio-t1000-s/rfswitch.h b/variants/nrf52840/wio-t1000-s/rfswitch.h index e229d77cf..a0994c63c 100644 --- a/variants/nrf52840/wio-t1000-s/rfswitch.h +++ b/variants/nrf52840/wio-t1000-s/rfswitch.h @@ -1,8 +1,8 @@ #include "RadioLib.h" #include "nrf.h" -static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, - RADIOLIB_LR11X0_DIO7, RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, + RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 DIO8 diff --git a/variants/nrf52840/wio-t1000-s/variant.cpp b/variants/nrf52840/wio-t1000-s/variant.cpp index 85e0c44f3..62c5e671b 100644 --- a/variants/nrf52840/wio-t1000-s/variant.cpp +++ b/variants/nrf52840/wio-t1000-s/variant.cpp @@ -30,35 +30,34 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() { + // LED1 & LED2 + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); - pinMode(PIN_3V3_ACC_EN, OUTPUT); - digitalWrite(PIN_3V3_ACC_EN, LOW); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + digitalWrite(PIN_3V3_ACC_EN, LOW); - pinMode(BUZZER_EN_PIN, OUTPUT); - digitalWrite(BUZZER_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); + digitalWrite(BUZZER_EN_PIN, HIGH); - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); - digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, INPUT); + pinMode(GPS_RESETB_OUT, INPUT); } \ No newline at end of file diff --git a/variants/nrf52840/wio-tracker-wm1110/rfswitch.h b/variants/nrf52840/wio-tracker-wm1110/rfswitch.h index cda6364f5..8f753df83 100644 --- a/variants/nrf52840/wio-tracker-wm1110/rfswitch.h +++ b/variants/nrf52840/wio-tracker-wm1110/rfswitch.h @@ -8,8 +8,6 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/wio-tracker-wm1110/variant.cpp b/variants/nrf52840/wio-tracker-wm1110/variant.cpp index 5a3587982..50282108c 100644 --- a/variants/nrf52840/wio-tracker-wm1110/variant.cpp +++ b/variants/nrf52840/wio-tracker-wm1110/variant.cpp @@ -30,16 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() -{ - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/rp2040/ec_catsniffer/variant.cpp b/variants/rp2040/ec_catsniffer/variant.cpp index db5226541..343ff065a 100644 --- a/variants/rp2040/ec_catsniffer/variant.cpp +++ b/variants/rp2040/ec_catsniffer/variant.cpp @@ -26,14 +26,13 @@ #define CTF2 9 #define CTF3 10 -void initVariant() -{ - // Config the LoRa Switch - pinMode(CTF1, OUTPUT); - pinMode(CTF2, OUTPUT); - pinMode(CTF3, OUTPUT); +void initVariant() { + // Config the LoRa Switch + pinMode(CTF1, OUTPUT); + pinMode(CTF2, OUTPUT); + pinMode(CTF3, OUTPUT); - digitalWrite(CTF1, HIGH); - digitalWrite(CTF2, LOW); - digitalWrite(CTF3, LOW); + digitalWrite(CTF1, HIGH); + digitalWrite(CTF2, LOW); + digitalWrite(CTF3, LOW); } \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h index daf4aaaf9..9c15e5193 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h +++ b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h @@ -1,7 +1,7 @@ // From E77-900M22S Product Specification // https://www.cdebyte.com/pdf-down.aspx?id=2963 -// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, RF_TXEN=1 -// RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel +// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, +// RF_TXEN=1 RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA7, PA6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; From beb268ff25e26ad7854e7450a24932c42ec703d3 Mon Sep 17 00:00:00 2001 From: Jorropo Date: Sun, 4 Jan 2026 12:15:53 +0100 Subject: [PATCH 2/4] Revert "add a .clang-format file (#9154)" (#9172) I thought git would be smart enough to understand all the whitespace changes but even with all the flags I know to make it ignore theses it still blows up if there are identical changes on both sides. I have a solution but it require creating a new commit at the merge base for each conflicting PR and merging it into develop. I don't think blowing up all PRs is worth for now, maybe if we can coordinate this for V3 let's say. This reverts commit 0d11331d185a60adf84e64ec98642baf6a15a95a. --- .clang-format | 2 - .clusterfuzzlite/router_fuzzer.cpp | 263 +- src/AmbientLightingThread.h | 239 +- src/AudioThread.h | 129 +- src/BluetoothCommon.cpp | 18 +- src/BluetoothCommon.h | 15 +- src/BluetoothStatus.h | 185 +- src/DebugConfiguration.cpp | 234 +- src/DebugConfiguration.h | 65 +- src/DisplayFormatters.cpp | 155 +- src/DisplayFormatters.h | 10 +- src/FSCommon.cpp | 394 +- src/Fusion/FusionAhrs.c | 622 +-- src/Fusion/FusionAhrs.h | 73 +- src/Fusion/FusionAxes.h | 293 +- src/Fusion/FusionCalibration.h | 13 +- src/Fusion/FusionCompass.c | 44 +- src/Fusion/FusionCompass.h | 3 +- src/Fusion/FusionConvention.h | 6 +- src/Fusion/FusionMath.h | 360 +- src/Fusion/FusionOffset.c | 47 +- src/Fusion/FusionOffset.h | 8 +- src/GPSStatus.h | 192 +- src/GpioLogic.cpp | 122 +- src/GpioLogic.h | 162 +- src/Led.cpp | 38 +- src/MessageStore.cpp | 529 +- src/MessageStore.h | 119 +- src/NodeStatus.h | 88 +- src/Observer.h | 125 +- src/Power.cpp | 1931 +++---- src/PowerFSM.cpp | 508 +- src/PowerFSM.h | 29 +- src/PowerFSMThread.h | 48 +- src/PowerMon.cpp | 47 +- src/PowerMon.h | 35 +- src/PowerStatus.h | 153 +- src/RedirectablePrint.cpp | 617 +-- src/RedirectablePrint.h | 65 +- src/SPILock.cpp | 7 +- src/SafeFile.cpp | 158 +- src/SafeFile.h | 52 +- src/SerialConsole.cpp | 140 +- src/SerialConsole.h | 54 +- src/Status.h | 60 +- src/airtime.cpp | 313 +- src/airtime.h | 73 +- src/buzz/BuzzerFeedbackThread.cpp | 102 +- src/buzz/BuzzerFeedbackThread.h | 13 +- src/buzz/buzz.cpp | 206 +- src/commands.h | 22 +- src/concurrency/BinarySemaphoreFreeRTOS.cpp | 28 +- src/concurrency/BinarySemaphoreFreeRTOS.h | 26 +- src/concurrency/BinarySemaphorePosix.cpp | 10 +- src/concurrency/BinarySemaphorePosix.h | 26 +- src/concurrency/InterruptableDelay.cpp | 26 +- src/concurrency/InterruptableDelay.h | 29 +- src/concurrency/Lock.cpp | 32 +- src/concurrency/Lock.h | 34 +- src/concurrency/LockGuard.cpp | 13 +- src/concurrency/LockGuard.h | 20 +- src/concurrency/NotifiedWorkerThread.cpp | 95 +- src/concurrency/NotifiedWorkerThread.h | 72 +- src/concurrency/OSThread.cpp | 138 +- src/concurrency/OSThread.h | 67 +- src/concurrency/Periodic.h | 18 +- src/configuration.h | 4 +- src/detect/LoRaRadioType.h | 22 +- src/detect/ScanI2C.cpp | 102 +- src/detect/ScanI2C.h | 257 +- src/detect/ScanI2CConsumer.cpp | 14 +- src/detect/ScanI2CConsumer.h | 9 +- src/detect/ScanI2CTwoWire.cpp | 1107 ++-- src/detect/ScanI2CTwoWire.h | 52 +- src/detect/einkScan.h | 102 +- src/freertosinc.h | 4 +- src/gps/GPS.cpp | 2903 +++++----- src/gps/GPS.h | 317 +- src/gps/GPSUpdateScheduling.cpp | 132 +- src/gps/GPSUpdateScheduling.h | 37 +- src/gps/GeoCoord.cpp | 857 +-- src/gps/GeoCoord.h | 193 +- src/gps/NMEAWPL.cpp | 101 +- src/gps/RTC.cpp | 596 ++- src/gps/RTC.h | 28 +- src/gps/ubx.h | 50 +- src/graphics/EInkDisplay2.cpp | 351 +- src/graphics/EInkDisplay2.h | 99 +- src/graphics/EInkDynamicDisplay.cpp | 726 +-- src/graphics/EInkDynamicDisplay.h | 211 +- src/graphics/GxEPD2Multi.h | 235 +- src/graphics/Panel_sdl.cpp | 1116 ++-- src/graphics/Panel_sdl.hpp | 187 +- src/graphics/PointStruct.h | 4 +- src/graphics/Screen.cpp | 2501 ++++----- src/graphics/Screen.h | 1030 ++-- src/graphics/ScreenFonts.h | 6 +- src/graphics/SharedUIDisplay.cpp | 826 +-- src/graphics/SharedUIDisplay.h | 6 +- src/graphics/TFTDisplay.cpp | 2193 ++++---- src/graphics/TFTDisplay.h | 79 +- src/graphics/TimeFormatters.cpp | 211 +- src/graphics/VirtualKeyboard.cpp | 1313 ++--- src/graphics/VirtualKeyboard.h | 103 +- src/graphics/draw/ClockRenderer.cpp | 740 +-- src/graphics/draw/ClockRenderer.h | 6 +- src/graphics/draw/CompassRenderer.cpp | 180 +- src/graphics/draw/CompassRenderer.h | 6 +- src/graphics/draw/DebugRenderer.cpp | 1065 ++-- src/graphics/draw/DebugRenderer.h | 6 +- src/graphics/draw/DrawRenderers.h | 6 +- src/graphics/draw/MenuHandler.cpp | 4589 ++++++++-------- src/graphics/draw/MenuHandler.h | 236 +- src/graphics/draw/MessageRenderer.cpp | 1649 +++--- src/graphics/draw/MessageRenderer.h | 9 +- src/graphics/draw/NodeListRenderer.cpp | 1181 +++-- src/graphics/draw/NodeListRenderer.h | 15 +- src/graphics/draw/NotificationRenderer.cpp | 1282 ++--- src/graphics/draw/NotificationRenderer.h | 64 +- src/graphics/draw/UIRenderer.cpp | 2208 ++++---- src/graphics/draw/UIRenderer.h | 74 +- src/graphics/emotes.cpp | 469 +- src/graphics/emotes.h | 11 +- src/graphics/fonts/EinkDisplayFonts.cpp | 1756 +++--- src/graphics/fonts/OLEDDisplayFontsCS.cpp | 2143 ++++---- src/graphics/fonts/OLEDDisplayFontsRU.cpp | 1992 +++---- src/graphics/fonts/OLEDDisplayFontsUA.cpp | 2199 ++++---- src/graphics/images.h | 100 +- .../Drivers/Backlight/LatchingBacklight.cpp | 109 +- .../Drivers/Backlight/LatchingBacklight.h | 42 +- .../niche/Drivers/EInk/DEPG0213BNS800.cpp | 140 +- .../niche/Drivers/EInk/DEPG0213BNS800.h | 32 +- .../niche/Drivers/EInk/DEPG0290BNS800.cpp | 128 +- .../niche/Drivers/EInk/DEPG0290BNS800.h | 32 +- src/graphics/niche/Drivers/EInk/E0213A367.cpp | 113 +- src/graphics/niche/Drivers/EInk/E0213A367.h | 30 +- src/graphics/niche/Drivers/EInk/EInk.cpp | 103 +- src/graphics/niche/Drivers/EInk/EInk.h | 62 +- .../niche/Drivers/EInk/GDEY0154D67.cpp | 66 +- src/graphics/niche/Drivers/EInk/GDEY0154D67.h | 30 +- .../niche/Drivers/EInk/GDEY0213B74.cpp | 66 +- src/graphics/niche/Drivers/EInk/GDEY0213B74.h | 30 +- .../niche/Drivers/EInk/HINK_E0213A289.cpp | 68 +- .../niche/Drivers/EInk/HINK_E0213A289.h | 30 +- .../niche/Drivers/EInk/HINK_E042A87.cpp | 65 +- .../niche/Drivers/EInk/HINK_E042A87.h | 28 +- .../niche/Drivers/EInk/LCMEN2R13ECC1.cpp | 82 +- .../niche/Drivers/EInk/LCMEN2R13ECC1.h | 30 +- .../niche/Drivers/EInk/LCMEN2R13EFC1.cpp | 332 +- .../niche/Drivers/EInk/LCMEN2R13EFC1.h | 74 +- src/graphics/niche/Drivers/EInk/SSD1682.cpp | 47 +- src/graphics/niche/Drivers/EInk/SSD1682.h | 14 +- src/graphics/niche/Drivers/EInk/SSD16XX.cpp | 371 +- src/graphics/niche/Drivers/EInk/SSD16XX.h | 72 +- .../Drivers/EInk/ZJY122250_0213BAAMFGN.cpp | 86 +- .../Drivers/EInk/ZJY122250_0213BAAMFGN.h | 30 +- .../Drivers/EInk/ZJY128296_029EAAMFGN.cpp | 68 +- .../niche/Drivers/EInk/ZJY128296_029EAAMFGN.h | 30 +- .../Drivers/EInk/ZJY200200_0154DAAMFGN.h | 3 +- src/graphics/niche/InkHUD/Applet.cpp | 1273 ++--- src/graphics/niche/InkHUD/Applet.h | 237 +- src/graphics/niche/InkHUD/AppletFont.cpp | 1202 ++--- src/graphics/niche/InkHUD/AppletFont.h | 52 +- .../InkHUD/Applets/Bases/Map/MapApplet.cpp | 890 ++-- .../InkHUD/Applets/Bases/Map/MapApplet.h | 58 +- .../Applets/Bases/NodeList/NodeListApplet.cpp | 435 +- .../Applets/Bases/NodeList/NodeListApplet.h | 62 +- .../BasicExample/BasicExampleApplet.cpp | 13 +- .../BasicExample/BasicExampleApplet.h | 14 +- .../NewMsgExample/NewMsgExampleApplet.cpp | 64 +- .../NewMsgExample/NewMsgExampleApplet.h | 46 +- .../System/AlignStick/AlignStickApplet.cpp | 263 +- .../System/AlignStick/AlignStickApplet.h | 46 +- .../System/BatteryIcon/BatteryIconApplet.cpp | 133 +- .../System/BatteryIcon/BatteryIconApplet.h | 24 +- .../InkHUD/Applets/System/Logo/LogoApplet.cpp | 248 +- .../InkHUD/Applets/System/Logo/LogoApplet.h | 34 +- .../InkHUD/Applets/System/Menu/MenuAction.h | 41 +- .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 1345 ++--- .../InkHUD/Applets/System/Menu/MenuApplet.h | 149 +- .../InkHUD/Applets/System/Menu/MenuItem.h | 30 +- .../InkHUD/Applets/System/Menu/MenuPage.h | 19 +- .../System/Notification/Notification.h | 33 +- .../Notification/NotificationApplet.cpp | 371 +- .../System/Notification/NotificationApplet.h | 52 +- .../Applets/System/Pairing/PairingApplet.cpp | 111 +- .../Applets/System/Pairing/PairingApplet.h | 28 +- .../System/Placeholder/PlaceholderApplet.cpp | 7 +- .../System/Placeholder/PlaceholderApplet.h | 16 +- .../InkHUD/Applets/System/Tips/TipsApplet.cpp | 400 +- .../InkHUD/Applets/System/Tips/TipsApplet.h | 46 +- .../User/AllMessage/AllMessageApplet.cpp | 199 +- .../User/AllMessage/AllMessageApplet.h | 31 +- .../niche/InkHUD/Applets/User/DM/DMApplet.cpp | 189 +- .../niche/InkHUD/Applets/User/DM/DMApplet.h | 31 +- .../InkHUD/Applets/User/Heard/HeardApplet.cpp | 169 +- .../InkHUD/Applets/User/Heard/HeardApplet.h | 22 +- .../User/Positions/PositionsApplet.cpp | 190 +- .../Applets/User/Positions/PositionsApplet.h | 28 +- .../User/RecentsList/RecentsListApplet.cpp | 192 +- .../User/RecentsList/RecentsListApplet.h | 46 +- .../ThreadedMessage/ThreadedMessageApplet.cpp | 365 +- .../ThreadedMessage/ThreadedMessageApplet.h | 34 +- src/graphics/niche/InkHUD/DisplayHealth.cpp | 206 +- src/graphics/niche/InkHUD/DisplayHealth.h | 39 +- src/graphics/niche/InkHUD/Events.cpp | 542 +- src/graphics/niche/InkHUD/Events.h | 78 +- src/graphics/niche/InkHUD/InkHUD.cpp | 307 +- src/graphics/niche/InkHUD/InkHUD.h | 122 +- src/graphics/niche/InkHUD/MessageStore.cpp | 216 +- src/graphics/niche/InkHUD/MessageStore.h | 36 +- src/graphics/niche/InkHUD/Persistence.cpp | 69 +- src/graphics/niche/InkHUD/Persistence.h | 192 +- src/graphics/niche/InkHUD/Renderer.cpp | 559 +- src/graphics/niche/InkHUD/Renderer.h | 96 +- src/graphics/niche/InkHUD/SystemApplet.h | 31 +- src/graphics/niche/InkHUD/Tile.cpp | 289 +- src/graphics/niche/InkHUD/Tile.h | 52 +- src/graphics/niche/InkHUD/WindowManager.cpp | 837 +-- src/graphics/niche/InkHUD/WindowManager.h | 78 +- src/graphics/niche/Inputs/TwoButton.cpp | 362 +- src/graphics/niche/Inputs/TwoButton.h | 125 +- .../niche/Inputs/TwoButtonExtended.cpp | 675 +-- src/graphics/niche/Inputs/TwoButtonExtended.h | 153 +- .../niche/Utils/CannedMessageStore.cpp | 188 +- src/graphics/niche/Utils/CannedMessageStore.h | 34 +- src/graphics/niche/Utils/FlashData.h | 215 +- src/graphics/tftSetup.cpp | 190 +- src/input/BBQ10Keyboard.cpp | 210 +- src/input/BBQ10Keyboard.h | 57 +- src/input/ButtonThread.cpp | 508 +- src/input/ButtonThread.h | 161 +- src/input/ExpressLRSFiveWay.cpp | 321 +- src/input/ExpressLRSFiveWay.h | 85 +- src/input/HackadayCommunicatorKeyboard.cpp | 157 +- src/input/HackadayCommunicatorKeyboard.h | 39 +- src/input/InputBroker.cpp | 109 +- src/input/InputBroker.h | 92 +- src/input/LinuxInput.cpp | 307 +- src/input/LinuxInput.h | 78 +- src/input/LinuxInputImpl.cpp | 5 +- src/input/LinuxInputImpl.h | 9 +- src/input/MPR121Keyboard.cpp | 589 ++- src/input/MPR121Keyboard.h | 67 +- src/input/RotaryEncoderImpl.cpp | 189 +- src/input/RotaryEncoderImpl.h | 48 +- src/input/RotaryEncoderInterruptBase.cpp | 226 +- src/input/RotaryEncoderInterruptBase.h | 77 +- src/input/RotaryEncoderInterruptImpl1.cpp | 55 +- src/input/RotaryEncoderInterruptImpl1.h | 15 +- src/input/SeesawRotary.cpp | 131 +- src/input/SeesawRotary.h | 21 +- src/input/SerialKeyboard.cpp | 287 +- src/input/SerialKeyboard.h | 35 +- src/input/SerialKeyboardImpl.cpp | 13 +- src/input/SerialKeyboardImpl.h | 9 +- src/input/TCA8418Keyboard.cpp | 158 +- src/input/TCA8418Keyboard.h | 29 +- src/input/TCA8418KeyboardBase.cpp | 574 +- src/input/TCA8418KeyboardBase.h | 277 +- src/input/TDeckProKeyboard.cpp | 192 +- src/input/TDeckProKeyboard.h | 41 +- src/input/TLoraPagerKeyboard.cpp | 233 +- src/input/TLoraPagerKeyboard.h | 47 +- src/input/TouchScreenBase.cpp | 237 +- src/input/TouchScreenBase.h | 71 +- src/input/TouchScreenImpl1.cpp | 103 +- src/input/TouchScreenImpl1.h | 17 +- src/input/TrackballInterruptBase.cpp | 354 +- src/input/TrackballInterruptBase.h | 99 +- src/input/TrackballInterruptImpl1.cpp | 89 +- src/input/TrackballInterruptImpl1.h | 19 +- src/input/UpDownInterruptBase.cpp | 260 +- src/input/UpDownInterruptBase.h | 100 +- src/input/UpDownInterruptImpl1.cpp | 55 +- src/input/UpDownInterruptImpl1.h | 15 +- src/input/cardKbI2cImpl.cpp | 107 +- src/input/cardKbI2cImpl.h | 9 +- src/input/i2cButton.cpp | 104 +- src/input/i2cButton.h | 11 +- src/input/kbI2cBase.cpp | 953 ++-- src/input/kbI2cBase.h | 27 +- src/input/kbMatrixBase.cpp | 174 +- src/input/kbMatrixBase.h | 23 +- src/input/kbMatrixImpl.cpp | 13 +- src/input/kbMatrixImpl.h | 9 +- src/main.cpp | 1727 +++--- src/memGet.cpp | 71 +- src/memGet.h | 13 +- src/mesh/Channels.cpp | 610 +-- src/mesh/Channels.h | 189 +- src/mesh/CryptoEngine.cpp | 305 +- src/mesh/CryptoEngine.h | 109 +- src/mesh/Default.cpp | 62 +- src/mesh/Default.h | 68 +- src/mesh/FloodingRouter.cpp | 205 +- src/mesh/FloodingRouter.h | 81 +- src/mesh/LLCC68Interface.cpp | 7 +- src/mesh/LLCC68Interface.h | 13 +- src/mesh/LR1110Interface.cpp | 7 +- src/mesh/LR1110Interface.h | 8 +- src/mesh/LR1120Interface.cpp | 12 +- src/mesh/LR1120Interface.h | 10 +- src/mesh/LR1121Interface.cpp | 12 +- src/mesh/LR1121Interface.h | 10 +- src/mesh/LR11x0Interface.cpp | 331 +- src/mesh/LR11x0Interface.h | 92 +- src/mesh/MemoryPool.h | 231 +- src/mesh/MeshModule.cpp | 429 +- src/mesh/MeshModule.h | 284 +- src/mesh/MeshPacketQueue.cpp | 250 +- src/mesh/MeshPacketQueue.h | 55 +- src/mesh/MeshRadio.h | 20 +- src/mesh/MeshService.cpp | 616 +-- src/mesh/MeshService.h | 225 +- src/mesh/MeshTypes.h | 21 +- src/mesh/NextHopRouter.cpp | 480 +- src/mesh/NextHopRouter.h | 212 +- src/mesh/NodeDB.cpp | 3152 +++++------ src/mesh/NodeDB.h | 456 +- src/mesh/PacketCache.cpp | 367 +- src/mesh/PacketCache.h | 105 +- src/mesh/PacketHistory.cpp | 637 +-- src/mesh/PacketHistory.h | 108 +- src/mesh/PhoneAPI.cpp | 1404 ++--- src/mesh/PhoneAPI.h | 251 +- src/mesh/PointerQueue.h | 29 +- src/mesh/ProtobufModule.h | 204 +- src/mesh/RF95Interface.cpp | 394 +- src/mesh/RF95Interface.h | 94 +- src/mesh/RadioInterface.cpp | 756 +-- src/mesh/RadioInterface.h | 348 +- src/mesh/RadioLibInterface.cpp | 778 +-- src/mesh/RadioLibInterface.h | 435 +- src/mesh/RadioLibRF95.cpp | 88 +- src/mesh/RadioLibRF95.h | 83 +- src/mesh/ReliableRouter.cpp | 284 +- src/mesh/ReliableRouter.h | 55 +- src/mesh/Router.cpp | 1195 +++-- src/mesh/Router.h | 237 +- src/mesh/STM32WLE5JCInterface.cpp | 35 +- src/mesh/STM32WLE5JCInterface.h | 10 +- src/mesh/SX1262Interface.cpp | 7 +- src/mesh/SX1262Interface.h | 8 +- src/mesh/SX1268Interface.cpp | 20 +- src/mesh/SX1268Interface.h | 10 +- src/mesh/SX126xInterface.cpp | 461 +- src/mesh/SX126xInterface.h | 104 +- src/mesh/SX1280Interface.cpp | 7 +- src/mesh/SX1280Interface.h | 8 +- src/mesh/SX128xInterface.cpp | 354 +- src/mesh/SX128xInterface.h | 94 +- src/mesh/SinglePortModule.h | 50 +- src/mesh/StaticPointerQueue.h | 103 +- src/mesh/StreamAPI.cpp | 309 +- src/mesh/StreamAPI.h | 115 +- src/mesh/Throttle.cpp | 36 +- src/mesh/Throttle.h | 9 +- src/mesh/TypeConversions.cpp | 177 +- src/mesh/TypeConversions.h | 15 +- src/mesh/TypedQueue.h | 148 +- src/mesh/aes-ccm.cpp | 298 +- src/mesh/aes-ccm.h | 8 +- src/mesh/api/PacketAPI.cpp | 194 +- src/mesh/api/PacketAPI.h | 39 +- src/mesh/api/ServerAPI.cpp | 92 +- src/mesh/api/ServerAPI.h | 60 +- src/mesh/api/WiFiServerAPI.cpp | 33 +- src/mesh/api/WiFiServerAPI.h | 14 +- src/mesh/api/ethServerAPI.cpp | 22 +- src/mesh/api/ethServerAPI.h | 14 +- src/mesh/compression/unishox2.cpp | 2125 ++++---- src/mesh/compression/unishox2.h | 180 +- src/mesh/eth/ethClient.cpp | 244 +- src/mesh/http/ContentHandler.cpp | 1552 +++--- src/mesh/http/ContentHandler.h | 17 +- src/mesh/http/ContentHelper.cpp | 17 +- src/mesh/http/WebServer.cpp | 257 +- src/mesh/http/WebServer.h | 21 +- src/mesh/mesh-pb-constants.cpp | 85 +- src/mesh/mesh-pb-constants.h | 27 +- src/mesh/raspihttp/PiWebServer.cpp | 703 +-- src/mesh/raspihttp/PiWebServer.h | 58 +- src/mesh/udp/UdpMulticastHandler.h | 109 +- src/mesh/wifi/WiFiAPClient.cpp | 782 +-- src/meshUtils.cpp | 125 +- src/meshUtils.h | 13 +- src/modules/AdminModule.cpp | 2485 ++++----- src/modules/AdminModule.h | 100 +- src/modules/AtakPluginModule.cpp | 316 +- src/modules/AtakPluginModule.h | 27 +- src/modules/CannedMessageModule.cpp | 4120 ++++++++------- src/modules/CannedMessageModule.h | 393 +- src/modules/DetectionSensorModule.cpp | 219 +- src/modules/DetectionSensorModule.h | 27 +- src/modules/DropzoneModule.cpp | 123 +- src/modules/DropzoneModule.h | 38 +- src/modules/ExternalNotificationModule.cpp | 850 +-- src/modules/ExternalNotificationModule.h | 77 +- src/modules/GenericThreadModule.cpp | 23 +- src/modules/GenericThreadModule.h | 15 +- src/modules/KeyVerificationModule.cpp | 531 +- src/modules/KeyVerificationModule.h | 94 +- src/modules/Modules.cpp | 219 +- src/modules/NeighborInfoModule.cpp | 337 +- src/modules/NeighborInfoModule.h | 103 +- src/modules/NodeInfoModule.cpp | 326 +- src/modules/NodeInfoModule.h | 64 +- src/modules/OnScreenKeyboardModule.cpp | 464 +- src/modules/OnScreenKeyboardModule.h | 57 +- src/modules/PositionModule.cpp | 891 ++-- src/modules/PositionModule.h | 106 +- src/modules/PowerStressModule.cpp | 214 +- src/modules/PowerStressModule.h | 45 +- src/modules/RangeTestModule.cpp | 493 +- src/modules/RangeTestModule.h | 65 +- src/modules/RemoteHardwareModule.cpp | 213 +- src/modules/RemoteHardwareModule.h | 57 +- src/modules/ReplyModule.cpp | 23 +- src/modules/ReplyModule.h | 23 +- src/modules/RoutingModule.cpp | 140 +- src/modules/RoutingModule.h | 46 +- src/modules/SerialModule.cpp | 1000 ++-- src/modules/SerialModule.h | 85 +- src/modules/StatusLEDModule.cpp | 167 +- src/modules/StatusLEDModule.h | 45 +- src/modules/StoreForwardModule.cpp | 875 +-- src/modules/StoreForwardModule.h | 164 +- src/modules/SystemCommandsModule.cpp | 190 +- src/modules/SystemCommandsModule.h | 13 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 370 +- src/modules/Telemetry/AirQualityTelemetry.h | 89 +- src/modules/Telemetry/DeviceTelemetry.cpp | 281 +- src/modules/Telemetry/DeviceTelemetry.h | 94 +- .../Telemetry/EnvironmentTelemetry.cpp | 851 +-- src/modules/Telemetry/EnvironmentTelemetry.h | 84 +- src/modules/Telemetry/HealthTelemetry.cpp | 374 +- src/modules/Telemetry/HealthTelemetry.h | 73 +- src/modules/Telemetry/HostMetrics.cpp | 160 +- src/modules/Telemetry/HostMetrics.h | 59 +- src/modules/Telemetry/PowerTelemetry.cpp | 407 +- src/modules/Telemetry/PowerTelemetry.h | 73 +- src/modules/Telemetry/Sensor/AHT10.cpp | 42 +- src/modules/Telemetry/Sensor/AHT10.h | 15 +- src/modules/Telemetry/Sensor/BH1750Sensor.cpp | 56 +- src/modules/Telemetry/Sensor/BH1750Sensor.h | 15 +- src/modules/Telemetry/Sensor/BME280Sensor.cpp | 50 +- src/modules/Telemetry/Sensor/BME280Sensor.h | 15 +- src/modules/Telemetry/Sensor/BME680Sensor.cpp | 221 +- src/modules/Telemetry/Sensor/BME680Sensor.h | 51 +- src/modules/Telemetry/Sensor/BMP085Sensor.cpp | 28 +- src/modules/Telemetry/Sensor/BMP085Sensor.h | 15 +- src/modules/Telemetry/Sensor/BMP280Sensor.cpp | 46 +- src/modules/Telemetry/Sensor/BMP280Sensor.h | 15 +- src/modules/Telemetry/Sensor/BMP3XXSensor.cpp | 113 +- src/modules/Telemetry/Sensor/BMP3XXSensor.h | 52 +- .../Telemetry/Sensor/CGRadSensSensor.cpp | 76 +- .../Telemetry/Sensor/CGRadSensSensor.h | 23 +- src/modules/Telemetry/Sensor/CurrentSensor.h | 7 +- .../Telemetry/Sensor/DFRobotGravitySensor.cpp | 51 +- .../Telemetry/Sensor/DFRobotGravitySensor.h | 17 +- .../Telemetry/Sensor/DFRobotLarkSensor.cpp | 62 +- .../Telemetry/Sensor/DFRobotLarkSensor.h | 15 +- src/modules/Telemetry/Sensor/DPS310Sensor.cpp | 48 +- src/modules/Telemetry/Sensor/DPS310Sensor.h | 15 +- src/modules/Telemetry/Sensor/INA219Sensor.cpp | 48 +- src/modules/Telemetry/Sensor/INA219Sensor.h | 23 +- src/modules/Telemetry/Sensor/INA226Sensor.cpp | 89 +- src/modules/Telemetry/Sensor/INA226Sensor.h | 33 +- src/modules/Telemetry/Sensor/INA260Sensor.cpp | 39 +- src/modules/Telemetry/Sensor/INA260Sensor.h | 21 +- .../Telemetry/Sensor/INA3221Sensor.cpp | 128 +- src/modules/Telemetry/Sensor/INA3221Sensor.h | 51 +- .../Telemetry/Sensor/IndicatorSensor.cpp | 270 +- .../Telemetry/Sensor/IndicatorSensor.h | 15 +- .../Telemetry/Sensor/LPS22HBSensor.cpp | 38 +- src/modules/Telemetry/Sensor/LPS22HBSensor.h | 15 +- .../Telemetry/Sensor/LTR390UVSensor.cpp | 83 +- src/modules/Telemetry/Sensor/LTR390UVSensor.h | 19 +- .../Telemetry/Sensor/MAX17048Sensor.cpp | 240 +- src/modules/Telemetry/Sensor/MAX17048Sensor.h | 100 +- .../Telemetry/Sensor/MAX30102Sensor.cpp | 116 +- src/modules/Telemetry/Sensor/MAX30102Sensor.h | 21 +- .../Telemetry/Sensor/MCP9808Sensor.cpp | 30 +- src/modules/Telemetry/Sensor/MCP9808Sensor.h | 15 +- .../Telemetry/Sensor/MLX90614Sensor.cpp | 52 +- src/modules/Telemetry/Sensor/MLX90614Sensor.h | 19 +- .../Telemetry/Sensor/MLX90632Sensor.cpp | 36 +- src/modules/Telemetry/Sensor/MLX90632Sensor.h | 15 +- .../Telemetry/Sensor/NAU7802Sensor.cpp | 213 +- src/modules/Telemetry/Sensor/NAU7802Sensor.h | 31 +- .../Telemetry/Sensor/OPT3001Sensor.cpp | 58 +- src/modules/Telemetry/Sensor/OPT3001Sensor.h | 17 +- .../Telemetry/Sensor/PCT2075Sensor.cpp | 20 +- src/modules/Telemetry/Sensor/PCT2075Sensor.h | 15 +- .../Telemetry/Sensor/RAK12035Sensor.cpp | 171 +- src/modules/Telemetry/Sensor/RAK12035Sensor.h | 19 +- .../Telemetry/Sensor/RAK9154Sensor.cpp | 274 +- src/modules/Telemetry/Sensor/RAK9154Sensor.h | 29 +- .../Telemetry/Sensor/RCWL9620Sensor.cpp | 94 +- src/modules/Telemetry/Sensor/RCWL9620Sensor.h | 29 +- src/modules/Telemetry/Sensor/SHT31Sensor.cpp | 26 +- src/modules/Telemetry/Sensor/SHT31Sensor.h | 15 +- src/modules/Telemetry/Sensor/SHT4XSensor.cpp | 54 +- src/modules/Telemetry/Sensor/SHT4XSensor.h | 15 +- src/modules/Telemetry/Sensor/SHTC3Sensor.cpp | 28 +- src/modules/Telemetry/Sensor/SHTC3Sensor.h | 15 +- src/modules/Telemetry/Sensor/T1000xSensor.cpp | 142 +- src/modules/Telemetry/Sensor/T1000xSensor.h | 15 +- .../Telemetry/Sensor/TSL2561Sensor.cpp | 36 +- src/modules/Telemetry/Sensor/TSL2561Sensor.h | 17 +- .../Telemetry/Sensor/TSL2591Sensor.cpp | 40 +- src/modules/Telemetry/Sensor/TSL2591Sensor.h | 15 +- .../Telemetry/Sensor/TelemetrySensor.h | 84 +- .../Telemetry/Sensor/VEML7700Sensor.cpp | 61 +- src/modules/Telemetry/Sensor/VEML7700Sensor.h | 25 +- src/modules/Telemetry/Sensor/VoltageSensor.h | 7 +- src/modules/Telemetry/Sensor/nullSensor.cpp | 20 +- src/modules/Telemetry/Sensor/nullSensor.h | 21 +- src/modules/Telemetry/UnitConversions.cpp | 20 +- src/modules/Telemetry/UnitConversions.h | 13 +- src/modules/TextMessageModule.cpp | 52 +- src/modules/TextMessageModule.h | 29 +- src/modules/TraceRouteModule.cpp | 1488 +++--- src/modules/TraceRouteModule.h | 97 +- src/modules/WaypointModule.cpp | 234 +- src/modules/WaypointModule.h | 33 +- src/modules/esp32/AudioModule.cpp | 446 +- src/modules/esp32/AudioModule.h | 79 +- src/modules/esp32/PaxcounterModule.cpp | 163 +- src/modules/esp32/PaxcounterModule.h | 31 +- src/motion/AccelerometerThread.h | 224 +- src/motion/BMA423Sensor.cpp | 76 +- src/motion/BMA423Sensor.h | 17 +- src/motion/BMM150Sensor.cpp | 98 +- src/motion/BMM150Sensor.h | 52 +- src/motion/BMX160Sensor.cpp | 195 +- src/motion/BMX160Sensor.h | 28 +- src/motion/ICM20948Sensor.cpp | 448 +- src/motion/ICM20948Sensor.h | 69 +- src/motion/LIS3DHSensor.cpp | 44 +- src/motion/LIS3DHSensor.h | 15 +- src/motion/LSM6DS3Sensor.cpp | 36 +- src/motion/LSM6DS3Sensor.h | 15 +- src/motion/MPU6050Sensor.cpp | 38 +- src/motion/MPU6050Sensor.h | 15 +- src/motion/MotionSensor.cpp | 97 +- src/motion/MotionSensor.h | 57 +- src/motion/QMA6100PSensor.cpp | 227 +- src/motion/QMA6100PSensor.h | 54 +- src/motion/STK8XXXSensor.cpp | 40 +- src/motion/STK8XXXSensor.h | 22 +- src/mqtt/MQTT.cpp | 1389 ++--- src/mqtt/MQTT.h | 139 +- src/mqtt/ServiceEnvelope.cpp | 24 +- src/mqtt/ServiceEnvelope.h | 12 +- src/network-stubs.cpp | 20 +- src/nimble/NimbleBluetooth.cpp | 1471 +++--- src/nimble/NimbleBluetooth.h | 31 +- src/platform/esp32/BleOta.cpp | 61 +- src/platform/esp32/BleOta.h | 17 +- src/platform/esp32/ESP32CryptoEngine.cpp | 54 +- src/platform/esp32/SimpleAllocator.cpp | 32 +- src/platform/esp32/SimpleAllocator.h | 30 +- src/platform/esp32/WiFiOTA.cpp | 123 +- src/platform/esp32/WiFiOTA.h | 3 +- src/platform/esp32/architecture.h | 4 +- src/platform/esp32/iram-quirk.c | 5 +- src/platform/esp32/main-esp32.cpp | 291 +- .../heltec_wireless_tracker/variant.cpp | 35 +- .../extra_variants/t_deck_pro/variant.cpp | 24 +- .../extra_variants/t_lora_pager/variant.cpp | 27 +- .../tbeam_displayshield/variant.cpp | 48 +- src/platform/nrf52/AsyncUDP.cpp | 80 +- src/platform/nrf52/AsyncUDP.h | 61 +- src/platform/nrf52/BLEDfuScure.cpp | 156 +- src/platform/nrf52/BLEDfuSecure.h | 13 +- src/platform/nrf52/NRF52Bluetooth.cpp | 653 +-- src/platform/nrf52/NRF52Bluetooth.h | 33 +- src/platform/nrf52/NRF52CryptoEngine.cpp | 42 +- src/platform/nrf52/aes-256/tiny-aes.cpp | 341 +- src/platform/nrf52/aes-256/tiny-aes.h | 4 +- src/platform/nrf52/alloc.cpp | 28 +- src/platform/nrf52/architecture.h | 4 +- src/platform/nrf52/hardfault.cpp | 147 +- src/platform/nrf52/main-nrf52.cpp | 616 +-- src/platform/nrf52/softdevice/ble.h | 241 +- src/platform/nrf52/softdevice/ble_err.h | 4 +- src/platform/nrf52/softdevice/ble_gap.h | 1782 +++---- src/platform/nrf52/softdevice/ble_gatt.h | 84 +- src/platform/nrf52/softdevice/ble_gattc.h | 390 +- src/platform/nrf52/softdevice/ble_gatts.h | 536 +- src/platform/nrf52/softdevice/ble_hci.h | 4 +- src/platform/nrf52/softdevice/ble_l2cap.h | 189 +- src/platform/nrf52/softdevice/ble_types.h | 142 +- src/platform/nrf52/softdevice/nrf52/nrf_mbr.h | 61 +- src/platform/nrf52/softdevice/nrf_error_sdm.h | 9 +- src/platform/nrf52/softdevice/nrf_nvic.h | 320 +- src/platform/nrf52/softdevice/nrf_sdm.h | 116 +- src/platform/nrf52/softdevice/nrf_soc.h | 377 +- src/platform/nrf52/softdevice/nrf_svc.h | 27 +- src/platform/portduino/PortduinoGlue.cpp | 1477 +++--- src/platform/portduino/PortduinoGlue.h | 884 ++-- src/platform/portduino/SimRadio.cpp | 528 +- src/platform/portduino/SimRadio.h | 117 +- src/platform/portduino/USBHal.h | 228 +- .../hardware_rosc/include/hardware/rosc.h | 31 +- src/platform/rp2xx0/hardware_rosc/rosc.c | 77 +- src/platform/rp2xx0/main-rp2xx0.cpp | 211 +- .../rp2xx0/pico_sleep/include/pico/sleep.h | 20 +- src/platform/rp2xx0/pico_sleep/sleep.c | 165 +- src/platform/stm32wl/LittleFS.cpp | 159 +- src/platform/stm32wl/LittleFS.h | 11 +- src/platform/stm32wl/STM32_LittleFS.cpp | 344 +- src/platform/stm32wl/STM32_LittleFS.h | 95 +- src/platform/stm32wl/STM32_LittleFS_File.cpp | 592 ++- src/platform/stm32wl/STM32_LittleFS_File.h | 99 +- src/platform/stm32wl/architecture.h | 2 +- src/platform/stm32wl/littlefs/lfs.c | 4684 +++++++++-------- src/platform/stm32wl/littlefs/lfs.h | 304 +- src/platform/stm32wl/littlefs/lfs_util.c | 21 +- src/platform/stm32wl/littlefs/lfs_util.h | 110 +- src/platform/stm32wl/main-stm32wl.cpp | 48 +- src/power.h | 59 +- src/serialization/JSON.cpp | 252 +- src/serialization/JSON.h | 44 +- src/serialization/JSONValue.cpp | 957 ++-- src/serialization/JSONValue.h | 93 +- src/serialization/MeshPacketSerializer.cpp | 835 +-- src/serialization/MeshPacketSerializer.h | 28 +- .../MeshPacketSerializer_nRF52.cpp | 751 +-- src/serialization/cobs.cpp | 216 +- src/serialization/cobs.h | 24 +- src/sleep.cpp | 598 +-- src/xmodem.cpp | 371 +- src/xmodem.h | 45 +- test/TestUtil.cpp | 17 +- test/test_crypto/test_main.cpp | 299 +- .../ports/test_encrypted.cpp | 71 +- .../ports/test_nodeinfo.cpp | 66 +- .../ports/test_position.cpp | 76 +- .../ports/test_telemetry.cpp | 776 +-- .../ports/test_text_message.cpp | 133 +- .../ports/test_waypoint.cpp | 70 +- .../test_meshpacket_serializer/test_helpers.h | 61 +- .../test_serializer.cpp | 56 +- test/test_mqtt/MQTT.cpp | 1188 +++-- test/test_serial/SerialModule.cpp | 162 +- variants/esp32/chatter2/variant.h | 43 +- variants/esp32/diy/dr-dev/variant.h | 6 +- variants/esp32/diy/hydra/variant.h | 5 +- .../esp32/heltec_wireless_bridge/variant.h | 3 +- variants/esp32/nano-g1-explorer/variant.h | 4 +- variants/esp32/nano-g1/variant.h | 4 +- .../esp32/radiomaster_900_bandit/variant.h | 8 +- variants/esp32/rak11200/variant.h | 3 +- variants/esp32/station-g1/variant.h | 6 +- variants/esp32/tbeam/variant.h | 8 +- variants/esp32/tlora_v2/variant.h | 6 +- variants/esp32/wiphone/variant.h | 10 +- variants/esp32c6/m5stack_unitc6l/variant.cpp | 81 +- variants/esp32s3/CDEBYTE_EoRa-S3/variant.h | 25 +- variants/esp32s3/EBYTE_ESP32-S3/variant.h | 172 +- variants/esp32s3/dreamcatcher/rfswitch.h | 3 +- .../heltec_vision_master_e213/einkDetect.h | 41 +- .../heltec_vision_master_e213/nicheGraphics.h | 129 +- .../heltec_vision_master_e290/nicheGraphics.h | 109 +- .../heltec_wireless_paper/einkDetect.h | 41 +- .../heltec_wireless_paper/nicheGraphics.h | 117 +- .../esp32s3/heltec_wireless_tracker/variant.h | 9 +- variants/esp32s3/picomputer-s3/variant.h | 12 +- .../seeed-sensecap-indicator/variant.h | 8 +- variants/esp32s3/t-deck-pro/variant.h | 4 +- variants/esp32s3/t-deck/variant.h | 4 +- variants/esp32s3/t-eth-elite/rfswitch.h | 6 +- variants/esp32s3/t-eth-elite/variant.h | 4 +- variants/esp32s3/tbeam-s3-core/rfswitch.h | 6 +- variants/esp32s3/tbeam-s3-core/variant.h | 8 +- variants/esp32s3/tlora-pager/rfswitch.h | 6 +- .../esp32s3/tlora_t3s3_epaper/nicheGraphics.h | 93 +- variants/esp32s3/tlora_t3s3_v1/rfswitch.h | 6 +- variants/esp32s3/tlora_t3s3_v1/variant.h | 4 +- .../esp32s3/tracksenger/internal/variant.h | 12 +- variants/esp32s3/tracksenger/lcd/variant.h | 12 +- variants/esp32s3/tracksenger/oled/variant.h | 12 +- variants/esp32s3/unphone/variant.cpp | 27 +- variants/esp32s3/unphone/variant.h | 8 +- .../Dongle_nRF52840-pca10059-v1/variant.cpp | 9 +- .../ELECROW-ThinkNode-M1/nicheGraphics.h | 137 +- .../nrf52840/ELECROW-ThinkNode-M1/variant.cpp | 17 +- .../nrf52840/ELECROW-ThinkNode-M1/variant.h | 4 +- .../nrf52840/ELECROW-ThinkNode-M3/rfswitch.h | 6 +- .../nrf52840/ELECROW-ThinkNode-M3/variant.cpp | 121 +- .../nrf52840/ELECROW-ThinkNode-M6/variant.cpp | 55 +- variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h | 6 +- variants/nrf52840/ME25LS01-4Y10TD/variant.cpp | 11 +- .../nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h | 6 +- .../ME25LS01-4Y10TD_e-ink/variant.cpp | 11 +- .../MakePython_nRF52840_eink/variant.cpp | 13 +- .../MakePython_nRF52840_oled/variant.cpp | 13 +- variants/nrf52840/TWC_mesh_v4/variant.cpp | 13 +- variants/nrf52840/canaryone/variant.cpp | 35 +- variants/nrf52840/canaryone/variant.h | 3 +- variants/nrf52840/cpp_overrides/lfs_util.h | 130 +- .../nrf52_promicro_diy_tcxo/nicheGraphics.h | 91 +- .../diy/nrf52_promicro_diy_tcxo/rfswitch.h | 3 +- .../diy/nrf52_promicro_diy_tcxo/variant.cpp | 9 +- .../diy/nrf52_promicro_diy_tcxo/variant.h | 8 +- .../variant.cpp | 9 +- .../gat562_mesh_trial_tracker/variant.cpp | 19 +- .../gat562_mesh_trial_tracker/variant.h | 12 +- .../nicheGraphics.h | 91 +- .../heltec_mesh_node_t114-inkhud/variant.cpp | 9 +- .../heltec_mesh_node_t114-inkhud/variant.h | 7 +- .../heltec_mesh_node_t114/variant.cpp | 9 +- .../nrf52840/heltec_mesh_node_t114/variant.h | 7 +- .../heltec_mesh_pocket/nicheGraphics.h | 95 +- .../nrf52840/heltec_mesh_pocket/variant.h | 4 +- .../heltec_mesh_solar/nicheGraphics.h | 95 +- .../nrf52840/heltec_mesh_solar/variant.cpp | 9 +- variants/nrf52840/heltec_mesh_solar/variant.h | 4 +- variants/nrf52840/meshlink/variant.cpp | 13 +- variants/nrf52840/meshtiny/variant.cpp | 33 +- variants/nrf52840/monteops_hw1/variant.cpp | 13 +- variants/nrf52840/monteops_hw1/variant.h | 4 +- variants/nrf52840/muzi_base/rfswitch.h | 6 +- variants/nrf52840/muzi_base/variant.cpp | 33 +- variants/nrf52840/muzi_base/variant.h | 57 +- variants/nrf52840/nano-g2-ultra/variant.cpp | 5 +- variants/nrf52840/nano-g2-ultra/variant.h | 3 +- variants/nrf52840/r1-neo/variant.cpp | 19 +- variants/nrf52840/rak2560/variant.cpp | 19 +- variants/nrf52840/rak2560/variant.h | 12 +- variants/nrf52840/rak3401_1watt/variant.cpp | 19 +- variants/nrf52840/rak3401_1watt/variant.h | 8 +- variants/nrf52840/rak4631/variant.cpp | 19 +- variants/nrf52840/rak4631/variant.h | 12 +- variants/nrf52840/rak4631_epaper/variant.cpp | 19 +- variants/nrf52840/rak4631_epaper/variant.h | 7 +- .../rak4631_epaper_onrxtx/variant.cpp | 19 +- .../nrf52840/rak4631_epaper_onrxtx/variant.h | 9 +- variants/nrf52840/rak4631_eth_gw/variant.cpp | 19 +- variants/nrf52840/rak4631_eth_gw/variant.h | 12 +- .../rak4631_nomadstar_meteor_pro/variant.cpp | 19 +- .../rak4631_nomadstar_meteor_pro/variant.h | 12 +- variants/nrf52840/rak_wismeshtag/variant.cpp | 19 +- variants/nrf52840/rak_wismeshtag/variant.h | 4 +- variants/nrf52840/rak_wismeshtap/variant.cpp | 19 +- variants/nrf52840/rak_wismeshtap/variant.h | 12 +- .../nrf52840/seeed_solar_node/variant.cpp | 35 +- variants/nrf52840/seeed_solar_node/variant.h | 9 +- .../nrf52840/seeed_wio_tracker_L1/variant.cpp | 25 +- .../nrf52840/seeed_wio_tracker_L1/variant.h | 3 +- .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 131 +- .../seeed_wio_tracker_L1_eink/variant.cpp | 23 +- .../seeed_wio_tracker_L1_eink/variant.h | 3 +- .../seeed_xiao_nrf52840_kit/variant.cpp | 23 +- variants/nrf52840/t-echo-lite/variant.cpp | 17 +- variants/nrf52840/t-echo/nicheGraphics.h | 141 +- variants/nrf52840/t-echo/variant.cpp | 17 +- variants/nrf52840/t-echo/variant.h | 12 +- variants/nrf52840/tracker-t1000-e/rfswitch.h | 4 +- variants/nrf52840/tracker-t1000-e/variant.cpp | 43 +- variants/nrf52840/wio-sdk-wm1110/rfswitch.h | 6 +- variants/nrf52840/wio-sdk-wm1110/variant.cpp | 19 +- variants/nrf52840/wio-t1000-s/rfswitch.h | 4 +- variants/nrf52840/wio-t1000-s/variant.cpp | 43 +- .../nrf52840/wio-tracker-wm1110/rfswitch.h | 6 +- .../nrf52840/wio-tracker-wm1110/variant.cpp | 19 +- variants/rp2040/ec_catsniffer/variant.cpp | 17 +- variants/stm32/CDEBYTE_E77-MBL/rfswitch.h | 4 +- 771 files changed, 83399 insertions(+), 77967 deletions(-) delete mode 100644 .clang-format diff --git a/.clang-format b/.clang-format deleted file mode 100644 index 5b4c5bf86..000000000 --- a/.clang-format +++ /dev/null @@ -1,2 +0,0 @@ -BasedOnStyle: LLVM -ColumnLimit: 150 diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index e87b8c094..71e88dbff 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -16,7 +16,8 @@ #include "mesh/TypeConversions.h" #include "mesh/mesh-pb-constants.h" -namespace { +namespace +{ constexpr uint32_t nodeId = 0x12345678; // Set to true when lateInitVariant finishes. Used to ensure lateInitVariant was called during startup. bool hasBeenConfigured = false; @@ -34,130 +35,135 @@ std::condition_variable loopCV; std::thread meshtasticThread; // This exception is thrown when the portuino main thread should exit. -class ShouldExitException : public std::runtime_error { -public: - using std::runtime_error::runtime_error; +class ShouldExitException : public std::runtime_error +{ + public: + using std::runtime_error::runtime_error; }; // Start the loop for one test case and wait till the loop has completed. This ensures fuzz // test cases do not overlap with one another. This helps the fuzzer attribute a crash to the // single, currently running, test case. -void runLoopOnce() { - realHardware = true; // Avoids delay(100) within portduino/main.cpp - std::unique_lock lck(loopLock); - fuzzerRunning = true; - loopCanRun = true; - loopCV.notify_one(); - loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; }); +void runLoopOnce() +{ + realHardware = true; // Avoids delay(100) within portduino/main.cpp + std::unique_lock lck(loopLock); + fuzzerRunning = true; + loopCanRun = true; + loopCV.notify_one(); + loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; }); } } // namespace // Called in the main Arduino loop function to determine if the loop can delay/sleep before running again. // We use this as a way to block the loop from sleeping and to start the loop function immediately when a // fuzzer input is ready. -bool loopCanSleep() { - std::unique_lock lck(loopLock); - loopIsWaiting = true; - loopCV.notify_one(); - loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; }); - loopIsWaiting = false; - if (loopShouldExit) - throw ShouldExitException("exit"); - if (!fuzzerRunning) - return true; // The loop can sleep before the fuzzer starts. - loopCanRun = false; // Only run the loop once before waiting again. - return false; +bool loopCanSleep() +{ + std::unique_lock lck(loopLock); + loopIsWaiting = true; + loopCV.notify_one(); + loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; }); + loopIsWaiting = false; + if (loopShouldExit) + throw ShouldExitException("exit"); + if (!fuzzerRunning) + return true; // The loop can sleep before the fuzzer starts. + loopCanRun = false; // Only run the loop once before waiting again. + return false; } // Called just prior to starting Meshtastic. Allows for setting config values before startup. -void lateInitVariant() { - portduino_config.logoutputlevel = level_error; - channelFile.channels[0] = meshtastic_Channel{ - .has_settings = true, - .settings = - meshtastic_ChannelSettings{ - .psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}}, - .name = "LongFast", - .uplink_enabled = true, - .has_module_settings = true, - .module_settings = {.position_precision = 16}, - }, - .role = meshtastic_Channel_Role_PRIMARY, - }; - config.security.admin_key[0] = { - .size = 32, - .bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, - 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c}, - }; - config.security.admin_key_count = 1; - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; - moduleConfig.has_mqtt = true; - moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{ - .enabled = true, - .proxy_to_client_enabled = true, - }; - moduleConfig.has_store_forward = true; - moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{ - .enabled = true, - .history_return_max = 4, - .history_return_window = 600, - .is_server = true, - }; - meshtastic_Position fixedGPS = meshtastic_Position{ - .has_latitude_i = true, - .latitude_i = static_cast(1 * 1e7), - .has_longitude_i = true, - .longitude_i = static_cast(3 * 1e7), - .has_altitude = true, - .altitude = 64, - .location_source = meshtastic_Position_LocSource_LOC_MANUAL, - }; - nodeDB->setLocalPosition(fixedGPS); - config.has_position = true; - config.position.fixed_position = true; - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum()); - info->has_position = true; - info->position = TypeConversions::ConvertToPositionLite(fixedGPS); - hasBeenConfigured = true; +void lateInitVariant() +{ + portduino_config.logoutputlevel = level_error; + channelFile.channels[0] = meshtastic_Channel{ + .has_settings = true, + .settings = + meshtastic_ChannelSettings{ + .psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}}, + .name = "LongFast", + .uplink_enabled = true, + .has_module_settings = true, + .module_settings = {.position_precision = 16}, + }, + .role = meshtastic_Channel_Role_PRIMARY, + }; + config.security.admin_key[0] = { + .size = 32, + .bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, + 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c}, + }; + config.security.admin_key_count = 1; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + moduleConfig.has_mqtt = true; + moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{ + .enabled = true, + .proxy_to_client_enabled = true, + }; + moduleConfig.has_store_forward = true; + moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{ + .enabled = true, + .history_return_max = 4, + .history_return_window = 600, + .is_server = true, + }; + meshtastic_Position fixedGPS = meshtastic_Position{ + .has_latitude_i = true, + .latitude_i = static_cast(1 * 1e7), + .has_longitude_i = true, + .longitude_i = static_cast(3 * 1e7), + .has_altitude = true, + .altitude = 64, + .location_source = meshtastic_Position_LocSource_LOC_MANUAL, + }; + nodeDB->setLocalPosition(fixedGPS); + config.has_position = true; + config.position.fixed_position = true; + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum()); + info->has_position = true; + info->position = TypeConversions::ConvertToPositionLite(fixedGPS); + hasBeenConfigured = true; } extern "C" { int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary. // Start Meshtastic in a thread and wait till it has reached the ON state. -int LLVMFuzzerInitialize(int *argc, char ***argv) { - portduino_config.maxtophone = 5; +int LLVMFuzzerInitialize(int *argc, char ***argv) +{ + portduino_config.maxtophone = 5; - meshtasticThread = std::thread([program = *argv[0]]() { - char nodeIdStr[12]; - strcpy(nodeIdStr, std::to_string(nodeId).c_str()); - int argc = 7; - char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr}; - try { - portduino_main(argc, argv); - } catch (const ShouldExitException &) { - } - }); - std::atexit([] { - { - const std::lock_guard lck(loopLock); - loopShouldExit = true; - loopCV.notify_one(); - } - meshtasticThread.join(); - }); + meshtasticThread = std::thread([program = *argv[0]]() { + char nodeIdStr[12]; + strcpy(nodeIdStr, std::to_string(nodeId).c_str()); + int argc = 7; + char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr}; + try { + portduino_main(argc, argv); + } catch (const ShouldExitException &) { + } + }); + std::atexit([] { + { + const std::lock_guard lck(loopLock); + loopShouldExit = true; + loopCV.notify_one(); + } + meshtasticThread.join(); + }); - // Wait for startup. - for (int i = 1; i < 20; ++i) { - if (powerFSM.getState() == &stateON) { - assert(hasBeenConfigured); - assert(router); - assert(nodeDB); - return 0; + // Wait for startup. + for (int i = 1; i < 20; ++i) { + if (powerFSM.getState() == &stateON) { + assert(hasBeenConfigured); + assert(router); + assert(nodeDB); + return 0; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); } - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - return 1; + return 1; } // This is the main entrypoint for the fuzzer (the fuzz target). The fuzzer will provide an array of bytes to be @@ -167,33 +173,34 @@ int LLVMFuzzerInitialize(int *argc, char ***argv) { // // This guide provides best practices for writing a fuzzer target. // https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length) { - meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default; - pb_istream_t stream = pb_istream_from_buffer(data, length); - // Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa. - if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed || p.public_key.size || - p.next_hop || p.relay_node || p.tx_after) - return -1; // Reject: The input will not be added to the corpus. - if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - meshtastic_Data d; - stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size); - if (!pb_decode(&stream, &meshtastic_Data_msg, &d)) - return -1; // Reject: The input will not be added to the corpus. - } +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length) +{ + meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default; + pb_istream_t stream = pb_istream_from_buffer(data, length); + // Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa. + if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed || + p.public_key.size || p.next_hop || p.relay_node || p.tx_after) + return -1; // Reject: The input will not be added to the corpus. + if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + meshtastic_Data d; + stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size); + if (!pb_decode(&stream, &meshtastic_Data_msg, &d)) + return -1; // Reject: The input will not be added to the corpus. + } - // Provide default values for a few fields so the fuzzer doesn't need to guess them. - if (p.from == 0) - p.from = nodeDB->getNodeNum(); - if (p.to == 0) - p.to = nodeDB->getNodeNum(); - static uint32_t packetId = 0; - if (p.id == 0) - p.id == ++packetId; - if (p.pki_encrypted && config.security.admin_key_count) - memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key)); + // Provide default values for a few fields so the fuzzer doesn't need to guess them. + if (p.from == 0) + p.from = nodeDB->getNodeNum(); + if (p.to == 0) + p.to = nodeDB->getNodeNum(); + static uint32_t packetId = 0; + if (p.id == 0) + p.id == ++packetId; + if (p.pki_encrypted && config.security.admin_key_count) + memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key)); - router->enqueueReceivedMessage(packetPool.allocCopy(p)); - runLoopOnce(); - return 0; // Accept: The input may be added to the corpus. + router->enqueueReceivedMessage(packetPool.allocCopy(p)); + runLoopOnce(); + return 0; // Accept: The input may be added to the corpus. } } \ No newline at end of file diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index 59068f4e0..947b1e054 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -21,183 +21,192 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); extern unPhone unphone; #endif -namespace concurrency { -class AmbientLightingThread : public concurrency::OSThread { -public: - explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") { - notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. +namespace concurrency +{ +class AmbientLightingThread : public concurrency::OSThread +{ + public: + explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") + { + notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. // Enables Ambient Lighting by default if conditions are meet. #ifdef HAS_RGB_LED #ifdef ENABLE_AMBIENTLIGHTING - moduleConfig.ambient_lighting.led_state = true; + moduleConfig.ambient_lighting.led_state = true; #endif #endif - // Uncomment to test module - // moduleConfig.ambient_lighting.led_state = true; - // moduleConfig.ambient_lighting.current = 10; - // Default to a color based on our node number - // moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; - // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; - // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; + // Uncomment to test module + // moduleConfig.ambient_lighting.led_state = true; + // moduleConfig.ambient_lighting.current = 10; + // Default to a color based on our node number + // moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; + // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; + // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; #if defined(HAS_NCP5623) || defined(HAS_LP5562) - _type = type; - if (_type == ScanI2C::DeviceType::NONE) { - LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus"); - disable(); - return; - } + _type = type; + if (_type == ScanI2C::DeviceType::NONE) { + LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus"); + disable(); + return; + } #endif #ifdef HAS_RGB_LED - if (!moduleConfig.ambient_lighting.led_state) { - LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); - disable(); - return; - } - LOG_DEBUG("AmbientLighting init"); + if (!moduleConfig.ambient_lighting.led_state) { + LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); + disable(); + return; + } + LOG_DEBUG("AmbientLighting init"); #ifdef HAS_NCP5623 - if (_type == ScanI2C::NCP5623) { - rgb.begin(); + if (_type == ScanI2C::NCP5623) { + rgb.begin(); #endif #ifdef HAS_LP5562 - if (_type == ScanI2C::LP5562) { - rgbw.begin(); + if (_type == ScanI2C::LP5562) { + rgbw.begin(); #endif #ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); + pinMode(RGBLED_RED, OUTPUT); + pinMode(RGBLED_GREEN, OUTPUT); + pinMode(RGBLED_BLUE, OUTPUT); #endif #ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); + pixels.begin(); // Initialise the pixel(s) + pixels.clear(); // Set all pixel colors to 'off' + pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - setLighting(); + setLighting(); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) - } + } #endif - } + } - protected: - int32_t runOnce() override { + protected: + int32_t runOnce() override + { #ifdef HAS_RGB_LED #if defined(HAS_NCP5623) || defined(HAS_LP5562) - if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { + if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif - setLighting(); - return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification + setLighting(); + return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) - } + } #endif #endif - return disable(); - } + return disable(); + } - // When shutdown() is issued, setLightingOff will be called. - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &AmbientLightingThread::setLightingOff); + // When shutdown() is issued, setLightingOff will be called. + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &AmbientLightingThread::setLightingOff); - private: - ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; + private: + ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; - // Turn RGB lighting off, is used in junction to shutdown() - int setLightingOff(void *unused) { + // Turn RGB lighting off, is used in junction to shutdown() + int setLightingOff(void *unused) + { #ifdef HAS_NCP5623 - rgb.setCurrent(0); - rgb.setRed(0); - rgb.setGreen(0); - rgb.setBlue(0); - LOG_INFO("OFF: NCP5623 Ambient lighting"); + rgb.setCurrent(0); + rgb.setRed(0); + rgb.setGreen(0); + rgb.setBlue(0); + LOG_INFO("OFF: NCP5623 Ambient lighting"); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(0); - rgbw.setRed(0); - rgbw.setGreen(0); - rgbw.setBlue(0); - rgbw.setWhite(0); - LOG_INFO("OFF: LP5562 Ambient lighting"); + rgbw.setCurrent(0); + rgbw.setRed(0); + rgbw.setGreen(0); + rgbw.setBlue(0); + rgbw.setWhite(0); + LOG_INFO("OFF: LP5562 Ambient lighting"); #endif #ifdef HAS_NEOPIXEL - pixels.clear(); - pixels.show(); - LOG_INFO("OFF: NeoPixel Ambient lighting"); + pixels.clear(); + pixels.show(); + LOG_INFO("OFF: NeoPixel Ambient lighting"); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - 0); - analogWrite(RGBLED_GREEN, 255 - 0); - analogWrite(RGBLED_BLUE, 255 - 0); - LOG_INFO("OFF: Ambient light RGB Common Anode"); + analogWrite(RGBLED_RED, 255 - 0); + analogWrite(RGBLED_GREEN, 255 - 0); + analogWrite(RGBLED_BLUE, 255 - 0); + LOG_INFO("OFF: Ambient light RGB Common Anode"); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, 0); - analogWrite(RGBLED_GREEN, 0); - analogWrite(RGBLED_BLUE, 0); - LOG_INFO("OFF: Ambient light RGB Common Cathode"); + analogWrite(RGBLED_RED, 0); + analogWrite(RGBLED_GREEN, 0); + analogWrite(RGBLED_BLUE, 0); + LOG_INFO("OFF: Ambient light RGB Common Cathode"); #endif #ifdef UNPHONE - unphone.rgb(0, 0, 0); - LOG_INFO("OFF: unPhone Ambient lighting"); + unphone.rgb(0, 0, 0); + LOG_INFO("OFF: unPhone Ambient lighting"); #endif - return 0; - } + return 0; + } - void setLighting() { + void setLighting() + { #ifdef HAS_NCP5623 - rgb.setCurrent(moduleConfig.ambient_lighting.current); - rgb.setRed(moduleConfig.ambient_lighting.red); - rgb.setGreen(moduleConfig.ambient_lighting.green); - rgb.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgb.setCurrent(moduleConfig.ambient_lighting.current); + rgb.setRed(moduleConfig.ambient_lighting.red); + rgb.setGreen(moduleConfig.ambient_lighting.green); + rgb.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", + moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(moduleConfig.ambient_lighting.current); - rgbw.setRed(moduleConfig.ambient_lighting.red); - rgbw.setGreen(moduleConfig.ambient_lighting.green); - rgbw.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgbw.setCurrent(moduleConfig.ambient_lighting.current); + rgbw.setRed(moduleConfig.ambient_lighting.red); + rgbw.setGreen(moduleConfig.ambient_lighting.green); + rgbw.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, + moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue), 0, - NEOPIXEL_COUNT); + pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue), + 0, NEOPIXEL_COUNT); // RadioMaster Bandit has addressable LED at the two buttons // this allow us to set different lighting for them in variant.h file. #ifdef RADIOMASTER_900_BANDIT #if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX) - pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); + pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) - pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); + pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); #endif #endif - pixels.show(); - // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + pixels.show(); + // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", + // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); + analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); + analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); + analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); + analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef UNPHONE - unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue); + unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif - } - }; + } + }; } // namespace concurrency diff --git a/src/AudioThread.h b/src/AudioThread.h index 79f57f730..23552c421 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -18,86 +18,93 @@ extern ExtensionIOXL9555 io; #define AUDIO_THREAD_INTERVAL_MS 100 -class AudioThread : public concurrency::OSThread { -public: - AudioThread() : OSThread("Audio") { initOutput(); } +class AudioThread : public concurrency::OSThread +{ + public: + AudioThread() : OSThread("Audio") { initOutput(); } - void beginRttl(const void *data, uint32_t len) { + void beginRttl(const void *data, uint32_t len) + { #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif - setCPUFast(true); - rtttlFile = new AudioFileSourcePROGMEM(data, len); - i2sRtttl = new AudioGeneratorRTTTL(); - i2sRtttl->begin(rtttlFile, audioOut); - } - - // Also handles actually playing the RTTTL, needs to be called in loop - bool isPlaying() { - if (i2sRtttl != nullptr) { - return i2sRtttl->isRunning() && i2sRtttl->loop(); - } - return false; - } - - void stop() { - if (i2sRtttl != nullptr) { - i2sRtttl->stop(); - delete i2sRtttl; - i2sRtttl = nullptr; + setCPUFast(true); + rtttlFile = new AudioFileSourcePROGMEM(data, len); + i2sRtttl = new AudioGeneratorRTTTL(); + i2sRtttl->begin(rtttlFile, audioOut); } - if (rtttlFile != nullptr) { - delete rtttlFile; - rtttlFile = nullptr; + // Also handles actually playing the RTTTL, needs to be called in loop + bool isPlaying() + { + if (i2sRtttl != nullptr) { + return i2sRtttl->isRunning() && i2sRtttl->loop(); + } + return false; } - setCPUFast(false); -#ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); -#endif - } + void stop() + { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; + } - void readAloud(const char *text) { - if (i2sRtttl != nullptr) { - i2sRtttl->stop(); - delete i2sRtttl; - i2sRtttl = nullptr; + if (rtttlFile != nullptr) { + delete rtttlFile; + rtttlFile = nullptr; + } + + setCPUFast(false); +#ifdef T_LORA_PAGER + io.digitalWrite(EXPANDS_AMP_EN, LOW); +#endif } + void readAloud(const char *text) + { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; + } + #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif - ESP8266SAM *sam = new ESP8266SAM; - sam->Say(audioOut, text); - delete sam; - setCPUFast(false); + ESP8266SAM *sam = new ESP8266SAM; + sam->Say(audioOut, text); + delete sam; + setCPUFast(false); #ifdef T_LORA_PAGER - io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.digitalWrite(EXPANDS_AMP_EN, LOW); #endif - } + } -protected: - int32_t runOnce() override { - canSleep = true; // Assume we should not keep the board awake + protected: + int32_t runOnce() override + { + canSleep = true; // Assume we should not keep the board awake - // if (i2sRtttl != nullptr && i2sRtttl->isRunning()) { - // i2sRtttl->loop(); - // } - return AUDIO_THREAD_INTERVAL_MS; - } + // if (i2sRtttl != nullptr && i2sRtttl->isRunning()) { + // i2sRtttl->loop(); + // } + return AUDIO_THREAD_INTERVAL_MS; + } -private: - void initOutput() { - audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); - audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); - audioOut->SetGain(0.2); - }; + private: + void initOutput() + { + audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); + audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); + audioOut->SetGain(0.2); + }; - AudioGeneratorRTTTL *i2sRtttl = nullptr; - AudioOutputI2S *audioOut = nullptr; + AudioGeneratorRTTTL *i2sRtttl = nullptr; + AudioOutputI2S *audioOut = nullptr; - AudioFileSourcePROGMEM *rtttlFile = nullptr; + AudioFileSourcePROGMEM *rtttlFile = nullptr; }; #endif diff --git a/src/BluetoothCommon.cpp b/src/BluetoothCommon.cpp index 7fc6be4df..d9502e4f5 100644 --- a/src/BluetoothCommon.cpp +++ b/src/BluetoothCommon.cpp @@ -3,9 +3,15 @@ // NRF52 wants these constants as byte arrays // Generated here https://yupana-engineering.com/online-uuid-to-c-array-converter - but in REVERSE BYTE ORDER -const uint8_t MESH_SERVICE_UUID_16[16u] = {0xfd, 0xea, 0x73, 0xe2, 0xca, 0x5d, 0xa8, 0x9f, 0x1f, 0x46, 0xa8, 0x15, 0x18, 0xb2, 0xa1, 0x6b}; -const uint8_t TORADIO_UUID_16[16u] = {0xe7, 0x01, 0x44, 0x12, 0x66, 0x78, 0xdd, 0xa1, 0xad, 0x4d, 0x9e, 0x12, 0xd2, 0x76, 0x5c, 0xf7}; -const uint8_t FROMRADIO_UUID_16[16u] = {0x02, 0x00, 0x12, 0xac, 0x42, 0x02, 0x78, 0xb8, 0xed, 0x11, 0x93, 0x49, 0x9e, 0xe6, 0x55, 0x2c}; -const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70, 0xa6, 0x66, 0x4f, 0x00, 0xa8, 0x8c, 0xa1, 0x9d, 0xed}; -const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa, 0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c}; -const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99, 0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A}; \ No newline at end of file +const uint8_t MESH_SERVICE_UUID_16[16u] = {0xfd, 0xea, 0x73, 0xe2, 0xca, 0x5d, 0xa8, 0x9f, + 0x1f, 0x46, 0xa8, 0x15, 0x18, 0xb2, 0xa1, 0x6b}; +const uint8_t TORADIO_UUID_16[16u] = {0xe7, 0x01, 0x44, 0x12, 0x66, 0x78, 0xdd, 0xa1, + 0xad, 0x4d, 0x9e, 0x12, 0xd2, 0x76, 0x5c, 0xf7}; +const uint8_t FROMRADIO_UUID_16[16u] = {0x02, 0x00, 0x12, 0xac, 0x42, 0x02, 0x78, 0xb8, + 0xed, 0x11, 0x93, 0x49, 0x9e, 0xe6, 0x55, 0x2c}; +const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70, 0xa6, + 0x66, 0x4f, 0x00, 0xa8, 0x8c, 0xa1, 0x9d, 0xed}; +const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa, + 0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c}; +const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99, + 0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A}; \ No newline at end of file diff --git a/src/BluetoothCommon.h b/src/BluetoothCommon.h index d47180872..440d13844 100644 --- a/src/BluetoothCommon.h +++ b/src/BluetoothCommon.h @@ -21,11 +21,12 @@ extern const uint8_t MESH_SERVICE_UUID_16[], TORADIO_UUID_16[16u], FROMRADIO_UUI /// Given a level between 0-100, update the BLE attribute void updateBatteryLevel(uint8_t level); -class BluetoothApi { -public: - virtual void setup(); - virtual void shutdown(); - virtual void clearBonds(); - virtual bool isConnected(); - virtual int getRssi() = 0; +class BluetoothApi +{ + public: + virtual void setup(); + virtual void shutdown(); + virtual void clearBonds(); + virtual bool isConnected(); + virtual int getRssi() = 0; }; \ No newline at end of file diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 851843093..680aec929 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -5,106 +5,113 @@ #include "meshUtils.h" #include -namespace meshtastic { +namespace meshtastic +{ // Describes the state of the Bluetooth connection // Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code -class BluetoothStatus : public Status { -public: - enum class ConnectionState { - DISCONNECTED, - PAIRING, - CONNECTED, - }; +class BluetoothStatus : public Status +{ + public: + enum class ConnectionState { + DISCONNECTED, + PAIRING, + CONNECTED, + }; -private: - CallbackObserver statusObserver = - CallbackObserver(this, &BluetoothStatus::updateStatus); + private: + CallbackObserver statusObserver = + CallbackObserver(this, &BluetoothStatus::updateStatus); - ConnectionState state = ConnectionState::DISCONNECTED; - std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero + ConnectionState state = ConnectionState::DISCONNECTED; + std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero -public: - BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; } + public: + BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; } - // New BluetoothStatus: connected or disconnected - explicit BluetoothStatus(ConnectionState state) { - assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey - statusType = STATUS_TYPE_BLUETOOTH; - this->state = state; - } - - // New BluetoothStatus: pairing, with passkey - explicit BluetoothStatus(const std::string &passkey) : Status() { - statusType = STATUS_TYPE_BLUETOOTH; - this->state = ConnectionState::PAIRING; - this->passkey = passkey; - } - - ConnectionState getConnectionState() const { return this->state; } - - std::string getPasskey() const { - assert(state == ConnectionState::PAIRING); - return this->passkey; - } - - void observe(Observable *source) { statusObserver.observe(source); } - - bool matches(const BluetoothStatus *newStatus) const { - if (this->state == newStatus->getConnectionState()) { - // Same state: CONNECTED / DISCONNECTED - if (this->state != ConnectionState::PAIRING) - return true; - // Same state: PAIRING, and passkey matches - else if (this->getPasskey() == newStatus->getPasskey()) - return true; + // New BluetoothStatus: connected or disconnected + explicit BluetoothStatus(ConnectionState state) + { + assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey + statusType = STATUS_TYPE_BLUETOOTH; + this->state = state; } - return false; - } - - int updateStatus(const BluetoothStatus *newStatus) { - // Has the status changed? - if (!matches(newStatus)) { - // Copy the members - state = newStatus->getConnectionState(); - if (state == ConnectionState::PAIRING) - passkey = newStatus->getPasskey(); - - // Tell anyone interested that we have an update - onNewStatus.notifyObservers(this); - - // Debug only: - switch (state) { - case ConnectionState::PAIRING: - LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str()); - break; - case ConnectionState::CONNECTED: - LOG_DEBUG("BluetoothStatus CONNECTED"); -#ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, LOW); -#else - digitalWrite(BLE_LED, HIGH); -#endif -#endif - break; - - case ConnectionState::DISCONNECTED: - LOG_DEBUG("BluetoothStatus DISCONNECTED"); -#ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif -#endif - break; - } + // New BluetoothStatus: pairing, with passkey + explicit BluetoothStatus(const std::string &passkey) : Status() + { + statusType = STATUS_TYPE_BLUETOOTH; + this->state = ConnectionState::PAIRING; + this->passkey = passkey; } - return 0; - } + ConnectionState getConnectionState() const { return this->state; } + + std::string getPasskey() const + { + assert(state == ConnectionState::PAIRING); + return this->passkey; + } + + void observe(Observable *source) { statusObserver.observe(source); } + + bool matches(const BluetoothStatus *newStatus) const + { + if (this->state == newStatus->getConnectionState()) { + // Same state: CONNECTED / DISCONNECTED + if (this->state != ConnectionState::PAIRING) + return true; + // Same state: PAIRING, and passkey matches + else if (this->getPasskey() == newStatus->getPasskey()) + return true; + } + + return false; + } + + int updateStatus(const BluetoothStatus *newStatus) + { + // Has the status changed? + if (!matches(newStatus)) { + // Copy the members + state = newStatus->getConnectionState(); + if (state == ConnectionState::PAIRING) + passkey = newStatus->getPasskey(); + + // Tell anyone interested that we have an update + onNewStatus.notifyObservers(this); + + // Debug only: + switch (state) { + case ConnectionState::PAIRING: + LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str()); + break; + case ConnectionState::CONNECTED: + LOG_DEBUG("BluetoothStatus CONNECTED"); +#ifdef BLE_LED +#ifdef BLE_LED_INVERTED + digitalWrite(BLE_LED, LOW); +#else + digitalWrite(BLE_LED, HIGH); +#endif +#endif + break; + + case ConnectionState::DISCONNECTED: + LOG_DEBUG("BluetoothStatus DISCONNECTED"); +#ifdef BLE_LED +#ifdef BLE_LED_INVERTED + digitalWrite(BLE_LED, HIGH); +#else + digitalWrite(BLE_LED, LOW); +#endif +#endif + break; + } + } + + return 0; + } }; } // namespace meshtastic diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index def8155d7..d65c4f1e8 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -31,150 +31,168 @@ SOFTWARE.*/ #endif /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic -extern "C" void logLegacy(const char *level, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - if (console) - console->vprintf(level, fmt, args); - va_end(args); +extern "C" void logLegacy(const char *level, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + if (console) + console->vprintf(level, fmt, args); + va_end(args); } #if HAS_NETWORKING -Syslog::Syslog(UDP &client) { - this->_client = &client; - this->_server = NULL; - this->_port = 0; - this->_deviceHostname = SYSLOG_NILVALUE; - this->_appName = SYSLOG_NILVALUE; - this->_priDefault = LOGLEVEL_KERN; -} - -Syslog &Syslog::server(const char *server, uint16_t port) { - if (this->_ip.fromString(server)) { +Syslog::Syslog(UDP &client) +{ + this->_client = &client; this->_server = NULL; - } else { - this->_server = server; - } - this->_port = port; - return *this; + this->_port = 0; + this->_deviceHostname = SYSLOG_NILVALUE; + this->_appName = SYSLOG_NILVALUE; + this->_priDefault = LOGLEVEL_KERN; } -Syslog &Syslog::server(IPAddress ip, uint16_t port) { - this->_ip = ip; - this->_server = NULL; - this->_port = port; - return *this; +Syslog &Syslog::server(const char *server, uint16_t port) +{ + if (this->_ip.fromString(server)) { + this->_server = NULL; + } else { + this->_server = server; + } + this->_port = port; + return *this; } -Syslog &Syslog::deviceHostname(const char *deviceHostname) { - this->_deviceHostname = (deviceHostname == NULL) ? SYSLOG_NILVALUE : deviceHostname; - return *this; +Syslog &Syslog::server(IPAddress ip, uint16_t port) +{ + this->_ip = ip; + this->_server = NULL; + this->_port = port; + return *this; } -Syslog &Syslog::appName(const char *appName) { - this->_appName = (appName == NULL) ? SYSLOG_NILVALUE : appName; - return *this; +Syslog &Syslog::deviceHostname(const char *deviceHostname) +{ + this->_deviceHostname = (deviceHostname == NULL) ? SYSLOG_NILVALUE : deviceHostname; + return *this; } -Syslog &Syslog::defaultPriority(uint16_t pri) { - this->_priDefault = pri; - return *this; +Syslog &Syslog::appName(const char *appName) +{ + this->_appName = (appName == NULL) ? SYSLOG_NILVALUE : appName; + return *this; } -Syslog &Syslog::logMask(uint8_t priMask) { - this->_priMask = priMask; - return *this; +Syslog &Syslog::defaultPriority(uint16_t pri) +{ + this->_priDefault = pri; + return *this; } -void Syslog::enable() { - this->_client->begin(this->_port); - this->_enabled = true; +Syslog &Syslog::logMask(uint8_t priMask) +{ + this->_priMask = priMask; + return *this; } -void Syslog::disable() { - this->_enabled = false; - this->_client->stop(); +void Syslog::enable() +{ + this->_client->begin(this->_port); + this->_enabled = true; } -bool Syslog::isEnabled() { return this->_enabled; } +void Syslog::disable() +{ + this->_enabled = false; + this->_client->stop(); +} -bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args) { return this->vlogf(pri, this->_appName, fmt, args); } +bool Syslog::isEnabled() +{ + return this->_enabled; +} -bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) { - char *message; - size_t initialLen; - size_t len; - bool result; +bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args) +{ + return this->vlogf(pri, this->_appName, fmt, args); +} - initialLen = strlen(fmt); +bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) +{ + char *message; + size_t initialLen; + size_t len; + bool result; - message = new char[initialLen + 1]; + initialLen = strlen(fmt); + + message = new char[initialLen + 1]; + + len = vsnprintf(message, initialLen + 1, fmt, args); + if (len > initialLen) { + delete[] message; + message = new char[len + 1]; + + vsnprintf(message, len + 1, fmt, args); + } + + result = this->_sendLog(pri, appName, message); - len = vsnprintf(message, initialLen + 1, fmt, args); - if (len > initialLen) { delete[] message; - message = new char[len + 1]; - - vsnprintf(message, len + 1, fmt, args); - } - - result = this->_sendLog(pri, appName, message); - - delete[] message; - return result; + return result; } -inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message) { - int result; +inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message) +{ + int result; #ifdef ARCH_PORTDUINO - bool utf = !portduino_config.ascii_logs; + bool utf = !portduino_config.ascii_logs; #else - bool utf = true; + bool utf = true; #endif - if (!this->_enabled) - return false; + if (!this->_enabled) + return false; - if ((this->_server == NULL && this->_ip == INADDR_NONE) || this->_port == 0) - return false; + if ((this->_server == NULL && this->_ip == INADDR_NONE) || this->_port == 0) + return false; + + // Check priority against priMask values. + if ((LOG_MASK(LOG_PRI(pri)) & this->_priMask) == 0) + return true; + + // Set default facility if none specified. + if ((pri & LOG_FACMASK) == 0) + pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); + + if (this->_server != NULL) { + result = this->_client->beginPacket(this->_server, this->_port); + } else { + result = this->_client->beginPacket(this->_ip, this->_port); + } + + if (result != 1) + return false; + + this->_client->print('<'); + this->_client->print(pri); + this->_client->print(F(">1 - ")); + this->_client->print(this->_deviceHostname); + this->_client->print(' '); + this->_client->print(appName); + this->_client->print(F(" - - - ")); + if (utf) { + this->_client->print(F("\xEF\xBB\xBF")); + } else { + this->_client->print(F(" ")); + } + this->_client->print(F("[")); + this->_client->print(int(millis() / 1000)); + this->_client->print(F("]: ")); + this->_client->print(message); + this->_client->endPacket(); - // Check priority against priMask values. - if ((LOG_MASK(LOG_PRI(pri)) & this->_priMask) == 0) return true; - - // Set default facility if none specified. - if ((pri & LOG_FACMASK) == 0) - pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); - - if (this->_server != NULL) { - result = this->_client->beginPacket(this->_server, this->_port); - } else { - result = this->_client->beginPacket(this->_ip, this->_port); - } - - if (result != 1) - return false; - - this->_client->print('<'); - this->_client->print(pri); - this->_client->print(F(">1 - ")); - this->_client->print(this->_deviceHostname); - this->_client->print(' '); - this->_client->print(appName); - this->_client->print(F(" - - - ")); - if (utf) { - this->_client->print(F("\xEF\xBB\xBF")); - } else { - this->_client->print(F(" ")); - } - this->_client->print(F("[")); - this->_client->print(int(millis() / 1000)); - this->_client->print(F("]: ")); - this->_client->print(message); - this->_client->endPacket(); - - return true; } #endif diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 44481ab1c..98bbe0f72 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -74,13 +74,13 @@ extern MemGet memGet; // Macro-based heap debugging #define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); -#define DEBUG_HEAP_AFTER(context, ptr) \ - do { \ - auto heapAfter = memGet.getFreeHeap(); \ - if (heapBefore != heapAfter) { \ - LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ - } \ - } while (0) +#define DEBUG_HEAP_AFTER(context, ptr) \ + do { \ + auto heapAfter = memGet.getFreeHeap(); \ + if (heapBefore != heapAfter) { \ + LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ + } \ + } while (0) #else #define LOG_HEAP(...) @@ -162,36 +162,37 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...); #if HAS_NETWORKING -class Syslog { -private: - UDP *_client; - IPAddress _ip; - const char *_server; - uint16_t _port; - const char *_deviceHostname; - const char *_appName; - uint16_t _priDefault; - uint8_t _priMask = 0xff; - bool _enabled = false; +class Syslog +{ + private: + UDP *_client; + IPAddress _ip; + const char *_server; + uint16_t _port; + const char *_deviceHostname; + const char *_appName; + uint16_t _priDefault; + uint8_t _priMask = 0xff; + bool _enabled = false; - bool _sendLog(uint16_t pri, const char *appName, const char *message); + bool _sendLog(uint16_t pri, const char *appName, const char *message); -public: - explicit Syslog(UDP &client); + public: + explicit Syslog(UDP &client); - Syslog &server(const char *server, uint16_t port); - Syslog &server(IPAddress ip, uint16_t port); - Syslog &deviceHostname(const char *deviceHostname); - Syslog &appName(const char *appName); - Syslog &defaultPriority(uint16_t pri = LOGLEVEL_KERN); - Syslog &logMask(uint8_t priMask); + Syslog &server(const char *server, uint16_t port); + Syslog &server(IPAddress ip, uint16_t port); + Syslog &deviceHostname(const char *deviceHostname); + Syslog &appName(const char *appName); + Syslog &defaultPriority(uint16_t pri = LOGLEVEL_KERN); + Syslog &logMask(uint8_t priMask); - void enable(); - void disable(); - bool isEnabled(); + void enable(); + void disable(); + bool isEnabled(); - bool vlogf(uint16_t pri, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); - bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); + bool vlogf(uint16_t pri, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); + bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); }; #endif // HAS_NETWORKING \ No newline at end of file diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index eea9bf33b..d88f9fc9f 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -1,83 +1,86 @@ #include "DisplayFormatters.h" -const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset) { +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset) +{ - // If use_preset is false, always return "Custom" - if (!usePreset) { - return "Custom"; - } + // If use_preset is false, always return "Custom" + if (!usePreset) { + return "Custom"; + } - switch (preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - return useShortName ? "ShortT" : "ShortTurbo"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - return useShortName ? "ShortS" : "ShortSlow"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - return useShortName ? "ShortF" : "ShortFast"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - return useShortName ? "MedS" : "MediumSlow"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - return useShortName ? "MedF" : "MediumFast"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - return useShortName ? "LongS" : "LongSlow"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: - return useShortName ? "LongF" : "LongFast"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: - return useShortName ? "LongT" : "LongTurbo"; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - return useShortName ? "LongM" : "LongMod"; - break; - default: - return useShortName ? "Custom" : "Invalid"; - break; - } + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return useShortName ? "ShortT" : "ShortTurbo"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + return useShortName ? "ShortS" : "ShortSlow"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + return useShortName ? "ShortF" : "ShortFast"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + return useShortName ? "MedS" : "MediumSlow"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return useShortName ? "MedF" : "MediumFast"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + return useShortName ? "LongS" : "LongSlow"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return useShortName ? "LongF" : "LongFast"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + return useShortName ? "LongT" : "LongTurbo"; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + return useShortName ? "LongM" : "LongMod"; + break; + default: + return useShortName ? "Custom" : "Invalid"; + break; + } } -const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) { - switch (role) { - case meshtastic_Config_DeviceConfig_Role_CLIENT: - return "Client"; - break; - case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: - return "Client Mute"; - break; - case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: - return "Client Hidden"; - break; - case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: - return "Client Base"; - break; - case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: - return "Lost and Found"; - break; - case meshtastic_Config_DeviceConfig_Role_TRACKER: - return "Tracker"; - break; - case meshtastic_Config_DeviceConfig_Role_SENSOR: - return "Sensor"; - break; - case meshtastic_Config_DeviceConfig_Role_TAK: - return "TAK"; - break; - case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: - return "TAK Tracker"; - break; - case meshtastic_Config_DeviceConfig_Role_ROUTER: - return "Router"; - break; - case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: - return "Router Late"; - break; - default: - return "Unknown"; - break; - } +const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) +{ + switch (role) { + case meshtastic_Config_DeviceConfig_Role_CLIENT: + return "Client"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: + return "Client Mute"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: + return "Client Hidden"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: + return "Client Base"; + break; + case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: + return "Lost and Found"; + break; + case meshtastic_Config_DeviceConfig_Role_TRACKER: + return "Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_SENSOR: + return "Sensor"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK: + return "TAK"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: + return "TAK Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER: + return "Router"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: + return "Router Late"; + break; + default: + return "Unknown"; + break; + } } \ No newline at end of file diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index 025225ec3..981010b33 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -1,8 +1,10 @@ #pragma once #include "NodeDB.h" -class DisplayFormatters { -public: - static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset); - static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); +class DisplayFormatters +{ + public: + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset); + static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); }; diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 512fb5a57..f215be80f 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -1,11 +1,11 @@ /** * @file FSCommon.cpp - * @brief This file contains functions for common filesystem operations such as copying, renaming, listing and deleting - * files and directories. + * @brief This file contains functions for common filesystem operations such as copying, renaming, listing and deleting files and + * directories. * - * The functions in this file are used to perform common filesystem operations such as copying, renaming, listing and - * deleting files and directories. These functions are used in the Meshtastic-device project to manage files and - * directories on the device's filesystem. + * The functions in this file are used to perform common filesystem operations such as copying, renaming, listing and deleting + * files and directories. These functions are used in the Meshtastic-device project to manage files and directories on the + * device's filesystem. * */ #include "FSCommon.h" @@ -37,33 +37,34 @@ SPIClass SPI_HSPI(HSPI); * @param to The path of the destination file. * @return true if the file was successfully copied, false otherwise. */ -bool copyFile(const char *from, const char *to) { +bool copyFile(const char *from, const char *to) +{ #ifdef FSCom - // take SPI Lock - concurrency::LockGuard g(spiLock); - unsigned char cbuffer[16]; + // take SPI Lock + concurrency::LockGuard g(spiLock); + unsigned char cbuffer[16]; - File f1 = FSCom.open(from, FILE_O_READ); - if (!f1) { - LOG_ERROR("Failed to open source file %s", from); - return false; - } + File f1 = FSCom.open(from, FILE_O_READ); + if (!f1) { + LOG_ERROR("Failed to open source file %s", from); + return false; + } - File f2 = FSCom.open(to, FILE_O_WRITE); - if (!f2) { - LOG_ERROR("Failed to open destination file %s", to); - return false; - } + File f2 = FSCom.open(to, FILE_O_WRITE); + if (!f2) { + LOG_ERROR("Failed to open destination file %s", to); + return false; + } - while (f1.available() > 0) { - byte i = f1.read(cbuffer, 16); - f2.write(cbuffer, i); - } + while (f1.available() > 0) { + byte i = f1.read(cbuffer, 16); + f2.write(cbuffer, i); + } - f2.flush(); - f2.close(); - f1.close(); - return true; + f2.flush(); + f2.close(); + f1.close(); + return true; #endif } @@ -75,23 +76,24 @@ bool copyFile(const char *from, const char *to) { * * @return True if the file was successfully renamed, false otherwise. */ -bool renameFile(const char *pathFrom, const char *pathTo) { +bool renameFile(const char *pathFrom, const char *pathTo) +{ #ifdef FSCom #ifdef ARCH_ESP32 - // take SPI Lock - spiLock->lock(); - // rename was fixed for ESP32 IDF LittleFS in April - bool result = FSCom.rename(pathFrom, pathTo); - spiLock->unlock(); - return result; + // take SPI Lock + spiLock->lock(); + // rename was fixed for ESP32 IDF LittleFS in April + bool result = FSCom.rename(pathFrom, pathTo); + spiLock->unlock(); + return result; #else - // copyFile does its own locking. - if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { - return true; - } else { - return false; - } + // copyFile does its own locking. + if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { + return true; + } else { + return false; + } #endif #endif @@ -109,44 +111,45 @@ bool renameFile(const char *pathFrom, const char *pathTo) { * @param levels The number of levels of subdirectories to list. * @return A vector of strings containing the full path of each file in the directory. */ -std::vector getFiles(const char *dirname, uint8_t levels) { - std::vector filenames = {}; +std::vector getFiles(const char *dirname, uint8_t levels) +{ + std::vector filenames = {}; #ifdef FSCom - File root = FSCom.open(dirname, FILE_O_READ); - if (!root) - return filenames; - if (!root.isDirectory()) - return filenames; + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) + return filenames; + if (!root.isDirectory()) + return filenames; - File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { #ifdef ARCH_ESP32 - std::vector subDirFilenames = getFiles(file.path(), levels - 1); + std::vector subDirFilenames = getFiles(file.path(), levels - 1); #else - std::vector subDirFilenames = getFiles(file.name(), levels - 1); + std::vector subDirFilenames = getFiles(file.name(), levels - 1); #endif - filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end()); - file.close(); - } - } else { - meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; + filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end()); + file.close(); + } + } else { + meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; #ifdef ARCH_ESP32 - strcpy(fileInfo.file_name, file.path()); + strcpy(fileInfo.file_name, file.path()); #else - strcpy(fileInfo.file_name, file.name()); + strcpy(fileInfo.file_name, file.name()); #endif - if (!String(fileInfo.file_name).endsWith(".")) { - filenames.push_back(fileInfo); - } - file.close(); + if (!String(fileInfo.file_name).endsWith(".")) { + filenames.push_back(fileInfo); + } + file.close(); + } + file = root.openNextFile(); } - file = root.openNextFile(); - } - root.close(); + root.close(); #endif - return filenames; + return filenames; } /** @@ -157,98 +160,100 @@ std::vector getFiles(const char *dirname, uint8_t levels) { * @param levels The number of levels of subdirectories to list. * @param del Whether or not to delete the contents of the directory after listing. */ -void listDir(const char *dirname, uint8_t levels, bool del) { +void listDir(const char *dirname, uint8_t levels, bool del) +{ #ifdef FSCom #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - char buffer[255]; -#endif - File root = FSCom.open(dirname, FILE_O_READ); - if (!root) { - return; - } - if (!root.isDirectory()) { - return; - } - - File file = root.openNextFile(); - while (file && file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 - // glue (see issue 4395) - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { -#ifdef ARCH_ESP32 - listDir(file.path(), levels - 1, del); - if (del) { - LOG_DEBUG("Remove %s", file.path()); - strncpy(buffer, file.path(), sizeof(buffer)); - file.close(); - FSCom.rmdir(buffer); - } else { - file.close(); - } -#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - listDir(file.name(), levels - 1, del); - if (del) { - LOG_DEBUG("Remove %s", file.name()); - strncpy(buffer, file.name(), sizeof(buffer)); - file.close(); - FSCom.rmdir(buffer); - } else { - file.close(); - } -#else - LOG_DEBUG(" %s (directory)", file.name()); - listDir(file.name(), levels - 1, del); - file.close(); -#endif - } - } else { -#ifdef ARCH_ESP32 - if (del) { - LOG_DEBUG("Delete %s", file.path()); - strncpy(buffer, file.path(), sizeof(buffer)); - file.close(); - FSCom.remove(buffer); - } else { - LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); - file.close(); - } -#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - if (del) { - LOG_DEBUG("Delete %s", file.name()); - strncpy(buffer, file.name(), sizeof(buffer)); - file.close(); - FSCom.remove(buffer); - } else { - LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); - file.close(); - } -#else - LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); - file.close(); + char buffer[255]; #endif + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) { + return; } - file = root.openNextFile(); - } + if (!root.isDirectory()) { + return; + } + + File file = root.openNextFile(); + while ( + file && + file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395) + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { #ifdef ARCH_ESP32 - if (del) { - LOG_DEBUG("Remove %s", root.path()); - strncpy(buffer, root.path(), sizeof(buffer)); - root.close(); - FSCom.rmdir(buffer); - } else { - root.close(); - } + listDir(file.path(), levels - 1, del); + if (del) { + LOG_DEBUG("Remove %s", file.path()); + strncpy(buffer, file.path(), sizeof(buffer)); + file.close(); + FSCom.rmdir(buffer); + } else { + file.close(); + } #elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - if (del) { - LOG_DEBUG("Remove %s", root.name()); - strncpy(buffer, root.name(), sizeof(buffer)); - root.close(); - FSCom.rmdir(buffer); - } else { - root.close(); - } + listDir(file.name(), levels - 1, del); + if (del) { + LOG_DEBUG("Remove %s", file.name()); + strncpy(buffer, file.name(), sizeof(buffer)); + file.close(); + FSCom.rmdir(buffer); + } else { + file.close(); + } #else - root.close(); + LOG_DEBUG(" %s (directory)", file.name()); + listDir(file.name(), levels - 1, del); + file.close(); +#endif + } + } else { +#ifdef ARCH_ESP32 + if (del) { + LOG_DEBUG("Delete %s", file.path()); + strncpy(buffer, file.path(), sizeof(buffer)); + file.close(); + FSCom.remove(buffer); + } else { + LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); + file.close(); + } +#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + if (del) { + LOG_DEBUG("Delete %s", file.name()); + strncpy(buffer, file.name(), sizeof(buffer)); + file.close(); + FSCom.remove(buffer); + } else { + LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); + file.close(); + } +#else + LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); + file.close(); +#endif + } + file = root.openNextFile(); + } +#ifdef ARCH_ESP32 + if (del) { + LOG_DEBUG("Remove %s", root.path()); + strncpy(buffer, root.path(), sizeof(buffer)); + root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + if (del) { + LOG_DEBUG("Remove %s", root.name()); + strncpy(buffer, root.name(), sizeof(buffer)); + root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#else + root.close(); #endif #endif } @@ -260,14 +265,15 @@ void listDir(const char *dirname, uint8_t levels, bool del) { * * @param dirname The name of the directory to remove. */ -void rmDir(const char *dirname) { +void rmDir(const char *dirname) +{ #ifdef FSCom #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - listDir(dirname, 10, true); + listDir(dirname, 10, true); #elif defined(ARCH_NRF52) - // nRF52 implementation of LittleFS has a recursive delete function - FSCom.rmdir_r(dirname); + // nRF52 implementation of LittleFS has a recursive delete function + FSCom.rmdir_r(dirname); #endif #endif @@ -278,53 +284,55 @@ void rmDir(const char *dirname) { */ __attribute__((weak, noinline)) void preFSBegin() {} -void fsInit() { +void fsInit() +{ #ifdef FSCom - concurrency::LockGuard g(spiLock); - preFSBegin(); - if (!FSBegin()) { - LOG_ERROR("Filesystem mount failed"); - // assert(0); This auto-formats the partition, so no need to fail here. - } + concurrency::LockGuard g(spiLock); + preFSBegin(); + if (!FSBegin()) { + LOG_ERROR("Filesystem mount failed"); + // assert(0); This auto-formats the partition, so no need to fail here. + } #if defined(ARCH_ESP32) - LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes()); + LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes()); #else - LOG_DEBUG("Filesystem files:"); + LOG_DEBUG("Filesystem files:"); #endif - listDir("/", 10); + listDir("/", 10); #endif } /** * Initializes the SD card and mounts the file system. */ -void setupSDCard() { +void setupSDCard() +{ #if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - concurrency::LockGuard g(spiLock); - SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); - if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { - LOG_DEBUG("No SD_MMC card detected"); - return; - } - uint8_t cardType = SD.cardType(); - if (cardType == CARD_NONE) { - LOG_DEBUG("No SD_MMC card attached"); - return; - } - LOG_DEBUG("SD_MMC Card Type: "); - if (cardType == CARD_MMC) { - LOG_DEBUG("MMC"); - } else if (cardType == CARD_SD) { - LOG_DEBUG("SDSC"); - } else if (cardType == CARD_SDHC) { - LOG_DEBUG("SDHC"); - } else { - LOG_DEBUG("UNKNOWN"); - } + concurrency::LockGuard g(spiLock); + SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { + LOG_DEBUG("No SD_MMC card detected"); + return; + } + uint8_t cardType = SD.cardType(); + if (cardType == CARD_NONE) { + LOG_DEBUG("No SD_MMC card attached"); + return; + } + LOG_DEBUG("SD_MMC Card Type: "); + if (cardType == CARD_MMC) { + LOG_DEBUG("MMC"); + } else if (cardType == CARD_SD) { + LOG_DEBUG("SDSC"); + } else if (cardType == CARD_SDHC) { + LOG_DEBUG("SDHC"); + } else { + LOG_DEBUG("UNKNOWN"); + } - uint64_t cardSize = SD.cardSize() / (1024 * 1024); - LOG_DEBUG("SD Card Size: %lu MB", (uint32_t)cardSize); - LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); - LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); + uint64_t cardSize = SD.cardSize() / (1024 * 1024); + LOG_DEBUG("SD Card Size: %lu MB", (uint32_t)cardSize); + LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); + LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); #endif } \ No newline at end of file diff --git a/src/Fusion/FusionAhrs.c b/src/Fusion/FusionAhrs.c index c5ac6016a..d6c1d0215 100644 --- a/src/Fusion/FusionAhrs.c +++ b/src/Fusion/FusionAhrs.c @@ -43,17 +43,18 @@ static inline int Clamp(const int value, const int min, const int max); * @brief Initialises the AHRS algorithm structure. * @param ahrs AHRS algorithm structure. */ -void FusionAhrsInitialise(FusionAhrs *const ahrs) { - const FusionAhrsSettings settings = { - .convention = FusionConventionNwu, - .gain = 0.5f, - .gyroscopeRange = 0.0f, - .accelerationRejection = 90.0f, - .magneticRejection = 90.0f, - .recoveryTriggerPeriod = 0, - }; - FusionAhrsSetSettings(ahrs, &settings); - FusionAhrsReset(ahrs); +void FusionAhrsInitialise(FusionAhrs *const ahrs) +{ + const FusionAhrsSettings settings = { + .convention = FusionConventionNwu, + .gain = 0.5f, + .gyroscopeRange = 0.0f, + .accelerationRejection = 90.0f, + .magneticRejection = 90.0f, + .recoveryTriggerPeriod = 0, + }; + FusionAhrsSetSettings(ahrs, &settings); + FusionAhrsReset(ahrs); } /** @@ -61,20 +62,21 @@ void FusionAhrsInitialise(FusionAhrs *const ahrs) { * algorithm while maintaining the current settings. * @param ahrs AHRS algorithm structure. */ -void FusionAhrsReset(FusionAhrs *const ahrs) { - ahrs->quaternion = FUSION_IDENTITY_QUATERNION; - ahrs->accelerometer = FUSION_VECTOR_ZERO; - ahrs->initialising = true; - ahrs->rampedGain = INITIAL_GAIN; - ahrs->angularRateRecovery = false; - ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO; - ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO; - ahrs->accelerometerIgnored = false; - ahrs->accelerationRecoveryTrigger = 0; - ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - ahrs->magnetometerIgnored = false; - ahrs->magneticRecoveryTrigger = 0; - ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; +void FusionAhrsReset(FusionAhrs *const ahrs) +{ + ahrs->quaternion = FUSION_IDENTITY_QUATERNION; + ahrs->accelerometer = FUSION_VECTOR_ZERO; + ahrs->initialising = true; + ahrs->rampedGain = INITIAL_GAIN; + ahrs->angularRateRecovery = false; + ahrs->halfAccelerometerFeedback = FUSION_VECTOR_ZERO; + ahrs->halfMagnetometerFeedback = FUSION_VECTOR_ZERO; + ahrs->accelerometerIgnored = false; + ahrs->accelerationRecoveryTrigger = 0; + ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + ahrs->magnetometerIgnored = false; + ahrs->magneticRecoveryTrigger = 0; + ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; } /** @@ -82,25 +84,28 @@ void FusionAhrsReset(FusionAhrs *const ahrs) { * @param ahrs AHRS algorithm structure. * @param settings Settings. */ -void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings) { - ahrs->settings.convention = settings->convention; - ahrs->settings.gain = settings->gain; - ahrs->settings.gyroscopeRange = settings->gyroscopeRange == 0.0f ? FLT_MAX : 0.98f * settings->gyroscopeRange; - ahrs->settings.accelerationRejection = - settings->accelerationRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2); - ahrs->settings.magneticRejection = - settings->magneticRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2); - ahrs->settings.recoveryTriggerPeriod = settings->recoveryTriggerPeriod; - ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - if ((settings->gain == 0.0f) || (settings->recoveryTriggerPeriod == 0)) { // disable acceleration and magnetic rejection features if gain is zero - ahrs->settings.accelerationRejection = FLT_MAX; - ahrs->settings.magneticRejection = FLT_MAX; - } - if (ahrs->initialising == false) { - ahrs->rampedGain = ahrs->settings.gain; - } - ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD; +void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings) +{ + ahrs->settings.convention = settings->convention; + ahrs->settings.gain = settings->gain; + ahrs->settings.gyroscopeRange = settings->gyroscopeRange == 0.0f ? FLT_MAX : 0.98f * settings->gyroscopeRange; + ahrs->settings.accelerationRejection = settings->accelerationRejection == 0.0f + ? FLT_MAX + : powf(0.5f * sinf(FusionDegreesToRadians(settings->accelerationRejection)), 2); + ahrs->settings.magneticRejection = + settings->magneticRejection == 0.0f ? FLT_MAX : powf(0.5f * sinf(FusionDegreesToRadians(settings->magneticRejection)), 2); + ahrs->settings.recoveryTriggerPeriod = settings->recoveryTriggerPeriod; + ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + if ((settings->gain == 0.0f) || + (settings->recoveryTriggerPeriod == 0)) { // disable acceleration and magnetic rejection features if gain is zero + ahrs->settings.accelerationRejection = FLT_MAX; + ahrs->settings.magneticRejection = FLT_MAX; + } + if (ahrs->initialising == false) { + ahrs->rampedGain = ahrs->settings.gain; + } + ahrs->rampedGainStep = (INITIAL_GAIN - ahrs->settings.gain) / INITIALISATION_PERIOD; } /** @@ -112,113 +117,119 @@ void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *con * @param magnetometer Magnetometer measurement in arbitrary units. * @param deltaTime Delta time in seconds. */ -void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, - const float deltaTime) { +void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, + const FusionVector magnetometer, const float deltaTime) +{ #define Q ahrs->quaternion.element - // Store accelerometer - ahrs->accelerometer = accelerometer; + // Store accelerometer + ahrs->accelerometer = accelerometer; - // Reinitialise if gyroscope range exceeded - if ((fabsf(gyroscope.axis.x) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.y) > ahrs->settings.gyroscopeRange) || - (fabsf(gyroscope.axis.z) > ahrs->settings.gyroscopeRange)) { - const FusionQuaternion quaternion = ahrs->quaternion; - FusionAhrsReset(ahrs); - ahrs->quaternion = quaternion; - ahrs->angularRateRecovery = true; - } - - // Ramp down gain during initialisation - if (ahrs->initialising) { - ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime; - if ((ahrs->rampedGain < ahrs->settings.gain) || (ahrs->settings.gain == 0.0f)) { - ahrs->rampedGain = ahrs->settings.gain; - ahrs->initialising = false; - ahrs->angularRateRecovery = false; - } - } - - // Calculate direction of gravity indicated by algorithm - const FusionVector halfGravity = HalfGravity(ahrs); - - // Calculate accelerometer feedback - FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO; - ahrs->accelerometerIgnored = true; - if (FusionVectorIsZero(accelerometer) == false) { - - // Calculate accelerometer feedback scaled by 0.5 - ahrs->halfAccelerometerFeedback = Feedback(FusionVectorNormalise(accelerometer), halfGravity); - - // Don't ignore accelerometer if acceleration error below threshold - if (ahrs->initialising || ((FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection))) { - ahrs->accelerometerIgnored = false; - ahrs->accelerationRecoveryTrigger -= 9; - } else { - ahrs->accelerationRecoveryTrigger += 1; + // Reinitialise if gyroscope range exceeded + if ((fabsf(gyroscope.axis.x) > ahrs->settings.gyroscopeRange) || (fabsf(gyroscope.axis.y) > ahrs->settings.gyroscopeRange) || + (fabsf(gyroscope.axis.z) > ahrs->settings.gyroscopeRange)) { + const FusionQuaternion quaternion = ahrs->quaternion; + FusionAhrsReset(ahrs); + ahrs->quaternion = quaternion; + ahrs->angularRateRecovery = true; } - // Don't ignore accelerometer during acceleration recovery - if (ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout) { - ahrs->accelerationRecoveryTimeout = 0; - ahrs->accelerometerIgnored = false; - } else { - ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; - } - ahrs->accelerationRecoveryTrigger = Clamp(ahrs->accelerationRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); - - // Apply accelerometer feedback - if (ahrs->accelerometerIgnored == false) { - halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback; - } - } - - // Calculate magnetometer feedback - FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO; - ahrs->magnetometerIgnored = true; - if (FusionVectorIsZero(magnetometer) == false) { - - // Calculate direction of magnetic field indicated by algorithm - const FusionVector halfMagnetic = HalfMagnetic(ahrs); - - // Calculate magnetometer feedback scaled by 0.5 - ahrs->halfMagnetometerFeedback = Feedback(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfMagnetic); - - // Don't ignore magnetometer if magnetic error below threshold - if (ahrs->initialising || ((FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection))) { - ahrs->magnetometerIgnored = false; - ahrs->magneticRecoveryTrigger -= 9; - } else { - ahrs->magneticRecoveryTrigger += 1; + // Ramp down gain during initialisation + if (ahrs->initialising) { + ahrs->rampedGain -= ahrs->rampedGainStep * deltaTime; + if ((ahrs->rampedGain < ahrs->settings.gain) || (ahrs->settings.gain == 0.0f)) { + ahrs->rampedGain = ahrs->settings.gain; + ahrs->initialising = false; + ahrs->angularRateRecovery = false; + } } - // Don't ignore magnetometer during magnetic recovery - if (ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout) { - ahrs->magneticRecoveryTimeout = 0; - ahrs->magnetometerIgnored = false; - } else { - ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + // Calculate direction of gravity indicated by algorithm + const FusionVector halfGravity = HalfGravity(ahrs); + + // Calculate accelerometer feedback + FusionVector halfAccelerometerFeedback = FUSION_VECTOR_ZERO; + ahrs->accelerometerIgnored = true; + if (FusionVectorIsZero(accelerometer) == false) { + + // Calculate accelerometer feedback scaled by 0.5 + ahrs->halfAccelerometerFeedback = Feedback(FusionVectorNormalise(accelerometer), halfGravity); + + // Don't ignore accelerometer if acceleration error below threshold + if (ahrs->initialising || + ((FusionVectorMagnitudeSquared(ahrs->halfAccelerometerFeedback) <= ahrs->settings.accelerationRejection))) { + ahrs->accelerometerIgnored = false; + ahrs->accelerationRecoveryTrigger -= 9; + } else { + ahrs->accelerationRecoveryTrigger += 1; + } + + // Don't ignore accelerometer during acceleration recovery + if (ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout) { + ahrs->accelerationRecoveryTimeout = 0; + ahrs->accelerometerIgnored = false; + } else { + ahrs->accelerationRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + } + ahrs->accelerationRecoveryTrigger = Clamp(ahrs->accelerationRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); + + // Apply accelerometer feedback + if (ahrs->accelerometerIgnored == false) { + halfAccelerometerFeedback = ahrs->halfAccelerometerFeedback; + } } - ahrs->magneticRecoveryTrigger = Clamp(ahrs->magneticRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); - // Apply magnetometer feedback - if (ahrs->magnetometerIgnored == false) { - halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback; + // Calculate magnetometer feedback + FusionVector halfMagnetometerFeedback = FUSION_VECTOR_ZERO; + ahrs->magnetometerIgnored = true; + if (FusionVectorIsZero(magnetometer) == false) { + + // Calculate direction of magnetic field indicated by algorithm + const FusionVector halfMagnetic = HalfMagnetic(ahrs); + + // Calculate magnetometer feedback scaled by 0.5 + ahrs->halfMagnetometerFeedback = + Feedback(FusionVectorNormalise(FusionVectorCrossProduct(halfGravity, magnetometer)), halfMagnetic); + + // Don't ignore magnetometer if magnetic error below threshold + if (ahrs->initialising || + ((FusionVectorMagnitudeSquared(ahrs->halfMagnetometerFeedback) <= ahrs->settings.magneticRejection))) { + ahrs->magnetometerIgnored = false; + ahrs->magneticRecoveryTrigger -= 9; + } else { + ahrs->magneticRecoveryTrigger += 1; + } + + // Don't ignore magnetometer during magnetic recovery + if (ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout) { + ahrs->magneticRecoveryTimeout = 0; + ahrs->magnetometerIgnored = false; + } else { + ahrs->magneticRecoveryTimeout = ahrs->settings.recoveryTriggerPeriod; + } + ahrs->magneticRecoveryTrigger = Clamp(ahrs->magneticRecoveryTrigger, 0, ahrs->settings.recoveryTriggerPeriod); + + // Apply magnetometer feedback + if (ahrs->magnetometerIgnored == false) { + halfMagnetometerFeedback = ahrs->halfMagnetometerFeedback; + } } - } - // Convert gyroscope to radians per second scaled by 0.5 - const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f)); + // Convert gyroscope to radians per second scaled by 0.5 + const FusionVector halfGyroscope = FusionVectorMultiplyScalar(gyroscope, FusionDegreesToRadians(0.5f)); - // Apply feedback to gyroscope - const FusionVector adjustedHalfGyroscope = FusionVectorAdd( - halfGyroscope, FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain)); + // Apply feedback to gyroscope + const FusionVector adjustedHalfGyroscope = FusionVectorAdd( + halfGyroscope, + FusionVectorMultiplyScalar(FusionVectorAdd(halfAccelerometerFeedback, halfMagnetometerFeedback), ahrs->rampedGain)); - // Integrate rate of change of quaternion - ahrs->quaternion = FusionQuaternionAdd( - ahrs->quaternion, FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime))); + // Integrate rate of change of quaternion + ahrs->quaternion = FusionQuaternionAdd( + ahrs->quaternion, + FusionQuaternionMultiplyVector(ahrs->quaternion, FusionVectorMultiplyScalar(adjustedHalfGyroscope, deltaTime))); - // Normalise quaternion - ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion); + // Normalise quaternion + ahrs->quaternion = FusionQuaternionNormalise(ahrs->quaternion); #undef Q } @@ -227,28 +238,29 @@ void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, cons * @param ahrs AHRS algorithm structure. * @return Direction of gravity scaled by 0.5. */ -static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) { +static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) +{ #define Q ahrs->quaternion.element - switch (ahrs->settings.convention) { - case FusionConventionNwu: - case FusionConventionEnu: { - const FusionVector halfGravity = {.axis = { - .x = Q.x * Q.z - Q.w * Q.y, - .y = Q.y * Q.z + Q.w * Q.x, - .z = Q.w * Q.w - 0.5f + Q.z * Q.z, - }}; // third column of transposed rotation matrix scaled by 0.5 - return halfGravity; - } - case FusionConventionNed: { - const FusionVector halfGravity = {.axis = { - .x = Q.w * Q.y - Q.x * Q.z, - .y = -1.0f * (Q.y * Q.z + Q.w * Q.x), - .z = 0.5f - Q.w * Q.w - Q.z * Q.z, - }}; // third column of transposed rotation matrix scaled by -0.5 - return halfGravity; - } - } - return FUSION_VECTOR_ZERO; // avoid compiler warning + switch (ahrs->settings.convention) { + case FusionConventionNwu: + case FusionConventionEnu: { + const FusionVector halfGravity = {.axis = { + .x = Q.x * Q.z - Q.w * Q.y, + .y = Q.y * Q.z + Q.w * Q.x, + .z = Q.w * Q.w - 0.5f + Q.z * Q.z, + }}; // third column of transposed rotation matrix scaled by 0.5 + return halfGravity; + } + case FusionConventionNed: { + const FusionVector halfGravity = {.axis = { + .x = Q.w * Q.y - Q.x * Q.z, + .y = -1.0f * (Q.y * Q.z + Q.w * Q.x), + .z = 0.5f - Q.w * Q.w - Q.z * Q.z, + }}; // third column of transposed rotation matrix scaled by -0.5 + return halfGravity; + } + } + return FUSION_VECTOR_ZERO; // avoid compiler warning #undef Q } @@ -257,35 +269,36 @@ static inline FusionVector HalfGravity(const FusionAhrs *const ahrs) { * @param ahrs AHRS algorithm structure. * @return Direction of the magnetic field scaled by 0.5. */ -static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) { +static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) +{ #define Q ahrs->quaternion.element - switch (ahrs->settings.convention) { - case FusionConventionNwu: { - const FusionVector halfMagnetic = {.axis = { - .x = Q.x * Q.y + Q.w * Q.z, - .y = Q.w * Q.w - 0.5f + Q.y * Q.y, - .z = Q.y * Q.z - Q.w * Q.x, - }}; // second column of transposed rotation matrix scaled by 0.5 - return halfMagnetic; - } - case FusionConventionEnu: { - const FusionVector halfMagnetic = {.axis = { - .x = 0.5f - Q.w * Q.w - Q.x * Q.x, - .y = Q.w * Q.z - Q.x * Q.y, - .z = -1.0f * (Q.x * Q.z + Q.w * Q.y), - }}; // first column of transposed rotation matrix scaled by -0.5 - return halfMagnetic; - } - case FusionConventionNed: { - const FusionVector halfMagnetic = {.axis = { - .x = -1.0f * (Q.x * Q.y + Q.w * Q.z), - .y = 0.5f - Q.w * Q.w - Q.y * Q.y, - .z = Q.w * Q.x - Q.y * Q.z, - }}; // second column of transposed rotation matrix scaled by -0.5 - return halfMagnetic; - } - } - return FUSION_VECTOR_ZERO; // avoid compiler warning + switch (ahrs->settings.convention) { + case FusionConventionNwu: { + const FusionVector halfMagnetic = {.axis = { + .x = Q.x * Q.y + Q.w * Q.z, + .y = Q.w * Q.w - 0.5f + Q.y * Q.y, + .z = Q.y * Q.z - Q.w * Q.x, + }}; // second column of transposed rotation matrix scaled by 0.5 + return halfMagnetic; + } + case FusionConventionEnu: { + const FusionVector halfMagnetic = {.axis = { + .x = 0.5f - Q.w * Q.w - Q.x * Q.x, + .y = Q.w * Q.z - Q.x * Q.y, + .z = -1.0f * (Q.x * Q.z + Q.w * Q.y), + }}; // first column of transposed rotation matrix scaled by -0.5 + return halfMagnetic; + } + case FusionConventionNed: { + const FusionVector halfMagnetic = {.axis = { + .x = -1.0f * (Q.x * Q.y + Q.w * Q.z), + .y = 0.5f - Q.w * Q.w - Q.y * Q.y, + .z = Q.w * Q.x - Q.y * Q.z, + }}; // second column of transposed rotation matrix scaled by -0.5 + return halfMagnetic; + } + } + return FUSION_VECTOR_ZERO; // avoid compiler warning #undef Q } @@ -295,11 +308,12 @@ static inline FusionVector HalfMagnetic(const FusionAhrs *const ahrs) { * @param reference Reference. * @return Feedback. */ -static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference) { - if (FusionVectorDotProduct(sensor, reference) < 0.0f) { // if error is >90 degrees - return FusionVectorNormalise(FusionVectorCrossProduct(sensor, reference)); - } - return FusionVectorCrossProduct(sensor, reference); +static inline FusionVector Feedback(const FusionVector sensor, const FusionVector reference) +{ + if (FusionVectorDotProduct(sensor, reference) < 0.0f) { // if error is >90 degrees + return FusionVectorNormalise(FusionVectorCrossProduct(sensor, reference)); + } + return FusionVectorCrossProduct(sensor, reference); } /** @@ -309,14 +323,15 @@ static inline FusionVector Feedback(const FusionVector sensor, const FusionVecto * @param max Maximum value. * @return Value limited to maximum and minimum. */ -static inline int Clamp(const int value, const int min, const int max) { - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; +static inline int Clamp(const int value, const int min, const int max) +{ + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; } /** @@ -327,15 +342,17 @@ static inline int Clamp(const int value, const int min, const int max) { * @param accelerometer Accelerometer measurement in g. * @param deltaTime Delta time in seconds. */ -void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime) { +void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, + const float deltaTime) +{ - // Update AHRS algorithm - FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime); + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, FUSION_VECTOR_ZERO, deltaTime); - // Zero heading during initialisation - if (ahrs->initialising) { - FusionAhrsSetHeading(ahrs, 0.0f); - } + // Zero heading during initialisation + if (ahrs->initialising) { + FusionAhrsSetHeading(ahrs, 0.0f); + } } /** @@ -347,24 +364,25 @@ void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector g * @param heading Heading measurement in degrees. * @param deltaTime Delta time in seconds. */ -void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, - const float deltaTime) { +void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, + const float heading, const float deltaTime) +{ #define Q ahrs->quaternion.element - // Calculate roll - const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x); + // Calculate roll + const float roll = atan2f(Q.w * Q.x + Q.y * Q.z, 0.5f - Q.y * Q.y - Q.x * Q.x); - // Calculate magnetometer - const float headingRadians = FusionDegreesToRadians(heading); - const float sinHeadingRadians = sinf(headingRadians); - const FusionVector magnetometer = {.axis = { - .x = cosf(headingRadians), - .y = -1.0f * cosf(roll) * sinHeadingRadians, - .z = sinHeadingRadians * sinf(roll), - }}; + // Calculate magnetometer + const float headingRadians = FusionDegreesToRadians(heading); + const float sinHeadingRadians = sinf(headingRadians); + const FusionVector magnetometer = {.axis = { + .x = cosf(headingRadians), + .y = -1.0f * cosf(roll) * sinHeadingRadians, + .z = sinHeadingRadians * sinf(roll), + }}; - // Update AHRS algorithm - FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime); + // Update AHRS algorithm + FusionAhrsUpdate(ahrs, gyroscope, accelerometer, magnetometer, deltaTime); #undef Q } @@ -373,14 +391,20 @@ void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector * @param ahrs AHRS algorithm structure. * @return Quaternion describing the sensor relative to the Earth. */ -FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) { return ahrs->quaternion; } +FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs) +{ + return ahrs->quaternion; +} /** * @brief Sets the quaternion describing the sensor relative to the Earth. * @param ahrs AHRS algorithm structure. * @param quaternion Quaternion describing the sensor relative to the Earth. */ -void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion) { ahrs->quaternion = quaternion; } +void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quaternion) +{ + ahrs->quaternion = quaternion; +} /** * @brief Returns the linear acceleration measurement equal to the accelerometer @@ -388,27 +412,28 @@ void FusionAhrsSetQuaternion(FusionAhrs *const ahrs, const FusionQuaternion quat * @param ahrs AHRS algorithm structure. * @return Linear acceleration measurement in g. */ -FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) { +FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) +{ #define Q ahrs->quaternion.element - // Calculate gravity in the sensor coordinate frame - const FusionVector gravity = {.axis = { - .x = 2.0f * (Q.x * Q.z - Q.w * Q.y), - .y = 2.0f * (Q.y * Q.z + Q.w * Q.x), - .z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), - }}; // third column of transposed rotation matrix + // Calculate gravity in the sensor coordinate frame + const FusionVector gravity = {.axis = { + .x = 2.0f * (Q.x * Q.z - Q.w * Q.y), + .y = 2.0f * (Q.y * Q.z + Q.w * Q.x), + .z = 2.0f * (Q.w * Q.w - 0.5f + Q.z * Q.z), + }}; // third column of transposed rotation matrix - // Remove gravity from accelerometer measurement - switch (ahrs->settings.convention) { - case FusionConventionNwu: - case FusionConventionEnu: { - return FusionVectorSubtract(ahrs->accelerometer, gravity); - } - case FusionConventionNed: { - return FusionVectorAdd(ahrs->accelerometer, gravity); - } - } - return FUSION_VECTOR_ZERO; // avoid compiler warning + // Remove gravity from accelerometer measurement + switch (ahrs->settings.convention) { + case FusionConventionNwu: + case FusionConventionEnu: { + return FusionVectorSubtract(ahrs->accelerometer, gravity); + } + case FusionConventionNed: { + return FusionVectorAdd(ahrs->accelerometer, gravity); + } + } + return FUSION_VECTOR_ZERO; // avoid compiler warning #undef Q } @@ -418,35 +443,36 @@ FusionVector FusionAhrsGetLinearAcceleration(const FusionAhrs *const ahrs) { * @param ahrs AHRS algorithm structure. * @return Earth acceleration measurement in g. */ -FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) { +FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) +{ #define Q ahrs->quaternion.element #define A ahrs->accelerometer.axis - // Calculate accelerometer measurement in the Earth coordinate frame - const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations - const float qwqx = Q.w * Q.x; - const float qwqy = Q.w * Q.y; - const float qwqz = Q.w * Q.z; - const float qxqy = Q.x * Q.y; - const float qxqz = Q.x * Q.z; - const float qyqz = Q.y * Q.z; - FusionVector accelerometer = {.axis = { - .x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), - .y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), - .z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z), - }}; // rotation matrix multiplied with the accelerometer + // Calculate accelerometer measurement in the Earth coordinate frame + const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations + const float qwqx = Q.w * Q.x; + const float qwqy = Q.w * Q.y; + const float qwqz = Q.w * Q.z; + const float qxqy = Q.x * Q.y; + const float qxqz = Q.x * Q.z; + const float qyqz = Q.y * Q.z; + FusionVector accelerometer = {.axis = { + .x = 2.0f * ((qwqw - 0.5f + Q.x * Q.x) * A.x + (qxqy - qwqz) * A.y + (qxqz + qwqy) * A.z), + .y = 2.0f * ((qxqy + qwqz) * A.x + (qwqw - 0.5f + Q.y * Q.y) * A.y + (qyqz - qwqx) * A.z), + .z = 2.0f * ((qxqz - qwqy) * A.x + (qyqz + qwqx) * A.y + (qwqw - 0.5f + Q.z * Q.z) * A.z), + }}; // rotation matrix multiplied with the accelerometer - // Remove gravity from accelerometer measurement - switch (ahrs->settings.convention) { - case FusionConventionNwu: - case FusionConventionEnu: - accelerometer.axis.z -= 1.0f; - break; - case FusionConventionNed: - accelerometer.axis.z += 1.0f; - break; - } - return accelerometer; + // Remove gravity from accelerometer measurement + switch (ahrs->settings.convention) { + case FusionConventionNwu: + case FusionConventionEnu: + accelerometer.axis.z -= 1.0f; + break; + case FusionConventionNed: + accelerometer.axis.z += 1.0f; + break; + } + return accelerometer; #undef Q #undef A } @@ -456,18 +482,22 @@ FusionVector FusionAhrsGetEarthAcceleration(const FusionAhrs *const ahrs) { * @param ahrs AHRS algorithm structure. * @return AHRS algorithm internal states. */ -FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs) { - const FusionAhrsInternalStates internalStates = { - .accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))), - .accelerometerIgnored = ahrs->accelerometerIgnored, - .accelerationRecoveryTrigger = - ahrs->settings.recoveryTriggerPeriod == 0 ? 0.0f : (float)ahrs->accelerationRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, - .magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))), - .magnetometerIgnored = ahrs->magnetometerIgnored, - .magneticRecoveryTrigger = - ahrs->settings.recoveryTriggerPeriod == 0 ? 0.0f : (float)ahrs->magneticRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, - }; - return internalStates; +FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahrs) +{ + const FusionAhrsInternalStates internalStates = { + .accelerationError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfAccelerometerFeedback))), + .accelerometerIgnored = ahrs->accelerometerIgnored, + .accelerationRecoveryTrigger = + ahrs->settings.recoveryTriggerPeriod == 0 + ? 0.0f + : (float)ahrs->accelerationRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, + .magneticError = FusionRadiansToDegrees(FusionAsin(2.0f * FusionVectorMagnitude(ahrs->halfMagnetometerFeedback))), + .magnetometerIgnored = ahrs->magnetometerIgnored, + .magneticRecoveryTrigger = ahrs->settings.recoveryTriggerPeriod == 0 + ? 0.0f + : (float)ahrs->magneticRecoveryTrigger / (float)ahrs->settings.recoveryTriggerPeriod, + }; + return internalStates; } /** @@ -475,14 +505,15 @@ FusionAhrsInternalStates FusionAhrsGetInternalStates(const FusionAhrs *const ahr * @param ahrs AHRS algorithm structure. * @return AHRS algorithm flags. */ -FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) { - const FusionAhrsFlags flags = { - .initialising = ahrs->initialising, - .angularRateRecovery = ahrs->angularRateRecovery, - .accelerationRecovery = ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout, - .magneticRecovery = ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout, - }; - return flags; +FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) +{ + const FusionAhrsFlags flags = { + .initialising = ahrs->initialising, + .angularRateRecovery = ahrs->angularRateRecovery, + .accelerationRecovery = ahrs->accelerationRecoveryTrigger > ahrs->accelerationRecoveryTimeout, + .magneticRecovery = ahrs->magneticRecoveryTrigger > ahrs->magneticRecoveryTimeout, + }; + return flags; } /** @@ -492,17 +523,18 @@ FusionAhrsFlags FusionAhrsGetFlags(const FusionAhrs *const ahrs) { * @param ahrs AHRS algorithm structure. * @param heading Heading angle in degrees. */ -void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) { +void FusionAhrsSetHeading(FusionAhrs *const ahrs, const float heading) +{ #define Q ahrs->quaternion.element - const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z); - const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading)); - const FusionQuaternion rotation = {.element = { - .w = cosf(halfYawMinusHeading), - .x = 0.0f, - .y = 0.0f, - .z = -1.0f * sinf(halfYawMinusHeading), - }}; - ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion); + const float yaw = atan2f(Q.w * Q.z + Q.x * Q.y, 0.5f - Q.y * Q.y - Q.z * Q.z); + const float halfYawMinusHeading = 0.5f * (yaw - FusionDegreesToRadians(heading)); + const FusionQuaternion rotation = {.element = { + .w = cosf(halfYawMinusHeading), + .x = 0.0f, + .y = 0.0f, + .z = -1.0f * sinf(halfYawMinusHeading), + }}; + ahrs->quaternion = FusionQuaternionMultiply(rotation, ahrs->quaternion); #undef Q } diff --git a/src/Fusion/FusionAhrs.h b/src/Fusion/FusionAhrs.h index aa4e40e71..aa2326e43 100644 --- a/src/Fusion/FusionAhrs.h +++ b/src/Fusion/FusionAhrs.h @@ -22,12 +22,12 @@ * @brief AHRS algorithm settings. */ typedef struct { - FusionConvention convention; - float gain; - float gyroscopeRange; - float accelerationRejection; - float magneticRejection; - unsigned int recoveryTriggerPeriod; + FusionConvention convention; + float gain; + float gyroscopeRange; + float accelerationRejection; + float magneticRejection; + unsigned int recoveryTriggerPeriod; } FusionAhrsSettings; /** @@ -35,43 +35,43 @@ typedef struct { * must not be accessed by the application. */ typedef struct { - FusionAhrsSettings settings; - FusionQuaternion quaternion; - FusionVector accelerometer; - bool initialising; - float rampedGain; - float rampedGainStep; - bool angularRateRecovery; - FusionVector halfAccelerometerFeedback; - FusionVector halfMagnetometerFeedback; - bool accelerometerIgnored; - int accelerationRecoveryTrigger; - int accelerationRecoveryTimeout; - bool magnetometerIgnored; - int magneticRecoveryTrigger; - int magneticRecoveryTimeout; + FusionAhrsSettings settings; + FusionQuaternion quaternion; + FusionVector accelerometer; + bool initialising; + float rampedGain; + float rampedGainStep; + bool angularRateRecovery; + FusionVector halfAccelerometerFeedback; + FusionVector halfMagnetometerFeedback; + bool accelerometerIgnored; + int accelerationRecoveryTrigger; + int accelerationRecoveryTimeout; + bool magnetometerIgnored; + int magneticRecoveryTrigger; + int magneticRecoveryTimeout; } FusionAhrs; /** * @brief AHRS algorithm internal states. */ typedef struct { - float accelerationError; - bool accelerometerIgnored; - float accelerationRecoveryTrigger; - float magneticError; - bool magnetometerIgnored; - float magneticRecoveryTrigger; + float accelerationError; + bool accelerometerIgnored; + float accelerationRecoveryTrigger; + float magneticError; + bool magnetometerIgnored; + float magneticRecoveryTrigger; } FusionAhrsInternalStates; /** * @brief AHRS algorithm flags. */ typedef struct { - bool initialising; - bool angularRateRecovery; - bool accelerationRecovery; - bool magneticRecovery; + bool initialising; + bool angularRateRecovery; + bool accelerationRecovery; + bool magneticRecovery; } FusionAhrsFlags; //------------------------------------------------------------------------------ @@ -83,13 +83,14 @@ void FusionAhrsReset(FusionAhrs *const ahrs); void FusionAhrsSetSettings(FusionAhrs *const ahrs, const FusionAhrsSettings *const settings); -void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const FusionVector magnetometer, - const float deltaTime); +void FusionAhrsUpdate(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, + const FusionVector magnetometer, const float deltaTime); -void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float deltaTime); +void FusionAhrsUpdateNoMagnetometer(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, + const float deltaTime); -void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, const float heading, - const float deltaTime); +void FusionAhrsUpdateExternalHeading(FusionAhrs *const ahrs, const FusionVector gyroscope, const FusionVector accelerometer, + const float heading, const float deltaTime); FusionQuaternion FusionAhrsGetQuaternion(const FusionAhrs *const ahrs); diff --git a/src/Fusion/FusionAxes.h b/src/Fusion/FusionAxes.h index 20b41e129..9673c88ff 100644 --- a/src/Fusion/FusionAxes.h +++ b/src/Fusion/FusionAxes.h @@ -22,30 +22,30 @@ * then alignment is +Y-X+Z. */ typedef enum { - FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */ - FusionAxesAlignmentPXNZPY, /* +X-Z+Y */ - FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */ - FusionAxesAlignmentPXPZNY, /* +X+Z-Y */ - FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */ - FusionAxesAlignmentNXPZPY, /* -X+Z+Y */ - FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */ - FusionAxesAlignmentNXNZNY, /* -X-Z-Y */ - FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */ - FusionAxesAlignmentPYNZNX, /* +Y-Z-X */ - FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */ - FusionAxesAlignmentPYPZPX, /* +Y+Z+X */ - FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */ - FusionAxesAlignmentNYNZPX, /* -Y-Z+X */ - FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */ - FusionAxesAlignmentNYPZNX, /* -Y+Z-X */ - FusionAxesAlignmentPZPYNX, /* +Z+Y-X */ - FusionAxesAlignmentPZPXPY, /* +Z+X+Y */ - FusionAxesAlignmentPZNYPX, /* +Z-Y+X */ - FusionAxesAlignmentPZNXNY, /* +Z-X-Y */ - FusionAxesAlignmentNZPYPX, /* -Z+Y+X */ - FusionAxesAlignmentNZNXPY, /* -Z-X+Y */ - FusionAxesAlignmentNZNYNX, /* -Z-Y-X */ - FusionAxesAlignmentNZPXNY, /* -Z+X-Y */ + FusionAxesAlignmentPXPYPZ, /* +X+Y+Z */ + FusionAxesAlignmentPXNZPY, /* +X-Z+Y */ + FusionAxesAlignmentPXNYNZ, /* +X-Y-Z */ + FusionAxesAlignmentPXPZNY, /* +X+Z-Y */ + FusionAxesAlignmentNXPYNZ, /* -X+Y-Z */ + FusionAxesAlignmentNXPZPY, /* -X+Z+Y */ + FusionAxesAlignmentNXNYPZ, /* -X-Y+Z */ + FusionAxesAlignmentNXNZNY, /* -X-Z-Y */ + FusionAxesAlignmentPYNXPZ, /* +Y-X+Z */ + FusionAxesAlignmentPYNZNX, /* +Y-Z-X */ + FusionAxesAlignmentPYPXNZ, /* +Y+X-Z */ + FusionAxesAlignmentPYPZPX, /* +Y+Z+X */ + FusionAxesAlignmentNYPXPZ, /* -Y+X+Z */ + FusionAxesAlignmentNYNZPX, /* -Y-Z+X */ + FusionAxesAlignmentNYNXNZ, /* -Y-X-Z */ + FusionAxesAlignmentNYPZNX, /* -Y+Z-X */ + FusionAxesAlignmentPZPYNX, /* +Z+Y-X */ + FusionAxesAlignmentPZPXPY, /* +Z+X+Y */ + FusionAxesAlignmentPZNYPX, /* +Z-Y+X */ + FusionAxesAlignmentPZNXNY, /* +Z-X-Y */ + FusionAxesAlignmentNZPYPX, /* -Z+Y+X */ + FusionAxesAlignmentNZNXPY, /* -Z-X+Y */ + FusionAxesAlignmentNZNYNX, /* -Z-Y-X */ + FusionAxesAlignmentNZPXNY, /* -Z+X-Y */ } FusionAxesAlignment; //------------------------------------------------------------------------------ @@ -57,128 +57,129 @@ typedef enum { * @param alignment Axes alignment. * @return Sensor axes aligned with the body axes. */ -static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment) { - FusionVector result; - switch (alignment) { - case FusionAxesAlignmentPXPYPZ: - break; - case FusionAxesAlignmentPXNZPY: - result.axis.x = +sensor.axis.x; - result.axis.y = -sensor.axis.z; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentPXNYNZ: - result.axis.x = +sensor.axis.x; - result.axis.y = -sensor.axis.y; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentPXPZNY: - result.axis.x = +sensor.axis.x; - result.axis.y = +sensor.axis.z; - result.axis.z = -sensor.axis.y; - return result; - case FusionAxesAlignmentNXPYNZ: - result.axis.x = -sensor.axis.x; - result.axis.y = +sensor.axis.y; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentNXPZPY: - result.axis.x = -sensor.axis.x; - result.axis.y = +sensor.axis.z; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentNXNYPZ: - result.axis.x = -sensor.axis.x; - result.axis.y = -sensor.axis.y; - result.axis.z = +sensor.axis.z; - return result; - case FusionAxesAlignmentNXNZNY: - result.axis.x = -sensor.axis.x; - result.axis.y = -sensor.axis.z; - result.axis.z = -sensor.axis.y; - return result; - case FusionAxesAlignmentPYNXPZ: - result.axis.x = +sensor.axis.y; - result.axis.y = -sensor.axis.x; - result.axis.z = +sensor.axis.z; - return result; - case FusionAxesAlignmentPYNZNX: - result.axis.x = +sensor.axis.y; - result.axis.y = -sensor.axis.z; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentPYPXNZ: - result.axis.x = +sensor.axis.y; - result.axis.y = +sensor.axis.x; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentPYPZPX: - result.axis.x = +sensor.axis.y; - result.axis.y = +sensor.axis.z; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentNYPXPZ: - result.axis.x = -sensor.axis.y; - result.axis.y = +sensor.axis.x; - result.axis.z = +sensor.axis.z; - return result; - case FusionAxesAlignmentNYNZPX: - result.axis.x = -sensor.axis.y; - result.axis.y = -sensor.axis.z; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentNYNXNZ: - result.axis.x = -sensor.axis.y; - result.axis.y = -sensor.axis.x; - result.axis.z = -sensor.axis.z; - return result; - case FusionAxesAlignmentNYPZNX: - result.axis.x = -sensor.axis.y; - result.axis.y = +sensor.axis.z; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentPZPYNX: - result.axis.x = +sensor.axis.z; - result.axis.y = +sensor.axis.y; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentPZPXPY: - result.axis.x = +sensor.axis.z; - result.axis.y = +sensor.axis.x; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentPZNYPX: - result.axis.x = +sensor.axis.z; - result.axis.y = -sensor.axis.y; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentPZNXNY: - result.axis.x = +sensor.axis.z; - result.axis.y = -sensor.axis.x; - result.axis.z = -sensor.axis.y; - return result; - case FusionAxesAlignmentNZPYPX: - result.axis.x = -sensor.axis.z; - result.axis.y = +sensor.axis.y; - result.axis.z = +sensor.axis.x; - return result; - case FusionAxesAlignmentNZNXPY: - result.axis.x = -sensor.axis.z; - result.axis.y = -sensor.axis.x; - result.axis.z = +sensor.axis.y; - return result; - case FusionAxesAlignmentNZNYNX: - result.axis.x = -sensor.axis.z; - result.axis.y = -sensor.axis.y; - result.axis.z = -sensor.axis.x; - return result; - case FusionAxesAlignmentNZPXNY: - result.axis.x = -sensor.axis.z; - result.axis.y = +sensor.axis.x; - result.axis.z = -sensor.axis.y; - return result; - } - return sensor; // avoid compiler warning +static inline FusionVector FusionAxesSwap(const FusionVector sensor, const FusionAxesAlignment alignment) +{ + FusionVector result; + switch (alignment) { + case FusionAxesAlignmentPXPYPZ: + break; + case FusionAxesAlignmentPXNZPY: + result.axis.x = +sensor.axis.x; + result.axis.y = -sensor.axis.z; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentPXNYNZ: + result.axis.x = +sensor.axis.x; + result.axis.y = -sensor.axis.y; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentPXPZNY: + result.axis.x = +sensor.axis.x; + result.axis.y = +sensor.axis.z; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentNXPYNZ: + result.axis.x = -sensor.axis.x; + result.axis.y = +sensor.axis.y; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentNXPZPY: + result.axis.x = -sensor.axis.x; + result.axis.y = +sensor.axis.z; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentNXNYPZ: + result.axis.x = -sensor.axis.x; + result.axis.y = -sensor.axis.y; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentNXNZNY: + result.axis.x = -sensor.axis.x; + result.axis.y = -sensor.axis.z; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentPYNXPZ: + result.axis.x = +sensor.axis.y; + result.axis.y = -sensor.axis.x; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentPYNZNX: + result.axis.x = +sensor.axis.y; + result.axis.y = -sensor.axis.z; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPYPXNZ: + result.axis.x = +sensor.axis.y; + result.axis.y = +sensor.axis.x; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentPYPZPX: + result.axis.x = +sensor.axis.y; + result.axis.y = +sensor.axis.z; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNYPXPZ: + result.axis.x = -sensor.axis.y; + result.axis.y = +sensor.axis.x; + result.axis.z = +sensor.axis.z; + return result; + case FusionAxesAlignmentNYNZPX: + result.axis.x = -sensor.axis.y; + result.axis.y = -sensor.axis.z; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNYNXNZ: + result.axis.x = -sensor.axis.y; + result.axis.y = -sensor.axis.x; + result.axis.z = -sensor.axis.z; + return result; + case FusionAxesAlignmentNYPZNX: + result.axis.x = -sensor.axis.y; + result.axis.y = +sensor.axis.z; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPZPYNX: + result.axis.x = +sensor.axis.z; + result.axis.y = +sensor.axis.y; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentPZPXPY: + result.axis.x = +sensor.axis.z; + result.axis.y = +sensor.axis.x; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentPZNYPX: + result.axis.x = +sensor.axis.z; + result.axis.y = -sensor.axis.y; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentPZNXNY: + result.axis.x = +sensor.axis.z; + result.axis.y = -sensor.axis.x; + result.axis.z = -sensor.axis.y; + return result; + case FusionAxesAlignmentNZPYPX: + result.axis.x = -sensor.axis.z; + result.axis.y = +sensor.axis.y; + result.axis.z = +sensor.axis.x; + return result; + case FusionAxesAlignmentNZNXPY: + result.axis.x = -sensor.axis.z; + result.axis.y = -sensor.axis.x; + result.axis.z = +sensor.axis.y; + return result; + case FusionAxesAlignmentNZNYNX: + result.axis.x = -sensor.axis.z; + result.axis.y = -sensor.axis.y; + result.axis.z = -sensor.axis.x; + return result; + case FusionAxesAlignmentNZPXNY: + result.axis.x = -sensor.axis.z; + result.axis.y = +sensor.axis.x; + result.axis.z = -sensor.axis.y; + return result; + } + return sensor; // avoid compiler warning } #endif diff --git a/src/Fusion/FusionCalibration.h b/src/Fusion/FusionCalibration.h index 05fad4207..be7102b73 100644 --- a/src/Fusion/FusionCalibration.h +++ b/src/Fusion/FusionCalibration.h @@ -23,9 +23,11 @@ * @param offset Offset. * @return Calibrated measurement. */ -static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment, const FusionVector sensitivity, - const FusionVector offset) { - return FusionMatrixMultiplyVector(misalignment, FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity)); +static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibrated, const FusionMatrix misalignment, + const FusionVector sensitivity, const FusionVector offset) +{ + return FusionMatrixMultiplyVector(misalignment, + FusionVectorHadamardProduct(FusionVectorSubtract(uncalibrated, offset), sensitivity)); } /** @@ -36,8 +38,9 @@ static inline FusionVector FusionCalibrationInertial(const FusionVector uncalibr * @return Calibrated measurement. */ static inline FusionVector FusionCalibrationMagnetic(const FusionVector uncalibrated, const FusionMatrix softIronMatrix, - const FusionVector hardIronOffset) { - return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset)); + const FusionVector hardIronOffset) +{ + return FusionMatrixMultiplyVector(softIronMatrix, FusionVectorSubtract(uncalibrated, hardIronOffset)); } #endif diff --git a/src/Fusion/FusionCompass.c b/src/Fusion/FusionCompass.c index 7e4ddc6ec..6a6f9591a 100644 --- a/src/Fusion/FusionCompass.c +++ b/src/Fusion/FusionCompass.c @@ -22,27 +22,29 @@ * @param magnetometer Magnetometer measurement in any calibrated units. * @return Heading angle in degrees. */ -float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, const FusionVector magnetometer) { - switch (convention) { - case FusionConventionNwu: { - const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); - const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); - return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); - } - case FusionConventionEnu: { - const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); - const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); - const FusionVector east = FusionVectorMultiplyScalar(west, -1.0f); - return FusionRadiansToDegrees(atan2f(north.axis.x, east.axis.x)); - } - case FusionConventionNed: { - const FusionVector up = FusionVectorMultiplyScalar(accelerometer, -1.0f); - const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(up, magnetometer)); - const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, up)); - return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); - } - } - return 0; // avoid compiler warning +float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, + const FusionVector magnetometer) +{ + switch (convention) { + case FusionConventionNwu: { + const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); + const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); + return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); + } + case FusionConventionEnu: { + const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(accelerometer, magnetometer)); + const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, accelerometer)); + const FusionVector east = FusionVectorMultiplyScalar(west, -1.0f); + return FusionRadiansToDegrees(atan2f(north.axis.x, east.axis.x)); + } + case FusionConventionNed: { + const FusionVector up = FusionVectorMultiplyScalar(accelerometer, -1.0f); + const FusionVector west = FusionVectorNormalise(FusionVectorCrossProduct(up, magnetometer)); + const FusionVector north = FusionVectorNormalise(FusionVectorCrossProduct(west, up)); + return FusionRadiansToDegrees(atan2f(west.axis.x, north.axis.x)); + } + } + return 0; // avoid compiler warning } //------------------------------------------------------------------------------ diff --git a/src/Fusion/FusionCompass.h b/src/Fusion/FusionCompass.h index 78326c064..a3d0b466a 100644 --- a/src/Fusion/FusionCompass.h +++ b/src/Fusion/FusionCompass.h @@ -17,7 +17,8 @@ //------------------------------------------------------------------------------ // Function declarations -float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, const FusionVector magnetometer); +float FusionCompassCalculateHeading(const FusionConvention convention, const FusionVector accelerometer, + const FusionVector magnetometer); #endif diff --git a/src/Fusion/FusionConvention.h b/src/Fusion/FusionConvention.h index c1fb77f21..0b0d43adc 100644 --- a/src/Fusion/FusionConvention.h +++ b/src/Fusion/FusionConvention.h @@ -14,9 +14,9 @@ * @brief Earth axes convention. */ typedef enum { - FusionConventionNwu, /* North-West-Up */ - FusionConventionEnu, /* East-North-Up */ - FusionConventionNed, /* North-East-Down */ + FusionConventionNwu, /* North-West-Up */ + FusionConventionEnu, /* East-North-Up */ + FusionConventionNed, /* North-East-Down */ } FusionConvention; #endif diff --git a/src/Fusion/FusionMath.h b/src/Fusion/FusionMath.h index e8d50a7dd..c3fc34b2d 100644 --- a/src/Fusion/FusionMath.h +++ b/src/Fusion/FusionMath.h @@ -21,27 +21,27 @@ * @brief 3D vector. */ typedef union { - float array[3]; + float array[3]; - struct { - float x; - float y; - float z; - } axis; + struct { + float x; + float y; + float z; + } axis; } FusionVector; /** * @brief Quaternion. */ typedef union { - float array[4]; + float array[4]; - struct { - float w; - float x; - float y; - float z; - } element; + struct { + float w; + float x; + float y; + float z; + } element; } FusionQuaternion; /** @@ -49,19 +49,19 @@ typedef union { * See http://en.wikipedia.org/wiki/Row-major_order */ typedef union { - float array[3][3]; + float array[3][3]; - struct { - float xx; - float xy; - float xz; - float yx; - float yy; - float yz; - float zx; - float zy; - float zz; - } element; + struct { + float xx; + float xy; + float xz; + float yx; + float yy; + float yz; + float zx; + float zy; + float zz; + } element; } FusionMatrix; /** @@ -69,13 +69,13 @@ typedef union { * X, Y, and Z respectively. */ typedef union { - float array[3]; + float array[3]; - struct { - float roll; - float pitch; - float yaw; - } angle; + struct { + float roll; + float pitch; + float yaw; + } angle; } FusionEuler; /** @@ -124,14 +124,20 @@ typedef union { * @param degrees Degrees. * @return Radians. */ -static inline float FusionDegreesToRadians(const float degrees) { return degrees * ((float)M_PI / 180.0f); } +static inline float FusionDegreesToRadians(const float degrees) +{ + return degrees * ((float)M_PI / 180.0f); +} /** * @brief Converts radians to degrees. * @param radians Radians. * @return Degrees. */ -static inline float FusionRadiansToDegrees(const float radians) { return radians * (180.0f / (float)M_PI); } +static inline float FusionRadiansToDegrees(const float radians) +{ + return radians * (180.0f / (float)M_PI); +} //------------------------------------------------------------------------------ // Inline functions - Arc sine @@ -141,14 +147,15 @@ static inline float FusionRadiansToDegrees(const float radians) { return radians * @param value Value. * @return Arc sine of the value. */ -static inline float FusionAsin(const float value) { - if (value <= -1.0f) { - return (float)M_PI / -2.0f; - } - if (value >= 1.0f) { - return (float)M_PI / 2.0f; - } - return asinf(value); +static inline float FusionAsin(const float value) +{ + if (value <= -1.0f) { + return (float)M_PI / -2.0f; + } + if (value >= 1.0f) { + return (float)M_PI / 2.0f; + } + return asinf(value); } //------------------------------------------------------------------------------ @@ -162,16 +169,17 @@ static inline float FusionAsin(const float value) { * @param x Operand. * @return Reciprocal of the square root of x. */ -static inline float FusionFastInverseSqrt(const float x) { +static inline float FusionFastInverseSqrt(const float x) +{ - typedef union { - float f; - int32_t i; - } Union32; + typedef union { + float f; + int32_t i; + } Union32; - Union32 union32 = {.f = x}; - union32.i = 0x5F1F1412 - (union32.i >> 1); - return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f); + Union32 union32 = {.f = x}; + union32.i = 0x5F1F1412 - (union32.i >> 1); + return union32.f * (1.69000231f - 0.714158168f * x * union32.f * union32.f); } #endif @@ -184,8 +192,9 @@ static inline float FusionFastInverseSqrt(const float x) { * @param vector Vector. * @return True if the vector is zero. */ -static inline bool FusionVectorIsZero(const FusionVector vector) { - return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f); +static inline bool FusionVectorIsZero(const FusionVector vector) +{ + return (vector.axis.x == 0.0f) && (vector.axis.y == 0.0f) && (vector.axis.z == 0.0f); } /** @@ -194,13 +203,14 @@ static inline bool FusionVectorIsZero(const FusionVector vector) { * @param vectorB Vector B. * @return Sum of two vectors. */ -static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) { - const FusionVector result = {.axis = { - .x = vectorA.axis.x + vectorB.axis.x, - .y = vectorA.axis.y + vectorB.axis.y, - .z = vectorA.axis.z + vectorB.axis.z, - }}; - return result; +static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const FusionVector vectorB) +{ + const FusionVector result = {.axis = { + .x = vectorA.axis.x + vectorB.axis.x, + .y = vectorA.axis.y + vectorB.axis.y, + .z = vectorA.axis.z + vectorB.axis.z, + }}; + return result; } /** @@ -209,13 +219,14 @@ static inline FusionVector FusionVectorAdd(const FusionVector vectorA, const Fus * @param vectorB Vector B. * @return Vector B subtracted from vector A. */ -static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) { - const FusionVector result = {.axis = { - .x = vectorA.axis.x - vectorB.axis.x, - .y = vectorA.axis.y - vectorB.axis.y, - .z = vectorA.axis.z - vectorB.axis.z, - }}; - return result; +static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, const FusionVector vectorB) +{ + const FusionVector result = {.axis = { + .x = vectorA.axis.x - vectorB.axis.x, + .y = vectorA.axis.y - vectorB.axis.y, + .z = vectorA.axis.z - vectorB.axis.z, + }}; + return result; } /** @@ -223,7 +234,10 @@ static inline FusionVector FusionVectorSubtract(const FusionVector vectorA, cons * @param vector Vector. * @return Sum of the elements. */ -static inline float FusionVectorSum(const FusionVector vector) { return vector.axis.x + vector.axis.y + vector.axis.z; } +static inline float FusionVectorSum(const FusionVector vector) +{ + return vector.axis.x + vector.axis.y + vector.axis.z; +} /** * @brief Returns the multiplication of a vector by a scalar. @@ -231,13 +245,14 @@ static inline float FusionVectorSum(const FusionVector vector) { return vector.a * @param scalar Scalar. * @return Multiplication of a vector by a scalar. */ -static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) { - const FusionVector result = {.axis = { - .x = vector.axis.x * scalar, - .y = vector.axis.y * scalar, - .z = vector.axis.z * scalar, - }}; - return result; +static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, const float scalar) +{ + const FusionVector result = {.axis = { + .x = vector.axis.x * scalar, + .y = vector.axis.y * scalar, + .z = vector.axis.z * scalar, + }}; + return result; } /** @@ -246,13 +261,14 @@ static inline FusionVector FusionVectorMultiplyScalar(const FusionVector vector, * @param vectorB Vector B. * @return Hadamard product. */ -static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) { - const FusionVector result = {.axis = { - .x = vectorA.axis.x * vectorB.axis.x, - .y = vectorA.axis.y * vectorB.axis.y, - .z = vectorA.axis.z * vectorB.axis.z, - }}; - return result; +static inline FusionVector FusionVectorHadamardProduct(const FusionVector vectorA, const FusionVector vectorB) +{ + const FusionVector result = {.axis = { + .x = vectorA.axis.x * vectorB.axis.x, + .y = vectorA.axis.y * vectorB.axis.y, + .z = vectorA.axis.z * vectorB.axis.z, + }}; + return result; } /** @@ -261,15 +277,16 @@ static inline FusionVector FusionVectorHadamardProduct(const FusionVector vector * @param vectorB Vector B. * @return Cross product. */ -static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) { +static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, const FusionVector vectorB) +{ #define A vectorA.axis #define B vectorB.axis - const FusionVector result = {.axis = { - .x = A.y * B.z - A.z * B.y, - .y = A.z * B.x - A.x * B.z, - .z = A.x * B.y - A.y * B.x, - }}; - return result; + const FusionVector result = {.axis = { + .x = A.y * B.z - A.z * B.y, + .y = A.z * B.x - A.x * B.z, + .z = A.x * B.y - A.y * B.x, + }}; + return result; #undef A #undef B } @@ -280,8 +297,9 @@ static inline FusionVector FusionVectorCrossProduct(const FusionVector vectorA, * @param vectorB Vector B. * @return Dot product. */ -static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB) { - return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB)); +static inline float FusionVectorDotProduct(const FusionVector vectorA, const FusionVector vectorB) +{ + return FusionVectorSum(FusionVectorHadamardProduct(vectorA, vectorB)); } /** @@ -289,27 +307,34 @@ static inline float FusionVectorDotProduct(const FusionVector vectorA, const Fus * @param vector Vector. * @return Vector magnitude squared. */ -static inline float FusionVectorMagnitudeSquared(const FusionVector vector) { return FusionVectorSum(FusionVectorHadamardProduct(vector, vector)); } +static inline float FusionVectorMagnitudeSquared(const FusionVector vector) +{ + return FusionVectorSum(FusionVectorHadamardProduct(vector, vector)); +} /** * @brief Returns the vector magnitude. * @param vector Vector. * @return Vector magnitude. */ -static inline float FusionVectorMagnitude(const FusionVector vector) { return sqrtf(FusionVectorMagnitudeSquared(vector)); } +static inline float FusionVectorMagnitude(const FusionVector vector) +{ + return sqrtf(FusionVectorMagnitudeSquared(vector)); +} /** * @brief Returns the normalised vector. * @param vector Vector. * @return Normalised vector. */ -static inline FusionVector FusionVectorNormalise(const FusionVector vector) { +static inline FusionVector FusionVectorNormalise(const FusionVector vector) +{ #ifdef FUSION_USE_NORMAL_SQRT - const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector)); + const float magnitudeReciprocal = 1.0f / sqrtf(FusionVectorMagnitudeSquared(vector)); #else - const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector)); + const float magnitudeReciprocal = FusionFastInverseSqrt(FusionVectorMagnitudeSquared(vector)); #endif - return FusionVectorMultiplyScalar(vector, magnitudeReciprocal); + return FusionVectorMultiplyScalar(vector, magnitudeReciprocal); } //------------------------------------------------------------------------------ @@ -321,14 +346,15 @@ static inline FusionVector FusionVectorNormalise(const FusionVector vector) { * @param quaternionB Quaternion B. * @return Sum of two quaternions. */ -static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { - const FusionQuaternion result = {.element = { - .w = quaternionA.element.w + quaternionB.element.w, - .x = quaternionA.element.x + quaternionB.element.x, - .y = quaternionA.element.y + quaternionB.element.y, - .z = quaternionA.element.z + quaternionB.element.z, - }}; - return result; +static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) +{ + const FusionQuaternion result = {.element = { + .w = quaternionA.element.w + quaternionB.element.w, + .x = quaternionA.element.x + quaternionB.element.x, + .y = quaternionA.element.y + quaternionB.element.y, + .z = quaternionA.element.z + quaternionB.element.z, + }}; + return result; } /** @@ -337,16 +363,17 @@ static inline FusionQuaternion FusionQuaternionAdd(const FusionQuaternion quater * @param quaternionB Quaternion B (to be pre-multiplied). * @return Multiplication of two quaternions. */ -static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) { +static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion quaternionA, const FusionQuaternion quaternionB) +{ #define A quaternionA.element #define B quaternionB.element - const FusionQuaternion result = {.element = { - .w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z, - .x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y, - .y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x, - .z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w, - }}; - return result; + const FusionQuaternion result = {.element = { + .w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z, + .x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y, + .y = A.w * B.y - A.x * B.z + A.y * B.w + A.z * B.x, + .z = A.w * B.z + A.x * B.y - A.y * B.x + A.z * B.w, + }}; + return result; #undef A #undef B } @@ -360,16 +387,17 @@ static inline FusionQuaternion FusionQuaternionMultiply(const FusionQuaternion q * @param vector Vector. * @return Multiplication of a quaternion with a vector. */ -static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) { +static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuaternion quaternion, const FusionVector vector) +{ #define Q quaternion.element #define V vector.axis - const FusionQuaternion result = {.element = { - .w = -Q.x * V.x - Q.y * V.y - Q.z * V.z, - .x = Q.w * V.x + Q.y * V.z - Q.z * V.y, - .y = Q.w * V.y - Q.x * V.z + Q.z * V.x, - .z = Q.w * V.z + Q.x * V.y - Q.y * V.x, - }}; - return result; + const FusionQuaternion result = {.element = { + .w = -Q.x * V.x - Q.y * V.y - Q.z * V.z, + .x = Q.w * V.x + Q.y * V.z - Q.z * V.y, + .y = Q.w * V.y - Q.x * V.z + Q.z * V.x, + .z = Q.w * V.z + Q.x * V.y - Q.y * V.x, + }}; + return result; #undef Q #undef V } @@ -379,20 +407,21 @@ static inline FusionQuaternion FusionQuaternionMultiplyVector(const FusionQuater * @param quaternion Quaternion. * @return Normalised quaternion. */ -static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion) { +static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion quaternion) +{ #define Q quaternion.element #ifdef FUSION_USE_NORMAL_SQRT - const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); + const float magnitudeReciprocal = 1.0f / sqrtf(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); #else - const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); + const float magnitudeReciprocal = FusionFastInverseSqrt(Q.w * Q.w + Q.x * Q.x + Q.y * Q.y + Q.z * Q.z); #endif - const FusionQuaternion result = {.element = { - .w = Q.w * magnitudeReciprocal, - .x = Q.x * magnitudeReciprocal, - .y = Q.y * magnitudeReciprocal, - .z = Q.z * magnitudeReciprocal, - }}; - return result; + const FusionQuaternion result = {.element = { + .w = Q.w * magnitudeReciprocal, + .x = Q.x * magnitudeReciprocal, + .y = Q.y * magnitudeReciprocal, + .z = Q.z * magnitudeReciprocal, + }}; + return result; #undef Q } @@ -405,14 +434,15 @@ static inline FusionQuaternion FusionQuaternionNormalise(const FusionQuaternion * @param vector Vector. * @return Multiplication of a matrix with a vector. */ -static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) { +static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, const FusionVector vector) +{ #define R matrix.element - const FusionVector result = {.axis = { - .x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z, - .y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z, - .z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z, - }}; - return result; + const FusionVector result = {.axis = { + .x = R.xx * vector.axis.x + R.xy * vector.axis.y + R.xz * vector.axis.z, + .y = R.yx * vector.axis.x + R.yy * vector.axis.y + R.yz * vector.axis.z, + .z = R.zx * vector.axis.x + R.zy * vector.axis.y + R.zz * vector.axis.z, + }}; + return result; #undef R } @@ -424,27 +454,28 @@ static inline FusionVector FusionMatrixMultiplyVector(const FusionMatrix matrix, * @param quaternion Quaternion. * @return Rotation matrix. */ -static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion) { +static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quaternion) +{ #define Q quaternion.element - const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations - const float qwqx = Q.w * Q.x; - const float qwqy = Q.w * Q.y; - const float qwqz = Q.w * Q.z; - const float qxqy = Q.x * Q.y; - const float qxqz = Q.x * Q.z; - const float qyqz = Q.y * Q.z; - const FusionMatrix matrix = {.element = { - .xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x), - .xy = 2.0f * (qxqy - qwqz), - .xz = 2.0f * (qxqz + qwqy), - .yx = 2.0f * (qxqy + qwqz), - .yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y), - .yz = 2.0f * (qyqz - qwqx), - .zx = 2.0f * (qxqz - qwqy), - .zy = 2.0f * (qyqz + qwqx), - .zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z), - }}; - return matrix; + const float qwqw = Q.w * Q.w; // calculate common terms to avoid repeated operations + const float qwqx = Q.w * Q.x; + const float qwqy = Q.w * Q.y; + const float qwqz = Q.w * Q.z; + const float qxqy = Q.x * Q.y; + const float qxqz = Q.x * Q.z; + const float qyqz = Q.y * Q.z; + const FusionMatrix matrix = {.element = { + .xx = 2.0f * (qwqw - 0.5f + Q.x * Q.x), + .xy = 2.0f * (qxqy - qwqz), + .xz = 2.0f * (qxqz + qwqy), + .yx = 2.0f * (qxqy + qwqz), + .yy = 2.0f * (qwqw - 0.5f + Q.y * Q.y), + .yz = 2.0f * (qyqz - qwqx), + .zx = 2.0f * (qxqz - qwqy), + .zy = 2.0f * (qyqz + qwqx), + .zz = 2.0f * (qwqw - 0.5f + Q.z * Q.z), + }}; + return matrix; #undef Q } @@ -453,15 +484,16 @@ static inline FusionMatrix FusionQuaternionToMatrix(const FusionQuaternion quate * @param quaternion Quaternion. * @return Euler angles in degrees. */ -static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) { +static inline FusionEuler FusionQuaternionToEuler(const FusionQuaternion quaternion) +{ #define Q quaternion.element - const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations - const FusionEuler euler = {.angle = { - .roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)), - .pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))), - .yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)), - }}; - return euler; + const float halfMinusQySquared = 0.5f - Q.y * Q.y; // calculate common terms to avoid repeated operations + const FusionEuler euler = {.angle = { + .roll = FusionRadiansToDegrees(atan2f(Q.w * Q.x + Q.y * Q.z, halfMinusQySquared - Q.x * Q.x)), + .pitch = FusionRadiansToDegrees(FusionAsin(2.0f * (Q.w * Q.y - Q.z * Q.x))), + .yaw = FusionRadiansToDegrees(atan2f(Q.w * Q.z + Q.x * Q.y, halfMinusQySquared - Q.z * Q.z)), + }}; + return euler; #undef Q } diff --git a/src/Fusion/FusionOffset.c b/src/Fusion/FusionOffset.c index 4a0546a88..d4334c874 100644 --- a/src/Fusion/FusionOffset.c +++ b/src/Fusion/FusionOffset.c @@ -37,11 +37,12 @@ * @param offset Gyroscope offset algorithm structure. * @param sampleRate Sample rate in Hz. */ -void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate) { - offset->filterCoefficient = 2.0f * (float)M_PI * CUTOFF_FREQUENCY * (1.0f / (float)sampleRate); - offset->timeout = TIMEOUT * sampleRate; - offset->timer = 0; - offset->gyroscopeOffset = FUSION_VECTOR_ZERO; +void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampleRate) +{ + offset->filterCoefficient = 2.0f * (float)M_PI * CUTOFF_FREQUENCY * (1.0f / (float)sampleRate); + offset->timeout = TIMEOUT * sampleRate; + offset->timer = 0; + offset->gyroscopeOffset = FUSION_VECTOR_ZERO; } /** @@ -51,26 +52,28 @@ void FusionOffsetInitialise(FusionOffset *const offset, const unsigned int sampl * @param gyroscope Gyroscope measurement in degrees per second. * @return Corrected gyroscope measurement in degrees per second. */ -FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) { +FusionVector FusionOffsetUpdate(FusionOffset *const offset, FusionVector gyroscope) +{ - // Subtract offset from gyroscope measurement - gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset); + // Subtract offset from gyroscope measurement + gyroscope = FusionVectorSubtract(gyroscope, offset->gyroscopeOffset); - // Reset timer if gyroscope not stationary - if ((fabsf(gyroscope.axis.x) > THRESHOLD) || (fabsf(gyroscope.axis.y) > THRESHOLD) || (fabsf(gyroscope.axis.z) > THRESHOLD)) { - offset->timer = 0; + // Reset timer if gyroscope not stationary + if ((fabsf(gyroscope.axis.x) > THRESHOLD) || (fabsf(gyroscope.axis.y) > THRESHOLD) || (fabsf(gyroscope.axis.z) > THRESHOLD)) { + offset->timer = 0; + return gyroscope; + } + + // Increment timer while gyroscope stationary + if (offset->timer < offset->timeout) { + offset->timer++; + return gyroscope; + } + + // Adjust offset if timer has elapsed + offset->gyroscopeOffset = + FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient)); return gyroscope; - } - - // Increment timer while gyroscope stationary - if (offset->timer < offset->timeout) { - offset->timer++; - return gyroscope; - } - - // Adjust offset if timer has elapsed - offset->gyroscopeOffset = FusionVectorAdd(offset->gyroscopeOffset, FusionVectorMultiplyScalar(gyroscope, offset->filterCoefficient)); - return gyroscope; } //------------------------------------------------------------------------------ diff --git a/src/Fusion/FusionOffset.h b/src/Fusion/FusionOffset.h index 7ad945460..51ae4a896 100644 --- a/src/Fusion/FusionOffset.h +++ b/src/Fusion/FusionOffset.h @@ -21,10 +21,10 @@ * internally and must not be accessed by the application. */ typedef struct { - float filterCoefficient; - unsigned int timeout; - unsigned int timer; - FusionVector gyroscopeOffset; + float filterCoefficient; + unsigned int timeout; + unsigned int timer; + FusionVector gyroscopeOffset; } FusionOffset; //------------------------------------------------------------------------------ diff --git a/src/GPSStatus.h b/src/GPSStatus.h index 162be8b5a..a1a9f2c56 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -4,124 +4,136 @@ #include "configuration.h" #include -namespace meshtastic { +namespace meshtastic +{ /// Describes the state of the GPS system. -class GPSStatus : public Status { +class GPSStatus : public Status +{ -private: - CallbackObserver statusObserver = CallbackObserver(this, &GPSStatus::updateStatus); + private: + CallbackObserver statusObserver = + CallbackObserver(this, &GPSStatus::updateStatus); - bool hasLock = false; // default to false, until we complete our first read - bool isConnected = false; // Do we have a GPS we are talking to + bool hasLock = false; // default to false, until we complete our first read + bool isConnected = false; // Do we have a GPS we are talking to - bool isPowerSaving = false; // Are we in power saving state + bool isPowerSaving = false; // Are we in power saving state - meshtastic_Position p = meshtastic_Position_init_default; + meshtastic_Position p = meshtastic_Position_init_default; - /// Time of last valid GPS fix (millis since boot) - uint32_t lastFixMillis = 0; + /// Time of last valid GPS fix (millis since boot) + uint32_t lastFixMillis = 0; -public: - GPSStatus() { statusType = STATUS_TYPE_GPS; } + public: + GPSStatus() { statusType = STATUS_TYPE_GPS; } - // preferred method - GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos) : Status() { - this->hasLock = hasLock; - this->isConnected = isConnected; - this->isPowerSaving = isPowerSaving; + // preferred method + GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos) : Status() + { + this->hasLock = hasLock; + this->isConnected = isConnected; + this->isPowerSaving = isPowerSaving; - // all-in-one struct copy - this->p = pos; - } - - GPSStatus(const GPSStatus &); - GPSStatus &operator=(const GPSStatus &); - - void observe(Observable *source) { statusObserver.observe(source); } - - bool getHasLock() const { return hasLock; } - - bool getIsConnected() const { return isConnected; } - - bool getIsPowerSaving() const { return isPowerSaving; } - - int32_t getLatitude() const { - if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.latitude_i; - } else { - return p.latitude_i; + // all-in-one struct copy + this->p = pos; } - } - int32_t getLongitude() const { - if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.longitude_i; - } else { - return p.longitude_i; + GPSStatus(const GPSStatus &); + GPSStatus &operator=(const GPSStatus &); + + void observe(Observable *source) { statusObserver.observe(source); } + + bool getHasLock() const { return hasLock; } + + bool getIsConnected() const { return isConnected; } + + bool getIsPowerSaving() const { return isPowerSaving; } + + int32_t getLatitude() const + { + if (config.position.fixed_position) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.latitude_i; + } else { + return p.latitude_i; + } } - } - int32_t getAltitude() const { - if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.altitude; - } else { - return p.altitude; + int32_t getLongitude() const + { + if (config.position.fixed_position) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.longitude_i; + } else { + return p.longitude_i; + } } - } - uint32_t getDOP() const { return p.PDOP; } + int32_t getAltitude() const + { + if (config.position.fixed_position) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + return node->position.altitude; + } else { + return p.altitude; + } + } - uint32_t getHeading() const { return p.ground_track; } + uint32_t getDOP() const { return p.PDOP; } - uint32_t getNumSatellites() const { return p.sats_in_view; } + uint32_t getHeading() const { return p.ground_track; } - /// Return millis() when the last GPS fix occurred (0 = never) - uint32_t getLastFixMillis() const { return lastFixMillis; } + uint32_t getNumSatellites() const { return p.sats_in_view; } - bool matches(const GPSStatus *newStatus) const { + /// Return millis() when the last GPS fix occurred (0 = never) + uint32_t getLastFixMillis() const { return lastFixMillis; } + + bool matches(const GPSStatus *newStatus) const + { #ifdef GPS_DEBUG - LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp); + LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp); #endif - return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || newStatus->isPowerSaving != isPowerSaving || - newStatus->p.latitude_i != p.latitude_i || newStatus->p.longitude_i != p.longitude_i || newStatus->p.altitude != p.altitude || - newStatus->p.altitude_hae != p.altitude_hae || newStatus->p.PDOP != p.PDOP || newStatus->p.ground_track != p.ground_track || - newStatus->p.ground_speed != p.ground_speed || newStatus->p.sats_in_view != p.sats_in_view); - } - - int updateStatus(const GPSStatus *newStatus) { - // Only update the status if values have actually changed - bool isDirty = matches(newStatus); - - if (isDirty && p.timestamp && (newStatus->p.timestamp == p.timestamp)) { - // We can NEVER be in two locations at the same time! (also PR #886) - LOG_ERROR("BUG: Positional timestamp unchanged from prev solution"); + return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || + newStatus->isPowerSaving != isPowerSaving || newStatus->p.latitude_i != p.latitude_i || + newStatus->p.longitude_i != p.longitude_i || newStatus->p.altitude != p.altitude || + newStatus->p.altitude_hae != p.altitude_hae || newStatus->p.PDOP != p.PDOP || + newStatus->p.ground_track != p.ground_track || newStatus->p.ground_speed != p.ground_speed || + newStatus->p.sats_in_view != p.sats_in_view); } - initialized = true; - hasLock = newStatus->hasLock; - isConnected = newStatus->isConnected; + int updateStatus(const GPSStatus *newStatus) + { + // Only update the status if values have actually changed + bool isDirty = matches(newStatus); - p = newStatus->p; + if (isDirty && p.timestamp && (newStatus->p.timestamp == p.timestamp)) { + // We can NEVER be in two locations at the same time! (also PR #886) + LOG_ERROR("BUG: Positional timestamp unchanged from prev solution"); + } - if (isDirty) { - if (hasLock) { - // Record time of last valid GPS fix - lastFixMillis = millis(); + initialized = true; + hasLock = newStatus->hasLock; + isConnected = newStatus->isConnected; - // In debug logs, identify position by @timestamp:stage (stage 3 = notify) - LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, p.latitude_i * 1e-7, - p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, p.ground_speed * 1e-2, p.sats_in_view); - } else { - LOG_DEBUG("No GPS lock"); - } - onNewStatus.notifyObservers(this); + p = newStatus->p; + + if (isDirty) { + if (hasLock) { + // Record time of last valid GPS fix + lastFixMillis = millis(); + + // In debug logs, identify position by @timestamp:stage (stage 3 = notify) + LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, + p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, + p.ground_speed * 1e-2, p.sats_in_view); + } else { + LOG_DEBUG("No GPS lock"); + } + onNewStatus.notifyObservers(this); + } + return 0; } - return 0; - } }; } // namespace meshtastic diff --git a/src/GpioLogic.cpp b/src/GpioLogic.cpp index 1bb34ca78..ecdf514e4 100644 --- a/src/GpioLogic.cpp +++ b/src/GpioLogic.cpp @@ -1,90 +1,102 @@ #include "GpioLogic.h" #include -void GpioVirtPin::set(bool value) { - if (value != this->value) { - this->value = value ? PinState::On : PinState::Off; - if (dependentPin) - dependentPin->update(); - } +void GpioVirtPin::set(bool value) +{ + if (value != this->value) { + this->value = value ? PinState::On : PinState::Off; + if (dependentPin) + dependentPin->update(); + } } -void GpioHwPin::set(bool value) { - pinMode(num, OUTPUT); - digitalWrite(num, value); +void GpioHwPin::set(bool value) +{ + pinMode(num, OUTPUT); + digitalWrite(num, value); } GpioTransformer::GpioTransformer(GpioPin *outPin) : outPin(outPin) {} -void GpioTransformer::set(bool value) { outPin->set(value); } +void GpioTransformer::set(bool value) +{ + outPin->set(value); +} -GpioUnaryTransformer::GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioTransformer(outPin), inPin(inPin) { - assert(!inPin->dependentPin); // We only allow one dependent pin - inPin->dependentPin = this; +GpioUnaryTransformer::GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioTransformer(outPin), inPin(inPin) +{ + assert(!inPin->dependentPin); // We only allow one dependent pin + inPin->dependentPin = this; - // Don't update at construction time, because various GpioPins might be global constructor based not yet initied - // because order of operations for global constructors is not defined. update(); + // Don't update at construction time, because various GpioPins might be global constructor based not yet initied because + // order of operations for global constructors is not defined. + // update(); } /** * Update the output pin based on the current state of the input pin. */ -void GpioUnaryTransformer::update() { - auto p = inPin->get(); - if (p == GpioVirtPin::PinState::Unset) - return; // Not yet fully initialized +void GpioUnaryTransformer::update() +{ + auto p = inPin->get(); + if (p == GpioVirtPin::PinState::Unset) + return; // Not yet fully initialized - set(p); + set(p); } /** * Update the output pin based on the current state of the input pin. */ -void GpioNotTransformer::update() { - auto p = inPin->get(); - if (p == GpioVirtPin::PinState::Unset) - return; // Not yet fully initialized +void GpioNotTransformer::update() +{ + auto p = inPin->get(); + if (p == GpioVirtPin::PinState::Unset) + return; // Not yet fully initialized - set(!p); + set(!p); } GpioBinaryTransformer::GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation) - : GpioTransformer(outPin), inPin1(inPin1), inPin2(inPin2), operation(operation) { - assert(!inPin1->dependentPin); // We only allow one dependent pin - inPin1->dependentPin = this; - assert(!inPin2->dependentPin); // We only allow one dependent pin - inPin2->dependentPin = this; + : GpioTransformer(outPin), inPin1(inPin1), inPin2(inPin2), operation(operation) +{ + assert(!inPin1->dependentPin); // We only allow one dependent pin + inPin1->dependentPin = this; + assert(!inPin2->dependentPin); // We only allow one dependent pin + inPin2->dependentPin = this; - // Don't update at construction time, because various GpioPins might be global constructor based not yet initiated - // because order of operations for global constructors is not defined. update(); + // Don't update at construction time, because various GpioPins might be global constructor based not yet initiated because + // order of operations for global constructors is not defined. + // update(); } -void GpioBinaryTransformer::update() { - auto p1 = inPin1->get(), p2 = inPin2->get(); - GpioVirtPin::PinState newValue = GpioVirtPin::PinState::Unset; +void GpioBinaryTransformer::update() +{ + auto p1 = inPin1->get(), p2 = inPin2->get(); + GpioVirtPin::PinState newValue = GpioVirtPin::PinState::Unset; - if (p1 == GpioVirtPin::PinState::Unset) - newValue = p2; // Not yet fully initialized - else if (p2 == GpioVirtPin::PinState::Unset) - newValue = p1; // Not yet fully initialized + if (p1 == GpioVirtPin::PinState::Unset) + newValue = p2; // Not yet fully initialized + else if (p2 == GpioVirtPin::PinState::Unset) + newValue = p1; // Not yet fully initialized - // If we've already found our value just use it, otherwise need to do the operation - if (newValue == GpioVirtPin::PinState::Unset) { - switch (operation) { - case And: - newValue = (GpioVirtPin::PinState)(p1 && p2); - break; - case Or: - newValue = (GpioVirtPin::PinState)(p1 || p2); - break; - case Xor: - newValue = (GpioVirtPin::PinState)(p1 != p2); - break; - default: - assert(false); + // If we've already found our value just use it, otherwise need to do the operation + if (newValue == GpioVirtPin::PinState::Unset) { + switch (operation) { + case And: + newValue = (GpioVirtPin::PinState)(p1 && p2); + break; + case Or: + newValue = (GpioVirtPin::PinState)(p1 || p2); + break; + case Xor: + newValue = (GpioVirtPin::PinState)(p1 != p2); + break; + default: + assert(false); + } } - } - set(newValue); + set(newValue); } GpioSplitter::GpioSplitter(GpioPin *outPin1, GpioPin *outPin2) : outPin1(outPin1), outPin2(outPin2) {} diff --git a/src/GpioLogic.h b/src/GpioLogic.h index 096721ee8..947d49625 100644 --- a/src/GpioLogic.h +++ b/src/GpioLogic.h @@ -3,9 +3,8 @@ #include "configuration.h" /**This is a set of classes to mediate access to GPIOs in a structured way. Most usage of GPIOs do not - require these classes! But if your hardware has a GPIO that is 'shared' between multiple devices (i.e. a shared - power enable) then using these classes might be able to let you cleanly turn on that enable when either dependent - device is needed. + require these classes! But if your hardware has a GPIO that is 'shared' between multiple devices (i.e. a shared power enable) + then using these classes might be able to let you cleanly turn on that enable when either dependent device is needed. Note: these classes are intended to be 99% inline for the common case so should have minimal impact on flash or RAM requirements. @@ -14,21 +13,23 @@ /** * A logical GPIO pin (not necessary raw hardware). */ -class GpioPin { -public: - virtual void set(bool value) = 0; +class GpioPin +{ + public: + virtual void set(bool value) = 0; }; /** * A physical GPIO hw pin. */ -class GpioHwPin : public GpioPin { - uint32_t num; +class GpioHwPin : public GpioPin +{ + uint32_t num; -public: - explicit GpioHwPin(uint32_t num) : num(num) {} + public: + explicit GpioHwPin(uint32_t num) : num(num) {} - void set(bool value); + void set(bool value); }; class GpioTransformer; @@ -38,115 +39,122 @@ class GpioBinaryTransformer; /** * A virtual GPIO pin. */ -class GpioVirtPin : public GpioPin { - friend class GpioBinaryTransformer; - friend class GpioUnaryTransformer; +class GpioVirtPin : public GpioPin +{ + friend class GpioBinaryTransformer; + friend class GpioUnaryTransformer; -public: - enum PinState { On = true, Off = false, Unset = 2 }; + public: + enum PinState { On = true, Off = false, Unset = 2 }; - void set(bool value); - PinState get() const { return value; } + void set(bool value); + PinState get() const { return value; } -private: - PinState value = PinState::Unset; - GpioTransformer *dependentPin = NULL; + private: + PinState value = PinState::Unset; + GpioTransformer *dependentPin = NULL; }; #include /** - * A 'smart' trigger that can depend in a fake GPIO and if that GPIO changes, drive some other downstream GPIO to - * change. notably: the set method is not public (because it always is calculated by a subclass) + * A 'smart' trigger that can depend in a fake GPIO and if that GPIO changes, drive some other downstream GPIO to change. + * notably: the set method is not public (because it always is calculated by a subclass) */ -class GpioTransformer { -public: - /** - * Update the output pin based on the current state of the input pin. - */ - virtual void update() = 0; +class GpioTransformer +{ + public: + /** + * Update the output pin based on the current state of the input pin. + */ + virtual void update() = 0; -protected: - GpioTransformer(GpioPin *outPin); + protected: + GpioTransformer(GpioPin *outPin); - void set(bool value); + void set(bool value); -private: - GpioPin *outPin; + private: + GpioPin *outPin; }; /** * A transformer that just drives a hw pin based on a virtual pin. */ -class GpioUnaryTransformer : public GpioTransformer { -public: - GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin); +class GpioUnaryTransformer : public GpioTransformer +{ + public: + GpioUnaryTransformer(GpioVirtPin *inPin, GpioPin *outPin); -protected: - friend class GpioVirtPin; + protected: + friend class GpioVirtPin; - /** - * Update the output pin based on the current state of the input pin. - */ - virtual void update(); + /** + * Update the output pin based on the current state of the input pin. + */ + virtual void update(); - GpioVirtPin *inPin; + GpioVirtPin *inPin; }; /** * A transformer that performs a unary NOT operation from an input. */ -class GpioNotTransformer : public GpioUnaryTransformer { -public: - GpioNotTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioUnaryTransformer(inPin, outPin) {} +class GpioNotTransformer : public GpioUnaryTransformer +{ + public: + GpioNotTransformer(GpioVirtPin *inPin, GpioPin *outPin) : GpioUnaryTransformer(inPin, outPin) {} -protected: - friend class GpioVirtPin; + protected: + friend class GpioVirtPin; - /** - * Update the output pin based on the current state of the input pin. - */ - void update(); + /** + * Update the output pin based on the current state of the input pin. + */ + void update(); }; /** * A transformer that combines multiple virtual pins to drive an output pin */ -class GpioBinaryTransformer : public GpioTransformer { +class GpioBinaryTransformer : public GpioTransformer +{ -public: - enum Operation { And, Or, Xor }; + public: + enum Operation { And, Or, Xor }; - GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation); + GpioBinaryTransformer(GpioVirtPin *inPin1, GpioVirtPin *inPin2, GpioPin *outPin, Operation operation); -protected: - friend class GpioVirtPin; + protected: + friend class GpioVirtPin; - /** - * Update the output pin based on the current state of the input pins. - */ - void update(); + /** + * Update the output pin based on the current state of the input pins. + */ + void update(); -private: - GpioVirtPin *inPin1; - GpioVirtPin *inPin2; - Operation operation; + private: + GpioVirtPin *inPin1; + GpioVirtPin *inPin2; + Operation operation; }; /** * Sometimes a single output GPIO single needs to drive multiple physical GPIOs. This class provides that. */ -class GpioSplitter : public GpioPin { +class GpioSplitter : public GpioPin +{ -public: - GpioSplitter(GpioPin *outPin1, GpioPin *outPin2); + public: + GpioSplitter(GpioPin *outPin1, GpioPin *outPin2); - void set(bool value) { - outPin1->set(value); - outPin2->set(value); - } + void set(bool value) + { + outPin1->set(value); + outPin2->set(value); + } -private: - GpioPin *outPin1; - GpioPin *outPin2; + private: + GpioPin *outPin1; + GpioPin *outPin2; }; \ No newline at end of file diff --git a/src/Led.cpp b/src/Led.cpp index ecd0058b6..6406cd2f7 100644 --- a/src/Led.cpp +++ b/src/Led.cpp @@ -23,14 +23,16 @@ static GpioPin &ledHwPin = ledRawHwPin; /** * A GPIO controlled by the PMU */ -class GpioPmuPin : public GpioPin { -public: - void set(bool value) { - if (pmu_found && PMU) { - // blink the axp led - PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); +class GpioPmuPin : public GpioPin +{ + public: + void set(bool value) + { + if (pmu_found && PMU) { + // blink the axp led + PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); + } } - } } ledPmuHwPin; // In some cases we need to drive a PMU LED and a normal LED @@ -43,17 +45,19 @@ static GpioPin &ledFinalPin = ledHwPin; /** * We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff. */ -class MonitoredLedPin : public GpioPin { -public: - void set(bool value) { - if (powerMon) { - if (value) - powerMon->setState(meshtastic_PowerMon_State_LED_On); - else - powerMon->clearState(meshtastic_PowerMon_State_LED_On); +class MonitoredLedPin : public GpioPin +{ + public: + void set(bool value) + { + if (powerMon) { + if (value) + powerMon->setState(meshtastic_PowerMon_State_LED_On); + else + powerMon->clearState(meshtastic_PowerMon_State_LED_On); + } + ledFinalPin.set(value); } - ledFinalPin.set(value); - } } monitoredLedPin; #else static GpioPin &monitoredLedPin = ledFinalPin; diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index f6b5eece5..c96645b1c 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -18,238 +18,257 @@ static char *g_messagePool = nullptr; static size_t g_poolWritePos = 0; // Reset pool (called on boot or clear) -static inline void resetMessagePool() { - if (!g_messagePool) { - g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); +static inline void resetMessagePool() +{ if (!g_messagePool) { - LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); - return; + g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); + if (!g_messagePool) { + LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); + return; + } } - } - g_poolWritePos = 0; - memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); + g_poolWritePos = 0; + memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); } // Allocate text in pool and return offset // If not enough space remains, wrap around (ring buffer style) -static inline uint16_t storeTextInPool(const char *src, size_t len) { - if (len >= MAX_MESSAGE_SIZE) - len = MAX_MESSAGE_SIZE - 1; +static inline uint16_t storeTextInPool(const char *src, size_t len) +{ + if (len >= MAX_MESSAGE_SIZE) + len = MAX_MESSAGE_SIZE - 1; - // Wrap pool if out of space - if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { - g_poolWritePos = 0; - } + // Wrap pool if out of space + if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { + g_poolWritePos = 0; + } - uint16_t offset = g_poolWritePos; - memcpy(&g_messagePool[g_poolWritePos], src, len); - g_messagePool[g_poolWritePos + len] = '\0'; - g_poolWritePos += (len + 1); - return offset; + uint16_t offset = g_poolWritePos; + memcpy(&g_messagePool[g_poolWritePos], src, len); + g_messagePool[g_poolWritePos + len] = '\0'; + g_poolWritePos += (len + 1); + return offset; } // Retrieve a const pointer to message text by offset -static inline const char *getTextFromPool(uint16_t offset) { - if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) - return ""; - return &g_messagePool[offset]; +static inline const char *getTextFromPool(uint16_t offset) +{ + if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) + return ""; + return &g_messagePool[offset]; } // Helper: assign a timestamp (RTC if available, else boot-relative) -static inline void assignTimestamp(StoredMessage &sm) { - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - if (nowSecs) { - sm.timestamp = nowSecs; - sm.isBootRelative = false; - } else { - sm.timestamp = millis() / 1000; - sm.isBootRelative = true; - } +static inline void assignTimestamp(StoredMessage &sm) +{ + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs) { + sm.timestamp = nowSecs; + sm.isBootRelative = false; + } else { + sm.timestamp = millis() / 1000; + sm.isBootRelative = true; + } } // Generic push with cap (used by live + persisted queues) -template static inline void pushWithLimit(std::deque &queue, const T &msg) { - if (queue.size() >= MAX_MESSAGES_SAVED) - queue.pop_front(); - queue.push_back(msg); +template static inline void pushWithLimit(std::deque &queue, const T &msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.push_back(msg); } -template static inline void pushWithLimit(std::deque &queue, T &&msg) { - if (queue.size() >= MAX_MESSAGES_SAVED) - queue.pop_front(); - queue.emplace_back(std::move(msg)); +template static inline void pushWithLimit(std::deque &queue, T &&msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.emplace_back(std::move(msg)); } -MessageStore::MessageStore(const std::string &label) { - filename = "/Messages_" + label + ".msgs"; - resetMessagePool(); // initialize text pool on boot +MessageStore::MessageStore(const std::string &label) +{ + filename = "/Messages_" + label + ".msgs"; + resetMessagePool(); // initialize text pool on boot } // Live message handling (RAM only) -void MessageStore::addLiveMessage(StoredMessage &&msg) { pushWithLimit(liveMessages, std::move(msg)); } -void MessageStore::addLiveMessage(const StoredMessage &msg) { pushWithLimit(liveMessages, msg); } +void MessageStore::addLiveMessage(StoredMessage &&msg) +{ + pushWithLimit(liveMessages, std::move(msg)); +} +void MessageStore::addLiveMessage(const StoredMessage &msg) +{ + pushWithLimit(liveMessages, msg); +} // Add from incoming/outgoing packet -const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { - StoredMessage sm; - assignTimestamp(sm); - sm.channelIndex = packet.channel; +const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) +{ + StoredMessage sm; + assignTimestamp(sm); + sm.channelIndex = packet.channel; - const char *payload = reinterpret_cast(packet.decoded.payload.bytes); - size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); - sm.textOffset = storeTextInPool(payload, len); - sm.textLength = len; + const char *payload = reinterpret_cast(packet.decoded.payload.bytes); + size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); + sm.textOffset = storeTextInPool(payload, len); + sm.textLength = len; - // Determine sender - uint32_t localNode = nodeDB->getNodeNum(); - sm.sender = (packet.from == 0) ? localNode : packet.from; + // Determine sender + uint32_t localNode = nodeDB->getNodeNum(); + sm.sender = (packet.from == 0) ? localNode : packet.from; - sm.dest = packet.to; + sm.dest = packet.to; - bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); + bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); - if (packet.from == 0) { - sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; - sm.ackStatus = AckStatus::NONE; - } else { - sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; - sm.ackStatus = AckStatus::ACKED; - } + if (packet.from == 0) { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::NONE; + } else { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::ACKED; + } - addLiveMessage(sm); - return liveMessages.back(); + addLiveMessage(sm); + return liveMessages.back(); } // Outgoing/manual message -void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) { - StoredMessage sm; +void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) +{ + StoredMessage sm; - // Always use our local time (helper handles RTC vs boot time) - assignTimestamp(sm); + // Always use our local time (helper handles RTC vs boot time) + assignTimestamp(sm); - sm.sender = sender; - sm.channelIndex = channelIndex; - sm.textOffset = storeTextInPool(text.c_str(), text.size()); - sm.textLength = text.size(); + sm.sender = sender; + sm.channelIndex = channelIndex; + sm.textOffset = storeTextInPool(text.c_str(), text.size()); + sm.textLength = text.size(); - // Use the provided destination - sm.dest = sender; - sm.type = MessageType::DM_TO_US; + // Use the provided destination + sm.dest = sender; + sm.type = MessageType::DM_TO_US; - // Outgoing messages always start with unknown ack status - sm.ackStatus = AckStatus::NONE; + // Outgoing messages always start with unknown ack status + sm.ackStatus = AckStatus::NONE; - addLiveMessage(sm); + addLiveMessage(sm); } #if ENABLE_MESSAGE_PERSISTENCE // Compact, fixed-size on-flash representation using offset + length struct __attribute__((packed)) StoredMessageRecord { - uint32_t timestamp; - uint32_t sender; - uint8_t channelIndex; - uint32_t dest; - uint8_t isBootRelative; - uint8_t ackStatus; // static_cast(AckStatus) - uint8_t type; // static_cast(MessageType) - uint16_t textLength; // message length - char text[MAX_MESSAGE_SIZE]; // store actual text here + uint32_t timestamp; + uint32_t sender; + uint8_t channelIndex; + uint32_t dest; + uint8_t isBootRelative; + uint8_t ackStatus; // static_cast(AckStatus) + uint8_t type; // static_cast(MessageType) + uint16_t textLength; // message length + char text[MAX_MESSAGE_SIZE]; // store actual text here }; // Serialize one StoredMessage to flash -static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) { - StoredMessageRecord rec = {}; - rec.timestamp = m.timestamp; - rec.sender = m.sender; - rec.channelIndex = m.channelIndex; - rec.dest = m.dest; - rec.isBootRelative = m.isBootRelative; - rec.ackStatus = static_cast(m.ackStatus); - rec.type = static_cast(m.type); - rec.textLength = m.textLength; +static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) +{ + StoredMessageRecord rec = {}; + rec.timestamp = m.timestamp; + rec.sender = m.sender; + rec.channelIndex = m.channelIndex; + rec.dest = m.dest; + rec.isBootRelative = m.isBootRelative; + rec.ackStatus = static_cast(m.ackStatus); + rec.type = static_cast(m.type); + rec.textLength = m.textLength; - // Copy the actual text into the record from RAM pool - const char *txt = getTextFromPool(m.textOffset); - strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); - rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; + // Copy the actual text into the record from RAM pool + const char *txt = getTextFromPool(m.textOffset); + strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); + rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; - f.write(reinterpret_cast(&rec), sizeof(rec)); + f.write(reinterpret_cast(&rec), sizeof(rec)); } // Deserialize one StoredMessage from flash; returns false on short read -static inline bool readMessageRecord(File &f, StoredMessage &m) { - StoredMessageRecord rec = {}; - if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) - return false; +static inline bool readMessageRecord(File &f, StoredMessage &m) +{ + StoredMessageRecord rec = {}; + if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) + return false; - m.timestamp = rec.timestamp; - m.sender = rec.sender; - m.channelIndex = rec.channelIndex; - m.dest = rec.dest; - m.isBootRelative = rec.isBootRelative; - m.ackStatus = static_cast(rec.ackStatus); - m.type = static_cast(rec.type); - m.textLength = rec.textLength; + m.timestamp = rec.timestamp; + m.sender = rec.sender; + m.channelIndex = rec.channelIndex; + m.dest = rec.dest; + m.isBootRelative = rec.isBootRelative; + m.ackStatus = static_cast(rec.ackStatus); + m.type = static_cast(rec.type); + m.textLength = rec.textLength; - // 💡 Re-store text into pool and update offset - m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); - m.textOffset = storeTextInPool(rec.text, m.textLength); + // 💡 Re-store text into pool and update offset + m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); + m.textOffset = storeTextInPool(rec.text, m.textLength); - return true; + return true; } -void MessageStore::saveToFlash() { +void MessageStore::saveToFlash() +{ #ifdef FSCom - // Ensure root exists - spiLock->lock(); - FSCom.mkdir("/"); - spiLock->unlock(); + // Ensure root exists + spiLock->lock(); + FSCom.mkdir("/"); + spiLock->unlock(); - SafeFile f(filename.c_str(), false); + SafeFile f(filename.c_str(), false); - spiLock->lock(); - uint8_t count = static_cast(liveMessages.size()); - if (count > MAX_MESSAGES_SAVED) - count = MAX_MESSAGES_SAVED; - f.write(&count, 1); + spiLock->lock(); + uint8_t count = static_cast(liveMessages.size()); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; + f.write(&count, 1); - for (uint8_t i = 0; i < count; ++i) { - writeMessageRecord(f, liveMessages[i]); - } - spiLock->unlock(); + for (uint8_t i = 0; i < count; ++i) { + writeMessageRecord(f, liveMessages[i]); + } + spiLock->unlock(); - f.close(); + f.close(); #endif } -void MessageStore::loadFromFlash() { - std::deque().swap(liveMessages); - resetMessagePool(); // reset pool when loading +void MessageStore::loadFromFlash() +{ + std::deque().swap(liveMessages); + resetMessagePool(); // reset pool when loading #ifdef FSCom - concurrency::LockGuard guard(spiLock); + concurrency::LockGuard guard(spiLock); - if (!FSCom.exists(filename.c_str())) - return; + if (!FSCom.exists(filename.c_str())) + return; - auto f = FSCom.open(filename.c_str(), FILE_O_READ); - if (!f) - return; + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + if (!f) + return; - uint8_t count = 0; - f.readBytes(reinterpret_cast(&count), 1); - if (count > MAX_MESSAGES_SAVED) - count = MAX_MESSAGES_SAVED; + uint8_t count = 0; + f.readBytes(reinterpret_cast(&count), 1); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; - for (uint8_t i = 0; i < count; ++i) { - StoredMessage m; - if (!readMessageRecord(f, m)) - break; - liveMessages.push_back(m); - } + for (uint8_t i = 0; i < count; ++i) { + StoredMessage m; + if (!readMessageRecord(f, m)) + break; + liveMessages.push_back(m); + } - f.close(); + f.close(); #endif } @@ -260,134 +279,146 @@ void MessageStore::loadFromFlash() {} #endif // Clear all messages (RAM + persisted queue) -void MessageStore::clearAllMessages() { - std::deque().swap(liveMessages); - resetMessagePool(); +void MessageStore::clearAllMessages() +{ + std::deque().swap(liveMessages); + resetMessagePool(); #ifdef FSCom - SafeFile f(filename.c_str(), false); - uint8_t count = 0; - f.write(&count, 1); // write "0 messages" - f.close(); + SafeFile f(filename.c_str(), false); + uint8_t count = 0; + f.write(&count, 1); // write "0 messages" + f.close(); #endif } // Internal helper: erase first or last message matching a predicate -template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) { - if (fromBack) { - // Iterate from the back and erase all matches from the end - for (auto it = deque.rbegin(); it != deque.rend();) { - if (pred(*it)) { - it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); - } else { - ++it; - } +template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) +{ + if (fromBack) { + // Iterate from the back and erase all matches from the end + for (auto it = deque.rbegin(); it != deque.rend();) { + if (pred(*it)) { + it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); + } else { + ++it; + } + } + } else { + // Manual forward search to erase all matches + for (auto it = deque.begin(); it != deque.end();) { + if (pred(*it)) { + it = deque.erase(it); + } else { + ++it; + } + } } - } else { - // Manual forward search to erase all matches - for (auto it = deque.begin(); it != deque.end();) { - if (pred(*it)) { - it = deque.erase(it); - } else { - ++it; - } - } - } } // Delete oldest message (RAM + persisted queue) -void MessageStore::deleteOldestMessage() { - eraseIf(liveMessages, [](StoredMessage &) { return true; }); - saveToFlash(); +void MessageStore::deleteOldestMessage() +{ + eraseIf(liveMessages, [](StoredMessage &) { return true; }); + saveToFlash(); } // Delete oldest message in a specific channel -void MessageStore::deleteOldestMessageInChannel(uint8_t channel) { - auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; - eraseIf(liveMessages, pred); - saveToFlash(); +void MessageStore::deleteOldestMessageInChannel(uint8_t channel) +{ + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred); + saveToFlash(); } -void MessageStore::deleteAllMessagesInChannel(uint8_t channel) { - auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; - eraseIf(liveMessages, pred, false /* delete ALL, not just first */); - saveToFlash(); +void MessageStore::deleteAllMessagesInChannel(uint8_t channel) +{ + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred, false /* delete ALL, not just first */); + saveToFlash(); } -void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) { - uint32_t local = nodeDB->getNodeNum(); - auto pred = [&](const StoredMessage &m) { - if (m.type != MessageType::DM_TO_US) - return false; - uint32_t other = (m.sender == local) ? m.dest : m.sender; - return other == peer; - }; - eraseIf(liveMessages, pred, false); - saveToFlash(); +void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) +{ + uint32_t local = nodeDB->getNodeNum(); + auto pred = [&](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == local) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred, false); + saveToFlash(); } // Delete oldest message in a direct chat with a node -void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) { - auto pred = [peer](const StoredMessage &m) { - if (m.type != MessageType::DM_TO_US) - return false; - uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; - return other == peer; - }; - eraseIf(liveMessages, pred); - saveToFlash(); +void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) +{ + auto pred = [peer](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred); + saveToFlash(); } -std::deque MessageStore::getChannelMessages(uint8_t channel) const { - std::deque result; - for (const auto &m : liveMessages) { - if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { - result.push_back(m); +std::deque MessageStore::getChannelMessages(uint8_t channel) const +{ + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { + result.push_back(m); + } } - } - return result; + return result; } -std::deque MessageStore::getDirectMessages() const { - std::deque result; - for (const auto &m : liveMessages) { - if (m.type == MessageType::DM_TO_US) { - result.push_back(m); +std::deque MessageStore::getDirectMessages() const +{ + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::DM_TO_US) { + result.push_back(m); + } } - } - return result; + return result; } // Upgrade boot-relative timestamps once RTC is valid // Only same-boot boot-relative messages are healed. // Persisted boot-relative messages from old boots stay ??? forever. -void MessageStore::upgradeBootRelativeTimestamps() { - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - if (nowSecs == 0) - return; // Still no valid RTC +void MessageStore::upgradeBootRelativeTimestamps() +{ + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs == 0) + return; // Still no valid RTC - uint32_t bootNow = millis() / 1000; + uint32_t bootNow = millis() / 1000; - auto fix = [&](std::deque &dq) { - for (auto &m : dq) { - if (m.isBootRelative && m.timestamp <= bootNow) { - uint32_t bootOffset = nowSecs - bootNow; - m.timestamp += bootOffset; - m.isBootRelative = false; - } - } - }; - fix(liveMessages); + auto fix = [&](std::deque &dq) { + for (auto &m : dq) { + if (m.isBootRelative && m.timestamp <= bootNow) { + uint32_t bootOffset = nowSecs - bootNow; + m.timestamp += bootOffset; + m.isBootRelative = false; + } + } + }; + fix(liveMessages); } -const char *MessageStore::getText(const StoredMessage &msg) { - // Wrapper around the internal helper - return getTextFromPool(msg.textOffset); +const char *MessageStore::getText(const StoredMessage &msg) +{ + // Wrapper around the internal helper + return getTextFromPool(msg.textOffset); } -uint16_t MessageStore::storeText(const char *src, size_t len) { - // Wrapper around the internal helper - return storeTextInPool(src, len); +uint16_t MessageStore::storeText(const char *src, size_t len) +{ + // Wrapper around the internal helper + return storeTextInPool(src, len); } // Global definition diff --git a/src/MessageStore.h b/src/MessageStore.h index 362f0a9e9..41eb56b66 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -39,87 +39,90 @@ // Explicit message classification enum class MessageType : uint8_t { - BROADCAST = 0, // broadcast message - DM_TO_US = 1 // direct message addressed to this node + BROADCAST = 0, // broadcast message + DM_TO_US = 1 // direct message addressed to this node }; // Delivery status for messages we sent enum class AckStatus : uint8_t { - NONE = 0, // just sent, waiting (no symbol shown) - ACKED = 1, // got a valid ACK from destination - NACKED = 2, // explicitly failed - TIMEOUT = 3, // no ACK after retry window - RELAYED = 4 // got an ACK from relay, not destination + NONE = 0, // just sent, waiting (no symbol shown) + ACKED = 1, // got a valid ACK from destination + NACKED = 2, // explicitly failed + TIMEOUT = 3, // no ACK after retry window + RELAYED = 4 // got an ACK from relay, not destination }; struct StoredMessage { - uint32_t timestamp; // When message was created (secs since boot or RTC) - uint32_t sender; // NodeNum of sender - uint8_t channelIndex; // Channel index used - uint32_t dest; // Destination node (broadcast or direct) - MessageType type; // Derived from dest (explicit classification) - bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute - AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) + uint32_t timestamp; // When message was created (secs since boot or RTC) + uint32_t sender; // NodeNum of sender + uint8_t channelIndex; // Channel index used + uint32_t dest; // Destination node (broadcast or direct) + MessageType type; // Derived from dest (explicit classification) + bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute + AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) - // Text storage metadata — rebuilt from flash at boot - uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) - uint16_t textLength; // Length of text in bytes + // Text storage metadata — rebuilt from flash at boot + uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) + uint16_t textLength; // Length of text in bytes - // Default constructor initializes all fields safely - StoredMessage() - : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), ackStatus(AckStatus::NONE), - textOffset(0), textLength(0) {} + // Default constructor initializes all fields safely + StoredMessage() + : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), + ackStatus(AckStatus::NONE), textOffset(0), textLength(0) + { + } }; -class MessageStore { -public: - explicit MessageStore(const std::string &label); +class MessageStore +{ + public: + explicit MessageStore(const std::string &label); - // Live RAM methods (always current, used by UI and runtime) - void addLiveMessage(StoredMessage &&msg); - void addLiveMessage(const StoredMessage &msg); // convenience overload - const std::deque &getLiveMessages() const { return liveMessages; } + // Live RAM methods (always current, used by UI and runtime) + void addLiveMessage(StoredMessage &&msg); + void addLiveMessage(const StoredMessage &msg); // convenience overload + const std::deque &getLiveMessages() const { return liveMessages; } - // Add new messages from packets or manual input - const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only - void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add + // Add new messages from packets or manual input + const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only + void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add - // Persistence methods (used only on boot/shutdown) - void saveToFlash(); // Save messages to flash - void loadFromFlash(); // Load messages from flash + // Persistence methods (used only on boot/shutdown) + void saveToFlash(); // Save messages to flash + void loadFromFlash(); // Load messages from flash - // Clear all messages (RAM + persisted queue + text pool) - void clearAllMessages(); + // Clear all messages (RAM + persisted queue + text pool) + void clearAllMessages(); - // Delete helpers - void deleteOldestMessage(); // remove oldest from RAM (and flash on save) - void deleteOldestMessageInChannel(uint8_t channel); - void deleteOldestMessageWithPeer(uint32_t peer); - void deleteAllMessagesInChannel(uint8_t channel); - void deleteAllMessagesWithPeer(uint32_t peer); + // Delete helpers + void deleteOldestMessage(); // remove oldest from RAM (and flash on save) + void deleteOldestMessageInChannel(uint8_t channel); + void deleteOldestMessageWithPeer(uint32_t peer); + void deleteAllMessagesInChannel(uint8_t channel); + void deleteAllMessagesWithPeer(uint32_t peer); - // Unified accessor (for UI code, defaults to RAM buffer) - const std::deque &getMessages() const { return liveMessages; } + // Unified accessor (for UI code, defaults to RAM buffer) + const std::deque &getMessages() const { return liveMessages; } - // Helper filters for future use - std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel - std::deque getDirectMessages() const; // Only direct messages + // Helper filters for future use + std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel + std::deque getDirectMessages() const; // Only direct messages - // Upgrade boot-relative timestamps once RTC is valid - void upgradeBootRelativeTimestamps(); + // Upgrade boot-relative timestamps once RTC is valid + void upgradeBootRelativeTimestamps(); - // Retrieve the C-string text for a stored message - static const char *getText(const StoredMessage &msg); + // Retrieve the C-string text for a stored message + static const char *getText(const StoredMessage &msg); - // Allocate text into pool (used by sender-side code) - static uint16_t storeText(const char *src, size_t len); + // Allocate text into pool (used by sender-side code) + static uint16_t storeText(const char *src, size_t len); - // Used when loading from flash to rebuild the text pool - static uint16_t rebuildTextFromFlash(const char *src, size_t len); + // Used when loading from flash to rebuild the text pool + static uint16_t rebuildTextFromFlash(const char *src, size_t len); -private: - std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) - std::string filename; // Flash filename for persistence + private: + std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) + std::string filename; // Flash filename for persistence }; // Global instance (defined in MessageStore.cpp) diff --git a/src/NodeStatus.h b/src/NodeStatus.h index e75bc20c9..550f6254a 100644 --- a/src/NodeStatus.h +++ b/src/NodeStatus.h @@ -3,56 +3,64 @@ #include "configuration.h" #include -namespace meshtastic { +namespace meshtastic +{ /// Describes the state of the NodeDB system. -class NodeStatus : public Status { +class NodeStatus : public Status +{ -private: - CallbackObserver statusObserver = CallbackObserver(this, &NodeStatus::updateStatus); + private: + CallbackObserver statusObserver = + CallbackObserver(this, &NodeStatus::updateStatus); - uint16_t numOnline = 0; - uint16_t numTotal = 0; + uint16_t numOnline = 0; + uint16_t numTotal = 0; - uint16_t lastNumTotal = 0; + uint16_t lastNumTotal = 0; -public: - bool forceUpdate = false; + public: + bool forceUpdate = false; - NodeStatus() { statusType = STATUS_TYPE_NODE; } - NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status() { - this->forceUpdate = forceUpdate; - this->numOnline = numOnline; - this->numTotal = numTotal; - } - NodeStatus(const NodeStatus &); - NodeStatus &operator=(const NodeStatus &); - - void observe(Observable *source) { statusObserver.observe(source); } - - uint16_t getNumOnline() const { return numOnline; } - - uint16_t getNumTotal() const { return numTotal; } - - uint16_t getLastNumTotal() const { return lastNumTotal; } - - bool matches(const NodeStatus *newStatus) const { return (newStatus->getNumOnline() != numOnline || newStatus->getNumTotal() != numTotal); } - int updateStatus(const NodeStatus *newStatus) { - // Only update the status if values have actually changed - lastNumTotal = numTotal; - bool isDirty; + NodeStatus() { statusType = STATUS_TYPE_NODE; } + NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status() { - isDirty = matches(newStatus); - initialized = true; - numOnline = newStatus->getNumOnline(); - numTotal = newStatus->getNumTotal(); + this->forceUpdate = forceUpdate; + this->numOnline = numOnline; + this->numTotal = numTotal; } - if (isDirty || newStatus->forceUpdate) { - LOG_DEBUG("Node status update: %u online, %u total", numOnline, numTotal); - onNewStatus.notifyObservers(this); + NodeStatus(const NodeStatus &); + NodeStatus &operator=(const NodeStatus &); + + void observe(Observable *source) { statusObserver.observe(source); } + + uint16_t getNumOnline() const { return numOnline; } + + uint16_t getNumTotal() const { return numTotal; } + + uint16_t getLastNumTotal() const { return lastNumTotal; } + + bool matches(const NodeStatus *newStatus) const + { + return (newStatus->getNumOnline() != numOnline || newStatus->getNumTotal() != numTotal); + } + int updateStatus(const NodeStatus *newStatus) + { + // Only update the status if values have actually changed + lastNumTotal = numTotal; + bool isDirty; + { + isDirty = matches(newStatus); + initialized = true; + numOnline = newStatus->getNumOnline(); + numTotal = newStatus->getNumTotal(); + } + if (isDirty || newStatus->forceUpdate) { + LOG_DEBUG("Node status update: %u online, %u total", numOnline, numTotal); + onNewStatus.notifyObservers(this); + } + return 0; } - return 0; - } }; } // namespace meshtastic diff --git a/src/Observer.h b/src/Observer.h index 53466905b..6e1ec44c8 100644 --- a/src/Observer.h +++ b/src/Observer.h @@ -8,90 +8,99 @@ template class Observable; /** * An observer which can be mixed in as a baseclass. Implement onNotify as a method in your class. */ -template class Observer { - std::list *> observables; +template class Observer +{ + std::list *> observables; -public: - virtual ~Observer(); + public: + virtual ~Observer(); - /// Stop watching the observable - void unobserve(Observable *o); + /// Stop watching the observable + void unobserve(Observable *o); - /// Start watching a specified observable - void observe(Observable *o); + /// Start watching a specified observable + void observe(Observable *o); -private: - friend class Observable; + private: + friend class Observable; -protected: - /** - * returns 0 if other observers should continue to be called - * returns !0 if the observe calls should be aborted and this result code returned for notifyObservers - **/ - virtual int onNotify(T arg) = 0; + protected: + /** + * returns 0 if other observers should continue to be called + * returns !0 if the observe calls should be aborted and this result code returned for notifyObservers + **/ + virtual int onNotify(T arg) = 0; }; /** * An observer that calls an arbitrary method */ -template class CallbackObserver : public Observer { - typedef int (Callback::*ObserverCallback)(T arg); +template class CallbackObserver : public Observer +{ + typedef int (Callback::*ObserverCallback)(T arg); - Callback *objPtr; - ObserverCallback method; + Callback *objPtr; + ObserverCallback method; -public: - CallbackObserver(Callback *_objPtr, ObserverCallback _method) : objPtr(_objPtr), method(_method) {} + public: + CallbackObserver(Callback *_objPtr, ObserverCallback _method) : objPtr(_objPtr), method(_method) {} -protected: - virtual int onNotify(T arg) override { return (objPtr->*method)(arg); } + protected: + virtual int onNotify(T arg) override { return (objPtr->*method)(arg); } }; /** - * An observable class that will notify observers anytime notifyObservers is called. Argument type T can be any type, - * but for performance reasons a pointer or word sized object is recommended. + * An observable class that will notify observers anytime notifyObservers is called. Argument type T can be any type, but for + * performance reasons a pointer or word sized object is recommended. */ -template class Observable { - std::list *> observers; +template class Observable +{ + std::list *> observers; -public: - /** - * Tell all observers about a change, observers can process arg as they wish - * - * returns !0 if an observer chose to abort processing by returning this code - */ - int notifyObservers(T arg) { - for (typename std::list *>::const_iterator iterator = observers.begin(); iterator != observers.end(); ++iterator) { - int result = (*iterator)->onNotify(arg); - if (result != 0) - return result; + public: + /** + * Tell all observers about a change, observers can process arg as they wish + * + * returns !0 if an observer chose to abort processing by returning this code + */ + int notifyObservers(T arg) + { + for (typename std::list *>::const_iterator iterator = observers.begin(); iterator != observers.end(); + ++iterator) { + int result = (*iterator)->onNotify(arg); + if (result != 0) + return result; + } + + return 0; } - return 0; - } + private: + friend class Observer; -private: - friend class Observer; + // Not called directly, instead call observer.observe + void addObserver(Observer *o) { observers.push_back(o); } - // Not called directly, instead call observer.observe - void addObserver(Observer *o) { observers.push_back(o); } - - void removeObserver(Observer *o) { observers.remove(o); } + void removeObserver(Observer *o) { observers.remove(o); } }; -template Observer::~Observer() { - for (typename std::list *>::const_iterator iterator = observables.begin(); iterator != observables.end(); ++iterator) { - (*iterator)->removeObserver(this); - } - observables.clear(); +template Observer::~Observer() +{ + for (typename std::list *>::const_iterator iterator = observables.begin(); iterator != observables.end(); + ++iterator) { + (*iterator)->removeObserver(this); + } + observables.clear(); } -template void Observer::unobserve(Observable *o) { - o->removeObserver(this); - observables.remove(o); +template void Observer::unobserve(Observable *o) +{ + o->removeObserver(this); + observables.remove(o); } -template void Observer::observe(Observable *o) { - observables.push_back(o); - o->addObserver(this); +template void Observer::observe(Observable *o) +{ + observables.push_back(o); + o->addObserver(this); } \ No newline at end of file diff --git a/src/Power.cpp b/src/Power.cpp index 51fd1e8b3..33dda8e11 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1,11 +1,11 @@ /** * @file Power.cpp - * @brief This file contains the implementation of the Power class, which is responsible for managing power-related - * functionality of the device. It includes battery level sensing, power management unit (PMU) control, and power state - * machine management. The Power class is used by the main device class to manage power-related functionality. + * @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality + * of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The + * Power class is used by the main device class to manage power-related functionality. * - * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which - * assumes the battery voltage is attached via a voltage-divider to an analog input. + * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes + * the battery voltage is attached via a voltage-divider to an analog input. * * This file is part of the Meshtastic project. * For more information, see: https://meshtastic.org/ @@ -142,25 +142,26 @@ XPowersLibInterface *PMU = NULL; // Copy of the base class defined in axp20x.h. // I'd rather not include axp20x.h as it brings Wire dependency. -class HasBatteryLevel { -public: - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() { return -1; } +class HasBatteryLevel +{ + public: + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() { return -1; } - /** - * The raw voltage of the battery or NAN if unknown - */ - virtual uint16_t getBattVoltage() { return 0; } + /** + * The raw voltage of the battery or NAN if unknown + */ + virtual uint16_t getBattVoltage() { return 0; } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() { return false; } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() { return false; } - virtual bool isVbusIn() { return false; } - virtual bool isCharging() { return false; } + virtual bool isVbusIn() { return false; } + virtual bool isCharging() { return false; } }; #endif @@ -194,34 +195,36 @@ static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level se #ifdef BATTERY_PIN -void battery_adcEnable() { +void battery_adcEnable() +{ #ifdef ADC_CTRL // enable adc voltage divider when we need to read #ifdef ADC_USE_PULLUP - pinMode(ADC_CTRL, INPUT_PULLUP); + pinMode(ADC_CTRL, INPUT_PULLUP); #else #ifdef HELTEC_V3 - pinMode(ADC_CTRL, INPUT); - uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL)); - pinMode(ADC_CTRL, OUTPUT); - digitalWrite(ADC_CTRL, adc_ctl_enable_value); + pinMode(ADC_CTRL, INPUT); + uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL)); + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, adc_ctl_enable_value); #else - pinMode(ADC_CTRL, OUTPUT); - digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED); + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED); #endif #endif - delay(10); + delay(10); #endif } -static void battery_adcDisable() { +static void battery_adcDisable() +{ #ifdef ADC_CTRL // disable adc voltage divider when we need to read #ifdef ADC_USE_PULLUP - pinMode(ADC_CTRL, INPUT_PULLDOWN); + pinMode(ADC_CTRL, INPUT_PULLDOWN); #else #ifdef HELTEC_V3 - pinMode(ADC_CTRL, ANALOG); + pinMode(ADC_CTRL, ANALOG); #else - digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); + digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); #endif #endif #endif @@ -232,70 +235,74 @@ static void battery_adcDisable() { /** * A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input */ -class AnalogBatteryLevel : public HasBatteryLevel { -public: - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override { +class AnalogBatteryLevel : public HasBatteryLevel +{ + public: + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override + { #if defined(HAS_RAKPROT) && !defined(HAS_PMU) - if (hasRAK()) { - return rak9154Sensor.getBusBatteryPercent(); - } + if (hasRAK()) { + return rak9154Sensor.getBusBatteryPercent(); + } #endif - float v = getBattVoltage(); + float v = getBattVoltage(); - if (v < noBatVolt) - return -1; // If voltage is super low assume no battery installed + if (v < noBatVolt) + return -1; // If voltage is super low assume no battery installed #ifdef NO_BATTERY_LEVEL_ON_CHARGE - // This does not work on a RAK4631 with battery connected - if (v > chargingVolt) - return 0; // While charging we can't report % full on the battery + // This does not work on a RAK4631 with battery connected + if (v > chargingVolt) + return 0; // While charging we can't report % full on the battery #endif - /** - * @brief Battery voltage lookup table interpolation to obtain a more - * precise percentage rather than the old proportional one. - * @author Gabriele Russo - * @date 06/02/2024 - */ - float battery_SOC = 0.0; - uint16_t voltage = v / NUM_CELLS; // single cell voltage (average) - for (int i = 0; i < NUM_OCV_POINTS; i++) { - if (OCV[i] <= voltage) { - if (i == 0) { - battery_SOC = 100.0; // 100% full - } else { - // interpolate between OCV[i] and OCV[i-1] - battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) * (NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i])); + /** + * @brief Battery voltage lookup table interpolation to obtain a more + * precise percentage rather than the old proportional one. + * @author Gabriele Russo + * @date 06/02/2024 + */ + float battery_SOC = 0.0; + uint16_t voltage = v / NUM_CELLS; // single cell voltage (average) + for (int i = 0; i < NUM_OCV_POINTS; i++) { + if (OCV[i] <= voltage) { + if (i == 0) { + battery_SOC = 100.0; // 100% full + } else { + // interpolate between OCV[i] and OCV[i-1] + battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) * + (NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i])); + } + break; + } } - break; - } - } #if defined(BATTERY_CHARGING_INV) - // bit of trickery to show 99% up until the charge finishes - if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99) - battery_SOC = 99; + // bit of trickery to show 99% up until the charge finishes + if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99) + battery_SOC = 99; #endif - return clamp((int)(battery_SOC), 0, 100); - } + return clamp((int)(battery_SOC), 0, 100); + } - /** - * The raw voltage of the batteryin millivolts or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { + /** + * The raw voltage of the batteryin millivolts or NAN if unknown + */ + virtual uint16_t getBattVoltage() override + { #if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (hasRAK()) { - return getRAKVoltage(); - } + if (hasRAK()) { + return getRAKVoltage(); + } #endif #if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (hasINA()) { - return getINAVoltage(); - } + if (hasINA()) { + return getINAVoltage(); + } #endif #ifndef ADC_MULTIPLIER @@ -303,294 +310,314 @@ public: #endif #ifndef BATTERY_SENSE_SAMPLES -#define BATTERY_SENSE_SAMPLES 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment. +#define BATTERY_SENSE_SAMPLES \ + 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment. #endif #ifdef BATTERY_PIN - // Override variant or default ADC_MULTIPLIER if we have the override pref - float operativeAdcMultiplier = config.power.adc_multiplier_override > 0 ? config.power.adc_multiplier_override : ADC_MULTIPLIER; - // Do not call analogRead() often. - const uint32_t min_read_interval = 5000; - if (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) { - last_read_time_ms = millis(); + // Override variant or default ADC_MULTIPLIER if we have the override pref + float operativeAdcMultiplier = + config.power.adc_multiplier_override > 0 ? config.power.adc_multiplier_override : ADC_MULTIPLIER; + // Do not call analogRead() often. + const uint32_t min_read_interval = 5000; + if (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) { + last_read_time_ms = millis(); - uint32_t raw = 0; - float scaled = 0; + uint32_t raw = 0; + float scaled = 0; - battery_adcEnable(); + battery_adcEnable(); #ifdef ARCH_ESP32 // ADC block for espressif platforms - raw = espAdcRead(); - scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); - scaled *= operativeAdcMultiplier; + raw = espAdcRead(); + scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); + scaled *= operativeAdcMultiplier; #else // block for all other platforms - for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - raw += analogRead(BATTERY_PIN); - } - raw = raw / BATTERY_SENSE_SAMPLES; - scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw; + for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + raw += analogRead(BATTERY_PIN); + } + raw = raw / BATTERY_SENSE_SAMPLES; + scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw; #endif - battery_adcDisable(); + battery_adcDisable(); - if (!initial_read_done) { - // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct - if (scaled > last_read_value) - last_read_value = scaled; - initial_read_done = true; - } else { - // Already initialized - filter this reading - last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF - } + if (!initial_read_done) { + // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct + if (scaled > last_read_value) + last_read_value = scaled; + initial_read_done = true; + } else { + // Already initialized - filter this reading + last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF + } - // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) - // (last_read_value)); - } - return last_read_value; + // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) + // (last_read_value)); + } + return last_read_value; #endif // BATTERY_PIN - return 0; - } + return 0; + } #if defined(ARCH_ESP32) && !defined(HAS_PMU) && defined(BATTERY_PIN) - /** - * ESP32 specific function for getting calibrated ADC reads - */ - uint32_t espAdcRead() { + /** + * ESP32 specific function for getting calibrated ADC reads + */ + uint32_t espAdcRead() + { - uint32_t raw = 0; - uint8_t raw_c = 0; // raw reading counter + uint32_t raw = 0; + uint8_t raw_c = 0; // raw reading counter #ifndef BAT_MEASURE_ADC_UNIT // ADC1 - for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - int val_ = adc1_get_raw(adc_channel); - if (val_ >= 0) { // save only valid readings - raw += val_; - raw_c++; - } - // delayMicroseconds(100); - } + for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + int val_ = adc1_get_raw(adc_channel); + if (val_ >= 0) { // save only valid readings + raw += val_; + raw_c++; + } + // delayMicroseconds(100); + } #else // ADC2 #ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 - // ADC2 wifi bug workaround not required, breaks compile - // On ESP32S3, ADC2 can take turns with Wifi (?) + // ADC2 wifi bug workaround not required, breaks compile + // On ESP32S3, ADC2 can take turns with Wifi (?) - int32_t adc_buf; - esp_err_t read_result; + int32_t adc_buf; + esp_err_t read_result; - // Multiple samples - for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - adc_buf = 0; - read_result = -1; + // Multiple samples + for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + adc_buf = 0; + read_result = -1; - read_result = adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); - if (read_result == ESP_OK) { - raw += adc_buf; - raw_c++; // Count valid samples - } else { - LOG_DEBUG("An attempt to sample ADC2 failed"); - } - } + read_result = adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); + if (read_result == ESP_OK) { + raw += adc_buf; + raw_c++; // Count valid samples + } else { + LOG_DEBUG("An attempt to sample ADC2 failed"); + } + } #else // Other ESP32 - int32_t adc_buf = 0; - for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { - // ADC2 wifi bug workaround, see - // https://github.com/espressif/arduino-esp32/issues/102 - WRITE_PERI_REG(SENS_SAR_READ_CTRL2_REG, RTC_reg_b); - SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV); - adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); - raw += adc_buf; - raw_c++; - } + int32_t adc_buf = 0; + for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) { + // ADC2 wifi bug workaround, see + // https://github.com/espressif/arduino-esp32/issues/102 + WRITE_PERI_REG(SENS_SAR_READ_CTRL2_REG, RTC_reg_b); + SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV); + adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf); + raw += adc_buf; + raw_c++; + } #endif // BAT_MEASURE_ADC_UNIT #endif // End BAT_MEASURE_ADC_UNIT - return (raw / (raw_c < 1 ? 1 : raw_c)); - } -#endif - - /** - * return true if there is a battery installed in this unit - */ - // if we have a integrated device with a battery, we can assume that the battery is always connected -#ifdef BATTERY_IMMUTABLE - virtual bool isBatteryConnect() override { return true; } -#elif defined(ADC_V) - virtual bool isBatteryConnect() override { - int lastReading = digitalRead(ADC_V); - // 判断值是否变化 - for (int i = 2; i < 500; i++) { - int reading = digitalRead(ADC_V); - if (reading != lastReading) { - return false; // 有变化,USB供电, 没接电池 - } + return (raw / (raw_c < 1 ? 1 : raw_c)); } - - return true; - } -#else - virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power - /// On some boards we don't have the power management chip (like AXPxxxx) - /// so we use EXT_PWR_DETECT GPIO pin to detect external power source - virtual bool isVbusIn() override { + /** + * return true if there is a battery installed in this unit + */ + // if we have a integrated device with a battery, we can assume that the battery is always connected +#ifdef BATTERY_IMMUTABLE + virtual bool isBatteryConnect() override { return true; } +#elif defined(ADC_V) + virtual bool isBatteryConnect() override + { + int lastReading = digitalRead(ADC_V); + // 判断值是否变化 + for (int i = 2; i < 500; i++) { + int reading = digitalRead(ADC_V); + if (reading != lastReading) { + return false; // 有变化,USB供电, 没接电池 + } + } + + return true; + } +#else + virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } +#endif + + /// If we see a battery voltage higher than physics allows - assume charger is pumping + /// in power + /// On some boards we don't have the power management chip (like AXPxxxx) + /// so we use EXT_PWR_DETECT GPIO pin to detect external power source + virtual bool isVbusIn() override + { #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - // if external powered that pin will be pulled down - if (digitalRead(EXT_PWR_DETECT) == LOW) { - return true; - } - // if it's not LOW - check the battery + // if external powered that pin will be pulled down + if (digitalRead(EXT_PWR_DETECT) == LOW) { + return true; + } + // if it's not LOW - check the battery #else - // if external powered that pin will be pulled up - if (digitalRead(EXT_PWR_DETECT) == HIGH) { - return true; - } - // if it's not HIGH - check the battery + // if external powered that pin will be pulled up + if (digitalRead(EXT_PWR_DETECT) == HIGH) { + return true; + } + // if it's not HIGH - check the battery #endif #elif defined(MUZI_BASE) - return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; + return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; #endif - return getBattVoltage() > chargingVolt; - } - - /// Assume charging if we have a battery and external power is connected. - /// we can't be smart enough to say 'full'? - virtual bool isCharging() override { -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) - if (hasRAK()) { - return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; + return getBattVoltage() > chargingVolt; } + + /// Assume charging if we have a battery and external power is connected. + /// we can't be smart enough to say 'full'? + virtual bool isCharging() override + { +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) + if (hasRAK()) { + return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; + } #endif #ifdef EXT_CHRG_DETECT - return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #elif defined(BATTERY_CHARGING_INV) - return !digitalRead(BATTERY_CHARGING_INV); + return !digitalRead(BATTERY_CHARGING_INV); #else #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) - if (hasINA()) { - // get current flow from INA sensor - negative value means power flowing into the battery - // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD - LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); + if (hasINA()) { + // get current flow from INA sensor - negative value means power flowing into the battery + // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD + LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); #if defined(INA_CHARGING_DETECTION_INVERT) - return getINACurrent() > 0; + return getINACurrent() > 0; #else - return getINACurrent() < 0; + return getINACurrent() < 0; #endif + } + return isBatteryConnect() && isVbusIn(); +#endif +#endif + // by default, we check the battery voltage only + return isVbusIn(); } - return isBatteryConnect() && isVbusIn(); -#endif -#endif - // by default, we check the battery voltage only - return isVbusIn(); - } -private: - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power + private: + /// If we see a battery voltage higher than physics allows - assume charger is pumping + /// in power - /// For heltecs with no battery connected, the measured voltage is 2204, so - // need to be higher than that, in this case is 2500mV (3000-500) - const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY}; - const float chargingVolt = (OCV[0] + 10) * NUM_CELLS; - const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; - // Start value from minimum voltage for the filter to not start from 0 - // that could trigger some events. - // This value is over-written by the first ADC reading, it the voltage seems reasonable. - bool initial_read_done = false; - float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); - uint32_t last_read_time_ms = 0; + /// For heltecs with no battery connected, the measured voltage is 2204, so + // need to be higher than that, in this case is 2500mV (3000-500) + const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY}; + const float chargingVolt = (OCV[0] + 10) * NUM_CELLS; + const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; + // Start value from minimum voltage for the filter to not start from 0 + // that could trigger some events. + // This value is over-written by the first ADC reading, it the voltage seems reasonable. + bool initial_read_done = false; + float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); + uint32_t last_read_time_ms = 0; #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) - uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } + uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } - bool hasRAK() { - if (!rak9154Sensor.isInitialized()) - return rak9154Sensor.runOnce() > 0; - return rak9154Sensor.isRunning(); - } + bool hasRAK() + { + if (!rak9154Sensor.isInitialized()) + return rak9154Sensor.runOnce() > 0; + return rak9154Sensor.isRunning(); + } #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - uint16_t getINAVoltage() { - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { - return ina219Sensor.getBusVoltageMv(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { - return ina226Sensor.getBusVoltageMv(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == config.power.device_battery_ina_address) { - return ina260Sensor.getBusVoltageMv(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { - return ina3221Sensor.getBusVoltageMv(); + uint16_t getINAVoltage() + { + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + return ina219Sensor.getBusVoltageMv(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == + config.power.device_battery_ina_address) { + return ina226Sensor.getBusVoltageMv(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == + config.power.device_battery_ina_address) { + return ina260Sensor.getBusVoltageMv(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == + config.power.device_battery_ina_address) { + return ina3221Sensor.getBusVoltageMv(); + } + return 0; } - return 0; - } - int16_t getINACurrent() { - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { - return ina219Sensor.getCurrentMa(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { - return ina226Sensor.getCurrentMa(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { - return ina3221Sensor.getCurrentMa(); + int16_t getINACurrent() + { + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + return ina219Sensor.getCurrentMa(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == + config.power.device_battery_ina_address) { + return ina226Sensor.getCurrentMa(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == + config.power.device_battery_ina_address) { + return ina3221Sensor.getCurrentMa(); + } + return 0; } - return 0; - } - bool hasINA() { - if (!config.power.device_battery_ina_address) { - return false; + bool hasINA() + { + if (!config.power.device_battery_ina_address) { + return false; + } + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { + if (!ina219Sensor.isInitialized()) + return ina219Sensor.runOnce() > 0; + return ina219Sensor.isRunning(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == + config.power.device_battery_ina_address) { + if (!ina226Sensor.isInitialized()) + return ina226Sensor.runOnce() > 0; + return ina226Sensor.isRunning(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == + config.power.device_battery_ina_address) { + if (!ina260Sensor.isInitialized()) + return ina260Sensor.runOnce() > 0; + return ina260Sensor.isRunning(); + } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == + config.power.device_battery_ina_address) { + if (!ina3221Sensor.isInitialized()) + return ina3221Sensor.runOnce() > 0; + return ina3221Sensor.isRunning(); + } + return false; } - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { - if (!ina219Sensor.isInitialized()) - return ina219Sensor.runOnce() > 0; - return ina219Sensor.isRunning(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { - if (!ina226Sensor.isInitialized()) - return ina226Sensor.runOnce() > 0; - return ina226Sensor.isRunning(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == config.power.device_battery_ina_address) { - if (!ina260Sensor.isInitialized()) - return ina260Sensor.runOnce() > 0; - return ina260Sensor.isRunning(); - } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { - if (!ina3221Sensor.isInitialized()) - return ina3221Sensor.runOnce() > 0; - return ina3221Sensor.isRunning(); - } - return false; - } #endif }; static AnalogBatteryLevel analogLevel; -Power::Power() : OSThread("Power") { - statusHandler = {}; - low_voltage_counter = 0; +Power::Power() : OSThread("Power") +{ + statusHandler = {}; + low_voltage_counter = 0; #ifdef DEBUG_HEAP - lastheap = memGet.getFreeHeap(); + lastheap = memGet.getFreeHeap(); #endif } -bool Power::analogInit() { +bool Power::analogInit() +{ #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - pinMode(EXT_PWR_DETECT, INPUT_PULLUP); + pinMode(EXT_PWR_DETECT, INPUT_PULLUP); #else - pinMode(EXT_PWR_DETECT, INPUT); + pinMode(EXT_PWR_DETECT, INPUT); #endif #endif #ifdef EXT_CHRG_DETECT - pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); + pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); #endif #ifdef BATTERY_PIN - LOG_DEBUG("Use analog input %d for battery level", BATTERY_PIN); + LOG_DEBUG("Use analog input %d for battery level", BATTERY_PIN); - // disable any internal pullups - pinMode(BATTERY_PIN, INPUT); + // disable any internal pullups + pinMode(BATTERY_PIN, INPUT); #ifndef BATTERY_SENSE_RESOLUTION_BITS #define BATTERY_SENSE_RESOLUTION_BITS 10 @@ -599,56 +626,56 @@ bool Power::analogInit() { #ifdef ARCH_ESP32 // ESP32 needs special analog stuff #ifndef ADC_WIDTH // max resolution by default - static const adc_bits_width_t width = ADC_WIDTH_BIT_12; + static const adc_bits_width_t width = ADC_WIDTH_BIT_12; #else - static const adc_bits_width_t width = ADC_WIDTH; + static const adc_bits_width_t width = ADC_WIDTH; #endif #ifndef BAT_MEASURE_ADC_UNIT // ADC1 - adc1_config_width(width); - adc1_config_channel_atten(adc_channel, atten); + adc1_config_width(width); + adc1_config_channel_atten(adc_channel, atten); #else // ADC2 - adc2_config_channel_atten(adc_channel, atten); + adc2_config_channel_atten(adc_channel, atten); #ifndef CONFIG_IDF_TARGET_ESP32S3 - // ADC2 wifi bug workaround - // Not required with ESP32S3, breaks compile - RTC_reg_b = READ_PERI_REG(SENS_SAR_READ_CTRL2_REG); + // ADC2 wifi bug workaround + // Not required with ESP32S3, breaks compile + RTC_reg_b = READ_PERI_REG(SENS_SAR_READ_CTRL2_REG); #endif #endif - // calibrate ADC - esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_characs); - // show ADC characterization base - if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { - LOG_INFO("ADC config based on Two Point values stored in eFuse"); - } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { - LOG_INFO("ADC config based on reference voltage stored in eFuse"); - } + // calibrate ADC + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_characs); + // show ADC characterization base + if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + LOG_INFO("ADC config based on Two Point values stored in eFuse"); + } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + LOG_INFO("ADC config based on reference voltage stored in eFuse"); + } #ifdef CONFIG_IDF_TARGET_ESP32S3 - // ESP32S3 - else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { - LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse"); - } + // ESP32S3 + else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { + LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse"); + } #endif - else { - LOG_INFO("ADC config based on default reference voltage"); - } + else { + LOG_INFO("ADC config based on default reference voltage"); + } #endif // ARCH_ESP32 #ifdef ARCH_NRF52 #ifdef VBAT_AR_INTERNAL - analogReference(VBAT_AR_INTERNAL); + analogReference(VBAT_AR_INTERNAL); #else - analogReference(AR_INTERNAL); // 3.6V + analogReference(AR_INTERNAL); // 3.6V #endif #endif // ARCH_NRF52 #ifndef ARCH_ESP32 - analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); + analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); #endif - batteryLevel = &analogLevel; - return true; + batteryLevel = &analogLevel; + return true; #else - return false; + return false; #endif } @@ -657,302 +684,308 @@ bool Power::analogInit() { * * @return true if the setup was successful, false otherwise. */ -bool Power::setup() { - bool found = false; - if (axpChipInit()) { - found = true; - } else if (lipoInit()) { - found = true; - } else if (lipoChargerInit()) { - found = true; - } else if (meshSolarInit()) { - found = true; - } else if (analogInit()) { - found = true; - } +bool Power::setup() +{ + bool found = false; + if (axpChipInit()) { + found = true; + } else if (lipoInit()) { + found = true; + } else if (lipoChargerInit()) { + found = true; + } else if (meshSolarInit()) { + found = true; + } else if (analogInit()) { + found = true; + } #ifdef NRF_APM - found = true; + found = true; #endif #ifdef EXT_PWR_DETECT - attachInterrupt( - EXT_PWR_DETECT, - []() { - power->setIntervalFromNow(0); - runASAP = true; - }, - CHANGE); + attachInterrupt( + EXT_PWR_DETECT, + []() { + power->setIntervalFromNow(0); + runASAP = true; + }, + CHANGE); #endif #ifdef BATTERY_CHARGING_INV - attachInterrupt( - BATTERY_CHARGING_INV, - []() { - power->setIntervalFromNow(0); - runASAP = true; - }, - CHANGE); + attachInterrupt( + BATTERY_CHARGING_INV, + []() { + power->setIntervalFromNow(0); + runASAP = true; + }, + CHANGE); #endif - enabled = found; - low_voltage_counter = 0; + enabled = found; + low_voltage_counter = 0; - return found; + return found; } -void Power::powerCommandsCheck() { - if (rebootAtMsec && millis() > rebootAtMsec) { - LOG_INFO("Rebooting"); - reboot(); - } +void Power::powerCommandsCheck() +{ + if (rebootAtMsec && millis() > rebootAtMsec) { + LOG_INFO("Rebooting"); + reboot(); + } - if (shutdownAtMsec && millis() > shutdownAtMsec) { - shutdownAtMsec = 0; - shutdown(); - } + if (shutdownAtMsec && millis() > shutdownAtMsec) { + shutdownAtMsec = 0; + shutdown(); + } } -void Power::reboot() { - notifyReboot.notifyObservers(NULL); +void Power::reboot() +{ + notifyReboot.notifyObservers(NULL); #if defined(ARCH_ESP32) - ESP.restart(); + ESP.restart(); #elif defined(ARCH_NRF52) - NVIC_SystemReset(); + NVIC_SystemReset(); #elif defined(ARCH_RP2040) - rp2040.reboot(); + rp2040.reboot(); #elif defined(ARCH_PORTDUINO) - deInitApiServer(); - if (aLinuxInputImpl) - aLinuxInputImpl->deInit(); - SPI.end(); - Wire.end(); - Serial1.end(); - if (screen) { - delete screen; - screen = nullptr; - } - LOG_DEBUG("final reboot!"); - ::reboot(); + deInitApiServer(); + if (aLinuxInputImpl) + aLinuxInputImpl->deInit(); + SPI.end(); + Wire.end(); + Serial1.end(); + if (screen) { + delete screen; + screen = nullptr; + } + LOG_DEBUG("final reboot!"); + ::reboot(); #elif defined(ARCH_STM32WL) - HAL_NVIC_SystemReset(); + HAL_NVIC_SystemReset(); #else - rebootAtMsec = -1; - LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied"); + rebootAtMsec = -1; + LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied"); #endif } -void Power::shutdown() { +void Power::shutdown() +{ #if HAS_SCREEN - if (screen) { + if (screen) { #ifdef T_DECK_PRO - screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button #elif defined(USE_EINK) - screen->showSimpleBanner("Shutting Down...", - 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen + screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen #else - screen->showSimpleBanner("Shutting Down...", 0); // stays on screen + screen->showSimpleBanner("Shutting Down...", 0); // stays on screen #endif - } + } #endif #if !defined(ARCH_STM32WL) - playShutdownMelody(); + playShutdownMelody(); #endif - nodeDB->saveToDisk(); + nodeDB->saveToDisk(); #if HAS_SCREEN - messageStore.saveToFlash(); + messageStore.saveToFlash(); #endif #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #ifdef PIN_LED1 - ledOff(PIN_LED1); + ledOff(PIN_LED1); #endif #ifdef PIN_LED2 - ledOff(PIN_LED2); + ledOff(PIN_LED2); #endif #ifdef PIN_LED3 - ledOff(PIN_LED3); + ledOff(PIN_LED3); #endif - doDeepSleep(DELAY_FOREVER, true, true); + doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) - exit(EXIT_SUCCESS); + exit(EXIT_SUCCESS); #else - LOG_WARN("FIXME implement shutdown for this platform"); + LOG_WARN("FIXME implement shutdown for this platform"); #endif } /// Reads power status to powerStatus singleton. // // TODO(girts): move this and other axp stuff to power.h/power.cpp. -void Power::readPowerStatus() { - int32_t batteryVoltageMv = -1; // Assume unknown - int8_t batteryChargePercent = -1; - OptionalBool usbPowered = OptUnknown; - OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time - OptionalBool isChargingNow = OptUnknown; +void Power::readPowerStatus() +{ + int32_t batteryVoltageMv = -1; // Assume unknown + int8_t batteryChargePercent = -1; + OptionalBool usbPowered = OptUnknown; + OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time + OptionalBool isChargingNow = OptUnknown; - if (batteryLevel) { - hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; - usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; - isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; - if (hasBattery) { - batteryVoltageMv = batteryLevel->getBattVoltage(); - // If the AXP192 returns a valid battery percentage, use it - if (batteryLevel->getBatteryPercent() >= 0) { - batteryChargePercent = batteryLevel->getBatteryPercent(); - } else { - // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error - // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined - // in power.h - batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / - ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), - 0, 100); - } - } - } - -// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered -// subclass (which shares a superclass with the BatteryLevel stuff) that just provides a few methods. But in the -// interest of fixing this bug I'm going to follow current practice. -#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so - // to detect changes. - - nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); - // LOG_DEBUG("NRF Power %d", nrf_usb_state); - - // If changed to DISCONNECTED - if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED) - isChargingNow = usbPowered = OptFalse; - // If changed to CONNECTED / READY - else - isChargingNow = usbPowered = OptTrue; - -#endif - - // Notify any status instances that are observing us - const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent); - if (millis() > lastLogTime + 50 * 1000) { - LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), powerStatus2.getIsCharging(), - powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); - lastLogTime = millis(); - } - newStatus.notifyObservers(&powerStatus2); -#ifdef DEBUG_HEAP - if (lastheap != memGet.getFreeHeap()) { - // Use stack-allocated buffer to avoid heap allocations in monitoring code - char threadlist[256] = "Threads running:"; - int threadlistLen = strlen(threadlist); - int running = 0; - for (int i = 0; i < MAX_THREADS; i++) { - auto thread = concurrency::mainController.get(i); - if ((thread != nullptr) && (thread->enabled)) { - // Use snprintf to safely append to stack buffer without heap allocation - int remaining = sizeof(threadlist) - threadlistLen - 1; - if (remaining > 0) { - int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); - if (written > 0 && written < remaining) { - threadlistLen += written; - } + if (batteryLevel) { + hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; + usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; + isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; + if (hasBattery) { + batteryVoltageMv = batteryLevel->getBattVoltage(); + // If the AXP192 returns a valid battery percentage, use it + if (batteryLevel->getBatteryPercent() >= 0) { + batteryChargePercent = batteryLevel->getBatteryPercent(); + } else { + // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error + // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined + // in power.h + batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / + ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), + 0, 100); + } } - running++; - } } - LOG_HEAP(threadlist); - LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreeHeap() - lastheap, - running, concurrency::mainController.size(false)); - lastheap = memGet.getFreeHeap(); - } + +// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass +// (which shares a superclass with the BatteryLevel stuff) +// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current +// practice. +#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect + // changes. + + nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); + // LOG_DEBUG("NRF Power %d", nrf_usb_state); + + // If changed to DISCONNECTED + if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED) + isChargingNow = usbPowered = OptFalse; + // If changed to CONNECTED / READY + else + isChargingNow = usbPowered = OptTrue; + +#endif + + // Notify any status instances that are observing us + const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent); + if (millis() > lastLogTime + 50 * 1000) { + LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), + powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); + lastLogTime = millis(); + } + newStatus.notifyObservers(&powerStatus2); +#ifdef DEBUG_HEAP + if (lastheap != memGet.getFreeHeap()) { + // Use stack-allocated buffer to avoid heap allocations in monitoring code + char threadlist[256] = "Threads running:"; + int threadlistLen = strlen(threadlist); + int running = 0; + for (int i = 0; i < MAX_THREADS; i++) { + auto thread = concurrency::mainController.get(i); + if ((thread != nullptr) && (thread->enabled)) { + // Use snprintf to safely append to stack buffer without heap allocation + int remaining = sizeof(threadlist) - threadlistLen - 1; + if (remaining > 0) { + int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); + if (written > 0 && written < remaining) { + threadlistLen += written; + } + } + running++; + } + } + LOG_HEAP(threadlist); + LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), + memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); + lastheap = memGet.getFreeHeap(); + } #ifdef DEBUG_HEAP_MQTT - if (mqtt) { - // send MQTT-Packet with Heap-Size - uint8_t dmac[6]; - getMacAddr(dmac); // Get our hardware ID - char mac[18]; - sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); + if (mqtt) { + // send MQTT-Packet with Heap-Size + uint8_t dmac[6]; + getMacAddr(dmac); // Get our hardware ID + char mac[18]; + sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); - auto newHeap = memGet.getFreeHeap(); - // Use stack-allocated buffers to avoid heap allocations in monitoring code - char heapTopic[128]; - snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); - char heapString[16]; - snprintf(heapString, sizeof(heapString), "%u", newHeap); - mqtt->pubSub.publish(heapTopic, heapString, false); + auto newHeap = memGet.getFreeHeap(); + // Use stack-allocated buffers to avoid heap allocations in monitoring code + char heapTopic[128]; + snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char heapString[16]; + snprintf(heapString, sizeof(heapString), "%u", newHeap); + mqtt->pubSub.publish(heapTopic, heapString, false); - auto wifiRSSI = WiFi.RSSI(); - char wifiTopic[128]; - snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); - char wifiString[16]; - snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); - mqtt->pubSub.publish(wifiTopic, wifiString, false); - } -#endif - -#endif - - // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in - // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. - // - - if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { - if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { - low_voltage_counter++; - LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); - if (low_voltage_counter > 10) { - LOG_INFO("Low voltage detected, trigger deep sleep"); - powerFSM.trigger(EVENT_LOW_BATTERY); - } - } else { - low_voltage_counter = 0; + auto wifiRSSI = WiFi.RSSI(); + char wifiTopic[128]; + snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char wifiString[16]; + snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); + mqtt->pubSub.publish(wifiTopic, wifiString, false); + } +#endif + +#endif + + // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in + // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. + // + + if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { + if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { + low_voltage_counter++; + LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); + if (low_voltage_counter > 10) { + LOG_INFO("Low voltage detected, trigger deep sleep"); + powerFSM.trigger(EVENT_LOW_BATTERY); + } + } else { + low_voltage_counter = 0; + } } - } } -int32_t Power::runOnce() { - readPowerStatus(); +int32_t Power::runOnce() +{ + readPowerStatus(); #ifdef HAS_PMU - // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll - // the IRQ status by reading the registers over I2C - if (PMU) { + // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll + // the IRQ status by reading the registers over I2C + if (PMU) { - PMU->getIrqStatus(); + PMU->getIrqStatus(); - if (PMU->isVbusRemoveIrq()) { - LOG_INFO("USB unplugged"); - powerFSM.trigger(EVENT_POWER_DISCONNECTED); - } + if (PMU->isVbusRemoveIrq()) { + LOG_INFO("USB unplugged"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } - if (PMU->isVbusInsertIrq()) { - LOG_INFO("USB plugged In"); - powerFSM.trigger(EVENT_POWER_CONNECTED); - } + if (PMU->isVbusInsertIrq()) { + LOG_INFO("USB plugged In"); + powerFSM.trigger(EVENT_POWER_CONNECTED); + } - /* - Other things we could check if we cared... + /* + Other things we could check if we cared... - if (PMU->isBatChagerStartIrq()) { - LOG_DEBUG("Battery start charging"); - } - if (PMU->isBatChagerDoneIrq()) { - LOG_DEBUG("Battery fully charged"); - } - if (PMU->isBatInsertIrq()) { - LOG_DEBUG("Battery inserted"); - } - if (PMU->isBatRemoveIrq()) { - LOG_DEBUG("Battery removed"); - } - */ + if (PMU->isBatChagerStartIrq()) { + LOG_DEBUG("Battery start charging"); + } + if (PMU->isBatChagerDoneIrq()) { + LOG_DEBUG("Battery fully charged"); + } + if (PMU->isBatInsertIrq()) { + LOG_DEBUG("Battery inserted"); + } + if (PMU->isBatRemoveIrq()) { + LOG_DEBUG("Battery removed"); + } + */ #ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? - if (PMU->isPekeyLongPressIrq()) { - LOG_DEBUG("PEK long button press"); - if (screen) - screen->setOn(false); - } + if (PMU->isPekeyLongPressIrq()) { + LOG_DEBUG("PEK long button press"); + if (screen) + screen->setOn(false); + } #endif - PMU->clearIrqStatus(); - } + PMU->clearIrqStatus(); + } #endif - // Only read once every 20 seconds once the power status for the app has been initialized - return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; + // Only read once every 20 seconds once the power status for the app has been initialized + return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; } /** @@ -960,237 +993,251 @@ int32_t Power::runOnce() { * * axp192 power DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the - axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep - this on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for - a couple of days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS + axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this + on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of + days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS * */ -bool Power::axpChipInit() { +bool Power::axpChipInit() +{ #ifdef HAS_PMU - TwoWire *w = NULL; + TwoWire *w = NULL; - // Use macro to distinguish which wire is used by PMU + // Use macro to distinguish which wire is used by PMU #ifdef PMU_USE_WIRE1 - w = &Wire1; + w = &Wire1; #else - w = &Wire; + w = &Wire; #endif - /** - * It is not necessary to specify the wire pin, - * just input the wire, because the wire has been initialized in main.cpp - */ - if (!PMU) { - PMU = new XPowersAXP2101(*w); - if (!PMU->init()) { - LOG_WARN("No AXP2101 power management"); - delete PMU; - PMU = NULL; - } else { - LOG_INFO("AXP2101 PMU init succeeded"); + /** + * It is not necessary to specify the wire pin, + * just input the wire, because the wire has been initialized in main.cpp + */ + if (!PMU) { + PMU = new XPowersAXP2101(*w); + if (!PMU->init()) { + LOG_WARN("No AXP2101 power management"); + delete PMU; + PMU = NULL; + } else { + LOG_INFO("AXP2101 PMU init succeeded"); + } } - } - if (!PMU) { - PMU = new XPowersAXP192(*w); - if (!PMU->init()) { - LOG_WARN("No AXP192 power management"); - delete PMU; - PMU = NULL; - } else { - LOG_INFO("AXP192 PMU init succeeded"); + if (!PMU) { + PMU = new XPowersAXP192(*w); + if (!PMU->init()) { + LOG_WARN("No AXP192 power management"); + delete PMU; + PMU = NULL; + } else { + LOG_INFO("AXP192 PMU init succeeded"); + } } - } - if (!PMU) { - /* - * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time. - * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized - * once, if there are multiple devices sharing the bus. - * * */ + if (!PMU) { + /* + * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time. + * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once, + * if there are multiple devices sharing the bus. + * * */ #ifndef PMU_USE_WIRE1 - w->begin(I2C_SDA, I2C_SCL); + w->begin(I2C_SDA, I2C_SCL); #endif - return false; - } + return false; + } - batteryLevel = PMU; + batteryLevel = PMU; - if (PMU->getChipModel() == XPOWERS_AXP192) { + if (PMU->getChipModel() == XPOWERS_AXP192) { - // lora radio power channel - PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); - PMU->enablePowerOutput(XPOWERS_LDO2); + // lora radio power channel + PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); + PMU->enablePowerOutput(XPOWERS_LDO2); - // oled module power channel, - // disable it will cause abnormal communication between boot and AXP power supply, - // do not turn it off - PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); - // enable oled power - PMU->enablePowerOutput(XPOWERS_DCDC1); + // oled module power channel, + // disable it will cause abnormal communication between boot and AXP power supply, + // do not turn it off + PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); + // enable oled power + PMU->enablePowerOutput(XPOWERS_DCDC1); - // gnss module power channel - now turned on in setGpsPower - PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); - // PMU->enablePowerOutput(XPOWERS_LDO3); + // gnss module power channel - now turned on in setGpsPower + PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); + // PMU->enablePowerOutput(XPOWERS_LDO3); - // protected oled power source - PMU->setProtectedChannel(XPOWERS_DCDC1); - // protected esp32 power source - PMU->setProtectedChannel(XPOWERS_DCDC3); + // protected oled power source + PMU->setProtectedChannel(XPOWERS_DCDC1); + // protected esp32 power source + PMU->setProtectedChannel(XPOWERS_DCDC3); - // disable not use channel - PMU->disablePowerOutput(XPOWERS_DCDC2); + // disable not use channel + PMU->disablePowerOutput(XPOWERS_DCDC2); - // disable all axp chip interrupt - PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); + // disable all axp chip interrupt + PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); - // Set constant current charging current - PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA); + // Set constant current charging current + PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA); - // Set up the charging voltage - PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); - } else if (PMU->getChipModel() == XPOWERS_AXP2101) { + // Set up the charging voltage + PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); + } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/ - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - // Unuse power channel - PMU->disablePowerOutput(XPOWERS_DCDC2); - PMU->disablePowerOutput(XPOWERS_DCDC3); - PMU->disablePowerOutput(XPOWERS_DCDC4); - PMU->disablePowerOutput(XPOWERS_DCDC5); - PMU->disablePowerOutput(XPOWERS_ALDO1); - PMU->disablePowerOutput(XPOWERS_ALDO4); - PMU->disablePowerOutput(XPOWERS_BLDO1); - PMU->disablePowerOutput(XPOWERS_BLDO2); - PMU->disablePowerOutput(XPOWERS_DLDO1); - PMU->disablePowerOutput(XPOWERS_DLDO2); + /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/ + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + // Unuse power channel + PMU->disablePowerOutput(XPOWERS_DCDC2); + PMU->disablePowerOutput(XPOWERS_DCDC3); + PMU->disablePowerOutput(XPOWERS_DCDC4); + PMU->disablePowerOutput(XPOWERS_DCDC5); + PMU->disablePowerOutput(XPOWERS_ALDO1); + PMU->disablePowerOutput(XPOWERS_ALDO4); + PMU->disablePowerOutput(XPOWERS_BLDO1); + PMU->disablePowerOutput(XPOWERS_BLDO2); + PMU->disablePowerOutput(XPOWERS_DLDO1); + PMU->disablePowerOutput(XPOWERS_DLDO2); - // GNSS RTC PowerVDD 3300mV - PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); - PMU->enablePowerOutput(XPOWERS_VBACKUP); + // GNSS RTC PowerVDD 3300mV + PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); + PMU->enablePowerOutput(XPOWERS_VBACKUP); - // ESP32 VDD 3300mV - // ! No need to set, automatically open , Don't close it - // PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); - // PMU->setProtectedChannel(XPOWERS_DCDC1); + // ESP32 VDD 3300mV + // ! No need to set, automatically open , Don't close it + // PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); + // PMU->setProtectedChannel(XPOWERS_DCDC1); - // LoRa VDD 3300mV - PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO2); + // LoRa VDD 3300mV + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO2); - // GNSS VDD 3300mV - PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO3); - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { - // t-beam s3 core - /** - * gnss module power channel - * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during - * initialization - */ - PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO4); + // GNSS VDD 3300mV + PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO3); + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || + HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { + // t-beam s3 core + /** + * gnss module power channel + * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during + * initialization + */ + PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO4); - // lora radio power channel - PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO3); + // lora radio power channel + PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO3); - // m.2 interface - PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300); - PMU->enablePowerOutput(XPOWERS_DCDC3); + // m.2 interface + PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300); + PMU->enablePowerOutput(XPOWERS_DCDC3); - /** - * ALDO2 cannot be turned off. - * It is a necessary condition for sensor communication. - * It must be turned on to properly access the sensor and screen - * It is also responsible for the power supply of PCF8563 - */ - PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO2); + /** + * ALDO2 cannot be turned off. + * It is a necessary condition for sensor communication. + * It must be turned on to properly access the sensor and screen + * It is also responsible for the power supply of PCF8563 + */ + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO2); - // 6-axis , magnetometer ,bme280 , oled screen power channel - PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); - PMU->enablePowerOutput(XPOWERS_ALDO1); + // 6-axis , magnetometer ,bme280 , oled screen power channel + PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO1); - // sdcard power channel - PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); - PMU->enablePowerOutput(XPOWERS_BLDO1); + // sdcard power channel + PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO1); #ifdef T_WATCH_S3 - // DRV2605 power channel - PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); - PMU->enablePowerOutput(XPOWERS_BLDO2); + // DRV2605 power channel + PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO2); #endif - // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); - // PMU->enablePowerOutput(XPOWERS_DCDC4); + // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); + // PMU->enablePowerOutput(XPOWERS_DCDC4); - // not use channel - PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited - PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited - PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist - PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist - PMU->disablePowerOutput(XPOWERS_VBACKUP); + // not use channel + PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited + PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited + PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist + PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist + PMU->disablePowerOutput(XPOWERS_VBACKUP); + } + + // disable all axp chip interrupt + PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + + // Set the constant current charging current of AXP2101, temporarily use 500mA by default + PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); + + // Set up the charging voltage + PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); } - // disable all axp chip interrupt - PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + PMU->clearIrqStatus(); - // Set the constant current charging current of AXP2101, temporarily use 500mA by default - PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); + // TBeam1.1 /T-Beam S3-Core has no external TS detection, + // it needs to be disabled, otherwise it will cause abnormal charging + PMU->disableTSPinMeasure(); - // Set up the charging voltage - PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); - } + // PMU->enableSystemVoltageMeasure(); + PMU->enableVbusVoltageMeasure(); + PMU->enableBattVoltageMeasure(); - PMU->clearIrqStatus(); - - // TBeam1.1 /T-Beam S3-Core has no external TS detection, - // it needs to be disabled, otherwise it will cause abnormal charging - PMU->disableTSPinMeasure(); - - // PMU->enableSystemVoltageMeasure(); - PMU->enableVbusVoltageMeasure(); - PMU->enableBattVoltageMeasure(); - - if (PMU->isChannelAvailable(XPOWERS_DCDC1)) { - LOG_DEBUG("DC1 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC1) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC1)); - } - if (PMU->isChannelAvailable(XPOWERS_DCDC2)) { - LOG_DEBUG("DC2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC2)); - } - if (PMU->isChannelAvailable(XPOWERS_DCDC3)) { - LOG_DEBUG("DC3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC3) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC3)); - } - if (PMU->isChannelAvailable(XPOWERS_DCDC4)) { - LOG_DEBUG("DC4 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC4) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_DCDC4)); - } - if (PMU->isChannelAvailable(XPOWERS_LDO2)) { - LOG_DEBUG("LDO2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_LDO2)); - } - if (PMU->isChannelAvailable(XPOWERS_LDO3)) { - LOG_DEBUG("LDO3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO3) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_LDO3)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO1)) { - LOG_DEBUG("ALDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO1) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO1)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO2)) { - LOG_DEBUG("ALDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO2)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO3)) { - LOG_DEBUG("ALDO3: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO3) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO3)); - } - if (PMU->isChannelAvailable(XPOWERS_ALDO4)) { - LOG_DEBUG("ALDO4: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO4) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_ALDO4)); - } - if (PMU->isChannelAvailable(XPOWERS_BLDO1)) { - LOG_DEBUG("BLDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_BLDO1)); - } - if (PMU->isChannelAvailable(XPOWERS_BLDO2)) { - LOG_DEBUG("BLDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO2) ? "+" : "-", PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); - } + if (PMU->isChannelAvailable(XPOWERS_DCDC1)) { + LOG_DEBUG("DC1 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC1) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_DCDC1)); + } + if (PMU->isChannelAvailable(XPOWERS_DCDC2)) { + LOG_DEBUG("DC2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC2) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_DCDC2)); + } + if (PMU->isChannelAvailable(XPOWERS_DCDC3)) { + LOG_DEBUG("DC3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC3) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_DCDC3)); + } + if (PMU->isChannelAvailable(XPOWERS_DCDC4)) { + LOG_DEBUG("DC4 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC4) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_DCDC4)); + } + if (PMU->isChannelAvailable(XPOWERS_LDO2)) { + LOG_DEBUG("LDO2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO2) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_LDO2)); + } + if (PMU->isChannelAvailable(XPOWERS_LDO3)) { + LOG_DEBUG("LDO3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO3) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_LDO3)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO1)) { + LOG_DEBUG("ALDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO1) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_ALDO1)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO2)) { + LOG_DEBUG("ALDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO2) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_ALDO2)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO3)) { + LOG_DEBUG("ALDO3: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO3) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_ALDO3)); + } + if (PMU->isChannelAvailable(XPOWERS_ALDO4)) { + LOG_DEBUG("ALDO4: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO4) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_ALDO4)); + } + if (PMU->isChannelAvailable(XPOWERS_BLDO1)) { + LOG_DEBUG("BLDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_BLDO1)); + } + if (PMU->isChannelAvailable(XPOWERS_BLDO2)) { + LOG_DEBUG("BLDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO2) ? "+" : "-", + PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); + } // We can safely ignore this approach for most (or all) boards because MCU turned off // earlier than battery discharged to 2.6V. @@ -1198,40 +1245,40 @@ bool Power::axpChipInit() { // Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with // battery voltage measurement. Probably it sometimes drops to low values. #ifndef RAK4630 - // Set PMU shutdown voltage at 2.6V to maximize battery utilization - PMU->setSysPowerDownVoltage(2600); + // Set PMU shutdown voltage at 2.6V to maximize battery utilization + PMU->setSysPowerDownVoltage(2600); #endif #ifdef PMU_IRQ - uint64_t pmuIrqMask = 0; + uint64_t pmuIrqMask = 0; - if (PMU->getChipModel() == XPOWERS_AXP192) { - pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ; - } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ; - } + if (PMU->getChipModel() == XPOWERS_AXP192) { + pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ; + } else if (PMU->getChipModel() == XPOWERS_AXP2101) { + pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ; + } - pinMode(PMU_IRQ, INPUT); - attachInterrupt( - PMU_IRQ, [] { pmu_irq = true; }, FALLING); + pinMode(PMU_IRQ, INPUT); + attachInterrupt( + PMU_IRQ, [] { pmu_irq = true; }, FALLING); - // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is - // no battery also it could cause inadvertent waking from light sleep just because the battery filled - // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed - // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus - PMU->enableIRQ(pmuIrqMask); + // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is + // no battery also it could cause inadvertent waking from light sleep just because the battery filled + // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed + // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus + PMU->enableIRQ(pmuIrqMask); - PMU->clearIrqStatus(); + PMU->clearIrqStatus(); #endif /*PMU_IRQ*/ - readPowerStatus(); + readPowerStatus(); - pmu_found = true; + pmu_found = true; - return pmu_found; + return pmu_found; #else - return false; + return false; #endif } @@ -1240,50 +1287,52 @@ bool Power::axpChipInit() { /** * Wrapper class for an I2C MAX17048 Lipo battery sensor. */ -class LipoBatteryLevel : public HasBatteryLevel { -private: - MAX17048Singleton *max17048 = nullptr; +class LipoBatteryLevel : public HasBatteryLevel +{ + private: + MAX17048Singleton *max17048 = nullptr; -public: - /** - * Init the I2C MAX17048 Lipo battery level sensor - */ - bool runOnce() { - if (max17048 == nullptr) { - max17048 = MAX17048Singleton::GetInstance(); + public: + /** + * Init the I2C MAX17048 Lipo battery level sensor + */ + bool runOnce() + { + if (max17048 == nullptr) { + max17048 = MAX17048Singleton::GetInstance(); + } + + // try to start if the sensor has been detected + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].first != 0) { + return max17048->runOnce(nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].second); + } + return false; } - // try to start if the sensor has been detected - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].first != 0) { - return max17048->runOnce(nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].second); - } - return false; - } + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return max17048->getBusBatteryPercent(); } - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override { return max17048->getBusBatteryPercent(); } + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return max17048->getBusVoltageMv(); } - /** - * The raw voltage of the battery in millivolts, or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { return max17048->getBusVoltageMv(); } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return max17048->isBatteryConnected(); } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() override { return max17048->isBatteryConnected(); } + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return max17048->isExternallyPowered(); } - /** - * return true if there is an external power source detected - */ - virtual bool isVbusIn() override { return max17048->isExternallyPowered(); } - - /** - * return true if the battery is currently charging - */ - virtual bool isCharging() override { return max17048->isBatteryCharging(); } + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return max17048->isBatteryCharging(); } }; LipoBatteryLevel lipoLevel; @@ -1291,20 +1340,24 @@ LipoBatteryLevel lipoLevel; /** * Init the Lipo battery level sensor */ -bool Power::lipoInit() { - bool result = lipoLevel.runOnce(); - LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet"); - if (!result) - return false; - batteryLevel = &lipoLevel; - return true; +bool Power::lipoInit() +{ + bool result = lipoLevel.runOnce(); + LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &lipoLevel; + return true; } #else /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::lipoInit() { return false; } +bool Power::lipoInit() +{ + return false; +} #endif #if defined(HAS_PPM) && HAS_PPM @@ -1312,110 +1365,114 @@ bool Power::lipoInit() { return false; } /** * Adapter class for BQ25896/BQ27220 Lipo battery charger. */ -class LipoCharger : public HasBatteryLevel { -private: - BQ27220 *bq = nullptr; +class LipoCharger : public HasBatteryLevel +{ + private: + BQ27220 *bq = nullptr; -public: - /** - * Init the I2C BQ25896 Lipo battery charger - */ - bool runOnce() { - if (PPM == nullptr) { - PPM = new XPowersPPM; - bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); - if (result) { - LOG_INFO("PPM BQ25896 init succeeded"); - // Set the minimum operating voltage. Below this voltage, the PPM will protect - // PPM->setSysPowerDownVoltage(3100); + public: + /** + * Init the I2C BQ25896 Lipo battery charger + */ + bool runOnce() + { + if (PPM == nullptr) { + PPM = new XPowersPPM; + bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); + if (result) { + LOG_INFO("PPM BQ25896 init succeeded"); + // Set the minimum operating voltage. Below this voltage, the PPM will protect + // PPM->setSysPowerDownVoltage(3100); - // Set input current limit, default is 500mA - // PPM->setInputCurrentLimit(800); + // Set input current limit, default is 500mA + // PPM->setInputCurrentLimit(800); - // Disable current limit pin - // PPM->disableCurrentLimitPin(); + // Disable current limit pin + // PPM->disableCurrentLimitPin(); - // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV - PPM->setChargeTargetVoltage(4288); + // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV + PPM->setChargeTargetVoltage(4288); - // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA - // PPM->setPrechargeCurr(64); + // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA + // PPM->setPrechargeCurr(64); - // The premise is that limit pin is disabled, or it will - // only follow the maximum charging current set by limit pin. - // Set the charging current , Range:0~5056mA ,step:64mA - PPM->setChargerConstantCurr(1024); + // The premise is that limit pin is disabled, or it will + // only follow the maximum charging current set by limit pin. + // Set the charging current , Range:0~5056mA ,step:64mA + PPM->setChargerConstantCurr(1024); - // To obtain voltage data, the ADC must be enabled first - PPM->enableMeasure(); + // To obtain voltage data, the ADC must be enabled first + PPM->enableMeasure(); - // Turn on charging function - // If there is no battery connected, do not turn on the charging function - PPM->enableCharge(); - } else { - LOG_WARN("PPM BQ25896 init failed"); - delete PPM; - PPM = nullptr; + // Turn on charging function + // If there is no battery connected, do not turn on the charging function + PPM->enableCharge(); + } else { + LOG_WARN("PPM BQ25896 init failed"); + delete PPM; + PPM = nullptr; + return false; + } + } + if (bq == nullptr) { + bq = new BQ27220; + bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY); + + bool result = bq->init(); + if (result) { + LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity()); + LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity()); + LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity()); + return true; + } else { + LOG_WARN("BQ27220 init failed"); + delete bq; + bq = nullptr; + return false; + } + } return false; - } } - if (bq == nullptr) { - bq = new BQ27220; - bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY); - bool result = bq->init(); - if (result) { - LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity()); - LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity()); - LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity()); - return true; - } else { - LOG_WARN("BQ27220 init failed"); - delete bq; - bq = nullptr; - return false; - } + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override + { + return -1; + // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated } - return false; - } - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override { - return -1; - // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated - } + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return bq->getVoltage(); } - /** - * The raw voltage of the battery in millivolts, or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { return bq->getVoltage(); } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return PPM->isVbusIn(); } - /** - * return true if there is an external power source detected - */ - virtual bool isVbusIn() override { return PPM->isVbusIn(); } - - /** - * return true if the battery is currently charging - */ - virtual bool isCharging() override { - bool isCharging = PPM->isCharging(); - if (isCharging) { - LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); - } else { - if (!PPM->isVbusIn()) { - LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); - } + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override + { + bool isCharging = PPM->isCharging(); + if (isCharging) { + LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); + } else { + if (!PPM->isVbusIn()) { + LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); + } + } + return isCharging; } - return isCharging; - } }; LipoCharger lipoCharger; @@ -1423,20 +1480,24 @@ LipoCharger lipoCharger; /** * Init the Lipo battery charger */ -bool Power::lipoChargerInit() { - bool result = lipoCharger.runOnce(); - LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet"); - if (!result) - return false; - batteryLevel = &lipoCharger; - return true; +bool Power::lipoChargerInit() +{ + bool result = lipoCharger.runOnce(); + LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &lipoCharger; + return true; } #else /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::lipoChargerInit() { return false; } +bool Power::lipoChargerInit() +{ + return false; +} #endif #ifdef HELTEC_MESH_SOLAR @@ -1445,41 +1506,43 @@ bool Power::lipoChargerInit() { return false; } /** * meshSolar class for an SMBUS battery sensor. */ -class meshSolarBatteryLevel : public HasBatteryLevel { +class meshSolarBatteryLevel : public HasBatteryLevel +{ -public: - /** - * Init the I2C meshSolar battery level sensor - */ - bool runOnce() { - meshSolarStart(); - return true; - } + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + meshSolarStart(); + return true; + } - /** - * Battery state of charge, from 0 to 100 or -1 for unknown - */ - virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } - /** - * The raw voltage of the battery in millivolts, or NAN if unknown - */ - virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } - /** - * return true if there is a battery installed in this unit - */ - virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } - /** - * return true if there is an external power source detected - */ - virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } - /** - * return true if the battery is currently charging - */ - virtual bool isCharging() override { return meshSolarIsCharging(); } + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return meshSolarIsCharging(); } }; meshSolarBatteryLevel meshSolarLevel; @@ -1487,18 +1550,22 @@ meshSolarBatteryLevel meshSolarLevel; /** * Init the meshSolar battery level sensor */ -bool Power::meshSolarInit() { - bool result = meshSolarLevel.runOnce(); - LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); - if (!result) - return false; - batteryLevel = &meshSolarLevel; - return true; +bool Power::meshSolarInit() +{ + bool result = meshSolarLevel.runOnce(); + LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &meshSolarLevel; + return true; } #else /** * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::meshSolarInit() { return false; } +bool Power::meshSolarInit() +{ + return false; +} #endif diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 2bbfc3742..9f8097b84 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -31,202 +31,222 @@ FakeFsm powerFSM; void PowerFSM_setup(){}; #else /// Should we behave as if we have AC power now? -static bool isPowered() { +static bool isPowered() +{ // Circumvent the battery sensing logic and assumes constant power if no battery pin or power mgmt IC #if !defined(BATTERY_PIN) && !defined(HAS_AXP192) && !defined(HAS_AXP2101) && !defined(NRF_APM) - return true; + return true; #endif - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); - // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON - // We assume routers might be powered all the time, but from a low current (solar) source - bool isPowerSavingMode = config.power.is_power_saving || isRouter; + // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON + // We assume routers might be powered all the time, but from a low current (solar) source + bool isPowerSavingMode = config.power.is_power_saving || isRouter; - /* To determine if we're externally powered, assumptions - 1) If we're powered up and there's no battery, we must be getting power externally. (because we'd be dead - otherwise) + /* To determine if we're externally powered, assumptions + 1) If we're powered up and there's no battery, we must be getting power externally. (because we'd be dead otherwise) - 2) If we detect USB power from the power management chip, we must be getting power externally. + 2) If we detect USB power from the power management chip, we must be getting power externally. - 3) On some boards we don't have the power management chip (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to - detect external power source (see `isVbusIn()` in `Power.cpp`) - */ - return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB()); + 3) On some boards we don't have the power management chip (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect + external power source (see `isVbusIn()` in `Power.cpp`) + */ + return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB()); } -static void sdsEnter() { - LOG_POWERFSM("State: SDS"); - // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw - doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); +static void sdsEnter() +{ + LOG_POWERFSM("State: SDS"); + // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw + doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); } -static void lowBattSDSEnter() { - LOG_POWERFSM("State: Lower batt SDS"); - doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); +static void lowBattSDSEnter() +{ + LOG_POWERFSM("State: Lower batt SDS"); + doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); } extern Power *power; -static void shutdownEnter() { - LOG_POWERFSM("State: SHUTDOWN"); - shutdownAtMsec = millis(); +static void shutdownEnter() +{ + LOG_POWERFSM("State: SHUTDOWN"); + shutdownAtMsec = millis(); } #include "error.h" static uint32_t secsSlept; -static void lsEnter() { - LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); - if (screen) - screen->setOn(false); - secsSlept = 0; // How long have we been sleeping this time +static void lsEnter() +{ + LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); + if (screen) + screen->setOn(false); + secsSlept = 0; // How long have we been sleeping this time - // LOG_INFO("lsEnter end"); + // LOG_INFO("lsEnter end"); } -static void lsIdle() { - // LOG_INFO("lsIdle begin ls_secs=%u", getPref_ls_secs()); +static void lsIdle() +{ + // LOG_INFO("lsIdle begin ls_secs=%u", getPref_ls_secs()); #ifdef ARCH_ESP32 - // Do we have more sleeping to do? - if (secsSlept < config.power.ls_secs) { - // If some other service would stall sleep, don't let sleep happen yet - if (doPreflightSleep()) { - // Briefly come out of sleep long enough to blink the led once every few seconds - uint32_t sleepTime = SLEEP_TIME; + // Do we have more sleeping to do? + if (secsSlept < config.power.ls_secs) { + // If some other service would stall sleep, don't let sleep happen yet + if (doPreflightSleep()) { + // Briefly come out of sleep long enough to blink the led once every few seconds + uint32_t sleepTime = SLEEP_TIME; - powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); - ledBlink.set(false); // Never leave led on while in light sleep - esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); - powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); + powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); + ledBlink.set(false); // Never leave led on while in light sleep + esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); + powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); - switch (wakeCause2) { - case ESP_SLEEP_WAKEUP_TIMER: - // Normal case: timer expired, we should just go back to sleep ASAP + switch (wakeCause2) { + case ESP_SLEEP_WAKEUP_TIMER: + // Normal case: timer expired, we should just go back to sleep ASAP - ledBlink.set(true); // briefly turn on led - wakeCause2 = doLightSleep(100); // leave led on for 1ms + ledBlink.set(true); // briefly turn on led + wakeCause2 = doLightSleep(100); // leave led on for 1ms - secsSlept += sleepTime; - // LOG_INFO("Sleep, flash led!"); - break; + secsSlept += sleepTime; + // LOG_INFO("Sleep, flash led!"); + break; - case ESP_SLEEP_WAKEUP_UART: - // Not currently used (because uart triggers in hw have problems) - powerFSM.trigger(EVENT_SERIAL_CONNECTED); - break; + case ESP_SLEEP_WAKEUP_UART: + // Not currently used (because uart triggers in hw have problems) + powerFSM.trigger(EVENT_SERIAL_CONNECTED); + break; - default: - // We woke for some other reason (button press, device IRQ interrupt) + default: + // We woke for some other reason (button press, device IRQ interrupt) #ifdef BUTTON_PIN - bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); + bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); #else - bool pressed = false; + bool pressed = false; #endif - if (pressed) { // If we woke because of press, instead generate a PRESS event. - powerFSM.trigger(EVENT_PRESS); + if (pressed) { // If we woke because of press, instead generate a PRESS event. + powerFSM.trigger(EVENT_PRESS); + } else { + // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) + // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code + powerFSM.trigger(EVENT_WAKE_TIMER); + } + break; + } } else { - // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) - // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code - powerFSM.trigger(EVENT_WAKE_TIMER); + // Someone says we can't sleep now, so just save some power by sleeping the CPU for 100ms or so + delay(100); } - break; - } } else { - // Someone says we can't sleep now, so just save some power by sleeping the CPU for 100ms or so - delay(100); + // Time to stop sleeping! + ledBlink.set(false); + LOG_INFO("Reached ls_secs, service loop()"); + powerFSM.trigger(EVENT_WAKE_TIMER); } - } else { - // Time to stop sleeping! - ledBlink.set(false); - LOG_INFO("Reached ls_secs, service loop()"); - powerFSM.trigger(EVENT_WAKE_TIMER); - } #endif } -static void lsExit() { LOG_POWERFSM("State: lsExit"); } - -static void nbEnter() { - LOG_POWERFSM("State: nbEnter"); - if (screen) - screen->setOn(false); -#ifdef ARCH_ESP32 - // Only ESP32 should turn off bluetooth - setBluetoothEnable(false); -#endif - - // FIXME - check if we already have packets for phone and immediately trigger EVENT_PACKETS_FOR_PHONE +static void lsExit() +{ + LOG_POWERFSM("State: lsExit"); } -static void darkEnter() { - LOG_POWERFSM("State: darkEnter"); - setBluetoothEnable(true); - if (screen) - screen->setOn(false); -} - -static void serialEnter() { - LOG_POWERFSM("State: serialEnter"); - setBluetoothEnable(false); - if (screen) { - screen->setOn(true); - } -} - -static void serialExit() { - LOG_POWERFSM("State: serialExit"); - // Turn bluetooth back on when we leave serial stream API - setBluetoothEnable(true); -} - -static void powerEnter() { - LOG_POWERFSM("State: powerEnter"); - if (!isPowered()) { - // If we got here, we are in the wrong state - we should be in powered, let that state handle things - LOG_INFO("Loss of power in Powered"); - powerFSM.trigger(EVENT_POWER_DISCONNECTED); - } else { +static void nbEnter() +{ + LOG_POWERFSM("State: nbEnter"); if (screen) - screen->setOn(true); + screen->setOn(false); +#ifdef ARCH_ESP32 + // Only ESP32 should turn off bluetooth + setBluetoothEnable(false); +#endif + + // FIXME - check if we already have packets for phone and immediately trigger EVENT_PACKETS_FOR_PHONE +} + +static void darkEnter() +{ + LOG_POWERFSM("State: darkEnter"); setBluetoothEnable(true); - // within enter() the function getState() returns the state we came from - } + if (screen) + screen->setOn(false); } -static void powerIdle() { - // LOG_POWERFSM("State: powerIdle"); // very chatty - if (!isPowered()) { - // If we got here, we are in the wrong state - LOG_INFO("Loss of power in Powered"); - powerFSM.trigger(EVENT_POWER_DISCONNECTED); - } +static void serialEnter() +{ + LOG_POWERFSM("State: serialEnter"); + setBluetoothEnable(false); + if (screen) { + screen->setOn(true); + } } -static void powerExit() { - LOG_POWERFSM("State: powerExit"); - setBluetoothEnable(true); +static void serialExit() +{ + LOG_POWERFSM("State: serialExit"); + // Turn bluetooth back on when we leave serial stream API + setBluetoothEnable(true); } -static void onEnter() { - LOG_POWERFSM("State: onEnter"); - if (screen) - screen->setOn(true); - setBluetoothEnable(true); +static void powerEnter() +{ + LOG_POWERFSM("State: powerEnter"); + if (!isPowered()) { + // If we got here, we are in the wrong state - we should be in powered, let that state handle things + LOG_INFO("Loss of power in Powered"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } else { + if (screen) + screen->setOn(true); + setBluetoothEnable(true); + // within enter() the function getState() returns the state we came from + } } -static void onIdle() { - LOG_POWERFSM("State: onIdle"); - if (isPowered()) { - // If we got here, we are in the wrong state - we should be in powered, let that state handle things - powerFSM.trigger(EVENT_POWER_CONNECTED); - } +static void powerIdle() +{ + // LOG_POWERFSM("State: powerIdle"); // very chatty + if (!isPowered()) { + // If we got here, we are in the wrong state + LOG_INFO("Loss of power in Powered"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); + } } -static void bootEnter() { LOG_POWERFSM("State: bootEnter"); } +static void powerExit() +{ + LOG_POWERFSM("State: powerExit"); + setBluetoothEnable(true); +} + +static void onEnter() +{ + LOG_POWERFSM("State: onEnter"); + if (screen) + screen->setOn(true); + setBluetoothEnable(true); +} + +static void onIdle() +{ + LOG_POWERFSM("State: onIdle"); + if (isPowered()) { + // If we got here, we are in the wrong state - we should be in powered, let that state handle things + powerFSM.trigger(EVENT_POWER_CONNECTED); + } +} + +static void bootEnter() +{ + LOG_POWERFSM("State: bootEnter"); +} State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN"); State stateSDS(sdsEnter, NULL, NULL, "SDS"); @@ -240,141 +260,147 @@ State stateON(onEnter, onIdle, NULL, "ON"); State statePOWER(powerEnter, powerIdle, powerExit, "POWER"); Fsm powerFSM(&stateBOOT); -void PowerFSM_setup() { - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); - bool hasPower = isPowered(); +void PowerFSM_setup() +{ + bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool hasPower = isPowered(); - LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); - powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); + LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); + powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); - // wake timer expired or a packet arrived - // if we are a router node, we go to NB (no need for bluetooth) otherwise we go to DARK (so we can send message to - // phone) + // wake timer expired or a packet arrived + // if we are a router node, we go to NB (no need for bluetooth) otherwise we go to DARK (so we can send message to phone) #ifdef ARCH_ESP32 - powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); + powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); #else // Don't go into a no-bluetooth state on low power platforms - powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); + powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, NULL, "Wake timer"); #endif - // We need this transition, because we might not transition if we were waiting to enter light-sleep, because when we - // wake from light sleep we _always_ transition to NB or dark and - powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, exiting light sleep"); - powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake"); + // We need this transition, because we might not transition if we were waiting to enter light-sleep, because when we wake from + // light sleep we _always_ transition to NB or dark and + powerFSM.add_transition(&stateLS, isRouter ? &stateNB : &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, + "Received packet, exiting light sleep"); + powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake"); - // Handle press events - note: we ignore button presses when in API mode - powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers - powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, - "Press"); // Allow button to work while in serial API + // Handle press events - note: we ignore button presses when in API mode + powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, + "Press"); // Allow button to work while in serial API - // Handle critically low power battery by forcing deep sleep - powerFSM.add_transition(&stateBOOT, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateLS, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateNB, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateDARK, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateON, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - powerFSM.add_transition(&stateSERIAL, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + // Handle critically low power battery by forcing deep sleep + powerFSM.add_transition(&stateBOOT, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateLS, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateNB, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateDARK, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateON, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); + powerFSM.add_transition(&stateSERIAL, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); - // Handle being told to power off - powerFSM.add_transition(&stateBOOT, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateLS, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateNB, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateDARK, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateON, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + // Handle being told to power off + powerFSM.add_transition(&stateBOOT, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateLS, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateNB, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateDARK, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateON, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); + powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown"); - // Inputbroker - powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device"); - powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer + // Inputbroker + powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device"); + powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer - powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); - powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); + powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing"); - // if we are a router we don't turn the screen on for these things - if (!isRouter) { - // if any packet destined for phone arrives, turn on bluetooth at least - powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); + // if we are a router we don't turn the screen on for these things + if (!isRouter) { + // if any packet destined for phone arrives, turn on bluetooth at least + powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); - // Show the received text message - powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); - powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); - powerFSM.add_transition(&stateON, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); // restarts the sleep timer - } + // Show the received text message + powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); + powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); + powerFSM.add_transition(&stateDARK, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); + powerFSM.add_transition(&stateON, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); // restarts the sleep timer + } - // If we are not in statePOWER but get a serial connection, suppress sleep (and keep the screen on) while connected - powerFSM.add_transition(&stateLS, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateNB, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - powerFSM.add_transition(&statePOWER, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + // If we are not in statePOWER but get a serial connection, suppress sleep (and keep the screen on) while connected + powerFSM.add_transition(&stateLS, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateNB, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&statePOWER, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); - // If we get power connected, go to the power connect state - powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + // If we get power connected, go to the power connect state + powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); - // powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + // powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); - // the only way to leave state serial is for the client to disconnect (or we timeout and force disconnect them) - // when we leave, go to ON (which might not be the correct state if we have power connected, we will fix that in - // onEnter) - powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_SERIAL_DISCONNECTED, NULL, "serial disconnect"); + // the only way to leave state serial is for the client to disconnect (or we timeout and force disconnect them) + // when we leave, go to ON (which might not be the correct state if we have power connected, we will fix that in onEnter) + powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_SERIAL_DISCONNECTED, NULL, "serial disconnect"); - powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); + powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); #ifdef USE_EINK - // Allow E-Ink devices to suppress the screensaver, if screen timeout set to 0 - if (config.display.screen_on_secs > 0) + // Allow E-Ink devices to suppress the screensaver, if screen timeout set to 0 + if (config.display.screen_on_secs > 0) #endif - { - powerFSM.add_timed_transition(&stateON, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); - powerFSM.add_timed_transition(&statePOWER, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); - } + { + powerFSM.add_timed_transition(&stateON, &stateDARK, + Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + powerFSM.add_timed_transition(&statePOWER, &stateDARK, + Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + } // We never enter light-sleep or NB states on NRF52 (because the CPU uses so little power normally) #ifdef ARCH_ESP32 - // See: https://github.com/meshtastic/firmware/issues/1071 - // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be - // initiated through the modules + // See: https://github.com/meshtastic/firmware/issues/1071 + // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated + // through the modules #if HAS_WIFI && !defined(MESHTASTIC_EXCLUDE_WIFI) - bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || - config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; + bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; - if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { - powerFSM.add_timed_transition(&stateNB, &stateLS, Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, - "Min wake timeout"); + if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { + powerFSM.add_timed_transition(&stateNB, &stateLS, + Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, + "Min wake timeout"); - // If ESP32 and using power-saving, timer mover from DARK to light-sleep - // Also serves purpose of the old DARK to DARK transition(?) See https://github.com/meshtastic/firmware/issues/3517 - powerFSM.add_timed_transition(&stateDARK, &stateLS, - Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, - "Bluetooth timeout"); - } else { - // If ESP32, but not using power-saving, check periodically if config has drifted out of stateDark - powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); - } + // If ESP32 and using power-saving, timer mover from DARK to light-sleep + // Also serves purpose of the old DARK to DARK transition(?) See https://github.com/meshtastic/firmware/issues/3517 + powerFSM.add_timed_transition( + &stateDARK, &stateLS, + Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, + "Bluetooth timeout"); + } else { + // If ESP32, but not using power-saving, check periodically if config has drifted out of stateDark + powerFSM.add_timed_transition(&stateDARK, &stateDARK, + Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + } #endif // HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) #else // (not) ARCH_ESP32 - // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark - powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), - NULL, "Screen-on timeout"); + // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark + powerFSM.add_timed_transition(&stateDARK, &stateDARK, + Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, + "Screen-on timeout"); #endif - powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state + powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state } #endif diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 47725181b..182ac082a 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -17,8 +17,7 @@ #define EVENT_RECEIVED_MSG 5 // #define EVENT_BOOT 6 // now done with a timed transition #define EVENT_BLUETOOTH_PAIR 7 -// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on -// the screen +// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth #define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 @@ -30,19 +29,21 @@ #define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen #if MESHTASTIC_EXCLUDE_POWER_FSM -class FakeFsm { -public: - void trigger(int event) { - if (event == EVENT_SERIAL_CONNECTED) { - serialConnected = true; - } else if (event == EVENT_SERIAL_DISCONNECTED) { - serialConnected = false; - } - }; - bool getState() { return serialConnected; }; +class FakeFsm +{ + public: + void trigger(int event) + { + if (event == EVENT_SERIAL_CONNECTED) { + serialConnected = true; + } else if (event == EVENT_SERIAL_DISCONNECTED) { + serialConnected = false; + } + }; + bool getState() { return serialConnected; }; -private: - bool serialConnected = false; + private: + bool serialConnected = false; }; extern FakeFsm powerFSM; void PowerFSM_setup(); diff --git a/src/PowerFSMThread.h b/src/PowerFSMThread.h index 1696c8454..135f53298 100644 --- a/src/PowerFSMThread.h +++ b/src/PowerFSMThread.h @@ -6,36 +6,40 @@ #include "main.h" #include "power.h" -namespace concurrency { +namespace concurrency +{ /// Wrapper to convert our powerFSM stuff into a 'thread' -class PowerFSMThread : public OSThread { -public: - // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - PowerFSMThread() : OSThread("PowerFSM") {} +class PowerFSMThread : public OSThread +{ + public: + // callback returns the period for the next callback invocation (or 0 if we should no longer be called) + PowerFSMThread() : OSThread("PowerFSM") {} -protected: - int32_t runOnce() override { + protected: + int32_t runOnce() override + { #if !MESHTASTIC_EXCLUDE_POWER_FSM - powerFSM.run_machine(); + powerFSM.run_machine(); - /// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake - /// cpu for serial rx - FIXME) - const State *state = powerFSM.getState(); - canSleep = (state != &statePOWER) && (state != &stateSERIAL); + /// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake + /// cpu for serial rx - FIXME) + const State *state = powerFSM.getState(); + canSleep = (state != &statePOWER) && (state != &stateSERIAL); - if (powerStatus->getHasUSB()) { - timeLastPowered = millis(); - } else if (config.power.on_battery_shutdown_after_secs > 0 && config.power.on_battery_shutdown_after_secs != UINT32_MAX && - millis() > (timeLastPowered + - Default::getConfiguredOrDefaultMs(config.power.on_battery_shutdown_after_secs))) { // shutdown after 30 minutes unpowered - powerFSM.trigger(EVENT_SHUTDOWN); - } + if (powerStatus->getHasUSB()) { + timeLastPowered = millis(); + } else if (config.power.on_battery_shutdown_after_secs > 0 && config.power.on_battery_shutdown_after_secs != UINT32_MAX && + millis() > (timeLastPowered + + Default::getConfiguredOrDefaultMs( + config.power.on_battery_shutdown_after_secs))) { // shutdown after 30 minutes unpowered + powerFSM.trigger(EVENT_SHUTDOWN); + } - return 100; + return 100; #else - return INT32_MAX; + return INT32_MAX; #endif - } + } }; } // namespace concurrency \ No newline at end of file diff --git a/src/PowerMon.cpp b/src/PowerMon.cpp index 6cca5a626..38740b6ae 100644 --- a/src/PowerMon.cpp +++ b/src/PowerMon.cpp @@ -2,39 +2,46 @@ #include "NodeDB.h" // Use the 'live' config flag to figure out if we should be showing this message -bool PowerMon::is_power_enabled(uint64_t m) { - // FIXME: VERY STRANGE BUG: if I or in "force_enabled || " the flashed image on a rak4631 is not accepted by the - // bootloader as valid!!! Possibly a linker/gcc/bootloader bug somewhere? - return ((m & config.power.powermon_enables) ? true : false); +bool PowerMon::is_power_enabled(uint64_t m) +{ + // FIXME: VERY STRANGE BUG: if I or in "force_enabled || " the flashed image on a rak4631 is not accepted by the bootloader as + // valid!!! Possibly a linker/gcc/bootloader bug somewhere? + return ((m & config.power.powermon_enables) ? true : false); } -void PowerMon::setState(_meshtastic_PowerMon_State state, const char *reason) { +void PowerMon::setState(_meshtastic_PowerMon_State state, const char *reason) +{ #ifdef USE_POWERMON - auto oldstates = states; - states |= state; - if (oldstates != states && is_power_enabled(state)) { - emitLog(reason); - } + auto oldstates = states; + states |= state; + if (oldstates != states && is_power_enabled(state)) { + emitLog(reason); + } #endif } -void PowerMon::clearState(_meshtastic_PowerMon_State state, const char *reason) { +void PowerMon::clearState(_meshtastic_PowerMon_State state, const char *reason) +{ #ifdef USE_POWERMON - auto oldstates = states; - states &= ~state; - if (oldstates != states && is_power_enabled(state)) { - emitLog(reason); - } + auto oldstates = states; + states &= ~state; + if (oldstates != states && is_power_enabled(state)) { + emitLog(reason); + } #endif } -void PowerMon::emitLog(const char *reason) { +void PowerMon::emitLog(const char *reason) +{ #ifdef USE_POWERMON - // The nrf52 printf doesn't understand 64 bit ints, so if we ever reach that point this function will need to change. - LOG_INFO("S:PM:0x%08lx,%s", (uint32_t)states, reason); + // The nrf52 printf doesn't understand 64 bit ints, so if we ever reach that point this function will need to change. + LOG_INFO("S:PM:0x%08lx,%s", (uint32_t)states, reason); #endif } PowerMon *powerMon; -void powerMonInit() { powerMon = new PowerMon(); } \ No newline at end of file +void powerMonInit() +{ + powerMon = new PowerMon(); +} \ No newline at end of file diff --git a/src/PowerMon.h b/src/PowerMon.h index 6338db935..a19a6d010 100644 --- a/src/PowerMon.h +++ b/src/PowerMon.h @@ -13,29 +13,30 @@ * * For more information see the PowerMon docs. */ -class PowerMon { - uint64_t states = 0UL; +class PowerMon +{ + uint64_t states = 0UL; - friend class PowerStressModule; + friend class PowerStressModule; - /** - * If stress testing we always want all events logged - */ - bool force_enabled = false; + /** + * If stress testing we always want all events logged + */ + bool force_enabled = false; -public: - PowerMon() {} + public: + PowerMon() {} - // Mark entry/exit of a power consuming state - void setState(_meshtastic_PowerMon_State state, const char *reason = ""); - void clearState(_meshtastic_PowerMon_State state, const char *reason = ""); + // Mark entry/exit of a power consuming state + void setState(_meshtastic_PowerMon_State state, const char *reason = ""); + void clearState(_meshtastic_PowerMon_State state, const char *reason = ""); -private: - // Emit the coded log message - void emitLog(const char *reason); + private: + // Emit the coded log message + void emitLog(const char *reason); - // Use the 'live' config flag to figure out if we should be showing this message - bool is_power_enabled(uint64_t m); + // Use the 'live' config flag to figure out if we should be showing this message + bool is_power_enabled(uint64_t m); }; extern PowerMon *powerMon; diff --git a/src/PowerStatus.h b/src/PowerStatus.h index 799db32c5..fe4543e08 100644 --- a/src/PowerStatus.h +++ b/src/PowerStatus.h @@ -3,7 +3,8 @@ #include "configuration.h" #include -namespace meshtastic { +namespace meshtastic +{ /** * A boolean where we have a third state of Unknown @@ -11,84 +12,90 @@ namespace meshtastic { enum OptionalBool { OptFalse = 0, OptTrue = 1, OptUnknown = 2 }; /// Describes the state of the Power system. -class PowerStatus : public Status { +class PowerStatus : public Status +{ -private: - CallbackObserver statusObserver = - CallbackObserver(this, &PowerStatus::updateStatus); + private: + CallbackObserver statusObserver = + CallbackObserver(this, &PowerStatus::updateStatus); - /// Whether we have a battery connected - OptionalBool hasBattery = OptUnknown; - /// Battery voltage in mV, valid if haveBattery is true - int batteryVoltageMv = 0; - /// Battery charge percentage, either read directly or estimated - int8_t batteryChargePercent = 0; - /// Whether USB is connected - OptionalBool hasUSB = OptUnknown; - /// Whether we are charging the battery - OptionalBool isCharging = OptUnknown; + /// Whether we have a battery connected + OptionalBool hasBattery = OptUnknown; + /// Battery voltage in mV, valid if haveBattery is true + int batteryVoltageMv = 0; + /// Battery charge percentage, either read directly or estimated + int8_t batteryChargePercent = 0; + /// Whether USB is connected + OptionalBool hasUSB = OptUnknown; + /// Whether we are charging the battery + OptionalBool isCharging = OptUnknown; -public: - PowerStatus() { statusType = STATUS_TYPE_POWER; } - PowerStatus(OptionalBool hasBattery, OptionalBool hasUSB, OptionalBool isCharging, int batteryVoltageMv = -1, int8_t batteryChargePercent = 0) - : Status() { - this->hasBattery = hasBattery; - this->hasUSB = hasUSB; - this->isCharging = isCharging; - this->batteryVoltageMv = batteryVoltageMv; - this->batteryChargePercent = batteryChargePercent; - } - PowerStatus(const PowerStatus &); - PowerStatus &operator=(const PowerStatus &); - - void observe(Observable *source) { statusObserver.observe(source); } - - bool getHasBattery() const { return hasBattery == OptTrue; } - - bool getHasUSB() const { return hasUSB == OptTrue; } - - /// Can we even know if this board has USB power or not - bool knowsUSB() const { return hasUSB != OptUnknown; } - - bool getIsCharging() const { return isCharging == OptTrue; } - - int getBatteryVoltageMv() const { return batteryVoltageMv; } - - /** - * Note: for boards with battery pin or PMU, 0% battery means 'unknown/this board doesn't have a battery installed' - */ -#if defined(HAS_PMU) || defined(BATTERY_PIN) - uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 0; } -#endif - - /** - * Note: for boards without battery pin and PMU, 101% battery means 'the board is using external power' - */ -#if !defined(HAS_PMU) && !defined(BATTERY_PIN) - uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 101; } -#endif - - bool matches(const PowerStatus *newStatus) const { - return (newStatus->getHasBattery() != hasBattery || newStatus->getHasUSB() != hasUSB || newStatus->getBatteryVoltageMv() != batteryVoltageMv); - } - int updateStatus(const PowerStatus *newStatus) { - // Only update the status if values have actually changed - bool isDirty; + public: + PowerStatus() { statusType = STATUS_TYPE_POWER; } + PowerStatus(OptionalBool hasBattery, OptionalBool hasUSB, OptionalBool isCharging, int batteryVoltageMv = -1, + int8_t batteryChargePercent = 0) + : Status() { - isDirty = matches(newStatus); - initialized = true; - hasBattery = newStatus->hasBattery; - batteryVoltageMv = newStatus->getBatteryVoltageMv(); - batteryChargePercent = newStatus->getBatteryChargePercent(); - hasUSB = newStatus->hasUSB; - isCharging = newStatus->isCharging; + this->hasBattery = hasBattery; + this->hasUSB = hasUSB; + this->isCharging = isCharging; + this->batteryVoltageMv = batteryVoltageMv; + this->batteryChargePercent = batteryChargePercent; } - if (isDirty) { - // LOG_DEBUG("Battery %dmV %d%%", batteryVoltageMv, batteryChargePercent); - onNewStatus.notifyObservers(this); + PowerStatus(const PowerStatus &); + PowerStatus &operator=(const PowerStatus &); + + void observe(Observable *source) { statusObserver.observe(source); } + + bool getHasBattery() const { return hasBattery == OptTrue; } + + bool getHasUSB() const { return hasUSB == OptTrue; } + + /// Can we even know if this board has USB power or not + bool knowsUSB() const { return hasUSB != OptUnknown; } + + bool getIsCharging() const { return isCharging == OptTrue; } + + int getBatteryVoltageMv() const { return batteryVoltageMv; } + + /** + * Note: for boards with battery pin or PMU, 0% battery means 'unknown/this board doesn't have a battery installed' + */ +#if defined(HAS_PMU) || defined(BATTERY_PIN) + uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 0; } +#endif + + /** + * Note: for boards without battery pin and PMU, 101% battery means 'the board is using external power' + */ +#if !defined(HAS_PMU) && !defined(BATTERY_PIN) + uint8_t getBatteryChargePercent() const { return getHasBattery() ? batteryChargePercent : 101; } +#endif + + bool matches(const PowerStatus *newStatus) const + { + return (newStatus->getHasBattery() != hasBattery || newStatus->getHasUSB() != hasUSB || + newStatus->getBatteryVoltageMv() != batteryVoltageMv); + } + int updateStatus(const PowerStatus *newStatus) + { + // Only update the status if values have actually changed + bool isDirty; + { + isDirty = matches(newStatus); + initialized = true; + hasBattery = newStatus->hasBattery; + batteryVoltageMv = newStatus->getBatteryVoltageMv(); + batteryChargePercent = newStatus->getBatteryChargePercent(); + hasUSB = newStatus->hasUSB; + isCharging = newStatus->isCharging; + } + if (isDirty) { + // LOG_DEBUG("Battery %dmV %d%%", batteryVoltageMv, batteryChargePercent); + onNewStatus.notifyObservers(this); + } + return 0; } - return 0; - } }; } // namespace meshtastic diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index fcc1fdc37..895dcb147 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -20,376 +20,387 @@ #if HAS_NETWORKING extern Syslog syslog; #endif -void RedirectablePrint::rpInit() { +void RedirectablePrint::rpInit() +{ #ifdef HAS_FREE_RTOS - inDebugPrint = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace); + inDebugPrint = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace); #endif } -void RedirectablePrint::setDestination(Print *_dest) { - assert(_dest); - dest = _dest; +void RedirectablePrint::setDestination(Print *_dest) +{ + assert(_dest); + dest = _dest; } -size_t RedirectablePrint::write(uint8_t c) { - // Always send the characters to our segger JTAG debugger +size_t RedirectablePrint::write(uint8_t c) +{ + // Always send the characters to our segger JTAG debugger #ifdef USE_SEGGER - SEGGER_RTT_PutChar(SEGGER_STDOUT_CH, c); + SEGGER_RTT_PutChar(SEGGER_STDOUT_CH, c); #endif - // Account for legacy config transition - bool serialEnabled = config.has_security ? config.security.serial_enabled : config.device.serial_enabled; - if (!config.has_lora || serialEnabled) - dest->write(c); + // Account for legacy config transition + bool serialEnabled = config.has_security ? config.security.serial_enabled : config.device.serial_enabled; + if (!config.has_lora || serialEnabled) + dest->write(c); - return 1; // We always claim one was written, rather than trusting what the - // serial port said (which could be zero) + return 1; // We always claim one was written, rather than trusting what the + // serial port said (which could be zero) } -size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg) { - va_list copy; +size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg) +{ + va_list copy; #if ENABLE_JSON_LOGGING || ARCH_PORTDUINO - static char printBuf[512]; + static char printBuf[512]; #else - static char printBuf[160]; + static char printBuf[160]; #endif #ifdef ARCH_PORTDUINO - bool color = !portduino_config.ascii_logs; + bool color = !portduino_config.ascii_logs; #else - bool color = true; + bool color = true; #endif - va_copy(copy, arg); - size_t len = vsnprintf(printBuf, sizeof(printBuf), format, copy); - va_end(copy); + va_copy(copy, arg); + size_t len = vsnprintf(printBuf, sizeof(printBuf), format, copy); + va_end(copy); - // If the resulting string is longer than sizeof(printBuf)-1 characters, the remaining characters are still counted - // for the return value + // If the resulting string is longer than sizeof(printBuf)-1 characters, the remaining characters are still counted for the + // return value - if (len > sizeof(printBuf) - 1) { - len = sizeof(printBuf) - 1; - printBuf[sizeof(printBuf) - 2] = '\n'; - } - for (size_t f = 0; f < len; f++) { - if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') - printBuf[f] = '#'; - } - if (color && logLevel != nullptr) { - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - Print::write("\u001b[34m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - Print::write("\u001b[32m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - Print::write("\u001b[33m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) - Print::write("\u001b[31m", 5); - } - len = Print::write(printBuf, len); - if (color && logLevel != nullptr) { - Print::write("\u001b[0m", 4); - } - return len; + if (len > sizeof(printBuf) - 1) { + len = sizeof(printBuf) - 1; + printBuf[sizeof(printBuf) - 2] = '\n'; + } + for (size_t f = 0; f < len; f++) { + if (!std::isprint(static_cast(printBuf[f])) && printBuf[f] != '\n') + printBuf[f] = '#'; + } + if (color && logLevel != nullptr) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 5); + } + len = Print::write(printBuf, len); + if (color && logLevel != nullptr) { + Print::write("\u001b[0m", 4); + } + return len; } -void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) { - size_t r = 0; +void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) +{ + size_t r = 0; #ifdef ARCH_PORTDUINO - bool color = !portduino_config.ascii_logs; + bool color = !portduino_config.ascii_logs; #else - bool color = true; + bool color = true; #endif - // include the header - if (color) { - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) - Print::write("\u001b[34m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) - Print::write("\u001b[32m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) - Print::write("\u001b[33m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) - Print::write("\u001b[31m", 5); - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) - Print::write("\u001b[35m", 5); - } + // include the header + if (color) { + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) + Print::write("\u001b[34m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) + Print::write("\u001b[32m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) + Print::write("\u001b[33m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) + Print::write("\u001b[31m", 5); + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) + Print::write("\u001b[35m", 5); + } - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN #ifdef ARCH_PORTDUINO - ::printf("%s ", logLevel); - if (color) { - ::printf("\u001b[0m"); - } - ::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); + ::printf("%s ", logLevel); + if (color) { + ::printf("\u001b[0m"); + } + ::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); #else - printf("%s ", logLevel); - if (color) { - printf("\u001b[0m"); - } - printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); + printf("%s ", logLevel); + if (color) { + printf("\u001b[0m"); + } + printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); #endif - } else { + } else { #ifdef ARCH_PORTDUINO - ::printf("%s ", logLevel); - if (color) { - ::printf("\u001b[0m"); - } - ::printf("| ??:??:?? %u ", millis() / 1000); + ::printf("%s ", logLevel); + if (color) { + ::printf("\u001b[0m"); + } + ::printf("| ??:??:?? %u ", millis() / 1000); #else - printf("%s ", logLevel); - if (color) { - printf("\u001b[0m"); - } - printf("| ??:??:?? %u ", millis() / 1000); + printf("%s ", logLevel); + if (color) { + printf("\u001b[0m"); + } + printf("| ??:??:?? %u ", millis() / 1000); #endif - } - auto thread = concurrency::OSThread::currentThread; - if (thread) { - print("["); - // printf("%p ", thread); - // assert(thread->ThreadName.length()); - print(thread->ThreadName); - print("] "); - } - -#ifdef DEBUG_HEAP - // Add heap free space bytes prefix before every log message -#ifdef ARCH_PORTDUINO - ::printf("[heap %u] ", memGet.getFreeHeap()); -#else - printf("[heap %u] ", memGet.getFreeHeap()); -#endif -#endif // DEBUG_HEAP - - r += vprintf(logLevel, format, arg); -} - -void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) { -#if HAS_NETWORKING && !defined(ARCH_PORTDUINO) - // if syslog is in use, collect the log messages and send them to syslog - if (syslog.isEnabled()) { - int ll = 0; - switch (logLevel[0]) { - case 'D': - ll = SYSLOG_DEBUG; - break; - case 'I': - ll = SYSLOG_INFO; - break; - case 'W': - ll = SYSLOG_WARN; - break; - case 'E': - ll = SYSLOG_ERR; - break; - case 'C': - ll = SYSLOG_CRIT; - break; - default: - ll = 0; } auto thread = concurrency::OSThread::currentThread; if (thread) { - syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); - } else { - syslog.vlogf(ll, format, arg); + print("["); + // printf("%p ", thread); + // assert(thread->ThreadName.length()); + print(thread->ThreadName); + print("] "); } - } -#endif -} -void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_list arg) { -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) { - bool isBleConnected = false; -#ifdef ARCH_ESP32 - isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); -#elif defined(ARCH_NRF52) - isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); -#endif - if (isBleConnected) { - char *message; - size_t initialLen; - size_t len; - initialLen = strlen(format); - message = new char[initialLen + 1]; - len = vsnprintf(message, initialLen + 1, format, arg); - if (len > initialLen) { - delete[] message; - message = new char[len + 1]; - vsnprintf(message, len + 1, format, arg); - } - auto thread = concurrency::OSThread::currentThread; - meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; - logRecord.level = getLogLevel(logLevel); - strcpy(logRecord.message, message); - if (thread) - strcpy(logRecord.source, thread->ThreadName.c_str()); - logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); - - uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size]; - size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); -#ifdef ARCH_ESP32 - nimbleBluetooth->sendLog(buffer, size); -#elif defined(ARCH_NRF52) - nrf52Bluetooth->sendLog(buffer, size); -#endif - delete[] message; - delete[] buffer; - } - } +#ifdef DEBUG_HEAP + // Add heap free space bytes prefix before every log message +#ifdef ARCH_PORTDUINO + ::printf("[heap %u] ", memGet.getFreeHeap()); #else - (void)logLevel; - (void)format; - (void)arg; + printf("[heap %u] ", memGet.getFreeHeap()); +#endif +#endif // DEBUG_HEAP + + r += vprintf(logLevel, format, arg); +} + +void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) +{ +#if HAS_NETWORKING && !defined(ARCH_PORTDUINO) + // if syslog is in use, collect the log messages and send them to syslog + if (syslog.isEnabled()) { + int ll = 0; + switch (logLevel[0]) { + case 'D': + ll = SYSLOG_DEBUG; + break; + case 'I': + ll = SYSLOG_INFO; + break; + case 'W': + ll = SYSLOG_WARN; + break; + case 'E': + ll = SYSLOG_ERR; + break; + case 'C': + ll = SYSLOG_CRIT; + break; + default: + ll = 0; + } + auto thread = concurrency::OSThread::currentThread; + if (thread) { + syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); + } else { + syslog.vlogf(ll, format, arg); + } + } #endif } -meshtastic_LogRecord_Level RedirectablePrint::getLogLevel(const char *logLevel) { - meshtastic_LogRecord_Level ll = meshtastic_LogRecord_Level_UNSET; // default to unset - switch (logLevel[0]) { - case 'D': - ll = meshtastic_LogRecord_Level_DEBUG; - break; - case 'I': - ll = meshtastic_LogRecord_Level_INFO; - break; - case 'W': - ll = meshtastic_LogRecord_Level_WARNING; - break; - case 'E': - ll = meshtastic_LogRecord_Level_ERROR; - break; - case 'C': - ll = meshtastic_LogRecord_Level_CRITICAL; - break; - } - return ll; +void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_list arg) +{ +#if !MESHTASTIC_EXCLUDE_BLUETOOTH + if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) { + bool isBleConnected = false; +#ifdef ARCH_ESP32 + isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); +#elif defined(ARCH_NRF52) + isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); +#endif + if (isBleConnected) { + char *message; + size_t initialLen; + size_t len; + initialLen = strlen(format); + message = new char[initialLen + 1]; + len = vsnprintf(message, initialLen + 1, format, arg); + if (len > initialLen) { + delete[] message; + message = new char[len + 1]; + vsnprintf(message, len + 1, format, arg); + } + auto thread = concurrency::OSThread::currentThread; + meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; + logRecord.level = getLogLevel(logLevel); + strcpy(logRecord.message, message); + if (thread) + strcpy(logRecord.source, thread->ThreadName.c_str()); + logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); + + uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size]; + size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); +#ifdef ARCH_ESP32 + nimbleBluetooth->sendLog(buffer, size); +#elif defined(ARCH_NRF52) + nrf52Bluetooth->sendLog(buffer, size); +#endif + delete[] message; + delete[] buffer; + } + } +#else + (void)logLevel; + (void)format; + (void)arg; +#endif } -void RedirectablePrint::log(const char *logLevel, const char *format, ...) { +meshtastic_LogRecord_Level RedirectablePrint::getLogLevel(const char *logLevel) +{ + meshtastic_LogRecord_Level ll = meshtastic_LogRecord_Level_UNSET; // default to unset + switch (logLevel[0]) { + case 'D': + ll = meshtastic_LogRecord_Level_DEBUG; + break; + case 'I': + ll = meshtastic_LogRecord_Level_INFO; + break; + case 'W': + ll = meshtastic_LogRecord_Level_WARNING; + break; + case 'E': + ll = meshtastic_LogRecord_Level_ERROR; + break; + case 'C': + ll = meshtastic_LogRecord_Level_CRITICAL; + break; + } + return ll; +} - // append \n to format - size_t len = strlen(format); - char *newFormat = new char[len + 2]; - strcpy(newFormat, format); - newFormat[len] = '\n'; - newFormat[len + 1] = '\0'; +void RedirectablePrint::log(const char *logLevel, const char *format, ...) +{ + + // append \n to format + size_t len = strlen(format); + char *newFormat = new char[len + 2]; + strcpy(newFormat, format); + newFormat[len] = '\n'; + newFormat[len + 1] = '\0'; #if ARCH_PORTDUINO - // level trace is special, two possible ways to handle it. - if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - if (portduino_config.traceFilename != "") { - va_list arg; - va_start(arg, format); - try { - traceFile << va_arg(arg, char *) << std::endl; - } catch (const std::ios_base::failure &e) { - } - va_end(arg); + // level trace is special, two possible ways to handle it. + if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + if (portduino_config.traceFilename != "") { + va_list arg; + va_start(arg, format); + try { + traceFile << va_arg(arg, char *) << std::endl; + } catch (const std::ios_base::failure &e) { + } + va_end(arg); + } + if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + delete[] newFormat; + return; + } } - if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - delete[] newFormat; - return; + if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + delete[] newFormat; + return; + } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { + delete[] newFormat; + return; + } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { + delete[] newFormat; + return; } - } - if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; - return; - } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { - delete[] newFormat; - return; - } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { - delete[] newFormat; - return; - } #endif - if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; - return; - } + if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + delete[] newFormat; + return; + } #ifdef HAS_FREE_RTOS - if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { + if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { #else - if (!inDebugPrint) { - inDebugPrint = true; + if (!inDebugPrint) { + inDebugPrint = true; #endif - va_list arg; - va_start(arg, format); + va_list arg; + va_start(arg, format); - log_to_serial(logLevel, newFormat, arg); - log_to_syslog(logLevel, newFormat, arg); - log_to_ble(logLevel, newFormat, arg); + log_to_serial(logLevel, newFormat, arg); + log_to_syslog(logLevel, newFormat, arg); + log_to_ble(logLevel, newFormat, arg); - va_end(arg); + va_end(arg); #ifdef HAS_FREE_RTOS - xSemaphoreGive(inDebugPrint); + xSemaphoreGive(inDebugPrint); #else - inDebugPrint = false; + inDebugPrint = false; #endif - } + } - delete[] newFormat; - return; + delete[] newFormat; + return; } -void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) { - const char alphabet[17] = "0123456789abcdef"; - log(logLevel, " +------------------------------------------------+ +----------------+"); - log(logLevel, " |.0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .a .b .c .d .e .f | | ASCII |"); - for (uint16_t i = 0; i < len; i += 16) { - if (i % 128 == 0) - log(logLevel, " +------------------------------------------------+ +----------------+"); - char s[] = " | | | |\n"; - uint8_t ix = 5, iy = 56; - for (uint8_t j = 0; j < 16; j++) { - if (i + j < len) { - uint8_t c = buf[i + j]; - s[ix++] = alphabet[(c >> 4) & 0x0F]; - s[ix++] = alphabet[c & 0x0F]; - ix++; - if (c > 31 && c < 128) - s[iy++] = c; +void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) +{ + const char alphabet[17] = "0123456789abcdef"; + log(logLevel, " +------------------------------------------------+ +----------------+"); + log(logLevel, " |.0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .a .b .c .d .e .f | | ASCII |"); + for (uint16_t i = 0; i < len; i += 16) { + if (i % 128 == 0) + log(logLevel, " +------------------------------------------------+ +----------------+"); + char s[] = " | | | |\n"; + uint8_t ix = 5, iy = 56; + for (uint8_t j = 0; j < 16; j++) { + if (i + j < len) { + uint8_t c = buf[i + j]; + s[ix++] = alphabet[(c >> 4) & 0x0F]; + s[ix++] = alphabet[c & 0x0F]; + ix++; + if (c > 31 && c < 128) + s[iy++] = c; + else + s[iy++] = '.'; + } + } + uint8_t index = i / 16; + sprintf(s, "%03x", index); + s[3] = '.'; + log(logLevel, s); + } + log(logLevel, " +------------------------------------------------+ +----------------+"); +} + +std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) +{ + int n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */ + std::unique_ptr formatted; + va_list ap; + while (1) { + formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */ + strcpy(&formatted[0], fmt_str.c_str()); + va_start(ap, fmt_str); + int final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap); + va_end(ap); + if (final_n < 0 || final_n >= n) + n += abs(final_n - n + 1); else - s[iy++] = '.'; - } + break; } - uint8_t index = i / 16; - sprintf(s, "%03x", index); - s[3] = '.'; - log(logLevel, s); - } - log(logLevel, " +------------------------------------------------+ +----------------+"); -} - -std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) { - int n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */ - std::unique_ptr formatted; - va_list ap; - while (1) { - formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */ - strcpy(&formatted[0], fmt_str.c_str()); - va_start(ap, fmt_str); - int final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap); - va_end(ap); - if (final_n < 0 || final_n >= n) - n += abs(final_n - n + 1); - else - break; - } - return std::string(formatted.get()); + return std::string(formatted.get()); } diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 44e418c74..45b62b7af 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -11,48 +11,49 @@ * This class is mostly useful to allow debug printing to be redirected away from Serial * to some other transport if we switch Serial usage (on the fly) to some other purpose. */ -class RedirectablePrint : public Print { - Print *dest; +class RedirectablePrint : public Print +{ + Print *dest; #ifdef HAS_FREE_RTOS - SemaphoreHandle_t inDebugPrint = nullptr; - StaticSemaphore_t _MutexStorageSpace; + SemaphoreHandle_t inDebugPrint = nullptr; + StaticSemaphore_t _MutexStorageSpace; #else - volatile bool inDebugPrint = false; + volatile bool inDebugPrint = false; #endif -public: - explicit RedirectablePrint(Print *_dest) : dest(_dest) {} + public: + explicit RedirectablePrint(Print *_dest) : dest(_dest) {} - /** - * Set a new destination - */ - void rpInit(); - void setDestination(Print *dest); + /** + * Set a new destination + */ + void rpInit(); + void setDestination(Print *dest); - virtual size_t write(uint8_t c); + virtual size_t write(uint8_t c); - /** - * Debug logging print message - * - * If the provide format string ends with a newline we assume it is the final print of a single - * log message. Otherwise we assume more prints will come before the log message ends. This - * allows you to call logDebug a few times to build up a single log message line if you wish. - */ - void log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); + /** + * Debug logging print message + * + * If the provide format string ends with a newline we assume it is the final print of a single + * log message. Otherwise we assume more prints will come before the log message ends. This + * allows you to call logDebug a few times to build up a single log message line if you wish. + */ + void log(const char *logLevel, const char *format, ...) __attribute__((format(printf, 3, 4))); - /** like printf but va_list based */ - size_t vprintf(const char *logLevel, const char *format, va_list arg); + /** like printf but va_list based */ + size_t vprintf(const char *logLevel, const char *format, va_list arg); - void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); + void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); - std::string mt_sprintf(const std::string fmt_str, ...); + std::string mt_sprintf(const std::string fmt_str, ...); -protected: - /// Subclasses can override if they need to change how we format over the serial port - virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); - meshtastic_LogRecord_Level getLogLevel(const char *logLevel); + protected: + /// Subclasses can override if they need to change how we format over the serial port + virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); + meshtastic_LogRecord_Level getLogLevel(const char *logLevel); -private: - void log_to_syslog(const char *logLevel, const char *format, va_list arg); - void log_to_ble(const char *logLevel, const char *format, va_list arg); + private: + void log_to_syslog(const char *logLevel, const char *format, va_list arg); + void log_to_ble(const char *logLevel, const char *format, va_list arg); }; \ No newline at end of file diff --git a/src/SPILock.cpp b/src/SPILock.cpp index 0a0e0fbfd..13fa556fc 100644 --- a/src/SPILock.cpp +++ b/src/SPILock.cpp @@ -5,7 +5,8 @@ concurrency::Lock *spiLock; -void initSPI() { - assert(!spiLock); - spiLock = new concurrency::Lock(); +void initSPI() +{ + assert(!spiLock); + spiLock = new concurrency::Lock(); } \ No newline at end of file diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp index 3a7face87..45b96ad07 100644 --- a/src/SafeFile.cpp +++ b/src/SafeFile.cpp @@ -3,48 +3,54 @@ #ifdef FSCom // Only way to work on both esp32 and nrf52 -static File openFile(const char *filename, bool fullAtomic) { - concurrency::LockGuard g(spiLock); - LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); +static File openFile(const char *filename, bool fullAtomic) +{ + concurrency::LockGuard g(spiLock); + LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); #ifdef ARCH_NRF52 - FSCom.remove(filename); - return FSCom.open(filename, FILE_O_WRITE); + FSCom.remove(filename); + return FSCom.open(filename, FILE_O_WRITE); #endif - if (!fullAtomic) { - FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) - } + if (!fullAtomic) { + FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) + } - String filenameTmp = filename; - filenameTmp += ".tmp"; + String filenameTmp = filename; + filenameTmp += ".tmp"; - // FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now - // if (fullAtomic) { - // FSCom.remove(filename); - // } + // FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now + // if (fullAtomic) { + // FSCom.remove(filename); + // } - // clear any previous LFS errors - return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE); + // clear any previous LFS errors + return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE); } -SafeFile::SafeFile(const char *_filename, bool fullAtomic) : filename(_filename), f(openFile(_filename, fullAtomic)), fullAtomic(fullAtomic) {} - -size_t SafeFile::write(uint8_t ch) { - if (!f) - return 0; - - hash ^= ch; - return f.write(ch); +SafeFile::SafeFile(const char *_filename, bool fullAtomic) + : filename(_filename), f(openFile(_filename, fullAtomic)), fullAtomic(fullAtomic) +{ } -size_t SafeFile::write(const uint8_t *buffer, size_t size) { - if (!f) - return 0; +size_t SafeFile::write(uint8_t ch) +{ + if (!f) + return 0; - for (size_t i = 0; i < size; i++) { - hash ^= buffer[i]; - } - return f.write((uint8_t const *)buffer, size); // This nasty cast is _IMPORTANT_ otherwise the correct adafruit method - // does not get used (they made a mistake in their typing) + hash ^= ch; + return f.write(ch); +} + +size_t SafeFile::write(const uint8_t *buffer, size_t size) +{ + if (!f) + return 0; + + for (size_t i = 0; i < size; i++) { + hash ^= buffer[i]; + } + return f.write((uint8_t const *)buffer, size); // This nasty cast is _IMPORTANT_ otherwise the correct adafruit method does + // not get used (they made a mistake in their typing) } /** @@ -52,64 +58,66 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size) { * * @return false for failure */ -bool SafeFile::close() { - if (!f) - return false; +bool SafeFile::close() +{ + if (!f) + return false; - spiLock->lock(); - f.close(); - spiLock->unlock(); + spiLock->lock(); + f.close(); + spiLock->unlock(); #ifdef ARCH_NRF52 - return true; + return true; #endif - if (!testReadback()) - return false; + if (!testReadback()) + return false; - { // Scope for lock - concurrency::LockGuard g(spiLock); - // brief window of risk here ;-) - if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) { - LOG_ERROR("Can't remove old pref file"); - return false; + { // Scope for lock + concurrency::LockGuard g(spiLock); + // brief window of risk here ;-) + if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) { + LOG_ERROR("Can't remove old pref file"); + return false; + } } - } - String filenameTmp = filename; - filenameTmp += ".tmp"; - if (!renameFile(filenameTmp.c_str(), filename.c_str())) { - LOG_ERROR("Error: can't rename new pref file"); - return false; - } + String filenameTmp = filename; + filenameTmp += ".tmp"; + if (!renameFile(filenameTmp.c_str(), filename.c_str())) { + LOG_ERROR("Error: can't rename new pref file"); + return false; + } - return true; + return true; } /// Read our (closed) tempfile back in and compare the hash -bool SafeFile::testReadback() { - concurrency::LockGuard g(spiLock); +bool SafeFile::testReadback() +{ + concurrency::LockGuard g(spiLock); - String filenameTmp = filename; - filenameTmp += ".tmp"; - auto f2 = FSCom.open(filenameTmp.c_str(), FILE_O_READ); - if (!f2) { - LOG_ERROR("Can't open tmp file for readback"); - return false; - } + String filenameTmp = filename; + filenameTmp += ".tmp"; + auto f2 = FSCom.open(filenameTmp.c_str(), FILE_O_READ); + if (!f2) { + LOG_ERROR("Can't open tmp file for readback"); + return false; + } - int c = 0; - uint8_t test_hash = 0; - while ((c = f2.read()) >= 0) { - test_hash ^= (uint8_t)c; - } - f2.close(); + int c = 0; + uint8_t test_hash = 0; + while ((c = f2.read()) >= 0) { + test_hash ^= (uint8_t)c; + } + f2.close(); - if (test_hash != hash) { - LOG_ERROR("Readback failed hash mismatch"); - return false; - } + if (test_hash != hash) { + LOG_ERROR("Readback failed hash mismatch"); + return false; + } - return true; + return true; } #endif \ No newline at end of file diff --git a/src/SafeFile.h b/src/SafeFile.h index a03b3b4f9..3d0f81cad 100644 --- a/src/SafeFile.h +++ b/src/SafeFile.h @@ -10,41 +10,41 @@ * This class provides 'safe'/paranoid file writing. * * Some of our filesystems (in particular the nrf52) may have bugs beneath our layer. Therefore we want to - * be very careful about how we write files. This class provides a restricted (Stream only) writing API for writing to - * files. + * be very careful about how we write files. This class provides a restricted (Stream only) writing API for writing to files. * * Notably: * - we keep a simple xor hash of all characters that were written. * - We do not allow seeking (because we want to maintain our hash) - * - we provide an close() method which is similar to close but returns false if we were unable to successfully write - * the file. Also this method - * - atomically replaces any old version of the file on the disk with our new file (after first rereading the file from - * the disk to confirm the hash matches) - * - Some files are super huge so we can't do the full atomic rename/copy (because of filesystem size limits). If - * !fullAtomic then we still do the readback to verify file is valid so higher level code can handle failures. + * - we provide an close() method which is similar to close but returns false if we were unable to successfully write the + * file. Also this method + * - atomically replaces any old version of the file on the disk with our new file (after first rereading the file from the disk + * to confirm the hash matches) + * - Some files are super huge so we can't do the full atomic rename/copy (because of filesystem size limits). If !fullAtomic + * then we still do the readback to verify file is valid so higher level code can handle failures. */ -class SafeFile : public Print { -public: - explicit SafeFile(char const *filepath, bool fullAtomic = false); +class SafeFile : public Print +{ + public: + explicit SafeFile(char const *filepath, bool fullAtomic = false); - virtual size_t write(uint8_t); - virtual size_t write(const uint8_t *buffer, size_t size); + virtual size_t write(uint8_t); + virtual size_t write(const uint8_t *buffer, size_t size); - /** - * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches - * - * @return false for failure - */ - bool close(); + /** + * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches + * + * @return false for failure + */ + bool close(); -private: - /// Read our (closed) tempfile back in and compare the hash - bool testReadback(); + private: + /// Read our (closed) tempfile back in and compare the hash + bool testReadback(); - String filename; - File f; - bool fullAtomic; - uint8_t hash = 0; + String filename; + File f; + bool fullAtomic; + uint8_t hash = 0; }; #endif \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 268e3f19e..dd2acb599 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -28,105 +28,121 @@ SerialConsole *console; -void consoleInit() { - auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread +void consoleInit() +{ + auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread #if defined(SERIAL_HAS_ON_RECEIVE) - // onReceive does only exist for HardwareSerial not for USB CDC serial - Port.onReceive([sc]() { sc->rxInt(); }); + // onReceive does only exist for HardwareSerial not for USB CDC serial + Port.onReceive([sc]() { sc->rxInt(); }); #endif - DEBUG_PORT.rpInit(); // Simply sets up semaphore + DEBUG_PORT.rpInit(); // Simply sets up semaphore } -void consolePrintf(const char *format, ...) { - va_list arg; - va_start(arg, format); - console->vprintf(nullptr, format, arg); - va_end(arg); - console->flush(); +void consolePrintf(const char *format, ...) +{ + va_list arg; + va_start(arg, format); + console->vprintf(nullptr, format, arg); + va_end(arg); + console->flush(); } -SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole") { - api_type = TYPE_SERIAL; - assert(!console); - console = this; - canWrite = false; // We don't send packets to our port until it has talked to us first +SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole") +{ + api_type = TYPE_SERIAL; + assert(!console); + console = this; + canWrite = false; // We don't send packets to our port until it has talked to us first #ifdef RP2040_SLOW_CLOCK - Port.setTX(SERIAL2_TX); - Port.setRX(SERIAL2_RX); + Port.setTX(SERIAL2_TX); + Port.setRX(SERIAL2_RX); #endif - Port.begin(SERIAL_BAUD); -#if defined(ARCH_NRF52) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(ARCH_RP2040) || \ + Port.begin(SERIAL_BAUD); +#if defined(ARCH_NRF52) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(ARCH_RP2040) || \ defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) - time_t timeout = millis(); - while (!Port) { - if (Throttle::isWithinTimespanMs(timeout, FIVE_SECONDS_MS)) { - delay(100); - } else { - break; + time_t timeout = millis(); + while (!Port) { + if (Throttle::isWithinTimespanMs(timeout, FIVE_SECONDS_MS)) { + delay(100); + } else { + break; + } } - } #endif #if !ARCH_PORTDUINO - emitRebooted(); + emitRebooted(); #endif } -int32_t SerialConsole::runOnce() { +int32_t SerialConsole::runOnce() +{ #ifdef HELTEC_MESH_SOLAR - // After enabling the mesh solar serial port module configuration, command processing is handled by the serial port - // module. - if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port && - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) { - return 250; - } + // After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. + if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port && + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) { + return 250; + } #endif - int32_t delay = runOncePart(); + int32_t delay = runOncePart(); #if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2) - return Port.available() ? delay : INT32_MAX; + return Port.available() ? delay : INT32_MAX; #elif defined(IS_USB_SERIAL) - return HWCDC::isPlugged() ? delay : (1000 * 20); + return HWCDC::isPlugged() ? delay : (1000 * 20); #else - return delay; + return delay; #endif } -void SerialConsole::flush() { Port.flush(); } +void SerialConsole::flush() +{ + Port.flush(); +} // trigger tx of serial data -void SerialConsole::onNowHasData(uint32_t fromRadioNum) { setIntervalFromNow(0); } +void SerialConsole::onNowHasData(uint32_t fromRadioNum) +{ + setIntervalFromNow(0); +} // trigger rx of serial data -void SerialConsole::rxInt() { setIntervalFromNow(0); } +void SerialConsole::rxInt() +{ + setIntervalFromNow(0); +} -// For the serial port we can't really detect if any client is on the other side, so instead just look for recent -// messages -bool SerialConsole::checkIsConnected() { return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); } +// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages +bool SerialConsole::checkIsConnected() +{ + return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); +} /** * we override this to notice when we've received a protobuf over the serial * stream. Then we shut off debug serial output. */ -bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len) { - // only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled. - if (config.has_lora && config.security.serial_enabled) { - // Switch to protobufs for log messages - usingProtobufs = true; - canWrite = true; +bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len) +{ + // only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled. + if (config.has_lora && config.security.serial_enabled) { + // Switch to protobufs for log messages + usingProtobufs = true; + canWrite = true; - return StreamAPI::handleToRadio(buf, len); - } else { - return false; - } + return StreamAPI::handleToRadio(buf, len); + } else { + return false; + } } -void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg) { - if (usingProtobufs && config.security.debug_log_api_enabled) { - meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel); - auto thread = concurrency::OSThread::currentThread; - emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg); - } else - RedirectablePrint::log_to_serial(logLevel, format, arg); +void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg) +{ + if (usingProtobufs && config.security.debug_log_api_enabled) { + meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel); + auto thread = concurrency::OSThread::currentThread; + emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg); + } else + RedirectablePrint::log_to_serial(logLevel, format, arg); } \ No newline at end of file diff --git a/src/SerialConsole.h b/src/SerialConsole.h index e920ee80d..98577e4bc 100644 --- a/src/SerialConsole.h +++ b/src/SerialConsole.h @@ -6,40 +6,42 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -class SerialConsole : public StreamAPI, public RedirectablePrint, private concurrency::OSThread { - /** - * If true we are talking to a smart host and all messages (including log messages) must be framed as protobufs. - */ - bool usingProtobufs = false; +class SerialConsole : public StreamAPI, public RedirectablePrint, private concurrency::OSThread +{ + /** + * If true we are talking to a smart host and all messages (including log messages) must be framed as protobufs. + */ + bool usingProtobufs = false; -public: - SerialConsole(); + public: + SerialConsole(); - /** - * we override this to notice when we've received a protobuf over the serial stream. Then we shunt off - * debug serial output. - */ - virtual bool handleToRadio(const uint8_t *buf, size_t len) override; + /** + * we override this to notice when we've received a protobuf over the serial stream. Then we shunt off + * debug serial output. + */ + virtual bool handleToRadio(const uint8_t *buf, size_t len) override; - virtual size_t write(uint8_t c) override { - if (c == '\n') // prefix any newlines with carriage return - RedirectablePrint::write('\r'); - return RedirectablePrint::write(c); - } + virtual size_t write(uint8_t c) override + { + if (c == '\n') // prefix any newlines with carriage return + RedirectablePrint::write('\r'); + return RedirectablePrint::write(c); + } - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - void flush(); - void rxInt(); + void flush(); + void rxInt(); -protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; + protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; - virtual void onNowHasData(uint32_t fromRadioNum) override; + virtual void onNowHasData(uint32_t fromRadioNum) override; - /// Possibly switch to protobufs if we see a valid protobuf message - virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); + /// Possibly switch to protobufs if we see a valid protobuf message + virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); }; // A simple wrapper to allow non class aware code write to the console diff --git a/src/Status.h b/src/Status.h index 893c40544..59d443ab7 100644 --- a/src/Status.h +++ b/src/Status.h @@ -9,45 +9,49 @@ #define STATUS_TYPE_NODE 3 #define STATUS_TYPE_BLUETOOTH 4 -namespace meshtastic { +namespace meshtastic +{ // A base class for observable status -class Status { -protected: - // Allows us to observe an Observable - CallbackObserver statusObserver = CallbackObserver(this, &Status::updateStatus); - bool initialized = false; - // Workaround for no typeid support - int statusType = 0; +class Status +{ + protected: + // Allows us to observe an Observable + CallbackObserver statusObserver = + CallbackObserver(this, &Status::updateStatus); + bool initialized = false; + // Workaround for no typeid support + int statusType = 0; -public: - // Allows us to generate observable events - Observable onNewStatus; + public: + // Allows us to generate observable events + Observable onNewStatus; - // Enable polymorphism ? - virtual ~Status() = default; + // Enable polymorphism ? + virtual ~Status() = default; - Status() { - if (!statusType) { - statusType = STATUS_TYPE_BASE; + Status() + { + if (!statusType) { + statusType = STATUS_TYPE_BASE; + } } - } - // Prevent object copy/move - Status(const Status &) = delete; - Status &operator=(const Status &) = delete; + // Prevent object copy/move + Status(const Status &) = delete; + Status &operator=(const Status &) = delete; - // Start observing a source of data - void observe(Observable *source) { statusObserver.observe(source); } + // Start observing a source of data + void observe(Observable *source) { statusObserver.observe(source); } - // Determines whether or not existing data matches the data in another Status instance - bool matches(const Status *otherStatus) const { return true; } + // Determines whether or not existing data matches the data in another Status instance + bool matches(const Status *otherStatus) const { return true; } - bool isInitialized() const { return initialized; } + bool isInitialized() const { return initialized; } - int getStatusType() const { return statusType; } + int getStatusType() const { return statusType; } - // Called when the Observable we're observing generates a new notification - int updateStatus(const Status *newStatus) { return 0; } + // Called when the Observable we're observing generates a new notification + int updateStatus(const Status *newStatus) { return 0; } }; }; // namespace meshtastic diff --git a/src/airtime.cpp b/src/airtime.cpp index e5702544e..a7736d667 100644 --- a/src/airtime.cpp +++ b/src/airtime.cpp @@ -9,175 +9,202 @@ AirTime *airTime = NULL; uint32_t air_period_tx[PERIODS_TO_LOG]; uint32_t air_period_rx[PERIODS_TO_LOG]; -void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms) { +void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms) +{ - if (reportType == TX_LOG) { - LOG_DEBUG("Packet TX: %ums", airtime_ms); - this->airtimes.periodTX[0] = this->airtimes.periodTX[0] + airtime_ms; - air_period_tx[0] = air_period_tx[0] + airtime_ms; + if (reportType == TX_LOG) { + LOG_DEBUG("Packet TX: %ums", airtime_ms); + this->airtimes.periodTX[0] = this->airtimes.periodTX[0] + airtime_ms; + air_period_tx[0] = air_period_tx[0] + airtime_ms; - this->utilizationTX[this->getPeriodUtilHour()] = this->utilizationTX[this->getPeriodUtilHour()] + airtime_ms; - } else if (reportType == RX_LOG) { - LOG_DEBUG("Packet RX: %ums", airtime_ms); - this->airtimes.periodRX[0] = this->airtimes.periodRX[0] + airtime_ms; - air_period_rx[0] = air_period_rx[0] + airtime_ms; - } else if (reportType == RX_ALL_LOG) { - LOG_DEBUG("Packet RX (noise?) : %ums", airtime_ms); - this->airtimes.periodRX_ALL[0] = this->airtimes.periodRX_ALL[0] + airtime_ms; - } - - // Log all airtime type for channel utilization - this->channelUtilization[this->getPeriodUtilMinute()] = channelUtilization[this->getPeriodUtilMinute()] + airtime_ms; -} - -uint8_t AirTime::currentPeriodIndex() { return ((getSecondsSinceBoot() / SECONDS_PER_PERIOD) % PERIODS_TO_LOG); } - -uint8_t AirTime::getPeriodUtilMinute() { return (getSecondsSinceBoot() / 10) % CHANNEL_UTILIZATION_PERIODS; } - -uint8_t AirTime::getPeriodUtilHour() { return (getSecondsSinceBoot() / 60) % MINUTES_IN_HOUR; } - -void AirTime::airtimeRotatePeriod() { - - if (this->airtimes.lastPeriodIndex != this->currentPeriodIndex()) { - LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex()); - - for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) { - this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i]; - this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i]; - this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i]; - - air_period_tx[i + 1] = this->airtimes.periodTX[i]; - air_period_rx[i + 1] = this->airtimes.periodRX[i]; + this->utilizationTX[this->getPeriodUtilHour()] = this->utilizationTX[this->getPeriodUtilHour()] + airtime_ms; + } else if (reportType == RX_LOG) { + LOG_DEBUG("Packet RX: %ums", airtime_ms); + this->airtimes.periodRX[0] = this->airtimes.periodRX[0] + airtime_ms; + air_period_rx[0] = air_period_rx[0] + airtime_ms; + } else if (reportType == RX_ALL_LOG) { + LOG_DEBUG("Packet RX (noise?) : %ums", airtime_ms); + this->airtimes.periodRX_ALL[0] = this->airtimes.periodRX_ALL[0] + airtime_ms; } - this->airtimes.periodTX[0] = 0; - this->airtimes.periodRX[0] = 0; - this->airtimes.periodRX_ALL[0] = 0; - - air_period_tx[0] = 0; - air_period_rx[0] = 0; - - this->airtimes.lastPeriodIndex = this->currentPeriodIndex(); - } + // Log all airtime type for channel utilization + this->channelUtilization[this->getPeriodUtilMinute()] = channelUtilization[this->getPeriodUtilMinute()] + airtime_ms; } -uint32_t *AirTime::airtimeReport(reportTypes reportType) { - - if (reportType == TX_LOG) { - return this->airtimes.periodTX; - } else if (reportType == RX_LOG) { - return this->airtimes.periodRX; - } else if (reportType == RX_ALL_LOG) { - return this->airtimes.periodRX_ALL; - } - return 0; +uint8_t AirTime::currentPeriodIndex() +{ + return ((getSecondsSinceBoot() / SECONDS_PER_PERIOD) % PERIODS_TO_LOG); } -uint8_t AirTime::getPeriodsToLog() { return PERIODS_TO_LOG; } - -uint32_t AirTime::getSecondsPerPeriod() { return SECONDS_PER_PERIOD; } - -uint32_t AirTime::getSecondsSinceBoot() { return this->secSinceBoot; } - -float AirTime::channelUtilizationPercent() { - uint32_t sum = 0; - for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { - sum += this->channelUtilization[i]; - } - - return (float(sum) / float(CHANNEL_UTILIZATION_PERIODS * 10 * 1000)) * 100; +uint8_t AirTime::getPeriodUtilMinute() +{ + return (getSecondsSinceBoot() / 10) % CHANNEL_UTILIZATION_PERIODS; } -float AirTime::utilizationTXPercent() { - uint32_t sum = 0; - for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { - sum += this->utilizationTX[i]; - } - - return (float(sum) / float(MS_IN_HOUR)) * 100; +uint8_t AirTime::getPeriodUtilHour() +{ + return (getSecondsSinceBoot() / 60) % MINUTES_IN_HOUR; } -bool AirTime::isTxAllowedChannelUtil(bool polite) { - uint8_t percentage = (polite ? polite_channel_util_percent : max_channel_util_percent); - if (channelUtilizationPercent() < percentage) { - return true; - } else { - LOG_WARN("Ch. util >%d%%. Skip send", percentage); - return false; - } +void AirTime::airtimeRotatePeriod() +{ + + if (this->airtimes.lastPeriodIndex != this->currentPeriodIndex()) { + LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex()); + + for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) { + this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i]; + this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i]; + this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i]; + + air_period_tx[i + 1] = this->airtimes.periodTX[i]; + air_period_rx[i + 1] = this->airtimes.periodRX[i]; + } + + this->airtimes.periodTX[0] = 0; + this->airtimes.periodRX[0] = 0; + this->airtimes.periodRX_ALL[0] = 0; + + air_period_tx[0] = 0; + air_period_rx[0] = 0; + + this->airtimes.lastPeriodIndex = this->currentPeriodIndex(); + } } -bool AirTime::isTxAllowedAirUtil() { - if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { - if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) { - return true; +uint32_t *AirTime::airtimeReport(reportTypes reportType) +{ + + if (reportType == TX_LOG) { + return this->airtimes.periodTX; + } else if (reportType == RX_LOG) { + return this->airtimes.periodRX; + } else if (reportType == RX_ALL_LOG) { + return this->airtimes.periodRX_ALL; + } + return 0; +} + +uint8_t AirTime::getPeriodsToLog() +{ + return PERIODS_TO_LOG; +} + +uint32_t AirTime::getSecondsPerPeriod() +{ + return SECONDS_PER_PERIOD; +} + +uint32_t AirTime::getSecondsSinceBoot() +{ + return this->secSinceBoot; +} + +float AirTime::channelUtilizationPercent() +{ + uint32_t sum = 0; + for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { + sum += this->channelUtilization[i]; + } + + return (float(sum) / float(CHANNEL_UTILIZATION_PERIODS * 10 * 1000)) * 100; +} + +float AirTime::utilizationTXPercent() +{ + uint32_t sum = 0; + for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { + sum += this->utilizationTX[i]; + } + + return (float(sum) / float(MS_IN_HOUR)) * 100; +} + +bool AirTime::isTxAllowedChannelUtil(bool polite) +{ + uint8_t percentage = (polite ? polite_channel_util_percent : max_channel_util_percent); + if (channelUtilizationPercent() < percentage) { + return true; } else { - LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100); - return false; + LOG_WARN("Ch. util >%d%%. Skip send", percentage); + return false; } - } - return true; +} + +bool AirTime::isTxAllowedAirUtil() +{ + if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { + if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) { + return true; + } else { + LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100); + return false; + } + } + return true; } // Get the amount of minutes we have to be silent before we can send again -uint8_t AirTime::getSilentMinutes(float txPercent, float dutyCycle) { - float newTxPercent = txPercent; - for (int8_t i = MINUTES_IN_HOUR - 1; i >= 0; --i) { - newTxPercent -= ((float)this->utilizationTX[i] / (MS_IN_MINUTE * MINUTES_IN_HOUR / 100)); - if (newTxPercent < dutyCycle) - return MINUTES_IN_HOUR - 1 - i; - } +uint8_t AirTime::getSilentMinutes(float txPercent, float dutyCycle) +{ + float newTxPercent = txPercent; + for (int8_t i = MINUTES_IN_HOUR - 1; i >= 0; --i) { + newTxPercent -= ((float)this->utilizationTX[i] / (MS_IN_MINUTE * MINUTES_IN_HOUR / 100)); + if (newTxPercent < dutyCycle) + return MINUTES_IN_HOUR - 1 - i; + } - return MINUTES_IN_HOUR; + return MINUTES_IN_HOUR; } AirTime::AirTime() : concurrency::OSThread("AirTime"), airtimes({}) {} -int32_t AirTime::runOnce() { - secSinceBoot++; +int32_t AirTime::runOnce() +{ + secSinceBoot++; - uint8_t utilPeriod = this->getPeriodUtilMinute(); - uint8_t utilPeriodTX = this->getPeriodUtilHour(); + uint8_t utilPeriod = this->getPeriodUtilMinute(); + uint8_t utilPeriodTX = this->getPeriodUtilHour(); - if (firstTime) { + if (firstTime) { - // Init utilizationTX window to all 0 - for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { - this->utilizationTX[i] = 0; + // Init utilizationTX window to all 0 + for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { + this->utilizationTX[i] = 0; + } + + // Init channelUtilization window to all 0 + for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { + this->channelUtilization[i] = 0; + } + + // Init airtime windows to all 0 + for (int i = 0; i < PERIODS_TO_LOG; i++) { + this->airtimes.periodTX[i] = 0; + this->airtimes.periodRX[i] = 0; + this->airtimes.periodRX_ALL[i] = 0; + + // air_period_tx[i] = 0; + // air_period_rx[i] = 0; + } + + firstTime = false; + lastUtilPeriod = utilPeriod; + } else { + this->airtimeRotatePeriod(); + + // Reset the channelUtilization window when we roll over + if (lastUtilPeriod != utilPeriod) { + lastUtilPeriod = utilPeriod; + + this->channelUtilization[utilPeriod] = 0; + } + + if (lastUtilPeriodTX != utilPeriodTX) { + lastUtilPeriodTX = utilPeriodTX; + + this->utilizationTX[utilPeriodTX] = 0; + } } - - // Init channelUtilization window to all 0 - for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { - this->channelUtilization[i] = 0; - } - - // Init airtime windows to all 0 - for (int i = 0; i < PERIODS_TO_LOG; i++) { - this->airtimes.periodTX[i] = 0; - this->airtimes.periodRX[i] = 0; - this->airtimes.periodRX_ALL[i] = 0; - - // air_period_tx[i] = 0; - // air_period_rx[i] = 0; - } - - firstTime = false; - lastUtilPeriod = utilPeriod; - } else { - this->airtimeRotatePeriod(); - - // Reset the channelUtilization window when we roll over - if (lastUtilPeriod != utilPeriod) { - lastUtilPeriod = utilPeriod; - - this->channelUtilization[utilPeriod] = 0; - } - - if (lastUtilPeriodTX != utilPeriodTX) { - lastUtilPeriodTX = utilPeriodTX; - - this->utilizationTX[utilPeriodTX] = 0; - } - } - return (1000 * 1); + return (1000 * 1); } diff --git a/src/airtime.h b/src/airtime.h index 48cc659c6..3ed7b6d7c 100644 --- a/src/airtime.h +++ b/src/airtime.h @@ -39,50 +39,51 @@ void logAirtime(reportTypes reportType, uint32_t airtime_ms); uint32_t *airtimeReport(reportTypes reportType); -class AirTime : private concurrency::OSThread { +class AirTime : private concurrency::OSThread +{ -public: - AirTime(); + public: + AirTime(); - void logAirtime(reportTypes reportType, uint32_t airtime_ms); - float channelUtilizationPercent(); - float utilizationTXPercent(); + void logAirtime(reportTypes reportType, uint32_t airtime_ms); + float channelUtilizationPercent(); + float utilizationTXPercent(); - float UtilizationPercentTX(); - uint32_t channelUtilization[CHANNEL_UTILIZATION_PERIODS] = {0}; - uint32_t utilizationTX[MINUTES_IN_HOUR] = {0}; + float UtilizationPercentTX(); + uint32_t channelUtilization[CHANNEL_UTILIZATION_PERIODS] = {0}; + uint32_t utilizationTX[MINUTES_IN_HOUR] = {0}; - void airtimeRotatePeriod(); - uint8_t getPeriodsToLog(); - uint32_t getSecondsPerPeriod(); - uint32_t getSecondsSinceBoot(); - uint32_t *airtimeReport(reportTypes reportType); - uint8_t getSilentMinutes(float txPercent, float dutyCycle); - bool isTxAllowedChannelUtil(bool polite = false); - bool isTxAllowedAirUtil(); + void airtimeRotatePeriod(); + uint8_t getPeriodsToLog(); + uint32_t getSecondsPerPeriod(); + uint32_t getSecondsSinceBoot(); + uint32_t *airtimeReport(reportTypes reportType); + uint8_t getSilentMinutes(float txPercent, float dutyCycle); + bool isTxAllowedChannelUtil(bool polite = false); + bool isTxAllowedAirUtil(); -private: - bool firstTime = true; - uint8_t lastUtilPeriod = 0; - uint8_t lastUtilPeriodTX = 0; - uint32_t secSinceBoot = 0; - uint8_t max_channel_util_percent = 40; - uint8_t polite_channel_util_percent = 25; - uint8_t polite_duty_cycle_percent = 50; // half of Duty Cycle allowance is ok for metadata + private: + bool firstTime = true; + uint8_t lastUtilPeriod = 0; + uint8_t lastUtilPeriodTX = 0; + uint32_t secSinceBoot = 0; + uint8_t max_channel_util_percent = 40; + uint8_t polite_channel_util_percent = 25; + uint8_t polite_duty_cycle_percent = 50; // half of Duty Cycle allowance is ok for metadata - struct airtimeStruct { - uint32_t periodTX[PERIODS_TO_LOG]; // AirTime transmitted - uint32_t periodRX[PERIODS_TO_LOG]; // AirTime received and repeated (Only valid mesh packets) - uint32_t periodRX_ALL[PERIODS_TO_LOG]; // AirTime received regardless of valid mesh packet. Could include noise. - uint8_t lastPeriodIndex; - } airtimes; + struct airtimeStruct { + uint32_t periodTX[PERIODS_TO_LOG]; // AirTime transmitted + uint32_t periodRX[PERIODS_TO_LOG]; // AirTime received and repeated (Only valid mesh packets) + uint32_t periodRX_ALL[PERIODS_TO_LOG]; // AirTime received regardless of valid mesh packet. Could include noise. + uint8_t lastPeriodIndex; + } airtimes; - uint8_t getPeriodUtilMinute(); - uint8_t getPeriodUtilHour(); - uint8_t currentPeriodIndex(); + uint8_t getPeriodUtilMinute(); + uint8_t getPeriodUtilHour(); + uint8_t currentPeriodIndex(); -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; }; extern AirTime *airTime; diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 218a101b5..0716dedd0 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -5,58 +5,60 @@ BuzzerFeedbackThread *buzzerFeedbackThread; -BuzzerFeedbackThread::BuzzerFeedbackThread() { - if (inputBroker) - inputObserver.observe(inputBroker); +BuzzerFeedbackThread::BuzzerFeedbackThread() +{ + if (inputBroker) + inputObserver.observe(inputBroker); } -int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) { - // Only provide feedback if buzzer is enabled for notifications - if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) { - return 0; // Let other handlers process the event - } - - // Handle different input events with appropriate buzzer feedback - switch (event->inputEvent) { - case INPUT_BROKER_USER_PRESS: - case INPUT_BROKER_ALT_PRESS: - playClick(); // Low delay feedback - break; - - case INPUT_BROKER_SELECT: - case INPUT_BROKER_SELECT_LONG: - playBeep(); // Confirmation feedback - break; - - case INPUT_BROKER_UP: - case INPUT_BROKER_UP_LONG: - case INPUT_BROKER_DOWN: - case INPUT_BROKER_DOWN_LONG: - case INPUT_BROKER_LEFT: - case INPUT_BROKER_RIGHT: - playChirp(); // Navigation feedback - break; - - case INPUT_BROKER_CANCEL: - case INPUT_BROKER_BACK: - playBoop(); // Cancel/back feedback - break; - - case INPUT_BROKER_SEND_PING: - playComboTune(); // Ping sent feedback - break; - - default: - // For other events, check if it's a printable character - if (event->kbchar >= 32 && event->kbchar <= 126) { - // Typing feedback - very short boop - // Removing this for now, too chatty - // playChirp(); +int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) +{ + // Only provide feedback if buzzer is enabled for notifications + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) { + return 0; // Let other handlers process the event } - break; - } - return 0; // Allow other handlers to process the event + // Handle different input events with appropriate buzzer feedback + switch (event->inputEvent) { + case INPUT_BROKER_USER_PRESS: + case INPUT_BROKER_ALT_PRESS: + playClick(); // Low delay feedback + break; + + case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: + playBeep(); // Confirmation feedback + break; + + case INPUT_BROKER_UP: + case INPUT_BROKER_UP_LONG: + case INPUT_BROKER_DOWN: + case INPUT_BROKER_DOWN_LONG: + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: + playChirp(); // Navigation feedback + break; + + case INPUT_BROKER_CANCEL: + case INPUT_BROKER_BACK: + playBoop(); // Cancel/back feedback + break; + + case INPUT_BROKER_SEND_PING: + playComboTune(); // Ping sent feedback + break; + + default: + // For other events, check if it's a printable character + if (event->kbchar >= 32 && event->kbchar <= 126) { + // Typing feedback - very short boop + // Removing this for now, too chatty + // playChirp(); + } + break; + } + + return 0; // Allow other handlers to process the event } diff --git a/src/buzz/BuzzerFeedbackThread.h b/src/buzz/BuzzerFeedbackThread.h index df8243524..7dc08ead5 100644 --- a/src/buzz/BuzzerFeedbackThread.h +++ b/src/buzz/BuzzerFeedbackThread.h @@ -4,13 +4,14 @@ #include "concurrency/OSThread.h" #include "input/InputBroker.h" -class BuzzerFeedbackThread { - CallbackObserver inputObserver = - CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); +class BuzzerFeedbackThread +{ + CallbackObserver inputObserver = + CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); -public: - BuzzerFeedbackThread(); - int handleInputEvent(const InputEvent *event); + public: + BuzzerFeedbackThread(); + int handleInputEvent(const InputEvent *event); }; extern BuzzerFeedbackThread *buzzerFeedbackThread; diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index a5b91189e..00ad71031 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -11,8 +11,8 @@ extern "C" void delay(uint32_t dwMs); #endif struct ToneDuration { - int frequency_khz; - int duration_ms; + int frequency_khz; + int duration_ms; }; // Some common frequencies. @@ -42,92 +42,105 @@ const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note -void playTones(const ToneDuration *tone_durations, int size) { - if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { - // Buzzer is disabled or not set to system tones - return; - } -#ifdef PIN_BUZZER - if (!config.device.buzzer_gpio) - config.device.buzzer_gpio = PIN_BUZZER; -#endif - if (config.device.buzzer_gpio) { - for (int i = 0; i < size; i++) { - const auto &tone_duration = tone_durations[i]; - tone(config.device.buzzer_gpio, tone_duration.frequency_khz, tone_duration.duration_ms); - // to distinguish the notes, set a minimum time between them. - delay(1.3 * tone_duration.duration_ms); +void playTones(const ToneDuration *tone_durations, int size) +{ + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + // Buzzer is disabled or not set to system tones + return; } - } -} - -void playBeep() { - ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); -} - -void playLongBeep() { - ToneDuration melody[] = {{NOTE_B3, DURATION_1_1}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); -} - -void playGPSEnableBeep() { -#if defined(R1_NEO) || defined(MUZI_BASE) - ToneDuration melody[] = {{NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}}; -#else - ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; +#ifdef PIN_BUZZER + if (!config.device.buzzer_gpio) + config.device.buzzer_gpio = PIN_BUZZER; #endif - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); + if (config.device.buzzer_gpio) { + for (int i = 0; i < size; i++) { + const auto &tone_duration = tone_durations[i]; + tone(config.device.buzzer_gpio, tone_duration.frequency_khz, tone_duration.duration_ms); + // to distinguish the notes, set a minimum time between them. + delay(1.3 * tone_duration.duration_ms); + } + } } -void playGPSDisableBeep() { +void playBeep() +{ + ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playLongBeep() +{ + ToneDuration melody[] = {{NOTE_B3, DURATION_1_1}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playGPSEnableBeep() +{ #if defined(R1_NEO) || defined(MUZI_BASE) - ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, {NOTE_F3, DURATION_1_16}, - {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}}; + ToneDuration melody[] = { + {NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}}; #else - ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; + ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; #endif - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playStartMelody() { - ToneDuration melody[] = {{NOTE_FS3, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_CS4, DURATION_1_4}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playGPSDisableBeep() +{ +#if defined(R1_NEO) || defined(MUZI_BASE) + ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, + {NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, + {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}}; +#else + ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; +#endif + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playShutdownMelody() { - ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playStartMelody() +{ + ToneDuration melody[] = {{NOTE_FS3, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_CS4, DURATION_1_4}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playChirp() { - // A short, friendly "chirp" sound for key presses - ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playShutdownMelody() +{ + ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playClick() { - // A very short "click" sound with minimum delay; ideal for rotary encoder events - ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playChirp() +{ + // A short, friendly "chirp" sound for key presses + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playBoop() { - // A short, friendly "boop" sound for button presses - ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playClick() +{ + // A very short "click" sound with minimum delay; ideal for rotary encoder events + ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } -void playLongPressLeadUp() { - // An ascending lead-up sequence for long press - builds anticipation - ToneDuration melody[] = { - {NOTE_C3, 100}, // Start low - {NOTE_E3, 100}, // Step up - {NOTE_G3, 100}, // Keep climbing - {NOTE_B3, 150} // Peak with longer note for emphasis - }; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void playBoop() +{ + // A short, friendly "boop" sound for button presses + ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playLongPressLeadUp() +{ + // An ascending lead-up sequence for long press - builds anticipation + ToneDuration melody[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } // Static state for progressive lead-up notes @@ -140,34 +153,39 @@ static const ToneDuration leadUpNotes[] = { }; static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration); -bool playNextLeadUpNote() { - if (leadUpNoteIndex >= leadUpNotesCount) { - return false; // All notes have been played - } +bool playNextLeadUpNote() +{ + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // All notes have been played + } - // Use playTones to handle buzzer logic consistently - const auto ¬e = leadUpNotes[leadUpNoteIndex]; - playTones(¬e, 1); // Play single note using existing playTones function + // Use playTones to handle buzzer logic consistently + const auto ¬e = leadUpNotes[leadUpNoteIndex]; + playTones(¬e, 1); // Play single note using existing playTones function - leadUpNoteIndex++; + leadUpNoteIndex++; - if (leadUpNoteIndex >= leadUpNotesCount) { - return false; // this was the final note - } - return true; // Note was played (playTones handles buzzer availability internally) + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // this was the final note + } + return true; // Note was played (playTones handles buzzer availability internally) } -void resetLeadUpSequence() { leadUpNoteIndex = 0; } - -void playComboTune() { - // Quick high-pitched notes with trills - ToneDuration melody[] = { - {NOTE_G3, 80}, // Quick chirp - {NOTE_B3, 60}, // Higher chirp - {NOTE_CS4, 80}, // Even higher - {NOTE_G3, 60}, // Quick trill down - {NOTE_CS4, 60}, // Quick trill up - {NOTE_B3, 120} // Ending chirp - }; - playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +void resetLeadUpSequence() +{ + leadUpNoteIndex = 0; +} + +void playComboTune() +{ + // Quick high-pitched notes with trills + ToneDuration melody[] = { + {NOTE_G3, 80}, // Quick chirp + {NOTE_B3, 60}, // Higher chirp + {NOTE_CS4, 80}, // Even higher + {NOTE_G3, 60}, // Quick trill down + {NOTE_CS4, 60}, // Quick trill up + {NOTE_B3, 120} // Ending chirp + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } diff --git a/src/commands.h b/src/commands.h index 687e08637..603003e5c 100644 --- a/src/commands.h +++ b/src/commands.h @@ -4,15 +4,15 @@ */ enum class Cmd { - INVALID, - SET_ON, - SET_OFF, - ON_PRESS, - START_ALERT_FRAME, - STOP_ALERT_FRAME, - START_FIRMWARE_UPDATE_SCREEN, - STOP_BOOT_SCREEN, - SHOW_PREV_FRAME, - SHOW_NEXT_FRAME, - NOOP + INVALID, + SET_ON, + SET_OFF, + ON_PRESS, + START_ALERT_FRAME, + STOP_ALERT_FRAME, + START_FIRMWARE_UPDATE_SCREEN, + STOP_BOOT_SCREEN, + SHOW_PREV_FRAME, + SHOW_NEXT_FRAME, + NOOP }; \ No newline at end of file diff --git a/src/concurrency/BinarySemaphoreFreeRTOS.cpp b/src/concurrency/BinarySemaphoreFreeRTOS.cpp index 411e284c2..36e55eae7 100644 --- a/src/concurrency/BinarySemaphoreFreeRTOS.cpp +++ b/src/concurrency/BinarySemaphoreFreeRTOS.cpp @@ -4,21 +4,35 @@ #ifdef HAS_FREE_RTOS -namespace concurrency { +namespace concurrency +{ -BinarySemaphoreFreeRTOS::BinarySemaphoreFreeRTOS() : semaphore(xSemaphoreCreateBinary()) { assert(semaphore); } +BinarySemaphoreFreeRTOS::BinarySemaphoreFreeRTOS() : semaphore(xSemaphoreCreateBinary()) +{ + assert(semaphore); +} -BinarySemaphoreFreeRTOS::~BinarySemaphoreFreeRTOS() { vSemaphoreDelete(semaphore); } +BinarySemaphoreFreeRTOS::~BinarySemaphoreFreeRTOS() +{ + vSemaphoreDelete(semaphore); +} /** * Returns false if we were interrupted */ -bool BinarySemaphoreFreeRTOS::take(uint32_t msec) { return xSemaphoreTake(semaphore, pdMS_TO_TICKS(msec)); } +bool BinarySemaphoreFreeRTOS::take(uint32_t msec) +{ + return xSemaphoreTake(semaphore, pdMS_TO_TICKS(msec)); +} -void BinarySemaphoreFreeRTOS::give() { xSemaphoreGive(semaphore); } +void BinarySemaphoreFreeRTOS::give() +{ + xSemaphoreGive(semaphore); +} -IRAM_ATTR void BinarySemaphoreFreeRTOS::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) { - xSemaphoreGiveFromISR(semaphore, pxHigherPriorityTaskWoken); +IRAM_ATTR void BinarySemaphoreFreeRTOS::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) +{ + xSemaphoreGiveFromISR(semaphore, pxHigherPriorityTaskWoken); } } // namespace concurrency diff --git a/src/concurrency/BinarySemaphoreFreeRTOS.h b/src/concurrency/BinarySemaphoreFreeRTOS.h index aee910480..2883151d8 100644 --- a/src/concurrency/BinarySemaphoreFreeRTOS.h +++ b/src/concurrency/BinarySemaphoreFreeRTOS.h @@ -2,25 +2,27 @@ #include "../freertosinc.h" -namespace concurrency { +namespace concurrency +{ #ifdef HAS_FREE_RTOS -class BinarySemaphoreFreeRTOS { - SemaphoreHandle_t semaphore; +class BinarySemaphoreFreeRTOS +{ + SemaphoreHandle_t semaphore; -public: - BinarySemaphoreFreeRTOS(); - ~BinarySemaphoreFreeRTOS(); + public: + BinarySemaphoreFreeRTOS(); + ~BinarySemaphoreFreeRTOS(); - /** - * Returns false if we timed out - */ - bool take(uint32_t msec); + /** + * Returns false if we timed out + */ + bool take(uint32_t msec); - void give(); + void give(); - void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); + void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); }; #endif diff --git a/src/concurrency/BinarySemaphorePosix.cpp b/src/concurrency/BinarySemaphorePosix.cpp index 1ae9b216f..dc49a489b 100644 --- a/src/concurrency/BinarySemaphorePosix.cpp +++ b/src/concurrency/BinarySemaphorePosix.cpp @@ -3,7 +3,8 @@ #ifndef HAS_FREE_RTOS -namespace concurrency { +namespace concurrency +{ BinarySemaphorePosix::BinarySemaphorePosix() {} @@ -12,9 +13,10 @@ BinarySemaphorePosix::~BinarySemaphorePosix() {} /** * Returns false if we timed out */ -bool BinarySemaphorePosix::take(uint32_t msec) { - delay(msec); // FIXME - return false; +bool BinarySemaphorePosix::take(uint32_t msec) +{ + delay(msec); // FIXME + return false; } void BinarySemaphorePosix::give() {} diff --git a/src/concurrency/BinarySemaphorePosix.h b/src/concurrency/BinarySemaphorePosix.h index e5add3780..475b29874 100644 --- a/src/concurrency/BinarySemaphorePosix.h +++ b/src/concurrency/BinarySemaphorePosix.h @@ -2,25 +2,27 @@ #include "../freertosinc.h" -namespace concurrency { +namespace concurrency +{ #ifndef HAS_FREE_RTOS -class BinarySemaphorePosix { - // SemaphoreHandle_t semaphore; +class BinarySemaphorePosix +{ + // SemaphoreHandle_t semaphore; -public: - BinarySemaphorePosix(); - ~BinarySemaphorePosix(); + public: + BinarySemaphorePosix(); + ~BinarySemaphorePosix(); - /** - * Returns false if we timed out - */ - bool take(uint32_t msec); + /** + * Returns false if we timed out + */ + bool take(uint32_t msec); - void give(); + void give(); - void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); + void giveFromISR(BaseType_t *pxHigherPriorityTaskWoken); }; #endif diff --git a/src/concurrency/InterruptableDelay.cpp b/src/concurrency/InterruptableDelay.cpp index bec4f2e83..8bc85436b 100644 --- a/src/concurrency/InterruptableDelay.cpp +++ b/src/concurrency/InterruptableDelay.cpp @@ -1,7 +1,8 @@ #include "concurrency/InterruptableDelay.h" #include "configuration.h" -namespace concurrency { +namespace concurrency +{ InterruptableDelay::InterruptableDelay() {} @@ -10,18 +11,25 @@ InterruptableDelay::~InterruptableDelay() {} /** * Returns false if we were interrupted */ -bool InterruptableDelay::delay(uint32_t msec) { - // LOG_DEBUG("delay %u ", msec); +bool InterruptableDelay::delay(uint32_t msec) +{ + // LOG_DEBUG("delay %u ", msec); - // sem take will return false if we timed out (i.e. were not interrupted) - bool r = semaphore.take(msec); + // sem take will return false if we timed out (i.e. were not interrupted) + bool r = semaphore.take(msec); - // LOG_DEBUG("interrupt=%d", r); - return !r; + // LOG_DEBUG("interrupt=%d", r); + return !r; } -void InterruptableDelay::interrupt() { semaphore.give(); } +void InterruptableDelay::interrupt() +{ + semaphore.give(); +} -IRAM_ATTR void InterruptableDelay::interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken) { semaphore.giveFromISR(pxHigherPriorityTaskWoken); } +IRAM_ATTR void InterruptableDelay::interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken) +{ + semaphore.giveFromISR(pxHigherPriorityTaskWoken); +} } // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/InterruptableDelay.h b/src/concurrency/InterruptableDelay.h index 50a4e74c7..41bc40a21 100644 --- a/src/concurrency/InterruptableDelay.h +++ b/src/concurrency/InterruptableDelay.h @@ -10,31 +10,32 @@ #define BinarySemaphore BinarySemaphorePosix #endif -namespace concurrency { +namespace concurrency +{ /** * An object that provides delay(msec) like functionality, but can be interrupted by calling interrupt(). * - * Useful for they top level loop() delay call to keep the CPU powered down until our next scheduled event or some - * external event. + * Useful for they top level loop() delay call to keep the CPU powered down until our next scheduled event or some external event. * * This is implemented for FreeRTOS but should be easy to port to other operating systems. */ -class InterruptableDelay { - BinarySemaphore semaphore; +class InterruptableDelay +{ + BinarySemaphore semaphore; -public: - InterruptableDelay(); - ~InterruptableDelay(); + public: + InterruptableDelay(); + ~InterruptableDelay(); - /** - * Returns false if we were interrupted - */ - bool delay(uint32_t msec); + /** + * Returns false if we were interrupted + */ + bool delay(uint32_t msec); - void interrupt(); + void interrupt(); - void interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken); + void interruptFromISR(BaseType_t *pxHigherPriorityTaskWoken); }; } // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/Lock.cpp b/src/concurrency/Lock.cpp index 15c59d081..0fe80e455 100644 --- a/src/concurrency/Lock.cpp +++ b/src/concurrency/Lock.cpp @@ -2,26 +2,30 @@ #include "configuration.h" #include -namespace concurrency { +namespace concurrency +{ #ifdef HAS_FREE_RTOS -Lock::Lock() : handle(xSemaphoreCreateBinary()) { - assert(handle); - if (xSemaphoreGive(handle) == false) { - abort(); - } +Lock::Lock() : handle(xSemaphoreCreateBinary()) +{ + assert(handle); + if (xSemaphoreGive(handle) == false) { + abort(); + } } -void Lock::lock() { - if (xSemaphoreTake(handle, portMAX_DELAY) == false) { - abort(); - } +void Lock::lock() +{ + if (xSemaphoreTake(handle, portMAX_DELAY) == false) { + abort(); + } } -void Lock::unlock() { - if (xSemaphoreGive(handle) == false) { - abort(); - } +void Lock::unlock() +{ + if (xSemaphoreGive(handle) == false) { + abort(); + } } #else Lock::Lock() {} diff --git a/src/concurrency/Lock.h b/src/concurrency/Lock.h index e4bcfe5f6..1b9ea20d5 100644 --- a/src/concurrency/Lock.h +++ b/src/concurrency/Lock.h @@ -2,31 +2,33 @@ #include "../freertosinc.h" -namespace concurrency { +namespace concurrency +{ /** * @brief Simple wrapper around FreeRTOS API for implementing a mutex lock */ -class Lock { -public: - Lock(); +class Lock +{ + public: + Lock(); - Lock(const Lock &) = delete; - Lock &operator=(const Lock &) = delete; + Lock(const Lock &) = delete; + Lock &operator=(const Lock &) = delete; - /// Locks the lock. - // - // Must not be called from an ISR. - void lock(); + /// Locks the lock. + // + // Must not be called from an ISR. + void lock(); - // Unlocks the lock. - // - // Must not be called from an ISR. - void unlock(); + // Unlocks the lock. + // + // Must not be called from an ISR. + void unlock(); -private: + private: #ifdef HAS_FREE_RTOS - SemaphoreHandle_t handle; + SemaphoreHandle_t handle; #endif }; diff --git a/src/concurrency/LockGuard.cpp b/src/concurrency/LockGuard.cpp index fc0c3b81d..d855266cb 100644 --- a/src/concurrency/LockGuard.cpp +++ b/src/concurrency/LockGuard.cpp @@ -1,10 +1,17 @@ #include "LockGuard.h" #include "configuration.h" -namespace concurrency { +namespace concurrency +{ -LockGuard::LockGuard(Lock *lock) : lock(lock) { lock->lock(); } +LockGuard::LockGuard(Lock *lock) : lock(lock) +{ + lock->lock(); +} -LockGuard::~LockGuard() { lock->unlock(); } +LockGuard::~LockGuard() +{ + lock->unlock(); +} } // namespace concurrency diff --git a/src/concurrency/LockGuard.h b/src/concurrency/LockGuard.h index a4428d4c8..647e7960d 100644 --- a/src/concurrency/LockGuard.h +++ b/src/concurrency/LockGuard.h @@ -2,21 +2,23 @@ #include "Lock.h" -namespace concurrency { +namespace concurrency +{ /** * @brief RAII lock guard */ -class LockGuard { -public: - explicit LockGuard(Lock *lock); - ~LockGuard(); +class LockGuard +{ + public: + explicit LockGuard(Lock *lock); + ~LockGuard(); - LockGuard(const LockGuard &) = delete; - LockGuard &operator=(const LockGuard &) = delete; + LockGuard(const LockGuard &) = delete; + LockGuard &operator=(const LockGuard &) = delete; -private: - Lock *lock; + private: + Lock *lock; }; } // namespace concurrency diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp index 655c8d4f1..0e4e31d9b 100644 --- a/src/concurrency/NotifiedWorkerThread.cpp +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -2,42 +2,45 @@ #include "configuration.h" #include "main.h" -namespace concurrency { +namespace concurrency +{ static bool debugNotification; /** * Notify this thread so it can run */ -bool NotifiedWorkerThread::notify(uint32_t v, bool overwrite) { - bool r = notifyCommon(v, overwrite); +bool NotifiedWorkerThread::notify(uint32_t v, bool overwrite) +{ + bool r = notifyCommon(v, overwrite); - if (r) - mainDelay.interrupt(); + if (r) + mainDelay.interrupt(); - return r; + return r; } /** * Notify this thread so it can run */ -IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) { - if (overwrite || notification == 0) { - enabled = true; - setInterval(0); // Run ASAP - runASAP = true; +IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) +{ + if (overwrite || notification == 0) { + enabled = true; + setInterval(0); // Run ASAP + runASAP = true; - notification = v; - if (debugNotification) { - LOG_DEBUG("Set notification %d", v); + notification = v; + if (debugNotification) { + LOG_DEBUG("Set notification %d", v); + } + return true; + } else { + if (debugNotification) { + LOG_DEBUG("Drop notification %d", v); + } + return false; } - return true; - } else { - if (debugNotification) { - LOG_DEBUG("Drop notification %d", v); - } - return false; - } } /** @@ -45,43 +48,47 @@ IRAM_ATTR bool NotifiedWorkerThread::notifyCommon(uint32_t v, bool overwrite) { * * This must be inline or IRAM_ATTR on ESP32 */ -IRAM_ATTR bool NotifiedWorkerThread::notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite) { - bool r = notifyCommon(v, overwrite); - if (r) - mainDelay.interruptFromISR(highPriWoken); +IRAM_ATTR bool NotifiedWorkerThread::notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite) +{ + bool r = notifyCommon(v, overwrite); + if (r) + mainDelay.interruptFromISR(highPriWoken); - return r; + return r; } /** * Schedule a notification to fire in delay msecs */ -bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrite) { - bool didIt = notify(v, overwrite); +bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrite) +{ + bool didIt = notify(v, overwrite); - if (didIt) { // If we didn't already have something queued, override the delay to be larger - setIntervalFromNow(delay); // a new version of setInterval relative to the current time - if (debugNotification) { - LOG_DEBUG("Delay notification %u", delay); + if (didIt) { // If we didn't already have something queued, override the delay to be larger + setIntervalFromNow(delay); // a new version of setInterval relative to the current time + if (debugNotification) { + LOG_DEBUG("Delay notification %u", delay); + } } - } - return didIt; + return didIt; } -void NotifiedWorkerThread::checkNotification() { - auto n = notification; - notification = 0; // clear notification - if (n) { - onNotify(n); - } +void NotifiedWorkerThread::checkNotification() +{ + auto n = notification; + notification = 0; // clear notification + if (n) { + onNotify(n); + } } -int32_t NotifiedWorkerThread::runOnce() { - enabled = false; // Only run once per notification - checkNotification(); +int32_t NotifiedWorkerThread::runOnce() +{ + enabled = false; // Only run once per notification + checkNotification(); - return RUN_SAME; + return RUN_SAME; } } // namespace concurrency \ No newline at end of file diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h index b914db8d0..7a150b0b0 100644 --- a/src/concurrency/NotifiedWorkerThread.h +++ b/src/concurrency/NotifiedWorkerThread.h @@ -2,53 +2,55 @@ #include "OSThread.h" -namespace concurrency { +namespace concurrency +{ /** * @brief A worker thread that waits on a freertos notification */ -class NotifiedWorkerThread : public OSThread { - /** - * The notification that was most recently used to wake the thread. Read from runOnce() - */ - uint32_t notification = 0; +class NotifiedWorkerThread : public OSThread +{ + /** + * The notification that was most recently used to wake the thread. Read from runOnce() + */ + uint32_t notification = 0; -public: - NotifiedWorkerThread(const char *name) : OSThread(name) {} + public: + NotifiedWorkerThread(const char *name) : OSThread(name) {} - /** - * Notify this thread so it can run - */ - bool notify(uint32_t v, bool overwrite); + /** + * Notify this thread so it can run + */ + bool notify(uint32_t v, bool overwrite); - /** - * Notify from an ISR - * - * This must be inline or IRAM_ATTR on ESP32 - */ - bool notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite); + /** + * Notify from an ISR + * + * This must be inline or IRAM_ATTR on ESP32 + */ + bool notifyFromISR(BaseType_t *highPriWoken, uint32_t v, bool overwrite); - /** - * Schedule a notification to fire in delay msecs - */ - bool notifyLater(uint32_t delay, uint32_t v, bool overwrite); + /** + * Schedule a notification to fire in delay msecs + */ + bool notifyLater(uint32_t delay, uint32_t v, bool overwrite); -protected: - virtual void onNotify(uint32_t notification) = 0; + protected: + virtual void onNotify(uint32_t notification) = 0; - /// just calls checkNotification() - virtual int32_t runOnce() override; + /// just calls checkNotification() + virtual int32_t runOnce() override; - /// Sometimes we might want to check notifications independently of when our thread was getting woken up (i.e. if we - /// are about to change radio transmit/receive modes we want to handle any pending interrupts first). You can call - /// this method and if any notifications are currently pending they will be handled immediately. - void checkNotification(); + /// Sometimes we might want to check notifications independently of when our thread was getting woken up (i.e. if we are about + /// to change radio transmit/receive modes we want to handle any pending interrupts first). You can call this method and if + /// any notifications are currently pending they will be handled immediately. + void checkNotification(); -private: - /** - * Notify this thread so it can run - */ - bool notifyCommon(uint32_t v, bool overwrite); + private: + /** + * Notify this thread so it can run + */ + bool notifyCommon(uint32_t v, bool overwrite); }; } // namespace concurrency diff --git a/src/concurrency/OSThread.cpp b/src/concurrency/OSThread.cpp index 8e0f81995..ce9a256b7 100644 --- a/src/concurrency/OSThread.cpp +++ b/src/concurrency/OSThread.cpp @@ -3,7 +3,8 @@ #include "memGet.h" #include -namespace concurrency { +namespace concurrency +{ /// Show debugging info for disabled threads bool OSThread::showDisabled; @@ -19,85 +20,93 @@ const OSThread *OSThread::currentThread; ThreadController mainController, timerController; InterruptableDelay mainDelay; -void OSThread::setup() { - mainController.ThreadName = "mainController"; - timerController.ThreadName = "timerController"; +void OSThread::setup() +{ + mainController.ThreadName = "mainController"; + timerController.ThreadName = "timerController"; } -OSThread::OSThread(const char *_name, uint32_t period, ThreadController *_controller) : Thread(NULL, period), controller(_controller) { - assertIsSetup(); +OSThread::OSThread(const char *_name, uint32_t period, ThreadController *_controller) + : Thread(NULL, period), controller(_controller) +{ + assertIsSetup(); - ThreadName = _name; + ThreadName = _name; - if (controller) { - bool added = controller->add(this); - assert(added); - } + if (controller) { + bool added = controller->add(this); + assert(added); + } } -OSThread::~OSThread() { - if (controller) - controller->remove(this); +OSThread::~OSThread() +{ + if (controller) + controller->remove(this); } /** * Wait a specified number msecs starting from the current time (rather than the last time we were run) */ -void OSThread::setIntervalFromNow(unsigned long _interval) { - // Save interval - interval = _interval; +void OSThread::setIntervalFromNow(unsigned long _interval) +{ + // Save interval + interval = _interval; - // Cache the next run based on the last_run - _cached_next_run = millis() + interval; + // Cache the next run based on the last_run + _cached_next_run = millis() + interval; } -bool OSThread::shouldRun(unsigned long time) { - bool r = Thread::shouldRun(time); +bool OSThread::shouldRun(unsigned long time) +{ + bool r = Thread::shouldRun(time); - if (showRun && r) { - LOG_DEBUG("Thread %s: run", ThreadName.c_str()); - } + if (showRun && r) { + LOG_DEBUG("Thread %s: run", ThreadName.c_str()); + } - if (showWaiting && enabled && !r) { - LOG_DEBUG("Thread %s: wait %lu", ThreadName.c_str(), interval); - } + if (showWaiting && enabled && !r) { + LOG_DEBUG("Thread %s: wait %lu", ThreadName.c_str(), interval); + } - if (showDisabled && !enabled) { - LOG_DEBUG("Thread %s: disabled", ThreadName.c_str()); - } + if (showDisabled && !enabled) { + LOG_DEBUG("Thread %s: disabled", ThreadName.c_str()); + } - return r; + return r; } -void OSThread::run() { +void OSThread::run() +{ #ifdef DEBUG_HEAP - auto heap = memGet.getFreeHeap(); + auto heap = memGet.getFreeHeap(); #endif - currentThread = this; - auto newDelay = runOnce(); + currentThread = this; + auto newDelay = runOnce(); #ifdef DEBUG_HEAP - auto newHeap = memGet.getFreeHeap(); - if (newHeap < heap) - LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); - if (heap < newHeap) - LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); + auto newHeap = memGet.getFreeHeap(); + if (newHeap < heap) + LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); + if (heap < newHeap) + LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); #endif #ifdef DEBUG_LOOP_TIMING - LOG_DEBUG("====== Thread next run in: %d", newDelay); + LOG_DEBUG("====== Thread next run in: %d", newDelay); #endif - runned(); + runned(); - if (newDelay >= 0) - setInterval(newDelay); + if (newDelay >= 0) + setInterval(newDelay); - currentThread = NULL; + currentThread = NULL; } -int32_t OSThread::disable() { - enabled = false; - setInterval(INT32_MAX); +int32_t OSThread::disable() +{ + enabled = false; + setInterval(INT32_MAX); - return INT32_MAX; + return INT32_MAX; } /** @@ -113,22 +122,23 @@ int32_t OSThread::disable() { */ bool hasBeenSetup; -void assertIsSetup() { +void assertIsSetup() +{ - /** - * Dear developer comrade - If this assert fails() that means you need to fix the following: - * - * This flag is set **only** when setup() starts, to provide a way for us to check for sloppy static constructor - * calls. Call assertIsSetup() to force a crash if someone tries to create an instance too early. - * - * it is super important to never allocate those object statically. instead, you should explicitly - * new them at a point where you are guaranteed that other objects that this instance - * depends on have already been created. - * - * in particular, for OSThread that means "all instances must be declared via new() in setup() or later" - - * this makes it guaranteed that the global mainController is fully constructed first. - */ - assert(hasBeenSetup); + /** + * Dear developer comrade - If this assert fails() that means you need to fix the following: + * + * This flag is set **only** when setup() starts, to provide a way for us to check for sloppy static constructor calls. + * Call assertIsSetup() to force a crash if someone tries to create an instance too early. + * + * it is super important to never allocate those object statically. instead, you should explicitly + * new them at a point where you are guaranteed that other objects that this instance + * depends on have already been created. + * + * in particular, for OSThread that means "all instances must be declared via new() in setup() or later" - + * this makes it guaranteed that the global mainController is fully constructed first. + */ + assert(hasBeenSetup); } } // namespace concurrency diff --git a/src/concurrency/OSThread.h b/src/concurrency/OSThread.h index ba94043d9..0fbfe2e17 100644 --- a/src/concurrency/OSThread.h +++ b/src/concurrency/OSThread.h @@ -7,7 +7,8 @@ #include "ThreadController.h" #include "concurrency/InterruptableDelay.h" -namespace concurrency { +namespace concurrency +{ extern ThreadController mainController, timerController; extern InterruptableDelay mainDelay; @@ -17,8 +18,7 @@ extern InterruptableDelay mainDelay; /** * @brief Base threading * - * This is a pseudo threading layer that is super easy to port, well suited to our slow network and very ram & power - * efficient. + * This is a pseudo threading layer that is super easy to port, well suited to our slow network and very ram & power efficient. * * TODO FIXME @geeksville * @@ -28,48 +28,49 @@ extern InterruptableDelay mainDelay; * move typedQueue into concurrency * remove freertos from typedqueue */ -class OSThread : public Thread { - ThreadController *controller; +class OSThread : public Thread +{ + ThreadController *controller; - /// Show debugging info for disabled threads - static bool showDisabled; + /// Show debugging info for disabled threads + static bool showDisabled; - /// Show debugging info for threads when we run them - static bool showRun; + /// Show debugging info for threads when we run them + static bool showRun; - /// Show debugging info for threads we decide not to run; - static bool showWaiting; + /// Show debugging info for threads we decide not to run; + static bool showWaiting; -public: - /// For debug printing only (might be null) - static const OSThread *currentThread; + public: + /// For debug printing only (might be null) + static const OSThread *currentThread; - OSThread(const char *name, uint32_t period = 0, ThreadController *controller = &mainController); + OSThread(const char *name, uint32_t period = 0, ThreadController *controller = &mainController); - virtual ~OSThread(); + virtual ~OSThread(); - virtual bool shouldRun(unsigned long time); + virtual bool shouldRun(unsigned long time); - static void setup(); + static void setup(); - virtual int32_t disable(); + virtual int32_t disable(); - /** - * Wait a specified number msecs starting from the current time (rather than the last time we were run) - */ - void setIntervalFromNow(unsigned long _interval); + /** + * Wait a specified number msecs starting from the current time (rather than the last time we were run) + */ + void setIntervalFromNow(unsigned long _interval); -protected: - /** - * The method that will be called each time our thread gets a chance to run - * - * Returns desired period for next invocation (or RUN_SAME for no change) - */ - virtual int32_t runOnce() = 0; - bool sleepOnNextExecution = false; + protected: + /** + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce() = 0; + bool sleepOnNextExecution = false; - // Do not override this - virtual void run(); + // Do not override this + virtual void run(); }; /** diff --git a/src/concurrency/Periodic.h b/src/concurrency/Periodic.h index 9364d94c0..db07145a6 100644 --- a/src/concurrency/Periodic.h +++ b/src/concurrency/Periodic.h @@ -2,21 +2,23 @@ #include "concurrency/OSThread.h" -namespace concurrency { +namespace concurrency +{ /** * @brief Periodically invoke a callback. This just provides C-style callback conventions * rather than a virtual function - FIXME, remove? */ -class Periodic : public OSThread { - int32_t (*callback)(); +class Periodic : public OSThread +{ + int32_t (*callback)(); -public: - // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {} + public: + // callback returns the period for the next callback invocation (or 0 if we should no longer be called) + Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {} -protected: - int32_t runOnce() override { return callback(); } + protected: + int32_t runOnce() override { return callback(); } }; } // namespace concurrency diff --git a/src/configuration.h b/src/configuration.h index 643bea072..650e1cc71 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -68,8 +68,8 @@ along with this program. If not, see . #error APP_VERSION must be set by the build environment #endif -// FIXME: This is still needed by the Bluetooth Stack and needs to be replaced by something better. Remnant of the old -// versioning system. +// FIXME: This is still needed by the Bluetooth Stack and needs to be replaced by something better. Remnant of the old versioning +// system. #ifndef HW_VERSION #define HW_VERSION "1.0" #endif diff --git a/src/detect/LoRaRadioType.h b/src/detect/LoRaRadioType.h index fd3d9b852..a059a3668 100644 --- a/src/detect/LoRaRadioType.h +++ b/src/detect/LoRaRadioType.h @@ -1,17 +1,17 @@ #pragma once enum LoRaRadioType { - NO_RADIO, - STM32WLx_RADIO, - SIM_RADIO, - RF95_RADIO, - SX1262_RADIO, - SX1268_RADIO, - LLCC68_RADIO, - SX1280_RADIO, - LR1110_RADIO, - LR1120_RADIO, - LR1121_RADIO + NO_RADIO, + STM32WLx_RADIO, + SIM_RADIO, + RF95_RADIO, + SX1262_RADIO, + SX1268_RADIO, + LLCC68_RADIO, + SX1280_RADIO, + LR1110_RADIO, + LR1120_RADIO, + LR1121_RADIO }; extern LoRaRadioType radioType; \ No newline at end of file diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index c1d7113a6..83a455de7 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -8,60 +8,82 @@ ScanI2C::ScanI2C() = default; void ScanI2C::scanPort(ScanI2C::I2CPort port) {} void ScanI2C::scanPort(ScanI2C::I2CPort port, uint8_t *address, uint8_t asize) {} -void ScanI2C::setSuppressScreen() { shouldSuppressScreen = true; } +void ScanI2C::setSuppressScreen() +{ + shouldSuppressScreen = true; +} -ScanI2C::FoundDevice ScanI2C::firstScreen() const { - // Allow to override the scanner results for screen - if (shouldSuppressScreen) +ScanI2C::FoundDevice ScanI2C::firstScreen() const +{ + // Allow to override the scanner results for screen + if (shouldSuppressScreen) + return DEVICE_NONE; + + ScanI2C::DeviceType types[] = {SCREEN_SSD1306, SCREEN_SH1106, SCREEN_ST7567, SCREEN_UNKNOWN}; + return firstOfOrNONE(4, types); +} + +ScanI2C::FoundDevice ScanI2C::firstRTC() const +{ + ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE}; + return firstOfOrNONE(4, types); +} + +ScanI2C::FoundDevice ScanI2C::firstKeyboard() const +{ + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; + return firstOfOrNONE(6, types); +} + +ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const +{ + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; + return firstOfOrNONE(9, types); +} + +ScanI2C::FoundDevice ScanI2C::firstAQI() const +{ + ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + return firstOfOrNONE(2, types); +} + +ScanI2C::FoundDevice ScanI2C::firstRGBLED() const +{ + ScanI2C::DeviceType types[] = {NCP5623, LP5562}; + return firstOfOrNONE(2, types); +} + +ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const +{ return DEVICE_NONE; - - ScanI2C::DeviceType types[] = {SCREEN_SSD1306, SCREEN_SH1106, SCREEN_ST7567, SCREEN_UNKNOWN}; - return firstOfOrNONE(4, types); } -ScanI2C::FoundDevice ScanI2C::firstRTC() const { - ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE}; - return firstOfOrNONE(4, types); +bool ScanI2C::exists(ScanI2C::DeviceType) const +{ + return false; } -ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { - ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; - return firstOfOrNONE(6, types); +ScanI2C::FoundDevice ScanI2C::firstOfOrNONE(size_t count, ScanI2C::DeviceType *types) const +{ + return DEVICE_NONE; } -ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; - return firstOfOrNONE(9, types); +size_t ScanI2C::countDevices() const +{ + return 0; } -ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; - return firstOfOrNONE(2, types); -} - -ScanI2C::FoundDevice ScanI2C::firstRGBLED() const { - ScanI2C::DeviceType types[] = {NCP5623, LP5562}; - return firstOfOrNONE(2, types); -} - -ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const { return DEVICE_NONE; } - -bool ScanI2C::exists(ScanI2C::DeviceType) const { return false; } - -ScanI2C::FoundDevice ScanI2C::firstOfOrNONE(size_t count, ScanI2C::DeviceType *types) const { return DEVICE_NONE; } - -size_t ScanI2C::countDevices() const { return 0; } - ScanI2C::DeviceAddress::DeviceAddress(ScanI2C::I2CPort port, uint8_t address) : port(port), address(address) {} ScanI2C::DeviceAddress::DeviceAddress() : DeviceAddress(I2CPort::NO_I2C, 0) {} -bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) const { - return - // If this one has no port and other has a port - (port == NO_I2C && other.port != NO_I2C) - // if both have a port and this one's address is lower - || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); +bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) const +{ + return + // If this one has no port and other has a port + (port == NO_I2C && other.port != NO_I2C) + // if both have a port and this one's address is lower + || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); } ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 48c5b807b..3a79d97c5 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -3,155 +3,156 @@ #include #include -class ScanI2C { -public: - typedef enum DeviceType { - NONE, - SCREEN_SSD1306, - SCREEN_SH1106, - SCREEN_UNKNOWN, // has the same address as the two above but does not respond to the same commands - SCREEN_ST7567, - RTC_RV3028, - RTC_PCF8563, - RTC_PCF85063, - RTC_RX8130CE, - CARDKB, - TDECKKB, - BBQ10KB, - RAK14004, - PMU_AXP192_AXP2101, // has the same adress as the TCA8418KB - BME_680, - BME_280, - BMP_280, - BMP_085, - BMP_3XX, - INA260, - INA219, - INA3221, - MAX17048, - MCP9808, - SHT31, - SHT4X, - SHTC3, - LPS22HB, - QMC6310, - QMI8658, - QMC5883L, - HMC5883L, - PMSA0031, - QMA6100P, - MPU6050, - LIS3DH, - BMA423, - BQ24295, - LSM6DS3, - TCA9535, - TCA9555, - VEML7700, - RCWL9620, - NCP5623, - LP5562, - TSL2591, - OPT3001, - MLX90632, - MLX90614, - AHT10, - BMX160, - DFROBOT_LARK, - NAU7802, - FT6336U, - STK8BAXX, - ICM20948, - SCD4X, - MAX30102, - TPS65233, - MPR121KB, - CGRADSENS, - INA226, - NXP_SE050, - DFROBOT_RAIN, - DPS310, - LTR390UV, - RAK12035, - TCA8418KB, - PCT2075, - CST328, - BQ25896, - BQ27220, - LTR553ALS, - BHI260AP, - BMM150, - TSL2561, - DRV2605, - BH1750, - DA217, - CHSC6X, - CST226SE - } DeviceType; +class ScanI2C +{ + public: + typedef enum DeviceType { + NONE, + SCREEN_SSD1306, + SCREEN_SH1106, + SCREEN_UNKNOWN, // has the same address as the two above but does not respond to the same commands + SCREEN_ST7567, + RTC_RV3028, + RTC_PCF8563, + RTC_PCF85063, + RTC_RX8130CE, + CARDKB, + TDECKKB, + BBQ10KB, + RAK14004, + PMU_AXP192_AXP2101, // has the same adress as the TCA8418KB + BME_680, + BME_280, + BMP_280, + BMP_085, + BMP_3XX, + INA260, + INA219, + INA3221, + MAX17048, + MCP9808, + SHT31, + SHT4X, + SHTC3, + LPS22HB, + QMC6310, + QMI8658, + QMC5883L, + HMC5883L, + PMSA0031, + QMA6100P, + MPU6050, + LIS3DH, + BMA423, + BQ24295, + LSM6DS3, + TCA9535, + TCA9555, + VEML7700, + RCWL9620, + NCP5623, + LP5562, + TSL2591, + OPT3001, + MLX90632, + MLX90614, + AHT10, + BMX160, + DFROBOT_LARK, + NAU7802, + FT6336U, + STK8BAXX, + ICM20948, + SCD4X, + MAX30102, + TPS65233, + MPR121KB, + CGRADSENS, + INA226, + NXP_SE050, + DFROBOT_RAIN, + DPS310, + LTR390UV, + RAK12035, + TCA8418KB, + PCT2075, + CST328, + BQ25896, + BQ27220, + LTR553ALS, + BHI260AP, + BMM150, + TSL2561, + DRV2605, + BH1750, + DA217, + CHSC6X, + CST226SE + } DeviceType; - // typedef uint8_t DeviceAddress; - typedef enum I2CPort { - NO_I2C, - WIRE, - WIRE1, - } I2CPort; + // typedef uint8_t DeviceAddress; + typedef enum I2CPort { + NO_I2C, + WIRE, + WIRE1, + } I2CPort; - typedef struct DeviceAddress { - // set default values for ADDRESS_NONE - I2CPort port = I2CPort::NO_I2C; - uint8_t address = 0; + typedef struct DeviceAddress { + // set default values for ADDRESS_NONE + I2CPort port = I2CPort::NO_I2C; + uint8_t address = 0; - explicit DeviceAddress(I2CPort port, uint8_t address); - DeviceAddress(); + explicit DeviceAddress(I2CPort port, uint8_t address); + DeviceAddress(); - bool operator<(const DeviceAddress &other) const; - } DeviceAddress; + bool operator<(const DeviceAddress &other) const; + } DeviceAddress; - static const DeviceAddress ADDRESS_NONE; + static const DeviceAddress ADDRESS_NONE; - typedef uint8_t RegisterAddress; + typedef uint8_t RegisterAddress; - typedef struct FoundDevice { - DeviceType type; - DeviceAddress address; + typedef struct FoundDevice { + DeviceType type; + DeviceAddress address; - explicit FoundDevice(DeviceType = DeviceType::NONE, DeviceAddress = ADDRESS_NONE); - } FoundDevice; + explicit FoundDevice(DeviceType = DeviceType::NONE, DeviceAddress = ADDRESS_NONE); + } FoundDevice; - static const FoundDevice DEVICE_NONE; + static const FoundDevice DEVICE_NONE; -public: - ScanI2C(); + public: + ScanI2C(); - virtual void scanPort(ScanI2C::I2CPort); - virtual void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t); + virtual void scanPort(ScanI2C::I2CPort); + virtual void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t); - /* - * A bit of a hack, this tells the scanner not to tell later systems there is a screen to avoid enabling it. - */ - void setSuppressScreen(); + /* + * A bit of a hack, this tells the scanner not to tell later systems there is a screen to avoid enabling it. + */ + void setSuppressScreen(); - FoundDevice firstScreen() const; + FoundDevice firstScreen() const; - FoundDevice firstRTC() const; + FoundDevice firstRTC() const; - FoundDevice firstKeyboard() const; + FoundDevice firstKeyboard() const; - FoundDevice firstAccelerometer() const; + FoundDevice firstAccelerometer() const; - FoundDevice firstAQI() const; + FoundDevice firstAQI() const; - FoundDevice firstRGBLED() const; + FoundDevice firstRGBLED() const; - virtual FoundDevice find(DeviceType) const; + virtual FoundDevice find(DeviceType) const; - virtual bool exists(DeviceType) const; + virtual bool exists(DeviceType) const; - virtual size_t countDevices() const; + virtual size_t countDevices() const; -protected: - virtual FoundDevice firstOfOrNONE(size_t, DeviceType[]) const; + protected: + virtual FoundDevice firstOfOrNONE(size_t, DeviceType[]) const; -private: - bool shouldSuppressScreen = false; + private: + bool shouldSuppressScreen = false; }; diff --git a/src/detect/ScanI2CConsumer.cpp b/src/detect/ScanI2CConsumer.cpp index b8ba10d39..a70fa5398 100644 --- a/src/detect/ScanI2CConsumer.cpp +++ b/src/detect/ScanI2CConsumer.cpp @@ -3,10 +3,14 @@ static std::forward_list ScanI2CConsumers; -ScanI2CConsumer::ScanI2CConsumer() { ScanI2CConsumers.push_front(this); } +ScanI2CConsumer::ScanI2CConsumer() +{ + ScanI2CConsumers.push_front(this); +} -void ScanI2CCompleted(ScanI2C *i2cScanner) { - for (ScanI2CConsumer *consumer : ScanI2CConsumers) { - consumer->i2cScanFinished(i2cScanner); - } +void ScanI2CCompleted(ScanI2C *i2cScanner) +{ + for (ScanI2CConsumer *consumer : ScanI2CConsumers) { + consumer->i2cScanFinished(i2cScanner); + } } \ No newline at end of file diff --git a/src/detect/ScanI2CConsumer.h b/src/detect/ScanI2CConsumer.h index 01b0cc1fb..fd97f7edc 100644 --- a/src/detect/ScanI2CConsumer.h +++ b/src/detect/ScanI2CConsumer.h @@ -3,10 +3,11 @@ #include "ScanI2C.h" #include -class ScanI2CConsumer { -public: - ScanI2CConsumer(); - virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0; +class ScanI2CConsumer +{ + public: + ScanI2CConsumer(); + virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0; }; void ScanI2CCompleted(ScanI2C *i2cScanner); \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 3f0031320..8e91d1787 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -10,636 +10,655 @@ #include "meshUtils.h" // vformat #endif -bool in_array(uint8_t *array, int size, uint8_t lookfor) { - int i; - for (i = 0; i < size; i++) - if (lookfor == array[i]) - return true; - return false; +bool in_array(uint8_t *array, int size, uint8_t lookfor) +{ + int i; + for (i = 0; i < size; i++) + if (lookfor == array[i]) + return true; + return false; } -ScanI2C::FoundDevice ScanI2CTwoWire::find(ScanI2C::DeviceType type) const { - concurrency::LockGuard guard((concurrency::Lock *)&lock); +ScanI2C::FoundDevice ScanI2CTwoWire::find(ScanI2C::DeviceType type) const +{ + concurrency::LockGuard guard((concurrency::Lock *)&lock); - return exists(type) ? ScanI2C::FoundDevice(type, deviceAddresses.at(type)) : DEVICE_NONE; + return exists(type) ? ScanI2C::FoundDevice(type, deviceAddresses.at(type)) : DEVICE_NONE; } -bool ScanI2CTwoWire::exists(ScanI2C::DeviceType type) const { return deviceAddresses.find(type) != deviceAddresses.end(); } +bool ScanI2CTwoWire::exists(ScanI2C::DeviceType type) const +{ + return deviceAddresses.find(type) != deviceAddresses.end(); +} -ScanI2C::FoundDevice ScanI2CTwoWire::firstOfOrNONE(size_t count, DeviceType types[]) const { - concurrency::LockGuard guard((concurrency::Lock *)&lock); +ScanI2C::FoundDevice ScanI2CTwoWire::firstOfOrNONE(size_t count, DeviceType types[]) const +{ + concurrency::LockGuard guard((concurrency::Lock *)&lock); - for (size_t k = 0; k < count; k++) { - ScanI2C::DeviceType current = types[k]; + for (size_t k = 0; k < count; k++) { + ScanI2C::DeviceType current = types[k]; - if (exists(current)) { - return ScanI2C::FoundDevice(current, deviceAddresses.at(current)); + if (exists(current)) { + return ScanI2C::FoundDevice(current, deviceAddresses.at(current)); + } } - } - return DEVICE_NONE; + return DEVICE_NONE; } -ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const { - TwoWire *i2cBus = fetchI2CBus(addr); +ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const +{ + TwoWire *i2cBus = fetchI2CBus(addr); - uint8_t r = 0; - uint8_t r_prev = 0; - uint8_t c = 0; - ScanI2C::DeviceType o_probe = ScanI2C::DeviceType::SCREEN_UNKNOWN; - do { - r_prev = r; - i2cBus->beginTransmission(addr.address); - i2cBus->write((uint8_t)0x00); + uint8_t r = 0; + uint8_t r_prev = 0; + uint8_t c = 0; + ScanI2C::DeviceType o_probe = ScanI2C::DeviceType::SCREEN_UNKNOWN; + do { + r_prev = r; + i2cBus->beginTransmission(addr.address); + i2cBus->write((uint8_t)0x00); + i2cBus->endTransmission(); + i2cBus->requestFrom((int)addr.address, 1); + if (i2cBus->available()) { + r = i2cBus->read(); + } + r &= 0x0f; + + if (r == 0x08 || r == 0x00) { + logFoundDevice("SH1106", (uint8_t)addr.address); + o_probe = SCREEN_SH1106; // SH1106 + } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { + logFoundDevice("SSD1306", (uint8_t)addr.address); + o_probe = SCREEN_SSD1306; // SSD1306 + } + c++; + } while ((r != r_prev) && (c < 4)); + LOG_DEBUG("0x%x subtype probed in %i tries ", r, c); + + return o_probe; +} +uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, + ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const +{ + uint16_t value = 0x00; + TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); + + i2cBus->beginTransmission(registerLocation.i2cAddress.address); + i2cBus->write(registerLocation.registerAddress); + if (zeropad) { + // Lark Commands need the argument list length in 2 bytes. + i2cBus->write((int)0); + i2cBus->write((int)0); + } i2cBus->endTransmission(); - i2cBus->requestFrom((int)addr.address, 1); - if (i2cBus->available()) { - r = i2cBus->read(); + delay(20); + i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth); + if (i2cBus->available() > 1) { + // Read MSB, then LSB + value = (uint16_t)i2cBus->read() << 8; + value |= i2cBus->read(); + } else if (i2cBus->available()) { + value = i2cBus->read(); } - r &= 0x0f; - - if (r == 0x08 || r == 0x00) { - logFoundDevice("SH1106", (uint8_t)addr.address); - o_probe = SCREEN_SH1106; // SH1106 - } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { - logFoundDevice("SSD1306", (uint8_t)addr.address); - o_probe = SCREEN_SSD1306; // SSD1306 + // Drain excess bytes + for (uint8_t i = 0; i < responseWidth - 1; i++) { + if (i2cBus->available()) + i2cBus->read(); } - c++; - } while ((r != r_prev) && (c < 4)); - LOG_DEBUG("0x%x subtype probed in %i tries ", r, c); - - return o_probe; -} -uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, ScanI2CTwoWire::ResponseWidth responseWidth, - bool zeropad = false) const { - uint16_t value = 0x00; - TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); - - i2cBus->beginTransmission(registerLocation.i2cAddress.address); - i2cBus->write(registerLocation.registerAddress); - if (zeropad) { - // Lark Commands need the argument list length in 2 bytes. - i2cBus->write((int)0); - i2cBus->write((int)0); - } - i2cBus->endTransmission(); - delay(20); - i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth); - if (i2cBus->available() > 1) { - // Read MSB, then LSB - value = (uint16_t)i2cBus->read() << 8; - value |= i2cBus->read(); - } else if (i2cBus->available()) { - value = i2cBus->read(); - } - // Drain excess bytes - for (uint8_t i = 0; i < responseWidth - 1; i++) { - if (i2cBus->available()) - i2cBus->read(); - } - LOG_DEBUG("Register value: 0x%x", value); - return value; + LOG_DEBUG("Register value: 0x%x", value); + return value; } -#define SCAN_SIMPLE_CASE(ADDR, T, ...) \ - case ADDR: \ - logFoundDevice(__VA_ARGS__); \ - type = T; \ - break; +#define SCAN_SIMPLE_CASE(ADDR, T, ...) \ + case ADDR: \ + logFoundDevice(__VA_ARGS__); \ + type = T; \ + break; -void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) { - concurrency::LockGuard guard((concurrency::Lock *)&lock); +void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) +{ + concurrency::LockGuard guard((concurrency::Lock *)&lock); - LOG_DEBUG("Scan for I2C devices on port %d", port); + LOG_DEBUG("Scan for I2C devices on port %d", port); - uint8_t err; + uint8_t err; - DeviceAddress addr(port, 0x00); + DeviceAddress addr(port, 0x00); - uint16_t registerValue = 0x00; - ScanI2C::DeviceType type; - TwoWire *i2cBus; + uint16_t registerValue = 0x00; + ScanI2C::DeviceType type; + TwoWire *i2cBus; #ifdef RV3028_RTC - Melopero_RV3028 rtc; + Melopero_RV3028 rtc; #endif #if WIRE_INTERFACES_COUNT == 2 - if (port == I2CPort::WIRE1) { - i2cBus = &Wire1; - } else { -#endif - i2cBus = &Wire; -#if WIRE_INTERFACES_COUNT == 2 - } -#endif - - // We only need to scan 112 addresses, the rest is reserved for special purposes - // 0x00 General Call - // 0x01 CBUS addresses - // 0x02 Reserved for different bus formats - // 0x03 Reserved for future purposes - // 0x04-0x07 High Speed Master Code - // 0x78-0x7B 10-bit slave addressing - // 0x7C-0x7F Reserved for future purposes - - for (addr.address = 8; addr.address < 120; addr.address++) { - if (asize != 0) { - if (!in_array(address, asize, (uint8_t)addr.address)) - continue; - LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address); - } - i2cBus->beginTransmission(addr.address); -#ifdef ARCH_PORTDUINO - err = 2; - if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) { - if (i2cBus->read() != -1) - err = 0; + if (port == I2CPort::WIRE1) { + i2cBus = &Wire1; } else { - err = i2cBus->writeQuick((uint8_t)0); - } - if (err != 0) - err = 2; -#else - err = i2cBus->endTransmission(); #endif - type = NONE; - if (err == 0) { - switch (addr.address) { - case SSD1306_ADDRESS: - type = probeOLED(addr); - break; + i2cBus = &Wire; +#if WIRE_INTERFACES_COUNT == 2 + } +#endif + + // We only need to scan 112 addresses, the rest is reserved for special purposes + // 0x00 General Call + // 0x01 CBUS addresses + // 0x02 Reserved for different bus formats + // 0x03 Reserved for future purposes + // 0x04-0x07 High Speed Master Code + // 0x78-0x7B 10-bit slave addressing + // 0x7C-0x7F Reserved for future purposes + + for (addr.address = 8; addr.address < 120; addr.address++) { + if (asize != 0) { + if (!in_array(address, asize, (uint8_t)addr.address)) + continue; + LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address); + } + i2cBus->beginTransmission(addr.address); +#ifdef ARCH_PORTDUINO + err = 2; + if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) { + if (i2cBus->read() != -1) + err = 0; + } else { + err = i2cBus->writeQuick((uint8_t)0); + } + if (err != 0) + err = 2; +#else + err = i2cBus->endTransmission(); +#endif + type = NONE; + if (err == 0) { + switch (addr.address) { + case SSD1306_ADDRESS: + type = probeOLED(addr); + break; #ifdef RV3028_RTC - case RV3028_RTC: - // foundDevices[addr] = RTC_RV3028; - type = RTC_RV3028; - logFoundDevice("RV3028", (uint8_t)addr.address); - rtc.initI2C(*i2cBus); - // Update RTC EEPROM settings, if necessary - if (rtc.readEEPROMRegister(0x35) != 0x07) { - rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout - } - if (rtc.readEEPROMRegister(0x37) != 0xB4) { - rtc.writeEEPROMRegister(0x37, 0xB4); - } - break; + case RV3028_RTC: + // foundDevices[addr] = RTC_RV3028; + type = RTC_RV3028; + logFoundDevice("RV3028", (uint8_t)addr.address); + rtc.initI2C(*i2cBus); + // Update RTC EEPROM settings, if necessary + if (rtc.readEEPROMRegister(0x35) != 0x07) { + rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout + } + if (rtc.readEEPROMRegister(0x37) != 0xB4) { + rtc.writeEEPROMRegister(0x37, 0xB4); + } + break; #endif #ifdef PCF8563_RTC - SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address) #endif #ifdef RX8130CE_RTC - SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) #endif #ifdef PCF85063_RTC - SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) #endif - case CARDKB_ADDR: - // Do we have the RAK14006 instead? - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); - if (registerValue == 0x02) { - // KEYPAD_VERSION - logFoundDevice("RAK14004", (uint8_t)addr.address); - type = RAK14004; - } else { - logFoundDevice("M5 cardKB", (uint8_t)addr.address); - type = CARDKB; - } - break; + case CARDKB_ADDR: + // Do we have the RAK14006 instead? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); + if (registerValue == 0x02) { + // KEYPAD_VERSION + logFoundDevice("RAK14004", (uint8_t)addr.address); + type = RAK14004; + } else { + logFoundDevice("M5 cardKB", (uint8_t)addr.address); + type = CARDKB; + } + break; - case TDECK_KB_ADDR: - // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor? - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); - if (registerValue != 0) { - logFoundDevice("BQ27220", (uint8_t)addr.address); - type = BQ27220; - } else { - logFoundDevice("TDECKKB", (uint8_t)addr.address); - type = TDECKKB; - } - break; - SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address); + case TDECK_KB_ADDR: + // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); + if (registerValue != 0) { + logFoundDevice("BQ27220", (uint8_t)addr.address); + type = BQ27220; + } else { + logFoundDevice("TDECKKB", (uint8_t)addr.address); + type = TDECKKB; + } + break; + SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); #ifdef HAS_NCP5623 - SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); #endif #ifdef HAS_LP5562 - SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); #endif - case XPOWERS_AXP192_AXP2101_ADDRESS: - // Do we have the axp2101/192 or the TCA8418 - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1); - if (registerValue == 0x0) { - logFoundDevice("TCA8418", (uint8_t)addr.address); - type = TCA8418KB; - } else { - logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address); - type = PMU_AXP192_AXP2101; - } - break; - case BME_ADDR: - case BME_ADDR_ALTERNATE: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID - switch (registerValue) { - case 0x61: - logFoundDevice("BME680", (uint8_t)addr.address); - type = BME_680; - break; - case 0x60: - logFoundDevice("BME280", (uint8_t)addr.address); - type = BME_280; - break; - case 0x55: - logFoundDevice("BMP085/BMP180", (uint8_t)addr.address); - type = BMP_085; - break; - case 0x00: - // do we have a DPS310 instead? - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1); - switch (registerValue) { - case 0x10: - logFoundDevice("DPS310", (uint8_t)addr.address); - type = DPS310; - break; - } - break; - default: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID - switch (registerValue) { - case 0x50: // BMP-388 should be 0x50 - logFoundDevice("BMP-388", (uint8_t)addr.address); - type = BMP_3XX; - break; - case 0x60: // BMP-390 should be 0x60 - logFoundDevice("BMP-390", (uint8_t)addr.address); - type = BMP_3XX; - break; - case 0x58: // BMP-280 should be 0x58 - default: - logFoundDevice("BMP-280", (uint8_t)addr.address); - type = BMP_280; - break; - } - break; - } - break; + case XPOWERS_AXP192_AXP2101_ADDRESS: + // Do we have the axp2101/192 or the TCA8418 + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1); + if (registerValue == 0x0) { + logFoundDevice("TCA8418", (uint8_t)addr.address); + type = TCA8418KB; + } else { + logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address); + type = PMU_AXP192_AXP2101; + } + break; + case BME_ADDR: + case BME_ADDR_ALTERNATE: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID + switch (registerValue) { + case 0x61: + logFoundDevice("BME680", (uint8_t)addr.address); + type = BME_680; + break; + case 0x60: + logFoundDevice("BME280", (uint8_t)addr.address); + type = BME_280; + break; + case 0x55: + logFoundDevice("BMP085/BMP180", (uint8_t)addr.address); + type = BMP_085; + break; + case 0x00: + // do we have a DPS310 instead? + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1); + switch (registerValue) { + case 0x10: + logFoundDevice("DPS310", (uint8_t)addr.address); + type = DPS310; + break; + } + break; + default: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID + switch (registerValue) { + case 0x50: // BMP-388 should be 0x50 + logFoundDevice("BMP-388", (uint8_t)addr.address); + type = BMP_3XX; + break; + case 0x60: // BMP-390 should be 0x60 + logFoundDevice("BMP-390", (uint8_t)addr.address); + type = BMP_3XX; + break; + case 0x58: // BMP-280 should be 0x58 + default: + logFoundDevice("BMP-280", (uint8_t)addr.address); + type = BMP_280; + break; + } + break; + } + break; #ifndef HAS_NCP5623 - case AHT10_ADDR: - logFoundDevice("AHT10", (uint8_t)addr.address); - type = AHT10; - break; + case AHT10_ADDR: + logFoundDevice("AHT10", (uint8_t)addr.address); + type = AHT10; + break; #endif #if !defined(M5STACK_UNITC6L) - case INA_ADDR: - case INA_ADDR_ALTERNATE: - case INA_ADDR_WAVESHARE_UPS: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); - LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); - if (registerValue == 0x5449) { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); - LOG_DEBUG("Register DIE_UID: 0x%x", registerValue); + case INA_ADDR: + case INA_ADDR_ALTERNATE: + case INA_ADDR_WAVESHARE_UPS: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); + LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); + if (registerValue == 0x5449) { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); + LOG_DEBUG("Register DIE_UID: 0x%x", registerValue); - if (registerValue == 0x2260) { - logFoundDevice("INA226", (uint8_t)addr.address); - type = INA226; - } else { - logFoundDevice("INA260", (uint8_t)addr.address); - type = INA260; - } - } else { // Assume INA219 if INA260 ID is not found - logFoundDevice("INA219", (uint8_t)addr.address); - type = INA219; - } - break; - case INA3221_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); - LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue); - if (registerValue == 0x5449) { - logFoundDevice("INA3221", (uint8_t)addr.address); - type = INA3221; - } else { - /* check the first 2 bytes of the 6 byte response register - LARK FW 1.0 should return: - RESPONSE_STATUS STATUS_SUCCESS (0x53) - RESPONSE_CMD CMD_GET_VERSION (0x05) - RESPONSE_LEN_L 0x02 - RESPONSE_LEN_H 0x00 - RESPONSE_PAYLOAD 0x01 - RESPONSE_PAYLOAD+1 0x00 - */ - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true); - LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue); - if (registerValue == 0x5305) { - logFoundDevice("DFRobot Lark", (uint8_t)addr.address); - type = DFROBOT_LARK; - } - // else: probably a RAK12500/UBLOX GPS on I2C - } - break; + if (registerValue == 0x2260) { + logFoundDevice("INA226", (uint8_t)addr.address); + type = INA226; + } else { + logFoundDevice("INA260", (uint8_t)addr.address); + type = INA260; + } + } else { // Assume INA219 if INA260 ID is not found + logFoundDevice("INA219", (uint8_t)addr.address); + type = INA219; + } + break; + case INA3221_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); + LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue); + if (registerValue == 0x5449) { + logFoundDevice("INA3221", (uint8_t)addr.address); + type = INA3221; + } else { + /* check the first 2 bytes of the 6 byte response register + LARK FW 1.0 should return: + RESPONSE_STATUS STATUS_SUCCESS (0x53) + RESPONSE_CMD CMD_GET_VERSION (0x05) + RESPONSE_LEN_L 0x02 + RESPONSE_LEN_H 0x00 + RESPONSE_PAYLOAD 0x01 + RESPONSE_PAYLOAD+1 0x00 + */ + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true); + LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue); + if (registerValue == 0x5305) { + logFoundDevice("DFRobot Lark", (uint8_t)addr.address); + type = DFROBOT_LARK; + } + // else: probably a RAK12500/UBLOX GPS on I2C + } + break; #endif - case MCP9808_ADDR: - // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some - // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. - { + case MCP9808_ADDR: + // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some + // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. + { #ifdef HAS_STK8XXX - // Check register 0x00 for 0x8700 response to ID STK8BA53 chip. - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); - if (registerValue == 0x8700) { - type = STK8BAXX; - logFoundDevice("STK8BAXX", (uint8_t)addr.address); - break; - } + // Check register 0x00 for 0x8700 response to ID STK8BA53 chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); + if (registerValue == 0x8700) { + type = STK8BAXX; + logFoundDevice("STK8BAXX", (uint8_t)addr.address); + break; + } #endif - // Check register 0x07 for 0x0400 response to ID MCP9808 chip. - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2); - if (registerValue == 0x0400) { - type = MCP9808; - logFoundDevice("MCP9808", (uint8_t)addr.address); - break; - } + // Check register 0x07 for 0x0400 response to ID MCP9808 chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2); + if (registerValue == 0x0400) { + type = MCP9808; + logFoundDevice("MCP9808", (uint8_t)addr.address); + break; + } - // Check register 0x0F for 0x3300 response to ID LIS3DH chip. - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); - if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 - type = LIS3DH; - logFoundDevice("LIS3DH", (uint8_t)addr.address); - } - break; - } - case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT - case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); - if (registerValue == 0x5449) { - type = OPT3001; - logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number - type = SHT4X; - logFoundDevice("SHT4X", (uint8_t)addr.address); - } else { - type = SHT31; - logFoundDevice("SHT31", (uint8_t)addr.address); - } + // Check register 0x0F for 0x3300 response to ID LIS3DH chip. + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); + if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 + type = LIS3DH; + logFoundDevice("LIS3DH", (uint8_t)addr.address); + } + break; + } + case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT + case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); + if (registerValue == 0x5449) { + type = OPT3001; + logFoundDevice("OPT3001", (uint8_t)addr.address); + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number + type = SHT4X; + logFoundDevice("SHT4X", (uint8_t)addr.address); + } else { + type = SHT31; + logFoundDevice("SHT31", (uint8_t)addr.address); + } - break; + break; - SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) - case RCWL9620_ADDR: - // get MAX30102 PARTID - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); - if (registerValue == 0x15) { - type = MAX30102; - logFoundDevice("MAX30102", (uint8_t)addr.address); - break; - } else { - type = RCWL9620; - logFoundDevice("RCWL9620", (uint8_t)addr.address); - } - break; + SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) + case RCWL9620_ADDR: + // get MAX30102 PARTID + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); + if (registerValue == 0x15) { + type = MAX30102; + logFoundDevice("MAX30102", (uint8_t)addr.address); + break; + } else { + type = RCWL9620; + logFoundDevice("RCWL9620", (uint8_t)addr.address); + } + break; - case LPS22HB_ADDR_ALT: - SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) + case LPS22HB_ADDR_ALT: + SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) - case QMI8658_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID - if (registerValue == 0xC0) { - type = BQ24295; - logFoundDevice("BQ24295", (uint8_t)addr.address); - break; - } - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID - if ((registerValue & 0b00000011) == 0b00000010) { - type = BQ25896; - logFoundDevice("BQ25896", (uint8_t)addr.address); - break; - } - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID - if (registerValue == 0x6A) { - type = LSM6DS3; - logFoundDevice("LSM6DS3", (uint8_t)addr.address); - } else { - type = QMI8658; - logFoundDevice("QMI8658", (uint8_t)addr.address); - } - break; + case QMI8658_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID + if (registerValue == 0xC0) { + type = BQ24295; + logFoundDevice("BQ24295", (uint8_t)addr.address); + break; + } + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID + if ((registerValue & 0b00000011) == 0b00000010) { + type = BQ25896; + logFoundDevice("BQ25896", (uint8_t)addr.address); + break; + } + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID + if (registerValue == 0x6A) { + type = LSM6DS3; + logFoundDevice("LSM6DS3", (uint8_t)addr.address); + } else { + type = QMI8658; + logFoundDevice("QMI8658", (uint8_t)addr.address); + } + break; - SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L", (uint8_t)addr.address) #ifdef HAS_QMA6100P - SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else - SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) #endif - case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); - if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 - type = LIS3DH; - logFoundDevice("LIS3DH", (uint8_t)addr.address); - } else { - type = BMA423; - logFoundDevice("BMA423", (uint8_t)addr.address); - } - break; - case TCA9535_ADDR: - case RAK120352_ADDR: - case RAK120353_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); - if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) - type = RAK12035; - logFoundDevice("RAK12035", (uint8_t)addr.address); - } else { - type = TCA9535; - logFoundDevice("TCA9535", (uint8_t)addr.address); - } + case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); + if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 + type = LIS3DH; + logFoundDevice("LIS3DH", (uint8_t)addr.address); + } else { + type = BMA423; + logFoundDevice("BMA423", (uint8_t)addr.address); + } + break; + case TCA9535_ADDR: + case RAK120352_ADDR: + case RAK120353_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); + if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) + type = RAK12035; + logFoundDevice("RAK12035", (uint8_t)addr.address); + } else { + type = TCA9535; + logFoundDevice("TCA9535", (uint8_t)addr.address); + } - break; + break; - SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); - case TCA9555_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1); - if (registerValue == 0x13) { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); - if (registerValue == 0x81) { - type = DA217; - logFoundDevice("DA217", (uint8_t)addr.address); - } else { - type = TCA9555; - logFoundDevice("TCA9555", (uint8_t)addr.address); - } - } else { - type = TCA9555; - logFoundDevice("TCA9555", (uint8_t)addr.address); - } - break; - case TSL25911_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); - if (registerValue == 0x50) { - type = TSL2591; - logFoundDevice("TSL25911", (uint8_t)addr.address); - } else { - type = TSL2561; - logFoundDevice("TSL2561", (uint8_t)addr.address); - } - break; + SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); + case TCA9555_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1); + if (registerValue == 0x13) { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + if (registerValue == 0x81) { + type = DA217; + logFoundDevice("DA217", (uint8_t)addr.address); + } else { + type = TCA9555; + logFoundDevice("TCA9555", (uint8_t)addr.address); + } + } else { + type = TCA9555; + logFoundDevice("TCA9555", (uint8_t)addr.address); + } + break; + case TSL25911_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + if (registerValue == 0x50) { + type = TSL2591; + logFoundDevice("TSL25911", (uint8_t)addr.address); + } else { + type = TSL2561; + logFoundDevice("TSL2561", (uint8_t)addr.address); + } + break; - SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); - case CST328_ADDR: - // Do we have the CST328 or the CST226SE - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); - if (registerValue == 0xA9) { - type = CST226SE; - logFoundDevice("CST226SE", (uint8_t)addr.address); - } else { - type = CST328; - logFoundDevice("CST328", (uint8_t)addr.address); - } - break; + SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); + case CST328_ADDR: + // Do we have the CST328 or the CST226SE + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); + if (registerValue == 0xA9) { + type = CST226SE; + logFoundDevice("CST226SE", (uint8_t)addr.address); + } else { + type = CST328; + logFoundDevice("CST328", (uint8_t)addr.address); + } + break; - SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); - case LTR553ALS_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register - if (registerValue == 0x92) { // LTR553ALS Part ID - type = LTR553ALS; - logFoundDevice("LTR553ALS", (uint8_t)addr.address); - } else { - // Test BH1750 - send power on command - i2cBus->beginTransmission(addr.address); - i2cBus->write(0x01); // Power On command - uint8_t bh1750_error = i2cBus->endTransmission(); - if (bh1750_error == 0) { - type = BH1750; - logFoundDevice("BH1750", (uint8_t)addr.address); - } else { - LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); - } - } - break; + SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); + case LTR553ALS_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register + if (registerValue == 0x92) { // LTR553ALS Part ID + type = LTR553ALS; + logFoundDevice("LTR553ALS", (uint8_t)addr.address); + } else { + // Test BH1750 - send power on command + i2cBus->beginTransmission(addr.address); + i2cBus->write(0x01); // Power On command + uint8_t bh1750_error = i2cBus->endTransmission(); + if (bh1750_error == 0) { + type = BH1750; + logFoundDevice("BH1750", (uint8_t)addr.address); + } else { + LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); + } + } + break; - SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 - SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); #endif - case MLX90614_ADDR_DEF: - // Do we have the MLX90614 or the MPR121KB or the CST226SE - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); - if (registerValue == 0xAB) { - type = CST226SE; - logFoundDevice("CST226SE", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) { - type = MLX90614; - logFoundDevice("MLX90614", (uint8_t)addr.address); - } else { - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS - if (registerValue == 0xe0) { - type = DRV2605; - logFoundDevice("DRV2605", (uint8_t)addr.address); - } else { - type = MPR121KB; - logFoundDevice("MPR121KB", (uint8_t)addr.address); - } - } - break; + case MLX90614_ADDR_DEF: + // Do we have the MLX90614 or the MPR121KB or the CST226SE + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); + if (registerValue == 0xAB) { + type = CST226SE; + logFoundDevice("CST226SE", (uint8_t)addr.address); + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) { + type = MLX90614; + logFoundDevice("MLX90614", (uint8_t)addr.address); + } else { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS + if (registerValue == 0xe0) { + type = DRV2605; + logFoundDevice("DRV2605", (uint8_t)addr.address); + } else { + type = MPR121KB; + logFoundDevice("MPR121KB", (uint8_t)addr.address); + } + } + break; - case ICM20948_ADDR: // same as BMX160_ADDR - case ICM20948_ADDR_ALT: // same as MPU6050_ADDR - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + case ICM20948_ADDR: // same as BMX160_ADDR + case ICM20948_ADDR_ALT: // same as MPU6050_ADDR + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 - type = ICM20948; - logFoundDevice("ICM20948", (uint8_t)addr.address); - break; + type = ICM20948; + logFoundDevice("ICM20948", (uint8_t)addr.address); + break; #endif - if (registerValue == 0xEA) { - type = ICM20948; - logFoundDevice("ICM20948", (uint8_t)addr.address); - break; - } else if (addr.address == BMX160_ADDR) { - type = BMX160; - logFoundDevice("BMX160", (uint8_t)addr.address); - break; - } else { - type = MPU6050; - logFoundDevice("MPU6050", (uint8_t)addr.address); - break; + if (registerValue == 0xEA) { + type = ICM20948; + logFoundDevice("ICM20948", (uint8_t)addr.address); + break; + } else if (addr.address == BMX160_ADDR) { + type = BMX160; + logFoundDevice("BMX160", (uint8_t)addr.address); + break; + } else { + type = MPU6050; + logFoundDevice("MPU6050", (uint8_t)addr.address); + break; + } + break; + + case CGRADSENS_ADDR: + // Register 0x00 of the RadSens sensor contains is product identifier 0x7D + // Undocumented, but some devices return a product identifier of 0x7A + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); + if (registerValue == 0x7D || registerValue == 0x7A) { + type = CGRADSENS; + logFoundDevice("ClimateGuard RadSens", (uint8_t)addr.address); + break; + } else { + LOG_DEBUG("Unexpected Device ID for RadSense: addr=0x%x id=0x%x", CGRADSENS_ADDR, registerValue); + } + break; + + case 0x48: { + i2cBus->beginTransmission(addr.address); + uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC}; + uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19}; + uint8_t info[5]; + size_t len = 0; + i2cBus->write(getInfo, 5); + i2cBus->endTransmission(); + len = i2cBus->readBytes(info, 5); + if (len == 5 && memcmp(expectedInfo, info, len) == 0) { + LOG_INFO("NXP SE050 crypto chip found"); + type = NXP_SE050; + + } else { + LOG_INFO("FT6336U touchscreen found"); + type = FT6336U; + } + break; + } + + default: + LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); + } + } else if (err == 4) { + LOG_ERROR("Unknown error at address 0x%x", (uint8_t)addr.address); } - break; - case CGRADSENS_ADDR: - // Register 0x00 of the RadSens sensor contains is product identifier 0x7D - // Undocumented, but some devices return a product identifier of 0x7A - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); - if (registerValue == 0x7D || registerValue == 0x7A) { - type = CGRADSENS; - logFoundDevice("ClimateGuard RadSens", (uint8_t)addr.address); - break; - } else { - LOG_DEBUG("Unexpected Device ID for RadSense: addr=0x%x id=0x%x", CGRADSENS_ADDR, registerValue); + // Check if a type was found for the enumerated device - save, if so + if (type != NONE) { + deviceAddresses[type] = addr; + foundDevices[addr] = type; } - break; - - case 0x48: { - i2cBus->beginTransmission(addr.address); - uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC}; - uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19}; - uint8_t info[5]; - size_t len = 0; - i2cBus->write(getInfo, 5); - i2cBus->endTransmission(); - len = i2cBus->readBytes(info, 5); - if (len == 5 && memcmp(expectedInfo, info, len) == 0) { - LOG_INFO("NXP SE050 crypto chip found"); - type = NXP_SE050; - - } else { - LOG_INFO("FT6336U touchscreen found"); - type = FT6336U; - } - break; - } - - default: - LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); - } - } else if (err == 4) { - LOG_ERROR("Unknown error at address 0x%x", (uint8_t)addr.address); } - - // Check if a type was found for the enumerated device - save, if so - if (type != NONE) { - deviceAddresses[type] = addr; - foundDevices[addr] = type; - } - } } -void ScanI2CTwoWire::scanPort(I2CPort port) { scanPort(port, nullptr, 0); } +void ScanI2CTwoWire::scanPort(I2CPort port) +{ + scanPort(port, nullptr, 0); +} -TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) { - if (address.port == ScanI2C::I2CPort::WIRE) { - return &Wire; - } else { +TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) +{ + if (address.port == ScanI2C::I2CPort::WIRE) { + return &Wire; + } else { #if WIRE_INTERFACES_COUNT == 2 - return &Wire1; + return &Wire1; #else - return &Wire; + return &Wire; #endif - } + } } -size_t ScanI2CTwoWire::countDevices() const { return foundDevices.size(); } +size_t ScanI2CTwoWire::countDevices() const +{ + return foundDevices.size(); +} -void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) { LOG_INFO("%s found at address 0x%x", device, address); } +void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) +{ + LOG_INFO("%s found at address 0x%x", device, address); +} #endif diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index a6f9432bf..c5b791920 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -14,45 +14,49 @@ #include "../concurrency/Lock.h" -class ScanI2CTwoWire : public ScanI2C { -public: - void scanPort(ScanI2C::I2CPort) override; +class ScanI2CTwoWire : public ScanI2C +{ + public: + void scanPort(ScanI2C::I2CPort) override; - void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t) override; + void scanPort(ScanI2C::I2CPort, uint8_t *, uint8_t) override; - ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override; + ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override; - bool exists(ScanI2C::DeviceType) const override; + bool exists(ScanI2C::DeviceType) const override; - size_t countDevices() const override; + size_t countDevices() const override; - static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress); + static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress); -protected: - FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; + protected: + FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; -private: - typedef struct RegisterLocation { - DeviceAddress i2cAddress; - RegisterAddress registerAddress; + private: + typedef struct RegisterLocation { + DeviceAddress i2cAddress; + RegisterAddress registerAddress; - RegisterLocation(DeviceAddress deviceAddress, RegisterAddress registerAddress) : i2cAddress(deviceAddress), registerAddress(registerAddress) {} + RegisterLocation(DeviceAddress deviceAddress, RegisterAddress registerAddress) + : i2cAddress(deviceAddress), registerAddress(registerAddress) + { + } - } RegisterLocation; + } RegisterLocation; - typedef uint8_t ResponseWidth; + typedef uint8_t ResponseWidth; - std::map foundDevices; + std::map foundDevices; - // note: prone to overwriting if multiple devices of a type are added at different addresses (rare?) - std::map deviceAddresses; + // note: prone to overwriting if multiple devices of a type are added at different addresses (rare?) + std::map deviceAddresses; - concurrency::Lock lock; + concurrency::Lock lock; - uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; + uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; - DeviceType probeOLED(ScanI2C::DeviceAddress) const; + DeviceType probeOLED(ScanI2C::DeviceAddress) const; - static void logFoundDevice(const char *device, uint8_t address); + static void logFoundDevice(const char *device, uint8_t address); }; #endif \ No newline at end of file diff --git a/src/detect/einkScan.h b/src/detect/einkScan.h index d347d90e9..d20c7b6e5 100644 --- a/src/detect/einkScan.h +++ b/src/detect/einkScan.h @@ -4,60 +4,64 @@ #include "../main.h" #include -void d_writeCommand(uint8_t c) { - SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); - if (PIN_EINK_DC >= 0) - digitalWrite(PIN_EINK_DC, LOW); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, LOW); - SPI1.transfer(c); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, HIGH); - if (PIN_EINK_DC >= 0) - digitalWrite(PIN_EINK_DC, HIGH); - SPI1.endTransaction(); +void d_writeCommand(uint8_t c) +{ + SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + if (PIN_EINK_DC >= 0) + digitalWrite(PIN_EINK_DC, LOW); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, LOW); + SPI1.transfer(c); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, HIGH); + if (PIN_EINK_DC >= 0) + digitalWrite(PIN_EINK_DC, HIGH); + SPI1.endTransaction(); } -void d_writeData(uint8_t d) { - SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, LOW); - SPI1.transfer(d); - if (PIN_EINK_CS >= 0) - digitalWrite(PIN_EINK_CS, HIGH); - SPI1.endTransaction(); +void d_writeData(uint8_t d) +{ + SPI1.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, LOW); + SPI1.transfer(d); + if (PIN_EINK_CS >= 0) + digitalWrite(PIN_EINK_CS, HIGH); + SPI1.endTransaction(); } -unsigned long d_waitWhileBusy(uint16_t busy_time) { - if (PIN_EINK_BUSY >= 0) { - delay(1); // add some margin to become active - unsigned long start = micros(); - while (1) { - if (digitalRead(PIN_EINK_BUSY) != HIGH) - break; - delay(1); - if (digitalRead(PIN_EINK_BUSY) != HIGH) - break; - if (micros() - start > 10000000) - break; - } - unsigned long elapsed = micros() - start; - (void)start; - return elapsed; - } else - return busy_time; +unsigned long d_waitWhileBusy(uint16_t busy_time) +{ + if (PIN_EINK_BUSY >= 0) { + delay(1); // add some margin to become active + unsigned long start = micros(); + while (1) { + if (digitalRead(PIN_EINK_BUSY) != HIGH) + break; + delay(1); + if (digitalRead(PIN_EINK_BUSY) != HIGH) + break; + if (micros() - start > 10000000) + break; + } + unsigned long elapsed = micros() - start; + (void)start; + return elapsed; + } else + return busy_time; } -void scanEInkDevice(void) { - SPI1.begin(); - d_writeCommand(0x22); - d_writeData(0x83); - d_writeCommand(0x20); - eink_found = (d_waitWhileBusy(150) > 0) ? true : false; - if (eink_found) - LOG_DEBUG("EInk display found"); - else - LOG_DEBUG("EInk display not found"); - SPI1.end(); +void scanEInkDevice(void) +{ + SPI1.begin(); + d_writeCommand(0x22); + d_writeData(0x83); + d_writeCommand(0x20); + eink_found = (d_waitWhileBusy(150) > 0) ? true : false; + if (eink_found) + LOG_DEBUG("EInk display found"); + else + LOG_DEBUG("EInk display not found"); + SPI1.end(); } #endif \ No newline at end of file diff --git a/src/freertosinc.h b/src/freertosinc.h index 5867c9c5f..e9e6cd53a 100644 --- a/src/freertosinc.h +++ b/src/freertosinc.h @@ -1,7 +1,7 @@ #pragma once -// The FreeRTOS includes are in a different directory on ESP32 and I can't figure out how to make that work with -// platformio gcc options so this is my quick hack to make things work +// The FreeRTOS includes are in a different directory on ESP32 and I can't figure out how to make that work with platformio gcc +// options so this is my quick hack to make things work #if defined(ARDUINO_ARCH_ESP32) #define HAS_FREE_RTOS diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 67b537891..a61a71dde 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -33,7 +33,10 @@ #endif // Not all platforms have std::size(). -template std::size_t array_count(const T (&)[N]) { return N; } +template std::size_t array_count(const T (&)[N]) +{ + return N; +} #ifndef GPS_SERIAL_PORT #define GPS_SERIAL_PORT Serial1 @@ -58,33 +61,34 @@ static GPSUpdateScheduling scheduling; static bool didSerialInit; static struct uBloxGnssModelInfo { - char swVersion[30]; - char hwVersion[10]; - uint8_t extensionNo; - char extension[10][30]; - uint8_t protocol_version; + char swVersion[30]; + char hwVersion[10]; + uint8_t extensionNo; + char extension[10][30]; + uint8_t protocol_version; } ublox_info; #define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway #define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc) // For logging -static const char *getGPSPowerStateString(GPSPowerState state) { - switch (state) { - case GPS_ACTIVE: - return "ACTIVE"; - case GPS_IDLE: - return "IDLE"; - case GPS_SOFTSLEEP: - return "SOFTSLEEP"; - case GPS_HARDSLEEP: - return "HARDSLEEP"; - case GPS_OFF: - return "OFF"; - default: - assert(false); // Unhandled enum value.. - return "FALSE"; // to make new ESP-IDF happy - } +static const char *getGPSPowerStateString(GPSPowerState state) +{ + switch (state) { + case GPS_ACTIVE: + return "ACTIVE"; + case GPS_IDLE: + return "IDLE"; + case GPS_SOFTSLEEP: + return "SOFTSLEEP"; + case GPS_HARDSLEEP: + return "HARDSLEEP"; + case GPS_OFF: + return "OFF"; + default: + assert(false); // Unhandled enum value.. + return "FALSE"; // to make new ESP-IDF happy + } } #ifdef PIN_GPS_SWITCH @@ -94,369 +98,377 @@ static const char *getGPSPowerStateString(GPSPowerState state) { int lastState = LOW; bool firstrun = true; -static int32_t gpsSwitch() { - if (gps) { - int currentState = digitalRead(PIN_GPS_SWITCH); +static int32_t gpsSwitch() +{ + if (gps) { + int currentState = digitalRead(PIN_GPS_SWITCH); - // if the switch is set to zero, disable the GPS Thread - if (firstrun) - if (currentState == LOW) - lastState = HIGH; + // if the switch is set to zero, disable the GPS Thread + if (firstrun) + if (currentState == LOW) + lastState = HIGH; - if (currentState != lastState) { - if (currentState == LOW) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - if (!firstrun) - playGPSDisableBeep(); - gps->disable(); - } else { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - if (!firstrun) - playGPSEnableBeep(); - gps->enable(); - } - lastState = currentState; + if (currentState != lastState) { + if (currentState == LOW) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + if (!firstrun) + playGPSDisableBeep(); + gps->disable(); + } else { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + if (!firstrun) + playGPSEnableBeep(); + gps->enable(); + } + lastState = currentState; + } + firstrun = false; } - firstrun = false; - } - return 1000; + return 1000; } static concurrency::Periodic *gpsPeriodic; #endif -static void UBXChecksum(uint8_t *message, size_t length) { - uint8_t CK_A = 0, CK_B = 0; +static void UBXChecksum(uint8_t *message, size_t length) +{ + uint8_t CK_A = 0, CK_B = 0; - // Calculate the checksum, starting from the CLASS field (which is message[2]) - for (size_t i = 2; i < length - 2; i++) { - CK_A = (CK_A + message[i]) & 0xFF; - CK_B = (CK_B + CK_A) & 0xFF; - } + // Calculate the checksum, starting from the CLASS field (which is message[2]) + for (size_t i = 2; i < length - 2; i++) { + CK_A = (CK_A + message[i]) & 0xFF; + CK_B = (CK_B + CK_A) & 0xFF; + } - // Place the calculated checksum values in the message - message[length - 2] = CK_A; - message[length - 1] = CK_B; + // Place the calculated checksum values in the message + message[length - 2] = CK_A; + message[length - 1] = CK_B; } // Calculate the checksum for a CAS packet -static void CASChecksum(uint8_t *message, size_t length) { - uint32_t cksum = ((uint32_t)message[5] << 24); // Message ID - cksum += ((uint32_t)message[4]) << 16; // Class - cksum += message[2]; // Payload Len +static void CASChecksum(uint8_t *message, size_t length) +{ + uint32_t cksum = ((uint32_t)message[5] << 24); // Message ID + cksum += ((uint32_t)message[4]) << 16; // Class + cksum += message[2]; // Payload Len - // Iterate over the payload as a series of uint32_t's and - // accumulate the cksum - for (size_t i = 0; i < (length - 10) / 4; i++) { - uint32_t pl = 0; - memcpy(&pl, (message + 6) + (i * sizeof(uint32_t)), sizeof(uint32_t)); // avoid pointer dereference - cksum += pl; - } + // Iterate over the payload as a series of uint32_t's and + // accumulate the cksum + for (size_t i = 0; i < (length - 10) / 4; i++) { + uint32_t pl = 0; + memcpy(&pl, (message + 6) + (i * sizeof(uint32_t)), sizeof(uint32_t)); // avoid pointer dereference + cksum += pl; + } - // Place the checksum values in the message - message[length - 4] = (cksum & 0xFF); - message[length - 3] = (cksum & (0xFF << 8)) >> 8; - message[length - 2] = (cksum & (0xFF << 16)) >> 16; - message[length - 1] = (cksum & (0xFF << 24)) >> 24; + // Place the checksum values in the message + message[length - 4] = (cksum & 0xFF); + message[length - 3] = (cksum & (0xFF << 8)) >> 8; + message[length - 2] = (cksum & (0xFF << 16)) >> 16; + message[length - 1] = (cksum & (0xFF << 24)) >> 24; } // Function to create a ublox packet for editing in memory -uint8_t GPS::makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) { - // Construct the UBX packet - UBXscratch[0] = 0xB5; // header - UBXscratch[1] = 0x62; // header - UBXscratch[2] = class_id; // class - UBXscratch[3] = msg_id; // id - UBXscratch[4] = payload_size; // length - UBXscratch[5] = 0x00; +uint8_t GPS::makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) +{ + // Construct the UBX packet + UBXscratch[0] = 0xB5; // header + UBXscratch[1] = 0x62; // header + UBXscratch[2] = class_id; // class + UBXscratch[3] = msg_id; // id + UBXscratch[4] = payload_size; // length + UBXscratch[5] = 0x00; - UBXscratch[6 + payload_size] = 0x00; // CK_A - UBXscratch[7 + payload_size] = 0x00; // CK_B + UBXscratch[6 + payload_size] = 0x00; // CK_A + UBXscratch[7 + payload_size] = 0x00; // CK_B - for (int i = 0; i < payload_size; i++) { - UBXscratch[6 + i] = pgm_read_byte(&msg[i]); - } - UBXChecksum(UBXscratch, (payload_size + 8)); - return (payload_size + 8); + for (int i = 0; i < payload_size; i++) { + UBXscratch[6 + i] = pgm_read_byte(&msg[i]); + } + UBXChecksum(UBXscratch, (payload_size + 8)); + return (payload_size + 8); } // Function to create a CAS packet for editing in memory -uint8_t GPS::makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) { - // General CAS structure - // | H1 | H2 | payload_len | cls | msg | Payload ... | Checksum | - // Size: | 1 | 1 | 2 | 1 | 1 | payload_len | 4 | - // Pos: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 ... | 6 + payload_len ... | - // |------|------|-------------|------|------|------|--------------|---------------------------| - // | 0xBA | 0xCE | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX ... | 0xXX | 0xXX | 0xXX | 0xXX | +uint8_t GPS::makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg) +{ + // General CAS structure + // | H1 | H2 | payload_len | cls | msg | Payload ... | Checksum | + // Size: | 1 | 1 | 2 | 1 | 1 | payload_len | 4 | + // Pos: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 ... | 6 + payload_len ... | + // |------|------|-------------|------|------|------|--------------|---------------------------| + // | 0xBA | 0xCE | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX | 0xXX ... | 0xXX | 0xXX | 0xXX | 0xXX | - // Construct the CAS packet - UBXscratch[0] = 0xBA; // header 1 (0xBA) - UBXscratch[1] = 0xCE; // header 2 (0xCE) - UBXscratch[2] = payload_size; // length 1 - UBXscratch[3] = 0; // length 2 - UBXscratch[4] = class_id; // class - UBXscratch[5] = msg_id; // id + // Construct the CAS packet + UBXscratch[0] = 0xBA; // header 1 (0xBA) + UBXscratch[1] = 0xCE; // header 2 (0xCE) + UBXscratch[2] = payload_size; // length 1 + UBXscratch[3] = 0; // length 2 + UBXscratch[4] = class_id; // class + UBXscratch[5] = msg_id; // id - UBXscratch[6 + payload_size] = 0x00; // Checksum - UBXscratch[7 + payload_size] = 0x00; - UBXscratch[8 + payload_size] = 0x00; - UBXscratch[9 + payload_size] = 0x00; + UBXscratch[6 + payload_size] = 0x00; // Checksum + UBXscratch[7 + payload_size] = 0x00; + UBXscratch[8 + payload_size] = 0x00; + UBXscratch[9 + payload_size] = 0x00; - for (int i = 0; i < payload_size; i++) { - UBXscratch[6 + i] = pgm_read_byte(&msg[i]); - } - CASChecksum(UBXscratch, (payload_size + 10)); + for (int i = 0; i < payload_size; i++) { + UBXscratch[6 + i] = pgm_read_byte(&msg[i]); + } + CASChecksum(UBXscratch, (payload_size + 10)); #if defined(GPS_DEBUG) && defined(DEBUG_PORT) - LOG_DEBUG("CAS packet: "); - DEBUG_PORT.hexDump(MESHTASTIC_LOG_LEVEL_DEBUG, UBXscratch, payload_size + 10); + LOG_DEBUG("CAS packet: "); + DEBUG_PORT.hexDump(MESHTASTIC_LOG_LEVEL_DEBUG, UBXscratch, payload_size + 10); #endif - return (payload_size + 10); + return (payload_size + 10); } -GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis) { - uint8_t buffer[768] = {0}; - uint8_t b; - int bytesRead = 0; - uint32_t startTimeout = millis() + waitMillis; +GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis) +{ + uint8_t buffer[768] = {0}; + uint8_t b; + int bytesRead = 0; + uint32_t startTimeout = millis() + waitMillis; #ifdef GPS_DEBUG - std::string debugmsg = ""; + std::string debugmsg = ""; #endif - while (millis() < startTimeout) { - if (_serial_gps->available()) { - b = _serial_gps->read(); + while (millis() < startTimeout) { + if (_serial_gps->available()) { + b = _serial_gps->read(); #ifdef GPS_DEBUG - debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.'); + debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.'); #endif - buffer[bytesRead] = b; - bytesRead++; - if ((bytesRead == 767) || (b == '\r')) { + buffer[bytesRead] = b; + bytesRead++; + if ((bytesRead == 767) || (b == '\r')) { #ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); + LOG_DEBUG(debugmsg.c_str()); #endif - if (strnstr((char *)buffer, message, bytesRead) != nullptr) { + if (strnstr((char *)buffer, message, bytesRead) != nullptr) { #ifdef GPS_DEBUG - LOG_DEBUG("Found: %s", message); // Log the found message + LOG_DEBUG("Found: %s", message); // Log the found message #endif - return GNSS_RESPONSE_OK; - } else { - bytesRead = 0; + return GNSS_RESPONSE_OK; + } else { + bytesRead = 0; + } + } } - } } - } - return GNSS_RESPONSE_NONE; + return GNSS_RESPONSE_NONE; } -GPS_RESPONSE GPS::getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) { - uint32_t startTime = millis(); - uint8_t buffer[CAS_ACK_NACK_MSG_SIZE] = {0}; - uint8_t bufferPos = 0; +GPS_RESPONSE GPS::getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) +{ + uint32_t startTime = millis(); + uint8_t buffer[CAS_ACK_NACK_MSG_SIZE] = {0}; + uint8_t bufferPos = 0; - // CAS-ACK-(N)ACK structure - // | H1 | H2 | Payload Len | cls | msg | Payload | Checksum (4) | - // | | | | | | Cls | Msg | Reserved | | - // |------|------|-------------|------|------|------|------|-------------|---------------------------| - // ACK-NACK| 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x00 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | - // ACK-ACK | 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x01 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | + // CAS-ACK-(N)ACK structure + // | H1 | H2 | Payload Len | cls | msg | Payload | Checksum (4) | + // | | | | | | Cls | Msg | Reserved | | + // |------|------|-------------|------|------|------|------|-------------|---------------------------| + // ACK-NACK| 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x00 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | + // ACK-ACK | 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x01 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX | - while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { - if (_serial_gps->available()) { - buffer[bufferPos++] = _serial_gps->read(); + while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { + if (_serial_gps->available()) { + buffer[bufferPos++] = _serial_gps->read(); - // keep looking at the first two bytes of buffer until - // we have found the CAS frame header (0xBA, 0xCE), if not - // keep reading bytes until we find a frame header or we run - // out of time. - if ((bufferPos == 2) && !(buffer[0] == 0xBA && buffer[1] == 0xCE)) { - buffer[0] = buffer[1]; - buffer[1] = 0; - bufferPos = 1; - } - } + // keep looking at the first two bytes of buffer until + // we have found the CAS frame header (0xBA, 0xCE), if not + // keep reading bytes until we find a frame header or we run + // out of time. + if ((bufferPos == 2) && !(buffer[0] == 0xBA && buffer[1] == 0xCE)) { + buffer[0] = buffer[1]; + buffer[1] = 0; + bufferPos = 1; + } + } - // we have read all the bytes required for the Ack/Nack (14-bytes) - // and we must have found a frame to get this far - if (bufferPos == sizeof(buffer) - 1) { - uint8_t msg_cls = buffer[4]; // message class should be 0x05 - uint8_t msg_msg_id = buffer[5]; // message id should be 0x00 or 0x01 - uint8_t payload_cls = buffer[6]; // payload class id - uint8_t payload_msg = buffer[7]; // payload message id + // we have read all the bytes required for the Ack/Nack (14-bytes) + // and we must have found a frame to get this far + if (bufferPos == sizeof(buffer) - 1) { + uint8_t msg_cls = buffer[4]; // message class should be 0x05 + uint8_t msg_msg_id = buffer[5]; // message id should be 0x00 or 0x01 + uint8_t payload_cls = buffer[6]; // payload class id + uint8_t payload_msg = buffer[7]; // payload message id - // Check for an ACK-ACK for the specified class and message id - if ((msg_cls == 0x05) && (msg_msg_id == 0x01) && payload_cls == class_id && payload_msg == msg_id) { + // Check for an ACK-ACK for the specified class and message id + if ((msg_cls == 0x05) && (msg_msg_id == 0x01) && payload_cls == class_id && payload_msg == msg_id) { #ifdef GPS_DEBUG - LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); + LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); #endif - return GNSS_RESPONSE_OK; - } + return GNSS_RESPONSE_OK; + } - // Check for an ACK-NACK for the specified class and message id - if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) { + // Check for an ACK-NACK for the specified class and message id + if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) { #ifdef GPS_DEBUG - LOG_WARN("Got NACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); + LOG_WARN("Got NACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); #endif - return GNSS_RESPONSE_NAK; - } + return GNSS_RESPONSE_NAK; + } - // This isn't the frame we are looking for, clear the buffer - // and try again until we run out of time. - memset(buffer, 0x0, sizeof(buffer)); - bufferPos = 0; + // This isn't the frame we are looking for, clear the buffer + // and try again until we run out of time. + memset(buffer, 0x0, sizeof(buffer)); + bufferPos = 0; + } } - } - return GNSS_RESPONSE_NONE; + return GNSS_RESPONSE_NONE; } -GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) { - uint8_t b; - uint8_t ack = 0; - const uint8_t ackP[2] = {class_id, msg_id}; - uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00}; - uint32_t startTime = millis(); - const char frame_errors[] = "More than 100 frame errors"; - int sCounter = 0; +GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) +{ + uint8_t b; + uint8_t ack = 0; + const uint8_t ackP[2] = {class_id, msg_id}; + uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint32_t startTime = millis(); + const char frame_errors[] = "More than 100 frame errors"; + int sCounter = 0; #ifdef GPS_DEBUG - std::string debugmsg = ""; + std::string debugmsg = ""; #endif - for (int j = 2; j < 6; j++) { - buf[8] += buf[j]; - buf[9] += buf[8]; - } - - for (int j = 0; j < 2; j++) { - buf[6 + j] = ackP[j]; - buf[8] += buf[6 + j]; - buf[9] += buf[8]; - } - - while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { - if (ack > 9) { -#ifdef GPS_DEBUG - LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); -#endif - return GNSS_RESPONSE_OK; // ACK received + for (int j = 2; j < 6; j++) { + buf[8] += buf[j]; + buf[9] += buf[8]; } - if (_serial_gps->available()) { - b = _serial_gps->read(); - if (b == frame_errors[sCounter]) { - sCounter++; - if (sCounter == 26) { + + for (int j = 0; j < 2; j++) { + buf[6 + j] = ackP[j]; + buf[8] += buf[6 + j]; + buf[9] += buf[8]; + } + + while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { + if (ack > 9) { +#ifdef GPS_DEBUG + LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); +#endif + return GNSS_RESPONSE_OK; // ACK received + } + if (_serial_gps->available()) { + b = _serial_gps->read(); + if (b == frame_errors[sCounter]) { + sCounter++; + if (sCounter == 26) { #ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); + LOG_DEBUG(debugmsg.c_str()); #endif - return GNSS_RESPONSE_FRAME_ERRORS; - } - } else { - sCounter = 0; - } + return GNSS_RESPONSE_FRAME_ERRORS; + } + } else { + sCounter = 0; + } #ifdef GPS_DEBUG - debugmsg += vformat("%02X", b); + debugmsg += vformat("%02X", b); #endif - if (b == buf[ack]) { - ack++; - } else { - if (ack == 3 && b == 0x00) { // UBX-ACK-NAK message + if (b == buf[ack]) { + ack++; + } else { + if (ack == 3 && b == 0x00) { // UBX-ACK-NAK message #ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); + LOG_DEBUG(debugmsg.c_str()); #endif - LOG_WARN("Got NAK for class %02X message %02X", class_id, msg_id); - return GNSS_RESPONSE_NAK; // NAK received + LOG_WARN("Got NAK for class %02X message %02X", class_id, msg_id); + return GNSS_RESPONSE_NAK; // NAK received + } + ack = 0; // Reset the acknowledgement counter + } } - ack = 0; // Reset the acknowledgement counter - } } - } #ifdef GPS_DEBUG - LOG_DEBUG(debugmsg.c_str()); - LOG_WARN("No response for class %02X message %02X", class_id, msg_id); + LOG_DEBUG(debugmsg.c_str()); + LOG_WARN("No response for class %02X message %02X", class_id, msg_id); #endif - return GNSS_RESPONSE_NONE; // No response received within timeout + return GNSS_RESPONSE_NONE; // No response received within timeout } /** * @brief * @note New method, this method can wait for the specified class and message ID, and return the payload - * @param *buffer: The message buffer, if there is a response payload message, it will be returned through the buffer - * parameter + * @param *buffer: The message buffer, if there is a response payload message, it will be returned through the buffer parameter * @param size: size of buffer * @param requestedClass: request class constant * @param requestedID: request message ID constant * @retval length of payload message */ -int GPS::getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis) { - uint16_t ubxFrameCounter = 0; - uint32_t startTime = millis(); - uint16_t needRead = 0; +int GPS::getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis) +{ + uint16_t ubxFrameCounter = 0; + uint32_t startTime = millis(); + uint16_t needRead = 0; - while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { - if (_serial_gps->available()) { - int c = _serial_gps->read(); - switch (ubxFrameCounter) { - case 0: - // ubxFrame 'μ' - if (c == 0xB5) { - ubxFrameCounter++; - } - break; - case 1: - // ubxFrame 'b' - if (c == 0x62) { - ubxFrameCounter++; - } else { - ubxFrameCounter = 0; - } - break; - case 2: - // Class - if (c == requestedClass) { - ubxFrameCounter++; - } else { - ubxFrameCounter = 0; - } - break; - case 3: - // Message ID - if (c == requestedID) { - ubxFrameCounter++; - } else { - ubxFrameCounter = 0; - } - break; - case 4: - // Payload length lsb - needRead = c; - ubxFrameCounter++; - break; - case 5: - // Payload length msb - needRead |= (c << 8); - ubxFrameCounter++; - // Check for buffer overflow - if (needRead >= size) { - ubxFrameCounter = 0; - break; - } - if (_serial_gps->readBytes(buffer, needRead) != needRead) { - ubxFrameCounter = 0; - } else { - // return payload length + while (Throttle::isWithinTimespanMs(startTime, waitMillis)) { + if (_serial_gps->available()) { + int c = _serial_gps->read(); + switch (ubxFrameCounter) { + case 0: + // ubxFrame 'μ' + if (c == 0xB5) { + ubxFrameCounter++; + } + break; + case 1: + // ubxFrame 'b' + if (c == 0x62) { + ubxFrameCounter++; + } else { + ubxFrameCounter = 0; + } + break; + case 2: + // Class + if (c == requestedClass) { + ubxFrameCounter++; + } else { + ubxFrameCounter = 0; + } + break; + case 3: + // Message ID + if (c == requestedID) { + ubxFrameCounter++; + } else { + ubxFrameCounter = 0; + } + break; + case 4: + // Payload length lsb + needRead = c; + ubxFrameCounter++; + break; + case 5: + // Payload length msb + needRead |= (c << 8); + ubxFrameCounter++; + // Check for buffer overflow + if (needRead >= size) { + ubxFrameCounter = 0; + break; + } + if (_serial_gps->readBytes(buffer, needRead) != needRead) { + ubxFrameCounter = 0; + } else { + // return payload length #ifdef GPS_DEBUG - LOG_INFO("Got ACK for class %02X message %02X in %dms", requestedClass, requestedID, millis() - startTime); + LOG_INFO("Got ACK for class %02X message %02X in %dms", requestedClass, requestedID, millis() - startTime); #endif - return needRead; - } - break; + return needRead; + } + break; - default: - break; - } + default: + break; + } + } } - } - return 0; + return 0; } #if GPS_BAUDRATE_FIXED @@ -479,1128 +491,1150 @@ static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE}; * to known GPS responses. * @retval Whether setup reached the end of its potential to configure the GPS. */ -bool GPS::setup() { - if (!didSerialInit) { - int msglen = 0; - if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { - if (probeTries < GPS_PROBETRIES) { - gnssModel = probe(serialSpeeds[speedSelect]); - if (gnssModel == GNSS_MODEL_UNKNOWN) { - if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) { - speedSelect = 0; - ++probeTries; - } - } - } - // Rare Serial Speeds +bool GPS::setup() +{ + if (!didSerialInit) { + int msglen = 0; + if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { + if (probeTries < GPS_PROBETRIES) { + gnssModel = probe(serialSpeeds[speedSelect]); + if (gnssModel == GNSS_MODEL_UNKNOWN) { + if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) { + speedSelect = 0; + ++probeTries; + } + } + } + // Rare Serial Speeds #ifndef CONFIG_IDF_TARGET_ESP32C6 - if (probeTries == GPS_PROBETRIES) { - gnssModel = probe(rareSerialSpeeds[speedSelect]); - if (gnssModel == GNSS_MODEL_UNKNOWN) { - if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) { - LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE); - return true; - } - } - } + if (probeTries == GPS_PROBETRIES) { + gnssModel = probe(rareSerialSpeeds[speedSelect]); + if (gnssModel == GNSS_MODEL_UNKNOWN) { + if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) { + LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE); + return true; + } + } + } #endif + } + + if (gnssModel != GNSS_MODEL_UNKNOWN) { + setConnected(); + } else { + return false; + } + + if (gnssModel == GNSS_MODEL_MTK) { + /* + * t-beam-s3-core uses the same L76K GNSS module as t-echo. + * Unlike t-echo, L76K uses 9600 baud rate for communication by default. + * */ + + // Initialize the L76K Chip, use GPS + GLONASS + BEIDOU + _serial_gps->write("$PCAS04,7*1E\r\n"); + delay(250); + // only ask for RMC and GGA + _serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n"); + delay(250); + // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g + _serial_gps->write("$PCAS11,3*1E\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_L76B) { + // Waveshare Pico-GPS hat uses the L76B with 9600 baud + // Initialize the L76B Chip, use GPS + GLONASS + // See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29 + _serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // only ask for RMC and GGA (GNRMC and GNGGA) + // See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1 + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); + // Enable PPS for 2D/3D fix only + _serial_gps->write("$PMTK285,3,100*3F\r\n"); + delay(250); + // Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s) + _serial_gps->write("$PMTK886,1*29\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_PA1010D) { + // PA1010D is used in the Pimoroni GPS board. + + // Enable all constellations. + _serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // Only ask for RMC and GGA (GNRMC and GNGGA) + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS / WAAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_MTK_PA1616S) { + // PA1616S is used in some GPS breakout boards from Adafruit + // PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here. + _serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n"); + // Above command will reset the GPS and takes longer before it will accept new commands + delay(1000); + // Only ask for RMC and GGA (GNRMC and GNGGA) + _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); + delay(250); + // Enable SBAS / WAAS + _serial_gps->write("$PMTK301,2*2E\r\n"); + delay(250); + } else if (gnssModel == GNSS_MODEL_ATGM336H) { + // Set the intial configuration of the device - these _should_ work for most AT6558 devices + msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); + _serial_gps->write(UBXscratch, msglen); + if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { + LOG_WARN("ATGM336H: Could not set Config"); + } + + // Set the update frequence to 1Hz + msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); + _serial_gps->write(UBXscratch, msglen); + if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { + LOG_WARN("ATGM336H: Could not set Update Frequency"); + } + + // Set the NEMA output messages + // Ask for only RMC and GGA + uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA}; + for (unsigned int i = 0; i < sizeof(fields); i++) { + // Construct a CAS-CFG-MSG packet + uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00}; + msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet); + _serial_gps->write(UBXscratch, msglen); + if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) { + LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]); + } + } + } else if (gnssModel == GNSS_MODEL_UC6580) { + // The Unicore UC6580 can use a lot of sat systems, enable it to + // use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS + // This will reset the receiver, so wait a bit afterwards + // The paranoid will wait for the OK*04 confirmation response after each command. + _serial_gps->write("$CFGSYS,h35155\r\n"); + delay(750); + // Must be done after the CFGSYS command + // Turn off GSV messages, we don't really care about which and where the sats are, maybe someday. + _serial_gps->write("$CFGMSG,0,3,0\r\n"); + delay(250); + // Turn off GSA messages, TinyGPS++ doesn't use this message. + _serial_gps->write("$CFGMSG,0,2,0\r\n"); + delay(250); + // Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care. + _serial_gps->write("$CFGMSG,6,0,0\r\n"); + delay(250); + _serial_gps->write("$CFGMSG,6,1,0\r\n"); + delay(250); + } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) { + + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN || + config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) { + _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC + // GPS GLONASS GALILEO BDS QZSS NAVIC + // 1 0 1 0 0 1 + } else { + _serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS + // GPS GLONASS GALILEO BDS QZSS NAVIC + // 1 1 1 1 0 0 + } + // Configure NMEA (sentences will output once per fix) + _serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON + _serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF + _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF + _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF + _serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON + _serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF + _serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON + + delay(250); + _serial_gps->write("$PAIR513*3D\r\n"); // save configuration + } else if (gnssModel == GNSS_MODEL_UBLOX6) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); + SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); + SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); + + // Turn off unwanted NMEA messages, set update rate + SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); + + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500); + + msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); + _serial_gps->write(UBXscratch, msglen); + if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { + LOG_WARN("Unable to save GNSS module config"); + } else { + LOG_INFO("GNSS module config saved!"); + } + } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) { + if (gnssModel == GNSS_MODEL_UBLOX7) { + LOG_DEBUG("Set GPS+SBAS"); + msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7); + _serial_gps->write(UBXscratch, msglen); + } else { // 8,9 + msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8); + _serial_gps->write(UBXscratch, msglen); + } + + if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) { + // It's not critical if the module doesn't acknowledge this configuration. + LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?"); + } else { + if (gnssModel == GNSS_MODEL_UBLOX7) { + LOG_INFO("GPS+SBAS configured"); + } else { // 8,9 + LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); + } + // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next + // commands for the M8 it tends to be more... 1 sec should be enough ;>) + delay(1000); + } + + // Disable Text Info messages //6,7,8,9 + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); + + if (gnssModel == GNSS_MODEL_UBLOX8) { // 8 + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500); + + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500); + } else { // 6,7,9 + SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); + SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); + } + // Turn off unwanted NMEA messages, set update rate + SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); + SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); + + if (ublox_info.protocol_version >= 18) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); + + // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats. + if (gnssModel == GNSS_MODEL_UBLOX8) { + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); + } + } else { + SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500); + SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); + } + + msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); + _serial_gps->write(UBXscratch, msglen); + if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { + LOG_WARN("Unable to save GNSS module config"); + } else { + LOG_INFO("GNSS module configuration saved!"); + } + } else if (gnssModel == GNSS_MODEL_UBLOX10) { + delay(1000); + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300); + delay(750); + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300); + delay(750); + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300); + delay(750); + // Next disable Info txt messages in BBR layer + clearBuffer(); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300); + delay(750); + // Do M10 configuration for Power Management. + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300); + delay(750); + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300); + delay(750); + // Here is where the init commands should go to do further M10 initialization. + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300); + delay(750); // will cause a receiver restart so wait a bit + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300); + delay(750); // will cause a receiver restart so wait a bit + + // Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic + // sleep. + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300); + delay(750); + // Next enable wanted NMEA messages in RAM layer + SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500); + delay(750); + + // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. + // BBR will survive a restart, and power off for a while, but modules with small backup + // batteries or super caps will not retain the config for a long power off time. + msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10); + _serial_gps->write(UBXscratch, msglen); + if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { + LOG_WARN("Unable to save GNSS module config"); + } else { + LOG_INFO("GNSS module configuration saved!"); + } + } else if (gnssModel == GNSS_MODEL_CM121) { + // only ask for RMC and GGA + // enable GGA + _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); + delay(250); + // enable RMC + _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); + delay(250); + } + didSerialInit = true; } - if (gnssModel != GNSS_MODEL_UNKNOWN) { - setConnected(); - } else { - return false; - } + notifyDeepSleepObserver.observe(¬ifyDeepSleep); - if (gnssModel == GNSS_MODEL_MTK) { - /* - * t-beam-s3-core uses the same L76K GNSS module as t-echo. - * Unlike t-echo, L76K uses 9600 baud rate for communication by default. - * */ - - // Initialize the L76K Chip, use GPS + GLONASS + BEIDOU - _serial_gps->write("$PCAS04,7*1E\r\n"); - delay(250); - // only ask for RMC and GGA - _serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n"); - delay(250); - // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g - _serial_gps->write("$PCAS11,3*1E\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_MTK_L76B) { - // Waveshare Pico-GPS hat uses the L76B with 9600 baud - // Initialize the L76B Chip, use GPS + GLONASS - // See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29 - _serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n"); - // Above command will reset the GPS and takes longer before it will accept new commands - delay(1000); - // only ask for RMC and GGA (GNRMC and GNGGA) - // See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1 - _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); - delay(250); - // Enable SBAS - _serial_gps->write("$PMTK301,2*2E\r\n"); - delay(250); - // Enable PPS for 2D/3D fix only - _serial_gps->write("$PMTK285,3,100*3F\r\n"); - delay(250); - // Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s) - _serial_gps->write("$PMTK886,1*29\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_MTK_PA1010D) { - // PA1010D is used in the Pimoroni GPS board. - - // Enable all constellations. - _serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n"); - // Above command will reset the GPS and takes longer before it will accept new commands - delay(1000); - // Only ask for RMC and GGA (GNRMC and GNGGA) - _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); - delay(250); - // Enable SBAS / WAAS - _serial_gps->write("$PMTK301,2*2E\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_MTK_PA1616S) { - // PA1616S is used in some GPS breakout boards from Adafruit - // PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here. - _serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n"); - // Above command will reset the GPS and takes longer before it will accept new commands - delay(1000); - // Only ask for RMC and GGA (GNRMC and GNGGA) - _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); - delay(250); - // Enable SBAS / WAAS - _serial_gps->write("$PMTK301,2*2E\r\n"); - delay(250); - } else if (gnssModel == GNSS_MODEL_ATGM336H) { - // Set the intial configuration of the device - these _should_ work for most AT6558 devices - msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); - _serial_gps->write(UBXscratch, msglen); - if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { - LOG_WARN("ATGM336H: Could not set Config"); - } - - // Set the update frequence to 1Hz - msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); - _serial_gps->write(UBXscratch, msglen); - if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { - LOG_WARN("ATGM336H: Could not set Update Frequency"); - } - - // Set the NEMA output messages - // Ask for only RMC and GGA - uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA}; - for (unsigned int i = 0; i < sizeof(fields); i++) { - // Construct a CAS-CFG-MSG packet - uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00}; - msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet); - _serial_gps->write(UBXscratch, msglen); - if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) { - LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]); - } - } - } else if (gnssModel == GNSS_MODEL_UC6580) { - // The Unicore UC6580 can use a lot of sat systems, enable it to - // use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS - // This will reset the receiver, so wait a bit afterwards - // The paranoid will wait for the OK*04 confirmation response after each command. - _serial_gps->write("$CFGSYS,h35155\r\n"); - delay(750); - // Must be done after the CFGSYS command - // Turn off GSV messages, we don't really care about which and where the sats are, maybe someday. - _serial_gps->write("$CFGMSG,0,3,0\r\n"); - delay(250); - // Turn off GSA messages, TinyGPS++ doesn't use this message. - _serial_gps->write("$CFGMSG,0,2,0\r\n"); - delay(250); - // Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care. - _serial_gps->write("$CFGMSG,6,0,0\r\n"); - delay(250); - _serial_gps->write("$CFGMSG,6,1,0\r\n"); - delay(250); - } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) { - - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN || config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) { - _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC - // GPS GLONASS GALILEO BDS QZSS NAVIC - // 1 0 1 0 0 1 - } else { - _serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS - // GPS GLONASS GALILEO BDS QZSS NAVIC - // 1 1 1 1 0 0 - } - // Configure NMEA (sentences will output once per fix) - _serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON - _serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF - _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF - _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF - _serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON - _serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF - _serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON - - delay(250); - _serial_gps->write("$PAIR513*3D\r\n"); // save configuration - } else if (gnssModel == GNSS_MODEL_UBLOX6) { - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); - SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); - SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); - - // Turn off unwanted NMEA messages, set update rate - SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); - - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500); - SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500); - - msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); - _serial_gps->write(UBXscratch, msglen); - if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { - LOG_WARN("Unable to save GNSS module config"); - } else { - LOG_INFO("GNSS module config saved!"); - } - } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) { - if (gnssModel == GNSS_MODEL_UBLOX7) { - LOG_DEBUG("Set GPS+SBAS"); - msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7); - _serial_gps->write(UBXscratch, msglen); - } else { // 8,9 - msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8); - _serial_gps->write(UBXscratch, msglen); - } - - if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) { - // It's not critical if the module doesn't acknowledge this configuration. - LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?"); - } else { - if (gnssModel == GNSS_MODEL_UBLOX7) { - LOG_INFO("GPS+SBAS configured"); - } else { // 8,9 - LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); - } - // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next - // commands for the M8 it tends to be more... 1 sec should be enough ;>) - delay(1000); - } - - // Disable Text Info messages //6,7,8,9 - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); - - if (gnssModel == GNSS_MODEL_UBLOX8) { // 8 - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500); - - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500); - } else { // 6,7,9 - SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); - SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); - } - // Turn off unwanted NMEA messages, set update rate - SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); - SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); - - if (ublox_info.protocol_version >= 18) { - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); - SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - - // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats. - if (gnssModel == GNSS_MODEL_UBLOX8) { - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); - } - } else { - SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500); - SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - } - - msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); - _serial_gps->write(UBXscratch, msglen); - if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { - LOG_WARN("Unable to save GNSS module config"); - } else { - LOG_INFO("GNSS module configuration saved!"); - } - } else if (gnssModel == GNSS_MODEL_UBLOX10) { - delay(1000); - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300); - delay(750); - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300); - delay(750); - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300); - delay(750); - // Next disable Info txt messages in BBR layer - clearBuffer(); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300); - delay(750); - // Do M10 configuration for Power Management. - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300); - delay(750); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300); - delay(750); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300); - delay(750); - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300); - delay(750); - // Here is where the init commands should go to do further M10 initialization. - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300); - delay(750); // will cause a receiver restart so wait a bit - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300); - delay(750); // will cause a receiver restart so wait a bit - - // Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic - // sleep. - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300); - delay(750); - // Next enable wanted NMEA messages in RAM layer - SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500); - delay(750); - - // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. - // BBR will survive a restart, and power off for a while, but modules with small backup - // batteries or super caps will not retain the config for a long power off time. - msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10); - _serial_gps->write(UBXscratch, msglen); - if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { - LOG_WARN("Unable to save GNSS module config"); - } else { - LOG_INFO("GNSS module configuration saved!"); - } - } else if (gnssModel == GNSS_MODEL_CM121) { - // only ask for RMC and GGA - // enable GGA - _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); - delay(250); - // enable RMC - _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); - delay(250); - } - didSerialInit = true; - } - - notifyDeepSleepObserver.observe(¬ifyDeepSleep); - - return true; + return true; } -GPS::~GPS() { - // we really should unregister our sleep observer - notifyDeepSleepObserver.unobserve(¬ifyDeepSleep); +GPS::~GPS() +{ + // we really should unregister our sleep observer + notifyDeepSleepObserver.unobserve(¬ifyDeepSleep); } // Put the GPS hardware into a specified state -void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) { - // Update the stored GPSPowerstate, and create local copies - GPSPowerState oldState = powerState; - powerState = newState; - LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); +void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) +{ + // Update the stored GPSPowerstate, and create local copies + GPSPowerState oldState = powerState; + powerState = newState; + LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); - switch (newState) { - case GPS_ACTIVE: - case GPS_IDLE: - if (oldState == GPS_ACTIVE || oldState == GPS_IDLE) // If hardware already awake, no changes needed - break; - if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer - clearBuffer(); - powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(true); // Power (EN pin): on - setPowerPMU(true); // Power (PMU): on - writePinStandby(false); // Standby (pin): awake (not standby) - setPowerUBLOX(true); // Standby (UBLOX): awake - break; + switch (newState) { + case GPS_ACTIVE: + case GPS_IDLE: + if (oldState == GPS_ACTIVE || oldState == GPS_IDLE) // If hardware already awake, no changes needed + break; + if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer + clearBuffer(); + powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(true); // Power (EN pin): on + setPowerPMU(true); // Power (PMU): on + writePinStandby(false); // Standby (pin): awake (not standby) + setPowerUBLOX(true); // Standby (UBLOX): awake + break; - case GPS_SOFTSLEEP: - powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(true); // Power (EN pin): on - setPowerPMU(true); // Power (PMU): on - writePinStandby(true); // Standby (pin): asleep (not awake) - setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed - break; + case GPS_SOFTSLEEP: + powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(true); // Power (EN pin): on + setPowerPMU(true); // Power (PMU): on + writePinStandby(true); // Standby (pin): asleep (not awake) + setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed + break; - case GPS_HARDSLEEP: - powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(false); // Power (EN pin): off - setPowerPMU(false); // Power (PMU): off - writePinStandby(true); // Standby (pin): asleep (not awake) - setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed + case GPS_HARDSLEEP: + powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(false); // Power (EN pin): off + setPowerPMU(false); // Power (PMU): off + writePinStandby(true); // Standby (pin): asleep (not awake) + setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed #ifdef GNSS_AIROHA - digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); #endif - break; + break; - case GPS_OFF: - assert(sleepTime == 0); // This is an indefinite sleep - powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) - writePinEN(false); // Power (EN pin): off - setPowerPMU(false); // Power (PMU): off - writePinStandby(true); // Standby (pin): asleep - setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely + case GPS_OFF: + assert(sleepTime == 0); // This is an indefinite sleep + powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing) + writePinEN(false); // Power (EN pin): off + setPowerPMU(false); // Power (PMU): off + writePinStandby(true); // Standby (pin): asleep + setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely #ifdef GNSS_AIROHA - digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); #endif - break; - } + break; + } } // Set power with EN pin, if relevant -void GPS::writePinEN(bool on) { - // Abort: if conflict with Canned Messages when using Wisblock(?) - if ((HW_VENDOR == meshtastic_HardwareModel_RAK4631 || HW_VENDOR == meshtastic_HardwareModel_WISMESH_TAP) && - (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1)) - return; +void GPS::writePinEN(bool on) +{ + // Abort: if conflict with Canned Messages when using Wisblock(?) + if ((HW_VENDOR == meshtastic_HardwareModel_RAK4631 || HW_VENDOR == meshtastic_HardwareModel_WISMESH_TAP) && + (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1)) + return; - // Write and log - enablePin->set(on); + // Write and log + enablePin->set(on); #ifdef GPS_DEBUG - LOG_DEBUG("Pin EN %s", on == HIGH ? "HI" : "LOW"); + LOG_DEBUG("Pin EN %s", on == HIGH ? "HI" : "LOW"); #endif } // Set the value of the STANDBY pin, if relevant // true for standby state, false for awake -void GPS::writePinStandby(bool standby) { +void GPS::writePinStandby(bool standby) +{ #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones // Determine the new value for the pin // Normally: active HIGH for awake #ifdef PIN_GPS_STANDBY_INVERTED - bool val = standby; + bool val = standby; #else - bool val = !standby; + bool val = !standby; #endif - // Write and log - pinMode(PIN_GPS_STANDBY, OUTPUT); - digitalWrite(PIN_GPS_STANDBY, val); + // Write and log + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, val); #ifdef GPS_DEBUG - LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); + LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); #endif #endif } // Enable / Disable GPS with PMU, if present -void GPS::setPowerPMU(bool on) { - // We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera, - // so treat as a standby. +void GPS::setPowerPMU(bool on) +{ + // We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera, + // so treat as a standby. #ifdef HAS_PMU - // Abort: if no PMU - if (!pmu_found) - return; + // Abort: if no PMU + if (!pmu_found) + return; - // Abort: if PMU not initialized - if (!PMU) - return; + // Abort: if PMU not initialized + if (!PMU) + return; - uint8_t model = PMU->getChipModel(); - if (model == XPOWERS_AXP2101) { - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - // t-beam v1.2 GNSS power channel - on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3); - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { - // t-beam-s3-core GNSS power channel - on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4); + uint8_t model = PMU->getChipModel(); + if (model == XPOWERS_AXP2101) { + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + // t-beam v1.2 GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3); + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { + // t-beam-s3-core GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4); + } + } else if (model == XPOWERS_AXP192) { + // t-beam v1.1 GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3); } - } else if (model == XPOWERS_AXP192) { - // t-beam v1.1 GNSS power channel - on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3); - } #ifdef GPS_DEBUG - LOG_DEBUG("PMU %s", on ? "on" : "off"); + LOG_DEBUG("PMU %s", on ? "on" : "off"); #endif #endif } // Set UBLOX power, if relevant -void GPS::setPowerUBLOX(bool on, uint32_t sleepMs) { - // Abort: if not UBLOX hardware - if (!IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) - return; +void GPS::setPowerUBLOX(bool on, uint32_t sleepMs) +{ + // Abort: if not UBLOX hardware + if (!IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) + return; - // If waking - if (on) { - gps->_serial_gps->write(0xFF); - clearBuffer(); // This often returns old data, so drop it - } - - // If putting to sleep - else { - uint8_t msglen; - - // If we're being asked to sleep indefinitely, make *sure* we're awake first, to process the new sleep command - if (sleepMs == 0) { - setPowerUBLOX(true); - delay(500); + // If waking + if (on) { + gps->_serial_gps->write(0xFF); + clearBuffer(); // This often returns old data, so drop it } - // Determine hardware version - if (gnssModel != GNSS_MODEL_UBLOX10) { - // Encode the sleep time in millis into the packet - for (int i = 0; i < 4; i++) - _message_PMREQ[0 + i] = sleepMs >> (i * 8); + // If putting to sleep + else { + uint8_t msglen; - // Record the message length - msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), _message_PMREQ); - } else { - // Encode the sleep time in millis into the packet - for (int i = 0; i < 4; i++) - _message_PMREQ_10[4 + i] = sleepMs >> (i * 8); + // If we're being asked to sleep indefinitely, make *sure* we're awake first, to process the new sleep command + if (sleepMs == 0) { + setPowerUBLOX(true); + delay(500); + } - // Record the message length - msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), _message_PMREQ_10); - } + // Determine hardware version + if (gnssModel != GNSS_MODEL_UBLOX10) { + // Encode the sleep time in millis into the packet + for (int i = 0; i < 4; i++) + _message_PMREQ[0 + i] = sleepMs >> (i * 8); - // Send the UBX packet - gps->_serial_gps->write(gps->UBXscratch, msglen); + // Record the message length + msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), _message_PMREQ); + } else { + // Encode the sleep time in millis into the packet + for (int i = 0; i < 4; i++) + _message_PMREQ_10[4 + i] = sleepMs >> (i * 8); + + // Record the message length + msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), _message_PMREQ_10); + } + + // Send the UBX packet + gps->_serial_gps->write(gps->UBXscratch, msglen); #ifdef GPS_DEBUG - LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs); + LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs); #endif - } + } } /// Record that we have a GPS -void GPS::setConnected() { - if (!hasGPS) { - hasGPS = true; - shouldPublish = true; - } +void GPS::setConnected() +{ + if (!hasGPS) { + hasGPS = true; + shouldPublish = true; + } } // We want a GPS lock. Wake the hardware -void GPS::up() { - scheduling.informSearching(); - setPowerState(GPS_ACTIVE); +void GPS::up() +{ + scheduling.informSearching(); + setPowerState(GPS_ACTIVE); } // We've got a GPS lock. Enter a low power state, potentially. -void GPS::down() { - scheduling.informGotLock(); - uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs(); - uint32_t sleepTime = scheduling.msUntilNextSearch(); - uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); +void GPS::down() +{ + scheduling.informGotLock(); + uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs(); + uint32_t sleepTime = scheduling.msUntilNextSearch(); + uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); - LOG_DEBUG("%us until next search", sleepTime / 1000); + LOG_DEBUG("%us until next search", sleepTime / 1000); - // If update interval less than 10 seconds, no attempt to sleep - if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0) - setPowerState(GPS_IDLE); + // If update interval less than 10 seconds, no attempt to sleep + if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0) + setPowerState(GPS_IDLE); - else { + else { // Check whether the GPS hardware is capable of GPS_SOFTSLEEP // If not, fallback to GPS_HARDSLEEP instead #ifdef PIN_GPS_STANDBY // L76B, L76K and clones have a standby pin - bool softsleepSupported = true; + bool softsleepSupported = true; #else - bool softsleepSupported = false; + bool softsleepSupported = false; #endif - // U-blox is supported via PMREQ - if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) - softsleepSupported = true; + // U-blox is supported via PMREQ + if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) + softsleepSupported = true; - if (softsleepSupported) { - // How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than - // GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M - // and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an - // improvement over a single, fixed threshold - uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22)); - LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep", hardsleepThreshold / 1000); + if (softsleepSupported) { + // How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than + // GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M + // and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an + // improvement over a single, fixed threshold + uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22)); + LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep", hardsleepThreshold / 1000); - // If update interval too short: softsleep (if supported by hardware) - if (updateInterval < hardsleepThreshold) { - setPowerState(GPS_SOFTSLEEP, sleepTime); - return; - } + // If update interval too short: softsleep (if supported by hardware) + if (updateInterval < hardsleepThreshold) { + setPowerState(GPS_SOFTSLEEP, sleepTime); + return; + } + } + // If update interval long enough (or softsleep unsupported): hardsleep instead + setPowerState(GPS_HARDSLEEP, sleepTime); + // Reset the fix quality to 0, since we're off. + fixQual = 0; } - // If update interval long enough (or softsleep unsupported): hardsleep instead - setPowerState(GPS_HARDSLEEP, sleepTime); - // Reset the fix quality to 0, since we're off. - fixQual = 0; - } } -void GPS::publishUpdate() { - if (shouldPublish) { - shouldPublish = false; +void GPS::publishUpdate() +{ + if (shouldPublish) { + shouldPublish = false; - // In debug logs, identify position by @timestamp:stage (stage 2 = publish) - LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock()); + // In debug logs, identify position by @timestamp:stage (stage 2 = publish) + LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock()); - // Notify any status instances that are observing us - const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p); - newStatus.notifyObservers(&status); - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - positionModule->handleNewPosition(); + // Notify any status instances that are observing us + const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p); + newStatus.notifyObservers(&status); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + positionModule->handleNewPosition(); + } } - } } -int32_t GPS::runOnce() { - if (!GPSInitFinished) { - if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - LOG_INFO("GPS set to not-present. Skip probe"); - return disable(); - } - if (!setup()) - return currentDelay; // Setup failed, re-run in two seconds +int32_t GPS::runOnce() +{ + if (!GPSInitFinished) { + if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + LOG_INFO("GPS set to not-present. Skip probe"); + return disable(); + } + if (!setup()) + return currentDelay; // Setup failed, re-run in two seconds - // We have now loaded our saved preferences from flash - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - return disable(); - } - GPSInitFinished = true; - publishUpdate(); - } - - // ======================== GPS_ACTIVE state ======================== - // In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. - // We use the following logic to determine when to update the local position - // or time by running GPS::publishUpdate. - // Note: Local position update is asynchronous to position broadcast. We - // generally run this state every gps_update_interval seconds, and in most cases - // gps_update_interval is faster than the position broadcast interval so there's a - // fresh position ready when the device wants to broadcast one on the mesh. - // - // 1. Got a time for the first time --> set the time, don't publish. - // 2. Got a lock for the first time - // --> If gps_update_interval is <= 10s --> publishUpdate - // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) - // 3. Got a lock after turning back on - // --> If gps_update_interval is <= 10s --> publishUpdate - // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) - // 4. Hold has expired - // --> If we have a time and a location --> publishUpdate - // --> down() - // 5. Search time has expired - // --> If we have a time and a location --> publishUpdate - // --> If we had a location before but don't now --> publishUpdate - // --> down() - if (whileActive()) { - // if we have received valid NMEA claim we are connected - setConnected(); - } - - // If we're due for an update, wake the GPS - if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) - up(); - - // quality of the previous fix. We set it to 0 when we go down, so it's a way - // to check if we're getting a lock after being GPS_OFF. - uint8_t prev_fixQual = fixQual; - - if (powerState == GPS_ACTIVE) { - // if gps_update_interval is <=10s, GPS never goes off, so we treat that differently - uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); - - // 1. Got a time for the first time - bool gotTime = (getRTCQuality() >= RTCQualityGPS); - if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time - gotTime = true; - } - - // 2. Got a lock for the first time, or 3. Got a lock after turning back on - bool gotLoc = lookForLocation(); - if (gotLoc) { -#ifdef GPS_DEBUG - if (!hasValidLocation) { // declare that we have location ASAP - LOG_DEBUG("hasValidLocation RISING EDGE"); - } -#endif - if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) { - hasValidLocation = true; - shouldPublish = true; - } else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) { - hasValidLocation = true; - // Hold for up to 20secs after getting a lock to download ephemeris etc - uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS; - if (holdTime > GPS_FIX_HOLD_MAX_MS) - holdTime = GPS_FIX_HOLD_MAX_MS; - fixHoldEnds = millis() + holdTime; -#ifdef GPS_DEBUG - LOG_DEBUG("Holding for %ums after lock", holdTime); -#endif - } - } - - bool tooLong = scheduling.searchedTooLong(); - if (tooLong && !gotLoc) { - LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time"); - // we didn't get a location during this ack window, therefore declare loss of lock - if (hasValidLocation) { - p = meshtastic_Position_init_default; - hasValidLocation = false; - shouldPublish = true; -#ifdef GPS_DEBUG - LOG_DEBUG("hasValidLocation FALLING EDGE"); -#endif - } - } - - // Hold has expired , Search time has expired, we got a time only, or we never needed to hold. - bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds); - if (shouldPublish || tooLong || holdExpired) { - if (gotTime && hasValidLocation) { - shouldPublish = true; - } - if (shouldPublish) { - fixHoldEnds = 0; + // We have now loaded our saved preferences from flash + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + return disable(); + } + GPSInitFinished = true; publishUpdate(); - } + } - // There's a chance we just got a time, so keep going to see if we can get a location too - if (tooLong || holdExpired) { - down(); - } + // ======================== GPS_ACTIVE state ======================== + // In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. + // We use the following logic to determine when to update the local position + // or time by running GPS::publishUpdate. + // Note: Local position update is asynchronous to position broadcast. We + // generally run this state every gps_update_interval seconds, and in most cases + // gps_update_interval is faster than the position broadcast interval so there's a + // fresh position ready when the device wants to broadcast one on the mesh. + // + // 1. Got a time for the first time --> set the time, don't publish. + // 2. Got a lock for the first time + // --> If gps_update_interval is <= 10s --> publishUpdate + // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) + // 3. Got a lock after turning back on + // --> If gps_update_interval is <= 10s --> publishUpdate + // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) + // 4. Hold has expired + // --> If we have a time and a location --> publishUpdate + // --> down() + // 5. Search time has expired + // --> If we have a time and a location --> publishUpdate + // --> If we had a location before but don't now --> publishUpdate + // --> down() + if (whileActive()) { + // if we have received valid NMEA claim we are connected + setConnected(); + } + + // If we're due for an update, wake the GPS + if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) + up(); + + // quality of the previous fix. We set it to 0 when we go down, so it's a way + // to check if we're getting a lock after being GPS_OFF. + uint8_t prev_fixQual = fixQual; + + if (powerState == GPS_ACTIVE) { + // if gps_update_interval is <=10s, GPS never goes off, so we treat that differently + uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); + + // 1. Got a time for the first time + bool gotTime = (getRTCQuality() >= RTCQualityGPS); + if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time + gotTime = true; + } + + // 2. Got a lock for the first time, or 3. Got a lock after turning back on + bool gotLoc = lookForLocation(); + if (gotLoc) { +#ifdef GPS_DEBUG + if (!hasValidLocation) { // declare that we have location ASAP + LOG_DEBUG("hasValidLocation RISING EDGE"); + } +#endif + if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) { + hasValidLocation = true; + shouldPublish = true; + } else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) { + hasValidLocation = true; + // Hold for up to 20secs after getting a lock to download ephemeris etc + uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS; + if (holdTime > GPS_FIX_HOLD_MAX_MS) + holdTime = GPS_FIX_HOLD_MAX_MS; + fixHoldEnds = millis() + holdTime; +#ifdef GPS_DEBUG + LOG_DEBUG("Holding for %ums after lock", holdTime); +#endif + } + } + + bool tooLong = scheduling.searchedTooLong(); + if (tooLong && !gotLoc) { + LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time"); + // we didn't get a location during this ack window, therefore declare loss of lock + if (hasValidLocation) { + p = meshtastic_Position_init_default; + hasValidLocation = false; + shouldPublish = true; +#ifdef GPS_DEBUG + LOG_DEBUG("hasValidLocation FALLING EDGE"); +#endif + } + } + + // Hold has expired , Search time has expired, we got a time only, or we never needed to hold. + bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds); + if (shouldPublish || tooLong || holdExpired) { + if (gotTime && hasValidLocation) { + shouldPublish = true; + } + if (shouldPublish) { + fixHoldEnds = 0; + publishUpdate(); + } + + // There's a chance we just got a time, so keep going to see if we can get a location too + if (tooLong || holdExpired) { + down(); + } #ifdef GPS_DEBUG - } else if (fixHoldEnds != 0) { - LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); + } else if (fixHoldEnds != 0) { + LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); #endif + } } - } - // ===================== end GPS_ACTIVE state ======================== + // ===================== end GPS_ACTIVE state ======================== - if (config.position.fixed_position == true && hasValidLocation) - return disable(); // This should trigger when we have a fixed position, and get that first position + if (config.position.fixed_position == true && hasValidLocation) + return disable(); // This should trigger when we have a fixed position, and get that first position - // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms - // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake. - return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; + // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms + // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake. + return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; } // clear the GPS rx/tx buffer as quickly as possible -void GPS::clearBuffer() { +void GPS::clearBuffer() +{ #ifdef ARCH_ESP32 - _serial_gps->flush(false); + _serial_gps->flush(false); #else - int x = _serial_gps->available(); - while (x--) - _serial_gps->read(); + int x = _serial_gps->available(); + while (x--) + _serial_gps->read(); #endif } /// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs -int GPS::prepareDeepSleep(void *unused) { - LOG_INFO("GPS deep sleep!"); - disable(); - return 0; +int GPS::prepareDeepSleep(void *unused) +{ + LOG_INFO("GPS deep sleep!"); + disable(); + return 0; } static const char *PROBE_MESSAGE = "Trying %s (%s)..."; static const char *DETECTED_MESSAGE = "%s detected"; -#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \ - do { \ - LOG_DEBUG(PROBE_MESSAGE, TOWRITE, CHIP); \ - clearBuffer(); \ - _serial_gps->write(TOWRITE "\r\n"); \ - if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \ - LOG_INFO(DETECTED_MESSAGE, CHIP); \ - return DRIVER; \ - } \ - } while (0) +#define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \ + do { \ + LOG_DEBUG(PROBE_MESSAGE, TOWRITE, CHIP); \ + clearBuffer(); \ + _serial_gps->write(TOWRITE "\r\n"); \ + if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \ + LOG_INFO(DETECTED_MESSAGE, CHIP); \ + return DRIVER; \ + } \ + } while (0) -#define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \ - do { \ - LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ - clearBuffer(); \ - _serial_gps->write(COMMAND "\r\n"); \ - GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ - if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ - return detectedDriver; \ - } \ - } while (0) +#define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \ + do { \ + LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ + clearBuffer(); \ + _serial_gps->write(COMMAND "\r\n"); \ + GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ + if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ + return detectedDriver; \ + } \ + } while (0) -GnssModel_t GPS::probe(int serialSpeed) { - uint8_t buffer[768] = {0}; +GnssModel_t GPS::probe(int serialSpeed) +{ + uint8_t buffer[768] = {0}; - switch (currentStep) { - case 0: { + switch (currentStep) { + case 0: { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) - _serial_gps->end(); - _serial_gps->begin(serialSpeed); + _serial_gps->end(); + _serial_gps->begin(serialSpeed); #elif defined(ARCH_RP2040) - _serial_gps->end(); - _serial_gps->setFIFOSize(256); - _serial_gps->begin(serialSpeed); + _serial_gps->end(); + _serial_gps->setFIFOSize(256); + _serial_gps->begin(serialSpeed); #else - if (_serial_gps->baudRate() != serialSpeed) { - LOG_DEBUG("Set GPS Baud to %i", serialSpeed); - _serial_gps->updateBaudRate(serialSpeed); - } + if (_serial_gps->baudRate() != serialSpeed) { + LOG_DEBUG("Set GPS Baud to %i", serialSpeed); + _serial_gps->updateBaudRate(serialSpeed); + } #endif - memset(&ublox_info, 0, sizeof(ublox_info)); - delay(100); + memset(&ublox_info, 0, sizeof(ublox_info)); + delay(100); #if defined(PIN_GPS_RESET) && PIN_GPS_RESET != -1 - digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms - delay(10); - digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); + digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms + delay(10); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); - // attempt to detect the chip based on boot messages - std::vector passive_detect = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, - {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, - {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}, - {"UC6580", "UC6580", GNSS_MODEL_UC6580}, - // as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now. - /*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/}; - GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed); - if (detectedDriver != GNSS_MODEL_UNKNOWN) { - return detectedDriver; - } + // attempt to detect the chip based on boot messages + std::vector passive_detect = { + {"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, + {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, + {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}, + {"UC6580", "UC6580", GNSS_MODEL_UC6580}, + // as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now. + /*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/}; + GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed); + if (detectedDriver != GNSS_MODEL_UNKNOWN) { + return detectedDriver; + } #endif - // Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices) - _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); - delay(20); - // Close NMEA sequences on Ublox - _serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"); - _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); - _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); - delay(20); - // Close NMEA sequences on CM121 - _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); - _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); - _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); - currentDelay = 20; - currentStep = 1; - return GNSS_MODEL_UNKNOWN; - } - case 1: { - - // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 - std::vector unicore = { - {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; - PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); - currentDelay = 20; - currentStep = 2; - return GNSS_MODEL_UNKNOWN; - } - case 2: { - std::vector atgm = {{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H}, - /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), - -71-0(GPS+BDS+GLONASS)) based on AT6558 */ - {"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}}; - PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500); - currentDelay = 20; - currentStep = 3; - return GNSS_MODEL_UNKNOWN; - } - case 3: { - /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */ - _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume - _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume - _serial_gps->write("$PAIR513*3D\r\n"); // save configuration - std::vector airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, - {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, - {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}}; - PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000); - currentDelay = 20; - currentStep = 4; - return GNSS_MODEL_UNKNOWN; - } - case 4: { - PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500); - PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500); - currentDelay = 20; - currentStep = 5; - return GNSS_MODEL_UNKNOWN; - } - case 5: { - - // Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms - _serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); - delay(20); - std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, - {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, - {"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B}, - {"L80", "_3339_", GNSS_MODEL_MTK_L76B}}; - - PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); - currentDelay = 20; - currentStep = 6; - return GNSS_MODEL_UNKNOWN; - } - case 6: { - uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; - UBXChecksum(cfg_rate, sizeof(cfg_rate)); - clearBuffer(); - _serial_gps->write(cfg_rate, sizeof(cfg_rate)); - // Check that the returned response class and message ID are correct - GPS_RESPONSE response = getACK(0x06, 0x08, 750); - if (response == GNSS_RESPONSE_NONE) { - LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); - currentDelay = 2000; - currentStep = 0; - return GNSS_MODEL_UNKNOWN; - } else if (response == GNSS_RESPONSE_FRAME_ERRORS) { - LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed); + // Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices) + _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); + delay(20); + // Close NMEA sequences on Ublox + _serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"); + _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); + _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); + delay(20); + // Close NMEA sequences on CM121 + _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); + _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); + _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); + currentDelay = 20; + currentStep = 1; + return GNSS_MODEL_UNKNOWN; } + case 1: { - memset(buffer, 0, sizeof(buffer)); - uint8_t _message_MONVER[8] = { - 0xB5, 0x62, // Sync message for UBX protocol - 0x0A, 0x04, // Message class and ID (UBX-MON-VER) - 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload) - 0x00, 0x00 // Checksum - }; - // Get Ublox gnss module hardware and software info - UBXChecksum(_message_MONVER, sizeof(_message_MONVER)); - clearBuffer(); - _serial_gps->write(_message_MONVER, sizeof(_message_MONVER)); - - uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200); - if (len) { - uint16_t position = 0; - for (int i = 0; i < 30; i++) { - ublox_info.swVersion[i] = buffer[position]; - position++; - } - for (int i = 0; i < 10; i++) { - ublox_info.hwVersion[i] = buffer[position]; - position++; - } - - while (len >= position + 30) { - for (int i = 0; i < 30; i++) { - ublox_info.extension[ublox_info.extensionNo][i] = buffer[position]; - position++; - } - ublox_info.extensionNo++; - if (ublox_info.extensionNo > 9) - break; - } - - LOG_DEBUG("Module Info : "); - LOG_DEBUG("Soft version: %s", ublox_info.swVersion); - LOG_DEBUG("Hard version: %s", ublox_info.hwVersion); - LOG_DEBUG("Extensions:%d", ublox_info.extensionNo); - for (int i = 0; i < ublox_info.extensionNo; i++) { - LOG_DEBUG(" %s", ublox_info.extension[i]); - } - - memset(buffer, 0, sizeof(buffer)); - - // tips: extensionNo field is 0 on some 6M GNSS modules - for (int i = 0; i < ublox_info.extensionNo; ++i) { - if (!strncmp(ublox_info.extension[i], "MOD=", 4)) { - strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer)); - } else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) { - char *ptr = nullptr; - memset(buffer, 0, sizeof(buffer)); - strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer)); - LOG_DEBUG("Protocol Version:%s", (char *)buffer); - if (strlen((char *)buffer)) { - ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10); - LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version); - } else { - ublox_info.protocol_version = 0; - } - } - } - if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6"); - return GNSS_MODEL_UBLOX6; - } else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7"); - return GNSS_MODEL_UBLOX7; - } else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8"); - return GNSS_MODEL_UBLOX8; - } else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9"); - return GNSS_MODEL_UBLOX9; - } else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) { - LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10"); - return GNSS_MODEL_UBLOX10; - } + // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 + std::vector unicore = { + {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; + PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); + currentDelay = 20; + currentStep = 2; + return GNSS_MODEL_UNKNOWN; } - } - } - LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); - currentDelay = 2000; - currentStep = 0; - return GNSS_MODEL_UNKNOWN; + case 2: { + std::vector atgm = { + {"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H}, + /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */ + {"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}}; + PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500); + currentDelay = 20; + currentStep = 3; + return GNSS_MODEL_UNKNOWN; + } + case 3: { + /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */ + _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume + _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume + _serial_gps->write("$PAIR513*3D\r\n"); // save configuration + std::vector airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, + {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, + {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}}; + PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000); + currentDelay = 20; + currentStep = 4; + return GNSS_MODEL_UNKNOWN; + } + case 4: { + PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500); + PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500); + currentDelay = 20; + currentStep = 5; + return GNSS_MODEL_UNKNOWN; + } + case 5: { + + // Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms + _serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); + delay(20); + std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, + {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, + {"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B}, + {"L80", "_3339_", GNSS_MODEL_MTK_L76B}}; + + PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); + currentDelay = 20; + currentStep = 6; + return GNSS_MODEL_UNKNOWN; + } + case 6: { + uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; + UBXChecksum(cfg_rate, sizeof(cfg_rate)); + clearBuffer(); + _serial_gps->write(cfg_rate, sizeof(cfg_rate)); + // Check that the returned response class and message ID are correct + GPS_RESPONSE response = getACK(0x06, 0x08, 750); + if (response == GNSS_RESPONSE_NONE) { + LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); + currentDelay = 2000; + currentStep = 0; + return GNSS_MODEL_UNKNOWN; + } else if (response == GNSS_RESPONSE_FRAME_ERRORS) { + LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed); + } + + memset(buffer, 0, sizeof(buffer)); + uint8_t _message_MONVER[8] = { + 0xB5, 0x62, // Sync message for UBX protocol + 0x0A, 0x04, // Message class and ID (UBX-MON-VER) + 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload) + 0x00, 0x00 // Checksum + }; + // Get Ublox gnss module hardware and software info + UBXChecksum(_message_MONVER, sizeof(_message_MONVER)); + clearBuffer(); + _serial_gps->write(_message_MONVER, sizeof(_message_MONVER)); + + uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200); + if (len) { + uint16_t position = 0; + for (int i = 0; i < 30; i++) { + ublox_info.swVersion[i] = buffer[position]; + position++; + } + for (int i = 0; i < 10; i++) { + ublox_info.hwVersion[i] = buffer[position]; + position++; + } + + while (len >= position + 30) { + for (int i = 0; i < 30; i++) { + ublox_info.extension[ublox_info.extensionNo][i] = buffer[position]; + position++; + } + ublox_info.extensionNo++; + if (ublox_info.extensionNo > 9) + break; + } + + LOG_DEBUG("Module Info : "); + LOG_DEBUG("Soft version: %s", ublox_info.swVersion); + LOG_DEBUG("Hard version: %s", ublox_info.hwVersion); + LOG_DEBUG("Extensions:%d", ublox_info.extensionNo); + for (int i = 0; i < ublox_info.extensionNo; i++) { + LOG_DEBUG(" %s", ublox_info.extension[i]); + } + + memset(buffer, 0, sizeof(buffer)); + + // tips: extensionNo field is 0 on some 6M GNSS modules + for (int i = 0; i < ublox_info.extensionNo; ++i) { + if (!strncmp(ublox_info.extension[i], "MOD=", 4)) { + strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer)); + } else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) { + char *ptr = nullptr; + memset(buffer, 0, sizeof(buffer)); + strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer)); + LOG_DEBUG("Protocol Version:%s", (char *)buffer); + if (strlen((char *)buffer)) { + ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10); + LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version); + } else { + ublox_info.protocol_version = 0; + } + } + } + if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6"); + return GNSS_MODEL_UBLOX6; + } else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7"); + return GNSS_MODEL_UBLOX7; + } else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8"); + return GNSS_MODEL_UBLOX8; + } else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9"); + return GNSS_MODEL_UBLOX9; + } else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) { + LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10"); + return GNSS_MODEL_UBLOX10; + } + } + } + } + LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); + currentDelay = 2000; + currentStep = 0; + return GNSS_MODEL_UNKNOWN; } -GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { - // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline - // Higher baud rates get proportionally larger buffers to handle more data - int bufferSize = (serialSpeed * 256) / 9600; - // Clamp buffer size between reasonable limits - if (bufferSize < 128) - bufferSize = 128; - if (bufferSize > 2048) - bufferSize = 2048; +GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) +{ + // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline + // Higher baud rates get proportionally larger buffers to handle more data + int bufferSize = (serialSpeed * 256) / 9600; + // Clamp buffer size between reasonable limits + if (bufferSize < 128) + bufferSize = 128; + if (bufferSize > 2048) + bufferSize = 2048; - char *response = new char[bufferSize](); // Dynamically allocate based on baud rate - uint16_t responseLen = 0; - unsigned long start = millis(); - while (millis() - start < timeout) { - if (_serial_gps->available()) { - char c = _serial_gps->read(); + char *response = new char[bufferSize](); // Dynamically allocate based on baud rate + uint16_t responseLen = 0; + unsigned long start = millis(); + while (millis() - start < timeout) { + if (_serial_gps->available()) { + char c = _serial_gps->read(); - // Add char to buffer if there's space - if (responseLen < bufferSize - 1) { - response[responseLen++] = c; - response[responseLen] = '\0'; - } + // Add char to buffer if there's space + if (responseLen < bufferSize - 1) { + response[responseLen++] = c; + response[responseLen] = '\0'; + } - if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { - // check if we can see our chips - for (const auto &chipInfo : responseMap) { - if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { + if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { + // check if we can see our chips + for (const auto &chipInfo : responseMap) { + if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { #ifdef GPS_DEBUG - LOG_DEBUG(response); + LOG_DEBUG(response); #endif - LOG_INFO("%s detected", chipInfo.chipName.c_str()); - delete[] response; // Cleanup before return - return chipInfo.driver; - } + LOG_INFO("%s detected", chipInfo.chipName.c_str()); + delete[] response; // Cleanup before return + return chipInfo.driver; + } + } + } + if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { +#ifdef GPS_DEBUG + LOG_DEBUG(response); +#endif + // Reset the response buffer for the next potential message + responseLen = 0; + response[0] = '\0'; + } } - } - if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { -#ifdef GPS_DEBUG - LOG_DEBUG(response); -#endif - // Reset the response buffer for the next potential message - responseLen = 0; - response[0] = '\0'; - } } - } #ifdef GPS_DEBUG - LOG_DEBUG(response); + LOG_DEBUG(response); #endif - delete[] response; // Cleanup before return - return GNSS_MODEL_UNKNOWN; // Return unknown on timeout + delete[] response; // Cleanup before return + return GNSS_MODEL_UNKNOWN; // Return unknown on timeout } -GPS *GPS::createGps() { - int8_t _rx_gpio = config.position.rx_gpio; - int8_t _tx_gpio = config.position.tx_gpio; - int8_t _en_gpio = config.position.gps_en_gpio; +GPS *GPS::createGps() +{ + int8_t _rx_gpio = config.position.rx_gpio; + int8_t _tx_gpio = config.position.tx_gpio; + int8_t _en_gpio = config.position.gps_en_gpio; #if defined(GPS_RX_PIN) - if (!_rx_gpio) - _rx_gpio = GPS_RX_PIN; + if (!_rx_gpio) + _rx_gpio = GPS_RX_PIN; #endif #if defined(GPS_TX_PIN) - if (!_tx_gpio) - _tx_gpio = GPS_TX_PIN; + if (!_tx_gpio) + _tx_gpio = GPS_TX_PIN; #endif #if defined(PIN_GPS_EN) - if (!_en_gpio) - _en_gpio = PIN_GPS_EN; + if (!_en_gpio) + _en_gpio = PIN_GPS_EN; #endif #ifdef ARCH_PORTDUINO - if (!portduino_config.has_gps) - return nullptr; + if (!portduino_config.has_gps) + return nullptr; #endif - if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all - return nullptr; + if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all + return nullptr; - GPS *new_gps = new GPS; - new_gps->rx_gpio = _rx_gpio; - new_gps->tx_gpio = _tx_gpio; + GPS *new_gps = new GPS; + new_gps->rx_gpio = _rx_gpio; + new_gps->tx_gpio = _tx_gpio; - GpioVirtPin *virtPin = new GpioVirtPin(); - new_gps->enablePin = virtPin; // Always at least populate a virtual pin - if (_en_gpio) { - GpioPin *p = new GpioHwPin(_en_gpio); + GpioVirtPin *virtPin = new GpioVirtPin(); + new_gps->enablePin = virtPin; // Always at least populate a virtual pin + if (_en_gpio) { + GpioPin *p = new GpioHwPin(_en_gpio); - if (!GPS_EN_ACTIVE) { // Need to invert the pin before hardware - new GpioNotTransformer(virtPin, - p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio - } else { - new GpioUnaryTransformer(virtPin, - p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio + if (!GPS_EN_ACTIVE) { // Need to invert the pin before hardware + new GpioNotTransformer( + virtPin, + p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio + } else { + new GpioUnaryTransformer( + virtPin, + p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio + } } - } #ifdef PIN_GPS_PPS - // pulse per second - pinMode(PIN_GPS_PPS, INPUT); + // pulse per second + pinMode(PIN_GPS_PPS, INPUT); #endif #ifdef PIN_GPS_SWITCH - // toggle GPS via external GPIO switch - pinMode(PIN_GPS_SWITCH, INPUT); - gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); + // toggle GPS via external GPIO switch + pinMode(PIN_GPS_SWITCH, INPUT); + gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); #endif // Currently disabled per issue #525 (TinyGPS++ crash bug) // when fixed upstream, can be un-disabled to enable 3D FixType and PDOP #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - // see NMEAGPS.h - gsafixtype.begin(reader, NMEA_MSG_GXGSA, 2); - gsapdop.begin(reader, NMEA_MSG_GXGSA, 15); - LOG_DEBUG("Use " NMEA_MSG_GXGSA " for 3DFIX and PDOP"); + // see NMEAGPS.h + gsafixtype.begin(reader, NMEA_MSG_GXGSA, 2); + gsapdop.begin(reader, NMEA_MSG_GXGSA, 15); + LOG_DEBUG("Use " NMEA_MSG_GXGSA " for 3DFIX and PDOP"); #endif - // Make sure the GPS is awake before performing any init. - new_gps->up(); + // Make sure the GPS is awake before performing any init. + new_gps->up(); #ifdef PIN_GPS_RESET - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); #endif - if (_serial_gps) { + if (_serial_gps) { #ifdef ARCH_ESP32 - // In esp32 framework, setRxBufferSize needs to be initialized before Serial - _serial_gps->setRxBufferSize(SERIAL_BUFFER_SIZE); // the default is 256 + // In esp32 framework, setRxBufferSize needs to be initialized before Serial + _serial_gps->setRxBufferSize(SERIAL_BUFFER_SIZE); // the default is 256 #endif - LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio); - LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio); + LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio); + LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio); // ESP32 has a special set of parameters vs other arduino ports #if defined(ARCH_ESP32) - _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, new_gps->rx_gpio, new_gps->tx_gpio); + _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, new_gps->rx_gpio, new_gps->tx_gpio); #elif defined(ARCH_RP2040) - _serial_gps->setPinout(new_gps->tx_gpio, new_gps->rx_gpio); - _serial_gps->setFIFOSize(256); - _serial_gps->begin(GPS_BAUDRATE); + _serial_gps->setPinout(new_gps->tx_gpio, new_gps->rx_gpio); + _serial_gps->setFIFOSize(256); + _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_NRF52) - _serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio); - _serial_gps->begin(GPS_BAUDRATE); + _serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio); + _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_STM32WL) - _serial_gps->setTx(new_gps->tx_gpio); - _serial_gps->setRx(new_gps->rx_gpio); - _serial_gps->begin(GPS_BAUDRATE); + _serial_gps->setTx(new_gps->tx_gpio); + _serial_gps->setRx(new_gps->rx_gpio); + _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_PORTDUINO) - // Portduino can't set the GPS pins directly. - _serial_gps->begin(GPS_BAUDRATE); + // Portduino can't set the GPS pins directly. + _serial_gps->begin(GPS_BAUDRATE); #else #error Unsupported architecture! #endif - } - return new_gps; + } + return new_gps; } -static int32_t toDegInt(RawDegrees d) { - int32_t degMult = 10000000; // 1e7 - int32_t r = d.deg * degMult + d.billionths / 100; - if (d.negative) - r *= -1; - return r; +static int32_t toDegInt(RawDegrees d) +{ + int32_t degMult = 10000000; // 1e7 + int32_t r = d.deg * degMult + d.billionths / 100; + if (d.negative) + r *= -1; + return r; } /** @@ -1609,33 +1643,35 @@ static int32_t toDegInt(RawDegrees d) { * * @return true if we've set a new time */ -bool GPS::lookForTime() { - auto ti = reader.time; - auto d = reader.date; - if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed - /* Convert to unix time -The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January -1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). +bool GPS::lookForTime() +{ + auto ti = reader.time; + auto d = reader.date; + if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed + /* Convert to unix time +The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, +1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). */ - struct tm t; - t.tm_sec = ti.second() + round(ti.age() / 1000); - t.tm_min = ti.minute(); - t.tm_hour = ti.hour(); - t.tm_mday = d.day(); - t.tm_mon = d.month() - 1; - t.tm_year = d.year() - 1900; - t.tm_isdst = false; - if (t.tm_mon > -1) { - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { - LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); - return true; - } else { - return false; - } + struct tm t; + t.tm_sec = ti.second() + round(ti.age() / 1000); + t.tm_min = ti.minute(); + t.tm_hour = ti.hour(); + t.tm_mday = d.day(); + t.tm_mon = d.month() - 1; + t.tm_year = d.year() - 1900; + t.tm_isdst = false; + if (t.tm_mon > -1) { + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, + t.tm_min, t.tm_sec, ti.age()); + return true; + } else { + return false; + } + } else + return false; } else - return false; - } else - return false; + return false; } /** @@ -1644,227 +1680,238 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s * * @return true if we've acquired a new location */ -bool GPS::lookForLocation() { - // By default, TinyGPS++ does not parse GPGSA lines, which give us - // the 2D/3D fixType (see NMEAGPS.h) - // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) - fixQual = reader.fixQuality(); +bool GPS::lookForLocation() +{ + // By default, TinyGPS++ does not parse GPGSA lines, which give us + // the 2D/3D fixType (see NMEAGPS.h) + // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) + fixQual = reader.fixQuality(); #ifndef TINYGPS_OPTION_NO_STATISTICS - if (reader.failedChecksum() > lastChecksumFailCount) { + if (reader.failedChecksum() > lastChecksumFailCount) { // In a GPS_DEBUG build we want to log all of these. In production, we only care if there are many of them. #ifndef GPS_DEBUG - if (reader.failedChecksum() > 4) + if (reader.failedChecksum() > 4) #endif - LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount, reader.failedChecksum()); - lastChecksumFailCount = reader.failedChecksum(); - } + LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount, + reader.failedChecksum()); + lastChecksumFailCount = reader.failedChecksum(); + } #endif #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - fixType = atoi(gsafixtype.value()); // will set to zero if no data + fixType = atoi(gsafixtype.value()); // will set to zero if no data #endif - // check if GPS has an acceptable lock - if (!hasLock()) - return false; + // check if GPS has an acceptable lock + if (!hasLock()) + return false; #ifdef GPS_DEBUG - LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(), + LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(), #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - gsafixtype.age(), + gsafixtype.age(), #else - 0, + 0, #endif - reader.date.age(), reader.time.age()); + reader.date.age(), reader.time.age()); #endif // GPS_DEBUG - // Is this a new point or are we re-reading the previous one? - if (!reader.location.isUpdated() && !reader.altitude.isUpdated()) - return false; + // Is this a new point or are we re-reading the previous one? + if (!reader.location.isUpdated() && !reader.altitude.isUpdated()) + return false; - // check if a complete GPS solution set is available for reading - // tinyGPSDatum::age() also includes isValid() test - // FIXME - if (!((reader.location.age() < GPS_SOL_EXPIRY_MS) && + // check if a complete GPS solution set is available for reading + // tinyGPSDatum::age() also includes isValid() test + // FIXME + if (!((reader.location.age() < GPS_SOL_EXPIRY_MS) && #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - (gsafixtype.age() < GPS_SOL_EXPIRY_MS) && + (gsafixtype.age() < GPS_SOL_EXPIRY_MS) && #endif - (reader.time.age() < GPS_SOL_EXPIRY_MS) && (reader.date.age() < GPS_SOL_EXPIRY_MS))) { - LOG_WARN("SOME data is TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age()); - return false; - } + (reader.time.age() < GPS_SOL_EXPIRY_MS) && (reader.date.age() < GPS_SOL_EXPIRY_MS))) { + LOG_WARN("SOME data is TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age()); + return false; + } - // We know the solution is fresh and valid, so just read the data - auto loc = reader.location.value(); + // We know the solution is fresh and valid, so just read the data + auto loc = reader.location.value(); - // Bail out EARLY to avoid overwriting previous good data (like #857) - if (toDegInt(loc.lat) > 900000000) { + // Bail out EARLY to avoid overwriting previous good data (like #857) + if (toDegInt(loc.lat) > 900000000) { #ifdef GPS_DEBUG - LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat)); + LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat)); #endif - return false; - } - if (toDegInt(loc.lng) > 1800000000) { + return false; + } + if (toDegInt(loc.lng) > 1800000000) { #ifdef GPS_DEBUG - LOG_DEBUG("Bail out EARLY on LNG %i", toDegInt(loc.lng)); + LOG_DEBUG("Bail out EARLY on LNG %i", toDegInt(loc.lng)); #endif - return false; - } + return false; + } - p.location_source = meshtastic_Position_LocSource_LOC_INTERNAL; + p.location_source = meshtastic_Position_LocSource_LOC_INTERNAL; - // Dilution of precision (an accuracy metric) is reported in 10^2 units, so we need to scale down when we use it + // Dilution of precision (an accuracy metric) is reported in 10^2 units, so we need to scale down when we use it #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - p.HDOP = reader.hdop.value(); - p.PDOP = TinyGPSPlus::parseDecimal(gsapdop.value()); + p.HDOP = reader.hdop.value(); + p.PDOP = TinyGPSPlus::parseDecimal(gsapdop.value()); #else - // FIXME! naive PDOP emulation (assumes VDOP==HDOP) - // correct formula is PDOP = SQRT(HDOP^2 + VDOP^2) - p.HDOP = reader.hdop.value(); - p.PDOP = 1.41 * reader.hdop.value(); + // FIXME! naive PDOP emulation (assumes VDOP==HDOP) + // correct formula is PDOP = SQRT(HDOP^2 + VDOP^2) + p.HDOP = reader.hdop.value(); + p.PDOP = 1.41 * reader.hdop.value(); #endif - // Discard incomplete or erroneous readings - if (reader.hdop.value() == 0) { - LOG_WARN("BOGUS hdop.value() REJECTED: %d", reader.hdop.value()); - return false; - } - - p.latitude_i = toDegInt(loc.lat); - p.longitude_i = toDegInt(loc.lng); - - p.altitude_geoidal_separation = reader.geoidHeight.meters(); - p.altitude_hae = reader.altitude.meters() + p.altitude_geoidal_separation; - p.altitude = reader.altitude.meters(); - - p.fix_quality = fixQual; -#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - p.fix_type = fixType; -#endif - - // positional timestamp - struct tm t; - t.tm_sec = reader.time.second(); - t.tm_min = reader.time.minute(); - t.tm_hour = reader.time.hour(); - t.tm_mday = reader.date.day(); - t.tm_mon = reader.date.month() - 1; - t.tm_year = reader.date.year() - 1900; - t.tm_isdst = false; - p.timestamp = gm_mktime(&t); - - // Nice to have, if available - if (reader.satellites.isUpdated()) { - p.sats_in_view = reader.satellites.value(); - } - - if (reader.course.isUpdated() && reader.course.isValid()) { - if (reader.course.value() < 36000) { // sanity check - p.ground_track = reader.course.value() * 1e3; // Scale the heading (in degrees * 10^-2) to match the expected degrees * 10^-5 - } else { - LOG_WARN("BOGUS course.value() REJECTED: %d", reader.course.value()); + // Discard incomplete or erroneous readings + if (reader.hdop.value() == 0) { + LOG_WARN("BOGUS hdop.value() REJECTED: %d", reader.hdop.value()); + return false; } - } - if (reader.speed.isUpdated() && reader.speed.isValid()) { - p.ground_speed = reader.speed.kmph(); - } + p.latitude_i = toDegInt(loc.lat); + p.longitude_i = toDegInt(loc.lng); - return true; -} + p.altitude_geoidal_separation = reader.geoidHeight.meters(); + p.altitude_hae = reader.altitude.meters() + p.altitude_geoidal_separation; + p.altitude = reader.altitude.meters(); -bool GPS::hasLock() { - // Using GPGGA fix quality indicator - if (fixQual >= 1 && fixQual <= 5) { + p.fix_quality = fixQual; #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - // Use GPGSA fix type 2D/3D (better) if available - if (fixType == 3 || fixType == 0) // zero means "no data received" + p.fix_type = fixType; #endif - return true; - } - return false; + // positional timestamp + struct tm t; + t.tm_sec = reader.time.second(); + t.tm_min = reader.time.minute(); + t.tm_hour = reader.time.hour(); + t.tm_mday = reader.date.day(); + t.tm_mon = reader.date.month() - 1; + t.tm_year = reader.date.year() - 1900; + t.tm_isdst = false; + p.timestamp = gm_mktime(&t); + + // Nice to have, if available + if (reader.satellites.isUpdated()) { + p.sats_in_view = reader.satellites.value(); + } + + if (reader.course.isUpdated() && reader.course.isValid()) { + if (reader.course.value() < 36000) { // sanity check + p.ground_track = + reader.course.value() * 1e3; // Scale the heading (in degrees * 10^-2) to match the expected degrees * 10^-5 + } else { + LOG_WARN("BOGUS course.value() REJECTED: %d", reader.course.value()); + } + } + + if (reader.speed.isUpdated() && reader.speed.isValid()) { + p.ground_speed = reader.speed.kmph(); + } + + return true; } -bool GPS::hasFlow() { return reader.passedChecksum() > 0; } - -bool GPS::whileActive() { - unsigned int charsInBuf = 0; - bool isValid = false; -#ifdef GPS_DEBUG - std::string debugmsg = ""; +bool GPS::hasLock() +{ + // Using GPGGA fix quality indicator + if (fixQual >= 1 && fixQual <= 5) { +#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS + // Use GPGSA fix type 2D/3D (better) if available + if (fixType == 3 || fixType == 0) // zero means "no data received" #endif - if (powerState != GPS_ACTIVE) { - clearBuffer(); + return true; + } + return false; - } +} + +bool GPS::hasFlow() +{ + return reader.passedChecksum() > 0; +} + +bool GPS::whileActive() +{ + unsigned int charsInBuf = 0; + bool isValid = false; +#ifdef GPS_DEBUG + std::string debugmsg = ""; +#endif + if (powerState != GPS_ACTIVE) { + clearBuffer(); + return false; + } #ifdef SERIAL_BUFFER_SIZE - if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) { - LOG_WARN("GPS Buffer full with %u bytes waiting. Flush to avoid corruption", _serial_gps->available()); - clearBuffer(); - } -#endif - // First consume any chars that have piled up at the receiver - while (_serial_gps->available() > 0) { - int c = _serial_gps->read(); - UBXscratch[charsInBuf] = c; -#ifdef GPS_DEBUG - debugmsg += vformat("%c", (c >= 32 && c <= 126) ? c : '.'); -#endif - isValid |= reader.encode(c); - if (charsInBuf > sizeof(UBXscratch) - 10 || c == '\r') { - if (strnstr((char *)UBXscratch, "$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50", charsInBuf)) { - rebootsSeen++; - } - charsInBuf = 0; - } else { - charsInBuf++; + if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) { + LOG_WARN("GPS Buffer full with %u bytes waiting. Flush to avoid corruption", _serial_gps->available()); + clearBuffer(); } - } -#ifdef GPS_DEBUG - if (debugmsg != "") { - LOG_DEBUG(debugmsg.c_str()); - } #endif - return isValid; + // First consume any chars that have piled up at the receiver + while (_serial_gps->available() > 0) { + int c = _serial_gps->read(); + UBXscratch[charsInBuf] = c; +#ifdef GPS_DEBUG + debugmsg += vformat("%c", (c >= 32 && c <= 126) ? c : '.'); +#endif + isValid |= reader.encode(c); + if (charsInBuf > sizeof(UBXscratch) - 10 || c == '\r') { + if (strnstr((char *)UBXscratch, "$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50", charsInBuf)) { + rebootsSeen++; + } + charsInBuf = 0; + } else { + charsInBuf++; + } + } +#ifdef GPS_DEBUG + if (debugmsg != "") { + LOG_DEBUG(debugmsg.c_str()); + } +#endif + return isValid; } -void GPS::enable() { - // Clear the old scheduling info (reset the lock-time prediction) - scheduling.reset(); +void GPS::enable() +{ + // Clear the old scheduling info (reset the lock-time prediction) + scheduling.reset(); - enabled = true; - setInterval(GPS_THREAD_INTERVAL); + enabled = true; + setInterval(GPS_THREAD_INTERVAL); - scheduling.informSearching(); - setPowerState(GPS_ACTIVE); + scheduling.informSearching(); + setPowerState(GPS_ACTIVE); } -int32_t GPS::disable() { - enabled = false; - setInterval(INT32_MAX); - setPowerState(GPS_OFF); +int32_t GPS::disable() +{ + enabled = false; + setInterval(INT32_MAX); + setPowerState(GPS_OFF); - return INT32_MAX; + return INT32_MAX; } -void GPS::toggleGpsMode() { - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - LOG_INFO("User toggled GpsMode. Now DISABLED"); - playGPSDisableBeep(); +void GPS::toggleGpsMode() +{ + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + LOG_INFO("User toggled GpsMode. Now DISABLED"); + playGPSDisableBeep(); #ifdef GNSS_AIROHA - if (powerState == GPS_ACTIVE) { - LOG_DEBUG("User power Off GPS"); - digitalWrite(PIN_GPS_EN, LOW); - } + if (powerState == GPS_ACTIVE) { + LOG_DEBUG("User power Off GPS"); + digitalWrite(PIN_GPS_EN, LOW); + } #endif - disable(); - } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - LOG_INFO("User toggled GpsMode. Now ENABLED"); - playGPSEnableBeep(); - enable(); - } + disable(); + } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + LOG_INFO("User toggled GpsMode. Now ENABLED"); + playGPSEnableBeep(); + enable(); + } } #endif // Exclude GPS diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 32c29f813..59cee7113 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -20,234 +20,235 @@ static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; typedef enum { - GNSS_MODEL_ATGM336H, - GNSS_MODEL_MTK, - GNSS_MODEL_UBLOX6, - GNSS_MODEL_UBLOX7, - GNSS_MODEL_UBLOX8, - GNSS_MODEL_UBLOX9, - GNSS_MODEL_UBLOX10, - GNSS_MODEL_UC6580, - GNSS_MODEL_UNKNOWN, - GNSS_MODEL_MTK_L76B, - GNSS_MODEL_MTK_PA1010D, - GNSS_MODEL_MTK_PA1616S, - GNSS_MODEL_AG3335, - GNSS_MODEL_AG3352, - GNSS_MODEL_LS20031, - GNSS_MODEL_CM121 + GNSS_MODEL_ATGM336H, + GNSS_MODEL_MTK, + GNSS_MODEL_UBLOX6, + GNSS_MODEL_UBLOX7, + GNSS_MODEL_UBLOX8, + GNSS_MODEL_UBLOX9, + GNSS_MODEL_UBLOX10, + GNSS_MODEL_UC6580, + GNSS_MODEL_UNKNOWN, + GNSS_MODEL_MTK_L76B, + GNSS_MODEL_MTK_PA1010D, + GNSS_MODEL_MTK_PA1616S, + GNSS_MODEL_AG3335, + GNSS_MODEL_AG3352, + GNSS_MODEL_LS20031, + GNSS_MODEL_CM121 } GnssModel_t; typedef enum { - GNSS_RESPONSE_NONE, - GNSS_RESPONSE_NAK, - GNSS_RESPONSE_FRAME_ERRORS, - GNSS_RESPONSE_OK, + GNSS_RESPONSE_NONE, + GNSS_RESPONSE_NAK, + GNSS_RESPONSE_FRAME_ERRORS, + GNSS_RESPONSE_OK, } GPS_RESPONSE; enum GPSPowerState : uint8_t { - GPS_ACTIVE, // Awake and want a position - GPS_IDLE, // Awake, but not wanting another position yet - GPS_SOFTSLEEP, // Physically powered on, but soft-sleeping - GPS_HARDSLEEP, // Physically powered off, but scheduled to wake - GPS_OFF // Powered off indefinitely + GPS_ACTIVE, // Awake and want a position + GPS_IDLE, // Awake, but not wanting another position yet + GPS_SOFTSLEEP, // Physically powered on, but soft-sleeping + GPS_HARDSLEEP, // Physically powered off, but scheduled to wake + GPS_OFF // Powered off indefinitely }; struct ChipInfo { - String chipName; // The name of the chip (for logging) - String detectionString; // The string to match in the response - GnssModel_t driver; // The driver to use + String chipName; // The name of the chip (for logging) + String detectionString; // The string to match in the response + GnssModel_t driver; // The driver to use }; /** * A gps class that only reads from the GPS periodically and keeps the gps powered down except when reading * * When new data is available it will notify observers. */ -class GPS : private concurrency::OSThread { -public: - meshtastic_Position p = meshtastic_Position_init_default; +class GPS : private concurrency::OSThread +{ + public: + meshtastic_Position p = meshtastic_Position_init_default; - /** This is normally bound to config.position.gps_en_gpio but some rare boards (like heltec tracker) need more - * advanced implementations. Those boards will set this public variable to a custom implementation. - * - * Normally set by GPS::createGPS() - */ - GpioVirtPin *enablePin = NULL; + /** This is normally bound to config.position.gps_en_gpio but some rare boards (like heltec tracker) need more advanced + * implementations. Those boards will set this public variable to a custom implementation. + * + * Normally set by GPS::createGPS() + */ + GpioVirtPin *enablePin = NULL; - virtual ~GPS(); + virtual ~GPS(); - /** We will notify this observable anytime GPS state has changed meaningfully */ - Observable newStatus; + /** We will notify this observable anytime GPS state has changed meaningfully */ + Observable newStatus; - /** - * Returns true if we succeeded - */ - virtual bool setup(); + /** + * Returns true if we succeeded + */ + virtual bool setup(); - // re-enable the thread - void enable(); + // re-enable the thread + void enable(); - // Disable the thread - int32_t disable() override; + // Disable the thread + int32_t disable() override; - // toggle between enabled/disabled - void toggleGpsMode(); + // toggle between enabled/disabled + void toggleGpsMode(); - // Change the power state of the GPS - for power saving / shutdown - void setPowerState(GPSPowerState newState, uint32_t sleepMs = 0); + // Change the power state of the GPS - for power saving / shutdown + void setPowerState(GPSPowerState newState, uint32_t sleepMs = 0); - /// Returns true if we have acquired GPS lock. - virtual bool hasLock(); + /// Returns true if we have acquired GPS lock. + virtual bool hasLock(); - /// Returns true if there's valid data flow with the chip. - virtual bool hasFlow(); + /// Returns true if there's valid data flow with the chip. + virtual bool hasFlow(); - /// Return true if we are connected to a GPS - bool isConnected() const { return hasGPS; } + /// Return true if we are connected to a GPS + bool isConnected() const { return hasGPS; } - bool isPowerSaving() const { return config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED; } + bool isPowerSaving() const { return config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED; } - // Empty the input buffer as quickly as possible - void clearBuffer(); + // Empty the input buffer as quickly as possible + void clearBuffer(); - // Creates an instance of the GPS class. - // Returns the new instance or null if the GPS is not present. - static GPS *createGps(); + // Creates an instance of the GPS class. + // Returns the new instance or null if the GPS is not present. + static GPS *createGps(); - // Wake the GPS hardware - ready for an update - void up(); + // Wake the GPS hardware - ready for an update + void up(); - // Let the GPS hardware save power between updates - void down(); + // Let the GPS hardware save power between updates + void down(); -private: - GPS() : concurrency::OSThread("GPS") {} + private: + GPS() : concurrency::OSThread("GPS") {} - /// Record that we have a GPS - void setConnected(); + /// Record that we have a GPS + void setConnected(); - /** Subclasses should look for serial rx characters here and feed it to their GPS parser - * - * Return true if we received a valid message from the GPS - */ - virtual bool whileActive(); + /** Subclasses should look for serial rx characters here and feed it to their GPS parser + * + * Return true if we received a valid message from the GPS + */ + virtual bool whileActive(); - /** - * Perform any processing that should be done only while the GPS is awake and looking for a fix. - * Override this method to check for new locations - * - * @return true if we've acquired a time - */ - virtual bool lookForTime(); + /** + * Perform any processing that should be done only while the GPS is awake and looking for a fix. + * Override this method to check for new locations + * + * @return true if we've acquired a time + */ + virtual bool lookForTime(); - /** - * Perform any processing that should be done only while the GPS is awake and looking for a fix. - * Override this method to check for new locations - * - * @return true if we've acquired a new location - */ - virtual bool lookForLocation(); + /** + * Perform any processing that should be done only while the GPS is awake and looking for a fix. + * Override this method to check for new locations + * + * @return true if we've acquired a new location + */ + virtual bool lookForLocation(); - GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; + GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; - TinyGPSPlus reader; - uint8_t fixQual = 0; // fix quality from GPGGA - uint32_t lastChecksumFailCount = 0; - uint8_t currentStep = 0; - int32_t currentDelay = 2000; + TinyGPSPlus reader; + uint8_t fixQual = 0; // fix quality from GPGGA + uint32_t lastChecksumFailCount = 0; + uint8_t currentStep = 0; + int32_t currentDelay = 2000; #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS - // (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field - // via optional feature "custom fields", currently disabled (bug #525) - TinyGPSCustom gsafixtype; // custom extract fix type from GPGSA - TinyGPSCustom gsapdop; // custom extract PDOP from GPGSA - uint8_t fixType = 0; // fix type from GPGSA + // (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field + // via optional feature "custom fields", currently disabled (bug #525) + TinyGPSCustom gsafixtype; // custom extract fix type from GPGSA + TinyGPSCustom gsapdop; // custom extract PDOP from GPGSA + uint8_t fixType = 0; // fix type from GPGSA #endif - uint32_t fixHoldEnds = 0; - uint32_t rx_gpio = 0; - uint32_t tx_gpio = 0; + uint32_t fixHoldEnds = 0; + uint32_t rx_gpio = 0; + uint32_t tx_gpio = 0; - uint8_t speedSelect = 0; - uint8_t probeTries = 0; + uint8_t speedSelect = 0; + uint8_t probeTries = 0; - /** - * hasValidLocation - indicates that the position variables contain a complete - * GPS location, valid and fresh (< gps_update_interval + position_broadcast_secs) - */ - bool hasValidLocation = false; // default to false, until we complete our first read + /** + * hasValidLocation - indicates that the position variables contain a complete + * GPS location, valid and fresh (< gps_update_interval + position_broadcast_secs) + */ + bool hasValidLocation = false; // default to false, until we complete our first read - bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop() + bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop() - bool hasGPS = false; // Do we have a GPS we are talking to + bool hasGPS = false; // Do we have a GPS we are talking to - bool GPSInitFinished = false; // Init thread finished? - bool GPSInitStarted = false; // Init thread finished? + bool GPSInitFinished = false; // Init thread finished? + bool GPSInitStarted = false; // Init thread finished? - GPSPowerState powerState = GPS_OFF; // GPS_ACTIVE if we want a location right now + GPSPowerState powerState = GPS_OFF; // GPS_ACTIVE if we want a location right now - uint8_t numSatellites = 0; + uint8_t numSatellites = 0; - CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); + CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); - /** If !NULL we will use this serial port to construct our GPS */ + /** If !NULL we will use this serial port to construct our GPS */ #if defined(ARCH_RP2040) - static SerialUART *_serial_gps; + static SerialUART *_serial_gps; #elif defined(ARCH_NRF52) - static Uart *_serial_gps; + static Uart *_serial_gps; #else - static HardwareSerial *_serial_gps; + static HardwareSerial *_serial_gps; #endif - // Create a ublox packet for editing in memory - uint8_t makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); - uint8_t makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); + // Create a ublox packet for editing in memory + uint8_t makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); + uint8_t makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); - // scratch space for creating ublox packets - uint8_t UBXscratch[250] = {0}; + // scratch space for creating ublox packets + uint8_t UBXscratch[250] = {0}; - int rebootsSeen = 0; + int rebootsSeen = 0; - int getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis); - GPS_RESPONSE getACK(uint8_t c, uint8_t i, uint32_t waitMillis); - GPS_RESPONSE getACK(const char *message, uint32_t waitMillis); + int getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID, uint32_t waitMillis); + GPS_RESPONSE getACK(uint8_t c, uint8_t i, uint32_t waitMillis); + GPS_RESPONSE getACK(const char *message, uint32_t waitMillis); - GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis); + GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis); - /// Prepare the GPS for the cpu entering deep sleep, expect to be gone for at least 100s of msecs - /// always returns 0 to indicate okay to sleep - int prepareDeepSleep(void *unused); + /// Prepare the GPS for the cpu entering deep sleep, expect to be gone for at least 100s of msecs + /// always returns 0 to indicate okay to sleep + int prepareDeepSleep(void *unused); - /** Set power with EN pin, if relevant - */ - void writePinEN(bool on); + /** Set power with EN pin, if relevant + */ + void writePinEN(bool on); - /** Set the value of the STANDBY pin, if relevant - */ - void writePinStandby(bool standby); + /** Set the value of the STANDBY pin, if relevant + */ + void writePinStandby(bool standby); - /** Set GPS power with PMU, if relevant - */ - void setPowerPMU(bool on); + /** Set GPS power with PMU, if relevant + */ + void setPowerPMU(bool on); - /** Set UBLOX power, if relevant - */ - void setPowerUBLOX(bool on, uint32_t sleepMs = 0); + /** Set UBLOX power, if relevant + */ + void setPowerUBLOX(bool on, uint32_t sleepMs = 0); - /** - * Tell users we have new GPS readings - */ - void publishUpdate(); + /** + * Tell users we have new GPS readings + */ + void publishUpdate(); - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); + GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); - // Get GNSS model - GnssModel_t probe(int serialSpeed); + // Get GNSS model + GnssModel_t probe(int serialSpeed); - // delay counter to allow more sats before fixed position stops GPS thread - uint8_t fixeddelayCtr = 0; + // delay counter to allow more sats before fixed position stops GPS thread + uint8_t fixeddelayCtr = 0; }; extern GPS *gps; diff --git a/src/gps/GPSUpdateScheduling.cpp b/src/gps/GPSUpdateScheduling.cpp index 91a3cb1be..5eaf7a8ba 100644 --- a/src/gps/GPSUpdateScheduling.cpp +++ b/src/gps/GPSUpdateScheduling.cpp @@ -3,100 +3,116 @@ #include "Default.h" // Mark the time when searching for GPS position begins -void GPSUpdateScheduling::informSearching() { searchStartedMs = millis(); } +void GPSUpdateScheduling::informSearching() +{ + searchStartedMs = millis(); +} // Mark the time when searching for GPS is complete, // then update the predicted lock-time -void GPSUpdateScheduling::informGotLock() { - searchEndedMs = millis(); - LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000); - updateLockTimePrediction(); +void GPSUpdateScheduling::informGotLock() +{ + searchEndedMs = millis(); + LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000); + updateLockTimePrediction(); } // Clear old lock-time prediction data. // When re-enabling GPS with user button. -void GPSUpdateScheduling::reset() { - searchStartedMs = 0; - searchEndedMs = 0; - searchCount = 0; - predictedMsToGetLock = 0; +void GPSUpdateScheduling::reset() +{ + searchStartedMs = 0; + searchEndedMs = 0; + searchCount = 0; + predictedMsToGetLock = 0; } // How many milliseconds before we should next search for GPS position // Used by GPS hardware directly, to enter timed hardware sleep -uint32_t GPSUpdateScheduling::msUntilNextSearch() { - uint32_t now = millis(); +uint32_t GPSUpdateScheduling::msUntilNextSearch() +{ + uint32_t now = millis(); - // Target interval (seconds), between GPS updates - uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval); + // Target interval (seconds), between GPS updates + uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval); - // Check how long until we should start searching, to hopefully hit our target interval - uint32_t dueAtMs = searchEndedMs + updateInterval; - uint32_t compensatedStart = dueAtMs - predictedMsToGetLock; - int32_t remainingMs = compensatedStart - now; + // Check how long until we should start searching, to hopefully hit our target interval + uint32_t dueAtMs = searchEndedMs + updateInterval; + uint32_t compensatedStart = dueAtMs - predictedMsToGetLock; + int32_t remainingMs = compensatedStart - now; - // If we should have already started (negative value), start ASAP - if (remainingMs < 0) - remainingMs = 0; + // If we should have already started (negative value), start ASAP + if (remainingMs < 0) + remainingMs = 0; - return (uint32_t)remainingMs; + return (uint32_t)remainingMs; } // How long have we already been searching? // Used to abort a search in progress, if it runs unacceptably long -uint32_t GPSUpdateScheduling::elapsedSearchMs() { - // If searching - if (searchStartedMs > searchEndedMs) - return millis() - searchStartedMs; +uint32_t GPSUpdateScheduling::elapsedSearchMs() +{ + // If searching + if (searchStartedMs > searchEndedMs) + return millis() - searchStartedMs; - // If not searching - 0ms. We shouldn't really consume this value - else - return 0; + // If not searching - 0ms. We shouldn't really consume this value + else + return 0; } // Is it now time to begin searching for a GPS position? -bool GPSUpdateScheduling::isUpdateDue() { return (msUntilNextSearch() == 0); } +bool GPSUpdateScheduling::isUpdateDue() +{ + return (msUntilNextSearch() == 0); +} // Have we been searching for a GPS position for too long? -bool GPSUpdateScheduling::searchedTooLong() { - uint32_t minimumOrConfiguredSecs = Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs); - uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs); - // If broadcast interval set to max, no such thing as "too long" - if (maxSearchMs == UINT32_MAX) - return false; +bool GPSUpdateScheduling::searchedTooLong() +{ + uint32_t minimumOrConfiguredSecs = + Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs); + uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs); + // If broadcast interval set to max, no such thing as "too long" + if (maxSearchMs == UINT32_MAX) + return false; - // If we've been searching longer than our position broadcast interval: that's too long - else if (elapsedSearchMs() > maxSearchMs) - return true; + // If we've been searching longer than our position broadcast interval: that's too long + else if (elapsedSearchMs() > maxSearchMs) + return true; - // Otherwise, not too long yet! - else - return false; + // Otherwise, not too long yet! + else + return false; } // Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation -void GPSUpdateScheduling::updateLockTimePrediction() { +void GPSUpdateScheduling::updateLockTimePrediction() +{ - // How long did it take to get GPS lock this time? - // Duration between down() calls - int32_t lockTime = searchEndedMs - searchStartedMs; - if (lockTime < 0) - lockTime = 0; + // How long did it take to get GPS lock this time? + // Duration between down() calls + int32_t lockTime = searchEndedMs - searchStartedMs; + if (lockTime < 0) + lockTime = 0; - // Ignore the first lock-time: likely to be long, will skew data + // Ignore the first lock-time: likely to be long, will skew data - // Second locktime: likely stable. Use to initialize the smoothing filter - if (searchCount == 1) - predictedMsToGetLock = lockTime; + // Second locktime: likely stable. Use to initialize the smoothing filter + if (searchCount == 1) + predictedMsToGetLock = lockTime; - // Third locktime and after: predict using exponential smoothing. Respond slowly to changes - else if (searchCount > 1) - predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting)); + // Third locktime and after: predict using exponential smoothing. Respond slowly to changes + else if (searchCount > 1) + predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting)); - searchCount++; // Only tracked so we can disregard initial lock-times + searchCount++; // Only tracked so we can disregard initial lock-times - LOG_DEBUG("Predict %us to get next lock", predictedMsToGetLock / 1000); + LOG_DEBUG("Predict %us to get next lock", predictedMsToGetLock / 1000); } // How long do we expect to spend searching for a lock? -uint32_t GPSUpdateScheduling::predictedSearchDurationMs() { return GPSUpdateScheduling::predictedMsToGetLock; } +uint32_t GPSUpdateScheduling::predictedSearchDurationMs() +{ + return GPSUpdateScheduling::predictedMsToGetLock; +} diff --git a/src/gps/GPSUpdateScheduling.h b/src/gps/GPSUpdateScheduling.h index b7ba09524..7e121c9b6 100644 --- a/src/gps/GPSUpdateScheduling.h +++ b/src/gps/GPSUpdateScheduling.h @@ -3,26 +3,27 @@ #include "configuration.h" // Encapsulates code responsible for the timing of GPS updates -class GPSUpdateScheduling { -public: - // Marks the time of these events, for calculation use - void informSearching(); - void informGotLock(); // Predicted lock-time is recalculated here +class GPSUpdateScheduling +{ + public: + // Marks the time of these events, for calculation use + void informSearching(); + void informGotLock(); // Predicted lock-time is recalculated here - void reset(); // Reset the prediction - after GPS::disable() / GPS::enable() - bool isUpdateDue(); // Is it time to begin searching for a GPS position? - bool searchedTooLong(); // Have we been searching for too long? + void reset(); // Reset the prediction - after GPS::disable() / GPS::enable() + bool isUpdateDue(); // Is it time to begin searching for a GPS position? + bool searchedTooLong(); // Have we been searching for too long? - uint32_t msUntilNextSearch(); // How long until we need to begin searching for a GPS? Info provided to GPS hardware for sleep - uint32_t elapsedSearchMs(); // How long have we been searching so far? - uint32_t predictedSearchDurationMs(); // How long do we expect to spend searching for a lock? + uint32_t msUntilNextSearch(); // How long until we need to begin searching for a GPS? Info provided to GPS hardware for sleep + uint32_t elapsedSearchMs(); // How long have we been searching so far? + uint32_t predictedSearchDurationMs(); // How long do we expect to spend searching for a lock? -private: - void updateLockTimePrediction(); // Called from informGotLock - uint32_t searchStartedMs = 0; - uint32_t searchEndedMs = 0; - uint32_t searchCount = 0; - uint32_t predictedMsToGetLock = 0; + private: + void updateLockTimePrediction(); // Called from informGotLock + uint32_t searchStartedMs = 0; + uint32_t searchEndedMs = 0; + uint32_t searchCount = 0; + uint32_t predictedMsToGetLock = 0; - const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time". + const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time". }; \ No newline at end of file diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp index 3bba9ff65..6d1f2da6d 100644 --- a/src/gps/GeoCoord.cpp +++ b/src/gps/GeoCoord.cpp @@ -1,378 +1,402 @@ #include "GeoCoord.h" -GeoCoord::GeoCoord() { _dirty = true; } - -GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _longitude(lon), _altitude(alt) { GeoCoord::setCoords(); } - -GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) { - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 - _latitude = int32_t(lat * 1e+7); - _longitude = int32_t(lon * 1e+7); - GeoCoord::setCoords(); +GeoCoord::GeoCoord() +{ + _dirty = true; } -GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) { - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 - _latitude = int32_t(lat * 1e+7); - _longitude = int32_t(lon * 1e+7); - GeoCoord::setCoords(); +GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _longitude(lon), _altitude(alt) +{ + GeoCoord::setCoords(); +} + +GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) +{ + // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + _latitude = int32_t(lat * 1e+7); + _longitude = int32_t(lon * 1e+7); + GeoCoord::setCoords(); +} + +GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) +{ + // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + _latitude = int32_t(lat * 1e+7); + _longitude = int32_t(lon * 1e+7); + GeoCoord::setCoords(); } // Initialize all the coordinate systems -void GeoCoord::setCoords() { - double lat = _latitude * 1e-7; - double lon = _longitude * 1e-7; - GeoCoord::latLongToDMS(lat, lon, _dms); - GeoCoord::latLongToUTM(lat, lon, _utm); - GeoCoord::latLongToMGRS(lat, lon, _mgrs); - GeoCoord::latLongToOSGR(lat, lon, _osgr); - GeoCoord::latLongToOLC(lat, lon, _olc); - _dirty = false; +void GeoCoord::setCoords() +{ + double lat = _latitude * 1e-7; + double lon = _longitude * 1e-7; + GeoCoord::latLongToDMS(lat, lon, _dms); + GeoCoord::latLongToUTM(lat, lon, _utm); + GeoCoord::latLongToMGRS(lat, lon, _mgrs); + GeoCoord::latLongToOSGR(lat, lon, _osgr); + GeoCoord::latLongToOLC(lat, lon, _olc); + _dirty = false; } -void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt) { - // If marked dirty or new coordinates - if (_dirty || _latitude != lat || _longitude != lon || _altitude != alt) { - _dirty = true; - _latitude = lat; - _longitude = lon; - _altitude = alt; - setCoords(); - } +void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt) +{ + // If marked dirty or new coordinates + if (_dirty || _latitude != lat || _longitude != lon || _altitude != alt) { + _dirty = true; + _latitude = lat; + _longitude = lon; + _altitude = alt; + setCoords(); + } } -void GeoCoord::updateCoords(const double lat, const double lon, const int32_t alt) { - int32_t iLat = lat * 1e+7; - int32_t iLon = lon * 1e+7; - // If marked dirty or new coordinates - if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { - _dirty = true; - _latitude = iLat; - _longitude = iLon; - _altitude = alt; - setCoords(); - } +void GeoCoord::updateCoords(const double lat, const double lon, const int32_t alt) +{ + int32_t iLat = lat * 1e+7; + int32_t iLon = lon * 1e+7; + // If marked dirty or new coordinates + if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { + _dirty = true; + _latitude = iLat; + _longitude = iLon; + _altitude = alt; + setCoords(); + } } -void GeoCoord::updateCoords(const float lat, const float lon, const int32_t alt) { - int32_t iLat = lat * 1e+7; - int32_t iLon = lon * 1e+7; - // If marked dirty or new coordinates - if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { - _dirty = true; - _latitude = iLat; - _longitude = iLon; - _altitude = alt; - setCoords(); - } +void GeoCoord::updateCoords(const float lat, const float lon, const int32_t alt) +{ + int32_t iLat = lat * 1e+7; + int32_t iLon = lon * 1e+7; + // If marked dirty or new coordinates + if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { + _dirty = true; + _latitude = iLat; + _longitude = iLon; + _altitude = alt; + setCoords(); + } } /** * Converts lat long coordinates from decimal degrees to degrees minutes seconds format. * DD°MM'SS"C DDD°MM'SS"C */ -void GeoCoord::latLongToDMS(const double lat, const double lon, DMS &dms) { - if (lat < 0) - dms.latCP = 'S'; - else - dms.latCP = 'N'; +void GeoCoord::latLongToDMS(const double lat, const double lon, DMS &dms) +{ + if (lat < 0) + dms.latCP = 'S'; + else + dms.latCP = 'N'; - double latDeg = lat; + double latDeg = lat; - if (lat < 0) - latDeg = latDeg * -1; + if (lat < 0) + latDeg = latDeg * -1; - dms.latDeg = floor(latDeg); - double latMin = (latDeg - dms.latDeg) * 60; - dms.latMin = floor(latMin); - dms.latSec = (latMin - dms.latMin) * 60; + dms.latDeg = floor(latDeg); + double latMin = (latDeg - dms.latDeg) * 60; + dms.latMin = floor(latMin); + dms.latSec = (latMin - dms.latMin) * 60; - if (lon < 0) - dms.lonCP = 'W'; - else - dms.lonCP = 'E'; + if (lon < 0) + dms.lonCP = 'W'; + else + dms.lonCP = 'E'; - double lonDeg = lon; + double lonDeg = lon; - if (lon < 0) - lonDeg = lonDeg * -1; + if (lon < 0) + lonDeg = lonDeg * -1; - dms.lonDeg = floor(lonDeg); - double lonMin = (lonDeg - dms.lonDeg) * 60; - dms.lonMin = floor(lonMin); - dms.lonSec = (lonMin - dms.lonMin) * 60; + dms.lonDeg = floor(lonDeg); + double lonMin = (lonDeg - dms.lonDeg) * 60; + dms.lonMin = floor(lonMin); + dms.lonSec = (lonMin - dms.lonMin) * 60; } /** * Converts lat long coordinates to UTM. * based on this: https://github.com/walvok/LatLonToUTM/blob/master/latlon_utm.ino */ -void GeoCoord::latLongToUTM(const double lat, const double lon, UTM &utm) { +void GeoCoord::latLongToUTM(const double lat, const double lon, UTM &utm) +{ - const std::string latBands = "CDEFGHJKLMNPQRSTUVWXX"; - utm.zone = int((lon + 180) / 6 + 1); - utm.band = latBands[int(lat / 8 + 10)]; - double a = 6378137; // WGS84 - equatorial radius - double k0 = 0.9996; // UTM point scale on the central meridian - double eccSquared = 0.00669438; // eccentricity squared - double lonTemp = (lon + 180) - int((lon + 180) / 360) * 360 - 180; // Make sure the longitude is between -180.00 .. 179.9 - double latRad = toRadians(lat); - double lonRad = toRadians(lonTemp); + const std::string latBands = "CDEFGHJKLMNPQRSTUVWXX"; + utm.zone = int((lon + 180) / 6 + 1); + utm.band = latBands[int(lat / 8 + 10)]; + double a = 6378137; // WGS84 - equatorial radius + double k0 = 0.9996; // UTM point scale on the central meridian + double eccSquared = 0.00669438; // eccentricity squared + double lonTemp = (lon + 180) - int((lon + 180) / 360) * 360 - 180; // Make sure the longitude is between -180.00 .. 179.9 + double latRad = toRadians(lat); + double lonRad = toRadians(lonTemp); - // Special Zones for Norway and Svalbard - if (lat >= 56.0 && lat < 64.0 && lonTemp >= 3.0 && lonTemp < 12.0) // Norway - utm.zone = 32; - if (lat >= 72.0 && lat < 84.0) { // Svalbard - if (lonTemp >= 0.0 && lonTemp < 9.0) - utm.zone = 31; - else if (lonTemp >= 9.0 && lonTemp < 21.0) - utm.zone = 33; - else if (lonTemp >= 21.0 && lonTemp < 33.0) - utm.zone = 35; - else if (lonTemp >= 33.0 && lonTemp < 42.0) - utm.zone = 37; - } + // Special Zones for Norway and Svalbard + if (lat >= 56.0 && lat < 64.0 && lonTemp >= 3.0 && lonTemp < 12.0) // Norway + utm.zone = 32; + if (lat >= 72.0 && lat < 84.0) { // Svalbard + if (lonTemp >= 0.0 && lonTemp < 9.0) + utm.zone = 31; + else if (lonTemp >= 9.0 && lonTemp < 21.0) + utm.zone = 33; + else if (lonTemp >= 21.0 && lonTemp < 33.0) + utm.zone = 35; + else if (lonTemp >= 33.0 && lonTemp < 42.0) + utm.zone = 37; + } - double lonOrigin = (utm.zone - 1) * 6 - 180 + 3; // puts origin in middle of zone - double lonOriginRad = toRadians(lonOrigin); - double eccPrimeSquared = (eccSquared) / (1 - eccSquared); - double N = a / sqrt(1 - eccSquared * sin(latRad) * sin(latRad)); - double T = tan(latRad) * tan(latRad); - double C = eccPrimeSquared * cos(latRad) * cos(latRad); - double A = cos(latRad) * (lonRad - lonOriginRad); - double M = a * ((1 - eccSquared / 4 - 3 * eccSquared * eccSquared / 64 - 5 * eccSquared * eccSquared * eccSquared / 256) * latRad - - (3 * eccSquared / 8 + 3 * eccSquared * eccSquared / 32 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(2 * latRad) + - (15 * eccSquared * eccSquared / 256 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(4 * latRad) - - (35 * eccSquared * eccSquared * eccSquared / 3072) * sin(6 * latRad)); - utm.easting = (double)(k0 * N * (A + (1 - T + C) * pow(A, 3) / 6 + (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120) + - 500000.0); - utm.northing = (double)(k0 * (M + N * tan(latRad) * - (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 + - (61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720))); + double lonOrigin = (utm.zone - 1) * 6 - 180 + 3; // puts origin in middle of zone + double lonOriginRad = toRadians(lonOrigin); + double eccPrimeSquared = (eccSquared) / (1 - eccSquared); + double N = a / sqrt(1 - eccSquared * sin(latRad) * sin(latRad)); + double T = tan(latRad) * tan(latRad); + double C = eccPrimeSquared * cos(latRad) * cos(latRad); + double A = cos(latRad) * (lonRad - lonOriginRad); + double M = + a * ((1 - eccSquared / 4 - 3 * eccSquared * eccSquared / 64 - 5 * eccSquared * eccSquared * eccSquared / 256) * latRad - + (3 * eccSquared / 8 + 3 * eccSquared * eccSquared / 32 + 45 * eccSquared * eccSquared * eccSquared / 1024) * + sin(2 * latRad) + + (15 * eccSquared * eccSquared / 256 + 45 * eccSquared * eccSquared * eccSquared / 1024) * sin(4 * latRad) - + (35 * eccSquared * eccSquared * eccSquared / 3072) * sin(6 * latRad)); + utm.easting = (double)(k0 * N * + (A + (1 - T + C) * pow(A, 3) / 6 + + (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120) + + 500000.0); + utm.northing = + (double)(k0 * (M + N * tan(latRad) * + (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24 + + (61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720))); - if (lat < 0) - utm.northing += 10000000.0; // 10000000 meter offset for southern hemisphere + if (lat < 0) + utm.northing += 10000000.0; // 10000000 meter offset for southern hemisphere } // Converts lat long coordinates to an MGRS. -void GeoCoord::latLongToMGRS(const double lat, const double lon, MGRS &mgrs) { - const std::string e100kLetters[3] = {"ABCDEFGH", "JKLMNPQR", "STUVWXYZ"}; - const std::string n100kLetters[2] = {"ABCDEFGHJKLMNPQRSTUV", "FGHJKLMNPQRSTUVABCDE"}; - UTM utm; - latLongToUTM(lat, lon, utm); - mgrs.zone = utm.zone; - mgrs.band = utm.band; - double col = floor(utm.easting / 100000); - mgrs.east100k = e100kLetters[(mgrs.zone - 1) % 3][col - 1]; - double row = (int32_t)floor(utm.northing / 100000.0) % 20; - mgrs.north100k = n100kLetters[(mgrs.zone - 1) % 2][row]; - mgrs.easting = (int32_t)utm.easting % 100000; - mgrs.northing = (int32_t)utm.northing % 100000; +void GeoCoord::latLongToMGRS(const double lat, const double lon, MGRS &mgrs) +{ + const std::string e100kLetters[3] = {"ABCDEFGH", "JKLMNPQR", "STUVWXYZ"}; + const std::string n100kLetters[2] = {"ABCDEFGHJKLMNPQRSTUV", "FGHJKLMNPQRSTUVABCDE"}; + UTM utm; + latLongToUTM(lat, lon, utm); + mgrs.zone = utm.zone; + mgrs.band = utm.band; + double col = floor(utm.easting / 100000); + mgrs.east100k = e100kLetters[(mgrs.zone - 1) % 3][col - 1]; + double row = (int32_t)floor(utm.northing / 100000.0) % 20; + mgrs.north100k = n100kLetters[(mgrs.zone - 1) % 2][row]; + mgrs.easting = (int32_t)utm.easting % 100000; + mgrs.northing = (int32_t)utm.northing % 100000; } /** * Converts lat long coordinates to Ordnance Survey Grid Reference (UK National Grid Ref). * Based on: https://www.movable-type.co.uk/scripts/latlong-os-gridref.html */ -void GeoCoord::latLongToOSGR(const double lat, const double lon, OSGR &osgr) { - const char letter[] = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; // No 'I' in OSGR - double a = 6377563.396; // Airy 1830 semi-major axis - double b = 6356256.909; // Airy 1830 semi-minor axis - double f0 = 0.9996012717; // National Grid point scale factor on the central meridian - double phi0 = toRadians(49); - double lambda0 = toRadians(-2); - double n0 = -100000; - double e0 = 400000; - double e2 = 1 - (b * b) / (a * a); // eccentricity squared - double n = (a - b) / (a + b); +void GeoCoord::latLongToOSGR(const double lat, const double lon, OSGR &osgr) +{ + const char letter[] = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; // No 'I' in OSGR + double a = 6377563.396; // Airy 1830 semi-major axis + double b = 6356256.909; // Airy 1830 semi-minor axis + double f0 = 0.9996012717; // National Grid point scale factor on the central meridian + double phi0 = toRadians(49); + double lambda0 = toRadians(-2); + double n0 = -100000; + double e0 = 400000; + double e2 = 1 - (b * b) / (a * a); // eccentricity squared + double n = (a - b) / (a + b); - double osgb_Latitude; - double osgb_Longitude; - convertWGS84ToOSGB36(lat, lon, osgb_Latitude, osgb_Longitude); - double phi = osgb_Latitude; // already in radians - double lambda = osgb_Longitude; // already in radians - double v = a * f0 / sqrt(1 - e2 * sin(phi) * sin(phi)); - double rho = a * f0 * (1 - e2) / pow(1 - e2 * sin(phi) * sin(phi), 1.5); - double eta2 = v / rho - 1; - double mA = (1 + n + (5 / 4) * n * n + (5 / 4) * n * n * n) * (phi - phi0); - double mB = (3 * n + 3 * n * n + (21 / 8) * n * n * n) * sin(phi - phi0) * cos(phi + phi0); - // loss of precision in mC & mD due to floating point rounding can cause inaccuracy of northing by a few meters - double mC = (15 / 8 * n * n + 15 / 8 * n * n * n) * sin(2 * (phi - phi0)) * cos(2 * (phi + phi0)); - double mD = (35 / 24) * n * n * n * sin(3 * (phi - phi0)) * cos(3 * (phi + phi0)); - double m = b * f0 * (mA - mB + mC - mD); + double osgb_Latitude; + double osgb_Longitude; + convertWGS84ToOSGB36(lat, lon, osgb_Latitude, osgb_Longitude); + double phi = osgb_Latitude; // already in radians + double lambda = osgb_Longitude; // already in radians + double v = a * f0 / sqrt(1 - e2 * sin(phi) * sin(phi)); + double rho = a * f0 * (1 - e2) / pow(1 - e2 * sin(phi) * sin(phi), 1.5); + double eta2 = v / rho - 1; + double mA = (1 + n + (5 / 4) * n * n + (5 / 4) * n * n * n) * (phi - phi0); + double mB = (3 * n + 3 * n * n + (21 / 8) * n * n * n) * sin(phi - phi0) * cos(phi + phi0); + // loss of precision in mC & mD due to floating point rounding can cause inaccuracy of northing by a few meters + double mC = (15 / 8 * n * n + 15 / 8 * n * n * n) * sin(2 * (phi - phi0)) * cos(2 * (phi + phi0)); + double mD = (35 / 24) * n * n * n * sin(3 * (phi - phi0)) * cos(3 * (phi + phi0)); + double m = b * f0 * (mA - mB + mC - mD); - double cos3Phi = cos(phi) * cos(phi) * cos(phi); - double cos5Phi = cos3Phi * cos(phi) * cos(phi); - double tan2Phi = tan(phi) * tan(phi); - double tan4Phi = tan2Phi * tan2Phi; - double I = m + n0; - double II = (v / 2) * sin(phi) * cos(phi); - double III = (v / 24) * sin(phi) * cos3Phi * (5 - tan2Phi + 9 * eta2); - double IIIA = (v / 720) * sin(phi) * cos5Phi * (61 - 58 * tan2Phi + tan4Phi); - double IV = v * cos(phi); - double V = (v / 6) * cos3Phi * (v / rho - tan2Phi); - double VI = (v / 120) * cos5Phi * (5 - 18 * tan2Phi + tan4Phi + 14 * eta2 - 58 * tan2Phi * eta2); + double cos3Phi = cos(phi) * cos(phi) * cos(phi); + double cos5Phi = cos3Phi * cos(phi) * cos(phi); + double tan2Phi = tan(phi) * tan(phi); + double tan4Phi = tan2Phi * tan2Phi; + double I = m + n0; + double II = (v / 2) * sin(phi) * cos(phi); + double III = (v / 24) * sin(phi) * cos3Phi * (5 - tan2Phi + 9 * eta2); + double IIIA = (v / 720) * sin(phi) * cos5Phi * (61 - 58 * tan2Phi + tan4Phi); + double IV = v * cos(phi); + double V = (v / 6) * cos3Phi * (v / rho - tan2Phi); + double VI = (v / 120) * cos5Phi * (5 - 18 * tan2Phi + tan4Phi + 14 * eta2 - 58 * tan2Phi * eta2); - double deltaLambda = lambda - lambda0; - double deltaLambda2 = deltaLambda * deltaLambda; - double northing = I + II * deltaLambda2 + III * deltaLambda2 * deltaLambda2 + IIIA * deltaLambda2 * deltaLambda2 * deltaLambda2; - double easting = e0 + IV * deltaLambda + V * deltaLambda2 * deltaLambda + VI * deltaLambda2 * deltaLambda2 * deltaLambda; + double deltaLambda = lambda - lambda0; + double deltaLambda2 = deltaLambda * deltaLambda; + double northing = + I + II * deltaLambda2 + III * deltaLambda2 * deltaLambda2 + IIIA * deltaLambda2 * deltaLambda2 * deltaLambda2; + double easting = e0 + IV * deltaLambda + V * deltaLambda2 * deltaLambda + VI * deltaLambda2 * deltaLambda2 * deltaLambda; - if (easting < 0 || easting > 700000 || northing < 0 || northing > 1300000) // Check if out of boundaries - osgr = {'I', 'I', 0, 0}; - else { - uint32_t e100k = floor(easting / 100000); - uint32_t n100k = floor(northing / 100000); - int8_t l1 = (19 - n100k) - (19 - n100k) % 5 + floor((e100k + 10) / 5); - int8_t l2 = (19 - n100k) * 5 % 25 + e100k % 5; - osgr.e100k = letter[l1]; - osgr.n100k = letter[l2]; - osgr.easting = floor((int)easting % 100000); - osgr.northing = floor((int)northing % 100000); - } + if (easting < 0 || easting > 700000 || northing < 0 || northing > 1300000) // Check if out of boundaries + osgr = {'I', 'I', 0, 0}; + else { + uint32_t e100k = floor(easting / 100000); + uint32_t n100k = floor(northing / 100000); + int8_t l1 = (19 - n100k) - (19 - n100k) % 5 + floor((e100k + 10) / 5); + int8_t l2 = (19 - n100k) * 5 % 25 + e100k % 5; + osgr.e100k = letter[l1]; + osgr.n100k = letter[l2]; + osgr.easting = floor((int)easting % 100000); + osgr.northing = floor((int)northing % 100000); + } } /** * Converts lat long coordinates to Open Location Code. * Based on: https://github.com/google/open-location-code/blob/main/c/src/olc.c */ -void GeoCoord::latLongToOLC(double lat, double lon, OLC &olc) { - char tempCode[] = "1234567890abc"; - const char kAlphabet[] = "23456789CFGHJMPQRVWX"; - double latitude; - double longitude = lon; - double latitude_degrees = std::min(90.0, std::max(-90.0, lat)); +void GeoCoord::latLongToOLC(double lat, double lon, OLC &olc) +{ + char tempCode[] = "1234567890abc"; + const char kAlphabet[] = "23456789CFGHJMPQRVWX"; + double latitude; + double longitude = lon; + double latitude_degrees = std::min(90.0, std::max(-90.0, lat)); - if (latitude_degrees < 90) // Check latitude less than lat max - latitude = latitude_degrees; - else { - double precision; - if (OLC_CODE_LEN <= 10) - precision = pow_neg(20, floor((OLC_CODE_LEN / -2) + 2)); - else - precision = pow_neg(20, -3) / pow(5, OLC_CODE_LEN - 10); - latitude = latitude_degrees - precision / 2; - } - while (longitude < -180) // Normalize longitude - longitude += 360; - while (longitude >= 180) - longitude -= 360; - int64_t lat_val = 90 * 2.5e7; - int64_t lng_val = 180 * 8.192e6; - lat_val += latitude * 2.5e7; - lng_val += longitude * 8.192e6; - size_t pos = OLC_CODE_LEN; - - if (OLC_CODE_LEN > 10) { // Compute grid part of code if needed - for (size_t i = 0; i < 5; i++) { - int lat_digit = lat_val % 5; - int lng_digit = lng_val % 4; - int ndx = lat_digit * 4 + lng_digit; - tempCode[pos--] = kAlphabet[ndx]; - lat_val /= 5; - lng_val /= 4; + if (latitude_degrees < 90) // Check latitude less than lat max + latitude = latitude_degrees; + else { + double precision; + if (OLC_CODE_LEN <= 10) + precision = pow_neg(20, floor((OLC_CODE_LEN / -2) + 2)); + else + precision = pow_neg(20, -3) / pow(5, OLC_CODE_LEN - 10); + latitude = latitude_degrees - precision / 2; } - } else { - lat_val /= pow(5, 5); - lng_val /= pow(4, 5); - } + while (longitude < -180) // Normalize longitude + longitude += 360; + while (longitude >= 180) + longitude -= 360; + int64_t lat_val = 90 * 2.5e7; + int64_t lng_val = 180 * 8.192e6; + lat_val += latitude * 2.5e7; + lng_val += longitude * 8.192e6; + size_t pos = OLC_CODE_LEN; - pos = 10; + if (OLC_CODE_LEN > 10) { // Compute grid part of code if needed + for (size_t i = 0; i < 5; i++) { + int lat_digit = lat_val % 5; + int lng_digit = lng_val % 4; + int ndx = lat_digit * 4 + lng_digit; + tempCode[pos--] = kAlphabet[ndx]; + lat_val /= 5; + lng_val /= 4; + } + } else { + lat_val /= pow(5, 5); + lng_val /= pow(4, 5); + } - for (size_t i = 0; i < 5; i++) { // Compute pair section of code - int lat_ndx = lat_val % 20; - int lng_ndx = lng_val % 20; - tempCode[pos--] = kAlphabet[lng_ndx]; - tempCode[pos--] = kAlphabet[lat_ndx]; - lat_val /= 20; - lng_val /= 20; + pos = 10; - if (i == 0) - tempCode[pos--] = '+'; - } + for (size_t i = 0; i < 5; i++) { // Compute pair section of code + int lat_ndx = lat_val % 20; + int lng_ndx = lng_val % 20; + tempCode[pos--] = kAlphabet[lng_ndx]; + tempCode[pos--] = kAlphabet[lat_ndx]; + lat_val /= 20; + lng_val /= 20; - if (OLC_CODE_LEN < 9) { // Add padding if needed - for (size_t i = OLC_CODE_LEN; i < 9; i++) - tempCode[i] = '0'; - tempCode[9] = '+'; - } + if (i == 0) + tempCode[pos--] = '+'; + } - size_t char_count = OLC_CODE_LEN; - if (10 > char_count) { - char_count = 10; - } - for (size_t i = 0; i < char_count; i++) { - olc.code[i] = tempCode[i]; - } - olc.code[char_count] = '\0'; + if (OLC_CODE_LEN < 9) { // Add padding if needed + for (size_t i = OLC_CODE_LEN; i < 9; i++) + tempCode[i] = '0'; + tempCode[9] = '+'; + } + + size_t char_count = OLC_CODE_LEN; + if (10 > char_count) { + char_count = 10; + } + for (size_t i = 0; i < char_count; i++) { + olc.code[i] = tempCode[i]; + } + olc.code[char_count] = '\0'; } // Converts the coordinate in WGS84 datum to the OSGB36 datum. -void GeoCoord::convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude) { - // Convert lat long to cartesian - double phi = toRadians(lat); - double lambda = toRadians(lon); - double h = 0.0; // No OSTN height data used, some loss of accuracy (up to 5m) - double wgsA = 6378137; // WGS84 datum semi major axis - double wgsF = 1 / 298.257223563; // WGS84 datum flattening - double ecc = 2 * wgsF - wgsF * wgsF; - double vee = wgsA / sqrt(1 - ecc * pow(sin(phi), 2)); - double wgsX = (vee + h) * cos(phi) * cos(lambda); - double wgsY = (vee + h) * cos(phi) * sin(lambda); - double wgsZ = ((1 - ecc) * vee + h) * sin(phi); +void GeoCoord::convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude) +{ + // Convert lat long to cartesian + double phi = toRadians(lat); + double lambda = toRadians(lon); + double h = 0.0; // No OSTN height data used, some loss of accuracy (up to 5m) + double wgsA = 6378137; // WGS84 datum semi major axis + double wgsF = 1 / 298.257223563; // WGS84 datum flattening + double ecc = 2 * wgsF - wgsF * wgsF; + double vee = wgsA / sqrt(1 - ecc * pow(sin(phi), 2)); + double wgsX = (vee + h) * cos(phi) * cos(lambda); + double wgsY = (vee + h) * cos(phi) * sin(lambda); + double wgsZ = ((1 - ecc) * vee + h) * sin(phi); - // 7-parameter Helmert transform - double tx = -446.448; // x shift in meters - double ty = 125.157; // y shift in meters - double tz = -542.060; // z shift in meters - double s = 20.4894 / 1e6 + 1; // scale normalized parts per million to (s + 1) - double rx = toRadians(-0.1502 / 3600); // x rotation normalize arcseconds to radians - double ry = toRadians(-0.2470 / 3600); // y rotation normalize arcseconds to radians - double rz = toRadians(-0.8421 / 3600); // z rotation normalize arcseconds to radians - double osgbX = tx + wgsX * s - wgsY * rz + wgsZ * ry; - double osgbY = ty + wgsX * rz + wgsY * s - wgsZ * rx; - double osgbZ = tz - wgsX * ry + wgsY * rx + wgsZ * s; + // 7-parameter Helmert transform + double tx = -446.448; // x shift in meters + double ty = 125.157; // y shift in meters + double tz = -542.060; // z shift in meters + double s = 20.4894 / 1e6 + 1; // scale normalized parts per million to (s + 1) + double rx = toRadians(-0.1502 / 3600); // x rotation normalize arcseconds to radians + double ry = toRadians(-0.2470 / 3600); // y rotation normalize arcseconds to radians + double rz = toRadians(-0.8421 / 3600); // z rotation normalize arcseconds to radians + double osgbX = tx + wgsX * s - wgsY * rz + wgsZ * ry; + double osgbY = ty + wgsX * rz + wgsY * s - wgsZ * rx; + double osgbZ = tz - wgsX * ry + wgsY * rx + wgsZ * s; - // Convert cartesian to lat long - double airyA = 6377563.396; // Airy1830 datum semi major axis - double airyB = 6356256.909; // Airy1830 datum semi minor axis - double airyF = 1 / 299.3249646; // Airy1830 datum flattening - double airyEcc = 2 * airyF - airyF * airyF; - double airyEcc2 = airyEcc / (1 - airyEcc); - double p = sqrt(osgbX * osgbX + osgbY * osgbY); - double R = sqrt(p * p + osgbZ * osgbZ); - double tanBeta = (airyB * osgbZ) / (airyA * p) * (1 + airyEcc2 * airyB / R); - double sinBeta = tanBeta / sqrt(1 + tanBeta * tanBeta); - double cosBeta = sinBeta / tanBeta; - osgb_Latitude = atan2(osgbZ + airyEcc2 * airyB * sinBeta * sinBeta * sinBeta, - p - airyEcc * airyA * cosBeta * cosBeta * cosBeta); // leave in radians - osgb_Longitude = atan2(osgbY, osgbX); // leave in radians - // osgb height = p*cos(osgb.latitude) + osgbZ*sin(osgb.latitude) - - //(airyA*airyA/(airyA / sqrt(1 - - // airyEcc*sin(osgb.latitude)*sin(osgb.latitude)))); // Not used, no OSTN data + // Convert cartesian to lat long + double airyA = 6377563.396; // Airy1830 datum semi major axis + double airyB = 6356256.909; // Airy1830 datum semi minor axis + double airyF = 1 / 299.3249646; // Airy1830 datum flattening + double airyEcc = 2 * airyF - airyF * airyF; + double airyEcc2 = airyEcc / (1 - airyEcc); + double p = sqrt(osgbX * osgbX + osgbY * osgbY); + double R = sqrt(p * p + osgbZ * osgbZ); + double tanBeta = (airyB * osgbZ) / (airyA * p) * (1 + airyEcc2 * airyB / R); + double sinBeta = tanBeta / sqrt(1 + tanBeta * tanBeta); + double cosBeta = sinBeta / tanBeta; + osgb_Latitude = atan2(osgbZ + airyEcc2 * airyB * sinBeta * sinBeta * sinBeta, + p - airyEcc * airyA * cosBeta * cosBeta * cosBeta); // leave in radians + osgb_Longitude = atan2(osgbY, osgbX); // leave in radians + // osgb height = p*cos(osgb.latitude) + osgbZ*sin(osgb.latitude) - + //(airyA*airyA/(airyA / sqrt(1 - airyEcc*sin(osgb.latitude)*sin(osgb.latitude)))); // Not used, no OSTN data } /// Ported from my old java code, returns distance in meters along the globe /// surface (by Haversine formula) -float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) { - // Don't do math if the points are the same - if (lat_a == lat_b && lng_a == lng_b) - return 0.0; +float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) +{ + // Don't do math if the points are the same + if (lat_a == lat_b && lng_a == lng_b) + return 0.0; - double a1 = lat_a / DEG_CONVERT; - double a2 = lng_a / DEG_CONVERT; - double b1 = lat_b / DEG_CONVERT; - double b2 = lng_b / DEG_CONVERT; - double cos_b1 = cos(b1); - double cos_a1 = cos(a1); - double t1 = cos_a1 * cos(a2) * cos_b1 * cos(b2); - double t2 = cos_a1 * sin(a2) * cos_b1 * sin(b2); - double t3 = sin(a1) * sin(b1); - double tt = acos(t1 + t2 + t3); - if (std::isnan(tt)) - tt = 0.0; // Must have been the same point? + double a1 = lat_a / DEG_CONVERT; + double a2 = lng_a / DEG_CONVERT; + double b1 = lat_b / DEG_CONVERT; + double b2 = lng_b / DEG_CONVERT; + double cos_b1 = cos(b1); + double cos_a1 = cos(a1); + double t1 = cos_a1 * cos(a2) * cos_b1 * cos(b2); + double t2 = cos_a1 * sin(a2) * cos_b1 * sin(b2); + double t3 = sin(a1) * sin(b1); + double tt = acos(t1 + t2 + t3); + if (std::isnan(tt)) + tt = 0.0; // Must have been the same point? - return (float)(6366000 * tt); + return (float)(6366000 * tt); } /** @@ -390,13 +414,14 @@ float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double * @return Bearing from point 1 to point 2 in radians. A value of 0 means due * north. */ -float GeoCoord::bearing(double lat1, double lon1, double lat2, double lon2) { - double lat1Rad = toRadians(lat1); - double lat2Rad = toRadians(lat2); - double deltaLonRad = toRadians(lon2 - lon1); - double y = sin(deltaLonRad) * cos(lat2Rad); - double x = cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad) * cos(deltaLonRad)); - return atan2(y, x); +float GeoCoord::bearing(double lat1, double lon1, double lat2, double lon2) +{ + double lat1Rad = toRadians(lat1); + double lat2Rad = toRadians(lat2); + double deltaLonRad = toRadians(lon2 - lon1); + double y = sin(deltaLonRad) * cos(lat2Rad); + double x = cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad) * cos(deltaLonRad)); + return atan2(y, x); } /** @@ -406,10 +431,11 @@ float GeoCoord::bearing(double lat1, double lon1, double lat2, double lon2) { * The range in meters * @return range in radians on a great circle */ -float GeoCoord::rangeMetersToRadians(double range_meters) { - // 1 nm is 1852 meters - double distance_nm = range_meters * 1852; - return (PI / (180 * 60)) * distance_nm; +float GeoCoord::rangeMetersToRadians(double range_meters) +{ + // 1 nm is 1852 meters + double distance_nm = range_meters * 1852; + return (PI / (180 * 60)) * distance_nm; } /** @@ -419,20 +445,25 @@ float GeoCoord::rangeMetersToRadians(double range_meters) { * The range in radians * @return Range in meters on a great circle */ -float GeoCoord::rangeRadiansToMeters(double range_radians) { - double distance_nm = ((180 * 60) / PI) * range_radians; - // 1 meter is 0.000539957 nm - return distance_nm * 0.000539957; +float GeoCoord::rangeRadiansToMeters(double range_radians) +{ + double distance_nm = ((180 * 60) / PI) * range_radians; + // 1 meter is 0.000539957 nm + return distance_nm * 0.000539957; } // Find distance from point to passed in point -int32_t GeoCoord::distanceTo(const GeoCoord &pointB) { - return latLongToMeter(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, pointB.getLongitude() * 1e-7); +int32_t GeoCoord::distanceTo(const GeoCoord &pointB) +{ + return latLongToMeter(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, + pointB.getLongitude() * 1e-7); } // Find bearing from point to passed in point -int32_t GeoCoord::bearingTo(const GeoCoord &pointB) { - return bearing(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, pointB.getLongitude() * 1e-7); +int32_t GeoCoord::bearingTo(const GeoCoord &pointB) +{ + return bearing(this->getLatitude() * 1e-7, this->getLongitude() * 1e-7, pointB.getLatitude() * 1e-7, + pointB.getLongitude() * 1e-7); } /** @@ -444,15 +475,16 @@ int32_t GeoCoord::bearingTo(const GeoCoord &pointB) { * range in meters * @return GeoCoord object of point at bearing and range from initial point */ -std::shared_ptr GeoCoord::pointAtDistance(double bearing, double range_meters) { - double range_radians = rangeMetersToRadians(range_meters); - double lat1 = this->getLatitude() * 1e-7; - double lon1 = this->getLongitude() * 1e-7; - double lat = asin(sin(lat1) * cos(range_radians) + cos(lat1) * sin(range_radians) * cos(bearing)); - double dlon = atan2(sin(bearing) * sin(range_radians) * cos(lat1), cos(range_radians) - sin(lat1) * sin(lat)); - double lon = fmod(lon1 - dlon + PI, 2 * PI) - PI; +std::shared_ptr GeoCoord::pointAtDistance(double bearing, double range_meters) +{ + double range_radians = rangeMetersToRadians(range_meters); + double lat1 = this->getLatitude() * 1e-7; + double lon1 = this->getLongitude() * 1e-7; + double lat = asin(sin(lat1) * cos(range_radians) + cos(lat1) * sin(range_radians) * cos(bearing)); + double dlon = atan2(sin(bearing) * sin(range_radians) * cos(lat1), cos(range_radians) - sin(lat1) * sin(lat)); + double lon = fmod(lon1 - dlon + PI, 2 * PI) - PI; - return std::make_shared(double(lat), double(lon), this->getAltitude()); + return std::make_shared(double(lat), double(lon), this->getAltitude()); } /** @@ -461,41 +493,42 @@ std::shared_ptr GeoCoord::pointAtDistance(double bearing, double range * The bearing in string format * @return Bearing in degrees */ -unsigned int GeoCoord::bearingToDegrees(const char *bearing) { - if (strcmp(bearing, "N") == 0) - return 0; - else if (strcmp(bearing, "NNE") == 0) - return 22; - else if (strcmp(bearing, "NE") == 0) - return 45; - else if (strcmp(bearing, "ENE") == 0) - return 67; - else if (strcmp(bearing, "E") == 0) - return 90; - else if (strcmp(bearing, "ESE") == 0) - return 112; - else if (strcmp(bearing, "SE") == 0) - return 135; - else if (strcmp(bearing, "SSE") == 0) - return 157; - else if (strcmp(bearing, "S") == 0) - return 180; - else if (strcmp(bearing, "SSW") == 0) - return 202; - else if (strcmp(bearing, "SW") == 0) - return 225; - else if (strcmp(bearing, "WSW") == 0) - return 247; - else if (strcmp(bearing, "W") == 0) - return 270; - else if (strcmp(bearing, "WNW") == 0) - return 292; - else if (strcmp(bearing, "NW") == 0) - return 315; - else if (strcmp(bearing, "NNW") == 0) - return 337; - else - return 0; +unsigned int GeoCoord::bearingToDegrees(const char *bearing) +{ + if (strcmp(bearing, "N") == 0) + return 0; + else if (strcmp(bearing, "NNE") == 0) + return 22; + else if (strcmp(bearing, "NE") == 0) + return 45; + else if (strcmp(bearing, "ENE") == 0) + return 67; + else if (strcmp(bearing, "E") == 0) + return 90; + else if (strcmp(bearing, "ESE") == 0) + return 112; + else if (strcmp(bearing, "SE") == 0) + return 135; + else if (strcmp(bearing, "SSE") == 0) + return 157; + else if (strcmp(bearing, "S") == 0) + return 180; + else if (strcmp(bearing, "SSW") == 0) + return 202; + else if (strcmp(bearing, "SW") == 0) + return 225; + else if (strcmp(bearing, "WSW") == 0) + return 247; + else if (strcmp(bearing, "W") == 0) + return 270; + else if (strcmp(bearing, "WNW") == 0) + return 292; + else if (strcmp(bearing, "NW") == 0) + return 315; + else if (strcmp(bearing, "NNW") == 0) + return 337; + else + return 0; } /** @@ -504,52 +537,60 @@ unsigned int GeoCoord::bearingToDegrees(const char *bearing) { * The bearing in degrees * @return Bearing in string format */ -const char *GeoCoord::degreesToBearing(unsigned int degrees) { - if (degrees >= 348 || degrees < 11) - return "N"; - else if (degrees >= 11 && degrees < 34) - return "NNE"; - else if (degrees >= 34 && degrees < 56) - return "NE"; - else if (degrees >= 56 && degrees < 79) - return "ENE"; - else if (degrees >= 79 && degrees < 101) - return "E"; - else if (degrees >= 101 && degrees < 124) - return "ESE"; - else if (degrees >= 124 && degrees < 146) - return "SE"; - else if (degrees >= 146 && degrees < 169) - return "SSE"; - else if (degrees >= 169 && degrees < 191) - return "S"; - else if (degrees >= 191 && degrees < 214) - return "SSW"; - else if (degrees >= 214 && degrees < 236) - return "SW"; - else if (degrees >= 236 && degrees < 259) - return "WSW"; - else if (degrees >= 259 && degrees < 281) - return "W"; - else if (degrees >= 281 && degrees < 304) - return "WNW"; - else if (degrees >= 304 && degrees < 326) - return "NW"; - else if (degrees >= 326 && degrees < 348) - return "NNW"; - else - return "N"; +const char *GeoCoord::degreesToBearing(unsigned int degrees) +{ + if (degrees >= 348 || degrees < 11) + return "N"; + else if (degrees >= 11 && degrees < 34) + return "NNE"; + else if (degrees >= 34 && degrees < 56) + return "NE"; + else if (degrees >= 56 && degrees < 79) + return "ENE"; + else if (degrees >= 79 && degrees < 101) + return "E"; + else if (degrees >= 101 && degrees < 124) + return "ESE"; + else if (degrees >= 124 && degrees < 146) + return "SE"; + else if (degrees >= 146 && degrees < 169) + return "SSE"; + else if (degrees >= 169 && degrees < 191) + return "S"; + else if (degrees >= 191 && degrees < 214) + return "SSW"; + else if (degrees >= 214 && degrees < 236) + return "SW"; + else if (degrees >= 236 && degrees < 259) + return "WSW"; + else if (degrees >= 259 && degrees < 281) + return "W"; + else if (degrees >= 281 && degrees < 304) + return "WNW"; + else if (degrees >= 304 && degrees < 326) + return "NW"; + else if (degrees >= 326 && degrees < 348) + return "NNW"; + else + return "N"; } -double GeoCoord::pow_neg(double base, double exponent) { - if (exponent == 0) { - return 1; - } else if (exponent > 0) { - return pow(base, exponent); - } - return 1 / pow(base, -exponent); +double GeoCoord::pow_neg(double base, double exponent) +{ + if (exponent == 0) { + return 1; + } else if (exponent > 0) { + return pow(base, exponent); + } + return 1 / pow(base, -exponent); } -double GeoCoord::toRadians(double deg) { return deg * PI / 180; } +double GeoCoord::toRadians(double deg) +{ + return deg * PI / 180; +} -double GeoCoord::toDegrees(double r) { return r * 180 / PI; } \ No newline at end of file +double GeoCoord::toDegrees(double r) +{ + return r * 180 / PI; +} \ No newline at end of file diff --git a/src/gps/GeoCoord.h b/src/gps/GeoCoord.h index 5129c605e..658c177b3 100644 --- a/src/gps/GeoCoord.h +++ b/src/gps/GeoCoord.h @@ -16,132 +16,133 @@ // GeoCoord structs/classes // A struct to hold the data for a DMS coordinate. struct DMS { - uint8_t latDeg; - uint8_t latMin; - uint32_t latSec; - char latCP; - uint8_t lonDeg; - uint8_t lonMin; - uint32_t lonSec; - char lonCP; + uint8_t latDeg; + uint8_t latMin; + uint32_t latSec; + char latCP; + uint8_t lonDeg; + uint8_t lonMin; + uint32_t lonSec; + char lonCP; }; // A struct to hold the data for a UTM coordinate, this is also used when creating an MGRS coordinate. struct UTM { - uint8_t zone; - char band; - uint32_t easting; - uint32_t northing; + uint8_t zone; + char band; + uint32_t easting; + uint32_t northing; }; // A struct to hold the data for a MGRS coordinate. struct MGRS { - uint8_t zone; - char band; - char east100k; - char north100k; - uint32_t easting; - uint32_t northing; + uint8_t zone; + char band; + char east100k; + char north100k; + uint32_t easting; + uint32_t northing; }; // A struct to hold the data for a OSGR coordinate struct OSGR { - char e100k; - char n100k; - uint32_t easting; - uint32_t northing; + char e100k; + char n100k; + uint32_t easting; + uint32_t northing; }; // A struct to hold the data for a OLC coordinate struct OLC { - char code[OLC_CODE_LEN + 1]; // +1 for null termination + char code[OLC_CODE_LEN + 1]; // +1 for null termination }; -class GeoCoord { -private: - int32_t _latitude = 0; - int32_t _longitude = 0; - int32_t _altitude = 0; +class GeoCoord +{ + private: + int32_t _latitude = 0; + int32_t _longitude = 0; + int32_t _altitude = 0; - DMS _dms = {}; - UTM _utm = {}; - MGRS _mgrs = {}; - OSGR _osgr = {}; - OLC _olc = {}; + DMS _dms = {}; + UTM _utm = {}; + MGRS _mgrs = {}; + OSGR _osgr = {}; + OLC _olc = {}; - bool _dirty = true; + bool _dirty = true; - void setCoords(); + void setCoords(); -public: - GeoCoord(); - GeoCoord(int32_t lat, int32_t lon, int32_t alt); - GeoCoord(double lat, double lon, int32_t alt); - GeoCoord(float lat, float lon, int32_t alt); + public: + GeoCoord(); + GeoCoord(int32_t lat, int32_t lon, int32_t alt); + GeoCoord(double lat, double lon, int32_t alt); + GeoCoord(float lat, float lon, int32_t alt); - void updateCoords(const int32_t lat, const int32_t lon, const int32_t alt); - void updateCoords(const double lat, const double lon, const int32_t alt); - void updateCoords(const float lat, const float lon, const int32_t alt); + void updateCoords(const int32_t lat, const int32_t lon, const int32_t alt); + void updateCoords(const double lat, const double lon, const int32_t alt); + void updateCoords(const float lat, const float lon, const int32_t alt); - // Conversions - static void latLongToDMS(const double lat, const double lon, DMS &dms); - static void latLongToUTM(const double lat, const double lon, UTM &utm); - static void latLongToMGRS(const double lat, const double lon, MGRS &mgrs); - static void latLongToOSGR(const double lat, const double lon, OSGR &osgr); - static void latLongToOLC(const double lat, const double lon, OLC &olc); - static void convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude); - static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b); - static float bearing(double lat1, double lon1, double lat2, double lon2); - static float rangeRadiansToMeters(double range_radians); - static float rangeMetersToRadians(double range_meters); - static unsigned int bearingToDegrees(const char *bearing); - static const char *degreesToBearing(unsigned int degrees); + // Conversions + static void latLongToDMS(const double lat, const double lon, DMS &dms); + static void latLongToUTM(const double lat, const double lon, UTM &utm); + static void latLongToMGRS(const double lat, const double lon, MGRS &mgrs); + static void latLongToOSGR(const double lat, const double lon, OSGR &osgr); + static void latLongToOLC(const double lat, const double lon, OLC &olc); + static void convertWGS84ToOSGB36(const double lat, const double lon, double &osgb_Latitude, double &osgb_Longitude); + static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b); + static float bearing(double lat1, double lon1, double lat2, double lon2); + static float rangeRadiansToMeters(double range_radians); + static float rangeMetersToRadians(double range_meters); + static unsigned int bearingToDegrees(const char *bearing); + static const char *degreesToBearing(unsigned int degrees); - // Raises a number to an exponent, handling negative exponents. - static double pow_neg(double base, double exponent); - static double toRadians(double deg); - static double toDegrees(double r); + // Raises a number to an exponent, handling negative exponents. + static double pow_neg(double base, double exponent); + static double toRadians(double deg); + static double toDegrees(double r); - // Point to point conversions - int32_t distanceTo(const GeoCoord &pointB); - int32_t bearingTo(const GeoCoord &pointB); - std::shared_ptr pointAtDistance(double bearing, double range); + // Point to point conversions + int32_t distanceTo(const GeoCoord &pointB); + int32_t bearingTo(const GeoCoord &pointB); + std::shared_ptr pointAtDistance(double bearing, double range); - // Lat lon alt getters - int32_t getLatitude() const { return _latitude; } - int32_t getLongitude() const { return _longitude; } - int32_t getAltitude() const { return _altitude; } + // Lat lon alt getters + int32_t getLatitude() const { return _latitude; } + int32_t getLongitude() const { return _longitude; } + int32_t getAltitude() const { return _altitude; } - // DMS getters - uint8_t getDMSLatDeg() const { return _dms.latDeg; } - uint8_t getDMSLatMin() const { return _dms.latMin; } - uint32_t getDMSLatSec() const { return _dms.latSec; } - char getDMSLatCP() const { return _dms.latCP; } - uint8_t getDMSLonDeg() const { return _dms.lonDeg; } - uint8_t getDMSLonMin() const { return _dms.lonMin; } - uint32_t getDMSLonSec() const { return _dms.lonSec; } - char getDMSLonCP() const { return _dms.lonCP; } + // DMS getters + uint8_t getDMSLatDeg() const { return _dms.latDeg; } + uint8_t getDMSLatMin() const { return _dms.latMin; } + uint32_t getDMSLatSec() const { return _dms.latSec; } + char getDMSLatCP() const { return _dms.latCP; } + uint8_t getDMSLonDeg() const { return _dms.lonDeg; } + uint8_t getDMSLonMin() const { return _dms.lonMin; } + uint32_t getDMSLonSec() const { return _dms.lonSec; } + char getDMSLonCP() const { return _dms.lonCP; } - // UTM getters - uint8_t getUTMZone() const { return _utm.zone; } - char getUTMBand() const { return _utm.band; } - uint32_t getUTMEasting() const { return _utm.easting; } - uint32_t getUTMNorthing() const { return _utm.northing; } + // UTM getters + uint8_t getUTMZone() const { return _utm.zone; } + char getUTMBand() const { return _utm.band; } + uint32_t getUTMEasting() const { return _utm.easting; } + uint32_t getUTMNorthing() const { return _utm.northing; } - // MGRS getters - uint8_t getMGRSZone() const { return _mgrs.zone; } - char getMGRSBand() const { return _mgrs.band; } - char getMGRSEast100k() const { return _mgrs.east100k; } - char getMGRSNorth100k() const { return _mgrs.north100k; } - uint32_t getMGRSEasting() const { return _mgrs.easting; } - uint32_t getMGRSNorthing() const { return _mgrs.northing; } + // MGRS getters + uint8_t getMGRSZone() const { return _mgrs.zone; } + char getMGRSBand() const { return _mgrs.band; } + char getMGRSEast100k() const { return _mgrs.east100k; } + char getMGRSNorth100k() const { return _mgrs.north100k; } + uint32_t getMGRSEasting() const { return _mgrs.easting; } + uint32_t getMGRSNorthing() const { return _mgrs.northing; } - // OSGR getters - char getOSGRE100k() const { return _osgr.e100k; } - char getOSGRN100k() const { return _osgr.n100k; } - uint32_t getOSGREasting() const { return _osgr.easting; } - uint32_t getOSGRNorthing() const { return _osgr.northing; } + // OSGR getters + char getOSGRE100k() const { return _osgr.e100k; } + char getOSGRN100k() const { return _osgr.n100k; } + uint32_t getOSGREasting() const { return _osgr.easting; } + uint32_t getOSGRNorthing() const { return _osgr.northing; } - // OLC getter - void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination + // OLC getter + void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination }; \ No newline at end of file diff --git a/src/gps/NMEAWPL.cpp b/src/gps/NMEAWPL.cpp index 2d6dc094f..f4249ca62 100644 --- a/src/gps/NMEAWPL.cpp +++ b/src/gps/NMEAWPL.cpp @@ -19,32 +19,36 @@ * ------------------------------------------- */ -uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_PositionLite &pos, const char *name, bool isCaltopoMode) { - GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); - char type = isCaltopoMode ? 'P' : 'N'; - uint32_t len = snprintf(buf, bufsz, "\r\n$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), - (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), - (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), name); - uint32_t chk = 0; - for (uint32_t i = 1; i < len; i++) { - chk ^= buf[i]; - } - len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); - return len; +uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_PositionLite &pos, const char *name, bool isCaltopoMode) +{ + GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); + char type = isCaltopoMode ? 'P' : 'N'; + uint32_t len = snprintf(buf, bufsz, "\r\n$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), + (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), + geoCoord.getDMSLonDeg(), (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, + geoCoord.getDMSLonCP(), name); + uint32_t chk = 0; + for (uint32_t i = 1; i < len; i++) { + chk ^= buf[i]; + } + len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); + return len; } -uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const char *name, bool isCaltopoMode) { - GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); - char type = isCaltopoMode ? 'P' : 'N'; - uint32_t len = snprintf(buf, bufsz, "$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), - (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), - (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), name); - uint32_t chk = 0; - for (uint32_t i = 1; i < len; i++) { - chk ^= buf[i]; - } - len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); - return len; +uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const char *name, bool isCaltopoMode) +{ + GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); + char type = isCaltopoMode ? 'P' : 'N'; + uint32_t len = snprintf(buf, bufsz, "$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), + (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), + geoCoord.getDMSLonDeg(), (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, + geoCoord.getDMSLonCP(), name); + uint32_t chk = 0; + for (uint32_t i = 1; i < len; i++) { + chk ^= buf[i]; + } + len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); + return len; } /* ------------------------------------------- * 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @@ -62,36 +66,37 @@ uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const * 8 Horizontal Dilution of precision (meters) * 9 Antenna Altitude above/below mean-sea-level (geoid) (in meters) * 10 Units of antenna altitude, meters - * 11 Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level (geoid), "-" means - * mean-sea-level below ellipsoid 12 Units of geoidal separation, meters 13 Age of differential GPS data, time in - * seconds since last SC104 type 1 or 9 update, null field when DGPS is not used 14 Differential reference station ID, - * 0000-1023 15 Checksum + * 11 Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level (geoid), "-" means mean-sea-level + * below ellipsoid 12 Units of geoidal separation, meters 13 Age of differential GPS data, time in seconds since last SC104 type 1 + * or 9 update, null field when DGPS is not used 14 Differential reference station ID, 0000-1023 15 Checksum * ------------------------------------------- */ -uint32_t printGGA(char *buf, size_t bufsz, const meshtastic_Position &pos) { - GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); - time_t timestamp = pos.timestamp; +uint32_t printGGA(char *buf, size_t bufsz, const meshtastic_Position &pos) +{ + GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); + time_t timestamp = pos.timestamp; - tm *t = gmtime(×tamp); - if (getRTCQuality() > 0) { // use the device clock if we got time from somewhere. If not, use the GPS timestamp. - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice); - timestamp = rtc_sec; - t = gmtime(×tamp); - } + tm *t = gmtime(×tamp); + if (getRTCQuality() > 0) { // use the device clock if we got time from somewhere. If not, use the GPS timestamp. + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice); + timestamp = rtc_sec; + t = gmtime(×tamp); + } - uint32_t len = snprintf(buf, bufsz, "$GNGGA,%02d%02d%02d.%02d,%02d%07.4f,%c,%03d%07.4f,%c,%u,%02u,%04u,%04d,%c,%04d,%c,%d,%04d", t->tm_hour, - t->tm_min, t->tm_sec, pos.timestamp_millis_adjust, geoCoord.getDMSLatDeg(), - (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), - (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), pos.fix_quality, - pos.sats_in_view, pos.HDOP, geoCoord.getAltitude(), 'M', pos.altitude_geoidal_separation, 'M', 0, 0); + uint32_t len = snprintf( + buf, bufsz, "$GNGGA,%02d%02d%02d.%02d,%02d%07.4f,%c,%03d%07.4f,%c,%u,%02u,%04u,%04d,%c,%04d,%c,%d,%04d", t->tm_hour, + t->tm_min, t->tm_sec, pos.timestamp_millis_adjust, geoCoord.getDMSLatDeg(), + (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), + (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), pos.fix_quality, + pos.sats_in_view, pos.HDOP, geoCoord.getAltitude(), 'M', pos.altitude_geoidal_separation, 'M', 0, 0); - uint32_t chk = 0; - for (uint32_t i = 1; i < len; i++) { - chk ^= buf[i]; - } - len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); - return len; + uint32_t chk = 0; + for (uint32_t i = 1; i < len; i++) { + chk ^= buf[i]; + } + len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); + return len; } #endif \ No newline at end of file diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 8c2efd420..25cd3ceff 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -12,145 +12,149 @@ uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds -RTCQuality getRTCQuality() { return currentQuality; } +RTCQuality getRTCQuality() +{ + return currentQuality; +} // stuff that really should be in in the instance instead... -static uint32_t timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds - // to that time +static uint32_t + timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock /** * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -RTCSetResult readFromRTC() { - struct timeval tv; /* btw settimeofday() is helpful here too*/ +RTCSetResult readFromRTC() +{ + struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC - if (rtc_found.address == RV3028_RTC) { - uint32_t now = millis(); - Melopero_RV3028 rtc; + if (rtc_found.address == RV3028_RTC) { + uint32_t now = millis(); + Melopero_RV3028 rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.initI2C(); + rtc.initI2C(); #endif - tm t; - t.tm_year = rtc.getYear() - 1900; - t.tm_mon = rtc.getMonth() - 1; - t.tm_mday = rtc.getDate(); - t.tm_hour = rtc.getHour(); - t.tm_min = rtc.getMinute(); - t.tm_sec = rtc.getSecond(); - tv.tv_sec = gm_mktime(&t); - tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + tm t; + t.tm_year = rtc.getYear() - 1900; + t.tm_mon = rtc.getMonth() - 1; + t.tm_mday = rtc.getDate(); + t.tm_hour = rtc.getHour(); + t.tm_min = rtc.getMinute(); + t.tm_sec = rtc.getSecond(); + tv.tv_sec = gm_mktime(&t); + tv.tv_usec = 0; + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - } - return RTCSetResultInvalidTime; - } + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + } + return RTCSetResultInvalidTime; + } #endif - LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, - t.tm_min, t.tm_sec, printableEpoch); - if (currentQuality == RTCQualityNone) { - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - currentQuality = RTCQualityDevice; + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, + t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); + if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + currentQuality = RTCQualityDevice; + } + return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } - return RTCSetResultSuccess; - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); - } #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) - if (rtc_found.address == PCF8563_RTC) { + if (rtc_found.address == PCF8563_RTC) { #elif defined(PCF85063_RTC) - if (rtc_found.address == PCF85063_RTC) { + if (rtc_found.address == PCF85063_RTC) { #endif - uint32_t now = millis(); - SensorRtcHelper rtc; + uint32_t now = millis(); + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(Wire); + rtc.begin(Wire); #endif - RTC_DateTime datetime = rtc.getDateTime(); - tm t = datetime.toUnixTime(); - tv.tv_sec = gm_mktime(&t); - tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + RTC_DateTime datetime = rtc.getDateTime(); + tm t = datetime.toUnixTime(); + tv.tv_sec = gm_mktime(&t); + tv.tv_usec = 0; + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } -#endif - - LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, - t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - if (currentQuality == RTCQualityNone) { - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - currentQuality = RTCQualityDevice; - } - return RTCSetResultSuccess; - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); - } -#elif defined(RX8130CE_RTC) - if (rtc_found.address == RX8130CE_RTC) { - uint32_t now = millis(); -#ifdef MUZI_BASE - ArtronShop_RX8130CE rtc(&Wire1); -#else - ArtronShop_RX8130CE rtc(&Wire); -#endif - tm t; - if (rtc.getTime(&t)) { - tv.tv_sec = gm_mktime(&t); - tv.tv_usec = 0; - - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms - LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, - t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); -#ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; } - return RTCSetResultInvalidTime; - } #endif - if (currentQuality == RTCQualityNone) { + + LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, + t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); + if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + currentQuality = RTCQualityDevice; + } + return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + } +#elif defined(RX8130CE_RTC) + if (rtc_found.address == RX8130CE_RTC) { + uint32_t now = millis(); +#ifdef MUZI_BASE + ArtronShop_RX8130CE rtc(&Wire1); +#else + ArtronShop_RX8130CE rtc(&Wire); +#endif + tm t; + if (rtc.getTime(&t)) { + tv.tv_sec = gm_mktime(&t); + tv.tv_usec = 0; + + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, + t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } +#endif + if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + currentQuality = RTCQualityDevice; + } + return RTCSetResultSuccess; + } + } +#else + if (!gettimeofday(&tv, NULL)) { + uint32_t now = millis(); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; - currentQuality = RTCQualityDevice; - } - return RTCSetResultSuccess; + return RTCSetResultSuccess; } - } -#else - if (!gettimeofday(&tv, NULL)) { - uint32_t now = millis(); - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms - LOG_DEBUG("Read RTC time as %ld", printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - return RTCSetResultSuccess; - } #endif - return RTCSetResultNotSet; + return RTCSetResultNotSet; } /** @@ -162,140 +166,143 @@ RTCSetResult readFromRTC() { * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ -RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) { - static uint32_t lastSetMsec = 0; - uint32_t now = millis(); - uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms +RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) +{ + static uint32_t lastSetMsec = 0; + uint32_t now = millis(); + uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv->tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); + if (tv->tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; } - return RTCSetResultInvalidTime; - } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - // Calculate max allowed time safely to avoid overflow in logging - uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; - uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, (uint32_t)BUILD_EPOCH, - maxAllowedPrintable); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } #endif - bool shouldSet; - if (forceUpdate) { - shouldSet = true; - LOG_DEBUG("Override current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); - } else if (q > currentQuality) { - shouldSet = true; - LOG_DEBUG("Upgrade time to quality %s", RtcName(q)); - } else if (q == RTCQualityGPS) { - shouldSet = true; - LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch); - } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (12 * 60 * 60 * 1000UL))) { - // Every 12 hrs we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift - shouldSet = true; - LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch); - } else { - shouldSet = false; - LOG_DEBUG("Current RTC quality: %s. Ignore time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); - } - - if (shouldSet) { - currentQuality = q; - lastSetMsec = now; - if (currentQuality >= RTCQualityNTP) { - lastSetFromPhoneNtpOrGps = now; - } - - // This delta value works on all platforms - timeStartMsec = now; - zeroOffsetSecs = tv->tv_sec; - // If this platform has a setable RTC, set it -#ifdef RV3028_RTC - if (rtc_found.address == RV3028_RTC) { - Melopero_RV3028 rtc; -#if WIRE_INTERFACES_COUNT == 2 - rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); -#else - rtc.initI2C(); -#endif - tm *t = gmtime(&tv->tv_sec); - rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); - LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, - t->tm_sec, printableEpoch); + bool shouldSet; + if (forceUpdate) { + shouldSet = true; + LOG_DEBUG("Override current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality), + RtcName(q)); + } else if (q > currentQuality) { + shouldSet = true; + LOG_DEBUG("Upgrade time to quality %s", RtcName(q)); + } else if (q == RTCQualityGPS) { + shouldSet = true; + LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch); + } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (12 * 60 * 60 * 1000UL))) { + // Every 12 hrs we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift + shouldSet = true; + LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch); } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + shouldSet = false; + LOG_DEBUG("Current RTC quality: %s. Ignore time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); } + + if (shouldSet) { + currentQuality = q; + lastSetMsec = now; + if (currentQuality >= RTCQualityNTP) { + lastSetFromPhoneNtpOrGps = now; + } + + // This delta value works on all platforms + timeStartMsec = now; + zeroOffsetSecs = tv->tv_sec; + // If this platform has a setable RTC, set it +#ifdef RV3028_RTC + if (rtc_found.address == RV3028_RTC) { + Melopero_RV3028 rtc; +#if WIRE_INTERFACES_COUNT == 2 + rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); +#else + rtc.initI2C(); +#endif + tm *t = gmtime(&tv->tv_sec); + rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); + LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + } #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) - if (rtc_found.address == PCF8563_RTC) { + if (rtc_found.address == PCF8563_RTC) { #elif defined(PCF85063_RTC) - if (rtc_found.address == PCF85063_RTC) { + if (rtc_found.address == PCF85063_RTC) { #endif - SensorRtcHelper rtc; + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 - rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(Wire); + rtc.begin(Wire); #endif - tm *t = gmtime(&tv->tv_sec); - rtc.setDateTime(*t); - LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, - t->tm_min, t->tm_sec, printableEpoch); - } else { - LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); - } + tm *t = gmtime(&tv->tv_sec); + rtc.setDateTime(*t); + LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, + t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); + } #elif defined(RX8130CE_RTC) - if (rtc_found.address == RX8130CE_RTC) { + if (rtc_found.address == RX8130CE_RTC) { #ifdef MUZI_BASE - ArtronShop_RX8130CE rtc(&Wire1); + ArtronShop_RX8130CE rtc(&Wire1); #else - ArtronShop_RX8130CE rtc(&Wire); + ArtronShop_RX8130CE rtc(&Wire); #endif - tm *t = gmtime(&tv->tv_sec); - if (rtc.setTime(*t)) { - LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, - t->tm_sec, printableEpoch); - } else { - LOG_WARN("Failed to set time for RX8130CE"); - } - } + tm *t = gmtime(&tv->tv_sec); + if (rtc.setTime(*t)) { + LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, + t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); + } else { + LOG_WARN("Failed to set time for RX8130CE"); + } + } #elif defined(ARCH_ESP32) - settimeofday(tv, NULL); + settimeofday(tv, NULL); #endif - // nrf52 doesn't have a readable RTC (yet - software not written) + // nrf52 doesn't have a readable RTC (yet - software not written) #if HAS_RTC - readFromRTC(); + readFromRTC(); #endif - return RTCSetResultSuccess; - } else { - return RTCSetResultNotSet; // RTC was already set with a higher quality time - } + return RTCSetResultSuccess; + } else { + return RTCSetResultNotSet; // RTC was already set with a higher quality time + } } -const char *RtcName(RTCQuality quality) { - switch (quality) { - case RTCQualityNone: - return "None"; - case RTCQualityDevice: - return "Device"; - case RTCQualityFromNet: - return "Net"; - case RTCQualityNTP: - return "NTP"; - case RTCQualityGPS: - return "GPS"; - default: - return "Unknown"; - } +const char *RtcName(RTCQuality quality) +{ + switch (quality) { + case RTCQualityNone: + return "None"; + case RTCQualityDevice: + return "Device"; + case RTCQualityFromNet: + return "Net"; + case RTCQualityNTP: + return "NTP"; + case RTCQualityGPS: + return "GPS"; + default: + return "Unknown"; + } } /** @@ -305,45 +312,46 @@ const char *RtcName(RTCQuality quality) { * @param t The time to potentially set the RTC to. * @return True if the RTC was set to the provided time, false otherwise. */ -RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) { - /* Convert to unix time - The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January - 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). - */ - // horrible hack to make mktime TZ agnostic - best practise according to - // https://www.gnu.org/software/libc/manual/html_node/Broken_002ddown-Time.html - time_t res = gm_mktime(&t); - struct timeval tv; - tv.tv_sec = res; - tv.tv_usec = 0; // time.centisecond() * (10 / 1000); - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms +RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) +{ + /* Convert to unix time + The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 + (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). + */ + // horrible hack to make mktime TZ agnostic - best practise according to + // https://www.gnu.org/software/libc/manual/html_node/Broken_002ddown-Time.html + time_t res = gm_mktime(&t); + struct timeval tv; + tv.tv_sec = res; + tv.tv_usec = 0; // time.centisecond() * (10 / 1000); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH - if (tv.tv_sec < BUILD_EPOCH) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH); - lastTimeValidationWarning = millis(); + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; } - return RTCSetResultInvalidTime; - } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { - if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { - // Calculate max allowed time safely to avoid overflow in logging - uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; - uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; - LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch, (uint32_t)BUILD_EPOCH, - maxAllowedPrintable); - lastTimeValidationWarning = millis(); - } - return RTCSetResultInvalidTime; - } #endif - // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); - if (t.tm_year < 0 || t.tm_year >= 300) { - // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); - return RTCSetResultInvalidTime; - } else { - return perhapsSetRTC(q, &tv); - } + // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); + if (t.tm_year < 0 || t.tm_year >= 300) { + // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); + return RTCSetResultInvalidTime; + } else { + return perhapsSetRTC(q, &tv); + } } /** @@ -351,15 +359,16 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) { * * @return The timezone offset in seconds. */ -int32_t getTZOffset() { +int32_t getTZOffset() +{ #if MESHTASTIC_EXCLUDE_TZ - return 0; + return 0; #else - time_t now = getTime(false); - struct tm *gmt; - gmt = gmtime(&now); - gmt->tm_isdst = -1; - return (int32_t)difftime(now, mktime(gmt)); + time_t now = getTime(false); + struct tm *gmt; + gmt = gmtime(&now); + gmt->tm_isdst = -1; + return (int32_t)difftime(now, mktime(gmt)); #endif } @@ -368,12 +377,13 @@ int32_t getTZOffset() { * * @return The current time in seconds since the Unix epoch. */ -uint32_t getTime(bool local) { - if (local) { - return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs + getTZOffset(); - } else { - return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs; - } +uint32_t getTime(bool local) +{ + if (local) { + return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs + getTZOffset(); + } else { + return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs; + } } /** @@ -382,45 +392,49 @@ uint32_t getTime(bool local) { * @param minQuality The minimum quality of the RTC time required for it to be considered valid. * @return The current time from the RTC if it meets the minimum quality requirement, or 0 if the time is not valid. */ -uint32_t getValidTime(RTCQuality minQuality, bool local) { return (currentQuality >= minQuality) ? getTime(local) : 0; } +uint32_t getValidTime(RTCQuality minQuality, bool local) +{ + return (currentQuality >= minQuality) ? getTime(local) : 0; +} -time_t gm_mktime(struct tm *tm) { +time_t gm_mktime(struct tm *tm) +{ #if !MESHTASTIC_EXCLUDE_TZ - time_t result = 0; + time_t result = 0; - // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. - int year = 1900 + tm->tm_year; // tm_year is years since 1900 - int year_minus_one = year - 1; - int days_before_this_year = 0; - days_before_this_year += year_minus_one * 365; - // leap days: every 4 years, except 100s, but including 400s. - days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; - // subtract from 1970-01-01 to get days since epoch - days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); - // Now, within this tm->year, compute the days *before* this tm->month starts. - int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year - int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + // Now, within this tm->year, compute the days *before* this tm->month starts. + int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 - // If this is a leap year, and we're past February, add a day: - if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { - days_this_year_before_this_month += 1; - } + // If this is a leap year, and we're past February, add a day: + if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { + days_this_year_before_this_month += 1; + } - // And within this month: - int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 - // Now combine them all together, and convert days to seconds: - result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); - result *= 86400L; + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; - // Finally, add in the hours, minutes, and seconds of today: - result += tm->tm_hour * 3600; - result += tm->tm_min * 60; - result += tm->tm_sec; + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; - return result; + return result; #else - return mktime(tm); + return mktime(tm); #endif } diff --git a/src/gps/RTC.h b/src/gps/RTC.h index efdc677f0..06dd34c16 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -10,29 +10,29 @@ enum RTCQuality { - /// We haven't had our RTC set yet - RTCQualityNone = 0, + /// We haven't had our RTC set yet + RTCQualityNone = 0, - /// We got time from an onboard peripheral after boot. - RTCQualityDevice = 1, + /// We got time from an onboard peripheral after boot. + RTCQualityDevice = 1, - /// Some other node gave us a time we can use - RTCQualityFromNet = 2, + /// Some other node gave us a time we can use + RTCQualityFromNet = 2, - /// Our time is based on NTP - RTCQualityNTP = 3, + /// Our time is based on NTP + RTCQualityNTP = 3, - /// Our time is based on our own GPS - RTCQualityGPS = 4 + /// Our time is based on our own GPS + RTCQualityGPS = 4 }; /// The RTC set result codes /// Used to indicate the result of an attempt to set the RTC. enum RTCSetResult { - RTCSetResultNotSet = 0, ///< RTC was set successfully - RTCSetResultSuccess = 1, ///< RTC was set successfully - RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) - RTCSetResultError = 4 ///< An error occurred while setting the RTC + RTCSetResultNotSet = 0, ///< RTC was set successfully + RTCSetResultSuccess = 1, ///< RTC was set successfully + RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) + RTCSetResultError = 4 ///< An error occurred while setting the RTC }; RTCQuality getRTCQuality(); diff --git a/src/gps/ubx.h b/src/gps/ubx.h index 8bc0b091e..0fe2f01fb 100644 --- a/src/gps/ubx.h +++ b/src/gps/ubx.h @@ -1,13 +1,13 @@ static const char *failMessage = "Unable to %s"; -#define SEND_UBX_PACKET(TYPE, ID, DATA, ERRMSG, TIMEOUT) \ - do { \ - msglen = makeUBXPacket(TYPE, ID, sizeof(DATA), DATA); \ - _serial_gps->write(UBXscratch, msglen); \ - if (getACK(TYPE, ID, TIMEOUT) != GNSS_RESPONSE_OK) { \ - LOG_WARN(failMessage, #ERRMSG); \ - } \ - } while (0) +#define SEND_UBX_PACKET(TYPE, ID, DATA, ERRMSG, TIMEOUT) \ + do { \ + msglen = makeUBXPacket(TYPE, ID, sizeof(DATA), DATA); \ + _serial_gps->write(UBXscratch, msglen); \ + if (getACK(TYPE, ID, TIMEOUT) != GNSS_RESPONSE_OK) { \ + LOG_WARN(failMessage, #ERRMSG); \ + } \ + } while (0) // Power Management @@ -337,8 +337,8 @@ static const uint8_t _message_SAVE_10[] = { // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. // BBR will survive a restart, and power off for a while, but modules with small backup // batteries or super caps will not retain the config for a long power off time. -// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast -// aquisition after sleep +// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast aquisition after +// sleep // VALSET Commands for M10 // Please refer to the M10 Protocol Specification: @@ -370,13 +370,11 @@ EXTINTACTIVITY U4 0 no ext ints LIMITPEAKCURRENT L 1 // Ram layer config message: -// b5 62 06 8a 26 00 00 01 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 -10 00 d0 +// b5 62 06 8a 26 00 00 01 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 10 00 d0 // 10 01 8b de // BBR layer config message: -// b5 62 06 8a 26 00 00 02 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 -10 00 d0 +// b5 62 06 8a 26 00 00 02 00 00 01 00 d0 20 02 02 00 d0 40 05 00 00 00 05 00 d0 30 01 00 08 00 d0 10 01 09 00 d0 10 01 10 00 d0 // 10 01 8c 03 */ static const uint8_t _message_VALSET_PM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xd0, 0x20, 0x02, 0x02, 0x00, 0xd0, 0x40, @@ -398,21 +396,21 @@ CFG-ITFM replaced by 5 valset messages which can be combined into one for RAM an b5 62 06 8a 0e 00 00 01 00 00 0d 00 41 10 01 13 00 41 10 01 63 c6 */ -static const uint8_t _message_VALSET_ITFM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x41, 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; -static const uint8_t _message_VALSET_ITFM_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x41, 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; +static const uint8_t _message_VALSET_ITFM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x41, + 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; +static const uint8_t _message_VALSET_ITFM_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x41, + 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; // Turn off all NMEA messages: // Ram layer config message: -// b5 62 06 8a 22 00 00 01 00 00 c0 00 91 20 00 ca 00 91 20 00 c5 00 91 20 00 ac 00 91 20 00 b1 00 91 20 00 bb 00 91 20 -// 00 40 8f +// b5 62 06 8a 22 00 00 01 00 00 c0 00 91 20 00 ca 00 91 20 00 c5 00 91 20 00 ac 00 91 20 00 b1 00 91 20 00 bb 00 91 20 00 40 8f // Disable GLL, GSV, VTG messages in BBR layer // BBR layer config message: // b5 62 06 8a 13 00 00 02 00 00 ca 00 91 20 00 c5 00 91 20 00 b1 00 91 20 00 f8 4e static const uint8_t _message_VALSET_DISABLE_NMEA_RAM[] = { - /*0x00, 0x01, 0x00, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00 - */ + /*0x00, 0x01, 0x00, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00 */ 0x00, 0x01, 0x00, 0x00, 0xc0, 0x00, 0x91, 0x20, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xac, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x00}; @@ -439,10 +437,14 @@ static const uint8_t _message_VALSET_DISABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x0 static const uint8_t _message_VALSET_DISABLE_TXT_INFO_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; static const uint8_t _message_VALSET_DISABLE_TXT_INFO_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; -static const uint8_t _message_VALSET_ENABLE_NMEA_RAM[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; -static const uint8_t _message_VALSET_ENABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; -static const uint8_t _message_VALSET_DISABLE_SBAS_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x20, 0x00, 0x31, 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; -static const uint8_t _message_VALSET_DISABLE_SBAS_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x31, 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; +static const uint8_t _message_VALSET_ENABLE_NMEA_RAM[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, + 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; +static const uint8_t _message_VALSET_ENABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, + 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; +static const uint8_t _message_VALSET_DISABLE_SBAS_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x20, 0x00, 0x31, + 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; +static const uint8_t _message_VALSET_DISABLE_SBAS_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x31, + 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; /* Operational issues with the M10: diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 920bf82e2..4209baf5d 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -33,243 +33,252 @@ */ // Constructor -EInkDisplay::EInkDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) { - // Set dimensions in OLEDDisplay base class - this->geometry = GEOMETRY_RAWMODE; - this->displayWidth = EINK_WIDTH; - this->displayHeight = EINK_HEIGHT; +EInkDisplay::EInkDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) +{ + // Set dimensions in OLEDDisplay base class + this->geometry = GEOMETRY_RAWMODE; + this->displayWidth = EINK_WIDTH; + this->displayHeight = EINK_HEIGHT; - // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer - uint16_t shortSide = min(EINK_WIDTH, EINK_HEIGHT); - uint16_t longSide = max(EINK_WIDTH, EINK_HEIGHT); - if (shortSide % 8 != 0) - shortSide = (shortSide | 7) + 1; + // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer + uint16_t shortSide = min(EINK_WIDTH, EINK_HEIGHT); + uint16_t longSide = max(EINK_WIDTH, EINK_HEIGHT); + if (shortSide % 8 != 0) + shortSide = (shortSide | 7) + 1; - this->displayBufferSize = longSide * (shortSide / 8); + this->displayBufferSize = longSide * (shortSide / 8); } /** * Force a display update if we haven't drawn within the specified msecLimit */ -bool EInkDisplay::forceDisplay(uint32_t msecLimit) { - // No need to grab this lock because we are on our own SPI bus - // concurrency::LockGuard g(spiLock); +bool EInkDisplay::forceDisplay(uint32_t msecLimit) +{ + // No need to grab this lock because we are on our own SPI bus + // concurrency::LockGuard g(spiLock); - uint32_t now = millis(); - uint32_t sinceLast = now - lastDrawMsec; + uint32_t now = millis(); + uint32_t sinceLast = now - lastDrawMsec; - if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) - lastDrawMsec = now; - else - return false; + if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0)) + lastDrawMsec = now; + else + return false; - // FIXME - only draw bits have changed (use backbuf similar to the other displays) - const bool flipped = config.display.flip_screen; - // HACK for L1 EInk + // FIXME - only draw bits have changed (use backbuf similar to the other displays) + const bool flipped = config.display.flip_screen; + // HACK for L1 EInk #if defined(SEEED_WIO_TRACKER_L1_EINK) - // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes - for (uint32_t y = 0; y < displayHeight; y++) { - for (uint32_t x = 0; x < displayWidth; x++) { - auto b = buffer[x + (y / 8) * displayWidth]; - auto isset = b & (1 << (y & 7)); - adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + } } - } #else - for (uint32_t y = 0; y < displayHeight; y++) { - for (uint32_t x = 0; x < displayWidth; x++) { - auto b = buffer[x + (y / 8) * displayWidth]; - auto isset = b & (1 << (y & 7)); - if (flipped) - adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); - else - adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + if (flipped) + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + else + adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); + } } - } #endif - // Trigger the refresh in GxEPD2 - LOG_DEBUG("Update E-Paper"); - adafruitDisplay->nextPage(); + // Trigger the refresh in GxEPD2 + LOG_DEBUG("Update E-Paper"); + adafruitDisplay->nextPage(); - // End the update process - endUpdate(); + // End the update process + endUpdate(); - LOG_DEBUG("done"); - return true; + LOG_DEBUG("done"); + return true; } // End the update process - virtual method, overriden in derived class -void EInkDisplay::endUpdate() { - // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) - adafruitDisplay->hibernate(); +void EInkDisplay::endUpdate() +{ + // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) + adafruitDisplay->hibernate(); } // Write the buffer to the display memory -void EInkDisplay::display(void) { - // We don't allow regular 'dumb' display() calls to draw on eink until we've shown - // at least one forceDisplay() keyframe. This prevents flashing when we should the critical - // bootscreen (that we want to look nice) +void EInkDisplay::display(void) +{ + // We don't allow regular 'dumb' display() calls to draw on eink until we've shown + // at least one forceDisplay() keyframe. This prevents flashing when we should the critical + // bootscreen (that we want to look nice) - if (lastDrawMsec) { - forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower - } + if (lastDrawMsec) { + forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower + } } // Send a command to the display (low level function) -void EInkDisplay::sendCommand(uint8_t com) { - (void)com; - // Drop all commands to device (we just update the buffer) +void EInkDisplay::sendCommand(uint8_t com) +{ + (void)com; + // Drop all commands to device (we just update the buffer) } -void EInkDisplay::setDetected(uint8_t detected) { (void)detected; } +void EInkDisplay::setDetected(uint8_t detected) +{ + (void)detected; +} // Connect to the display - variant specific -bool EInkDisplay::connect() { - LOG_INFO("Do EInk init"); +bool EInkDisplay::connect() +{ + LOG_INFO("Do EInk init"); #ifdef PIN_EINK_EN - // backlight power, HIGH is backlight on, LOW is off - pinMode(PIN_EINK_EN, OUTPUT); + // backlight power, HIGH is backlight on, LOW is off + pinMode(PIN_EINK_EN, OUTPUT); #ifdef ELECROW_ThinkNode_M1 - // ThinkNode M1 has a hardware dimmable backlight. Start enabled - digitalWrite(PIN_EINK_EN, HIGH); + // ThinkNode M1 has a hardware dimmable backlight. Start enabled + digitalWrite(PIN_EINK_EN, HIGH); #else - digitalWrite(PIN_EINK_EN, LOW); + digitalWrite(PIN_EINK_EN, LOW); #endif #endif #if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(); #if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) - adafruitDisplay->setRotation(4); + adafruitDisplay->setRotation(4); #else - adafruitDisplay->setRotation(3); + adafruitDisplay->setRotation(3); #endif - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } -#elif defined(ELECROW_ThinkNode_M5) - { - // Start HSPI - hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); - - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(); - - adafruitDisplay->setRotation(4); - - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } -#elif defined(MESHLINK) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); - - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(); - adafruitDisplay->setRotation(3); - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } -#elif defined(RAK4630) || defined(MAKERPYTHON) - { - if (eink_found) { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 10, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - // RAK14000 2.13 inch b/w 250x122 does actually now support fast refresh - adafruitDisplay->setRotation(3); - // Fast refresh support for 1.54, 2.13 RAK14000 b/w , 2.9 and 4.2 - // adafruitDisplay->setRotation(1); - adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); - } else { - (void)adafruitDisplay; + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); } - } +#elif defined(ELECROW_ThinkNode_M5) + { + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS -#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(); + + adafruitDisplay->setRotation(4); + + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } +#elif defined(MESHLINK) + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); + + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } +#elif defined(RAK4630) || defined(MAKERPYTHON) + { + if (eink_found) { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 10, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + // RAK14000 2.13 inch b/w 250x122 does actually now support fast refresh + adafruitDisplay->setRotation(3); + // Fast refresh support for 1.54, 2.13 RAK14000 b/w , 2.9 and 4.2 + // adafruitDisplay->setRotation(1); + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } else { + (void)adafruitDisplay; + } + } + +#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) - { - // Start HSPI - hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - // VExt already enabled in setup() - // RTC GPIO hold disabled in setup() + { + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + // VExt already enabled in setup() + // RTC GPIO hold disabled in setup() - // Create GxEPD2 objects - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); - adafruitDisplay = new GxEPD2_BW(*lowLevel); + // Create GxEPD2 objects + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + adafruitDisplay = new GxEPD2_BW(*lowLevel); - // Init GxEPD2 - adafruitDisplay->init(); - adafruitDisplay->setRotation(3); + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); #if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) - adafruitDisplay->setRotation(0); + adafruitDisplay->setRotation(0); #endif - } + } #elif defined(PCA10059) || defined(ME25LS01) - { - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 40, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - adafruitDisplay->setRotation(0); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); - } + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 40, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + adafruitDisplay->setRotation(0); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); + } #elif defined(M5_COREINK) || defined(T_DECK_PRO) - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - adafruitDisplay->setRotation(0); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); -#elif defined(my) || defined(ESP32_S3_PICO) - { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - adafruitDisplay->setRotation(1); + adafruitDisplay->setRotation(0); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); - } +#elif defined(my) || defined(ESP32_S3_PICO) + { + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + adafruitDisplay->setRotation(1); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); + } #elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) - { - spi1 = &SPI1; - spi1->begin(); - // VExt already enabled in setup() - // RTC GPIO hold disabled in setup() + { + spi1 = &SPI1; + spi1->begin(); + // VExt already enabled in setup() + // RTC GPIO hold disabled in setup() - // Create GxEPD2 objects - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); - adafruitDisplay = new GxEPD2_BW(*lowLevel); + // Create GxEPD2 objects + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); + } +#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) + + // Detect display model, before starting SPI + EInkDetectionResult displayModel = detectEInk(); + + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + + // Create GxEPD2 object + adafruitDisplay = new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, + PIN_EINK_RES, PIN_EINK_BUSY, *hspi); // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); - adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); - } -#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) - - // Detect display model, before starting SPI - EInkDetectionResult displayModel = detectEInk(); - - // Start HSPI - hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS - - // Create GxEPD2 object - adafruitDisplay = - new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); - - // Init GxEPD2 - adafruitDisplay->init(); - adafruitDisplay->setRotation(3); #endif - return true; + return true; } #endif diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 5be061e90..9975527aa 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -22,74 +22,75 @@ * turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted? * Suggestion: perhaps similar to HELTEC_WIRELESS_PAPER issue, which resolved with rtc_gpio_hold_dis() */ -class EInkDisplay : public OLEDDisplay { - /// How often should we update the display - /// thereafter we do once per 5 minutes - uint32_t slowUpdateMsec = 5 * 60 * 1000; +class EInkDisplay : public OLEDDisplay +{ + /// How often should we update the display + /// thereafter we do once per 5 minutes + uint32_t slowUpdateMsec = 5 * 60 * 1000; -public: - /* constructor - FIXME - the parameters are not used, just a temporary hack to keep working like the old displays - */ - EInkDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); + public: + /* constructor + FIXME - the parameters are not used, just a temporary hack to keep working like the old displays + */ + EInkDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); - // Write the buffer to the display memory (for eink we only do this occasionally) - virtual void display(void) override; + // Write the buffer to the display memory (for eink we only do this occasionally) + virtual void display(void) override; - /** - * Force a display update if we haven't drawn within the specified msecLimit - * - * @return true if we did draw the screen - */ - virtual bool forceDisplay(uint32_t msecLimit = 1000); + /** + * Force a display update if we haven't drawn within the specified msecLimit + * + * @return true if we did draw the screen + */ + virtual bool forceDisplay(uint32_t msecLimit = 1000); - /** - * Run any code needed to complete an update, after the physical refresh has completed. - * Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class. - * - */ - virtual void endUpdate(); + /** + * Run any code needed to complete an update, after the physical refresh has completed. + * Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class. + * + */ + virtual void endUpdate(); - /** - * shim to make the abstraction happy - * - */ - void setDetected(uint8_t detected); + /** + * shim to make the abstraction happy + * + */ + void setDetected(uint8_t detected); -protected: - // the header size of the buffer used, e.g. for the SPI command header - virtual int getBufferOffset(void) override { return 0; } + protected: + // the header size of the buffer used, e.g. for the SPI command header + virtual int getBufferOffset(void) override { return 0; } - // Send a command to the display (low level function) - virtual void sendCommand(uint8_t com) override; + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) override; - // Connect to the display - virtual bool connect() override; + // Connect to the display + virtual bool connect() override; #ifdef GXEPD2_DRIVER_0 - // AdafruitGFX display object - wrapper for multiple drivers - // Allows runtime detection of multiple displays - // Avoid this situation if possible! - GxEPD2_Multi *adafruitDisplay = NULL; + // AdafruitGFX display object - wrapper for multiple drivers + // Allows runtime detection of multiple displays + // Avoid this situation if possible! + GxEPD2_Multi *adafruitDisplay = NULL; #else - // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific - GxEPD2_BW *adafruitDisplay = NULL; + // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific + GxEPD2_BW *adafruitDisplay = NULL; #endif - // If display uses HSPI -#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ - defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ + // If display uses HSPI +#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ + defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) - SPIClass *hspi = NULL; + SPIClass *hspi = NULL; #endif #if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) - SPIClass *spi1 = NULL; + SPIClass *spi1 = NULL; #endif -private: - // FIXME quick hack to limit drawing to a very slow rate - uint32_t lastDrawMsec = 0; + private: + // FIXME quick hack to limit drawing to a very slow rate + uint32_t lastDrawMsec = 0; }; #endif diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp index b98c0f369..8e4adf87e 100644 --- a/src/graphics/EInkDynamicDisplay.cpp +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -6,524 +6,558 @@ // Constructor EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) - : EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay") { - // If tracking ghost pixels, grab memory + : EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay") +{ + // If tracking ghost pixels, grab memory #ifdef EINK_LIMIT_GHOSTING_PX - dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros + dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros #endif } // Destructor -EInkDynamicDisplay::~EInkDynamicDisplay() { - // If we were tracking ghost pixels, free the memory +EInkDynamicDisplay::~EInkDynamicDisplay() +{ + // If we were tracking ghost pixels, free the memory #ifdef EINK_LIMIT_GHOSTING_PX - delete[] dirtyPixels; + delete[] dirtyPixels; #endif } // Screen requests a BACKGROUND frame -void EInkDynamicDisplay::display() { - addFrameFlag(BACKGROUND); - update(); +void EInkDynamicDisplay::display() +{ + addFrameFlag(BACKGROUND); + update(); } // Screen requests a RESPONSIVE frame -bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit) { - addFrameFlag(RESPONSIVE); - return update(); // (Unutilized) Base class promises to return true if update ran +bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit) +{ + addFrameFlag(RESPONSIVE); + return update(); // (Unutilized) Base class promises to return true if update ran } // Add flag for the next frame -void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag) { - // OR the new flag into the existing flags - this->frameFlags = (frameFlagTypes)(this->frameFlags | flag); +void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag) +{ + // OR the new flag into the existing flags + this->frameFlags = (frameFlagTypes)(this->frameFlags | flag); } // GxEPD2 code to set fast refresh -void EInkDynamicDisplay::configForFastRefresh() { - // Variant-specific code can go here +void EInkDynamicDisplay::configForFastRefresh() +{ + // Variant-specific code can go here #if defined(PRIVATE_HW) #else - // Otherwise: - adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); + // Otherwise: + adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height()); #endif } // GxEPD2 code to set full refresh -void EInkDynamicDisplay::configForFullRefresh() { - // Variant-specific code can go here +void EInkDynamicDisplay::configForFullRefresh() +{ + // Variant-specific code can go here #if defined(PRIVATE_HW) #else - // Otherwise: - adafruitDisplay->setFullWindow(); + // Otherwise: + adafruitDisplay->setFullWindow(); #endif } // Run any relevant GxEPD2 code, so next update will use correct refresh type -void EInkDynamicDisplay::applyRefreshMode() { - // Change from FULL to FAST - if (currentConfig == FULL && refresh == FAST) { - configForFastRefresh(); - currentConfig = FAST; - } +void EInkDynamicDisplay::applyRefreshMode() +{ + // Change from FULL to FAST + if (currentConfig == FULL && refresh == FAST) { + configForFastRefresh(); + currentConfig = FAST; + } - // Change from FAST back to FULL - else if (currentConfig == FAST && refresh == FULL) { - configForFullRefresh(); - currentConfig = FULL; - } + // Change from FAST back to FULL + else if (currentConfig == FAST && refresh == FULL) { + configForFullRefresh(); + currentConfig = FULL; + } } // Update fastRefreshCount -void EInkDynamicDisplay::adjustRefreshCounters() { - if (refresh == FAST) - fastRefreshCount++; +void EInkDynamicDisplay::adjustRefreshCounters() +{ + if (refresh == FAST) + fastRefreshCount++; - else if (refresh == FULL) - fastRefreshCount = 0; + else if (refresh == FULL) + fastRefreshCount = 0; } // Trigger the display update by calling base class -bool EInkDynamicDisplay::update() { - // Detemine the refresh mode to use, and start the update - bool refreshApproved = determineMode(); - if (refreshApproved) { - EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system - storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach() - endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL) - } else - storeAndReset(); // No update, no post-update code, just store the results +bool EInkDynamicDisplay::update() +{ + // Detemine the refresh mode to use, and start the update + bool refreshApproved = determineMode(); + if (refreshApproved) { + EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system + storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach() + endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL) + } else + storeAndReset(); // No update, no post-update code, just store the results - return refreshApproved; // (Unutilized) Base class promises to return true if update ran + return refreshApproved; // (Unutilized) Base class promises to return true if update ran } // Figure out who runs the post-update code -void EInkDynamicDisplay::endOrDetach() { - // If the GxEPD2 version reports that it has the async modifications +void EInkDynamicDisplay::endOrDetach() +{ + // If the GxEPD2 version reports that it has the async modifications #ifdef HAS_EINK_ASYNCFULL - if (previousRefresh == FULL) { - asyncRefreshRunning = true; // Set the flag - checked in determineMode(); cleared by onNotify() + if (previousRefresh == FULL) { + asyncRefreshRunning = true; // Set the flag - checked in determineMode(); cleared by onNotify() - if (previousFrameFlags & BLOCKING) - awaitRefresh(); - else { - // Async begins - LOG_DEBUG("Async full-refresh begins (drop frames)"); - notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread + if (previousFrameFlags & BLOCKING) + awaitRefresh(); + else { + // Async begins + LOG_DEBUG("Async full-refresh begins (drop frames)"); + notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread + } } - } - // Fast Refresh - else if (previousRefresh == FAST) - EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves. + // Fast Refresh + else if (previousRefresh == FAST) + EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves. - // Fallback - If using an unmodified version of GxEPD2 for some reason + // Fallback - If using an unmodified version of GxEPD2 for some reason #else - if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..) - LOG_WARN("GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update " - "lib_deps in " - "variant's platformio.ini file"); - EInkDisplay::endUpdate(); - } + if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..) + LOG_WARN( + "GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update lib_deps in " + "variant's platformio.ini file"); + EInkDisplay::endUpdate(); + } #endif } // Assess situation, pick a refresh type -bool EInkDynamicDisplay::determineMode() { - checkInitialized(); - checkForPromotion(); +bool EInkDynamicDisplay::determineMode() +{ + checkInitialized(); + checkForPromotion(); #if defined(HAS_EINK_ASYNCFULL) - checkBusyAsyncRefresh(); + checkBusyAsyncRefresh(); #endif - checkRateLimiting(); + checkRateLimiting(); - // If too soon for a new frame, or display busy, abort early - if (refresh == SKIPPED) - return false; // No refresh + // If too soon for a new frame, or display busy, abort early + if (refresh == SKIPPED) + return false; // No refresh - // -- New frame is due -- + // -- New frame is due -- - resetRateLimiting(); // Once determineMode() ends, will have to wait again - hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check - LOG_DEBUG("determineMode(): "); // Begin log entry + resetRateLimiting(); // Once determineMode() ends, will have to wait again + hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check + LOG_DEBUG("determineMode(): "); // Begin log entry - // Once mode determined, any remaining checks will bypass - checkCosmetic(); - checkDemandingFast(); - checkFrameMatchesPrevious(); - checkConsecutiveFastRefreshes(); + // Once mode determined, any remaining checks will bypass + checkCosmetic(); + checkDemandingFast(); + checkFrameMatchesPrevious(); + checkConsecutiveFastRefreshes(); #ifdef EINK_LIMIT_GHOSTING_PX - checkExcessiveGhosting(); + checkExcessiveGhosting(); #endif - checkFastRequested(); + checkFastRequested(); - if (refresh == UNSPECIFIED) - LOG_WARN("There was a flaw in the determineMode() logic"); + if (refresh == UNSPECIFIED) + LOG_WARN("There was a flaw in the determineMode() logic"); - // -- Decision has been reached -- - applyRefreshMode(); - adjustRefreshCounters(); + // -- Decision has been reached -- + applyRefreshMode(); + adjustRefreshCounters(); #ifdef EINK_LIMIT_GHOSTING_PX - // Full refresh clears any ghosting - if (refresh == FULL) - resetGhostPixelTracking(); + // Full refresh clears any ghosting + if (refresh == FULL) + resetGhostPixelTracking(); #endif - // Return - call a refresh or not? - if (refresh == SKIPPED) - return false; // Don't trigger a refresh - else - return true; // Do trigger a refresh + // Return - call a refresh or not? + if (refresh == SKIPPED) + return false; // Don't trigger a refresh + else + return true; // Do trigger a refresh } // Is this the very first frame? -void EInkDynamicDisplay::checkInitialized() { - if (!initialized) { - // Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect() - configForFullRefresh(); +void EInkDynamicDisplay::checkInitialized() +{ + if (!initialized) { + // Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect() + configForFullRefresh(); - // Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write - adafruitDisplay->clearScreen(); + // Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write + adafruitDisplay->clearScreen(); - LOG_DEBUG("initialized, "); - initialized = true; + LOG_DEBUG("initialized, "); + initialized = true; - // Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep - addFrameFlag(DEMAND_FAST); - } + // Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep + addFrameFlag(DEMAND_FAST); + } } // Was a frame skipped (rate, display busy) that should have been a FAST refresh? -void EInkDynamicDisplay::checkForPromotion() { - // If a frame was skipped (rate, display busy), then promote a BACKGROUND frame - // Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it +void EInkDynamicDisplay::checkForPromotion() +{ + // If a frame was skipped (rate, display busy), then promote a BACKGROUND frame + // Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it - switch (previousReason) { - case ASYNC_REFRESH_BLOCKED_DEMANDFAST: - addFrameFlag(DEMAND_FAST); - break; - case ASYNC_REFRESH_BLOCKED_COSMETIC: - addFrameFlag(COSMETIC); - break; - case ASYNC_REFRESH_BLOCKED_RESPONSIVE: - case EXCEEDED_RATELIMIT_FAST: - addFrameFlag(RESPONSIVE); - break; - default: - break; - } + switch (previousReason) { + case ASYNC_REFRESH_BLOCKED_DEMANDFAST: + addFrameFlag(DEMAND_FAST); + break; + case ASYNC_REFRESH_BLOCKED_COSMETIC: + addFrameFlag(COSMETIC); + break; + case ASYNC_REFRESH_BLOCKED_RESPONSIVE: + case EXCEEDED_RATELIMIT_FAST: + addFrameFlag(RESPONSIVE); + break; + default: + break; + } } // Is it too soon for another frame of this type? -void EInkDynamicDisplay::checkRateLimiting() { - // Sanity check: millis() overflow - just let the update run.. - if (previousRunMs > millis()) - return; +void EInkDynamicDisplay::checkRateLimiting() +{ + // Sanity check: millis() overflow - just let the update run.. + if (previousRunMs > millis()) + return; - // Skip update: too soon for BACKGROUND - if (frameFlags == BACKGROUND) { - if (Throttle::isWithinTimespanMs(previousRunMs, 30000)) { - refresh = SKIPPED; - reason = EXCEEDED_RATELIMIT_FULL; - return; + // Skip update: too soon for BACKGROUND + if (frameFlags == BACKGROUND) { + if (Throttle::isWithinTimespanMs(previousRunMs, 30000)) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FULL; + return; + } } - } - // No rate-limit for these special cases - if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST) - return; + // No rate-limit for these special cases + if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST) + return; - // Skip update: too soon for RESPONSIVE - if (frameFlags & RESPONSIVE) { - if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) { - refresh = SKIPPED; - reason = EXCEEDED_RATELIMIT_FAST; - LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags); - return; + // Skip update: too soon for RESPONSIVE + if (frameFlags & RESPONSIVE) { + if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) { + refresh = SKIPPED; + reason = EXCEEDED_RATELIMIT_FAST; + LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags); + return; + } } - } } // Is this frame COSMETIC (splash screens?) -void EInkDynamicDisplay::checkCosmetic() { - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkCosmetic() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // A full refresh is requested for cosmetic purposes: we have a decision - if (frameFlags & COSMETIC) { - refresh = FULL; - reason = FLAGGED_COSMETIC; - LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags); - } + // A full refresh is requested for cosmetic purposes: we have a decision + if (frameFlags & COSMETIC) { + refresh = FULL; + reason = FLAGGED_COSMETIC; + LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags); + } } // Is this a one-off special circumstance, where we REALLY want a fast refresh? -void EInkDynamicDisplay::checkDemandingFast() { - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkDemandingFast() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // A fast refresh is demanded: we have a decision - if (frameFlags & DEMAND_FAST) { - refresh = FAST; - reason = FLAGGED_DEMAND_FAST; - LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags); - } + // A fast refresh is demanded: we have a decision + if (frameFlags & DEMAND_FAST) { + refresh = FAST; + reason = FLAGGED_DEMAND_FAST; + LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags); + } } // Does the new frame match the currently displayed image? -void EInkDynamicDisplay::checkFrameMatchesPrevious() { - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkFrameMatchesPrevious() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // If frame is *not* a duplicate, abort the check - if (imageHash != previousImageHash) - return; + // If frame is *not* a duplicate, abort the check + if (imageHash != previousImageHash) + return; #if !defined(EINK_BACKGROUND_USES_FAST) - // If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality) - if (frameFlags == BACKGROUND && fastRefreshCount > 0) { - refresh = FULL; - reason = REDRAW_WITH_FULL; - LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags); - return; - } + // If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality) + if (frameFlags == BACKGROUND && fastRefreshCount > 0) { + refresh = FULL; + reason = REDRAW_WITH_FULL; + LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags); + return; + } #endif - // Not redrawn, not COSMETIC, not DEMAND_FAST - refresh = SKIPPED; - reason = FRAME_MATCHED_PREVIOUS; - LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); + // Not redrawn, not COSMETIC, not DEMAND_FAST + refresh = SKIPPED; + reason = FRAME_MATCHED_PREVIOUS; + LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); } // Have too many fast-refreshes occured consecutively, since last full refresh? -void EInkDynamicDisplay::checkConsecutiveFastRefreshes() { - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkConsecutiveFastRefreshes() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // Bypass limit if UNLIMITED_FAST mode is active - if (frameFlags & UNLIMITED_FAST) { - refresh = FAST; - reason = NO_OBJECTIONS; - LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags); - return; - } + // Bypass limit if UNLIMITED_FAST mode is active + if (frameFlags & UNLIMITED_FAST) { + refresh = FAST; + reason = NO_OBJECTIONS; + LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags); + return; + } - // If too many FAST refreshes consecutively - force a FULL refresh - if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) { - refresh = FULL; - reason = EXCEEDED_LIMIT_FASTREFRESH; - LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags); - } + // If too many FAST refreshes consecutively - force a FULL refresh + if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) { + refresh = FULL; + reason = EXCEEDED_LIMIT_FASTREFRESH; + LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags); + } } // No objections, we can perform fast-refresh, if desired -void EInkDynamicDisplay::checkFastRequested() { - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkFastRequested() +{ + if (refresh != UNSPECIFIED) + return; - if (frameFlags == BACKGROUND) { + if (frameFlags == BACKGROUND) { #ifdef EINK_BACKGROUND_USES_FAST - // If we want BACKGROUND to use fast. (FULL only when a limit is hit) - refresh = FAST; - reason = BACKGROUND_USES_FAST; - LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags); + // If we want BACKGROUND to use fast. (FULL only when a limit is hit) + refresh = FAST; + reason = BACKGROUND_USES_FAST; + LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, + frameFlags); #else - // If we do want to use FULL for BACKGROUND updates - refresh = FULL; - reason = FLAGGED_BACKGROUND; - LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND"); + // If we do want to use FULL for BACKGROUND updates + refresh = FULL; + reason = FLAGGED_BACKGROUND; + LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND"); #endif - } + } - // Sanity: confirm that we did ask for a RESPONSIVE frame. - if (frameFlags & RESPONSIVE) { - refresh = FAST; - reason = NO_OBJECTIONS; - LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags); - } + // Sanity: confirm that we did ask for a RESPONSIVE frame. + if (frameFlags & RESPONSIVE) { + refresh = FAST; + reason = NO_OBJECTIONS; + LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags); + } } // Reset the timer used for rate-limiting -void EInkDynamicDisplay::resetRateLimiting() { previousRunMs = millis(); } +void EInkDynamicDisplay::resetRateLimiting() +{ + previousRunMs = millis(); +} // Generate a hash of this frame, to compare against previous update -void EInkDynamicDisplay::hashImage() { - imageHash = 0; +void EInkDynamicDisplay::hashImage() +{ + imageHash = 0; - // Sum all bytes of the image buffer together - for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { - imageHash ^= buffer[b] << b; - } + // Sum all bytes of the image buffer together + for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) { + imageHash ^= buffer[b] << b; + } } // Store the results of determineMode() for future use, and reset for next call -void EInkDynamicDisplay::storeAndReset() { - previousFrameFlags = frameFlags; - previousRefresh = refresh; - previousReason = reason; +void EInkDynamicDisplay::storeAndReset() +{ + previousFrameFlags = frameFlags; + previousRefresh = refresh; + previousReason = reason; - // Only store image hash if the display will update - if (refresh != SKIPPED) { - previousImageHash = imageHash; - } + // Only store image hash if the display will update + if (refresh != SKIPPED) { + previousImageHash = imageHash; + } - frameFlags = BACKGROUND; - refresh = UNSPECIFIED; + frameFlags = BACKGROUND; + refresh = UNSPECIFIED; } #ifdef EINK_LIMIT_GHOSTING_PX // Count how many ghost pixels the new image will display -void EInkDynamicDisplay::countGhostPixels() { - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::countGhostPixels() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - // Start a new count - ghostPixelCount = 0; + // Start a new count + ghostPixelCount = 0; - // Check new image, bit by bit, for any white pixels at locations marked "dirty" - for (uint16_t i = 0; i < displayBufferSize; i++) { - for (uint8_t bit = 0; bit < 7; bit++) { + // Check new image, bit by bit, for any white pixels at locations marked "dirty" + for (uint16_t i = 0; i < displayBufferSize; i++) { + for (uint8_t bit = 0; bit < 7; bit++) { - const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh? - const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image? + const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh? + const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image? - // If pixel is (or has been) black since last full-refresh, and now is white: ghosting - if (dirty && shouldBeBlank) - ghostPixelCount++; + // If pixel is (or has been) black since last full-refresh, and now is white: ghosting + if (dirty && shouldBeBlank) + ghostPixelCount++; - // Update the dirty status for this pixel - will this location become a ghost if set white in future? - if (!dirty && !shouldBeBlank) - dirtyPixels[i] |= (1 << bit); + // Update the dirty status for this pixel - will this location become a ghost if set white in future? + if (!dirty && !shouldBeBlank) + dirtyPixels[i] |= (1 << bit); + } } - } - LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount); + LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount); } // Check if ghost pixel count exceeds the defined limit -void EInkDynamicDisplay::checkExcessiveGhosting() { - // If a decision was already reached, don't run the check - if (refresh != UNSPECIFIED) - return; +void EInkDynamicDisplay::checkExcessiveGhosting() +{ + // If a decision was already reached, don't run the check + if (refresh != UNSPECIFIED) + return; - countGhostPixels(); + countGhostPixels(); - // If too many ghost pixels, select full refresh - if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) { - refresh = FULL; - reason = EXCEEDED_GHOSTINGLIMIT; - LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags); - } + // If too many ghost pixels, select full refresh + if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) { + refresh = FULL; + reason = EXCEEDED_GHOSTINGLIMIT; + LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags); + } } // Clear the dirty pixels array. Call when full-refresh cleans the display. -void EInkDynamicDisplay::resetGhostPixelTracking() { - // Copy the current frame into dirtyPixels[] from the display buffer - memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); +void EInkDynamicDisplay::resetGhostPixelTracking() +{ + // Copy the current frame into dirtyPixels[] from the display buffer + memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); } #endif // EINK_LIMIT_GHOSTING_PX // Handle any asyc tasks -void EInkDynamicDisplay::onNotify(uint32_t notification) { - // Which task - switch (notification) { - case DUE_POLL_ASYNCREFRESH: - pollAsyncRefresh(); - break; - } +void EInkDynamicDisplay::onNotify(uint32_t notification) +{ + // Which task + switch (notification) { + case DUE_POLL_ASYNCREFRESH: + pollAsyncRefresh(); + break; + } } #ifdef HAS_EINK_ASYNCFULL // Public: wait for an refresh already in progress, then run the post-update code. See Screen::setScreensaverFrames() -void EInkDynamicDisplay::joinAsyncRefresh() { - // If no async refresh running, nothing to do - if (!asyncRefreshRunning) - return; +void EInkDynamicDisplay::joinAsyncRefresh() +{ + // If no async refresh running, nothing to do + if (!asyncRefreshRunning) + return; - LOG_DEBUG("Join an async refresh in progress"); + LOG_DEBUG("Join an async refresh in progress"); - // Continually poll the BUSY pin - while (adafruitDisplay->epd2.isBusy()) - yield(); + // Continually poll the BUSY pin + while (adafruitDisplay->epd2.isBusy()) + yield(); - // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done - adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code - EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) - asyncRefreshRunning = false; // Unset the flag - LOG_DEBUG("Refresh complete"); + // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done + adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code + EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) + asyncRefreshRunning = false; // Unset the flag + LOG_DEBUG("Refresh complete"); - // Note: this code only works because of a modification to meshtastic/GxEPD2. - // It is only equipped to intercept calls to nextPage() + // Note: this code only works because of a modification to meshtastic/GxEPD2. + // It is only equipped to intercept calls to nextPage() } // Called from NotifiedWorkerThread. Run the post-update code if the hardware is ready -void EInkDynamicDisplay::pollAsyncRefresh() { - // In theory, this condition should never be met - if (!asyncRefreshRunning) - return; +void EInkDynamicDisplay::pollAsyncRefresh() +{ + // In theory, this condition should never be met + if (!asyncRefreshRunning) + return; - // Still running, check back later - if (adafruitDisplay->epd2.isBusy()) { - // Schedule next call of pollAsyncRefresh() - NotifiedWorkerThread::notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); - return; - } + // Still running, check back later + if (adafruitDisplay->epd2.isBusy()) { + // Schedule next call of pollAsyncRefresh() + NotifiedWorkerThread::notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); + return; + } - // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done - adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code - EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) - asyncRefreshRunning = false; // Unset the flag - LOG_DEBUG("Async full-refresh complete"); + // If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done + adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code + EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) + asyncRefreshRunning = false; // Unset the flag + LOG_DEBUG("Async full-refresh complete"); - // Note: this code only works because of a modification to meshtastic/GxEPD2. - // It is only equipped to intercept calls to nextPage() + // Note: this code only works because of a modification to meshtastic/GxEPD2. + // It is only equipped to intercept calls to nextPage() } // Check the status of "async full-refresh"; skip if running -void EInkDynamicDisplay::checkBusyAsyncRefresh() { - // No refresh taking place, continue with determineMode() - if (!asyncRefreshRunning) - return; +void EInkDynamicDisplay::checkBusyAsyncRefresh() +{ + // No refresh taking place, continue with determineMode() + if (!asyncRefreshRunning) + return; - // Full refresh still running - if (adafruitDisplay->epd2.isBusy()) { - // No refresh - refresh = SKIPPED; + // Full refresh still running + if (adafruitDisplay->epd2.isBusy()) { + // No refresh + refresh = SKIPPED; - // Set the reason, marking what type of frame we're skipping - if (frameFlags & DEMAND_FAST) - reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST; - else if (frameFlags & COSMETIC) - reason = ASYNC_REFRESH_BLOCKED_COSMETIC; - else if (frameFlags & RESPONSIVE) - reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE; + // Set the reason, marking what type of frame we're skipping + if (frameFlags & DEMAND_FAST) + reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST; + else if (frameFlags & COSMETIC) + reason = ASYNC_REFRESH_BLOCKED_COSMETIC; + else if (frameFlags & RESPONSIVE) + reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE; + else + reason = ASYNC_REFRESH_BLOCKED_BACKGROUND; + + return; + } + + // Async refresh appears to have stopped, but wasn't caught by onNotify() else - reason = ASYNC_REFRESH_BLOCKED_BACKGROUND; - - return; - } - - // Async refresh appears to have stopped, but wasn't caught by onNotify() - else - pollAsyncRefresh(); // Check (and terminate) the async refresh manually + pollAsyncRefresh(); // Check (and terminate) the async refresh manually } // Hold control while an async refresh runs -void EInkDynamicDisplay::awaitRefresh() { - // Continually poll the BUSY pin - while (adafruitDisplay->epd2.isBusy()) - yield(); +void EInkDynamicDisplay::awaitRefresh() +{ + // Continually poll the BUSY pin + while (adafruitDisplay->epd2.isBusy()) + yield(); - // End the full-refresh process - adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code - EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) - asyncRefreshRunning = false; // Unset the flag + // End the full-refresh process + adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code + EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override) + asyncRefreshRunning = false; // Unset the flag } #endif // HAS_EINK_ASYNCFULL diff --git a/src/graphics/EInkDynamicDisplay.h b/src/graphics/EInkDynamicDisplay.h index 78b6476ec..d5e29e3f0 100644 --- a/src/graphics/EInkDynamicDisplay.h +++ b/src/graphics/EInkDynamicDisplay.h @@ -15,130 +15,131 @@ (Full, Fast, Skip) */ -class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWorkerThread { -public: - // Constructor - // ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class ) - EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus); - ~EInkDynamicDisplay(); +class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWorkerThread +{ + public: + // Constructor + // ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class ) + EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus); + ~EInkDynamicDisplay(); - // Methods to enable or disable unlimited fast refresh mode - void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); } - void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); } + // Methods to enable or disable unlimited fast refresh mode + void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); } + void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); } - // What kind of frame is this - enum frameFlagTypes : uint8_t { - BACKGROUND = (1 << 0), // For frames via display() - RESPONSIVE = (1 << 1), // For frames via forceDisplay() - COSMETIC = (1 << 2), // For splashes - DEMAND_FAST = (1 << 3), // Special case only - BLOCKING = (1 << 4), // Modifier - block while refresh runs - UNLIMITED_FAST = (1 << 5) - }; - void addFrameFlag(frameFlagTypes flag); + // What kind of frame is this + enum frameFlagTypes : uint8_t { + BACKGROUND = (1 << 0), // For frames via display() + RESPONSIVE = (1 << 1), // For frames via forceDisplay() + COSMETIC = (1 << 2), // For splashes + DEMAND_FAST = (1 << 3), // Special case only + BLOCKING = (1 << 4), // Modifier - block while refresh runs + UNLIMITED_FAST = (1 << 5) + }; + void addFrameFlag(frameFlagTypes flag); - // Set the correct frame flag, then call universal "update()" method - void display() override; - bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused. + // Set the correct frame flag, then call universal "update()" method + void display() override; + bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused. -protected: - enum refreshTypes : uint8_t { // Which refresh operation will be used - UNSPECIFIED, - FULL, - FAST, - SKIPPED, - }; - enum reasonTypes : uint8_t { // How was the decision reached - NO_OBJECTIONS, - ASYNC_REFRESH_BLOCKED_DEMANDFAST, - ASYNC_REFRESH_BLOCKED_COSMETIC, - ASYNC_REFRESH_BLOCKED_RESPONSIVE, - ASYNC_REFRESH_BLOCKED_BACKGROUND, - EXCEEDED_RATELIMIT_FAST, - EXCEEDED_RATELIMIT_FULL, - FLAGGED_COSMETIC, - FLAGGED_DEMAND_FAST, - EXCEEDED_LIMIT_FASTREFRESH, - EXCEEDED_GHOSTINGLIMIT, - FRAME_MATCHED_PREVIOUS, - BACKGROUND_USES_FAST, - FLAGGED_BACKGROUND, - REDRAW_WITH_FULL, - }; + protected: + enum refreshTypes : uint8_t { // Which refresh operation will be used + UNSPECIFIED, + FULL, + FAST, + SKIPPED, + }; + enum reasonTypes : uint8_t { // How was the decision reached + NO_OBJECTIONS, + ASYNC_REFRESH_BLOCKED_DEMANDFAST, + ASYNC_REFRESH_BLOCKED_COSMETIC, + ASYNC_REFRESH_BLOCKED_RESPONSIVE, + ASYNC_REFRESH_BLOCKED_BACKGROUND, + EXCEEDED_RATELIMIT_FAST, + EXCEEDED_RATELIMIT_FULL, + FLAGGED_COSMETIC, + FLAGGED_DEMAND_FAST, + EXCEEDED_LIMIT_FASTREFRESH, + EXCEEDED_GHOSTINGLIMIT, + FRAME_MATCHED_PREVIOUS, + BACKGROUND_USES_FAST, + FLAGGED_BACKGROUND, + REDRAW_WITH_FULL, + }; - enum notificationTypes : uint8_t { // What was onNotify() called for - NONE = 0, // This behavior (NONE=0) is fixed by NotifiedWorkerThread class - DUE_POLL_ASYNCREFRESH = 1, - }; - const uint32_t intervalPollAsyncRefresh = 100; + enum notificationTypes : uint8_t { // What was onNotify() called for + NONE = 0, // This behavior (NONE=0) is fixed by NotifiedWorkerThread class + DUE_POLL_ASYNCREFRESH = 1, + }; + const uint32_t intervalPollAsyncRefresh = 100; - void onNotify(uint32_t notification) override; // Handle any async tasks - overrides NotifiedWorkerThread - void configForFastRefresh(); // GxEPD2 code to set fast-refresh - void configForFullRefresh(); // GxEPD2 code to set full-refresh - bool determineMode(); // Assess situation, pick a refresh type - void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type - void adjustRefreshCounters(); // Update fastRefreshCount - bool update(); // Trigger the display update - determine mode, then call base class - void endOrDetach(); // Run the post-update code, or delegate it off to checkBusyAsyncRefresh() + void onNotify(uint32_t notification) override; // Handle any async tasks - overrides NotifiedWorkerThread + void configForFastRefresh(); // GxEPD2 code to set fast-refresh + void configForFullRefresh(); // GxEPD2 code to set full-refresh + bool determineMode(); // Assess situation, pick a refresh type + void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type + void adjustRefreshCounters(); // Update fastRefreshCount + bool update(); // Trigger the display update - determine mode, then call base class + void endOrDetach(); // Run the post-update code, or delegate it off to checkBusyAsyncRefresh() - // Checks as part of determineMode() - void checkInitialized(); // Is this the very first frame? - void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh? - void checkRateLimiting(); // Is this frame too soon? - void checkCosmetic(); // Was the COSMETIC flag set? - void checkDemandingFast(); // Was the DEMAND_FAST flag set? - void checkFrameMatchesPrevious(); // Does the new frame match the existing display image? - void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively? - void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND? + // Checks as part of determineMode() + void checkInitialized(); // Is this the very first frame? + void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh? + void checkRateLimiting(); // Is this frame too soon? + void checkCosmetic(); // Was the COSMETIC flag set? + void checkDemandingFast(); // Was the DEMAND_FAST flag set? + void checkFrameMatchesPrevious(); // Does the new frame match the existing display image? + void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively? + void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND? - void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting - void hashImage(); // Generate a hashed version of this frame, to compare against previous update - void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call + void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting + void hashImage(); // Generate a hashed version of this frame, to compare against previous update + void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call - // What we are determining for this frame - frameFlagTypes frameFlags = BACKGROUND; // Frame characteristics - determineMode() input - refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output - reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used + // What we are determining for this frame + frameFlagTypes frameFlags = BACKGROUND; // Frame characteristics - determineMode() input + refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output + reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used - // What happened last time determineMode() ran - frameFlagTypes previousFrameFlags = BACKGROUND; // (Previous) Frame flags - refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome - reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason + // What happened last time determineMode() ran + frameFlagTypes previousFrameFlags = BACKGROUND; // (Previous) Frame flags + refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome + reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason - bool initialized = false; // Have we drawn at least one frame yet? - uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting) - uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed! - uint32_t previousImageHash = 0; // Hash of the previous update's frame - uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh? - refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for + bool initialized = false; // Have we drawn at least one frame yet? + uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting) + uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed! + uint32_t previousImageHash = 0; // Hash of the previous update's frame + uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh? + refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for - // Optional - track ghosting, pixel by pixel - // May 2024: no longer used by any display. Kept for possible future use. + // Optional - track ghosting, pixel by pixel + // May 2024: no longer used by any display. Kept for possible future use. #ifdef EINK_LIMIT_GHOSTING_PX - void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh - void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit - void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. - uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) - uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use + void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh + void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit + void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. + uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) + uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use #endif - // Conditional - async full refresh - only with modified meshtastic/GxEPD2 + // Conditional - async full refresh - only with modified meshtastic/GxEPD2 #if defined(HAS_EINK_ASYNCFULL) -public: - void joinAsyncRefresh(); // Main thread joins an async refresh already in progress. Blocks, then runs post-update code + public: + void joinAsyncRefresh(); // Main thread joins an async refresh already in progress. Blocks, then runs post-update code -protected: - void pollAsyncRefresh(); // Run the post-update code if the hardware is ready - void checkBusyAsyncRefresh(); // Check if display is busy running an async full-refresh (rejecting new frames) - void awaitRefresh(); // Hold control while an async refresh runs - void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay() - bool asyncRefreshRunning = false; // Flag, checked by checkBusyAsyncRefresh() + protected: + void pollAsyncRefresh(); // Run the post-update code if the hardware is ready + void checkBusyAsyncRefresh(); // Check if display is busy running an async full-refresh (rejecting new frames) + void awaitRefresh(); // Hold control while an async refresh runs + void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay() + bool asyncRefreshRunning = false; // Flag, checked by checkBusyAsyncRefresh() #else -public: - void joinAsyncRefresh() {} // Dummy method + public: + void joinAsyncRefresh() {} // Dummy method -protected: - void pollAsyncRefresh() {} // Dummy method. In theory, not reachable + protected: + void pollAsyncRefresh() {} // Dummy method. In theory, not reachable #endif }; diff --git a/src/graphics/GxEPD2Multi.h b/src/graphics/GxEPD2Multi.h index 0ab5e5415..f3807c9de 100644 --- a/src/graphics/GxEPD2Multi.h +++ b/src/graphics/GxEPD2Multi.h @@ -4,117 +4,132 @@ // Workaround for issue of GxEPD2_BW objects not having a shared base class // Only exposes methods which we are actually using -template class GxEPD2_Multi { -public: - void drawPixel(int16_t x, int16_t y, uint16_t color) { - if (which == 0) - driver0->drawPixel(x, y, color); - else - driver1->drawPixel(x, y, color); - } - - bool nextPage() { - if (which == 0) - return driver0->nextPage(); - else - return driver1->nextPage(); - } - - void hibernate() { - if (which == 0) - driver0->hibernate(); - else - driver1->hibernate(); - } - - void init(uint32_t serial_diag_bitrate = 0) { - if (which == 0) - driver0->init(serial_diag_bitrate); - else - driver1->init(serial_diag_bitrate); - } - - void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) { - if (which == 0) - driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); - else - driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); - } - - void setRotation(uint8_t x) { - if (which == 0) - driver0->setRotation(x); - else - driver1->setRotation(x); - } - - void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { - if (which == 0) - driver0->setPartialWindow(x, y, w, h); - else - driver1->setPartialWindow(x, y, w, h); - } - - void setFullWindow() { - if (which == 0) - driver0->setFullWindow(); - else - driver1->setFullWindow(); - } - - int16_t width() { - if (which == 0) - return driver0->width(); - else - return driver1->width(); - } - - int16_t height() { - if (which == 0) - return driver0->height(); - else - return driver1->height(); - } - - void clearScreen(uint8_t value = 0xFF) { - if (which == 0) - driver0->clearScreen(); - else - driver1->clearScreen(); - } - - void endAsyncFull() { - if (which == 0) - driver0->endAsyncFull(); - else - driver1->endAsyncFull(); - } - - // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd - class Epd2Wrapper { +template class GxEPD2_Multi +{ public: - bool isBusy() { return m_epd2->isBusy(); } - GxEPD2_EPD *m_epd2; - } epd2; - - // Constructor - // Select driver by passing whichDriver as 0 or 1 - GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) { - assert(whichDriver == 0 || whichDriver == 1); - which = whichDriver; - LOG_DEBUG("GxEPD2_Multi driver: %d", which); - - if (which == 0) { - driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); - epd2.m_epd2 = &(driver0->epd2); - } else if (which == 1) { - driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); - epd2.m_epd2 = &(driver1->epd2); + void drawPixel(int16_t x, int16_t y, uint16_t color) + { + if (which == 0) + driver0->drawPixel(x, y, color); + else + driver1->drawPixel(x, y, color); } - } -private: - uint8_t which; - GxEPD2_BW *driver0; - GxEPD2_BW *driver1; + bool nextPage() + { + if (which == 0) + return driver0->nextPage(); + else + return driver1->nextPage(); + } + + void hibernate() + { + if (which == 0) + driver0->hibernate(); + else + driver1->hibernate(); + } + + void init(uint32_t serial_diag_bitrate = 0) + { + if (which == 0) + driver0->init(serial_diag_bitrate); + else + driver1->init(serial_diag_bitrate); + } + + void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) + { + if (which == 0) + driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + else + driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + } + + void setRotation(uint8_t x) + { + if (which == 0) + driver0->setRotation(x); + else + driver1->setRotation(x); + } + + void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) + { + if (which == 0) + driver0->setPartialWindow(x, y, w, h); + else + driver1->setPartialWindow(x, y, w, h); + } + + void setFullWindow() + { + if (which == 0) + driver0->setFullWindow(); + else + driver1->setFullWindow(); + } + + int16_t width() + { + if (which == 0) + return driver0->width(); + else + return driver1->width(); + } + + int16_t height() + { + if (which == 0) + return driver0->height(); + else + return driver1->height(); + } + + void clearScreen(uint8_t value = 0xFF) + { + if (which == 0) + driver0->clearScreen(); + else + driver1->clearScreen(); + } + + void endAsyncFull() + { + if (which == 0) + driver0->endAsyncFull(); + else + driver1->endAsyncFull(); + } + + // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd + class Epd2Wrapper + { + public: + bool isBusy() { return m_epd2->isBusy(); } + GxEPD2_EPD *m_epd2; + } epd2; + + // Constructor + // Select driver by passing whichDriver as 0 or 1 + GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) + { + assert(whichDriver == 0 || whichDriver == 1); + which = whichDriver; + LOG_DEBUG("GxEPD2_Multi driver: %d", which); + + if (which == 0) { + driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver0->epd2); + } else if (which == 1) { + driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver1->epd2); + } + } + + private: + uint8_t which; + GxEPD2_BW *driver0; + GxEPD2_BW *driver1; }; \ No newline at end of file diff --git a/src/graphics/Panel_sdl.cpp b/src/graphics/Panel_sdl.cpp index 3aa2f3bd3..bad6072f9 100644 --- a/src/graphics/Panel_sdl.cpp +++ b/src/graphics/Panel_sdl.cpp @@ -33,8 +33,10 @@ Porting for SDL: #define M_PI 3.14159265358979323846 #endif -namespace lgfx { -inline namespace v1 { +namespace lgfx +{ +inline namespace v1 +{ SDL_Keymod Panel_sdl::_keymod = KMOD_NONE; static SDL_semaphore *_update_in_semaphore = nullptr; static SDL_semaphore *_update_out_semaphore = nullptr; @@ -45,593 +47,637 @@ static bool _all_close = false; volatile uint8_t Panel_sdl::_gpio_dummy_values[EMULATED_GPIO_MAX]; -static inline void *heap_alloc_dma(size_t length) { return malloc(length); } // aligned_alloc(16, length); -static inline void heap_free(void *buf) { free(buf); } +static inline void *heap_alloc_dma(size_t length) +{ + return malloc(length); +} // aligned_alloc(16, length); +static inline void heap_free(void *buf) +{ + free(buf); +} static std::list _list_monitor; -static monitor_t *const getMonitorByWindowID(uint32_t windowID) { - for (auto &m : _list_monitor) { - if (SDL_GetWindowID(m->window) == windowID) { - return m; +static monitor_t *const getMonitorByWindowID(uint32_t windowID) +{ + for (auto &m : _list_monitor) { + if (SDL_GetWindowID(m->window) == windowID) { + return m; + } } - } - return nullptr; + return nullptr; } //---------------------------------------------------------------------------- static std::vector _key_code_map; -void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio) { - if (gpio > EMULATED_GPIO_MAX) - return; - KeyCodeMapping_t map; - map.keycode = keyCode; - map.gpio = gpio; - _key_code_map.push_back(map); +void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio) +{ + if (gpio > EMULATED_GPIO_MAX) + return; + KeyCodeMapping_t map; + map.keycode = keyCode; + map.gpio = gpio; + _key_code_map.push_back(map); } -int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode) { - for (const auto &i : _key_code_map) { - if (i.keycode == keyCode) - return i.gpio; - } - return -1; -} - -void Panel_sdl::_event_proc(void) { - SDL_Event event; - while (SDL_PollEvent(&event)) { - if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) { - auto mon = getMonitorByWindowID(event.button.windowID); - int gpio = -1; - - /// Check key mapping - gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym); - if (gpio < 0) { - switch (event.key.keysym.sym) { /// M5StackのBtnA~BtnCのエミュレート; - // case SDLK_LEFT: gpio = 39; break; - // case SDLK_DOWN: gpio = 38; break; - // case SDLK_RIGHT: gpio = 37; break; - // case SDLK_UP: gpio = 36; break; - - /// L/Rキーで画面回転 - case SDLK_r: - case SDLK_l: - if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { - if (mon != nullptr) { - mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1); - int x, y, w, h; - SDL_GetWindowSize(mon->window, &w, &h); - SDL_GetWindowPosition(mon->window, &x, &y); - SDL_SetWindowSize(mon->window, h, w); - SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2); - mon->panel->sdl_invalidate(); - } - } - break; - - /// 1~6キーで画面拡大率変更 - case SDLK_1: - case SDLK_2: - case SDLK_3: - case SDLK_4: - case SDLK_5: - case SDLK_6: - if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { - if (mon != nullptr) { - int size = 1 + (event.key.keysym.sym - SDLK_1); - _update_scaling(mon, size, size); - } - } - break; - default: - continue; - } - } - - if (event.type == SDL_KEYDOWN) { - Panel_sdl::gpio_lo(gpio); - } else { - Panel_sdl::gpio_hi(gpio); - } - } else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) { - auto mon = getMonitorByWindowID(event.button.windowID); - if (mon != nullptr) { - { - int x, y, w, h; - SDL_GetWindowSize(mon->window, &w, &h); - SDL_GetMouseState(&x, &y); - float sf = sinf(mon->frame_angle * M_PI / 180); - float cf = cosf(mon->frame_angle * M_PI / 180); - x -= w / 2.0f; - y -= h / 2.0f; - float nx = y * sf + x * cf; - float ny = y * cf - x * sf; - if (mon->frame_rotation & 1) { - std::swap(w, h); - } - x = (nx * mon->frame_width / w) + (mon->frame_width >> 1); - y = (ny * mon->frame_height / h) + (mon->frame_height >> 1); - mon->touch_x = x - mon->frame_inner_x; - mon->touch_y = y - mon->frame_inner_y; - } - if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) { - mon->touched = true; - } - if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) { - mon->touched = false; - } - } - } else if (event.type == SDL_WINDOWEVENT) { - auto monitor = getMonitorByWindowID(event.window.windowID); - if (monitor) { - if (event.window.event == SDL_WINDOWEVENT_RESIZED) { - int mw, mh; - SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh); - if (monitor->frame_rotation & 1) { - std::swap(mw, mh); - } - monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f; - monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f; - monitor->panel->sdl_invalidate(); - } else if (event.window.event == SDL_WINDOWEVENT_CLOSE) { - monitor->closing = true; - } - } - } else if (event.type == SDL_QUIT) { - for (auto &m : _list_monitor) { - m->closing = true; - } +int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode) +{ + for (const auto &i : _key_code_map) { + if (i.keycode == keyCode) + return i.gpio; + } + return -1; +} + +void Panel_sdl::_event_proc(void) +{ + SDL_Event event; + while (SDL_PollEvent(&event)) { + if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) { + auto mon = getMonitorByWindowID(event.button.windowID); + int gpio = -1; + + /// Check key mapping + gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym); + if (gpio < 0) { + switch (event.key.keysym.sym) { /// M5StackのBtnA~BtnCのエミュレート; + // case SDLK_LEFT: gpio = 39; break; + // case SDLK_DOWN: gpio = 38; break; + // case SDLK_RIGHT: gpio = 37; break; + // case SDLK_UP: gpio = 36; break; + + /// L/Rキーで画面回転 + case SDLK_r: + case SDLK_l: + if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { + if (mon != nullptr) { + mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1); + int x, y, w, h; + SDL_GetWindowSize(mon->window, &w, &h); + SDL_GetWindowPosition(mon->window, &x, &y); + SDL_SetWindowSize(mon->window, h, w); + SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2); + mon->panel->sdl_invalidate(); + } + } + break; + + /// 1~6キーで画面拡大率変更 + case SDLK_1: + case SDLK_2: + case SDLK_3: + case SDLK_4: + case SDLK_5: + case SDLK_6: + if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { + if (mon != nullptr) { + int size = 1 + (event.key.keysym.sym - SDLK_1); + _update_scaling(mon, size, size); + } + } + break; + default: + continue; + } + } + + if (event.type == SDL_KEYDOWN) { + Panel_sdl::gpio_lo(gpio); + } else { + Panel_sdl::gpio_hi(gpio); + } + } else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) { + auto mon = getMonitorByWindowID(event.button.windowID); + if (mon != nullptr) { + { + int x, y, w, h; + SDL_GetWindowSize(mon->window, &w, &h); + SDL_GetMouseState(&x, &y); + float sf = sinf(mon->frame_angle * M_PI / 180); + float cf = cosf(mon->frame_angle * M_PI / 180); + x -= w / 2.0f; + y -= h / 2.0f; + float nx = y * sf + x * cf; + float ny = y * cf - x * sf; + if (mon->frame_rotation & 1) { + std::swap(w, h); + } + x = (nx * mon->frame_width / w) + (mon->frame_width >> 1); + y = (ny * mon->frame_height / h) + (mon->frame_height >> 1); + mon->touch_x = x - mon->frame_inner_x; + mon->touch_y = y - mon->frame_inner_y; + } + if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) { + mon->touched = true; + } + if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) { + mon->touched = false; + } + } + } else if (event.type == SDL_WINDOWEVENT) { + auto monitor = getMonitorByWindowID(event.window.windowID); + if (monitor) { + if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + int mw, mh; + SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh); + if (monitor->frame_rotation & 1) { + std::swap(mw, mh); + } + monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f; + monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f; + monitor->panel->sdl_invalidate(); + } else if (event.window.event == SDL_WINDOWEVENT_CLOSE) { + monitor->closing = true; + } + } + } else if (event.type == SDL_QUIT) { + for (auto &m : _list_monitor) { + m->closing = true; + } + } } - } } /// デバッガでステップ実行されていることを検出するスレッド用関数。 -static int detectDebugger(bool *running) { - uint32_t prev_ms = SDL_GetTicks(); - do { - SDL_Delay(1); - uint32_t ms = SDL_GetTicks(); - /// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。 - /// また、解除されたと判断した後も1023msecほど状態を維持する。 - if (ms - prev_ms > 64) { - _in_step_exec = _msec_step_exec; - } else if (_in_step_exec) { - --_in_step_exec; +static int detectDebugger(bool *running) +{ + uint32_t prev_ms = SDL_GetTicks(); + do { + SDL_Delay(1); + uint32_t ms = SDL_GetTicks(); + /// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。 + /// また、解除されたと判断した後も1023msecほど状態を維持する。 + if (ms - prev_ms > 64) { + _in_step_exec = _msec_step_exec; + } else if (_in_step_exec) { + --_in_step_exec; + } + prev_ms = ms; + } while (*running); + return 0; +} + +void Panel_sdl::_update_proc(void) +{ + for (auto it = _list_monitor.begin(); it != _list_monitor.end();) { + if ((*it)->closing) { + if ((*it)->texture_frameimage) { + SDL_DestroyTexture((*it)->texture_frameimage); + } + SDL_DestroyTexture((*it)->texture); + SDL_DestroyRenderer((*it)->renderer); + SDL_DestroyWindow((*it)->window); + _list_monitor.erase(it++); + if (_list_monitor.empty()) { + _all_close = true; + return; + } + continue; + } + (*it)->panel->sdl_update(); + ++it; } - prev_ms = ms; - } while (*running); - return 0; } -void Panel_sdl::_update_proc(void) { - for (auto it = _list_monitor.begin(); it != _list_monitor.end();) { - if ((*it)->closing) { - if ((*it)->texture_frameimage) { - SDL_DestroyTexture((*it)->texture_frameimage); - } - SDL_DestroyTexture((*it)->texture); - SDL_DestroyRenderer((*it)->renderer); - SDL_DestroyWindow((*it)->window); - _list_monitor.erase(it++); - if (_list_monitor.empty()) { - _all_close = true; - return; - } - continue; +int Panel_sdl::setup(void) +{ + if (_inited) + return 1; + _inited = true; + + /// Add default keycode mapping + /// M5StackのBtnA~BtnCのエミュレート; + addKeyCodeMapping(SDLK_LEFT, 39); + addKeyCodeMapping(SDLK_DOWN, 38); + addKeyCodeMapping(SDLK_RIGHT, 37); + addKeyCodeMapping(SDLK_UP, 36); + + SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited); + + _update_in_semaphore = SDL_CreateSemaphore(0); + _update_out_semaphore = SDL_CreateSemaphore(0); + for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) { + gpio_hi(pin); } - (*it)->panel->sdl_update(); - ++it; - } + /*Initialize the SDL*/ + SDL_Init(SDL_INIT_VIDEO); + SDL_StartTextInput(); + + // SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH); + return 0; } -int Panel_sdl::setup(void) { - if (_inited) - return 1; - _inited = true; +int Panel_sdl::loop(void) +{ + if (!_inited) + return 1; - /// Add default keycode mapping - /// M5StackのBtnA~BtnCのエミュレート; - addKeyCodeMapping(SDLK_LEFT, 39); - addKeyCodeMapping(SDLK_DOWN, 38); - addKeyCodeMapping(SDLK_RIGHT, 37); - addKeyCodeMapping(SDLK_UP, 36); - - SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited); - - _update_in_semaphore = SDL_CreateSemaphore(0); - _update_out_semaphore = SDL_CreateSemaphore(0); - for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) { - gpio_hi(pin); - } - /*Initialize the SDL*/ - SDL_Init(SDL_INIT_VIDEO); - SDL_StartTextInput(); - - // SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH); - return 0; -} - -int Panel_sdl::loop(void) { - if (!_inited) - return 1; - - _event_proc(); - SDL_SemWaitTimeout(_update_in_semaphore, 1); - _update_proc(); - _event_proc(); - if (SDL_SemValue(_update_out_semaphore) == 0) { - SDL_SemPost(_update_out_semaphore); - } - - return _all_close; -} - -int Panel_sdl::close(void) { - if (!_inited) - return 1; - _inited = false; - - SDL_StopTextInput(); - SDL_DestroySemaphore(_update_in_semaphore); - SDL_DestroySemaphore(_update_out_semaphore); - SDL_Quit(); - return 0; -} - -int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec) { - _msec_step_exec = msec_step_exec; - - /// SDLの準備 - if (0 != Panel_sdl::setup()) { - return 1; - } - - /// ユーザコード関数の動作・停止フラグ - bool running = true; - - /// ユーザコード関数を起動する - auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running); - - /// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続 - while (0 == Panel_sdl::loop()) { - }; - - /// ユーザコード関数を終了する - running = false; - SDL_WaitThread(thread, nullptr); - - /// SDLを終了する - return Panel_sdl::close(); -} - -void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y) { - monitor.scaling_x = scaling_x; - monitor.scaling_y = scaling_y; -} - -void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y) { - monitor.frame_image = frame_image; - monitor.frame_width = frame_width; - monitor.frame_height = frame_height; - monitor.frame_inner_x = inner_x; - monitor.frame_inner_y = inner_y; -} - -void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation) { - monitor.frame_rotation = frame_rotation; - monitor.frame_angle = (monitor.frame_rotation) * 90; -} - -Panel_sdl::~Panel_sdl(void) { - _list_monitor.remove(&monitor); - SDL_DestroyMutex(_sdl_mutex); -} - -Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase() { - _sdl_mutex = SDL_CreateMutex(); - _auto_display = true; - monitor.panel = this; -} - -bool Panel_sdl::init(bool use_reset) { - initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height); - bool res = Panel_FrameBufferBase::init(use_reset); - - _list_monitor.push_back(&monitor); - - return res; -} - -color_depth_t Panel_sdl::setColorDepth(color_depth_t depth) { - auto bits = depth & color_depth_t::bit_mask; - if (bits >= 16) { - depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte; - } else { - depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte; - } - _write_depth = depth; - _read_depth = depth; - - return depth; -} - -Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent} { SDL_LockMutex(parent->_sdl_mutex); }; - -Panel_sdl::lock_t::~lock_t(void) { - ++_parent->_modified_counter; - SDL_UnlockMutex(_parent->_sdl_mutex); - if (SDL_SemValue(_update_in_semaphore) < 2) { - SDL_SemPost(_update_in_semaphore); - if (!_in_step_exec) { - SDL_SemWaitTimeout(_update_out_semaphore, 1); + _event_proc(); + SDL_SemWaitTimeout(_update_in_semaphore, 1); + _update_proc(); + _event_proc(); + if (SDL_SemValue(_update_out_semaphore) == 0) { + SDL_SemPost(_update_out_semaphore); } - } + + return _all_close; +} + +int Panel_sdl::close(void) +{ + if (!_inited) + return 1; + _inited = false; + + SDL_StopTextInput(); + SDL_DestroySemaphore(_update_in_semaphore); + SDL_DestroySemaphore(_update_out_semaphore); + SDL_Quit(); + return 0; +} + +int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec) +{ + _msec_step_exec = msec_step_exec; + + /// SDLの準備 + if (0 != Panel_sdl::setup()) { + return 1; + } + + /// ユーザコード関数の動作・停止フラグ + bool running = true; + + /// ユーザコード関数を起動する + auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running); + + /// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続 + while (0 == Panel_sdl::loop()) { + }; + + /// ユーザコード関数を終了する + running = false; + SDL_WaitThread(thread, nullptr); + + /// SDLを終了する + return Panel_sdl::close(); +} + +void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y) +{ + monitor.scaling_x = scaling_x; + monitor.scaling_y = scaling_y; +} + +void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y) +{ + monitor.frame_image = frame_image; + monitor.frame_width = frame_width; + monitor.frame_height = frame_height; + monitor.frame_inner_x = inner_x; + monitor.frame_inner_y = inner_y; +} + +void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation) +{ + monitor.frame_rotation = frame_rotation; + monitor.frame_angle = (monitor.frame_rotation) * 90; +} + +Panel_sdl::~Panel_sdl(void) +{ + _list_monitor.remove(&monitor); + SDL_DestroyMutex(_sdl_mutex); +} + +Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase() +{ + _sdl_mutex = SDL_CreateMutex(); + _auto_display = true; + monitor.panel = this; +} + +bool Panel_sdl::init(bool use_reset) +{ + initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height); + bool res = Panel_FrameBufferBase::init(use_reset); + + _list_monitor.push_back(&monitor); + + return res; +} + +color_depth_t Panel_sdl::setColorDepth(color_depth_t depth) +{ + auto bits = depth & color_depth_t::bit_mask; + if (bits >= 16) { + depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte; + } else { + depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte; + } + _write_depth = depth; + _read_depth = depth; + + return depth; +} + +Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent} +{ + SDL_LockMutex(parent->_sdl_mutex); }; -void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) { - lock_t lock(this); - Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor); -} - -void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) { - lock_t lock(this); - Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor); -} - -void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length) { - // lock_t lock(this); - Panel_FrameBufferBase::writeBlock(rawcolor, length); -} - -void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) { - lock_t lock(this); - Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma); -} - -void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) { - lock_t lock(this); - Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param); -} - -void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) { - lock_t lock(this); - Panel_FrameBufferBase::writePixels(param, len, use_dma); -} - -void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) { - (void)x; - (void)y; - (void)w; - (void)h; - if (_in_step_exec) { - if (_display_counter != _modified_counter) { - do { +Panel_sdl::lock_t::~lock_t(void) +{ + ++_parent->_modified_counter; + SDL_UnlockMutex(_parent->_sdl_mutex); + if (SDL_SemValue(_update_in_semaphore) < 2) { SDL_SemPost(_update_in_semaphore); - SDL_SemWaitTimeout(_update_out_semaphore, 1); - } while (_display_counter != _modified_counter); - SDL_Delay(1); + if (!_in_step_exec) { + SDL_SemWaitTimeout(_update_out_semaphore, 1); + } } - } +}; + +void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) +{ + lock_t lock(this); + Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor); } -uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count) { - (void)count; - tp->x = monitor.touch_x; - tp->y = monitor.touch_y; - tp->size = monitor.touched ? 1 : 0; - tp->id = 0; - return monitor.touched; +void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) +{ + lock_t lock(this); + Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor); } -void Panel_sdl::setWindowTitle(const char *title) { - _window_title = title; - if (monitor.window) { - SDL_SetWindowTitle(monitor.window, _window_title); - } +void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length) +{ + // lock_t lock(this); + Panel_FrameBufferBase::writeBlock(rawcolor, length); } -void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy) { - mon->scaling_x = sx; - mon->scaling_y = sy; - int nw = mon->frame_width; - int nh = mon->frame_height; - if (mon->frame_rotation & 1) { - std::swap(nw, nh); - } - - int x, y, w, h; - int rw, rh; - SDL_GetRendererOutputSize(mon->renderer, &rw, &rh); - SDL_GetWindowSize(mon->window, &w, &h); - nw = nw * sx * w / rw; - nh = nh * sy * h / rh; - SDL_GetWindowPosition(mon->window, &x, &y); - SDL_SetWindowSize(mon->window, nw, nh); - SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2); - mon->panel->sdl_invalidate(); +void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) +{ + lock_t lock(this); + Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma); } -void Panel_sdl::sdl_create(monitor_t *m) { - int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; +void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) +{ + lock_t lock(this); + Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param); +} + +void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) +{ + lock_t lock(this); + Panel_FrameBufferBase::writePixels(param, len, use_dma); +} + +void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) +{ + (void)x; + (void)y; + (void)w; + (void)h; + if (_in_step_exec) { + if (_display_counter != _modified_counter) { + do { + SDL_SemPost(_update_in_semaphore); + SDL_SemWaitTimeout(_update_out_semaphore, 1); + } while (_display_counter != _modified_counter); + SDL_Delay(1); + } + } +} + +uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count) +{ + (void)count; + tp->x = monitor.touch_x; + tp->y = monitor.touch_y; + tp->size = monitor.touched ? 1 : 0; + tp->id = 0; + return monitor.touched; +} + +void Panel_sdl::setWindowTitle(const char *title) +{ + _window_title = title; + if (monitor.window) { + SDL_SetWindowTitle(monitor.window, _window_title); + } +} + +void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy) +{ + mon->scaling_x = sx; + mon->scaling_y = sy; + int nw = mon->frame_width; + int nh = mon->frame_height; + if (mon->frame_rotation & 1) { + std::swap(nw, nh); + } + + int x, y, w, h; + int rw, rh; + SDL_GetRendererOutputSize(mon->renderer, &rw, &rh); + SDL_GetWindowSize(mon->window, &w, &h); + nw = nw * sx * w / rw; + nh = nh * sy * h / rh; + SDL_GetWindowPosition(mon->window, &x, &y); + SDL_SetWindowSize(mon->window, nw, nh); + SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2); + mon->panel->sdl_invalidate(); +} + +void Panel_sdl::sdl_create(monitor_t *m) +{ + int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; #if SDL_FULLSCREEN - flag |= SDL_WINDOW_FULLSCREEN; + flag |= SDL_WINDOW_FULLSCREEN; #endif - if (m->frame_width < _cfg.panel_width) { - m->frame_width = _cfg.panel_width; - } - if (m->frame_height < _cfg.panel_height) { - m->frame_height = _cfg.panel_height; - } - - int window_width = m->frame_width * m->scaling_x; - int window_height = m->frame_height * m->scaling_y; - int scaling_x = m->scaling_x; - int scaling_y = m->scaling_y; - if (m->frame_rotation & 1) { - std::swap(window_width, window_height); - std::swap(scaling_x, scaling_y); - } - - { - m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, - flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/ - } - m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); - m->texture = SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height); - SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE); - - if (m->frame_image) { - // 枠画像用のサーフェイスを作成 - auto sf = - SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4, 0xFF000000, 0xFF0000, 0xFF00, 0xFF); - if (sf != nullptr) { - // 枠画像からテクスチャを作成 - m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf); - SDL_FreeSurface(sf); + if (m->frame_width < _cfg.panel_width) { + m->frame_width = _cfg.panel_width; } - } - SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND); - _update_scaling(m, scaling_x, scaling_y); -} - -void Panel_sdl::sdl_update(void) { - if (monitor.renderer == nullptr) { - sdl_create(&monitor); - } - - bool step_exec = _in_step_exec; - - if (_texupdate_counter != _modified_counter) { - pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false); - if (_write_depth == rgb565_2Byte) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } else if (_write_depth == rgb888_3Byte) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } else if (_write_depth == rgb332_1Byte) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; - } else if (_write_depth == grayscale_8bit) { - pc.fp_copy = pixelcopy_t::copy_rgb_fast; + if (m->frame_height < _cfg.panel_height) { + m->frame_height = _cfg.panel_height; } - if (0 == SDL_LockMutex(_sdl_mutex)) { - _texupdate_counter = _modified_counter; - for (int y = 0; y < _cfg.panel_height; ++y) { - pc.src_x32 = 0; - pc.src_data = _lines_buffer[y]; - pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc); - } - SDL_UnlockMutex(_sdl_mutex); - SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t)); + int window_width = m->frame_width * m->scaling_x; + int window_height = m->frame_height * m->scaling_y; + int scaling_x = m->scaling_x; + int scaling_y = m->scaling_y; + if (m->frame_rotation & 1) { + std::swap(window_width, window_height); + std::swap(scaling_x, scaling_y); } - } - int angle = monitor.frame_angle; - int target = (monitor.frame_rotation) * 90; - angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3); - - if (monitor.frame_angle != angle) { // 表示する向きを変える - monitor.frame_angle = angle; - sdl_invalidate(); - } else if (monitor.frame_rotation & ~3u) { - monitor.frame_rotation &= 3; - monitor.frame_angle = (monitor.frame_rotation) * 90; - sdl_invalidate(); - } - - if (_invalidated || (_display_counter != _texupdate_counter)) { - SDL_RendererInfo info; - if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) { - // ステップ実行中はVSYNCを待機しない - if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) { - SDL_RenderSetVSync(monitor.renderer, !step_exec); - } - } { - int red = 0; - int green = 0; - int blue = 0; + m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, + flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/ + } + m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + m->texture = + SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height); + SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE); + + if (m->frame_image) { + // 枠画像用のサーフェイスを作成 + auto sf = SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4, + 0xFF000000, 0xFF0000, 0xFF00, 0xFF); + if (sf != nullptr) { + // 枠画像からテクスチャを作成 + m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf); + SDL_FreeSurface(sf); + } + } + SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND); + _update_scaling(m, scaling_x, scaling_y); +} + +void Panel_sdl::sdl_update(void) +{ + if (monitor.renderer == nullptr) { + sdl_create(&monitor); + } + + bool step_exec = _in_step_exec; + + if (_texupdate_counter != _modified_counter) { + pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false); + if (_write_depth == rgb565_2Byte) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } else if (_write_depth == rgb888_3Byte) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } else if (_write_depth == rgb332_1Byte) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } else if (_write_depth == grayscale_8bit) { + pc.fp_copy = pixelcopy_t::copy_rgb_fast; + } + + if (0 == SDL_LockMutex(_sdl_mutex)) { + _texupdate_counter = _modified_counter; + for (int y = 0; y < _cfg.panel_height; ++y) { + pc.src_x32 = 0; + pc.src_data = _lines_buffer[y]; + pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc); + } + SDL_UnlockMutex(_sdl_mutex); + SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t)); + } + } + + int angle = monitor.frame_angle; + int target = (monitor.frame_rotation) * 90; + angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3); + + if (monitor.frame_angle != angle) { // 表示する向きを変える + monitor.frame_angle = angle; + sdl_invalidate(); + } else if (monitor.frame_rotation & ~3u) { + monitor.frame_rotation &= 3; + monitor.frame_angle = (monitor.frame_rotation) * 90; + sdl_invalidate(); + } + + if (_invalidated || (_display_counter != _texupdate_counter)) { + SDL_RendererInfo info; + if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) { + // ステップ実行中はVSYNCを待機しない + if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) { + SDL_RenderSetVSync(monitor.renderer, !step_exec); + } + } + { + int red = 0; + int green = 0; + int blue = 0; #if defined(M5GFX_BACK_COLOR) - red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF; - green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF; - blue = ((M5GFX_BACK_COLOR)) & 0xFF; + red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF; + green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF; + blue = ((M5GFX_BACK_COLOR)) & 0xFF; #endif - SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF); + SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF); + } + SDL_RenderClear(monitor.renderer); + if (_invalidated) { + _invalidated = false; + int mw, mh; + SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); + } + render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); + render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); + SDL_RenderPresent(monitor.renderer); + _display_counter = _texupdate_counter; + if (_invalidated) { + _invalidated = false; + SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF); + SDL_RenderClear(monitor.renderer); + render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, + angle); + render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); + SDL_RenderPresent(monitor.renderer); + } } - SDL_RenderClear(monitor.renderer); - if (_invalidated) { - _invalidated = false; - int mw, mh; - SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); - } - render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); - render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); - SDL_RenderPresent(monitor.renderer); - _display_counter = _texupdate_counter; - if (_invalidated) { - _invalidated = false; - SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF); - SDL_RenderClear(monitor.renderer); - render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); - render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); - SDL_RenderPresent(monitor.renderer); - } - } } -void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle) { - SDL_Point pivot; - pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x; - pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y; - SDL_Rect dstrect; - dstrect.w = tw * monitor.scaling_x; - dstrect.h = th * monitor.scaling_y; - int mw, mh; - SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); - dstrect.x = mw / 2.0f - pivot.x; - dstrect.y = mh / 2.0f - pivot.y; - SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE); +void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle) +{ + SDL_Point pivot; + pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x; + pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y; + SDL_Rect dstrect; + dstrect.w = tw * monitor.scaling_x; + dstrect.h = th * monitor.scaling_y; + int mw, mh; + SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); + dstrect.x = mw / 2.0f - pivot.x; + dstrect.y = mh / 2.0f - pivot.y; + SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE); } -bool Panel_sdl::initFrameBuffer(size_t width, size_t height) { - uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *)); - if (nullptr == lineArray) { - return false; - } - - _texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t)); - - /// 8byte alignment; - width = (width + 7) & ~7u; - - _lines_buffer = lineArray; - memset(lineArray, 0, height * sizeof(uint8_t *)); - - uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16); - - auto fb = framebuffer; - { - for (size_t y = 0; y < height; ++y) { - lineArray[y] = fb; - fb += width; +bool Panel_sdl::initFrameBuffer(size_t width, size_t height) +{ + uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *)); + if (nullptr == lineArray) { + return false; } - } - return true; + + _texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t)); + + /// 8byte alignment; + width = (width + 7) & ~7u; + + _lines_buffer = lineArray; + memset(lineArray, 0, height * sizeof(uint8_t *)); + + uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16); + + auto fb = framebuffer; + { + for (size_t y = 0; y < height; ++y) { + lineArray[y] = fb; + fb += width; + } + } + return true; } -void Panel_sdl::deinitFrameBuffer(void) { - auto lines = _lines_buffer; - _lines_buffer = nullptr; - if (lines != nullptr) { - heap_free(lines[0]); - heap_free(lines); - } - if (_texturebuf) { - heap_free(_texturebuf); - _texturebuf = nullptr; - } +void Panel_sdl::deinitFrameBuffer(void) +{ + auto lines = _lines_buffer; + _lines_buffer = nullptr; + if (lines != nullptr) { + heap_free(lines[0]); + heap_free(lines); + } + if (_texturebuf) { + heap_free(_texturebuf); + _texturebuf = nullptr; + } } //---------------------------------------------------------------------------- diff --git a/src/graphics/Panel_sdl.hpp b/src/graphics/Panel_sdl.hpp index 0d72ad26e..802c6c5dc 100644 --- a/src/graphics/Panel_sdl.hpp +++ b/src/graphics/Panel_sdl.hpp @@ -36,126 +36,129 @@ Porting for SDL: #include "lgfx/v1/panel/Panel_FrameBufferBase.hpp" #include -namespace lgfx { -inline namespace v1 { +namespace lgfx +{ +inline namespace v1 +{ struct Panel_sdl; struct monitor_t { - SDL_Window *window = nullptr; - SDL_Renderer *renderer = nullptr; - SDL_Texture *texture = nullptr; - SDL_Texture *texture_frameimage = nullptr; - Panel_sdl *panel = nullptr; + SDL_Window *window = nullptr; + SDL_Renderer *renderer = nullptr; + SDL_Texture *texture = nullptr; + SDL_Texture *texture_frameimage = nullptr; + Panel_sdl *panel = nullptr; - // 外枠 - const void *frame_image = 0; - uint_fast16_t frame_width = 0; - uint_fast16_t frame_height = 0; - uint_fast16_t frame_inner_x = 0; - uint_fast16_t frame_inner_y = 0; - int_fast16_t frame_rotation = 0; - int_fast16_t frame_angle = 0; + // 外枠 + const void *frame_image = 0; + uint_fast16_t frame_width = 0; + uint_fast16_t frame_height = 0; + uint_fast16_t frame_inner_x = 0; + uint_fast16_t frame_inner_y = 0; + int_fast16_t frame_rotation = 0; + int_fast16_t frame_angle = 0; - float scaling_x = 1; - float scaling_y = 1; - int_fast16_t touch_x, touch_y; - bool touched = false; - bool closing = false; + float scaling_x = 1; + float scaling_y = 1; + int_fast16_t touch_x, touch_y; + bool touched = false; + bool closing = false; }; //---------------------------------------------------------------------------- struct Touch_sdl : public ITouch { - bool init(void) override { return true; } - void wakeup(void) override {} - void sleep(void) override {} - bool isEnable(void) override { return true; }; - uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; } + bool init(void) override { return true; } + void wakeup(void) override {} + void sleep(void) override {} + bool isEnable(void) override { return true; }; + uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; } }; //---------------------------------------------------------------------------- struct Panel_sdl : public Panel_FrameBufferBase { - static constexpr size_t EMULATED_GPIO_MAX = 128; - static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX]; + static constexpr size_t EMULATED_GPIO_MAX = 128; + static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX]; -public: - Panel_sdl(void); - virtual ~Panel_sdl(void); + public: + Panel_sdl(void); + virtual ~Panel_sdl(void); - bool init(bool use_reset) override; + bool init(bool use_reset) override; - color_depth_t setColorDepth(color_depth_t depth) override; + color_depth_t setColorDepth(color_depth_t depth) override; - void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; + void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; - // void setInvert(bool invert) override {} - void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override; - void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override; - void writeBlock(uint32_t rawcolor, uint32_t length) override; - void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) override; - void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override; - void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override; + // void setInvert(bool invert) override {} + void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override; + void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override; + void writeBlock(uint32_t rawcolor, uint32_t length) override; + void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, + bool use_dma) override; + void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override; + void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override; - uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override; + uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override; - void setWindowTitle(const char *title); - void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y); - void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y); - void setFrameRotation(uint_fast16_t frame_rotaion); - void setBrightness(uint8_t brightness) override{}; + void setWindowTitle(const char *title); + void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y); + void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y); + void setFrameRotation(uint_fast16_t frame_rotaion); + void setBrightness(uint8_t brightness) override{}; - static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; } - static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; } - static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; } + static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; } + static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; } + static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; } - static int setup(void); - static int loop(void); - static int close(void); + static int setup(void); + static int loop(void); + static int close(void); - static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512); + static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512); - static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; } + static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; } - struct KeyCodeMapping_t { - SDL_KeyCode keycode = SDLK_UNKNOWN; - uint8_t gpio = 0; - }; - static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio); - static int getKeyCodeMapping(SDL_KeyCode keyCode); - -protected: - const char *_window_title = "LGFX Simulator"; - SDL_mutex *_sdl_mutex = nullptr; - - void sdl_create(monitor_t *m); - void sdl_update(void); - - touch_point_t _touch_point; - monitor_t monitor; - - rgb888_t *_texturebuf = nullptr; - uint_fast16_t _modified_counter; - uint_fast16_t _texupdate_counter; - uint_fast16_t _display_counter; - bool _invalidated; - - static void _event_proc(void); - static void _update_proc(void); - static void _update_scaling(monitor_t *m, float sx, float sy); - void sdl_invalidate(void) { _invalidated = true; } - void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle); - bool initFrameBuffer(size_t width, size_t height); - void deinitFrameBuffer(void); - - static SDL_Keymod _keymod; - - struct lock_t { - lock_t(Panel_sdl *parent); - ~lock_t(); + struct KeyCodeMapping_t { + SDL_KeyCode keycode = SDLK_UNKNOWN; + uint8_t gpio = 0; + }; + static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio); + static int getKeyCodeMapping(SDL_KeyCode keyCode); protected: - Panel_sdl *_parent; - }; + const char *_window_title = "LGFX Simulator"; + SDL_mutex *_sdl_mutex = nullptr; + + void sdl_create(monitor_t *m); + void sdl_update(void); + + touch_point_t _touch_point; + monitor_t monitor; + + rgb888_t *_texturebuf = nullptr; + uint_fast16_t _modified_counter; + uint_fast16_t _texupdate_counter; + uint_fast16_t _display_counter; + bool _invalidated; + + static void _event_proc(void); + static void _update_proc(void); + static void _update_scaling(monitor_t *m, float sx, float sy); + void sdl_invalidate(void) { _invalidated = true; } + void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle); + bool initFrameBuffer(size_t width, size_t height); + void deinitFrameBuffer(void); + + static SDL_Keymod _keymod; + + struct lock_t { + lock_t(Panel_sdl *parent); + ~lock_t(); + + protected: + Panel_sdl *_parent; + }; }; //---------------------------------------------------------------------------- } // namespace v1 diff --git a/src/graphics/PointStruct.h b/src/graphics/PointStruct.h index 890f255fa..218731978 100644 --- a/src/graphics/PointStruct.h +++ b/src/graphics/PointStruct.h @@ -1,4 +1,4 @@ struct PointStruct { - int x; - int y; + int x; + int y; }; \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 32d0aa2e0..0012aeb5d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -92,7 +92,8 @@ uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); using namespace meshtastic; /** @todo remove */ -namespace graphics { +namespace graphics +{ // This means the *visible* area (sh1106 can address 132, but shows 128 for example) #define IDLE_FRAMERATE 1 // in fps @@ -139,117 +140,126 @@ extern bool hasUnreadMessage; // Displays a temporary centered banner message (e.g., warning, status, etc.) // The banner appears in the center of the screen and disappears after the specified duration -void Screen::showSimpleBanner(const char *message, uint32_t durationMs) { - BannerOverlayOptions options; - options.message = message; - options.durationMs = durationMs; - options.notificationType = notificationTypeEnum::text_banner; - showOverlayBanner(options); +void Screen::showSimpleBanner(const char *message, uint32_t durationMs) +{ + BannerOverlayOptions options; + options.message = message; + options.durationMs = durationMs; + options.notificationType = notificationTypeEnum::text_banner; + showOverlayBanner(options); } // Called to trigger a banner with custom message and duration -void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) { +void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) +{ #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif - // Store the message and set the expiration timestamp - strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination - NotificationRenderer::alertBannerUntil = (banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs; - NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr; - NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr; - NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount; - NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback; - NotificationRenderer::curSelected = banner_overlay_options.InitialSelected; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = + (banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs; + NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr; + NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr; + NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount; + NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback; + NotificationRenderer::curSelected = banner_overlay_options.InitialSelected; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } // Called to trigger a banner with custom message and duration -void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback) { +void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback) +{ #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif - nodeDB->pause_sort(true); - // Store the message and set the expiration timestamp - strncpy(NotificationRenderer::alertBannerMessage, message, 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::alertBannerCallback = bannerCallback; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::curSelected = 0; - NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; + nodeDB->pause_sort(true); + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; + NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } // Called to trigger a banner with custom message and duration -void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback) { +void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, + std::function bannerCallback) +{ #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif - // Store the message and set the expiration timestamp - strncpy(NotificationRenderer::alertBannerMessage, message, 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::alertBannerCallback = bannerCallback; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::curSelected = 0; - NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker; - NotificationRenderer::numDigits = digits; - NotificationRenderer::currentNumber = 0; + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::curSelected = 0; + NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker; + NotificationRenderer::numDigits = digits; + NotificationRenderer::currentNumber = 0; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } -void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, std::function textCallback) { - LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); +void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback) +{ + LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - // Start OnScreenKeyboardModule session (non-touch variant) - OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); - NotificationRenderer::textInputCallback = textCallback; + // Start OnScreenKeyboardModule session (non-touch variant) + OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); + NotificationRenderer::textInputCallback = textCallback; - // Store the message and set the expiration timestamp (use same pattern as other notifications) - strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; + // Store the message and set the expiration timestamp (use same pattern as other notifications) + strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; - // Set the overlay using the same pattern as other notification types - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); + // Set the overlay using the same pattern as other notification types + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); } -static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - uint8_t module_frame; - // there's a little but in the UI transition code - // where it invokes the function at the correct offset - // in the array of "drawScreen" functions; however, - // the passed-state doesn't quite reflect the "current" - // screen, so we have to detect it. - if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) { - // if we're transitioning from the end of the frame list back around to the first - // frame, then we want this to be `0` - module_frame = state->transitionFrameTarget; - } else { - // otherwise, just display the module frame that's aligned with the current frame - module_frame = state->currentFrame; - } - MeshModule &pi = *moduleFrames.at(module_frame); - pi.drawFrame(display, state, x, y); +static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + uint8_t module_frame; + // there's a little but in the UI transition code + // where it invokes the function at the correct offset + // in the array of "drawScreen" functions; however, + // the passed-state doesn't quite reflect the "current" + // screen, so we have to detect it. + if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) { + // if we're transitioning from the end of the frame list back around to the first + // frame, then we want this to be `0` + module_frame = state->transitionFrameTarget; + } else { + // otherwise, just display the module frame that's aligned with the current frame + module_frame = state->currentFrame; + } + MeshModule &pi = *moduleFrames.at(module_frame); + pi.drawFrame(display, state, x, y); } /** @@ -258,27 +268,28 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int * We keep a series of "after you've gone 10 meters, what is your heading since * the last reference point?" */ -float Screen::estimatedHeading(double lat, double lon) { - static double oldLat, oldLon; - static float b; +float Screen::estimatedHeading(double lat, double lon) +{ + static double oldLat, oldLon; + static float b; - if (oldLat == 0) { - // just prepare for next time + if (oldLat == 0) { + // just prepare for next time + oldLat = lat; + oldLon = lon; + + return b; + } + + float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon); + if (d < 10) // haven't moved enough, just keep current bearing + return b; + + b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; oldLat = lat; oldLon = lon; return b; - } - - float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon); - if (d < 10) // haven't moved enough, just keep current bearing - return b; - - b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; - oldLat = lat; - oldLon = lon; - - return b; } /// We will skip one node - the one for us, so we just blindly loop over all @@ -293,1447 +304,1495 @@ SPIClass SPI1(HSPI); #endif Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry) - : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) { - graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; + : concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32) +{ + graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; - int32_t rawRGB = uiconfig.screen_rgb_color; + int32_t rawRGB = uiconfig.screen_rgb_color; - // Only validate the combined value once - if (rawRGB > 0 && rawRGB <= 255255255) { - // Extract each component as a normal int first - int r = (rawRGB >> 16) & 0xFF; - int g = (rawRGB >> 8) & 0xFF; - int b = rawRGB & 0xFF; - if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { - TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); + // Only validate the combined value once + if (rawRGB > 0 && rawRGB <= 255255255) { + // Extract each component as a normal int first + int r = (rawRGB >> 16) & 0xFF; + int g = (rawRGB >> 8) & 0xFF; + int b = rawRGB & 0xFF; + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); + } } - } #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) - dispdev = new SH1106Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new SH1106Wire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_ST7789) #ifdef ESP_PLATFORM - dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7789_SDA, ST7789_MISO, ST7789_SCK); + dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7789_SDA, + ST7789_MISO, ST7789_SCK); #else - dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif #elif defined(USE_ST7796) #ifdef ESP_PLATFORM - dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA, ST7796_MISO, ST7796_SCK, - TFT_SPI_FREQUENCY); + dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA, + ST7796_MISO, ST7796_SCK, TFT_SPI_FREQUENCY); #else - dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif #elif defined(USE_SSD1306) - dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new SSD1306Wire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_SPISSD1306) - dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); - if (!dispdev->init()) { - LOG_DEBUG("Error: SSD1306 not detected!"); - } else { - static_cast(dispdev)->setHorizontalOffset(32); - LOG_INFO("SSD1306 init success"); - } -#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || defined(RAK14014) || \ - defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) - dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) - dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) - dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_ST7567) - dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (portduino_config.displayPanel != no_screen) { - LOG_DEBUG("Make TFTDisplay!"); - dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); + if (!dispdev->init()) { + LOG_DEBUG("Error: SSD1306 not detected!"); } else { - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; + static_cast(dispdev)->setHorizontalOffset(32); + LOG_INFO("SSD1306 init success"); + } +#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) + dispdev = new TFTDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) + dispdev = new EInkDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_ST7567) + dispdev = new ST7567Wire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (portduino_config.displayPanel != no_screen) { + LOG_DEBUG("Make TFTDisplay!"); + dispdev = new TFTDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + } else { + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; + } } - } #else - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; #endif #if defined(USE_ST7789) - static_cast(dispdev)->setRGB(TFT_MESH); + static_cast(dispdev)->setRGB(TFT_MESH); #elif defined(USE_ST7796) - static_cast(dispdev)->setRGB(TFT_MESH); + static_cast(dispdev)->setRGB(TFT_MESH); #endif - ui = new OLEDDisplayUi(dispdev); - cmdQueue.setReader(this); + ui = new OLEDDisplayUi(dispdev); + cmdQueue.setReader(this); } -Screen::~Screen() { delete[] graphics::normalFrames; } +Screen::~Screen() +{ + delete[] graphics::normalFrames; +} /** * Prepare the display for the unit going to the lowest power mode possible. Most screens will just * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code */ -void Screen::doDeepSleep() { +void Screen::doDeepSleep() +{ #ifdef USE_EINK - setOn(false, graphics::UIRenderer::drawDeepSleepFrame); + setOn(false, graphics::UIRenderer::drawDeepSleepFrame); #else - // Without E-Ink display: - setOn(false); + // Without E-Ink display: + setOn(false); #endif } -void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) { - if (!useDisplay) - return; +void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) +{ + if (!useDisplay) + return; - if (on != screenOn) { - if (on) { - LOG_INFO("Turn on screen"); - powerMon->setState(meshtastic_PowerMon_State_Screen_On); + if (on != screenOn) { + if (on) { + LOG_INFO("Turn on screen"); + powerMon->setState(meshtastic_PowerMon_State_Screen_On); #ifdef T_WATCH_S3 - PMU->enablePowerOutput(XPOWERS_ALDO2); + PMU->enablePowerOutput(XPOWERS_ALDO2); #endif #if defined(MUZI_BASE) - dispdev->init(); - dispdev->setBrightness(brightness); - dispdev->flipScreenVertically(); - dispdev->resetDisplay(); - digitalWrite(SCREEN_12V_ENABLE, HIGH); - delay(100); + dispdev->init(); + dispdev->setBrightness(brightness); + dispdev->flipScreenVertically(); + dispdev->resetDisplay(); + digitalWrite(SCREEN_12V_ENABLE, HIGH); + delay(100); #endif #if !ARCH_PORTDUINO - dispdev->displayOn(); + dispdev->displayOn(); #endif #ifdef PIN_EINK_EN - if (uiconfig.screen_brightness == 1) - digitalWrite(PIN_EINK_EN, HIGH); + if (uiconfig.screen_brightness == 1) + digitalWrite(PIN_EINK_EN, HIGH); #elif defined(PCA_PIN_EINK_EN) - if (uiconfig.screen_brightness > 0) - io.digitalWrite(PCA_PIN_EINK_EN, HIGH); + if (uiconfig.screen_brightness > 0) + io.digitalWrite(PCA_PIN_EINK_EN, HIGH); #endif -#if defined(ST7789_CS) && !defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here. - static_cast(dispdev)->setDisplayBrightness(brightness); +#if defined(ST7789_CS) && \ + !defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here. + static_cast(dispdev)->setDisplayBrightness(brightness); #endif - dispdev->displayOn(); + dispdev->displayOn(); #if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) - ui->init(); + ui->init(); #endif #ifdef USE_ST7789 - pinMode(VTFT_CTRL, OUTPUT); - digitalWrite(VTFT_CTRL, LOW); - ui->init(); + pinMode(VTFT_CTRL, OUTPUT); + digitalWrite(VTFT_CTRL, LOW); + ui->init(); #ifdef ESP_PLATFORM - analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); + analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); #else - pinMode(VTFT_LEDA, OUTPUT); - digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); #endif #endif #ifdef USE_ST7796 - ui->init(); + ui->init(); #ifdef ESP_PLATFORM - analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); + analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); #else - pinMode(VTFT_LEDA, OUTPUT); - digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); #endif #endif - enabled = true; - setInterval(0); // Draw ASAP - runASAP = true; - } else { - powerMon->clearState(meshtastic_PowerMon_State_Screen_On); + enabled = true; + setInterval(0); // Draw ASAP + runASAP = true; + } else { + powerMon->clearState(meshtastic_PowerMon_State_Screen_On); #ifdef USE_EINK - // eInkScreensaver parameter is usually NULL (default argument), default frame used instead - setScreensaverFrames(einkScreensaver); + // eInkScreensaver parameter is usually NULL (default argument), default frame used instead + setScreensaverFrames(einkScreensaver); #endif #ifdef PIN_EINK_EN - digitalWrite(PIN_EINK_EN, LOW); + digitalWrite(PIN_EINK_EN, LOW); #elif defined(PCA_PIN_EINK_EN) - io.digitalWrite(PCA_PIN_EINK_EN, LOW); + io.digitalWrite(PCA_PIN_EINK_EN, LOW); #endif - dispdev->displayOff(); + dispdev->displayOff(); #ifdef SCREEN_12V_ENABLE - digitalWrite(SCREEN_12V_ENABLE, LOW); + digitalWrite(SCREEN_12V_ENABLE, LOW); #endif #ifdef USE_ST7789 - SPI1.end(); + SPI1.end(); #if defined(ARCH_ESP32) - pinMode(VTFT_LEDA, ANALOG); - pinMode(VTFT_CTRL, ANALOG); - pinMode(ST7789_RESET, ANALOG); - pinMode(ST7789_RS, ANALOG); - pinMode(ST7789_NSS, ANALOG); + pinMode(VTFT_LEDA, ANALOG); + pinMode(VTFT_CTRL, ANALOG); + pinMode(ST7789_RESET, ANALOG); + pinMode(ST7789_RS, ANALOG); + pinMode(ST7789_NSS, ANALOG); #else - nrf_gpio_cfg_default(VTFT_LEDA); - nrf_gpio_cfg_default(VTFT_CTRL); - nrf_gpio_cfg_default(ST7789_RESET); - nrf_gpio_cfg_default(ST7789_RS); - nrf_gpio_cfg_default(ST7789_NSS); + nrf_gpio_cfg_default(VTFT_LEDA); + nrf_gpio_cfg_default(VTFT_CTRL); + nrf_gpio_cfg_default(ST7789_RESET); + nrf_gpio_cfg_default(ST7789_RS); + nrf_gpio_cfg_default(ST7789_NSS); #endif #endif #ifdef USE_ST7796 - SPI1.end(); + SPI1.end(); #if defined(ARCH_ESP32) - pinMode(VTFT_LEDA, OUTPUT); - digitalWrite(VTFT_LEDA, LOW); - pinMode(ST7796_RESET, ANALOG); - pinMode(ST7796_RS, ANALOG); - pinMode(ST7796_NSS, ANALOG); + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, LOW); + pinMode(ST7796_RESET, ANALOG); + pinMode(ST7796_RS, ANALOG); + pinMode(ST7796_NSS, ANALOG); #else - nrf_gpio_cfg_default(VTFT_LEDA); - nrf_gpio_cfg_default(ST7796_RESET); - nrf_gpio_cfg_default(ST7796_RS); - nrf_gpio_cfg_default(ST7796_NSS); + nrf_gpio_cfg_default(VTFT_LEDA); + nrf_gpio_cfg_default(ST7796_RESET); + nrf_gpio_cfg_default(ST7796_RS); + nrf_gpio_cfg_default(ST7796_NSS); #endif #endif #ifdef T_WATCH_S3 - PMU->disablePowerOutput(XPOWERS_ALDO2); + PMU->disablePowerOutput(XPOWERS_ALDO2); #endif - enabled = false; + enabled = false; + } + screenOn = on; } - screenOn = on; - } } -void Screen::setup() { +void Screen::setup() +{ - // Enable display rendering - useDisplay = true; + // Enable display rendering + useDisplay = true; - // Load saved brightness from UI config - // For OLED displays (SSD1306), default brightness is 255 if not set - if (uiconfig.screen_brightness == 0) { + // Load saved brightness from UI config + // For OLED displays (SSD1306), default brightness is 255 if not set + if (uiconfig.screen_brightness == 0) { #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - brightness = 255; // Default for OLED + brightness = 255; // Default for OLED #else - brightness = BRIGHTNESS_DEFAULT; + brightness = BRIGHTNESS_DEFAULT; #endif - } else { - brightness = uiconfig.screen_brightness; - } + } else { + brightness = uiconfig.screen_brightness; + } - // Detect OLED subtype (if supported by board variant) + // Detect OLED subtype (if supported by board variant) #ifdef AutoOLEDWire_h - if (isAUTOOled) - static_cast(dispdev)->setDetected(model); + if (isAUTOOled) + static_cast(dispdev)->setDetected(model); #endif #if defined(USE_SH1107_128_64) || defined(USE_SH1107) - static_cast(dispdev)->setSubtype(7); + static_cast(dispdev)->setSubtype(7); #endif #if defined(USE_ST7789) && defined(TFT_MESH) - // Apply custom RGB color (e.g. Heltec T114/T190) - static_cast(dispdev)->setRGB(TFT_MESH); + // Apply custom RGB color (e.g. Heltec T114/T190) + static_cast(dispdev)->setRGB(TFT_MESH); #endif #if defined(MUZI_BASE) - dispdev->delayPoweron = true; + dispdev->delayPoweron = true; #endif #if defined(USE_ST7796) && defined(TFT_MESH) - // Custom text color, if defined in variant.h - static_cast(dispdev)->setRGB(TFT_MESH); + // Custom text color, if defined in variant.h + static_cast(dispdev)->setRGB(TFT_MESH); #endif - // Initialize display and UI system - ui->init(); - displayWidth = dispdev->width(); - displayHeight = dispdev->height(); + // Initialize display and UI system + ui->init(); + displayWidth = dispdev->width(); + displayHeight = dispdev->height(); - ui->setTimePerTransition(0); // Disable animation delays - ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) - ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) - ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active - ui->disableAllIndicators(); // Disable page indicator dots - ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance + ui->setTimePerTransition(0); // Disable animation delays + ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) + ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) + ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active + ui->disableAllIndicators(); // Disable page indicator dots + ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - // Apply loaded brightness + // Apply loaded brightness #if defined(ST7789_CS) - static_cast(dispdev)->setDisplayBrightness(brightness); + static_cast(dispdev)->setDisplayBrightness(brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) - dispdev->setBrightness(brightness); + dispdev->setBrightness(brightness); #endif - LOG_INFO("Applied screen brightness: %d", brightness); + LOG_INFO("Applied screen brightness: %d", brightness); - // Set custom overlay callbacks - static OverlayCallback overlays[] = { - graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame - }; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + // Set custom overlay callbacks + static OverlayCallback overlays[] = { + graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - // Enable UTF-8 to display mapping - dispdev->setFontTableLookupFunction(customFontTableLookup); + // Enable UTF-8 to display mapping + dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT - logo_timeout *= 2; // Give more time for branded boot logos + logo_timeout *= 2; // Give more time for branded boot logos #endif - // Configure alert frames (e.g., "Resuming..." or region name) - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh - alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Configure alert frames (e.g., "Resuming..." or region name) + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) - graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); - else + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) + graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); + else #endif - { - const char *region = myRegion ? myRegion->name : nullptr; - graphics::UIRenderer::drawIconScreen(region, display, state, x, y); - } - }; - ui->setFrames(alertFrames, 1); - ui->disableAutoTransition(); // Require manual navigation between frames + { + const char *region = myRegion ? myRegion->name : nullptr; + graphics::UIRenderer::drawIconScreen(region, display, state, x, y); + } + }; + ui->setFrames(alertFrames, 1); + ui->disableAutoTransition(); // Require manual navigation between frames - // Log buffer for on-screen logs (3 lines max) - dispdev->setLogBuffer(3, 32); + // Log buffer for on-screen logs (3 lines max) + dispdev->setLogBuffer(3, 32); - // Optional screen mirroring or flipping (e.g. for T-Beam orientation) + // Optional screen mirroring or flipping (e.g. for T-Beam orientation) #ifdef SCREEN_MIRROR - dispdev->mirrorScreen(); + dispdev->mirrorScreen(); #else - if (!config.display.flip_screen) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || defined(RAK14014) || \ - defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) - static_cast(dispdev)->flipScreenVertically(); + if (!config.display.flip_screen) { +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) + static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) - static_cast(dispdev)->flipScreenVertically(); + static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7796) - static_cast(dispdev)->mirrorScreen(); + static_cast(dispdev)->mirrorScreen(); #elif !defined(M5STACK_UNITC6L) - dispdev->flipScreenVertically(); + dispdev->flipScreenVertically(); #endif - } -#endif - - // Generate device ID from MAC address - uint8_t dmac[6]; - getMacAddr(dmac); - snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); - -#if ARCH_PORTDUINO - handleSetOn(false); // Ensure proper init for Arduino targets -#endif - - // Turn on display and trigger first draw - handleSetOn(true); - graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); - ui->update(); -#ifndef USE_EINK - ui->update(); // Some SSD1306 clones drop the first draw, so run twice -#endif - serialSinceMsec = millis(); - -#if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (portduino_config.touchscreenModule) { - touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); } - } -#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE - touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); #endif - // Subscribe to device status updates - powerStatusObserver.observe(&powerStatus->onNewStatus); - gpsStatusObserver.observe(&gpsStatus->onNewStatus); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + // Generate device ID from MAC address + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + +#if ARCH_PORTDUINO + handleSetOn(false); // Ensure proper init for Arduino targets +#endif + + // Turn on display and trigger first draw + handleSetOn(true); + graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); + ui->update(); +#ifndef USE_EINK + ui->update(); // Some SSD1306 clones drop the first draw, so run twice +#endif + serialSinceMsec = millis(); + +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (portduino_config.touchscreenModule) { + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); + } + } +#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); +#endif + + // Subscribe to device status updates + powerStatusObserver.observe(&powerStatus->onNewStatus); + gpsStatusObserver.observe(&gpsStatus->onNewStatus); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe(adminModule); #endif - if (inputBroker) - inputObserver.observe(inputBroker); + if (inputBroker) + inputObserver.observe(inputBroker); - // Load persisted messages into RAM - messageStore.loadFromFlash(); - LOG_INFO("MessageStore loaded from flash"); + // Load persisted messages into RAM + messageStore.loadFromFlash(); + LOG_INFO("MessageStore loaded from flash"); - // Notify modules that support UI events - MeshModule::observeUIEvents(&uiFrameEventObserver); + // Notify modules that support UI events + MeshModule::observeUIEvents(&uiFrameEventObserver); } -void Screen::setOn(bool on, FrameCallback einkScreensaver) { +void Screen::setOn(bool on, FrameCallback einkScreensaver) +{ #if defined(T_LORA_PAGER) - if (cardKbI2cImpl) - cardKbI2cImpl->toggleBacklight(on); + if (cardKbI2cImpl) + cardKbI2cImpl->toggleBacklight(on); #endif - if (!on) - // We handle off commands immediately, because they might be called because the CPU is shutting down - handleSetOn(false, einkScreensaver); - else - enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON}); + if (!on) + // We handle off commands immediately, because they might be called because the CPU is shutting down + handleSetOn(false, einkScreensaver); + else + enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON}); } -void Screen::forceDisplay(bool forceUiUpdate) { - // Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup. +void Screen::forceDisplay(bool forceUiUpdate) +{ + // Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup. #ifdef USE_EINK - // If requested, make sure queued commands are run, and UI has rendered a new frame - if (forceUiUpdate) { - // Force a display refresh, in addition to the UI update - // Changing the GPS status bar icon apparently doesn't register as a change in image - // (False negative of the image hashing algorithm used to skip identical frames) - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); + // If requested, make sure queued commands are run, and UI has rendered a new frame + if (forceUiUpdate) { + // Force a display refresh, in addition to the UI update + // Changing the GPS status bar icon apparently doesn't register as a change in image + // (False negative of the image hashing algorithm used to skip identical frames) + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - // No delay between UI frame rendering - setFastFramerate(); + // No delay between UI frame rendering + setFastFramerate(); - // Make sure all CMDs have run first - while (!cmdQueue.isEmpty()) - runOnce(); + // Make sure all CMDs have run first + while (!cmdQueue.isEmpty()) + runOnce(); - // Ensure at least one frame has drawn - uint64_t startUpdate; - do { - startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. - delay(10); - ui->update(); - } while (ui->getUiState()->lastUpdate < startUpdate); + // Ensure at least one frame has drawn + uint64_t startUpdate; + do { + startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. + delay(10); + ui->update(); + } while (ui->getUiState()->lastUpdate < startUpdate); - // Return to normal frame rate - targetFramerate = IDLE_FRAMERATE; - ui->setTargetFPS(targetFramerate); - } + // Return to normal frame rate + targetFramerate = IDLE_FRAMERATE; + ui->setTargetFPS(targetFramerate); + } - // Tell EInk class to update the display - static_cast(dispdev)->forceDisplay(); + // Tell EInk class to update the display + static_cast(dispdev)->forceDisplay(); #else - // No delay between UI frame rendering - if (forceUiUpdate) { - setFastFramerate(); - } + // No delay between UI frame rendering + if (forceUiUpdate) { + setFastFramerate(); + } #endif } static uint32_t lastScreenTransition; -int32_t Screen::runOnce() { - // If we don't have a screen, don't ever spend any CPU for us. - if (!useDisplay) { - enabled = false; - return RUN_SAME; - } - - if (displayHeight == 0) { - displayHeight = dispdev->getHeight(); - } - - // Detect frame transitions and clear message cache when leaving text message screen - { - static int8_t lastFrameIndex = -1; - int8_t currentFrameIndex = ui->getUiState()->currentFrame; - int8_t textMsgIndex = framesetInfo.positions.textMessage; - - if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { - - if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { - graphics::MessageRenderer::clearMessageCache(); - } +int32_t Screen::runOnce() +{ + // If we don't have a screen, don't ever spend any CPU for us. + if (!useDisplay) { + enabled = false; + return RUN_SAME; } - lastFrameIndex = currentFrameIndex; - } + if (displayHeight == 0) { + displayHeight = dispdev->getHeight(); + } - menuHandler::handleMenuSwitch(dispdev); + // Detect frame transitions and clear message cache when leaving text message screen + { + static int8_t lastFrameIndex = -1; + int8_t currentFrameIndex = ui->getUiState()->currentFrame; + int8_t textMsgIndex = framesetInfo.positions.textMessage; - // Show boot screen for first logo_timeout seconds, then switch to normal operation. - // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup - static bool showingBootScreen = true; - if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) { - LOG_INFO("Done with boot screen"); - stopBootScreen(); - showingBootScreen = false; - } + if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { + + if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { + graphics::MessageRenderer::clearMessageCache(); + } + } + + lastFrameIndex = currentFrameIndex; + } + + menuHandler::handleMenuSwitch(dispdev); + + // Show boot screen for first logo_timeout seconds, then switch to normal operation. + // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup + static bool showingBootScreen = true; + if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) { + LOG_INFO("Done with boot screen"); + stopBootScreen(); + showingBootScreen = false; + } #ifdef USERPREFS_OEM_TEXT - static bool showingOEMBootScreen = true; - if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { - LOG_INFO("Switch to OEM screen..."); - // Change frames. - static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; - static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); - ui->setFrames(bootOEMFrames, bootOEMFrameCount); - ui->update(); + static bool showingOEMBootScreen = true; + if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { + LOG_INFO("Switch to OEM screen..."); + // Change frames. + static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; + static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); + ui->setFrames(bootOEMFrames, bootOEMFrameCount); + ui->update(); #ifndef USE_EINK - ui->update(); + ui->update(); #endif - showingOEMBootScreen = false; - } + showingOEMBootScreen = false; + } #endif #ifndef DISABLE_WELCOME_UNSET - if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { #if defined(M5STACK_UNITC6L) - menuHandler::LoraRegionPicker(); + menuHandler::LoraRegionPicker(); #else - menuHandler::OnboardMessage(); + menuHandler::OnboardMessage(); #endif - } -#endif - if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { - showSimpleBanner("Rebooting...", 0); - } - - // Process incoming commands. - for (;;) { - ScreenCmd cmd; - if (!cmdQueue.dequeue(&cmd, 0)) { - break; } - switch (cmd.cmd) { - case Cmd::SET_ON: - handleSetOn(true); - break; - case Cmd::SET_OFF: - handleSetOn(false); - break; - case Cmd::ON_PRESS: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - showFrame(FrameDirection::NEXT); - } - break; - case Cmd::SHOW_PREV_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - showFrame(FrameDirection::PREVIOUS); - } - break; - case Cmd::SHOW_NEXT_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - showFrame(FrameDirection::NEXT); - } - break; - case Cmd::START_ALERT_FRAME: { - showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away - showingNormalScreen = false; - NotificationRenderer::pauseBanner = true; - alertFrames[0] = alertFrame; +#endif + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + showSimpleBanner("Rebooting...", 0); + } + + // Process incoming commands. + for (;;) { + ScreenCmd cmd; + if (!cmdQueue.dequeue(&cmd, 0)) { + break; + } + switch (cmd.cmd) { + case Cmd::SET_ON: + handleSetOn(true); + break; + case Cmd::SET_OFF: + handleSetOn(false); + break; + case Cmd::ON_PRESS: + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + showFrame(FrameDirection::NEXT); + } + break; + case Cmd::SHOW_PREV_FRAME: + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + showFrame(FrameDirection::PREVIOUS); + } + break; + case Cmd::SHOW_NEXT_FRAME: + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + showFrame(FrameDirection::NEXT); + } + break; + case Cmd::START_ALERT_FRAME: { + showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away + showingNormalScreen = false; + NotificationRenderer::pauseBanner = true; + alertFrames[0] = alertFrame; #ifdef USE_EINK - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) #endif - setFrameImmediateDraw(alertFrames); - break; + setFrameImmediateDraw(alertFrames); + break; + } + case Cmd::START_FIRMWARE_UPDATE_SCREEN: + handleStartFirmwareUpdateScreen(); + break; + case Cmd::STOP_ALERT_FRAME: + NotificationRenderer::pauseBanner = false; + break; + case Cmd::STOP_BOOT_SCREEN: + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } + break; + case Cmd::NOOP: + break; + default: + LOG_ERROR("Invalid screen cmd"); + } } - case Cmd::START_FIRMWARE_UPDATE_SCREEN: - handleStartFirmwareUpdateScreen(); - break; - case Cmd::STOP_ALERT_FRAME: - NotificationRenderer::pauseBanner = false; - break; - case Cmd::STOP_BOOT_SCREEN: - EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - setFrames(); - } - break; - case Cmd::NOOP: - break; - default: - LOG_ERROR("Invalid screen cmd"); + + if (!screenOn) { // If we didn't just wake and the screen is still off, then + // stop updating until it is on again + enabled = false; + return 0; } - } - if (!screenOn) { // If we didn't just wake and the screen is still off, then - // stop updating until it is on again - enabled = false; - return 0; - } + // this must be before the frameState == FIXED check, because we always + // want to draw at least one FIXED frame before doing forceDisplay + ui->update(); - // this must be before the frameState == FIXED check, because we always - // want to draw at least one FIXED frame before doing forceDisplay - ui->update(); + // Switch to a low framerate (to save CPU) when we are not in transition + // but we should only call setTargetFPS when framestate changes, because + // otherwise that breaks animations. - // Switch to a low framerate (to save CPU) when we are not in transition - // but we should only call setTargetFPS when framestate changes, because - // otherwise that breaks animations. + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { + // oldFrameState = ui->getUiState()->frameState; + targetFramerate = IDLE_FRAMERATE; - if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { - // oldFrameState = ui->getUiState()->frameState; - targetFramerate = IDLE_FRAMERATE; + ui->setTargetFPS(targetFramerate); + forceDisplay(); + } - ui->setTargetFPS(targetFramerate); - forceDisplay(); - } + // While showing the bootscreen or Bluetooth pair screen all of our + // standard screen switching is stopped. + if (showingNormalScreen) { + // standard screen loop handling here + if (config.display.auto_screen_carousel_secs > 0 && + NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && + !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { - // While showing the bootscreen or Bluetooth pair screen all of our - // standard screen switching is stopped. - if (showingNormalScreen) { - // standard screen loop handling here - if (config.display.auto_screen_carousel_secs > 0 && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && - !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { - - // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead - // Carousel is potentially a major source of E-Ink display wear + // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead + // Carousel is potentially a major source of E-Ink display wear #if !defined(EINK_BACKGROUND_USES_FAST) - EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); #endif - LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition)); - handleOnPress(); + LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition)); + handleOnPress(); + } } - } - // LOG_DEBUG("want fps %d, fixed=%d", targetFramerate, - // ui->getUiState()->frameState); If we are scrolling we need to be called - // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice - // as fast as we really need so that any rounding errors still result with - // the correct framerate - return (1000 / targetFramerate); + // LOG_DEBUG("want fps %d, fixed=%d", targetFramerate, + // ui->getUiState()->frameState); If we are scrolling we need to be called + // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice + // as fast as we really need so that any rounding errors still result with + // the correct framerate + return (1000 / targetFramerate); } /* show a message that the SSL cert is being built * it is expected that this will be used during the boot phase */ -void Screen::setSSLFrames() { - if (address_found.address) { - // LOG_DEBUG("Show SSL frames"); - static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; - ui->setFrames(sslFrames, 1); - ui->update(); - } +void Screen::setSSLFrames() +{ + if (address_found.address) { + // LOG_DEBUG("Show SSL frames"); + static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; + ui->setFrames(sslFrames, 1); + ui->update(); + } } #ifdef USE_EINK /// Determine which screensaver frame to use, then set the FrameCallback -void Screen::setScreensaverFrames(FrameCallback einkScreensaver) { - // Retain specified frame / overlay callback beyond scope of this method - static FrameCallback screensaverFrame; - static OverlayCallback screensaverOverlay; +void Screen::setScreensaverFrames(FrameCallback einkScreensaver) +{ + // Retain specified frame / overlay callback beyond scope of this method + static FrameCallback screensaverFrame; + static OverlayCallback screensaverOverlay; #if defined(HAS_EINK_ASYNCFULL) && defined(USE_EINK_DYNAMICDISPLAY) - // Join (await) a currently running async refresh, then run the post-update code. - // Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread. - EINK_JOIN_ASYNCREFRESH(dispdev); + // Join (await) a currently running async refresh, then run the post-update code. + // Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread. + EINK_JOIN_ASYNCREFRESH(dispdev); #endif - // If: one-off screensaver frame passed as argument. Handles doDeepSleep() - if (einkScreensaver != NULL) { - screensaverFrame = einkScreensaver; - ui->setFrames(&screensaverFrame, 1); - } + // If: one-off screensaver frame passed as argument. Handles doDeepSleep() + if (einkScreensaver != NULL) { + screensaverFrame = einkScreensaver; + ui->setFrames(&screensaverFrame, 1); + } - // Else, display the usual "overlay" screensaver - else { - screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; - ui->setOverlays(&screensaverOverlay, 1); - } + // Else, display the usual "overlay" screensaver + else { + screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; + ui->setOverlays(&screensaverOverlay, 1); + } - // Request new frame, ASAP - setFastFramerate(); - uint64_t startUpdate; - do { - startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. - delay(1); - ui->update(); - } while (ui->getUiState()->lastUpdate < startUpdate); + // Request new frame, ASAP + setFastFramerate(); + uint64_t startUpdate; + do { + startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. + delay(1); + ui->update(); + } while (ui->getUiState()->lastUpdate < startUpdate); - // Old EInkDisplay class + // Old EInkDisplay class #if !defined(USE_EINK_DYNAMICDISPLAY) - static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit + static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit #endif - // Prepare now for next frame, shown when display wakes - ui->setOverlays(NULL, 0); // Clear overlay - setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally + // Prepare now for next frame, shown when display wakes + ui->setOverlays(NULL, 0); // Clear overlay + setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally - // Pick a refresh method, for when display wakes + // Pick a refresh method, for when display wakes #ifdef EINK_HASQUIRK_GHOSTING - EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" + EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" #else - EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh + EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh #endif } #endif // Regenerate the normal set of frames, focusing a specific frame if requested // Called when a frame should be added / removed, or custom frames should be cleared -void Screen::setFrames(FrameFocus focus) { - // Block setFrames calls when virtual keyboard is active to prevent overlay interference - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return; - } +void Screen::setFrames(FrameFocus focus) +{ + // Block setFrames calls when virtual keyboard is active to prevent overlay interference + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return; + } - uint8_t originalPosition = ui->getUiState()->currentFrame; - uint8_t previousFrameCount = framesetInfo.frameCount; - FramesetInfo fsi; // Location of specific frames, for applying focus parameter + uint8_t originalPosition = ui->getUiState()->currentFrame; + uint8_t previousFrameCount = framesetInfo.frameCount; + FramesetInfo fsi; // Location of specific frames, for applying focus parameter - graphics::UIRenderer::rebuildFavoritedNodes(); + graphics::UIRenderer::rebuildFavoritedNodes(); - LOG_DEBUG("Show standard frames"); - showingNormalScreen = true; + LOG_DEBUG("Show standard frames"); + showingNormalScreen = true; - indicatorIcons.clear(); + indicatorIcons.clear(); - size_t numframes = 0; + size_t numframes = 0; - // If we have a critical fault, show it first - fsi.positions.fault = numframes; - if (error_code) { - normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; - indicatorIcons.push_back(icon_error); - focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame - } + // If we have a critical fault, show it first + fsi.positions.fault = numframes; + if (error_code) { + normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; + indicatorIcons.push_back(icon_error); + focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame + } #if defined(DISPLAY_CLOCK_FRAME) - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; #if defined(M5STACK_UNITC6L) - normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else - normalFrames[numframes++] = - uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; #endif - indicatorIcons.push_back(digital_icon_clock); - } + indicatorIcons.push_back(digital_icon_clock); + } #endif - if (!hiddenFrames.home) { - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); - } + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } - fsi.positions.textMessage = numframes; - normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; - indicatorIcons.push_back(icon_mail); + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist_nodes) { - fsi.positions.nodelist_nodes = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; - indicatorIcons.push_back(icon_nodes); - } - if (!hiddenFrames.nodelist_location) { - fsi.positions.nodelist_location = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; - indicatorIcons.push_back(icon_list); - } + if (!hiddenFrames.nodelist_nodes) { + fsi.positions.nodelist_nodes = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_location) { + fsi.positions.nodelist_location = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; + indicatorIcons.push_back(icon_list); + } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - if (!hiddenFrames.nodelist_lastheard) { - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - } - if (!hiddenFrames.nodelist_hopsignal) { - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - } - if (!hiddenFrames.nodelist_distance) { - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); - } + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS #ifdef USE_EINK - if (!hiddenFrames.nodelist_bearings) { - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - } + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } #endif - if (!hiddenFrames.gps) { - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); - } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance && !hiddenFrames.lora) { - fsi.positions.lora = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; - indicatorIcons.push_back(icon_radio); - } - if (!hiddenFrames.system) { - fsi.positions.system = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; - indicatorIcons.push_back(icon_system); - } + if (RadioLibInterface::instance && !hiddenFrames.lora) { + fsi.positions.lora = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + } + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); + } #if !defined(DISPLAY_CLOCK_FRAME) - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; - normalFrames[numframes++] = - uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); - } + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif - if (!hiddenFrames.chirpy) { - fsi.positions.chirpy = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; - indicatorIcons.push_back(chirpy_small); - } + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(chirpy_small); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!hiddenFrames.wifi && isWifiAvailable()) { - fsi.positions.wifi = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; - indicatorIcons.push_back(icon_wifi); - } + if (!hiddenFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); + } #endif - // Beware of what changes you make in this code! - // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! - // Inside of that callback, goes over to MeshModule.cpp and we run - // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr - // entries until we're ready to start building the matching entries. - // We are doing our best to keep the normalFrames vector - // and the moduleFrames vector in lock step. - moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); - LOG_DEBUG("Show %d module frames", moduleFrames.size()); + // Beware of what changes you make in this code! + // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! + // Inside of that callback, goes over to MeshModule.cpp and we run + // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr + // entries until we're ready to start building the matching entries. + // We are doing our best to keep the normalFrames vector + // and the moduleFrames vector in lock step. + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); + LOG_DEBUG("Show %d module frames", moduleFrames.size()); - for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { - // Draw the module frame, using the hack described above - if (*i != nullptr) { - normalFrames[numframes] = drawModuleFrame; + for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { + // Draw the module frame, using the hack described above + if (*i != nullptr) { + normalFrames[numframes] = drawModuleFrame; - // Check if the module being drawn has requested focus - // We will honor this request later, if setFrames was triggered by a UIFrameEvent - MeshModule *m = *i; - if (m && m->isRequestingFocus()) - fsi.positions.focusedModule = numframes; - if (m && m == waypointModule) - fsi.positions.waypoint = numframes; + // Check if the module being drawn has requested focus + // We will honor this request later, if setFrames was triggered by a UIFrameEvent + MeshModule *m = *i; + if (m && m->isRequestingFocus()) + fsi.positions.focusedModule = numframes; + if (m && m == waypointModule) + fsi.positions.waypoint = numframes; - indicatorIcons.push_back(icon_module); - numframes++; - } - } - - LOG_DEBUG("Added modules. numframes: %d", numframes); - - // We don't show the node info of our node (if we have it yet - we should) - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (numMeshNodes > 0) - numMeshNodes--; - - if (!hiddenFrames.show_favorites) { - // Temporary array to hold favorite node frames - std::vector favoriteFrames; - - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); - } + indicatorIcons.push_back(icon_module); + numframes++; + } } - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); - } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; + LOG_DEBUG("Added modules. numframes: %d", numframes); + + // We don't show the node info of our node (if we have it yet - we should) + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (numMeshNodes > 0) + numMeshNodes--; + + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; + + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } + } + + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; + } } - } - fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE - this->frameCount = numframes; // Save frame count for use in custom overlay - LOG_DEBUG("Finished build frames. numframes: %d", numframes); + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + this->frameCount = numframes; // Save frame count for use in custom overlay + LOG_DEBUG("Finished build frames. numframes: %d", numframes); - ui->setFrames(normalFrames, numframes); - ui->disableAllIndicators(); + ui->setFrames(normalFrames, numframes); + ui->disableAllIndicators(); - // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) - // Focus on a specific frame, in the frame set we just created - switch (focus) { - case FOCUS_DEFAULT: - ui->switchToFrame(fsi.positions.deviceFocused); - break; - case FOCUS_FAULT: - ui->switchToFrame(fsi.positions.fault); - break; - case FOCUS_MODULE: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.focusedModule); - break; - case FOCUS_CLOCK: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.clock); - break; - case FOCUS_SYSTEM: - ui->switchToFrame(fsi.positions.system); - break; + // Focus on a specific frame, in the frame set we just created + switch (focus) { + case FOCUS_DEFAULT: + ui->switchToFrame(fsi.positions.deviceFocused); + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_MODULE: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.focusedModule); + break; + case FOCUS_CLOCK: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.clock); + break; + case FOCUS_SYSTEM: + ui->switchToFrame(fsi.positions.system); + break; - case FOCUS_PRESERVE: - // No more adjustment — force stay on same index - if (previousFrameCount > fsi.frameCount) { - ui->switchToFrame(originalPosition - 1); - } else if (previousFrameCount < fsi.frameCount) { - ui->switchToFrame(originalPosition + 1); - } else { - ui->switchToFrame(originalPosition); + case FOCUS_PRESERVE: + // No more adjustment — force stay on same index + if (previousFrameCount > fsi.frameCount) { + ui->switchToFrame(originalPosition - 1); + } else if (previousFrameCount < fsi.frameCount) { + ui->switchToFrame(originalPosition + 1); + } else { + ui->switchToFrame(originalPosition); + } + break; } - break; - } - // Store the info about this frameset, for future setFrames calls - this->framesetInfo = fsi; + // Store the info about this frameset, for future setFrames calls + this->framesetInfo = fsi; - setFastFramerate(); // Draw ASAP + setFastFramerate(); // Draw ASAP } -void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) { - ui->disableAllIndicators(); - ui->setFrames(drawFrames, 1); - setFastFramerate(); +void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) +{ + ui->disableAllIndicators(); + ui->setFrames(drawFrames, 1); + setFastFramerate(); } -void Screen::toggleFrameVisibility(const std::string &frameName) { +void Screen::toggleFrameVisibility(const std::string &frameName) +{ #ifndef USE_EINK - if (frameName == "nodelist_nodes") { - hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; - } - if (frameName == "nodelist_location") { - hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; - } + if (frameName == "nodelist_nodes") { + hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; + } + if (frameName == "nodelist_location") { + hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; + } #endif #ifdef USE_EINK - if (frameName == "nodelist_lastheard") { - hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; - } - if (frameName == "nodelist_hopsignal") { - hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; - } - if (frameName == "nodelist_distance") { - hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; - } + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } #endif #if HAS_GPS #ifdef USE_EINK - if (frameName == "nodelist_bearings") { - hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; - } + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } #endif - if (frameName == "gps") { - hiddenFrames.gps = !hiddenFrames.gps; - } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } #endif - if (frameName == "lora") { - hiddenFrames.lora = !hiddenFrames.lora; - } - if (frameName == "clock") { - hiddenFrames.clock = !hiddenFrames.clock; - } - if (frameName == "show_favorites") { - hiddenFrames.show_favorites = !hiddenFrames.show_favorites; - } - if (frameName == "chirpy") { - hiddenFrames.chirpy = !hiddenFrames.chirpy; - } + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; + } } -bool Screen::isFrameHidden(const std::string &frameName) const { +bool Screen::isFrameHidden(const std::string &frameName) const +{ #ifndef USE_EINK - if (frameName == "nodelist_nodes") - return hiddenFrames.nodelist_nodes; - if (frameName == "nodelist_location") - return hiddenFrames.nodelist_location; + if (frameName == "nodelist_nodes") + return hiddenFrames.nodelist_nodes; + if (frameName == "nodelist_location") + return hiddenFrames.nodelist_location; #endif #ifdef USE_EINK - if (frameName == "nodelist_lastheard") - return hiddenFrames.nodelist_lastheard; - if (frameName == "nodelist_hopsignal") - return hiddenFrames.nodelist_hopsignal; - if (frameName == "nodelist_distance") - return hiddenFrames.nodelist_distance; + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; #endif #if HAS_GPS #ifdef USE_EINK - if (frameName == "nodelist_bearings") - return hiddenFrames.nodelist_bearings; + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; #endif - if (frameName == "gps") - return hiddenFrames.gps; + if (frameName == "gps") + return hiddenFrames.gps; #endif - if (frameName == "lora") - return hiddenFrames.lora; - if (frameName == "clock") - return hiddenFrames.clock; - if (frameName == "show_favorites") - return hiddenFrames.show_favorites; - if (frameName == "chirpy") - return hiddenFrames.chirpy; + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; - return false; + return false; } -void Screen::handleStartFirmwareUpdateScreen() { - LOG_DEBUG("Show firmware screen"); - showingNormalScreen = false; - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame +void Screen::handleStartFirmwareUpdateScreen() +{ + LOG_DEBUG("Show firmware screen"); + showingNormalScreen = false; + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; - setFrameImmediateDraw(frames); + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; + setFrameImmediateDraw(frames); } -void Screen::blink() { - setFastFramerate(); - uint8_t count = 10; - dispdev->setBrightness(254); - while (count > 0) { - dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); - dispdev->display(); - delay(50); - dispdev->clear(); - dispdev->display(); - delay(50); - count = count - 1; - } - // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in - // OLEDDisplay. - dispdev->setBrightness(brightness); -} - -void Screen::increaseBrightness() { - brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); - -#if defined(ST7789_CS) - // run the setDisplayBrightness function. This works on t-decks - static_cast(dispdev)->setDisplayBrightness(brightness); -#endif - - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ -} - -void Screen::decreaseBrightness() { - brightness = (brightness < 70) ? brightness : (brightness - 62); - -#if defined(ST7789_CS) - static_cast(dispdev)->setDisplayBrightness(brightness); -#endif - - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ -} - -void Screen::handleOnPress() { - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); +void Screen::blink() +{ setFastFramerate(); - } -} - -void Screen::showFrame(FrameDirection direction) { - // Only advance frames when UI is stable - if (ui->getUiState()->frameState == FIXED) { - - if (direction == FrameDirection::NEXT) { - ui->nextFrame(); - } else { - ui->previousFrame(); + uint8_t count = 10; + dispdev->setBrightness(254); + while (count > 0) { + dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); + dispdev->display(); + delay(50); + dispdev->clear(); + dispdev->display(); + delay(50); + count = count - 1; } + // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in + // OLEDDisplay. + dispdev->setBrightness(brightness); +} - lastScreenTransition = millis(); - setFastFramerate(); - } +void Screen::increaseBrightness() +{ + brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); + +#if defined(ST7789_CS) + // run the setDisplayBrightness function. This works on t-decks + static_cast(dispdev)->setDisplayBrightness(brightness); +#endif + + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ +} + +void Screen::decreaseBrightness() +{ + brightness = (brightness < 70) ? brightness : (brightness - 62); + +#if defined(ST7789_CS) + static_cast(dispdev)->setDisplayBrightness(brightness); +#endif + + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ +} + +void Screen::handleOnPress() +{ + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->nextFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} + +void Screen::showFrame(FrameDirection direction) +{ + // Only advance frames when UI is stable + if (ui->getUiState()->frameState == FIXED) { + + if (direction == FrameDirection::NEXT) { + ui->nextFrame(); + } else { + ui->previousFrame(); + } + + lastScreenTransition = millis(); + setFastFramerate(); + } } #ifndef SCREEN_TRANSITION_FRAMERATE #define SCREEN_TRANSITION_FRAMERATE 30 // fps #endif -void Screen::setFastFramerate() { +void Screen::setFastFramerate() +{ #if defined(M5STACK_UNITC6L) - dispdev->clear(); - dispdev->display(); + dispdev->clear(); + dispdev->display(); #endif - // We are about to start a transition so speed up fps - targetFramerate = SCREEN_TRANSITION_FRAMERATE; + // We are about to start a transition so speed up fps + targetFramerate = SCREEN_TRANSITION_FRAMERATE; - ui->setTargetFPS(targetFramerate); - setInterval(0); // redraw ASAP - runASAP = true; + ui->setTargetFPS(targetFramerate); + setInterval(0); // redraw ASAP + runASAP = true; } -int Screen::handleStatusUpdate(const meshtastic::Status *arg) { - switch (arg->getStatusType()) { - case STATUS_TYPE_NODE: - if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { - setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) +int Screen::handleStatusUpdate(const meshtastic::Status *arg) +{ + switch (arg->getStatusType()) { + case STATUS_TYPE_NODE: + if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { + setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) + } + nodeDB->updateGUI = false; + break; + case STATUS_TYPE_POWER: + forceDisplay(true); + break; } - nodeDB->updateGUI = false; - break; - case STATUS_TYPE_POWER: - forceDisplay(true); - break; - } - return 0; + return 0; } // Handles when message is received; will jump to text message frame. -int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { - if (showingNormalScreen) { - if (packet->from == 0) { - // Outgoing message (likely sent from phone) - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - hiddenFrames.textMessage = true; - hasUnreadMessage = false; // Clear unread state when user replies +int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) +{ + if (showingNormalScreen) { + if (packet->from == 0) { + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + hiddenFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies - setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list - } else { - // Incoming message - devicestate.has_rx_text_message = true; // Needed to include the message frame - hasUnreadMessage = true; // Enables mail icon in the header - setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) - - // Only wake/force display if the configuration allows it - if (shouldWakeOnReceivedMessage()) { - setOn(true); // Wake up the screen first - forceDisplay(); // Forces screen redraw - } - // === Prepare banner/popup content === - const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); - const meshtastic_Channel channel = channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); - const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; - - const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); - - char banner[256]; - - bool isAlert = false; - - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_bell_buzzer) - // Check for bell character to determine if this message is an alert - for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { - if (msgRaw[i] == ASCII_BELL) { - isAlert = true; - break; - } - } - - // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any - // 'mute' preferences set to any specific node or channel. - // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - // Wake and force redraw so popup is visible immediately - if (shouldWakeOnReceivedMessage()) { - setOn(true); - forceDisplay(); - } - - // Build popup: title = message source name, content = message text (sanitized) - // Title - char titleBuf[64] = {0}; - if (longName && longName[0]) { - // Sanitize sender name - std::string t = sanitizeString(longName); - strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1); + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list } else { - strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); - } + // Incoming message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) - // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize - char content[256] = {0}; - { - std::string raw; - raw.reserve(packet->decoded.payload.size); - for (size_t i = 0; i < packet->decoded.payload.size; ++i) { - char c = msgRaw[i]; - if (c == ASCII_BELL) - continue; // strip bell - raw.push_back(c); - } - std::string sanitized = sanitizeString(raw); - strncpy(content, sanitized.c_str(), sizeof(content) - 1); - } + // Only wake/force display if the configuration allows it + if (shouldWakeOnReceivedMessage()) { + setOn(true); // Wake up the screen first + forceDisplay(); // Forces screen redraw + } + // === Prepare banner/popup content === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + const meshtastic_Channel channel = + channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); + const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; - NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000); + const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); + + char banner[256]; + + bool isAlert = false; + + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_bell_buzzer) + // Check for bell character to determine if this message is an alert + for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == ASCII_BELL) { + isAlert = true; + break; + } + } + + // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any + // 'mute' preferences set to any specific node or channel. + // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + // Wake and force redraw so popup is visible immediately + if (shouldWakeOnReceivedMessage()) { + setOn(true); + forceDisplay(); + } + + // Build popup: title = message source name, content = message text (sanitized) + // Title + char titleBuf[64] = {0}; + if (longName && longName[0]) { + // Sanitize sender name + std::string t = sanitizeString(longName); + strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1); + } else { + strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); + } + + // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize + char content[256] = {0}; + { + std::string raw; + raw.reserve(packet->decoded.payload.size); + for (size_t i = 0; i < packet->decoded.payload.size; ++i) { + char c = msgRaw[i]; + if (c == ASCII_BELL) + continue; // strip bell + raw.push_back(c); + } + std::string sanitized = sanitizeString(raw); + strncpy(content, sanitized.c_str(), sizeof(content) - 1); + } + + NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000); // Maintain existing buzzer behavior on M5 if applicable #if defined(M5STACK_UNITC6L) - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { - playLongBeep(); - } + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || + (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || + (!isBroadcast(packet->to) && isToUs(packet))) { + playLongBeep(); + } #endif - } else { - // No keyboard active: use regular banner flow, respecting mute settings - if (isAlert) { - if (longName && longName[0]) { - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); - } else { - strcpy(banner, "Alert Received"); - } - screen->showSimpleBanner(banner, 3000); - } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { - if (longName && longName[0]) { - if (currentResolution == ScreenResolution::UltraLow) { - strcpy(banner, "New Message"); } else { - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); - } - } else { - strcpy(banner, "New Message"); - } + // No keyboard active: use regular banner flow, respecting mute settings + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + } else { + strcpy(banner, "Alert Received"); + } + screen->showSimpleBanner(banner, 3000); + } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else { + strcpy(banner, "New Message"); + } #if defined(M5STACK_UNITC6L) - screen->setOn(true); - screen->showSimpleBanner(banner, 1500); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { - // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either - // - packet contains an alert and alert bell buzzer is enabled - // - packet is a non-broadcast that is addressed to this node - playLongBeep(); - } + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || + (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || + (!isBroadcast(packet->to) && isToUs(packet))) { + // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either + // - packet contains an alert and alert bell buzzer is enabled + // - packet is a non-broadcast that is addressed to this node + playLongBeep(); + } #else - screen->showSimpleBanner(banner, 3000); + screen->showSimpleBanner(banner, 3000); #endif + } + } } - } } - } - return 0; + return 0; } // Triggered by MeshModules -int Screen::handleUIFrameEvent(const UIFrameEvent *event) { - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { +int Screen::handleUIFrameEvent(const UIFrameEvent *event) +{ + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return 0; + } + + if (showingNormalScreen) { + // Regenerate the frameset, potentially honoring a module's internal requestFocus() call + if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { + setFrames(FOCUS_MODULE); + } + + // Regenerate the frameset, while attempting to maintain focus on the current frame + else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { + setFrames(FOCUS_PRESERVE); + } + + // Don't regenerate the frameset, just re-draw whatever is on screen ASAP + else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { + setFastFramerate(); + } + + // Jump directly to the Text Message screen + else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { + setFrames(FOCUS_PRESERVE); // preserve current frame ordering + ui->switchToFrame(framesetInfo.positions.textMessage); + setFastFramerate(); // force redraw ASAP + } + } + return 0; - } - - if (showingNormalScreen) { - // Regenerate the frameset, potentially honoring a module's internal requestFocus() call - if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { - setFrames(FOCUS_MODULE); - } - - // Regenerate the frameset, while attempting to maintain focus on the current frame - else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { - setFrames(FOCUS_PRESERVE); - } - - // Don't regenerate the frameset, just re-draw whatever is on screen ASAP - else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { - setFastFramerate(); - } - - // Jump directly to the Text Message screen - else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { - setFrames(FOCUS_PRESERVE); // preserve current frame ordering - ui->switchToFrame(framesetInfo.positions.textMessage); - setFastFramerate(); // force redraw ASAP - } - } - - return 0; } -int Screen::handleInputEvent(const InputEvent *event) { - LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar); - if (!screenOn) - return 0; - - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } - -#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a - // screen draw. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) - setFastFramerate(); // Draw ASAP -#endif - if (NotificationRenderer::isOverlayBannerShowing()) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - - menuHandler::handleMenuSwitch(dispdev); - return 0; - } - // UP/DOWN in message screen scrolls through message threads - if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - - if (event->inputEvent == INPUT_BROKER_UP) { - if (messageStore.getMessages().empty()) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else { - graphics::MessageRenderer::scrollUp(); - setFastFramerate(); // match existing behavior +int Screen::handleInputEvent(const InputEvent *event) +{ + LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar); + if (!screenOn) return 0; - } - } - if (event->inputEvent == INPUT_BROKER_DOWN) { - if (messageStore.getMessages().empty()) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else { - graphics::MessageRenderer::scrollDown(); - setFastFramerate(); + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); return 0; - } - } - } - // UP/DOWN in node list screens scrolls through node pages - if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { - if (event->inputEvent == INPUT_BROKER_UP) { - graphics::NodeListRenderer::scrollUp(); - setFastFramerate(); - return 0; } - if (event->inputEvent == INPUT_BROKER_DOWN) { - graphics::NodeListRenderer::scrollDown(); - setFastFramerate(); - return 0; - } - } - // Use left or right input from a keyboard to move between frames, - // so long as a mesh module isn't using these events for some other purpose - if (showingNormalScreen) { +#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + setFastFramerate(); // Draw ASAP +#endif + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); - // Ask any MeshModules if they're handling keyboard input right now - bool inputIntercepted = false; - for (MeshModule *module : moduleFrames) { - if (module && module->interceptingKeyboardInput()) - inputIntercepted = true; - } - - // If no modules are using the input, move between frames - if (!inputIntercepted) { -#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2 - bool handledEncoderScroll = false; - const bool isTextMessageFrame = - (framesetInfo.positions.textMessage != 255 && this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && - !messageStore.getMessages().empty()); - if (isTextMessageFrame) { - if (event->inputEvent == INPUT_BROKER_UP_LONG) { - graphics::MessageRenderer::nudgeScroll(-1); - handledEncoderScroll = true; - } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { - graphics::MessageRenderer::nudgeScroll(1); - handledEncoderScroll = true; - } - } - - if (handledEncoderScroll) { - setFastFramerate(); + menuHandler::handleMenuSwitch(dispdev); return 0; - } -#endif - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showFrame(FrameDirection::PREVIOUS); - } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showFrame(FrameDirection::NEXT); - } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { - // Long press up button for fast frame switching - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { - // Long press down button for fast frame switching - showNextFrame(); - } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && - this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { - menuHandler::systemBaseMenu(); -#if HAS_GPS - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { - menuHandler::positionBaseMenu(); -#endif - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { - menuHandler::clockMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::loraMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (!messageStore.getMessages().empty()) { - menuHandler::messageResponseMenu(); - } else { - if (currentResolution == ScreenResolution::UltraLow) { - menuHandler::textMessageMenu(); + } + // UP/DOWN in message screen scrolls through message threads + if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + + if (event->inputEvent == INPUT_BROKER_UP) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else { - menuHandler::textMessageBaseMenu(); + graphics::MessageRenderer::scrollUp(); + setFastFramerate(); // match existing behavior + return 0; + } + } + + if (event->inputEvent == INPUT_BROKER_DOWN) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollDown(); + setFastFramerate(); + return 0; } - } - } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && - this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { - menuHandler::favoriteBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { - menuHandler::nodeListMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { - menuHandler::wifiBaseMenu(); } - } else if (event->inputEvent == INPUT_BROKER_BACK) { - showFrame(FrameDirection::PREVIOUS); - } else if (event->inputEvent == INPUT_BROKER_CANCEL) { - setOn(false); - } } - } + // UP/DOWN in node list screens scrolls through node pages + if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + if (event->inputEvent == INPUT_BROKER_UP) { + graphics::NodeListRenderer::scrollUp(); + setFastFramerate(); + return 0; + } - return 0; + if (event->inputEvent == INPUT_BROKER_DOWN) { + graphics::NodeListRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } + // Use left or right input from a keyboard to move between frames, + // so long as a mesh module isn't using these events for some other purpose + if (showingNormalScreen) { + + // Ask any MeshModules if they're handling keyboard input right now + bool inputIntercepted = false; + for (MeshModule *module : moduleFrames) { + if (module && module->interceptingKeyboardInput()) + inputIntercepted = true; + } + + // If no modules are using the input, move between frames + if (!inputIntercepted) { +#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2 + bool handledEncoderScroll = false; + const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 && + this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && + !messageStore.getMessages().empty()); + if (isTextMessageFrame) { + if (event->inputEvent == INPUT_BROKER_UP_LONG) { + graphics::MessageRenderer::nudgeScroll(-1); + handledEncoderScroll = true; + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + graphics::MessageRenderer::nudgeScroll(1); + handledEncoderScroll = true; + } + } + + if (handledEncoderScroll) { + setFastFramerate(); + return 0; + } +#endif + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { + showFrame(FrameDirection::PREVIOUS); + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { + showFrame(FrameDirection::NEXT); + } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { + // Long press up button for fast frame switching + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + // Long press down button for fast frame switching + showNextFrame(); + } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && + this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + menuHandler::homeBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { + menuHandler::systemBaseMenu(); +#if HAS_GPS + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { + menuHandler::positionBaseMenu(); +#endif + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { + menuHandler::clockMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { + menuHandler::loraMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + if (!messageStore.getMessages().empty()) { + menuHandler::messageResponseMenu(); + } else { + if (currentResolution == ScreenResolution::UltraLow) { + menuHandler::textMessageMenu(); + } else { + menuHandler::textMessageBaseMenu(); + } + } + } else if (framesetInfo.positions.firstFavorite != 255 && + this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && + this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { + menuHandler::favoriteBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + menuHandler::nodeListMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { + menuHandler::wifiBaseMenu(); + } + } else if (event->inputEvent == INPUT_BROKER_BACK) { + showFrame(FrameDirection::PREVIOUS); + } else if (event->inputEvent == INPUT_BROKER_CANCEL) { + setOn(false); + } + } + } + + return 0; } -int Screen::handleAdminMessage(AdminModule_ObserverData *arg) { - switch (arg->request->which_payload_variant) { - // Node removed manually (i.e. via app) - case meshtastic_AdminMessage_remove_by_nodenum_tag: - setFrames(FOCUS_PRESERVE); - *arg->result = AdminMessageHandleResult::HANDLED; - break; +int Screen::handleAdminMessage(AdminModule_ObserverData *arg) +{ + switch (arg->request->which_payload_variant) { + // Node removed manually (i.e. via app) + case meshtastic_AdminMessage_remove_by_nodenum_tag: + setFrames(FOCUS_PRESERVE); + *arg->result = AdminMessageHandleResult::HANDLED; + break; - // Default no-op, in case the admin message observable gets used by other classes in future - default: - break; - } - return 0; + // Default no-op, in case the admin message observable gets used by other classes in future + default: + break; + } + return 0; } -bool Screen::isOverlayBannerShowing() { return NotificationRenderer::isOverlayBannerShowing(); } +bool Screen::isOverlayBannerShowing() +{ + return NotificationRenderer::isOverlayBannerShowing(); +} } // namespace graphics @@ -1741,22 +1800,24 @@ bool Screen::isOverlayBannerShowing() { return NotificationRenderer::isOverlayBa graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN -bool shouldWakeOnReceivedMessage() { - /* - The goal here is to determine when we do NOT wake up the screen on message received: - - Any ext. notifications are turned on - - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - - If the battery level is very low - */ - if (moduleConfig.external_notification.enabled) { - return false; - } - if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, - meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { - return false; - } - if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { - return false; - } - return true; +bool shouldWakeOnReceivedMessage() +{ + /* + The goal here is to determine when we do NOT wake up the screen on message received: + - Any ext. notifications are turned on + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE + - If the battery level is very low + */ + if (moduleConfig.external_notification.enabled) { + return false; + } + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { + return false; + } + if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { + return false; + } + return true; } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 388e7b53f..4bb808970 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -10,18 +10,19 @@ #include #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) -namespace graphics { +namespace graphics +{ enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; struct BannerOverlayOptions { - const char *message; - uint32_t durationMs = 30000; - const char **optionsArrayPtr = nullptr; - const int *optionsEnumPtr = nullptr; - uint8_t optionsCount = 0; - std::function bannerCallback = nullptr; - int8_t InitialSelected = 0; - notificationTypeEnum notificationType = notificationTypeEnum::text_banner; + const char *message; + uint32_t durationMs = 30000; + const char **optionsArrayPtr = nullptr; + const int *optionsEnumPtr = nullptr; + uint8_t optionsCount = 0; + std::function bannerCallback = nullptr; + int8_t InitialSelected = 0; + notificationTypeEnum notificationType = notificationTypeEnum::text_banner; }; } // namespace graphics @@ -29,33 +30,35 @@ bool shouldWakeOnReceivedMessage(); #if !HAS_SCREEN #include "power.h" -namespace graphics { +namespace graphics +{ // Noop class for boards without screen. -class Screen { -public: - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - FOCUS_CLOCK, - FOCUS_SYSTEM, - }; +class Screen +{ + public: + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, + FOCUS_SYSTEM, + }; - explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - void onPress() {} - void setup() {} - void setOn(bool) {} - void doDeepSleep() {} - void forceDisplay(bool forceUiUpdate = false) {} - void startFirmwareUpdateScreen() {} - void increaseBrightness() {} - void decreaseBrightness() {} - void startAlert(const char *) {} - void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} - void showOverlayBanner(BannerOverlayOptions) {} - void setFrames(FrameFocus focus) {} - void endAlert() {} + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + void onPress() {} + void setup() {} + void setOn(bool) {} + void doDeepSleep() {} + void forceDisplay(bool forceUiUpdate = false) {} + void startFirmwareUpdateScreen() {} + void increaseBrightness() {} + void decreaseBrightness() {} + void startAlert(const char *) {} + void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} + void showOverlayBanner(BannerOverlayOptions) {} + void setFrames(FrameFocus focus) {} + void endAlert() {} }; } // namespace graphics #else @@ -126,39 +129,45 @@ public: /// Convert an integer GPS coords to a floating point #define DegD(i) (i * 1e-7) extern bool hasUnreadMessage; -namespace { +namespace +{ /// A basic 2D point class for drawing -class Point { -public: - float x, y; +class Point +{ + public: + float x, y; - Point(float _x, float _y) : x(_x), y(_y) {} + Point(float _x, float _y) : x(_x), y(_y) {} - /// Apply a rotation around zero (standard rotation matrix math) - void rotate(float radian) { - float cos = cosf(radian), sin = sinf(radian); - float rx = x * cos + y * sin, ry = -x * sin + y * cos; + /// Apply a rotation around zero (standard rotation matrix math) + void rotate(float radian) + { + float cos = cosf(radian), sin = sinf(radian); + float rx = x * cos + y * sin, ry = -x * sin + y * cos; - x = rx; - y = ry; - } + x = rx; + y = ry; + } - void translate(int16_t dx, int dy) { - x += dx; - y += dy; - } + void translate(int16_t dx, int dy) + { + x += dx; + y += dy; + } - void scale(float f) { - // We use -f here to counter the flip that happens - // on the y axis when drawing and rotating on screen - x *= f; - y *= -f; - } + void scale(float f) + { + // We use -f here to counter the flip that happens + // on the y axis when drawing and rotating on screen + x *= f; + y *= -f; + } }; } // namespace -namespace graphics { +namespace graphics +{ enum class FrameDirection { NEXT, PREVIOUS }; @@ -166,23 +175,24 @@ enum class FrameDirection { NEXT, PREVIOUS }; class Screen; /// Handles gathering and displaying debug information. -class DebugInfo { -public: - DebugInfo(const DebugInfo &) = delete; - DebugInfo &operator=(const DebugInfo &) = delete; +class DebugInfo +{ + public: + DebugInfo(const DebugInfo &) = delete; + DebugInfo &operator=(const DebugInfo &) = delete; -private: - friend Screen; + private: + friend Screen; - DebugInfo() {} + DebugInfo() {} - /// Renders the debug screen. - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + /// Renders the debug screen. + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - /// Protects all of internal state. - concurrency::Lock lock; + /// Protects all of internal state. + concurrency::Lock lock; }; /** @@ -192,523 +202,535 @@ private: * multiple times simultaneously. All state-changing calls are queued and executed * when the main loop calls us. */ -class Screen : public concurrency::OSThread { - CallbackObserver powerStatusObserver = - CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver gpsStatusObserver = - CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver uiFrameEventObserver = - CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules - CallbackObserver inputObserver = CallbackObserver(this, &Screen::handleInputEvent); - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Screen::handleAdminMessage); +class Screen : public concurrency::OSThread +{ + CallbackObserver powerStatusObserver = + CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver gpsStatusObserver = + CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver uiFrameEventObserver = + CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules + CallbackObserver inputObserver = + CallbackObserver(this, &Screen::handleInputEvent); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Screen::handleAdminMessage); -public: - OLEDDisplay *getDisplayDevice() { return dispdev; } - explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + public: + OLEDDisplay *getDisplayDevice() { return dispdev; } + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - // Screen dimension accessors - inline int getHeight() const { return displayHeight; } - inline int getWidth() const { return displayWidth; } - size_t frameCount = 0; // Total number of active frames - ~Screen(); + // Screen dimension accessors + inline int getHeight() const { return displayHeight; } + inline int getWidth() const { return displayWidth; } + size_t frameCount = 0; // Total number of active frames + ~Screen(); - // Which frame we want to be displayed, after we regen the frameset by calling setFrames - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - FOCUS_CLOCK, - FOCUS_SYSTEM, - }; + // Which frame we want to be displayed, after we regen the frameset by calling setFrames + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, + FOCUS_SYSTEM, + }; - // Regenerate the normal set of frames, focusing a specific frame if requested - // Call when a frame should be added / removed, or custom frames should be cleared - void setFrames(FrameFocus focus = FOCUS_DEFAULT); + // Regenerate the normal set of frames, focusing a specific frame if requested + // Call when a frame should be added / removed, or custom frames should be cleared + void setFrames(FrameFocus focus = FOCUS_DEFAULT); - std::vector indicatorIcons; // Per-frame custom icon pointers - Screen(const Screen &) = delete; - Screen &operator=(const Screen &) = delete; + std::vector indicatorIcons; // Per-frame custom icon pointers + Screen(const Screen &) = delete; + Screen &operator=(const Screen &) = delete; - ScanI2C::DeviceAddress address_found; - meshtastic_Config_DisplayConfig_OledType model; - OLEDDISPLAY_GEOMETRY geometry; + ScanI2C::DeviceAddress address_found; + meshtastic_Config_DisplayConfig_OledType model; + OLEDDISPLAY_GEOMETRY geometry; - bool isOverlayBannerShowing(); + bool isOverlayBannerShowing(); - bool isScreenOn() { return screenOn; } + bool isScreenOn() { return screenOn; } - // Stores the last 4 of our hardware ID, to make finding the device for pairing easier - // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class - char ourId[5]; + // Stores the last 4 of our hardware ID, to make finding the device for pairing easier + // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class + char ourId[5]; - /// Initializes the UI, turns on the display, starts showing boot screen. - // - // Not thread safe - must be called before any other methods are called. - void setup(); + /// Initializes the UI, turns on the display, starts showing boot screen. + // + // Not thread safe - must be called before any other methods are called. + void setup(); - /// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink - void setOn(bool on, FrameCallback einkScreensaver = NULL); - /** - * Prepare the display for the unit going to the lowest power mode possible. Most screens will just - * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code - */ - void doDeepSleep(); + /// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink + void setOn(bool on, FrameCallback einkScreensaver = NULL); + /** + * Prepare the display for the unit going to the lowest power mode possible. Most screens will just + * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code + */ + void doDeepSleep(); - void blink(); + void blink(); - // Draw north - float estimatedHeading(double lat, double lon); + // Draw north + float estimatedHeading(double lat, double lon); - /// Handle button press, trackball or swipe action) - void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } - void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } - void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } - void showFrame(FrameDirection direction); + /// Handle button press, trackball or swipe action) + void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } + void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } + void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } + void showFrame(FrameDirection direction); - // generic alert start - void startAlert(FrameCallback _alertFrame) { - alertFrame = _alertFrame; - ScreenCmd cmd; - cmd.cmd = Cmd::START_ALERT_FRAME; - enqueueCmd(cmd); - } - - void startAlert(const char *_alertMessage) { - startAlert([_alertMessage](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, _alertMessage); - }); - } - - void endAlert() { - ScreenCmd cmd; - cmd.cmd = Cmd::STOP_ALERT_FRAME; - enqueueCmd(cmd); - } - - void showSimpleBanner(const char *message, uint32_t durationMs = 0); - void showOverlayBanner(BannerOverlayOptions); - - void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); - void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); - void showTextInput(const char *header, const char *initialText, uint32_t durationMs, std::function textCallback); - - void requestMenu(graphics::menuHandler::screenMenus menuToShow) { - graphics::menuHandler::menuQueue = menuToShow; - runNow(); - } - - void startFirmwareUpdateScreen() { - ScreenCmd cmd; - cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; - enqueueCmd(cmd); - } - - // Function to allow the AccelerometerThread to set the heading if a sensor provides it - // Mutex needed? - void setHeading(long _heading) { - hasCompass = true; - compassHeading = fmod(_heading, 360); - } - - bool hasHeading() { return hasCompass; } - - long getHeading() { return compassHeading; } - - void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } - uint32_t getEndCalibration() { return endCalibrationAt; } - - // functions for display brightness - void increaseBrightness(); - void decreaseBrightness(); - - /// Stops showing the boot screen. - void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } - - void runNow() { - setFastFramerate(); - enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP}); - } - - /// Overrides the default utf8 character conversion, to replace empty space with question marks - static char customFontTableLookup(const uint8_t ch) { - // UTF-8 to font table index converter - // Code from http://playground.arduino.cc/Main/Utf8ascii - static uint8_t LASTCHAR; - static bool SKIPREST; // Only display a single unconvertable-character symbol per sequence of unconvertable characters - - if (ch < 128) { // Standard ASCII-set 0..0x7F handling - LASTCHAR = 0; - SKIPREST = false; - return ch; + // generic alert start + void startAlert(FrameCallback _alertFrame) + { + alertFrame = _alertFrame; + ScreenCmd cmd; + cmd.cmd = Cmd::START_ALERT_FRAME; + enqueueCmd(cmd); } - uint8_t last = LASTCHAR; // get last char - LASTCHAR = ch; - - switch (last) { - case 0xC2: { - SKIPREST = false; - return (uint8_t)ch; + void startAlert(const char *_alertMessage) + { + startAlert([_alertMessage](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, _alertMessage); + }); } - case 0xC3: { - SKIPREST = false; - return (uint8_t)(ch | 0xC0); - } + void endAlert() + { + ScreenCmd cmd; + cmd.cmd = Cmd::STOP_ALERT_FRAME; + enqueueCmd(cmd); } - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3) - return (uint8_t)0; + void showSimpleBanner(const char *message, uint32_t durationMs = 0); + void showOverlayBanner(BannerOverlayOptions); + + void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); + void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); + void showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback); + + void requestMenu(graphics::menuHandler::screenMenus menuToShow) + { + graphics::menuHandler::menuQueue = menuToShow; + runNow(); + } + + void startFirmwareUpdateScreen() + { + ScreenCmd cmd; + cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; + enqueueCmd(cmd); + } + + // Function to allow the AccelerometerThread to set the heading if a sensor provides it + // Mutex needed? + void setHeading(long _heading) + { + hasCompass = true; + compassHeading = fmod(_heading, 360); + } + + bool hasHeading() { return hasCompass; } + + long getHeading() { return compassHeading; } + + void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } + uint32_t getEndCalibration() { return endCalibrationAt; } + + // functions for display brightness + void increaseBrightness(); + void decreaseBrightness(); + + /// Stops showing the boot screen. + void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } + + void runNow() + { + setFastFramerate(); + enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP}); + } + + /// Overrides the default utf8 character conversion, to replace empty space with question marks + static char customFontTableLookup(const uint8_t ch) + { + // UTF-8 to font table index converter + // Code from http://playground.arduino.cc/Main/Utf8ascii + static uint8_t LASTCHAR; + static bool SKIPREST; // Only display a single unconvertable-character symbol per sequence of unconvertable characters + + if (ch < 128) { // Standard ASCII-set 0..0x7F handling + LASTCHAR = 0; + SKIPREST = false; + return ch; + } + + uint8_t last = LASTCHAR; // get last char + LASTCHAR = ch; + + switch (last) { + case 0xC2: { + SKIPREST = false; + return (uint8_t)ch; + } + + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } + } + + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3) + return (uint8_t)0; #if defined(OLED_PL) - switch (last) { - case 0xC3: { + switch (last) { + case 0xC3: { - if (ch == 147) - return (uint8_t)(ch); // Ó - else if (ch == 179) - return (uint8_t)(148); // ó - else - return (uint8_t)(ch | 0xC0); - break; - } + if (ch == 147) + return (uint8_t)(ch); // Ó + else if (ch == 179) + return (uint8_t)(148); // ó + else + return (uint8_t)(ch | 0xC0); + break; + } - case 0xC4: { - SKIPREST = false; - return (uint8_t)(ch); - } + case 0xC4: { + SKIPREST = false; + return (uint8_t)(ch); + } - case 0xC5: { - SKIPREST = false; - if (ch == 132) - return (uint8_t)(136); // ń - else if (ch == 186) - return (uint8_t)(137); // ź - else - return (uint8_t)(ch); - break; - } - } + case 0xC5: { + SKIPREST = false; + if (ch == 132) + return (uint8_t)(136); // ń + else if (ch == 186) + return (uint8_t)(137); // ź + else + return (uint8_t)(ch); + break; + } + } - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) - return (uint8_t)0; + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) + return (uint8_t)0; #endif #if defined(OLED_UA) || defined(OLED_RU) - switch (last) { - case 0xC3: { - SKIPREST = false; - return (uint8_t)(ch | 0xC0); - } - // map UTF-8 cyrillic chars to it Windows-1251 (CP-1251) ASCII codes - // note: in this case we must use compatible font - provided ArialMT_Plain_10/16/24 by - // 'ThingPulse/esp8266-oled-ssd1306' library have empty chars for non-latin ASCII symbols - case 0xD0: { - SKIPREST = false; - if (ch == 132) - return (uint8_t)(170); // Є - if (ch == 134) - return (uint8_t)(178); // І - if (ch == 135) - return (uint8_t)(175); // Ї - if (ch == 129) - return (uint8_t)(168); // Ё - if (ch > 143 && ch < 192) - return (uint8_t)(ch + 48); - break; - } - case 0xD1: { - SKIPREST = false; - if (ch == 148) - return (uint8_t)(186); // є - if (ch == 150) - return (uint8_t)(179); // і - if (ch == 151) - return (uint8_t)(191); // ї - if (ch == 145) - return (uint8_t)(184); // ё - if (ch > 127 && ch < 144) - return (uint8_t)(ch + 112); - break; - } - case 0xD2: { - SKIPREST = false; - if (ch == 144) - return (uint8_t)(165); // Ґ - if (ch == 145) - return (uint8_t)(180); // ґ - break; - } - } + switch (last) { + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } + // map UTF-8 cyrillic chars to it Windows-1251 (CP-1251) ASCII codes + // note: in this case we must use compatible font - provided ArialMT_Plain_10/16/24 by 'ThingPulse/esp8266-oled-ssd1306' + // library have empty chars for non-latin ASCII symbols + case 0xD0: { + SKIPREST = false; + if (ch == 132) + return (uint8_t)(170); // Є + if (ch == 134) + return (uint8_t)(178); // І + if (ch == 135) + return (uint8_t)(175); // Ї + if (ch == 129) + return (uint8_t)(168); // Ё + if (ch > 143 && ch < 192) + return (uint8_t)(ch + 48); + break; + } + case 0xD1: { + SKIPREST = false; + if (ch == 148) + return (uint8_t)(186); // є + if (ch == 150) + return (uint8_t)(179); // і + if (ch == 151) + return (uint8_t)(191); // ї + if (ch == 145) + return (uint8_t)(184); // ё + if (ch > 127 && ch < 144) + return (uint8_t)(ch + 112); + break; + } + case 0xD2: { + SKIPREST = false; + if (ch == 144) + return (uint8_t)(165); // Ґ + if (ch == 145) + return (uint8_t)(180); // ґ + break; + } + } - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3 || ch == 0x82 || ch == 0xD0 || ch == 0xD1) - return (uint8_t)0; + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0x82 || ch == 0xD0 || ch == 0xD1) + return (uint8_t)0; #endif #if defined(OLED_CS) - switch (last) { - case 0xC2: { - SKIPREST = false; - return (uint8_t)ch; - } + switch (last) { + case 0xC2: { + SKIPREST = false; + return (uint8_t)ch; + } - case 0xC3: { - SKIPREST = false; - return (uint8_t)(ch | 0xC0); - } + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } - case 0xC4: { - SKIPREST = false; - if (ch == 140) - return (uint8_t)(129); // Č - if (ch == 141) - return (uint8_t)(138); // č - if (ch == 142) - return (uint8_t)(130); // Ď - if (ch == 143) - return (uint8_t)(139); // ď - if (ch == 154) - return (uint8_t)(131); // Ě - if (ch == 155) - return (uint8_t)(140); // ě - // Slovak specific glyphs - if (ch == 185) - return (uint8_t)(147); // Ĺ - if (ch == 186) - return (uint8_t)(148); // ĺ - if (ch == 189) - return (uint8_t)(149); // Ľ - if (ch == 190) - return (uint8_t)(150); // ľ - break; - } + case 0xC4: { + SKIPREST = false; + if (ch == 140) + return (uint8_t)(129); // Č + if (ch == 141) + return (uint8_t)(138); // č + if (ch == 142) + return (uint8_t)(130); // Ď + if (ch == 143) + return (uint8_t)(139); // ď + if (ch == 154) + return (uint8_t)(131); // Ě + if (ch == 155) + return (uint8_t)(140); // ě + // Slovak specific glyphs + if (ch == 185) + return (uint8_t)(147); // Ĺ + if (ch == 186) + return (uint8_t)(148); // ĺ + if (ch == 189) + return (uint8_t)(149); // Ľ + if (ch == 190) + return (uint8_t)(150); // ľ + break; + } - case 0xC5: { - SKIPREST = false; - if (ch == 135) - return (uint8_t)(132); // Ň - if (ch == 136) - return (uint8_t)(141); // ň - if (ch == 152) - return (uint8_t)(133); // Ř - if (ch == 153) - return (uint8_t)(142); // ř - if (ch == 160) - return (uint8_t)(134); // Š - if (ch == 161) - return (uint8_t)(143); // š - if (ch == 164) - return (uint8_t)(135); // Ť - if (ch == 165) - return (uint8_t)(144); // ť - if (ch == 174) - return (uint8_t)(136); // Ů - if (ch == 175) - return (uint8_t)(145); // ů - if (ch == 189) - return (uint8_t)(137); // Ž - if (ch == 190) - return (uint8_t)(146); // ž - // Slovak specific glyphs - if (ch == 148) - return (uint8_t)(151); // Ŕ - if (ch == 149) - return (uint8_t)(152); // ŕ - break; - } - } + case 0xC5: { + SKIPREST = false; + if (ch == 135) + return (uint8_t)(132); // Ň + if (ch == 136) + return (uint8_t)(141); // ň + if (ch == 152) + return (uint8_t)(133); // Ř + if (ch == 153) + return (uint8_t)(142); // ř + if (ch == 160) + return (uint8_t)(134); // Š + if (ch == 161) + return (uint8_t)(143); // š + if (ch == 164) + return (uint8_t)(135); // Ť + if (ch == 165) + return (uint8_t)(144); // ť + if (ch == 174) + return (uint8_t)(136); // Ů + if (ch == 175) + return (uint8_t)(145); // ů + if (ch == 189) + return (uint8_t)(137); // Ž + if (ch == 190) + return (uint8_t)(146); // ž + // Slovak specific glyphs + if (ch == 148) + return (uint8_t)(151); // Ŕ + if (ch == 149) + return (uint8_t)(152); // ŕ + break; + } + } - // We want to strip out prefix chars for two-byte char formats - if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) - return (uint8_t)0; + // We want to strip out prefix chars for two-byte char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) + return (uint8_t)0; #endif - // If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs - // for the rest of it - if (SKIPREST) - return (uint8_t)0; - SKIPREST = true; + // If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the + // rest of it + if (SKIPREST) + return (uint8_t)0; + SKIPREST = true; - return (uint8_t)191; // otherwise: return ¿ if character can't be converted (note that the font map we're using - // doesn't stick to standard EASCII codes) - } + return (uint8_t)191; // otherwise: return ¿ if character can't be converted (note that the font map we're using doesn't + // stick to standard EASCII codes) + } - /// Returns a handle to the DebugInfo screen. - // - // Use this handle to set things like battery status, user count, GPS status, etc. - DebugInfo *debug_info() { return &debugInfo; } + /// Returns a handle to the DebugInfo screen. + // + // Use this handle to set things like battery status, user count, GPS status, etc. + DebugInfo *debug_info() { return &debugInfo; } - // Handle observer events - int handleStatusUpdate(const meshtastic::Status *arg); - int handleTextMessage(const meshtastic_MeshPacket *packet); - int handleUIFrameEvent(const UIFrameEvent *arg); - int handleInputEvent(const InputEvent *arg); - int handleAdminMessage(AdminModule_ObserverData *arg); + // Handle observer events + int handleStatusUpdate(const meshtastic::Status *arg); + int handleTextMessage(const meshtastic_MeshPacket *packet); + int handleUIFrameEvent(const UIFrameEvent *arg); + int handleInputEvent(const InputEvent *arg); + int handleAdminMessage(AdminModule_ObserverData *arg); - /// Used to force (super slow) eink displays to draw critical frames - void forceDisplay(bool forceUiUpdate = false); + /// Used to force (super slow) eink displays to draw critical frames + void forceDisplay(bool forceUiUpdate = false); - /// Draws our SSL cert screen during boot (called from WebServer) - void setSSLFrames(); + /// Draws our SSL cert screen during boot (called from WebServer) + void setSSLFrames(); - // Menu-driven Show / Hide Toggle - void toggleFrameVisibility(const std::string &frameName); - bool isFrameHidden(const std::string &frameName) const; + // Menu-driven Show / Hide Toggle + void toggleFrameVisibility(const std::string &frameName); + bool isFrameHidden(const std::string &frameName) const; #ifdef USE_EINK - /// Draw an image to remain on E-Ink display after screen off - void setScreensaverFrames(FrameCallback einkScreensaver = NULL); + /// Draw an image to remain on E-Ink display after screen off + void setScreensaverFrames(FrameCallback einkScreensaver = NULL); #endif -protected: - /// Updates the UI. - // - // Called periodically from the main loop. - int32_t runOnce() final; + protected: + /// Updates the UI. + // + // Called periodically from the main loop. + int32_t runOnce() final; - bool isAUTOOled = false; + bool isAUTOOled = false; - // Screen dimensions (for convenience) - // Defined during Screen::setup - uint16_t displayWidth = 0; - uint16_t displayHeight = 0; + // Screen dimensions (for convenience) + // Defined during Screen::setup + uint16_t displayWidth = 0; + uint16_t displayHeight = 0; -private: - FrameCallback alertFrames[1]; - struct ScreenCmd { - Cmd cmd; - union { - uint32_t bluetooth_pin; - char *print_text; + private: + FrameCallback alertFrames[1]; + struct ScreenCmd { + Cmd cmd; + union { + uint32_t bluetooth_pin; + char *print_text; + }; }; - }; - /// Enques given command item to be processed by main loop(). - bool enqueueCmd(const ScreenCmd &cmd) { - if (!useDisplay) - return false; // not enqueued if our display is not in use - else { - bool success = cmdQueue.enqueue(cmd, 0); - enabled = true; // handle ASAP (we are the registered reader for cmdQueue, but might have been disabled) - return success; + /// Enques given command item to be processed by main loop(). + bool enqueueCmd(const ScreenCmd &cmd) + { + if (!useDisplay) + return false; // not enqueued if our display is not in use + else { + bool success = cmdQueue.enqueue(cmd, 0); + enabled = true; // handle ASAP (we are the registered reader for cmdQueue, but might have been disabled) + return success; + } } - } - // Implementations of various commands, called from doTask(). - void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); - void handleOnPress(); - void handleStartFirmwareUpdateScreen(); + // Implementations of various commands, called from doTask(). + void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); + void handleOnPress(); + void handleStartFirmwareUpdateScreen(); - // Info collected by setFrames method. - // Index location of specific frames. - // - Used to apply the FrameFocus parameter of setFrames - // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo - struct FramesetInfo { - struct FramePositions { - uint8_t fault = 255; - uint8_t waypoint = 255; - uint8_t focusedModule = 255; - uint8_t log = 255; - uint8_t settings = 255; - uint8_t wifi = 255; - uint8_t deviceFocused = 255; - uint8_t system = 255; - uint8_t gps = 255; - uint8_t home = 255; - uint8_t textMessage = 255; - uint8_t nodelist_nodes = 255; - uint8_t nodelist_location = 255; - uint8_t nodelist_lastheard = 255; - uint8_t nodelist_hopsignal = 255; - uint8_t nodelist_distance = 255; - uint8_t nodelist_bearings = 255; - uint8_t clock = 255; - uint8_t chirpy = 255; - uint8_t firstFavorite = 255; - uint8_t lastFavorite = 255; - uint8_t lora = 255; - } positions; + // Info collected by setFrames method. + // Index location of specific frames. + // - Used to apply the FrameFocus parameter of setFrames + // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo + struct FramesetInfo { + struct FramePositions { + uint8_t fault = 255; + uint8_t waypoint = 255; + uint8_t focusedModule = 255; + uint8_t log = 255; + uint8_t settings = 255; + uint8_t wifi = 255; + uint8_t deviceFocused = 255; + uint8_t system = 255; + uint8_t gps = 255; + uint8_t home = 255; + uint8_t textMessage = 255; + uint8_t nodelist_nodes = 255; + uint8_t nodelist_location = 255; + uint8_t nodelist_lastheard = 255; + uint8_t nodelist_hopsignal = 255; + uint8_t nodelist_distance = 255; + uint8_t nodelist_bearings = 255; + uint8_t clock = 255; + uint8_t chirpy = 255; + uint8_t firstFavorite = 255; + uint8_t lastFavorite = 255; + uint8_t lora = 255; + } positions; - uint8_t frameCount = 0; - } framesetInfo; + uint8_t frameCount = 0; + } framesetInfo; - struct hiddenFrames { - bool textMessage = false; - bool waypoint = false; - bool wifi = false; - bool system = false; - bool home = false; - bool clock = false; + struct hiddenFrames { + bool textMessage = false; + bool waypoint = false; + bool wifi = false; + bool system = false; + bool home = false; + bool clock = false; #ifndef USE_EINK - bool nodelist_nodes = false; - bool nodelist_location = false; + bool nodelist_nodes = false; + bool nodelist_location = false; #endif #ifdef USE_EINK - bool nodelist_lastheard = false; - bool nodelist_hopsignal = false; - bool nodelist_distance = false; + bool nodelist_lastheard = false; + bool nodelist_hopsignal = false; + bool nodelist_distance = false; #endif #if HAS_GPS #ifdef USE_EINK - bool nodelist_bearings = false; + bool nodelist_bearings = false; #endif - bool gps = false; + bool gps = false; #endif - bool lora = false; - bool show_favorites = false; - bool chirpy = true; - } hiddenFrames; + bool lora = false; + bool show_favorites = false; + bool chirpy = true; + } hiddenFrames; - /// Try to start drawing ASAP - void setFastFramerate(); + /// Try to start drawing ASAP + void setFastFramerate(); - // Sets frame up for immediate drawing - void setFrameImmediateDraw(FrameCallback *drawFrames); + // Sets frame up for immediate drawing + void setFrameImmediateDraw(FrameCallback *drawFrames); - /// callback for current alert frame - FrameCallback alertFrame; + /// callback for current alert frame + FrameCallback alertFrame; - /// Queue of commands to execute in doTask. - TypedQueue cmdQueue; - /// Whether we are using a display - bool useDisplay = false; - /// Whether the display is currently powered - bool screenOn = false; - // Whether we are showing the regular screen (as opposed to booth screen or - // Bluetooth PIN screen) - bool showingNormalScreen = false; + /// Queue of commands to execute in doTask. + TypedQueue cmdQueue; + /// Whether we are using a display + bool useDisplay = false; + /// Whether the display is currently powered + bool screenOn = false; + // Whether we are showing the regular screen (as opposed to booth screen or + // Bluetooth PIN screen) + bool showingNormalScreen = false; - // Implementation to Adjust Brightness - uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 + // Implementation to Adjust Brightness + uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 - bool hasCompass = false; - float compassHeading; - uint32_t endCalibrationAt; + bool hasCompass = false; + float compassHeading; + uint32_t endCalibrationAt; - /// Holds state for debug information - DebugInfo debugInfo; + /// Holds state for debug information + DebugInfo debugInfo; - /// Display device - OLEDDisplay *dispdev; + /// Display device + OLEDDisplay *dispdev; - /// UI helper for rendering to frames and switching between them - OLEDDisplayUi *ui; + /// UI helper for rendering to frames and switching between them + OLEDDisplayUi *ui; }; } // namespace graphics diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index ff085d7e9..d54fc9958 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -72,9 +72,9 @@ #endif #endif -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ - defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ - defined(USE_ST7796)) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 3cf693184..8f06fcf9f 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -13,43 +13,46 @@ #include #include -namespace graphics { +namespace graphics +{ -ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) { +ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) +{ #ifdef FORCE_LOW_RES - return ScreenResolution::Low; -#else - // Unit C6L and other ultra low res screens - if (screenwidth <= 64 || screenheight <= 48) { - return ScreenResolution::UltraLow; - } - - // Standard OLED screens - if (screenwidth > 128 && screenheight <= 64) { return ScreenResolution::Low; - } +#else + // Unit C6L and other ultra low res screens + if (screenwidth <= 64 || screenheight <= 48) { + return ScreenResolution::UltraLow; + } - // High Resolutions screens like T114, TDeck, TLora Pager, etc - if (screenwidth > 128) { - return ScreenResolution::High; - } + // Standard OLED screens + if (screenwidth > 128 && screenheight <= 64) { + return ScreenResolution::Low; + } - // Default to low resolution - return ScreenResolution::Low; + // High Resolutions screens like T114, TDeck, TLora Pager, etc + if (screenwidth > 128) { + return ScreenResolution::High; + } + + // Default to low resolution + return ScreenResolution::Low; #endif } -void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) { - hour = 0; - minute = 0; - second = 0; - if (rtc_sec == 0) - return; - uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - hour = hms / SEC_PER_HOUR; - minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - second = hms % SEC_PER_MIN; +void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) +{ + hour = 0; + minute = 0; + second = 0; + if (rtc_sec == 0) + return; + uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = hms % SEC_PER_MIN; } // === Shared External State === @@ -65,448 +68,457 @@ uint32_t lastMailBlink = 0; // ********************************* // * Rounded Header when inverted * // ********************************* -void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { - // Draw the center and side rectangles - display->fillRect(x + r, y, w - 2 * r, h); // center bar - display->fillRect(x, y + r, r, h - 2 * r); // left edge - display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) +{ + // Draw the center and side rectangles + display->fillRect(x + r, y, w - 2 * r, h); // center bar + display->fillRect(x, y + r, r, h - 2 * r); // left edge + display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge - // Draw the rounded corners using filled circles - display->fillCircle(x + r + 1, y + r, r); // top-left - display->fillCircle(x + w - r - 1, y + r, r); // top-right - display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left - display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right + // Draw the rounded corners using filled circles + display->fillCircle(x + r + 1, y + r, r); // top-left + display->fillCircle(x + w - r - 1, y + r, r); // top-right + display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right } // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) { - constexpr int HEADER_OFFSET_Y = 1; - y += HEADER_OFFSET_Y; +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) +{ + constexpr int HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); - const int xOffset = 4; - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - const bool isBold = config.display.heading_bold; + const int xOffset = 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); - if (!force_no_invert) { - // === Inverted Header Background === - if (isInverted) { - display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 2); - display->setColor(WHITE); - drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 2); - display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - display->drawLine(0, 20, screenW, 20); - } else { - display->drawLine(0, 14, screenW, 14); - } + if (!force_no_invert) { + // === Inverted Header Background === + if (isInverted) { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } + } + + // === Screen Title === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(SCREEN_WIDTH / 2, y, titleStr); + if (config.display.heading_bold) { + display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + } + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Battery State === + int chargePercent = powerStatus->getBatteryChargePercent(); + bool isCharging = powerStatus->getIsCharging(); + bool usbPowered = powerStatus->getHasUSB(); + + if (chargePercent >= 100) { + isCharging = false; + } + if (chargePercent == 101) { + usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable + // plugged in } - // === Screen Title === - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(SCREEN_WIDTH / 2, y, titleStr); - if (config.display.heading_bold) { - display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); - } - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Battery State === - int chargePercent = powerStatus->getBatteryChargePercent(); - bool isCharging = powerStatus->getIsCharging(); - bool usbPowered = powerStatus->getHasUSB(); - - if (chargePercent >= 100) { - isCharging = false; - } - if (chargePercent == 101) { - usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB - // cable plugged in - } - - uint32_t now = millis(); + uint32_t now = millis(); #ifndef USE_EINK - if (isCharging && now - lastBlinkShared > 500) { - isBoltVisibleShared = !isBoltVisibleShared; - lastBlinkShared = now; - } + if (isCharging && now - lastBlinkShared > 500) { + isBoltVisibleShared = !isBoltVisibleShared; + lastBlinkShared = now; + } #endif - bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - int batteryX = 1; - int batteryY = HEADER_OFFSET_Y + 1; + int batteryX = 1; + int batteryY = HEADER_OFFSET_Y + 1; #if !defined(M5STACK_UNITC6L) - // === Battery Icons === - if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging - batteryX += 1; - batteryY += 2; - if (currentResolution == ScreenResolution::High) { - display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); - batteryX += 20; // Icon + 1 pixel - } else { - display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); - batteryX += 11; // Icon + 1 pixel - } - } else { - if (useHorizontalBattery) { - batteryX += 1; - batteryY += 2; - display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); - display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); - if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); - else { - display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); - display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); - int fillWidth = 14 * chargePercent / 100; - display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); - } - batteryX += 18; // Icon + 2 pixels + // === Battery Icons === + if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging + batteryX += 1; + batteryY += 2; + if (currentResolution == ScreenResolution::High) { + display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); + batteryX += 20; // Icon + 1 pixel + } else { + display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); + batteryX += 11; // Icon + 1 pixel + } } else { + if (useHorizontalBattery) { + batteryX += 1; + batteryY += 2; + display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); + display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); + else { + display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); + display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); + int fillWidth = 14 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); + } + batteryX += 18; // Icon + 2 pixels + } else { #ifdef USE_EINK - batteryY += 2; + batteryY += 2; #endif - display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); - if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); - else { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); - int fillHeight = 8 * chargePercent / 100; - int fillY = batteryY - fillHeight; - display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); - } - batteryX += 9; // Icon + 2 pixels - } - } - - if (chargePercent != 101) { - // === Battery % Display === - char chargeStr[4]; - snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); - int chargeNumWidth = display->getStringWidth(chargeStr); - display->drawString(batteryX, textY, chargeStr); - display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); - if (isBold) { - display->drawString(batteryX + 1, textY, chargeStr); - display->drawString(batteryX + chargeNumWidth, textY, "%"); - } - } - - // === Time and Right-aligned Icons === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char timeStr[10] = "--:--"; // Fallback display - int timeStrWidth = display->getStringWidth("12:34"); // Default alignment - int timeX = screenW - xOffset - timeStrWidth + 4; - - if (rtc_sec > 0) { - // === Build Time String === - long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - int hour, minute, second; - graphics::decomposeTime(rtc_sec, hour, minute, second); - snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); - - // === Build Date String === - char datetimeStr[25]; - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); - char dateLine[40]; - - if (currentResolution == ScreenResolution::High) { - snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); - } else { - if (hasUnreadMessage) { - snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); - } else { - snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); - } + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); + else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + } + batteryX += 9; // Icon + 2 pixels + } } - if (config.display.use_12h_clock) { - bool isPM = hour >= 12; - hour %= 12; - if (hour == 0) - hour = 12; - snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + if (chargePercent != 101) { + // === Battery % Display === + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); + display->drawString(batteryX, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); + if (isBold) { + display->drawString(batteryX + 1, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth, textY, "%"); + } } - if (show_date) { - timeStrWidth = display->getStringWidth(dateLine); - } else { - timeStrWidth = display->getStringWidth(timeStr); - } - timeX = screenW - xOffset - timeStrWidth + 3; + // === Time and Right-aligned Icons === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char timeStr[10] = "--:--"; // Fallback display + int timeStrWidth = display->getStringWidth("12:34"); // Default alignment + int timeX = screenW - xOffset - timeStrWidth + 4; - // === Show Mail or Mute Icon to the Left of Time === - int iconRightEdge = timeX - 2; + if (rtc_sec > 0) { + // === Build Time String === + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + int hour, minute, second; + graphics::decomposeTime(rtc_sec, hour, minute, second); + snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); - bool showMail = false; + // === Build Date String === + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char dateLine[40]; + + if (currentResolution == ScreenResolution::High) { + snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); + } else { + if (hasUnreadMessage) { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); + } else { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); + } + } + + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + hour %= 12; + if (hour == 0) + hour = 12; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + } + + if (show_date) { + timeStrWidth = display->getStringWidth(dateLine); + } else { + timeStrWidth = display->getStringWidth(timeStr); + } + timeX = screenW - xOffset - timeStrWidth + 3; + + // === Show Mail or Mute Icon to the Left of Time === + int iconRightEdge = timeX - 2; + + bool showMail = false; #ifndef USE_EINK - if (hasUnreadMessage) { - if (now - lastMailBlink > 500) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; - } - showMail = isMailIconVisible; - } + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } #else - if (hasUnreadMessage) { - showMail = true; - } + if (hasUnreadMessage) { + showMail = true; + } #endif - if (showMail) { - if (useHorizontalBattery) { - int iconW = 16, iconH = 12; - int iconX = iconRightEdge - iconW; - int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); - display->setColor(WHITE); - } - display->drawRect(iconX, iconY, iconW + 1, iconH); - display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); - display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); - } else { - int iconX = iconRightEdge - (mail_width - 2); - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); - } - } else if (externalNotificationModule->getMute()) { - if (currentResolution == ScreenResolution::High) { - int iconX = iconRightEdge - mute_symbol_big_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(WHITE); + } + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - (mail_width - 2); + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); - } else { - int iconX = iconRightEdge - mute_symbol_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted && !force_no_invert) { - display->setColor(WHITE); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); - display->setColor(WHITE); + if (isInverted && !force_no_invert) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + + if (show_date) { + // === Draw Date === + display->drawString(timeX, textY, dateLine); + if (isBold) + display->drawString(timeX - 1, textY, dateLine); + } else { + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); } - display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); - } - } - if (show_date) { - // === Draw Date === - display->drawString(timeX, textY, dateLine); - if (isBold) - display->drawString(timeX - 1, textY, dateLine); } else { - // === Draw Time === - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); - } + // === No Time Available: Mail/Mute Icon Moves to Far Right === + int iconRightEdge = screenW - xOffset; - } else { - // === No Time Available: Mail/Mute Icon Moves to Far Right === - int iconRightEdge = screenW - xOffset; - - bool showMail = false; + bool showMail = false; #ifndef USE_EINK - if (hasUnreadMessage) { - if (now - lastMailBlink > 500) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; - } - showMail = isMailIconVisible; - } + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } #else - if (hasUnreadMessage) { - showMail = true; - } + if (hasUnreadMessage) { + showMail = true; + } #endif - if (showMail) { - if (useHorizontalBattery) { - int iconW = 16, iconH = 12; - int iconX = iconRightEdge - iconW; - int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - display->drawRect(iconX, iconY, iconW + 1, iconH); - display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); - display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); - } else { - int iconX = iconRightEdge - mail_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); - } - } else if (externalNotificationModule->getMute()) { - if (currentResolution == ScreenResolution::High) { - int iconX = iconRightEdge - mute_symbol_big_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); - } else { - int iconX = iconRightEdge - mute_symbol_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); - } + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } } - } #endif - display->setColor(WHITE); // Reset for other UI + display->setColor(WHITE); // Reset for other UI } -const int *getTextPositions(OLEDDisplay *display) { - static int textPositions[7]; // Static array that persists beyond function scope +const int *getTextPositions(OLEDDisplay *display) +{ + static int textPositions[7]; // Static array that persists beyond function scope - if (currentResolution == ScreenResolution::High) { - textPositions[0] = textZeroLine; - textPositions[1] = textFirstLine_medium; - textPositions[2] = textSecondLine_medium; - textPositions[3] = textThirdLine_medium; - textPositions[4] = textFourthLine_medium; - textPositions[5] = textFifthLine_medium; - textPositions[6] = textSixthLine_medium; - } else { - textPositions[0] = textZeroLine; - textPositions[1] = textFirstLine; - textPositions[2] = textSecondLine; - textPositions[3] = textThirdLine; - textPositions[4] = textFourthLine; - textPositions[5] = textFifthLine; - textPositions[6] = textSixthLine; - } - return textPositions; + if (currentResolution == ScreenResolution::High) { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine_medium; + textPositions[2] = textSecondLine_medium; + textPositions[3] = textThirdLine_medium; + textPositions[4] = textFourthLine_medium; + textPositions[5] = textFifthLine_medium; + textPositions[6] = textSixthLine_medium; + } else { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine; + textPositions[2] = textSecondLine; + textPositions[3] = textThirdLine; + textPositions[4] = textFourthLine; + textPositions[5] = textFifthLine; + textPositions[6] = textSixthLine; + } + return textPositions; } // ************************* // * Common Footer Drawing * // ************************* -void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) { - bool drawConnectionState = false; - if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || service->api_state == service->STATE_SERIAL || - service->api_state == service->STATE_PACKET || service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) { - drawConnectionState = true; - } +void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) +{ + bool drawConnectionState = false; + if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || + service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET || + service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) { + drawConnectionState = true; + } - if (drawConnectionState) { - const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; - display->setColor(BLACK); - display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), - (connection_icon_height * scale) + (2 * scale)); - display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - const int bytesPerRow = (connection_icon_width + 7) / 8; - int iconX = 0; - int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); + if (drawConnectionState) { + const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; + display->setColor(BLACK); + display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), + (connection_icon_height * scale) + (2 * scale)); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { + const int bytesPerRow = (connection_icon_width + 7) / 8; + int iconX = 0; + int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); - for (int yy = 0; yy < connection_icon_height; ++yy) { - const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; - for (int xx = 0; xx < connection_icon_width; ++xx) { - const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); - const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first - if (byteVal & bitMask) { - display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); - } + for (int yy = 0; yy < connection_icon_height; ++yy) { + const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; + for (int xx = 0; xx < connection_icon_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); + } + } + } + + } else { + display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, + connection_icon); } - } - - } else { - display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, connection_icon); } - } } -bool isAllowedPunctuation(char c) { - const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; - return allowed.find(c) != std::string::npos; +bool isAllowedPunctuation(char c) +{ + const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; + return allowed.find(c) != std::string::npos; } -static void replaceAll(std::string &s, const std::string &from, const std::string &to) { - if (from.empty()) - return; - size_t pos = 0; - while ((pos = s.find(from, pos)) != std::string::npos) { - s.replace(pos, from.size(), to); - pos += to.size(); - } -} - -std::string sanitizeString(const std::string &input) { - std::string output; - bool inReplacement = false; - - // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. - std::string s = input; - - // Curly single quotes: ‘ ’ - replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 - replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 - - // Curly double quotes: “ ” - replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C - replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D - - // En dash / Em dash: – — - replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 - replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 - - // Non-breaking space - replaceAll(s, "\xC2\xA0", " "); // U+00A0 - - // Now do your original sanitize pass over the normalized string. - for (unsigned char uc : s) { - char c = static_cast(uc); - if (std::isalnum(uc) || isAllowedPunctuation(c)) { - output += c; - inReplacement = false; - } else { - if (!inReplacement) { - output += static_cast(0xBF); // ISO-8859-1 for inverted question mark - inReplacement = true; - } +static void replaceAll(std::string &s, const std::string &from, const std::string &to) +{ + if (from.empty()) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.size(), to); + pos += to.size(); } - } +} - return output; +std::string sanitizeString(const std::string &input) +{ + std::string output; + bool inReplacement = false; + + // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. + std::string s = input; + + // Curly single quotes: ‘ ’ + replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 + replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 + + // Curly double quotes: “ ” + replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C + replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D + + // En dash / Em dash: – — + replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 + replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 + + // Non-breaking space + replaceAll(s, "\xC2\xA0", " "); // U+00A0 + + // Now do your original sanitize pass over the normalized string. + for (unsigned char uc : s) { + char c = static_cast(uc); + if (std::isalnum(uc) || isAllowedPunctuation(c)) { + output += c; + inReplacement = false; + } else { + if (!inReplacement) { + output += static_cast(0xBF); // ISO-8859-1 for inverted question mark + inReplacement = true; + } + } + } + + return output; } } // namespace graphics diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 6af4ff9d0..a8ecdfada 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -3,7 +3,8 @@ #include #include -namespace graphics { +namespace graphics +{ // ======================= // Shared UI Helpers @@ -50,7 +51,8 @@ void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, bool show_date = false); +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, + bool show_date = false); // Shared battery/time/mail header void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 6ca766514..12fac4f34 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -29,82 +29,85 @@ uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #define TFT_INVERT true #endif -class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_ST7735S _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7735S _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; -public: - LGFX(void) { + public: + LGFX(void) { - auto cfg = _bus_instance.config(); + { + auto cfg = _bus_instance.config(); - // configure SPI - cfg.spi_host = ST7735_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by - // dividing 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ST7735_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ST7735_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ST7735_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ST7735_RS; // Set SPI DC pin number (-1 = disable) + // configure SPI + cfg.spi_host = ST7735_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7735_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7735_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7735_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7735_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ST7735_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = ST7735_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = ST7735_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ST7735_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7735_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7735_BUSY; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef TFT_BL - // Set the backlight control - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected - cfg.invert = true; // true to invert the brightness of the backlight - // cfg.freq = 44100; // PWM frequency of backlight - // cfg.pwm_channel = 1; // PWM channel number to use + cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected + cfg.invert = true; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif - setPanel(&_panel_instance); - } + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -116,7 +119,10 @@ TFT_eSPI *tft = nullptr; FT6336U ft6336u; static uint8_t _rak14014_touch_int = false; // TP interrupt generation flag. -static void rak14014_tpIntHandle(void) { _rak14014_touch_int = true; } +static void rak14014_tpIntHandle(void) +{ + _rak14014_touch_int = true; +} #elif defined(HACKADAY_COMMUNICATOR) #include @@ -130,161 +136,164 @@ Arduino_GFX *tft = nullptr; #include TCA9534 ioex; -class LGFX : public lgfx::LGFX_Device { - lgfx::Bus_RGB _bus_instance; - lgfx::Panel_RGB _panel_instance; - lgfx::Touch_GT911 _touch_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Bus_RGB _bus_instance; + lgfx::Panel_RGB _panel_instance; + lgfx::Touch_GT911 _touch_instance; -public: - const uint16_t screenWidth = TFT_WIDTH; - const uint16_t screenHeight = TFT_HEIGHT; + public: + const uint16_t screenWidth = TFT_WIDTH; + const uint16_t screenHeight = TFT_HEIGHT; - bool init_impl(bool use_reset, bool use_clear) override { - ioex.attach(Wire); - ioex.setDeviceAddress(0x18); - ioex.config(1, TCA9534::Config::OUT); - ioex.config(2, TCA9534::Config::OUT); - ioex.config(3, TCA9534::Config::OUT); - ioex.config(4, TCA9534::Config::OUT); - - ioex.output(1, TCA9534::Level::H); - ioex.output(3, TCA9534::Level::L); - ioex.output(4, TCA9534::Level::H); - - pinMode(1, OUTPUT); - digitalWrite(1, LOW); - ioex.output(2, TCA9534::Level::L); - delay(20); - ioex.output(2, TCA9534::Level::H); - delay(100); - pinMode(1, INPUT); - - return LGFX_Device::init_impl(use_reset, use_clear); - } - - LGFX(void) { + bool init_impl(bool use_reset, bool use_clear) override { - auto cfg = _panel_instance.config(); + ioex.attach(Wire); + ioex.setDeviceAddress(0x18); + ioex.config(1, TCA9534::Config::OUT); + ioex.config(2, TCA9534::Config::OUT); + ioex.config(3, TCA9534::Config::OUT); + ioex.config(4, TCA9534::Config::OUT); - cfg.memory_width = screenWidth; - cfg.memory_height = screenHeight; - cfg.panel_width = screenWidth; - cfg.panel_height = screenHeight; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 0; - _panel_instance.config(cfg); + ioex.output(1, TCA9534::Level::H); + ioex.output(3, TCA9534::Level::L); + ioex.output(4, TCA9534::Level::H); + + pinMode(1, OUTPUT); + digitalWrite(1, LOW); + ioex.output(2, TCA9534::Level::L); + delay(20); + ioex.output(2, TCA9534::Level::H); + delay(100); + pinMode(1, INPUT); + + return LGFX_Device::init_impl(use_reset, use_clear); } + LGFX(void) { - auto cfg = _panel_instance.config_detail(); - cfg.use_psram = 0; - _panel_instance.config_detail(cfg); - } + { + auto cfg = _panel_instance.config(); - { - auto cfg = _bus_instance.config(); - cfg.panel = &_panel_instance; - cfg.pin_d0 = ST72xx_B0; // B0 - cfg.pin_d1 = ST72xx_B1; // B1 - cfg.pin_d2 = ST72xx_B2; // B2 - cfg.pin_d3 = ST72xx_B3; // B3 - cfg.pin_d4 = ST72xx_B4; // B4 - cfg.pin_d5 = ST72xx_G0; // G0 - cfg.pin_d6 = ST72xx_G1; // G1 - cfg.pin_d7 = ST72xx_G2; // G2 - cfg.pin_d8 = ST72xx_G3; // G3 - cfg.pin_d9 = ST72xx_G4; // G4 - cfg.pin_d10 = ST72xx_G5; // G5 - cfg.pin_d11 = ST72xx_R0; // R0 - cfg.pin_d12 = ST72xx_R1; // R1 - cfg.pin_d13 = ST72xx_R2; // R2 - cfg.pin_d14 = ST72xx_R3; // R3 - cfg.pin_d15 = ST72xx_R4; // R4 + cfg.memory_width = screenWidth; + cfg.memory_height = screenHeight; + cfg.panel_width = screenWidth; + cfg.panel_height = screenHeight; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + _panel_instance.config(cfg); + } - cfg.pin_henable = ST72xx_DE; - cfg.pin_vsync = ST72xx_VSYNC; - cfg.pin_hsync = ST72xx_HSYNC; - cfg.pin_pclk = ST72xx_PCLK; - cfg.freq_write = 13000000; + { + auto cfg = _panel_instance.config_detail(); + cfg.use_psram = 0; + _panel_instance.config_detail(cfg); + } + + { + auto cfg = _bus_instance.config(); + cfg.panel = &_panel_instance; + cfg.pin_d0 = ST72xx_B0; // B0 + cfg.pin_d1 = ST72xx_B1; // B1 + cfg.pin_d2 = ST72xx_B2; // B2 + cfg.pin_d3 = ST72xx_B3; // B3 + cfg.pin_d4 = ST72xx_B4; // B4 + cfg.pin_d5 = ST72xx_G0; // G0 + cfg.pin_d6 = ST72xx_G1; // G1 + cfg.pin_d7 = ST72xx_G2; // G2 + cfg.pin_d8 = ST72xx_G3; // G3 + cfg.pin_d9 = ST72xx_G4; // G4 + cfg.pin_d10 = ST72xx_G5; // G5 + cfg.pin_d11 = ST72xx_R0; // R0 + cfg.pin_d12 = ST72xx_R1; // R1 + cfg.pin_d13 = ST72xx_R2; // R2 + cfg.pin_d14 = ST72xx_R3; // R3 + cfg.pin_d15 = ST72xx_R4; // R4 + + cfg.pin_henable = ST72xx_DE; + cfg.pin_vsync = ST72xx_VSYNC; + cfg.pin_hsync = ST72xx_HSYNC; + cfg.pin_pclk = ST72xx_PCLK; + cfg.freq_write = 13000000; #ifdef ST7265_HSYNC_POLARITY - cfg.hsync_polarity = ST7265_HSYNC_POLARITY; - cfg.hsync_front_porch = ST7265_HSYNC_FRONT_PORCH; // 8; - cfg.hsync_pulse_width = ST7265_HSYNC_PULSE_WIDTH; // 4; - cfg.hsync_back_porch = ST7265_HSYNC_BACK_PORCH; // 8; + cfg.hsync_polarity = ST7265_HSYNC_POLARITY; + cfg.hsync_front_porch = ST7265_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = ST7265_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = ST7265_HSYNC_BACK_PORCH; // 8; - cfg.vsync_polarity = ST7265_VSYNC_POLARITY; // 0; - cfg.vsync_front_porch = ST7265_VSYNC_FRONT_PORCH; // 8; - cfg.vsync_pulse_width = ST7265_VSYNC_PULSE_WIDTH; // 4; - cfg.vsync_back_porch = ST7265_VSYNC_BACK_PORCH; // 8; + cfg.vsync_polarity = ST7265_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = ST7265_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = ST7265_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = ST7265_VSYNC_BACK_PORCH; // 8; - cfg.pclk_idle_high = 1; - cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif #ifdef ST7262_HSYNC_POLARITY - cfg.hsync_polarity = ST7262_HSYNC_POLARITY; - cfg.hsync_front_porch = ST7262_HSYNC_FRONT_PORCH; // 8; - cfg.hsync_pulse_width = ST7262_HSYNC_PULSE_WIDTH; // 4; - cfg.hsync_back_porch = ST7262_HSYNC_BACK_PORCH; // 8; + cfg.hsync_polarity = ST7262_HSYNC_POLARITY; + cfg.hsync_front_porch = ST7262_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = ST7262_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = ST7262_HSYNC_BACK_PORCH; // 8; - cfg.vsync_polarity = ST7262_VSYNC_POLARITY; // 0; - cfg.vsync_front_porch = ST7262_VSYNC_FRONT_PORCH; // 8; - cfg.vsync_pulse_width = ST7262_VSYNC_PULSE_WIDTH; // 4; - cfg.vsync_back_porch = ST7262_VSYNC_BACK_PORCH; // 8; + cfg.vsync_polarity = ST7262_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = ST7262_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = ST7262_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = ST7262_VSYNC_BACK_PORCH; // 8; - cfg.pclk_idle_high = 1; - cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif #ifdef SC7277_HSYNC_POLARITY - cfg.hsync_polarity = SC7277_HSYNC_POLARITY; - cfg.hsync_front_porch = SC7277_HSYNC_FRONT_PORCH; // 8; - cfg.hsync_pulse_width = SC7277_HSYNC_PULSE_WIDTH; // 4; - cfg.hsync_back_porch = SC7277_HSYNC_BACK_PORCH; // 8; + cfg.hsync_polarity = SC7277_HSYNC_POLARITY; + cfg.hsync_front_porch = SC7277_HSYNC_FRONT_PORCH; // 8; + cfg.hsync_pulse_width = SC7277_HSYNC_PULSE_WIDTH; // 4; + cfg.hsync_back_porch = SC7277_HSYNC_BACK_PORCH; // 8; - cfg.vsync_polarity = SC7277_VSYNC_POLARITY; // 0; - cfg.vsync_front_porch = SC7277_VSYNC_FRONT_PORCH; // 8; - cfg.vsync_pulse_width = SC7277_VSYNC_PULSE_WIDTH; // 4; - cfg.vsync_back_porch = SC7277_VSYNC_BACK_PORCH; // 8; + cfg.vsync_polarity = SC7277_VSYNC_POLARITY; // 0; + cfg.vsync_front_porch = SC7277_VSYNC_FRONT_PORCH; // 8; + cfg.vsync_pulse_width = SC7277_VSYNC_PULSE_WIDTH; // 4; + cfg.vsync_back_porch = SC7277_VSYNC_BACK_PORCH; // 8; - cfg.pclk_idle_high = 1; - cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; - // cfg.pclk_idle_high = 0; - // cfg.de_idle_high = 1; + cfg.pclk_idle_high = 1; + cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; + // cfg.pclk_idle_high = 0; + // cfg.de_idle_high = 1; #endif - _bus_instance.config(cfg); + _bus_instance.config(cfg); + } + _panel_instance.setBus(&_bus_instance); + + { + auto cfg = _touch_instance.config(); + cfg.x_min = 0; + cfg.x_max = TFT_WIDTH; + cfg.y_min = 0; + cfg.y_max = TFT_HEIGHT; + cfg.pin_int = -1; + cfg.pin_rst = -1; + cfg.bus_shared = true; + cfg.offset_rotation = 0; + + cfg.i2c_port = 0; + cfg.i2c_addr = 0x5D; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; + cfg.freq = 400000; + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); } - _panel_instance.setBus(&_bus_instance); - - { - auto cfg = _touch_instance.config(); - cfg.x_min = 0; - cfg.x_max = TFT_WIDTH; - cfg.y_min = 0; - cfg.y_max = TFT_HEIGHT; - cfg.pin_int = -1; - cfg.pin_rst = -1; - cfg.bus_shared = true; - cfg.offset_rotation = 0; - - cfg.i2c_port = 0; - cfg.i2c_addr = 0x5D; - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; - cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } - - setPanel(&_panel_instance); - } }; static LGFX *tft = nullptr; @@ -292,124 +301,127 @@ static LGFX *tft = nullptr; #elif defined(ILI9488_CS) #include // Graphics and font library for ILI9488 driver chip -class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_ILI9488 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; - lgfx::Touch_GT911 _touch_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ILI9488 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_GT911 _touch_instance; -public: - LGFX(void) { + public: + LGFX(void) { - auto cfg = _bus_instance.config(); + { + auto cfg = _bus_instance.config(); - // configure SPI - cfg.spi_host = ILI9488_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by - // dividing 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ILI9488_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ILI9488_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ILI9488_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ILI9488_RS; // Set SPI DC pin number (-1 = disable) + // configure SPI + cfg.spi_host = ILI9488_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ILI9488_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ILI9488_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ILI9488_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ILI9488_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ILI9488_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ILI9488_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS - cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef ILI9488_BL - // Set the backlight control - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = ILI9488_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - // cfg.freq = 44100; // PWM frequency of backlight - // cfg.pwm_channel = 1; // PWM channel number to use + cfg.pin_bl = ILI9488_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif #if HAS_TOUCHSCREEN - // Configure settings for touch screen control. - { - auto cfg = _touch_instance.config(); + // Configure settings for touch screen control. + { + auto cfg = _touch_instance.config(); - cfg.pin_cs = -1; - cfg.x_min = 0; - cfg.x_max = TFT_HEIGHT - 1; - cfg.y_min = 0; - cfg.y_max = TFT_WIDTH - 1; - cfg.pin_int = SCREEN_TOUCH_INT; + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = TFT_HEIGHT - 1; + cfg.y_min = 0; + cfg.y_max = TFT_WIDTH - 1; + cfg.pin_int = SCREEN_TOUCH_INT; #ifdef SCREEN_TOUCH_RST - cfg.pin_rst = SCREEN_TOUCH_RST; + cfg.pin_rst = SCREEN_TOUCH_RST; #endif - cfg.bus_shared = true; - cfg.offset_rotation = TFT_OFFSET_ROTATION; - // cfg.freq = 2500000; + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + // cfg.freq = 2500000; - // I2C - cfg.i2c_port = TOUCH_I2C_PORT; - cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + // I2C + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; #ifdef SCREEN_TOUCH_USE_I2C1 - cfg.pin_sda = I2C_SDA1; - cfg.pin_scl = I2C_SCL1; + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; #else - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; #endif - // cfg.freq = 400000; + // cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } +#endif + + setPanel(&_panel_instance); } -#endif - - setPanel(&_panel_instance); - } }; static LGFX *tft = nullptr; @@ -419,182 +431,191 @@ static LGFX *tft = nullptr; #ifdef HELTEC_V4_TFT #include "chsc6x.h" #include "lgfx/v1/Touch.hpp" -namespace lgfx { -inline namespace v1 { -class TOUCH_CHSC6X : public ITouch { -public: - TOUCH_CHSC6X(void) { - _cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; - _cfg.x_min = 0; - _cfg.x_max = 240; - _cfg.y_min = 0; - _cfg.y_max = 320; - }; +namespace lgfx +{ +inline namespace v1 +{ +class TOUCH_CHSC6X : public ITouch +{ + public: + TOUCH_CHSC6X(void) + { + _cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + _cfg.x_min = 0; + _cfg.x_max = 240; + _cfg.y_min = 0; + _cfg.y_max = 320; + }; - bool init(void) override { - if (chsc6xTouch == nullptr) { - chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN); - } - chsc6xTouch->chsc6x_init(); - return true; - }; + bool init(void) override + { + if (chsc6xTouch == nullptr) { + chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN); + } + chsc6xTouch->chsc6x_init(); + return true; + }; - uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { - uint16_t raw_x, raw_y; - if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) { - tp[0].x = 320 - 1 - raw_y; - tp[0].y = 240 - 1 - raw_x; - tp[0].size = 1; - tp[0].id = 1; - return 1; - } - tp[0].size = 0; - return 0; - }; + uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override + { + uint16_t raw_x, raw_y; + if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) { + tp[0].x = 320 - 1 - raw_y; + tp[0].y = 240 - 1 - raw_x; + tp[0].size = 1; + tp[0].id = 1; + return 1; + } + tp[0].size = 0; + return 0; + }; - void wakeup(void) override{}; - void sleep(void) override{}; + void wakeup(void) override{}; + void sleep(void) override{}; -private: - chsc6x *chsc6xTouch = nullptr; + private: + chsc6x *chsc6xTouch = nullptr; }; } // namespace v1 } // namespace lgfx #endif -class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_ST7789 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7789 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; #if HAS_TOUCHSCREEN #if defined(T_WATCH_S3) || defined(ELECROW) - lgfx::Touch_FT5x06 _touch_instance; + lgfx::Touch_FT5x06 _touch_instance; #elif defined(HELTEC_V4_TFT) - lgfx::TOUCH_CHSC6X _touch_instance; + lgfx::TOUCH_CHSC6X _touch_instance; #else - lgfx::Touch_GT911 _touch_instance; + lgfx::Touch_GT911 _touch_instance; #endif #endif -public: - LGFX(void) { + public: + LGFX(void) { - auto cfg = _bus_instance.config(); + { + auto cfg = _bus_instance.config(); - // SPI - cfg.spi_host = ST7789_SPI_HOST; - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by - // dividing 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ST7789_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ST7789_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ST7789_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ST7789_RS; // Set SPI DC pin number (-1 = disable) + // SPI + cfg.spi_host = ST7789_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7789_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7789_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7789_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7789_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. #if defined(T_WATCH_S3) - cfg.panel_width = 240; - cfg.panel_height = 240; - cfg.memory_width = 240; - cfg.memory_height = 320; - cfg.offset_x = 0; - cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned - cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout + cfg.panel_width = 240; + cfg.panel_height = 240; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned + cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout #else - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #endif #ifdef TFT_DUMMY_READ_PIXELS - cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef ST7789_BL - // Set the backlight control. (delete if not necessary) - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = ST7789_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - // cfg.pwm_channel = 0; + cfg.pin_bl = ST7789_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.pwm_channel = 0; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif #if HAS_TOUCHSCREEN - // Configure settings for touch screen control. - { - auto cfg = _touch_instance.config(); + // Configure settings for touch screen control. + { + auto cfg = _touch_instance.config(); - cfg.pin_cs = -1; - cfg.x_min = 0; - cfg.x_max = TFT_HEIGHT - 1; - cfg.y_min = 0; - cfg.y_max = TFT_WIDTH - 1; - cfg.pin_int = SCREEN_TOUCH_INT; + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = TFT_HEIGHT - 1; + cfg.y_min = 0; + cfg.y_max = TFT_WIDTH - 1; + cfg.pin_int = SCREEN_TOUCH_INT; #ifdef SCREEN_TOUCH_RST - cfg.pin_rst = SCREEN_TOUCH_RST; + cfg.pin_rst = SCREEN_TOUCH_RST; #endif - cfg.bus_shared = true; - cfg.offset_rotation = TFT_OFFSET_ROTATION; - // cfg.freq = 2500000; + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + // cfg.freq = 2500000; - // I2C - cfg.i2c_port = TOUCH_I2C_PORT; - cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + // I2C + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; #ifdef SCREEN_TOUCH_USE_I2C1 - cfg.pin_sda = I2C_SDA1; - cfg.pin_scl = I2C_SCL1; + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; #else - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; #endif - // cfg.freq = 400000; + // cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. } -#endif - - setPanel(&_panel_instance); // Sets the panel to use. - } }; static LGFX *tft = nullptr; @@ -602,81 +623,84 @@ static LGFX *tft = nullptr; #elif defined(ST7796_CS) #include // Graphics and font library for ST7796 driver chip -class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_ST7796 _panel_instance; - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7796 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; -public: - LGFX(void) { + public: + LGFX(void) { - auto cfg = _bus_instance.config(); + { + auto cfg = _bus_instance.config(); - // SPI - cfg.spi_host = ST7796_SPI_HOST; - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by - // dividing 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number - cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) + // SPI + cfg.spi_host = ST7796_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) - // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS - cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout #endif - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = true; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - _panel_instance.config(cfg); - } + _panel_instance.config(cfg); + } #ifdef ST7796_BL - // Set the backlight control. (delete if not necessary) - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - cfg.freq = 44100; - cfg.pwm_channel = 7; + cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + cfg.freq = 44100; + cfg.pwm_channel = 7; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif - setPanel(&_panel_instance); // Sets the panel to use. - } + setPanel(&_panel_instance); // Sets the panel to use. + } }; static LGFX *tft = nullptr; @@ -689,90 +713,93 @@ static LGFX *tft = nullptr; #define TFT_BL ILI9341_BACKLIGHT_EN #endif -class LGFX : public lgfx::LGFX_Device { +class LGFX : public lgfx::LGFX_Device +{ #if defined(ILI9341_DRIVER) - lgfx::Panel_ILI9341 _panel_instance; + lgfx::Panel_ILI9341 _panel_instance; #elif defined(ILI9342_DRIVER) - lgfx::Panel_ILI9342 _panel_instance; + lgfx::Panel_ILI9342 _panel_instance; #endif - lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; -public: - LGFX(void) { + public: + LGFX(void) { - auto cfg = _bus_instance.config(); + { + auto cfg = _bus_instance.config(); - // configure SPI + // configure SPI #if defined(ILI9341_DRIVER) - cfg.spi_host = ILI9341_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_host = ILI9341_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST #elif defined(ILI9342_DRIVER) - cfg.spi_host = ILI9342_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_host = ILI9342_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST #endif - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by - // dividing 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = TFT_SCLK; // Set SPI SCLK pin number - cfg.pin_mosi = TFT_MOSI; // Set SPI MOSI pin number - cfg.pin_miso = TFT_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = TFT_DC; // Set SPI DC pin number (-1 = disable) + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = TFT_SCLK; // Set SPI SCLK pin number + cfg.pin_mosi = TFT_MOSI; // Set SPI MOSI pin number + cfg.pin_miso = TFT_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = TFT_DC; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } - { // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = TFT_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = TFT_RST; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = TFT_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = TFT_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = TFT_RST; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = TFT_BUSY; // Pin number where BUSY is connected (-1 = disable) - // The following setting values ​​are general initial values ​​for each panel, so please comment out any - // unknown items and try them. + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = false; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = false; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the - // ST7735 or ILI9163. - cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC - cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC - _panel_instance.config(cfg); - } + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } #ifdef TFT_BL - // Set the backlight control - { - auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. - cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected - cfg.invert = false; // true to invert the brightness of the backlight - // cfg.freq = 44100; // PWM frequency of backlight - // cfg.pwm_channel = 1; // PWM channel number to use + cfg.pin_bl = TFT_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. - } + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } #endif - setPanel(&_panel_instance); - } + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -785,106 +812,108 @@ static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h #include "Panel_sdl.hpp" #include // Graphics and font library for ST7735 driver chip -class LGFX : public lgfx::LGFX_Device { - lgfx::Bus_SPI _bus_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Bus_SPI _bus_instance; - lgfx::ITouch *_touch_instance; + lgfx::ITouch *_touch_instance; -public: - lgfx::Panel_Device *_panel_instance; + public: + lgfx::Panel_Device *_panel_instance; - LGFX(void) { - if (portduino_config.displayPanel == st7789) - _panel_instance = new lgfx::Panel_ST7789; - else if (portduino_config.displayPanel == st7735) - _panel_instance = new lgfx::Panel_ST7735; - else if (portduino_config.displayPanel == st7735s) - _panel_instance = new lgfx::Panel_ST7735S; - else if (portduino_config.displayPanel == st7796) - _panel_instance = new lgfx::Panel_ST7796; - else if (portduino_config.displayPanel == ili9341) - _panel_instance = new lgfx::Panel_ILI9341; - else if (portduino_config.displayPanel == ili9342) - _panel_instance = new lgfx::Panel_ILI9342; - else if (portduino_config.displayPanel == ili9488) - _panel_instance = new lgfx::Panel_ILI9488; - else if (portduino_config.displayPanel == hx8357d) - _panel_instance = new lgfx::Panel_HX8357D; + LGFX(void) + { + if (portduino_config.displayPanel == st7789) + _panel_instance = new lgfx::Panel_ST7789; + else if (portduino_config.displayPanel == st7735) + _panel_instance = new lgfx::Panel_ST7735; + else if (portduino_config.displayPanel == st7735s) + _panel_instance = new lgfx::Panel_ST7735S; + else if (portduino_config.displayPanel == st7796) + _panel_instance = new lgfx::Panel_ST7796; + else if (portduino_config.displayPanel == ili9341) + _panel_instance = new lgfx::Panel_ILI9341; + else if (portduino_config.displayPanel == ili9342) + _panel_instance = new lgfx::Panel_ILI9342; + else if (portduino_config.displayPanel == ili9488) + _panel_instance = new lgfx::Panel_ILI9488; + else if (portduino_config.displayPanel == hx8357d) + _panel_instance = new lgfx::Panel_HX8357D; #if defined(SDL_h_) - else if (portduino_config.displayPanel == x11) - _panel_instance = new lgfx::Panel_sdl; + else if (portduino_config.displayPanel == x11) + _panel_instance = new lgfx::Panel_sdl; #endif - else { - _panel_instance = new lgfx::Panel_NULL; - LOG_ERROR("Unknown display panel configured!"); - } + else { + _panel_instance = new lgfx::Panel_NULL; + LOG_ERROR("Unknown display panel configured!"); + } - auto buscfg = _bus_instance.config(); - buscfg.spi_mode = 0; - buscfg.spi_host = portduino_config.display_spi_dev_int; + auto buscfg = _bus_instance.config(); + buscfg.spi_mode = 0; + buscfg.spi_host = portduino_config.display_spi_dev_int; - buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) + buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(buscfg); // applies the set value to the bus. - if (portduino_config.displayPanel != x11) - _panel_instance->setBus(&_bus_instance); // set the bus on the panel. + _bus_instance.config(buscfg); // applies the set value to the bus. + if (portduino_config.displayPanel != x11) + _panel_instance->setBus(&_bus_instance); // set the bus on the panel. - auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); - cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = portduino_config.displayReset.pin; - if (portduino_config.displayRotate) { - cfg.panel_width = portduino_config.displayHeight; // actual displayable width - cfg.panel_height = portduino_config.displayWidth; // actual displayable height - } else { - cfg.panel_width = portduino_config.displayWidth; // actual displayable width - cfg.panel_height = portduino_config.displayHeight; // actual displayable height - } - cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction - cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction - cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) - cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed + auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. + LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); + cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = portduino_config.displayReset.pin; + if (portduino_config.displayRotate) { + cfg.panel_width = portduino_config.displayHeight; // actual displayable width + cfg.panel_height = portduino_config.displayWidth; // actual displayable height + } else { + cfg.panel_width = portduino_config.displayWidth; // actual displayable width + cfg.panel_height = portduino_config.displayHeight; // actual displayable height + } + cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction + cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction + cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed - _panel_instance->config(cfg); + _panel_instance->config(cfg); - // Configure settings for touch control. - if (portduino_config.touchscreenModule) { - if (portduino_config.touchscreenModule == xpt2046) { - _touch_instance = new lgfx::Touch_XPT2046; - } else if (portduino_config.touchscreenModule == stmpe610) { - _touch_instance = new lgfx::Touch_STMPE610; - } else if (portduino_config.touchscreenModule == ft5x06) { - _touch_instance = new lgfx::Touch_FT5x06; - } - auto touch_cfg = _touch_instance->config(); + // Configure settings for touch control. + if (portduino_config.touchscreenModule) { + if (portduino_config.touchscreenModule == xpt2046) { + _touch_instance = new lgfx::Touch_XPT2046; + } else if (portduino_config.touchscreenModule == stmpe610) { + _touch_instance = new lgfx::Touch_STMPE610; + } else if (portduino_config.touchscreenModule == ft5x06) { + _touch_instance = new lgfx::Touch_FT5x06; + } + auto touch_cfg = _touch_instance->config(); - touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; - touch_cfg.x_min = 0; - touch_cfg.x_max = portduino_config.displayHeight - 1; - touch_cfg.y_min = 0; - touch_cfg.y_max = portduino_config.displayWidth - 1; - touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; - touch_cfg.bus_shared = true; - touch_cfg.offset_rotation = portduino_config.touchscreenRotate; - if (portduino_config.touchscreenI2CAddr != -1) { - touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; - } else { - touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; - } + touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; + touch_cfg.x_min = 0; + touch_cfg.x_max = portduino_config.displayHeight - 1; + touch_cfg.y_min = 0; + touch_cfg.y_max = portduino_config.displayWidth - 1; + touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; + touch_cfg.bus_shared = true; + touch_cfg.offset_rotation = portduino_config.touchscreenRotate; + if (portduino_config.touchscreenI2CAddr != -1) { + touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; + } else { + touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; + } - _touch_instance->config(touch_cfg); - _panel_instance->setTouch(_touch_instance); - } + _touch_instance->config(touch_cfg); + _panel_instance->setTouch(_touch_instance); + } #if defined(SDL_h_) - if (portduino_config.displayPanel == x11) { - lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; - sdl_panel_->setup(); - sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); - } + if (portduino_config.displayPanel == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; + sdl_panel_->setup(); + sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); + } #endif - setPanel(_panel_instance); // Sets the panel to use. - } + setPanel(_panel_instance); // Sets the panel to use. + } }; static LGFX *tft = nullptr; @@ -892,80 +921,82 @@ static LGFX *tft = nullptr; #elif defined(HX8357_CS) #include // Graphics and font library for HX8357 driver chip -class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_HX8357D _panel_instance; - lgfx::Bus_SPI _bus_instance; +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_HX8357D _panel_instance; + lgfx::Bus_SPI _bus_instance; #if defined(USE_XPT2046) - lgfx::Touch_XPT2046 _touch_instance; + lgfx::Touch_XPT2046 _touch_instance; #endif -public: - LGFX(void) { - // Panel_HX8357D + public: + LGFX(void) { - // configure SPI - auto cfg = _bus_instance.config(); + // Panel_HX8357D + { + // configure SPI + auto cfg = _bus_instance.config(); - cfg.spi_host = HX8357_SPI_HOST; - cfg.spi_mode = 0; - cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by - // dividing 80MHz by an integer) - cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving - cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin - cfg.use_lock = true; // Set to true to use transaction locking - cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / - // SPI_DMA_CH_AUTO=auto setting) - cfg.pin_sclk = HX8357_SCK; // Set SPI SCLK pin number - cfg.pin_mosi = HX8357_MOSI; // Set SPI MOSI pin number - cfg.pin_miso = HX8357_MISO; // Set SPI MISO pin number (-1 = disable) - cfg.pin_dc = HX8357_RS; // Set SPI DC pin number (-1 = disable) + cfg.spi_host = HX8357_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = HX8357_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = HX8357_MOSI; // Set SPI MOSI pin number + cfg.pin_miso = HX8357_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = HX8357_RS; // Set SPI DC pin number (-1 = disable) - _bus_instance.config(cfg); // applies the set value to the bus. - _panel_instance.setBus(&_bus_instance); // set the bus on the panel. - } - { - // Set the display panel control. - auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + { + // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. - cfg.pin_cs = HX8357_CS; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = HX8357_RESET; // Pin number where RST is connected (-1 = disable) - cfg.pin_busy = HX8357_BUSY; // Pin number where BUSY is connected (-1 = disable) + cfg.pin_cs = HX8357_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = HX8357_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = HX8357_BUSY; // Pin number where BUSY is connected (-1 = disable) - cfg.panel_width = TFT_WIDTH; // actual displayable width - cfg.panel_height = TFT_HEIGHT; // actual displayable height - cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction - cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction - cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is upside down) - cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout - cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read - cfg.readable = true; // Set to true if data can be read - cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed - cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped - cfg.dlen_16bit = false; - cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = TFT_INVERT; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = false; + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) - _panel_instance.config(cfg); - } + _panel_instance.config(cfg); + } #if defined(USE_XPT2046) - { - // Configure settings for touch control. - auto touch_cfg = _touch_instance.config(); + { + // Configure settings for touch control. + auto touch_cfg = _touch_instance.config(); - touch_cfg.pin_cs = TOUCH_CS; - touch_cfg.x_min = 0; - touch_cfg.x_max = TFT_HEIGHT - 1; - touch_cfg.y_min = 0; - touch_cfg.y_max = TFT_WIDTH - 1; - touch_cfg.pin_int = -1; - touch_cfg.bus_shared = true; - touch_cfg.offset_rotation = 1; + touch_cfg.pin_cs = TOUCH_CS; + touch_cfg.x_min = 0; + touch_cfg.x_max = TFT_HEIGHT - 1; + touch_cfg.y_min = 0; + touch_cfg.y_max = TFT_WIDTH - 1; + touch_cfg.pin_int = -1; + touch_cfg.bus_shared = true; + touch_cfg.offset_rotation = 1; - _touch_instance.config(touch_cfg); - _panel_instance.setTouch(&_touch_instance); - } + _touch_instance.config(touch_cfg); + _panel_instance.setTouch(&_touch_instance); + } #endif - setPanel(&_panel_instance); - } + setPanel(&_panel_instance); + } }; static LGFX *tft = nullptr; @@ -975,129 +1006,133 @@ static LGFX *tft = nullptr; #include #include -class PanelInit_ST7701 : public lgfx::Panel_ST7701 { -public: - const uint8_t *getInitCommands(uint8_t listno) const override { - // 180 degree hw rotation: vertical flip, horizontal flip - static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip - 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL - 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip) - 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS - 0xFF, 0xFF}; - switch (listno) { - case 1: - return list1; - default: - return lgfx::Panel_ST7701::getInitCommands(listno); +class PanelInit_ST7701 : public lgfx::Panel_ST7701 +{ + public: + const uint8_t *getInitCommands(uint8_t listno) const override + { + // 180 degree hw rotation: vertical flip, horizontal flip + static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL + 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip) + 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS + 0xFF, 0xFF}; + switch (listno) { + case 1: + return list1; + default: + return lgfx::Panel_ST7701::getInitCommands(listno); + } } - } }; -class LGFX : public lgfx::LGFX_Device { - PanelInit_ST7701 _panel_instance; - lgfx::Bus_RGB _bus_instance; - lgfx::Light_PWM _light_instance; - lgfx::Touch_FT5x06 _touch_instance; +class LGFX : public lgfx::LGFX_Device +{ + PanelInit_ST7701 _panel_instance; + lgfx::Bus_RGB _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_FT5x06 _touch_instance; -public: - LGFX(void) { + public: + LGFX(void) { - auto cfg = _panel_instance.config(); - cfg.memory_width = 800; - cfg.memory_height = 480; - cfg.panel_width = TFT_WIDTH; - cfg.panel_height = TFT_HEIGHT; - cfg.offset_x = TFT_OFFSET_X; - cfg.offset_y = TFT_OFFSET_Y; - _panel_instance.config(cfg); - } + { + auto cfg = _panel_instance.config(); + cfg.memory_width = 800; + cfg.memory_height = 480; + cfg.panel_width = TFT_WIDTH; + cfg.panel_height = TFT_HEIGHT; + cfg.offset_x = TFT_OFFSET_X; + cfg.offset_y = TFT_OFFSET_Y; + _panel_instance.config(cfg); + } - { - auto cfg = _panel_instance.config_detail(); - cfg.pin_cs = ST7701_CS; - cfg.pin_sclk = ST7701_SCK; - cfg.pin_mosi = ST7701_SDA; - // cfg.use_psram = 1; - _panel_instance.config_detail(cfg); - } + { + auto cfg = _panel_instance.config_detail(); + cfg.pin_cs = ST7701_CS; + cfg.pin_sclk = ST7701_SCK; + cfg.pin_mosi = ST7701_SDA; + // cfg.use_psram = 1; + _panel_instance.config_detail(cfg); + } - { - auto cfg = _bus_instance.config(); - cfg.panel = &_panel_instance; + { + auto cfg = _bus_instance.config(); + cfg.panel = &_panel_instance; #ifdef SENSECAP_INDICATOR - cfg.pin_d0 = GPIO_NUM_15; // B0 - cfg.pin_d1 = GPIO_NUM_14; // B1 - cfg.pin_d2 = GPIO_NUM_13; // B2 - cfg.pin_d3 = GPIO_NUM_12; // B3 - cfg.pin_d4 = GPIO_NUM_11; // B4 + cfg.pin_d0 = GPIO_NUM_15; // B0 + cfg.pin_d1 = GPIO_NUM_14; // B1 + cfg.pin_d2 = GPIO_NUM_13; // B2 + cfg.pin_d3 = GPIO_NUM_12; // B3 + cfg.pin_d4 = GPIO_NUM_11; // B4 - cfg.pin_d5 = GPIO_NUM_10; // G0 - cfg.pin_d6 = GPIO_NUM_9; // G1 - cfg.pin_d7 = GPIO_NUM_8; // G2 - cfg.pin_d8 = GPIO_NUM_7; // G3 - cfg.pin_d9 = GPIO_NUM_6; // G4 - cfg.pin_d10 = GPIO_NUM_5; // G5 + cfg.pin_d5 = GPIO_NUM_10; // G0 + cfg.pin_d6 = GPIO_NUM_9; // G1 + cfg.pin_d7 = GPIO_NUM_8; // G2 + cfg.pin_d8 = GPIO_NUM_7; // G3 + cfg.pin_d9 = GPIO_NUM_6; // G4 + cfg.pin_d10 = GPIO_NUM_5; // G5 - cfg.pin_d11 = GPIO_NUM_4; // R0 - cfg.pin_d12 = GPIO_NUM_3; // R1 - cfg.pin_d13 = GPIO_NUM_2; // R2 - cfg.pin_d14 = GPIO_NUM_1; // R3 - cfg.pin_d15 = GPIO_NUM_0; // R4 + cfg.pin_d11 = GPIO_NUM_4; // R0 + cfg.pin_d12 = GPIO_NUM_3; // R1 + cfg.pin_d13 = GPIO_NUM_2; // R2 + cfg.pin_d14 = GPIO_NUM_1; // R3 + cfg.pin_d15 = GPIO_NUM_0; // R4 - cfg.pin_henable = GPIO_NUM_18; - cfg.pin_vsync = GPIO_NUM_17; - cfg.pin_hsync = GPIO_NUM_16; - cfg.pin_pclk = GPIO_NUM_21; - cfg.freq_write = 12000000; + cfg.pin_henable = GPIO_NUM_18; + cfg.pin_vsync = GPIO_NUM_17; + cfg.pin_hsync = GPIO_NUM_16; + cfg.pin_pclk = GPIO_NUM_21; + cfg.freq_write = 12000000; - cfg.hsync_polarity = 0; - cfg.hsync_front_porch = 10; - cfg.hsync_pulse_width = 8; - cfg.hsync_back_porch = 50; + cfg.hsync_polarity = 0; + cfg.hsync_front_porch = 10; + cfg.hsync_pulse_width = 8; + cfg.hsync_back_porch = 50; - cfg.vsync_polarity = 0; - cfg.vsync_front_porch = 10; - cfg.vsync_pulse_width = 8; - cfg.vsync_back_porch = 20; + cfg.vsync_polarity = 0; + cfg.vsync_front_porch = 10; + cfg.vsync_pulse_width = 8; + cfg.vsync_back_porch = 20; - cfg.pclk_active_neg = 0; - cfg.de_idle_high = 1; - cfg.pclk_idle_high = 0; + cfg.pclk_active_neg = 0; + cfg.de_idle_high = 1; + cfg.pclk_idle_high = 0; #endif - _bus_instance.config(cfg); + _bus_instance.config(cfg); + } + _panel_instance.setBus(&_bus_instance); + + { + auto cfg = _light_instance.config(); + cfg.pin_bl = ST7701_BL; + _light_instance.config(cfg); + } + _panel_instance.light(&_light_instance); + + { + auto cfg = _touch_instance.config(); + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = 479; + cfg.y_min = 0; + cfg.y_max = 479; + cfg.pin_int = -1; // don't use SCREEN_TOUCH_INT; + cfg.pin_rst = SCREEN_TOUCH_RST; + cfg.bus_shared = true; + cfg.offset_rotation = TFT_OFFSET_ROTATION; + + cfg.i2c_port = TOUCH_I2C_PORT; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; + cfg.freq = 400000; + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); } - _panel_instance.setBus(&_bus_instance); - - { - auto cfg = _light_instance.config(); - cfg.pin_bl = ST7701_BL; - _light_instance.config(cfg); - } - _panel_instance.light(&_light_instance); - - { - auto cfg = _touch_instance.config(); - cfg.pin_cs = -1; - cfg.x_min = 0; - cfg.x_max = 479; - cfg.y_min = 0; - cfg.y_max = 479; - cfg.pin_int = -1; // don't use SCREEN_TOUCH_INT; - cfg.pin_rst = SCREEN_TOUCH_RST; - cfg.bus_shared = true; - cfg.offset_rotation = TFT_OFFSET_ROTATION; - - cfg.i2c_port = TOUCH_I2C_PORT; - cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; - cfg.pin_sda = I2C_SDA; - cfg.pin_scl = I2C_SCL; - cfg.freq = 400000; - _touch_instance.config(cfg); - _panel_instance.setTouch(&_touch_instance); - } - - setPanel(&_panel_instance); - } }; static LGFX *tft = nullptr; @@ -1115,354 +1150,370 @@ extern unPhone unphone; GpioPin *TFTDisplay::backlightEnable = NULL; -TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) { - LOG_DEBUG("TFTDisplay!"); +TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) +{ + LOG_DEBUG("TFTDisplay!"); #ifdef TFT_BL - GpioPin *p = new GpioHwPin(TFT_BL); + GpioPin *p = new GpioHwPin(TFT_BL); - if (!TFT_BACKLIGHT_ON) { // Need to invert the pin before hardware - auto virtPin = new GpioVirtPin(); - new GpioNotTransformer(virtPin, - p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio - p = virtPin; - } + if (!TFT_BACKLIGHT_ON) { // Need to invert the pin before hardware + auto virtPin = new GpioVirtPin(); + new GpioNotTransformer( + virtPin, p); // We just leave this created object on the heap so it can stay watching virtPin and driving en_gpio + p = virtPin; + } #else - GpioPin *p = new GpioVirtPin(); // Just simulate a pin + GpioPin *p = new GpioVirtPin(); // Just simulate a pin #endif - backlightEnable = p; + backlightEnable = p; #if ARCH_PORTDUINO - if (portduino_config.displayRotate) { - setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); - } else { - setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); - } + if (portduino_config.displayRotate) { + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); + } else { + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); + } #elif defined(SCREEN_ROTATE) - setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); + setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); #else - setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif } -TFTDisplay::~TFTDisplay() { - // Clean up allocated line pixel buffer to prevent memory leak - if (linePixelBuffer != nullptr) { - free(linePixelBuffer); - linePixelBuffer = nullptr; - } +TFTDisplay::~TFTDisplay() +{ + // Clean up allocated line pixel buffer to prevent memory leak + if (linePixelBuffer != nullptr) { + free(linePixelBuffer); + linePixelBuffer = nullptr; + } } // Write the buffer to the display memory -void TFTDisplay::display(bool fromBlank) { - if (fromBlank) - tft->fillScreen(TFT_BLACK); +void TFTDisplay::display(bool fromBlank) +{ + if (fromBlank) + tft->fillScreen(TFT_BLACK); - concurrency::LockGuard g(spiLock); + concurrency::LockGuard g(spiLock); - uint32_t x, y; - uint32_t y_byteIndex; - uint8_t y_byteMask; - uint32_t x_FirstPixelUpdate; - uint32_t x_LastPixelUpdate; - bool isset, dblbuf_isset; - uint16_t colorTftMesh, colorTftBlack; - bool somethingChanged = false; + uint32_t x, y; + uint32_t y_byteIndex; + uint8_t y_byteMask; + uint32_t x_FirstPixelUpdate; + uint32_t x_LastPixelUpdate; + bool isset, dblbuf_isset; + uint16_t colorTftMesh, colorTftBlack; + bool somethingChanged = false; - // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step - colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); - colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step + colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); + colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); - y = 0; - while (y < displayHeight) { - y_byteIndex = (y / 8) * displayWidth; - y_byteMask = (1 << (y & 7)); + y = 0; + while (y < displayHeight) { + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); - // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. - if (y_byteMask == 1) { - if (!fromBlank) { - for (x = 0; x < displayWidth; x++) { - if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) - break; + // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. + if (y_byteMask == 1) { + if (!fromBlank) { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) + break; + } + } else { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != 0) + break; + } + } + if (x >= displayWidth) { + // No changed pixels found in these 8 rows, fast-forward to the next 8 + y = y + 8; + continue; + } } - } else { - for (x = 0; x < displayWidth; x++) { - if (buffer[x + y_byteIndex] != 0) - break; + + // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating + for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { + isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses + dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + break; + } + } else if (isset) { + break; + } } - } - if (x >= displayWidth) { - // No changed pixels found in these 8 rows, fast-forward to the next 8 - y = y + 8; - continue; - } - } - // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating - for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { - isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + // Did we find a pixel that needs updating on this row? + if (x_FirstPixelUpdate < displayWidth) { - if (!fromBlank) { - // get src pixel in the page based ordering the OLED lib uses - dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; - if (isset != dblbuf_isset) { - break; - } - } else if (isset) { - break; - } - } + // Quickly write out the first changed pixel (saves another array lookup) + linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; + x_LastPixelUpdate = x_FirstPixelUpdate; - // Did we find a pixel that needs updating on this row? - if (x_FirstPixelUpdate < displayWidth) { + // Step 3: copy all remaining pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating + for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; + linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; - // Quickly write out the first changed pixel (saves another array lookup) - linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; - x_LastPixelUpdate = x_FirstPixelUpdate; - - // Step 3: copy all remaining pixels in this row into the pixel line buffer, - // while also recording the last pixel in the row that needs updating - for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { - isset = buffer[x + y_byteIndex] & y_byteMask; - linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; - - if (!fromBlank) { - dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; - if (isset != dblbuf_isset) { - x_LastPixelUpdate = x; - } - } else if (isset) { - x_LastPixelUpdate = x; - } - } + if (!fromBlank) { + dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + x_LastPixelUpdate = x; + } + } else if (isset) { + x_LastPixelUpdate = x; + } + } #if defined(HACKADAY_COMMUNICATOR) - tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); + tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], + (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); #else - // Step 4: Send the changed pixels on this line to the screen as a single block transfer. - // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. - tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, &linePixelBuffer[x_FirstPixelUpdate]); + // Step 4: Send the changed pixels on this line to the screen as a single block transfer. + // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. + tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, + &linePixelBuffer[x_FirstPixelUpdate]); #endif - somethingChanged = true; + somethingChanged = true; + } + y++; } - y++; - } - // Copy the Buffer to the Back Buffer - if (somethingChanged) - memcpy(buffer_back, buffer, displayBufferSize); + // Copy the Buffer to the Back Buffer + if (somethingChanged) + memcpy(buffer_back, buffer, displayBufferSize); } -void TFTDisplay::sdlLoop() { +void TFTDisplay::sdlLoop() +{ #if defined(SDL_h_) - static int lastPressed = 0; - static int shuttingDown = false; - if (portduino_config.displayPanel == x11) { - lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; - if (sdl_panel_->loop() && !shuttingDown) { - LOG_WARN("Window Closed!"); - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); + static int lastPressed = 0; + static int shuttingDown = false; + if (portduino_config.displayPanel == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; + if (sdl_panel_->loop() && !shuttingDown) { + LOG_WARN("Window Closed!"); + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + // debounce + if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed)) + return; + if (!sdl_panel_->gpio_in(37)) { + lastPressed = 37; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(36)) { + lastPressed = 36; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(38)) { + lastPressed = 38; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(39)) { + lastPressed = 39; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) { + lastPressed = SDL_SCANCODE_KP_ENTER; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else { + lastPressed = 0; + } } - // debounce - if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed)) - return; - if (!sdl_panel_->gpio_in(37)) { - lastPressed = 37; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(36)) { - lastPressed = 36; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(38)) { - lastPressed = 38; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(39)) { - lastPressed = 39; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) { - lastPressed = SDL_SCANCODE_KP_ENTER; - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else { - lastPressed = 0; - } - } #endif } // Send a command to the display (low level function) -void TFTDisplay::sendCommand(uint8_t com) { - // handle display on/off directly - switch (com) { - case DISPLAYON: { - // LOG_DEBUG("Display on"); - backlightEnable->set(true); +void TFTDisplay::sendCommand(uint8_t com) +{ + // handle display on/off directly + switch (com) { + case DISPLAYON: { + // LOG_DEBUG("Display on"); + backlightEnable->set(true); #if ARCH_PORTDUINO - display(true); - if (portduino_config.displayBacklight.pin > 0) - digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); + display(true); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) - tft->displayOn(); + tft->displayOn(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) - tft->wakeup(); - tft->powerSaveOff(); + tft->wakeup(); + tft->powerSaveOff(); #endif #ifdef VTFT_CTRL - digitalWrite(VTFT_CTRL, LOW); + digitalWrite(VTFT_CTRL, LOW); #endif +#ifdef UNPHONE + unphone.backlight(true); // using unPhone library +#endif +#ifdef RAK14014 +#elif !defined(M5STACK) && !defined(ST7789_CS) && \ + !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function + tft->setBrightness(172); +#endif + break; + } + case DISPLAYOFF: { + // LOG_DEBUG("Display off"); + backlightEnable->set(false); +#if ARCH_PORTDUINO + tft->clear(); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); +#elif defined(HACKADAY_COMMUNICATOR) + tft->displayOff(); +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) + tft->sleep(); + tft->powerSaveOn(); +#endif + +#ifdef VTFT_CTRL + digitalWrite(VTFT_CTRL, HIGH); +#endif +#ifdef UNPHONE + unphone.backlight(false); // using unPhone library +#endif +#ifdef RAK14014 +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) + tft->setBrightness(0); +#endif + break; + } + default: + break; + } + + // Drop all other commands to device (we just update the buffer) +} + +void TFTDisplay::setDisplayBrightness(uint8_t _brightness) +{ +#ifdef RAK14014 + // todo +#elif !defined(HACKADAY_COMMUNICATOR) + tft->setBrightness(_brightness); + LOG_DEBUG("Brightness is set to value: %i ", _brightness); +#endif +} + +void TFTDisplay::flipScreenVertically() +{ +#if defined(T_WATCH_S3) + LOG_DEBUG("Flip TFT vertically"); // T-Watch S3 right-handed orientation + tft->setRotation(0); +#endif +} + +bool TFTDisplay::hasTouch(void) +{ +#ifdef RAK14014 + return true; +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) + return tft->touch() != nullptr; +#else + return false; +#endif +} + +bool TFTDisplay::getTouch(int16_t *x, int16_t *y) +{ +#ifdef RAK14014 + if (_rak14014_touch_int) { + _rak14014_touch_int = false; + /* The X and Y axes have to be switched */ + *y = ft6336u.read_touch1_x(); + *x = TFT_HEIGHT - ft6336u.read_touch1_y(); + return true; + } else { + return false; + } +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) + return tft->getTouch(x, y); +#else + return false; +#endif +} + +void TFTDisplay::setDetected(uint8_t detected) +{ + (void)detected; +} + +// Connect to the display +bool TFTDisplay::connect() +{ + concurrency::LockGuard g(spiLock); + LOG_INFO("Do TFT init"); +#ifdef RAK14014 + tft = new TFT_eSPI; +#elif defined(HACKADAY_COMMUNICATOR) + bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); + tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, + 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, + sizeof(nv3007_279_init_operations)); + +#else + tft = new LGFX; +#endif + + backlightEnable->set(true); + LOG_INFO("Power to TFT Backlight"); + #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#ifdef RAK14014 -#elif !defined(M5STACK) && !defined(ST7789_CS) && \ - !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function - tft->setBrightness(172); -#endif - break; - } - case DISPLAYOFF: { - // LOG_DEBUG("Display off"); - backlightEnable->set(false); -#if ARCH_PORTDUINO - tft->clear(); - if (portduino_config.displayBacklight.pin > 0) - digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); -#elif defined(HACKADAY_COMMUNICATOR) - tft->displayOff(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) - tft->sleep(); - tft->powerSaveOn(); -#endif - -#ifdef VTFT_CTRL - digitalWrite(VTFT_CTRL, HIGH); -#endif -#ifdef UNPHONE - unphone.backlight(false); // using unPhone library -#endif -#ifdef RAK14014 -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) - tft->setBrightness(0); -#endif - break; - } - default: - break; - } - - // Drop all other commands to device (we just update the buffer) -} - -void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { -#ifdef RAK14014 - // todo -#elif !defined(HACKADAY_COMMUNICATOR) - tft->setBrightness(_brightness); - LOG_DEBUG("Brightness is set to value: %i ", _brightness); -#endif -} - -void TFTDisplay::flipScreenVertically() { -#if defined(T_WATCH_S3) - LOG_DEBUG("Flip TFT vertically"); // T-Watch S3 right-handed orientation - tft->setRotation(0); -#endif -} - -bool TFTDisplay::hasTouch(void) { -#ifdef RAK14014 - return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) - return tft->touch() != nullptr; -#else - return false; -#endif -} - -bool TFTDisplay::getTouch(int16_t *x, int16_t *y) { -#ifdef RAK14014 - if (_rak14014_touch_int) { - _rak14014_touch_int = false; - /* The X and Y axes have to be switched */ - *y = ft6336u.read_touch1_x(); - *x = TFT_HEIGHT - ft6336u.read_touch1_y(); - return true; - } else { - return false; - } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) - return tft->getTouch(x, y); -#else - return false; -#endif -} - -void TFTDisplay::setDetected(uint8_t detected) { (void)detected; } - -// Connect to the display -bool TFTDisplay::connect() { - concurrency::LockGuard g(spiLock); - LOG_INFO("Do TFT init"); -#ifdef RAK14014 - tft = new TFT_eSPI; -#elif defined(HACKADAY_COMMUNICATOR) - bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); - tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, 0 /* row offset 1 */, - 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, sizeof(nv3007_279_init_operations)); - -#else - tft = new LGFX; -#endif - - backlightEnable->set(true); - LOG_INFO("Power to TFT Backlight"); - -#ifdef UNPHONE - unphone.backlight(true); // using unPhone library -#endif #ifdef HACKADAY_COMMUNICATOR - bool beginStatus = tft->begin(); - if (beginStatus) - LOG_DEBUG("TFT Success!"); - else - LOG_ERROR("TFT Fail!"); + bool beginStatus = tft->begin(); + if (beginStatus) + LOG_DEBUG("TFT Success!"); + else + LOG_ERROR("TFT Fail!"); #else - tft->init(); + tft->init(); #endif #if defined(M5STACK) - tft->setRotation(0); + tft->setRotation(0); #elif defined(RAK14014) - tft->setRotation(1); - tft->setSwapBytes(true); - // tft->fillScreen(TFT_BLACK); - ft6336u.begin(); - pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); + tft->setRotation(1); + tft->setSwapBytes(true); + // tft->fillScreen(TFT_BLACK); + ft6336u.begin(); + pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); #elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) - tft->setRotation(1); // T-Deck has the TFT in landscape + tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) - tft->setRotation(2); // T-Watch S3 left-handed orientation + tft->setRotation(2); // T-Watch S3 left-handed orientation #elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) - tft->setRotation(0); // use config.yaml to set rotation + tft->setRotation(0); // use config.yaml to set rotation #else - tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label + tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif - tft->fillScreen(TFT_BLACK); + tft->fillScreen(TFT_BLACK); - if (this->linePixelBuffer == NULL) { - this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); + if (this->linePixelBuffer == NULL) { + this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); - if (!this->linePixelBuffer) { - LOG_ERROR("Not enough memory to create TFT line buffer\n"); - return false; + if (!this->linePixelBuffer) { + LOG_ERROR("Not enough memory to create TFT line buffer\n"); + return false; + } } - } - return true; + return true; } #endif // USE_TFTDISPLAY diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 0fa65633a..a64922d23 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -12,54 +12,55 @@ * * turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted? */ -class TFTDisplay : public OLEDDisplay { -public: - /* constructor - FIXME - the parameters are not used, just a temporary hack to keep working like the old displays - */ - TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); +class TFTDisplay : public OLEDDisplay +{ + public: + /* constructor + FIXME - the parameters are not used, just a temporary hack to keep working like the old displays + */ + TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); - // Destructor to clean up allocated memory - ~TFTDisplay(); + // Destructor to clean up allocated memory + ~TFTDisplay(); - // Write the buffer to the display memory - virtual void display() override { display(false); }; - virtual void display(bool fromBlank); - void sdlLoop(); + // Write the buffer to the display memory + virtual void display() override { display(false); }; + virtual void display(bool fromBlank); + void sdlLoop(); - // Turn the display upside down - virtual void flipScreenVertically(); + // Turn the display upside down + virtual void flipScreenVertically(); - // Touch screen (static handlers) - static bool hasTouch(void); - static bool getTouch(int16_t *x, int16_t *y); + // Touch screen (static handlers) + static bool hasTouch(void); + static bool getTouch(int16_t *x, int16_t *y); - // Functions for changing display brightness - void setDisplayBrightness(uint8_t); + // Functions for changing display brightness + void setDisplayBrightness(uint8_t); - /** - * shim to make the abstraction happy - * - */ - void setDetected(uint8_t detected); + /** + * shim to make the abstraction happy + * + */ + void setDetected(uint8_t detected); - /** - * This is normally managed entirely by TFTDisplay, but some rare applications (heltec tracker) might need to replace - * the default GPIO behavior with something a bit more complex. - * - * We (cruftily) make it static so that variant.cpp can access it without needing a ptr to the TFTDisplay instance. - */ - static GpioPin *backlightEnable; + /** + * This is normally managed entirely by TFTDisplay, but some rare applications (heltec tracker) might need to replace the + * default GPIO behavior with something a bit more complex. + * + * We (cruftily) make it static so that variant.cpp can access it without needing a ptr to the TFTDisplay instance. + */ + static GpioPin *backlightEnable; -protected: - // the header size of the buffer used, e.g. for the SPI command header - virtual int getBufferOffset(void) override { return 0; } + protected: + // the header size of the buffer used, e.g. for the SPI command header + virtual int getBufferOffset(void) override { return 0; } - // Send a command to the display (low level function) - virtual void sendCommand(uint8_t com) override; + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) override; - // Connect to the display - virtual bool connect() override; + // Connect to the display + virtual bool connect() override; - uint16_t *linePixelBuffer = nullptr; + uint16_t *linePixelBuffer = nullptr; }; \ No newline at end of file diff --git a/src/graphics/TimeFormatters.cpp b/src/graphics/TimeFormatters.cpp index 6f41a934f..0a1c23341 100644 --- a/src/graphics/TimeFormatters.cpp +++ b/src/graphics/TimeFormatters.cpp @@ -4,117 +4,120 @@ #include "mesh/NodeDB.h" #include -bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) { - // Cache the result - avoid frequent recalculation - static uint8_t hoursCached = 0, minutesCached = 0; - static uint32_t daysAgoCached = 0; - static uint32_t secondsAgoCached = 0; - static bool validCached = false; +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) +{ + // Cache the result - avoid frequent recalculation + static uint8_t hoursCached = 0, minutesCached = 0; + static uint32_t daysAgoCached = 0; + static uint32_t secondsAgoCached = 0; + static bool validCached = false; - // Abort: if timezone not set - if (strlen(config.device.tzdef) == 0) { - validCached = false; - return validCached; - } - - // Abort: if invalid pointers passed - if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { - validCached = false; - return validCached; - } - - // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) - if (secondsAgo > SEC_PER_DAY * 30UL * 6) { - validCached = false; - return validCached; - } - - // If repeated request, don't bother recalculating - if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { - if (validCached) { - *hours = hoursCached; - *minutes = minutesCached; - *daysAgo = daysAgoCached; + // Abort: if timezone not set + if (strlen(config.device.tzdef) == 0) { + validCached = false; + return validCached; } + + // Abort: if invalid pointers passed + if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { + validCached = false; + return validCached; + } + + // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) + if (secondsAgo > SEC_PER_DAY * 30UL * 6) { + validCached = false; + return validCached; + } + + // If repeated request, don't bother recalculating + if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { + if (validCached) { + *hours = hoursCached; + *minutes = minutesCached; + *daysAgo = daysAgoCached; + } + return validCached; + } + + // Get local time + uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time + + // Abort: if RTC not set + if (!secondsRTC) { + validCached = false; + return validCached; + } + + // Get absolute time when last seen + uint32_t secondsSeenAt = secondsRTC - secondsAgo; + + // Calculate daysAgo + *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed + + // Get seconds since midnight + uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into hours and minutes + *hours = hms / SEC_PER_HOUR; + *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Cache the result + daysAgoCached = *daysAgo; + hoursCached = *hours; + minutesCached = *minutes; + secondsAgoCached = secondsAgo; + + validCached = true; return validCached; - } - - // Get local time - uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time - - // Abort: if RTC not set - if (!secondsRTC) { - validCached = false; - return validCached; - } - - // Get absolute time when last seen - uint32_t secondsSeenAt = secondsRTC - secondsAgo; - - // Calculate daysAgo - *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed - - // Get seconds since midnight - uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into hours and minutes - *hours = hms / SEC_PER_HOUR; - *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - - // Cache the result - daysAgoCached = *daysAgo; - hoursCached = *hours; - minutesCached = *minutes; - secondsAgoCached = secondsAgo; - - validCached = true; - return validCached; } -void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) { - // Use an absolute timestamp in some cases. - // Particularly useful with E-Ink displays. Static UI, fewer refreshes. - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) +{ + // Use an absolute timestamp in some cases. + // Particularly useful with E-Ink displays. Static UI, fewer refreshes. + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); - if (agoSecs < 120) // last 2 mins? - snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); - // -- if suitable for timestamp -- - else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); - else if (useTimestamp && daysAgo == 0) // Today - snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); - else if (useTimestamp && daysAgo == 1) // Yesterday - snprintf(timeStr, maxLength, "Seen yesterday"); - else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) - snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); - // -- if using time delta instead -- - else if (agoSecs < 120 * 60) // last 2 hrs - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); - // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. - else if ((agoSecs / 60 / 60) < (730 * 6)) - snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); - else - snprintf(timeStr, maxLength, "unknown age"); + if (agoSecs < 120) // last 2 mins? + snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); + // -- if suitable for timestamp -- + else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); + else if (useTimestamp && daysAgo == 0) // Today + snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); + else if (useTimestamp && daysAgo == 1) // Yesterday + snprintf(timeStr, maxLength, "Seen yesterday"); + else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) + snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); + // -- if using time delta instead -- + else if (agoSecs < 120 * 60) // last 2 hrs + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); + // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. + else if ((agoSecs / 60 / 60) < (730 * 6)) + snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); + else + snprintf(timeStr, maxLength, "unknown age"); } -void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs) { - uint32_t days = uptimeMillis / 86400000; - uint32_t hours = (uptimeMillis % 86400000) / 3600000; - uint32_t mins = (uptimeMillis % 3600000) / 60000; - uint32_t secs = (uptimeMillis % 60000) / 1000; +void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs) +{ + uint32_t days = uptimeMillis / 86400000; + uint32_t hours = (uptimeMillis % 86400000) / 3600000; + uint32_t mins = (uptimeMillis % 3600000) / 60000; + uint32_t secs = (uptimeMillis % 60000) / 1000; - if (days) { - snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours); - } else if (hours) { - snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins); - } else if (!includeSecs) { - snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins); - } else if (mins) { - snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs); - } else { - snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs); - } + if (days) { + snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours); + } else if (hours) { + snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins); + } else if (!includeSecs) { + snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins); + } else if (mins) { + snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs); + } else { + snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs); + } } diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index 6672eb5e9..a24f5b15c 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -8,695 +8,734 @@ #include #include -namespace graphics { +namespace graphics +{ -VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) { - initializeKeyboard(); - // Set cursor to H(2, 5) - cursorRow = 2; - cursorCol = 5; +VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) +{ + initializeKeyboard(); + // Set cursor to H(2, 5) + cursorRow = 2; + cursorCol = 5; } VirtualKeyboard::~VirtualKeyboard() {} -void VirtualKeyboard::initializeKeyboard() { - // New 4 row, 11 column keyboard layout: - static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, - {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; +void VirtualKeyboard::initializeKeyboard() +{ + // New 4 row, 11 column keyboard layout: + static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; - // Derive layout dimensions and assert they match the configured keyboard grid - constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); - constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); - static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); - static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); + // Derive layout dimensions and assert they match the configured keyboard grid + constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); + constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); + static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); + static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); - // Initialize all keys to empty first - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; + // Initialize all keys to empty first + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; + } } - } - // Fill keyboard from the 2D layout - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - char ch = LAYOUT[row][col]; - // No empty slots in the simplified layout + // Fill keyboard from the 2D layout + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + char ch = LAYOUT[row][col]; + // No empty slots in the simplified layout - VirtualKeyType type = VK_CHAR; - if (ch == '\b') { - type = VK_BACKSPACE; - } else if (ch == '\n') { - type = VK_ENTER; - } else if (ch == '\x1b') { // ESC - type = VK_ESC; - } else if (ch == ' ') { - type = VK_SPACE; - } + VirtualKeyType type = VK_CHAR; + if (ch == '\b') { + type = VK_BACKSPACE; + } else if (ch == '\n') { + type = VK_ENTER; + } else if (ch == '\x1b') { // ESC + type = VK_ESC; + } else if (ch == ' ') { + type = VK_SPACE; + } - // Make action keys wider to fit text while keeping the last column aligned - uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; - keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; + // Make action keys wider to fit text while keeping the last column aligned + uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; + keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; + } } - } } -void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) { - // Repeat ticking is driven by NotificationRenderer once per frame - // Base styles - display->setColor(WHITE); - display->setFont(FONT_SMALL); - - // Screen geometry - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); - - // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column - // labels Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide - const bool isWide = screenW >= 200; - - // Determine last-column label max width - display->setFont(FONT_SMALL); - const int wENTER = display->getStringWidth("ENTER"); - int lastColLabelW = wENTER; // ENTER is usually the widest - // Smaller padding on very small screens to avoid excessive whitespace - const int lastColPad = (screenW <= 128 ? 2 : 6); - const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys - - // Always reserve width for the rightmost text column to avoid overlap on small screens - int cellW = 0; - int leftoverW = 0; - { - const int leftCols = KEYBOARD_COLS - 1; // 10 input characters - int usableW = screenW - reservedLastColW; - if (usableW < leftCols) { - // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) - usableW = leftCols; - } - cellW = usableW / leftCols; - leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) - } - - // Dynamic key geometry - int cellH = KEY_HEIGHT; - int keyboardStartY = 0; - if (screenH <= 64) { - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); - const int gapBelowHeader = 0; - const int singleLineBoxHeight = FONT_HEIGHT_SMALL; - const int gapAboveKeyboard = 0; - keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; - if (keyboardStartY < 0) - keyboardStartY = 0; - if (keyboardStartY > screenH) - keyboardStartY = screenH; - int keyboardHeight = screenH - keyboardStartY; - cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); - } else if (isWide) { - // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. - cellH = std::max((int)KEY_HEIGHT, cellW); - - // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. - // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 +void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) +{ + // Repeat ticking is driven by NotificationRenderer once per frame + // Base styles + display->setColor(WHITE); display->setFont(FONT_SMALL); - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); - const int headerToBoxGap = 1; - const int gapAboveKb = 1; - const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom - int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); - int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; - if (maxCellHAllowed < (int)KEY_HEIGHT) - maxCellHAllowed = KEY_HEIGHT; - if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { - cellH = maxCellHAllowed; + + // Screen geometry + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels + // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide + const bool isWide = screenW >= 200; + + // Determine last-column label max width + display->setFont(FONT_SMALL); + const int wENTER = display->getStringWidth("ENTER"); + int lastColLabelW = wENTER; // ENTER is usually the widest + // Smaller padding on very small screens to avoid excessive whitespace + const int lastColPad = (screenW <= 128 ? 2 : 6); + const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys + + // Always reserve width for the rightmost text column to avoid overlap on small screens + int cellW = 0; + int leftoverW = 0; + { + const int leftCols = KEYBOARD_COLS - 1; // 10 input characters + int usableW = screenW - reservedLastColW; + if (usableW < leftCols) { + // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) + usableW = leftCols; + } + cellW = usableW / leftCols; + leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) } - // Keyboard placement from bottom for wide screens - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } else { - // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom - cellH = KEY_HEIGHT; - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } - // Draw input area above keyboard - drawInputArea(display, offsetX, offsetY, keyboardStartY); + // Dynamic key geometry + int cellH = KEY_HEIGHT; + int keyboardStartY = 0; + if (screenH <= 64) { + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); + const int gapBelowHeader = 0; + const int singleLineBoxHeight = FONT_HEIGHT_SMALL; + const int gapAboveKeyboard = 0; + keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; + if (keyboardStartY < 0) + keyboardStartY = 0; + if (keyboardStartY > screenH) + keyboardStartY = screenH; + int keyboardHeight = screenH - keyboardStartY; + cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); + } else if (isWide) { + // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. + cellH = std::max((int)KEY_HEIGHT, cellW); - // Precompute per-column x and width with leftover distributed over left columns for even spacing - int colX[KEYBOARD_COLS]; - int colW[KEYBOARD_COLS]; - int runningX = offsetX; - for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { - int wcol = cellW + (col < leftoverW ? 1 : 0); - colX[col] = runningX; - colW[col] = wcol; - runningX += wcol; - } - // Last column - colX[KEYBOARD_COLS - 1] = runningX; - colW[KEYBOARD_COLS - 1] = reservedLastColW; - - // Draw keyboard grid - for (int row = 0; row < KEYBOARD_ROWS; row++) { - for (int col = 0; col < KEYBOARD_COLS; col++) { - const VirtualKey &k = keyboard[row][col]; - if (k.character != 0 || k.type != VK_CHAR) { - const bool isLastCol = (col == KEYBOARD_COLS - 1); - int x = colX[col]; - int w = colW[col]; - int y = offsetY + keyboardStartY + row * cellH; - int h = cellH; - bool selected = (row == cursorRow && col == cursorCol); - drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); - } + // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. + // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 + display->setFont(FONT_SMALL); + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); + const int headerToBoxGap = 1; + const int gapAboveKb = 1; + const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom + int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); + int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; + if (maxCellHAllowed < (int)KEY_HEIGHT) + maxCellHAllowed = KEY_HEIGHT; + if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { + cellH = maxCellHAllowed; + } + // Keyboard placement from bottom for wide screens + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } else { + // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom + cellH = KEY_HEIGHT; + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } + + // Draw input area above keyboard + drawInputArea(display, offsetX, offsetY, keyboardStartY); + + // Precompute per-column x and width with leftover distributed over left columns for even spacing + int colX[KEYBOARD_COLS]; + int colW[KEYBOARD_COLS]; + int runningX = offsetX; + for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { + int wcol = cellW + (col < leftoverW ? 1 : 0); + colX[col] = runningX; + colW[col] = wcol; + runningX += wcol; + } + // Last column + colX[KEYBOARD_COLS - 1] = runningX; + colW[KEYBOARD_COLS - 1] = reservedLastColW; + + // Draw keyboard grid + for (int row = 0; row < KEYBOARD_ROWS; row++) { + for (int col = 0; col < KEYBOARD_COLS; col++) { + const VirtualKey &k = keyboard[row][col]; + if (k.character != 0 || k.type != VK_CHAR) { + const bool isLastCol = (col == KEYBOARD_COLS - 1); + int x = colX[col]; + int w = colW[col]; + int y = offsetY + keyboardStartY + row * cellH; + int h = cellH; + bool selected = (row == cursorRow && col == cursorCol); + drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); + } + } } - } } -void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) { - display->setColor(WHITE); +void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) +{ + display->setColor(WHITE); - const int screenWidth = display->getWidth(); - const int screenHeight = display->getHeight(); - // Use the standard small font metrics for input box sizing (restore original size) - const int inputLineH = FONT_HEIGHT_SMALL; + const int screenWidth = display->getWidth(); + const int screenHeight = display->getHeight(); + // Use the standard small font metrics for input box sizing (restore original size) + const int inputLineH = FONT_HEIGHT_SMALL; - // Header uses the standard small (which may be larger on big screens) - display->setFont(FONT_SMALL); - int headerHeight = 0; - if (!headerText.empty()) { - // Draw header and reserve exact font height (plus a tighter gap) to maximize input area - display->drawString(offsetX + 2, offsetY, headerText.c_str()); - if (screenHeight <= 64) { - headerHeight = FONT_HEIGHT_SMALL - 2; // 11px - } else { - headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in - } - } - - const int boxX = offsetX; - const int boxWidth = screenWidth; - int boxY; - int boxHeight; - if (screenHeight <= 64) { - const int gapBelowHeader = 0; - const int fixedBoxHeight = inputLineH; - const int gapAboveKeyboard = 0; - boxY = offsetY + headerHeight + gapBelowHeader; - boxHeight = fixedBoxHeight; - if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { - int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; - boxHeight = std::max(1, fixedBoxHeight - over); - } - } else { - const int gapBelowHeader = 1; - int gapAboveKeyboard = 1; - int tmpBoxY = offsetY + headerHeight + gapBelowHeader; - const int minBoxHeight = inputLineH + 2; - int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; - if (availableH < minBoxHeight) - availableH = minBoxHeight; - boxY = tmpBoxY; - boxHeight = availableH; - } - - // Draw box border - display->drawRect(boxX, boxY, boxWidth, boxHeight); - - display->setFont(FONT_SMALL); - - // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis - const int textX = boxX + 2; - const int maxTextWidth = boxWidth - 4; - const int maxLines = (boxHeight - 2) / inputLineH; - if (maxLines >= 2) { - // Inner bounds for caret clamping - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Wrap text greedily into lines that fit maxTextWidth - std::vector lines; - { - std::string remaining = inputText; - while (!remaining.empty()) { - int bestLen = 0; - for (int len = 1; len <= (int)remaining.size(); ++len) { - int w = display->getStringWidth(remaining.substr(0, len).c_str()); - if (w <= maxTextWidth) - bestLen = len; - else - break; + // Header uses the standard small (which may be larger on big screens) + display->setFont(FONT_SMALL); + int headerHeight = 0; + if (!headerText.empty()) { + // Draw header and reserve exact font height (plus a tighter gap) to maximize input area + display->drawString(offsetX + 2, offsetY, headerText.c_str()); + if (screenHeight <= 64) { + headerHeight = FONT_HEIGHT_SMALL - 2; // 11px + } else { + headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in } - if (bestLen == 0) { - // At least show one character to make progress - bestLen = 1; - } - lines.emplace_back(remaining.substr(0, bestLen)); - remaining.erase(0, bestLen); - } } - const bool scrolledUp = ((int)lines.size() > maxLines); - int caretX = textX; - int caretY = innerTop; - - // Leave a small top gap to render '...' without replacing the first line - const int topInset = 2; - const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height - int lineY = innerTop + topInset; - - if (scrolledUp) { - // Draw three small dots centered horizontally, vertically at the midpoint of the gap - // between the inner top and the first line's top baseline. This avoids using a tall glyph. - const int firstLineTop = lineY; // baseline top for the first visible line - const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested - const int centerX = boxX + boxWidth / 2; - const int dotSpacing = 3; // px between dots - const int dotSize = 1; // small square dot - display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); - display->fillRect(centerX, gapMidY, dotSize, dotSize); - display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); - } - - // How many lines fit with our top inset and tighter step - const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); - const int linesToShow = std::min((int)lines.size(), linesCapacity); - const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; - - for (int i = 0; i < linesToShow; ++i) { - const std::string &chunk = lines[startIndex + i]; - display->drawString(textX, lineY, chunk.c_str()); - caretX = textX + display->getStringWidth(chunk.c_str()); - caretY = lineY; - lineY += lineStep; - } - - // Draw caret at end of the last visible line - int caretPadY = 2; - if (boxHeight >= inputLineH + 4) - caretPadY = 3; - int cursorTop = caretY + caretPadY; - // Use lineStep so caret height matches the row spacing - int cursorH = lineStep - caretPadY * 2; - if (cursorH < 1) - cursorH = 1; - // Clamp vertical bounds to stay inside the inner rect - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - // Only draw if cursor is inside inner bounds - if (caretX >= innerLeft && caretX <= innerRight) { - display->drawVerticalLine(caretX, cursorTop, cursorH); - } - } else { - std::string displayText = inputText; - int textW = display->getStringWidth(displayText.c_str()); - std::string scrolled = displayText; - if (textW > maxTextWidth) { - // Trim from the left until it fits - while (textW > maxTextWidth && !scrolled.empty()) { - scrolled.erase(0, 1); - textW = display->getStringWidth(scrolled.c_str()); - } - // Add leading ellipsis and ensure it still fits - if (scrolled != displayText) { - scrolled = "..." + scrolled; - textW = display->getStringWidth(scrolled.c_str()); - // If adding ellipsis causes overflow, trim more after the ellipsis - while (textW > maxTextWidth && scrolled.size() > 3) { - scrolled.erase(3, 1); // remove chars after the ellipsis - textW = display->getStringWidth(scrolled.c_str()); - } - } - } else { - // Keep textW in sync with what we draw - textW = display->getStringWidth(scrolled.c_str()); - } - - int textY; + const int boxX = offsetX; + const int boxWidth = screenWidth; + int boxY; + int boxHeight; if (screenHeight <= 64) { - textY = boxY + (boxHeight - inputLineH) / 2; + const int gapBelowHeader = 0; + const int fixedBoxHeight = inputLineH; + const int gapAboveKeyboard = 0; + boxY = offsetY + headerHeight + gapBelowHeader; + boxHeight = fixedBoxHeight; + if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { + int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; + boxHeight = std::max(1, fixedBoxHeight - over); + } } else { - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Center text vertically within inner box for single-line, then clamp so it never overlaps borders - int innerH = innerBottom - innerTop + 1; - textY = innerTop + std::max(0, (innerH - inputLineH) / 2); - // Clamp fully inside the inner rect - if (textY < innerTop) - textY = innerTop; - int maxTop = innerBottom - inputLineH + 1; - if (textY > maxTop) - textY = maxTop; + const int gapBelowHeader = 1; + int gapAboveKeyboard = 1; + int tmpBoxY = offsetY + headerHeight + gapBelowHeader; + const int minBoxHeight = inputLineH + 2; + int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; + if (availableH < minBoxHeight) + availableH = minBoxHeight; + boxY = tmpBoxY; + boxHeight = availableH; } - if (!scrolled.empty()) { - display->drawString(textX, textY, scrolled.c_str()); - } + // Draw box border + display->drawRect(boxX, boxY, boxWidth, boxHeight); - int cursorX = textX + textW; - if (screenHeight > 64) { - const int innerRight = boxX + boxWidth - 2; - if (cursorX > innerRight) - cursorX = innerRight; - } + display->setFont(FONT_SMALL); - int cursorTop, cursorH; - if (screenHeight <= 64) { - cursorH = 10; - cursorTop = boxY + (boxHeight - cursorH) / 2; + // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis + const int textX = boxX + 2; + const int maxTextWidth = boxWidth - 4; + const int maxLines = (boxHeight - 2) / inputLineH; + if (maxLines >= 2) { + // Inner bounds for caret clamping + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Wrap text greedily into lines that fit maxTextWidth + std::vector lines; + { + std::string remaining = inputText; + while (!remaining.empty()) { + int bestLen = 0; + for (int len = 1; len <= (int)remaining.size(); ++len) { + int w = display->getStringWidth(remaining.substr(0, len).c_str()); + if (w <= maxTextWidth) + bestLen = len; + else + break; + } + if (bestLen == 0) { + // At least show one character to make progress + bestLen = 1; + } + lines.emplace_back(remaining.substr(0, bestLen)); + remaining.erase(0, bestLen); + } + } + + const bool scrolledUp = ((int)lines.size() > maxLines); + int caretX = textX; + int caretY = innerTop; + + // Leave a small top gap to render '...' without replacing the first line + const int topInset = 2; + const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height + int lineY = innerTop + topInset; + + if (scrolledUp) { + // Draw three small dots centered horizontally, vertically at the midpoint of the gap + // between the inner top and the first line's top baseline. This avoids using a tall glyph. + const int firstLineTop = lineY; // baseline top for the first visible line + const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested + const int centerX = boxX + boxWidth / 2; + const int dotSpacing = 3; // px between dots + const int dotSize = 1; // small square dot + display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); + display->fillRect(centerX, gapMidY, dotSize, dotSize); + display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); + } + + // How many lines fit with our top inset and tighter step + const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); + const int linesToShow = std::min((int)lines.size(), linesCapacity); + const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; + + for (int i = 0; i < linesToShow; ++i) { + const std::string &chunk = lines[startIndex + i]; + display->drawString(textX, lineY, chunk.c_str()); + caretX = textX + display->getStringWidth(chunk.c_str()); + caretY = lineY; + lineY += lineStep; + } + + // Draw caret at end of the last visible line + int caretPadY = 2; + if (boxHeight >= inputLineH + 4) + caretPadY = 3; + int cursorTop = caretY + caretPadY; + // Use lineStep so caret height matches the row spacing + int cursorH = lineStep - caretPadY * 2; + if (cursorH < 1) + cursorH = 1; + // Clamp vertical bounds to stay inside the inner rect + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + // Only draw if cursor is inside inner bounds + if (caretX >= innerLeft && caretX <= innerRight) { + display->drawVerticalLine(caretX, cursorTop, cursorH); + } } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; + std::string displayText = inputText; + int textW = display->getStringWidth(displayText.c_str()); + std::string scrolled = displayText; + if (textW > maxTextWidth) { + // Trim from the left until it fits + while (textW > maxTextWidth && !scrolled.empty()) { + scrolled.erase(0, 1); + textW = display->getStringWidth(scrolled.c_str()); + } + // Add leading ellipsis and ensure it still fits + if (scrolled != displayText) { + scrolled = "..." + scrolled; + textW = display->getStringWidth(scrolled.c_str()); + // If adding ellipsis causes overflow, trim more after the ellipsis + while (textW > maxTextWidth && scrolled.size() > 3) { + scrolled.erase(3, 1); // remove chars after the ellipsis + textW = display->getStringWidth(scrolled.c_str()); + } + } + } else { + // Keep textW in sync with what we draw + textW = display->getStringWidth(scrolled.c_str()); + } - cursorTop = boxY + 2; - cursorH = boxHeight - 4; - if (cursorH < 1) - cursorH = 1; - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; + int textY; + if (screenHeight <= 64) { + textY = boxY + (boxHeight - inputLineH) / 2; + } else { + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; - if (cursorX < innerLeft || cursorX > innerRight) + // Center text vertically within inner box for single-line, then clamp so it never overlaps borders + int innerH = innerBottom - innerTop + 1; + textY = innerTop + std::max(0, (innerH - inputLineH) / 2); + // Clamp fully inside the inner rect + if (textY < innerTop) + textY = innerTop; + int maxTop = innerBottom - inputLineH + 1; + if (textY > maxTop) + textY = maxTop; + } + + if (!scrolled.empty()) { + display->drawString(textX, textY, scrolled.c_str()); + } + + int cursorX = textX + textW; + if (screenHeight > 64) { + const int innerRight = boxX + boxWidth - 2; + if (cursorX > innerRight) + cursorX = innerRight; + } + + int cursorTop, cursorH; + if (screenHeight <= 64) { + cursorH = 10; + cursorTop = boxY + (boxHeight - cursorH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + cursorTop = boxY + 2; + cursorH = boxHeight - 4; + if (cursorH < 1) + cursorH = 1; + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + + if (cursorX < innerLeft || cursorX > innerRight) + return; + } + + display->drawVerticalLine(cursorX, cursorTop, cursorH); + } +} + +void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, + uint8_t height, bool isLastCol) +{ + // Draw key content + display->setFont(FONT_SMALL); + const int fontH = FONT_HEIGHT_SMALL; + // Build label and metrics first + std::string keyText; + if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { + // Keep literal text labels for the action keys on the rightmost column + keyText = (key.type == VK_BACKSPACE) ? "BACK" + : (key.type == VK_ENTER) ? "ENTER" + : (key.type == VK_SPACE) ? "SPACE" + : (key.type == VK_ESC) ? "ESC" + : ""; + } else { + char c = getCharForKey(key, false); + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + } + + int textWidth = display->getStringWidth(keyText.c_str()); + // Label alignment + // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. + // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. + int textX; + if (isLastCol) { + const int rightPad = 1; + textX = x + width - textWidth - rightPad; + if (textX < x) + textX = x; // guard + } else { + if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { + textX = x + (width - textWidth + 1) / 2; + } else { + textX = x + (width - textWidth) / 2; + } + } + int contentTop = y; + int contentH = height; + if (selected) { + display->setColor(WHITE); + bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); + + if (display->getHeight() <= 64 && !isAction) { + display->fillRect(x, y, width, height); + } else if (isAction) { + const int padX = 1; + const int padY = 2; + int hlW = textWidth + padX * 2; + int hlX = textX - padX; + + if (hlX < x) { + hlW -= (x - hlX); + hlX = x; + } + int maxW = (x + width) - hlX; + if (hlW > maxW) + hlW = maxW; + if (hlW < 1) + hlW = 1; + + int hlH = std::min(fontH + padY * 2, (int)height); + int hlY = y + (height - hlH) / 2; + display->fillRect(hlX, hlY, hlW, hlH); + contentTop = hlY; + contentH = hlH; + } else { + display->fillRect(x, y, width, height); + } + display->setColor(BLACK); + } else { + display->setColor(WHITE); + } + + int centeredTextY; + if (display->getHeight() <= 64) { + centeredTextY = y + (height - fontH) / 2; + } else { + centeredTextY = contentTop + (contentH - fontH) / 2; + } + if (display->getHeight() > 64) { + if (centeredTextY < contentTop) + centeredTextY = contentTop; + if (centeredTextY + fontH > contentTop + contentH) + centeredTextY = std::max(contentTop, contentTop + contentH - fontH); + } + + if (display->getHeight() <= 64 && keyText.size() == 1) { + char ch = keyText[0]; + if (ch == '.' || ch == ',' || ch == ';') { + centeredTextY -= 1; + } + } +#ifdef MUZI_BASE // Correct issue with character vertical position on MUZI_BASE + centeredTextY -= 2; +#endif + display->drawString(textX, centeredTextY, keyText.c_str()); +} + +char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) +{ + if (key.type != VK_CHAR) { + return key.character; + } + + char c = key.character; + + // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings + if (isLongPress && c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } + + return c; +} + +void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) +{ + resetTimeout(); + // wrap around rows and cols in the 4x11 grid + int r = (int)cursorRow + dRow; + int c = (int)cursorCol + dCol; + if (r < 0) + r = KEYBOARD_ROWS - 1; + else if (r >= KEYBOARD_ROWS) + r = 0; + if (c < 0) + c = KEYBOARD_COLS - 1; + else if (c >= KEYBOARD_COLS) + c = 0; + cursorRow = (uint8_t)r; + cursorCol = (uint8_t)c; +} + +void VirtualKeyboard::moveCursorUp() +{ + moveCursorDelta(-1, 0); +} +void VirtualKeyboard::moveCursorDown() +{ + moveCursorDelta(1, 0); +} +void VirtualKeyboard::moveCursorLeft() +{ + resetTimeout(); + + if (cursorCol > 0) { + cursorCol--; + } else { + if (cursorRow > 0) { + cursorRow--; + cursorCol = KEYBOARD_COLS - 1; + } else { + cursorRow = KEYBOARD_ROWS - 1; + cursorCol = KEYBOARD_COLS - 1; + } + } +} +void VirtualKeyboard::moveCursorRight() +{ + resetTimeout(); + + if (cursorCol < KEYBOARD_COLS - 1) { + cursorCol++; + } else { + if (cursorRow < KEYBOARD_ROWS - 1) { + cursorRow++; + cursorCol = 0; + } else { + cursorRow = 0; + cursorCol = 0; + } + } +} + +void VirtualKeyboard::handlePress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { return; } - display->drawVerticalLine(cursorX, cursorTop, cursorH); - } -} - -void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, uint8_t height, - bool isLastCol) { - // Draw key content - display->setFont(FONT_SMALL); - const int fontH = FONT_HEIGHT_SMALL; - // Build label and metrics first - std::string keyText; - if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { - // Keep literal text labels for the action keys on the rightmost column - keyText = (key.type == VK_BACKSPACE) ? "BACK" - : (key.type == VK_ENTER) ? "ENTER" - : (key.type == VK_SPACE) ? "SPACE" - : (key.type == VK_ESC) ? "ESC" - : ""; - } else { - char c = getCharForKey(key, false); - if (c >= 'a' && c <= 'z') { - c = c - 'a' + 'A'; + // For character keys, insert lowercase character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char + return; } - keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); - } - int textWidth = display->getStringWidth(keyText.c_str()); - // Label alignment - // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. - // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. - int textX; - if (isLastCol) { - const int rightPad = 1; - textX = x + width - textWidth - rightPad; - if (textX < x) - textX = x; // guard - } else { - if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { - textX = x + (width - textWidth + 1) / 2; - } else { - textX = x + (width - textWidth) / 2; - } - } - int contentTop = y; - int contentH = height; - if (selected) { - display->setColor(WHITE); - bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); - - if (display->getHeight() <= 64 && !isAction) { - display->fillRect(x, y, width, height); - } else if (isAction) { - const int padX = 1; - const int padY = 2; - int hlW = textWidth + padX * 2; - int hlX = textX - padX; - - if (hlX < x) { - hlW -= (x - hlX); - hlX = x; - } - int maxW = (x + width) - hlX; - if (hlW > maxW) - hlW = maxW; - if (hlW < 1) - hlW = 1; - - int hlH = std::min(fontH + padY * 2, (int)height); - int hlY = y + (height - hlH) / 2; - display->fillRect(hlX, hlY, hlW, hlH); - contentTop = hlY; - contentH = hlH; - } else { - display->fillRect(x, y, width, height); - } - display->setColor(BLACK); - } else { - display->setColor(WHITE); - } - - int centeredTextY; - if (display->getHeight() <= 64) { - centeredTextY = y + (height - fontH) / 2; - } else { - centeredTextY = contentTop + (contentH - fontH) / 2; - } - if (display->getHeight() > 64) { - if (centeredTextY < contentTop) - centeredTextY = contentTop; - if (centeredTextY + fontH > contentTop + contentH) - centeredTextY = std::max(contentTop, contentTop + contentH - fontH); - } - - if (display->getHeight() <= 64 && keyText.size() == 1) { - char ch = keyText[0]; - if (ch == '.' || ch == ',' || ch == ';') { - centeredTextY -= 1; - } - } -#ifdef MUZI_BASE // Correct issue with character vertical position on MUZI_BASE - centeredTextY -= 2; -#endif - display->drawString(textX, centeredTextY, keyText.c_str()); -} - -char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) { - if (key.type != VK_CHAR) { - return key.character; - } - - char c = key.character; - - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); - } - - return c; -} - -void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) { - resetTimeout(); - // wrap around rows and cols in the 4x11 grid - int r = (int)cursorRow + dRow; - int c = (int)cursorCol + dCol; - if (r < 0) - r = KEYBOARD_ROWS - 1; - else if (r >= KEYBOARD_ROWS) - r = 0; - if (c < 0) - c = KEYBOARD_COLS - 1; - else if (c >= KEYBOARD_COLS) - c = 0; - cursorRow = (uint8_t)r; - cursorCol = (uint8_t)c; -} - -void VirtualKeyboard::moveCursorUp() { moveCursorDelta(-1, 0); } -void VirtualKeyboard::moveCursorDown() { moveCursorDelta(1, 0); } -void VirtualKeyboard::moveCursorLeft() { - resetTimeout(); - - if (cursorCol > 0) { - cursorCol--; - } else { - if (cursorRow > 0) { - cursorRow--; - cursorCol = KEYBOARD_COLS - 1; - } else { - cursorRow = KEYBOARD_ROWS - 1; - cursorCol = KEYBOARD_COLS - 1; - } - } -} -void VirtualKeyboard::moveCursorRight() { - resetTimeout(); - - if (cursorCol < KEYBOARD_COLS - 1) { - cursorCol++; - } else { - if (cursorRow < KEYBOARD_ROWS - 1) { - cursorRow++; - cursorCol = 0; - } else { - cursorRow = 0; - cursorCol = 0; - } - } -} - -void VirtualKeyboard::handlePress() { - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert lowercase character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char - return; - } - - // Handle non-character keys immediately - switch (key.type) { - case VK_BACKSPACE: - deleteCharacter(); - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - std::function callback = onTextEntered; - onTextEntered = nullptr; - inputText = ""; - callback(""); - } - return; - default: - break; - } -} - -void VirtualKeyboard::handleLongPress() { - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert uppercase/alternate character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char - return; - } - - switch (key.type) { - case VK_BACKSPACE: - // One-shot: delete up to 5 characters on long press - for (int i = 0; i < 5; ++i) { - if (inputText.empty()) + // Handle non-character keys immediately + switch (key.type) { + case VK_BACKSPACE: + deleteCharacter(); + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + std::function callback = onTextEntered; + onTextEntered = nullptr; + inputText = ""; + callback(""); + } + return; + default: break; - deleteCharacter(); } - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - onTextEntered(""); +} + +void VirtualKeyboard::handleLongPress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; } - break; - default: - break; - } -} -void VirtualKeyboard::insertCharacter(char c) { - if (inputText.length() < 160) { // Reasonable text length limit - inputText += c; - } -} - -void VirtualKeyboard::deleteCharacter() { - if (!inputText.empty()) { - inputText.pop_back(); - } -} - -void VirtualKeyboard::submitText() { - LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); - - // Only submit if text is not empty - if (!inputText.empty() && onTextEntered) { - // Store callback and text to submit before clearing callback - std::function callback = onTextEntered; - std::string textToSubmit = inputText; - onTextEntered = nullptr; - // Don't clear inputText here - let the calling module handle cleanup - // inputText = ""; // Removed: keep text visible until module cleans up - callback(textToSubmit); - } else if (inputText.empty()) { - // For empty text, just ignore the submission - don't clear callback - // This keeps the virtual keyboard responsive for further input - LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); - } else { - // No callback available - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + // For character keys, insert uppercase/alternate character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char + return; + } + + switch (key.type) { + case VK_BACKSPACE: + // One-shot: delete up to 5 characters on long press + for (int i = 0; i < 5; ++i) { + if (inputText.empty()) + break; + deleteCharacter(); + } + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + onTextEntered(""); + } + break; + default: + break; } - } } -void VirtualKeyboard::setInputText(const std::string &text) { inputText = text; } +void VirtualKeyboard::insertCharacter(char c) +{ + if (inputText.length() < 160) { // Reasonable text length limit + inputText += c; + } +} -std::string VirtualKeyboard::getInputText() const { return inputText; } +void VirtualKeyboard::deleteCharacter() +{ + if (!inputText.empty()) { + inputText.pop_back(); + } +} -void VirtualKeyboard::setHeader(const std::string &header) { headerText = header; } +void VirtualKeyboard::submitText() +{ + LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); -void VirtualKeyboard::setCallback(std::function callback) { onTextEntered = callback; } + // Only submit if text is not empty + if (!inputText.empty() && onTextEntered) { + // Store callback and text to submit before clearing callback + std::function callback = onTextEntered; + std::string textToSubmit = inputText; + onTextEntered = nullptr; + // Don't clear inputText here - let the calling module handle cleanup + // inputText = ""; // Removed: keep text visible until module cleans up + callback(textToSubmit); + } else if (inputText.empty()) { + // For empty text, just ignore the submission - don't clear callback + // This keeps the virtual keyboard responsive for further input + LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); + } else { + // No callback available + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } +} -void VirtualKeyboard::resetTimeout() { lastActivityTime = millis(); } +void VirtualKeyboard::setInputText(const std::string &text) +{ + inputText = text; +} -bool VirtualKeyboard::isTimedOut() const { return (millis() - lastActivityTime) > TIMEOUT_MS; } +std::string VirtualKeyboard::getInputText() const +{ + return inputText; +} + +void VirtualKeyboard::setHeader(const std::string &header) +{ + headerText = header; +} + +void VirtualKeyboard::setCallback(std::function callback) +{ + onTextEntered = callback; +} + +void VirtualKeyboard::resetTimeout() +{ + lastActivityTime = millis(); +} + +bool VirtualKeyboard::isTimedOut() const +{ + return (millis() - lastActivityTime) > TIMEOUT_MS; +} } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h index 6c6c80ff7..169163b57 100644 --- a/src/graphics/VirtualKeyboard.h +++ b/src/graphics/VirtualKeyboard.h @@ -5,73 +5,76 @@ #include #include -namespace graphics { +namespace graphics +{ enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; struct VirtualKey { - char character; - VirtualKeyType type; - uint8_t x; - uint8_t y; - uint8_t width; - uint8_t height; + char character; + VirtualKeyType type; + uint8_t x; + uint8_t y; + uint8_t width; + uint8_t height; }; -class VirtualKeyboard { -public: - VirtualKeyboard(); - ~VirtualKeyboard(); +class VirtualKeyboard +{ + public: + VirtualKeyboard(); + ~VirtualKeyboard(); - void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); - void setInputText(const std::string &text); - std::string getInputText() const; - void setHeader(const std::string &header); - void setCallback(std::function callback); + void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); + void setInputText(const std::string &text); + std::string getInputText() const; + void setHeader(const std::string &header); + void setCallback(std::function callback); - // Navigation methods for encoder input - void moveCursorUp(); - void moveCursorDown(); - void moveCursorLeft(); - void moveCursorRight(); - void handlePress(); - void handleLongPress(); + // Navigation methods for encoder input + void moveCursorUp(); + void moveCursorDown(); + void moveCursorLeft(); + void moveCursorRight(); + void handlePress(); + void handleLongPress(); - // Timeout management - void resetTimeout(); - bool isTimedOut() const; + // Timeout management + void resetTimeout(); + bool isTimedOut() const; -private: - static const uint8_t KEYBOARD_ROWS = 4; - static const uint8_t KEYBOARD_COLS = 11; - static const uint8_t KEY_WIDTH = 9; - static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays - static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom + private: + static const uint8_t KEYBOARD_ROWS = 4; + static const uint8_t KEYBOARD_COLS = 11; + static const uint8_t KEY_WIDTH = 9; + static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays + static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom - VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; + VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; - std::string inputText; - std::string headerText; - std::function onTextEntered; + std::string inputText; + std::string headerText; + std::function onTextEntered; - uint8_t cursorRow; - uint8_t cursorCol; + uint8_t cursorRow; + uint8_t cursorCol; - // Timeout management for auto-exit - uint32_t lastActivityTime; - static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout + // Timeout management for auto-exit + uint32_t lastActivityTime; + static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout - void initializeKeyboard(); - void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, bool isLastCol); - void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); + void initializeKeyboard(); + void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, + bool isLastCol); + void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); - // Unified cursor movement helper - void moveCursorDelta(int dRow, int dCol); + // Unified cursor movement helper + void moveCursorDelta(int dRow, int dCol); - char getCharForKey(const VirtualKey &key, bool isLongPress = false); - void insertCharacter(char c); - void deleteCharacter(); - void submitText(); + char getCharForKey(const VirtualKey &key, bool isLongPress = false); + void insertCharacter(char c); + void deleteCharacter(); + void submitText(); }; } // namespace graphics diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 617a31fc4..66bbe1bfe 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -12,9 +12,11 @@ #include "nimble/NimbleBluetooth.h" #endif -namespace graphics { +namespace graphics +{ -namespace ClockRenderer { +namespace ClockRenderer +{ // Segment bitmaps for numerals 0-9 stored in flash to save RAM. // Each row is a digit, each column is a segment state (1 = on, 0 = off). @@ -41,426 +43,436 @@ static const uint8_t PROGMEM digitSegments[10][7] = { {1, 1, 1, 1, 0, 1, 1} // 9 }; -void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) +{ + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; + uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - uint16_t topAndBottomX = x + static_cast(4 * scale); + uint16_t topAndBottomX = x + static_cast(4 * scale); - uint16_t quarterCellHeight = cellHeight / 4; + uint16_t quarterCellHeight = cellHeight / 4; - uint16_t topY = y + quarterCellHeight; - uint16_t bottomY = y + (quarterCellHeight * 3); + uint16_t topY = y + quarterCellHeight; + uint16_t bottomY = y + (quarterCellHeight * 3); - display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); - display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); } -void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) { - // Read 7-segment pattern for the digit from flash - uint8_t seg[7]; - for (uint8_t i = 0; i < 7; i++) { - seg[i] = pgm_read_byte(&digitSegments[number][i]); - } - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // Precompute segment positions - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentTwoX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; - - uint16_t segmentFourX = segmentOneX; - uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; - - uint16_t segmentFiveX = x; - uint16_t segmentFiveY = segmentThreeY; - - uint16_t segmentSixX = x; - uint16_t segmentSixY = segmentTwoY; - - uint16_t segmentSevenX = segmentOneX; - uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; - - // Draw only the active segments - if (seg[0]) - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - if (seg[1]) - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - if (seg[2]) - drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - if (seg[3]) - drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - if (seg[4]) - drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); - if (seg[5]) - drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); - if (seg[6]) - drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); -} - -void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) { - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, width, height); - - // draw end triangles - display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); - - display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); -} - -void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) { - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, height, width); - - // draw end triangles - display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); - - display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); -} - -void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Set Title, Blank for Clock - const char *titleStr = ""; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true, true); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - char timeString[16]; - int hour = 0; - int minute = 0; - int second = 0; - - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - hour = hms / SEC_PER_HOUR; - minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - } - - bool isPM = hour >= 12; - if (config.display.use_12h_clock) { - hour %= 12; - if (hour == 0) { - hour = 12; - } - snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); - } else { - snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); - } - - // Format seconds string - char secondString[8]; - snprintf(secondString, sizeof(secondString), "%02d", second); - - static bool scaleInitialized = false; - static float scale = 0.75f; - static float segmentWidth = SEGMENT_WIDTH * 0.75f; - static float segmentHeight = SEGMENT_HEIGHT * 0.75f; - - if (!scaleInitialized) { - float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable) - float max_scale = 3.5f; // Safety limit to avoid runaway scaling - float step = 0.05f; // Step increment per iteration - - float target_width = display->getWidth() * screenwidth_target_ratio; - float target_height = - display->getHeight() - ((currentResolution == ScreenResolution::High) - ? 46 - : 33); // Be careful adjusting this number, we have to account for header and the text under the time - - float calculated_width_size = 0.0f; - float calculated_height_size = 0.0f; - - while (true) { - segmentWidth = SEGMENT_WIDTH * scale; - segmentHeight = SEGMENT_HEIGHT * scale; - - calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4); - calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2); - - if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) { - break; - } - - scale += step; +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) +{ + // Read 7-segment pattern for the digit from flash + uint8_t seg[7]; + for (uint8_t i = 0; i < 7; i++) { + seg[i] = pgm_read_byte(&digitSegments[number][i]); } - // If we overshot width, back off one step and recompute segment sizes - if (calculated_width_size > target_width || calculated_height_size > target_height) { - scale -= step; - segmentWidth = SEGMENT_WIDTH * scale; - segmentHeight = SEGMENT_HEIGHT * scale; + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // Precompute segment positions + uint16_t segmentOneX = x + segmentHeight + 2; + uint16_t segmentOneY = y; + + uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; + uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; + + uint16_t segmentThreeX = segmentTwoX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; + + uint16_t segmentFourX = segmentOneX; + uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; + + uint16_t segmentFiveX = x; + uint16_t segmentFiveY = segmentThreeY; + + uint16_t segmentSixX = x; + uint16_t segmentSixY = segmentTwoY; + + uint16_t segmentSevenX = segmentOneX; + uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; + + // Draw only the active segments + if (seg[0]) + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + if (seg[1]) + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + if (seg[2]) + drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + if (seg[3]) + drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + if (seg[4]) + drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + if (seg[5]) + drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + if (seg[6]) + drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); +} + +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) +{ + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, width, height); + + // draw end triangles + display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); + + display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); +} + +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) +{ + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, height, width); + + // draw end triangles + display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); + + display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); +} + +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true, true); + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + char timeString[16]; + int hour = 0; + int minute = 0; + int second = 0; + + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN } - scaleInitialized = true; - } - - // calculate hours:minutes string width - size_t len = strlen(timeString); - uint16_t timeStringWidth = len * 5; - - for (size_t i = 0; i < len; i++) { - char character = timeString[i]; - - if (character == ':') { - timeStringWidth += segmentHeight; + bool isPM = hour >= 12; + if (config.display.use_12h_clock) { + hour %= 12; + if (hour == 0) { + hour = 12; + } + snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); } else { - timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; - } - } - - uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); - uint16_t startingHourMinuteTextX = hourMinuteTextX; - - uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2; - - // iterate over characters in hours:minutes string and draw segmented characters - for (size_t i = 0; i < len; i++) { - char character = timeString[i]; - - if (character == ':') { - drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); - - hourMinuteTextX += segmentHeight + 6; - if (scale >= 2.0f) { - hourMinuteTextX += (uint16_t)(4.5f * scale); - } - } else { - drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); - - hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; + snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); } - hourMinuteTextX += 5; - } + // Format seconds string + char secondString[8]; + snprintf(secondString, sizeof(secondString), "%02d", second); - // draw seconds string + AM/PM - display->setFont(FONT_SMALL); - int xOffset = -1; - if (currentResolution == ScreenResolution::High) { - xOffset = 0; - } - if (hour >= 10) { + static bool scaleInitialized = false; + static float scale = 0.75f; + static float segmentWidth = SEGMENT_WIDTH * 0.75f; + static float segmentHeight = SEGMENT_HEIGHT * 0.75f; + + if (!scaleInitialized) { + float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable) + float max_scale = 3.5f; // Safety limit to avoid runaway scaling + float step = 0.05f; // Step increment per iteration + + float target_width = display->getWidth() * screenwidth_target_ratio; + float target_height = + display->getHeight() - + ((currentResolution == ScreenResolution::High) + ? 46 + : 33); // Be careful adjusting this number, we have to account for header and the text under the time + + float calculated_width_size = 0.0f; + float calculated_height_size = 0.0f; + + while (true) { + segmentWidth = SEGMENT_WIDTH * scale; + segmentHeight = SEGMENT_HEIGHT * scale; + + calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4); + calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2); + + if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) { + break; + } + + scale += step; + } + + // If we overshot width, back off one step and recompute segment sizes + if (calculated_width_size > target_width || calculated_height_size > target_height) { + scale -= step; + segmentWidth = SEGMENT_WIDTH * scale; + segmentHeight = SEGMENT_HEIGHT * scale; + } + + scaleInitialized = true; + } + + // calculate hours:minutes string width + size_t len = strlen(timeString); + uint16_t timeStringWidth = len * 5; + + for (size_t i = 0; i < len; i++) { + char character = timeString[i]; + + if (character == ':') { + timeStringWidth += segmentHeight; + } else { + timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; + } + } + + uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); + uint16_t startingHourMinuteTextX = hourMinuteTextX; + + uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2; + + // iterate over characters in hours:minutes string and draw segmented characters + for (size_t i = 0; i < len; i++) { + char character = timeString[i]; + + if (character == ':') { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + if (scale >= 2.0f) { + hourMinuteTextX += (uint16_t)(4.5f * scale); + } + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); + + hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; + } + + hourMinuteTextX += 5; + } + + // draw seconds string + AM/PM + display->setFont(FONT_SMALL); + int xOffset = -1; if (currentResolution == ScreenResolution::High) { - xOffset += 32; - } else { - xOffset += 18; + xOffset = 0; + } + if (hour >= 10) { + if (currentResolution == ScreenResolution::High) { + xOffset += 32; + } else { + xOffset += 18; + } } - } - if (config.display.use_12h_clock) { - display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am"); - } + if (config.display.use_12h_clock) { + display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am"); + } #ifndef USE_EINK - xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; - if (scale >= 2.0f) { - xOffset -= (int)(4.5f * scale); - } - display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1, secondString); + xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; + if (scale >= 2.0f) { + xOffset -= (int)(4.5f * scale); + } + display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1, + secondString); #endif - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // Draw an analog clock -void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - // === Set Title, Blank for Clock - const char *titleStr = ""; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true, true); +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true, true); - // clock face center coordinates - int16_t centerX = display->getWidth() / 2; - int16_t centerY = display->getHeight() / 2; + // clock face center coordinates + int16_t centerX = display->getWidth() / 2; + int16_t centerY = display->getHeight() / 2; - // clock face radius - int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; + // clock face radius + int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; #ifdef T_WATCH_S3 - radius = (display->getWidth() / 2) * 0.8; + radius = (display->getWidth() / 2) * 0.8; #endif - // noon (0 deg) coordinates (outermost circle) - int16_t noonX = centerX; - int16_t noonY = centerY - radius; + // noon (0 deg) coordinates (outermost circle) + int16_t noonX = centerX; + int16_t noonY = centerY - radius; - // second hand radius and y coordinate (outermost circle) - int16_t secondHandNoonY = noonY + 1; + // second hand radius and y coordinate (outermost circle) + int16_t secondHandNoonY = noonY + 1; - // tick mark outer y coordinate; (first nested circle) - int16_t tickMarkOuterNoonY = secondHandNoonY; + // tick mark outer y coordinate; (first nested circle) + int16_t tickMarkOuterNoonY = secondHandNoonY; - double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); - double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); + double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); + double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); - // minute hand y coordinate - int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; + // minute hand y coordinate + int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; - // hour string y coordinate - int16_t hourStringNoonY = minuteHandNoonY + 18; + // hour string y coordinate + int16_t hourStringNoonY = minuteHandNoonY + 18; - // hour hand radius and y coordinate - int16_t hourHandRadius = radius * 0.35; - if (currentResolution == ScreenResolution::High) { - hourHandRadius = radius * 0.55; - } - int16_t hourHandNoonY = centerY - hourHandRadius; - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawCircle(centerX, centerY, radius); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - int hour, minute, second; - decomposeTime(rtc_sec, hour, minute, second); - - if (config.display.use_12h_clock) { - bool isPM = hour >= 12; - display->setFont(FONT_SMALL); - int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; -#ifdef USE_EINK - yOffset += 3; -#endif - display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, isPM ? "pm" : "am"); + // hour hand radius and y coordinate + int16_t hourHandRadius = radius * 0.35; + if (currentResolution == ScreenResolution::High) { + hourHandRadius = radius * 0.55; } - hour %= 12; - if (hour == 0) - hour = 12; + int16_t hourHandNoonY = centerY - hourHandRadius; - int16_t degreesPerHour = 30; - int16_t degreesPerMinuteOrSecond = 6; + display->setColor(OLEDDISPLAY_COLOR::WHITE); + display->drawCircle(centerX, centerY, radius); - double hourBaseAngle = hour * degreesPerHour; - double hourAngleOffset = ((double)minute / 60) * degreesPerHour; - double hourAngle = radians(hourBaseAngle + hourAngleOffset); + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + int hour, minute, second; + decomposeTime(rtc_sec, hour, minute, second); - double minuteBaseAngle = minute * degreesPerMinuteOrSecond; - double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; - double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); - - double secondAngle = radians(second * degreesPerMinuteOrSecond); - - double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; - double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; - - double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; - double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; - - double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; - double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; - - display->setFont(FONT_MEDIUM); - - // draw minute and hour tick marks and hour numbers - for (uint16_t angle = 0; angle < 360; angle += 6) { - double angleInRadians = radians(angle); - - double sineAngleInRadians = sin(-angleInRadians); - double cosineAngleInRadians = cos(-angleInRadians); - - double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; - double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; - - if (angle % degreesPerHour == 0) { - double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; - - // draw hour tick mark - display->drawLine(startX, startY, endX, endY); - - static char buffer[2]; - - uint8_t hourInt = (angle / 30); - - if (hourInt == 0) { - hourInt = 12; + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + display->setFont(FONT_SMALL); + int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; +#ifdef USE_EINK + yOffset += 3; +#endif + display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, + isPM ? "pm" : "am"); } + hour %= 12; + if (hour == 0) + hour = 12; - // hour number x offset needs to be adjusted for some cases - int8_t hourStringXOffset; - int8_t hourStringYOffset = 13; + int16_t degreesPerHour = 30; + int16_t degreesPerMinuteOrSecond = 6; - switch (hourInt) { - case 3: - hourStringXOffset = 5; - break; - case 9: - hourStringXOffset = 7; - break; - case 10: - case 11: - hourStringXOffset = 8; - break; - case 12: - hourStringXOffset = 13; - break; - default: - hourStringXOffset = 6; - break; - } + double hourBaseAngle = hour * degreesPerHour; + double hourAngleOffset = ((double)minute / 60) * degreesPerHour; + double hourAngle = radians(hourBaseAngle + hourAngleOffset); - double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; - double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; + double minuteBaseAngle = minute * degreesPerMinuteOrSecond; + double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; + double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); + + double secondAngle = radians(second * degreesPerMinuteOrSecond); + + double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; + double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; + + double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; + double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; + + double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; + double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; + + display->setFont(FONT_MEDIUM); + + // draw minute and hour tick marks and hour numbers + for (uint16_t angle = 0; angle < 360; angle += 6) { + double angleInRadians = radians(angle); + + double sineAngleInRadians = sin(-angleInRadians); + double cosineAngleInRadians = cos(-angleInRadians); + + double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; + double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; + + if (angle % degreesPerHour == 0) { + double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; + + // draw hour tick mark + display->drawLine(startX, startY, endX, endY); + + static char buffer[2]; + + uint8_t hourInt = (angle / 30); + + if (hourInt == 0) { + hourInt = 12; + } + + // hour number x offset needs to be adjusted for some cases + int8_t hourStringXOffset; + int8_t hourStringYOffset = 13; + + switch (hourInt) { + case 3: + hourStringXOffset = 5; + break; + case 9: + hourStringXOffset = 7; + break; + case 10: + case 11: + hourStringXOffset = 8; + break; + case 12: + hourStringXOffset = 13; + break; + default: + hourStringXOffset = 6; + break; + } + + double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; + double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; #ifdef T_WATCH_S3 - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); #else #ifdef USE_EINK - if (currentResolution == ScreenResolution::High) { - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } + if (currentResolution == ScreenResolution::High) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } #else - if (currentResolution == ScreenResolution::High && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } + if (currentResolution == ScreenResolution::High && + (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } #endif #endif - } + } - if (angle % degreesPerMinuteOrSecond == 0) { - double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; + if (angle % degreesPerMinuteOrSecond == 0) { + double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - if (currentResolution == ScreenResolution::High) { - // draw minute tick mark - display->drawLine(startX, startY, endX, endY); + if (currentResolution == ScreenResolution::High) { + // draw minute tick mark + display->drawLine(startX, startY, endX, endY); + } + } } - } - } - // draw hour hand - display->drawLine(centerX, centerY, hourX, hourY); + // draw hour hand + display->drawLine(centerX, centerY, hourX, hourY); - // draw minute hand - display->drawLine(centerX, centerY, minuteX, minuteY); + // draw minute hand + display->drawLine(centerX, centerY, minuteX, minuteY); #ifndef USE_EINK - // draw second hand - display->drawLine(centerX, centerY, secondX, secondY); + // draw second hand + display->drawLine(centerX, centerY, secondX, secondY); #endif - } - graphics::drawCommonFooter(display, x, y); + } + graphics::drawCommonFooter(display, x, y); } } // namespace ClockRenderer diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h index e3354bc64..eace26cf5 100644 --- a/src/graphics/draw/ClockRenderer.h +++ b/src/graphics/draw/ClockRenderer.h @@ -3,12 +3,14 @@ #include #include -namespace graphics { +namespace graphics +{ /// Forward declarations class Screen; -namespace ClockRenderer { +namespace ClockRenderer +{ // Clock frame functions void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index b5450b1f2..42600ce96 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -9,120 +9,130 @@ #include "graphics/SharedUIDisplay.h" #include -namespace graphics { -namespace CompassRenderer { +namespace graphics +{ +namespace CompassRenderer +{ // Point helper class for compass calculations struct Point { - float x, y; - Point(float x, float y) : x(x), y(y) {} + float x, y; + Point(float x, float y) : x(x), y(y) {} - void rotate(float angle) { - float cos_a = cos(angle); - float sin_a = sin(angle); - float new_x = x * cos_a - y * sin_a; - float new_y = x * sin_a + y * cos_a; - x = new_x; - y = new_y; - } + void rotate(float angle) + { + float cos_a = cos(angle); + float sin_a = sin(angle); + float new_x = x * cos_a - y * sin_a; + float new_y = x * sin_a + y * cos_a; + x = new_x; + y = new_y; + } - void scale(float factor) { - x *= factor; - y *= factor; - } + void scale(float factor) + { + x *= factor; + y *= factor; + } - void translate(float dx, float dy) { - x += dx; - y += dy; - } + void translate(float dx, float dy) + { + x += dx; + y += dy; + } }; -void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) { - // Show the compass heading (not implemented in original) - // This could draw a "N" indicator or north arrow - // For now, we'll draw a simple north indicator - // const float radius = 17.0f; - if (currentResolution == ScreenResolution::High) { - radius += 4; - } - Point north(0, -radius); - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - north.rotate(-myHeading); - north.translate(compassX, compassY); +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) +{ + // Show the compass heading (not implemented in original) + // This could draw a "N" indicator or north arrow + // For now, we'll draw a simple north indicator + // const float radius = 17.0f; + if (currentResolution == ScreenResolution::High) { + radius += 4; + } + Point north(0, -radius); + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + north.rotate(-myHeading); + north.translate(compassX, compassY); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setColor(BLACK); - if (currentResolution == ScreenResolution::High) { - display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); - } else { - display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); - } - display->setColor(WHITE); - display->drawString(north.x, north.y - 3, "N"); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setColor(BLACK); + if (currentResolution == ScreenResolution::High) { + display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + } else { + display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + } + display->setColor(WHITE); + display->drawString(north.x, north.y - 3, "N"); } -void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { - Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; - Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) +{ + Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially + float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; + Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); - Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; + Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; - for (int i = 0; i < 4; i++) { - arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(compassDiam * 0.6); - arrowPoints[i]->translate(compassX, compassY); - } + for (int i = 0; i < 4; i++) { + arrowPoints[i]->rotate(headingRadian); + arrowPoints[i]->scale(compassDiam * 0.6); + arrowPoints[i]->translate(compassX, compassY); + } #ifdef USE_EINK - display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); + display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #else - display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #endif - display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); + display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); } -void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) { - float radians = bearing * DEG_TO_RAD; +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) +{ + float radians = bearing * DEG_TO_RAD; - Point tip(0, -size / 2); - Point left(-size / 6, size / 4); - Point right(size / 6, size / 4); - Point tail(0, size / 4.5); + Point tip(0, -size / 2); + Point left(-size / 6, size / 4); + Point right(size / 6, size / 4); + Point tail(0, size / 4.5); - tip.rotate(radians); - left.rotate(radians); - right.rotate(radians); - tail.rotate(radians); + tip.rotate(radians); + left.rotate(radians); + right.rotate(radians); + tail.rotate(radians); - tip.translate(x, y); - left.translate(x, y); - right.translate(x, y); - tail.translate(x, y); + tip.translate(x, y); + left.translate(x, y); + right.translate(x, y); + tail.translate(x, y); - display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); - display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); } -float estimatedHeading(double lat, double lon) { - // Simple magnetic declination estimation - // This is a very basic implementation - the original might be more sophisticated - return 0.0f; // Return 0 for now, indicating no heading available +float estimatedHeading(double lat, double lon) +{ + // Simple magnetic declination estimation + // This is a very basic implementation - the original might be more sophisticated + return 0.0f; // Return 0 for now, indicating no heading available } -uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) { - // Calculate appropriate compass diameter based on display size - uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; - uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) +{ + // Calculate appropriate compass diameter based on display size + uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; + uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension - // Ensure minimum and maximum bounds - if (maxDiam < 16) - maxDiam = 16; - if (maxDiam > 64) - maxDiam = 64; + // Ensure minimum and maximum bounds + if (maxDiam < 16) + maxDiam = 16; + if (maxDiam > 64) + maxDiam = 64; - return maxDiam; + return maxDiam; } } // namespace CompassRenderer diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index 0e937cea1..ca7532b66 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -5,7 +5,8 @@ #include #include -namespace graphics { +namespace graphics +{ /// Forward declarations class Screen; @@ -16,7 +17,8 @@ class Screen; * Contains all functions related to drawing compass elements, headings, * navigation arrows, and location-based UI components. */ -namespace CompassRenderer { +namespace CompassRenderer +{ // Compass drawing functions void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius); void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index da0343c36..75b65c65f 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -50,667 +50,696 @@ extern bool heartbeat; extern StoreForwardModule *storeForwardModule; #endif -namespace graphics { -namespace DebugRenderer { +namespace graphics +{ +namespace DebugRenderer +{ -void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setFont(FONT_SMALL); +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } - char channelStr[20]; - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - // Display nodes status - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); - } else { - UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); - } -#if HAS_GPS - // Display GPS status - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); - } else { + char channelStr[20]; + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + // Display nodes status if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); } else { - UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); + } +#if HAS_GPS + // Display GPS status + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); + } else { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + } else { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); + } } - } #endif - display->setColor(WHITE); - // Draw the channel name - display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); - // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo - if (moduleConfig.store_forward.enabled) { + display->setColor(WHITE); + // Draw the channel name + display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); + // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo + if (moduleConfig.store_forward.enabled) { #ifdef ARCH_ESP32 - if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, - (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ - defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ - defined(USE_ST7796) || ARCH_PORTDUINO) && \ + if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, + (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL2); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgQuestion); + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, + 8, imgQuestion); +#endif + } else { +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, + 8, imgSF); +#endif + } #endif } else { -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ - defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ - defined(USE_ST7796)) && \ + // TODO: Raspberry Pi supports more than just the one screen size +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, imgSFL2); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, imgSF); + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, + imgInfo); #endif } -#endif - } else { - // TODO: Raspberry Pi supports more than just the one screen size -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ - defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ - defined(USE_ST7796) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); -#endif - } - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); - // Draw any log messages - display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); + // Draw any log messages + display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; #endif } // **************************** // * WiFi Screen * // **************************** -void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ #if HAS_WIFI && !defined(ARCH_PORTDUINO) - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "WiFi"; + // === Set Title + const char *titleStr = "WiFi"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - const char *wifiName = config.network.wifi_ssid; + const char *wifiName = config.network.wifi_ssid; - if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); - } else { - display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); + if (WiFi.status() != WL_CONNECTED) { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); + } else { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); - char rssiStr[32]; - snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); - display->drawString(x, getTextPositions(display)[line++], rssiStr); - } + char rssiStr[32]; + snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); + display->drawString(x, getTextPositions(display)[line++], rssiStr); + } - /* - - WL_CONNECTED: assigned when connected to a WiFi network; - - WL_NO_SSID_AVAIL: assigned when no SSID are available; - - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; - - WL_CONNECTION_LOST: assigned when the connection is lost; - - WL_DISCONNECTED: assigned when disconnected from a network; - - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of - attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); - - WL_SCAN_COMPLETED: assigned when the scan networks is completed; - - WL_NO_SHIELD: assigned when no WiFi shield is present; + /* + - WL_CONNECTED: assigned when connected to a WiFi network; + - WL_NO_SSID_AVAIL: assigned when no SSID are available; + - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; + - WL_CONNECTION_LOST: assigned when the connection is lost; + - WL_DISCONNECTED: assigned when disconnected from a network; + - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of + attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); + - WL_SCAN_COMPLETED: assigned when the scan networks is completed; + - WL_NO_SHIELD: assigned when no WiFi shield is present; - */ - if (WiFi.status() == WL_CONNECTED) { - char ipStr[64]; - snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); - display->drawString(x, getTextPositions(display)[line++], ipStr); - } else if (WiFi.status() == WL_NO_SSID_AVAIL) { - display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); - } else if (WiFi.status() == WL_CONNECTION_LOST) { - display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); - } else if (WiFi.status() == WL_IDLE_STATUS) { - display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); - } else if (WiFi.status() == WL_CONNECT_FAILED) { - display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); - } + */ + if (WiFi.status() == WL_CONNECTED) { + char ipStr[64]; + snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); + display->drawString(x, getTextPositions(display)[line++], ipStr); + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); + } #ifdef ARCH_ESP32 - else { - // Codes: - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - display->drawString(x, getTextPositions(display)[line++], WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); - } + else { + // Codes: + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + display->drawString(x, getTextPositions(display)[line++], + WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); + } #else - else { - char statusStr[32]; - snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); - display->drawString(x, getTextPositions(display)[line++], statusStr); - } + else { + char statusStr[32]; + snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); + display->drawString(x, getTextPositions(display)[line++], statusStr); + } #endif - char ssidStr[64]; - snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); - display->drawString(x, getTextPositions(display)[line++], ssidStr); + char ssidStr[64]; + snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); + display->drawString(x, getTextPositions(display)[line++], ssidStr); - display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); + display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; #endif #endif } -void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setFont(FONT_SMALL); +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - char batStr[20]; - if (powerStatus->getHasBattery()) { - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - - snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), - powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); - - // Line 1 - display->drawString(x, y, batStr); - if (config.display.heading_bold) - display->drawString(x + 1, y, batStr); - } else { - // Line 1 - display->drawString(x, y, "USB"); - if (config.display.heading_bold) - display->drawString(x + 1, y, "USB"); - } - - uint32_t currentMillis = millis(); - uint32_t seconds = currentMillis / 1000; - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - // currentMillis %= 1000; - // seconds %= 60; - // minutes %= 60; - // hours %= 24; - - // Show uptime as days, hours, minutes OR seconds - std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); - - // Line 1 (Still) - if (currentResolution != graphics::ScreenResolution::UltraLow) { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - if (config.display.heading_bold) - display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - - display->setColor(WHITE); - } - // Setup string to assemble analogClock string - std::string analogClock = ""; - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour, min, sec; - graphics::decomposeTime(rtc_sec, hour, min, sec); - - char timebuf[12]; - - if (config.display.use_12h_clock) { - std::string meridiem = "am"; - if (hour >= 12) { - if (hour > 12) - hour -= 12; - meridiem = "pm"; - } - if (hour == 00) { - hour = 12; - } - snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); - } else { - snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); } - analogClock += timebuf; - } - // Line 2 - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - // Display Channel Utilization - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); + snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), + powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); + + // Line 1 + display->drawString(x, y, batStr); + if (config.display.heading_bold) + display->drawString(x + 1, y, batStr); + } else { + // Line 1 + display->drawString(x, y, "USB"); + if (config.display.heading_bold) + display->drawString(x + 1, y, "USB"); + } + + uint32_t currentMillis = millis(); + uint32_t seconds = currentMillis / 1000; + uint32_t minutes = seconds / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + // currentMillis %= 1000; + // seconds %= 60; + // minutes %= 60; + // hours %= 24; + + // Show uptime as days, hours, minutes OR seconds + std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); + + // Line 1 (Still) + if (currentResolution != graphics::ScreenResolution::UltraLow) { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + + display->setColor(WHITE); + } + // Setup string to assemble analogClock string + std::string analogClock = ""; + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour, min, sec; + graphics::decomposeTime(rtc_sec, hour, min, sec); + + char timebuf[12]; + + if (config.display.use_12h_clock) { + std::string meridiem = "am"; + if (hour >= 12) { + if (hour > 12) + hour -= 12; + meridiem = "pm"; + } + if (hour == 00) { + hour = 12; + } + snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); + } else { + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); + } + analogClock += timebuf; + } + + // Line 2 + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); + + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); #if HAS_GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - // Line 3 - if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude - UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // Line 3 + if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - // Line 4 - UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); - } else { - UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - } + // Line 4 + UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); + } else { + UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + } #endif /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; #endif } // Trampoline functions for DebugInfo class access -void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrame(display, state, x, y); } - -void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - drawFrameSettings(display, state, x, y); +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrame(display, state, x, y); } -void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrameWiFi(display, state, x, y); } +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameSettings(display, state, x, y); +} + +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameWiFi(display, state, x, y); +} // **************************** // * LoRa Focused Screen * // **************************** -void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; + // === Set Title + const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === First Row: Region / BLE Name === - graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); + // === First Row: Region / BLE Name === + graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); - uint8_t dmac[6]; - char shortnameble[35]; - getMacAddr(dmac); - snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); - if (currentResolution == ScreenResolution::UltraLow) { - snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); - } else { - snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); - } - int textWidth = display->getStringWidth(shortnameble); - int nameX = (SCREEN_WIDTH - textWidth); - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - - // === Second Row: Role === - auto role = DisplayFormatters::getDeviceRole(config.device.role); - char device_role[25]; - snprintf(device_role, sizeof(device_role), "Role: %s", role); - textWidth = display->getStringWidth(device_role); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], device_role); - - // === Third Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); - - char regionradiopreset[25]; - const char *region = myRegion ? myRegion->name : NULL; - if (region != nullptr) { + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); if (currentResolution == ScreenResolution::UltraLow) { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); + snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); } else { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); } - } - textWidth = display->getStringWidth(regionradiopreset); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); + int textWidth = display->getStringWidth(shortnameble); + int nameX = (SCREEN_WIDTH - textWidth); + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - // === Fourth Row: Frequency / ChanNum === - char frequencyslot[35]; - char freqStr[16]; - float freq = RadioLibInterface::instance->getFreq(); - snprintf(freqStr, sizeof(freqStr), "%.3f", freq); - if (config.lora.channel_num == 0) { - if (currentResolution == ScreenResolution::UltraLow) { - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); - } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); + // === Second Row: Role === + auto role = DisplayFormatters::getDeviceRole(config.device.role); + char device_role[25]; + snprintf(device_role, sizeof(device_role), "Role: %s", role); + textWidth = display->getStringWidth(device_role); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], device_role); + + // === Third Row: Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + if (region != nullptr) { + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); + } else { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + } } - } else { - if (currentResolution == ScreenResolution::UltraLow) { - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); + textWidth = display->getStringWidth(regionradiopreset); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); + + // === Fourth Row: Frequency / ChanNum === + char frequencyslot[35]; + char freqStr[16]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqStr, sizeof(freqStr), "%.3f", freq); + if (config.lora.channel_num == 0) { + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); + } } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); + } } - } - size_t len = strlen(frequencyslot); - if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { - frequencyslot[len - 4] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(frequencyslot); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); + size_t len = strlen(frequencyslot); + if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { + frequencyslot[len - 4] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(frequencyslot); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); #if !defined(M5STACK_UNITC6L) - // === Fifth Row: Channel Utilization === - const char *chUtil = "ChUtil:"; - char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + // === Fifth Row: Channel Utilization === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; - int chUtil_y = getTextPositions(display)[line] + 3; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; - int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; - int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; - int chutil_percent = airTime->channelUtilizationPercent(); + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; + int chutil_percent = airTime->channelUtilizationPercent(); - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; - display->drawString(starting_position, getTextPositions(display)[line], chUtil); + display->drawString(starting_position, getTextPositions(display)[line], chUtil); - // Force 56% or higher to show a full 100% bar, text would still show related percent. - if (chutil_percent >= 61) { - chutil_percent = 100; - } + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0–25% - float weight2 = 0.35; // Weight for 25–40% - float weight3 = 0.20; // Weight for 40–100% - float totalWeight = weight1 + weight2 + weight3; + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); - int fillRight = 0; + int fillRight = 0; - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); - } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } - // Draw outline - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); - // Fill progress - if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); - } + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], chUtilPercentage); + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], + chUtilPercentage); #endif - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } // **************************** // * System Screen * // **************************** -void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); - // === Set Title - const char *titleStr = "System"; + // === Set Title + const char *titleStr = "System"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === Layout === - int line = 1; - const int barHeight = 6; - const int labelX = x; - int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; + // === Layout === + int line = 1; + const int barHeight = 6; + const int labelX = x; + int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; #ifdef USE_EINK #ifndef T_DECK_PRO - barsOffset -= 12; + barsOffset -= 12; #endif #endif - int barX = x + barsOffset; - if (currentResolution == ScreenResolution::UltraLow) { - barX += 45; - } else { - barX += 40; - } - auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { - if (total == 0) - return; - - int percent = (used * 100) / total; - - char combinedStr[24]; - if (currentResolution == ScreenResolution::High) { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); + int barX = x + barsOffset; + if (currentResolution == ScreenResolution::UltraLow) { + barX += 45; } else { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); + barX += 40; } + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { + if (total == 0) + return; - int textWidth = display->getStringWidth(combinedStr); - int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; - if (adjustedBarWidth < 10) - adjustedBarWidth = 10; + int percent = (used * 100) / total; - int fillWidth = (used * adjustedBarWidth) / total; + char combinedStr[24]; + if (currentResolution == ScreenResolution::High) { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, + total / 1024); + } else { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); + } - // Label - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(labelX, getTextPositions(display)[line], label); + int textWidth = display->getStringWidth(combinedStr); + int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; + if (adjustedBarWidth < 10) + adjustedBarWidth = 10; + + int fillWidth = (used * adjustedBarWidth) / total; + + // Label + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(labelX, getTextPositions(display)[line], label); #if !defined(M5STACK_UNITC6L) - // Bar - int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; - display->setColor(WHITE); - display->drawRect(barX, barY, adjustedBarWidth, barHeight); + // Bar + int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; + display->setColor(WHITE); + display->drawRect(barX, barY, adjustedBarWidth, barHeight); - display->fillRect(barX, barY, fillWidth, barHeight); - display->setColor(WHITE); + display->fillRect(barX, barY, fillWidth, barHeight); + display->setColor(WHITE); #endif - // Value string - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); - }; + // Value string + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); + }; - // === Memory values === - uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); - uint32_t heapTotal = memGet.getHeapSize(); + // === Memory values === + uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); + uint32_t heapTotal = memGet.getHeapSize(); - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); - uint32_t flashUsed = 0, flashTotal = 0; + uint32_t flashUsed = 0, flashTotal = 0; #ifdef ESP32 - flashUsed = FSCom.usedBytes(); - flashTotal = FSCom.totalBytes(); + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); #endif - uint32_t sdUsed = 0, sdTotal = 0; - bool hasSD = false; - /* - #ifdef HAS_SDCARD - hasSD = SD.cardType() != CARD_NONE; - if (hasSD) { - sdUsed = SD.usedBytes(); - sdTotal = SD.totalBytes(); - } - #endif - */ - // === Draw memory rows - drawUsageRow("Heap:", heapUsed, heapTotal, true); + uint32_t sdUsed = 0, sdTotal = 0; + bool hasSD = false; + /* + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif + */ + // === Draw memory rows + drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 - if (psramUsed > 0) { - line += 1; - drawUsageRow("PSRAM:", psramUsed, psramTotal); - } - if (flashTotal > 0) { - line += 1; - drawUsageRow("Flash:", flashUsed, flashTotal); - } + if (psramUsed > 0) { + line += 1; + drawUsageRow("PSRAM:", psramUsed, psramTotal); + } + if (flashTotal > 0) { + line += 1; + drawUsageRow("Flash:", flashUsed, flashTotal); + } #endif - if (hasSD && sdTotal > 0) { + if (hasSD && sdTotal > 0) { + line += 1; + drawUsageRow("SD:", sdUsed, sdTotal); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + // System Uptime + if (line < 2) { + line += 1; + } line += 1; - drawUsageRow("SD:", sdUsed, sdTotal); - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - // System Uptime - if (line < 2) { - line += 1; - } - line += 1; + char appversionstr[35]; + char appversionstr_formatted[40]; - char appversionstr[35]; - char appversionstr_formatted[40]; + const char *ver = optstr(APP_VERSION); + char verbuf[32]; + strncpy(verbuf, ver, sizeof(verbuf) - 1); + verbuf[sizeof(verbuf) - 1] = '\0'; - const char *ver = optstr(APP_VERSION); - char verbuf[32]; - strncpy(verbuf, ver, sizeof(verbuf) - 1); - verbuf[sizeof(verbuf) - 1] = '\0'; + char *lastDot = strrchr(verbuf, '.'); - char *lastDot = strrchr(verbuf, '.'); - - if (currentResolution == ScreenResolution::UltraLow) { - if (lastDot != nullptr) { - *lastDot = '\0'; - } - snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); - } else { - if (lastDot) { - size_t prefixLen = (size_t)(lastDot - verbuf); - snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); - strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); - appversionstr[sizeof(appversionstr) - 1] = '\0'; + if (currentResolution == ScreenResolution::UltraLow) { + if (lastDot != nullptr) { + *lastDot = '\0'; + } + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); } else { - snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + if (lastDot) { + size_t prefixLen = (size_t)(lastDot - verbuf); + snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); + strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); + appversionstr[sizeof(appversionstr) - 1] = '\0'; + } else { + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + } } - } - int textWidth = display->getStringWidth(appversionstr); - int nameX = (SCREEN_WIDTH - textWidth) / 2; + int textWidth = display->getStringWidth(appversionstr); + int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], appversionstr); + display->drawString(nameX, getTextPositions(display)[line++], appversionstr); - if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it - char uptimeStr[32] = ""; - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); - textWidth = display->getStringWidth(uptimeStr); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); - } - - if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show API state if the screen can show it - char api_state[32] = ""; - const char *clientWord = nullptr; - - // Determine if narrow or wide screen - if (currentResolution == ScreenResolution::High) { - clientWord = "Client"; - } else { - clientWord = "App"; + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it + char uptimeStr[32] = ""; + getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + textWidth = display->getStringWidth(uptimeStr); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); } - snprintf(api_state, sizeof(api_state), "No %ss Connected", clientWord); - if (service->api_state == service->STATE_BLE) { - snprintf(api_state, sizeof(api_state), "%s Connected (BLE)", clientWord); - } else if (service->api_state == service->STATE_WIFI) { - snprintf(api_state, sizeof(api_state), "%s Connected (WiFi)", clientWord); - } else if (service->api_state == service->STATE_SERIAL) { - snprintf(api_state, sizeof(api_state), "%s Connected (Serial)", clientWord); - } else if (service->api_state == service->STATE_PACKET) { - snprintf(api_state, sizeof(api_state), "%s Connected (Internal)", clientWord); - } else if (service->api_state == service->STATE_HTTP) { - snprintf(api_state, sizeof(api_state), "%s Connected (HTTP)", clientWord); - } else if (service->api_state == service->STATE_ETH) { - snprintf(api_state, sizeof(api_state), "%s Connected (Ethernet)", clientWord); - } - if (api_state[0] != '\0') { - display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++], api_state); - } - } + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show API state if the screen can show it + char api_state[32] = ""; + const char *clientWord = nullptr; - graphics::drawCommonFooter(display, x, y); + // Determine if narrow or wide screen + if (currentResolution == ScreenResolution::High) { + clientWord = "Client"; + } else { + clientWord = "App"; + } + snprintf(api_state, sizeof(api_state), "No %ss Connected", clientWord); + + if (service->api_state == service->STATE_BLE) { + snprintf(api_state, sizeof(api_state), "%s Connected (BLE)", clientWord); + } else if (service->api_state == service->STATE_WIFI) { + snprintf(api_state, sizeof(api_state), "%s Connected (WiFi)", clientWord); + } else if (service->api_state == service->STATE_SERIAL) { + snprintf(api_state, sizeof(api_state), "%s Connected (Serial)", clientWord); + } else if (service->api_state == service->STATE_PACKET) { + snprintf(api_state, sizeof(api_state), "%s Connected (Internal)", clientWord); + } else if (service->api_state == service->STATE_HTTP) { + snprintf(api_state, sizeof(api_state), "%s Connected (HTTP)", clientWord); + } else if (service->api_state == service->STATE_ETH) { + snprintf(api_state, sizeof(api_state), "%s Connected (Ethernet)", clientWord); + } + if (api_state[0] != '\0') { + display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++], + api_state); + } + } + + graphics::drawCommonFooter(display, x, y); } // **************************** // * Chirpy Screen * // **************************** -void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; - int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); - int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; - int textX_offset = 10; - if (currentResolution == ScreenResolution::High) { - textX_offset = textX_offset * 4; - const int scale = 2; - const int bytesPerRow = (chirpy_width + 7) / 8; +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); + int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; + int textX_offset = 10; + if (currentResolution == ScreenResolution::High) { + textX_offset = textX_offset * 4; + const int scale = 2; + const int bytesPerRow = (chirpy_width + 7) / 8; - for (int yy = 0; yy < chirpy_height; ++yy) { - iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); - iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; - const uint8_t *rowPtr = chirpy + yy * bytesPerRow; - for (int xx = 0; xx < chirpy_width; ++xx) { - const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); - const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first - if (byteVal & bitMask) { - display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); + for (int yy = 0; yy < chirpy_height; ++yy) { + iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); + iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; + const uint8_t *rowPtr = chirpy + yy * bytesPerRow; + for (int xx = 0; xx < chirpy_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); + } + } } - } + } else { + display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); } - } else { - display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); - } - int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); - display->drawString(textX, getTextPositions(display)[line++], "Hello"); - textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); - display->drawString(textX, getTextPositions(display)[line++], "World!"); + int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); + display->drawString(textX, getTextPositions(display)[line++], "Hello"); + textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); + display->drawString(textX, getTextPositions(display)[line++], "World!"); } } // namespace DebugRenderer diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index ecbed6769..65fa74ca6 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -3,7 +3,8 @@ #include #include -namespace graphics { +namespace graphics +{ /// Forward declarations class Screen; @@ -15,7 +16,8 @@ class DebugInfo; * Contains all functions related to drawing debug information, * WiFi status, settings screens, and diagnostic data. */ -namespace DebugRenderer { +namespace DebugRenderer +{ // Debug frame functions void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h index 174b638bf..c55e66ede 100644 --- a/src/graphics/draw/DrawRenderers.h +++ b/src/graphics/draw/DrawRenderers.h @@ -13,7 +13,8 @@ #include "graphics/draw/NodeListRenderer.h" #include "graphics/draw/UIRenderer.h" -namespace graphics { +namespace graphics +{ /** * @brief Collection of all draw renderers @@ -21,7 +22,8 @@ namespace graphics { * This namespace provides access to all the specialized rendering * functions organized by category. */ -namespace DrawRenderers { +namespace DrawRenderers +{ // Re-export all renderer namespaces for convenience using namespace ClockRenderer; using namespace CompassRenderer; diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 91f71a893..5687c8620 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -30,27 +30,30 @@ extern uint16_t TFT_MESH; -namespace graphics { +namespace graphics +{ -namespace { +namespace +{ // Caller must ensure the provided options array outlives the banner callback. template -BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], std::array &labels, - Callback &&onSelection) { - for (size_t i = 0; i < N; ++i) { - labels[i] = options[i].label; - } +BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], + std::array &labels, Callback &&onSelection) +{ + for (size_t i = 0; i < N; ++i) { + labels[i] = options[i].label; + } - const MenuOption *optionsPtr = options; - auto callback = std::function &, int)>(std::forward(onSelection)); + const MenuOption *optionsPtr = options; + auto callback = std::function &, int)>(std::forward(onSelection)); - BannerOverlayOptions bannerOptions; - bannerOptions.message = message; - bannerOptions.optionsArrayPtr = labels.data(); - bannerOptions.optionsCount = static_cast(N); - bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; - return bannerOptions; + BannerOverlayOptions bannerOptions; + bannerOptions.message = message; + bannerOptions.optionsArrayPtr = labels.data(); + bannerOptions.optionsCount = static_cast(N); + bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; + return bannerOptions; } } // namespace @@ -59,2415 +62,2497 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; -void menuHandler::loraMenu() { - static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"}; - enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "LoRa Actions"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - // No action - } else if (selected == device_role_picker) { - menuHandler::menuQueue = menuHandler::device_role_picker; - } else if (selected == radio_preset_picker) { - menuHandler::menuQueue = menuHandler::radio_preset_picker; - } else if (selected == lora_picker) { - menuHandler::menuQueue = menuHandler::lora_picker; - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::OnboardMessage() { - static const char *optionsArray[] = {"OK", "Got it!"}; - enum optionsNumbers { OK, got }; - BannerOverlayOptions bannerOptions; -#if HAS_TFT - bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu."; -#elif defined(BUTTON_PIN) - bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu."; -#else - bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections."; -#endif - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; - screen->runNow(); - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::LoraRegionPicker(uint32_t duration) { - static const LoraRegionOption regionOptions[] = { - {"Back", OptionsAction::Back}, - {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, - {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, - {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, - {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, - {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, - {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, - {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, - {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, - {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, - {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, - {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, - {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, - {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, - {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, - {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, - {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, - {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, - {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, - {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, - {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, - {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, - {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, - {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, - {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, - {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, - {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, - }; - - constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); - static std::array regionLabels{}; - - const char *bannerMessage = "Set the LoRa region"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerMessage = "LoRa Region"; - } - - auto bannerOptions = createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { - if (!option.hasValue) { - return; - } - - auto selectedRegion = option.value; - if (config.lora.region == selectedRegion) { - return; - } - - config.lora.region = selectedRegion; - auto changes = SEGMENT_CONFIG; - - // FIXME: This should be a method consolidated with the same logic in the admin message as well - // This is needed as we wait til picking the LoRa region to generate keys for the first time. -#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - // public key is derived from private, so this will always have the same result. - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; +void menuHandler::loraMenu() +{ + static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"}; + enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "LoRa Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + // No action + } else if (selected == device_role_picker) { + menuHandler::menuQueue = menuHandler::device_role_picker; + } else if (selected == radio_preset_picker) { + menuHandler::menuQueue = menuHandler::radio_preset_picker; + } else if (selected == lora_picker) { + menuHandler::menuQueue = menuHandler::lora_picker; } + }; + screen->showOverlayBanner(bannerOptions); +} - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } +void menuHandler::OnboardMessage() +{ + static const char *optionsArray[] = {"OK", "Got it!"}; + enum optionsNumbers { OK, got }; + BannerOverlayOptions bannerOptions; +#if HAS_TFT + bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu."; +#elif defined(BUTTON_PIN) + bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu."; +#else + bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections."; #endif - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default broker is in use, so subscribe to the appropriate MQTT root topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes |= SEGMENT_MODULECONFIG; - } - - service->reloadConfig(changes); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }); - - bannerOptions.durationMs = duration; - - int initialSelection = 0; - for (size_t i = 0; i < regionCount; ++i) { - if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { - initialSelection = static_cast(i); - break; - } - } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; + screen->runNow(); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::DeviceRolePicker() { - static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; - enum optionsNumbers { Back = 0, devicerole_client = 1, devicerole_clientmute = 2, devicerole_lostandfound = 3, devicerole_tracker = 4 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Device Role"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 5; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; - screen->runNow(); - return; - } else if (selected == devicerole_client) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - } else if (selected == devicerole_clientmute) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; - } else if (selected == devicerole_lostandfound) { - config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; - } else if (selected == devicerole_tracker) { - config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; +void menuHandler::LoraRegionPicker(uint32_t duration) +{ + static const LoraRegionOption regionOptions[] = { + {"Back", OptionsAction::Back}, + {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, + {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, + {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, + {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, + {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, + {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, + {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, + {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, + {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, + {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, + {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, + {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, + {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, + {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, + {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, + {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, + {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, + {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, + {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, + {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, + {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, + {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, + {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, + {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, + {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, + {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, + }; + + constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); + static std::array regionLabels{}; + + const char *bannerMessage = "Set the LoRa region"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerMessage = "LoRa Region"; } - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }; - screen->showOverlayBanner(bannerOptions); + + auto bannerOptions = + createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { + if (!option.hasValue) { + return; + } + + auto selectedRegion = option.value; + if (config.lora.region == selectedRegion) { + return; + } + + config.lora.region = selectedRegion; + auto changes = SEGMENT_CONFIG; + + // FIXME: This should be a method consolidated with the same logic in the admin message as well + // This is needed as we wait til picking the LoRa region to generate keys for the first time. +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + // public key is derived from private, so this will always have the same result. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } +#endif + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + // Default broker is in use, so subscribe to the appropriate MQTT root topic for this region + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes |= SEGMENT_MODULECONFIG; + } + + service->reloadConfig(changes); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + bannerOptions.durationMs = duration; + + int initialSelection = 0; + for (size_t i = 0; i < regionCount; ++i) { + if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); } -void menuHandler::RadioPresetPicker() { - static const RadioPresetOption presetOptions[] = { - {"Back", OptionsAction::Back}, - {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, - {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, - {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, - {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, - {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, - {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, - {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, - {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, - }; - - constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); - static std::array presetLabels{}; - - auto bannerOptions = createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - config.lora.modem_preset = option.value; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }); - - screen->showOverlayBanner(bannerOptions); +void menuHandler::DeviceRolePicker() +{ + static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; + enum optionsNumbers { + Back = 0, + devicerole_client = 1, + devicerole_clientmute = 2, + devicerole_lostandfound = 3, + devicerole_tracker = 4 + }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Device Role"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } else if (selected == devicerole_client) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else if (selected == devicerole_clientmute) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; + } else if (selected == devicerole_lostandfound) { + config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; + } else if (selected == devicerole_tracker) { + config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::TwelveHourPicker() { - static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; - enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Time Format"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == twelve) { - config.display.use_12h_clock = true; - } else { - config.display.use_12h_clock = false; - } - service->reloadConfig(SEGMENT_CONFIG); - }; - screen->showOverlayBanner(bannerOptions); +void menuHandler::RadioPresetPicker() +{ + static const RadioPresetOption presetOptions[] = { + {"Back", OptionsAction::Back}, + {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, + {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, + {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, + {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, + {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, + {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, + {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, + {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, + }; + + constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); + static std::array presetLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + config.lora.modem_preset = option.value; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TwelveHourPicker() +{ + static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; + enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Time Format"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + } else if (selected == twelve) { + config.display.use_12h_clock = true; + } else { + config.display.use_12h_clock = false; + } + service->reloadConfig(SEGMENT_CONFIG); + }; + screen->showOverlayBanner(bannerOptions); } // Reusable confirmation prompt function -void menuHandler::showConfirmationBanner(const char *message, std::function onConfirm) { - static const char *confirmOptions[] = {"No", "Yes"}; - BannerOverlayOptions confirmBanner; - confirmBanner.message = message; - confirmBanner.optionsArrayPtr = confirmOptions; - confirmBanner.optionsCount = 2; - confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void { - if (confirmSelected == 1) { - onConfirm(); - } - }; - screen->showOverlayBanner(confirmBanner); +void menuHandler::showConfirmationBanner(const char *message, std::function onConfirm) +{ + static const char *confirmOptions[] = {"No", "Yes"}; + BannerOverlayOptions confirmBanner; + confirmBanner.message = message; + confirmBanner.optionsArrayPtr = confirmOptions; + confirmBanner.optionsCount = 2; + confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void { + if (confirmSelected == 1) { + onConfirm(); + } + }; + screen->showOverlayBanner(confirmBanner); } -void menuHandler::ClockFacePicker() { - static const ClockFaceOption clockFaceOptions[] = { - {"Back", OptionsAction::Back}, - {"Digital", OptionsAction::Select, false}, - {"Analog", OptionsAction::Select, true}, - }; +void menuHandler::ClockFacePicker() +{ + static const ClockFaceOption clockFaceOptions[] = { + {"Back", OptionsAction::Back}, + {"Digital", OptionsAction::Select, false}, + {"Analog", OptionsAction::Select, true}, + }; - constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); - static std::array clockFaceLabels{}; + constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); + static std::array clockFaceLabels{}; - auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, [](const ClockFaceOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - return; - } + auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, + [](const ClockFaceOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } - if (!option.hasValue) { - return; - } + if (!option.hasValue) { + return; + } - if (uiconfig.is_clockface_analog == option.value) { - return; - } + if (uiconfig.is_clockface_analog == option.value) { + return; + } - uiconfig.is_clockface_analog = option.value; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - }); + uiconfig.is_clockface_analog = option.value; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + }); - bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; - screen->showOverlayBanner(bannerOptions); + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::TZPicker() { - static const TimezoneOption timezoneOptions[] = { - {"Back", OptionsAction::Back}, - {"US/Hawaii", OptionsAction::Select, "HST10"}, - {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, - {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, - {"US/Arizona", OptionsAction::Select, "MST7"}, - {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, - {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, - {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, - {"BR/Brasilia", OptionsAction::Select, "BRT3"}, - {"UTC", OptionsAction::Select, "UTC0"}, - {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, - {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, - {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, - {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, - {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, - {"AU/AWST", OptionsAction::Select, "AWST-8"}, - {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, - {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, - {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, - }; +void menuHandler::TZPicker() +{ + static const TimezoneOption timezoneOptions[] = { + {"Back", OptionsAction::Back}, + {"US/Hawaii", OptionsAction::Select, "HST10"}, + {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, + {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, + {"US/Arizona", OptionsAction::Select, "MST7"}, + {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, + {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, + {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, + {"BR/Brasilia", OptionsAction::Select, "BRT3"}, + {"UTC", OptionsAction::Select, "UTC0"}, + {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, + {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, + {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, + {"AU/AWST", OptionsAction::Select, "AWST-8"}, + {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + }; - constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); - static std::array timezoneLabels{}; + constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); + static std::array timezoneLabels{}; - auto bannerOptions = createStaticBannerOptions("Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - return; + auto bannerOptions = createStaticBannerOptions( + "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { + return; + } + + strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + + setenv("TZ", config.device.tzdef, 1); + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < timezoneCount; ++i) { + if (timezoneOptions[i].hasValue && + strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { + initialSelection = static_cast(i); + break; + } } + bannerOptions.InitialSelected = initialSelection; - if (!option.hasValue) { - return; - } - - if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { - return; - } - - strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); - config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; - - setenv("TZ", config.device.tzdef, 1); - service->reloadConfig(SEGMENT_CONFIG); - }); - - int initialSelection = 0; - for (size_t i = 0; i < timezoneCount; ++i) { - if (timezoneOptions[i].hasValue && strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { - initialSelection = static_cast(i); - break; - } - } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); + screen->showOverlayBanner(bannerOptions); } -void menuHandler::clockMenu() { +void menuHandler::clockMenu() +{ #if defined(M5STACK_UNITC6L) - static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; + static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; #else - static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; + static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; #endif - enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Clock Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Clock) { - menuHandler::menuQueue = menuHandler::clock_face_picker; - screen->runNow(); - } else if (selected == Time) { - menuHandler::menuQueue = menuHandler::twelve_hour_picker; - screen->runNow(); - } else if (selected == Timezone) { - menuHandler::menuQueue = menuHandler::TZ_picker; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Clock Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Clock) { + menuHandler::menuQueue = menuHandler::clock_face_picker; + screen->runNow(); + } else if (selected == Time) { + menuHandler::menuQueue = menuHandler::twelve_hour_picker; + screen->runNow(); + } else if (selected == Timezone) { + menuHandler::menuQueue = menuHandler::TZ_picker; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::messageResponseMenu() { - enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd }; +void menuHandler::messageResponseMenu() +{ + enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd }; - static const char *optionsArray[enumEnd]; - static int optionsEnumArray[enumEnd]; - int options = 0; - - auto mode = graphics::MessageRenderer::getThreadMode(); - - optionsArray[options] = "Back"; - optionsEnumArray[options++] = Back; - - // New Reply submenu (replaces Preset and Freetext directly in this menu) - optionsArray[options] = "Reply"; - optionsEnumArray[options++] = ReplyMenu; - - optionsArray[options] = "View Chats"; - optionsEnumArray[options++] = ViewMode; - - // Delete submenu - optionsArray[options] = "Delete"; - optionsEnumArray[options++] = 900; - -#ifdef HAS_I2S - optionsArray[options] = "Read Aloud"; - optionsEnumArray[options++] = Aloud; -#endif - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Message Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Message"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - LOG_DEBUG("messageResponseMenu: selected %d", selected); + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; - if (selected == ViewMode) { - menuHandler::menuQueue = menuHandler::message_viewmode_menu; - screen->runNow(); + // New Reply submenu (replaces Preset and Freetext directly in this menu) + optionsArray[options] = "Reply"; + optionsEnumArray[options++] = ReplyMenu; - // Reply submenu - } else if (selected == ReplyMenu) { - menuHandler::menuQueue = menuHandler::reply_menu; - screen->runNow(); + optionsArray[options] = "View Chats"; + optionsEnumArray[options++] = ViewMode; - // Delete submenu - } else if (selected == 900) { - menuHandler::menuQueue = menuHandler::delete_messages_menu; - screen->runNow(); - - // Delete oldest FIRST (only change) - } else if (selected == DeleteOldest) { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - // Global oldest - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - // Oldest in current channel - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - // Oldest in current DM - messageStore.deleteOldestMessageWithPeer(peer); - } - - // Delete all messages - } else if (selected == DeleteAll) { - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); + // Delete submenu + optionsArray[options] = "Delete"; + optionsEnumArray[options++] = 900; #ifdef HAS_I2S - } else if (selected == Aloud) { - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - audioThread->readAloud(msg); + optionsArray[options] = "Read Aloud"; + optionsEnumArray[options++] = Aloud; #endif + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Message"; } - }; - screen->showOverlayBanner(bannerOptions); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + LOG_DEBUG("messageResponseMenu: selected %d", selected); + + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); + + if (selected == ViewMode) { + menuHandler::menuQueue = menuHandler::message_viewmode_menu; + screen->runNow(); + + // Reply submenu + } else if (selected == ReplyMenu) { + menuHandler::menuQueue = menuHandler::reply_menu; + screen->runNow(); + + // Delete submenu + } else if (selected == 900) { + menuHandler::menuQueue = menuHandler::delete_messages_menu; + screen->runNow(); + + // Delete oldest FIRST (only change) + } else if (selected == DeleteOldest) { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + // Global oldest + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + // Oldest in current channel + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + // Oldest in current DM + messageStore.deleteOldestMessageWithPeer(peer); + } + + // Delete all messages + } else if (selected == DeleteAll) { + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); + +#ifdef HAS_I2S + } else if (selected == Aloud) { + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + audioThread->readAloud(msg); +#endif + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::replyMenu() { - enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; +void menuHandler::replyMenu() +{ + enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; - static const char *optionsArray[enumEnd]; - static int optionsEnumArray[enumEnd]; - int options = 0; + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; - // Back - optionsArray[options] = "Back"; - optionsEnumArray[options++] = Back; - - // Preset reply - optionsArray[options] = "With Preset"; - optionsEnumArray[options++] = ReplyPreset; - - // Freetext reply (only when keyboard exists) - if (kb_found) { - optionsArray[options] = "With Freetext"; - optionsEnumArray[options++] = ReplyFreetext; - } - - BannerOverlayOptions bannerOptions; - - // Dynamic title based on thread mode - auto mode = graphics::MessageRenderer::getThreadMode(); - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - bannerOptions.message = "Reply to Channel"; - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - bannerOptions.message = "Reply to DM"; - } else { - // View All - bannerOptions.message = "Reply to Last Msg"; - } - - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.InitialSelected = 1; - - bannerOptions.bannerCallback = [](int selected) -> void { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; - screen->runNow(); - return; - } + // Back + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; // Preset reply - if (selected == ReplyPreset) { + optionsArray[options] = "With Preset"; + optionsEnumArray[options++] = ReplyPreset; - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); - - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - cannedMessageModule->LaunchWithDestination(peer); - - } else { - // Fallback for last received message - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); - } - } - - return; + // Freetext reply (only when keyboard exists) + if (kb_found) { + optionsArray[options] = "With Freetext"; + optionsEnumArray[options++] = ReplyFreetext; } - // Freetext reply - if (selected == ReplyFreetext) { + BannerOverlayOptions bannerOptions; - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); - - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - cannedMessageModule->LaunchFreetextWithDestination(peer); - - } else { - // Fallback for last received message - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); - } - } - - return; - } - }; - screen->showOverlayBanner(bannerOptions); -} -void menuHandler::deleteMessagesMenu() { - enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; - - static const char *optionsArray[enumEnd]; - static int optionsEnumArray[enumEnd]; - int options = 0; - - auto mode = graphics::MessageRenderer::getThreadMode(); - - optionsArray[options] = "Back"; - optionsEnumArray[options++] = Back; - - optionsArray[options] = "Delete Oldest"; - optionsEnumArray[options++] = DeleteOldest; - - // If viewing ALL chats → hide “Delete This Chat” - if (mode != graphics::MessageRenderer::ThreadMode::ALL) { - optionsArray[options] = "Delete This Chat"; - optionsEnumArray[options++] = DeleteThis; - } - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "Delete All"; - } else { - optionsArray[options] = "Delete All Chats"; - } - optionsEnumArray[options++] = DeleteAll; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Delete Messages"; - - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [mode](int selected) -> void { - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; - screen->runNow(); - return; - } - - if (selected == DeleteAll) { - LOG_INFO("Deleting all messages"); - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); - return; - } - - if (selected == DeleteOldest) { - LOG_INFO("Deleting oldest message"); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - messageStore.deleteOldestMessageWithPeer(peer); - } - - return; - } - - // This only appears in non-ALL modes - if (selected == DeleteThis) { - LOG_INFO("Deleting all messages in this thread"); - - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - messageStore.deleteAllMessagesInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - messageStore.deleteAllMessagesWithPeer(peer); - } - - return; - } - }; - - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::messageViewModeMenu() { - auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; - auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; - - static std::vector labels; - static std::vector ids; - static std::vector idToPeer; // DM lookup - - labels.clear(); - ids.clear(); - idToPeer.clear(); - - labels.push_back("Back"); - ids.push_back(-1); - labels.push_back("View All Chats"); - ids.push_back(-2); - - // Channels with messages - for (int ch = 0; ch < 8; ++ch) { - auto msgs = messageStore.getChannelMessages((uint8_t)ch); - if (!msgs.empty()) { - char buf[40]; - const char *cname = channels.getName(ch); - snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); - labels.push_back(buf); - ids.push_back(encodeChannelId(ch)); - LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); - } - } - - // Registry channels - for (int ch : graphics::MessageRenderer::getSeenChannels()) { - if (ch < 0 || ch >= 8) - continue; - auto msgs = messageStore.getChannelMessages((uint8_t)ch); - if (msgs.empty()) - continue; - int enc = encodeChannelId(ch); - if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { - char buf[40]; - const char *cname = channels.getName(ch); - snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); - labels.push_back(buf); - ids.push_back(enc); - LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); - } - } - - // Gather unique peers - auto dms = messageStore.getDirectMessages(); - std::vector uniquePeers; - for (auto &m : dms) { - uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; - if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) - uniquePeers.push_back(peer); - } - for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { - if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) - uniquePeers.push_back(peer); - } - std::sort(uniquePeers.begin(), uniquePeers.end()); - - // Encode peers - for (size_t i = 0; i < uniquePeers.size(); ++i) { - uint32_t peer = uniquePeers[i]; - auto node = nodeDB->getMeshNode(peer); - std::string name; - if (node && node->has_user) - name = sanitizeString(node->user.long_name).substr(0, 15); - else { - char buf[20]; - snprintf(buf, sizeof(buf), "Node %08X", peer); - name = buf; - } - labels.push_back("@" + name); - int encPeer = 1000 + (int)idToPeer.size(); - ids.push_back(encPeer); - idToPeer.push_back(peer); - LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); - } - - // Active ID - int activeId = -2; - auto mode = graphics::MessageRenderer::getThreadMode(); - if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) - activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); - else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - uint32_t cur = graphics::MessageRenderer::getThreadPeer(); - for (size_t i = 0; i < idToPeer.size(); ++i) - if (idToPeer[i] == cur) { - activeId = 1000 + (int)i; - break; - } - } - - LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); - - // Build banner - static std::vector options; - static std::vector optionIds; - options.clear(); - optionIds.clear(); - - int initialIndex = 0; - for (size_t i = 0; i < labels.size(); i++) { - options.push_back(labels[i].c_str()); - optionIds.push_back(ids[i]); - if (ids[i] == activeId) - initialIndex = (int)i; - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Select Conversation"; - bannerOptions.optionsArrayPtr = options.data(); - bannerOptions.optionsEnumPtr = optionIds.data(); - bannerOptions.optionsCount = options.size(); - bannerOptions.InitialSelected = initialIndex; - - bannerOptions.bannerCallback = [=](int selected) -> void { - LOG_DEBUG("messageViewModeMenu: selected=%d", selected); - if (selected == -1) { - menuHandler::menuQueue = menuHandler::message_response_menu; - screen->runNow(); - } else if (selected == -2) { - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); - } else if (isChannelSel(selected)) { - int ch = selected - 100; - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); - } else if (selected >= 1000) { - int idx = selected - 1000; - if (idx >= 0 && (size_t)idx < idToPeer.size()) { - uint32_t peer = idToPeer[idx]; - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); - } - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::homeBaseMenu() { - enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; - - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - if (moduleConfig.external_notification.enabled && externalNotificationModule && - config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { - if (!externalNotificationModule->getMute()) { - optionsArray[options] = "Temporarily Mute"; + // Dynamic title based on thread mode + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + bannerOptions.message = "Reply to Channel"; + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + bannerOptions.message = "Reply to DM"; } else { - optionsArray[options] = "Unmute"; + // View All + bannerOptions.message = "Reply to Last Msg"; + } + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.InitialSelected = 1; + + bannerOptions.bannerCallback = [](int selected) -> void { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + // Preset reply + if (selected == ReplyPreset) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchWithDestination(peer); + + } else { + // Fallback for last received message + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); + } + } + + return; + } + + // Freetext reply + if (selected == ReplyFreetext) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchFreetextWithDestination(peer); + + } else { + // Fallback for last received message + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + } + } + + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} +void menuHandler::deleteMessagesMenu() +{ + enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; + + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + optionsArray[options] = "Delete Oldest"; + optionsEnumArray[options++] = DeleteOldest; + + // If viewing ALL chats → hide “Delete This Chat” + if (mode != graphics::MessageRenderer::ThreadMode::ALL) { + optionsArray[options] = "Delete This Chat"; + optionsEnumArray[options++] = DeleteThis; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Delete All"; + } else { + optionsArray[options] = "Delete All Chats"; + } + optionsEnumArray[options++] = DeleteAll; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Delete Messages"; + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [mode](int selected) -> void { + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + if (selected == DeleteAll) { + LOG_INFO("Deleting all messages"); + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); + return; + } + + if (selected == DeleteOldest) { + LOG_INFO("Deleting oldest message"); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteOldestMessageWithPeer(peer); + } + + return; + } + + // This only appears in non-ALL modes + if (selected == DeleteThis) { + LOG_INFO("Deleting all messages in this thread"); + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteAllMessagesInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteAllMessagesWithPeer(peer); + } + + return; + } + }; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageViewModeMenu() +{ + auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; + auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; + + static std::vector labels; + static std::vector ids; + static std::vector idToPeer; // DM lookup + + labels.clear(); + ids.clear(); + idToPeer.clear(); + + labels.push_back("Back"); + ids.push_back(-1); + labels.push_back("View All Chats"); + ids.push_back(-2); + + // Channels with messages + for (int ch = 0; ch < 8; ++ch) { + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (!msgs.empty()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(encodeChannelId(ch)); + LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); + } + } + + // Registry channels + for (int ch : graphics::MessageRenderer::getSeenChannels()) { + if (ch < 0 || ch >= 8) + continue; + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (msgs.empty()) + continue; + int enc = encodeChannelId(ch); + if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(enc); + LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); + } + } + + // Gather unique peers + auto dms = messageStore.getDirectMessages(); + std::vector uniquePeers; + for (auto &m : dms) { + uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + std::sort(uniquePeers.begin(), uniquePeers.end()); + + // Encode peers + for (size_t i = 0; i < uniquePeers.size(); ++i) { + uint32_t peer = uniquePeers[i]; + auto node = nodeDB->getMeshNode(peer); + std::string name; + if (node && node->has_user) + name = sanitizeString(node->user.long_name).substr(0, 15); + else { + char buf[20]; + snprintf(buf, sizeof(buf), "Node %08X", peer); + name = buf; + } + labels.push_back("@" + name); + int encPeer = 1000 + (int)idToPeer.size(); + ids.push_back(encPeer); + idToPeer.push_back(peer); + LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); + } + + // Active ID + int activeId = -2; + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) + activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); + else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + uint32_t cur = graphics::MessageRenderer::getThreadPeer(); + for (size_t i = 0; i < idToPeer.size(); ++i) + if (idToPeer[i] == cur) { + activeId = 1000 + (int)i; + break; + } + } + + LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); + + // Build banner + static std::vector options; + static std::vector optionIds; + options.clear(); + optionIds.clear(); + + int initialIndex = 0; + for (size_t i = 0; i < labels.size(); i++) { + options.push_back(labels[i].c_str()); + optionIds.push_back(ids[i]); + if (ids[i] == activeId) + initialIndex = (int)i; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Select Conversation"; + bannerOptions.optionsArrayPtr = options.data(); + bannerOptions.optionsEnumPtr = optionIds.data(); + bannerOptions.optionsCount = options.size(); + bannerOptions.InitialSelected = initialIndex; + + bannerOptions.bannerCallback = [=](int selected) -> void { + LOG_DEBUG("messageViewModeMenu: selected=%d", selected); + if (selected == -1) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + } else if (selected == -2) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); + } else if (isChannelSel(selected)) { + int ch = selected - 100; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); + } else if (selected >= 1000) { + int idx = selected - 1000; + if (idx >= 0 && (size_t)idx < idToPeer.size()) { + uint32_t peer = idToPeer[idx]; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); + } + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::homeBaseMenu() +{ + enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + if (moduleConfig.external_notification.enabled && externalNotificationModule && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { + if (!externalNotificationModule->getMute()) { + optionsArray[options] = "Temporarily Mute"; + } else { + optionsArray[options] = "Unmute"; + } + optionsEnumArray[options++] = Mute; } - optionsEnumArray[options++] = Mute; - } #if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) - optionsArray[options] = "Toggle Backlight"; - optionsEnumArray[options++] = Backlight; + optionsArray[options] = "Toggle Backlight"; + optionsEnumArray[options++] = Backlight; #else - optionsArray[options] = "Sleep Screen"; - optionsEnumArray[options++] = Sleep; + optionsArray[options] = "Sleep Screen"; + optionsEnumArray[options++] = Sleep; #endif - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - optionsArray[options] = "Send Position"; - } else { - optionsArray[options] = "Send Node Info"; - } - optionsEnumArray[options++] = Position; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Home Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Home"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Mute) { - if (moduleConfig.external_notification.enabled && externalNotificationModule) { - externalNotificationModule->setMute(!externalNotificationModule->getMute()); - IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) - } - } else if (selected == Backlight) { - screen->setOn(false); -#if defined(PIN_EINK_EN) - if (uiconfig.screen_brightness == 1) { - uiconfig.screen_brightness = 0; - digitalWrite(PIN_EINK_EN, LOW); - } else { - uiconfig.screen_brightness = 1; - digitalWrite(PIN_EINK_EN, HIGH); - } - saveUIConfig(); -#elif defined(PCA_PIN_EINK_EN) - if (uiconfig.screen_brightness > 0) { - uiconfig.screen_brightness = 0; - io.digitalWrite(PCA_PIN_EINK_EN, LOW); - } else { - uiconfig.screen_brightness = 1; - io.digitalWrite(PCA_PIN_EINK_EN, HIGH); - } - saveUIConfig(); -#endif - } else if (selected == Sleep) { - screen->setOn(false); - } else if (selected == Position) { - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); - } else { - IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); - } - } else if (selected == Preset) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (selected == Freetext) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::textMessageMenu() { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } - -void menuHandler::textMessageBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, enumEnd }; - - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - optionsArray[options] = "New Preset Msg"; - optionsEnumArray[options++] = Preset; - if (kb_found) { - optionsArray[options] = "New Freetext Msg"; - optionsEnumArray[options++] = Freetext; - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Message Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Preset) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (selected == Freetext) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::systemBaseMenu() { - enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - optionsArray[options] = "Notifications"; - optionsEnumArray[options++] = Notifications; - - optionsArray[options] = "Display Options"; - optionsEnumArray[options++] = ScreenOptions; - - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "Bluetooth"; - } else { - optionsArray[options] = "Bluetooth Toggle"; - } - optionsEnumArray[options++] = Bluetooth; -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - optionsArray[options] = "WiFi Toggle"; - optionsEnumArray[options++] = WiFiToggle; -#endif - - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "Power"; - } else { - optionsArray[options] = "Reboot/Shutdown"; - } - optionsEnumArray[options++] = PowerMenu; - - if (test_enabled) { - optionsArray[options] = "Test Menu"; - optionsEnumArray[options++] = Test; - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "System Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "System"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; - screen->runNow(); - } else if (selected == ScreenOptions) { - menuHandler::menuQueue = menuHandler::screen_options_menu; - screen->runNow(); - } else if (selected == PowerMenu) { - menuHandler::menuQueue = menuHandler::power_menu; - screen->runNow(); - } else if (selected == Test) { - menuHandler::menuQueue = menuHandler::test_menu; - screen->runNow(); - } else if (selected == Bluetooth) { - menuQueue = bluetooth_toggle_menu; - screen->runNow(); -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - } else if (selected == WiFiToggle) { - menuQueue = wifi_toggle_menu; - screen->runNow(); -#endif - } else if (selected == Back && !test_enabled) { - test_count++; - if (test_count > 4) { - test_enabled = true; - } - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::favoriteBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; - - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - // Only show "View Conversation" if a message exists with this node - uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; - bool hasConversation = false; - for (const auto &m : messageStore.getMessages()) { - if ((m.sender == peer || m.dest == peer)) { - hasConversation = true; - break; - } - } - if (hasConversation) { - optionsArray[options] = "Go To Chat"; - optionsEnumArray[options++] = GoToChat; - } - if (currentResolution == ScreenResolution::UltraLow) { - optionsArray[options] = "New Preset"; - } else { - optionsArray[options] = "New Preset Msg"; - } - optionsEnumArray[options++] = Preset; - - if (kb_found) { - optionsArray[options] = "New Freetext Msg"; - optionsEnumArray[options++] = Freetext; - } - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; - } - optionsArray[options] = "Remove Favorite"; - optionsEnumArray[options++] = Remove; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Favorites Action"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Favorites"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Preset) { - cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } else if (selected == Freetext) { - cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } - // Handle new Go To Thread action - else if (selected == GoToChat) { - // Switch thread to direct conversation with this node - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, graphics::UIRenderer::currentFavoriteNodeNum); - - // Manually create and send a UIFrameEvent to trigger the jump - UIFrameEvent evt; - evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; - screen->handleUIFrameEvent(&evt); - } else if (selected == Remove) { - menuHandler::menuQueue = menuHandler::remove_favorite; - screen->runNow(); - } else if (selected == TraceRoute) { - if (traceRouteModule) { - traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); - } - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::positionBaseMenu() { - enum class PositionAction { GpsToggle, GpsFormat, CompassMenu, CompassCalibrate, GPSSmartPosition, GPSUpdateInterval, GPSPositionBroadcast }; - - static const PositionMenuOption baseOptions[] = { - {"Back", OptionsAction::Back}, - {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, - {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, - {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, - {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, - {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, - {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, - }; - - static const PositionMenuOption calibrateOptions[] = { - {"Back", OptionsAction::Back}, - {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, - {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, - {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, - {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, - {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, - {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, - {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, - }; - - constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); - constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); - static std::array baseLabels{}; - static std::array calibrateLabels{}; - - auto onSelection = [](const PositionMenuOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - return; - } - - if (!option.hasValue) { - return; - } - - auto action = static_cast(option.value); - switch (action) { - case PositionAction::GpsToggle: - menuQueue = gps_toggle_menu; - screen->runNow(); - break; - case PositionAction::GpsFormat: - menuQueue = gps_format_menu; - screen->runNow(); - break; - case PositionAction::CompassMenu: - menuQueue = compass_point_north_menu; - screen->runNow(); - break; - case PositionAction::CompassCalibrate: - if (accelerometerThread) { - accelerometerThread->calibrate(30); - } - break; - case PositionAction::GPSSmartPosition: - menuQueue = gps_smart_position_menu; - screen->runNow(); - break; - case PositionAction::GPSUpdateInterval: - menuQueue = gps_update_interval_menu; - screen->runNow(); - break; - case PositionAction::GPSPositionBroadcast: - menuQueue = gps_position_broadcast_menu; - screen->runNow(); - break; - } - }; - - BannerOverlayOptions bannerOptions; - if (accelerometerThread) { - bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); - } else { - bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); - } - - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; - static const char *optionsArray[enumEnd] = {"Back"}; - static int optionsEnumArray[enumEnd] = {Back}; - int options = 1; - - optionsArray[options] = "Add Favorite"; - optionsEnumArray[options++] = Favorite; - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Key Verification"; - optionsEnumArray[options++] = Verify; - } - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Show Long/Short Name"; - optionsEnumArray[options++] = NodeNameLength; - } - optionsArray[options] = "Reset NodeDB"; - optionsEnumArray[options++] = Reset; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Node Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Favorite) { - menuQueue = add_favorite; - screen->runNow(); - } else if (selected == Verify) { - menuQueue = key_verification_init; - screen->runNow(); - } else if (selected == Reset) { - menuQueue = reset_node_db_menu; - screen->runNow(); - } else if (selected == TraceRoute) { - menuQueue = trace_route_menu; - screen->runNow(); - } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::nodeNameLengthMenu() { - static const NodeNameOption nodeNameOptions[] = { - {"Back", OptionsAction::Back}, - {"Long", OptionsAction::Select, true}, - {"Short", OptionsAction::Select, false}, - }; - - constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); - static std::array nodeNameLabels{}; - - auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, [](const NodeNameOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = node_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (config.display.use_long_node_name == option.value) { - return; - } - - config.display.use_long_node_name = option.value; - LOG_INFO("Setting names to %s", option.value ? "long" : "short"); - }); - - int initialSelection = config.display.use_long_node_name ? 1 : 2; - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::resetNodeDBMenu() { - static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Confirm Reset NodeDB"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1 || selected == 2) { - disableBluetooth(); - screen->setFrames(Screen::FOCUS_DEFAULT); - } - if (selected == 1) { - LOG_INFO("Initiate node-db reset"); - nodeDB->resetNodes(); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == 2) { - LOG_INFO("Initiate node-db reset but keeping favorites"); - nodeDB->resetNodes(1); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == 0) { - menuQueue = node_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::compassNorthMenu() { - static const CompassOption compassOptions[] = { - {"Back", OptionsAction::Back}, - {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, - {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, - {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, - }; - - constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); - static std::array compassLabels{}; - - auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, [](const CompassOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (uiconfig.compass_mode == option.value) { - return; - } - - uiconfig.compass_mode = option.value; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); - - int initialSelection = 0; - for (size_t i = 0; i < compassCount; ++i) { - if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { - initialSelection = static_cast(i); - break; - } - } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); -} - -#if !MESHTASTIC_EXCLUDE_GPS -void menuHandler::GPSToggleMenu() { - static const GPSToggleOption gpsToggleOptions[] = { - {"Back", OptionsAction::Back}, - {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, - {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, - }; - - constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); - static std::array toggleLabels{}; - - auto bannerOptions = createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (config.position.gps_mode == option.value) { - return; - } - - config.position.gps_mode = option.value; - if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - playGPSEnableBeep(); - gps->enable(); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + optionsArray[options] = "Send Position"; } else { - playGPSDisableBeep(); - gps->disable(); + optionsArray[options] = "Send Node Info"; } - service->reloadConfig(SEGMENT_CONFIG); - }); + optionsEnumArray[options++] = Position; - int initialSelection = 0; - for (size_t i = 0; i < toggleCount; ++i) { - if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { - initialSelection = static_cast(i); - break; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Home Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Home"; } - } - bannerOptions.InitialSelected = initialSelection; - - screen->showOverlayBanner(bannerOptions); -} -void menuHandler::GPSFormatMenu() { - static const GPSFormatOption formatOptionsHigh[] = { - {"Back", OptionsAction::Back}, - {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, - {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, - {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, - {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, - {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, - {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, - {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, - }; - - static const GPSFormatOption formatOptionsLow[] = { - {"Back", OptionsAction::Back}, - {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, - {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, - {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, - {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, - {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, - {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, - {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, - }; - - constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); - static std::array formatLabelsHigh{}; - static std::array formatLabelsLow{}; - - auto onSelection = [](const GPSFormatOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (uiconfig.gps_format == option.value) { - return; - } - - uiconfig.gps_format = option.value; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - }; - - BannerOverlayOptions bannerOptions; - int initialSelection = 0; - - if (currentResolution == ScreenResolution::High) { - bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); - for (size_t i = 0; i < formatCount; ++i) { - if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { - initialSelection = static_cast(i); - break; - } - } - } else { - bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); - for (size_t i = 0; i < formatCount; ++i) { - if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { - initialSelection = static_cast(i); - break; - } - } - } - - bannerOptions.InitialSelected = initialSelection; - screen->showOverlayBanner(bannerOptions); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Mute) { + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + } + } else if (selected == Backlight) { + screen->setOn(false); +#if defined(PIN_EINK_EN) + if (uiconfig.screen_brightness == 1) { + uiconfig.screen_brightness = 0; + digitalWrite(PIN_EINK_EN, LOW); + } else { + uiconfig.screen_brightness = 1; + digitalWrite(PIN_EINK_EN, HIGH); + } + saveUIConfig(); +#elif defined(PCA_PIN_EINK_EN) + if (uiconfig.screen_brightness > 0) { + uiconfig.screen_brightness = 0; + io.digitalWrite(PCA_PIN_EINK_EN, LOW); + } else { + uiconfig.screen_brightness = 1; + io.digitalWrite(PCA_PIN_EINK_EN, HIGH); + } + saveUIConfig(); +#endif + } else if (selected == Sleep) { + screen->setOn(false); + } else if (selected == Position) { + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } + } else if (selected == Preset) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (selected == Freetext) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::GPSSmartPositionMenu() { - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle Smart Position"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Smrt Postn"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuQueue = position_base_menu; - screen->runNow(); - } else if (selected == 1) { - config.position.position_broadcast_smart_enabled = true; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == 2) { - config.position.position_broadcast_smart_enabled = false; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; - screen->showOverlayBanner(bannerOptions); +void menuHandler::textMessageMenu() +{ + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } -void menuHandler::GPSUpdateIntervalMenu() { - static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", "2 minutes", "5 minutes", - "10 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "24 hours", "At Boot Only"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Update Interval"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 16; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuQueue = position_base_menu; - screen->runNow(); - } else if (selected == 1) { - config.position.gps_update_interval = 8; - } else if (selected == 2) { - config.position.gps_update_interval = 20; - } else if (selected == 3) { - config.position.gps_update_interval = 40; - } else if (selected == 4) { - config.position.gps_update_interval = 60; - } else if (selected == 5) { - config.position.gps_update_interval = 80; - } else if (selected == 6) { - config.position.gps_update_interval = 120; - } else if (selected == 7) { - config.position.gps_update_interval = 300; - } else if (selected == 8) { - config.position.gps_update_interval = 600; - } else if (selected == 9) { - config.position.gps_update_interval = 900; - } else if (selected == 10) { - config.position.gps_update_interval = 1800; - } else if (selected == 11) { - config.position.gps_update_interval = 3600; - } else if (selected == 12) { - config.position.gps_update_interval = 21600; - } else if (selected == 13) { - config.position.gps_update_interval = 43200; - } else if (selected == 14) { - config.position.gps_update_interval = 86400; - } else if (selected == 15) { - config.position.gps_update_interval = 2147483647; // At Boot Only +void menuHandler::textMessageBaseMenu() +{ + enum optionsNumbers { Back, Preset, Freetext, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + optionsArray[options] = "New Preset Msg"; + optionsEnumArray[options++] = Preset; + if (kb_found) { + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; } - if (selected != 0) { - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - - if (config.position.gps_update_interval == 8) { - bannerOptions.InitialSelected = 1; - } else if (config.position.gps_update_interval == 20) { - bannerOptions.InitialSelected = 2; - } else if (config.position.gps_update_interval == 40) { - bannerOptions.InitialSelected = 3; - } else if (config.position.gps_update_interval == 60) { - bannerOptions.InitialSelected = 4; - } else if (config.position.gps_update_interval == 80) { - bannerOptions.InitialSelected = 5; - } else if (config.position.gps_update_interval == 120) { - bannerOptions.InitialSelected = 6; - } else if (config.position.gps_update_interval == 300) { - bannerOptions.InitialSelected = 7; - } else if (config.position.gps_update_interval == 600) { - bannerOptions.InitialSelected = 8; - } else if (config.position.gps_update_interval == 900) { - bannerOptions.InitialSelected = 9; - } else if (config.position.gps_update_interval == 1800) { - bannerOptions.InitialSelected = 10; - } else if (config.position.gps_update_interval == 3600) { - bannerOptions.InitialSelected = 11; - } else if (config.position.gps_update_interval == 21600) { - bannerOptions.InitialSelected = 12; - } else if (config.position.gps_update_interval == 43200) { - bannerOptions.InitialSelected = 13; - } else if (config.position.gps_update_interval == 86400) { - bannerOptions.InitialSelected = 14; - } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only - bannerOptions.InitialSelected = 15; - } else { - bannerOptions.InitialSelected = 0; - } - screen->showOverlayBanner(bannerOptions); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Preset) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (selected == Freetext) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::GPSPositionBroadcastMenu() { - static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", "2 hours", "3 hours", "4 hours", - "5 hours", "6 hours", "12 hours", "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Broadcast Interval"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 17; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuQueue = position_base_menu; - screen->runNow(); - } else if (selected == 1) { - config.position.position_broadcast_secs = 60; - } else if (selected == 2) { - config.position.position_broadcast_secs = 90; - } else if (selected == 3) { - config.position.position_broadcast_secs = 300; - } else if (selected == 4) { - config.position.position_broadcast_secs = 900; - } else if (selected == 5) { - config.position.position_broadcast_secs = 3600; - } else if (selected == 6) { - config.position.position_broadcast_secs = 7200; - } else if (selected == 7) { - config.position.position_broadcast_secs = 10800; - } else if (selected == 8) { - config.position.position_broadcast_secs = 14400; - } else if (selected == 9) { - config.position.position_broadcast_secs = 18000; - } else if (selected == 10) { - config.position.position_broadcast_secs = 21600; - } else if (selected == 11) { - config.position.position_broadcast_secs = 43200; - } else if (selected == 12) { - config.position.position_broadcast_secs = 64800; - } else if (selected == 13) { - config.position.position_broadcast_secs = 86400; - } else if (selected == 14) { - config.position.position_broadcast_secs = 129600; - } else if (selected == 15) { - config.position.position_broadcast_secs = 172800; - } else if (selected == 16) { - config.position.position_broadcast_secs = 259200; +void menuHandler::systemBaseMenu() +{ + enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + optionsArray[options] = "Notifications"; + optionsEnumArray[options++] = Notifications; + + optionsArray[options] = "Display Options"; + optionsEnumArray[options++] = ScreenOptions; + + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Bluetooth"; + } else { + optionsArray[options] = "Bluetooth Toggle"; } - - if (selected != 0) { - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - - if (config.position.position_broadcast_secs == 60) { - bannerOptions.InitialSelected = 1; - } else if (config.position.position_broadcast_secs == 90) { - bannerOptions.InitialSelected = 2; - } else if (config.position.position_broadcast_secs == 300) { - bannerOptions.InitialSelected = 3; - } else if (config.position.position_broadcast_secs == 900) { - bannerOptions.InitialSelected = 4; - } else if (config.position.position_broadcast_secs == 3600) { - bannerOptions.InitialSelected = 5; - } else if (config.position.position_broadcast_secs == 7200) { - bannerOptions.InitialSelected = 6; - } else if (config.position.position_broadcast_secs == 10800) { - bannerOptions.InitialSelected = 7; - } else if (config.position.position_broadcast_secs == 14400) { - bannerOptions.InitialSelected = 8; - } else if (config.position.position_broadcast_secs == 18000) { - bannerOptions.InitialSelected = 9; - } else if (config.position.position_broadcast_secs == 21600) { - bannerOptions.InitialSelected = 10; - } else if (config.position.position_broadcast_secs == 43200) { - bannerOptions.InitialSelected = 11; - } else if (config.position.position_broadcast_secs == 64800) { - bannerOptions.InitialSelected = 12; - } else if (config.position.position_broadcast_secs == 86400) { - bannerOptions.InitialSelected = 13; - } else if (config.position.position_broadcast_secs == 129600) { - bannerOptions.InitialSelected = 14; - } else if (config.position.position_broadcast_secs == 172800) { - bannerOptions.InitialSelected = 15; - } else if (config.position.position_broadcast_secs == 259200) { - bannerOptions.InitialSelected = 16; - } else { - bannerOptions.InitialSelected = 0; - } - screen->showOverlayBanner(bannerOptions); -} - + optionsEnumArray[options++] = Bluetooth; +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + optionsArray[options] = "WiFi Toggle"; + optionsEnumArray[options++] = WiFiToggle; #endif -void menuHandler::BluetoothToggleMenu() { - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle Bluetooth"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Bluetooth"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) - return; - else if (selected != (config.bluetooth.enabled ? 1 : 2)) { - InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Power"; + } else { + optionsArray[options] = "Reboot/Shutdown"; } - }; - bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; - screen->showOverlayBanner(bannerOptions); -} + optionsEnumArray[options++] = PowerMenu; -void menuHandler::BuzzerModeMenu() { - static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Notification Sounds"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 5; - bannerOptions.bannerCallback = [](int selected) -> void { - config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; - service->reloadConfig(SEGMENT_CONFIG); - }; - bannerOptions.InitialSelected = config.device.buzzer_mode; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::BrightnessPickerMenu() { - static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; - - // Get current brightness level to set initial selection - int currentSelection = 1; // Default to Medium - if (uiconfig.screen_brightness >= 255) { - currentSelection = 3; // Very High - } else if (uiconfig.screen_brightness >= 128) { - currentSelection = 2; // High - } else { - currentSelection = 1; // Medium - } - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Brightness"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { // Medium - uiconfig.screen_brightness = 64; - } else if (selected == 2) { // High - uiconfig.screen_brightness = 128; - } else if (selected == 3) { // Very High - uiconfig.screen_brightness = 255; + if (test_enabled) { + optionsArray[options] = "Test Menu"; + optionsEnumArray[options++] = Test; } - if (selected != 0) { // Not "Back" - // Apply brightness immediately -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) - // For HELTEC devices, use analogWrite to control backlight - analogWrite(VTFT_LEDA, uiconfig.screen_brightness); -#elif defined(ST7789_CS) || defined(ST7796_CS) - static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); -#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "System Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "System"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Notifications) { + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + screen->runNow(); + } else if (selected == ScreenOptions) { + menuHandler::menuQueue = menuHandler::screen_options_menu; + screen->runNow(); + } else if (selected == PowerMenu) { + menuHandler::menuQueue = menuHandler::power_menu; + screen->runNow(); + } else if (selected == Test) { + menuHandler::menuQueue = menuHandler::test_menu; + screen->runNow(); + } else if (selected == Bluetooth) { + menuQueue = bluetooth_toggle_menu; + screen->runNow(); +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + } else if (selected == WiFiToggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); #endif - - // Save to device - saveUIConfig(); - - LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); - } - }; - bannerOptions.InitialSelected = currentSelection; - screen->showOverlayBanner(bannerOptions); + } else if (selected == Back && !test_enabled) { + test_count++; + if (test_count > 4) { + test_enabled = true; + } + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::switchToMUIMenu() { - static const char *optionsArray[] = {"No", "Yes"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Switch to MUI?"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - config.bluetooth.enabled = false; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); +void menuHandler::favoriteBaseMenu() +{ + enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + // Only show "View Conversation" if a message exists with this node + uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; + bool hasConversation = false; + for (const auto &m : messageStore.getMessages()) { + if ((m.sender == peer || m.dest == peer)) { + hasConversation = true; + break; + } } - }; - screen->showOverlayBanner(bannerOptions); + if (hasConversation) { + optionsArray[options] = "Go To Chat"; + optionsEnumArray[options++] = GoToChat; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "New Preset"; + } else { + optionsArray[options] = "New Preset Msg"; + } + optionsEnumArray[options++] = Preset; + + if (kb_found) { + optionsArray[options] = "New Freetext Msg"; + optionsEnumArray[options++] = Freetext; + } + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + } + optionsArray[options] = "Remove Favorite"; + optionsEnumArray[options++] = Remove; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Favorites Action"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Favorites"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Preset) { + cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if (selected == Freetext) { + cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } + // Handle new Go To Thread action + else if (selected == GoToChat) { + // Switch thread to direct conversation with this node + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, + graphics::UIRenderer::currentFavoriteNodeNum); + + // Manually create and send a UIFrameEvent to trigger the jump + UIFrameEvent evt; + evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + screen->handleUIFrameEvent(&evt); + } else if (selected == Remove) { + menuHandler::menuQueue = menuHandler::remove_favorite; + screen->runNow(); + } else if (selected == TraceRoute) { + if (traceRouteModule) { + traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); + } + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) { - static const ScreenColorOption colorOptions[] = { - {"Back", OptionsAction::Back}, - {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, - {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, - {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, - {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, - {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, - {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, - {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, - {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, - {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, - {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, - {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, - {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, - {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, - }; +void menuHandler::positionBaseMenu() +{ + enum class PositionAction { + GpsToggle, + GpsFormat, + CompassMenu, + CompassCalibrate, + GPSSmartPosition, + GPSUpdateInterval, + GPSPositionBroadcast + }; - constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); - static std::array colorLabels{}; + static const PositionMenuOption baseOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + }; - auto bannerOptions = - createStaticBannerOptions("Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { + static const PositionMenuOption calibrateOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, + }; + + constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); + static std::array baseLabels{}; + static std::array calibrateLabels{}; + + auto onSelection = [](const PositionMenuOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = system_base_menu; - screen->runNow(); - return; + return; } if (!option.hasValue) { - return; + return; } -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT || \ - defined(HACKADAY_COMMUNICATOR) - const ScreenColor &color = option.value; - if (color.useVariant) { - LOG_INFO("Setting color to system default or defined variant"); - } else { - LOG_INFO("Setting color to %s", option.label); + auto action = static_cast(option.value); + switch (action) { + case PositionAction::GpsToggle: + menuQueue = gps_toggle_menu; + screen->runNow(); + break; + case PositionAction::GpsFormat: + menuQueue = gps_format_menu; + screen->runNow(); + break; + case PositionAction::CompassMenu: + menuQueue = compass_point_north_menu; + screen->runNow(); + break; + case PositionAction::CompassCalibrate: + if (accelerometerThread) { + accelerometerThread->calibrate(30); + } + break; + case PositionAction::GPSSmartPosition: + menuQueue = gps_smart_position_menu; + screen->runNow(); + break; + case PositionAction::GPSUpdateInterval: + menuQueue = gps_update_interval_menu; + screen->runNow(); + break; + case PositionAction::GPSPositionBroadcast: + menuQueue = gps_position_broadcast_menu; + screen->runNow(); + break; + } + }; + + BannerOverlayOptions bannerOptions; + if (accelerometerThread) { + bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); + } else { + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); + } + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::nodeListMenu() +{ + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + optionsArray[options] = "Add Favorite"; + optionsEnumArray[options++] = Favorite; + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = Verify; + } + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Show Long/Short Name"; + optionsEnumArray[options++] = NodeNameLength; + } + optionsArray[options] = "Reset NodeDB"; + optionsEnumArray[options++] = Reset; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Node Action"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Favorite) { + menuQueue = add_favorite; + screen->runNow(); + } else if (selected == Verify) { + menuQueue = key_verification_init; + screen->runNow(); + } else if (selected == Reset) { + menuQueue = reset_node_db_menu; + screen->runNow(); + } else if (selected == TraceRoute) { + menuQueue = trace_route_menu; + screen->runNow(); + } else if (selected == NodeNameLength) { + menuHandler::menuQueue = menuHandler::node_name_length_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::nodeNameLengthMenu() +{ + static const NodeNameOption nodeNameOptions[] = { + {"Back", OptionsAction::Back}, + {"Long", OptionsAction::Select, true}, + {"Short", OptionsAction::Select, false}, + }; + + constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); + static std::array nodeNameLabels{}; + + auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, + [](const NodeNameOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.display.use_long_node_name == option.value) { + return; + } + + config.display.use_long_node_name = option.value; + LOG_INFO("Setting names to %s", option.value ? "long" : "short"); + }); + + int initialSelection = config.display.use_long_node_name ? 1 : 2; + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::resetNodeDBMenu() +{ + static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Confirm Reset NodeDB"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1 || selected == 2) { + disableBluetooth(); + screen->setFrames(Screen::FOCUS_DEFAULT); + } + if (selected == 1) { + LOG_INFO("Initiate node-db reset"); + nodeDB->resetNodes(); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + LOG_INFO("Initiate node-db reset but keeping favorites"); + nodeDB->resetNodes(1); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 0) { + menuQueue = node_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::compassNorthMenu() +{ + static const CompassOption compassOptions[] = { + {"Back", OptionsAction::Back}, + {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, + {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, + {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, + }; + + constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); + static std::array compassLabels{}; + + auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, + [](const CompassOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.compass_mode == option.value) { + return; + } + + uiconfig.compass_mode = option.value; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); + + int initialSelection = 0; + for (size_t i = 0; i < compassCount; ++i) { + if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); +} + +#if !MESHTASTIC_EXCLUDE_GPS +void menuHandler::GPSToggleMenu() +{ + static const GPSToggleOption gpsToggleOptions[] = { + {"Back", OptionsAction::Back}, + {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, + {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, + }; + + constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); + static std::array toggleLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.position.gps_mode == option.value) { + return; + } + + config.position.gps_mode = option.value; + if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + playGPSEnableBeep(); + gps->enable(); + } else { + playGPSDisableBeep(); + gps->disable(); + } + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < toggleCount; ++i) { + if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + + screen->showOverlayBanner(bannerOptions); +} +void menuHandler::GPSFormatMenu() +{ + static const GPSFormatOption formatOptionsHigh[] = { + {"Back", OptionsAction::Back}, + {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; + + static const GPSFormatOption formatOptionsLow[] = { + {"Back", OptionsAction::Back}, + {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; + + constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); + static std::array formatLabelsHigh{}; + static std::array formatLabelsLow{}; + + auto onSelection = [](const GPSFormatOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; } - uint8_t r = color.r; - uint8_t g = color.g; - uint8_t b = color.b; + if (!option.hasValue) { + return; + } - display->setColor(BLACK); - display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - display->setColor(WHITE); + if (uiconfig.gps_format == option.value) { + return; + } - if (color.useVariant || (r == 0 && g == 0 && b == 0)) { + uiconfig.gps_format = option.value; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + }; + + BannerOverlayOptions bannerOptions; + int initialSelection = 0; + + if (currentResolution == ScreenResolution::High) { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { + initialSelection = static_cast(i); + break; + } + } + } else { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { + initialSelection = static_cast(i); + break; + } + } + } + + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSSmartPositionMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Smart Position"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Smrt Postn"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_smart_enabled = true; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + config.position.position_broadcast_smart_enabled = false; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSUpdateIntervalMenu() +{ + static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", + "2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", + "6 hours", "12 hours", "24 hours", "At Boot Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Update Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 16; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.gps_update_interval = 8; + } else if (selected == 2) { + config.position.gps_update_interval = 20; + } else if (selected == 3) { + config.position.gps_update_interval = 40; + } else if (selected == 4) { + config.position.gps_update_interval = 60; + } else if (selected == 5) { + config.position.gps_update_interval = 80; + } else if (selected == 6) { + config.position.gps_update_interval = 120; + } else if (selected == 7) { + config.position.gps_update_interval = 300; + } else if (selected == 8) { + config.position.gps_update_interval = 600; + } else if (selected == 9) { + config.position.gps_update_interval = 900; + } else if (selected == 10) { + config.position.gps_update_interval = 1800; + } else if (selected == 11) { + config.position.gps_update_interval = 3600; + } else if (selected == 12) { + config.position.gps_update_interval = 21600; + } else if (selected == 13) { + config.position.gps_update_interval = 43200; + } else if (selected == 14) { + config.position.gps_update_interval = 86400; + } else if (selected == 15) { + config.position.gps_update_interval = 2147483647; // At Boot Only + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.gps_update_interval == 8) { + bannerOptions.InitialSelected = 1; + } else if (config.position.gps_update_interval == 20) { + bannerOptions.InitialSelected = 2; + } else if (config.position.gps_update_interval == 40) { + bannerOptions.InitialSelected = 3; + } else if (config.position.gps_update_interval == 60) { + bannerOptions.InitialSelected = 4; + } else if (config.position.gps_update_interval == 80) { + bannerOptions.InitialSelected = 5; + } else if (config.position.gps_update_interval == 120) { + bannerOptions.InitialSelected = 6; + } else if (config.position.gps_update_interval == 300) { + bannerOptions.InitialSelected = 7; + } else if (config.position.gps_update_interval == 600) { + bannerOptions.InitialSelected = 8; + } else if (config.position.gps_update_interval == 900) { + bannerOptions.InitialSelected = 9; + } else if (config.position.gps_update_interval == 1800) { + bannerOptions.InitialSelected = 10; + } else if (config.position.gps_update_interval == 3600) { + bannerOptions.InitialSelected = 11; + } else if (config.position.gps_update_interval == 21600) { + bannerOptions.InitialSelected = 12; + } else if (config.position.gps_update_interval == 43200) { + bannerOptions.InitialSelected = 13; + } else if (config.position.gps_update_interval == 86400) { + bannerOptions.InitialSelected = 14; + } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only + bannerOptions.InitialSelected = 15; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSPositionBroadcastMenu() +{ + static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", + "2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours", + "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Broadcast Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_secs = 60; + } else if (selected == 2) { + config.position.position_broadcast_secs = 90; + } else if (selected == 3) { + config.position.position_broadcast_secs = 300; + } else if (selected == 4) { + config.position.position_broadcast_secs = 900; + } else if (selected == 5) { + config.position.position_broadcast_secs = 3600; + } else if (selected == 6) { + config.position.position_broadcast_secs = 7200; + } else if (selected == 7) { + config.position.position_broadcast_secs = 10800; + } else if (selected == 8) { + config.position.position_broadcast_secs = 14400; + } else if (selected == 9) { + config.position.position_broadcast_secs = 18000; + } else if (selected == 10) { + config.position.position_broadcast_secs = 21600; + } else if (selected == 11) { + config.position.position_broadcast_secs = 43200; + } else if (selected == 12) { + config.position.position_broadcast_secs = 64800; + } else if (selected == 13) { + config.position.position_broadcast_secs = 86400; + } else if (selected == 14) { + config.position.position_broadcast_secs = 129600; + } else if (selected == 15) { + config.position.position_broadcast_secs = 172800; + } else if (selected == 16) { + config.position.position_broadcast_secs = 259200; + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.position_broadcast_secs == 60) { + bannerOptions.InitialSelected = 1; + } else if (config.position.position_broadcast_secs == 90) { + bannerOptions.InitialSelected = 2; + } else if (config.position.position_broadcast_secs == 300) { + bannerOptions.InitialSelected = 3; + } else if (config.position.position_broadcast_secs == 900) { + bannerOptions.InitialSelected = 4; + } else if (config.position.position_broadcast_secs == 3600) { + bannerOptions.InitialSelected = 5; + } else if (config.position.position_broadcast_secs == 7200) { + bannerOptions.InitialSelected = 6; + } else if (config.position.position_broadcast_secs == 10800) { + bannerOptions.InitialSelected = 7; + } else if (config.position.position_broadcast_secs == 14400) { + bannerOptions.InitialSelected = 8; + } else if (config.position.position_broadcast_secs == 18000) { + bannerOptions.InitialSelected = 9; + } else if (config.position.position_broadcast_secs == 21600) { + bannerOptions.InitialSelected = 10; + } else if (config.position.position_broadcast_secs == 43200) { + bannerOptions.InitialSelected = 11; + } else if (config.position.position_broadcast_secs == 64800) { + bannerOptions.InitialSelected = 12; + } else if (config.position.position_broadcast_secs == 86400) { + bannerOptions.InitialSelected = 13; + } else if (config.position.position_broadcast_secs == 129600) { + bannerOptions.InitialSelected = 14; + } else if (config.position.position_broadcast_secs == 172800) { + bannerOptions.InitialSelected = 15; + } else if (config.position.position_broadcast_secs == 259200) { + bannerOptions.InitialSelected = 16; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +#endif + +void menuHandler::BluetoothToggleMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Bluetooth"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Bluetooth"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) + return; + else if (selected != (config.bluetooth.enabled ? 1 : 2)) { + InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + }; + bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BuzzerModeMenu() +{ + static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Notification Sounds"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; + service->reloadConfig(SEGMENT_CONFIG); + }; + bannerOptions.InitialSelected = config.device.buzzer_mode; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::BrightnessPickerMenu() +{ + static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; + + // Get current brightness level to set initial selection + int currentSelection = 1; // Default to Medium + if (uiconfig.screen_brightness >= 255) { + currentSelection = 3; // Very High + } else if (uiconfig.screen_brightness >= 128) { + currentSelection = 2; // High + } else { + currentSelection = 1; // Medium + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Brightness"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 4; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { // Medium + uiconfig.screen_brightness = 64; + } else if (selected == 2) { // High + uiconfig.screen_brightness = 128; + } else if (selected == 3) { // Very High + uiconfig.screen_brightness = 255; + } + + if (selected != 0) { // Not "Back" + // Apply brightness immediately +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) + // For HELTEC devices, use analogWrite to control backlight + analogWrite(VTFT_LEDA, uiconfig.screen_brightness); +#elif defined(ST7789_CS) || defined(ST7796_CS) + static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); +#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); +#endif + + // Save to device + saveUIConfig(); + + LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); + } + }; + bannerOptions.InitialSelected = currentSelection; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::switchToMUIMenu() +{ + static const char *optionsArray[] = {"No", "Yes"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Switch to MUI?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) +{ + static const ScreenColorOption colorOptions[] = { + {"Back", OptionsAction::Back}, + {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, + {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, + {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, + {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, + {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, + {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, + {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, + {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, + {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, + {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, + {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, + {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, + {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, + }; + + constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); + static std::array colorLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = system_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ + HAS_TFT || defined(HACKADAY_COMMUNICATOR) + const ScreenColor &color = option.value; + if (color.useVariant) { + LOG_INFO("Setting color to system default or defined variant"); + } else { + LOG_INFO("Setting color to %s", option.label); + } + + uint8_t r = color.r; + uint8_t g = color.g; + uint8_t b = color.b; + + display->setColor(BLACK); + display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + display->setColor(WHITE); + + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { #ifdef TFT_MESH_OVERRIDE - TFT_MESH = TFT_MESH_OVERRIDE; + TFT_MESH = TFT_MESH_OVERRIDE; #else TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif - } else { - TFT_MESH = COLOR565(r, g, b); - } + } else { + TFT_MESH = COLOR565(r, g, b); + } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) - static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); + static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); #endif - screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (color.useVariant || (r == 0 && g == 0 && b == 0)) { - uiconfig.screen_rgb_color = 0; - } else { - uiconfig.screen_rgb_color = (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); + screen->setFrames(graphics::Screen::FOCUS_SYSTEM); + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { + uiconfig.screen_rgb_color = 0; + } else { + uiconfig.screen_rgb_color = + (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); + } + LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); + saveUIConfig(); +#endif + }); + + int initialSelection = 0; + if (uiconfig.screen_rgb_color == 0) { + initialSelection = 1; + } else { + uint32_t currentColor = uiconfig.screen_rgb_color; + for (size_t i = 0; i < colorCount; ++i) { + if (!colorOptions[i].hasValue) { + continue; + } + const ScreenColor &color = colorOptions[i].value; + if (color.useVariant) { + continue; + } + uint32_t encoded = + (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); + if (encoded == currentColor) { + initialSelection = static_cast(i); + break; + } } - LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); - saveUIConfig(); -#endif - }); - - int initialSelection = 0; - if (uiconfig.screen_rgb_color == 0) { - initialSelection = 1; - } else { - uint32_t currentColor = uiconfig.screen_rgb_color; - for (size_t i = 0; i < colorCount; ++i) { - if (!colorOptions[i].hasValue) { - continue; - } - const ScreenColor &color = colorOptions[i].value; - if (color.useVariant) { - continue; - } - uint32_t encoded = (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); - if (encoded == currentColor) { - initialSelection = static_cast(i); - break; - } } - } - bannerOptions.InitialSelected = initialSelection; + bannerOptions.InitialSelected = initialSelection; - screen->showOverlayBanner(bannerOptions); + screen->showOverlayBanner(bannerOptions); } -void menuHandler::rebootMenu() { - static const char *optionsArray[] = {"Back", "Confirm"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Reboot Device?"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Reboot"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); - nodeDB->saveToDisk(); - messageStore.saveToFlash(); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +void menuHandler::rebootMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot Device?"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Reboot"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); + messageStore.saveToFlash(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } else { + menuQueue = power_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::shutdownMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Shutdown Device?"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Shutdown"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else { + menuQueue = power_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::addFavoriteMenu() +{ + const char *NODE_PICKER_TITLE; + if (currentResolution == ScreenResolution::UltraLow) { + NODE_PICKER_TITLE = "Node Favorite"; } else { - menuQueue = power_menu; - screen->runNow(); + NODE_PICKER_TITLE = "Node To Favorite"; } - }; - screen->showOverlayBanner(bannerOptions); + screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { + LOG_WARN("Nodenum: %u", nodenum); + nodeDB->set_favorite(true, nodenum); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); } -void menuHandler::shutdownMenu() { - static const char *optionsArray[] = {"Back", "Confirm"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Shutdown Device?"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Shutdown"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); - } else { - menuQueue = power_menu; - screen->runNow(); +void menuHandler::removeFavoriteMenu() +{ + + static const char *optionsArray[] = {"Back", "Yes"}; + BannerOverlayOptions bannerOptions; + std::string message = "Unfavorite This Node?\n"; + auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); + if (node && node->has_user) { + message += sanitizeString(node->user.long_name).substr(0, 15); } - }; - screen->showOverlayBanner(bannerOptions); + bannerOptions.message = message.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum); + nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); + screen->setFrames(graphics::Screen::FOCUS_DEFAULT); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::addFavoriteMenu() { - const char *NODE_PICKER_TITLE; - if (currentResolution == ScreenResolution::UltraLow) { - NODE_PICKER_TITLE = "Node Favorite"; - } else { - NODE_PICKER_TITLE = "Node To Favorite"; - } - screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { - LOG_WARN("Nodenum: %u", nodenum); - nodeDB->set_favorite(true, nodenum); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); +void menuHandler::traceRouteMenu() +{ + screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(nodenum); + } + }); } -void menuHandler::removeFavoriteMenu() { +void menuHandler::testMenu() +{ - static const char *optionsArray[] = {"Back", "Yes"}; - BannerOverlayOptions bannerOptions; - std::string message = "Unfavorite This Node?\n"; - auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); - if (node && node->has_user) { - message += sanitizeString(node->user.long_name).substr(0, 15); - } - bannerOptions.message = message.c_str(); - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum); - nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); - screen->setFrames(graphics::Screen::FOCUS_DEFAULT); - } - }; - screen->showOverlayBanner(bannerOptions); + enum optionsNumbers { Back, NumberPicker, ShowChirpy }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; + + optionsArray[options] = "Number Picker"; + optionsEnumArray[options++] = NumberPicker; + + optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; + optionsEnumArray[options++] = ShowChirpy; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Hidden Test Menu"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == NumberPicker) { + menuQueue = number_test; + screen->runNow(); + } else if (selected == ShowChirpy) { + screen->toggleFrameVisibility("chirpy"); + screen->setFrames(Screen::FOCUS_SYSTEM); + + } else { + menuQueue = system_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::traceRouteMenu() { - screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { - LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); - if (traceRouteModule) { - traceRouteModule->startTraceRoute(nodenum); - } - }); +void menuHandler::numberTest() +{ + screen->showNumberPicker("Pick a number\n ", 30000, 4, + [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); } -void menuHandler::testMenu() { +void menuHandler::wifiBaseMenu() +{ + enum optionsNumbers { Back, Wifi_toggle }; - enum optionsNumbers { Back, NumberPicker, ShowChirpy }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; - int options = 1; - - optionsArray[options] = "Number Picker"; - optionsEnumArray[options++] = NumberPicker; - - optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; - optionsEnumArray[options++] = ShowChirpy; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Hidden Test Menu"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == NumberPicker) { - menuQueue = number_test; - screen->runNow(); - } else if (selected == ShowChirpy) { - screen->toggleFrameVisibility("chirpy"); - screen->setFrames(Screen::FOCUS_SYSTEM); - - } else { - menuQueue = system_base_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + static const char *optionsArray[] = {"Back", "WiFi Toggle"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "WiFi Menu"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_toggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::numberTest() { - screen->showNumberPicker("Pick a number\n ", 30000, 4, [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); +void menuHandler::wifiToggleMenu() +{ + enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; + + static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "WiFi Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + if (config.network.wifi_enabled == true) + bannerOptions.InitialSelected = 2; + else + bannerOptions.InitialSelected = 1; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_disable) { + config.network.wifi_enabled = false; + config.bluetooth.enabled = true; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == Wifi_enable) { + config.network.wifi_enabled = true; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::wifiBaseMenu() { - enum optionsNumbers { Back, Wifi_toggle }; - - static const char *optionsArray[] = {"Back", "WiFi Toggle"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "WiFi Menu"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Wifi_toggle) { - menuQueue = wifi_toggle_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::wifiToggleMenu() { - enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; - - static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "WiFi Actions"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - if (config.network.wifi_enabled == true) - bannerOptions.InitialSelected = 2; - else - bannerOptions.InitialSelected = 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Wifi_disable) { - config.network.wifi_enabled = false; - config.bluetooth.enabled = true; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } else if (selected == Wifi_enable) { - config.network.wifi_enabled = true; - config.bluetooth.enabled = false; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::screenOptionsMenu() { - // Check if brightness is supported - bool hasSupportBrightness = false; +void menuHandler::screenOptionsMenu() +{ + // Check if brightness is supported + bool hasSupportBrightness = false; #if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - hasSupportBrightness = true; + hasSupportBrightness = true; #endif #if defined(T_DECK) - // TDeck Doesn't seem to support brightness at all, at least not reliably - hasSupportBrightness = false; + // TDeck Doesn't seem to support brightness at all, at least not reliably + hasSupportBrightness = false; #endif - enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; - static const char *optionsArray[5] = {"Back"}; - static int optionsEnumArray[5] = {Back}; - int options = 1; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; + static const char *optionsArray[5] = {"Back"}; + static int optionsEnumArray[5] = {Back}; + int options = 1; - // Only show brightness for B&W displays - if (hasSupportBrightness) { - optionsArray[options] = "Brightness"; - optionsEnumArray[options++] = Brightness; - } - - // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT || \ - defined(HACKADAY_COMMUNICATOR) - optionsArray[options] = "Screen Color"; - optionsEnumArray[options++] = ScreenColor; -#endif - - optionsArray[options] = "Frame Visibility"; - optionsEnumArray[options++] = FrameToggles; - - optionsArray[options] = "Display Units"; - optionsEnumArray[options++] = DisplayUnits; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Display Options"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Brightness) { - menuHandler::menuQueue = menuHandler::brightness_picker; - screen->runNow(); - } else if (selected == ScreenColor) { - menuHandler::menuQueue = menuHandler::tftcolormenupicker; - screen->runNow(); - } else if (selected == FrameToggles) { - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == DisplayUnits) { - menuHandler::menuQueue = menuHandler::DisplayUnits; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); + // Only show brightness for B&W displays + if (hasSupportBrightness) { + optionsArray[options] = "Brightness"; + optionsEnumArray[options++] = Brightness; } - }; - screen->showOverlayBanner(bannerOptions); + + // Only show screen color for TFT displays +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ + HAS_TFT || defined(HACKADAY_COMMUNICATOR) + optionsArray[options] = "Screen Color"; + optionsEnumArray[options++] = ScreenColor; +#endif + + optionsArray[options] = "Frame Visibility"; + optionsEnumArray[options++] = FrameToggles; + + optionsArray[options] = "Display Units"; + optionsEnumArray[options++] = DisplayUnits; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Display Options"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Brightness) { + menuHandler::menuQueue = menuHandler::brightness_picker; + screen->runNow(); + } else if (selected == ScreenColor) { + menuHandler::menuQueue = menuHandler::tftcolormenupicker; + screen->runNow(); + } else if (selected == FrameToggles) { + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == DisplayUnits) { + menuHandler::menuQueue = menuHandler::DisplayUnits; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); } -void menuHandler::powerMenu() { +void menuHandler::powerMenu() +{ - enum optionsNumbers { Back, Reboot, Shutdown, MUI }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; - int options = 1; + enum optionsNumbers { Back, Reboot, Shutdown, MUI }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; - optionsArray[options] = "Reboot"; - optionsEnumArray[options++] = Reboot; + optionsArray[options] = "Reboot"; + optionsEnumArray[options++] = Reboot; - optionsArray[options] = "Shutdown"; - optionsEnumArray[options++] = Shutdown; + optionsArray[options] = "Shutdown"; + optionsEnumArray[options++] = Shutdown; #if HAS_TFT - optionsArray[options] = "Switch to MUI"; - optionsEnumArray[options++] = MUI; + optionsArray[options] = "Switch to MUI"; + optionsEnumArray[options++] = MUI; #endif - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Reboot / Shutdown"; - if (currentResolution == ScreenResolution::UltraLow) { - bannerOptions.message = "Power"; - } - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Reboot) { - menuHandler::menuQueue = menuHandler::reboot_menu; - screen->runNow(); - } else if (selected == Shutdown) { - menuHandler::menuQueue = menuHandler::shutdown_menu; - screen->runNow(); - } else if (selected == MUI) { - menuHandler::menuQueue = menuHandler::mui_picker; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Reboot / Shutdown"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Power"; } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::keyVerificationInitMenu() { - screen->showNodePicker("Node to Verify", 30000, [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); }); -} - -void menuHandler::keyVerificationFinalPrompt() { - char message[40] = {0}; - memset(message, 0, sizeof(message)); - sprintf(message, "Verification: \n"); - keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet - - if (screen) { - static const char *optionsArray[] = {"Reject", "Accept"}; - graphics::BannerOverlayOptions options; - options.message = message; - options.durationMs = 30000; - options.optionsArrayPtr = optionsArray; - options.optionsCount = 2; - options.notificationType = graphics::notificationTypeEnum::selection_picker; - options.bannerCallback = [=](int selected) { - if (selected == 1) { - auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode()); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Reboot) { + menuHandler::menuQueue = menuHandler::reboot_menu; + screen->runNow(); + } else if (selected == Shutdown) { + menuHandler::menuQueue = menuHandler::shutdown_menu; + screen->runNow(); + } else if (selected == MUI) { + menuHandler::menuQueue = menuHandler::mui_picker; + screen->runNow(); + } else { + menuQueue = system_base_menu; + screen->runNow(); + } }; - screen->showOverlayBanner(options); - } + screen->showOverlayBanner(bannerOptions); } -void menuHandler::FrameToggles_menu() { - enum optionsNumbers { - Finish, - nodelist_nodes, - nodelist_location, - nodelist_lastheard, - nodelist_hopsignal, - nodelist_distance, - nodelist_bearings, - gps, - lora, - clock, - show_favorites, - show_telemetry, - show_power, - enumEnd - }; - static const char *optionsArray[enumEnd] = {"Finish"}; - static int optionsEnumArray[enumEnd] = {Finish}; - int options = 1; +void menuHandler::keyVerificationInitMenu() +{ + screen->showNodePicker("Node to Verify", 30000, + [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); }); +} - // Track last selected index (not enum value!) - static int lastSelectedIndex = 0; +void menuHandler::keyVerificationFinalPrompt() +{ + char message[40] = {0}; + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet + + if (screen) { + static const char *optionsArray[] = {"Reject", "Accept"}; + graphics::BannerOverlayOptions options; + options.message = message; + options.durationMs = 30000; + options.optionsArrayPtr = optionsArray; + options.optionsCount = 2; + options.notificationType = graphics::notificationTypeEnum::selection_picker; + options.bannerCallback = [=](int selected) { + if (selected == 1) { + auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode()); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + }; + screen->showOverlayBanner(options); + } +} + +void menuHandler::FrameToggles_menu() +{ + enum optionsNumbers { + Finish, + nodelist_nodes, + nodelist_location, + nodelist_lastheard, + nodelist_hopsignal, + nodelist_distance, + nodelist_bearings, + gps, + lora, + clock, + show_favorites, + show_telemetry, + show_power, + enumEnd + }; + static const char *optionsArray[enumEnd] = {"Finish"}; + static int optionsEnumArray[enumEnd] = {Finish}; + int options = 1; + + // Track last selected index (not enum value!) + static int lastSelectedIndex = 0; #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; - optionsEnumArray[options++] = nodelist_nodes; + optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; + optionsEnumArray[options++] = nodelist_nodes; #else - optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; - optionsEnumArray[options++] = nodelist_lastheard; - optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; - optionsEnumArray[options++] = nodelist_hopsignal; + optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; + optionsEnumArray[options++] = nodelist_lastheard; + optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; + optionsEnumArray[options++] = nodelist_hopsignal; #endif #if HAS_GPS #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; - optionsEnumArray[options++] = nodelist_location; + optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; + optionsEnumArray[options++] = nodelist_location; #else - optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; - optionsEnumArray[options++] = nodelist_distance; - optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; - optionsEnumArray[options++] = nodelist_bearings; + optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; + optionsEnumArray[options++] = nodelist_distance; + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; + optionsEnumArray[options++] = nodelist_bearings; #endif - optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; - optionsEnumArray[options++] = gps; + optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; + optionsEnumArray[options++] = gps; #endif - optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; - optionsEnumArray[options++] = lora; + optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; + optionsEnumArray[options++] = lora; - optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; - optionsEnumArray[options++] = clock; + optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; + optionsEnumArray[options++] = clock; - optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; - optionsEnumArray[options++] = show_favorites; + optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; + optionsEnumArray[options++] = show_favorites; - optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; - optionsEnumArray[options++] = show_telemetry; + optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; + optionsEnumArray[options++] = show_telemetry; - optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; - optionsEnumArray[options++] = show_power; + optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; + optionsEnumArray[options++] = show_power; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Show/Hide Frames"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Show/Hide Frames"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value - bannerOptions.bannerCallback = [options](int selected) mutable -> void { - // Find the index of selected in optionsEnumArray - int idx = 0; - for (; idx < options; ++idx) { - if (optionsEnumArray[idx] == selected) + bannerOptions.bannerCallback = [options](int selected) mutable -> void { + // Find the index of selected in optionsEnumArray + int idx = 0; + for (; idx < options; ++idx) { + if (optionsEnumArray[idx] == selected) + break; + } + lastSelectedIndex = idx; + + if (selected == Finish) { + screen->setFrames(Screen::FOCUS_DEFAULT); + } else if (selected == nodelist_nodes) { + screen->toggleFrameVisibility("nodelist_nodes"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_location) { + screen->toggleFrameVisibility("nodelist_location"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_lastheard) { + screen->toggleFrameVisibility("nodelist_lastheard"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_hopsignal) { + screen->toggleFrameVisibility("nodelist_hopsignal"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_distance) { + screen->toggleFrameVisibility("nodelist_distance"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_bearings) { + screen->toggleFrameVisibility("nodelist_bearings"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == gps) { + screen->toggleFrameVisibility("gps"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == lora) { + screen->toggleFrameVisibility("lora"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == clock) { + screen->toggleFrameVisibility("clock"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_favorites) { + screen->toggleFrameVisibility("show_favorites"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_telemetry) { + moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_power) { + moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::DisplayUnits_menu() +{ + enum optionsNumbers { Back, MetricUnits, ImperialUnits }; + + static const char *optionsArray[] = {"Back", "Metric", "Imperial"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = " Select display units"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + bannerOptions.InitialSelected = 2; + else + bannerOptions.InitialSelected = 1; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == MetricUnits) { + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == ImperialUnits) { + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuHandler::menuQueue = menuHandler::screen_options_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::handleMenuSwitch(OLEDDisplay *display) +{ + if (menuQueue != menu_none) + test_count = 0; + switch (menuQueue) { + case menu_none: + break; + case lora_Menu: + loraMenu(); + break; + case lora_picker: + LoraRegionPicker(); + break; + case device_role_picker: + DeviceRolePicker(); + break; + case radio_preset_picker: + RadioPresetPicker(); + break; + case no_timeout_lora_picker: + LoraRegionPicker(0); + break; + case TZ_picker: + TZPicker(); + break; + case twelve_hour_picker: + TwelveHourPicker(); + break; + case clock_face_picker: + ClockFacePicker(); + break; + case clock_menu: + clockMenu(); + break; + case system_base_menu: + systemBaseMenu(); + break; + case position_base_menu: + positionBaseMenu(); + break; + case node_base_menu: + nodeListMenu(); + break; +#if !MESHTASTIC_EXCLUDE_GPS + case gps_toggle_menu: + GPSToggleMenu(); + break; + case gps_format_menu: + GPSFormatMenu(); + break; + case gps_smart_position_menu: + GPSSmartPositionMenu(); + break; + case gps_update_interval_menu: + GPSUpdateIntervalMenu(); + break; + case gps_position_broadcast_menu: + GPSPositionBroadcastMenu(); + break; +#endif + case compass_point_north_menu: + compassNorthMenu(); + break; + case reset_node_db_menu: + resetNodeDBMenu(); + break; + case buzzermodemenupicker: + BuzzerModeMenu(); + break; + case mui_picker: + switchToMUIMenu(); + break; + case tftcolormenupicker: + TFTColorPickerMenu(display); + break; + case brightness_picker: + BrightnessPickerMenu(); + break; + case node_name_length_menu: + nodeNameLengthMenu(); + break; + case reboot_menu: + rebootMenu(); + break; + case shutdown_menu: + shutdownMenu(); + break; + case add_favorite: + addFavoriteMenu(); + break; + case remove_favorite: + removeFavoriteMenu(); + break; + case trace_route_menu: + traceRouteMenu(); + break; + case test_menu: + testMenu(); + break; + case number_test: + numberTest(); + break; + case wifi_toggle_menu: + wifiToggleMenu(); + break; + case key_verification_init: + keyVerificationInitMenu(); + break; + case key_verification_final_prompt: + keyVerificationFinalPrompt(); + break; + case bluetooth_toggle_menu: + BluetoothToggleMenu(); + break; + case screen_options_menu: + screenOptionsMenu(); + break; + case power_menu: + powerMenu(); + break; + case FrameToggles: + FrameToggles_menu(); + break; + case DisplayUnits: + DisplayUnits_menu(); + break; + case throttle_message: + screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); + break; + case message_response_menu: + messageResponseMenu(); + break; + case reply_menu: + replyMenu(); + break; + case delete_messages_menu: + deleteMessagesMenu(); + break; + case message_viewmode_menu: + messageViewModeMenu(); break; } - lastSelectedIndex = idx; - - if (selected == Finish) { - screen->setFrames(Screen::FOCUS_DEFAULT); - } else if (selected == nodelist_nodes) { - screen->toggleFrameVisibility("nodelist_nodes"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_location) { - screen->toggleFrameVisibility("nodelist_location"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_lastheard) { - screen->toggleFrameVisibility("nodelist_lastheard"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_hopsignal) { - screen->toggleFrameVisibility("nodelist_hopsignal"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_distance) { - screen->toggleFrameVisibility("nodelist_distance"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == nodelist_bearings) { - screen->toggleFrameVisibility("nodelist_bearings"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == gps) { - screen->toggleFrameVisibility("gps"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == lora) { - screen->toggleFrameVisibility("lora"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == clock) { - screen->toggleFrameVisibility("clock"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == show_favorites) { - screen->toggleFrameVisibility("show_favorites"); - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == show_telemetry) { - moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } else if (selected == show_power) { - moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; - menuHandler::menuQueue = menuHandler::FrameToggles; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); + menuQueue = menu_none; } -void menuHandler::DisplayUnits_menu() { - enum optionsNumbers { Back, MetricUnits, ImperialUnits }; - - static const char *optionsArray[] = {"Back", "Metric", "Imperial"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = " Select display units"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - bannerOptions.InitialSelected = 2; - else - bannerOptions.InitialSelected = 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == MetricUnits) { - config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == ImperialUnits) { - config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; - service->reloadConfig(SEGMENT_CONFIG); - } else { - menuHandler::menuQueue = menuHandler::screen_options_menu; - screen->runNow(); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::handleMenuSwitch(OLEDDisplay *display) { - if (menuQueue != menu_none) - test_count = 0; - switch (menuQueue) { - case menu_none: - break; - case lora_Menu: - loraMenu(); - break; - case lora_picker: - LoraRegionPicker(); - break; - case device_role_picker: - DeviceRolePicker(); - break; - case radio_preset_picker: - RadioPresetPicker(); - break; - case no_timeout_lora_picker: - LoraRegionPicker(0); - break; - case TZ_picker: - TZPicker(); - break; - case twelve_hour_picker: - TwelveHourPicker(); - break; - case clock_face_picker: - ClockFacePicker(); - break; - case clock_menu: - clockMenu(); - break; - case system_base_menu: - systemBaseMenu(); - break; - case position_base_menu: - positionBaseMenu(); - break; - case node_base_menu: - nodeListMenu(); - break; -#if !MESHTASTIC_EXCLUDE_GPS - case gps_toggle_menu: - GPSToggleMenu(); - break; - case gps_format_menu: - GPSFormatMenu(); - break; - case gps_smart_position_menu: - GPSSmartPositionMenu(); - break; - case gps_update_interval_menu: - GPSUpdateIntervalMenu(); - break; - case gps_position_broadcast_menu: - GPSPositionBroadcastMenu(); - break; -#endif - case compass_point_north_menu: - compassNorthMenu(); - break; - case reset_node_db_menu: - resetNodeDBMenu(); - break; - case buzzermodemenupicker: - BuzzerModeMenu(); - break; - case mui_picker: - switchToMUIMenu(); - break; - case tftcolormenupicker: - TFTColorPickerMenu(display); - break; - case brightness_picker: - BrightnessPickerMenu(); - break; - case node_name_length_menu: - nodeNameLengthMenu(); - break; - case reboot_menu: - rebootMenu(); - break; - case shutdown_menu: - shutdownMenu(); - break; - case add_favorite: - addFavoriteMenu(); - break; - case remove_favorite: - removeFavoriteMenu(); - break; - case trace_route_menu: - traceRouteMenu(); - break; - case test_menu: - testMenu(); - break; - case number_test: - numberTest(); - break; - case wifi_toggle_menu: - wifiToggleMenu(); - break; - case key_verification_init: - keyVerificationInitMenu(); - break; - case key_verification_final_prompt: - keyVerificationFinalPrompt(); - break; - case bluetooth_toggle_menu: - BluetoothToggleMenu(); - break; - case screen_options_menu: - screenOptionsMenu(); - break; - case power_menu: - powerMenu(); - break; - case FrameToggles: - FrameToggles_menu(); - break; - case DisplayUnits: - DisplayUnits_menu(); - break; - case throttle_message: - screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); - break; - case message_response_menu: - messageResponseMenu(); - break; - case reply_menu: - replyMenu(); - break; - case delete_messages_menu: - deleteMessagesMenu(); - break; - case message_viewmode_menu: - messageViewModeMenu(); - break; - } - menuQueue = menu_none; -} - -void menuHandler::saveUIConfig() { - nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); +void menuHandler::saveUIConfig() +{ + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); } } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index c06c7a706..445513e25 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -1,135 +1,143 @@ #pragma once #if HAS_SCREEN #include "configuration.h" -namespace graphics { +namespace graphics +{ -class menuHandler { -public: - enum screenMenus { - menu_none, - lora_Menu, - lora_picker, - device_role_picker, - radio_preset_picker, - no_timeout_lora_picker, - TZ_picker, - twelve_hour_picker, - clock_face_picker, - clock_menu, - position_base_menu, - node_base_menu, - gps_toggle_menu, - gps_format_menu, - gps_smart_position_menu, - gps_update_interval_menu, - gps_position_broadcast_menu, - compass_point_north_menu, - reset_node_db_menu, - buzzermodemenupicker, - mui_picker, - tftcolormenupicker, - brightness_picker, - reboot_menu, - shutdown_menu, - add_favorite, - remove_favorite, - test_menu, - number_test, - wifi_toggle_menu, - bluetooth_toggle_menu, - screen_options_menu, - power_menu, - system_base_menu, - key_verification_init, - key_verification_final_prompt, - trace_route_menu, - throttle_message, - message_response_menu, - message_viewmode_menu, - reply_menu, - delete_messages_menu, - node_name_length_menu, - FrameToggles, - DisplayUnits - }; - static screenMenus menuQueue; +class menuHandler +{ + public: + enum screenMenus { + menu_none, + lora_Menu, + lora_picker, + device_role_picker, + radio_preset_picker, + no_timeout_lora_picker, + TZ_picker, + twelve_hour_picker, + clock_face_picker, + clock_menu, + position_base_menu, + node_base_menu, + gps_toggle_menu, + gps_format_menu, + gps_smart_position_menu, + gps_update_interval_menu, + gps_position_broadcast_menu, + compass_point_north_menu, + reset_node_db_menu, + buzzermodemenupicker, + mui_picker, + tftcolormenupicker, + brightness_picker, + reboot_menu, + shutdown_menu, + add_favorite, + remove_favorite, + test_menu, + number_test, + wifi_toggle_menu, + bluetooth_toggle_menu, + screen_options_menu, + power_menu, + system_base_menu, + key_verification_init, + key_verification_final_prompt, + trace_route_menu, + throttle_message, + message_response_menu, + message_viewmode_menu, + reply_menu, + delete_messages_menu, + node_name_length_menu, + FrameToggles, + DisplayUnits + }; + static screenMenus menuQueue; - static void OnboardMessage(); - static void LoraRegionPicker(uint32_t duration = 30000); - static void loraMenu(); - static void DeviceRolePicker(); - static void RadioPresetPicker(); - static void handleMenuSwitch(OLEDDisplay *display); - static void showConfirmationBanner(const char *message, std::function onConfirm); - static void clockMenu(); - static void TZPicker(); - static void TwelveHourPicker(); - static void ClockFacePicker(); - static void messageResponseMenu(); - static void messageViewModeMenu(); - static void replyMenu(); - static void deleteMessagesMenu(); - static void homeBaseMenu(); - static void textMessageBaseMenu(); - static void systemBaseMenu(); - static void favoriteBaseMenu(); - static void positionBaseMenu(); - static void compassNorthMenu(); - static void GPSToggleMenu(); - static void GPSFormatMenu(); - static void GPSSmartPositionMenu(); - static void GPSUpdateIntervalMenu(); - static void GPSPositionBroadcastMenu(); - static void BuzzerModeMenu(); - static void switchToMUIMenu(); - static void TFTColorPickerMenu(OLEDDisplay *display); - static void nodeListMenu(); - static void resetNodeDBMenu(); - static void BrightnessPickerMenu(); - static void rebootMenu(); - static void shutdownMenu(); - static void addFavoriteMenu(); - static void removeFavoriteMenu(); - static void traceRouteMenu(); - static void testMenu(); - static void numberTest(); - static void wifiBaseMenu(); - static void wifiToggleMenu(); - static void screenOptionsMenu(); - static void powerMenu(); - static void nodeNameLengthMenu(); - static void FrameToggles_menu(); - static void DisplayUnits_menu(); - static void textMessageMenu(); + static void OnboardMessage(); + static void LoraRegionPicker(uint32_t duration = 30000); + static void loraMenu(); + static void DeviceRolePicker(); + static void RadioPresetPicker(); + static void handleMenuSwitch(OLEDDisplay *display); + static void showConfirmationBanner(const char *message, std::function onConfirm); + static void clockMenu(); + static void TZPicker(); + static void TwelveHourPicker(); + static void ClockFacePicker(); + static void messageResponseMenu(); + static void messageViewModeMenu(); + static void replyMenu(); + static void deleteMessagesMenu(); + static void homeBaseMenu(); + static void textMessageBaseMenu(); + static void systemBaseMenu(); + static void favoriteBaseMenu(); + static void positionBaseMenu(); + static void compassNorthMenu(); + static void GPSToggleMenu(); + static void GPSFormatMenu(); + static void GPSSmartPositionMenu(); + static void GPSUpdateIntervalMenu(); + static void GPSPositionBroadcastMenu(); + static void BuzzerModeMenu(); + static void switchToMUIMenu(); + static void TFTColorPickerMenu(OLEDDisplay *display); + static void nodeListMenu(); + static void resetNodeDBMenu(); + static void BrightnessPickerMenu(); + static void rebootMenu(); + static void shutdownMenu(); + static void addFavoriteMenu(); + static void removeFavoriteMenu(); + static void traceRouteMenu(); + static void testMenu(); + static void numberTest(); + static void wifiBaseMenu(); + static void wifiToggleMenu(); + static void screenOptionsMenu(); + static void powerMenu(); + static void nodeNameLengthMenu(); + static void FrameToggles_menu(); + static void DisplayUnits_menu(); + static void textMessageMenu(); -private: - static void saveUIConfig(); - static void keyVerificationInitMenu(); - static void keyVerificationFinalPrompt(); - static void BluetoothToggleMenu(); + private: + static void saveUIConfig(); + static void keyVerificationInitMenu(); + static void keyVerificationFinalPrompt(); + static void BluetoothToggleMenu(); }; /* Generic Menu Options designations */ enum class OptionsAction { Back, Select }; template struct MenuOption { - const char *label; - OptionsAction action; - bool hasValue; - T value; + const char *label; + OptionsAction action; + bool hasValue; + T value; - MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) : label(labelIn), action(actionIn), hasValue(true), value(valueIn) {} + MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) + : label(labelIn), action(actionIn), hasValue(true), value(valueIn) + { + } - MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} + MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} }; struct ScreenColor { - uint8_t r; - uint8_t g; - uint8_t b; - bool useVariant; + uint8_t r; + uint8_t g; + uint8_t b; + bool useVariant; - ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) : r(rIn), g(gIn), b(bIn), useVariant(variantIn) {} + ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) + : r(rIn), g(gIn), b(bIn), useVariant(variantIn) + { + } }; using RadioPresetOption = MenuOption; diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 2b11451bf..09b798e06 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -27,47 +27,51 @@ using graphics::Emote; using graphics::emotes; using graphics::numEmotes; -namespace graphics { -namespace MessageRenderer { +namespace graphics +{ +namespace MessageRenderer +{ static std::vector cachedLines; static std::vector cachedHeights; static bool manualScrolling = false; // UTF-8 skip helper -static inline size_t utf8CharLen(uint8_t c) { - if ((c & 0xE0) == 0xC0) - return 2; - if ((c & 0xF0) == 0xE0) - return 3; - if ((c & 0xF8) == 0xF0) - return 4; - return 1; +static inline size_t utf8CharLen(uint8_t c) +{ + if ((c & 0xE0) == 0xC0) + return 2; + if ((c & 0xF0) == 0xE0) + return 3; + if ((c & 0xF8) == 0xF0) + return 4; + return 1; } // Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels -std::string normalizeEmoji(const std::string &s) { - std::string out; - for (size_t i = 0; i < s.size();) { - uint8_t c = static_cast(s[i]); - size_t len = utf8CharLen(c); +std::string normalizeEmoji(const std::string &s) +{ + std::string out; + for (size_t i = 0; i < s.size();) { + uint8_t c = static_cast(s[i]); + size_t len = utf8CharLen(c); - if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { - i += 3; - continue; + if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { + i += 3; + continue; + } + + // Skip skin tone modifiers + if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && + ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { + i += 4; + continue; + } + + out.append(s, i, len); + i += len; } - - // Skip skin tone modifiers - if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && - ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { - i += 4; - continue; - } - - out.append(s, i, len); - i += len; - } - return out; + return out; } // Scroll state (file scope so we can reset on new message) @@ -79,205 +83,211 @@ bool waitingToReset = false; bool scrollStarted = false; static bool didReset = false; -void scrollUp() { - manualScrolling = true; - scrollY -= 12; - if (scrollY < 0) - scrollY = 0; +void scrollUp() +{ + manualScrolling = true; + scrollY -= 12; + if (scrollY < 0) + scrollY = 0; } -void scrollDown() { - manualScrolling = true; +void scrollDown() +{ + manualScrolling = true; - int totalHeight = 0; - for (int h : cachedHeights) - totalHeight += h; + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; - int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); - int maxScroll = totalHeight - visibleHeight; - if (maxScroll < 0) - maxScroll = 0; + int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); + int maxScroll = totalHeight - visibleHeight; + if (maxScroll < 0) + maxScroll = 0; - scrollY += 12; - if (scrollY > maxScroll) - scrollY = maxScroll; + scrollY += 12; + if (scrollY > maxScroll) + scrollY = maxScroll; } -void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { - std::string renderLine; - for (size_t i = 0; i < line.size();) { - uint8_t c = (uint8_t)line[i]; - size_t len = utf8CharLen(c); - if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) { - i += 3; - continue; +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) +{ + std::string renderLine; + for (size_t i = 0; i < line.size();) { + uint8_t c = (uint8_t)line[i]; + size_t len = utf8CharLen(c); + if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) { + i += 3; + continue; + } + if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F && + ((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) { + i += 4; + continue; + } + renderLine.append(line, i, len); + i += len; } - if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F && - ((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) { - i += 4; - continue; - } - renderLine.append(line, i, len); - i += len; - } - int cursorX = x; - const int fontHeight = FONT_HEIGHT_SMALL; + int cursorX = x; + const int fontHeight = FONT_HEIGHT_SMALL; - // Step 1: Find tallest emote in the line - int maxIconHeight = fontHeight; - for (size_t i = 0; i < line.length();) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (line.compare(i, emojiLen, emotes[e].label) == 0) { - if (emotes[e].height > maxIconHeight) - maxIconHeight = emotes[e].height; - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - i += utf8CharLen(static_cast(line[i])); - } - } - - // Step 2: Baseline alignment - int lineHeight = std::max(fontHeight, maxIconHeight); - int baselineOffset = (lineHeight - fontHeight) / 2; - int fontY = y + baselineOffset; - - // Step 3: Render line in segments - size_t i = 0; - bool inBold = false; - - while (i < line.length()) { - // Check for ** start/end for faux bold - if (line.compare(i, 2, "**") == 0) { - inBold = !inBold; - i += 2; - continue; + // Step 1: Find tallest emote in the line + int maxIconHeight = fontHeight; + for (size_t i = 0; i < line.length();) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (line.compare(i, emojiLen, emotes[e].label) == 0) { + if (emotes[e].height > maxIconHeight) + maxIconHeight = emotes[e].height; + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + i += utf8CharLen(static_cast(line[i])); + } } - // Look ahead for the next emote match - size_t nextEmotePos = std::string::npos; - const Emote *matchedEmote = nullptr; - size_t emojiLen = 0; + // Step 2: Baseline alignment + int lineHeight = std::max(fontHeight, maxIconHeight); + int baselineOffset = (lineHeight - fontHeight) / 2; + int fontY = y + baselineOffset; - for (int e = 0; e < emoteCount; ++e) { - size_t pos = line.find(emotes[e].label, i); - if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { - nextEmotePos = pos; - matchedEmote = &emotes[e]; - emojiLen = strlen(emotes[e].label); - } - } + // Step 3: Render line in segments + size_t i = 0; + bool inBold = false; - // Render normal text segment up to the emote or bold toggle - size_t nextControl = std::min(nextEmotePos, line.find("**", i)); - if (nextControl == std::string::npos) - nextControl = line.length(); + while (i < line.length()) { + // Check for ** start/end for faux bold + if (line.compare(i, 2, "**") == 0) { + inBold = !inBold; + i += 2; + continue; + } - if (nextControl > i) { - std::string textChunk = line.substr(i, nextControl - i); - if (inBold) { - // Faux bold: draw twice, offset by 1px - display->drawString(cursorX + 1, fontY, textChunk.c_str()); - } - display->drawString(cursorX, fontY, textChunk.c_str()); + // Look ahead for the next emote match + size_t nextEmotePos = std::string::npos; + const Emote *matchedEmote = nullptr; + size_t emojiLen = 0; + + for (int e = 0; e < emoteCount; ++e) { + size_t pos = line.find(emotes[e].label, i); + if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { + nextEmotePos = pos; + matchedEmote = &emotes[e]; + emojiLen = strlen(emotes[e].label); + } + } + + // Render normal text segment up to the emote or bold toggle + size_t nextControl = std::min(nextEmotePos, line.find("**", i)); + if (nextControl == std::string::npos) + nextControl = line.length(); + + if (nextControl > i) { + std::string textChunk = line.substr(i, nextControl - i); + if (inBold) { + // Faux bold: draw twice, offset by 1px + display->drawString(cursorX + 1, fontY, textChunk.c_str()); + } + display->drawString(cursorX, fontY, textChunk.c_str()); #if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); + cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); #else - cursorX += display->getStringWidth(textChunk.c_str()); + cursorX += display->getStringWidth(textChunk.c_str()); #endif - i = nextControl; - continue; - } + i = nextControl; + continue; + } - // Render the emote (if found) - if (matchedEmote && i == nextEmotePos) { - // Vertically center emote relative to font baseline (not just midline) - int iconY = fontY + (fontHeight - matchedEmote->height) / 2; - display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); - cursorX += matchedEmote->width + 1; - i += emojiLen; - continue; - } else { - // No more emotes — render the rest of the line - std::string remaining = line.substr(i); - if (inBold) { - display->drawString(cursorX + 1, fontY, remaining.c_str()); - } - display->drawString(cursorX, fontY, remaining.c_str()); + // Render the emote (if found) + if (matchedEmote && i == nextEmotePos) { + // Vertically center emote relative to font baseline (not just midline) + int iconY = fontY + (fontHeight - matchedEmote->height) / 2; + display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); + cursorX += matchedEmote->width + 1; + i += emojiLen; + continue; + } else { + // No more emotes — render the rest of the line + std::string remaining = line.substr(i); + if (inBold) { + display->drawString(cursorX + 1, fontY, remaining.c_str()); + } + display->drawString(cursorX, fontY, remaining.c_str()); #if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); + cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); #else - cursorX += display->getStringWidth(remaining.c_str()); + cursorX += display->getStringWidth(remaining.c_str()); #endif - break; + break; + } } - } } // Reset scroll state when new messages arrive -void resetScrollState() { - scrollY = 0.0f; - scrollStarted = false; - waitingToReset = false; - scrollStartDelay = millis(); - lastTime = millis(); - manualScrolling = false; - didReset = false; -} - -void nudgeScroll(int8_t direction) { - if (direction == 0) - return; - - if (cachedHeights.empty()) { +void resetScrollState() +{ scrollY = 0.0f; - return; - } - - OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; - const int displayHeight = display ? display->getHeight() : 64; - const int navHeight = FONT_HEIGHT_SMALL; - const int usableHeight = std::max(0, displayHeight - navHeight); - - int totalHeight = 0; - for (int h : cachedHeights) - totalHeight += h; - - if (totalHeight <= usableHeight) { - scrollY = 0.0f; - return; - } - - const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); - const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); - - float newScroll = scrollY + static_cast(direction) * static_cast(step); - if (newScroll < 0.0f) - newScroll = 0.0f; - if (newScroll > scrollStop) - newScroll = static_cast(scrollStop); - - if (newScroll != scrollY) { - scrollY = newScroll; - waitingToReset = false; scrollStarted = false; + waitingToReset = false; scrollStartDelay = millis(); lastTime = millis(); - } + manualScrolling = false; + didReset = false; +} + +void nudgeScroll(int8_t direction) +{ + if (direction == 0) + return; + + if (cachedHeights.empty()) { + scrollY = 0.0f; + return; + } + + OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; + const int displayHeight = display ? display->getHeight() : 64; + const int navHeight = FONT_HEIGHT_SMALL; + const int usableHeight = std::max(0, displayHeight - navHeight); + + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; + + if (totalHeight <= usableHeight) { + scrollY = 0.0f; + return; + } + + const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); + const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); + + float newScroll = scrollY + static_cast(direction) * static_cast(step); + if (newScroll < 0.0f) + newScroll = 0.0f; + if (newScroll > scrollStop) + newScroll = static_cast(scrollStop); + + if (newScroll != scrollY) { + scrollY = newScroll; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = millis(); + lastTime = millis(); + } } // Fully free cached message data from heap -void clearMessageCache() { - std::vector().swap(cachedLines); - std::vector().swap(cachedHeights); +void clearMessageCache() +{ + std::vector().swap(cachedLines); + std::vector().swap(cachedHeights); - // Reset scroll so we rebuild cleanly next time we enter the screen - resetScrollState(); + // Reset scroll so we rebuild cleanly next time we enter the screen + resetScrollState(); } // Current thread state @@ -290,702 +300,733 @@ static std::vector seenChannels; static std::vector seenPeers; // Public helper so menus / store can clear stale registries -void clearThreadRegistries() { - seenChannels.clear(); - seenPeers.clear(); +void clearThreadRegistries() +{ + seenChannels.clear(); + seenPeers.clear(); } // Setter so other code can switch threads -void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) { - currentMode = mode; - currentChannel = channel; - currentPeer = peer; - didReset = false; // force reset when mode changes +void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) +{ + currentMode = mode; + currentChannel = channel; + currentPeer = peer; + didReset = false; // force reset when mode changes - // Track channels we’ve seen - if (mode == ThreadMode::CHANNEL && channel >= 0) { - if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { - seenChannels.push_back(channel); + // Track channels we’ve seen + if (mode == ThreadMode::CHANNEL && channel >= 0) { + if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { + seenChannels.push_back(channel); + } } - } - // Track DMs we’ve seen - if (mode == ThreadMode::DIRECT && peer != 0) { - if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { - seenPeers.push_back(peer); + // Track DMs we’ve seen + if (mode == ThreadMode::DIRECT && peer != 0) { + if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { + seenPeers.push_back(peer); + } } - } } -ThreadMode getThreadMode() { return currentMode; } +ThreadMode getThreadMode() +{ + return currentMode; +} -int getThreadChannel() { return currentChannel; } +int getThreadChannel() +{ + return currentChannel; +} -uint32_t getThreadPeer() { return currentPeer; } +uint32_t getThreadPeer() +{ + return currentPeer; +} // Accessors for menuHandler -const std::vector &getSeenChannels() { return seenChannels; } -const std::vector &getSeenPeers() { return seenPeers; } +const std::vector &getSeenChannels() +{ + return seenChannels; +} +const std::vector &getSeenPeers() +{ + return seenPeers; +} -static int centerYForRow(int y, int size) { - int midY = y + (FONT_HEIGHT_SMALL / 2); - return midY - (size / 2); +static int centerYForRow(int y, int size) +{ + int midY = y + (FONT_HEIGHT_SMALL / 2); + return midY - (size / 2); } // Helpers for drawing status marks (thickened strokes) -static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) { - int topY = centerYForRow(y, size); - display->setColor(WHITE); - display->drawLine(x, topY + size / 2, x + size / 3, topY + size); - display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); - display->drawLine(x + size / 3, topY + size, x + size, topY); - display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); +static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) +{ + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY + size / 2, x + size / 3, topY + size); + display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); + display->drawLine(x + size / 3, topY + size, x + size, topY); + display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); } -static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) { - int topY = centerYForRow(y, size); - display->setColor(WHITE); - display->drawLine(x, topY, x + size, topY + size); - display->drawLine(x, topY + 1, x + size, topY + size + 1); - display->drawLine(x + size, topY, x, topY + size); - display->drawLine(x + size, topY + 1, x, topY + size + 1); +static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) +{ + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY, x + size, topY + size); + display->drawLine(x, topY + 1, x + size, topY + size + 1); + display->drawLine(x + size, topY, x, topY + size); + display->drawLine(x + size, topY + 1, x, topY + size + 1); } -static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) { - int r = size / 2; - int centerY = centerYForRow(y, size) + r; - int centerX = x + r; - display->setColor(WHITE); - display->drawCircle(centerX, centerY, r); - display->drawLine(centerX, centerY - 2, centerX, centerY); - display->setPixel(centerX, centerY + 2); - display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); +static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) +{ + int r = size / 2; + int centerY = centerYForRow(y, size) + r; + int centerX = x + r; + display->setColor(WHITE); + display->drawCircle(centerX, centerY, r); + display->drawLine(centerX, centerY - 2, centerX, centerY); + display->setPixel(centerX, centerY + 2); + display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); } -static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) { - std::string normalized = normalizeEmoji(line); - int totalWidth = 0; +static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) +{ + std::string normalized = normalizeEmoji(line); + int totalWidth = 0; - size_t i = 0; - while (i < normalized.length()) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { - totalWidth += emotes[e].width + 1; // +1 spacing - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - size_t charLen = utf8CharLen(static_cast(normalized[i])); + size_t i = 0; + while (i < normalized.length()) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { + totalWidth += emotes[e].width + 1; // +1 spacing + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + size_t charLen = utf8CharLen(static_cast(normalized[i])); #if defined(OLED_UA) || defined(OLED_RU) - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); + totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); #else - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); + totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); #endif - i += charLen; + i += charLen; + } } - } - return totalWidth; + return totalWidth; } -static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) { - if (totalHeight <= visibleHeight) - return; // no scrollbar needed +static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) +{ + if (totalHeight <= visibleHeight) + return; // no scrollbar needed - int scrollbarX = display->getWidth() - 2; - int scrollbarHeight = visibleHeight; - int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); - int maxScroll = std::max(1, totalHeight - visibleHeight); - int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = visibleHeight; + int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); + int maxScroll = std::max(1, totalHeight - visibleHeight); + int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; - for (int i = 0; i < thumbHeight; i++) { - display->setPixel(scrollbarX, thumbY + i); - } + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } } -void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // Ensure any boot-relative timestamps are upgraded if RTC is valid - messageStore.upgradeBootRelativeTimestamps(); +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Ensure any boot-relative timestamps are upgraded if RTC is valid + messageStore.upgradeBootRelativeTimestamps(); - if (!didReset) { - resetScrollState(); - didReset = true; - } + if (!didReset) { + resetScrollState(); + didReset = true; + } - // Clear the unread message indicator when viewing the message - hasUnreadMessage = false; + // Clear the unread message indicator when viewing the message + hasUnreadMessage = false; - // Filter messages based on thread mode - std::deque filtered; - for (const auto &m : messageStore.getLiveMessages()) { - bool include = false; + // Filter messages based on thread mode + std::deque filtered; + for (const auto &m : messageStore.getLiveMessages()) { + bool include = false; + switch (currentMode) { + case ThreadMode::ALL: + include = true; + break; + case ThreadMode::CHANNEL: + if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) + include = true; + break; + case ThreadMode::DIRECT: + if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) + include = true; + break; + } + if (include) + filtered.push_back(m); + } + + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + const int navHeight = FONT_HEIGHT_SMALL; + const int scrollBottom = SCREEN_HEIGHT - navHeight; + const int usableHeight = scrollBottom; + constexpr int LEFT_MARGIN = 2; + constexpr int RIGHT_MARGIN = 2; + constexpr int SCROLLBAR_WIDTH = 3; + + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; + + const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; + + // Title string depending on mode + static char titleBuf[32]; + const char *titleStr = "Messages"; switch (currentMode) { case ThreadMode::ALL: - include = true; - break; - case ThreadMode::CHANNEL: - if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) - include = true; - break; - case ThreadMode::DIRECT: - if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) - include = true; - break; - } - if (include) - filtered.push_back(m); - } - - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - const int navHeight = FONT_HEIGHT_SMALL; - const int scrollBottom = SCREEN_HEIGHT - navHeight; - const int usableHeight = scrollBottom; - constexpr int LEFT_MARGIN = 2; - constexpr int RIGHT_MARGIN = 2; - constexpr int SCROLLBAR_WIDTH = 3; - - const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; - - const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; - - // Title string depending on mode - static char titleBuf[32]; - const char *titleStr = "Messages"; - switch (currentMode) { - case ThreadMode::ALL: - titleStr = "Messages"; - break; - case ThreadMode::CHANNEL: { - const char *cname = channels.getName(currentChannel); - if (cname && cname[0]) { - snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); - } else { - snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); - } - titleStr = titleBuf; - break; - } - case ThreadMode::DIRECT: { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); - if (node && node->has_user) { - snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); - } else { - snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); - } - titleStr = titleBuf; - break; - } - } - - if (filtered.empty()) { - // If current conversation is empty go back to ALL view - if (currentMode != ThreadMode::ALL) { - setThreadMode(ThreadMode::ALL); - resetScrollState(); - return; // Next draw will rerun in ALL mode - } - - // Still in ALL mode and no messages at all → show placeholder - graphics::drawCommonHeader(display, x, y, titleStr); - didReset = false; - const char *messageString = "No messages"; - int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); - display->drawString(center_text, getTextPositions(display)[2], messageString); - graphics::drawCommonFooter(display, x, y); - return; - } - - // Build lines for filtered messages (newest first) - std::vector allLines; - std::vector isMine; // track alignment - std::vector isHeader; // track header lines - std::vector ackForLine; - - for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { - const auto &m = *it; - - // Channel / destination labeling - char chanType[32] = ""; - if (currentMode == ThreadMode::ALL) { - if (m.dest == NODENUM_BROADCAST) { - snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex)); - } else { - snprintf(chanType, sizeof(chanType), "(DM)"); - } - } - - // Calculate how long ago - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - uint32_t seconds = 0; - bool invalidTime = true; - - if (m.timestamp > 0 && nowSecs > 0) { - if (nowSecs >= m.timestamp) { - seconds = nowSecs - m.timestamp; - invalidTime = (seconds > 315360000); // >10 years - } else { - uint32_t ahead = m.timestamp - nowSecs; - if (ahead <= 600) { // allow small skew - seconds = 0; - invalidTime = false; - } - } - } else if (m.timestamp > 0 && nowSecs == 0) { - // RTC not valid: only trust boot-relative if same boot - uint32_t bootNow = millis() / 1000; - if (m.isBootRelative && m.timestamp <= bootNow) { - seconds = bootNow - m.timestamp; - invalidTime = false; - } else { - invalidTime = true; // old persisted boot-relative, ignore until healed - } - } - - char timeBuf[16]; - if (invalidTime) { - snprintf(timeBuf, sizeof(timeBuf), "???"); - } else if (seconds < 60) { - snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); - } else if (seconds < 3600) { - snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); - } else if (seconds < 86400) { - snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); - } else { - snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); - } - - // Build header line for this message - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); - meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); - - char senderBuf[48] = ""; - if (node && node->has_user) { - // Use long name if present - strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); - senderBuf[sizeof(senderBuf) - 1] = '\0'; - } else { - // No long/short name → show NodeID in parentheses - snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); - } - - // If this is *our own* message, override senderBuf to who the recipient was - bool mine = (m.sender == nodeDB->getNodeNum()); - if (mine && node_recipient && node_recipient->has_user) { - strcpy(senderBuf, node_recipient->user.long_name); - } - - // Shrink Sender name if needed - int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - display->getStringWidth(" @...") - 10; - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(senderBuf); - while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { - senderBuf[strlen(senderBuf) - 1] = '\0'; - } - - // If we actually truncated, append "..." - if (strlen(senderBuf) < origLen) { - strcat(senderBuf, "..."); - } - - // Final header line - char headerStr[96]; - if (mine) { - if (currentMode == ThreadMode::ALL) { - if (strcmp(chanType, "(DM)") == 0) { - snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + titleStr = "Messages"; + break; + case ThreadMode::CHANNEL: { + const char *cname = channels.getName(currentChannel); + if (cname && cname[0]) { + snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); } else { - snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); + snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); } - } else { - snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); - } - } else { - snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + titleStr = titleBuf; + break; + } + case ThreadMode::DIRECT: { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); + if (node && node->has_user) { + snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); + } else { + snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); + } + titleStr = titleBuf; + break; + } } - // Push header line - allLines.push_back(std::string(headerStr)); - isMine.push_back(mine); - isHeader.push_back(true); - ackForLine.push_back(m.ackStatus); + if (filtered.empty()) { + // If current conversation is empty go back to ALL view + if (currentMode != ThreadMode::ALL) { + setThreadMode(ThreadMode::ALL); + resetScrollState(); + return; // Next draw will rerun in ALL mode + } - const char *msgText = MessageStore::getText(m); - - int wrapWidth = mine ? rightTextWidth : leftTextWidth; - std::vector wrapped = generateLines(display, "", msgText, wrapWidth); - for (auto &ln : wrapped) { - allLines.push_back(ln); - isMine.push_back(mine); - isHeader.push_back(false); - ackForLine.push_back(AckStatus::NONE); + // Still in ALL mode and no messages at all → show placeholder + graphics::drawCommonHeader(display, x, y, titleStr); + didReset = false; + const char *messageString = "No messages"; + int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); + display->drawString(center_text, getTextPositions(display)[2], messageString); + graphics::drawCommonFooter(display, x, y); + return; } - } - // Cache lines and heights - cachedLines = allLines; - cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); + // Build lines for filtered messages (newest first) + std::vector allLines; + std::vector isMine; // track alignment + std::vector isHeader; // track header lines + std::vector ackForLine; - // Scrolling logic (unchanged) - int totalHeight = 0; - for (size_t i = 0; i < cachedHeights.size(); ++i) - totalHeight += cachedHeights[i]; - int usableScrollHeight = usableHeight; - int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); + for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { + const auto &m = *it; + + // Channel / destination labeling + char chanType[32] = ""; + if (currentMode == ThreadMode::ALL) { + if (m.dest == NODENUM_BROADCAST) { + snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex)); + } else { + snprintf(chanType, sizeof(chanType), "(DM)"); + } + } + + // Calculate how long ago + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + uint32_t seconds = 0; + bool invalidTime = true; + + if (m.timestamp > 0 && nowSecs > 0) { + if (nowSecs >= m.timestamp) { + seconds = nowSecs - m.timestamp; + invalidTime = (seconds > 315360000); // >10 years + } else { + uint32_t ahead = m.timestamp - nowSecs; + if (ahead <= 600) { // allow small skew + seconds = 0; + invalidTime = false; + } + } + } else if (m.timestamp > 0 && nowSecs == 0) { + // RTC not valid: only trust boot-relative if same boot + uint32_t bootNow = millis() / 1000; + if (m.isBootRelative && m.timestamp <= bootNow) { + seconds = bootNow - m.timestamp; + invalidTime = false; + } else { + invalidTime = true; // old persisted boot-relative, ignore until healed + } + } + + char timeBuf[16]; + if (invalidTime) { + snprintf(timeBuf, sizeof(timeBuf), "???"); + } else if (seconds < 60) { + snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); + } else if (seconds < 3600) { + snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); + } else if (seconds < 86400) { + snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); + } else { + snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); + } + + // Build header line for this message + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); + meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); + + char senderBuf[48] = ""; + if (node && node->has_user) { + // Use long name if present + strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); + senderBuf[sizeof(senderBuf) - 1] = '\0'; + } else { + // No long/short name → show NodeID in parentheses + snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); + } + + // If this is *our own* message, override senderBuf to who the recipient was + bool mine = (m.sender == nodeDB->getNodeNum()); + if (mine && node_recipient && node_recipient->has_user) { + strcpy(senderBuf, node_recipient->user.long_name); + } + + // Shrink Sender name if needed + int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - + display->getStringWidth(" @...") - 10; + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(senderBuf); + while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { + senderBuf[strlen(senderBuf) - 1] = '\0'; + } + + // If we actually truncated, append "..." + if (strlen(senderBuf) < origLen) { + strcat(senderBuf, "..."); + } + + // Final header line + char headerStr[96]; + if (mine) { + if (currentMode == ThreadMode::ALL) { + if (strcmp(chanType, "(DM)") == 0) { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + } else { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + } + + // Push header line + allLines.push_back(std::string(headerStr)); + isMine.push_back(mine); + isHeader.push_back(true); + ackForLine.push_back(m.ackStatus); + + const char *msgText = MessageStore::getText(m); + + int wrapWidth = mine ? rightTextWidth : leftTextWidth; + std::vector wrapped = generateLines(display, "", msgText, wrapWidth); + for (auto &ln : wrapped) { + allLines.push_back(ln); + isMine.push_back(mine); + isHeader.push_back(false); + ackForLine.push_back(AckStatus::NONE); + } + } + + // Cache lines and heights + cachedLines = allLines; + cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); + + // Scrolling logic (unchanged) + int totalHeight = 0; + for (size_t i = 0; i < cachedHeights.size(); ++i) + totalHeight += cachedHeights[i]; + int usableScrollHeight = usableHeight; + int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); #ifndef USE_EINK - uint32_t now = millis(); - float delta = (now - lastTime) / 400.0f; - lastTime = now; - const float scrollSpeed = 2.0f; + uint32_t now = millis(); + float delta = (now - lastTime) / 400.0f; + lastTime = now; + const float scrollSpeed = 2.0f; - if (scrollStartDelay == 0) - scrollStartDelay = now; - if (!scrollStarted && now - scrollStartDelay > 2000) - scrollStarted = true; + if (scrollStartDelay == 0) + scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) + scrollStarted = true; - if (!manualScrolling && totalHeight > usableScrollHeight) { - if (scrollStarted) { - if (!waitingToReset) { - scrollY += delta * scrollSpeed; - if (scrollY >= scrollStop) { - scrollY = scrollStop; - waitingToReset = true; - pauseStart = lastTime; + if (!manualScrolling && totalHeight > usableScrollHeight) { + if (scrollStarted) { + if (!waitingToReset) { + scrollY += delta * scrollSpeed; + if (scrollY >= scrollStop) { + scrollY = scrollStop; + waitingToReset = true; + pauseStart = lastTime; + } + } else if (lastTime - pauseStart > 3000) { + scrollY = 0; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = lastTime; + } } - } else if (lastTime - pauseStart > 3000) { + } else if (!manualScrolling) { scrollY = 0; - waitingToReset = false; - scrollStarted = false; - scrollStartDelay = lastTime; - } } - } else if (!manualScrolling) { - scrollY = 0; - } #else - // E-Ink: disable autoscroll - scrollY = 0.0f; - waitingToReset = false; - scrollStarted = false; - lastTime = millis(); + // E-Ink: disable autoscroll + scrollY = 0.0f; + waitingToReset = false; + scrollStarted = false; + lastTime = millis(); #endif - int finalScroll = (int)scrollY; - int yOffset = -finalScroll + getTextPositions(display)[1]; + int finalScroll = (int)scrollY; + int yOffset = -finalScroll + getTextPositions(display)[1]; - // Render visible lines - for (size_t i = 0; i < cachedLines.size(); ++i) { - int lineY = yOffset; - for (size_t j = 0; j < i; ++j) - lineY += cachedHeights[j]; + // Render visible lines + for (size_t i = 0; i < cachedLines.size(); ++i) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += cachedHeights[j]; - if (lineY > -cachedHeights[i] && lineY < scrollBottom) { - if (isHeader[i]) { + if (lineY > -cachedHeights[i] && lineY < scrollBottom) { + if (isHeader[i]) { - int w = display->getStringWidth(cachedLines[i].c_str()); - int headerX; - if (isMine[i]) { - // push header left to avoid overlap with scrollbar - headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN; - if (headerX < LEFT_MARGIN) - headerX = LEFT_MARGIN; - } else { - headerX = x; + int w = display->getStringWidth(cachedLines[i].c_str()); + int headerX; + if (isMine[i]) { + // push header left to avoid overlap with scrollbar + headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN; + if (headerX < LEFT_MARGIN) + headerX = LEFT_MARGIN; + } else { + headerX = x; + } + display->drawString(headerX, lineY, cachedLines[i].c_str()); + + // Draw ACK/NACK mark for our own messages + if (isMine[i]) { + int markX = headerX - 10; + int markY = lineY; + if (ackForLine[i] == AckStatus::ACKED) { + // Destination ACK + drawCheckMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { + // Failure or timeout + drawXMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::RELAYED) { + // Relay ACK + drawRelayMark(display, markX, markY, 8); + } + // AckStatus::NONE → show nothing + } + + // Draw underline just under header text + int underlineY = lineY + FONT_HEIGHT_SMALL; + for (int px = 0; px < w; ++px) { + display->setPixel(headerX + px, underlineY); + } + } else { + // Render message line + if (isMine[i]) { + // Calculate actual rendered width including emotes + int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN; + if (rightX < LEFT_MARGIN) + rightX = LEFT_MARGIN; + + drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); + } else { + drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); + } + } } - display->drawString(headerX, lineY, cachedLines[i].c_str()); - - // Draw ACK/NACK mark for our own messages - if (isMine[i]) { - int markX = headerX - 10; - int markY = lineY; - if (ackForLine[i] == AckStatus::ACKED) { - // Destination ACK - drawCheckMark(display, markX, markY, 8); - } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { - // Failure or timeout - drawXMark(display, markX, markY, 8); - } else if (ackForLine[i] == AckStatus::RELAYED) { - // Relay ACK - drawRelayMark(display, markX, markY, 8); - } - // AckStatus::NONE → show nothing - } - - // Draw underline just under header text - int underlineY = lineY + FONT_HEIGHT_SMALL; - for (int px = 0; px < w; ++px) { - display->setPixel(headerX + px, underlineY); - } - } else { - // Render message line - if (isMine[i]) { - // Calculate actual rendered width including emotes - int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); - int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN; - if (rightX < LEFT_MARGIN) - rightX = LEFT_MARGIN; - - drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); - } else { - drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); - } - } } - } - int totalContentHeight = totalHeight; - int visibleHeight = usableHeight; + int totalContentHeight = totalHeight; + int visibleHeight = usableHeight; - // Draw scrollbar - drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]); - graphics::drawCommonHeader(display, x, y, titleStr); - graphics::drawCommonFooter(display, x, y); + // Draw scrollbar + drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]); + graphics::drawCommonHeader(display, x, y, titleStr); + graphics::drawCommonFooter(display, x, y); } -std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) { - std::vector lines; +std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) +{ + std::vector lines; - // Only push headerStr if it's not empty (prevents extra blank line after headers) - if (headerStr && headerStr[0] != '\0') { - lines.push_back(std::string(headerStr)); - } - - std::string line, word; - for (int i = 0; messageBuf[i]; ++i) { - char ch = messageBuf[i]; - if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && (unsigned char)messageBuf[i + 2] == 0x99) { - ch = '\''; // plain apostrophe - i += 2; // skip over the extra UTF-8 bytes + // Only push headerStr if it's not empty (prevents extra blank line after headers) + if (headerStr && headerStr[0] != '\0') { + lines.push_back(std::string(headerStr)); } - if (ch == '\n') { - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); - line.clear(); - word.clear(); - } else if (ch == ' ') { - line += word + ' '; - word.clear(); - } else { - word += ch; - std::string test = line + word; + + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { + char ch = messageBuf[i]; + if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && + (unsigned char)messageBuf[i + 2] == 0x99) { + ch = '\''; // plain apostrophe + i += 2; // skip over the extra UTF-8 bytes + } + if (ch == '\n') { + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); + } else { + word += ch; + std::string test = line + word; #if defined(OLED_UA) || defined(OLED_RU) - uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); + uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); #else - uint16_t strWidth = display->getStringWidth(test.c_str()); + uint16_t strWidth = display->getStringWidth(test.c_str()); #endif - if (strWidth > textWidth) { - if (!line.empty()) - lines.push_back(line); - line = word; - word.clear(); - } + if (strWidth > textWidth) { + if (!line.empty()) + lines.push_back(line); + line = word; + word.clear(); + } + } } - } - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); - return lines; + return lines; } -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, const std::vector &isHeaderVec) { - // Tunables for layout control - constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line - constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) - constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines - constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header - constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) - constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec) +{ + // Tunables for layout control + constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line + constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) + constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines + constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header + constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) + constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) - std::vector rowHeights; - rowHeights.reserve(lines.size()); + std::vector rowHeights; + rowHeights.reserve(lines.size()); - for (size_t idx = 0; idx < lines.size(); ++idx) { - const auto &line = lines[idx]; - const int baseHeight = FONT_HEIGHT_SMALL; + for (size_t idx = 0; idx < lines.size(); ++idx) { + const auto &line = lines[idx]; + const int baseHeight = FONT_HEIGHT_SMALL; - // Detect if THIS line or NEXT line contains an emote - bool hasEmote = false; - int tallestEmote = baseHeight; - for (int i = 0; i < numEmotes; ++i) { - if (line.find(emotes[i].label) != std::string::npos) { - hasEmote = true; - tallestEmote = std::max(tallestEmote, emotes[i].height); - } - } - - bool nextHasEmote = false; - if (idx + 1 < lines.size()) { - for (int i = 0; i < numEmotes; ++i) { - if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { - nextHasEmote = true; - break; + // Detect if THIS line or NEXT line contains an emote + bool hasEmote = false; + int tallestEmote = baseHeight; + for (int i = 0; i < numEmotes; ++i) { + if (line.find(emotes[i].label) != std::string::npos) { + hasEmote = true; + tallestEmote = std::max(tallestEmote, emotes[i].height); + } } - } - } - int lineHeight = baseHeight; - - if (isHeaderVec[idx]) { - // Header line spacing - lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; - } else { - // Base spacing for normal lines - int desiredBody = baseHeight + BODY_LINE_LEADING; - - if (hasEmote) { - // Emote line: add overshoot + bottom padding - int overshoot = std::max(0, tallestEmote - baseHeight); - lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; - } else { - // Regular line: no emote → standard spacing - lineHeight = desiredBody; - - // If next line has an emote → add top padding *here* - if (nextHasEmote) { - lineHeight += EMOTE_PADDING_ABOVE; + bool nextHasEmote = false; + if (idx + 1 < lines.size()) { + for (int i = 0; i < numEmotes; ++i) { + if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { + nextHasEmote = true; + break; + } + } } - } - // Add block gap if next is a header - if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { - lineHeight += MESSAGE_BLOCK_GAP; - } - } + int lineHeight = baseHeight; - rowHeights.push_back(lineHeight); - } - - return rowHeights; -} - -void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) { - if (packet.from != 0) { - hasUnreadMessage = true; - - // Determine if message belongs to a muted channel - bool isChannelMuted = false; - if (sm.type == MessageType::BROADCAST) { - const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); - if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) - isChannelMuted = true; - } - - // Banner logic - const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); - char longName[48] = "???"; - if (node && node->user.long_name) { - strncpy(longName, node->user.long_name, sizeof(longName) - 1); - longName[sizeof(longName) - 1] = '\0'; - } - int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(longName); - while (longName[0] && display->getStringWidth(longName) > availWidth) { - longName[strlen(longName) - 1] = '\0'; - } - if (strlen(longName) < origLen) { - strcat(longName, "..."); - } - const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); - - char banner[256]; - bool isAlert = false; - - // Check if alert detection is enabled via external notification module - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_bell_buzzer) { - for (size_t i = 0; i < packet.decoded.payload.size && i < 100; i++) { - if (msgRaw[i] == '\x07') { - isAlert = true; - break; - } - } - } - - if (isAlert) { - if (longName && longName[0]) - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); - else - strcpy(banner, "Alert Received"); - } else { - // Skip muted channels unless it's an alert - if (isChannelMuted) - return; - - if (longName && longName[0]) { - if (currentResolution == ScreenResolution::UltraLow) { - strcpy(banner, "New Message"); + if (isHeaderVec[idx]) { + // Header line spacing + lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; } else { - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + // Base spacing for normal lines + int desiredBody = baseHeight + BODY_LINE_LEADING; + + if (hasEmote) { + // Emote line: add overshoot + bottom padding + int overshoot = std::max(0, tallestEmote - baseHeight); + lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; + } else { + // Regular line: no emote → standard spacing + lineHeight = desiredBody; + + // If next line has an emote → add top padding *here* + if (nextHasEmote) { + lineHeight += EMOTE_PADDING_ABOVE; + } + } + + // Add block gap if next is a header + if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { + lineHeight += MESSAGE_BLOCK_GAP; + } } - } else - strcpy(banner, "New Message"); + + rowHeights.push_back(lineHeight); } - // Append context (which channel or DM) so the banner shows where the message arrived - { - char contextBuf[64] = ""; - if (sm.type == MessageType::BROADCAST) { - const char *cname = channels.getName(sm.channelIndex); - if (cname && cname[0]) - snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); - else - snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); - } - - if (contextBuf[0]) { - size_t cur = strlen(banner); - if (cur + 1 < sizeof(banner)) { - if (cur > 0 && banner[cur - 1] != '\n') { - banner[cur] = '\n'; - banner[cur + 1] = '\0'; - cur++; - } - strncat(banner, contextBuf, sizeof(banner) - cur - 1); - } - } - } - - // Shorter banner if already in a conversation (Channel or Direct) - bool inThread = (getThreadMode() != ThreadMode::ALL); - - if (shouldWakeOnReceivedMessage()) { - screen->setOn(true); - } - - screen->showSimpleBanner(banner, inThread ? 1000 : 3000); - } - - // Always focus into the correct conversation thread when a message with real text arrives - const char *msgText = MessageStore::getText(sm); - if (msgText && msgText[0] != '\0') { - setThreadFor(sm, packet); - } - - // Reset scroll for a clean start - resetScrollState(); + return rowHeights; } -void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) { - if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { - setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); - } else { - uint32_t localNode = nodeDB->getNodeNum(); - uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; - setThreadMode(ThreadMode::DIRECT, -1, peer); - } +void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) +{ + if (packet.from != 0) { + hasUnreadMessage = true; + + // Determine if message belongs to a muted channel + bool isChannelMuted = false; + if (sm.type == MessageType::BROADCAST) { + const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); + if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) + isChannelMuted = true; + } + + // Banner logic + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); + char longName[48] = "???"; + if (node && node->user.long_name) { + strncpy(longName, node->user.long_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } + int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(longName); + while (longName[0] && display->getStringWidth(longName) > availWidth) { + longName[strlen(longName) - 1] = '\0'; + } + if (strlen(longName) < origLen) { + strcat(longName, "..."); + } + const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); + + char banner[256]; + bool isAlert = false; + + // Check if alert detection is enabled via external notification module + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_bell_buzzer) { + for (size_t i = 0; i < packet.decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; + } + } + } + + if (isAlert) { + if (longName && longName[0]) + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + else + strcpy(banner, "Alert Received"); + } else { + // Skip muted channels unless it's an alert + if (isChannelMuted) + return; + + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else + strcpy(banner, "New Message"); + } + + // Append context (which channel or DM) so the banner shows where the message arrived + { + char contextBuf[64] = ""; + if (sm.type == MessageType::BROADCAST) { + const char *cname = channels.getName(sm.channelIndex); + if (cname && cname[0]) + snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); + else + snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); + } + + if (contextBuf[0]) { + size_t cur = strlen(banner); + if (cur + 1 < sizeof(banner)) { + if (cur > 0 && banner[cur - 1] != '\n') { + banner[cur] = '\n'; + banner[cur + 1] = '\0'; + cur++; + } + strncat(banner, contextBuf, sizeof(banner) - cur - 1); + } + } + } + + // Shorter banner if already in a conversation (Channel or Direct) + bool inThread = (getThreadMode() != ThreadMode::ALL); + + if (shouldWakeOnReceivedMessage()) { + screen->setOn(true); + } + + screen->showSimpleBanner(banner, inThread ? 1000 : 3000); + } + + // Always focus into the correct conversation thread when a message with real text arrives + const char *msgText = MessageStore::getText(sm); + if (msgText && msgText[0] != '\0') { + setThreadFor(sm, packet); + } + + // Reset scroll for a clean start + resetScrollState(); +} + +void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) +{ + if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { + setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); + } else { + uint32_t localNode = nodeDB->getNodeNum(); + uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; + setThreadMode(ThreadMode::DIRECT, -1, peer); + } } } // namespace MessageRenderer diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index cf27d0c1e..7dec6adec 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -9,8 +9,10 @@ #include #include -namespace graphics { -namespace MessageRenderer { +namespace graphics +{ +namespace MessageRenderer +{ // Thread filter modes enum class ThreadMode { ALL, CHANNEL, DIRECT }; @@ -43,7 +45,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); // Function to calculate heights for each line -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, const std::vector &isHeaderVec); +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec); // Reset scroll state when new messages arrive void resetScrollState(); diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 3315856e9..e10d8c40a 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -13,7 +13,8 @@ #include // Forward declarations for functions defined in Screen.cpp -namespace graphics { +namespace graphics +{ extern bool haveGlyphs(const char *str); } // namespace graphics @@ -23,21 +24,24 @@ extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; #endif -namespace graphics { -namespace NodeListRenderer { +namespace graphics +{ +namespace NodeListRenderer +{ // Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here -void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) { - for (int row = 0; row < height; row++) { - uint8_t rowMask = (1 << row); - for (int col = 0; col < width; col++) { - uint8_t colData = pgm_read_byte(&bitmapXBM[col]); - if (colData & rowMask) { - // Note: rows become X, columns become Y after transpose - display->fillRect(x + row * 2, y + col * 2, 2, 2); - } +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) +{ + for (int row = 0; row < height; row++) { + uint8_t rowMask = (1 << row); + for (int col = 0; col < width; col++) { + uint8_t colData = pgm_read_byte(&bitmapXBM[col]); + if (colData & rowMask) { + // Note: rows become X, columns become Y after transpose + display->fillRect(x + row * 2, y + col * 2, 2, 2); + } + } } - } } // Static variables for dynamic cycling @@ -57,543 +61,575 @@ static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible // ============================= // Scrolling Logic // ============================= -void scrollUp() { - if (scrollIndex > 0) - scrollIndex--; +void scrollUp() +{ + if (scrollIndex > 0) + scrollIndex--; - popupTime = millis(); // show popup + popupTime = millis(); // show popup } -void scrollDown() { - scrollIndex++; - popupTime = millis(); +void scrollDown() +{ + scrollIndex++; + popupTime = millis(); } // ============================= // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { - static char nodeName[25]; // single static buffer we return - nodeName[0] = '\0'; +const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) +{ + static char nodeName[25]; // single static buffer we return + nodeName[0] = '\0'; - auto writeFallbackId = [&] { std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); }; + auto writeFallbackId = [&] { + std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + }; - // 1) Choose target candidate (long vs short) only if present - const char *raw = nullptr; - if (node && node->has_user) { - raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; - } - - // 2) Sanitize (empty if raw is null/empty) - std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - - // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) - if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) { - writeFallbackId(); - } else { - // %.*s ensures null-termination and safe truncation to buffer size - 1 - std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); - } - - // 4) Width-based truncation + ellipsis (long-name mode only) - if (config.display.use_long_node_name && display) { - int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); - if (availWidth < 0) - availWidth = 0; - - const size_t beforeLen = std::strlen(nodeName); - - // Trim from the end until it fits or is empty - size_t len = beforeLen; - while (len && display->getStringWidth(nodeName) > availWidth) { - nodeName[--len] = '\0'; + // 1) Choose target candidate (long vs short) only if present + const char *raw = nullptr; + if (node && node->has_user) { + raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; } - // If truncated, append "..." (respect buffer size) - if (len < beforeLen) { - // Make sure there's room for "..." and '\0' - const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' - const size_t needed = 3; // "..." - if (len > capForText - needed) { - len = capForText - needed; - nodeName[len] = '\0'; - } - std::strcat(nodeName, "..."); - } - } + // 2) Sanitize (empty if raw is null/empty) + std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - return nodeName; + // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) + if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) { + writeFallbackId(); + } else { + // %.*s ensures null-termination and safe truncation to buffer size - 1 + std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); + } + + // 4) Width-based truncation + ellipsis (long-name mode only) + if (config.display.use_long_node_name && display) { + int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); + if (availWidth < 0) + availWidth = 0; + + const size_t beforeLen = std::strlen(nodeName); + + // Trim from the end until it fits or is empty + size_t len = beforeLen; + while (len && display->getStringWidth(nodeName) > availWidth) { + nodeName[--len] = '\0'; + } + + // If truncated, append "..." (respect buffer size) + if (len < beforeLen) { + // Make sure there's room for "..." and '\0' + const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' + const size_t needed = 3; // "..." + if (len > capForText - needed) { + len = capForText - needed; + nodeName[len] = '\0'; + } + std::strcat(nodeName, "..."); + } + } + + return nodeName; } -const char *getCurrentModeTitle_Nodes(int screenWidth) { - switch (currentMode_Nodes) { - case MODE_LAST_HEARD: - return "Last Heard"; - case MODE_HOP_SIGNAL: +const char *getCurrentModeTitle_Nodes(int screenWidth) +{ + switch (currentMode_Nodes) { + case MODE_LAST_HEARD: + return "Last Heard"; + case MODE_HOP_SIGNAL: #ifdef USE_EINK - return "Hops/Sig"; + return "Hops/Sig"; #else - return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; + return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; #endif - default: - return "Nodes"; - } + default: + return "Nodes"; + } } -const char *getCurrentModeTitle_Location(int screenWidth) { - switch (currentMode_Location) { - case MODE_DISTANCE: - return "Distance"; - case MODE_BEARING: - return "Bearings"; - default: - return "Nodes"; - } +const char *getCurrentModeTitle_Location(int screenWidth) +{ + switch (currentMode_Location) { + case MODE_DISTANCE: + return "Distance"; + case MODE_BEARING: + return "Bearings"; + default: + return "Nodes"; + } } // Use dynamic timing based on mode -unsigned long getModeCycleIntervalMs() { return 3000; } - -int calculateMaxScroll(int totalEntries, int visibleRows) { return std::max(0, (totalEntries - 1) / (visibleRows * 2)); } - -void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { - for (int y = yStart; y <= yEnd; y += 2) { - display->setPixel(x, y); - } +unsigned long getModeCycleIntervalMs() +{ + return 3000; } -void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) { - if (totalEntries <= visibleNodeRows * columns) - return; +int calculateMaxScroll(int totalEntries, int visibleRows) +{ + return std::max(0, (totalEntries - 1) / (visibleRows * 2)); +} - int scrollbarX = display->getWidth() - 2; - int scrollbarHeight = display->getHeight() - scrollStartY - 10; - int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); - int perPage = visibleNodeRows * columns; - int maxScroll = std::max(0, (totalEntries - 1) / perPage); - int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) +{ + for (int y = yStart; y <= yEnd; y += 2) { + display->setPixel(x, y); + } +} - for (int i = 0; i < thumbHeight; i++) { - display->setPixel(scrollbarX, thumbY + i); - } +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) +{ + if (totalEntries <= visibleNodeRows * columns) + return; + + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = display->getHeight() - scrollStartY - 10; + int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); + int perPage = visibleNodeRows * columns; + int maxScroll = std::max(0, (totalEntries - 1) / perPage); + int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); + + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } } // ============================= // Entry Renderers // ============================= -void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const char *nodeName = getSafeNodeName(display, node, columnWidth); - char timeStr[10]; - uint32_t seconds = sinceLastSeen(node); - if (seconds == 0 || seconds == UINT32_MAX) { - snprintf(timeStr, sizeof(timeStr), "?"); - } else { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "?"); } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); } - } - int rightEdge = x + columnWidth - timeOffset; - if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time - rightEdge -= 1; - int textWidth = display->getStringWidth(timeStr); - display->drawString(rightEdge - textWidth, y, timeStr); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + int rightEdge = x + columnWidth - timeOffset; + if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time + rightEdge -= 1; + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); } -void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - bool isLeftCol = (x < SCREEN_WIDTH / 2); +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - 25; - int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); - int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + int nameMaxWidth = columnWidth - 25; + int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); - int barsXOffset = columnWidth - barsOffset; + int barsXOffset = columnWidth - barsOffset; - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const char *nodeName = getSafeNodeName(display, node, columnWidth); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } } - } - // Draw signal strength bars - int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; - int barWidth = 2; - int barStartX = x + barsXOffset; - int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + // Draw signal strength bars + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; - for (int b = 0; b < 4; b++) { - if (b < bars) { - int height = (b * 2); - display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + } } - } - // Draw hop count - char hopStr[6] = ""; - if (node->has_hops_away && node->hops_away > 0) - snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + // Draw hop count + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); - if (hopStr[0] != '\0') { - int rightEdge = x + columnWidth - hopOffset; - int textWidth = display->getStringWidth(hopStr); - display->drawString(rightEdge - textWidth, y, hopStr); - } + if (hopStr[0] != '\0') { + int rightEdge = x + columnWidth - hopOffset; + int textWidth = display->getStringWidth(hopStr); + display->drawString(rightEdge - textWidth, y, hopStr); + } } -void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = + columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node, columnWidth); - char distStr[10] = ""; + const char *nodeName = getSafeNodeName(display, node, columnWidth); + char distStr[10] = ""; - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet < 1000) - snprintf(distStr, sizeof(distStr), "%dft", feet); - else - snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max - } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles < 1000) - snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); - else - snprintf(distStr, sizeof(distStr), "999"); // Max display cap - } - } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters < 1000) - snprintf(distStr, sizeof(distStr), "%dm", meters); - else - snprintf(distStr, sizeof(distStr), "1k"); - } else { - int km = (int)(distanceKm + 0.5); - if (km < 1000) - snprintf(distStr, sizeof(distStr), "%dk", km); - else - snprintf(distStr, sizeof(distStr), "999"); - } + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet < 1000) + snprintf(distStr, sizeof(distStr), "%dft", feet); + else + snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles < 1000) + snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); + else + snprintf(distStr, sizeof(distStr), "999"); // Max display cap + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters < 1000) + snprintf(distStr, sizeof(distStr), "%dm", meters); + else + snprintf(distStr, sizeof(distStr), "1k"); + } else { + int km = (int)(distanceKm + 0.5); + if (km < 1000) + snprintf(distStr, sizeof(distStr), "%dk", km); + else + snprintf(distStr, sizeof(distStr), "999"); + } + } } - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } } - } - if (strlen(distStr) > 0) { - int offset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) - int rightEdge = x + columnWidth - offset; - int textWidth = display->getStringWidth(distStr); - display->drawString(rightEdge - textWidth, y, distStr); - } + if (strlen(distStr) > 0) { + int offset = (currentResolution == ScreenResolution::High) + ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distStr); + display->drawString(rightEdge - textWidth, y, distStr); + } } -void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - switch (currentMode_Nodes) { - case MODE_LAST_HEARD: - drawEntryLastHeard(display, node, x, y, columnWidth); - break; - case MODE_HOP_SIGNAL: - drawEntryHopSignal(display, node, x, y, columnWidth); - break; - default: - break; - } -} - -void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - bool isLeftCol = (x < SCREEN_WIDTH / 2); - - // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - - const char *nodeName = getSafeNodeName(display, node, columnWidth); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (currentResolution == ScreenResolution::High) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + switch (currentMode_Nodes) { + case MODE_LAST_HEARD: + drawEntryLastHeard(display, node, x, y, columnWidth); + break; + case MODE_HOP_SIGNAL: + drawEntryHopSignal(display, node, x, y, columnWidth); + break; + default: + break; } - } } -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, - double userLon) { - if (!nodeDB->hasValidPosition(node)) - return; +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + // Adjust max text width depending on column and screen width + int nameMaxWidth = + columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - int centerX = x + columnWidth - arrowXOffset; - int centerY = y + FONT_HEIGHT_SMALL / 2; + const char *nodeName = getSafeNodeName(display, node, columnWidth); - double nodeLat = node->position.latitude_i * 1e-7; - double nodeLon = node->position.longitude_i * 1e-7; - float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); - float bearingToNode = RAD_TO_DEG * bearing; - float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); - // Shrink size by 2px - int size = FONT_HEIGHT_SMALL - 5; - CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); - /* - float angle = relativeBearing * DEG_TO_RAD; - float halfSize = size / 2.0; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (currentResolution == ScreenResolution::High) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } +} - // Point of the arrow - int tipX = centerX + halfSize * cos(angle); - int tipY = centerY - halfSize * sin(angle); +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon) +{ + if (!nodeDB->hasValidPosition(node)) + return; - float baseAngle = radians(35); - float sideLen = halfSize * 0.95; - float notchInset = halfSize * 0.35; + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); - // Left and right corners - int leftX = centerX + sideLen * cos(angle + PI - baseAngle); - int leftY = centerY - sideLen * sin(angle + PI - baseAngle); + int centerX = x + columnWidth - arrowXOffset; + int centerY = y + FONT_HEIGHT_SMALL / 2; - int rightX = centerX + sideLen * cos(angle + PI + baseAngle); - int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); + float bearingToNode = RAD_TO_DEG * bearing; + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + // Shrink size by 2px + int size = FONT_HEIGHT_SMALL - 5; + CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); + /* + float angle = relativeBearing * DEG_TO_RAD; + float halfSize = size / 2.0; - // Center notch (cut-in) - int notchX = centerX - notchInset * cos(angle); - int notchY = centerY + notchInset * sin(angle); + // Point of the arrow + int tipX = centerX + halfSize * cos(angle); + int tipY = centerY - halfSize * sin(angle); - // Draw the chevron-style arrowhead - display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); - display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); - */ + float baseAngle = radians(35); + float sideLen = halfSize * 0.95; + float notchInset = halfSize * 0.35; + + // Left and right corners + int leftX = centerX + sideLen * cos(angle + PI - baseAngle); + int leftY = centerY - sideLen * sin(angle + PI - baseAngle); + + int rightX = centerX + sideLen * cos(angle + PI + baseAngle); + int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + + // Center notch (cut-in) + int notchX = centerX - notchInset * cos(angle); + int notchY = centerY + notchInset * sin(angle); + + // Draw the chevron-style arrowhead + display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); + display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); + */ } // ============================= // Main Screen Functions // ============================= -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, - NodeExtrasRenderer extras, float heading, double lat, double lon) { - const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; - bool locationScreen = false; +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) +{ + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + bool locationScreen = false; - if (strcmp(title, "Bearings") == 0) - locationScreen = true; - else if (strcmp(title, "Distance") == 0) - locationScreen = true; - display->clear(); + if (strcmp(title, "Bearings") == 0) + locationScreen = true; + else if (strcmp(title, "Distance") == 0) + locationScreen = true; + display->clear(); - // Draw the battery/time header - graphics::drawCommonHeader(display, x, y, title); + // Draw the battery/time header + graphics::drawCommonHeader(display, x, y, title); - // Space below header - y += COMMON_HEADER_HEIGHT; + // Space below header + y += COMMON_HEADER_HEIGHT; - int totalColumns = 1; // Default to 1 column + int totalColumns = 1; // Default to 1 column - if (config.display.use_long_node_name) { - if (SCREEN_WIDTH <= 240) { - totalColumns = 1; - } else if (SCREEN_WIDTH > 240) { - totalColumns = 2; - } - } else { - if (SCREEN_WIDTH <= 64) { - totalColumns = 1; - } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { - totalColumns = 2; + if (config.display.use_long_node_name) { + if (SCREEN_WIDTH <= 240) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 240) { + totalColumns = 2; + } } else { - totalColumns = 3; + if (SCREEN_WIDTH <= 64) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { + totalColumns = 2; + } else { + totalColumns = 3; + } } - } - int columnWidth = display->getWidth() / totalColumns; + int columnWidth = display->getWidth() / totalColumns; - int totalEntries = nodeDB->getNumMeshNodes(); - int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; - int numskipped = 0; - int visibleNodeRows = totalRowsAvailable; + int totalEntries = nodeDB->getNumMeshNodes(); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + int numskipped = 0; + int visibleNodeRows = totalRowsAvailable; - // Build filtered + ordered list - std::vector drawList; - drawList.reserve(totalEntries); - for (int i = 0; i < totalEntries; i++) { - auto *n = nodeDB->getMeshNodeByIndex(i); + // Build filtered + ordered list + std::vector drawList; + drawList.reserve(totalEntries); + for (int i = 0; i < totalEntries; i++) { + auto *n = nodeDB->getMeshNodeByIndex(i); - if (!n) - continue; - if (n->num == nodeDB->getNodeNum()) - continue; - if (locationScreen && !n->has_position) - continue; + if (!n) + continue; + if (n->num == nodeDB->getNodeNum()) + continue; + if (locationScreen && !n->has_position) + continue; - drawList.push_back(n->num); - } - totalEntries = drawList.size(); - int perPage = visibleNodeRows * totalColumns; - - int maxScroll = 0; - if (perPage > 0) { - maxScroll = std::max(0, (totalEntries - 1) / perPage); - } - - if (scrollIndex > maxScroll) - scrollIndex = maxScroll; - int startIndex = scrollIndex * visibleNodeRows * totalColumns; - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); - int yOffset = 0; - int col = 0; - int lastNodeY = y; - int shownCount = 0; - int rowCount = 0; - - for (int idx = startIndex; idx < endIndex; idx++) { - uint32_t nodeNum = drawList[idx]; - auto *node = nodeDB->getMeshNode(nodeNum); - int xPos = x + (col * columnWidth); - int yPos = y + yOffset; - - renderer(display, node, xPos, yPos, columnWidth); - - if (extras) - extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); - - lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); - yOffset += rowYOffset; - shownCount++; - rowCount++; - - if (rowCount >= totalRowsAvailable) { - yOffset = 0; - rowCount = 0; - col++; - if (col > (totalColumns - 1)) - break; + drawList.push_back(n->num); } - } - - // This should correct the scrollbar - totalEntries -= numskipped; - - // Draw column separator - if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { - const int firstNodeY = y + 3; - for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { - drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); - } - } - - const int scrollStartY = y + 3; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); - graphics::drawCommonFooter(display, x, y); - - // Scroll Popup Overlay - if (millis() - popupTime < POPUP_DURATION_MS) { - popupTotal = totalEntries; - + totalEntries = drawList.size(); int perPage = visibleNodeRows * totalColumns; - popupStart = startIndex + 1; - popupEnd = std::min(startIndex + perPage, totalEntries); + int maxScroll = 0; + if (perPage > 0) { + maxScroll = std::max(0, (totalEntries - 1) / perPage); + } - popupPage = (scrollIndex + 1); - popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + if (scrollIndex > maxScroll) + scrollIndex = maxScroll; + int startIndex = scrollIndex * visibleNodeRows * totalColumns; + int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + int yOffset = 0; + int col = 0; + int lastNodeY = y; + int shownCount = 0; + int rowCount = 0; - char buf[32]; - snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); + for (int idx = startIndex; idx < endIndex; idx++) { + uint32_t nodeNum = drawList[idx]; + auto *node = nodeDB->getMeshNode(nodeNum); + int xPos = x + (col * columnWidth); + int yPos = y + yOffset; - display->setTextAlignment(TEXT_ALIGN_LEFT); + renderer(display, node, xPos, yPos, columnWidth); - // Box padding - int padding = 2; - int textW = display->getStringWidth(buf); - int textH = FONT_HEIGHT_SMALL; - int boxWidth = textW + padding * 3; - int boxHeight = textH + padding * 2; + if (extras) + extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); - // Center of usable screen area: - int headerHeight = FONT_HEIGHT_SMALL - 1; - int footerHeight = FONT_HEIGHT_SMALL + 2; + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; + shownCount++; + rowCount++; - int usableTop = headerHeight; - int usableBottom = display->getHeight() - footerHeight; - int usableHeight = usableBottom - usableTop; + if (rowCount >= totalRowsAvailable) { + yOffset = 0; + rowCount = 0; + col++; + if (col > (totalColumns - 1)) + break; + } + } - // Center point inside usable area - int boxLeft = (display->getWidth() - boxWidth) / 2; - int boxTop = usableTop + (usableHeight - boxHeight) / 2; + // This should correct the scrollbar + totalEntries -= numskipped; - // Draw Box - display->setColor(BLACK); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); - display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); - display->setColor(WHITE); + // Draw column separator + if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { + const int firstNodeY = y + 3; + for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { + drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); + } + } - // Text - display->drawString(boxLeft + padding, boxTop + padding, buf); - } + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); + graphics::drawCommonFooter(display, x, y); + + // Scroll Popup Overlay + if (millis() - popupTime < POPUP_DURATION_MS) { + popupTotal = totalEntries; + + int perPage = visibleNodeRows * totalColumns; + + popupStart = startIndex + 1; + popupEnd = std::min(startIndex + perPage, totalEntries); + + popupPage = (scrollIndex + 1); + popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + + char buf[32]; + snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Box padding + int padding = 2; + int textW = display->getStringWidth(buf); + int textH = FONT_HEIGHT_SMALL; + int boxWidth = textW + padding * 3; + int boxHeight = textH + padding * 2; + + // Center of usable screen area: + int headerHeight = FONT_HEIGHT_SMALL - 1; + int footerHeight = FONT_HEIGHT_SMALL + 2; + + int usableTop = headerHeight; + int usableBottom = display->getHeight() - footerHeight; + int usableHeight = usableBottom - usableTop; + + // Center point inside usable area + int boxLeft = (display->getWidth() - boxWidth) / 2; + int boxTop = usableTop + (usableHeight - boxHeight) / 2; + + // Draw Box + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); + + // Text + display->drawString(boxLeft + padding, boxTop + padding, buf); + } } // ============================= @@ -602,154 +638,161 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t #ifndef USE_EINK // Node list for Last Heard and Hop Signal views -void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // Static variables to track mode and duration - static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; - static unsigned long modeStartTime = 0; +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; + static unsigned long modeStartTime = 0; - unsigned long now = millis(); + unsigned long now = millis(); #if defined(M5STACK_UNITC6L) - display->clear(); - if (now - lastSwitchTime >= 3000) { - display->display(); - lastSwitchTime = now; - } + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } #endif - // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT_NODE) { - currentMode_Nodes = MODE_LAST_HEARD; - modeStartTime = now; - } + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_NODE) { + currentMode_Nodes = MODE_LAST_HEARD; + modeStartTime = now; + } - // Time to switch to next mode? - if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); - modeStartTime = now; - } + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); + modeStartTime = now; + } - // Render screen based on currentMode - const char *title = getCurrentModeTitle_Nodes(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Nodes(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); - // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode_Nodes; + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Nodes; } // Node list for Distance and Bearings views -void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // Static variables to track mode and duration - static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; - static unsigned long modeStartTime = 0; +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; + static unsigned long modeStartTime = 0; - unsigned long now = millis(); + unsigned long now = millis(); #if defined(M5STACK_UNITC6L) - display->clear(); - if (now - lastSwitchTime >= 3000) { - display->display(); - lastSwitchTime = now; - } -#endif - // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT_LOCATION) { - currentMode_Location = MODE_DISTANCE; - modeStartTime = now; - } - - // Time to switch to next mode? - if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); - modeStartTime = now; - } - - // Render screen based on currentMode - const char *title = getCurrentModeTitle_Location(display->getWidth()); - - // Render screen based on currentMode_Location - if (currentMode_Location == MODE_DISTANCE) { - drawNodeListScreen(display, state, x, y, title, drawNodeDistance); - } else if (currentMode_Location == MODE_BEARING) { - drawNodeListWithCompasses(display, state, x, y); - } - - // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode_Location; -} -#endif - -#ifdef USE_EINK -void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - const char *title = "Last Heard"; - drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); -} - -void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { -#ifdef USE_EINK - const char *title = "Hops/Sig"; -#else - - const char *title = "Hops/Signal"; -#endif - drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); -} -void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - const char *title = "Distance"; - drawNodeListScreen(display, state, x, y, title, drawNodeDistance); -} -#endif -void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - float heading = 0; - bool validHeading = false; - auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - double lat = DegD(ourNode->position.latitude_i); - double lon = DegD(ourNode->position.longitude_i); - -#if defined(M5STACK_UNITC6L) - display->clear(); - uint32_t now = millis(); - if (now - lastSwitchTime >= 2000) { - display->display(); - lastSwitchTime = now; - } -#endif - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { -#if HAS_GPS - if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees - validHeading = true; - } else { - heading = screen->estimatedHeading(lat, lon); - validHeading = !isnan(heading); + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; } #endif + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_LOCATION) { + currentMode_Location = MODE_DISTANCE; + modeStartTime = now; + } - if (!validHeading) - return; - } - drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Location(display->getWidth()); + + // Render screen based on currentMode_Location + if (currentMode_Location == MODE_DISTANCE) { + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); + } else if (currentMode_Location == MODE_BEARING) { + drawNodeListWithCompasses(display, state, x, y); + } + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Location; +} +#endif + +#ifdef USE_EINK +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Last Heard"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +} + +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#ifdef USE_EINK + const char *title = "Hops/Sig"; +#else + + const char *title = "Hops/Signal"; +#endif + drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); +} +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Distance"; + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); +} +#endif +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + float heading = 0; + bool validHeading = false; + auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + double lat = DegD(ourNode->position.latitude_i); + double lon = DegD(ourNode->position.longitude_i); + +#if defined(M5STACK_UNITC6L) + display->clear(); + uint32_t now = millis(); + if (now - lastSwitchTime >= 2000) { + display->display(); + lastSwitchTime = now; + } +#endif + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { +#if HAS_GPS + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } +#endif + + if (!validHeading) + return; + } + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } /// Draw a series of fields in a column, wrapping to multiple columns if needed -void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) { - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +{ + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - const char **f = fields; - int xo = x, yo = y; - while (*f) { - display->drawString(xo, yo, *f); - if ((display->getColor() == BLACK) && config.display.heading_bold) - display->drawString(xo + 1, yo, *f); + const char **f = fields; + int xo = x, yo = y; + while (*f) { + display->drawString(xo, yo, *f); + if ((display->getColor() == BLACK) && config.display.heading_bold) + display->drawString(xo + 1, yo, *f); - display->setColor(WHITE); - yo += FONT_HEIGHT_SMALL; - if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { - xo += SCREEN_WIDTH / 2; - yo = 0; + display->setColor(WHITE); + yo += FONT_HEIGHT_SMALL; + if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { + xo += SCREEN_WIDTH / 2; + yo = 0; + } + f++; } - f++; - } } } // namespace NodeListRenderer diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index 612e9c627..e212c031b 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -5,7 +5,8 @@ #include #include -namespace graphics { +namespace graphics +{ /// Forward declarations class Screen; @@ -16,7 +17,8 @@ class Screen; * Contains all functions related to drawing node lists and individual node entries * including last heard, hop signal, distance, and compass views. */ -namespace NodeListRenderer { +namespace NodeListRenderer +{ // Entry renderer function types typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); @@ -28,8 +30,9 @@ enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 }; // Main node list screen function -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, - NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, double lon = 0); +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, + double lon = 0); // Entry renderers void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); @@ -39,8 +42,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, - double userLon); +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon); // Screen frame functions void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index aaca9b988..8d76b4592 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -36,7 +36,8 @@ extern std::vector functionSymbol; extern std::string functionSymbolString; extern bool hasUnreadMessage; -namespace graphics { +namespace graphics +{ int bannerSignalBars = -1; InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; @@ -53,711 +54,730 @@ uint32_t NotificationRenderer::currentNumber = 0; VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; std::function NotificationRenderer::textInputCallback = nullptr; -uint32_t pow_of_10(uint32_t n) { - uint32_t ret = 1; - for (uint32_t i = 0; i < n; i++) { - ret *= 10; - } - return ret; +uint32_t pow_of_10(uint32_t n) +{ + uint32_t ret = 1; + for (uint32_t i = 0; i < n; i++) { + ret *= 10; + } + return ret; } // Used on boot when a certificate is being created -void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(64 + x, y, "Creating SSL certificate"); +void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(64 + x, y, "Creating SSL certificate"); #ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); + yield(); + esp_task_wdt_reset(); #endif - display->setFont(FONT_SMALL); - if ((millis() / 1000) % 2) { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); - } else { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); - } -} - -void NotificationRenderer::resetBanner() { - notificationTypeEnum previousType = current_notification_type; - - alertBannerMessage[0] = '\0'; - current_notification_type = notificationTypeEnum::none; - - OnScreenKeyboardModule::instance().clearPopup(); - - inEvent.inputEvent = INPUT_BROKER_NONE; - inEvent.kbchar = 0; - curSelected = 0; - alertBannerOptions = 0; // last x lines are seelctable options - optionsArrayPtr = nullptr; - optionsEnumPtr = nullptr; - alertBannerCallback = NULL; - pauseBanner = false; - numDigits = 0; - currentNumber = 0; - - nodeDB->pause_sort(false); - - // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update - // to ensure any messages received during keyboard use are now displayed - if (previousType == notificationTypeEnum::text_input && screen) { - OnScreenKeyboardModule::instance().stop(false); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } -} - -void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - // Handle text_input notifications first - they have their own timeout/banner logic - if (current_notification_type == notificationTypeEnum::text_input) { - // Check for timeout and reset if needed for text input - if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); - return; - } - drawTextInput(display, state); - return; - } - - if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); - } - - // Exit if no banner is showing or banner is paused - if (!isOverlayBannerShowing() || pauseBanner) { - return; - } - - switch (current_notification_type) { - case notificationTypeEnum::none: - // Do nothing - no notification to display - break; - case notificationTypeEnum::text_input: - // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. - break; - case notificationTypeEnum::text_banner: - case notificationTypeEnum::selection_picker: - drawAlertBannerOverlay(display, state); - break; - case notificationTypeEnum::node_picker: - drawNodePicker(display, state); - break; - case notificationTypeEnum::number_picker: - drawNumberPicker(display, state); - break; - } -} - -void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) { - const char *lineStarts[MAX_LINES + 1] = {0}; - uint16_t lineCount = 0; - - // Parse lines - char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); - lineStarts[lineCount] = alertBannerMessage; - - // Find lines - while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); - if (lineStarts[lineCount + 1][0] == '\n') - lineStarts[lineCount + 1] += 1; - lineCount++; - } - // modulo to extract - uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); - // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - if (this_digit == 9) { - currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); + display->setFont(FONT_SMALL); + if ((millis() / 1000) % 2) { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); } else { - currentNumber += (pow_of_10(numDigits - curSelected - 1)); + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); } - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || - inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - if (this_digit == 0) { - currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); - } else { - currentNumber -= (pow_of_10(numDigits - curSelected - 1)); - } - } else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) { - if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit - currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1)); - currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1)); - curSelected++; - } - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) { - curSelected++; - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - curSelected--; - } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { - resetBanner(); - return; - } - if (curSelected == static_cast(numDigits)) { - alertBannerCallback(currentNumber); - resetBanner(); - return; - } - - inEvent.inputEvent = INPUT_BROKER_NONE; - if (alertBannerMessage[0] == '\0') - return; - - uint16_t totalLines = lineCount + 2; - const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation - - // copy the linestarts to display to the linePointers holder - for (uint16_t i = 0; i < lineCount; i++) { - linePointers[i] = lineStarts[i]; - } - std::string digits = " "; - std::string arrowPointer = " "; - for (uint16_t i = 0; i < numDigits; i++) { - // Modulo minus modulo to return just the current number - digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; - if (curSelected == i) { - arrowPointer += "^ "; - } else { - arrowPointer += "_ "; - } - } - - linePointers[lineCount++] = digits.c_str(); - linePointers[lineCount++] = arrowPointer.c_str(); - - drawNotificationBox(display, state, linePointers, totalLines, 0); } -void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) { - static uint32_t selectedNodenum = 0; +void NotificationRenderer::resetBanner() +{ + notificationTypeEnum previousType = current_notification_type; - // === Layout Configuration === - constexpr uint16_t vPadding = 2; - alertBannerOptions = nodeDB->getNumMeshNodes() - 1; + alertBannerMessage[0] = '\0'; + current_notification_type = notificationTypeEnum::none; - // let the box drawing function calculate the widths? + OnScreenKeyboardModule::instance().clearPopup(); - const char *lineStarts[MAX_LINES + 1] = {0}; - uint16_t lineCount = 0; - - // Parse lines - char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); - lineStarts[lineCount] = alertBannerMessage; - - while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); - if (lineStarts[lineCount + 1][0] == '\n') - lineStarts[lineCount + 1] += 1; - lineCount++; - } - - // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || - inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || - inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - curSelected++; - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - alertBannerCallback(selectedNodenum); - resetBanner(); - return; - } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { - resetBanner(); - return; - } - - if (curSelected == -1) - curSelected = alertBannerOptions - 1; - if (curSelected == alertBannerOptions) + inEvent.inputEvent = INPUT_BROKER_NONE; + inEvent.kbchar = 0; curSelected = 0; + alertBannerOptions = 0; // last x lines are seelctable options + optionsArrayPtr = nullptr; + optionsEnumPtr = nullptr; + alertBannerCallback = NULL; + pauseBanner = false; + numDigits = 0; + currentNumber = 0; - inEvent.inputEvent = INPUT_BROKER_NONE; - if (alertBannerMessage[0] == '\0') - return; + nodeDB->pause_sort(false); - uint16_t totalLines = lineCount + alertBannerOptions; - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint8_t linesShown = lineCount; - const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation - - // copy the linestarts to display to the linePointers holder - for (int i = 0; i < lineCount; i++) { - linePointers[i] = lineStarts[i]; - } - char scratchLineBuffer[visibleTotalLines - lineCount][40]; - - uint8_t firstOptionToShow = 0; - if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { - if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) - firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; - else - firstOptionToShow = curSelected - 1; - } else { - firstOptionToShow = 0; - } - int scratchLineNum = 0; - for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - char temp_name[16] = {0}; - if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { - std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); - strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); - } else { - snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update + // to ensure any messages received during keyboard use are now displayed + if (previousType == notificationTypeEnum::text_input && screen) { + OnScreenKeyboardModule::instance().stop(false); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } - if (i == curSelected) { - selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; - if (currentResolution == ScreenResolution::High) { - strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); - strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); - } else { - strncpy(scratchLineBuffer[scratchLineNum], ">", 2); - strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); - } - scratchLineBuffer[scratchLineNum][39] = '\0'; - } else { - strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); - scratchLineBuffer[scratchLineNum][39] = '\0'; - } - linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; - } - drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); } -void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { - // === Layout Configuration === - constexpr uint16_t vPadding = 2; +void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // Handle text_input notifications first - they have their own timeout/banner logic + if (current_notification_type == notificationTypeEnum::text_input) { + // Check for timeout and reset if needed for text input + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + return; + } + drawTextInput(display, state); + return; + } - uint16_t optionWidths[alertBannerOptions] = {0}; - uint16_t maxWidth = 0; - uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); - uint16_t lineWidths[MAX_LINES] = {0}; - uint16_t lineLengths[MAX_LINES] = {0}; - const char *lineStarts[MAX_LINES + 1] = {0}; - uint16_t lineCount = 0; - char lineBuffer[40] = {0}; + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + } - // Parse lines - char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); - lineStarts[lineCount] = alertBannerMessage; + // Exit if no banner is showing or banner is paused + if (!isOverlayBannerShowing() || pauseBanner) { + return; + } - while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); - lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; - if (lineStarts[lineCount + 1][0] == '\n') - lineStarts[lineCount + 1] += 1; - lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); - if (lineWidths[lineCount] > maxWidth) - maxWidth = lineWidths[lineCount]; - lineCount++; - } + switch (current_notification_type) { + case notificationTypeEnum::none: + // Do nothing - no notification to display + break; + case notificationTypeEnum::text_input: + // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. + break; + case notificationTypeEnum::text_banner: + case notificationTypeEnum::selection_picker: + drawAlertBannerOverlay(display, state); + break; + case notificationTypeEnum::node_picker: + drawNodePicker(display, state); + break; + case notificationTypeEnum::number_picker: + drawNumberPicker(display, state); + break; + } +} - // Measure option widths - for (int i = 0; i < alertBannerOptions; i++) { - optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); - if (optionWidths[i] > maxWidth) - maxWidth = optionWidths[i]; - if (optionWidths[i] + arrowsWidth > maxWidth) - maxWidth = optionWidths[i] + arrowsWidth; - } +void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; - // Handle input - if (alertBannerOptions > 0) { - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + // Find lines + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + // modulo to extract + uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); + // Handle input + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || + if (this_digit == 9) { + currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber += (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - curSelected++; + if (this_digit == 0) { + currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); + } else { + currentNumber -= (pow_of_10(numDigits - curSelected - 1)); + } + } else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) { + if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit + currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1)); + currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1)); + curSelected++; + } + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { + curSelected--; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && + alertBannerUntil != 0) { + resetBanner(); + return; + } + if (curSelected == static_cast(numDigits)) { + alertBannerCallback(currentNumber); + resetBanner(); + return; + } + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + 2; + const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (uint16_t i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + std::string digits = " "; + std::string arrowPointer = " "; + for (uint16_t i = 0; i < numDigits; i++) { + // Modulo minus modulo to return just the current number + digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; + if (curSelected == i) { + arrowPointer += "^ "; + } else { + arrowPointer += "_ "; + } + } + + linePointers[lineCount++] = digits.c_str(); + linePointers[lineCount++] = arrowPointer.c_str(); + + drawNotificationBox(display, state, linePointers, totalLines, 0); +} + +void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + static uint32_t selectedNodenum = 0; + + // === Layout Configuration === + constexpr uint16_t vPadding = 2; + alertBannerOptions = nodeDB->getNumMeshNodes() - 1; + + // let the box drawing function calculate the widths? + + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineCount++; + } + + // Handle input + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { + curSelected--; + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || + inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { + curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - if (optionsEnumPtr != nullptr) { - alertBannerCallback(optionsEnumPtr[curSelected]); - optionsEnumPtr = nullptr; - } else { - alertBannerCallback(curSelected); - } - resetBanner(); - return; - } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { - resetBanner(); - return; + alertBannerCallback(selectedNodenum); + resetBanner(); + return; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && + alertBannerUntil != 0) { + resetBanner(); + return; } if (curSelected == -1) - curSelected = alertBannerOptions - 1; + curSelected = alertBannerOptions - 1; if (curSelected == alertBannerOptions) - curSelected = 0; - } else { - if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_ALT_LONG || inEvent.inputEvent == INPUT_BROKER_CANCEL) { - resetBanner(); - return; + curSelected = 0; + + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; } - } + char scratchLineBuffer[visibleTotalLines - lineCount][40]; - inEvent.inputEvent = INPUT_BROKER_NONE; - if (alertBannerMessage[0] == '\0') - return; - - uint16_t totalLines = lineCount + alertBannerOptions; - - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint8_t linesShown = lineCount; - const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation - - // copy the linestarts to display to the linePointers holder - for (int i = 0; i < lineCount; i++) { - linePointers[i] = lineStarts[i]; - } - - uint8_t firstOptionToShow = 0; - if (alertBannerOptions > 0) { - if (visibleTotalLines - lineCount == 1) { - firstOptionToShow = curSelected; - } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { - if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) - firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; - else - firstOptionToShow = curSelected - 1; + uint8_t firstOptionToShow = 0; + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; } else { - firstOptionToShow = 0; + firstOptionToShow = 0; } - } - // Useful log line for troubleshooting: - /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u", - alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */ - - for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - if (i == curSelected) { - if (currentResolution == ScreenResolution::High) { - strncpy(lineBuffer, "> ", 3); - strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); - strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); - } else { - strncpy(lineBuffer, ">", 2); - strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); - strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); - } - lineBuffer[39] = '\0'; - linePointers[linesShown] = lineBuffer; - } else { - linePointers[linesShown] = optionsArrayPtr[i]; + int scratchLineNum = 0; + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + char temp_name[16] = {0}; + if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { + std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); + strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + } else { + snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + } + if (i == curSelected) { + selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + if (currentResolution == ScreenResolution::High) { + strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + } else { + strncpy(scratchLineBuffer[scratchLineNum], ">", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); + strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + } + scratchLineBuffer[scratchLineNum][39] = '\0'; + } else { + strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); + scratchLineBuffer[scratchLineNum][39] = '\0'; + } + linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; } - } - if (alertBannerOptions > 0) { - drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); - } else { drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); - } } -void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], uint16_t totalLines, - uint8_t firstOptionToShow, uint16_t maxWidth) { +void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // === Layout Configuration === + constexpr uint16_t vPadding = 2; - bool is_picker = false; - uint16_t lineCount = 0; - // Layout Configuration - constexpr uint16_t hPadding = 5; - constexpr uint16_t vPadding = 2; - bool needs_bell = false; - uint16_t lineWidths[totalLines] = {0}; - uint16_t lineLengths[totalLines] = {0}; + uint16_t optionWidths[alertBannerOptions] = {0}; + uint16_t maxWidth = 0; + uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); + uint16_t lineWidths[MAX_LINES] = {0}; + uint16_t lineLengths[MAX_LINES] = {0}; + const char *lineStarts[MAX_LINES + 1] = {0}; + uint16_t lineCount = 0; + char lineBuffer[40] = {0}; - if (maxWidth != 0) - is_picker = true; + // Parse lines + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; - // Setup font and alignment - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // Track widest line INCLUDING bars (but don't change per-line widths) - uint16_t widestLineWithBars = 0; - - while (lines[lineCount] != nullptr) { - auto newlinePointer = strchr(lines[lineCount], '\n'); - if (newlinePointer) - lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first - else // if the newline wasn't found, then pull string length from strlen - lineLengths[lineCount] = strlen(lines[lineCount]); - - lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); - - // Consider extra width for signal bars on lines that contain "Signal:" - uint16_t potentialWidth = lineWidths[lineCount]; - if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { - const int totalBars = 5; - const int barWidth = 3; - const int barSpacing = 2; - const int gap = 6; // space between text and bars - int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; - potentialWidth += barsWidth; + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); + lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; + lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; + lineCount++; } - if (potentialWidth > widestLineWithBars) - widestLineWithBars = potentialWidth; - - if (!is_picker) { - needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); - if (lineWidths[lineCount] > maxWidth) - maxWidth = lineWidths[lineCount]; + // Measure option widths + for (int i = 0; i < alertBannerOptions; i++) { + optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); + if (optionWidths[i] > maxWidth) + maxWidth = optionWidths[i]; + if (optionWidths[i] + arrowsWidth > maxWidth) + maxWidth = optionWidths[i] + arrowsWidth; } - lineCount++; - } - // count lines - // Ensure box accounts for signal bars if present - if (widestLineWithBars > maxWidth) - maxWidth = widestLineWithBars; + // Handle input + if (alertBannerOptions > 0) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { + curSelected--; + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || + inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { + curSelected++; + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + if (optionsEnumPtr != nullptr) { + alertBannerCallback(optionsEnumPtr[curSelected]); + optionsEnumPtr = nullptr; + } else { + alertBannerCallback(curSelected); + } + resetBanner(); + return; + } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && + alertBannerUntil != 0) { + resetBanner(); + return; + } - uint16_t boxWidth = hPadding * 2 + maxWidth; + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + } else { + if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_ALT_LONG || + inEvent.inputEvent == INPUT_BROKER_CANCEL) { + resetBanner(); + return; + } + } - if (needs_bell) { - if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) - boxWidth += 26; - if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) - boxWidth += 20; - } + inEvent.inputEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; - uint16_t boxHeight = contentHeight + vPadding * 2; - if (visibleTotalLines == 1) { - boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; - } + uint16_t totalLines = lineCount + alertBannerOptions; - int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - if (totalLines > visibleTotalLines) { - boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; - } - int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint8_t linesShown = lineCount; + const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation + + // copy the linestarts to display to the linePointers holder + for (int i = 0; i < lineCount; i++) { + linePointers[i] = lineStarts[i]; + } + + uint8_t firstOptionToShow = 0; + if (alertBannerOptions > 0) { + if (visibleTotalLines - lineCount == 1) { + firstOptionToShow = curSelected; + } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { + if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) + firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; + else + firstOptionToShow = curSelected - 1; + } else { + firstOptionToShow = 0; + } + } + // Useful log line for troubleshooting: + /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u", + alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */ + + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + if (i == curSelected) { + if (currentResolution == ScreenResolution::High) { + strncpy(lineBuffer, "> ", 3); + strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); + } else { + strncpy(lineBuffer, ">", 2); + strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); + } + lineBuffer[39] = '\0'; + linePointers[linesShown] = lineBuffer; + } else { + linePointers[linesShown] = optionsArrayPtr[i]; + } + } + if (alertBannerOptions > 0) { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); + } else { + drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); + } +} + +void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], + uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth) +{ + + bool is_picker = false; + uint16_t lineCount = 0; + // Layout Configuration + constexpr uint16_t hPadding = 5; + constexpr uint16_t vPadding = 2; + bool needs_bell = false; + uint16_t lineWidths[totalLines] = {0}; + uint16_t lineLengths[totalLines] = {0}; + + if (maxWidth != 0) + is_picker = true; + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Track widest line INCLUDING bars (but don't change per-line widths) + uint16_t widestLineWithBars = 0; + + while (lines[lineCount] != nullptr) { + auto newlinePointer = strchr(lines[lineCount], '\n'); + if (newlinePointer) + lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first + else // if the newline wasn't found, then pull string length from strlen + lineLengths[lineCount] = strlen(lines[lineCount]); + + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + + // Consider extra width for signal bars on lines that contain "Signal:" + uint16_t potentialWidth = lineWidths[lineCount]; + if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int gap = 6; // space between text and bars + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + potentialWidth += barsWidth; + } + + if (potentialWidth > widestLineWithBars) + widestLineWithBars = potentialWidth; + + if (!is_picker) { + needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); + if (lineWidths[lineCount] > maxWidth) + maxWidth = lineWidths[lineCount]; + } + lineCount++; + } + // count lines + + // Ensure box accounts for signal bars if present + if (widestLineWithBars > maxWidth) + maxWidth = widestLineWithBars; + + uint16_t boxWidth = hPadding * 2 + maxWidth; + + if (needs_bell) { + if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) + boxWidth += 26; + if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) + boxWidth += 20; + } + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; + uint16_t boxHeight = contentHeight + vPadding * 2; + if (visibleTotalLines == 1) { + boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; + } + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + if (totalLines > visibleTotalLines) { + boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; + } + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; #if defined(M5STACK_UNITC6L) - if (visibleTotalLines == 1) { - boxTop += 25; - } - if (alertBannerOptions < 3) { - int missingLines = 3 - alertBannerOptions; - int moveUp = missingLines * (effectiveLineHeight / 2); - boxTop -= moveUp; - if (boxTop < 0) - boxTop = 0; - } + if (visibleTotalLines == 1) { + boxTop += 25; + } + if (alertBannerOptions < 3) { + int missingLines = 3 - alertBannerOptions; + int moveUp = missingLines * (effectiveLineHeight / 2); + boxTop -= moveUp; + if (boxTop < 0) + boxTop = 0; + } #endif - // Draw Box - display->setColor(BLACK); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); - display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); - display->setColor(WHITE); + // Draw Box + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); - // Draw Content - int16_t lineY = boxTop + vPadding; - for (int i = 0; i < lineCount; i++) { - int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; - if (needs_bell && i == 0) { - int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; - display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); - display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); - } - char lineBuffer[lineLengths[i] + 1]; - strncpy(lineBuffer, lines[i], lineLengths[i]); - lineBuffer[lineLengths[i]] = '\0'; - // Determine if this is a pop-up or a pick list - if (alertBannerOptions > 0 && i == 0) { - // Pick List - display->setColor(WHITE); - int background_yOffset = 1; - // Determine if we have low hanging characters - if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { - background_yOffset = -1; - } - display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); - display->setColor(BLACK); - int yOffset = 3; - display->drawString(textX, lineY - yOffset, lineBuffer); - display->setColor(WHITE); - lineY += (effectiveLineHeight - 2 - background_yOffset); - } else { - // Pop-up - // If this is the Signal line, center text + bars as one group - bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); - if (isSignalLine) { - const int totalBars = 5; - const int barWidth = 3; - const int barSpacing = 2; - const int barHeightStep = 2; - const int gap = 6; - - int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); - int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; - int totalWidth = textWidth + barsWidth; - int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; - - display->drawString(groupStartX, lineY, lineBuffer); - - int baseX = groupStartX + textWidth + gap; - int baseY = lineY + effectiveLineHeight - 1; - for (int b = 0; b < totalBars; b++) { - int barHeight = (b + 1) * barHeightStep; - int x = baseX + b * (barWidth + barSpacing); - int y = baseY - barHeight; - - if (b < graphics::bannerSignalBars) { - display->fillRect(x, y, barWidth, barHeight); - } else { - display->drawRect(x, y, barWidth, barHeight); - } + // Draw Content + int16_t lineY = boxTop + vPadding; + for (int i = 0; i < lineCount; i++) { + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + if (needs_bell && i == 0) { + int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); + display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); + } + char lineBuffer[lineLengths[i] + 1]; + strncpy(lineBuffer, lines[i], lineLengths[i]); + lineBuffer[lineLengths[i]] = '\0'; + // Determine if this is a pop-up or a pick list + if (alertBannerOptions > 0 && i == 0) { + // Pick List + display->setColor(WHITE); + int background_yOffset = 1; + // Determine if we have low hanging characters + if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { + background_yOffset = -1; + } + display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); + display->setColor(BLACK); + int yOffset = 3; + display->drawString(textX, lineY - yOffset, lineBuffer); + display->setColor(WHITE); + lineY += (effectiveLineHeight - 2 - background_yOffset); + } else { + // Pop-up + // If this is the Signal line, center text + bars as one group + bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); + if (isSignalLine) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int barHeightStep = 2; + const int gap = 6; + + int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + int totalWidth = textWidth + barsWidth; + int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; + + display->drawString(groupStartX, lineY, lineBuffer); + + int baseX = groupStartX + textWidth + gap; + int baseY = lineY + effectiveLineHeight - 1; + for (int b = 0; b < totalBars; b++) { + int barHeight = (b + 1) * barHeightStep; + int x = baseX + b * (barWidth + barSpacing); + int y = baseY - barHeight; + + if (b < graphics::bannerSignalBars) { + display->fillRect(x, y, barWidth, barHeight); + } else { + display->drawRect(x, y, barWidth, barHeight); + } + } + } else { + display->drawString(textX, lineY, lineBuffer); + } + lineY += (effectiveLineHeight); } - } else { - display->drawString(textX, lineY, lineBuffer); - } - lineY += (effectiveLineHeight); } - } - // Scroll Bar (Thicker, inside box, not over title) - if (totalLines > visibleTotalLines) { - const uint8_t scrollBarWidth = 5; - int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; - int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; - uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + // Scroll Bar (Thicker, inside box, not over title) + if (totalLines > visibleTotalLines) { + const uint8_t scrollBarWidth = 5; + int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; + uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; - float ratio = (float)visibleTotalLines / totalLines; - uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); - float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); - uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + float ratio = (float)visibleTotalLines / totalLines; + uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); + float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); + uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); - display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); - display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); - } + display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); + display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); + } } /// Draw the last text message we received -void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); +void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); - char tempBuf[24]; - snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); - display->drawString(0 + x, 0 + y, tempBuf); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); + char tempBuf[24]; + snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); + display->drawString(0 + x, 0 + y, tempBuf); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); } -void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, y, "Updating"); +void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(64 + x, y, "Updating"); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), "Please be patient and do not power off."); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), + "Please be patient and do not power off."); } -void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) { - if (virtualKeyboard) { - // Check for timeout and auto-exit if needed - if (virtualKeyboard->isTimedOut()) { - LOG_INFO("Virtual keyboard timeout - auto-exiting"); - // Cancel virtual keyboard - call callback with empty string to indicate timeout - auto callback = textInputCallback; // Store callback before clearing +void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + if (virtualKeyboard) { + // Check for timeout and auto-exit if needed + if (virtualKeyboard->isTimedOut()) { + LOG_INFO("Virtual keyboard timeout - auto-exiting"); + // Cancel virtual keyboard - call callback with empty string to indicate timeout + auto callback = textInputCallback; // Store callback before clearing - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); - // Call callback after cleanup - if (callback) { - callback(""); - } + // Call callback after cleanup + if (callback) { + callback(""); + } - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } - if (inEvent.inputEvent != INPUT_BROKER_NONE) { - bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); - if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { - auto callback = textInputCallback; - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; + if (inEvent.inputEvent != INPUT_BROKER_NONE) { + bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); + if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { + auto callback = textInputCallback; + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + if (callback) { + callback(""); + } + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Consume the event after processing for virtual keyboard + inEvent.inputEvent = INPUT_BROKER_NONE; + } + + // Re-check pointer before drawing to avoid use-after-free and crashes + if (!virtualKeyboard) { + // Ensure we exit text_input state and restore frames + if (current_notification_type == notificationTypeEnum::text_input) { + resetBanner(); + } + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + // If screen is null, do nothing (safe fallback) + return; + } + + // Clear the screen to avoid overlapping with underlying frames or overlays + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + // Draw the virtual keyboard + virtualKeyboard->draw(display, 0, 0); + + // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule + OnScreenKeyboardModule::instance().drawPopupOverlay(display); + } else { + // If virtualKeyboard is null, reset the banner to avoid getting stuck + LOG_INFO("Virtual keyboard is null - resetting banner"); resetBanner(); - if (callback) { - callback(""); - } - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } + } +} + +bool NotificationRenderer::isOverlayBannerShowing() +{ + return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); +} + +void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) +{ + if (!title || !content || current_notification_type != notificationTypeEnum::text_input) return; - } - - // Consume the event after processing for virtual keyboard - inEvent.inputEvent = INPUT_BROKER_NONE; - } - - // Re-check pointer before drawing to avoid use-after-free and crashes - if (!virtualKeyboard) { - // Ensure we exit text_input state and restore frames - if (current_notification_type == notificationTypeEnum::text_input) { - resetBanner(); - } - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - // If screen is null, do nothing (safe fallback) - return; - } - - // Clear the screen to avoid overlapping with underlying frames or overlays - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - // Draw the virtual keyboard - virtualKeyboard->draw(display, 0, 0); - - // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule - OnScreenKeyboardModule::instance().drawPopupOverlay(display); - } else { - // If virtualKeyboard is null, reset the banner to avoid getting stuck - LOG_INFO("Virtual keyboard is null - resetting banner"); - resetBanner(); - } -} - -bool NotificationRenderer::isOverlayBannerShowing() { - return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); -} - -void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) { - if (!title || !content || current_notification_type != notificationTypeEnum::text_input) - return; - OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); + OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); } } // namespace graphics diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index bdc85f3b0..e51bfa5ab 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -9,42 +9,44 @@ #include #define MAX_LINES 5 -namespace graphics { +namespace graphics +{ -class NotificationRenderer { -public: - static InputEvent inEvent; - static char inKeypress; - static int8_t curSelected; - static char alertBannerMessage[256]; - static uint32_t alertBannerUntil; // 0 is a special case meaning forever - static const char **optionsArrayPtr; - static const int *optionsEnumPtr; - static uint8_t alertBannerOptions; // last x lines are seelctable options - static std::function alertBannerCallback; - static uint32_t numDigits; - static uint32_t currentNumber; - static VirtualKeyboard *virtualKeyboard; - static std::function textInputCallback; +class NotificationRenderer +{ + public: + static InputEvent inEvent; + static char inKeypress; + static int8_t curSelected; + static char alertBannerMessage[256]; + static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static const char **optionsArrayPtr; + static const int *optionsEnumPtr; + static uint8_t alertBannerOptions; // last x lines are seelctable options + static std::function alertBannerCallback; + static uint32_t numDigits; + static uint32_t currentNumber; + static VirtualKeyboard *virtualKeyboard; + static std::function textInputCallback; - static bool pauseBanner; + static bool pauseBanner; - static void resetBanner(); - static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); - static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, - uint8_t firstOptionToShow, uint16_t maxWidth = 0); + static void resetBanner(); + static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); + static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], + uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); - static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static bool isOverlayBannerShowing(); + static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static bool isOverlayBannerShowing(); - static graphics::notificationTypeEnum current_notification_type; + static graphics::notificationTypeEnum current_notification_type; }; } // namespace graphics diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index fa41e4e30..7ce9d5afe 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -21,32 +21,35 @@ extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; #endif -namespace graphics { +namespace graphics +{ NodeNum UIRenderer::currentFavoriteNodeNum = 0; std::vector graphics::UIRenderer::favoritedNodes; -static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) { - int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); - } else { - display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); - } +static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) +{ + int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); + } } -void graphics::UIRenderer::rebuildFavoritedNodes() { - favoritedNodes.clear(); - size_t total = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < total; i++) { - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (!n || n->num == nodeDB->getNodeNum()) - continue; - if (n->is_favorite) - favoritedNodes.push_back(n); - } +void graphics::UIRenderer::rebuildFavoritedNodes() +{ + favoritedNodes.clear(); + size_t total = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < total; i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (!n || n->num == nodeDB->getNodeNum()) + continue; + if (n->is_favorite) + favoritedNodes.push_back(n); + } - std::sort(favoritedNodes.begin(), favoritedNodes.end(), - [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + std::sort(favoritedNodes.begin(), favoritedNodes.end(), + [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } #if !MESHTASTIC_EXCLUDE_GPS @@ -57,1109 +60,1147 @@ extern GeoCoord geoCoord; extern uint32_t dopThresholds[5]; // Draw GPS status summary -void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { - // Draw satellite image - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); - } else { - display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); - } - char textString[10]; +void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + // Draw satellite image + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); + } + char textString[10]; - if (config.position.fixed_position) { - // GPS coordinates are currently fixed - snprintf(textString, sizeof(textString), "Fixed"); - } - if (!gps->getIsConnected()) { - snprintf(textString, sizeof(textString), "No Lock"); - } - if (!gps->getHasLock()) { - // Draw "No sats" to the right of the icon with slightly more gap - snprintf(textString, sizeof(textString), "No Sats"); - } else { - snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); - } - if (currentResolution == ScreenResolution::High) { - display->drawString(x + 18, y, textString); - } else { - display->drawString(x + 11, y, textString); - } + if (config.position.fixed_position) { + // GPS coordinates are currently fixed + snprintf(textString, sizeof(textString), "Fixed"); + } + if (!gps->getIsConnected()) { + snprintf(textString, sizeof(textString), "No Lock"); + } + if (!gps->getHasLock()) { + // Draw "No sats" to the right of the icon with slightly more gap + snprintf(textString, sizeof(textString), "No Sats"); + } else { + snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); + } + if (currentResolution == ScreenResolution::High) { + display->drawString(x + 18, y, textString); + } else { + display->drawString(x + 11, y, textString); + } } // Draw status when GPS is disabled or not present -void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { - const char *displayLine; - int pos; - if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - pos = display->getWidth() - display->getStringWidth(displayLine); - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" : "GPS is disabled"; - pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; - } - display->drawString(x + pos, y, displayLine); +void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + const char *displayLine; + int pos; + if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + pos = display->getWidth() - display->getStringWidth(displayLine); + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" + : "GPS is disabled"; + pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; + } + display->drawString(x + pos, y, displayLine); } -void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { - char displayLine[32]; - if (!gps->getIsConnected() && !config.position.fixed_position) { - // displayLine = "No GPS Module"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - // displayLine = "No GPS Lock"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); - else - snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); - display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } +void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + char displayLine[32]; + if (!gps->getIsConnected() && !config.position.fixed_position) { + // displayLine = "No GPS Module"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + // displayLine = "No GPS Lock"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + else + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } } // Draw GPS status coordinates -void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, const char *mode) { - auto gpsFormat = uiconfig.gps_format; - char displayLine[32]; +void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, + const char *mode) +{ + auto gpsFormat = uiconfig.gps_format; + char displayLine[32]; - if (!gps->getIsConnected() && !config.position.fixed_position) { - if (strcmp(mode, "line1") == 0) { - strcpy(displayLine, "No GPS present"); - display->drawString(x, y, displayLine); - } - } else if (!gps->getHasLock() && !config.position.fixed_position) { - if (strcmp(mode, "line1") == 0) { - strcpy(displayLine, "No GPS Lock"); - display->drawString(x, y, displayLine); - } - } else { - - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - - if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { - char coordinateLine_1[22]; - char coordinateLine_2[22]; - if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), geoCoord.getUTMBand(), geoCoord.getUTMEasting()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), geoCoord.getMGRSBand(), - geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine_1); - coordinateLine_2[0] = '\0'; - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); - coordinateLine_2[0] = '\0'; - } else { - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), geoCoord.getOSGRN100k()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + if (!gps->getIsConnected() && !config.position.fixed_position) { + if (strcmp(mode, "line1") == 0) { + strcpy(displayLine, "No GPS present"); + display->drawString(x, y, displayLine); } - } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System - double lat = geoCoord.getLatitude() * 1e-7; - double lon = geoCoord.getLongitude() * 1e-7; - - // Normalize - if (lat > 90.0) - lat = 90.0; - if (lat < -90.0) - lat = -90.0; - while (lon < -180.0) - lon += 360.0; - while (lon >= 180.0) - lon -= 360.0; - - double adjLon = lon + 180.0; - double adjLat = lat + 90.0; - - char maiden[10]; // enough for 8-char + null - - // Field (2 letters) - int lonField = int(adjLon / 20.0); - int latField = int(adjLat / 10.0); - adjLon -= lonField * 20.0; - adjLat -= latField * 10.0; - - // Square (2 digits) - int lonSquare = int(adjLon / 2.0); - int latSquare = int(adjLat / 1.0); - adjLon -= lonSquare * 2.0; - adjLat -= latSquare * 1.0; - - // Subsquare (2 letters) - double lonUnit = 2.0 / 24.0; - double latUnit = 1.0 / 24.0; - int lonSub = int(adjLon / lonUnit); - int latSub = int(adjLat / latUnit); - - snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare, 'A' + lonSub, - 'A' + latSub); - - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden); - coordinateLine_2[0] = '\0'; // only need one line - } - - if (strcmp(mode, "line1") == 0) { - display->drawString(x, y, coordinateLine_1); - } else if (strcmp(mode, "line2") == 0) { - display->drawString(x, y, coordinateLine_2); - } else if (strcmp(mode, "combined") == 0) { - display->drawString(x, y, coordinateLine_1); - if (coordinateLine_2[0] != '\0') { - display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + if (strcmp(mode, "line1") == 0) { + strcpy(displayLine, "No GPS Lock"); + display->drawString(x, y, displayLine); } - } - } else { - char coordinateLine_1[22]; - char coordinateLine_2[22]; - snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), - geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), - geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - if (strcmp(mode, "line1") == 0) { - display->drawString(x, y, coordinateLine_1); - } else if (strcmp(mode, "line2") == 0) { - display->drawString(x, y, coordinateLine_2); - } else { // both - display->drawString(x, y, coordinateLine_1); - display->drawString(x, y + 10, coordinateLine_2); - } + + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + + if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { + char coordinateLine_1[22]; + char coordinateLine_2[22]; + if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), + geoCoord.getUTMBand(), geoCoord.getUTMEasting()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), + geoCoord.getMGRSNorthing()); + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code + geoCoord.getOLCCode(coordinateLine_1); + coordinateLine_2[0] = '\0'; + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); + coordinateLine_2[0] = '\0'; + } else { + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), + geoCoord.getOSGRNorthing()); + } + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System + double lat = geoCoord.getLatitude() * 1e-7; + double lon = geoCoord.getLongitude() * 1e-7; + + // Normalize + if (lat > 90.0) + lat = 90.0; + if (lat < -90.0) + lat = -90.0; + while (lon < -180.0) + lon += 360.0; + while (lon >= 180.0) + lon -= 360.0; + + double adjLon = lon + 180.0; + double adjLat = lat + 90.0; + + char maiden[10]; // enough for 8-char + null + + // Field (2 letters) + int lonField = int(adjLon / 20.0); + int latField = int(adjLat / 10.0); + adjLon -= lonField * 20.0; + adjLat -= latField * 10.0; + + // Square (2 digits) + int lonSquare = int(adjLon / 2.0); + int latSquare = int(adjLat / 1.0); + adjLon -= lonSquare * 2.0; + adjLat -= latSquare * 1.0; + + // Subsquare (2 letters) + double lonUnit = 2.0 / 24.0; + double latUnit = 1.0 / 24.0; + int lonSub = int(adjLon / lonUnit); + int latSub = int(adjLat / latUnit); + + snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare, + 'A' + lonSub, 'A' + latSub); + + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden); + coordinateLine_2[0] = '\0'; // only need one line + } + + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else if (strcmp(mode, "combined") == 0) { + display->drawString(x, y, coordinateLine_1); + if (coordinateLine_2[0] != '\0') { + display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); + } + } + + } else { + char coordinateLine_1[22]; + char coordinateLine_2[22]; + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), + geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), + geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else { // both + display->drawString(x, y, coordinateLine_1); + display->drawString(x, y + 10, coordinateLine_2); + } + } } - } } #endif // !MESHTASTIC_EXCLUDE_GPS // Draw nodes status -void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, bool show_total, - const char *additional_words) { - char usersString[20]; - int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; +void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, + bool show_total, const char *additional_words) +{ + char usersString[20]; + int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); + snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); - if (show_total) { - int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); - } + if (show_total) { + int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; + snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); + } -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ - defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \ - defined(USE_ST7796)) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); - } else { - display->drawFastImage(x, y + 3, 8, 8, imgUser); - } + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 3, 8, 8, imgUser); + } #else - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); - } else { - display->drawFastImage(x, y + 1, 8, 8, imgUser); - } + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 1, 8, 8, imgUser); + } #endif - int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; - display->drawString(x + 10 + string_offset, y - 2, usersString); + int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; + display->drawString(x + 10 + string_offset, y - 2, usersString); } // ********************** // * Favorite Node Info * // ********************** -void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) { - if (favoritedNodes.empty()) - return; - - // --- Only display if index is valid --- - int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); - if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) - return; - - meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; - if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) - return; - display->clear(); -#if defined(M5STACK_UNITC6L) - uint32_t now = millis(); - if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 - { - display->display(); - lastSwitchTime = now; - } -#endif - currentFavoriteNodeNum = node->num; - // === Create the shortName and title string === - const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - char titlestr[32] = {0}; - snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); - - // === Draw battery/time/mail header (common across screens) === - graphics::drawCommonHeader(display, x, y, titlestr); - - // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== - // 1. Each potential info row has a macro-defined Y position (not regular increments!). - // 2. Each row is only shown if it has valid data. - // 3. Each row "moves up" if previous are empty, so there are never any blank rows. - // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. - - // List of available macro Y positions in order, from top to bottom. - int line = 1; // which slot to use next - std::string usernameStr; - // === 1. Long Name (always try to show first) === - const char *username; - if (currentResolution == ScreenResolution::UltraLow) { - username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; - } else { - username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; - } - - if (username) { - usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case - // Print node's long name (e.g. "Backpack Node") - display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); - } - - // === 2. Signal and Hops (combined on one line, if available) === - // If both are present: "Sig: 97% [2hops]" - // If only one: show only that one - char signalHopsStr[32] = ""; - bool haveSignal = false; - int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); - - // Always use "Sig" for the label - const char *signalLabel = " Sig"; - - // --- Build the Signal/Hops line --- - // If SNR looks reasonable, show signal - if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { - snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); - haveSignal = true; - } - // If hops is valid (>0), show right after signal - if (node->hops_away > 0) { - size_t len = strlen(signalHopsStr); - // Decide between "1 Hop" and "N Hops" - if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); - } else { - snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); - } - } - if (signalHopsStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], signalHopsStr); - } - - // === 3. Heard (last seen, skip if node never seen) === - char seenStr[20] = ""; - uint32_t seconds = sinceLastSeen(node); - if (seconds != 0 && seconds != UINT32_MAX) { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" - snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); - } - if (seenStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], seenStr); - } -#if !defined(M5STACK_UNITC6L) - // === 4. Uptime (only show if metric is present) === - char uptimeStr[32] = ""; - if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { - getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr)); - } - if (uptimeStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], uptimeStr); - } - - // === 5. Distance (only if both nodes have GPS position) === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - char distStr[24] = ""; // Make buffer big enough for any string - bool haveDistance = false; - - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; - - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); - haveDistance = true; - } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); - haveDistance = true; - } - } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); - haveDistance = true; - } - } - } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); - haveDistance = true; - } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: 1km"); - haveDistance = true; - } - } else { - int km = (int)(distanceKm + 0.5); - if (km > 0 && km < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); - haveDistance = true; - } - } - } - } - // Only display if we actually have a value! - if (haveDistance && distStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], distStr); - } - - // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { - const int16_t topY = getTextPositions(display)[1]; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); - const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; - } else { - bearing -= myHeading; - } - - display->drawCircle(compassX, compassY, compassRadius); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - } - // else show nothing - } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { - int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) : getTextPositions(display)[1]; - const int margin = 4; -// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- -#if defined(USE_EINK) - const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; - const int navBarHeight = iconSize + 6; -#else - const int navBarHeight = 0; -#endif - int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; - // --------- END PATCH FOR EINK NAV BAR ----------- - - if (availableHeight < FONT_HEIGHT_SMALL * 2) +void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + if (favoritedNodes.empty()) return; - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; + // --- Only display if index is valid --- + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; - - const auto &op = ourNode->position; - float myHeading = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); - - const auto &p = node->position; - /* unused - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - */ - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearing -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); - - display->drawCircle(compassX, compassY, compassRadius); + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; + display->clear(); +#if defined(M5STACK_UNITC6L) + uint32_t now = millis(); + if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 + { + display->display(); + lastSwitchTime = now; } - // else show nothing - } #endif - graphics::drawCommonFooter(display, x, y); + currentFavoriteNodeNum = node->num; + // === Create the shortName and title string === + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + char titlestr[32] = {0}; + snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); + + // === Draw battery/time/mail header (common across screens) === + graphics::drawCommonHeader(display, x, y, titlestr); + + // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== + // 1. Each potential info row has a macro-defined Y position (not regular increments!). + // 2. Each row is only shown if it has valid data. + // 3. Each row "moves up" if previous are empty, so there are never any blank rows. + // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. + + // List of available macro Y positions in order, from top to bottom. + int line = 1; // which slot to use next + std::string usernameStr; + // === 1. Long Name (always try to show first) === + const char *username; + if (currentResolution == ScreenResolution::UltraLow) { + username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; + } else { + username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + } + + if (username) { + usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case + // Print node's long name (e.g. "Backpack Node") + display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); + } + + // === 2. Signal and Hops (combined on one line, if available) === + // If both are present: "Sig: 97% [2hops]" + // If only one: show only that one + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + + // Always use "Sig" for the label + const char *signalLabel = " Sig"; + + // --- Build the Signal/Hops line --- + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + // Decide between "1 Hop" and "N Hops" + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, + (node->hops_away == 1 ? "Hop" : "Hops")); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], signalHopsStr); + } + + // === 3. Heard (last seen, skip if node never seen) === + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], seenStr); + } +#if !defined(M5STACK_UNITC6L) + // === 4. Uptime (only show if metric is present) === + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr)); + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], uptimeStr); + } + + // === 5. Distance (only if both nodes have GPS position) === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; // Make buffer big enough for any string + bool haveDistance = false; + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); + haveDistance = true; + } + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + haveDistance = true; + } + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + haveDistance = true; + } else if (meters >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: 1km"); + haveDistance = true; + } + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + haveDistance = true; + } + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], distStr); + } + + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = 0; + } else { + bearing -= myHeading; + } + + display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + } + // else show nothing + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) + : getTextPositions(display)[1]; + const int margin = 4; +// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- +#if defined(USE_EINK) + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + // --------- END PATCH FOR EINK NAV BAR ----------- + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + const auto &op = ourNode->position; + float myHeading = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) + bearing -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing + } +#endif + graphics::drawCommonFooter(display, x, y); } // **************************** // * Device Focused Screen * // **************************** -void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); +void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - // === Header === - if (currentResolution == ScreenResolution::UltraLow) { - graphics::drawCommonHeader(display, x, y, "Home"); - } else { - graphics::drawCommonHeader(display, x, y, ""); - } + // === Header === + if (currentResolution == ScreenResolution::UltraLow) { + graphics::drawCommonHeader(display, x, y, "Home"); + } else { + graphics::drawCommonHeader(display, x, y, ""); + } - // === Content below header === + // === Content below header === - // Determine if we need to show 4 or 5 rows on the screen - int rows = 4; - if (!config.bluetooth.enabled) { - rows = 5; - } + // Determine if we need to show 4 or 5 rows on the screen + int rows = 4; + if (!config.bluetooth.enabled) { + rows = 5; + } - // === First Row: Region / Channel Utilization and Uptime === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; + // === First Row: Region / Channel Utilization and Uptime === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; - // Display Region and Channel Utilization - if (currentResolution == ScreenResolution::UltraLow) { - drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); - } else { - drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); - } - char uptimeStr[32] = ""; - if (currentResolution != ScreenResolution::UltraLow) { - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); - } - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); + // Display Region and Channel Utilization + if (currentResolution == ScreenResolution::UltraLow) { + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } else { + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } + char uptimeStr[32] = ""; + if (currentResolution != ScreenResolution::UltraLow) { + getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + } + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); - // === Second Row: Satellites and Voltage === - config.display.heading_bold = false; + // === Second Row: Satellites and Voltage === + config.display.heading_bold = false; #if HAS_GPS - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - const char *displayLine; - if (config.position.fixed_position) { - displayLine = "Fixed GPS"; + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + const char *displayLine; + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); } - drawSatelliteIcon(display, x, getTextPositions(display)[line]); - int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; - display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); - } else { - UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); - } #endif #if defined(M5STACK_UNITC6L) - line += 1; + line += 1; - // === Node Identity === - int textWidth = 0; - int nameX = 0; - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - // === ShortName Centered === - textWidth = display->getStringWidth(shortnameble); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); -#else - if (powerStatus->getHasBattery()) { - char batStr[20]; - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); - } else { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); - } - - config.display.heading_bold = origBold; - - // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === - const char *chUtil = "ChUtil:"; - char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - - int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; - int chUtil_y = getTextPositions(display)[line] + 3; - - int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; - if (!config.bluetooth.enabled) { -#if defined(USE_EINK) - chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; -#else - chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; -#endif - } - int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; - int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; - if (!config.bluetooth.enabled) { - extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; - } - int chutil_percent = airTime->channelUtilizationPercent(); - - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; - if (!config.bluetooth.enabled) { - starting_position = 0; - } - - display->drawString(starting_position, getTextPositions(display)[line], chUtil); - - // Force 56% or higher to show a full 100% bar, text would still show related percent. - if (chutil_percent >= 61) { - chutil_percent = 100; - } - - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0–25% - float weight2 = 0.35; // Weight for 25–40% - float weight3 = 0.20; // Weight for 40–100% - float totalWeight = weight1 + weight2 + weight3; - - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); - - int fillRight = 0; - - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); - } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } - - // Draw outline - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); - - // Fill progress - if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); - } - - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], chUtilPercentage); - - if (!config.bluetooth.enabled) { - display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); - } - - line += 1; - - // === Fourth & Fifth Rows: Node Identity === - int textWidth = 0; - int nameX = 0; - int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; - std::string longNameStr; - - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - longNameStr = sanitizeString(ourNode->user.long_name); - } - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - char combinedName[50]; - snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); - if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) { - size_t len = strlen(combinedName); - if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { - combinedName[len - 3] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(combinedName); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); - } else { - // === LongName Centered === - textWidth = display->getStringWidth(longNameStr.c_str()); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); + // === Node Identity === + int textWidth = 0; + int nameX = 0; + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); // === ShortName Centered === textWidth = display->getStringWidth(shortnameble); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - } +#else + if (powerStatus->getHasBattery()) { + char batStr[20]; + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); + } else { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); + } + + config.display.heading_bold = origBold; + + // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + if (!config.bluetooth.enabled) { +#if defined(USE_EINK) + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; +#else + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; #endif - graphics::drawCommonFooter(display, x, y); + } + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; + if (!config.bluetooth.enabled) { + extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; + } + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + if (!config.bluetooth.enabled) { + starting_position = 0; + } + + display->drawString(starting_position, getTextPositions(display)[line], chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], + chUtilPercentage); + + if (!config.bluetooth.enabled) { + display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); + } + + line += 1; + + // === Fourth & Fifth Rows: Node Identity === + int textWidth = 0; + int nameX = 0; + int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; + std::string longNameStr; + + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + longNameStr = sanitizeString(ourNode->user.long_name); + } + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + char combinedName[50]; + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); + if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) { + size_t len = strlen(combinedName); + if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { + combinedName[len - 3] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(combinedName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString( + nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); + } else { + // === LongName Centered === + textWidth = display->getStringWidth(longNameStr.c_str()); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); + + // === ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + } +#endif + graphics::drawCommonFooter(display, x, y); } // Start Functions to write date/time to the screen // Helper function to check if a year is a leap year -constexpr bool isLeapYear(int year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } +constexpr bool isLeapYear(int year) +{ + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} // Array of days in each month (non-leap year) const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // Fills the buffer with a formatted date/time string and returns pixel width -int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) { - int sec = rtc_sec % 60; - rtc_sec /= 60; - int min = rtc_sec % 60; - rtc_sec /= 60; - int hour = rtc_sec % 24; - rtc_sec /= 24; +int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) +{ + int sec = rtc_sec % 60; + rtc_sec /= 60; + int min = rtc_sec % 60; + rtc_sec /= 60; + int hour = rtc_sec % 24; + rtc_sec /= 24; - int year = 1970; - while (true) { - int daysInYear = isLeapYear(year) ? 366 : 365; - if (rtc_sec >= (uint32_t)daysInYear) { - rtc_sec -= daysInYear; - year++; - } else { - break; + int year = 1970; + while (true) { + int daysInYear = isLeapYear(year) ? 366 : 365; + if (rtc_sec >= (uint32_t)daysInYear) { + rtc_sec -= daysInYear; + year++; + } else { + break; + } } - } - int month = 0; - while (month < 12) { - int dim = daysInMonth[month]; - if (month == 1 && isLeapYear(year)) - dim++; - if (rtc_sec >= (uint32_t)dim) { - rtc_sec -= dim; - month++; - } else { - break; + int month = 0; + while (month < 12) { + int dim = daysInMonth[month]; + if (month == 1 && isLeapYear(year)) + dim++; + if (rtc_sec >= (uint32_t)dim) { + rtc_sec -= dim; + month++; + } else { + break; + } } - } - int day = rtc_sec + 1; + int day = rtc_sec + 1; - if (includeTime) { - snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); - } else { - snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); - } + if (includeTime) { + snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + } else { + snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + } - return display->getStringWidth(buf); + return display->getStringWidth(buf); } // Check if the display can render a string (detect special chars; emoji) -bool UIRenderer::haveGlyphs(const char *str) { +bool UIRenderer::haveGlyphs(const char *str) +{ #if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) - // Don't want to make any assumptions about custom language support - return true; + // Don't want to make any assumptions about custom language support + return true; #endif - // Check each character with the lookup function for the OLED library - // We're not really meant to use this directly.. - bool have = true; - for (uint16_t i = 0; i < strlen(str); i++) { - uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); - // If font doesn't support a character, it is substituted for ¿ - if (result == 191 && (uint8_t)str[i] != 191) { - have = false; - break; + // Check each character with the lookup function for the OLED library + // We're not really meant to use this directly.. + bool have = true; + for (uint16_t i = 0; i < strlen(str); i++) { + uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); + // If font doesn't support a character, it is substituted for ¿ + if (result == 191 && (uint8_t)str[i] != 191) { + have = false; + break; + } } - } - // LOG_DEBUG("haveGlyphs=%d", have); - return have; + // LOG_DEBUG("haveGlyphs=%d", have); + return have; } #ifdef USE_EINK /// Used on eink displays while in deep sleep -void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ - // Next frame should use full-refresh, and block while running, else device will sleep before async callback - EINK_ADD_FRAMEFLAG(display, COSMETIC); - EINK_ADD_FRAMEFLAG(display, BLOCKING); + // Next frame should use full-refresh, and block while running, else device will sleep before async callback + EINK_ADD_FRAMEFLAG(display, COSMETIC); + EINK_ADD_FRAMEFLAG(display, BLOCKING); - LOG_DEBUG("Draw deep sleep screen"); + LOG_DEBUG("Draw deep sleep screen"); - // Display displayStr on the screen - graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); + // Display displayStr on the screen + graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); } /// Used on eink displays when screen updates are paused -void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { - LOG_DEBUG("Draw screensaver overlay"); +void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + LOG_DEBUG("Draw screensaver overlay"); - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver - // Config - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *pauseText = "Screen Paused"; - const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); - constexpr uint8_t padding = 2; - constexpr uint8_t dividerGap = 1; + // Config + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *pauseText = "Screen Paused"; + const char *idText = owner.short_name; + const bool useId = haveGlyphs(idText); + constexpr uint8_t padding = 2; + constexpr uint8_t dividerGap = 1; - // Text widths - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); - const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); - const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; - const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); + // Text widths + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); + const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); - // Flush with bottom - const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - const int16_t boxTop = display->height() - boxHeight; - const int16_t boxBottom = display->height() - 1; - const int16_t idTextLeft = boxLeft + padding; - const int16_t idTextTop = boxTop + padding; - const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; - const int16_t pauseTextTop = boxTop + padding; - const int16_t dividerX = boxLeft + padding + idTextWidth + padding; - const int16_t dividerTop = boxTop + dividerGap; - const int16_t dividerBottom = boxBottom - dividerGap; + // Flush with bottom + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + const int16_t boxTop = display->height() - boxHeight; + const int16_t boxBottom = display->height() - 1; + const int16_t idTextLeft = boxLeft + padding; + const int16_t idTextTop = boxTop + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; + const int16_t pauseTextTop = boxTop + padding; + const int16_t dividerX = boxLeft + padding + idTextWidth + padding; + const int16_t dividerTop = boxTop + dividerGap; + const int16_t dividerBottom = boxBottom - dividerGap; - // Draw: box - display->setColor(EINK_WHITE); - display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(EINK_BLACK); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + // Draw: box + display->setColor(EINK_WHITE); + display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(EINK_BLACK); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - // Draw: text - if (useId) - display->drawString(idTextLeft, idTextTop, idText); - display->drawString(pauseTextLeft, pauseTextTop, pauseText); - display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold + // Draw: text + if (useId) + display->drawString(idTextLeft, idTextTop, idText); + display->drawString(pauseTextLeft, pauseTextTop, pauseText); + display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold - // Draw: divider - if (useId) - display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); + // Draw: divider + if (useId) + display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); } #endif /** * Draw the icon with extra info printed around the corners */ -void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // draw an xbm image. - // Please note that everything that should be transitioned - // needs to be drawn relative to x and y +void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // draw an xbm image. + // Please note that everything that should be transitioned + // needs to be drawn relative to x and y - // draw centered icon left to right and centered above the one line of app text + // draw centered icon left to right and centered above the one line of app text #if defined(M5STACK_UNITC6L) - display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - // Draw region in upper left - if (upperMsg) { - int msgWidth = display->getStringWidth(upperMsg); - int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; - int msgY = y; - display->drawString(msgX, msgY, upperMsg); - } - // Draw version and short name in bottom middle - char buf[25]; - snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) { + int msgWidth = display->getStringWidth(upperMsg); + int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; + int msgY = y; + display->drawString(msgX, msgY, upperMsg); + } + // Draw version and short name in bottom middle + char buf[25]; + snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); - screen->forceDisplay(); + display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); + screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code #else - display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, icon_width, icon_height, - icon_bits); + display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, + icon_width, icon_height, icon_bits); - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = "meshtastic.org"; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = "meshtastic.org"; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); - // Draw version and short name in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + // Draw version and short name in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code #endif } // **************************** // * My Position Screen * // **************************** -void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "Position"; + // === Set Title + const char *titleStr = "Position"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === First Row: My Location === + // === First Row: My Location === #if HAS_GPS - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; - const char *displayLine = ""; // Initialize to empty string by default - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const char *displayLine = ""; // Initialize to empty string by default + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - if (config.position.fixed_position) { - displayLine = "Fixed GPS"; + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + // Onboard GPS + UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); } - drawSatelliteIcon(display, x, getTextPositions(display)[line]); - int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; - display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); - } else { - // Onboard GPS - UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); - } - config.display.heading_bold = origBold; + config.display.heading_bold = origBold; - // === Update GeoCoord === - geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); + // === Update GeoCoord === + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude())); - // === Determine Compass Heading === - float heading = 0; - bool validHeading = false; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - validHeading = true; - } else { - if (screen->hasHeading()) { - heading = radians(screen->getHeading()); - validHeading = true; + // === Determine Compass Heading === + float heading = 0; + bool validHeading = false; + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + validHeading = true; } else { - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); - validHeading = !isnan(heading); + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; + } else { + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); + } } - } - // If GPS is off, no need to display these parts - if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { - // === Second Row: Last GPS Fix === - if (gpsStatus->getLastFixMillis() > 0) { - uint32_t delta = millis() - gpsStatus->getLastFixMillis(); - char uptimeStr[32]; + // If GPS is off, no need to display these parts + if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = millis() - gpsStatus->getLastFixMillis(); + char uptimeStr[32]; #if defined(USE_EINK) - // E-Ink: skip seconds, show only days/hours/mins - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false); + // E-Ink: skip seconds, show only days/hours/mins + getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false); #else - // Non E-Ink: include seconds where useful - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true); + // Non E-Ink: include seconds where useful + getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true); #endif - display->drawString(0, getTextPositions(display)[line++], uptimeStr); - } else { - display->drawString(0, getTextPositions(display)[line++], "Last: ?"); - } + display->drawString(0, getTextPositions(display)[line++], uptimeStr); + } else { + display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + } - // === Third Row: Line 1 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); + // === Third Row: Line 1 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); - if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && - uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { - // === Fourth Row: Line 2 GPS Info === - UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); - } + if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && + uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { + // === Fourth Row: Line 2 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); + } - // === Final Row: Altitude === - char altitudeLine[32] = {0}; - int32_t alt = geoCoord.getAltitude(); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); - } else { - snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); + // === Final Row: Altitude === + char altitudeLine[32] = {0}; + int32_t alt = geoCoord.getAltitude(); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); + } else { + snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); + } + display->drawString(x, getTextPositions(display)[line++], altitudeLine); } - display->drawString(x, getTextPositions(display)[line++], altitudeLine); - } #if !defined(M5STACK_UNITC6L) - // === Draw Compass if heading is valid === - if (validHeading) { - // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - const int16_t topY = getTextPositions(display)[1]; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height - const int16_t usableHeight = bottomY - topY - 5; + // === Draw Compass if heading is valid === + if (validHeading) { + // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - // Center vertically and nudge down slightly to keep "N" clear of header - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); - display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassRadius); - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; - const int margin = 4; - int availableHeight = + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + // For E-Ink screens, account for navigation bar at the bottom! + int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; + const int margin = 4; + int availableHeight = #if defined(USE_EINK) - SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink + SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink #else - SCREEN_HEIGHT - yBelowContent - margin; + SCREEN_HEIGHT - yBelowContent - margin; #endif - if (availableHeight < FONT_HEIGHT_SMALL * 2) - return; + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); - display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + display->drawCircle(compassX, compassY, compassRadius); - // "N" label - float northAngle = 0; - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) - northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + // "N" label + float northAngle = 0; + if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) + northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } } - } #endif #endif // HAS_GPS - graphics::drawCommonFooter(display, x, y); + graphics::drawCommonFooter(display, x, y); } #ifdef USERPREFS_OEM_TEXT -void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - if (currentResolution == ScreenResolution::High) { - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); - } else { +void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; + if (currentResolution == ScreenResolution::High) { + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } else { - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, - USERPREFS_OEM_IMAGE_WIDTH, USERPREFS_OEM_IMAGE_HEIGHT, xbm); - } + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + } - switch (USERPREFS_OEM_FONT_SIZE) { - case 0: + switch (USERPREFS_OEM_FONT_SIZE) { + case 0: + display->setFont(FONT_SMALL); + break; + case 2: + display->setFont(FONT_LARGE); + break; + default: + display->setFont(FONT_MEDIUM); + break; + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = USERPREFS_OEM_TEXT; + if (currentResolution == ScreenResolution::High) { + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + } display->setFont(FONT_SMALL); - break; - case 2: - display->setFont(FONT_LARGE); - break; - default: - display->setFont(FONT_MEDIUM); - break; - } - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = USERPREFS_OEM_TEXT; - if (currentResolution == ScreenResolution::High) { - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - } - display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); - - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code } -void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawOEMIconScreen(region, display, state, x, y); +void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawOEMIconScreen(region, display, state, x, y); } #endif @@ -1169,169 +1210,172 @@ static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; -void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { - int currentFrame = state->currentFrame; +void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + int currentFrame = state->currentFrame; - // Detect frame change and record time - if (currentFrame != lastFrameIndex) { - lastFrameIndex = currentFrame; - lastFrameChangeTime = millis(); - } + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } - const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; - const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; - const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; + const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; + const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; - const size_t totalIcons = screen->indicatorIcons.size(); - if (totalIcons == 0) - return; + const size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) + return; - const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side + const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side - int usableWidth = SCREEN_WIDTH - (navPadding * 2); - if (usableWidth < iconSize) - usableWidth = iconSize; + int usableWidth = SCREEN_WIDTH - (navPadding * 2); + if (usableWidth < iconSize) + usableWidth = iconSize; - const size_t iconsPerPage = usableWidth / (iconSize + spacing); - const size_t currentPage = currentFrame / iconsPerPage; - const size_t pageStart = currentPage * iconsPerPage; - const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); + const size_t iconsPerPage = usableWidth / (iconSize + spacing); + const size_t currentPage = currentFrame / iconsPerPage; + const size_t pageStart = currentPage * iconsPerPage; + const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); - const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; - const int xStart = (SCREEN_WIDTH - totalWidth) / 2; + const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; + const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; - int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; + bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; + int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; #if defined(USE_EINK) - // Only show bar briefly after switching frames - static uint32_t navBarLastShown = 0; - static bool cosmeticRefreshDone = false; - static bool navBarPrevVisible = false; + // Only show bar briefly after switching frames + static uint32_t navBarLastShown = 0; + static bool cosmeticRefreshDone = false; + static bool navBarPrevVisible = false; - if (navBarVisible && !navBarPrevVisible) { - EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar - cosmeticRefreshDone = false; - navBarLastShown = millis(); - } - - if (!navBarVisible && navBarPrevVisible) { - EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar - navBarLastShown = millis(); // Mark when it disappeared - } - - if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { - if (millis() - navBarLastShown > 10000) { // 10s after hidden - EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup - cosmeticRefreshDone = true; + if (navBarVisible && !navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar + cosmeticRefreshDone = false; + navBarLastShown = millis(); } - } - navBarPrevVisible = navBarVisible; + if (!navBarVisible && navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar + navBarLastShown = millis(); // Mark when it disappeared + } + + if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { + if (millis() - navBarLastShown > 10000) { // 10s after hidden + EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup + cosmeticRefreshDone = true; + } + } + + navBarPrevVisible = navBarVisible; #endif - // Pre-calculate bounding rect - const int rectX = xStart - 2 - bigOffset; - const int rectWidth = totalWidth + 4 + (bigOffset * 2); - const int rectHeight = iconSize + 6; + // Pre-calculate bounding rect + const int rectX = xStart - 2 - bigOffset; + const int rectWidth = totalWidth + 4 + (bigOffset * 2); + const int rectHeight = iconSize + 6; - // Clear background and draw border - display->setColor(BLACK); - display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); - display->setColor(WHITE); - display->drawRect(rectX, y - 2, rectWidth, rectHeight); - - // Icon drawing loop for the current page - for (size_t i = pageStart; i < pageEnd; ++i) { - const uint8_t *icon = screen->indicatorIcons[i]; - const int x = xStart + (i - pageStart) * (iconSize + spacing); - const bool isActive = (i == static_cast(currentFrame)); - - if (isActive) { - display->setColor(WHITE); - display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); - display->setColor(BLACK); - } - - if (currentResolution == ScreenResolution::High) { - NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); - } else { - display->drawXbm(x, y, iconSize, iconSize, icon); - } - - if (isActive) { - display->setColor(WHITE); - } - } - - // Compact arrow drawer - auto drawArrow = [&](bool rightSide) { + // Clear background and draw border + display->setColor(BLACK); + display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); display->setColor(WHITE); + display->drawRect(rectX, y - 2, rectWidth, rectHeight); - const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; - const int halfH = rectHeight / 2; + // Icon drawing loop for the current page + for (size_t i = pageStart; i < pageEnd; ++i) { + const uint8_t *icon = screen->indicatorIcons[i]; + const int x = xStart + (i - pageStart) * (iconSize + spacing); + const bool isActive = (i == static_cast(currentFrame)); - const int top = (y - 2) + (rectHeight - halfH) / 2; - const int bottom = top + halfH - 1; - const int midY = top + (halfH / 2); + if (isActive) { + display->setColor(WHITE); + display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); + display->setColor(BLACK); + } - const int maxW = 4; + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } - // Determine left X coordinate - int baseX = rightSide ? (rectX + rectWidth + offset) : // right arrow - (rectX - offset - 1); // left arrow - - for (int yy = top; yy <= bottom; yy++) { - int dist = abs(yy - midY); - int lineW = maxW - (dist * maxW / (halfH / 2)); - if (lineW < 1) - lineW = 1; - - if (rightSide) { - display->drawHorizontalLine(baseX, yy, lineW); - } else { - display->drawHorizontalLine(baseX - lineW + 1, yy, lineW); - } + if (isActive) { + display->setColor(WHITE); + } } - }; - // Right arrow - if (pageEnd < totalIcons) { - drawArrow(true); - } - // Left arrow - if (pageStart > 0) { - drawArrow(false); - } + // Compact arrow drawer + auto drawArrow = [&](bool rightSide) { + display->setColor(WHITE); - // Knock the corners off the square - display->setColor(BLACK); - display->drawRect(rectX, y - 2, 1, 1); - display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); - display->setColor(WHITE); + const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; + const int halfH = rectHeight / 2; + + const int top = (y - 2) + (rectHeight - halfH) / 2; + const int bottom = top + halfH - 1; + const int midY = top + (halfH / 2); + + const int maxW = 4; + + // Determine left X coordinate + int baseX = rightSide ? (rectX + rectWidth + offset) : // right arrow + (rectX - offset - 1); // left arrow + + for (int yy = top; yy <= bottom; yy++) { + int dist = abs(yy - midY); + int lineW = maxW - (dist * maxW / (halfH / 2)); + if (lineW < 1) + lineW = 1; + + if (rightSide) { + display->drawHorizontalLine(baseX, yy, lineW); + } else { + display->drawHorizontalLine(baseX - lineW + 1, yy, lineW); + } + } + }; + // Right arrow + if (pageEnd < totalIcons) { + drawArrow(true); + } + + // Left arrow + if (pageStart > 0) { + drawArrow(false); + } + + // Knock the corners off the square + display->setColor(BLACK); + display->drawRect(rectX, y - 2, 1, 1); + display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->setColor(WHITE); } -void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) { - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, message); +void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) +{ + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, message); } -std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) { - std::string uptime; +std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) +{ + std::string uptime; - if (days > (HOURS_IN_MONTH * 6)) - uptime = "?"; - else if (days >= 2) - uptime = std::to_string(days) + "d"; - else if (hours >= 2) - uptime = std::to_string(hours) + "h"; - else if (minutes >= 1) - uptime = std::to_string(minutes) + "m"; - else - uptime = std::to_string(seconds) + "s"; - return uptime; + if (days > (HOURS_IN_MONTH * 6)) + uptime = "?"; + else if (days >= 2) + uptime = std::to_string(days) + "d"; + else if (hours >= 2) + uptime = std::to_string(hours) + "h"; + else if (minutes >= 1) + uptime = std::to_string(minutes) + "m"; + else + uptime = std::to_string(seconds) + "s"; + return uptime; } } // namespace graphics diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index f6aba487e..6e37b68f2 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -10,13 +10,15 @@ #define HOURS_IN_MONTH 730 // Forward declarations for status types -namespace meshtastic { +namespace meshtastic +{ class PowerStatus; class NodeStatus; class GPSStatus; } // namespace meshtastic -namespace graphics { +namespace graphics +{ /// Forward declarations class Screen; @@ -27,57 +29,59 @@ class Screen; * Contains utility functions for drawing common UI elements, overlays, * battery indicators, and other shared graphical components. */ -class UIRenderer { -public: - // Common UI elements - static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, - bool show_total = true, const char *additional_words = ""); +class UIRenderer +{ + public: + // Common UI elements + static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, + int node_offset = 0, bool show_total = true, const char *additional_words = ""); - // GPS status functions - static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, const char *mode = "line1"); - static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + // GPS status functions + static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, + const char *mode = "line1"); + static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - // Overlay and special screens - static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); + // Overlay and special screens + static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); - // Navigation bar overlay - static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); + // Navigation bar overlay + static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - // Icon and screen drawing functions - static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // Icon and screen drawing functions + static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - // Compass and location screen - static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // Compass and location screen + static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static NodeNum currentFavoriteNodeNum; - static std::vector favoritedNodes; - static void rebuildFavoritedNodes(); + static NodeNum currentFavoriteNodeNum; + static std::vector favoritedNodes; + static void rebuildFavoritedNodes(); // OEM screens #ifdef USERPREFS_OEM_TEXT - static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif #ifdef USE_EINK - /// Used on eink displays while in deep sleep - static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + /// Used on eink displays while in deep sleep + static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - /// Used on eink displays when screen updates are paused - static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + /// Used on eink displays when screen updates are paused + static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); #endif - static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); - static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); + static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); - // Check if the display can render a string (detect special chars; emoji) - static bool haveGlyphs(const char *str); + // Check if the display can render a string (detect special chars; emoji) + static bool haveGlyphs(const char *str); }; // namespace UIRenderer } // namespace graphics diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp index 24b154dad..aa54ef2f1 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -2,7 +2,8 @@ #if HAS_SCREEN #include "emotes.h" -namespace graphics { +namespace graphics +{ // Always define Emote list and count const Emote emotes[] = { @@ -12,13 +13,13 @@ const Emote emotes[] = { {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- - {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes - {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face - {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face - {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face + {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes + {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face + {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face + {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face {"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // 😁 Grinning Smiling Eyes {"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // 😍 Heart Eyes - {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts + {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts // --- Question/Alert --- {"\u2753", question, question_width, question_height}, // ❓ Question Mark @@ -26,16 +27,17 @@ const Emote emotes[] = { {"\u26A0\uFE0F", caution, caution_width, caution_height}, // ⚠️ Warning Sign // --- Laughing Faces --- - {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy - {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing - {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes - {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat - {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes - {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face - {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting - {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses - {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes - {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye + {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy + {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing + {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat + {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, + grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes + {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face + {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting + {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses + {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes + {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye // --- Gestures and People --- {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand @@ -54,33 +56,38 @@ const Emote emotes[] = { {"\U0001F3E0", house, house_width, house_height}, // 🏠 House // --- Weather --- - {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) - {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector) - {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain - {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud - {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog - {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake - {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet - {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer - {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud - {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud - {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud - {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow - {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, cloud_with_lightning_height}, // 🌩️ Cloud with Lightning - {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain - {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain - {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face + {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog + {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake + {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet + {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer + {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, + sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud + {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud + {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud + {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow + {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, + cloud_with_lightning_height}, // 🌩️ Cloud with Lightning + {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, + cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain + {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, + cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain + {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face // --- Moon Phases --- - {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon - {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon - {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon - {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon - {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon - {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon - {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon - {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon - {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face + {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon + {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon + {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon + {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon + {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon + {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon + {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon + {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon + {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, + first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face // --- Misc Faces --- {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns @@ -149,249 +156,325 @@ const Emote emotes[] = { const int numEmotes = sizeof(emotes) / sizeof(emotes[0]); #ifndef EXCLUDE_EMOJI -const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18, 0x02, 0x06, 0x3F, 0x06, 0x40, - 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00}; +const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18, + 0x02, 0x06, 0x3F, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, + 0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00}; -const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, - 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; +const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06, + 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, + 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; -const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, - 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, + 0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, + 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, - 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, + 0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, - 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, + 0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, 0x46, 0x02, 0x40, 0x02, 0x40, - 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, + 0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52, 0x0A, 0x02, 0xD8, 0x02, 0xF8, - 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; +const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52, + 0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, + 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; -const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, 0x5F, 0x72, 0x4E, 0x22, 0x44, - 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, + 0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, + 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x11, - 0x80, 0x08, 0x40, 0x04, 0x40, 0x02, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01}; +const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48, + 0x12, 0x48, 0x12, 0x30, 0x11, 0x80, 0x08, 0x40, 0x04, 0x40, 0x02, + 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01}; -const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, - 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00}; +const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, + 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C, + 0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00}; -const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x0A, 0x50, 0x0E, 0x70, - 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, + 0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, 0x4C, 0x02, 0x4A, 0x1A, 0x49, - 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, + 0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, + 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, - 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, + 0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, 0x52, 0x12, 0x48, 0x12, 0x48, - 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; +const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, + 0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, + 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; -const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24, 0x25, 0x4A, 0x24, 0x12, 0x44, - 0x22, 0x44, 0x04, 0x40, 0x08, 0x40, 0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00}; +const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24, + 0x25, 0x4A, 0x24, 0x12, 0x44, 0x22, 0x44, 0x04, 0x40, 0x08, 0x40, + 0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00}; -const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E, 0x78, 0xE2, 0x47, 0x42, 0x42, - 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E, + 0x78, 0xE2, 0x47, 0x42, 0x42, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A, 0x50, 0x0E, 0x70, 0x11, 0x88, - 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00}; +const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A, + 0x50, 0x0E, 0x70, 0x11, 0x88, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, + 0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00}; -const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, - 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00}; +const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, + 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, + 0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00}; -const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; +const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, + 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; -const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, + 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x44, 0x44, 0xAA, 0x2A, - 0x11, 0x11, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, + 0x00, 0x44, 0x44, 0xAA, 0x2A, 0x11, 0x11, 0x00, 0x00, 0x88, 0x88, + 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A, 0x5C, 0x32, 0x4C, 0x52, 0x4A, - 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A, + 0x5C, 0x32, 0x4C, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00}; +const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F, + 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00}; -const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0, 0x08, 0x10, 0x10, 0x48, 0x12, - 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40, 0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00}; +const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0, + 0x08, 0x10, 0x10, 0x48, 0x12, 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40, + 0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00}; -const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xF8, 0x1F, 0xF8, 0x1F, - 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00}; +const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, + 0x0F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F, + 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00}; -const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32, 0x40, 0x02, 0x58, 0x82, 0x5B, - 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32, + 0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, + 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, - 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; +const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, + 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; -const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38, 0x0A, 0x54, 0x68, 0x54, 0x58, - 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38, + 0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, + 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, 0x09, 0x98, 0x19, 0x94, 0x29, - 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; +const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, + 0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, + 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; -const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, 0x1F, 0x80, 0x3F, 0xE0, 0xFF, - 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; +const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, + 0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, + 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; -const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72, 0x4E, 0x22, 0x44, 0x82, 0x41, - 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F, 0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72, + 0x4E, 0x22, 0x44, 0x82, 0x41, 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F, + 0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74, 0x2E, 0x52, 0x4A, 0x72, 0x4E, - 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A, 0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00}; +const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74, + 0x2E, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A, + 0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00}; -const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C, 0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00}; +const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C, + 0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00}; -const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00, 0x38, 0x00, 0x28, 0x78, 0x44, - 0x84, 0x82, 0x22, 0x83, 0x52, 0x83, 0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00}; +const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00, + 0x38, 0x00, 0x28, 0x78, 0x44, 0x84, 0x82, 0x22, 0x83, 0x52, 0x83, + 0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00}; -const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, 0x0A, 0x02, 0x38, 0x04, 0x48, - 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, + 0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, + 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, 0xA0, 0x31, 0x8C, 0x51, 0x8A, - 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; +const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, + 0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, + 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; -const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, - 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; +const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, + 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, + 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; -const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, 0xE3, 0x87, 0xE1, 0x87, 0xE1, - 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; +const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, + 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, + 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; -const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, 0x44, 0x42, 0x42, 0x22, 0x44, - 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; +const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, + 0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, + 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; -const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, 0x5F, 0x72, 0x4E, 0x02, 0x40, - 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, + 0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, 0xB8, 0x10, 0x87, 0xC8, 0x80, - 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; +const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, + 0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, + 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; -const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x70, 0x0E, 0x70, 0x0E, - 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; +const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, + 0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, + 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; -const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, 0x5F, 0x8A, 0x54, 0xFA, 0x5F, - 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; +const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, + 0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, + 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; -const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, 0x03, 0x32, 0x26, 0x1C, 0x1C, - 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; +const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, + 0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, + 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; -const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, - 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; +const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, + 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, + 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; -const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, 0x00, 0x70, 0x01, 0xE0, 0x02, - 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; +const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, + 0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, + 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; -const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, 0x77, 0xDE, 0x7B, 0x3E, 0x7C, - 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; +const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, + 0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, + 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; -const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, - 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, + 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; +const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, + 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; -const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; +const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, + 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21, 0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01}; -const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, 0x01, 0x99, 0x01, 0xF9, 0x01, - 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; +const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, + 0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, + 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; -const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, - 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, + 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, + 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, - 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, + 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, + 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, - 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, + 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, + 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, - 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, + 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, + 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, - 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, + 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, + 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, - 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, + 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, + 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, - 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, + 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, + 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00, 0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80, 0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07}; -const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, - 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; +const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, + 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, + 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; -const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, 0x21, 0x2C, 0x56, 0x14, 0x58, - 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; +const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, + 0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, + 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; -const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, 0x01, 0x10, 0x0E, 0x20, 0x30, - 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; +const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, + 0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, + 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; -const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, 0x1F, 0x80, 0x0F, 0xC2, 0x07, - 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; +const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, + 0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, + 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; -const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x43, - 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; +const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, + 0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, + 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; -const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0xE6, 0x03, - 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; +const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, + 0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, + 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; -const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0xC0, 0x67, - 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, + 0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, + 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, - 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; +const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, + 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; -const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, 0x00, 0xFE, 0x3F, 0xFF, 0x3F, - 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, + 0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, + 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; -const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xC0, 0x03, 0xC0, 0x03, - 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; +const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, + 0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; -const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, - 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; +const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, + 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, + 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; -const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, 0x00, 0x11, 0x3C, 0x11, 0x42, - 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; +const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, + 0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, + 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; -const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x07, - 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; +const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, + 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, + 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; -const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, 0x20, 0x02, 0x40, 0xFF, 0xFF, - 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; +const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, + 0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, + 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; -const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, 0x09, 0x27, 0xE4, 0x49, 0x92, - 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; +const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, + 0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, + 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; -const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, - 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; +const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; -const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, 0x2F, 0x7A, 0x5E, 0x39, 0x9C, - 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, + 0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, + 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #endif } // namespace graphics diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h index 773f5edd0..0637712cc 100644 --- a/src/graphics/emotes.h +++ b/src/graphics/emotes.h @@ -1,14 +1,15 @@ #pragma once #include -namespace graphics { +namespace graphics +{ // === Emote List === struct Emote { - const char *label; - const unsigned char *bitmap; - int width; - int height; + const char *label; + const unsigned char *bitmap; + int width; + int height; }; extern const Emote emotes[/* numEmotes */]; diff --git a/src/graphics/fonts/EinkDisplayFonts.cpp b/src/graphics/fonts/EinkDisplayFonts.cpp index b1b008802..497b3b389 100644 --- a/src/graphics/fonts/EinkDisplayFonts.cpp +++ b/src/graphics/fonts/EinkDisplayFonts.cpp @@ -237,926 +237,952 @@ const uint8_t Monospaced_plain_30[] PROGMEM = { 0x44, 0x11, 0x52, 0x12, // 255:17425 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, - 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, // 33 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 34 - 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x38, 0x08, 0x00, 0x00, 0x70, 0xB8, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, - 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, 0x38, 0x08, 0x00, 0xC0, 0x70, 0xB8, - 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, - 0x38, 0x00, 0x00, 0xC0, 0x70, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 35 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x07, 0x00, 0x00, 0xF8, 0x03, 0x07, - 0x00, 0x00, 0xFC, 0x03, 0x0E, 0x00, 0x00, 0x1E, 0x07, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, - 0xFF, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x1C, 0x0F, 0x00, 0x00, 0x1C, - 0xF8, 0x07, 0x00, 0x00, 0x1C, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, // 33 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 34 + 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x38, 0x08, 0x00, 0x00, 0x70, 0xB8, 0x0F, 0x00, 0x00, + 0x70, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, + 0x38, 0x08, 0x00, 0xC0, 0x70, 0xB8, 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x3F, + 0x00, 0x00, 0xC0, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, 0x38, 0x00, 0x00, 0xC0, 0x70, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x70, // 35 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x07, 0x00, 0x00, + 0xF8, 0x03, 0x07, 0x00, 0x00, 0xFC, 0x03, 0x0E, 0x00, 0x00, 0x1E, 0x07, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0x00, 0x0E, + 0x06, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x0C, + 0x0E, 0x00, 0x00, 0x0E, 0x1C, 0x0F, 0x00, 0x00, 0x1C, 0xF8, 0x07, 0x00, 0x00, 0x1C, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x01, // 36 - 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x10, 0x00, 0x00, 0xC0, 0x71, 0x18, 0x00, 0x00, 0xC0, 0x60, 0x08, 0x00, 0x00, 0xC0, 0x60, 0x0C, 0x00, - 0x00, 0xC0, 0x60, 0x04, 0x00, 0x00, 0xC0, 0x71, 0x06, 0x00, 0x00, 0x80, 0x3F, 0x02, 0x00, 0x00, 0x00, 0x1F, 0xE3, 0x03, 0x00, 0x00, 0x00, 0xF1, - 0x07, 0x00, 0x00, 0x80, 0x39, 0x0E, 0x00, 0x00, 0x80, 0x18, 0x0C, 0x00, 0x00, 0xC0, 0x18, 0x0C, 0x00, 0x00, 0x40, 0x18, 0x0C, 0x00, 0x00, 0x60, - 0x38, 0x0E, 0x00, 0x00, 0x20, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x10, 0x00, 0x00, 0xC0, 0x71, 0x18, 0x00, 0x00, 0xC0, 0x60, 0x08, 0x00, 0x00, 0xC0, + 0x60, 0x0C, 0x00, 0x00, 0xC0, 0x60, 0x04, 0x00, 0x00, 0xC0, 0x71, 0x06, 0x00, 0x00, 0x80, 0x3F, 0x02, 0x00, 0x00, 0x00, 0x1F, + 0xE3, 0x03, 0x00, 0x00, 0x00, 0xF1, 0x07, 0x00, 0x00, 0x80, 0x39, 0x0E, 0x00, 0x00, 0x80, 0x18, 0x0C, 0x00, 0x00, 0xC0, 0x18, + 0x0C, 0x00, 0x00, 0x40, 0x18, 0x0C, 0x00, 0x00, 0x60, 0x38, 0x0E, 0x00, 0x00, 0x20, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x1F, 0x87, 0x07, - 0x00, 0x80, 0xFF, 0x03, 0x0F, 0x00, 0x80, 0xFF, 0x01, 0x0F, 0x00, 0xC0, 0xE3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1F, - 0x0E, 0x00, 0xC0, 0x01, 0x3E, 0x0E, 0x00, 0xC0, 0x01, 0x7C, 0x07, 0x00, 0x80, 0x03, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, - 0xC0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x08, // 38 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x80, 0x3F, 0x80, - 0x3F, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, + 0x1F, 0x87, 0x07, 0x00, 0x80, 0xFF, 0x03, 0x0F, 0x00, 0x80, 0xFF, 0x01, 0x0F, 0x00, 0xC0, 0xE3, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1F, 0x0E, 0x00, 0xC0, 0x01, 0x3E, 0x0E, 0x00, 0xC0, 0x01, 0x7C, 0x07, 0x00, 0x80, 0x03, 0xF0, + 0x07, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x0E, + 0x00, 0x00, 0x00, 0x1E, 0x08, // 38 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, + 0x00, 0x00, 0x00, 0xC0, 0x3F, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xFE, + 0xFF, 0x0F, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, 0x80, // 40 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x20, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0x00, 0x00, 0xFE, 0xFF, - 0x0F, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xC0, - 0x7F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, - 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, - 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x86, - 0x01, 0x00, 0x00, 0x00, - 0x84, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0xF8, 0xFF, - 0x07, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0x3F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 44 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0x80, 0x3F, + 0x80, 0x3F, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, + 0xCC, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, + 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, 0x84, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF8, + 0xFF, 0x07, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 44 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, - 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 46 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x7C, - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x80, 0x3F, - 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x07, - 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, - 0x0E, 0x00, 0xC0, 0x01, 0x02, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, - 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, - 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 46 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, + 0xFE, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x01, + 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x02, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0x80, 0x0F, 0xC0, + 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x80, 0x0F, - 0x00, 0xC0, 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0E, 0x00, 0xC0, 0x01, 0xF0, 0x0E, 0x00, 0xC0, 0x01, 0x78, 0x0E, 0x00, 0xC0, 0x01, 0x3C, - 0x0E, 0x00, 0xC0, 0x01, 0x1E, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0x80, 0x87, 0x0F, 0x0E, 0x00, 0x80, 0xFF, 0x07, 0x0E, 0x00, 0x00, 0xFF, - 0x01, 0x0E, 0x00, 0x00, 0xFC, 0x00, 0x0E, // 50 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0E, - 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0x80, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, - 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, - 0x00, 0x00, 0xC0, 0x77, 0x00, 0x00, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0x78, 0x70, 0x00, 0x00, 0x00, 0x1E, 0x70, 0x00, 0x00, 0x00, 0x0F, 0x70, - 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xC0, 0xFF, 0x03, 0x07, 0x00, 0xC0, 0xFF, 0x01, 0x0E, - 0x00, 0xC0, 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, - 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x07, 0x00, 0xC0, 0x81, 0x87, 0x07, 0x00, 0xC0, 0x81, 0xFF, 0x03, 0x00, 0x00, 0x00, - 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x1F, 0x87, 0x07, 0x00, 0x80, 0x87, 0x03, 0x0F, 0x00, 0xC0, 0x83, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, - 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0F, 0x00, 0xC0, 0x81, 0x87, 0x07, 0x00, 0x80, 0x83, 0xFF, 0x07, 0x00, 0x00, 0x00, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFC, // 54 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, - 0x00, 0xC0, 0x01, 0x00, 0x08, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x07, 0x00, 0xC0, 0x01, 0xFF, - 0x01, 0x00, 0xC0, 0xE1, 0x3F, 0x00, 0x00, 0xC0, 0xF9, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x07, - 0x00, 0x00, 0x00, 0xC0, - 0x01, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x7F, 0xFE, 0x07, - 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, - 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x07, 0x07, - 0x00, 0x80, 0x87, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, - 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0F, 0x00, 0xC0, 0x03, 0x87, 0x07, 0x00, 0x80, 0x87, 0xE1, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, - 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, - 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x00, 0x00, 0xF0, 0x00, - 0x3F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, - 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x80, 0x33, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, - 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x30, - 0x80, 0x01, 0x00, 0x00, 0x38, 0x80, 0x03, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, - 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, - 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, - 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x80, 0x03, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, - 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x31, - 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, - 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x3E, 0x0F, 0x00, 0xC0, 0x01, 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x3F, - 0x0F, 0x00, 0xC0, 0xC1, 0x03, 0x00, 0x00, 0xC0, 0xF3, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, - 0x1E, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x00, 0x38, 0x00, 0x38, - 0x00, 0x00, 0x0C, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x7C, 0xE0, 0x00, 0x00, 0x06, 0xFF, 0xC1, 0x00, 0x00, 0x83, 0x83, 0xC3, 0x00, 0x00, 0x83, 0x01, - 0x87, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC7, 0x00, 0x86, 0x01, 0x00, 0x86, - 0x01, 0xC3, 0x01, 0x00, 0x9E, 0x83, 0xC3, 0x01, 0x00, 0xFC, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, // 64 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, - 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x07, 0x0F, 0x00, 0x80, 0xFF, 0x0E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, - 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x01, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, - 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, - 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, - 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, - 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, - 0x03, 0x00, 0x00, 0xC0, - 0x01, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, - 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0x80, 0x03, - 0xFF, 0x07, 0x00, 0x00, 0x07, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, + 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0E, 0x00, 0xC0, 0x01, 0xF0, 0x0E, 0x00, 0xC0, 0x01, + 0x78, 0x0E, 0x00, 0xC0, 0x01, 0x3C, 0x0E, 0x00, 0xC0, 0x01, 0x1E, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0x80, 0x87, 0x0F, + 0x0E, 0x00, 0x80, 0xFF, 0x07, 0x0E, 0x00, 0x00, 0xFF, 0x01, 0x0E, 0x00, 0x00, 0xFC, 0x00, 0x0E, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, + 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0x80, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, + 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0x77, 0x00, 0x00, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0x78, 0x70, 0x00, 0x00, 0x00, 0x1E, + 0x70, 0x00, 0x00, 0x00, 0x0F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xC0, 0xFF, 0x03, 0x07, 0x00, 0xC0, + 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, + 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x07, 0x00, 0xC0, 0x81, 0x87, + 0x07, 0x00, 0xC0, 0x81, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x1F, 0x87, 0x07, 0x00, 0x80, 0x87, 0x03, 0x0F, 0x00, 0xC0, 0x83, 0x01, 0x0E, 0x00, 0xC0, 0xC1, + 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0F, 0x00, 0xC0, 0x81, 0x87, + 0x07, 0x00, 0x80, 0x83, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFC, // 54 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, + 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x08, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0F, 0x00, 0xC0, 0x01, + 0xF8, 0x07, 0x00, 0xC0, 0x01, 0xFF, 0x01, 0x00, 0xC0, 0xE1, 0x3F, 0x00, 0x00, 0xC0, 0xF9, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, + 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x01, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, + 0x7F, 0xFE, 0x07, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, + 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x80, + 0xFF, 0x07, 0x07, 0x00, 0x80, 0x87, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, + 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0F, 0x00, 0xC0, 0x03, 0x87, 0x07, 0x00, 0x80, 0x87, 0xE1, + 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x01, 0x00, 0xF0, + 0x00, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x3F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, + 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x80, 0x33, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0xC0, + 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, + 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x38, 0x80, 0x03, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, + 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, + 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, + 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x80, 0x03, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, + 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, + 0x71, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x3E, 0x0F, 0x00, 0xC0, 0x01, + 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x03, 0x00, 0x00, 0xC0, 0xF3, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x1E, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x00, + 0x38, 0x00, 0x38, 0x00, 0x00, 0x0C, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x7C, 0xE0, 0x00, 0x00, 0x06, 0xFF, 0xC1, 0x00, 0x00, 0x83, + 0x83, 0xC3, 0x00, 0x00, 0x83, 0x01, 0x87, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, + 0x86, 0x01, 0x00, 0xC7, 0x00, 0x86, 0x01, 0x00, 0x86, 0x01, 0xC3, 0x01, 0x00, 0x9E, 0x83, 0xC3, 0x01, 0x00, 0xFC, 0xFF, 0x07, + 0x00, 0x00, 0xF0, 0xFF, 0x07, // 64 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, + 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x07, 0x0F, 0x00, 0x80, 0xFF, 0x0E, + 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x01, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, + 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 67 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, + 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 68 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, + 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, + 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x01, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, + 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, + 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0x80, 0x03, 0xFF, 0x07, 0x00, 0x00, 0x07, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x03, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, - 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, - 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, - 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x1F, - 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x01, 0x00, 0x80, 0x03, 0xE0, 0x07, 0x00, 0xC0, 0x01, - 0x80, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0x40, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, + 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, + 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, + 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xF0, + 0x07, 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0x00, 0x0F, 0xF0, + 0x01, 0x00, 0x80, 0x03, 0xE0, 0x07, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0x40, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x08, // 75 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x00, - 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, - 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0x03, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, + 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, - 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, - 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0x80, 0xC7, 0x03, 0x00, 0x00, 0x80, 0xFF, - 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7C, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x3E, 0x00, 0x80, 0x03, 0x00, 0x7F, 0x00, 0x80, 0x0F, 0xC0, 0xF7, 0x00, 0x00, 0xFF, - 0xFF, 0x63, 0x00, 0x00, 0xFE, 0xFF, 0x41, 0x00, 0x00, 0xF0, 0x3F, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, - 0x00, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0x00, 0xC0, 0x83, 0x1F, 0x00, 0x00, 0x80, 0xC7, 0x7D, 0x00, 0x00, 0x80, 0xFF, 0xF9, 0x01, 0x00, 0x00, 0xFF, - 0xF0, 0x07, 0x00, 0x00, 0x7C, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x08, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x80, 0xFF, 0x01, 0x0E, - 0x00, 0x80, 0xC7, 0x01, 0x0E, 0x00, 0xC0, 0xC3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, - 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x8F, 0x07, 0x00, 0x80, 0x03, 0xFE, 0x07, 0x00, 0x80, 0x03, - 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 83 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, - 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, - 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, - 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, - 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 85 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, - 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, - 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, - 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, // 86 - 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, - 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0x01, - 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFE, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x01, // 87 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x07, 0xC0, 0x0F, - 0x00, 0x80, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, - 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x00, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x80, 0x0F, - 0xE0, 0x03, 0x00, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0C, // 88 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, - 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, - 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, - 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 89 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x01, 0xC0, 0x0F, - 0x00, 0xC0, 0x01, 0xF0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x0E, 0x00, 0xC0, 0x01, 0x7E, 0x0E, 0x00, 0xC0, 0x01, 0x1F, 0x0E, 0x00, 0xC0, 0xC1, 0x0F, - 0x0E, 0x00, 0xC0, 0xE1, 0x03, 0x0E, 0x00, 0xC0, 0xF9, 0x01, 0x0E, 0x00, 0xC0, 0x7D, 0x00, 0x0E, 0x00, 0xC0, 0x3F, 0x00, 0x0E, 0x00, 0xC0, 0x0F, - 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x80, + 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, + 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, + 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x83, 0x07, + 0x00, 0x00, 0x80, 0xC7, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7C, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x3E, 0x00, 0x80, 0x03, 0x00, + 0x7F, 0x00, 0x80, 0x0F, 0xC0, 0xF7, 0x00, 0x00, 0xFF, 0xFF, 0x63, 0x00, 0x00, 0xFE, 0xFF, 0x41, 0x00, 0x00, 0xF0, 0x3F, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, + 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0x00, 0xC0, 0x83, 0x1F, 0x00, 0x00, 0x80, 0xC7, 0x7D, + 0x00, 0x00, 0x80, 0xFF, 0xF9, 0x01, 0x00, 0x00, 0xFF, 0xF0, 0x07, 0x00, 0x00, 0x7C, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x08, // 82 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x80, + 0xFF, 0x01, 0x0E, 0x00, 0x80, 0xC7, 0x01, 0x0E, 0x00, 0xC0, 0xC3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x8F, + 0x07, 0x00, 0x80, 0x03, 0xFE, 0x07, 0x00, 0x80, 0x03, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 83 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, + 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, + 0x00, 0xC0, 0x01, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, + 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, + 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 85 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0x80, + 0xFF, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, + 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xF8, 0x3F, + 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, // 86 + 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, + 0x01, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xE0, + 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, + 0x00, 0xC0, 0x01, // 87 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, + 0x07, 0xC0, 0x0F, 0x00, 0x80, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x7E, 0x00, 0x00, 0x00, 0xF0, + 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x7C, 0x7C, + 0x00, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x80, 0x0F, 0xE0, 0x03, 0x00, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x0F, + 0x00, 0xC0, 0x00, 0x00, 0x0C, // 88 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, + 0x00, 0x40, // 89 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, + 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xF0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x0E, 0x00, 0xC0, 0x01, 0x7E, 0x0E, 0x00, 0xC0, 0x01, + 0x1F, 0x0E, 0x00, 0xC0, 0xC1, 0x0F, 0x0E, 0x00, 0xC0, 0xE1, 0x03, 0x0E, 0x00, 0xC0, 0xF9, 0x01, 0x0E, 0x00, 0xC0, 0x7D, 0x00, + 0x0E, 0x00, 0xC0, 0x3F, 0x00, 0x0E, 0x00, 0xC0, 0x0F, 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 90 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, - 0xFF, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, // 91 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, - 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x3F, - 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, - 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, + 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, + 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, // 91 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, + 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, + 0x0F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x40, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0xFF, 0xFF, - 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x38, - 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x20, // 94 - 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, // 95 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x00, 0x02, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, - 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, - 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, - 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, - 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, - 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, - 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, - 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, - 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, - 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, - 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 101 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, - 0x00, 0x00, 0x00, 0xE0, - 0x70, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0xC7, - 0x01, 0x00, 0xE0, 0x81, 0xC7, 0x01, 0x00, 0xF0, 0x00, 0x8F, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, - 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0xE0, 0x00, 0xC7, 0x03, 0x00, 0xC0, 0x81, 0xE3, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x01, 0x00, 0xF0, - 0xFF, 0xFF, 0x00, 0x00, 0xF0, 0xFF, 0x3F, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, - 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, - 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, - 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, - 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 105 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, - 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0xC0, 0x03, 0xE0, 0xF0, 0xFF, - 0xFF, 0x01, 0xE0, 0xF0, 0xFF, 0xFF, 0x00, 0xE0, 0xF0, 0xFF, 0x7F, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, - 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x7F, - 0x00, 0x00, 0x00, 0xC0, 0xFB, 0x00, 0x00, 0x00, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0x80, 0x0F, 0x00, 0x00, 0x70, - 0x00, 0x0E, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, + 0x00, 0xE0, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, + 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x20, // 94 + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, // 95 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x02, // 96 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, + 0xE0, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, + 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, + 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, + 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 99 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, + 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, + 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, + 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, + 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, + 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 101 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, + 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, + 0xE0, 0xFF, 0xC7, 0x01, 0x00, 0xE0, 0x81, 0xC7, 0x01, 0x00, 0xF0, 0x00, 0x8F, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, + 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0xE0, 0x00, 0xC7, 0x03, 0x00, 0xC0, 0x81, + 0xE3, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0xF0, 0xFF, 0x3F, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, + 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, + 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 105 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, + 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, + 0x00, 0xC0, 0x03, 0xE0, 0xF0, 0xFF, 0xFF, 0x01, 0xE0, 0xF0, 0xFF, 0xFF, 0x00, 0xE0, 0xF0, 0xFF, 0x7F, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, + 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFB, 0x00, 0x00, 0x00, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0xF0, 0xC1, + 0x07, 0x00, 0x00, 0xF0, 0x80, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, 0x08, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0xE0, 0xFF, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, - 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, - 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0xE0, 0xFF, + 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, + 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, - 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, - 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, - 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, - 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, - 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, - 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x07, - 0x00, 0x00, 0xE0, 0x0F, 0x07, 0x00, 0x00, 0xF0, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x1E, - 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x00, - 0xF0, 0x01, // 115 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0xFF, 0xFF, 0x07, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x70, 0x00, - 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, - 0x00, 0x0E, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, - 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 117 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, - 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF0, - 0x00, 0x00, 0x00, 0x00, - 0x10, // 118 - 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x0F, - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, - 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x30, // 119 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0F, - 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x7F, - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x30, - 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, + 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, + 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, + 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, + 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, + 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, + 0x03, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, + 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0x00, + 0xE0, 0x07, 0x07, 0x00, 0x00, 0xE0, 0x0F, 0x07, 0x00, 0x00, 0xF0, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, + 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x1E, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0F, 0x00, 0x00, 0x70, 0xFC, + 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x01, // 115 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0xFF, 0xFF, 0x07, 0x00, 0x80, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, + 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 117 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, + 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x3F, + 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 118 + 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, + 0x00, 0x00, 0x30, // 119 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, + 0x7F, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xF0, 0xC1, + 0x07, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, - 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, - 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, - 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0F, - 0x00, 0x00, 0x70, 0xC0, 0x0F, 0x00, 0x00, 0x70, 0xE0, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x7C, 0x0E, 0x00, 0x00, 0x70, 0x3F, - 0x0E, 0x00, 0x00, 0xF0, 0x0F, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x0E, 0x00, 0x00, 0xF0, 0x03, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0E, 0x00, 0x00, 0x70, - 0x00, 0x0E, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0xC0, 0xFF, 0xFB, - 0xFF, 0x00, 0xE0, 0xFF, 0xF1, 0xFF, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, - 0x00, 0xC0, 0x01, // 123 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, - 0xFF, 0x07, // 124 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xC0, - 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0xFF, 0xF1, 0xFF, 0x00, 0xC0, 0xFF, 0xFB, - 0xFF, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0E, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, + 0xF0, 0x03, 0x80, 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, + 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x80, 0x7F, + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 121 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x0F, 0x00, 0x00, 0x70, 0xC0, 0x0F, 0x00, 0x00, 0x70, 0xE0, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, + 0x7C, 0x0E, 0x00, 0x00, 0x70, 0x3F, 0x0E, 0x00, 0x00, 0xF0, 0x0F, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x0E, 0x00, 0x00, 0xF0, 0x03, + 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, // 122 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0xFF, + 0xFB, 0x7F, 0x00, 0xC0, 0xFF, 0xFB, 0xFF, 0x00, 0xE0, 0xFF, 0xF1, 0xFF, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0x00, + 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, + 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0x07, // 124 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0xFF, + 0xF1, 0xFF, 0x00, 0xC0, 0xFF, 0xFB, 0xFF, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0E, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 127 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 128 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 129 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 130 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 131 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 132 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 133 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 134 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 135 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 136 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 137 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 138 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 139 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 140 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 141 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 142 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 143 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 144 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 145 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 146 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 147 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 148 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 149 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 150 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 151 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 152 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 153 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 154 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 155 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 156 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 157 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 158 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, - 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, - 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, - 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, + 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, + 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, + 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 159 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, - 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, // 161 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, - 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xFF, 0xFF, - 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, - 0x00, 0x07, // 162 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0E, - 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0xFC, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0x07, 0x07, - 0x0E, 0x00, 0xC0, 0x03, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, + 0x80, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, + 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, // 162 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, + 0x00, 0x07, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0xFC, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, + 0xFF, 0x0F, 0x00, 0x80, 0x07, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, + 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0E, // 163 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, - 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, - 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xE0, - 0xDE, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, + 0x70, 0x80, 0x03, 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x61, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 164 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xCF, 0x18, 0x00, - 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, - 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0xC0, 0xCF, - 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0x40, // 165 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xC3, 0xFF, 0x01, 0x80, 0xFF, 0xC3, - 0xFF, 0x01, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8F, 0x1F, 0x38, - 0x00, 0x80, 0xDF, 0x1F, 0x70, 0x00, 0x80, 0xFF, 0x38, 0x60, 0x00, 0xC0, 0x71, 0x30, 0x60, 0x00, 0xC0, 0xE0, 0x60, 0x60, 0x00, 0xC0, 0xC0, 0xE0, - 0x60, 0x00, 0xC0, 0xC0, 0xC1, 0x71, 0x00, 0xC0, 0x80, 0xE3, 0x3F, 0x00, 0xC0, 0x81, 0xBF, 0x3F, 0x00, 0x80, 0x03, 0x3F, 0x0E, 0x00, 0x00, 0x00, - 0x1E, // 167 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, // 168 - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x8E, 0xCF, 0x01, - 0x00, 0x00, 0xE6, 0x9F, 0x01, 0x00, 0x00, 0x63, 0x38, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, - 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x63, 0x18, 0x03, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, - 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, // 169 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x30, 0x00, - 0x00, 0x80, 0xF9, 0x31, 0x00, 0x00, 0xC0, 0x98, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, - 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x8C, 0x31, 0x00, 0x00, 0x80, 0xCD, 0x30, 0x00, 0x00, 0x80, 0xFF, 0x33, 0x00, 0x00, 0x00, 0xFF, - 0x33, // 170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, - 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x0E, - 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, - 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, - 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, - 0x3F, 0x00, 0x00, 0x00, 0x80, 0x3F, // 172 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, + 0xCF, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xE0, + 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xFC, 0x18, + 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0xC0, 0xCF, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, + 0x00, 0x40, // 165 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, + 0xC3, 0xFF, 0x01, 0x80, 0xFF, 0xC3, 0xFF, 0x01, // 166 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x8F, 0x1F, 0x38, 0x00, 0x80, 0xDF, 0x1F, 0x70, 0x00, 0x80, 0xFF, 0x38, 0x60, 0x00, 0xC0, 0x71, 0x30, 0x60, 0x00, 0xC0, 0xE0, + 0x60, 0x60, 0x00, 0xC0, 0xC0, 0xE0, 0x60, 0x00, 0xC0, 0xC0, 0xC1, 0x71, 0x00, 0xC0, 0x80, 0xE3, 0x3F, 0x00, 0xC0, 0x81, 0xBF, + 0x3F, 0x00, 0x80, 0x03, 0x3F, 0x0E, 0x00, 0x00, 0x00, 0x1E, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0x00, 0xE0, // 168 + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, + 0x8E, 0xCF, 0x01, 0x00, 0x00, 0xE6, 0x9F, 0x01, 0x00, 0x00, 0x63, 0x38, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, + 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x63, 0x18, 0x03, 0x00, 0x00, 0x06, 0x80, + 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, + 0x00, 0x00, 0xC0, 0x0F, // 169 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF0, 0x30, 0x00, 0x00, 0x80, 0xF9, 0x31, 0x00, 0x00, 0xC0, 0x98, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, + 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x8C, 0x31, 0x00, 0x00, 0x80, 0xCD, 0x30, + 0x00, 0x00, 0x80, 0xFF, 0x33, 0x00, 0x00, 0x00, 0xFF, 0x33, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0xC0, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, + 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x3F, // 172 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 173 - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x0E, 0xC0, 0x01, - 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0x33, 0x07, 0x03, 0x00, 0x00, 0x33, 0x1F, - 0x03, 0x00, 0x00, 0xF3, 0x3D, 0x03, 0x00, 0x00, 0xE3, 0x30, 0x03, 0x00, 0x00, 0x06, 0xA0, 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, - 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, // 174 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, // 175 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, - 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, - 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1F, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, - 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0xF8, 0x7F, 0x0C, 0x00, 0x00, 0xF8, 0x7F, - 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, - 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, // 177 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x70, 0x03, - 0x00, 0x00, 0xC0, 0x39, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x03, 0x00, 0x00, 0x00, 0x07, + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, + 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0x33, + 0x07, 0x03, 0x00, 0x00, 0x33, 0x1F, 0x03, 0x00, 0x00, 0xF3, 0x3D, 0x03, 0x00, 0x00, 0xE3, 0x30, 0x03, 0x00, 0x00, 0x06, 0xA0, + 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, + 0x00, 0x00, 0xC0, 0x0F, // 174 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, + 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0xC0, 0x01, // 175 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, + 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1F, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, + 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0xF8, + 0x7F, 0x0C, 0x00, 0x00, 0xF8, 0x7F, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, + 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, // 177 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0xE0, + 0x03, 0x00, 0x00, 0xC0, 0x70, 0x03, 0x00, 0x00, 0xC0, 0x39, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x03, 0x00, 0x00, 0x00, 0x07, 0x03, // 178 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, - 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x9C, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xF3, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, - 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x10, // 180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, - 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 181 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, - 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, - 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, - 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, - 0x00, 0x00, 0x00, 0x80, 0x0F, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x30, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, // 184 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, + 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x9C, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, + 0x00, 0x00, 0x80, 0xF3, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x10, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, + 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, + 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x0E, // 181 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x80, + 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, + 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, + 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, // 184 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0xFF, + 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, // 185 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x30, 0x00, - 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, - 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x7E, - 0x30, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, - 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, - 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, - 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 187 - 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, - 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x18, 0x00, 0x00, 0xC0, 0x06, 0x1E, 0x00, 0x00, 0x00, 0x06, - 0x1B, 0x00, 0x00, 0x00, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, - 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, 0x30, 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0xC0, 0x00, + 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0x80, 0xC3, 0x31, + 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x7E, 0x30, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, + 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, + 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 187 + 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, + 0xFF, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x18, 0x00, 0x00, 0xC0, + 0x06, 0x1E, 0x00, 0x00, 0x00, 0x06, 0x1B, 0x00, 0x00, 0x00, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, + 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 188 - 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, - 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x66, 0xC0, 0x00, 0x00, 0x00, 0x36, - 0xE0, 0x00, 0x00, 0x00, 0x33, 0xF0, 0x00, 0x00, 0x00, 0x33, 0xF8, 0x00, 0x00, 0x00, 0x33, 0xDC, 0x00, 0x00, 0x00, 0x73, 0xCE, 0x00, 0x00, 0x80, - 0xE1, 0xC7, 0x00, 0x00, 0x80, 0xC1, 0xC1, // 189 - 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x60, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x0C, 0x00, - 0x00, 0x30, 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x18, 0x00, 0x30, 0xE7, 0x06, 0x1E, 0x00, 0xE0, 0x7F, 0x06, - 0x1B, 0x00, 0xE0, 0x3C, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, - 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, + 0xFF, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, + 0x66, 0xC0, 0x00, 0x00, 0x00, 0x36, 0xE0, 0x00, 0x00, 0x00, 0x33, 0xF0, 0x00, 0x00, 0x00, 0x33, 0xF8, 0x00, 0x00, 0x00, 0x33, + 0xDC, 0x00, 0x00, 0x00, 0x73, 0xCE, 0x00, 0x00, 0x80, 0xE1, 0xC7, 0x00, 0x00, 0x80, 0xC1, 0xC1, // 189 + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x60, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0x30, + 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x18, 0x00, 0x30, 0xE7, + 0x06, 0x1E, 0x00, 0xE0, 0x7F, 0x06, 0x1B, 0x00, 0xE0, 0x3C, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, + 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xFE, - 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0xF0, 0xFC, 0x81, 0x03, 0x00, 0xF0, 0xFC, - 0x80, 0x03, 0x00, 0xF0, 0x7C, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, - 0x00, 0xE0, // 191 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, - 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 192 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, - 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 193 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0xC2, 0x03, 0x70, - 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 194 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x0C, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0xCE, 0x03, 0x70, - 0x00, 0x00, 0xCC, 0x3F, 0x70, 0x00, 0x00, 0x8C, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 195 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, - 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 196 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, - 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xC3, 0x01, 0x70, - 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 197 - 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x80, 0xFF, 0x77, 0x00, 0x00, 0xC0, 0x7F, 0x70, 0x00, 0x00, 0xC0, 0x07, 0x70, 0x00, 0x00, 0xC0, 0x01, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, + 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0xF0, + 0xFC, 0x81, 0x03, 0x00, 0xF0, 0xFC, 0x80, 0x03, 0x00, 0xF0, 0x7C, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, // 191 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xCE, 0x3F, + 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 192 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0xD8, 0x3F, + 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0x02, 0xFC, 0x7F, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 193 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, + 0x70, 0x00, 0x00, 0xC2, 0x03, 0x70, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0x18, 0xFC, 0x7F, + 0x00, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 194 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x0C, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, + 0x70, 0x00, 0x00, 0xCE, 0x03, 0x70, 0x00, 0x00, 0xCC, 0x3F, 0x70, 0x00, 0x00, 0x8C, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, + 0x00, 0x00, 0x06, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 195 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, + 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, + 0x00, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 196 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0xC3, 0x1F, + 0x70, 0x00, 0x00, 0xC3, 0x01, 0x70, 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0x7E, 0xFE, 0x7F, + 0x00, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x0C, // 197 + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x77, 0x00, 0x00, 0xC0, 0x7F, 0x70, 0x00, 0x00, 0xC0, 0x07, 0x70, 0x00, 0x00, 0xC0, 0x01, + 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 198 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, - 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x03, 0xC0, 0x01, 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, - 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x3E, 0x03, 0xC0, 0x01, 0x00, 0xFE, 0x03, 0xC0, 0x01, 0x00, 0xCE, 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, - 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 199 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, - 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 200 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, - 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 201 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xD0, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, - 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 202 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, - 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 203 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, - 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, - 0x0F, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 204 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, - 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, - 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 205 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, - 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0xFF, 0xFF, - 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 206 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, - 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, - 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, - 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 207 - 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, - 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 208 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, - 0x00, 0xC7, 0x07, 0x00, 0x00, 0x00, 0x03, 0x3F, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0x00, 0x00, 0x06, 0x80, 0x1F, - 0x00, 0x00, 0x06, 0x00, 0xFC, 0x00, 0x00, 0x06, 0x00, 0xF0, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0x00, 0xC3, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, - 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 209 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, - 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 210 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, - 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 211 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, - 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x8C, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, - 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0x86, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 213 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, - 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, - 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 214 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0xE0, 0xC0, 0x01, - 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0xE0, 0xC0, 0x01, 0x00, 0x00, 0x70, - 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 215 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xF0, 0x3F, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, - 0x00, 0x80, 0x0F, 0xF0, 0x07, 0x00, 0x80, 0x03, 0x38, 0x07, 0x00, 0xC0, 0x01, 0x1C, 0x0E, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x81, 0x03, - 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xE1, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, 0x07, 0x00, 0x80, 0x1F, 0xC0, 0x07, 0x00, 0x00, 0xFE, - 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00, 0xC0, 0xF1, 0x3F, 0x00, 0x00, 0xC0, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, - 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x02, 0x00, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, - 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, - 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 217 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, - 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, - 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x00, 0x02, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, - 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, - 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, - 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, - 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 219 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, - 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, - 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 220 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, - 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x10, 0xF8, 0x01, 0x00, 0x00, 0x18, 0xE0, 0xFF, 0x0F, 0x00, 0x1C, 0x80, 0xFF, - 0x0F, 0x00, 0x0E, 0xE0, 0xFF, 0x0F, 0x00, 0x06, 0xF8, 0x01, 0x00, 0x00, 0x02, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, - 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, - 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, 0x00, 0xF8, - 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0xFF, 0x0F, - 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0xE0, 0x01, 0x0E, 0x00, 0xE0, 0xF0, 0x03, - 0x0E, 0x00, 0xE0, 0xF8, 0x07, 0x0E, 0x00, 0xE0, 0x19, 0x06, 0x0E, 0x00, 0xC0, 0x0F, 0x0E, 0x0E, 0x00, 0x80, 0x0F, 0x1C, 0x0F, 0x00, 0x00, 0x0E, - 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, + 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x03, 0xC0, 0x01, + 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x3E, 0x03, 0xC0, 0x01, 0x00, 0xFE, 0x03, 0xC0, 0x01, 0x00, + 0xCE, 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 199 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, + 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, + 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, + 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, + 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, + 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 202 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, + 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, + 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0xFF, + 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 204 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, + 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0xFF, + 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, + 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 205 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, + 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0xFF, + 0xFF, 0x0F, 0x00, 0xC2, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, + 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 206 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, + 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, + 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 207 + 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, + 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, + 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 208 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, + 0xFF, 0xFF, 0x0F, 0x00, 0xC7, 0x07, 0x00, 0x00, 0x00, 0x03, 0x3F, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x03, 0xE0, + 0x07, 0x00, 0x00, 0x06, 0x80, 0x1F, 0x00, 0x00, 0x06, 0x00, 0xFC, 0x00, 0x00, 0x06, 0x00, 0xF0, 0x03, 0x00, 0x07, 0x00, 0x80, + 0x0F, 0x00, 0xC3, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 209 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, + 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, + 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 210 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, + 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0x82, 0x03, 0x00, + 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, + 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0x98, 0x03, 0x00, + 0x07, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x8C, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, + 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, + 0x07, 0x00, 0x86, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, + 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, + 0x07, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 214 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, + 0xE0, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0xC0, 0xE1, + 0x00, 0x00, 0x00, 0xE0, 0xC0, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 215 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xF0, 0x3F, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xF0, 0x07, 0x00, 0x80, 0x03, 0x38, 0x07, 0x00, 0xC0, 0x01, 0x1C, 0x0E, 0x00, 0xC0, 0x01, + 0x06, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xE1, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, + 0x07, 0x00, 0x80, 0x1F, 0xC0, 0x07, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00, 0xC0, 0xF1, 0x3F, 0x00, + 0x00, 0xC0, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, + 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x02, 0x00, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, + 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, + 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 217 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, + 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, + 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x00, 0x02, 0x00, 0x80, + 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, + 0xFF, 0xFF, 0x07, 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, + 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x80, + 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, + 0xFF, 0xFF, 0x07, 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x80, + 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 220 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x10, 0xF8, 0x01, 0x00, 0x00, 0x18, 0xE0, + 0xFF, 0x0F, 0x00, 0x1C, 0x80, 0xFF, 0x0F, 0x00, 0x0E, 0xE0, 0xFF, 0x0F, 0x00, 0x06, 0xF8, 0x01, 0x00, 0x00, 0x02, 0xFC, 0x00, + 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, + 0x00, 0x40, // 221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, + 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x78, + 0x00, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, + 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0xE0, + 0x01, 0x0E, 0x00, 0xE0, 0xF0, 0x03, 0x0E, 0x00, 0xE0, 0xF8, 0x07, 0x0E, 0x00, 0xE0, 0x19, 0x06, 0x0E, 0x00, 0xC0, 0x0F, 0x0E, + 0x0E, 0x00, 0x80, 0x0F, 0x1C, 0x0F, 0x00, 0x00, 0x0E, 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x01, // 223 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x10, 0xE0, 0xF8, 0x07, - 0x00, 0x30, 0xE0, 0x38, 0x0F, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, - 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, - 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, - 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x06, 0x00, 0x70, 0x70, 0x1C, 0x07, 0x00, 0x30, 0xE0, 0x9C, 0x01, 0x00, 0x10, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 225 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE2, 0xF8, 0x07, - 0x00, 0x80, 0xE3, 0x38, 0x0F, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, - 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x73, 0x1C, 0x07, 0x00, 0x00, 0xE2, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 226 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0xC0, 0xE1, 0xF8, 0x07, - 0x00, 0xE0, 0xE1, 0x38, 0x0F, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, - 0x0E, 0x00, 0x80, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x71, 0x1C, 0x07, 0x00, 0xE0, 0xE1, 0x9C, 0x01, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 227 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, - 0x00, 0xE0, 0xE0, 0x38, 0x0F, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, - 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0xE0, 0x70, 0x1C, 0x07, 0x00, 0xE0, 0xE0, 0x9C, 0x01, 0x00, 0xE0, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 228 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, - 0x00, 0xF0, 0xE0, 0x38, 0x0F, 0x00, 0xF8, 0x71, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, - 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x06, 0x00, 0xF8, 0x71, 0x1C, 0x07, 0x00, 0xF0, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 229 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x3C, 0x0F, - 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, - 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, - 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1F, 0x0E, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x07, // 230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, - 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x03, 0x00, 0x70, 0x00, - 0x0E, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x3E, 0x03, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x70, 0x00, 0xCE, 0x01, 0x00, 0xE0, - 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 231 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x10, 0xC0, 0xFF, 0x03, - 0x00, 0x30, 0xE0, 0x9D, 0x07, 0x00, 0x70, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, - 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, - 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 232 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, - 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, - 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0xF0, 0x1C, 0x0E, 0x00, 0x10, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, - 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 233 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC2, 0xFF, 0x03, - 0x00, 0x80, 0xE3, 0x9D, 0x07, 0x00, 0xC0, 0xE1, 0x1C, 0x07, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, - 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0xF2, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, - 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 234 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, - 0x00, 0xE0, 0xE0, 0x9D, 0x07, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, - 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0xF0, 0x1C, 0x0E, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, - 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 235 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x70, 0x00, 0x0E, - 0x00, 0x30, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0xF3, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0xFF, - 0x0F, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 236 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, - 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF3, 0xFF, 0x0F, 0x00, 0xC0, 0xF3, 0xFF, - 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 237 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, - 0x00, 0x80, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0xF0, 0x70, 0x00, 0x0E, 0x00, 0x30, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0xF0, 0xFF, - 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 238 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, - 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, - 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 239 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0xE6, 0xFF, 0x07, - 0x00, 0x60, 0xE2, 0x81, 0x07, 0x00, 0xE0, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x77, 0x00, 0x0E, 0x00, 0x80, 0x7F, 0x00, - 0x0E, 0x00, 0x80, 0x7E, 0x00, 0x0E, 0x00, 0xC0, 0x7C, 0x00, 0x0F, 0x00, 0xC0, 0xF8, 0x81, 0x07, 0x00, 0x40, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, // 240 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, - 0x00, 0xE0, 0xF1, 0xFF, 0x0F, 0x00, 0x60, 0xC0, 0x01, 0x00, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, - 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xF1, 0x00, 0x00, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, - 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 241 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x10, 0xE0, 0xFF, 0x07, - 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, - 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 242 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, - 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, - 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 243 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, - 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, - 0x0E, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 244 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xC0, 0xE1, 0xFF, 0x07, - 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xF0, 0x00, 0x0F, 0x00, 0x60, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, - 0x0E, 0x00, 0x80, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF1, 0x00, 0x0F, 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 245 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xE0, 0xE0, 0xFF, 0x07, - 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, - 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 246 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, - 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 247 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x7F, 0x0E, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x03, - 0x00, 0x00, 0xE0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0xE0, 0x07, 0x00, 0x00, 0x70, 0x70, 0x0E, 0x00, 0x00, 0x70, 0x38, 0x0E, 0x00, 0x00, 0x70, 0x1C, - 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x00, 0xE0, 0x83, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, - 0xFF, 0x03, 0x00, 0x00, 0x38, 0xFE, 0x00, 0x00, 0x00, 0x10, // 248 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x07, - 0x00, 0x30, 0xF0, 0xFF, 0x07, 0x00, 0x70, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, - 0x0E, 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, - 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, - 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x30, 0x00, 0x80, 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, - 0x00, 0x00, 0xF2, 0xFF, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, - 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x07, 0x00, 0x80, 0x03, 0x80, 0x01, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 251 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, - 0x00, 0xE0, 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x80, 0x01, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, - 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 252 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, - 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x02, 0xFC, 0xF3, 0x01, 0x00, 0x03, 0xF0, 0xFF, 0x01, 0xC0, 0x03, 0xC0, - 0x7F, 0x00, 0xE0, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x30, 0x80, 0x7F, 0x00, 0x00, 0x10, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, - 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 253 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, - 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, - 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, - 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 254 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, - 0x03, 0xE0, 0xE0, 0x1F, 0x80, 0x03, 0xE0, 0x80, 0x7F, 0xC0, 0x03, 0xE0, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, - 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F, 0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, - 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x10, + 0xE0, 0xF8, 0x07, 0x00, 0x30, 0xE0, 0x38, 0x0F, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, + 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, + 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, + 0xE0, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, + 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x06, 0x00, 0x70, 0x70, 0x1C, 0x07, 0x00, 0x30, 0xE0, 0x9C, + 0x01, 0x00, 0x10, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, + 0xE2, 0xF8, 0x07, 0x00, 0x80, 0xE3, 0x38, 0x0F, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, + 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x73, 0x1C, 0x07, 0x00, 0x00, 0xE2, 0x9C, + 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0xC0, + 0xE1, 0xF8, 0x07, 0x00, 0xE0, 0xE1, 0x38, 0x0F, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, + 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x71, 0x1C, 0x07, 0x00, 0xE0, 0xE1, 0x9C, + 0x01, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 227 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, + 0xE0, 0xF8, 0x07, 0x00, 0xE0, 0xE0, 0x38, 0x0F, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, + 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0xE0, 0x70, 0x1C, 0x07, 0x00, 0xE0, 0xE0, 0x9C, + 0x01, 0x00, 0xE0, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, + 0xE0, 0xF8, 0x07, 0x00, 0xF0, 0xE0, 0x38, 0x0F, 0x00, 0xF8, 0x71, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, + 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x06, 0x00, 0xF8, 0x71, 0x1C, 0x07, 0x00, 0xF0, 0xE0, 0x9C, + 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 229 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, + 0x70, 0x3C, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0F, 0x00, 0x00, 0xE0, + 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, + 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1F, 0x0E, 0x00, 0x00, 0xE0, 0x1F, 0x07, + 0x00, 0x00, 0x80, 0x1F, 0x07, // 230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, + 0x00, 0x0F, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x3E, 0x03, 0x00, 0x70, 0x00, + 0xFE, 0x03, 0x00, 0x70, 0x00, 0xCE, 0x01, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 231 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x10, + 0xC0, 0xFF, 0x03, 0x00, 0x30, 0xE0, 0x9D, 0x07, 0x00, 0x70, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, + 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, + 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, + 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, + 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0xF0, 0x1C, + 0x0E, 0x00, 0x10, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, + 0xC2, 0xFF, 0x03, 0x00, 0x80, 0xE3, 0x9D, 0x07, 0x00, 0xC0, 0xE1, 0x1C, 0x07, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, + 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0xF2, 0x1C, + 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, + 0xC0, 0xFF, 0x03, 0x00, 0xE0, 0xE0, 0x9D, 0x07, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, + 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0xF0, 0x1C, + 0x0E, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 235 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x10, + 0x70, 0x00, 0x0E, 0x00, 0x30, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0xF3, + 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0xFF, 0x0F, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 236 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF3, + 0xFF, 0x0F, 0x00, 0xC0, 0xF3, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, + 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x72, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0xF0, 0x70, 0x00, 0x0E, 0x00, 0x30, 0xF0, + 0xFF, 0x0F, 0x00, 0xF0, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 238 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, + 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, + 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, + 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 239 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, + 0xE6, 0xFF, 0x07, 0x00, 0x60, 0xE2, 0x81, 0x07, 0x00, 0xE0, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x77, + 0x00, 0x0E, 0x00, 0x80, 0x7F, 0x00, 0x0E, 0x00, 0x80, 0x7E, 0x00, 0x0E, 0x00, 0xC0, 0x7C, 0x00, 0x0F, 0x00, 0xC0, 0xF8, 0x81, + 0x07, 0x00, 0x40, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, + 0xF1, 0xFF, 0x0F, 0x00, 0xE0, 0xF1, 0xFF, 0x0F, 0x00, 0x60, 0xC0, 0x01, 0x00, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x60, + 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xF1, 0x00, + 0x00, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x10, + 0xE0, 0xFF, 0x07, 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x73, + 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, + 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 242 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, + 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0x73, + 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0x30, 0xE0, 0x81, + 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 243 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, + 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x70, 0x70, + 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0x00, 0xE2, 0x81, + 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 244 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xC0, + 0xE1, 0xFF, 0x07, 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xF0, 0x00, 0x0F, 0x00, 0x60, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, + 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF1, 0x00, 0x0F, 0x00, 0xE0, 0xE1, 0x81, + 0x07, 0x00, 0x60, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 245 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xE0, + 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0xE0, 0x81, + 0x07, 0x00, 0xE0, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, + 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 247 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x7F, 0x0E, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, + 0xE0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0xE0, 0x07, 0x00, 0x00, 0x70, 0x70, 0x0E, 0x00, 0x00, 0x70, + 0x38, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x00, 0xE0, 0x83, + 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x38, 0xFE, 0x00, 0x00, 0x00, 0x10, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x10, + 0xF0, 0xFF, 0x07, 0x00, 0x30, 0xF0, 0xFF, 0x07, 0x00, 0x70, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, + 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, + 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x03, + 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x30, 0x00, 0x80, + 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, + 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF2, 0xFF, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xF0, 0x00, + 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x07, 0x00, 0x80, 0x03, 0x80, + 0x01, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, + 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x80, + 0x01, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, + 0xF0, 0x03, 0x80, 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x02, 0xFC, 0xF3, 0x01, 0x00, 0x03, + 0xF0, 0xFF, 0x01, 0xC0, 0x03, 0xC0, 0x7F, 0x00, 0xE0, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x30, 0x80, 0x7F, + 0x00, 0x00, 0x10, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 253 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, + 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, + 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, + 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, + 0xF0, 0x03, 0x80, 0x03, 0xE0, 0xE0, 0x1F, 0x80, 0x03, 0xE0, 0x80, 0x7F, 0xC0, 0x03, 0xE0, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, + 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F, + 0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255 }; #endif // USE_EINK diff --git a/src/graphics/fonts/OLEDDisplayFontsCS.cpp b/src/graphics/fonts/OLEDDisplayFontsCS.cpp index f78ea13f5..c8045285e 100644 --- a/src/graphics/fonts/OLEDDisplayFontsCS.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsCS.cpp @@ -234,220 +234,219 @@ const uint8_t ArialMT_Plain_10_CS[] PROGMEM = { 0x08, 0x0A, 0x0A, 0x06, // 254 0x08, 0x14, 0x09, 0x06, // 255 // Font Data: - 0x00, 0x00, 0xF8, 0x02, // 33 - 0x38, 0x00, 0x00, 0x00, 0x38, // 34 - 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 - 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 - 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 - 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 - 0x38, // 39 - 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 - 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 - 0x28, 0x00, 0x18, 0x00, 0x28, // 42 - 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 - 0x00, 0x00, 0x00, 0x06, // 44 - 0x80, 0x00, 0x80, // 45 - 0x00, 0x00, 0x00, 0x02, // 46 - 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 - 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 - 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 - 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 - 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 - 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 - 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 - 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 - 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 - 0x00, 0x00, 0x20, 0x02, // 58 - 0x00, 0x00, 0x20, 0x06, // 59 - 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 - 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 - 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 - 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 - 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, - 0x04, // 64 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 - 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 - 0x00, 0x00, 0xF8, 0x03, // 73 - 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 - 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 - 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 - 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 - 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 - 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 - 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 - 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 - 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 - 0x08, 0x08, 0xF8, 0x0F, // 93 - 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 - 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 - 0x08, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 - 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 - 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 - 0x00, 0x00, 0xE8, 0x03, // 105 - 0x00, 0x08, 0xE8, 0x07, // 106 - 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 - 0x00, 0x00, 0xF8, 0x03, // 108 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 - 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 - 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 - 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 - 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 - 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 - 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 - 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 - 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 - 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 - 0x00, 0x00, 0xF8, 0x0F, // 124 - 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 - 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 - 0x00, 0x00, 0xF0, 0x01, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 129 - 0x00, 0x00, 0xF8, 0x03, 0x09, 0x02, 0x0A, 0x02, 0x11, 0x01, 0xE0, // 130 - 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 131 - 0x00, 0x00, 0xF8, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 132 - 0x00, 0x00, 0xF8, 0x03, 0x49, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 133 - 0x00, 0x00, 0x30, 0x01, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 134 - 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0xFA, 0x03, 0x09, 0x00, 0x08, // 135 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x05, 0x02, 0x02, 0x02, 0xF8, 0x01, // 136 - 0x08, 0x03, 0x88, 0x02, 0xC9, 0x02, 0x6A, 0x02, 0x39, 0x02, 0x18, 0x02, // 137 - 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0x44, 0x01, // 138 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, 0x00, 0x00, 0x18, // 139 - 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC4, 0x02, // 140 - 0x00, 0x00, 0xE0, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 141 - 0x00, 0x00, 0xE4, 0x03, 0x28, 0x00, 0x04, // 142 - 0x40, 0x02, 0xA4, 0x02, 0xA8, 0x02, 0x24, 0x01, // 143 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x18, // 144 - 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x14, 0x02, 0xE8, 0x03, // 145 - 0x20, 0x02, 0x24, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 146 - 0x00, 0x00, 0xFA, 0x03, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 147 - 0x00, 0x00, 0xFA, 0x03, 0x01, // 148 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x18, 0x02, 0x00, 0x02, // 149 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x18, // 150 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 151 - 0x00, 0x00, 0xE0, 0x03, 0x28, 0x00, 0x04, // 152 - 0x00, 0x00, 0xA0, 0x0F, // 161 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 - 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 - 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 - 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 - 0x00, 0x00, 0x38, 0x0F, // 166 - 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 - 0x08, 0x00, 0x00, 0x00, 0x08, // 168 - 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 - 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 - 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 - 0x80, 0x00, 0x80, // 173 - 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 - 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 - 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 - 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 - 0x48, 0x00, 0x68, 0x00, 0x58, // 178 - 0x48, 0x00, 0x58, 0x00, 0x68, // 179 - 0x00, 0x00, 0x10, 0x00, 0x08, // 180 - 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 - 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 - 0x00, 0x00, 0x40, // 183 - 0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 - 0x00, 0x00, 0x10, 0x00, 0x78, // 185 - 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 - 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 - 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 - 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 - 0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 - 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 - 0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 - 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 - 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 - 0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 - 0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 - 0x00, 0x00, 0xF9, 0x03, 0x02, // 204 - 0x02, 0x00, 0xF9, 0x03, // 205 - 0x01, 0x00, 0xFA, 0x03, // 206 - 0x02, 0x00, 0xF8, 0x03, 0x02, // 207 - 0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 - 0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 - 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 - 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 - 0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 - 0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 - 0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 - 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 - 0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 - 0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 - 0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 - 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 - 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 - 0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 - 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 - 0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 - 0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 - 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 - 0x00, 0x00, 0xE4, 0x03, 0x08, // 236 - 0x08, 0x00, 0xE4, 0x03, // 237 - 0x08, 0x00, 0xE4, 0x03, 0x08, // 238 - 0x08, 0x00, 0xE0, 0x03, 0x08, // 239 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 - 0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 - 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 - 0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 - 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 - 0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 - 0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 - 0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 - 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 - 0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 - 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 - 0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 - 0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 - 0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xF0, 0x01, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 129 + 0x00, 0x00, 0xF8, 0x03, 0x09, 0x02, 0x0A, 0x02, 0x11, 0x01, 0xE0, // 130 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 131 + 0x00, 0x00, 0xF8, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 132 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 133 + 0x00, 0x00, 0x30, 0x01, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 134 + 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0xFA, 0x03, 0x09, 0x00, 0x08, // 135 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x05, 0x02, 0x02, 0x02, 0xF8, 0x01, // 136 + 0x08, 0x03, 0x88, 0x02, 0xC9, 0x02, 0x6A, 0x02, 0x39, 0x02, 0x18, 0x02, // 137 + 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0x44, 0x01, // 138 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, 0x00, 0x00, 0x18, // 139 + 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC4, 0x02, // 140 + 0x00, 0x00, 0xE0, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 141 + 0x00, 0x00, 0xE4, 0x03, 0x28, 0x00, 0x04, // 142 + 0x40, 0x02, 0xA4, 0x02, 0xA8, 0x02, 0x24, 0x01, // 143 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x18, // 144 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x14, 0x02, 0xE8, 0x03, // 145 + 0x20, 0x02, 0x24, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 146 + 0x00, 0x00, 0xFA, 0x03, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 147 + 0x00, 0x00, 0xFA, 0x03, 0x01, // 148 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x18, 0x02, 0x00, 0x02, // 149 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x18, // 150 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 151 + 0x00, 0x00, 0xE0, 0x03, 0x28, 0x00, 0x04, // 152 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x08, 0x00, 0x00, 0x00, 0x08, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x48, 0x00, 0x68, 0x00, 0x58, // 178 + 0x48, 0x00, 0x58, 0x00, 0x68, // 179 + 0x00, 0x00, 0x10, 0x00, 0x08, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 + 0x00, 0x00, 0x10, 0x00, 0x78, // 185 + 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 + 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 + 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 + 0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 + 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 + 0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 + 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 + 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 + 0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 + 0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 + 0x00, 0x00, 0xF9, 0x03, 0x02, // 204 + 0x02, 0x00, 0xF9, 0x03, // 205 + 0x01, 0x00, 0xFA, 0x03, // 206 + 0x02, 0x00, 0xF8, 0x03, 0x02, // 207 + 0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 + 0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 + 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 + 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 + 0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 + 0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 + 0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 + 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 + 0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 + 0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 + 0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 + 0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 + 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 + 0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 + 0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 + 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 + 0x00, 0x00, 0xE4, 0x03, 0x08, // 236 + 0x08, 0x00, 0xE4, 0x03, // 237 + 0x08, 0x00, 0xE4, 0x03, 0x08, // 238 + 0x08, 0x00, 0xE0, 0x03, 0x08, // 239 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 + 0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 + 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 + 0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 + 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 + 0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 + 0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 + 0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 + 0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 + 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 + 0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 + 0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 }; const uint8_t ArialMT_Plain_16_CS[] PROGMEM = { @@ -683,344 +682,354 @@ const uint8_t ArialMT_Plain_16_CS[] PROGMEM = { // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 - 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, - 0x80, 0x08, // 35 - 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 36 - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, - 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, - 0x3C, // 37 - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x28, 0x00, 0x00, 0x44, // 38 - 0x00, 0x00, 0x00, 0x78, // 39 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 - 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 - 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 - 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 - 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 - 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, + 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, + 0x20, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, + 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, + 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, + 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, + 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, + 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, + 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x08, 0x07, 0x00, 0xC8, 0x00, 0x00, 0x28, 0x00, 0x00, - 0x18, // 55 - 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 - 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 - 0x00, 0x00, 0x00, 0x40, 0x40, // 58 - 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 - 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, + 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, + 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, - 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 67 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 68 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 69 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, // 70 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, - 0x0E, // 71 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, - 0x3F, // 74 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, - 0x10, 0x20, 0x00, 0x08, 0x40, // 75 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 79 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x10, 0x01, 0x00, 0xE0, // 80 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, - 0x4F, // 81 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, - 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 84 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, - 0xE0, 0x00, 0x00, 0x18, // 86 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, + 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, + 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, + 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, + 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, - 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, - 0x40, // 88 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x08, // 89 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, - 0x08, 0x40, // 90 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 - 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0x01, // 94 - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x02, // 95 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, - 0x20, // 99 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, - 0x7F, // 104 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, - 0x7F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, - 0x03, // 113 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, - 0x38, // 115 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, - 0x7F, // 117 - 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, - 0xC0, // 118 - 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, - 0x40, // 120 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 121 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, - 0x40, // 122 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 - 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 129 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 130 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 131 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x81, 0x00, 0x00, 0x02, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x7F, // 132 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x09, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x0A, 0x06, 0x00, 0x09, 0x1A, 0x00, - 0x10, 0x21, 0x00, 0xE0, 0x40, // 133 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x42, 0x00, 0x09, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x1C, // 134 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 135 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x06, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x06, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 136 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x09, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x8A, 0x40, 0x00, 0x69, 0x40, 0x00, 0x18, 0x40, 0x00, - 0x08, 0x40, // 137 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, - 0x20, // 138 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x00, 0x00, 0x38, // 139 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x88, 0x24, 0x00, 0x00, 0x17, // 140 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x88, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, - 0x7F, // 141 - 0x00, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 142 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, - 0x38, // 143 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, // 144 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x0C, 0x40, 0x00, 0x12, 0x40, 0x00, 0x12, 0x40, 0x00, 0x0C, 0x20, 0x00, 0xC0, - 0x7F, // 145 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x48, 0x58, 0x00, 0x50, 0x44, 0x00, 0x50, 0x43, 0x00, 0xC8, 0x40, 0x00, 0x40, - 0x40, // 146 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 147 - 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 148 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x38, 0x40, 0x00, 0x00, 0x40, // 149 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 150 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x09, 0x06, 0x00, 0x08, 0x1A, 0x00, - 0x10, 0x21, 0x00, 0xE0, 0x40, // 151 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 152 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 - 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 - 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, - 0x0B, // 164 - 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, - 0x08, 0x0A, // 165 - 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 - 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, - 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 - 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, - 0x20, // 171 - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, - 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 - 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x04, // 175 - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 - 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 - 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 - 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, - 0x08, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 - 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, - 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, - 0x20, // 188 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, - 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, - 0x4C, // 189 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, - 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, - 0x20, // 190 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, - 0x00, 0xC0, // 191 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, 0x82, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, 0x81, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 - 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, 0x08, 0x04, 0x00, - 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, // 198 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, 0x08, 0x40, 0x03, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 199 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 200 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 201 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 202 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 203 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, + 0x18, 0x40, 0x00, 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, + 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, + 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xC0, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, + 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 129 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, + 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 130 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x41, 0x00, + 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 131 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x81, 0x00, 0x00, 0x02, 0x03, 0x00, 0x02, 0x04, 0x00, + 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 132 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x09, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x0A, 0x06, 0x00, + 0x09, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 133 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x42, 0x00, + 0x09, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 134 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 135 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x06, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, + 0x06, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 136 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x09, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x8A, 0x40, 0x00, 0x69, 0x40, 0x00, + 0x18, 0x40, 0x00, 0x08, 0x40, // 137 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, // 138 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 139 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x88, 0x24, 0x00, + 0x00, 0x17, // 140 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x88, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 141 + 0x00, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 142 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x38, // 143 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, // 144 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x0C, 0x40, 0x00, 0x12, 0x40, 0x00, 0x12, 0x40, 0x00, 0x0C, 0x20, 0x00, 0xC0, 0x7F, // 145 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x48, 0x58, 0x00, 0x50, 0x44, 0x00, 0x50, 0x43, 0x00, 0xC8, 0x40, 0x00, 0x40, 0x40, // 146 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, // 147 + 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 148 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x38, 0x40, 0x00, + 0x00, 0x40, // 149 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 150 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x09, 0x06, 0x00, + 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 151 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 152 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, + 0x00, 0x11, // 162 + 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, + 0x20, 0x20, // 163 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 + 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, + 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, + 0x00, 0x0C, // 167 + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, + 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x04, // 175 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, // 177 + 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, + 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0xF8, 0xFF, 0x03, 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, + 0x00, 0x04, // 187 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, + 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x01, 0x00, 0xC0, // 191 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, + 0x82, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, + 0x81, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 + 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, + 0x08, 0x04, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, // 198 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, + 0x08, 0x40, 0x03, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 199 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 200 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 201 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, + 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 202 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 203 0x01, 0x00, 0x00, 0xFA, 0x7F, // 204 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 205 0x02, 0x00, 0x00, 0xF9, 0x7F, 0x00, 0x01, 0x00, 0x00, 0x02, // 206 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 207 - 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 208 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 210 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 211 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 212 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 213 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 214 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 215 - 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, 0xC8, 0x40, 0x00, - 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, - 0x0F, // 216 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x08, // 221 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, - 0x40, 0x08, 0x00, 0x80, 0x07, // 222 - 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 223 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 225 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, 0x80, 0x7F, // 226 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, 0x80, 0x7F, // 227 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 228 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 229 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x3F, 0x00, - 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, - 0x20, // 231 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 232 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 233 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 234 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 235 - 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 - 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 - 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 - 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, 0x00, 0x1F, // 240 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, - 0x7F, // 241 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 242 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 243 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, 0x00, 0x1F, // 244 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, 0x00, 0x1F, // 245 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 246 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 247 - 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, 0x40, 0x1F, // 248 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, - 0x7F, // 249 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, - 0x7F, // 250 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, - 0x7F, // 251 - 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, - 0x7F, // 252 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 253 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 254 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 255 + 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 208 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, + 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 210 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 211 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, + 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 212 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, + 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 213 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 214 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, + 0x40, 0x10, // 215 + 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, + 0xC8, 0x40, 0x00, 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, 0x0F, // 216 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, + 0x02, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x08, // 221 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, + 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 222 + 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x38, // 223 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 224 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 225 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, + 0x80, 0x7F, // 226 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, + 0x80, 0x7F, // 227 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 228 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 229 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x3F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, 0x20, // 231 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 232 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 233 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, + 0x00, 0x17, // 234 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 235 + 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 + 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 + 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 + 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, + 0x00, 0x1F, // 240 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 242 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 243 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, + 0x00, 0x1F, // 244 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, + 0x00, 0x1F, // 245 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 246 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x02, // 247 + 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, + 0x40, 0x1F, // 248 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 249 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, 0x7F, // 250 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, 0x7F, // 251 + 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 252 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 253 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 254 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 255 }; const uint8_t ArialMT_Plain_24_CS[] PROGMEM = { @@ -1254,542 +1263,604 @@ const uint8_t ArialMT_Plain_24_CS[] PROGMEM = { 0x26, 0x59, 0x2F, 0x0D, // 254 0x26, 0x88, 0x2A, 0x0C, // 255 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, - 0x33, // 33 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, - 0xE0, 0x07, // 34 - 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, - 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, - 0x00, 0x0C, 0x03, // 35 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, - 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, - 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, - 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, - 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, 0x3C, 0x30, 0x00, - 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, - 0x20, 0x00, 0x00, 0x04, // 40 - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, - 0x00, 0xF0, 0x0F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, - 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, - 0x80, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, - 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, - 0x00, 0x60, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 + 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, + 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, + 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, + 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, + 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, + 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, + 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, + 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, + 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, + 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x80, 0x01, // 45 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, - 0x60, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, - 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, - 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, - 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, - 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, - 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, - 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, - 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, - 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, - 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, - 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, - 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, - 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, - 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, - 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, - 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x60, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, + 0x00, 0x33, 0x00, 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, + 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, + 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, + 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x03, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, + 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, + 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, + 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, + 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, + 0x80, 0x3F, 0x00, 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, + 0x00, 0x00, 0x60, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, + 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, + 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, + 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, + 0x03, 0x00, 0x00, 0x03, 0x06, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, + 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, + 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x20, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, + 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, + 0x00, 0x00, 0x00, 0x07, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, + 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, + 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, + 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, + 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, + 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, - 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, - 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, - 0x0F, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, - 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, + 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, + 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, + 0x00, 0x00, 0xE2, 0x0F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, + 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, - 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, - 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, - 0x0F, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, - 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, - 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, + 0x00, 0xE0, 0xFF, 0x3F, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, + 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, + 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, + 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, + 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, - 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 - 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, - 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, - 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, - 0x20, // 86 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 - 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, - 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, - 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 - 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, - 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, - 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, - 0x06, // 91 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, - 0x00, 0x00, 0x30, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, - 0x07, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, + 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, + 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 + 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, + 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, + 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, + 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, + 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, + 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, + 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, + 0xFF, 0xFF, 0x07, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x00, 0x00, 0x06, // 95 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, - 0x00, 0xE0, 0x03, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, + 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, - 0x60, 0x06, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, - 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, - 0x00, 0xE0, 0x03, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, - 0x07, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, + 0x06, 0x00, 0x00, 0x60, 0x06, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, + 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, + 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, + 0x30, 0x00, 0x00, 0x00, 0x20, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, + 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 - 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0x0E, // 119 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, - 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, + 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, - 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, + 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, - 0x60, 0x00, 0x00, 0x06, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, + 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, - 0x00, 0x80, 0x01, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0x60, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, - 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, + 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 129 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0xE2, 0x00, 0x18, 0x00, - 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, + 0x30, 0x00, 0xE2, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 130 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 131 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, - 0x02, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x06, 0x80, 0x03, 0x00, 0x02, 0x00, 0x0F, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 132 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x62, 0x30, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, 0x6C, 0x70, 0x00, 0x00, 0x6C, 0xF0, 0x00, 0x00, 0x66, 0xF0, 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, - 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, + 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 131 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x02, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x06, 0x80, + 0x03, 0x00, 0x02, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 132 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x62, 0x30, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, 0x6C, 0x70, 0x00, 0x00, 0x6C, 0xF0, 0x00, 0x00, 0x66, 0xF0, + 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 133 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x62, 0x38, 0x38, 0x00, - 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, - 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 134 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0xEC, 0xFF, 0x3F, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0x66, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, // 135 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x0E, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x11, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 136 - 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x62, 0xC0, 0x31, 0x00, - 0x66, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x6C, 0x1C, 0x30, 0x00, 0x66, 0x0E, 0x30, 0x00, 0x62, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, - 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 137 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, - 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x62, + 0x38, 0x38, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, + 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 134 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x66, + 0x00, 0x00, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0x66, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 135 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x11, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x0E, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 136 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x62, + 0xC0, 0x31, 0x00, 0x66, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x6C, 0x1C, 0x30, 0x00, 0x66, 0x0E, 0x30, 0x00, 0x62, 0x07, + 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 137 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, + 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 138 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, - 0x01, // 139 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, - 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xCE, 0x38, 0x00, 0x20, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 140 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x20, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, - 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 141 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, - 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 142 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x20, 0xEE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, - 0xC0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 139 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, + 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xCE, 0x38, 0x00, 0x20, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x20, 0x18, 0x00, 0x00, 0x60, + 0x0C, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 141 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, + 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 142 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x20, 0xEE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, + 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 143 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, - 0x01, // 144 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x70, 0x00, 0x30, 0x00, - 0xD8, 0x00, 0x30, 0x00, 0x88, 0x00, 0x30, 0x00, 0xD8, 0x00, 0x18, 0x00, 0x70, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 145 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x20, 0x06, 0x37, 0x00, 0x60, 0xC6, 0x33, 0x00, - 0xC0, 0xE6, 0x30, 0x00, 0xC0, 0x76, 0x30, 0x00, 0x60, 0x3E, 0x30, 0x00, 0x20, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 144 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x70, + 0x00, 0x30, 0x00, 0xD8, 0x00, 0x30, 0x00, 0x88, 0x00, 0x30, 0x00, 0xD8, 0x00, 0x18, 0x00, 0x70, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 145 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x20, 0x06, 0x37, 0x00, 0x60, + 0xC6, 0x33, 0x00, 0xC0, 0xE6, 0x30, 0x00, 0xC0, 0x76, 0x30, 0x00, 0x60, 0x3E, 0x30, 0x00, 0x20, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 146 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, - 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 147 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, + 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, // 147 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, // 148 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x03, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x00, 0x00, 0x30, // 149 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, - 0xE0, 0x01, // 150 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x68, 0x70, 0x00, 0x00, 0x6E, 0xF0, 0x00, 0x00, 0x66, 0xF0, 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, - 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x03, 0x30, 0x00, 0xE0, 0x01, + 0x30, 0x00, 0x00, 0x00, 0x30, // 149 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x03, 0x00, 0x00, 0xE0, 0x01, // 150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x68, 0x70, 0x00, 0x00, 0x6E, 0xF0, 0x00, 0x00, 0x66, 0xF0, + 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 151 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x80, 0x0C, 0x00, 0x00, 0xE0, 0x06, 0x00, 0x00, - 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 152 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, - 0x07, // 161 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, - 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x80, 0x0C, 0x00, 0x00, 0xE0, + 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 152 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, + 0x06, 0x3F, 0x00, 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 - 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, - 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, - 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, - 0x00, 0x02, 0x04, // 164 - 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, - 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, - 0x20, // 165 + 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, + 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, + 0x38, 0x00, 0x00, 0x00, 0x10, // 163 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, + 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, + 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 + 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, + 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, + 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, - 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, // 168 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, - 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, - 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, - 0xC0, 0x3F, // 170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, - 0x10, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x80, 0x01, // 173 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, - 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, - 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0C, // 175 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, - 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, - 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, - 0x00, 0x60, 0x30, // 177 - 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, - 0xC0, 0x21, // 178 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, - 0xC0, 0x1D, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x20, // 180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 - 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, - 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, - 0x60, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, + 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, + 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, // 168 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, + 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, + 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, + 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, + 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, + 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, + 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, + 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, + 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 + 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, + 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, + 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x20, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 + 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, + 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, + 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, - 0x01, // 184 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, + 0x00, 0x00, 0x01, // 184 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, // 185 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, - 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, // 187 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, 0x3F, 0x1C, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, - 0x00, 0xC0, 0x21, // 189 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, - 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, - 0x00, 0x00, 0x08, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, 0x00, 0x07, 0x07, - 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, - 0x00, 0x00, 0xC0, // 191 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, - 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, - 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, 0x8F, 0x01, 0x00, - 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, 0x8F, 0x01, 0x00, - 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, - 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, - 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xBC, 0x01, 0x00, - 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, - 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, + 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, // 187 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, + 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, + 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, + 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, + 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, + 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, + 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, + 0x00, 0x07, 0x07, 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, // 191 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, + 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, + 0x8F, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, + 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, + 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, + 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, + 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, + 0x8F, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, + 0xBC, 0x01, 0x00, 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, + 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, + 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, + 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, + 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 199 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, + 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, + 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x08, // 204 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x02, // 205 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, // 206 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 207 - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, - 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 208 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, 0x03, 0x00, 0x00, - 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, - 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, - 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, 0x00, 0x18, 0x00, - 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, - 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, - 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0xF8, 0x00, 0x00, - 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, + 0x03, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, + 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, + 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, + 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, + 0x00, 0x18, 0x00, 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, + 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, + 0xF8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, 0x03, // 215 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, 0x00, 0x1F, 0x00, - 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, - 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, - 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, - 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, - 0xF8, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x08, 0x00, - 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, - 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, - 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, 0x86, 0x31, 0x00, - 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, - 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, - 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, 0x86, 0x31, 0x00, - 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, - 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, 0x06, 0x30, 0x02, - 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, + 0x00, 0x1F, 0x00, 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, + 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, + 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, + 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, + 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, + 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, + 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x08, 0x00, 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, + 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, + 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, + 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, + 0x86, 0x31, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, + 0x86, 0x31, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, + 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, + 0x86, 0x31, 0x00, 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, + 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, + 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, + 0x06, 0x30, 0x02, 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 231 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, - 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, 0xC6, 0x30, 0x00, - 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, - 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, + 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, + 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, + 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 0x80, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 238 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 239 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, 0x06, 0x30, 0x00, - 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, - 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, - 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, - 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, - 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, - 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, // 247 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x3F, 0x00, - 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, - 0x00, 0xF3, 0x07, // 248 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, - 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, + 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, + 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, + 0x0C, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, + 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, + 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, + 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, + 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 247 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x0E, 0x3F, 0x00, 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0x07, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, + 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, - 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, + 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 }; diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp index c3bb7b139..3a1159511 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -236,196 +236,195 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { 0x07, 0x7E, 0x0C, 0x07, // 255 // Font Data: - 0x00, 0x00, 0xF8, 0x02, // 33 - 0x38, 0x00, 0x00, 0x00, 0x38, // 34 - 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 - 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 - 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 - 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 - 0x38, // 39 - 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 - 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 - 0x28, 0x00, 0x18, 0x00, 0x28, // 42 - 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 - 0x00, 0x00, 0x00, 0x06, // 44 - 0x80, 0x00, 0x80, // 45 - 0x00, 0x00, 0x00, 0x02, // 46 - 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 - 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 - 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 - 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 - 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 - 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 - 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 - 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 - 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 - 0x00, 0x00, 0x20, 0x02, // 58 - 0x00, 0x00, 0x20, 0x06, // 59 - 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 - 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 - 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 - 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 - 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, - 0x04, // 64 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 - 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 - 0x00, 0x00, 0xF8, 0x03, // 73 - 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 - 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 - 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 - 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 - 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 - 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 - 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 - 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 - 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 - 0x08, 0x08, 0xF8, 0x0F, // 93 - 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 - 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 - 0x08, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 - 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 - 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 - 0x00, 0x00, 0xE8, 0x03, // 105 - 0x00, 0x08, 0xE8, 0x07, // 106 - 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 - 0x00, 0x00, 0xF8, 0x03, // 108 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 - 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 - 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 - 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 - 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 - 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 - 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 - 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 - 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 - 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 - 0x00, 0x00, 0xF8, 0x0F, // 124 - 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 - 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 - 0x00, 0x00, 0xA0, 0x0F, // 161 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 - 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 - 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 - 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 - 0x00, 0x00, 0x38, 0x0F, // 166 - 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 - 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 - 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 - 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 - 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 - 0x80, 0x00, 0x80, // 173 - 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 - 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 - 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 - 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 - 0x48, 0x00, 0x68, 0x00, 0x58, // 178 - 0x48, 0x00, 0x58, 0x00, 0x68, // 179 - 0x00, 0x00, 0x10, 0x00, 0x08, // 180 - 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 - 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 - 0x00, 0x00, 0x40, // 183 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 - 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 - 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 - 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 - 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 - 0x00, 0x00, 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 - 0x00, 0x00, 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 - 0x00, 0x00, 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 - 0x00, 0x00, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 - 0x00, 0x00, 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 - 0x00, 0x00, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 - 0x00, 0x00, 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 - 0x00, 0x00, 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 - 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 - 0x00, 0x00, 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 - 0x00, 0x00, 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 - 0x00, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 - 0x00, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 - 0x00, 0x00, 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 - 0x00, 0x00, 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x03, // 224 - 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 - 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 - 0x00, 0x00, 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 - 0x00, 0x00, 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 - 0x00, 0x00, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x98, 0x00, 0x40, 0x00, 0xE0, 0x03, // 233 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 234 - 0x00, 0x00, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 235 - 0x00, 0x00, 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 - 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 240 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0x40, 0x02, // 241 - 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, // 242 - 0x00, 0x00, 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 - 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 - 0x00, 0x00, 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, // 246 - 0x00, 0x00, 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 - 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 - 0x00, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 250 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 - 0x00, 0x00, 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 - 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 - 0x00, 0x00, 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0xE0, 0x03, // 255 + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x48, 0x00, 0x68, 0x00, 0x58, // 178 + 0x48, 0x00, 0x58, 0x00, 0x68, // 179 + 0x00, 0x00, 0x10, 0x00, 0x08, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 + 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 + 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 + 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 + 0x00, 0x00, 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 + 0x00, 0x00, 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 + 0x00, 0x00, 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 + 0x00, 0x00, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 + 0x00, 0x00, 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 + 0x00, 0x00, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 + 0x00, 0x00, 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 + 0x00, 0x00, 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 + 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 + 0x00, 0x00, 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 + 0x00, 0x00, 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 + 0x00, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 + 0x00, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 + 0x00, 0x00, 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 + 0x00, 0x00, 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x03, // 224 + 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 + 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 + 0x00, 0x00, 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 + 0x00, 0x00, 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 + 0x00, 0x00, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x01, 0x98, 0x00, 0x40, 0x00, 0xE0, 0x03, // 233 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 234 + 0x00, 0x00, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 235 + 0x00, 0x00, 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 + 0x00, 0x00, 0xE0, 0x03, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 240 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x20, 0x02, 0x40, 0x02, // 241 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, // 242 + 0x00, 0x00, 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 + 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 + 0x00, 0x00, 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, // 246 + 0x00, 0x00, 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 + 0x00, 0x00, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 + 0x00, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 250 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 + 0x00, 0x00, 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 + 0x00, 0x00, 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x00, 0x00, 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0xE0, 0x03, // 255 }; // Font generated or edited with the glyphEditor (@mrekin) @@ -662,321 +661,327 @@ const uint8_t ArialMT_Plain_16_RU[] PROGMEM = { // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 - 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, - 0x80, 0x08, // 35 - 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00, 0x30, 0x1C, // 36 - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, - 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, - 0x3C, // 37 - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x28, 0x00, 0x00, 0x44, // 38 - 0x00, 0x00, 0x00, 0x78, // 39 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 - 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 - 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 - 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00, 0xE0, 0x40, // 50 - 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 - 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 - 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, + 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00, + 0x30, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, + 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, + 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00, + 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, + 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, + 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, + 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, - 0x18, // 55 - 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 - 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 - 0x00, 0x00, 0x00, 0x40, 0x40, // 58 - 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 - 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, + 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, + 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, - 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 67 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 68 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 69 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, // 70 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, - 0x0E, // 71 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, - 0x3F, // 74 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, - 0x10, 0x20, 0x00, 0x08, 0x40, // 75 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 79 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x10, 0x01, 0x00, 0xE0, // 80 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, - 0x5F, // 81 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, - 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 84 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, - 0xE0, 0x00, 0x00, 0x18, // 86 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, + 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, + 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, + 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x5F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, + 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, - 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, - 0x40, // 88 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x08, // 89 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, - 0x08, 0x40, // 90 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 - 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0x01, // 94 - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x02, // 95 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, - 0x20, // 99 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, - 0x7F, // 104 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, - 0x7F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, - 0x03, // 113 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, - 0x38, // 115 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, - 0x7F, // 117 - 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, - 0xC0, // 118 - 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, - 0x40, // 120 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 121 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, - 0x40, // 122 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 - 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 - 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 - 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, - 0x0B, // 164 - 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, - 0x08, 0x0A, // 165 - 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 - 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 168 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, - 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 - 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, - 0x20, // 171 - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, - 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 - 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x04, // 175 - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 - 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 - 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 - 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, - 0x08, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 184 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 - 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, - 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, - 0x20, // 188 - 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, - 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, - 0x4C, // 189 - 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, - 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, - 0x20, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02, 0x00, 0x00, 0x03, - 0x00, 0x80, 0x01, // 191 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x66, 0x00, - 0x08, 0x3C, // 193 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x90, 0x22, 0x00, 0x60, 0x1C, // 194 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x18, // 195 - 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 197 - 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00, 0x08, 0x40, // 198 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, - 0xE0, 0x1D, // 199 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, - 0x7F, // 200 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00, 0x01, 0x01, 0x00, - 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, - 0x7F, // 201 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, - 0x08, 0x40, // 202 - 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0xF8, 0x7F, // 203 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, - 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 206 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x10, 0x01, 0x00, 0xE0, // 208 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 209 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 210 - 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, - 0x60, 0x00, 0x00, 0x18, // 211 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, - 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212 - 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, - 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, - 0x40, // 213 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, - 0x01, // 214 - 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0xF8, 0x7F, // 215 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, - 0x7F, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, + 0x18, 0x40, 0x00, 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, + 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, + 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xC0, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, + 0x00, 0x11, // 162 + 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, + 0x20, 0x20, // 163 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 + 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, + 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, + 0x00, 0x0C, // 167 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, + 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 168 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, + 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x04, // 175 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, // 177 + 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, + 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0xF8, 0xFF, 0x03, 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00, + 0x00, 0x17, // 184 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, + 0x00, 0x04, // 187 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, + 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 + 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02, + 0x00, 0x00, 0x03, 0x00, 0x80, 0x01, // 191 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x66, 0x00, 0x08, 0x3C, // 193 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 194 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x18, // 195 + 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 197 + 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00, + 0x08, 0x40, // 198 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, + 0x10, 0x22, 0x00, 0xE0, 0x1D, // 199 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 200 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00, + 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 201 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00, + 0x08, 0x20, 0x00, 0x08, 0x40, // 202 + 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0xF8, 0x7F, // 203 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 206 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 208 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 209 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 210 + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00, + 0xC0, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 211 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, + 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212 + 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, 0x40, // 213 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 214 + 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0xF8, 0x7F, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 217 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, - 0x00, 0x3E, // 220 - 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x10, 0x61, 0x00, - 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00, 0x30, 0x30, 0x00, - 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00, 0x80, 0x0F, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, - 0x7F, // 223 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00, 0x18, 0x3F, 0x00, - 0x00, 0x0C, // 225 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, - 0x3B, // 226 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227 - 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0xC0, 0x01, // 228 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 229 - 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, - 0x40, // 230 - 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x7F, // 232 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00, 0xC0, 0x7F, // 233 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, - 0x40, // 234 - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 235 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, 0xC0, 0x07, 0x00, - 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, // 237 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 238 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 239 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 240 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, - 0x20, // 241 - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0x40, // 242 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 243 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03, 0xC0, 0x60, 0x00, - 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244 - 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 245 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0xC0, // 246 - 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, - 0x7F, // 247 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249 - 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, - 0x00, 0x38, // 250 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x38, // 252 - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, - 0x1F, // 253 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, - 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, - 0x1F, // 254 - 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00, 0xC0, 0x7F, // 255 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x63, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x63, 0x00, 0x00, 0x3E, // 220 + 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x10, 0x61, 0x00, 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00, + 0x30, 0x30, 0x00, 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00, + 0x80, 0x0F, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 224 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00, + 0x18, 0x3F, 0x00, 0x00, 0x0C, // 225 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, 0x3B, // 226 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227 + 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, + 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 228 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 229 + 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x0E, 0x00, 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, // 230 + 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, + 0xC0, 0x7F, // 232 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00, + 0xC0, 0x7F, // 233 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, 0x40, // 234 + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0xC0, 0x7F, // 235 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, + 0xC0, 0x07, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0xC0, 0x7F, // 237 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 238 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0xC0, 0x7F, // 239 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 240 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 241 + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 242 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 243 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03, + 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244 + 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, + 0x40, 0x40, // 245 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 246 + 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 247 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249 + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x64, 0x00, 0x00, 0x38, // 250 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, + 0x00, 0x38, // 252 + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, 0x1F, // 253 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00, + 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, 0x1F, // 254 + 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00, + 0xC0, 0x7F, // 255 }; // Font generated or edited with the glyphEditor (@mrekin) @@ -1211,506 +1216,553 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { 0x23, 0x83, 0x43, 0x12, // 254 0x23, 0xC6, 0x2B, 0x0D, // 255 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, - 0x33, // 33 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, - 0xE0, 0x07, // 34 - 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, - 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, - 0x00, 0x0C, 0x03, // 35 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, - 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x80, 0x81, 0x07, // 36 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, - 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, - 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, - 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0, 0x3C, 0x30, 0x00, - 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, - 0x20, 0x00, 0x00, 0x04, // 40 - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, - 0x00, 0xF0, 0x0F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, - 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, - 0x80, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, - 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, - 0x00, 0x60, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, + 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 + 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, + 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, + 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, + 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, + 0x1F, 0x00, 0x80, 0x81, 0x07, // 36 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, + 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20, + 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0, + 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, + 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, + 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, + 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x80, 0x01, // 45 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, - 0x60, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, - 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, - 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, - 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, - 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, - 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, - 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, - 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, - 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, - 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, - 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, - 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, - 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, - 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, - 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, - 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x60, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, + 0x00, 0x33, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, + 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, + 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, + 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x03, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, + 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, + 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, + 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, + 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, + 0x80, 0x3F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, + 0x00, 0x00, 0x60, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7, + 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, + 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, + 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, + 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, + 0x03, 0x00, 0x00, 0x03, 0x06, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, + 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, + 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x20, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, + 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, + 0x00, 0x00, 0x00, 0x07, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, + 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, + 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, + 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, + 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, + 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, - 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, - 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, - 0x0F, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, - 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, + 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, + 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, + 0x00, 0x00, 0xE2, 0x0F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, + 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, - 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, - 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, - 0x0F, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, - 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, - 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, + 0x00, 0xE0, 0xFF, 0x3F, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, + 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, + 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, + 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F, + 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, + 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, - 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85 - 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, - 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, - 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, - 0x20, // 86 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 - 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00, 0xCF, 0x03, 0x00, - 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, - 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 - 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, - 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, - 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, - 0x06, // 91 - 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, - 0x00, 0x00, 0x30, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, - 0x07, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, + 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70, + 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85 + 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, + 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, + 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, + 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 + 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, + 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 + 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, + 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, + 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 + 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, + 0xFF, 0xFF, 0x07, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, - 0x00, 0x00, 0x00, 0x06, // 95 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, - 0x00, 0xF0, 0x03, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, + 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, - 0x60, 0x06, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, - 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 - 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, - 0x00, 0xF0, 0x03, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, - 0x07, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, + 0x06, 0x00, 0x00, 0x60, 0x06, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, + 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, + 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, + 0x30, 0x00, 0x00, 0x00, 0x20, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, + 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 - 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 + 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 - 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 - 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0x0E, // 119 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, - 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, + 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x06, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, - 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, + 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, - 0x60, 0x00, 0x00, 0x06, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, + 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, - 0x00, 0x80, 0x01, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, - 0x00, 0x60, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, - 0x07, // 161 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, - 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00, + 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, + 0x06, 0x3F, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 - 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, - 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, - 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, - 0x00, 0x02, 0x04, // 164 - 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, - 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, - 0x20, // 165 + 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, + 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, + 0x38, 0x00, 0x00, 0x00, 0x10, // 163 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, + 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, + 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 + 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, + 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, + 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, - 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, - 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, - 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, - 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, - 0xC0, 0x3F, // 170 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, - 0x10, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x80, 0x01, // 173 - 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, - 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, - 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x0C, // 175 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, - 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, - 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, - 0x00, 0x60, 0x30, // 177 - 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, - 0xC0, 0x21, // 178 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, - 0xC0, 0x1D, // 179 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x20, // 180 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 - 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, - 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, - 0x60, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, + 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, + 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, + 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, + 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0, + 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, + 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, + 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, + 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, + 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 + 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, + 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, + 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, + 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 + 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, + 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, + 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, + 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 + 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, + 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, + 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x20, // 180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 + 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, + 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, + 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, - 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8, 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00, + 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8, + 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, // 185 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, - 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, // 187 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, 0x3F, 0x1C, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, - 0x00, 0xC0, 0x21, // 189 - 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, - 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, - 0x00, 0x00, 0x08, // 190 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, - 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, - 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, - 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, // 195 - 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197 - 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, - 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, - 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x20, // 198 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C, 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, - 0x00, 0xC7, 0x0F, // 199 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06, 0x00, 0x0F, 0x00, - 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, - 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202 - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 204 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, - 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, - 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, - 0x0F, // 208 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, + 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, + 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x80, // 187 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, + 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, + 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, + 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, + 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 + 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, + 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09, + 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, + 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 195 + 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, + 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197 + 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, + 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00, + 0x60, 0x00, 0x20, // 198 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C, + 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, 0x00, 0xC7, 0x0F, // 199 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, + 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06, + 0x00, 0x0F, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E, + 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00, + 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202 + 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, + 0x00, 0xE0, 0xFF, 0x3F, // 204 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, + 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, + 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, + 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 208 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, + 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, + 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 209 - 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x60, // 210 - 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x78, 0x30, 0x00, - 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, - 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, - 0x20, // 211 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, 0x01, 0x06, 0x00, - 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, - 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 212 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, - 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, - 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, // 214 - 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, // 217 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, - 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, - 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, - 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0xE0, 0xFF, 0x3F, // 219 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, - 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, - 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, - 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 210 + 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, + 0x78, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x20, // 211 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, + 0x01, 0x06, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01, + 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01, + 0x00, 0x00, 0xF8, // 212 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, + 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, + 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0xF0, // 214 + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, + 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, + 0x00, 0x00, 0xF0, 0x01, // 217 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, + 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, + 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, + 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, + 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 219 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, + 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, + 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, + 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, + 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x03, // 221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, - 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03, 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, - 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, - 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0, 0xB8, 0x0F, 0x00, - 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, - 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60, 0x0C, 0x30, 0x00, - 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78, 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, - 0x10, 0xC0, 0x03, // 225 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, - 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78, 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227 - 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, - 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, - 0x00, 0x00, 0xF0, // 228 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229 - 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, - 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03, + 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, + 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, + 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0, + 0xB8, 0x0F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, + 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, + 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, + 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60, + 0x0C, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78, + 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, 0x10, 0xC0, 0x03, // 225 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, + 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78, + 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227 + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00, + 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0xF0, // 228 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, + 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229 + 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, + 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04, 0x20, // 230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, 0x86, 0x20, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, - 0x1F, // 231 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x07, 0x00, - 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80, 0x00, 0x0F, 0x00, - 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, - 0x30, // 234 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, - 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, + 0x86, 0x20, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, 0x1F, // 231 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80, + 0x00, 0x0F, 0x00, 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, 0x30, // 234 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, + 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 239 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, - 0x00, 0xF0, 0x03, // 240 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, + 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, + 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 240 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 241 - 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, - 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, + 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 242 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, - 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, + 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x06, // 243 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, - 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, - 0x00, 0xF0, 0x07, // 244 - 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, + 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF, + 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, + 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 + 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 245 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, - 0x00, 0x00, 0xF0, // 246 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 246 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 247 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, - 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, - 0x00, 0x00, 0xF0, // 249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, - 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, - 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, - 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, - 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, - 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, + 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, + 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, + 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, + 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, + 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 253 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xE0, 0x07, // 254 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00, 0x84, 0x03, 0x00, - 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, + 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F, + 0x00, 0x00, 0xE0, 0x07, // 254 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00, + 0x84, 0x03, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 255 }; diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp index 25b390ebf..8bc56ea94 100644 --- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -234,196 +234,195 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = { 0x06, 0xD8, 0x0A, 0x06, // 254 0x06, 0xE2, 0x08, 0x05, // 255 // Font Data: - 0x00, 0x00, 0xF8, 0x02, // 33 - 0x38, 0x00, 0x00, 0x00, 0x38, // 34 - 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 - 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 - 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 - 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 - 0x38, // 39 - 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 - 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 - 0x28, 0x00, 0x18, 0x00, 0x28, // 42 - 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 - 0x00, 0x00, 0x00, 0x06, // 44 - 0x80, 0x00, 0x80, // 45 - 0x00, 0x00, 0x00, 0x02, // 46 - 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 - 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 - 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 - 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 - 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 - 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 - 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 - 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 - 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 - 0x00, 0x00, 0x20, 0x02, // 58 - 0x00, 0x00, 0x20, 0x06, // 59 - 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 - 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 - 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 - 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 - 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, - 0x04, // 64 - 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 - 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 - 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 - 0x00, 0x00, 0xF8, 0x03, // 73 - 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 - 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 - 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 - 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 - 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 - 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 - 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 - 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 - 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 - 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 - 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 - 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 - 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 - 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 - 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 - 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 - 0x08, 0x08, 0xF8, 0x0F, // 93 - 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 - 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 - 0x08, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 - 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 - 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 - 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 - 0x00, 0x00, 0xE8, 0x03, // 105 - 0x00, 0x08, 0xE8, 0x07, // 106 - 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 - 0x00, 0x00, 0xF8, 0x03, // 108 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 - 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 - 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 - 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 - 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 - 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 - 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 - 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 - 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 - 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 - 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 - 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 - 0x00, 0x00, 0xF8, 0x0F, // 124 - 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 - 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 - 0x00, 0x00, 0xA0, 0x0F, // 161 - 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 - 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 - 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 - 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x0C, // 165 - 0x00, 0x00, 0x38, 0x0F, // 166 - 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 - 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 - 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 - 0x00, 0x00, 0xF0, 0x01, 0x58, 0x03, 0x48, 0x02, 0x08, 0x02, 0x10, 0x01, // 170 - 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 - 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 - 0x80, 0x00, 0x80, // 173 - 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 - 0x00, 0x00, 0x08, 0x02, 0x0A, 0x02, 0xF8, 0x03, 0x0A, 0x02, 0x08, 0x02, // 175 - 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 - 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 - 0x00, 0x00, 0x08, 0x02, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, // 178 - 0x00, 0x00, 0x20, 0x02, 0x20, 0x02, 0xE8, 0x03, 0x20, 0x02, 0x20, 0x02, // 179 - 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x30, // 180 - 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 - 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 - 0x00, 0x00, 0x40, // 183 - 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 - 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 - 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x10, 0x02, 0x20, 0x01, // 186 - 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 - 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 - 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 - 0x28, 0x02, 0xE0, 0x03, 0x28, 0x02, // 191 - 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 - 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 - 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 - 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 - 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 - 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 - 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 - 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 - 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 - 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 - 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 - 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 - 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 - 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 - 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 - 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 - 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 - 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 - 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 - 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 - 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 - 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 - 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 - 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 - 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 - 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 - 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 - 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 - 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 - 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 - 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 - 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x01, // 224 - 0xE0, 0x01, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 - 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 - 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 - 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 - 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 - 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 - 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 - 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 - 0xE0, 0x03, 0x08, 0x01, 0x88, 0x00, 0x48, 0x00, 0xE0, 0x03, // 233 - 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 234 - 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xE0, 0x03, // 235 - 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 - 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 - 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 - 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 - 0xE0, 0x03, 0xA0, 0x00, 0x40, // 240 - 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 241 - 0x20, 0x00, 0xE0, 0x03, 0x20, // 242 - 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 - 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 - 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 - 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 246 - 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 - 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 - 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 - 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, // 250 - 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 - 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 - 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 - 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 - 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xE0, 0x03, // 255 + 0x00, 0x00, 0xF8, 0x02, // 33 + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 + 0x38, // 39 + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + 0x00, 0x00, 0x00, 0x06, // 44 + 0x80, 0x00, 0x80, // 45 + 0x00, 0x00, 0x00, 0x02, // 46 + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 + 0x00, 0x00, 0x20, 0x02, // 58 + 0x00, 0x00, 0x20, 0x06, // 59 + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 + 0x00, 0x00, 0xF8, 0x03, // 73 + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 + 0x08, 0x08, 0xF8, 0x0F, // 93 + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 + 0x08, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 + 0x00, 0x00, 0xE8, 0x03, // 105 + 0x00, 0x08, 0xE8, 0x07, // 106 + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 + 0x00, 0x00, 0xF8, 0x03, // 108 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 + 0x00, 0x00, 0xF8, 0x0F, // 124 + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 + 0x00, 0x00, 0xA0, 0x0F, // 161 + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 + 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 + 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x0C, // 165 + 0x00, 0x00, 0x38, 0x0F, // 166 + 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 + 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 + 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 + 0x00, 0x00, 0xF0, 0x01, 0x58, 0x03, 0x48, 0x02, 0x08, 0x02, 0x10, 0x01, // 170 + 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 + 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 + 0x80, 0x00, 0x80, // 173 + 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 + 0x00, 0x00, 0x08, 0x02, 0x0A, 0x02, 0xF8, 0x03, 0x0A, 0x02, 0x08, 0x02, // 175 + 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 + 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 + 0x00, 0x00, 0x08, 0x02, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, // 178 + 0x00, 0x00, 0x20, 0x02, 0x20, 0x02, 0xE8, 0x03, 0x20, 0x02, 0x20, 0x02, // 179 + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x30, // 180 + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 + 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 + 0x00, 0x00, 0x40, // 183 + 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 + 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 + 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x10, 0x02, 0x20, 0x01, // 186 + 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 + 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 + 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 + 0x28, 0x02, 0xE0, 0x03, 0x28, 0x02, // 191 + 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 + 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 + 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 + 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 + 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 + 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 + 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 + 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 + 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 + 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 + 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 + 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 + 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 + 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 + 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 + 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 + 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 + 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 + 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 + 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 + 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 + 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 + 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 + 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 + 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 + 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 + 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 + 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 + 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 + 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x01, // 224 + 0xE0, 0x01, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 + 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 + 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 + 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 + 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 + 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 + 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 + 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 + 0xE0, 0x03, 0x08, 0x01, 0x88, 0x00, 0x48, 0x00, 0xE0, 0x03, // 233 + 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 234 + 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xE0, 0x03, // 235 + 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 + 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 + 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 + 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 + 0xE0, 0x03, 0xA0, 0x00, 0x40, // 240 + 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 241 + 0x20, 0x00, 0xE0, 0x03, 0x20, // 242 + 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 + 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 + 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 246 + 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 + 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 + 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 + 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, // 250 + 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 + 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 + 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 + 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 + 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xE0, 0x03, // 255 }; const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { @@ -659,181 +658,189 @@ const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 - 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, - 0x80, 0x08, // 35 - 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x43, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x18, 0x22, 0x00, 0x20, 0x1C, // 36 - 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, - 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, - 0x3C, // 37 - 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x28, 0x00, 0x00, 0x44, // 38 - 0x00, 0x00, 0x00, 0x78, // 39 - 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 - 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 - 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 - 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 - 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 - 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 + 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, + 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 + 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x43, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x18, 0x22, 0x00, + 0x20, 0x1C, // 36 + 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, + 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 + 0x00, 0x00, 0x00, 0x78, // 39 + 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 + 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x02, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 + 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, + 0xE0, 0x1F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 + 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, + 0xE0, 0x40, // 50 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, + 0x00, 0x1C, // 51 + 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x08, // 52 + 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, + 0x08, 0x1E, // 53 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, + 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, - 0x18, // 55 - 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 - 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 - 0x00, 0x00, 0x00, 0x40, 0x40, // 58 - 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 - 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 - 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 - 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 + 0x18, // 55 + 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, + 0x60, 0x1C, // 56 + 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, + 0xE0, 0x1F, // 57 + 0x00, 0x00, 0x00, 0x40, 0x40, // 58 + 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x40, 0x10, // 60 + 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, + 0x80, 0x08, // 61 + 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, - 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 67 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 68 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 69 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, // 70 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, - 0x0E, // 71 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 - 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, - 0x3F, // 74 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, - 0x10, 0x20, 0x00, 0x08, 0x40, // 75 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 79 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x10, 0x01, 0x00, 0xE0, // 80 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, - 0x4F, // 81 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x1A, 0x00, - 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 84 - 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, - 0xE0, 0x00, 0x00, 0x18, // 86 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, + 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, + 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, // 70 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 + 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, + 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, // 76 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, + 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x0E, 0x00, + 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 84 + 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, - 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, - 0x40, // 88 - 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x08, // 89 - 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, - 0x08, 0x40, // 90 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 - 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 - 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0x01, // 94 - 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x02, // 95 - 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, - 0x20, // 99 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, - 0x7F, // 104 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, - 0x7F, // 110 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, - 0x03, // 113 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 - 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, - 0x38, // 115 - 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 - 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, - 0x7F, // 117 - 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, - 0xC0, // 118 - 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, - 0x40, // 120 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 121 - 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, - 0x40, // 122 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 - 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 - 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 - 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, - 0x20, 0x40, 0x00, 0xE0, 0x7F, // 127 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, - 0x3E, // 1026 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 1027 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 + 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x08, // 89 + 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, + 0x18, 0x40, 0x00, 0x08, 0x40, // 90 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 + 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 + 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 97 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xF8, 0x7F, // 100 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 101 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, + 0xC0, 0xFF, // 103 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, + 0x40, 0x40, // 107 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 111 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0xC0, 0xFF, 0x03, // 113 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 + 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 + 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 + 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 + 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 + 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 + 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 + 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 + 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 + 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, + 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0xE0, 0x7F, // 127 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1026 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 1027 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 8218 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x40, // 1107 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 8222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x40, // 8230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 8230 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0x40, // 8224 - 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 8225 - 0x00, 0x05, 0x00, 0xC0, 0x1F, 0x00, 0x20, 0x25, 0x00, 0x10, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x10, 0x20, // 8364 - 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x71, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x3C, 0x00, 0x18, 0x42, 0x00, - 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, - 0x00, 0x3C, // 8240 - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1033 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 8249 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1034 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x82, 0x02, 0x00, 0x61, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, - 0x08, 0x40, // 1036 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, - 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7E, // 1035 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1039 + 0x40, // 8224 + 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, + 0x40, 0x40, // 8225 + 0x00, 0x05, 0x00, 0xC0, 0x1F, 0x00, 0x20, 0x25, 0x00, 0x10, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, + 0x10, 0x20, // 8364 + 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x71, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x3C, 0x00, + 0x18, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x42, 0x00, + 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 8240 + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x3E, // 1033 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 8249 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x3E, // 1034 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x82, 0x02, 0x00, 0x61, 0x04, 0x00, 0x10, 0x08, 0x00, + 0x08, 0x30, 0x00, 0x08, 0x40, // 1036 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, + 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7E, // 1035 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1039 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x80, 0xFF, 0x03, // 1106 0x00, 0x00, 0x00, 0x38, // 8216 @@ -841,157 +848,157 @@ const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, // 8220 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, // 8221 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, // 8226 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x04, // 8211 - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 8212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, - 0x20, 0x40, 0x00, 0xE0, 0x7F, // 65533 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xF8, 0x01, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x01, 0x00, 0xE0, 0x00, 0x00, 0x18, 0x00, 0x00, 0xF8, 0x01, // 8482 - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, // 8211 + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x04, // 8212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, + 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0xE0, 0x7F, // 65533 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x01, 0x00, 0xE0, 0x00, 0x00, 0x18, 0x00, 0x00, + 0xF8, 0x01, // 8482 + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, + 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, 0x00, 0x38, // 1113 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 8250 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1114 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1114 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x10, 0x04, 0x00, 0x08, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1116 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 1115 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1119 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x81, 0x41, 0x00, 0x02, 0x66, 0x00, 0x02, 0x18, 0x00, 0x02, 0x06, 0x00, 0x81, 0x01, 0x00, - 0x60, 0x00, 0x00, 0x18, // 1038 - 0xC0, 0x01, 0x00, 0x08, 0x06, 0x02, 0x10, 0x38, 0x02, 0x10, 0xC0, 0x01, 0x10, 0x38, 0x00, 0x08, 0x07, 0x00, - 0xC0, // 1118 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x81, 0x41, 0x00, 0x02, 0x66, 0x00, 0x02, 0x18, 0x00, 0x02, 0x06, 0x00, + 0x81, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 1038 + 0xC0, 0x01, 0x00, 0x08, 0x06, 0x02, 0x10, 0x38, 0x02, 0x10, 0xC0, 0x01, 0x10, 0x38, 0x00, 0x08, 0x07, 0x00, 0xC0, // 1118 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 1032 - 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, - 0x0B, // 164 + 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x0F, // 1168 - 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 - 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 1025 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, - 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x11, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 1028 - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, - 0x20, // 171 - 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 - 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 - 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, - 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 - 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 1031 - 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 - 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 - 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1030 - 0x00, 0x00, 0x00, 0xC8, 0x7F, // 1110 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x78, // 1169 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 - 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, - 0x08, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 1105 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x27, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x80, 0x27, // 8470 + 0x0F, // 1168 + 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 + 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, + 0x00, 0x0C, // 167 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 1025 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, + 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x11, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 1028 + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 + 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x80, 0x0F, // 172 + 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 + 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, + 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 + 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 1031 + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 + 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, // 177 + 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1030 + 0x00, 0x00, 0x00, 0xC8, 0x7F, // 1110 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x78, // 1169 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, + 0xC0, 0x7F, // 181 + 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0xF8, 0xFF, 0x03, 0x08, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 1105 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x08, 0x00, + 0x00, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x27, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, + 0x40, 0x28, 0x00, 0x80, 0x27, // 8470 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x20, 0x00, 0x00, - 0x11, // 1108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 - 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 1112 - 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, - 0x10, 0x22, 0x00, 0x20, 0x1C, // 1029 + 0x11, // 1108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, + 0x00, 0x04, // 187 + 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 1112 + 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, + 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 1029 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 1109 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 1111 - 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, - 0x00, 0x1C, 0x00, 0x00, 0x60, // 1040 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x00, 0x3E, // 1041 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x90, 0x22, 0x00, 0x60, 0x1C, // 1042 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 1043 - 0x00, 0xC0, 0x03, 0x00, 0x60, 0x00, 0x00, 0x5C, 0x00, 0xF8, 0x43, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1044 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x08, 0x41, 0x00, 0x08, 0x40, // 1045 - 0x08, 0x30, 0x00, 0x10, 0x08, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, + 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 1040 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x00, 0x3E, // 1041 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 1042 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 1043 + 0x00, 0xC0, 0x03, 0x00, 0x60, 0x00, 0x00, 0x5C, 0x00, 0xF8, 0x43, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1044 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 1045 + 0x08, 0x30, 0x00, 0x10, 0x08, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, 0x40, // 1046 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x42, 0x00, 0x70, 0x42, 0x00, - 0x00, 0x3C, // 1047 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, - 0x7F, // 1048 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x01, 0x04, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x82, 0x00, 0x00, - 0x41, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, - 0x7F, // 1049 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, - 0x08, 0x40, // 1050 - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1051 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1052 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x01, 0x00, 0xF8, 0x7F, // 1053 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, - 0x0F, // 1054 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1055 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x10, 0x01, 0x00, 0xE0, // 1056 - 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, - 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, - 0x10, // 1057 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, - 0x08, // 1058 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x80, 0x41, 0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x01, 0x00, - 0x60, 0x00, 0x00, 0x18, // 1059 - 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x40, 0x08, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, - 0x40, 0x08, 0x00, 0x80, 0x07, // 1060 - 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, - 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, - 0x40, // 1061 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, - 0x03, // 1062 - 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, - 0xF8, 0x7F, // 1063 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1064 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, - 0x03, // 1065 - 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1066 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, - 0x7F, // 1067 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, - 0x00, 0x41, 0x00, 0x00, 0x3E, // 1068 - 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, - 0x10, 0x21, 0x00, 0x20, 0x11, 0x00, 0xC0, - 0x0F, // 1069 - 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, - 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 1070 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40, 0x00, 0x10, 0x21, 0x00, 0x08, 0x1A, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, - 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, - 0x7F, // 1071 - 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 1072 - 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x90, 0x20, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x88, 0x20, 0x00, 0x08, 0x1F, // 1073 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 1074 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1075 - 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0xC0, 0x4F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, - 0x01, // 1076 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 1077 - 0xC0, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, - 0xC0, 0x20, 0x00, 0x40, 0x40, // 1078 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x42, 0x00, + 0x70, 0x42, 0x00, 0x00, 0x3C, // 1047 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, + 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1048 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x01, 0x04, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, + 0x82, 0x00, 0x00, 0x41, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1049 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, + 0x08, 0x30, 0x00, 0x08, 0x40, // 1050 + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1051 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1052 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 1053 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 1054 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1055 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 1056 + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, + 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 1057 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x08, // 1058 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x80, 0x41, 0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, + 0x80, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 1059 + 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x40, 0x08, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, + 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 1060 + 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 1061 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1062 + 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x02, 0x00, 0xF8, 0x7F, // 1063 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1064 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1065 + 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1066 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1067 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, + 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1068 + 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, + 0x08, 0x41, 0x00, 0x10, 0x21, 0x00, 0x20, 0x11, 0x00, 0xC0, 0x0F, // 1069 + 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, + 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, + 0xC0, 0x0F, // 1070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40, 0x00, 0x10, 0x21, 0x00, 0x08, 0x1A, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x02, 0x00, + 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 1071 + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, + 0x80, 0x7F, // 1072 + 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x90, 0x20, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x88, 0x20, 0x00, + 0x08, 0x1F, // 1073 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, + 0x80, 0x3B, // 1074 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1075 + 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0xC0, 0x4F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, + 0x00, 0xC0, 0x01, // 1076 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, + 0x00, 0x17, // 1077 + 0xC0, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1078 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 1079 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, 0xC0, 0x7F, // 1080 @@ -1001,45 +1008,44 @@ const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { 0x40, // 1082 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 1083 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, - 0xC0, 0x01, 0x00, 0xC0, 0x7F, // 1084 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0xC0, 0x7F, // 1084 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, - 0x7F, // 1085 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1086 + 0x7F, // 1085 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 1086 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, - 0x7F, // 1087 - 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1088 + 0x7F, // 1087 + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, + 0x00, 0x1F, // 1088 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, - 0x20, // 1089 - 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, - 0x40, // 1090 - 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, - 0xC0, // 1091 - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, - 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1092 + 0x20, // 1089 + 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1090 + 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 1091 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0xFF, 0x03, + 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1092 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 1093 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, - 0x00, 0xC0, 0x03, // 1094 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, + 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1094 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 1095 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1096 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, - 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, - 0x03, // 1097 - 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, - 0x00, 0x38, // 1098 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, - 0x7F, // 1099 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1100 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1096 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1097 + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x44, 0x00, 0x00, 0x38, // 1098 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 1099 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, + 0x00, 0x38, // 1100 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x1F, // 1101 - 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, - 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, - 0x1F, // 1102 - 0x00, 0x00, 0x00, 0x80, 0x63, 0x00, 0x40, 0x14, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x7F, // 1103 + 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, + 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1102 + 0x00, 0x00, 0x00, 0x80, 0x63, 0x00, 0x40, 0x14, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, + 0xC0, 0x7F, // 1103 }; const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { @@ -1273,597 +1279,648 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0x29, 0xDD, 0x43, 0x12, // 1102=�.�.:10717 0x2A, 0x20, 0x2B, 0x0D, // 1103=�.�.:10784 // Font Data: - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xCF, 0x00, 0x80, 0xFF, - 0xCF, // 33 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, - 0x80, 0x1F, // 34 - 0x00, 0x30, 0x0C, 0x00, 0x00, 0x30, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x80, 0x3F, 0x0C, 0x00, - 0x80, 0x31, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0D, 0x00, 0x80, 0x3F, 0x0C, 0x00, 0x80, 0x31, 0x0C, 0x00, - 0x00, 0x30, 0x0C, // 35 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x18, 0x00, 0x00, 0x3F, 0x78, 0x00, 0x00, 0x63, 0x70, 0x00, 0x80, 0x61, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0xC0, 0xFF, 0xFF, 0x03, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x07, 0x7F, 0x00, 0x00, 0x04, 0x1E, // 36 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, - 0x80, 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0x78, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, - 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x0F, 0x7F, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, - 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 37 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x8E, 0x73, 0x00, 0x00, 0xDF, 0xE1, 0x00, 0x80, 0xF3, 0xC0, 0x00, - 0x80, 0xE1, 0xC0, 0x00, 0x80, 0xE1, 0xC1, 0x00, 0x80, 0xB3, 0xE3, 0x00, 0x00, 0x3F, 0x6E, 0x00, 0x00, 0x0E, 0x7C, 0x00, 0x00, 0x00, 0x3C, 0x00, - 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xE2, 0x00, 0x00, 0x00, 0x40, // 38 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 39 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x80, 0x01, 0x00, 0x18, - 0x80, 0x00, 0x00, 0x10, // 40 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x80, 0x01, 0x00, 0x18, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0xF8, 0xFF, 0x01, - 0x00, 0xC0, 0x3F, // 41 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, // 42 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x80, 0x01, // 43 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xCF, 0x00, 0x80, 0xFF, 0xCF, // 33 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x1F, 0x00, 0x00, 0x80, 0x1F, // 34 + 0x00, 0x30, 0x0C, 0x00, 0x00, 0x30, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x80, + 0x3F, 0x0C, 0x00, 0x80, 0x31, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0D, 0x00, 0x80, 0x3F, + 0x0C, 0x00, 0x80, 0x31, 0x0C, 0x00, 0x00, 0x30, 0x0C, // 35 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x18, 0x00, 0x00, 0x3F, 0x78, 0x00, 0x00, 0x63, 0x70, 0x00, 0x80, 0x61, 0xE0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x07, + 0x7F, 0x00, 0x00, 0x04, 0x1E, // 36 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, + 0x80, 0x80, 0x00, 0x80, 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0x78, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, + 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x0F, 0x7F, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x80, 0x80, 0x80, + 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 37 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x8E, 0x73, 0x00, 0x00, 0xDF, 0xE1, 0x00, 0x80, + 0xF3, 0xC0, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0xE1, 0xC1, 0x00, 0x80, 0xB3, 0xE3, 0x00, 0x00, 0x3F, 0x6E, 0x00, 0x00, 0x0E, + 0x7C, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xE2, 0x00, 0x00, 0x00, 0x40, // 38 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 39 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x80, + 0x01, 0x00, 0x18, 0x80, 0x00, 0x00, 0x10, // 40 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x80, 0x01, 0x00, 0x18, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x3E, 0xC0, 0x07, 0x00, + 0xF8, 0xFF, 0x01, 0x00, 0xC0, 0x3F, // 41 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, + 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, // 42 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 44 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, // 45 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 46 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, - 0x80, 0x01, // 47 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x07, 0x70, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xF8, 0x0F, // 48 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, - 0x00, 0x06, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 49 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x01, 0xCC, 0x00, - 0x80, 0x01, 0xC6, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0xC3, 0xC0, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x00, 0x3C, 0xC0, // 50 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x18, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x03, 0x70, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 51 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x38, 0x0C, 0x00, - 0x00, 0x1E, 0x0C, 0x00, 0x00, 0x07, 0x0C, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 52 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x00, 0xFE, 0x38, 0x00, 0x80, 0x7F, 0x60, 0x00, 0x80, 0x21, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, - 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x61, 0x70, 0x00, 0x80, 0xC1, 0x3F, 0x00, 0x00, 0x80, 0x0F, // 53 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x8F, 0x71, 0x00, 0x00, 0xC3, 0xE0, 0x00, 0x80, 0x61, 0xC0, 0x00, - 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0xC3, 0x60, 0x00, 0x00, 0xC7, 0x3F, 0x00, 0x00, 0x06, 0x1F, // 54 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xFE, 0x00, - 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xE1, 0x01, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 55 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 56 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x30, 0x00, 0x00, 0xFE, 0x71, 0x00, 0x00, 0x87, 0xE1, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, - 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0xC7, 0x78, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xF8, 0x07, // 57 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 58 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x0C, 0x00, 0x18, 0xC0, 0x07, // 59 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, - 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, // 60 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, - 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, // 61 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x30, 0x06, 0x00, - 0x00, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, // 62 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0xCE, 0x00, - 0x80, 0x01, 0xCF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x1C, // 63 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x78, 0xC0, 0x03, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x0E, 0x1F, 0x06, - 0x00, 0xC7, 0x7F, 0x0E, 0x00, 0xE3, 0x60, 0x0C, 0x00, 0x33, 0xC0, 0x0C, 0x80, 0x39, 0xC0, 0x18, 0x80, 0x19, 0xC0, 0x18, 0x80, 0x19, 0x60, 0x18, - 0x80, 0x19, 0x30, 0x18, 0x80, 0x31, 0x78, 0x18, 0x80, 0xE1, 0xFF, 0x18, 0x80, 0xFB, 0xC7, 0x18, 0x00, 0x3B, 0xC0, 0x18, 0x00, 0x07, 0x60, 0x0C, - 0x00, 0x0E, 0x70, 0x0C, 0x00, 0x1C, 0x3C, 0x06, 0x00, 0xF8, 0x1F, 0x06, 0x00, 0xE0, 0x07, 0x03, 0x00, 0x00, 0x00, 0x01, // 64 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x3E, 0x06, 0x00, - 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, - 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 65 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, - 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 66 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, + 0x0F, 0x00, 0x00, 0x80, 0x01, // 47 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x07, 0x70, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, + 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xF8, 0x0F, // 48 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 49 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, + 0x01, 0xCC, 0x00, 0x80, 0x01, 0xC6, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0xC3, 0xC0, 0x00, 0x00, 0x7F, + 0xC0, 0x00, 0x00, 0x3C, 0xC0, // 50 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x18, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x03, 0x70, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, + 0x3F, 0x00, 0x00, 0x00, 0x1E, // 51 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF0, 0x0C, 0x00, 0x00, + 0x38, 0x0C, 0x00, 0x00, 0x1E, 0x0C, 0x00, 0x00, 0x07, 0x0C, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, + 0x0C, 0x00, 0x00, 0x00, 0x0C, // 52 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x00, 0xFE, 0x38, 0x00, 0x80, 0x7F, 0x60, 0x00, 0x80, 0x21, 0xC0, 0x00, 0x80, + 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x61, 0x70, 0x00, 0x80, 0xC1, + 0x3F, 0x00, 0x00, 0x80, 0x0F, // 53 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x8F, 0x71, 0x00, 0x00, 0xC3, 0xE0, 0x00, 0x80, + 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0xC3, 0x60, 0x00, 0x00, 0xC7, + 0x3F, 0x00, 0x00, 0x06, 0x1F, // 54 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, + 0x01, 0xFE, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xE1, 0x01, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x0D, 0x00, 0x00, 0x80, 0x07, + 0x00, 0x00, 0x80, 0x01, // 55 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, + 0x7F, 0x00, 0x00, 0x00, 0x1E, // 56 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x30, 0x00, 0x00, 0xFE, 0x71, 0x00, 0x00, 0x87, 0xE1, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, + 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0xC7, 0x78, 0x00, 0x00, 0xFE, + 0x3F, 0x00, 0x00, 0xF8, 0x07, // 57 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 58 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x0C, 0x00, 0x18, 0xC0, 0x07, // 59 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, + 0x60, 0x03, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, + 0x0C, 0x00, 0x00, 0x0C, 0x18, // 60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, + 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, + 0x06, 0x00, 0x00, 0x30, 0x06, // 61 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, + 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xC0, + 0x01, 0x00, 0x00, 0x80, // 62 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, + 0x01, 0xCE, 0x00, 0x80, 0x01, 0xCF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0x7F, + 0x00, 0x00, 0x00, 0x1C, // 63 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x78, 0xC0, 0x03, 0x00, 0x1C, 0x00, 0x07, 0x00, + 0x0E, 0x1F, 0x06, 0x00, 0xC7, 0x7F, 0x0E, 0x00, 0xE3, 0x60, 0x0C, 0x00, 0x33, 0xC0, 0x0C, 0x80, 0x39, 0xC0, 0x18, 0x80, 0x19, + 0xC0, 0x18, 0x80, 0x19, 0x60, 0x18, 0x80, 0x19, 0x30, 0x18, 0x80, 0x31, 0x78, 0x18, 0x80, 0xE1, 0xFF, 0x18, 0x80, 0xFB, 0xC7, + 0x18, 0x00, 0x3B, 0xC0, 0x18, 0x00, 0x07, 0x60, 0x0C, 0x00, 0x0E, 0x70, 0x0C, 0x00, 0x1C, 0x3C, 0x06, 0x00, 0xF8, 0x1F, 0x06, + 0x00, 0xE0, 0x07, 0x03, 0x00, 0x00, 0x00, 0x01, // 64 + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, + 0x3E, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, + 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 65 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 66 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, 0x0C, // 67 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0x60, 0x00, - 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, + 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 68 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 69 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, - 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, - 0x80, 0xC1, 0x00, 0x00, 0x80, 0x01, // 70 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x80, 0x81, 0xC1, 0x00, - 0x80, 0x83, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x87, 0x61, 0x00, 0x00, 0x8E, 0x3F, 0x00, 0x00, 0x88, 0x3F, // 71 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 72 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 73 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, - 0x3F, // 74 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x9C, 0x07, 0x00, 0x00, 0x0E, 0x1E, 0x00, 0x00, 0x07, 0x3C, 0x00, - 0x80, 0x03, 0x78, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 69 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, + 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, + 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x01, // 70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x81, + 0xC1, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x87, 0x61, 0x00, 0x00, 0x8E, 0x3F, + 0x00, 0x00, 0x88, 0x3F, // 71 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 72 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 73 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, // 74 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x9C, 0x07, 0x00, 0x00, 0x0E, + 0x1E, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x80, 0x03, 0x78, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 75 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 76 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, - 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 77 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, - 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 78 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 79 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, - 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, - 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, - 0x3C, // 80 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x30, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x01, 0xD8, 0x00, - 0x80, 0x03, 0xF0, 0x00, 0x00, 0x03, 0x70, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0xFC, 0xDF, 0x01, 0x00, 0xF0, 0x87, - 0x01, // 81 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, - 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x1E, 0x00, - 0x80, 0x63, 0x7C, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, // 76 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, + 0x00, 0x80, 0xFF, 0xFF, // 77 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 78 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, + 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 79 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, + 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, + 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x3C, // 80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x30, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xD8, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x03, 0xF0, 0x00, 0x00, 0x03, 0x70, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0xFC, + 0x00, 0x00, 0xFC, 0xDF, 0x01, 0x00, 0xF0, 0x87, 0x01, // 81 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, + 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, + 0x0F, 0x00, 0x80, 0xC1, 0x1E, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x00, 0x80, // 82 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, 0xE1, 0xE0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, - 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 83 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 84 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, - 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, // 85 - 0x80, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, - 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, - 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, - 0x80, // 86 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xFE, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x01, // 87 - 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, 0x3C, 0x0F, 0x00, - 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, - 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 88 - 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, - 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, // 89 - 0x00, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xDC, 0x00, 0x80, 0x01, 0xCE, 0x00, 0x80, 0x01, 0xC7, 0x00, - 0x80, 0x81, 0xC3, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0x71, 0xC0, 0x00, 0x80, 0x39, 0xC0, 0x00, 0x80, 0x1D, 0xC0, 0x00, 0x80, 0x07, 0xC0, 0x00, - 0x80, 0x03, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 90 - 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, - 0x18, // 91 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, - 0x00, 0x00, 0xC0, // 92 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, - 0x1F, // 93 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, - 0x80, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, + 0xE1, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 83 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 84 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, // 85 + 0x80, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, + 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, + 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, // 86 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, + 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xFE, 0x03, 0x00, + 0x80, 0x1F, 0x00, 0x00, 0x80, 0x01, // 87 + 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, + 0x3C, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, + 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 88 + 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, // 89 + 0x00, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xDC, 0x00, 0x80, 0x01, 0xCE, 0x00, 0x80, + 0x01, 0xC7, 0x00, 0x80, 0x81, 0xC3, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0x71, 0xC0, 0x00, 0x80, 0x39, 0xC0, 0x00, 0x80, 0x1D, + 0xC0, 0x00, 0x80, 0x07, 0xC0, 0x00, 0x80, 0x03, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 90 + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, // 91 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, + 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 92 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xFF, 0x1F, 0x80, + 0xFF, 0xFF, 0x1F, // 93 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, + 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 94 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, - 0x00, 0x00, 0x00, 0x18, // 95 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 95 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x02, // 96 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC6, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, // 97 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0x80, 0x0F, // 98 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, + 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, + 0xFF, 0x00, 0x00, 0x00, 0x80, // 97 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, + 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x80, 0x0F, // 98 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, 0x30, // 99 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x70, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 101 - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, - 0x80, 0x19, // 102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x06, 0x00, 0xE0, 0x3F, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x00, 0x38, 0xE0, 0x18, 0x00, 0x18, 0xC0, 0x18, - 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x30, 0x60, 0x1C, 0x00, 0x60, 0x30, 0x0E, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xF8, 0xFF, - 0x03, // 103 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 104 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 105 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 106 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, - 0x00, 0xC0, 0x07, 0x00, 0x00, 0x60, 0x1E, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x08, 0xC0, 0x00, 0x00, 0x00, 0x80, // 107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 109 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 111 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0xC0, 0x0F, // 112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, - 0x1F, // 113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x70, 0x30, 0x00, 0x80, 0xFF, + 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, + 0x33, 0x00, 0x00, 0xC0, 0x13, // 101 + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, + 0x19, 0x00, 0x00, 0x80, 0x19, // 102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x06, 0x00, 0xE0, 0x3F, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x00, 0x38, 0xE0, 0x18, 0x00, + 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x30, 0x60, 0x1C, 0x00, 0x60, 0x30, 0x0E, 0x00, 0xF8, + 0xFF, 0x07, 0x00, 0xF8, 0xFF, 0x03, // 103 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 104 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 105 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x60, 0x1E, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x08, + 0xC0, 0x00, 0x00, 0x00, 0x80, // 107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 108 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, + 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 109 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 110 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, + 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 111 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, + 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x0F, // 112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xF8, + 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, // 113 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, 0x3C, // 115 - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, // 116 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 117 - 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, - 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 116 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 117 + 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, + 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 118 - 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, - 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x38, // 119 - 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, 0x00, - 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, + 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, + 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x80, + 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, + 0x00, 0x00, 0x38, // 119 + 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, 0x80, // 120 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, 0x00, 0xF8, 0x0F, - 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, + 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 121 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x18, 0xF8, 0x00, 0x00, 0x18, 0xDC, 0x00, 0x00, 0x18, 0xCF, 0x00, - 0x00, 0x98, 0xC3, 0x00, 0x00, 0xD8, 0xC1, 0x00, 0x00, 0xF8, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x18, 0xF8, 0x00, 0x00, 0x18, 0xDC, 0x00, 0x00, + 0x18, 0xCF, 0x00, 0x00, 0x98, 0xC3, 0x00, 0x00, 0xD8, 0xC1, 0x00, 0x00, 0xF8, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 122 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xF9, 0x0F, 0x80, 0xFF, 0xF0, 0x1F, 0x80, 0x01, 0x00, 0x18, - 0x80, 0x01, 0x00, 0x18, // 123 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xF9, 0x0F, 0x80, 0xFF, 0xF0, 0x1F, 0x80, + 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFF, 0x3F, // 124 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xF0, 0x1F, 0x00, 0xFF, 0xFD, 0x0F, 0x00, 0x00, 0x0F, 0x00, - 0x00, 0x00, 0x06, // 125 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, - 0x00, 0x80, 0x01, // 126 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, - 0x80, 0x61, 0x60, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x00, 0x60, 0xC0, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0xC0, 0x71, 0x00, - 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x1F, // 1026 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0xA0, 0x01, 0x00, 0x00, 0xB8, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, // 1027 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xF0, 0x1F, 0x00, 0xFF, 0xFD, 0x0F, 0x00, + 0x00, 0x0F, 0x00, 0x00, 0x00, 0x06, // 125 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, // 126 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, + 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x60, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x00, 0x60, 0xC0, + 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x1F, // 1026 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0xA0, 0x01, 0x00, 0x00, 0xB8, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1027 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 8218 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x80, 0x1B, 0x00, 0x00, - 0x80, 0x19, 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0x00, 0x18, // 1107 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, - 0x00, 0x00, 0xC0, 0x07, // 8222 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, // 8230 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, // 8224 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, // 8225 - 0x00, 0x30, 0x03, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x36, 0x3B, 0x00, 0x00, 0x33, 0x73, 0x00, 0x00, 0x33, 0x63, 0x00, - 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x00, 0x03, 0x60, // 8364 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0xE0, 0x00, - 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x80, 0x0F, 0x7F, 0x00, - 0x80, 0x81, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, - 0x00, 0x00, 0x3E, // 8240 - 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, - 0x1E, // 1033 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x40, // 8249 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1034 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x01, 0x00, 0x20, 0x60, 0x03, 0x00, 0x38, 0x78, 0x07, 0x00, 0x18, 0x1E, 0x0E, 0x00, 0x08, 0x07, 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1036 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, - 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, - 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xFF, // 1035 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1039 - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, - 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x18, 0x00, 0xF0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, - 0x0F, // 1106 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x80, + 0x1B, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0x00, 0x18, // 1107 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 8222 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 8230 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 8224 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, + 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 8225 + 0x00, 0x30, 0x03, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x36, 0x3B, 0x00, 0x00, 0x33, 0x73, 0x00, 0x00, + 0x33, 0x63, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, + 0xC0, 0x00, 0x00, 0x03, 0x60, // 8364 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, + 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, + 0x3E, 0x00, 0x80, 0x0F, 0x7F, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, + 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, + 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 8240 + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, + 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, + 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1033 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, + 0x10, 0x40, // 8249 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, + 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, + 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1034 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x20, 0x60, 0x03, 0x00, 0x38, 0x78, 0x07, 0x00, 0x18, 0x1E, 0x0E, 0x00, 0x08, 0x07, + 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1036 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, + 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0x60, 0x00, + 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xFF, // 1035 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1039 + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x18, 0x00, 0xF0, + 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 1106 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, // 8216 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, // 8217 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, - 0x80, 0x19, // 8220 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, - 0x80, 0x0F, // 8221 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, - 0x00, 0xC0, 0x03, // 8226 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, // 8211 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x06, // 8212 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0xF0, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, // 8482 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, - 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, - 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1F, 0x00, 0x00, 0x80, 0x19, // 8220 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x19, 0x00, 0x00, 0x80, 0x0F, // 8221 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, + 0xE0, 0x07, 0x00, 0x00, 0xC0, 0x03, // 8226 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 8211 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 8212 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1E, 0x00, + 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, // 8482 + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, + 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1113 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, - 0x00, 0x00, 0x02, // 8250 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, - 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, - 0x00, 0x00, 0x3C, // 1114 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x02, 0x02, 0x00, 0x80, 0x03, 0x07, 0x00, - 0x80, 0xC1, 0x0D, 0x00, 0x80, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, + 0x80, 0x0F, 0x00, 0x00, 0x00, 0x02, // 8250 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, + 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1114 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x02, 0x02, 0x00, 0x80, + 0x03, 0x07, 0x00, 0x80, 0xC1, 0x0D, 0x00, 0x80, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1116 - 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, - 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 1115 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1119 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x18, 0x78, 0xC0, 0x00, 0x30, 0xE0, 0xC1, 0x00, - 0x20, 0x80, 0x77, 0x00, 0x20, 0x00, 0x3E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x30, 0xE0, 0x01, 0x00, 0x18, 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1038 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x80, 0xC1, 0x07, 0x18, 0x00, 0x03, 0x3E, 0x1C, 0x00, 0x02, 0xF8, 0x0F, - 0x00, 0x02, 0xF0, 0x03, 0x00, 0x02, 0x7F, 0x00, 0x00, 0xE3, 0x0F, 0x00, 0x80, 0xF9, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 1115 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1119 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x18, 0x78, 0xC0, 0x00, 0x30, + 0xE0, 0xC1, 0x00, 0x20, 0x80, 0x77, 0x00, 0x20, 0x00, 0x3E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x30, 0xE0, 0x01, 0x00, 0x18, 0x78, + 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1038 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x80, 0xC1, 0x07, 0x18, 0x00, 0x03, 0x3E, 0x1C, 0x00, + 0x02, 0xF8, 0x0F, 0x00, 0x02, 0xF0, 0x03, 0x00, 0x02, 0x7F, 0x00, 0x00, 0xE3, 0x0F, 0x00, 0x80, 0xF9, 0x00, 0x00, 0x00, 0x18, // 1118 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, - 0x3F, // 1032 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0x18, 0x18, 0x00, - 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xDC, 0x3B, 0x00, - 0x00, 0x08, 0x10, // 164 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, // 1032 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, + 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0xF8, + 0x1F, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0x08, 0x10, // 164 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 1168 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xE1, 0x3F, 0x80, 0xFF, 0xE1, 0x3F, // 166 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xCE, 0x07, 0x03, 0x00, 0x7F, 0x0C, 0x0F, 0x80, 0x33, 0x1C, 0x0C, 0x80, 0x71, 0x18, 0x18, - 0x80, 0x61, 0x30, 0x18, 0x80, 0xC1, 0x70, 0x18, 0x80, 0xC3, 0xE1, 0x1C, 0x00, 0x87, 0xD3, 0x0F, 0x00, 0x06, 0x9F, 0x07, 0x00, 0x00, 0x0E, // 167 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, - 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1025 - 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0xE7, 0x71, 0x00, 0x00, 0xFB, 0x67, 0x00, - 0x80, 0x19, 0xC6, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x1D, 0xCE, 0x00, - 0x00, 0x1B, 0x66, 0x00, 0x00, 0x17, 0x72, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 169 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCE, 0x3C, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, 0xC3, 0x60, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, - 0x00, 0x03, 0x60, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xCE, 0x07, 0x03, 0x00, 0x7F, 0x0C, 0x0F, 0x80, 0x33, 0x1C, 0x0C, 0x80, + 0x71, 0x18, 0x18, 0x80, 0x61, 0x30, 0x18, 0x80, 0xC1, 0x70, 0x18, 0x80, 0xC3, 0xE1, 0x1C, 0x00, 0x87, 0xD3, 0x0F, 0x00, 0x06, + 0x9F, 0x07, 0x00, 0x00, 0x0E, // 167 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, + 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, + 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1025 + 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0xE7, 0x71, 0x00, 0x00, + 0xFB, 0x67, 0x00, 0x80, 0x19, 0xC6, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, + 0xCC, 0x00, 0x80, 0x1D, 0xCE, 0x00, 0x00, 0x1B, 0x66, 0x00, 0x00, 0x17, 0x72, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, + 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 169 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCE, 0x3C, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, + 0xC3, 0x60, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x08, 0x08, // 1028 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x42, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, - 0x40, // 171 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, - 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF0, 0x07, // 172 - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x00, 0x00, 0x06, // 173 - 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFB, 0x6F, 0x00, - 0x80, 0xF9, 0xCF, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC3, 0x00, 0x80, 0xF9, 0xC7, 0x00, 0x80, 0xF1, 0xCC, 0x00, - 0x00, 0x03, 0x68, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 174 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, + 0x10, 0x42, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x40, // 171 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xF0, + 0x07, 0x00, 0x00, 0xF0, 0x07, // 172 + 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 173 + 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0xFB, 0x6F, 0x00, 0x80, 0xF9, 0xCF, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC3, 0x00, 0x80, 0xF9, + 0xC7, 0x00, 0x80, 0xF1, 0xCC, 0x00, 0x00, 0x03, 0x68, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, + 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 174 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 1031 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, - 0x00, 0x11, 0x00, 0x00, 0x00, 0x0E, // 176 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, - 0x00, 0xFC, 0xFF, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, - 0x00, 0x80, 0xC1, // 177 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, + 0x20, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0E, // 176 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, + 0x80, 0xC1, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, + 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, // 177 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1030 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 1110 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 1169 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 181 - 0x00, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0xFF, 0x1F, - 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, // 182 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, - 0x01, // 183 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x1B, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x73, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 1105 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, - 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, - 0x00, 0x00, 0x78, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x63, 0x00, - 0x00, 0xF0, 0x67, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0x18, 0x6C, 0x00, 0x00, 0x18, 0x6C, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0xF0, 0x67, 0x00, - 0x00, 0xE0, 0x63, // 8470 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 1169 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 181 + 0x00, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, + 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, + 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 182 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 183 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, + 0x1B, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x73, 0x63, 0x00, 0x00, 0xE0, + 0x33, 0x00, 0x00, 0xC0, 0x13, // 1105 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, + 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x63, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0x18, 0x6C, 0x00, + 0x00, 0x18, 0x6C, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xE0, 0x63, // 8470 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x60, 0x18, // 1108 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, - 0x00, 0x80, 0x0F, 0x00, 0x00, 0x10, 0x42, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x02, // 187 - 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 1112 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, 0xE1, 0xE0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, - 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 1029 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, + 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x10, 0x42, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, + 0x0F, 0x00, 0x00, 0x00, 0x02, // 187 + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 1112 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, + 0xE1, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 1029 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, 0x3C, // 1109 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 1111 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x3E, 0x06, 0x00, - 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, - 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 1040 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0x81, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1041 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, - 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1042 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, // 1043 - 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xDF, 0x00, 0x80, 0xFF, 0xC7, 0x00, 0x80, 0x7F, 0xC0, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, + 0x3E, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, + 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 1040 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1041 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1042 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1043 + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xDF, 0x00, 0x80, 0xFF, 0xC7, 0x00, 0x80, + 0x7F, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x0F, // 1044 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1045 - 0x80, 0x01, 0x80, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x38, 0x07, 0x00, - 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x38, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, - 0x00, 0x07, 0x3C, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1046 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x78, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xE0, 0x00, - 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xE1, 0x00, 0x00, 0xBF, 0x73, 0x00, - 0x00, 0x1C, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 1047 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x00, - 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1048 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x18, 0x00, 0x3C, 0x00, - 0x30, 0x00, 0x0E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x20, 0xC0, 0x01, 0x00, 0x20, 0xF0, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0x1E, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1049 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x78, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1050 - 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1051 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, - 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, - 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1052 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, - 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1053 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1054 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1055 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, - 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, - 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, - 0x3C, // 1056 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, + 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1045 + 0x80, 0x01, 0x80, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, + 0x38, 0x07, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, + 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x03, + 0x00, 0x00, 0x38, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, + 0x80, 0x01, 0x80, // 1046 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x78, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, + 0x01, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, + 0xE1, 0x00, 0x00, 0xBF, 0x73, 0x00, 0x00, 0x1C, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 1047 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1048 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x18, + 0x00, 0x3C, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x20, 0xC0, 0x01, 0x00, 0x20, 0xF0, 0x00, 0x00, 0x30, 0x38, + 0x00, 0x00, 0x18, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1049 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x78, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, + 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1050 + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1051 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, + 0x00, 0x80, 0xFF, 0xFF, // 1052 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1053 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, + 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1054 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1055 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, + 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, + 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x3C, // 1056 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, + 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, 0x0C, // 1057 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1058 - 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0xE0, 0xC1, 0x00, - 0x00, 0x80, 0x77, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, - 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1059 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x0E, 0x38, 0x00, - 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, - 0x00, 0x0E, 0x38, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 1060 - 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, 0x3C, 0x0F, 0x00, - 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, - 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 1061 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1062 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1063 - 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1058 + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, + 0xE0, 0xC1, 0x00, 0x00, 0x80, 0x77, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1059 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, + 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x06, + 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, + 0x00, 0x00, 0xE0, 0x03, // 1060 + 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, + 0x3C, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, + 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 1061 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, + 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1062 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1063 + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, + 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1064 - 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1065 - 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1066 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, - 0x80, 0xFF, 0xFF, // 1067 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, - 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1068 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xC3, 0x60, 0x00, - 0x00, 0xC7, 0x70, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, + 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, + 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1065 + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, + 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, + 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, + 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1066 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, + 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, + 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1067 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, + 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, + 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1068 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, + 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, + 0xC0, 0x00, 0x00, 0xC3, 0x60, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1069 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, - 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, - 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, - 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1070 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x80, 0xC1, 0x1E, 0x00, - 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, - 0x80, 0xC1, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1071 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC6, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, // 1072 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x73, 0x70, 0x00, 0x00, 0x31, 0xE0, 0x00, 0x80, 0x19, 0xC0, 0x00, - 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x39, 0xE0, 0x00, 0x80, 0x71, 0x70, 0x00, 0x80, 0xE1, 0x3F, 0x00, - 0x80, 0x80, 0x0F, // 1073 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0xF0, 0xE7, 0x00, 0x00, 0xF0, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1074 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1075 - 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xF8, 0xDF, 0x00, 0x00, 0xF8, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, - 0x00, 0x00, 0xC0, 0x0F, // 1076 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 1077 - 0x00, 0x18, 0xE0, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0xF8, 0x38, 0x00, - 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1078 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE7, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, - 0x3C, // 1079 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, - 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1080 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x03, 0x3C, 0x00, - 0x00, 0x02, 0x0F, 0x00, 0x00, 0x82, 0x07, 0x00, 0x00, 0xE2, 0x01, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1081 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, - 0x00, 0xC0, 0x0D, 0x00, 0x00, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, + 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, + 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, + 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1070 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x80, + 0xC1, 0x1E, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, + 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1071 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, + 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, + 0xFF, 0x00, 0x00, 0x00, 0x80, // 1072 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x73, 0x70, 0x00, 0x00, 0x31, 0xE0, 0x00, 0x80, + 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x39, 0xE0, 0x00, 0x80, 0x71, + 0x70, 0x00, 0x80, 0xE1, 0x3F, 0x00, 0x80, 0x80, 0x0F, // 1073 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0xF0, 0xE7, 0x00, 0x00, 0xF0, + 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1074 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1075 + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xF8, 0xDF, 0x00, 0x00, 0xF8, 0xC3, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x0F, // 1076 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, + 0x33, 0x00, 0x00, 0xC0, 0x13, // 1077 + 0x00, 0x18, 0xE0, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, + 0x0D, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1078 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE7, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0x3C, // 1079 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1080 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, + 0x03, 0x3C, 0x00, 0x00, 0x02, 0x0F, 0x00, 0x00, 0x82, 0x07, 0x00, 0x00, 0xE2, 0x01, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x80, 0xF9, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1081 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0xC0, 0x0D, 0x00, 0x00, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1082 - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1083 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, - 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1084 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1085 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1086 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1087 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0xC0, 0x0F, // 1088 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, + 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1083 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, + 0xF0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, + 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1084 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1085 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, + 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1086 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1087 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, + 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, + 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x0F, // 1088 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, 0x30, // 1089 - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, - 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x18, // 1090 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, 0x00, 0xF8, 0x0F, - 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, + 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1090 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, + 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 1091 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x70, 0x60, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x00, 0x70, 0x60, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, - 0x00, 0xC0, 0x1F, // 1092 - 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, 0x00, - 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, + 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x70, 0x60, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, + 0xFF, 0x1F, 0x00, 0x70, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xE0, + 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1092 + 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, 0x80, // 1093 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, - 0x00, 0x00, 0xC0, 0x1F, // 1094 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, - 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1094 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, + 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1095 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1096 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, - 0x00, 0x00, 0xC0, - 0x1F, // 1097 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, - 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, - 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1098 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, - 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1099 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, - 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1100 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC3, 0x00, - 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1096 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, + 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1097 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, + 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, + 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1098 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, + 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, + 0x7E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1099 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, + 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, + 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1100 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, + 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xC0, 0x1F, // 1101 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, - 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, - 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1102 - 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00, 0x18, 0x0E, 0x00, - 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, + 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, + 0x00, 0x00, 0xC0, 0x1F, // 1102 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00, + 0x18, 0x0E, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1103 }; diff --git a/src/graphics/images.h b/src/graphics/images.h index 9ed9402b9..ef9ffef78 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -2,8 +2,9 @@ #define SATELLITE_IMAGE_WIDTH 16 #define SATELLITE_IMAGE_HEIGHT 15 -const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02, 0xF8, 0x00, 0xF0, 0x01, 0xE0, - 0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; +const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02, + 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54, + 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; #define imgSatellite_width 8 #define imgSatellite_height 8 @@ -15,7 +16,8 @@ const uint8_t imgUSB[] PROGMEM = {0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe const uint8_t imgUSB_HighResolution[] PROGMEM = {0x00, 0x3e, 0xf8, 0x80, 0x43, 0xf8, 0xc0, 0xc2, 0xff, 0x60, 0x42, 0xfc, 0x3c, 0xc2, 0xff, 0x22, 0x42, 0xf8, 0x3d, 0x42, 0xf8, 0x22, 0xc2, 0xff, 0x61, 0x42, 0xfc, 0xc0, 0xc2, 0xff, 0x80, 0x43, 0xf8, 0x00, 0x3e, 0xf8}; -const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; +const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, + 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3C}; const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF}; const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF}; @@ -24,16 +26,18 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \ - defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_ST7796) || \ - defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgInfoL1[] PROGMEM = {0xff, 0x01, 0x01, 0x01, 0x1e, 0x7f, 0x1e, 0x01, 0x01, 0x01, 0x01, 0xff}; const uint8_t imgInfoL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; -const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01, 0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb}; -const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, 0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f}; +const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01, + 0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb}; +const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, + 0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f}; #else const uint8_t imgInfo[] PROGMEM = {0xff, 0x81, 0x00, 0xfb, 0xfb, 0x00, 0x81, 0xff}; const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1, 0xdf}; @@ -42,21 +46,21 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, // === Horizontal battery === // Basic battery design and all related pieces -const unsigned char batteryBitmap_h_bottom[] PROGMEM = {0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, - 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, - 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, - 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; +const unsigned char batteryBitmap_h_bottom[] PROGMEM = { + 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, + 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; -const unsigned char batteryBitmap_h_top[] PROGMEM = {0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, - 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, - 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, - 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; +const unsigned char batteryBitmap_h_top[] PROGMEM = { + 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, + 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, + 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; // Lightning Bolt -const unsigned char lightning_bolt_h[] PROGMEM = {0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, - 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, - 0b01111000, 0b00000000, 0b00111100, 0b00000000, 0b00011100, 0b00000000, 0b00001100, - 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; +const unsigned char lightning_bolt_h[] PROGMEM = { + 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, + 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000, + 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; // === Vertical battery === // Basic battery design and all related pieces @@ -127,7 +131,8 @@ const uint8_t icon_system[] PROGMEM = { }; // 🌐 Wi-Fi -const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000, 0b00011000, 0b00000000}; +const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, + 0b11011011, 0b00011000, 0b00011000, 0b00000000}; const uint8_t icon_nodes[] PROGMEM = { 0xF9, // Row 0 #..####### @@ -227,23 +232,27 @@ const uint8_t mute_symbol[] PROGMEM = { #define mute_symbol_big_width 16 #define mute_symbol_big_height 16 -const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, 0b00001000, - 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, 0b10001000, 0b00010000, - 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, 0b00010100, 0b00000100, 0b00101000, - 0b11111100, 0b00111111, 0b01000000, 0b00100010, 0b10000000, 0b01000001, 0b00000000, 0b10000000}; +const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, + 0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, + 0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, + 0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010, + 0b10000000, 0b01000001, 0b00000000, 0b10000000}; // Bell icon for Alert Message #define bell_alert_width 8 #define bell_alert_height 8 -const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000}; +const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, + 0b01000010, 0b01000010, 0b11111111, 0b00011000}; #define key_symbol_width 8 #define key_symbol_height 8 -const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, 0b10101001, 0b10000110, 0b00000000, 0b00000000}; +const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, + 0b10101001, 0b10000110, 0b00000000, 0b00000000}; #define placeholder_width 8 #define placeholder_height 8 -const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111}; +const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, 0b11111111}; #define icon_node_width 8 #define icon_node_height 8 @@ -260,35 +269,40 @@ static const uint8_t icon_node[] PROGMEM = { #define bluetoothdisabled_width 8 #define bluetoothdisabled_height 8 -const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, 0b01001100, 0b00000000, 0b00000000, 0b00000000}; +const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, + 0b01001100, 0b00000000, 0b00000000, 0b00000000}; #define smallbulletpoint_width 8 #define smallbulletpoint_height 8 -const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; +const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000}; // Digital Clock #define digital_icon_clock_width 8 #define digital_icon_clock_height 8 -const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, 0b10010001, 0b10000001, 0b01000010, 0b00111100}; +const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, + 0b10010001, 0b10000001, 0b01000010, 0b00111100}; // Analog Clock #define analog_icon_clock_width 8 #define analog_icon_clock_height 8 -const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; +const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, + 0b00100100, 0b01000010, 0b01000010, 0b11111111}; #define chirpy_width 38 #define chirpy_height 50 const uint8_t chirpy[] = { - 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, - 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x81, - 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, - 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, - 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, - 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, - 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, - 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, - 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, - 0x0c, 0x0c, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, - 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; + 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, + 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, + 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, + 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, + 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, + 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, + 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, + 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, + 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, + 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, + 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, + 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; #define chirpy_small_image_width 8 #define chirpy_small_image_height 8 diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp index 14af1d351..6d9b709b1 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp @@ -10,86 +10,99 @@ using namespace NicheGraphics::Drivers; // Private constructor // Called by getInstance -LatchingBacklight::LatchingBacklight() { - // Attach the deep sleep callback - deepSleepObserver.observe(¬ifyDeepSleep); +LatchingBacklight::LatchingBacklight() +{ + // Attach the deep sleep callback + deepSleepObserver.observe(¬ifyDeepSleep); } // Get access to (or create) the singleton instance of this class -LatchingBacklight *LatchingBacklight::getInstance() { - // Instantiate the class the first time this method is called - static LatchingBacklight *const singletonInstance = new LatchingBacklight; +LatchingBacklight *LatchingBacklight::getInstance() +{ + // Instantiate the class the first time this method is called + static LatchingBacklight *const singletonInstance = new LatchingBacklight; - return singletonInstance; + return singletonInstance; } // Which pin controls the backlight? // Is the light active HIGH (default) or active LOW? -void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) { - this->pin = pin; - this->logicActive = activeWhen; +void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) +{ + this->pin = pin; + this->logicActive = activeWhen; - pinMode(pin, OUTPUT); - off(); // Explicit off seem required by T-Echo? + pinMode(pin, OUTPUT); + off(); // Explicit off seem required by T-Echo? } // Called when device is shutting down // Ensures the backlight is off -int LatchingBacklight::beforeDeepSleep(void *unused) { - // Contingency only - // - pin wasn't set - if (pin != (uint8_t)-1) { - off(); - pinMode(pin, INPUT); // High impedance - unnecessary? - } else - LOG_WARN("LatchingBacklight instantiated, but pin not set"); - return 0; // Continue with deep sleep +int LatchingBacklight::beforeDeepSleep(void *unused) +{ + // Contingency only + // - pin wasn't set + if (pin != (uint8_t)-1) { + off(); + pinMode(pin, INPUT); // High impedance - unnecessary? + } else + LOG_WARN("LatchingBacklight instantiated, but pin not set"); + return 0; // Continue with deep sleep } // Turn the backlight on *temporarily* // This should be used for momentary illumination, such as while a button is held // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling -void LatchingBacklight::peek() { - assert(pin != (uint8_t)-1); - digitalWrite(pin, logicActive); // On - on = true; - latched = false; +void LatchingBacklight::peek() +{ + assert(pin != (uint8_t)-1); + digitalWrite(pin, logicActive); // On + on = true; + latched = false; } // Turn the backlight on, and keep it on // This should be used when the backlight should remain active, even after user input ends // e.g. when enabled via the menu // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling -void LatchingBacklight::latch() { - assert(pin != (uint8_t)-1); +void LatchingBacklight::latch() +{ + assert(pin != (uint8_t)-1); + + // Blink if moving from peek to latch + // Indicates to user that the transition has taken place + if (on && !latched) { + digitalWrite(pin, !logicActive); // Off + delay(25); + digitalWrite(pin, logicActive); // On + delay(25); + digitalWrite(pin, !logicActive); // Off + delay(25); + } - // Blink if moving from peek to latch - // Indicates to user that the transition has taken place - if (on && !latched) { - digitalWrite(pin, !logicActive); // Off - delay(25); digitalWrite(pin, logicActive); // On - delay(25); - digitalWrite(pin, !logicActive); // Off - delay(25); - } - - digitalWrite(pin, logicActive); // On - on = true; - latched = true; + on = true; + latched = true; } // Turn the backlight off // Suitable for ending both peek and latch -void LatchingBacklight::off() { - assert(pin != (uint8_t)-1); - digitalWrite(pin, !logicActive); // Off - on = false; - latched = false; +void LatchingBacklight::off() +{ + assert(pin != (uint8_t)-1); + digitalWrite(pin, !logicActive); // Off + on = false; + latched = false; } -bool LatchingBacklight::isOn() { return on; } +bool LatchingBacklight::isOn() +{ + return on; +} -bool LatchingBacklight::isLatched() { return latched; } +bool LatchingBacklight::isLatched() +{ + return latched; +} #endif diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h index be7eb41de..0097cae4c 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h @@ -15,34 +15,36 @@ #include "Observer.h" -namespace NicheGraphics::Drivers { +namespace NicheGraphics::Drivers +{ -class LatchingBacklight { -public: - static LatchingBacklight *getInstance(); // Create or get the singleton instance - void setPin(uint8_t pin, bool activeWhen = HIGH); +class LatchingBacklight +{ + public: + static LatchingBacklight *getInstance(); // Create or get the singleton instance + void setPin(uint8_t pin, bool activeWhen = HIGH); - int beforeDeepSleep(void *unused); // Callback for auto-shutoff + int beforeDeepSleep(void *unused); // Callback for auto-shutoff - void peek(); // Backlight on temporarily, e.g. while button held - void latch(); // Backlight on permanently, e.g. toggled via menu - void off(); // Backlight off. Suitable for both peek and latch + void peek(); // Backlight on temporarily, e.g. while button held + void latch(); // Backlight on permanently, e.g. toggled via menu + void off(); // Backlight off. Suitable for both peek and latch - bool isOn(); // Either peek or latch - bool isLatched(); + bool isOn(); // Either peek or latch + bool isLatched(); -private: - LatchingBacklight(); // Constructor made private: force use of getInstance + private: + LatchingBacklight(); // Constructor made private: force use of getInstance - // Get notified when the system is shutting down - CallbackObserver deepSleepObserver = - CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); + // Get notified when the system is shutting down + CallbackObserver deepSleepObserver = + CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); - uint8_t pin = (uint8_t)-1; - bool logicActive = HIGH; // Is light active HIGH or active LOW + uint8_t pin = (uint8_t)-1; + bool logicActive = HIGH; // Is light active HIGH or active LOW - bool on = false; // Is light on (either peek or latched) - bool latched = false; // Is light latched on + bool on = false; // Is light on (either peek or latched) + bool latched = false; // Is light latched on }; } // namespace NicheGraphics::Drivers \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp index 6c47d7f28..2c8df96ed 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp @@ -30,99 +30,103 @@ static const uint8_t LUT_FAST[] = { }; // How strongly the pixels are pulled and pushed -void DEPG0213BNS800::configVoltages() { - switch (updateType) { - case FAST: - // Reference: display datasheet, GxEPD1 - sendCommand(0x03); // Gate voltage - sendData(0x17); // VGH: 20V +void DEPG0213BNS800::configVoltages() +{ + switch (updateType) { + case FAST: + // Reference: display datasheet, GxEPD1 + sendCommand(0x03); // Gate voltage + sendData(0x17); // VGH: 20V - // Reference: display datasheet, GxEPD1 - sendCommand(0x04); // Source voltage - sendData(0x41); // VSH1: 15V - sendData(0x00); // VSH2: NA - sendData(0x32); // VSL: -15V + // Reference: display datasheet, GxEPD1 + sendCommand(0x04); // Source voltage + sendData(0x41); // VSH1: 15V + sendData(0x00); // VSH2: NA + sendData(0x32); // VSL: -15V - // GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard - sendCommand(0x2C); // VCOM voltage - sendData(0x08); // VCOM: -0.2V - break; + // GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard + sendCommand(0x2C); // VCOM voltage + sendData(0x08); // VCOM: -0.2V + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory -void DEPG0213BNS800::configWaveform() { - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x80); // VSS +void DEPG0213BNS800::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VSS - sendCommand(0x32); // Write LUT register from MCU: - sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) - break; + sendCommand(0x32); // Write LUT register from MCU: + sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void DEPG0213BNS800::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xCF); // Differential, use manually loaded waveform - break; +void DEPG0213BNS800::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xCF); // Differential, use manually loaded waveform + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void DEPG0213BNS800::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms, then poll every 50ms - case FULL: - default: - return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms - } +void DEPG0213BNS800::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms, then poll every 50ms + case FULL: + default: + return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms + } } // For this display, we do not need to re-write the new image. // We're overriding SSD16XX::finalizeUpdate to make this small optimization. // The display does also work just fine with the generic SSD16XX method, though. -void DEPG0213BNS800::finalizeUpdate() { - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in - // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST - // etc. - if (updateType != FULL) { - // writeNewImage(); // Not required for this display - writeOldImage(); - sendCommand(0x7F); // Terminate image write without update - wait(); - } +void DEPG0213BNS800::finalizeUpdate() +{ + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place + // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. + if (updateType != FULL) { + // writeNewImage(); // Not required for this display + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } - // Enter deep-sleep to save a few µA - // Waking from this requires that display's reset pin is broken out - if (pin_rst != 0xFF) - deepSleep(); + // Enter deep-sleep to save a few µA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h index a17f274d1..3ce16e473 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -19,23 +19,25 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class DEPG0213BNS800 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class DEPG0213BNS800 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte + public: + DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte -protected: - void configVoltages() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization + protected: + void configVoltages() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; + void finalizeUpdate() override; // Only overriden for a slight optimization }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp index 2664cec21..15134d5ad 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp @@ -31,91 +31,95 @@ static const uint8_t LUT_FAST[] = { }; // How strongly the pixels are pulled and pushed -void DEPG0290BNS800::configVoltages() { - switch (updateType) { - case FAST: - // Listed as "typical" in datasheet - sendCommand(0x04); - sendData(0x41); // VSH1 15V - sendData(0x00); // VSH2 NA - sendData(0x32); // VSL -15V - break; +void DEPG0290BNS800::configVoltages() +{ + switch (updateType) { + case FAST: + // Listed as "typical" in datasheet + sendCommand(0x04); + sendData(0x41); // VSH1 15V + sendData(0x00); // VSH2 NA + sendData(0x32); // VSL -15V + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory -void DEPG0290BNS800::configWaveform() { - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x60); // Actively hold screen border during update +void DEPG0290BNS800::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x60); // Actively hold screen border during update - sendCommand(0x32); // Write LUT register from MCU: - sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) - break; + sendCommand(0x32); // Write LUT register from MCU: + sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void DEPG0290BNS800::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xCF); // Differential, use manually loaded waveform - break; +void DEPG0290BNS800::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xCF); // Differential, use manually loaded waveform + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void DEPG0290BNS800::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 450); // At least 450ms for fast refresh - case FULL: - default: - return beginPolling(100, 3000); // At least 3 seconds for full refresh - } +void DEPG0290BNS800::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 450); // At least 450ms for fast refresh + case FULL: + default: + return beginPolling(100, 3000); // At least 3 seconds for full refresh + } } // For this display, we do not need to re-write the new image. // We're overriding SSD16XX::finalizeUpdate to make this small optimization. // The display does also work just fine with the generic SSD16XX method, though. -void DEPG0290BNS800::finalizeUpdate() { - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in - // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST - // etc. - if (updateType != FULL) { - // writeNewImage(); // Not required for this display - writeOldImage(); - sendCommand(0x7F); // Terminate image write without update - wait(); - } +void DEPG0290BNS800::finalizeUpdate() +{ + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place + // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. + if (updateType != FULL) { + // writeNewImage(); // Not required for this display + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } - // Enter deep-sleep to save a few µA - // Waking from this requires that display's reset pin is broken out - if (pin_rst != 0xFF) - deepSleep(); + // Enter deep-sleep to save a few µA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h index d6f02184a..257fed1a6 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -17,23 +17,25 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class DEPG0290BNS800 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 128; - static constexpr uint32_t height = 296; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class DEPG0290BNS800 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 128; + static constexpr uint32_t height = 296; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte + public: + DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte -protected: - void configVoltages() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization + protected: + void configVoltages() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; + void finalizeUpdate() override; // Only overriden for a slight optimization }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.cpp b/src/graphics/niche/Drivers/EInk/E0213A367.cpp index a12688df9..f19cb4ff7 100644 --- a/src/graphics/niche/Drivers/EInk/E0213A367.cpp +++ b/src/graphics/niche/Drivers/EInk/E0213A367.cpp @@ -5,77 +5,80 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void E0213A367::configScanning() { - // "Driver output control" - // Scan gates from 0 to 249 (vertical resolution 250px) - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); +void E0213A367::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels -void E0213A367::configWaveform() { - // This command (0x37) is poorly documented - // As of July 2025, the datasheet for this display's controller IC is unavailable - // The values are supplied by Heltec, who presumably have privileged access to information from the display - // manufacturer Datasheet for the similar SSD1680 IC hints at the function of this command: +void E0213A367::configWaveform() +{ + // This command (0x37) is poorly documented + // As of July 2025, the datasheet for this display's controller IC is unavailable + // The values are supplied by Heltec, who presumably have privileged access to information from the display manufacturer + // Datasheet for the similar SSD1680 IC hints at the function of this command: - // "Spare VCOM OTP selection": - // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. - // Maybe value is redundant? No noticeable impact when set to 0x00. - // We'll leave it set to 0x40, following Heltec's lead, just in case. + // "Spare VCOM OTP selection": + // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. + // Maybe value is redundant? No noticeable impact when set to 0x00. + // We'll leave it set to 0x40, following Heltec's lead, just in case. - // "Display Mode" - // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential - // refresh) + // "Display Mode" + // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential refresh) - // Unusual that waveforms are programmed to OTP, but this meta information is not ..? + // Unusual that waveforms are programmed to OTP, but this meta information is not ..? - sendCommand(0x37); // "Write Register for Display Option" ? - sendData(0x40); // "Spare VCOM OTP selection" ? - sendData(0x80); // "Display Mode for WS[7:0]" ? - sendData(0x03); // "Display Mode for WS[15:8]" ? - sendData(0x0E); // "Display Mode [23:16]" ? + sendCommand(0x37); // "Write Register for Display Option" ? + sendData(0x40); // "Spare VCOM OTP selection" ? + sendData(0x80); // "Display Mode for WS[7:0]" ? + sendData(0x03); // "Display Mode for WS[15:8]" ? + sendData(0x0E); // "Display Mode [23:16]" ? - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. - break; - case FULL: - default: - sendCommand(0x3C); // Border waveform: - sendData(0x01); // Follow LUT 1 (blink same as white pixels) - break; - } + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } } // Tell controller IC which operations to run -void E0213A367::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh" - break; - } +void E0213A367::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh" + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void E0213A367::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 1500); // At least 1.5 seconds for full refresh - } +void E0213A367::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.h b/src/graphics/niche/Drivers/EInk/E0213A367.h index fc089eb42..a36fcb407 100644 --- a/src/graphics/niche/Drivers/EInk/E0213A367.h +++ b/src/graphics/niche/Drivers/EInk/E0213A367.h @@ -17,22 +17,24 @@ E-Ink display driver #include "./SSD1682.h" -namespace NicheGraphics::Drivers { -class E0213A367 : public SSD1682 { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class E0213A367 : public SSD1682 +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - E0213A367() : SSD1682(width, height, supported, 0) {} + public: + E0213A367() : SSD1682(width, height, supported, 0) {} -protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/EInk.cpp b/src/graphics/niche/Drivers/EInk/EInk.cpp index b45699a88..cd2e9dc98 100644 --- a/src/graphics/niche/Drivers/EInk/EInk.cpp +++ b/src/graphics/niche/Drivers/EInk/EInk.cpp @@ -6,76 +6,81 @@ using namespace NicheGraphics::Drivers; // Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported) - : concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported) { - OSThread::disable(); + : concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported) +{ + OSThread::disable(); } // Used by NicheGraphics implementations to check if a display supports a specific refresh operation. // Whether or not the update type is supported is specified in the constructor -bool EInk::supports(UpdateTypes type) { - // The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set. - if (supportedUpdateTypes & type) - return true; - else - return false; +bool EInk::supports(UpdateTypes type) +{ + // The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set. + if (supportedUpdateTypes & type) + return true; + else + return false; } // Begins using the OSThread to detect when a display update is complete // This allows the refresh operation to run "asynchronously". -// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY -// pin The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an -// update takes. Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", provided its -// isUpdateDone() override always returns true. -void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) { - updateRunning = true; - pollingInterval = interval; - pollingBegunAt = millis(); +// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin +// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes. +// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", +// provided its isUpdateDone() override always returns true. +void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) +{ + updateRunning = true; + pollingInterval = interval; + pollingBegunAt = millis(); - // To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will - // take By default, expectedDuration is 0, and we'll start polling immediately - OSThread::setIntervalFromNow(expectedDuration); - OSThread::enabled = true; + // To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take + // By default, expectedDuration is 0, and we'll start polling immediately + OSThread::setIntervalFromNow(expectedDuration); + OSThread::enabled = true; } // Meshtastic's pseudo-threading layer // We're using this as a timer, to periodically check if an update is complete // This is what allows us to update the display asynchronously -int32_t EInk::runOnce() { - // Check for polling timeout - // Manually set at 10 seconds, in case some big task holds up the firmware's cooperative multitasking - if (millis() - pollingBegunAt > 10000) - failed = true; +int32_t EInk::runOnce() +{ + // Check for polling timeout + // Manually set at 10 seconds, in case some big task holds up the firmware's cooperative multitasking + if (millis() - pollingBegunAt > 10000) + failed = true; - // Handle failure - // - polling timeout - // - other error (derived classes) - if (failed) { - LOG_WARN("Display update failed. Check wiring & power supply."); - updateRunning = false; - failed = false; - return disable(); - } + // Handle failure + // - polling timeout + // - other error (derived classes) + if (failed) { + LOG_WARN("Display update failed. Check wiring & power supply."); + updateRunning = false; + failed = false; + return disable(); + } - // If update not yet done - if (!isUpdateDone()) - return pollingInterval; // Poll again in a few ms + // If update not yet done + if (!isUpdateDone()) + return pollingInterval; // Poll again in a few ms - // If update done - finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc - updateRunning = false; // Change what we report via EInk::busy() - return disable(); // Stop polling + // If update done + finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc + updateRunning = false; // Change what we report via EInk::busy() + return disable(); // Stop polling } // Wait for an in progress update to complete before continuing // Run a normal (async) update first, *then* call await -void EInk::await() { - // Stop our concurrency thread - OSThread::disable(); +void EInk::await() +{ + // Stop our concurrency thread + OSThread::disable(); - // Sit and block until the update is complete - while (updateRunning) { - runOnce(); - yield(); - } + // Sit and block until the update is complete + while (updateRunning) { + runOnce(); + yield(); + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/EInk.h b/src/graphics/niche/Drivers/EInk/EInk.h index 2b39b3e97..3c51d4f1d 100644 --- a/src/graphics/niche/Drivers/EInk/EInk.h +++ b/src/graphics/niche/Drivers/EInk/EInk.h @@ -12,42 +12,44 @@ #include "concurrency/OSThread.h" #include -namespace NicheGraphics::Drivers { +namespace NicheGraphics::Drivers +{ -class EInk : private concurrency::OSThread { -public: - // Different possible operations used to update an E-Ink display - // Some displays will not support all operations - // Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType) - enum UpdateTypes : uint8_t { - UNSPECIFIED = 0, - FULL = 1 << 0, - FAST = 1 << 1, // "Partial Refresh" - }; +class EInk : private concurrency::OSThread +{ + public: + // Different possible operations used to update an E-Ink display + // Some displays will not support all operations + // Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType) + enum UpdateTypes : uint8_t { + UNSPECIFIED = 0, + FULL = 1 << 0, + FAST = 1 << 1, // "Partial Refresh" + }; - EInk(uint16_t width, uint16_t height, UpdateTypes supported); - virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0; - virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image - void await(); // Wait for an in-progress update to complete before proceeding - bool supports(UpdateTypes type); // Can display perform a certain update type - bool busy() { return updateRunning; } // Display able to update right now? + EInk(uint16_t width, uint16_t height, UpdateTypes supported); + virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0; + virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image + void await(); // Wait for an in-progress update to complete before proceeding + bool supports(UpdateTypes type); // Can display perform a certain update type + bool busy() { return updateRunning; } // Display able to update right now? - const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const. - const uint16_t height; + const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const. + const uint16_t height; -protected: - void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished - virtual bool isUpdateDone() = 0; // Check once if update finished - virtual void finalizeUpdate() {} // Run any post-update code - bool failed = false; // If an error occurred during update + protected: + void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished + virtual bool isUpdateDone() = 0; // Check once if update finished + virtual void finalizeUpdate() {} // Run any post-update code + bool failed = false; // If an error occurred during update -private: - int32_t runOnce() override; // Repeated checking if update finished + private: + int32_t runOnce() override; // Repeated checking if update finished - const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class - bool updateRunning = false; // see EInk::busy() - uint32_t pollingInterval = 0; // How often to check if update complete (ms) - uint32_t pollingBegunAt = 0; // To timeout during polling + const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class + bool updateRunning = false; // see EInk::busy() + uint32_t pollingInterval = 0; // How often to check if update complete (ms) + uint32_t pollingBegunAt = 0; // To timeout during polling }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp index 3bdb2980b..9a06fa841 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp @@ -5,50 +5,54 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void GDEY0154D67::configScanning() { - // "Driver output control" - sendCommand(0x01); - sendData(0xC7); // Scan until gate 199 (200px vertical res.) - sendData(0x00); - sendData(0x00); +void GDEY0154D67::configScanning() +{ + // "Driver output control" + sendCommand(0x01); + sendData(0xC7); // Scan until gate 199 (200px vertical res.) + sendData(0x00); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void GDEY0154D67::configWaveform() { - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void GDEY0154D67::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void GDEY0154D67::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void GDEY0154D67::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void GDEY0154D67::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 300); // At least 300ms for fast refresh - case FULL: - default: - return beginPolling(100, 1500); // At least 1.5 seconds for full refresh - } +void GDEY0154D67::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 300); // At least 300ms for fast refresh + case FULL: + default: + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h index 6e4025b54..e391eea50 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h @@ -17,22 +17,24 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class GDEY0154D67 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 200; - static constexpr uint32_t height = 200; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class GDEY0154D67 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 200; + static constexpr uint32_t height = 200; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - GDEY0154D67() : SSD16XX(width, height, supported) {} + public: + GDEY0154D67() : SSD16XX(width, height, supported) {} -protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp index ac214dfc8..b3a585eb7 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp @@ -5,50 +5,54 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void GDEY0213B74::configScanning() { - // "Driver output control" - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); - sendData(0x00); +void GDEY0213B74::configScanning() +{ + // "Driver output control" + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void GDEY0213B74::configWaveform() { - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void GDEY0213B74::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void GDEY0213B74::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void GDEY0213B74::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void GDEY0213B74::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh - } +void GDEY0213B74::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h index 50ffc8e65..1c36f295d 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h @@ -19,22 +19,24 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class GDEY0213B74 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class GDEY0213B74 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - GDEY0213B74() : SSD16XX(width, height, supported) {} + public: + GDEY0213B74() : SSD16XX(width, height, supported) {} -protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp index 3f8c4f8b2..0509b0502 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp @@ -5,53 +5,57 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void HINK_E0213A289::configScanning() { - // "Driver output control" - // Scan gates from 0 to 249 (vertical resolution 250px) - sendCommand(0x01); - sendData(0xF9); // Maximum gate # (249, bits 0-7) - sendData(0x00); // Maximum gate # (bit 8) - sendData(0x00); // (Do not invert scanning order) +void HINK_E0213A289::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); // Maximum gate # (249, bits 0-7) + sendData(0x00); // Maximum gate # (bit 8) + sendData(0x00); // (Do not invert scanning order) } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void HINK_E0213A289::configWaveform() { - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void HINK_E0213A289::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void HINK_E0213A289::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void HINK_E0213A289::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void HINK_E0213A289::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) - } +void HINK_E0213A289::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h index 765413864..eab0bf59d 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h @@ -19,22 +19,24 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class HINK_E0213A289 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class HINK_E0213A289 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} + public: + HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} -protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp b/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp index b2e172e13..1b72bc4a9 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp +++ b/src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp @@ -7,49 +7,52 @@ using namespace NicheGraphics::Drivers; // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory -void HINK_E042A87::configWaveform() { - sendCommand(0x3C); // Border waveform: - sendData(0x01); // Follow LUT for VSH1 +void HINK_E042A87::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT for VSH1 - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc -void HINK_E042A87::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x21); // Use both "old" and "new" image memory (differential) - sendData(0x00); - sendData(0x00); +void HINK_E042A87::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x21); // Use both "old" and "new" image memory (differential) + sendData(0x00); + sendData(0x00); - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Differential, load waveform from OTP - break; + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Differential, load waveform from OTP + break; - case FULL: - default: - sendCommand(0x21); // Bypass "old" image memory (non-differential) - sendData(0x40); - sendData(0x00); + case FULL: + default: + sendCommand(0x21); // Bypass "old" image memory (non-differential) + sendData(0x40); + sendData(0x00); - sendCommand(0x22); // Set "update sequence": - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } + sendCommand(0x22); // Set "update sequence": + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void HINK_E042A87::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 1000); // At least 1 second, then check every 50ms - case FULL: - default: - return beginPolling(100, 3500); // At least 3.5 seconds, then check every 100ms - } +void HINK_E042A87::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 1000); // At least 1 second, then check every 50ms + case FULL: + default: + return beginPolling(100, 3500); // At least 3.5 seconds, then check every 100ms + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h index c32d159fb..612072b50 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h +++ b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h @@ -20,21 +20,23 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class HINK_E042A87 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 400; - static constexpr uint32_t height = 300; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class HINK_E042A87 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 400; + static constexpr uint32_t height = 300; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - HINK_E042A87() : SSD16XX(width, height, supported) {} + public: + HINK_E042A87() : SSD16XX(width, height, supported) {} -protected: - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp index 090945216..e9a663f80 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp @@ -5,60 +5,64 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void LCMEN2R13ECC1::configScanning() { - // "Driver output control" - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); - sendData(0x00); +void LCMEN2R13ECC1::configScanning() +{ + // "Driver output control" + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); - // To-do: delete this method? - // Values set here might be redundant: F9, 00, 00 seems to be default + // To-do: delete this method? + // Values set here might be redundant: F9, 00, 00 seems to be default } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void LCMEN2R13ECC1::configWaveform() { - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x85); - break; +void LCMEN2R13ECC1::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x85); + break; - case FULL: - default: - // From OTP memory - break; - } + case FULL: + default: + // From OTP memory + break; + } } -void LCMEN2R13ECC1::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void LCMEN2R13ECC1::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void LCMEN2R13ECC1::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 800); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 2500); // At least 2 seconds for full refresh - } +void LCMEN2R13ECC1::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 800); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2500); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h index 19a7d2ca2..9fa6eaac9 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h @@ -16,22 +16,24 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class LCMEN2R13ECC1 : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class LCMEN2R13ECC1 : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte + public: + LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte -protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp index 37c9b47d3..fb37544b2 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp @@ -68,223 +68,239 @@ static const uint8_t LUT_FAST_BB[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; -LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) { - // Pre-calculate size of the image buffer, for convenience +LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) +{ + // Pre-calculate size of the image buffer, for convenience - // Determine the X dimension of the image buffer, in bytes. - // Along rows, pixels are stored 8 per byte. - // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. - bufferRowSize = ((width - 1) / 8) + 1; + // Determine the X dimension of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + bufferRowSize = ((width - 1) / 8) + 1; - // Total size of image buffer, in bytes. - bufferSize = bufferRowSize * height; + // Total size of image buffer, in bytes. + bufferSize = bufferRowSize * height; } -void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { - this->spi = spi; - this->pin_dc = pin_dc; - this->pin_cs = pin_cs; - this->pin_busy = pin_busy; - this->pin_rst = pin_rst; +void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; - pinMode(pin_dc, OUTPUT); - pinMode(pin_cs, OUTPUT); - pinMode(pin_busy, INPUT); + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); - // Reset is active low, hold high - pinMode(pin_rst, INPUT_PULLUP); + // Reset is active low, hold high + pinMode(pin_rst, INPUT_PULLUP); - reset(); + reset(); } // Display an image on the display -void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) { - this->updateType = type; - this->buffer = imageData; +void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) +{ + this->updateType = type; + this->buffer = imageData; - reset(); + reset(); - // Config - if (updateType == FULL) - configFull(); - else - configFast(); + // Config + if (updateType == FULL) + configFull(); + else + configFast(); - // Transfer image data - if (updateType == FULL) { - writeNewImage(); - writeOldImage(); - } else { - writeNewImage(); - } + // Transfer image data + if (updateType == FULL) { + writeNewImage(); + writeOldImage(); + } else { + writeNewImage(); + } - sendCommand(0x04); // Power on the panel voltage - wait(); + sendCommand(0x04); // Power on the panel voltage + wait(); - sendCommand(0x12); // Begin executing the update + sendCommand(0x12); // Begin executing the update - // Let the update run async, on display hardware. Base class will poll completion, then finalize. - // For a blocking update, call await after update - detachFromUpdate(); + // Let the update run async, on display hardware. Base class will poll completion, then finalize. + // For a blocking update, call await after update + detachFromUpdate(); } -void LCMEN213EFC1::wait() { - // Busy when LOW - while (digitalRead(pin_busy) == LOW) - yield(); +void LCMEN213EFC1::wait() +{ + // Busy when LOW + while (digitalRead(pin_busy) == LOW) + yield(); } -void LCMEN213EFC1::reset() { - pinMode(pin_rst, OUTPUT); - digitalWrite(pin_rst, LOW); - delay(10); - pinMode(pin_rst, INPUT_PULLUP); - wait(); +void LCMEN213EFC1::reset() +{ + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, LOW); + delay(10); + pinMode(pin_rst, INPUT_PULLUP); + wait(); - sendCommand(0x12); - wait(); + sendCommand(0x12); + wait(); } -void LCMEN213EFC1::sendCommand(const uint8_t command) { - // Take firmware's SPI lock - spiLock->lock(); +void LCMEN213EFC1::sendCommand(const uint8_t command) +{ + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, LOW); // DC pin low indicates command - digitalWrite(pin_cs, LOW); - spi->transfer(command); - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); // DC pin low indicates command + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void LCMEN213EFC1::sendData(uint8_t data) { sendData(&data, 1); } +void LCMEN213EFC1::sendData(uint8_t data) +{ + sendData(&data, 1); +} -void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) { - // Take firmware's SPI lock - spiLock->lock(); +void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) +{ + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command - digitalWrite(pin_cs, LOW); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + digitalWrite(pin_cs, LOW); - // Platform-specific SPI command - // Mothballing. This display model is only used by Heltec Wireless Paper (ESP32) + // Platform-specific SPI command + // Mothballing. This display model is only used by Heltec Wireless Paper (ESP32) #if defined(ARCH_ESP32) - spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer + spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer #elif defined(ARCH_NRF52) - spi->transfer(data, NULL, size); // NULL for a "write only" transfer + spi->transfer(data, NULL, size); // NULL for a "write only" transfer #else #error Not implemented yet? Feel free to add other platforms here. #endif - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void LCMEN213EFC1::configFull() { - sendCommand(0x00); // Panel setting register - sendData(0b11 << 6 // Display resolution - | 1 << 4 // B&W only - | 1 << 3 // Vertical scan direction - | 1 << 2 // Horizontal scan direction - | 1 << 1 // Shutdown: no - | 1 << 0 // Reset: no - ); +void LCMEN213EFC1::configFull() +{ + sendCommand(0x00); // Panel setting register + sendData(0b11 << 6 // Display resolution + | 1 << 4 // B&W only + | 1 << 3 // Vertical scan direction + | 1 << 2 // Horizontal scan direction + | 1 << 1 // Shutdown: no + | 1 << 0 // Reset: no + ); - sendCommand(0x50); // VCOM and data interval setting register - sendData(0b10 << 6 // Border driven white - | 0b11 << 4 // Invert image colors: no - | 0b0111 << 0 // Interval between VCOM on and image data (default) - ); + sendCommand(0x50); // VCOM and data interval setting register + sendData(0b10 << 6 // Border driven white + | 0b11 << 4 // Invert image colors: no + | 0b0111 << 0 // Interval between VCOM on and image data (default) + ); } -void LCMEN213EFC1::configFast() { - sendCommand(0x00); // Panel setting register - sendData(0b11 << 6 // Display resolution - | 1 << 5 // LUT from registers (set below) - | 1 << 4 // B&W only - | 1 << 3 // Vertical scan direction - | 1 << 2 // Horizontal scan direction - | 1 << 1 // Shutdown: no - | 1 << 0 // Reset: no - ); +void LCMEN213EFC1::configFast() +{ + sendCommand(0x00); // Panel setting register + sendData(0b11 << 6 // Display resolution + | 1 << 5 // LUT from registers (set below) + | 1 << 4 // B&W only + | 1 << 3 // Vertical scan direction + | 1 << 2 // Horizontal scan direction + | 1 << 1 // Shutdown: no + | 1 << 0 // Reset: no + ); - sendCommand(0x50); // VCOM and data interval setting register - sendData(0b11 << 6 // Border floating - | 0b01 << 4 // Invert image colors: no - | 0b0111 << 0 // Interval between VCOM on and image data (default) - ); + sendCommand(0x50); // VCOM and data interval setting register + sendData(0b11 << 6 // Border floating + | 0b01 << 4 // Invert image colors: no + | 0b0111 << 0 // Interval between VCOM on and image data (default) + ); - // Load the various LUTs - sendCommand(0x20); // VCOM - sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC)); + // Load the various LUTs + sendCommand(0x20); // VCOM + sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC)); - sendCommand(0x21); // White -> White - sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW)); + sendCommand(0x21); // White -> White + sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW)); - sendCommand(0x22); // Black -> White - sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW)); + sendCommand(0x22); // Black -> White + sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW)); - sendCommand(0x23); // White -> Black - sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB)); + sendCommand(0x23); // White -> Black + sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB)); - sendCommand(0x24); // Black -> Black - sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB)); + sendCommand(0x24); // Black -> Black + sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB)); } -void LCMEN213EFC1::writeNewImage() { - sendCommand(0x13); - sendData(buffer, bufferSize); +void LCMEN213EFC1::writeNewImage() +{ + sendCommand(0x13); + sendData(buffer, bufferSize); } -void LCMEN213EFC1::writeOldImage() { - sendCommand(0x10); - sendData(buffer, bufferSize); +void LCMEN213EFC1::writeOldImage() +{ + sendCommand(0x10); + sendData(buffer, bufferSize); } -void LCMEN213EFC1::detachFromUpdate() { - // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types - // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed - // If not implemented, we'll just poll right from the get-go - switch (updateType) { - case FULL: - EInk::beginPolling(10, 3650); - break; - case FAST: - EInk::beginPolling(10, 720); - break; - default: - assert(false); - } +void LCMEN213EFC1::detachFromUpdate() +{ + // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types + // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed + // If not implemented, we'll just poll right from the get-go + switch (updateType) { + case FULL: + EInk::beginPolling(10, 3650); + break; + case FAST: + EInk::beginPolling(10, 720); + break; + default: + assert(false); + } } -bool LCMEN213EFC1::isUpdateDone() { - // Busy when LOW - if (digitalRead(pin_busy) == LOW) - return false; - else - return true; +bool LCMEN213EFC1::isUpdateDone() +{ + // Busy when LOW + if (digitalRead(pin_busy) == LOW) + return false; + else + return true; } -void LCMEN213EFC1::finalizeUpdate() { - // Power off the panel voltages - sendCommand(0x02); - wait(); - - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in - // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST - // etc. - if (updateType != FULL) { - writeOldImage(); +void LCMEN213EFC1::finalizeUpdate() +{ + // Power off the panel voltages + sendCommand(0x02); wait(); - } + + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place + // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. + if (updateType != FULL) { + writeOldImage(); + wait(); + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h index 103d15bcf..499daef05 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h @@ -20,48 +20,50 @@ It is implemented as a "one-off", directly inheriting the EInk base class, unlik #include "./EInk.h" -namespace NicheGraphics::Drivers { +namespace NicheGraphics::Drivers +{ -class LCMEN213EFC1 : public EInk { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +class LCMEN213EFC1 : public EInk +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - LCMEN213EFC1(); - void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst); - void update(uint8_t *imageData, UpdateTypes type) override; + public: + LCMEN213EFC1(); + void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst); + void update(uint8_t *imageData, UpdateTypes type) override; -protected: - void wait(); - void reset(); - void sendCommand(const uint8_t command); - void sendData(const uint8_t data); - void sendData(const uint8_t *data, uint32_t size); - void configFull(); // Configure display for FULL refresh - void configFast(); // Configure display for FAST refresh - void writeNewImage(); - void writeOldImage(); // Used for "differential update", aka FAST refresh + protected: + void wait(); + void reset(); + void sendCommand(const uint8_t command); + void sendData(const uint8_t data); + void sendData(const uint8_t *data, uint32_t size); + void configFull(); // Configure display for FULL refresh + void configFast(); // Configure display for FAST refresh + void writeNewImage(); + void writeOldImage(); // Used for "differential update", aka FAST refresh - void detachFromUpdate(); - bool isUpdateDone(); - void finalizeUpdate(); + void detachFromUpdate(); + bool isUpdateDone(); + void finalizeUpdate(); -protected: - uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? - uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) - uint32_t bufferSize = 0; // In bytes. Rows * Columns - uint8_t *buffer = nullptr; - UpdateTypes updateType = UpdateTypes::UNSPECIFIED; + protected: + uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? + uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) + uint32_t bufferSize = 0; // In bytes. Rows * Columns + uint8_t *buffer = nullptr; + UpdateTypes updateType = UpdateTypes::UNSPECIFIED; - uint8_t pin_dc = -1; - uint8_t pin_cs = -1; - uint8_t pin_busy = -1; - uint8_t pin_rst = -1; - SPIClass *spi = nullptr; - SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0); + uint8_t pin_dc = -1; + uint8_t pin_cs = -1; + uint8_t pin_busy = -1; + uint8_t pin_rst = -1; + SPIClass *spi = nullptr; + SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.cpp b/src/graphics/niche/Drivers/EInk/SSD1682.cpp index b797cdfa6..c3d7f7786 100644 --- a/src/graphics/niche/Drivers/EInk/SSD1682.cpp +++ b/src/graphics/niche/Drivers/EInk/SSD1682.cpp @@ -5,34 +5,37 @@ using namespace NicheGraphics::Drivers; SSD1682::SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX) - : SSD16XX(width, height, supported, bufferOffsetX) {} + : SSD16XX(width, height, supported, bufferOffsetX) +{ +} // SSD1682 only accepts single-byte x and y values // This causes an incompatibility with the default SSD16XX::configFullscreen -void SSD1682::configFullscreen() { - // Define the boundaries of the "fullscreen" region, for the controller IC - static const uint8_t sx = bufferOffsetX; // Notice the offset - static const uint8_t sy = 0; - static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this - static const uint8_t ey = height; +void SSD1682::configFullscreen() +{ + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint8_t sx = bufferOffsetX; // Notice the offset + static const uint8_t sy = 0; + static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint8_t ey = height; - // Data entry mode - Left to Right, Top to Bottom - sendCommand(0x11); - sendData(0x03); + // Data entry mode - Left to Right, Top to Bottom + sendCommand(0x11); + sendData(0x03); - // Select controller IC memory region to display a fullscreen image - sendCommand(0x44); // Memory X start - end - sendData(sx); - sendData(ex); - sendCommand(0x45); // Memory Y start - end - sendData(sy); - sendData(ey); + // Select controller IC memory region to display a fullscreen image + sendCommand(0x44); // Memory X start - end + sendData(sx); + sendData(ex); + sendCommand(0x45); // Memory Y start - end + sendData(sy); + sendData(ey); - // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 - sendCommand(0x4E); // Memory cursor X - sendData(sx); - sendCommand(0x4F); // Memory cursor y - sendData(sy); + // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 + sendCommand(0x4E); // Memory cursor X + sendData(sx); + sendCommand(0x4F); // Memory cursor y + sendData(sy); } #endif \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.h b/src/graphics/niche/Drivers/EInk/SSD1682.h index cbb8909c4..ba3008537 100644 --- a/src/graphics/niche/Drivers/EInk/SSD1682.h +++ b/src/graphics/niche/Drivers/EInk/SSD1682.h @@ -15,13 +15,15 @@ to avoid re-implementing them every time we need to add a new SSD1682-based disp #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { +namespace NicheGraphics::Drivers +{ -class SSD1682 : public SSD16XX { -public: - SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); - virtual void configFullscreen(); // Select memory region on controller IC - virtual void deepSleep() {} // Not usable (image memory not retained) +class SSD1682 : public SSD16XX +{ + public: + SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void deepSleep() {} // Not usable (image memory not retained) }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp index add2031be..d0d030be8 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.cpp +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.cpp @@ -7,249 +7,266 @@ using namespace NicheGraphics::Drivers; SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX) - : EInk(width, height, supported), bufferOffsetX(bufferOffsetX) { - // Pre-calculate size of the image buffer, for convenience + : EInk(width, height, supported), bufferOffsetX(bufferOffsetX) +{ + // Pre-calculate size of the image buffer, for convenience - // Determine the X dimension of the image buffer, in bytes. - // Along rows, pixels are stored 8 per byte. - // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. - bufferRowSize = ((width - 1) / 8) + 1; + // Determine the X dimension of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + bufferRowSize = ((width - 1) / 8) + 1; - // Total size of image buffer, in bytes. - bufferSize = bufferRowSize * height; + // Total size of image buffer, in bytes. + bufferSize = bufferRowSize * height; } -void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { - this->spi = spi; - this->pin_dc = pin_dc; - this->pin_cs = pin_cs; - this->pin_busy = pin_busy; - this->pin_rst = pin_rst; +void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; - pinMode(pin_dc, OUTPUT); - pinMode(pin_cs, OUTPUT); - pinMode(pin_busy, INPUT); + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); - // If using a reset pin, hold high - // Reset is active low for Solomon Systech ICs - if (pin_rst != 0xFF) - pinMode(pin_rst, INPUT_PULLUP); + // If using a reset pin, hold high + // Reset is active low for Solomon Systech ICs + if (pin_rst != 0xFF) + pinMode(pin_rst, INPUT_PULLUP); - reset(); + reset(); } // Poll the displays busy pin until an operation is complete // Timeout and set fail flag if something went wrong and the display got stuck -void SSD16XX::wait(uint32_t timeout) { - // Don't bother waiting if part of the update sequence failed - // In that situation, we're now just failing-through the process, until we can try again with next update. - if (failed) - return; +void SSD16XX::wait(uint32_t timeout) +{ + // Don't bother waiting if part of the update sequence failed + // In that situation, we're now just failing-through the process, until we can try again with next update. + if (failed) + return; - uint32_t startMs = millis(); + uint32_t startMs = millis(); - // Busy when HIGH - while (digitalRead(pin_busy) == HIGH) { - // Check for timeout - if (millis() - startMs > timeout) { - failed = true; - break; + // Busy when HIGH + while (digitalRead(pin_busy) == HIGH) { + // Check for timeout + if (millis() - startMs > timeout) { + failed = true; + break; + } + yield(); } - yield(); - } } -void SSD16XX::reset() { - // Check if reset pin is defined - if (pin_rst != 0xFF) { - pinMode(pin_rst, OUTPUT); - digitalWrite(pin_rst, LOW); - delay(10); - digitalWrite(pin_rst, HIGH); - delay(10); +void SSD16XX::reset() +{ + // Check if reset pin is defined + if (pin_rst != 0xFF) { + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, LOW); + delay(10); + digitalWrite(pin_rst, HIGH); + delay(10); + wait(); + } + + sendCommand(0x12); wait(); - } - - sendCommand(0x12); - wait(); } -void SSD16XX::sendCommand(const uint8_t command) { - // Abort if part of the update sequence failed - // This will unlock again once we have failed-through the entire process - if (failed) - return; +void SSD16XX::sendCommand(const uint8_t command) +{ + // Abort if part of the update sequence failed + // This will unlock again once we have failed-through the entire process + if (failed) + return; - // Take firmware's SPI lock - spiLock->lock(); + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, LOW); // DC pin low indicates command - digitalWrite(pin_cs, LOW); - spi->transfer(command); - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); // DC pin low indicates command + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void SSD16XX::sendData(uint8_t data) { sendData(&data, 1); } +void SSD16XX::sendData(uint8_t data) +{ + sendData(&data, 1); +} -void SSD16XX::sendData(const uint8_t *data, uint32_t size) { - // Abort if part of the update sequence failed - // This will unlock again once we have failed-through the entire process - if (failed) - return; +void SSD16XX::sendData(const uint8_t *data, uint32_t size) +{ + // Abort if part of the update sequence failed + // This will unlock again once we have failed-through the entire process + if (failed) + return; - // Take firmware's SPI lock - spiLock->lock(); + // Take firmware's SPI lock + spiLock->lock(); - spi->beginTransaction(spiSettings); - digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command - digitalWrite(pin_cs, LOW); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command + digitalWrite(pin_cs, LOW); - // Platform-specific SPI command + // Platform-specific SPI command #if defined(ARCH_ESP32) - spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer + spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer #elif defined(ARCH_NRF52) - spi->transfer(data, NULL, size); // NULL for a "write only" transfer + spi->transfer(data, NULL, size); // NULL for a "write only" transfer #else #error Not implemented yet? Feel free to add other platforms here. #endif - digitalWrite(pin_cs, HIGH); - digitalWrite(pin_dc, HIGH); - spi->endTransaction(); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); - spiLock->unlock(); + spiLock->unlock(); } -void SSD16XX::configFullscreen() { - // Placing this code in a separate method because it's probably pretty consistent between displays - // Should make it tidier to override SSD16XX::configure +void SSD16XX::configFullscreen() +{ + // Placing this code in a separate method because it's probably pretty consistent between displays + // Should make it tidier to override SSD16XX::configure - // Define the boundaries of the "fullscreen" region, for the controller IC - static const uint16_t sx = bufferOffsetX; // Notice the offset - static const uint16_t sy = 0; - static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this - static const uint16_t ey = height; + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint16_t sx = bufferOffsetX; // Notice the offset + static const uint16_t sy = 0; + static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint16_t ey = height; - // Split into bytes - static const uint8_t sy1 = sy & 0xFF; - static const uint8_t sy2 = (sy >> 8) & 0xFF; - static const uint8_t ey1 = ey & 0xFF; - static const uint8_t ey2 = (ey >> 8) & 0xFF; + // Split into bytes + static const uint8_t sy1 = sy & 0xFF; + static const uint8_t sy2 = (sy >> 8) & 0xFF; + static const uint8_t ey1 = ey & 0xFF; + static const uint8_t ey2 = (ey >> 8) & 0xFF; - // Data entry mode - Left to Right, Top to Bottom - sendCommand(0x11); - sendData(0x03); + // Data entry mode - Left to Right, Top to Bottom + sendCommand(0x11); + sendData(0x03); - // Select controller IC memory region to display a fullscreen image - sendCommand(0x44); // Memory X start - end - sendData(sx); - sendData(ex); - sendCommand(0x45); // Memory Y start - end - sendData(sy1); - sendData(sy2); - sendData(ey1); - sendData(ey2); + // Select controller IC memory region to display a fullscreen image + sendCommand(0x44); // Memory X start - end + sendData(sx); + sendData(ex); + sendCommand(0x45); // Memory Y start - end + sendData(sy1); + sendData(sy2); + sendData(ey1); + sendData(ey2); - // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 - sendCommand(0x4E); // Memory cursor X - sendData(sx); - sendCommand(0x4F); // Memory cursor y - sendData(sy1); - sendData(sy2); + // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 + sendCommand(0x4E); // Memory cursor X + sendData(sx); + sendCommand(0x4F); // Memory cursor y + sendData(sy1); + sendData(sy2); } -void SSD16XX::update(uint8_t *imageData, UpdateTypes type) { - this->updateType = type; - this->buffer = imageData; +void SSD16XX::update(uint8_t *imageData, UpdateTypes type) +{ + this->updateType = type; + this->buffer = imageData; - reset(); + reset(); - configFullscreen(); - configScanning(); // Virtual, unused by base class - configVoltages(); // Virtual, unused by base class - configWaveform(); // Virtual, unused by base class - wait(); + configFullscreen(); + configScanning(); // Virtual, unused by base class + configVoltages(); // Virtual, unused by base class + configWaveform(); // Virtual, unused by base class + wait(); - if (updateType == FULL) { - writeNewImage(); - writeOldImage(); - } else { - writeNewImage(); - } + if (updateType == FULL) { + writeNewImage(); + writeOldImage(); + } else { + writeNewImage(); + } - configUpdateSequence(); - sendCommand(0x20); // Begin executing the update + configUpdateSequence(); + sendCommand(0x20); // Begin executing the update - // Let the update run async, on display hardware. Base class will poll completion, then finalize. - // For a blocking update, call await after update - detachFromUpdate(); + // Let the update run async, on display hardware. Base class will poll completion, then finalize. + // For a blocking update, call await after update + detachFromUpdate(); } // Send SPI commands for controller IC to begin executing the refresh operation -void SSD16XX::configUpdateSequence() { - switch (updateType) { - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Non-differential, load waveform from OTP - break; - } +void SSD16XX::configUpdateSequence() +{ + switch (updateType) { + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Non-differential, load waveform from OTP + break; + } } -void SSD16XX::writeNewImage() { - sendCommand(0x24); - sendData(buffer, bufferSize); +void SSD16XX::writeNewImage() +{ + sendCommand(0x24); + sendData(buffer, bufferSize); } -void SSD16XX::writeOldImage() { - sendCommand(0x26); - sendData(buffer, bufferSize); +void SSD16XX::writeOldImage() +{ + sendCommand(0x26); + sendData(buffer, bufferSize); } -void SSD16XX::detachFromUpdate() { - // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types - // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed - // If not implemented, we'll just poll right from the get-go - switch (updateType) { - default: - EInk::beginPolling(100, 0); - } +void SSD16XX::detachFromUpdate() +{ + // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types + // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed + // If not implemented, we'll just poll right from the get-go + switch (updateType) { + default: + EInk::beginPolling(100, 0); + } } -bool SSD16XX::isUpdateDone() { - // Busy when HIGH - if (digitalRead(pin_busy) == HIGH) - return false; - else - return true; +bool SSD16XX::isUpdateDone() +{ + // Busy when HIGH + if (digitalRead(pin_busy) == HIGH) + return false; + else + return true; } -void SSD16XX::finalizeUpdate() { - // Put a copy of the image into the "old memory". - // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in - // place We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST - // etc. - if (updateType != FULL) { - writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678? - writeOldImage(); - sendCommand(0x7F); // Terminate image write without update - wait(); - } +void SSD16XX::finalizeUpdate() +{ + // Put a copy of the image into the "old memory". + // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place + // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. + if (updateType != FULL) { + writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678? + writeOldImage(); + sendCommand(0x7F); // Terminate image write without update + wait(); + } - // Enter deep-sleep to save a few µA - // Waking from this requires that display's reset pin is broken out - if (pin_rst != 0xFF) - deepSleep(); + // Enter deep-sleep to save a few µA + // Waking from this requires that display's reset pin is broken out + if (pin_rst != 0xFF) + deepSleep(); } // Enter a lower-power state // May only save a few µA.. -void SSD16XX::deepSleep() { - sendCommand(0x10); // Enter deep sleep - sendData(0x01); // Mode 1: preserve image RAM +void SSD16XX::deepSleep() +{ + sendCommand(0x10); // Enter deep sleep + sendData(0x01); // Mode 1: preserve image RAM } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD16XX.h b/src/graphics/niche/Drivers/EInk/SSD16XX.h index 232939bbd..3f92818ce 100644 --- a/src/graphics/niche/Drivers/EInk/SSD16XX.h +++ b/src/graphics/niche/Drivers/EInk/SSD16XX.h @@ -16,47 +16,49 @@ See DEPG0154BNS800 and DEPG0290BNS800 for examples. #include "./EInk.h" -namespace NicheGraphics::Drivers { +namespace NicheGraphics::Drivers +{ -class SSD16XX : public EInk { -public: - SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0); - virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1); - virtual void update(uint8_t *imageData, UpdateTypes type) override; +class SSD16XX : public EInk +{ + public: + SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1); + virtual void update(uint8_t *imageData, UpdateTypes type) override; -protected: - virtual void wait(uint32_t timeout = 1000); - virtual void reset(); - virtual void sendCommand(const uint8_t command); - virtual void sendData(const uint8_t data); - virtual void sendData(const uint8_t *data, uint32_t size); - virtual void configFullscreen(); // Select memory region on controller IC - virtual void configScanning() {} // Optional. First & last gates, scan direction, etc - virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc - virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc - virtual void configUpdateSequence(); // Tell controller IC which operations to run + protected: + virtual void wait(uint32_t timeout = 1000); + virtual void reset(); + virtual void sendCommand(const uint8_t command); + virtual void sendData(const uint8_t data); + virtual void sendData(const uint8_t *data, uint32_t size); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void configScanning() {} // Optional. First & last gates, scan direction, etc + virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc + virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc + virtual void configUpdateSequence(); // Tell controller IC which operations to run - virtual void writeNewImage(); - virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh" + virtual void writeNewImage(); + virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh" - virtual void detachFromUpdate(); - virtual bool isUpdateDone() override; - virtual void finalizeUpdate() override; - virtual void deepSleep(); + virtual void detachFromUpdate(); + virtual bool isUpdateDone() override; + virtual void finalizeUpdate() override; + virtual void deepSleep(); -protected: - uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? - uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) - uint32_t bufferSize = 0; // In bytes. Rows * Columns - uint8_t *buffer = nullptr; - UpdateTypes updateType = UpdateTypes::UNSPECIFIED; + protected: + uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? + uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) + uint32_t bufferSize = 0; // In bytes. Rows * Columns + uint8_t *buffer = nullptr; + UpdateTypes updateType = UpdateTypes::UNSPECIFIED; - uint8_t pin_dc = -1; - uint8_t pin_cs = -1; - uint8_t pin_busy = -1; - uint8_t pin_rst = -1; - SPIClass *spi = nullptr; - SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); + uint8_t pin_dc = -1; + uint8_t pin_cs = -1; + uint8_t pin_busy = -1; + uint8_t pin_rst = -1; + SPIClass *spi = nullptr; + SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp index 56b1bbbb9..e83588905 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp @@ -5,60 +5,64 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void ZJY122250_0213BAAMFGN::configScanning() { - // "Driver output control" - // Scan gates from 0 to 249 (vertical resolution 250px) - sendCommand(0x01); - sendData(0xF9); - sendData(0x00); - sendData(0x00); +void ZJY122250_0213BAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void ZJY122250_0213BAAMFGN::configWaveform() { - switch (updateType) { - case FAST: - sendCommand(0x3C); // Border waveform: - sendData(0x80); // VCOM - break; - case FULL: - default: - sendCommand(0x3C); // Border waveform: - sendData(0x01); // Follow LUT 1 (blink same as white pixels) - break; - } +void ZJY122250_0213BAAMFGN::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VCOM + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void ZJY122250_0213BAAMFGN::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void ZJY122250_0213BAAMFGN::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void ZJY122250_0213BAAMFGN::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh - case FULL: - default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh - } +void ZJY122250_0213BAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h index e1faa30c0..82c4ec107 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h @@ -17,22 +17,24 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class ZJY122250_0213BAAMFGN : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 122; - static constexpr uint32_t height = 250; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class ZJY122250_0213BAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} + public: + ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} -protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp index 62f439873..a8f43420f 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp @@ -5,51 +5,55 @@ using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel -void ZJY128296_029EAAMFGN::configScanning() { - // "Driver output control" - // Scan gates from 0 to 295 (vertical resolution 296px) - sendCommand(0x01); - sendData(0x27); // Number of gates (295, bits 0-7) - sendData(0x01); // Number of gates (295, bit 8) - sendData(0x00); // (Do not invert scanning order) +void ZJY128296_029EAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 295 (vertical resolution 296px) + sendCommand(0x01); + sendData(0x27); // Number of gates (295, bits 0-7) + sendData(0x01); // Number of gates (295, bit 8) + sendData(0x00); // (Do not invert scanning order) } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. -void ZJY128296_029EAAMFGN::configWaveform() { - sendCommand(0x3C); // Border waveform: - sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) +void ZJY128296_029EAAMFGN::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) - sendCommand(0x18); // Temperature sensor: - sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } -void ZJY128296_029EAAMFGN::configUpdateSequence() { - switch (updateType) { - case FAST: - sendCommand(0x22); // Set "update sequence" - sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" - break; +void ZJY128296_029EAAMFGN::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; - case FULL: - default: - sendCommand(0x22); // Set "update sequence" - sendData(0xF7); // Will load LUT from OTP memory - break; - } + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" -void ZJY128296_029EAAMFGN::detachFromUpdate() { - switch (updateType) { - case FAST: - return beginPolling(50, 300); // At least 300ms for fast refresh - case FULL: - default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh - } +void ZJY128296_029EAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 300); // At least 300ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h index d718544ae..27644e709 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h @@ -19,22 +19,24 @@ E-Ink display driver #include "./SSD16XX.h" -namespace NicheGraphics::Drivers { -class ZJY128296_029EAAMFGN : public SSD16XX { - // Display properties -private: - static constexpr uint32_t width = 128; - static constexpr uint32_t height = 296; - static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); +namespace NicheGraphics::Drivers +{ +class ZJY128296_029EAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 128; + static constexpr uint32_t height = 296; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); -public: - ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} + public: + ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} -protected: - void configScanning() override; - void configWaveform() override; - void configUpdateSequence() override; - void detachFromUpdate() override; + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers diff --git a/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h index af89f3f8e..fb16bcf2f 100644 --- a/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h +++ b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h @@ -22,7 +22,8 @@ E-Ink display driver #include "./GDEY0154D67.h" -namespace NicheGraphics::Drivers { +namespace NicheGraphics::Drivers +{ typedef GDEY0154D67 ZJY200200_0154DAAMFGN; diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index b496e2270..1e89ebe1b 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -13,109 +13,127 @@ InkHUD::AppletFont InkHUD::Applet::fontMedium; InkHUD::AppletFont InkHUD::Applet::fontSmall; constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo -InkHUD::Applet::Applet() : GFX(0, 0) { - // GFX is given initial dimensions of 0 - // The width and height will change dynamically, depending on Applet tiling - // If you're getting a "divide by zero error", consider it an assert: - // WindowManager should be the only one controlling the rendering +InkHUD::Applet::Applet() : GFX(0, 0) +{ + // GFX is given initial dimensions of 0 + // The width and height will change dynamically, depending on Applet tiling + // If you're getting a "divide by zero error", consider it an assert: + // WindowManager should be the only one controlling the rendering - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; - latestMessage = &inkhud->persistence->latestMessage; + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; + latestMessage = &inkhud->persistence->latestMessage; } // Draw a single pixel // The raw pixel output generated by AdafruitGFX drawing all passes through here // Hand off to the applet's tile, which will in-turn pass to the renderer -void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) { - // Only render pixels if they fall within user's cropped region - if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) - assignedTile->handleAppletPixel(x, y, (Color)color); +void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) +{ + // Only render pixels if they fall within user's cropped region + if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) + assignedTile->handleAppletPixel(x, y, (Color)color); } // Link our applet to a tile // This can only be called by Tile::assignApplet // The tile determines the applets dimensions // Pixel output is passed to tile during render() -void InkHUD::Applet::setTile(Tile *t) { - // If we're setting (not clearing), make sure the link is "reciprocal" - if (t) - assert(t->getAssignedApplet() == this); +void InkHUD::Applet::setTile(Tile *t) +{ + // If we're setting (not clearing), make sure the link is "reciprocal" + if (t) + assert(t->getAssignedApplet() == this); - assignedTile = t; + assignedTile = t; } // The tile to which our applet is assigned -InkHUD::Tile *InkHUD::Applet::getTile() { return assignedTile; } +InkHUD::Tile *InkHUD::Applet::getTile() +{ + return assignedTile; +} // Draw the applet -void InkHUD::Applet::render() { - assert(assignedTile); // Ensure that we have a tile - assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile +void InkHUD::Applet::render() +{ + assert(assignedTile); // Ensure that we have a tile + assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile - // WindowManager::update has now consumed the info about our update request - // Clear everything for future requests - wantRender = false; // Flag set by requestUpdate - wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. - wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. + // WindowManager::update has now consumed the info about our update request + // Clear everything for future requests + wantRender = false; // Flag set by requestUpdate + wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. + wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. - updateDimensions(); - resetDrawingSpace(); - onRender(); // Derived applet's drawing takes place here + updateDimensions(); + resetDrawingSpace(); + onRender(); // Derived applet's drawing takes place here - // Handle "Tile Highlighting" - // Some devices may use an auxiliary button to switch between tiles - // When this happens, we temporarily highlight the newly focused tile with a border + // Handle "Tile Highlighting" + // Some devices may use an auxiliary button to switch between tiles + // When this happens, we temporarily highlight the newly focused tile with a border - // If our tile is (or was) highlighted, to indicate a change in focus - if (Tile::highlightTarget == assignedTile) { - // Draw the highlight - if (!Tile::highlightShown) { - drawRect(0, 0, width(), height(), BLACK); - Tile::startHighlightTimeout(); - Tile::highlightShown = true; + // If our tile is (or was) highlighted, to indicate a change in focus + if (Tile::highlightTarget == assignedTile) { + // Draw the highlight + if (!Tile::highlightShown) { + drawRect(0, 0, width(), height(), BLACK); + Tile::startHighlightTimeout(); + Tile::highlightShown = true; + } + + // Clear the highlight + else { + Tile::cancelHighlightTimeout(); + Tile::highlightShown = false; + Tile::highlightTarget = nullptr; + } } - - // Clear the highlight - else { - Tile::cancelHighlightTimeout(); - Tile::highlightShown = false; - Tile::highlightTarget = nullptr; - } - } } // Does the applet want to render now? // Checks whether the applet called requestUpdate recently, in response to an event // Used by WindowManager::update -bool InkHUD::Applet::wantsToRender() { return wantRender; } +bool InkHUD::Applet::wantsToRender() +{ + return wantRender; +} // Does the applet want to be moved to foreground before next render, to show new data? // User specifies whether an applet has permission for this, using the on-screen menu // Used by WindowManager::update -bool InkHUD::Applet::wantsToAutoshow() { return wantAutoshow; } +bool InkHUD::Applet::wantsToAutoshow() +{ + return wantAutoshow; +} // Which technique would this applet prefer that the display use to change the image? // Used by WindowManager::update -Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() { return wantUpdateType; } +Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() +{ + return wantUpdateType; +} // Get size of the applet's drawing space from its tile // Performed immediately before derived applet's drawing code runs -void InkHUD::Applet::updateDimensions() { - assert(assignedTile); - WIDTH = assignedTile->getWidth(); - HEIGHT = assignedTile->getHeight(); - _width = WIDTH; - _height = HEIGHT; +void InkHUD::Applet::updateDimensions() +{ + assert(assignedTile); + WIDTH = assignedTile->getWidth(); + HEIGHT = assignedTile->getHeight(); + _width = WIDTH; + _height = HEIGHT; } // Ensure that render() always starts with the same initial drawing config -void InkHUD::Applet::resetDrawingSpace() { - resetCrop(); // Allow pixel from any region of the applet to draw - setTextColor(BLACK); // Reset text params - setCursor(0, 0); - setTextWrap(false); - setFont(fontSmall); +void InkHUD::Applet::resetDrawingSpace() +{ + resetCrop(); // Allow pixel from any region of the applet to draw + setTextColor(BLACK); // Reset text params + setCursor(0, 0); + setTextWrap(false); + setFont(fontSmall); } // Tell InkHUD::Renderer that we want to render now @@ -124,24 +142,29 @@ void InkHUD::Applet::resetDrawingSpace() { // Once the renderer has given other applets a chance to process whatever event we just detected, // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow -void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) { - wantRender = true; - wantUpdateType = type; - inkhud->requestUpdate(); +void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) +{ + wantRender = true; + wantUpdateType = type; + inkhud->requestUpdate(); } // Ask window manager to move this applet to foreground at start of next render // Users select which applets have permission for this using the on-screen menu -void InkHUD::Applet::requestAutoshow() { wantAutoshow = true; } +void InkHUD::Applet::requestAutoshow() +{ + wantAutoshow = true; +} // Called when an Applet begins running // Active applets are considered "enabled" // They should now listen for events, and request their own updates // They may also be unexpectedly renderer at any time by other InkHUD components // Applets can be activated at run-time through the on-screen menu -void InkHUD::Applet::activate() { - onActivate(); // Call derived class' handler - active = true; +void InkHUD::Applet::activate() +{ + onActivate(); // Call derived class' handler + active = true; } // Called when an Applet stops running @@ -149,42 +172,48 @@ void InkHUD::Applet::activate() { // They should not listen for events, process data // They will not be rendered // Applets can be deactivated at run-time through the on-screen menu -void InkHUD::Applet::deactivate() { - // If applet is still in foreground, run its onBackground code first - if (isForeground()) - sendToBackground(); +void InkHUD::Applet::deactivate() +{ + // If applet is still in foreground, run its onBackground code first + if (isForeground()) + sendToBackground(); - // If applet is active, run its onDeactivate code first - if (isActive()) - onDeactivate(); // Derived class' handler - active = false; + // If applet is active, run its onDeactivate code first + if (isActive()) + onDeactivate(); // Derived class' handler + active = false; } // Is the Applet running? // Note: active / inactive is not related to background / foreground // An inactive applet is *fully* disabled -bool InkHUD::Applet::isActive() { return active; } +bool InkHUD::Applet::isActive() +{ + return active; +} // Begin showing the Applet // It will be rendered immediately to whichever tile it is assigned // The Renderer will also now honor requestUpdate() calls from this applet -void InkHUD::Applet::bringToForeground() { - if (!foreground) { - foreground = true; - onForeground(); // Run derived applet class' handler - } +void InkHUD::Applet::bringToForeground() +{ + if (!foreground) { + foreground = true; + onForeground(); // Run derived applet class' handler + } - requestUpdate(); + requestUpdate(); } // Stop showing the Applet // Calls to requestUpdate() will no longer be honored // When one applet moves to background, another should move to foreground (exception: some system applets) -void InkHUD::Applet::sendToBackground() { - if (foreground) { - foreground = false; - onBackground(); // Run derived applet class' handler - } +void InkHUD::Applet::sendToBackground() +{ + if (foreground) { + foreground = false; + onBackground(); // Run derived applet class' handler + } } // Is the applet currently displayed on a tile @@ -192,493 +221,534 @@ void InkHUD::Applet::sendToBackground() { // This can occur when a system applet is covering the screen (e.g. during BLE pairing) // This is not our applets responsibility to handle, // as in those situations, the system applet will have "locked" rendering -bool InkHUD::Applet::isForeground() { return foreground; } +bool InkHUD::Applet::isForeground() +{ + return foreground; +} // Limit drawing to a certain region of the applet // Pixels outside this region will be discarded -void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) { - cropLeft = left; - cropTop = top; - cropWidth = width; - cropHeight = height; +void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) +{ + cropLeft = left; + cropTop = top; + cropWidth = width; + cropHeight = height; } // Allow drawing to any region of the Applet // Reverses Applet::setCrop -void InkHUD::Applet::resetCrop() { setCrop(0, 0, width(), height()); } +void InkHUD::Applet::resetCrop() +{ + setCrop(0, 0, width(), height()); +} // Convert relative width to absolute width, in px // X(0) is 0 // X(0.5) is width() / 2 // X(1) is width() -uint16_t InkHUD::Applet::X(float f) { return width() * f; } +uint16_t InkHUD::Applet::X(float f) +{ + return width() * f; +} // Convert relative hight to absolute height, in px // Y(0) is 0 // Y(0.5) is height() / 2 // Y(1) is height() -uint16_t InkHUD::Applet::Y(float f) { return height() * f; } - -// Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) { - // We do still have to run getTextBounds to find the width - int16_t textOffsetX, textOffsetY; - uint16_t textWidth, textHeight; - getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); - - int16_t cursorX = 0; - int16_t cursorY = 0; - - switch (ha) { - case LEFT: - cursorX = x - textOffsetX; - break; - case CENTER: - cursorX = (x - textOffsetX) - (textWidth / 2); - break; - case RIGHT: - cursorX = (x - textOffsetX) - textWidth; - break; - } - - // We're using a fixed line height, rather than sizing to text (getTextBounds) - - switch (va) { - case TOP: - cursorY = y + currentFont.heightAboveCursor(); - break; - case MIDDLE: - cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); - break; - case BOTTOM: - cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); - break; - } - - setCursor(cursorX, cursorY); - print(text); +uint16_t InkHUD::Applet::Y(float f) +{ + return height() * f; } // Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) { - printAt(x, y, text.c_str(), ha, va); +void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) +{ + // We do still have to run getTextBounds to find the width + int16_t textOffsetX, textOffsetY; + uint16_t textWidth, textHeight; + getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); + + int16_t cursorX = 0; + int16_t cursorY = 0; + + switch (ha) { + case LEFT: + cursorX = x - textOffsetX; + break; + case CENTER: + cursorX = (x - textOffsetX) - (textWidth / 2); + break; + case RIGHT: + cursorX = (x - textOffsetX) - textWidth; + break; + } + + // We're using a fixed line height, rather than sizing to text (getTextBounds) + + switch (va) { + case TOP: + cursorY = y + currentFont.heightAboveCursor(); + break; + case MIDDLE: + cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); + break; + case BOTTOM: + cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); + break; + } + + setCursor(cursorX, cursorY); + print(text); +} + +// Print text, specifying the position of any edge / corner of the textbox +void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) +{ + printAt(x, y, text.c_str(), ha, va); } // Set which font should be used for subsequent drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data -void InkHUD::Applet::setFont(AppletFont f) { - GFX::setFont(f.gfxFont); - currentFont = f; +void InkHUD::Applet::setFont(AppletFont f) +{ + GFX::setFont(f.gfxFont); + currentFont = f; } // Get which font is currently being used for drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data -InkHUD::AppletFont InkHUD::Applet::getFont() { return currentFont; } +InkHUD::AppletFont InkHUD::Applet::getFont() +{ + return currentFont; +} // Parse any text which might have "special characters" // Re-encodes UTF-8 characters to match our 8-bit encoded fonts -std::string InkHUD::Applet::parse(std::string text) { return getFont().decodeUTF8(text); } +std::string InkHUD::Applet::parse(std::string text) +{ + return getFont().decodeUTF8(text); +} // Get the best version of a node's short name available to us // Parses any non-ascii chars // Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji) -std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) { - assert(node); +std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) +{ + assert(node); - // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) - if (node->has_user) { - std::string parsed = parse(node->user.short_name); - if (isPrintable(parsed)) - return parsed; - } + // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) + if (node->has_user) { + std::string parsed = parse(node->user.short_name); + if (isPrintable(parsed)) + return parsed; + } - // Otherwise, use the "last 4" of node id - // - if short name unknown, or - // - if short name is emoji (we can't render this) - std::string nodeID = hexifyNodeNum(node->num); - return nodeID.substr(nodeID.length() - 4); + // Otherwise, use the "last 4" of node id + // - if short name unknown, or + // - if short name is emoji (we can't render this) + std::string nodeID = hexifyNodeNum(node->num); + return nodeID.substr(nodeID.length() - 4); } // Determine if all characters of a string are printable using the current font -bool InkHUD::Applet::isPrintable(std::string text) { - // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled - for (char &c : text) { - if (c == '\x1A') - return false; - } +bool InkHUD::Applet::isPrintable(std::string text) +{ + // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled + for (char &c : text) { + if (c == '\x1A') + return false; + } - // No unprintable characters found - return true; + // No unprintable characters found + return true; } // Gets rendered width of a string // Wrapper for getTextBounds -uint16_t InkHUD::Applet::getTextWidth(const char *text) { - // We do still have to run getTextBounds to find the width - int16_t textOffsetX, textOffsetY; - uint16_t textWidth, textHeight; - getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); +uint16_t InkHUD::Applet::getTextWidth(const char *text) +{ + // We do still have to run getTextBounds to find the width + int16_t textOffsetX, textOffsetY; + uint16_t textWidth, textHeight; + getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); - return textWidth; + return textWidth; } // Gets rendered width of a string // Wrapper for getTextBounds -uint16_t InkHUD::Applet::getTextWidth(std::string text) { return getTextWidth(text.c_str()); } +uint16_t InkHUD::Applet::getTextWidth(std::string text) +{ + return getTextWidth(text.c_str()); +} // Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels // Roughly comparable to values used by the iOS app; // I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator -InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) { - uint8_t score = 0; +InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) +{ + uint8_t score = 0; - // Give a score for the SNR - if (snr > -17.5) - score += 2; - else if (snr > -26.0) - score += 1; + // Give a score for the SNR + if (snr > -17.5) + score += 2; + else if (snr > -26.0) + score += 1; - // Give a score for the RSSI - if (rssi > -115.0) - score += 3; - else if (rssi > -120.0) - score += 2; - else if (rssi > -126.0) - score += 1; + // Give a score for the RSSI + if (rssi > -115.0) + score += 3; + else if (rssi > -120.0) + score += 2; + else if (rssi > -126.0) + score += 1; - // Combine scores, then give a result - if (score >= 5) - return SIGNAL_GOOD; - else if (score >= 4) - return SIGNAL_FAIR; - else if (score > 0) - return SIGNAL_BAD; - else - return SIGNAL_NONE; + // Combine scores, then give a result + if (score >= 5) + return SIGNAL_GOOD; + else if (score >= 4) + return SIGNAL_FAIR; + else if (score > 0) + return SIGNAL_BAD; + else + return SIGNAL_NONE; } // Apply the standard "node id" formatting to a nodenum int: !0123abdc -std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) { - // Not found in nodeDB, show a hex nodeid instead - char nodeIdHex[10]; - sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format - return std::string(nodeIdHex); +std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) +{ + // Not found in nodeDB, show a hex nodeid instead + char nodeIdHex[10]; + sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format + return std::string(nodeIdHex); } // Print text, with word wrapping // Avoids splitting words in half, instead moving the entire word to a new line wherever possible -void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) { - // Place the AdafruitGFX cursor to suit our "top" coord - setCursor(left, top + getFont().heightAboveCursor()); +void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) +{ + // Place the AdafruitGFX cursor to suit our "top" coord + setCursor(left, top + getFont().heightAboveCursor()); - // How wide a space character is - // Used when simulating print, for dimensioning - // Works around issues where getTextDimensions() doesn't account for whitespace - const uint8_t wSp = getFont().widthBetweenWords(); + // How wide a space character is + // Used when simulating print, for dimensioning + // Works around issues where getTextDimensions() doesn't account for whitespace + const uint8_t wSp = getFont().widthBetweenWords(); - // Move through our text, character by character - uint16_t wordStart = 0; - for (uint16_t i = 0; i < text.length(); i++) { + // Move through our text, character by character + uint16_t wordStart = 0; + for (uint16_t i = 0; i < text.length(); i++) { - // Found: end of word (split by spaces or newline) - // Also handles end of string - if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { - // Isolate this word - uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 - std::string word = text.substr(wordStart, wordLength); - wordStart = i + 1; // Next word starts *after* the space + // Found: end of word (split by spaces or newline) + // Also handles end of string + if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { + // Isolate this word + uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 + std::string word = text.substr(wordStart, wordLength); + wordStart = i + 1; // Next word starts *after* the space - // If word is terminated by a newline char, don't actually print it. - // We'll manually add a new line later - if (word.back() == '\n') - word.pop_back(); + // If word is terminated by a newline char, don't actually print it. + // We'll manually add a new line later + if (word.back() == '\n') + word.pop_back(); - // Measure the word, in px - int16_t l, t; - uint16_t w, h; - getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); + // Measure the word, in px + int16_t l, t; + uint16_t w, h; + getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); - // Word is short - if (w < width) { - // Word fits on current line - if ((l + w + wSp) < left + width) - print(word.c_str()); + // Word is short + if (w < width) { + // Word fits on current line + if ((l + w + wSp) < left + width) + print(word.c_str()); - // Word doesn't fit on current line - else { - setCursor(left, getCursorY() + getFont().lineHeight()); // Newline - print(word.c_str()); + // Word doesn't fit on current line + else { + setCursor(left, getCursorY() + getFont().lineHeight()); // Newline + print(word.c_str()); + } + } + + // Word is really long + // (wider than applet) + else { + // Horribly inefficient: + // Rather than working directly with the glyph sizes, + // we're going to run everything through getTextBounds as a c-string of length 1 + // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, + // which would be a pain to add manually here. + // These super-long strings probably don't come up often so we can maybe tolerate this. + + // Todo: rewrite making use of AdafruitGFX native text wrapping + char cstr[] = {0, 0}; + int16_t l, t; + uint16_t w, h; + for (uint16_t c = 0; c < word.length(); c++) { + // Shove next char into a c string + cstr[0] = word[c]; + getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); + + // Manual newline, if next character will spill beyond screen edge + if ((l + w) > left + width) + setCursor(left, getCursorY() + getFont().lineHeight()); + + // Print next character + print(word[c]); + } + } } - } - // Word is really long - // (wider than applet) - else { - // Horribly inefficient: - // Rather than working directly with the glyph sizes, - // we're going to run everything through getTextBounds as a c-string of length 1 - // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, - // which would be a pain to add manually here. - // These super-long strings probably don't come up often so we can maybe tolerate this. - - // Todo: rewrite making use of AdafruitGFX native text wrapping - char cstr[] = {0, 0}; - int16_t l, t; - uint16_t w, h; - for (uint16_t c = 0; c < word.length(); c++) { - // Shove next char into a c string - cstr[0] = word[c]; - getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); - - // Manual newline, if next character will spill beyond screen edge - if ((l + w) > left + width) - setCursor(left, getCursorY() + getFont().lineHeight()); - - // Print next character - print(word[c]); + // If word was terminated by a newline char, manually add the new line now + if (text[i] == '\n') { + setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline + wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line } - } } - - // If word was terminated by a newline char, manually add the new line now - if (text[i] == '\n') { - setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline - wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line - } - } } // Simulate running printWrapped, to determine how tall the block of text will be. // This is a wasteful way of handling things. Maybe some way to optimize in future? -uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) { - // Cache the current crop region - int16_t cL = cropLeft; - int16_t cT = cropTop; - uint16_t cW = cropWidth; - uint16_t cH = cropHeight; +uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) +{ + // Cache the current crop region + int16_t cL = cropLeft; + int16_t cT = cropTop; + uint16_t cW = cropWidth; + uint16_t cH = cropHeight; - setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels - printWrapped(left, 0, width, text); // Simulate only - no pixels drawn + setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels + printWrapped(left, 0, width, text); // Simulate only - no pixels drawn - // Restore previous crop region - cropLeft = cL; - cropTop = cT; - cropWidth = cW; - cropHeight = cH; + // Restore previous crop region + cropLeft = cL; + cropTop = cT; + cropWidth = cW; + cropHeight = cH; - // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, - // so we need to account for that when determining the height - return (getCursorY() + getFont().heightBelowCursor()); + // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, + // so we need to account for that when determining the height + return (getCursorY() + getFont().heightBelowCursor()); } // Fill a region with sparse diagonal lines, to create a pseudo-translucent fill -void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) { - // Cache the currently cropped region - int16_t oldCropL = cropLeft; - int16_t oldCropT = cropTop; - uint16_t oldCropW = cropWidth; - uint16_t oldCropH = cropHeight; +void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) +{ + // Cache the currently cropped region + int16_t oldCropL = cropLeft; + int16_t oldCropT = cropTop; + uint16_t oldCropW = cropWidth; + uint16_t oldCropH = cropHeight; - setCrop(x, y, w, h); + setCrop(x, y, w, h); - // Draw lines starting along the top edge, every few px - for (int16_t ix = x; ix < x + w; ix += spacing) { - for (int16_t i = 0; i < w || i < h; i++) { - drawPixel(ix + i, y + i, color); + // Draw lines starting along the top edge, every few px + for (int16_t ix = x; ix < x + w; ix += spacing) { + for (int16_t i = 0; i < w || i < h; i++) { + drawPixel(ix + i, y + i, color); + } } - } - // Draw lines starting along the left edge, every few px - for (int16_t iy = y; iy < y + h; iy += spacing) { - for (int16_t i = 0; i < w || i < h; i++) { - drawPixel(x + i, iy + i, color); + // Draw lines starting along the left edge, every few px + for (int16_t iy = y; iy < y + h; iy += spacing) { + for (int16_t i = 0; i < w || i < h; i++) { + drawPixel(x + i, iy + i, color); + } } - } - // Restore any previous crop - // If none was set, this will clear - cropLeft = oldCropL; - cropTop = oldCropT; - cropWidth = oldCropW; - cropHeight = oldCropH; + // Restore any previous crop + // If none was set, this will clear + cropLeft = oldCropL; + cropTop = oldCropT; + cropWidth = oldCropW; + cropHeight = oldCropH; } // Get a human readable time representation of an epoch time (seconds since 1970) // If time is invalid, this will be an empty string -std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) { +std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) +{ #ifdef BUILD_EPOCH - constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build + constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build #else - constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT + constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT #endif - uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); + uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); - int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; - int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; + int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; + int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; - // Times are invalid: rtc is much older than when code was built - // Don't give any human readable string - if (epochNow <= validAfterEpoch) - return ""; + // Times are invalid: rtc is much older than when code was built + // Don't give any human readable string + if (epochNow <= validAfterEpoch) + return ""; - // Times are invalid: argument time is significantly ahead of RTC - // Don't give any human readable string - if (daysAgo < -2) - return ""; + // Times are invalid: argument time is significantly ahead of RTC + // Don't give any human readable string + if (daysAgo < -2) + return ""; - // Times are probably invalid: more than 6 months ago - if (daysAgo > 6 * 30) - return ""; + // Times are probably invalid: more than 6 months ago + if (daysAgo > 6 * 30) + return ""; - if (daysAgo > 1) - return to_string(daysAgo) + " days ago"; + if (daysAgo > 1) + return to_string(daysAgo) + " days ago"; - else if (hoursAgo > 18) - return "Yesterday"; + else if (hoursAgo > 18) + return "Yesterday"; - else { + else { - uint32_t hms = epochSeconds % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + uint32_t hms = epochSeconds % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - // Tear apart hms into h:m - uint32_t hour = hms / SEC_PER_HOUR; - uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + // Tear apart hms into h:m + uint32_t hour = hms / SEC_PER_HOUR; + uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - // Format the clock string, either 12 hour or 24 hour - char clockStr[11]; - if (config.display.use_12h_clock) - sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); - else - sprintf(clockStr, "%02u:%02u", hour, min); + // Format the clock string, either 12 hour or 24 hour + char clockStr[11]; + if (config.display.use_12h_clock) + sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); + else + sprintf(clockStr, "%02u:%02u", hour, min); - return clockStr; - } + return clockStr; + } } // If no argument specified, get time string for the current RTC time -std::string InkHUD::Applet::getTimeString() { return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); } +std::string InkHUD::Applet::getTimeString() +{ + return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); +} // Calculate how many nodes have been seen within our preferred window of activity // This period is set by user, via the menu // Todo: optimize to calculate once only per WindowManager::render -uint16_t InkHUD::Applet::getActiveNodeCount() { - // Don't even try to count nodes if RTC isn't set - // The last heard values in nodedb will be incomprehensible - if (getRTCQuality() == RTCQualityNone) - return 0; +uint16_t InkHUD::Applet::getActiveNodeCount() +{ + // Don't even try to count nodes if RTC isn't set + // The last heard values in nodedb will be incomprehensible + if (getRTCQuality() == RTCQualityNone) + return 0; - uint16_t count = 0; + uint16_t count = 0; - // For each node in db - for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // For each node in db + for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Check if heard recently, and not our own node - if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) - count++; - } + // Check if heard recently, and not our own node + if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) + count++; + } - return count; + return count; } // Get an abbreviated, human readable, distance string // Honors config.display.units, to offer both metric and imperial -std::string InkHUD::Applet::localizeDistance(uint32_t meters) { - constexpr float FEET_PER_METER = 3.28084; - constexpr uint16_t FEET_PER_MILE = 5280; +std::string InkHUD::Applet::localizeDistance(uint32_t meters) +{ + constexpr float FEET_PER_METER = 3.28084; + constexpr uint16_t FEET_PER_MILE = 5280; - // Resulting string - std::string localized; + // Resulting string + std::string localized; - // Imperial - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - uint32_t feet = meters * FEET_PER_METER; - // Distant (miles, rounded) - if (feet > FEET_PER_MILE / 2) { - localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); - localized += "mi"; + // Imperial + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + uint32_t feet = meters * FEET_PER_METER; + // Distant (miles, rounded) + if (feet > FEET_PER_MILE / 2) { + localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); + localized += "mi"; + } + // Nearby (feet) + else { + localized += to_string(feet); + localized += "ft"; + } } - // Nearby (feet) + + // Metric else { - localized += to_string(feet); - localized += "ft"; + // Distant (kilometers, rounded) + if (meters >= 500) { + localized += to_string((uint32_t)roundf(meters / 1000.0)); + localized += "km"; + } + // Nearby (meters) + else { + localized += to_string(meters); + localized += "m"; + } } - } - // Metric - else { - // Distant (kilometers, rounded) - if (meters >= 500) { - localized += to_string((uint32_t)roundf(meters / 1000.0)); - localized += "km"; - } - // Nearby (meters) - else { - localized += to_string(meters); - localized += "m"; - } - } - - return localized; + return localized; } // Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly -void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) { - // How many times to draw along x axis - int16_t xStart; - int16_t xEnd; - switch (thicknessX) { - case 0: - assert(false); - case 1: - xStart = xCenter; - xEnd = xCenter; - break; - case 2: - xStart = xCenter; - xEnd = xCenter + 1; - break; - default: - xStart = xCenter - (thicknessX / 2); - xEnd = xCenter + (thicknessX / 2); - } - - // How many times to draw along Y axis - int16_t yStart; - int16_t yEnd; - switch (thicknessY) { - case 0: - assert(false); - case 1: - yStart = yCenter; - yEnd = yCenter; - break; - case 2: - yStart = yCenter; - yEnd = yCenter + 1; - break; - default: - yStart = yCenter - (thicknessY / 2); - yEnd = yCenter + (thicknessY / 2); - } - - // Print multiple times, overlapping - for (int16_t x = xStart; x <= xEnd; x++) { - for (int16_t y = yStart; y <= yEnd; y++) { - printAt(x, y, text, CENTER, MIDDLE); +void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) +{ + // How many times to draw along x axis + int16_t xStart; + int16_t xEnd; + switch (thicknessX) { + case 0: + assert(false); + case 1: + xStart = xCenter; + xEnd = xCenter; + break; + case 2: + xStart = xCenter; + xEnd = xCenter + 1; + break; + default: + xStart = xCenter - (thicknessX / 2); + xEnd = xCenter + (thicknessX / 2); + } + + // How many times to draw along Y axis + int16_t yStart; + int16_t yEnd; + switch (thicknessY) { + case 0: + assert(false); + case 1: + yStart = yCenter; + yEnd = yCenter; + break; + case 2: + yStart = yCenter; + yEnd = yCenter + 1; + break; + default: + yStart = yCenter - (thicknessY / 2); + yEnd = yCenter + (thicknessY / 2); + } + + // Print multiple times, overlapping + for (int16_t x = xStart; x <= xEnd; x++) { + for (int16_t y = yStart; y <= yEnd; y++) { + printAt(x, y, text, CENTER, MIDDLE); + } } - } } // Allow this applet to suppress notifications // Asked before a notification is shown via the NotificationApplet // An applet might want to suppress a notification if the applet itself already displays this info // Example: AllMessageApplet should not approve notifications for messages, if it is in foreground -bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) { - // By default, no objection - return true; +bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) +{ + // By default, no objection + return true; } // Draw the standard header, used by most Applets @@ -691,56 +761,60 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) │ │ └───────────────────────────────┘ */ -void InkHUD::Applet::drawHeader(std::string text) { - // Y position for divider - // - between header text and messages - constexpr int16_t padDivH = 2; - const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; +void InkHUD::Applet::drawHeader(std::string text) +{ + // Y position for divider + // - between header text and messages + constexpr int16_t padDivH = 2; + const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; - // Print header - printAt(0, padDivH, text); + // Print header + printAt(0, padDivH, text); - // Divider - // - below header text: separates message - // - above header text: separates other applets - for (int16_t x = 0; x < width(); x += 2) { - drawPixel(x, 0, BLACK); - drawPixel(x, headerDivY, BLACK); // Dotted 50% - } + // Divider + // - below header text: separates message + // - above header text: separates other applets + for (int16_t x = 0; x < width(); x += 2) { + drawPixel(x, 0, BLACK); + drawPixel(x, headerDivY, BLACK); // Dotted 50% + } } // Get the height of the standard applet header // This will vary, depending on font // Applets use this value to avoid drawing overtop the header -uint16_t InkHUD::Applet::getHeaderHeight() { - // Y position for divider - // - between header text and messages - constexpr int16_t padDivH = 2; - const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; +uint16_t InkHUD::Applet::getHeaderHeight() +{ + // Y position for divider + // - between header text and messages + constexpr int16_t padDivH = 2; + const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; - return headerDivY + 1; // "Plus one": height is always one more than Y position + return headerDivY + 1; // "Plus one": height is always one more than Y position } // "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio -uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) { - // Determine whether we're limited by width or height - // Makes sure we draw the logo as large as possible, within the specified region, - // while still maintaining correct aspect ratio - if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) - return limitHeight * LOGO_ASPECT_RATIO; - else - return limitWidth; +uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) +{ + // Determine whether we're limited by width or height + // Makes sure we draw the logo as large as possible, within the specified region, + // while still maintaining correct aspect ratio + if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) + return limitHeight * LOGO_ASPECT_RATIO; + else + return limitWidth; } // "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio -uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) { - // Determine whether we're limited by width or height - // Makes sure we draw the logo as large as possible, within the specified region, - // while still maintaining correct aspect ratio - if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) - return limitWidth / LOGO_ASPECT_RATIO; - else - return limitHeight; +uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) +{ + // Determine whether we're limited by width or height + // Makes sure we draw the logo as large as possible, within the specified region, + // while still maintaining correct aspect ratio + if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) + return limitWidth / LOGO_ASPECT_RATIO; + else + return limitHeight; } // Draw a scalable Meshtastic logo @@ -755,149 +829,150 @@ uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight // // \\ */ -void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color) { - struct Point { - int x; - int y; - }; - typedef Point Distance; +void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color) +{ + struct Point { + int x; + int y; + }; + typedef Point Distance; - int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. - int16_t logoL = centerX - (width / 2) + (logoTh / 2); - int16_t logoT = centerY - (height / 2) + (logoTh / 2); - int16_t logoW = width - logoTh; - int16_t logoH = height - logoTh; - int16_t logoR = logoL + logoW - 1; - int16_t logoB = logoT + logoH - 1; + int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. + int16_t logoL = centerX - (width / 2) + (logoTh / 2); + int16_t logoT = centerY - (height / 2) + (logoTh / 2); + int16_t logoW = width - logoTh; + int16_t logoH = height - logoTh; + int16_t logoR = logoL + logoW - 1; + int16_t logoB = logoT + logoH - 1; - // Points for paths (a, b, and c) - /* - +-----------------------------+ - --| a2 b2/c1 | - | | - | | - | | - --| a1 b1 c2 | - +-----------------------------+ - | | | | - */ + // Points for paths (a, b, and c) + /* + +-----------------------------+ + --| a2 b2/c1 | + | | + | | + | | + --| a1 b1 c2 | + +-----------------------------+ + | | | | + */ - Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; - Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; - Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; - Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; - Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; - Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; + Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; + Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; + Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; + Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; + Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; + Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; - // Find angle of the path(s) - // Used to thicken the single pixel paths - /* - +-------------------------------+ - | a2 | - | -| | - | -/ | | - | -/ | | - | -/# | | - | -/ # | | - | / # | | - | a1---------- | - +-------------------------------+ - */ + // Find angle of the path(s) + // Used to thicken the single pixel paths + /* + +-------------------------------+ + | a2 | + | -| | + | -/ | | + | -/ | | + | -/# | | + | -/ # | | + | / # | | + | a1---------- | + +-------------------------------+ + */ - Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; - float angle = tanh((float)deltaA.y / deltaA.x); + Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; + float angle = tanh((float)deltaA.y / deltaA.x); - // Distance (at right angle to the paths), which will give corners for our "quads" - // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner - /* - | a2 - | . - | .. - | aq1 .. - | # .. - | | # .. - |fromPath.y | # .. - | +----a1 - | - | fromPath.x - +-------------------------------- - */ + // Distance (at right angle to the paths), which will give corners for our "quads" + // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner + /* + | a2 + | . + | .. + | aq1 .. + | # .. + | | # .. + |fromPath.y | # .. + | +----a1 + | + | fromPath.x + +-------------------------------- + */ - Distance fromPath; - fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; - fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; + Distance fromPath; + fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; + fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; - // Make the paths thick - // Corner points for the rectangles (quads): - /* + // Make the paths thick + // Corner points for the rectangles (quads): + /* - aq2 - a2 - / aq3 - / - / - aq1 / - a1 - aq3 - */ + aq2 + a2 + / aq3 + / + / + aq1 / + a1 + aq3 + */ - // Filled as two triangles per quad: - /* - aq2 # - # ### - ## # aq3 - ## ### - - ## #### -/ - ## ### -/ - ## #### -/ - aq1 ## -/ - --- -/ - \---aq4 - */ + // Filled as two triangles per quad: + /* + aq2 # + # ### + ## # aq3 + ## ### - + ## #### -/ + ## ### -/ + ## #### -/ + aq1 ## -/ + --- -/ + \---aq4 + */ - // Make the path thick: path a becomes quad a - Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; - Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; - Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; - Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; - fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, color); - fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color); + // Make the path thick: path a becomes quad a + Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; + Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; + Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; + Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; + fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, color); + fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color); - // Make the path thick: path b becomes quad b - Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; - Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; - Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; - Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; - fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, color); - fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color); + // Make the path thick: path b becomes quad b + Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; + Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; + Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; + Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; + fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, color); + fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color); - // Make the path thick: path c becomes quad c - Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; - Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; - Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; - Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; - fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, color); - fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color); + // Make the path thick: path c becomes quad c + Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; + Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; + Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; + Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; + fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, color); + fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color); - // Radius the intersection of quad b and quad c - /* - b2 / c1 - #### - ## ## - / \ - / \/ \ - / /\ \ - / / \ \ + // Radius the intersection of quad b and quad c + /* + b2 / c1 + #### + ## ## + / \ + / \/ \ + / /\ \ + / / \ \ - */ + */ - // Don't attempt if logo is tiny - if (logoTh > 3) { - // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding - // We get better results just re-deriving it - int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); - fillCircle(b2.x, b2.y, capRad, color); - } + // Don't attempt if logo is tiny + if (logoTh > 3) { + // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding + // We get better results just re-deriving it + int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); + fillCircle(b2.x, b2.y, capRad, color); + } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index cbfe468e2..b35ca5cc0 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -24,160 +24,157 @@ #include "./Tile.h" #include "graphics/niche/Drivers/EInk/EInk.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ using NicheGraphics::Drivers::EInk; using std::to_string; -class Applet : public GFX { -public: - // Which edge Applet::printAt will place on the Y parameter - enum VerticalAlignment : uint8_t { - TOP, - MIDDLE, - BOTTOM, - }; +class Applet : public GFX +{ + public: + // Which edge Applet::printAt will place on the Y parameter + enum VerticalAlignment : uint8_t { + TOP, + MIDDLE, + BOTTOM, + }; - // Which edge Applet::printAt will place on the X parameter - enum HorizontalAlignment : uint8_t { - LEFT, - RIGHT, - CENTER, - }; + // Which edge Applet::printAt will place on the X parameter + enum HorizontalAlignment : uint8_t { + LEFT, + RIGHT, + CENTER, + }; - // An easy-to-understand interpretation of SNR and RSSI - // Calculate with Applet::getSignalStrength - enum SignalStrength : int8_t { - SIGNAL_UNKNOWN = -1, - SIGNAL_NONE, - SIGNAL_BAD, - SIGNAL_FAIR, - SIGNAL_GOOD, - }; + // An easy-to-understand interpretation of SNR and RSSI + // Calculate with Applet::getSignalStrength + enum SignalStrength : int8_t { + SIGNAL_UNKNOWN = -1, + SIGNAL_NONE, + SIGNAL_BAD, + SIGNAL_FAIR, + SIGNAL_GOOD, + }; - Applet(); + Applet(); - void setTile(Tile *t); // Should only be called via Tile::setApplet - Tile *getTile(); // Tile with which this applet is linked + void setTile(Tile *t); // Should only be called via Tile::setApplet + Tile *getTile(); // Tile with which this applet is linked - // Rendering + // Rendering - void render(); // Draw the applet - bool wantsToRender(); // Check whether applet wants to render - bool wantsToAutoshow(); // Check whether applet wants to become foreground - Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer - void updateDimensions(); // Get current size from tile - void resetDrawingSpace(); // Makes sure every render starts with same parameters + void render(); // Draw the applet + bool wantsToRender(); // Check whether applet wants to render + bool wantsToAutoshow(); // Check whether applet wants to become foreground + Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer + void updateDimensions(); // Get current size from tile + void resetDrawingSpace(); // Makes sure every render starts with same parameters - // State of the applet + // State of the applet - void activate(); // Begin running - void deactivate(); // Stop running - void bringToForeground(); // Show - void sendToBackground(); // Hide - bool isActive(); - bool isForeground(); + void activate(); // Begin running + void deactivate(); // Stop running + void bringToForeground(); // Show + void sendToBackground(); // Hide + bool isActive(); + bool isForeground(); - // Event handlers + // Event handlers - virtual void onRender() = 0; // All drawing happens here - virtual void onActivate() {} - virtual void onDeactivate() {} - virtual void onForeground() {} - virtual void onBackground() {} - virtual void onShutdown() {} - virtual void onButtonShortPress() {} - virtual void onButtonLongPress() {} - virtual void onExitShort() {} - virtual void onExitLong() {} - virtual void onNavUp() {} - virtual void onNavDown() {} - virtual void onNavLeft() {} - virtual void onNavRight() {} + virtual void onRender() = 0; // All drawing happens here + virtual void onActivate() {} + virtual void onDeactivate() {} + virtual void onForeground() {} + virtual void onBackground() {} + virtual void onShutdown() {} + virtual void onButtonShortPress() {} + virtual void onButtonLongPress() {} + virtual void onExitShort() {} + virtual void onExitLong() {} + virtual void onNavUp() {} + virtual void onNavDown() {} + virtual void onNavLeft() {} + virtual void onNavRight() {} - virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification + virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification - static uint16_t getHeaderHeight(); // How tall the "standard" applet header is + static uint16_t getHeaderHeight(); // How tall the "standard" applet header is - static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets + static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets - const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet + const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet -protected: - void drawPixel(int16_t x, int16_t y, - uint16_t color) override; // Place a single pixel. All drawing output passes through here + protected: + void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here - void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update - void requestAutoshow(); // Ask for applet to be moved to foreground + void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update + void requestAutoshow(); // Ask for applet to be moved to foreground - uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 - uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 - void setCrop(int16_t left, int16_t top, uint16_t width, - uint16_t height); // Ignore pixels drawn outside a certain region - void resetCrop(); // Removes setCrop() + uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 + uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 + void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region + void resetCrop(); // Removes setCrop() - // Text + // Text - void setFont(AppletFont f); - AppletFont getFont(); - uint16_t getTextWidth(std::string text); - uint16_t getTextWidth(const char *text); - uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped - void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, - uint8_t thicknessY); // Faux bold - void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping + void setFont(AppletFont f); + AppletFont getFont(); + uint16_t getTextWidth(std::string text); + uint16_t getTextWidth(const char *text); + uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped + void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold + void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping - void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, - Color color); // Fill with sparse lines - void drawHeader(std::string text); // Draw the standard applet header + void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines + void drawHeader(std::string text); // Draw the standard applet header - // Meshtastic Logo + // Meshtastic Logo - static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo - uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region - uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region - void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, - Color color = BLACK); // Draw the Meshtastic logo + static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo + uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region + uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region + void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, + Color color = BLACK); // Draw the Meshtastic logo - std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc - SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value - std::string getTimeString(uint32_t epochSeconds); // Human readable - std::string getTimeString(); // Current time, human readable - uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu - std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric - std::string parse(std::string text); // Handle text which might contain special chars - std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars - bool isPrintable(std::string); // Check for characters which the font can't print + std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc + SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value + std::string getTimeString(uint32_t epochSeconds); // Human readable + std::string getTimeString(); // Current time, human readable + uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu + std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric + std::string parse(std::string text); // Handle text which might contain special chars + std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars + bool isPrintable(std::string); // Check for characters which the font can't print - // Convenient references + // Convenient references - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; - Persistence::LatestMessage *latestMessage = nullptr; + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; + Persistence::LatestMessage *latestMessage = nullptr; -private: - Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM - bool active = false; // Has the user enabled this applet (at run-time)? - bool foreground = false; // Is the applet currently drawn on a tile? + private: + Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM + bool active = false; // Has the user enabled this applet (at run-time)? + bool foreground = false; // Is the applet currently drawn on a tile? - bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing. - bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? - NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = - NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the - // display + bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing. + bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? + NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = + NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display - using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly - using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. + using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly + using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. - AppletFont currentFont; // As passed to setFont + AppletFont currentFont; // As passed to setFont - // As set by setCrop - int16_t cropLeft = 0; - int16_t cropTop = 0; - uint16_t cropWidth = 0; - uint16_t cropHeight = 0; + // As set by setCrop + int16_t cropLeft = 0; + int16_t cropTop = 0; + uint16_t cropWidth = 0; + uint16_t cropHeight = 0; }; }; // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 946378532..6c7a7b491 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -6,51 +6,53 @@ using namespace NicheGraphics; -InkHUD::AppletFont::AppletFont() { - // Default constructor uses the in-built AdafruitGFX font (not recommended) +InkHUD::AppletFont::AppletFont() +{ + // Default constructor uses the in-built AdafruitGFX font (not recommended) } InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding, int8_t paddingTop, int8_t paddingBottom) - : gfxFont(&adafruitGFXFont), encoding(encoding) { - // AdafruitGFX fonts are drawn relative to a "cursor line"; - // they print as if the glyphs are resting on the line of piece of ruled paper. - // The glyphs also each have a different height. + : gfxFont(&adafruitGFXFont), encoding(encoding) +{ + // AdafruitGFX fonts are drawn relative to a "cursor line"; + // they print as if the glyphs are resting on the line of piece of ruled paper. + // The glyphs also each have a different height. - // To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text - // We also need to know where that "cursor line" sits inside this "line height"; - // we need this additional info in order to align text by top-left, bottom-right, etc + // To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text + // We also need to know where that "cursor line" sits inside this "line height"; + // we need this additional info in order to align text by top-left, bottom-right, etc - // AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding, - // which we'd rather not deal with. If we want padding, we'll add it manually. + // AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding, + // which we'd rather not deal with. If we want padding, we'll add it manually. - this->ascenderHeight = 0; - this->descenderHeight = 0; - this->height = 0; + this->ascenderHeight = 0; + this->descenderHeight = 0; + this->height = 0; - // Scan each glyph in the AdafruitGFX font - for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) { - uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph - this->height = max(this->height, glyphHeight); // Store if it's a new max + // Scan each glyph in the AdafruitGFX font + for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) { + uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph + this->height = max(this->height, glyphHeight); // Store if it's a new max - // Calculate how far the glyph rises the cursor line - // Store if new max value - // Caution: signed and unsigned types - int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; - if (glyphAscender > 0) - this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); + // Calculate how far the glyph rises the cursor line + // Store if new max value + // Caution: signed and unsigned types + int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; + if (glyphAscender > 0) + this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); - int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; - if (glyphDescender > 0) - this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); - } + int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; + if (glyphDescender > 0) + this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); + } - // Apply any manual padding to grow or shrink the line size - // Helpful if a font has one or two exceptionally large characters, which would make the lines ridiculously tall - ascenderHeight += paddingTop; - descenderHeight += paddingBottom; + // Apply any manual padding to grow or shrink the line size + // Helpful if a font has one or two exceptionally large characters, which would make the lines ridiculously tall + ascenderHeight += paddingTop; + descenderHeight += paddingBottom; - // Find how far the cursor advances when we "print" a space character - spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; + // Find how far the cursor advances when we "print" a space character + spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; } /* @@ -66,650 +68,664 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding ▼ ### ▼ */ -uint8_t InkHUD::AppletFont::lineHeight() { return this->height; } +uint8_t InkHUD::AppletFont::lineHeight() +{ + return this->height; +} // AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). // This value is the height of the font, above that imaginary line. // Used to calculate the true height of the font -uint8_t InkHUD::AppletFont::heightAboveCursor() { return this->ascenderHeight; } +uint8_t InkHUD::AppletFont::heightAboveCursor() +{ + return this->ascenderHeight; +} // AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). // This value is the height of the font, below that imaginary line. // Used to calculate the true height of the font -uint8_t InkHUD::AppletFont::heightBelowCursor() { return this->descenderHeight; } +uint8_t InkHUD::AppletFont::heightBelowCursor() +{ + return this->descenderHeight; +} // Width of the space character // Used with Applet::printWrapped -uint8_t InkHUD::AppletFont::widthBetweenWords() { return this->spaceCharWidth; } +uint8_t InkHUD::AppletFont::widthBetweenWords() +{ + return this->spaceCharWidth; +} // Convert a unicode char from set of UTF-8 bytes to UTF-32 // Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value -uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) { - uint32_t utf32 = 0; +uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) +{ + uint32_t utf32 = 0; - switch (utf8.length()) { - case 2: - // 5 bits + 6 bits - utf32 |= (utf8.at(0) & 0b00011111) << 6; - utf32 |= (utf8.at(1) & 0b00111111); - break; + switch (utf8.length()) { + case 2: + // 5 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00011111) << 6; + utf32 |= (utf8.at(1) & 0b00111111); + break; - case 3: - // 4 bits + 6 bits + 6 bits - utf32 |= (utf8.at(0) & 0b00001111) << (6 + 6); - utf32 |= (utf8.at(1) & 0b00111111) << 6; - utf32 |= (utf8.at(2) & 0b00111111); - break; + case 3: + // 4 bits + 6 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00001111) << (6 + 6); + utf32 |= (utf8.at(1) & 0b00111111) << 6; + utf32 |= (utf8.at(2) & 0b00111111); + break; - case 4: - // 3 bits + 6 bits + 6 bits + 6 bits - utf32 |= (utf8.at(0) & 0b00000111) << (6 + 6 + 6); - utf32 |= (utf8.at(1) & 0b00111111) << (6 + 6); - utf32 |= (utf8.at(2) & 0b00111111) << 6; - utf32 |= (utf8.at(3) & 0b00111111); - break; - default: - return 0; - } + case 4: + // 3 bits + 6 bits + 6 bits + 6 bits + utf32 |= (utf8.at(0) & 0b00000111) << (6 + 6 + 6); + utf32 |= (utf8.at(1) & 0b00111111) << (6 + 6); + utf32 |= (utf8.at(2) & 0b00111111) << 6; + utf32 |= (utf8.at(3) & 0b00111111); + break; + default: + return 0; + } - return utf32; + return utf32; } // Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII // Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars -std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) { - // Final processed output - std::string decoded; +std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) +{ + // Final processed output + std::string decoded; - // Holds bytes for one UTF-8 char during parsing - std::string utf8Char; - uint8_t utf8CharSize = 0; + // Holds bytes for one UTF-8 char during parsing + std::string utf8Char; + uint8_t utf8CharSize = 0; - for (char &c : encoded) { + for (char &c : encoded) { - // If first byte - if (utf8Char.empty()) { - // If MSB is unset, byte is an ASCII char - // If MSB is set, byte is part of a UTF-8 char. Counting number of higher-order bits tells how many bytes in char - if ((c & 0x80)) { - char c1 = c; - while (c1 & 0x80) { - c1 <<= 1; - utf8CharSize++; + // If first byte + if (utf8Char.empty()) { + // If MSB is unset, byte is an ASCII char + // If MSB is set, byte is part of a UTF-8 char. Counting number of higher-order bits tells how many bytes in char + if ((c & 0x80)) { + char c1 = c; + while (c1 & 0x80) { + c1 <<= 1; + utf8CharSize++; + } + } } - } - } - // Append the byte to the UTF-8 char we're building - utf8Char += c; + // Append the byte to the UTF-8 char we're building + utf8Char += c; - // More bytes left to collect. Iterate. - if (utf8Char.length() < utf8CharSize) - continue; + // More bytes left to collect. Iterate. + if (utf8Char.length() < utf8CharSize) + continue; - // Now collected all bytes for this char - // Remap the value to match the encoding of our 8-bit AppletFont - decoded += applyEncoding(utf8Char); + // Now collected all bytes for this char + // Remap the value to match the encoding of our 8-bit AppletFont + decoded += applyEncoding(utf8Char); - // Reset, ready to build next UTF-8 char from the encoded bytes - utf8Char.clear(); - utf8CharSize = 0; - } // For each char + // Reset, ready to build next UTF-8 char from the encoded bytes + utf8Char.clear(); + utf8CharSize = 0; + } // For each char - // All chars processed, return result - return decoded; + // All chars processed, return result + return decoded; } // Re-encode a single UTF-8 character to extended ASCII // Target encoding depends on the font -char InkHUD::AppletFont::applyEncoding(std::string utf8) { - // ##################################################### Syntactic Sugar - // ##################################################### -#define REMAP(in, out) \ - case in: \ - return out; - // ########################################################################################################################### +char InkHUD::AppletFont::applyEncoding(std::string utf8) +{ + // ##################################################### Syntactic Sugar ##################################################### +#define REMAP(in, out) \ + case in: \ + return out; + // ########################################################################################################################### - // Latin - Central Europe - // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1250.TXT - if (encoding == WINDOWS_1250) { - // 1-Byte chars: no remapping - if (utf8.length() == 1) - return utf8.at(0); + // Latin - Central Europe + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1250.TXT + if (encoding == WINDOWS_1250) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); - // Multi-byte chars: - switch (toUtf32(utf8)) { - REMAP(0x20AC, 0x80); // EURO SIGN - REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK - REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK - REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS - REMAP(0x2020, 0x86); // DAGGER - REMAP(0x2021, 0x87); // DOUBLE DAGGER - REMAP(0x2030, 0x89); // PER MILLE SIGN - REMAP(0x0160, 0x8A); // LATIN CAPITAL LETTER S WITH CARON - REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - REMAP(0x015A, 0x8C); // LATIN CAPITAL LETTER S WITH ACUTE - REMAP(0x0164, 0x8D); // LATIN CAPITAL LETTER T WITH CARON - REMAP(0x017D, 0x8E); // LATIN CAPITAL LETTER Z WITH CARON - REMAP(0x0179, 0x8F); // LATIN CAPITAL LETTER Z WITH ACUTE + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x20AC, 0x80); // EURO SIGN + REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK + REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86); // DAGGER + REMAP(0x2021, 0x87); // DOUBLE DAGGER + REMAP(0x2030, 0x89); // PER MILLE SIGN + REMAP(0x0160, 0x8A); // LATIN CAPITAL LETTER S WITH CARON + REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x015A, 0x8C); // LATIN CAPITAL LETTER S WITH ACUTE + REMAP(0x0164, 0x8D); // LATIN CAPITAL LETTER T WITH CARON + REMAP(0x017D, 0x8E); // LATIN CAPITAL LETTER Z WITH CARON + REMAP(0x0179, 0x8F); // LATIN CAPITAL LETTER Z WITH ACUTE - REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK - REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK - REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK - REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK - REMAP(0x2022, 0x95); // BULLET - REMAP(0x2013, 0x96); // EN DASH - REMAP(0x2014, 0x97); // EM DASH - REMAP(0x2122, 0x99); // TRADE MARK SIGN - REMAP(0x0161, 0x9A); // LATIN SMALL LETTER S WITH CARON - REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - REMAP(0x015B, 0x9C); // LATIN SMALL LETTER S WITH ACUTE - REMAP(0x0165, 0x9D); // LATIN SMALL LETTER T WITH CARON - REMAP(0x017E, 0x9E); // LATIN SMALL LETTER Z WITH CARON - REMAP(0x017A, 0x9F); // LATIN SMALL LETTER Z WITH ACUTE + REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95); // BULLET + REMAP(0x2013, 0x96); // EN DASH + REMAP(0x2014, 0x97); // EM DASH + REMAP(0x2122, 0x99); // TRADE MARK SIGN + REMAP(0x0161, 0x9A); // LATIN SMALL LETTER S WITH CARON + REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x015B, 0x9C); // LATIN SMALL LETTER S WITH ACUTE + REMAP(0x0165, 0x9D); // LATIN SMALL LETTER T WITH CARON + REMAP(0x017E, 0x9E); // LATIN SMALL LETTER Z WITH CARON + REMAP(0x017A, 0x9F); // LATIN SMALL LETTER Z WITH ACUTE - REMAP(0x00A0, 0xA0); // NO-BREAK SPACE - REMAP(0x02C7, 0xA1); // CARON - REMAP(0x02D8, 0xA2); // BREVE - REMAP(0x0141, 0xA3); // LATIN CAPITAL LETTER L WITH STROKE - REMAP(0x00A4, 0xA4); // CURRENCY SIGN - REMAP(0x0104, 0xA5); // LATIN CAPITAL LETTER A WITH OGONEK - REMAP(0x00A6, 0xA6); // BROKEN BAR - REMAP(0x00A7, 0xA7); // SECTION SIGN - REMAP(0x00A8, 0xA8); // DIAERESIS - REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN - REMAP(0x015E, 0xAA); // LATIN CAPITAL LETTER S WITH CEDILLA - REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00AC, 0xAC); // NOT SIGN - REMAP(0x00AD, 0xAD); // SOFT HYPHEN - REMAP(0x00AE, 0xAE); // REGISTERED SIGN - REMAP(0x017B, 0xAF); // LATIN CAPITAL LETTER Z WITH DOT ABOVE + REMAP(0x00A0, 0xA0); // NO-BREAK SPACE + REMAP(0x02C7, 0xA1); // CARON + REMAP(0x02D8, 0xA2); // BREVE + REMAP(0x0141, 0xA3); // LATIN CAPITAL LETTER L WITH STROKE + REMAP(0x00A4, 0xA4); // CURRENCY SIGN + REMAP(0x0104, 0xA5); // LATIN CAPITAL LETTER A WITH OGONEK + REMAP(0x00A6, 0xA6); // BROKEN BAR + REMAP(0x00A7, 0xA7); // SECTION SIGN + REMAP(0x00A8, 0xA8); // DIAERESIS + REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN + REMAP(0x015E, 0xAA); // LATIN CAPITAL LETTER S WITH CEDILLA + REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC); // NOT SIGN + REMAP(0x00AD, 0xAD); // SOFT HYPHEN + REMAP(0x00AE, 0xAE); // REGISTERED SIGN + REMAP(0x017B, 0xAF); // LATIN CAPITAL LETTER Z WITH DOT ABOVE - REMAP(0x00B0, 0xB0); // DEGREE SIGN - REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN - REMAP(0x02DB, 0xB2); // OGONEK - REMAP(0x0142, 0xB3); // LATIN SMALL LETTER L WITH STROKE - REMAP(0x00B4, 0xB4); // ACUTE ACCENT - REMAP(0x00B5, 0xB5); // MICRO SIGN - REMAP(0x00B6, 0xB6); // PILCROW SIGN - REMAP(0x00B7, 0xB7); // MIDDLE DOT - REMAP(0x00B8, 0xB8); // CEDILLA - REMAP(0x0105, 0xB9); // LATIN SMALL LETTER A WITH OGONEK - REMAP(0x015F, 0xBA); // LATIN SMALL LETTER S WITH CEDILLA - REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x013D, 0xBC); // LATIN CAPITAL LETTER L WITH CARON - REMAP(0x02DD, 0xBD); // DOUBLE ACUTE ACCENT - REMAP(0x013E, 0xBE); // LATIN SMALL LETTER L WITH CARON - REMAP(0x017C, 0xBF); // LATIN SMALL LETTER Z WITH DOT ABOVE + REMAP(0x00B0, 0xB0); // DEGREE SIGN + REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN + REMAP(0x02DB, 0xB2); // OGONEK + REMAP(0x0142, 0xB3); // LATIN SMALL LETTER L WITH STROKE + REMAP(0x00B4, 0xB4); // ACUTE ACCENT + REMAP(0x00B5, 0xB5); // MICRO SIGN + REMAP(0x00B6, 0xB6); // PILCROW SIGN + REMAP(0x00B7, 0xB7); // MIDDLE DOT + REMAP(0x00B8, 0xB8); // CEDILLA + REMAP(0x0105, 0xB9); // LATIN SMALL LETTER A WITH OGONEK + REMAP(0x015F, 0xBA); // LATIN SMALL LETTER S WITH CEDILLA + REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x013D, 0xBC); // LATIN CAPITAL LETTER L WITH CARON + REMAP(0x02DD, 0xBD); // DOUBLE ACUTE ACCENT + REMAP(0x013E, 0xBE); // LATIN SMALL LETTER L WITH CARON + REMAP(0x017C, 0xBF); // LATIN SMALL LETTER Z WITH DOT ABOVE - REMAP(0x0154, 0xC0); // LATIN CAPITAL LETTER R WITH ACUTE - REMAP(0x00C1, 0xC1); // LATIN CAPITAL LETTER A WITH ACUTE - REMAP(0x00C2, 0xC2); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX - REMAP(0x0102, 0xC3); // LATIN CAPITAL LETTER A WITH BREVE - REMAP(0x00C4, 0xC4); // LATIN CAPITAL LETTER A WITH DIAERESIS - REMAP(0x0139, 0xC5); // LATIN CAPITAL LETTER L WITH ACUTE - REMAP(0x0106, 0xC6); // LATIN CAPITAL LETTER C WITH ACUTE - REMAP(0x00C7, 0xC7); // LATIN CAPITAL LETTER C WITH CEDILLA - REMAP(0x010C, 0xC8); // LATIN CAPITAL LETTER C WITH CARON - REMAP(0x00C9, 0xC9); // LATIN CAPITAL LETTER E WITH ACUTE - REMAP(0x0118, 0xCA); // LATIN CAPITAL LETTER E WITH OGONEK - REMAP(0x00CB, 0xCB); // LATIN CAPITAL LETTER E WITH DIAERESIS - REMAP(0x011A, 0xCC); // LATIN CAPITAL LETTER E WITH CARON - REMAP(0x00CD, 0xCD); // LATIN CAPITAL LETTER I WITH ACUTE - REMAP(0x00CE, 0xCE); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX - REMAP(0x010E, 0xCF); // LATIN CAPITAL LETTER D WITH CARON + REMAP(0x0154, 0xC0); // LATIN CAPITAL LETTER R WITH ACUTE + REMAP(0x00C1, 0xC1); // LATIN CAPITAL LETTER A WITH ACUTE + REMAP(0x00C2, 0xC2); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + REMAP(0x0102, 0xC3); // LATIN CAPITAL LETTER A WITH BREVE + REMAP(0x00C4, 0xC4); // LATIN CAPITAL LETTER A WITH DIAERESIS + REMAP(0x0139, 0xC5); // LATIN CAPITAL LETTER L WITH ACUTE + REMAP(0x0106, 0xC6); // LATIN CAPITAL LETTER C WITH ACUTE + REMAP(0x00C7, 0xC7); // LATIN CAPITAL LETTER C WITH CEDILLA + REMAP(0x010C, 0xC8); // LATIN CAPITAL LETTER C WITH CARON + REMAP(0x00C9, 0xC9); // LATIN CAPITAL LETTER E WITH ACUTE + REMAP(0x0118, 0xCA); // LATIN CAPITAL LETTER E WITH OGONEK + REMAP(0x00CB, 0xCB); // LATIN CAPITAL LETTER E WITH DIAERESIS + REMAP(0x011A, 0xCC); // LATIN CAPITAL LETTER E WITH CARON + REMAP(0x00CD, 0xCD); // LATIN CAPITAL LETTER I WITH ACUTE + REMAP(0x00CE, 0xCE); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + REMAP(0x010E, 0xCF); // LATIN CAPITAL LETTER D WITH CARON - REMAP(0x0110, 0xD0); // LATIN CAPITAL LETTER D WITH STROKE - REMAP(0x0143, 0xD1); // LATIN CAPITAL LETTER N WITH ACUTE - REMAP(0x0147, 0xD2); // LATIN CAPITAL LETTER N WITH CARON - REMAP(0x00D3, 0xD3); // LATIN CAPITAL LETTER O WITH ACUTE - REMAP(0x00D4, 0xD4); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX - REMAP(0x0150, 0xD5); // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE - REMAP(0x00D6, 0xD6); // LATIN CAPITAL LETTER O WITH DIAERESIS - REMAP(0x00D7, 0xD7); // MULTIPLICATION SIGN - REMAP(0x0158, 0xD8); // LATIN CAPITAL LETTER R WITH CARON - REMAP(0x016E, 0xD9); // LATIN CAPITAL LETTER U WITH RING ABOVE - REMAP(0x00DA, 0xDA); // LATIN CAPITAL LETTER U WITH ACUTE - REMAP(0x0170, 0xDB); // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE - REMAP(0x00DC, 0xDC); // LATIN CAPITAL LETTER U WITH DIAERESIS - REMAP(0x00DD, 0xDD); // LATIN CAPITAL LETTER Y WITH ACUTE - REMAP(0x0162, 0xDE); // LATIN CAPITAL LETTER T WITH CEDILLA - REMAP(0x00DF, 0xDF); // LATIN SMALL LETTER SHARP S + REMAP(0x0110, 0xD0); // LATIN CAPITAL LETTER D WITH STROKE + REMAP(0x0143, 0xD1); // LATIN CAPITAL LETTER N WITH ACUTE + REMAP(0x0147, 0xD2); // LATIN CAPITAL LETTER N WITH CARON + REMAP(0x00D3, 0xD3); // LATIN CAPITAL LETTER O WITH ACUTE + REMAP(0x00D4, 0xD4); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + REMAP(0x0150, 0xD5); // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + REMAP(0x00D6, 0xD6); // LATIN CAPITAL LETTER O WITH DIAERESIS + REMAP(0x00D7, 0xD7); // MULTIPLICATION SIGN + REMAP(0x0158, 0xD8); // LATIN CAPITAL LETTER R WITH CARON + REMAP(0x016E, 0xD9); // LATIN CAPITAL LETTER U WITH RING ABOVE + REMAP(0x00DA, 0xDA); // LATIN CAPITAL LETTER U WITH ACUTE + REMAP(0x0170, 0xDB); // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + REMAP(0x00DC, 0xDC); // LATIN CAPITAL LETTER U WITH DIAERESIS + REMAP(0x00DD, 0xDD); // LATIN CAPITAL LETTER Y WITH ACUTE + REMAP(0x0162, 0xDE); // LATIN CAPITAL LETTER T WITH CEDILLA + REMAP(0x00DF, 0xDF); // LATIN SMALL LETTER SHARP S - REMAP(0x0155, 0xE0); // LATIN SMALL LETTER R WITH ACUTE - REMAP(0x00E1, 0xE1); // LATIN SMALL LETTER A WITH ACUTE - REMAP(0x00E2, 0xE2); // LATIN SMALL LETTER A WITH CIRCUMFLEX - REMAP(0x0103, 0xE3); // LATIN SMALL LETTER A WITH BREVE - REMAP(0x00E4, 0xE4); // LATIN SMALL LETTER A WITH DIAERESIS - REMAP(0x013A, 0xE5); // LATIN SMALL LETTER L WITH ACUTE - REMAP(0x0107, 0xE6); // LATIN SMALL LETTER C WITH ACUTE - REMAP(0x00E7, 0xE7); // LATIN SMALL LETTER C WITH CEDILLA - REMAP(0x010D, 0xE8); // LATIN SMALL LETTER C WITH CARON - REMAP(0x00E9, 0xE9); // LATIN SMALL LETTER E WITH ACUTE - REMAP(0x0119, 0xEA); // LATIN SMALL LETTER E WITH OGONEK - REMAP(0x00EB, 0xEB); // LATIN SMALL LETTER E WITH DIAERESIS - REMAP(0x011B, 0xEC); // LATIN SMALL LETTER E WITH CARON - REMAP(0x00ED, 0xED); // LATIN SMALL LETTER I WITH ACUTE - REMAP(0x00EE, 0xEE); // LATIN SMALL LETTER I WITH CIRCUMFLEX - REMAP(0x010F, 0xEF); // LATIN SMALL LETTER D WITH CARON + REMAP(0x0155, 0xE0); // LATIN SMALL LETTER R WITH ACUTE + REMAP(0x00E1, 0xE1); // LATIN SMALL LETTER A WITH ACUTE + REMAP(0x00E2, 0xE2); // LATIN SMALL LETTER A WITH CIRCUMFLEX + REMAP(0x0103, 0xE3); // LATIN SMALL LETTER A WITH BREVE + REMAP(0x00E4, 0xE4); // LATIN SMALL LETTER A WITH DIAERESIS + REMAP(0x013A, 0xE5); // LATIN SMALL LETTER L WITH ACUTE + REMAP(0x0107, 0xE6); // LATIN SMALL LETTER C WITH ACUTE + REMAP(0x00E7, 0xE7); // LATIN SMALL LETTER C WITH CEDILLA + REMAP(0x010D, 0xE8); // LATIN SMALL LETTER C WITH CARON + REMAP(0x00E9, 0xE9); // LATIN SMALL LETTER E WITH ACUTE + REMAP(0x0119, 0xEA); // LATIN SMALL LETTER E WITH OGONEK + REMAP(0x00EB, 0xEB); // LATIN SMALL LETTER E WITH DIAERESIS + REMAP(0x011B, 0xEC); // LATIN SMALL LETTER E WITH CARON + REMAP(0x00ED, 0xED); // LATIN SMALL LETTER I WITH ACUTE + REMAP(0x00EE, 0xEE); // LATIN SMALL LETTER I WITH CIRCUMFLEX + REMAP(0x010F, 0xEF); // LATIN SMALL LETTER D WITH CARON - REMAP(0x0111, 0xF0); // LATIN SMALL LETTER D WITH STROKE - REMAP(0x0144, 0xF1); // LATIN SMALL LETTER N WITH ACUTE - REMAP(0x0148, 0xF2); // LATIN SMALL LETTER N WITH CARON - REMAP(0x00F3, 0xF3); // LATIN SMALL LETTER O WITH ACUTE - REMAP(0x00F4, 0xF4); // LATIN SMALL LETTER O WITH CIRCUMFLEX - REMAP(0x0151, 0xF5); // LATIN SMALL LETTER O WITH DOUBLE ACUTE - REMAP(0x00F6, 0xF6); // LATIN SMALL LETTER O WITH DIAERESIS - REMAP(0x00F7, 0xF7); // DIVISION SIGN - REMAP(0x0159, 0xF8); // LATIN SMALL LETTER R WITH CARON - REMAP(0x016F, 0xF9); // LATIN SMALL LETTER U WITH RING ABOVE - REMAP(0x00FA, 0xFA); // LATIN SMALL LETTER U WITH ACUTE - REMAP(0x0171, 0xFB); // LATIN SMALL LETTER U WITH DOUBLE ACUTE - REMAP(0x00FC, 0xFC); // LATIN SMALL LETTER U WITH DIAERESIS - REMAP(0x00FD, 0xFD); // LATIN SMALL LETTER Y WITH ACUTE - REMAP(0x0163, 0xFE); // LATIN SMALL LETTER T WITH CEDILLA - REMAP(0x02D9, 0xFF); // DOT ABOVE + REMAP(0x0111, 0xF0); // LATIN SMALL LETTER D WITH STROKE + REMAP(0x0144, 0xF1); // LATIN SMALL LETTER N WITH ACUTE + REMAP(0x0148, 0xF2); // LATIN SMALL LETTER N WITH CARON + REMAP(0x00F3, 0xF3); // LATIN SMALL LETTER O WITH ACUTE + REMAP(0x00F4, 0xF4); // LATIN SMALL LETTER O WITH CIRCUMFLEX + REMAP(0x0151, 0xF5); // LATIN SMALL LETTER O WITH DOUBLE ACUTE + REMAP(0x00F6, 0xF6); // LATIN SMALL LETTER O WITH DIAERESIS + REMAP(0x00F7, 0xF7); // DIVISION SIGN + REMAP(0x0159, 0xF8); // LATIN SMALL LETTER R WITH CARON + REMAP(0x016F, 0xF9); // LATIN SMALL LETTER U WITH RING ABOVE + REMAP(0x00FA, 0xFA); // LATIN SMALL LETTER U WITH ACUTE + REMAP(0x0171, 0xFB); // LATIN SMALL LETTER U WITH DOUBLE ACUTE + REMAP(0x00FC, 0xFC); // LATIN SMALL LETTER U WITH DIAERESIS + REMAP(0x00FD, 0xFD); // LATIN SMALL LETTER Y WITH ACUTE + REMAP(0x0163, 0xFE); // LATIN SMALL LETTER T WITH CEDILLA + REMAP(0x02D9, 0xFF); // DOT ABOVE + } } - } - // Latin - Cyrillic - // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1251.TXT - else if (encoding == WINDOWS_1251) { - // 1-Byte chars: no remapping - if (utf8.length() == 1) - return utf8.at(0); + // Latin - Cyrillic + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1251.TXT + else if (encoding == WINDOWS_1251) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); - // Multi-byte chars: - switch (toUtf32(utf8)) { - REMAP(0x0402, 0x80); // CYRILLIC CAPITAL LETTER DJE - REMAP(0x0403, 0x81); // CYRILLIC CAPITAL LETTER GJE - REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK - REMAP(0x0453, 0x83); // CYRILLIC SMALL LETTER GJE - REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK - REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS - REMAP(0x2020, 0x86); // DAGGER - REMAP(0x2021, 0x87); // DOUBLE DAGGER - REMAP(0x20AC, 0x88); // EURO SIGN - REMAP(0x2030, 0x89); // PER MILLE SIGN - REMAP(0x0409, 0x8A); // CYRILLIC CAPITAL LETTER LJE - REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - REMAP(0x040A, 0x8C); // CYRILLIC CAPITAL LETTER NJE - REMAP(0x040C, 0x8D); // CYRILLIC CAPITAL LETTER KJE - REMAP(0x040B, 0x8E); // CYRILLIC CAPITAL LETTER TSHE - REMAP(0x040F, 0x8F); // CYRILLIC CAPITAL LETTER DZHE + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x0402, 0x80); // CYRILLIC CAPITAL LETTER DJE + REMAP(0x0403, 0x81); // CYRILLIC CAPITAL LETTER GJE + REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK + REMAP(0x0453, 0x83); // CYRILLIC SMALL LETTER GJE + REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86); // DAGGER + REMAP(0x2021, 0x87); // DOUBLE DAGGER + REMAP(0x20AC, 0x88); // EURO SIGN + REMAP(0x2030, 0x89); // PER MILLE SIGN + REMAP(0x0409, 0x8A); // CYRILLIC CAPITAL LETTER LJE + REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x040A, 0x8C); // CYRILLIC CAPITAL LETTER NJE + REMAP(0x040C, 0x8D); // CYRILLIC CAPITAL LETTER KJE + REMAP(0x040B, 0x8E); // CYRILLIC CAPITAL LETTER TSHE + REMAP(0x040F, 0x8F); // CYRILLIC CAPITAL LETTER DZHE - REMAP(0x0452, 0x90); // CYRILLIC SMALL LETTER DJE - REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK - REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK - REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK - REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK - REMAP(0x2022, 0x95); // BULLET - REMAP(0x2013, 0x96); // EN DASH - REMAP(0x2014, 0x97); // EM DASH - REMAP(0x2122, 0x99); // TRADE MARK SIGN - REMAP(0x0459, 0x9A); // CYRILLIC SMALL LETTER LJE - REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - REMAP(0x045A, 0x9C); // CYRILLIC SMALL LETTER NJE - REMAP(0x045C, 0x9D); // CYRILLIC SMALL LETTER KJE - REMAP(0x045B, 0x9E); // CYRILLIC SMALL LETTER TSHE - REMAP(0x045F, 0x9F); // CYRILLIC SMALL LETTER DZHE + REMAP(0x0452, 0x90); // CYRILLIC SMALL LETTER DJE + REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95); // BULLET + REMAP(0x2013, 0x96); // EN DASH + REMAP(0x2014, 0x97); // EM DASH + REMAP(0x2122, 0x99); // TRADE MARK SIGN + REMAP(0x0459, 0x9A); // CYRILLIC SMALL LETTER LJE + REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x045A, 0x9C); // CYRILLIC SMALL LETTER NJE + REMAP(0x045C, 0x9D); // CYRILLIC SMALL LETTER KJE + REMAP(0x045B, 0x9E); // CYRILLIC SMALL LETTER TSHE + REMAP(0x045F, 0x9F); // CYRILLIC SMALL LETTER DZHE - REMAP(0x00A0, 0xA0); // NO-BREAK SPACE - REMAP(0x040E, 0xA1); // CYRILLIC CAPITAL LETTER SHORT U - REMAP(0x045E, 0xA2); // CYRILLIC SMALL LETTER SHORT U - REMAP(0x0408, 0xA3); // CYRILLIC CAPITAL LETTER JE - REMAP(0x00A4, 0xA4); // CURRENCY SIGN - REMAP(0x0490, 0xA5); // CYRILLIC CAPITAL LETTER GHE WITH UPTURN - REMAP(0x00A6, 0xA6); // BROKEN BAR - REMAP(0x00A7, 0xA7); // SECTION SIGN - REMAP(0x0401, 0xA8); // CYRILLIC CAPITAL LETTER IO - REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN - REMAP(0x0404, 0xAA); // CYRILLIC CAPITAL LETTER UKRAINIAN IE - REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00AC, 0xAC); // NOT SIGN - REMAP(0x00AD, 0xAD); // SOFT HYPHEN - REMAP(0x00AE, 0xAE); // REGISTERED SIGN - REMAP(0x0407, 0xAF); // CYRILLIC CAPITAL LETTER YI + REMAP(0x00A0, 0xA0); // NO-BREAK SPACE + REMAP(0x040E, 0xA1); // CYRILLIC CAPITAL LETTER SHORT U + REMAP(0x045E, 0xA2); // CYRILLIC SMALL LETTER SHORT U + REMAP(0x0408, 0xA3); // CYRILLIC CAPITAL LETTER JE + REMAP(0x00A4, 0xA4); // CURRENCY SIGN + REMAP(0x0490, 0xA5); // CYRILLIC CAPITAL LETTER GHE WITH UPTURN + REMAP(0x00A6, 0xA6); // BROKEN BAR + REMAP(0x00A7, 0xA7); // SECTION SIGN + REMAP(0x0401, 0xA8); // CYRILLIC CAPITAL LETTER IO + REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN + REMAP(0x0404, 0xAA); // CYRILLIC CAPITAL LETTER UKRAINIAN IE + REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC); // NOT SIGN + REMAP(0x00AD, 0xAD); // SOFT HYPHEN + REMAP(0x00AE, 0xAE); // REGISTERED SIGN + REMAP(0x0407, 0xAF); // CYRILLIC CAPITAL LETTER YI - REMAP(0x00B0, 0xB0); // DEGREE SIGN - REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN - REMAP(0x0406, 0xB2); // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I - REMAP(0x0456, 0xB3); // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - REMAP(0x0491, 0xB4); // CYRILLIC SMALL LETTER GHE WITH UPTURN - REMAP(0x00B5, 0xB5); // MICRO SIGN - REMAP(0x00B6, 0xB6); // PILCROW SIGN - REMAP(0x00B7, 0xB7); // MIDDLE DOT - REMAP(0x0451, 0xB8); // CYRILLIC SMALL LETTER IO - REMAP(0x2116, 0xB9); // NUMERO SIGN - REMAP(0x0454, 0xBA); // CYRILLIC SMALL LETTER UKRAINIAN IE - REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x0458, 0xBC); // CYRILLIC SMALL LETTER JE - REMAP(0x0405, 0xBD); // CYRILLIC CAPITAL LETTER DZE - REMAP(0x0455, 0xBE); // CYRILLIC SMALL LETTER DZE - REMAP(0x0457, 0xBF); // CYRILLIC SMALL LETTER YI + REMAP(0x00B0, 0xB0); // DEGREE SIGN + REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN + REMAP(0x0406, 0xB2); // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I + REMAP(0x0456, 0xB3); // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + REMAP(0x0491, 0xB4); // CYRILLIC SMALL LETTER GHE WITH UPTURN + REMAP(0x00B5, 0xB5); // MICRO SIGN + REMAP(0x00B6, 0xB6); // PILCROW SIGN + REMAP(0x00B7, 0xB7); // MIDDLE DOT + REMAP(0x0451, 0xB8); // CYRILLIC SMALL LETTER IO + REMAP(0x2116, 0xB9); // NUMERO SIGN + REMAP(0x0454, 0xBA); // CYRILLIC SMALL LETTER UKRAINIAN IE + REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x0458, 0xBC); // CYRILLIC SMALL LETTER JE + REMAP(0x0405, 0xBD); // CYRILLIC CAPITAL LETTER DZE + REMAP(0x0455, 0xBE); // CYRILLIC SMALL LETTER DZE + REMAP(0x0457, 0xBF); // CYRILLIC SMALL LETTER YI - REMAP(0x0410, 0xC0); // CYRILLIC CAPITAL LETTER A - REMAP(0x0411, 0xC1); // CYRILLIC CAPITAL LETTER BE - REMAP(0x0412, 0xC2); // CYRILLIC CAPITAL LETTER VE - REMAP(0x0413, 0xC3); // CYRILLIC CAPITAL LETTER GHE - REMAP(0x0414, 0xC4); // CYRILLIC CAPITAL LETTER DE - REMAP(0x0415, 0xC5); // CYRILLIC CAPITAL LETTER IE - REMAP(0x0416, 0xC6); // CYRILLIC CAPITAL LETTER ZHE - REMAP(0x0417, 0xC7); // CYRILLIC CAPITAL LETTER ZE - REMAP(0x0418, 0xC8); // CYRILLIC CAPITAL LETTER I - REMAP(0x0419, 0xC9); // CYRILLIC CAPITAL LETTER SHORT I - REMAP(0x041A, 0xCA); // CYRILLIC CAPITAL LETTER KA - REMAP(0x041B, 0xCB); // CYRILLIC CAPITAL LETTER EL - REMAP(0x041C, 0xCC); // CYRILLIC CAPITAL LETTER EM - REMAP(0x041D, 0xCD); // CYRILLIC CAPITAL LETTER EN - REMAP(0x041E, 0xCE); // CYRILLIC CAPITAL LETTER O - REMAP(0x041F, 0xCF); // CYRILLIC CAPITAL LETTER PE + REMAP(0x0410, 0xC0); // CYRILLIC CAPITAL LETTER A + REMAP(0x0411, 0xC1); // CYRILLIC CAPITAL LETTER BE + REMAP(0x0412, 0xC2); // CYRILLIC CAPITAL LETTER VE + REMAP(0x0413, 0xC3); // CYRILLIC CAPITAL LETTER GHE + REMAP(0x0414, 0xC4); // CYRILLIC CAPITAL LETTER DE + REMAP(0x0415, 0xC5); // CYRILLIC CAPITAL LETTER IE + REMAP(0x0416, 0xC6); // CYRILLIC CAPITAL LETTER ZHE + REMAP(0x0417, 0xC7); // CYRILLIC CAPITAL LETTER ZE + REMAP(0x0418, 0xC8); // CYRILLIC CAPITAL LETTER I + REMAP(0x0419, 0xC9); // CYRILLIC CAPITAL LETTER SHORT I + REMAP(0x041A, 0xCA); // CYRILLIC CAPITAL LETTER KA + REMAP(0x041B, 0xCB); // CYRILLIC CAPITAL LETTER EL + REMAP(0x041C, 0xCC); // CYRILLIC CAPITAL LETTER EM + REMAP(0x041D, 0xCD); // CYRILLIC CAPITAL LETTER EN + REMAP(0x041E, 0xCE); // CYRILLIC CAPITAL LETTER O + REMAP(0x041F, 0xCF); // CYRILLIC CAPITAL LETTER PE - REMAP(0x0420, 0xD0); // CYRILLIC CAPITAL LETTER ER - REMAP(0x0421, 0xD1); // CYRILLIC CAPITAL LETTER ES - REMAP(0x0422, 0xD2); // CYRILLIC CAPITAL LETTER TE - REMAP(0x0423, 0xD3); // CYRILLIC CAPITAL LETTER U - REMAP(0x0424, 0xD4); // CYRILLIC CAPITAL LETTER EF - REMAP(0x0425, 0xD5); // CYRILLIC CAPITAL LETTER HA - REMAP(0x0426, 0xD6); // CYRILLIC CAPITAL LETTER TSE - REMAP(0x0427, 0xD7); // CYRILLIC CAPITAL LETTER CHE - REMAP(0x0428, 0xD8); // CYRILLIC CAPITAL LETTER SHA - REMAP(0x0429, 0xD9); // CYRILLIC CAPITAL LETTER SHCHA - REMAP(0x042A, 0xDA); // CYRILLIC CAPITAL LETTER HARD SIGN - REMAP(0x042B, 0xDB); // CYRILLIC CAPITAL LETTER YERU - REMAP(0x042C, 0xDC); // CYRILLIC CAPITAL LETTER SOFT SIGN - REMAP(0x042D, 0xDD); // CYRILLIC CAPITAL LETTER E - REMAP(0x042E, 0xDE); // CYRILLIC CAPITAL LETTER YU - REMAP(0x042F, 0xDF); // CYRILLIC CAPITAL LETTER YA + REMAP(0x0420, 0xD0); // CYRILLIC CAPITAL LETTER ER + REMAP(0x0421, 0xD1); // CYRILLIC CAPITAL LETTER ES + REMAP(0x0422, 0xD2); // CYRILLIC CAPITAL LETTER TE + REMAP(0x0423, 0xD3); // CYRILLIC CAPITAL LETTER U + REMAP(0x0424, 0xD4); // CYRILLIC CAPITAL LETTER EF + REMAP(0x0425, 0xD5); // CYRILLIC CAPITAL LETTER HA + REMAP(0x0426, 0xD6); // CYRILLIC CAPITAL LETTER TSE + REMAP(0x0427, 0xD7); // CYRILLIC CAPITAL LETTER CHE + REMAP(0x0428, 0xD8); // CYRILLIC CAPITAL LETTER SHA + REMAP(0x0429, 0xD9); // CYRILLIC CAPITAL LETTER SHCHA + REMAP(0x042A, 0xDA); // CYRILLIC CAPITAL LETTER HARD SIGN + REMAP(0x042B, 0xDB); // CYRILLIC CAPITAL LETTER YERU + REMAP(0x042C, 0xDC); // CYRILLIC CAPITAL LETTER SOFT SIGN + REMAP(0x042D, 0xDD); // CYRILLIC CAPITAL LETTER E + REMAP(0x042E, 0xDE); // CYRILLIC CAPITAL LETTER YU + REMAP(0x042F, 0xDF); // CYRILLIC CAPITAL LETTER YA - REMAP(0x0430, 0xE0); // CYRILLIC SMALL LETTER A - REMAP(0x0431, 0xE1); // CYRILLIC SMALL LETTER BE - REMAP(0x0432, 0xE2); // CYRILLIC SMALL LETTER VE - REMAP(0x0433, 0xE3); // CYRILLIC SMALL LETTER GHE - REMAP(0x0434, 0xE4); // CYRILLIC SMALL LETTER DE - REMAP(0x0435, 0xE5); // CYRILLIC SMALL LETTER IE - REMAP(0x0436, 0xE6); // CYRILLIC SMALL LETTER ZHE - REMAP(0x0437, 0xE7); // CYRILLIC SMALL LETTER ZE - REMAP(0x0438, 0xE8); // CYRILLIC SMALL LETTER I - REMAP(0x0439, 0xE9); // CYRILLIC SMALL LETTER SHORT I - REMAP(0x043A, 0xEA); // CYRILLIC SMALL LETTER KA - REMAP(0x043B, 0xEB); // CYRILLIC SMALL LETTER EL - REMAP(0x043C, 0xEC); // CYRILLIC SMALL LETTER EM - REMAP(0x043D, 0xED); // CYRILLIC SMALL LETTER EN - REMAP(0x043E, 0xEE); // CYRILLIC SMALL LETTER O - REMAP(0x043F, 0xEF); // CYRILLIC SMALL LETTER PE + REMAP(0x0430, 0xE0); // CYRILLIC SMALL LETTER A + REMAP(0x0431, 0xE1); // CYRILLIC SMALL LETTER BE + REMAP(0x0432, 0xE2); // CYRILLIC SMALL LETTER VE + REMAP(0x0433, 0xE3); // CYRILLIC SMALL LETTER GHE + REMAP(0x0434, 0xE4); // CYRILLIC SMALL LETTER DE + REMAP(0x0435, 0xE5); // CYRILLIC SMALL LETTER IE + REMAP(0x0436, 0xE6); // CYRILLIC SMALL LETTER ZHE + REMAP(0x0437, 0xE7); // CYRILLIC SMALL LETTER ZE + REMAP(0x0438, 0xE8); // CYRILLIC SMALL LETTER I + REMAP(0x0439, 0xE9); // CYRILLIC SMALL LETTER SHORT I + REMAP(0x043A, 0xEA); // CYRILLIC SMALL LETTER KA + REMAP(0x043B, 0xEB); // CYRILLIC SMALL LETTER EL + REMAP(0x043C, 0xEC); // CYRILLIC SMALL LETTER EM + REMAP(0x043D, 0xED); // CYRILLIC SMALL LETTER EN + REMAP(0x043E, 0xEE); // CYRILLIC SMALL LETTER O + REMAP(0x043F, 0xEF); // CYRILLIC SMALL LETTER PE - REMAP(0x0440, 0xF0); // CYRILLIC SMALL LETTER ER - REMAP(0x0441, 0xF1); // CYRILLIC SMALL LETTER ES - REMAP(0x0442, 0xF2); // CYRILLIC SMALL LETTER TE - REMAP(0x0443, 0xF3); // CYRILLIC SMALL LETTER U - REMAP(0x0444, 0xF4); // CYRILLIC SMALL LETTER EF - REMAP(0x0445, 0xF5); // CYRILLIC SMALL LETTER HA - REMAP(0x0446, 0xF6); // CYRILLIC SMALL LETTER TSE - REMAP(0x0447, 0xF7); // CYRILLIC SMALL LETTER CHE - REMAP(0x0448, 0xF8); // CYRILLIC SMALL LETTER SHA - REMAP(0x0449, 0xF9); // CYRILLIC SMALL LETTER SHCHA - REMAP(0x044A, 0xFA); // CYRILLIC SMALL LETTER HARD SIGN - REMAP(0x044B, 0xFB); // CYRILLIC SMALL LETTER YERU - REMAP(0x044C, 0xFC); // CYRILLIC SMALL LETTER SOFT SIGN - REMAP(0x044D, 0xFD); // CYRILLIC SMALL LETTER E - REMAP(0x044E, 0xFE); // CYRILLIC SMALL LETTER YU - REMAP(0x044F, 0xFF); // CYRILLIC SMALL LETTER YA + REMAP(0x0440, 0xF0); // CYRILLIC SMALL LETTER ER + REMAP(0x0441, 0xF1); // CYRILLIC SMALL LETTER ES + REMAP(0x0442, 0xF2); // CYRILLIC SMALL LETTER TE + REMAP(0x0443, 0xF3); // CYRILLIC SMALL LETTER U + REMAP(0x0444, 0xF4); // CYRILLIC SMALL LETTER EF + REMAP(0x0445, 0xF5); // CYRILLIC SMALL LETTER HA + REMAP(0x0446, 0xF6); // CYRILLIC SMALL LETTER TSE + REMAP(0x0447, 0xF7); // CYRILLIC SMALL LETTER CHE + REMAP(0x0448, 0xF8); // CYRILLIC SMALL LETTER SHA + REMAP(0x0449, 0xF9); // CYRILLIC SMALL LETTER SHCHA + REMAP(0x044A, 0xFA); // CYRILLIC SMALL LETTER HARD SIGN + REMAP(0x044B, 0xFB); // CYRILLIC SMALL LETTER YERU + REMAP(0x044C, 0xFC); // CYRILLIC SMALL LETTER SOFT SIGN + REMAP(0x044D, 0xFD); // CYRILLIC SMALL LETTER E + REMAP(0x044E, 0xFE); // CYRILLIC SMALL LETTER YU + REMAP(0x044F, 0xFF); // CYRILLIC SMALL LETTER YA + } } - } - // Latin - Western Europe - // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT - else if (encoding == WINDOWS_1252) { - // 1-Byte chars: no remapping - if (utf8.length() == 1) - return utf8.at(0); + // Latin - Western Europe + // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT + else if (encoding == WINDOWS_1252) { + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); - // Multi-byte chars: - switch (toUtf32(utf8)) { - REMAP(0x20AC, 0x80) // EURO SIGN - REMAP(0x201A, 0x82) // SINGLE LOW-9 QUOTATION MARK - REMAP(0x0192, 0x83) // LATIN SMALL LETTER F WITH HOOK - REMAP(0x201E, 0x84) // DOUBLE LOW-9 QUOTATION MARK - REMAP(0x2026, 0x85) // HORIZONTAL ELLIPSIS - REMAP(0x2020, 0x86) // DAGGER - REMAP(0x2021, 0x87) // DOUBLE DAGGER - REMAP(0x02C6, 0x88) // MODIFIER LETTER CIRCUMFLEX ACCENT - REMAP(0x2030, 0x89) // PER MILLE SIGN - REMAP(0x0160, 0x8A) // LATIN CAPITAL LETTER S WITH CARON - REMAP(0x2039, 0x8B) // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - REMAP(0x0152, 0x8C) // LATIN CAPITAL LIGATURE OE - REMAP(0x017D, 0x8E) // LATIN CAPITAL LETTER Z WITH CARON + // Multi-byte chars: + switch (toUtf32(utf8)) { + REMAP(0x20AC, 0x80) // EURO SIGN + REMAP(0x201A, 0x82) // SINGLE LOW-9 QUOTATION MARK + REMAP(0x0192, 0x83) // LATIN SMALL LETTER F WITH HOOK + REMAP(0x201E, 0x84) // DOUBLE LOW-9 QUOTATION MARK + REMAP(0x2026, 0x85) // HORIZONTAL ELLIPSIS + REMAP(0x2020, 0x86) // DAGGER + REMAP(0x2021, 0x87) // DOUBLE DAGGER + REMAP(0x02C6, 0x88) // MODIFIER LETTER CIRCUMFLEX ACCENT + REMAP(0x2030, 0x89) // PER MILLE SIGN + REMAP(0x0160, 0x8A) // LATIN CAPITAL LETTER S WITH CARON + REMAP(0x2039, 0x8B) // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + REMAP(0x0152, 0x8C) // LATIN CAPITAL LIGATURE OE + REMAP(0x017D, 0x8E) // LATIN CAPITAL LETTER Z WITH CARON - REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK - REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK - REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK - REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK - REMAP(0x2022, 0x95) // BULLET - REMAP(0x2013, 0x96) // EN DASH - REMAP(0x2014, 0x97) // EM DASH - REMAP(0x02DC, 0x98) // SMALL TILDE - REMAP(0x2122, 0x99) // TRADE MARK SIGN - REMAP(0x0161, 0x9A) // LATIN SMALL LETTER S WITH CARON - REMAP(0x203A, 0x9B) // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - REMAP(0x0153, 0x9C) // LATIN SMALL LIGATURE OE - REMAP(0x017E, 0x9E) // LATIN SMALL LETTER Z WITH CARON - REMAP(0x0178, 0x9F) // LATIN CAPITAL LETTER Y WITH DIAERESIS + REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95) // BULLET + REMAP(0x2013, 0x96) // EN DASH + REMAP(0x2014, 0x97) // EM DASH + REMAP(0x02DC, 0x98) // SMALL TILDE + REMAP(0x2122, 0x99) // TRADE MARK SIGN + REMAP(0x0161, 0x9A) // LATIN SMALL LETTER S WITH CARON + REMAP(0x203A, 0x9B) // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + REMAP(0x0153, 0x9C) // LATIN SMALL LIGATURE OE + REMAP(0x017E, 0x9E) // LATIN SMALL LETTER Z WITH CARON + REMAP(0x0178, 0x9F) // LATIN CAPITAL LETTER Y WITH DIAERESIS - REMAP(0x00A0, 0xA0) // NO-BREAK SPACE - REMAP(0x00A1, 0xA1) // INVERTED EXCLAMATION MARK - REMAP(0x00A2, 0xA2) // CENT SIGN - REMAP(0x00A3, 0xA3) // POUND SIGN - REMAP(0x00A4, 0xA4) // CURRENCY SIGN - REMAP(0x00A5, 0xA5) // YEN SIGN - REMAP(0x00A6, 0xA6) // BROKEN BAR - REMAP(0x00A7, 0xA7) // SECTION SIGN - REMAP(0x00A8, 0xA8) // DIAERESIS - REMAP(0x00A9, 0xA9) // COPYRIGHT SIGN - REMAP(0x00AA, 0xAA) // FEMININE ORDINAL INDICATOR - REMAP(0x00AB, 0xAB) // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00AC, 0xAC) // NOT SIGN - REMAP(0x00AD, 0xAD) // SOFT HYPHEN - REMAP(0x00AE, 0xAE) // REGISTERED SIGN - REMAP(0x00AF, 0xAF) // MACRON + REMAP(0x00A0, 0xA0) // NO-BREAK SPACE + REMAP(0x00A1, 0xA1) // INVERTED EXCLAMATION MARK + REMAP(0x00A2, 0xA2) // CENT SIGN + REMAP(0x00A3, 0xA3) // POUND SIGN + REMAP(0x00A4, 0xA4) // CURRENCY SIGN + REMAP(0x00A5, 0xA5) // YEN SIGN + REMAP(0x00A6, 0xA6) // BROKEN BAR + REMAP(0x00A7, 0xA7) // SECTION SIGN + REMAP(0x00A8, 0xA8) // DIAERESIS + REMAP(0x00A9, 0xA9) // COPYRIGHT SIGN + REMAP(0x00AA, 0xAA) // FEMININE ORDINAL INDICATOR + REMAP(0x00AB, 0xAB) // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00AC, 0xAC) // NOT SIGN + REMAP(0x00AD, 0xAD) // SOFT HYPHEN + REMAP(0x00AE, 0xAE) // REGISTERED SIGN + REMAP(0x00AF, 0xAF) // MACRON - REMAP(0x00B0, 0xB0) // DEGREE SIGN - REMAP(0x00B1, 0xB1) // PLUS-MINUS SIGN - REMAP(0x00B2, 0xB2) // SUPERSCRIPT TWO - REMAP(0x00B3, 0xB3) // SUPERSCRIPT THREE - REMAP(0x00B4, 0xB4) // ACUTE ACCENT - REMAP(0x00B5, 0xB5) // MICRO SIGN - REMAP(0x00B6, 0xB6) // PILCROW SIGN - REMAP(0x00B7, 0xB7) // MIDDLE DOT - REMAP(0x00B8, 0xB8) // CEDILLA - REMAP(0x00B9, 0xB9) // SUPERSCRIPT ONE - REMAP(0x00BA, 0xBA) // MASCULINE ORDINAL INDICATOR - REMAP(0x00BB, 0xBB) // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - REMAP(0x00BC, 0xBC) // VULGAR FRACTION ONE QUARTER - REMAP(0x00BD, 0xBD) // VULGAR FRACTION ONE HALF - REMAP(0x00BE, 0xBE) // VULGAR FRACTION THREE QUARTERS - REMAP(0x00BF, 0xBF) // INVERTED QUESTION MARK + REMAP(0x00B0, 0xB0) // DEGREE SIGN + REMAP(0x00B1, 0xB1) // PLUS-MINUS SIGN + REMAP(0x00B2, 0xB2) // SUPERSCRIPT TWO + REMAP(0x00B3, 0xB3) // SUPERSCRIPT THREE + REMAP(0x00B4, 0xB4) // ACUTE ACCENT + REMAP(0x00B5, 0xB5) // MICRO SIGN + REMAP(0x00B6, 0xB6) // PILCROW SIGN + REMAP(0x00B7, 0xB7) // MIDDLE DOT + REMAP(0x00B8, 0xB8) // CEDILLA + REMAP(0x00B9, 0xB9) // SUPERSCRIPT ONE + REMAP(0x00BA, 0xBA) // MASCULINE ORDINAL INDICATOR + REMAP(0x00BB, 0xBB) // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + REMAP(0x00BC, 0xBC) // VULGAR FRACTION ONE QUARTER + REMAP(0x00BD, 0xBD) // VULGAR FRACTION ONE HALF + REMAP(0x00BE, 0xBE) // VULGAR FRACTION THREE QUARTERS + REMAP(0x00BF, 0xBF) // INVERTED QUESTION MARK - REMAP(0x00C0, 0xC0) // LATIN CAPITAL LETTER A WITH GRAVE - REMAP(0x00C1, 0xC1) // LATIN CAPITAL LETTER A WITH ACUTE - REMAP(0x00C2, 0xC2) // LATIN CAPITAL LETTER A WITH CIRCUMFLEX - REMAP(0x00C3, 0xC3) // LATIN CAPITAL LETTER A WITH TILDE - REMAP(0x00C4, 0xC4) // LATIN CAPITAL LETTER A WITH DIAERESIS - REMAP(0x00C5, 0xC5) // LATIN CAPITAL LETTER A WITH RING ABOVE - REMAP(0x00C6, 0xC6) // LATIN CAPITAL LETTER AE - REMAP(0x00C7, 0xC7) // LATIN CAPITAL LETTER C WITH CEDILLA - REMAP(0x00C8, 0xC8) // LATIN CAPITAL LETTER E WITH GRAVE - REMAP(0x00C9, 0xC9) // LATIN CAPITAL LETTER E WITH ACUTE - REMAP(0x00CA, 0xCA) // LATIN CAPITAL LETTER E WITH CIRCUMFLEX - REMAP(0x00CB, 0xCB) // LATIN CAPITAL LETTER E WITH DIAERESIS - REMAP(0x00CC, 0xCC) // LATIN CAPITAL LETTER I WITH GRAVE - REMAP(0x00CD, 0xCD) // LATIN CAPITAL LETTER I WITH ACUTE - REMAP(0x00CE, 0xCE) // LATIN CAPITAL LETTER I WITH CIRCUMFLEX - REMAP(0x00CF, 0xCF) // LATIN CAPITAL LETTER I WITH DIAERESIS + REMAP(0x00C0, 0xC0) // LATIN CAPITAL LETTER A WITH GRAVE + REMAP(0x00C1, 0xC1) // LATIN CAPITAL LETTER A WITH ACUTE + REMAP(0x00C2, 0xC2) // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + REMAP(0x00C3, 0xC3) // LATIN CAPITAL LETTER A WITH TILDE + REMAP(0x00C4, 0xC4) // LATIN CAPITAL LETTER A WITH DIAERESIS + REMAP(0x00C5, 0xC5) // LATIN CAPITAL LETTER A WITH RING ABOVE + REMAP(0x00C6, 0xC6) // LATIN CAPITAL LETTER AE + REMAP(0x00C7, 0xC7) // LATIN CAPITAL LETTER C WITH CEDILLA + REMAP(0x00C8, 0xC8) // LATIN CAPITAL LETTER E WITH GRAVE + REMAP(0x00C9, 0xC9) // LATIN CAPITAL LETTER E WITH ACUTE + REMAP(0x00CA, 0xCA) // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + REMAP(0x00CB, 0xCB) // LATIN CAPITAL LETTER E WITH DIAERESIS + REMAP(0x00CC, 0xCC) // LATIN CAPITAL LETTER I WITH GRAVE + REMAP(0x00CD, 0xCD) // LATIN CAPITAL LETTER I WITH ACUTE + REMAP(0x00CE, 0xCE) // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + REMAP(0x00CF, 0xCF) // LATIN CAPITAL LETTER I WITH DIAERESIS - REMAP(0x00D0, 0xD0) // LATIN CAPITAL LETTER ETH - REMAP(0x00D1, 0xD1) // LATIN CAPITAL LETTER N WITH TILDE - REMAP(0x00D2, 0xD2) // LATIN CAPITAL LETTER O WITH GRAVE - REMAP(0x00D3, 0xD3) // LATIN CAPITAL LETTER O WITH ACUTE - REMAP(0x00D4, 0xD4) // LATIN CAPITAL LETTER O WITH CIRCUMFLEX - REMAP(0x00D5, 0xD5) // LATIN CAPITAL LETTER O WITH TILDE - REMAP(0x00D6, 0xD6) // LATIN CAPITAL LETTER O WITH DIAERESIS - REMAP(0x00D7, 0xD7) // MULTIPLICATION SIGN - REMAP(0x00D8, 0xD8) // LATIN CAPITAL LETTER O WITH STROKE - REMAP(0x00D9, 0xD9) // LATIN CAPITAL LETTER U WITH GRAVE - REMAP(0x00DA, 0xDA) // LATIN CAPITAL LETTER U WITH ACUTE - REMAP(0x00DB, 0xDB) // LATIN CAPITAL LETTER U WITH CIRCUMFLEX - REMAP(0x00DC, 0xDC) // LATIN CAPITAL LETTER U WITH DIAERESIS - REMAP(0x00DD, 0xDD) // LATIN CAPITAL LETTER Y WITH ACUTE - REMAP(0x00DE, 0xDE) // LATIN CAPITAL LETTER THORN - REMAP(0x00DF, 0xDF) // LATIN SMALL LETTER SHARP S + REMAP(0x00D0, 0xD0) // LATIN CAPITAL LETTER ETH + REMAP(0x00D1, 0xD1) // LATIN CAPITAL LETTER N WITH TILDE + REMAP(0x00D2, 0xD2) // LATIN CAPITAL LETTER O WITH GRAVE + REMAP(0x00D3, 0xD3) // LATIN CAPITAL LETTER O WITH ACUTE + REMAP(0x00D4, 0xD4) // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + REMAP(0x00D5, 0xD5) // LATIN CAPITAL LETTER O WITH TILDE + REMAP(0x00D6, 0xD6) // LATIN CAPITAL LETTER O WITH DIAERESIS + REMAP(0x00D7, 0xD7) // MULTIPLICATION SIGN + REMAP(0x00D8, 0xD8) // LATIN CAPITAL LETTER O WITH STROKE + REMAP(0x00D9, 0xD9) // LATIN CAPITAL LETTER U WITH GRAVE + REMAP(0x00DA, 0xDA) // LATIN CAPITAL LETTER U WITH ACUTE + REMAP(0x00DB, 0xDB) // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + REMAP(0x00DC, 0xDC) // LATIN CAPITAL LETTER U WITH DIAERESIS + REMAP(0x00DD, 0xDD) // LATIN CAPITAL LETTER Y WITH ACUTE + REMAP(0x00DE, 0xDE) // LATIN CAPITAL LETTER THORN + REMAP(0x00DF, 0xDF) // LATIN SMALL LETTER SHARP S - REMAP(0x00E0, 0xE0) // LATIN SMALL LETTER A WITH GRAVE - REMAP(0x00E1, 0xE1) // LATIN SMALL LETTER A WITH ACUTE - REMAP(0x00E2, 0xE2) // LATIN SMALL LETTER A WITH CIRCUMFLEX - REMAP(0x00E3, 0xE3) // LATIN SMALL LETTER A WITH TILDE - REMAP(0x00E4, 0xE4) // LATIN SMALL LETTER A WITH DIAERESIS - REMAP(0x00E5, 0xE5) // LATIN SMALL LETTER A WITH RING ABOVE - REMAP(0x00E6, 0xE6) // LATIN SMALL LETTER AE - REMAP(0x00E7, 0xE7) // LATIN SMALL LETTER C WITH CEDILLA - REMAP(0x00E8, 0xE8) // LATIN SMALL LETTER E WITH GRAVE - REMAP(0x00E9, 0xE9) // LATIN SMALL LETTER E WITH ACUTE - REMAP(0x00EA, 0xEA) // LATIN SMALL LETTER E WITH CIRCUMFLEX - REMAP(0x00EB, 0xEB) // LATIN SMALL LETTER E WITH DIAERESIS - REMAP(0x00EC, 0xEC) // LATIN SMALL LETTER I WITH GRAVE - REMAP(0x00ED, 0xED) // LATIN SMALL LETTER I WITH ACUTE - REMAP(0x00EE, 0xEE) // LATIN SMALL LETTER I WITH CIRCUMFLEX - REMAP(0x00EF, 0xEF) // LATIN SMALL LETTER I WITH DIAERESIS + REMAP(0x00E0, 0xE0) // LATIN SMALL LETTER A WITH GRAVE + REMAP(0x00E1, 0xE1) // LATIN SMALL LETTER A WITH ACUTE + REMAP(0x00E2, 0xE2) // LATIN SMALL LETTER A WITH CIRCUMFLEX + REMAP(0x00E3, 0xE3) // LATIN SMALL LETTER A WITH TILDE + REMAP(0x00E4, 0xE4) // LATIN SMALL LETTER A WITH DIAERESIS + REMAP(0x00E5, 0xE5) // LATIN SMALL LETTER A WITH RING ABOVE + REMAP(0x00E6, 0xE6) // LATIN SMALL LETTER AE + REMAP(0x00E7, 0xE7) // LATIN SMALL LETTER C WITH CEDILLA + REMAP(0x00E8, 0xE8) // LATIN SMALL LETTER E WITH GRAVE + REMAP(0x00E9, 0xE9) // LATIN SMALL LETTER E WITH ACUTE + REMAP(0x00EA, 0xEA) // LATIN SMALL LETTER E WITH CIRCUMFLEX + REMAP(0x00EB, 0xEB) // LATIN SMALL LETTER E WITH DIAERESIS + REMAP(0x00EC, 0xEC) // LATIN SMALL LETTER I WITH GRAVE + REMAP(0x00ED, 0xED) // LATIN SMALL LETTER I WITH ACUTE + REMAP(0x00EE, 0xEE) // LATIN SMALL LETTER I WITH CIRCUMFLEX + REMAP(0x00EF, 0xEF) // LATIN SMALL LETTER I WITH DIAERESIS - REMAP(0x00F0, 0xF0) // LATIN SMALL LETTER ETH - REMAP(0x00F1, 0xF1) // LATIN SMALL LETTER N WITH TILDE - REMAP(0x00F2, 0xF2) // LATIN SMALL LETTER O WITH GRAVE - REMAP(0x00F3, 0xF3) // LATIN SMALL LETTER O WITH ACUTE - REMAP(0x00F4, 0xF4) // LATIN SMALL LETTER O WITH CIRCUMFLEX - REMAP(0x00F5, 0xF5) // LATIN SMALL LETTER O WITH TILDE - REMAP(0x00F6, 0xF6) // LATIN SMALL LETTER O WITH DIAERESIS - REMAP(0x00F7, 0xF7) // DIVISION SIGN - REMAP(0x00F8, 0xF8) // LATIN SMALL LETTER O WITH STROKE - REMAP(0x00F9, 0xF9) // LATIN SMALL LETTER U WITH GRAVE - REMAP(0x00FA, 0xFA) // LATIN SMALL LETTER U WITH ACUTE - REMAP(0x00FB, 0xFB) // LATIN SMALL LETTER U WITH CIRCUMFLEX - REMAP(0x00FC, 0xFC) // LATIN SMALL LETTER U WITH DIAERESIS - REMAP(0x00FD, 0xFD) // LATIN SMALL LETTER Y WITH ACUTE - REMAP(0x00FE, 0xFE) // LATIN SMALL LETTER THORN - REMAP(0x00FF, 0xFF) // LATIN SMALL LETTER Y WITH DIAERESIS + REMAP(0x00F0, 0xF0) // LATIN SMALL LETTER ETH + REMAP(0x00F1, 0xF1) // LATIN SMALL LETTER N WITH TILDE + REMAP(0x00F2, 0xF2) // LATIN SMALL LETTER O WITH GRAVE + REMAP(0x00F3, 0xF3) // LATIN SMALL LETTER O WITH ACUTE + REMAP(0x00F4, 0xF4) // LATIN SMALL LETTER O WITH CIRCUMFLEX + REMAP(0x00F5, 0xF5) // LATIN SMALL LETTER O WITH TILDE + REMAP(0x00F6, 0xF6) // LATIN SMALL LETTER O WITH DIAERESIS + REMAP(0x00F7, 0xF7) // DIVISION SIGN + REMAP(0x00F8, 0xF8) // LATIN SMALL LETTER O WITH STROKE + REMAP(0x00F9, 0xF9) // LATIN SMALL LETTER U WITH GRAVE + REMAP(0x00FA, 0xFA) // LATIN SMALL LETTER U WITH ACUTE + REMAP(0x00FB, 0xFB) // LATIN SMALL LETTER U WITH CIRCUMFLEX + REMAP(0x00FC, 0xFC) // LATIN SMALL LETTER U WITH DIAERESIS + REMAP(0x00FD, 0xFD) // LATIN SMALL LETTER Y WITH ACUTE + REMAP(0x00FE, 0xFE) // LATIN SMALL LETTER THORN + REMAP(0x00FF, 0xFF) // LATIN SMALL LETTER Y WITH DIAERESIS + } } - } - else /*ASCII or Unhandled*/ { - if (utf8.length() == 1) - return utf8.at(0); - } + else /*ASCII or Unhandled*/ { + if (utf8.length() == 1) + return utf8.at(0); + } - // All single-byte (ASCII) characters should have been handled by now - // Only unhandled multi-byte UTF8 characters should remain - assert(utf8.length() > 1); + // All single-byte (ASCII) characters should have been handled by now + // Only unhandled multi-byte UTF8 characters should remain + assert(utf8.length() > 1); - // Parse emoji - // Strip emoji modifiers - switch (toUtf32(utf8)) { - REMAP(0x1F44D, 0x01) // 👍 Thumbs Up - REMAP(0x1F44E, 0x02) // 👎 Thumbs Down + // Parse emoji + // Strip emoji modifiers + switch (toUtf32(utf8)) { + REMAP(0x1F44D, 0x01) // 👍 Thumbs Up + REMAP(0x1F44E, 0x02) // 👎 Thumbs Down - REMAP(0x1F60A, 0x03) // 😊 Smiling Face with Smiling Eyes - REMAP(0x1F642, 0x03) // 🙂 Slightly Smiling Face - REMAP(0x1F601, 0x03) // 😁 Grinning Face with Smiling Eye + REMAP(0x1F60A, 0x03) // 😊 Smiling Face with Smiling Eyes + REMAP(0x1F642, 0x03) // 🙂 Slightly Smiling Face + REMAP(0x1F601, 0x03) // 😁 Grinning Face with Smiling Eye - REMAP(0x1F602, 0x04) // 😂 Face with Tears of Joy - REMAP(0x1F923, 0x04) // 🤣 Rolling on the Floor Laughing - REMAP(0x1F606, 0x04) // 😆 Smiling with Open Mouth and Closed Eyes + REMAP(0x1F602, 0x04) // 😂 Face with Tears of Joy + REMAP(0x1F923, 0x04) // 🤣 Rolling on the Floor Laughing + REMAP(0x1F606, 0x04) // 😆 Smiling with Open Mouth and Closed Eyes - REMAP(0x1F44B, 0x05) // 👋 Waving Hand + REMAP(0x1F44B, 0x05) // 👋 Waving Hand - REMAP(0x02600, 0x06) // ☀ Sun - REMAP(0x1F31E, 0x06) // 🌞 Sun with Face + REMAP(0x02600, 0x06) // ☀ Sun + REMAP(0x1F31E, 0x06) // 🌞 Sun with Face - // 0x07 - Bell character (unused) - REMAP(0x1F327, 0x08) // 🌧️ Cloud with Rain + // 0x07 - Bell character (unused) + REMAP(0x1F327, 0x08) // 🌧️ Cloud with Rain - REMAP(0x02601, 0x09) // ☁️ Cloud - REMAP(0x1F32B, 0x09) // Fog + REMAP(0x02601, 0x09) // ☁️ Cloud + REMAP(0x1F32B, 0x09) // Fog - REMAP(0x1F9E1, 0x0B) // 🧡 Orange Heart - REMAP(0x02763, 0x0B) // ❣ Heart Exclamation - REMAP(0x02764, 0x0B) // ❤ Heart - REMAP(0x1F495, 0x0B) // 💕 Two Hearts - REMAP(0x1F496, 0x0B) // 💖 Sparkling Heart - REMAP(0x1F497, 0x0B) // 💗 Growing Heart - REMAP(0x1F498, 0x0B) // 💘 Heart with Arrow + REMAP(0x1F9E1, 0x0B) // 🧡 Orange Heart + REMAP(0x02763, 0x0B) // ❣ Heart Exclamation + REMAP(0x02764, 0x0B) // ❤ Heart + REMAP(0x1F495, 0x0B) // 💕 Two Hearts + REMAP(0x1F496, 0x0B) // 💖 Sparkling Heart + REMAP(0x1F497, 0x0B) // 💗 Growing Heart + REMAP(0x1F498, 0x0B) // 💘 Heart with Arrow - REMAP(0x1F4A9, 0x0C) // 💩 Pile of Poo - // 0x0D - Carriage return (unused) - REMAP(0x1F514, 0x0E) // 🔔 Bell + REMAP(0x1F4A9, 0x0C) // 💩 Pile of Poo + // 0x0D - Carriage return (unused) + REMAP(0x1F514, 0x0E) // 🔔 Bell - REMAP(0x1F62D, 0x0F) // 😭 Loudly Crying Face - REMAP(0x1F622, 0x0F) // 😢 Crying Face + REMAP(0x1F62D, 0x0F) // 😭 Loudly Crying Face + REMAP(0x1F622, 0x0F) // 😢 Crying Face - REMAP(0x1F64F, 0x10) // 🙏 Person with Folded Hands - REMAP(0x1F618, 0x11) // 😘 Face Throwing a Kiss - REMAP(0x1F389, 0x12) // 🎉 Party Popper + REMAP(0x1F64F, 0x10) // 🙏 Person with Folded Hands + REMAP(0x1F618, 0x11) // 😘 Face Throwing a Kiss + REMAP(0x1F389, 0x12) // 🎉 Party Popper - REMAP(0x1F600, 0x13) // 😀 Grinning Face - REMAP(0x1F603, 0x13) // 😃 Smiling Face with Open Mouth - REMAP(0x1F604, 0x13) // 😄 Smiling Face with Open Mouth and Smiling Eyes + REMAP(0x1F600, 0x13) // 😀 Grinning Face + REMAP(0x1F603, 0x13) // 😃 Smiling Face with Open Mouth + REMAP(0x1F604, 0x13) // 😄 Smiling Face with Open Mouth and Smiling Eyes - REMAP(0x1F97A, 0x14) // 🥺 Face with Pleading Eyes - REMAP(0x1F605, 0x15) // 😅 Smiling with Sweat - REMAP(0x1F525, 0x16) // 🔥 Fire - REMAP(0x1F926, 0x17) // 🤦 Face Palm - REMAP(0x1F937, 0x18) // 🤷 Shrug - REMAP(0x1F644, 0x19) // 🙄 Face with Rolling Eyes - // 0x1A Substitution (unused) - REMAP(0x1F917, 0x1B) // 🤗 Hugging Face + REMAP(0x1F97A, 0x14) // 🥺 Face with Pleading Eyes + REMAP(0x1F605, 0x15) // 😅 Smiling with Sweat + REMAP(0x1F525, 0x16) // 🔥 Fire + REMAP(0x1F926, 0x17) // 🤦 Face Palm + REMAP(0x1F937, 0x18) // 🤷 Shrug + REMAP(0x1F644, 0x19) // 🙄 Face with Rolling Eyes + // 0x1A Substitution (unused) + REMAP(0x1F917, 0x1B) // 🤗 Hugging Face - REMAP(0x1F609, 0x1C) // 😉 Winking Face - REMAP(0x1F61C, 0x1C) // 😜 Face with Stuck-Out Tongue and Winking Eye - REMAP(0x1F60F, 0x1C) // 😏 Smirking Face + REMAP(0x1F609, 0x1C) // 😉 Winking Face + REMAP(0x1F61C, 0x1C) // 😜 Face with Stuck-Out Tongue and Winking Eye + REMAP(0x1F60F, 0x1C) // 😏 Smirking Face - REMAP(0x1F914, 0x1D) // 🤔 Thinking Face - REMAP(0x1FAE1, 0x1E) // 🫡 Saluting Face - REMAP(0x1F44C, 0x1F) // 👌 OK Hand Sign + REMAP(0x1F914, 0x1D) // 🤔 Thinking Face + REMAP(0x1FAE1, 0x1E) // 🫡 Saluting Face + REMAP(0x1F44C, 0x1F) // 👌 OK Hand Sign - REMAP(0x02755, '!') // ❕ - REMAP(0x02757, '!') // ❗ - REMAP(0x0203C, '!') // ‼ - REMAP(0x02753, '?') // ❓ - REMAP(0x02754, '?') // ❔ - REMAP(0x02049, '?') // ⁉ + REMAP(0x02755, '!') // ❕ + REMAP(0x02757, '!') // ❗ + REMAP(0x0203C, '!') // ‼ + REMAP(0x02753, '?') // ❓ + REMAP(0x02754, '?') // ❔ + REMAP(0x02049, '?') // ⁉ - // Modifiers (deleted) - REMAP(0x02640, 0x7F) // Gender - REMAP(0x02642, 0x7F) - REMAP(0x1F3FB, 0x7F) // Skin Tones - REMAP(0x1F3FC, 0x7F) - REMAP(0x1F3FD, 0x7F) - REMAP(0x1F3FE, 0x7F) - REMAP(0x1F3FF, 0x7F) - REMAP(0x0FE00, 0x7F) // Variation Selectors - REMAP(0x0FE01, 0x7F) - REMAP(0x0FE02, 0x7F) - REMAP(0x0FE03, 0x7F) - REMAP(0x0FE04, 0x7F) - REMAP(0x0FE05, 0x7F) - REMAP(0x0FE06, 0x7F) - REMAP(0x0FE07, 0x7F) - REMAP(0x0FE08, 0x7F) - REMAP(0x0FE09, 0x7F) - REMAP(0x0FE0A, 0x7F) - REMAP(0x0FE0B, 0x7F) - REMAP(0x0FE0C, 0x7F) - REMAP(0x0FE0D, 0x7F) - REMAP(0x0FE0E, 0x7F) - REMAP(0x0FE0F, 0x7F) - REMAP(0x0200D, 0x7F) // Zero Width Joiner - } + // Modifiers (deleted) + REMAP(0x02640, 0x7F) // Gender + REMAP(0x02642, 0x7F) + REMAP(0x1F3FB, 0x7F) // Skin Tones + REMAP(0x1F3FC, 0x7F) + REMAP(0x1F3FD, 0x7F) + REMAP(0x1F3FE, 0x7F) + REMAP(0x1F3FF, 0x7F) + REMAP(0x0FE00, 0x7F) // Variation Selectors + REMAP(0x0FE01, 0x7F) + REMAP(0x0FE02, 0x7F) + REMAP(0x0FE03, 0x7F) + REMAP(0x0FE04, 0x7F) + REMAP(0x0FE05, 0x7F) + REMAP(0x0FE06, 0x7F) + REMAP(0x0FE07, 0x7F) + REMAP(0x0FE08, 0x7F) + REMAP(0x0FE09, 0x7F) + REMAP(0x0FE0A, 0x7F) + REMAP(0x0FE0B, 0x7F) + REMAP(0x0FE0C, 0x7F) + REMAP(0x0FE0D, 0x7F) + REMAP(0x0FE0E, 0x7F) + REMAP(0x0FE0F, 0x7F) + REMAP(0x0200D, 0x7F) // Zero Width Joiner + } - // If not handled, return SUB - return '\x1A'; + // If not handled, return SUB + return '\x1A'; // Sweep up the syntactic sugar // Don't want ants in the house diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 560608601..e1fe37974 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -14,40 +14,42 @@ #include // GFXRoot drawing lib -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ // An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD -class AppletFont { -public: - enum Encoding { - ASCII, - WINDOWS_1250, - WINDOWS_1251, - WINDOWS_1252, - }; +class AppletFont +{ + public: + enum Encoding { + ASCII, + WINDOWS_1250, + WINDOWS_1251, + WINDOWS_1252, + }; - AppletFont(); - AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); + AppletFont(); + AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); - uint8_t lineHeight(); - uint8_t heightAboveCursor(); - uint8_t heightBelowCursor(); - uint8_t widthBetweenWords(); // Width of the space character + uint8_t lineHeight(); + uint8_t heightAboveCursor(); + uint8_t heightBelowCursor(); + uint8_t widthBetweenWords(); // Width of the space character - std::string decodeUTF8(std::string encoded); + std::string decodeUTF8(std::string encoded); - const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font + const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font -private: - uint32_t toUtf32(std::string utf8); - char applyEncoding(std::string utf8); + private: + uint32_t toUtf32(std::string utf8); + char applyEncoding(std::string utf8); - uint8_t height = 8; // Default value: in-built AdafruitGFX font - uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font - uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font - uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font + uint8_t height = 8; // Default value: in-built AdafruitGFX font + uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font + uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font + uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font - Encoding encoding = ASCII; + Encoding encoding = ASCII; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index 946b4dcb4..d383a11e4 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -4,156 +4,157 @@ using namespace NicheGraphics; -void InkHUD::MapApplet::onRender() { - // Abort if no markers to render - if (!enoughMarkers()) { - printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE); - printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); - return; - } - - // Helper: draw rounded rectangle centered at x,y - auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) { - int16_t x = cx - (w / 2); - int16_t y = cy - (h / 2); - - // center rects - fillRect(x + r, y, w - 2 * r, h, color); - fillRect(x, y + r, r, h - 2 * r, color); - fillRect(x + w - r, y + r, r, h - 2 * r, color); - - // corners - fillCircle(x + r, y + r, r, color); - fillCircle(x + w - r - 1, y + r, r, color); - fillCircle(x + r, y + h - r - 1, r, color); - fillCircle(x + w - r - 1, y + h - r - 1, r, color); - }; - - // Find center of map - getMapCenter(&latCenter, &lngCenter); - calculateAllMarkers(); - getMapSize(&widthMeters, &heightMeters); - calculateMapScale(); - - // Draw all markers first - for (Marker m : markers) { - int16_t x = X(0.5) + (m.eastMeters * metersToPx); - int16_t y = Y(0.5) - (m.northMeters * metersToPx); - - // Add white halo outline first - constexpr int outlinePad = 1; - int boxSize = 11; - int radius = 2; // rounded corner radius - - // White halo background - fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE); - - // Draw inner box - fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK); - - // Text inside - setFont(fontSmall); - setTextColor(WHITE); - - // Draw actual marker on top - if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) { - printAt(x + 1, y + 1, "X", CENTER, MIDDLE); - } else if (!m.hasHopsAway) { - printAt(x + 1, y + 1, "?", CENTER, MIDDLE); - } else { - char hopStr[4]; - snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway); - printAt(x, y + 1, hopStr, CENTER, MIDDLE); +void InkHUD::MapApplet::onRender() +{ + // Abort if no markers to render + if (!enoughMarkers()) { + printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE); + printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); + return; } - // Restore default font and color - setFont(fontSmall); - setTextColor(BLACK); - } + // Helper: draw rounded rectangle centered at x,y + auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) { + int16_t x = cx - (w / 2); + int16_t y = cy - (h / 2); - // Dual map scale bars - int16_t horizPx = width() * 0.25f; - int16_t vertPx = height() * 0.25f; - float horizMeters = horizPx / metersToPx; - float vertMeters = vertPx / metersToPx; + // center rects + fillRect(x + r, y, w - 2 * r, h, color); + fillRect(x, y + r, r, h - 2 * r, color); + fillRect(x + w - r, y + r, r, h - 2 * r, color); - auto formatDistance = [&](float meters, char *out, size_t len) { - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - float feet = meters * 3.28084f; - if (feet < 528) - snprintf(out, len, "%.0f ft", feet); - else { - float miles = feet / 5280.0f; - snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles); - } - } else { - if (meters >= 1000) - snprintf(out, len, "%.1f km", meters / 1000.0f); - else - snprintf(out, len, "%.0f m", meters); + // corners + fillCircle(x + r, y + r, r, color); + fillCircle(x + w - r - 1, y + r, r, color); + fillCircle(x + r, y + h - r - 1, r, color); + fillCircle(x + w - r - 1, y + h - r - 1, r, color); + }; + + // Find center of map + getMapCenter(&latCenter, &lngCenter); + calculateAllMarkers(); + getMapSize(&widthMeters, &heightMeters); + calculateMapScale(); + + // Draw all markers first + for (Marker m : markers) { + int16_t x = X(0.5) + (m.eastMeters * metersToPx); + int16_t y = Y(0.5) - (m.northMeters * metersToPx); + + // Add white halo outline first + constexpr int outlinePad = 1; + int boxSize = 11; + int radius = 2; // rounded corner radius + + // White halo background + fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE); + + // Draw inner box + fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK); + + // Text inside + setFont(fontSmall); + setTextColor(WHITE); + + // Draw actual marker on top + if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) { + printAt(x + 1, y + 1, "X", CENTER, MIDDLE); + } else if (!m.hasHopsAway) { + printAt(x + 1, y + 1, "?", CENTER, MIDDLE); + } else { + char hopStr[4]; + snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway); + printAt(x, y + 1, hopStr, CENTER, MIDDLE); + } + + // Restore default font and color + setFont(fontSmall); + setTextColor(BLACK); } - }; - // Horizontal scale bar - int16_t horizBarY = height() - 2; - int16_t horizBarX = 1; - drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK); - drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK); - drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK); + // Dual map scale bars + int16_t horizPx = width() * 0.25f; + int16_t vertPx = height() * 0.25f; + float horizMeters = horizPx / metersToPx; + float vertMeters = vertPx / metersToPx; - char horizLabel[32]; - formatDistance(horizMeters, horizLabel, sizeof(horizLabel)); - int16_t horizLabelW = getTextWidth(horizLabel); - int16_t horizLabelH = getFont().lineHeight(); - int16_t horizLabelX = horizBarX + horizPx + 4; - int16_t horizLabelY = horizBarY - horizLabelH + 1; - fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE); - printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM); + auto formatDistance = [&](float meters, char *out, size_t len) { + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + float feet = meters * 3.28084f; + if (feet < 528) + snprintf(out, len, "%.0f ft", feet); + else { + float miles = feet / 5280.0f; + snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles); + } + } else { + if (meters >= 1000) + snprintf(out, len, "%.1f km", meters / 1000.0f); + else + snprintf(out, len, "%.0f m", meters); + } + }; - // Vertical scale bar - int16_t vertBarX = 1; - int16_t vertBarBottom = horizBarY; - int16_t vertBarTop = vertBarBottom - vertPx; - drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK); - drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK); - drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK); + // Horizontal scale bar + int16_t horizBarY = height() - 2; + int16_t horizBarX = 1; + drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK); + drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK); + drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK); - char vertTopLabel[32]; - formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel)); - int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2; - int16_t topLabelW = getTextWidth(vertTopLabel); - int16_t topLabelH = getFont().lineHeight(); - fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE); - printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE); + char horizLabel[32]; + formatDistance(horizMeters, horizLabel, sizeof(horizLabel)); + int16_t horizLabelW = getTextWidth(horizLabel); + int16_t horizLabelH = getFont().lineHeight(); + int16_t horizLabelX = horizBarX + horizPx + 4; + int16_t horizLabelY = horizBarY - horizLabelH + 1; + fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE); + printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM); - char vertBottomLabel[32]; - formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel)); - int16_t bottomLabelY = vertBarBottom + 4; - int16_t bottomLabelW = getTextWidth(vertBottomLabel); - int16_t bottomLabelH = getFont().lineHeight(); - fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE); - printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); + // Vertical scale bar + int16_t vertBarX = 1; + int16_t vertBarBottom = horizBarY; + int16_t vertBarTop = vertBarBottom - vertPx; + drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK); + drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK); + drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK); - // Draw our node LAST with full white fill + outline - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && nodeDB->hasValidPosition(ourNode)) { - Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); + char vertTopLabel[32]; + formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel)); + int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2; + int16_t topLabelW = getTextWidth(vertTopLabel); + int16_t topLabelH = getFont().lineHeight(); + fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE); + printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE); - int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); - int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); + char vertBottomLabel[32]; + formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel)); + int16_t bottomLabelY = vertBarBottom + 4; + int16_t bottomLabelW = getTextWidth(vertBottomLabel); + int16_t bottomLabelH = getFont().lineHeight(); + fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE); + printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); - // White fill background + halo - fillCircle(centerX, centerY, 8, WHITE); // big white base - drawCircle(centerX, centerY, 8, WHITE); // crisp edge + // Draw our node LAST with full white fill + outline + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && nodeDB->hasValidPosition(ourNode)) { + Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); - // Black bullseye on top - drawCircle(centerX, centerY, 6, BLACK); - fillCircle(centerX, centerY, 2, BLACK); + int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); + int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); - // Crosshairs - drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK); - drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK); - } + // White fill background + halo + fillCircle(centerX, centerY, 8, WHITE); // big white base + drawCircle(centerX, centerY, 8, WHITE); // crisp edge + + // Black bullseye on top + drawCircle(centerX, centerY, 6, BLACK); + fillCircle(centerX, centerY, 2, BLACK); + + // Crosshairs + drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK); + drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK); + } } // Find the center point, in the middle of all node positions @@ -162,387 +163,396 @@ void InkHUD::MapApplet::onRender() { // - Calculates furthest nodes from "mean lat long" // - Place map center directly between these furthest nodes -void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) { - // If we have a valid position for our own node, use that as the anchor - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && nodeDB->hasValidPosition(ourNode)) { - *lat = ourNode->position.latitude_i * 1e-7; - *lng = ourNode->position.longitude_i * 1e-7; - } else { - // Find mean lat long coords - // ============================ - // - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet - // - averages the x, y and z coords - // - uses tan to find angles for lat / long degrees - // - longitude: triangle formed by x and y (on plane of the equator) - // - latitude: triangle formed by z (north south), - // and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's - // surface +void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) +{ + // If we have a valid position for our own node, use that as the anchor + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && nodeDB->hasValidPosition(ourNode)) { + *lat = ourNode->position.latitude_i * 1e-7; + *lng = ourNode->position.longitude_i * 1e-7; + } else { + // Find mean lat long coords + // ============================ + // - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet + // - averages the x, y and z coords + // - uses tan to find angles for lat / long degrees + // - longitude: triangle formed by x and y (on plane of the equator) + // - latitude: triangle formed by z (north south), + // and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's + // surface - // Working totals, averaged after nodeDB processed - uint32_t positionCount = 0; - float xAvg = 0; - float yAvg = 0; - float zAvg = 0; + // Working totals, averaged after nodeDB processed + uint32_t positionCount = 0; + float xAvg = 0; + float yAvg = 0; + float zAvg = 0; - // For each node in db - for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // For each node in db + for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Skip if no position - if (!nodeDB->hasValidPosition(node)) - continue; + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; - // Skip if derived applet doesn't want to show this node on the map - if (!shouldDrawNode(node)) - continue; + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; - // Latitude and Longitude of node, in radians - float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; - float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; + // Latitude and Longitude of node, in radians + float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; + float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; - // Convert to cartesian points, with center of earth at 0, 0, 0 - // Exact distance from center is irrelevant, as we're only interested in the vector - float x = cos(latRad) * cos(lngRad); - float y = cos(latRad) * sin(lngRad); - float z = sin(latRad); + // Convert to cartesian points, with center of earth at 0, 0, 0 + // Exact distance from center is irrelevant, as we're only interested in the vector + float x = cos(latRad) * cos(lngRad); + float y = cos(latRad) * sin(lngRad); + float z = sin(latRad); - // To find mean values shortly - xAvg += x; - yAvg += y; - zAvg += z; - positionCount++; + // To find mean values shortly + xAvg += x; + yAvg += y; + zAvg += z; + positionCount++; + } + + // All NodeDB processed, find mean values + xAvg /= positionCount; + yAvg /= positionCount; + zAvg /= positionCount; + + // Longitude from cartesian coords + // (Angle from 3D coords describing a point of globe's surface) + /* + UK + /-------\ + (Top View) /- -\ + /- (You) -\ + /- . -\ + /- . X -\ + Asia - ... - USA + \- Y -/ + \- -/ + \- -/ + \- -/ + \- -----/ + Pacific + + */ + + *lng = atan2(yAvg, xAvg) * RAD_TO_DEG; + + // Latitude from cartesian coords + // (Angle from 3D coords describing a point on the globe's surface) + // As latitude increases, distance from the Earth's north-south axis out to our surface point decreases. + // Means we need to first find the hypotenuse which becomes base of our triangle in the second step + /* + UK North + /-------\ (Front View) /-------\ + (Top View) /- -\ /- -\ + /- (You) -\ /-(You) -\ + /- /. -\ /- . -\ + /- √X²+Y²/ . X -\ /- Z . -\ + Asia - /... - USA - ..... - + \- Y -/ \- √X²+Y² -/ + \- -/ \- -/ + \- -/ \- -/ + \- -/ \- -/ + \- -----/ \- -----/ + Pacific South + */ + + float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect + *lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG; } - // All NodeDB processed, find mean values - xAvg /= positionCount; - yAvg /= positionCount; - zAvg /= positionCount; + // Use either our node position, or the mean fallback as the center + latCenter = *lat; + lngCenter = *lng; - // Longitude from cartesian coords - // (Angle from 3D coords describing a point of globe's surface) - /* - UK - /-------\ - (Top View) /- -\ - /- (You) -\ - /- . -\ - /- . X -\ - Asia - ... - USA - \- Y -/ - \- -/ - \- -/ - \- -/ - \- -----/ - Pacific + // ---------------------------------------------- + // This has given us either: + // - our actual position (preferred), or + // - a mean position (fallback if we had no fix) + // + // What we actually want is to place our center so that our outermost nodes + // end up on the border of our map. The only real use of our "center" is to give + // us a reference frame: which direction is east, and which is west. + //------------------------------------------------ - */ + // Find furthest nodes from our center + // ======================================== + float northernmost = latCenter; + float southernmost = latCenter; + float easternmost = lngCenter; + float westernmost = lngCenter; - *lng = atan2(yAvg, xAvg) * RAD_TO_DEG; + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Latitude from cartesian coords - // (Angle from 3D coords describing a point on the globe's surface) - // As latitude increases, distance from the Earth's north-south axis out to our surface point decreases. - // Means we need to first find the hypotenuse which becomes base of our triangle in the second step - /* - UK North - /-------\ (Front View) /-------\ - (Top View) /- -\ /- -\ - /- (You) -\ /-(You) -\ - /- /. -\ /- . -\ - /- √X²+Y²/ . X -\ /- Z . -\ - Asia - /... - USA - ..... - - \- Y -/ \- √X²+Y² -/ - \- -/ \- -/ - \- -/ \- -/ - \- -/ \- -/ - \- -----/ \- -----/ - Pacific South - */ + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; - float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect - *lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG; - } + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; - // Use either our node position, or the mean fallback as the center - latCenter = *lat; - lngCenter = *lng; + // Check for a new top or bottom latitude + float latNode = node->position.latitude_i * 1e-7; + northernmost = max(northernmost, latNode); + southernmost = min(southernmost, latNode); - // ---------------------------------------------- - // This has given us either: - // - our actual position (preferred), or - // - a mean position (fallback if we had no fix) - // - // What we actually want is to place our center so that our outermost nodes - // end up on the border of our map. The only real use of our "center" is to give - // us a reference frame: which direction is east, and which is west. - //------------------------------------------------ + // Longitude is trickier + float lngNode = node->position.longitude_i * 1e-7; + float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node + float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node + if (degEastward < degWestward) + easternmost = max(easternmost, lngCenter + degEastward); + else + westernmost = min(westernmost, lngCenter - degWestward); + } - // Find furthest nodes from our center - // ======================================== - float northernmost = latCenter; - float southernmost = latCenter; - float easternmost = lngCenter; - float westernmost = lngCenter; + // Todo: check for issues with map spans >180 deg. MQTT only.. + latCenter = (northernmost + southernmost) / 2; + lngCenter = (westernmost + easternmost) / 2; - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - - // Skip if no position - if (!nodeDB->hasValidPosition(node)) - continue; - - // Skip if derived applet doesn't want to show this node on the map - if (!shouldDrawNode(node)) - continue; - - // Check for a new top or bottom latitude - float latNode = node->position.latitude_i * 1e-7; - northernmost = max(northernmost, latNode); - southernmost = min(southernmost, latNode); - - // Longitude is trickier - float lngNode = node->position.longitude_i * 1e-7; - float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node - float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node - if (degEastward < degWestward) - easternmost = max(easternmost, lngCenter + degEastward); - else - westernmost = min(westernmost, lngCenter - degWestward); - } - - // Todo: check for issues with map spans >180 deg. MQTT only.. - latCenter = (northernmost + southernmost) / 2; - lngCenter = (westernmost + easternmost) / 2; - - // In case our new center is west of -180, or east of +180, for some reason - lngCenter = fmod(lngCenter, 180); + // In case our new center is west of -180, or east of +180, for some reason + lngCenter = fmod(lngCenter, 180); } // Size of map in meters // Grown to fit the nodes furthest from map center // Overridable if derived applet wants a custom map size (fixed size?) -void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) { - // Reset the value - *widthMeters = 0; - *heightMeters = 0; +void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) +{ + // Reset the value + *widthMeters = 0; + *heightMeters = 0; - // Find the greatest distance horizontally and vertically from map center - for (Marker m : markers) { - *widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2); - *heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2); - } + // Find the greatest distance horizontally and vertically from map center + for (Marker m : markers) { + *widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2); + *heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2); + } - // Add padding - *widthMeters *= 1.1; - *heightMeters *= 1.1; + // Add padding + *widthMeters *= 1.1; + *heightMeters *= 1.1; } // Convert and store info we need for drawing a marker // Lat / long to "meters relative to map center", for position on screen // Info about hopsAway, for marker size -InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) { - assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling. +InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) +{ + assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling. - // Bearing and distance from map center to node - float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng); - float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians + // Bearing and distance from map center to node + float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng); + float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians - // Split into meters north and meters east components (signed) - // - signedness of cos / sin automatically sets negative if south or west - float northMeters = cos(bearingFromCenter) * distanceFromCenter; - float eastMeters = sin(bearingFromCenter) * distanceFromCenter; + // Split into meters north and meters east components (signed) + // - signedness of cos / sin automatically sets negative if south or west + float northMeters = cos(bearingFromCenter) * distanceFromCenter; + float eastMeters = sin(bearingFromCenter) * distanceFromCenter; - // Store this as a new marker - Marker m; - m.eastMeters = eastMeters; - m.northMeters = northMeters; - m.hasHopsAway = hasHopsAway; - m.hopsAway = hopsAway; - return m; + // Store this as a new marker + Marker m; + m.eastMeters = eastMeters; + m.northMeters = northMeters; + m.hasHopsAway = hasHopsAway; + m.hopsAway = hopsAway; + return m; } // Draw a marker on the map for a node, with a shortname label, and backing box -void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) { - // Find x and y position based on node's position in nodeDB - assert(nodeDB->hasValidPosition(node)); - Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style - node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style - node->has_hops_away, // Is the hopsAway number valid - node->hops_away // Hops away - ); +void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) +{ + // Find x and y position based on node's position in nodeDB + assert(nodeDB->hasValidPosition(node)); + Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + ); - // Convert to pixel coords - int16_t markerX = X(0.5) + (m.eastMeters * metersToPx); - int16_t markerY = Y(0.5) - (m.northMeters * metersToPx); + // Convert to pixel coords + int16_t markerX = X(0.5) + (m.eastMeters * metersToPx); + int16_t markerY = Y(0.5) - (m.northMeters * metersToPx); - constexpr uint16_t paddingH = 2; - constexpr uint16_t paddingW = 4; - uint16_t paddingInnerW = 2; // Zero'd out if no text - constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) - constexpr uint16_t markerSizeMin = 5; + constexpr uint16_t paddingH = 2; + constexpr uint16_t paddingW = 4; + uint16_t paddingInnerW = 2; // Zero'd out if no text + constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) + constexpr uint16_t markerSizeMin = 5; - int16_t textX; - int16_t textY; - uint16_t textW; - uint16_t textH; - int16_t labelX; - int16_t labelY; - uint16_t labelW; - uint16_t labelH; - uint8_t markerSize; + int16_t textX; + int16_t textY; + uint16_t textW; + uint16_t textH; + int16_t labelX; + int16_t labelY; + uint16_t labelW; + uint16_t labelH; + uint8_t markerSize; - bool tooManyHops = node->hops_away > config.lora.hop_limit; - bool isOurNode = node->num == nodeDB->getNodeNum(); - bool unknownHops = !node->has_hops_away && !isOurNode; + bool tooManyHops = node->hops_away > config.lora.hop_limit; + bool isOurNode = node->num == nodeDB->getNodeNum(); + bool unknownHops = !node->has_hops_away && !isOurNode; - // Parse any non-ascii chars in the short name, - // and use last 4 instead if unknown / can't render - std::string shortName = parseShortName(node); + // Parse any non-ascii chars in the short name, + // and use last 4 instead if unknown / can't render + std::string shortName = parseShortName(node); - // We will draw a left or right hand variant, to place text towards screen center - // Hopefully avoid text spilling off screen - // Most values are the same, regardless of left-right handedness + // We will draw a left or right hand variant, to place text towards screen center + // Hopefully avoid text spilling off screen + // Most values are the same, regardless of left-right handedness - // Pick emblem style - if (tooManyHops) - markerSize = getTextWidth("!"); - else if (unknownHops) - markerSize = markerSizeMin; - else - markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); + // Pick emblem style + if (tooManyHops) + markerSize = getTextWidth("!"); + else if (unknownHops) + markerSize = markerSizeMin; + else + markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); - // Common dimensions (left or right variant) - textW = getTextWidth(shortName); - if (textW == 0) - paddingInnerW = 0; // If no text, no padding for text - textH = fontSmall.lineHeight(); - labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH; - labelY = markerY - (labelH / 2); - textY = markerY; - labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant + // Common dimensions (left or right variant) + textW = getTextWidth(shortName); + if (textW == 0) + paddingInnerW = 0; // If no text, no padding for text + textH = fontSmall.lineHeight(); + labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH; + labelY = markerY - (labelH / 2); + textY = markerY; + labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant - // Left-side variant - if (markerX < width() / 2) { - labelX = markerX - (markerSize / 2) - paddingW; - textX = labelX + paddingW + markerSize + paddingInnerW; - } + // Left-side variant + if (markerX < width() / 2) { + labelX = markerX - (markerSize / 2) - paddingW; + textX = labelX + paddingW + markerSize + paddingInnerW; + } - // Right-side variant - else { - labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW; - textX = labelX + paddingW; - } + // Right-side variant + else { + labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW; + textX = labelX + paddingW; + } - // Prevent overlap with scale bars and their labels - // Define a "safe zone" in the bottom-left where the scale bars and text are drawn - constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height - constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone - bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth); + // Prevent overlap with scale bars and their labels + // Define a "safe zone" in the bottom-left where the scale bars and text are drawn + constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height + constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone + bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth); - // If it overlaps, shift label upward slightly above the safe zone - if (overlapsScale) { - labelY = height() - safeZoneHeight - labelH - 2; - textY = labelY + (labelH / 2); - } + // If it overlaps, shift label upward slightly above the safe zone + if (overlapsScale) { + labelY = height() - safeZoneHeight - labelH - 2; + textY = labelY + (labelH / 2); + } - // Backing box - fillRect(labelX, labelY, labelW, labelH, WHITE); - drawRect(labelX, labelY, labelW, labelH, BLACK); + // Backing box + fillRect(labelX, labelY, labelW, labelH, WHITE); + drawRect(labelX, labelY, labelW, labelH, BLACK); - // Short name - printAt(textX, textY, shortName, LEFT, MIDDLE); + // Short name + printAt(textX, textY, shortName, LEFT, MIDDLE); - // If the label is for our own node, - // fade it by overdrawing partially with white - if (node == nodeDB->getMeshNode(nodeDB->getNodeNum())) - hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE); + // If the label is for our own node, + // fade it by overdrawing partially with white + if (node == nodeDB->getMeshNode(nodeDB->getNodeNum())) + hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE); - // Draw the marker emblem - // - after the fading, because hatching (own node) can align with cross and make it look weird - if (tooManyHops) - printAt(markerX, markerY, "!", CENTER, MIDDLE); - else - drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops + // Draw the marker emblem + // - after the fading, because hatching (own node) can align with cross and make it look weird + if (tooManyHops) + printAt(markerX, markerY, "!", CENTER, MIDDLE); + else + drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops } // Check if we actually have enough nodes which would be shown on the map // Need at least two, to draw a sensible map -bool InkHUD::MapApplet::enoughMarkers() { - size_t count = 0; - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); +bool InkHUD::MapApplet::enoughMarkers() +{ + size_t count = 0; + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Count nodes - if (nodeDB->hasValidPosition(node) && shouldDrawNode(node)) - count++; + // Count nodes + if (nodeDB->hasValidPosition(node) && shouldDrawNode(node)) + count++; - // We need to find two - if (count == 2) - return true; // Two nodes is enough for a sensible map - } + // We need to find two + if (count == 2) + return true; // Two nodes is enough for a sensible map + } - return false; // No nodes would be drawn (or just the one, uselessly at 0,0) + return false; // No nodes would be drawn (or just the one, uselessly at 0,0) } // Calculate how far north and east of map center each node is // Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode -void InkHUD::MapApplet::calculateAllMarkers() { - // Clear old markers - markers.clear(); +void InkHUD::MapApplet::calculateAllMarkers() +{ + // Clear old markers + markers.clear(); - // For each node in db - for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // For each node in db + for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - // Skip if no position - if (!nodeDB->hasValidPosition(node)) - continue; + // Skip if no position + if (!nodeDB->hasValidPosition(node)) + continue; - // Skip if derived applet doesn't want to show this node on the map - if (!shouldDrawNode(node)) - continue; + // Skip if derived applet doesn't want to show this node on the map + if (!shouldDrawNode(node)) + continue; - // Skip if our own node - // - special handling in render() - if (node->num == nodeDB->getNodeNum()) - continue; + // Skip if our own node + // - special handling in render() + if (node->num == nodeDB->getNodeNum()) + continue; - // Calculate marker and store it - markers.push_back(calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style - node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style - node->has_hops_away, // Is the hopsAway number valid - node->hops_away // Hops away - )); - } + // Calculate marker and store it + markers.push_back( + calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + )); + } } // Determine the conversion factor between metres, and pixels on screen // May be overriden by derived applet, if custom scale required (fixed map size?) -void InkHUD::MapApplet::calculateMapScale() { - // Aspect ratio of map and screen - // - larger = wide, smaller = tall - // - used to set scale, so that widest map dimension fits in applet - float mapAspectRatio = (float)widthMeters / heightMeters; - float appletAspectRatio = (float)width() / height(); +void InkHUD::MapApplet::calculateMapScale() +{ + // Aspect ratio of map and screen + // - larger = wide, smaller = tall + // - used to set scale, so that widest map dimension fits in applet + float mapAspectRatio = (float)widthMeters / heightMeters; + float appletAspectRatio = (float)width() / height(); - // "Shrink to fit" - // Scale the map so that the largest dimension is fully displayed - // Because aspect ratio will be maintained, the other dimension will appear "padded" - if (mapAspectRatio > appletAspectRatio) - metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width. - else - metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height. + // "Shrink to fit" + // Scale the map so that the largest dimension is fully displayed + // Because aspect ratio will be maintained, the other dimension will appear "padded" + if (mapAspectRatio > appletAspectRatio) + metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width. + else + metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height. } // Draw an x, centered on a specific point // Most markers will draw with this method -void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) { - int16_t x0 = x - (size / 2); - int16_t y0 = y - (size / 2); - int16_t x1 = x0 + size - 1; - int16_t y1 = y0 + size - 1; - drawLine(x0, y0, x1, y1, BLACK); - drawLine(x0, y1, x1, y0, BLACK); +void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) +{ + int16_t x0 = x - (size / 2); + int16_t y0 = y - (size / 2); + int16_t x1 = x0 + size - 1; + int16_t y1 = y0 + size - 1; + drawLine(x0, y0, x1, y1, BLACK); + drawLine(x0, y1, x1, y0, BLACK); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h index a3f780a6d..f45a36071 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h @@ -21,41 +21,43 @@ The base applet doesn't handle any events; this is left to the derived applets. #include "MeshModule.h" #include "gps/GeoCoord.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class MapApplet : public Applet { -public: - void onRender() override; +class MapApplet : public Applet +{ + public: + void onRender() override; -protected: - virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes - virtual void getMapCenter(float *lat, float *lng); - virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters); + protected: + virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes + virtual void getMapCenter(float *lat, float *lng); + virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters); - bool enoughMarkers(); // Anything to draw? - void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker + bool enoughMarkers(); // Anything to draw? + void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker -private: - // Position and size of a marker to be drawn - struct Marker { - float eastMeters = 0; // Meters east of map center. Negative if west. - float northMeters = 0; // Meters north of map center. Negative if south. - bool hasHopsAway = false; - uint8_t hopsAway = 0; // Determines marker size - }; + private: + // Position and size of a marker to be drawn + struct Marker { + float eastMeters = 0; // Meters east of map center. Negative if west. + float northMeters = 0; // Meters north of map center. Negative if south. + bool hasHopsAway = false; + uint8_t hopsAway = 0; // Determines marker size + }; - Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway); - void calculateAllMarkers(); - void calculateMapScale(); // Conversion factor for meters to pixels - void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers + Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway); + void calculateAllMarkers(); + void calculateMapScale(); // Conversion factor for meters to pixels + void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers - float metersToPx = 0; // Conversion factor for meters to pixels - float latCenter = 0; // Map center: latitude - float lngCenter = 0; // Map center: longitude + float metersToPx = 0; // Conversion factor for meters to pixels + float latCenter = 0; // Map center: latitude + float lngCenter = 0; // Map center: longitude - std::list markers; - uint32_t widthMeters = 0; // Map width: meters - uint32_t heightMeters = 0; // Map height: meters + std::list markers; + uint32_t widthMeters = 0; // Map width: meters + uint32_t heightMeters = 0; // Map height: meters }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 79487bbde..5c9906fba 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -9,276 +9,283 @@ using namespace NicheGraphics; -InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) { - // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule - // For all other packets, we manually act as if isPromiscuous=false, in wantPacket - MeshModule::isPromiscuous = true; +InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) +{ + // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule + // For all other packets, we manually act as if isPromiscuous=false, in wantPacket + MeshModule::isPromiscuous = true; } // Do we want to process this packet with handleReceived()? -bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) { - // Only interested if: - return isActive() // Applet is active - && !isFromUs(p) // Packet is incoming (not outgoing) - && (isToUs(p) || isBroadcast(p->to) || // Either: intended for us, - p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo +bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) +{ + // Only interested if: + return isActive() // Applet is active + && !isFromUs(p) // Packet is incoming (not outgoing) + && (isToUs(p) || isBroadcast(p->to) || // Either: intended for us, + p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo - // To match the behavior seen in the client apps: - // - NodeInfoModule's ProtoBufModule base is "promiscuous" - // - All other activity is *not* promiscuous + // To match the behavior seen in the client apps: + // - NodeInfoModule's ProtoBufModule base is "promiscuous" + // - All other activity is *not* promiscuous - // To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here, - // to match the code in MeshModule::callModules + // To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here, + // to match the code in MeshModule::callModules } // MeshModule packets arrive here // Extract the info and pass it to the derived applet // Derived applet will store the CardInfo, and perform any required sorting of the CardInfo collection // Derived applet might also need to keep other tallies (active nodes count?) -ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) { - // Abort if applet fully deactivated - // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early - // return - if (!isActive()) - return ProcessMessage::CONTINUE; +ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // Abort if applet fully deactivated + // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return + if (!isActive()) + return ProcessMessage::CONTINUE; - // Assemble info: from this event - CardInfo c; - c.nodeNum = mp.from; - c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); + // Assemble info: from this event + CardInfo c; + c.nodeNum = mp.from; + c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); - // Assemble info: from nodeDB (needed to detect changes) - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (node) { - if (node->has_hops_away) - c.hopsAway = node->hops_away; + // Assemble info: from nodeDB (needed to detect changes) + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (node) { + if (node->has_hops_away) + c.hopsAway = node->hops_away; - if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { - // Get lat and long as float - // Meshtastic stores these as integers internally - float ourLat = ourNode->position.latitude_i * 1e-7; - float ourLong = ourNode->position.longitude_i * 1e-7; - float theirLat = node->position.latitude_i * 1e-7; - float theirLong = node->position.longitude_i * 1e-7; + if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { + // Get lat and long as float + // Meshtastic stores these as integers internally + float ourLat = ourNode->position.latitude_i * 1e-7; + float ourLong = ourNode->position.longitude_i * 1e-7; + float theirLat = node->position.latitude_i * 1e-7; + float theirLong = node->position.longitude_i * 1e-7; - c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + } } - } - // Pass to the derived applet - // Derived applet is responsible for requesting update, if justified - // That request will eventually trigger our class' onRender method - handleParsed(c); + // Pass to the derived applet + // Derived applet is responsible for requesting update, if justified + // That request will eventually trigger our class' onRender method + handleParsed(c); - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } // Calculate maximum number of cards we may ever need to render, in our tallest layout config // Number might be slightly in excess of the true value: applet header text not accounted for -uint8_t InkHUD::NodeListApplet::maxCards() { - // Cache result. Shouldn't change during execution - static uint8_t cards = 0; +uint8_t InkHUD::NodeListApplet::maxCards() +{ + // Cache result. Shouldn't change during execution + static uint8_t cards = 0; - if (!cards) { - const uint16_t height = Tile::maxDisplayDimension(); + if (!cards) { + const uint16_t height = Tile::maxDisplayDimension(); - // Use a loop instead of arithmetic, because it's easier for my brain to follow - // Add cards one by one, until the latest card extends below screen + // Use a loop instead of arithmetic, because it's easier for my brain to follow + // Add cards one by one, until the latest card extends below screen - uint16_t y = cardH; // First card: no margin above - cards = 1; + uint16_t y = cardH; // First card: no margin above + cards = 1; - while (y < height) { - y += cardMarginH; - y += cardH; - cards++; + while (y < height) { + y += cardMarginH; + y += cardH; + cards++; + } } - } - return cards; + return cards; } // Draw, using info which derived applet placed into NodeListApplet::cards for us -void InkHUD::NodeListApplet::onRender() { +void InkHUD::NodeListApplet::onRender() +{ - // ================================ - // Draw the standard applet header - // ================================ + // ================================ + // Draw the standard applet header + // ================================ - drawHeader(getHeaderText()); // Ask derived applet for the title + drawHeader(getHeaderText()); // Ask derived applet for the title - // Dimensions of the header - int16_t headerDivY = getHeaderHeight() - 1; - constexpr uint16_t padDivH = 2; + // Dimensions of the header + int16_t headerDivY = getHeaderHeight() - 1; + constexpr uint16_t padDivH = 2; - // ======================== - // Draw the main node list - // ======================== + // ======================== + // Draw the main node list + // ======================== - // Imaginary vertical line dividing left-side and right-side info - // Long-name will crop here - const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); + // Imaginary vertical line dividing left-side and right-side info + // Long-name will crop here + const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); - // Y value (top) of the current card. Increases as we draw. - uint16_t cardTopY = headerDivY + padDivH; + // Y value (top) of the current card. Increases as we draw. + uint16_t cardTopY = headerDivY + padDivH; - // Clean up deleted nodes before drawing - cards.erase(std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }), cards.end()); + // Clean up deleted nodes before drawing + cards.erase( + std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }), + cards.end()); - // -- Each node in list -- - for (auto card = cards.begin(); card != cards.end(); ++card) { + // -- Each node in list -- + for (auto card = cards.begin(); card != cards.end(); ++card) { - // Gather info - // ======================================== - NodeNum &nodeNum = card->nodeNum; - SignalStrength &signal = card->signal; - std::string longName; // handled below - std::string shortName; // handled below - std::string distance; // handled below; - uint8_t &hopsAway = card->hopsAway; + // Gather info + // ======================================== + NodeNum &nodeNum = card->nodeNum; + SignalStrength &signal = card->signal; + std::string longName; // handled below + std::string shortName; // handled below + std::string distance; // handled below; + uint8_t &hopsAway = card->hopsAway; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); - // Skip deleted nodes - if (!node) { - continue; + // Skip deleted nodes + if (!node) { + continue; + } + + // -- Shortname -- + // Parse special chars in the short name + // Use "?" if unknown + if (node) + shortName = parseShortName(node); + else + shortName = "?"; + + // -- Longname -- + // Parse special chars in long name + // Use node id if unknown + if (node && node->has_user) + longName = parse(node->user.long_name); // Found in nodeDB + else { + // Not found in nodeDB, show a hex nodeid instead + longName = hexifyNodeNum(nodeNum); + } + + // -- Distance -- + if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN) + distance = localizeDistance(card->distanceMeters); + + // Draw the info + // ==================================== + + // Define two lines of text for the card + // We will center our text on these lines + uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2); + uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2); + + // Print the short name + setFont(fontMedium); + printAt(0, lineAY, shortName, LEFT, MIDDLE); + + // Print the distance + setFont(fontSmall); + printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + + // If we have a direct connection to the node, draw the signal indicator + if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { + uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label + uint16_t signalH = fontMedium.lineHeight() * 0.75; + int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); + int16_t signalX = width() - signalW; + drawSignalIndicator(signalX, signalY, signalW, signalH, signal); + } + // Otherwise, print "hops away" info, if available + else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) { + std::string hopString = to_string(node->hops_away); + hopString += " Hop"; + if (node->hops_away != 1) + hopString += "s"; // Append s for "Hops", rather than "Hop" + + printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); + } + + // Print the long name, cropping to prevent overflow onto the right-side info + setCrop(0, 0, dividerX - 1, height()); + printAt(0, lineBY, longName, LEFT, MIDDLE); + + // GFX effect: "hatch" the right edge of longName area + // If a longName has been cropped, it will appear to fade out, + // creating a soft barrier with the right-side info + const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); + const int16_t hatchWidth = fontSmall.lineHeight(); + hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); + + // Prepare to draw the next card + resetCrop(); + cardTopY += cardH; + + // Once we've run out of screen, stop drawing cards + // Depending on tiles / rotation, this may be before we hit maxCards + if (cardTopY > height()) + break; } - - // -- Shortname -- - // Parse special chars in the short name - // Use "?" if unknown - if (node) - shortName = parseShortName(node); - else - shortName = "?"; - - // -- Longname -- - // Parse special chars in long name - // Use node id if unknown - if (node && node->has_user) - longName = parse(node->user.long_name); // Found in nodeDB - else { - // Not found in nodeDB, show a hex nodeid instead - longName = hexifyNodeNum(nodeNum); - } - - // -- Distance -- - if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN) - distance = localizeDistance(card->distanceMeters); - - // Draw the info - // ==================================== - - // Define two lines of text for the card - // We will center our text on these lines - uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2); - uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2); - - // Print the short name - setFont(fontMedium); - printAt(0, lineAY, shortName, LEFT, MIDDLE); - - // Print the distance - setFont(fontSmall); - printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); - - // If we have a direct connection to the node, draw the signal indicator - if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { - uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label - uint16_t signalH = fontMedium.lineHeight() * 0.75; - int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); - int16_t signalX = width() - signalW; - drawSignalIndicator(signalX, signalY, signalW, signalH, signal); - } - // Otherwise, print "hops away" info, if available - else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) { - std::string hopString = to_string(node->hops_away); - hopString += " Hop"; - if (node->hops_away != 1) - hopString += "s"; // Append s for "Hops", rather than "Hop" - - printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); - } - - // Print the long name, cropping to prevent overflow onto the right-side info - setCrop(0, 0, dividerX - 1, height()); - printAt(0, lineBY, longName, LEFT, MIDDLE); - - // GFX effect: "hatch" the right edge of longName area - // If a longName has been cropped, it will appear to fade out, - // creating a soft barrier with the right-side info - const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); - const int16_t hatchWidth = fontSmall.lineHeight(); - hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); - - // Prepare to draw the next card - resetCrop(); - cardTopY += cardH; - - // Once we've run out of screen, stop drawing cards - // Depending on tiles / rotation, this may be before we hit maxCards - if (cardTopY > height()) - break; - } } // Draw element: a "mobile phone" style signal indicator // We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc // This prevents issues with premature rounding when rendering tiny elements -void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) { +void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) +{ - /* - +-------------------------------------------+ - | | - | | - | barHeightRelative=1.0 - | +--+ ^ | - | gutterW +--+ | | | | - | <--> +--+ | | | | | | - | +--+ | | | | | | | | - | | | | | | | | | | | - | <-> +--+ +--+ +--+ +--+ v | - | paddingW ^ | - | paddingH | | - | v | - +-------------------------------------------+ - */ + /* + +-------------------------------------------+ + | | + | | + | barHeightRelative=1.0 + | +--+ ^ | + | gutterW +--+ | | | | + | <--> +--+ | | | | | | + | +--+ | | | | | | | | + | | | | | | | | | | | + | <-> +--+ +--+ +--+ +--+ v | + | paddingW ^ | + | paddingH | | + | v | + +-------------------------------------------+ + */ - constexpr float paddingW = 0.1; // Either side - constexpr float paddingH = 0.1; // Above and below - constexpr float gutterW = 0.1; // Between bars + constexpr float paddingW = 0.1; // Either side + constexpr float paddingH = 0.1; // Above and below + constexpr float gutterW = 0.1; // Between bars - constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest - constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count. + constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest + constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count. - // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions - float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount; - float barHMax = 1.0 - (paddingH + paddingH); + // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions + float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount; + float barHMax = 1.0 - (paddingH + paddingH); - // Draw signal bar rectangles, then placeholder lines once strength reached - for (uint8_t i = 0; i < barCount; i++) { - // Coords for this specific bar - float barH = barHMax * barHRel[i]; - float barX = paddingW + (i * (gutterW + barW)); - float barY = paddingH + (barHMax - barH); + // Draw signal bar rectangles, then placeholder lines once strength reached + for (uint8_t i = 0; i < barCount; i++) { + // Coords for this specific bar + float barH = barHMax * barHRel[i]; + float barX = paddingW + (i * (gutterW + barW)); + float barY = paddingH + (barHMax - barH); - // Rasterize to px coords at the last moment - int16_t rX = (x + (w * barX)) + 0.5; - int16_t rY = (y + (h * barY)) + 0.5; - uint16_t rW = (w * barW) + 0.5; - uint16_t rH = (h * barH) + 0.5; + // Rasterize to px coords at the last moment + int16_t rX = (x + (w * barX)) + 0.5; + int16_t rY = (y + (h * barY)) + 0.5; + uint16_t rW = (w * barW) + 0.5; + uint16_t rH = (h * barH) + 0.5; - // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines - if (i <= strength) - drawRect(rX, rY, rW, rH, BLACK); - else { - // Just draw a placeholder line - float lineY = barY + barH; - uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize - drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK); + // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines + if (i <= strength) + drawRect(rX, rY, rW, rH, BLACK); + else { + // Just draw a placeholder line + float lineY = barY + barH; + uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize + drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK); + } } - } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index 8fadd1339..c2340027b 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -25,46 +25,48 @@ Used by the "Recents" and "Heard" applets. Possibly more in future? #include "main.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class NodeListApplet : public Applet, public MeshModule { -protected: - // Info needed to draw a node card to the list - // - generated each time we hear a node - struct CardInfo { - static constexpr uint8_t HOPS_UNKNOWN = -1; - static constexpr uint32_t DISTANCE_UNKNOWN = -1; +class NodeListApplet : public Applet, public MeshModule +{ + protected: + // Info needed to draw a node card to the list + // - generated each time we hear a node + struct CardInfo { + static constexpr uint8_t HOPS_UNKNOWN = -1; + static constexpr uint32_t DISTANCE_UNKNOWN = -1; - NodeNum nodeNum = 0; - SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN; - uint32_t distanceMeters = DISTANCE_UNKNOWN; - uint8_t hopsAway = HOPS_UNKNOWN; - }; + NodeNum nodeNum = 0; + SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN; + uint32_t distanceMeters = DISTANCE_UNKNOWN; + uint8_t hopsAway = HOPS_UNKNOWN; + }; -public: - NodeListApplet(const char *name); + public: + NodeListApplet(const char *name); - void onRender() override; + void onRender() override; - bool wantPacket(const meshtastic_MeshPacket *p) override; - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + bool wantPacket(const meshtastic_MeshPacket *p) override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; -protected: - virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node - virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be + protected: + virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node + virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be - uint8_t maxCards(); // Max number of cards which could ever fit on screen + uint8_t maxCards(); // Max number of cards which could ever fit on screen - std::deque cards; // Cards to be rendered. Derived applet fills this. + std::deque cards; // Cards to be rendered. Derived applet fills this. -private: - void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, - SignalStrength signal); // Draw a "mobile phone" style signal indicator + private: + void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, + SignalStrength signal); // Draw a "mobile phone" style signal indicator - // Card Dimensions - // - for rendering and for maxCards calc - uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards - uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card + // Card Dimensions + // - for rendering and for maxCards calc + uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards + uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp index 6d749d804..c52719e55 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp @@ -6,14 +6,15 @@ using namespace NicheGraphics; // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. -void InkHUD::BasicExampleApplet::onRender() { - printAt(0, 0, "Hello, World!"); +void InkHUD::BasicExampleApplet::onRender() +{ + printAt(0, 0, "Hello, World!"); - // If text might contain "special characters", is needs parsing first - // This applies to data such as text-messages and and node names + // If text might contain "special characters", is needs parsing first + // This applies to data such as text-messages and and node names - // std::string greeting = parse("Grüezi mitenand!"); - // printAt(0, 0, greeting); + // std::string greeting = parse("Grüezi mitenand!"); + // printAt(0, 0, greeting); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h index c3d4d7b83..aed63cdc8 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h @@ -19,14 +19,16 @@ In variants//nicheGraphics.h: #include "graphics/niche/InkHUD/Applet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class BasicExampleApplet : public Applet { -public: - // You must have an onRender() method - // All drawing happens here +class BasicExampleApplet : public Applet +{ + public: + // You must have an onRender() method + // All drawing happens here - void onRender() override; + void onRender() override; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp index a949ff200..6b02f4c92 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp @@ -5,47 +5,49 @@ using namespace NicheGraphics; // We configured the Module API to call this method when we receive a new text message -ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) { +ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ - // Abort if applet fully deactivated - // Don't waste time: we wouldn't be rendered anyway - if (!isActive()) + // Abort if applet fully deactivated + // Don't waste time: we wouldn't be rendered anyway + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Check that this is an incoming message + // Outgoing messages (sent by us) will also call handleReceived + + if (!isFromUs(&mp)) { + // Store the sender's nodenum + // We need to keep this information, so we can re-use it anytime render() is called + haveMessage = true; + fromWho = mp.from; + + // Tell InkHUD that we have something new to show on the screen + requestUpdate(); + } + + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages return ProcessMessage::CONTINUE; - - // Check that this is an incoming message - // Outgoing messages (sent by us) will also call handleReceived - - if (!isFromUs(&mp)) { - // Store the sender's nodenum - // We need to keep this information, so we can re-use it anytime render() is called - haveMessage = true; - fromWho = mp.from; - - // Tell InkHUD that we have something new to show on the screen - requestUpdate(); - } - - // Tell Module API to continue informing other firmware components about this message - // We're not the only component which is interested in new text messages - return ProcessMessage::CONTINUE; } // All drawing happens here // We can trigger a render by calling requestUpdate() // Render might be called by some external source // We should always be ready to draw -void InkHUD::NewMsgExampleApplet::onRender() { - printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) +void InkHUD::NewMsgExampleApplet::onRender() +{ + printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) - int16_t centerX = X(0.5); // Same as width() / 2 - int16_t centerY = Y(0.5); // Same as height() / 2 + int16_t centerX = X(0.5); // Same as width() / 2 + int16_t centerY = Y(0.5); // Same as height() / 2 - if (haveMessage) { - printAt(centerX, centerY, "New Message", CENTER, BOTTOM); - printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP); - } else { - printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY) - } + if (haveMessage) { + printAt(centerX, centerY, "New Message", CENTER, BOTTOM); + printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP); + } else { + printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY) + } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h index 1a0aab23f..22670a0f0 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -24,34 +24,36 @@ In variants//nicheGraphics.h: #include "mesh/SinglePortModule.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class NewMsgExampleApplet : public Applet, public SinglePortModule { -public: - // The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages. - NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} +class NewMsgExampleApplet : public Applet, public SinglePortModule +{ + public: + // The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages. + NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} - // All drawing happens here - void onRender() override; + // All drawing happens here + void onRender() override; - // Your applet might also want to use some of these - // Useful for setting up or tidying up + // Your applet might also want to use some of these + // Useful for setting up or tidying up - /* - void onActivate(); // When started - void onDeactivate(); // When stopped - void onForeground(); // When shown by short-press - void onBackground(); // When hidden by short-press - */ + /* + void onActivate(); // When started + void onDeactivate(); // When stopped + void onForeground(); // When shown by short-press + void onBackground(); // When hidden by short-press + */ -private: - // Called when we receive new text messages - // Part of the MeshModule API - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + private: + // Called when we receive new text messages + // Part of the MeshModule API + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - // Store info from handleReceived - bool haveMessage = false; - NodeNum fromWho = 0; + // Store info from handleReceived + bool haveMessage = false; + NodeNum fromWho = 0; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp index 62517d2d0..67ef87f41 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -4,76 +4,79 @@ using namespace NicheGraphics; -InkHUD::AlignStickApplet::AlignStickApplet() { - if (!settings->joystick.aligned) - bringToForeground(); +InkHUD::AlignStickApplet::AlignStickApplet() +{ + if (!settings->joystick.aligned) + bringToForeground(); } -void InkHUD::AlignStickApplet::onRender() { - setFont(fontMedium); - printAt(0, 0, "Align Joystick:"); - setFont(fontSmall); - std::string instructions = "Move joystick in the direction indicated"; - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); +void InkHUD::AlignStickApplet::onRender() +{ + setFont(fontMedium); + printAt(0, 0, "Align Joystick:"); + setFont(fontSmall); + std::string instructions = "Move joystick in the direction indicated"; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); - // Size of the region in which the joystick graphic should fit - uint16_t joyXLimit = X(0.8); - uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; - if (getTextWidth(instructions) > width()) - contentH += fontSmall.lineHeight(); - uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; - uint16_t joyYLimit = freeY * 0.8; + // Size of the region in which the joystick graphic should fit + uint16_t joyXLimit = X(0.8); + uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; + if (getTextWidth(instructions) > width()) + contentH += fontSmall.lineHeight(); + uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; + uint16_t joyYLimit = freeY * 0.8; - // Use the shorter of the two - uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; + // Use the shorter of the two + uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; - // Center the joystick graphic - uint16_t centerX = X(0.5); - uint16_t centerY = contentH + freeY * 0.5; + // Center the joystick graphic + uint16_t centerX = X(0.5); + uint16_t centerY = contentH + freeY * 0.5; - // Draw joystick graphic - drawStick(centerX, centerY, joyWidth); + // Draw joystick graphic + drawStick(centerX, centerY, joyWidth); - setFont(fontSmall); - printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); + setFont(fontSmall); + printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); } // Draw a scalable joystick graphic -void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) { - if (width < 9) // too small to draw - return; +void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) +{ + if (width < 9) // too small to draw + return; - else if (width < 40) { // only draw up arrow - uint16_t chamfer = width < 20 ? 1 : 2; + else if (width < 40) { // only draw up arrow + uint16_t chamfer = width < 20 ? 1 : 2; - // Draw filled up arrow - drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); + // Draw filled up arrow + drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); - } else { // large enough to draw the full thing - uint16_t chamfer = width < 80 ? 1 : 2; - uint16_t stroke = 3; // pixels - uint16_t arrowW = width * 0.22; - uint16_t hollowW = arrowW - stroke * 2; + } else { // large enough to draw the full thing + uint16_t chamfer = width < 80 ? 1 : 2; + uint16_t stroke = 3; // pixels + uint16_t arrowW = width * 0.22; + uint16_t hollowW = arrowW - stroke * 2; - // Draw center circle - fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); - fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); + // Draw center circle + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); - // Draw filled up arrow - drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); + // Draw filled up arrow + drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); - // Draw down arrow - drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); - drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); + // Draw down arrow + drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); + drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); - // Draw left arrow - drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); - drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); + // Draw left arrow + drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); + drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); - // Draw right arrow - drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); - drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); - } + // Draw right arrow + drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); + drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); + } } // Draw a scalable joystick direction arrow @@ -87,98 +90,116 @@ void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uin v |_________| */ -void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color) { - uint16_t chamferW = chamfer * 2 + 1; - uint16_t triangleW = size - chamferW; +void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, + uint16_t chamfer, Color color) +{ + uint16_t chamferW = chamfer * 2 + 1; + uint16_t triangleW = size - chamferW; - // Draw arrow - switch (direction) { - case Direction::UP: - fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); - fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); - fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, pointY + triangleW, color); - fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, pointY + triangleW, color); - break; - case Direction::DOWN: - fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); - fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); - fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, pointY - triangleW, color); - fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, pointY - triangleW, color); - break; - case Direction::LEFT: - fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); - fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); - fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, pointY - chamfer, color); - fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, pointY + chamfer, color); - break; - case Direction::RIGHT: - fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); - fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); - fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, pointY - chamfer, color); - fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, pointY + chamfer, color); - break; - } + // Draw arrow + switch (direction) { + case Direction::UP: + fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, + pointY + triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, + pointY + triangleW, color); + break; + case Direction::DOWN: + fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, + pointY - triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, + pointY - triangleW, color); + break; + case Direction::LEFT: + fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, + pointY + chamfer, color); + break; + case Direction::RIGHT: + fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, + pointY + chamfer, color); + break; + } } -void InkHUD::AlignStickApplet::onForeground() { - // Prevent most other applets from requesting update, and skip their rendering entirely - // Another system applet with a higher precedence can potentially ignore this - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; +void InkHUD::AlignStickApplet::onForeground() +{ + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; - handleInput = true; // Intercept the button input for our applet + handleInput = true; // Intercept the button input for our applet } -void InkHUD::AlignStickApplet::onBackground() { - // Allow normal update behavior to resume - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; +void InkHUD::AlignStickApplet::onBackground() +{ + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onButtonLongPress() { - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::AlignStickApplet::onButtonLongPress() +{ + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onExitLong() { - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::AlignStickApplet::onExitLong() +{ + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavUp() { - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavUp() +{ + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavDown() { - inkhud->rotateJoystick(2); // 180 deg - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavDown() +{ + inkhud->rotateJoystick(2); // 180 deg + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavLeft() { - inkhud->rotateJoystick(3); // 270 deg - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavLeft() +{ + inkhud->rotateJoystick(3); // 270 deg + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::AlignStickApplet::onNavRight() { - inkhud->rotateJoystick(1); // 90 deg - settings->joystick.aligned = true; +void InkHUD::AlignStickApplet::onNavRight() +{ + inkhud->rotateJoystick(1); // 90 deg + settings->joystick.aligned = true; - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h index 68893657a..8dba33165 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -15,32 +15,34 @@ and not aligned to the screen #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class AlignStickApplet : public SystemApplet { -public: - AlignStickApplet(); +class AlignStickApplet : public SystemApplet +{ + public: + AlignStickApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onButtonLongPress() override; - void onExitLong() override; - void onNavUp() override; - void onNavDown() override; - void onNavLeft() override; - void onNavRight() override; + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonLongPress() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; -protected: - enum Direction { - UP, - DOWN, - LEFT, - RIGHT, - }; + protected: + enum Direction { + UP, + DOWN, + LEFT, + RIGHT, + }; - void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); - void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); + void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index 64c878565..4f99d99ee 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -4,95 +4,98 @@ using namespace NicheGraphics; -InkHUD::BatteryIconApplet::BatteryIconApplet() { - // Show at boot, if user has previously enabled the feature - if (settings->optionalFeatures.batteryIcon) - bringToForeground(); +InkHUD::BatteryIconApplet::BatteryIconApplet() +{ + // Show at boot, if user has previously enabled the feature + if (settings->optionalFeatures.batteryIcon) + bringToForeground(); - // Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available - // This happens whether or not the battery icon feature is enabled - powerStatusObserver.observe(&powerStatus->onNewStatus); + // Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available + // This happens whether or not the battery icon feature is enabled + powerStatusObserver.observe(&powerStatus->onNewStatus); } // We handle power status' even when the feature is disabled, // so that we have up to date data ready if the feature is enabled later. // Otherwise could be 30s before new status update, with weird battery value displayed -int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) { - // System applets are always active - assert(isActive()); +int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) +{ + // System applets are always active + assert(isActive()); - // This method should only receive power statuses - // If we get a different type of status, something has gone weird elsewhere - assert(status->getStatusType() == STATUS_TYPE_POWER); + // This method should only receive power statuses + // If we get a different type of status, something has gone weird elsewhere + assert(status->getStatusType() == STATUS_TYPE_POWER); - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; + meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; - // Get the new state of charge %, and round to the nearest 10% - uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; + // Get the new state of charge %, and round to the nearest 10% + uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; - // If rounded value has changed, trigger a display update - // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() - // Don't trigger an update if the feature is disabled - if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon) - requestUpdate(); + // If rounded value has changed, trigger a display update + // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() + // Don't trigger an update if the feature is disabled + if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon) + requestUpdate(); - // Store the new value - this->socRounded = newSocRounded; + // Store the new value + this->socRounded = newSocRounded; - return 0; // Tell Observable to continue informing other observers + return 0; // Tell Observable to continue informing other observers } -void InkHUD::BatteryIconApplet::onRender() { - // Fill entire tile - // - size of icon controlled by size of tile - int16_t l = 0; - int16_t t = 0; - uint16_t w = width(); - int16_t h = height(); +void InkHUD::BatteryIconApplet::onRender() +{ + // Fill entire tile + // - size of icon controlled by size of tile + int16_t l = 0; + int16_t t = 0; + uint16_t w = width(); + int16_t h = height(); - // Clear the region beneath the tile - // Most applets are drawing onto an empty frame buffer and don't need to do this - // We do need to do this with the battery though, as it is an "overlay" - fillRect(l, t, w, h, WHITE); + // Clear the region beneath the tile + // Most applets are drawing onto an empty frame buffer and don't need to do this + // We do need to do this with the battery though, as it is an "overlay" + fillRect(l, t, w, h, WHITE); - // Vertical centerline - const int16_t m = t + (h / 2); + // Vertical centerline + const int16_t m = t + (h / 2); - // ===================== - // Draw battery outline - // ===================== + // ===================== + // Draw battery outline + // ===================== - // Positive terminal "bump" - const int16_t &bumpL = l; - const uint16_t bumpH = h / 2; - const int16_t bumpT = m - (bumpH / 2); - constexpr uint16_t bumpW = 2; - fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); + // Positive terminal "bump" + const int16_t &bumpL = l; + const uint16_t bumpH = h / 2; + const int16_t bumpT = m - (bumpH / 2); + constexpr uint16_t bumpW = 2; + fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); - // Main body of battery - const int16_t bodyL = bumpL + bumpW; - const int16_t &bodyT = t; - const int16_t &bodyH = h; - const int16_t bodyW = w - bumpW; - drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); + // Main body of battery + const int16_t bodyL = bumpL + bumpW; + const int16_t &bodyT = t; + const int16_t &bodyH = h; + const int16_t bodyW = w - bumpW; + drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); - // Erase join between bump and body - drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE); + // Erase join between bump and body + drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE); - // =================== - // Draw battery level - // =================== + // =================== + // Draw battery level + // =================== - constexpr int16_t slicePad = 2; - const int16_t sliceL = bodyL + slicePad; - const int16_t sliceT = bodyT + slicePad; - const uint16_t sliceH = bodyH - (slicePad * 2); - uint16_t sliceW = bodyW - (slicePad * 2); + constexpr int16_t slicePad = 2; + const int16_t sliceL = bodyL + slicePad; + const int16_t sliceT = bodyT + slicePad; + const uint16_t sliceH = bodyH - (slicePad * 2); + uint16_t sliceW = bodyW - (slicePad * 2); - sliceW = (sliceW * socRounded) / 100; // Apply percentage + sliceW = (sliceW * socRounded) / 100; // Apply percentage - hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); - drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); + hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); + drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h index 60fe29e8b..e5b4172be 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h @@ -15,21 +15,23 @@ It should be optional, enabled by the on-screen menu #include "PowerStatus.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class BatteryIconApplet : public SystemApplet { -public: - BatteryIconApplet(); +class BatteryIconApplet : public SystemApplet +{ + public: + BatteryIconApplet(); - void onRender() override; - int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available + void onRender() override; + int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available -private: - // Get informed when new information about the battery is available (via onPowerStatusUpdate method) - CallbackObserver powerStatusObserver = - CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); + private: + // Get informed when new information about the battery is available (via onPowerStatusUpdate method) + CallbackObserver powerStatusObserver = + CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); - uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10% + uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10% }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index 32e360af5..ecaa7cea3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -6,166 +6,172 @@ using namespace NicheGraphics; -InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") { - OSThread::setIntervalFromNow(8 * 1000UL); - OSThread::enabled = true; +InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") +{ + OSThread::setIntervalFromNow(8 * 1000UL); + OSThread::enabled = true; - // During onboarding, show the default short name as well as the version string - // This behavior assists manufacturers during mass production, and should not be modified without good reason - if (!settings->tips.safeShutdownSeen) { - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - fontTitle = fontMedium; - textLeft = xstr(APP_VERSION_SHORT); - textRight = parseShortName(ourNode); - textTitle = "Meshtastic"; - } else { - fontTitle = fontSmall; - textLeft = ""; - textRight = ""; - textTitle = xstr(APP_VERSION_SHORT); - } + // During onboarding, show the default short name as well as the version string + // This behavior assists manufacturers during mass production, and should not be modified without good reason + if (!settings->tips.safeShutdownSeen) { + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + fontTitle = fontMedium; + textLeft = xstr(APP_VERSION_SHORT); + textRight = parseShortName(ourNode); + textTitle = "Meshtastic"; + } else { + fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = xstr(APP_VERSION_SHORT); + } - bringToForeground(); - // This is then drawn with a FULL refresh by Renderer::begin + bringToForeground(); + // This is then drawn with a FULL refresh by Renderer::begin } -void InkHUD::LogoApplet::onRender() { - // Size of the region which the logo should "scale to fit" - uint16_t logoWLimit = X(0.8); - uint16_t logoHLimit = Y(0.5); +void InkHUD::LogoApplet::onRender() +{ + // Size of the region which the logo should "scale to fit" + uint16_t logoWLimit = X(0.8); + uint16_t logoHLimit = Y(0.5); - // Get the max width and height we can manage within the region, while still maintaining aspect ratio - uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); - uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); + // Get the max width and height we can manage within the region, while still maintaining aspect ratio + uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); + uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); - // Where to place the center of the logo - int16_t logoCX = X(0.5); - int16_t logoCY = Y(0.5 - 0.05); + // Where to place the center of the logo + int16_t logoCX = X(0.5); + int16_t logoCY = Y(0.5 - 0.05); - // Invert colors if black-on-white - // Used during shutdown, to resport display health - // Todo: handle this in InkHUD::Renderer instead - if (inverted) { - fillScreen(BLACK); - setTextColor(WHITE); - } + // Invert colors if black-on-white + // Used during shutdown, to resport display health + // Todo: handle this in InkHUD::Renderer instead + if (inverted) { + fillScreen(BLACK); + setTextColor(WHITE); + } #ifdef USERPREFS_OEM_IMAGE_DATA // Custom boot screen, if defined in userPrefs.jsonc - // Only show the custom screen at startup - // This allows us to draw the usual Meshtastic logo at shutdown - // The effect is similar to the two-stage userPrefs boot screen used by BaseUI - if (millis() < 10 * 1000UL) { + // Only show the custom screen at startup + // This allows us to draw the usual Meshtastic logo at shutdown + // The effect is similar to the two-stage userPrefs boot screen used by BaseUI + if (millis() < 10 * 1000UL) { - // Draw the custom logo - const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; - drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left - logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top - logo, // XBM data - USERPREFS_OEM_IMAGE_WIDTH, // Width - USERPREFS_OEM_IMAGE_HEIGHT, // Height - inverted ? WHITE : BLACK // Color - ); + // Draw the custom logo + const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; + drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left + logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top + logo, // XBM data + USERPREFS_OEM_IMAGE_WIDTH, // Width + USERPREFS_OEM_IMAGE_HEIGHT, // Height + inverted ? WHITE : BLACK // Color + ); - // Select the largest font which will still comfortably fit the custom text - setFont(fontLarge); - if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) - setFont(fontMedium); - if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) - setFont(fontSmall); + // Select the largest font which will still comfortably fit the custom text + setFont(fontLarge); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontMedium); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontSmall); - // Draw custom text below logo - int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo - printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); + // Draw custom text below logo + int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo + printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); - // Don't draw the normal boot screen, we've already drawn our custom version - return; - } + // Don't draw the normal boot screen, we've already drawn our custom version + return; + } #endif - drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); + drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); - if (!textLeft.empty()) { - setFont(fontSmall); - printAt(0, 0, textLeft, LEFT, TOP); - } + if (!textLeft.empty()) { + setFont(fontSmall); + printAt(0, 0, textLeft, LEFT, TOP); + } - if (!textRight.empty()) { - setFont(fontSmall); - printAt(X(1), 0, textRight, RIGHT, TOP); - } + if (!textRight.empty()) { + setFont(fontSmall); + printAt(X(1), 0, textRight, RIGHT, TOP); + } - if (!textTitle.empty()) { - int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo - setFont(fontTitle); - printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP); - } + if (!textTitle.empty()) { + int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo + setFont(fontTitle); + printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP); + } } -void InkHUD::LogoApplet::onForeground() { - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; - SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it. +void InkHUD::LogoApplet::onForeground() +{ + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; + SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it. } -void InkHUD::LogoApplet::onBackground() { - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; +void InkHUD::LogoApplet::onBackground() +{ + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // Begin displaying the screen which is shown at shutdown -void InkHUD::LogoApplet::onShutdown() { - bringToForeground(); +void InkHUD::LogoApplet::onShutdown() +{ + bringToForeground(); - textLeft = ""; - textRight = ""; - textTitle = "Shutting Down..."; - fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = "Shutting Down..."; + fontTitle = fontSmall; - // Draw a shutting down screen, twice. - // Once white on black, once black on white. - // Intention is to restore display health. + // Draw a shutting down screen, twice. + // Once white on black, once black on white. + // Intention is to restore display health. - inverted = true; - inkhud->forceUpdate(Drivers::EInk::FULL, false); - delay(1000); // Cooldown. Back to back updates aren't great for health. - inverted = false; - inkhud->forceUpdate(Drivers::EInk::FULL, false); - delay(1000); // Cooldown + inverted = true; + inkhud->forceUpdate(Drivers::EInk::FULL, false); + delay(1000); // Cooldown. Back to back updates aren't great for health. + inverted = false; + inkhud->forceUpdate(Drivers::EInk::FULL, false); + delay(1000); // Cooldown - // Prepare for the powered-off screen now - // We can change these values because the initial "shutting down" screen has already rendered at this point - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - textLeft = ""; - textRight = ""; - textTitle = parseShortName(ourNode); - fontTitle = fontMedium; + // Prepare for the powered-off screen now + // We can change these values because the initial "shutting down" screen has already rendered at this point + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + textLeft = ""; + textRight = ""; + textTitle = parseShortName(ourNode); + fontTitle = fontMedium; - // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is - // complete + // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete } -void InkHUD::LogoApplet::onReboot() { - bringToForeground(); +void InkHUD::LogoApplet::onReboot() +{ + bringToForeground(); - textLeft = ""; - textRight = ""; - textTitle = "Rebooting..."; - fontTitle = fontSmall; + textLeft = ""; + textRight = ""; + textTitle = "Rebooting..."; + fontTitle = fontSmall; - inkhud->forceUpdate(Drivers::EInk::FULL, false); - // Perform the update right now, waiting here until complete + inkhud->forceUpdate(Drivers::EInk::FULL, false); + // Perform the update right now, waiting here until complete } -int32_t InkHUD::LogoApplet::runOnce() { - sendToBackground(); - return OSThread::disable(); +int32_t InkHUD::LogoApplet::runOnce() +{ + sendToBackground(); + return OSThread::disable(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h index 7b0b0b377..3f604baed 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -14,25 +14,27 @@ #include "concurrency/OSThread.h" #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class LogoApplet : public SystemApplet, public concurrency::OSThread { -public: - LogoApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onShutdown() override; - void onReboot() override; +class LogoApplet : public SystemApplet, public concurrency::OSThread +{ + public: + LogoApplet(); + void onRender() override; + void onForeground() override; + void onBackground() override; + void onShutdown() override; + void onReboot() override; -protected: - int32_t runOnce() override; + protected: + int32_t runOnce() override; - std::string textLeft; - std::string textRight; - std::string textTitle; - AppletFont fontTitle; - bool inverted = false; // Invert colors. Used during shutdown, to restore display health. + std::string textLeft; + std::string textRight; + std::string textTitle; + AppletFont fontTitle; + bool inverted = false; // Invert colors. Used during shutdown, to restore display health. }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index 7ef655d9d..debe2b719 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -13,28 +13,29 @@ Behaviors assigned in MenuApplet::execute #include "configuration.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ enum MenuAction { - NO_ACTION, - SEND_PING, - STORE_CANNEDMESSAGE_SELECTION, - SEND_CANNEDMESSAGE, - SHUTDOWN, - NEXT_TILE, - TOGGLE_BACKLIGHT, - TOGGLE_GPS, - ENABLE_BLUETOOTH, - TOGGLE_APPLET, - TOGGLE_AUTOSHOW_APPLET, - SET_RECENTS, - ROTATE, - ALIGN_JOYSTICK, - LAYOUT, - TOGGLE_BATTERY_ICON, - TOGGLE_NOTIFICATIONS, - TOGGLE_INVERT_COLOR, - TOGGLE_12H_CLOCK, + NO_ACTION, + SEND_PING, + STORE_CANNEDMESSAGE_SELECTION, + SEND_CANNEDMESSAGE, + SHUTDOWN, + NEXT_TILE, + TOGGLE_BACKLIGHT, + TOGGLE_GPS, + ENABLE_BLUETOOTH, + TOGGLE_APPLET, + TOGGLE_AUTOSHOW_APPLET, + SET_RECENTS, + ROTATE, + ALIGN_JOYSTICK, + LAYOUT, + TOGGLE_BATTERY_ICON, + TOGGLE_NOTIFICATIONS, + TOGGLE_INVERT_COLOR, + TOGGLE_12H_CLOCK, }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 1599456b2..7e7093857 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -22,829 +22,856 @@ static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu // These are offered to users as possible values for settings.recentlyActiveSeconds static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; -InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") { - // No timer tasks at boot - OSThread::disable(); +InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") +{ + // No timer tasks at boot + OSThread::disable(); - // Note: don't get instance if we're not actually using the backlight, - // or else you will unintentionally instantiate it - if (settings->optionalMenuItems.backlight) { - backlight = Drivers::LatchingBacklight::getInstance(); - } + // Note: don't get instance if we're not actually using the backlight, + // or else you will unintentionally instantiate it + if (settings->optionalMenuItems.backlight) { + backlight = Drivers::LatchingBacklight::getInstance(); + } - // Initialize the Canned Message store - // This is a shared nicheGraphics component - // - handles loading & parsing the canned messages - // - handles setting / getting of canned messages via apps (Client API Admin Messages) - cm.store = CannedMessageStore::getInstance(); + // Initialize the Canned Message store + // This is a shared nicheGraphics component + // - handles loading & parsing the canned messages + // - handles setting / getting of canned messages via apps (Client API Admin Messages) + cm.store = CannedMessageStore::getInstance(); } -void InkHUD::MenuApplet::onForeground() { - // We do need this before we render, but we can optimize by just calculating it once now - systemInfoPanelHeight = getSystemInfoPanelHeight(); +void InkHUD::MenuApplet::onForeground() +{ + // We do need this before we render, but we can optimize by just calculating it once now + systemInfoPanelHeight = getSystemInfoPanelHeight(); - // Display initial menu page - showPage(MenuPage::ROOT); + // Display initial menu page + showPage(MenuPage::ROOT); - // If device has a backlight which isn't controlled by aux button: - // backlight on always when menu opens. - // Courtesy to T-Echo users who removed the capacitive touch button - if (settings->optionalMenuItems.backlight) { - assert(backlight); - if (!backlight->isOn()) - backlight->peek(); - } + // If device has a backlight which isn't controlled by aux button: + // backlight on always when menu opens. + // Courtesy to T-Echo users who removed the capacitive touch button + if (settings->optionalMenuItems.backlight) { + assert(backlight); + if (!backlight->isOn()) + backlight->peek(); + } - // Prevent user applets requesting update while menu is open - // Handle button input with this applet - SystemApplet::lockRequests = true; - SystemApplet::handleInput = true; + // Prevent user applets requesting update while menu is open + // Handle button input with this applet + SystemApplet::lockRequests = true; + SystemApplet::handleInput = true; - // Begin the auto-close timeout - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - OSThread::enabled = true; + // Begin the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; - // Upgrade the refresh to FAST, for guaranteed responsiveness - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Upgrade the refresh to FAST, for guaranteed responsiveness + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } -void InkHUD::MenuApplet::onBackground() { - // Discard any data we generated while selecting a canned message - // Frees heap mem - freeCannedMessageResources(); +void InkHUD::MenuApplet::onBackground() +{ + // Discard any data we generated while selecting a canned message + // Frees heap mem + freeCannedMessageResources(); - // If device has a backlight which isn't controlled by aux button: - // Item in options submenu allows keeping backlight on after menu is closed - // If this item is deselected we will turn backlight off again, now that menu is closing - if (settings->optionalMenuItems.backlight) { - assert(backlight); - if (!backlight->isLatched()) - backlight->off(); - } + // If device has a backlight which isn't controlled by aux button: + // Item in options submenu allows keeping backlight on after menu is closed + // If this item is deselected we will turn backlight off again, now that menu is closing + if (settings->optionalMenuItems.backlight) { + assert(backlight); + if (!backlight->isLatched()) + backlight->off(); + } - // Stop the auto-timeout - OSThread::disable(); + // Stop the auto-timeout + OSThread::disable(); - // Resume normal rendering and button behavior of user applets - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; + // Resume normal rendering and button behavior of user applets + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Restore the user applet whose tile we borrowed - if (borrowedTileOwner) - borrowedTileOwner->bringToForeground(); - Tile *t = getTile(); - t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one) - borrowedTileOwner = nullptr; + // Restore the user applet whose tile we borrowed + if (borrowedTileOwner) + borrowedTileOwner->bringToForeground(); + Tile *t = getTile(); + t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one) + borrowedTileOwner = nullptr; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Open the menu // Parameter specifies which user-tile the menu will use // The user applet originally on this tile will be restored when the menu closes -void InkHUD::MenuApplet::show(Tile *t) { - // Remember who *really* owns this tile - borrowedTileOwner = t->getAssignedApplet(); +void InkHUD::MenuApplet::show(Tile *t) +{ + // Remember who *really* owns this tile + borrowedTileOwner = t->getAssignedApplet(); - // Hide the owner, if it is a valid applet - if (borrowedTileOwner) - borrowedTileOwner->sendToBackground(); + // Hide the owner, if it is a valid applet + if (borrowedTileOwner) + borrowedTileOwner->sendToBackground(); - // Break the owner's link with tile - // Relink it to menu applet - t->assignApplet(this); + // Break the owner's link with tile + // Relink it to menu applet + t->assignApplet(this); - // Show menu - bringToForeground(); + // Show menu + bringToForeground(); } // Auto-exit the menu applet after a period of inactivity // The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open. // By exiting the menu, we prevent users mistakenly believing that the data will update. -int32_t InkHUD::MenuApplet::runOnce() { - // runOnce's interval is pushed back when a button is pressed - // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC, - // so we close the menu. - showPage(EXIT); +int32_t InkHUD::MenuApplet::runOnce() +{ + // runOnce's interval is pushed back when a button is pressed + // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC, + // so we close the menu. + showPage(EXIT); - // Timer should disable after firing - // This is redundant, as onBackground() will also disable - return OSThread::disable(); + // Timer should disable after firing + // This is redundant, as onBackground() will also disable + return OSThread::disable(); } // Perform action for a menu item, then change page // Behaviors for MenuActions are defined here -void InkHUD::MenuApplet::execute(MenuItem item) { - // Perform an action - // ------------------ - switch (item.action) { +void InkHUD::MenuApplet::execute(MenuItem item) +{ + // Perform an action + // ------------------ + switch (item.action) { - // Open a submenu without performing any action - // Also handles exit - case NO_ACTION: - break; + // Open a submenu without performing any action + // Also handles exit + case NO_ACTION: + break; - case NEXT_TILE: - inkhud->nextTile(); - break; + case NEXT_TILE: + inkhud->nextTile(); + break; - case SEND_PING: - service->refreshLocalMeshNode(); - service->trySendPosition(NODENUM_BROADCAST, true); + case SEND_PING: + service->refreshLocalMeshNode(); + service->trySendPosition(NODENUM_BROADCAST, true); - // Force the next refresh to use FULL, to protect the display, as some users will probably spam this button - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); - break; + // Force the next refresh to use FULL, to protect the display, as some users will probably spam this button + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); + break; - case STORE_CANNEDMESSAGE_SELECTION: - cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry - break; + case STORE_CANNEDMESSAGE_SELECTION: + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + break; - case SEND_CANNEDMESSAGE: - cm.selectedRecipientItem = &cm.recipientItems.at(cursor); - sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here - break; + case SEND_CANNEDMESSAGE: + cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here + break; - case ROTATE: - inkhud->rotate(); - break; + case ROTATE: + inkhud->rotate(); + break; - case ALIGN_JOYSTICK: - inkhud->openAlignStick(); - break; + case ALIGN_JOYSTICK: + inkhud->openAlignStick(); + break; - case LAYOUT: - // Todo: smarter incrementing of tile count - settings->userTiles.count++; + case LAYOUT: + // Todo: smarter incrementing of tile count + settings->userTiles.count++; - if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet - settings->userTiles.count++; + if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet + settings->userTiles.count++; - if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high - settings->userTiles.count = 1; + if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high + settings->userTiles.count = 1; - inkhud->updateLayout(); - break; + inkhud->updateLayout(); + break; - case TOGGLE_APPLET: - settings->userApplets.active[cursor] = !settings->userApplets.active[cursor]; - inkhud->updateAppletSelection(); - break; + case TOGGLE_APPLET: + settings->userApplets.active[cursor] = !settings->userApplets.active[cursor]; + inkhud->updateAppletSelection(); + break; - case TOGGLE_AUTOSHOW_APPLET: - // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() - *items.at(cursor).checkState = !(*items.at(cursor).checkState); - break; + case TOGGLE_AUTOSHOW_APPLET: + // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() + *items.at(cursor).checkState = !(*items.at(cursor).checkState); + break; - case TOGGLE_NOTIFICATIONS: - settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; - break; + case TOGGLE_NOTIFICATIONS: + settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; + break; - case TOGGLE_INVERT_COLOR: - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - else - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + case TOGGLE_INVERT_COLOR: + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + else + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; - nodeDB->saveToDisk(SEGMENT_CONFIG); - break; + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; - case SET_RECENTS: - // Set value of settings.recentlyActiveSeconds - // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) - assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); - settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes - break; + case SET_RECENTS: + // Set value of settings.recentlyActiveSeconds + // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) + assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); + settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes + break; - case SHUTDOWN: - LOG_INFO("Shutting down from menu"); - shutdownAtMsec = millis(); - // Menu is then sent to background via onShutdown - break; + case SHUTDOWN: + LOG_INFO("Shutting down from menu"); + shutdownAtMsec = millis(); + // Menu is then sent to background via onShutdown + break; - case TOGGLE_BATTERY_ICON: - inkhud->toggleBatteryIcon(); - break; + case TOGGLE_BATTERY_ICON: + inkhud->toggleBatteryIcon(); + break; - case TOGGLE_BACKLIGHT: - // Note: backlight is already on in this situation - // We're marking that it should *remain* on once menu closes - assert(backlight); - if (backlight->isLatched()) - backlight->off(); - else - backlight->latch(); - break; + case TOGGLE_BACKLIGHT: + // Note: backlight is already on in this situation + // We're marking that it should *remain* on once menu closes + assert(backlight); + if (backlight->isLatched()) + backlight->off(); + else + backlight->latch(); + break; - case TOGGLE_12H_CLOCK: - config.display.use_12h_clock = !config.display.use_12h_clock; - nodeDB->saveToDisk(SEGMENT_CONFIG); - break; + case TOGGLE_12H_CLOCK: + config.display.use_12h_clock = !config.display.use_12h_clock; + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; - case TOGGLE_GPS: - gps->toggleGpsMode(); - nodeDB->saveToDisk(SEGMENT_CONFIG); - break; + case TOGGLE_GPS: + gps->toggleGpsMode(); + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; - case ENABLE_BLUETOOTH: - // This helps users recover from a bad wifi config - LOG_INFO("Enabling Bluetooth"); - config.network.wifi_enabled = false; - config.bluetooth.enabled = true; - nodeDB->saveToDisk(); - rebootAtMsec = millis() + 2000; - break; + case ENABLE_BLUETOOTH: + // This helps users recover from a bad wifi config + LOG_INFO("Enabling Bluetooth"); + config.network.wifi_enabled = false; + config.bluetooth.enabled = true; + nodeDB->saveToDisk(); + rebootAtMsec = millis() + 2000; + break; - default: - LOG_WARN("Action not implemented"); - } + default: + LOG_WARN("Action not implemented"); + } - // Move to next page, as defined for the MenuItem - showPage(item.nextPage); + // Move to next page, as defined for the MenuItem + showPage(item.nextPage); } // Display a new page of MenuItems // May reload same page, or exit menu applet entirely // Fills the MenuApplet::items vector -void InkHUD::MenuApplet::showPage(MenuPage page) { - items.clear(); - items.shrink_to_fit(); +void InkHUD::MenuApplet::showPage(MenuPage page) +{ + items.clear(); + items.shrink_to_fit(); - switch (page) { - case ROOT: - // Optional: next applet - if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) - items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown + switch (page) { + case ROOT: + // Optional: next applet + if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) + items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown - items.push_back(MenuItem("Send", MenuPage::SEND)); - items.push_back(MenuItem("Options", MenuPage::OPTIONS)); - // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO - items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::EXIT; - break; + items.push_back(MenuItem("Send", MenuPage::SEND)); + items.push_back(MenuItem("Options", MenuPage::OPTIONS)); + // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO + items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::EXIT; + break; - case SEND: - populateSendPage(); - previousPage = MenuPage::ROOT; - break; + case SEND: + populateSendPage(); + previousPage = MenuPage::ROOT; + break; - case CANNEDMESSAGE_RECIPIENT: - populateRecipientPage(); - previousPage = MenuPage::OPTIONS; - break; + case CANNEDMESSAGE_RECIPIENT: + populateRecipientPage(); + previousPage = MenuPage::OPTIONS; + break; - case OPTIONS: - // Optional: backlight - if (settings->optionalMenuItems.backlight) - items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label - MenuAction::TOGGLE_BACKLIGHT, // Action - MenuPage::EXIT // Exit once complete - )); + case OPTIONS: + // Optional: backlight + if (settings->optionalMenuItems.backlight) + items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label + MenuAction::TOGGLE_BACKLIGHT, // Action + MenuPage::EXIT // Exit once complete + )); - // Optional: GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) - items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); + // Optional: GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) + items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - // Optional: Enable Bluetooth, in case of lost wifi connection - if (!config.bluetooth.enabled || config.network.wifi_enabled) - items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT)); + // Optional: Enable Bluetooth, in case of lost wifi connection + if (!config.bluetooth.enabled || config.network.wifi_enabled) + items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT)); - items.push_back(MenuItem("Applets", MenuPage::APPLETS)); - items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); - items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); - if (settings->userTiles.maxCount > 1) - items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); - items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); - if (settings->joystick.enabled) - items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); - items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); - items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings->optionalFeatures.batteryIcon)); + items.push_back(MenuItem("Applets", MenuPage::APPLETS)); + items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); + items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); + if (settings->userTiles.maxCount > 1) + items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); + items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); + if (settings->joystick.enabled) + items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); + items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, + &settings->optionalFeatures.notifications)); + items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, + &settings->optionalFeatures.batteryIcon)); - invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); + invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); - items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::ROOT; - break; + items.push_back( + MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::ROOT; + break; - case APPLETS: - populateAppletPage(); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::OPTIONS; - break; + case APPLETS: + populateAppletPage(); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; + break; - case AUTOSHOW: - populateAutoshowPage(); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::OPTIONS; - break; + case AUTOSHOW: + populateAutoshowPage(); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; + break; - case RECENTS: - populateRecentsPage(); - previousPage = MenuPage::OPTIONS; - break; + case RECENTS: + populateRecentsPage(); + previousPage = MenuPage::OPTIONS; + break; - case EXIT: - sendToBackground(); // Menu applet dismissed, allow normal behavior to resume - break; + case EXIT: + sendToBackground(); // Menu applet dismissed, allow normal behavior to resume + break; - default: - LOG_WARN("Page not implemented"); - } - - // Reset the cursor, unless reloading same page - // (or now out-of-bounds) - if (page != currentPage || cursor >= items.size()) { - cursor = 0; - - // ROOT menu has special handling: unselected at first, to emphasise the system info panel - if (page == ROOT) - cursorShown = false; - } - - // Remember which page we are on now - currentPage = page; -} - -void InkHUD::MenuApplet::onRender() { - if (items.size() == 0) - LOG_ERROR("Empty Menu"); - - // Dimensions for the slots where we will draw menuItems - const float padding = 0.05; - const uint16_t itemH = fontSmall.lineHeight() * 2; - const int16_t itemW = width() - X(padding) - X(padding); - const int16_t itemL = X(padding); - const int16_t itemR = X(1 - padding); - int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu. - - // How many full menuItems will fit on screen - uint8_t slotCount = (height() - itemT) / itemH; - - // System info panel at the top of the menu - // ========================================= - - uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground - const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel - - // System info - top - // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen. - // This is the same behavior we expect from the non-root menus. - // Implementing this with the systemp panel is slightly annoying though, - // and required adding the MenuApplet::getSystemInfoPanelHeight method - int16_t siT; - if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count) - siT = 0; - else - siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH); - - // If showing ROOT menu, - // and the panel isn't yet scrolled off screen top - if (currentPage == ROOT) { - drawSystemInfoPanel(0, siT, width()); // Draw the panel. - itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel - } - - // Draw menu items - // =================== - - // Which item will be drawn to the top-most slot? - // Initially, this is the item 0, but may increase once we begin scrolling - uint8_t firstItem; - if (cursor < slotCount) - firstItem = 0; - else - firstItem = cursor - (slotCount - 1); - - // Which item will be drawn to the bottom-most slot? - // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow - // This may be less than the slot-count, if we are reaching the end of the menuItems - uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1); - - // -- Loop: draw each (visible) menu item -- - for (uint8_t i = firstItem; i <= lastItem; i++) { - // Grab the menuItem - MenuItem item = items.at(i); - - // Center-line for the text - int16_t center = itemT + (itemH / 2); - - // Box, if currently selected - if (cursorShown && i == cursor) - drawRect(itemL, itemT, itemW, itemH, BLACK); - - // Item's text - printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); - - // Checkbox, if relevant - if (item.checkState) { - const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height - const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left - const int16_t cbT = center - (cbWH / 2); // Checkbox : top - // Checkbox ticked - if (*(item.checkState)) { - drawRect(cbL, cbT, cbWH, cbWH, BLACK); - // First point of tick: pen down - const int16_t t1Y = center; - const int16_t t1X = cbL + 3; - // Second point of tick: base - const int16_t t2Y = center + (cbWH / 2) - 2; - const int16_t t2X = cbL + (cbWH / 2); - // Third point of tick: end of tail - const int16_t t3Y = center - (cbWH / 2) - 2; - const int16_t t3X = cbL + cbWH + 2; - // Draw twice: faux bold - drawLine(t1X, t1Y, t2X, t2Y, BLACK); - drawLine(t2X, t2Y, t3X, t3Y, BLACK); - drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK); - drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK); - } - // Checkbox ticked - else - drawRect(cbL, cbT, cbWH, cbWH, BLACK); + default: + LOG_WARN("Page not implemented"); } - // Increment the y value (top) as we go - itemT += itemH; - } + // Reset the cursor, unless reloading same page + // (or now out-of-bounds) + if (page != currentPage || cursor >= items.size()) { + cursor = 0; + + // ROOT menu has special handling: unselected at first, to emphasise the system info panel + if (page == ROOT) + cursorShown = false; + } + + // Remember which page we are on now + currentPage = page; } -void InkHUD::MenuApplet::onButtonShortPress() { - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); +void InkHUD::MenuApplet::onRender() +{ + if (items.size() == 0) + LOG_ERROR("Empty Menu"); + + // Dimensions for the slots where we will draw menuItems + const float padding = 0.05; + const uint16_t itemH = fontSmall.lineHeight() * 2; + const int16_t itemW = width() - X(padding) - X(padding); + const int16_t itemL = X(padding); + const int16_t itemR = X(1 - padding); + int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu. + + // How many full menuItems will fit on screen + uint8_t slotCount = (height() - itemT) / itemH; + + // System info panel at the top of the menu + // ========================================= + + uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground + const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel + + // System info - top + // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen. + // This is the same behavior we expect from the non-root menus. + // Implementing this with the systemp panel is slightly annoying though, + // and required adding the MenuApplet::getSystemInfoPanelHeight method + int16_t siT; + if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count) + siT = 0; + else + siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH); + + // If showing ROOT menu, + // and the panel isn't yet scrolled off screen top + if (currentPage == ROOT) { + drawSystemInfoPanel(0, siT, width()); // Draw the panel. + itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel + } + + // Draw menu items + // =================== + + // Which item will be drawn to the top-most slot? + // Initially, this is the item 0, but may increase once we begin scrolling + uint8_t firstItem; + if (cursor < slotCount) + firstItem = 0; + else + firstItem = cursor - (slotCount - 1); + + // Which item will be drawn to the bottom-most slot? + // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow + // This may be less than the slot-count, if we are reaching the end of the menuItems + uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1); + + // -- Loop: draw each (visible) menu item -- + for (uint8_t i = firstItem; i <= lastItem; i++) { + // Grab the menuItem + MenuItem item = items.at(i); + + // Center-line for the text + int16_t center = itemT + (itemH / 2); + + // Box, if currently selected + if (cursorShown && i == cursor) + drawRect(itemL, itemT, itemW, itemH, BLACK); + + // Item's text + printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + + // Checkbox, if relevant + if (item.checkState) { + const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height + const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left + const int16_t cbT = center - (cbWH / 2); // Checkbox : top + // Checkbox ticked + if (*(item.checkState)) { + drawRect(cbL, cbT, cbWH, cbWH, BLACK); + // First point of tick: pen down + const int16_t t1Y = center; + const int16_t t1X = cbL + 3; + // Second point of tick: base + const int16_t t2Y = center + (cbWH / 2) - 2; + const int16_t t2X = cbL + (cbWH / 2); + // Third point of tick: end of tail + const int16_t t3Y = center - (cbWH / 2) - 2; + const int16_t t3X = cbL + cbWH + 2; + // Draw twice: faux bold + drawLine(t1X, t1Y, t2X, t2Y, BLACK); + drawLine(t2X, t2Y, t3X, t3Y, BLACK); + drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK); + drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK); + } + // Checkbox ticked + else + drawRect(cbL, cbT, cbWH, cbWH, BLACK); + } + + // Increment the y value (top) as we go + itemT += itemH; + } +} + +void InkHUD::MenuApplet::onButtonShortPress() +{ + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (!settings->joystick.enabled) { + // Move menu cursor to next entry, then update + if (cursorShown) + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } else { + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onButtonLongPress() +{ + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + + // If we didn't already request a specialized update, when handling a menu action, + // then perform the usual fast update. + // FAST keeps things responsive: important because we're dealing with user input + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onExitShort() +{ + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavUp() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to previous entry, then update + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + + if (!cursorShown) + cursorShown = true; + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavDown() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (!settings->joystick.enabled) { // Move menu cursor to next entry, then update if (cursorShown) - cursor = (cursor + 1) % items.size(); + cursor = (cursor + 1) % items.size(); else - cursorShown = true; + cursorShown = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); - } else { +} + +void InkHUD::MenuApplet::onNavLeft() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Go to the previous menu page + showPage(previousPage); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavRight() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); + execute(items.at(cursor)); if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); - } -} - -void InkHUD::MenuApplet::onButtonLongPress() { - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close - - // If we didn't already request a specialized update, when handling a menu action, - // then perform the usual fast update. - // FAST keeps things responsive: important because we're dealing with user input - if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onExitShort() { - // Exit the menu - showPage(MenuPage::EXIT); - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavUp() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - // Move menu cursor to previous entry, then update - if (cursor == 0) - cursor = items.size() - 1; - else - cursor--; - - if (!cursorShown) - cursorShown = true; - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavDown() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - // Move menu cursor to next entry, then update - if (cursorShown) - cursor = (cursor + 1) % items.size(); - else - cursorShown = true; - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavLeft() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - // Go to the previous menu page - showPage(previousPage); - requestUpdate(Drivers::EInk::UpdateTypes::FAST); -} - -void InkHUD::MenuApplet::onNavRight() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - - if (cursorShown) - execute(items.at(cursor)); - if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } // Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu -void InkHUD::MenuApplet::populateAppletPage() { - assert(items.size() == 0); +void InkHUD::MenuApplet::populateAppletPage() +{ + assert(items.size() == 0); - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - const char *name = inkhud->userApplets.at(i)->name; - bool *isActive = &(settings->userApplets.active[i]); - items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive)); - } + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + const char *name = inkhud->userApplets.at(i)->name; + bool *isActive = &(settings->userApplets.active[i]); + items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive)); + } } -// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have -// new data We only populate this menu page with applets which are actually active We use the MenuItem::checkState -// pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient. -void InkHUD::MenuApplet::populateAutoshowPage() { - assert(items.size() == 0); +// Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data +// We only populate this menu page with applets which are actually active +// We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient. +void InkHUD::MenuApplet::populateAutoshowPage() +{ + assert(items.size() == 0); - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - // Only add a menu item if applet is active - if (settings->userApplets.active[i]) { - const char *name = inkhud->userApplets.at(i)->name; - bool *isActive = &(settings->userApplets.autoshow[i]); - items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive)); + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + // Only add a menu item if applet is active + if (settings->userApplets.active[i]) { + const char *name = inkhud->userApplets.at(i)->name; + bool *isActive = &(settings->userApplets.autoshow[i]); + items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive)); + } } - } } // Create MenuItem entries to select our definition of "Recent" // Controls how long data will remain in any "Recents" flavored applets -void InkHUD::MenuApplet::populateRecentsPage() { - // How many values are shown for use to choose from - constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); +void InkHUD::MenuApplet::populateRecentsPage() +{ + // How many values are shown for use to choose from + constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); - // Create an entry for each item in RECENTS_OPTIONS_MINUTES array - // (Defined at top of this file) - for (uint8_t i = 0; i < optionCount; i++) { - std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; - items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT)); - } + // Create an entry for each item in RECENTS_OPTIONS_MINUTES array + // (Defined at top of this file) + for (uint8_t i = 0; i < optionCount; i++) { + std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; + items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT)); + } } // MenuItem entries for the "send" page // Dynamically creates menu items based on available canned messages -void InkHUD::MenuApplet::populateSendPage() { - // Position / NodeInfo packet - items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); +void InkHUD::MenuApplet::populateSendPage() +{ + // Position / NodeInfo packet + items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); - // One menu item for each canned message - uint8_t count = cm.store->size(); - for (uint8_t i = 0; i < count; i++) { - // Gather the information for this item - CannedMessages::MessageItem messageItem; - messageItem.rawText = cm.store->at(i); - messageItem.label = parse(messageItem.rawText); + // One menu item for each canned message + uint8_t count = cm.store->size(); + for (uint8_t i = 0; i < count; i++) { + // Gather the information for this item + CannedMessages::MessageItem messageItem; + messageItem.rawText = cm.store->at(i); + messageItem.label = parse(messageItem.rawText); - // Store the item (until the menu closes) - cm.messageItems.push_back(messageItem); + // Store the item (until the menu closes) + cm.messageItems.push_back(messageItem); - // Create a menu item - const char *itemText = cm.messageItems.back().label.c_str(); - items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); - } + // Create a menu item + const char *itemText = cm.messageItems.back().label.c_str(); + items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); + } - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); } // Dynamically create MenuItem entries for possible canned message destinations // All available channels are shown // Favorite nodes are shown, provided we don't have an *excessive* amount -void InkHUD::MenuApplet::populateRecipientPage() { - // Create recipient data (and menu items) for any channels - // -------------------------------------------------------- +void InkHUD::MenuApplet::populateRecipientPage() +{ + // Create recipient data (and menu items) for any channels + // -------------------------------------------------------- - for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { - // Get the channel, and check if it's enabled - meshtastic_Channel &channel = channels.getByIndex(i); - if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) - continue; + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + // Get the channel, and check if it's enabled + meshtastic_Channel &channel = channels.getByIndex(i); + if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) + continue; - CannedMessages::RecipientItem r; + CannedMessages::RecipientItem r; - // Set index - r.channelIndex = channel.index; + // Set index + r.channelIndex = channel.index; - // Set a label for the menu item - r.label = "Ch " + to_string(i) + ": "; - if (channel.role == meshtastic_Channel_Role_PRIMARY) - r.label += "Primary"; - else - r.label += parse(channel.settings.name); + // Set a label for the menu item + r.label = "Ch " + to_string(i) + ": "; + if (channel.role == meshtastic_Channel_Role_PRIMARY) + r.label += "Primary"; + else + r.label += parse(channel.settings.name); - // Add to the list of recipients - cm.recipientItems.push_back(r); + // Add to the list of recipients + cm.recipientItems.push_back(r); - // Add a menu item for this recipient - const char *itemText = cm.recipientItems.back().label.c_str(); - items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); - } - - // Create recipient data (and menu items) for favorite nodes - // --------------------------------------------------------- - - uint32_t nodeCount = nodeDB->getNumMeshNodes(); - uint32_t favoriteCount = 0; - - // Count favorites - for (uint32_t i = 0; i < nodeCount; i++) { - if (nodeDB->getMeshNodeByIndex(i)->is_favorite) - favoriteCount++; - } - - // Only add favorites if the number is reasonable - // Don't want some monstrous list that takes 100 clicks to reach exit - if (favoriteCount < 20) { - for (uint32_t i = 0; i < nodeCount; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - - // Skip node if not a favorite - if (!node->is_favorite) - continue; - - CannedMessages::RecipientItem r; - - r.dest = node->num; - r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) - - // Set a label for the menu item - r.label = "DM: "; - if (node->has_user) - r.label += parse(node->user.long_name); - else - r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? - - // Add to the list of recipients - cm.recipientItems.push_back(r); - - // Add a menu item for this recipient - const char *itemText = cm.recipientItems.back().label.c_str(); - items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); } - } - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + // Create recipient data (and menu items) for favorite nodes + // --------------------------------------------------------- + + uint32_t nodeCount = nodeDB->getNumMeshNodes(); + uint32_t favoriteCount = 0; + + // Count favorites + for (uint32_t i = 0; i < nodeCount; i++) { + if (nodeDB->getMeshNodeByIndex(i)->is_favorite) + favoriteCount++; + } + + // Only add favorites if the number is reasonable + // Don't want some monstrous list that takes 100 clicks to reach exit + if (favoriteCount < 20) { + for (uint32_t i = 0; i < nodeCount; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip node if not a favorite + if (!node->is_favorite) + continue; + + CannedMessages::RecipientItem r; + + r.dest = node->num; + r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) + + // Set a label for the menu item + r.label = "DM: "; + if (node->has_user) + r.label += parse(node->user.long_name); + else + r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); } // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. -void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight) { - // Reset the height - // We'll add to this as we add elements - uint16_t height = 0; +void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight) +{ + // Reset the height + // We'll add to this as we add elements + uint16_t height = 0; - // Clock (potentially) - // ==================== - std::string clockString = getTimeString(); - if (clockString.length() > 0) { - setFont(fontMedium); - printAt(width / 2, top, clockString, CENTER, TOP); + // Clock (potentially) + // ==================== + std::string clockString = getTimeString(); + if (clockString.length() > 0) { + setFont(fontMedium); + printAt(width / 2, top, clockString, CENTER, TOP); - height += fontMedium.lineHeight(); - height += fontMedium.lineHeight() * 0.1; // Padding below clock - } + height += fontMedium.lineHeight(); + height += fontMedium.lineHeight() * 0.1; // Padding below clock + } - // Stats - // =================== + // Stats + // =================== - setFont(fontSmall); + setFont(fontSmall); - // Position of the label row for the system info - const int16_t labelT = top + height; - height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing + // Position of the label row for the system info + const int16_t labelT = top + height; + height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing - // Position of the data row for the system info - const int16_t valT = top + height; - height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider) + // Position of the data row for the system info + const int16_t valT = top + height; + height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider) - // Position of divider between the info panel and the menu entries - const int16_t divY = top + height; - height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) + // Position of divider between the info panel and the menu entries + const int16_t divY = top + height; + height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) - // Create a variable number of columns - // Either 3 or 4, depending on whether we have GPS - // Todo - constexpr uint8_t N_COL = 3; - int16_t colL[N_COL]; - int16_t colC[N_COL]; - int16_t colR[N_COL]; - for (uint8_t i = 0; i < N_COL; i++) { - colL[i] = left + ((width / N_COL) * i); - colC[i] = colL[i] + ((width / N_COL) / 2); - colR[i] = colL[i] + (width / N_COL); - } + // Create a variable number of columns + // Either 3 or 4, depending on whether we have GPS + // Todo + constexpr uint8_t N_COL = 3; + int16_t colL[N_COL]; + int16_t colC[N_COL]; + int16_t colR[N_COL]; + for (uint8_t i = 0; i < N_COL; i++) { + colL[i] = left + ((width / N_COL) * i); + colC[i] = colL[i] + ((width / N_COL) / 2); + colR[i] = colL[i] + (width / N_COL); + } - // Info blocks, left to right + // Info blocks, left to right - // Voltage - float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; - char voltageStr[6]; // "XX.XV" - sprintf(voltageStr, "%.2fV", voltage); - printAt(colC[0], labelT, "Bat", CENTER, TOP); - printAt(colC[0], valT, voltageStr, CENTER, TOP); + // Voltage + float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; + char voltageStr[6]; // "XX.XV" + sprintf(voltageStr, "%.2fV", voltage); + printAt(colC[0], labelT, "Bat", CENTER, TOP); + printAt(colC[0], valT, voltageStr, CENTER, TOP); - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[0], y, BLACK); + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[0], y, BLACK); - // Channel Util - char chUtilStr[4]; // "XX%" - sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); - printAt(colC[1], labelT, "Ch", CENTER, TOP); - printAt(colC[1], valT, chUtilStr, CENTER, TOP); + // Channel Util + char chUtilStr[4]; // "XX%" + sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); + printAt(colC[1], labelT, "Ch", CENTER, TOP); + printAt(colC[1], valT, chUtilStr, CENTER, TOP); - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[1], y, BLACK); + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[1], y, BLACK); - // Duty Cycle (AirTimeTx) - char dutyUtilStr[4]; // "XX%" - sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); - printAt(colC[2], labelT, "Duty", CENTER, TOP); - printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); + // Duty Cycle (AirTimeTx) + char dutyUtilStr[4]; // "XX%" + sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); + printAt(colC[2], labelT, "Duty", CENTER, TOP); + printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); - /* - // Divider - for (int16_t y = valT; y <= divY; y += 3) - drawPixel(colR[2], y, BLACK); + /* + // Divider + for (int16_t y = valT; y <= divY; y += 3) + drawPixel(colR[2], y, BLACK); - // GPS satellites - todo - printAt(colC[3], labelT, "Sats", CENTER, TOP); - printAt(colC[3], valT, "ToDo", CENTER, TOP); - */ + // GPS satellites - todo + printAt(colC[3], labelT, "Sats", CENTER, TOP); + printAt(colC[3], valT, "ToDo", CENTER, TOP); + */ - // Horizontal divider, at bottom of system info panel - for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item - drawPixel(x, divY, BLACK); + // Horizontal divider, at bottom of system info panel + for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item + drawPixel(x, divY, BLACK); - if (renderedHeight != nullptr) - *renderedHeight = height; + if (renderedHeight != nullptr) + *renderedHeight = height; } // Get the height of the the panel drawn at the top of the menu // This is inefficient, as we do actually have to render the panel to determine the height // It solves a catch-22 situation, where slotCount needs to know panel height, and panel height needs to know slotCount -uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() { - // Render *far* off screen - uint16_t height = 0; - drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height); +uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() +{ + // Render *far* off screen + uint16_t height = 0; + drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height); - return height; + return height; } // Send a text message to the mesh // Used to send our canned messages -void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) { - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - p->to = dest; - p->channel = channel; - p->want_ack = true; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); +void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) +{ + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - // Tack on a bell character if requested - if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator - p->decoded.payload.size++; - } + // Tack on a bell character if requested + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator + p->decoded.payload.size++; + } - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone + service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone } // Free up any heap mmemory we'd used while selecting / sending canned messages -void InkHUD::MenuApplet::freeCannedMessageResources() { - cm.selectedMessageItem = nullptr; - cm.selectedRecipientItem = nullptr; - cm.messageItems.clear(); - cm.recipientItems.clear(); +void InkHUD::MenuApplet::freeCannedMessageResources() +{ + cm.selectedMessageItem = nullptr; + cm.selectedRecipientItem = nullptr; + cm.messageItems.clear(); + cm.recipientItems.clear(); } #endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index cbf2f3340..4f9f92227 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -14,88 +14,91 @@ #include "Channels.h" #include "concurrency/OSThread.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ class Applet; -class MenuApplet : public SystemApplet, public concurrency::OSThread { -public: - MenuApplet(); - void onForeground() override; - void onBackground() override; - void onButtonShortPress() override; - void onButtonLongPress() override; - void onExitShort() override; - void onNavUp() override; - void onNavDown() override; - void onNavLeft() override; - void onNavRight() override; - void onRender() override; - - void show(Tile *t); // Open the menu, onto a user tile - -protected: - Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton - - int32_t runOnce() override; - - void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any - void showPage(MenuPage page); // Load and display a MenuPage - - void populateSendPage(); // Dynamically create MenuItems including canned messages - void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message - void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets - void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow - void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds - - uint16_t getSystemInfoPanelHeight(); - void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, - uint16_t *height = nullptr); // Info panel at top of root menu - void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh - void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data - - MenuPage currentPage = MenuPage::ROOT; - MenuPage previousPage = MenuPage::EXIT; - uint8_t cursor = 0; // Which menu item is currently highlighted - bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) - - uint16_t systemInfoPanelHeight = 0; // Need to know before we render - - std::vector items; // MenuItems for the current page. Filled by ShowPage - - // Data for selecting and sending canned messages via the menu - // Placed into a sub-class for organization only - class CannedMessages { +class MenuApplet : public SystemApplet, public concurrency::OSThread +{ public: - // Share NicheGraphics component - // Handles loading, getting, setting - CannedMessageStore *store; + MenuApplet(); + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + void onRender() override; - // One canned message - // Links the menu item to the true message text - struct MessageItem { - std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed - std::string rawText; // The message which will be sent, if this item is selected - } *selectedMessageItem; + void show(Tile *t); // Open the menu, onto a user tile - // One possible destination for a canned message - // Links the menu item to the intended recipient - // May represent either broadcast or DM - struct RecipientItem { - std::string label; // Shown in menu - NodeNum dest = NODENUM_BROADCAST; - uint8_t channelIndex = 0; - } *selectedRecipientItem; + protected: + Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton - // These lists are generated when the menu page is populated - // Cleared onBackground (when MenuApplet closes) - std::vector messageItems; - std::vector recipientItems; - } cm; + int32_t runOnce() override; - Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu + void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any + void showPage(MenuPage page); // Load and display a MenuPage - bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options + void populateSendPage(); // Dynamically create MenuItems including canned messages + void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message + void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets + void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow + void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + + uint16_t getSystemInfoPanelHeight(); + void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, + uint16_t *height = nullptr); // Info panel at top of root menu + void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh + void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data + + MenuPage currentPage = MenuPage::ROOT; + MenuPage previousPage = MenuPage::EXIT; + uint8_t cursor = 0; // Which menu item is currently highlighted + bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) + + uint16_t systemInfoPanelHeight = 0; // Need to know before we render + + std::vector items; // MenuItems for the current page. Filled by ShowPage + + // Data for selecting and sending canned messages via the menu + // Placed into a sub-class for organization only + class CannedMessages + { + public: + // Share NicheGraphics component + // Handles loading, getting, setting + CannedMessageStore *store; + + // One canned message + // Links the menu item to the true message text + struct MessageItem { + std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed + std::string rawText; // The message which will be sent, if this item is selected + } *selectedMessageItem; + + // One possible destination for a canned message + // Links the menu item to the intended recipient + // May represent either broadcast or DM + struct RecipientItem { + std::string label; // Shown in menu + NodeNum dest = NODENUM_BROADCAST; + uint8_t channelIndex = 0; + } *selectedRecipientItem; + + // These lists are generated when the menu page is populated + // Cleared onBackground (when MenuApplet closes) + std::vector messageItems; + std::vector recipientItems; + } cm; + + Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu + + bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h index b483fd6d1..c74fe3d8a 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h @@ -19,23 +19,27 @@ Added to MenuPages in InkHUD::showPage #include "./MenuAction.h" #include "./MenuPage.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ // One item of a MenuPage -class MenuItem { -public: - std::string label; - MenuAction action = NO_ACTION; - MenuPage nextPage = EXIT; - bool *checkState = nullptr; +class MenuItem +{ + public: + std::string label; + MenuAction action = NO_ACTION; + MenuPage nextPage = EXIT; + bool *checkState = nullptr; - // Various constructors, depending on the intended function of the item + // Various constructors, depending on the intended function of the item - MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {} - MenuItem(const char *label, MenuAction action) : label(label), action(action) {} - MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {} - MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState) - : label(label), action(action), nextPage(nextPage), checkState(checkState) {} + MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {} + MenuItem(const char *label, MenuAction action) : label(label), action(action) {} + MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {} + MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState) + : label(label), action(action), nextPage(nextPage), checkState(checkState) + { + } }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index ab2ddc170..389e411c3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -11,18 +11,19 @@ Structure of the menu is defined in InkHUD::showPage #include "configuration.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ // Sub-menu for MenuApplet enum MenuPage : uint8_t { - ROOT, // Initial menu page - SEND, - CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message - OPTIONS, - APPLETS, - AUTOSHOW, - RECENTS, // Select length of "recentlyActiveSeconds" - EXIT, // Dismiss the menu applet + ROOT, // Initial menu page + SEND, + CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message + OPTIONS, + APPLETS, + AUTOSHOW, + RECENTS, // Select length of "recentlyActiveSeconds" + EXIT, // Dismiss the menu applet }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h b/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h index c82e667e8..d8c4f8366 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h @@ -4,9 +4,8 @@ A notification which might be displayed by the NotificationApplet -An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the -notification. An Applet should veto a notification if it is already displaying the same info which the notification -would convey. +An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification. +An Applet should veto a notification if it is already displaying the same info which the notification would convey. */ @@ -14,24 +13,26 @@ would convey. #include "configuration.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class Notification { -public: - enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type; +class Notification +{ + public: + enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type; - uint32_t timestamp; + uint32_t timestamp; - uint8_t getChannel() { return channel; } - uint32_t getSender() { return sender; } - uint8_t getBatteryPercentage() { return batteryPercentage; } + uint8_t getChannel() { return channel; } + uint32_t getSender() { return sender; } + uint8_t getBatteryPercentage() { return batteryPercentage; } - friend class NotificationApplet; + friend class NotificationApplet; -protected: - uint8_t channel; - uint32_t sender; - uint8_t batteryPercentage; + protected: + uint8_t channel; + uint32_t sender; + uint8_t batteryPercentage; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index dc759a80c..2ea9c7fe0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -12,247 +12,268 @@ using namespace NicheGraphics; -InkHUD::NotificationApplet::NotificationApplet() { textMessageObserver.observe(textMessageModule); } +InkHUD::NotificationApplet::NotificationApplet() +{ + textMessageObserver.observe(textMessageModule); +} // Collect meta-info about the text message, and ask for approval for the notification // No need to save the message itself; we can use the cached InkHUD::latestMessage data during render() -int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { - // System applets are always active - assert(isActive()); +int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +{ + // System applets are always active + assert(isActive()); - // Abort if feature disabled - // This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled - if (!settings->optionalFeatures.notifications) + // Abort if feature disabled + // This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled + if (!settings->optionalFeatures.notifications) + return 0; + + // Abort if this is an outgoing message + if (getFrom(p) == nodeDB->getNodeNum()) + return 0; + + Notification n; + n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + + // Gather info: in-channel message + if (isBroadcast(p->to)) { + n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + n.channel = p->channel; + } + + // Gather info: DM + else { + n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT; + n.sender = p->from; + } + + // Close an old notification, if shown + dismiss(); + + // Check if we should display the notification + // A foreground applet might already be displaying this info + hasNotification = true; + currentNotification = n; + if (isApproved()) { + bringToForeground(); + inkhud->forceUpdate(); + } else + hasNotification = false; // Clear the pending notification: it was rejected + + // Return zero: no issues here, carry on notifying other observers! return 0; - - // Abort if this is an outgoing message - if (getFrom(p) == nodeDB->getNodeNum()) - return 0; - - Notification n; - n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - - // Gather info: in-channel message - if (isBroadcast(p->to)) { - n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; - n.channel = p->channel; - } - - // Gather info: DM - else { - n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT; - n.sender = p->from; - } - - // Close an old notification, if shown - dismiss(); - - // Check if we should display the notification - // A foreground applet might already be displaying this info - hasNotification = true; - currentNotification = n; - if (isApproved()) { - bringToForeground(); - inkhud->forceUpdate(); - } else - hasNotification = false; // Clear the pending notification: it was rejected - - // Return zero: no issues here, carry on notifying other observers! - return 0; } -void InkHUD::NotificationApplet::onRender() { - // Clear the region beneath the tile - // Most applets are drawing onto an empty frame buffer and don't need to do this - // We do need to do this with the battery though, as it is an "overlay" - fillRect(0, 0, width(), height(), WHITE); +void InkHUD::NotificationApplet::onRender() +{ + // Clear the region beneath the tile + // Most applets are drawing onto an empty frame buffer and don't need to do this + // We do need to do this with the battery though, as it is an "overlay" + fillRect(0, 0, width(), height(), WHITE); - // Padding (horizontal) - const uint16_t padW = 4; + // Padding (horizontal) + const uint16_t padW = 4; - // Main border - drawRect(0, 0, width(), height(), BLACK); - // drawRect(1, 1, width() - 2, height() - 2, BLACK); + // Main border + drawRect(0, 0, width(), height(), BLACK); + // drawRect(1, 1, width() - 2, height() - 2, BLACK); - // Timestamp (potentially) - // ==================== - std::string ts = getTimeString(currentNotification.timestamp); - uint16_t tsW = 0; - int16_t divX = 0; + // Timestamp (potentially) + // ==================== + std::string ts = getTimeString(currentNotification.timestamp); + uint16_t tsW = 0; + int16_t divX = 0; - // Timestamp available - if (ts.length() > 0) { - tsW = getTextWidth(ts); - divX = padW + tsW + padW; + // Timestamp available + if (ts.length() > 0) { + tsW = getTextWidth(ts); + divX = padW + tsW + padW; - hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background - drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text + hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background + drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text - setCrop(1, 1, divX - 1, height() - 2); + setCrop(1, 1, divX - 1, height() - 2); + + // Drop shadow + setTextColor(WHITE); + printThick(padW + (tsW / 2), height() / 2, ts, 4, 4); + + // Bold text + setTextColor(BLACK); + printThick(padW + (tsW / 2), height() / 2, ts, 2, 1); + } + + // Main text + // ===================== + + // Background fill + // - medium dark (1/3) + hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK); + + uint16_t availableWidth = width() - divX - padW; + std::string text = getNotificationText(availableWidth); + + int16_t textM = divX + padW + (getTextWidth(text) / 2); + + // Restrict area for printing + // - don't overlap border, or divider + setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); // Drop shadow + // - thick white text setTextColor(WHITE); - printThick(padW + (tsW / 2), height() / 2, ts, 4, 4); + printThick(textM, height() / 2, text, 4, 4); - // Bold text + // Main text + // - faux bold: double width setTextColor(BLACK); - printThick(padW + (tsW / 2), height() / 2, ts, 2, 1); - } - - // Main text - // ===================== - - // Background fill - // - medium dark (1/3) - hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK); - - uint16_t availableWidth = width() - divX - padW; - std::string text = getNotificationText(availableWidth); - - int16_t textM = divX + padW + (getTextWidth(text) / 2); - - // Restrict area for printing - // - don't overlap border, or divider - setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); - - // Drop shadow - // - thick white text - setTextColor(WHITE); - printThick(textM, height() / 2, text, 4, 4); - - // Main text - // - faux bold: double width - setTextColor(BLACK); - printThick(textM, height() / 2, text, 2, 1); + printThick(textM, height() / 2, text, 2, 1); } -void InkHUD::NotificationApplet::onForeground() { - handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification +void InkHUD::NotificationApplet::onForeground() +{ + handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification } -void InkHUD::NotificationApplet::onBackground() { handleInput = false; } - -void InkHUD::NotificationApplet::onButtonShortPress() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onBackground() +{ + handleInput = false; } -void InkHUD::NotificationApplet::onButtonLongPress() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onButtonShortPress() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onExitShort() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onButtonLongPress() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onExitLong() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onExitShort() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavUp() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onExitLong() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavDown() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavUp() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavLeft() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavDown() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } -void InkHUD::NotificationApplet::onNavRight() { - dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); +void InkHUD::NotificationApplet::onNavLeft() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onNavRight() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification // Called internally when we first get a "notifiable event", and then again before render, // in case autoshow swapped which applet was displayed -bool InkHUD::NotificationApplet::isApproved() { - // Instead of an assert - if (!hasNotification) { - LOG_WARN("No notif to approve"); - return false; - } +bool InkHUD::NotificationApplet::isApproved() +{ + // Instead of an assert + if (!hasNotification) { + LOG_WARN("No notif to approve"); + return false; + } - // Ask all visible user applets for approval - for (Applet *ua : inkhud->userApplets) { - if (ua->isForeground() && !ua->approveNotification(currentNotification)) - return false; - } + // Ask all visible user applets for approval + for (Applet *ua : inkhud->userApplets) { + if (ua->isForeground() && !ua->approveNotification(currentNotification)) + return false; + } - return true; + return true; } // Mark that the notification should no-longer be rendered // In addition to calling thing method, code needs to request a re-render of all applets -void InkHUD::NotificationApplet::dismiss() { - sendToBackground(); - hasNotification = false; - // Not requesting update directly from this method, - // as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever - // drawn +void InkHUD::NotificationApplet::dismiss() +{ + sendToBackground(); + hasNotification = false; + // Not requesting update directly from this method, + // as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn } // Get a string for the main body text of a notification // Formatted to suit screen width // Takes info from InkHUD::currentNotification -std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) { - assert(hasNotification); +std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) +{ + assert(hasNotification); - std::string text; + std::string text; - // Text message - // ============== + // Text message + // ============== - if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { + if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, + Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { - // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently - bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently + bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; - // Pick source of message - MessageStore::Message *message = isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; + // Pick source of message + MessageStore::Message *message = + isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; - // Find info about the sender - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); + // Find info about the sender + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); - // Leading tag (channel vs. DM) - text += isBroadcast ? "From:" : "DM: "; + // Leading tag (channel vs. DM) + text += isBroadcast ? "From:" : "DM: "; - // Sender id - if (node && node->has_user) - text += parseShortName(node); - else - text += hexifyNodeNum(message->sender); + // Sender id + if (node && node->has_user) + text += parseShortName(node); + else + text += hexifyNodeNum(message->sender); - // Check if text fits - // - use a longer string, if we have the space - if (getTextWidth(text) < widthAvailable * 0.5) { - text.clear(); + // Check if text fits + // - use a longer string, if we have the space + if (getTextWidth(text) < widthAvailable * 0.5) { + text.clear(); - // Leading tag (channel vs. DM) - text += isBroadcast ? "Msg from " : "DM from "; + // Leading tag (channel vs. DM) + text += isBroadcast ? "Msg from " : "DM from "; - // Sender id - if (node && node->has_user) - text += parseShortName(node); - else - text += hexifyNodeNum(message->sender); + // Sender id + if (node && node->has_user) + text += parseShortName(node); + else + text += hexifyNodeNum(message->sender); - text += ": "; - text += message->text; + text += ": "; + text += message->text; + } } - } - // Parse any non-ascii characters and return - return parse(text); + // Parse any non-ascii characters and return + return parse(text); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index 795ab36e8..16ea13407 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -18,38 +18,40 @@ Feature should be optional; enable disable via on-screen menu #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class NotificationApplet : public SystemApplet { -public: - NotificationApplet(); +class NotificationApplet : public SystemApplet +{ + public: + NotificationApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onButtonShortPress() override; - void onButtonLongPress() override; - void onExitShort() override; - void onExitLong() override; - void onNavUp() override; - void onNavDown() override; - void onNavLeft() override; - void onNavRight() override; + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + int onReceiveTextMessage(const meshtastic_MeshPacket *p); - bool isApproved(); // Does a foreground applet make notification redundant? - void dismiss(); // Close the Notification Popup + bool isApproved(); // Does a foreground applet make notification redundant? + void dismiss(); // Close the Notification Popup -protected: - // Get notified when a new text message arrives - CallbackObserver textMessageObserver = - CallbackObserver(this, &NotificationApplet::onReceiveTextMessage); + protected: + // Get notified when a new text message arrives + CallbackObserver textMessageObserver = + CallbackObserver(this, &NotificationApplet::onReceiveTextMessage); - std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width + std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width - bool hasNotification = false; // Only used for assert. Todo: remove? - Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render() + bool hasNotification = false; // Only used for assert. Todo: remove? + Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render() }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index e777488fb..09931f109 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -4,67 +4,74 @@ using namespace NicheGraphics; -InkHUD::PairingApplet::PairingApplet() { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); } - -void InkHUD::PairingApplet::onRender() { - // Header - setFont(fontMedium); - printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM); - setFont(fontSmall); - printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP); - - // Passkey - setFont(fontMedium); - printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2); - - // Device's bluetooth name, if it will fit - setFont(fontSmall); - std::string name = "Name: " + parse(getDeviceName()); - if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " - name = parse(getDeviceName()); - if (getTextWidth(name) < width()) // Does it fit? - printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); +InkHUD::PairingApplet::PairingApplet() +{ + bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); } -void InkHUD::PairingApplet::onForeground() { - // Prevent most other applets from requesting update, and skip their rendering entirely - // Another system applet with a higher precedence can potentially ignore this - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; -} -void InkHUD::PairingApplet::onBackground() { - // Allow normal update behavior to resume - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; +void InkHUD::PairingApplet::onRender() +{ + // Header + setFont(fontMedium); + printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM); + setFont(fontSmall); + printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP); - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Passkey + setFont(fontMedium); + printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2); + + // Device's bluetooth name, if it will fit + setFont(fontSmall); + std::string name = "Name: " + parse(getDeviceName()); + if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " + name = parse(getDeviceName()); + if (getTextWidth(name) < width()) // Does it fit? + printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); } -int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) { - // The standard Meshtastic convention is to pass these "generic" Status objects, - // check their type, and then cast them. - // We'll mimic that behavior, just to keep in line with the other Statuses, - // even though I'm not sure what the original reason for jumping through these extra hoops was. - assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; +void InkHUD::PairingApplet::onForeground() +{ + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; +} +void InkHUD::PairingApplet::onBackground() +{ + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; - // When pairing begins - if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { - // Store the passkey for rendering - passkey = bluetoothStatus->getPasskey(); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} - // Show pairing screen - bringToForeground(); - } +int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) +{ + // The standard Meshtastic convention is to pass these "generic" Status objects, + // check their type, and then cast them. + // We'll mimic that behavior, just to keep in line with the other Statuses, + // even though I'm not sure what the original reason for jumping through these extra hoops was. + assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); + meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; - // When pairing ends - // or rather, when something changes, and we shouldn't be showing the pairing screen - else if (isForeground()) - sendToBackground(); + // When pairing begins + if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { + // Store the passkey for rendering + passkey = bluetoothStatus->getPasskey(); - return 0; // No special result to report back to Observable + // Show pairing screen + bringToForeground(); + } + + // When pairing ends + // or rather, when something changes, and we shouldn't be showing the pairing screen + else if (isForeground()) + sendToBackground(); + + return 0; // No special result to report back to Observable } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h index a60a79fb6..b89783a25 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h @@ -14,24 +14,26 @@ #include "main.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class PairingApplet : public SystemApplet { -public: - PairingApplet(); +class PairingApplet : public SystemApplet +{ + public: + PairingApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; + void onRender() override; + void onForeground() override; + void onBackground() override; - int onBluetoothStatusUpdate(const meshtastic::Status *status); + int onBluetoothStatusUpdate(const meshtastic::Status *status); -protected: - // Get notified when status of the Bluetooth connection changes - CallbackObserver bluetoothStatusObserver = - CallbackObserver(this, &PairingApplet::onBluetoothStatusUpdate); + protected: + // Get notified when status of the Bluetooth connection changes + CallbackObserver bluetoothStatusObserver = + CallbackObserver(this, &PairingApplet::onBluetoothStatusUpdate); - std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros + std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp index ae6a466e8..99cdeb0ac 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp @@ -4,9 +4,10 @@ using namespace NicheGraphics; -void InkHUD::PlaceholderApplet::onRender() { - // This placeholder applet fills its area with sparse diagonal lines - hatchRegion(0, 0, width(), height(), 8, BLACK); +void InkHUD::PlaceholderApplet::onRender() +{ + // This placeholder applet fills its area with sparse diagonal lines + hatchRegion(0, 0, width(), height(), 8, BLACK); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h index b10147eb5..78ba5cd89 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h @@ -11,15 +11,17 @@ Fills the area with diagonal lines #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class PlaceholderApplet : public SystemApplet { -public: - void onRender() override; +class PlaceholderApplet : public SystemApplet +{ + public: + void onRender() override; - // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. - // The window manager decides when and where it should be rendered - // It may be drawn to several different tiles during an Renderer::render call + // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. + // The window manager decides when and where it should be rendered + // It may be drawn to several different tiles during an Renderer::render call }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index 415118281..a9d579873 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -8,240 +8,248 @@ using namespace NicheGraphics; -InkHUD::TipsApplet::TipsApplet() { - // Decide which tips (if any) should be shown to user after the boot screen +InkHUD::TipsApplet::TipsApplet() +{ + // Decide which tips (if any) should be shown to user after the boot screen - // Welcome screen - if (settings->tips.firstBoot) - tipQueue.push_back(Tip::WELCOME); + // Welcome screen + if (settings->tips.firstBoot) + tipQueue.push_back(Tip::WELCOME); - // Antenna, region, timezone - // Shown at boot if region not yet set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) - tipQueue.push_back(Tip::FINISH_SETUP); + // Antenna, region, timezone + // Shown at boot if region not yet set + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + tipQueue.push_back(Tip::FINISH_SETUP); - // Shutdown info - // Shown until user performs one valid shutdown - if (!settings->tips.safeShutdownSeen) - tipQueue.push_back(Tip::SAFE_SHUTDOWN); + // Shutdown info + // Shown until user performs one valid shutdown + if (!settings->tips.safeShutdownSeen) + tipQueue.push_back(Tip::SAFE_SHUTDOWN); - // Using the UI - if (settings->tips.firstBoot) { - tipQueue.push_back(Tip::CUSTOMIZATION); - tipQueue.push_back(Tip::BUTTONS); - } + // Using the UI + if (settings->tips.firstBoot) { + tipQueue.push_back(Tip::CUSTOMIZATION); + tipQueue.push_back(Tip::BUTTONS); + } - // Catch an incorrect attempt at rotating display - if (config.display.flip_screen) - tipQueue.push_back(Tip::ROTATION); + // Catch an incorrect attempt at rotating display + if (config.display.flip_screen) + tipQueue.push_back(Tip::ROTATION); - // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground - // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets - // vector - if (!tipQueue.empty()) - bringToForeground(); + // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground + // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector + if (!tipQueue.empty()) + bringToForeground(); } -void InkHUD::TipsApplet::onRender() { - switch (tipQueue.front()) { - case Tip::WELCOME: - renderWelcome(); - break; +void InkHUD::TipsApplet::onRender() +{ + switch (tipQueue.front()) { + case Tip::WELCOME: + renderWelcome(); + break; - case Tip::FINISH_SETUP: { - setFont(fontMedium); - printAt(0, 0, "Tip: Finish Setup"); + case Tip::FINISH_SETUP: { + setFont(fontMedium); + printAt(0, 0, "Tip: Finish Setup"); - setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; - printAt(0, cursorY, "- connect antenna"); + setFont(fontSmall); + int16_t cursorY = fontMedium.lineHeight() * 1.5; + printAt(0, cursorY, "- connect antenna"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- connect a client app"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- connect a client app"); - // Only if region not set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set region"); + // Only if region not set + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- set region"); + } + + // Only if tz not set + if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- set timezone"); + } + + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "More info at meshtastic.org"); + + setFont(fontSmall); + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::SAFE_SHUTDOWN: { + setFont(fontMedium); + printAt(0, 0, "Tip: Shutdown"); + + setFont(fontSmall); + std::string shutdown; + shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; + shutdown += "\n"; + shutdown += "This ensures data is saved."; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + + } break; + + case Tip::CUSTOMIZATION: { + setFont(fontMedium); + printAt(0, 0, "Tip: Customization"); + + setFont(fontSmall); + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::BUTTONS: { + setFont(fontMedium); + printAt(0, 0, "Tip: Buttons"); + + setFont(fontSmall); + int16_t cursorY = fontMedium.lineHeight() * 1.5; + + if (!settings->joystick.enabled) { + printAt(0, cursorY, "User Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- short press: next"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- long press: select / open menu"); + } else { + printAt(0, cursorY, "Joystick"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- open menu / select"); + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "Exit Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- switch tile / close menu"); + } + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + } break; + + case Tip::ROTATION: { + setFont(fontMedium); + printAt(0, 0, "Tip: Rotation"); + + setFont(fontSmall); + if (!settings->joystick.enabled) { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + } else { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); + } + + printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); + + // Revert the "flip screen" setting, preventing this message showing again + config.display.flip_screen = false; + nodeDB->saveToDisk(SEGMENT_DEVICESTATE); + } break; } - - // Only if tz not set - if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set timezone"); - } - - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "More info at meshtastic.org"); - - setFont(fontSmall); - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; - - case Tip::SAFE_SHUTDOWN: { - setFont(fontMedium); - printAt(0, 0, "Tip: Shutdown"); - - setFont(fontSmall); - std::string shutdown; - shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; - shutdown += "\n"; - shutdown += "This ensures data is saved."; - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - - } break; - - case Tip::CUSTOMIZATION: { - setFont(fontMedium); - printAt(0, 0, "Tip: Customization"); - - setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; - - case Tip::BUTTONS: { - setFont(fontMedium); - printAt(0, 0, "Tip: Buttons"); - - setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; - - if (!settings->joystick.enabled) { - printAt(0, cursorY, "User Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: next"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- long press: select / open menu"); - } else { - printAt(0, cursorY, "Joystick"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- open menu / select"); - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "Exit Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- switch tile / close menu"); - } - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; - - case Tip::ROTATION: { - setFont(fontMedium); - printAt(0, 0, "Tip: Rotation"); - - setFont(fontSmall); - if (!settings->joystick.enabled) { - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); - } else { - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); - } - - printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - - // Revert the "flip screen" setting, preventing this message showing again - config.display.flip_screen = false; - nodeDB->saveToDisk(SEGMENT_DEVICESTATE); - } break; - } } // This tip has its own render method, only because it's a big block of code // Didn't want to clutter up the switch in onRender too much -void InkHUD::TipsApplet::renderWelcome() { - uint16_t padW = X(0.05); +void InkHUD::TipsApplet::renderWelcome() +{ + uint16_t padW = X(0.05); - // Block 1 - logo & title - // ======================== + // Block 1 - logo & title + // ======================== - // Logo size - uint16_t logoWLimit = X(0.3); - uint16_t logoHLimit = Y(0.3); - uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); - uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); + // Logo size + uint16_t logoWLimit = X(0.3); + uint16_t logoHLimit = Y(0.3); + uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); + uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); - // Title size - setFont(fontMedium); - std::string title; - if (width() >= 200) // Future proofing: hide if *tiny* display - title = "meshtastic.org"; - uint16_t titleW = getTextWidth(title); + // Title size + setFont(fontMedium); + std::string title; + if (width() >= 200) // Future proofing: hide if *tiny* display + title = "meshtastic.org"; + uint16_t titleW = getTextWidth(title); - // Center the block - // Desired effect: equal margin from display edge for logo left and title right - int16_t block1Y = Y(0.3); - int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); - int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); - int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); + // Center the block + // Desired effect: equal margin from display edge for logo left and title right + int16_t block1Y = Y(0.3); + int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); + int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); + int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); - // Draw block - drawLogo(logoCX, block1Y, logoW, logoH); - printAt(titleCX, block1Y, title, CENTER, MIDDLE); + // Draw block + drawLogo(logoCX, block1Y, logoW, logoH); + printAt(titleCX, block1Y, title, CENTER, MIDDLE); - // Block 2 - subtitle - // ======================= - setFont(fontSmall); - std::string subtitle = "InkHUD"; - if (width() >= 200) - subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display - printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); + // Block 2 - subtitle + // ======================= + setFont(fontSmall); + std::string subtitle = "InkHUD"; + if (width() >= 200) + subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display + printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); - // Block 3 - press to continue - // ============================ - printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); + // Block 3 - press to continue + // ============================ + printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); } -void InkHUD::TipsApplet::onForeground() { - // Prevent most other applets from requesting update, and skip their rendering entirely - // Another system applet with a higher precedence can potentially ignore this - SystemApplet::lockRendering = true; - SystemApplet::lockRequests = true; +void InkHUD::TipsApplet::onForeground() +{ + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; - SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first) + SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first) } -void InkHUD::TipsApplet::onBackground() { - // Allow normal update behavior to resume - SystemApplet::lockRendering = false; - SystemApplet::lockRequests = false; - SystemApplet::handleInput = false; +void InkHUD::TipsApplet::onBackground() +{ + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; - // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background - // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // While our SystemApplet::handleInput flag is true -void InkHUD::TipsApplet::onButtonShortPress() { - tipQueue.pop_front(); +void InkHUD::TipsApplet::onButtonShortPress() +{ + tipQueue.pop_front(); - // All tips done - if (tipQueue.empty()) { - // Record that user has now seen the "tutorial" set of tips - // Don't show them on subsequent boots - if (settings->tips.firstBoot) { - settings->tips.firstBoot = false; - inkhud->persistence->saveSettings(); + // All tips done + if (tipQueue.empty()) { + // Record that user has now seen the "tutorial" set of tips + // Don't show them on subsequent boots + if (settings->tips.firstBoot) { + settings->tips.firstBoot = false; + inkhud->persistence->saveSettings(); + } + + // Close applet, and full refresh to clean the screen + // Need to force update, because our request would be ignored otherwise, as we are now background + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); } - // Close applet, and full refresh to clean the screen - // Need to force update, because our request would be ignored otherwise, as we are now background - sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); - } - - // More tips left - else - requestUpdate(); + // More tips left + else + requestUpdate(); } // Functions the same as the user button in this instance -void InkHUD::TipsApplet::onExitShort() { onButtonShortPress(); } +void InkHUD::TipsApplet::onExitShort() +{ + onButtonShortPress(); +} #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index 2ed6e234c..159e6f58f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -14,34 +14,36 @@ #include "graphics/niche/InkHUD/SystemApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class TipsApplet : public SystemApplet { -protected: - enum class Tip { - WELCOME, - FINISH_SETUP, - SAFE_SHUTDOWN, - CUSTOMIZATION, - BUTTONS, - ROTATION, - }; +class TipsApplet : public SystemApplet +{ + protected: + enum class Tip { + WELCOME, + FINISH_SETUP, + SAFE_SHUTDOWN, + CUSTOMIZATION, + BUTTONS, + ROTATION, + }; -public: - TipsApplet(); + public: + TipsApplet(); - void onRender() override; - void onForeground() override; - void onBackground() override; - void onButtonShortPress() override; - void onExitShort() override; + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onExitShort() override; -protected: - void renderWelcome(); // Very first screen of tutorial + protected: + void renderWelcome(); // Very first screen of tutorial - std::deque tipQueue; // List of tips to show, one after another + std::deque tipQueue; // List of tips to show, one after another - WindowManager *windowManager = nullptr; // For convenience. Set in constructor. + WindowManager *windowManager = nullptr; // For convenience. Set in constructor. }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 2c5e8f78d..7c6232f3b 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -4,127 +4,136 @@ using namespace NicheGraphics; -void InkHUD::AllMessageApplet::onActivate() { textMessageObserver.observe(textMessageModule); } +void InkHUD::AllMessageApplet::onActivate() +{ + textMessageObserver.observe(textMessageModule); +} -void InkHUD::AllMessageApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); } +void InkHUD::AllMessageApplet::onDeactivate() +{ + textMessageObserver.unobserve(textMessageModule); +} // We're not consuming the data passed to this method; // we're just just using it to trigger a render -int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { - // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets - if (!isActive()) +int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +{ + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + if (!isActive()) + return 0; + + // Abort if this is an outgoing message + if (getFrom(p) == nodeDB->getNodeNum()) + return 0; + + requestAutoshow(); // Want to become foreground, if permitted + requestUpdate(); // Want to update display, if applet is foreground + + // Return zero: no issues here, carry on notifying other observers! return 0; - - // Abort if this is an outgoing message - if (getFrom(p) == nodeDB->getNodeNum()) - return 0; - - requestAutoshow(); // Want to become foreground, if permitted - requestUpdate(); // Want to update display, if applet is foreground - - // Return zero: no issues here, carry on notifying other observers! - return 0; } -void InkHUD::AllMessageApplet::onRender() { - // Find newest message, regardless of whether DM or broadcast - MessageStore::Message *message; - if (latestMessage->wasBroadcast) - message = &latestMessage->broadcast; - else - message = &latestMessage->dm; +void InkHUD::AllMessageApplet::onRender() +{ + // Find newest message, regardless of whether DM or broadcast + MessageStore::Message *message; + if (latestMessage->wasBroadcast) + message = &latestMessage->broadcast; + else + message = &latestMessage->dm; - // Short circuit: no text message - if (!message->sender) { - printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE); - return; - } + // Short circuit: no text message + if (!message->sender) { + printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE); + return; + } - // =========================== - // Header (sender, timestamp) - // =========================== + // =========================== + // Header (sender, timestamp) + // =========================== - // Y position for divider - // - between header text and messages + // Y position for divider + // - between header text and messages - std::string header; + std::string header; - // RX Time - // - if valid - std::string timeString = getTimeString(message->timestamp); - if (timeString.length() > 0) { - header += timeString; - header += ": "; - } + // RX Time + // - if valid + std::string timeString = getTimeString(message->timestamp); + if (timeString.length() > 0) { + header += timeString; + header += ": "; + } - // Sender's id - // - short name and long name, if available, or - // - node id - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); - if (sender && sender->has_user) { - header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) - header += " ("; - header += parse(sender->user.long_name); - header += ")"; - } else - header += hexifyNodeNum(message->sender); + // Sender's id + // - short name and long name, if available, or + // - node id + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); + if (sender && sender->has_user) { + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) + header += " ("; + header += parse(sender->user.long_name); + header += ")"; + } else + header += hexifyNodeNum(message->sender); - // Draw a "standard" applet header - drawHeader(header); + // Draw a "standard" applet header + drawHeader(header); - // Fade the right edge of the header, if text spills over edge - uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect - uint8_t hF = getHeaderHeight(); // Height of fade effect - if (getCursorX() > width()) - hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); + // Fade the right edge of the header, if text spills over edge + uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect + uint8_t hF = getHeaderHeight(); // Height of fade effect + if (getCursorX() > width()) + hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); - // Dimensions of the header - constexpr int16_t padDivH = 2; - const int16_t headerDivY = Applet::getHeaderHeight() - 1; + // Dimensions of the header + constexpr int16_t padDivH = 2; + const int16_t headerDivY = Applet::getHeaderHeight() - 1; - // =================== - // Print message text - // =================== + // =================== + // Print message text + // =================== - // Parse any non-ascii chars in the message - std::string text = parse(message->text); + // Parse any non-ascii chars in the message + std::string text = parse(message->text); - // Extra gap below the header - int16_t textTop = headerDivY + padDivH; + // Extra gap below the header + int16_t textTop = headerDivY + padDivH; - // Attempt to print with fontLarge - uint32_t textHeight; - setFont(fontLarge); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { + // Attempt to print with fontLarge + uint32_t textHeight; + setFont(fontLarge); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): attempt to print with fontMedium + setFont(fontMedium); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): print with fontSmall + setFont(fontSmall); printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): attempt to print with fontMedium - setFont(fontMedium); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { - printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): print with fontSmall - setFont(fontSmall); - printWrapped(0, textTop, width(), text); } // Don't show notifications for text messages when our applet is displayed -bool InkHUD::AllMessageApplet::approveNotification(Notification &n) { - if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST) - return false; +bool InkHUD::AllMessageApplet::approveNotification(Notification &n) +{ + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST) + return false; - else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) - return false; + else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) + return false; - else - return true; + else + return true; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h index 71ad56d00..c74e16196 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h @@ -6,9 +6,8 @@ Shows the latest incoming text message, as well as sender. Both broadcast and direct messages will be shown here, from all channels. This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages -This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text -message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via -InkHUD::latestMessage +This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. +This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage We do still receive notifications from the text message module though, to know when a new message has arrived, and trigger the update. @@ -23,24 +22,26 @@ to know when a new message has arrived, and trigger the update. #include "modules/TextMessageModule.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ class Applet; -class AllMessageApplet : public Applet { -public: - void onRender() override; +class AllMessageApplet : public Applet +{ + public: + void onRender() override; - void onActivate() override; - void onDeactivate() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + void onActivate() override; + void onDeactivate() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); - bool approveNotification(Notification &n) override; // Which notifications to suppress + bool approveNotification(Notification &n) override; // Which notifications to suppress -protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, &AllMessageApplet::onReceiveTextMessage); + protected: + // Used to register our text message callback + CallbackObserver textMessageObserver = + CallbackObserver(this, &AllMessageApplet::onReceiveTextMessage); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index 4e4d0cb43..a3b9615a5 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -4,120 +4,129 @@ using namespace NicheGraphics; -void InkHUD::DMApplet::onActivate() { textMessageObserver.observe(textMessageModule); } +void InkHUD::DMApplet::onActivate() +{ + textMessageObserver.observe(textMessageModule); +} -void InkHUD::DMApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); } +void InkHUD::DMApplet::onDeactivate() +{ + textMessageObserver.unobserve(textMessageModule); +} // We're not consuming the data passed to this method; // we're just just using it to trigger a render -int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { - // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets - if (!isActive()) +int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +{ + // Abort if applet fully deactivated + // Already handled by onActivate and onDeactivate, but good practice for all applets + if (!isActive()) + return 0; + + // If DM (not broadcast) + if (!isBroadcast(p->to)) { + // Want to update display, if applet is foreground + requestUpdate(); + + // If this was an incoming message, suggest that our applet becomes foreground, if permitted + if (getFrom(p) != nodeDB->getNodeNum()) + requestAutoshow(); + } + + // Return zero: no issues here, carry on notifying other observers! return 0; - - // If DM (not broadcast) - if (!isBroadcast(p->to)) { - // Want to update display, if applet is foreground - requestUpdate(); - - // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(p) != nodeDB->getNodeNum()) - requestAutoshow(); - } - - // Return zero: no issues here, carry on notifying other observers! - return 0; } -void InkHUD::DMApplet::onRender() { - // Abort if no text message - if (!latestMessage->dm.sender) { - printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE); - return; - } +void InkHUD::DMApplet::onRender() +{ + // Abort if no text message + if (!latestMessage->dm.sender) { + printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE); + return; + } - // =========================== - // Header (sender, timestamp) - // =========================== + // =========================== + // Header (sender, timestamp) + // =========================== - // Y position for divider - // - between header text and messages + // Y position for divider + // - between header text and messages - std::string header; + std::string header; - // RX Time - // - if valid - std::string timeString = getTimeString(latestMessage->dm.timestamp); - if (timeString.length() > 0) { - header += timeString; - header += ": "; - } + // RX Time + // - if valid + std::string timeString = getTimeString(latestMessage->dm.timestamp); + if (timeString.length() > 0) { + header += timeString; + header += ": "; + } - // Sender's id - // - shortname and long name, if available, or - // - node id - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); - if (sender && sender->has_user) { - header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) - header += " ("; - header += parse(sender->user.long_name); - header += ")"; - } else - header += hexifyNodeNum(latestMessage->dm.sender); + // Sender's id + // - shortname and long name, if available, or + // - node id + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); + if (sender && sender->has_user) { + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) + header += " ("; + header += parse(sender->user.long_name); + header += ")"; + } else + header += hexifyNodeNum(latestMessage->dm.sender); - // Draw a "standard" applet header - drawHeader(header); + // Draw a "standard" applet header + drawHeader(header); - // Fade the right edge of the header, if text spills over edge - uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect - uint8_t hF = getHeaderHeight(); // Height of fade effect - if (getCursorX() > width()) - hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); + // Fade the right edge of the header, if text spills over edge + uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect + uint8_t hF = getHeaderHeight(); // Height of fade effect + if (getCursorX() > width()) + hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); - // Dimensions of the header - constexpr int16_t padDivH = 2; - const int16_t headerDivY = Applet::getHeaderHeight() - 1; + // Dimensions of the header + constexpr int16_t padDivH = 2; + const int16_t headerDivY = Applet::getHeaderHeight() - 1; - // =================== - // Print message text - // =================== + // =================== + // Print message text + // =================== - // Parse any non-ascii chars in the message - std::string text = parse(latestMessage->dm.text); + // Parse any non-ascii chars in the message + std::string text = parse(latestMessage->dm.text); - // Extra gap below the header - int16_t textTop = headerDivY + padDivH; + // Extra gap below the header + int16_t textTop = headerDivY + padDivH; - // Attempt to print with fontLarge - uint32_t textHeight; - setFont(fontLarge); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { + // Attempt to print with fontLarge + uint32_t textHeight; + setFont(fontLarge); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): attempt to print with fontMedium + setFont(fontMedium); + textHeight = getWrappedTextHeight(0, width(), text); + if (textHeight <= (uint32_t)height()) { + printWrapped(0, textTop, width(), text); + return; + } + + // Fallback (too large): print with fontSmall + setFont(fontSmall); printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): attempt to print with fontMedium - setFont(fontMedium); - textHeight = getWrappedTextHeight(0, width(), text); - if (textHeight <= (uint32_t)height()) { - printWrapped(0, textTop, width(), text); - return; - } - - // Fallback (too large): print with fontSmall - setFont(fontSmall); - printWrapped(0, textTop, width(), text); } // Don't show notifications for direct messages when our applet is displayed -bool InkHUD::DMApplet::approveNotification(Notification &n) { - if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) - return false; +bool InkHUD::DMApplet::approveNotification(Notification &n) +{ + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) + return false; - else - return true; + else + return true; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h index 9da61598d..b3dc36e66 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h @@ -6,9 +6,8 @@ Shows the latest incoming *Direct Message* (DM), as well as sender. This compliments the threaded message applets This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages -This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text -message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via -InkHUD::latestMessage +This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. +This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage We do still receive notifications from the text message module though, to know when a new message has arrived, and trigger the update. @@ -23,24 +22,26 @@ to know when a new message has arrived, and trigger the update. #include "modules/TextMessageModule.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ class Applet; -class DMApplet : public Applet { -public: - void onRender() override; +class DMApplet : public Applet +{ + public: + void onRender() override; - void onActivate() override; - void onDeactivate() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + void onActivate() override; + void onDeactivate() override; + int onReceiveTextMessage(const meshtastic_MeshPacket *p); - bool approveNotification(Notification &n) override; // Which notifications to suppress + bool approveNotification(Notification &n) override; // Which notifications to suppress -protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, &DMApplet::onReceiveTextMessage); + protected: + // Used to register our text message callback + CallbackObserver textMessageObserver = + CallbackObserver(this, &DMApplet::onReceiveTextMessage); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp index 2255c2e7b..5a659c606 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -8,112 +8,117 @@ using namespace NicheGraphics; -void InkHUD::HeardApplet::onActivate() { - // When applet begins, pre-fill with stale info from NodeDB - populateFromNodeDB(); +void InkHUD::HeardApplet::onActivate() +{ + // When applet begins, pre-fill with stale info from NodeDB + populateFromNodeDB(); } -void InkHUD::HeardApplet::onDeactivate() { - // Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB - cards.clear(); +void InkHUD::HeardApplet::onDeactivate() +{ + // Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB + cards.clear(); } // When base applet hears a new packet, it extracts the info and passes it to us as CardInfo // We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result -void InkHUD::HeardApplet::handleParsed(CardInfo c) { - // Grab the previous entry. - // To check if the new data is different enough to justify re-render - // Need to cache now, before we manipulate the deque - CardInfo previous; - if (!cards.empty()) - previous = cards.at(0); +void InkHUD::HeardApplet::handleParsed(CardInfo c) +{ + // Grab the previous entry. + // To check if the new data is different enough to justify re-render + // Need to cache now, before we manipulate the deque + CardInfo previous; + if (!cards.empty()) + previous = cards.at(0); - // If we're updating an existing entry, remove the old one. Will reinsert at front - for (auto it = cards.begin(); it != cards.end(); ++it) { - if (it->nodeNum == c.nodeNum) { - cards.erase(it); - break; + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = cards.begin(); it != cards.end(); ++it) { + if (it->nodeNum == c.nodeNum) { + cards.erase(it); + break; + } } - } - cards.push_front(c); // Insert into base class' card collection - cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen - cards.shrink_to_fit(); + cards.push_front(c); // Insert into base class' card collection + cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen + cards.shrink_to_fit(); - // Our rendered image needs to change if: - if (previous.nodeNum != c.nodeNum // Different node - || previous.signal != c.signal // or different signal strength - || previous.distanceMeters != c.distanceMeters // or different position - || previous.hopsAway != c.hopsAway) // or different hops away - { - requestAutoshow(); - requestUpdate(); - } + // Our rendered image needs to change if: + if (previous.nodeNum != c.nodeNum // Different node + || previous.signal != c.signal // or different signal strength + || previous.distanceMeters != c.distanceMeters // or different position + || previous.hopsAway != c.hopsAway) // or different hops away + { + requestAutoshow(); + requestUpdate(); + } } // When applet is activated, pre-fill with stale data from NodeDB // We're sorting using the last_heard value. Susceptible to weirdness if node's RTC changes. // No SNR is available in node db, so we can't calculate signal either -// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet -// instead -void InkHUD::HeardApplet::populateFromNodeDB() { - // Fill a collection with pointers to each node in db - std::vector ordered; - for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) { - // Only copy if valid, and not our own node - if (mn->num != 0 && mn->num != nodeDB->getNodeNum()) - ordered.push_back(&*mn); - } - - // Sort the collection by age - std::sort(ordered.begin(), ordered.end(), - [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { return (top->last_heard > bottom->last_heard); }); - - // Keep the most recent entries only - // Just enough to fill the screen - if (ordered.size() > maxCards()) - ordered.resize(maxCards()); - - // Create card info for these (stale) node observations - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - for (meshtastic_NodeInfoLite *node : ordered) { - CardInfo c; - c.nodeNum = node->num; - - if (node->has_hops_away) - c.hopsAway = node->hops_away; - - if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { - // Get lat and long as float - // Meshtastic stores these as integers internally - float ourLat = ourNode->position.latitude_i * 1e-7; - float ourLong = ourNode->position.longitude_i * 1e-7; - float theirLat = node->position.latitude_i * 1e-7; - float theirLong = node->position.longitude_i * 1e-7; - - c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); +// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead +void InkHUD::HeardApplet::populateFromNodeDB() +{ + // Fill a collection with pointers to each node in db + std::vector ordered; + for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) { + // Only copy if valid, and not our own node + if (mn->num != 0 && mn->num != nodeDB->getNodeNum()) + ordered.push_back(&*mn); } - // Insert into the card collection (member of base class) - cards.push_back(c); - } + // Sort the collection by age + std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { + return (top->last_heard > bottom->last_heard); + }); + + // Keep the most recent entries only + // Just enough to fill the screen + if (ordered.size() > maxCards()) + ordered.resize(maxCards()); + + // Create card info for these (stale) node observations + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + for (meshtastic_NodeInfoLite *node : ordered) { + CardInfo c; + c.nodeNum = node->num; + + if (node->has_hops_away) + c.hopsAway = node->hops_away; + + if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { + // Get lat and long as float + // Meshtastic stores these as integers internally + float ourLat = ourNode->position.latitude_i * 1e-7; + float ourLong = ourNode->position.longitude_i * 1e-7; + float theirLat = node->position.latitude_i * 1e-7; + float theirLong = node->position.longitude_i * 1e-7; + + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + } + + // Insert into the card collection (member of base class) + cards.push_back(c); + } } // Text drawn in the usual applet header // Handled by base class: ChronoListApplet -std::string InkHUD::HeardApplet::getHeaderText() { - uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node +std::string InkHUD::HeardApplet::getHeaderText() +{ + uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node - std::string text = "Heard: "; + std::string text = "Heard: "; - // Print node count, if nodeDB not yet nearing full - if (nodeCount < MAX_NUM_NODES) { - text += to_string(nodeCount); // Max nodes - text += " "; - text += (nodeCount == 1) ? "node" : "nodes"; - } + // Print node count, if nodeDB not yet nearing full + if (nodeCount < MAX_NUM_NODES) { + text += to_string(nodeCount); // Max nodes + text += " "; + text += (nodeCount == 1) ? "node" : "nodes"; + } - return text; + return text; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h index fbdaca33c..932b5a75e 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h @@ -13,19 +13,21 @@ Most of the work is done by the InkHUD::NodeListApplet base class #include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class HeardApplet : public NodeListApplet { -public: - HeardApplet() : NodeListApplet("HeardApplet") {} - void onActivate() override; - void onDeactivate() override; +class HeardApplet : public NodeListApplet +{ + public: + HeardApplet() : NodeListApplet("HeardApplet") {} + void onActivate() override; + void onDeactivate() override; -protected: - void handleParsed(CardInfo c) override; // Store new info, and update display if needed - std::string getHeaderText() override; // Set title for this applet + protected: + void handleParsed(CardInfo c) override; // Store new info, and update display if needed + std::string getHeaderText() override; // Set title for this applet - void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB + void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp index af14a8426..ad0f9fc47 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -5,105 +5,107 @@ using namespace NicheGraphics; -void InkHUD::PositionsApplet::onRender() { - // Draw the usual map applet first - MapApplet::onRender(); +void InkHUD::PositionsApplet::onRender() +{ + // Draw the usual map applet first + MapApplet::onRender(); - // Draw our latest "node of interest" as a special marker - // ------------------------------------------------------- - // We might be rendering because we got a position packet from them - // We might be rendering because our own position updated - // Either way, we still highlight which node most recently sent us a position packet - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); - if (node && nodeDB->hasValidPosition(node) && enoughMarkers()) - drawLabeledMarker(node); + // Draw our latest "node of interest" as a special marker + // ------------------------------------------------------- + // We might be rendering because we got a position packet from them + // We might be rendering because our own position updated + // Either way, we still highlight which node most recently sent us a position packet + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); + if (node && nodeDB->hasValidPosition(node) && enoughMarkers()) + drawLabeledMarker(node); } // Determine if we need to redraw the map, when we receive a new position packet -ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) { - // If applet is not active, we shouldn't be handling any data - // It's good practice for all applets to implement an early return like this - // for PositionsApplet, this is **required** - it's where we're handling active vs deactive - if (!isActive()) +ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // If applet is not active, we shouldn't be handling any data + // It's good practice for all applets to implement an early return like this + // for PositionsApplet, this is **required** - it's where we're handling active vs deactive + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Try decode a position from the packet + bool hasPosition = false; + float lat; + float lng; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position position = meshtastic_Position_init_default; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { + if (position.has_latitude_i && position.has_longitude_i // Actually has position + && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" + { + hasPosition = true; + lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format + lng = position.longitude_i * 1e-7; + } + } + } + + // Skip if we didn't get a valid position + if (!hasPosition) + return ProcessMessage::CONTINUE; + + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; + + // Determine if the position packet would change anything on-screen + // ----------------------------------------------------------------- + + bool somethingChanged = false; + + // If our own position + if (isFromUs(&mp)) { + // We get frequent position updates from connected phone + // Only update if we're travelled some distance, for rate limiting + // Todo: smarter detection of position changes + if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { + somethingChanged = true; + ourLastLat = lat; + ourLastLng = lng; + } + } + + // If someone else's position + else { + // Check if this position is from someone different than our previous position packet + if (mp.from != lastFrom) { + somethingChanged = true; + lastFrom = mp.from; + lastLat = lat; + lastLng = lng; + lastHopsAway = hopsAway; + } + + // Same sender: check if position changed + // Todo: smarter detection of position changes + else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { + somethingChanged = true; + lastLat = lat; + lastLng = lng; + } + + // Same sender, same position: check if hops changed + // Only pay attention if the hopsAway value is valid + else if (hasHopsAway && (hopsAway != lastHopsAway)) { + somethingChanged = true; + lastHopsAway = hopsAway; + } + } + + // Decision reached + // ----------------- + + if (somethingChanged) { + requestAutoshow(); // Todo: only request this in some situations? + requestUpdate(); + } + return ProcessMessage::CONTINUE; - - // Try decode a position from the packet - bool hasPosition = false; - float lat; - float lng; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { - meshtastic_Position position = meshtastic_Position_init_default; - if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { - if (position.has_latitude_i && position.has_longitude_i // Actually has position - && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" - { - hasPosition = true; - lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format - lng = position.longitude_i * 1e-7; - } - } - } - - // Skip if we didn't get a valid position - if (!hasPosition) - return ProcessMessage::CONTINUE; - - const int8_t hopsAway = getHopsAway(mp); - const bool hasHopsAway = hopsAway >= 0; - - // Determine if the position packet would change anything on-screen - // ----------------------------------------------------------------- - - bool somethingChanged = false; - - // If our own position - if (isFromUs(&mp)) { - // We get frequent position updates from connected phone - // Only update if we're travelled some distance, for rate limiting - // Todo: smarter detection of position changes - if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { - somethingChanged = true; - ourLastLat = lat; - ourLastLng = lng; - } - } - - // If someone else's position - else { - // Check if this position is from someone different than our previous position packet - if (mp.from != lastFrom) { - somethingChanged = true; - lastFrom = mp.from; - lastLat = lat; - lastLng = lng; - lastHopsAway = hopsAway; - } - - // Same sender: check if position changed - // Todo: smarter detection of position changes - else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { - somethingChanged = true; - lastLat = lat; - lastLng = lng; - } - - // Same sender, same position: check if hops changed - // Only pay attention if the hopsAway value is valid - else if (hasHopsAway && (hopsAway != lastHopsAway)) { - somethingChanged = true; - lastHopsAway = hopsAway; - } - } - - // Decision reached - // ----------------- - - if (somethingChanged) { - requestAutoshow(); // Todo: only request this in some situations? - requestUpdate(); - } - - return ProcessMessage::CONTINUE; } #endif diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h index 74278f7dd..28a53cb0f 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h @@ -17,23 +17,25 @@ The node which has most recently sent a position will be labeled. #include "SinglePortModule.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class PositionsApplet : public MapApplet, public SinglePortModule { -public: - PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} - void onRender() override; +class PositionsApplet : public MapApplet, public SinglePortModule +{ + public: + PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} + void onRender() override; -protected: - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + protected: + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet - float lastLat = 0.0; - float lastLng = 0.0; - float lastHopsAway = 0; + NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet + float lastLat = 0.0; + float lastLng = 0.0; + float lastHopsAway = 0; - float ourLastLat = 0.0; // Info about the most recent (non-local) position packet - float ourLastLng = 0.0; // Info about most recent *local* position + float ourLastLat = 0.0; // Info about the most recent (non-local) position packet + float ourLastLng = 0.0; // Info about most recent *local* position }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp index 18687ef16..1ccf7fc14 100644 --- a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp @@ -6,140 +6,148 @@ using namespace NicheGraphics; -InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") { - // No scheduled tasks initially - OSThread::disable(); +InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") +{ + // No scheduled tasks initially + OSThread::disable(); } -void InkHUD::RecentsListApplet::onActivate() { - // When the applet is activated, begin scheduled purging of any nodes which are no longer "active" - OSThread::enabled = true; - OSThread::setIntervalFromNow(60 * 1000UL); // Every minute +void InkHUD::RecentsListApplet::onActivate() +{ + // When the applet is activated, begin scheduled purging of any nodes which are no longer "active" + OSThread::enabled = true; + OSThread::setIntervalFromNow(60 * 1000UL); // Every minute } -void InkHUD::RecentsListApplet::onDeactivate() { - // Halt scheduled purging - OSThread::disable(); +void InkHUD::RecentsListApplet::onDeactivate() +{ + // Halt scheduled purging + OSThread::disable(); } -int32_t InkHUD::RecentsListApplet::runOnce() { - prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently - return OSThread::interval; +int32_t InkHUD::RecentsListApplet::runOnce() +{ + prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently + return OSThread::interval; } // When base applet hears a new packet, it extracts the info and passes it to us as CardInfo // We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result // We also need to record the current time against the nodenum, so we know when it becomes inactive -void InkHUD::RecentsListApplet::handleParsed(CardInfo c) { - // Grab the previous entry. - // To check if the new data is different enough to justify re-render - // Need to cache now, before we manipulate the deque - CardInfo previous; - if (!cards.empty()) - previous = cards.at(0); +void InkHUD::RecentsListApplet::handleParsed(CardInfo c) +{ + // Grab the previous entry. + // To check if the new data is different enough to justify re-render + // Need to cache now, before we manipulate the deque + CardInfo previous; + if (!cards.empty()) + previous = cards.at(0); - // If we're updating an existing entry, remove the old one. Will reinsert at front - for (auto it = cards.begin(); it != cards.end(); ++it) { - if (it->nodeNum == c.nodeNum) { - cards.erase(it); - break; + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = cards.begin(); it != cards.end(); ++it) { + if (it->nodeNum == c.nodeNum) { + cards.erase(it); + break; + } } - } - cards.push_front(c); // Store this CardInfo - cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen - cards.shrink_to_fit(); + cards.push_front(c); // Store this CardInfo + cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen + cards.shrink_to_fit(); - // Record the time of this observation - // Used to count active nodes, and to know when to prune inactive nodes - seenNow(c.nodeNum); + // Record the time of this observation + // Used to count active nodes, and to know when to prune inactive nodes + seenNow(c.nodeNum); - // Our rendered image needs to change if: - if (previous.nodeNum != c.nodeNum // Different node - || previous.signal != c.signal // or different signal strength - || previous.distanceMeters != c.distanceMeters // or different position - || previous.hopsAway != c.hopsAway) // or different hops away - { - prune(); // Take the opportunity now to remove inactive nodes - requestAutoshow(); - requestUpdate(); - } + // Our rendered image needs to change if: + if (previous.nodeNum != c.nodeNum // Different node + || previous.signal != c.signal // or different signal strength + || previous.distanceMeters != c.distanceMeters // or different position + || previous.hopsAway != c.hopsAway) // or different hops away + { + prune(); // Take the opportunity now to remove inactive nodes + requestAutoshow(); + requestUpdate(); + } } // Record the time (millis, right now) that we hear a node -// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs -// regularly -void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) { - // If we're updating an existing entry, remove the old one. Will reinsert at front - for (auto it = ages.begin(); it != ages.end(); ++it) { - if (it->nodeNum == nodeNum) { - ages.erase(it); - break; +// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly +void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) +{ + // If we're updating an existing entry, remove the old one. Will reinsert at front + for (auto it = ages.begin(); it != ages.end(); ++it) { + if (it->nodeNum == nodeNum) { + ages.erase(it); + break; + } } - } - Age a; - a.nodeNum = nodeNum; - a.seenAtMs = millis(); + Age a; + a.nodeNum = nodeNum; + a.seenAtMs = millis(); - ages.push_front(a); + ages.push_front(a); } // Remove Card and Age info for any nodes which are now inactive // Determined by when a node was last heard, in our internal record (not from nodeDB) -void InkHUD::RecentsListApplet::prune() { - // Iterate age records from newest to oldest - for (uint16_t i = 0; i < ages.size(); i++) { - // Found the first record which is too old - if (!isActive(ages.at(i).seenAtMs)) { - // Drop this item, and all others behind it - ages.resize(i); - ages.shrink_to_fit(); - cards.resize(i); - cards.shrink_to_fit(); +void InkHUD::RecentsListApplet::prune() +{ + // Iterate age records from newest to oldest + for (uint16_t i = 0; i < ages.size(); i++) { + // Found the first record which is too old + if (!isActive(ages.at(i).seenAtMs)) { + // Drop this item, and all others behind it + ages.resize(i); + ages.shrink_to_fit(); + cards.resize(i); + cards.shrink_to_fit(); - // Request an update, if pruning did modify our data - // Required if pruning was scheduled. Redundant if pruning was prior to rendering. - requestAutoshow(); - requestUpdate(); + // Request an update, if pruning did modify our data + // Required if pruning was scheduled. Redundant if pruning was prior to rendering. + requestAutoshow(); + requestUpdate(); - break; + break; + } } - } - // Push next scheduled pruning back - // Pruning may be called from by handleParsed, immediately prior to rendering - // In that case, we can slightly delay our scheduled pruning - OSThread::setIntervalFromNow(60 * 1000UL); + // Push next scheduled pruning back + // Pruning may be called from by handleParsed, immediately prior to rendering + // In that case, we can slightly delay our scheduled pruning + OSThread::setIntervalFromNow(60 * 1000UL); } // Is a timestamp old enough that it would make a node inactive, and in need of purging? -bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) { - uint32_t now = millis(); - uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe +bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) +{ + uint32_t now = millis(); + uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe - return (secsAgo < settings->recentlyActiveSeconds); + return (secsAgo < settings->recentlyActiveSeconds); } // Text to be shown at top of applet // ChronoListApplet base class allows us to set this dynamically // Might want to adjust depending on node count, RTC status, etc -std::string InkHUD::RecentsListApplet::getHeaderText() { - std::string text; +std::string InkHUD::RecentsListApplet::getHeaderText() +{ + std::string text; - // Print the length of our "Recents" time-window - text += "Last "; - text += to_string(settings->recentlyActiveSeconds / 60); - text += " mins"; + // Print the length of our "Recents" time-window + text += "Last "; + text += to_string(settings->recentlyActiveSeconds / 60); + text += " mins"; - // Print the node count - const uint16_t nodeCount = ages.size(); - text += ": "; - text += to_string(nodeCount); - text += " "; - text += (nodeCount == 1) ? "node" : "nodes"; + // Print the node count + const uint16_t nodeCount = ages.size(); + text += ": "; + text += to_string(nodeCount); + text += " "; + text += (nodeCount == 1) ? "node" : "nodes"; - return text; + return text; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h index 12a4d24f1..74f5f3e57 100644 --- a/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h @@ -15,34 +15,36 @@ Most of the work is done by the shared InkHUD::NodeListApplet base class #include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class RecentsListApplet : public NodeListApplet, public concurrency::OSThread { -protected: - // Used internally to count the number of active nodes - // We count for ourselves, instead of using the value provided by NodeDB, - // as the values occasionally differ, due to the timing of our Applet's purge method - struct Age { - uint32_t nodeNum; - uint32_t seenAtMs; - }; +class RecentsListApplet : public NodeListApplet, public concurrency::OSThread +{ + protected: + // Used internally to count the number of active nodes + // We count for ourselves, instead of using the value provided by NodeDB, + // as the values occasionally differ, due to the timing of our Applet's purge method + struct Age { + uint32_t nodeNum; + uint32_t seenAtMs; + }; -public: - RecentsListApplet(); - void onActivate() override; - void onDeactivate() override; + public: + RecentsListApplet(); + void onActivate() override; + void onDeactivate() override; -protected: - int32_t runOnce() override; + protected: + int32_t runOnce() override; - void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed - std::string getHeaderText() override; // Set title for this applet + void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed + std::string getHeaderText() override; // Set title for this applet - void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count - void prune(); // Remove cards for nodes which we haven't seen recently - bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it? + void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count + void prune(); // Remove cards for nodes which we haven't seen recently + bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it? - std::deque ages; // Information about when we last heard nodes. Independent of NodeDB + std::deque ages; // Information about when we last heard nodes. Independent of NodeDB }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index 478904b8a..fdb5a168d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -14,246 +14,255 @@ constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) - : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { - // Create the message store - // Will shortly attempt to load messages from RAM, if applet is active - // Label (filename in flash) is set from channel index - store = new MessageStore("ch" + to_string(channelIndex)); + : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) +{ + // Create the message store + // Will shortly attempt to load messages from RAM, if applet is active + // Label (filename in flash) is set from channel index + store = new MessageStore("ch" + to_string(channelIndex)); } -void InkHUD::ThreadedMessageApplet::onRender() { - // ============= - // Draw a header - // ============= +void InkHUD::ThreadedMessageApplet::onRender() +{ + // ============= + // Draw a header + // ============= - // Header text - std::string headerText; - headerText += "Channel "; - headerText += to_string(channelIndex); - headerText += ": "; - if (channels.isDefaultChannel(channelIndex)) - headerText += "Public"; - else - headerText += channels.getByIndex(channelIndex).settings.name; + // Header text + std::string headerText; + headerText += "Channel "; + headerText += to_string(channelIndex); + headerText += ": "; + if (channels.isDefaultChannel(channelIndex)) + headerText += "Public"; + else + headerText += channels.getByIndex(channelIndex).settings.name; - // Draw a "standard" applet header - drawHeader(headerText); + // Draw a "standard" applet header + drawHeader(headerText); - // Y position for divider - const int16_t dividerY = Applet::getHeaderHeight() - 1; + // Y position for divider + const int16_t dividerY = Applet::getHeaderHeight() - 1; - // ================== - // Draw each message - // ================== + // ================== + // Draw each message + // ================== - // Restrict drawing area - // - don't overdraw the header - // - small gap below divider - setCrop(0, dividerY + 2, width(), height() - (dividerY + 2)); + // Restrict drawing area + // - don't overdraw the header + // - small gap below divider + setCrop(0, dividerY + 2, width(), height() - (dividerY + 2)); - // Set padding - // - separates text from the vertical line which marks its edge - constexpr uint16_t padW = 2; - constexpr int16_t msgL = padW; - const int16_t msgR = (width() - 1) - padW; - const uint16_t msgW = (msgR - msgL) + 1; + // Set padding + // - separates text from the vertical line which marks its edge + constexpr uint16_t padW = 2; + constexpr int16_t msgL = padW; + const int16_t msgR = (width() - 1) - padW; + const uint16_t msgW = (msgR - msgL) + 1; - int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. - uint8_t i = 0; // Index of stored message + int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. + uint8_t i = 0; // Index of stored message - // Loop over messages - // - until no messages left, or - // - until no part of message fits on screen - while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { + // Loop over messages + // - until no messages left, or + // - until no part of message fits on screen + while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { - // Grab data for message - MessageStore::Message &m = store->messages.at(i); - bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message - std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message + // Grab data for message + MessageStore::Message &m = store->messages.at(i); + bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message + std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message - // Cache bottom Y of message text - // - Used when drawing vertical line alongside - const int16_t dotsB = msgB; + // Cache bottom Y of message text + // - Used when drawing vertical line alongside + const int16_t dotsB = msgB; - // Get dimensions for message text - uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText); - int16_t bodyT = msgB - bodyH; + // Get dimensions for message text + uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText); + int16_t bodyT = msgB - bodyH; - // Print message - // - if incoming - if (!outgoing) - printWrapped(msgL, bodyT, msgW, bodyText); + // Print message + // - if incoming + if (!outgoing) + printWrapped(msgL, bodyT, msgW, bodyText); - // Print message - // - if outgoing - else { - if (getTextWidth(bodyText) < width()) // If short, - printAt(msgR, bodyT, bodyText, RIGHT); // print right align - else // If long, - printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align - } + // Print message + // - if outgoing + else { + if (getTextWidth(bodyText) < width()) // If short, + printAt(msgR, bodyT, bodyText, RIGHT); // print right align + else // If long, + printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align + } - // Move cursor up - // - above message text - msgB -= bodyH; - msgB -= getFont().lineHeight() * 0.2; // Padding between message and header + // Move cursor up + // - above message text + msgB -= bodyH; + msgB -= getFont().lineHeight() * 0.2; // Padding between message and header - // Compose info string - // - shortname, if possible, or "me" - // - time received, if possible - std::string info; - if (outgoing) - info += "Me"; - else { - // Check if sender is node db - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); - if (sender) - info += parseShortName(sender); // Handle any unprintable chars in short name - else - info += hexifyNodeNum(m.sender); // No node info at all. Print the node num - } + // Compose info string + // - shortname, if possible, or "me" + // - time received, if possible + std::string info; + if (outgoing) + info += "Me"; + else { + // Check if sender is node db + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); + if (sender) + info += parseShortName(sender); // Handle any unprintable chars in short name + else + info += hexifyNodeNum(m.sender); // No node info at all. Print the node num + } - std::string timeString = getTimeString(m.timestamp); - if (timeString.length() > 0) { - info += " - "; - info += timeString; - } + std::string timeString = getTimeString(m.timestamp); + if (timeString.length() > 0) { + info += " - "; + info += timeString; + } - // Print the info string - // - Faux bold: printed twice, shifted horizontally by one px - printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); - printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); + // Print the info string + // - Faux bold: printed twice, shifted horizontally by one px + printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); + printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); - // Underline the info string - const int16_t divY = msgB; - int16_t divL; - int16_t divR; - if (!outgoing) { - // Left side - incoming - divL = msgL; - divR = getTextWidth(info) + getFont().lineHeight() / 2; - } else { - // Right side - outgoing - divR = msgR; - divL = divR - getTextWidth(info) - getFont().lineHeight() / 2; - } - for (int16_t x = divL; x <= divR; x += 2) - drawPixel(x, divY, BLACK); + // Underline the info string + const int16_t divY = msgB; + int16_t divL; + int16_t divR; + if (!outgoing) { + // Left side - incoming + divL = msgL; + divR = getTextWidth(info) + getFont().lineHeight() / 2; + } else { + // Right side - outgoing + divR = msgR; + divL = divR - getTextWidth(info) - getFont().lineHeight() / 2; + } + for (int16_t x = divL; x <= divR; x += 2) + drawPixel(x, divY, BLACK); - // Move cursor up: above info string - msgB -= fontSmall.lineHeight(); + // Move cursor up: above info string + msgB -= fontSmall.lineHeight(); - // Vertical line alongside message - for (int16_t y = msgB; y < dotsB; y += 1) - drawPixel(outgoing ? width() - 1 : 0, y, BLACK); + // Vertical line alongside message + for (int16_t y = msgB; y < dotsB; y += 1) + drawPixel(outgoing ? width() - 1 : 0, y, BLACK); - // Move cursor up: padding before next message - msgB -= fontSmall.lineHeight() * 0.5; + // Move cursor up: padding before next message + msgB -= fontSmall.lineHeight() * 0.5; - i++; - } // End of loop: drawing each message + i++; + } // End of loop: drawing each message - // Fade effect: - // Area immediately below the divider. Overdraw with sparse white lines. - // Make text appear to pass behind the header - hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); + // Fade effect: + // Area immediately below the divider. Overdraw with sparse white lines. + // Make text appear to pass behind the header + hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); - // If we've run out of screen to draw messages, we can drop any leftover data from the queue - // Those messages have been pushed off the screen-top by newer ones - while (i < store->messages.size()) - store->messages.pop_back(); + // If we've run out of screen to draw messages, we can drop any leftover data from the queue + // Those messages have been pushed off the screen-top by newer ones + while (i < store->messages.size()) + store->messages.pop_back(); } // Code which runs when the applet begins running // This might happen at boot, or if user enables the applet at run-time, via the menu -void InkHUD::ThreadedMessageApplet::onActivate() { - loadMessagesFromFlash(); - loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) +void InkHUD::ThreadedMessageApplet::onActivate() +{ + loadMessagesFromFlash(); + loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) } // Code which runs when the applet stop running // This might be at shutdown, or if the user disables the applet at run-time, via the menu -void InkHUD::ThreadedMessageApplet::onDeactivate() { - loopbackOk = false; // Slightly reduce our impact if the applet is disabled +void InkHUD::ThreadedMessageApplet::onDeactivate() +{ + loopbackOk = false; // Slightly reduce our impact if the applet is disabled } // Handle new text messages // These might be incoming, from the mesh, or outgoing from phone // Each instance of the ThreadMessageApplet will only listen on one specific channel -ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) { - // Abort if applet fully deactivated - if (!isActive()) +ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // Abort if applet fully deactivated + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Abort if wrong channel + if (mp.channel != this->channelIndex) + return ProcessMessage::CONTINUE; + + // Abort if message was a DM + if (mp.to != NODENUM_BROADCAST) + return ProcessMessage::CONTINUE; + + // Extract info into our slimmed-down "StoredMessage" type + MessageStore::Message newMessage; + newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + newMessage.sender = mp.from; + newMessage.channelIndex = mp.channel; + newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); + + // Store newest message at front + // These records are used when rendering, and also stored in flash at shutdown + store->messages.push_front(newMessage); + + // If this was an incoming message, suggest that our applet becomes foreground, if permitted + if (getFrom(&mp) != nodeDB->getNodeNum()) + requestAutoshow(); + + // Redraw the applet, perhaps. + requestUpdate(); // Want to update display, if applet is foreground + + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages return ProcessMessage::CONTINUE; - - // Abort if wrong channel - if (mp.channel != this->channelIndex) - return ProcessMessage::CONTINUE; - - // Abort if message was a DM - if (mp.to != NODENUM_BROADCAST) - return ProcessMessage::CONTINUE; - - // Extract info into our slimmed-down "StoredMessage" type - MessageStore::Message newMessage; - newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - newMessage.sender = mp.from; - newMessage.channelIndex = mp.channel; - newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); - - // Store newest message at front - // These records are used when rendering, and also stored in flash at shutdown - store->messages.push_front(newMessage); - - // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(&mp) != nodeDB->getNodeNum()) - requestAutoshow(); - - // Redraw the applet, perhaps. - requestUpdate(); // Want to update display, if applet is foreground - - // Tell Module API to continue informing other firmware components about this message - // We're not the only component which is interested in new text messages - return ProcessMessage::CONTINUE; } // Don't show notifications for text messages broadcast to our channel, when the applet is displayed -bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) { - if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex) - return false; +bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) +{ + if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex) + return false; - // None of our business. Allow the notification. - else - return true; + // None of our business. Allow the notification. + else + return true; } // Save several recent messages to flash // Stores the contents of ThreadedMessageApplet::messages // Just enough messages to fill the display // Messages are packed "back-to-back", to minimize blocks of flash used -void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() { - // Create a label (will become the filename in flash) - std::string label = "ch" + to_string(channelIndex); +void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() +{ + // Create a label (will become the filename in flash) + std::string label = "ch" + to_string(channelIndex); - store->saveToFlash(); + store->saveToFlash(); } // Load recent messages to flash // Fills ThreadedMessageApplet::messages with previous messages // Just enough messages have been stored to cover the display -void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() { - // Create a label (will become the filename in flash) - std::string label = "ch" + to_string(channelIndex); +void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() +{ + // Create a label (will become the filename in flash) + std::string label = "ch" + to_string(channelIndex); - store->loadFromFlash(); + store->loadFromFlash(); } // Code to run when device is shutting down // This is in addition to any onDeactivate() code, which will also run // Todo: implement before a reboot also -void InkHUD::ThreadedMessageApplet::onShutdown() { - // Save our current set of messages to flash, provided the applet isn't disabled - if (isActive()) - saveMessagesToFlash(); +void InkHUD::ThreadedMessageApplet::onShutdown() +{ + // Save our current set of messages to flash, provided the applet isn't disabled + if (isActive()) + saveMessagesToFlash(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index d6012795d..c986539b3 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -25,30 +25,32 @@ Suggest a max of two channel, to minimize fs usage? #include "modules/TextMessageModule.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ class Applet; -class ThreadedMessageApplet : public Applet, public SinglePortModule { -public: - explicit ThreadedMessageApplet(uint8_t channelIndex); - ThreadedMessageApplet() = delete; +class ThreadedMessageApplet : public Applet, public SinglePortModule +{ + public: + explicit ThreadedMessageApplet(uint8_t channelIndex); + ThreadedMessageApplet() = delete; - void onRender() override; + void onRender() override; - void onActivate() override; - void onDeactivate() override; - void onShutdown() override; - ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + void onActivate() override; + void onDeactivate() override; + void onShutdown() override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - bool approveNotification(Notification &n) override; // Which notifications to suppress + bool approveNotification(Notification &n) override; // Which notifications to suppress -protected: - void saveMessagesToFlash(); - void loadMessagesFromFlash(); + protected: + void saveMessagesToFlash(); + void loadMessagesFromFlash(); - MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown - uint8_t channelIndex = 0; + MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown + uint8_t channelIndex = 0; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index 76716d871..e8849b72e 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -10,157 +10,167 @@ using namespace NicheGraphics; static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; -InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") { - // Timer disabled by default - OSThread::disable(); +InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") +{ + // Timer disabled by default + OSThread::disable(); } // Request which update type we would prefer, when the display image next changes // DisplayHealth class will consider our suggestion, and weigh it against other requests -void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type) { - // Update our "working decision", to decide if this request is important enough to change our plan - if (!forced) - workingDecision = prioritize(workingDecision, type); +void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type) +{ + // Update our "working decision", to decide if this request is important enough to change our plan + if (!forced) + workingDecision = prioritize(workingDecision, type); } // Demand that a specific update type be used, when the display image next changes // Note: multiple DisplayHealth::force calls should not be made, // but if they are, the importance of the type will be weighed the same as if both calls were to DisplayHealth::request -void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type) { - if (!forced) - workingDecision = type; - else - workingDecision = prioritize(workingDecision, type); +void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type) +{ + if (!forced) + workingDecision = type; + else + workingDecision = prioritize(workingDecision, type); - forced = true; + forced = true; } // Find out which update type the DisplayHealth has chosen for us // Calling this method consumes the result, and resets for the next update -Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType() { - LOG_DEBUG("FULL-update debt:%f", debt); +Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType() +{ + LOG_DEBUG("FULL-update debt:%f", debt); - // For convenience - typedef Drivers::EInk::UpdateTypes UpdateTypes; + // For convenience + typedef Drivers::EInk::UpdateTypes UpdateTypes; - // Grab our final decision for the update type, so we can reset now, for the next update - // We do this at top of the method, so we can return early - UpdateTypes finalDecision = workingDecision; - workingDecision = UpdateTypes::UNSPECIFIED; - forced = false; + // Grab our final decision for the update type, so we can reset now, for the next update + // We do this at top of the method, so we can return early + UpdateTypes finalDecision = workingDecision; + workingDecision = UpdateTypes::UNSPECIFIED; + forced = false; - // Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress) - // This maintenance behavior will also have opportunity to halt itself when the timer next fires, - // but that could be an hour away, so we can stop it early here and free up resources - if (OSThread::enabled && debt == 0.0) - endMaintenance(); + // Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress) + // This maintenance behavior will also have opportunity to halt itself when the timer next fires, + // but that could be an hour away, so we can stop it early here and free up resources + if (OSThread::enabled && debt == 0.0) + endMaintenance(); - // Explicitly requested FULL - if (finalDecision == UpdateTypes::FULL) { - LOG_DEBUG("Explicit FULL"); - debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt - return UpdateTypes::FULL; - } + // Explicitly requested FULL + if (finalDecision == UpdateTypes::FULL) { + LOG_DEBUG("Explicit FULL"); + debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt + return UpdateTypes::FULL; + } - // Explicitly requested FAST - if (finalDecision == UpdateTypes::FAST) { - LOG_DEBUG("Explicit FAST"); - // Add to the FULL refresh debt - if (debt < 1.0) - debt += 1.0 / fastPerFull; - else - debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes + // Explicitly requested FAST + if (finalDecision == UpdateTypes::FAST) { + LOG_DEBUG("Explicit FAST"); + // Add to the FULL refresh debt + if (debt < 1.0) + debt += 1.0 / fastPerFull; + else + debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes - // If *significant debt*, begin occasionally refreshing *unprovoked* - // This maintenance behavior is only triggered here, by periods of user interaction - // Debt would otherwise not be able to climb above 1.0 - if (debt >= 2.0) - beginMaintenance(); + // If *significant debt*, begin occasionally refreshing *unprovoked* + // This maintenance behavior is only triggered here, by periods of user interaction + // Debt would otherwise not be able to climb above 1.0 + if (debt >= 2.0) + beginMaintenance(); - return UpdateTypes::FAST; // Give them what the asked for - } + return UpdateTypes::FAST; // Give them what the asked for + } - // Handling UpdateTypes::UNSPECIFIED - // ----------------------------------- - // In this case, the UI doesn't care which refresh we use + // Handling UpdateTypes::UNSPECIFIED + // ----------------------------------- + // In this case, the UI doesn't care which refresh we use - // Not much debt: suggest FAST - if (debt < 1.0) { - LOG_DEBUG("UNSPECIFIED: using FAST"); - debt += 1.0 / fastPerFull; - return UpdateTypes::FAST; - } + // Not much debt: suggest FAST + if (debt < 1.0) { + LOG_DEBUG("UNSPECIFIED: using FAST"); + debt += 1.0 / fastPerFull; + return UpdateTypes::FAST; + } - // In debt: suggest FULL - else { - LOG_DEBUG("UNSPECIFIED: using FULL"); - debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt + // In debt: suggest FULL + else { + LOG_DEBUG("UNSPECIFIED: using FULL"); + debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt - // When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so) - // If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh - // We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically - if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) - OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow + // When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so) + // If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh + // We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically + if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) + OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow - return UpdateTypes::FULL; - } + return UpdateTypes::FULL; + } } // Determine which of two update types is more important to honor // Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness // Explicit FULL is more important than explicit FAST - prioritize image quality: explicit FULL is rare // Used when multiple applets have all requested update simultaneously, each with their own preferred UpdateType -Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) { - switch (type1) { - case Drivers::EInk::UpdateTypes::UNSPECIFIED: - return type2; +Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) +{ + switch (type1) { + case Drivers::EInk::UpdateTypes::UNSPECIFIED: + return type2; - case Drivers::EInk::UpdateTypes::FAST: - return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST; + case Drivers::EInk::UpdateTypes::FAST: + return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST; - case Drivers::EInk::UpdateTypes::FULL: - return type1; - } + case Drivers::EInk::UpdateTypes::FULL: + return type1; + } - return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only + return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only } // We're using the timer to perform "maintenance" // If significant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked. // This prevents gradual build-up of debt, // in case we aren't doing enough UNSPECIFIED refreshes to pay the debt back organically. -// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the -// restoration Subsequent refreshes take place *much* less frequently. Hopefully an applet will want to render before -// this, meaning we can cancel the maintenance. -int32_t InkHUD::DisplayHealth::runOnce() { - if (debt > 0.0) { - LOG_DEBUG("debt=%f: performing maintenance", debt); +// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration +// Subsequent refreshes take place *much* less frequently. +// Hopefully an applet will want to render before this, meaning we can cancel the maintenance. +int32_t InkHUD::DisplayHealth::runOnce() +{ + if (debt > 0.0) { + LOG_DEBUG("debt=%f: performing maintenance", debt); - // Ask WindowManager to redraw everything, purely for the refresh - // Todo: optimize? Could update without re-rendering - InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL); + // Ask WindowManager to redraw everything, purely for the refresh + // Todo: optimize? Could update without re-rendering + InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL); - // Record that we have paid back (some of) the FULL refresh debt - debt = max(debt - 1.0, 0.0); + // Record that we have paid back (some of) the FULL refresh debt + debt = max(debt - 1.0, 0.0); - // Next maintenance refresh scheduled - long wait (an hour?) - return MAINTENANCE_MS; - } + // Next maintenance refresh scheduled - long wait (an hour?) + return MAINTENANCE_MS; + } - else - return endMaintenance(); + else + return endMaintenance(); } // Begin periodically refreshing the display, to repay FULL-refresh debt // We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED // After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently // This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable -void InkHUD::DisplayHealth::beginMaintenance() { - OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL); - OSThread::enabled = true; +void InkHUD::DisplayHealth::beginMaintenance() +{ + OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL); + OSThread::enabled = true; } // FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates -int32_t InkHUD::DisplayHealth::endMaintenance() { return OSThread::disable(); } +int32_t InkHUD::DisplayHealth::endMaintenance() +{ + return OSThread::disable(); +} #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/DisplayHealth.h b/src/graphics/niche/InkHUD/DisplayHealth.h index ed9d8fceb..2bd887f9d 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.h +++ b/src/graphics/niche/InkHUD/DisplayHealth.h @@ -18,31 +18,34 @@ Responsible for maintaining display health, by optimizing the ratio of FAST vs F #include "graphics/niche/Drivers/EInk/EInk.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class DisplayHealth : protected concurrency::OSThread { -public: - DisplayHealth(); +class DisplayHealth : protected concurrency::OSThread +{ + public: + DisplayHealth(); - void requestUpdateType(Drivers::EInk::UpdateTypes type); - void forceUpdateType(Drivers::EInk::UpdateTypes type); - Drivers::EInk::UpdateTypes decideUpdateType(); + void requestUpdateType(Drivers::EInk::UpdateTypes type); + void forceUpdateType(Drivers::EInk::UpdateTypes type); + Drivers::EInk::UpdateTypes decideUpdateType(); - uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes - float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull? + uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes + float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull? -private: - int32_t runOnce() override; - void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health - int32_t endMaintenance(); // End unprovoked refreshing: debt paid + private: + int32_t runOnce() override; + void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health + int32_t endMaintenance(); // End unprovoked refreshing: debt paid - Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1, - Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor + Drivers::EInk::UpdateTypes + prioritize(Drivers::EInk::UpdateTypes type1, + Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor - bool forced = false; - Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED; + bool forced = false; + Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED; - float debt = 0.0; // How many full refreshes are due + float debt = 0.0; // How many full refreshes are due }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index 3cdc8ce8f..5382d2391 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -14,78 +14,30 @@ using namespace NicheGraphics; -InkHUD::Events::Events() { - // Get convenient references - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; +InkHUD::Events::Events() +{ + // Get convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } -void InkHUD::Events::begin() { - // Register our callbacks for the various events +void InkHUD::Events::begin() +{ + // Register our callbacks for the various events - deepSleepObserver.observe(¬ifyDeepSleep); - rebootObserver.observe(¬ifyReboot); - textMessageObserver.observe(textMessageModule); + deepSleepObserver.observe(¬ifyDeepSleep); + rebootObserver.observe(¬ifyReboot); + textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe((Observable *)adminModule); + adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 - lightSleepObserver.observe(¬ifyLightSleep); + lightSleepObserver.observe(¬ifyLightSleep); #endif } -void InkHUD::Events::onButtonShort() { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - // If no system applet is handling input, default behavior instead is to cycle applets - // or open menu if joystick is enabled - if (consumer) { - consumer->onButtonShortPress(); - } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module - if (!settings->joystick.enabled) - inkhud->nextApplet(); - else - inkhud->openMenu(); - } -} - -void InkHUD::Events::onButtonLong() { - // Audio feedback (via buzzer) - // Slightly longer than playChirp - playBoop(); - - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } - } - - // If no system applet is handling input, default behavior instead is to open the menu - if (consumer) - consumer->onButtonLongPress(); - else - inkhud->openMenu(); -} - -void InkHUD::Events::onExitShort() { - if (settings->joystick.enabled) { +void InkHUD::Events::onButtonShort() +{ // Audio feedback (via buzzer) // Short tone playChirp(); @@ -96,22 +48,26 @@ void InkHUD::Events::onExitShort() { // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + if (sa->handleInput) { + consumer = sa; + break; + } } - // If no system applet is handling input, default behavior instead is change tiles - if (consumer) - consumer->onExitShort(); - else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module - inkhud->nextTile(); - } + // If no system applet is handling input, default behavior instead is to cycle applets + // or open menu if joystick is enabled + if (consumer) { + consumer->onButtonShortPress(); + } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } } -void InkHUD::Events::onExitLong() { - if (settings->joystick.enabled) { +void InkHUD::Events::onButtonLong() +{ // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); @@ -119,265 +75,325 @@ void InkHUD::Events::onExitLong() { // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + if (sa->handleInput) { + consumer = sa; + break; + } } + // If no system applet is handling input, default behavior instead is to open the menu if (consumer) - consumer->onExitLong(); - } + consumer->onButtonLongPress(); + else + inkhud->openMenu(); } -void InkHUD::Events::onNavUp() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onExitShort() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is change tiles + if (consumer) + consumer->onExitShort(); + else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module + inkhud->nextTile(); } - - if (consumer) - consumer->onNavUp(); - } } -void InkHUD::Events::onNavDown() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onExitLong() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Slightly longer than playChirp + playBoop(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onExitLong(); } - - if (consumer) - consumer->onNavDown(); - } } -void InkHUD::Events::onNavLeft() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onNavUp() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavUp(); } - - // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) - consumer->onNavLeft(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->prevApplet(); - } } -void InkHUD::Events::onNavRight() { - if (settings->joystick.enabled) { - // Audio feedback (via buzzer) - // Short tone - playChirp(); - // Cancel any beeping, buzzing, blinking - // Some button handling suppressed if we are dismissing an external notification (see below) - bool dismissedExt = dismissExternalNotification(); +void InkHUD::Events::onNavDown() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); - // Check which system applet wants to handle the button press (if any) - SystemApplet *consumer = nullptr; - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->handleInput) { - consumer = sa; - break; - } + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavDown(); } +} - // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) - consumer->onNavRight(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->nextApplet(); - } +void InkHUD::Events::onNavLeft() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavLeft(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->prevApplet(); + } +} + +void InkHUD::Events::onNavRight() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavRight(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->nextApplet(); + } } // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now -int InkHUD::Events::beforeDeepSleep(void *unused) { - // If a previous display update is in progress, wait for it to complete. - inkhud->awaitUpdate(); +int InkHUD::Events::beforeDeepSleep(void *unused) +{ + // If a previous display update is in progress, wait for it to complete. + inkhud->awaitUpdate(); - // Notify all applets that we're shutting down - for (Applet *ua : inkhud->userApplets) { - ua->onDeactivate(); - ua->onShutdown(); - } - for (SystemApplet *sa : inkhud->systemApplets) { - // Note: no onDeactivate. System applets are always active. - sa->onShutdown(); - } + // Notify all applets that we're shutting down + for (Applet *ua : inkhud->userApplets) { + ua->onDeactivate(); + ua->onShutdown(); + } + for (SystemApplet *sa : inkhud->systemApplets) { + // Note: no onDeactivate. System applets are always active. + sa->onShutdown(); + } - // User has successful executed a safe shutdown - // We don't need to nag at boot anymore - settings->tips.safeShutdownSeen = true; + // User has successful executed a safe shutdown + // We don't need to nag at boot anymore + settings->tips.safeShutdownSeen = true; - inkhud->persistence->saveSettings(); - inkhud->persistence->saveLatestMessage(); + inkhud->persistence->saveSettings(); + inkhud->persistence->saveLatestMessage(); - // LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice, - // then prepared a final powered-off screen for us, which shows device shortname. - // We're updating to show that one now. + // LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice, + // then prepared a final powered-off screen for us, which shows device shortname. + // We're updating to show that one now. - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); - delay(1000); // Cooldown, before potentially yanking display power + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + delay(1000); // Cooldown, before potentially yanking display power - // InkHUD shutdown complete - // Firmware shutdown continues for several seconds more; flash write still pending - playShutdownMelody(); + // InkHUD shutdown complete + // Firmware shutdown continues for several seconds more; flash write still pending + playShutdownMelody(); - return 0; // We agree: deep sleep now + return 0; // We agree: deep sleep now } // Callback for rebootObserver // Same as shutdown, without drawing the logoApplet // Makes sure we don't lose message history / InkHUD config -int InkHUD::Events::beforeReboot(void *unused) { +int InkHUD::Events::beforeReboot(void *unused) +{ - // Notify all applets that we're "shutting down" - // They don't need to know that it's really a reboot - for (Applet *a : inkhud->userApplets) { - a->onDeactivate(); - a->onShutdown(); - } - for (SystemApplet *sa : inkhud->systemApplets) { - // Note: no onDeactivate. System applets are always active. - sa->onReboot(); - } + // Notify all applets that we're "shutting down" + // They don't need to know that it's really a reboot + for (Applet *a : inkhud->userApplets) { + a->onDeactivate(); + a->onShutdown(); + } + for (SystemApplet *sa : inkhud->systemApplets) { + // Note: no onDeactivate. System applets are always active. + sa->onReboot(); + } - // Save settings to flash, or erase if factory reset in progress - if (!eraseOnReboot) { - inkhud->persistence->saveSettings(); - inkhud->persistence->saveLatestMessage(); - } else { - NicheGraphics::clearFlashData(); - } + // Save settings to flash, or erase if factory reset in progress + if (!eraseOnReboot) { + inkhud->persistence->saveSettings(); + inkhud->persistence->saveLatestMessage(); + } else { + NicheGraphics::clearFlashData(); + } - // Note: no forceUpdate call here - // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen + // Note: no forceUpdate call here + // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen - return 0; // No special status to report. Ignored anyway by this Observable + return 0; // No special status to report. Ignored anyway by this Observable } // Callback when a new text message is received // Caches the most recently received message, for use by applets // Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc. // Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message -int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) { - // Short circuit: don't store outgoing messages - if (getFrom(packet) == nodeDB->getNodeNum()) - return 0; +int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) +{ + // Short circuit: don't store outgoing messages + if (getFrom(packet) == nodeDB->getNodeNum()) + return 0; - // Determine whether the message is broadcast or a DM - // Store this info to prevent confusion after a reboot - // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not - // set - inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); + // Determine whether the message is broadcast or a DM + // Store this info to prevent confusion after a reboot + // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set + inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); - // Pick the appropriate variable to store the message in - MessageStore::Message *storedMessage = - inkhud->persistence->latestMessage.wasBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; + // Pick the appropriate variable to store the message in + MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast + ? &inkhud->persistence->latestMessage.broadcast + : &inkhud->persistence->latestMessage.dm; - // Store nodenum of the sender - // Applets can use this to fetch user data from nodedb, if they want - storedMessage->sender = packet->from; + // Store nodenum of the sender + // Applets can use this to fetch user data from nodedb, if they want + storedMessage->sender = packet->from; - // Store the time (epoch seconds) when message received - storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time + // Store the time (epoch seconds) when message received + storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - // Store the channel - // - (potentially) used to determine whether notification shows - // - (potentially) used to determine which applet to focus - storedMessage->channelIndex = packet->channel; + // Store the channel + // - (potentially) used to determine whether notification shows + // - (potentially) used to determine which applet to focus + storedMessage->channelIndex = packet->channel; - // Store the text - // Need to specify manually how many bytes, because source not null-terminated - storedMessage->text = std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); + // Store the text + // Need to specify manually how many bytes, because source not null-terminated + storedMessage->text = + std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); - return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } -int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { - switch (data->request->which_payload_variant) { - // Factory reset - // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. - case meshtastic_AdminMessage_factory_reset_device_tag: - case meshtastic_AdminMessage_factory_reset_config_tag: - eraseOnReboot = true; - *data->result = AdminMessageHandleResult::HANDLED; - break; +int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) +{ + switch (data->request->which_payload_variant) { + // Factory reset + // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. + case meshtastic_AdminMessage_factory_reset_device_tag: + case meshtastic_AdminMessage_factory_reset_config_tag: + eraseOnReboot = true; + *data->result = AdminMessageHandleResult::HANDLED; + break; - default: - break; - } + default: + break; + } - return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } #ifdef ARCH_ESP32 // Callback for lightSleepObserver // Make sure the display is not partway through an update when we begin light sleep -// This is because some displays require active input from us to terminate the update process, and protect the panel -// hardware -int InkHUD::Events::beforeLightSleep(void *unused) { - inkhud->awaitUpdate(); - return 0; // No special status to report. Ignored anyway by this Observable +// This is because some displays require active input from us to terminate the update process, and protect the panel hardware +int InkHUD::Events::beforeLightSleep(void *unused) +{ + inkhud->awaitUpdate(); + return 0; // No special status to report. Ignored anyway by this Observable } #endif // Silence all ongoing beeping, blinking, buzzing, coming from the external notification module // Returns true if an external notification was active, and we dismissed it // Button handling changes depending on our result -bool InkHUD::Events::dismissExternalNotification() { - // Abort if not using external notifications - if (!moduleConfig.external_notification.enabled) - return false; +bool InkHUD::Events::dismissExternalNotification() +{ + // Abort if not using external notifications + if (!moduleConfig.external_notification.enabled) + return false; - // Abort if nothing to dismiss - if (!externalNotificationModule->nagging()) - return false; + // Abort if nothing to dismiss + if (!externalNotificationModule->nagging()) + return false; - // Stop the beep buzz blink - externalNotificationModule->stopNow(); + // Stop the beep buzz blink + externalNotificationModule->stopNow(); - // Inform that we did indeed dismiss an external notification - return true; + // Inform that we did indeed dismiss an external notification + return true; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 4d4519581..664ca19f0 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -18,59 +18,61 @@ however this class handles general events which concern InkHUD as a whole, e.g. #include "./InkHUD.h" #include "./Persistence.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class Events { -public: - Events(); - void begin(); +class Events +{ + public: + Events(); + void begin(); - void onButtonShort(); // User button: short press - void onButtonLong(); // User button: long press - void onExitShort(); // Exit button: short press - void onExitLong(); // Exit button: long press - void onNavUp(); // Navigate up - void onNavDown(); // Navigate down - void onNavLeft(); // Navigate left - void onNavRight(); // Navigate right + void onButtonShort(); // User button: short press + void onButtonLong(); // User button: long press + void onExitShort(); // Exit button: short press + void onExitLong(); // Exit button: long press + void onNavUp(); // Navigate up + void onNavDown(); // Navigate down + void onNavLeft(); // Navigate left + void onNavRight(); // Navigate right - int beforeDeepSleep(void *unused); // Prepare for shutdown - int beforeReboot(void *unused); // Prepare for reboot - int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message - int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + int beforeDeepSleep(void *unused); // Prepare for shutdown + int beforeReboot(void *unused); // Prepare for reboot + int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); // Prepare for light sleep + int beforeLightSleep(void *unused); // Prepare for light sleep #endif -private: - // For convenience - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; + private: + // For convenience + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; - // Get notified when the system is shutting down - CallbackObserver deepSleepObserver = CallbackObserver(this, &Events::beforeDeepSleep); + // Get notified when the system is shutting down + CallbackObserver deepSleepObserver = CallbackObserver(this, &Events::beforeDeepSleep); - // Get notified when the system is rebooting - CallbackObserver rebootObserver = CallbackObserver(this, &Events::beforeReboot); + // Get notified when the system is rebooting + CallbackObserver rebootObserver = CallbackObserver(this, &Events::beforeReboot); - // Cache *incoming* text messages, for use by applets - CallbackObserver textMessageObserver = - CallbackObserver(this, &Events::onReceiveTextMessage); + // Cache *incoming* text messages, for use by applets + CallbackObserver textMessageObserver = + CallbackObserver(this, &Events::onReceiveTextMessage); - // Get notified of incoming admin messages, and handle any which are relevant to InkHUD - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Events::onAdminMessage); + // Get notified of incoming admin messages, and handle any which are relevant to InkHUD + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Events::onAdminMessage); #ifdef ARCH_ESP32 - // Get notified when the system is entering light sleep - CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); + // Get notified when the system is entering light sleep + CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); #endif - // End any externalNotification beeping, buzzing, blinking etc - bool dismissExternalNotification(); + // End any externalNotification beeping, buzzing, blinking etc + bool dismissExternalNotification(); - // If set, InkHUD's data will be erased during onReboot - bool eraseOnReboot = false; + // If set, InkHUD's data will be erased during onReboot + bool eraseOnReboot = false; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index b89de8d4e..9f05ae5bb 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -13,174 +13,222 @@ using namespace NicheGraphics; // Get or create the singleton -InkHUD::InkHUD *InkHUD::InkHUD::getInstance() { - // Create the singleton instance of our class, if not yet done - static InkHUD *instance = nullptr; - if (!instance) { - instance = new InkHUD; +InkHUD::InkHUD *InkHUD::InkHUD::getInstance() +{ + // Create the singleton instance of our class, if not yet done + static InkHUD *instance = nullptr; + if (!instance) { + instance = new InkHUD; - instance->persistence = new Persistence; - instance->windowManager = new WindowManager; - instance->renderer = new Renderer; - instance->events = new Events; - } + instance->persistence = new Persistence; + instance->windowManager = new WindowManager; + instance->renderer = new Renderer; + instance->events = new Events; + } - return instance; + return instance; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called -void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) { renderer->setDriver(driver); } +void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) +{ + renderer->setDriver(driver); +} // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health -void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { - renderer->setDisplayResilience(fastPerFull, stressMultiplier); +void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) +{ + renderer->setDisplayResilience(fastPerFull, stressMultiplier); } // Register a user applet with InkHUD // A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method // Passing an applet to this method is all that is required to make it available to the user in your InkHUD build -void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { - windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); +void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) +{ + windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); } // Start InkHUD! // Call this only after you have configured InkHUD -void InkHUD::InkHUD::begin() { - persistence->loadSettings(); - persistence->loadLatestMessage(); +void InkHUD::InkHUD::begin() +{ + persistence->loadSettings(); + persistence->loadLatestMessage(); - windowManager->begin(); - events->begin(); - renderer->begin(); - // LogoApplet shows boot screen here + windowManager->begin(); + events->begin(); + renderer->begin(); + // LogoApplet shows boot screen here } // Call this when your user button gets a short press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) -void InkHUD::InkHUD::shortpress() { events->onButtonShort(); } +void InkHUD::InkHUD::shortpress() +{ + events->onButtonShort(); +} // Call this when your user button gets a long press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) -void InkHUD::InkHUD::longpress() { events->onButtonLong(); } +void InkHUD::InkHUD::longpress() +{ + events->onButtonLong(); +} // Call this when your exit button gets a short press -void InkHUD::InkHUD::exitShort() { events->onExitShort(); } +void InkHUD::InkHUD::exitShort() +{ + events->onExitShort(); +} // Call this when your exit button gets a long press -void InkHUD::InkHUD::exitLong() { events->onExitLong(); } +void InkHUD::InkHUD::exitLong() +{ + events->onExitLong(); +} // Call this when your joystick gets an up input -void InkHUD::InkHUD::navUp() { - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavLeft(); - break; - case 2: // 180 deg - events->onNavDown(); - break; - case 3: // 270 deg - events->onNavRight(); - break; - default: // 0 deg - events->onNavUp(); - break; - } +void InkHUD::InkHUD::navUp() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavLeft(); + break; + case 2: // 180 deg + events->onNavDown(); + break; + case 3: // 270 deg + events->onNavRight(); + break; + default: // 0 deg + events->onNavUp(); + break; + } } // Call this when your joystick gets a down input -void InkHUD::InkHUD::navDown() { - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavRight(); - break; - case 2: // 180 deg - events->onNavUp(); - break; - case 3: // 270 deg - events->onNavLeft(); - break; - default: // 0 deg - events->onNavDown(); - break; - } +void InkHUD::InkHUD::navDown() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavRight(); + break; + case 2: // 180 deg + events->onNavUp(); + break; + case 3: // 270 deg + events->onNavLeft(); + break; + default: // 0 deg + events->onNavDown(); + break; + } } // Call this when your joystick gets a left input -void InkHUD::InkHUD::navLeft() { - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavDown(); - break; - case 2: // 180 deg - events->onNavRight(); - break; - case 3: // 270 deg - events->onNavUp(); - break; - default: // 0 deg - events->onNavLeft(); - break; - } +void InkHUD::InkHUD::navLeft() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavDown(); + break; + case 2: // 180 deg + events->onNavRight(); + break; + case 3: // 270 deg + events->onNavUp(); + break; + default: // 0 deg + events->onNavLeft(); + break; + } } // Call this when your joystick gets a right input -void InkHUD::InkHUD::navRight() { - switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { - case 1: // 90 deg - events->onNavUp(); - break; - case 2: // 180 deg - events->onNavLeft(); - break; - case 3: // 270 deg - events->onNavDown(); - break; - default: // 0 deg - events->onNavRight(); - break; - } +void InkHUD::InkHUD::navRight() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavUp(); + break; + case 2: // 180 deg + events->onNavLeft(); + break; + case 3: // 270 deg + events->onNavDown(); + break; + default: // 0 deg + events->onNavRight(); + break; + } } // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" -void InkHUD::InkHUD::nextApplet() { windowManager->nextApplet(); } +void InkHUD::InkHUD::nextApplet() +{ + windowManager->nextApplet(); +} // Cycle the previous user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" -void InkHUD::InkHUD::prevApplet() { windowManager->prevApplet(); } +void InkHUD::InkHUD::prevApplet() +{ + windowManager->prevApplet(); +} // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes -void InkHUD::InkHUD::openMenu() { windowManager->openMenu(); } +void InkHUD::InkHUD::openMenu() +{ + windowManager->openMenu(); +} // Bring AlignStick applet to the foreground -void InkHUD::InkHUD::openAlignStick() { windowManager->openAlignStick(); } +void InkHUD::InkHUD::openAlignStick() +{ + windowManager->openAlignStick(); +} // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press -void InkHUD::InkHUD::nextTile() { windowManager->nextTile(); } +void InkHUD::InkHUD::nextTile() +{ + windowManager->nextTile(); +} // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press -void InkHUD::InkHUD::prevTile() { windowManager->prevTile(); } +void InkHUD::InkHUD::prevTile() +{ + windowManager->prevTile(); +} // Rotate the display image by 90 degrees -void InkHUD::InkHUD::rotate() { windowManager->rotate(); } +void InkHUD::InkHUD::rotate() +{ + windowManager->rotate(); +} // rotate the joystick in 90 degree increments -void InkHUD::InkHUD::rotateJoystick(uint8_t angle) { - persistence->settings.joystick.alignment += angle; - persistence->settings.joystick.alignment %= 4; +void InkHUD::InkHUD::rotateJoystick(uint8_t angle) +{ + persistence->settings.joystick.alignment += angle; + persistence->settings.joystick.alignment %= 4; } // Show / hide the battery indicator in top-right -void InkHUD::InkHUD::toggleBatteryIcon() { windowManager->toggleBatteryIcon(); } +void InkHUD::InkHUD::toggleBatteryIcon() +{ + windowManager->toggleBatteryIcon(); +} // An applet asking for the display to be updated // This does not occur immediately @@ -188,40 +236,64 @@ void InkHUD::InkHUD::toggleBatteryIcon() { windowManager->toggleBatteryIcon(); } // This allows multiple applets to observe the same event, and then share the same opportunity to update // Applets should requestUpdate, whether or not they are currently displayed ("foreground") // This is because they *might* be automatically brought to foreground by WindowManager::autoshow -void InkHUD::InkHUD::requestUpdate() { renderer->requestUpdate(); } +void InkHUD::InkHUD::requestUpdate() +{ + renderer->requestUpdate(); +} // Demand that the display be updated // Ignores all diplomacy: // - the display *will* update // - the specified update type *will* be used // If the async parameter is false, code flow is blocked while the update takes place -void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) { renderer->forceUpdate(type, async); } +void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) +{ + renderer->forceUpdate(type, async); +} // Wait for any in-progress display update to complete before continuing -void InkHUD::InkHUD::awaitUpdate() { renderer->awaitUpdate(); } +void InkHUD::InkHUD::awaitUpdate() +{ + renderer->awaitUpdate(); +} // Ask the window manager to potentially bring a different user applet to foreground // An applet will be brought to foreground if it has just received new and relevant info // For Example: AllMessagesApplet has just received a new text message // Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis // If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event -void InkHUD::InkHUD::autoshow() { windowManager->autoshow(); } +void InkHUD::InkHUD::autoshow() +{ + windowManager->autoshow(); +} // Tell the window manager that the Persistence::Settings value for applet activation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or when the user enables / disabled applets via the on-screen menu -void InkHUD::InkHUD::updateAppletSelection() { windowManager->changeActivatedApplets(); } +void InkHUD::InkHUD::updateAppletSelection() +{ + windowManager->changeActivatedApplets(); +} // Tell the window manager that the Persistence::Settings value for layout or rotation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or by rotate / layout options in the on-screen menu -void InkHUD::InkHUD::updateLayout() { windowManager->changeLayout(); } +void InkHUD::InkHUD::updateLayout() +{ + windowManager->changeLayout(); +} // Width of the display, in the context of the current rotation -uint16_t InkHUD::InkHUD::width() { return renderer->width(); } +uint16_t InkHUD::InkHUD::width() +{ + return renderer->width(); +} // Height of the display, in the context of the current rotation -uint16_t InkHUD::InkHUD::height() { return renderer->height(); } +uint16_t InkHUD::InkHUD::height() +{ + return renderer->height(); +} // A collection of any user tiles which do not have a valid user applet // This can occur in various situations, such as when a user enables fewer applets than their layout has tiles @@ -229,19 +301,23 @@ uint16_t InkHUD::InkHUD::height() { return renderer->height(); } // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- -std::vector InkHUD::InkHUD::getEmptyTiles() { return windowManager->getEmptyTiles(); } +std::vector InkHUD::InkHUD::getEmptyTiles() +{ + return windowManager->getEmptyTiles(); +} // Get a system applet by its name // This isn't particularly elegant, but it does avoid: // - passing around a big set of references // - having two sets of references (systemApplet vector for iteration) -InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) { - for (SystemApplet *sa : systemApplets) { - if (strcmp(name, sa->name) == 0) - return sa; - } +InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) +{ + for (SystemApplet *sa : systemApplets) { + if (strcmp(name, sa->name) == 0) + return sa; + } - assert(false); // Invalid name + assert(false); // Invalid name } // Place a pixel into the image buffer @@ -250,6 +326,9 @@ InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) { // - Tiles pass translated pixels to this method // - this methods (Renderer) places rotated pixels into the image buffer // This method provides the final formatting step required. The image buffer is suitable for writing to display -void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) { renderer->handlePixel(x, y, c); } +void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) +{ + renderer->handlePixel(x, y, c); +} #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 6f6f191c1..7325d8262 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -18,13 +18,14 @@ #include -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ // Color, understood by display controller IC (as bit values) // Also suitable for use as AdafruitGFX colors enum Color : uint8_t { - BLACK = 0, - WHITE = 1, + BLACK = 0, + WHITE = 1, }; class Applet; @@ -35,82 +36,83 @@ class SystemApplet; class Tile; class WindowManager; -class InkHUD { -public: - static InkHUD *getInstance(); // Access to this singleton class +class InkHUD +{ + public: + static InkHUD *getInstance(); // Access to this singleton class - // Configuration - // - before InkHUD::begin, in variant nicheGraphics.h, + // Configuration + // - before InkHUD::begin, in variant nicheGraphics.h, - void setDriver(Drivers::EInk *driver); - void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); - void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); + void setDriver(Drivers::EInk *driver); + void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); + void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); - void begin(); + void begin(); - // Handle user-button press - // - connected to an input source, in variant nicheGraphics.h + // Handle user-button press + // - connected to an input source, in variant nicheGraphics.h - void shortpress(); - void longpress(); - void exitShort(); - void exitLong(); - void navUp(); - void navDown(); - void navLeft(); - void navRight(); + void shortpress(); + void longpress(); + void exitShort(); + void exitLong(); + void navUp(); + void navDown(); + void navLeft(); + void navRight(); - // Trigger UI changes - // - called by various InkHUD components - // - suitable(?) for use by aux button, connected in variant nicheGraphics.h + // Trigger UI changes + // - called by various InkHUD components + // - suitable(?) for use by aux button, connected in variant nicheGraphics.h - void nextApplet(); - void prevApplet(); - void openMenu(); - void openAlignStick(); - void nextTile(); - void prevTile(); - void rotate(); - void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default - void toggleBatteryIcon(); + void nextApplet(); + void prevApplet(); + void openMenu(); + void openAlignStick(); + void nextTile(); + void prevTile(); + void rotate(); + void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default + void toggleBatteryIcon(); - // Updating the display - // - called by various InkHUD components + // Updating the display + // - called by various InkHUD components - void requestUpdate(); - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true); - void awaitUpdate(); + void requestUpdate(); + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true); + void awaitUpdate(); - // (Re)configuring WindowManager + // (Re)configuring WindowManager - void autoshow(); // Bring an applet to foreground - void updateAppletSelection(); // Change which applets are active - void updateLayout(); // Change multiplexing (count, rotation) + void autoshow(); // Bring an applet to foreground + void updateAppletSelection(); // Change which applets are active + void updateLayout(); // Change multiplexing (count, rotation) - // Information passed between components + // Information passed between components - uint16_t width(); // From E-Ink driver - uint16_t height(); // From E-Ink driver - std::vector getEmptyTiles(); // From WindowManager + uint16_t width(); // From E-Ink driver + uint16_t height(); // From E-Ink driver + std::vector getEmptyTiles(); // From WindowManager - // Applets + // Applets - SystemApplet *getSystemApplet(const char *name); - std::vector userApplets; - std::vector systemApplets; + SystemApplet *getSystemApplet(const char *name); + std::vector userApplets; + std::vector systemApplets; - // Pass drawing output to Renderer - void drawPixel(int16_t x, int16_t y, Color c); + // Pass drawing output to Renderer + void drawPixel(int16_t x, int16_t y, Color c); - // Shared data which persists between boots - Persistence *persistence = nullptr; + // Shared data which persists between boots + Persistence *persistence = nullptr; -private: - InkHUD() {} // Constructor made private to force use of InkHUD::getInstance + private: + InkHUD() {} // Constructor made private to force use of InkHUD::getInstance - Events *events = nullptr; // Handle non-specific firmware events - Renderer *renderer = nullptr; // Co-ordinate display updates - WindowManager *windowManager = nullptr; // Multiplexing of applets + Events *events = nullptr; // Handle non-specific firmware events + Renderer *renderer = nullptr; // Co-ordinate display updates + WindowManager *windowManager = nullptr; // Multiplexing of applets }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp index 0833bd0a6..94e0aa661 100644 --- a/src/graphics/niche/InkHUD/MessageStore.cpp +++ b/src/graphics/niche/InkHUD/MessageStore.cpp @@ -12,140 +12,142 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::MessageStore::MessageStore(std::string label) { - filename = ""; - filename += "/NicheGraphics"; - filename += "/"; - filename += label; - filename += ".msgs"; +InkHUD::MessageStore::MessageStore(std::string label) +{ + filename = ""; + filename += "/NicheGraphics"; + filename += "/"; + filename += label; + filename += ".msgs"; } // Write the contents of the MessageStore::messages object to flash -// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD -// card. Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally -void InkHUD::MessageStore::saveToFlash() { - assert(!filename.empty()); +// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. +// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally +void InkHUD::MessageStore::saveToFlash() +{ + assert(!filename.empty()); #ifdef FSCom - // Make the directory, if doesn't already exist - // This is the same directory accessed by NicheGraphics::FlashData - spiLock->lock(); - FSCom.mkdir("/NicheGraphics"); - spiLock->unlock(); + // Make the directory, if doesn't already exist + // This is the same directory accessed by NicheGraphics::FlashData + spiLock->lock(); + FSCom.mkdir("/NicheGraphics"); + spiLock->unlock(); - // Open or create the file - // No "full atomic": don't save then rename - auto f = SafeFile(filename.c_str(), false); + // Open or create the file + // No "full atomic": don't save then rename + auto f = SafeFile(filename.c_str(), false); - LOG_INFO("Saving messages in %s", filename.c_str()); + LOG_INFO("Saving messages in %s", filename.c_str()); - // Take firmware's SPI Lock while writing - spiLock->lock(); + // Take firmware's SPI Lock while writing + spiLock->lock(); - // 1st byte: how many messages will be written to store - f.write(messages.size()); + // 1st byte: how many messages will be written to store + f.write(messages.size()); - // For each message - for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { - Message &m = messages.at(i); - f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes - f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes - f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte - f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length - f.write('\0'); // Append null term - LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); - } + // For each message + for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { + Message &m = messages.at(i); + f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes + f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes + f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte + f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length + f.write('\0'); // Append null term + LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); + } - // Release firmware's SPI lock, because SafeFile::close needs it - spiLock->unlock(); + // Release firmware's SPI lock, because SafeFile::close needs it + spiLock->unlock(); - bool writeSucceeded = f.close(); + bool writeSucceeded = f.close(); - if (!writeSucceeded) { - LOG_ERROR("Can't write data!"); - } + if (!writeSucceeded) { + LOG_ERROR("Can't write data!"); + } #else - LOG_ERROR("ERROR: Filesystem not implemented\n"); + LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } // Attempt to load the previous contents of the MessageStore:message deque from flash. // Filename is controlled by the "label" parameter -// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD -// card. -void InkHUD::MessageStore::loadFromFlash() { - // Hopefully redundant. Initial intention is to only load / save once per boot. - messages.clear(); +// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. +void InkHUD::MessageStore::loadFromFlash() +{ + // Hopefully redundant. Initial intention is to only load / save once per boot. + messages.clear(); #ifdef FSCom - // Take the firmware's SPI Lock, in case filesystem is on SD card - concurrency::LockGuard guard(spiLock); + // Take the firmware's SPI Lock, in case filesystem is on SD card + concurrency::LockGuard guard(spiLock); - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_WARN("'%s' not found. Using default values", filename.c_str()); - return; - } - - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_INFO("'%s' not found.", filename.c_str()); - return; - } - - // Open the file - auto f = FSCom.open(filename.c_str(), FILE_O_READ); - - if (f.size() == 0) { - LOG_INFO("%s is empty", filename.c_str()); - f.close(); - return; - } - - // If opened, start reading - if (f) { - LOG_INFO("Loading threaded messages '%s'", filename.c_str()); - - // First byte: how many messages are in the flash store - uint8_t flashMessageCount = 0; - f.readBytes((char *)&flashMessageCount, 1); - LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); - - // For each message - for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { - Message m; - - // Read meta data (fixed width) - f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); - f.readBytes((char *)&m.sender, sizeof(m.sender)); - f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); - - // Read characters until we find a null term - char c; - while (m.text.size() < MAX_MESSAGE_SIZE) { - f.readBytes(&c, 1); - if (c != '\0') - m.text += c; - else - break; - } - - // Store in RAM - messages.push_back(m); - - LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_WARN("'%s' not found. Using default values", filename.c_str()); + return; } - f.close(); - } else { - LOG_ERROR("Could not open / read %s", filename.c_str()); - } + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_INFO("'%s' not found.", filename.c_str()); + return; + } + + // Open the file + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + + if (f.size() == 0) { + LOG_INFO("%s is empty", filename.c_str()); + f.close(); + return; + } + + // If opened, start reading + if (f) { + LOG_INFO("Loading threaded messages '%s'", filename.c_str()); + + // First byte: how many messages are in the flash store + uint8_t flashMessageCount = 0; + f.readBytes((char *)&flashMessageCount, 1); + LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); + + // For each message + for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { + Message m; + + // Read meta data (fixed width) + f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); + f.readBytes((char *)&m.sender, sizeof(m.sender)); + f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); + + // Read characters until we find a null term + char c; + while (m.text.size() < MAX_MESSAGE_SIZE) { + f.readBytes(&c, 1); + if (c != '\0') + m.text += c; + else + break; + } + + // Store in RAM + messages.push_back(m); + + LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); + } + + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename.c_str()); + } #else - LOG_ERROR("Filesystem not implemented"); - state = LoadFileState::NO_FILESYSTEM; + LOG_ERROR("Filesystem not implemented"); + state = LoadFileState::NO_FILESYSTEM; #endif - return; + return; } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h index 2b7eab70f..745c3b2eb 100644 --- a/src/graphics/niche/InkHUD/MessageStore.h +++ b/src/graphics/niche/InkHUD/MessageStore.h @@ -16,28 +16,30 @@ and methods for serializing them to flash. #include "mesh/MeshTypes.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class MessageStore { -public: - // A stored message - struct Message { - uint32_t timestamp; // Epoch seconds - NodeNum sender = 0; - uint8_t channelIndex; - std::string text; - }; +class MessageStore +{ + public: + // A stored message + struct Message { + uint32_t timestamp; // Epoch seconds + NodeNum sender = 0; + uint8_t channelIndex; + std::string text; + }; - MessageStore() = delete; - explicit MessageStore(std::string label); // Label determines filename in flash + MessageStore() = delete; + explicit MessageStore(std::string label); // Label determines filename in flash - void saveToFlash(); - void loadFromFlash(); + void saveToFlash(); + void loadFromFlash(); - std::deque messages; // Interact with this object! + std::deque messages; // Interact with this object! -private: - std::string filename; + private: + std::string filename; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Persistence.cpp b/src/graphics/niche/InkHUD/Persistence.cpp index a68594888..20909f2dc 100644 --- a/src/graphics/niche/InkHUD/Persistence.cpp +++ b/src/graphics/niche/InkHUD/Persistence.cpp @@ -5,47 +5,52 @@ using namespace NicheGraphics; // Load settings and latestMessage data -void InkHUD::Persistence::loadSettings() { - // Load the InkHUD settings from flash, and check version number - // We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load - // flash data - Settings loadedSettings; - bool loadSucceeded = FlashData::load(&loadedSettings, "settings"); - if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0) - settings = loadedSettings; // Version matched, replace the defaults with the loaded values - else - LOG_WARN("Settings version changed. Using defaults"); +void InkHUD::Persistence::loadSettings() +{ + // Load the InkHUD settings from flash, and check version number + // We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data + Settings loadedSettings; + bool loadSucceeded = FlashData::load(&loadedSettings, "settings"); + if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0) + settings = loadedSettings; // Version matched, replace the defaults with the loaded values + else + LOG_WARN("Settings version changed. Using defaults"); } // Load settings and latestMessage data -void InkHUD::Persistence::loadLatestMessage() { - // Load previous "latestMessages" data from flash - MessageStore store("latest"); - store.loadFromFlash(); +void InkHUD::Persistence::loadLatestMessage() +{ + // Load previous "latestMessages" data from flash + MessageStore store("latest"); + store.loadFromFlash(); - // Place into latestMessage struct, for convenient access - // Number of strings loaded determines whether last message was broadcast or dm - if (store.messages.size() == 1) { - latestMessage.dm = store.messages.at(0); - latestMessage.wasBroadcast = false; - } else if (store.messages.size() == 2) { - latestMessage.dm = store.messages.at(0); - latestMessage.broadcast = store.messages.at(1); - latestMessage.wasBroadcast = true; - } + // Place into latestMessage struct, for convenient access + // Number of strings loaded determines whether last message was broadcast or dm + if (store.messages.size() == 1) { + latestMessage.dm = store.messages.at(0); + latestMessage.wasBroadcast = false; + } else if (store.messages.size() == 2) { + latestMessage.dm = store.messages.at(0); + latestMessage.broadcast = store.messages.at(1); + latestMessage.wasBroadcast = true; + } } // Save the InkHUD settings to flash -void InkHUD::Persistence::saveSettings() { FlashData::save(&settings, "settings"); } +void InkHUD::Persistence::saveSettings() +{ + FlashData::save(&settings, "settings"); +} // Save latestMessage data to flash -void InkHUD::Persistence::saveLatestMessage() { - // Number of strings saved determines whether last message was broadcast or dm - MessageStore store("latest"); - store.messages.push_back(latestMessage.dm); - if (latestMessage.wasBroadcast) - store.messages.push_back(latestMessage.broadcast); - store.saveToFlash(); +void InkHUD::Persistence::saveLatestMessage() +{ + // Number of strings saved determines whether last message was broadcast or dm + MessageStore store("latest"); + store.messages.push_back(latestMessage.dm); + if (latestMessage.wasBroadcast) + store.messages.push_back(latestMessage.broadcast); + store.saveToFlash(); } /* diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index b69e15140..5054b7234 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -18,124 +18,126 @@ The save / load mechanism is a shared NicheGraphics feature. #include "graphics/niche/InkHUD/MessageStore.h" #include "graphics/niche/Utils/FlashData.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class Persistence { -public: - static constexpr uint8_t MAX_TILES_GLOBAL = 4; - static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16; +class Persistence +{ + public: + static constexpr uint8_t MAX_TILES_GLOBAL = 4; + static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16; - // Used to invalidate old settings, if needed - // Version 0 is reserved for testing, and will always load defaults - static constexpr uint32_t SETTINGS_VERSION = 3; + // Used to invalidate old settings, if needed + // Version 0 is reserved for testing, and will always load defaults + static constexpr uint32_t SETTINGS_VERSION = 3; - struct Settings { - struct Meta { - // Used to invalidate old savefiles, if we make breaking changes - uint32_t version = SETTINGS_VERSION; - } meta; + struct Settings { + struct Meta { + // Used to invalidate old savefiles, if we make breaking changes + uint32_t version = SETTINGS_VERSION; + } meta; - struct UserTiles { - // How many tiles are shown - uint8_t count = 1; + struct UserTiles { + // How many tiles are shown + uint8_t count = 1; - // Maximum amount of tiles for this display - uint8_t maxCount = 4; + // Maximum amount of tiles for this display + uint8_t maxCount = 4; - // Which tile is focused (responding to user button input) - uint8_t focused = 0; + // Which tile is focused (responding to user button input) + uint8_t focused = 0; - // Which applet is displayed on which tile - // Index of array: which tile, as indexed in WindowManager::userTiles - // Value of array: which applet, as indexed in InkHUD::userApplets - uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3}; - } userTiles; + // Which applet is displayed on which tile + // Index of array: which tile, as indexed in WindowManager::userTiles + // Value of array: which applet, as indexed in InkHUD::userApplets + uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3}; + } userTiles; - struct UserApplets { - // Which applets are running (either displayed, or in the background) - // Index of array: which applet, as indexed in InkHUD::userApplets - // Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method - bool active[MAX_USERAPPLETS_GLOBAL]{false}; + struct UserApplets { + // Which applets are running (either displayed, or in the background) + // Index of array: which applet, as indexed in InkHUD::userApplets + // Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method + bool active[MAX_USERAPPLETS_GLOBAL]{false}; - // Which user applets should be automatically shown when they have important data to show - // If none set, foreground applets should remain foreground without manual user input - // If multiple applets request this at once, - // priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method - bool autoshow[MAX_USERAPPLETS_GLOBAL]{false}; - } userApplets; + // Which user applets should be automatically shown when they have important data to show + // If none set, foreground applets should remain foreground without manual user input + // If multiple applets request this at once, + // priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method + bool autoshow[MAX_USERAPPLETS_GLOBAL]{false}; + } userApplets; - // Features which the user can enable / disable via the on-screen menu - struct OptionalFeatures { - bool notifications = true; - bool batteryIcon = false; - } optionalFeatures; + // Features which the user can enable / disable via the on-screen menu + struct OptionalFeatures { + bool notifications = true; + bool batteryIcon = false; + } optionalFeatures; - // Some menu items may not be required, based on device / configuration - // We can enable them only when needed, to de-clutter the menu - struct OptionalMenuItems { - // If aux button is used to swap between tiles, we have no need for this menu item - bool nextTile = true; + // Some menu items may not be required, based on device / configuration + // We can enable them only when needed, to de-clutter the menu + struct OptionalMenuItems { + // If aux button is used to swap between tiles, we have no need for this menu item + bool nextTile = true; - // Used if backlight present, and not controlled by AUX button - // If this item is added to menu: backlight is always active when menu is open - // The added menu items then allows the user to "Keep Backlight On", globally. - bool backlight = false; - } optionalMenuItems; + // Used if backlight present, and not controlled by AUX button + // If this item is added to menu: backlight is always active when menu is open + // The added menu items then allows the user to "Keep Backlight On", globally. + bool backlight = false; + } optionalMenuItems; - // Allows tips to be run once only - struct Tips { - // Enables the longer "tutorial" shown only on first boot - // Once tutorial has been completed, it is no longer shown - bool firstBoot = true; + // Allows tips to be run once only + struct Tips { + // Enables the longer "tutorial" shown only on first boot + // Once tutorial has been completed, it is no longer shown + bool firstBoot = true; - // User is advised to shut down before removing device power - // Once user executes a shutdown (either via menu or client app), - // this tip is no longer shown - bool safeShutdownSeen = false; - } tips; + // User is advised to shut down before removing device power + // Once user executes a shutdown (either via menu or client app), + // this tip is no longer shown + bool safeShutdownSeen = false; + } tips; - // Joystick settings for enabling and aligning to the screen - struct Joystick { - // Modifies the UI for joystick use - bool enabled = false; + // Joystick settings for enabling and aligning to the screen + struct Joystick { + // Modifies the UI for joystick use + bool enabled = false; - // gets set to true when AlignStick applet is completed - bool aligned = false; + // gets set to true when AlignStick applet is completed + bool aligned = false; - // Rotation of the joystick - // Multiples of 90 degrees clockwise - uint8_t alignment = 0; - } joystick; + // Rotation of the joystick + // Multiples of 90 degrees clockwise + uint8_t alignment = 0; + } joystick; - // Rotation of the display - // Multiples of 90 degrees clockwise - // Most commonly: rotation is 0 when flex connector is oriented below display - uint8_t rotation = 0; + // Rotation of the display + // Multiples of 90 degrees clockwise + // Most commonly: rotation is 0 when flex connector is oriented below display + uint8_t rotation = 0; - // How long do we consider another node to be "active"? - // Used when applets want to filter for "active nodes" only - uint32_t recentlyActiveSeconds = 2 * 60; - }; + // How long do we consider another node to be "active"? + // Used when applets want to filter for "active nodes" only + uint32_t recentlyActiveSeconds = 2 * 60; + }; - // Most recently received text message - // Value is updated by InkHUD::WindowManager, as a courtesy to applets - // Note: different from devicestate.rx_text_message, - // which may contain an *outgoing message* to broadcast - struct LatestMessage { - MessageStore::Message broadcast; // Most recent message received broadcast - MessageStore::Message dm; // Most recent received DM - bool wasBroadcast; // True if most recent broadcast is newer than most recent dm - }; + // Most recently received text message + // Value is updated by InkHUD::WindowManager, as a courtesy to applets + // Note: different from devicestate.rx_text_message, + // which may contain an *outgoing message* to broadcast + struct LatestMessage { + MessageStore::Message broadcast; // Most recent message received broadcast + MessageStore::Message dm; // Most recent received DM + bool wasBroadcast; // True if most recent broadcast is newer than most recent dm + }; - void loadSettings(); - void saveSettings(); - void loadLatestMessage(); - void saveLatestMessage(); + void loadSettings(); + void saveSettings(); + void loadLatestMessage(); + void saveLatestMessage(); - // void printSettings(Settings *settings); // Debugging use only + // void printSettings(Settings *settings); // Debugging use only - Settings settings; - LatestMessage latestMessage; + Settings settings; + LatestMessage latestMessage; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index 8db6cca92..072e9dbd6 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -10,61 +10,67 @@ using namespace NicheGraphics; -InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer") { - // Nothing for the timer to do just yet - OSThread::disable(); +InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer") +{ + // Nothing for the timer to do just yet + OSThread::disable(); - // Convenient references - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; + // Convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called -void InkHUD::Renderer::setDriver(Drivers::EInk *driver) { - // Make sure not already set - if (this->driver) { - LOG_ERROR("Driver already set"); - delay(2000); // Wait for native serial.. - assert(false); - } +void InkHUD::Renderer::setDriver(Drivers::EInk *driver) +{ + // Make sure not already set + if (this->driver) { + LOG_ERROR("Driver already set"); + delay(2000); // Wait for native serial.. + assert(false); + } - // Store the driver which was created in setupNicheGraphics() - this->driver = driver; + // Store the driver which was created in setupNicheGraphics() + this->driver = driver; - // Determine the dimensions of the image buffer, in bytes. - // Along rows, pixels are stored 8 per byte. - // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. - imageBufferWidth = ((driver->width - 1) / 8) + 1; - imageBufferHeight = driver->height; + // Determine the dimensions of the image buffer, in bytes. + // Along rows, pixels are stored 8 per byte. + // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. + imageBufferWidth = ((driver->width - 1) / 8) + 1; + imageBufferHeight = driver->height; - // Allocate the image buffer - imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight]; + // Allocate the image buffer + imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight]; } // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health -void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { - displayHealth.fastPerFull = fastPerFull; - displayHealth.stressMultiplier = stressMultiplier; +void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) +{ + displayHealth.fastPerFull = fastPerFull; + displayHealth.stressMultiplier = stressMultiplier; } -void InkHUD::Renderer::begin() { forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); } +void InkHUD::Renderer::begin() +{ + forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); +} // Set a flag, which will be picked up by runOnce, ASAP. // Quite likely, multiple applets will all want to respond to one event (Observable, etc) -// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next -// runOnce -void InkHUD::Renderer::requestUpdate() { - requested = true; +// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce +void InkHUD::Renderer::requestUpdate() +{ + requested = true; - // We will run the thread as soon as we loop(), - // after all Applets have had a chance to observe whatever event set this off - OSThread::setIntervalFromNow(0); - OSThread::enabled = true; - runASAP = true; + // We will run the thread as soon as we loop(), + // after all Applets have had a chance to observe whatever event set this off + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; } // requestUpdate will not actually update if no requests were made by applets which are actually visible @@ -73,166 +79,174 @@ void InkHUD::Renderer::requestUpdate() { // Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event // Display health, for example. // In these situations, we use forceUpdate -void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) { - requested = true; - forced = true; - displayHealth.forceUpdateType(type); +void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) +{ + requested = true; + forced = true; + displayHealth.forceUpdateType(type); - // Normally, we need to start the timer, in case the display is busy and we briefly defer the update - if (async) { - // We will run the thread as soon as we loop(), - // after all Applets have had a chance to observe whatever event set this off - OSThread::setIntervalFromNow(0); - OSThread::enabled = true; - runASAP = true; - } + // Normally, we need to start the timer, in case the display is busy and we briefly defer the update + if (async) { + // We will run the thread as soon as we loop(), + // after all Applets have had a chance to observe whatever event set this off + OSThread::setIntervalFromNow(0); + OSThread::enabled = true; + runASAP = true; + } - // If the update is *not* asynchronous, we begin the render process directly here - // so that it can block code flow while running - else - render(false); + // If the update is *not* asynchronous, we begin the render process directly here + // so that it can block code flow while running + else + render(false); } // Wait for any in-progress display update to complete before continuing -void InkHUD::Renderer::awaitUpdate() { - if (driver->busy()) { - LOG_INFO("Waiting for display"); - driver->await(); // Wait here for update to complete - } +void InkHUD::Renderer::awaitUpdate() +{ + if (driver->busy()) { + LOG_INFO("Waiting for display"); + driver->await(); // Wait here for update to complete + } } // Set a ready-to-draw pixel into the image buffer // All rotations / translations have already taken place: this buffer data is formatted ready for the driver -void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c) { - rotatePixelCoords(&x, &y); +void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c) +{ + rotatePixelCoords(&x, &y); - uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte - uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte. + uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte + uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte. - bitWrite(imageBuffer[byteNum], bitNum, c); + bitWrite(imageBuffer[byteNum], bitNum, c); } // Width of the display, relative to rotation -uint16_t InkHUD::Renderer::width() { - if (settings->rotation % 2) - return driver->height; - else - return driver->width; +uint16_t InkHUD::Renderer::width() +{ + if (settings->rotation % 2) + return driver->height; + else + return driver->width; } // Height of the display, relative to rotation -uint16_t InkHUD::Renderer::height() { - if (settings->rotation % 2) - return driver->width; - else - return driver->height; +uint16_t InkHUD::Renderer::height() +{ + if (settings->rotation % 2) + return driver->width; + else + return driver->height; } // Runs at regular intervals // - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render // - queuing another render: while one is already is progress -int32_t InkHUD::Renderer::runOnce() { - // If an applet asked to render, and hardware is able, lets try now - if (requested && !driver->busy()) { - render(); - } +int32_t InkHUD::Renderer::runOnce() +{ + // If an applet asked to render, and hardware is able, lets try now + if (requested && !driver->busy()) { + render(); + } - // If our render() call failed, try again shortly - // otherwise, stop our thread until next update due - if (requested) - return 250UL; - else - return OSThread::disable(); + // If our render() call failed, try again shortly + // otherwise, stop our thread until next update due + if (requested) + return 250UL; + else + return OSThread::disable(); } // Applies the system-wide rotation to pixel positions // This step is applied to image data which has already been translated by a Tile object // This is the final step before the pixel is placed into the image buffer // No return: values of the *x and *y parameters are modified by the method -void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y) { - // Apply a global rotation to pixel locations - int16_t x1 = 0; - int16_t y1 = 0; - switch (settings->rotation) { - case 0: - x1 = *x; - y1 = *y; - break; - case 1: - x1 = (driver->width - 1) - *y; - y1 = *x; - break; - case 2: - x1 = (driver->width - 1) - *x; - y1 = (driver->height - 1) - *y; - break; - case 3: - x1 = *y; - y1 = (driver->height - 1) - *x; - break; - } - *x = x1; - *y = y1; +void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y) +{ + // Apply a global rotation to pixel locations + int16_t x1 = 0; + int16_t y1 = 0; + switch (settings->rotation) { + case 0: + x1 = *x; + y1 = *y; + break; + case 1: + x1 = (driver->width - 1) - *y; + y1 = *x; + break; + case 2: + x1 = (driver->width - 1) - *x; + y1 = (driver->height - 1) - *y; + break; + case 3: + x1 = *y; + y1 = (driver->height - 1) - *x; + break; + } + *x = x1; + *y = y1; } // Make an attempt to gather image data from some / all applets, and update the display // Might not be possible right now, if update already is progress. -void InkHUD::Renderer::render(bool async) { - // Make sure the display is ready for a new update - if (async) { - // Previous update still running, Will try again shortly, via runOnce() - if (driver->busy()) - return; - } else { - // Wait here for previous update to complete - driver->await(); - } - - // Determine if a system applet has requested exclusive rights to request an update, - // or exclusive rights to render - checkLocks(); - - // (Potentially) change applet to display new info, - // then check if this newly displayed applet makes a pending notification redundant - inkhud->autoshow(); - - // If an update is justified. - // We don't know this until after autoshow has run, as new applets may now be in foreground - if (shouldUpdate()) { - - // Decide which technique the display will use to change image - // Done early, as rendering resets the Applets' requested types - Drivers::EInk::UpdateTypes updateType = decideUpdateType(); - - // Render the new image - clearBuffer(); - renderUserApplets(); - renderPlaceholders(); - renderSystemApplets(); - - // Invert Buffer if set by user - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) { - imageBuffer[i] = ~imageBuffer[i]; - } +void InkHUD::Renderer::render(bool async) +{ + // Make sure the display is ready for a new update + if (async) { + // Previous update still running, Will try again shortly, via runOnce() + if (driver->busy()) + return; + } else { + // Wait here for previous update to complete + driver->await(); } - // Tell display to begin process of drawing new image - LOG_INFO("Updating display"); - driver->update(imageBuffer, updateType); + // Determine if a system applet has requested exclusive rights to request an update, + // or exclusive rights to render + checkLocks(); - // If not async, wait here until the update is complete - if (!async) - driver->await(); - } + // (Potentially) change applet to display new info, + // then check if this newly displayed applet makes a pending notification redundant + inkhud->autoshow(); - // Our part is done now. - // If update is async, the display hardware is still performing the update process, - // but that's all handled by NicheGraphics::Drivers::EInk + // If an update is justified. + // We don't know this until after autoshow has run, as new applets may now be in foreground + if (shouldUpdate()) { - // Tidy up, ready for a new request - requested = false; - forced = false; + // Decide which technique the display will use to change image + // Done early, as rendering resets the Applets' requested types + Drivers::EInk::UpdateTypes updateType = decideUpdateType(); + + // Render the new image + clearBuffer(); + renderUserApplets(); + renderPlaceholders(); + renderSystemApplets(); + + // Invert Buffer if set by user + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) { + imageBuffer[i] = ~imageBuffer[i]; + } + } + + // Tell display to begin process of drawing new image + LOG_INFO("Updating display"); + driver->update(imageBuffer, updateType); + + // If not async, wait here until the update is complete + if (!async) + driver->await(); + } + + // Our part is done now. + // If update is async, the display hardware is still performing the update process, + // but that's all handled by NicheGraphics::Drivers::EInk + + // Tidy up, ready for a new request + requested = false; + forced = false; } // Manually fill the image buffer with WHITE @@ -240,157 +254,166 @@ void InkHUD::Renderer::render(bool async) { // Note: benchmarking revealed that this is *much* faster than setting pixels individually // So much so that it's more efficient to re-render all applets, // rather than rendering selectively, and manually blanking a portion of the display -void InkHUD::Renderer::clearBuffer() { memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } - -void InkHUD::Renderer::checkLocks() { - lockRendering = nullptr; - lockRequests = nullptr; - - for (SystemApplet *sa : inkhud->systemApplets) { - if (!lockRendering && sa->lockRendering && sa->isForeground()) { - lockRendering = sa; - } - if (!lockRequests && sa->lockRequests && sa->isForeground()) { - lockRequests = sa; - } - } +void InkHUD::Renderer::clearBuffer() +{ + memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } -bool InkHUD::Renderer::shouldUpdate() { - bool should = false; +void InkHUD::Renderer::checkLocks() +{ + lockRendering = nullptr; + lockRequests = nullptr; - // via forceUpdate - should |= forced; - - // via a system applet (which has locked update requests) - if (lockRequests) { - should |= lockRequests->wantsToRender(); - return should; // Early exit - no other requests considered - } - - // via system applet (not locked) - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->wantsToRender() // This applet requested - && sa->isForeground()) // This applet is currently shown - { - should = true; - break; + for (SystemApplet *sa : inkhud->systemApplets) { + if (!lockRendering && sa->lockRendering && sa->isForeground()) { + lockRendering = sa; + } + if (!lockRequests && sa->lockRequests && sa->isForeground()) { + lockRequests = sa; + } } - } +} - // via user applet - for (Applet *ua : inkhud->userApplets) { - if (ua // Tile has valid applet - && ua->wantsToRender() // This applet requested display update - && ua->isForeground()) // This applet is currently shown - { - should = true; - break; +bool InkHUD::Renderer::shouldUpdate() +{ + bool should = false; + + // via forceUpdate + should |= forced; + + // via a system applet (which has locked update requests) + if (lockRequests) { + should |= lockRequests->wantsToRender(); + return should; // Early exit - no other requests considered } - } - return should; + // via system applet (not locked) + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->wantsToRender() // This applet requested + && sa->isForeground()) // This applet is currently shown + { + should = true; + break; + } + } + + // via user applet + for (Applet *ua : inkhud->userApplets) { + if (ua // Tile has valid applet + && ua->wantsToRender() // This applet requested display update + && ua->isForeground()) // This applet is currently shown + { + should = true; + break; + } + } + + return should; } // Determine which type of E-Ink update the display will perform, to change the image. // Considers the needs of the various applets, then weighs against display health. // An update type specified by forceUpdate will be granted with no further questioning. -Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() { - // Ask applets which update type they would prefer - // Some update types take priority over others +Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() +{ + // Ask applets which update type they would prefer + // Some update types take priority over others - // No need to consider the "requests" if somebody already forced an update - if (!forced) { - // User applets - for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isForeground()) - displayHealth.requestUpdateType(ua->wantsUpdateType()); + // No need to consider the "requests" if somebody already forced an update + if (!forced) { + // User applets + for (Applet *ua : inkhud->userApplets) { + if (ua && ua->isForeground()) + displayHealth.requestUpdateType(ua->wantsUpdateType()); + } + // System Applets + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa && sa->isForeground()) + displayHealth.requestUpdateType(sa->wantsUpdateType()); + } } - // System Applets - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa && sa->isForeground()) - displayHealth.requestUpdateType(sa->wantsUpdateType()); - } - } - return displayHealth.decideUpdateType(); + return displayHealth.decideUpdateType(); } // Run the drawing operations of any user applets which are currently displayed // Pixel output is placed into the framebuffer, ready for handoff to the EInk driver -void InkHUD::Renderer::renderUserApplets() { - // Don't render user applets if a system applet has demanded the whole display to itself - if (lockRendering) - return; +void InkHUD::Renderer::renderUserApplets() +{ + // Don't render user applets if a system applet has demanded the whole display to itself + if (lockRendering) + return; - // Render any user applets which are currently visible - for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isActive() && ua->isForeground()) { - uint32_t start = millis(); - ua->render(); // Draw! - uint32_t stop = millis(); - LOG_DEBUG("%s took %dms to render", ua->name, stop - start); + // Render any user applets which are currently visible + for (Applet *ua : inkhud->userApplets) { + if (ua && ua->isActive() && ua->isForeground()) { + uint32_t start = millis(); + ua->render(); // Draw! + uint32_t stop = millis(); + LOG_DEBUG("%s took %dms to render", ua->name, stop - start); + } } - } } // Run the drawing operations of any system applets which are currently displayed // Pixel output is placed into the framebuffer, ready for handoff to the EInk driver -void InkHUD::Renderer::renderSystemApplets() { - SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon"); - SystemApplet *menu = inkhud->getSystemApplet("Menu"); - SystemApplet *notifications = inkhud->getSystemApplet("Notification"); +void InkHUD::Renderer::renderSystemApplets() +{ + SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon"); + SystemApplet *menu = inkhud->getSystemApplet("Menu"); + SystemApplet *notifications = inkhud->getSystemApplet("Notification"); - // Each system applet - for (SystemApplet *sa : inkhud->systemApplets) { + // Each system applet + for (SystemApplet *sa : inkhud->systemApplets) { - // Skip if not shown - if (!sa->isForeground()) - continue; + // Skip if not shown + if (!sa->isForeground()) + continue; - // Skip if locked by another applet - if (lockRendering && lockRendering != sa) - continue; + // Skip if locked by another applet + if (lockRendering && lockRendering != sa) + continue; - // Don't draw the battery or notifications overtop the menu - // Todo: smarter way to handle this - if (menu->isForeground() && (sa == battery || sa == notifications)) - continue; + // Don't draw the battery or notifications overtop the menu + // Todo: smarter way to handle this + if (menu->isForeground() && (sa == battery || sa == notifications)) + continue; - assert(sa->getTile()); + assert(sa->getTile()); - // uint32_t start = millis(); - sa->render(); // Draw! - // uint32_t stop = millis(); - // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); - } + // uint32_t start = millis(); + sa->render(); // Draw! + // uint32_t stop = millis(); + // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); + } } // In some situations (e.g. layout or applet selection changes), // a user tile can end up without an assigned applet. // In this case, we will fill the empty space with diagonal lines. -void InkHUD::Renderer::renderPlaceholders() { - // Don't fill empty space with placeholders if a system applet wants exclusive use of the display - if (lockRendering) - return; +void InkHUD::Renderer::renderPlaceholders() +{ + // Don't fill empty space with placeholders if a system applet wants exclusive use of the display + if (lockRendering) + return; - // Ask the window manager which tiles are empty - std::vector emptyTiles = inkhud->getEmptyTiles(); + // Ask the window manager which tiles are empty + std::vector emptyTiles = inkhud->getEmptyTiles(); - // No empty tiles - if (emptyTiles.size() == 0) - return; + // No empty tiles + if (emptyTiles.size() == 0) + return; - SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder"); + SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder"); - // uint32_t start = millis(); - for (Tile *t : emptyTiles) { - t->assignApplet(placeholder); - placeholder->render(); - t->assignApplet(nullptr); - } - // uint32_t stop = millis(); - // LOG_DEBUG("Placeholders took %dms to render", stop - start); + // uint32_t start = millis(); + for (Tile *t : emptyTiles) { + t->assignApplet(placeholder); + placeholder->render(); + t->assignApplet(nullptr); + } + // uint32_t stop = millis(); + // LOG_DEBUG("Placeholders took %dms to render", stop - start); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Renderer.h b/src/graphics/niche/InkHUD/Renderer.h index a0a466567..b6cf9e215 100644 --- a/src/graphics/niche/InkHUD/Renderer.h +++ b/src/graphics/niche/InkHUD/Renderer.h @@ -19,74 +19,76 @@ Orchestrates updating of the display image #include "./Persistence.h" #include "graphics/niche/Drivers/EInk/EInk.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class Renderer : protected concurrency::OSThread { +class Renderer : protected concurrency::OSThread +{ -public: - Renderer(); + public: + Renderer(); - // Configuration, before begin + // Configuration, before begin - void setDriver(Drivers::EInk *driver); - void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); + void setDriver(Drivers::EInk *driver); + void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); - void begin(); + void begin(); - // Call these to make the image change + // Call these to make the image change - void requestUpdate(); // Update display, if a foreground applet has info it wants to show - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, - bool async = true); // Update display, regardless of whether any applets requested this + void requestUpdate(); // Update display, if a foreground applet has info it wants to show + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, + bool async = true); // Update display, regardless of whether any applets requested this - // Wait for an update to complete - void awaitUpdate(); + // Wait for an update to complete + void awaitUpdate(); - // Receives pixel output from an applet (via a tile, which translates the coordinates) - void handlePixel(int16_t x, int16_t y, Color c); + // Receives pixel output from an applet (via a tile, which translates the coordinates) + void handlePixel(int16_t x, int16_t y, Color c); - // Size of display, in context of current rotation + // Size of display, in context of current rotation - uint16_t width(); - uint16_t height(); + uint16_t width(); + uint16_t height(); -private: - // Make attemps to render / update, once triggered by requestUpdate or forceUpdate - int32_t runOnce() override; + private: + // Make attemps to render / update, once triggered by requestUpdate or forceUpdate + int32_t runOnce() override; - // Apply the display rotation to handled pixels - void rotatePixelCoords(int16_t *x, int16_t *y); + // Apply the display rotation to handled pixels + void rotatePixelCoords(int16_t *x, int16_t *y); - // Execute the render process now, then hand off to driver for display update - void render(bool async = true); + // Execute the render process now, then hand off to driver for display update + void render(bool async = true); - // Steps of the rendering process + // Steps of the rendering process - void clearBuffer(); - void checkLocks(); - bool shouldUpdate(); - Drivers::EInk::UpdateTypes decideUpdateType(); - void renderUserApplets(); - void renderSystemApplets(); - void renderPlaceholders(); + void clearBuffer(); + void checkLocks(); + bool shouldUpdate(); + Drivers::EInk::UpdateTypes decideUpdateType(); + void renderUserApplets(); + void renderSystemApplets(); + void renderPlaceholders(); - Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware - DisplayHealth displayHealth; // Manages display health by controlling type of update + Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware + DisplayHealth displayHealth; // Manages display health by controlling type of update - uint8_t *imageBuffer = nullptr; // Fed into driver - uint16_t imageBufferHeight = 0; - uint16_t imageBufferWidth = 0; - uint32_t imageBufferSize = 0; // Bytes + uint8_t *imageBuffer = nullptr; // Fed into driver + uint16_t imageBufferHeight = 0; + uint16_t imageBufferWidth = 0; + uint32_t imageBufferSize = 0; // Bytes - SystemApplet *lockRendering = nullptr; // Render this applet *only* - SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only* + SystemApplet *lockRendering = nullptr; // Render this applet *only* + SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only* - bool requested = false; - bool forced = false; + bool requested = false; + bool forced = false; - // For convenience - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; + // For convenience + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/SystemApplet.h b/src/graphics/niche/InkHUD/SystemApplet.h index 2ac15986f..7ee47eeb9 100644 --- a/src/graphics/niche/InkHUD/SystemApplet.h +++ b/src/graphics/niche/InkHUD/SystemApplet.h @@ -14,27 +14,28 @@ For features like the menu, and the battery icon. #include "./Applet.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class SystemApplet : public Applet { -public: - // System applets have the right to: +class SystemApplet : public Applet +{ + public: + // System applets have the right to: - bool handleInput = false; // - respond to input from the user button - bool lockRendering = false; // - prevent other applets from being rendered during an update - bool lockRequests = false; // - prevent other applets from triggering display updates + bool handleInput = false; // - respond to input from the user button + bool lockRendering = false; // - prevent other applets from being rendered during an update + bool lockRequests = false; // - prevent other applets from triggering display updates - virtual void onReboot() { onShutdown(); } // - handle reboot specially + virtual void onReboot() { onShutdown(); } // - handle reboot specially - // Other system applets may take precedence over our own system applet though - // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher - // rank) + // Other system applets may take precedence over our own system applet though + // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank) -private: - // System applets are always running (active), but may not be visible (foreground) + private: + // System applets are always running (active), but may not be visible (foreground) - void onActivate() override {} - void onDeactivate() override {} + void onActivate() override {} + void onDeactivate() override {} }; }; // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Tile.cpp b/src/graphics/niche/InkHUD/Tile.cpp index 7f05a7dee..5e548de74 100644 --- a/src/graphics/niche/InkHUD/Tile.cpp +++ b/src/graphics/niche/InkHUD/Tile.cpp @@ -13,132 +13,138 @@ bool InkHUD::Tile::highlightShown; // For dismissing the highlight indicator, after a few seconds // Highlighting is used to inform user of which tile is now focused static concurrency::Periodic *taskHighlight; -static int32_t runtaskHighlight() { - LOG_DEBUG("Dismissing Highlight"); - InkHUD::Tile::highlightShown = false; - InkHUD::Tile::highlightTarget = nullptr; - InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting - return taskHighlight->disable(); +static int32_t runtaskHighlight() +{ + LOG_DEBUG("Dismissing Highlight"); + InkHUD::Tile::highlightShown = false; + InkHUD::Tile::highlightTarget = nullptr; + InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting + return taskHighlight->disable(); } -static void inittaskHighlight() { - static bool doneOnce = false; - if (!doneOnce) { - taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight); - taskHighlight->disable(); - doneOnce = true; - } +static void inittaskHighlight() +{ + static bool doneOnce = false; + if (!doneOnce) { + taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight); + taskHighlight->disable(); + doneOnce = true; + } } -InkHUD::Tile::Tile() { - inkhud = InkHUD::getInstance(); +InkHUD::Tile::Tile() +{ + inkhud = InkHUD::getInstance(); - inittaskHighlight(); - Tile::highlightTarget = nullptr; - Tile::highlightShown = false; + inittaskHighlight(); + Tile::highlightTarget = nullptr; + Tile::highlightShown = false; } -InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height) { - assert(width > 0 && height > 0); +InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height) +{ + assert(width > 0 && height > 0); - this->left = left; - this->top = top; - this->width = width; - this->height = height; + this->left = left; + this->top = top; + this->width = width; + this->height = height; } // Set the region of the tile automatically, based on the user's chosen layout // This method places tiles which will host user applets // The WindowManager multiplexes the applets to these tiles automatically -void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex) { - uint16_t displayWidth = inkhud->width(); - uint16_t displayHeight = inkhud->height(); +void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex) +{ + uint16_t displayWidth = inkhud->width(); + uint16_t displayHeight = inkhud->height(); - bool landscape = displayWidth > displayHeight; + bool landscape = displayWidth > displayHeight; - // Check for any stray tiles - if (tileIndex > (userTileCount - 1)) { - // Dummy values to prevent rendering - LOG_WARN("Tile index out of bounds"); - left = -2; - top = -2; - width = 1; - height = 1; - return; - } - - // Todo: special handling for 3 tile layout - - // Gutters between tiles - const uint16_t spacing = 4; - - switch (userTileCount) { - // One tile only - case 1: - left = 0; - top = 0; - width = displayWidth; - height = displayHeight; - break; - - // Two tiles - case 2: - if (landscape) { - // Side by side - left = ((displayWidth / 2) + (spacing / 2)) * tileIndex; - top = 0; - width = (displayWidth / 2) - (spacing / 2); - height = displayHeight; - } else { - // Above and below - left = 0; - top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex); - width = displayWidth; - height = (displayHeight / 2) - (spacing / 2); + // Check for any stray tiles + if (tileIndex > (userTileCount - 1)) { + // Dummy values to prevent rendering + LOG_WARN("Tile index out of bounds"); + left = -2; + top = -2; + width = 1; + height = 1; + return; } - break; - // Four tiles - case 4: - width = (displayWidth / 2) - (spacing / 2); - height = (displayHeight / 2) - (spacing / 2); - switch (tileIndex) { - case 0: - left = 0; - top = 0; - break; + // Todo: special handling for 3 tile layout + + // Gutters between tiles + const uint16_t spacing = 4; + + switch (userTileCount) { + // One tile only case 1: - left = 0 + (width - 1) + spacing; - top = 0; - break; + left = 0; + top = 0; + width = displayWidth; + height = displayHeight; + break; + + // Two tiles case 2: - left = 0; - top = 0 + (height - 1) + spacing; - break; - case 3: - left = 0 + (width - 1) + spacing; - top = 0 + (height - 1) + spacing; - break; + if (landscape) { + // Side by side + left = ((displayWidth / 2) + (spacing / 2)) * tileIndex; + top = 0; + width = (displayWidth / 2) - (spacing / 2); + height = displayHeight; + } else { + // Above and below + left = 0; + top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex); + width = displayWidth; + height = (displayHeight / 2) - (spacing / 2); + } + break; + + // Four tiles + case 4: + width = (displayWidth / 2) - (spacing / 2); + height = (displayHeight / 2) - (spacing / 2); + switch (tileIndex) { + case 0: + left = 0; + top = 0; + break; + case 1: + left = 0 + (width - 1) + spacing; + top = 0; + break; + case 2: + left = 0; + top = 0 + (height - 1) + spacing; + break; + case 3: + left = 0 + (width - 1) + spacing; + top = 0 + (height - 1) + spacing; + break; + } + break; + + default: + LOG_ERROR("Unsupported tile layout"); + assert(0); } - break; - default: - LOG_ERROR("Unsupported tile layout"); - assert(0); - } - - assert(width > 0 && height > 0); + assert(width > 0 && height > 0); } // Manually set the region for a tile // This is only done for tiles which will host certain "System Applets", which have unique position / sizes: // Things like the NotificationApplet, BatteryIconApplet, etc -void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height) { - assert(width > 0 && height > 0); +void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height) +{ + assert(width > 0 && height > 0); - this->left = left; - this->top = top; - this->width = width; - this->height = height; + this->left = left; + this->top = top; + this->width = width; + this->height = height; } // Place an applet onto a tile @@ -148,73 +154,88 @@ void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t // This is enforced with asserts // Assigning a new applet will break a previous link // Link may also be broken by assigning a nullptr -void InkHUD::Tile::assignApplet(Applet *a) { - // Break the link between old applet and this tile - if (assignedApplet) - assignedApplet->setTile(nullptr); +void InkHUD::Tile::assignApplet(Applet *a) +{ + // Break the link between old applet and this tile + if (assignedApplet) + assignedApplet->setTile(nullptr); - // Store the new applet - assignedApplet = a; + // Store the new applet + assignedApplet = a; - // Create the reciprocal link between the new applet and this tile - if (a) - a->setTile(this); + // Create the reciprocal link between the new applet and this tile + if (a) + a->setTile(this); } // Get pointer to whichever applet is displayed on this tile -InkHUD::Applet *InkHUD::Tile::getAssignedApplet() { return assignedApplet; } +InkHUD::Applet *InkHUD::Tile::getAssignedApplet() +{ + return assignedApplet; +} // Receive drawing output from the assigned applet, // and translate it from "applet-space" coordinates, to it's true location. // The final "rotation" step is performed by the windowManager -void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) { - // Move pixels from applet-space to tile-space - x += left; - y += top; +void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) +{ + // Move pixels from applet-space to tile-space + x += left; + y += top; - // Crop to tile borders - if (x >= left && x < (left + width) && y >= top && y < (top + height)) { - // Pass to the renderer - inkhud->drawPixel(x, y, c); - } + // Crop to tile borders + if (x >= left && x < (left + width) && y >= top && y < (top + height)) { + // Pass to the renderer + inkhud->drawPixel(x, y, c); + } } // Called by Applet base class, when setting applet dimensions, immediately before render -uint16_t InkHUD::Tile::getWidth() { return width; } +uint16_t InkHUD::Tile::getWidth() +{ + return width; +} // Called by Applet base class, when setting applet dimensions, immediately before render -uint16_t InkHUD::Tile::getHeight() { return height; } +uint16_t InkHUD::Tile::getHeight() +{ + return height; +} // Longest edge of the display, in pixels // A 296px x 250px display will return 296, for example // Maximum possible size of any tile's width / height // Used by some components to allocate resources for the "worst possible situation" // "Sizing the cathedral for christmas eve" -uint16_t InkHUD::Tile::maxDisplayDimension() { - InkHUD *inkhud = InkHUD::getInstance(); - return max(inkhud->height(), inkhud->width()); +uint16_t InkHUD::Tile::maxDisplayDimension() +{ + InkHUD *inkhud = InkHUD::getInstance(); + return max(inkhud->height(), inkhud->width()); } // Ask for this tile to be highlighted // Used to indicate which tile is now indicated after focus changes // Only used for aux button focus changes, not changes via menu -void InkHUD::Tile::requestHighlight() { - Tile::highlightTarget = this; - Tile::highlightShown = false; - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST); +void InkHUD::Tile::requestHighlight() +{ + Tile::highlightTarget = this; + Tile::highlightShown = false; + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST); } // Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first -void InkHUD::Tile::startHighlightTimeout() { - taskHighlight->setIntervalFromNow(5 * 1000UL); - taskHighlight->enabled = true; +void InkHUD::Tile::startHighlightTimeout() +{ + taskHighlight->setIntervalFromNow(5 * 1000UL); + taskHighlight->enabled = true; } // Stop the timer which would automatically dismiss the highlighting // Called if the tile organically renders before the timer is up -void InkHUD::Tile::cancelHighlightTimeout() { - if (taskHighlight->enabled) - taskHighlight->disable(); +void InkHUD::Tile::cancelHighlightTimeout() +{ + if (taskHighlight->enabled) + taskHighlight->disable(); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Tile.h b/src/graphics/niche/InkHUD/Tile.h index 48a518781..0f5444f17 100644 --- a/src/graphics/niche/InkHUD/Tile.h +++ b/src/graphics/niche/InkHUD/Tile.h @@ -17,39 +17,41 @@ #include "./InkHUD.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class Tile { -public: - Tile(); - Tile(int16_t left, int16_t top, uint16_t width, uint16_t height); +class Tile +{ + public: + Tile(); + Tile(int16_t left, int16_t top, uint16_t width, uint16_t height); - void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout - void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually - void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet - uint16_t getWidth(); - uint16_t getHeight(); - static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter + void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout + void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually + void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet + uint16_t getWidth(); + uint16_t getHeight(); + static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter - void assignApplet(Applet *a); // Link an applet with this tile - Applet *getAssignedApplet(); // Applet which is currently linked with this tile + void assignApplet(Applet *a); // Link an applet with this tile + Applet *getAssignedApplet(); // Applet which is currently linked with this tile - void requestHighlight(); // Ask for this tile to be highlighted - static void startHighlightTimeout(); // Start the auto-dismissal timer - static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed + void requestHighlight(); // Ask for this tile to be highlighted + static void startHighlightTimeout(); // Start the auto-dismissal timer + static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed - static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?) - static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss + static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?) + static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss -private: - InkHUD *inkhud = nullptr; + private: + InkHUD *inkhud = nullptr; - int16_t left = 0; - int16_t top = 0; - uint16_t width = 0; - uint16_t height = 0; + int16_t left = 0; + int16_t top = 0; + uint16_t width = 0; + uint16_t height = 0; - Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile + Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index fbd7db157..0548de1eb 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -14,10 +14,11 @@ using namespace NicheGraphics; -InkHUD::WindowManager::WindowManager() { - // Convenient references - inkhud = InkHUD::getInstance(); - settings = &inkhud->persistence->settings; +InkHUD::WindowManager::WindowManager() +{ + // Convenient references + inkhud = InkHUD::getInstance(); + settings = &inkhud->persistence->settings; } // Register a user applet with InkHUD @@ -25,351 +26,365 @@ InkHUD::WindowManager::WindowManager() { // This should be the only time that specific user applets are mentioned in the code // If a user applet is not added with this method, its code should not be built // Call before begin -void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { - inkhud->userApplets.push_back(a); +void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) +{ + inkhud->userApplets.push_back(a); - // If requested, mark in settings that this applet should be active by default - // This means that it will be available for the user to cycle to with short-press of the button - // This is the default state only: user can activate or deactivate applets through the menu. - // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present - if (defaultActive) - settings->userApplets.active[inkhud->userApplets.size() - 1] = true; + // If requested, mark in settings that this applet should be active by default + // This means that it will be available for the user to cycle to with short-press of the button + // This is the default state only: user can activate or deactivate applets through the menu. + // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present + if (defaultActive) + settings->userApplets.active[inkhud->userApplets.size() - 1] = true; - // If requested, mark in settings that this applet should "autoshow" by default - // This means that the applet will be automatically brought to foreground when it has new data to show - // This is the default state only: user can select which applets have this behavior through the menu - // User's selection is stored in settings, and will be honored instead of these defaults, if present - if (defaultAutoshow) - settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true; + // If requested, mark in settings that this applet should "autoshow" by default + // This means that the applet will be automatically brought to foreground when it has new data to show + // This is the default state only: user can select which applets have this behavior through the menu + // User's selection is stored in settings, and will be honored instead of these defaults, if present + if (defaultAutoshow) + settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true; - // If specified, mark this as the default applet for a given tile index - // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile - if (onTile != (uint8_t)-1) - settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1; + // If specified, mark this as the default applet for a given tile index + // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile + if (onTile != (uint8_t)-1) + settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1; - // The label that will be show in the applet selection menu, on the device - a->name = name; + // The label that will be show in the applet selection menu, on the device + a->name = name; } // Initial configuration at startup -void InkHUD::WindowManager::begin() { - assert(inkhud); +void InkHUD::WindowManager::begin() +{ + assert(inkhud); - createSystemApplets(); - placeSystemTiles(); + createSystemApplets(); + placeSystemTiles(); - createUserApplets(); - createUserTiles(); - placeUserTiles(); - assignUserAppletsToTiles(); - refocusTile(); + createUserApplets(); + createUserTiles(); + placeUserTiles(); + assignUserAppletsToTiles(); + refocusTile(); } // Focus on a different tile // The "focused tile" is the one which cycles applets on user button press, // and the one where the menu will be displayed -void InkHUD::WindowManager::nextTile() { - // Close the menu applet if open - // We don't *really* want to do this, but it simplifies handling *a lot* - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - bool menuWasOpen = false; - if (menu->isForeground()) { - menu->sendToBackground(); - menuWasOpen = true; - } +void InkHUD::WindowManager::nextTile() +{ + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } - // Swap to next tile - settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count; + // Swap to next tile + settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count; - // Make sure that we don't get stuck on the placeholder tile - refocusTile(); + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); - if (menuWasOpen) - menu->show(userTiles.at(settings->userTiles.focused)); + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); - // Ask the tile to draw an indicator showing which tile is now focused - // Requests a render - // We only draw this indicator if the device uses an aux button to switch tiles. - // Assume aux button is used to switch tiles if the "next tile" menu item is hidden - if (!settings->optionalMenuItems.nextTile) - userTiles.at(settings->userTiles.focused)->requestHighlight(); + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); } // Focus on a different tile but decrement index -void InkHUD::WindowManager::prevTile() { - // Close the menu applet if open - // We don't *really* want to do this, but it simplifies handling *a lot* - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - bool menuWasOpen = false; - if (menu->isForeground()) { - menu->sendToBackground(); - menuWasOpen = true; - } +void InkHUD::WindowManager::prevTile() +{ + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } - // Swap to next tile - if (settings->userTiles.focused == 0) - settings->userTiles.focused = settings->userTiles.count - 1; - else - settings->userTiles.focused--; + // Swap to next tile + if (settings->userTiles.focused == 0) + settings->userTiles.focused = settings->userTiles.count - 1; + else + settings->userTiles.focused--; - // Make sure that we don't get stuck on the placeholder tile - refocusTile(); + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); - if (menuWasOpen) - menu->show(userTiles.at(settings->userTiles.focused)); + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); - // Ask the tile to draw an indicator showing which tile is now focused - // Requests a render - // We only draw this indicator if the device uses an aux button to switch tiles. - // Assume aux button is used to switch tiles if the "next tile" menu item is hidden - if (!settings->optionalMenuItems.nextTile) - userTiles.at(settings->userTiles.focused)->requestHighlight(); + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); } // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes -void InkHUD::WindowManager::openMenu() { - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - menu->show(userTiles.at(settings->userTiles.focused)); +void InkHUD::WindowManager::openMenu() +{ + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + menu->show(userTiles.at(settings->userTiles.focused)); } // Bring the AlignStick applet to the foreground -void InkHUD::WindowManager::openAlignStick() { - if (settings->joystick.enabled) { - AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); - alignStick->bringToForeground(); - } +void InkHUD::WindowManager::openAlignStick() +{ + if (settings->joystick.enabled) { + AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); + alignStick->bringToForeground(); + } } // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile -void InkHUD::WindowManager::nextApplet() { - Tile *t = userTiles.at(settings->userTiles.focused); +void InkHUD::WindowManager::nextApplet() +{ + Tile *t = userTiles.at(settings->userTiles.focused); - // Abort if zero applets available - // nullptr means WindowManager::refocusTile determined that there were no available applets - if (!t->getAssignedApplet()) - return; + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; - // Find the index of the applet currently shown on the tile - uint8_t appletIndex = -1; - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { - appletIndex = i; - break; + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; + } } - } - // Confirm that we did find the applet - assert(appletIndex != (uint8_t)-1); + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); - // Iterate forward through the WindowManager::applets, looking for the next valid applet - Applet *nextValidApplet = nullptr; - for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { - uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size(); - Applet *a = inkhud->userApplets.at(newAppletIndex); + // Iterate forward through the WindowManager::applets, looking for the next valid applet + Applet *nextValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size(); + Applet *a = inkhud->userApplets.at(newAppletIndex); - // Looking for an applet which is active (enabled by user), but currently in background - if (a->isActive() && !a->isForeground()) { - nextValidApplet = a; - settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! - break; + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + nextValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = + newAppletIndex; // Remember this setting between boots! + break; + } } - } - // Confirm that we found another applet - if (!nextValidApplet) - return; + // Confirm that we found another applet + if (!nextValidApplet) + return; - // Hide old applet, show new applet - t->getAssignedApplet()->sendToBackground(); - t->assignApplet(nextValidApplet); - nextValidApplet->bringToForeground(); - inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(nextValidApplet); + nextValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } // On the currently focussed tile: cycle to the previous available user applet // Applets available for this must be activated, and not already displayed on another tile -void InkHUD::WindowManager::prevApplet() { - Tile *t = userTiles.at(settings->userTiles.focused); +void InkHUD::WindowManager::prevApplet() +{ + Tile *t = userTiles.at(settings->userTiles.focused); - // Abort if zero applets available - // nullptr means WindowManager::refocusTile determined that there were no available applets - if (!t->getAssignedApplet()) - return; + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; - // Find the index of the applet currently shown on the tile - uint8_t appletIndex = -1; - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { - appletIndex = i; - break; + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; + } } - } - // Confirm that we did find the applet - assert(appletIndex != (uint8_t)-1); + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); - // Iterate forward through the WindowManager::applets, looking for the previous valid applet - Applet *prevValidApplet = nullptr; - for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { - uint8_t newAppletIndex = 0; - if (i > appletIndex) - newAppletIndex = inkhud->userApplets.size() + appletIndex - i; - else - newAppletIndex = (appletIndex - i); - Applet *a = inkhud->userApplets.at(newAppletIndex); + // Iterate forward through the WindowManager::applets, looking for the previous valid applet + Applet *prevValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = 0; + if (i > appletIndex) + newAppletIndex = inkhud->userApplets.size() + appletIndex - i; + else + newAppletIndex = (appletIndex - i); + Applet *a = inkhud->userApplets.at(newAppletIndex); - // Looking for an applet which is active (enabled by user), but currently in background - if (a->isActive() && !a->isForeground()) { - prevValidApplet = a; - settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! - break; + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + prevValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = + newAppletIndex; // Remember this setting between boots! + break; + } } - } - // Confirm that we found another applet - if (!prevValidApplet) - return; + // Confirm that we found another applet + if (!prevValidApplet) + return; - // Hide old applet, show new applet - t->getAssignedApplet()->sendToBackground(); - t->assignApplet(prevValidApplet); - prevValidApplet->bringToForeground(); - inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(prevValidApplet); + prevValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } // Rotate the display image by 90 degrees -void InkHUD::WindowManager::rotate() { - settings->rotation = (settings->rotation + 1) % 4; - changeLayout(); +void InkHUD::WindowManager::rotate() +{ + settings->rotation = (settings->rotation + 1) % 4; + changeLayout(); } // Change whether the battery icon is displayed (top right corner) // Don't toggle the OptionalFeatures value before calling this, our method handles it internally -void InkHUD::WindowManager::toggleBatteryIcon() { - BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon"); +void InkHUD::WindowManager::toggleBatteryIcon() +{ + BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon"); - settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots + settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots - // Show or hide the applet - if (settings->optionalFeatures.batteryIcon) - batteryIcon->bringToForeground(); - else - batteryIcon->sendToBackground(); + // Show or hide the applet + if (settings->optionalFeatures.batteryIcon) + batteryIcon->bringToForeground(); + else + batteryIcon->sendToBackground(); - // Force-render - // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time // Call after changing settings.tiles.count -void InkHUD::WindowManager::changeLayout() { - // Recreate tiles - // - correct number created, from settings.userTiles.count - // - set dimension and position of tiles, according to layout - createUserTiles(); - placeUserTiles(); - placeSystemTiles(); +void InkHUD::WindowManager::changeLayout() +{ + // Recreate tiles + // - correct number created, from settings.userTiles.count + // - set dimension and position of tiles, according to layout + createUserTiles(); + placeUserTiles(); + placeSystemTiles(); - // Handle fewer tiles - // - background any applets which have lost their tile - findOrphanApplets(); + // Handle fewer tiles + // - background any applets which have lost their tile + findOrphanApplets(); - // Handle more tiles - // - create extra applets - // - assign them to the new extra tiles - createUserApplets(); - assignUserAppletsToTiles(); + // Handle more tiles + // - create extra applets + // - assign them to the new extra tiles + createUserApplets(); + assignUserAppletsToTiles(); - // Focus a valid tile - // - info: focused tile is the one which cycles applets when user button pressed - // - may now be out of bounds if tile count has decreased - refocusTile(); + // Focus a valid tile + // - info: focused tile is the one which cycles applets when user button pressed + // - may now be out of bounds if tile count has decreased + refocusTile(); - // Restore menu - // - its tile was just destroyed and recreated (createUserTiles) - // - its assignment was cleared (assignUserAppletsToTiles) - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - if (menu->isForeground()) { - Tile *ft = userTiles.at(settings->userTiles.focused); - menu->show(ft); - } + // Restore menu + // - its tile was just destroyed and recreated (createUserTiles) + // - its assignment was cleared (assignUserAppletsToTiles) + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + if (menu->isForeground()) { + Tile *ft = userTiles.at(settings->userTiles.focused); + menu->show(ft); + } - // Force-render - // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user activates or deactivates applets at run-time // Call after changing settings.userApplets.active -void InkHUD::WindowManager::changeActivatedApplets() { - MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); +void InkHUD::WindowManager::changeActivatedApplets() +{ + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); - assert(menu->isForeground()); + assert(menu->isForeground()); - // Activate or deactivate applets - // - to match value of settings.userApplets.active - createUserApplets(); + // Activate or deactivate applets + // - to match value of settings.userApplets.active + createUserApplets(); - // Assign the placeholder applet - // - if applet was foreground on a tile when deactivated, swap it with a placeholder - // - placeholder applet may be assigned to multiple tiles, if needed - assignUserAppletsToTiles(); + // Assign the placeholder applet + // - if applet was foreground on a tile when deactivated, swap it with a placeholder + // - placeholder applet may be assigned to multiple tiles, if needed + assignUserAppletsToTiles(); - // Ensure focused tile has a valid applet - // - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder - // - reason: nextApplet() won't cycle applets if placeholder is shown - refocusTile(); + // Ensure focused tile has a valid applet + // - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder + // - reason: nextApplet() won't cycle applets if placeholder is shown + refocusTile(); - // Restore menu - // - its assignment was cleared (assignUserAppletsToTiles) - if (menu->isForeground()) { - Tile *ft = userTiles.at(settings->userTiles.focused); - menu->show(ft); - } + // Restore menu + // - its assignment was cleared (assignUserAppletsToTiles) + if (menu->isForeground()) { + Tile *ft = userTiles.at(settings->userTiles.focused); + menu->show(ft); + } - // Force-render - // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + // Force-render + // - redraw all applets + inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Some applets may be permitted to bring themselves to foreground, to show new data // User selects which applets have this permission via on-screen menu // Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics // We will only autoshow one applet -void InkHUD::WindowManager::autoshow() { - // Don't perform autoshow if a system applet has exclusive use of the display right now - // Note: lockRequests prevents autoshow attempting to hide menuApplet - for (SystemApplet *sa : inkhud->systemApplets) { - if (sa->lockRendering || sa->lockRequests) - return; - } - - NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification"); - - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - Applet *a = inkhud->userApplets.at(i); - if (a->wantsToAutoshow() // Applet wants to become foreground - && !a->isForeground() // Not yet foreground - && settings->userApplets.autoshow[i]) // User permits this applet to autoshow - { - Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile - t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile - t->assignApplet(a); // Assign our new applet to tile - a->bringToForeground(); // Foreground our new applet - - // Check if autoshown applet shows the same information as notification intended to - // In this case, we can dismiss the notification before it is shown - // Note: we are re-running the approval process. This normally occurs when the notification is initially - // triggered. - if (notificationApplet->isForeground() && !notificationApplet->isApproved()) - notificationApplet->dismiss(); - - break; // One autoshow only! Avoid conflicts +void InkHUD::WindowManager::autoshow() +{ + // Don't perform autoshow if a system applet has exclusive use of the display right now + // Note: lockRequests prevents autoshow attempting to hide menuApplet + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->lockRendering || sa->lockRequests) + return; + } + + NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification"); + + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a->wantsToAutoshow() // Applet wants to become foreground + && !a->isForeground() // Not yet foreground + && settings->userApplets.autoshow[i]) // User permits this applet to autoshow + { + Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile + t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile + t->assignApplet(a); // Assign our new applet to tile + a->bringToForeground(); // Foreground our new applet + + // Check if autoshown applet shows the same information as notification intended to + // In this case, we can dismiss the notification before it is shown + // Note: we are re-running the approval process. This normally occurs when the notification is initially triggered. + if (notificationApplet->isForeground() && !notificationApplet->isApproved()) + notificationApplet->dismiss(); + + break; // One autoshow only! Avoid conflicts + } } - } } // A collection of any user tiles which do not have a valid user applet @@ -378,16 +393,17 @@ void InkHUD::WindowManager::autoshow() { // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- -std::vector InkHUD::WindowManager::getEmptyTiles() { - std::vector empty; +std::vector InkHUD::WindowManager::getEmptyTiles() +{ + std::vector empty; - for (Tile *t : userTiles) { - Applet *a = t->getAssignedApplet(); - if (!a || !a->isActive()) - empty.push_back(t); - } + for (Tile *t : userTiles) { + Applet *a = t->getAssignedApplet(); + if (!a || !a->isActive()) + empty.push_back(t); + } - return empty; + return empty; } // Complete the configuration of one newly instantiated system applet @@ -399,112 +415,118 @@ std::vector InkHUD::WindowManager::getEmptyTiles() { // The name is our only reference to specific system applets, via InkHUD->getSystemApplet // - add it to the list of system applets -void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile) { - // Some system applets might not have their own tile (e.g. menu, placeholder) - if (tile) - tile->assignApplet(applet); +void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile) +{ + // Some system applets might not have their own tile (e.g. menu, placeholder) + if (tile) + tile->assignApplet(applet); - applet->name = name; - inkhud->systemApplets.push_back(applet); + applet->name = name; + inkhud->systemApplets.push_back(applet); } // Create the "system applets" // These handle things like bootscreen, pop-up notifications etc // They are processed separately from the user applets, because they might need to do "weird things" -void InkHUD::WindowManager::createSystemApplets() { - addSystemApplet("Logo", new LogoApplet, new Tile); - addSystemApplet("Pairing", new PairingApplet, new Tile); - addSystemApplet("Tips", new TipsApplet, new Tile); - if (settings->joystick.enabled) - addSystemApplet("AlignStick", new AlignStickApplet, new Tile); +void InkHUD::WindowManager::createSystemApplets() +{ + addSystemApplet("Logo", new LogoApplet, new Tile); + addSystemApplet("Pairing", new PairingApplet, new Tile); + addSystemApplet("Tips", new TipsApplet, new Tile); + if (settings->joystick.enabled) + addSystemApplet("AlignStick", new AlignStickApplet, new Tile); - addSystemApplet("Menu", new MenuApplet, nullptr); + addSystemApplet("Menu", new MenuApplet, nullptr); - // Battery and notifications *behind* the menu - addSystemApplet("Notification", new NotificationApplet, new Tile); - addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); + // Battery and notifications *behind* the menu + addSystemApplet("Notification", new NotificationApplet, new Tile); + addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); - // Special handling only, via Rendering::renderPlaceholders - addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); + // Special handling only, via Rendering::renderPlaceholders + addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); - // System applets are always active - for (SystemApplet *sa : inkhud->systemApplets) - sa->activate(); + // System applets are always active + for (SystemApplet *sa : inkhud->systemApplets) + sa->activate(); } // Set the position and size of most system applets // Most system applets have their own tile. We manually set the region this tile occupies -void InkHUD::WindowManager::placeSystemTiles() { - inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - if (settings->joystick.enabled) - inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); +void InkHUD::WindowManager::placeSystemTiles() +{ + inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + if (settings->joystick.enabled) + inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); + inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); - const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; - const uint16_t batteryIconWidth = batteryIconHeight * 1.8; - inkhud->getSystemApplet("BatteryIcon") - ->getTile() - ->setRegion(inkhud->width() - batteryIconWidth, // x - 2, // y - batteryIconWidth, // width - batteryIconHeight); // height + const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; + const uint16_t batteryIconWidth = batteryIconHeight * 1.8; + inkhud->getSystemApplet("BatteryIcon") + ->getTile() + ->setRegion(inkhud->width() - batteryIconWidth, // x + 2, // y + batteryIconWidth, // width + batteryIconHeight); // height - // Note: the tiles of placeholder and menu applets are manipulated specially - // - menuApplet borrows user tiles - // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles + // Note: the tiles of placeholder and menu applets are manipulated specially + // - menuApplet borrows user tiles + // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles } // Activate or deactivate user applets, to match settings // Called at boot, or after run-time config changes via menu // Note: this method does not instantiate the applets; // this is done in setupNicheGraphics, when passing to InkHUD::addApplet -void InkHUD::WindowManager::createUserApplets() { - // Deactivate and remove any no-longer-needed applets - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - Applet *a = inkhud->userApplets.at(i); +void InkHUD::WindowManager::createUserApplets() +{ + // Deactivate and remove any no-longer-needed applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); - // If the applet is active, but settings say it shouldn't be: - // - run applet's custom deactivation code - // - mark applet as inactive (internally) - if (a->isActive() && !settings->userApplets.active[i]) - a->deactivate(); - } + // If the applet is active, but settings say it shouldn't be: + // - run applet's custom deactivation code + // - mark applet as inactive (internally) + if (a->isActive() && !settings->userApplets.active[i]) + a->deactivate(); + } - // Activate and add any new applets - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + // Activate and add any new applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - // If not activated, but it now should be: - // - run applet's custom activation code - // - mark applet as active (internally) - if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i]) - inkhud->userApplets.at(i)->activate(); - } + // If not activated, but it now should be: + // - run applet's custom activation code + // - mark applet as active (internally) + if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i]) + inkhud->userApplets.at(i)->activate(); + } } // Creates the tiles which will host user applets // The amount of these is controlled by the user, via "layout" option in the InkHUD menu -void InkHUD::WindowManager::createUserTiles() { - // Delete any tiles which currently exist - for (Tile *t : userTiles) - delete t; - userTiles.clear(); +void InkHUD::WindowManager::createUserTiles() +{ + // Delete any tiles which currently exist + for (Tile *t : userTiles) + delete t; + userTiles.clear(); - // Create new tiles - for (uint8_t i = 0; i < settings->userTiles.count; i++) { - Tile *t = new Tile; - userTiles.push_back(t); - } + // Create new tiles + for (uint8_t i = 0; i < settings->userTiles.count; i++) { + Tile *t = new Tile; + userTiles.push_back(t); + } } // Calculate the display region occupied by each tile // This determines how pixels are translated from "relative" applet-space to "absolute" windowmanager-space // The size and position depend on the amount of tiles the user prefers, set by the "layout" option -void InkHUD::WindowManager::placeUserTiles() { - for (uint8_t i = 0; i < userTiles.size(); i++) - userTiles.at(i)->setRegion(settings->userTiles.count, i); +void InkHUD::WindowManager::placeUserTiles() +{ + for (uint8_t i = 0; i < userTiles.size(); i++) + userTiles.at(i)->setRegion(settings->userTiles.count, i); } // Link "foreground" user applets with tiles @@ -512,96 +534,99 @@ void InkHUD::WindowManager::placeUserTiles() { // This initial state changes once WindowManager::nextApplet is called. // Performed at startup, or during certain run-time reconfigurations (e.g number of tiles) // This state of "which applets are foreground" is preserved between reboots, but the value needs validating at startup. -void InkHUD::WindowManager::assignUserAppletsToTiles() { - // Each user tile - for (uint8_t i = 0; i < userTiles.size(); i++) { - Tile *t = userTiles.at(i); +void InkHUD::WindowManager::assignUserAppletsToTiles() +{ + // Each user tile + for (uint8_t i = 0; i < userTiles.size(); i++) { + Tile *t = userTiles.at(i); - // Check whether tile can display the previously shown applet again - uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets - bool canRestore = true; - if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds - canRestore = false; - else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated - canRestore = false; - else { // Check that the old applet isn't now shown already on a different tile - for (uint8_t i2 = 0; i2 < i; i2++) { - if (settings->userTiles.displayedUserApplet[i2] == oldIndex) { - canRestore = false; - break; + // Check whether tile can display the previously shown applet again + uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets + bool canRestore = true; + if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds + canRestore = false; + else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated + canRestore = false; + else { // Check that the old applet isn't now shown already on a different tile + for (uint8_t i2 = 0; i2 < i; i2++) { + if (settings->userTiles.displayedUserApplet[i2] == oldIndex) { + canRestore = false; + break; + } + } } - } - } - // Restore previously shown applet if possible, - // otherwise assign nullptr, which will render specially using placeholderApplet - if (canRestore) { - Applet *a = inkhud->userApplets.at(oldIndex); - t->assignApplet(a); - a->bringToForeground(); - } else { - t->assignApplet(nullptr); - settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet + // Restore previously shown applet if possible, + // otherwise assign nullptr, which will render specially using placeholderApplet + if (canRestore) { + Applet *a = inkhud->userApplets.at(oldIndex); + t->assignApplet(a); + a->bringToForeground(); + } else { + t->assignApplet(nullptr); + settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet + } } - } } // During layout changes, our focused tile setting can become invalid // This method identifies that situation and corrects for it -void InkHUD::WindowManager::refocusTile() { - // Validate "focused tile" setting - // - info: focused tile responds to button presses: applet cycling, menu, etc - // - if number of tiles changed, might now be out of index - if (settings->userTiles.focused >= userTiles.size()) - settings->userTiles.focused = 0; +void InkHUD::WindowManager::refocusTile() +{ + // Validate "focused tile" setting + // - info: focused tile responds to button presses: applet cycling, menu, etc + // - if number of tiles changed, might now be out of index + if (settings->userTiles.focused >= userTiles.size()) + settings->userTiles.focused = 0; - // Give "focused tile" a valid applet - // - scan for another valid applet, which we can addSubstitution - // - reason: nextApplet() won't cycle if no applet is assigned - Tile *focusedTile = userTiles.at(settings->userTiles.focused); - if (!focusedTile->getAssignedApplet()) { - // Search for available applets - for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { - Applet *a = inkhud->userApplets.at(i); - if (a->isActive() && !a->isForeground()) { - // Found a suitable applet - // Assign it to the focused tile - focusedTile->assignApplet(a); - a->bringToForeground(); - settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot - break; - } + // Give "focused tile" a valid applet + // - scan for another valid applet, which we can addSubstitution + // - reason: nextApplet() won't cycle if no applet is assigned + Tile *focusedTile = userTiles.at(settings->userTiles.focused); + if (!focusedTile->getAssignedApplet()) { + // Search for available applets + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + Applet *a = inkhud->userApplets.at(i); + if (a->isActive() && !a->isForeground()) { + // Found a suitable applet + // Assign it to the focused tile + focusedTile->assignApplet(a); + a->bringToForeground(); + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot + break; + } + } } - } } // Seach for any applets which believe they are foreground, but no longer have a valid tile // Tidies up after layout changes at runtime -void InkHUD::WindowManager::findOrphanApplets() { - for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) { - Applet *a = inkhud->userApplets.at(ia); +void InkHUD::WindowManager::findOrphanApplets() +{ + for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) { + Applet *a = inkhud->userApplets.at(ia); - // Applet doesn't believe it is displayed: not orphaned - if (!a->isForeground()) - continue; + // Applet doesn't believe it is displayed: not orphaned + if (!a->isForeground()) + continue; - // Check each tile, to see if anyone claims this applet - bool foundOwner = false; - for (uint8_t it = 0; it < userTiles.size(); it++) { - Tile *t = userTiles.at(it); - // A tile claims this applet: not orphaned - if (t->getAssignedApplet() == a) { - foundOwner = true; - break; - } + // Check each tile, to see if anyone claims this applet + bool foundOwner = false; + for (uint8_t it = 0; it < userTiles.size(); it++) { + Tile *t = userTiles.at(it); + // A tile claims this applet: not orphaned + if (t->getAssignedApplet() == a) { + foundOwner = true; + break; + } + } + + // Orphan found + // Tell the applet that no tile is currently displaying it + // This allows the focussed tile to cycle to this applet again by pressing user button + if (!foundOwner) + a->sendToBackground(); } - - // Orphan found - // Tell the applet that no tile is currently displaying it - // This allows the focussed tile to cycle to this applet again by pressing user button - if (!foundOwner) - a->sendToBackground(); - } } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index dc11d0f5e..5def48f8c 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -15,57 +15,59 @@ Responsible for managing which applets are shown, and their sizes / positions #include "./Persistence.h" #include "./Tile.h" -namespace NicheGraphics::InkHUD { +namespace NicheGraphics::InkHUD +{ -class WindowManager { -public: - WindowManager(); - void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile); - void begin(); +class WindowManager +{ + public: + WindowManager(); + void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile); + void begin(); - // - call these to make stuff change + // - call these to make stuff change - void nextTile(); - void prevTile(); - void openMenu(); - void openAlignStick(); - void nextApplet(); - void prevApplet(); - void rotate(); - void toggleBatteryIcon(); + void nextTile(); + void prevTile(); + void openMenu(); + void openAlignStick(); + void nextApplet(); + void prevApplet(); + void rotate(); + void toggleBatteryIcon(); - // - call these to manifest changes already made to the relevant Persistence::Settings values + // - call these to manifest changes already made to the relevant Persistence::Settings values - void changeLayout(); // Change tile layout or count - void changeActivatedApplets(); // Change which applets are activated + void changeLayout(); // Change tile layout or count + void changeActivatedApplets(); // Change which applets are activated - // - called during the rendering operation + // - called during the rendering operation - void autoshow(); // Show a different applet, to display new info - std::vector getEmptyTiles(); // Any user tiles without a valid applet + void autoshow(); // Show a different applet, to display new info + std::vector getEmptyTiles(); // Any user tiles without a valid applet -private: - // Steps for configuring (or reconfiguring) the window manager - // - all steps required at startup - // - various combinations of steps required for on-the-fly reconfiguration (by user, via menu) + private: + // Steps for configuring (or reconfiguring) the window manager + // - all steps required at startup + // - various combinations of steps required for on-the-fly reconfiguration (by user, via menu) - void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile); - void createSystemApplets(); // Instantiate the system applets - void placeSystemTiles(); // Assign manual positions to (most) system applets + void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile); + void createSystemApplets(); // Instantiate the system applets + void placeSystemTiles(); // Assign manual positions to (most) system applets - void createUserApplets(); // Activate user's selected applets - void createUserTiles(); // Instantiate enough tiles for user's selected layout - void assignUserAppletsToTiles(); - void placeUserTiles(); // Automatically place tiles, according to user's layout - void refocusTile(); // Ensure focused tile has a valid applet + void createUserApplets(); // Activate user's selected applets + void createUserTiles(); // Instantiate enough tiles for user's selected layout + void assignUserAppletsToTiles(); + void placeUserTiles(); // Automatically place tiles, according to user's layout + void refocusTile(); // Ensure focused tile has a valid applet - void findOrphanApplets(); // Find any applets left-behind when layout changes + void findOrphanApplets(); // Find any applets left-behind when layout changes - std::vector userTiles; // Tiles which can host user applets + std::vector userTiles; // Tiles which can host user applets - // For convenience - InkHUD *inkhud = nullptr; - Persistence::Settings *settings = nullptr; + // For convenience + InkHUD *inkhud = nullptr; + Persistence::Settings *settings = nullptr; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index f470e156b..bd29f981d 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -8,284 +8,302 @@ using namespace NicheGraphics::Inputs; -TwoButton::TwoButton() : concurrency::OSThread("TwoButton") { - // Don't start polling buttons for release immediately - // Assume they are in a "released" state at boot - OSThread::disable(); +TwoButton::TwoButton() : concurrency::OSThread("TwoButton") +{ + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); #endif - // Explicitly initialize these, just to keep cppcheck quiet.. - buttons[0] = Button(); - buttons[1] = Button(); + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); } // Get access to (or create) the singleton instance of this class // Accessible inside the ISRs, even though we maybe shouldn't -TwoButton *TwoButton::getInstance() { - // Instantiate the class the first time this method is called - static TwoButton *const singletonInstance = new TwoButton; +TwoButton *TwoButton::getInstance() +{ + // Instantiate the class the first time this method is called + static TwoButton *const singletonInstance = new TwoButton; - return singletonInstance; + return singletonInstance; } // Begin receiving button input // We probably need to do this after sleep, as well as at boot -void TwoButton::start() { - if (buttons[0].pin != 0xFF) - attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); +void TwoButton::start() +{ + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); - if (buttons[1].pin != 0xFF) - attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); } // Stop receiving button input, and run custom sleep code // Called before device sleeps. This might be power-off, or just ESP32 light sleep // Some devices will want to attach interrupts here, for the user button to wake from sleep -void TwoButton::stop() { - if (buttons[0].pin != 0xFF) - detachInterrupt(buttons[0].pin); +void TwoButton::stop() +{ + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); - if (buttons[1].pin != 0xFF) - detachInterrupt(buttons[1].pin); + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // This helper method isn't used by the TweButton class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. -uint8_t TwoButton::getUserButtonPin() { - uint8_t pin = 0xFF; // Unset +uint8_t TwoButton::getUserButtonPin() +{ + uint8_t pin = 0xFF; // Unset - // Use default pin for variant, if no better source + // Use default pin for variant, if no better source #ifdef BUTTON_PIN - pin = BUTTON_PIN; + pin = BUTTON_PIN; #endif - // From userPrefs.jsonc, if set + // From userPrefs.jsonc, if set #ifdef USERPREFS_BUTTON_PIN - pin = USERPREFS_BUTTON_PIN; + pin = USERPREFS_BUTTON_PIN; #endif - // From user's override in device settings, if set - if (config.device.button_gpio) - pin = config.device.button_gpio; + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; - return pin; + return pin; } // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp -void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { - // Prevent the same GPIO being assigned to multiple buttons - // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button - for (uint8_t i = 0; i < whichButton; i++) { - if (buttons[i].pin == pin) { - LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); - return; +void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) +{ + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; + } } - } - assert(whichButton < 2); - buttons[whichButton].pin = pin; - buttons[whichButton].activeLogic = LOW; // Unimplemented + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; // Unimplemented - pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } -void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { - assert(whichButton < 2); - buttons[whichButton].debounceLength = debounceMs; - buttons[whichButton].longpressLength = longpressMs; +void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) +{ + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; } // Set what should happen when a button becomes pressed // Use this to implement a "while held" behavior -void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) { - assert(whichButton < 2); - buttons[whichButton].onDown = onDown; +void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) +{ + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; } // Set what should happen when a button becomes unpressed // Use this to implement a "While held" behavior -void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) { - assert(whichButton < 2); - buttons[whichButton].onUp = onUp; +void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) +{ + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; } // Set what should happen when a "short press" event has occurred -void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) { - assert(whichButton < 2); - buttons[whichButton].onShortPress = onShortPress; +void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) +{ + assert(whichButton < 2); + buttons[whichButton].onShortPress = onShortPress; } // Set what should happen when a "long press" event has fired // Note: this will occur while the button is still held -void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) { - assert(whichButton < 2); - buttons[whichButton].onLongPress = onLongPress; +void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) +{ + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; } // Handle the start of a press to the primary button // Wakes our button thread -void TwoButton::isrPrimary() { - static volatile bool isrRunning = false; +void TwoButton::isrPrimary() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButton *b = TwoButton::getInstance(); - if (b->buttons[0].state == State::REST) { - b->buttons[0].state = State::IRQ; - b->buttons[0].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButton *b = TwoButton::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } // Handle the start of a press to the secondary button // Wakes our button thread -void TwoButton::isrSecondary() { - static volatile bool isrRunning = false; +void TwoButton::isrSecondary() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButton *b = TwoButton::getInstance(); - if (b->buttons[1].state == State::REST) { - b->buttons[1].state = State::IRQ; - b->buttons[1].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButton *b = TwoButton::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } // Concise method to start our button thread // Follows an ISR, listening for button release -void TwoButton::startThread() { - if (!OSThread::enabled) { - OSThread::setInterval(10); - OSThread::enabled = true; - } +void TwoButton::startThread() +{ + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } } // Concise method to stop our button thread // Called when we no longer need to poll for button release -void TwoButton::stopThread() { - if (OSThread::enabled) { - OSThread::disable(); - } +void TwoButton::stopThread() +{ + if (OSThread::enabled) { + OSThread::disable(); + } - // Reset both buttons manually - // Just in case an IRQ fires during the process of resetting the system - // Can occur with super rapid presses? - buttons[0].state = REST; - buttons[1].state = REST; + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; } // Our button thread // Started by an IRQ, on either button // Polls for button releases // Stops when both buttons released -int32_t TwoButton::runOnce() { - constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); +int32_t TwoButton::runOnce() +{ + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); - // Allow either button to request that our thread should continue polling - bool awaitingRelease = false; + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; - // Check both primary and secondary buttons - for (uint8_t i = 0; i < BUTTON_COUNT; i++) { - switch (buttons[i].state) { - // No action: button has not been pressed - case REST: - break; + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; - // New press detected by interrupt - case IRQ: - powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) - buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) - buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled - awaitingRelease = true; // Mark that polling-for-release should continue - break; + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; - // An existing press continues - // Not held long enough to register as longpress - case POLLING_UNFIRED: { - uint32_t length = millis() - buttons[i].irqAtMillis; + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; - // If button released since last thread tick, - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) - buttons[i].state = State::REST; // Mark that the button has reset - if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, - buttons[i].onShortPress(); // Run callback: short press - } + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onShortPress(); // Run callback: short press + } - // If button not yet released - else { - awaitingRelease = true; // Mark that polling-for-release should continue - if (length >= buttons[i].longpressLength) { - // Run callback: long press (once) - // Then continue waiting for release, to rearm - buttons[i].state = State::POLLING_FIRED; - buttons[i].onLongPress(); + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); + } + } + break; + } + + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; } - } - break; } - // Button still held, but duration long enough that longpress event already fired - // Just waiting for release - case POLLING_FIRED: - // Release detected - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].state = State::REST; - buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) - } - // Not yet released, keep polling - else - awaitingRelease = true; - break; - } - } + // If both buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); - // If both buttons are now released - // we don't need to waste cpu resources polling - // IRQ will restart this thread when we next need it - if (!awaitingRelease) - stopThread(); - - // Run this method again, or don't.. - // Use whatever behavior was previously set by stopThread() or startThread() - return OSThread::interval; + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int TwoButton::beforeLightSleep(void *unused) { - stop(); - return 0; // Indicates success +int TwoButton::beforeLightSleep(void *unused) +{ + stop(); + return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) { - start(); +int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + start(); - // Manually trigger the button-down ISR - // - during light sleep, our ISR is disabled - // - if light sleep ends by button press, pretend our own ISR caught it - // - need to manually confirm by reading pin ourselves, to avoid occasional false positives - // (false positive only when using internal pullup resistors?) - if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) - isrPrimary(); + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); - return 0; // Indicates success + return 0; // Indicates success } #endif diff --git a/src/graphics/niche/Inputs/TwoButton.h b/src/graphics/niche/Inputs/TwoButton.h index 89623d82c..ae66adf96 100644 --- a/src/graphics/niche/Inputs/TwoButton.h +++ b/src/graphics/niche/Inputs/TwoButton.h @@ -22,78 +22,81 @@ Interrupt driven #include "Observer.h" -namespace NicheGraphics::Inputs { +namespace NicheGraphics::Inputs +{ -class TwoButton : protected concurrency::OSThread { -public: - typedef std::function Callback; - - static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition - - static TwoButton *getInstance(); // Create or get the singleton instance - void start(); // Start handling button input - void stop(); // Stop handling button input (disconnect ISRs for sleep) - void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); - void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); - void setHandlerDown(uint8_t whichButton, Callback onDown); - void setHandlerUp(uint8_t whichButton, Callback onUp); - void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); - void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); - - // Disconnect and reconnect interrupts for light sleep -#ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); -#endif - -private: - // Internal state of a specific button - enum State { - REST, // Up, no activity - IRQ, // Down detected, not yet handled - POLLING_UNFIRED, // Down handled, polling for release - POLLING_FIRED, // Longpress fired, button still held - }; - - // Contains info about a specific button - // (Array of this struct below) - class Button { +class TwoButton : protected concurrency::OSThread +{ public: - // Per-button config - uint8_t pin = 0xFF; // 0xFF: unset - bool activeLogic = LOW; // Active LOW by default. Currently unimplemented. - uint32_t debounceLength = 50; // Minimum length for shortpress, in ms - uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms - volatile State state = State::REST; // Internal state - volatile uint32_t irqAtMillis; // millis() when button went down + typedef std::function Callback; - // Per-button event callbacks - static void noop(){}; - std::function onDown = noop; - std::function onUp = noop; - std::function onShortPress = noop; - std::function onLongPress = noop; - }; + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + static TwoButton *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + void stop(); // Stop handling button input (disconnect ISRs for sleep) + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); + void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = CallbackObserver(this, &TwoButton::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &TwoButton::afterLightSleep); + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif - int32_t runOnce() override; // Timer method. Polls for button release + private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; - void startThread(); // Start polling for release - void stopThread(); // Stop polling for release + // Contains info about a specific button + // (Array of this struct below) + class Button + { + public: + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + bool activeLogic = LOW; // Active LOW by default. Currently unimplemented. + uint32_t debounceLength = 50; // Minimum length for shortpress, in ms + uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down - static void isrPrimary(); // Detect start of press - static void isrSecondary(); // Detect start of press (optional aux button) + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onShortPress = noop; + std::function onLongPress = noop; + }; - TwoButton(); // Constructor made private: force use of Button::instance() +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = CallbackObserver(this, &TwoButton::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButton::afterLightSleep); +#endif - // Info about both buttons - Button buttons[2]; + int32_t runOnce() override; // Timer method. Polls for button release + + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release + + static void isrPrimary(); // Detect start of press + static void isrSecondary(); // Detect start of press (optional aux button) + + TwoButton(); // Constructor made private: force use of Button::instance() + + // Info about both buttons + Button buttons[2]; }; }; // namespace NicheGraphics::Inputs diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp index ac541e383..287fb943f 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.cpp +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -8,481 +8,514 @@ using namespace NicheGraphics::Inputs; -TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") { - // Don't start polling buttons for release immediately - // Assume they are in a "released" state at boot - OSThread::disable(); +TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") +{ + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); #endif - // Explicitly initialize these, just to keep cppcheck quiet.. - buttons[0] = Button(); - buttons[1] = Button(); - joystick[Direction::UP] = SimpleButton(); - joystick[Direction::DOWN] = SimpleButton(); - joystick[Direction::LEFT] = SimpleButton(); - joystick[Direction::RIGHT] = SimpleButton(); + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); + joystick[Direction::UP] = SimpleButton(); + joystick[Direction::DOWN] = SimpleButton(); + joystick[Direction::LEFT] = SimpleButton(); + joystick[Direction::RIGHT] = SimpleButton(); } // Get access to (or create) the singleton instance of this class // Accessible inside the ISRs, even though we maybe shouldn't -TwoButtonExtended *TwoButtonExtended::getInstance() { - // Instantiate the class the first time this method is called - static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; +TwoButtonExtended *TwoButtonExtended::getInstance() +{ + // Instantiate the class the first time this method is called + static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; - return singletonInstance; + return singletonInstance; } // Begin receiving button input // We probably need to do this after sleep, as well as at boot -void TwoButtonExtended::start() { - if (buttons[0].pin != 0xFF) - attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); +void TwoButtonExtended::start() +{ + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); - if (buttons[1].pin != 0xFF) - attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); - if (joystick[Direction::UP].pin != 0xFF) - attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::UP].pin != 0xFF) + attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, + joystickActiveLogic == LOW ? FALLING : RISING); - if (joystick[Direction::DOWN].pin != 0xFF) - attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::DOWN].pin != 0xFF) + attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, + joystickActiveLogic == LOW ? FALLING : RISING); - if (joystick[Direction::LEFT].pin != 0xFF) - attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::LEFT].pin != 0xFF) + attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, + joystickActiveLogic == LOW ? FALLING : RISING); - if (joystick[Direction::RIGHT].pin != 0xFF) - attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, joystickActiveLogic == LOW ? FALLING : RISING); + if (joystick[Direction::RIGHT].pin != 0xFF) + attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, + joystickActiveLogic == LOW ? FALLING : RISING); } // Stop receiving button input, and run custom sleep code // Called before device sleeps. This might be power-off, or just ESP32 light sleep // Some devices will want to attach interrupts here, for the user button to wake from sleep -void TwoButtonExtended::stop() { - if (buttons[0].pin != 0xFF) - detachInterrupt(buttons[0].pin); +void TwoButtonExtended::stop() +{ + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); - if (buttons[1].pin != 0xFF) - detachInterrupt(buttons[1].pin); + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); - if (joystick[Direction::UP].pin != 0xFF) - detachInterrupt(joystick[Direction::UP].pin); + if (joystick[Direction::UP].pin != 0xFF) + detachInterrupt(joystick[Direction::UP].pin); - if (joystick[Direction::DOWN].pin != 0xFF) - detachInterrupt(joystick[Direction::DOWN].pin); + if (joystick[Direction::DOWN].pin != 0xFF) + detachInterrupt(joystick[Direction::DOWN].pin); - if (joystick[Direction::LEFT].pin != 0xFF) - detachInterrupt(joystick[Direction::LEFT].pin); + if (joystick[Direction::LEFT].pin != 0xFF) + detachInterrupt(joystick[Direction::LEFT].pin); - if (joystick[Direction::RIGHT].pin != 0xFF) - detachInterrupt(joystick[Direction::RIGHT].pin); + if (joystick[Direction::RIGHT].pin != 0xFF) + detachInterrupt(joystick[Direction::RIGHT].pin); } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method. -uint8_t TwoButtonExtended::getUserButtonPin() { - uint8_t pin = 0xFF; // Unset +uint8_t TwoButtonExtended::getUserButtonPin() +{ + uint8_t pin = 0xFF; // Unset - // Use default pin for variant, if no better source + // Use default pin for variant, if no better source #ifdef BUTTON_PIN - pin = BUTTON_PIN; + pin = BUTTON_PIN; #endif - // From userPrefs.jsonc, if set + // From userPrefs.jsonc, if set #ifdef USERPREFS_BUTTON_PIN - pin = USERPREFS_BUTTON_PIN; + pin = USERPREFS_BUTTON_PIN; #endif - // From user's override in device settings, if set - if (config.device.button_gpio) - pin = config.device.button_gpio; + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; - return pin; + return pin; } // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp -void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { - // Prevent the same GPIO being assigned to multiple buttons - // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button - for (uint8_t i = 0; i < whichButton; i++) { - if (buttons[i].pin == pin) { - LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); - return; +void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) +{ + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; + } } - } - assert(whichButton < 2); - buttons[whichButton].pin = pin; - buttons[whichButton].activeLogic = LOW; + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; - pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } // Configures the wiring and logic of the joystick buttons // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp -void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) { - if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || - joystick[Direction::RIGHT].pin == rPin) { - LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); - return; - } +void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) +{ + if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || + joystick[Direction::RIGHT].pin == rPin) { + LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); + return; + } - joystick[Direction::UP].pin = uPin; - joystick[Direction::DOWN].pin = dPin; - joystick[Direction::LEFT].pin = lPin; - joystick[Direction::RIGHT].pin = rPin; - joystickActiveLogic = LOW; + joystick[Direction::UP].pin = uPin; + joystick[Direction::DOWN].pin = dPin; + joystick[Direction::LEFT].pin = lPin; + joystick[Direction::RIGHT].pin = rPin; + joystickActiveLogic = LOW; - pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); - pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); - pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); - pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); } -void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { - assert(whichButton < 2); - buttons[whichButton].debounceLength = debounceMs; - buttons[whichButton].longpressLength = longpressMs; +void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) +{ + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; } -void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) { joystickDebounceLength = debounceMs; } +void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) +{ + joystickDebounceLength = debounceMs; +} // Set what should happen when a button becomes pressed // Use this to implement a "while held" behavior -void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) { - assert(whichButton < 2); - buttons[whichButton].onDown = onDown; +void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) +{ + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; } // Set what should happen when a button becomes unpressed // Use this to implement a "While held" behavior -void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) { - assert(whichButton < 2); - buttons[whichButton].onUp = onUp; +void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) +{ + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; } // Set what should happen when a "short press" event has occurred -void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) { - assert(whichButton < 2); - buttons[whichButton].onPress = onPress; +void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) +{ + assert(whichButton < 2); + buttons[whichButton].onPress = onPress; } // Set what should happen when a "long press" event has fired // Note: this will occur while the button is still held -void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) { - assert(whichButton < 2); - buttons[whichButton].onLongPress = onLongPress; +void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) +{ + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; } // Set what should happen when a joystick button becomes pressed // Use this to implement a "while held" behavior -void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) { - joystick[Direction::UP].onDown = uDown; - joystick[Direction::DOWN].onDown = dDown; - joystick[Direction::LEFT].onDown = lDown; - joystick[Direction::RIGHT].onDown = rDown; +void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) +{ + joystick[Direction::UP].onDown = uDown; + joystick[Direction::DOWN].onDown = dDown; + joystick[Direction::LEFT].onDown = lDown; + joystick[Direction::RIGHT].onDown = rDown; } // Set what should happen when a joystick button becomes unpressed // Use this to implement a "while held" behavior -void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) { - joystick[Direction::UP].onUp = uUp; - joystick[Direction::DOWN].onUp = dUp; - joystick[Direction::LEFT].onUp = lUp; - joystick[Direction::RIGHT].onUp = rUp; +void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) +{ + joystick[Direction::UP].onUp = uUp; + joystick[Direction::DOWN].onUp = dUp; + joystick[Direction::LEFT].onUp = lUp; + joystick[Direction::RIGHT].onUp = rUp; } // Set what should happen when a "press" event has fired // Note: this will occur while the joystick button is still held -void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) { - joystick[Direction::UP].onPress = uPress; - joystick[Direction::DOWN].onPress = dPress; - joystick[Direction::LEFT].onPress = lPress; - joystick[Direction::RIGHT].onPress = rPress; +void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) +{ + joystick[Direction::UP].onPress = uPress; + joystick[Direction::DOWN].onPress = dPress; + joystick[Direction::LEFT].onPress = lPress; + joystick[Direction::RIGHT].onPress = rPress; } // Handle the start of a press to the primary button // Wakes our button thread -void TwoButtonExtended::isrPrimary() { - static volatile bool isrRunning = false; +void TwoButtonExtended::isrPrimary() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->buttons[0].state == State::REST) { - b->buttons[0].state = State::IRQ; - b->buttons[0].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } // Handle the start of a press to the secondary button // Wakes our button thread -void TwoButtonExtended::isrSecondary() { - static volatile bool isrRunning = false; +void TwoButtonExtended::isrSecondary() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->buttons[1].state == State::REST) { - b->buttons[1].state = State::IRQ; - b->buttons[1].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } // Handle the start of a press to the joystick buttons // Also wakes our button thread -void TwoButtonExtended::isrJoystickUp() { - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickUp() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::UP].state == State::REST) { - b->joystick[Direction::UP].state = State::IRQ; - b->joystick[Direction::UP].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::UP].state == State::REST) { + b->joystick[Direction::UP].state = State::IRQ; + b->joystick[Direction::UP].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } -void TwoButtonExtended::isrJoystickDown() { - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickDown() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::DOWN].state == State::REST) { - b->joystick[Direction::DOWN].state = State::IRQ; - b->joystick[Direction::DOWN].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::DOWN].state == State::REST) { + b->joystick[Direction::DOWN].state = State::IRQ; + b->joystick[Direction::DOWN].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } -void TwoButtonExtended::isrJoystickLeft() { - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickLeft() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::LEFT].state == State::REST) { - b->joystick[Direction::LEFT].state = State::IRQ; - b->joystick[Direction::LEFT].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::LEFT].state == State::REST) { + b->joystick[Direction::LEFT].state = State::IRQ; + b->joystick[Direction::LEFT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } -void TwoButtonExtended::isrJoystickRight() { - static volatile bool isrRunning = false; +void TwoButtonExtended::isrJoystickRight() +{ + static volatile bool isrRunning = false; - if (!isrRunning) { - isrRunning = true; - TwoButtonExtended *b = TwoButtonExtended::getInstance(); - if (b->joystick[Direction::RIGHT].state == State::REST) { - b->joystick[Direction::RIGHT].state = State::IRQ; - b->joystick[Direction::RIGHT].irqAtMillis = millis(); - b->startThread(); + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::RIGHT].state == State::REST) { + b->joystick[Direction::RIGHT].state = State::IRQ; + b->joystick[Direction::RIGHT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; } - isrRunning = false; - } } // Concise method to start our button thread // Follows an ISR, listening for button release -void TwoButtonExtended::startThread() { - if (!OSThread::enabled) { - OSThread::setInterval(10); - OSThread::enabled = true; - } +void TwoButtonExtended::startThread() +{ + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } } // Concise method to stop our button thread // Called when we no longer need to poll for button release -void TwoButtonExtended::stopThread() { - if (OSThread::enabled) { - OSThread::disable(); - } +void TwoButtonExtended::stopThread() +{ + if (OSThread::enabled) { + OSThread::disable(); + } - // Reset both buttons manually - // Just in case an IRQ fires during the process of resetting the system - // Can occur with super rapid presses? - buttons[0].state = REST; - buttons[1].state = REST; - joystick[Direction::UP].state = REST; - joystick[Direction::DOWN].state = REST; - joystick[Direction::LEFT].state = REST; - joystick[Direction::RIGHT].state = REST; + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; + joystick[Direction::UP].state = REST; + joystick[Direction::DOWN].state = REST; + joystick[Direction::LEFT].state = REST; + joystick[Direction::RIGHT].state = REST; } // Our button thread // Started by an IRQ, on either button // Polls for button releases // Stops when both buttons released -int32_t TwoButtonExtended::runOnce() { - constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); - constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); +int32_t TwoButtonExtended::runOnce() +{ + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); + constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); - // Allow either button to request that our thread should continue polling - bool awaitingRelease = false; + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; - // Check both primary and secondary buttons - for (uint8_t i = 0; i < BUTTON_COUNT; i++) { - switch (buttons[i].state) { - // No action: button has not been pressed - case REST: - break; + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; - // New press detected by interrupt - case IRQ: - powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) - buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) - buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled - awaitingRelease = true; // Mark that polling-for-release should continue - break; + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; - // An existing press continues - // Not held long enough to register as longpress - case POLLING_UNFIRED: { - uint32_t length = millis() - buttons[i].irqAtMillis; + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; - // If button released since last thread tick, - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) - buttons[i].state = State::REST; // Mark that the button has reset - if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, - buttons[i].onPress(); // Run callback: press - } - // If button not yet released - else { - awaitingRelease = true; // Mark that polling-for-release should continue - if (length >= buttons[i].longpressLength) { - // Run callback: long press (once) - // Then continue waiting for release, to rearm - buttons[i].state = State::POLLING_FIRED; - buttons[i].onLongPress(); + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onPress(); // Run callback: press + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); + } + } + break; } - } - break; - } - // Button still held, but duration long enough that longpress event already fired - // Just waiting for release - case POLLING_FIRED: - // Release detected - if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { - buttons[i].state = State::REST; - buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) - } - // Not yet released, keep polling - else - awaitingRelease = true; - break; - } - } - - // Check all the joystick directions - for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { - switch (joystick[i].state) { - // No action: button has not been pressed - case REST: - break; - - // New press detected by interrupt - case IRQ: - powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) - joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) - joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled - awaitingRelease = true; // Mark that polling-for-release should continue - break; - - // An existing press continues - // Not held long enough to register as press - case POLLING_UNFIRED: { - uint32_t length = millis() - joystick[i].irqAtMillis; - - // If button released since last thread tick, - if (digitalRead(joystick[i].pin) != joystickActiveLogic) { - joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) - joystick[i].state = State::REST; // Mark that the button has reset - } - // If button not yet released - else { - awaitingRelease = true; // Mark that polling-for-release should continue - if (length >= joystickDebounceLength) { - // Run callback: long press (once) - // Then continue waiting for release, to rearm - joystick[i].state = State::POLLING_FIRED; - joystick[i].onPress(); + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; } - } - break; } - // Button still held after press - // Just waiting for release - case POLLING_FIRED: - // Release detected - if (digitalRead(joystick[i].pin) != joystickActiveLogic) { - joystick[i].state = State::REST; - joystick[i].onUp(); // Callback: release of hold - } - // Not yet released, keep polling - else - awaitingRelease = true; - break; + // Check all the joystick directions + for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { + switch (joystick[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) + joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as press + case POLLING_UNFIRED: { + uint32_t length = millis() - joystick[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) + joystick[i].state = State::REST; // Mark that the button has reset + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= joystickDebounceLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + joystick[i].state = State::POLLING_FIRED; + joystick[i].onPress(); + } + } + break; + } + + // Button still held after press + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].state = State::REST; + joystick[i].onUp(); // Callback: release of hold + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } } - } - // If all buttons are now released - // we don't need to waste cpu resources polling - // IRQ will restart this thread when we next need it - if (!awaitingRelease) - stopThread(); + // If all buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); - // Run this method again, or don't.. - // Use whatever behavior was previously set by stopThread() or startThread() - return OSThread::interval; + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int TwoButtonExtended::beforeLightSleep(void *unused) { - stop(); - return 0; // Indicates success +int TwoButtonExtended::beforeLightSleep(void *unused) +{ + stop(); + return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) { - start(); +int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + start(); - // Manually trigger the button-down ISR - // - during light sleep, our ISR is disabled - // - if light sleep ends by button press, pretend our own ISR caught it - // - need to manually confirm by reading pin ourselves, to avoid occasional false positives - // (false positive only when using internal pullup resistors?) - if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) - isrPrimary(); + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); - return 0; // Indicates success + return 0; // Indicates success } #endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h index b47dbad92..23fd78a2a 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.h +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -30,100 +30,105 @@ Interrupt driven #include "Observer.h" -namespace NicheGraphics::Inputs { +namespace NicheGraphics::Inputs +{ -class TwoButtonExtended : protected concurrency::OSThread { -public: - typedef std::function Callback; +class TwoButtonExtended : protected concurrency::OSThread +{ + public: + typedef std::function Callback; - static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition - static TwoButtonExtended *getInstance(); // Create or get the singleton instance - void start(); // Start handling button input - void stop(); // Stop handling button input (disconnect ISRs for sleep) - void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); - void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); - void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); - void setJoystickDebounce(uint32_t debounceMs); - void setHandlerDown(uint8_t whichButton, Callback onDown); - void setHandlerUp(uint8_t whichButton, Callback onUp); - void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); - void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); - void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); - void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); - void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + static TwoButtonExtended *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + void stop(); // Stop handling button input (disconnect ISRs for sleep) + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); + void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); + void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setJoystickDebounce(uint32_t debounceMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); + void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); + void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); - // Disconnect and reconnect interrupts for light sleep + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif -private: - // Internal state of a specific button - enum State { - REST, // Up, no activity - IRQ, // Down detected, not yet handled - POLLING_UNFIRED, // Down handled, polling for release - POLLING_FIRED, // Longpress fired, button still held - }; + private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; - // Joystick Directions - enum Direction { UP = 0, DOWN, LEFT, RIGHT }; + // Joystick Directions + enum Direction { UP = 0, DOWN, LEFT, RIGHT }; - // Data used for direction (single-action) buttons - class SimpleButton { - public: - // Per-button config - uint8_t pin = 0xFF; // 0xFF: unset - volatile State state = State::REST; // Internal state - volatile uint32_t irqAtMillis; // millis() when button went down + // Data used for direction (single-action) buttons + class SimpleButton + { + public: + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down - // Per-button event callbacks - static void noop(){}; - std::function onDown = noop; - std::function onUp = noop; - std::function onPress = noop; - }; + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onPress = noop; + }; - // Data used for double-action buttons - class Button : public SimpleButton { - public: - // Per-button extended config - bool activeLogic = LOW; // Active LOW by default. - uint32_t debounceLength = 50; // Minimum length for shortpress in ms - uint32_t longpressLength = 500; // Time until longpress in ms + // Data used for double-action buttons + class Button : public SimpleButton + { + public: + // Per-button extended config + bool activeLogic = LOW; // Active LOW by default. + uint32_t debounceLength = 50; // Minimum length for shortpress in ms + uint32_t longpressLength = 500; // Time until longpress in ms - // Per-button event callbacks - std::function onLongPress = noop; - }; + // Per-button event callbacks + std::function onLongPress = noop; + }; #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &TwoButtonExtended::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButtonExtended::afterLightSleep); #endif - int32_t runOnce() override; // Timer method. Polls for button release + int32_t runOnce() override; // Timer method. Polls for button release - void startThread(); // Start polling for release - void stopThread(); // Stop polling for release + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release - static void isrPrimary(); // User Button ISR - static void isrSecondary(); // optional aux button or joystick center - static void isrJoystickUp(); - static void isrJoystickDown(); - static void isrJoystickLeft(); - static void isrJoystickRight(); + static void isrPrimary(); // User Button ISR + static void isrSecondary(); // optional aux button or joystick center + static void isrJoystickUp(); + static void isrJoystickDown(); + static void isrJoystickLeft(); + static void isrJoystickRight(); - TwoButtonExtended(); // Constructor made private: force use of Button::instance() + TwoButtonExtended(); // Constructor made private: force use of Button::instance() - // Info about both buttons - Button buttons[2]; - bool joystickActiveLogic = LOW; // Active LOW by default - uint32_t joystickDebounceLength = 50; // time until press in ms - SimpleButton joystick[4]; + // Info about both buttons + Button buttons[2]; + bool joystickActiveLogic = LOW; // Active LOW by default + uint32_t joystickDebounceLength = 50; // time until press in ms + SimpleButton joystick[4]; }; }; // namespace NicheGraphics::Inputs diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp index 21665ee59..50998930d 100644 --- a/src/graphics/niche/Utils/CannedMessageStore.cpp +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -12,142 +12,152 @@ using namespace NicheGraphics; // Location of the file which stores the canned messages on flash static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; -CannedMessageStore::CannedMessageStore() { +CannedMessageStore::CannedMessageStore() +{ #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe(adminModule); #endif - // Load & parse messages from flash - load(); + // Load & parse messages from flash + load(); } // Get access to (or create) the singleton instance of this class -CannedMessageStore *CannedMessageStore::getInstance() { - // Instantiate the class the first time this method is called - static CannedMessageStore *const singletonInstance = new CannedMessageStore; +CannedMessageStore *CannedMessageStore::getInstance() +{ + // Instantiate the class the first time this method is called + static CannedMessageStore *const singletonInstance = new CannedMessageStore; - return singletonInstance; + return singletonInstance; } // Access canned messages by index // Consumer should check CannedMessageStore::size to avoid accessing out of bounds -const std::string &CannedMessageStore::at(uint8_t i) { - assert(i < messages.size()); - return messages.at(i); +const std::string &CannedMessageStore::at(uint8_t i) +{ + assert(i < messages.size()); + return messages.at(i); } // Number of canned message strings available -uint8_t CannedMessageStore::size() { return messages.size(); } +uint8_t CannedMessageStore::size() +{ + return messages.size(); +} // Load canned message data from flash, and parse into the individual strings -void CannedMessageStore::load() { - // In case we're reloading - messages.clear(); +void CannedMessageStore::load() +{ + // In case we're reloading + messages.clear(); - // Attempt to load the bulk canned message data from flash - meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; - LoadFileResult result = - nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, sizeof(meshtastic_CannedMessageModuleConfig), - &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + // Attempt to load the bulk canned message data from flash + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, + sizeof(meshtastic_CannedMessageModuleConfig), + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); - // Abort if nothing to load - if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) - return; + // Abort if nothing to load + if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) + return; - // Split into individual canned messages - // These are concatenated when stored in flash, using '|' as a delimiter - std::string s; - for (char c : cannedMessageModuleConfig.messages) { // Character by character + // Split into individual canned messages + // These are concatenated when stored in flash, using '|' as a delimiter + std::string s; + for (char c : cannedMessageModuleConfig.messages) { // Character by character - // If found end of a string - if (c == '|' || c == '\0') { - // Copy into the vector (if non-empty) - if (!s.empty()) - messages.push_back(s); + // If found end of a string + if (c == '|' || c == '\0') { + // Copy into the vector (if non-empty) + if (!s.empty()) + messages.push_back(s); - // Reset the string builder - s.clear(); + // Reset the string builder + s.clear(); - // End of data, all strings processed - if (c == 0) - break; + // End of data, all strings processed + if (c == 0) + break; + } + + // Otherwise, append char (continue building string) + else + s.push_back(c); } - - // Otherwise, append char (continue building string) - else - s.push_back(c); - } } // Handle incoming admin messages // We get these as an observer of AdminModule // It's our responsibility to handle setting and getting of canned messages via the client API -// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for -// NicheGraphics -int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) { - switch (data->request->which_payload_variant) { +// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics +int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) +{ + switch (data->request->which_payload_variant) { - // Client API changing the canned messages - case meshtastic_AdminMessage_set_canned_message_module_messages_tag: - handleSet(data->request); - *data->result = AdminMessageHandleResult::HANDLED; - break; + // Client API changing the canned messages + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + handleSet(data->request); + *data->result = AdminMessageHandleResult::HANDLED; + break; - // Client API wants to know the current canned messages - case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: - handleGet(data->response); - *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; - break; + // Client API wants to know the current canned messages + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + handleGet(data->response); + *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; - default: - break; - } + default: + break; + } - return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } // Client API changing the canned messages -void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) { - // Copy into the correct struct (for writing to flash as protobuf) - meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; - strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, sizeof(cannedMessageModuleConfig.messages)); +void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) +{ + // Copy into the correct struct (for writing to flash as protobuf) + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, + sizeof(cannedMessageModuleConfig.messages)); - // Ensure the directory exists + // Ensure the directory exists #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - // Write to flash - nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg, - &cannedMessageModuleConfig); + // Write to flash + nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); - // Reload from flash, to update the canned messages in RAM - // (This is a lazy way to handle it) - load(); + // Reload from flash, to update the canned messages in RAM + // (This is a lazy way to handle it) + load(); } // Client API wants to know the current canned messages // We're reconstructing the monolithic canned message string from our copy of the messages in RAM // Lazy, but more convenient that reloading the monolithic string from flash just for this -void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) { - // Merge the canned messages back into the delimited format expected - std::string merged; - if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 - merged.reserve(201); - for (std::string &s : messages) { - merged += s; - merged += '|'; +void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) +{ + // Merge the canned messages back into the delimited format expected + std::string merged; + if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 + merged.reserve(201); + for (std::string &s : messages) { + merged += s; + merged += '|'; + } + merged.pop_back(); // Drop the final delimiter (loop added one too many) } - merged.pop_back(); // Drop the final delimiter (loop added one too many) - } - // Place the data into the response - // This response is scoped to AdminModule::handleReceivedProtobuf - // We were passed reference to it via the observable - response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; - strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); + // Place the data into the response + // This response is scoped to AdminModule::handleReceivedProtobuf + // We were passed reference to it via the observable + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); } #endif \ No newline at end of file diff --git a/src/graphics/niche/Utils/CannedMessageStore.h b/src/graphics/niche/Utils/CannedMessageStore.h index f6715bb3f..c00e1cf5c 100644 --- a/src/graphics/niche/Utils/CannedMessageStore.h +++ b/src/graphics/niche/Utils/CannedMessageStore.h @@ -22,29 +22,31 @@ The necessary interaction with the AdminModule is done as an observer. #include "modules/AdminModule.h" -namespace NicheGraphics { +namespace NicheGraphics +{ -class CannedMessageStore { -public: - static CannedMessageStore *getInstance(); // Create or get the singleton instance - const std::string &at(uint8_t i); // Get canned message at index - uint8_t size(); // Get total number of canned messages +class CannedMessageStore +{ + public: + static CannedMessageStore *getInstance(); // Create or get the singleton instance + const std::string &at(uint8_t i); // Get canned message at index + uint8_t size(); // Get total number of canned messages - int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages -private: - CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() + private: + CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() - void load(); // Load from flash, and parse + void load(); // Load from flash, and parse - void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages - void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages + void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages + void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages - std::vector messages; + std::vector messages; - // Get notified of incoming admin messages, to get / set canned messages - CallbackObserver adminMessageObserver = - CallbackObserver(this, &CannedMessageStore::onAdminMessage); + // Get notified of incoming admin messages, to get / set canned messages + CallbackObserver adminMessageObserver = + CallbackObserver(this, &CannedMessageStore::onAdminMessage); }; }; // namespace NicheGraphics diff --git a/src/graphics/niche/Utils/FlashData.h b/src/graphics/niche/Utils/FlashData.h index 77fc3d89f..233d0922e 100644 --- a/src/graphics/niche/Utils/FlashData.h +++ b/src/graphics/niche/Utils/FlashData.h @@ -16,149 +16,156 @@ Avoid bloating everyone's protobuf code for our one-off UI implementations #include "SPILock.h" #include "SafeFile.h" -namespace NicheGraphics { +namespace NicheGraphics +{ -template class FlashData { -private: - static std::string getFilename(const char *label) { - std::string filename; - filename += "/NicheGraphics"; - filename += "/"; - filename += label; - filename += ".data"; +template class FlashData +{ + private: + static std::string getFilename(const char *label) + { + std::string filename; + filename += "/NicheGraphics"; + filename += "/"; + filename += label; + filename += ".data"; - return filename; - } + return filename; + } - static uint32_t getHash(T *data) { - uint32_t hash = 0; + static uint32_t getHash(T *data) + { + uint32_t hash = 0; - // Sum all bytes of the image buffer together - for (uint32_t i = 0; i < sizeof(T); i++) - hash ^= ((uint8_t *)data)[i] + 1; + // Sum all bytes of the image buffer together + for (uint32_t i = 0; i < sizeof(T); i++) + hash ^= ((uint8_t *)data)[i] + 1; - return hash; - } + return hash; + } -public: - static bool load(T *data, const char *label) { - // Take firmware's SPI lock - concurrency::LockGuard guard(spiLock); + public: + static bool load(T *data, const char *label) + { + // Take firmware's SPI lock + concurrency::LockGuard guard(spiLock); - // Set false if we run into issues - bool okay = true; + // Set false if we run into issues + bool okay = true; - // Get a filename based on the label - std::string filename = getFilename(label); + // Get a filename based on the label + std::string filename = getFilename(label); #ifdef FSCom - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_WARN("'%s' not found. Using default values", filename.c_str()); - okay = false; - return okay; - } + // Check that the file *does* actually exist + if (!FSCom.exists(filename.c_str())) { + LOG_WARN("'%s' not found. Using default values", filename.c_str()); + okay = false; + return okay; + } - // Open the file - auto f = FSCom.open(filename.c_str(), FILE_O_READ); + // Open the file + auto f = FSCom.open(filename.c_str(), FILE_O_READ); - // If opened, start reading - if (f) { - LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str()); + // If opened, start reading + if (f) { + LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str()); - // Create an object which will received data from flash - // We read here first, so we can verify the checksum, without committing to overwriting the *data object - // Allows us to retain any defaults that might be set after we declared *data, but before loading settings, - // in case the flash values are corrupt - T flashData; + // Create an object which will received data from flash + // We read here first, so we can verify the checksum, without committing to overwriting the *data object + // Allows us to retain any defaults that might be set after we declared *data, but before loading settings, + // in case the flash values are corrupt + T flashData; - // Read the actual data - f.readBytes((char *)&flashData, sizeof(T)); + // Read the actual data + f.readBytes((char *)&flashData, sizeof(T)); - // Read the hash - uint32_t savedHash = 0; - f.readBytes((char *)&savedHash, sizeof(savedHash)); + // Read the hash + uint32_t savedHash = 0; + f.readBytes((char *)&savedHash, sizeof(savedHash)); - // Calculate hash of the loaded data, then compare with the saved hash - // If hash looks good, copy the values to the main data object - uint32_t calculatedHash = getHash(&flashData); - if (savedHash != calculatedHash) { - LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str()); + // Calculate hash of the loaded data, then compare with the saved hash + // If hash looks good, copy the values to the main data object + uint32_t calculatedHash = getHash(&flashData); + if (savedHash != calculatedHash) { + LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str()); + okay = false; + } else + *data = flashData; + + f.close(); + } else { + LOG_ERROR("Could not open / read %s", filename.c_str()); + okay = false; + } +#else + LOG_ERROR("Filesystem not implemented"); + state = LoadFileState::NO_FILESYSTEM; okay = false; - } else - *data = flashData; - - f.close(); - } else { - LOG_ERROR("Could not open / read %s", filename.c_str()); - okay = false; - } -#else - LOG_ERROR("Filesystem not implemented"); - state = LoadFileState::NO_FILESYSTEM; - okay = false; #endif - return okay; - } + return okay; + } - // Save module's custom data (settings?) to flash. Doesn't use protobufs - // Takes the firmware's SPI lock, in case the files are stored on SD card - // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally. - static void save(T *data, const char *label) { - // Get a filename based on the label - std::string filename = getFilename(label); + // Save module's custom data (settings?) to flash. Doesn't use protobufs + // Takes the firmware's SPI lock, in case the files are stored on SD card + // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally. + static void save(T *data, const char *label) + { + // Get a filename based on the label + std::string filename = getFilename(label); #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/NicheGraphics"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/NicheGraphics"); + spiLock->unlock(); - auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. + auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. - LOG_INFO("Saving %s", filename.c_str()); + LOG_INFO("Saving %s", filename.c_str()); - // Calculate a hash of the data - uint32_t hash = getHash(data); + // Calculate a hash of the data + uint32_t hash = getHash(data); - spiLock->lock(); - f.write((uint8_t *)data, sizeof(T)); // Write the actual data - f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash - spiLock->unlock(); + spiLock->lock(); + f.write((uint8_t *)data, sizeof(T)); // Write the actual data + f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash + spiLock->unlock(); - bool writeSucceeded = f.close(); + bool writeSucceeded = f.close(); - if (!writeSucceeded) { - LOG_ERROR("Can't write data!"); - } + if (!writeSucceeded) { + LOG_ERROR("Can't write data!"); + } #else - LOG_ERROR("ERROR: Filesystem not implemented\n"); + LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif - } + } }; // Erase contents of the NicheGraphics data directory -inline void clearFlashData() { +inline void clearFlashData() +{ - // Take firmware's SPI lock, in case the files are stored on SD card - concurrency::LockGuard guard(spiLock); + // Take firmware's SPI lock, in case the files are stored on SD card + concurrency::LockGuard guard(spiLock); #ifdef FSCom - File dir = FSCom.open("/NicheGraphics"); // Open the directory - File file = dir.openNextFile(); // Attempt to open the first file in the directory + File dir = FSCom.open("/NicheGraphics"); // Open the directory + File file = dir.openNextFile(); // Attempt to open the first file in the directory - // While the directory still contains files - while (file) { - std::string path = "/NicheGraphics/"; - path += file.name(); - LOG_DEBUG("Erasing %s", path.c_str()); - file.close(); - FSCom.remove(path.c_str()); + // While the directory still contains files + while (file) { + std::string path = "/NicheGraphics/"; + path += file.name(); + LOG_DEBUG("Erasing %s", path.c_str()); + file.close(); + FSCom.remove(path.c_str()); - file = dir.openNextFile(); - } + file = dir.openNextFile(); + } #else - LOG_ERROR("ERROR: Filesystem not implemented\n"); + LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index c33de3db0..5654fa02a 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -18,110 +18,120 @@ DeviceScreen *deviceScreen = nullptr; #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep -CallbackObserver tftSleepObserver = CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); +CallbackObserver tftSleepObserver = + CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); CallbackObserver endSleepObserver = CallbackObserver(deviceScreen, &DeviceScreen::wakeUp); #endif -void tft_task_handler(void *param = nullptr) { - while (true) { - spiLock->lock(); - deviceScreen->task_handler(); - spiLock->unlock(); - deviceScreen->sleep(); - } +void tft_task_handler(void *param = nullptr) +{ + while (true) { + spiLock->lock(); + deviceScreen->task_handler(); + spiLock->unlock(); + deviceScreen->sleep(); + } } -void tftSetup(void) { +void tftSetup(void) +{ #ifndef ARCH_PORTDUINO - deviceScreen = &DeviceScreen::create(); - PacketAPI::create(PacketServer::init()); - deviceScreen->init(new PacketClient); -#else - if (portduino_config.displayPanel != no_screen) { - DisplayDriverConfig displayConfig; - static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; - static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; -#if defined(USE_X11) - if (portduino_config.displayPanel == x11) { - if (portduino_config.displayWidth && portduino_config.displayHeight) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, - (uint16_t)portduino_config.displayHeight); - else - displayConfig.device(DisplayDriverConfig::device_t::X11); - } else -#elif defined(USE_FRAMEBUFFER) - if (portduino_config.displayPanel == fb) { - if (portduino_config.displayWidth && portduino_config.displayHeight) - displayConfig = - DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, (uint16_t)portduino_config.displayHeight); - else - displayConfig.device(DisplayDriverConfig::device_t::FB); - } else -#endif - { - displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) - .panel( - DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], - .panel_width = (uint16_t)portduino_config.displayWidth, - .panel_height = (uint16_t)portduino_config.displayHeight, - .rotation = (bool)portduino_config.displayRotate, - .pin_cs = (int16_t)portduino_config.displayCS.pin, - .pin_rst = (int16_t)portduino_config.displayReset.pin, - .offset_x = (uint16_t)portduino_config.displayOffsetX, - .offset_y = (uint16_t)portduino_config.displayOffsetY, - .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, - .invert = portduino_config.displayInvert ? true : false, - .rgb_order = (bool)portduino_config.displayRGBOrder, - .dlen_16bit = portduino_config.displayPanel == ili9486 || portduino_config.displayPanel == ili9488}) - .bus(DisplayDriverConfig::bus_config_t{ - .freq_write = (uint32_t)portduino_config.displayBusFrequency, - .freq_read = 16000000, - .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, .use_lock = true, .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) - .input( - DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, .pointerDevice = portduino_config.pointerDevice}) - .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, - .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, - .invert = (bool)portduino_config.displayBacklightInvert}); - if (portduino_config.touchscreenI2CAddr == -1) { - displayConfig.touch(DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], - .freq = (uint32_t)portduino_config.touchscreenBusFrequency, - .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, - .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, - .spi{ - .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, - }, - .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); - } else { - displayConfig.touch(DisplayDriverConfig::touch_config_t{ - .type = touch[portduino_config.touchscreenModule], - .freq = (uint32_t)portduino_config.touchscreenBusFrequency, - .x_min = 0, - .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth : portduino_config.displayHeight) - 1), - .y_min = 0, - .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight : portduino_config.displayWidth) - 1), - .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, - .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, - .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); - } - } - deviceScreen = &DeviceScreen::create(&displayConfig); + deviceScreen = &DeviceScreen::create(); PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); - } else { - LOG_INFO("Running without TFT display!"); - } +#else + if (portduino_config.displayPanel != no_screen) { + DisplayDriverConfig displayConfig; + static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", + "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; + static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; +#if defined(USE_X11) + if (portduino_config.displayPanel == x11) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); + else + displayConfig.device(DisplayDriverConfig::device_t::X11); + } else +#elif defined(USE_FRAMEBUFFER) + if (portduino_config.displayPanel == fb) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); + else + displayConfig.device(DisplayDriverConfig::device_t::FB); + } else +#endif + { + displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) + .panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], + .panel_width = (uint16_t)portduino_config.displayWidth, + .panel_height = (uint16_t)portduino_config.displayHeight, + .rotation = (bool)portduino_config.displayRotate, + .pin_cs = (int16_t)portduino_config.displayCS.pin, + .pin_rst = (int16_t)portduino_config.displayReset.pin, + .offset_x = (uint16_t)portduino_config.displayOffsetX, + .offset_y = (uint16_t)portduino_config.displayOffsetY, + .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, + .invert = portduino_config.displayInvert ? true : false, + .rgb_order = (bool)portduino_config.displayRGBOrder, + .dlen_16bit = portduino_config.displayPanel == ili9486 || + portduino_config.displayPanel == ili9488}) + .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency, + .freq_read = 16000000, + .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, + .use_lock = true, + .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) + .input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, + .pointerDevice = portduino_config.pointerDevice}) + .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, + .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, + .invert = (bool)portduino_config.displayBacklightInvert}); + if (portduino_config.touchscreenI2CAddr == -1) { + displayConfig.touch( + DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, + .spi{ + .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, + }, + .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); + } else { + displayConfig.touch(DisplayDriverConfig::touch_config_t{ + .type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, + .x_min = 0, + .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth + : portduino_config.displayHeight) - + 1), + .y_min = 0, + .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight + : portduino_config.displayWidth) - + 1), + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, + .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); + } + } + deviceScreen = &DeviceScreen::create(&displayConfig); + PacketAPI::create(PacketServer::init()); + deviceScreen->init(new PacketClient); + } else { + LOG_INFO("Running without TFT display!"); + } #endif - if (deviceScreen) { + if (deviceScreen) { #ifdef ARCH_ESP32 - tftSleepObserver.observe(¬ifyLightSleep); - endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + tftSleepObserver.observe(¬ifyLightSleep); + endSleepObserver.observe(¬ifyLightSleepEnd); + xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) - std::thread *tft_task = new std::thread([] { tft_task_handler(); }); + std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif - } + } } #endif \ No newline at end of file diff --git a/src/input/BBQ10Keyboard.cpp b/src/input/BBQ10Keyboard.cpp index c717b3866..8f8399aef 100644 --- a/src/input/BBQ10Keyboard.cpp +++ b/src/input/BBQ10Keyboard.cpp @@ -37,119 +37,145 @@ BBQ10Keyboard::BBQ10Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) {} -void BBQ10Keyboard::begin(uint8_t addr, TwoWire *wire) { - m_addr = addr; - m_wire = wire; +void BBQ10Keyboard::begin(uint8_t addr, TwoWire *wire) +{ + m_addr = addr; + m_wire = wire; - m_wire->begin(); + m_wire->begin(); - reset(); + reset(); } -void BBQ10Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { - m_addr = addr; - m_wire = nullptr; - writeCallback = w; - readCallback = r; - reset(); +void BBQ10Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) +{ + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); } -void BBQ10Keyboard::reset() { - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(_REG_RST); - m_wire->endTransmission(); - } - if (writeCallback) { - uint8_t data = 0; - writeCallback(m_addr, _REG_RST, &data, 0); - } - delay(100); - writeRegister(_REG_CFG, readRegister8(_REG_CFG) | CFG_REPORT_MODS); - delay(100); +void BBQ10Keyboard::reset() +{ + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(_REG_RST); + m_wire->endTransmission(); + } + if (writeCallback) { + uint8_t data = 0; + writeCallback(m_addr, _REG_RST, &data, 0); + } + delay(100); + writeRegister(_REG_CFG, readRegister8(_REG_CFG) | CFG_REPORT_MODS); + delay(100); } -void BBQ10Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const { - pinMode(pin, INPUT_PULLUP); - ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); +void BBQ10Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const +{ + pinMode(pin, INPUT_PULLUP); + ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); } -void BBQ10Keyboard::detachInterrupt(uint8_t pin) const { ::detachInterrupt(pin); } +void BBQ10Keyboard::detachInterrupt(uint8_t pin) const +{ + ::detachInterrupt(pin); +} -void BBQ10Keyboard::clearInterruptStatus() { writeRegister(_REG_INT, 0x00); } +void BBQ10Keyboard::clearInterruptStatus() +{ + writeRegister(_REG_INT, 0x00); +} -uint8_t BBQ10Keyboard::status() const { return readRegister8(_REG_KEY); } +uint8_t BBQ10Keyboard::status() const +{ + return readRegister8(_REG_KEY); +} -uint8_t BBQ10Keyboard::keyCount() const { return status() & KEY_COUNT_MASK; } +uint8_t BBQ10Keyboard::keyCount() const +{ + return status() & KEY_COUNT_MASK; +} -BBQ10Keyboard::KeyEvent BBQ10Keyboard::keyEvent() const { - KeyEvent event = {.key = '\0', .state = StateIdle}; +BBQ10Keyboard::KeyEvent BBQ10Keyboard::keyEvent() const +{ + KeyEvent event = {.key = '\0', .state = StateIdle}; + + if (keyCount() == 0) + return event; + + const uint16_t buf = readRegister16(_REG_FIF); + event.key = buf >> 8; + event.state = KeyState(buf & 0xFF); - if (keyCount() == 0) return event; - - const uint16_t buf = readRegister16(_REG_FIF); - event.key = buf >> 8; - event.state = KeyState(buf & 0xFF); - - return event; } -float BBQ10Keyboard::backlight() const { return readRegister8(_REG_BKL) / 255.0f; } - -void BBQ10Keyboard::setBacklight(float value) { writeRegister(_REG_BKL, value * 255); } - -uint8_t BBQ10Keyboard::readRegister8(uint8_t reg) const { - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); - - m_wire->requestFrom(m_addr, (uint8_t)1); - if (m_wire->available() < 1) - return 0; - - return m_wire->read(); - } - if (readCallback) { - uint8_t data; - readCallback(m_addr, reg, &data, 1); - return data; - } - return 0; +float BBQ10Keyboard::backlight() const +{ + return readRegister8(_REG_BKL) / 255.0f; } -uint16_t BBQ10Keyboard::readRegister16(uint8_t reg) const { - uint8_t data[2] = {0}; - // uint8_t low = 0, high = 0; - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); - - m_wire->requestFrom(m_addr, (uint8_t)2); - if (m_wire->available() < 2) - return 0; - data[0] = m_wire->read(); - data[1] = m_wire->read(); - } - if (readCallback) { - readCallback(m_addr, reg, data, 2); - } - return (data[1] << 8) | data[0]; +void BBQ10Keyboard::setBacklight(float value) +{ + writeRegister(_REG_BKL, value * 255); } -void BBQ10Keyboard::writeRegister(uint8_t reg, uint8_t value) { - uint8_t data[2]; - data[0] = reg | _WRITE_MASK; - data[1] = value; +uint8_t BBQ10Keyboard::readRegister8(uint8_t reg) const +{ + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(data, sizeof(uint8_t) * 2); - m_wire->endTransmission(); - } - if (writeCallback) { - writeCallback(m_addr, data[0], &(data[1]), 1); - } + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) + return 0; + + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; +} + +uint16_t BBQ10Keyboard::readRegister16(uint8_t reg) const +{ + uint8_t data[2] = {0}; + // uint8_t low = 0, high = 0; + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); + + m_wire->requestFrom(m_addr, (uint8_t)2); + if (m_wire->available() < 2) + return 0; + data[0] = m_wire->read(); + data[1] = m_wire->read(); + } + if (readCallback) { + readCallback(m_addr, reg, data, 2); + } + return (data[1] << 8) | data[0]; +} + +void BBQ10Keyboard::writeRegister(uint8_t reg, uint8_t value) +{ + uint8_t data[2]; + data[0] = reg | _WRITE_MASK; + data[1] = value; + + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(data, sizeof(uint8_t) * 2); + m_wire->endTransmission(); + } + if (writeCallback) { + writeCallback(m_addr, data[0], &(data[1]), 1); + } } diff --git a/src/input/BBQ10Keyboard.h b/src/input/BBQ10Keyboard.h index c99a61299..07d02308f 100644 --- a/src/input/BBQ10Keyboard.h +++ b/src/input/BBQ10Keyboard.h @@ -8,43 +8,44 @@ #define KEY_MOD_SHR (0x1C) #define KEY_MOD_SYM (0x1D) -class BBQ10Keyboard { -public: - typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); +class BBQ10Keyboard +{ + public: + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); - enum KeyState { StateIdle = 0, StatePress, StateLongPress, StateRelease }; + enum KeyState { StateIdle = 0, StatePress, StateLongPress, StateRelease }; - struct KeyEvent { - char key; - KeyState state; - }; + struct KeyEvent { + char key; + KeyState state; + }; - BBQ10Keyboard(); + BBQ10Keyboard(); - void begin(uint8_t addr = BBQ10_KB_ADDR, TwoWire *wire = &Wire); + void begin(uint8_t addr = BBQ10_KB_ADDR, TwoWire *wire = &Wire); - void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = BBQ10_KB_ADDR); + void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = BBQ10_KB_ADDR); - void reset(void); + void reset(void); - void attachInterrupt(uint8_t pin, void (*func)(void)) const; - void detachInterrupt(uint8_t pin) const; - void clearInterruptStatus(void); + void attachInterrupt(uint8_t pin, void (*func)(void)) const; + void detachInterrupt(uint8_t pin) const; + void clearInterruptStatus(void); - uint8_t status(void) const; - uint8_t keyCount(void) const; - KeyEvent keyEvent(void) const; + uint8_t status(void) const; + uint8_t keyCount(void) const; + KeyEvent keyEvent(void) const; - float backlight() const; - void setBacklight(float value); + float backlight() const; + void setBacklight(float value); - uint8_t readRegister8(uint8_t reg) const; - uint16_t readRegister16(uint8_t reg) const; - void writeRegister(uint8_t reg, uint8_t value); + uint8_t readRegister8(uint8_t reg) const; + uint16_t readRegister16(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); -private: - TwoWire *m_wire; - uint8_t m_addr; - i2c_com_fptr_t readCallback; - i2c_com_fptr_t writeCallback; + private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; }; diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 200b84118..9f53b06f4 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -22,291 +22,307 @@ using namespace concurrency; #if HAS_BUTTON #endif -ButtonThread::ButtonThread(const char *name) : OSThread(name) { _originName = name; } - -bool ButtonThread::initButton(const ButtonConfig &config) { - if (inputBroker) - inputBroker->registerSource(this); - _longPressTime = config.longPressTime; - _longLongPressTime = config.longLongPressTime; - _pinNum = config.pinNumber; - _activeLow = config.activeLow; - _touchQuirk = config.touchQuirk; - _intRoutine = config.intRoutine; - _longLongPress = config.longLongPress; - - userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); - - if (config.pullupSense != 0) { - pinMode(config.pinNumber, config.pullupSense); - } - - _singlePress = config.singlePress; - userButton.attachClick( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - thread->btnEvent = BUTTON_EVENT_PRESSED; - }, - this); - - _longPress = config.longPress; - userButton.attachLongPressStart( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - // if (millis() > 30000) // hold off 30s after boot - thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; - }, - this); - userButton.attachLongPressStop( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - // if (millis() > 30000) // hold off 30s after boot - thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; - }, - this); - - if (config.doublePress != INPUT_BROKER_NONE) { - _doublePress = config.doublePress; - userButton.attachDoubleClick( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; - }, - this); - } - - if (config.triplePress != INPUT_BROKER_NONE) { - _triplePress = config.triplePress; - userButton.attachMultiClick( - [](void *callerThread) -> void { - ButtonThread *thread = (ButtonThread *)callerThread; - thread->storeClickCount(); - thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; - }, - this); - } - if (config.shortLong != INPUT_BROKER_NONE) { - _shortLong = config.shortLong; - } -#ifdef USE_EINK - userButton.setDebounceMs(0); -#else - userButton.setDebounceMs(1); -#endif - userButton.setPressMs(_longPressTime); - - if (screen) { - userButton.setClickMs(20); - } else { - userButton.setClickMs(BUTTON_CLICK_MS); - } - attachButtonInterrupts(); -#ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); -#endif - return true; +ButtonThread::ButtonThread(const char *name) : OSThread(name) +{ + _originName = name; } -int32_t ButtonThread::runOnce() { - // If the button is pressed we suppress CPU sleep until release - canSleep = true; // Assume we should not keep the board awake +bool ButtonThread::initButton(const ButtonConfig &config) +{ + if (inputBroker) + inputBroker->registerSource(this); + _longPressTime = config.longPressTime; + _longLongPressTime = config.longLongPressTime; + _pinNum = config.pinNumber; + _activeLow = config.activeLow; + _touchQuirk = config.touchQuirk; + _intRoutine = config.intRoutine; + _longLongPress = config.longLongPress; - // Check for combination timeout - if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { - waitingForLongPress = false; - } + userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); - userButton.tick(); - canSleep &= userButton.isIdle(); - - // Check if we should play lead-up sound during long press - // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers - bool buttonCurrentlyPressed = isButtonPressed(_pinNum); - - // Detect start of button press - if (buttonCurrentlyPressed && !buttonWasPressed) { - buttonPressStartTime = millis(); - leadUpPlayed = false; - leadUpSequenceActive = false; - resetLeadUpSequence(); - } - - // Progressive lead-up sound system - if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { - - // Start the progressive sequence if not already active - if (!leadUpSequenceActive) { - leadUpSequenceActive = true; - lastLeadUpNoteTime = millis(); - playNextLeadUpNote(); // Play the first note immediately - } - // Continue playing notes at intervals - else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes - if (playNextLeadUpNote()) { - lastLeadUpNoteTime = millis(); - } else { - leadUpPlayed = true; - } - } - } - - // Reset when button is released - if (!buttonCurrentlyPressed && buttonWasPressed) { - leadUpSequenceActive = false; - resetLeadUpSequence(); - } - - buttonWasPressed = buttonCurrentlyPressed; - - // new behavior - if (btnEvent != BUTTON_EVENT_NONE) { - InputEvent evt; - evt.source = _originName; - evt.kbchar = 0; - evt.touchX = 0; - evt.touchY = 0; - switch (btnEvent) { - case BUTTON_EVENT_PRESSED: { - // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) - evt.inputEvent = _singlePress; - // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types - this->notifyObservers(&evt); - - // Start tracking for potential combination - waitingForLongPress = true; - shortPressTime = millis(); - - break; - } - case BUTTON_EVENT_LONG_PRESSED: { - // Ignore if: TX in progress - // Uncommon T-Echo hardware bug, LoRa TX triggers touch button - if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) - break; - - // Check if this is part of a short-press + long-press combination - if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { - evt.inputEvent = _shortLong; - // evt.kbchar = _shortLong; - this->notifyObservers(&evt); - // Play the combination tune - playComboTune(); - - break; - } - if (_longPress != INPUT_BROKER_NONE) { - // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) - evt.inputEvent = _longPress; - this->notifyObservers(&evt); - } - // Reset combination tracking - waitingForLongPress = false; - - break; + if (config.pullupSense != 0) { + pinMode(config.pinNumber, config.pullupSense); } - case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected - LOG_INFO("Double press!"); + _singlePress = config.singlePress; + userButton.attachClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_PRESSED; + }, + this); - // Reset combination tracking - waitingForLongPress = false; + _longPress = config.longPress; + userButton.attachLongPressStart( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + // if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; + }, + this); + userButton.attachLongPressStop( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + // if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; + }, + this); - evt.inputEvent = _doublePress; - // evt.kbchar = _doublePress; - this->notifyObservers(&evt); - playComboTune(); - - break; + if (config.doublePress != INPUT_BROKER_NONE) { + _doublePress = config.doublePress; + userButton.attachDoubleClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; + }, + this); } - case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present - LOG_INFO("Mulitipress! %hux", multipressClickCount); + if (config.triplePress != INPUT_BROKER_NONE) { + _triplePress = config.triplePress; + userButton.attachMultiClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->storeClickCount(); + thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; + }, + this); + } + if (config.shortLong != INPUT_BROKER_NONE) { + _shortLong = config.shortLong; + } +#ifdef USE_EINK + userButton.setDebounceMs(0); +#else + userButton.setDebounceMs(1); +#endif + userButton.setPressMs(_longPressTime); - // Reset combination tracking - waitingForLongPress = false; + if (screen) { + userButton.setClickMs(20); + } else { + userButton.setClickMs(BUTTON_CLICK_MS); + } + attachButtonInterrupts(); +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + return true; +} - switch (multipressClickCount) { - case 3: - evt.inputEvent = _triplePress; - // evt.kbchar = _triplePress; - this->notifyObservers(&evt); - playComboTune(); - break; +int32_t ButtonThread::runOnce() +{ + // If the button is pressed we suppress CPU sleep until release + canSleep = true; // Assume we should not keep the board awake - // No valid multipress action - default: - break; - } // end switch: click count - - break; - } // end multipress event - - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. - case BUTTON_EVENT_LONG_RELEASED: { - - LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); - if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { - evt.inputEvent = _longLongPress; - this->notifyObservers(&evt); - } - // Reset combination tracking - waitingForLongPress = false; - leadUpPlayed = false; - - break; + // Check for combination timeout + if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { + waitingForLongPress = false; } - // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG - default: { - break; - } - } - } - btnEvent = BUTTON_EVENT_NONE; + userButton.tick(); + canSleep &= userButton.isIdle(); - // only pull when the button is pressed, we get notified via IRQ on a new press - if (!userButton.isIdle() || waitingForLongPress) { - return 50; - } - return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here? + // Check if we should play lead-up sound during long press + // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers + bool buttonCurrentlyPressed = isButtonPressed(_pinNum); + + // Detect start of button press + if (buttonCurrentlyPressed && !buttonWasPressed) { + buttonPressStartTime = millis(); + leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + // Progressive lead-up sound system + if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { + + // Start the progressive sequence if not already active + if (!leadUpSequenceActive) { + leadUpSequenceActive = true; + lastLeadUpNoteTime = millis(); + playNextLeadUpNote(); // Play the first note immediately + } + // Continue playing notes at intervals + else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes + if (playNextLeadUpNote()) { + lastLeadUpNoteTime = millis(); + } else { + leadUpPlayed = true; + } + } + } + + // Reset when button is released + if (!buttonCurrentlyPressed && buttonWasPressed) { + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + buttonWasPressed = buttonCurrentlyPressed; + + // new behavior + if (btnEvent != BUTTON_EVENT_NONE) { + InputEvent evt; + evt.source = _originName; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + evt.inputEvent = _singlePress; + // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types + this->notifyObservers(&evt); + + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); + + break; + } + case BUTTON_EVENT_LONG_PRESSED: { + // Ignore if: TX in progress + // Uncommon T-Echo hardware bug, LoRa TX triggers touch button + if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) + break; + + // Check if this is part of a short-press + long-press combination + if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && + (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { + evt.inputEvent = _shortLong; + // evt.kbchar = _shortLong; + this->notifyObservers(&evt); + // Play the combination tune + playComboTune(); + + break; + } + if (_longPress != INPUT_BROKER_NONE) { + // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) + evt.inputEvent = _longPress; + this->notifyObservers(&evt); + } + // Reset combination tracking + waitingForLongPress = false; + + break; + } + + case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected + LOG_INFO("Double press!"); + + // Reset combination tracking + waitingForLongPress = false; + + evt.inputEvent = _doublePress; + // evt.kbchar = _doublePress; + this->notifyObservers(&evt); + playComboTune(); + + break; + } + + case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present + LOG_INFO("Mulitipress! %hux", multipressClickCount); + + // Reset combination tracking + waitingForLongPress = false; + + switch (multipressClickCount) { + case 3: + evt.inputEvent = _triplePress; + // evt.kbchar = _triplePress; + this->notifyObservers(&evt); + playComboTune(); + break; + + // No valid multipress action + default: + break; + } // end switch: click count + + break; + } // end multipress event + + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediatedly. + case BUTTON_EVENT_LONG_RELEASED: { + + LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); + if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && + (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { + evt.inputEvent = _longLongPress; + this->notifyObservers(&evt); + } + // Reset combination tracking + waitingForLongPress = false; + leadUpPlayed = false; + + break; + } + + // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG + default: { + break; + } + } + } + btnEvent = BUTTON_EVENT_NONE; + + // only pull when the button is pressed, we get notified via IRQ on a new press + if (!userButton.isIdle() || waitingForLongPress) { + return 50; + } + return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here? } /* * Attach (or re-attach) hardware interrupts for buttons * Public method. Used outside class when waking from MCU sleep */ -void ButtonThread::attachButtonInterrupts() { - // Interrupt for user button, during normal use. Improves responsiveness. - attachInterrupt(_pinNum, _intRoutine, CHANGE); +void ButtonThread::attachButtonInterrupts() +{ + // Interrupt for user button, during normal use. Improves responsiveness. + attachInterrupt(_pinNum, _intRoutine, CHANGE); } /* * Detach the "normal" button interrupts. * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep */ -void ButtonThread::detachButtonInterrupts() { detachInterrupt(_pinNum); } +void ButtonThread::detachButtonInterrupts() +{ + detachInterrupt(_pinNum); +} #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int ButtonThread::beforeLightSleep(void *unused) { - detachButtonInterrupts(); - return 0; // Indicates success +int ButtonThread::beforeLightSleep(void *unused) +{ + detachButtonInterrupts(); + return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) { - attachButtonInterrupts(); - return 0; // Indicates success +int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachButtonInterrupts(); + return 0; // Indicates success } #endif // Non-static method, runs during callback. Grabs info while still valid -void ButtonThread::storeClickCount() { multipressClickCount = userButton.getNumberClicks(); } \ No newline at end of file +void ButtonThread::storeClickCount() +{ + multipressClickCount = userButton.getNumberClicks(); +} \ No newline at end of file diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index 78d08dce1..7de38341c 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -8,23 +8,23 @@ typedef void (*voidFuncPtr)(void); struct ButtonConfig { - uint8_t pinNumber; - bool activeLow = true; - bool activePullup = true; - uint32_t pullupSense = 0; - voidFuncPtr intRoutine = nullptr; - input_broker_event singlePress = INPUT_BROKER_NONE; - input_broker_event longPress = INPUT_BROKER_NONE; - uint16_t longPressTime = 500; - input_broker_event doublePress = INPUT_BROKER_NONE; - input_broker_event longLongPress = INPUT_BROKER_NONE; - uint16_t longLongPressTime = 3900; - input_broker_event triplePress = INPUT_BROKER_NONE; - input_broker_event shortLong = INPUT_BROKER_NONE; - bool touchQuirk = false; + uint8_t pinNumber; + bool activeLow = true; + bool activePullup = true; + uint32_t pullupSense = 0; + voidFuncPtr intRoutine = nullptr; + input_broker_event singlePress = INPUT_BROKER_NONE; + input_broker_event longPress = INPUT_BROKER_NONE; + uint16_t longPressTime = 500; + input_broker_event doublePress = INPUT_BROKER_NONE; + input_broker_event longLongPress = INPUT_BROKER_NONE; + uint16_t longLongPressTime = 3900; + input_broker_event triplePress = INPUT_BROKER_NONE; + input_broker_event shortLong = INPUT_BROKER_NONE; + bool touchQuirk = false; - // Constructor to set required parameter - explicit ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {} + // Constructor to set required parameter + explicit ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {} }; #ifndef BUTTON_CLICK_MS @@ -43,86 +43,89 @@ struct ButtonConfig { #define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding #endif -class ButtonThread : public Observable, public concurrency::OSThread { -public: - const char *_originName; - static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot - bool initButton(const ButtonConfig &config); +class ButtonThread : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot + bool initButton(const ButtonConfig &config); - enum ButtonEventType { - BUTTON_EVENT_NONE, - BUTTON_EVENT_PRESSED, - BUTTON_EVENT_PRESSED_SCREEN, - BUTTON_EVENT_DOUBLE_PRESSED, - BUTTON_EVENT_MULTI_PRESSED, - BUTTON_EVENT_LONG_PRESSED, - BUTTON_EVENT_LONG_RELEASED, - BUTTON_EVENT_TOUCH_LONG_PRESSED, - BUTTON_EVENT_COMBO_SHORT_LONG, - }; + enum ButtonEventType { + BUTTON_EVENT_NONE, + BUTTON_EVENT_PRESSED, + BUTTON_EVENT_PRESSED_SCREEN, + BUTTON_EVENT_DOUBLE_PRESSED, + BUTTON_EVENT_MULTI_PRESSED, + BUTTON_EVENT_LONG_PRESSED, + BUTTON_EVENT_LONG_RELEASED, + BUTTON_EVENT_TOUCH_LONG_PRESSED, + BUTTON_EVENT_COMBO_SHORT_LONG, + }; - explicit ButtonThread(const char *name); - int32_t runOnce() override; - OneButton userButton; - void attachButtonInterrupts(); - void detachButtonInterrupts(); - void storeClickCount(); - bool isButtonPressed(int buttonPin) { - if (_activeLow) - return !digitalRead(buttonPin); // Active low: pressed = LOW - else - return digitalRead(buttonPin); // Most buttons are active low by default - } + explicit ButtonThread(const char *name); + int32_t runOnce() override; + OneButton userButton; + void attachButtonInterrupts(); + void detachButtonInterrupts(); + void storeClickCount(); + bool isButtonPressed(int buttonPin) + { + if (_activeLow) + return !digitalRead(buttonPin); // Active low: pressed = LOW + else + return digitalRead(buttonPin); // Most buttons are active low by default + } - // Returns true while this thread's button is physically held down - bool isHeld() { return isButtonPressed(_pinNum); } + // Returns true while this thread's button is physically held down + bool isHeld() { return isButtonPressed(_pinNum); } - // Disconnect and reconnect interrupts for light sleep + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif -private: - input_broker_event _singlePress = INPUT_BROKER_NONE; - input_broker_event _longPress = INPUT_BROKER_NONE; - input_broker_event _longLongPress = INPUT_BROKER_NONE; + private: + input_broker_event _singlePress = INPUT_BROKER_NONE; + input_broker_event _longPress = INPUT_BROKER_NONE; + input_broker_event _longLongPress = INPUT_BROKER_NONE; - input_broker_event _doublePress = INPUT_BROKER_NONE; - input_broker_event _triplePress = INPUT_BROKER_NONE; - input_broker_event _shortLong = INPUT_BROKER_NONE; + input_broker_event _doublePress = INPUT_BROKER_NONE; + input_broker_event _triplePress = INPUT_BROKER_NONE; + input_broker_event _shortLong = INPUT_BROKER_NONE; - voidFuncPtr _intRoutine = nullptr; - uint16_t _longPressTime = 500; - uint16_t _longLongPressTime = 3900; - int _pinNum = 0; - bool _activeLow = true; - bool _touchQuirk = false; + voidFuncPtr _intRoutine = nullptr; + uint16_t _longPressTime = 500; + uint16_t _longLongPressTime = 3900; + int _pinNum = 0; + bool _activeLow = true; + bool _touchQuirk = false; - uint32_t buttonPressStartTime = 0; - bool buttonWasPressed = false; + uint32_t buttonPressStartTime = 0; + bool buttonWasPressed = false; #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = CallbackObserver(this, &ButtonThread::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &ButtonThread::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &ButtonThread::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &ButtonThread::afterLightSleep); #endif - volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; + volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; - // Store click count during callback, for later use - volatile int multipressClickCount = 0; + // Store click count during callback, for later use + volatile int multipressClickCount = 0; - // Combination tracking state - bool waitingForLongPress = false; - uint32_t shortPressTime = 0; + // Combination tracking state + bool waitingForLongPress = false; + uint32_t shortPressTime = 0; - // Long press lead-up tracking - bool leadUpPlayed = false; - uint32_t lastLeadUpNoteTime = 0; - bool leadUpSequenceActive = false; + // Long press lead-up tracking + bool leadUpPlayed = false; + uint32_t lastLeadUpNoteTime = 0; + bool leadUpSequenceActive = false; - static void wakeOnIrq(int irq, int mode); + static void wakeOnIrq(int irq, int mode); }; extern ButtonThread *buttonThread; diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index f6dc0e19c..01712ad2a 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -21,210 +21,225 @@ static const char inputSourceName[] = "ExpressLRS5Way"; // should match "allow i * Example Fuzz values: {20, 20, 100, 100, 125, 300} now the fuzz for the 1600 * position is 300 instead of 20 */ -void ExpressLRSFiveWay::calcFuzzValues() { - for (unsigned int i = 0; i < N_JOY_ADC_VALUES; i++) { - uint16_t closestDist = 0xffff; - uint16_t ival = joyAdcValues[i]; - // Find the closest value to ival - for (unsigned int j = 0; j < N_JOY_ADC_VALUES; j++) { - // Don't compare value with itself - if (j == i) - continue; - uint16_t jval = joyAdcValues[j]; - if (jval < ival && (ival - jval < closestDist)) - closestDist = ival - jval; - if (jval > ival && (jval - ival < closestDist)) - closestDist = jval - ival; - } // for j +void ExpressLRSFiveWay::calcFuzzValues() +{ + for (unsigned int i = 0; i < N_JOY_ADC_VALUES; i++) { + uint16_t closestDist = 0xffff; + uint16_t ival = joyAdcValues[i]; + // Find the closest value to ival + for (unsigned int j = 0; j < N_JOY_ADC_VALUES; j++) { + // Don't compare value with itself + if (j == i) + continue; + uint16_t jval = joyAdcValues[j]; + if (jval < ival && (ival - jval < closestDist)) + closestDist = ival - jval; + if (jval > ival && (jval - ival < closestDist)) + closestDist = jval - ival; + } // for j - // And the fuzz is half the distance to the closest value - fuzzValues[i] = closestDist / 2; - // DBG("joy%u=%u f=%u, ", i, ival, fuzzValues[i]); - } // for i + // And the fuzz is half the distance to the closest value + fuzzValues[i] = closestDist / 2; + // DBG("joy%u=%u f=%u, ", i, ival, fuzzValues[i]); + } // for i } -int ExpressLRSFiveWay::readKey() { - uint16_t value = analogRead(PIN_JOYSTICK); +int ExpressLRSFiveWay::readKey() +{ + uint16_t value = analogRead(PIN_JOYSTICK); - constexpr uint8_t IDX_TO_INPUT[N_JOY_ADC_VALUES - 1] = {UP, DOWN, LEFT, RIGHT, OK}; - for (unsigned int i = 0; i < N_JOY_ADC_VALUES - 1; ++i) { - if (value < (joyAdcValues[i] + fuzzValues[i]) && value > (joyAdcValues[i] - fuzzValues[i])) - return IDX_TO_INPUT[i]; - } - return NO_PRESS; + constexpr uint8_t IDX_TO_INPUT[N_JOY_ADC_VALUES - 1] = {UP, DOWN, LEFT, RIGHT, OK}; + for (unsigned int i = 0; i < N_JOY_ADC_VALUES - 1; ++i) { + if (value < (joyAdcValues[i] + fuzzValues[i]) && value > (joyAdcValues[i] - fuzzValues[i])) + return IDX_TO_INPUT[i]; + } + return NO_PRESS; } -ExpressLRSFiveWay::ExpressLRSFiveWay() : concurrency::OSThread(inputSourceName) { - // ExpressLRS: init values - isLongPressed = false; - keyInProcess = NO_PRESS; - keyDownStart = 0; +ExpressLRSFiveWay::ExpressLRSFiveWay() : concurrency::OSThread(inputSourceName) +{ + // ExpressLRS: init values + isLongPressed = false; + keyInProcess = NO_PRESS; + keyDownStart = 0; - // Express LRS: calculate the threshold for interpreting ADC values as various buttons - calcFuzzValues(); + // Express LRS: calculate the threshold for interpreting ADC values as various buttons + calcFuzzValues(); - // Meshtastic: register with canned messages - inputBroker->registerSource(this); + // Meshtastic: register with canned messages + inputBroker->registerSource(this); } // ExpressLRS: interpret reading as key events -void ExpressLRSFiveWay::update(int *keyValue, bool *keyLongPressed) { - *keyValue = NO_PRESS; +void ExpressLRSFiveWay::update(int *keyValue, bool *keyLongPressed) +{ + *keyValue = NO_PRESS; - int newKey = readKey(); - if (keyInProcess == NO_PRESS) { - // New key down - if (newKey != NO_PRESS) { - keyDownStart = millis(); - // DBGLN("down=%u", newKey); - } - } else { - // if key released - if (newKey == NO_PRESS) { - // DBGLN("up=%u", keyInProcess); - if (!isLongPressed) { - if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_DEBOUNCE_MS)) { - *keyValue = keyInProcess; - *keyLongPressed = false; + int newKey = readKey(); + if (keyInProcess == NO_PRESS) { + // New key down + if (newKey != NO_PRESS) { + keyDownStart = millis(); + // DBGLN("down=%u", newKey); } - } - isLongPressed = false; - } - // else if the key has changed while down, reset state for next go-around - else if (newKey != keyInProcess) { - newKey = NO_PRESS; - } - // else still pressing, waiting for long if not already signaled - else if (!isLongPressed) { - if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_LONG_PRESS_MS)) { - *keyValue = keyInProcess; - *keyLongPressed = true; - isLongPressed = true; - } - } - } // if keyInProcess != NO_PRESS + } else { + // if key released + if (newKey == NO_PRESS) { + // DBGLN("up=%u", keyInProcess); + if (!isLongPressed) { + if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_DEBOUNCE_MS)) { + *keyValue = keyInProcess; + *keyLongPressed = false; + } + } + isLongPressed = false; + } + // else if the key has changed while down, reset state for next go-around + else if (newKey != keyInProcess) { + newKey = NO_PRESS; + } + // else still pressing, waiting for long if not already signaled + else if (!isLongPressed) { + if (!Throttle::isWithinTimespanMs(keyDownStart, KEY_LONG_PRESS_MS)) { + *keyValue = keyInProcess; + *keyLongPressed = true; + isLongPressed = true; + } + } + } // if keyInProcess != NO_PRESS - keyInProcess = newKey; + keyInProcess = newKey; } // Meshtastic: runs at regular intervals -int32_t ExpressLRSFiveWay::runOnce() { - uint32_t now = millis(); +int32_t ExpressLRSFiveWay::runOnce() +{ + uint32_t now = millis(); - // Dismiss any alert frames after 2 seconds - // Feedback for GPS toggle / adhoc ping - if (alerting && now > alertingSinceMs + 2000) { - alerting = false; - screen->endAlert(); - } + // Dismiss any alert frames after 2 seconds + // Feedback for GPS toggle / adhoc ping + if (alerting && now > alertingSinceMs + 2000) { + alerting = false; + screen->endAlert(); + } - // Get key events from ExpressLRS code - int keyValue; - bool longPressed; - update(&keyValue, &longPressed); + // Get key events from ExpressLRS code + int keyValue; + bool longPressed; + update(&keyValue, &longPressed); - // Do something about this key press - determineAction((KeyType)keyValue, longPressed ? LONG : SHORT); + // Do something about this key press + determineAction((KeyType)keyValue, longPressed ? LONG : SHORT); - // If there has been recent key activity, poll the joystick slightly more frequently - if (now < keyDownStart + (20 * 1000UL)) // Within last 20 seconds - return 100; + // If there has been recent key activity, poll the joystick slightly more frequently + if (now < keyDownStart + (20 * 1000UL)) // Within last 20 seconds + return 100; - // Otherwise, poll slightly less often - // Too many missed pressed if much slower than 250ms - return 250; + // Otherwise, poll slightly less often + // Too many missed pressed if much slower than 250ms + return 250; } // Determine what action to take when a button press is detected // Written verbose for easier remapping by user -void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) { - switch (key) { - case LEFT: - if (inCannedMessageMenu()) // If in canned message menu - sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) - else - sendKey(INPUT_BROKER_LEFT); - break; +void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) +{ + switch (key) { + case LEFT: + if (inCannedMessageMenu()) // If in canned message menu + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) + else + sendKey(INPUT_BROKER_LEFT); + break; - case RIGHT: - if (inCannedMessageMenu()) // If in canned message menu: - sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) - else - sendKey(INPUT_BROKER_RIGHT); - break; + case RIGHT: + if (inCannedMessageMenu()) // If in canned message menu: + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) + else + sendKey(INPUT_BROKER_RIGHT); + break; - case UP: - if (length == LONG) - toggleGPS(); - else - sendKey(INPUT_BROKER_UP); - break; + case UP: + if (length == LONG) + toggleGPS(); + else + sendKey(INPUT_BROKER_UP); + break; - case DOWN: - if (length == LONG) - sendAdhocPing(); - else - sendKey(INPUT_BROKER_DOWN); - break; + case DOWN: + if (length == LONG) + sendAdhocPing(); + else + sendKey(INPUT_BROKER_DOWN); + break; - case OK: - if (length == LONG) - shutdown(); - else - click(); // Use instead of sendKey(OK). Works better when canned message module disabled - break; + case OK: + if (length == LONG) + shutdown(); + else + click(); // Use instead of sendKey(OK). Works better when canned message module disabled + break; - default: - break; - } + default: + break; + } } // Feed input to the canned messages module -void ExpressLRSFiveWay::sendKey(input_broker_event key) { - InputEvent e = {}; - e.source = inputSourceName; - e.inputEvent = key; - notifyObservers(&e); +void ExpressLRSFiveWay::sendKey(input_broker_event key) +{ + InputEvent e = {}; + e.source = inputSourceName; + e.inputEvent = key; + notifyObservers(&e); } // Enable or Disable a connected GPS // Contained as one method for easier remapping of buttons by user -void ExpressLRSFiveWay::toggleGPS() { +void ExpressLRSFiveWay::toggleGPS() +{ #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) { - gps->toggleGpsMode(); - screen->startAlert("GPS Toggled"); - alerting = true; - alertingSinceMs = millis(); - } + if (gps != nullptr) { + gps->toggleGpsMode(); + screen->startAlert("GPS Toggled"); + alerting = true; + alertingSinceMs = millis(); + } #endif } // Send either node-info or position, on demand // Contained as one method for easier remapping of buttons by user -void ExpressLRSFiveWay::sendAdhocPing() { - service->refreshLocalMeshNode(); - bool sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); +void ExpressLRSFiveWay::sendAdhocPing() +{ + service->refreshLocalMeshNode(); + bool sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); - // Show custom alert frame, with multi-line centering - screen->startAlert([sentPosition](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - uint16_t x_offset = display->width() / 2; - uint16_t y_offset = 26; // Same constant as the default startAlert frame - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Sent ad-hoc"); - display->drawString(x_offset + x, y_offset + FONT_HEIGHT_MEDIUM + y, sentPosition ? "position" : "nodeinfo"); - }); + // Show custom alert frame, with multi-line centering + screen->startAlert([sentPosition](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + uint16_t x_offset = display->width() / 2; + uint16_t y_offset = 26; // Same constant as the default startAlert frame + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Sent ad-hoc"); + display->drawString(x_offset + x, y_offset + FONT_HEIGHT_MEDIUM + y, sentPosition ? "position" : "nodeinfo"); + }); - alerting = true; - alertingSinceMs = millis(); + alerting = true; + alertingSinceMs = millis(); } // Shutdown the node (enter deep-sleep) // Contained as one method for easier remapping of buttons by user -void ExpressLRSFiveWay::shutdown() { sendKey(INPUT_BROKER_SHUTDOWN); } +void ExpressLRSFiveWay::shutdown() +{ + sendKey(INPUT_BROKER_SHUTDOWN); +} -void ExpressLRSFiveWay::click() { sendKey(INPUT_BROKER_SELECT); } +void ExpressLRSFiveWay::click() +{ + sendKey(INPUT_BROKER_SELECT); +} ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; diff --git a/src/input/ExpressLRSFiveWay.h b/src/input/ExpressLRSFiveWay.h index a9bccc9ad..7c7f210f8 100644 --- a/src/input/ExpressLRSFiveWay.h +++ b/src/input/ExpressLRSFiveWay.h @@ -28,55 +28,56 @@ #include "GPS.h" // For toggle GPS action #endif -class ExpressLRSFiveWay : public Observable, public concurrency::OSThread { -private: - // Number of values in JOY_ADC_VALUES, if defined - // These must be ADC readings for {UP, DOWN, LEFT, RIGHT, ENTER, IDLE} - static constexpr size_t N_JOY_ADC_VALUES = 6; - static constexpr uint32_t KEY_DEBOUNCE_MS = 25; - static constexpr uint32_t KEY_LONG_PRESS_MS = 3000; // How many milliseconds to hold key for a long press +class ExpressLRSFiveWay : public Observable, public concurrency::OSThread +{ + private: + // Number of values in JOY_ADC_VALUES, if defined + // These must be ADC readings for {UP, DOWN, LEFT, RIGHT, ENTER, IDLE} + static constexpr size_t N_JOY_ADC_VALUES = 6; + static constexpr uint32_t KEY_DEBOUNCE_MS = 25; + static constexpr uint32_t KEY_LONG_PRESS_MS = 3000; // How many milliseconds to hold key for a long press - // This merged an enum used by the ExpressLRS code, with meshtastic canned message values - // Key names are kept simple, to allow user customizaton - typedef enum { - UP = INPUT_BROKER_UP, - DOWN = INPUT_BROKER_DOWN, - LEFT = INPUT_BROKER_LEFT, - RIGHT = INPUT_BROKER_RIGHT, - OK = INPUT_BROKER_SELECT, - CANCEL = INPUT_BROKER_CANCEL, - NO_PRESS = INPUT_BROKER_NONE - } KeyType; + // This merged an enum used by the ExpressLRS code, with meshtastic canned message values + // Key names are kept simple, to allow user customizaton + typedef enum { + UP = INPUT_BROKER_UP, + DOWN = INPUT_BROKER_DOWN, + LEFT = INPUT_BROKER_LEFT, + RIGHT = INPUT_BROKER_RIGHT, + OK = INPUT_BROKER_SELECT, + CANCEL = INPUT_BROKER_CANCEL, + NO_PRESS = INPUT_BROKER_NONE + } KeyType; - typedef enum { SHORT, LONG } PressLength; + typedef enum { SHORT, LONG } PressLength; - // From ExpressLRS - int keyInProcess; - uint32_t keyDownStart; - bool isLongPressed; - const uint16_t joyAdcValues[N_JOY_ADC_VALUES] = {JOYSTICK_ADC_VALS}; - uint16_t fuzzValues[N_JOY_ADC_VALUES]; - void calcFuzzValues(); - int readKey(); - void update(int *keyValue, bool *keyLongPressed); + // From ExpressLRS + int keyInProcess; + uint32_t keyDownStart; + bool isLongPressed; + const uint16_t joyAdcValues[N_JOY_ADC_VALUES] = {JOYSTICK_ADC_VALS}; + uint16_t fuzzValues[N_JOY_ADC_VALUES]; + void calcFuzzValues(); + int readKey(); + void update(int *keyValue, bool *keyLongPressed); - // Meshtastic code - void determineAction(KeyType key, PressLength length); - void sendKey(input_broker_event key); - inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } - int32_t runOnce() override; + // Meshtastic code + void determineAction(KeyType key, PressLength length); + void sendKey(input_broker_event key); + inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } + int32_t runOnce() override; - // Simplified Meshtastic actions, for easier remapping by user - void toggleGPS(); - void sendAdhocPing(); - void shutdown(); - void click(); + // Simplified Meshtastic actions, for easier remapping by user + void toggleGPS(); + void sendAdhocPing(); + void shutdown(); + void click(); - bool alerting = false; // Is the screen showing an alert frame? Feedback for GPS toggle / adhoc ping actions - uint32_t alertingSinceMs = 0; // When did screen begin showing an alert frame? Used to auto-dismiss + bool alerting = false; // Is the screen showing an alert frame? Feedback for GPS toggle / adhoc ping actions + uint32_t alertingSinceMs = 0; // When did screen begin showing an alert frame? Used to auto-dismiss -public: - ExpressLRSFiveWay(); + public: + ExpressLRSFiveWay(); }; extern ExpressLRSFiveWay *expressLRSFiveWayInput; diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp index 46f2c08ce..87c8a24ae 100644 --- a/src/input/HackadayCommunicatorKeyboard.cpp +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -106,103 +106,112 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, {}}; HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), - char_idx(0), tap_interval(0) { - reset(); + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ + reset(); } -void HackadayCommunicatorKeyboard::reset(void) { - TCA8418KeyboardBase::reset(); - enableInterrupts(); +void HackadayCommunicatorKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + enableInterrupts(); } // handle multi-key presses (shift and alt) -void HackadayCommunicatorKeyboard::trigger() { - uint8_t count = keyCount(); - if (count == 0) - return; - for (uint8_t i = 0; i < count; ++i) { - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); - uint8_t key = k & 0x7F; - if (k & 0x80) { - pressed(key); - } else { - released(); - state = Idle; +void HackadayCommunicatorKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } } - } } -void HackadayCommunicatorKeyboard::pressed(uint8_t key) { - if (state == Init || state == Busy) { - return; - } +void HackadayCommunicatorKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } - if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { - modifierFlag = 0; - } + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } - next_key = row * _TCA8418_COLS + col; - state = Held; + next_key = row * _TCA8418_COLS + col; + state = Held; - uint32_t now = millis(); - tap_interval = now - last_tap; + uint32_t now = millis(); + tap_interval = now - last_tap; - updateModifierFlag(next_key); - if (isModifierKey(next_key)) { - last_modifier_time = now; - } + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } - if (tap_interval < 0) { - last_tap = 0; - state = Busy; - return; - } + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; - } else { - char_idx += 1; - } + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } - last_key = next_key; - last_tap = now; + last_key = next_key; + last_tap = now; } -void HackadayCommunicatorKeyboard::released() { - if (state != Held) { - return; - } +void HackadayCommunicatorKeyboard::released() +{ + if (state != Held) { + return; + } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; - state = Idle; - return; - } + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } - uint32_t now = millis(); - last_tap = now; - if (HackadayCommunicatorTapMod[last_key]) - queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]); - if (isModifierKey(last_key) == false) - modifierFlag = 0; + uint32_t now = millis(); + last_tap = now; + if (HackadayCommunicatorTapMod[last_key]) + queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; } -void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key) { - if (key == modifierRightShiftKey) { - modifierFlag ^= modifierRightShift; - } else if (key == modifierLeftShiftKey) { - modifierFlag ^= modifierLeftShift; - } +void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierLeftShiftKey) { + modifierFlag ^= modifierLeftShift; + } } -bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierLeftShiftKey); } +bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierLeftShiftKey); +} #endif \ No newline at end of file diff --git a/src/input/HackadayCommunicatorKeyboard.h b/src/input/HackadayCommunicatorKeyboard.h index 2d862c863..8316bed72 100644 --- a/src/input/HackadayCommunicatorKeyboard.h +++ b/src/input/HackadayCommunicatorKeyboard.h @@ -1,25 +1,26 @@ #include "TCA8418KeyboardBase.h" -class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase { -public: - HackadayCommunicatorKeyboard(); - void reset(void); - void trigger(void) override; - virtual ~HackadayCommunicatorKeyboard() {} +class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase +{ + public: + HackadayCommunicatorKeyboard(); + void reset(void); + void trigger(void) override; + virtual ~HackadayCommunicatorKeyboard() {} -protected: - void pressed(uint8_t key) override; - void released(void) override; + protected: + void pressed(uint8_t key) override; + void released(void) override; - void updateModifierFlag(uint8_t key); - bool isModifierKey(uint8_t key); + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); -private: - uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed - uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index ca4a330e1..0aa78e2b8 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -5,63 +5,72 @@ InputBroker *inputBroker = nullptr; -InputBroker::InputBroker() { +InputBroker::InputBroker() +{ #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - inputEventQueue = xQueueCreate(5, sizeof(InputEvent)); - pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *)); - xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask); + inputEventQueue = xQueueCreate(5, sizeof(InputEvent)); + pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *)); + xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask); #endif } -void InputBroker::registerSource(Observable *source) { this->inputEventObserver.observe(source); } - -#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) -void InputBroker::requestPollSoon(InputPollable *pollable) { - if (xPortInIsrContext() == pdTRUE) { - xQueueSendFromISR(pollSoonQueue, &pollable, NULL); - } else { - xQueueSend(pollSoonQueue, &pollable, 0); - } -} - -void InputBroker::queueInputEvent(const InputEvent *event) { - if (xPortInIsrContext() == pdTRUE) { - xQueueSendFromISR(inputEventQueue, event, NULL); - } else { - xQueueSend(inputEventQueue, event, portMAX_DELAY); - } -} - -void InputBroker::processInputEventQueue() { - InputEvent event; - while (xQueueReceive(inputEventQueue, &event, 0)) { - handleInputEvent(&event); - } -} -#endif - -int InputBroker::handleInputEvent(const InputEvent *event) { - powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release - - if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && moduleConfig.external_notification.enabled && - externalNotificationModule->nagging()) { - externalNotificationModule->stopNow(); - } - - this->notifyObservers(event); - return 0; +void InputBroker::registerSource(Observable *source) +{ + this->inputEventObserver.observe(source); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) -void InputBroker::pollSoonWorker(void *p) { - InputBroker *instance = (InputBroker *)p; - while (true) { - InputPollable *pollable = NULL; - xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY); - if (pollable) { - pollable->pollOnce(); +void InputBroker::requestPollSoon(InputPollable *pollable) +{ + if (xPortInIsrContext() == pdTRUE) { + xQueueSendFromISR(pollSoonQueue, &pollable, NULL); + } else { + xQueueSend(pollSoonQueue, &pollable, 0); + } +} + +void InputBroker::queueInputEvent(const InputEvent *event) +{ + if (xPortInIsrContext() == pdTRUE) { + xQueueSendFromISR(inputEventQueue, event, NULL); + } else { + xQueueSend(inputEventQueue, event, portMAX_DELAY); + } +} + +void InputBroker::processInputEventQueue() +{ + InputEvent event; + while (xQueueReceive(inputEventQueue, &event, 0)) { + handleInputEvent(&event); } - } - vTaskDelete(NULL); +} +#endif + +int InputBroker::handleInputEvent(const InputEvent *event) +{ + powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release + + if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && + moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) { + externalNotificationModule->stopNow(); + } + + this->notifyObservers(event); + return 0; +} + +#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) +void InputBroker::pollSoonWorker(void *p) +{ + InputBroker *instance = (InputBroker *)p; + while (true) { + InputPollable *pollable = NULL; + xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY); + if (pollable) { + pollable->pollOnce(); + } + } + vTaskDelete(NULL); } #endif diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 486b80b4b..c55d7fa53 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -10,25 +10,25 @@ #endif enum input_broker_event { - INPUT_BROKER_NONE = 0, - INPUT_BROKER_SELECT = 10, - INPUT_BROKER_SELECT_LONG = 11, - INPUT_BROKER_UP_LONG = 12, - INPUT_BROKER_DOWN_LONG = 13, - INPUT_BROKER_UP = 17, - INPUT_BROKER_DOWN = 18, - INPUT_BROKER_LEFT = 19, - INPUT_BROKER_RIGHT = 20, - INPUT_BROKER_CANCEL = 24, - INPUT_BROKER_BACK = 27, - INPUT_BROKER_USER_PRESS, - INPUT_BROKER_ALT_PRESS, - INPUT_BROKER_ALT_LONG, - INPUT_BROKER_SHUTDOWN = 0x9b, - INPUT_BROKER_GPS_TOGGLE = 0x9e, - INPUT_BROKER_SEND_PING = 0xaf, - INPUT_BROKER_MATRIXKEY = 0xFE, - INPUT_BROKER_ANYKEY = 0xff + INPUT_BROKER_NONE = 0, + INPUT_BROKER_SELECT = 10, + INPUT_BROKER_SELECT_LONG = 11, + INPUT_BROKER_UP_LONG = 12, + INPUT_BROKER_DOWN_LONG = 13, + INPUT_BROKER_UP = 17, + INPUT_BROKER_DOWN = 18, + INPUT_BROKER_LEFT = 19, + INPUT_BROKER_RIGHT = 20, + INPUT_BROKER_CANCEL = 24, + INPUT_BROKER_BACK = 27, + INPUT_BROKER_USER_PRESS, + INPUT_BROKER_ALT_PRESS, + INPUT_BROKER_ALT_LONG, + INPUT_BROKER_SHUTDOWN = 0x9b, + INPUT_BROKER_GPS_TOGGLE = 0x9e, + INPUT_BROKER_SEND_PING = 0xaf, + INPUT_BROKER_MATRIXKEY = 0xFE, + INPUT_BROKER_ANYKEY = 0xff }; @@ -43,42 +43,44 @@ enum input_broker_event { #define INPUT_BROKER_MSG_EMOTE_LIST 0x8F typedef struct _InputEvent { - const char *source; - input_broker_event inputEvent; - unsigned char kbchar; - uint16_t touchX; - uint16_t touchY; + const char *source; + input_broker_event inputEvent; + unsigned char kbchar; + uint16_t touchX; + uint16_t touchY; } InputEvent; -class InputPollable { -public: - virtual ~InputPollable() = default; - virtual void pollOnce() = 0; +class InputPollable +{ + public: + virtual ~InputPollable() = default; + virtual void pollOnce() = 0; }; -class InputBroker : public Observable { - CallbackObserver inputEventObserver = - CallbackObserver(this, &InputBroker::handleInputEvent); +class InputBroker : public Observable +{ + CallbackObserver inputEventObserver = + CallbackObserver(this, &InputBroker::handleInputEvent); -public: - InputBroker(); - void registerSource(Observable *source); - void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } + public: + InputBroker(); + void registerSource(Observable *source); + void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - void requestPollSoon(InputPollable *pollable); - void queueInputEvent(const InputEvent *event); - void processInputEventQueue(); + void requestPollSoon(InputPollable *pollable); + void queueInputEvent(const InputEvent *event); + void processInputEventQueue(); #endif -protected: - int handleInputEvent(const InputEvent *event); + protected: + int handleInputEvent(const InputEvent *event); -private: + private: #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - QueueHandle_t inputEventQueue; - QueueHandle_t pollSoonQueue; - TaskHandle_t pollSoonTask; - static void pollSoonWorker(void *p); + QueueHandle_t inputEventQueue; + QueueHandle_t pollSoonQueue; + TaskHandle_t pollSoonTask; + static void pollSoonWorker(void *p); #endif }; diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index d020ec3b6..fee7c8ded 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -18,167 +18,172 @@ // Inspired by https://github.com/librerpi/rpi-tools/blob/master/keyboard-proxy/main.c which is GPL-v2 -LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name) { this->_originName = name; } - -void LinuxInput::deInit() { - if (fd >= 0) - close(fd); +LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; } -int32_t LinuxInput::runOnce() { +void LinuxInput::deInit() +{ + if (fd >= 0) + close(fd); +} - if (firstTime) { - if (portduino_config.keyboardDevice == "") - return disable(); - fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); - if (fd < 0) - return disable(); - ret = ioctl(fd, EVIOCGRAB, (void *)1); - if (ret != 0) - return disable(); +int32_t LinuxInput::runOnce() +{ - epollfd = epoll_create1(0); - assert(epollfd >= 0); + if (firstTime) { + if (portduino_config.keyboardDevice == "") + return disable(); + fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); + if (fd < 0) + return disable(); + ret = ioctl(fd, EVIOCGRAB, (void *)1); + if (ret != 0) + return disable(); - ev.events = EPOLLIN; - ev.data.fd = fd; - if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) { - perror("unable to epoll add"); - return disable(); - } - kb_found = true; - // This is the first time the OSThread library has called this function, so do port setup - firstTime = 0; - } + epollfd = epoll_create1(0); + assert(epollfd >= 0); - int nfds = epoll_wait(epollfd, events, MAX_EVENTS, 1); - if (nfds < 0) { - printf("%d ", nfds); - perror("epoll_wait failed"); - return disable(); - } else if (nfds == 0) { - return 50; - } - - int keys = 0; - memset(report, 0, 8); - for (int i = 0; i < nfds; i++) { - - struct input_event ev[64]; - int rd = read(events[i].data.fd, ev, sizeof(ev)); - assert(rd > ((signed int)sizeof(struct input_event))); - for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - e.kbchar = 0; - unsigned int type, code; - type = ev[j].type; - code = ev[j].code; - int value = ev[j].value; - // printf("Event: time %ld.%06ld, ", ev[j].time.tv_sec, ev[j].time.tv_usec); - - if (type == EV_KEY) { - uint8_t mod = 0; - - switch (code) { - case KEY_LEFTCTRL: - mod = 0x01; - break; - case KEY_RIGHTCTRL: - mod = 0x10; - break; - case KEY_LEFTSHIFT: - mod = 0x02; - break; - case KEY_RIGHTSHIFT: - mod = 0x20; - break; - case KEY_LEFTALT: - mod = 0x04; - break; - case KEY_RIGHTALT: - mod = 0x40; - break; - case KEY_LEFTMETA: - mod = 0x08; - break; + ev.events = EPOLLIN; + ev.data.fd = fd; + if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) { + perror("unable to epoll add"); + return disable(); } - if (value == 1) { - switch (code) { - case KEY_LEFTCTRL: - mod = 0x01; - break; - case KEY_RIGHTCTRL: - mod = 0x10; - break; - case KEY_LEFTSHIFT: - mod = 0x02; - break; - case KEY_RIGHTSHIFT: - mod = 0x20; - break; - case KEY_LEFTALT: - mod = 0x04; - break; - case KEY_RIGHTALT: - mod = 0x40; - break; - case KEY_LEFTMETA: - mod = 0x08; - break; - case KEY_ESC: // ESC - e.inputEvent = INPUT_BROKER_CANCEL; - break; - case KEY_BACK: // Back - e.inputEvent = INPUT_BROKER_BACK; - // e.kbchar = key; - break; + kb_found = true; + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 0; + } - case KEY_UP: // Up - e.inputEvent = INPUT_BROKER_UP; - break; - case KEY_DOWN: // Down - e.inputEvent = INPUT_BROKER_DOWN; - break; - case KEY_LEFT: // Left - e.inputEvent = INPUT_BROKER_LEFT; - break; - e.kbchar = INPUT_BROKER_LEFT; - case KEY_RIGHT: // Right - e.inputEvent = INPUT_BROKER_RIGHT; - break; + int nfds = epoll_wait(epollfd, events, MAX_EVENTS, 1); + if (nfds < 0) { + printf("%d ", nfds); + perror("epoll_wait failed"); + return disable(); + } else if (nfds == 0) { + return 50; + } + + int keys = 0; + memset(report, 0, 8); + for (int i = 0; i < nfds; i++) { + + struct input_event ev[64]; + int rd = read(events[i].data.fd, ev, sizeof(ev)); + assert(rd > ((signed int)sizeof(struct input_event))); + for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; e.kbchar = 0; - case KEY_ENTER: // Enter - e.inputEvent = INPUT_BROKER_SELECT; - break; - case KEY_POWER: - system("poweroff"); - break; - default: // all other keys - if (keymap[code]) { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = keymap[code]; - } - break; - } - } - if (ev[j].value) { - modifiers |= mod; - } else { - modifiers &= ~mod; - } - report[0] = modifiers; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) - e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. - this->notifyObservers(&e); - } - } - } + unsigned int type, code; + type = ev[j].type; + code = ev[j].code; + int value = ev[j].value; + // printf("Event: time %ld.%06ld, ", ev[j].time.tv_sec, ev[j].time.tv_usec); - return 50; // Keyscan every 50msec to avoid key bounce + if (type == EV_KEY) { + uint8_t mod = 0; + + switch (code) { + case KEY_LEFTCTRL: + mod = 0x01; + break; + case KEY_RIGHTCTRL: + mod = 0x10; + break; + case KEY_LEFTSHIFT: + mod = 0x02; + break; + case KEY_RIGHTSHIFT: + mod = 0x20; + break; + case KEY_LEFTALT: + mod = 0x04; + break; + case KEY_RIGHTALT: + mod = 0x40; + break; + case KEY_LEFTMETA: + mod = 0x08; + break; + } + if (value == 1) { + switch (code) { + case KEY_LEFTCTRL: + mod = 0x01; + break; + case KEY_RIGHTCTRL: + mod = 0x10; + break; + case KEY_LEFTSHIFT: + mod = 0x02; + break; + case KEY_RIGHTSHIFT: + mod = 0x20; + break; + case KEY_LEFTALT: + mod = 0x04; + break; + case KEY_RIGHTALT: + mod = 0x40; + break; + case KEY_LEFTMETA: + mod = 0x08; + break; + case KEY_ESC: // ESC + e.inputEvent = INPUT_BROKER_CANCEL; + break; + case KEY_BACK: // Back + e.inputEvent = INPUT_BROKER_BACK; + // e.kbchar = key; + break; + + case KEY_UP: // Up + e.inputEvent = INPUT_BROKER_UP; + break; + case KEY_DOWN: // Down + e.inputEvent = INPUT_BROKER_DOWN; + break; + case KEY_LEFT: // Left + e.inputEvent = INPUT_BROKER_LEFT; + break; + e.kbchar = INPUT_BROKER_LEFT; + case KEY_RIGHT: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + break; + e.kbchar = 0; + case KEY_ENTER: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + break; + case KEY_POWER: + system("poweroff"); + break; + default: // all other keys + if (keymap[code]) { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = keymap[code]; + } + break; + } + } + if (ev[j].value) { + modifiers |= mod; + } else { + modifiers &= ~mod; + } + report[0] = modifiers; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) + e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. + this->notifyObservers(&e); + } + } + } + + return 50; // Keyscan every 50msec to avoid key bounce } #endif \ No newline at end of file diff --git a/src/input/LinuxInput.h b/src/input/LinuxInput.h index 606a04c77..43d08493c 100644 --- a/src/input/LinuxInput.h +++ b/src/input/LinuxInput.h @@ -17,47 +17,49 @@ #define MAX_EVENTS 10 -class LinuxInput : public Observable, public concurrency::OSThread { -public: - explicit LinuxInput(const char *name); - void deInit(); // Strictly for cleanly "rebooting" the binary on native +class LinuxInput : public Observable, public concurrency::OSThread +{ + public: + explicit LinuxInput(const char *name); + void deInit(); // Strictly for cleanly "rebooting" the binary on native -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; -private: - const char *_originName; - bool firstTime = 1; - int shift = 0; - char key = 0; - char prevkey = 0; + private: + const char *_originName; + bool firstTime = 1; + int shift = 0; + char key = 0; + char prevkey = 0; - InputEvent eventqueue[50]; // The Linux API will return multiple keypresses at a time. Queue them to not miss any. - int queue_length = 0; - int queue_progress = 0; + InputEvent eventqueue[50]; // The Linux API will return multiple keypresses at a time. Queue them to not miss any. + int queue_length = 0; + int queue_progress = 0; - struct epoll_event events[MAX_EVENTS]; - int fd = -1; - int ret; - uint8_t report[8]; - int epollfd; - struct epoll_event ev; - uint8_t modifiers = 0; - std::map keymap{{KEY_A, 'a'}, {KEY_B, 'b'}, {KEY_C, 'c'}, {KEY_D, 'd'}, {KEY_E, 'e'}, - {KEY_F, 'f'}, {KEY_G, 'g'}, {KEY_H, 'h'}, {KEY_I, 'i'}, {KEY_J, 'j'}, - {KEY_K, 'k'}, {KEY_L, 'l'}, {KEY_M, 'm'}, {KEY_N, 'n'}, {KEY_O, 'o'}, - {KEY_P, 'p'}, {KEY_Q, 'q'}, {KEY_R, 'r'}, {KEY_S, 's'}, {KEY_T, 't'}, - {KEY_U, 'u'}, {KEY_V, 'v'}, {KEY_W, 'w'}, {KEY_X, 'x'}, {KEY_Y, 'y'}, - {KEY_Z, 'z'}, {KEY_BACKSPACE, 0x08}, {KEY_SPACE, ' '}, {KEY_1, '1'}, {KEY_2, '2'}, - {KEY_3, '3'}, {KEY_4, '4'}, {KEY_5, '5'}, {KEY_6, '6'}, {KEY_7, '7'}, - {KEY_8, '8'}, {KEY_9, '9'}, {KEY_0, '0'}, {KEY_DOT, '.'}, {KEY_COMMA, ','}, - {KEY_MINUS, '-'}, {KEY_EQUAL, '='}, {KEY_LEFTBRACE, '['}, {KEY_RIGHTBRACE, ']'}, {KEY_BACKSLASH, '\\'}, - {KEY_SEMICOLON, ';'}, {KEY_APOSTROPHE, '\''}, {KEY_SLASH, '/'}, {KEY_TAB, 0x09}}; - std::map uppers{{'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'}, {'f', 'F'}, {'g', 'G'}, {'h', 'H'}, - {'i', 'I'}, {'j', 'J'}, {'k', 'K'}, {'l', 'L'}, {'m', 'M'}, {'n', 'N'}, {'o', 'O'}, {'p', 'P'}, - {'q', 'Q'}, {'r', 'R'}, {'s', 'S'}, {'t', 'T'}, {'u', 'U'}, {'v', 'V'}, {'w', 'W'}, {'x', 'X'}, - {'y', 'Y'}, {'z', 'Z'}, {'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'}, {'6', '^'}, - {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'}, {'.', '>'}, {',', '<'}, {'-', '_'}, {'=', '+'}, - {'[', '{'}, {']', '}'}, {'\\', '|'}, {';', ':'}, {'\'', '"'}, {'/', '?'}}; + struct epoll_event events[MAX_EVENTS]; + int fd = -1; + int ret; + uint8_t report[8]; + int epollfd; + struct epoll_event ev; + uint8_t modifiers = 0; + std::map keymap{ + {KEY_A, 'a'}, {KEY_B, 'b'}, {KEY_C, 'c'}, {KEY_D, 'd'}, {KEY_E, 'e'}, + {KEY_F, 'f'}, {KEY_G, 'g'}, {KEY_H, 'h'}, {KEY_I, 'i'}, {KEY_J, 'j'}, + {KEY_K, 'k'}, {KEY_L, 'l'}, {KEY_M, 'm'}, {KEY_N, 'n'}, {KEY_O, 'o'}, + {KEY_P, 'p'}, {KEY_Q, 'q'}, {KEY_R, 'r'}, {KEY_S, 's'}, {KEY_T, 't'}, + {KEY_U, 'u'}, {KEY_V, 'v'}, {KEY_W, 'w'}, {KEY_X, 'x'}, {KEY_Y, 'y'}, + {KEY_Z, 'z'}, {KEY_BACKSPACE, 0x08}, {KEY_SPACE, ' '}, {KEY_1, '1'}, {KEY_2, '2'}, + {KEY_3, '3'}, {KEY_4, '4'}, {KEY_5, '5'}, {KEY_6, '6'}, {KEY_7, '7'}, + {KEY_8, '8'}, {KEY_9, '9'}, {KEY_0, '0'}, {KEY_DOT, '.'}, {KEY_COMMA, ','}, + {KEY_MINUS, '-'}, {KEY_EQUAL, '='}, {KEY_LEFTBRACE, '['}, {KEY_RIGHTBRACE, ']'}, {KEY_BACKSLASH, '\\'}, + {KEY_SEMICOLON, ';'}, {KEY_APOSTROPHE, '\''}, {KEY_SLASH, '/'}, {KEY_TAB, 0x09}}; + std::map uppers{{'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'}, {'f', 'F'}, {'g', 'G'}, {'h', 'H'}, + {'i', 'I'}, {'j', 'J'}, {'k', 'K'}, {'l', 'L'}, {'m', 'M'}, {'n', 'N'}, {'o', 'O'}, {'p', 'P'}, + {'q', 'Q'}, {'r', 'R'}, {'s', 'S'}, {'t', 'T'}, {'u', 'U'}, {'v', 'V'}, {'w', 'W'}, {'x', 'X'}, + {'y', 'Y'}, {'z', 'Z'}, {'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'}, {'6', '^'}, + {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'}, {'.', '>'}, {',', '<'}, {'-', '_'}, {'=', '+'}, + {'[', '{'}, {']', '}'}, {'\\', '|'}, {';', ':'}, {'\'', '"'}, {'/', '?'}}; }; #endif \ No newline at end of file diff --git a/src/input/LinuxInputImpl.cpp b/src/input/LinuxInputImpl.cpp index 96e5260f4..4ddda1923 100644 --- a/src/input/LinuxInputImpl.cpp +++ b/src/input/LinuxInputImpl.cpp @@ -7,6 +7,9 @@ LinuxInputImpl *aLinuxInputImpl; LinuxInputImpl::LinuxInputImpl() : LinuxInput("LinuxInput") {} -void LinuxInputImpl::init() { inputBroker->registerSource(this); } +void LinuxInputImpl::init() +{ + inputBroker->registerSource(this); +} #endif \ No newline at end of file diff --git a/src/input/LinuxInputImpl.h b/src/input/LinuxInputImpl.h index ef4ead5ec..e734b0294 100644 --- a/src/input/LinuxInputImpl.h +++ b/src/input/LinuxInputImpl.h @@ -11,10 +11,11 @@ * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class LinuxInputImpl : public LinuxInput { -public: - LinuxInputImpl(); - void init(); +class LinuxInputImpl : public LinuxInput +{ + public: + LinuxInputImpl(); + void init(); }; extern LinuxInputImpl *aLinuxInputImpl; #endif \ No newline at end of file diff --git a/src/input/MPR121Keyboard.cpp b/src/input/MPR121Keyboard.cpp index b7858f51a..9bca6801d 100644 --- a/src/input/MPR121Keyboard.cpp +++ b/src/input/MPR121Keyboard.cpp @@ -87,320 +87,347 @@ unsigned char MPR121_LongPressMap[12] = {MPR121_ESC, ' ', MPR121_NONE, // Rotated Layout uint8_t MPR121_KeyMap[12] = {2, 5, 8, 11, 1, 4, 7, 10, 0, 3, 6, 9}; -MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) { - // LOG_DEBUG("MPR121 @ %02x", m_addr); - state = Init; - last_key = -1; - last_tap = 0L; - char_idx = 0; - queue = ""; +MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) +{ + // LOG_DEBUG("MPR121 @ %02x", m_addr); + state = Init; + last_key = -1; + last_tap = 0L; + char_idx = 0; + queue = ""; } -void MPR121Keyboard::begin(uint8_t addr, TwoWire *wire) { - m_addr = addr; - m_wire = wire; +void MPR121Keyboard::begin(uint8_t addr, TwoWire *wire) +{ + m_addr = addr; + m_wire = wire; - m_wire->begin(); + m_wire->begin(); - reset(); + reset(); } -void MPR121Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { - m_addr = addr; - m_wire = nullptr; - writeCallback = w; - readCallback = r; - reset(); +void MPR121Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) +{ + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); } -void MPR121Keyboard::reset() { - LOG_DEBUG("MPR121 Reset"); - // Trigger a MPR121 Soft Reset - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(_MPR121_REG_SOFT_RESET); - m_wire->endTransmission(); - } - if (writeCallback) { - uint8_t data = 0; - writeCallback(m_addr, _MPR121_REG_SOFT_RESET, &data, 0); - } - delay(100); - // Reset Electrode Configuration to 0x00, Stop Mode - writeRegister(_MPR121_REG_ELECTRODE_CONFIG, 0x00); - delay(100); - - LOG_DEBUG("MPR121 Configuring"); - // Set touch release thresholds - for (uint8_t i = 0; i < 12; i++) { - // Set touch threshold - writeRegister(_MPR121_REG_TOUCH_THRESHOLD + (i * 2), 10); - delay(20); - // Set release threshold - writeRegister(_MPR121_REG_RELEASE_THRESHOLD + (i * 2), 5); - delay(20); - } - // Configure filtering and baseline registers - writeRegister(_MPR121_REG_MAX_HALF_DELTA_RISING, 0x05); - delay(20); - writeRegister(_MPR121_REG_MAX_HALF_DELTA_FALLING, 0x01); - delay(20); - writeRegister(_MPR121_REG_NOISE_HALF_DELTA_RISING, 0x01); - delay(20); - writeRegister(_MPR121_REG_NOISE_HALF_DELTA_FALLING, 0x05); - delay(20); - writeRegister(_MPR121_REG_NOISE_HALF_DELTA_TOUCHED, 0x00); - delay(20); - writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_RISING, 0x05); - delay(20); - writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_FALLING, 0x01); - delay(20); - writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_TOUCHED, 0x00); - delay(20); - writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_RISING, 0x00); - delay(20); - writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_FALLING, 0x00); - delay(20); - writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_TOUCHED, 0x00); - delay(20); - writeRegister(_MPR121_REG_AUTOCONF_CTRL0, 0x04); // Auto-config enable - delay(20); - writeRegister(_MPR121_REG_AUTOCONF_CTRL1, 0x00); // Ensure no auto-config interrupt - delay(20); - writeRegister(_MPR121_REG_DEBOUNCE, 0x02); - delay(20); - writeRegister(_MPR121_REG_CONFIG1, 0x20); - delay(20); - writeRegister(_MPR121_REG_CONFIG2, 0x21); - delay(20); - // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels - writeRegister(_MPR121_REG_ELECTRODE_CONFIG, ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); - delay(100); - LOG_DEBUG("MPR121 Run"); - state = Idle; -} - -void MPR121Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const { - pinMode(pin, INPUT_PULLUP); - ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); -} - -void MPR121Keyboard::detachInterrupt(uint8_t pin) const { ::detachInterrupt(pin); } - -uint8_t MPR121Keyboard::status() const { return readRegister16(_MPR121_REG_KEY); } - -uint8_t MPR121Keyboard::keyCount() const { - // Read the key register - uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); - return keyCount(keyRegister); -} - -uint8_t MPR121Keyboard::keyCount(uint16_t value) const { - // Mask the first 12 bits - uint16_t buttonState = value & _KEY_MASK; - - // Count how many bits are set to 1 (i.e., how many buttons are pressed) - uint8_t numButtonsPressed = 0; - for (uint8_t i = 0; i < 12; ++i) { - if (buttonState & (1 << i)) { - numButtonsPressed++; +void MPR121Keyboard::reset() +{ + LOG_DEBUG("MPR121 Reset"); + // Trigger a MPR121 Soft Reset + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(_MPR121_REG_SOFT_RESET); + m_wire->endTransmission(); } - } + if (writeCallback) { + uint8_t data = 0; + writeCallback(m_addr, _MPR121_REG_SOFT_RESET, &data, 0); + } + delay(100); + // Reset Electrode Configuration to 0x00, Stop Mode + writeRegister(_MPR121_REG_ELECTRODE_CONFIG, 0x00); + delay(100); - return numButtonsPressed; + LOG_DEBUG("MPR121 Configuring"); + // Set touch release thresholds + for (uint8_t i = 0; i < 12; i++) { + // Set touch threshold + writeRegister(_MPR121_REG_TOUCH_THRESHOLD + (i * 2), 10); + delay(20); + // Set release threshold + writeRegister(_MPR121_REG_RELEASE_THRESHOLD + (i * 2), 5); + delay(20); + } + // Configure filtering and baseline registers + writeRegister(_MPR121_REG_MAX_HALF_DELTA_RISING, 0x05); + delay(20); + writeRegister(_MPR121_REG_MAX_HALF_DELTA_FALLING, 0x01); + delay(20); + writeRegister(_MPR121_REG_NOISE_HALF_DELTA_RISING, 0x01); + delay(20); + writeRegister(_MPR121_REG_NOISE_HALF_DELTA_FALLING, 0x05); + delay(20); + writeRegister(_MPR121_REG_NOISE_HALF_DELTA_TOUCHED, 0x00); + delay(20); + writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_RISING, 0x05); + delay(20); + writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_FALLING, 0x01); + delay(20); + writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_TOUCHED, 0x00); + delay(20); + writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_RISING, 0x00); + delay(20); + writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_FALLING, 0x00); + delay(20); + writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_TOUCHED, 0x00); + delay(20); + writeRegister(_MPR121_REG_AUTOCONF_CTRL0, 0x04); // Auto-config enable + delay(20); + writeRegister(_MPR121_REG_AUTOCONF_CTRL1, 0x00); // Ensure no auto-config interrupt + delay(20); + writeRegister(_MPR121_REG_DEBOUNCE, 0x02); + delay(20); + writeRegister(_MPR121_REG_CONFIG1, 0x20); + delay(20); + writeRegister(_MPR121_REG_CONFIG2, 0x21); + delay(20); + // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels + writeRegister(_MPR121_REG_ELECTRODE_CONFIG, + ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); + delay(100); + LOG_DEBUG("MPR121 Run"); + state = Idle; } -bool MPR121Keyboard::hasEvent() { return queue.length() > 0; } - -void MPR121Keyboard::queueEvent(char next) { - if (next == MPR121_NONE) { - return; - } - queue.concat(next); +void MPR121Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const +{ + pinMode(pin, INPUT_PULLUP); + ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); } -char MPR121Keyboard::dequeueEvent() { - if (queue.length() < 1) { - return MPR121_NONE; - } - char next = queue.charAt(0); - queue.remove(0, 1); - return next; +void MPR121Keyboard::detachInterrupt(uint8_t pin) const +{ + ::detachInterrupt(pin); } -void MPR121Keyboard::trigger() { - // Intended to fire in response to an interrupt from the MPR121 or a longpress callback - // Only functional if not in Init state - if (state != Init) { +uint8_t MPR121Keyboard::status() const +{ + return readRegister16(_MPR121_REG_KEY); +} + +uint8_t MPR121Keyboard::keyCount() const +{ // Read the key register uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); - uint8_t keysPressed = keyCount(keyRegister); - if (keysPressed == 0) { - // No buttons pressed - if (state == Held) - released(); - state = Idle; - return; - } - if (keysPressed == 1) { - // No buttons pressed - if (state == Held || state == HeldLong) - held(keyRegister); - if (state == Idle) - pressed(keyRegister); - return; - } - if (keysPressed > 1) { - // Multipress - state = Busy; - return; - } - } else { - reset(); - } + return keyCount(keyRegister); } -void MPR121Keyboard::pressed(uint16_t keyRegister) { - if (state == Init || state == Busy) { - return; - } - if (keyCount(keyRegister) != 1) { - LOG_DEBUG("Multipress"); - return; - } else { - LOG_DEBUG("Pressed"); - } - uint16_t buttonState = keyRegister & _KEY_MASK; - uint8_t next_pin = 0; - for (uint8_t i = 0; i < 12; ++i) { - if (buttonState & (1 << i)) { - next_pin = i; +uint8_t MPR121Keyboard::keyCount(uint16_t value) const +{ + // Mask the first 12 bits + uint16_t buttonState = value & _KEY_MASK; + + // Count how many bits are set to 1 (i.e., how many buttons are pressed) + uint8_t numButtonsPressed = 0; + for (uint8_t i = 0; i < 12; ++i) { + if (buttonState & (1 << i)) { + numButtonsPressed++; + } } - } - uint8_t next_key = MPR121_KeyMap[next_pin]; - LOG_DEBUG("MPR121 Pin: %i Key: %i", next_pin, next_key); - uint32_t now = millis(); - int32_t tap_interval = now - last_tap; - if (tap_interval < 0) { - // long running, millis has overflowed. - last_tap = 0; - state = Busy; - return; - } - if (next_key != last_key || tap_interval > MULTI_TAP_THRESHOLD) { - char_idx = 0; - } else { - char_idx += 1; - } - last_key = next_key; - last_tap = now; - state = Held; - return; + + return numButtonsPressed; } -void MPR121Keyboard::held(uint16_t keyRegister) { - if (state == Init || state == Busy) { - return; - } - if (keyCount(keyRegister) != 1) { - return; - } - LOG_DEBUG("Held"); - uint16_t buttonState = keyRegister & _KEY_MASK; - uint8_t next_key = 0; - for (uint8_t i = 0; i < 12; ++i) { - if (buttonState & (1 << i)) { - next_key = MPR121_KeyMap[i]; +bool MPR121Keyboard::hasEvent() +{ + return queue.length() > 0; +} + +void MPR121Keyboard::queueEvent(char next) +{ + if (next == MPR121_NONE) { + return; } - } - uint32_t now = millis(); - int32_t held_interval = now - last_tap; - if (held_interval < 0 || next_key != last_key) { - // long running, millis has overflowed, or a key has been switched quickly... - last_tap = 0; - state = Busy; - return; - } - if (held_interval > LONG_PRESS_THRESHOLD) { - // Set state to heldlong, send a longpress, and reset the timer... - state = HeldLong; // heldlong will allow this function to still fire, but prevent a "release" - queueEvent(MPR121_LongPressMap[last_key]); + queue.concat(next); +} + +char MPR121Keyboard::dequeueEvent() +{ + if (queue.length() < 1) { + return MPR121_NONE; + } + char next = queue.charAt(0); + queue.remove(0, 1); + return next; +} + +void MPR121Keyboard::trigger() +{ + // Intended to fire in response to an interrupt from the MPR121 or a longpress callback + // Only functional if not in Init state + if (state != Init) { + // Read the key register + uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); + uint8_t keysPressed = keyCount(keyRegister); + if (keysPressed == 0) { + // No buttons pressed + if (state == Held) + released(); + state = Idle; + return; + } + if (keysPressed == 1) { + // No buttons pressed + if (state == Held || state == HeldLong) + held(keyRegister); + if (state == Idle) + pressed(keyRegister); + return; + } + if (keysPressed > 1) { + // Multipress + state = Busy; + return; + } + } else { + reset(); + } +} + +void MPR121Keyboard::pressed(uint16_t keyRegister) +{ + if (state == Init || state == Busy) { + return; + } + if (keyCount(keyRegister) != 1) { + LOG_DEBUG("Multipress"); + return; + } else { + LOG_DEBUG("Pressed"); + } + uint16_t buttonState = keyRegister & _KEY_MASK; + uint8_t next_pin = 0; + for (uint8_t i = 0; i < 12; ++i) { + if (buttonState & (1 << i)) { + next_pin = i; + } + } + uint8_t next_key = MPR121_KeyMap[next_pin]; + LOG_DEBUG("MPR121 Pin: %i Key: %i", next_pin, next_key); + uint32_t now = millis(); + int32_t tap_interval = now - last_tap; + if (tap_interval < 0) { + // long running, millis has overflowed. + last_tap = 0; + state = Busy; + return; + } + if (next_key != last_key || tap_interval > MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + last_key = next_key; last_tap = now; - LOG_DEBUG("Long Press Key: %i Map: %i", last_key, MPR121_LongPressMap[last_key]); - } - return; -} - -void MPR121Keyboard::released() { - if (state != Held) { + state = Held; return; - } - // would clear longpress callback... later. - if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; - state = Idle; +} + +void MPR121Keyboard::held(uint16_t keyRegister) +{ + if (state == Init || state == Busy) { + return; + } + if (keyCount(keyRegister) != 1) { + return; + } + LOG_DEBUG("Held"); + uint16_t buttonState = keyRegister & _KEY_MASK; + uint8_t next_key = 0; + for (uint8_t i = 0; i < 12; ++i) { + if (buttonState & (1 << i)) { + next_key = MPR121_KeyMap[i]; + } + } + uint32_t now = millis(); + int32_t held_interval = now - last_tap; + if (held_interval < 0 || next_key != last_key) { + // long running, millis has overflowed, or a key has been switched quickly... + last_tap = 0; + state = Busy; + return; + } + if (held_interval > LONG_PRESS_THRESHOLD) { + // Set state to heldlong, send a longpress, and reset the timer... + state = HeldLong; // heldlong will allow this function to still fire, but prevent a "release" + queueEvent(MPR121_LongPressMap[last_key]); + last_tap = now; + LOG_DEBUG("Long Press Key: %i Map: %i", last_key, MPR121_LongPressMap[last_key]); + } return; - } - LOG_DEBUG("Released"); - if (char_idx > 0 && TapMod[last_key] > 1) { - queueEvent(MPR121_BSP); - LOG_DEBUG("Multi Press, Backspace"); - } - queueEvent(MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); - LOG_DEBUG("Key Press: %i Index:%i if %i Map: %i", last_key, char_idx, TapMod[last_key], MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); } -uint8_t MPR121Keyboard::readRegister8(uint8_t reg) const { - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); - - m_wire->requestFrom(m_addr, (uint8_t)1); - if (m_wire->available() < 1) - return 0; - - return m_wire->read(); - } - if (readCallback) { - uint8_t data; - readCallback(m_addr, reg, &data, 1); - return data; - } - return 0; +void MPR121Keyboard::released() +{ + if (state != Held) { + return; + } + // would clear longpress callback... later. + if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = -1; + state = Idle; + return; + } + LOG_DEBUG("Released"); + if (char_idx > 0 && TapMod[last_key] > 1) { + queueEvent(MPR121_BSP); + LOG_DEBUG("Multi Press, Backspace"); + } + queueEvent(MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); + LOG_DEBUG("Key Press: %i Index:%i if %i Map: %i", last_key, char_idx, TapMod[last_key], + MPR121_TapMap[last_key][(char_idx % TapMod[last_key])]); } -uint16_t MPR121Keyboard::readRegister16(uint8_t reg) const { - uint8_t data[2] = {0}; - // uint8_t low = 0, high = 0; - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); +uint8_t MPR121Keyboard::readRegister8(uint8_t reg) const +{ + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); - m_wire->requestFrom(m_addr, (uint8_t)2); - if (m_wire->available() < 2) - return 0; - data[0] = m_wire->read(); - data[1] = m_wire->read(); - } - if (readCallback) { - readCallback(m_addr, reg, data, 2); - } - return (data[1] << 8) | data[0]; + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) + return 0; + + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; } -void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value) { - uint8_t data[2]; - data[0] = reg; - data[1] = value; +uint16_t MPR121Keyboard::readRegister16(uint8_t reg) const +{ + uint8_t data[2] = {0}; + // uint8_t low = 0, high = 0; + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(data, sizeof(uint8_t) * 2); - m_wire->endTransmission(); - } - if (writeCallback) { - writeCallback(m_addr, data[0], &(data[1]), 1); - } + m_wire->requestFrom(m_addr, (uint8_t)2); + if (m_wire->available() < 2) + return 0; + data[0] = m_wire->read(); + data[1] = m_wire->read(); + } + if (readCallback) { + readCallback(m_addr, reg, data, 2); + } + return (data[1] << 8) | data[0]; +} + +void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value) +{ + uint8_t data[2]; + data[0] = reg; + data[1] = value; + + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(data, sizeof(uint8_t) * 2); + m_wire->endTransmission(); + } + if (writeCallback) { + writeCallback(m_addr, data[0], &(data[1]), 1); + } } \ No newline at end of file diff --git a/src/input/MPR121Keyboard.h b/src/input/MPR121Keyboard.h index baa29d6e1..6349750ce 100644 --- a/src/input/MPR121Keyboard.h +++ b/src/input/MPR121Keyboard.h @@ -5,51 +5,52 @@ #include #include -class MPR121Keyboard { -public: - typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); +class MPR121Keyboard +{ + public: + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); - enum MPR121States { Init = 0, Idle, Held, HeldLong, Busy }; + enum MPR121States { Init = 0, Idle, Held, HeldLong, Busy }; - MPR121States state; + MPR121States state; - int8_t last_key; - uint32_t last_tap; - uint8_t char_idx; + int8_t last_key; + uint32_t last_tap; + uint8_t char_idx; - String queue; + String queue; - MPR121Keyboard(); + MPR121Keyboard(); - void begin(uint8_t addr = MPR121_KB_ADDR, TwoWire *wire = &Wire); + void begin(uint8_t addr = MPR121_KB_ADDR, TwoWire *wire = &Wire); - void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = MPR121_KB_ADDR); + void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = MPR121_KB_ADDR); - void reset(void); + void reset(void); - void attachInterrupt(uint8_t pin, void (*func)(void)) const; - void detachInterrupt(uint8_t pin) const; + void attachInterrupt(uint8_t pin, void (*func)(void)) const; + void detachInterrupt(uint8_t pin) const; - void trigger(void); - void pressed(uint16_t value); - void held(uint16_t value); - void released(void); + void trigger(void); + void pressed(uint16_t value); + void held(uint16_t value); + void released(void); - uint8_t status(void) const; - uint8_t keyCount(void) const; - uint8_t keyCount(uint16_t value) const; + uint8_t status(void) const; + uint8_t keyCount(void) const; + uint8_t keyCount(uint16_t value) const; - bool hasEvent(void); - char dequeueEvent(void); - void queueEvent(char); + bool hasEvent(void); + char dequeueEvent(void); + void queueEvent(char); - uint8_t readRegister8(uint8_t reg) const; - uint16_t readRegister16(uint8_t reg) const; - void writeRegister(uint8_t reg, uint8_t value); + uint8_t readRegister8(uint8_t reg) const; + uint16_t readRegister16(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); -private: - TwoWire *m_wire; - uint8_t m_addr; - i2c_com_fptr_t readCallback; - i2c_com_fptr_t writeCallback; + private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; }; \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index dbcaf1814..cc1222595 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -11,122 +11,131 @@ RotaryEncoderImpl *rotaryEncoderImpl; -RotaryEncoderImpl::RotaryEncoderImpl() { - rotary = nullptr; -#ifdef ARCH_ESP32 - isFirstInit = true; -#endif -} - -RotaryEncoderImpl::~RotaryEncoderImpl() { - LOG_DEBUG("RotaryEncoderImpl destructor"); - detachRotaryEncoderInterrupts(); - - if (rotary != nullptr) { - delete rotary; +RotaryEncoderImpl::RotaryEncoderImpl() +{ rotary = nullptr; - } +#ifdef ARCH_ESP32 + isFirstInit = true; +#endif } -bool RotaryEncoderImpl::init() { - if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || - moduleConfig.canned_message.inputbroker_pin_b == 0) { - // Input device is disabled. - return false; - } +RotaryEncoderImpl::~RotaryEncoderImpl() +{ + LOG_DEBUG("RotaryEncoderImpl destructor"); + detachRotaryEncoderInterrupts(); - eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); - eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); - eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + if (rotary != nullptr) { + delete rotary; + rotary = nullptr; + } +} - if (rotary == nullptr) { - rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, - moduleConfig.canned_message.inputbroker_pin_press); - } +bool RotaryEncoderImpl::init() +{ + if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || + moduleConfig.canned_message.inputbroker_pin_b == 0) { + // Input device is disabled. + return false; + } - attachRotaryEncoderInterrupts(); + eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + + if (rotary == nullptr) { + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + } + + attachRotaryEncoderInterrupts(); #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - if (isFirstInit) { - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); - isFirstInit = false; - } + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + if (isFirstInit) { + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + isFirstInit = false; + } #endif - LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, - moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, eventPressed); - return true; + LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, + moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, + eventPressed); + return true; } -void RotaryEncoderImpl::pollOnce() { - InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0}; +void RotaryEncoderImpl::pollOnce() +{ + InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0}; - static uint32_t lastPressed = millis(); - if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { - if (lastPressed + 200 < millis()) { - LOG_DEBUG("Rotary event Press"); - lastPressed = millis(); - e.inputEvent = this->eventPressed; - inputBroker->queueInputEvent(&e); + static uint32_t lastPressed = millis(); + if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { + if (lastPressed + 200 < millis()) { + LOG_DEBUG("Rotary event Press"); + lastPressed = millis(); + e.inputEvent = this->eventPressed; + inputBroker->queueInputEvent(&e); + } } - } - switch (rotary->process()) { - case RotaryEncoder::DIRECTION_CW: - LOG_DEBUG("Rotary event CW"); - e.inputEvent = this->eventCw; - inputBroker->queueInputEvent(&e); - break; - case RotaryEncoder::DIRECTION_CCW: - LOG_DEBUG("Rotary event CCW"); - e.inputEvent = this->eventCcw; - inputBroker->queueInputEvent(&e); - break; - default: - break; - } + switch (rotary->process()) { + case RotaryEncoder::DIRECTION_CW: + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->eventCw; + inputBroker->queueInputEvent(&e); + break; + case RotaryEncoder::DIRECTION_CCW: + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->eventCcw; + inputBroker->queueInputEvent(&e); + break; + default: + break; + } } -void RotaryEncoderImpl::detachRotaryEncoderInterrupts() { - LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); - if (interruptInstance == this) { - detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); - detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); - detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); - interruptInstance = nullptr; - } else { - LOG_WARN("RotaryEncoderImpl: interrupts already detached"); - } +void RotaryEncoderImpl::detachRotaryEncoderInterrupts() +{ + LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); + if (interruptInstance == this) { + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); + interruptInstance = nullptr; + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already detached"); + } } -void RotaryEncoderImpl::attachRotaryEncoderInterrupts() { - LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); - if (rotary != nullptr && interruptInstance == nullptr) { - rotary->resetButton(); +void RotaryEncoderImpl::attachRotaryEncoderInterrupts() +{ + LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); + if (rotary != nullptr && interruptInstance == nullptr) { + rotary->resetButton(); - interruptInstance = this; - auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); - } else { - LOG_WARN("RotaryEncoderImpl: interrupts already attached"); - } + interruptInstance = this; + auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already attached"); + } } #ifdef ARCH_ESP32 -int RotaryEncoderImpl::beforeLightSleep(void *unused) { - detachRotaryEncoderInterrupts(); - return 0; // Indicates success; +int RotaryEncoderImpl::beforeLightSleep(void *unused) +{ + detachRotaryEncoderInterrupts(); + return 0; // Indicates success; } -int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) { - attachRotaryEncoderInterrupts(); - return 0; // Indicates success; +int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachRotaryEncoderInterrupts(); + return 0; // Indicates success; } #endif diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h index 3a6fdd693..ec8a064bd 100644 --- a/src/input/RotaryEncoderImpl.h +++ b/src/input/RotaryEncoderImpl.h @@ -8,39 +8,41 @@ class RotaryEncoder; -class RotaryEncoderImpl final : public InputPollable { -public: - RotaryEncoderImpl(); - ~RotaryEncoderImpl() override; - bool init(); - virtual void pollOnce() override; - // Disconnect and reconnect interrupts for light sleep +class RotaryEncoderImpl final : public InputPollable +{ + public: + RotaryEncoderImpl(); + ~RotaryEncoderImpl() override; + bool init(); + virtual void pollOnce() override; + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif -protected: - static RotaryEncoderImpl *interruptInstance; + protected: + static RotaryEncoderImpl *interruptInstance; - input_broker_event eventCw = INPUT_BROKER_NONE; - input_broker_event eventCcw = INPUT_BROKER_NONE; - input_broker_event eventPressed = INPUT_BROKER_NONE; + input_broker_event eventCw = INPUT_BROKER_NONE; + input_broker_event eventCcw = INPUT_BROKER_NONE; + input_broker_event eventPressed = INPUT_BROKER_NONE; - RotaryEncoder *rotary; + RotaryEncoder *rotary; -private: + private: #ifdef ARCH_ESP32 - bool isFirstInit; + bool isFirstInit; #endif - void detachRotaryEncoderInterrupts(); - void attachRotaryEncoderInterrupts(); + void detachRotaryEncoderInterrupts(); + void attachRotaryEncoderInterrupts(); #ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); #endif }; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 8cd050bb7..c315f23d9 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -1,120 +1,129 @@ #include "RotaryEncoderInterruptBase.h" #include "configuration.h" -RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concurrency::OSThread(name) { this->_originName = name; } +RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; +} void RotaryEncoderInterruptBase::init( - uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, input_broker_event eventPressed, - input_broker_event eventPressedLong, + uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, + input_broker_event eventPressed, input_broker_event eventPressedLong, // std::function onIntA, std::function onIntB, std::function onIntPress) : - void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) { - this->_pinA = pinA; - this->_pinB = pinB; - this->_pinPress = pinPress; - this->_eventCw = eventCw; - this->_eventCcw = eventCcw; - this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) +{ + this->_pinA = pinA; + this->_pinB = pinB; + this->_pinPress = pinPress; + this->_eventCw = eventCw; + this->_eventCcw = eventCcw; + this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; - bool isRAK = false; + bool isRAK = false; #ifdef RAK_4631 - isRAK = true; + isRAK = true; #endif - if (!isRAK || pinPress != 0) { - pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, CHANGE); - } - if (!isRAK || this->_pinA != 0) { - pinMode(this->_pinA, INPUT_PULLUP); - attachInterrupt(this->_pinA, onIntA, CHANGE); - } - if (!isRAK || this->_pinA != 0) { - pinMode(this->_pinB, INPUT_PULLUP); - attachInterrupt(this->_pinB, onIntB, CHANGE); - } - - this->rotaryLevelA = digitalRead(this->_pinA); - this->rotaryLevelB = digitalRead(this->_pinB); - LOG_INFO("Rotary initialized (%d, %d, %d)", this->_pinA, this->_pinB, pinPress); -} - -int32_t RotaryEncoderInterruptBase::runOnce() { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - unsigned long now = millis(); - - // Handle press long/short detection - if (this->action == ROTARY_ACTION_PRESSED) { - bool buttonPressed = !digitalRead(_pinPress); - if (!pressDetected && buttonPressed) { - pressDetected = true; - pressStartTime = now; + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinA, INPUT_PULLUP); + attachInterrupt(this->_pinA, onIntA, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinB, INPUT_PULLUP); + attachInterrupt(this->_pinB, onIntB, CHANGE); } - if (pressDetected) { - uint32_t duration = now - pressStartTime; - if (!buttonPressed) { - // released -> if short press, send short, else already sent long - if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { - lastPressKeyTime = now; - LOG_DEBUG("Rotary event Press short"); - e.inputEvent = this->_eventPressed; + this->rotaryLevelA = digitalRead(this->_pinA); + this->rotaryLevelB = digitalRead(this->_pinB); + LOG_INFO("Rotary initialized (%d, %d, %d)", this->_pinA, this->_pinB, pinPress); +} + +int32_t RotaryEncoderInterruptBase::runOnce() +{ + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + unsigned long now = millis(); + + // Handle press long/short detection + if (this->action == ROTARY_ACTION_PRESSED) { + bool buttonPressed = !digitalRead(_pinPress); + if (!pressDetected && buttonPressed) { + pressDetected = true; + pressStartTime = now; } - pressDetected = false; - pressStartTime = 0; - lastPressLongEventTime = 0; - this->action = ROTARY_ACTION_NONE; - } else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) { - // fire single-shot long press - lastPressLongEventTime = now; - LOG_DEBUG("Rotary event Press long"); - e.inputEvent = this->_eventPressedLong; - } + + if (pressDetected) { + uint32_t duration = now - pressStartTime; + if (!buttonPressed) { + // released -> if short press, send short, else already sent long + if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + LOG_DEBUG("Rotary event Press short"); + e.inputEvent = this->_eventPressed; + } + pressDetected = false; + pressStartTime = 0; + lastPressLongEventTime = 0; + this->action = ROTARY_ACTION_NONE; + } else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && + lastPressLongEventTime == 0) { + // fire single-shot long press + lastPressLongEventTime = now; + LOG_DEBUG("Rotary event Press long"); + e.inputEvent = this->_eventPressedLong; + } + } + } else if (this->action == ROTARY_ACTION_CW) { + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->_eventCw; + } else if (this->action == ROTARY_ACTION_CCW) { + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->_eventCcw; } - } else if (this->action == ROTARY_ACTION_CW) { - LOG_DEBUG("Rotary event CW"); - e.inputEvent = this->_eventCw; - } else if (this->action == ROTARY_ACTION_CCW) { - LOG_DEBUG("Rotary event CCW"); - e.inputEvent = this->_eventCcw; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } - if (!pressDetected) { - this->action = ROTARY_ACTION_NONE; - } + if (!pressDetected) { + this->action = ROTARY_ACTION_NONE; + } - return INT32_MAX; + return INT32_MAX; } -void RotaryEncoderInterruptBase::intPressHandler() { - this->action = ROTARY_ACTION_PRESSED; - setIntervalFromNow(20); // start checking for long/short +void RotaryEncoderInterruptBase::intPressHandler() +{ + this->action = ROTARY_ACTION_PRESSED; + setIntervalFromNow(20); // start checking for long/short } -void RotaryEncoderInterruptBase::intAHandler() { - // CW rotation (at least on most common rotary encoders) - int currentLevelA = digitalRead(this->_pinA); - if (this->rotaryLevelA == currentLevelA) { - return; - } - this->rotaryLevelA = currentLevelA; - this->rotaryStateCCW = intHandler(currentLevelA == HIGH, this->rotaryLevelB, ROTARY_ACTION_CCW, this->rotaryStateCCW); +void RotaryEncoderInterruptBase::intAHandler() +{ + // CW rotation (at least on most common rotary encoders) + int currentLevelA = digitalRead(this->_pinA); + if (this->rotaryLevelA == currentLevelA) { + return; + } + this->rotaryLevelA = currentLevelA; + this->rotaryStateCCW = intHandler(currentLevelA == HIGH, this->rotaryLevelB, ROTARY_ACTION_CCW, this->rotaryStateCCW); } -void RotaryEncoderInterruptBase::intBHandler() { - // CW rotation (at least on most common rotary encoders) - int currentLevelB = digitalRead(this->_pinB); - if (this->rotaryLevelB == currentLevelB) { - return; - } - this->rotaryLevelB = currentLevelB; - this->rotaryStateCW = intHandler(currentLevelB == HIGH, this->rotaryLevelA, ROTARY_ACTION_CW, this->rotaryStateCW); +void RotaryEncoderInterruptBase::intBHandler() +{ + // CW rotation (at least on most common rotary encoders) + int currentLevelB = digitalRead(this->_pinB); + if (this->rotaryLevelB == currentLevelB) { + return; + } + this->rotaryLevelB = currentLevelB; + this->rotaryStateCW = intHandler(currentLevelB == HIGH, this->rotaryLevelA, ROTARY_ACTION_CW, this->rotaryStateCW); } /** @@ -128,20 +137,21 @@ void RotaryEncoderInterruptBase::intBHandler() { */ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool actualPinRaising, int otherPinLevel, RotaryEncoderInterruptBaseActionType action, - RotaryEncoderInterruptBaseStateType state) { - RotaryEncoderInterruptBaseStateType newState = state; - if (actualPinRaising && (otherPinLevel == LOW)) { - if (state == ROTARY_EVENT_CLEARED) { - newState = ROTARY_EVENT_OCCURRED; - if ((this->action != ROTARY_ACTION_PRESSED) && (this->action != action)) { - this->action = action; - } + RotaryEncoderInterruptBaseStateType state) +{ + RotaryEncoderInterruptBaseStateType newState = state; + if (actualPinRaising && (otherPinLevel == LOW)) { + if (state == ROTARY_EVENT_CLEARED) { + newState = ROTARY_EVENT_OCCURRED; + if ((this->action != ROTARY_ACTION_PRESSED) && (this->action != action)) { + this->action = action; + } + } + } else if (!actualPinRaising && (otherPinLevel == HIGH)) { + // Logic to prevent bouncing. + newState = ROTARY_EVENT_CLEARED; } - } else if (!actualPinRaising && (otherPinLevel == HIGH)) { - // Logic to prevent bouncing. - newState = ROTARY_EVENT_CLEARED; - } - setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! + setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! - return newState; + return newState; } diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h index b856d69bd..4f9757609 100644 --- a/src/input/RotaryEncoderInterruptBase.h +++ b/src/input/RotaryEncoderInterruptBase.h @@ -8,46 +8,47 @@ enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_C enum RotaryEncoderInterruptBaseActionType { ROTARY_ACTION_NONE, ROTARY_ACTION_PRESSED, ROTARY_ACTION_CW, ROTARY_ACTION_CCW }; -class RotaryEncoderInterruptBase : public Observable, public concurrency::OSThread { -public: - explicit RotaryEncoderInterruptBase(const char *name); - void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, input_broker_event eventPressed, - input_broker_event eventPressedLong, - // std::function onIntA, std::function onIntB, std::function - // onIntPress); - void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); - void intPressHandler(); - void intAHandler(); - void intBHandler(); +class RotaryEncoderInterruptBase : public Observable, public concurrency::OSThread +{ + public: + explicit RotaryEncoderInterruptBase(const char *name); + void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, + input_broker_event eventPressed, input_broker_event eventPressedLong, + // std::function onIntA, std::function onIntB, std::function onIntPress); + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); + void intPressHandler(); + void intAHandler(); + void intBHandler(); -protected: - virtual int32_t runOnce() override; - RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel, RotaryEncoderInterruptBaseActionType action, - RotaryEncoderInterruptBaseStateType state); + protected: + virtual int32_t runOnce() override; + RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel, + RotaryEncoderInterruptBaseActionType action, + RotaryEncoderInterruptBaseStateType state); - volatile RotaryEncoderInterruptBaseStateType rotaryStateCW = ROTARY_EVENT_CLEARED; - volatile RotaryEncoderInterruptBaseStateType rotaryStateCCW = ROTARY_EVENT_CLEARED; - volatile int rotaryLevelA = LOW; - volatile int rotaryLevelB = LOW; - volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; + volatile RotaryEncoderInterruptBaseStateType rotaryStateCW = ROTARY_EVENT_CLEARED; + volatile RotaryEncoderInterruptBaseStateType rotaryStateCCW = ROTARY_EVENT_CLEARED; + volatile int rotaryLevelA = LOW; + volatile int rotaryLevelB = LOW; + volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; -private: - // pins and events - uint8_t _pinA = 0; - uint8_t _pinB = 0; - uint8_t _pinPress = 0; - input_broker_event _eventCw = INPUT_BROKER_NONE; - input_broker_event _eventCcw = INPUT_BROKER_NONE; - input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; - const char *_originName; + private: + // pins and events + uint8_t _pinA = 0; + uint8_t _pinB = 0; + uint8_t _pinPress = 0; + input_broker_event _eventCw = INPUT_BROKER_NONE; + input_broker_event _eventCcw = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + const char *_originName; - // Long press detection variables - uint32_t pressStartTime = 0; - bool pressDetected = false; - uint32_t lastPressLongEventTime = 0; - unsigned long lastPressKeyTime = 0; - static const uint32_t LONG_PRESS_DURATION = 300; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select - const unsigned long pressDebounceMs = 200; // ms + // Long press detection variables + uint32_t pressStartTime = 0; + bool pressDetected = false; + uint32_t lastPressLongEventTime = 0; + unsigned long lastPressKeyTime = 0; + static const uint32_t LONG_PRESS_DURATION = 300; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select + const unsigned long pressDebounceMs = 200; // ms }; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 03458ed29..1da2ea008 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -6,31 +6,42 @@ RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; RotaryEncoderInterruptImpl1::RotaryEncoderInterruptImpl1() : RotaryEncoderInterruptBase("rotEnc1") {} -bool RotaryEncoderInterruptImpl1::init() { - if (!moduleConfig.canned_message.rotary1_enabled) { - // Input device is disabled. - disable(); - return false; - } +bool RotaryEncoderInterruptImpl1::init() +{ + if (!moduleConfig.canned_message.rotary1_enabled) { + // Input device is disabled. + disable(); + return false; + } - uint8_t pinA = moduleConfig.canned_message.inputbroker_pin_a; - uint8_t pinB = moduleConfig.canned_message.inputbroker_pin_b; - uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); - input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); - input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; + uint8_t pinA = moduleConfig.canned_message.inputbroker_pin_a; + uint8_t pinB = moduleConfig.canned_message.inputbroker_pin_b; + uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; + input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; - // moduleConfig.canned_message.ext_notification_module_output - RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong, RotaryEncoderInterruptImpl1::handleIntA, - RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntPressed); - inputBroker->registerSource(this); + // moduleConfig.canned_message.ext_notification_module_output + RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong, + RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, + RotaryEncoderInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif - return true; + return true; } -void RotaryEncoderInterruptImpl1::handleIntA() { rotaryEncoderInterruptImpl1->intAHandler(); } -void RotaryEncoderInterruptImpl1::handleIntB() { rotaryEncoderInterruptImpl1->intBHandler(); } -void RotaryEncoderInterruptImpl1::handleIntPressed() { rotaryEncoderInterruptImpl1->intPressHandler(); } \ No newline at end of file +void RotaryEncoderInterruptImpl1::handleIntA() +{ + rotaryEncoderInterruptImpl1->intAHandler(); +} +void RotaryEncoderInterruptImpl1::handleIntB() +{ + rotaryEncoderInterruptImpl1->intBHandler(); +} +void RotaryEncoderInterruptImpl1::handleIntPressed() +{ + rotaryEncoderInterruptImpl1->intPressHandler(); +} \ No newline at end of file diff --git a/src/input/RotaryEncoderInterruptImpl1.h b/src/input/RotaryEncoderInterruptImpl1.h index 901f29d71..22ecba37a 100644 --- a/src/input/RotaryEncoderInterruptImpl1.h +++ b/src/input/RotaryEncoderInterruptImpl1.h @@ -8,13 +8,14 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class RotaryEncoderInterruptImpl1 : public RotaryEncoderInterruptBase { -public: - RotaryEncoderInterruptImpl1(); - bool init(); - static void handleIntA(); - static void handleIntB(); - static void handleIntPressed(); +class RotaryEncoderInterruptImpl1 : public RotaryEncoderInterruptBase +{ + public: + RotaryEncoderInterruptImpl1(); + bool init(); + static void handleIntA(); + static void handleIntB(); + static void handleIntPressed(); }; extern RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; \ No newline at end of file diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp index dce06c342..0a6e6e974 100644 --- a/src/input/SeesawRotary.cpp +++ b/src/input/SeesawRotary.cpp @@ -6,73 +6,78 @@ using namespace concurrency; SeesawRotary *seesawRotary; -SeesawRotary::SeesawRotary(const char *name) : OSThread(name) { _originName = name; } - -bool SeesawRotary::init() { - if (inputBroker) - inputBroker->registerSource(this); - - if (!ss.begin(SEESAW_ADDR)) { - return false; - } - // attachButtonInterrupts(); - - uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF); - if (version != 4991) { - LOG_WARN("Wrong firmware loaded? %u", version); - } else { - LOG_INFO("Found Product 4991"); - } - /* - #ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); - #endif - */ - ss.pinMode(SS_SWITCH, INPUT_PULLUP); - - // get starting position - encoder_position = ss.getEncoderPosition(); - - ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); - ss.enableEncoderInterrupt(); - canSleep = true; // Assume we should not keep the board awake - - return true; +SeesawRotary::SeesawRotary(const char *name) : OSThread(name) +{ + _originName = name; } -int32_t SeesawRotary::runOnce() { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - bool currentlyPressed = !ss.digitalRead(SS_SWITCH); +bool SeesawRotary::init() +{ + if (inputBroker) + inputBroker->registerSource(this); - if (currentlyPressed && !wasPressed) { - e.inputEvent = INPUT_BROKER_SELECT; - } - wasPressed = currentlyPressed; - - int32_t new_position = ss.getEncoderPosition(); - // did we move arounde? - if (encoder_position != new_position) { - if (encoder_position == 0 && new_position != 1) { - e.inputEvent = INPUT_BROKER_ALT_PRESS; - } else if (new_position == 0 && encoder_position != 1) { - e.inputEvent = INPUT_BROKER_USER_PRESS; - } else if (new_position > encoder_position) { - e.inputEvent = INPUT_BROKER_USER_PRESS; - } else { - e.inputEvent = INPUT_BROKER_ALT_PRESS; + if (!ss.begin(SEESAW_ADDR)) { + return false; } - encoder_position = new_position; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - e.source = this->_originName; - e.kbchar = 0x00; - this->notifyObservers(&e); - } + // attachButtonInterrupts(); - return 50; + uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF); + if (version != 4991) { + LOG_WARN("Wrong firmware loaded? %u", version); + } else { + LOG_INFO("Found Product 4991"); + } + /* + #ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + #endif + */ + ss.pinMode(SS_SWITCH, INPUT_PULLUP); + + // get starting position + encoder_position = ss.getEncoderPosition(); + + ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); + ss.enableEncoderInterrupt(); + canSleep = true; // Assume we should not keep the board awake + + return true; +} + +int32_t SeesawRotary::runOnce() +{ + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + bool currentlyPressed = !ss.digitalRead(SS_SWITCH); + + if (currentlyPressed && !wasPressed) { + e.inputEvent = INPUT_BROKER_SELECT; + } + wasPressed = currentlyPressed; + + int32_t new_position = ss.getEncoderPosition(); + // did we move arounde? + if (encoder_position != new_position) { + if (encoder_position == 0 && new_position != 1) { + e.inputEvent = INPUT_BROKER_ALT_PRESS; + } else if (new_position == 0 && encoder_position != 1) { + e.inputEvent = INPUT_BROKER_USER_PRESS; + } else if (new_position > encoder_position) { + e.inputEvent = INPUT_BROKER_USER_PRESS; + } else { + e.inputEvent = INPUT_BROKER_ALT_PRESS; + } + encoder_position = new_position; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = 0x00; + this->notifyObservers(&e); + } + + return 50; } #endif \ No newline at end of file diff --git a/src/input/SeesawRotary.h b/src/input/SeesawRotary.h index 6af0bb5ee..3812b130a 100644 --- a/src/input/SeesawRotary.h +++ b/src/input/SeesawRotary.h @@ -11,17 +11,18 @@ #define SEESAW_ADDR 0x36 -class SeesawRotary : public Observable, public concurrency::OSThread { -public: - const char *_originName; - bool init(); - SeesawRotary(const char *name); - int32_t runOnce() override; +class SeesawRotary : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + bool init(); + SeesawRotary(const char *name); + int32_t runOnce() override; -private: - Adafruit_seesaw ss; - int32_t encoder_position; - bool wasPressed = false; + private: + Adafruit_seesaw ss; + int32_t encoder_position; + bool wasPressed = false; }; extern SeesawRotary *seesawRotary; diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index 61c7b9ab5..a5d2c614f 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -24,161 +24,164 @@ unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w', #endif -SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) { - this->_originName = name; +SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; - globalSerialKeyboard = this; + globalSerialKeyboard = this; } -void SerialKeyboard::erase() { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - e.source = this->_originName; - this->notifyObservers(&e); +void SerialKeyboard::erase() +{ + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + e.source = this->_originName; + this->notifyObservers(&e); } -int32_t SerialKeyboard::runOnce() { - if (!INPUTBROKER_SERIAL_TYPE) { - // Input device is not requested. - return disable(); - } - - if (firstTime) { - // This is the first time the OSThread library has called this function, so do port setup - firstTime = 0; - pinMode(KB_LOAD, OUTPUT); - pinMode(KB_CLK, OUTPUT); - pinMode(KB_DATA, INPUT); - digitalWrite(KB_LOAD, HIGH); - digitalWrite(KB_CLK, LOW); - prevKeys = 0b1111111111111111; - LOG_DEBUG("Serial Keyboard setup"); - } - - if (INPUTBROKER_SERIAL_TYPE == 1) { // Chatter V1.0 & V2.0 keypads - // scan for keypresses - // Write pulse to load pin - digitalWrite(KB_LOAD, LOW); - delayMicroseconds(5); - digitalWrite(KB_LOAD, HIGH); - delayMicroseconds(5); - - // Get data from 74HC165 - byte shiftRegister1 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); - byte shiftRegister2 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); - - keys = (shiftRegister1 << 8) + shiftRegister2; - - // Print to serial monitor - // Serial.print (shiftRegister1, BIN); - // Serial.print ("X"); - // Serial.println (shiftRegister2, BIN); - - if (!Throttle::isWithinTimespanMs(lastPressTime, 500)) { - quickPress = 0; +int32_t SerialKeyboard::runOnce() +{ + if (!INPUTBROKER_SERIAL_TYPE) { + // Input device is not requested. + return disable(); } - if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once - // but shouldn't be a limitation - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - // SELECT OR SEND OR CANCEL EVENT - if (!(shiftRegister2 & (1 << 3))) { - if (shift > 0) { - e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED - e.kbchar = 0x09; // TAB - shift = 0; // reset shift after TAB - } else { - e.inputEvent = INPUT_BROKER_LEFT; - } - } else if (!(shiftRegister2 & (1 << 2))) { - if (shift > 0) { - e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED - e.kbchar = 0x09; // TAB - shift = 0; // reset shift after TAB - } else { - e.inputEvent = INPUT_BROKER_RIGHT; - } - e.kbchar = 0; - } else if (!(shiftRegister2 & (1 << 1))) { - e.inputEvent = INPUT_BROKER_SELECT; - } else if (!(shiftRegister2 & (1 << 0))) { - e.inputEvent = INPUT_BROKER_CANCEL; - } + if (firstTime) { + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 0; + pinMode(KB_LOAD, OUTPUT); + pinMode(KB_CLK, OUTPUT); + pinMode(KB_DATA, INPUT); + digitalWrite(KB_LOAD, HIGH); + digitalWrite(KB_CLK, LOW); + prevKeys = 0b1111111111111111; + LOG_DEBUG("Serial Keyboard setup"); + } - // TEXT INPUT EVENT - else if (!(shiftRegister1 & (1 << 4))) { - keyPressed = 0; - } else if (!(shiftRegister1 & (1 << 3))) { - keyPressed = 1; - } else if (!(shiftRegister2 & (1 << 4))) { - keyPressed = 2; - } else if (!(shiftRegister1 & (1 << 5))) { - keyPressed = 3; - } else if (!(shiftRegister1 & (1 << 2))) { - keyPressed = 4; - } else if (!(shiftRegister2 & (1 << 5))) { - keyPressed = 5; - } else if (!(shiftRegister1 & (1 << 6))) { - keyPressed = 6; - } else if (!(shiftRegister1 & (1 << 1))) { - keyPressed = 7; - } else if (!(shiftRegister2 & (1 << 6))) { - keyPressed = 8; - } else if (!(shiftRegister1 & (1 << 0))) { - keyPressed = 9; - } - // BACKSPACE or TAB - else if (!(shiftRegister1 & (1 << 7))) { - if (shift == 0 || shift == 2) { // BACKSPACE - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - } else { // shift = 1 => TAB - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x09; - } - } - // SHIFT - else if (!(shiftRegister2 & (1 << 7))) { - keyPressed = 10; - } + if (INPUTBROKER_SERIAL_TYPE == 1) { // Chatter V1.0 & V2.0 keypads + // scan for keypresses + // Write pulse to load pin + digitalWrite(KB_LOAD, LOW); + delayMicroseconds(5); + digitalWrite(KB_LOAD, HIGH); + delayMicroseconds(5); - if (keyPressed < 11) { - if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { - quickPress += 1; - if (quickPress > 3) { + // Get data from 74HC165 + byte shiftRegister1 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); + byte shiftRegister2 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); + + keys = (shiftRegister1 << 8) + shiftRegister2; + + // Print to serial monitor + // Serial.print (shiftRegister1, BIN); + // Serial.print ("X"); + // Serial.println (shiftRegister2, BIN); + + if (!Throttle::isWithinTimespanMs(lastPressTime, 500)) { quickPress = 0; - } } - if (keyPressed != lastKeyPressed) { - quickPress = 0; - } - if (keyPressed < 10) { // if it's a letter - if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { - erase(); - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); - } else { // then it's shift - shift += 1; - if (shift > 2) { - shift = 0; - } - } - lastPressTime = millis(); - lastKeyPressed = keyPressed; - keyPressed = 13; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } + if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but + // shouldn't be a limitation + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + // SELECT OR SEND OR CANCEL EVENT + if (!(shiftRegister2 & (1 << 3))) { + if (shift > 0) { + e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED + e.kbchar = 0x09; // TAB + shift = 0; // reset shift after TAB + } else { + e.inputEvent = INPUT_BROKER_LEFT; + } + } else if (!(shiftRegister2 & (1 << 2))) { + if (shift > 0) { + e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED + e.kbchar = 0x09; // TAB + shift = 0; // reset shift after TAB + } else { + e.inputEvent = INPUT_BROKER_RIGHT; + } + e.kbchar = 0; + } else if (!(shiftRegister2 & (1 << 1))) { + e.inputEvent = INPUT_BROKER_SELECT; + } else if (!(shiftRegister2 & (1 << 0))) { + e.inputEvent = INPUT_BROKER_CANCEL; + } + + // TEXT INPUT EVENT + else if (!(shiftRegister1 & (1 << 4))) { + keyPressed = 0; + } else if (!(shiftRegister1 & (1 << 3))) { + keyPressed = 1; + } else if (!(shiftRegister2 & (1 << 4))) { + keyPressed = 2; + } else if (!(shiftRegister1 & (1 << 5))) { + keyPressed = 3; + } else if (!(shiftRegister1 & (1 << 2))) { + keyPressed = 4; + } else if (!(shiftRegister2 & (1 << 5))) { + keyPressed = 5; + } else if (!(shiftRegister1 & (1 << 6))) { + keyPressed = 6; + } else if (!(shiftRegister1 & (1 << 1))) { + keyPressed = 7; + } else if (!(shiftRegister2 & (1 << 6))) { + keyPressed = 8; + } else if (!(shiftRegister1 & (1 << 0))) { + keyPressed = 9; + } + // BACKSPACE or TAB + else if (!(shiftRegister1 & (1 << 7))) { + if (shift == 0 || shift == 2) { // BACKSPACE + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + } else { // shift = 1 => TAB + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x09; + } + } + // SHIFT + else if (!(shiftRegister2 & (1 << 7))) { + keyPressed = 10; + } + + if (keyPressed < 11) { + if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { + quickPress += 1; + if (quickPress > 3) { + quickPress = 0; + } + } + if (keyPressed != lastKeyPressed) { + quickPress = 0; + } + if (keyPressed < 10) { // if it's a letter + if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { + erase(); + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); + } else { // then it's shift + shift += 1; + if (shift > 2) { + shift = 0; + } + } + lastPressTime = millis(); + lastKeyPressed = keyPressed; + keyPressed = 13; + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + prevKeys = keys; } - prevKeys = keys; - } - return 50; + return 50; } #endif // INPUTBROKER_SERIAL_TYPE \ No newline at end of file diff --git a/src/input/SerialKeyboard.h b/src/input/SerialKeyboard.h index ebac7d673..f25eb2630 100644 --- a/src/input/SerialKeyboard.h +++ b/src/input/SerialKeyboard.h @@ -3,26 +3,27 @@ #include "InputBroker.h" #include "concurrency/OSThread.h" -class SerialKeyboard : public Observable, public concurrency::OSThread { -public: - explicit SerialKeyboard(const char *name); +class SerialKeyboard : public Observable, public concurrency::OSThread +{ + public: + explicit SerialKeyboard(const char *name); - uint8_t getShift() const { return shift; } + uint8_t getShift() const { return shift; } -protected: - virtual int32_t runOnce() override; - void erase(); + protected: + virtual int32_t runOnce() override; + void erase(); -private: - const char *_originName; - bool firstTime = 1; - int prevKeys = 0; - int keys = 0; - int shift = 0; - int keyPressed = 13; - int lastKeyPressed = 13; - int quickPress = 0; - unsigned long lastPressTime = 0; + private: + const char *_originName; + bool firstTime = 1; + int prevKeys = 0; + int keys = 0; + int shift = 0; + int keyPressed = 13; + int lastKeyPressed = 13; + int quickPress = 0; + unsigned long lastPressTime = 0; }; extern SerialKeyboard *globalSerialKeyboard; \ No newline at end of file diff --git a/src/input/SerialKeyboardImpl.cpp b/src/input/SerialKeyboardImpl.cpp index e1d42bb19..249b76fe3 100644 --- a/src/input/SerialKeyboardImpl.cpp +++ b/src/input/SerialKeyboardImpl.cpp @@ -8,13 +8,14 @@ SerialKeyboardImpl *aSerialKeyboardImpl; SerialKeyboardImpl::SerialKeyboardImpl() : SerialKeyboard("serialKB") {} -void SerialKeyboardImpl::init() { - if (!INPUTBROKER_SERIAL_TYPE) { - disable(); - return; - } +void SerialKeyboardImpl::init() +{ + if (!INPUTBROKER_SERIAL_TYPE) { + disable(); + return; + } - inputBroker->registerSource(this); + inputBroker->registerSource(this); } #endif // INPUTBROKER_SERIAL_TYPE \ No newline at end of file diff --git a/src/input/SerialKeyboardImpl.h b/src/input/SerialKeyboardImpl.h index d8908b81b..7f62aa420 100644 --- a/src/input/SerialKeyboardImpl.h +++ b/src/input/SerialKeyboardImpl.h @@ -9,10 +9,11 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class SerialKeyboardImpl : public SerialKeyboard { -public: - SerialKeyboardImpl(); - void init(); +class SerialKeyboardImpl : public SerialKeyboard +{ + public: + SerialKeyboardImpl(); + void init(); }; extern SerialKeyboardImpl *aSerialKeyboardImpl; \ No newline at end of file diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index 5c77cab30..bd8338acf 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -44,88 +44,94 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = { TCA8418Keyboard::TCA8418Keyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0), - should_backspace(false) {} - -void TCA8418Keyboard::reset() { - TCA8418KeyboardBase::reset(); - - // Set COL9 as GPIO output - writeRegister(TCA8418_REG_GPIO_DIR_3, 0x02); - // Switch off keyboard backlight (COL9 = LOW) - writeRegister(TCA8418_REG_GPIO_DAT_OUT_3, 0x00); + should_backspace(false) +{ } -void TCA8418Keyboard::pressed(uint8_t key) { - if (state == Init || state == Busy) { - return; - } - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; +void TCA8418Keyboard::reset() +{ + TCA8418KeyboardBase::reset(); - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } - - // Compute key index based on dynamic row/column - next_key = row * _TCA8418_COLS + col; - - // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); - - state = Held; - uint32_t now = millis(); - tap_interval = now - last_tap; - if (tap_interval < 0) { - // Long running, millis has overflowed. - last_tap = 0; - state = Busy; - return; - } - - // Check if the key is the same as the last one or if the time interval has passed - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; // Reset char index if new key or long press - should_backspace = false; // dont backspace on new key - } else { - char_idx += 1; // Cycle through characters if same key pressed - should_backspace = true; // allow backspace on same key - } - - // Store the current key as the last key - last_key = next_key; - last_tap = now; + // Set COL9 as GPIO output + writeRegister(TCA8418_REG_GPIO_DIR_3, 0x02); + // Switch off keyboard backlight (COL9 = LOW) + writeRegister(TCA8418_REG_GPIO_DAT_OUT_3, 0x00); } -void TCA8418Keyboard::released() { - if (state != Held) { - return; - } +void TCA8418Keyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; - if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; - state = Idle; - return; - } - uint32_t now = millis(); - int32_t held_interval = now - last_tap; - last_tap = now; - if (tap_interval < _TCA8418_MULTI_TAP_THRESHOLD && should_backspace) { - queueEvent(BSP); - } - if (held_interval > _TCA8418_LONG_PRESS_THRESHOLD) { - queueEvent(TCA8418LongPressMap[last_key]); - // LOG_DEBUG("Long Press Key: %i Map: %i", last_key, TCA8418LongPressMap[last_key]); - } else { - queueEvent(TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); - // LOG_DEBUG("Key Press: %i Index:%i if %i Map: %c", last_key, char_idx, TCA8418TapMod[last_key], - // TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); - } + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + // Compute key index based on dynamic row/column + next_key = row * _TCA8418_COLS + col; + + // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); + + state = Held; + uint32_t now = millis(); + tap_interval = now - last_tap; + if (tap_interval < 0) { + // Long running, millis has overflowed. + last_tap = 0; + state = Busy; + return; + } + + // Check if the key is the same as the last one or if the time interval has passed + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; // Reset char index if new key or long press + should_backspace = false; // dont backspace on new key + } else { + char_idx += 1; // Cycle through characters if same key pressed + should_backspace = true; // allow backspace on same key + } + + // Store the current key as the last key + last_key = next_key; + last_tap = now; } -void TCA8418Keyboard::setBacklight(bool on) { - if (on) { - digitalWrite(TCA8418_COL9, HIGH); - } else { - digitalWrite(TCA8418_COL9, LOW); - } +void TCA8418Keyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = -1; + state = Idle; + return; + } + uint32_t now = millis(); + int32_t held_interval = now - last_tap; + last_tap = now; + if (tap_interval < _TCA8418_MULTI_TAP_THRESHOLD && should_backspace) { + queueEvent(BSP); + } + if (held_interval > _TCA8418_LONG_PRESS_THRESHOLD) { + queueEvent(TCA8418LongPressMap[last_key]); + // LOG_DEBUG("Long Press Key: %i Map: %i", last_key, TCA8418LongPressMap[last_key]); + } else { + queueEvent(TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); + // LOG_DEBUG("Key Press: %i Index:%i if %i Map: %c", last_key, char_idx, TCA8418TapMod[last_key], + // TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); + } +} + +void TCA8418Keyboard::setBacklight(bool on) +{ + if (on) { + digitalWrite(TCA8418_COL9, HIGH); + } else { + digitalWrite(TCA8418_COL9, LOW); + } } diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index 62b8505e0..b76916643 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -3,20 +3,21 @@ /** * @brief 3x4 keypad with 3 columns and 4 rows */ -class TCA8418Keyboard : public TCA8418KeyboardBase { -public: - TCA8418Keyboard(); - void reset(void) override; - void setBacklight(bool on) override; +class TCA8418Keyboard : public TCA8418KeyboardBase +{ + public: + TCA8418Keyboard(); + void reset(void) override; + void setBacklight(bool on) override; -protected: - void pressed(uint8_t key) override; - void released(void) override; + protected: + void pressed(uint8_t key) override; + void released(void) override; - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; - bool should_backspace; + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; + bool should_backspace; }; diff --git a/src/input/TCA8418KeyboardBase.cpp b/src/input/TCA8418KeyboardBase.cpp index 7c2d114c8..00aed9962 100644 --- a/src/input/TCA8418KeyboardBase.cpp +++ b/src/input/TCA8418KeyboardBase.cpp @@ -32,314 +32,346 @@ #define _TCA8418_REG_LCK_EC_KLEC_0 0x01 // Key event count bit 0 TCA8418KeyboardBase::TCA8418KeyboardBase(uint8_t rows, uint8_t columns) - : rows(rows), columns(columns), state(Init), queue(""), m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) {} - -void TCA8418KeyboardBase::begin(uint8_t addr, TwoWire *wire) { - m_addr = addr; - m_wire = wire; - m_wire->begin(); - reset(); + : rows(rows), columns(columns), state(Init), queue(""), m_wire(nullptr), m_addr(0), readCallback(nullptr), + writeCallback(nullptr) +{ } -void TCA8418KeyboardBase::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { - m_addr = addr; - m_wire = nullptr; - writeCallback = w; - readCallback = r; - reset(); -} - -void TCA8418KeyboardBase::reset() { - LOG_DEBUG("TCA8418 Reset"); - // GPIO - // set default all GIO pins to INPUT - writeRegister(TCA8418_REG_GPIO_DIR_1, 0x00); - writeRegister(TCA8418_REG_GPIO_DIR_2, 0x00); - writeRegister(TCA8418_REG_GPIO_DIR_3, 0x00); - - // add all pins to key events - writeRegister(TCA8418_REG_GPI_EM_1, 0xFF); - writeRegister(TCA8418_REG_GPI_EM_2, 0xFF); - writeRegister(TCA8418_REG_GPI_EM_3, 0xFF); - - // set all pins to FALLING interrupts - writeRegister(TCA8418_REG_GPIO_INT_LVL_1, 0x00); - writeRegister(TCA8418_REG_GPIO_INT_LVL_2, 0x00); - writeRegister(TCA8418_REG_GPIO_INT_LVL_3, 0x00); - - // add all pins to interrupts - writeRegister(TCA8418_REG_GPIO_INT_EN_1, 0xFF); - writeRegister(TCA8418_REG_GPIO_INT_EN_2, 0xFF); - writeRegister(TCA8418_REG_GPIO_INT_EN_3, 0xFF); - - // Set keyboard matrix size - matrix(rows, columns); - enableDebounce(); - flush(); - state = Idle; -} - -bool TCA8418KeyboardBase::matrix(uint8_t rows, uint8_t columns) { - if (rows < 1 || rows > 8 || columns < 1 || columns > 10) - return false; - - // Setup the keypad matrix. - uint8_t mask = 0x00; - for (int r = 0; r < rows; r++) { - mask <<= 1; - mask |= 1; - } - writeRegister(TCA8418_REG_KP_GPIO_1, mask); - - mask = 0x00; - for (int c = 0; c < columns && c < 8; c++) { - mask <<= 1; - mask |= 1; - } - writeRegister(TCA8418_REG_KP_GPIO_2, mask); - - if (columns > 8) { - if (columns == 9) - mask = 0x01; - else - mask = 0x03; - writeRegister(TCA8418_REG_KP_GPIO_3, mask); - } - - return true; -} - -uint8_t TCA8418KeyboardBase::keyCount() const { - uint8_t eventCount = readRegister(TCA8418_REG_KEY_LCK_EC); - eventCount &= 0x0F; // lower 4 bits only - return eventCount; -} - -bool TCA8418KeyboardBase::hasEvent() const { return queue.length() > 0; } - -void TCA8418KeyboardBase::queueEvent(char next) { - if (next == NONE) { - return; - } - queue.concat(next); -} - -char TCA8418KeyboardBase::dequeueEvent() { - if (queue.length() < 1) { - return NONE; - } - char next = queue.charAt(0); - queue.remove(0, 1); - return next; -} - -void TCA8418KeyboardBase::trigger() { - if (keyCount() == 0) { - return; - } - if (state != Init) { - // Read the key register - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A); - uint8_t key = k & 0x7F; - if (k & 0x80) { - if (state == Idle) - pressed(key); - return; - } else { - if (state == Held) { - released(); - } - state = Idle; - return; - } - } else { +void TCA8418KeyboardBase::begin(uint8_t addr, TwoWire *wire) +{ + m_addr = addr; + m_wire = wire; + m_wire->begin(); reset(); - } } -void TCA8418KeyboardBase::pressed(uint8_t key) { - // must be defined in derived class - LOG_ERROR("pressed() not implemented in derived class"); +void TCA8418KeyboardBase::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) +{ + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); } -void TCA8418KeyboardBase::released() { - // must be defined in derived class - LOG_ERROR("released() not implemented in derived class"); +void TCA8418KeyboardBase::reset() +{ + LOG_DEBUG("TCA8418 Reset"); + // GPIO + // set default all GIO pins to INPUT + writeRegister(TCA8418_REG_GPIO_DIR_1, 0x00); + writeRegister(TCA8418_REG_GPIO_DIR_2, 0x00); + writeRegister(TCA8418_REG_GPIO_DIR_3, 0x00); + + // add all pins to key events + writeRegister(TCA8418_REG_GPI_EM_1, 0xFF); + writeRegister(TCA8418_REG_GPI_EM_2, 0xFF); + writeRegister(TCA8418_REG_GPI_EM_3, 0xFF); + + // set all pins to FALLING interrupts + writeRegister(TCA8418_REG_GPIO_INT_LVL_1, 0x00); + writeRegister(TCA8418_REG_GPIO_INT_LVL_2, 0x00); + writeRegister(TCA8418_REG_GPIO_INT_LVL_3, 0x00); + + // add all pins to interrupts + writeRegister(TCA8418_REG_GPIO_INT_EN_1, 0xFF); + writeRegister(TCA8418_REG_GPIO_INT_EN_2, 0xFF); + writeRegister(TCA8418_REG_GPIO_INT_EN_3, 0xFF); + + // Set keyboard matrix size + matrix(rows, columns); + enableDebounce(); + flush(); + state = Idle; } -uint8_t TCA8418KeyboardBase::flush() { - // Flush key events - uint8_t count = 0; - while (readRegister(TCA8418_REG_KEY_EVENT_A) != 0) - count++; +bool TCA8418KeyboardBase::matrix(uint8_t rows, uint8_t columns) +{ + if (rows < 1 || rows > 8 || columns < 1 || columns > 10) + return false; - // Flush gpio events - readRegister(TCA8418_REG_GPIO_INT_STAT_1); - readRegister(TCA8418_REG_GPIO_INT_STAT_2); - readRegister(TCA8418_REG_GPIO_INT_STAT_3); + // Setup the keypad matrix. + uint8_t mask = 0x00; + for (int r = 0; r < rows; r++) { + mask <<= 1; + mask |= 1; + } + writeRegister(TCA8418_REG_KP_GPIO_1, mask); - // Clear INT_STAT register - writeRegister(TCA8418_REG_INT_STAT, 3); - return count; + mask = 0x00; + for (int c = 0; c < columns && c < 8; c++) { + mask <<= 1; + mask |= 1; + } + writeRegister(TCA8418_REG_KP_GPIO_2, mask); + + if (columns > 8) { + if (columns == 9) + mask = 0x01; + else + mask = 0x03; + writeRegister(TCA8418_REG_KP_GPIO_3, mask); + } + + return true; } -void TCA8418KeyboardBase::clearInt() { writeRegister(TCA8418_REG_INT_STAT, 3); } - -uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const { - if (pinnum > TCA8418_COL9) - return 0xFF; - - uint8_t reg = TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8; - uint8_t mask = (1 << (pinnum % 8)); - - // Level 0 = low other = high - uint8_t value = readRegister(reg); - if (value & mask) - return HIGH; - return LOW; +uint8_t TCA8418KeyboardBase::keyCount() const +{ + uint8_t eventCount = readRegister(TCA8418_REG_KEY_LCK_EC); + eventCount &= 0x0F; // lower 4 bits only + return eventCount; } -bool TCA8418KeyboardBase::digitalWrite(uint8_t pinnum, uint8_t level) { - if (pinnum > TCA8418_COL9) - return false; +bool TCA8418KeyboardBase::hasEvent() const +{ + return queue.length() > 0; +} - uint8_t reg = TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8; - uint8_t mask = (1 << (pinnum % 8)); +void TCA8418KeyboardBase::queueEvent(char next) +{ + if (next == NONE) { + return; + } + queue.concat(next); +} - // Level 0 = low other = high - uint8_t value = readRegister(reg); - if (level == LOW) - value &= ~mask; - else +char TCA8418KeyboardBase::dequeueEvent() +{ + if (queue.length() < 1) { + return NONE; + } + char next = queue.charAt(0); + queue.remove(0, 1); + return next; +} + +void TCA8418KeyboardBase::trigger() +{ + if (keyCount() == 0) { + return; + } + if (state != Init) { + // Read the key register + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A); + uint8_t key = k & 0x7F; + if (k & 0x80) { + if (state == Idle) + pressed(key); + return; + } else { + if (state == Held) { + released(); + } + state = Idle; + return; + } + } else { + reset(); + } +} + +void TCA8418KeyboardBase::pressed(uint8_t key) +{ + // must be defined in derived class + LOG_ERROR("pressed() not implemented in derived class"); +} + +void TCA8418KeyboardBase::released() +{ + // must be defined in derived class + LOG_ERROR("released() not implemented in derived class"); +} + +uint8_t TCA8418KeyboardBase::flush() +{ + // Flush key events + uint8_t count = 0; + while (readRegister(TCA8418_REG_KEY_EVENT_A) != 0) + count++; + + // Flush gpio events + readRegister(TCA8418_REG_GPIO_INT_STAT_1); + readRegister(TCA8418_REG_GPIO_INT_STAT_2); + readRegister(TCA8418_REG_GPIO_INT_STAT_3); + + // Clear INT_STAT register + writeRegister(TCA8418_REG_INT_STAT, 3); + return count; +} + +void TCA8418KeyboardBase::clearInt() +{ + writeRegister(TCA8418_REG_INT_STAT, 3); +} + +uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const +{ + if (pinnum > TCA8418_COL9) + return 0xFF; + + uint8_t reg = TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8; + uint8_t mask = (1 << (pinnum % 8)); + + // Level 0 = low other = high + uint8_t value = readRegister(reg); + if (value & mask) + return HIGH; + return LOW; +} + +bool TCA8418KeyboardBase::digitalWrite(uint8_t pinnum, uint8_t level) +{ + if (pinnum > TCA8418_COL9) + return false; + + uint8_t reg = TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8; + uint8_t mask = (1 << (pinnum % 8)); + + // Level 0 = low other = high + uint8_t value = readRegister(reg); + if (level == LOW) + value &= ~mask; + else + value |= mask; + writeRegister(reg, value); + return true; +} + +bool TCA8418KeyboardBase::pinMode(uint8_t pinnum, uint8_t mode) +{ + if (pinnum > TCA8418_COL9) + return false; + + uint8_t idx = pinnum / 8; + uint8_t reg = TCA8418_REG_GPIO_DIR_1 + idx; + uint8_t mask = (1 << (pinnum % 8)); + + // Mode 0 = input 1 = output + uint8_t value = readRegister(reg); + if (mode == OUTPUT) + value |= mask; + else + value &= ~mask; + writeRegister(reg, value); + + // Pullup 0 = enabled 1 = disabled + reg = TCA8418_REG_GPIO_PULL_1 + idx; + value = readRegister(reg); + if (mode == INPUT_PULLUP) + value &= ~mask; + else + value |= mask; + writeRegister(reg, value); + + return true; +} + +bool TCA8418KeyboardBase::pinIRQMode(uint8_t pinnum, uint8_t mode) +{ + if (pinnum > TCA8418_COL9) + return false; + if ((mode != RISING) && (mode != FALLING)) + return false; + + // Mode 0 = falling 1 = rising + uint8_t idx = pinnum / 8; + uint8_t reg = TCA8418_REG_GPIO_INT_LVL_1 + idx; + uint8_t mask = (1 << (pinnum % 8)); + + uint8_t value = readRegister(reg); + if (mode == RISING) + value |= mask; + else + value &= ~mask; + writeRegister(reg, value); + + // Enable interrupt + reg = TCA8418_REG_GPIO_INT_EN_1 + idx; + value = readRegister(reg); value |= mask; - writeRegister(reg, value); - return true; + writeRegister(reg, value); + + return true; } -bool TCA8418KeyboardBase::pinMode(uint8_t pinnum, uint8_t mode) { - if (pinnum > TCA8418_COL9) - return false; - - uint8_t idx = pinnum / 8; - uint8_t reg = TCA8418_REG_GPIO_DIR_1 + idx; - uint8_t mask = (1 << (pinnum % 8)); - - // Mode 0 = input 1 = output - uint8_t value = readRegister(reg); - if (mode == OUTPUT) - value |= mask; - else - value &= ~mask; - writeRegister(reg, value); - - // Pullup 0 = enabled 1 = disabled - reg = TCA8418_REG_GPIO_PULL_1 + idx; - value = readRegister(reg); - if (mode == INPUT_PULLUP) - value &= ~mask; - else - value |= mask; - writeRegister(reg, value); - - return true; -} - -bool TCA8418KeyboardBase::pinIRQMode(uint8_t pinnum, uint8_t mode) { - if (pinnum > TCA8418_COL9) - return false; - if ((mode != RISING) && (mode != FALLING)) - return false; - - // Mode 0 = falling 1 = rising - uint8_t idx = pinnum / 8; - uint8_t reg = TCA8418_REG_GPIO_INT_LVL_1 + idx; - uint8_t mask = (1 << (pinnum % 8)); - - uint8_t value = readRegister(reg); - if (mode == RISING) - value |= mask; - else - value &= ~mask; - writeRegister(reg, value); - - // Enable interrupt - reg = TCA8418_REG_GPIO_INT_EN_1 + idx; - value = readRegister(reg); - value |= mask; - writeRegister(reg, value); - - return true; -} - -void TCA8418KeyboardBase::enableInterrupts() { - uint8_t value = readRegister(TCA8418_REG_CFG); - value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); - writeRegister(TCA8418_REG_CFG, value); +void TCA8418KeyboardBase::enableInterrupts() +{ + uint8_t value = readRegister(TCA8418_REG_CFG); + value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); + writeRegister(TCA8418_REG_CFG, value); }; -void TCA8418KeyboardBase::disableInterrupts() { - uint8_t value = readRegister(TCA8418_REG_CFG); - value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); - writeRegister(TCA8418_REG_CFG, value); +void TCA8418KeyboardBase::disableInterrupts() +{ + uint8_t value = readRegister(TCA8418_REG_CFG); + value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); + writeRegister(TCA8418_REG_CFG, value); }; -void TCA8418KeyboardBase::enableMatrixOverflow() { - uint8_t value = readRegister(TCA8418_REG_CFG); - value |= _TCA8418_REG_CFG_OVR_FLOW_M; - writeRegister(TCA8418_REG_CFG, value); +void TCA8418KeyboardBase::enableMatrixOverflow() +{ + uint8_t value = readRegister(TCA8418_REG_CFG); + value |= _TCA8418_REG_CFG_OVR_FLOW_M; + writeRegister(TCA8418_REG_CFG, value); }; -void TCA8418KeyboardBase::disableMatrixOverflow() { - uint8_t value = readRegister(TCA8418_REG_CFG); - value &= ~_TCA8418_REG_CFG_OVR_FLOW_M; - writeRegister(TCA8418_REG_CFG, value); +void TCA8418KeyboardBase::disableMatrixOverflow() +{ + uint8_t value = readRegister(TCA8418_REG_CFG); + value &= ~_TCA8418_REG_CFG_OVR_FLOW_M; + writeRegister(TCA8418_REG_CFG, value); }; -void TCA8418KeyboardBase::enableDebounce() { - writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0x00); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0x00); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0x00); +void TCA8418KeyboardBase::enableDebounce() +{ + writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0x00); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0x00); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0x00); } -void TCA8418KeyboardBase::disableDebounce() { - writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0xFF); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0xFF); - writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0xFF); +void TCA8418KeyboardBase::disableDebounce() +{ + writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0xFF); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0xFF); + writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0xFF); } void TCA8418KeyboardBase::setBacklight(bool on) {} -uint8_t TCA8418KeyboardBase::readRegister(uint8_t reg) const { - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(reg); - m_wire->endTransmission(); +uint8_t TCA8418KeyboardBase::readRegister(uint8_t reg) const +{ + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); - m_wire->requestFrom(m_addr, (uint8_t)1); - if (m_wire->available() < 1) - return 0; + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) + return 0; - return m_wire->read(); - } - if (readCallback) { - uint8_t data; - readCallback(m_addr, reg, &data, 1); - return data; - } - return 0; + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; } -void TCA8418KeyboardBase::writeRegister(uint8_t reg, uint8_t value) { - uint8_t data[2]; - data[0] = reg; - data[1] = value; +void TCA8418KeyboardBase::writeRegister(uint8_t reg, uint8_t value) +{ + uint8_t data[2]; + data[0] = reg; + data[1] = value; - if (m_wire) { - m_wire->beginTransmission(m_addr); - m_wire->write(data, sizeof(uint8_t) * 2); - m_wire->endTransmission(); - } - if (writeCallback) { - writeCallback(m_addr, data[0], &(data[1]), 1); - } + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(data, sizeof(uint8_t) * 2); + m_wire->endTransmission(); + } + if (writeCallback) { + writeCallback(m_addr, data[0], &(data[1]), 1); + } } \ No newline at end of file diff --git a/src/input/TCA8418KeyboardBase.h b/src/input/TCA8418KeyboardBase.h index 82f7a4363..8e509ac7e 100644 --- a/src/input/TCA8418KeyboardBase.h +++ b/src/input/TCA8418KeyboardBase.h @@ -8,164 +8,165 @@ * and handling key states. It is designed to be extended for specific keyboard implementations. * It supports both I2C communication and function pointers for custom I2C operations. */ -class TCA8418KeyboardBase { -public: - enum TCA8418Key : uint8_t { - NONE = 0x00, - BSP = 0x08, - TAB = 0x09, - SELECT = 0x0d, - ESC = 0x1b, - REBOOT = 0x90, - LEFT = 0xb4, - UP = 0xb5, - DOWN = 0xb6, - RIGHT = 0xb7, - BT_TOGGLE = 0xAA, - GPS_TOGGLE = 0x9E, - MUTE_TOGGLE = 0xAC, - SEND_PING = 0xAF, - BL_TOGGLE = 0xAB - }; +class TCA8418KeyboardBase +{ + public: + enum TCA8418Key : uint8_t { + NONE = 0x00, + BSP = 0x08, + TAB = 0x09, + SELECT = 0x0d, + ESC = 0x1b, + REBOOT = 0x90, + LEFT = 0xb4, + UP = 0xb5, + DOWN = 0xb6, + RIGHT = 0xb7, + BT_TOGGLE = 0xAA, + GPS_TOGGLE = 0x9E, + MUTE_TOGGLE = 0xAC, + SEND_PING = 0xAF, + BL_TOGGLE = 0xAB + }; - typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); - TCA8418KeyboardBase(uint8_t rows, uint8_t columns); + TCA8418KeyboardBase(uint8_t rows, uint8_t columns); - virtual void begin(uint8_t addr = TCA8418_KB_ADDR, TwoWire *wire = &Wire); - virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR); + virtual void begin(uint8_t addr = TCA8418_KB_ADDR, TwoWire *wire = &Wire); + virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR); - virtual void reset(void); - void clearInt(void); + virtual void reset(void); + void clearInt(void); - virtual void trigger(void); + virtual void trigger(void); - virtual void setBacklight(bool on); + virtual void setBacklight(bool on); - // Key events available - virtual bool hasEvent(void) const; - virtual char dequeueEvent(void); + // Key events available + virtual bool hasEvent(void) const; + virtual char dequeueEvent(void); -protected: - enum KeyState { Init, Idle, Held, Busy }; + protected: + enum KeyState { Init, Idle, Held, Busy }; - enum TCA8418Register : uint8_t { - TCA8418_REG_RESERVED = 0x00, - TCA8418_REG_CFG = 0x01, - TCA8418_REG_INT_STAT = 0x02, - TCA8418_REG_KEY_LCK_EC = 0x03, - TCA8418_REG_KEY_EVENT_A = 0x04, - TCA8418_REG_KEY_EVENT_B = 0x05, - TCA8418_REG_KEY_EVENT_C = 0x06, - TCA8418_REG_KEY_EVENT_D = 0x07, - TCA8418_REG_KEY_EVENT_E = 0x08, - TCA8418_REG_KEY_EVENT_F = 0x09, - TCA8418_REG_KEY_EVENT_G = 0x0A, - TCA8418_REG_KEY_EVENT_H = 0x0B, - TCA8418_REG_KEY_EVENT_I = 0x0C, - TCA8418_REG_KEY_EVENT_J = 0x0D, - TCA8418_REG_KP_LCK_TIMER = 0x0E, - TCA8418_REG_UNLOCK_1 = 0x0F, - TCA8418_REG_UNLOCK_2 = 0x10, - TCA8418_REG_GPIO_INT_STAT_1 = 0x11, - TCA8418_REG_GPIO_INT_STAT_2 = 0x12, - TCA8418_REG_GPIO_INT_STAT_3 = 0x13, - TCA8418_REG_GPIO_DAT_STAT_1 = 0x14, - TCA8418_REG_GPIO_DAT_STAT_2 = 0x15, - TCA8418_REG_GPIO_DAT_STAT_3 = 0x16, - TCA8418_REG_GPIO_DAT_OUT_1 = 0x17, - TCA8418_REG_GPIO_DAT_OUT_2 = 0x18, - TCA8418_REG_GPIO_DAT_OUT_3 = 0x19, - TCA8418_REG_GPIO_INT_EN_1 = 0x1A, - TCA8418_REG_GPIO_INT_EN_2 = 0x1B, - TCA8418_REG_GPIO_INT_EN_3 = 0x1C, - TCA8418_REG_KP_GPIO_1 = 0x1D, - TCA8418_REG_KP_GPIO_2 = 0x1E, - TCA8418_REG_KP_GPIO_3 = 0x1F, - TCA8418_REG_GPI_EM_1 = 0x20, - TCA8418_REG_GPI_EM_2 = 0x21, - TCA8418_REG_GPI_EM_3 = 0x22, - TCA8418_REG_GPIO_DIR_1 = 0x23, - TCA8418_REG_GPIO_DIR_2 = 0x24, - TCA8418_REG_GPIO_DIR_3 = 0x25, - TCA8418_REG_GPIO_INT_LVL_1 = 0x26, - TCA8418_REG_GPIO_INT_LVL_2 = 0x27, - TCA8418_REG_GPIO_INT_LVL_3 = 0x28, - TCA8418_REG_DEBOUNCE_DIS_1 = 0x29, - TCA8418_REG_DEBOUNCE_DIS_2 = 0x2A, - TCA8418_REG_DEBOUNCE_DIS_3 = 0x2B, - TCA8418_REG_GPIO_PULL_1 = 0x2C, - TCA8418_REG_GPIO_PULL_2 = 0x2D, - TCA8418_REG_GPIO_PULL_3 = 0x2E - }; + enum TCA8418Register : uint8_t { + TCA8418_REG_RESERVED = 0x00, + TCA8418_REG_CFG = 0x01, + TCA8418_REG_INT_STAT = 0x02, + TCA8418_REG_KEY_LCK_EC = 0x03, + TCA8418_REG_KEY_EVENT_A = 0x04, + TCA8418_REG_KEY_EVENT_B = 0x05, + TCA8418_REG_KEY_EVENT_C = 0x06, + TCA8418_REG_KEY_EVENT_D = 0x07, + TCA8418_REG_KEY_EVENT_E = 0x08, + TCA8418_REG_KEY_EVENT_F = 0x09, + TCA8418_REG_KEY_EVENT_G = 0x0A, + TCA8418_REG_KEY_EVENT_H = 0x0B, + TCA8418_REG_KEY_EVENT_I = 0x0C, + TCA8418_REG_KEY_EVENT_J = 0x0D, + TCA8418_REG_KP_LCK_TIMER = 0x0E, + TCA8418_REG_UNLOCK_1 = 0x0F, + TCA8418_REG_UNLOCK_2 = 0x10, + TCA8418_REG_GPIO_INT_STAT_1 = 0x11, + TCA8418_REG_GPIO_INT_STAT_2 = 0x12, + TCA8418_REG_GPIO_INT_STAT_3 = 0x13, + TCA8418_REG_GPIO_DAT_STAT_1 = 0x14, + TCA8418_REG_GPIO_DAT_STAT_2 = 0x15, + TCA8418_REG_GPIO_DAT_STAT_3 = 0x16, + TCA8418_REG_GPIO_DAT_OUT_1 = 0x17, + TCA8418_REG_GPIO_DAT_OUT_2 = 0x18, + TCA8418_REG_GPIO_DAT_OUT_3 = 0x19, + TCA8418_REG_GPIO_INT_EN_1 = 0x1A, + TCA8418_REG_GPIO_INT_EN_2 = 0x1B, + TCA8418_REG_GPIO_INT_EN_3 = 0x1C, + TCA8418_REG_KP_GPIO_1 = 0x1D, + TCA8418_REG_KP_GPIO_2 = 0x1E, + TCA8418_REG_KP_GPIO_3 = 0x1F, + TCA8418_REG_GPI_EM_1 = 0x20, + TCA8418_REG_GPI_EM_2 = 0x21, + TCA8418_REG_GPI_EM_3 = 0x22, + TCA8418_REG_GPIO_DIR_1 = 0x23, + TCA8418_REG_GPIO_DIR_2 = 0x24, + TCA8418_REG_GPIO_DIR_3 = 0x25, + TCA8418_REG_GPIO_INT_LVL_1 = 0x26, + TCA8418_REG_GPIO_INT_LVL_2 = 0x27, + TCA8418_REG_GPIO_INT_LVL_3 = 0x28, + TCA8418_REG_DEBOUNCE_DIS_1 = 0x29, + TCA8418_REG_DEBOUNCE_DIS_2 = 0x2A, + TCA8418_REG_DEBOUNCE_DIS_3 = 0x2B, + TCA8418_REG_GPIO_PULL_1 = 0x2C, + TCA8418_REG_GPIO_PULL_2 = 0x2D, + TCA8418_REG_GPIO_PULL_3 = 0x2E + }; - // Pin IDs for matrix rows/columns - enum TCA8418PinId : uint8_t { - TCA8418_ROW0, // Pin ID for row 0 - TCA8418_ROW1, // Pin ID for row 1 - TCA8418_ROW2, // Pin ID for row 2 - TCA8418_ROW3, // Pin ID for row 3 - TCA8418_ROW4, // Pin ID for row 4 - TCA8418_ROW5, // Pin ID for row 5 - TCA8418_ROW6, // Pin ID for row 6 - TCA8418_ROW7, // Pin ID for row 7 - TCA8418_COL0, // Pin ID for column 0 - TCA8418_COL1, // Pin ID for column 1 - TCA8418_COL2, // Pin ID for column 2 - TCA8418_COL3, // Pin ID for column 3 - TCA8418_COL4, // Pin ID for column 4 - TCA8418_COL5, // Pin ID for column 5 - TCA8418_COL6, // Pin ID for column 6 - TCA8418_COL7, // Pin ID for column 7 - TCA8418_COL8, // Pin ID for column 8 - TCA8418_COL9 // Pin ID for column 9 - }; + // Pin IDs for matrix rows/columns + enum TCA8418PinId : uint8_t { + TCA8418_ROW0, // Pin ID for row 0 + TCA8418_ROW1, // Pin ID for row 1 + TCA8418_ROW2, // Pin ID for row 2 + TCA8418_ROW3, // Pin ID for row 3 + TCA8418_ROW4, // Pin ID for row 4 + TCA8418_ROW5, // Pin ID for row 5 + TCA8418_ROW6, // Pin ID for row 6 + TCA8418_ROW7, // Pin ID for row 7 + TCA8418_COL0, // Pin ID for column 0 + TCA8418_COL1, // Pin ID for column 1 + TCA8418_COL2, // Pin ID for column 2 + TCA8418_COL3, // Pin ID for column 3 + TCA8418_COL4, // Pin ID for column 4 + TCA8418_COL5, // Pin ID for column 5 + TCA8418_COL6, // Pin ID for column 6 + TCA8418_COL7, // Pin ID for column 7 + TCA8418_COL8, // Pin ID for column 8 + TCA8418_COL9 // Pin ID for column 9 + }; - virtual void pressed(uint8_t key); - virtual void released(void); + virtual void pressed(uint8_t key); + virtual void released(void); - virtual void queueEvent(char); + virtual void queueEvent(char); - virtual ~TCA8418KeyboardBase() {} + virtual ~TCA8418KeyboardBase() {} -protected: - // Set the size of the keypad matrix - // All other rows and columns are set as inputs. - bool matrix(uint8_t rows, uint8_t columns); + protected: + // Set the size of the keypad matrix + // All other rows and columns are set as inputs. + bool matrix(uint8_t rows, uint8_t columns); - uint8_t keyCount(void) const; + uint8_t keyCount(void) const; - // Flush all events in the FIFO buffer + GPIO events. - uint8_t flush(void); + // Flush all events in the FIFO buffer + GPIO events. + uint8_t flush(void); - // debounce keys. - void enableDebounce(); - void disableDebounce(); + // debounce keys. + void enableDebounce(); + void disableDebounce(); - // enable / disable interrupts for matrix and GPI pins - void enableInterrupts(); - void disableInterrupts(); + // enable / disable interrupts for matrix and GPI pins + void enableInterrupts(); + void disableInterrupts(); - // ignore key events when FIFO buffer is full or not. - void enableMatrixOverflow(); - void disableMatrixOverflow(); + // ignore key events when FIFO buffer is full or not. + void enableMatrixOverflow(); + void disableMatrixOverflow(); - uint8_t digitalRead(uint8_t pinnum) const; - bool digitalWrite(uint8_t pinnum, uint8_t level); - bool pinMode(uint8_t pinnum, uint8_t mode); - bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING - uint8_t readRegister(uint8_t reg) const; - void writeRegister(uint8_t reg, uint8_t value); + uint8_t digitalRead(uint8_t pinnum) const; + bool digitalWrite(uint8_t pinnum, uint8_t level); + bool pinMode(uint8_t pinnum, uint8_t mode); + bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING + uint8_t readRegister(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); -protected: - uint8_t rows; - uint8_t columns; - KeyState state; - String queue; + protected: + uint8_t rows; + uint8_t columns; + KeyState state; + String queue; -private: - TwoWire *m_wire; - uint8_t m_addr; - i2c_com_fptr_t readCallback; - i2c_com_fptr_t writeCallback; + private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; }; diff --git a/src/input/TDeckProKeyboard.cpp b/src/input/TDeckProKeyboard.cpp index 62056f2e7..eeafe4949 100644 --- a/src/input/TDeckProKeyboard.cpp +++ b/src/input/TDeckProKeyboard.cpp @@ -62,123 +62,135 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = { }; TDeckProKeyboard::TDeckProKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), - char_idx(0), tap_interval(0) {} + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ +} -void TDeckProKeyboard::reset() { - TCA8418KeyboardBase::reset(); - pinMode(KB_BL_PIN, OUTPUT); - setBacklight(false); +void TDeckProKeyboard::reset() +{ + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + setBacklight(false); } // handle multi-key presses (shift and alt) -void TDeckProKeyboard::trigger() { - uint8_t count = keyCount(); - if (count == 0) - return; - for (uint8_t i = 0; i < count; ++i) { - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); - uint8_t key = k & 0x7F; - if (k & 0x80) { - pressed(key); - } else { - released(); - state = Idle; +void TDeckProKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } } - } } -void TDeckProKeyboard::pressed(uint8_t key) { - if (state == Init || state == Busy) { - return; - } - if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { - modifierFlag = 0; - } +void TDeckProKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } - next_key = row * _TCA8418_COLS + col; - state = Held; + next_key = row * _TCA8418_COLS + col; + state = Held; - uint32_t now = millis(); - tap_interval = now - last_tap; + uint32_t now = millis(); + tap_interval = now - last_tap; - updateModifierFlag(next_key); - if (isModifierKey(next_key)) { - last_modifier_time = now; - } + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } - if (tap_interval < 0) { - last_tap = 0; - state = Busy; - return; - } + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; - } else { - char_idx += 1; - } + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } - last_key = next_key; - last_tap = now; + last_key = next_key; + last_tap = now; } -void TDeckProKeyboard::released() { - if (state != Held) { - return; - } +void TDeckProKeyboard::released() +{ + if (state != Held) { + return; + } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; - state = Idle; - return; - } + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } - uint32_t now = millis(); - last_tap = now; + uint32_t now = millis(); + last_tap = now; - if (TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]] == Key::BL_TOGGLE) { - toggleBacklight(); - return; - } + if (TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } - queueEvent(TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]]); - if (isModifierKey(last_key) == false) - modifierFlag = 0; + queueEvent(TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; } -void TDeckProKeyboard::setBacklight(bool on) { - if (on) { - digitalWrite(KB_BL_PIN, HIGH); - } else { - digitalWrite(KB_BL_PIN, LOW); - } +void TDeckProKeyboard::setBacklight(bool on) +{ + if (on) { + digitalWrite(KB_BL_PIN, HIGH); + } else { + digitalWrite(KB_BL_PIN, LOW); + } } -void TDeckProKeyboard::toggleBacklight(void) { digitalWrite(KB_BL_PIN, !digitalRead(KB_BL_PIN)); } - -void TDeckProKeyboard::updateModifierFlag(uint8_t key) { - if (key == modifierRightShiftKey) { - modifierFlag ^= modifierRightShift; - } else if (key == modifierLeftShiftKey) { - modifierFlag ^= modifierLeftShift; - } else if (key == modifierSymKey) { - modifierFlag ^= modifierSym; - } else if (key == modifierAltKey) { - modifierFlag ^= modifierAlt; - } +void TDeckProKeyboard::toggleBacklight(void) +{ + digitalWrite(KB_BL_PIN, !digitalRead(KB_BL_PIN)); } -bool TDeckProKeyboard::isModifierKey(uint8_t key) { - return (key == modifierRightShiftKey || key == modifierLeftShiftKey || key == modifierAltKey || key == modifierSymKey); +void TDeckProKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierLeftShiftKey) { + modifierFlag ^= modifierLeftShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } else if (key == modifierAltKey) { + modifierFlag ^= modifierAlt; + } +} + +bool TDeckProKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierLeftShiftKey || key == modifierAltKey || key == modifierSymKey); } #endif // T_DECK_PRO \ No newline at end of file diff --git a/src/input/TDeckProKeyboard.h b/src/input/TDeckProKeyboard.h index 14106c2f8..617f3f20b 100644 --- a/src/input/TDeckProKeyboard.h +++ b/src/input/TDeckProKeyboard.h @@ -1,26 +1,27 @@ #include "TCA8418KeyboardBase.h" -class TDeckProKeyboard : public TCA8418KeyboardBase { -public: - TDeckProKeyboard(); - void reset(void) override; - void trigger(void) override; - void setBacklight(bool on) override; +class TDeckProKeyboard : public TCA8418KeyboardBase +{ + public: + TDeckProKeyboard(); + void reset(void) override; + void trigger(void) override; + void setBacklight(bool on) override; -protected: - void pressed(uint8_t key) override; - void released(void) override; + protected: + void pressed(uint8_t key) override; + void released(void) override; - void updateModifierFlag(uint8_t key); - bool isModifierKey(uint8_t key); - void toggleBacklight(void); + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(void); -private: - uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed - uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp index f04bb04a8..9a4fd8679 100644 --- a/src/input/TLoraPagerKeyboard.cpp +++ b/src/input/TLoraPagerKeyboard.cpp @@ -29,7 +29,8 @@ constexpr uint8_t modifierSymKey = 21 - 1; constexpr uint8_t modifierSym = 0b0010; // Num chars per key, Modulus for rotating through characters -static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; +static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {'w', 'W', '2'}, @@ -64,156 +65,168 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {' ', 0x00, Key::BL_TOGGLE}}; TLoraPagerKeyboard::TLoraPagerKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), - char_idx(0), tap_interval(0) { + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); #else - ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); - ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); + ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); #endif - reset(); + reset(); } -void TLoraPagerKeyboard::reset(void) { - TCA8418KeyboardBase::reset(); - pinMode(KB_BL_PIN, OUTPUT); - digitalWrite(KB_BL_PIN, LOW); - setBacklight(false); +void TLoraPagerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + digitalWrite(KB_BL_PIN, LOW); + setBacklight(false); } // handle multi-key presses (shift and alt) -void TLoraPagerKeyboard::trigger() { - uint8_t count = keyCount(); - if (count == 0) - return; - for (uint8_t i = 0; i < count; ++i) { - uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); - uint8_t key = k & 0x7F; - if (k & 0x80) { - pressed(key); - } else { - released(); - state = Idle; +void TLoraPagerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } } - } } -void TLoraPagerKeyboard::setBacklight(bool on) { - uint32_t _brightness = 0; - if (on) - _brightness = brightness; +void TLoraPagerKeyboard::setBacklight(bool on) +{ + uint32_t _brightness = 0; + if (on) + _brightness = brightness; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - ledcWrite(KB_BL_PIN, _brightness); + ledcWrite(KB_BL_PIN, _brightness); #else - ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness); + ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness); #endif } -void TLoraPagerKeyboard::pressed(uint8_t key) { - if (state == Init || state == Busy) { - return; - } - if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || - config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { - hapticFeedback(); - } +void TLoraPagerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + hapticFeedback(); + } - if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { - modifierFlag = 0; - } + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } - uint8_t next_key = 0; - int row = (key - 1) / 10; - int col = (key - 1) % 10; + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; - if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { - return; // Invalid key - } + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } - next_key = row * _TCA8418_COLS + col; - state = Held; + next_key = row * _TCA8418_COLS + col; + state = Held; - uint32_t now = millis(); - tap_interval = now - last_tap; + uint32_t now = millis(); + tap_interval = now - last_tap; - updateModifierFlag(next_key); - if (isModifierKey(next_key)) { - last_modifier_time = now; - } + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } - if (tap_interval < 0) { - last_tap = 0; - state = Busy; - return; - } + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } - if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { - char_idx = 0; - } else { - char_idx += 1; - } + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } - last_key = next_key; - last_tap = now; + last_key = next_key; + last_tap = now; } -void TLoraPagerKeyboard::released() { - if (state != Held) { - return; - } +void TLoraPagerKeyboard::released() +{ + if (state != Held) { + return; + } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; - state = Idle; - return; - } + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } - uint32_t now = millis(); - last_tap = now; + uint32_t now = millis(); + last_tap = now; - if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { - toggleBacklight(); - return; - } + if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } - queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); - if (isModifierKey(last_key) == false) - modifierFlag = 0; + queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; } -void TLoraPagerKeyboard::hapticFeedback() { - drv.setWaveform(0, 14); // strong buzz 100% - drv.setWaveform(1, 0); // end waveform - drv.go(); +void TLoraPagerKeyboard::hapticFeedback() +{ + drv.setWaveform(0, 14); // strong buzz 100% + drv.setWaveform(1, 0); // end waveform + drv.go(); } // toggle brightness of the backlight in three steps -void TLoraPagerKeyboard::toggleBacklight(bool off) { - if (off) { - brightness = 0; - } else { - if (brightness == 0) { - brightness = 40; - } else if (brightness == 40) { - brightness = 127; - } else if (brightness >= 127) { - brightness = 0; +void TLoraPagerKeyboard::toggleBacklight(bool off) +{ + if (off) { + brightness = 0; + } else { + if (brightness == 0) { + brightness = 40; + } else if (brightness == 40) { + brightness = 127; + } else if (brightness >= 127) { + brightness = 0; + } } - } - LOG_DEBUG("Toggle backlight: %d", brightness); + LOG_DEBUG("Toggle backlight: %d", brightness); - setBacklight(true); + setBacklight(true); } -void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) { - if (key == modifierRightShiftKey) { - modifierFlag ^= modifierRightShift; - } else if (key == modifierSymKey) { - modifierFlag ^= modifierSym; - } +void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } } -bool TLoraPagerKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierSymKey); } +bool TLoraPagerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierSymKey); +} #endif \ No newline at end of file diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index 61333149f..f04d2ce6a 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -1,29 +1,30 @@ #include "TCA8418KeyboardBase.h" -class TLoraPagerKeyboard : public TCA8418KeyboardBase { -public: - TLoraPagerKeyboard(); - void reset(void); - void trigger(void) override; - void setBacklight(bool on) override; - virtual ~TLoraPagerKeyboard() {} +class TLoraPagerKeyboard : public TCA8418KeyboardBase +{ + public: + TLoraPagerKeyboard(); + void reset(void); + void trigger(void) override; + void setBacklight(bool on) override; + virtual ~TLoraPagerKeyboard() {} -protected: - void pressed(uint8_t key) override; - void released(void) override; - void hapticFeedback(void); + protected: + void pressed(uint8_t key) override; + void released(void) override; + void hapticFeedback(void); - void updateModifierFlag(uint8_t key); - bool isModifierKey(uint8_t key); - void toggleBacklight(bool off = false); + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(bool off = false); -private: - uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed - uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; - uint32_t last_tap; - uint8_t char_idx; - int32_t tap_interval; - uint32_t brightness = 0; + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; + uint32_t brightness = 0; }; diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index d3e353f6b..c2755980e 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -19,136 +19,141 @@ #endif TouchScreenBase::TouchScreenBase(const char *name, uint16_t width, uint16_t height) - : concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0), _last_y(0), _start(0), - _tapped(false), _originName(name) {} - -void TouchScreenBase::init(bool hasTouch) { - if (hasTouch) { - LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y); - this->setInterval(100); - } else { - disable(); - this->setInterval(UINT_MAX); - } + : concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0), + _last_y(0), _start(0), _tapped(false), _originName(name) +{ } -int32_t TouchScreenBase::runOnce() { - TouchEvent e; - e.touchEvent = static_cast(TOUCH_ACTION_NONE); - - // process touch events - int16_t x, y; - bool touched = getTouch(x, y); - if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen - touched = false; - if (touched) { - this->setInterval(20); - _last_x = x; - _last_y = y; - } - if (touched != _touchedOld) { - if (touched) { - hapticFeedback(); - _state = TOUCH_EVENT_OCCURRED; - _start = millis(); - _first_x = x; - _first_y = y; +void TouchScreenBase::init(bool hasTouch) +{ + if (hasTouch) { + LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y); + this->setInterval(100); } else { - _state = TOUCH_EVENT_CLEARED; - time_t duration = millis() - _start; - x = _last_x; - y = _last_y; - this->setInterval(50); - - // compute distance - int16_t dx = x - _first_x; - int16_t dy = y - _first_y; - uint16_t adx = abs(dx); - uint16_t ady = abs(dy); - - // swipe horizontal - if (adx > ady && adx > TOUCH_THRESHOLD_X) { - if (0 > dx) { // swipe right to left - e.touchEvent = static_cast(TOUCH_ACTION_LEFT); - LOG_DEBUG("action SWIPE: right to left"); - } else { // swipe left to right - e.touchEvent = static_cast(TOUCH_ACTION_RIGHT); - LOG_DEBUG("action SWIPE: left to right"); - } - } - // swipe vertical - else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { - if (0 > dy) { // swipe bottom to top - e.touchEvent = static_cast(TOUCH_ACTION_UP); - LOG_DEBUG("action SWIPE: bottom to top"); - } else { // swipe top to bottom - e.touchEvent = static_cast(TOUCH_ACTION_DOWN); - LOG_DEBUG("action SWIPE: top to bottom"); - } - } - // tap - else { - if (duration > 0 && duration < TIME_LONG_PRESS) { - if (_tapped) { - _tapped = false; - } else { - _tapped = true; - } - } else { - _tapped = false; - } - } + disable(); + this->setInterval(UINT_MAX); } - } - _touchedOld = touched; +} + +int32_t TouchScreenBase::runOnce() +{ + TouchEvent e; + e.touchEvent = static_cast(TOUCH_ACTION_NONE); + + // process touch events + int16_t x, y; + bool touched = getTouch(x, y); + if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen + touched = false; + if (touched) { + this->setInterval(20); + _last_x = x; + _last_y = y; + } + if (touched != _touchedOld) { + if (touched) { + hapticFeedback(); + _state = TOUCH_EVENT_OCCURRED; + _start = millis(); + _first_x = x; + _first_y = y; + } else { + _state = TOUCH_EVENT_CLEARED; + time_t duration = millis() - _start; + x = _last_x; + y = _last_y; + this->setInterval(50); + + // compute distance + int16_t dx = x - _first_x; + int16_t dy = y - _first_y; + uint16_t adx = abs(dx); + uint16_t ady = abs(dy); + + // swipe horizontal + if (adx > ady && adx > TOUCH_THRESHOLD_X) { + if (0 > dx) { // swipe right to left + e.touchEvent = static_cast(TOUCH_ACTION_LEFT); + LOG_DEBUG("action SWIPE: right to left"); + } else { // swipe left to right + e.touchEvent = static_cast(TOUCH_ACTION_RIGHT); + LOG_DEBUG("action SWIPE: left to right"); + } + } + // swipe vertical + else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { + if (0 > dy) { // swipe bottom to top + e.touchEvent = static_cast(TOUCH_ACTION_UP); + LOG_DEBUG("action SWIPE: bottom to top"); + } else { // swipe top to bottom + e.touchEvent = static_cast(TOUCH_ACTION_DOWN); + LOG_DEBUG("action SWIPE: top to bottom"); + } + } + // tap + else { + if (duration > 0 && duration < TIME_LONG_PRESS) { + if (_tapped) { + _tapped = false; + } else { + _tapped = true; + } + } else { + _tapped = false; + } + } + } + } + _touchedOld = touched; #if defined RAK14014 - // Speed up the processing speed of the keyboard in virtual keyboard mode - auto state = cannedMessageModule->getRunState(); - if (state == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - if (_tapped) { - _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_TAP); - LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + // Speed up the processing speed of the keyboard in virtual keyboard mode + auto state = cannedMessageModule->getRunState(); + if (state == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (_tapped) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + } + } else { + if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + } } - } else { - if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { - _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_TAP); - LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); - } - } #else - // fire TAP event when no 2nd tap occured within time - if (_tapped) { - _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_TAP); - LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); - } + // fire TAP event when no 2nd tap occured within time + if (_tapped) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); + } #endif - // fire LONG_PRESS event without the need for release - if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { - // tricky: prevent reoccurring events and another touch event when releasing - _start = millis() + 30000; - e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); - LOG_DEBUG("action LONG PRESS(%d/%d)", _last_x, _last_y); - } + // fire LONG_PRESS event without the need for release + if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { + // tricky: prevent reoccurring events and another touch event when releasing + _start = millis() + 30000; + e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); + LOG_DEBUG("action LONG PRESS(%d/%d)", _last_x, _last_y); + } - if (e.touchEvent != TOUCH_ACTION_NONE) { - e.source = this->_originName; - e.x = _last_x; - e.y = _last_y; - onEvent(e); - } + if (e.touchEvent != TOUCH_ACTION_NONE) { + e.source = this->_originName; + e.x = _last_x; + e.y = _last_y; + onEvent(e); + } - return interval; + return interval; } -void TouchScreenBase::hapticFeedback() { +void TouchScreenBase::hapticFeedback() +{ #ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 0); // end waveform - drv.go(); + drv.setWaveform(0, 75); + drv.setWaveform(1, 0); // end waveform + drv.go(); #endif } diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h index 67502fde6..90314cf02 100644 --- a/src/input/TouchScreenBase.h +++ b/src/input/TouchScreenBase.h @@ -6,49 +6,50 @@ #include "time.h" typedef struct _TouchEvent { - const char *source; - char touchEvent; - uint16_t x; - uint16_t y; + const char *source; + char touchEvent; + uint16_t x; + uint16_t y; } TouchEvent; -class TouchScreenBase : public Observable, public concurrency::OSThread { -public: - explicit TouchScreenBase(const char *name, uint16_t width, uint16_t height); - void init(bool hasTouch); +class TouchScreenBase : public Observable, public concurrency::OSThread +{ + public: + explicit TouchScreenBase(const char *name, uint16_t width, uint16_t height); + void init(bool hasTouch); -protected: - enum TouchScreenBaseStateType { TOUCH_EVENT_OCCURRED, TOUCH_EVENT_CLEARED }; + protected: + enum TouchScreenBaseStateType { TOUCH_EVENT_OCCURRED, TOUCH_EVENT_CLEARED }; - enum TouchScreenBaseEventType { - TOUCH_ACTION_NONE, - TOUCH_ACTION_UP, - TOUCH_ACTION_DOWN, - TOUCH_ACTION_LEFT, - TOUCH_ACTION_RIGHT, - TOUCH_ACTION_TAP, - TOUCH_ACTION_LONG_PRESS - }; + enum TouchScreenBaseEventType { + TOUCH_ACTION_NONE, + TOUCH_ACTION_UP, + TOUCH_ACTION_DOWN, + TOUCH_ACTION_LEFT, + TOUCH_ACTION_RIGHT, + TOUCH_ACTION_TAP, + TOUCH_ACTION_LONG_PRESS + }; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - virtual bool getTouch(int16_t &x, int16_t &y) = 0; - virtual void onEvent(const TouchEvent &event) = 0; + virtual bool getTouch(int16_t &x, int16_t &y) = 0; + virtual void onEvent(const TouchEvent &event) = 0; - volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED; - volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE; - void hapticFeedback(); + volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED; + volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE; + void hapticFeedback(); -protected: - uint16_t _display_width; - uint16_t _display_height; + protected: + uint16_t _display_width; + uint16_t _display_height; -private: - bool _touchedOld = false; // previous touch state - int16_t _first_x, _last_x; // horizontal swipe direction - int16_t _first_y, _last_y; // vertical swipe direction - time_t _start; // for LONG_PRESS - bool _tapped; // for DOUBLE_TAP + private: + bool _touchedOld = false; // previous touch state + int16_t _first_x, _last_x; // horizontal swipe direction + int16_t _first_y, _last_y; // vertical swipe direction + time_t _start; // for LONG_PRESS + bool _tapped; // for DOUBLE_TAP - const char *_originName; + const char *_originName; }; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index 8369cabb9..69dcab04e 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -11,26 +11,32 @@ TouchScreenImpl1 *touchScreenImpl1; TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)) - : TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch) {} + : TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch) +{ +} -void TouchScreenImpl1::init() { +void TouchScreenImpl1::init() +{ #if ARCH_PORTDUINO - if (portduino_config.touchscreenModule) { + if (portduino_config.touchscreenModule) { + TouchScreenBase::init(true); + inputBroker->registerSource(this); + } else { + TouchScreenBase::init(false); + } +#elif !HAS_TOUCHSCREEN + TouchScreenBase::init(false); + return; +#else TouchScreenBase::init(true); inputBroker->registerSource(this); - } else { - TouchScreenBase::init(false); - } -#elif !HAS_TOUCHSCREEN - TouchScreenBase::init(false); - return; -#else - TouchScreenBase::init(true); - inputBroker->registerSource(this); #endif } -bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) { return _getTouch(&x, &y); } +bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) +{ + return _getTouch(&x, &y); +} /** * @brief forward touchscreen event @@ -39,40 +45,41 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) { return _getTouch(&x, & * * The touchscreen events are translated to input events and reversed */ -void TouchScreenImpl1::onEvent(const TouchEvent &event) { - InputEvent e = {}; - e.source = event.source; - e.kbchar = 0; - e.touchX = event.x; - e.touchY = event.y; +void TouchScreenImpl1::onEvent(const TouchEvent &event) +{ + InputEvent e = {}; + e.source = event.source; + e.kbchar = 0; + e.touchX = event.x; + e.touchY = event.y; - switch (event.touchEvent) { - case TOUCH_ACTION_LEFT: { - e.inputEvent = INPUT_BROKER_LEFT; - break; - } - case TOUCH_ACTION_RIGHT: { - e.inputEvent = INPUT_BROKER_RIGHT; - break; - } - case TOUCH_ACTION_UP: { - e.inputEvent = INPUT_BROKER_UP; - break; - } - case TOUCH_ACTION_DOWN: { - e.inputEvent = INPUT_BROKER_DOWN; - break; - } - case TOUCH_ACTION_LONG_PRESS: { - e.inputEvent = INPUT_BROKER_SELECT; - break; - } - case TOUCH_ACTION_TAP: { - e.inputEvent = INPUT_BROKER_USER_PRESS; - break; - } - default: - return; - } - this->notifyObservers(&e); + switch (event.touchEvent) { + case TOUCH_ACTION_LEFT: { + e.inputEvent = INPUT_BROKER_LEFT; + break; + } + case TOUCH_ACTION_RIGHT: { + e.inputEvent = INPUT_BROKER_RIGHT; + break; + } + case TOUCH_ACTION_UP: { + e.inputEvent = INPUT_BROKER_UP; + break; + } + case TOUCH_ACTION_DOWN: { + e.inputEvent = INPUT_BROKER_DOWN; + break; + } + case TOUCH_ACTION_LONG_PRESS: { + e.inputEvent = INPUT_BROKER_SELECT; + break; + } + case TOUCH_ACTION_TAP: { + e.inputEvent = INPUT_BROKER_USER_PRESS; + break; + } + default: + return; + } + this->notifyObservers(&e); } \ No newline at end of file diff --git a/src/input/TouchScreenImpl1.h b/src/input/TouchScreenImpl1.h index 5d55442d9..0c5338459 100644 --- a/src/input/TouchScreenImpl1.h +++ b/src/input/TouchScreenImpl1.h @@ -1,16 +1,17 @@ #pragma once #include "TouchScreenBase.h" -class TouchScreenImpl1 : public TouchScreenBase { -public: - TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)); - void init(void); +class TouchScreenImpl1 : public TouchScreenBase +{ + public: + TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)); + void init(void); -protected: - virtual bool getTouch(int16_t &x, int16_t &y); - virtual void onEvent(const TouchEvent &event); + protected: + virtual bool getTouch(int16_t &x, int16_t &y); + virtual void onEvent(const TouchEvent &event); - bool (*_getTouch)(int16_t *, int16_t *); + bool (*_getTouch)(int16_t *, int16_t *); }; extern TouchScreenImpl1 *touchScreenImpl1; diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index bf9a82a33..bbd07e199 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -4,201 +4,219 @@ extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} -void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, - input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { - this->_pinDown = pinDown; - this->_pinUp = pinUp; - this->_pinLeft = pinLeft; - this->_pinRight = pinRight; - this->_pinPress = pinPress; - this->_eventDown = eventDown; - this->_eventUp = eventUp; - this->_eventLeft = eventLeft; - this->_eventRight = eventRight; - this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; +void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, + input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, + input_broker_event eventRight, input_broker_event eventPressed, + input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) +{ + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_pinLeft = pinLeft; + this->_pinRight = pinRight; + this->_pinPress = pinPress; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventLeft = eventLeft; + this->_eventRight = eventRight; + this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; - if (pinPress != 255) { - pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, TB_DIRECTION); - } - if (this->_pinDown != 255) { - pinMode(this->_pinDown, INPUT_PULLUP); - attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); - } - if (this->_pinUp != 255) { - pinMode(this->_pinUp, INPUT_PULLUP); - attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); - } - if (this->_pinLeft != 255) { - pinMode(this->_pinLeft, INPUT_PULLUP); - attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); - } - if (this->_pinRight != 255) { - pinMode(this->_pinRight, INPUT_PULLUP); - attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); - } + if (pinPress != 255) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, TB_DIRECTION); + } + if (this->_pinDown != 255) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); + } + if (this->_pinUp != 255) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); + } + if (this->_pinLeft != 255) { + pinMode(this->_pinLeft, INPUT_PULLUP); + attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); + } + if (this->_pinRight != 255) { + pinMode(this->_pinRight, INPUT_PULLUP); + attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); + } - LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, - pinPress); + LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, + this->_pinLeft, this->_pinRight, pinPress); #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif - this->setInterval(100); + this->setInterval(100); } -int32_t TrackballInterruptBase::runOnce() { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; +int32_t TrackballInterruptBase::runOnce() +{ + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = millis() - pressStartTime; - bool buttonStillPressed = false; + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = millis() - pressStartTime; + bool buttonStillPressed = false; #if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); + buttonStillPressed = (this->action == TB_ACTION_PRESSED); #else - buttonStillPressed = !digitalRead(_pinPress); + buttonStillPressed = !digitalRead(_pinPress); #endif - if (!buttonStillPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION) { - // Short press - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastLongPressEventTime = 0; - this->action = TB_ACTION_NONE; - } else if (pressDuration >= LONG_PRESS_DURATION) { - // Long press detected - uint32_t currentTime = millis(); - // Only trigger long press event if enough time has passed since the last one - if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventPressedLong; - lastLongPressEventTime = currentTime; - } - this->action = TB_ACTION_PRESSED_LONG; - } - } - - if (directionDetected && directionStartTime > 0) { - uint32_t directionDuration = millis() - directionStartTime; - uint8_t directionPressedNow = 0; - directionInterval++; - - if (!digitalRead(_pinUp)) { - directionPressedNow = TB_ACTION_UP; - } else if (!digitalRead(_pinDown)) { - directionPressedNow = TB_ACTION_DOWN; - } else if (!digitalRead(_pinLeft)) { - directionPressedNow = TB_ACTION_LEFT; - } else if (!digitalRead(_pinRight)) { - directionPressedNow = TB_ACTION_RIGHT; + if (!buttonStillPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION) { + // Short press + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastLongPressEventTime = 0; + this->action = TB_ACTION_NONE; + } else if (pressDuration >= LONG_PRESS_DURATION) { + // Long press detected + uint32_t currentTime = millis(); + // Only trigger long press event if enough time has passed since the last one + if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventPressedLong; + lastLongPressEventTime = currentTime; + } + this->action = TB_ACTION_PRESSED_LONG; + } } - const uint8_t DIRECTION_REPEAT_THRESHOLD = 3; + if (directionDetected && directionStartTime > 0) { + uint32_t directionDuration = millis() - directionStartTime; + uint8_t directionPressedNow = 0; + directionInterval++; - if (directionPressedNow == TB_ACTION_NONE) { - // Reset state - directionDetected = false; - directionStartTime = 0; - directionInterval = 0; - this->action = TB_ACTION_NONE; - } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) { - // repeat event when long press these direction. - switch (directionPressedNow) { - case TB_ACTION_UP: - e.inputEvent = this->_eventUp; - break; - case TB_ACTION_DOWN: - e.inputEvent = this->_eventDown; - break; - case TB_ACTION_LEFT: - e.inputEvent = this->_eventLeft; - break; - case TB_ACTION_RIGHT: - e.inputEvent = this->_eventRight; - break; - } + if (!digitalRead(_pinUp)) { + directionPressedNow = TB_ACTION_UP; + } else if (!digitalRead(_pinDown)) { + directionPressedNow = TB_ACTION_DOWN; + } else if (!digitalRead(_pinLeft)) { + directionPressedNow = TB_ACTION_LEFT; + } else if (!digitalRead(_pinRight)) { + directionPressedNow = TB_ACTION_RIGHT; + } - directionInterval = 0; + const uint8_t DIRECTION_REPEAT_THRESHOLD = 3; + + if (directionPressedNow == TB_ACTION_NONE) { + // Reset state + directionDetected = false; + directionStartTime = 0; + directionInterval = 0; + this->action = TB_ACTION_NONE; + } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) { + // repeat event when long press these direction. + switch (directionPressedNow) { + case TB_ACTION_UP: + e.inputEvent = this->_eventUp; + break; + case TB_ACTION_DOWN: + e.inputEvent = this->_eventDown; + break; + case TB_ACTION_LEFT: + e.inputEvent = this->_eventLeft; + break; + case TB_ACTION_RIGHT: + e.inputEvent = this->_eventRight; + break; + } + + directionInterval = 0; + } } - } #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { - // LOG_DEBUG("Trackball event UP"); - e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { - // LOG_DEBUG("Trackball event DOWN"); - e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { - // LOG_DEBUG("Trackball event LEFT"); - e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { - // LOG_DEBUG("Trackball event RIGHT"); - e.inputEvent = this->_eventRight; - } + if (this->action == TB_ACTION_PRESSED && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press + } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { + // LOG_DEBUG("Trackball event UP"); + e.inputEvent = this->_eventUp; + } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { + // LOG_DEBUG("Trackball event DOWN"); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { + // LOG_DEBUG("Trackball event LEFT"); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { + // LOG_DEBUG("Trackball event RIGHT"); + e.inputEvent = this->_eventRight; + } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventUp; - // send event first,will automatically trigger every 50ms * 3 after 500ms - } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) { - directionDetected = true; - directionStartTime = millis(); - e.inputEvent = this->_eventRight; - } + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press + } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventUp; + // send event first,will automatically trigger every 50ms * 3 after 500ms + } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); + e.inputEvent = this->_eventRight; + } #endif - if (e.inputEvent != INPUT_BROKER_NONE) { - e.source = this->_originName; - e.kbchar = 0x00; - this->notifyObservers(&e); - } - - // Only update lastEvent for non-press actions or completed press actions - if (this->action != TB_ACTION_PRESSED || !pressDetected) { - lastEvent = action; - if (!pressDetected) { - this->action = TB_ACTION_NONE; + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = 0x00; + this->notifyObservers(&e); } - } - return 50; // Check more frequently for better long press detection + // Only update lastEvent for non-press actions or completed press actions + if (this->action != TB_ACTION_PRESSED || !pressDetected) { + lastEvent = action; + if (!pressDetected) { + this->action = TB_ACTION_NONE; + } + } + + return 50; // Check more frequently for better long press detection } -void TrackballInterruptBase::intPressHandler() { this->action = TB_ACTION_PRESSED; } +void TrackballInterruptBase::intPressHandler() +{ + this->action = TB_ACTION_PRESSED; +} -void TrackballInterruptBase::intDownHandler() { this->action = TB_ACTION_DOWN; } +void TrackballInterruptBase::intDownHandler() +{ + this->action = TB_ACTION_DOWN; +} -void TrackballInterruptBase::intUpHandler() { this->action = TB_ACTION_UP; } +void TrackballInterruptBase::intUpHandler() +{ + this->action = TB_ACTION_UP; +} -void TrackballInterruptBase::intLeftHandler() { this->action = TB_ACTION_LEFT; } +void TrackballInterruptBase::intLeftHandler() +{ + this->action = TB_ACTION_LEFT; +} -void TrackballInterruptBase::intRightHandler() { this->action = TB_ACTION_RIGHT; } +void TrackballInterruptBase::intRightHandler() +{ + this->action = TB_ACTION_RIGHT; +} diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 29c124d5d..67d4ee449 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -12,58 +12,59 @@ #endif #endif -class TrackballInterruptBase : public Observable, public concurrency::OSThread { -public: - explicit TrackballInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, - input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, input_broker_event eventPressed, - input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), - void (*onIntPress)()); - void intPressHandler(); - void intDownHandler(); - void intUpHandler(); - void intLeftHandler(); - void intRightHandler(); - uint32_t lastTime = 0; +class TrackballInterruptBase : public Observable, public concurrency::OSThread +{ + public: + explicit TrackballInterruptBase(const char *name); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, + input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, + input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); + void intLeftHandler(); + void intRightHandler(); + uint32_t lastTime = 0; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; -protected: - enum TrackballInterruptBaseActionType { - TB_ACTION_NONE, - TB_ACTION_PRESSED, - TB_ACTION_PRESSED_LONG, - TB_ACTION_UP, - TB_ACTION_DOWN, - TB_ACTION_LEFT, - TB_ACTION_RIGHT - }; - uint8_t _pinDown = 0; - uint8_t _pinUp = 0; - uint8_t _pinLeft = 0; - uint8_t _pinRight = 0; - uint8_t _pinPress = 0; + protected: + enum TrackballInterruptBaseActionType { + TB_ACTION_NONE, + TB_ACTION_PRESSED, + TB_ACTION_PRESSED_LONG, + TB_ACTION_UP, + TB_ACTION_DOWN, + TB_ACTION_LEFT, + TB_ACTION_RIGHT + }; + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + uint8_t _pinLeft = 0; + uint8_t _pinRight = 0; + uint8_t _pinPress = 0; - volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - // Long press detection for press button - uint32_t pressStartTime = 0; - uint32_t directionStartTime = 0; - uint8_t directionInterval = 0; - bool pressDetected = false; - bool directionDetected = false; - uint32_t lastLongPressEventTime = 0; - uint32_t lastDirectionPressEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = 500; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events + // Long press detection for press button + uint32_t pressStartTime = 0; + uint32_t directionStartTime = 0; + uint8_t directionInterval = 0; + bool pressDetected = false; + bool directionDetected = false; + uint32_t lastLongPressEventTime = 0; + uint32_t lastDirectionPressEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = 500; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events -private: - input_broker_event _eventDown = INPUT_BROKER_NONE; - input_broker_event _eventUp = INPUT_BROKER_NONE; - input_broker_event _eventLeft = INPUT_BROKER_NONE; - input_broker_event _eventRight = INPUT_BROKER_NONE; - input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; - const char *_originName; - TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; + private: + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventLeft = INPUT_BROKER_NONE; + input_broker_event _eventRight = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + const char *_originName; + TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index e9e71b7a0..594facdeb 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -6,52 +6,59 @@ TrackballInterruptImpl1 *trackballInterruptImpl1; TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} -void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) { - input_broker_event eventDown = INPUT_BROKER_DOWN; - input_broker_event eventUp = INPUT_BROKER_UP; - input_broker_event eventLeft = INPUT_BROKER_LEFT; - input_broker_event eventRight = INPUT_BROKER_RIGHT; - input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; +void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) +{ + input_broker_event eventDown = INPUT_BROKER_DOWN; + input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventLeft = INPUT_BROKER_LEFT; + input_broker_event eventRight = INPUT_BROKER_RIGHT; + input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; - TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, eventPressed, eventPressedLong, - TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, - TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); - inputBroker->registerSource(this); + TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, + eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, + TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, + TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); } -void TrackballInterruptImpl1::handleIntDown() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intDownHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntDown() +{ + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intDownHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntUp() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intUpHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntUp() +{ + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intUpHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntLeft() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intLeftHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntLeft() +{ + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intLeftHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntRight() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intRightHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntRight() +{ + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intRightHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } -void TrackballInterruptImpl1::handleIntPressed() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intPressHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } +void TrackballInterruptImpl1::handleIntPressed() +{ + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intPressHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } diff --git a/src/input/TrackballInterruptImpl1.h b/src/input/TrackballInterruptImpl1.h index 0647c71b2..4683efa41 100644 --- a/src/input/TrackballInterruptImpl1.h +++ b/src/input/TrackballInterruptImpl1.h @@ -1,15 +1,16 @@ #pragma once #include "TrackballInterruptBase.h" -class TrackballInterruptImpl1 : public TrackballInterruptBase { -public: - TrackballInterruptImpl1(); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); - static void handleIntDown(); - static void handleIntUp(); - static void handleIntLeft(); - static void handleIntRight(); - static void handleIntPressed(); +class TrackballInterruptImpl1 : public TrackballInterruptBase +{ + public: + TrackballInterruptImpl1(); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); + static void handleIntDown(); + static void handleIntUp(); + static void handleIntLeft(); + static void handleIntRight(); + static void handleIntPressed(); }; extern TrackballInterruptImpl1 *trackballInterruptImpl1; diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index a3d74142b..d597c8d8f 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -1,151 +1,165 @@ #include "UpDownInterruptBase.h" #include "configuration.h" -UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThread(name) { this->_originName = name; } +UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; +} -void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, - input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, - input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), - unsigned long updownDebounceMs) { - this->_pinDown = pinDown; - this->_pinUp = pinUp; - this->_pinPress = pinPress; - this->_eventDown = eventDown; - this->_eventUp = eventUp; - this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; - this->_eventUpLong = eventUpLong; - this->_eventDownLong = eventDownLong; +void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, + input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong, + input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) +{ + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_pinPress = pinPress; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; + this->_eventUpLong = eventUpLong; + this->_eventDownLong = eventDownLong; - // Store debounce configuration passed by caller - this->updownDebounceMs = updownDebounceMs; - bool isRAK = false; + // Store debounce configuration passed by caller + this->updownDebounceMs = updownDebounceMs; + bool isRAK = false; #ifdef RAK_4631 - isRAK = true; + isRAK = true; #endif - if (!isRAK || pinPress != 0) { - pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, FALLING); - } - if (!isRAK || this->_pinDown != 0) { - pinMode(this->_pinDown, INPUT_PULLUP); - attachInterrupt(this->_pinDown, onIntDown, FALLING); - } - if (!isRAK || this->_pinUp != 0) { - pinMode(this->_pinUp, INPUT_PULLUP); - attachInterrupt(this->_pinUp, onIntUp, FALLING); - } + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, FALLING); + } + if (!isRAK || this->_pinDown != 0) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, FALLING); + } + if (!isRAK || this->_pinUp != 0) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, FALLING); + } - LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); + LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); - this->setInterval(20); + this->setInterval(20); } -int32_t UpDownInterruptBase::runOnce() { - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - unsigned long now = millis(); +int32_t UpDownInterruptBase::runOnce() +{ + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + unsigned long now = millis(); - // Read all button states once at the beginning - bool pressButtonPressed = !digitalRead(_pinPress); - bool upButtonPressed = !digitalRead(_pinUp); - bool downButtonPressed = !digitalRead(_pinDown); + // Read all button states once at the beginning + bool pressButtonPressed = !digitalRead(_pinPress); + bool upButtonPressed = !digitalRead(_pinUp); + bool downButtonPressed = !digitalRead(_pinDown); - // Handle initial button press detection - only if not already detected - if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) { - pressDetected = true; - pressStartTime = now; - } else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) { - upDetected = true; - upStartTime = now; - } else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) { - downDetected = true; - downStartTime = now; - } - - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = now - pressStartTime; - - if (!pressButtonPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { - lastPressKeyTime = now; - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastPressLongEventTime = 0; - } else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) { - // First long press event only - avoid repeated events causing lag - e.inputEvent = this->_eventPressedLong; - lastPressLongEventTime = now; + // Handle initial button press detection - only if not already detected + if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) { + pressDetected = true; + pressStartTime = now; + } else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) { + upDetected = true; + upStartTime = now; + } else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) { + downDetected = true; + downStartTime = now; } - } - // Handle long press detection for up button - if (upDetected && upStartTime > 0) { - uint32_t upDuration = now - upStartTime; + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = now - pressStartTime; - if (!upButtonPressed) { - // Button released - if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) { - lastUpKeyTime = now; - e.inputEvent = this->_eventUp; - } - // Reset state - upDetected = false; - upStartTime = 0; - lastUpLongEventTime = 0; - } else if (upDuration >= LONG_PRESS_DURATION) { - // Auto-repeat long press events - if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventUpLong; - lastUpLongEventTime = now; - } + if (!pressButtonPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastPressLongEventTime = 0; + } else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) { + // First long press event only - avoid repeated events causing lag + e.inputEvent = this->_eventPressedLong; + lastPressLongEventTime = now; + } } - } - // Handle long press detection for down button - if (downDetected && downStartTime > 0) { - uint32_t downDuration = now - downStartTime; + // Handle long press detection for up button + if (upDetected && upStartTime > 0) { + uint32_t upDuration = now - upStartTime; - if (!downButtonPressed) { - // Button released - if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) { - lastDownKeyTime = now; - e.inputEvent = this->_eventDown; - } - // Reset state - downDetected = false; - downStartTime = 0; - lastDownLongEventTime = 0; - } else if (downDuration >= LONG_PRESS_DURATION) { - // Auto-repeat long press events - if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventDownLong; - lastDownLongEventTime = now; - } + if (!upButtonPressed) { + // Button released + if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) { + lastUpKeyTime = now; + e.inputEvent = this->_eventUp; + } + // Reset state + upDetected = false; + upStartTime = 0; + lastUpLongEventTime = 0; + } else if (upDuration >= LONG_PRESS_DURATION) { + // Auto-repeat long press events + if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventUpLong; + lastUpLongEventTime = now; + } + } } - } - if (e.inputEvent != INPUT_BROKER_NONE) { - e.source = this->_originName; - e.kbchar = INPUT_BROKER_NONE; - this->notifyObservers(&e); - } + // Handle long press detection for down button + if (downDetected && downStartTime > 0) { + uint32_t downDuration = now - downStartTime; - if (!pressDetected && !upDetected && !downDetected) { - this->action = UPDOWN_ACTION_NONE; - } + if (!downButtonPressed) { + // Button released + if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) { + lastDownKeyTime = now; + e.inputEvent = this->_eventDown; + } + // Reset state + downDetected = false; + downStartTime = 0; + lastDownLongEventTime = 0; + } else if (downDuration >= LONG_PRESS_DURATION) { + // Auto-repeat long press events + if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventDownLong; + lastDownLongEventTime = now; + } + } + } - return 20; // This will control how the input frequency + if (e.inputEvent != INPUT_BROKER_NONE) { + e.source = this->_originName; + e.kbchar = INPUT_BROKER_NONE; + this->notifyObservers(&e); + } + + if (!pressDetected && !upDetected && !downDetected) { + this->action = UPDOWN_ACTION_NONE; + } + + return 20; // This will control how the input frequency } -void UpDownInterruptBase::intPressHandler() { this->action = UPDOWN_ACTION_PRESSED; } +void UpDownInterruptBase::intPressHandler() +{ + this->action = UPDOWN_ACTION_PRESSED; +} -void UpDownInterruptBase::intDownHandler() { this->action = UPDOWN_ACTION_DOWN; } +void UpDownInterruptBase::intDownHandler() +{ + this->action = UPDOWN_ACTION_DOWN; +} -void UpDownInterruptBase::intUpHandler() { this->action = UPDOWN_ACTION_UP; } +void UpDownInterruptBase::intUpHandler() +{ + this->action = UPDOWN_ACTION_UP; +} diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h index 1da38ecd9..2b9d38c83 100644 --- a/src/input/UpDownInterruptBase.h +++ b/src/input/UpDownInterruptBase.h @@ -11,59 +11,61 @@ #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 300 #endif -class UpDownInterruptBase : public Observable, public concurrency::OSThread { -public: - explicit UpDownInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, - input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, input_broker_event eventDownLong, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs = 50); - void intPressHandler(); - void intDownHandler(); - void intUpHandler(); +class UpDownInterruptBase : public Observable, public concurrency::OSThread +{ + public: + explicit UpDownInterruptBase(const char *name); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, + input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, + input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), + unsigned long updownDebounceMs = 50); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); - int32_t runOnce() override; + int32_t runOnce() override; -protected: - enum UpDownInterruptBaseActionType { - UPDOWN_ACTION_NONE, - UPDOWN_ACTION_PRESSED, - UPDOWN_ACTION_PRESSED_LONG, - UPDOWN_ACTION_UP, - UPDOWN_ACTION_UP_LONG, - UPDOWN_ACTION_DOWN, - UPDOWN_ACTION_DOWN_LONG - }; + protected: + enum UpDownInterruptBaseActionType { + UPDOWN_ACTION_NONE, + UPDOWN_ACTION_PRESSED, + UPDOWN_ACTION_PRESSED_LONG, + UPDOWN_ACTION_UP, + UPDOWN_ACTION_UP_LONG, + UPDOWN_ACTION_DOWN, + UPDOWN_ACTION_DOWN_LONG + }; - volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; + volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; - // Long press detection variables - uint32_t pressStartTime = 0; - uint32_t upStartTime = 0; - uint32_t downStartTime = 0; - bool pressDetected = false; - bool upDetected = false; - bool downDetected = false; - uint32_t lastPressLongEventTime = 0; - uint32_t lastUpLongEventTime = 0; - uint32_t lastDownLongEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION; - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL; + // Long press detection variables + uint32_t pressStartTime = 0; + uint32_t upStartTime = 0; + uint32_t downStartTime = 0; + bool pressDetected = false; + bool upDetected = false; + bool downDetected = false; + uint32_t lastPressLongEventTime = 0; + uint32_t lastUpLongEventTime = 0; + uint32_t lastDownLongEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION; + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL; -private: - uint8_t _pinDown = 0; - uint8_t _pinUp = 0; - uint8_t _pinPress = 0; - input_broker_event _eventDown = INPUT_BROKER_NONE; - input_broker_event _eventUp = INPUT_BROKER_NONE; - input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; - input_broker_event _eventUpLong = INPUT_BROKER_NONE; - input_broker_event _eventDownLong = INPUT_BROKER_NONE; - const char *_originName; + private: + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + uint8_t _pinPress = 0; + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + input_broker_event _eventUpLong = INPUT_BROKER_NONE; + input_broker_event _eventDownLong = INPUT_BROKER_NONE; + const char *_originName; - unsigned long lastUpKeyTime = 0; - unsigned long lastDownKeyTime = 0; - unsigned long lastPressKeyTime = 0; - unsigned long updownDebounceMs = 50; - const unsigned long pressDebounceMs = 200; + unsigned long lastUpKeyTime = 0; + unsigned long lastDownKeyTime = 0; + unsigned long lastPressKeyTime = 0; + unsigned long updownDebounceMs = 50; + const unsigned long pressDebounceMs = 200; }; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 99e4d8464..906dcd2a8 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -6,33 +6,44 @@ UpDownInterruptImpl1 *upDownInterruptImpl1; UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {} -bool UpDownInterruptImpl1::init() { +bool UpDownInterruptImpl1::init() +{ - if (!moduleConfig.canned_message.updown1_enabled) { - // Input device is disabled. - return false; - } + if (!moduleConfig.canned_message.updown1_enabled) { + // Input device is disabled. + return false; + } - uint8_t pinUp = moduleConfig.canned_message.inputbroker_pin_a; - uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; - uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; + uint8_t pinUp = moduleConfig.canned_message.inputbroker_pin_a; + uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; + uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN - input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP - input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; - input_broker_event eventUpLong = INPUT_BROKER_UP_LONG; - input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG; + input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN + input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP + input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; + input_broker_event eventUpLong = INPUT_BROKER_UP_LONG; + input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG; - UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong, eventDownLong, - UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); - inputBroker->registerSource(this); + UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong, + eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, + UpDownInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif - return true; + return true; } -void UpDownInterruptImpl1::handleIntDown() { upDownInterruptImpl1->intDownHandler(); } -void UpDownInterruptImpl1::handleIntUp() { upDownInterruptImpl1->intUpHandler(); } -void UpDownInterruptImpl1::handleIntPressed() { upDownInterruptImpl1->intPressHandler(); } \ No newline at end of file +void UpDownInterruptImpl1::handleIntDown() +{ + upDownInterruptImpl1->intDownHandler(); +} +void UpDownInterruptImpl1::handleIntUp() +{ + upDownInterruptImpl1->intUpHandler(); +} +void UpDownInterruptImpl1::handleIntPressed() +{ + upDownInterruptImpl1->intPressHandler(); +} \ No newline at end of file diff --git a/src/input/UpDownInterruptImpl1.h b/src/input/UpDownInterruptImpl1.h index 8c12bb87c..4739cd2ff 100644 --- a/src/input/UpDownInterruptImpl1.h +++ b/src/input/UpDownInterruptImpl1.h @@ -1,13 +1,14 @@ #pragma once #include "UpDownInterruptBase.h" -class UpDownInterruptImpl1 : public UpDownInterruptBase { -public: - UpDownInterruptImpl1(); - bool init(); - static void handleIntDown(); - static void handleIntUp(); - static void handleIntPressed(); +class UpDownInterruptImpl1 : public UpDownInterruptBase +{ + public: + UpDownInterruptImpl1(); + bool init(); + static void handleIntDown(); + static void handleIntUp(); + static void handleIntPressed(); }; extern UpDownInterruptImpl1 *upDownInterruptImpl1; \ No newline at end of file diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index 18d58a439..cb03eb4ff 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -7,68 +7,69 @@ CardKbI2cImpl *cardKbI2cImpl; CardKbI2cImpl::CardKbI2cImpl() : KbI2cBase("cardKB") {} -void CardKbI2cImpl::init() { +void CardKbI2cImpl::init() +{ #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) - if (cardkb_found.address == 0x00) { - LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; + if (cardkb_found.address == 0x00) { + LOG_DEBUG("Rescan for I2C keyboard"); + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; #if defined(T_LORA_PAGER) - uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); + uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); #else - uint8_t i2caddr_asize = 5; + uint8_t i2caddr_asize = 5; #endif - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); #endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto kb_info = i2cScanner->firstKeyboard(); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto kb_info = i2cScanner->firstKeyboard(); - if (kb_info.type != ScanI2C::DeviceType::NONE) { - cardkb_found = kb_info.address; - switch (kb_info.type) { - case ScanI2C::DeviceType::RAK14004: - kb_model = 0x02; - break; - case ScanI2C::DeviceType::CARDKB: - kb_model = 0x00; - break; - case ScanI2C::DeviceType::TDECKKB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x10; - break; - case ScanI2C::DeviceType::BBQ10KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x11; - break; - case ScanI2C::DeviceType::MPR121KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x37; - break; - case ScanI2C::DeviceType::TCA8418KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x84; - break; - default: - // use this as default since it's also just zero - LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); - kb_model = 0x00; - } + if (kb_info.type != ScanI2C::DeviceType::NONE) { + cardkb_found = kb_info.address; + switch (kb_info.type) { + case ScanI2C::DeviceType::RAK14004: + kb_model = 0x02; + break; + case ScanI2C::DeviceType::CARDKB: + kb_model = 0x00; + break; + case ScanI2C::DeviceType::TDECKKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x10; + break; + case ScanI2C::DeviceType::BBQ10KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x11; + break; + case ScanI2C::DeviceType::MPR121KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x37; + break; + case ScanI2C::DeviceType::TCA8418KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x84; + break; + default: + // use this as default since it's also just zero + LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); + kb_model = 0x00; + } + } + if (cardkb_found.address == 0x00) { + disable(); + return; + } else { + LOG_DEBUG("Keyboard Type: 0x%02x Model: 0x%02x Address: 0x%02x", kb_info.type, kb_model, cardkb_found.address); + } } - if (cardkb_found.address == 0x00) { - disable(); - return; - } else { - LOG_DEBUG("Keyboard Type: 0x%02x Model: 0x%02x Address: 0x%02x", kb_info.type, kb_model, cardkb_found.address); - } - } #else - if (cardkb_found.address == 0x00) { - disable(); - return; - } + if (cardkb_found.address == 0x00) { + disable(); + return; + } #endif - inputBroker->registerSource(this); - kb_found = true; + inputBroker->registerSource(this); + kb_found = true; } \ No newline at end of file diff --git a/src/input/cardKbI2cImpl.h b/src/input/cardKbI2cImpl.h index ce6a0758b..811a0558c 100644 --- a/src/input/cardKbI2cImpl.h +++ b/src/input/cardKbI2cImpl.h @@ -8,10 +8,11 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class CardKbI2cImpl : public KbI2cBase { -public: - CardKbI2cImpl(); - void init(); +class CardKbI2cImpl : public KbI2cBase +{ + public: + CardKbI2cImpl(); + void init(); }; extern CardKbI2cImpl *cardKbI2cImpl; \ No newline at end of file diff --git a/src/input/i2cButton.cpp b/src/input/i2cButton.cpp index 105f4ff60..d874146cd 100644 --- a/src/input/i2cButton.cpp +++ b/src/input/i2cButton.cpp @@ -31,63 +31,65 @@ extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value); #define PI4IO_REG_IN_STA 0x0F #define PI4IO_REG_CHIP_RESET 0x01 -i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) { - _originName = name; - if (inputBroker) - inputBroker->registerSource(this); +i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) +{ + _originName = name; + if (inputBroker) + inputBroker->registerSource(this); } -int32_t i2cButtonThread::runOnce() { - static bool btn1_pressed = false; - static uint32_t press_start_time = 0; - const uint32_t LONG_PRESS_TIME = 1000; - static bool long_press_triggered = false; +int32_t i2cButtonThread::runOnce() +{ + static bool btn1_pressed = false; + static uint32_t press_start_time = 0; + const uint32_t LONG_PRESS_TIME = 1000; + static bool long_press_triggered = false; - uint8_t in_data; - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); - if (getbit(in_data, 0)) { - uint8_t input_state; - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); + uint8_t in_data; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); + if (getbit(in_data, 0)) { + uint8_t input_state; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); - if (!getbit(input_state, 0)) { - if (!btn1_pressed) { - btn1_pressed = true; - press_start_time = millis(); - long_press_triggered = false; - } - } else { - if (btn1_pressed) { - btn1_pressed = false; - uint32_t press_duration = millis() - press_start_time; - if (long_press_triggered) { - long_press_triggered = false; - return 50; + if (!getbit(input_state, 0)) { + if (!btn1_pressed) { + btn1_pressed = true; + press_start_time = millis(); + long_press_triggered = false; + } + } else { + if (btn1_pressed) { + btn1_pressed = false; + uint32_t press_duration = millis() - press_start_time; + if (long_press_triggered) { + long_press_triggered = false; + return 50; + } + + if (press_duration < LONG_PRESS_TIME) { + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_USER_PRESS; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + } } - - if (press_duration < LONG_PRESS_TIME) { - InputEvent evt; - evt.source = "UserButton"; - evt.inputEvent = INPUT_BROKER_USER_PRESS; - evt.kbchar = 0; - evt.touchX = 0; - evt.touchY = 0; - this->notifyObservers(&evt); - } - } } - } - if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { - long_press_triggered = true; - InputEvent evt; - evt.source = "UserButton"; - evt.inputEvent = INPUT_BROKER_SELECT; - evt.kbchar = 0; - evt.touchX = 0; - evt.touchY = 0; - this->notifyObservers(&evt); - } - return 50; + if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { + long_press_triggered = true; + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_SELECT; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + return 50; } #endif \ No newline at end of file diff --git a/src/input/i2cButton.h b/src/input/i2cButton.h index 821f8f3a6..1ad908606 100644 --- a/src/input/i2cButton.h +++ b/src/input/i2cButton.h @@ -6,11 +6,12 @@ #include "configuration.h" #if defined(M5STACK_UNITC6L) -class i2cButtonThread : public Observable, public concurrency::OSThread { -public: - const char *_originName; - explicit i2cButtonThread(const char *name); - int32_t runOnce() override; +class i2cButtonThread : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + explicit i2cButtonThread(const char *name); + int32_t runOnce() override; }; extern i2cButtonThread *i2cButton; diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index e4276caea..12d0822f6 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -28,502 +28,505 @@ KbI2cBase::KbI2cBase(const char *name) TCAKeyboard(*(new TCA8418Keyboard())) #endif { - this->_originName = name; + this->_originName = name; } -uint8_t read_from_14004(TwoWire *i2cBus, uint8_t reg, uint8_t *data, uint8_t length) { - uint8_t readflag = 0; - i2cBus->beginTransmission(CARDKB_ADDR); - i2cBus->write(reg); - i2cBus->endTransmission(); // stop transmitting - delay(20); - i2cBus->requestFrom(CARDKB_ADDR, (int)length); - int i = 0; - while (i2cBus->available()) // slave may send less than requested - { - data[i++] = i2cBus->read(); // receive a byte as a proper uint8_t - readflag = 1; - } - return readflag; +uint8_t read_from_14004(TwoWire *i2cBus, uint8_t reg, uint8_t *data, uint8_t length) +{ + uint8_t readflag = 0; + i2cBus->beginTransmission(CARDKB_ADDR); + i2cBus->write(reg); + i2cBus->endTransmission(); // stop transmitting + delay(20); + i2cBus->requestFrom(CARDKB_ADDR, (int)length); + int i = 0; + while (i2cBus->available()) // slave may send less than requested + { + data[i++] = i2cBus->read(); // receive a byte as a proper uint8_t + readflag = 1; + } + return readflag; } -int32_t KbI2cBase::runOnce() { - if (!i2cBus) { - switch (cardkb_found.port) { - case ScanI2C::WIRE1: +int32_t KbI2cBase::runOnce() +{ + if (!i2cBus) { + switch (cardkb_found.port) { + case ScanI2C::WIRE1: #if WIRE_INTERFACES_COUNT == 2 - LOG_DEBUG("Use I2C Bus 1 (the second one)"); - i2cBus = &Wire1; - if (cardkb_found.address == BBQ10_KB_ADDR) { - Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1); - Q10keyboard.setBacklight(0); - } - if (cardkb_found.address == MPR121_KB_ADDR) { - MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1); - } - if (cardkb_found.address == TCA8418_KB_ADDR) { - TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1); - } - break; + LOG_DEBUG("Use I2C Bus 1 (the second one)"); + i2cBus = &Wire1; + if (cardkb_found.address == BBQ10_KB_ADDR) { + Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1); + Q10keyboard.setBacklight(0); + } + if (cardkb_found.address == MPR121_KB_ADDR) { + MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1); + } + if (cardkb_found.address == TCA8418_KB_ADDR) { + TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1); + } + break; #endif - case ScanI2C::WIRE: - LOG_DEBUG("Use I2C Bus 0 (the first one)"); - i2cBus = &Wire; - if (cardkb_found.address == BBQ10_KB_ADDR) { - Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); - Q10keyboard.setBacklight(0); - } - if (cardkb_found.address == MPR121_KB_ADDR) { - MPRkeyboard.begin(MPR121_KB_ADDR, &Wire); - } - if (cardkb_found.address == TCA8418_KB_ADDR) { - TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire); - } - break; - case ScanI2C::NO_I2C: - default: - i2cBus = 0; + case ScanI2C::WIRE: + LOG_DEBUG("Use I2C Bus 0 (the first one)"); + i2cBus = &Wire; + if (cardkb_found.address == BBQ10_KB_ADDR) { + Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); + Q10keyboard.setBacklight(0); + } + if (cardkb_found.address == MPR121_KB_ADDR) { + MPRkeyboard.begin(MPR121_KB_ADDR, &Wire); + } + if (cardkb_found.address == TCA8418_KB_ADDR) { + TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire); + } + break; + case ScanI2C::NO_I2C: + default: + i2cBus = 0; + } } - } - switch (kb_model) { - case 0x11: { // BB Q10 - int keyCount = Q10keyboard.keyCount(); - while (keyCount--) { - const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); - if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { + switch (kb_model) { + case 0x11: { // BB Q10 + int keyCount = Q10keyboard.keyCount(); + while (keyCount--) { + const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); + if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + switch (key.key) { + case 'p': // TAB + case 't': // TAB as well + if (is_sym) { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x09; // TAB Scancode + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 'q': // ESC + if (is_sym) { + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 0x08: // Back + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = key.key; + break; + case 'e': // sym e + if (is_sym) { + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = INPUT_BROKER_UP; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 'x': // sym x + if (is_sym) { + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 's': // sym s + if (is_sym) { + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; // tweak for destSelect + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 'f': // sym f + if (is_sym) { + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; // tweak for destSelect + is_sym = false; // reset sym state after second keypress + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + } + break; + case 0x13: // Code scanner says the SYM key is 0x13 + is_sym = !is_sym; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that + : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active + break; + case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return + e.inputEvent = INPUT_BROKER_SELECT; + break; + case 0x00: // nopress + e.inputEvent = INPUT_BROKER_NONE; + break; + default: // all other keys + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key.key; + is_sym = false; // reset sym state after second keypress + break; + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + } + break; + } + case 0x37: { // MPR121 + MPRkeyboard.trigger(); InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - switch (key.key) { - case 'p': // TAB - case 't': // TAB as well - if (is_sym) { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x09; // TAB Scancode - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 'q': // ESC - if (is_sym) { - e.inputEvent = INPUT_BROKER_CANCEL; - e.kbchar = 0; - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 0x08: // Back - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = key.key; - break; - case 'e': // sym e - if (is_sym) { - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = INPUT_BROKER_UP; - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 'x': // sym x - if (is_sym) { - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0; - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 's': // sym s - if (is_sym) { - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0x00; // tweak for destSelect - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 'f': // sym f - if (is_sym) { - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0x00; // tweak for destSelect - is_sym = false; // reset sym state after second keypress - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - } - break; - case 0x13: // Code scanner says the SYM key is 0x13 - is_sym = !is_sym; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that - : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active - break; - case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return - e.inputEvent = INPUT_BROKER_SELECT; - break; - case 0x00: // nopress - e.inputEvent = INPUT_BROKER_NONE; - break; - default: // all other keys - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key.key; - is_sym = false; // reset sym state after second keypress - break; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); + while (MPRkeyboard.hasEvent()) { + char nextEvent = MPRkeyboard.dequeueEvent(); + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x00; + e.source = this->_originName; + switch (nextEvent) { + case 0x00: // MPR121_NONE + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + case 0x90: // MPR121_REBOOT + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_REBOOT; + break; + case 0xb4: // MPR121_LEFT + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; + break; + case 0xb5: // MPR121_UP + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0x00; + break; + case 0xb6: // MPR121_DOWN + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0x00; + break; + case 0xb7: // MPR121_RIGHT + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; + break; + case 0x1b: // MPR121_ESC + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; + break; + case 0x08: // MPR121_BSP + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + break; + case 0x0d: // MPR121_SELECT + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; + break; + default: + if (nextEvent > 127) { + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = nextEvent; + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); + this->notifyObservers(&e); + } } - } + break; } - break; - } - case 0x37: { // MPR121 - MPRkeyboard.trigger(); - InputEvent e = {}; + case 0x84: { // Adafruit TCA8418 + TCAKeyboard.trigger(); + InputEvent e = {}; + while (TCAKeyboard.hasEvent()) { + char nextEvent = TCAKeyboard.dequeueEvent(); + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x00; + e.source = this->_originName; + switch (nextEvent) { + case TCA8418KeyboardBase::NONE: + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::REBOOT: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_REBOOT; + break; + case TCA8418KeyboardBase::LEFT: + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::UP: + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::DOWN: + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::RIGHT: + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::BSP: + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + break; + case TCA8418KeyboardBase::SELECT: + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::ESC: + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::GPS_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_GPS_TOGGLE; + break; + case TCA8418KeyboardBase::SEND_PING: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_SEND_PING; + break; + case TCA8418KeyboardBase::MUTE_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; + break; + case TCA8418KeyboardBase::BT_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; + break; + case TCA8418KeyboardBase::BL_TOGGLE: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; + break; + case TCA8418KeyboardBase::TAB: + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_TAB; + break; + default: + if (nextEvent > 127) { + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = nextEvent; + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); + this->notifyObservers(&e); + } + TCAKeyboard.trigger(); + } + TCAKeyboard.clearInt(); + break; + } + case 0x02: { + // RAK14004 + uint8_t rDataBuf[8] = {0}; + uint8_t PrintDataBuf = 0; + if (read_from_14004(i2cBus, 0x01, rDataBuf, 0x04) == 1) { + for (uint8_t aCount = 0; aCount < 0x04; aCount++) { + for (uint8_t bCount = 0; bCount < 0x04; bCount++) { + if (((rDataBuf[aCount] >> bCount) & 0x01) == 0x01) { + PrintDataBuf = aCount * 0x04 + bCount + 1; + } + } + } + } + if (PrintDataBuf != 0) { + LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_MATRIXKEY; + e.source = this->_originName; + e.kbchar = PrintDataBuf; + this->notifyObservers(&e); + } + break; + } + case 0x00: // CARDKB + case 0x10: { // T-DECK - while (MPRkeyboard.hasEvent()) { - char nextEvent = MPRkeyboard.dequeueEvent(); - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x00; - e.source = this->_originName; - switch (nextEvent) { - case 0x00: // MPR121_NONE - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - case 0x90: // MPR121_REBOOT - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_REBOOT; - break; - case 0xb4: // MPR121_LEFT - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0x00; - break; - case 0xb5: // MPR121_UP - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = 0x00; - break; - case 0xb6: // MPR121_DOWN - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0x00; - break; - case 0xb7: // MPR121_RIGHT - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0x00; - break; - case 0x1b: // MPR121_ESC - e.inputEvent = INPUT_BROKER_CANCEL; - e.kbchar = 0; - break; - case 0x08: // MPR121_BSP - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - break; - case 0x0d: // MPR121_SELECT - e.inputEvent = INPUT_BROKER_SELECT; - e.kbchar = 0x00; - break; - default: - if (nextEvent > 127) { - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = nextEvent; - break; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); - this->notifyObservers(&e); - } - } - break; - } - case 0x84: { // Adafruit TCA8418 - TCAKeyboard.trigger(); - InputEvent e = {}; - while (TCAKeyboard.hasEvent()) { - char nextEvent = TCAKeyboard.dequeueEvent(); - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x00; - e.source = this->_originName; - switch (nextEvent) { - case TCA8418KeyboardBase::NONE: - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::REBOOT: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_REBOOT; - break; - case TCA8418KeyboardBase::LEFT: - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::UP: - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::DOWN: - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::RIGHT: - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::BSP: - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0x08; - break; - case TCA8418KeyboardBase::SELECT: - e.inputEvent = INPUT_BROKER_SELECT; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::ESC: - e.inputEvent = INPUT_BROKER_CANCEL; - e.kbchar = 0x00; - break; - case TCA8418KeyboardBase::GPS_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_GPS_TOGGLE; - break; - case TCA8418KeyboardBase::SEND_PING: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_SEND_PING; - break; - case TCA8418KeyboardBase::MUTE_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; - break; - case TCA8418KeyboardBase::BT_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; - break; - case TCA8418KeyboardBase::BL_TOGGLE: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; - break; - case TCA8418KeyboardBase::TAB: - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_TAB; - break; - default: - if (nextEvent > 127) { - e.inputEvent = INPUT_BROKER_NONE; - e.kbchar = 0x00; - break; - } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = nextEvent; - break; - } - if (e.inputEvent != INPUT_BROKER_NONE) { - // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); - this->notifyObservers(&e); - } - TCAKeyboard.trigger(); - } - TCAKeyboard.clearInt(); - break; - } - case 0x02: { - // RAK14004 - uint8_t rDataBuf[8] = {0}; - uint8_t PrintDataBuf = 0; - if (read_from_14004(i2cBus, 0x01, rDataBuf, 0x04) == 1) { - for (uint8_t aCount = 0; aCount < 0x04; aCount++) { - for (uint8_t bCount = 0; bCount < 0x04; bCount++) { - if (((rDataBuf[aCount] >> bCount) & 0x01) == 0x01) { - PrintDataBuf = aCount * 0x04 + bCount + 1; - } - } - } - } - if (PrintDataBuf != 0) { - LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_MATRIXKEY; - e.source = this->_originName; - e.kbchar = PrintDataBuf; - this->notifyObservers(&e); - } - break; - } - case 0x00: // CARDKB - case 0x10: { // T-DECK + i2cBus->requestFrom((int)cardkb_found.address, 1); - i2cBus->requestFrom((int)cardkb_found.address, 1); + if (i2cBus->available()) { + char c = i2cBus->read(); + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + switch (c) { + case 0x71: // This is the button q. If modifier and q pressed, it cancels the input + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_CANCEL; + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x74: // letter t. if modifier and t pressed call 'tab' + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x09; // TAB Scancode + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x6d: // letter m. Modifier makes it mute notifications + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x6f: // letter o(+). Modifier makes screen increase in brightness + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x69: // letter i(-). Modifier makes screen decrease in brightness + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x20: // Space. Send network ping like double press does + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x67: // letter g. toggle gps + if (is_sym) { + is_sym = false; + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = INPUT_BROKER_GPS_TOGGLE; + } else { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } + break; + case 0x1b: // ESC + e.inputEvent = INPUT_BROKER_CANCEL; + break; + case 0x08: // Back + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; + break; + case 0xb5: // Up + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0; + break; + case 0xb6: // Down + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; + break; + case 0xb4: // Left + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; + break; + case 0xb7: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; + break; + case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) + // toggle moddifiers button. + is_sym = !is_sym; + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the + : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active + break; + case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = c; + break; + case 0xaf: // fn+space INPUT_BROKER_SEND_PING + e.inputEvent = INPUT_BROKER_SEND_PING; + e.kbchar = c; + break; + case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN + e.inputEvent = INPUT_BROKER_SHUTDOWN; + e.kbchar = c; + break; - if (i2cBus->available()) { - char c = i2cBus->read(); - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - switch (c) { - case 0x71: // This is the button q. If modifier and q pressed, it cancels the input - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_CANCEL; - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x74: // letter t. if modifier and t pressed call 'tab' - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = 0x09; // TAB Scancode - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x6d: // letter m. Modifier makes it mute notifications - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x6f: // letter o(+). Modifier makes screen increase in brightness - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x69: // letter i(-). Modifier makes screen decrease in brightness - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x20: // Space. Send network ping like double press does - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x67: // letter g. toggle gps - if (is_sym) { - is_sym = false; - e.inputEvent = INPUT_BROKER_GPS_TOGGLE; - e.kbchar = INPUT_BROKER_GPS_TOGGLE; - } else { - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - } - break; - case 0x1b: // ESC - e.inputEvent = INPUT_BROKER_CANCEL; - break; - case 0x08: // Back - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0; - break; - case 0xb5: // Up - e.inputEvent = INPUT_BROKER_UP; - e.kbchar = 0; - break; - case 0xb6: // Down - e.inputEvent = INPUT_BROKER_DOWN; - e.kbchar = 0; - break; - case 0xb4: // Left - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0; - break; - case 0xb7: // Right - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0; - break; - case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) - // toggle moddifiers button. - is_sym = !is_sym; - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the - : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active - break; - case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE - e.inputEvent = INPUT_BROKER_GPS_TOGGLE; - e.kbchar = c; - break; - case 0xaf: // fn+space INPUT_BROKER_SEND_PING - e.inputEvent = INPUT_BROKER_SEND_PING; - e.kbchar = c; - break; - case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN - e.inputEvent = INPUT_BROKER_SHUTDOWN; - e.kbchar = c; - break; + case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT + case 0x91: // fn+t + case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE + case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE + case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST + // just pass those unmodified + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + break; + case 0x0d: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + break; + case 0x00: // nopress + e.inputEvent = INPUT_BROKER_NONE; + break; + default: // all other keys + if (c > 127) { // bogus key value + e.inputEvent = INPUT_BROKER_NONE; + break; + } + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + is_sym = false; + break; + } - case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT - case 0x91: // fn+t - case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE - case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE - case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST - // just pass those unmodified - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - break; - case 0x0d: // Enter - e.inputEvent = INPUT_BROKER_SELECT; - break; - case 0x00: // nopress - e.inputEvent = INPUT_BROKER_NONE; - break; - default: // all other keys - if (c > 127) { // bogus key value - e.inputEvent = INPUT_BROKER_NONE; - break; + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } } - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = c; - is_sym = false; break; - } - - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } } - break; - } - default: - LOG_WARN("Unknown kb_model 0x%02x", kb_model); - } - return 300; + default: + LOG_WARN("Unknown kb_model 0x%02x", kb_model); + } + return 300; } -void KbI2cBase::toggleBacklight(bool on) { +void KbI2cBase::toggleBacklight(bool on) +{ #if defined(T_LORA_PAGER) - TCAKeyboard.setBacklight(on); + TCAKeyboard.setBacklight(on); #endif } diff --git a/src/input/kbI2cBase.h b/src/input/kbI2cBase.h index 2b6af0a7e..ae769dff8 100644 --- a/src/input/kbI2cBase.h +++ b/src/input/kbI2cBase.h @@ -8,21 +8,22 @@ class TCA8418KeyboardBase; -class KbI2cBase : public Observable, public concurrency::OSThread { -public: - explicit KbI2cBase(const char *name); - void toggleBacklight(bool on); +class KbI2cBase : public Observable, public concurrency::OSThread +{ + public: + explicit KbI2cBase(const char *name); + void toggleBacklight(bool on); -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; -private: - const char *_originName; + private: + const char *_originName; - TwoWire *i2cBus = 0; + TwoWire *i2cBus = 0; - BBQ10Keyboard Q10keyboard; - MPR121Keyboard MPRkeyboard; - TCA8418KeyboardBase &TCAKeyboard; - bool is_sym = false; + BBQ10Keyboard Q10keyboard; + MPR121Keyboard MPRkeyboard; + TCA8418KeyboardBase &TCAKeyboard; + bool is_sym = false; }; \ No newline at end of file diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp index bbd238dfe..18243f3bf 100644 --- a/src/input/kbMatrixBase.cpp +++ b/src/input/kbMatrixBase.cpp @@ -30,98 +30,102 @@ unsigned char KeyMap[3][sizeof(keys_rows)][sizeof(keys_cols)] = {{{' ', '.', 'm' {'1', '2', '3', '4', '5', 0x1a}}}; #endif -KbMatrixBase::KbMatrixBase(const char *name) : concurrency::OSThread(name) { this->_originName = name; } +KbMatrixBase::KbMatrixBase(const char *name) : concurrency::OSThread(name) +{ + this->_originName = name; +} -int32_t KbMatrixBase::runOnce() { - if (!INPUTBROKER_MATRIX_TYPE) { - // Input device is not requested. - return disable(); - } - - if (firstTime) { - // This is the first time the OSThread library has called this function, so do port setup - firstTime = 0; - for (byte i = 0; i < sizeof(keys_rows); i++) { - pinMode(keys_rows[i], OUTPUT); - digitalWrite(keys_rows[i], HIGH); +int32_t KbMatrixBase::runOnce() +{ + if (!INPUTBROKER_MATRIX_TYPE) { + // Input device is not requested. + return disable(); } - for (byte i = 0; i < sizeof(keys_cols); i++) { - pinMode(keys_cols[i], INPUT_PULLUP); - } - } - key = 0; - - if (INPUTBROKER_MATRIX_TYPE == 1) { - // scan for keypresses - for (byte i = 0; i < sizeof(keys_rows); i++) { - digitalWrite(keys_rows[i], LOW); - for (byte j = 0; j < sizeof(keys_cols); j++) { - if (digitalRead(keys_cols[j]) == LOW) { - key = KeyMap[shift][i][j]; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do port setup + firstTime = 0; + for (byte i = 0; i < sizeof(keys_rows); i++) { + pinMode(keys_rows[i], OUTPUT); + digitalWrite(keys_rows[i], HIGH); } - } - digitalWrite(keys_rows[i], HIGH); - } - // debounce - if (key != prevkey) { - if (key != 0) { - LOG_DEBUG("Key 0x%x pressed", key); - // reset shift now that we have a keypress - InputEvent e = {}; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->_originName; - switch (key) { - case 0x1b: // ESC - e.inputEvent = INPUT_BROKER_CANCEL; - break; - case 0x08: // Back - e.inputEvent = INPUT_BROKER_BACK; - e.kbchar = 0; - break; - case 0xb5: // Up - e.inputEvent = INPUT_BROKER_UP; - break; - case 0xb6: // Down - e.inputEvent = INPUT_BROKER_DOWN; - break; - case 0xb4: // Left - e.inputEvent = INPUT_BROKER_LEFT; - e.kbchar = 0; - break; - case 0xb7: // Right - e.inputEvent = INPUT_BROKER_RIGHT; - e.kbchar = 0; - break; - case 0x0d: // Enter - e.inputEvent = INPUT_BROKER_SELECT; - break; - case 0x00: // nopress - e.inputEvent = INPUT_BROKER_NONE; - break; - case 0x1a: // Shift - shift++; - if (shift > 2) { - shift = 0; - } - break; - default: // all other keys - e.inputEvent = INPUT_BROKER_ANYKEY; - e.kbchar = key; - break; + for (byte i = 0; i < sizeof(keys_cols); i++) { + pinMode(keys_cols[i], INPUT_PULLUP); } - if (e.inputEvent != INPUT_BROKER_NONE) { - this->notifyObservers(&e); - } - } - prevkey = key; } - } else { - LOG_WARN("Unknown kb_model 0x%02x", INPUTBROKER_MATRIX_TYPE); - return disable(); - } - return 50; // Keyscan every 50msec to avoid key bounce + key = 0; + + if (INPUTBROKER_MATRIX_TYPE == 1) { + // scan for keypresses + for (byte i = 0; i < sizeof(keys_rows); i++) { + digitalWrite(keys_rows[i], LOW); + for (byte j = 0; j < sizeof(keys_cols); j++) { + if (digitalRead(keys_cols[j]) == LOW) { + key = KeyMap[shift][i][j]; + } + } + digitalWrite(keys_rows[i], HIGH); + } + // debounce + if (key != prevkey) { + if (key != 0) { + LOG_DEBUG("Key 0x%x pressed", key); + // reset shift now that we have a keypress + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + switch (key) { + case 0x1b: // ESC + e.inputEvent = INPUT_BROKER_CANCEL; + break; + case 0x08: // Back + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; + break; + case 0xb5: // Up + e.inputEvent = INPUT_BROKER_UP; + break; + case 0xb6: // Down + e.inputEvent = INPUT_BROKER_DOWN; + break; + case 0xb4: // Left + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; + break; + case 0xb7: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; + break; + case 0x0d: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + break; + case 0x00: // nopress + e.inputEvent = INPUT_BROKER_NONE; + break; + case 0x1a: // Shift + shift++; + if (shift > 2) { + shift = 0; + } + break; + default: // all other keys + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = key; + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + prevkey = key; + } + + } else { + LOG_WARN("Unknown kb_model 0x%02x", INPUTBROKER_MATRIX_TYPE); + return disable(); + } + return 50; // Keyscan every 50msec to avoid key bounce } #endif // INPUTBROKER_MATRIX_TYPE \ No newline at end of file diff --git a/src/input/kbMatrixBase.h b/src/input/kbMatrixBase.h index 873979d13..8259fc07c 100644 --- a/src/input/kbMatrixBase.h +++ b/src/input/kbMatrixBase.h @@ -3,17 +3,18 @@ #include "InputBroker.h" #include "concurrency/OSThread.h" -class KbMatrixBase : public Observable, public concurrency::OSThread { -public: - explicit KbMatrixBase(const char *name); +class KbMatrixBase : public Observable, public concurrency::OSThread +{ + public: + explicit KbMatrixBase(const char *name); -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; -private: - const char *_originName; - bool firstTime = 1; - int shift = 0; - char key = 0; - char prevkey = 0; + private: + const char *_originName; + bool firstTime = 1; + int shift = 0; + char key = 0; + char prevkey = 0; }; \ No newline at end of file diff --git a/src/input/kbMatrixImpl.cpp b/src/input/kbMatrixImpl.cpp index 6b6eb8c69..0561b16fe 100644 --- a/src/input/kbMatrixImpl.cpp +++ b/src/input/kbMatrixImpl.cpp @@ -7,13 +7,14 @@ KbMatrixImpl *kbMatrixImpl; KbMatrixImpl::KbMatrixImpl() : KbMatrixBase("matrixKB") {} -void KbMatrixImpl::init() { - if (!INPUTBROKER_MATRIX_TYPE) { - disable(); - return; - } +void KbMatrixImpl::init() +{ + if (!INPUTBROKER_MATRIX_TYPE) { + disable(); + return; + } - inputBroker->registerSource(this); + inputBroker->registerSource(this); } #endif // INPUTBROKER_MATRIX_TYPE \ No newline at end of file diff --git a/src/input/kbMatrixImpl.h b/src/input/kbMatrixImpl.h index 1d26a93df..ead4a2d57 100644 --- a/src/input/kbMatrixImpl.h +++ b/src/input/kbMatrixImpl.h @@ -9,10 +9,11 @@ * to your device as you wish, but you always need to have separate event * handlers, thus you need to have a RotaryEncoderInterrupt implementation. */ -class KbMatrixImpl : public KbMatrixBase { -public: - KbMatrixImpl(); - void init(); +class KbMatrixImpl : public KbMatrixBase +{ + public: + KbMatrixImpl(); + void init(); }; extern KbMatrixImpl *kbMatrixImpl; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 1f9e2dbc2..9ac060d37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -230,36 +230,38 @@ Router *router = NULL; // Users of router don't care what sort of subclass imple const char *firmware_version = optstr(APP_VERSION_SHORT); -const char *getDeviceName() { - uint8_t dmac[6]; +const char *getDeviceName() +{ + uint8_t dmac[6]; - getMacAddr(dmac); + getMacAddr(dmac); - // Meshtastic_ab3c or Shortname_abcd - static char name[20]; - snprintf(name, sizeof(name), "%02x%02x", dmac[4], dmac[5]); - // if the shortname exists and is NOT the new default of ab3c, use it for BLE name. - if (strcmp(owner.short_name, name) != 0) { - snprintf(name, sizeof(name), "%s_%02x%02x", owner.short_name, dmac[4], dmac[5]); - } else { - snprintf(name, sizeof(name), "Meshtastic_%02x%02x", dmac[4], dmac[5]); - } - return name; + // Meshtastic_ab3c or Shortname_abcd + static char name[20]; + snprintf(name, sizeof(name), "%02x%02x", dmac[4], dmac[5]); + // if the shortname exists and is NOT the new default of ab3c, use it for BLE name. + if (strcmp(owner.short_name, name) != 0) { + snprintf(name, sizeof(name), "%s_%02x%02x", owner.short_name, dmac[4], dmac[5]); + } else { + snprintf(name, sizeof(name), "Meshtastic_%02x%02x", dmac[4], dmac[5]); + } + return name; } -static int32_t ledBlinker() { - // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if - // config.device.led_heartbeat_disabled is changed - if (config.device.led_heartbeat_disabled) - return 1000; +static int32_t ledBlinker() +{ + // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if + // config.device.led_heartbeat_disabled is changed + if (config.device.led_heartbeat_disabled) + return 1000; - static bool ledOn; - ledOn ^= 1; + static bool ledOn; + ledOn ^= 1; - ledBlink.set(ledOn); + ledBlink.set(ledOn); - // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that - return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); + // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that + return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); } uint32_t timeLastPowered = 0; @@ -276,7 +278,10 @@ RadioLibHal *RadioLibHAL = NULL; /** * Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep. */ -__attribute__((weak, noinline)) bool loopCanSleep() { return true; } +__attribute__((weak, noinline)) bool loopCanSleep() +{ + return true; +} // Weak empty variant initialization function. // May be redefined by variant files. @@ -286,224 +291,228 @@ void lateInitVariant() {} /** * Print info as a structured log message (for automated log processing) */ -void printInfo() { LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); } +void printInfo() +{ + LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); +} #ifndef PIO_UNIT_TESTING -void setup() { +void setup() +{ #if defined(R1_NEO) - pinMode(DCDC_EN_HOLD, OUTPUT); - digitalWrite(DCDC_EN_HOLD, HIGH); - pinMode(NRF_ON, OUTPUT); - digitalWrite(NRF_ON, HIGH); + pinMode(DCDC_EN_HOLD, OUTPUT); + digitalWrite(DCDC_EN_HOLD, HIGH); + pinMode(NRF_ON, OUTPUT); + digitalWrite(NRF_ON, HIGH); #endif #if defined(PIN_POWER_EN) - pinMode(PIN_POWER_EN, OUTPUT); - digitalWrite(PIN_POWER_EN, HIGH); + pinMode(PIN_POWER_EN, OUTPUT); + digitalWrite(PIN_POWER_EN, HIGH); #endif #if defined(ELECROW_ThinkNode_M5) - Wire.begin(48, 47); - io.pinMode(PCA_PIN_EINK_EN, OUTPUT); - io.pinMode(PCA_PIN_POWER_EN, OUTPUT); - io.digitalWrite(PCA_PIN_POWER_EN, HIGH); - // io.pinMode(C2_PIN, OUTPUT); + Wire.begin(48, 47); + io.pinMode(PCA_PIN_EINK_EN, OUTPUT); + io.pinMode(PCA_PIN_POWER_EN, OUTPUT); + io.digitalWrite(PCA_PIN_POWER_EN, HIGH); + // io.pinMode(C2_PIN, OUTPUT); #endif #ifdef LED_POWER - pinMode(LED_POWER, OUTPUT); - digitalWrite(LED_POWER, LED_STATE_ON); + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LED_STATE_ON); #endif #ifdef USER_LED - pinMode(USER_LED, OUTPUT); - digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); + pinMode(USER_LED, OUTPUT); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); #endif #ifdef WIFI_LED - pinMode(WIFI_LED, OUTPUT); - digitalWrite(WIFI_LED, LOW); + pinMode(WIFI_LED, OUTPUT); + digitalWrite(WIFI_LED, LOW); #endif #ifdef BLE_LED - pinMode(BLE_LED, OUTPUT); + pinMode(BLE_LED, OUTPUT); #ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); + digitalWrite(BLE_LED, HIGH); #else - digitalWrite(BLE_LED, LOW); + digitalWrite(BLE_LED, LOW); #endif #endif #if defined(T_DECK) - // GPIO10 manages all peripheral power supplies - // Turn on peripheral power immediately after MUC starts. - // If some boards are turned on late, ESP32 will reset due to low voltage. - // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , - // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) - pinMode(KB_POWERON, OUTPUT); - digitalWrite(KB_POWERON, HIGH); - // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus - // We need to initialize all CS pins in advance otherwise there will be SPI communication issues - // e.g. when detecting the SD card - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - delay(100); + // GPIO10 manages all peripheral power supplies + // Turn on peripheral power immediately after MUC starts. + // If some boards are turned on late, ESP32 will reset due to low voltage. + // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , + // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) + pinMode(KB_POWERON, OUTPUT); + digitalWrite(KB_POWERON, HIGH); + // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus + // We need to initialize all CS pins in advance otherwise there will be SPI communication issues + // e.g. when detecting the SD card + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + delay(100); #elif defined(T_DECK_PRO) - pinMode(LORA_EN, OUTPUT); - digitalWrite(LORA_EN, HIGH); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(PIN_EINK_CS, OUTPUT); - digitalWrite(PIN_EINK_CS, HIGH); + pinMode(LORA_EN, OUTPUT); + digitalWrite(LORA_EN, HIGH); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(PIN_EINK_CS, OUTPUT); + digitalWrite(PIN_EINK_CS, HIGH); #elif defined(T_LORA_PAGER) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - pinMode(KB_INT, INPUT_PULLUP); - // io expander - io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); - io.pinMode(EXPANDS_DRV_EN, OUTPUT); - io.digitalWrite(EXPANDS_DRV_EN, HIGH); - io.pinMode(EXPANDS_AMP_EN, OUTPUT); - io.digitalWrite(EXPANDS_AMP_EN, LOW); - io.pinMode(EXPANDS_LORA_EN, OUTPUT); - io.digitalWrite(EXPANDS_LORA_EN, HIGH); - io.pinMode(EXPANDS_GPS_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPS_EN, HIGH); - io.pinMode(EXPANDS_KB_EN, OUTPUT); - io.digitalWrite(EXPANDS_KB_EN, HIGH); - io.pinMode(EXPANDS_SD_EN, OUTPUT); - io.digitalWrite(EXPANDS_SD_EN, HIGH); - io.pinMode(EXPANDS_GPIO_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPIO_EN, HIGH); - io.pinMode(EXPANDS_SD_PULLEN, INPUT); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + pinMode(KB_INT, INPUT_PULLUP); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); #elif defined(HACKADAY_COMMUNICATOR) - pinMode(KB_INT, INPUT); + pinMode(KB_INT, INPUT); #endif - concurrency::hasBeenSetup = true; + concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO - SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); + SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else - SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); + SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif - meshtastic_Config_DisplayConfig_OledType screen_model = - meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; - OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; + meshtastic_Config_DisplayConfig_OledType screen_model = + meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; + OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; #ifdef USE_SEGGER - auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; + auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; #ifdef NRF52840_XXAA - auto buflen = 4096; // this board has a fair amount of ram + auto buflen = 4096; // this board has a fair amount of ram #else - auto buflen = 256; // this board has a fair amount of ram + auto buflen = 256; // this board has a fair amount of ram #endif - SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode); + SEGGER_RTT_ConfigUpBuffer(SEGGER_STDOUT_CH, NULL, NULL, buflen, mode); #endif #ifdef DEBUG_PORT - consoleInit(); // Set serial baud rate and init our mesh console + consoleInit(); // Set serial baud rate and init our mesh console #endif #ifdef UNPHONE - unphone.printStore(); + unphone.printStore(); #endif #if ARCH_PORTDUINO - RTCQuality ourQuality = RTCQualityDevice; + RTCQuality ourQuality = RTCQualityDevice; - std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); - if (timeCommandResult[0] == '1') { - ourQuality = RTCQualityNTP; - } + std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); + if (timeCommandResult[0] == '1') { + ourQuality = RTCQualityNTP; + } - struct timeval tv; - tv.tv_sec = time(NULL); - tv.tv_usec = 0; - perhapsSetRTC(ourQuality, &tv); + struct timeval tv; + tv.tv_sec = time(NULL); + tv.tv_usec = 0; + perhapsSetRTC(ourQuality, &tv); #endif - powerMonInit(); - serialSinceMsec = millis(); + powerMonInit(); + serialSinceMsec = millis(); - LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); + LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR - // use PSRAM for malloc calls > 256 bytes - heap_caps_malloc_extmem_enable(256); + // use PSRAM for malloc calls > 256 bytes + heap_caps_malloc_extmem_enable(256); #endif #endif #if defined(DEBUG_MUTE) && defined(DEBUG_PORT) - DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); - DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); - DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n"); + DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); + DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); + DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n"); #endif - initDeepSleep(); + initDeepSleep(); #if defined(MODEM_POWER_EN) - pinMode(MODEM_POWER_EN, OUTPUT); - digitalWrite(MODEM_POWER_EN, LOW); + pinMode(MODEM_POWER_EN, OUTPUT); + digitalWrite(MODEM_POWER_EN, LOW); #endif #if defined(MODEM_PWRKEY) - pinMode(MODEM_PWRKEY, OUTPUT); - digitalWrite(MODEM_PWRKEY, LOW); + pinMode(MODEM_PWRKEY, OUTPUT); + digitalWrite(MODEM_PWRKEY, LOW); #endif #if defined(LORA_TCXO_GPIO) - pinMode(LORA_TCXO_GPIO, OUTPUT); - digitalWrite(LORA_TCXO_GPIO, HIGH); + pinMode(LORA_TCXO_GPIO, OUTPUT); + digitalWrite(LORA_TCXO_GPIO, HIGH); #endif #if defined(VEXT_ENABLE) - pinMode(VEXT_ENABLE, OUTPUT); - digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power + pinMode(VEXT_ENABLE, OUTPUT); + digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power #endif #if defined(BIAS_T_ENABLE) - pinMode(BIAS_T_ENABLE, OUTPUT); - digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna + pinMode(BIAS_T_ENABLE, OUTPUT); + digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna #endif #if defined(VTFT_CTRL) - pinMode(VTFT_CTRL, OUTPUT); - digitalWrite(VTFT_CTRL, LOW); + pinMode(VTFT_CTRL, OUTPUT); + digitalWrite(VTFT_CTRL, LOW); #endif #ifdef RESET_OLED - pinMode(RESET_OLED, OUTPUT); - digitalWrite(RESET_OLED, 1); - delay(2); - digitalWrite(RESET_OLED, 0); - delay(10); - digitalWrite(RESET_OLED, 1); + pinMode(RESET_OLED, OUTPUT); + digitalWrite(RESET_OLED, 1); + delay(2); + digitalWrite(RESET_OLED, 0); + delay(10); + digitalWrite(RESET_OLED, 1); #endif #ifdef SENSOR_POWER_CTRL_PIN - pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); - digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); + pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); #endif #ifdef SENSOR_GPS_CONFLICT - bool sensor_detected = false; + bool sensor_detected = false; #endif #ifdef PERIPHERAL_WARMUP_MS - // Some peripherals may require additional time to stabilize after power is connected - // e.g. I2C on Heltec Vision Master - LOG_INFO("Wait for peripherals to stabilize"); - delay(PERIPHERAL_WARMUP_MS); + // Some peripherals may require additional time to stabilize after power is connected + // e.g. I2C on Heltec Vision Master + LOG_INFO("Wait for peripherals to stabilize"); + delay(PERIPHERAL_WARMUP_MS); #endif #ifdef BUTTON_PIN @@ -511,200 +520,199 @@ void setup() { #if ESP_ARDUINO_VERSION_MAJOR >= 3 #ifdef BUTTON_NEED_PULLUP - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); #else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN #endif #else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN #ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - delay(10); + gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + delay(10); #endif #ifdef BUTTON_NEED_PULLUP2 - gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); - delay(10); + gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); + delay(10); #endif #endif #endif #endif - initSPI(); + initSPI(); - OSThread::setup(); + OSThread::setup(); #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - // The ThinkNodes have their own blink logic - // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); + // The ThinkNodes have their own blink logic + // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); #else - ledPeriodic = new Periodic("Blink", ledBlinker); + ledPeriodic = new Periodic("Blink", ledBlinker); #endif - fsInit(); + fsInit(); #if !MESHTASTIC_EXCLUDE_I2C #if defined(I2C_SDA1) && defined(ARCH_RP2040) - Wire1.setSDA(I2C_SDA1); - Wire1.setSCL(I2C_SCL1); - Wire1.begin(); + Wire1.setSDA(I2C_SDA1); + Wire1.setSCL(I2C_SCL1); + Wire1.begin(); #elif defined(I2C_SDA1) && !defined(ARCH_RP2040) - Wire1.begin(I2C_SDA1, I2C_SCL1); + Wire1.begin(I2C_SDA1, I2C_SCL1); #elif WIRE_INTERFACES_COUNT == 2 - Wire1.begin(); + Wire1.begin(); #endif #if defined(I2C_SDA) && defined(ARCH_RP2040) - Wire.setSDA(I2C_SDA); - Wire.setSCL(I2C_SCL); - Wire.begin(); + Wire.setSDA(I2C_SDA); + Wire.setSCL(I2C_SCL); + Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) - Wire.begin(I2C_SDA, I2C_SCL); + Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) - if (portduino_config.i2cdev != "") { - LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); - Wire.begin(portduino_config.i2cdev.c_str()); - } else { - LOG_INFO("No I2C device configured, Skip"); - } + if (portduino_config.i2cdev != "") { + LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); + Wire.begin(portduino_config.i2cdev.c_str()); + } else { + LOG_INFO("No I2C device configured, Skip"); + } #elif HAS_WIRE - Wire.begin(); + Wire.begin(); #endif #endif #if defined(M5STACK_UNITC6L) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, 1); - c6l_init(); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, 1); + c6l_init(); #endif #ifdef PIN_LCD_RESET - // FIXME - move this someplace better, LCD is at address 0x3F - pinMode(PIN_LCD_RESET, OUTPUT); - digitalWrite(PIN_LCD_RESET, 0); - delay(1); - digitalWrite(PIN_LCD_RESET, 1); - delay(1); + // FIXME - move this someplace better, LCD is at address 0x3F + pinMode(PIN_LCD_RESET, OUTPUT); + digitalWrite(PIN_LCD_RESET, 0); + delay(1); + digitalWrite(PIN_LCD_RESET, 1); + delay(1); #endif #ifdef AQ_SET_PIN - // RAK-12039 set pin for Air quality sensor. Detectable on I2C after ~3 seconds, so we need to rescan later - pinMode(AQ_SET_PIN, OUTPUT); - digitalWrite(AQ_SET_PIN, HIGH); + // RAK-12039 set pin for Air quality sensor. Detectable on I2C after ~3 seconds, so we need to rescan later + pinMode(AQ_SET_PIN, OUTPUT); + digitalWrite(AQ_SET_PIN, HIGH); #endif - // Currently only the tbeam has a PMU - // PMU initialization needs to be placed before i2c scanning - power = new Power(); - power->setStatusHandler(powerStatus); - powerStatus->observe(&power->newStatus); - power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial - // configuration + // Currently only the tbeam has a PMU + // PMU initialization needs to be placed before i2c scanning + power = new Power(); + power->setStatusHandler(powerStatus); + powerStatus->observe(&power->newStatus); + power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial configuration #if !MESHTASTIC_EXCLUDE_I2C - // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to - // accessories - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); + // We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to + // accessories + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if HAS_WIRE - LOG_INFO("Scan for i2c devices"); + LOG_INFO("Scan for i2c devices"); #endif #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif #if defined(I2C_SDA) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); -#elif defined(ARCH_PORTDUINO) - if (portduino_config.i2cdev != "") { - LOG_INFO("Scan for i2c devices"); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); - } +#elif defined(ARCH_PORTDUINO) + if (portduino_config.i2cdev != "") { + LOG_INFO("Scan for i2c devices"); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); + } #elif HAS_WIRE - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #endif - auto i2cCount = i2cScanner->countDevices(); - if (i2cCount == 0) { - LOG_INFO("No I2C devices found"); - } else { - LOG_INFO("%i I2C devices found", i2cCount); + auto i2cCount = i2cScanner->countDevices(); + if (i2cCount == 0) { + LOG_INFO("No I2C devices found"); + } else { + LOG_INFO("%i I2C devices found", i2cCount); #ifdef SENSOR_GPS_CONFLICT - sensor_detected = true; + sensor_detected = true; #endif - } + } #ifdef ARCH_ESP32 - // Don't init display if we don't have one or we are waking headless due to a timer event - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { - LOG_DEBUG("suppress screen wake because this is a headless timer wakeup"); - i2cScanner->setSuppressScreen(); - } + // Don't init display if we don't have one or we are waking headless due to a timer event + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { + LOG_DEBUG("suppress screen wake because this is a headless timer wakeup"); + i2cScanner->setSuppressScreen(); + } #endif - auto screenInfo = i2cScanner->firstScreen(); - screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; + auto screenInfo = i2cScanner->firstScreen(); + screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) { - switch (screenInfo.type) { - case ScanI2C::DeviceType::SCREEN_SH1106: - screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SH1106; - break; - case ScanI2C::DeviceType::SCREEN_SSD1306: - screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306; - break; - case ScanI2C::DeviceType::SCREEN_ST7567: - case ScanI2C::DeviceType::SCREEN_UNKNOWN: - default: - screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) { + switch (screenInfo.type) { + case ScanI2C::DeviceType::SCREEN_SH1106: + screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SH1106; + break; + case ScanI2C::DeviceType::SCREEN_SSD1306: + screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306; + break; + case ScanI2C::DeviceType::SCREEN_ST7567: + case ScanI2C::DeviceType::SCREEN_UNKNOWN: + default: + screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; + } } - } #define UPDATE_FROM_SCANNER(FIND_FN) #if defined(USE_VIRTUAL_KEYBOARD) - kb_found = true; -#endif - auto rtc_info = i2cScanner->firstRTC(); - rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; - - auto kb_info = i2cScanner->firstKeyboard(); - - if (kb_info.type != ScanI2C::DeviceType::NONE) { kb_found = true; - cardkb_found = kb_info.address; - switch (kb_info.type) { - case ScanI2C::DeviceType::RAK14004: - kb_model = 0x02; - break; - case ScanI2C::DeviceType::CARDKB: - kb_model = 0x00; - break; - case ScanI2C::DeviceType::TDECKKB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x10; - break; - case ScanI2C::DeviceType::BBQ10KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x11; - break; - case ScanI2C::DeviceType::MPR121KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x37; - break; - case ScanI2C::DeviceType::TCA8418KB: - // assign an arbitrary value to distinguish from other models - kb_model = 0x84; - break; - default: - // use this as default since it's also just zero - LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); - kb_model = 0x00; +#endif + auto rtc_info = i2cScanner->firstRTC(); + rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; + + auto kb_info = i2cScanner->firstKeyboard(); + + if (kb_info.type != ScanI2C::DeviceType::NONE) { + kb_found = true; + cardkb_found = kb_info.address; + switch (kb_info.type) { + case ScanI2C::DeviceType::RAK14004: + kb_model = 0x02; + break; + case ScanI2C::DeviceType::CARDKB: + kb_model = 0x00; + break; + case ScanI2C::DeviceType::TDECKKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x10; + break; + case ScanI2C::DeviceType::BBQ10KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x11; + break; + case ScanI2C::DeviceType::MPR121KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x37; + break; + case ScanI2C::DeviceType::TCA8418KB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x84; + break; + default: + // use this as default since it's also just zero + LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); + kb_model = 0x00; + } } - } - pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); + pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); - auto aqiInfo = i2cScanner->firstAQI(); - aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE; + auto aqiInfo = i2cScanner->firstAQI(); + aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE; /* * There are a bunch of sensors that have no further logic than to be found and stuffed into the @@ -714,379 +722,380 @@ void setup() { // Two supported RGB LED currently #ifdef HAS_RGB_LED - rgb_found = i2cScanner->firstRGBLED(); + rgb_found = i2cScanner->firstRGBLED(); #endif #ifdef HAS_TPS65233 - // TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher - // We are switching it off here since we don't use an LNB. - if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) { - Wire.beginTransmission(TPS65233_ADDR); - Wire.write(0); // Register 0 - Wire.write(128); // Turn off the LNB power, keep I2C Control enabled - Wire.endTransmission(); - Wire.beginTransmission(TPS65233_ADDR); - Wire.write(1); // Register 1 - Wire.write(0); // Turn off Tone Generator 22kHz - Wire.endTransmission(); - } + // TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher + // We are switching it off here since we don't use an LNB. + if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) { + Wire.beginTransmission(TPS65233_ADDR); + Wire.write(0); // Register 0 + Wire.write(128); // Turn off the LNB power, keep I2C Control enabled + Wire.endTransmission(); + Wire.beginTransmission(TPS65233_ADDR); + Wire.write(1); // Register 1 + Wire.write(0); // Turn off Tone Generator 22kHz + Wire.endTransmission(); + } #endif #if !defined(ARCH_STM32WL) - auto acc_info = i2cScanner->firstAccelerometer(); - accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; - LOG_DEBUG("acc_info = %i", acc_info.type); + auto acc_info = i2cScanner->firstAccelerometer(); + accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; + LOG_DEBUG("acc_info = %i", acc_info.type); #endif - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); #endif #ifdef HAS_SDCARD - setupSDCard(); + setupSDCard(); #endif - // LED init + // LED init #ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now #endif - // Hello - printInfo(); + // Hello + printInfo(); #ifdef BUILD_EPOCH - LOG_INFO("Build timestamp: %ld", BUILD_EPOCH); + LOG_INFO("Build timestamp: %ld", BUILD_EPOCH); #endif #ifdef ARCH_ESP32 - esp32Setup(); + esp32Setup(); #endif #ifdef ARCH_NRF52 - nrf52Setup(); + nrf52Setup(); #endif #ifdef ARCH_RP2040 - rp2040Setup(); + rp2040Setup(); #endif - // We do this as early as possible because this loads preferences from flash - // but we need to do this after main cpu init (esp32setup), because we need the random seed set - nodeDB = new NodeDB; + // We do this as early as possible because this loads preferences from flash + // but we need to do this after main cpu init (esp32setup), because we need the random seed set + nodeDB = new NodeDB; #if HAS_TFT - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - tftSetup(); - } + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + tftSetup(); + } #endif - router = new ReliableRouter(); + router = new ReliableRouter(); - // only play start melody when role is not tracker or sensor - if (config.power.is_power_saving == true && IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR)) - LOG_DEBUG("Tracker/Sensor: Skip start melody"); - else - playStartMelody(); + // only play start melody when role is not tracker or sensor + if (config.power.is_power_saving == true && + IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR)) + LOG_DEBUG("Tracker/Sensor: Skip start melody"); + else + playStartMelody(); - // fixed screen override? - if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) - screen_model = config.display.oled; + // fixed screen override? + if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) + screen_model = config.display.oled; #if defined(USE_SH1107) - screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 - screen_geometry = GEOMETRY_128_128; + screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 + screen_geometry = GEOMETRY_128_128; #endif #if defined(USE_SH1107_128_64) - screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 + screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 #endif #if !MESHTASTIC_EXCLUDE_I2C #if !defined(ARCH_STM32WL) - if (acc_info.type != ScanI2C::DeviceType::NONE) { - accelerometerThread = new AccelerometerThread(acc_info.type); - } + if (acc_info.type != ScanI2C::DeviceType::NONE) { + accelerometerThread = new AccelerometerThread(acc_info.type); + } #endif #if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED) - ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE); + ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE); #elif !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) - if (rgb_found.type != ScanI2C::DeviceType::NONE) { - ambientLightingThread = new AmbientLightingThread(rgb_found.type); - } + if (rgb_found.type != ScanI2C::DeviceType::NONE) { + ambientLightingThread = new AmbientLightingThread(rgb_found.type); + } #endif #endif #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - drv.begin(); - drv.selectLibrary(1); - // I2C trigger by sending 'go' command - drv.setMode(DRV2605_MODE_INTTRIG); + drv.begin(); + drv.selectLibrary(1); + // I2C trigger by sending 'go' command + drv.setMode(DRV2605_MODE_INTTRIG); #endif - // Init our SPI controller (must be before screen and lora) + // Init our SPI controller (must be before screen and lora) #ifdef ARCH_RP2040 #ifdef HW_SPI1_DEVICE - SPI1.setSCK(LORA_SCK); - SPI1.setTX(LORA_MOSI); - SPI1.setRX(LORA_MISO); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - SPI1.begin(false); + SPI1.setSCK(LORA_SCK); + SPI1.setTX(LORA_MOSI); + SPI1.setRX(LORA_MISO); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + SPI1.begin(false); #else // HW_SPI1_DEVICE - SPI.setSCK(LORA_SCK); - SPI.setTX(LORA_MOSI); - SPI.setRX(LORA_MISO); - SPI.begin(false); + SPI.setSCK(LORA_SCK); + SPI.setTX(LORA_MOSI); + SPI.setRX(LORA_MISO); + SPI.begin(false); #endif // HW_SPI1_DEVICE #elif ARCH_PORTDUINO - if (portduino_config.lora_spi_dev != "ch341") { - SPI.begin(); - } + if (portduino_config.lora_spi_dev != "ch341") { + SPI.begin(); + } #elif !defined(ARCH_ESP32) // ARCH_RP2040 #if defined(RAK3401) || defined(RAK13302) - pinMode(WB_IO2, OUTPUT); - digitalWrite(WB_IO2, HIGH); - SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); - SPI1.begin(); + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, HIGH); + SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); + SPI1.begin(); #else - SPI.begin(); + SPI.begin(); #endif #else - // ESP32 + // ESP32 #if defined(HW_SPI1_DEVICE) - SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - SPI1.setFrequency(4000000); + SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + SPI1.setFrequency(4000000); #else - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - LOG_DEBUG("SPI.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - SPI.setFrequency(4000000); + SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + LOG_DEBUG("SPI.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + SPI.setFrequency(4000000); #endif #endif - // Initialize the screen first so we can show the logo while we start up everything else. + // Initialize the screen first so we can show the logo while we start up everything else. #if HAS_SCREEN - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || defined(USE_ST7796) || \ - defined(HACKADAY_COMMUNICATOR) - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && - config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); - } + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + } #else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #endif - } + } #endif // HAS_SCREEN - // setup TZ prior to time actions. + // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ - LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string - if (*config.device.tzdef && config.device.tzdef[0] != 0) { - LOG_DEBUG("Saved TZ: %s ", config.device.tzdef); - setenv("TZ", config.device.tzdef, 1); - } else { - if (strncmp((const char *)slipstreamTZString, "tzpl", 4) == 0) { - setenv("TZ", "GMT0", 1); + LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string + if (*config.device.tzdef && config.device.tzdef[0] != 0) { + LOG_DEBUG("Saved TZ: %s ", config.device.tzdef); + setenv("TZ", config.device.tzdef, 1); } else { - setenv("TZ", (const char *)slipstreamTZString, 1); - strcpy(config.device.tzdef, (const char *)slipstreamTZString); + if (strncmp((const char *)slipstreamTZString, "tzpl", 4) == 0) { + setenv("TZ", "GMT0", 1); + } else { + setenv("TZ", (const char *)slipstreamTZString, 1); + strcpy(config.device.tzdef, (const char *)slipstreamTZString); + } } - } - tzset(); - LOG_DEBUG("Set Timezone to %s", getenv("TZ")); + tzset(); + LOG_DEBUG("Set Timezone to %s", getenv("TZ")); #endif - readFromRTC(); // read the main CPU RTC at first (in case we can't get GPS time) + readFromRTC(); // read the main CPU RTC at first (in case we can't get GPS time) #if !MESHTASTIC_EXCLUDE_GPS - // If we're taking on the repeater role, ignore GPS + // If we're taking on the repeater role, ignore GPS #ifdef SENSOR_GPS_CONFLICT - if (sensor_detected == false) { + if (sensor_detected == false) { #endif - if (HAS_GPS) { - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - gps = GPS::createGps(); - if (gps) { - gpsStatus->observe(&gps->newStatus); - } else { - LOG_DEBUG("Run without GPS"); + if (HAS_GPS) { + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + gps = GPS::createGps(); + if (gps) { + gpsStatus->observe(&gps->newStatus); + } else { + LOG_DEBUG("Run without GPS"); + } + } } - } - } #ifdef SENSOR_GPS_CONFLICT - } + } #endif #endif - nodeStatus->observe(&nodeDB->newStatus); + nodeStatus->observe(&nodeDB->newStatus); #ifdef HAS_I2S - LOG_DEBUG("Start audio thread"); - audioThread = new AudioThread(); + LOG_DEBUG("Start audio thread"); + audioThread = new AudioThread(); #endif #ifdef HAS_UDP_MULTICAST - LOG_DEBUG("Start multicast thread"); - udpHandler = new UdpMulticastHandler(); + LOG_DEBUG("Start multicast thread"); + udpHandler = new UdpMulticastHandler(); #ifdef ARCH_PORTDUINO - // FIXME: portduino does not ever call onNetworkConnected so call it here because I don't know what happen if I call - // onNetworkConnected there - if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->start(); - } + // FIXME: portduino does not ever call onNetworkConnected so call it here because I don't know what happen if I call + // onNetworkConnected there + if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->start(); + } #endif #endif - service = new MeshService(); - service->init(); + service = new MeshService(); + service->init(); - // Now that the mesh service is created, create any modules - setupModules(); + // Now that the mesh service is created, create any modules + setupModules(); #if !MESHTASTIC_EXCLUDE_I2C - // Inform modules about I2C devices - ScanI2CCompleted(i2cScanner.get()); - i2cScanner.reset(); + // Inform modules about I2C devices + ScanI2CCompleted(i2cScanner.get()); + i2cScanner.reset(); #endif #if !defined(MESHTASTIC_EXCLUDE_PKI) - // warn the user about a low entropy key - if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) { - LOG_WARN(LOW_ENTROPY_WARNING); - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, LOW_ENTROPY_WARNING); - service->sendClientNotification(cn); - nodeDB->hasWarned = true; - } + // warn the user about a low entropy key + if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) { + LOG_WARN(LOW_ENTROPY_WARNING); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, LOW_ENTROPY_WARNING); + service->sendClientNotification(cn); + nodeDB->hasWarned = true; + } #endif // buttons are now inputBroker, so have to come after setupModules #if HAS_BUTTON - int pullup_sense = 0; + int pullup_sense = 0; #ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did #ifdef BUTTON_SENSE_TYPE - pullup_sense = BUTTON_SENSE_TYPE; + pullup_sense = BUTTON_SENSE_TYPE; #else - pullup_sense = INPUT_PULLUP_SENSE; + pullup_sense = INPUT_PULLUP_SENSE; #endif #endif #if defined(ARCH_PORTDUINO) - if (portduino_config.userButtonPin.enabled) { + if (portduino_config.userButtonPin.enabled) { - LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig config; - config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; - config.activeLow = true; - config.activePullup = true; - config.pullupSense = INPUT_PULLUP; - config.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - config.singlePress = INPUT_BROKER_USER_PRESS; - config.longPress = INPUT_BROKER_SELECT; - UserButtonThread->initButton(config); + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); + } } - } #endif #ifdef BUTTON_PIN_TOUCH - TouchButtonThread = new ButtonThread("BackButton"); - ButtonConfig touchConfig; - touchConfig.pinNumber = BUTTON_PIN_TOUCH; - touchConfig.activeLow = true; - touchConfig.activePullup = true; - touchConfig.pullupSense = pullup_sense; - touchConfig.intRoutine = []() { - TouchButtonThread->userButton.tick(); - TouchButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - touchConfig.singlePress = INPUT_BROKER_NONE; - touchConfig.longPress = INPUT_BROKER_BACK; - TouchButtonThread->initButton(touchConfig); + TouchButtonThread = new ButtonThread("BackButton"); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + TouchButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; + TouchButtonThread->initButton(touchConfig); #endif #if defined(CANCEL_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - CancelButtonThread = new ButtonThread("CancelButton"); - ButtonConfig cancelConfig; - cancelConfig.pinNumber = CANCEL_BUTTON_PIN; - cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; - cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; - cancelConfig.pullupSense = pullup_sense; - cancelConfig.intRoutine = []() { - CancelButtonThread->userButton.tick(); - CancelButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - cancelConfig.singlePress = INPUT_BROKER_CANCEL; - cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; - cancelConfig.longPressTime = 4000; - CancelButtonThread->initButton(cancelConfig); + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + CancelButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); #endif #if defined(ALT_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - BackButtonThread = new ButtonThread("BackButton"); - ButtonConfig backConfig; - backConfig.pinNumber = ALT_BUTTON_PIN; - backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; - backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; - backConfig.pullupSense = pullup_sense; - backConfig.intRoutine = []() { - BackButtonThread->userButton.tick(); - BackButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - backConfig.singlePress = INPUT_BROKER_ALT_PRESS; - backConfig.longPress = INPUT_BROKER_ALT_LONG; - backConfig.longPressTime = 500; - BackButtonThread->initButton(backConfig); + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + BackButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); #endif #if defined(BUTTON_PIN) #if defined(USERPREFS_BUTTON_PIN) - int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; + int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; #else - int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; + int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; #endif #ifndef BUTTON_ACTIVE_LOW #define BUTTON_ACTIVE_LOW true @@ -1095,409 +1104,411 @@ void setup() { #define BUTTON_ACTIVE_PULLUP true #endif - // Buttons. Moved here cause we need NodeDB to be initialized - // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig userConfig; - userConfig.pinNumber = (uint8_t)_pinNum; - userConfig.activeLow = BUTTON_ACTIVE_LOW; - userConfig.activePullup = BUTTON_ACTIVE_PULLUP; - userConfig.pullupSense = pullup_sense; - userConfig.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfig.singlePress = INPUT_BROKER_USER_PRESS; - userConfig.longPress = INPUT_BROKER_SELECT; - userConfig.longPressTime = 500; - userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; - UserButtonThread->initButton(userConfig); - } else { - ButtonConfig userConfigNoScreen; - userConfigNoScreen.pinNumber = (uint8_t)_pinNum; - userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; - userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; - userConfigNoScreen.pullupSense = pullup_sense; - userConfigNoScreen.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; - userConfigNoScreen.longPress = INPUT_BROKER_NONE; - userConfigNoScreen.longPressTime = 500; - userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; - userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; - userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; - UserButtonThread->initButton(userConfigNoScreen); - } + // Buttons. Moved here cause we need NodeDB to be initialized + // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_NONE; + userConfigNoScreen.longPressTime = 500; + userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } #endif #endif #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS - // After modules are setup, so we can observe modules - setupNicheGraphics(); + // After modules are setup, so we can observe modules + setupNicheGraphics(); #endif #ifdef LED_PIN - // Turn LED off after boot, if heartbeat by config - if (config.device.led_heartbeat_disabled) - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); + // Turn LED off after boot, if heartbeat by config + if (config.device.led_heartbeat_disabled) + digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); #endif // Do this after service.init (because that clears error_code) #ifdef HAS_PMU - if (!pmu_found) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_AXP192); // Record a hardware fault for missing hardware + if (!pmu_found) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_AXP192); // Record a hardware fault for missing hardware #endif #if !MESHTASTIC_EXCLUDE_I2C // Don't call screen setup until after nodedb is setup (because we need // the current region name) -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_ST7796) || defined(USE_SPISSD1306) || \ - defined(HACKADAY_COMMUNICATOR) - if (screen) - screen->setup(); +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(HACKADAY_COMMUNICATOR) + if (screen) + screen->setup(); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && - config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - screen->setup(); - } + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen->setup(); + } #else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) - screen->setup(); + if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) + screen->setup(); #endif #endif #ifdef ARCH_PORTDUINO - // as one can't use a function pointer to the class constructor: - auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) { - switch (portduino_config.lora_module) { - case use_rf95: - return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); - case use_sx1262: - return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); - case use_sx1268: - return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); - case use_sx1280: - return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); - case use_lr1110: - return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); - case use_lr1120: - return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); - case use_lr1121: - return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); - case use_llcc68: - return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); - case use_simradio: - return (RadioInterface *)new SimRadio; - default: - assert(0); // shouldn't happen - return (RadioInterface *)nullptr; - } - }; + // as one can't use a function pointer to the class constructor: + auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) { + switch (portduino_config.lora_module) { + case use_rf95: + return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); + case use_sx1262: + return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); + case use_sx1268: + return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); + case use_sx1280: + return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); + case use_lr1110: + return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); + case use_lr1120: + return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); + case use_lr1121: + return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); + case use_llcc68: + return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + case use_simradio: + return (RadioInterface *)new SimRadio; + default: + assert(0); // shouldn't happen + return (RadioInterface *)nullptr; + } + }; - LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), - portduino_config.lora_spi_dev.c_str()); - if (portduino_config.lora_spi_dev == "ch341") { - RadioLibHAL = ch341Hal; - } else { - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); - } - rIf = loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, + LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), + portduino_config.lora_spi_dev.c_str()); + if (portduino_config.lora_spi_dev == "ch341") { + RadioLibHAL = ch341Hal; + } else { + RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + } + rIf = + loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); - if (!rIf->init()) { - LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); - delete rIf; - rIf = NULL; - exit(EXIT_FAILURE); - } else { - LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); - } + if (!rIf->init()) { + LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); + } #elif defined(HW_SPI1_DEVICE) - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); #else // HW_SPI1_DEVICE - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); #endif - // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) + // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) #if defined(USE_STM32WLx) - if (!rIf) { - rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No STM32WL radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("STM32WL init success"); - radioType = STM32WLx_RADIO; + if (!rIf) { + rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No STM32WL radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("STM32WL init success"); + radioType = STM32WLx_RADIO; + } } - } #endif #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); - if (!rIf->init()) { - LOG_WARN("No RF95 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("RF95 init success"); - radioType = RF95_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); + if (!rIf->init()) { + LOG_WARN("No RF95 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("RF95 init success"); + radioType = RF95_RADIO; + } } - } #endif #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); #ifdef SX126X_DIO3_TCXO_VOLTAGE - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); #endif - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio"); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success"); - rIf = sxIf; - radioType = SX1262_RADIO; + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio"); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success"); + rIf = sxIf; + radioType = SX1262_RADIO; + } } - } #endif #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1262_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1262_RADIO; + } } - } - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); - radioType = SX1262_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); + radioType = SX1262_RADIO; + } } - } #endif #if defined(USE_SX1268) #if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1268_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1268_RADIO; + } } - } #endif - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1268 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success"); - radioType = SX1268_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1268 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success"); + radioType = SX1268_RADIO; + } } - } #endif #if defined(USE_LLCC68) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No LLCC68 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LLCC68 init success"); - radioType = LLCC68_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No LLCC68 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LLCC68 init success"); + radioType = LLCC68_RADIO; + } } - } #endif #if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1110 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1110 init success"); - radioType = LR1110_RADIO; + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1110 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1110 init success"); + radioType = LR1110_RADIO; + } } - } #endif #if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1120 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1120 init success"); - radioType = LR1120_RADIO; + if (!rIf) { + rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1120 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1120 init success"); + radioType = LR1120_RADIO; + } } - } #endif #if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1121 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1121 init success"); - radioType = LR1121_RADIO; + if (!rIf) { + rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1121 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1121 init success"); + radioType = LR1121_RADIO; + } } - } #endif #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 - if (!rIf) { - rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1280 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1280 init success"); - radioType = SX1280_RADIO; + if (!rIf) { + rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1280 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1280 init success"); + radioType = SX1280_RADIO; + } } - } #endif - // check if the radio chip matches the selected region - if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { - LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; - nodeDB->saveToDisk(SEGMENT_CONFIG); + // check if the radio chip matches the selected region + if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { + LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + nodeDB->saveToDisk(SEGMENT_CONFIG); - if (!rIf->reconfigure()) { - LOG_WARN("Reconfigure failed, rebooting"); - if (screen) { - screen->showSimpleBanner("Rebooting..."); - } - rebootAtMsec = millis() + 5000; + if (!rIf->reconfigure()) { + LOG_WARN("Reconfigure failed, rebooting"); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } + rebootAtMsec = millis() + 5000; + } } - } - lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) + lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) #if !MESHTASTIC_EXCLUDE_MQTT - mqttInit(); + mqttInit(); #endif #ifdef RF95_FAN_EN - // Ability to disable FAN if PIN has been set with RF95_FAN_EN. - // Make sure LoRa has been started before disabling FAN. - if (config.lora.pa_fan_disabled) - digitalWrite(RF95_FAN_EN, LOW ^ 0); + // Ability to disable FAN if PIN has been set with RF95_FAN_EN. + // Make sure LoRa has been started before disabling FAN. + if (config.lora.pa_fan_disabled) + digitalWrite(RF95_FAN_EN, LOW ^ 0); #endif #ifndef ARCH_PORTDUINO - // Initialize Wifi + // Initialize Wifi #if HAS_WIFI - initWifi(); + initWifi(); #endif #if HAS_ETHERNET - // Initialize Ethernet - initEthernet(); + // Initialize Ethernet + initEthernet(); #endif #endif #if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) #ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; + osk_found = true; #endif #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - // Start web server thread. - webServerThread = new WebServerThread(); + // Start web server thread. + webServerThread = new WebServerThread(); #endif #ifdef ARCH_PORTDUINO #if __has_include() - if (portduino_config.webserverport != -1) { - piwebServerThread = new PiWebServerThread(); - std::atexit([] { delete piwebServerThread; }); - } + if (portduino_config.webserverport != -1) { + piwebServerThread = new PiWebServerThread(); + std::atexit([] { delete piwebServerThread; }); + } #endif - initApiServer(TCPPort); + initApiServer(TCPPort); #endif - // Start airtime logger thread. - airTime = new AirTime(); + // Start airtime logger thread. + airTime = new AirTime(); - if (!rIf) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); - else { - router->addInterface(rIf); + if (!rIf) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); + else { + router->addInterface(rIf); - // Log bit rate to debug output - LOG_DEBUG("LoRA bitrate = %f bytes / sec", - (float(meshtastic_Constants_DATA_PAYLOAD_LEN) / (float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) * 1000); - } + // Log bit rate to debug output + LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) / + (float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) * + 1000); + } - // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values - PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking - // from SDS - powerFSMthread = new PowerFSMThread(); + // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values + PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS + powerFSMthread = new PowerFSMThread(); #if !HAS_TFT - setCPUFast(false); // 80MHz is fine for our slow peripherals + setCPUFast(false); // 80MHz is fine for our slow peripherals #endif #ifdef ARDUINO_ARCH_ESP32 - LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); - LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); + LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); + LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); #endif - // We manually run this to update the NodeStatus - nodeDB->notifyObservers(true); + // We manually run this to update the NodeStatus + nodeDB->notifyObservers(true); } #endif @@ -1508,118 +1519,122 @@ uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to s // This will suppress the current delay and instead try to run ASAP. bool runASAP; -extern meshtastic_DeviceMetadata getDeviceMetadata() { - meshtastic_DeviceMetadata deviceMetadata; - strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version)); - deviceMetadata.device_state_version = DEVICESTATE_CUR_VER; - deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN; - deviceMetadata.hasBluetooth = HAS_BLUETOOTH; - deviceMetadata.hasWifi = HAS_WIFI; - deviceMetadata.hasEthernet = HAS_ETHERNET; - deviceMetadata.role = config.device.role; - deviceMetadata.position_flags = config.position.position_flags; - deviceMetadata.hw_model = HW_VENDOR; - deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; - deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; +extern meshtastic_DeviceMetadata getDeviceMetadata() +{ + meshtastic_DeviceMetadata deviceMetadata; + strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version)); + deviceMetadata.device_state_version = DEVICESTATE_CUR_VER; + deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN; + deviceMetadata.hasBluetooth = HAS_BLUETOOTH; + deviceMetadata.hasWifi = HAS_WIFI; + deviceMetadata.hasEthernet = HAS_ETHERNET; + deviceMetadata.role = config.device.role; + deviceMetadata.position_flags = config.position.position_flags; + deviceMetadata.hw_model = HW_VENDOR; + deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; + deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; #if MESHTASTIC_EXCLUDE_REMOTEHARDWARE - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; #endif #if MESHTASTIC_EXCLUDE_AUDIO - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; #endif // Option to explicitly include canned messages for edge cases, e.g. niche graphics #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif #if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts // We'll have to macro guard against those targets potentially #if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG; #endif // If we don't have any GPIO and we don't have GPS OR we don't want too - no purpose in having serial config #if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; #endif #ifndef ARCH_ESP32 - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; #endif #if !defined(HAS_RGB_LED) && !RAK_4631 - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; #endif // No bluetooth on these targets (yet): // Pico W / 2W may get it at some point // Portduino and ESP32-C6 are excluded because we don't have a working bluetooth stacks integrated yet. #if defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) || defined(CONFIG_IDF_TARGET_ESP32C6) - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG; + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG; #endif #if defined(ARCH_NRF52) && !HAS_ETHERNET // nrf52 doesn't have network unless it's a RAK ethernet gateway currently - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on nRF52 + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on nRF52 #elif defined(ARCH_RP2040) && !HAS_WIFI && !HAS_ETHERNET - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on RP2040 + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on RP2040 #endif #if !(MESHTASTIC_EXCLUDE_PKI) - deviceMetadata.hasPKC = true; + deviceMetadata.hasPKC = true; #endif - return deviceMetadata; + return deviceMetadata; } #if !MESHTASTIC_EXCLUDE_I2C void scannerToSensorsMap(const std::unique_ptr &i2cScanner, ScanI2C::DeviceType deviceType, - meshtastic_TelemetrySensorType sensorType) { - auto found = i2cScanner->find(deviceType); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[sensorType].first = found.address.address; - nodeTelemetrySensorsMap[sensorType].second = i2cScanner->fetchI2CBus(found.address); - } + meshtastic_TelemetrySensorType sensorType) +{ + auto found = i2cScanner->find(deviceType); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[sensorType].first = found.address.address; + nodeTelemetrySensorsMap[sensorType].second = i2cScanner->fetchI2CBus(found.address); + } } #endif #ifndef PIO_UNIT_TESTING -void loop() { - runASAP = false; +void loop() +{ + runASAP = false; #ifdef ARCH_ESP32 - esp32Loop(); + esp32Loop(); #endif #ifdef ARCH_NRF52 - nrf52Loop(); + nrf52Loop(); #endif - power->powerCommandsCheck(); + power->powerCommandsCheck(); #ifdef DEBUG_STACK - static uint32_t lastPrint = 0; - if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { - lastPrint = millis(); - meshtastic::printThreadInfo("main"); - } + static uint32_t lastPrint = 0; + if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { + lastPrint = millis(); + meshtastic::printThreadInfo("main"); + } #endif - service->loop(); + service->loop(); #if !MESHTASTIC_EXCLUDE_INPUTBROKER && defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) - if (inputBroker) - inputBroker->processInputEventQueue(); + if (inputBroker) + inputBroker->processInputEventQueue(); #endif #if ARCH_PORTDUINO && HAS_TFT - if (screen && portduino_config.displayPanel == x11 && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - auto dispdev = screen->getDisplayDevice(); - if (dispdev) - static_cast(dispdev)->sdlLoop(); - } + if (screen && portduino_config.displayPanel == x11 && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + auto dispdev = screen->getDisplayDevice(); + if (dispdev) + static_cast(dispdev)->sdlLoop(); + } #endif - long delayMsec = mainController.runOrDelay(); + long delayMsec = mainController.runOrDelay(); - // We want to sleep as long as possible here - because it saves power - if (!runASAP && loopCanSleep()) { + // We want to sleep as long as possible here - because it saves power + if (!runASAP && loopCanSleep()) { #ifdef DEBUG_LOOP_TIMING - LOG_DEBUG("main loop delay: %d", delayMsec); + LOG_DEBUG("main loop delay: %d", delayMsec); #endif - mainDelay.delay(delayMsec); - } + mainDelay.delay(delayMsec); + } } #endif diff --git a/src/memGet.cpp b/src/memGet.cpp index 72f40d470..14e614014 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -20,19 +20,20 @@ MemGet memGet; * Returns the amount of free heap memory in bytes. * @return uint32_t The amount of free heap memory in bytes. */ -uint32_t MemGet::getFreeHeap() { +uint32_t MemGet::getFreeHeap() +{ #ifdef ARCH_ESP32 - return ESP.getFreeHeap(); + return ESP.getFreeHeap(); #elif defined(ARCH_NRF52) - return dbgHeapFree(); + return dbgHeapFree(); #elif defined(ARCH_RP2040) - return rp2040.getFreeHeap(); + return rp2040.getFreeHeap(); #elif defined(ARCH_STM32WL) - struct mallinfo m = mallinfo(); - return m.fordblks; // Total free space (bytes) + struct mallinfo m = mallinfo(); + return m.fordblks; // Total free space (bytes) #else - // this platform does not have heap management function implemented - return UINT32_MAX; + // this platform does not have heap management function implemented + return UINT32_MAX; #endif } @@ -40,19 +41,20 @@ uint32_t MemGet::getFreeHeap() { * Returns the size of the heap memory in bytes. * @return uint32_t The size of the heap memory in bytes. */ -uint32_t MemGet::getHeapSize() { +uint32_t MemGet::getHeapSize() +{ #ifdef ARCH_ESP32 - return ESP.getHeapSize(); + return ESP.getHeapSize(); #elif defined(ARCH_NRF52) - return dbgHeapTotal(); + return dbgHeapTotal(); #elif defined(ARCH_RP2040) - return rp2040.getTotalHeap(); + return rp2040.getTotalHeap(); #elif defined(ARCH_STM32WL) - struct mallinfo m = mallinfo(); - return m.arena; // Non-mmapped space allocated (bytes) + struct mallinfo m = mallinfo(); + return m.arena; // Non-mmapped space allocated (bytes) #else - // this platform does not have heap management function implemented - return UINT32_MAX; + // this platform does not have heap management function implemented + return UINT32_MAX; #endif } @@ -61,13 +63,14 @@ uint32_t MemGet::getHeapSize() { * * @return The amount of free psram memory in bytes. */ -uint32_t MemGet::getFreePsram() { +uint32_t MemGet::getFreePsram() +{ #ifdef ARCH_ESP32 - return ESP.getFreePsram(); + return ESP.getFreePsram(); #elif defined(ARCH_PORTDUINO) - return 4194252; + return 4194252; #else - return 0; + return 0; #endif } @@ -76,23 +79,25 @@ uint32_t MemGet::getFreePsram() { * * @return uint32_t The size of the PSRAM memory. */ -uint32_t MemGet::getPsramSize() { +uint32_t MemGet::getPsramSize() +{ #ifdef ARCH_ESP32 - return ESP.getPsramSize(); + return ESP.getPsramSize(); #elif defined(ARCH_PORTDUINO) - return 4194252; + return 4194252; #else - return 0; + return 0; #endif } -void displayPercentHeapFree() { - uint32_t freeHeap = memGet.getFreeHeap(); - uint32_t totalHeap = memGet.getHeapSize(); - if (totalHeap == 0 || totalHeap == UINT32_MAX) { - LOG_INFO("Heap size unavailable"); - return; - } - int percent = (int)((freeHeap * 100) / totalHeap); - LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); +void displayPercentHeapFree() +{ + uint32_t freeHeap = memGet.getFreeHeap(); + uint32_t totalHeap = memGet.getHeapSize(); + if (totalHeap == 0 || totalHeap == UINT32_MAX) { + LOG_INFO("Heap size unavailable"); + return; + } + int percent = (int)((freeHeap * 100) / totalHeap); + LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); } \ No newline at end of file diff --git a/src/memGet.h b/src/memGet.h index 608f52c5a..130d2ca7e 100644 --- a/src/memGet.h +++ b/src/memGet.h @@ -4,12 +4,13 @@ #include -class MemGet { -public: - uint32_t getFreeHeap(); - uint32_t getHeapSize(); - uint32_t getFreePsram(); - uint32_t getPsramSize(); +class MemGet +{ + public: + uint32_t getFreeHeap(); + uint32_t getHeapSize(); + uint32_t getFreePsram(); + uint32_t getPsramSize(); }; extern MemGet memGet; diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 0834279ca..4dcd94e3b 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -22,369 +22,386 @@ const char *Channels::serialChannel = "serial"; const char *Channels::mqttChannel = "mqtt"; #endif -uint8_t xorHash(const uint8_t *p, size_t len) { - uint8_t code = 0; - for (size_t i = 0; i < len; i++) - code ^= p[i]; - return code; +uint8_t xorHash(const uint8_t *p, size_t len) +{ + uint8_t code = 0; + for (size_t i = 0; i < len; i++) + code ^= p[i]; + return code; } /** Given a channel number, return the (0 to 255) hash for that channel. * The hash is just an xor of the channel name followed by the channel PSK being used for encryption * If no suitable channel could be found, return -1 */ -int16_t Channels::generateHash(ChannelIndex channelNum) { - auto k = getKey(channelNum); - if (k.length < 0) - return -1; // invalid - else { - const char *name = getName(channelNum); - uint8_t h = xorHash((const uint8_t *)name, strlen(name)); +int16_t Channels::generateHash(ChannelIndex channelNum) +{ + auto k = getKey(channelNum); + if (k.length < 0) + return -1; // invalid + else { + const char *name = getName(channelNum); + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); - h ^= xorHash(k.bytes, k.length); + h ^= xorHash(k.bytes, k.length); - return h; - } + return h; + } } /** * Validate a channel, fixing any errors as needed */ -meshtastic_Channel &Channels::fixupChannel(ChannelIndex chIndex) { - meshtastic_Channel &ch = getByIndex(chIndex); +meshtastic_Channel &Channels::fixupChannel(ChannelIndex chIndex) +{ + meshtastic_Channel &ch = getByIndex(chIndex); - ch.index = chIndex; // Preinit the index so it be ready to share with the phone (we'll never change it later) + ch.index = chIndex; // Preinit the index so it be ready to share with the phone (we'll never change it later) - if (!ch.has_settings) { - // No settings! Must disable and skip - ch.role = meshtastic_Channel_Role_DISABLED; - memset(&ch.settings, 0, sizeof(ch.settings)); - ch.has_settings = true; - } else { - meshtastic_ChannelSettings &meshtastic_channelSettings = ch.settings; + if (!ch.has_settings) { + // No settings! Must disable and skip + ch.role = meshtastic_Channel_Role_DISABLED; + memset(&ch.settings, 0, sizeof(ch.settings)); + ch.has_settings = true; + } else { + meshtastic_ChannelSettings &meshtastic_channelSettings = ch.settings; - // Convert the old string "Default" to our new short representation - if (strcmp(meshtastic_channelSettings.name, "Default") == 0) - *meshtastic_channelSettings.name = '\0'; - } + // Convert the old string "Default" to our new short representation + if (strcmp(meshtastic_channelSettings.name, "Default") == 0) + *meshtastic_channelSettings.name = '\0'; + } - hashes[chIndex] = generateHash(chIndex); + hashes[chIndex] = generateHash(chIndex); - return ch; + return ch; } -void Channels::initDefaultLoraConfig() { - meshtastic_Config_LoRaConfig &loraConfig = config.lora; +void Channels::initDefaultLoraConfig() +{ + meshtastic_Config_LoRaConfig &loraConfig = config.lora; - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; // Default to Long Range & Fast - loraConfig.use_preset = true; - loraConfig.tx_power = 0; // default - loraConfig.channel_num = 0; + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; // Default to Long Range & Fast + loraConfig.use_preset = true; + loraConfig.tx_power = 0; // default + loraConfig.channel_num = 0; #ifdef USERPREFS_LORACONFIG_MODEM_PRESET - loraConfig.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; + loraConfig.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; #endif #ifdef USERPREFS_LORACONFIG_CHANNEL_NUM - loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM; + loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM; #endif } -bool Channels::ensureLicensedOperation() { - if (!owner.is_licensed) { - return false; - } - bool hasEncryptionOrAdmin = false; - for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { - auto channel = channels.getByIndex(i); - if (!channel.has_settings) { - continue; +bool Channels::ensureLicensedOperation() +{ + if (!owner.is_licensed) { + return false; } - auto &channelSettings = channel.settings; - if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) { - channel.role = meshtastic_Channel_Role_DISABLED; - channelSettings.psk.bytes[0] = 0; - channelSettings.psk.size = 0; - hasEncryptionOrAdmin = true; - channels.setChannel(channel); + bool hasEncryptionOrAdmin = false; + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + auto channel = channels.getByIndex(i); + if (!channel.has_settings) { + continue; + } + auto &channelSettings = channel.settings; + if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) { + channel.role = meshtastic_Channel_Role_DISABLED; + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + hasEncryptionOrAdmin = true; + channels.setChannel(channel); - } else if (channelSettings.psk.size > 0) { - channelSettings.psk.bytes[0] = 0; - channelSettings.psk.size = 0; - hasEncryptionOrAdmin = true; - channels.setChannel(channel); + } else if (channelSettings.psk.size > 0) { + channelSettings.psk.bytes[0] = 0; + channelSettings.psk.size = 0; + hasEncryptionOrAdmin = true; + channels.setChannel(channel); + } } - } - return hasEncryptionOrAdmin; + return hasEncryptionOrAdmin; } /** * Write a default channel to the specified channel index */ -void Channels::initDefaultChannel(ChannelIndex chIndex) { - meshtastic_Channel &ch = getByIndex(chIndex); - meshtastic_ChannelSettings &channelSettings = ch.settings; +void Channels::initDefaultChannel(ChannelIndex chIndex) +{ + meshtastic_Channel &ch = getByIndex(chIndex); + meshtastic_ChannelSettings &channelSettings = ch.settings; - uint8_t defaultpskIndex = 1; - channelSettings.psk.bytes[0] = defaultpskIndex; - channelSettings.psk.size = 1; - strncpy(channelSettings.name, "", sizeof(channelSettings.name)); - channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel - channelSettings.has_module_settings = true; + uint8_t defaultpskIndex = 1; + channelSettings.psk.bytes[0] = defaultpskIndex; + channelSettings.psk.size = 1; + strncpy(channelSettings.name, "", sizeof(channelSettings.name)); + channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel + channelSettings.has_module_settings = true; - ch.has_settings = true; - ch.role = chIndex == 0 ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; + ch.has_settings = true; + ch.role = chIndex == 0 ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; - switch (chIndex) { - case 0: + switch (chIndex) { + case 0: #ifdef USERPREFS_CHANNEL_0_PSK - static const uint8_t defaultpsk0[] = USERPREFS_CHANNEL_0_PSK; - memcpy(channelSettings.psk.bytes, defaultpsk0, sizeof(defaultpsk0)); - channelSettings.psk.size = sizeof(defaultpsk0); + static const uint8_t defaultpsk0[] = USERPREFS_CHANNEL_0_PSK; + memcpy(channelSettings.psk.bytes, defaultpsk0, sizeof(defaultpsk0)); + channelSettings.psk.size = sizeof(defaultpsk0); #endif #ifdef USERPREFS_CHANNEL_0_NAME - strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_0_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_0_NAME); #endif #ifdef USERPREFS_CHANNEL_0_PRECISION - channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_0_PRECISION; + channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_0_PRECISION; #endif #ifdef USERPREFS_CHANNEL_0_UPLINK_ENABLED - channelSettings.uplink_enabled = USERPREFS_CHANNEL_0_UPLINK_ENABLED; + channelSettings.uplink_enabled = USERPREFS_CHANNEL_0_UPLINK_ENABLED; #endif #ifdef USERPREFS_CHANNEL_0_DOWNLINK_ENABLED - channelSettings.downlink_enabled = USERPREFS_CHANNEL_0_DOWNLINK_ENABLED; + channelSettings.downlink_enabled = USERPREFS_CHANNEL_0_DOWNLINK_ENABLED; #endif - break; - case 1: + break; + case 1: #ifdef USERPREFS_CHANNEL_1_PSK - static const uint8_t defaultpsk1[] = USERPREFS_CHANNEL_1_PSK; - memcpy(channelSettings.psk.bytes, defaultpsk1, sizeof(defaultpsk1)); - channelSettings.psk.size = sizeof(defaultpsk1); + static const uint8_t defaultpsk1[] = USERPREFS_CHANNEL_1_PSK; + memcpy(channelSettings.psk.bytes, defaultpsk1, sizeof(defaultpsk1)); + channelSettings.psk.size = sizeof(defaultpsk1); #endif #ifdef USERPREFS_CHANNEL_1_NAME - strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_1_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_1_NAME); #endif #ifdef USERPREFS_CHANNEL_1_PRECISION - channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_1_PRECISION; + channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_1_PRECISION; #endif #ifdef USERPREFS_CHANNEL_1_UPLINK_ENABLED - channelSettings.uplink_enabled = USERPREFS_CHANNEL_1_UPLINK_ENABLED; + channelSettings.uplink_enabled = USERPREFS_CHANNEL_1_UPLINK_ENABLED; #endif #ifdef USERPREFS_CHANNEL_1_DOWNLINK_ENABLED - channelSettings.downlink_enabled = USERPREFS_CHANNEL_1_DOWNLINK_ENABLED; + channelSettings.downlink_enabled = USERPREFS_CHANNEL_1_DOWNLINK_ENABLED; #endif - break; - case 2: + break; + case 2: #ifdef USERPREFS_CHANNEL_2_PSK - static const uint8_t defaultpsk2[] = USERPREFS_CHANNEL_2_PSK; - memcpy(channelSettings.psk.bytes, defaultpsk2, sizeof(defaultpsk2)); - channelSettings.psk.size = sizeof(defaultpsk2); + static const uint8_t defaultpsk2[] = USERPREFS_CHANNEL_2_PSK; + memcpy(channelSettings.psk.bytes, defaultpsk2, sizeof(defaultpsk2)); + channelSettings.psk.size = sizeof(defaultpsk2); #endif #ifdef USERPREFS_CHANNEL_2_NAME - strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_2_NAME); + strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_2_NAME); #endif #ifdef USERPREFS_CHANNEL_2_PRECISION - channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_2_PRECISION; + channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_2_PRECISION; #endif #ifdef USERPREFS_CHANNEL_2_UPLINK_ENABLED - channelSettings.uplink_enabled = USERPREFS_CHANNEL_2_UPLINK_ENABLED; + channelSettings.uplink_enabled = USERPREFS_CHANNEL_2_UPLINK_ENABLED; #endif #ifdef USERPREFS_CHANNEL_2_DOWNLINK_ENABLED - channelSettings.downlink_enabled = USERPREFS_CHANNEL_2_DOWNLINK_ENABLED; + channelSettings.downlink_enabled = USERPREFS_CHANNEL_2_DOWNLINK_ENABLED; #endif - break; - default: - break; - } + break; + default: + break; + } } -CryptoKey Channels::getKey(ChannelIndex chIndex) { - meshtastic_Channel &ch = getByIndex(chIndex); - const meshtastic_ChannelSettings &channelSettings = ch.settings; +CryptoKey Channels::getKey(ChannelIndex chIndex) +{ + meshtastic_Channel &ch = getByIndex(chIndex); + const meshtastic_ChannelSettings &channelSettings = ch.settings; - CryptoKey k; - memset(k.bytes, 0, sizeof(k.bytes)); // In case the user provided a short key, we want to pad the rest with zeros + CryptoKey k; + memset(k.bytes, 0, sizeof(k.bytes)); // In case the user provided a short key, we want to pad the rest with zeros - if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED) { - k.length = -1; // invalid - } else { - memcpy(k.bytes, channelSettings.psk.bytes, channelSettings.psk.size); - k.length = channelSettings.psk.size; - if (k.length == 0) { - if (ch.role == meshtastic_Channel_Role_SECONDARY) { - LOG_DEBUG("Unset PSK for secondary channel %s. use primary key", ch.settings.name); - k = getKey(primaryIndex); - } else { - LOG_WARN("User disabled encryption"); - } - } else if (k.length == 1) { - // Convert the short single byte variants of psk into variant that can be used more generally + if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED) { + k.length = -1; // invalid + } else { + memcpy(k.bytes, channelSettings.psk.bytes, channelSettings.psk.size); + k.length = channelSettings.psk.size; + if (k.length == 0) { + if (ch.role == meshtastic_Channel_Role_SECONDARY) { + LOG_DEBUG("Unset PSK for secondary channel %s. use primary key", ch.settings.name); + k = getKey(primaryIndex); + } else { + LOG_WARN("User disabled encryption"); + } + } else if (k.length == 1) { + // Convert the short single byte variants of psk into variant that can be used more generally - uint8_t pskIndex = k.bytes[0]; - LOG_DEBUG("Expand short PSK #%d", pskIndex); - if (pskIndex == 0) - k.length = 0; // Turn off encryption - else { - memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); - k.length = sizeof(defaultpsk); - // Bump up the last byte of PSK as needed - uint8_t *last = k.bytes + sizeof(defaultpsk) - 1; - *last = *last + pskIndex - 1; // index of 1 means no change vs defaultPSK - } - } else if (k.length < 16) { - // Error! The user specified only the first few bits of an AES128 key. So by convention we just pad the rest of - // the key with zeros - LOG_WARN("User provided a too short AES128 key - padding"); - k.length = 16; - } else if (k.length < 32 && k.length != 16) { - // Error! The user specified only the first few bits of an AES256 key. So by convention we just pad the rest of - // the key with zeros - LOG_WARN("User provided a too short AES256 key - padding"); - k.length = 32; + uint8_t pskIndex = k.bytes[0]; + LOG_DEBUG("Expand short PSK #%d", pskIndex); + if (pskIndex == 0) + k.length = 0; // Turn off encryption + else { + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + // Bump up the last byte of PSK as needed + uint8_t *last = k.bytes + sizeof(defaultpsk) - 1; + *last = *last + pskIndex - 1; // index of 1 means no change vs defaultPSK + } + } else if (k.length < 16) { + // Error! The user specified only the first few bits of an AES128 key. So by convention we just pad the rest of the + // key with zeros + LOG_WARN("User provided a too short AES128 key - padding"); + k.length = 16; + } else if (k.length < 32 && k.length != 16) { + // Error! The user specified only the first few bits of an AES256 key. So by convention we just pad the rest of the + // key with zeros + LOG_WARN("User provided a too short AES256 key - padding"); + k.length = 32; + } } - } - return k; + return k; } /** Given a channel index, change to use the crypto key specified by that index */ -int16_t Channels::setCrypto(ChannelIndex chIndex) { - CryptoKey k = getKey(chIndex); +int16_t Channels::setCrypto(ChannelIndex chIndex) +{ + CryptoKey k = getKey(chIndex); - if (k.length < 0) - return -1; - else { - // Tell our crypto engine about the psk - crypto->setKey(k); - return getHash(chIndex); - } + if (k.length < 0) + return -1; + else { + // Tell our crypto engine about the psk + crypto->setKey(k); + return getHash(chIndex); + } } -void Channels::initDefaults() { - channelFile.channels_count = MAX_NUM_CHANNELS; - for (int i = 0; i < channelFile.channels_count; i++) - fixupChannel(i); - initDefaultLoraConfig(); +void Channels::initDefaults() +{ + channelFile.channels_count = MAX_NUM_CHANNELS; + for (int i = 0; i < channelFile.channels_count; i++) + fixupChannel(i); + initDefaultLoraConfig(); #ifdef USERPREFS_CHANNELS_TO_WRITE - for (int i = 0; i < USERPREFS_CHANNELS_TO_WRITE; i++) { - initDefaultChannel(i); - } + for (int i = 0; i < USERPREFS_CHANNELS_TO_WRITE; i++) { + initDefaultChannel(i); + } #else - initDefaultChannel(0); + initDefaultChannel(0); #endif } -void Channels::onConfigChanged() { - // Make sure the phone hasn't mucked anything up - for (int i = 0; i < channelFile.channels_count; i++) { - const meshtastic_Channel &ch = fixupChannel(i); +void Channels::onConfigChanged() +{ + // Make sure the phone hasn't mucked anything up + for (int i = 0; i < channelFile.channels_count; i++) { + const meshtastic_Channel &ch = fixupChannel(i); - if (ch.role == meshtastic_Channel_Role_PRIMARY) - primaryIndex = i; - } + if (ch.role == meshtastic_Channel_Role_PRIMARY) + primaryIndex = i; + } #if !MESHTASTIC_EXCLUDE_MQTT - if (channels.anyMqttEnabled() && mqtt && !mqtt->isEnabled()) { - LOG_DEBUG("MQTT is enabled on at least one channel, so set MQTT thread to run immediately"); - mqtt->start(); - } -#endif -} - -meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex) { - // remove this assert cause malformed packets can make our firmware reboot here. - if (chIndex < channelFile.channels_count) { // This should be equal to MAX_NUM_CHANNELS - meshtastic_Channel *ch = channelFile.channels + chIndex; - return *ch; - } else { - LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count); - - static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel)); - memset(ch, 0, sizeof(meshtastic_Channel)); - // ch.index -1 means we don't know the channel locally and need to look it up by settings.name - // not sure this is handled right everywhere - ch->index = -1; - return *ch; - } -} - -meshtastic_Channel &Channels::getByName(const char *chName) { - for (ChannelIndex i = 0; i < getNumChannels(); i++) { - if (strcasecmp(getGlobalId(i), chName) == 0) { - return channelFile.channels[i]; + if (channels.anyMqttEnabled() && mqtt && !mqtt->isEnabled()) { + LOG_DEBUG("MQTT is enabled on at least one channel, so set MQTT thread to run immediately"); + mqtt->start(); } - } - - return getByIndex(getPrimaryIndex()); -} - -void Channels::setChannel(const meshtastic_Channel &c) { - meshtastic_Channel &old = getByIndex(c.index); - - // if this is the new primary, demote any existing roles - if (c.role == meshtastic_Channel_Role_PRIMARY) - for (int i = 0; i < getNumChannels(); i++) - if (channelFile.channels[i].role == meshtastic_Channel_Role_PRIMARY) - channelFile.channels[i].role = meshtastic_Channel_Role_SECONDARY; - - old = c; // slam in the new settings/role -} - -bool Channels::anyMqttEnabled() { -#if USERPREFS_EVENT_MODE && !MESHTASTIC_EXCLUDE_MQTT - // Don't publish messages on the public MQTT broker if we are in event mode - if (mqtt && mqtt->isUsingDefaultServer() && mqtt->isUsingDefaultRootTopic()) { - return false; - } #endif - for (int i = 0; i < getNumChannels(); i++) - if (channelFile.channels[i].role != meshtastic_Channel_Role_DISABLED && channelFile.channels[i].has_settings && - (channelFile.channels[i].settings.downlink_enabled || channelFile.channels[i].settings.uplink_enabled)) - return true; - - return false; } -const char *Channels::getName(size_t chIndex) { - // Convert the short "" representation for Default into a usable string - const meshtastic_ChannelSettings &channelSettings = getByIndex(chIndex).settings; - const char *channelName = channelSettings.name; - if (!*channelName) { // emptystring - // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case - // the app effed up and forgot to set channelSettings.name - if (config.lora.use_preset) { - channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); +meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex) +{ + // remove this assert cause malformed packets can make our firmware reboot here. + if (chIndex < channelFile.channels_count) { // This should be equal to MAX_NUM_CHANNELS + meshtastic_Channel *ch = channelFile.channels + chIndex; + return *ch; } else { - channelName = "Custom"; + LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count); + + static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel)); + memset(ch, 0, sizeof(meshtastic_Channel)); + // ch.index -1 means we don't know the channel locally and need to look it up by settings.name + // not sure this is handled right everywhere + ch->index = -1; + return *ch; } - } - - return channelName; } -bool Channels::isDefaultChannel(ChannelIndex chIndex) { - const auto &ch = getByIndex(chIndex); - if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { - const char *name = getName(chIndex); - const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); - // Check if the name is the default derived from the modem preset - if (strcmp(name, presetName) == 0) - return true; - } - return false; +meshtastic_Channel &Channels::getByName(const char *chName) +{ + for (ChannelIndex i = 0; i < getNumChannels(); i++) { + if (strcasecmp(getGlobalId(i), chName) == 0) { + return channelFile.channels[i]; + } + } + + return getByIndex(getPrimaryIndex()); } -bool Channels::hasDefaultChannel() { - // If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default - // channel - if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency) +void Channels::setChannel(const meshtastic_Channel &c) +{ + meshtastic_Channel &old = getByIndex(c.index); + + // if this is the new primary, demote any existing roles + if (c.role == meshtastic_Channel_Role_PRIMARY) + for (int i = 0; i < getNumChannels(); i++) + if (channelFile.channels[i].role == meshtastic_Channel_Role_PRIMARY) + channelFile.channels[i].role = meshtastic_Channel_Role_SECONDARY; + + old = c; // slam in the new settings/role +} + +bool Channels::anyMqttEnabled() +{ +#if USERPREFS_EVENT_MODE && !MESHTASTIC_EXCLUDE_MQTT + // Don't publish messages on the public MQTT broker if we are in event mode + if (mqtt && mqtt->isUsingDefaultServer() && mqtt->isUsingDefaultRootTopic()) { + return false; + } +#endif + for (int i = 0; i < getNumChannels(); i++) + if (channelFile.channels[i].role != meshtastic_Channel_Role_DISABLED && channelFile.channels[i].has_settings && + (channelFile.channels[i].settings.downlink_enabled || channelFile.channels[i].settings.uplink_enabled)) + return true; + + return false; +} + +const char *Channels::getName(size_t chIndex) +{ + // Convert the short "" representation for Default into a usable string + const meshtastic_ChannelSettings &channelSettings = getByIndex(chIndex).settings; + const char *channelName = channelSettings.name; + if (!*channelName) { // emptystring + // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case + // the app effed up and forgot to set channelSettings.name + if (config.lora.use_preset) { + channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + } else { + channelName = "Custom"; + } + } + + return channelName; +} + +bool Channels::isDefaultChannel(ChannelIndex chIndex) +{ + const auto &ch = getByIndex(chIndex); + if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { + const char *name = getName(chIndex); + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + // Check if the name is the default derived from the modem preset + if (strcmp(name, presetName) == 0) + return true; + } + return false; +} + +bool Channels::hasDefaultChannel() +{ + // If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel + if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency) + return false; + // Check if any of the channels are using the default name and PSK + for (size_t i = 0; i < getNumChannels(); i++) { + if (isDefaultChannel(i)) + return true; + } return false; - // Check if any of the channels are using the default name and PSK - for (size_t i = 0; i < getNumChannels(); i++) { - if (isDefaultChannel(i)) - return true; - } - return false; } /** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured) @@ -393,40 +410,44 @@ bool Channels::hasDefaultChannel() { * * @return false if the channel hash or channel is invalid */ -bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) { - if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) { - // LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex), - // channelHash); - return false; - } else { - LOG_DEBUG("Use channel %d (hash 0x%x)", chIndex, channelHash); - setCrypto(chIndex); - return true; - } +bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) +{ + if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) { + // LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex), + // channelHash); + return false; + } else { + LOG_DEBUG("Use channel %d (hash 0x%x)", chIndex, channelHash); + setCrypto(chIndex); + return true; + } } -bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) { - // Iterate all known presets - for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { - const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, config.lora.use_preset); - if (!name) - continue; - if (strcmp(name, "Invalid") == 0) - continue; // skip invalid placeholder - uint8_t h = xorHash((const uint8_t *)name, strlen(name)); - // Expand default PSK alias 1 to actual bytes and xor into hash - uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); - if (tmp == channelHash) { - // Set crypto to defaultpsk and report success - CryptoKey k; - memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); - k.length = sizeof(defaultpsk); - crypto->setKey(k); - LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); - return true; +bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) +{ + // Iterate all known presets + for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; + ++preset) { + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, + config.lora.use_preset); + if (!name) + continue; + if (strcmp(name, "Invalid") == 0) + continue; // skip invalid placeholder + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); + // Expand default PSK alias 1 to actual bytes and xor into hash + uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); + if (tmp == channelHash) { + // Set crypto to defaultpsk and report success + CryptoKey k; + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + crypto->setKey(k); + LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); + return true; + } } - } - return false; + return false; } /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) @@ -435,4 +456,7 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) { * * @return the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 */ -int16_t Channels::setActiveByIndex(ChannelIndex channelIndex) { return setCrypto(channelIndex); } \ No newline at end of file +int16_t Channels::setActiveByIndex(ChannelIndex channelIndex) +{ + return setCrypto(channelIndex); +} \ No newline at end of file diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 518a8e909..a3cc7791c 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -15,132 +15,135 @@ typedef uint8_t ChannelIndex; typedef uint8_t ChannelHash; /** The container/on device API for working with channels */ -class Channels { - /// The index of the primary channel - ChannelIndex primaryIndex = 0; +class Channels +{ + /// The index of the primary channel + ChannelIndex primaryIndex = 0; - /** The channel index that was requested for sending/receiving. Note: if this channel is a secondary - channel and does not have a PSK, we will use the PSK from the primary channel. If this channel is disabled - no sending or receiving will be allowed */ - ChannelIndex activeChannelIndex = 0; + /** The channel index that was requested for sending/receiving. Note: if this channel is a secondary + channel and does not have a PSK, we will use the PSK from the primary channel. If this channel is disabled + no sending or receiving will be allowed */ + ChannelIndex activeChannelIndex = 0; - /// the precomputed hashes for each of our channels, or -1 for invalid - int16_t hashes[MAX_NUM_CHANNELS] = {}; + /// the precomputed hashes for each of our channels, or -1 for invalid + int16_t hashes[MAX_NUM_CHANNELS] = {}; -public: - Channels() {} + public: + Channels() {} - /// Well known channel names - static const char *adminChannel, *gpioChannel, *serialChannel, *mqttChannel; + /// Well known channel names + static const char *adminChannel, *gpioChannel, *serialChannel, *mqttChannel; - const meshtastic_ChannelSettings &getPrimary() { return getByIndex(getPrimaryIndex()).settings; } + const meshtastic_ChannelSettings &getPrimary() { return getByIndex(getPrimaryIndex()).settings; } - /** Return the Channel for a specified index */ - meshtastic_Channel &getByIndex(ChannelIndex chIndex); + /** Return the Channel for a specified index */ + meshtastic_Channel &getByIndex(ChannelIndex chIndex); - /** Return the Channel for a specified name, return primary if not found. */ - meshtastic_Channel &getByName(const char *chName); + /** Return the Channel for a specified name, return primary if not found. */ + meshtastic_Channel &getByName(const char *chName); - /** Using the index inside the channel, update the specified channel's settings and role. If this channel is being - * promoted to be primary, force all other channels to be secondary. - */ - void setChannel(const meshtastic_Channel &c); + /** Using the index inside the channel, update the specified channel's settings and role. If this channel is being promoted + * to be primary, force all other channels to be secondary. + */ + void setChannel(const meshtastic_Channel &c); - /** Return a human friendly name for this channel (and expand any short strings as needed) - */ - const char *getName(size_t chIndex); + /** Return a human friendly name for this channel (and expand any short strings as needed) + */ + const char *getName(size_t chIndex); - /** - * Return a globally unique channel ID usable with MQTT. - */ - const char *getGlobalId(size_t chIndex) { return getName(chIndex); } // FIXME, not correct + /** + * Return a globally unique channel ID usable with MQTT. + */ + const char *getGlobalId(size_t chIndex) { return getName(chIndex); } // FIXME, not correct - /** The index of the primary channel */ - ChannelIndex getPrimaryIndex() const { return primaryIndex; } + /** The index of the primary channel */ + ChannelIndex getPrimaryIndex() const { return primaryIndex; } - ChannelIndex getNumChannels() { return channelFile.channels_count; } + ChannelIndex getNumChannels() { return channelFile.channels_count; } - /// Called by NodeDB on initial boot when the radio config settings are unset. Set a default single channel config. - void initDefaults(); + /// Called by NodeDB on initial boot when the radio config settings are unset. Set a default single channel config. + void initDefaults(); - /// called when the user has just changed our radio config and we might need to change channel keys - void onConfigChanged(); + /// called when the user has just changed our radio config and we might need to change channel keys + void onConfigChanged(); - /** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured) - * - * This method is called before decoding inbound packets - * - * @return false if the channel hash or channel is invalid - */ - bool decryptForHash(ChannelIndex chIndex, ChannelHash channelHash); + /** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured) + * + * This method is called before decoding inbound packets + * + * @return false if the channel hash or channel is invalid + */ + bool decryptForHash(ChannelIndex chIndex, ChannelHash channelHash); - /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) - * - * This method is called before encoding outbound packets - * - * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 - */ - int16_t setActiveByIndex(ChannelIndex channelIndex); + /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) + * + * This method is called before encoding outbound packets + * + * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 + */ + int16_t setActiveByIndex(ChannelIndex channelIndex); - // Returns true if the channel has the default name and PSK - bool isDefaultChannel(ChannelIndex chIndex); + // Returns true if the channel has the default name and PSK + bool isDefaultChannel(ChannelIndex chIndex); - // Returns true if we can be reached via a channel with the default settings given a region and modem preset - bool hasDefaultChannel(); + // Returns true if we can be reached via a channel with the default settings given a region and modem preset + bool hasDefaultChannel(); - // Returns true if any of our channels have enabled MQTT uplink or downlink - bool anyMqttEnabled(); + // Returns true if any of our channels have enabled MQTT uplink or downlink + bool anyMqttEnabled(); - bool ensureLicensedOperation(); + bool ensureLicensedOperation(); - bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + bool setDefaultPresetCryptoForHash(ChannelHash channelHash); - int16_t getHash(ChannelIndex i) { return hashes[i]; } + int16_t getHash(ChannelIndex i) { return hashes[i]; } -private: - /** Given a channel index, change to use the crypto key specified by that index - * - * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 - */ - int16_t setCrypto(ChannelIndex chIndex); + private: + /** Given a channel index, change to use the crypto key specified by that index + * + * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 + */ + int16_t setCrypto(ChannelIndex chIndex); - /** Return the channel index for the specified channel hash, or -1 for not found */ - int8_t getIndexByHash(ChannelHash channelHash); + /** Return the channel index for the specified channel hash, or -1 for not found */ + int8_t getIndexByHash(ChannelHash channelHash); - /** Given a channel number, return the (0 to 255) hash for that channel - * If no suitable channel could be found, return -1 - * - * called by fixupChannel when a new channel is set - */ - int16_t generateHash(ChannelIndex channelNum); + /** Given a channel number, return the (0 to 255) hash for that channel + * If no suitable channel could be found, return -1 + * + * called by fixupChannel when a new channel is set + */ + int16_t generateHash(ChannelIndex channelNum); - /** - * Validate a channel, fixing any errors as needed - */ - meshtastic_Channel &fixupChannel(ChannelIndex chIndex); + /** + * Validate a channel, fixing any errors as needed + */ + meshtastic_Channel &fixupChannel(ChannelIndex chIndex); - /** - * Writes the default lora config - */ - void initDefaultLoraConfig(); + /** + * Writes the default lora config + */ + void initDefaultLoraConfig(); - /** - * Write default channels defined in UserPrefs - */ - void initDefaultChannel(ChannelIndex chIndex); + /** + * Write default channels defined in UserPrefs + */ + void initDefaultChannel(ChannelIndex chIndex); - /** - * Return the key used for encrypting this channel (if channel is secondary and no key provided, use the primary - * channel's PSK) - */ - CryptoKey getKey(ChannelIndex chIndex); + /** + * Return the key used for encrypting this channel (if channel is secondary and no key provided, use the primary channel's + * PSK) + */ + CryptoKey getKey(ChannelIndex chIndex); }; /// Singleton channel table extern Channels channels; /// 16 bytes of random PSK for our _public_ default channel that all devices power up on (AES128) -static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}; +static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, + 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}; -static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36, 0xb8, 0x61, 0x21, 0xe1, 0xfb, - 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74, 0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1}; \ No newline at end of file +static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36, + 0xb8, 0x61, 0x21, 0xe1, 0xfb, 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74, + 0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1}; \ No newline at end of file diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index e739fda39..9ca16878d 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -21,19 +21,20 @@ * @param pubKey The destination for the public key. * @param privKey The destination for the private key. */ -void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) { - // Mix in any randomness we can, to make key generation stronger. - CryptRNG.begin(optstr(APP_VERSION)); - if (myNodeInfo.device_id.size == 16) { - CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); - } - auto noise = random(); - CryptRNG.stir((uint8_t *)&noise, sizeof(noise)); +void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) +{ + // Mix in any randomness we can, to make key generation stronger. + CryptRNG.begin(optstr(APP_VERSION)); + if (myNodeInfo.device_id.size == 16) { + CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); + } + auto noise = random(); + CryptRNG.stir((uint8_t *)&noise, sizeof(noise)); - LOG_DEBUG("Generate Curve25519 keypair"); - Curve25519::dh1(public_key, private_key); - memcpy(pubKey, public_key, sizeof(public_key)); - memcpy(privKey, private_key, sizeof(private_key)); + LOG_DEBUG("Generate Curve25519 keypair"); + Curve25519::dh1(public_key, private_key); + memcpy(pubKey, public_key, sizeof(public_key)); + memcpy(privKey, private_key, sizeof(private_key)); } /** @@ -42,26 +43,28 @@ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) { * @param pubKey The destination for the public key. * @param privKey The source for the private key. */ -bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) { - if (!memfll(privKey, 0, sizeof(private_key))) { - Curve25519::eval(pubKey, privKey, 0); - if (Curve25519::isWeakPoint(pubKey)) { - LOG_ERROR("PKI key generation failed. Specified private key results in a weak"); - memset(pubKey, 0, 32); - return false; +bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) +{ + if (!memfll(privKey, 0, sizeof(private_key))) { + Curve25519::eval(pubKey, privKey, 0); + if (Curve25519::isWeakPoint(pubKey)) { + LOG_ERROR("PKI key generation failed. Specified private key results in a weak"); + memset(pubKey, 0, 32); + return false; + } + memcpy(private_key, privKey, sizeof(private_key)); + memcpy(public_key, pubKey, sizeof(public_key)); + } else { + LOG_WARN("X25519 key generation failed due to blank private key"); + return false; } - memcpy(private_key, privKey, sizeof(private_key)); - memcpy(public_key, pubKey, sizeof(public_key)); - } else { - LOG_WARN("X25519 key generation failed due to blank private key"); - return false; - } - return true; + return true; } #endif -void CryptoEngine::clearKeys() { - memset(public_key, 0, sizeof(public_key)); - memset(private_key, 0, sizeof(private_key)); +void CryptoEngine::clearKeys() +{ + memset(public_key, 0, sizeof(public_key)); + memset(private_key, 0, sizeof(private_key)); } /** @@ -76,32 +79,33 @@ void CryptoEngine::clearKeys() { * @param bytes Buffer containing plaintext input. * @param bytesOut Output buffer to be populated with encrypted ciphertext. */ -bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, - size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) { - uint8_t *auth; - long extraNonceTmp = random(); - auth = bytesOut + numBytes; - memcpy((uint8_t *)(auth + 8), &extraNonceTmp, - sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; - LOG_DEBUG("Random nonce value: %d", extraNonceTmp); - if (remotePublic.size == 0) { - LOG_DEBUG("Node %d or their public_key not found", toNode); - return false; - } - if (!setDHPublicKey(remotePublic.bytes)) { - return false; - } - hash(shared_key, 32); - initNonce(fromNode, packetNum, extraNonceTmp); +bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, + uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) +{ + uint8_t *auth; + long extraNonceTmp = random(); + auth = bytesOut + numBytes; + memcpy((uint8_t *)(auth + 8), &extraNonceTmp, + sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; + LOG_DEBUG("Random nonce value: %d", extraNonceTmp); + if (remotePublic.size == 0) { + LOG_DEBUG("Node %d or their public_key not found", toNode); + return false; + } + if (!setDHPublicKey(remotePublic.bytes)) { + return false; + } + hash(shared_key, 32); + initNonce(fromNode, packetNum, extraNonceTmp); - // Calculate the shared secret with the destination node and encrypt - printBytes("Attempt encrypt with nonce: ", nonce, 13); - printBytes("Attempt encrypt with shared_key starting with: ", shared_key, 8); - aes_ccm_ae(shared_key, 32, nonce, 8, bytes, numBytes, nullptr, 0, bytesOut, - auth); // this can write up to 15 bytes longer than numbytes past bytesOut - memcpy((uint8_t *)(auth + 8), &extraNonceTmp, - sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; - return true; + // Calculate the shared secret with the destination node and encrypt + printBytes("Attempt encrypt with nonce: ", nonce, 13); + printBytes("Attempt encrypt with shared_key starting with: ", shared_key, 8); + aes_ccm_ae(shared_key, 32, nonce, 8, bytes, numBytes, nullptr, 0, bytesOut, + auth); // this can write up to 15 bytes longer than numbytes past bytesOut + memcpy((uint8_t *)(auth + 8), &extraNonceTmp, + sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; + return true; } /** @@ -115,32 +119,36 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas * @param bytes Buffer containing ciphertext input. * @param bytesOut Output buffer to be populated with decrypted plaintext. */ -bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, - const uint8_t *bytes, uint8_t *bytesOut) { - const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? - uint32_t extraNonce; // pointer was not really used - memcpy(&extraNonce, auth + 8, - sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : (uint32_t *)(auth + 8); - LOG_INFO("Random nonce value: %d", extraNonce); +bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) +{ + const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? + uint32_t extraNonce; // pointer was not really used + memcpy(&extraNonce, auth + 8, + sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : (uint32_t *)(auth + 8); + LOG_INFO("Random nonce value: %d", extraNonce); - if (remotePublic.size == 0) { - LOG_DEBUG("Node or its public key not found in database"); - return false; - } + if (remotePublic.size == 0) { + LOG_DEBUG("Node or its public key not found in database"); + return false; + } - // Calculate the shared secret with the sending node and decrypt - if (!setDHPublicKey(remotePublic.bytes)) { - return false; - } - hash(shared_key, 32); + // Calculate the shared secret with the sending node and decrypt + if (!setDHPublicKey(remotePublic.bytes)) { + return false; + } + hash(shared_key, 32); - initNonce(fromNode, packetNum, extraNonce); - printBytes("Attempt decrypt with nonce: ", nonce, 13); - printBytes("Attempt decrypt with shared_key starting with: ", shared_key, 8); - return aes_ccm_ad(shared_key, 32, nonce, 8, bytes, numBytes - 12, nullptr, 0, auth, bytesOut); + initNonce(fromNode, packetNum, extraNonce); + printBytes("Attempt decrypt with nonce: ", nonce, 13); + printBytes("Attempt decrypt with shared_key starting with: ", shared_key, 8); + return aes_ccm_ad(shared_key, 32, nonce, 8, bytes, numBytes - 12, nullptr, 0, auth, bytesOut); } -void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) { memcpy(private_key, _private_key, 32); } +void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) +{ + memcpy(private_key, _private_key, 32); +} /** * Hash arbitrary data using SHA256. @@ -148,51 +156,58 @@ void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) { memcpy(private_key, * @param bytes * @param numBytes */ -void CryptoEngine::hash(uint8_t *bytes, size_t numBytes) { - SHA256 hash; - size_t posn; - uint8_t size = numBytes; - uint8_t inc = 16; - hash.reset(); - for (posn = 0; posn < size; posn += inc) { - size_t len = size - posn; - if (len > inc) - len = inc; - hash.update(bytes + posn, len); - } - hash.finalize(bytes, 32); +void CryptoEngine::hash(uint8_t *bytes, size_t numBytes) +{ + SHA256 hash; + size_t posn; + uint8_t size = numBytes; + uint8_t inc = 16; + hash.reset(); + for (posn = 0; posn < size; posn += inc) { + size_t len = size - posn; + if (len > inc) + len = inc; + hash.update(bytes + posn, len); + } + hash.finalize(bytes, 32); } -void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len) { - delete aes; - aes = nullptr; - if (key_len != 0) { - aes = new AESSmall256(); - aes->setKey(key_bytes, key_len); - } +void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len) +{ + delete aes; + aes = nullptr; + if (key_len != 0) { + aes = new AESSmall256(); + aes->setKey(key_bytes, key_len); + } } -void CryptoEngine::aesEncrypt(uint8_t *in, uint8_t *out) { aes->encryptBlock(out, in); } +void CryptoEngine::aesEncrypt(uint8_t *in, uint8_t *out) +{ + aes->encryptBlock(out, in); +} -bool CryptoEngine::setDHPublicKey(uint8_t *pubKey) { - uint8_t local_priv[32]; - memcpy(shared_key, pubKey, 32); - memcpy(local_priv, private_key, 32); - // Calculate the shared secret with the specified node's public key and our private key - // This includes an internal weak key check, which among other things looks for an all 0 public key and shared key. - if (!Curve25519::dh2(shared_key, local_priv)) { - LOG_WARN("Curve25519DH step 2 failed!"); - return false; - } - return true; +bool CryptoEngine::setDHPublicKey(uint8_t *pubKey) +{ + uint8_t local_priv[32]; + memcpy(shared_key, pubKey, 32); + memcpy(local_priv, private_key, 32); + // Calculate the shared secret with the specified node's public key and our private key + // This includes an internal weak key check, which among other things looks for an all 0 public key and shared key. + if (!Curve25519::dh2(shared_key, local_priv)) { + LOG_WARN("Curve25519DH step 2 failed!"); + return false; + } + return true; } #endif concurrency::Lock *cryptLock; -void CryptoEngine::setKey(const CryptoKey &k) { - LOG_DEBUG("Use AES%d key!", k.length * 8); - key = k; +void CryptoEngine::setKey(const CryptoKey &k) +{ + LOG_DEBUG("Use AES%d key!", k.length * 8); + key = k; } /** @@ -200,52 +215,56 @@ void CryptoEngine::setKey(const CryptoKey &k) { * * @param bytes is updated in place */ -void CryptoEngine::encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) { - if (key.length > 0) { - initNonce(fromNode, packetId); - if (numBytes <= MAX_BLOCKSIZE) { - encryptAESCtr(key, nonce, numBytes, bytes); - } else { - LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); +void CryptoEngine::encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) +{ + if (key.length > 0) { + initNonce(fromNode, packetId); + if (numBytes <= MAX_BLOCKSIZE) { + encryptAESCtr(key, nonce, numBytes, bytes); + } else { + LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); + } } - } } -void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) { - // For CTR, the implementation is the same - encryptPacket(fromNode, packetId, numBytes, bytes); +void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes) +{ + // For CTR, the implementation is the same + encryptPacket(fromNode, packetId, numBytes, bytes); } // Generic implementation of AES-CTR encryption. -void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) { - delete ctr; - ctr = nullptr; - if (_key.length == 16) - ctr = new CTR(); - else - ctr = new CTR(); - ctr->setKey(_key.bytes, _key.length); - static uint8_t scratch[MAX_BLOCKSIZE]; - memcpy(scratch, bytes, numBytes); - memset(scratch + numBytes, 0, - sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) +void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) +{ + delete ctr; + ctr = nullptr; + if (_key.length == 16) + ctr = new CTR(); + else + ctr = new CTR(); + ctr->setKey(_key.bytes, _key.length); + static uint8_t scratch[MAX_BLOCKSIZE]; + memcpy(scratch, bytes, numBytes); + memset(scratch + numBytes, 0, + sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) - ctr->setIV(_nonce, 16); - ctr->setCounterSize(4); - ctr->encrypt(bytes, scratch, numBytes); + ctr->setIV(_nonce, 16); + ctr->setCounterSize(4); + ctr->encrypt(bytes, scratch, numBytes); } /** * Init our 128 bit nonce for a new packet */ -void CryptoEngine::initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce) { - memset(nonce, 0, sizeof(nonce)); +void CryptoEngine::initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce) +{ + memset(nonce, 0, sizeof(nonce)); - // use memcpy to avoid breaking strict-aliasing - memcpy(nonce, &packetId, sizeof(uint64_t)); - memcpy(nonce + sizeof(uint64_t), &fromNode, sizeof(uint32_t)); - if (extraNonce) - memcpy(nonce + sizeof(uint32_t), &extraNonce, sizeof(uint32_t)); + // use memcpy to avoid breaking strict-aliasing + memcpy(nonce, &packetId, sizeof(uint64_t)); + memcpy(nonce + sizeof(uint64_t), &fromNode, sizeof(uint32_t)); + if (extraNonce) + memcpy(nonce + sizeof(uint32_t), &extraNonce, sizeof(uint32_t)); } #ifndef HAS_CUSTOM_CRYPTO_ENGINE CryptoEngine *crypto = new CryptoEngine; diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index 01ceb5068..6bbcb3b8a 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -9,10 +9,10 @@ extern concurrency::Lock *cryptLock; struct CryptoKey { - uint8_t bytes[32]; + uint8_t bytes[32]; - /// # of bytes, or -1 to mean "invalid key - do not use" - int8_t length; + /// # of bytes, or -1 to mean "invalid key - do not use" + int8_t length; }; /** @@ -23,74 +23,75 @@ struct CryptoKey { #define MAX_BLOCKSIZE 256 #define TEST_CURVE25519_FIELD_OPS // Exposes Curve25519::isWeakPoint() for testing keys -class CryptoEngine { -public: +class CryptoEngine +{ + public: #if !(MESHTASTIC_EXCLUDE_PKI) - uint8_t public_key[32] = {0}; + uint8_t public_key[32] = {0}; #endif - virtual ~CryptoEngine() {} + virtual ~CryptoEngine() {} #if !(MESHTASTIC_EXCLUDE_PKI) #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) - virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey); - virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); + virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey); + virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); #endif - void clearKeys(); - void setDHPrivateKey(uint8_t *_private_key); - virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, - size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); - virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, - const uint8_t *bytes, uint8_t *bytesOut); - virtual bool setDHPublicKey(uint8_t *publicKey); - virtual void hash(uint8_t *bytes, size_t numBytes); + void clearKeys(); + void setDHPrivateKey(uint8_t *_private_key); + virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, + uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); + virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); + virtual bool setDHPublicKey(uint8_t *publicKey); + virtual void hash(uint8_t *bytes, size_t numBytes); - virtual void aesSetKey(const uint8_t *key, size_t key_len); + virtual void aesSetKey(const uint8_t *key, size_t key_len); - virtual void aesEncrypt(uint8_t *in, uint8_t *out); - AESSmall256 *aes = NULL; + virtual void aesEncrypt(uint8_t *in, uint8_t *out); + AESSmall256 *aes = NULL; #endif - /** - * Set the key used for encrypt, decrypt. - * - * As a special case: If all bytes are zero, we assume _no encryption_ and send all data in cleartext. - * - * @param numBytes must be 16 (AES128), 32 (AES256) or 0 (no crypt) - * @param bytes a _static_ buffer that will remain valid for the life of this crypto instance (i.e. this class will - * cache the provided pointer) - */ - virtual void setKey(const CryptoKey &k); + /** + * Set the key used for encrypt, decrypt. + * + * As a special case: If all bytes are zero, we assume _no encryption_ and send all data in cleartext. + * + * @param numBytes must be 16 (AES128), 32 (AES256) or 0 (no crypt) + * @param bytes a _static_ buffer that will remain valid for the life of this crypto instance (i.e. this class will cache the + * provided pointer) + */ + virtual void setKey(const CryptoKey &k); - /** - * Encrypt a packet - * - * @param bytes is updated in place - */ - virtual void encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); - virtual void decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); - virtual void encryptAESCtr(CryptoKey key, uint8_t *nonce, size_t numBytes, uint8_t *bytes); + /** + * Encrypt a packet + * + * @param bytes is updated in place + */ + virtual void encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); + virtual void decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes); + virtual void encryptAESCtr(CryptoKey key, uint8_t *nonce, size_t numBytes, uint8_t *bytes); #ifndef PIO_UNIT_TESTING -protected: + protected: #endif - /** Our per packet nonce */ - uint8_t nonce[16] = {0}; - CryptoKey key = {}; - CTRCommon *ctr = NULL; + /** Our per packet nonce */ + uint8_t nonce[16] = {0}; + CryptoKey key = {}; + CTRCommon *ctr = NULL; #if !(MESHTASTIC_EXCLUDE_PKI) - uint8_t shared_key[32] = {0}; - uint8_t private_key[32] = {0}; + uint8_t shared_key[32] = {0}; + uint8_t private_key[32] = {0}; #endif - /** - * Init our 128 bit nonce for a new packet - * - * The NONCE is constructed by concatenating (from MSB to LSB): - * a 64 bit packet number (stored in little endian order) - * a 32 bit sending node number (stored in little endian order) - * a 32 bit block counter (starts at zero) - */ - void initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce = 0); + /** + * Init our 128 bit nonce for a new packet + * + * The NONCE is constructed by concatenating (from MSB to LSB): + * a 64 bit packet number (stored in little endian order) + * a 32 bit sending node number (stored in little endian order) + * a 32 bit block counter (starts at zero) + */ + void initNonce(uint32_t fromNode, uint64_t packetId, uint32_t extraNonce = 0); }; extern CryptoEngine *crypto; \ No newline at end of file diff --git a/src/mesh/Default.cpp b/src/mesh/Default.cpp index f595aed84..1bd0340f8 100644 --- a/src/mesh/Default.cpp +++ b/src/mesh/Default.cpp @@ -2,22 +2,25 @@ #include "meshUtils.h" -uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval) { - if (configuredInterval > 0) - return configuredInterval * 1000; - return defaultInterval * 1000; +uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval) +{ + if (configuredInterval > 0) + return configuredInterval * 1000; + return defaultInterval * 1000; } -uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval) { - if (configuredInterval > 0) - return configuredInterval * 1000; - return default_broadcast_interval_secs * 1000; +uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval) +{ + if (configuredInterval > 0) + return configuredInterval * 1000; + return default_broadcast_interval_secs * 1000; } -uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue) { - if (configured > 0) - return configured; - return defaultValue; +uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue) +{ + if (configured > 0) + return configured; + return defaultValue; } /** * Calculates the scaled value of the configured or default value in ms based on the number of online nodes. @@ -32,30 +35,33 @@ uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultVa * @param numOnlineNodes The number of online nodes. * @return The scaled value of the configured or default value. */ -uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) { - // If we are a router, we don't scale the value. It's already significantly higher. - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) - return getConfiguredOrDefaultMs(configured, defaultValue); +uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) +{ + // If we are a router, we don't scale the value. It's already significantly higher. + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) + return getConfiguredOrDefaultMs(configured, defaultValue); - // Additionally if we're a tracker or sensor, we want priority to send position and telemetry - if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER)) - return getConfiguredOrDefaultMs(configured, defaultValue); + // Additionally if we're a tracker or sensor, we want priority to send position and telemetry + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER)) + return getConfiguredOrDefaultMs(configured, defaultValue); - return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); + return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); } -uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue) { - // If zero, intervals should be coalesced later by getConfiguredOrDefault... methods - if (configured == 0) - return configured; +uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue) +{ + // If zero, intervals should be coalesced later by getConfiguredOrDefault... methods + if (configured == 0) + return configured; - return configured < minValue ? minValue : configured; + return configured < minValue ? minValue : configured; } -uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured) { +uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured) +{ #if USERPREFS_EVENT_MODE - return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit; + return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit; #else - return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit; + return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit; #endif } \ No newline at end of file diff --git a/src/mesh/Default.h b/src/mesh/Default.h index ec44c7190..a60e3af9b 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -38,43 +38,47 @@ #define default_mqtt_encryption_enabled true #define default_mqtt_tls_enabled false -#define IF_ROUTER(routerVal, normalVal) ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal)) +#define IF_ROUTER(routerVal, normalVal) \ + ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal)) -class Default { -public: - static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval); - static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval); - static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue); - // Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility, - // even though internal node counts use uint16_t (max 65535 nodes) - static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes); - static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured); - static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue); +class Default +{ + public: + static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval); + static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval); + static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue); + // Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility, + // even though internal node counts use uint16_t (max 65535 nodes) + static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes); + static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured); + static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue); -private: - // Note: Kept as uint32_t to match the public API parameter type - static float congestionScalingCoefficient(uint32_t numOnlineNodes) { - if (numOnlineNodes <= 40) { - return 1.0; - } else { - float throttlingFactor = 0.075; - if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW) - throttlingFactor = 0.04; - else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) - throttlingFactor = 0.02; - else if (config.lora.use_preset && - IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) - throttlingFactor = 0.01; + private: + // Note: Kept as uint32_t to match the public API parameter type + static float congestionScalingCoefficient(uint32_t numOnlineNodes) + { + if (numOnlineNodes <= 40) { + return 1.0; + } else { + float throttlingFactor = 0.075; + if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW) + throttlingFactor = 0.04; + else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) + throttlingFactor = 0.02; + else if (config.lora.use_preset && + IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) + throttlingFactor = 0.01; #if USERPREFS_EVENT_MODE - // If we are in event mode, scale down the throttling factor - throttlingFactor = 0.04; + // If we are in event mode, scale down the throttling factor + throttlingFactor = 0.04; #endif - // Scaling up traffic based on number of nodes over 40 - int nodesOverForty = (numOnlineNodes - 40); - return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) + // Scaling up traffic based on number of nodes over 40 + int nodesOverForty = (numOnlineNodes - 40); + return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) + } } - } }; diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 2e98b015e..b7459abe0 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -15,126 +15,139 @@ FloodingRouter::FloodingRouter() {} * later free() the packet to pool. This routine is not allowed to stall. * If the txmit queue is full it might return an error */ -ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p) { - // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) - p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us - wasSeenRecently(p); // FIXME, move this to a sniffSent method +ErrorCode FloodingRouter::send(meshtastic_MeshPacket *p) +{ + // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us + wasSeenRecently(p); // FIXME, move this to a sniffSent method - return Router::send(p); + return Router::send(p); } -bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { - bool wasUpgraded = false; - bool seenRecently = wasSeenRecently(p, true, nullptr, nullptr, - &wasUpgraded); // Updates history; returns false when an upgrade is detected +bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) +{ + bool wasUpgraded = false; + bool seenRecently = + wasSeenRecently(p, true, nullptr, nullptr, &wasUpgraded); // Updates history; returns false when an upgrade is detected - // Handle hop_limit upgrade scenario for rebroadcasters - if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { - return true; // we handled it, so stop processing - } - - if (seenRecently) { - printPacket("Ignore dupe incoming msg", p); - rxDupe++; - - /* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, - e.g., when the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */ - bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; - if (isRepeated) { - LOG_DEBUG("Repeated reliable tx"); - // Check if it's still in the Tx queue, if not, we have to relay it again - if (!findInTxQueue(p->from, p->id)) { - reprocessPacket(p); - perhapsRebroadcast(p); - } - } else { - perhapsCancelDupe(p); + // Handle hop_limit upgrade scenario for rebroadcasters + if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { + return true; // we handled it, so stop processing } - return true; - } + if (seenRecently) { + printPacket("Ignore dupe incoming msg", p); + rxDupe++; - return Router::shouldFilterReceived(p); -} + /* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, e.g., when + the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */ + bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; + if (isRepeated) { + LOG_DEBUG("Repeated reliable tx"); + // Check if it's still in the Tx queue, if not, we have to relay it again + if (!findInTxQueue(p->from, p->id)) { + reprocessPacket(p); + perhapsRebroadcast(p); + } + } else { + perhapsCancelDupe(p); + } -bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p) { - // isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages - if (isRebroadcaster() && iface && p->hop_limit > 0) { - // If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to - // rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead. - uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining - if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) { - LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id, p->hop_limit, dropThreshold); - - reprocessPacket(p); - perhapsRebroadcast(p); - - rxDupe++; - // We already enqueued the improved copy, so make sure the incoming packet stops here. - return true; + return true; } - } - return false; + return Router::shouldFilterReceived(p); } -void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) { - if (nodeDB) - nodeDB->updateFrom(*p); +bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p) +{ + // isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages + if (isRebroadcaster() && iface && p->hop_limit > 0) { + // If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to + // rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead. + uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining + if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) { + LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id, + p->hop_limit, dropThreshold); + + reprocessPacket(p); + perhapsRebroadcast(p); + + rxDupe++; + // We already enqueued the improved copy, so make sure the incoming packet stops here. + return true; + } + } + + return false; +} + +void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) +{ + if (nodeDB) + nodeDB->updateFrom(*p); #if !MESHTASTIC_EXCLUDE_TRACEROUTE - if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) - traceRouteModule->processUpgradedPacket(*p); + if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) + traceRouteModule->processUpgradedPacket(*p); #endif } -bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) { - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - // ROUTER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), - // even if we've heard another station rebroadcast it already. - return false; - } +bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) +{ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + // ROUTER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return false; + } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - // CLIENT_BASE: if the packet is from or to a favorited node, - // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), - // even if we've heard another station rebroadcast it already. - return !nodeDB->isFromOrToFavoritedNode(*p); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // CLIENT_BASE: if the packet is from or to a favorited node, + // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return !nodeDB->isFromOrToFavoritedNode(*p); + } - // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. - return true; + // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. + return true; } -void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { - if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { - // cancel rebroadcast of this message *if* there was already one, unless we're a router! - // But only LoRa packets should be able to trigger this. - if (Router::cancelSending(p->from, p->id)) - txRelayCanceled++; - } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { - iface->clampToLateRebroadcastWindow(getFrom(p), p->id); - } - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && nodeDB->isFromOrToFavoritedNode(*p)) { - iface->clampToLateRebroadcastWindow(getFrom(p), p->id); - } +void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) +{ + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { + // cancel rebroadcast of this message *if* there was already one, unless we're a router! + // But only LoRa packets should be able to trigger this. + if (Router::cancelSending(p->from, p->id)) + txRelayCanceled++; + } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && + nodeDB->isFromOrToFavoritedNode(*p)) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } } -bool FloodingRouter::isRebroadcaster() { - return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE && - config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; +bool FloodingRouter::isRebroadcaster() +{ + return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE && + config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; } -void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { - bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0 || p->decoded.reply_id != 0); - if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) { - // do not flood direct message that is ACKed or replied to - LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast"); - Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM - } +void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) +{ + bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && + (p->decoded.request_id != 0 || p->decoded.reply_id != 0); + if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) { + // do not flood direct message that is ACKed or replied to + LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast"); + Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM + } - perhapsRebroadcast(p); + perhapsRebroadcast(p); - // handle the packet as normal - Router::sniffReceived(p, c); + // handle the packet as normal + Router::sniffReceived(p, c); } diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index d3b9199ad..e8a2e9685 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -25,53 +25,54 @@ Any entries in recentBroadcasts that are older than X seconds (longer than the max time a flood can take) will be discarded. */ -class FloodingRouter : public Router { -public: - /** - * Constructor - * - */ - FloodingRouter(); +class FloodingRouter : public Router +{ + public: + /** + * Constructor + * + */ + FloodingRouter(); - /** - * Send a packet on a suitable interface. This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + /** + * Send a packet on a suitable interface. This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; -protected: - /** - * Should this incoming filter be dropped? - * - * Called immediately on reception, before any further processing. - * @return true to abandon the packet - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + protected: + /** + * Should this incoming filter be dropped? + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; - /** - * Look for broadcasts we need to rebroadcast - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + /** + * Look for broadcasts we need to rebroadcast + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /* Check if we should rebroadcast this packet, and do so if needed */ - virtual bool perhapsRebroadcast(const meshtastic_MeshPacket *p) = 0; + /* Check if we should rebroadcast this packet, and do so if needed */ + virtual bool perhapsRebroadcast(const meshtastic_MeshPacket *p) = 0; - /* Check if we should handle an upgraded packet (with higher hop_limit) - * @return true if we handled it (so stop processing) - */ - bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p); + /* Check if we should handle an upgraded packet (with higher hop_limit) + * @return true if we handled it (so stop processing) + */ + bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p); - /* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */ - void reprocessPacket(const meshtastic_MeshPacket *p); + /* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */ + void reprocessPacket(const meshtastic_MeshPacket *p); - // Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of - // the same packet - bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); + // Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of + // the same packet + bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); - /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ - void perhapsCancelDupe(const meshtastic_MeshPacket *p); + /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ + void perhapsCancelDupe(const meshtastic_MeshPacket *p); - // Return true if we are a rebroadcaster - bool isRebroadcaster(); + // Return true if we are a rebroadcaster + bool isRebroadcaster(); }; \ No newline at end of file diff --git a/src/mesh/LLCC68Interface.cpp b/src/mesh/LLCC68Interface.cpp index 7cc0ec4d7..d92ea5450 100644 --- a/src/mesh/LLCC68Interface.cpp +++ b/src/mesh/LLCC68Interface.cpp @@ -3,6 +3,9 @@ #include "configuration.h" #include "error.h" -LLCC68Interface::LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) {} +LLCC68Interface::LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) +{ +} #endif \ No newline at end of file diff --git a/src/mesh/LLCC68Interface.h b/src/mesh/LLCC68Interface.h index fb7cfccbb..1cd23e92b 100644 --- a/src/mesh/LLCC68Interface.h +++ b/src/mesh/LLCC68Interface.h @@ -6,11 +6,14 @@ * Our adapter for LLCC68 radios * https://www.semtech.com/products/wireless-rf/lora-core/llcc68 * ⚠️⚠️⚠️ - * Be aware that LLCC68 does not support Spreading Factor 12 (SF12) and will not work on the "LongSlow" and "VLongSlow" - * channels. You must change the channel if you get `Critical Error #3` with this module. ⚠️⚠️⚠️ + * Be aware that LLCC68 does not support Spreading Factor 12 (SF12) and will not work on the "LongSlow" and "VLongSlow" channels. + * You must change the channel if you get `Critical Error #3` with this module. + * ⚠️⚠️⚠️ */ -class LLCC68Interface : public SX126xInterface { -public: - LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +class LLCC68Interface : public SX126xInterface +{ + public: + LLCC68Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/LR1110Interface.cpp b/src/mesh/LR1110Interface.cpp index a1cfb0c2d..5dbd3ff38 100644 --- a/src/mesh/LR1110Interface.cpp +++ b/src/mesh/LR1110Interface.cpp @@ -4,6 +4,9 @@ #include "configuration.h" #include "error.h" -LR1110Interface::LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : LR11x0Interface(hal, cs, irq, rst, busy) {} +LR1110Interface::LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : LR11x0Interface(hal, cs, irq, rst, busy) +{ +} #endif \ No newline at end of file diff --git a/src/mesh/LR1110Interface.h b/src/mesh/LR1110Interface.h index 6edf76d57..2a2e6e861 100644 --- a/src/mesh/LR1110Interface.h +++ b/src/mesh/LR1110Interface.h @@ -5,8 +5,10 @@ /** * Our adapter for LR1110 radios */ -class LR1110Interface : public LR11x0Interface { -public: - LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +class LR1110Interface : public LR11x0Interface +{ + public: + LR1110Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/LR1120Interface.cpp b/src/mesh/LR1120Interface.cpp index 08c38f79c..a17ac87ef 100644 --- a/src/mesh/LR1120Interface.cpp +++ b/src/mesh/LR1120Interface.cpp @@ -4,8 +4,14 @@ #include "configuration.h" #include "error.h" -LR1120Interface::LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : LR11x0Interface(hal, cs, irq, rst, busy) {} +LR1120Interface::LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : LR11x0Interface(hal, cs, irq, rst, busy) +{ +} -bool LR1120Interface::wideLora() { return true; } +bool LR1120Interface::wideLora() +{ + return true; +} #endif \ No newline at end of file diff --git a/src/mesh/LR1120Interface.h b/src/mesh/LR1120Interface.h index 0e143bbf7..d81a480a9 100644 --- a/src/mesh/LR1120Interface.h +++ b/src/mesh/LR1120Interface.h @@ -5,9 +5,11 @@ /** * Our adapter for LR1120 wideband radios */ -class LR1120Interface : public LR11x0Interface { -public: - LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - bool wideLora() override; +class LR1120Interface : public LR11x0Interface +{ + public: + LR1120Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); + bool wideLora() override; }; #endif \ No newline at end of file diff --git a/src/mesh/LR1121Interface.cpp b/src/mesh/LR1121Interface.cpp index 5d86cfeca..29bd07d08 100644 --- a/src/mesh/LR1121Interface.cpp +++ b/src/mesh/LR1121Interface.cpp @@ -3,8 +3,14 @@ #include "configuration.h" #include "error.h" -LR1121Interface::LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : LR11x0Interface(hal, cs, irq, rst, busy) {} +LR1121Interface::LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : LR11x0Interface(hal, cs, irq, rst, busy) +{ +} -bool LR1121Interface::wideLora() { return true; } +bool LR1121Interface::wideLora() +{ + return true; +} #endif \ No newline at end of file diff --git a/src/mesh/LR1121Interface.h b/src/mesh/LR1121Interface.h index 1c15b4285..ebc5b59a9 100644 --- a/src/mesh/LR1121Interface.h +++ b/src/mesh/LR1121Interface.h @@ -6,9 +6,11 @@ /** * Our adapter for LR1121 wideband radios */ -class LR1121Interface : public LR11x0Interface { -public: - LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); - bool wideLora() override; +class LR1121Interface : public LR11x0Interface +{ + public: + LR1121Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); + bool wideLora() override; }; #endif \ No newline at end of file diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 03b9c858a..af6dd92e9 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -18,8 +18,8 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { }; #endif -// Particular boards might define a different max power based on what their hardware can do, default to max power output -// if not specified (may be dangerous if using external PA and LR11x0 power config forgotten) +// Particular boards might define a different max power based on what their hardware can do, default to max power output if not +// specified (may be dangerous if using external PA and LR11x0 power config forgotten) #if ARCH_PORTDUINO #define LR1110_MAX_POWER portduino_config.lr1110_max_power #endif @@ -39,254 +39,271 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { template LR11x0Interface::LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { - LOG_WARN("LR11x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) +{ + LOG_WARN("LR11x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -template bool LR11x0Interface::init() { +template bool LR11x0Interface::init() +{ #ifdef LR11X0_POWER_EN - pinMode(LR11X0_POWER_EN, OUTPUT); - digitalWrite(LR11X0_POWER_EN, HIGH); + pinMode(LR11X0_POWER_EN, OUTPUT); + digitalWrite(LR11X0_POWER_EN, HIGH); #endif #if ARCH_PORTDUINO - float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; // FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE #elif !defined(LR11X0_DIO3_TCXO_VOLTAGE) - float tcxoVoltage = - 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per - // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 - // (DIO3 is free to be used as an IRQ) - LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); + float tcxoVoltage = + 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per + // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 + // (DIO3 is free to be used as an IRQ) + LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); #else - float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE; - LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE); - // (DIO3 is not free to be used as an IRQ) + float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE; + LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE); + // (DIO3 is not free to be used as an IRQ) #endif - RadioLibInterface::init(); + RadioLibInterface::init(); - limitPower(LR1110_MAX_POWER); + limitPower(LR1110_MAX_POWER); - if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range - power = LR1120_MAX_POWER; - preambleLength = 12; // 12 is the default for operation above 2GHz - } + if ((power > LR1120_MAX_POWER) && + (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range + power = LR1120_MAX_POWER; + preambleLength = 12; // 12 is the default for operation above 2GHz + } #ifdef LR11X0_RF_SWITCH_SUBGHZ - pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); - digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); - LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); + pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); + digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); + LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif #ifdef LR11X0_RF_SWITCH_2_4GHZ - pinMode(LR11X0_RF_SWITCH_2_4GHZ, OUTPUT); - digitalWrite(LR11X0_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); - LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); + pinMode(LR11X0_RF_SWITCH_2_4GHZ, OUTPUT); + digitalWrite(LR11X0_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); + LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); - // \todo Display actual typename of the adapter, not just `LR11x0` - LOG_INFO("LR11x0 init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) - return false; + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + // \todo Display actual typename of the adapter, not just `LR11x0` + LOG_INFO("LR11x0 init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) + return false; - LR11x0VersionInfo_t version; - res = lora.getVersionInfo(&version); - if (res == RADIOLIB_ERR_NONE) - LOG_DEBUG("LR11x0 Device %d, HW %d, FW %d.%d, WiFi %d.%d, GNSS %d.%d", version.device, version.hardware, version.fwMajor, version.fwMinor, - version.fwMajorWiFi, version.fwMinorWiFi, version.fwGNSS, version.almanacGNSS); + LR11x0VersionInfo_t version; + res = lora.getVersionInfo(&version); + if (res == RADIOLIB_ERR_NONE) + LOG_DEBUG("LR11x0 Device %d, HW %d, FW %d.%d, WiFi %d.%d, GNSS %d.%d", version.device, version.hardware, version.fwMajor, + version.fwMinor, version.fwMajorWiFi, version.fwMinorWiFi, version.fwGNSS, version.almanacGNSS); - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); - if (res == RADIOLIB_ERR_NONE) - res = lora.setCRC(2); + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); - // FIXME: May want to set depending on a definition, currently all LR1110 variant files use the DC-DC regulator option - if (res == RADIOLIB_ERR_NONE) - res = lora.setRegulatorDCDC(); + // FIXME: May want to set depending on a definition, currently all LR1110 variant files use the DC-DC regulator option + if (res == RADIOLIB_ERR_NONE) + res = lora.setRegulatorDCDC(); #ifdef LR11X0_DIO_AS_RF_SWITCH - bool dioAsRfSwitch = true; + bool dioAsRfSwitch = true; #elif defined(ARCH_PORTDUINO) - bool dioAsRfSwitch = portduino_config.has_rfswitch_table; + bool dioAsRfSwitch = portduino_config.has_rfswitch_table; #else - bool dioAsRfSwitch = false; + bool dioAsRfSwitch = false; #endif - if (dioAsRfSwitch) { - lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); - LOG_DEBUG("Set DIO RF switch"); - } - - if (res == RADIOLIB_ERR_NONE) { - if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate - res = lora.setRxBoostedGainMode(true); - LOG_INFO("Set RX gain to boosted mode; result: %d", res); - } else { - res = lora.setRxBoostedGainMode(false); - LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); + if (dioAsRfSwitch) { + lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); + LOG_DEBUG("Set DIO RF switch"); } - } - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) { + if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate + res = lora.setRxBoostedGainMode(true); + LOG_INFO("Set RX gain to boosted mode; result: %d", res); + } else { + res = lora.setRxBoostedGainMode(false); + LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); + } + } - return res == RADIOLIB_ERR_NONE; + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; } -template bool LR11x0Interface::reconfigure() { - RadioLibInterface::reconfigure(); +template bool LR11x0Interface::reconfigure() +{ + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f)); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f)); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setSyncWord(syncWord); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setSyncWord(syncWord); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setPreambleLength(preambleLength); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setPreambleLength(preambleLength); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > LR1110_MAX_POWER) // This chip has lower power limits than some - power = LR1110_MAX_POWER; - if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit - power = LR1120_MAX_POWER; + if (power > LR1110_MAX_POWER) // This chip has lower power limits than some + power = LR1110_MAX_POWER; + if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit + power = LR1120_MAX_POWER; - err = lora.setOutputPower(power); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setOutputPower(power); + assert(err == RADIOLIB_ERR_NONE); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() { lora.clearIrqAction(); } +template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() +{ + lora.clearIrqAction(); +} -template void LR11x0Interface::setStandby() { - checkNotification(); // handle any pending interrupts before we force standby +template void LR11x0Interface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby - int err = lora.standby(); + int err = lora.standby(); - if (err != RADIOLIB_ERR_NONE) { - LOG_DEBUG("LR11x0 standby failed with error %d", err); - } + if (err != RADIOLIB_ERR_NONE) { + LOG_DEBUG("LR11x0 standby failed with error %d", err); + } - assert(err == RADIOLIB_ERR_NONE); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = false; // If we were receiving, not any more - activeReceiveStart = 0; - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** * Add SNR data to received messages */ -template void LR11x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) { - // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); - mp->rx_snr = lora.getSNR(); - mp->rx_rssi = lround(lora.getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); +template void LR11x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) +{ + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ -template void LR11x0Interface::configHardwareForSend() { RadioLibInterface::configHardwareForSend(); } +template void LR11x0Interface::configHardwareForSend() +{ + RadioLibInterface::configHardwareForSend(); +} // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY -template void LR11x0Interface::startReceive() { +template void LR11x0Interface::startReceive() +{ #ifdef SLEEP_ONLY - sleep(); + sleep(); #else - setStandby(); + setStandby(); - lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. + lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. - // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. - int err = lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); - if (err) - LOG_ERROR("StartReceive error: %d", err); - assert(err == RADIOLIB_ERR_NONE); + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + int err = + lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + if (err) + LOG_ERROR("StartReceive error: %d", err); + assert(err == RADIOLIB_ERR_NONE); - RadioLibInterface::startReceive(); + RadioLibInterface::startReceive(); - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register - // bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); #endif } /** Is the channel currently active? */ -template bool LR11x0Interface::isChannelActive() { - // check if we can detect a LoRa preamble on the current channel - ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, - .detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, - .detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, - .exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, - .timeout = 0, - .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, - .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; - int16_t result; +template bool LR11x0Interface::isChannelActive() +{ + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; - setStandby(); - result = lora.scanChannel(cfg); - if (result == RADIOLIB_LORA_DETECTED) - return true; + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; - assert(result != RADIOLIB_ERR_WRONG_MODEM); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - return false; + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -template bool LR11x0Interface::isActivelyReceiving() { - // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet - // received and handled the interrupt for reading the packet/handling errors. - return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID, RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); +template bool LR11x0Interface::isActivelyReceiving() +{ + // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet + // received and handled the interrupt for reading the packet/handling errors. + return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID, + RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); } -template bool LR11x0Interface::sleep() { - // \todo Display actual typename of the adapter, not just `LR11x0` - LOG_DEBUG("LR11x0 entering sleep mode"); - setStandby(); // Stop any pending operations +template bool LR11x0Interface::sleep() +{ + // \todo Display actual typename of the adapter, not just `LR11x0` + LOG_DEBUG("LR11x0 entering sleep mode"); + setStandby(); // Stop any pending operations - // turn off TCXO if it was powered - lora.setTCXO(0); + // turn off TCXO if it was powered + lora.setTCXO(0); - // put chipset into sleep mode (we've already disabled interrupts by now) - bool keepConfig = false; - lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = false; + lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed #ifdef LR11X0_POWER_EN - digitalWrite(LR11X0_POWER_EN, LOW); + digitalWrite(LR11X0_POWER_EN, LOW); #endif - return true; + return true; } #endif diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index b94c0077a..840184bbf 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -6,64 +6,66 @@ * \brief Adapter for LR11x0 radio family. Implements common logic for child classes. * \tparam T RadioLib module type for LR11x0: SX1262, SX1268. */ -template class LR11x0Interface : public RadioLibInterface { -public: - LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +template class LR11x0Interface : public RadioLibInterface +{ + public: + LR11x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - bool isIRQPending() override { return lora.getIrqFlags() != 0; } + bool isIRQPending() override { return lora.getIrqFlags() != 0; } -protected: - /** - * Specific module instance - */ - T lora; + protected: + /** + * Specific module instance + */ + T lora; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; #endif \ No newline at end of file diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index ad85685e1..eb5ac5109 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -8,139 +8,154 @@ #include "PointerQueue.h" #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP -template class Allocator { +template class Allocator +{ -public: - Allocator() : deleter([this](T *p) { this->release(p); }) {} - virtual ~Allocator() {} + public: + Allocator() : deleter([this](T *p) { this->release(p); }) {} + virtual ~Allocator() {} - /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available - /// Note: this method is safe to call from regular OR ISR code - T *allocZeroed() { - T *p = allocZeroed(0); - if (!p) { - LOG_WARN("Failed to allocate zeroed memory"); - } - return p; - } - - /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you - /// probably don't want this version). - T *allocZeroed(TickType_t maxWait) { - T *p = alloc(maxWait); - - if (p) - memset(p, 0, sizeof(T)); - return p; - } - - /// Return a queable object which is a copy of some other object - T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { - T *p = alloc(maxWait); - if (!p) { - LOG_WARN("Failed to allocate memory for copy"); - return nullptr; + /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available + /// Note: this method is safe to call from regular OR ISR code + T *allocZeroed() + { + T *p = allocZeroed(0); + if (!p) { + LOG_WARN("Failed to allocate zeroed memory"); + } + return p; } - *p = src; - return p; - } + /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably + /// don't want this version). + T *allocZeroed(TickType_t maxWait) + { + T *p = alloc(maxWait); - /// Variations of the above methods that return std::unique_ptr instead of raw pointers. - using UniqueAllocation = std::unique_ptr &>; - /// Return a queable object which has been prefilled with zeros. - /// std::unique_ptr wrapped variant of allocZeroed(). - UniqueAllocation allocUniqueZeroed() { return UniqueAllocation(allocZeroed(), deleter); } - /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you - /// probably don't want this version). std::unique_ptr wrapped variant of allocZeroed(TickType_t maxWait). - UniqueAllocation allocUniqueZeroed(TickType_t maxWait) { return UniqueAllocation(allocZeroed(maxWait), deleter); } - /// Return a queable object which is a copy of some other object - /// std::unique_ptr wrapped variant of allocCopy(const T &src, TickType_t maxWait). - UniqueAllocation allocUniqueCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { return UniqueAllocation(allocCopy(src, maxWait), deleter); } + if (p) + memset(p, 0, sizeof(T)); + return p; + } - /// Return a buffer for use by others - virtual void release(T *p) = 0; + /// Return a queable object which is a copy of some other object + T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) + { + T *p = alloc(maxWait); + if (!p) { + LOG_WARN("Failed to allocate memory for copy"); + return nullptr; + } -protected: - // Alloc some storage - virtual T *alloc(TickType_t maxWait) = 0; + *p = src; + return p; + } -private: - // std::unique_ptr Deleter function; calls release(). - const std::function deleter; + /// Variations of the above methods that return std::unique_ptr instead of raw pointers. + using UniqueAllocation = std::unique_ptr &>; + /// Return a queable object which has been prefilled with zeros. + /// std::unique_ptr wrapped variant of allocZeroed(). + UniqueAllocation allocUniqueZeroed() { return UniqueAllocation(allocZeroed(), deleter); } + /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably + /// don't want this version). + /// std::unique_ptr wrapped variant of allocZeroed(TickType_t maxWait). + UniqueAllocation allocUniqueZeroed(TickType_t maxWait) { return UniqueAllocation(allocZeroed(maxWait), deleter); } + /// Return a queable object which is a copy of some other object + /// std::unique_ptr wrapped variant of allocCopy(const T &src, TickType_t maxWait). + UniqueAllocation allocUniqueCopy(const T &src, TickType_t maxWait = portMAX_DELAY) + { + return UniqueAllocation(allocCopy(src, maxWait), deleter); + } + + /// Return a buffer for use by others + virtual void release(T *p) = 0; + + protected: + // Alloc some storage + virtual T *alloc(TickType_t maxWait) = 0; + + private: + // std::unique_ptr Deleter function; calls release(). + const std::function deleter; }; /** * An allocator that just uses regular free/malloc */ -template class MemoryDynamic : public Allocator { -public: - /// Return a buffer for use by others - virtual void release(T *p) override { - if (p == nullptr) - return; +template class MemoryDynamic : public Allocator +{ + public: + /// Return a buffer for use by others + virtual void release(T *p) override + { + if (p == nullptr) + return; - LOG_HEAP("Freeing 0x%x", p); + LOG_HEAP("Freeing 0x%x", p); - free(p); - } + free(p); + } -protected: - // Alloc some storage - virtual T *alloc(TickType_t maxWait) override { - T *p = (T *)malloc(sizeof(T)); - assert(p); - return p; - } + protected: + // Alloc some storage + virtual T *alloc(TickType_t maxWait) override + { + T *p = (T *)malloc(sizeof(T)); + assert(p); + return p; + } }; /** * A static memory pool that uses a fixed buffer instead of heap allocation */ -template class MemoryPool : public Allocator { -private: - T pool[MaxSize]; - bool used[MaxSize]; +template class MemoryPool : public Allocator +{ + private: + T pool[MaxSize]; + bool used[MaxSize]; -public: - MemoryPool() : pool{}, used{} { - // Arrays are now zero-initialized by member initializer list - // pool array: all elements are default-constructed (zero for POD types) - // used array: all elements are false (zero-initialized) - } - - /// Return a buffer for use by others - virtual void release(T *p) override { - if (!p) { - LOG_DEBUG("Failed to release memory, pointer is null"); - return; + public: + MemoryPool() : pool{}, used{} + { + // Arrays are now zero-initialized by member initializer list + // pool array: all elements are default-constructed (zero for POD types) + // used array: all elements are false (zero-initialized) } - // Find the index of this pointer in our pool - int index = p - pool; - if (index >= 0 && index < MaxSize) { - assert(used[index]); // Should be marked as used - used[index] = false; - LOG_HEAP("Released static pool item %d at 0x%x", index, p); - } else { - LOG_WARN("Pointer 0x%x not from our pool!", p); - } - } + /// Return a buffer for use by others + virtual void release(T *p) override + { + if (!p) { + LOG_DEBUG("Failed to release memory, pointer is null"); + return; + } -protected: - // Alloc some storage from our static pool - virtual T *alloc(TickType_t maxWait) override { - // Find first free slot - for (int i = 0; i < MaxSize; i++) { - if (!used[i]) { - used[i] = true; - LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); - return &pool[i]; - } + // Find the index of this pointer in our pool + int index = p - pool; + if (index >= 0 && index < MaxSize) { + assert(used[index]); // Should be marked as used + used[index] = false; + LOG_HEAP("Released static pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from our pool!", p); + } } - // No free slots available - return nullptr instead of asserting - LOG_WARN("No free slots available in static memory pool!"); - return nullptr; - } + protected: + // Alloc some storage from our static pool + virtual T *alloc(TickType_t maxWait) override + { + // Find first free slot + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + // No free slots available - return nullptr instead of asserting + LOG_WARN("No free slots available in static memory pool!"); + return nullptr; + } }; diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index a4c0e797a..83b64a873 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -18,278 +18,297 @@ uint8_t MeshModule::numPeriodicModules = 0; */ meshtastic_MeshPacket *MeshModule::currentReply; -MeshModule::MeshModule(const char *_name) : name(_name) { - // Can't trust static initializer order, so we check each time - if (!modules) - modules = new std::vector(); +MeshModule::MeshModule(const char *_name) : name(_name) +{ + // Can't trust static initializer order, so we check each time + if (!modules) + modules = new std::vector(); - modules->push_back(this); + modules->push_back(this); } void MeshModule::setup() {} -MeshModule::~MeshModule() { - auto it = std::find(modules->begin(), modules->end(), this); - assert(it != modules->end()); - modules->erase(it); +MeshModule::~MeshModule() +{ + auto it = std::find(modules->begin(), modules->end(), this); + assert(it != modules->end()); + modules->erase(it); } // ⚠️ **Only call once** to set the initial delay before a module starts broadcasting periodically -int32_t MeshModule::setStartDelay() { - int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS; - numPeriodicModules++; +int32_t MeshModule::setStartDelay() +{ + int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS; + numPeriodicModules++; - return startDelay; + return startDelay; } -meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { - meshtastic_Routing c = meshtastic_Routing_init_default; +meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit) +{ + meshtastic_Routing c = meshtastic_Routing_init_default; - c.error_reason = err; - c.which_variant = meshtastic_Routing_error_reason_tag; + c.error_reason = err; + c.which_variant = meshtastic_Routing_error_reason_tag; - // Now that we have moded sendAckNak up one level into the class hierarchy we can no longer assume we are a - // RoutingModule So we manually call pb_encode_to_bytes and specify routing port number auto p = allocDataProtobuf(c); - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_ROUTING_APP; - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Routing_msg, &c); + // Now that we have moded sendAckNak up one level into the class hierarchy we can no longer assume we are a RoutingModule + // So we manually call pb_encode_to_bytes and specify routing port number + // auto p = allocDataProtobuf(c); + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_ROUTING_APP; + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Routing_msg, &c); - p->priority = meshtastic_MeshPacket_Priority_ACK; + p->priority = meshtastic_MeshPacket_Priority_ACK; - p->hop_limit = hopLimit; // Flood ACK back to original sender - p->to = to; - p->decoded.request_id = idFrom; - p->channel = chIndex; - if (err != meshtastic_Routing_Error_NONE) - LOG_WARN("Alloc an err=%d,to=0x%x,idFrom=0x%x,id=0x%x", err, to, idFrom, p->id); + p->hop_limit = hopLimit; // Flood ACK back to original sender + p->to = to; + p->decoded.request_id = idFrom; + p->channel = chIndex; + if (err != meshtastic_Routing_Error_NONE) + LOG_WARN("Alloc an err=%d,to=0x%x,idFrom=0x%x,id=0x%x", err, to, idFrom, p->id); - return p; + return p; } -meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p) { - // If the original packet couldn't be decoded, use the primary channel - uint8_t channelIndex = p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? p->channel : channels.getPrimaryIndex(); - auto r = allocAckNak(err, getFrom(p), p->id, channelIndex); +meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p) +{ + // If the original packet couldn't be decoded, use the primary channel + uint8_t channelIndex = + p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? p->channel : channels.getPrimaryIndex(); + auto r = allocAckNak(err, getFrom(p), p->id, channelIndex); - setReplyTo(r, *p); + setReplyTo(r, *p); - return r; + return r; } -void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) { - // LOG_DEBUG("In call modules"); - bool moduleFound = false; +void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) +{ + // LOG_DEBUG("In call modules"); + bool moduleFound = false; - // We now allow **encrypted** packets to pass through the modules - bool isDecoded = mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag; + // We now allow **encrypted** packets to pass through the modules + bool isDecoded = mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag; - currentReply = NULL; // No reply yet + currentReply = NULL; // No reply yet - bool ignoreRequest = false; // No module asked to ignore the request yet + bool ignoreRequest = false; // No module asked to ignore the request yet - // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets - auto ourNodeNum = nodeDB->getNodeNum(); - bool toUs = isBroadcast(mp.to) || isToUs(&mp); + // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets + auto ourNodeNum = nodeDB->getNodeNum(); + bool toUs = isBroadcast(mp.to) || isToUs(&mp); - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; - pi.currentRequest = ∓ + pi.currentRequest = ∓ - /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) - bool wantsPacket = (isDecoded || pi.encryptedOk) && (pi.isPromiscuous || toUs) && pi.wantPacket(&mp); + /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) + bool wantsPacket = (isDecoded || pi.encryptedOk) && (pi.isPromiscuous || toUs) && pi.wantPacket(&mp); - if ((src == RX_SRC_LOCAL) && !(pi.loopbackOk)) { - // new case, monitor separately for now, then FIXME merge above - wantsPacket = false; - } - - assert(!pi.myReply); // If it is !null it means we have a bug, because it should have been sent the previous time - - if (wantsPacket) { - LOG_DEBUG("Module '%s' wantsPacket=%d", pi.name, wantsPacket); - - moduleFound = true; - - /// received channel (or NULL if not decoded) - meshtastic_Channel *ch = isDecoded ? &channels.getByIndex(mp.channel) : NULL; - - /// Is the channel this packet arrived on acceptable? (security check) - /// Note: we can't know channel names for encrypted packets, so those are NEVER sent to boundChannel modules - - /// Also: if a packet comes in on the local PC interface, we don't check for bound channels, because it is TRUSTED - /// and it needs to to be able to fetch the initial admin packets without yet knowing any channels. - - bool rxChannelOk = !pi.boundChannel || (mp.from == 0) || (ch && strcasecmp(ch->settings.name, pi.boundChannel) == 0); - - if (!rxChannelOk) { - // no one should have already replied! - assert(!currentReply); - - if (isDecoded && mp.decoded.want_response) { - printPacket("packet on wrong channel, returning error", &mp); - currentReply = pi.allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); - } else - printPacket("packet on wrong channel, but can't respond", &mp); - } else { - ProcessMessage handled = pi.handleReceived(mp); - - pi.alterReceived(mp); - - // Possibly send replies (but only if the message was directed to us specifically, i.e. not for promiscious - // sniffing) also: we only let the one module send a reply, once that happens, remaining modules are not - // considered - - // NOTE: we send a reply *even if the (non broadcast) request was from us* which is unfortunate but necessary - // because currently when the phone sends things, it sends things using the local node ID as the from address. A - // better solution (FIXME) would be to let phones have their own distinct addresses and we 'route' to them like - // any other node. - if (isDecoded && mp.decoded.want_response && toUs && (!isFromUs(&mp) || isToUs(&mp)) && !currentReply) { - pi.sendResponse(mp); - ignoreRequest = ignoreRequest || pi.ignoreRequest; // If at least one module asks it, we may ignore a request - LOG_INFO("Asked module '%s' to send a response", pi.name); - } else { - LOG_DEBUG("Module '%s' considered", pi.name); + if ((src == RX_SRC_LOCAL) && !(pi.loopbackOk)) { + // new case, monitor separately for now, then FIXME merge above + wantsPacket = false; } - // If the requester didn't ask for a response we might need to discard unused replies to prevent memory leaks - if (pi.myReply) { - LOG_DEBUG("Discard an unneeded response"); - packetPool.release(pi.myReply); - pi.myReply = NULL; + assert(!pi.myReply); // If it is !null it means we have a bug, because it should have been sent the previous time + + if (wantsPacket) { + LOG_DEBUG("Module '%s' wantsPacket=%d", pi.name, wantsPacket); + + moduleFound = true; + + /// received channel (or NULL if not decoded) + meshtastic_Channel *ch = isDecoded ? &channels.getByIndex(mp.channel) : NULL; + + /// Is the channel this packet arrived on acceptable? (security check) + /// Note: we can't know channel names for encrypted packets, so those are NEVER sent to boundChannel modules + + /// Also: if a packet comes in on the local PC interface, we don't check for bound channels, because it is TRUSTED and + /// it needs to to be able to fetch the initial admin packets without yet knowing any channels. + + bool rxChannelOk = !pi.boundChannel || (mp.from == 0) || (ch && strcasecmp(ch->settings.name, pi.boundChannel) == 0); + + if (!rxChannelOk) { + // no one should have already replied! + assert(!currentReply); + + if (isDecoded && mp.decoded.want_response) { + printPacket("packet on wrong channel, returning error", &mp); + currentReply = pi.allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + } else + printPacket("packet on wrong channel, but can't respond", &mp); + } else { + ProcessMessage handled = pi.handleReceived(mp); + + pi.alterReceived(mp); + + // Possibly send replies (but only if the message was directed to us specifically, i.e. not for promiscious + // sniffing) also: we only let the one module send a reply, once that happens, remaining modules are not + // considered + + // NOTE: we send a reply *even if the (non broadcast) request was from us* which is unfortunate but necessary + // because currently when the phone sends things, it sends things using the local node ID as the from address. A + // better solution (FIXME) would be to let phones have their own distinct addresses and we 'route' to them like + // any other node. + if (isDecoded && mp.decoded.want_response && toUs && (!isFromUs(&mp) || isToUs(&mp)) && !currentReply) { + pi.sendResponse(mp); + ignoreRequest = ignoreRequest || pi.ignoreRequest; // If at least one module asks it, we may ignore a request + LOG_INFO("Asked module '%s' to send a response", pi.name); + } else { + LOG_DEBUG("Module '%s' considered", pi.name); + } + + // If the requester didn't ask for a response we might need to discard unused replies to prevent memory leaks + if (pi.myReply) { + LOG_DEBUG("Discard an unneeded response"); + packetPool.release(pi.myReply); + pi.myReply = NULL; + } + + if (handled == ProcessMessage::STOP) { + LOG_DEBUG("Module '%s' handled and skipped other processing", pi.name); + break; + } + } } - if (handled == ProcessMessage::STOP) { - LOG_DEBUG("Module '%s' handled and skipped other processing", pi.name); - break; + pi.currentRequest = NULL; + } + + if (isDecoded && mp.decoded.want_response && toUs) { + if (currentReply) { + printPacket("Send response", currentReply); + service->sendToMesh(currentReply); + currentReply = NULL; + } else if (mp.from != ourNodeNum && !ignoreRequest) { + // Note: if the message started with the local node or a module asked to ignore the request, we don't want to send a + // no response reply + + // No one wanted to reply to this request, tell the requster that happened + LOG_DEBUG("No one responded, send a nak"); + + // SECURITY NOTE! I considered sending back a different error code if we didn't find the psk (i.e. !isDecoded) + // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" vs + // bad. + routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, + routingModule->getHopLimitForResponse(mp)); } - } } - pi.currentRequest = NULL; - } - - if (isDecoded && mp.decoded.want_response && toUs) { - if (currentReply) { - printPacket("Send response", currentReply); - service->sendToMesh(currentReply); - currentReply = NULL; - } else if (mp.from != ourNodeNum && !ignoreRequest) { - // Note: if the message started with the local node or a module asked to ignore the request, we don't want to send - // a no response reply - - // No one wanted to reply to this request, tell the requster that happened - LOG_DEBUG("No one responded, send a nak"); - - // SECURITY NOTE! I considered sending back a different error code if we didn't find the psk (i.e. !isDecoded) - // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" - // vs bad. - routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, routingModule->getHopLimitForResponse(mp)); + if (!moduleFound && isDecoded) { + LOG_DEBUG("No modules interested in portnum=%d, src=%s", mp.decoded.portnum, (src == RX_SRC_LOCAL) ? "LOCAL" : "REMOTE"); } - } - - if (!moduleFound && isDecoded) { - LOG_DEBUG("No modules interested in portnum=%d, src=%s", mp.decoded.portnum, (src == RX_SRC_LOCAL) ? "LOCAL" : "REMOTE"); - } } -meshtastic_MeshPacket *MeshModule::allocReply() { - auto r = myReply; - myReply = NULL; // Only use each reply once - return r; +meshtastic_MeshPacket *MeshModule::allocReply() +{ + auto r = myReply; + myReply = NULL; // Only use each reply once + return r; } /** Messages can be received that have the want_response bit set. If set, this callback will be invoked * so that subclasses can (optionally) send a response back to the original sender. Implementing this method * is optional */ -void MeshModule::sendResponse(const meshtastic_MeshPacket &req) { - auto r = allocReply(); - if (r) { - setReplyTo(r, req); - currentReply = r; - } else { - // Ignore - this is now expected behavior for routing module (because it ignores some replies) - // LOG_WARN("Client requested response but this module did not provide"); - } +void MeshModule::sendResponse(const meshtastic_MeshPacket &req) +{ + auto r = allocReply(); + if (r) { + setReplyTo(r, req); + currentReply = r; + } else { + // Ignore - this is now expected behavior for routing module (because it ignores some replies) + // LOG_WARN("Client requested response but this module did not provide"); + } } /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet * This ensures that if the request packet was sent reliably, the reply is sent that way as well. */ -void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) { - assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now - p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 - p->channel = to.channel; // Use the same channel that the request came in on - p->hop_limit = routingModule->getHopLimitForResponse(to); +void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) +{ + assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now + p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 + p->channel = to.channel; // Use the same channel that the request came in on + p->hop_limit = routingModule->getHopLimitForResponse(to); - // No need for an ack if we are just delivering locally (it just generates an ignored ack) - p->want_ack = (to.from != 0) ? to.want_ack : false; - if (p->priority == meshtastic_MeshPacket_Priority_UNSET) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - p->decoded.request_id = to.id; + // No need for an ack if we are just delivering locally (it just generates an ignored ack) + p->want_ack = (to.from != 0) ? to.want_ack : false; + if (p->priority == meshtastic_MeshPacket_Priority_UNSET) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + p->decoded.request_id = to.id; } -std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) { - std::vector modulesWithUIFrames; +std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) +{ + std::vector modulesWithUIFrames; - // Fill with nullptr up to startIndex - modulesWithUIFrames.resize(startIndex, nullptr); + // Fill with nullptr up to startIndex + modulesWithUIFrames.resize(startIndex, nullptr); - if (modules) { - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; - if (pi.wantUIFrame()) { - LOG_DEBUG("%s wants a UI Frame", pi.name); - modulesWithUIFrames.push_back(&pi); - } + if (modules) { + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + if (pi.wantUIFrame()) { + LOG_DEBUG("%s wants a UI Frame", pi.name); + modulesWithUIFrames.push_back(&pi); + } + } } - } - return modulesWithUIFrames; + return modulesWithUIFrames; } -void MeshModule::observeUIEvents(Observer *observer) { - if (modules) { - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; - Observable *observable = pi.getUIFrameObservable(); - if (observable != NULL) { - LOG_DEBUG("%s wants a UI Frame", pi.name); - observer->observe(observable); - } +void MeshModule::observeUIEvents(Observer *observer) +{ + if (modules) { + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + Observable *observable = pi.getUIFrameObservable(); + if (observable != NULL) { + LOG_DEBUG("%s wants a UI Frame", pi.name); + observer->observe(observable); + } + } } - } } -AdminMessageHandleResult MeshModule::handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - AdminMessageHandleResult handled = AdminMessageHandleResult::NOT_HANDLED; - if (modules) { - for (auto i = modules->begin(); i != modules->end(); ++i) { - auto &pi = **i; - AdminMessageHandleResult h = pi.handleAdminMessageForModule(mp, request, response); - if (h == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { - // In case we have a response it always has priority. - LOG_DEBUG("Reply prepared by module '%s' of variant: %d", pi.name, response->which_payload_variant); - handled = h; - } else if ((handled != AdminMessageHandleResult::HANDLED_WITH_RESPONSE) && (h == AdminMessageHandleResult::HANDLED)) { - // In case the message is handled it should be populated, but will not overwrite - // a result with response. - handled = h; - } +AdminMessageHandleResult MeshModule::handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult handled = AdminMessageHandleResult::NOT_HANDLED; + if (modules) { + for (auto i = modules->begin(); i != modules->end(); ++i) { + auto &pi = **i; + AdminMessageHandleResult h = pi.handleAdminMessageForModule(mp, request, response); + if (h == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + // In case we have a response it always has priority. + LOG_DEBUG("Reply prepared by module '%s' of variant: %d", pi.name, response->which_payload_variant); + handled = h; + } else if ((handled != AdminMessageHandleResult::HANDLED_WITH_RESPONSE) && (h == AdminMessageHandleResult::HANDLED)) { + // In case the message is handled it should be populated, but will not overwrite + // a result with response. + handled = h; + } + } } - } - return handled; + return handled; } #if HAS_SCREEN // Would our module like its frame to be focused after Screen::setFrames has regenerated the list of frames? // Only considered if setFrames is triggered by a UIFrameEvent -bool MeshModule::isRequestingFocus() { - if (_requestingFocus) { - _requestingFocus = false; // Consume the request - return true; - } else - return false; +bool MeshModule::isRequestingFocus() +{ + if (_requestingFocus) { + _requestingFocus = false; // Consume the request + return true; + } else + return false; } #endif \ No newline at end of file diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index 4fe04d1c7..63f401d18 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -19,8 +19,8 @@ * Use ProcessMessage::STOP to stop further message processing. */ enum class ProcessMessage { - CONTINUE = 0, - STOP = 1, + CONTINUE = 0, + STOP = 1, }; /** @@ -30,193 +30,197 @@ enum class ProcessMessage { * should be returned. */ enum class AdminMessageHandleResult { - NOT_HANDLED = 0, - HANDLED = 1, - HANDLED_WITH_RESPONSE = 2, + NOT_HANDLED = 0, + HANDLED = 1, + HANDLED_WITH_RESPONSE = 2, }; /* * This struct is used by Screen to figure out whether screen frame should be updated. */ struct UIFrameEvent { - // What do we actually want to happen? - enum Action { - REDRAW_ONLY, // Don't change which frames are show, just redraw, asap - REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() - REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout - SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen - } action = REDRAW_ONLY; + // What do we actually want to happen? + enum Action { + REDRAW_ONLY, // Don't change which frames are show, just redraw, asap + REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() + REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout + SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen + } action = REDRAW_ONLY; - // We might want to pass additional data inside this struct at some point + // We might want to pass additional data inside this struct at some point }; /** A baseclass for any mesh "module". * * A module allows you to add new features to meshtastic device code, without needing to know messaging details. * - * A key concept for this is that your module should use a particular "portnum" for each message type you want to - * receive and handle. + * A key concept for this is that your module should use a particular "portnum" for each message type you want to receive + * and handle. * * Internally we use modules to implement the core meshtastic text messaging and gps position sharing features. You * can use these classes as examples for how to write your own custom module. See here: (FIXME) */ -class MeshModule { - static std::vector *modules; +class MeshModule +{ + static std::vector *modules; -public: - /** Constructor - * name is for debugging output - */ - MeshModule(const char *_name); + public: + /** Constructor + * name is for debugging output + */ + MeshModule(const char *_name); - virtual ~MeshModule(); + virtual ~MeshModule(); - /** For use only by MeshService - */ - static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); + /** For use only by MeshService + */ + static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); - static std::vector GetMeshModulesWithUIFrames(int startIndex); - static void observeUIEvents(Observer *observer); - static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response); + static std::vector GetMeshModulesWithUIFrames(int startIndex); + static void observeUIEvents(Observer *observer); + static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response); #if HAS_SCREEN - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } - virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset - virtual bool interceptingKeyboardInput() { return false; } // Can screen use keyboard for nav, or is module handling input? + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } + virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset + virtual bool interceptingKeyboardInput() { return false; } // Can screen use keyboard for nav, or is module handling input? #endif -protected: - const char *name; + protected: + const char *name; - /** Most modules only care about packets that are destined for their node (i.e. broadcasts or has their node as the - specific recipient) But some plugs might want to 'sniff' packets that are merely being routed (passing through the - current node). Those modules can set this to true and their handleReceived() will be called for every packet. - */ - bool isPromiscuous = false; + /** Most modules only care about packets that are destined for their node (i.e. broadcasts or has their node as the specific + recipient) But some plugs might want to 'sniff' packets that are merely being routed (passing through the current node). Those + modules can set this to true and their handleReceived() will be called for every packet. + */ + bool isPromiscuous = false; - /** Also receive a copy of LOCALLY GENERATED messages - most modules should leave - * this setting disabled - see issue #877 */ - bool loopbackOk = false; + /** Also receive a copy of LOCALLY GENERATED messages - most modules should leave + * this setting disabled - see issue #877 */ + bool loopbackOk = false; - /** Most modules only understand decrypted packets. For modules that also want to see encrypted packets, they should - * set this flag */ - bool encryptedOk = false; + /** Most modules only understand decrypted packets. For modules that also want to see encrypted packets, they should set this + * flag */ + bool encryptedOk = false; - /* We allow modules to ignore a request without sending an error if they have a specific reason for it. */ - bool ignoreRequest = false; + /* We allow modules to ignore a request without sending an error if they have a specific reason for it. */ + bool ignoreRequest = false; - /** If a bound channel name is set, we will only accept received packets that come in on that channel. - * A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface - * are allowed on any channel (this lets the local user do anything). - * - * We will send responses on the same channel that the request arrived on. - */ - const char *boundChannel = NULL; + /** If a bound channel name is set, we will only accept received packets that come in on that channel. + * A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface + * are allowed on any channel (this lets the local user do anything). + * + * We will send responses on the same channel that the request arrived on. + */ + const char *boundChannel = NULL; - /** - * If this module is currently handling a request currentRequest will be preset - * to the packet with the request. This is mostly useful for reply handlers. - * - * Note: this can be static because we are guaranteed to be processing only one - * plumodulegin at a time. - */ - static const meshtastic_MeshPacket *currentRequest; + /** + * If this module is currently handling a request currentRequest will be preset + * to the packet with the request. This is mostly useful for reply handlers. + * + * Note: this can be static because we are guaranteed to be processing only one + * plumodulegin at a time. + */ + static const meshtastic_MeshPacket *currentRequest; - // We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time - static uint8_t numPeriodicModules; + // We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time + static uint8_t numPeriodicModules; - // Set the start delay for module that broadcasts periodically - int32_t setStartDelay(); + // Set the start delay for module that broadcasts periodically + int32_t setStartDelay(); - /** - * If your handler wants to send a response, simply set currentReply and it will be sent at the end of response - * handling. - */ - meshtastic_MeshPacket *myReply = NULL; + /** + * If your handler wants to send a response, simply set currentReply and it will be sent at the end of response handling. + */ + meshtastic_MeshPacket *myReply = NULL; - /** - * Initialize your module. This setup function is called once after all hardware and mesh protocol layers have - * been initialized - */ - virtual void setup(); + /** + * Initialize your module. This setup function is called once after all hardware and mesh protocol layers have + * been initialized + */ + virtual void setup(); - /** - * @return true if you want to receive the specified portnum - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) = 0; + /** + * @return true if you want to receive the specified portnum + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) = 0; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) { return ProcessMessage::CONTINUE; } + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) { return ProcessMessage::CONTINUE; } - /** Called to change a particular incoming message - This allows the module to change the message before it is passed through the rest of the call-chain. - */ - virtual void alterReceived(meshtastic_MeshPacket &mp) {} + /** Called to change a particular incoming message + This allows the module to change the message before it is passed through the rest of the call-chain. + */ + virtual void alterReceived(meshtastic_MeshPacket &mp) {} - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. - * - * Note: most implementers don't need to override this, instead: If while handling a request you have a reply, just - * set the protected reply field in this instance. - * */ - virtual meshtastic_MeshPacket *allocReply(); + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. + * + * Note: most implementers don't need to override this, instead: If while handling a request you have a reply, just set + * the protected reply field in this instance. + * */ + virtual meshtastic_MeshPacket *allocReply(); - /*** - * @return true if you want to be alloced a UI screen frame - */ - virtual bool wantUIFrame() { return false; } - virtual Observable *getUIFrameObservable() { return NULL; } + /*** + * @return true if you want to be alloced a UI screen frame + */ + virtual bool wantUIFrame() { return false; } + virtual Observable *getUIFrameObservable() { return NULL; } - meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit = 0); - /// Send an error response for the specified packet. - meshtastic_MeshPacket *allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p); + /// Send an error response for the specified packet. + meshtastic_MeshPacket *allocErrorResponse(meshtastic_Routing_Error err, const meshtastic_MeshPacket *p); - /** - * @brief An admin message arrived to AdminModule. Module was asked whether it want to handle the request. - * - * @param mp The mesh packet arrived. - * @param request The AdminMessage request extracted from the packet. - * @param response The prepared response - * @return AdminMessageHandleResult - * HANDLED if message was handled - * HANDLED_WITH_RESPONSE if a response is also prepared and to be sent. - */ - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - return AdminMessageHandleResult::NOT_HANDLED; - }; + /** + * @brief An admin message arrived to AdminModule. Module was asked whether it want to handle the request. + * + * @param mp The mesh packet arrived. + * @param request The AdminMessage request extracted from the packet. + * @param response The prepared response + * @return AdminMessageHandleResult + * HANDLED if message was handled + * HANDLED_WITH_RESPONSE if a response is also prepared and to be sent. + */ + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) + { + return AdminMessageHandleResult::NOT_HANDLED; + }; #if HAS_SCREEN - /** Request that our module's screen frame be focused when Screen::setFrames runs - * Only considered if Screen::setFrames is triggered via a UIFrameEvent - * - * Having this as a separate call, instead of part of the UIFrameEvent, allows the module to delay decision - * until drawFrame() is called. This required less restructuring. - */ - bool _requestingFocus = false; - void requestFocus() { _requestingFocus = true; } + /** Request that our module's screen frame be focused when Screen::setFrames runs + * Only considered if Screen::setFrames is triggered via a UIFrameEvent + * + * Having this as a separate call, instead of part of the UIFrameEvent, allows the module to delay decision + * until drawFrame() is called. This required less restructuring. + */ + bool _requestingFocus = false; + void requestFocus() { _requestingFocus = true; } #else - void requestFocus(){}; // No-op + void requestFocus(){}; // No-op #endif -private: - /** - * If any of the current chain of modules has already sent a reply, it will be here. This is useful to allow - * the RoutingModule to avoid sending redundant acks - */ - static meshtastic_MeshPacket *currentReply; + private: + /** + * If any of the current chain of modules has already sent a reply, it will be here. This is useful to allow + * the RoutingModule to avoid sending redundant acks + */ + static meshtastic_MeshPacket *currentReply; - friend class ReliableRouter; + friend class ReliableRouter; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. This method calls allocReply() - * to generate the reply message, and if !NULL that message will be delivered to whoever sent req - */ - void sendResponse(const meshtastic_MeshPacket &req); + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. This method calls allocReply() + * to generate the reply message, and if !NULL that message will be delivered to whoever sent req + */ + void sendResponse(const meshtastic_MeshPacket &req); }; /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index 8ce629c6b..cbea85c62 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -6,168 +6,184 @@ #include /// @return the priority of the specified packet -inline uint32_t getPriority(const meshtastic_MeshPacket *p) { - auto pri = p->priority; - return pri; +inline uint32_t getPriority(const meshtastic_MeshPacket *p) +{ + auto pri = p->priority; + return pri; } /// @return "true" if "p1" is ordered before "p2" -bool CompareMeshPacketFunc(const meshtastic_MeshPacket *p1, const meshtastic_MeshPacket *p2) { - assert(p1 && p2); +bool CompareMeshPacketFunc(const meshtastic_MeshPacket *p1, const meshtastic_MeshPacket *p2) +{ + assert(p1 && p2); - // If one packet is in the late transmit window, prefer the other one - if ((bool)p1->tx_after != (bool)p2->tx_after) { - return !p1->tx_after; - } + // If one packet is in the late transmit window, prefer the other one + if ((bool)p1->tx_after != (bool)p2->tx_after) { + return !p1->tx_after; + } - auto p1p = getPriority(p1), p2p = getPriority(p2); - // If priorities differ, use that - // for equal priorities, prefer packets already on mesh. - return (p1p != p2p) ? (p1p > p2p) : (!isFromUs(p1) && isFromUs(p2)); + auto p1p = getPriority(p1), p2p = getPriority(p2); + // If priorities differ, use that + // for equal priorities, prefer packets already on mesh. + return (p1p != p2p) ? (p1p > p2p) : (!isFromUs(p1) && isFromUs(p2)); } MeshPacketQueue::MeshPacketQueue(size_t _maxLen) : maxLen(_maxLen) {} -bool MeshPacketQueue::empty() { return queue.empty(); } +bool MeshPacketQueue::empty() +{ + return queue.empty(); +} /** * Some clients might not properly set priority, therefore we fix it here. */ -void fixPriority(meshtastic_MeshPacket *p) { - // We might receive acks from other nodes (and since generated remotely, they won't have priority assigned. Check for - // that and fix it - if (p->priority == meshtastic_MeshPacket_Priority_UNSET) { - // if a reliable message give a bit higher default priority - p->priority = (p->want_ack ? meshtastic_MeshPacket_Priority_RELIABLE : meshtastic_MeshPacket_Priority_DEFAULT); - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - // if acks/naks give very high priority - if (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) { - p->priority = meshtastic_MeshPacket_Priority_ACK; - // if text or admin, give high priority - } else if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { - p->priority = meshtastic_MeshPacket_Priority_HIGH; - // if it is a response, give higher priority to let it arrive early and stop the request being relayed - } else if (p->decoded.request_id != 0) { - p->priority = meshtastic_MeshPacket_Priority_RESPONSE; - // Also if we want a response, give a bit higher priority - } else if (p->decoded.want_response) { - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - } +void fixPriority(meshtastic_MeshPacket *p) +{ + // We might receive acks from other nodes (and since generated remotely, they won't have priority assigned. Check for that + // and fix it + if (p->priority == meshtastic_MeshPacket_Priority_UNSET) { + // if a reliable message give a bit higher default priority + p->priority = (p->want_ack ? meshtastic_MeshPacket_Priority_RELIABLE : meshtastic_MeshPacket_Priority_DEFAULT); + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + // if acks/naks give very high priority + if (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) { + p->priority = meshtastic_MeshPacket_Priority_ACK; + // if text or admin, give high priority + } else if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || + p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { + p->priority = meshtastic_MeshPacket_Priority_HIGH; + // if it is a response, give higher priority to let it arrive early and stop the request being relayed + } else if (p->decoded.request_id != 0) { + p->priority = meshtastic_MeshPacket_Priority_RESPONSE; + // Also if we want a response, give a bit higher priority + } else if (p->decoded.want_response) { + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + } + } } - } } /** enqueue a packet, return false if full */ -bool MeshPacketQueue::enqueue(meshtastic_MeshPacket *p, bool *dropped) { - // no space - try to replace a lower priority packet in the queue - if (queue.size() >= maxLen) { - bool replaced = replaceLowerPriorityPacket(p); - if (!replaced) { - LOG_WARN("TX queue is full, and there is no lower-priority packet available to evict in favour of 0x%08x", p->id); +bool MeshPacketQueue::enqueue(meshtastic_MeshPacket *p, bool *dropped) +{ + // no space - try to replace a lower priority packet in the queue + if (queue.size() >= maxLen) { + bool replaced = replaceLowerPriorityPacket(p); + if (!replaced) { + LOG_WARN("TX queue is full, and there is no lower-priority packet available to evict in favour of 0x%08x", p->id); + } + if (dropped) { + *dropped = true; + } + return replaced; } + if (dropped) { - *dropped = true; + *dropped = false; } - return replaced; - } - if (dropped) { - *dropped = false; - } - - // Find the correct position using upper_bound to maintain a stable order - auto it = std::upper_bound(queue.begin(), queue.end(), p, CompareMeshPacketFunc); - queue.insert(it, p); // Insert packet at the found position - return true; + // Find the correct position using upper_bound to maintain a stable order + auto it = std::upper_bound(queue.begin(), queue.end(), p, CompareMeshPacketFunc); + queue.insert(it, p); // Insert packet at the found position + return true; } -meshtastic_MeshPacket *MeshPacketQueue::dequeue() { - if (empty()) { - return NULL; - } +meshtastic_MeshPacket *MeshPacketQueue::dequeue() +{ + if (empty()) { + return NULL; + } - auto *p = queue.front(); - queue.erase(queue.begin()); // Remove the highest-priority packet - return p; + auto *p = queue.front(); + queue.erase(queue.begin()); // Remove the highest-priority packet + return p; } -meshtastic_MeshPacket *MeshPacketQueue::getFront() { - if (empty()) { - return NULL; - } +meshtastic_MeshPacket *MeshPacketQueue::getFront() +{ + if (empty()) { + return NULL; + } - auto *p = queue.front(); - return p; + auto *p = queue.front(); + return p; } /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ -meshtastic_MeshPacket *MeshPacketQueue::getPacketFromQueue(NodeNum from, PacketId id) { - for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); - if (getFrom(p) == from && p->id == id) { - return p; +meshtastic_MeshPacket *MeshPacketQueue::getPacketFromQueue(NodeNum from, PacketId id) +{ + for (auto it = queue.begin(); it != queue.end(); it++) { + auto p = (*it); + if (getFrom(p) == from && p->id == id) { + return p; + } } - } - return NULL; + return NULL; } -/** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found - */ -meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late, uint8_t hop_limit_lt) { - for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); - if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after)) && - (!hop_limit_lt || p->hop_limit < hop_limit_lt)) { - queue.erase(it); - return p; +/** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found */ +meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late, uint8_t hop_limit_lt) +{ + for (auto it = queue.begin(); it != queue.end(); it++) { + auto p = (*it); + if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after)) && + (!hop_limit_lt || p->hop_limit < hop_limit_lt)) { + queue.erase(it); + return p; + } } - } - return NULL; + return NULL; } /* Attempt to find a packet from this queue. Return true if it was found. */ -bool MeshPacketQueue::find(const NodeNum from, const PacketId id) { return getPacketFromQueue(from, id) != NULL; } +bool MeshPacketQueue::find(const NodeNum from, const PacketId id) +{ + return getPacketFromQueue(from, id) != NULL; +} /** * Attempt to find a lower-priority packet in the queue and replace it with the provided one. * @return True if the replacement succeeded, false otherwise */ -bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) { +bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) +{ - if (queue.empty()) { - return false; // No packets to replace - } - - // Check if the packet at the back has a lower priority than the new packet - auto *backPacket = queue.back(); - if (!backPacket->tx_after && backPacket->priority < p->priority) { - LOG_WARN("Dropping packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", backPacket->id, p->id); - // Remove the back packet - queue.pop_back(); - packetPool.release(backPacket); - // Insert the new packet in the correct order - enqueue(p); - return true; - } - - if (backPacket->tx_after) { - // Check if there's a non-late packet with lower priority - auto it = queue.end(); - auto refPacket = *--it; - for (; refPacket->tx_after && it != queue.begin(); refPacket = *--it) - ; - if (!refPacket->tx_after && refPacket->priority < p->priority) { - LOG_WARN("Dropping non-late packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", refPacket->id, p->id); - queue.erase(it); - packetPool.release(refPacket); - // Insert the new packet in the correct order - enqueue(p); - return true; + if (queue.empty()) { + return false; // No packets to replace } - } - // If the back packet's priority is not lower, no replacement occurs - return false; + // Check if the packet at the back has a lower priority than the new packet + auto *backPacket = queue.back(); + if (!backPacket->tx_after && backPacket->priority < p->priority) { + LOG_WARN("Dropping packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", backPacket->id, p->id); + // Remove the back packet + queue.pop_back(); + packetPool.release(backPacket); + // Insert the new packet in the correct order + enqueue(p); + return true; + } + + if (backPacket->tx_after) { + // Check if there's a non-late packet with lower priority + auto it = queue.end(); + auto refPacket = *--it; + for (; refPacket->tx_after && it != queue.begin(); refPacket = *--it) + ; + if (!refPacket->tx_after && refPacket->priority < p->priority) { + LOG_WARN("Dropping non-late packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", + refPacket->id, p->id); + queue.erase(it); + packetPool.release(refPacket); + // Insert the new packet in the correct order + enqueue(p); + return true; + } + } + + // If the back packet's priority is not lower, no replacement occurs + return false; } \ No newline at end of file diff --git a/src/mesh/MeshPacketQueue.h b/src/mesh/MeshPacketQueue.h index 4de595e7c..3d3902c1e 100644 --- a/src/mesh/MeshPacketQueue.h +++ b/src/mesh/MeshPacketQueue.h @@ -7,42 +7,43 @@ /** * A priority queue of packets */ -class MeshPacketQueue { - size_t maxLen; - std::vector queue; +class MeshPacketQueue +{ + size_t maxLen; + std::vector queue; - /** Replace a lower priority package in the queue with 'mp' (provided there are lower pri packages). Return true if - * replaced. - */ - bool replaceLowerPriorityPacket(meshtastic_MeshPacket *mp); + /** Replace a lower priority package in the queue with 'mp' (provided there are lower pri packages). Return true if replaced. + */ + bool replaceLowerPriorityPacket(meshtastic_MeshPacket *mp); -public: - explicit MeshPacketQueue(size_t _maxLen); + public: + explicit MeshPacketQueue(size_t _maxLen); - /** enqueue a packet, return false if full - * @param dropped Optional pointer to a bool that will be set to true if a packet was dropped - */ - bool enqueue(meshtastic_MeshPacket *p, bool *dropped = nullptr); + /** enqueue a packet, return false if full + * @param dropped Optional pointer to a bool that will be set to true if a packet was dropped + */ + bool enqueue(meshtastic_MeshPacket *p, bool *dropped = nullptr); - /** return true if the queue is empty */ - bool empty(); + /** return true if the queue is empty */ + bool empty(); - /** return amount of free packets in Queue */ - size_t getFree() { return maxLen - queue.size(); } + /** return amount of free packets in Queue */ + size_t getFree() { return maxLen - queue.size(); } - /** return total size of the Queue */ - size_t getMaxLen() { return maxLen; } + /** return total size of the Queue */ + size_t getMaxLen() { return maxLen; } - meshtastic_MeshPacket *dequeue(); + meshtastic_MeshPacket *dequeue(); - meshtastic_MeshPacket *getFront(); + meshtastic_MeshPacket *getFront(); - /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ - meshtastic_MeshPacket *getPacketFromQueue(NodeNum from, PacketId id); + /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ + meshtastic_MeshPacket *getPacketFromQueue(NodeNum from, PacketId id); - /** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ - meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true, uint8_t hop_limit_lt = 0); + /** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ + meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true, + uint8_t hop_limit_lt = 0); - /* Attempt to find a packet from this queue. Return true if it was found. */ - bool find(const NodeNum from, const PacketId id); + /* Attempt to find a packet from this queue. Return true if it was found. */ + bool find(const NodeNum from, const PacketId id); }; \ No newline at end of file diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index ce03fce12..f2514eea1 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -7,16 +7,16 @@ // Map from old region names to new region enums struct RegionInfo { - meshtastic_Config_LoRaConfig_RegionCode code; - float freqStart; - float freqEnd; - float dutyCycle; - float spacing; - uint8_t powerLimit; // Or zero for not set - bool audioPermitted; - bool freqSwitching; - bool wideLora; - const char *name; // EU433 etc + meshtastic_Config_LoRaConfig_RegionCode code; + float freqStart; + float freqEnd; + float dutyCycle; + float spacing; + uint8_t powerLimit; // Or zero for not set + bool audioPermitted; + bool freqSwitching; + bool wideLora; + const char *name; // EU433 etc }; extern const RegionInfo regions[]; diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 1e01a1ae0..c1b3839bb 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -28,22 +28,22 @@ #endif /* -receivedPacketQueue - this is a queue of messages we've received from the mesh, which we are keeping to deliver to the -phone. It is implemented with a FreeRTos queue (wrapped with a little RTQueue class) of pointers to MeshPacket protobufs -(which were alloced with new). After a packet ptr is removed from the queue and processed it should be deleted. -(eventually we should move sent packets into a 'sentToPhone' queue of packets we can delete just as soon as we are sure -the phone has acked those packets - when the phone writes to FromNum) +receivedPacketQueue - this is a queue of messages we've received from the mesh, which we are keeping to deliver to the phone. +It is implemented with a FreeRTos queue (wrapped with a little RTQueue class) of pointers to MeshPacket protobufs (which were +alloced with new). After a packet ptr is removed from the queue and processed it should be deleted. (eventually we should move +sent packets into a 'sentToPhone' queue of packets we can delete just as soon as we are sure the phone has acked those packets - +when the phone writes to FromNum) -mesh - an instance of Mesh class. Which manages the interface to the mesh radio library, reception of packets from -other nodes, arbitrating to select a node number and keeping the current nodedb. +mesh - an instance of Mesh class. Which manages the interface to the mesh radio library, reception of packets from other nodes, +arbitrating to select a node number and keeping the current nodedb. */ /* Broadcast when a newly powered mesh node wants to find a node num it can use The algorithm is as follows: -* when a node starts up, it broadcasts their user and the normal flow is for all other nodes to reply with their User as -well (so the new node can build its node db) +* when a node starts up, it broadcasts their user and the normal flow is for all other nodes to reply with their User as well (so +the new node can build its node db) */ MeshService *service; @@ -67,372 +67,398 @@ Allocator &queueStatusPool = staticQueueStatusPool; MeshService::MeshService() #ifdef ARCH_PORTDUINO - : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), - toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) + : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), + toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) #endif { - lastQueueStatus = {0, 0, 16, 0}; + lastQueueStatus = {0, 0, 16, 0}; } -void MeshService::init() { +void MeshService::init() +{ #if HAS_GPS - if (gps) - gpsObserver.observe(&gps->newStatus); + if (gps) + gpsObserver.observe(&gps->newStatus); #endif } -int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) { - powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping +int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) +{ + powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping - nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio - bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; - if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && - mp->decoded.request_id > 0) { - LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); - // ignore our request for its NodeInfo - } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && nodeInfoModule && - !isPreferredRebroadcaster && !nodeDB->isFull()) { - if (airTime->isTxAllowedChannelUtil(true)) { - const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); - if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { - LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); - } else { - LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); - nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel); - } - } else { - LOG_DEBUG("Skip sending NodeInfo > 25%% ch. util"); + nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio + bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { + LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); + // ignore our request for its NodeInfo + } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && + nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { + if (airTime->isTxAllowedChannelUtil(true)) { + const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); + if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { + LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); + } else { + LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); + nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel); + } + } else { + LOG_DEBUG("Skip sending NodeInfo > 25%% ch. util"); + } } - } - printPacket("Forwarding to phone", mp); - sendToPhone(packetPool.allocCopy(*mp)); + printPacket("Forwarding to phone", mp); + sendToPhone(packetPool.allocCopy(*mp)); - return 0; + return 0; } /// Do idle processing (mostly processing messages which have been queued from the radio) -void MeshService::loop() { - if (lastQueueStatus.free == 0) { // check if there is now free space in TX queue - meshtastic_QueueStatus qs = router->getQueueStatus(); - if (qs.free != lastQueueStatus.free) - (void)sendQueueStatusToPhone(qs, 0, 0); - } - if (oldFromNum != fromNum) { // We don't want to generate extra notifies for multiple new packets - int result = fromNumChanged.notifyObservers(fromNum); - if (result == 0) // If any observer returns non-zero, we will try again - oldFromNum = fromNum; - } +void MeshService::loop() +{ + if (lastQueueStatus.free == 0) { // check if there is now free space in TX queue + meshtastic_QueueStatus qs = router->getQueueStatus(); + if (qs.free != lastQueueStatus.free) + (void)sendQueueStatusToPhone(qs, 0, 0); + } + if (oldFromNum != fromNum) { // We don't want to generate extra notifies for multiple new packets + int result = fromNumChanged.notifyObservers(fromNum); + if (result == 0) // If any observer returns non-zero, we will try again + oldFromNum = fromNum; + } } /// The radioConfig object just changed, call this to force the hw to change to the new settings -void MeshService::reloadConfig(int saveWhat) { - // If we can successfully set this radio to these settings, save them to disk +void MeshService::reloadConfig(int saveWhat) +{ + // If we can successfully set this radio to these settings, save them to disk - // This will also update the region as needed - nodeDB->resetRadioConfig(); // Don't let the phone send us fatally bad settings + // This will also update the region as needed + nodeDB->resetRadioConfig(); // Don't let the phone send us fatally bad settings - configChanged.notifyObservers(NULL); // This will cause radio hardware to change freqs etc - nodeDB->saveToDisk(saveWhat); + configChanged.notifyObservers(NULL); // This will cause radio hardware to change freqs etc + nodeDB->saveToDisk(saveWhat); } /// The owner User record just got updated, update our node DB and broadcast the info into the mesh -void MeshService::reloadOwner(bool shouldSave) { - // LOG_DEBUG("reloadOwner()"); - // update our local data directly - nodeDB->updateUser(nodeDB->getNodeNum(), owner); - assert(nodeInfoModule); - // update everyone else and save to disk - if (nodeInfoModule && shouldSave) { - nodeInfoModule->sendOurNodeInfo(); - } +void MeshService::reloadOwner(bool shouldSave) +{ + // LOG_DEBUG("reloadOwner()"); + // update our local data directly + nodeDB->updateUser(nodeDB->getNodeNum(), owner); + assert(nodeInfoModule); + // update everyone else and save to disk + if (nodeInfoModule && shouldSave) { + nodeInfoModule->sendOurNodeInfo(); + } } // search the queue for a request id and return the matching nodenum -NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id) { - NodeNum nodenum = 0; - for (int i = 0; i < toPhoneQueue.numUsed(); i++) { - meshtastic_MeshPacket *p = toPhoneQueue.dequeuePtr(0); - if (p->id == request_id) { - nodenum = p->to; - // make sure to continue this to make one full loop +NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id) +{ + NodeNum nodenum = 0; + for (int i = 0; i < toPhoneQueue.numUsed(); i++) { + meshtastic_MeshPacket *p = toPhoneQueue.dequeuePtr(0); + if (p->id == request_id) { + nodenum = p->to; + // make sure to continue this to make one full loop + } + // put it right back on the queue + toPhoneQueue.enqueue(p, 0); } - // put it right back on the queue - toPhoneQueue.enqueue(p, 0); - } - return nodenum; + return nodenum; } /** * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) - * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can - * not keep a reference + * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep a + * reference */ -void MeshService::handleToRadio(meshtastic_MeshPacket &p) { +void MeshService::handleToRadio(meshtastic_MeshPacket &p) +{ #if defined(ARCH_PORTDUINO) - if (SimRadio::instance && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) { - // Simulates device received a packet via the LoRa chip - SimRadio::instance->unpackAndReceive(p); - return; - } + if (SimRadio::instance && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) { + // Simulates device received a packet via the LoRa chip + SimRadio::instance->unpackAndReceive(p); + return; + } #endif - p.from = 0; // We don't let clients assign nodenums to their sent messages - p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages - p.relay_node = NO_RELAY_NODE; // We don't let clients assign relay_node to their sent messages + p.from = 0; // We don't let clients assign nodenums to their sent messages + p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages + p.relay_node = NO_RELAY_NODE; // We don't let clients assign relay_node to their sent messages - if (p.id == 0) - p.id = generatePacketId(); // If the phone didn't supply one, then pick one + if (p.id == 0) + p.id = generatePacketId(); // If the phone didn't supply one, then pick one - p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone + p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone - IF_SCREEN( - if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST && p.to != 0) // DM only - { - perhapsDecode(&p); - const StoredMessage &sm = messageStore.addFromPacket(p); - graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI - }) - // Send the packet into the mesh - DEBUG_HEAP_BEFORE; - auto a = packetPool.allocCopy(p); - DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); - sendToMesh(a, RX_SRC_USER); - - bool loopback = false; // if true send any packet the phone sends back itself (for testing) - if (loopback) { - // no need to copy anymore because handle from radio assumes it should _not_ delete - // packetPool.allocCopy(r.variant.packet); - handleFromRadio(&p); - // handleFromRadio will tell the phone a new packet arrived - } -} - -/** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could - * cancel */ -bool MeshService::cancelSending(PacketId id) { return router->cancelSending(nodeDB->getNodeNum(), id); } - -ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id) { - meshtastic_QueueStatus *copied = queueStatusPool.allocCopy(qs); - - copied->res = res; - copied->mesh_packet_id = mesh_packet_id; - - if (toPhoneQueueStatusQueue.numFree() == 0) { - LOG_INFO("tophone queue status queue is full, discard oldest"); - meshtastic_QueueStatus *d = toPhoneQueueStatusQueue.dequeuePtr(0); - if (d) - releaseQueueStatusToPool(d); - } - - lastQueueStatus = *copied; - - res = toPhoneQueueStatusQueue.enqueue(copied, 0); - fromNum++; - - return res ? ERRNO_OK : ERRNO_UNKNOWN; -} - -void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPhone) { - uint32_t mesh_packet_id = p->id; - nodeDB->updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...) - - // Note: We might return !OK if our fifo was full, at that point the only option we have is to drop it - ErrorCode res = router->sendLocal(p, src); - - /* NOTE(pboldin): Prepare and send QueueStatus message to the phone as a - * high-priority message. */ - meshtastic_QueueStatus qs = router->getQueueStatus(); - ErrorCode r = sendQueueStatusToPhone(qs, res, mesh_packet_id); - if (r != ERRNO_OK) { - LOG_DEBUG("Can't send status to phone"); - } - - if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent + IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && + p.to != NODENUM_BROADCAST && p.to != 0) // DM only + { + perhapsDecode(&p); + const StoredMessage &sm = messageStore.addFromPacket(p); + graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI + }) + // Send the packet into the mesh DEBUG_HEAP_BEFORE; - auto a = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); + auto a = packetPool.allocCopy(p); + DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); + sendToMesh(a, RX_SRC_USER); - sendToPhone(a); - } - - // Router may ask us to release the packet if it wasn't sent - if (res == ERRNO_SHOULD_RELEASE) { - releaseToPool(p); - } + bool loopback = false; // if true send any packet the phone sends back itself (for testing) + if (loopback) { + // no need to copy anymore because handle from radio assumes it should _not_ delete + // packetPool.allocCopy(r.variant.packet); + handleFromRadio(&p); + // handleFromRadio will tell the phone a new packet arrived + } } -bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); +/** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could cancel */ +bool MeshService::cancelSending(PacketId id) +{ + return router->cancelSending(nodeDB->getNodeNum(), id); +} - assert(node); +ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id) +{ + meshtastic_QueueStatus *copied = queueStatusPool.allocCopy(qs); - if (nodeDB->hasValidPosition(node)) { + copied->res = res; + copied->mesh_packet_id = mesh_packet_id; + + if (toPhoneQueueStatusQueue.numFree() == 0) { + LOG_INFO("tophone queue status queue is full, discard oldest"); + meshtastic_QueueStatus *d = toPhoneQueueStatusQueue.dequeuePtr(0); + if (d) + releaseQueueStatusToPool(d); + } + + lastQueueStatus = *copied; + + res = toPhoneQueueStatusQueue.enqueue(copied, 0); + fromNum++; + + return res ? ERRNO_OK : ERRNO_UNKNOWN; +} + +void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPhone) +{ + uint32_t mesh_packet_id = p->id; + nodeDB->updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...) + + // Note: We might return !OK if our fifo was full, at that point the only option we have is to drop it + ErrorCode res = router->sendLocal(p, src); + + /* NOTE(pboldin): Prepare and send QueueStatus message to the phone as a + * high-priority message. */ + meshtastic_QueueStatus qs = router->getQueueStatus(); + ErrorCode r = sendQueueStatusToPhone(qs, res, mesh_packet_id); + if (r != ERRNO_OK) { + LOG_DEBUG("Can't send status to phone"); + } + + if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent + DEBUG_HEAP_BEFORE; + auto a = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); + + sendToPhone(a); + } + + // Router may ask us to release the packet if it wasn't sent + if (res == ERRNO_SHOULD_RELEASE) { + releaseToPool(p); + } +} + +bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) +{ + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + assert(node); + + if (nodeDB->hasValidPosition(node)) { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS - if (positionModule) { - if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { - LOG_DEBUG("Skip position ping; no fresh position since boot"); - return false; - } - LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); - positionModule->sendOurPosition(dest, wantReplies, node->channel); - return true; - } - } else { + if (positionModule) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position ping; no fresh position since boot"); + return false; + } + LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); + positionModule->sendOurPosition(dest, wantReplies, node->channel); + return true; + } + } else { #endif - if (nodeInfoModule) { - LOG_INFO("Send nodeinfo ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); - nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel); + if (nodeInfoModule) { + LOG_INFO("Send nodeinfo ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); + nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel); + } } - } - return false; + return false; } -void MeshService::sendToPhone(meshtastic_MeshPacket *p) { - perhapsDecode(p); +void MeshService::sendToPhone(meshtastic_MeshPacket *p) +{ + perhapsDecode(p); #ifdef ARCH_ESP32 #if !MESHTASTIC_EXCLUDE_STOREFORWARD - if (moduleConfig.store_forward.enabled && storeForwardModule->isServer() && p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - releaseToPool(p); // Copy is already stored in StoreForward history - fromNum++; // Notify observers for packet from radio - return; - } -#endif -#endif - - if (toPhoneQueue.numFree() == 0) { - if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { - LOG_WARN("ToPhone queue is full, discard oldest"); - meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); - if (d) - releaseToPool(d); - } else { - LOG_WARN("ToPhone queue is full, drop packet"); - releaseToPool(p); - fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets - return; + if (moduleConfig.store_forward.enabled && storeForwardModule->isServer() && + p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { + releaseToPool(p); // Copy is already stored in StoreForward history + fromNum++; // Notify observers for packet from radio + return; } - } +#endif +#endif - if (toPhoneQueue.enqueue(p, 0) == false) { - LOG_CRIT("Failed to queue a packet into toPhoneQueue!"); - abort(); - } - fromNum++; + if (toPhoneQueue.numFree() == 0) { + if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || + p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { + LOG_WARN("ToPhone queue is full, discard oldest"); + meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); + if (d) + releaseToPool(d); + } else { + LOG_WARN("ToPhone queue is full, drop packet"); + releaseToPool(p); + fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets + return; + } + } + + if (toPhoneQueue.enqueue(p, 0) == false) { + LOG_CRIT("Failed to queue a packet into toPhoneQueue!"); + abort(); + } + fromNum++; } -void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) { - LOG_DEBUG("Send mqtt message on topic '%s' to client for proxy", m->topic); - if (toPhoneMqttProxyQueue.numFree() == 0) { - LOG_WARN("MqttClientProxyMessagePool queue is full, discard oldest"); - meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); - if (d) - releaseMqttClientProxyMessageToPool(d); - } +void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) +{ + LOG_DEBUG("Send mqtt message on topic '%s' to client for proxy", m->topic); + if (toPhoneMqttProxyQueue.numFree() == 0) { + LOG_WARN("MqttClientProxyMessagePool queue is full, discard oldest"); + meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); + if (d) + releaseMqttClientProxyMessageToPool(d); + } - if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) { - LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!"); - abort(); - } - fromNum++; + if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) { + LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!"); + abort(); + } + fromNum++; } -void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp) { - if (!mp) { - LOG_WARN("Cannot send routing error response: null packet"); - return; - } +void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp) +{ + if (!mp) { + LOG_WARN("Cannot send routing error response: null packet"); + return; + } - // Use the routing module to send the error response - if (routingModule) { - routingModule->sendAckNak(error, mp->from, mp->id, mp->channel); - } else { - LOG_ERROR("Cannot send routing error response: no routing module"); - } + // Use the routing module to send the error response + if (routingModule) { + routingModule->sendAckNak(error, mp->from, mp->id, mp->channel); + } else { + LOG_ERROR("Cannot send routing error response: no routing module"); + } } -void MeshService::sendClientNotification(meshtastic_ClientNotification *n) { - LOG_DEBUG("Send client notification to phone"); - if (toPhoneClientNotificationQueue.numFree() == 0) { - LOG_WARN("ClientNotification queue is full, discard oldest"); - meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0); - if (d) - releaseClientNotificationToPool(d); - } +void MeshService::sendClientNotification(meshtastic_ClientNotification *n) +{ + LOG_DEBUG("Send client notification to phone"); + if (toPhoneClientNotificationQueue.numFree() == 0) { + LOG_WARN("ClientNotification queue is full, discard oldest"); + meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0); + if (d) + releaseClientNotificationToPool(d); + } - if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) { - LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!"); - abort(); - } - fromNum++; + if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) { + LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!"); + abort(); + } + fromNum++; } -meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - assert(node); +meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() +{ + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + assert(node); - // We might not have a position yet for our local node, in that case, at least try to send the time - if (!node->has_position) { - memset(&node->position, 0, sizeof(node->position)); - node->has_position = true; - } + // We might not have a position yet for our local node, in that case, at least try to send the time + if (!node->has_position) { + memset(&node->position, 0, sizeof(node->position)); + node->has_position = true; + } - meshtastic_PositionLite &position = node->position; + meshtastic_PositionLite &position = node->position; - // Update our local node info with our time (even if we don't decide to update anyone else) - node->last_heard = getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid + // Update our local node info with our time (even if we don't decide to update anyone else) + node->last_heard = + getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid - position.time = getValidTime(RTCQualityFromNet); + position.time = getValidTime(RTCQualityFromNet); - if (powerStatus->getHasBattery() == 1) { - updateBatteryLevel(powerStatus->getBatteryChargePercent()); - } + if (powerStatus->getHasBattery() == 1) { + updateBatteryLevel(powerStatus->getBatteryChargePercent()); + } - return node; + return node; } #if HAS_GPS -int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus) { - // Update our local node info with our position (even if we don't decide to update anyone else) - const meshtastic_NodeInfoLite *node = refreshLocalMeshNode(); - meshtastic_Position pos = meshtastic_Position_init_default; +int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus) +{ + // Update our local node info with our position (even if we don't decide to update anyone else) + const meshtastic_NodeInfoLite *node = refreshLocalMeshNode(); + meshtastic_Position pos = meshtastic_Position_init_default; - if (newStatus->getHasLock()) { - // load data from GPS object, will add timestamp + battery further down - pos = gps->p; - } else { - // The GPS has lost lock + if (newStatus->getHasLock()) { + // load data from GPS object, will add timestamp + battery further down + pos = gps->p; + } else { + // The GPS has lost lock #ifdef GPS_DEBUG - LOG_DEBUG("onGPSchanged() - lost validLocation"); + LOG_DEBUG("onGPSchanged() - lost validLocation"); #endif - } - // Used fixed position if configured regardless of GPS lock - if (config.position.fixed_position) { - LOG_WARN("Use fixed position"); - pos = TypeConversions::ConvertToPosition(node->position); - } + } + // Used fixed position if configured regardless of GPS lock + if (config.position.fixed_position) { + LOG_WARN("Use fixed position"); + pos = TypeConversions::ConvertToPosition(node->position); + } - // Add a fresh timestamp - pos.time = getValidTime(RTCQualityFromNet); + // Add a fresh timestamp + pos.time = getValidTime(RTCQualityFromNet); - // In debug logs, identify position by @timestamp:stage (stage 4 = nodeDB) - LOG_DEBUG("onGPSChanged() pos@%x time=%u lat=%d lon=%d alt=%d", pos.timestamp, pos.time, pos.latitude_i, pos.longitude_i, pos.altitude); + // In debug logs, identify position by @timestamp:stage (stage 4 = nodeDB) + LOG_DEBUG("onGPSChanged() pos@%x time=%u lat=%d lon=%d alt=%d", pos.timestamp, pos.time, pos.latitude_i, pos.longitude_i, + pos.altitude); - // Update our current position in the local DB - nodeDB->updatePosition(nodeDB->getNodeNum(), pos, RX_SRC_LOCAL); + // Update our current position in the local DB + nodeDB->updatePosition(nodeDB->getNodeNum(), pos, RX_SRC_LOCAL); - return 0; + return 0; } #endif -bool MeshService::isToPhoneQueueEmpty() { return toPhoneQueue.isEmpty(); } - -uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp) { - uint32_t now = getTime(); - - uint32_t last_seen = mp->rx_time; - int delta = (int)(now - last_seen); - if (delta < 0) // our clock must be slightly off still - not set from GPS yet - delta = 0; - - return delta; +bool MeshService::isToPhoneQueueEmpty() +{ + return toPhoneQueue.isEmpty(); +} + +uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp) +{ + uint32_t now = getTime(); + + uint32_t last_seen = mp->rx_time; + int delta = (int)(now - last_seen); + if (delta < 0) // our clock must be slightly off still - not set from GPS yet + delta = 0; + + return delta; } diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 2ad33388d..71fb544a0 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -32,171 +32,174 @@ extern Allocator &clientNotificationPool; * Top level app for this service. keeps the mesh, the radio config and the queue of received packets. * */ -class MeshService { +class MeshService +{ #if HAS_GPS - CallbackObserver gpsObserver = - CallbackObserver(this, &MeshService::onGPSChanged); + CallbackObserver gpsObserver = + CallbackObserver(this, &MeshService::onGPSChanged); #endif - /// received packets waiting for the phone to process them - /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure - /// we never hang because android hasn't been there in a while - /// FIXME - save this to flash on deep sleep + /// received packets waiting for the phone to process them + /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure + /// we never hang because android hasn't been there in a while + /// FIXME - save this to flash on deep sleep #ifdef ARCH_PORTDUINO - PointerQueue toPhoneQueue; + PointerQueue toPhoneQueue; #else - StaticPointerQueue toPhoneQueue; + StaticPointerQueue toPhoneQueue; #endif - // keep list of QueueStatus packets to be send to the phone + // keep list of QueueStatus packets to be send to the phone #ifdef ARCH_PORTDUINO - PointerQueue toPhoneQueueStatusQueue; + PointerQueue toPhoneQueueStatusQueue; #else - StaticPointerQueue toPhoneQueueStatusQueue; + StaticPointerQueue toPhoneQueueStatusQueue; #endif - // keep list of MqttClientProxyMessages to be send to the client for delivery + // keep list of MqttClientProxyMessages to be send to the client for delivery #ifdef ARCH_PORTDUINO - PointerQueue toPhoneMqttProxyQueue; + PointerQueue toPhoneMqttProxyQueue; #else - StaticPointerQueue toPhoneMqttProxyQueue; + StaticPointerQueue toPhoneMqttProxyQueue; #endif - // keep list of ClientNotifications to be send to the client (phone) + // keep list of ClientNotifications to be send to the client (phone) #ifdef ARCH_PORTDUINO - PointerQueue toPhoneClientNotificationQueue; + PointerQueue toPhoneClientNotificationQueue; #else - StaticPointerQueue toPhoneClientNotificationQueue; + StaticPointerQueue toPhoneClientNotificationQueue; #endif - // This holds the last QueueStatus send - meshtastic_QueueStatus lastQueueStatus; + // This holds the last QueueStatus send + meshtastic_QueueStatus lastQueueStatus; - /// The current nonce for the newest packet which has been queued for the phone - uint32_t fromNum = 0; + /// The current nonce for the newest packet which has been queued for the phone + uint32_t fromNum = 0; - /// Updated in loop() to detect when fromNum changes - uint32_t oldFromNum = 0; + /// Updated in loop() to detect when fromNum changes + uint32_t oldFromNum = 0; -public: - enum APIState { - STATE_DISCONNECTED, // Initial state, no API is connected - STATE_BLE, - STATE_WIFI, - STATE_SERIAL, - STATE_PACKET, - STATE_HTTP, - STATE_ETH - }; + public: + enum APIState { + STATE_DISCONNECTED, // Initial state, no API is connected + STATE_BLE, + STATE_WIFI, + STATE_SERIAL, + STATE_PACKET, + STATE_HTTP, + STATE_ETH + }; - APIState api_state = STATE_DISCONNECTED; + APIState api_state = STATE_DISCONNECTED; - static bool isTextPayload(const meshtastic_MeshPacket *p) { - if (moduleConfig.range_test.enabled && p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { - return true; + static bool isTextPayload(const meshtastic_MeshPacket *p) + { + if (moduleConfig.range_test.enabled && p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { + return true; + } + return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || + p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || + p->decoded.portnum == meshtastic_PortNum_ALERT_APP; } - return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || - p->decoded.portnum == meshtastic_PortNum_ALERT_APP; - } - /// Called when some new packets have arrived from one of the radios - Observable fromNumChanged; + /// Called when some new packets have arrived from one of the radios + Observable fromNumChanged; - /// Called when radio config has changed (radios should observe this and set their hardware as required) - Observable configChanged; + /// Called when radio config has changed (radios should observe this and set their hardware as required) + Observable configChanged; - MeshService(); + MeshService(); - void init(); + void init(); - /// Do idle processing (mostly processing messages which have been queued from the radio) - void loop(); + /// Do idle processing (mostly processing messages which have been queued from the radio) + void loop(); - /// Return the next packet destined to the phone. FIXME, somehow use fromNum to allow the phone to retry the - /// last few packets if needs to. - meshtastic_MeshPacket *getForPhone() { return toPhoneQueue.dequeuePtr(0); } + /// Return the next packet destined to the phone. FIXME, somehow use fromNum to allow the phone to retry the + /// last few packets if needs to. + meshtastic_MeshPacket *getForPhone() { return toPhoneQueue.dequeuePtr(0); } - /// Allows the bluetooth handler to free packets after they have been sent - void releaseToPool(meshtastic_MeshPacket *p) { packetPool.release(p); } + /// Allows the bluetooth handler to free packets after they have been sent + void releaseToPool(meshtastic_MeshPacket *p) { packetPool.release(p); } - /// Return the next QueueStatus packet destined to the phone. - meshtastic_QueueStatus *getQueueStatusForPhone() { return toPhoneQueueStatusQueue.dequeuePtr(0); } + /// Return the next QueueStatus packet destined to the phone. + meshtastic_QueueStatus *getQueueStatusForPhone() { return toPhoneQueueStatusQueue.dequeuePtr(0); } - /// Return the next MqttClientProxyMessage packet destined to the phone. - meshtastic_MqttClientProxyMessage *getMqttClientProxyMessageForPhone() { return toPhoneMqttProxyQueue.dequeuePtr(0); } + /// Return the next MqttClientProxyMessage packet destined to the phone. + meshtastic_MqttClientProxyMessage *getMqttClientProxyMessageForPhone() { return toPhoneMqttProxyQueue.dequeuePtr(0); } - /// Return the next ClientNotification packet destined to the phone. - meshtastic_ClientNotification *getClientNotificationForPhone() { return toPhoneClientNotificationQueue.dequeuePtr(0); } + /// Return the next ClientNotification packet destined to the phone. + meshtastic_ClientNotification *getClientNotificationForPhone() { return toPhoneClientNotificationQueue.dequeuePtr(0); } - // search the queue for a request id and return the matching nodenum - NodeNum getNodenumFromRequestId(uint32_t request_id); + // search the queue for a request id and return the matching nodenum + NodeNum getNodenumFromRequestId(uint32_t request_id); - // Release QueueStatus packet to pool - void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); } + // Release QueueStatus packet to pool + void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); } - // Release MqttClientProxyMessage packet to pool - void releaseMqttClientProxyMessageToPool(meshtastic_MqttClientProxyMessage *p) { mqttClientProxyMessagePool.release(p); } + // Release MqttClientProxyMessage packet to pool + void releaseMqttClientProxyMessageToPool(meshtastic_MqttClientProxyMessage *p) { mqttClientProxyMessagePool.release(p); } - /// Release the next ClientNotification packet to pool. - void releaseClientNotificationToPool(meshtastic_ClientNotification *p) { clientNotificationPool.release(p); } + /// Release the next ClientNotification packet to pool. + void releaseClientNotificationToPool(meshtastic_ClientNotification *p) { clientNotificationPool.release(p); } - /** - * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) - * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can - * not keep a reference - */ - void handleToRadio(meshtastic_MeshPacket &p); + /** + * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) + * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep + * a reference + */ + void handleToRadio(meshtastic_MeshPacket &p); - /** The radioConfig object just changed, call this to force the hw to change to the new settings - * @return true if client devices should be sent a new set of radio configs - */ - void reloadConfig(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + /** The radioConfig object just changed, call this to force the hw to change to the new settings + * @return true if client devices should be sent a new set of radio configs + */ + void reloadConfig(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); - /// The owner User record just got updated, update our node DB and broadcast the info into the mesh - void reloadOwner(bool shouldSave = true); + /// The owner User record just got updated, update our node DB and broadcast the info into the mesh + void reloadOwner(bool shouldSave = true); - /// Called when the user wakes up our GUI, normally sends our latest location to the mesh (if we have it), otherwise - /// at least sends our nodeinfo returns true if we sent a position - bool trySendPosition(NodeNum dest, bool wantReplies = false); + /// Called when the user wakes up our GUI, normally sends our latest location to the mesh (if we have it), otherwise at least + /// sends our nodeinfo + /// returns true if we sent a position + bool trySendPosition(NodeNum dest, bool wantReplies = false); - /// Send a packet into the mesh - note p must have been allocated from packetPool. We will return it to that pool - /// after sending. This is the ONLY function you should use for sending messages into the mesh, because it also - /// updates the nodedb cache - void sendToMesh(meshtastic_MeshPacket *p, RxSource src = RX_SRC_LOCAL, bool ccToPhone = false); + /// Send a packet into the mesh - note p must have been allocated from packetPool. We will return it to that pool after + /// sending. This is the ONLY function you should use for sending messages into the mesh, because it also updates the nodedb + /// cache + void sendToMesh(meshtastic_MeshPacket *p, RxSource src = RX_SRC_LOCAL, bool ccToPhone = false); - /** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could - * cancel */ - bool cancelSending(PacketId id); + /** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could cancel */ + bool cancelSending(PacketId id); - /// Pull the latest power and time info into my nodeinfo - meshtastic_NodeInfoLite *refreshLocalMeshNode(); + /// Pull the latest power and time info into my nodeinfo + meshtastic_NodeInfoLite *refreshLocalMeshNode(); - /// Send a packet to the phone - void sendToPhone(meshtastic_MeshPacket *p); + /// Send a packet to the phone + void sendToPhone(meshtastic_MeshPacket *p); - /// Send an MQTT message to the phone for client proxying - virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); + /// Send an MQTT message to the phone for client proxying + virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); - /// Send a ClientNotification to the phone - virtual void sendClientNotification(meshtastic_ClientNotification *cn); + /// Send a ClientNotification to the phone + virtual void sendClientNotification(meshtastic_ClientNotification *cn); - /// Send an error response to the phone - void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); + /// Send an error response to the phone + void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); - bool isToPhoneQueueEmpty(); + bool isToPhoneQueueEmpty(); - ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id); + ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id); - uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp); + uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp); -private: + private: #if HAS_GPS - /// Called when our gps position has changed - updates nodedb and sends Location message out into the mesh - /// returns 0 to allow further processing - int onGPSChanged(const meshtastic::GPSStatus *arg); + /// Called when our gps position has changed - updates nodedb and sends Location message out into the mesh + /// returns 0 to allow further processing + int onGPSChanged(const meshtastic::GPSStatus *arg); #endif - /// Handle a packet that just arrived from the radio. This method does _not_ free the provided packet. If it - /// needs to keep the packet around it makes a copy - int handleFromRadio(const meshtastic_MeshPacket *p); - friend class RoutingModule; + /// Handle a packet that just arrived from the radio. This method does _not_ free the provided packet. If it + /// needs to keep the packet around it makes a copy + int handleFromRadio(const meshtastic_MeshPacket *p); + friend class RoutingModule; }; extern MeshService *service; diff --git a/src/mesh/MeshTypes.h b/src/mesh/MeshTypes.h index 0112f1149..680926d3c 100644 --- a/src/mesh/MeshTypes.h +++ b/src/mesh/MeshTypes.h @@ -10,9 +10,8 @@ typedef uint32_t NodeNum; typedef uint32_t PacketId; // A packet sequence number #define NODENUM_BROADCAST UINT32_MAX -#define NODENUM_BROADCAST_NO_LORA \ - 1 // Reserved to only deliver packets over high speed (non-lora) transports, such as MQTT or BLE mesh (not yet - // implemented) +#define NODENUM_BROADCAST_NO_LORA \ + 1 // Reserved to only deliver packets over high speed (non-lora) transports, such as MQTT or BLE mesh (not yet implemented) #define ERRNO_OK 0 #define ERRNO_NO_INTERFACES 33 #define ERRNO_UNKNOWN 32 // pick something that doesn't conflict with RH_ROUTER_ERROR_UNABLE_TO_DELIVER @@ -24,17 +23,17 @@ typedef uint32_t PacketId; // A packet sequence number * Source of a received message */ enum RxSource { - RX_SRC_LOCAL, // message was generated locally - RX_SRC_RADIO, // message was received from radio mesh - RX_SRC_USER // message was received from end-user device + RX_SRC_LOCAL, // message was generated locally + RX_SRC_RADIO, // message was received from radio mesh + RX_SRC_USER // message was received from end-user device }; /** * the max number of hops a message can pass through, used as the default max for hop_limit in MeshPacket. * - * We reserve 3 bits in the header so this could be up to 7, but given the high range of lora and typical usecases, - *keeping maxhops to 3 should be fine for a while. This also serves to prevent routing/flooding attempts to be - *attempted for too long. + * We reserve 3 bits in the header so this could be up to 7, but given the high range of lora and typical usecases, keeping + * maxhops to 3 should be fine for a while. This also serves to prevent routing/flooding attempts to be attempted for + * too long. **/ #define HOP_MAX 7 @@ -53,8 +52,8 @@ extern Allocator &packetPool; using UniquePacketPoolPacket = Allocator::UniqueAllocation; /** - * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they - * originated on the local node. If from is zero this function returns our node number instead + * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on + * the local node. If from is zero this function returns our node number instead */ NodeNum getFrom(const meshtastic_MeshPacket *p); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index c26e7f6fa..5230e5b85 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -8,315 +8,331 @@ NextHopRouter::NextHopRouter() {} -PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions) { - packet = p; - this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send +PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions) +{ + packet = p; + this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send } /** * Send a packet */ -ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) { - // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) - p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us - wasSeenRecently(p); // FIXME, move this to a sniffSent method +ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) +{ + // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us + wasSeenRecently(p); // FIXME, move this to a sniffSent method - p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop - LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); + p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop + LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); - // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop - // limit is not 0 or want_ack is set, start retransmissions - if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) - startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet + // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is + // not 0 or want_ack is set, start retransmissions + if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) + startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet - return Router::send(p); + return Router::send(p); } -bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { - bool wasFallback = false; - bool weWereNextHop = false; - bool wasUpgraded = false; - bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop, - &wasUpgraded); // Updates history; returns false when an upgrade is detected +bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) +{ + bool wasFallback = false; + bool weWereNextHop = false; + bool wasUpgraded = false; + bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop, + &wasUpgraded); // Updates history; returns false when an upgrade is detected - // Handle hop_limit upgrade scenario for rebroadcasters - if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { - return true; // we handled it, so stop processing - } - - if (seenRecently) { - printPacket("Ignore dupe incoming msg", p); - - if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { - rxDupe++; - stopRetransmission(p->from, p->id); + // Handle hop_limit upgrade scenario for rebroadcasters + if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { + return true; // we handled it, so stop processing } - // If it was a fallback to flooding, try to relay again - if (wasFallback) { - LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node); - // Check if it's still in the Tx queue, if not, we have to relay it again - if (!findInTxQueue(p->from, p->id)) { - reprocessPacket(p); - perhapsRebroadcast(p); - } - } else { - bool isRepeated = getHopsAway(*p) == 0; - // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again - if (isRepeated) { - if (!findInTxQueue(p->from, p->id)) { - reprocessPacket(p); - if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) { - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } + if (seenRecently) { + printPacket("Ignore dupe incoming msg", p); + + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + rxDupe++; + stopRetransmission(p->from, p->id); } - } else if (!weWereNextHop) { - perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay - } - } - return true; - } - return Router::shouldFilterReceived(p); + // If it was a fallback to flooding, try to relay again + if (wasFallback) { + LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node); + // Check if it's still in the Tx queue, if not, we have to relay it again + if (!findInTxQueue(p->from, p->id)) { + reprocessPacket(p); + perhapsRebroadcast(p); + } + } else { + bool isRepeated = getHopsAway(*p) == 0; + // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again + if (isRepeated) { + if (!findInTxQueue(p->from, p->id)) { + reprocessPacket(p); + if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) { + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } + } + } else if (!weWereNextHop) { + perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay + } + } + return true; + } + + return Router::shouldFilterReceived(p); } -void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { - NodeNum ourNodeNum = getNodeNum(); - uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum); - bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0 || p->decoded.reply_id != 0); - if (isAckorReply) { - // Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if - // "from" is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to - // the destination - if (p->from != 0) { - meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); - if (origTx) { - // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came - // directly from the destination - bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); - bool weWereSoleRelayer = false; - bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); - if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { - if (origTx->next_hop != p->relay_node) { // Not already set - LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, p->relay_node, wasAlreadyRelayer, - weWereSoleRelayer); - origTx->next_hop = p->relay_node; - } +void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) +{ + NodeNum ourNodeNum = getNodeNum(); + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum); + bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && + (p->decoded.request_id != 0 || p->decoded.reply_id != 0); + if (isAckorReply) { + // Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from" + // is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the + // destination + if (p->from != 0) { + meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); + if (origTx) { + // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came + // directly from the destination + bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); + bool weWereSoleRelayer = false; + bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); + if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { + if (origTx->next_hop != p->relay_node) { // Not already set + LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, + p->relay_node, wasAlreadyRelayer, weWereSoleRelayer); + origTx->next_hop = p->relay_node; + } + } + } + } + if (!isToUs(p)) { + Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM + // stop retransmission for the original packet + stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id } - } } - if (!isToUs(p)) { - Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM - // stop retransmission for the original packet - stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id - } - } - perhapsRebroadcast(p); + perhapsRebroadcast(p); - // handle the packet as normal - Router::sniffReceived(p, c); + // handle the packet as normal + Router::sniffReceived(p, c); } /* Check if we should be rebroadcasting this packet if so, do so. */ -bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) { - if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { - if (p->id != 0) { - if (isRebroadcaster()) { - if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { - meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it - LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); +bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) +{ + if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { + if (p->id != 0) { + if (isRebroadcaster()) { + if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { + meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it + LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); - // Use shared logic to determine if hop_limit should be decremented - if (shouldDecrementHopLimit(p)) { - tosend->hop_limit--; // bump down the hop count - } else { - LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); - } + // Use shared logic to determine if hop_limit should be decremented + if (shouldDecrementHopLimit(p)) { + tosend->hop_limit--; // bump down the hop count + } else { + LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); + } #if USERPREFS_EVENT_MODE - if (tosend->hop_limit > 2) { - // if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away. - tosend->hop_start -= (tosend->hop_limit - 2); - tosend->hop_limit = 2; - } + if (tosend->hop_limit > 2) { + // if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away. + tosend->hop_start -= (tosend->hop_limit - 2); + tosend->hop_limit = 2; + } #endif - if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { - FloodingRouter::send(tosend); - } else { - NextHopRouter::send(tosend); - } + if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { + FloodingRouter::send(tosend); + } else { + NextHopRouter::send(tosend); + } - return true; + return true; + } + } else { + LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); + } + } else { + LOG_DEBUG("Ignore 0 id broadcast"); } - } else { - LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); - } - } else { - LOG_DEBUG("Ignore 0 id broadcast"); } - } - return false; + return false; } /** * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ -uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) { - if (isBroadcast(to)) - return NO_NEXT_HOP_PREFERENCE; +uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) +{ + if (isBroadcast(to)) + return NO_NEXT_HOP_PREFERENCE; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); - if (node && node->next_hop) { - // We are careful not to return the relay node as the next hop - if (node->next_hop != relay_node) { - // LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop); - return node->next_hop; - } else - LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); - } - return NO_NEXT_HOP_PREFERENCE; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); + if (node && node->next_hop) { + // We are careful not to return the relay node as the next hop + if (node->next_hop != relay_node) { + // LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop); + return node->next_hop; + } else + LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); + } + return NO_NEXT_HOP_PREFERENCE; } -PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) { - auto old = pending.find(key); // If we have an old record, someone messed up because id got reused - if (old != pending.end()) { - return &old->second; - } else - return NULL; +PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) +{ + auto old = pending.find(key); // If we have an old record, someone messed up because id got reused + if (old != pending.end()) { + return &old->second; + } else + return NULL; } /** * Stop any retransmissions we are doing of the specified node/packet ID pair */ -bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) { - auto key = GlobalPacketId(from, id); - return stopRetransmission(key); +bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) +{ + auto key = GlobalPacketId(from, id); + return stopRetransmission(key); } -bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) { - // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) +bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) +{ + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) - // Return false for roles like ROUTER, ROUTER_LATE which should always transmit the packet at least once. + // Return false for roles like ROUTER, ROUTER_LATE which should always transmit the packet at least once. - return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe + return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe } -bool NextHopRouter::stopRetransmission(GlobalPacketId key) { - auto old = findPendingPacket(key); - if (old) { - auto p = old->packet; - /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue - to avoid canceling a transmission if it was ACKed super fast via MQTT */ - if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { - // We only cancel it if we are the original sender or if we're not a router(_late) - if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { - // remove the 'original' (identified by originator and packet->id) from the txqueue and free it - cancelSending(getFrom(p), p->id); - } - } +bool NextHopRouter::stopRetransmission(GlobalPacketId key) +{ + auto old = findPendingPacket(key); + if (old) { + auto p = old->packet; + /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue + to avoid canceling a transmission if it was ACKed super fast via MQTT */ + if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { + // We only cancel it if we are the original sender or if we're not a router(_late) + if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { + // remove the 'original' (identified by originator and packet->id) from the txqueue and free it + cancelSending(getFrom(p), p->id); + } + } - // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it - // doesn't get scheduled again. (This is the core of stopRetransmission.) - auto numErased = pending.erase(key); - assert(numErased == 1); + // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it + // doesn't get scheduled again. (This is the core of stopRetransmission.) + auto numErased = pending.erase(key); + assert(numErased == 1); - // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the - // call to startRetransmission. - packetPool.release(p); + // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the + // call to startRetransmission. + packetPool.release(p); - return true; - } else - return false; + return true; + } else + return false; } /** * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. */ -PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx) { - auto id = GlobalPacketId(p); - auto rec = PendingPacket(p, numReTx); +PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx) +{ + auto id = GlobalPacketId(p); + auto rec = PendingPacket(p, numReTx); - stopRetransmission(getFrom(p), p->id); + stopRetransmission(getFrom(p), p->id); - setNextTx(&rec); - pending[id] = rec; + setNextTx(&rec); + pending[id] = rec; - return &pending[id]; + return &pending[id]; } /** * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) */ -int32_t NextHopRouter::doRetransmissions() { - uint32_t now = millis(); - int32_t d = INT32_MAX; +int32_t NextHopRouter::doRetransmissions() +{ + uint32_t now = millis(); + int32_t d = INT32_MAX; - // FIXME, we should use a better datastructure rather than walking through this map. - // for(auto el: pending) { - for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { - ++nextIt; // we use this odd pattern because we might be deleting it... - auto &p = it->second; + // FIXME, we should use a better datastructure rather than walking through this map. + // for(auto el: pending) { + for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { + ++nextIt; // we use this odd pattern because we might be deleting it... + auto &p = it->second; - bool stillValid = true; // assume we'll keep this record around + bool stillValid = true; // assume we'll keep this record around - // FIXME, handle 51 day rolloever here!!! - if (p.nextTxMsec <= now) { - if (p.numRetransmissions == 0) { - if (isFromUs(p.packet)) { - LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, p.packet->id); - sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); - } - // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived - stopRetransmission(it->first); - stillValid = false; // just deleted it - } else { - LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, p.packet->id, p.numRetransmissions); + // FIXME, handle 51 day rolloever here!!! + if (p.nextTxMsec <= now) { + if (p.numRetransmissions == 0) { + if (isFromUs(p.packet)) { + LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, + p.packet->id); + sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); + } + // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived + stopRetransmission(it->first); + stillValid = false; // just deleted it + } else { + LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, + p.packet->id, p.numRetransmissions); - if (!isBroadcast(p.packet->to)) { - if (p.numRetransmissions == 1) { - // Last retransmission, reset next_hop (fallback to FloodingRouter) - p.packet->next_hop = NO_NEXT_HOP_PREFERENCE; - // Also reset it in the nodeDB - meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); - if (sentTo) { - LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to); - sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; + if (!isBroadcast(p.packet->to)) { + if (p.numRetransmissions == 1) { + // Last retransmission, reset next_hop (fallback to FloodingRouter) + p.packet->next_hop = NO_NEXT_HOP_PREFERENCE; + // Also reset it in the nodeDB + meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); + if (sentTo) { + LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to); + sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; + } + FloodingRouter::send(packetPool.allocCopy(*p.packet)); + } else { + NextHopRouter::send(packetPool.allocCopy(*p.packet)); + } + } else { + // Note: we call the superclass version because we don't want to have our version of send() add a new + // retransmission record + FloodingRouter::send(packetPool.allocCopy(*p.packet)); + } + + // Queue again + --p.numRetransmissions; + setNextTx(&p); } - FloodingRouter::send(packetPool.allocCopy(*p.packet)); - } else { - NextHopRouter::send(packetPool.allocCopy(*p.packet)); - } - } else { - // Note: we call the superclass version because we don't want to have our version of send() add a new - // retransmission record - FloodingRouter::send(packetPool.allocCopy(*p.packet)); } - // Queue again - --p.numRetransmissions; - setNextTx(&p); - } + if (stillValid) { + // Update our desired sleep delay + int32_t t = p.nextTxMsec - now; + + d = min(t, d); + } } - if (stillValid) { - // Update our desired sleep delay - int32_t t = p.nextTxMsec - now; - - d = min(t, d); - } - } - - return d; + return d; } -void NextHopRouter::setNextTx(PendingPacket *pending) { - assert(iface); - auto d = iface->getRetransmissionMsec(pending->packet); - pending->nextTxMsec = millis() + d; - LOG_DEBUG("Setting next retransmission in %u msecs: ", d); - printPacket("", pending->packet); - setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time +void NextHopRouter::setNextTx(PendingPacket *pending) +{ + assert(iface); + auto d = iface->getRetransmissionMsec(pending->packet); + pending->nextTxMsec = millis() + d; + LOG_DEBUG("Setting next retransmission in %u msecs: ", d); + printPacket("", pending->packet); + setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time } diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index 0fbffaa5f..c1df3596b 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -8,143 +8,147 @@ * to that message */ struct GlobalPacketId { - NodeNum node; - PacketId id; + NodeNum node; + PacketId id; - bool operator==(const GlobalPacketId &p) const { return node == p.node && id == p.id; } + bool operator==(const GlobalPacketId &p) const { return node == p.node && id == p.id; } - explicit GlobalPacketId(const meshtastic_MeshPacket *p) { - node = getFrom(p); - id = p->id; - } + explicit GlobalPacketId(const meshtastic_MeshPacket *p) + { + node = getFrom(p); + id = p->id; + } - GlobalPacketId(NodeNum _from, PacketId _id) { - node = _from; - id = _id; - } + GlobalPacketId(NodeNum _from, PacketId _id) + { + node = _from; + id = _id; + } }; /** * A packet queued for retransmission */ struct PendingPacket { - meshtastic_MeshPacket *packet; + meshtastic_MeshPacket *packet; - /** The next time we should try to retransmit this packet */ - uint32_t nextTxMsec = 0; + /** The next time we should try to retransmit this packet */ + uint32_t nextTxMsec = 0; - /** Starts at NUM_RETRANSMISSIONS -1 and counts down. Once zero it will be removed from the list */ - uint8_t numRetransmissions = 0; + /** Starts at NUM_RETRANSMISSIONS -1 and counts down. Once zero it will be removed from the list */ + uint8_t numRetransmissions = 0; - PendingPacket() {} - explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions); + PendingPacket() {} + explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions); }; -class GlobalPacketIdHashFunction { -public: - size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } +class GlobalPacketIdHashFunction +{ + public: + size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } }; /* Router for direct messages, which only relays if it is the next hop for a packet. The next hop is set by the current - relayer of a packet, which bases this on information from a previous successful delivery to the destination via - flooding. Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered - back to us via a node that also relayed the original packet, we use that node as next hop for the destination from - then on. This makes sure that only when there’s a two-way connection, we assign a next hop. Both the ReliableRouter - and NextHopRouter will do retransmissions (the NextHopRouter only 1 time). For the final retry, if no one actually - relayed the packet, it will reset the next hop in order to fall back to the FloodingRouter again. Note that thus also - intermediate hops will do a single retransmission if the intended next-hop didn’t relay, in order to fix changes in - the middle of the route. + relayer of a packet, which bases this on information from a previous successful delivery to the destination via flooding. + Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered back to us via a node + that also relayed the original packet, we use that node as next hop for the destination from then on. This makes sure that only + when there’s a two-way connection, we assign a next hop. Both the ReliableRouter and NextHopRouter will do retransmissions (the + NextHopRouter only 1 time). For the final retry, if no one actually relayed the packet, it will reset the next hop in order to + fall back to the FloodingRouter again. Note that thus also intermediate hops will do a single retransmission if the intended + next-hop didn’t relay, in order to fix changes in the middle of the route. */ -class NextHopRouter : public FloodingRouter { -public: - /** - * Constructor - * - */ - NextHopRouter(); +class NextHopRouter : public FloodingRouter +{ + public: + /** + * Constructor + * + */ + NextHopRouter(); - /** - * Send a packet - * @return an error code - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + /** + * Send a packet + * @return an error code + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - /** Do our retransmission handling */ - virtual int32_t runOnce() override { - // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation - doRetransmissions(); + /** Do our retransmission handling */ + virtual int32_t runOnce() override + { + // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation + doRetransmissions(); - int32_t r = FloodingRouter::runOnce(); + int32_t r = FloodingRouter::runOnce(); - // Also after calling runOnce there might be new packets to retransmit - auto d = doRetransmissions(); - return min(d, r); - } + // Also after calling runOnce there might be new packets to retransmit + auto d = doRetransmissions(); + return min(d, r); + } - // The number of retransmissions intermediate nodes will do (actually 1 less than this) - constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2; - // The number of retransmissions the original sender will do - constexpr static uint8_t NUM_RELIABLE_RETX = 3; + // The number of retransmissions intermediate nodes will do (actually 1 less than this) + constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2; + // The number of retransmissions the original sender will do + constexpr static uint8_t NUM_RELIABLE_RETX = 3; -protected: - /** - * Pending retransmissions - */ - std::unordered_map pending; + protected: + /** + * Pending retransmissions + */ + std::unordered_map pending; - /** - * Should this incoming filter be dropped? - * - * Called immediately on reception, before any further processing. - * @return true to abandon the packet - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + /** + * Should this incoming filter be dropped? + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; - /** - * Look for packets we need to relay - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + /** + * Look for packets we need to relay + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /** - * Try to find the pending packet record for this ID (or NULL if not found) - */ - PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } - PendingPacket *findPendingPacket(GlobalPacketId p); + /** + * Try to find the pending packet record for this ID (or NULL if not found) + */ + PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } + PendingPacket *findPendingPacket(GlobalPacketId p); - /** - * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. - */ - PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); + /** + * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. + */ + PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); - // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) - bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); - /** - * Stop any retransmissions we are doing of the specified node/packet ID pair - * - * @return true if we found and removed a transmission with this ID - */ - bool stopRetransmission(NodeNum from, PacketId id); - bool stopRetransmission(GlobalPacketId p); + /** + * Stop any retransmissions we are doing of the specified node/packet ID pair + * + * @return true if we found and removed a transmission with this ID + */ + bool stopRetransmission(NodeNum from, PacketId id); + bool stopRetransmission(GlobalPacketId p); - /** - * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) - * - * @return the number of msecs until our next retransmission or MAXINT if none scheduled - */ - int32_t doRetransmissions(); + /** + * Do any retransmissions that are scheduled (FIXME - for the time being called from loop) + * + * @return the number of msecs until our next retransmission or MAXINT if none scheduled + */ + int32_t doRetransmissions(); - void setNextTx(PendingPacket *pending); + void setNextTx(PendingPacket *pending); -private: - /** - * Get the next hop for a destination, given the relay node - * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) - */ - uint8_t getNextHop(NodeNum to, uint8_t relay_node); + private: + /** + * Get the next hop for a destination, given the relay node + * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) + */ + uint8_t getNextHop(NodeNum to, uint8_t relay_node); - /** Check if we should be rebroadcasting this packet if so, do so. - * @return true if we did rebroadcast */ - bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override; + /** Check if we should be rebroadcasting this packet if so, do so. + * @return true if we did rebroadcast */ + bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override; }; \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d6ad61fad..3c408f01f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -79,89 +79,94 @@ static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2; #ifdef HELTEC_MESH_NODE_T114 -uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { - uint32_t ret = 0; - uint8_t SDAPIN = mosi; - pinMode(SDAPIN, INPUT_PULLUP); - digitalWrite(dc, HIGH); - for (int i = 0; i < dummy; i++) { // any dummy clocks - digitalWrite(sck, HIGH); - delay(1); - digitalWrite(sck, LOW); - delay(1); - } - for (int i = 0; i < bits; i++) { // read results - ret <<= 1; - delay(1); - if (digitalRead(SDAPIN)) - ret |= 1; - ; - digitalWrite(sck, HIGH); - delay(1); - digitalWrite(sck, LOW); - } - return ret; +uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) +{ + uint32_t ret = 0; + uint8_t SDAPIN = mosi; + pinMode(SDAPIN, INPUT_PULLUP); + digitalWrite(dc, HIGH); + for (int i = 0; i < dummy; i++) { // any dummy clocks + digitalWrite(sck, HIGH); + delay(1); + digitalWrite(sck, LOW); + delay(1); + } + for (int i = 0; i < bits; i++) { // read results + ret <<= 1; + delay(1); + if (digitalRead(SDAPIN)) + ret |= 1; + ; + digitalWrite(sck, HIGH); + delay(1); + digitalWrite(sck, LOW); + } + return ret; } -void write9(uint8_t val, uint8_t dc_val, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { - pinMode(mosi, OUTPUT); - digitalWrite(dc, dc_val); - for (int i = 0; i < 8; i++) { // send command - digitalWrite(mosi, (val & 0x80) != 0); - delay(1); - digitalWrite(sck, HIGH); - delay(1); - digitalWrite(sck, LOW); - val <<= 1; - } +void write9(uint8_t val, uint8_t dc_val, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) +{ + pinMode(mosi, OUTPUT); + digitalWrite(dc, dc_val); + for (int i = 0; i < 8; i++) { // send command + digitalWrite(mosi, (val & 0x80) != 0); + delay(1); + digitalWrite(sck, HIGH); + delay(1); + digitalWrite(sck, LOW); + val <<= 1; + } } -uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { - digitalWrite(cs, LOW); - write9(cmd, 0, cs, sck, mosi, dc, rst); - uint32_t ret = read8(bits, dummy, cs, sck, mosi, dc, rst); - digitalWrite(cs, HIGH); - return ret; +uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) +{ + digitalWrite(cs, LOW); + write9(cmd, 0, cs, sck, mosi, dc, rst); + uint32_t ret = read8(bits, dummy, cs, sck, mosi, dc, rst); + digitalWrite(cs, HIGH); + return ret; } -uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { - pinMode(cs, OUTPUT); - digitalWrite(cs, HIGH); - pinMode(cs, OUTPUT); - pinMode(sck, OUTPUT); - pinMode(mosi, OUTPUT); - pinMode(dc, OUTPUT); - pinMode(rst, OUTPUT); - digitalWrite(rst, LOW); // Hardware Reset - delay(10); - digitalWrite(rst, HIGH); - delay(10); +uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) +{ + pinMode(cs, OUTPUT); + digitalWrite(cs, HIGH); + pinMode(cs, OUTPUT); + pinMode(sck, OUTPUT); + pinMode(mosi, OUTPUT); + pinMode(dc, OUTPUT); + pinMode(rst, OUTPUT); + digitalWrite(rst, LOW); // Hardware Reset + delay(10); + digitalWrite(rst, HIGH); + delay(10); - uint32_t ID = 0; - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice - return ID; + uint32_t ID = 0; + ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); + ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice + return ID; } #endif -bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) { - if (ostream) { - std::vector const *vec = (std::vector *)field->pData; - for (auto item : *vec) { - if (!pb_encode_tag_for_field(ostream, field)) - return false; - pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); +bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) +{ + if (ostream) { + std::vector const *vec = (std::vector *)field->pData; + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); + } } - } - if (istream) { - meshtastic_NodeInfoLite node; // this gets good data - std::vector *vec = (std::vector *)field->pData; + if (istream) { + meshtastic_NodeInfoLite node; // this gets good data + std::vector *vec = (std::vector *)field->pData; - if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) - vec->push_back(node); - } - return true; + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) + vec->push_back(node); + } + return true; } /** The current change # for radio settings. Starts at 0 on boot and any time the radio settings @@ -186,884 +191,917 @@ uint32_t error_address = 0; static uint8_t ourMacAddr[6]; -NodeDB::NodeDB() { - LOG_INFO("Init NodeDB"); - loadFromDisk(); - cleanupMeshDB(); +NodeDB::NodeDB() +{ + LOG_INFO("Init NodeDB"); + loadFromDisk(); + cleanupMeshDB(); - uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); - uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)); - uint32_t configCRC = crc32Buffer(&config, sizeof(config)); - uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); + uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); + uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)); + uint32_t configCRC = crc32Buffer(&config, sizeof(config)); + uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); - int saveWhat = 0; - // Get device unique id + int saveWhat = 0; + // Get device unique id #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) - uint32_t unique_id[4]; - // ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series - // This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us - esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8); - if (err == ESP_OK) { - memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id)); - myNodeInfo.device_id.size = 16; - } else { - LOG_WARN("Failed to read unique id from efuse"); - } + uint32_t unique_id[4]; + // ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series + // This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us + esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8); + if (err == ESP_OK) { + memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id)); + myNodeInfo.device_id.size = 16; + } else { + LOG_WARN("Failed to read unique id from efuse"); + } #elif defined(ARCH_NRF52) - // Nordic applies a FIPS compliant Random ID to each chip at the factory - // We concatenate the device address to the Random ID to create a unique ID for now - // This will likely utilize a crypto module in the future - uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0]; - uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; - memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start)); - memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); - myNodeInfo.device_id.size = 16; - // Uncomment below to print the device id -#elif ARCH_PORTDUINO - if (portduino_config.has_device_id) { - memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); + // Nordic applies a FIPS compliant Random ID to each chip at the factory + // We concatenate the device address to the Random ID to create a unique ID for now + // This will likely utilize a crypto module in the future + uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0]; + uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; + memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start)); + memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); myNodeInfo.device_id.size = 16; - } + // Uncomment below to print the device id +#elif ARCH_PORTDUINO + if (portduino_config.has_device_id) { + memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); + myNodeInfo.device_id.size = 16; + } #else - // FIXME - implement for other platforms + // FIXME - implement for other platforms #endif - // if (myNodeInfo.device_id.size == 16) { - // std::string deviceIdHex; - // for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) { - // char buf[3]; - // snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]); - // deviceIdHex += buf; - // } - // LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str()); - // } + // if (myNodeInfo.device_id.size == 16) { + // std::string deviceIdHex; + // for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) { + // char buf[3]; + // snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]); + // deviceIdHex += buf; + // } + // LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str()); + // } - // likewise - we always want the app requirements to come from the running appload - myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00 - // Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we - // won't keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid - // conflicts) - pickNewNodeNum(); + // likewise - we always want the app requirements to come from the running appload + myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00 + // Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we won't + // keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid conflicts) + pickNewNodeNum(); - // Set our board type so we can share it with others - owner.hw_model = HW_VENDOR; - // Ensure user (nodeinfo) role is set to whatever we're configured to - owner.role = config.device.role; - // Ensure macaddr is set to our macaddr as it will be copied in our info below - memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); - // Ensure owner.id is always derived from the node number - snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); + // Set our board type so we can share it with others + owner.hw_model = HW_VENDOR; + // Ensure user (nodeinfo) role is set to whatever we're configured to + owner.role = config.device.role; + // Ensure macaddr is set to our macaddr as it will be copied in our info below + memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); + // Ensure owner.id is always derived from the node number + snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); - if (!config.has_security) { - config.has_security = true; - config.security = meshtastic_Config_SecurityConfig_init_default; - config.security.serial_enabled = config.device.serial_enabled; - config.security.is_managed = config.device.is_managed; - } + if (!config.has_security) { + config.has_security = true; + config.security = meshtastic_Config_SecurityConfig_init_default; + config.security.serial_enabled = config.device.serial_enabled; + config.security.is_managed = config.device.is_managed; + } #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - bool keygenSuccess = false; - keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key); - if (config.security.private_key.size == 32 && !keyIsLowEntropy) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + bool keygenSuccess = false; + keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key); + if (config.security.private_key.size == 32 && !keyIsLowEntropy) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } #elif !(MESHTASTIC_EXCLUDE_PKI) - // Calculate Curve25519 public and private keys - if (config.security.private_key.size == 32 && config.security.public_key.size == 32) { - owner.public_key.size = config.security.public_key.size; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); - crypto->setDHPrivateKey(config.security.private_key.bytes); - } + // Calculate Curve25519 public and private keys + if (config.security.private_key.size == 32 && config.security.public_key.size == 32) { + owner.public_key.size = config.security.public_key.size; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); + crypto->setDHPrivateKey(config.security.private_key.bytes); + } #endif - // Include our owner in the node db under our nodenum - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); - info->user = TypeConversions::ConvertToUserLite(owner); - info->has_user = true; + // Include our owner in the node db under our nodenum + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); + info->user = TypeConversions::ConvertToUserLite(owner); + info->has_user = true; - // If node database has not been saved for the first time, save it now + // If node database has not been saved for the first time, save it now #ifdef FSCom - if (!FSCom.exists(nodeDatabaseFileName)) { - saveNodeDatabaseToDisk(); - } + if (!FSCom.exists(nodeDatabaseFileName)) { + saveNodeDatabaseToDisk(); + } #endif #ifdef ARCH_ESP32 - Preferences preferences; - preferences.begin("meshtastic", false); - myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0); - preferences.end(); - LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count); + Preferences preferences; + preferences.begin("meshtastic", false); + myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0); + preferences.end(); + LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count); #endif - resetRadioConfig(); // If bogus settings got saved, then fix them - // nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes); + resetRadioConfig(); // If bogus settings got saved, then fix them + // nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes); - // Uncomment below to always enable UDP broadcasts - // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; + // Uncomment below to always enable UDP broadcasts + // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; - // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum - // value of 30 minutes or more - if (channels.isDefaultChannel(channels.getPrimaryIndex())) { - LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); - moduleConfig.telemetry.device_update_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.environment_update_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.environment_update_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.air_quality_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.air_quality_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.power_update_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.power_update_interval, min_default_telemetry_interval_secs); - moduleConfig.telemetry.health_update_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); - } - // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched - if (config.device.node_info_broadcast_secs > MAX_INTERVAL) - config.device.node_info_broadcast_secs = MAX_INTERVAL; - if (config.position.position_broadcast_secs > MAX_INTERVAL) - config.position.position_broadcast_secs = MAX_INTERVAL; - if (config.position.gps_update_interval > MAX_INTERVAL) - config.position.gps_update_interval = MAX_INTERVAL; - if (config.position.gps_attempt_time > MAX_INTERVAL) - config.position.gps_attempt_time = MAX_INTERVAL; - if (config.position.position_flags > MAX_INTERVAL) - config.position.position_flags = MAX_INTERVAL; - if (config.position.rx_gpio > MAX_INTERVAL) - config.position.rx_gpio = MAX_INTERVAL; - if (config.position.tx_gpio > MAX_INTERVAL) - config.position.tx_gpio = MAX_INTERVAL; - if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL) - config.position.broadcast_smart_minimum_distance = MAX_INTERVAL; - if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL) - config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL; - if (config.position.gps_en_gpio > MAX_INTERVAL) - config.position.gps_en_gpio = MAX_INTERVAL; - if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) - moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) - moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) - moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) - moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; - if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) - moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; + // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value + // of 30 minutes or more + if (channels.isDefaultChannel(channels.getPrimaryIndex())) { + LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); + moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue( + moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.environment_update_interval = Default::getConfiguredOrMinimumValue( + moduleConfig.telemetry.environment_update_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.air_quality_interval = Default::getConfiguredOrMinimumValue( + moduleConfig.telemetry.air_quality_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.power_update_interval = Default::getConfiguredOrMinimumValue( + moduleConfig.telemetry.power_update_interval, min_default_telemetry_interval_secs); + moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue( + moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); + } + // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched + if (config.device.node_info_broadcast_secs > MAX_INTERVAL) + config.device.node_info_broadcast_secs = MAX_INTERVAL; + if (config.position.position_broadcast_secs > MAX_INTERVAL) + config.position.position_broadcast_secs = MAX_INTERVAL; + if (config.position.gps_update_interval > MAX_INTERVAL) + config.position.gps_update_interval = MAX_INTERVAL; + if (config.position.gps_attempt_time > MAX_INTERVAL) + config.position.gps_attempt_time = MAX_INTERVAL; + if (config.position.position_flags > MAX_INTERVAL) + config.position.position_flags = MAX_INTERVAL; + if (config.position.rx_gpio > MAX_INTERVAL) + config.position.rx_gpio = MAX_INTERVAL; + if (config.position.tx_gpio > MAX_INTERVAL) + config.position.tx_gpio = MAX_INTERVAL; + if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL) + config.position.broadcast_smart_minimum_distance = MAX_INTERVAL; + if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL) + config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL; + if (config.position.gps_en_gpio > MAX_INTERVAL) + config.position.gps_en_gpio = MAX_INTERVAL; + if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; - if (moduleConfig.mqtt.has_map_report_settings && moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { - moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; - } + if (moduleConfig.mqtt.has_map_report_settings && + moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { + moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; + } - // Ensure that the neighbor info update interval is coerced to the minimum - moduleConfig.neighbor_info.update_interval = - Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs); + // Ensure that the neighbor info update interval is coerced to the minimum + moduleConfig.neighbor_info.update_interval = + Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs); - // Don't let licensed users to rebroadcast encrypted packets - if (owner.is_licensed) { - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - } + // Don't let licensed users to rebroadcast encrypted packets + if (owner.is_licensed) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + } #if !HAS_TFT - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - // On a device without MUI, this display mode makes no sense, and will break logic. - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - config.bluetooth.enabled = true; - } + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // On a device without MUI, this display mode makes no sense, and will break logic. + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + config.bluetooth.enabled = true; + } #endif - if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) - saveWhat |= SEGMENT_DEVICESTATE; - if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) - saveWhat |= SEGMENT_NODEDATABASE; - if (configCRC != crc32Buffer(&config, sizeof(config))) - saveWhat |= SEGMENT_CONFIG; - if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile))) - saveWhat |= SEGMENT_CHANNELS; + if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) + saveWhat |= SEGMENT_DEVICESTATE; + if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) + saveWhat |= SEGMENT_NODEDATABASE; + if (configCRC != crc32Buffer(&config, sizeof(config))) + saveWhat |= SEGMENT_CONFIG; + if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile))) + saveWhat |= SEGMENT_CHANNELS; - if (config.position.gps_enabled) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - config.position.gps_enabled = 0; - } + if (config.position.gps_enabled) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + config.position.gps_enabled = 0; + } #ifdef USERPREFS_FIRMWARE_EDITION - myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; + myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; #endif #ifdef USERPREFS_FIXED_GPS - if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. - meshtastic_Position fixedGPS = meshtastic_Position_init_default; + if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. + meshtastic_Position fixedGPS = meshtastic_Position_init_default; #ifdef USERPREFS_FIXED_GPS_LAT - fixedGPS.latitude_i = (int32_t)(USERPREFS_FIXED_GPS_LAT * 1e7); - fixedGPS.has_latitude_i = true; + fixedGPS.latitude_i = (int32_t)(USERPREFS_FIXED_GPS_LAT * 1e7); + fixedGPS.has_latitude_i = true; #endif #ifdef USERPREFS_FIXED_GPS_LON - fixedGPS.longitude_i = (int32_t)(USERPREFS_FIXED_GPS_LON * 1e7); - fixedGPS.has_longitude_i = true; + fixedGPS.longitude_i = (int32_t)(USERPREFS_FIXED_GPS_LON * 1e7); + fixedGPS.has_longitude_i = true; #endif #ifdef USERPREFS_FIXED_GPS_ALT - fixedGPS.altitude = USERPREFS_FIXED_GPS_ALT; - fixedGPS.has_altitude = true; + fixedGPS.altitude = USERPREFS_FIXED_GPS_ALT; + fixedGPS.has_altitude = true; #endif #if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON) - fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; - config.has_position = true; - info->has_position = true; - info->position = TypeConversions::ConvertToPositionLite(fixedGPS); - nodeDB->setLocalPosition(fixedGPS); - config.position.fixed_position = true; + fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; + config.has_position = true; + info->has_position = true; + info->position = TypeConversions::ConvertToPositionLite(fixedGPS); + nodeDB->setLocalPosition(fixedGPS); + config.position.fixed_position = true; #endif - } + } #endif - sortMeshDB(); - saveToDisk(saveWhat); + sortMeshDB(); + saveToDisk(saveWhat); } /** - * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they - * originated on the local node. If from is zero this function returns our node number instead + * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on + * the local node. If from is zero this function returns our node number instead */ -NodeNum getFrom(const meshtastic_MeshPacket *p) { return (p->from == 0) ? nodeDB->getNodeNum() : p->from; } +NodeNum getFrom(const meshtastic_MeshPacket *p) +{ + return (p->from == 0) ? nodeDB->getNodeNum() : p->from; +} // Returns true if the packet originated from the local node -bool isFromUs(const meshtastic_MeshPacket *p) { return p->from == 0 || p->from == nodeDB->getNodeNum(); } +bool isFromUs(const meshtastic_MeshPacket *p) +{ + return p->from == 0 || p->from == nodeDB->getNodeNum(); +} // Returns true if the packet is destined to us -bool isToUs(const meshtastic_MeshPacket *p) { return p->to == nodeDB->getNodeNum(); } - -bool isBroadcast(uint32_t dest) { return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; } - -void NodeDB::resetRadioConfig(bool is_fresh_install) { - if (is_fresh_install) { - radioGeneration++; - } - - if (channelFile.channels_count != MAX_NUM_CHANNELS) { - LOG_INFO("Set default channel and radio preferences!"); - - channels.initDefaults(); - } - - channels.onConfigChanged(); - - // Update the global myRegion - initRegion(); +bool isToUs(const meshtastic_MeshPacket *p) +{ + return p->to == nodeDB->getNodeNum(); } -bool NodeDB::factoryReset(bool eraseBleBonds) { - LOG_INFO("Perform factory reset!"); - // first, remove the "/prefs" (this removes most prefs) - spiLock->lock(); - rmDir("/prefs"); // this uses spilock internally... +bool isBroadcast(uint32_t dest) +{ + return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; +} + +void NodeDB::resetRadioConfig(bool is_fresh_install) +{ + if (is_fresh_install) { + radioGeneration++; + } + + if (channelFile.channels_count != MAX_NUM_CHANNELS) { + LOG_INFO("Set default channel and radio preferences!"); + + channels.initDefaults(); + } + + channels.onConfigChanged(); + + // Update the global myRegion + initRegion(); +} + +bool NodeDB::factoryReset(bool eraseBleBonds) +{ + LOG_INFO("Perform factory reset!"); + // first, remove the "/prefs" (this removes most prefs) + spiLock->lock(); + rmDir("/prefs"); // this uses spilock internally... #ifdef FSCom - if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) { - LOG_ERROR("Could not remove rangetest.csv file"); - } + if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) { + LOG_ERROR("Could not remove rangetest.csv file"); + } #endif - spiLock->unlock(); - // second, install default state (this will deal with the duplicate mac address issue) - installDefaultNodeDatabase(); - installDefaultDeviceState(); - installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds - installDefaultModuleConfig(); - installDefaultChannels(); - // third, write everything to disk - saveToDisk(); - if (eraseBleBonds) { - LOG_INFO("Erase BLE bonds"); + spiLock->unlock(); + // second, install default state (this will deal with the duplicate mac address issue) + installDefaultNodeDatabase(); + installDefaultDeviceState(); + installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds + installDefaultModuleConfig(); + installDefaultChannels(); + // third, write everything to disk + saveToDisk(); + if (eraseBleBonds) { + LOG_INFO("Erase BLE bonds"); #ifdef ARCH_ESP32 - // This will erase what's in NVS including ssl keys, persistent variables and ble pairing - nvs_flash_erase(); + // This will erase what's in NVS including ssl keys, persistent variables and ble pairing + nvs_flash_erase(); #endif #ifdef ARCH_NRF52 - LOG_INFO("Clear bluetooth bonds!"); - bond_print_list(BLE_GAP_ROLE_PERIPH); - bond_print_list(BLE_GAP_ROLE_CENTRAL); - Bluefruit.Periph.clearBonds(); - Bluefruit.Central.clearBonds(); + LOG_INFO("Clear bluetooth bonds!"); + bond_print_list(BLE_GAP_ROLE_PERIPH); + bond_print_list(BLE_GAP_ROLE_CENTRAL); + Bluefruit.Periph.clearBonds(); + Bluefruit.Central.clearBonds(); #endif - } - return true; + } + return true; } -void NodeDB::installDefaultNodeDatabase() { - LOG_DEBUG("Install default NodeDatabase"); - nodeDatabase.version = DEVICESTATE_CUR_VER; - nodeDatabase.nodes = std::vector(MAX_NUM_NODES); - numMeshNodes = 0; - meshNodes = &nodeDatabase.nodes; +void NodeDB::installDefaultNodeDatabase() +{ + LOG_DEBUG("Install default NodeDatabase"); + nodeDatabase.version = DEVICESTATE_CUR_VER; + nodeDatabase.nodes = std::vector(MAX_NUM_NODES); + numMeshNodes = 0; + meshNodes = &nodeDatabase.nodes; } -void NodeDB::installDefaultConfig(bool preserveKey = false) { - uint8_t private_key_temp[32]; - bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size > 0; - if (shouldPreserveKey) { - memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size); - } - LOG_INFO("Install default LocalConfig"); - memset(&config, 0, sizeof(meshtastic_LocalConfig)); - config.version = DEVICESTATE_CUR_VER; - config.has_device = true; - config.has_display = true; - config.has_lora = true; - config.has_position = true; - config.has_power = true; - config.has_network = true; - config.has_bluetooth = (HAS_BLUETOOTH ? true : false); - config.has_security = true; - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; +void NodeDB::installDefaultConfig(bool preserveKey = false) +{ + uint8_t private_key_temp[32]; + bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size > 0; + if (shouldPreserveKey) { + memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size); + } + LOG_INFO("Install default LocalConfig"); + memset(&config, 0, sizeof(meshtastic_LocalConfig)); + config.version = DEVICESTATE_CUR_VER; + config.has_device = true; + config.has_display = true; + config.has_lora = true; + config.has_position = true; + config.has_power = true; + config.has_network = true; + config.has_bluetooth = (HAS_BLUETOOTH ? true : false); + config.has_security = true; + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - config.lora.sx126x_rx_boosted_gain = true; - config.lora.tx_enabled = true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) - config.lora.override_duty_cycle = false; - config.lora.config_ok_to_mqtt = false; + config.lora.sx126x_rx_boosted_gain = true; + config.lora.tx_enabled = + true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) + config.lora.override_duty_cycle = false; + config.lora.config_ok_to_mqtt = false; #if HAS_TFT // For the devices that support MUI, default to that - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif #ifdef USERPREFS_CONFIG_DEVICE_ROLE - // Restrict ROUTER*, LOST AND FOUND roles for security reasons - if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, - meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { - LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - } else { - config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; - } + // Restrict ROUTER*, LOST AND FOUND roles for security reasons + if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { + LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else { + config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; + } #else - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. #endif #ifdef USERPREFS_CONFIG_LORA_REGION - config.lora.region = USERPREFS_CONFIG_LORA_REGION; + config.lora.region = USERPREFS_CONFIG_LORA_REGION; #else - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; #endif #ifdef USERPREFS_LORACONFIG_MODEM_PRESET - config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; + config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET; #else - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; #endif - config.lora.hop_limit = HOP_RELIABLE; + config.lora.hop_limit = HOP_RELIABLE; #ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT - config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT; + config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT; #else - config.lora.ignore_mqtt = false; + config.lora.ignore_mqtt = false; #endif - // Initialize admin_key_count to zero - byte numAdminKeys = 0; + // Initialize admin_key_count to zero + byte numAdminKeys = 0; #ifdef USERPREFS_USE_ADMIN_KEY_0 - // Check if USERPREFS_ADMIN_KEY_0 is non-empty - if (sizeof(userprefs_admin_key_0) > 0) { - memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); - config.security.admin_key[0].size = 32; - numAdminKeys++; - } + // Check if USERPREFS_ADMIN_KEY_0 is non-empty + if (sizeof(userprefs_admin_key_0) > 0) { + memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); + config.security.admin_key[0].size = 32; + numAdminKeys++; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 - // Check if USERPREFS_ADMIN_KEY_1 is non-empty - if (sizeof(userprefs_admin_key_1) > 0) { - memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); - config.security.admin_key[1].size = 32; - numAdminKeys++; - } + // Check if USERPREFS_ADMIN_KEY_1 is non-empty + if (sizeof(userprefs_admin_key_1) > 0) { + memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); + config.security.admin_key[1].size = 32; + numAdminKeys++; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 - // Check if USERPREFS_ADMIN_KEY_2 is non-empty - if (sizeof(userprefs_admin_key_2) > 0) { - memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); - config.security.admin_key[2].size = 32; - numAdminKeys++; - } + // Check if USERPREFS_ADMIN_KEY_2 is non-empty + if (sizeof(userprefs_admin_key_2) > 0) { + memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); + config.security.admin_key[2].size = 32; + numAdminKeys++; + } #endif - config.security.admin_key_count = numAdminKeys; + config.security.admin_key_count = numAdminKeys; - if (shouldPreserveKey) { - config.security.private_key.size = 32; - memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size); - printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size); - } else { - config.security.private_key.size = 0; - } - config.security.public_key.size = 0; + if (shouldPreserveKey) { + config.security.private_key.size = 32; + memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size); + printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size); + } else { + config.security.private_key.size = 0; + } + config.security.public_key.size = 0; #ifdef PIN_GPS_EN - config.position.gps_en_gpio = PIN_GPS_EN; + config.position.gps_en_gpio = PIN_GPS_EN; #endif #if defined(USERPREFS_CONFIG_GPS_MODE) - config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; + config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; #elif !HAS_GPS || GPS_DEFAULT_NOT_PRESENT - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; -#elif !defined(GPS_RX_PIN) - if (config.position.rx_gpio == 0) config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; - else - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; +#elif !defined(GPS_RX_PIN) + if (config.position.rx_gpio == 0) + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; + else + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; #else - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; #endif #ifdef USERPREFS_CONFIG_SMART_POSITION_ENABLED - config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED; + config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED; #else - config.position.position_broadcast_smart_enabled = true; + config.position.position_broadcast_smart_enabled = true; #endif - config.position.broadcast_smart_minimum_distance = 100; - config.position.broadcast_smart_minimum_interval_secs = 30; - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) - config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; - config.security.serial_enabled = true; - config.security.admin_channel_enabled = false; - resetRadioConfig(true); // This also triggers NodeInfo/Position requests since we're fresh - strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); + config.position.broadcast_smart_minimum_distance = 100; + config.position.broadcast_smart_minimum_interval_secs = 30; + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) + config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; + config.security.serial_enabled = true; + config.security.admin_channel_enabled = false; + resetRadioConfig(true); // This also triggers NodeInfo/Position requests since we're fresh + strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); -#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \ - defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \ +#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \ + defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \ HAS_TFT - // switch BT off by default; use TFT programming mode or hotkey to enable - config.bluetooth.enabled = false; + // switch BT off by default; use TFT programming mode or hotkey to enable + config.bluetooth.enabled = false; #else - // default to bluetooth capability of platform as default - config.bluetooth.enabled = true; + // default to bluetooth capability of platform as default + config.bluetooth.enabled = true; #endif - config.bluetooth.fixed_pin = defaultBLEPin; + config.bluetooth.fixed_pin = defaultBLEPin; -#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || defined(HX8357_CS) || \ - defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || defined(USE_ST7796) || \ - defined(HACKADAY_COMMUNICATOR) - bool hasScreen = true; +#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) + bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 - uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); - if (st7789_id == 0xFFFFFF) { - hasScreen = false; - } + uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); + if (st7789_id == 0xFFFFFF) { + hasScreen = false; + } #endif #elif ARCH_PORTDUINO - bool hasScreen = false; - if (portduino_config.displayPanel) - hasScreen = true; - else - hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; + bool hasScreen = false; + if (portduino_config.displayPanel) + hasScreen = true; + else + hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #elif MESHTASTIC_INCLUDE_NICHE_GRAPHICS // See "src/graphics/niche" - bool hasScreen = true; // Use random pin for Bluetooth pairing + bool hasScreen = true; // Use random pin for Bluetooth pairing #else - bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; + bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #endif #ifdef USERPREFS_FIXED_BLUETOOTH - config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH; - config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH; + config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; #else - config.bluetooth.mode = - hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN : meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + config.bluetooth.mode = hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN + : meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; #endif - // for backward compat, default position flags are ALT+MSL - config.position.position_flags = - (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL | - meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | - meshtastic_Config_PositionConfig_PositionFlags_DOP | meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW); + // for backward compat, default position flags are ALT+MSL + config.position.position_flags = + (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL | + meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | + meshtastic_Config_PositionConfig_PositionFlags_DOP | meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW); // Set default value for 'Mesh via UDP' #if HAS_UDP_MULTICAST #ifdef USERPREFS_NETWORK_ENABLED_PROTOCOLS - config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS; + config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS; #else - config.network.enabled_protocols = 0; + config.network.enabled_protocols = 0; #endif #endif #ifdef USERPREFS_NETWORK_WIFI_ENABLED - config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED; + config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED; #endif #ifdef USERPREFS_NETWORK_WIFI_SSID - strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid)); + strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid)); #endif #ifdef USERPREFS_NETWORK_WIFI_PSK - strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk)); + strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk)); #endif #if defined(USERPREFS_NETWORK_IPV6_ENABLED) - config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED; + config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED; #else - config.network.ipv6_enabled = default_network_ipv6_enabled; + config.network.ipv6_enabled = default_network_ipv6_enabled; #endif #ifdef DISPLAY_FLIP_SCREEN - config.display.flip_screen = true; + config.display.flip_screen = true; #endif #ifdef RAK4630 - config.display.wake_on_tap_or_motion = true; + config.display.wake_on_tap_or_motion = true; #endif #if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) - config.display.screen_on_secs = 30; - config.display.wake_on_tap_or_motion = true; + config.display.screen_on_secs = 30; + config.display.wake_on_tap_or_motion = true; #endif #ifdef COMPASS_ORIENTATION - config.display.compass_orientation = COMPASS_ORIENTATION; + config.display.compass_orientation = COMPASS_ORIENTATION; #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::isUpdated()) { - WiFiOTA::recoverConfig(&config.network); - } + if (WiFiOTA::isUpdated()) { + WiFiOTA::recoverConfig(&config.network); + } #endif #ifdef USERPREFS_CONFIG_DEVICE_ROLE - // Apply role-specific defaults when role is set via user preferences - installRoleDefaults(config.device.role); + // Apply role-specific defaults when role is set via user preferences + installRoleDefaults(config.device.role); #endif - initConfigIntervals(); + initConfigIntervals(); } -void NodeDB::initConfigIntervals() { +void NodeDB::initConfigIntervals() +{ #ifdef USERPREFS_CONFIG_GPS_UPDATE_INTERVAL - config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL; + config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL; #else - config.position.gps_update_interval = default_gps_update_interval; + config.position.gps_update_interval = default_gps_update_interval; #endif #ifdef USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL - config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL; + config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL; #else - config.position.position_broadcast_secs = default_broadcast_interval_secs; + config.position.position_broadcast_secs = default_broadcast_interval_secs; #endif - config.power.ls_secs = default_ls_secs; - config.power.min_wake_secs = default_min_wake_secs; - config.power.sds_secs = default_sds_secs; - config.power.wait_bluetooth_secs = default_wait_bluetooth_secs; + config.power.ls_secs = default_ls_secs; + config.power.min_wake_secs = default_min_wake_secs; + config.power.sds_secs = default_sds_secs; + config.power.wait_bluetooth_secs = default_wait_bluetooth_secs; - config.display.screen_on_secs = default_screen_on_secs; + config.display.screen_on_secs = default_screen_on_secs; #if defined(USE_POWERSAVE) - config.power.is_power_saving = true; - config.display.screen_on_secs = 30; - config.power.wait_bluetooth_secs = 30; + config.power.is_power_saving = true; + config.display.screen_on_secs = 30; + config.power.wait_bluetooth_secs = 30; #endif } -void NodeDB::installDefaultModuleConfig() { - LOG_INFO("Install default ModuleConfig"); - memset(&moduleConfig, 0, sizeof(meshtastic_ModuleConfig)); +void NodeDB::installDefaultModuleConfig() +{ + LOG_INFO("Install default ModuleConfig"); + memset(&moduleConfig, 0, sizeof(meshtastic_ModuleConfig)); - moduleConfig.version = DEVICESTATE_CUR_VER; - moduleConfig.has_mqtt = true; - moduleConfig.has_range_test = true; - moduleConfig.has_serial = true; - moduleConfig.has_store_forward = true; - moduleConfig.has_telemetry = true; - moduleConfig.has_external_notification = true; + moduleConfig.version = DEVICESTATE_CUR_VER; + moduleConfig.has_mqtt = true; + moduleConfig.has_range_test = true; + moduleConfig.has_serial = true; + moduleConfig.has_store_forward = true; + moduleConfig.has_telemetry = true; + moduleConfig.has_external_notification = true; #if defined(PIN_BUZZER) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output_buzzer = PIN_BUZZER; - moduleConfig.external_notification.use_pwm = true; - moduleConfig.external_notification.alert_message_buzzer = true; - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output_buzzer = PIN_BUZZER; + moduleConfig.external_notification.use_pwm = true; + moduleConfig.external_notification.alert_message_buzzer = true; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #if defined(PIN_VIBRATION) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output_vibra = PIN_VIBRATION; - moduleConfig.external_notification.alert_message_vibra = true; - moduleConfig.external_notification.output_ms = 500; - moduleConfig.external_notification.nag_timeout = 2; + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output_vibra = PIN_VIBRATION; + moduleConfig.external_notification.alert_message_vibra = true; + moduleConfig.external_notification.output_ms = 500; + moduleConfig.external_notification.nag_timeout = 2; #endif -#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || defined(ELECROW_ThinkNode_M6) - // Default to PIN_LED2 for external notification output (LED color depends on device variant) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = PIN_LED2; +#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ + defined(ELECROW_ThinkNode_M6) + // Default to PIN_LED2 for external notification output (LED color depends on device variant) + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output = PIN_LED2; #if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) - moduleConfig.external_notification.active = false; + moduleConfig.external_notification.active = false; #else - moduleConfig.external_notification.active = true; + moduleConfig.external_notification.active = true; #endif - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; + moduleConfig.external_notification.alert_message = true; + moduleConfig.external_notification.output_ms = 1000; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #ifdef HAS_I2S - // Don't worry about the other settings for T-Watch, we'll also use the DRV2056 behavior for notifications - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.use_i2s_as_buzzer = true; - moduleConfig.external_notification.alert_message_buzzer = true; + // Don't worry about the other settings for T-Watch, we'll also use the DRV2056 behavior for notifications + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.use_i2s_as_buzzer = true; + moduleConfig.external_notification.alert_message_buzzer = true; #if HAS_TFT - if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) - moduleConfig.external_notification.nag_timeout = 0; + if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) + moduleConfig.external_notification.nag_timeout = 0; #else - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; + moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #endif #ifdef NANO_G2_ULTRA - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 100; - moduleConfig.external_notification.active = true; + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.alert_message = true; + moduleConfig.external_notification.output_ms = 100; + moduleConfig.external_notification.active = true; #endif #ifdef ELECROW_ThinkNode_M1 - // Default to Elecrow USER_LED (blue) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = USER_LED; - moduleConfig.external_notification.active = true; - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = 60; + // Default to Elecrow USER_LED (blue) + moduleConfig.external_notification.enabled = true; + moduleConfig.external_notification.output = USER_LED; + moduleConfig.external_notification.active = true; + moduleConfig.external_notification.alert_message = true; + moduleConfig.external_notification.output_ms = 1000; + moduleConfig.external_notification.nag_timeout = 60; #endif #ifdef T_LORA_PAGER - moduleConfig.canned_message.updown1_enabled = true; - moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; - moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; - moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; - moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); - moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); - moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; + moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; + moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; + moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); + moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); + moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif - moduleConfig.has_canned_message = true; + moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT - moduleConfig.mqtt.enabled = true; + moduleConfig.mqtt.enabled = true; #endif #ifdef USERPREFS_MQTT_ADDRESS - strncpy(moduleConfig.mqtt.address, USERPREFS_MQTT_ADDRESS, sizeof(moduleConfig.mqtt.address)); + strncpy(moduleConfig.mqtt.address, USERPREFS_MQTT_ADDRESS, sizeof(moduleConfig.mqtt.address)); #else - strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address)); + strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address)); #endif #ifdef USERPREFS_MQTT_USERNAME - strncpy(moduleConfig.mqtt.username, USERPREFS_MQTT_USERNAME, sizeof(moduleConfig.mqtt.username)); + strncpy(moduleConfig.mqtt.username, USERPREFS_MQTT_USERNAME, sizeof(moduleConfig.mqtt.username)); #else - strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username)); + strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username)); #endif #ifdef USERPREFS_MQTT_PASSWORD - strncpy(moduleConfig.mqtt.password, USERPREFS_MQTT_PASSWORD, sizeof(moduleConfig.mqtt.password)); + strncpy(moduleConfig.mqtt.password, USERPREFS_MQTT_PASSWORD, sizeof(moduleConfig.mqtt.password)); #else - strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password)); + strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password)); #endif #ifdef USERPREFS_MQTT_ROOT_TOPIC - strncpy(moduleConfig.mqtt.root, USERPREFS_MQTT_ROOT_TOPIC, sizeof(moduleConfig.mqtt.root)); + strncpy(moduleConfig.mqtt.root, USERPREFS_MQTT_ROOT_TOPIC, sizeof(moduleConfig.mqtt.root)); #else - strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root)); + strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root)); #endif #ifdef USERPREFS_MQTT_ENCRYPTION_ENABLED - moduleConfig.mqtt.encryption_enabled = USERPREFS_MQTT_ENCRYPTION_ENABLED; + moduleConfig.mqtt.encryption_enabled = USERPREFS_MQTT_ENCRYPTION_ENABLED; #else - moduleConfig.mqtt.encryption_enabled = default_mqtt_encryption_enabled; + moduleConfig.mqtt.encryption_enabled = default_mqtt_encryption_enabled; #endif #ifdef USERPREFS_MQTT_TLS_ENABLED - moduleConfig.mqtt.tls_enabled = USERPREFS_MQTT_TLS_ENABLED; + moduleConfig.mqtt.tls_enabled = USERPREFS_MQTT_TLS_ENABLED; #else - moduleConfig.mqtt.tls_enabled = default_mqtt_tls_enabled; + moduleConfig.mqtt.tls_enabled = default_mqtt_tls_enabled; #endif - moduleConfig.has_neighbor_info = true; - moduleConfig.neighbor_info.enabled = false; + moduleConfig.has_neighbor_info = true; + moduleConfig.neighbor_info.enabled = false; - moduleConfig.has_detection_sensor = true; - moduleConfig.detection_sensor.enabled = false; - moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; - moduleConfig.detection_sensor.minimum_broadcast_secs = 45; + moduleConfig.has_detection_sensor = true; + moduleConfig.detection_sensor.enabled = false; + moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; + moduleConfig.detection_sensor.minimum_broadcast_secs = 45; - moduleConfig.has_ambient_lighting = true; - moduleConfig.ambient_lighting.current = 10; - // Default to a color based on our node number - moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; - moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; - moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; + moduleConfig.has_ambient_lighting = true; + moduleConfig.ambient_lighting.current = 10; + // Default to a color based on our node number + moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; + moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; + moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; - initModuleConfigIntervals(); -} - -void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) { - if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) { - initConfigIntervals(); initModuleConfigIntervals(); - moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - moduleConfig.telemetry.device_update_interval = ONE_DAY; - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; - moduleConfig.telemetry.environment_measurement_enabled = true; - moduleConfig.telemetry.environment_update_interval = 300; - } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { - config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = 300; // Every 5 minutes - } else if (role == meshtastic_Config_DeviceConfig_Role_TAK) { - config.device.node_info_broadcast_secs = ONE_DAY; - config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = ONE_DAY; - // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) - config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | - meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); - moduleConfig.telemetry.device_update_interval = ONE_DAY; - } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; - } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { - owner.has_is_unmessagable = true; - owner.is_unmessagable = true; - config.device.node_info_broadcast_secs = ONE_DAY; - config.position.position_broadcast_smart_enabled = true; - config.position.position_broadcast_secs = 3 * 60; // Every 3 minutes - config.position.broadcast_smart_minimum_distance = 20; - config.position.broadcast_smart_minimum_interval_secs = 15; - // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) - config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | - meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); - moduleConfig.telemetry.device_update_interval = ONE_DAY; - } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - config.device.node_info_broadcast_secs = MAX_INTERVAL; - config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = MAX_INTERVAL; - moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; - moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; - moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; - moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; - moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; - } } -void NodeDB::initModuleConfigIntervals() { - // Zero out telemetry intervals so that they coalesce to defaults in Default.h -#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL - moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; -#else - moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; -#endif - moduleConfig.telemetry.environment_update_interval = 0; - moduleConfig.telemetry.air_quality_interval = 0; - moduleConfig.telemetry.power_update_interval = 0; - moduleConfig.telemetry.health_update_interval = 0; - moduleConfig.neighbor_info.update_interval = 0; - moduleConfig.paxcounter.paxcounter_update_interval = 0; -} - -void NodeDB::installDefaultChannels() { - LOG_INFO("Install default ChannelFile"); - memset(&channelFile, 0, sizeof(meshtastic_ChannelFile)); - channelFile.version = DEVICESTATE_CUR_VER; -} - -void NodeDB::resetNodes(bool keepFavorites) { - if (!config.position.fixed_position) - clearLocalPosition(); - numMeshNodes = 1; - if (keepFavorites) { - LOG_INFO("Clearing node database - preserving favorites"); - for (size_t i = 0; i < meshNodes->size(); i++) { - meshtastic_NodeInfoLite &node = meshNodes->at(i); - if (i > 0 && !node.is_favorite) { - node = meshtastic_NodeInfoLite(); - } else { - numMeshNodes += 1; - } - }; - } else { - LOG_INFO("Clearing node database - removing favorites"); - std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); - } - devicestate.has_rx_text_message = false; - devicestate.has_rx_waypoint = false; - saveNodeDatabaseToDisk(); - saveDeviceStateToDisk(); - if (neighborInfoModule && moduleConfig.neighbor_info.enabled) - neighborInfoModule->resetNeighbors(); -} - -void NodeDB::removeNodeByNum(NodeNum nodeNum) { - int newPos = 0, removed = 0; - for (int i = 0; i < numMeshNodes; i++) { - if (meshNodes->at(i).num != nodeNum) - meshNodes->at(newPos++) = meshNodes->at(i); - else - removed++; - } - numMeshNodes -= removed; - std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, meshtastic_NodeInfoLite()); - LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); - saveNodeDatabaseToDisk(); -} - -void NodeDB::clearLocalPosition() { - meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); - node->position.latitude_i = 0; - node->position.longitude_i = 0; - node->position.altitude = 0; - node->position.time = 0; - setLocalPosition(meshtastic_Position_init_default); - localPositionUpdatedSinceBoot = false; -} - -void NodeDB::cleanupMeshDB() { - int newPos = 0, removed = 0; - for (int i = 0; i < numMeshNodes; i++) { - if (meshNodes->at(i).has_user) { - if (meshNodes->at(i).user.public_key.size > 0) { - if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { - meshNodes->at(i).user.public_key.size = 0; - } - } - if (newPos != i) - meshNodes->at(newPos++) = meshNodes->at(i); - else - newPos++; - } else { - removed++; +void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) +{ + if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + initConfigIntervals(); + initModuleConfigIntervals(); + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + moduleConfig.telemetry.device_update_interval = ONE_DAY; + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; + moduleConfig.telemetry.environment_measurement_enabled = true; + moduleConfig.telemetry.environment_update_interval = 300; + } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = 300; // Every 5 minutes + } else if (role == meshtastic_Config_DeviceConfig_Role_TAK) { + config.device.node_info_broadcast_secs = ONE_DAY; + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = ONE_DAY; + // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) + config.position.position_flags = + (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | + meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); + moduleConfig.telemetry.device_update_interval = ONE_DAY; + } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; + } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + owner.has_is_unmessagable = true; + owner.is_unmessagable = true; + config.device.node_info_broadcast_secs = ONE_DAY; + config.position.position_broadcast_smart_enabled = true; + config.position.position_broadcast_secs = 3 * 60; // Every 3 minutes + config.position.broadcast_smart_minimum_distance = 20; + config.position.broadcast_smart_minimum_interval_secs = 15; + // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) + config.position.position_flags = + (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | + meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); + moduleConfig.telemetry.device_update_interval = ONE_DAY; + } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + config.device.node_info_broadcast_secs = MAX_INTERVAL; + config.position.position_broadcast_smart_enabled = false; + config.position.position_broadcast_secs = MAX_INTERVAL; + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; } - } - numMeshNodes -= removed; - std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, meshtastic_NodeInfoLite()); - LOG_DEBUG("cleanupMeshDB purged %d entries", removed); } -void NodeDB::installDefaultDeviceState() { - LOG_INFO("Install default DeviceState"); - // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); - - // init our devicestate with valid flags so protobuf writing/reading will work - devicestate.has_my_node = true; - devicestate.has_owner = true; - devicestate.version = DEVICESTATE_CUR_VER; - devicestate.receive_queue_count = 0; // Not yet implemented FIXME - devicestate.has_rx_waypoint = false; - devicestate.has_rx_text_message = false; - - generatePacketId(); // FIXME - ugly way to init current_packet_id; - - // Set default owner name - pickNewNodeNum(); // based on macaddr now -#ifdef USERPREFS_CONFIG_OWNER_LONG_NAME - snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME); +void NodeDB::initModuleConfigIntervals() +{ + // Zero out telemetry intervals so that they coalesce to defaults in Default.h +#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL + moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; #else - snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff); + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; +#endif + moduleConfig.telemetry.environment_update_interval = 0; + moduleConfig.telemetry.air_quality_interval = 0; + moduleConfig.telemetry.power_update_interval = 0; + moduleConfig.telemetry.health_update_interval = 0; + moduleConfig.neighbor_info.update_interval = 0; + moduleConfig.paxcounter.paxcounter_update_interval = 0; +} + +void NodeDB::installDefaultChannels() +{ + LOG_INFO("Install default ChannelFile"); + memset(&channelFile, 0, sizeof(meshtastic_ChannelFile)); + channelFile.version = DEVICESTATE_CUR_VER; +} + +void NodeDB::resetNodes(bool keepFavorites) +{ + if (!config.position.fixed_position) + clearLocalPosition(); + numMeshNodes = 1; + if (keepFavorites) { + LOG_INFO("Clearing node database - preserving favorites"); + for (size_t i = 0; i < meshNodes->size(); i++) { + meshtastic_NodeInfoLite &node = meshNodes->at(i); + if (i > 0 && !node.is_favorite) { + node = meshtastic_NodeInfoLite(); + } else { + numMeshNodes += 1; + } + }; + } else { + LOG_INFO("Clearing node database - removing favorites"); + std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); + } + devicestate.has_rx_text_message = false; + devicestate.has_rx_waypoint = false; + saveNodeDatabaseToDisk(); + saveDeviceStateToDisk(); + if (neighborInfoModule && moduleConfig.neighbor_info.enabled) + neighborInfoModule->resetNeighbors(); +} + +void NodeDB::removeNodeByNum(NodeNum nodeNum) +{ + int newPos = 0, removed = 0; + for (int i = 0; i < numMeshNodes; i++) { + if (meshNodes->at(i).num != nodeNum) + meshNodes->at(newPos++) = meshNodes->at(i); + else + removed++; + } + numMeshNodes -= removed; + std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, + meshtastic_NodeInfoLite()); + LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); + saveNodeDatabaseToDisk(); +} + +void NodeDB::clearLocalPosition() +{ + meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); + node->position.latitude_i = 0; + node->position.longitude_i = 0; + node->position.altitude = 0; + node->position.time = 0; + setLocalPosition(meshtastic_Position_init_default); + localPositionUpdatedSinceBoot = false; +} + +void NodeDB::cleanupMeshDB() +{ + int newPos = 0, removed = 0; + for (int i = 0; i < numMeshNodes; i++) { + if (meshNodes->at(i).has_user) { + if (meshNodes->at(i).user.public_key.size > 0) { + if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { + meshNodes->at(i).user.public_key.size = 0; + } + } + if (newPos != i) + meshNodes->at(newPos++) = meshNodes->at(i); + else + newPos++; + } else { + removed++; + } + } + numMeshNodes -= removed; + std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, + meshtastic_NodeInfoLite()); + LOG_DEBUG("cleanupMeshDB purged %d entries", removed); +} + +void NodeDB::installDefaultDeviceState() +{ + LOG_INFO("Install default DeviceState"); + // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); + + // init our devicestate with valid flags so protobuf writing/reading will work + devicestate.has_my_node = true; + devicestate.has_owner = true; + devicestate.version = DEVICESTATE_CUR_VER; + devicestate.receive_queue_count = 0; // Not yet implemented FIXME + devicestate.has_rx_waypoint = false; + devicestate.has_rx_text_message = false; + + generatePacketId(); // FIXME - ugly way to init current_packet_id; + + // Set default owner name + pickNewNodeNum(); // based on macaddr now +#ifdef USERPREFS_CONFIG_OWNER_LONG_NAME + snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME); +#else + snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff); #endif #ifdef USERPREFS_CONFIG_OWNER_SHORT_NAME - snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME); + snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME); #else - snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff); + snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff); #endif - snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum - memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); - owner.has_is_unmessagable = true; - owner.is_unmessagable = false; + snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum + memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); + owner.has_is_unmessagable = true; + owner.is_unmessagable = false; } // We reserve a few nodenums for future use @@ -1072,456 +1110,477 @@ void NodeDB::installDefaultDeviceState() { /** * get our starting (provisional) nodenum from flash. */ -void NodeDB::pickNewNodeNum() { - NodeNum nodeNum = myNodeInfo.my_node_num; - getMacAddr(ourMacAddr); // Make sure ourMacAddr is set - if (nodeNum == 0) { - // Pick an initial nodenum based on the macaddr - nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; - } +void NodeDB::pickNewNodeNum() +{ + NodeNum nodeNum = myNodeInfo.my_node_num; + getMacAddr(ourMacAddr); // Make sure ourMacAddr is set + if (nodeNum == 0) { + // Pick an initial nodenum based on the macaddr + nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; + } - meshtastic_NodeInfoLite *found; - while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) || - (nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) { - NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice - if (found) - LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so " - "trying for 0x%x", - nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate); - nodeNum = candidate; - } - LOG_DEBUG("Use nodenum 0x%x ", nodeNum); + meshtastic_NodeInfoLite *found; + while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) || + (nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) { + NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice + if (found) + LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so " + "trying for 0x%x", + nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate); + nodeNum = candidate; + } + LOG_DEBUG("Use nodenum 0x%x ", nodeNum); - myNodeInfo.my_node_num = nodeNum; + myNodeInfo.my_node_num = nodeNum; } /** Load a protobuf from a file, return LoadFileResult */ -LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct) { - LoadFileResult state = LoadFileResult::OTHER_FAILURE; +LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, + void *dest_struct) +{ + LoadFileResult state = LoadFileResult::OTHER_FAILURE; #ifdef FSCom - concurrency::LockGuard g(spiLock); + concurrency::LockGuard g(spiLock); - auto f = FSCom.open(filename, FILE_O_READ); + auto f = FSCom.open(filename, FILE_O_READ); - if (f) { - LOG_INFO("Load %s", filename); - pb_istream_t stream = {&readcb, &f, protoSize}; - if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object - memset(dest_struct, 0, objSize); - if (!pb_decode(&stream, fields, dest_struct)) { - LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); - state = LoadFileResult::DECODE_FAILED; + if (f) { + LOG_INFO("Load %s", filename); + pb_istream_t stream = {&readcb, &f, protoSize}; + if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object + memset(dest_struct, 0, objSize); + if (!pb_decode(&stream, fields, dest_struct)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); + state = LoadFileResult::DECODE_FAILED; + } else { + LOG_INFO("Loaded %s successfully", filename); + state = LoadFileResult::LOAD_SUCCESS; + } + f.close(); } else { - LOG_INFO("Loaded %s successfully", filename); - state = LoadFileResult::LOAD_SUCCESS; + LOG_ERROR("Could not open / read %s", filename); } - f.close(); - } else { - LOG_ERROR("Could not open / read %s", filename); - } #else - LOG_ERROR("ERROR: Filesystem not implemented"); - state = LoadFileResult::NO_FILESYSTEM; + LOG_ERROR("ERROR: Filesystem not implemented"); + state = LoadFileResult::NO_FILESYSTEM; #endif - return state; + return state; } -void NodeDB::loadFromDisk() { - // Mark the current device state as completely unusable, so that if we fail reading the entire file from - // disk we will still factoryReset to restore things. - devicestate.version = 0; +void NodeDB::loadFromDisk() +{ + // Mark the current device state as completely unusable, so that if we fail reading the entire file from + // disk we will still factoryReset to restore things. + devicestate.version = 0; - meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero; + meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero; #ifdef ARCH_ESP32 - spiLock->lock(); - // If the legacy deviceState exists, start over with a factory reset - if (FSCom.exists("/static/static")) - rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release - spiLock->unlock(); + spiLock->lock(); + // If the legacy deviceState exists, start over with a factory reset + if (FSCom.exists("/static/static")) + rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release + spiLock->unlock(); #endif #ifdef FSCom #ifdef FACTORY_INSTALL - spiLock->lock(); - if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) { - LOG_WARN("Factory Install Reset!"); - FSCom.format(); - FSCom.mkdir("/prefs"); - File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE); - if (f2) { - f2.flush(); - f2.close(); - } - } - spiLock->unlock(); -#endif - spiLock->lock(); - if (FSCom.exists(legacyPrefFileName)) { - spiLock->unlock(); - LOG_WARN("Legacy prefs version found, factory resetting"); - if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config) == - LoadFileResult::LOAD_SUCCESS && - config.has_security && config.security.private_key.size > 0) { - LOG_DEBUG("Saving backup of security config and keys"); - backupSecurity = config.security; - } spiLock->lock(); - rmDir("/prefs"); + if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) { + LOG_WARN("Factory Install Reset!"); + FSCom.format(); + FSCom.mkdir("/prefs"); + File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE); + if (f2) { + f2.flush(); + f2.close(); + } + } spiLock->unlock(); - } else { - spiLock->unlock(); - } +#endif + spiLock->lock(); + if (FSCom.exists(legacyPrefFileName)) { + spiLock->unlock(); + LOG_WARN("Legacy prefs version found, factory resetting"); + if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, + &config) == LoadFileResult::LOAD_SUCCESS && + config.has_security && config.security.private_key.size > 0) { + LOG_DEBUG("Saving backup of security config and keys"); + backupSecurity = config.security; + } + spiLock->lock(); + rmDir("/prefs"); + spiLock->unlock(); + } else { + spiLock->unlock(); + } #endif - auto state = - loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase), &meshtastic_NodeDatabase_msg, &nodeDatabase); - if (nodeDatabase.version < DEVICESTATE_MIN_VER) { - LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); - installDefaultNodeDatabase(); - } else { - meshNodes = &nodeDatabase.nodes; - numMeshNodes = nodeDatabase.nodes.size(); - LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); - } - - if (numMeshNodes > MAX_NUM_NODES) { - LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); - numMeshNodes = MAX_NUM_NODES; - } - meshNodes->resize(MAX_NUM_NODES); - - // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM - state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), &meshtastic_DeviceState_msg, &devicestate); - - // See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786 - // It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most - // of our critical config may still be valid (in the other files - loaded next). Also, if we did fail on reading we - // probably failed on the enormous (and non critical) nodeDB. So DO NOT install default device state. if (state != - // LoadFileResult::LOAD_SUCCESS) { - // installDefaultDeviceState(); // Our in RAM copy might now be corrupt - //} else { - if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { - LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); - installDefaultDeviceState(); - } else { - LOG_INFO("Loaded saved devicestate version %d", devicestate.version); - } - - state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config); - if (state != LoadFileResult::LOAD_SUCCESS) { - installDefaultConfig(); // Our in RAM copy might now be corrupt - } else { - if (config.version < DEVICESTATE_MIN_VER) { - LOG_WARN("config %d is old, discard", config.version); - installDefaultConfig(true); + auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase), + &meshtastic_NodeDatabase_msg, &nodeDatabase); + if (nodeDatabase.version < DEVICESTATE_MIN_VER) { + LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); + installDefaultNodeDatabase(); } else { - LOG_INFO("Loaded saved config version %d", config.version); + meshNodes = &nodeDatabase.nodes; + numMeshNodes = nodeDatabase.nodes.size(); + LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); } - } - if (backupSecurity.private_key.size > 0) { - LOG_DEBUG("Restoring backup of security config"); - config.security = backupSecurity; - saveToDisk(SEGMENT_CONFIG); - } - // Make sure we load hard coded admin keys even when the configuration file has none. - // Initialize admin_key_count to zero - byte numAdminKeys = 0; + if (numMeshNodes > MAX_NUM_NODES) { + LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); + numMeshNodes = MAX_NUM_NODES; + } + meshNodes->resize(MAX_NUM_NODES); + + // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM + state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), + &meshtastic_DeviceState_msg, &devicestate); + + // See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786 + // It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most of our + // critical config may still be valid (in the other files - loaded next). + // Also, if we did fail on reading we probably failed on the enormous (and non critical) nodeDB. So DO NOT install default + // device state. + // if (state != LoadFileResult::LOAD_SUCCESS) { + // installDefaultDeviceState(); // Our in RAM copy might now be corrupt + //} else { + if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { + LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); + installDefaultDeviceState(); + } else { + LOG_INFO("Loaded saved devicestate version %d", devicestate.version); + } + + state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, + &config); + if (state != LoadFileResult::LOAD_SUCCESS) { + installDefaultConfig(); // Our in RAM copy might now be corrupt + } else { + if (config.version < DEVICESTATE_MIN_VER) { + LOG_WARN("config %d is old, discard", config.version); + installDefaultConfig(true); + } else { + LOG_INFO("Loaded saved config version %d", config.version); + } + } + if (backupSecurity.private_key.size > 0) { + LOG_DEBUG("Restoring backup of security config"); + config.security = backupSecurity; + saveToDisk(SEGMENT_CONFIG); + } + + // Make sure we load hard coded admin keys even when the configuration file has none. + // Initialize admin_key_count to zero + byte numAdminKeys = 0; #if defined(USERPREFS_USE_ADMIN_KEY_0) || defined(USERPREFS_USE_ADMIN_KEY_1) || defined(USERPREFS_USE_ADMIN_KEY_2) - uint16_t sum = 0; + uint16_t sum = 0; #endif #ifdef USERPREFS_USE_ADMIN_KEY_0 - for (uint8_t b = 0; b < 32; b++) { - sum += config.security.admin_key[0].bytes[b]; - } - if (sum == 0) { - numAdminKeys += 1; - LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences."); - memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); - config.security.admin_key[0].size = 32; - } + for (uint8_t b = 0; b < 32; b++) { + sum += config.security.admin_key[0].bytes[b]; + } + if (sum == 0) { + numAdminKeys += 1; + LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences."); + memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); + config.security.admin_key[0].size = 32; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 - sum = 0; - for (uint8_t b = 0; b < 32; b++) { - sum += config.security.admin_key[1].bytes[b]; - } - if (sum == 0) { - numAdminKeys += 1; - LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences."); - memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); - config.security.admin_key[1].size = 32; - } + sum = 0; + for (uint8_t b = 0; b < 32; b++) { + sum += config.security.admin_key[1].bytes[b]; + } + if (sum == 0) { + numAdminKeys += 1; + LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences."); + memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); + config.security.admin_key[1].size = 32; + } #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 - sum = 0; - for (uint8_t b = 0; b < 32; b++) { - sum += config.security.admin_key[2].bytes[b]; - } - if (sum == 0) { - numAdminKeys += 1; - LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences."); - memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); - config.security.admin_key[2].size = 32; - } + sum = 0; + for (uint8_t b = 0; b < 32; b++) { + sum += config.security.admin_key[2].bytes[b]; + } + if (sum == 0) { + numAdminKeys += 1; + LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences."); + memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); + config.security.admin_key[2].size = 32; + } #endif - if (numAdminKeys > 0) { - LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys); - config.security.admin_key_count = numAdminKeys; - saveToDisk(SEGMENT_CONFIG); - } - - state = loadProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, sizeof(meshtastic_LocalModuleConfig), &meshtastic_LocalModuleConfig_msg, - &moduleConfig); - if (state != LoadFileResult::LOAD_SUCCESS) { - installDefaultModuleConfig(); // Our in RAM copy might now be corrupt - } else { - if (moduleConfig.version < DEVICESTATE_MIN_VER) { - LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version); - installDefaultModuleConfig(); - } else { - LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version); + if (numAdminKeys > 0) { + LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys); + config.security.admin_key_count = numAdminKeys; + saveToDisk(SEGMENT_CONFIG); } - } - state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg, &channelFile); - if (state != LoadFileResult::LOAD_SUCCESS) { - installDefaultChannels(); // Our in RAM copy might now be corrupt - } else { - if (channelFile.version < DEVICESTATE_MIN_VER) { - LOG_WARN("channelFile %d is old, discard", channelFile.version); - installDefaultChannels(); + state = loadProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, sizeof(meshtastic_LocalModuleConfig), + &meshtastic_LocalModuleConfig_msg, &moduleConfig); + if (state != LoadFileResult::LOAD_SUCCESS) { + installDefaultModuleConfig(); // Our in RAM copy might now be corrupt } else { - LOG_INFO("Loaded saved channelFile version %d", channelFile.version); + if (moduleConfig.version < DEVICESTATE_MIN_VER) { + LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version); + installDefaultModuleConfig(); + } else { + LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version); + } } - } - state = loadProto(uiconfigFileName, meshtastic_DeviceUIConfig_size, sizeof(meshtastic_DeviceUIConfig), &meshtastic_DeviceUIConfig_msg, &uiconfig); - if (state == LoadFileResult::LOAD_SUCCESS) { - LOG_INFO("Loaded UIConfig"); - } + state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg, + &channelFile); + if (state != LoadFileResult::LOAD_SUCCESS) { + installDefaultChannels(); // Our in RAM copy might now be corrupt + } else { + if (channelFile.version < DEVICESTATE_MIN_VER) { + LOG_WARN("channelFile %d is old, discard", channelFile.version); + installDefaultChannels(); + } else { + LOG_INFO("Loaded saved channelFile version %d", channelFile.version); + } + } - // 2.4.X - configuration migration to update new default intervals - if (moduleConfig.version < 23) { - LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version); - moduleConfig.version = DEVICESTATE_CUR_VER; - if (moduleConfig.telemetry.device_update_interval == 900) - moduleConfig.telemetry.device_update_interval = 0; - if (moduleConfig.telemetry.environment_update_interval == 900) - moduleConfig.telemetry.environment_update_interval = 0; - if (moduleConfig.telemetry.air_quality_interval == 900) - moduleConfig.telemetry.air_quality_interval = 0; - if (moduleConfig.telemetry.power_update_interval == 900) - moduleConfig.telemetry.power_update_interval = 0; - if (moduleConfig.neighbor_info.update_interval == 900) - moduleConfig.neighbor_info.update_interval = 0; - if (moduleConfig.paxcounter.paxcounter_update_interval == 900) - moduleConfig.paxcounter.paxcounter_update_interval = 0; + state = loadProto(uiconfigFileName, meshtastic_DeviceUIConfig_size, sizeof(meshtastic_DeviceUIConfig), + &meshtastic_DeviceUIConfig_msg, &uiconfig); + if (state == LoadFileResult::LOAD_SUCCESS) { + LOG_INFO("Loaded UIConfig"); + } - saveToDisk(SEGMENT_MODULECONFIG); - } + // 2.4.X - configuration migration to update new default intervals + if (moduleConfig.version < 23) { + LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version); + moduleConfig.version = DEVICESTATE_CUR_VER; + if (moduleConfig.telemetry.device_update_interval == 900) + moduleConfig.telemetry.device_update_interval = 0; + if (moduleConfig.telemetry.environment_update_interval == 900) + moduleConfig.telemetry.environment_update_interval = 0; + if (moduleConfig.telemetry.air_quality_interval == 900) + moduleConfig.telemetry.air_quality_interval = 0; + if (moduleConfig.telemetry.power_update_interval == 900) + moduleConfig.telemetry.power_update_interval = 0; + if (moduleConfig.neighbor_info.update_interval == 900) + moduleConfig.neighbor_info.update_interval = 0; + if (moduleConfig.paxcounter.paxcounter_update_interval == 900) + moduleConfig.paxcounter.paxcounter_update_interval = 0; + + saveToDisk(SEGMENT_MODULECONFIG); + } #if ARCH_PORTDUINO - // set any config overrides - if (portduino_config.has_configDisplayMode) { - config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; - } + // set any config overrides + if (portduino_config.has_configDisplayMode) { + config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; + } #endif } /** Save a protobuf from a file, return true for success */ -bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic) { - bool okay = false; +bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, + bool fullAtomic) +{ + bool okay = false; #ifdef FSCom - auto f = SafeFile(filename, fullAtomic); + auto f = SafeFile(filename, fullAtomic); - LOG_INFO("Save %s", filename); - pb_ostream_t stream = {&writecb, static_cast(&f), protoSize}; + LOG_INFO("Save %s", filename); + pb_ostream_t stream = {&writecb, static_cast(&f), protoSize}; - if (!pb_encode(&stream, fields, dest_struct)) { - LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); - } else { - okay = true; - } + if (!pb_encode(&stream, fields, dest_struct)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + } else { + okay = true; + } - bool writeSucceeded = f.close(); + bool writeSucceeded = f.close(); - if (!okay || !writeSucceeded) { - LOG_ERROR("Can't write prefs!"); - } + if (!okay || !writeSucceeded) { + LOG_ERROR("Can't write prefs!"); + } #else - LOG_ERROR("ERROR: Filesystem not implemented"); + LOG_ERROR("ERROR: Filesystem not implemented"); #endif - return okay; + return okay; } -bool NodeDB::saveChannelsToDisk() { +bool NodeDB::saveChannelsToDisk() +{ #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); -#endif - return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile); -} - -bool NodeDB::saveDeviceStateToDisk() { -#ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); -#endif - // Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB - // Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of - // this - return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true); -} - -bool NodeDB::saveNodeDatabaseToDisk() { -#ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); -#endif - size_t nodeDatabaseSize; - pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); - return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); -} - -bool NodeDB::saveToDiskNoRetry(int saveWhat) { - bool success = true; -#ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); -#endif - if (saveWhat & SEGMENT_CONFIG) { - config.has_device = true; - config.has_display = true; - config.has_lora = true; - config.has_position = true; - config.has_power = true; - config.has_network = true; - config.has_bluetooth = true; - config.has_security = true; - - success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config); - } - - if (saveWhat & SEGMENT_MODULECONFIG) { - moduleConfig.has_canned_message = true; - moduleConfig.has_external_notification = true; - moduleConfig.has_mqtt = true; - moduleConfig.has_range_test = true; - moduleConfig.has_serial = true; - moduleConfig.has_store_forward = true; - moduleConfig.has_telemetry = true; - moduleConfig.has_neighbor_info = true; - moduleConfig.has_detection_sensor = true; - moduleConfig.has_ambient_lighting = true; - moduleConfig.has_audio = true; - moduleConfig.has_paxcounter = true; - - success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); - } - - if (saveWhat & SEGMENT_CHANNELS) { - success &= saveChannelsToDisk(); - } - - if (saveWhat & SEGMENT_DEVICESTATE) { - success &= saveDeviceStateToDisk(); - } - - if (saveWhat & SEGMENT_NODEDATABASE) { - success &= saveNodeDatabaseToDisk(); - } - - return success; -} - -bool NodeDB::saveToDisk(int saveWhat) { - LOG_DEBUG("Save to disk %d", saveWhat); - bool success = saveToDiskNoRetry(saveWhat); - - if (!success) { - LOG_ERROR("Failed to save to disk, retrying"); -#ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion spiLock->lock(); - FSCom.format(); + FSCom.mkdir("/prefs"); spiLock->unlock(); - #endif - success = saveToDiskNoRetry(saveWhat); - - RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE - : meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); - } - - return success; + return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile); } -const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex) { - if (readIndex < numMeshNodes) - return &meshNodes->at(readIndex++); - else - return NULL; +bool NodeDB::saveDeviceStateToDisk() +{ +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + // Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB + // Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this + return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true); +} + +bool NodeDB::saveNodeDatabaseToDisk() +{ +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + size_t nodeDatabaseSize; + pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); + return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); +} + +bool NodeDB::saveToDiskNoRetry(int saveWhat) +{ + bool success = true; +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + if (saveWhat & SEGMENT_CONFIG) { + config.has_device = true; + config.has_display = true; + config.has_lora = true; + config.has_position = true; + config.has_power = true; + config.has_network = true; + config.has_bluetooth = true; + config.has_security = true; + + success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config); + } + + if (saveWhat & SEGMENT_MODULECONFIG) { + moduleConfig.has_canned_message = true; + moduleConfig.has_external_notification = true; + moduleConfig.has_mqtt = true; + moduleConfig.has_range_test = true; + moduleConfig.has_serial = true; + moduleConfig.has_store_forward = true; + moduleConfig.has_telemetry = true; + moduleConfig.has_neighbor_info = true; + moduleConfig.has_detection_sensor = true; + moduleConfig.has_ambient_lighting = true; + moduleConfig.has_audio = true; + moduleConfig.has_paxcounter = true; + + success &= + saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); + } + + if (saveWhat & SEGMENT_CHANNELS) { + success &= saveChannelsToDisk(); + } + + if (saveWhat & SEGMENT_DEVICESTATE) { + success &= saveDeviceStateToDisk(); + } + + if (saveWhat & SEGMENT_NODEDATABASE) { + success &= saveNodeDatabaseToDisk(); + } + + return success; +} + +bool NodeDB::saveToDisk(int saveWhat) +{ + LOG_DEBUG("Save to disk %d", saveWhat); + bool success = saveToDiskNoRetry(saveWhat); + + if (!success) { + LOG_ERROR("Failed to save to disk, retrying"); +#ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion + spiLock->lock(); + FSCom.format(); + spiLock->unlock(); + +#endif + success = saveToDiskNoRetry(saveWhat); + + RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE + : meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + } + + return success; +} + +const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex) +{ + if (readIndex < numMeshNodes) + return &meshNodes->at(readIndex++); + else + return NULL; } /// Given a node, return how many seconds in the past (vs now) that we last heard from it -uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n) { - uint32_t now = getTime(); +uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n) +{ + uint32_t now = getTime(); - int delta = (int)(now - n->last_heard); - if (delta < 0) // our clock must be slightly off still - not set from GPS yet - delta = 0; + int delta = (int)(now - n->last_heard); + if (delta < 0) // our clock must be slightly off still - not set from GPS yet + delta = 0; - return delta; + return delta; } -uint32_t sinceReceived(const meshtastic_MeshPacket *p) { - uint32_t now = getTime(); +uint32_t sinceReceived(const meshtastic_MeshPacket *p) +{ + uint32_t now = getTime(); - int delta = (int)(now - p->rx_time); - if (delta < 0) // our clock must be slightly off still - not set from GPS yet - delta = 0; + int delta = (int)(now - p->rx_time); + if (delta < 0) // our clock must be slightly off still - not set from GPS yet + delta = 0; - return delta; + return delta; } -int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) { - // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a - // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware - // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as - // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns - // defaultIfUnknown when hop_start is 0. - if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) - return defaultIfUnknown; // Cannot reliably determine the number of hops. +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) +{ + // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a + // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware + // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as + // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns + // defaultIfUnknown when hop_start is 0. + if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) + return defaultIfUnknown; // Cannot reliably determine the number of hops. - // Guard against invalid values. - if (p.hop_start < p.hop_limit) - return defaultIfUnknown; + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return defaultIfUnknown; - return p.hop_start - p.hop_limit; + return p.hop_start - p.hop_limit; } #define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline -size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) { - size_t numseen = 0; +size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) +{ + size_t numseen = 0; - // FIXME this implementation is kinda expensive - for (int i = 0; i < numMeshNodes; i++) { - if (localOnly && meshNodes->at(i).via_mqtt) - continue; - if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) - numseen++; - } + // FIXME this implementation is kinda expensive + for (int i = 0; i < numMeshNodes; i++) { + if (localOnly && meshNodes->at(i).via_mqtt) + continue; + if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) + numseen++; + } - return numseen; + return numseen; } #include "MeshModule.h" @@ -1529,546 +1588,575 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) { /** Update position info for this node based on received position data */ -void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src) { - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - if (!info) { - return; - } +void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src) +{ + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); + if (!info) { + return; + } - if (src == RX_SRC_LOCAL) { - // Local packet, fully authoritative - LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i, p.altitude); + if (src == RX_SRC_LOCAL) { + // Local packet, fully authoritative + LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i, + p.altitude); - setLocalPosition(p); - info->position = TypeConversions::ConvertToPositionLite(p); - } else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) { - // FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO - // (stop-gap fix for issue #900) - LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time); - info->position.time = p.time; - } else { - // Be careful to only update fields that have been set by the REMOTE sender - // A lot of position reports don't have time populated. In that case, be careful to not blow away the time we - // recorded based on the packet rxTime - // - // FIXME perhaps handle RX_SRC_USER separately? - LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i); + setLocalPosition(p); + info->position = TypeConversions::ConvertToPositionLite(p); + } else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) { + // FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO + // (stop-gap fix for issue #900) + LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time); + info->position.time = p.time; + } else { + // Be careful to only update fields that have been set by the REMOTE sender + // A lot of position reports don't have time populated. In that case, be careful to not blow away the time we + // recorded based on the packet rxTime + // + // FIXME perhaps handle RX_SRC_USER separately? + LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i); - // First, back up fields that we want to protect from overwrite - uint32_t tmp_time = info->position.time; + // First, back up fields that we want to protect from overwrite + uint32_t tmp_time = info->position.time; - // Next, update atomically - info->position = TypeConversions::ConvertToPositionLite(p); + // Next, update atomically + info->position = TypeConversions::ConvertToPositionLite(p); - // Last, restore any fields that may have been overwritten - if (!info->position.time) - info->position.time = tmp_time; - } - info->has_position = true; - updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed + // Last, restore any fields that may have been overwritten + if (!info->position.time) + info->position.time = tmp_time; + } + info->has_position = true; + updateGUIforNode = info; + notifyObservers(true); // Force an update whether or not our node counts have changed } /** Update telemetry info for this node based on received metrics * We only care about device telemetry here */ -void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src) { - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - // Environment metrics should never go to NodeDb but we'll safegaurd anyway - if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) { - return; - } +void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src) +{ + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); + // Environment metrics should never go to NodeDb but we'll safegaurd anyway + if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) { + return; + } - if (src == RX_SRC_LOCAL) { - // Local packet, fully authoritative - LOG_DEBUG("updateTelemetry LOCAL"); - } else { - LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId); - } - info->device_metrics = t.variant.device_metrics; - info->has_device_metrics = true; - updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed + if (src == RX_SRC_LOCAL) { + // Local packet, fully authoritative + LOG_DEBUG("updateTelemetry LOCAL"); + } else { + LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId); + } + info->device_metrics = t.variant.device_metrics; + info->has_device_metrics = true; + updateGUIforNode = info; + notifyObservers(true); // Force an update whether or not our node counts have changed } /** * Update the node database with a new contact */ -void NodeDB::addFromContact(meshtastic_SharedContact contact) { - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num); - if (!info || !contact.has_user) { - return; - } - // If the local node has this node marked as manually verified - // and the client does not, do not allow the client to update the - // saved public key. - if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { - if (contact.user.public_key.size != info->user.public_key.size || - memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { - return; +void NodeDB::addFromContact(meshtastic_SharedContact contact) +{ + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num); + if (!info || !contact.has_user) { + return; } - } - info->num = contact.node_num; - info->has_user = true; - info->user = TypeConversions::ConvertToUserLite(contact.user); - if (contact.should_ignore) { - // If should_ignore is set, - // we need to clear the public key and other cruft, in addition to setting the node as ignored - info->is_ignored = true; - info->is_favorite = false; - info->has_device_metrics = false; - info->has_position = false; - info->user.public_key.size = 0; - info->user.public_key.bytes[0] = 0; - } else { - /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database - * with public keys than the radio holds). However, we don't want to update last_heard just because we sent someone - * a DM! - */ - - /* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed - * nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we - * set the new node as a favorite, and we leave last_heard alone (even if it's zero). - */ - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it - // without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to - // add contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll - // set last_heard to now, so that the add_contact node doesn't immediately get evicted. - info->last_heard = getTime(); + // If the local node has this node marked as manually verified + // and the client does not, do not allow the client to update the + // saved public key. + if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { + if (contact.user.public_key.size != info->user.public_key.size || + memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { + return; + } + } + info->num = contact.node_num; + info->has_user = true; + info->user = TypeConversions::ConvertToUserLite(contact.user); + if (contact.should_ignore) { + // If should_ignore is set, + // we need to clear the public key and other cruft, in addition to setting the node as ignored + info->is_ignored = true; + info->is_favorite = false; + info->has_device_metrics = false; + info->has_position = false; + info->user.public_key.size = 0; + info->user.public_key.bytes[0] = 0; } else { - // Normal case: set is_favorite to prevent expiration. - // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). - info->is_favorite = true; - } + /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with + * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! + */ - // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually - // verified - if (contact.manually_verified) { - info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + /* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed + * nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we set the + * new node as a favorite, and we leave last_heard alone (even if it's zero). + */ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it + // without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add + // contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set + // last_heard to now, so that the add_contact node doesn't immediately get evicted. + info->last_heard = getTime(); + } else { + // Normal case: set is_favorite to prevent expiration. + // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). + info->is_favorite = true; + } + + // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified + if (contact.manually_verified) { + info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + // Mark the node's key as manually verified to indicate trustworthiness. + updateGUIforNode = info; + sortMeshDB(); + notifyObservers(true); // Force an update whether or not our node counts have changed } - // Mark the node's key as manually verified to indicate trustworthiness. - updateGUIforNode = info; - sortMeshDB(); - notifyObservers(true); // Force an update whether or not our node counts have changed - } - saveNodeDatabaseToDisk(); + saveNodeDatabaseToDisk(); } /** Update user info and channel for this node based on received user data */ -bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex) { - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - if (!info) { - return false; - } +bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex) +{ + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); + if (!info) { + return false; + } #if !(MESHTASTIC_EXCLUDE_PKI) - if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { - printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); + if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { + printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); - // Alert the user if a remote node is advertising public key that matches our own - if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) { - if (!duplicateWarned) { - duplicateWarned = true; - char warning[] = "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " - "to regenerate your public keys."; - LOG_WARN(warning, p.long_name); - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, warning, p.long_name); - service->sendClientNotification(cn); - } - return false; + // Alert the user if a remote node is advertising public key that matches our own + if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) { + if (!duplicateWarned) { + duplicateWarned = true; + char warning[] = + "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " + "to regenerate your public keys."; + LOG_WARN(warning, p.long_name); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, warning, p.long_name); + service->sendClientNotification(cn); + } + return false; + } } - } - if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one - // if the key doesn't match, don't update nodeDB at all. - if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { - LOG_WARN("Public Key mismatch, dropping NodeInfo"); - return false; + if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one + // if the key doesn't match, don't update nodeDB at all. + if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { + LOG_WARN("Public Key mismatch, dropping NodeInfo"); + return false; + } + LOG_INFO("Public Key set for node, not updating!"); + } else if (p.public_key.size == 32) { + LOG_INFO("Update Node Pubkey!"); } - LOG_INFO("Public Key set for node, not updating!"); - } else if (p.public_key.size == 32) { - LOG_INFO("Update Node Pubkey!"); - } #endif - // Always ensure user.id is derived from nodeId, regardless of what was received - snprintf(p.id, sizeof(p.id), "!%08x", nodeId); + // Always ensure user.id is derived from nodeId, regardless of what was received + snprintf(p.id, sizeof(p.id), "!%08x", nodeId); - // Both of info->user and p start as filled with zero so I think this is okay - auto lite = TypeConversions::ConvertToUserLite(p); - bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); + // Both of info->user and p start as filled with zero so I think this is okay + auto lite = TypeConversions::ConvertToUserLite(p); + bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); - info->user = lite; - if (info->user.public_key.size == 32) { - printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32); - } - if (nodeId != getNodeNum()) - info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) - LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, info->channel); - info->has_user = true; - - if (changed) { - updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed - - // We just changed something about a User, - // store our DB unless we just did so less than a minute ago - - if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) { - saveToDisk(SEGMENT_NODEDATABASE); - lastNodeDbSave = millis(); - } else { - LOG_DEBUG("Defer NodeDB saveToDisk for now"); + info->user = lite; + if (info->user.public_key.size == 32) { + printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32); } - } + if (nodeId != getNodeNum()) + info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) + LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, + info->channel); + info->has_user = true; - return changed; + if (changed) { + updateGUIforNode = info; + notifyObservers(true); // Force an update whether or not our node counts have changed + + // We just changed something about a User, + // store our DB unless we just did so less than a minute ago + + if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) { + saveToDisk(SEGMENT_NODEDATABASE); + lastNodeDbSave = millis(); + } else { + LOG_DEBUG("Defer NodeDB saveToDisk for now"); + } + } + + return changed; } /// given a subpacket sniffed from the network, update our DB state /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw -void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { - if (mp.from == getNodeNum()) { - LOG_DEBUG("Ignore update from self"); - return; - } - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { - LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); - - meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getFrom(&mp)); - if (!info) { - return; +void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) +{ + if (mp.from == getNodeNum()) { + LOG_DEBUG("Ignore update from self"); + return; } + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { + LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); - if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard - info->last_heard = mp.rx_time; - - if (mp.rx_snr) - info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. - - info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT - - // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway - const int8_t hopsAway = getHopsAway(mp); - if (hopsAway >= 0) { - info->has_hops_away = true; - info->hops_away = hopsAway; - } - sortMeshDB(); - } -} - -void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) { - meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); - if (lite && lite->is_favorite != is_favorite) { - lite->is_favorite = is_favorite; - sortMeshDB(); - saveNodeDatabaseToDisk(); - } -} - -bool NodeDB::isFavorite(uint32_t nodeId) { - // returns true if nodeId is_favorite; false if not or not found - - // NODENUM_BROADCAST will never be in the DB - if (nodeId == NODENUM_BROADCAST) - return false; - - meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); - - if (lite) { - return lite->is_favorite; - } - return false; -} - -bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) { - // This method is logically equivalent to: - // return isFavorite(p.from) || isFavorite(p.to); - // but is more efficient by: - // 1. doing only one pass through the database, instead of two - // 2. exiting early when a favorite is found, or if both from and to have been seen - - if (p.to == NODENUM_BROADCAST) - return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from - - meshtastic_NodeInfoLite *lite = NULL; - - bool seenFrom = false; - bool seenTo = false; - - for (int i = 0; i < numMeshNodes; i++) { - lite = &meshNodes->at(i); - - if (lite->num == p.from) { - if (lite->is_favorite) - return true; - - seenFrom = true; - } - - if (lite->num == p.to) { - if (lite->is_favorite) - return true; - - seenTo = true; - } - - if (seenFrom && seenTo) - return false; // we've seen both, and neither is a favorite, so we can stop searching early - - // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after - // searching all favorited nodes first. - } - - return false; -} - -void NodeDB::pause_sort(bool paused) { sortingIsPaused = paused; } - -void NodeDB::sortMeshDB() { - if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { - lastSort = millis(); - bool changed = true; - while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing - changed = false; - for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 - if (meshNodes->at(i - 1).num == getNodeNum()) { - // noop - } else if (meshNodes->at(i).num == getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there - // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash - std::swap(meshNodes->at(i), meshNodes->at(i - 1)); - changed = true; - } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { - std::swap(meshNodes->at(i), meshNodes->at(i - 1)); - changed = true; - } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { - // noop - } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { - std::swap(meshNodes->at(i), meshNodes->at(i - 1)); - changed = true; + meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getFrom(&mp)); + if (!info) { + return; } - } + + if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard + info->last_heard = mp.rx_time; + + if (mp.rx_snr) + info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. + + info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT + + // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway + const int8_t hopsAway = getHopsAway(mp); + if (hopsAway >= 0) { + info->has_hops_away = true; + info->hops_away = hopsAway; + } + sortMeshDB(); } - LOG_INFO("Sort took %u milliseconds", millis() - lastSort); - } } -uint8_t NodeDB::getMeshNodeChannel(NodeNum n) { - const meshtastic_NodeInfoLite *info = getMeshNode(n); - if (!info) { - return 0; // defaults to PRIMARY - } - return info->channel; +void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) +{ + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + if (lite && lite->is_favorite != is_favorite) { + lite->is_favorite = is_favorite; + sortMeshDB(); + saveNodeDatabaseToDisk(); + } } -std::string NodeDB::getNodeId() const { - char nodeId[16]; - snprintf(nodeId, sizeof(nodeId), "!%08x", myNodeInfo.my_node_num); - return std::string(nodeId); +bool NodeDB::isFavorite(uint32_t nodeId) +{ + // returns true if nodeId is_favorite; false if not or not found + + // NODENUM_BROADCAST will never be in the DB + if (nodeId == NODENUM_BROADCAST) + return false; + + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + + if (lite) { + return lite->is_favorite; + } + return false; +} + +bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) +{ + // This method is logically equivalent to: + // return isFavorite(p.from) || isFavorite(p.to); + // but is more efficient by: + // 1. doing only one pass through the database, instead of two + // 2. exiting early when a favorite is found, or if both from and to have been seen + + if (p.to == NODENUM_BROADCAST) + return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from + + meshtastic_NodeInfoLite *lite = NULL; + + bool seenFrom = false; + bool seenTo = false; + + for (int i = 0; i < numMeshNodes; i++) { + lite = &meshNodes->at(i); + + if (lite->num == p.from) { + if (lite->is_favorite) + return true; + + seenFrom = true; + } + + if (lite->num == p.to) { + if (lite->is_favorite) + return true; + + seenTo = true; + } + + if (seenFrom && seenTo) + return false; // we've seen both, and neither is a favorite, so we can stop searching early + + // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching + // all favorited nodes first. + } + + return false; +} + +void NodeDB::pause_sort(bool paused) +{ + sortingIsPaused = paused; +} + +void NodeDB::sortMeshDB() +{ + if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { + lastSort = millis(); + bool changed = true; + while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing + changed = false; + for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 + if (meshNodes->at(i - 1).num == getNodeNum()) { + // noop + } else if (meshNodes->at(i).num == + getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there + // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { + // noop + } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { + std::swap(meshNodes->at(i), meshNodes->at(i - 1)); + changed = true; + } + } + } + LOG_INFO("Sort took %u milliseconds", millis() - lastSort); + } +} + +uint8_t NodeDB::getMeshNodeChannel(NodeNum n) +{ + const meshtastic_NodeInfoLite *info = getMeshNode(n); + if (!info) { + return 0; // defaults to PRIMARY + } + return info->channel; +} + +std::string NodeDB::getNodeId() const +{ + char nodeId[16]; + snprintf(nodeId, sizeof(nodeId), "!%08x", myNodeInfo.my_node_num); + return std::string(nodeId); } /// Find a node in our DB, return null for missing /// NOTE: This function might be called from an ISR -meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n) { - for (int i = 0; i < numMeshNodes; i++) - if (meshNodes->at(i).num == n) - return &meshNodes->at(i); +meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n) +{ + for (int i = 0; i < numMeshNodes; i++) + if (meshNodes->at(i).num == n) + return &meshNodes->at(i); - return NULL; + return NULL; } // returns true if the maximum number of nodes is reached or we are running low on memory -bool NodeDB::isFull() { return (numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP); } +bool NodeDB::isFull() +{ + return (numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP); +} /// Find a node in our DB, create an empty NodeInfo if missing -meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) { - meshtastic_NodeInfoLite *lite = getMeshNode(n); +meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) +{ + meshtastic_NodeInfoLite *lite = getMeshNode(n); - if (!lite) { - if (isFull()) { - LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes, memGet.getFreeHeap()); - // look for oldest node and erase it - uint32_t oldest = UINT32_MAX; - uint32_t oldestBoring = UINT32_MAX; - int oldestIndex = -1; - int oldestBoringIndex = -1; - for (int i = 1; i < numMeshNodes; i++) { - // Simply the oldest non-favorite, non-ignored, non-verified node - if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && - !(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && meshNodes->at(i).last_heard < oldest) { - oldest = meshNodes->at(i).last_heard; - oldestIndex = i; - } - // The oldest "boring" node - if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && - meshNodes->at(i).last_heard < oldestBoring) { - oldestBoring = meshNodes->at(i).last_heard; - oldestBoringIndex = i; - } - } - // if we found a "boring" node, evict it - if (oldestBoringIndex != -1) { - oldestIndex = oldestBoringIndex; - } + if (!lite) { + if (isFull()) { + LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes, + memGet.getFreeHeap()); + // look for oldest node and erase it + uint32_t oldest = UINT32_MAX; + uint32_t oldestBoring = UINT32_MAX; + int oldestIndex = -1; + int oldestBoringIndex = -1; + for (int i = 1; i < numMeshNodes; i++) { + // Simply the oldest non-favorite, non-ignored, non-verified node + if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && + !(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && + meshNodes->at(i).last_heard < oldest) { + oldest = meshNodes->at(i).last_heard; + oldestIndex = i; + } + // The oldest "boring" node + if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && + meshNodes->at(i).last_heard < oldestBoring) { + oldestBoring = meshNodes->at(i).last_heard; + oldestBoringIndex = i; + } + } + // if we found a "boring" node, evict it + if (oldestBoringIndex != -1) { + oldestIndex = oldestBoringIndex; + } - if (oldestIndex != -1) { - // Shove the remaining nodes down the chain - for (int i = oldestIndex; i < numMeshNodes - 1; i++) { - meshNodes->at(i) = meshNodes->at(i + 1); + if (oldestIndex != -1) { + // Shove the remaining nodes down the chain + for (int i = oldestIndex; i < numMeshNodes - 1; i++) { + meshNodes->at(i) = meshNodes->at(i + 1); + } + (numMeshNodes)--; + } } - (numMeshNodes)--; - } + // add the node at the end + lite = &meshNodes->at((numMeshNodes)++); + + // everything is missing except the nodenum + memset(lite, 0, sizeof(*lite)); + lite->num = n; + LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); } - // add the node at the end - lite = &meshNodes->at((numMeshNodes)++); - // everything is missing except the nodenum - memset(lite, 0, sizeof(*lite)); - lite->num = n; - LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); - } - - return lite; + return lite; } /// Sometimes we will have Position objects that only have a time, so check for /// valid lat/lon -bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) { - return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); +bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) +{ + return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); } /// If we have a node / user and they report is_licensed = true /// we consider them licensed -UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) { - meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); - if (!info || !info->has_user) { - return UserLicenseStatus::NotKnown; - } - return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; +UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) +{ + meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); + if (!info || !info->has_user) { + return UserLicenseStatus::NotKnown; + } + return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; } #if !defined(MESHTASTIC_EXCLUDE_PKI) -bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest) { - if (keyToTest.size == 32) { - uint8_t keyHash[32] = {0}; - memcpy(keyHash, keyToTest.bytes, keyToTest.size); - crypto->hash(keyHash, 32); - for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { - if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) { - return true; - } +bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest) +{ + if (keyToTest.size == 32) { + uint8_t keyHash[32] = {0}; + memcpy(keyHash, keyToTest.bytes, keyToTest.size); + crypto->hash(keyHash, 32); + for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { + if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) { + return true; + } + } } - } - return false; + return false; } #endif -bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { - bool success = false; - lastBackupAttempt = millis(); +bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) +{ + bool success = false; + lastBackupAttempt = millis(); #ifdef FSCom - if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { - meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; - backup.version = DEVICESTATE_CUR_VER; - backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); - backup.has_config = true; - backup.config = config; - backup.has_module_config = true; - backup.module_config = moduleConfig; - backup.has_channels = true; - backup.channels = channelFile; - backup.has_owner = true; - backup.owner = owner; + if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { + meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; + backup.version = DEVICESTATE_CUR_VER; + backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); + backup.has_config = true; + backup.config = config; + backup.has_module_config = true; + backup.module_config = moduleConfig; + backup.has_channels = true; + backup.channels = channelFile; + backup.has_owner = true; + backup.owner = owner; - size_t backupSize; - pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup); + size_t backupSize; + pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup); - spiLock->lock(); - FSCom.mkdir("/backups"); - spiLock->unlock(); - success = saveProto(backupFileName, backupSize, &meshtastic_BackupPreferences_msg, &backup); + spiLock->lock(); + FSCom.mkdir("/backups"); + spiLock->unlock(); + success = saveProto(backupFileName, backupSize, &meshtastic_BackupPreferences_msg, &backup); - if (success) { - LOG_INFO("Saved backup preferences"); - } else { - LOG_ERROR("Failed to save backup preferences to file"); + if (success) { + LOG_INFO("Saved backup preferences"); + } else { + LOG_ERROR("Failed to save backup preferences to file"); + } + } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { + // TODO: After more mainline SD card support } - } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support - } #endif - return success; + return success; } -bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat) { - bool success = false; +bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat) +{ + bool success = false; #ifdef FSCom - if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { - spiLock->lock(); - if (!FSCom.exists(backupFileName)) { - spiLock->unlock(); - LOG_WARN("Could not restore. No backup file found"); - return false; - } else { - spiLock->unlock(); - } - meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; - success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences), &meshtastic_BackupPreferences_msg, - &backup); - if (success) { - if (restoreWhat & SEGMENT_CONFIG) { - config = backup.config; - LOG_DEBUG("Restored config"); - } - if (restoreWhat & SEGMENT_MODULECONFIG) { - moduleConfig = backup.module_config; - LOG_DEBUG("Restored module config"); - } - if (restoreWhat & SEGMENT_DEVICESTATE) { - devicestate.owner = backup.owner; - LOG_DEBUG("Restored device state"); - } - if (restoreWhat & SEGMENT_CHANNELS) { - channelFile = backup.channels; - LOG_DEBUG("Restored channels"); - } + if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { + spiLock->lock(); + if (!FSCom.exists(backupFileName)) { + spiLock->unlock(); + LOG_WARN("Could not restore. No backup file found"); + return false; + } else { + spiLock->unlock(); + } + meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; + success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences), + &meshtastic_BackupPreferences_msg, &backup); + if (success) { + if (restoreWhat & SEGMENT_CONFIG) { + config = backup.config; + LOG_DEBUG("Restored config"); + } + if (restoreWhat & SEGMENT_MODULECONFIG) { + moduleConfig = backup.module_config; + LOG_DEBUG("Restored module config"); + } + if (restoreWhat & SEGMENT_DEVICESTATE) { + devicestate.owner = backup.owner; + LOG_DEBUG("Restored device state"); + } + if (restoreWhat & SEGMENT_CHANNELS) { + channelFile = backup.channels; + LOG_DEBUG("Restored channels"); + } - success = saveToDisk(restoreWhat); - if (success) { - LOG_INFO("Restored preferences from backup"); - } else { - LOG_ERROR("Failed to save restored preferences to flash"); - } - } else { - LOG_ERROR("Failed to restore preferences from backup file"); + success = saveToDisk(restoreWhat); + if (success) { + LOG_INFO("Restored preferences from backup"); + } else { + LOG_ERROR("Failed to save restored preferences to flash"); + } + } else { + LOG_ERROR("Failed to restore preferences from backup file"); + } + } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { + // TODO: After more mainline SD card support } - } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support - } - return success; + return success; #endif } /// Record an error that should be reported via analytics -void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) { - if (filename) { - LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); - } else { - LOG_ERROR("NOTE! Record critical error %d, address=0x%lx", code, address); - } +void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) +{ + if (filename) { + LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); + } else { + LOG_ERROR("NOTE! Record critical error %d, address=0x%lx", code, address); + } - // Record error to DB - error_code = code; - error_address = address; + // Record error to DB + error_code = code; + error_address = address; - // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened + // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened #ifdef ARCH_PORTDUINO - LOG_ERROR("A critical failure occurred, portduino is exiting"); - exit(2); + LOG_ERROR("A critical failure occurred, portduino is exiting"); + exit(2); #endif } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 33f8c0aa2..817e31617 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -20,54 +20,55 @@ #if !defined(MESHTASTIC_EXCLUDE_PKI) // E3B0C442 is the blank hash -static const uint8_t LOW_ENTROPY_HASHES[][32] = {{0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, 0x08, 0x4f, 0x39, 0x63, 0xea, - 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b}, - {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, 0x64, 0xa3, 0xa6, 0xfe, 0x71, - 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9}, - {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, 0x45, 0x5e, 0x2e, 0xbd, 0x16, - 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f}, - {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, 0x91, 0x18, 0x5a, 0x62, 0xe1, - 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d}, - {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, 0x25, 0x64, 0x54, 0x6a, 0x6b, - 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58}, - {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, 0xd9, 0xd1, 0x5d, 0xc4, 0x68, - 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f}, - {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, 0x8e, 0x98, 0x9d, 0xc2, 0x3e, - 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54}, - {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, 0x2b, 0x3a, 0xe2, 0x43, 0x42, - 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49}, - {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, 0xf9, 0x55, 0xcf, 0x30, 0xa0, - 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70}, - {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, 0x6c, 0x0a, 0x9b, 0x75, 0xa4, - 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b}, - {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, 0x64, 0x82, 0xf0, 0x4d, 0x1b, - 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49}, - {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, 0xa3, 0xc3, 0xe1, 0xcf, 0x97, - 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e}, - {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, 0xff, 0x58, 0x17, 0x91, 0xaa, - 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98}, - {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, 0xb4, 0x13, 0xd2, 0x01, 0x2f, - 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0}, - {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, 0x03, 0xe5, 0x80, 0x59, 0xd3, - 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64}, - {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, 0x2D, 0x3C, 0xDF, 0xB1, 0x22, - 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF}, - {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, 0x26, 0x60, 0xF4, 0x6D, 0xCE, - 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B}, - {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, 0x89, 0x36, 0x40, 0xC9, 0x99, - 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5}, - {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, 0xA4, 0xC2, 0x19, 0xA3, 0x83, - 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60}, - {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, 0x89, 0x9D, 0x0E, 0x24, 0xA1, - 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4}, - {0x91, 0x70, 0xb4, 0x7c, 0xfb, 0xff, 0xa0, 0x59, 0x6a, 0x25, 0x1c, 0xa9, 0x9e, 0xe9, 0x43, 0x81, - 0x5d, 0x74, 0xb1, 0xb1, 0x09, 0x28, 0x00, 0x4a, 0xaf, 0xe3, 0xfc, 0xa9, 0x4e, 0x27, 0x76, 0x4c}, - {0x85, 0xfe, 0x7c, 0xec, 0xb6, 0x78, 0x74, 0xc3, 0xec, 0xe1, 0x32, 0x7f, 0xb0, 0xb7, 0x02, 0x74, - 0xf9, 0x23, 0xd8, 0xe7, 0xfa, 0x14, 0xe6, 0xee, 0x66, 0x44, 0xb1, 0x8c, 0xa5, 0x2f, 0x7e, 0xd2}, - {0x8e, 0x66, 0x65, 0x7b, 0x3b, 0x6f, 0x7e, 0xcc, 0x57, 0xb4, 0x57, 0xea, 0xcc, 0x83, 0xf5, 0xaa, - 0xf7, 0x65, 0xa3, 0xce, 0x93, 0x72, 0x13, 0xc1, 0xb6, 0x46, 0x7b, 0x29, 0x45, 0xb5, 0xc8, 0x93}, - {0xcc, 0x11, 0xfb, 0x1a, 0xab, 0xa1, 0x31, 0x87, 0x6a, 0xc6, 0xde, 0x88, 0x87, 0xa9, 0xb9, 0x59, - 0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}}; +static const uint8_t LOW_ENTROPY_HASHES[][32] = { + {0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, 0x08, 0x4f, 0x39, 0x63, 0xea, + 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b}, + {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, 0x64, 0xa3, 0xa6, 0xfe, 0x71, + 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9}, + {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, 0x45, 0x5e, 0x2e, 0xbd, 0x16, + 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f}, + {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, 0x91, 0x18, 0x5a, 0x62, 0xe1, + 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d}, + {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, 0x25, 0x64, 0x54, 0x6a, 0x6b, + 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58}, + {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, 0xd9, 0xd1, 0x5d, 0xc4, 0x68, + 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f}, + {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, 0x8e, 0x98, 0x9d, 0xc2, 0x3e, + 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54}, + {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, 0x2b, 0x3a, 0xe2, 0x43, 0x42, + 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49}, + {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, 0xf9, 0x55, 0xcf, 0x30, 0xa0, + 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70}, + {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, 0x6c, 0x0a, 0x9b, 0x75, 0xa4, + 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b}, + {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, 0x64, 0x82, 0xf0, 0x4d, 0x1b, + 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49}, + {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, 0xa3, 0xc3, 0xe1, 0xcf, 0x97, + 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e}, + {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, 0xff, 0x58, 0x17, 0x91, 0xaa, + 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98}, + {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, 0xb4, 0x13, 0xd2, 0x01, 0x2f, + 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0}, + {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, 0x03, 0xe5, 0x80, 0x59, 0xd3, + 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64}, + {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, 0x2D, 0x3C, 0xDF, 0xB1, 0x22, + 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF}, + {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, 0x26, 0x60, 0xF4, 0x6D, 0xCE, + 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B}, + {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, 0x89, 0x36, 0x40, 0xC9, 0x99, + 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5}, + {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, 0xA4, 0xC2, 0x19, 0xA3, 0x83, + 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60}, + {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, 0x89, 0x9D, 0x0E, 0x24, 0xA1, + 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4}, + {0x91, 0x70, 0xb4, 0x7c, 0xfb, 0xff, 0xa0, 0x59, 0x6a, 0x25, 0x1c, 0xa9, 0x9e, 0xe9, 0x43, 0x81, + 0x5d, 0x74, 0xb1, 0xb1, 0x09, 0x28, 0x00, 0x4a, 0xaf, 0xe3, 0xfc, 0xa9, 0x4e, 0x27, 0x76, 0x4c}, + {0x85, 0xfe, 0x7c, 0xec, 0xb6, 0x78, 0x74, 0xc3, 0xec, 0xe1, 0x32, 0x7f, 0xb0, 0xb7, 0x02, 0x74, + 0xf9, 0x23, 0xd8, 0xe7, 0xfa, 0x14, 0xe6, 0xee, 0x66, 0x44, 0xb1, 0x8c, 0xa5, 0x2f, 0x7e, 0xd2}, + {0x8e, 0x66, 0x65, 0x7b, 0x3b, 0x6f, 0x7e, 0xcc, 0x57, 0xb4, 0x57, 0xea, 0xcc, 0x83, 0xf5, 0xaa, + 0xf7, 0x65, 0xa3, 0xce, 0x93, 0x72, 0x13, 0xc1, 0xb6, 0x46, 0x7b, 0x29, 0x45, 0xb5, 0xc8, 0x93}, + {0xcc, 0x11, 0xfb, 0x1a, 0xab, 0xa1, 0x31, 0x87, 0x6a, 0xc6, 0xde, 0x88, 0x87, 0xa9, 0xb9, 0x59, + 0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}}; static const char LOW_ENTROPY_WARNING[] = "Compromised keys were detected and regenerated."; #endif /* @@ -114,224 +115,233 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p); int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); enum LoadFileResult { - // Successfully opened the file - LOAD_SUCCESS = 1, - // File does not exist - NOT_FOUND = 2, - // Device does not have a filesystem - NO_FILESYSTEM = 3, - // File exists, but could not decode protobufs - DECODE_FAILED = 4, - // File exists, but open failed for some reason - OTHER_FAILURE = 5 + // Successfully opened the file + LOAD_SUCCESS = 1, + // File does not exist + NOT_FOUND = 2, + // Device does not have a filesystem + NO_FILESYSTEM = 3, + // File exists, but could not decode protobufs + DECODE_FAILED = 4, + // File exists, but open failed for some reason + OTHER_FAILURE = 5 }; enum UserLicenseStatus { NotKnown, NotLicensed, Licensed }; -class NodeDB { - // NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt +class NodeDB +{ + // NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt - // A NodeInfo for every node we've seen - // Eventually use a smarter datastructure - // HashMap nodes; - // Note: these two references just point into our static array we serialize to/from disk + // A NodeInfo for every node we've seen + // Eventually use a smarter datastructure + // HashMap nodes; + // Note: these two references just point into our static array we serialize to/from disk -public: - std::vector *meshNodes; - bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled - meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI - Observable newStatus; - pb_size_t numMeshNodes; + public: + std::vector *meshNodes; + bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled + meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI + Observable newStatus; + pb_size_t numMeshNodes; - bool keyIsLowEntropy = false; - bool hasWarned = false; + bool keyIsLowEntropy = false; + bool hasWarned = false; - /// don't do mesh based algorithm for node id assignment (initially) - /// instead just store in flash - possibly even in the initial alpha release do this hack - NodeDB(); + /// don't do mesh based algorithm for node id assignment (initially) + /// instead just store in flash - possibly even in the initial alpha release do this hack + NodeDB(); - /// write to flash - /// @return true if the save was successful - bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); + /// write to flash + /// @return true if the save was successful + bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | + SEGMENT_NODEDATABASE); - /** Reinit radio config if needed, because either: - * a) sometimes a buggy android app might send us bogus settings or - * b) the client set factory_reset - * - * @param factory_reset if true, reset all settings to factory defaults - * @param is_fresh_install set to true after a fresh install, to trigger NodeInfo/Position requests - * @return true if the config was completely reset, in that case, we should send it back to the client - */ - void resetRadioConfig(bool is_fresh_install = false); + /** Reinit radio config if needed, because either: + * a) sometimes a buggy android app might send us bogus settings or + * b) the client set factory_reset + * + * @param factory_reset if true, reset all settings to factory defaults + * @param is_fresh_install set to true after a fresh install, to trigger NodeInfo/Position requests + * @return true if the config was completely reset, in that case, we should send it back to the client + */ + void resetRadioConfig(bool is_fresh_install = false); - /// given a subpacket sniffed from the network, update our DB state - /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw - void updateFrom(const meshtastic_MeshPacket &p); + /// given a subpacket sniffed from the network, update our DB state + /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw + void updateFrom(const meshtastic_MeshPacket &p); - void addFromContact(const meshtastic_SharedContact); + void addFromContact(const meshtastic_SharedContact); - /** Update position info for this node based on received position data - */ - void updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src = RX_SRC_RADIO); + /** Update position info for this node based on received position data + */ + void updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src = RX_SRC_RADIO); - /** Update telemetry info for this node based on received metrics - */ - void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO); + /** Update telemetry info for this node based on received metrics + */ + void updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src = RX_SRC_RADIO); - /** Update user info and channel for this node based on received user data - */ - bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); + /** Update user info and channel for this node based on received user data + */ + bool updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex = 0); - /* - * Sets a node either favorite or unfavorite - */ - void set_favorite(bool is_favorite, uint32_t nodeId); + /* + * Sets a node either favorite or unfavorite + */ + void set_favorite(bool is_favorite, uint32_t nodeId); - /* - * Returns true if the node is in the NodeDB and marked as favorite - */ - bool isFavorite(uint32_t nodeId); + /* + * Returns true if the node is in the NodeDB and marked as favorite + */ + bool isFavorite(uint32_t nodeId); - /* - * Returns true if p->from or p->to is a favorited node - */ - bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); + /* + * Returns true if p->from or p->to is a favorited node + */ + bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); - /** - * Other functions like the node picker can request a pause in the node sorting - */ - void pause_sort(bool paused); + /** + * Other functions like the node picker can request a pause in the node sorting + */ + void pause_sort(bool paused); - /// @return our node number - NodeNum getNodeNum() { return myNodeInfo.my_node_num; } + /// @return our node number + NodeNum getNodeNum() { return myNodeInfo.my_node_num; } - /// @return our node ID as a string in the format "!xxxxxxxx" - std::string getNodeId() const; + /// @return our node ID as a string in the format "!xxxxxxxx" + std::string getNodeId() const; - // @return last byte of a NodeNum, 0xFF if it ended at 0x00 - uint8_t getLastByteOfNodeNum(NodeNum num) { return (uint8_t)((num & 0xFF) ? (num & 0xFF) : 0xFF); } + // @return last byte of a NodeNum, 0xFF if it ended at 0x00 + uint8_t getLastByteOfNodeNum(NodeNum num) { return (uint8_t)((num & 0xFF) ? (num & 0xFF) : 0xFF); } - /// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay - /// for use - // bool handleWantNodeNum(NodeNum n); + /// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay for use + // bool handleWantNodeNum(NodeNum n); - /* void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea - and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum - message. the unconfigured node num would only be used while initially joining the mesh so low odds of conflicting - (especially if we randomly select from a small number of nodenums which can be used temporarily for this operation). - figure out what the lower level mesh sw does if it does conflict? would it be better for people who are replying with - denynode num to just broadcast their denial?) - */ + /* void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea + and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum message. + the unconfigured node num would only be used while initially joining the mesh so low odds of conflicting (especially if we + randomly select from a small number of nodenums which can be used temporarily for this operation). figure out what the lower + level mesh sw does if it does conflict? would it be better for people who are replying with denynode num to just broadcast + their denial?) + */ - // get channel channel index we heard a nodeNum on, defaults to 0 if not found - uint8_t getMeshNodeChannel(NodeNum n); + // get channel channel index we heard a nodeNum on, defaults to 0 if not found + uint8_t getMeshNodeChannel(NodeNum n); - /* Return the number of nodes we've heard from recently (within the last 2 hrs?) - * @param localOnly if true, ignore nodes heard via MQTT - */ - size_t getNumOnlineMeshNodes(bool localOnly = false); + /* Return the number of nodes we've heard from recently (within the last 2 hrs?) + * @param localOnly if true, ignore nodes heard via MQTT + */ + size_t getNumOnlineMeshNodes(bool localOnly = false); - void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false), removeNodeByNum(NodeNum nodeNum); + void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false), + removeNodeByNum(NodeNum nodeNum); - bool factoryReset(bool eraseBleBonds = false); + bool factoryReset(bool eraseBleBonds = false); - LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct); - bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic = true); + LoadFileResult loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, + void *dest_struct); + bool saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, + bool fullAtomic = true); - void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role); + void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role); - const meshtastic_NodeInfoLite *readNextMeshNode(uint32_t &readIndex); + const meshtastic_NodeInfoLite *readNextMeshNode(uint32_t &readIndex); - meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x) { - assert(x < numMeshNodes); - return &meshNodes->at(x); - } - - virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); - size_t getNumMeshNodes() { return numMeshNodes; } - - UserLicenseStatus getLicenseStatus(uint32_t nodeNum); - - size_t getMaxNodesAllocatedSize() { - meshtastic_NodeDatabase emptyNodeDatabase; - emptyNodeDatabase.version = DEVICESTATE_CUR_VER; - size_t nodeDatabaseSize; - pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); - return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); - } - - // returns true if the maximum number of nodes is reached or we are running low on memory - bool isFull(); - - void clearLocalPosition(); - - void setLocalPosition(meshtastic_Position position, bool timeOnly = false) { - if (timeOnly) { - LOG_DEBUG("Set local position time only: time=%u timestamp=%u", position.time, position.timestamp); - localPosition.time = position.time; - localPosition.timestamp = position.timestamp > 0 ? position.timestamp : position.time; - return; + meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x) + { + assert(x < numMeshNodes); + return &meshNodes->at(x); } - LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, position.time, position.timestamp); - localPosition = position; - if (position.latitude_i != 0 || position.longitude_i != 0) { - localPositionUpdatedSinceBoot = true; - } - } - bool hasValidPosition(const meshtastic_NodeInfoLite *n); - bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } + virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); + size_t getNumMeshNodes() { return numMeshNodes; } + + UserLicenseStatus getLicenseStatus(uint32_t nodeNum); + + size_t getMaxNodesAllocatedSize() + { + meshtastic_NodeDatabase emptyNodeDatabase; + emptyNodeDatabase.version = DEVICESTATE_CUR_VER; + size_t nodeDatabaseSize; + pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); + return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); + } + + // returns true if the maximum number of nodes is reached or we are running low on memory + bool isFull(); + + void clearLocalPosition(); + + void setLocalPosition(meshtastic_Position position, bool timeOnly = false) + { + if (timeOnly) { + LOG_DEBUG("Set local position time only: time=%u timestamp=%u", position.time, position.timestamp); + localPosition.time = position.time; + localPosition.timestamp = position.timestamp > 0 ? position.timestamp : position.time; + return; + } + LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, + position.time, position.timestamp); + localPosition = position; + if (position.latitude_i != 0 || position.longitude_i != 0) { + localPositionUpdatedSinceBoot = true; + } + } + + bool hasValidPosition(const meshtastic_NodeInfoLite *n); + bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } #if !defined(MESHTASTIC_EXCLUDE_PKI) - bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); + bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); #endif - bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); - bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, - int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); + bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); + bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, + int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); - /// Notify observers of changes to the DB - void notifyObservers(bool forceUpdate = false) { - // Notify observers of the current node state - const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate); - newStatus.notifyObservers(&status); - } + /// Notify observers of changes to the DB + void notifyObservers(bool forceUpdate = false) + { + // Notify observers of the current node state + const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate); + newStatus.notifyObservers(&status); + } -private: - bool duplicateWarned = false; - bool localPositionUpdatedSinceBoot = false; - uint32_t lastNodeDbSave = 0; // when we last saved our db to flash - uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually - uint32_t lastSort = 0; // When last sorted the nodeDB - /// Find a node in our DB, create an empty NodeInfoLite if missing - meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); + private: + bool duplicateWarned = false; + bool localPositionUpdatedSinceBoot = false; + uint32_t lastNodeDbSave = 0; // when we last saved our db to flash + uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually + uint32_t lastSort = 0; // When last sorted the nodeDB + /// Find a node in our DB, create an empty NodeInfoLite if missing + meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); - /* - * Internal boolean to track sorting paused - */ - bool sortingIsPaused = false; + /* + * Internal boolean to track sorting paused + */ + bool sortingIsPaused = false; - /// pick a provisional nodenum we hope no one is using - void pickNewNodeNum(); + /// pick a provisional nodenum we hope no one is using + void pickNewNodeNum(); - /// read our db from flash - void loadFromDisk(); + /// read our db from flash + void loadFromDisk(); - /// purge db entries without user info - void cleanupMeshDB(); + /// purge db entries without user info + void cleanupMeshDB(); - /// Reinit device state from scratch (not loading from disk) - void installDefaultDeviceState(), installDefaultNodeDatabase(), installDefaultChannels(), installDefaultConfig(bool preserveKey), - installDefaultModuleConfig(); + /// Reinit device state from scratch (not loading from disk) + void installDefaultDeviceState(), installDefaultNodeDatabase(), installDefaultChannels(), + installDefaultConfig(bool preserveKey), installDefaultModuleConfig(); - /// write to flash - /// @return true if the save was successful - bool saveToDiskNoRetry(int saveWhat); + /// write to flash + /// @return true if the save was successful + bool saveToDiskNoRetry(int saveWhat); - bool saveChannelsToDisk(); - bool saveDeviceStateToDisk(); - bool saveNodeDatabaseToDisk(); - void sortMeshDB(); + bool saveChannelsToDisk(); + bool saveDeviceStateToDisk(); + bool saveNodeDatabaseToDisk(); + void sortMeshDB(); }; extern NodeDB *nodeDB; @@ -369,9 +379,9 @@ extern uint32_t error_address; #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) -#define Module_Config_size \ - (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ - ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + ModuleConfig_TelemetryConfig_size + \ - ModuleConfig_size) +#define Module_Config_size \ + (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ + ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \ + ModuleConfig_TelemetryConfig_size + ModuleConfig_size) // Please do not remove this comment, it makes trunk and compiler happy at the same time. diff --git a/src/mesh/PacketCache.cpp b/src/mesh/PacketCache.cpp index e8306760e..0edf81741 100644 --- a/src/mesh/PacketCache.cpp +++ b/src/mesh/PacketCache.cpp @@ -6,233 +6,248 @@ PacketCache packetCache{}; /** * Allocate a new cache entry and copy the packet header and payload into it */ -PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata) { - size_t payload_size = (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size; - PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size + (preserveMetadata ? sizeof(PacketCacheMetadata) : 0)); - if (!e) { - LOG_ERROR("Unable to allocate memory for packet cache entry"); - return NULL; - } - - *e = {}; - e->header.from = p->from; - e->header.to = p->to; - e->header.id = p->id; - e->header.channel = p->channel; - e->header.next_hop = p->next_hop; - e->header.relay_node = p->relay_node; - e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | - (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) | ((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK); - - PacketCacheMetadata m{}; - if (preserveMetadata) { - e->has_metadata = true; - m.rx_rssi = (uint8_t)(p->rx_rssi + 200); - m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f); - m.rx_time = p->rx_time; - m.transport_mechanism = p->transport_mechanism; - m.priority = p->priority; - } - if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { - e->encrypted = true; - e->payload_len = p->encrypted.size; - memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size); - } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - e->encrypted = false; - if (preserveMetadata) { - m.portnum = p->decoded.portnum; - m.want_response = p->decoded.want_response; - m.emoji = p->decoded.emoji; - m.bitfield = p->decoded.bitfield; - if (p->decoded.reply_id) - m.reply_id = p->decoded.reply_id; - else if (p->decoded.request_id) - m.request_id = p->decoded.request_id; +PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata) +{ + size_t payload_size = + (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size; + PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size + + (preserveMetadata ? sizeof(PacketCacheMetadata) : 0)); + if (!e) { + LOG_ERROR("Unable to allocate memory for packet cache entry"); + return NULL; } - e->payload_len = p->decoded.payload.size; - memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size); - } else { - LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant); - free(e); - return NULL; - } - if (preserveMetadata) - memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m)); - size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); - insert(e); - return e; + *e = {}; + e->header.from = p->from; + e->header.to = p->to; + e->header.id = p->id; + e->header.channel = p->channel; + e->header.next_hop = p->next_hop; + e->header.relay_node = p->relay_node; + e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | + (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) | + ((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK); + + PacketCacheMetadata m{}; + if (preserveMetadata) { + e->has_metadata = true; + m.rx_rssi = (uint8_t)(p->rx_rssi + 200); + m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f); + m.rx_time = p->rx_time; + m.transport_mechanism = p->transport_mechanism; + m.priority = p->priority; + } + if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + e->encrypted = true; + e->payload_len = p->encrypted.size; + memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size); + } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + e->encrypted = false; + if (preserveMetadata) { + m.portnum = p->decoded.portnum; + m.want_response = p->decoded.want_response; + m.emoji = p->decoded.emoji; + m.bitfield = p->decoded.bitfield; + if (p->decoded.reply_id) + m.reply_id = p->decoded.reply_id; + else if (p->decoded.request_id) + m.request_id = p->decoded.request_id; + } + e->payload_len = p->decoded.payload.size; + memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size); + } else { + LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant); + free(e); + return NULL; + } + if (preserveMetadata) + memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m)); + + size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); + insert(e); + return e; }; /** * Dump a list of packets into the provided buffer */ -void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries) { - unsigned char *pos = (unsigned char *)dest; - for (size_t i = 0; i < num_entries; i++) { - size_t entry_len = sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0); - memcpy(pos, entries[i], entry_len); - pos += entry_len; - } +void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries) +{ + unsigned char *pos = (unsigned char *)dest; + for (size_t i = 0; i < num_entries; i++) { + size_t entry_len = + sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0); + memcpy(pos, entries[i], entry_len); + pos += entry_len; + } } /** * Calculate the length of buffer needed to dump the specified entries */ -size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries) { - size_t total_size = 0; - for (size_t i = 0; i < num_entries; i++) { - total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len; - if (entries[i]->has_metadata) - total_size += sizeof(PacketCacheMetadata); - } - return total_size; +size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries) +{ + size_t total_size = 0; + for (size_t i = 0; i < num_entries; i++) { + total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len; + if (entries[i]->has_metadata) + total_size += sizeof(PacketCacheMetadata); + } + return total_size; } /** * Find a packet in the cache */ -PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id) { - uint16_t h = PACKET_HASH(from, id); - PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; - while (e) { - if (e->header.from == from && e->header.id == id) - return e; - e = e->next; - } - return NULL; +PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id) +{ + uint16_t h = PACKET_HASH(from, id); + PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; + while (e) { + if (e->header.from == from && e->header.id == id) + return e; + e = e->next; + } + return NULL; } /** * Find a packet in the cache by its hash */ -PacketCacheEntry *PacketCache::find(PacketHash h) { - PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; - while (e) { - if (PACKET_HASH(e->header.from, e->header.id) == h) - return e; - e = e->next; - } - return NULL; +PacketCacheEntry *PacketCache::find(PacketHash h) +{ + PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; + while (e) { + if (PACKET_HASH(e->header.from, e->header.id) == h) + return e; + e = e->next; + } + return NULL; } /** * Load a list of packets from the provided buffer */ -bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries) { - memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries); - unsigned char *pos = (unsigned char *)src; - for (size_t i = 0; i < num_entries; i++) { - PacketCacheEntry e{}; - memcpy(&e, pos, sizeof(PacketCacheEntry)); - size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0); - entries[i] = (PacketCacheEntry *)malloc(entry_len); - size += entry_len; - if (!entries[i]) { - LOG_ERROR("Unable to allocate memory for packet cache entry"); - for (size_t j = 0; j < i; j++) { - size -= sizeof(PacketCacheEntry) + entries[j]->payload_len + (entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0); - free(entries[j]); - entries[j] = NULL; - } - return false; +bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries) +{ + memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries); + unsigned char *pos = (unsigned char *)src; + for (size_t i = 0; i < num_entries; i++) { + PacketCacheEntry e{}; + memcpy(&e, pos, sizeof(PacketCacheEntry)); + size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0); + entries[i] = (PacketCacheEntry *)malloc(entry_len); + size += entry_len; + if (!entries[i]) { + LOG_ERROR("Unable to allocate memory for packet cache entry"); + for (size_t j = 0; j < i; j++) { + size -= sizeof(PacketCacheEntry) + entries[j]->payload_len + + (entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0); + free(entries[j]); + entries[j] = NULL; + } + return false; + } + memcpy(entries[i], pos, entry_len); + pos += entry_len; } - memcpy(entries[i], pos, entry_len); - pos += entry_len; - } - for (size_t i = 0; i < num_entries; i++) - insert(entries[i]); - return true; + for (size_t i = 0; i < num_entries; i++) + insert(entries[i]); + return true; } /** * Copy the cached packet into the provided MeshPacket structure */ -void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p) { - if (!e || !p) - return; +void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p) +{ + if (!e || !p) + return; - *p = {}; - p->from = e->header.from; - p->to = e->header.to; - p->id = e->header.id; - p->channel = e->header.channel; - p->next_hop = e->header.next_hop; - p->relay_node = e->header.relay_node; - p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; - p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK); - p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK); - p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; - p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag; + *p = {}; + p->from = e->header.from; + p->to = e->header.to; + p->id = e->header.id; + p->channel = e->header.channel; + p->next_hop = e->header.next_hop; + p->relay_node = e->header.relay_node; + p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; + p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK); + p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK); + p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; + p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag; - unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry); - PacketCacheMetadata m{}; - if (e->has_metadata) { - memcpy(&m, (payload + e->payload_len), sizeof(m)); - p->rx_rssi = ((int)m.rx_rssi) - 200; - p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f; - p->rx_time = m.rx_time; - p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism; - p->priority = (meshtastic_MeshPacket_Priority)m.priority; - } - if (e->encrypted) { - memcpy(p->encrypted.bytes, payload, e->payload_len); - p->encrypted.size = e->payload_len; - } else { - memcpy(p->decoded.payload.bytes, payload, e->payload_len); - p->decoded.payload.size = e->payload_len; + unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry); + PacketCacheMetadata m{}; if (e->has_metadata) { - // Decrypted-only metadata - p->decoded.portnum = (meshtastic_PortNum)m.portnum; - p->decoded.want_response = m.want_response; - p->decoded.emoji = m.emoji; - p->decoded.bitfield = m.bitfield; - if (m.reply_id) - p->decoded.reply_id = m.reply_id; - else if (m.request_id) - p->decoded.request_id = m.request_id; + memcpy(&m, (payload + e->payload_len), sizeof(m)); + p->rx_rssi = ((int)m.rx_rssi) - 200; + p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f; + p->rx_time = m.rx_time; + p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism; + p->priority = (meshtastic_MeshPacket_Priority)m.priority; + } + if (e->encrypted) { + memcpy(p->encrypted.bytes, payload, e->payload_len); + p->encrypted.size = e->payload_len; + } else { + memcpy(p->decoded.payload.bytes, payload, e->payload_len); + p->decoded.payload.size = e->payload_len; + if (e->has_metadata) { + // Decrypted-only metadata + p->decoded.portnum = (meshtastic_PortNum)m.portnum; + p->decoded.want_response = m.want_response; + p->decoded.emoji = m.emoji; + p->decoded.bitfield = m.bitfield; + if (m.reply_id) + p->decoded.reply_id = m.reply_id; + else if (m.request_id) + p->decoded.request_id = m.request_id; + } } - } } /** * Release a cache entry */ -void PacketCache::release(PacketCacheEntry *e) { - if (!e) - return; - remove(e); - size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); - free(e); +void PacketCache::release(PacketCacheEntry *e) +{ + if (!e) + return; + remove(e); + size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); + free(e); } /** * Insert a new entry into the hash table */ -void PacketCache::insert(PacketCacheEntry *e) { - assert(e); - PacketHash h = PACKET_HASH(e->header.from, e->header.id); - PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; - e->next = *target; - *target = e; - num_entries++; +void PacketCache::insert(PacketCacheEntry *e) +{ + assert(e); + PacketHash h = PACKET_HASH(e->header.from, e->header.id); + PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; + e->next = *target; + *target = e; + num_entries++; } /** * Remove an entry from the hash table */ -void PacketCache::remove(PacketCacheEntry *e) { - assert(e); - PacketHash h = PACKET_HASH(e->header.from, e->header.id); - PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; - while (*target) { - if (*target == e) { - *target = e->next; - e->next = NULL; - num_entries--; - break; - } else { - target = &(*target)->next; +void PacketCache::remove(PacketCacheEntry *e) +{ + assert(e); + PacketHash h = PACKET_HASH(e->header.from, e->header.id); + PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; + while (*target) { + if (*target == e) { + *target = e->next; + e->next = NULL; + num_entries--; + break; + } else { + target = &(*target)->next; + } } - } } \ No newline at end of file diff --git a/src/mesh/PacketCache.h b/src/mesh/PacketCache.h index 89c2dcfa6..85660922b 100644 --- a/src/mesh/PacketCache.h +++ b/src/mesh/PacketCache.h @@ -8,67 +8,68 @@ typedef uint16_t PacketHash; #define PACKET_CACHE_BUCKET(h) (((h >> 12) ^ (h >> 6) ^ h) & 0x3F) // Fold hash down to 6-bit bucket index typedef struct PacketCacheEntry { - PacketCacheEntry *next; - PacketHeader header; - uint16_t payload_len = 0; - union { - uint16_t bitfield; - struct { - uint8_t encrypted : 1; // Payload is encrypted - uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata - uint8_t : 6; // Reserved for future use - uint8_t : 8; // Reserved for future use + PacketCacheEntry *next; + PacketHeader header; + uint16_t payload_len = 0; + union { + uint16_t bitfield; + struct { + uint8_t encrypted : 1; // Payload is encrypted + uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata + uint8_t : 6; // Reserved for future use + uint8_t : 8; // Reserved for future use + }; }; - }; } PacketCacheEntry; typedef struct PacketCacheMetadata { - PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {} - union { - uint32_t _bitfield; - struct { - uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum - uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response - uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji - uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated) - uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200) - uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f) - }; - }; - union { - uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id - uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id - }; - uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time - uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism - struct { - uint8_t _bitfield2; + PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {} union { - uint8_t priority : 7; // meshtastic_MeshPacket::priority - uint8_t reserved : 1; // Reserved for future use + uint32_t _bitfield; + struct { + uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum + uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response + uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji + uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated) + uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200) + uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f) + }; + }; + union { + uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id + uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id + }; + uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time + uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism + struct { + uint8_t _bitfield2; + union { + uint8_t priority : 7; // meshtastic_MeshPacket::priority + uint8_t reserved : 1; // Reserved for future use + }; }; - }; } PacketCacheMetadata; -class PacketCache { -public: - PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata); - static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries); - size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries); - PacketCacheEntry *find(NodeNum from, PacketId id); - PacketCacheEntry *find(PacketHash h); - bool load(void *src, PacketCacheEntry **entries, size_t num_entries); - size_t getNumEntries() { return num_entries; } - size_t getSize() { return size; } - void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p); - void release(PacketCacheEntry *e); +class PacketCache +{ + public: + PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata); + static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries); + size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries); + PacketCacheEntry *find(NodeNum from, PacketId id); + PacketCacheEntry *find(PacketHash h); + bool load(void *src, PacketCacheEntry **entries, size_t num_entries); + size_t getNumEntries() { return num_entries; } + size_t getSize() { return size; } + void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p); + void release(PacketCacheEntry *e); -private: - PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{}; - size_t num_entries = 0; - size_t size = 0; - void insert(PacketCacheEntry *e); - void remove(PacketCacheEntry *e); + private: + PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{}; + size_t num_entries = 0; + size_t size = 0; + void insert(PacketCacheEntry *e); + void remove(PacketCacheEntry *e); }; extern PacketCache packetCache; \ No newline at end of file diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index e3dfd2e44..b4af707ae 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -7,425 +7,454 @@ #endif #include "Throttle.h" -#define PACKETHISTORY_MAX \ - max((u_int32_t)(MAX_NUM_NODES * 2.0), (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 +#define PACKETHISTORY_MAX \ + max((u_int32_t)(MAX_NUM_NODES * 2.0), \ + (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 #define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min #define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging #define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots -PacketHistory::PacketHistory(uint32_t size) - : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members +PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members { - if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense - LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); - size = PACKETHISTORY_MAX; // Use default size if invalid - } + if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense + LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); + size = PACKETHISTORY_MAX; // Use default size if invalid + } - // Allocate memory for the recent packets array - recentPacketsCapacity = size; - recentPackets = new PacketRecord[recentPacketsCapacity]; - if (!recentPackets) { // No logging here, console/log probably uninitialized yet. - LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, sizeof(PacketRecord) * recentPacketsCapacity); - recentPacketsCapacity = 0; // mark allocation fail - return; // return early - } + // Allocate memory for the recent packets array + recentPacketsCapacity = size; + recentPackets = new PacketRecord[recentPacketsCapacity]; + if (!recentPackets) { // No logging here, console/log probably uninitialized yet. + LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, + sizeof(PacketRecord) * recentPacketsCapacity); + recentPacketsCapacity = 0; // mark allocation fail + return; // return early + } - // Initialize the recent packets array to zero - memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); + // Initialize the recent packets array to zero + memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); } -PacketHistory::~PacketHistory() { - recentPacketsCapacity = 0; - delete[] recentPackets; - recentPackets = NULL; +PacketHistory::~PacketHistory() +{ + recentPacketsCapacity = 0; + delete[] recentPackets; + recentPackets = NULL; } /** Update recentPackets and return true if we have already seen this packet */ -bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop, bool *wasUpgraded) { - if (!initOk()) { - LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); - return false; - } - - if (p->id == 0) { -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); -#endif - return false; // Not a floodable message ID, so we don't care - } - - PacketRecord r; - memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero - - // Save basic info from checked packet - r.id = p->id; - r.sender = getFrom(p); // If 0 then use our ID - r.next_hop = p->next_hop; - setHighestHopLimit(r, p->hop_limit); - bool weWillRelay = false; - uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); - if (p->relay_node == ourRelayID) { // If the relay_node is us, store it - weWillRelay = true; - setOurTxHopLimit(r, p->hop_limit); - r.relayed_by[0] = p->relay_node; - } - - r.rxTimeMsec = millis(); // - if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special - r.rxTimeMsec = 1; - -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d " - "wWNH?%d", - r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, - weWereNextHop ? *weWereNextHop : -1); -#endif - - PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array - bool seenRecently = (found != NULL); // If found -> the packet was seen recently - - // Check for hop_limit upgrade scenario - if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) { - LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit, p->hop_limit); - *wasUpgraded = true; - } else if (wasUpgraded) { - *wasUpgraded = false; // Initialize to false if not an upgrade - } - - if (seenRecently) { - if (wasFallback) { - // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed - // already before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need - // to handle it now. - if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && found->next_hop != ourRelayID && - p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && !wasRelayer(ourRelayID, *found) && - !wasRelayer(found->next_hop, - *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", p->from, p->id, p->next_hop, - p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); -#endif - *wasFallback = true; - } else { - // debug log only -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", p->from, p->id, p->next_hop, - p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); -#endif - } +bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop, + bool *wasUpgraded) +{ + if (!initOk()) { + LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); + return false; } - // Check if we were the next hop for this packet - if (weWereNextHop) { - *weWereNextHop = (found->next_hop == ourRelayID); + if (p->id == 0) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", p->from, p->id, p->next_hop, - p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); + LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); #endif + return false; // Not a floodable message ID, so we don't care } - } - if (withUpdate) { - if (found != NULL) { -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", found->sender, found->id, - found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], millis() - found->rxTimeMsec); -#endif - // Only update the relayer if it heard us directly (meaning hopLimit is decreased by 1) - uint8_t startIdx = weWillRelay ? 1 : 0; - if (!weWillRelay) { - bool weWereRelayer = wasRelayer(ourRelayID, *found); - // We were a relayer and the packet came in with a hop limit that is one less than when we sent it out - if (weWereRelayer && (p->hop_limit == getOurTxHopLimit(*found) || p->hop_limit == getOurTxHopLimit(*found) - 1)) { - r.relayed_by[0] = p->relay_node; - startIdx = 1; // Start copying existing relayers from index 1 - } - // keep the original ourTxHopLimit - setOurTxHopLimit(r, getOurTxHopLimit(*found)); - } + PacketRecord r; + memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero - // Preserve the highest hop_limit we've ever seen for this packet so upgrades aren't lost when a later copy has - // fewer hops remaining. - if (getHighestHopLimit(*found) > getHighestHopLimit(r)) - setHighestHopLimit(r, getHighestHopLimit(*found)); - - // Add the existing relayed_by to the new record, avoiding duplicates - for (uint8_t i = 0; i < (NUM_RELAYERS - startIdx); i++) { - if (found->relayed_by[i] == 0) - continue; - - bool exists = false; - for (uint8_t j = 0; j < NUM_RELAYERS; j++) { - if (r.relayed_by[j] == found->relayed_by[i]) { - exists = true; - break; - } - } - - if (!exists) { - r.relayed_by[i + startIdx] = found->relayed_by[i]; - } - } - r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, r.id, r.next_hop, - r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); -#endif - // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this + // Save basic info from checked packet + r.id = p->id; + r.sender = getFrom(p); // If 0 then use our ID + r.next_hop = p->next_hop; + setHighestHopLimit(r, p->hop_limit); + bool weWillRelay = false; + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); + if (p->relay_node == ourRelayID) { // If the relay_node is us, store it + weWillRelay = true; + setOurTxHopLimit(r, p->hop_limit); + r.relayed_by[0] = p->relay_node; } - insert(r); // Insert or update the packet record in the history - } + + r.rxTimeMsec = millis(); // + if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special + r.rxTimeMsec = 1; + #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " - "found?%s seenRecently?%s wUpd?%s", - r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, found ? "YES" : "NO ", - seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); + LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d wWNH?%d", + r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, + weWereNextHop ? *weWereNextHop : -1); #endif - return seenRecently; + PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array + bool seenRecently = (found != NULL); // If found -> the packet was seen recently + + // Check for hop_limit upgrade scenario + if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) { + LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit, + p->hop_limit); + *wasUpgraded = true; + } else if (wasUpgraded) { + *wasUpgraded = false; // Initialize to false if not an upgrade + } + + if (seenRecently) { + if (wasFallback) { + // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already + // before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle + // it now. + if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && + found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && + !wasRelayer(ourRelayID, *found) && + !wasRelayer( + found->next_hop, + *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", + p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif + *wasFallback = true; + } else { + // debug log only +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", + p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif + } + } + + // Check if we were the next hop for this packet + if (weWereNextHop) { + *weWereNextHop = (found->next_hop == ourRelayID); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", + p->from, p->id, p->next_hop, p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); +#endif + } + } + + if (withUpdate) { + if (found != NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", + found->sender, found->id, found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], + millis() - found->rxTimeMsec); +#endif + // Only update the relayer if it heard us directly (meaning hopLimit is decreased by 1) + uint8_t startIdx = weWillRelay ? 1 : 0; + if (!weWillRelay) { + bool weWereRelayer = wasRelayer(ourRelayID, *found); + // We were a relayer and the packet came in with a hop limit that is one less than when we sent it out + if (weWereRelayer && (p->hop_limit == getOurTxHopLimit(*found) || p->hop_limit == getOurTxHopLimit(*found) - 1)) { + r.relayed_by[0] = p->relay_node; + startIdx = 1; // Start copying existing relayers from index 1 + } + // keep the original ourTxHopLimit + setOurTxHopLimit(r, getOurTxHopLimit(*found)); + } + + // Preserve the highest hop_limit we've ever seen for this packet so upgrades aren't lost when a later copy has + // fewer hops remaining. + if (getHighestHopLimit(*found) > getHighestHopLimit(r)) + setHighestHopLimit(r, getHighestHopLimit(*found)); + + // Add the existing relayed_by to the new record, avoiding duplicates + for (uint8_t i = 0; i < (NUM_RELAYERS - startIdx); i++) { + if (found->relayed_by[i] == 0) + continue; + + bool exists = false; + for (uint8_t j = 0; j < NUM_RELAYERS; j++) { + if (r.relayed_by[j] == found->relayed_by[i]) { + exists = true; + break; + } + } + + if (!exists) { + r.relayed_by[i + startIdx] = found->relayed_by[i]; + } + } + r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, + r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); +#endif + // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this + } + insert(r); // Insert or update the packet record in the history + } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " + "found?%s seenRecently?%s wUpd?%s", + r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, + found ? "YES" : "NO ", seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); +#endif + + return seenRecently; } /** Find a packet record in history. * @return pointer to PacketRecord if found, NULL if not found */ -PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) { - if (sender == 0 || id == 0) { +PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) +{ + if (sender == 0 || id == 0) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); + LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); #endif - return NULL; - } - - PacketRecord *it = NULL; - for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { - if (it->id == id && it->sender == sender) { -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, it->id, it->next_hop, - it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), it - recentPackets, recentPacketsCapacity); -#endif - // only the first match is returned, so be careful not to create duplicate entries - return it; // Return pointer to the found record + return NULL; + } + + PacketRecord *it = NULL; + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == id && it->sender == sender) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, + it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), + it - recentPackets, recentPacketsCapacity); +#endif + // only the first match is returned, so be careful not to create duplicate entries + return it; // Return pointer to the found record + } } - } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); + LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); #endif - return NULL; // Not found + return NULL; // Not found } /** Insert/Replace oldest PacketRecord in recentPackets. */ -void PacketHistory::insert(const PacketRecord &r) { - uint32_t now_millis = millis(); // Should not jump with time changes - uint32_t OldtrxTimeMsec = 0; - PacketRecord *tu = NULL; // Will insert here. - PacketRecord *it = NULL; +void PacketHistory::insert(const PacketRecord &r) +{ + uint32_t now_millis = millis(); // Should not jump with time changes + uint32_t OldtrxTimeMsec = 0; + PacketRecord *tu = NULL; // Will insert here. + PacketRecord *it = NULL; - // Find a free, matching or oldest used slot in the recentPackets array - for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { - if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty - tu = it; // Remember the free slot + // Find a free, matching or oldest used slot in the recentPackets array + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty + tu = it; // Remember the free slot #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); + LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); #endif - // We have that, Exit the loop - it = (recentPackets + recentPacketsCapacity); - } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert - tu = it; // Remember the matching slot - OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert + tu = it; // Remember the matching slot + OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); + LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); #endif - // We have that, Exit the loop - it = (recentPackets + recentPacketsCapacity); - } else { - if (it->rxTimeMsec == 0) { - LOG_WARN("Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never " - "happen!", - it->sender, it->id, it - recentPackets, recentPacketsCapacity); - } - if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly - OldtrxTimeMsec = now_millis - it->rxTimeMsec; - tu = it; // remember the oldest packet + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else { + if (it->rxTimeMsec == 0) { + LOG_WARN( + "Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!", + it->sender, it->id, it - recentPackets, recentPacketsCapacity); + } + if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly + OldtrxTimeMsec = now_millis - it->rxTimeMsec; + tu = it; // remember the oldest packet #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); + LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); #endif - } - // keep looking for oldest till entire array is checked + } + // keep looking for oldest till entire array is checked + } } - } - if (tu == NULL) { - LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx - // assert(false); // This should never happen, we should always have at least one packet to clear - return; // Return early if we can't update the history - } + if (tu == NULL) { + LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx + // assert(false); // This should never happen, we should always have at least one packet to clear + return; // Return early if we can't update the history + } #if VERBOSE_PACKET_HISTORY - if (tu->id == 0 && tu->sender == 0) { - LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); - } else if (tu->id == r.id && tu->sender == r.sender) { - LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); - } else { - LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); - } + if (tu->id == 0 && tu->sender == 0) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); + } else if (tu->id == r.id && tu->sender == r.sender) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); + } else { + LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); + } #endif - // If we are reusing a slot, we should warn if the packet is too recent + // If we are reusing a slot, we should warn if the packet is too recent #if RECENT_WARN_AGE > 0 - if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { - if (!(tu->id == r.id && tu->sender == r.sender)) { + if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { + if (!(tu->id == r.id && tu->sender == r.sender)) { #if VERBOSE_PACKET_HISTORY - LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, RECENT_WARN_AGE / 1000); + LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, + RECENT_WARN_AGE / 1000); #endif - } else { - // debug only + } else { + // debug only #if VERBOSE_PACKET_HISTORY - LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", OldtrxTimeMsec / 1000., - RECENT_WARN_AGE / 1000); + LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", + OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000); #endif + } } - } #if PACKET_HISTORY_TRACE_AGING - if (tu->rxTimeMsec != 0) { - LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., - (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); - } else { - LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); - } + if (tu->rxTimeMsec != 0) { + LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., + (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); + } else { + LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); + } #endif #endif #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - recentPackets, - recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2], tu->rxTimeMsec); + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", + tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], + tu->relayed_by[2], tu->rxTimeMsec); #endif - if (r.rxTimeMsec == 0) { + if (r.rxTimeMsec == 0) { #if VERBOSE_PACKET_HISTORY - LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); + LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); #endif - return; // Return early if we can't update the history - } + return; // Return early if we can't update the history + } - *tu = r; // store the packet + *tu = r; // store the packet #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - recentPackets, - recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2], tu->rxTimeMsec); + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", + tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], + tu->relayed_by[2], tu->rxTimeMsec); #endif } /* Check if a certain node was a relayer of a packet in the history given an ID and sender * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) { - if (!initOk()) { - LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); - return false; - } +bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) +{ + if (!initOk()) { + LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); + return false; + } - if (relayer == 0) { + if (relayer == 0) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); #endif - return false; - } + return false; + } - const PacketRecord *found = find(sender, id); + const PacketRecord *found = find(sender, id); - if (found == NULL) { + if (found == NULL) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); #endif - return false; - } + return false; + } #if VERBOSE_PACKET_HISTORY >= 2 - LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", found->sender, found->id, - found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", + found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], + found->relayed_by[2], relayer); #endif - return wasRelayer(relayer, *found, wasSole); + return wasRelayer(relayer, *found, wasSole); } /* Check if a certain node was a relayer of a packet in the history given iterator * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) { - bool found = false; - bool other_present = false; +bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) +{ + bool found = false; + bool other_present = false; - for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { - if (r.relayed_by[i] == relayer) { - found = true; - } else if (r.relayed_by[i] != 0) { - other_present = true; + for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { + if (r.relayed_by[i] == relayer) { + found = true; + } else if (r.relayed_by[i] != 0) { + other_present = true; + } } - } - if (wasSole) { - *wasSole = (found && !other_present); - } + if (wasSole) { + *wasSole = (found && !other_present); + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], r.relayed_by[1], - r.relayed_by[2], relayer); + LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], + r.relayed_by[1], r.relayed_by[2], relayer); #endif - return found; + return found; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender -void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { - if (!initOk()) { - LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); - return; - } +void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +{ + if (!initOk()) { + LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); + return; + } - PacketRecord *found = find(sender, id); - if (found == NULL) { + PacketRecord *found = find(sender, id); + if (found == NULL) { #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); #endif - return; // Nothing to remove - } + return; // Nothing to remove + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, found->relayed_by[0], - found->relayed_by[1], found->relayed_by[2], relayer); + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, + found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif - // nexthop and rxTimeMsec too stay in found entry + // nexthop and rxTimeMsec too stay in found entry - uint8_t j = 0; - uint8_t i = 0; - for (; i < NUM_RELAYERS; i++) { - if (found->relayed_by[i] != relayer) { - found->relayed_by[j] = found->relayed_by[i]; - j++; - } else - found->relayed_by[i] = 0; - } - for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array - found->relayed_by[j] = 0; - } + uint8_t j = 0; + uint8_t i = 0; + for (; i < NUM_RELAYERS; i++) { + if (found->relayed_by[i] != relayer) { + found->relayed_by[j] = found->relayed_by[i]; + j++; + } else + found->relayed_by[i] = 0; + } + for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array + found->relayed_by[j] = 0; + } #if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, found->id, - found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, + found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); #endif } // Getters and setters for hop limit fields packed in hop_limit -inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r) { return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; } - -inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) { - r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); +inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r) +{ + return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; } -inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) { return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; } +inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) +{ + r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); +} -inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) { - r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); +inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) +{ + return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; +} + +inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) +{ + r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); } \ No newline at end of file diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 038d5e31b..5fbad2dc9 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -11,68 +11,68 @@ /** * This is a mixin that adds a record of past packets we have seen */ -class PacketHistory { -private: - struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. - NodeNum sender; - PacketId id; - uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty - uint8_t next_hop; // The next hop asked for this packet - uint8_t hop_limit; // bit 0-2: Highest hop limit observed for this packet, - // bit 3-5: our hop limit when we first transmitted it - uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet - }; // 4B + 4B + 4B + 1B + 1B + 6B = 20B +class PacketHistory +{ + private: + struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. + NodeNum sender; + PacketId id; + uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty + uint8_t next_hop; // The next hop asked for this packet + uint8_t hop_limit; // bit 0-2: Highest hop limit observed for this packet, + // bit 3-5: our hop limit when we first transmitted it + uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet + }; // 4B + 4B + 4B + 1B + 1B + 6B = 20B - uint32_t recentPacketsCapacity = 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. - PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. + uint32_t recentPacketsCapacity = + 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. + PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. - /** Find a packet record in history. - * @param sender NodeNum - * @param id PacketId - * @return pointer to PacketRecord if found, NULL if not found */ - PacketRecord *find(NodeNum sender, PacketId id); + /** Find a packet record in history. + * @param sender NodeNum + * @param id PacketId + * @return pointer to PacketRecord if found, NULL if not found */ + PacketRecord *find(NodeNum sender, PacketId id); - /** Insert/Replace oldest PacketRecord in mx_recentPackets. - * @param r PacketRecord to insert or replace */ - void insert(const PacketRecord &r); // Insert or replace a packet record in the history + /** Insert/Replace oldest PacketRecord in mx_recentPackets. + * @param r PacketRecord to insert or replace */ + void insert(const PacketRecord &r); // Insert or replace a packet record in the history - /* Check if a certain node was a relayer of a packet in the history given iterator - * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet - * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); + /* Check if a certain node was a relayer of a packet in the history given iterator + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); - uint8_t getHighestHopLimit(PacketRecord &r); - void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); - uint8_t getOurTxHopLimit(PacketRecord &r); - void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); + uint8_t getHighestHopLimit(PacketRecord &r); + void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); + uint8_t getOurTxHopLimit(PacketRecord &r); + void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); - PacketHistory(const PacketHistory &); // non construction-copyable - PacketHistory &operator=(const PacketHistory &); // non copyable -public: - explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX - ~PacketHistory(); + PacketHistory(const PacketHistory &); // non construction-copyable + PacketHistory &operator=(const PacketHistory &); // non copyable + public: + explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX + ~PacketHistory(); - /** - * Update recentBroadcasts and return true if we have already seen this packet - * - * @param withUpdate if true and not found we add an entry to recentPackets - * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if - * so - * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true - * if so - * @param wasUpgraded if not nullptr, will be set to true if this packet has better hop_limit than previously seen - */ - bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr, bool *weWereNextHop = nullptr, - bool *wasUpgraded = nullptr); + /** + * Update recentBroadcasts and return true if we have already seen this packet + * + * @param withUpdate if true and not found we add an entry to recentPackets + * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if so + * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true if so + * @param wasUpgraded if not nullptr, will be set to true if this packet has better hop_limit than previously seen + */ + bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr, + bool *weWereNextHop = nullptr, bool *wasUpgraded = nullptr); - /* Check if a certain node was a relayer of a packet in the history given an ID and sender - * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet - * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); + /* Check if a certain node was a relayer of a packet in the history given an ID and sender + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); - // Remove a relayer from the list of relayers of a packet in the history given an ID and sender - void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); + // Remove a relayer from the list of relayers of a packet in the history given an ID and sender + void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); - // To check if the PacketHistory was initialized correctly by constructor - bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } + // To check if the PacketHistory was initialized correctly by constructor + bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } }; diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 47b378136..9050ee89d 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -35,163 +35,171 @@ // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; -PhoneAPI::PhoneAPI() { - lastContactMsec = millis(); - std::fill(std::begin(recentToRadioPacketIds), std::end(recentToRadioPacketIds), 0); +PhoneAPI::PhoneAPI() +{ + lastContactMsec = millis(); + std::fill(std::begin(recentToRadioPacketIds), std::end(recentToRadioPacketIds), 0); } -PhoneAPI::~PhoneAPI() { close(); } - -void PhoneAPI::handleStartConfig() { - // Must be before setting state (because state is how we know !connected) - if (!isConnected()) { - onConnectionChanged(true); - observe(&service->fromNumChanged); -#ifdef FSCom - observe(&xModem.packetReady); -#endif - } - - // Allow subclasses to prepare for high-throughput config traffic - onConfigStart(); - - // even if we were already connected - restart our state machine - if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { - // If client only wants node info, jump directly to sending nodes - state = STATE_SEND_OWN_NODEINFO; - LOG_INFO("Client only wants node info, skipping other config"); - } else { - state = STATE_SEND_MY_INFO; - } - pauseBluetoothLogging = true; - spiLock->lock(); - filesManifest = getFiles("/", 10); - spiLock->unlock(); - LOG_DEBUG("Got %d files in manifest", filesManifest.size()); - - LOG_INFO("Start API client config millis=%u", millis()); - // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone = {}; - nodeInfoQueue.clear(); - } - resetReadIndex(); +PhoneAPI::~PhoneAPI() +{ + close(); } -void PhoneAPI::close() { - LOG_DEBUG("PhoneAPI::close()"); - if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_SERIAL && api_type == TYPE_SERIAL) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_PACKET && api_type == TYPE_PACKET) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_HTTP && api_type == TYPE_HTTP) - service->api_state = service->STATE_DISCONNECTED; - else if (service->api_state == service->STATE_ETH && api_type == TYPE_ETH) - service->api_state = service->STATE_DISCONNECTED; - - if (state != STATE_SEND_NOTHING) { - state = STATE_SEND_NOTHING; - resetReadIndex(); - unobserve(&service->fromNumChanged); +void PhoneAPI::handleStartConfig() +{ + // Must be before setting state (because state is how we know !connected) + if (!isConnected()) { + onConnectionChanged(true); + observe(&service->fromNumChanged); #ifdef FSCom - unobserve(&xModem.packetReady); + observe(&xModem.packetReady); #endif - releasePhonePacket(); // Don't leak phone packets on shutdown - releaseQueueStatusPhonePacket(); - releaseMqttClientProxyPhonePacket(); - releaseClientNotification(); - onConnectionChanged(false); - fromRadioScratch = {}; - toRadioScratch = {}; - // Clear cached node info under lock because NimBLE callbacks can still be draining it. + } + + // Allow subclasses to prepare for high-throughput config traffic + onConfigStart(); + + // even if we were already connected - restart our state machine + if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { + // If client only wants node info, jump directly to sending nodes + state = STATE_SEND_OWN_NODEINFO; + LOG_INFO("Client only wants node info, skipping other config"); + } else { + state = STATE_SEND_MY_INFO; + } + pauseBluetoothLogging = true; + spiLock->lock(); + filesManifest = getFiles("/", 10); + spiLock->unlock(); + LOG_DEBUG("Got %d files in manifest", filesManifest.size()); + + LOG_INFO("Start API client config millis=%u", millis()); + // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone = {}; - nodeInfoQueue.clear(); + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = {}; + nodeInfoQueue.clear(); } - packetForPhone = NULL; - filesManifest.clear(); - fromRadioNum = 0; - config_nonce = 0; - config_state = 0; - pauseBluetoothLogging = false; - heartbeatReceived = false; - } + resetReadIndex(); } -bool PhoneAPI::checkConnectionTimeout() { - if (isConnected()) { - bool newContact = checkIsConnected(); - if (!newContact) { - LOG_INFO("Lost phone connection"); - close(); - return true; +void PhoneAPI::close() +{ + LOG_DEBUG("PhoneAPI::close()"); + if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_SERIAL && api_type == TYPE_SERIAL) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_PACKET && api_type == TYPE_PACKET) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_HTTP && api_type == TYPE_HTTP) + service->api_state = service->STATE_DISCONNECTED; + else if (service->api_state == service->STATE_ETH && api_type == TYPE_ETH) + service->api_state = service->STATE_DISCONNECTED; + + if (state != STATE_SEND_NOTHING) { + state = STATE_SEND_NOTHING; + resetReadIndex(); + unobserve(&service->fromNumChanged); +#ifdef FSCom + unobserve(&xModem.packetReady); +#endif + releasePhonePacket(); // Don't leak phone packets on shutdown + releaseQueueStatusPhonePacket(); + releaseMqttClientProxyPhonePacket(); + releaseClientNotification(); + onConnectionChanged(false); + fromRadioScratch = {}; + toRadioScratch = {}; + // Clear cached node info under lock because NimBLE callbacks can still be draining it. + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = {}; + nodeInfoQueue.clear(); + } + packetForPhone = NULL; + filesManifest.clear(); + fromRadioNum = 0; + config_nonce = 0; + config_state = 0; + pauseBluetoothLogging = false; + heartbeatReceived = false; } - } - return false; +} + +bool PhoneAPI::checkConnectionTimeout() +{ + if (isConnected()) { + bool newContact = checkIsConnected(); + if (!newContact) { + LOG_INFO("Lost phone connection"); + close(); + return true; + } + } + return false; } /** * Handle a ToRadio protobuf */ -bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) { - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep - lastContactMsec = millis(); +bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) +{ + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep + lastContactMsec = millis(); - memset(&toRadioScratch, 0, sizeof(toRadioScratch)); - if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) { - switch (toRadioScratch.which_payload_variant) { - case meshtastic_ToRadio_packet_tag: - return handleToRadioPacket(toRadioScratch.packet); - case meshtastic_ToRadio_want_config_id_tag: - config_nonce = toRadioScratch.want_config_id; - LOG_INFO("Client wants config, nonce=%u", config_nonce); - handleStartConfig(); - break; - case meshtastic_ToRadio_disconnect_tag: - LOG_INFO("Disconnect from phone"); - close(); - break; - case meshtastic_ToRadio_xmodemPacket_tag: - LOG_INFO("Got xmodem packet"); + memset(&toRadioScratch, 0, sizeof(toRadioScratch)); + if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) { + switch (toRadioScratch.which_payload_variant) { + case meshtastic_ToRadio_packet_tag: + return handleToRadioPacket(toRadioScratch.packet); + case meshtastic_ToRadio_want_config_id_tag: + config_nonce = toRadioScratch.want_config_id; + LOG_INFO("Client wants config, nonce=%u", config_nonce); + handleStartConfig(); + break; + case meshtastic_ToRadio_disconnect_tag: + LOG_INFO("Disconnect from phone"); + close(); + break; + case meshtastic_ToRadio_xmodemPacket_tag: + LOG_INFO("Got xmodem packet"); #ifdef FSCom - xModem.handlePacket(toRadioScratch.xmodemPacket); + xModem.handlePacket(toRadioScratch.xmodemPacket); #endif - break; + break; #if !MESHTASTIC_EXCLUDE_MQTT - case meshtastic_ToRadio_mqttClientProxyMessage_tag: - LOG_DEBUG("Got MqttClientProxy message"); - if (state != STATE_SEND_PACKETS) { - LOG_WARN("Ignore MqttClientProxy message while completing config handshake"); - break; - } - if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled && - (channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) { - mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage); - } else { - LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting " - "not enabled"); - } - break; + case meshtastic_ToRadio_mqttClientProxyMessage_tag: + LOG_DEBUG("Got MqttClientProxy message"); + if (state != STATE_SEND_PACKETS) { + LOG_WARN("Ignore MqttClientProxy message while completing config handshake"); + break; + } + if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled && + (channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) { + mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage); + } else { + LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting " + "not enabled"); + } + break; #endif - case meshtastic_ToRadio_heartbeat_tag: - LOG_DEBUG("Got client heartbeat"); - heartbeatReceived = true; - break; - default: - // Ignore nop messages - break; + case meshtastic_ToRadio_heartbeat_tag: + LOG_DEBUG("Got client heartbeat"); + heartbeatReceived = true; + break; + default: + // Ignore nop messages + break; + } + } else { + LOG_ERROR("Error: ignore malformed toradio"); } - } else { - LOG_ERROR("Error: ignore malformed toradio"); - } - return false; + return false; } /** @@ -213,610 +221,624 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) { STATE_SEND_PACKETS // send packets or debug strings */ -size_t PhoneAPI::getFromRadio(uint8_t *buf) { - // Respond to heartbeat by sending queue status - if (heartbeatReceived) { +size_t PhoneAPI::getFromRadio(uint8_t *buf) +{ + // Respond to heartbeat by sending queue status + if (heartbeatReceived) { + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; + fromRadioScratch.queueStatus = router->getQueueStatus(); + heartbeatReceived = false; + size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); + LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes); + return numbytes; + } + + if (!available()) { + return 0; + } + // In case we send a FromRadio packet memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; - fromRadioScratch.queueStatus = router->getQueueStatus(); - heartbeatReceived = false; - size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); - LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes); - return numbytes; - } - if (!available()) { - return 0; - } - // In case we send a FromRadio packet - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + // Advance states as needed + switch (state) { + case STATE_SEND_NOTHING: + LOG_DEBUG("FromRadio=STATE_SEND_NOTHING"); + break; + case STATE_SEND_MY_INFO: + LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO"); + // If the user has specified they don't want our node to share its location, make sure to tell the phone + // app not to send locations on our behalf. + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag; + strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env)); + myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes()); + fromRadioScratch.my_info = myNodeInfo; + state = STATE_SEND_UIDATA; - // Advance states as needed - switch (state) { - case STATE_SEND_NOTHING: - LOG_DEBUG("FromRadio=STATE_SEND_NOTHING"); - break; - case STATE_SEND_MY_INFO: - LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO"); - // If the user has specified they don't want our node to share its location, make sure to tell the phone - // app not to send locations on our behalf. - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag; - strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env)); - myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes()); - fromRadioScratch.my_info = myNodeInfo; - state = STATE_SEND_UIDATA; - - service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon. - break; - - case STATE_SEND_UIDATA: - LOG_INFO("getFromRadio=STATE_SEND_UIDATA"); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag; - fromRadioScratch.deviceuiConfig = uiconfig; - state = STATE_SEND_OWN_NODEINFO; - break; - - case STATE_SEND_OWN_NODEINFO: { - LOG_DEBUG("Send My NodeInfo"); - auto us = nodeDB->readNextMeshNode(readIndex); - if (us) { - auto info = TypeConversions::ConvertToNodeInfo(us); - info.has_hops_away = false; - info.is_favorite = true; - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone = info; - } - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; - fromRadioScratch.node_info = info; - // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS - { - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoForPhone.num = 0; - } - } - if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { - // If client only wants node info, jump directly to sending nodes - state = STATE_SEND_OTHER_NODEINFOS; - onNowHasData(0); - } else { - state = STATE_SEND_METADATA; - } - break; - } - - case STATE_SEND_METADATA: - LOG_DEBUG("Send device metadata"); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag; - fromRadioScratch.metadata = getDeviceMetadata(); - state = STATE_SEND_CHANNELS; - break; - - case STATE_SEND_CHANNELS: - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag; - fromRadioScratch.channel = channels.getByIndex(config_state); - config_state++; - // Advance when we have sent all of our Channels - if (config_state >= MAX_NUM_CHANNELS) { - LOG_DEBUG("Send channels %d", config_state); - state = STATE_SEND_CONFIG; - config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1; - } - break; - - case STATE_SEND_CONFIG: - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; - switch (config_state) { - case meshtastic_Config_device_tag: - LOG_DEBUG("Send config: device"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag; - fromRadioScratch.config.payload_variant.device = config.device; - break; - case meshtastic_Config_position_tag: - LOG_DEBUG("Send config: position"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag; - fromRadioScratch.config.payload_variant.position = config.position; - break; - case meshtastic_Config_power_tag: - LOG_DEBUG("Send config: power"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_power_tag; - fromRadioScratch.config.payload_variant.power = config.power; - fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs; - break; - case meshtastic_Config_network_tag: - LOG_DEBUG("Send config: network"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag; - fromRadioScratch.config.payload_variant.network = config.network; - break; - case meshtastic_Config_display_tag: - LOG_DEBUG("Send config: display"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag; - fromRadioScratch.config.payload_variant.display = config.display; - break; - case meshtastic_Config_lora_tag: - LOG_DEBUG("Send config: lora"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag; - fromRadioScratch.config.payload_variant.lora = config.lora; - break; - case meshtastic_Config_bluetooth_tag: - LOG_DEBUG("Send config: bluetooth"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; - fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; - break; - case meshtastic_Config_security_tag: - LOG_DEBUG("Send config: security"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag; - fromRadioScratch.config.payload_variant.security = config.security; - break; - case meshtastic_Config_sessionkey_tag: - LOG_DEBUG("Send config: sessionkey"); - fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_tag; - break; - case meshtastic_Config_device_ui_tag: // NOOP! - fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_ui_tag; - break; - default: - LOG_ERROR("Unknown config type %d", config_state); - } - // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. - // So even if we internally use 0 to represent 'use default' we still need to send the value we are - // using to the app (so that even old phone apps work with new device loads). - - config_state++; - // Advance when we have sent all of our config objects - if (config_state > (_meshtastic_AdminMessage_ConfigType_MAX + 1)) { - state = STATE_SEND_MODULECONFIG; - config_state = _meshtastic_AdminMessage_ModuleConfigType_MIN + 1; - } - break; - - case STATE_SEND_MODULECONFIG: - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; - switch (config_state) { - case meshtastic_ModuleConfig_mqtt_tag: - LOG_DEBUG("Send module config: mqtt"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; - fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt; - break; - case meshtastic_ModuleConfig_serial_tag: - LOG_DEBUG("Send module config: serial"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag; - fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; - break; - case meshtastic_ModuleConfig_external_notification_tag: - LOG_DEBUG("Send module config: ext notification"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; - fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; - break; - case meshtastic_ModuleConfig_store_forward_tag: - LOG_DEBUG("Send module config: store forward"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; - fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; - break; - case meshtastic_ModuleConfig_range_test_tag: - LOG_DEBUG("Send module config: range test"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; - fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; - break; - case meshtastic_ModuleConfig_telemetry_tag: - LOG_DEBUG("Send module config: telemetry"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; - fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry; - break; - case meshtastic_ModuleConfig_canned_message_tag: - LOG_DEBUG("Send module config: canned message"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; - fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; - break; - case meshtastic_ModuleConfig_audio_tag: - LOG_DEBUG("Send module config: audio"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; - fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; - break; - case meshtastic_ModuleConfig_remote_hardware_tag: - LOG_DEBUG("Send module config: remote hardware"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; - fromRadioScratch.moduleConfig.payload_variant.remote_hardware = moduleConfig.remote_hardware; - break; - case meshtastic_ModuleConfig_neighbor_info_tag: - LOG_DEBUG("Send module config: neighbor info"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; - fromRadioScratch.moduleConfig.payload_variant.neighbor_info = moduleConfig.neighbor_info; - break; - case meshtastic_ModuleConfig_detection_sensor_tag: - LOG_DEBUG("Send module config: detection sensor"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; - fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; - break; - case meshtastic_ModuleConfig_ambient_lighting_tag: - LOG_DEBUG("Send module config: ambient lighting"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; - fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; - break; - case meshtastic_ModuleConfig_paxcounter_tag: - LOG_DEBUG("Send module config: paxcounter"); - fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; - fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; - break; - default: - LOG_ERROR("Unknown module config type %d", config_state); - } - - config_state++; - // Advance when we have sent all of our ModuleConfig objects - if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { - // Handle special nonce behaviors: - // - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest - // - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete - if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { - state = STATE_SEND_FILEMANIFEST; - } else { - state = STATE_SEND_OTHER_NODEINFOS; - onNowHasData(0); - } - config_state = 0; - } - break; - - case STATE_SEND_OTHER_NODEINFOS: { - if (readIndex == 2) { // readIndex==2 will be true for the first non-us node - LOG_INFO("Start sending nodeinfos millis=%u", millis()); - } - - meshtastic_NodeInfo infoToSend = {}; - { - concurrency::LockGuard guard(&nodeInfoMutex); - if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { - // Serve the next cached node without re-reading from the DB iterator. - nodeInfoForPhone = nodeInfoQueue.front(); - nodeInfoQueue.pop_front(); - } - infoToSend = nodeInfoForPhone; - if (infoToSend.num != 0) - nodeInfoForPhone = {}; - } - - if (infoToSend.num != 0) { - // Just in case we stored a different user.id in the past, but should never happen going forward - sprintf(infoToSend.user.id, "!%08x", infoToSend.num); - - // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so - // only uncomment if you really need to: LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", - // nodeInfoForPhone.num, nodeInfoForPhone.last_heard, nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); - - // Occasional progress logging. (readIndex==2 will be true for the first non-us node) - if (readIndex == 2 || readIndex % 20 == 0) { - LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes()); - } - - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; - fromRadioScratch.node_info = infoToSend; - prefetchNodeInfos(); - } else { - LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); - concurrency::LockGuard guard(&nodeInfoMutex); - nodeInfoQueue.clear(); - state = STATE_SEND_FILEMANIFEST; - // Go ahead and send that ID right now - return getFromRadio(buf); - } - break; - } - - case STATE_SEND_FILEMANIFEST: { - LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); - // last element - if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest - config_state = 0; - filesManifest.clear(); - // Skip to complete packet - sendConfigComplete(); - } else { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag; - fromRadioScratch.fileInfo = filesManifest.at(config_state); - LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes); - config_state++; - } - break; - } - - case STATE_SEND_COMPLETE_ID: - sendConfigComplete(); - break; - - case STATE_SEND_PACKETS: - pauseBluetoothLogging = false; - // Do we have a message from the mesh or packet from the local device? - LOG_DEBUG("FromRadio=STATE_SEND_PACKETS"); - if (queueStatusPacketForPhone) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; - fromRadioScratch.queueStatus = *queueStatusPacketForPhone; - releaseQueueStatusPhonePacket(); - } else if (mqttClientProxyMessageForPhone) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag; - fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone; - releaseMqttClientProxyPhonePacket(); - } else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag; - fromRadioScratch.xmodemPacket = xmodemPacketForPhone; - xmodemPacketForPhone = meshtastic_XModem_init_zero; - } else if (clientNotification) { - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag; - fromRadioScratch.clientNotification = *clientNotification; - releaseClientNotification(); - } else if (packetForPhone) { - printPacket("phone downloaded packet", packetForPhone); - - // Encapsulate as a FromRadio packet - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; - fromRadioScratch.packet = *packetForPhone; - releasePhonePacket(); - } - break; - - default: - LOG_ERROR("getFromRadio unexpected state %d", state); - } - - // Do we have a message from the mesh? - if (fromRadioScratch.which_payload_variant != 0) { - // Encapsulate as a FromRadio packet - size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); - - // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer - // for logging (when we are encapsulating with protobufs) - return numbytes; - } - - LOG_DEBUG("No FromRadio packet available"); - return 0; -} - -void PhoneAPI::sendConfigComplete() { - LOG_INFO("Config Send Complete millis=%u", millis()); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; - fromRadioScratch.config_complete_id = config_nonce; - config_nonce = 0; - state = STATE_SEND_PACKETS; - if (api_type == TYPE_BLE) { - service->api_state = service->STATE_BLE; - } else if (api_type == TYPE_WIFI) { - service->api_state = service->STATE_WIFI; - } else if (api_type == TYPE_SERIAL) { - service->api_state = service->STATE_SERIAL; - } else if (api_type == TYPE_PACKET) { - service->api_state = service->STATE_PACKET; - } else if (api_type == TYPE_HTTP) { - service->api_state = service->STATE_HTTP; - } else if (api_type == TYPE_ETH) { - service->api_state = service->STATE_ETH; - } - - // Allow subclasses to know we've entered steady-state so they can lower power consumption - onConfigComplete(); - - pauseBluetoothLogging = false; -} - -void PhoneAPI::releasePhonePacket() { - if (packetForPhone) { - service->releaseToPool(packetForPhone); // we just copied the bytes, so don't need this buffer anymore - packetForPhone = NULL; - } -} - -void PhoneAPI::releaseQueueStatusPhonePacket() { - if (queueStatusPacketForPhone) { - service->releaseQueueStatusToPool(queueStatusPacketForPhone); - queueStatusPacketForPhone = NULL; - } -} - -void PhoneAPI::prefetchNodeInfos() { - bool added = false; - // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. - { - concurrency::LockGuard guard(&nodeInfoMutex); - while (nodeInfoQueue.size() < kNodePrefetchDepth) { - auto nextNode = nodeDB->readNextMeshNode(readIndex); - if (!nextNode) + service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon. break; - auto info = TypeConversions::ConvertToNodeInfo(nextNode); - bool isUs = info.num == nodeDB->getNodeNum(); - info.hops_away = isUs ? 0 : info.hops_away; - info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; - info.snr = isUs ? 0 : info.snr; - info.via_mqtt = isUs ? false : info.via_mqtt; - info.is_favorite = info.is_favorite || isUs; - nodeInfoQueue.push_back(info); - added = true; + case STATE_SEND_UIDATA: + LOG_INFO("getFromRadio=STATE_SEND_UIDATA"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag; + fromRadioScratch.deviceuiConfig = uiconfig; + state = STATE_SEND_OWN_NODEINFO; + break; + + case STATE_SEND_OWN_NODEINFO: { + LOG_DEBUG("Send My NodeInfo"); + auto us = nodeDB->readNextMeshNode(readIndex); + if (us) { + auto info = TypeConversions::ConvertToNodeInfo(us); + info.has_hops_away = false; + info.is_favorite = true; + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = info; + } + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; + fromRadioScratch.node_info = info; + // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone.num = 0; + } + } + if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { + // If client only wants node info, jump directly to sending nodes + state = STATE_SEND_OTHER_NODEINFOS; + onNowHasData(0); + } else { + state = STATE_SEND_METADATA; + } + break; } - } - if (added) - onNowHasData(0); + case STATE_SEND_METADATA: + LOG_DEBUG("Send device metadata"); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag; + fromRadioScratch.metadata = getDeviceMetadata(); + state = STATE_SEND_CHANNELS; + break; + + case STATE_SEND_CHANNELS: + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag; + fromRadioScratch.channel = channels.getByIndex(config_state); + config_state++; + // Advance when we have sent all of our Channels + if (config_state >= MAX_NUM_CHANNELS) { + LOG_DEBUG("Send channels %d", config_state); + state = STATE_SEND_CONFIG; + config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1; + } + break; + + case STATE_SEND_CONFIG: + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; + switch (config_state) { + case meshtastic_Config_device_tag: + LOG_DEBUG("Send config: device"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag; + fromRadioScratch.config.payload_variant.device = config.device; + break; + case meshtastic_Config_position_tag: + LOG_DEBUG("Send config: position"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag; + fromRadioScratch.config.payload_variant.position = config.position; + break; + case meshtastic_Config_power_tag: + LOG_DEBUG("Send config: power"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_power_tag; + fromRadioScratch.config.payload_variant.power = config.power; + fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs; + break; + case meshtastic_Config_network_tag: + LOG_DEBUG("Send config: network"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag; + fromRadioScratch.config.payload_variant.network = config.network; + break; + case meshtastic_Config_display_tag: + LOG_DEBUG("Send config: display"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag; + fromRadioScratch.config.payload_variant.display = config.display; + break; + case meshtastic_Config_lora_tag: + LOG_DEBUG("Send config: lora"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag; + fromRadioScratch.config.payload_variant.lora = config.lora; + break; + case meshtastic_Config_bluetooth_tag: + LOG_DEBUG("Send config: bluetooth"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; + fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; + break; + case meshtastic_Config_security_tag: + LOG_DEBUG("Send config: security"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag; + fromRadioScratch.config.payload_variant.security = config.security; + break; + case meshtastic_Config_sessionkey_tag: + LOG_DEBUG("Send config: sessionkey"); + fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_tag; + break; + case meshtastic_Config_device_ui_tag: // NOOP! + fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_ui_tag; + break; + default: + LOG_ERROR("Unknown config type %d", config_state); + } + // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. + // So even if we internally use 0 to represent 'use default' we still need to send the value we are + // using to the app (so that even old phone apps work with new device loads). + + config_state++; + // Advance when we have sent all of our config objects + if (config_state > (_meshtastic_AdminMessage_ConfigType_MAX + 1)) { + state = STATE_SEND_MODULECONFIG; + config_state = _meshtastic_AdminMessage_ModuleConfigType_MIN + 1; + } + break; + + case STATE_SEND_MODULECONFIG: + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; + switch (config_state) { + case meshtastic_ModuleConfig_mqtt_tag: + LOG_DEBUG("Send module config: mqtt"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; + fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt; + break; + case meshtastic_ModuleConfig_serial_tag: + LOG_DEBUG("Send module config: serial"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag; + fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + LOG_DEBUG("Send module config: ext notification"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; + fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; + break; + case meshtastic_ModuleConfig_store_forward_tag: + LOG_DEBUG("Send module config: store forward"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; + fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; + break; + case meshtastic_ModuleConfig_range_test_tag: + LOG_DEBUG("Send module config: range test"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; + fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; + break; + case meshtastic_ModuleConfig_telemetry_tag: + LOG_DEBUG("Send module config: telemetry"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; + fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + LOG_DEBUG("Send module config: canned message"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; + fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; + break; + case meshtastic_ModuleConfig_audio_tag: + LOG_DEBUG("Send module config: audio"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; + fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + LOG_DEBUG("Send module config: remote hardware"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; + fromRadioScratch.moduleConfig.payload_variant.remote_hardware = moduleConfig.remote_hardware; + break; + case meshtastic_ModuleConfig_neighbor_info_tag: + LOG_DEBUG("Send module config: neighbor info"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; + fromRadioScratch.moduleConfig.payload_variant.neighbor_info = moduleConfig.neighbor_info; + break; + case meshtastic_ModuleConfig_detection_sensor_tag: + LOG_DEBUG("Send module config: detection sensor"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; + fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; + break; + case meshtastic_ModuleConfig_ambient_lighting_tag: + LOG_DEBUG("Send module config: ambient lighting"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; + fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; + break; + case meshtastic_ModuleConfig_paxcounter_tag: + LOG_DEBUG("Send module config: paxcounter"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; + fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; + break; + default: + LOG_ERROR("Unknown module config type %d", config_state); + } + + config_state++; + // Advance when we have sent all of our ModuleConfig objects + if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { + // Handle special nonce behaviors: + // - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest + // - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete + if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { + state = STATE_SEND_FILEMANIFEST; + } else { + state = STATE_SEND_OTHER_NODEINFOS; + onNowHasData(0); + } + config_state = 0; + } + break; + + case STATE_SEND_OTHER_NODEINFOS: { + if (readIndex == 2) { // readIndex==2 will be true for the first non-us node + LOG_INFO("Start sending nodeinfos millis=%u", millis()); + } + + meshtastic_NodeInfo infoToSend = {}; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { + // Serve the next cached node without re-reading from the DB iterator. + nodeInfoForPhone = nodeInfoQueue.front(); + nodeInfoQueue.pop_front(); + } + infoToSend = nodeInfoForPhone; + if (infoToSend.num != 0) + nodeInfoForPhone = {}; + } + + if (infoToSend.num != 0) { + // Just in case we stored a different user.id in the past, but should never happen going forward + sprintf(infoToSend.user.id, "!%08x", infoToSend.num); + + // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only + // uncomment if you really need to: + // LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, + // nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); + + // Occasional progress logging. (readIndex==2 will be true for the first non-us node) + if (readIndex == 2 || readIndex % 20 == 0) { + LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes()); + } + + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; + fromRadioScratch.node_info = infoToSend; + prefetchNodeInfos(); + } else { + LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoQueue.clear(); + state = STATE_SEND_FILEMANIFEST; + // Go ahead and send that ID right now + return getFromRadio(buf); + } + break; + } + + case STATE_SEND_FILEMANIFEST: { + LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); + // last element + if (config_state == filesManifest.size() || + config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest + config_state = 0; + filesManifest.clear(); + // Skip to complete packet + sendConfigComplete(); + } else { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag; + fromRadioScratch.fileInfo = filesManifest.at(config_state); + LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes); + config_state++; + } + break; + } + + case STATE_SEND_COMPLETE_ID: + sendConfigComplete(); + break; + + case STATE_SEND_PACKETS: + pauseBluetoothLogging = false; + // Do we have a message from the mesh or packet from the local device? + LOG_DEBUG("FromRadio=STATE_SEND_PACKETS"); + if (queueStatusPacketForPhone) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; + fromRadioScratch.queueStatus = *queueStatusPacketForPhone; + releaseQueueStatusPhonePacket(); + } else if (mqttClientProxyMessageForPhone) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag; + fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone; + releaseMqttClientProxyPhonePacket(); + } else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag; + fromRadioScratch.xmodemPacket = xmodemPacketForPhone; + xmodemPacketForPhone = meshtastic_XModem_init_zero; + } else if (clientNotification) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag; + fromRadioScratch.clientNotification = *clientNotification; + releaseClientNotification(); + } else if (packetForPhone) { + printPacket("phone downloaded packet", packetForPhone); + + // Encapsulate as a FromRadio packet + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; + fromRadioScratch.packet = *packetForPhone; + releasePhonePacket(); + } + break; + + default: + LOG_ERROR("getFromRadio unexpected state %d", state); + } + + // Do we have a message from the mesh? + if (fromRadioScratch.which_payload_variant != 0) { + // Encapsulate as a FromRadio packet + size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); + + // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer + // for logging (when we are encapsulating with protobufs) + return numbytes; + } + + LOG_DEBUG("No FromRadio packet available"); + return 0; } -void PhoneAPI::releaseMqttClientProxyPhonePacket() { - if (mqttClientProxyMessageForPhone) { - service->releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone); - mqttClientProxyMessageForPhone = NULL; - } +void PhoneAPI::sendConfigComplete() +{ + LOG_INFO("Config Send Complete millis=%u", millis()); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; + fromRadioScratch.config_complete_id = config_nonce; + config_nonce = 0; + state = STATE_SEND_PACKETS; + if (api_type == TYPE_BLE) { + service->api_state = service->STATE_BLE; + } else if (api_type == TYPE_WIFI) { + service->api_state = service->STATE_WIFI; + } else if (api_type == TYPE_SERIAL) { + service->api_state = service->STATE_SERIAL; + } else if (api_type == TYPE_PACKET) { + service->api_state = service->STATE_PACKET; + } else if (api_type == TYPE_HTTP) { + service->api_state = service->STATE_HTTP; + } else if (api_type == TYPE_ETH) { + service->api_state = service->STATE_ETH; + } + + // Allow subclasses to know we've entered steady-state so they can lower power consumption + onConfigComplete(); + + pauseBluetoothLogging = false; } -void PhoneAPI::releaseClientNotification() { - if (clientNotification) { - service->releaseClientNotificationToPool(clientNotification); - clientNotification = NULL; - } +void PhoneAPI::releasePhonePacket() +{ + if (packetForPhone) { + service->releaseToPool(packetForPhone); // we just copied the bytes, so don't need this buffer anymore + packetForPhone = NULL; + } +} + +void PhoneAPI::releaseQueueStatusPhonePacket() +{ + if (queueStatusPacketForPhone) { + service->releaseQueueStatusToPool(queueStatusPacketForPhone); + queueStatusPacketForPhone = NULL; + } +} + +void PhoneAPI::prefetchNodeInfos() +{ + bool added = false; + // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. + { + concurrency::LockGuard guard(&nodeInfoMutex); + while (nodeInfoQueue.size() < kNodePrefetchDepth) { + auto nextNode = nodeDB->readNextMeshNode(readIndex); + if (!nextNode) + break; + + auto info = TypeConversions::ConvertToNodeInfo(nextNode); + bool isUs = info.num == nodeDB->getNodeNum(); + info.hops_away = isUs ? 0 : info.hops_away; + info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; + info.snr = isUs ? 0 : info.snr; + info.via_mqtt = isUs ? false : info.via_mqtt; + info.is_favorite = info.is_favorite || isUs; + nodeInfoQueue.push_back(info); + added = true; + } + } + + if (added) + onNowHasData(0); +} + +void PhoneAPI::releaseMqttClientProxyPhonePacket() +{ + if (mqttClientProxyMessageForPhone) { + service->releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone); + mqttClientProxyMessageForPhone = NULL; + } +} + +void PhoneAPI::releaseClientNotification() +{ + if (clientNotification) { + service->releaseClientNotificationToPool(clientNotification); + clientNotification = NULL; + } } /** * Return true if we have data available to send to the phone */ -bool PhoneAPI::available() { - switch (state) { - case STATE_SEND_NOTHING: - return false; - case STATE_SEND_MY_INFO: - case STATE_SEND_UIDATA: - case STATE_SEND_CHANNELS: - case STATE_SEND_CONFIG: - case STATE_SEND_MODULECONFIG: - case STATE_SEND_METADATA: - case STATE_SEND_OWN_NODEINFO: - case STATE_SEND_FILEMANIFEST: - case STATE_SEND_COMPLETE_ID: - return true; +bool PhoneAPI::available() +{ + switch (state) { + case STATE_SEND_NOTHING: + return false; + case STATE_SEND_MY_INFO: + case STATE_SEND_UIDATA: + case STATE_SEND_CHANNELS: + case STATE_SEND_CONFIG: + case STATE_SEND_MODULECONFIG: + case STATE_SEND_METADATA: + case STATE_SEND_OWN_NODEINFO: + case STATE_SEND_FILEMANIFEST: + case STATE_SEND_COMPLETE_ID: + return true; - case STATE_SEND_OTHER_NODEINFOS: { - concurrency::LockGuard guard(&nodeInfoMutex); - if (nodeInfoQueue.empty()) { - // Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it. - goto PREFETCH_NODEINFO; + case STATE_SEND_OTHER_NODEINFOS: { + concurrency::LockGuard guard(&nodeInfoMutex); + if (nodeInfoQueue.empty()) { + // Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it. + goto PREFETCH_NODEINFO; + } } - } - return true; // Always say we have something, because we might need to advance our state machine - PREFETCH_NODEINFO: - prefetchNodeInfos(); - return true; - case STATE_SEND_PACKETS: { - if (!queueStatusPacketForPhone) - queueStatusPacketForPhone = service->getQueueStatusForPhone(); - if (!mqttClientProxyMessageForPhone) - mqttClientProxyMessageForPhone = service->getMqttClientProxyMessageForPhone(); - if (!clientNotification) - clientNotification = service->getClientNotificationForPhone(); - bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone || !!clientNotification; - if (hasPacket) - return true; + return true; // Always say we have something, because we might need to advance our state machine + PREFETCH_NODEINFO: + prefetchNodeInfos(); + return true; + case STATE_SEND_PACKETS: { + if (!queueStatusPacketForPhone) + queueStatusPacketForPhone = service->getQueueStatusForPhone(); + if (!mqttClientProxyMessageForPhone) + mqttClientProxyMessageForPhone = service->getMqttClientProxyMessageForPhone(); + if (!clientNotification) + clientNotification = service->getClientNotificationForPhone(); + bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone || !!clientNotification; + if (hasPacket) + return true; #ifdef FSCom - if (xmodemPacketForPhone.control == meshtastic_XModem_Control_NUL) - xmodemPacketForPhone = xModem.getForPhone(); - if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { - xModem.resetForPhone(); - return true; - } + if (xmodemPacketForPhone.control == meshtastic_XModem_Control_NUL) + xmodemPacketForPhone = xModem.getForPhone(); + if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { + xModem.resetForPhone(); + return true; + } #endif #ifdef ARCH_ESP32 #if !MESHTASTIC_EXCLUDE_STOREFORWARD - // Check if StoreForward has packets stored for us. - if (!packetForPhone && storeForwardModule) - packetForPhone = storeForwardModule->getForPhone(); + // Check if StoreForward has packets stored for us. + if (!packetForPhone && storeForwardModule) + packetForPhone = storeForwardModule->getForPhone(); #endif #endif - if (!packetForPhone) - packetForPhone = service->getForPhone(); - hasPacket = !!packetForPhone; - return hasPacket; - } - default: - LOG_ERROR("PhoneAPI::available unexpected state %d", state); - } + if (!packetForPhone) + packetForPhone = service->getForPhone(); + hasPacket = !!packetForPhone; + return hasPacket; + } + default: + LOG_ERROR("PhoneAPI::available unexpected state %d", state); + } - return false; + return false; } -void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message) { - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->has_reply_id = true; - cn->reply_id = replyId; - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, message, sizeof(cn->message)); - service->sendClientNotification(cn); +void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message) +{ + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->has_reply_id = true; + cn->reply_id = replyId; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, message, sizeof(cn->message)); + service->sendClientNotification(cn); } -bool PhoneAPI::wasSeenRecently(uint32_t id) { - for (int i = 0; i < 20; i++) { - if (recentToRadioPacketIds[i] == id) { - return true; +bool PhoneAPI::wasSeenRecently(uint32_t id) +{ + for (int i = 0; i < 20; i++) { + if (recentToRadioPacketIds[i] == id) { + return true; + } + if (recentToRadioPacketIds[i] == 0) { + recentToRadioPacketIds[i] = id; + return false; + } } - if (recentToRadioPacketIds[i] == 0) { - recentToRadioPacketIds[i] = id; - return false; - } - } - // If the array is full, shift all elements to the left and add the new id at the end - memmove(recentToRadioPacketIds, recentToRadioPacketIds + 1, (19) * sizeof(uint32_t)); - recentToRadioPacketIds[19] = id; - return false; + // If the array is full, shift all elements to the left and add the new id at the end + memmove(recentToRadioPacketIds, recentToRadioPacketIds + 1, (19) * sizeof(uint32_t)); + recentToRadioPacketIds[19] = id; + return false; } /** * Handle a packet that the phone wants us to send. It is our responsibility to free the packet to the pool */ -bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) { - printPacket("PACKET FROM PHONE", &p); +bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) +{ + printPacket("PACKET FROM PHONE", &p); #if defined(ARCH_PORTDUINO) - // For use with the simulator, we should not ignore duplicate packets from the phone - if (SimRadio::instance == nullptr) + // For use with the simulator, we should not ignore duplicate packets from the phone + if (SimRadio::instance == nullptr) #endif - if (p.id > 0 && wasSeenRecently(p.id)) { - LOG_DEBUG("Ignore packet from phone, already seen recently"); - return false; + if (p.id > 0 && wasSeenRecently(p.id)) { + LOG_DEBUG("Ignore packet from phone, already seen recently"); + return false; + } + + if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) { + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds"); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + return false; + } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, + meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP) && + lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { + // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + // FIXME: Figure out why this continues to happen + // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); + return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p); + // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds"); + return false; } - if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] && - Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) { - LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds"); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - return false; - } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - return false; - } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_ALERT_APP, - meshtastic_PortNum_TELEMETRY_APP) && - lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { - // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset - LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - // FIXME: Figure out why this continues to happen - // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); - return false; - } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && - Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { - LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - meshtastic_QueueStatus qs = router->getQueueStatus(); - service->sendQueueStatusToPhone(qs, 0, p.id); - service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p); - // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 - // seconds"); - return false; - } + // Upgrade traceroute requests from phone to use reliable delivery, matching TraceRouteModule + if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && !isBroadcast(p.to)) { + // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) + p.want_ack = true; + } - // Upgrade traceroute requests from phone to use reliable delivery, matching TraceRouteModule - if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && !isBroadcast(p.to)) { - // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) - p.want_ack = true; - } - - lastPortNumToRadio[p.decoded.portnum] = millis(); - service->handleToRadio(p); - return true; + lastPortNumToRadio[p.decoded.portnum] = millis(); + service->handleToRadio(p); + return true; } /// If the mesh service tells us fromNum has changed, tell the phone -int PhoneAPI::onNotify(uint32_t newValue) { - bool timeout = checkConnectionTimeout(); // a handy place to check if we've heard from the phone (since the BLE - // version doesn't call this from idle) +int PhoneAPI::onNotify(uint32_t newValue) +{ + bool timeout = checkConnectionTimeout(); // a handy place to check if we've heard from the phone (since the BLE version + // doesn't call this from idle) - if (state == STATE_SEND_PACKETS) { - LOG_INFO("Tell client we have new packets %u", newValue); - onNowHasData(newValue); - } else { - LOG_DEBUG("Client not yet interested in packets (state=%d)", state); - } + if (state == STATE_SEND_PACKETS) { + LOG_INFO("Tell client we have new packets %u", newValue); + onNowHasData(newValue); + } else { + LOG_DEBUG("Client not yet interested in packets (state=%d)", state); + } - return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one + return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one } diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 9a6edf764..7f79b5792 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -32,171 +32,172 @@ * Eventually there should be once instance of this class for each live connection (because it has a bit of state * for that connection) */ -class PhoneAPI : public Observer // FIXME, we shouldn't be inheriting from Observer, instead use - // CallbackObserver as a member +class PhoneAPI + : public Observer // FIXME, we shouldn't be inheriting from Observer, instead use CallbackObserver as a member { - enum State { - STATE_SEND_NOTHING, // Initial state, don't send anything until the client starts asking for config - STATE_SEND_UIDATA, // send stored data for device-ui - STATE_SEND_MY_INFO, // send our my info record - STATE_SEND_OWN_NODEINFO, - STATE_SEND_METADATA, - STATE_SEND_CHANNELS, // Send all channels - STATE_SEND_CONFIG, // Replacement for the old Radioconfig - STATE_SEND_MODULECONFIG, // Send Module specific config - STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client - STATE_SEND_FILEMANIFEST, // Send file manifest - STATE_SEND_COMPLETE_ID, - STATE_SEND_PACKETS // send packets or debug strings - }; + enum State { + STATE_SEND_NOTHING, // Initial state, don't send anything until the client starts asking for config + STATE_SEND_UIDATA, // send stored data for device-ui + STATE_SEND_MY_INFO, // send our my info record + STATE_SEND_OWN_NODEINFO, + STATE_SEND_METADATA, + STATE_SEND_CHANNELS, // Send all channels + STATE_SEND_CONFIG, // Replacement for the old Radioconfig + STATE_SEND_MODULECONFIG, // Send Module specific config + STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client + STATE_SEND_FILEMANIFEST, // Send file manifest + STATE_SEND_COMPLETE_ID, + STATE_SEND_PACKETS // send packets or debug strings + }; - State state = STATE_SEND_NOTHING; + State state = STATE_SEND_NOTHING; - uint8_t config_state = 0; + uint8_t config_state = 0; - // Hashmap of timestamps for last time we received a packet on the API per portnum - std::unordered_map lastPortNumToRadio; - uint32_t recentToRadioPacketIds[20]; // Last 20 ToRadio MeshPacket IDs we have seen + // Hashmap of timestamps for last time we received a packet on the API per portnum + std::unordered_map lastPortNumToRadio; + uint32_t recentToRadioPacketIds[20]; // Last 20 ToRadio MeshPacket IDs we have seen - /** - * Each packet sent to the phone has an incrementing count - */ - uint32_t fromRadioNum = 0; + /** + * Each packet sent to the phone has an incrementing count + */ + uint32_t fromRadioNum = 0; - /// We temporarily keep the packet here between the call to available and getFromRadio. We will free it after the - /// phone downloads it - meshtastic_MeshPacket *packetForPhone = NULL; + /// We temporarily keep the packet here between the call to available and getFromRadio. We will free it after the phone + /// downloads it + meshtastic_MeshPacket *packetForPhone = NULL; - // file transfer packets destined for phone. Push it to the queue then free it. - meshtastic_XModem xmodemPacketForPhone = meshtastic_XModem_init_zero; + // file transfer packets destined for phone. Push it to the queue then free it. + meshtastic_XModem xmodemPacketForPhone = meshtastic_XModem_init_zero; - // Keep QueueStatus packet just as packetForPhone - meshtastic_QueueStatus *queueStatusPacketForPhone = NULL; + // Keep QueueStatus packet just as packetForPhone + meshtastic_QueueStatus *queueStatusPacketForPhone = NULL; - // Keep MqttClientProxyMessage packet just as packetForPhone - meshtastic_MqttClientProxyMessage *mqttClientProxyMessageForPhone = NULL; + // Keep MqttClientProxyMessage packet just as packetForPhone + meshtastic_MqttClientProxyMessage *mqttClientProxyMessageForPhone = NULL; - // Keep ClientNotification packet just as packetForPhone - meshtastic_ClientNotification *clientNotification = NULL; + // Keep ClientNotification packet just as packetForPhone + meshtastic_ClientNotification *clientNotification = NULL; - /// We temporarily keep the nodeInfo here between the call to available and getFromRadio - meshtastic_NodeInfo nodeInfoForPhone = meshtastic_NodeInfo_init_default; - // Prefetched node info entries ready for immediate transmission to the phone. - std::deque nodeInfoQueue; - // Tunable size of the node info cache so we can keep BLE reads non-blocking. - static constexpr size_t kNodePrefetchDepth = 4; - // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. - concurrency::Lock nodeInfoMutex; + /// We temporarily keep the nodeInfo here between the call to available and getFromRadio + meshtastic_NodeInfo nodeInfoForPhone = meshtastic_NodeInfo_init_default; + // Prefetched node info entries ready for immediate transmission to the phone. + std::deque nodeInfoQueue; + // Tunable size of the node info cache so we can keep BLE reads non-blocking. + static constexpr size_t kNodePrefetchDepth = 4; + // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. + concurrency::Lock nodeInfoMutex; - meshtastic_ToRadio toRadioScratch = {0}; // this is a static scratch object, any data must be copied elsewhere before returning + meshtastic_ToRadio toRadioScratch = { + 0}; // this is a static scratch object, any data must be copied elsewhere before returning - /// Use to ensure that clients don't get confused about old messages from the radio - uint32_t config_nonce = 0; - uint32_t readIndex = 0; + /// Use to ensure that clients don't get confused about old messages from the radio + uint32_t config_nonce = 0; + uint32_t readIndex = 0; - std::vector filesManifest = {}; + std::vector filesManifest = {}; - void resetReadIndex() { readIndex = 0; } + void resetReadIndex() { readIndex = 0; } -public: - PhoneAPI(); + public: + PhoneAPI(); - /// Destructor - calls close() - virtual ~PhoneAPI(); + /// Destructor - calls close() + virtual ~PhoneAPI(); - // Call this when the client drops the connection, resets the state to STATE_SEND_NOTHING - // Unregisters our observer. A closed connection **can** be reopened by calling init again. - virtual void close(); + // Call this when the client drops the connection, resets the state to STATE_SEND_NOTHING + // Unregisters our observer. A closed connection **can** be reopened by calling init again. + virtual void close(); - /** - * Handle a ToRadio protobuf - * @return true true if a packet was queued for sending (so that caller can yield) - */ - virtual bool handleToRadio(const uint8_t *buf, size_t len); + /** + * Handle a ToRadio protobuf + * @return true true if a packet was queued for sending (so that caller can yield) + */ + virtual bool handleToRadio(const uint8_t *buf, size_t len); - /** - * Send a (client)notification to the phone - */ - virtual void sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message); + /** + * Send a (client)notification to the phone + */ + virtual void sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message); - /** - * Get the next packet we want to send to the phone - * - * We assume buf is at least FromRadio_size bytes long. - * Returns number of bytes in the FromRadio packet (or 0 if no packet available) - */ - size_t getFromRadio(uint8_t *buf); + /** + * Get the next packet we want to send to the phone + * + * We assume buf is at least FromRadio_size bytes long. + * Returns number of bytes in the FromRadio packet (or 0 if no packet available) + */ + size_t getFromRadio(uint8_t *buf); - void sendConfigComplete(); + void sendConfigComplete(); - /** - * Return true if we have data available to send to the phone - */ - bool available(); + /** + * Return true if we have data available to send to the phone + */ + bool available(); - bool isConnected() { return state != STATE_SEND_NOTHING; } - bool isSendingPackets() { return state == STATE_SEND_PACKETS; } + bool isConnected() { return state != STATE_SEND_NOTHING; } + bool isSendingPackets() { return state == STATE_SEND_PACKETS; } -protected: - /// Our fromradio packet while it is being assembled - meshtastic_FromRadio fromRadioScratch = {}; + protected: + /// Our fromradio packet while it is being assembled + meshtastic_FromRadio fromRadioScratch = {}; - /** the last msec we heard from the client on the other side of this link */ - uint32_t lastContactMsec = 0; + /** the last msec we heard from the client on the other side of this link */ + uint32_t lastContactMsec = 0; - /// Hookable to find out when connection changes - virtual void onConnectionChanged(bool connected) {} + /// Hookable to find out when connection changes + virtual void onConnectionChanged(bool connected) {} - /// If we haven't heard from the other side in a while then say not connected. Returns true if timeout occurred - bool checkConnectionTimeout(); + /// If we haven't heard from the other side in a while then say not connected. Returns true if timeout occurred + bool checkConnectionTimeout(); - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() = 0; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() = 0; - /** - * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) - */ - virtual void onNowHasData(uint32_t fromRadioNum) {} + /** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + virtual void onNowHasData(uint32_t fromRadioNum) {} - /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state - /// (i.e. BLE connection params) - virtual void onConfigStart() {} - virtual void onConfigComplete() {} + /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state + /// (i.e. BLE connection params) + virtual void onConfigStart() {} + virtual void onConfigComplete() {} - /// begin a new connection - void handleStartConfig(); + /// begin a new connection + void handleStartConfig(); - enum APIType { - TYPE_NONE, // Initial state, don't send anything until the client starts asking for config - TYPE_BLE, - TYPE_WIFI, - TYPE_SERIAL, - TYPE_PACKET, - TYPE_HTTP, - TYPE_ETH - }; + enum APIType { + TYPE_NONE, // Initial state, don't send anything until the client starts asking for config + TYPE_BLE, + TYPE_WIFI, + TYPE_SERIAL, + TYPE_PACKET, + TYPE_HTTP, + TYPE_ETH + }; - APIType api_type = TYPE_NONE; + APIType api_type = TYPE_NONE; -private: - void releasePhonePacket(); + private: + void releasePhonePacket(); - void releaseQueueStatusPhonePacket(); + void releaseQueueStatusPhonePacket(); - void prefetchNodeInfos(); + void prefetchNodeInfos(); - void releaseMqttClientProxyPhonePacket(); + void releaseMqttClientProxyPhonePacket(); - void releaseClientNotification(); + void releaseClientNotification(); - bool wasSeenRecently(uint32_t packetId); + bool wasSeenRecently(uint32_t packetId); - /** - * Handle a packet that the phone wants us to send. We can write to it but can not keep a reference to it - * @return true true if a packet was queued for sending - */ - bool handleToRadioPacket(meshtastic_MeshPacket &p); + /** + * Handle a packet that the phone wants us to send. We can write to it but can not keep a reference to it + * @return true true if a packet was queued for sending + */ + bool handleToRadioPacket(meshtastic_MeshPacket &p); - /// If the mesh service tells us fromNum has changed, tell the phone - virtual int onNotify(uint32_t newValue) override; + /// If the mesh service tells us fromNum has changed, tell the phone + virtual int onNotify(uint32_t newValue) override; }; diff --git a/src/mesh/PointerQueue.h b/src/mesh/PointerQueue.h index 14fce8aeb..b45245eb8 100644 --- a/src/mesh/PointerQueue.h +++ b/src/mesh/PointerQueue.h @@ -5,23 +5,26 @@ /** * A wrapper for freertos queues that assumes each element is a pointer */ -template class PointerQueue : public TypedQueue { -public: - explicit PointerQueue(int maxElements) : TypedQueue(maxElements) {} +template class PointerQueue : public TypedQueue +{ + public: + explicit PointerQueue(int maxElements) : TypedQueue(maxElements) {} - // returns a ptr or null if the queue was empty - T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) { - T *p; + // returns a ptr or null if the queue was empty + T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) + { + T *p; - return this->dequeue(&p, maxWait) ? p : nullptr; - } + return this->dequeue(&p, maxWait) ? p : nullptr; + } #ifdef HAS_FREE_RTOS - // returns a ptr or null if the queue was empty - T *dequeuePtrFromISR(BaseType_t *higherPriWoken) { - T *p; + // returns a ptr or null if the queue was empty + T *dequeuePtrFromISR(BaseType_t *higherPriWoken) + { + T *p; - return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; - } + return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; + } #endif }; diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 5a9f4038b..725477eae 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -8,108 +8,116 @@ * If you are using protobufs to encode your packets (recommended) you can use this as a baseclass for your module * and avoid a bunch of boilerplate code. */ -template class ProtobufModule : protected SinglePortModule { - const pb_msgdesc_t *fields; +template class ProtobufModule : protected SinglePortModule +{ + const pb_msgdesc_t *fields; -public: - uint16_t numOnlineNodes = 0; - /** Constructor - * name is for debugging output - */ - ProtobufModule(const char *_name, meshtastic_PortNum _ourPortNum, const pb_msgdesc_t *_fields) - : SinglePortModule(_name, _ourPortNum), fields(_fields) {} - -protected: - /** - * Handle a received message, the data field in the message is already decoded and is provided - * - * In general decoded will always be !NULL. But in some special applications (where you have handling packets - * for multiple port numbers, decoding will ONLY be attempted for packets where the portnum matches our expected - * ourPortNum. - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, T *decoded) = 0; - - /** Called to make changes to a particular incoming message - */ - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, T *decoded){}; - - /** - * Return a mesh packet which has been preinited with a particular protobuf data payload and port number. - * You can then send this packet (after customizing any of the payload fields you might need) with - * service->sendToMesh() - */ - meshtastic_MeshPacket *allocDataProtobuf(const T &payload) { - // Update our local node info with our position (even if we don't decide to update anyone else) - meshtastic_MeshPacket *p = allocDataPacket(); - - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), fields, &payload); - // LOG_DEBUG("did encode"); - return p; - } - - /** - * Gets the short name from the sender of the mesh packet - * Returns "???" if unknown sender - */ - const char *getSenderShortName(const meshtastic_MeshPacket &mp) { - auto node = nodeDB->getMeshNode(getFrom(&mp)); - const char *sender = (node) ? node->user.short_name : "???"; - return sender; - } - - int handleStatusUpdate(const meshtastic::Status *arg) { - if (arg->getStatusType() == STATUS_TYPE_NODE) { - numOnlineNodes = nodeStatus->getNumOnline(); - } - return 0; - } - -private: - /** Called to handle a particular incoming message - - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override { - // FIXME - we currently update position data in the DB only if the message was a broadcast or destined to us - // it would be better to update even if the message was destined to others. - - auto &p = mp.decoded; - LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); - - T scratch; - T *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding proto module!"); - // if we can't decode it, nobody can process it! - return ProcessMessage::STOP; - } + public: + uint16_t numOnlineNodes = 0; + /** Constructor + * name is for debugging output + */ + ProtobufModule(const char *_name, meshtastic_PortNum _ourPortNum, const pb_msgdesc_t *_fields) + : SinglePortModule(_name, _ourPortNum), fields(_fields) + { } - return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; - } + protected: + /** + * Handle a received message, the data field in the message is already decoded and is provided + * + * In general decoded will always be !NULL. But in some special applications (where you have handling packets + * for multiple port numbers, decoding will ONLY be attempted for packets where the portnum matches our expected ourPortNum. + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, T *decoded) = 0; - /** Called to alter a particular incoming message - */ - virtual void alterReceived(meshtastic_MeshPacket &mp) override { - T scratch; - T *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { - memset(&scratch, 0, sizeof(scratch)); - const meshtastic_Data &p = mp.decoded; - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding proto module!"); - // if we can't decode it, nobody can process it! - return; - } + /** Called to make changes to a particular incoming message + */ + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, T *decoded){}; - return alterReceivedProtobuf(mp, decoded); + /** + * Return a mesh packet which has been preinited with a particular protobuf data payload and port number. + * You can then send this packet (after customizing any of the payload fields you might need) with + * service->sendToMesh() + */ + meshtastic_MeshPacket *allocDataProtobuf(const T &payload) + { + // Update our local node info with our position (even if we don't decide to update anyone else) + meshtastic_MeshPacket *p = allocDataPacket(); + + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), fields, &payload); + // LOG_DEBUG("did encode"); + return p; + } + + /** + * Gets the short name from the sender of the mesh packet + * Returns "???" if unknown sender + */ + const char *getSenderShortName(const meshtastic_MeshPacket &mp) + { + auto node = nodeDB->getMeshNode(getFrom(&mp)); + const char *sender = (node) ? node->user.short_name : "???"; + return sender; + } + + int handleStatusUpdate(const meshtastic::Status *arg) + { + if (arg->getStatusType() == STATUS_TYPE_NODE) { + numOnlineNodes = nodeStatus->getNumOnline(); + } + return 0; + } + + private: + /** Called to handle a particular incoming message + + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override + { + // FIXME - we currently update position data in the DB only if the message was a broadcast or destined to us + // it would be better to update even if the message was destined to others. + + auto &p = mp.decoded; + LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); + + T scratch; + T *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding proto module!"); + // if we can't decode it, nobody can process it! + return ProcessMessage::STOP; + } + } + + return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; + } + + /** Called to alter a particular incoming message + */ + virtual void alterReceived(meshtastic_MeshPacket &mp) override + { + T scratch; + T *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + const meshtastic_Data &p = mp.decoded; + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding proto module!"); + // if we can't decode it, nobody can process it! + return; + } + + return alterReceivedProtobuf(mp, decoded); + } } - } }; \ No newline at end of file diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 26c8a4c78..da0039d38 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -17,307 +17,325 @@ #endif // if we use 20 we are limited to 1% duty cycle or hw might overheat. For continuous operation set a limit of 17 -// In theory up to 27 dBm is possible, but the modules installed in most radios can cope with a max of 20. So BIG -// WARNING if you set power to something higher than 17 or 20 you might fry your board. +// In theory up to 27 dBm is possible, but the modules installed in most radios can cope with a max of 20. So BIG WARNING +// if you set power to something higher than 17 or 20 you might fry your board. #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) // Structure to hold DAC and DB values typedef struct { - uint8_t dac; - uint8_t db; + uint8_t dac; + uint8_t db; } DACDB; // Interpolation function -DACDB interpolate(uint8_t dbm, uint8_t dbm1, uint8_t dbm2, DACDB val1, DACDB val2) { - DACDB result; - double fraction = (double)(dbm - dbm1) / (dbm2 - dbm1); - result.dac = (uint8_t)(val1.dac + fraction * (val2.dac - val1.dac)); - result.db = (uint8_t)(val1.db + fraction * (val2.db - val1.db)); - return result; +DACDB interpolate(uint8_t dbm, uint8_t dbm1, uint8_t dbm2, DACDB val1, DACDB val2) +{ + DACDB result; + double fraction = (double)(dbm - dbm1) / (dbm2 - dbm1); + result.dac = (uint8_t)(val1.dac + fraction * (val2.dac - val1.dac)); + result.db = (uint8_t)(val1.db + fraction * (val2.db - val1.db)); + return result; } // Function to find the correct DAC and DB values based on dBm using interpolation -DACDB getDACandDB(uint8_t dbm) { - // Predefined values - static const struct { - uint8_t dbm; - DACDB values; - } -#ifdef RADIOMASTER_900_BANDIT_NANO - dbmToDACDB[] = { - {20, {168, 2}}, // 100mW - {24, {148, 6}}, // 250mW - {27, {128, 9}}, // 500mW - {30, {90, 12}} // 1000mW - }; -#endif -#ifdef RADIOMASTER_900_BANDIT - dbmToDACDB[] = { - {20, {165, 2}}, // 100mW - {24, {155, 6}}, // 250mW - {27, {142, 9}}, // 500mW - {30, {110, 10}} // 1000mW - }; -#endif - const int numValues = sizeof(dbmToDACDB) / sizeof(dbmToDACDB[0]); - - // Find the interval dbm falls within and interpolate - for (int i = 0; i < numValues - 1; i++) { - if (dbm >= dbmToDACDB[i].dbm && dbm <= dbmToDACDB[i + 1].dbm) { - return interpolate(dbm, dbmToDACDB[i].dbm, dbmToDACDB[i + 1].dbm, dbmToDACDB[i].values, dbmToDACDB[i + 1].values); +DACDB getDACandDB(uint8_t dbm) +{ + // Predefined values + static const struct { + uint8_t dbm; + DACDB values; } - } - - // Return a default value if no match is found and default to 100mW #ifdef RADIOMASTER_900_BANDIT_NANO - DACDB defaultValue = {168, 2}; + dbmToDACDB[] = { + {20, {168, 2}}, // 100mW + {24, {148, 6}}, // 250mW + {27, {128, 9}}, // 500mW + {30, {90, 12}} // 1000mW + }; #endif #ifdef RADIOMASTER_900_BANDIT - DACDB defaultValue = {165, 2}; + dbmToDACDB[] = { + {20, {165, 2}}, // 100mW + {24, {155, 6}}, // 250mW + {27, {142, 9}}, // 500mW + {30, {110, 10}} // 1000mW + }; #endif - return defaultValue; + const int numValues = sizeof(dbmToDACDB) / sizeof(dbmToDACDB[0]); + + // Find the interval dbm falls within and interpolate + for (int i = 0; i < numValues - 1; i++) { + if (dbm >= dbmToDACDB[i].dbm && dbm <= dbmToDACDB[i + 1].dbm) { + return interpolate(dbm, dbmToDACDB[i].dbm, dbmToDACDB[i + 1].dbm, dbmToDACDB[i].values, dbmToDACDB[i + 1].values); + } + } + + // Return a default value if no match is found and default to 100mW +#ifdef RADIOMASTER_900_BANDIT_NANO + DACDB defaultValue = {168, 2}; +#endif +#ifdef RADIOMASTER_900_BANDIT + DACDB defaultValue = {165, 2}; +#endif + return defaultValue; } #endif -RF95Interface::RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy) { - LOG_DEBUG("RF95Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); +RF95Interface::RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : RadioLibInterface(hal, cs, irq, rst, busy) +{ + LOG_DEBUG("RF95Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /** Some boards require GPIO control of tx vs rx paths */ -void RF95Interface::setTransmitEnable(bool txon) { +void RF95Interface::setTransmitEnable(bool txon) +{ #ifdef RF95_TXEN - digitalWrite(RF95_TXEN, txon ? 1 : 0); + digitalWrite(RF95_TXEN, txon ? 1 : 0); #elif ARCH_PORTDUINO - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); - } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); + } #endif #ifdef RF95_RXEN - digitalWrite(RF95_RXEN, txon ? 0 : 1); + digitalWrite(RF95_RXEN, txon ? 0 : 1); #elif ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); + } #endif } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -bool RF95Interface::init() { - RadioLibInterface::init(); +bool RF95Interface::init() +{ + RadioLibInterface::init(); #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) - // DAC and DB values based on dBm using interpolation - DACDB dacDbValues = getDACandDB(power); - int8_t powerDAC = dacDbValues.dac; - power = dacDbValues.db; + // DAC and DB values based on dBm using interpolation + DACDB dacDbValues = getDACandDB(power); + int8_t powerDAC = dacDbValues.dac; + power = dacDbValues.db; #endif - limitPower(RF95_MAX_POWER); + limitPower(RF95_MAX_POWER); - iface = lora = new RadioLibRF95(&module); + iface = lora = new RadioLibRF95(&module); #ifdef RF95_TCXO - pinMode(RF95_TCXO, OUTPUT); - digitalWrite(RF95_TCXO, 1); + pinMode(RF95_TCXO, OUTPUT); + digitalWrite(RF95_TCXO, 1); #endif - // enable PA + // enable PA #ifdef RF95_PA_EN #if defined(RF95_PA_DAC_EN) #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) - // Use calculated DAC value - dacWrite(RF95_PA_EN, powerDAC); + // Use calculated DAC value + dacWrite(RF95_PA_EN, powerDAC); #else - // Use Value set in /*/variant.h - dacWrite(RF95_PA_EN, RF95_PA_LEVEL); + // Use Value set in /*/variant.h + dacWrite(RF95_PA_EN, RF95_PA_LEVEL); #endif #endif #endif - /* - #define RF95_TXEN (22) // If defined, this pin should be set high prior to transmit (controls an external analog - switch) #define RF95_RXEN (23) // If defined, this pin should be set high prior to receive (controls an external - analog switch) - */ + /* + #define RF95_TXEN (22) // If defined, this pin should be set high prior to transmit (controls an external analog switch) + #define RF95_RXEN (23) // If defined, this pin should be set high prior to receive (controls an external analog switch) + */ #ifdef RF95_TXEN - pinMode(RF95_TXEN, OUTPUT); - digitalWrite(RF95_TXEN, 0); + pinMode(RF95_TXEN, OUTPUT); + digitalWrite(RF95_TXEN, 0); #endif #ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, 1); + pinMode(RF95_FAN_EN, OUTPUT); + digitalWrite(RF95_FAN_EN, 1); #endif #ifdef RF95_RXEN - pinMode(RF95_RXEN, OUTPUT); - digitalWrite(RF95_RXEN, 1); + pinMode(RF95_RXEN, OUTPUT); + digitalWrite(RF95_RXEN, 1); #endif #if ARCH_PORTDUINO - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_txen_pin.pin, 0); - } - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_rxen_pin.pin, 0); - } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, 0); + } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, 0); + } #endif - setTransmitEnable(false); + setTransmitEnable(false); - int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); - LOG_INFO("RF95 init result %d", res); - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); + LOG_INFO("RF95 init result %d", res); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) - LOG_INFO("DAC output set to %d", powerDAC); + LOG_INFO("DAC output set to %d", powerDAC); #endif - if (res == RADIOLIB_ERR_NONE) - res = lora->setCRC(RADIOLIB_SX126X_LORA_CRC_ON); + if (res == RADIOLIB_ERR_NONE) + res = lora->setCRC(RADIOLIB_SX126X_LORA_CRC_ON); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -void INTERRUPT_ATTR RF95Interface::disableInterrupt() { lora->clearDio0Action(); } +void INTERRUPT_ATTR RF95Interface::disableInterrupt() +{ + lora->clearDio0Action(); +} -bool RF95Interface::reconfigure() { - RadioLibInterface::reconfigure(); +bool RF95Interface::reconfigure() +{ + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora->setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora->setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora->setBandwidth(bw); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora->setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora->setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora->setSyncWord(syncWord); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 setSyncWord %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora->setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 setSyncWord %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora->setCurrentLimit(currentLimit); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 setCurrentLimit %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora->setCurrentLimit(currentLimit); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 setCurrentLimit %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora->setPreambleLength(preambleLength); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 setPreambleLength %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora->setPreambleLength(preambleLength); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 setPreambleLength %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora->setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora->setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > RF95_MAX_POWER) // This chip has lower power limits than some - power = RF95_MAX_POWER; + if (power > RF95_MAX_POWER) // This chip has lower power limits than some + power = RF95_MAX_POWER; #ifdef USE_RF95_RFO - err = lora->setOutputPower(power, true); + err = lora->setOutputPower(power, true); #else - err = lora->setOutputPower(power); + err = lora->setOutputPower(power); #endif - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } /** * Add SNR data to received messages */ -void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) { - mp->rx_snr = lora->getSNR(); - mp->rx_rssi = lround(lora->getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError()); +void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) +{ + mp->rx_snr = lora->getSNR(); + mp->rx_rssi = lround(lora->getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError()); } -void RF95Interface::setStandby() { - int err = lora->standby(); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 standby %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); +void RF95Interface::setStandby() +{ + int err = lora->standby(); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = false; // If we were receiving, not any more - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** We override to turn on transmitter power as needed. */ -void RF95Interface::configHardwareForSend() { - setTransmitEnable(true); +void RF95Interface::configHardwareForSend() +{ + setTransmitEnable(true); - RadioLibInterface::configHardwareForSend(); + RadioLibInterface::configHardwareForSend(); } -void RF95Interface::startReceive() { - setTransmitEnable(false); - setStandby(); - int err = lora->startReceive(); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("RF95 startReceive %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); +void RF95Interface::startReceive() +{ + setTransmitEnable(false); + setStandby(); + int err = lora->startReceive(); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("RF95 startReceive %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = true; + isReceiving = true; - // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); } -bool RF95Interface::isChannelActive() { - // check if we can detect a LoRa preamble on the current channel - int16_t result; - setTransmitEnable(false); - setStandby(); // needed for smooth transition - result = lora->scanChannel(); +bool RF95Interface::isChannelActive() +{ + // check if we can detect a LoRa preamble on the current channel + int16_t result; + setTransmitEnable(false); + setStandby(); // needed for smooth transition + result = lora->scanChannel(); - if (result == RADIOLIB_PREAMBLE_DETECTED) { - // LOG_DEBUG("Channel is busy!"); - return true; - } - if (result != RADIOLIB_CHANNEL_FREE) - LOG_ERROR("RF95 isChannelActive %s%d", radioLibErr, result); - assert(result != RADIOLIB_ERR_WRONG_MODEM); + if (result == RADIOLIB_PREAMBLE_DETECTED) { + // LOG_DEBUG("Channel is busy!"); + return true; + } + if (result != RADIOLIB_CHANNEL_FREE) + LOG_ERROR("RF95 isChannelActive %s%d", radioLibErr, result); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - // LOG_DEBUG("Channel is free!"); - return false; + // LOG_DEBUG("Channel is free!"); + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -bool RF95Interface::isActivelyReceiving() { return lora->isReceiving(); } +bool RF95Interface::isActivelyReceiving() +{ + return lora->isReceiving(); +} -bool RF95Interface::sleep() { - // put chipset into sleep mode - setStandby(); // First cancel any active receiving/sending - lora->sleep(); +bool RF95Interface::sleep() +{ + // put chipset into sleep mode + setStandby(); // First cancel any active receiving/sending + lora->sleep(); #ifdef RF95_FAN_EN - digitalWrite(RF95_FAN_EN, 0); + digitalWrite(RF95_FAN_EN, 0); #endif - return true; + return true; } #endif \ No newline at end of file diff --git a/src/mesh/RF95Interface.h b/src/mesh/RF95Interface.h index b63b9a441..ffd8ae008 100644 --- a/src/mesh/RF95Interface.h +++ b/src/mesh/RF95Interface.h @@ -7,66 +7,68 @@ /** * Our new not radiohead adapter for RF95 style radios */ -class RF95Interface : public RadioLibInterface { - RadioLibRF95 *lora = NULL; // Either a RFM95 or RFM96 depending on what was stuffed on this board +class RF95Interface : public RadioLibInterface +{ + RadioLibRF95 *lora = NULL; // Either a RFM95 or RFM96 depending on what was stuffed on this board -public: - RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); + public: + RF95Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); - // TODO: Verify that this irq flag works with RFM95 / SX1276 radios the way it used to - bool isIRQPending() override { return lora->getIRQFlags() & RADIOLIB_SX127X_MASK_IRQ_FLAG_VALID_HEADER; } + // TODO: Verify that this irq flag works with RFM95 / SX1276 radios the way it used to + bool isIRQPending() override { return lora->getIRQFlags() & RADIOLIB_SX127X_MASK_IRQ_FLAG_VALID_HEADER; } - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; -protected: - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + protected: + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora->setDio0Action(callback, RISING); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora->setDio0Action(callback, RISING); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(*lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(*lora, pl, received); } -private: - /** Some boards require GPIO control of tx vs rx paths */ - void setTransmitEnable(bool txon); + private: + /** Some boards require GPIO control of tx vs rx paths */ + void setTransmitEnable(bool txon); }; #endif diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index c326a3a68..5ee513e89 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -13,13 +13,16 @@ #include // Calculate 2^n without calling pow() -uint32_t pow_of_2(uint32_t n) { return 1 << n; } +uint32_t pow_of_2(uint32_t n) +{ + return 1 << n; +} -#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ - { \ - meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, \ - wide_lora, #name \ - } +#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ + { \ + meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \ + frequency_switching, wide_lora, #name \ + } const RegionInfo regions[] = { /* @@ -168,7 +171,8 @@ const RegionInfo regions[] = { 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ - RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), + RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), + RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), /* Nepal @@ -201,18 +205,19 @@ bool RadioInterface::uses_default_frequency_slot = true; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; -void initRegion() { - const RegionInfo *r = regions; +void initRegion() +{ + const RegionInfo *r = regions; #ifdef REGULATORY_LORA_REGIONCODE - for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != REGULATORY_LORA_REGIONCODE; r++) - ; - LOG_INFO("Wanted region %d, regulatory override to %s", config.lora.region, r->name); + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != REGULATORY_LORA_REGIONCODE; r++) + ; + LOG_INFO("Wanted region %d, regulatory override to %s", config.lora.region, r->name); #else - for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != config.lora.region; r++) - ; - LOG_INFO("Wanted region %d, using %s", config.lora.region, r->name); + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != config.lora.region; r++) + ; + LOG_INFO("Wanted region %d, using %s", config.lora.region, r->name); #endif - myRegion = r; + myRegion = r; } /** @@ -226,172 +231,186 @@ The band is from 902 to 928 MHz. It mentions channel number and its respective c separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. */ -uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) { - uint32_t pl = 0; - if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { - pl = p->encrypted.size + sizeof(PacketHeader); - } else { - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - pl = numbytes + sizeof(PacketHeader); - } - return getPacketTime(pl, received); +uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) +{ + uint32_t pl = 0; + if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + pl = p->encrypted.size + sizeof(PacketHeader); + } else { + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + pl = numbytes + sizeof(PacketHeader); + } + return getPacketTime(pl, received); } /** The delay to use for retransmitting dropped packets */ -uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) { - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); - // Make sure enough time has elapsed for this packet to be sent and an ACK is received. - // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); - float channelUtil = airTime->channelUtilizationPercent(); - uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); - // Assuming we pick max. of CWsize and there will be a client with SNR at half the range - return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec + PROCESSING_TIME_MSEC; +uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) +{ + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); + // Make sure enough time has elapsed for this packet to be sent and an ACK is received. + // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); + float channelUtil = airTime->channelUtilizationPercent(); + uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); + // Assuming we pick max. of CWsize and there will be a client with SNR at half the range + return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec + + PROCESSING_TIME_MSEC; } /** The delay to use when we want to send something */ -uint32_t RadioInterface::getTxDelayMsec() { - /** We wait a random multiple of 'slotTimes' (see definition in header file) in order to avoid collisions. - The pool to take a random multiple from is the contention window (CW), which size depends on the - current channel utilization. */ - float channelUtil = airTime->channelUtilizationPercent(); - uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); - // LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize); - return random(0, pow_of_2(CWsize)) * slotTimeMsec; +uint32_t RadioInterface::getTxDelayMsec() +{ + /** We wait a random multiple of 'slotTimes' (see definition in header file) in order to avoid collisions. + The pool to take a random multiple from is the contention window (CW), which size depends on the + current channel utilization. */ + float channelUtil = airTime->channelUtilizationPercent(); + uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); + // LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize); + return random(0, pow_of_2(CWsize)) * slotTimeMsec; } /** The CW size to use when calculating SNR_based delays */ -uint8_t RadioInterface::getCWsize(float snr) { - // The minimum value for a LoRa SNR - const int32_t SNR_MIN = -20; +uint8_t RadioInterface::getCWsize(float snr) +{ + // The minimum value for a LoRa SNR + const int32_t SNR_MIN = -20; - // The maximum value for a LoRa SNR - const int32_t SNR_MAX = 10; + // The maximum value for a LoRa SNR + const int32_t SNR_MAX = 10; - return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); + return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); } /** The worst-case SNR_based packet delay */ -uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) { - uint8_t CWsize = getCWsize(snr); - // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) - return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; +uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) +{ + uint8_t CWsize = getCWsize(snr); + // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) + return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } /** Returns true if we should rebroadcast early like a ROUTER */ -bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) { - // If we are a ROUTER, we always rebroadcast early - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { - return true; - } +bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) +{ + // If we are a ROUTER, we always rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + return true; + } - return false; + return false; } /** The delay to use when we want to flood a message */ -uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) { - // high SNR = large CW size (Long Delay) - // low SNR = small CW size (Short Delay) - float snr = p->rx_snr; - uint32_t delay = 0; - uint8_t CWsize = getCWsize(snr); - // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); - if (shouldRebroadcastEarlyLikeRouter(p)) { - delay = random(0, 2 * CWsize) * slotTimeMsec; - LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); - } else { - // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) - delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec; - LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); - } +uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) +{ + // high SNR = large CW size (Long Delay) + // low SNR = small CW size (Short Delay) + float snr = p->rx_snr; + uint32_t delay = 0; + uint8_t CWsize = getCWsize(snr); + // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); + if (shouldRebroadcastEarlyLikeRouter(p)) { + delay = random(0, 2 * CWsize) * slotTimeMsec; + LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); + } else { + // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) + delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec; + LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); + } - return delay; + return delay; } -void printPacket(const char *prefix, const meshtastic_MeshPacket *p) { +void printPacket(const char *prefix, const meshtastic_MeshPacket *p) +{ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - std::string out = DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, p->from, - p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel); - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - auto &s = p->decoded; + std::string out = + DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, + p->from, p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel); + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + auto &s = p->decoded; - out += DEBUG_PORT.mt_sprintf(" Portnum=%d", s.portnum); + out += DEBUG_PORT.mt_sprintf(" Portnum=%d", s.portnum); - if (s.want_response) - out += DEBUG_PORT.mt_sprintf(" WANTRESP"); + if (s.want_response) + out += DEBUG_PORT.mt_sprintf(" WANTRESP"); - if (p->pki_encrypted) - out += DEBUG_PORT.mt_sprintf(" PKI"); + if (p->pki_encrypted) + out += DEBUG_PORT.mt_sprintf(" PKI"); - if (s.source != 0) - out += DEBUG_PORT.mt_sprintf(" source=%08x", s.source); + if (s.source != 0) + out += DEBUG_PORT.mt_sprintf(" source=%08x", s.source); - if (s.dest != 0) - out += DEBUG_PORT.mt_sprintf(" dest=%08x", s.dest); + if (s.dest != 0) + out += DEBUG_PORT.mt_sprintf(" dest=%08x", s.dest); - if (s.request_id) - out += DEBUG_PORT.mt_sprintf(" requestId=%0x", s.request_id); + if (s.request_id) + out += DEBUG_PORT.mt_sprintf(" requestId=%0x", s.request_id); - /* now inside Data and therefore kinda opaque - if (s.which_ackVariant == SubPacket_success_id_tag) - out += DEBUG_PORT.mt_sprintf(" successId=%08x", s.ackVariant.success_id); - else if (s.which_ackVariant == SubPacket_fail_id_tag) - out += DEBUG_PORT.mt_sprintf(" failId=%08x", s.ackVariant.fail_id); */ - } else { - out += " encrypted"; - out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader)); - } + /* now inside Data and therefore kinda opaque + if (s.which_ackVariant == SubPacket_success_id_tag) + out += DEBUG_PORT.mt_sprintf(" successId=%08x", s.ackVariant.success_id); + else if (s.which_ackVariant == SubPacket_fail_id_tag) + out += DEBUG_PORT.mt_sprintf(" failId=%08x", s.ackVariant.fail_id); */ + } else { + out += " encrypted"; + out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader)); + } - if (p->rx_time != 0) - out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time); - if (p->rx_snr != 0.0) - out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr); - if (p->rx_rssi != 0) - out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi); - if (p->via_mqtt != 0) - out += DEBUG_PORT.mt_sprintf(" via MQTT"); - if (p->hop_start != 0) - out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start); - if (p->next_hop != 0) - out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop); - if (p->relay_node != 0) - out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node); - if (p->priority != 0) - out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); + if (p->rx_time != 0) + out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time); + if (p->rx_snr != 0.0) + out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr); + if (p->rx_rssi != 0) + out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi); + if (p->via_mqtt != 0) + out += DEBUG_PORT.mt_sprintf(" via MQTT"); + if (p->hop_start != 0) + out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start); + if (p->next_hop != 0) + out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop); + if (p->relay_node != 0) + out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node); + if (p->priority != 0) + out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); - out += ")"; - LOG_DEBUG("%s", out.c_str()); + out += ")"; + LOG_DEBUG("%s", out.c_str()); #endif } -RadioInterface::RadioInterface() { - assert(sizeof(PacketHeader) == MESHTASTIC_HEADER_LENGTH); // make sure the compiler did what we expected +RadioInterface::RadioInterface() +{ + assert(sizeof(PacketHeader) == MESHTASTIC_HEADER_LENGTH); // make sure the compiler did what we expected } -bool RadioInterface::reconfigure() { - applyModemConfig(); - return true; +bool RadioInterface::reconfigure() +{ + applyModemConfig(); + return true; } -bool RadioInterface::init() { - LOG_INFO("Start meshradio init"); +bool RadioInterface::init() +{ + LOG_INFO("Start meshradio init"); - configChangedObserver.observe(&service->configChanged); - preflightSleepObserver.observe(&preflightSleep); - notifyDeepSleepObserver.observe(¬ifyDeepSleep); + configChangedObserver.observe(&service->configChanged); + preflightSleepObserver.observe(&preflightSleep); + notifyDeepSleepObserver.observe(¬ifyDeepSleep); - // we now expect interfaces to operate in promiscuous mode - // radioIf.setThisAddress(nodeDB->getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at - // constructor time. + // we now expect interfaces to operate in promiscuous mode + // radioIf.setThisAddress(nodeDB->getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at + // constructor time. - applyModemConfig(); + applyModemConfig(); - return true; + return true; } -int RadioInterface::notifyDeepSleepCb(void *unused) { - sleep(); - return 0; +int RadioInterface::notifyDeepSleepCb(void *unused) +{ + sleep(); + return 0; } /** hash a string into an integer @@ -399,198 +418,215 @@ int RadioInterface::notifyDeepSleepCb(void *unused) { * djb2 by Dan Bernstein. * http://www.cse.yorku.ca/~oz/hash.html */ -uint32_t hash(const char *str) { - uint32_t hash = 5381; - int c; +uint32_t hash(const char *str) +{ + uint32_t hash = 5381; + int c; - while ((c = *str++) != 0) - hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */ + while ((c = *str++) != 0) + hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */ - return hash; + return hash; } /** * Save our frequency for later reuse. */ -void RadioInterface::saveFreq(float freq) { savedFreq = freq; } +void RadioInterface::saveFreq(float freq) +{ + savedFreq = freq; +} /** * Save our channel for later reuse. */ -void RadioInterface::saveChannelNum(uint32_t channel_num) { savedChannelNum = channel_num; } +void RadioInterface::saveChannelNum(uint32_t channel_num) +{ + savedChannelNum = channel_num; +} /** * Save our frequency for later reuse. */ -float RadioInterface::getFreq() { return savedFreq; } +float RadioInterface::getFreq() +{ + return savedFreq; +} /** * Save our channel for later reuse. */ -uint32_t RadioInterface::getChannelNum() { return savedChannelNum; } +uint32_t RadioInterface::getChannelNum() +{ + return savedChannelNum; +} /** * Pull our channel settings etc... from protobufs to the dumb interface settings */ -void RadioInterface::applyModemConfig() { - // Set up default configuration - // No Sync Words in LORA mode - meshtastic_Config_LoRaConfig &loraConfig = config.lora; - bool validConfig = false; // We need to check for a valid configuration - while (!validConfig) { - if (loraConfig.use_preset) { +void RadioInterface::applyModemConfig() +{ + // Set up default configuration + // No Sync Words in LORA mode + meshtastic_Config_LoRaConfig &loraConfig = config.lora; + bool validConfig = false; // We need to check for a valid configuration + while (!validConfig) { + if (loraConfig.use_preset) { - switch (loraConfig.modem_preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 8; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 9; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 10; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 8; - sf = 11; - break; - default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 12; - break; - } - if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { - cr = loraConfig.coding_rate; - LOG_INFO("Using custom Coding Rate %u", cr); - } - } else { - sf = loraConfig.spread_factor; - cr = loraConfig.coding_rate; - bw = loraConfig.bandwidth; + switch (loraConfig.modem_preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 9; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 10; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + cr = 8; + sf = 11; + break; + default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. + bw = (myRegion->wideLora) ? 812.5 : 250; + cr = 5; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + bw = (myRegion->wideLora) ? 406.25 : 125; + cr = 8; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + bw = (myRegion->wideLora) ? 406.25 : 125; + cr = 8; + sf = 12; + break; + } + if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { + cr = loraConfig.coding_rate; + LOG_INFO("Using custom Coding Rate %u", cr); + } + } else { + sf = loraConfig.spread_factor; + cr = loraConfig.coding_rate; + bw = loraConfig.bandwidth; - if (bw == 31) // This parameter is not an integer - bw = 31.25; - if (bw == 62) // Fix for 62.5Khz bandwidth - bw = 62.5; - if (bw == 200) - bw = 203.125; - if (bw == 400) - bw = 406.25; - if (bw == 800) - bw = 812.5; - if (bw == 1600) - bw = 1625.0; + if (bw == 31) // This parameter is not an integer + bw = 31.25; + if (bw == 62) // Fix for 62.5Khz bandwidth + bw = 62.5; + if (bw == 200) + bw = 203.125; + if (bw == 400) + bw = 406.25; + if (bw == 800) + bw = 812.5; + if (bw == 1600) + bw = 1625.0; + } + + if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { + const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; + const float requestedBwKHz = bw; + const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + + char err_string[160]; + if (isWideRequest) { + snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", + myRegion->name, presetName); + } else { + snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", + myRegion->name, regionSpanKHz, requestedBwKHz); + } + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + snprintf(cn->message, sizeof(cn->message), "%s", err_string); + service->sendClientNotification(cn); + + // Set to default modem preset + loraConfig.use_preset = true; + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + } else { + validConfig = true; + } } - if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { - const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; - const float requestedBwKHz = bw; - const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset - const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + power = loraConfig.tx_power; - char err_string[160]; - if (isWideRequest) { - snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", myRegion->name, - presetName); - } else { - snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", myRegion->name, - regionSpanKHz, requestedBwKHz); - } - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) + power = myRegion->powerLimit; - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - snprintf(cn->message, sizeof(cn->message), "%s", err_string); - service->sendClientNotification(cn); + if (power == 0) + power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults + // to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this + // variable set to 0 don't have a valid power limit) - // Set to default modem preset - loraConfig.use_preset = true; - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - } else { - validConfig = true; + // Set final tx_power back onto config + loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger + + // Calculate the number of channels + uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000))); + + // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name + const char *channelName = channels.getName(channels.getPrimaryIndex()); + // channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1) + uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels; + + // Check if we use the default frequency slot + RadioInterface::uses_default_frequency_slot = + channel_num == + hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; + + // Old frequency selection formula + // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); + + // New frequency selection formula + float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000)); + + // override if we have a verbatim frequency + if (loraConfig.override_frequency) { + freq = loraConfig.override_frequency; + channel_num = -1; } - } - power = loraConfig.tx_power; + saveChannelNum(channel_num); + saveFreq(freq + loraConfig.frequency_offset); - if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) - power = myRegion->powerLimit; + slotTimeMsec = computeSlotTimeMsec(); + preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); - if (power == 0) - power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults - // to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this - // variable set to 0 don't have a valid power limit) - - // Set final tx_power back onto config - loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger - - // Calculate the number of channels - uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000))); - - // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name - const char *channelName = channels.getName(channels.getPrimaryIndex()); - // channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1) - uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels; - - // Check if we use the default frequency slot - RadioInterface::uses_default_frequency_slot = - channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; - - // Old frequency selection formula - // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); - - // New frequency selection formula - float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000)); - - // override if we have a verbatim frequency - if (loraConfig.override_frequency) { - freq = loraConfig.override_frequency; - channel_num = -1; - } - - saveChannelNum(channel_num); - saveFreq(freq + loraConfig.frequency_offset); - - slotTimeMsec = computeSlotTimeMsec(); - preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); - - LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); - LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, channel_num, power); - LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd, - myRegion->freqEnd - myRegion->freqStart); - LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw); - LOG_INFO("channel_num: %d", channel_num + 1); - LOG_INFO("frequency: %f", getFreq()); - LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); + LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); + LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, + channel_num, power); + LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd, + myRegion->freqEnd - myRegion->freqStart); + LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw); + LOG_INFO("channel_num: %d", channel_num + 1); + LOG_INFO("frequency: %f", getFreq()); + LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); } /** Slottime is the time to detect a transmission has started, consisting of: @@ -598,93 +634,99 @@ void RadioInterface::applyModemConfig() { - roundtrip air propagation time (assuming max. 30km between nodes); - Tx/Rx turnaround time (maximum of SX126x and SX127x); - MAC processing time (measured on T-beam) */ -uint32_t RadioInterface::computeSlotTimeMsec() { - float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds - float symbolTime = pow_of_2(sf) / bw; // in milliseconds +uint32_t RadioInterface::computeSlotTimeMsec() +{ + float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds + float symbolTime = pow_of_2(sf) / bw; // in milliseconds - if (myRegion->wideLora) { - // CAD duration derived from AN1200.22 of SX1280 - return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime; - } else { - // CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol - return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime; - } + if (myRegion->wideLora) { + // CAD duration derived from AN1200.22 of SX1280 + return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime; + } else { + // CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol + return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime; + } } /** * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it */ -void RadioInterface::limitPower(int8_t loraMaxPower) { - uint8_t maxPower = 255; // No limit +void RadioInterface::limitPower(int8_t loraMaxPower) +{ + uint8_t maxPower = 255; // No limit - if (myRegion->powerLimit) - maxPower = myRegion->powerLimit; + if (myRegion->powerLimit) + maxPower = myRegion->powerLimit; - if ((power > maxPower) && !devicestate.owner.is_licensed) { - LOG_INFO("Lower transmit power because of regulatory limits"); - power = maxPower; - } + if ((power > maxPower) && !devicestate.owner.is_licensed) { + LOG_INFO("Lower transmit power because of regulatory limits"); + power = maxPower; + } #ifndef NUM_PA_POINTS - if (TX_GAIN_LORA > 0 && !devicestate.owner.is_licensed) { - LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA); - power -= TX_GAIN_LORA; - } -#else - if (!devicestate.owner.is_licensed) { - // we have an array of PA gain values. Find the highest power setting that works. - const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; - for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) { - if (((radio_dbm + tx_gain[radio_dbm]) > power) || ((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { - // we've exceeded the power limit, or hit the max we can do - LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); - power -= tx_gain[radio_dbm]; - break; - } + if (TX_GAIN_LORA > 0 && !devicestate.owner.is_licensed) { + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA); + power -= TX_GAIN_LORA; + } +#else + if (!devicestate.owner.is_licensed) { + // we have an array of PA gain values. Find the highest power setting that works. + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; + for (int radio_dbm = 0; radio_dbm < NUM_PA_POINTS; radio_dbm++) { + if (((radio_dbm + tx_gain[radio_dbm]) > power) || + ((radio_dbm == (NUM_PA_POINTS - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { + // we've exceeded the power limit, or hit the max we can do + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); + power -= tx_gain[radio_dbm]; + break; + } + } } - } #endif - if (power > loraMaxPower) // Clamp power to maximum defined level - power = loraMaxPower; + if (power > loraMaxPower) // Clamp power to maximum defined level + power = loraMaxPower; - LOG_INFO("Final Tx power: %d dBm", power); + LOG_INFO("Final Tx power: %d dBm", power); } -void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) { - if (router) { - p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; - router->enqueueReceivedMessage(p); - } +void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) +{ + if (router) { + p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; + router->enqueueReceivedMessage(p); + } } /*** * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of payload bytes to send */ -size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) { - assert(!sendingPacket); +size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) +{ + assert(!sendingPacket); - // LOG_DEBUG("Send queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); - assert(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag); // It should have already been encoded by now + // LOG_DEBUG("Send queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); + assert(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag); // It should have already been encoded by now - radioBuffer.header.from = p->from; - radioBuffer.header.to = p->to; - radioBuffer.header.id = p->id; - radioBuffer.header.channel = p->channel; - radioBuffer.header.next_hop = p->next_hop; - radioBuffer.header.relay_node = p->relay_node; - if (p->hop_limit > HOP_MAX) { - LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE); - p->hop_limit = HOP_RELIABLE; - } - radioBuffer.header.flags = p->hop_limit | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0); - radioBuffer.header.flags |= (p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK; + radioBuffer.header.from = p->from; + radioBuffer.header.to = p->to; + radioBuffer.header.id = p->id; + radioBuffer.header.channel = p->channel; + radioBuffer.header.next_hop = p->next_hop; + radioBuffer.header.relay_node = p->relay_node; + if (p->hop_limit > HOP_MAX) { + LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE); + p->hop_limit = HOP_RELIABLE; + } + radioBuffer.header.flags = + p->hop_limit | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0); + radioBuffer.header.flags |= (p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK; - // if the sender nodenum is zero, that means uninitialized - assert(radioBuffer.header.from); - assert(p->encrypted.size <= sizeof(radioBuffer.payload)); - memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); + // if the sender nodenum is zero, that means uninitialized + assert(radioBuffer.header.from); + assert(p->encrypted.size <= sizeof(radioBuffer.payload)); + memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); - sendingPacket = p; - return p->encrypted.size + sizeof(PacketHeader); + sendingPacket = p; + return p->encrypted.size + sizeof(PacketHeader); } diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 5e429580e..6049a11cc 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -24,25 +24,25 @@ * with the old radiohead implementation. */ typedef struct { - NodeNum to, from; // can be 1 byte or four bytes + NodeNum to, from; // can be 1 byte or four bytes - PacketId id; // can be 1 byte or 4 bytes + PacketId id; // can be 1 byte or 4 bytes - /** - * Usage of flags: - * - * The bottom three bits of flags are use to store hop_limit when sent over the wire. - **/ - uint8_t flags; + /** + * Usage of flags: + * + * The bottom three bits of flags are use to store hop_limit when sent over the wire. + **/ + uint8_t flags; - /** The channel hash - used as a hint for the decoder to limit which channels we consider */ - uint8_t channel; + /** The channel hash - used as a hint for the decoder to limit which channels we consider */ + uint8_t channel; - // Last byte of the NodeNum of the next-hop for this packet - uint8_t next_hop; + // Last byte of the NodeNum of the next-hop for this packet + uint8_t next_hop; - // Last byte of the NodeNum of the node that will relay/relayed this packet - uint8_t relay_node; + // Last byte of the NodeNum of the node that will relay/relayed this packet + uint8_t relay_node; } PacketHeader; /** @@ -51,11 +51,11 @@ typedef struct { * It makes the use of its data easier, and avoids manipulating pointers (and potential non aligned accesses) */ typedef struct { - /** The header, as defined just before */ - PacketHeader header; + /** The header, as defined just before */ + PacketHeader header; - /** The payload, of maximum length minus the header, aligned just to be sure */ - uint8_t payload[MAX_LORA_PAYLOAD_LEN + 1 - sizeof(PacketHeader)] __attribute__((__aligned__)); + /** The payload, of maximum length minus the header, aligned just to be sure */ + uint8_t payload[MAX_LORA_PAYLOAD_LEN + 1 - sizeof(PacketHeader)] __attribute__((__aligned__)); } RadioBuffer; @@ -64,204 +64,210 @@ typedef struct { * * This defines the SOLE API for talking to radios (because soon we will have alternate radio implementations) */ -class RadioInterface { - friend class MeshRadio; // for debugging we let that class touch pool +class RadioInterface +{ + friend class MeshRadio; // for debugging we let that class touch pool - CallbackObserver configChangedObserver = CallbackObserver(this, &RadioInterface::reloadConfig); + CallbackObserver configChangedObserver = + CallbackObserver(this, &RadioInterface::reloadConfig); - CallbackObserver preflightSleepObserver = CallbackObserver(this, &RadioInterface::preflightSleepCb); + CallbackObserver preflightSleepObserver = + CallbackObserver(this, &RadioInterface::preflightSleepCb); - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &RadioInterface::notifyDeepSleepCb); + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &RadioInterface::notifyDeepSleepCb); -protected: - bool disabled = false; + protected: + bool disabled = false; - float bw = 125; - uint8_t sf = 9; - uint8_t cr = 5; + float bw = 125; + uint8_t sf = 9; + uint8_t cr = 5; - const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 - const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 - uint32_t slotTimeMsec = computeSlotTimeMsec(); - uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving - uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast - const uint32_t PROCESSING_TIME_MSEC = 4500; // time to construct, process and construct a packet again (empirically determined) - const uint8_t CWmin = 3; // minimum CWsize - const uint8_t CWmax = 8; // maximum CWsize + const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 + const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 + uint32_t slotTimeMsec = computeSlotTimeMsec(); + uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast + const uint32_t PROCESSING_TIME_MSEC = + 4500; // time to construct, process and construct a packet again (empirically determined) + const uint8_t CWmin = 3; // minimum CWsize + const uint8_t CWmax = 8; // maximum CWsize - meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending - uint32_t lastTxStart = 0L; + meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending + uint32_t lastTxStart = 0L; - uint32_t computeSlotTimeMsec(); + uint32_t computeSlotTimeMsec(); - /** - * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need - * */ - RadioBuffer radioBuffer __attribute__((__aligned__)); - /** - * Enqueue a received packet for the registered receiver - */ - void deliverToReceiver(meshtastic_MeshPacket *p); + /** + * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need + * */ + RadioBuffer radioBuffer __attribute__((__aligned__)); + /** + * Enqueue a received packet for the registered receiver + */ + void deliverToReceiver(meshtastic_MeshPacket *p); -public: - /** pool is the pool we will alloc our rx packets from - */ - RadioInterface(); + public: + /** pool is the pool we will alloc our rx packets from + */ + RadioInterface(); - virtual ~RadioInterface() {} + virtual ~RadioInterface() {} - /** - * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) - * - * This method must be used before putting the CPU into deep or light sleep. - */ - virtual bool canSleep() { return true; } + /** + * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) + * + * This method must be used before putting the CPU into deep or light sleep. + */ + virtual bool canSleep() { return true; } - virtual bool wideLora() { return false; } + virtual bool wideLora() { return false; } - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() { return true; } + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() { return true; } - /// Disable this interface (while disabled, no packets can be sent or received) - void disable() { - disabled = true; - sleep(); - } + /// Disable this interface (while disabled, no packets can be sent or received) + void disable() + { + disabled = true; + sleep(); + } - /** - * Send a packet (possibly by enquing in a private fifo). This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) = 0; + /** + * Send a packet (possibly by enquing in a private fifo). This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) = 0; - /** Return TX queue status */ - virtual meshtastic_QueueStatus getQueueStatus() { - meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; - return qs; - } + /** Return TX queue status */ + virtual meshtastic_QueueStatus getQueueStatus() + { + meshtastic_QueueStatus qs; + qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; + return qs; + } - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - virtual bool cancelSending(NodeNum from, PacketId id) { return false; } + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) { return false; } - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; } + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; } - // methods from radiohead + // methods from radiohead - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init(); + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init(); - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure(); + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure(); - /** The delay to use for retransmitting dropped packets */ - uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); + /** The delay to use for retransmitting dropped packets */ + uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); - /** The delay to use when we want to send something */ - uint32_t getTxDelayMsec(); + /** The delay to use when we want to send something */ + uint32_t getTxDelayMsec(); - /** The CW to use when calculating SNR_based delays */ - uint8_t getCWsize(float snr); + /** The CW to use when calculating SNR_based delays */ + uint8_t getCWsize(float snr); - /** The worst-case SNR_based packet delay */ - uint32_t getTxDelayMsecWeightedWorst(float snr); + /** The worst-case SNR_based packet delay */ + uint32_t getTxDelayMsecWeightedWorst(float snr); - /** Returns true if we should rebroadcast early like a ROUTER */ - bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + /** Returns true if we should rebroadcast early like a ROUTER */ + bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); - /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); + /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ + uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); - /** If the packet is not already in the late rebroadcast window, move it there */ - virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } + /** If the packet is not already in the late rebroadcast window, move it there */ + virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } - /** - * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better - * version - * @return Whether a pending packet was removed - */ - virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; } + /** + * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version + * @return Whether a pending packet was removed + */ + virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; } - /** - * Calculate airtime per - * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf - * section 4 - * - * @return num msecs for the packet - */ - uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); - virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; + /** + * Calculate airtime per + * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf + * section 4 + * + * @return num msecs for the packet + */ + uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); + virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; - /** - * Get the channel we saved. - */ - uint32_t getChannelNum(); + /** + * Get the channel we saved. + */ + uint32_t getChannelNum(); - /** - * Get the frequency we saved. - */ - virtual float getFreq(); + /** + * Get the frequency we saved. + */ + virtual float getFreq(); - /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers - virtual bool isIRQPending() { return false; } + /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers + virtual bool isIRQPending() { return false; } - // Whether we use the default frequency slot given our LoRa config (region and modem preset) - static bool uses_default_frequency_slot; + // Whether we use the default frequency slot given our LoRa config (region and modem preset) + static bool uses_default_frequency_slot; -protected: - int8_t power = 17; // Set by applyModemConfig() + protected: + int8_t power = 17; // Set by applyModemConfig() - float savedFreq; - uint32_t savedChannelNum; + float savedFreq; + uint32_t savedChannelNum; - /*** - * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the - * PacketHeader & payload). - * - * Used as the first step of - */ - size_t beginSending(meshtastic_MeshPacket *p); + /*** + * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the + * PacketHeader & payload). + * + * Used as the first step of + */ + size_t beginSending(meshtastic_MeshPacket *p); - /** - * Some regulatory regions limit xmit power. - * This function should be called by subclasses after setting their desired power. It might lower it - */ - void limitPower(int8_t MAX_POWER); + /** + * Some regulatory regions limit xmit power. + * This function should be called by subclasses after setting their desired power. It might lower it + */ + void limitPower(int8_t MAX_POWER); - /** - * Save the frequency we selected for later reuse. - */ - virtual void saveFreq(float savedFreq); + /** + * Save the frequency we selected for later reuse. + */ + virtual void saveFreq(float savedFreq); - /** - * Save the channel we selected for later reuse. - */ - virtual void saveChannelNum(uint32_t savedChannelNum); + /** + * Save the channel we selected for later reuse. + */ + virtual void saveChannelNum(uint32_t savedChannelNum); -private: - /** - * Convert our modemConfig enum into wf, sf, etc... - * - * These parameters will be pull from the channelSettings global - */ - void applyModemConfig(); + private: + /** + * Convert our modemConfig enum into wf, sf, etc... + * + * These parameters will be pull from the channelSettings global + */ + void applyModemConfig(); - /// Return 0 if sleep is okay - int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; } + /// Return 0 if sleep is okay + int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; } - int notifyDeepSleepCb(void *unused = NULL); + int notifyDeepSleepCb(void *unused = NULL); - int reloadConfig(void *unused) { - reconfigure(); - return 0; - } + int reloadConfig(void *unused) + { + reconfigure(); + return 0; + } }; /// Debug printing for packets diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 46afb1738..80e51b8bc 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -15,28 +15,34 @@ #include "PortduinoGlue.h" #include "meshUtils.h" #endif -void LockingArduinoHal::spiBeginTransaction() { - spiLock->lock(); +void LockingArduinoHal::spiBeginTransaction() +{ + spiLock->lock(); - ArduinoHal::spiBeginTransaction(); + ArduinoHal::spiBeginTransaction(); } -void LockingArduinoHal::spiEndTransaction() { - ArduinoHal::spiEndTransaction(); +void LockingArduinoHal::spiEndTransaction() +{ + ArduinoHal::spiEndTransaction(); - spiLock->unlock(); + spiLock->unlock(); } #if ARCH_PORTDUINO -void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) { spi->transfer(out, in, len); } +void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) +{ + spi->transfer(out, in, len); +} #endif RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *_iface) - : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) { - instance = this; + : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) +{ + instance = this; #if defined(ARCH_STM32WL) && defined(USE_SX1262) - module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); - module.setCb_digitalRead(stm32wl_emulate_digitalRead); + module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); + module.setCb_digitalRead(stm32wl_emulate_digitalRead); #endif } @@ -47,179 +53,198 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c #define YIELD_FROM_ISR(x) portYIELD_FROM_ISR(x) #endif -void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause) { - instance->disableInterrupt(); +void INTERRUPT_ATTR RadioLibInterface::isrLevel0Common(PendingISR cause) +{ + instance->disableInterrupt(); - BaseType_t xHigherPriorityTaskWoken; - instance->notifyFromISR(&xHigherPriorityTaskWoken, cause, true); + BaseType_t xHigherPriorityTaskWoken; + instance->notifyFromISR(&xHigherPriorityTaskWoken, cause, true); - /* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE. - The macro used to do this is dependent on the port and may be called - portEND_SWITCHING_ISR. */ - YIELD_FROM_ISR(xHigherPriorityTaskWoken); + /* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE. + The macro used to do this is dependent on the port and may be called + portEND_SWITCHING_ISR. */ + YIELD_FROM_ISR(xHigherPriorityTaskWoken); } -void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() { isrLevel0Common(ISR_RX); } +void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() +{ + isrLevel0Common(ISR_RX); +} -void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() { isrLevel0Common(ISR_TX); } +void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() +{ + isrLevel0Common(ISR_TX); +} /** Our ISR code currently needs this to find our active instance */ RadioLibInterface *RadioLibInterface::instance; /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -bool RadioLibInterface::canSendImmediately() { - // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). - // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, - // we almost certainly guarantee no one outside will like the packet we are sending. - bool busyTx = sendingPacket != NULL; - bool busyRx = isReceiving && isActivelyReceiving(); +bool RadioLibInterface::canSendImmediately() +{ + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + bool busyTx = sendingPacket != NULL; + bool busyRx = isReceiving && isActivelyReceiving(); - if (busyTx || busyRx) { - if (busyTx) { - LOG_WARN("Can not send yet, busyTx"); - } - // If we've been trying to send the same packet more than one minute and we haven't gotten a - // TX IRQ from the radio, the radio is probably broken. - if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) { - LOG_ERROR("Hardware Failure! busyTx for more than 60s"); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED); - // reboot in 5 seconds when this condition occurs. - rebootAtMsec = lastTxStart + 65000; - } - if (busyRx) { - LOG_WARN("Can not send yet, busyRx"); - } - return false; - } else - return true; + if (busyTx || busyRx) { + if (busyTx) { + LOG_WARN("Can not send yet, busyTx"); + } + // If we've been trying to send the same packet more than one minute and we haven't gotten a + // TX IRQ from the radio, the radio is probably broken. + if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) { + LOG_ERROR("Hardware Failure! busyTx for more than 60s"); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED); + // reboot in 5 seconds when this condition occurs. + rebootAtMsec = lastTxStart + 65000; + } + if (busyRx) { + LOG_WARN("Can not send yet, busyRx"); + } + return false; + } else + return true; } -bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag) { - bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag)); - // Handle false detections - if (detected) { - if (!activeReceiveStart) { - activeReceiveStart = millis(); - } else if (!Throttle::isWithinTimespanMs(activeReceiveStart, 2 * preambleTimeMsec)) { - if (!(irq & syncWordHeaderValidFlag)) { - // The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag - activeReceiveStart = 0; - LOG_DEBUG("Ignore false preamble detection"); - return false; - } else { - uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); - if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) { - // We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag - activeReceiveStart = 0; - LOG_DEBUG("Ignore false header detection"); - return false; +bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag) +{ + bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag)); + // Handle false detections + if (detected) { + if (!activeReceiveStart) { + activeReceiveStart = millis(); + } else if (!Throttle::isWithinTimespanMs(activeReceiveStart, 2 * preambleTimeMsec)) { + if (!(irq & syncWordHeaderValidFlag)) { + // The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag + activeReceiveStart = 0; + LOG_DEBUG("Ignore false preamble detection"); + return false; + } else { + uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); + if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) { + // We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag + activeReceiveStart = 0; + LOG_DEBUG("Ignore false header detection"); + return false; + } + } } - } } - } - return detected; + return detected; } /// Send a packet (possibly by enquing in a private fifo). This routine will /// later free() the packet to pool. This routine is not allowed to stall because it is called from /// bluetooth comms code. If the txmit queue is empty it might return an error -ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p) { +ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p) +{ #ifndef DISABLE_WELCOME_UNSET - if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - if (disabled || !config.lora.tx_enabled) { - LOG_WARN("send - !config.lora.tx_enabled"); - packetPool.release(p); - return ERRNO_DISABLED; + if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (disabled || !config.lora.tx_enabled) { + LOG_WARN("send - !config.lora.tx_enabled"); + packetPool.release(p); + return ERRNO_DISABLED; + } + + } else { + LOG_WARN("send - lora tx disabled: Region unset"); + packetPool.release(p); + return ERRNO_DISABLED; } - } else { - LOG_WARN("send - lora tx disabled: Region unset"); - packetPool.release(p); - return ERRNO_DISABLED; - } - #else - if (disabled || !config.lora.tx_enabled) { - LOG_WARN("send - !config.lora.tx_enabled"); - packetPool.release(p); - return ERRNO_DISABLED; - } + if (disabled || !config.lora.tx_enabled) { + LOG_WARN("send - !config.lora.tx_enabled"); + packetPool.release(p); + return ERRNO_DISABLED; + } #endif - if (p->to == NODENUM_BROADCAST_NO_LORA) { - LOG_DEBUG("Drop no-LoRa pkt"); - return ERRNO_SHOULD_RELEASE; - } + if (p->to == NODENUM_BROADCAST_NO_LORA) { + LOG_DEBUG("Drop no-LoRa pkt"); + return ERRNO_SHOULD_RELEASE; + } - // Sometimes when testing it is useful to be able to never turn on the xmitter + // Sometimes when testing it is useful to be able to never turn on the xmitter #ifndef LORA_DISABLE_SENDING - printPacket("enqueue for send", p); + printPacket("enqueue for send", p); - LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); - bool dropped = false; - ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; + LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); + bool dropped = false; + ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; - if (dropped) { - txDrop++; - } + if (dropped) { + txDrop++; + } + + if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks + packetPool.release(p); + return res; + } + + // set (random) transmit delay to let others reconfigure their radio, + // to avoid collisions and implement timing-based flooding + setTransmitDelay(); - if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks - packetPool.release(p); return res; - } - - // set (random) transmit delay to let others reconfigure their radio, - // to avoid collisions and implement timing-based flooding - setTransmitDelay(); - - return res; #else - packetPool.release(p); - return ERRNO_DISABLED; + packetPool.release(p); + return ERRNO_DISABLED; #endif } -meshtastic_QueueStatus RadioLibInterface::getQueueStatus() { - meshtastic_QueueStatus qs; +meshtastic_QueueStatus RadioLibInterface::getQueueStatus() +{ + meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = 0; - qs.free = txQueue.getFree(); - qs.maxlen = txQueue.getMaxLen(); + qs.res = qs.mesh_packet_id = 0; + qs.free = txQueue.getFree(); + qs.maxlen = txQueue.getMaxLen(); - return qs; + return qs; } -bool RadioLibInterface::canSleep() { - bool res = txQueue.empty(); - if (!res) { // only print debug messages if we are vetoing sleep - LOG_DEBUG("Radio wait to sleep, txEmpty=%d", res); - } - return res; +bool RadioLibInterface::canSleep() +{ + bool res = txQueue.empty(); + if (!res) { // only print debug messages if we are vetoing sleep + LOG_DEBUG("Radio wait to sleep, txEmpty=%d", res); + } + return res; } /** Allow other firmware components to ask whether we are currently sending a packet Initially implemented to protect T-Echo's capacitive touch button from spurious presses during tx */ -bool RadioLibInterface::isSending() { return sendingPacket != NULL; } +bool RadioLibInterface::isSending() +{ + return sendingPacket != NULL; +} /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ -bool RadioLibInterface::cancelSending(NodeNum from, PacketId id) { - auto p = txQueue.remove(from, id); - if (p) - packetPool.release(p); // free the packet we just removed +bool RadioLibInterface::cancelSending(NodeNum from, PacketId id) +{ + auto p = txQueue.remove(from, id); + if (p) + packetPool.release(p); // free the packet we just removed - bool result = (p != NULL); - LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); - return result; + bool result = (p != NULL); + LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); + return result; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ -bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } +bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) +{ + return txQueue.find(from, id); +} /** radio helper thread callback. We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of @@ -228,132 +253,137 @@ The CW size is determined by setTransmitDelay() and depends either on the curren of a flooding message. After this, we perform channel activity detection (CAD) and reset the transmit delay if it is currently active. */ -void RadioLibInterface::onNotify(uint32_t notification) { - switch (notification) { - case ISR_TX: - handleTransmitInterrupt(); - startReceive(); - setTransmitDelay(); - break; - case ISR_RX: - handleReceiveInterrupt(); - startReceive(); - setTransmitDelay(); - break; - case TRANSMIT_DELAY_COMPLETED: +void RadioLibInterface::onNotify(uint32_t notification) +{ + switch (notification) { + case ISR_TX: + handleTransmitInterrupt(); + startReceive(); + setTransmitDelay(); + break; + case ISR_RX: + handleReceiveInterrupt(); + startReceive(); + setTransmitDelay(); + break; + case TRANSMIT_DELAY_COMPLETED: - // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread - // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? - if (!txQueue.empty()) { - if (!canSendImmediately()) { - setTransmitDelay(); // currently Rx/Tx-ing: reset random delay - } else { - meshtastic_MeshPacket *txp = txQueue.getFront(); - assert(txp); - long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; - if (delay_remaining > 0) { - // There's still some delay pending on this packet, so resume waiting for it to elapse - notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); + // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread + // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? + if (!txQueue.empty()) { + if (!canSendImmediately()) { + setTransmitDelay(); // currently Rx/Tx-ing: reset random delay + } else { + meshtastic_MeshPacket *txp = txQueue.getFront(); + assert(txp); + long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; + if (delay_remaining > 0) { + // There's still some delay pending on this packet, so resume waiting for it to elapse + notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); + } else { + if (isChannelActive()) { // check if there is currently a LoRa packet on the channel + startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again + setTransmitDelay(); + } else { + // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and + // actual transmission as short as possible + txp = txQueue.dequeue(); + assert(txp); + startSend(txp); + LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); + } + } + } } else { - if (isChannelActive()) { // check if there is currently a LoRa packet on the channel - startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again - setTransmitDelay(); - } else { - // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and - // actual transmission as short as possible - txp = txQueue.dequeue(); - assert(txp); - startSend(txp); - LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); - } + // Do nothing, because the queue is empty } - } - } else { - // Do nothing, because the queue is empty + break; + default: + assert(0); // We expected to receive a valid notification from the ISR } - break; - default: - assert(0); // We expected to receive a valid notification from the ISR - } } -void RadioLibInterface::setTransmitDelay() { - meshtastic_MeshPacket *p = txQueue.getFront(); - if (!p) { - return; // noop if there's nothing in the queue - } +void RadioLibInterface::setTransmitDelay() +{ + meshtastic_MeshPacket *p = txQueue.getFront(); + if (!p) { + return; // noop if there's nothing in the queue + } - // We want all sending/receiving to be done by our daemon thread. - // We use a delay here because this packet might have been sent in response to a packet we just received. - // So we want to make sure the other side has had a chance to reconfigure its radio. + // We want all sending/receiving to be done by our daemon thread. + // We use a delay here because this packet might have been sent in response to a packet we just received. + // So we want to make sure the other side has had a chance to reconfigure its radio. - if (p->tx_after) { - unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); - unsigned long now = millis(); - p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); - notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); - } else if (p->rx_snr == 0 && p->rx_rssi == 0) { - /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. - * This assumption is valid because of the offset generated by the radio to account for the noise - * floor. - */ - startTransmitTimer(true); - } else { - // If there is a SNR, start a timer scaled based on that SNR. - LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p); - } + if (p->tx_after) { + unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); + unsigned long now = millis(); + p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); + notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); + } else if (p->rx_snr == 0 && p->rx_rssi == 0) { + /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. + * This assumption is valid because of the offset generated by the radio to account for the noise + * floor. + */ + startTransmitTimer(true); + } else { + // If there is a SNR, start a timer scaled based on that SNR. + LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); + startTransmitTimerRebroadcast(p); + } } -void RadioLibInterface::startTransmitTimer(bool withDelay) { - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable - } +void RadioLibInterface::startTransmitTimer(bool withDelay) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + } } -void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delay = getTxDelayMsecWeighted(p); - notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable - } +void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delay = getTxDelayMsecWeighted(p); + notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable + } } /** * If the packet is not already in the late rebroadcast window, move it there */ -void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) { - // Look for non-late packets only, so we don't do this twice! - meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); - if (p) { - p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); - bool dropped = false; - if (txQueue.enqueue(p, &dropped)) { - LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); - } else { - packetPool.release(p); +void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) +{ + // Look for non-late packets only, so we don't do this twice! + meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); + if (p) { + p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); + bool dropped = false; + if (txQueue.enqueue(p, &dropped)) { + LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); + } else { + packetPool.release(p); + } + if (dropped) { + txDrop++; + } } - if (dropped) { - txDrop++; - } - } } /** - * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better - * version + * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version * @return Whether a pending packet was removed */ -bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { - meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt); - if (p) { - LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit); - packetPool.release(p); - return true; - } - return false; +bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) +{ + meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt); + if (p) { + LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit); + packetPool.release(p); + return true; + } + return false; } /** @@ -361,166 +391,176 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_ */ // void RadioLibInterface::removePending -void RadioLibInterface::handleTransmitInterrupt() { - // This can be null if we forced the device to enter standby mode. In that case - // ignore the transmit interrupt - if (sendingPacket) - completeSending(); - powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now +void RadioLibInterface::handleTransmitInterrupt() +{ + // This can be null if we forced the device to enter standby mode. In that case + // ignore the transmit interrupt + if (sendingPacket) + completeSending(); + powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now } -void RadioLibInterface::completeSending() { - // We are careful to clear sending packet before calling printPacket because - // that can take a long time - auto p = sendingPacket; - sendingPacket = NULL; +void RadioLibInterface::completeSending() +{ + // We are careful to clear sending packet before calling printPacket because + // that can take a long time + auto p = sendingPacket; + sendingPacket = NULL; - if (p) { - // Packet has been sent, count it toward our TX airtime utilization. - uint32_t xmitMsec = getPacketTime(p); - airTime->logAirtime(TX_LOG, xmitMsec); + if (p) { + // Packet has been sent, count it toward our TX airtime utilization. + uint32_t xmitMsec = getPacketTime(p); + airTime->logAirtime(TX_LOG, xmitMsec); - txGood++; - if (!isFromUs(p)) - txRelay++; - printPacket("Completed sending", p); + txGood++; + if (!isFromUs(p)) + txRelay++; + printPacket("Completed sending", p); - // We are done sending that packet, release it - packetPool.release(p); - } + // We are done sending that packet, release it + packetPool.release(p); + } } -void RadioLibInterface::handleReceiveInterrupt() { - // when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race - // Condition? - if (!isReceiving) { - LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen"); - return; - } +void RadioLibInterface::handleReceiveInterrupt() +{ + // when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race + // Condition? + if (!isReceiving) { + LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen"); + return; + } - isReceiving = false; + isReceiving = false; - // read the number of actually received bytes - size_t length = iface->getPacketLength(); + // read the number of actually received bytes + size_t length = iface->getPacketLength(); - uint32_t rxMsec = getPacketTime(length, true); + uint32_t rxMsec = getPacketTime(length, true); #ifndef DISABLE_WELCOME_UNSET - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - LOG_WARN("lora rx disabled: Region unset"); - airTime->logAirtime(RX_ALL_LOG, rxMsec); - return; - } -#endif - - int state = iface->readData((uint8_t *)&radioBuffer, length); -#if ARCH_PORTDUINO - if (portduino_config.logoutputlevel == level_trace) { - printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); - } -#endif - if (state != RADIOLIB_ERR_NONE) { - LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, radioBuffer.header.to, - radioBuffer.header.from, radioBuffer.header.flags); - rxBad++; - - airTime->logAirtime(RX_ALL_LOG, rxMsec); - - } else { - // Skip the 4 headers that are at the beginning of the rxBuf - int32_t payloadLen = length - sizeof(PacketHeader); - - // check for short packets - if (payloadLen < 0) { - LOG_WARN("Ignore received packet too short"); - rxBad++; - airTime->logAirtime(RX_ALL_LOG, rxMsec); - } else { - rxGood++; - // altered packet with "from == 0" can do Remote Node Administration without permission - if (radioBuffer.header.from == 0) { - LOG_WARN("Ignore received packet without sender"); + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + LOG_WARN("lora rx disabled: Region unset"); + airTime->logAirtime(RX_ALL_LOG, rxMsec); return; - } - - // Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous). - // This allows the router and other apps on our node to sniff packets (usually routing) between other - // nodes. - meshtastic_MeshPacket *mp = packetPool.allocZeroed(); - - // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto - mp->from = radioBuffer.header.from; - mp->to = radioBuffer.header.to; - mp->id = radioBuffer.header.id; - mp->channel = radioBuffer.header.channel; - assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code - mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; - mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; - mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); - mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); - // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) - mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; - mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; - - addReceiveMetadata(mp); - - mp->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point - assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes)); - memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen); - mp->encrypted.size = payloadLen; - - printPacket("Lora RX", mp); - - airTime->logAirtime(RX_LOG, rxMsec); - - deliverToReceiver(mp); } - } +#endif + + int state = iface->readData((uint8_t *)&radioBuffer, length); +#if ARCH_PORTDUINO + if (portduino_config.logoutputlevel == level_trace) { + printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); + } +#endif + if (state != RADIOLIB_ERR_NONE) { + LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, + radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags); + rxBad++; + + airTime->logAirtime(RX_ALL_LOG, rxMsec); + + } else { + // Skip the 4 headers that are at the beginning of the rxBuf + int32_t payloadLen = length - sizeof(PacketHeader); + + // check for short packets + if (payloadLen < 0) { + LOG_WARN("Ignore received packet too short"); + rxBad++; + airTime->logAirtime(RX_ALL_LOG, rxMsec); + } else { + rxGood++; + // altered packet with "from == 0" can do Remote Node Administration without permission + if (radioBuffer.header.from == 0) { + LOG_WARN("Ignore received packet without sender"); + return; + } + + // Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous). + // This allows the router and other apps on our node to sniff packets (usually routing) between other + // nodes. + meshtastic_MeshPacket *mp = packetPool.allocZeroed(); + + // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto + mp->from = radioBuffer.header.from; + mp->to = radioBuffer.header.to; + mp->id = radioBuffer.header.id; + mp->channel = radioBuffer.header.channel; + assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code + mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; + mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; + mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); + mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); + // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) + mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; + mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; + + addReceiveMetadata(mp); + + mp->which_payload_variant = + meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point + assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes)); + memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen); + mp->encrypted.size = payloadLen; + + printPacket("Lora RX", mp); + + airTime->logAirtime(RX_LOG, rxMsec); + + deliverToReceiver(mp); + } + } } -void RadioLibInterface::startReceive() { - isReceiving = true; - powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); +void RadioLibInterface::startReceive() +{ + isReceiving = true; + powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } -void RadioLibInterface::configHardwareForSend() { powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); } +void RadioLibInterface::configHardwareForSend() +{ + powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); +} -void RadioLibInterface::setStandby() { - // neither sending nor receiving - powerMon->clearState(meshtastic_PowerMon_State_Lora_RXOn); - powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); +void RadioLibInterface::setStandby() +{ + // neither sending nor receiving + powerMon->clearState(meshtastic_PowerMon_State_Lora_RXOn); + powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); } /** start an immediate transmit */ -bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) { - /* NOTE: Minimize the actions before startTransmit() to keep the time between - channel scan and actual transmit as low as possible to avoid collisions. */ - if (disabled || !config.lora.tx_enabled) { - LOG_WARN("Drop Tx packet because LoRa Tx disabled"); - packetPool.release(txp); - return false; - } else { - configHardwareForSend(); // must be after setStandby - - size_t numbytes = beginSending(txp); - - int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes); - if (res != RADIOLIB_ERR_NONE) { - LOG_ERROR("startTransmit failed, error=%d", res); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG); - - // This send failed, but make sure to 'complete' it properly - completeSending(); - powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now - startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode) +bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) +{ + /* NOTE: Minimize the actions before startTransmit() to keep the time between + channel scan and actual transmit as low as possible to avoid collisions. */ + if (disabled || !config.lora.tx_enabled) { + LOG_WARN("Drop Tx packet because LoRa Tx disabled"); + packetPool.release(txp); + return false; } else { - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register - // bits - enableInterrupt(isrTxLevel0); - lastTxStart = millis(); - printPacket("Started Tx", txp); - } + configHardwareForSend(); // must be after setStandby - return res == RADIOLIB_ERR_NONE; - } + size_t numbytes = beginSending(txp); + + int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes); + if (res != RADIOLIB_ERR_NONE) { + LOG_ERROR("startTransmit failed, error=%d", res); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG); + + // This send failed, but make sure to 'complete' it properly + completeSending(); + powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now + startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode) + } else { + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register + // bits + enableInterrupt(isrTxLevel0); + lastTxStart = millis(); + printPacket("Started Tx", txp); + } + + return res == RADIOLIB_ERR_NONE; + } } \ No newline at end of file diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 7b1802d0f..833c88710 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -22,14 +22,15 @@ /** * We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access */ -class LockingArduinoHal : public ArduinoHal { -public: - LockingArduinoHal(SPIClass &spi, SPISettings spiSettings) : ArduinoHal(spi, spiSettings){}; +class LockingArduinoHal : public ArduinoHal +{ + public: + LockingArduinoHal(SPIClass &spi, SPISettings spiSettings) : ArduinoHal(spi, spiSettings){}; - void spiBeginTransaction() override; - void spiEndTransaction() override; + void spiBeginTransaction() override; + void spiEndTransaction() override; #if ARCH_PORTDUINO - void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override; + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override; #endif }; @@ -38,225 +39,229 @@ public: /** * A wrapper for the RadioLib STM32WLx_Module class, that doesn't connect any pins as they are virtual */ -class STM32WLx_ModuleWrapper : public STM32WLx_Module { -public: - STM32WLx_ModuleWrapper(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : STM32WLx_Module(){}; +class STM32WLx_ModuleWrapper : public STM32WLx_Module +{ + public: + STM32WLx_ModuleWrapper(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : STM32WLx_Module(){}; }; #endif -class RadioLibInterface : public RadioInterface, protected concurrency::NotifiedWorkerThread { - /// Used as our notification from the ISR - enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; +class RadioLibInterface : public RadioInterface, protected concurrency::NotifiedWorkerThread +{ + /// Used as our notification from the ISR + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; - /** - * Raw ISR handler that just calls our polymorphic method - */ - static void isrTxLevel0(), isrLevel0Common(PendingISR code); + /** + * Raw ISR handler that just calls our polymorphic method + */ + static void isrTxLevel0(), isrLevel0Common(PendingISR code); - MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); -protected: - ModemType_t modemType = RADIOLIB_MODEM_LORA; - DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } - PacketConfig_t getPacketConfig() const { - return {.lora = {.preambleLength = preambleLength, - .implicitHeader = false, - .crcEnabled = true, - // We use auto LDRO, meaning it is enabled if the symbol time is >= 16msec - .ldrOptimize = (1 << sf) / bw >= 16}}; - } - - /** - * We use a meshtastic sync word, but hashed with the Channel name. For releases before 1.2 we used 0x12 (or for very - * old loads 0x14) Note: do not use 0x34 - that is reserved for lorawan - * - * We now use 0x2b (so that someday we can possibly use NOT 2b - because that would be funny pun). We will be staying - * with this code for a long time. - */ - const uint8_t syncWord = 0x2b; - - float currentLimit = 100; // 100mA OCP - Should be acceptable for RFM95/SX127x chipset. - -#if !defined(USE_STM32WLx) - Module module; // The HW interface to the radio -#else - STM32WLx_ModuleWrapper module; -#endif - - /** - * provides lowest common denominator RadioLib API - */ - PhysicalLayer *iface; - - /// are _trying_ to receive a packet currently (note - we might just be waiting for one) - bool isReceiving = false; - -public: - /** Our ISR code currently needs this to find our active instance - */ - static RadioLibInterface *instance; - - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() = 0; - - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*)()) = 0; - - /** - * Debugging counts - */ - uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; - uint16_t txDrop = 0; - -public: - RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, - PhysicalLayer *iface = NULL); - - virtual ErrorCode send(meshtastic_MeshPacket *p) override; - - /** - * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) - * - * This method must be used before putting the CPU into deep or light sleep. - */ - virtual bool canSleep() override; - - /** - * Start waiting to receive a message - * - * External functions can call this method to wake the device from sleep. - * Subclasses must override and call this base method - */ - virtual void startReceive(); - - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() = 0; - - /** are we actively receiving a packet (only called during receiving state) - * This method is only public to facilitate debugging. Do not call. - */ - virtual bool isActivelyReceiving() = 0; - - /** Are we are currently sending a packet? - * This method is public, intending to expose this information to other firmware components - */ - virtual bool isSending(); - - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - virtual bool cancelSending(NodeNum from, PacketId id) override; - - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - virtual bool findInTxQueue(NodeNum from, PacketId id) override; - -private: - /** if we have something waiting to send, start a short (random) timer so we can come check for collision before - * actually doing the transmit */ - void setTransmitDelay(); - - /** - * random timer with certain min. and max. settings - * @return Timestamp after which the packet may be sent - */ - void startTransmitTimer(bool withDelay = true); - - /** - * timer scaled to SNR of to be flooded packet - * @return Timestamp after which the packet may be sent - */ - void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); - - void handleTransmitInterrupt(); - void handleReceiveInterrupt(); - - static void timerCallback(void *p1, uint32_t p2); - - virtual void onNotify(uint32_t notification) override; - - /** start an immediate transmit - * This method is virtual so subclasses can hook as needed, subclasses should not call directly - * @return true if packet was sent - */ - virtual bool startSend(meshtastic_MeshPacket *txp); - - meshtastic_QueueStatus getQueueStatus(); - -protected: - uint32_t activeReceiveStart = 0; - - bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag); - - /** Do any hardware setup needed on entry into send configuration for the radio. - * Subclasses can customize, but must also call this base method */ - virtual void configHardwareForSend(); - - /** Could we send right now (i.e. either not actively receiving or transmitting)? */ - virtual bool canSendImmediately(); - - /** - * Raw ISR handler that just calls our polymorphic method - */ - static void isrRxLevel0(); - - /** - * If a send was in progress finish it and return the buffer to the pool */ - void completeSending(); - - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) = 0; - - /** - * Subclasses must override, implement and then call into this base class implementation - */ - virtual void setStandby(); - - /** - * Derive packet time either for a received (using header info) or a transmitted packet - */ - template uint32_t computePacketTime(T &lora, uint32_t pl, bool received) { - if (received) { - // First get the actual coding rate and CRC status from the received packet - uint8_t rxCR; - bool hasCRC; - lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC); - // Go from raw header value to denominator - if (rxCR < 5) { - rxCR += 4; - } else if (rxCR == 7) { - rxCR = 8; - } - - // Received packet configuration must be the same as configured, except for coding rate and CRC - DataRate_t dr = getDataRate(); - dr.lora.codingRate = rxCR; - - PacketConfig_t pc = getPacketConfig(); - pc.lora.crcEnabled = hasCRC; - - return lora.calculateTimeOnAir(modemType, dr, pc, pl) / 1000; + protected: + ModemType_t modemType = RADIOLIB_MODEM_LORA; + DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } + PacketConfig_t getPacketConfig() const + { + return {.lora = {.preambleLength = preambleLength, + .implicitHeader = false, + .crcEnabled = true, + // We use auto LDRO, meaning it is enabled if the symbol time is >= 16msec + .ldrOptimize = (1 << sf) / bw >= 16}}; } - return lora.getTimeOnAir(pl) / 1000; - } + /** + * We use a meshtastic sync word, but hashed with the Channel name. For releases before 1.2 we used 0x12 (or for very old + * loads 0x14) Note: do not use 0x34 - that is reserved for lorawan + * + * We now use 0x2b (so that someday we can possibly use NOT 2b - because that would be funny pun). We will be staying with + * this code for a long time. + */ + const uint8_t syncWord = 0x2b; - const char *radioLibErr = "RadioLib err="; + float currentLimit = 100; // 100mA OCP - Should be acceptable for RFM95/SX127x chipset. - /** - * If the packet is not already in the late rebroadcast window, move it there - */ - void clampToLateRebroadcastWindow(NodeNum from, PacketId id); +#if !defined(USE_STM32WLx) + Module module; // The HW interface to the radio +#else + STM32WLx_ModuleWrapper module; +#endif - /** - * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better - * version - * @return Whether a pending packet was removed - */ + /** + * provides lowest common denominator RadioLib API + */ + PhysicalLayer *iface; - bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; + /// are _trying_ to receive a packet currently (note - we might just be waiting for one) + bool isReceiving = false; + + public: + /** Our ISR code currently needs this to find our active instance + */ + static RadioLibInterface *instance; + + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() = 0; + + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*)()) = 0; + + /** + * Debugging counts + */ + uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; + uint16_t txDrop = 0; + + public: + RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL); + + virtual ErrorCode send(meshtastic_MeshPacket *p) override; + + /** + * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) + * + * This method must be used before putting the CPU into deep or light sleep. + */ + virtual bool canSleep() override; + + /** + * Start waiting to receive a message + * + * External functions can call this method to wake the device from sleep. + * Subclasses must override and call this base method + */ + virtual void startReceive(); + + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() = 0; + + /** are we actively receiving a packet (only called during receiving state) + * This method is only public to facilitate debugging. Do not call. + */ + virtual bool isActivelyReceiving() = 0; + + /** Are we are currently sending a packet? + * This method is public, intending to expose this information to other firmware components + */ + virtual bool isSending(); + + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) override; + + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) override; + + private: + /** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually + * doing the transmit */ + void setTransmitDelay(); + + /** + * random timer with certain min. and max. settings + * @return Timestamp after which the packet may be sent + */ + void startTransmitTimer(bool withDelay = true); + + /** + * timer scaled to SNR of to be flooded packet + * @return Timestamp after which the packet may be sent + */ + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); + + void handleTransmitInterrupt(); + void handleReceiveInterrupt(); + + static void timerCallback(void *p1, uint32_t p2); + + virtual void onNotify(uint32_t notification) override; + + /** start an immediate transmit + * This method is virtual so subclasses can hook as needed, subclasses should not call directly + * @return true if packet was sent + */ + virtual bool startSend(meshtastic_MeshPacket *txp); + + meshtastic_QueueStatus getQueueStatus(); + + protected: + uint32_t activeReceiveStart = 0; + + bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag); + + /** Do any hardware setup needed on entry into send configuration for the radio. + * Subclasses can customize, but must also call this base method */ + virtual void configHardwareForSend(); + + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately(); + + /** + * Raw ISR handler that just calls our polymorphic method + */ + static void isrRxLevel0(); + + /** + * If a send was in progress finish it and return the buffer to the pool */ + void completeSending(); + + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) = 0; + + /** + * Subclasses must override, implement and then call into this base class implementation + */ + virtual void setStandby(); + + /** + * Derive packet time either for a received (using header info) or a transmitted packet + */ + template uint32_t computePacketTime(T &lora, uint32_t pl, bool received) + { + if (received) { + // First get the actual coding rate and CRC status from the received packet + uint8_t rxCR; + bool hasCRC; + lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC); + // Go from raw header value to denominator + if (rxCR < 5) { + rxCR += 4; + } else if (rxCR == 7) { + rxCR = 8; + } + + // Received packet configuration must be the same as configured, except for coding rate and CRC + DataRate_t dr = getDataRate(); + dr.lora.codingRate = rxCR; + + PacketConfig_t pc = getPacketConfig(); + pc.lora.crcEnabled = hasCRC; + + return lora.calculateTimeOnAir(modemType, dr, pc, pl) / 1000; + } + + return lora.getTimeOnAir(pl) / 1000; + } + + const char *radioLibErr = "RadioLib err="; + + /** + * If the packet is not already in the late rebroadcast window, move it there + */ + void clampToLateRebroadcastWindow(NodeNum from, PacketId id); + + /** + * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version + * @return Whether a pending packet was removed + */ + + bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; }; diff --git a/src/mesh/RadioLibRF95.cpp b/src/mesh/RadioLibRF95.cpp index d22daab41..a34c0605f 100644 --- a/src/mesh/RadioLibRF95.cpp +++ b/src/mesh/RadioLibRF95.cpp @@ -7,57 +7,60 @@ RadioLibRF95::RadioLibRF95(Module *mod) : SX1278(mod) {} -int16_t RadioLibRF95::begin(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t syncWord, int8_t power, uint16_t preambleLength, uint8_t gain) { - // execute common part - uint8_t rf95versions[2] = {0x12, 0x11}; - int16_t state = SX127x::begin(rf95versions, sizeof(rf95versions), syncWord, preambleLength); - RADIOLIB_ASSERT(state); +int16_t RadioLibRF95::begin(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t syncWord, int8_t power, uint16_t preambleLength, + uint8_t gain) +{ + // execute common part + uint8_t rf95versions[2] = {0x12, 0x11}; + int16_t state = SX127x::begin(rf95versions, sizeof(rf95versions), syncWord, preambleLength); + RADIOLIB_ASSERT(state); - // current limit was removed from module' ctor - // override default value (60 mA) - state = setCurrentLimit(currentLimit); - LOG_DEBUG("Current limit set to %f", currentLimit); - LOG_DEBUG("Current limit set result %d", state); + // current limit was removed from module' ctor + // override default value (60 mA) + state = setCurrentLimit(currentLimit); + LOG_DEBUG("Current limit set to %f", currentLimit); + LOG_DEBUG("Current limit set result %d", state); - // configure settings not accessible by API - // state = config(); - RADIOLIB_ASSERT(state); + // configure settings not accessible by API + // state = config(); + RADIOLIB_ASSERT(state); #ifdef RF95_TCXO - state = _mod->SPIsetRegValue(RADIOLIB_SX127X_REG_TCXO, 0x10 | _mod->SPIgetRegValue(RADIOLIB_SX127X_REG_TCXO)); - RADIOLIB_ASSERT(state); + state = _mod->SPIsetRegValue(RADIOLIB_SX127X_REG_TCXO, 0x10 | _mod->SPIgetRegValue(RADIOLIB_SX127X_REG_TCXO)); + RADIOLIB_ASSERT(state); #endif - // configure publicly accessible settings - state = setFrequency(freq); - RADIOLIB_ASSERT(state); + // configure publicly accessible settings + state = setFrequency(freq); + RADIOLIB_ASSERT(state); - state = setBandwidth(bw); - RADIOLIB_ASSERT(state); + state = setBandwidth(bw); + RADIOLIB_ASSERT(state); - state = setSpreadingFactor(sf); - RADIOLIB_ASSERT(state); + state = setSpreadingFactor(sf); + RADIOLIB_ASSERT(state); - state = setCodingRate(cr); - RADIOLIB_ASSERT(state); + state = setCodingRate(cr); + RADIOLIB_ASSERT(state); #ifdef USE_RF95_RFO - state = setOutputPower(power, true); + state = setOutputPower(power, true); #else - state = setOutputPower(power); + state = setOutputPower(power); #endif - RADIOLIB_ASSERT(state); + RADIOLIB_ASSERT(state); - state = setGain(gain); + state = setGain(gain); - return (state); + return (state); } -int16_t RadioLibRF95::setFrequency(float freq) { - // RADIOLIB_CHECK_RANGE(freq, 862.0, 1020.0, ERR_INVALID_FREQUENCY); +int16_t RadioLibRF95::setFrequency(float freq) +{ + // RADIOLIB_CHECK_RANGE(freq, 862.0, 1020.0, ERR_INVALID_FREQUENCY); - // set frequency - return (SX127x::setFrequencyRaw(freq)); + // set frequency + return (SX127x::setFrequencyRaw(freq)); } #define RH_RF95_MODEM_STATUS_CLEAR 0x10 @@ -66,15 +69,18 @@ int16_t RadioLibRF95::setFrequency(float freq) { #define RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED 0x02 #define RH_RF95_MODEM_STATUS_SIGNAL_DETECTED 0x01 -bool RadioLibRF95::isReceiving() { - // 0x0b == Look for header info valid, signal synchronized or signal detected - uint8_t reg = readReg(RADIOLIB_SX127X_REG_MODEM_STAT); - // Serial.printf("reg %x", reg); - return (reg & (RH_RF95_MODEM_STATUS_SIGNAL_DETECTED | RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED | RH_RF95_MODEM_STATUS_HEADER_INFO_VALID)) != 0; +bool RadioLibRF95::isReceiving() +{ + // 0x0b == Look for header info valid, signal synchronized or signal detected + uint8_t reg = readReg(RADIOLIB_SX127X_REG_MODEM_STAT); + // Serial.printf("reg %x", reg); + return (reg & (RH_RF95_MODEM_STATUS_SIGNAL_DETECTED | RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED | + RH_RF95_MODEM_STATUS_HEADER_INFO_VALID)) != 0; } -uint8_t RadioLibRF95::readReg(uint8_t addr) { - Module *mod = this->getMod(); - return mod->SPIreadRegister(addr); +uint8_t RadioLibRF95::readReg(uint8_t addr) +{ + Module *mod = this->getMod(); + return mod->SPIreadRegister(addr); } #endif \ No newline at end of file diff --git a/src/mesh/RadioLibRF95.h b/src/mesh/RadioLibRF95.h index c4bcafa5e..916a33234 100644 --- a/src/mesh/RadioLibRF95.h +++ b/src/mesh/RadioLibRF95.h @@ -7,66 +7,67 @@ \brief Derived class for %RFM95 modules. Overrides some methods from SX1278 due to different parameter ranges. */ -class RadioLibRF95 : public SX1278 { -public: - // constructor +class RadioLibRF95 : public SX1278 +{ + public: + // constructor - /*! - \brief Default constructor. Called from Arduino sketch when creating new LoRa instance. + /*! + \brief Default constructor. Called from Arduino sketch when creating new LoRa instance. - \param mod Instance of Module that will be used to communicate with the %LoRa chip. - */ - explicit RadioLibRF95(Module *mod); + \param mod Instance of Module that will be used to communicate with the %LoRa chip. + */ + explicit RadioLibRF95(Module *mod); - // basic methods + // basic methods - /*! - \brief %LoRa modem initialization method. Must be called at least once from Arduino sketch to initialize the module. + /*! + \brief %LoRa modem initialization method. Must be called at least once from Arduino sketch to initialize the module. - \param freq Carrier frequency in MHz. Allowed values range from 868.0 MHz to 915.0 MHz. + \param freq Carrier frequency in MHz. Allowed values range from 868.0 MHz to 915.0 MHz. - \param bw %LoRa link bandwidth in kHz. Allowed values are 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250 and 500 kHz. + \param bw %LoRa link bandwidth in kHz. Allowed values are 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250 and 500 kHz. - \param sf %LoRa link spreading factor. Allowed values range from 6 to 12. + \param sf %LoRa link spreading factor. Allowed values range from 6 to 12. - \param cr %LoRa link coding rate denominator. Allowed values range from 5 to 8. + \param cr %LoRa link coding rate denominator. Allowed values range from 5 to 8. - \param syncWord %LoRa sync word. Can be used to distinguish different networks. Note that value 0x34 is reserved for - LoRaWAN networks. + \param syncWord %LoRa sync word. Can be used to distinguish different networks. Note that value 0x34 is reserved for LoRaWAN + networks. - \param power Transmission output power in dBm. Allowed values range from 2 to 17 dBm. + \param power Transmission output power in dBm. Allowed values range from 2 to 17 dBm. - \param preambleLength Length of %LoRa transmission preamble in symbols. The actual preamble length is 4.25 symbols - longer than the set number. Allowed values range from 6 to 65535. + \param preambleLength Length of %LoRa transmission preamble in symbols. The actual preamble length is 4.25 symbols longer + than the set number. Allowed values range from 6 to 65535. - \param gain Gain of receiver LNA (low-noise amplifier). Can be set to any integer in range 1 to 6 where 1 is the - highest gain. Set to 0 to enable automatic gain control (recommended). + \param gain Gain of receiver LNA (low-noise amplifier). Can be set to any integer in range 1 to 6 where 1 is the highest + gain. Set to 0 to enable automatic gain control (recommended). - \returns \ref status_codes - */ - int16_t begin(float freq = 915.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, uint8_t syncWord = RADIOLIB_SX127X_SYNC_WORD, int8_t power = 17, - uint16_t preambleLength = 8, uint8_t gain = 0); + \returns \ref status_codes + */ + int16_t begin(float freq = 915.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_SX127X_SYNC_WORD, int8_t power = 17, uint16_t preambleLength = 8, uint8_t gain = 0); - // configuration methods + // configuration methods - /*! - \brief Sets carrier frequency. Allowed values range from 868.0 MHz to 915.0 MHz. + /*! + \brief Sets carrier frequency. Allowed values range from 868.0 MHz to 915.0 MHz. - \param freq Carrier frequency to be set in MHz. + \param freq Carrier frequency to be set in MHz. - \returns \ref status_codes - */ - int16_t setFrequency(float freq); + \returns \ref status_codes + */ + int16_t setFrequency(float freq); - // Return true if we are actively receiving a message currently - bool isReceiving(); + // Return true if we are actively receiving a message currently + bool isReceiving(); - /// For debugging - uint8_t readReg(uint8_t addr); + /// For debugging + uint8_t readReg(uint8_t addr); -protected: - // since default current limit for SX126x/127x in updated RadioLib is 60mA - // use the previous value - float currentLimit = 100; + protected: + // since default current limit for SX126x/127x in updated RadioLib is 60mA + // use the previous value + float currentLimit = 100; }; #endif \ No newline at end of file diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index ed0b1a48b..2b9b17183 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -14,76 +14,78 @@ * If the message is want_ack, then add it to a list of packets to retransmit. * If we run out of retransmissions, send a nak packet towards the original client to indicate failure. */ -ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) { - if (p->want_ack) { - // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives - // our message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference - // on hop counts and we want this message to get through the whole mesh, so use the default. - if (p->hop_limit == 0) { - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); +ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) +{ + if (p->want_ack) { + // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our + // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop + // counts and we want this message to get through the whole mesh, so use the default. + if (p->hop_limit == 0) { + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + } + DEBUG_HEAP_BEFORE; + auto copy = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("ReliableRouter::send", copy); + + startRetransmission(copy, NUM_RELIABLE_RETX); } - DEBUG_HEAP_BEFORE; - auto copy = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("ReliableRouter::send", copy); - startRetransmission(copy, NUM_RELIABLE_RETX); - } - - /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot - receive an (implicit) ACK. Otherwise, we might retransmit too early. - */ - for (auto i = pending.begin(); i != pending.end(); i++) { - if (i->first.id != p->id) { - i->second.nextTxMsec += iface->getPacketTime(p); + /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an + (implicit) ACK. Otherwise, we might retransmit too early. + */ + for (auto i = pending.begin(); i != pending.end(); i++) { + if (i->first.id != p->id) { + i->second.nextTxMsec += iface->getPacketTime(p); + } } - } - return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p); + return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p); } -bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { - // Note: do not use getFrom() here, because we want to ignore messages sent from phone - if (p->from == getNodeNum()) { - printPacket("Rx someone rebroadcasting for us", p); +bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) +{ + // Note: do not use getFrom() here, because we want to ignore messages sent from phone + if (p->from == getNodeNum()) { + printPacket("Rx someone rebroadcasting for us", p); - // We are seeing someone rebroadcast one of our broadcast attempts. - // If this is the first time we saw this, cancel any retransmissions we have queued up and generate an internal ack - // for the original sending process. + // We are seeing someone rebroadcast one of our broadcast attempts. + // If this is the first time we saw this, cancel any retransmissions we have queued up and generate an internal ack for + // the original sending process. - // This "optimization", does save lots of airtime. For DMs, you also get a real ACK back - // from the intended recipient. - auto key = GlobalPacketId(getFrom(p), p->id); - auto old = findPendingPacket(key); - if (old) { - LOG_DEBUG("Generate implicit ack"); - // NOTE: we do NOT check p->wantAck here because p is the INCOMING rebroadcast and that packet is not expected to - // be marked as wantAck - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); + // This "optimization", does save lots of airtime. For DMs, you also get a real ACK back + // from the intended recipient. + auto key = GlobalPacketId(getFrom(p), p->id); + auto old = findPendingPacket(key); + if (old) { + LOG_DEBUG("Generate implicit ack"); + // NOTE: we do NOT check p->wantAck here because p is the INCOMING rebroadcast and that packet is not expected to be + // marked as wantAck + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); - // Only stop retransmissions if the rebroadcast came via LoRa - if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { - stopRetransmission(key); - } - } else { - LOG_DEBUG("Didn't find pending packet"); + // Only stop retransmissions if the rebroadcast came via LoRa + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + stopRetransmission(key); + } + } else { + LOG_DEBUG("Didn't find pending packet"); + } } - } - /* At this point we have already deleted the pending retransmission if this packet was an (implicit) ACK to it. - Now for all other pending retransmissions, we have to add the airtime of this received packet to the retransmission - timer, because while receiving this packet, we could not have received an (implicit) ACK for it. If we don't add - this, we will likely retransmit too early. - */ - for (auto i = pending.begin(); i != pending.end(); i++) { - i->second.nextTxMsec += iface->getPacketTime(p, true); - } + /* At this point we have already deleted the pending retransmission if this packet was an (implicit) ACK to it. + Now for all other pending retransmissions, we have to add the airtime of this received packet to the retransmission timer, + because while receiving this packet, we could not have received an (implicit) ACK for it. + If we don't add this, we will likely retransmit too early. + */ + for (auto i = pending.begin(); i != pending.end(); i++) { + i->second.nextTxMsec += iface->getPacketTime(p, true); + } - return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p); + return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p); } /** - * If we receive a want_ack packet (do not check for wasSeenRecently), send back an ack (this might generate multiple - * ack sends in case the our first ack gets lost) + * If we receive a want_ack packet (do not check for wasSeenRecently), send back an ack (this might generate multiple ack sends in + * case the our first ack gets lost) * * If we receive an ack packet (do check wasSeenRecently), clear out any retransmissions and * forward the ack to the application layer. @@ -93,100 +95,106 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { * * Otherwise, let superclass handle it. */ -void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { - if (isToUs(p)) { // ignore ack/nak/want_ack packets that are not address to us (we only handle 0 hop reliability) - if (!MeshModule::currentReply) { - if (p->want_ack) { - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - /* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received - an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to - make sure the other side stops retransmitting. */ +void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) +{ + if (isToUs(p)) { // ignore ack/nak/want_ack packets that are not address to us (we only handle 0 hop reliability) + if (!MeshModule::currentReply) { + if (p->want_ack) { + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + /* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received + an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to + make sure the other side stops retransmitting. */ - if (shouldSuccessAckWithWantAck(p)) { - // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we - // do that unconditionally. - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, routingModule->getHopLimitForResponse(*p), true); - } else if (!p->decoded.request_id && !p->decoded.reply_id) { - // If it's not an ACK or a reply, send an ACK. - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, routingModule->getHopLimitForResponse(*p)); - } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { - // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender - // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to - // stop the immediate relayer's retransmissions. - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } - } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && - (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { - LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); - sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(*p)); + if (shouldSuccessAckWithWantAck(p)) { + // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we + // do that unconditionally. + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, + routingModule->getHopLimitForResponse(*p), true); + } else if (!p->decoded.request_id && !p->decoded.reply_id) { + // If it's not an ACK or a reply, send an ACK. + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, + routingModule->getHopLimitForResponse(*p)); + } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { + // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender + // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to + // stop the immediate relayer's retransmissions. + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } + } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && + (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { + LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); + sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), + routingModule->getHopLimitForResponse(*p)); + } else { + // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded + sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), + routingModule->getHopLimitForResponse(*p)); + } + } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { + // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } } else { - // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded - sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), routingModule->getHopLimitForResponse(*p)); + LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); } - } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { - // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } - } else { - LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); - } - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c && c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { - if (owner.public_key.size == 32) { - LOG_INFO("PKI decrypt failure, send a NodeInfo"); - nodeInfoModule->sendOurNodeInfo(p->from, false, p->channel, true); - } - } - // We consider an ack to be either a !routing packet with a request ID or a routing packet with !error - PacketId ackId = ((c && c->error_reason == meshtastic_Routing_Error_NONE) || !c) ? p->decoded.request_id : 0; + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c && + c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { + if (owner.public_key.size == 32) { + LOG_INFO("PKI decrypt failure, send a NodeInfo"); + nodeInfoModule->sendOurNodeInfo(p->from, false, p->channel, true); + } + } + // We consider an ack to be either a !routing packet with a request ID or a routing packet with !error + PacketId ackId = ((c && c->error_reason == meshtastic_Routing_Error_NONE) || !c) ? p->decoded.request_id : 0; - // A nak is a routing packt that has an error code - PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; + // A nak is a routing packt that has an error code + PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; - // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission - // records - if ((ackId || nakId) && - // Implicit ACKs from MQTT should not stop retransmissions - !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { - LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); - if (ackId) { - stopRetransmission(p->to, ackId); - } else { - stopRetransmission(p->to, nakId); - } + // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records + if ((ackId || nakId) && + // Implicit ACKs from MQTT should not stop retransmissions + !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { + LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); + if (ackId) { + stopRetransmission(p->to, ackId); + } else { + stopRetransmission(p->to, nakId); + } + } } - } - // handle the packet as normal - isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c); + // handle the packet as normal + isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c); } /** * If we ACK this packet, should we set want_ack=true on the ACK for reliable delivery of the ACK packet? */ -bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p) { - // Don't ACK-with-want-ACK outgoing packets - if (isFromUs(p)) +bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p) +{ + // Don't ACK-with-want-ACK outgoing packets + if (isFromUs(p)) + return false; + + // Only ACK-with-want-ACK if the original packet asked for want_ack + if (!p->want_ack) + return false; + + // Only ACK-with-want-ACK packets to us (not broadcast) + if (!isToUs(p)) + return false; + + // Special case for text message DMs: + bool isTextMessage = + (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && + IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP); + + if (isTextMessage) { + // If it's a non-broadcast text message, and the original asked for want_ack, + // let's send an ACK that is itself want_ack to improve reliability of confirming delivery back to the sender. + // This should include all DMs regardless of whether or not reply_id is set. + return true; + } + return false; - - // Only ACK-with-want-ACK if the original packet asked for want_ack - if (!p->want_ack) - return false; - - // Only ACK-with-want-ACK packets to us (not broadcast) - if (!isToUs(p)) - return false; - - // Special case for text message DMs: - bool isTextMessage = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && - IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP); - - if (isTextMessage) { - // If it's a non-broadcast text message, and the original asked for want_ack, - // let's send an ACK that is itself want_ack to improve reliability of confirming delivery back to the sender. - // This should include all DMs regardless of whether or not reply_id is set. - return true; - } - - return false; } \ No newline at end of file diff --git a/src/mesh/ReliableRouter.h b/src/mesh/ReliableRouter.h index bf567551d..33121de6b 100644 --- a/src/mesh/ReliableRouter.h +++ b/src/mesh/ReliableRouter.h @@ -5,35 +5,36 @@ /** * This is a mixin that extends Router with the ability to do (one hop only) reliable message sends. */ -class ReliableRouter : public NextHopRouter { -public: - /** - * Constructor - * - */ - // ReliableRouter(); +class ReliableRouter : public NextHopRouter +{ + public: + /** + * Constructor + * + */ + // ReliableRouter(); - /** - * Send a packet on a suitable interface. This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - */ - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + /** + * Send a packet on a suitable interface. This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + */ + virtual ErrorCode send(meshtastic_MeshPacket *p) override; -protected: - /** - * Look for acks/naks or someone retransmitting us - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + protected: + /** + * Look for acks/naks or someone retransmitting us + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - /** - * We hook this method so we can see packets before FloodingRouter says they should be discarded - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; + /** + * We hook this method so we can see packets before FloodingRouter says they should be discarded + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; -private: - /** - * Should this packet be ACKed with a want_ack for reliable delivery? - */ - bool shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p); + private: + /** + * Should this packet be ACKed with a want_ack for reliable delivery? + */ + bool shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p); }; \ No newline at end of file diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index f840cfca1..6b197f3eb 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -23,33 +23,34 @@ #include "serialization/MeshPacketSerializer.h" #endif -#define MAX_RX_FROMRADIO 4 // max number of packets destined to our queue, we dispatch packets quickly so it doesn't need to be big +#define MAX_RX_FROMRADIO \ + 4 // max number of packets destined to our queue, we dispatch packets quickly so it doesn't need to be big // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment #ifdef ARCH_PORTDUINO // Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes -#define MAX_PACKETS \ - (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ - 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +#define MAX_PACKETS \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM) -// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this -// statically. For now, make it dynamic again. -#define MAX_PACKETS \ - (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ - 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically. +// For now, make it dynamic again. +#define MAX_PACKETS \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants -#define MAX_PACKETS_STATIC \ - (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ - 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +#define MAX_PACKETS_STATIC \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryPool staticPool; Allocator &packetPool = staticPool; @@ -62,204 +63,220 @@ static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); * * Currently we only allow one interface, that may change in the future */ -Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRADIO) { - // This is called pre main(), don't touch anything here, the following code is not safe +Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRADIO) +{ + // This is called pre main(), don't touch anything here, the following code is not safe - /* LOG_DEBUG("Size of NodeInfo %d", sizeof(NodeInfo)); - LOG_DEBUG("Size of SubPacket %d", sizeof(SubPacket)); - LOG_DEBUG("Size of MeshPacket %d", sizeof(MeshPacket)); */ + /* LOG_DEBUG("Size of NodeInfo %d", sizeof(NodeInfo)); + LOG_DEBUG("Size of SubPacket %d", sizeof(SubPacket)); + LOG_DEBUG("Size of MeshPacket %d", sizeof(MeshPacket)); */ - fromRadioQueue.setReader(this); + fromRadioQueue.setReader(this); - // init Lockguard for crypt operations - assert(!cryptLock); - cryptLock = new concurrency::Lock(); + // init Lockguard for crypt operations + assert(!cryptLock); + cryptLock = new concurrency::Lock(); } -bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) { - // First hop MUST always decrement to prevent retry issues - if (getHopsAway(*p) == 0) { - return true; // Always decrement on first hop - } +bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) +{ + // First hop MUST always decrement to prevent retry issues + if (getHopsAway(*p) == 0) { + return true; // Always decrement on first hop + } - // Check if both local device and previous relay are routers (including CLIENT_BASE) - bool localIsRouter = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, - meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); + // Check if both local device and previous relay are routers (including CLIENT_BASE) + bool localIsRouter = + IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); - // If local device isn't a router, always decrement - if (!localIsRouter) { + // If local device isn't a router, always decrement + if (!localIsRouter) { + return true; + } + + // For subsequent hops, check if previous relay is a favorite router + // Optimized search for favorite routers with matching last byte + // Check ordering optimized for IoT devices (cheapest checks first) + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node) + continue; + + // Check 1: is_favorite (cheapest - single bool) + if (!node->is_favorite) + continue; + + // Check 2: has_user (cheap - single bool) + if (!node->has_user) + continue; + + // Check 3: role check (moderate cost - multiple comparisons) + if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + continue; + } + + // Check 4: last byte extraction and comparison (most expensive) + if (nodeDB->getLastByteOfNodeNum(node->num) == p->relay_node) { + // Found a favorite router match + LOG_DEBUG("Identified favorite relay router 0x%x from last byte 0x%x", node->num, p->relay_node); + return false; // Don't decrement hop_limit + } + } + + // No favorite router match found, decrement hop_limit return true; - } - - // For subsequent hops, check if previous relay is a favorite router - // Optimized search for favorite routers with matching last byte - // Check ordering optimized for IoT devices (cheapest checks first) - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node) - continue; - - // Check 1: is_favorite (cheapest - single bool) - if (!node->is_favorite) - continue; - - // Check 2: has_user (cheap - single bool) - if (!node->has_user) - continue; - - // Check 3: role check (moderate cost - multiple comparisons) - if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { - continue; - } - - // Check 4: last byte extraction and comparison (most expensive) - if (nodeDB->getLastByteOfNodeNum(node->num) == p->relay_node) { - // Found a favorite router match - LOG_DEBUG("Identified favorite relay router 0x%x from last byte 0x%x", node->num, p->relay_node); - return false; // Don't decrement hop_limit - } - } - - // No favorite router match found, decrement hop_limit - return true; } /** * do idle processing * Mostly looking in our incoming rxPacket queue and calling handleReceived. */ -int32_t Router::runOnce() { - meshtastic_MeshPacket *mp; - while ((mp = fromRadioQueue.dequeuePtr(0)) != NULL) { - // printPacket("handle fromRadioQ", mp); - perhapsHandleReceived(mp); - } +int32_t Router::runOnce() +{ + meshtastic_MeshPacket *mp; + while ((mp = fromRadioQueue.dequeuePtr(0)) != NULL) { + // printPacket("handle fromRadioQ", mp); + perhapsHandleReceived(mp); + } - // LOG_DEBUG("Sleep forever!"); - return INT32_MAX; // Wait a long time - until we get woken for the message queue + // LOG_DEBUG("Sleep forever!"); + return INT32_MAX; // Wait a long time - until we get woken for the message queue } /** - * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible - * for freeing the packet + * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for + * freeing the packet */ -void Router::enqueueReceivedMessage(meshtastic_MeshPacket *p) { - // Try enqueue until successful - while (!fromRadioQueue.enqueue(p, 0)) { - meshtastic_MeshPacket *old_p; - old_p = fromRadioQueue.dequeuePtr(0); // Dequeue and discard the oldest packet - if (old_p) { - printPacket("fromRadioQ full, drop oldest!", old_p); - packetPool.release(old_p); +void Router::enqueueReceivedMessage(meshtastic_MeshPacket *p) +{ + // Try enqueue until successful + while (!fromRadioQueue.enqueue(p, 0)) { + meshtastic_MeshPacket *old_p; + old_p = fromRadioQueue.dequeuePtr(0); // Dequeue and discard the oldest packet + if (old_p) { + printPacket("fromRadioQ full, drop oldest!", old_p); + packetPool.release(old_p); + } } - } - // Nasty hack because our threading is primitive. interfaces shouldn't need to know about routers FIXME - setReceivedMessage(); + // Nasty hack because our threading is primitive. interfaces shouldn't need to know about routers FIXME + setReceivedMessage(); } /// Generate a unique packet id // FIXME, move this someplace better -PacketId generatePacketId() { - static uint32_t rollingPacketId; // Note: trying to keep this in noinit didn't help for working across reboots - static bool didInit = false; +PacketId generatePacketId() +{ + static uint32_t rollingPacketId; // Note: trying to keep this in noinit didn't help for working across reboots + static bool didInit = false; - if (!didInit) { - didInit = true; + if (!didInit) { + didInit = true; - // pick a random initial sequence number at boot (to prevent repeated reboots always starting at 0) - // Note: we mask the high order bit to ensure that we never pass a 'negative' number to random - rollingPacketId = random(UINT32_MAX & 0x7fffffff); - LOG_DEBUG("Initial packet id %u", rollingPacketId); - } + // pick a random initial sequence number at boot (to prevent repeated reboots always starting at 0) + // Note: we mask the high order bit to ensure that we never pass a 'negative' number to random + rollingPacketId = random(UINT32_MAX & 0x7fffffff); + LOG_DEBUG("Initial packet id %u", rollingPacketId); + } - rollingPacketId++; + rollingPacketId++; - rollingPacketId &= ID_COUNTER_MASK; // Mask out the top 22 bits - PacketId id = rollingPacketId | random(UINT32_MAX & 0x7fffffff) << 10; // top 22 bits - LOG_DEBUG("Partially randomized packet id %u", id); - return id; + rollingPacketId &= ID_COUNTER_MASK; // Mask out the top 22 bits + PacketId id = rollingPacketId | random(UINT32_MAX & 0x7fffffff) << 10; // top 22 bits + LOG_DEBUG("Partially randomized packet id %u", id); + return id; } -meshtastic_MeshPacket *Router::allocForSending() { - meshtastic_MeshPacket *p = packetPool.allocZeroed(); +meshtastic_MeshPacket *Router::allocForSending() +{ + meshtastic_MeshPacket *p = packetPool.allocZeroed(); - p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // Assume payload is decoded at start. - p->from = nodeDB->getNodeNum(); - p->to = NODENUM_BROADCAST; - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - p->id = generatePacketId(); - p->rx_time = getValidTime(RTCQualityFromNet); // Just in case we process the packet locally - make sure it has a valid timestamp + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // Assume payload is decoded at start. + p->from = nodeDB->getNodeNum(); + p->to = NODENUM_BROADCAST; + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + p->id = generatePacketId(); + p->rx_time = + getValidTime(RTCQualityFromNet); // Just in case we process the packet locally - make sure it has a valid timestamp - return p; + return p; } /** * Send an ack or a nak packet back towards whoever sent idFrom */ -void Router::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, bool ackWantsAck) { - routingModule->sendAckNak(err, to, idFrom, chIndex, hopLimit, ackWantsAck); +void Router::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, + bool ackWantsAck) +{ + routingModule->sendAckNak(err, to, idFrom, chIndex, hopLimit, ackWantsAck); } -void Router::abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p) { - LOG_ERROR("Error=%d, return NAK and drop packet", err); - sendAckNak(err, getFrom(p), p->id, p->channel); - packetPool.release(p); +void Router::abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p) +{ + LOG_ERROR("Error=%d, return NAK and drop packet", err); + sendAckNak(err, getFrom(p), p->id, p->channel); + packetPool.release(p); } -void Router::setReceivedMessage() { - // LOG_DEBUG("set interval to ASAP"); - setInterval(0); // Run ASAP, so we can figure out our correct sleep time - runASAP = true; +void Router::setReceivedMessage() +{ + // LOG_DEBUG("set interval to ASAP"); + setInterval(0); // Run ASAP, so we can figure out our correct sleep time + runASAP = true; } -meshtastic_QueueStatus Router::getQueueStatus() { - if (!iface) { - meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; - return qs; - } else - return iface->getQueueStatus(); +meshtastic_QueueStatus Router::getQueueStatus() +{ + if (!iface) { + meshtastic_QueueStatus qs; + qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; + return qs; + } else + return iface->getQueueStatus(); } -ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) { - if (p->to == 0) { - LOG_ERROR("Packet received with to: of 0!"); - } - // No need to deliver externally if the destination is the local node - if (isToUs(p)) { - printPacket("Enqueued local", p); - enqueueReceivedMessage(p); - return ERRNO_OK; - } else if (!iface) { - // We must be sending to remote nodes also, fail if no interface found - abortSendAndNak(meshtastic_Routing_Error_NO_INTERFACE, p); - - return ERRNO_NO_INTERFACES; - } else { - // If we are sending a broadcast, we also treat it as if we just received it ourself - // this allows local apps (and PCs) to see broadcasts sourced locally - if (isBroadcast(p->to)) { - handleReceived(p, src); +ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) +{ + if (p->to == 0) { + LOG_ERROR("Packet received with to: of 0!"); } + // No need to deliver externally if the destination is the local node + if (isToUs(p)) { + printPacket("Enqueued local", p); + enqueueReceivedMessage(p); + return ERRNO_OK; + } else if (!iface) { + // We must be sending to remote nodes also, fail if no interface found + abortSendAndNak(meshtastic_Routing_Error_NO_INTERFACE, p); - // don't override if a channel was requested and no need to set it when PKI is enforced - if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) { - meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to); - if (node) { - p->channel = node->channel; - LOG_DEBUG("localSend to channel %d", p->channel); - } + return ERRNO_NO_INTERFACES; + } else { + // If we are sending a broadcast, we also treat it as if we just received it ourself + // this allows local apps (and PCs) to see broadcasts sourced locally + if (isBroadcast(p->to)) { + handleReceived(p, src); + } + + // don't override if a channel was requested and no need to set it when PKI is enforced + if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) { + meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to); + if (node) { + p->channel = node->channel; + LOG_DEBUG("localSend to channel %d", p->channel); + } + } + + return send(p); } - - return send(p); - } } /** * Send a packet on a suitable interface. */ -ErrorCode Router::rawSend(meshtastic_MeshPacket *p) { - assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) - return iface->send(p); +ErrorCode Router::rawSend(meshtastic_MeshPacket *p) +{ + assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) + return iface->send(p); } /** @@ -267,512 +284,534 @@ ErrorCode Router::rawSend(meshtastic_MeshPacket *p) { * later free() the packet to pool. This routine is not allowed to stall. * If the txmit queue is full it might return an error. */ -ErrorCode Router::send(meshtastic_MeshPacket *p) { - if (isToUs(p)) { - LOG_ERROR("BUG! send() called with packet destined for local node!"); - packetPool.release(p); - return meshtastic_Routing_Error_BAD_REQUEST; - } // should have already been handled by sendLocal - - // Abort sending if we are violating the duty cycle - if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { - float hourlyTxPercent = airTime->utilizationTXPercent(); - if (hourlyTxPercent > myRegion->dutyCycle) { - uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); - - LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); - - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->has_reply_id = true; - cn->reply_id = p->id; - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); - service->sendClientNotification(cn); - - meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; - if (isFromUs(p)) { // only send NAK to API, not to the mesh - abortSendAndNak(err, p); - } else { +ErrorCode Router::send(meshtastic_MeshPacket *p) +{ + if (isToUs(p)) { + LOG_ERROR("BUG! send() called with packet destined for local node!"); packetPool.release(p); - } - return err; + return meshtastic_Routing_Error_BAD_REQUEST; + } // should have already been handled by sendLocal + + // Abort sending if we are violating the duty cycle + if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { + float hourlyTxPercent = airTime->utilizationTXPercent(); + if (hourlyTxPercent > myRegion->dutyCycle) { + uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); + + LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->has_reply_id = true; + cn->reply_id = p->id; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); + service->sendClientNotification(cn); + + meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; + if (isFromUs(p)) { // only send NAK to API, not to the mesh + abortSendAndNak(err, p); + } else { + packetPool.release(p); + } + return err; + } } - } - // PacketId nakId = p->decoded.which_ackVariant == SubPacket_fail_id_tag ? p->decoded.ackVariant.fail_id : 0; - // assert(!nakId); // I don't think we ever send 0hop naks over the wire (other than to the phone), test that - // assumption with assert + // PacketId nakId = p->decoded.which_ackVariant == SubPacket_fail_id_tag ? p->decoded.ackVariant.fail_id : 0; + // assert(!nakId); // I don't think we ever send 0hop naks over the wire (other than to the phone), test that assumption with + // assert - // Never set the want_ack flag on broadcast packets sent over the air. - if (isBroadcast(p->to)) - p->want_ack = false; + // Never set the want_ack flag on broadcast packets sent over the air. + if (isBroadcast(p->to)) + p->want_ack = false; - // Up until this point we might have been using 0 for the from address (if it started with the phone), but when we - // send over the lora we need to make sure we have replaced it with our local address - p->from = getFrom(p); + // Up until this point we might have been using 0 for the from address (if it started with the phone), but when we send over + // the lora we need to make sure we have replaced it with our local address + p->from = getFrom(p); - p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us - // If we are the original transmitter, set the hop limit with which we start - if (isFromUs(p)) - p->hop_start = p->hop_limit; + p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us + // If we are the original transmitter, set the hop limit with which we start + if (isFromUs(p)) + p->hop_start = p->hop_limit; - // If the packet hasn't yet been encrypted, do so now (it might already be encrypted if we are just forwarding it) + // If the packet hasn't yet been encrypted, do so now (it might already be encrypted if we are just forwarding it) - if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag || p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) { - return meshtastic_Routing_Error_BAD_REQUEST; - } - - fixPriority(p); // Before encryption, fix the priority if it's unset - - // If the packet is not yet encrypted, do so now - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it - - DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("Router::send", p_decoded); - - auto encodeResult = perhapsEncode(p); - if (encodeResult != meshtastic_Routing_Error_NONE) { - packetPool.release(p_decoded); - p->channel = 0; // Reset the channel to 0, so we don't use the failing hash again - abortSendAndNak(encodeResult, p); - return encodeResult; // FIXME - this isn't a valid ErrorCode + if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag || + p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) { + return meshtastic_Routing_Error_BAD_REQUEST; } + + fixPriority(p); // Before encryption, fix the priority if it's unset + + // If the packet is not yet encrypted, do so now + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it + + DEBUG_HEAP_BEFORE; + meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("Router::send", p_decoded); + + auto encodeResult = perhapsEncode(p); + if (encodeResult != meshtastic_Routing_Error_NONE) { + packetPool.release(p_decoded); + p->channel = 0; // Reset the channel to 0, so we don't use the failing hash again + abortSendAndNak(encodeResult, p); + return encodeResult; // FIXME - this isn't a valid ErrorCode + } #if !MESHTASTIC_EXCLUDE_MQTT - // Only publish to MQTT if we're the original transmitter of the packet - if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { - mqtt->onSend(*p, *p_decoded, chIndex); - } + // Only publish to MQTT if we're the original transmitter of the packet + if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { + mqtt->onSend(*p, *p_decoded, chIndex); + } #endif - packetPool.release(p_decoded); - } + packetPool.release(p_decoded); + } #if HAS_UDP_MULTICAST - if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->onSend(const_cast(p)); - } + if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->onSend(const_cast(p)); + } #endif - assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) - return iface->send(p); + assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) + return iface->send(p); } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ -bool Router::cancelSending(NodeNum from, PacketId id) { - if (iface && iface->cancelSending(from, id)) { - // We are not a relayer of this packet anymore - removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); - return true; - } - return false; +bool Router::cancelSending(NodeNum from, PacketId id) +{ + if (iface && iface->cancelSending(from, id)) { + // We are not a relayer of this packet anymore + removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); + return true; + } + return false; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ -bool Router::findInTxQueue(NodeNum from, PacketId id) { return iface->findInTxQueue(from, id); } +bool Router::findInTxQueue(NodeNum from, PacketId id) +{ + return iface->findInTxQueue(from, id); +} /** * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to * update routing tables etc... based on what we overhear (even for messages not destined to our node) */ -void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { - // FIXME, update nodedb here for any packet that passes through us +void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) +{ + // FIXME, update nodedb here for any packet that passes through us } -DecodeState perhapsDecode(meshtastic_MeshPacket *p) { - concurrency::LockGuard g(cryptLock); +DecodeState perhapsDecode(meshtastic_MeshPacket *p) +{ + concurrency::LockGuard g(cryptLock); - if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && - (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { - LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); - return DecodeState::DECODE_FAILURE; - } - - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) - return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return - - size_t rawSize = p->encrypted.size; - if (rawSize > sizeof(bytes)) { - LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize); - return DecodeState::DECODE_FATAL; - } - bool decrypted = false; - ChannelIndex chIndex = 0; -#if !(MESHTASTIC_EXCLUDE_PKI) - // Attempt PKI decryption first - if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && - nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && - rawSize > MESHTASTIC_PKC_OVERHEAD) { - LOG_DEBUG("Attempt PKI decryption"); - - if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, bytes)) { - LOG_INFO("PKI Decryption worked!"); - - meshtastic_Data decodedtmp; - memset(&decodedtmp, 0, sizeof(decodedtmp)); - rawSize -= MESHTASTIC_PKC_OVERHEAD; - if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { - decrypted = true; - LOG_INFO("Packet decrypted using PKI!"); - p->pki_encrypted = true; - memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); - p->public_key.size = 32; - p->decoded = decodedtmp; - p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded - } else { - LOG_ERROR("PKC Decrypted, but pb_decode failed!"); + if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && + (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { + LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); return DecodeState::DECODE_FAILURE; - } - } else { - LOG_WARN("PKC decrypt attempted but failed!"); } - } -#endif - // assert(p->which_payloadVariant == MeshPacket_encrypted_tag); - if (!decrypted) { - // Try to find a channel that works with this hash - for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) { - // Try to use this hash/channel pair - if (channels.decryptForHash(chIndex, p->channel)) { - // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a - // fresh copy for each decrypt attempt. - memcpy(bytes, p->encrypted.bytes, rawSize); - // Try to decrypt the packet if we can - crypto->decrypt(p->from, p->id, rawSize, bytes); + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) + return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return - // printBytes("plaintext", bytes, p->encrypted.size); - - // Take those raw bytes and convert them back into a well structured protobuf we can understand - meshtastic_Data decodedtmp; - memset(&decodedtmp, 0, sizeof(decodedtmp)); - if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) { - LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); - } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { - LOG_ERROR("Invalid portnum (bad psk?)!"); + size_t rawSize = p->encrypted.size; + if (rawSize > sizeof(bytes)) { + LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize); + return DecodeState::DECODE_FATAL; + } + bool decrypted = false; + ChannelIndex chIndex = 0; #if !(MESHTASTIC_EXCLUDE_PKI) - } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - LOG_WARN("Rejecting legacy DM"); - return DecodeState::DECODE_FAILURE; -#endif + // Attempt PKI decryption first + if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && + nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && + rawSize > MESHTASTIC_PKC_OVERHEAD) { + LOG_DEBUG("Attempt PKI decryption"); + + if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, + bytes)) { + LOG_INFO("PKI Decryption worked!"); + + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + rawSize -= MESHTASTIC_PKC_OVERHEAD; + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && + decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { + decrypted = true; + LOG_INFO("Packet decrypted using PKI!"); + p->pki_encrypted = true; + memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); + p->public_key.size = 32; + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded + } else { + LOG_ERROR("PKC Decrypted, but pb_decode failed!"); + return DecodeState::DECODE_FAILURE; + } } else { - p->decoded = decodedtmp; - p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded - decrypted = true; - break; + LOG_WARN("PKC decrypt attempted but failed!"); } - } - } - } - - if (decrypted) { - // parsing was successful - p->channel = chIndex; // change to store the index instead of the hash - if (p->decoded.has_bitfield) - p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK; - - /* Not actually ever used. - // Decompress if needed. jm - if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) { - // Decompress the payload - char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - int decompressed_len; - - memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size); - - decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out); - - // LOG_DEBUG("**Decompressed length - %d ", decompressed_len); - - memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len); - - // Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - } */ - - printPacket("decoded message", p); -#if ENABLE_JSON_LOGGING - LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); -#elif ARCH_PORTDUINO - if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { - LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); - } else if (portduino_config.JSONFilename != "") { - if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { - JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; - } } #endif - return DecodeState::DECODE_SUCCESS; - } else { - LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); - return DecodeState::DECODE_FAILURE; - } + + // assert(p->which_payloadVariant == MeshPacket_encrypted_tag); + if (!decrypted) { + // Try to find a channel that works with this hash + for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) { + // Try to use this hash/channel pair + if (channels.decryptForHash(chIndex, p->channel)) { + // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a + // fresh copy for each decrypt attempt. + memcpy(bytes, p->encrypted.bytes, rawSize); + // Try to decrypt the packet if we can + crypto->decrypt(p->from, p->id, rawSize, bytes); + + // printBytes("plaintext", bytes, p->encrypted.size); + + // Take those raw bytes and convert them back into a well structured protobuf we can understand + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) { + LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); + } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { + LOG_ERROR("Invalid portnum (bad psk?)!"); +#if !(MESHTASTIC_EXCLUDE_PKI) + } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { + LOG_WARN("Rejecting legacy DM"); + return DecodeState::DECODE_FAILURE; +#endif + } else { + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded + decrypted = true; + break; + } + } + } + } + + if (decrypted) { + // parsing was successful + p->channel = chIndex; // change to store the index instead of the hash + if (p->decoded.has_bitfield) + p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK; + + /* Not actually ever used. + // Decompress if needed. jm + if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) { + // Decompress the payload + char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + int decompressed_len; + + memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size); + + decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out); + + // LOG_DEBUG("**Decompressed length - %d ", decompressed_len); + + memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len); + + // Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + } */ + + printPacket("decoded message", p); +#if ENABLE_JSON_LOGGING + LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); +#elif ARCH_PORTDUINO + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { + LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); + } else if (portduino_config.JSONFilename != "") { + if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { + JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; + } + } +#endif + return DecodeState::DECODE_SUCCESS; + } else { + LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); + return DecodeState::DECODE_FAILURE; + } } /** Return 0 for success or a Routing_Error code for failure */ -meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) { - concurrency::LockGuard g(cryptLock); +meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) +{ + concurrency::LockGuard g(cryptLock); - int16_t hash; + int16_t hash; - // If the packet is not yet encrypted, do so now - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - if (isFromUs(p)) { - p->decoded.has_bitfield = true; - p->decoded.bitfield |= (config.lora.config_ok_to_mqtt << BITFIELD_OK_TO_MQTT_SHIFT); - p->decoded.bitfield |= (p->decoded.want_response << BITFIELD_WANT_RESPONSE_SHIFT); - } - - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - - /* Not actually used, so save the cycles - // TODO: Allow modules to opt into compression. - if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - - char original_payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; - memcpy(original_payload, p->decoded.payload.bytes, p->decoded.payload.size); - - char compressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; - - int compressed_len; - compressed_len = unishox2_compress_simple(original_payload, p->decoded.payload.size, compressed_out); - - LOG_DEBUG("Original length - %d ", p->decoded.payload.size); - LOG_DEBUG("Compressed length - %d ", compressed_len); - LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes); - - // If the compressed length is greater than or equal to the original size, don't use the compressed form - if (compressed_len >= p->decoded.payload.size) { - - LOG_DEBUG("Not using compressing message"); - // Set the uncompressed payload variant anyway. Shouldn't hurt? - // p->decoded.which_payloadVariant = Data_payload_tag; - - // Otherwise we use the compressor - } else { - LOG_DEBUG("Use compressed message"); - // Copy the compressed data into the meshpacket - - p->decoded.payload.size = compressed_len; - memcpy(p->decoded.payload.bytes, compressed_out, compressed_len); - - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP; + // If the packet is not yet encrypted, do so now + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (isFromUs(p)) { + p->decoded.has_bitfield = true; + p->decoded.bitfield |= (config.lora.config_ok_to_mqtt << BITFIELD_OK_TO_MQTT_SHIFT); + p->decoded.bitfield |= (p->decoded.want_response << BITFIELD_WANT_RESPONSE_SHIFT); } - } */ - if (numbytes + MESHTASTIC_HEADER_LENGTH > MAX_LORA_PAYLOAD_LEN) - return meshtastic_Routing_Error_TOO_LARGE; + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); - // printBytes("plaintext", bytes, numbytes); + /* Not actually used, so save the cycles + // TODO: Allow modules to opt into compression. + if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { - ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it + char original_payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; + memcpy(original_payload, p->decoded.payload.bytes, p->decoded.payload.size); + + char compressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; + + int compressed_len; + compressed_len = unishox2_compress_simple(original_payload, p->decoded.payload.size, compressed_out); + + LOG_DEBUG("Original length - %d ", p->decoded.payload.size); + LOG_DEBUG("Compressed length - %d ", compressed_len); + LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes); + + // If the compressed length is greater than or equal to the original size, don't use the compressed form + if (compressed_len >= p->decoded.payload.size) { + + LOG_DEBUG("Not using compressing message"); + // Set the uncompressed payload variant anyway. Shouldn't hurt? + // p->decoded.which_payloadVariant = Data_payload_tag; + + // Otherwise we use the compressor + } else { + LOG_DEBUG("Use compressed message"); + // Copy the compressed data into the meshpacket + + p->decoded.payload.size = compressed_len; + memcpy(p->decoded.payload.bytes, compressed_out, compressed_len); + + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP; + } + } */ + + if (numbytes + MESHTASTIC_HEADER_LENGTH > MAX_LORA_PAYLOAD_LEN) + return meshtastic_Routing_Error_TOO_LARGE; + + // printBytes("plaintext", bytes, numbytes); + + ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it #if !(MESHTASTIC_EXCLUDE_PKI) - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); - // We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the - // node is not in the local nodedb First, only PKC encrypt packets we are originating - if (isFromUs(p) && + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); + // We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node + // is not in the local nodedb + // First, only PKC encrypt packets we are originating + if (isFromUs(p) && #if ARCH_PORTDUINO - // Sim radio via the cli flag skips PKC - !portduino_config.force_simradio && + // Sim radio via the cli flag skips PKC + !portduino_config.force_simradio && #endif - // Don't use PKC with Ham mode - !owner.is_licensed && - // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested - !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || - strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && - // Check for valid keys and single node destination - config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && - // Check for a known public key for the destination - (node->user.public_key.size == 32) && - // Some portnums either make no sense to send with PKC - p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && - p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { - LOG_DEBUG("Use PKI!"); - if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) - return meshtastic_Routing_Error_TOO_LARGE; - if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { - LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, *node->user.public_key.bytes); - return meshtastic_Routing_Error_PKI_FAILED; - } - crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); - numbytes += MESHTASTIC_PKC_OVERHEAD; - p->channel = 0; - p->pki_encrypted = true; - } else { - if (p->pki_encrypted == true) { - // Client specifically requested PKI encryption - return meshtastic_Routing_Error_PKI_FAILED; - } - hash = channels.setActiveByIndex(chIndex); + // Don't use PKC with Ham mode + !owner.is_licensed && + // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested + !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || + strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && + // Check for valid keys and single node destination + config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && + // Check for a known public key for the destination + (node->user.public_key.size == 32) && + // Some portnums either make no sense to send with PKC + p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && + p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { + LOG_DEBUG("Use PKI!"); + if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) + return meshtastic_Routing_Error_TOO_LARGE; + if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && + memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { + LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, + *node->user.public_key.bytes); + return meshtastic_Routing_Error_PKI_FAILED; + } + crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); + numbytes += MESHTASTIC_PKC_OVERHEAD; + p->channel = 0; + p->pki_encrypted = true; + } else { + if (p->pki_encrypted == true) { + // Client specifically requested PKI encryption + return meshtastic_Routing_Error_PKI_FAILED; + } + hash = channels.setActiveByIndex(chIndex); - // Now that we are encrypting the packet channel should be the hash (no longer the index) - p->channel = hash; - if (hash < 0) { - // No suitable channel could be found for - return meshtastic_Routing_Error_NO_CHANNEL; - } - crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); - memcpy(p->encrypted.bytes, bytes, numbytes); - } + // Now that we are encrypting the packet channel should be the hash (no longer the index) + p->channel = hash; + if (hash < 0) { + // No suitable channel could be found for + return meshtastic_Routing_Error_NO_CHANNEL; + } + crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); + memcpy(p->encrypted.bytes, bytes, numbytes); + } #else - if (p->pki_encrypted == true) { - // Client specifically requested PKI encryption - return meshtastic_Routing_Error_PKI_FAILED; - } - hash = channels.setActiveByIndex(chIndex); + if (p->pki_encrypted == true) { + // Client specifically requested PKI encryption + return meshtastic_Routing_Error_PKI_FAILED; + } + hash = channels.setActiveByIndex(chIndex); - // Now that we are encrypting the packet channel should be the hash (no longer the index) - p->channel = hash; - if (hash < 0) { - // No suitable channel could be found for - return meshtastic_Routing_Error_NO_CHANNEL; - } - crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); - memcpy(p->encrypted.bytes, bytes, numbytes); + // Now that we are encrypting the packet channel should be the hash (no longer the index) + p->channel = hash; + if (hash < 0) { + // No suitable channel could be found for + return meshtastic_Routing_Error_NO_CHANNEL; + } + crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); + memcpy(p->encrypted.bytes, bytes, numbytes); #endif - // Copy back into the packet and set the variant type - p->encrypted.size = numbytes; - p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; - } + // Copy back into the packet and set the variant type + p->encrypted.size = numbytes; + p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + } - return meshtastic_Routing_Error_NONE; + return meshtastic_Routing_Error_NONE; } -NodeNum Router::getNodeNum() { return nodeDB->getNodeNum(); } +NodeNum Router::getNodeNum() +{ + return nodeDB->getNodeNum(); +} /** * Handle any packet that is received by an interface on this node. * Note: some packets may merely being passed through this node and will be forwarded elsewhere. */ -void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) { - bool skipHandle = false; - // Also, we should set the time from the ISR and it should have msec level resolution - p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone +void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) +{ + bool skipHandle = false; + // Also, we should set the time from the ISR and it should have msec level resolution + p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone - // Store a copy of encrypted packet for MQTT - DEBUG_HEAP_BEFORE; - p_encrypted = packetPool.allocCopy(*p); - DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); + // Store a copy of encrypted packet for MQTT + DEBUG_HEAP_BEFORE; + p_encrypted = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); - // Take those raw bytes and convert them back into a well structured protobuf we can understand - auto decodedState = perhapsDecode(p); - if (decodedState == DecodeState::DECODE_FATAL) { - // Fatal decoding error, we can't do anything with this packet - LOG_WARN("Fatal decode error, dropping packet"); - cancelSending(p->from, p->id); - skipHandle = true; - } else if (decodedState == DecodeState::DECODE_SUCCESS) { - // parsing was successful, queue for our recipient - if (src == RX_SRC_LOCAL) - printPacket("handleReceived(LOCAL)", p); - else if (src == RX_SRC_USER) - printPacket("handleReceived(USER)", p); - else - printPacket("handleReceived(REMOTE)", p); + // Take those raw bytes and convert them back into a well structured protobuf we can understand + auto decodedState = perhapsDecode(p); + if (decodedState == DecodeState::DECODE_FATAL) { + // Fatal decoding error, we can't do anything with this packet + LOG_WARN("Fatal decode error, dropping packet"); + cancelSending(p->from, p->id); + skipHandle = true; + } else if (decodedState == DecodeState::DECODE_SUCCESS) { + // parsing was successful, queue for our recipient + if (src == RX_SRC_LOCAL) + printPacket("handleReceived(LOCAL)", p); + else if (src == RX_SRC_USER) + printPacket("handleReceived(USER)", p); + else + printPacket("handleReceived(REMOTE)", p); - // Neighbor info module is disabled, ignore expensive neighbor info packets - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP && - (!moduleConfig.has_neighbor_info || !moduleConfig.neighbor_info.enabled)) { - LOG_DEBUG("Neighbor info module is disabled, ignore neighbor packet"); - cancelSending(p->from, p->id); - skipHandle = true; - } + // Neighbor info module is disabled, ignore expensive neighbor info packets + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP && + (!moduleConfig.has_neighbor_info || !moduleConfig.neighbor_info.enabled)) { + LOG_DEBUG("Neighbor info module is disabled, ignore neighbor packet"); + cancelSending(p->from, p->id); + skipHandle = true; + } - bool shouldIgnoreNonstandardPorts = config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; + bool shouldIgnoreNonstandardPorts = + config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; #if USERPREFS_EVENT_MODE - shouldIgnoreNonstandardPorts = true; + shouldIgnoreNonstandardPorts = true; #endif - if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP, - meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, meshtastic_PortNum_TELEMETRY_APP, - meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_KEY_VERIFICATION_APP, - meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, - meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { - LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); - cancelSending(p->from, p->id); - skipHandle = true; + if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP, + meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, + meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, + meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, + meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { + LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); + cancelSending(p->from, p->id); + skipHandle = true; + } + } else { + printPacket("packet decoding failed or skipped (no PSK?)", p); } - } else { - printPacket("packet decoding failed or skipped (no PSK?)", p); - } - // call modules here - // If this could be a spoofed packet, don't let the modules see it. - if (!skipHandle) { - MeshModule::callModules(*p, src); + // call modules here + // If this could be a spoofed packet, don't let the modules see it. + if (!skipHandle) { + MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT - if (p_encrypted == nullptr) { - LOG_WARN("p_encrypted is null, skipping MQTT publish"); - } else { - // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM - // not to us (because we would be able to decrypt it) - if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && !isBroadcast(p->to) && - !isToUs(p)) - p_encrypted->pki_encrypted = true; - // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the - // packet - if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && !isFromUs(p) && mqtt) - mqtt->onSend(*p_encrypted, *p, p->channel); - } + if (p_encrypted == nullptr) { + LOG_WARN("p_encrypted is null, skipping MQTT publish"); + } else { + // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not + // to us (because we would be able to decrypt it) + if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && + !isBroadcast(p->to) && !isToUs(p)) + p_encrypted->pki_encrypted = true; + // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet + if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && + !isFromUs(p) && mqtt) + mqtt->onSend(*p_encrypted, *p, p->channel); + } #endif - } + } - packetPool.release(p_encrypted); // Release the encrypted packet - p_encrypted = nullptr; + packetPool.release(p_encrypted); // Release the encrypted packet + p_encrypted = nullptr; } -void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) { +void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) +{ #if ENABLE_JSON_LOGGING - // Even ignored packets get logged in the trace - p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone - LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); -#elif ARCH_PORTDUINO - // Even ignored packets get logged in the trace - if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { + // Even ignored packets get logged in the trace p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); - } +#elif ARCH_PORTDUINO + // Even ignored packets get logged in the trace + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { + p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone + LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); + } #endif - // assert(radioConfig.has_preferences); - if (is_in_repeated(config.lora.ignore_incoming, p->from)) { - LOG_DEBUG("Ignore msg, 0x%x is in our ignore list", p->from); - packetPool.release(p); - return; - } + // assert(radioConfig.has_preferences); + if (is_in_repeated(config.lora.ignore_incoming, p->from)) { + LOG_DEBUG("Ignore msg, 0x%x is in our ignore list", p->from); + packetPool.release(p); + return; + } - meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); - if (node != NULL && node->is_ignored) { - LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); - packetPool.release(p); - return; - } + meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); + if (node != NULL && node->is_ignored) { + LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); + packetPool.release(p); + return; + } - if (p->from == NODENUM_BROADCAST) { - LOG_DEBUG("Ignore msg from broadcast address"); - packetPool.release(p); - return; - } + if (p->from == NODENUM_BROADCAST) { + LOG_DEBUG("Ignore msg from broadcast address"); + packetPool.release(p); + return; + } - if (config.lora.ignore_mqtt && p->via_mqtt) { - LOG_DEBUG("Msg came in via MQTT from 0x%x", p->from); - packetPool.release(p); - return; - } + if (config.lora.ignore_mqtt && p->via_mqtt) { + LOG_DEBUG("Msg came in via MQTT from 0x%x", p->from); + packetPool.release(p); + return; + } - if (shouldFilterReceived(p)) { - LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from); - packetPool.release(p); - return; - } + if (shouldFilterReceived(p)) { + LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from); + packetPool.release(p); + return; + } - // Note: we avoid calling shouldFilterReceived if we are supposed to ignore certain nodes - because some overrides - // might cache/learn of the existence of nodes (i.e. FloodRouter) that they should not - handleReceived(p); - packetPool.release(p); + // Note: we avoid calling shouldFilterReceived if we are supposed to ignore certain nodes - because some overrides might + // cache/learn of the existence of nodes (i.e. FloodRouter) that they should not + handleReceived(p); + packetPool.release(p); } diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 954a3a3bc..dbe6f4f39 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -12,147 +12,148 @@ /** * A mesh aware router that supports multiple interfaces. */ -class Router : protected concurrency::OSThread, protected PacketHistory { -private: - /// Packets which have just arrived from the radio, ready to be processed by this service and possibly - /// forwarded to the phone. - PointerQueue fromRadioQueue; +class Router : protected concurrency::OSThread, protected PacketHistory +{ + private: + /// Packets which have just arrived from the radio, ready to be processed by this service and possibly + /// forwarded to the phone. + PointerQueue fromRadioQueue; -protected: - RadioInterface *iface = NULL; + protected: + RadioInterface *iface = NULL; -public: - /** - * Constructor - * - */ - Router(); + public: + /** + * Constructor + * + */ + Router(); - /** - * Currently we only allow one interface, that may change in the future - */ - void addInterface(RadioInterface *_iface) { iface = _iface; } + /** + * Currently we only allow one interface, that may change in the future + */ + void addInterface(RadioInterface *_iface) { iface = _iface; } - /** - * do idle processing - * Mostly looking in our incoming rxPacket queue and calling handleReceived. - */ - virtual int32_t runOnce() override; + /** + * do idle processing + * Mostly looking in our incoming rxPacket queue and calling handleReceived. + */ + virtual int32_t runOnce() override; - /** - * Works like send, but if we are sending to the local node, we directly put the message in the receive queue. - * This is the primary method used for sending packets, because it handles both the remote and local cases. - * - * NOTE: This method will free the provided packet (even if we return an error code) - */ - ErrorCode sendLocal(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); + /** + * Works like send, but if we are sending to the local node, we directly put the message in the receive queue. + * This is the primary method used for sending packets, because it handles both the remote and local cases. + * + * NOTE: This method will free the provided packet (even if we return an error code) + */ + ErrorCode sendLocal(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - bool cancelSending(NodeNum from, PacketId id); + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + bool cancelSending(NodeNum from, PacketId id); - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - bool findInTxQueue(NodeNum from, PacketId id); + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + bool findInTxQueue(NodeNum from, PacketId id); - /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. - * The returned packet is guaranteed to have a unique packet ID already assigned - */ - meshtastic_MeshPacket *allocForSending(); + /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. + * The returned packet is guaranteed to have a unique packet ID already assigned + */ + meshtastic_MeshPacket *allocForSending(); - /** Return Underlying interface's TX queue status */ - meshtastic_QueueStatus getQueueStatus(); + /** Return Underlying interface's TX queue status */ + meshtastic_QueueStatus getQueueStatus(); - /** - * @return our local nodenum */ - NodeNum getNodeNum(); + /** + * @return our local nodenum */ + NodeNum getNodeNum(); - /** Wake up the router thread ASAP, because we just queued a message for it. - * FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this - * queue' - */ - void setReceivedMessage(); + /** Wake up the router thread ASAP, because we just queued a message for it. + * FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this queue' + */ + void setReceivedMessage(); - /** - * RadioInterface calls this to queue up packets that have been received from the radio. The router is now - * responsible for freeing the packet - */ - virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p); + /** + * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for + * freeing the packet + */ + virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p); - /** - * Send a packet on a suitable interface. This routine will - * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error - * - * NOTE: This method will free the provided packet (even if we return an error code) - */ - virtual ErrorCode send(meshtastic_MeshPacket *p); - virtual ErrorCode rawSend(meshtastic_MeshPacket *p); + /** + * Send a packet on a suitable interface. This routine will + * later free() the packet to pool. This routine is not allowed to stall. + * If the txmit queue is full it might return an error + * + * NOTE: This method will free the provided packet (even if we return an error code) + */ + virtual ErrorCode send(meshtastic_MeshPacket *p); + virtual ErrorCode rawSend(meshtastic_MeshPacket *p); - /* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone - did it before us */ - uint32_t rxDupe = 0, txRelayCanceled = 0; + /* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it + before us */ + uint32_t rxDupe = 0, txRelayCanceled = 0; - // pointer to the encrypted packet - meshtastic_MeshPacket *p_encrypted = nullptr; + // pointer to the encrypted packet + meshtastic_MeshPacket *p_encrypted = nullptr; -protected: - friend class RoutingModule; + protected: + friend class RoutingModule; - /** - * Should this incoming filter be dropped? - * - * FIXME, move this into the new RoutingModule and do the filtering there using the regular module logic - * - * Called immediately on reception, before any further processing. - * @return true to abandon the packet - */ - virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; } + /** + * Should this incoming filter be dropped? + * + * FIXME, move this into the new RoutingModule and do the filtering there using the regular module logic + * + * Called immediately on reception, before any further processing. + * @return true to abandon the packet + */ + virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; } - /** - * Determine if hop_limit should be decremented for a relay operation. - * Returns false (preserve hop_limit) only if all conditions are met: - * - It's NOT the first hop (first hop must always decrement) - * - Local device is a ROUTER, ROUTER_LATE, or CLIENT_BASE - * - Previous relay is a favorite ROUTER, ROUTER_LATE, or CLIENT_BASE - * - * @param p The packet being relayed - * @return true if hop_limit should be decremented, false to preserve it - */ - bool shouldDecrementHopLimit(const meshtastic_MeshPacket *p); + /** + * Determine if hop_limit should be decremented for a relay operation. + * Returns false (preserve hop_limit) only if all conditions are met: + * - It's NOT the first hop (first hop must always decrement) + * - Local device is a ROUTER, ROUTER_LATE, or CLIENT_BASE + * - Previous relay is a favorite ROUTER, ROUTER_LATE, or CLIENT_BASE + * + * @param p The packet being relayed + * @return true if hop_limit should be decremented, false to preserve it + */ + bool shouldDecrementHopLimit(const meshtastic_MeshPacket *p); - /** - * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to - * update routing tables etc... based on what we overhear (even for messages not destined to our node) - */ - virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c); + /** + * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to + * update routing tables etc... based on what we overhear (even for messages not destined to our node) + */ + virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c); - /** - * Send an ack or a nak packet back towards whoever sent idFrom - */ - void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); + /** + * Send an ack or a nak packet back towards whoever sent idFrom + */ + void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, + bool ackWantsAck = false); -private: - /** - * Called from loop() - * Handle any packet that is received by an interface on this node. - * Note: some packets may merely being passed through this node and will be forwarded elsewhere. - * - * Note: this packet will never be called for messages sent/generated by this node. - * Note: this method will free the provided packet. - */ - void perhapsHandleReceived(meshtastic_MeshPacket *p); + private: + /** + * Called from loop() + * Handle any packet that is received by an interface on this node. + * Note: some packets may merely being passed through this node and will be forwarded elsewhere. + * + * Note: this packet will never be called for messages sent/generated by this node. + * Note: this method will free the provided packet. + */ + void perhapsHandleReceived(meshtastic_MeshPacket *p); - /** - * Called from perhapsHandleReceived() - allows subclass message delivery behavior. - * Handle any packet that is received by an interface on this node. - * Note: some packets may merely being passed through this node and will be forwarded elsewhere. - * - * Note: this packet will never be called for messages sent/generated by this node. - * Note: this method will free the provided packet. - */ - void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); + /** + * Called from perhapsHandleReceived() - allows subclass message delivery behavior. + * Handle any packet that is received by an interface on this node. + * Note: some packets may merely being passed through this node and will be forwarded elsewhere. + * + * Note: this packet will never be called for messages sent/generated by this node. + * Note: this method will free the provided packet. + */ + void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); - /** Frees the provided packet, and generates a NAK indicating the specifed error while sending */ - void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p); + /** Frees the provided packet, and generates a NAK indicating the specifed error while sending */ + void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p); }; enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_FATAL }; diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index c7b37fd47..f6e4b3512 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -8,34 +8,37 @@ #define STM32WLx_MAX_POWER 22 #endif -STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) {} +STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, + RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) +{ +} -bool STM32WLE5JCInterface::init() { - RadioLibInterface::init(); +bool STM32WLE5JCInterface::init() +{ + RadioLibInterface::init(); // https://github.com/Seeed-Studio/LoRaWan-E5-Node/blob/main/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c #if (!defined(_VARIANT_RAK3172_)) - setTCXOVoltage(1.7); + setTCXOVoltage(1.7); #endif - lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); + lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); - limitPower(STM32WLx_MAX_POWER); + limitPower(STM32WLx_MAX_POWER); - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); - LOG_INFO("STM32WLx init result %d", res); + LOG_INFO("STM32WLx init result %d", res); - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } #endif // ARCH_STM32WL diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h index 0ba49f4d2..ee935375e 100644 --- a/src/mesh/STM32WLE5JCInterface.h +++ b/src/mesh/STM32WLE5JCInterface.h @@ -7,11 +7,13 @@ /** * Our adapter for STM32WLE5JC radios */ -class STM32WLE5JCInterface : public SX126xInterface { -public: - STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +class STM32WLE5JCInterface : public SX126xInterface +{ + public: + STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); - virtual bool init() override; + virtual bool init() override; }; #endif // ARCH_STM32WL \ No newline at end of file diff --git a/src/mesh/SX1262Interface.cpp b/src/mesh/SX1262Interface.cpp index 7eff8a8c3..4c0dea00b 100644 --- a/src/mesh/SX1262Interface.cpp +++ b/src/mesh/SX1262Interface.cpp @@ -3,6 +3,9 @@ #include "configuration.h" #include "error.h" -SX1262Interface::SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) {} +SX1262Interface::SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) +{ +} #endif \ No newline at end of file diff --git a/src/mesh/SX1262Interface.h b/src/mesh/SX1262Interface.h index cd8d425f2..6e4616c8b 100644 --- a/src/mesh/SX1262Interface.h +++ b/src/mesh/SX1262Interface.h @@ -6,8 +6,10 @@ /** * Our adapter for SX1262 radios */ -class SX1262Interface : public SX126xInterface { -public: - SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +class SX1262Interface : public SX126xInterface +{ + public: + SX1262Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/SX1268Interface.cpp b/src/mesh/SX1268Interface.cpp index edc8633c9..fe6e9af89 100644 --- a/src/mesh/SX1268Interface.cpp +++ b/src/mesh/SX1268Interface.cpp @@ -3,14 +3,18 @@ #include "configuration.h" #include "error.h" -SX1268Interface::SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : SX126xInterface(hal, cs, irq, rst, busy) {} +SX1268Interface::SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : SX126xInterface(hal, cs, irq, rst, busy) +{ +} -float SX1268Interface::getFreq() { - // Set frequency to default of EU_433 if outside of allowed range (e.g. when region is UNSET) - if (savedFreq < 410 || savedFreq > 810) - return 433.125f; - else - return savedFreq; +float SX1268Interface::getFreq() +{ + // Set frequency to default of EU_433 if outside of allowed range (e.g. when region is UNSET) + if (savedFreq < 410 || savedFreq > 810) + return 433.125f; + else + return savedFreq; } #endif \ No newline at end of file diff --git a/src/mesh/SX1268Interface.h b/src/mesh/SX1268Interface.h index 4c7eaa3d8..cc6dd3534 100644 --- a/src/mesh/SX1268Interface.h +++ b/src/mesh/SX1268Interface.h @@ -6,10 +6,12 @@ /** * Our adapter for SX1268 radios */ -class SX1268Interface : public SX126xInterface { -public: - virtual float getFreq() override; +class SX1268Interface : public SX126xInterface +{ + public: + virtual float getFreq() override; - SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); + SX1268Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); }; #endif \ No newline at end of file diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 5b30dfc49..e1f07a32b 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -9,8 +9,8 @@ #include "Throttle.h" -// Particular boards might define a different max power based on what their hardware can do, default to max power output -// if not specified (may be dangerous if using external PA and SX126x power config forgotten) +// Particular boards might define a different max power based on what their hardware can do, default to max power output if not +// specified (may be dangerous if using external PA and SX126x power config forgotten) #if ARCH_PORTDUINO #define SX126X_MAX_POWER portduino_config.sx126x_max_power #endif @@ -21,133 +21,135 @@ template SX126xInterface::SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { - LOG_DEBUG("SX126xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) +{ + LOG_DEBUG("SX126xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -template bool SX126xInterface::init() { +template bool SX126xInterface::init() +{ -// Typically, the RF switch on SX126x boards is controlled by two signals, which are negations of each other (switched -// RFIO paths). The negation is usually performed in hardware, or (suboptimal design) TXEN and RXEN are the two inputs -// to this style of RF switch. On some boards, there is no hardware negation between CTRL and ¬CTRL, but CTRL is -// internally connected to DIO2, and DIO2's switching is done by the SX126X itself, so the MCU can't control ¬CTRL at -// exactly the same time. One solution would be to set ¬CTRL as SX126X_TXEN or SX126X_RXEN, but they may already be used -// for another purpose, such as controlling another PA/LNA. Keeping ¬CTRL high seems to work, as long CTRL=1, ¬CTRL=1 -// has the opposite and stable RF path effect as CTRL=0 and ¬CTRL=1, this depends on the RF switch, but it seems this -// usually works. Better hardware design, which is done most the time, means this workaround is not necessary. -#ifdef SX126X_ANT_SW // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not - // commonly used and not part of the 'default' set of pin definitions. - digitalWrite(SX126X_ANT_SW, HIGH); - pinMode(SX126X_ANT_SW, OUTPUT); +// Typically, the RF switch on SX126x boards is controlled by two signals, which are negations of each other (switched RFIO +// paths). The negation is usually performed in hardware, or (suboptimal design) TXEN and RXEN are the two inputs to this style of +// RF switch. On some boards, there is no hardware negation between CTRL and ¬CTRL, but CTRL is internally connected to DIO2, and +// DIO2's switching is done by the SX126X itself, so the MCU can't control ¬CTRL at exactly the same time. One solution would be +// to set ¬CTRL as SX126X_TXEN or SX126X_RXEN, but they may already be used for another purpose, such as controlling another +// PA/LNA. Keeping ¬CTRL high seems to work, as long CTRL=1, ¬CTRL=1 has the opposite and stable RF path effect as CTRL=0 and +// ¬CTRL=1, this depends on the RF switch, but it seems this usually works. Better hardware design, which is done most the time, +// means this workaround is not necessary. +#ifdef SX126X_ANT_SW // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly + // used and not part of the 'default' set of pin definitions. + digitalWrite(SX126X_ANT_SW, HIGH); + pinMode(SX126X_ANT_SW, OUTPUT); #endif -#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not - // commonly used and not part of the 'default' set of pin definitions. - digitalWrite(SX126X_POWER_EN, HIGH); - pinMode(SX126X_POWER_EN, OUTPUT); +#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly + // used and not part of the 'default' set of pin definitions. + digitalWrite(SX126X_POWER_EN, HIGH); + pinMode(SX126X_POWER_EN, OUTPUT); #endif #if defined(USE_GC1109_PA) - pinMode(LORA_PA_POWER, OUTPUT); - digitalWrite(LORA_PA_POWER, HIGH); + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); - pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, LOW); - pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); + pinMode(LORA_PA_EN, OUTPUT); + digitalWrite(LORA_PA_EN, LOW); + pinMode(LORA_PA_TX_EN, OUTPUT); + digitalWrite(LORA_PA_TX_EN, LOW); #endif #if ARCH_PORTDUINO - tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; - if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); - pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); - } -#endif - if (tcxoVoltage == 0.0) - LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); - else - LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); - setTransmitEnable(false); - // FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option - bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC? - - RadioLibInterface::init(); - - limitPower(SX126X_MAX_POWER); - // Make sure we reach the minimum power supported to turn the chip on (-9dBm) - if (power < -9) - power = -9; - - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); - // \todo Display actual typename of the adapter, not just `SX126x` - LOG_INFO("SX126x init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) - return false; - - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); - - // Overriding current limit - // (https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.cpp#L85) - // using value in SX126xInterface.h (currently 140 mA) It may or may not be necessary, depending on how RadioLib - // functions, from SX1261/2 datasheet: OCP after setting DeviceSel with SetPaConfig(): SX1261 - 60 mA, SX1262 - 140 mA - // For the SX1268 the IC defaults to 140mA no matter the set power level, but RadioLib set it lower, this would need - // further checking Default values are: SX1262, SX1268: 0x38 (140 mA), SX1261: 0x18 (60 mA) - // FIXME: Not ideal to increase SX1261 current limit above 60mA as it can only transmit max 15dBm, should probably - // only do it if using SX1262 or SX1268 - res = lora.setCurrentLimit(currentLimit); - LOG_DEBUG("Current limit set to %f", currentLimit); - LOG_DEBUG("Current limit set result %d", res); - - if (res == RADIOLIB_ERR_NONE) { -#ifdef SX126X_DIO2_AS_RF_SWITCH - bool dio2AsRfSwitch = true; -#elif defined(ARCH_PORTDUINO) - bool dio2AsRfSwitch = false; - if (portduino_config.dio2_as_rf_switch) { - dio2AsRfSwitch = true; + tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; + if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); + pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); } -#else - bool dio2AsRfSwitch = false; #endif - res = lora.setDio2AsRfSwitch(dio2AsRfSwitch); - LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res); - } + if (tcxoVoltage == 0.0) + LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); + else + LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); + setTransmitEnable(false); + // FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option + bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC? -// If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as -// it has no effect + RadioLibInterface::init(); + + limitPower(SX126X_MAX_POWER); + // Make sure we reach the minimum power supported to turn the chip on (-9dBm) + if (power < -9) + power = -9; + + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); + // \todo Display actual typename of the adapter, not just `SX126x` + LOG_INFO("SX126x init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; + + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); + + // Overriding current limit + // (https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.cpp#L85) using + // value in SX126xInterface.h (currently 140 mA) It may or may not be necessary, depending on how RadioLib functions, from + // SX1261/2 datasheet: OCP after setting DeviceSel with SetPaConfig(): SX1261 - 60 mA, SX1262 - 140 mA For the SX1268 the IC + // defaults to 140mA no matter the set power level, but RadioLib set it lower, this would need further checking Default values + // are: SX1262, SX1268: 0x38 (140 mA), SX1261: 0x18 (60 mA) + // FIXME: Not ideal to increase SX1261 current limit above 60mA as it can only transmit max 15dBm, should probably only do it + // if using SX1262 or SX1268 + res = lora.setCurrentLimit(currentLimit); + LOG_DEBUG("Current limit set to %f", currentLimit); + LOG_DEBUG("Current limit set result %d", res); + + if (res == RADIOLIB_ERR_NONE) { +#ifdef SX126X_DIO2_AS_RF_SWITCH + bool dio2AsRfSwitch = true; +#elif defined(ARCH_PORTDUINO) + bool dio2AsRfSwitch = false; + if (portduino_config.dio2_as_rf_switch) { + dio2AsRfSwitch = true; + } +#else + bool dio2AsRfSwitch = false; +#endif + res = lora.setDio2AsRfSwitch(dio2AsRfSwitch); + LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res); + } + +// If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as it has +// no effect #if ARCH_PORTDUINO - if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, - portduino_config.lora_txen_pin.pin); - lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); - } + if (res == RADIOLIB_ERR_NONE) { + LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, + portduino_config.lora_txen_pin.pin); + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); + } #else #ifndef SX126X_RXEN #define SX126X_RXEN RADIOLIB_NC - LOG_DEBUG("SX126X_RXEN not defined, defaulting to RADIOLIB_NC"); + LOG_DEBUG("SX126X_RXEN not defined, defaulting to RADIOLIB_NC"); #endif #ifndef SX126X_TXEN #define SX126X_TXEN RADIOLIB_NC - LOG_DEBUG("SX126X_TXEN not defined, defaulting to RADIOLIB_NC"); + LOG_DEBUG("SX126X_TXEN not defined, defaulting to RADIOLIB_NC"); #endif - if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN); - lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); - } + if (res == RADIOLIB_ERR_NONE) { + LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN); + lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); + } #endif - if (config.lora.sx126x_rx_boosted_gain) { - uint16_t result = lora.setRxBoostedGainMode(true); - LOG_INFO("Set RX gain to boosted mode; result: %d", result); - } else { - uint16_t result = lora.setRxBoostedGainMode(false); - LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); - } + if (config.lora.sx126x_rx_boosted_gain) { + uint16_t result = lora.setRxBoostedGainMode(true); + LOG_INFO("Set RX gain to boosted mode; result: %d", result); + } else { + uint16_t result = lora.setRxBoostedGainMode(false); + LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); + } #if 0 // Read/write a register we are not using (only used for FSK mode) to test SPI comms @@ -173,192 +175,203 @@ template bool SX126xInterface::init() { // If we got this far register accesses (and therefore SPI comms) are good #endif - if (res == RADIOLIB_ERR_NONE) - res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON); + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -template bool SX126xInterface::reconfigure() { - RadioLibInterface::reconfigure(); +template bool SX126xInterface::reconfigure() +{ + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setBandwidth(bw); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setSyncWord(syncWord); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setSyncWord %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setSyncWord %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setCurrentLimit(currentLimit); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setCurrentLimit %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setCurrentLimit(currentLimit); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setCurrentLimit %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setPreambleLength(preambleLength); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setPreambleLength %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setPreambleLength(preambleLength); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setPreambleLength %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX126X_MAX_POWER) // This chip has lower power limits than some - power = SX126X_MAX_POWER; + if (power > SX126X_MAX_POWER) // This chip has lower power limits than some + power = SX126X_MAX_POWER; - err = lora.setOutputPower(power); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setOutputPower(power); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() { lora.clearDio1Action(); } +template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() +{ + lora.clearDio1Action(); +} -template void SX126xInterface::setStandby() { - checkNotification(); // handle any pending interrupts before we force standby +template void SX126xInterface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby - int err = lora.standby(); + int err = lora.standby(); - if (err != RADIOLIB_ERR_NONE) - LOG_DEBUG("SX126x standby %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) + LOG_DEBUG("SX126x standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - isReceiving = false; // If we were receiving, not any more - activeReceiveStart = 0; - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** * Add SNR data to received messages */ -template void SX126xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) { - // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); - mp->rx_snr = lora.getSNR(); - mp->rx_rssi = lround(lora.getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); +template void SX126xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) +{ + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ -template void SX126xInterface::configHardwareForSend() { - setTransmitEnable(true); - RadioLibInterface::configHardwareForSend(); +template void SX126xInterface::configHardwareForSend() +{ + setTransmitEnable(true); + RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY -template void SX126xInterface::startReceive() { +template void SX126xInterface::startReceive() +{ #ifdef SLEEP_ONLY - sleep(); + sleep(); #else - setTransmitEnable(false); - setStandby(); + setTransmitEnable(false); + setStandby(); - // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. - int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - RadioLibInterface::startReceive(); + RadioLibInterface::startReceive(); - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register - // bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); #endif } /** Is the channel currently active? */ -template bool SX126xInterface::isChannelActive() { - // check if we can detect a LoRa preamble on the current channel - ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, - .detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, - .detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, - .exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, - .timeout = 0, - .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, - .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; - int16_t result; - setTransmitEnable(false); - setStandby(); - result = lora.scanChannel(cfg); - if (result == RADIOLIB_LORA_DETECTED) - return true; - if (result != RADIOLIB_CHANNEL_FREE) - LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); - assert(result != RADIOLIB_ERR_WRONG_MODEM); +template bool SX126xInterface::isChannelActive() +{ + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; + setTransmitEnable(false); + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; + if (result != RADIOLIB_CHANNEL_FREE) + LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - return false; + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -template bool SX126xInterface::isActivelyReceiving() { - // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet - // received and handled the interrupt for reading the packet/handling errors. - return receiveDetected(lora.getIrqFlags(), RADIOLIB_SX126X_IRQ_HEADER_VALID, RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); +template bool SX126xInterface::isActivelyReceiving() +{ + // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet + // received and handled the interrupt for reading the packet/handling errors. + return receiveDetected(lora.getIrqFlags(), RADIOLIB_SX126X_IRQ_HEADER_VALID, RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); } -template bool SX126xInterface::sleep() { - // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet - // \todo Display actual typename of the adapter, not just `SX126x` - LOG_DEBUG("SX126x entering sleep mode"); // (FIXME, don't keep config) - setStandby(); // Stop any pending operations +template bool SX126xInterface::sleep() +{ + // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet + // \todo Display actual typename of the adapter, not just `SX126x` + LOG_DEBUG("SX126x entering sleep mode"); // (FIXME, don't keep config) + setStandby(); // Stop any pending operations - // turn off TCXO if it was powered - // FIXME - this isn't correct - // lora.setTCXO(0); + // turn off TCXO if it was powered + // FIXME - this isn't correct + // lora.setTCXO(0); - // put chipset into sleep mode (we've already disabled interrupts by now) - bool keepConfig = true; - lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = true; + lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed #ifdef SX126X_POWER_EN - digitalWrite(SX126X_POWER_EN, LOW); + digitalWrite(SX126X_POWER_EN, LOW); #endif #if defined(USE_GC1109_PA) - /* - * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. - * // digitalWrite(LORA_PA_POWER, LOW); - */ - digitalWrite(LORA_PA_EN, LOW); - digitalWrite(LORA_PA_TX_EN, LOW); + /* + * Do not switch the power on and off frequently. + * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. + * // digitalWrite(LORA_PA_POWER, LOW); + */ + digitalWrite(LORA_PA_EN, LOW); + digitalWrite(LORA_PA_TX_EN, LOW); #endif - return true; + return true; } /** Some boards require GPIO control of tx vs rx paths */ -template void SX126xInterface::setTransmitEnable(bool txon) { +template void SX126xInterface::setTransmitEnable(bool txon) +{ #if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); - digitalWrite(LORA_PA_EN, HIGH); - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); + digitalWrite(LORA_PA_POWER, HIGH); + digitalWrite(LORA_PA_EN, HIGH); + digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); #endif } diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index e47f28e38..b8f16ac6d 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -7,73 +7,75 @@ * \brief Adapter for SX126x radio family. Implements common logic for child classes. * \tparam T RadioLib module type for SX126x: SX1262, SX1268. */ -template class SX126xInterface : public RadioLibInterface { -public: - SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +template class SX126xInterface : public RadioLibInterface +{ + public: + SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - bool isIRQPending() override { return lora.getIrqFlags() != 0; } + bool isIRQPending() override { return lora.getIrqFlags() != 0; } - void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } + void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } -protected: - float currentLimit = 140; // Higher OCP limit for SX126x PA - float tcxoVoltage = 0.0; + protected: + float currentLimit = 140; // Higher OCP limit for SX126x PA + float tcxoVoltage = 0.0; - /** - * Specific module instance - */ - T lora; + /** + * Specific module instance + */ + T lora; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } -private: - /** Some boards require GPIO control of tx vs rx paths */ - void setTransmitEnable(bool txon); + private: + /** Some boards require GPIO control of tx vs rx paths */ + void setTransmitEnable(bool txon); }; #endif \ No newline at end of file diff --git a/src/mesh/SX1280Interface.cpp b/src/mesh/SX1280Interface.cpp index 5e0436b3c..9e0d42122 100644 --- a/src/mesh/SX1280Interface.cpp +++ b/src/mesh/SX1280Interface.cpp @@ -3,6 +3,9 @@ #include "configuration.h" #include "error.h" -SX1280Interface::SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : SX128xInterface(hal, cs, irq, rst, busy) {} +SX1280Interface::SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : SX128xInterface(hal, cs, irq, rst, busy) +{ +} #endif \ No newline at end of file diff --git a/src/mesh/SX1280Interface.h b/src/mesh/SX1280Interface.h index c0c39626e..534dd8084 100644 --- a/src/mesh/SX1280Interface.h +++ b/src/mesh/SX1280Interface.h @@ -6,8 +6,10 @@ * Our adapter for SX1280 radios */ -class SX1280Interface : public SX128xInterface { -public: - SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +class SX1280Interface : public SX128xInterface +{ + public: + SX1280Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); }; #endif diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 9b5285168..80872af07 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -20,291 +20,307 @@ template SX128xInterface::SX128xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) - : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { - LOG_DEBUG("SX128xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) +{ + LOG_DEBUG("SX128xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. -template bool SX128xInterface::init() { +template bool SX128xInterface::init() +{ #ifdef SX128X_POWER_EN - pinMode(SX128X_POWER_EN, OUTPUT); - digitalWrite(SX128X_POWER_EN, HIGH); + pinMode(SX128X_POWER_EN, OUTPUT); + digitalWrite(SX128X_POWER_EN, HIGH); #endif #ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, 1); + pinMode(RF95_FAN_EN, OUTPUT); + digitalWrite(RF95_FAN_EN, 1); #endif #if ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output - } - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); - digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output + } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output + } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // set not rx or tx mode - pinMode(SX128X_RXEN, OUTPUT); - digitalWrite(SX128X_RXEN, LOW); // Set low before becoming an output + pinMode(SX128X_RXEN, OUTPUT); + digitalWrite(SX128X_RXEN, LOW); // Set low before becoming an output #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) - pinMode(SX128X_TXEN, OUTPUT); - digitalWrite(SX128X_TXEN, LOW); + pinMode(SX128X_TXEN, OUTPUT); + digitalWrite(SX128X_TXEN, LOW); #endif #endif - RadioLibInterface::init(); + RadioLibInterface::init(); - limitPower(SX128X_MAX_POWER); + limitPower(SX128X_MAX_POWER); - preambleLength = 12; // 12 is the default for this chip, 32 does not RX at all + preambleLength = 12; // 12 is the default for this chip, 32 does not RX at all - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); - // \todo Display actual typename of the adapter, not just `SX128x` - LOG_INFO("SX128x init result %d", res); + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); + // \todo Display actual typename of the adapter, not just `SX128x` + LOG_INFO("SX128x init result %d", res); - if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { - LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; - nodeDB->saveToDisk(SEGMENT_CONFIG); - delay(2000); + if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { + LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + nodeDB->saveToDisk(SEGMENT_CONFIG); + delay(2000); #if defined(ARCH_ESP32) - ESP.restart(); + ESP.restart(); #elif defined(ARCH_NRF52) - NVIC_SystemReset(); + NVIC_SystemReset(); #else - LOG_ERROR("FIXME implement reboot for this platform. Skip for now"); + LOG_ERROR("FIXME implement reboot for this platform. Skip for now"); #endif - } + } - LOG_INFO("Frequency set to %f", getFreq()); - LOG_INFO("Bandwidth set to %f", bw); - LOG_INFO("Power output set to %d", power); + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) && defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) - if (res == RADIOLIB_ERR_NONE) { - lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN); - } + if (res == RADIOLIB_ERR_NONE) { + lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN); + } #elif ARCH_PORTDUINO - if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); - } + if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && + portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); + } #endif - if (res == RADIOLIB_ERR_NONE) - res = lora.setCRC(2); + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); - if (res == RADIOLIB_ERR_NONE) - startReceive(); // start receiving + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving - return res == RADIOLIB_ERR_NONE; + return res == RADIOLIB_ERR_NONE; } -template bool SX128xInterface::reconfigure() { - RadioLibInterface::reconfigure(); +template bool SX128xInterface::reconfigure() +{ + RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); + // set mode to standby + setStandby(); - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setBandwidth(bw); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setSyncWord(syncWord); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X setSyncWord %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X setSyncWord %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setPreambleLength(preambleLength); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X setPreambleLength %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setPreambleLength(preambleLength); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X setPreambleLength %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX128X_MAX_POWER) // This chip has lower power limits than some - power = SX128X_MAX_POWER; + if (power > SX128X_MAX_POWER) // This chip has lower power limits than some + power = SX128X_MAX_POWER; - err = lora.setOutputPower(power); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X setOutputPower %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + err = lora.setOutputPower(power); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X setOutputPower %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - startReceive(); // restart receiving + startReceive(); // restart receiving - return RADIOLIB_ERR_NONE; + return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() { lora.clearDio1Action(); } +template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() +{ + lora.clearDio1Action(); +} -template bool SX128xInterface::wideLora() { return true; } +template bool SX128xInterface::wideLora() +{ + return true; +} -template void SX128xInterface::setStandby() { - checkNotification(); // handle any pending interrupts before we force standby +template void SX128xInterface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby - int err = lora.standby(); + int err = lora.standby(); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128x standby %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128x standby %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); #if ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); - } - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, LOW); - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); + } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); + } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn off RX and TX power - digitalWrite(SX128X_RXEN, LOW); + digitalWrite(SX128X_RXEN, LOW); #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) - digitalWrite(SX128X_TXEN, LOW); + digitalWrite(SX128X_TXEN, LOW); #endif #endif - isReceiving = false; // If we were receiving, not any more - activeReceiveStart = 0; - disableInterrupt(); - completeSending(); // If we were sending, not anymore - RadioLibInterface::setStandby(); + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); } /** * Add SNR data to received messages */ -template void SX128xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) { - // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); - mp->rx_snr = lora.getSNR(); - mp->rx_rssi = lround(lora.getRSSI()); - LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); +template void SX128xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) +{ + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ -template void SX128xInterface::configHardwareForSend() { +template void SX128xInterface::configHardwareForSend() +{ #if ARCH_PORTDUINO - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); - } - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); - } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); + } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); + } #else #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn on TX power / off RX power - digitalWrite(SX128X_TXEN, HIGH); + digitalWrite(SX128X_TXEN, HIGH); #endif #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) - digitalWrite(SX128X_RXEN, LOW); + digitalWrite(SX128X_RXEN, LOW); #endif #endif - RadioLibInterface::configHardwareForSend(); + RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY -template void SX128xInterface::startReceive() { +template void SX128xInterface::startReceive() +{ #ifdef SLEEP_ONLY - sleep(); + sleep(); #else - setStandby(); + setStandby(); #if ARCH_PORTDUINO - if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); - } - if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { - digitalWrite(portduino_config.lora_txen_pin.pin, LOW); - } + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); + } + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); + } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn on RX power / off TX power - digitalWrite(SX128X_RXEN, HIGH); + digitalWrite(SX128X_RXEN, HIGH); #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) - digitalWrite(SX128X_TXEN, LOW); + digitalWrite(SX128X_TXEN, LOW); #endif #endif - int err = lora.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); + int err = lora.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); - if (err != RADIOLIB_ERR_NONE) - LOG_ERROR("SX128X startReceive %s%d", radioLibErr, err); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) + LOG_ERROR("SX128X startReceive %s%d", radioLibErr, err); + assert(err == RADIOLIB_ERR_NONE); - RadioLibInterface::startReceive(); + RadioLibInterface::startReceive(); - // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register - // bits - enableInterrupt(isrRxLevel0); + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); #endif } /** Is the channel currently active? */ -template bool SX128xInterface::isChannelActive() { - // check if we can detect a LoRa preamble on the current channel - ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD_24GHZ, - .detPeak = 0, - .detMin = 0, - .exitMode = 0, - .timeout = 0, - .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, - .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; - int16_t result; +template bool SX128xInterface::isChannelActive() +{ + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD_24GHZ, + .detPeak = 0, + .detMin = 0, + .exitMode = 0, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; - setStandby(); - result = lora.scanChannel(cfg); - if (result == RADIOLIB_LORA_DETECTED) - return true; - if (result != RADIOLIB_CHANNEL_FREE) - LOG_ERROR("SX128X scanChannel %s%d", radioLibErr, result); - assert(result != RADIOLIB_ERR_WRONG_MODEM); + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; + if (result != RADIOLIB_CHANNEL_FREE) + LOG_ERROR("SX128X scanChannel %s%d", radioLibErr, result); + assert(result != RADIOLIB_ERR_WRONG_MODEM); - return false; + return false; } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -template bool SX128xInterface::isActivelyReceiving() { - return receiveDetected(lora.getIrqStatus(), RADIOLIB_SX128X_IRQ_HEADER_VALID, RADIOLIB_SX128X_IRQ_PREAMBLE_DETECTED); +template bool SX128xInterface::isActivelyReceiving() +{ + return receiveDetected(lora.getIrqStatus(), RADIOLIB_SX128X_IRQ_HEADER_VALID, RADIOLIB_SX128X_IRQ_PREAMBLE_DETECTED); } -template bool SX128xInterface::sleep() { - // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet - // \todo Display actual typename of the adapter, not just `SX128x` - LOG_DEBUG("SX128x entering sleep mode"); // (FIXME, don't keep config) - setStandby(); // Stop any pending operations +template bool SX128xInterface::sleep() +{ + // Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet + // \todo Display actual typename of the adapter, not just `SX128x` + LOG_DEBUG("SX128x entering sleep mode"); // (FIXME, don't keep config) + setStandby(); // Stop any pending operations - // turn off TCXO if it was powered - // FIXME - this isn't correct - // lora.setTCXO(0); + // turn off TCXO if it was powered + // FIXME - this isn't correct + // lora.setTCXO(0); - // put chipset into sleep mode (we've already disabled interrupts by now) - bool keepConfig = true; - lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = true; + lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed #ifdef SX128X_POWER_EN - digitalWrite(SX128X_POWER_EN, LOW); + digitalWrite(SX128X_POWER_EN, LOW); #endif - return true; + return true; } #endif \ No newline at end of file diff --git a/src/mesh/SX128xInterface.h b/src/mesh/SX128xInterface.h index 7a3246f9c..acdcbbb27 100644 --- a/src/mesh/SX128xInterface.h +++ b/src/mesh/SX128xInterface.h @@ -6,65 +6,67 @@ * \brief Adapter for SX128x radio family. Implements common logic for child classes. * \tparam T RadioLib module type for SX128x: SX1280. */ -template class SX128xInterface : public RadioLibInterface { -public: - SX128xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy); +template class SX128xInterface : public RadioLibInterface +{ + public: + SX128xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); - /// Initialise the Driver transport hardware and software. - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool init() override; + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; - virtual bool wideLora() override; + virtual bool wideLora() override; - /// Apply any radio provisioning changes - /// Make sure the Driver is properly configured before calling init(). - /// \return true if initialisation succeeded. - virtual bool reconfigure() override; + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; - /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. - virtual bool sleep() override; + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; - bool isIRQPending() override { return lora.getIrqFlags() != 0; } + bool isIRQPending() override { return lora.getIrqFlags() != 0; } -protected: - /** - * Specific module instance - */ - T lora; + protected: + /** + * Specific module instance + */ + T lora; - /** - * Glue functions called from ISR land - */ - virtual void disableInterrupt() override; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; - /** - * Enable a particular ISR callback glue function - */ - virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive() override; + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; - /** are we actively receiving a packet (only called during receiving state) */ - virtual bool isActivelyReceiving() override; + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; - /** - * Start waiting to receive a message - */ - virtual void startReceive() override; + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; - /** - * We override to turn on transmitter power as needed. - */ - virtual void configHardwareForSend() override; + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; - /** - * Add SNR data to received messages - */ - virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; - virtual void setStandby() override; + virtual void setStandby() override; - uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; diff --git a/src/mesh/SinglePortModule.h b/src/mesh/SinglePortModule.h index 19de88abf..e43de09d1 100644 --- a/src/mesh/SinglePortModule.h +++ b/src/mesh/SinglePortModule.h @@ -6,32 +6,34 @@ * Most modules are only interested in sending/receiving one particular portnum. This baseclass simplifies that common * case. */ -class SinglePortModule : public MeshModule { -protected: - meshtastic_PortNum ourPortNum; +class SinglePortModule : public MeshModule +{ + protected: + meshtastic_PortNum ourPortNum; -public: - /** Constructor - * name is for debugging output - */ - SinglePortModule(const char *_name, meshtastic_PortNum _ourPortNum) : MeshModule(_name), ourPortNum(_ourPortNum) {} + public: + /** Constructor + * name is for debugging output + */ + SinglePortModule(const char *_name, meshtastic_PortNum _ourPortNum) : MeshModule(_name), ourPortNum(_ourPortNum) {} -protected: - /** - * @return true if you want to receive the specified portnum - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } + protected: + /** + * @return true if you want to receive the specified portnum + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } - /** - * Return a mesh packet which has been preinited as a data packet with a particular port number. - * You can then send this packet (after customizing any of the payload fields you might need) with - * service->sendToMesh() - */ - meshtastic_MeshPacket *allocDataPacket() { - // Update our local node info with our position (even if we don't decide to update anyone else) - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = ourPortNum; + /** + * Return a mesh packet which has been preinited as a data packet with a particular port number. + * You can then send this packet (after customizing any of the payload fields you might need) with + * service->sendToMesh() + */ + meshtastic_MeshPacket *allocDataPacket() + { + // Update our local node info with our position (even if we don't decide to update anyone else) + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = ourPortNum; - return p; - } + return p; + } }; \ No newline at end of file diff --git a/src/mesh/StaticPointerQueue.h b/src/mesh/StaticPointerQueue.h index 2a748dc10..398ee450c 100644 --- a/src/mesh/StaticPointerQueue.h +++ b/src/mesh/StaticPointerQueue.h @@ -9,64 +9,69 @@ * This provides the same interface as PointerQueue but uses a statically allocated * buffer instead of dynamic allocation. */ -template class StaticPointerQueue { - static_assert(MaxElements > 0, "MaxElements must be greater than 0"); +template class StaticPointerQueue +{ + static_assert(MaxElements > 0, "MaxElements must be greater than 0"); - T *buffer[MaxElements]; - int head = 0; - int tail = 0; - int count = 0; - concurrency::OSThread *reader = nullptr; + T *buffer[MaxElements]; + int head = 0; + int tail = 0; + int count = 0; + concurrency::OSThread *reader = nullptr; -public: - StaticPointerQueue() { - // Initialize all buffer elements to nullptr to silence warnings and ensure clean state - for (int i = 0; i < MaxElements; i++) { - buffer[i] = nullptr; - } - } - - int numFree() const { return MaxElements - count; } - - bool isEmpty() const { return count == 0; } - - int numUsed() const { return count; } - - bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) { - if (count >= MaxElements) { - return false; // Queue is full + public: + StaticPointerQueue() + { + // Initialize all buffer elements to nullptr to silence warnings and ensure clean state + for (int i = 0; i < MaxElements; i++) { + buffer[i] = nullptr; + } } - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interrupt(); + int numFree() const { return MaxElements - count; } + + bool isEmpty() const { return count == 0; } + + int numUsed() const { return count; } + + bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) + { + if (count >= MaxElements) { + return false; // Queue is full + } + + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); + } + + buffer[tail] = x; + tail = (tail + 1) % MaxElements; + count++; + return true; } - buffer[tail] = x; - tail = (tail + 1) % MaxElements; - count++; - return true; - } + bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) + { + if (count == 0) { + return false; // Queue is empty + } - bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) { - if (count == 0) { - return false; // Queue is empty + *p = buffer[head]; + head = (head + 1) % MaxElements; + count--; + return true; } - *p = buffer[head]; - head = (head + 1) % MaxElements; - count--; - return true; - } + // returns a ptr or null if the queue was empty + T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) + { + T *p; + return dequeue(&p, maxWait) ? p : nullptr; + } - // returns a ptr or null if the queue was empty - T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) { - T *p; - return dequeue(&p, maxWait) ? p : nullptr; - } + void setReader(concurrency::OSThread *t) { reader = t; } - void setReader(concurrency::OSThread *t) { reader = t; } - - // For compatibility with PointerQueue interface - int getMaxLen() const { return MaxElements; } + // For compatibility with PointerQueue interface + int getMaxLen() const { return MaxElements; } }; diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 2578d83bc..20026767e 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -8,211 +8,220 @@ #define START2 0xc3 #define HEADER_LEN 4 -int32_t StreamAPI::runOncePart() { - auto result = readStream(); - writeStream(); - checkConnectionTimeout(); - return result; +int32_t StreamAPI::runOncePart() +{ + auto result = readStream(); + writeStream(); + checkConnectionTimeout(); + return result; } -int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) { - auto result = readStream(buf, bufLen); - writeStream(); - checkConnectionTimeout(); - return result; +int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) +{ + auto result = readStream(buf, bufLen); + writeStream(); + checkConnectionTimeout(); + return result; } /** * Read any rx chars from the link and call handleRecStream */ -int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) { - if (bufLen < 1) { - // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a - // long time - bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); - return recentRx ? 5 : 250; - } else { - handleRecStream(buf, bufLen); - // we had bytes available this time, so assume we might have them next time also - lastRxMsec = millis(); - return 0; - } +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } } /** * call getFromRadio() and deliver encapsulated packets to the Stream */ -void StreamAPI::writeStream() { - if (canWrite) { - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - emitTxBuffer(len); - } while (len); - } +void StreamAPI::writeStream() +{ + if (canWrite) { + uint32_t len; + do { + // Send every packet we can + len = getFromRadio(txBuf + HEADER_LEN); + emitTxBuffer(len); + } while (len); + } } -int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) { - uint16_t index = 0; - while (bufLen > index) { // Currently we never want to block - int cInt = buf[index++]; - if (cInt < 0) - break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit - // arduino +int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + while (bufLen > index) { // Currently we never want to block + int cInt = buf[index++]; + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino - uint8_t c = (uint8_t)cInt; + uint8_t c = (uint8_t)cInt; - // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload - size_t ptr = rxPtr; + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; - rxPtr++; // assume we will probably advance the rxPtr - rxBuf[ptr] = c; // store all bytes (including framing) + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) - // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); - if (ptr == 0) { // looking for START1 - if (c != START1) - rxPtr = 0; // failed to find framing - } else if (ptr == 1) { // looking for START2 - if (c != START2) - rxPtr = 0; // failed to find framing - } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing - uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing - // console->printf("len %d\n", len); + // console->printf("len %d\n", len); - if (ptr == HEADER_LEN - 1) { - // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid - // protobuf also) - if (len > MAX_TO_FROM_RADIO_SIZE) - rxPtr = 0; // length is bogus, restart search for framing - } + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } - if (rxPtr != 0) // Is packet still considered 'good'? - if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? - rxPtr = 0; // start over again on the next packet + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet - // If we didn't just fail the packet and we now have the right # of bytes, parse it - handleToRadio(rxBuf + HEADER_LEN, len); + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } } } - } - return 0; + return 0; } /** * Read any rx chars from the link and call handleToRadio */ -int32_t StreamAPI::readStream() { - if (!stream->available()) { - // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a - // long time - bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); - return recentRx ? 5 : 250; - } else { - while (stream->available()) { // Currently we never want to block - int cInt = stream->read(); - if (cInt < 0) - break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit - // arduino +int32_t StreamAPI::readStream() +{ + if (!stream->available()) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + while (stream->available()) { // Currently we never want to block + int cInt = stream->read(); + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino - uint8_t c = (uint8_t)cInt; + uint8_t c = (uint8_t)cInt; - // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload - size_t ptr = rxPtr; + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; - rxPtr++; // assume we will probably advance the rxPtr - rxBuf[ptr] = c; // store all bytes (including framing) + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) - // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); - if (ptr == 0) { // looking for START1 - if (c != START1) - rxPtr = 0; // failed to find framing - } else if (ptr == 1) { // looking for START2 - if (c != START2) - rxPtr = 0; // failed to find framing - } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing - uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing - // console->printf("len %d\n", len); + // console->printf("len %d\n", len); - if (ptr == HEADER_LEN - 1) { - // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid - // protobuf also) - if (len > MAX_TO_FROM_RADIO_SIZE) - rxPtr = 0; // length is bogus, restart search for framing + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } } - if (rxPtr != 0) // Is packet still considered 'good'? - if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? - rxPtr = 0; // start over again on the next packet - - // If we didn't just fail the packet and we now have the right # of bytes, parse it - handleToRadio(rxBuf + HEADER_LEN, len); - } - } + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; } - - // we had bytes available this time, so assume we might have them next time also - lastRxMsec = millis(); - return 0; - } } /** * Send the current txBuffer over our stream */ -void StreamAPI::emitTxBuffer(size_t len) { - if (len != 0) { - txBuf[0] = START1; - txBuf[1] = START2; - txBuf[2] = (len >> 8) & 0xff; - txBuf[3] = len & 0xff; +void StreamAPI::emitTxBuffer(size_t len) +{ + if (len != 0) { + txBuf[0] = START1; + txBuf[1] = START2; + txBuf[2] = (len >> 8) & 0xff; + txBuf[3] = len & 0xff; - auto totalLen = len + HEADER_LEN; - stream->write(txBuf, totalLen); - stream->flush(); - } + auto totalLen = len + HEADER_LEN; + stream->write(txBuf, totalLen); + stream->flush(); + } } -void StreamAPI::emitRebooted() { - // In case we send a FromRadio packet - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_rebooted_tag; - fromRadioScratch.rebooted = true; +void StreamAPI::emitRebooted() +{ + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_rebooted_tag; + fromRadioScratch.rebooted = true; - // LOG_DEBUG("Emitting reboot packet for serial shell"); - emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); + // LOG_DEBUG("Emitting reboot packet for serial shell"); + emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); } -void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg) { - // In case we send a FromRadio packet - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag; - fromRadioScratch.log_record.level = level; +void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg) +{ + // In case we send a FromRadio packet + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag; + fromRadioScratch.log_record.level = level; - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - fromRadioScratch.log_record.time = rtc_sec; - strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1); + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + fromRadioScratch.log_record.time = rtc_sec; + strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1); - auto num_printed = vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg); - if (num_printed > 0 && - fromRadioScratch.log_record.message[num_printed - 1] == '\n') // Strip any ending newline, because we have records for framing instead. - fromRadioScratch.log_record.message[num_printed - 1] = '\0'; - emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); + auto num_printed = + vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg); + if (num_printed > 0 && fromRadioScratch.log_record.message[num_printed - 1] == + '\n') // Strip any ending newline, because we have records for framing instead. + fromRadioScratch.log_record.message[num_printed - 1] = '\0'; + emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch)); } /// Hookable to find out when connection changes -void StreamAPI::onConnectionChanged(bool connected) { - // FIXME do reference counting instead +void StreamAPI::onConnectionChanged(bool connected) +{ + // FIXME do reference counting instead - if (connected) { // To prevent user confusion, turn off bluetooth while using the serial port api - powerFSM.trigger(EVENT_SERIAL_CONNECTED); - } else { - // FIXME, we get no notice of serial going away, we should instead automatically generate this event if we haven't - // received a packet in a while - powerFSM.trigger(EVENT_SERIAL_DISCONNECTED); - } + if (connected) { // To prevent user confusion, turn off bluetooth while using the serial port api + powerFSM.trigger(EVENT_SERIAL_CONNECTED); + } else { + // FIXME, we get no notice of serial going away, we should instead automatically generate this event if we haven't + // received a packet in a while + powerFSM.trigger(EVENT_SERIAL_DISCONNECTED); + } } \ No newline at end of file diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index c5c76d9d6..4ca2c197f 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -14,80 +14,79 @@ * * ## Wire encoding -When sending protobuf packets over serial or TCP each packet is preceded by uint32 sent in network byte order (big -endian). The upper 16 bits must be 0x94C3. The lower 16 bits are packet length (this encoding gives room to eventually -allow quite large packets). +When sending protobuf packets over serial or TCP each packet is preceded by uint32 sent in network byte order (big endian). +The upper 16 bits must be 0x94C3. The lower 16 bits are packet length (this encoding gives room to eventually allow quite large +packets). -Implementations validate length against the maximum possible size of a BLE packet (our lowest common denominator) of 512 -bytes. If the length provided is larger than that we assume the packet is corrupted and begin again looking for 0x4403 -framing. +Implementations validate length against the maximum possible size of a BLE packet (our lowest common denominator) of 512 bytes. If +the length provided is larger than that we assume the packet is corrupted and begin again looking for 0x4403 framing. -The packets flowing towards the device are ToRadio protobufs, the packets flowing from the device are FromRadio -protobufs. The 0x94C3 marker can be used as framing to (eventually) resync if packets are corrupted over the wire. +The packets flowing towards the device are ToRadio protobufs, the packets flowing from the device are FromRadio protobufs. +The 0x94C3 marker can be used as framing to (eventually) resync if packets are corrupted over the wire. -Note: the 0x94C3 framing was chosen to prevent confusion with the 7 bit ascii character set. It also doesn't collide -with any valid utf8 encoding. This makes it a bit easier to start a device outputting regular debug output on its serial -port and then only after it has received a valid packet from the PC, turn off unencoded debug printing and switch to -this packet encoding. +Note: the 0x94C3 framing was chosen to prevent confusion with the 7 bit ascii character set. It also doesn't collide with any +valid utf8 encoding. This makes it a bit easier to start a device outputting regular debug output on its serial port and then only +after it has received a valid packet from the PC, turn off unencoded debug printing and switch to this packet encoding. */ -class StreamAPI : public PhoneAPI { - /** - * The stream we read/write from - */ - Stream *stream; +class StreamAPI : public PhoneAPI +{ + /** + * The stream we read/write from + */ + Stream *stream; - uint8_t rxBuf[MAX_STREAM_BUF_SIZE] = {0}; - size_t rxPtr = 0; + uint8_t rxBuf[MAX_STREAM_BUF_SIZE] = {0}; + size_t rxPtr = 0; - /// time of last rx, used, to slow down our polling if we haven't heard from anyone - uint32_t lastRxMsec = 0; + /// time of last rx, used, to slow down our polling if we haven't heard from anyone + uint32_t lastRxMsec = 0; -public: - StreamAPI(Stream *_stream) : stream(_stream) {} + public: + StreamAPI(Stream *_stream) : stream(_stream) {} - /** - * Currently we require frequent invocation from loop() to check for arrived serial packets and to send new packets to - * the phone. - */ - virtual int32_t runOncePart(); - virtual int32_t runOncePart(char *buf, uint16_t bufLen); + /** + * Currently we require frequent invocation from loop() to check for arrived serial packets and to send new packets to the + * phone. + */ + virtual int32_t runOncePart(); + virtual int32_t runOncePart(char *buf, uint16_t bufLen); -private: - /** - * Read any rx chars from the link and call handleToRadio - */ - int32_t readStream(); - int32_t readStream(char *buf, uint16_t bufLen); - int32_t handleRecStream(char *buf, uint16_t bufLen); + private: + /** + * Read any rx chars from the link and call handleToRadio + */ + int32_t readStream(); + int32_t readStream(char *buf, uint16_t bufLen); + int32_t handleRecStream(char *buf, uint16_t bufLen); - /** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ - void writeStream(); + /** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ + void writeStream(); -protected: - /** - * Send a FromRadio.rebooted = true packet to the phone - */ - void emitRebooted(); + protected: + /** + * Send a FromRadio.rebooted = true packet to the phone + */ + void emitRebooted(); - virtual void onConnectionChanged(bool connected) override; + virtual void onConnectionChanged(bool connected) override; - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override = 0; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override = 0; - /** - * Send the current txBuffer over our stream - */ - void emitTxBuffer(size_t len); + /** + * Send the current txBuffer over our stream + */ + void emitTxBuffer(size_t len); - /// Are we allowed to write packets to our output stream (subclasses can turn this off - i.e. SerialConsole) - bool canWrite = true; + /// Are we allowed to write packets to our output stream (subclasses can turn this off - i.e. SerialConsole) + bool canWrite = true; - /// Subclasses can use this scratch buffer if they wish - uint8_t txBuf[MAX_STREAM_BUF_SIZE] = {0}; + /// Subclasses can use this scratch buffer if they wish + uint8_t txBuf[MAX_STREAM_BUF_SIZE] = {0}; - /// Low level function to emit a protobuf encapsulated log record - void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg); + /// Low level function to emit a protobuf encapsulated log record + void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg); }; \ No newline at end of file diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index 170b7ba46..f278cc843 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -7,25 +7,29 @@ /// @param throttleFunc Function to execute if the execution is not deferred /// @param onDefer Default to NULL, execute the function if the execution is deferred /// @return true if the function was executed, false if it was deferred -bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*throttleFunc)(void), void (*onDefer)(void)) { - if (*lastExecutionMs == 0) { - *lastExecutionMs = millis(); - throttleFunc(); - return true; - } - uint32_t now = millis(); +bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*throttleFunc)(void), void (*onDefer)(void)) +{ + if (*lastExecutionMs == 0) { + *lastExecutionMs = millis(); + throttleFunc(); + return true; + } + uint32_t now = millis(); - if ((now - *lastExecutionMs) >= minumumIntervalMs) { - throttleFunc(); - *lastExecutionMs = now; - return true; - } else if (onDefer != NULL) { - onDefer(); - } - return false; + if ((now - *lastExecutionMs) >= minumumIntervalMs) { + throttleFunc(); + *lastExecutionMs = now; + return true; + } else if (onDefer != NULL) { + onDefer(); + } + return false; } /// @brief Check if the last execution time is within the interval /// @param lastExecutionMs The last execution time in milliseconds /// @param timeSpanMs The interval in milliseconds of the timespan -bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) { return (millis() - lastExecutionMs) < timeSpanMs; } \ No newline at end of file +bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) +{ + return (millis() - lastExecutionMs) < timeSpanMs; +} \ No newline at end of file diff --git a/src/mesh/Throttle.h b/src/mesh/Throttle.h index 7a42d971f..8b4bb5d30 100644 --- a/src/mesh/Throttle.h +++ b/src/mesh/Throttle.h @@ -2,8 +2,9 @@ #include #include -class Throttle { -public: - static bool execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*func)(void), void (*onDefer)(void) = NULL); - static bool isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t intervalMs); +class Throttle +{ + public: + static bool execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*func)(void), void (*onDefer)(void) = NULL); + static bool isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t intervalMs); }; \ No newline at end of file diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index 6e0d06003..17cd92851 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -2,106 +2,111 @@ #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h" -meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) { - meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default; +meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) +{ + meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default; - info.num = lite->num; - info.snr = lite->snr; - info.last_heard = lite->last_heard; - info.channel = lite->channel; - info.via_mqtt = lite->via_mqtt; - info.is_favorite = lite->is_favorite; - info.is_ignored = lite->is_ignored; - info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + info.num = lite->num; + info.snr = lite->snr; + info.last_heard = lite->last_heard; + info.channel = lite->channel; + info.via_mqtt = lite->via_mqtt; + info.is_favorite = lite->is_favorite; + info.is_ignored = lite->is_ignored; + info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - if (lite->has_hops_away) { - info.has_hops_away = true; - info.hops_away = lite->hops_away; - } + if (lite->has_hops_away) { + info.has_hops_away = true; + info.hops_away = lite->hops_away; + } - if (lite->has_position) { - info.has_position = true; - if (lite->position.latitude_i != 0) - info.position.has_latitude_i = true; - info.position.latitude_i = lite->position.latitude_i; - if (lite->position.longitude_i != 0) - info.position.has_longitude_i = true; - info.position.longitude_i = lite->position.longitude_i; - if (lite->position.altitude != 0) - info.position.has_altitude = true; - info.position.altitude = lite->position.altitude; - info.position.location_source = lite->position.location_source; - info.position.time = lite->position.time; - } - if (lite->has_user) { - info.has_user = true; - info.user = ConvertToUser(lite->num, lite->user); - } - if (lite->has_device_metrics) { - info.has_device_metrics = true; - info.device_metrics = lite->device_metrics; - } - return info; + if (lite->has_position) { + info.has_position = true; + if (lite->position.latitude_i != 0) + info.position.has_latitude_i = true; + info.position.latitude_i = lite->position.latitude_i; + if (lite->position.longitude_i != 0) + info.position.has_longitude_i = true; + info.position.longitude_i = lite->position.longitude_i; + if (lite->position.altitude != 0) + info.position.has_altitude = true; + info.position.altitude = lite->position.altitude; + info.position.location_source = lite->position.location_source; + info.position.time = lite->position.time; + } + if (lite->has_user) { + info.has_user = true; + info.user = ConvertToUser(lite->num, lite->user); + } + if (lite->has_device_metrics) { + info.has_device_metrics = true; + info.device_metrics = lite->device_metrics; + } + return info; } -meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position) { - meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; - lite.latitude_i = position.latitude_i; - lite.longitude_i = position.longitude_i; - lite.altitude = position.altitude; - lite.location_source = position.location_source; - lite.time = position.time; +meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position) +{ + meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; + lite.latitude_i = position.latitude_i; + lite.longitude_i = position.longitude_i; + lite.altitude = position.altitude; + lite.location_source = position.location_source; + lite.time = position.time; - return lite; + return lite; } -meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite lite) { - meshtastic_Position position = meshtastic_Position_init_default; - if (lite.latitude_i != 0) - position.has_latitude_i = true; - position.latitude_i = lite.latitude_i; - if (lite.longitude_i != 0) - position.has_longitude_i = true; - position.longitude_i = lite.longitude_i; - if (lite.altitude != 0) - position.has_altitude = true; - position.altitude = lite.altitude; - position.location_source = lite.location_source; - position.time = lite.time; +meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite lite) +{ + meshtastic_Position position = meshtastic_Position_init_default; + if (lite.latitude_i != 0) + position.has_latitude_i = true; + position.latitude_i = lite.latitude_i; + if (lite.longitude_i != 0) + position.has_longitude_i = true; + position.longitude_i = lite.longitude_i; + if (lite.altitude != 0) + position.has_altitude = true; + position.altitude = lite.altitude; + position.location_source = lite.location_source; + position.time = lite.time; - return position; + return position; } -meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) { - meshtastic_UserLite lite = meshtastic_UserLite_init_default; +meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) +{ + meshtastic_UserLite lite = meshtastic_UserLite_init_default; - strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); - strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); - lite.hw_model = user.hw_model; - lite.role = user.role; - lite.is_licensed = user.is_licensed; - memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); - memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes)); - lite.public_key.size = user.public_key.size; - lite.has_is_unmessagable = user.has_is_unmessagable; - lite.is_unmessagable = user.is_unmessagable; - return lite; + strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); + strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); + lite.hw_model = user.hw_model; + lite.role = user.role; + lite.is_licensed = user.is_licensed; + memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); + memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes)); + lite.public_key.size = user.public_key.size; + lite.has_is_unmessagable = user.has_is_unmessagable; + lite.is_unmessagable = user.is_unmessagable; + return lite; } -meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) { - meshtastic_User user = meshtastic_User_init_default; +meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) +{ + meshtastic_User user = meshtastic_User_init_default; - snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); - strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); - strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); - user.hw_model = lite.hw_model; - user.role = lite.role; - user.is_licensed = lite.is_licensed; - memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); - memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); - user.public_key.size = lite.public_key.size; - user.has_is_unmessagable = lite.has_is_unmessagable; - user.is_unmessagable = lite.is_unmessagable; + snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); + strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); + strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); + user.hw_model = lite.hw_model; + user.role = lite.role; + user.is_licensed = lite.is_licensed; + memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); + memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); + user.public_key.size = lite.public_key.size; + user.has_is_unmessagable = lite.has_is_unmessagable; + user.is_unmessagable = lite.is_unmessagable; - return user; + return user; } \ No newline at end of file diff --git a/src/mesh/TypeConversions.h b/src/mesh/TypeConversions.h index bdf347e94..19e471f98 100644 --- a/src/mesh/TypeConversions.h +++ b/src/mesh/TypeConversions.h @@ -4,11 +4,12 @@ #pragma once #include "NodeDB.h" -class TypeConversions { -public: - static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite); - static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position); - static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); - static meshtastic_UserLite ConvertToUserLite(meshtastic_User user); - static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite); +class TypeConversions +{ + public: + static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite); + static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position); + static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); + static meshtastic_UserLite ConvertToUserLite(meshtastic_User user); + static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite); }; diff --git a/src/mesh/TypedQueue.h b/src/mesh/TypedQueue.h index ddbb4297c..47d7200a5 100644 --- a/src/mesh/TypedQueue.h +++ b/src/mesh/TypedQueue.h @@ -12,51 +12,54 @@ * A wrapper for freertos queues. Note: each element object should be small * and POD (Plain Old Data type) as elements are memcpied by value. */ -template class TypedQueue { - static_assert(std::is_standard_layout::value, "T must be standard layout"); - QueueHandle_t h; - concurrency::OSThread *reader = NULL; +template class TypedQueue +{ + static_assert(std::is_standard_layout::value, "T must be standard layout"); + QueueHandle_t h; + concurrency::OSThread *reader = NULL; -public: - explicit TypedQueue(int maxElements) : h(xQueueCreate(maxElements, sizeof(T))) { assert(h); } + public: + explicit TypedQueue(int maxElements) : h(xQueueCreate(maxElements, sizeof(T))) { assert(h); } - ~TypedQueue() { vQueueDelete(h); } + ~TypedQueue() { vQueueDelete(h); } - int numFree() { return uxQueueSpacesAvailable(h); } + int numFree() { return uxQueueSpacesAvailable(h); } - bool isEmpty() { return uxQueueMessagesWaiting(h) == 0; } + bool isEmpty() { return uxQueueMessagesWaiting(h) == 0; } - int numUsed() { return uxQueueMessagesWaiting(h); } + int numUsed() { return uxQueueMessagesWaiting(h); } - /** euqueue a packet. Also, maxWait used to default to portMAX_DELAY, but we now want to callers to THINK about what - * blocking they want */ - bool enqueue(T x, TickType_t maxWait) { - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interrupt(); + /** euqueue a packet. Also, maxWait used to default to portMAX_DELAY, but we now want to callers to THINK about what blocking + * they want */ + bool enqueue(T x, TickType_t maxWait) + { + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); + } + return xQueueSendToBack(h, &x, maxWait) == pdTRUE; } - return xQueueSendToBack(h, &x, maxWait) == pdTRUE; - } - bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interruptFromISR(higherPriWoken); + bool enqueueFromISR(T x, BaseType_t *higherPriWoken) + { + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interruptFromISR(higherPriWoken); + } + return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; } - return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; - } - bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) { return xQueueReceive(h, p, maxWait) == pdTRUE; } + bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) { return xQueueReceive(h, p, maxWait) == pdTRUE; } - bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } + bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } - /** - * Set a thread that is reading from this queue - * If a message is pushed to this queue that thread will be scheduled to run ASAP. - * - * Note: thread will not be automatically enabled, just have its interval set to 0 - */ - void setReader(concurrency::OSThread *t) { reader = t; } + /** + * Set a thread that is reading from this queue + * If a message is pushed to this queue that thread will be scheduled to run ASAP. + * + * Note: thread will not be automatically enabled, just have its interval set to 0 + */ + void setReader(concurrency::OSThread *t) { reader = t; } }; #else @@ -67,52 +70,55 @@ public: * A wrapper for freertos queues. Note: each element object should be small * and POD (Plain Old Data type) as elements are memcpied by value. */ -template class TypedQueue { - std::queue q; - concurrency::OSThread *reader = NULL; - int maxElements; +template class TypedQueue +{ + std::queue q; + concurrency::OSThread *reader = NULL; + int maxElements; -public: - explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {} + public: + explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {} - int numFree() { - if (maxElements <= 0) - return 1; // Always claim 1 free, because we can grow to any size - return maxElements - numUsed(); - } - - bool isEmpty() { return q.empty(); } - - int numUsed() { return q.size(); } - - bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) { - if (numFree() <= 0) - return false; - - if (reader) { - reader->setInterval(0); - concurrency::mainDelay.interrupt(); + int numFree() + { + if (maxElements <= 0) + return 1; // Always claim 1 free, because we can grow to any size + return maxElements - numUsed(); } - q.push(x); - return true; - } + bool isEmpty() { return q.empty(); } - // bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { return xQueueSendToBackFromISR(h, &x, higherPriWoken) == - // pdTRUE; } + int numUsed() { return q.size(); } - bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) { - if (isEmpty()) - return false; - else { - *p = q.front(); - q.pop(); - return true; + bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) + { + if (numFree() <= 0) + return false; + + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); + } + + q.push(x); + return true; } - } - // bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } + // bool enqueueFromISR(T x, BaseType_t *higherPriWoken) { return xQueueSendToBackFromISR(h, &x, higherPriWoken) == pdTRUE; } - void setReader(concurrency::OSThread *t) { reader = t; } + bool dequeue(T *p, TickType_t maxWait = portMAX_DELAY) + { + if (isEmpty()) + return false; + else { + *p = q.front(); + q.pop(); + return true; + } + } + + // bool dequeueFromISR(T *p, BaseType_t *higherPriWoken) { return xQueueReceiveFromISR(h, p, higherPriWoken); } + + void setReader(concurrency::OSThread *t) { reader = t; } }; #endif \ No newline at end of file diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp index 6c22a2ae7..420d80e9a 100644 --- a/src/mesh/aes-ccm.cpp +++ b/src/mesh/aes-ccm.cpp @@ -18,151 +18,163 @@ * @param len Number of bytes to compare * @return 0 if arrays are equal, -1 if different or if inputs are invalid */ -static int constant_time_compare(const void *a_, const void *b_, size_t len) { - /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ - const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; - const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; - if (len == 0) +static int constant_time_compare(const void *a_, const void *b_, size_t len) +{ + /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ + const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; + const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; + if (len == 0) + return 0; + if (a == NULL || b == NULL) + return -1; + size_t i; + volatile uint8_t d = 0U; + for (i = 0U; i < len; i++) { + d |= (a[i] ^ b[i]); + } + /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ + return (1 & ((d - 1) >> 8)) - 1; +} + +static void WPA_PUT_BE16(uint8_t *a, uint16_t val) +{ + a[0] = val >> 8; + a[1] = val & 0xff; +} + +static void xor_aes_block(uint8_t *dst, const uint8_t *src) +{ + for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) { + dst[i] ^= src[i]; + } +} +static void aes_ccm_auth_start(size_t M, size_t L, const uint8_t *nonce, const uint8_t *aad, size_t aad_len, size_t plain_len, + uint8_t *x) +{ + uint8_t aad_buf[2 * AES_BLOCK_SIZE]; + uint8_t b[AES_BLOCK_SIZE]; + /* Authentication */ + /* B_0: Flags | Nonce N | l(m) */ + b[0] = aad_len ? 0x40 : 0 /* Adata */; + b[0] |= (((M - 2) / 2) /* M' */ << 3); + b[0] |= (L - 1) /* L' */; + memcpy(&b[1], nonce, 15 - L); + WPA_PUT_BE16(&b[AES_BLOCK_SIZE - L], plain_len); + crypto->aesEncrypt(b, x); /* X_1 = E(K, B_0) */ + if (!aad_len) + return; + WPA_PUT_BE16(aad_buf, aad_len); + memcpy(aad_buf + 2, aad, aad_len); + memset(aad_buf + 2 + aad_len, 0, sizeof(aad_buf) - 2 - aad_len); + xor_aes_block(aad_buf, x); + crypto->aesEncrypt(aad_buf, x); /* X_2 = E(K, X_1 XOR B_1) */ + if (aad_len > AES_BLOCK_SIZE - 2) { + xor_aes_block(&aad_buf[AES_BLOCK_SIZE], x); + /* X_3 = E(K, X_2 XOR B_2) */ + crypto->aesEncrypt(&aad_buf[AES_BLOCK_SIZE], x); + } +} +static void aes_ccm_auth(const uint8_t *data, size_t len, uint8_t *x) +{ + size_t last = len % AES_BLOCK_SIZE; + size_t i; + for (i = 0; i < len / AES_BLOCK_SIZE; i++) { + /* X_i+1 = E(K, X_i XOR B_i) */ + xor_aes_block(x, data); + data += AES_BLOCK_SIZE; + crypto->aesEncrypt(x, x); + } + if (last) { + /* XOR zero-padded last block */ + for (i = 0; i < last; i++) + x[i] ^= *data++; + crypto->aesEncrypt(x, x); + } +} +static void aes_ccm_encr_start(size_t L, const uint8_t *nonce, uint8_t *a) +{ + /* A_i = Flags | Nonce N | Counter i */ + a[0] = L - 1; /* Flags = L' */ + memcpy(&a[1], nonce, 15 - L); +} +static void aes_ccm_encr(size_t L, const uint8_t *in, size_t len, uint8_t *out, uint8_t *a) +{ + size_t last = len % AES_BLOCK_SIZE; + size_t i; + /* crypt = msg XOR (S_1 | S_2 | ... | S_n) */ + for (i = 1; i <= len / AES_BLOCK_SIZE; i++) { + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); + /* S_i = E(K, A_i) */ + crypto->aesEncrypt(a, out); + xor_aes_block(out, in); + out += AES_BLOCK_SIZE; + in += AES_BLOCK_SIZE; + } + if (last) { + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); + crypto->aesEncrypt(a, out); + /* XOR zero-padded last block */ + for (i = 0; i < last; i++) + *out++ ^= *in++; + } +} +static void aes_ccm_encr_auth(size_t M, const uint8_t *x, uint8_t *a, uint8_t *auth) +{ + size_t i; + uint8_t tmp[AES_BLOCK_SIZE]; + /* U = T XOR S_0; S_0 = E(K, A_0) */ + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); + crypto->aesEncrypt(a, tmp); + for (i = 0; i < M; i++) + auth[i] = x[i] ^ tmp[i]; +} +static void aes_ccm_decr_auth(size_t M, uint8_t *a, const uint8_t *auth, uint8_t *t) +{ + size_t i; + uint8_t tmp[AES_BLOCK_SIZE]; + /* U = T XOR S_0; S_0 = E(K, A_0) */ + WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); + crypto->aesEncrypt(a, tmp); + for (i = 0; i < M; i++) + t[i] = auth[i] ^ tmp[i]; +} +/* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ +int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, + const uint8_t *aad, size_t aad_len, uint8_t *crypt, uint8_t *auth) +{ + const size_t L = 2; + uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; + if (aad_len > 30 || M > AES_BLOCK_SIZE) + return -1; + crypto->aesSetKey(key, key_len); + aes_ccm_auth_start(M, L, nonce, aad, aad_len, plain_len, x); + aes_ccm_auth(plain, plain_len, x); + /* Encryption */ + aes_ccm_encr_start(L, nonce, a); + aes_ccm_encr(L, plain, plain_len, crypt, a); + aes_ccm_encr_auth(M, x, a, auth); return 0; - if (a == NULL || b == NULL) - return -1; - size_t i; - volatile uint8_t d = 0U; - for (i = 0U; i < len; i++) { - d |= (a[i] ^ b[i]); - } - /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ - return (1 & ((d - 1) >> 8)) - 1; -} - -static void WPA_PUT_BE16(uint8_t *a, uint16_t val) { - a[0] = val >> 8; - a[1] = val & 0xff; -} - -static void xor_aes_block(uint8_t *dst, const uint8_t *src) { - for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) { - dst[i] ^= src[i]; - } -} -static void aes_ccm_auth_start(size_t M, size_t L, const uint8_t *nonce, const uint8_t *aad, size_t aad_len, size_t plain_len, uint8_t *x) { - uint8_t aad_buf[2 * AES_BLOCK_SIZE]; - uint8_t b[AES_BLOCK_SIZE]; - /* Authentication */ - /* B_0: Flags | Nonce N | l(m) */ - b[0] = aad_len ? 0x40 : 0 /* Adata */; - b[0] |= (((M - 2) / 2) /* M' */ << 3); - b[0] |= (L - 1) /* L' */; - memcpy(&b[1], nonce, 15 - L); - WPA_PUT_BE16(&b[AES_BLOCK_SIZE - L], plain_len); - crypto->aesEncrypt(b, x); /* X_1 = E(K, B_0) */ - if (!aad_len) - return; - WPA_PUT_BE16(aad_buf, aad_len); - memcpy(aad_buf + 2, aad, aad_len); - memset(aad_buf + 2 + aad_len, 0, sizeof(aad_buf) - 2 - aad_len); - xor_aes_block(aad_buf, x); - crypto->aesEncrypt(aad_buf, x); /* X_2 = E(K, X_1 XOR B_1) */ - if (aad_len > AES_BLOCK_SIZE - 2) { - xor_aes_block(&aad_buf[AES_BLOCK_SIZE], x); - /* X_3 = E(K, X_2 XOR B_2) */ - crypto->aesEncrypt(&aad_buf[AES_BLOCK_SIZE], x); - } -} -static void aes_ccm_auth(const uint8_t *data, size_t len, uint8_t *x) { - size_t last = len % AES_BLOCK_SIZE; - size_t i; - for (i = 0; i < len / AES_BLOCK_SIZE; i++) { - /* X_i+1 = E(K, X_i XOR B_i) */ - xor_aes_block(x, data); - data += AES_BLOCK_SIZE; - crypto->aesEncrypt(x, x); - } - if (last) { - /* XOR zero-padded last block */ - for (i = 0; i < last; i++) - x[i] ^= *data++; - crypto->aesEncrypt(x, x); - } -} -static void aes_ccm_encr_start(size_t L, const uint8_t *nonce, uint8_t *a) { - /* A_i = Flags | Nonce N | Counter i */ - a[0] = L - 1; /* Flags = L' */ - memcpy(&a[1], nonce, 15 - L); -} -static void aes_ccm_encr(size_t L, const uint8_t *in, size_t len, uint8_t *out, uint8_t *a) { - size_t last = len % AES_BLOCK_SIZE; - size_t i; - /* crypt = msg XOR (S_1 | S_2 | ... | S_n) */ - for (i = 1; i <= len / AES_BLOCK_SIZE; i++) { - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); - /* S_i = E(K, A_i) */ - crypto->aesEncrypt(a, out); - xor_aes_block(out, in); - out += AES_BLOCK_SIZE; - in += AES_BLOCK_SIZE; - } - if (last) { - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); - crypto->aesEncrypt(a, out); - /* XOR zero-padded last block */ - for (i = 0; i < last; i++) - *out++ ^= *in++; - } -} -static void aes_ccm_encr_auth(size_t M, const uint8_t *x, uint8_t *a, uint8_t *auth) { - size_t i; - uint8_t tmp[AES_BLOCK_SIZE]; - /* U = T XOR S_0; S_0 = E(K, A_0) */ - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); - crypto->aesEncrypt(a, tmp); - for (i = 0; i < M; i++) - auth[i] = x[i] ^ tmp[i]; -} -static void aes_ccm_decr_auth(size_t M, uint8_t *a, const uint8_t *auth, uint8_t *t) { - size_t i; - uint8_t tmp[AES_BLOCK_SIZE]; - /* U = T XOR S_0; S_0 = E(K, A_0) */ - WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); - crypto->aesEncrypt(a, tmp); - for (i = 0; i < M; i++) - t[i] = auth[i] ^ tmp[i]; } /* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ -int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, const uint8_t *aad, - size_t aad_len, uint8_t *crypt, uint8_t *auth) { - const size_t L = 2; - uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; - if (aad_len > 30 || M > AES_BLOCK_SIZE) - return -1; - crypto->aesSetKey(key, key_len); - aes_ccm_auth_start(M, L, nonce, aad, aad_len, plain_len, x); - aes_ccm_auth(plain, plain_len, x); - /* Encryption */ - aes_ccm_encr_start(L, nonce, a); - aes_ccm_encr(L, plain, plain_len, crypt, a); - aes_ccm_encr_auth(M, x, a, auth); - return 0; -} -/* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ -bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, const uint8_t *aad, - size_t aad_len, const uint8_t *auth, uint8_t *plain) { - const size_t L = 2; - uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; - uint8_t t[AES_BLOCK_SIZE]; - if (aad_len > 30 || M > AES_BLOCK_SIZE) - return false; - crypto->aesSetKey(key, key_len); - /* Decryption */ - aes_ccm_encr_start(L, nonce, a); - aes_ccm_decr_auth(M, a, auth, t); - /* plaintext = msg XOR (S_1 | S_2 | ... | S_n) */ - aes_ccm_encr(L, crypt, crypt_len, plain, a); - aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); - aes_ccm_auth(plain, crypt_len, x); - if (constant_time_compare(x, t, M) != 0) { - return false; - } - return true; +bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, + const uint8_t *aad, size_t aad_len, const uint8_t *auth, uint8_t *plain) +{ + const size_t L = 2; + uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; + uint8_t t[AES_BLOCK_SIZE]; + if (aad_len > 30 || M > AES_BLOCK_SIZE) + return false; + crypto->aesSetKey(key, key_len); + /* Decryption */ + aes_ccm_encr_start(L, nonce, a); + aes_ccm_decr_auth(M, a, auth, t); + /* plaintext = msg XOR (S_1 | S_2 | ... | S_n) */ + aes_ccm_encr(L, crypt, crypt_len, plain, a); + aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); + aes_ccm_auth(plain, crypt_len, x); + if (constant_time_compare(x, t, M) != 0) { + return false; + } + return true; } #endif \ No newline at end of file diff --git a/src/mesh/aes-ccm.h b/src/mesh/aes-ccm.h index 5027853b7..6b8edcde4 100644 --- a/src/mesh/aes-ccm.h +++ b/src/mesh/aes-ccm.h @@ -2,9 +2,9 @@ #include "CryptoEngine.h" #if !MESHTASTIC_EXCLUDE_PKI -int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, const uint8_t *aad, - size_t aad_len, uint8_t *crypt, uint8_t *auth); +int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, + const uint8_t *aad, size_t aad_len, uint8_t *crypt, uint8_t *auth); -bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, const uint8_t *aad, - size_t aad_len, const uint8_t *auth, uint8_t *plain); +bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, + const uint8_t *aad, size_t aad_len, const uint8_t *auth, uint8_t *plain); #endif \ No newline at end of file diff --git a/src/mesh/api/PacketAPI.cpp b/src/mesh/api/PacketAPI.cpp index b92acd5eb..f4d5de540 100644 --- a/src/mesh/api/PacketAPI.cpp +++ b/src/mesh/api/PacketAPI.cpp @@ -8,116 +8,124 @@ PacketAPI *packetAPI = nullptr; -PacketAPI *PacketAPI::create(PacketServer *_server) { - if (!packetAPI) { - packetAPI = new PacketAPI(_server); - } - return packetAPI; +PacketAPI *PacketAPI::create(PacketServer *_server) +{ + if (!packetAPI) { + packetAPI = new PacketAPI(_server); + } + return packetAPI; } -PacketAPI::PacketAPI(PacketServer *_server) : concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server) { - api_type = TYPE_PACKET; +PacketAPI::PacketAPI(PacketServer *_server) + : concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server) +{ + api_type = TYPE_PACKET; } -int32_t PacketAPI::runOnce() { - bool success = false; +int32_t PacketAPI::runOnce() +{ + bool success = false; #ifndef ARCH_PORTDUINO - if (config.bluetooth.enabled) { - if (!programmingMode) { - // in programmingMode we don't send any packets to the client except this one notify - programmingMode = true; - success = notifyProgrammingMode(); - } - } else -#endif - { - success = sendPacket(); - } - success |= receivePacket(); - return success ? 10 : 50; -} - -bool PacketAPI::receivePacket(void) { - bool data_received = false; - while (server->hasData()) { - isConnected = true; - data_received = true; - - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); - lastContactMsec = millis(); - - meshtastic_ToRadio *mr; - auto p = server->receivePacket()->move(); - int id = p->getPacketId(); - LOG_DEBUG("Received packet id=%u", id); - mr = (meshtastic_ToRadio *)&static_cast *>(p.get())->getData(); - - switch (mr->which_payload_variant) { - case meshtastic_ToRadio_packet_tag: { - meshtastic_MeshPacket *mp = &mr->packet; - mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API; - printPacket("PACKET FROM QUEUE", mp); - service->handleToRadio(*mp); - break; - } - case meshtastic_ToRadio_want_config_id_tag: { - uint32_t config_nonce = mr->want_config_id; - LOG_INFO("Screen wants config, nonce=%u", config_nonce); - handleStartConfig(); - break; - } - case meshtastic_ToRadio_heartbeat_tag: - if (mr->heartbeat.nonce == 1) { - if (nodeInfoModule) { - LOG_INFO("Broadcasting nodeinfo ping"); - nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true); + if (config.bluetooth.enabled) { + if (!programmingMode) { + // in programmingMode we don't send any packets to the client except this one notify + programmingMode = true; + success = notifyProgrammingMode(); } - } else { - LOG_DEBUG("Got client heartbeat"); - } - break; - default: - LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant); - break; + } else +#endif + { + success = sendPacket(); } - } - return data_received; + success |= receivePacket(); + return success ? 10 : 50; } -bool PacketAPI::sendPacket(void) { - if (server->available()) { - // fill dummy buffer; we don't use it, we directly send the fromRadio structure - uint32_t len = getFromRadio(txBuf); - if (len != 0) { - static uint32_t id = 0; - fromRadioScratch.id = ++id; - bool result = server->sendPacket(DataPacket(id, fromRadioScratch)); - if (!result) { - LOG_ERROR("send queue full"); - } - return result; +bool PacketAPI::receivePacket(void) +{ + bool data_received = false; + while (server->hasData()) { + isConnected = true; + data_received = true; + + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + lastContactMsec = millis(); + + meshtastic_ToRadio *mr; + auto p = server->receivePacket()->move(); + int id = p->getPacketId(); + LOG_DEBUG("Received packet id=%u", id); + mr = (meshtastic_ToRadio *)&static_cast *>(p.get())->getData(); + + switch (mr->which_payload_variant) { + case meshtastic_ToRadio_packet_tag: { + meshtastic_MeshPacket *mp = &mr->packet; + mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API; + printPacket("PACKET FROM QUEUE", mp); + service->handleToRadio(*mp); + break; + } + case meshtastic_ToRadio_want_config_id_tag: { + uint32_t config_nonce = mr->want_config_id; + LOG_INFO("Screen wants config, nonce=%u", config_nonce); + handleStartConfig(); + break; + } + case meshtastic_ToRadio_heartbeat_tag: + if (mr->heartbeat.nonce == 1) { + if (nodeInfoModule) { + LOG_INFO("Broadcasting nodeinfo ping"); + nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true); + } + } else { + LOG_DEBUG("Got client heartbeat"); + } + break; + default: + LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant); + break; + } } - } - return false; + return data_received; } -bool PacketAPI::notifyProgrammingMode(void) { - // tell the client we are in programming mode by sending only the bluetooth config state - LOG_INFO("force client into programmingMode"); - memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); - fromRadioScratch.id = nodeDB->getNodeNum(); - fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; - fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; - fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; - return server->sendPacket(DataPacket(0, fromRadioScratch)); +bool PacketAPI::sendPacket(void) +{ + if (server->available()) { + // fill dummy buffer; we don't use it, we directly send the fromRadio structure + uint32_t len = getFromRadio(txBuf); + if (len != 0) { + static uint32_t id = 0; + fromRadioScratch.id = ++id; + bool result = server->sendPacket(DataPacket(id, fromRadioScratch)); + if (!result) { + LOG_ERROR("send queue full"); + } + return result; + } + } + return false; +} + +bool PacketAPI::notifyProgrammingMode(void) +{ + // tell the client we are in programming mode by sending only the bluetooth config state + LOG_INFO("force client into programmingMode"); + memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); + fromRadioScratch.id = nodeDB->getNodeNum(); + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; + fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; + fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; + return server->sendPacket(DataPacket(0, fromRadioScratch)); } /** * return true if we got (once!) contact from our client and the server send queue is not full */ -bool PacketAPI::checkIsConnected() { - isConnected |= server->hasData(); - return isConnected && server->available(); +bool PacketAPI::checkIsConnected() +{ + isConnected |= server->hasData(); + return isConnected && server->available(); } #endif \ No newline at end of file diff --git a/src/mesh/api/PacketAPI.h b/src/mesh/api/PacketAPI.h index a2593d59f..fc08ab209 100644 --- a/src/mesh/api/PacketAPI.h +++ b/src/mesh/api/PacketAPI.h @@ -9,29 +9,30 @@ * between two tasks running on CPU0 and CPU1, respectively. * */ -class PacketAPI : public PhoneAPI, public concurrency::OSThread { -public: - static PacketAPI *create(PacketServer *_server); - virtual ~PacketAPI(){}; - virtual int32_t runOnce(); +class PacketAPI : public PhoneAPI, public concurrency::OSThread +{ + public: + static PacketAPI *create(PacketServer *_server); + virtual ~PacketAPI(){}; + virtual int32_t runOnce(); -protected: - PacketAPI(PacketServer *_server); - // Check the current underlying physical queue to see if the client is fetching packets - bool checkIsConnected() override; + protected: + PacketAPI(PacketServer *_server); + // Check the current underlying physical queue to see if the client is fetching packets + bool checkIsConnected() override; - void onNowHasData(uint32_t fromRadioNum) override {} - void onConnectionChanged(bool connected) override {} + void onNowHasData(uint32_t fromRadioNum) override {} + void onConnectionChanged(bool connected) override {} -private: - bool receivePacket(void); - bool sendPacket(void); - bool notifyProgrammingMode(void); + private: + bool receivePacket(void); + bool sendPacket(void); + bool notifyProgrammingMode(void); - bool isConnected; - bool programmingMode; - PacketServer *server; - uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI + bool isConnected; + bool programmingMode; + PacketServer *server; + uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI }; extern PacketAPI *packetAPI; \ No newline at end of file diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index 087e7e19d..1a506421c 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -2,68 +2,82 @@ #include "configuration.h" #include -template ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) { - LOG_INFO("Incoming API connection"); +template +ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) +{ + LOG_INFO("Incoming API connection"); } -template ServerAPI::~ServerAPI() { client.stop(); } +template ServerAPI::~ServerAPI() +{ + client.stop(); +} -template void ServerAPI::close() { - client.stop(); // drop tcp connection - StreamAPI::close(); +template void ServerAPI::close() +{ + client.stop(); // drop tcp connection + StreamAPI::close(); } /// Check the current underlying physical link to see if the client is currently connected -template bool ServerAPI::checkIsConnected() { return client.connected(); } +template bool ServerAPI::checkIsConnected() +{ + return client.connected(); +} -template int32_t ServerAPI::runOnce() { - if (client.connected()) { - return StreamAPI::runOncePart(); - } else { - LOG_INFO("Client dropped connection, suspend API service"); - enabled = false; // we no longer need to run - return 0; - } +template int32_t ServerAPI::runOnce() +{ + if (client.connected()) { + return StreamAPI::runOncePart(); + } else { + LOG_INFO("Client dropped connection, suspend API service"); + enabled = false; // we no longer need to run + return 0; + } } template APIServerPort::APIServerPort(int port) : U(port), concurrency::OSThread("ApiServer") {} -template void APIServerPort::init() { U::begin(); } +template void APIServerPort::init() +{ + U::begin(); +} -template int32_t APIServerPort::runOnce() { +template int32_t APIServerPort::runOnce() +{ #ifdef ARCH_ESP32 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - auto client = U::accept(); + auto client = U::accept(); #else - auto client = U::available(); + auto client = U::available(); #endif #elif defined(ARCH_RP2040) - auto client = U::accept(); + auto client = U::accept(); #else - auto client = U::available(); + auto client = U::available(); #endif - if (client) { - // Close any previous connection (see FIXME in header file) - if (openAPI) { + if (client) { + // Close any previous connection (see FIXME in header file) + if (openAPI) { #if RAK_4631 - // RAK13800 Ethernet requests periodically take more time - // This backoff addresses most cases keeping max wait < 1s - // Reconnections are delayed by full wait time - if (waitTime < 400) { - waitTime *= 2; - LOG_INFO("Previous TCP connection still open, try again in %dms", waitTime); - return waitTime; - } + // RAK13800 Ethernet requests periodically take more time + // This backoff addresses most cases keeping max wait < 1s + // Reconnections are delayed by full wait time + if (waitTime < 400) { + waitTime *= 2; + LOG_INFO("Previous TCP connection still open, try again in %dms", waitTime); + return waitTime; + } #endif - LOG_INFO("Force close previous TCP connection"); - delete openAPI; + LOG_INFO("Force close previous TCP connection"); + delete openAPI; + } + + openAPI = new T(client); } - openAPI = new T(client); - } - #if RAK_4631 - waitTime = 100; + waitTime = 100; #endif - return 100; // only check occasionally for incoming connections + return 100; // only check occasionally for incoming connections } \ No newline at end of file diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h index ec28651f6..111314476 100644 --- a/src/mesh/api/ServerAPI.h +++ b/src/mesh/api/ServerAPI.h @@ -8,49 +8,51 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -template class ServerAPI : public StreamAPI, private concurrency::OSThread { -private: - T client; +template class ServerAPI : public StreamAPI, private concurrency::OSThread +{ + private: + T client; -public: - explicit ServerAPI(T &_client); + public: + explicit ServerAPI(T &_client); - virtual ~ServerAPI(); + virtual ~ServerAPI(); - /// override close to also shutdown the TCP link - virtual void close(); + /// override close to also shutdown the TCP link + virtual void close(); -protected: - /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the - /// board to stay in the POWERED state to prevent disabling wifi) - virtual void onConnectionChanged(bool connected) override {} + protected: + /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to + /// stay in the POWERED state to prevent disabling wifi) + virtual void onConnectionChanged(bool connected) override {} - virtual int32_t runOnce() override; // Check for dropped client connections + virtual int32_t runOnce() override; // Check for dropped client connections - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; }; /** * Listens for incoming connections and does accepts and creates instances of ServerAPI as needed */ -template class APIServerPort : public U, private concurrency::OSThread { - /** The currently open port - * - * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this - * class to delegate to the worker. Once coroutines are implemented we can relax this restriction. - */ - T *openAPI = NULL; +template class APIServerPort : public U, private concurrency::OSThread +{ + /** The currently open port + * + * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this class to + * delegate to the worker. Once coroutines are implemented we can relax this restriction. + */ + T *openAPI = NULL; #if defined(RAK_4631) || defined(RAK11310) - // Track wait time for RAK13800 Ethernet requests - int32_t waitTime = 100; + // Track wait time for RAK13800 Ethernet requests + int32_t waitTime = 100; #endif -public: - explicit APIServerPort(int port); + public: + explicit APIServerPort(int port); - void init(); + void init(); -protected: - int32_t runOnce() override; + protected: + int32_t runOnce() override; }; diff --git a/src/mesh/api/WiFiServerAPI.cpp b/src/mesh/api/WiFiServerAPI.cpp index 4bf6417cf..4d729f5c7 100644 --- a/src/mesh/api/WiFiServerAPI.cpp +++ b/src/mesh/api/WiFiServerAPI.cpp @@ -6,24 +6,27 @@ static WiFiServerPort *apiPort; -void initApiServer(int port) { - // Start API server on port 4403 - if (!apiPort) { - apiPort = new WiFiServerPort(port); - LOG_INFO("API server listen on TCP port %d", port); - apiPort->init(); - } +void initApiServer(int port) +{ + // Start API server on port 4403 + if (!apiPort) { + apiPort = new WiFiServerPort(port); + LOG_INFO("API server listen on TCP port %d", port); + apiPort->init(); + } } -void deInitApiServer() { - if (apiPort) { - delete apiPort; - apiPort = nullptr; - } +void deInitApiServer() +{ + if (apiPort) { + delete apiPort; + apiPort = nullptr; + } } -WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client) { - api_type = TYPE_WIFI; - LOG_INFO("Incoming wifi connection"); +WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client) +{ + api_type = TYPE_WIFI; + LOG_INFO("Incoming wifi connection"); } WiFiServerPort::WiFiServerPort(int port) : APIServerPort(port) {} diff --git a/src/mesh/api/WiFiServerAPI.h b/src/mesh/api/WiFiServerAPI.h index eea3e7559..5f2019983 100644 --- a/src/mesh/api/WiFiServerAPI.h +++ b/src/mesh/api/WiFiServerAPI.h @@ -12,17 +12,19 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -class WiFiServerAPI : public ServerAPI { -public: - explicit WiFiServerAPI(WiFiClient &_client); +class WiFiServerAPI : public ServerAPI +{ + public: + explicit WiFiServerAPI(WiFiClient &_client); }; /** * Listens for incoming connections and does accepts and creates instances of WiFiServerAPI as needed */ -class WiFiServerPort : public APIServerPort { -public: - explicit WiFiServerPort(int port); +class WiFiServerPort : public APIServerPort +{ + public: + explicit WiFiServerPort(int port); }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp index 42c0bae69..10ff06df2 100644 --- a/src/mesh/api/ethServerAPI.cpp +++ b/src/mesh/api/ethServerAPI.cpp @@ -7,18 +7,20 @@ static ethServerPort *apiPort; -void initApiServer(int port) { - // Start API server on port 4403 - if (!apiPort) { - apiPort = new ethServerPort(port); - LOG_INFO("API server listening on TCP port %d", port); - apiPort->init(); - } +void initApiServer(int port) +{ + // Start API server on port 4403 + if (!apiPort) { + apiPort = new ethServerPort(port); + LOG_INFO("API server listening on TCP port %d", port); + apiPort->init(); + } } -ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) { - LOG_INFO("Incoming ethernet connection"); - api_type = TYPE_ETH; +ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) +{ + LOG_INFO("Incoming ethernet connection"); + api_type = TYPE_ETH; } ethServerPort::ethServerPort(int port) : APIServerPort(port) {} diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index 262da80c5..c616c87be 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -8,17 +8,19 @@ * Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs * (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs). */ -class ethServerAPI : public ServerAPI { -public: - explicit ethServerAPI(EthernetClient &_client); +class ethServerAPI : public ServerAPI +{ + public: + explicit ethServerAPI(EthernetClient &_client); }; /** * Listens for incoming connections and does accepts and creates instances of EthernetServerAPI as needed */ -class ethServerPort : public APIServerPort { -public: - explicit ethServerPort(int port); +class ethServerPort : public APIServerPort +{ + public: + explicit ethServerPort(int port); }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); diff --git a/src/mesh/compression/unishox2.cpp b/src/mesh/compression/unishox2.cpp index dd81d81d5..9fc012a76 100644 --- a/src/mesh/compression/unishox2.cpp +++ b/src/mesh/compression/unishox2.cpp @@ -48,12 +48,14 @@ const char *USX_TEMPLATES[] = {"tfff-of-tfTtf:rf:rf.fffZ", "tfff-of-tf", "(fff) /// possible horizontal sets and states enum { USX_ALPHA = 0, USX_SYM, USX_NUM, USX_DICT, USX_DELTA, USX_NUM_TEMP }; -/// This 2D array has the characters for the sets USX_ALPHA, USX_SYM and USX_NUM. Where a character cannot fit into a -/// uint8_t, 0 is used and handled in code. -uint8_t usx_sets[][28] = { - {0, ' ', 'e', 't', 'a', 'o', 'i', 'n', 's', 'r', 'l', 'c', 'd', 'h', 'u', 'p', 'm', 'b', 'g', 'w', 'f', 'y', 'v', 'k', 'q', 'j', 'x', 'z'}, - {'"', '{', '}', '_', '<', '>', ':', '\n', 0, '[', ']', '\\', ';', '\'', '\t', '@', '*', '&', '?', '!', '^', '|', '\r', '~', '`', 0, 0, 0}, - {0, ',', '.', '0', '1', '9', '2', '5', '-', '/', '3', '4', '6', '7', '8', '(', ')', ' ', '=', '+', '$', '%', '#', 0, 0, 0, 0, 0}}; +/// This 2D array has the characters for the sets USX_ALPHA, USX_SYM and USX_NUM. Where a character cannot fit into a uint8_t, 0 +/// is used and handled in code. +uint8_t usx_sets[][28] = {{0, ' ', 'e', 't', 'a', 'o', 'i', 'n', 's', 'r', 'l', 'c', 'd', 'h', + 'u', 'p', 'm', 'b', 'g', 'w', 'f', 'y', 'v', 'k', 'q', 'j', 'x', 'z'}, + {'"', '{', '}', '_', '<', '>', ':', '\n', 0, '[', ']', '\\', ';', '\'', + '\t', '@', '*', '&', '?', '!', '^', '|', '\r', '~', '`', 0, 0, 0}, + {0, ',', '.', '0', '1', '9', '2', '5', '-', '/', '3', '4', '6', '7', + '8', '(', ')', ' ', '=', '+', '$', '%', '#', 0, 0, 0, 0, 0}}; /// Stores position of letter in usx_sets. /// First 3 bits - position in usx_hcodes @@ -67,8 +69,8 @@ uint8_t usx_vcodes[] = {0x00, 0x40, 0x60, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0x /// Length of each veritical code uint8_t usx_vcode_lens[] = {2, 3, 3, 4, 4, 4, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}; -/// Vertical Codes and Set number for frequent sequences in sets USX_SYM and USX_NUM. First 3 bits indicate set -/// (USX_SYM/USX_NUM) and rest are vcode positions +/// Vertical Codes and Set number for frequent sequences in sets USX_SYM and USX_NUM. First 3 bits indicate set (USX_SYM/USX_NUM) +/// and rest are vcode positions uint8_t usx_freq_codes[] = {(1 << 5) + 25, (1 << 5) + 26, (1 << 5) + 27, (2 << 5) + 23, (2 << 5) + 24, (2 << 5) + 25}; /// Not used @@ -123,21 +125,22 @@ uint8_t is_inited = 0; /// Fills the usx_code_94 94 letter array based on sets of characters at usx_sets \n /// For each element in usx_code_94, first 3 msb bits is set (USX_ALPHA / USX_SYM / USX_NUM) \n /// and the rest 5 bits indicate the vertical position in the corresponding set -void init_coder() { - if (is_inited) - return; - memset(usx_code_94, '\0', sizeof(usx_code_94)); - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 28; j++) { - uint8_t c = usx_sets[i][j]; - if (c > 32) { - usx_code_94[c - USX_OFFSET_94] = (i << 5) + j; - if (c >= 'a' && c <= 'z') - usx_code_94[c - USX_OFFSET_94 - ('a' - 'A')] = (i << 5) + j; - } +void init_coder() +{ + if (is_inited) + return; + memset(usx_code_94, '\0', sizeof(usx_code_94)); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 28; j++) { + uint8_t c = usx_sets[i][j]; + if (c > 32) { + usx_code_94[c - USX_OFFSET_94] = (i << 5) + j; + if (c >= 'a' && c <= 'z') + usx_code_94[c - USX_OFFSET_94 - ('a' - 'A')] = (i << 5) + j; + } + } } - } - is_inited = 1; + is_inited = 1; } /// Mask for retrieving each code to be encoded according to its length @@ -146,80 +149,84 @@ unsigned int usx_mask[] = {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF}; /// Appends specified number of bits to the output (out) \n /// If maximum limit (olen) is reached, -1 is returned \n /// Otherwise clen bits in code are appended to out starting with MSB -int append_bits(char *out, int olen, int ol, uint8_t code, int clen) { +int append_bits(char *out, int olen, int ol, uint8_t code, int clen) +{ - // printf("%d,%x,%d,%d\n", ol, code, clen, state); + // printf("%d,%x,%d,%d\n", ol, code, clen, state); - while (clen > 0) { - int oidx; - unsigned char a_byte; + while (clen > 0) { + int oidx; + unsigned char a_byte; - uint8_t cur_bit = ol % 8; - uint8_t blen = clen; - a_byte = code & usx_mask[blen - 1]; - a_byte >>= cur_bit; - if (blen + cur_bit > 8) - blen = (8 - cur_bit); - oidx = ol / 8; - if (oidx < 0 || olen <= oidx) - return -1; - if (cur_bit == 0) - out[oidx] = a_byte; - else - out[oidx] |= a_byte; - code <<= blen; - ol += blen; - clen -= blen; - } - return ol; + uint8_t cur_bit = ol % 8; + uint8_t blen = clen; + a_byte = code & usx_mask[blen - 1]; + a_byte >>= cur_bit; + if (blen + cur_bit > 8) + blen = (8 - cur_bit); + oidx = ol / 8; + if (oidx < 0 || olen <= oidx) + return -1; + if (cur_bit == 0) + out[oidx] = a_byte; + else + out[oidx] |= a_byte; + code <<= blen; + ol += blen; + clen -= blen; + } + return ol; } /// This is a safe call to append_bits() making sure it does not write past olen -#define SAFE_APPEND_BITS(exp) \ - do { \ - const int newidx = (exp); \ - if (newidx < 0) \ - return newidx; \ - } while (0) +#define SAFE_APPEND_BITS(exp) \ + do { \ + const int newidx = (exp); \ + if (newidx < 0) \ + return newidx; \ + } while (0) /// Appends switch code to out depending on the state (USX_DELTA or other) -int append_switch_code(char *out, int olen, int ol, uint8_t state) { - if (state == USX_DELTA) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SW_CODE, UNI_STATE_SW_CODE_LEN)); - } else - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, SW_CODE, SW_CODE_LEN)); - return ol; +int append_switch_code(char *out, int olen, int ol, uint8_t state) +{ + if (state == USX_DELTA) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, UNI_STATE_SW_CODE, UNI_STATE_SW_CODE_LEN)); + } else + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, SW_CODE, SW_CODE_LEN)); + return ol; } /// Appends given horizontal and veritical code bits to out -int append_code(char *out, int olen, int ol, uint8_t code, uint8_t *state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { - uint8_t hcode = code >> 5; - uint8_t vcode = code & 0x1F; - if (!usx_hcode_lens[hcode] && hcode != USX_ALPHA) +int append_code(char *out, int olen, int ol, uint8_t code, uint8_t *state, const uint8_t usx_hcodes[], + const uint8_t usx_hcode_lens[]) +{ + uint8_t hcode = code >> 5; + uint8_t vcode = code & 0x1F; + if (!usx_hcode_lens[hcode] && hcode != USX_ALPHA) + return ol; + switch (hcode) { + case USX_ALPHA: + if (*state != USX_ALPHA) { + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + *state = USX_ALPHA; + } + break; + case USX_SYM: + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_SYM], usx_hcode_lens[USX_SYM])); + break; + case USX_NUM: + if (*state != USX_NUM) { + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); + if (usx_sets[hcode][vcode] >= '0' && usx_sets[hcode][vcode] <= '9') + *state = USX_NUM; + } + } + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[vcode], usx_vcode_lens[vcode])); return ol; - switch (hcode) { - case USX_ALPHA: - if (*state != USX_ALPHA) { - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - *state = USX_ALPHA; - } - break; - case USX_SYM: - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_SYM], usx_hcode_lens[USX_SYM])); - break; - case USX_NUM: - if (*state != USX_NUM) { - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, *state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); - if (usx_sets[hcode][vcode] >= '0' && usx_sets[hcode][vcode] <= '9') - *state = USX_NUM; - } - } - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[vcode], usx_vcode_lens[vcode])); - return ol; } /// Length of bits used to represent count for each level @@ -229,21 +236,22 @@ const int32_t count_adder[5] = {4, 20, 148, 2196, 67732}; /// Codes used to specify the level that the count belongs to const uint8_t count_codes[] = {0x01, 0x82, 0xC3, 0xE4, 0xF4}; /// Encodes given count to out -int encodeCount(char *out, int olen, int ol, int count) { - // First five bits are code and Last three bits of codes represent length - for (int i = 0; i < 5; i++) { - if (count < count_adder[i]) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); - uint16_t count16 = (count - (i ? count_adder[i - 1] : 0)) << (16 - count_bit_lens[i]); - if (count_bit_lens[i] > 8) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 & 0xFF, count_bit_lens[i] - 8)); - } else - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, count_bit_lens[i])); - return ol; +int encodeCount(char *out, int olen, int ol, int count) +{ + // First five bits are code and Last three bits of codes represent length + for (int i = 0; i < 5; i++) { + if (count < count_adder[i]) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); + uint16_t count16 = (count - (i ? count_adder[i - 1] : 0)) << (16 - count_bit_lens[i]); + if (count_bit_lens[i] > 8) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 & 0xFF, count_bit_lens[i] - 8)); + } else + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, count16 >> 8, count_bit_lens[i])); + return ol; + } } - } - return ol; + return ol; } /// Length of bits used to represent delta code for each level @@ -252,75 +260,78 @@ const uint8_t uni_bit_len[5] = {6, 12, 14, 16, 21}; const int32_t uni_adder[5] = {0, 64, 4160, 20544, 86080}; /// Encodes the unicode code point given by code to out. prev_code is used to calculate the delta -int encodeUnicode(char *out, int olen, int ol, int32_t code, int32_t prev_code) { - // First five bits are code and Last three bits of codes represent length - // const uint8_t codes[8] = {0x00, 0x42, 0x83, 0xA3, 0xC3, 0xE4, 0xF5, 0xFD}; - const uint8_t codes[6] = {0x01, 0x82, 0xC3, 0xE4, 0xF5, 0xFD}; - int32_t till = 0; - int32_t diff = code - prev_code; - if (diff < 0) - diff = -diff; - // printf("%ld, ", code); - // printf("Diff: %d\n", diff); - for (int i = 0; i < 5; i++) { - till += (1 << uni_bit_len[i]); - if (diff < till) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (codes[i] & 0xF8), codes[i] & 0x07)); - // if (diff) { - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, prev_code > code ? 0x80 : 0, 1)); - int32_t val = diff - uni_adder[i]; - // printf("Val: %d\n", val); - if (uni_bit_len[i] > 16) { - val <<= (24 - uni_bit_len[i]); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 16, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (val >> 8) & 0xFF, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 16)); - } else if (uni_bit_len[i] > 8) { - val <<= (16 - uni_bit_len[i]); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 8, 8)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 8)); - } else { - val <<= (8 - uni_bit_len[i]); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i])); - } - return ol; +int encodeUnicode(char *out, int olen, int ol, int32_t code, int32_t prev_code) +{ + // First five bits are code and Last three bits of codes represent length + // const uint8_t codes[8] = {0x00, 0x42, 0x83, 0xA3, 0xC3, 0xE4, 0xF5, 0xFD}; + const uint8_t codes[6] = {0x01, 0x82, 0xC3, 0xE4, 0xF5, 0xFD}; + int32_t till = 0; + int32_t diff = code - prev_code; + if (diff < 0) + diff = -diff; + // printf("%ld, ", code); + // printf("Diff: %d\n", diff); + for (int i = 0; i < 5; i++) { + till += (1 << uni_bit_len[i]); + if (diff < till) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (codes[i] & 0xF8), codes[i] & 0x07)); + // if (diff) { + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, prev_code > code ? 0x80 : 0, 1)); + int32_t val = diff - uni_adder[i]; + // printf("Val: %d\n", val); + if (uni_bit_len[i] > 16) { + val <<= (24 - uni_bit_len[i]); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 16, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (val >> 8) & 0xFF, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 16)); + } else if (uni_bit_len[i] > 8) { + val <<= (16 - uni_bit_len[i]); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val >> 8, 8)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i] - 8)); + } else { + val <<= (8 - uni_bit_len[i]); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, val & 0xFF, uni_bit_len[i])); + } + return ol; + } } - } - return ol; + return ol; } /// Reads UTF-8 character from in. Also returns the number of bytes occupied by the UTF-8 character in utf8len -int32_t readUTF8(const char *in, int len, int l, int *utf8len) { - int32_t ret = 0; - if (l < (len - 1) && (in[l] & 0xE0) == 0xC0 && (in[l + 1] & 0xC0) == 0x80) { - *utf8len = 2; - ret = (in[l] & 0x1F); - ret <<= 6; - ret += (in[l + 1] & 0x3F); - if (ret < 0x80) - ret = 0; - } else if (l < (len - 2) && (in[l] & 0xF0) == 0xE0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80) { - *utf8len = 3; - ret = (in[l] & 0x0F); - ret <<= 6; - ret += (in[l + 1] & 0x3F); - ret <<= 6; - ret += (in[l + 2] & 0x3F); - if (ret < 0x0800) - ret = 0; - } else if (l < (len - 3) && (in[l] & 0xF8) == 0xF0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80 && (in[l + 3] & 0xC0) == 0x80) { - *utf8len = 4; - ret = (in[l] & 0x07); - ret <<= 6; - ret += (in[l + 1] & 0x3F); - ret <<= 6; - ret += (in[l + 2] & 0x3F); - ret <<= 6; - ret += (in[l + 3] & 0x3F); - if (ret < 0x10000) - ret = 0; - } - return ret; +int32_t readUTF8(const char *in, int len, int l, int *utf8len) +{ + int32_t ret = 0; + if (l < (len - 1) && (in[l] & 0xE0) == 0xC0 && (in[l + 1] & 0xC0) == 0x80) { + *utf8len = 2; + ret = (in[l] & 0x1F); + ret <<= 6; + ret += (in[l + 1] & 0x3F); + if (ret < 0x80) + ret = 0; + } else if (l < (len - 2) && (in[l] & 0xF0) == 0xE0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80) { + *utf8len = 3; + ret = (in[l] & 0x0F); + ret <<= 6; + ret += (in[l + 1] & 0x3F); + ret <<= 6; + ret += (in[l + 2] & 0x3F); + if (ret < 0x0800) + ret = 0; + } else if (l < (len - 3) && (in[l] & 0xF8) == 0xF0 && (in[l + 1] & 0xC0) == 0x80 && (in[l + 2] & 0xC0) == 0x80 && + (in[l + 3] & 0xC0) == 0x80) { + *utf8len = 4; + ret = (in[l] & 0x07); + ret <<= 6; + ret += (in[l + 1] & 0x3F); + ret <<= 6; + ret += (in[l + 2] & 0x3F); + ret <<= 6; + ret += (in[l + 3] & 0x3F); + if (ret < 0x10000) + ret = 0; + } + return ret; } /// Finds the longest matching sequence from the beginning of the string. \n @@ -329,40 +340,41 @@ int32_t readUTF8(const char *in, int len, int l, int *utf8len) { /// This is a crude implementation that is not optimized. Assuming only short strings \n /// are encoded, this is not much of an issue. int matchOccurance(const char *in, int len, int l, char *out, int olen, int *ol, const uint8_t *state, const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[]) { - int j, k; - int longest_dist = 0; - int longest_len = 0; - for (j = l - NICE_LEN; j >= 0; j--) { - for (k = l; k < len && j + k - l < l; k++) { - if (in[k] != in[j + k - l]) - break; + const uint8_t usx_hcode_lens[]) +{ + int j, k; + int longest_dist = 0; + int longest_len = 0; + for (j = l - NICE_LEN; j >= 0; j--) { + for (k = l; k < len && j + k - l < l; k++) { + if (in[k] != in[j + k - l]) + break; + } + while ((((unsigned char)in[k]) >> 6) == 2) + k--; // Skip partial UTF-8 matches + // if ((in[k - 1] >> 3) == 0x1E || (in[k - 1] >> 4) == 0x0E || (in[k - 1] >> 5) == 0x06) + // k--; + if ((k - l) > (NICE_LEN - 1)) { + int match_len = k - l - NICE_LEN; + int match_dist = l - j - NICE_LEN + 1; + if (match_len > longest_len) { + longest_len = match_len; + longest_dist = match_dist; + } + } } - while ((((unsigned char)in[k]) >> 6) == 2) - k--; // Skip partial UTF-8 matches - // if ((in[k - 1] >> 3) == 0x1E || (in[k - 1] >> 4) == 0x0E || (in[k - 1] >> 5) == 0x06) - // k--; - if ((k - l) > (NICE_LEN - 1)) { - int match_len = k - l - NICE_LEN; - int match_dist = l - j - NICE_LEN + 1; - if (match_len > longest_len) { - longest_len = match_len; - longest_dist = match_dist; - } + if (longest_len) { + SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); + SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); + // printf("Len:%d / Dist:%d/%.*s\n", longest_len, longest_dist, longest_len + NICE_LEN, in + l - longest_dist - NICE_LEN + + // 1); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_len)); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_dist)); + l += (longest_len + NICE_LEN); + l--; + return l; } - } - if (longest_len) { - SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); - SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); - // printf("Len:%d / Dist:%d/%.*s\n", longest_len, longest_dist, longest_len + NICE_LEN, in + l - longest_dist - - // NICE_LEN + 1); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_len)); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, longest_dist)); - l += (longest_len + NICE_LEN); - l--; - return l; - } - return -l; + return -l; } /// This is used only when encoding a string array @@ -372,73 +384,75 @@ int matchOccurance(const char *in, int len, int l, char *out, int olen, int *ol, /// This is a crude implementation that is not optimized. Assuming only short strings \n /// are encoded, this is not much of an issue. int matchLine(const char *in, int len, int l, char *out, int olen, int *ol, struct us_lnk_lst *prev_lines, const uint8_t *state, - const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { - int last_ol = *ol; - int last_len = 0; - int last_dist = 0; - int last_ctx = 0; - int line_ctr = 0; - int j = 0; - do { - int i, k; - int line_len = (int)strlen(prev_lines->data); - int limit = (line_ctr == 0 ? l : line_len); - for (; j < limit; j++) { - for (i = l, k = j; k < line_len && i < len; k++, i++) { - if (prev_lines->data[k] != in[i]) - break; - } - while ((((unsigned char)prev_lines->data[k]) >> 6) == 2) - k--; // Skip partial UTF-8 matches - if ((k - j) >= NICE_LEN) { - if (last_len) { - if (j > last_dist) - continue; - // int saving = ((k - j) - last_len) + (last_dist - j) + (last_ctx - line_ctr); - // if (saving < 0) { - // //printf("No savng: %d\n", saving); - // continue; - // } - *ol = last_ol; + const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) +{ + int last_ol = *ol; + int last_len = 0; + int last_dist = 0; + int last_ctx = 0; + int line_ctr = 0; + int j = 0; + do { + int i, k; + int line_len = (int)strlen(prev_lines->data); + int limit = (line_ctr == 0 ? l : line_len); + for (; j < limit; j++) { + for (i = l, k = j; k < line_len && i < len; k++, i++) { + if (prev_lines->data[k] != in[i]) + break; + } + while ((((unsigned char)prev_lines->data[k]) >> 6) == 2) + k--; // Skip partial UTF-8 matches + if ((k - j) >= NICE_LEN) { + if (last_len) { + if (j > last_dist) + continue; + // int saving = ((k - j) - last_len) + (last_dist - j) + (last_ctx - line_ctr); + // if (saving < 0) { + // //printf("No savng: %d\n", saving); + // continue; + // } + *ol = last_ol; + } + last_len = (k - j); + last_dist = j; + last_ctx = line_ctr; + SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); + SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_len - NICE_LEN)); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_dist)); + SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_ctx)); + /* + if ((*ol - last_ol) > (last_len * 4)) { + last_len = 0; + *ol = last_ol; + }*/ + // printf("Len: %d, Dist: %d, Line: %d\n", last_len, last_dist, last_ctx); + j += last_len; + } } - last_len = (k - j); - last_dist = j; - last_ctx = line_ctr; - SAFE_APPEND_BITS(*ol = append_switch_code(out, olen, *ol, *state)); - SAFE_APPEND_BITS(*ol = append_bits(out, olen, *ol, usx_hcodes[USX_DICT], usx_hcode_lens[USX_DICT])); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_len - NICE_LEN)); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_dist)); - SAFE_APPEND_BITS(*ol = encodeCount(out, olen, *ol, last_ctx)); - /* - if ((*ol - last_ol) > (last_len * 4)) { - last_len = 0; - *ol = last_ol; - }*/ - // printf("Len: %d, Dist: %d, Line: %d\n", last_len, last_dist, last_ctx); - j += last_len; - } + line_ctr++; + prev_lines = prev_lines->previous; + } while (prev_lines && prev_lines->data != NULL); + if (last_len) { + l += last_len; + l--; + return l; } - line_ctr++; - prev_lines = prev_lines->previous; - } while (prev_lines && prev_lines->data != NULL); - if (last_len) { - l += last_len; - l--; - return l; - } - return -l; + return -l; } /// Returns 4 bit code assuming ch falls between '0' to '9', \n /// 'A' to 'F' or 'a' to 'f' -uint8_t getBaseCode(char ch) { - if (ch >= '0' && ch <= '9') - return (ch - '0') << 4; - else if (ch >= 'A' && ch <= 'F') - return (ch - 'A' + 10) << 4; - else if (ch >= 'a' && ch <= 'f') - return (ch - 'a' + 10) << 4; - return 0; +uint8_t getBaseCode(char ch) +{ + if (ch >= '0' && ch <= '9') + return (ch - '0') << 4; + else if (ch >= 'A' && ch <= 'F') + return (ch - 'A' + 10) << 4; + else if (ch >= 'a' && ch <= 'f') + return (ch - 'a' + 10) << 4; + return 0; } /// Enum indicating nibble type - USX_NIB_NUM means ch is a number '0' to '9', \n @@ -447,415 +461,440 @@ uint8_t getBaseCode(char ch) { enum { USX_NIB_NUM = 0, USX_NIB_HEX_LOWER, USX_NIB_HEX_UPPER, USX_NIB_NOT }; /// Gets 4 bit code assuming ch falls between '0' to '9', \n /// 'A' to 'F' or 'a' to 'f' -char getNibbleType(char ch) { - if (ch >= '0' && ch <= '9') - return USX_NIB_NUM; - else if (ch >= 'a' && ch <= 'f') - return USX_NIB_HEX_LOWER; - else if (ch >= 'A' && ch <= 'F') - return USX_NIB_HEX_UPPER; - return USX_NIB_NOT; +char getNibbleType(char ch) +{ + if (ch >= '0' && ch <= '9') + return USX_NIB_NUM; + else if (ch >= 'a' && ch <= 'f') + return USX_NIB_HEX_LOWER; + else if (ch >= 'A' && ch <= 'F') + return USX_NIB_HEX_UPPER; + return USX_NIB_NOT; } /// Starts coding of nibble sets -int append_nibble_escape(char *out, int olen, int ol, uint8_t state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, 0, 2)); - return ol; +int append_nibble_escape(char *out, int olen, int ol, uint8_t state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) +{ + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, 0, 2)); + return ol; } /// Returns minimum value of two longs -long min_of(long c, long i) { return c > i ? i : c; } +long min_of(long c, long i) +{ + return c > i ? i : c; +} -/// Appends the terminator code depending on the state, preset and whether full terminator needs to be encoded to out or -/// not \n -int append_final_bits(char *const out, const int olen, int ol, const uint8_t state, const uint8_t is_all_upper, const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[]) { - if (usx_hcode_lens[USX_ALPHA]) { - if (USX_NUM != state) { - // for num state, append TERM_CODE directly - // for other state, switch to Num Set first - SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); +/// Appends the terminator code depending on the state, preset and whether full terminator needs to be encoded to out or not \n +int append_final_bits(char *const out, const int olen, int ol, const uint8_t state, const uint8_t is_all_upper, + const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) +{ + if (usx_hcode_lens[USX_ALPHA]) { + if (USX_NUM != state) { + // for num state, append TERM_CODE directly + // for other state, switch to Num Set first + SAFE_APPEND_BITS(ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_hcodes[USX_NUM], usx_hcode_lens[USX_NUM])); + } + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[TERM_CODE & 0x1F], usx_vcode_lens[TERM_CODE & 0x1F])); + } else { + // preset 1, terminate at 2 or 3 SW_CODE, i.e., 4 or 6 continuous 0 bits + // see discussion: https://github.com/siara-cc/Unishox/issues/19#issuecomment-922435580 + SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, TERM_BYTE_PRESET_1, + is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER)); } - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, usx_vcodes[TERM_CODE & 0x1F], usx_vcode_lens[TERM_CODE & 0x1F])); - } else { - // preset 1, terminate at 2 or 3 SW_CODE, i.e., 4 or 6 continuous 0 bits - // see discussion: https://github.com/siara-cc/Unishox/issues/19#issuecomment-922435580 - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, TERM_BYTE_PRESET_1, is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER)); - } - // fill uint8_t with the last bit - SAFE_APPEND_BITS(ol = append_bits(out, olen, ol, (ol == 0 || out[(ol - 1) / 8] << ((ol - 1) & 7) >= 0) ? 0 : 0xFF, (8 - ol % 8) & 7)); + // fill uint8_t with the last bit + SAFE_APPEND_BITS( + ol = append_bits(out, olen, ol, (ol == 0 || out[(ol - 1) / 8] << ((ol - 1) & 7) >= 0) ? 0 : 0xFF, (8 - ol % 8) & 7)); - return ol; + return ol; } /// Macro used in the main compress function so that if the output len exceeds given maximum length (olen) it can exit -#define SAFE_APPEND_BITS2(olen, exp) \ - do { \ - const int newidx = (exp); \ - const int __olen = (olen); \ - if (newidx < 0) \ - return __olen >= 0 ? __olen + 1 : (1 - __olen) * 4; \ - } while (0) +#define SAFE_APPEND_BITS2(olen, exp) \ + do { \ + const int newidx = (exp); \ + const int __olen = (olen); \ + if (newidx < 0) \ + return __olen >= 0 ? __olen + 1 : (1 - __olen) * 4; \ + } while (0) // Main API function. See unishox2.h for documentation int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines) { + const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], + struct us_lnk_lst *prev_lines) +{ - uint8_t state; + uint8_t state; - int l, ll, ol; - char c_in, c_next; - int prev_uni; - uint8_t is_upper, is_all_upper; + int l, ll, ol; + char c_in, c_next; + int prev_uni; + uint8_t is_upper, is_all_upper; #if (UNISHOX_API_OUT_AND_LEN(0, 1)) == 0 - const int olen = INT_MAX - 1; - const int rawolen = olen; - const uint8_t need_full_term_codes = 0; + const int olen = INT_MAX - 1; + const int rawolen = olen; + const uint8_t need_full_term_codes = 0; #else - const int rawolen = olen; - uint8_t need_full_term_codes = 0; - if (olen < 0) { - need_full_term_codes = 1; - olen *= -1; - } + const int rawolen = olen; + uint8_t need_full_term_codes = 0; + if (olen < 0) { + need_full_term_codes = 1; + olen *= -1; + } #endif - init_coder(); - ol = 0; - prev_uni = 0; - state = USX_ALPHA; - is_all_upper = 0; - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNISHOX_MAGIC_BITS, UNISHOX_MAGIC_BIT_LEN)); // magic bit(s) - for (l = 0; l < len; l++) { + init_coder(); + ol = 0; + prev_uni = 0; + state = USX_ALPHA; + is_all_upper = 0; + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNISHOX_MAGIC_BITS, UNISHOX_MAGIC_BIT_LEN)); // magic bit(s) + for (l = 0; l < len; l++) { - if (usx_hcode_lens[USX_DICT] && l < (len - NICE_LEN + 1)) { - if (prev_lines) { - l = matchLine(in, len, l, out, olen, &ol, prev_lines, &state, usx_hcodes, usx_hcode_lens); - if (l > 0) { - continue; - } else if (l < 0 && ol < 0) { - return olen + 1; - } - l = -l; - } else { - l = matchOccurance(in, len, l, out, olen, &ol, &state, usx_hcodes, usx_hcode_lens); - if (l > 0) { - continue; - } else if (l < 0 && ol < 0) { - return olen + 1; - } - l = -l; - } - } - - c_in = in[l]; - if (l && len > 4 && l < (len - 4) && usx_hcode_lens[USX_NUM]) { - if (c_in == in[l - 1] && c_in == in[l + 1] && c_in == in[l + 2] && c_in == in[l + 3]) { - int rpt_count = l + 4; - while (rpt_count < len && in[rpt_count] == c_in) - rpt_count++; - rpt_count -= l; - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, RPT_CODE, &state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rpt_count - 4)); - l += rpt_count; - l--; - continue; - } - } - - if (l <= (len - 36) && usx_hcode_lens[USX_NUM]) { - if (in[l + 8] == '-' && in[l + 13] == '-' && in[l + 18] == '-' && in[l + 23] == '-') { - char hex_type = USX_NIB_NUM; - int uid_pos = l; - for (; uid_pos < l + 36; uid_pos++) { - char c_uid = in[uid_pos]; - if (c_uid == '-' && (uid_pos == 8 || uid_pos == 13 || uid_pos == 18 || uid_pos == 23)) - continue; - char nib_type = getNibbleType(c_uid); - if (nib_type == USX_NIB_NOT) - break; - if (nib_type != USX_NIB_NUM) { - if (hex_type != USX_NIB_NUM && hex_type != nib_type) - break; - hex_type = nib_type; - } - } - if (uid_pos == l + 36) { - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, - ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0xC0 : 0xF0), (hex_type == USX_NIB_HEX_LOWER ? 3 : 5))); - for (uid_pos = l; uid_pos < l + 36; uid_pos++) { - char c_uid = in[uid_pos]; - if (c_uid != '-') - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(c_uid), 4)); - } - // printf("GUID:\n"); - l += 35; - continue; - } - } - } - - if (l < (len - 5) && usx_hcode_lens[USX_NUM]) { - char hex_type = USX_NIB_NUM; - int hex_len = 0; - do { - char nib_type = getNibbleType(in[l + hex_len]); - if (nib_type == USX_NIB_NOT) - break; - if (nib_type != USX_NIB_NUM) { - if (hex_type != USX_NIB_NUM && hex_type != nib_type) - break; - hex_type = nib_type; - } - hex_len++; - } while (l + hex_len < len); - if (hex_len > 10 && hex_type == USX_NIB_NUM) - hex_type = USX_NIB_HEX_LOWER; - if ((hex_type == USX_NIB_HEX_LOWER || hex_type == USX_NIB_HEX_UPPER) && hex_len > 3) { - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, - ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0x80 : 0xE0), (hex_type == USX_NIB_HEX_LOWER ? 2 : 4))); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, hex_len)); - do { - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l++]), 4)); - } while (--hex_len); - l--; - continue; - } - } - - if (usx_templates != NULL) { - int i; - for (i = 0; i < 5; i++) { - if (usx_templates[i]) { - int rem = (int)strlen(usx_templates[i]); - int j = 0; - for (; j < rem && l + j < len; j++) { - char c_t = usx_templates[i][j]; - c_in = in[l + j]; - if (c_t == 'f' || c_t == 'F') { - if (getNibbleType(c_in) != (c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER) && getNibbleType(c_in) != USX_NIB_NUM) { - break; - } - } else if (c_t == 'r' || c_t == 't' || c_t == 'o') { - if (c_in < '0' || c_in > (c_t == 'r' ? '7' : (c_t == 't' ? '3' : '1'))) - break; - } else if (c_t != c_in) - break; - } - if (((float)j / rem) > 0.66) { - // printf("%s\n", usx_templates[i]); - rem = rem - j; - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0, 1)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rem)); - for (int k = 0; k < j; k++) { - char c_t = usx_templates[i][k]; - if (c_t == 'f' || c_t == 'F') - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l + k]), 4)); - else if (c_t == 'r' || c_t == 't' || c_t == 'o') { - c_t = (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (in[l + k] - '0') << (8 - c_t), c_t)); - } + if (usx_hcode_lens[USX_DICT] && l < (len - NICE_LEN + 1)) { + if (prev_lines) { + l = matchLine(in, len, l, out, olen, &ol, prev_lines, &state, usx_hcodes, usx_hcode_lens); + if (l > 0) { + continue; + } else if (l < 0 && ol < 0) { + return olen + 1; + } + l = -l; + } else { + l = matchOccurance(in, len, l, out, olen, &ol, &state, usx_hcodes, usx_hcode_lens); + if (l > 0) { + continue; + } else if (l < 0 && ol < 0) { + return olen + 1; + } + l = -l; } - l += j; - l--; - break; - } } - } - if (i < 5) - continue; - } - if (usx_freq_seq != NULL) { - int i; - for (i = 0; i < 6; i++) { - int seq_len = (int)strlen(usx_freq_seq[i]); - if (len - seq_len >= 0 && l <= len - seq_len) { - if (memcmp(usx_freq_seq[i], in + l, seq_len) == 0 && usx_hcode_lens[usx_freq_codes[i] >> 5]) { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, usx_freq_codes[i], &state, usx_hcodes, usx_hcode_lens)); - l += seq_len; - l--; - break; - } + c_in = in[l]; + if (l && len > 4 && l < (len - 4) && usx_hcode_lens[USX_NUM]) { + if (c_in == in[l - 1] && c_in == in[l + 1] && c_in == in[l + 2] && c_in == in[l + 3]) { + int rpt_count = l + 4; + while (rpt_count < len && in[rpt_count] == c_in) + rpt_count++; + rpt_count -= l; + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, RPT_CODE, &state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rpt_count - 4)); + l += rpt_count; + l--; + continue; + } } - } - if (i < 6) - continue; - } - c_in = in[l]; + if (l <= (len - 36) && usx_hcode_lens[USX_NUM]) { + if (in[l + 8] == '-' && in[l + 13] == '-' && in[l + 18] == '-' && in[l + 23] == '-') { + char hex_type = USX_NIB_NUM; + int uid_pos = l; + for (; uid_pos < l + 36; uid_pos++) { + char c_uid = in[uid_pos]; + if (c_uid == '-' && (uid_pos == 8 || uid_pos == 13 || uid_pos == 18 || uid_pos == 23)) + continue; + char nib_type = getNibbleType(c_uid); + if (nib_type == USX_NIB_NOT) + break; + if (nib_type != USX_NIB_NUM) { + if (hex_type != USX_NIB_NUM && hex_type != nib_type) + break; + hex_type = nib_type; + } + } + if (uid_pos == l + 36) { + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0xC0 : 0xF0), + (hex_type == USX_NIB_HEX_LOWER ? 3 : 5))); + for (uid_pos = l; uid_pos < l + 36; uid_pos++) { + char c_uid = in[uid_pos]; + if (c_uid != '-') + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(c_uid), 4)); + } + // printf("GUID:\n"); + l += 35; + continue; + } + } + } - is_upper = 0; - if (c_in >= 'A' && c_in <= 'Z') - is_upper = 1; - else { - if (is_all_upper) { - is_all_upper = 0; - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - state = USX_ALPHA; - } - } - if (is_upper && !is_all_upper) { - if (state == USX_NUM) { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - state = USX_ALPHA; - } - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - if (state == USX_DELTA) { - state = USX_ALPHA; - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - } - } - c_next = 0; - if (l + 1 < len) - c_next = in[l + 1]; + if (l < (len - 5) && usx_hcode_lens[USX_NUM]) { + char hex_type = USX_NIB_NUM; + int hex_len = 0; + do { + char nib_type = getNibbleType(in[l + hex_len]); + if (nib_type == USX_NIB_NOT) + break; + if (nib_type != USX_NIB_NUM) { + if (hex_type != USX_NIB_NUM && hex_type != nib_type) + break; + hex_type = nib_type; + } + hex_len++; + } while (l + hex_len < len); + if (hex_len > 10 && hex_type == USX_NIB_NUM) + hex_type = USX_NIB_HEX_LOWER; + if ((hex_type == USX_NIB_HEX_LOWER || hex_type == USX_NIB_HEX_UPPER) && hex_len > 3) { + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (hex_type == USX_NIB_HEX_LOWER ? 0x80 : 0xE0), + (hex_type == USX_NIB_HEX_LOWER ? 2 : 4))); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, hex_len)); + do { + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l++]), 4)); + } while (--hex_len); + l--; + continue; + } + } - if (c_in >= 32 && c_in <= 126) { - if (is_upper && !is_all_upper) { - for (ll = l + 4; ll >= l && ll < len; ll--) { - if (in[ll] < 'A' || in[ll] > 'Z') - break; + if (usx_templates != NULL) { + int i; + for (i = 0; i < 5; i++) { + if (usx_templates[i]) { + int rem = (int)strlen(usx_templates[i]); + int j = 0; + for (; j < rem && l + j < len; j++) { + char c_t = usx_templates[i][j]; + c_in = in[l + j]; + if (c_t == 'f' || c_t == 'F') { + if (getNibbleType(c_in) != (c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER) && + getNibbleType(c_in) != USX_NIB_NUM) { + break; + } + } else if (c_t == 'r' || c_t == 't' || c_t == 'o') { + if (c_in < '0' || c_in > (c_t == 'r' ? '7' : (c_t == 't' ? '3' : '1'))) + break; + } else if (c_t != c_in) + break; + } + if (((float)j / rem) > 0.66) { + // printf("%s\n", usx_templates[i]); + rem = rem - j; + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0, 1)); + SAFE_APPEND_BITS2(rawolen, + ol = append_bits(out, olen, ol, (count_codes[i] & 0xF8), count_codes[i] & 0x07)); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, rem)); + for (int k = 0; k < j; k++) { + char c_t = usx_templates[i][k]; + if (c_t == 'f' || c_t == 'F') + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, getBaseCode(in[l + k]), 4)); + else if (c_t == 'r' || c_t == 't' || c_t == 'o') { + c_t = (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, (in[l + k] - '0') << (8 - c_t), c_t)); + } + } + l += j; + l--; + break; + } + } + } + if (i < 5) + continue; } - if (ll == l - 1) { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - state = USX_ALPHA; - is_all_upper = 1; + + if (usx_freq_seq != NULL) { + int i; + for (i = 0; i < 6; i++) { + int seq_len = (int)strlen(usx_freq_seq[i]); + if (len - seq_len >= 0 && l <= len - seq_len) { + if (memcmp(usx_freq_seq[i], in + l, seq_len) == 0 && usx_hcode_lens[usx_freq_codes[i] >> 5]) { + SAFE_APPEND_BITS2(rawolen, + ol = append_code(out, olen, ol, usx_freq_codes[i], &state, usx_hcodes, usx_hcode_lens)); + l += seq_len; + l--; + break; + } + } + } + if (i < 6) + continue; } - } - if (state == USX_DELTA && (c_in == ' ' || c_in == '.' || c_in == ',')) { - uint8_t spl_code = (c_in == ',' ? 0xC0 : (c_in == '.' ? 0xE0 : (c_in == ' ' ? 0 : 0xFF))); - if (spl_code != 0xFF) { - uint8_t spl_code_len = (c_in == ',' ? 3 : (c_in == '.' ? 4 : (c_in == ' ' ? 1 : 4))); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, spl_code, spl_code_len)); - continue; + + c_in = in[l]; + + is_upper = 0; + if (c_in >= 'A' && c_in <= 'Z') + is_upper = 1; + else { + if (is_all_upper) { + is_all_upper = 0; + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + state = USX_ALPHA; + } } - } - c_in -= 32; - if (is_all_upper && is_upper) - c_in += 32; - if (c_in == 0) { - if (state == USX_NUM) - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[NUM_SPC_CODE & 0x1F], usx_vcode_lens[NUM_SPC_CODE & 0x1F])); - else - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); - } else { - c_in--; - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, usx_code_94[(int)c_in], &state, usx_hcodes, usx_hcode_lens)); - } - } else if (c_in == 13 && c_next == 10) { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CRLF_CODE, &state, usx_hcodes, usx_hcode_lens)); - l++; - } else if (c_in == 10) { - if (state == USX_DELTA) { - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF0, 4)); - } else - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, LF_CODE, &state, usx_hcodes, usx_hcode_lens)); - } else if (c_in == 13) { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CR_CODE, &state, usx_hcodes, usx_hcode_lens)); - } else if (c_in == '\t') { - SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, TAB_CODE, &state, usx_hcodes, usx_hcode_lens)); - } else { - int utf8len; - int32_t uni = readUTF8(in, len, l, &utf8len); - if (uni) { - l += utf8len; - if (state != USX_DELTA) { - int32_t uni2 = readUTF8(in, len, l, &utf8len); - if (uni2) { - if (state != USX_ALPHA) { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + if (is_upper && !is_all_upper) { + if (state == USX_NUM) { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + state = USX_ALPHA; } SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); // code for space (' ') - state = USX_DELTA; - } else { - SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_DELTA], usx_hcode_lens[USX_DELTA])); - } + if (state == USX_DELTA) { + state = USX_ALPHA; + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + } } - SAFE_APPEND_BITS2(rawolen, ol = encodeUnicode(out, olen, ol, uni, prev_uni)); - // printf("%d:%d:%d\n", l, utf8len, uni); - prev_uni = uni; - l--; - } else { - int bin_count = 1; - for (int bi = l + 1; bi < len; bi++) { - char c_bi = in[bi]; - // if (c_bi > 0x1F && c_bi != 0x7F) - // break; - if (readUTF8(in, len, bi, &utf8len)) - break; - if (bi < (len - 4) && c_bi == in[bi - 1] && c_bi == in[bi + 1] && c_bi == in[bi + 2] && c_bi == in[bi + 3]) - break; - bin_count++; - } - // printf("Bin:%d:%d:%x:%d\n", l, (unsigned char) c_in, (unsigned char) c_in, bin_count); - SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF8, 5)); - SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, bin_count)); - do { - SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, in[l++], 8)); - } while (--bin_count); - l--; - } - } - } + c_next = 0; + if (l + 1 < len) + c_next = in[l + 1]; - if (need_full_term_codes) { - const int orig_ol = ol; - SAFE_APPEND_BITS2(rawolen, ol = append_final_bits(out, olen, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens)); - return (ol / 8) * 4 + (((ol - orig_ol) / 8) & 3); - } else { - const int rst = (ol + 7) / 8; - append_final_bits(out, rst, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens); - return rst; - } + if (c_in >= 32 && c_in <= 126) { + if (is_upper && !is_all_upper) { + for (ll = l + 4; ll >= l && ll < len; ll--) { + if (in[ll] < 'A' || in[ll] > 'Z') + break; + } + if (ll == l - 1) { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + state = USX_ALPHA; + is_all_upper = 1; + } + } + if (state == USX_DELTA && (c_in == ' ' || c_in == '.' || c_in == ',')) { + uint8_t spl_code = (c_in == ',' ? 0xC0 : (c_in == '.' ? 0xE0 : (c_in == ' ' ? 0 : 0xFF))); + if (spl_code != 0xFF) { + uint8_t spl_code_len = (c_in == ',' ? 3 : (c_in == '.' ? 4 : (c_in == ' ' ? 1 : 4))); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, spl_code, spl_code_len)); + continue; + } + } + c_in -= 32; + if (is_all_upper && is_upper) + c_in += 32; + if (c_in == 0) { + if (state == USX_NUM) + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[NUM_SPC_CODE & 0x1F], + usx_vcode_lens[NUM_SPC_CODE & 0x1F])); + else + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); + } else { + c_in--; + SAFE_APPEND_BITS2(rawolen, + ol = append_code(out, olen, ol, usx_code_94[(int)c_in], &state, usx_hcodes, usx_hcode_lens)); + } + } else if (c_in == 13 && c_next == 10) { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CRLF_CODE, &state, usx_hcodes, usx_hcode_lens)); + l++; + } else if (c_in == 10) { + if (state == USX_DELTA) { + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, UNI_STATE_SPL_CODE, UNI_STATE_SPL_CODE_LEN)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF0, 4)); + } else + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, LF_CODE, &state, usx_hcodes, usx_hcode_lens)); + } else if (c_in == 13) { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, CR_CODE, &state, usx_hcodes, usx_hcode_lens)); + } else if (c_in == '\t') { + SAFE_APPEND_BITS2(rawolen, ol = append_code(out, olen, ol, TAB_CODE, &state, usx_hcodes, usx_hcode_lens)); + } else { + int utf8len; + int32_t uni = readUTF8(in, len, l, &utf8len); + if (uni) { + l += utf8len; + if (state != USX_DELTA) { + int32_t uni2 = readUTF8(in, len, l, &utf8len); + if (uni2) { + if (state != USX_ALPHA) { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, + ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + } + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, + ol = append_bits(out, olen, ol, usx_hcodes[USX_ALPHA], usx_hcode_lens[USX_ALPHA])); + SAFE_APPEND_BITS2( + rawolen, ol = append_bits(out, olen, ol, usx_vcodes[1], usx_vcode_lens[1])); // code for space (' ') + state = USX_DELTA; + } else { + SAFE_APPEND_BITS2(rawolen, ol = append_switch_code(out, olen, ol, state)); + SAFE_APPEND_BITS2(rawolen, + ol = append_bits(out, olen, ol, usx_hcodes[USX_DELTA], usx_hcode_lens[USX_DELTA])); + } + } + SAFE_APPEND_BITS2(rawolen, ol = encodeUnicode(out, olen, ol, uni, prev_uni)); + // printf("%d:%d:%d\n", l, utf8len, uni); + prev_uni = uni; + l--; + } else { + int bin_count = 1; + for (int bi = l + 1; bi < len; bi++) { + char c_bi = in[bi]; + // if (c_bi > 0x1F && c_bi != 0x7F) + // break; + if (readUTF8(in, len, bi, &utf8len)) + break; + if (bi < (len - 4) && c_bi == in[bi - 1] && c_bi == in[bi + 1] && c_bi == in[bi + 2] && c_bi == in[bi + 3]) + break; + bin_count++; + } + // printf("Bin:%d:%d:%x:%d\n", l, (unsigned char) c_in, (unsigned char) c_in, bin_count); + SAFE_APPEND_BITS2(rawolen, ol = append_nibble_escape(out, olen, ol, state, usx_hcodes, usx_hcode_lens)); + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, 0xF8, 5)); + SAFE_APPEND_BITS2(rawolen, ol = encodeCount(out, olen, ol, bin_count)); + do { + SAFE_APPEND_BITS2(rawolen, ol = append_bits(out, olen, ol, in[l++], 8)); + } while (--bin_count); + l--; + } + } + } + + if (need_full_term_codes) { + const int orig_ol = ol; + SAFE_APPEND_BITS2(rawolen, ol = append_final_bits(out, olen, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens)); + return (ol / 8) * 4 + (((ol - orig_ol) / 8) & 3); + } else { + const int rst = (ol + 7) / 8; + append_final_bits(out, rst, ol, state, is_all_upper, usx_hcodes, usx_hcode_lens); + return rst; + } } // Main API function. See unishox2.h for documentation int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) { - return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, usx_templates, NULL); + const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) +{ + return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, + usx_templates, NULL); } // Main API function. See unishox2.h for documentation -int unishox2_compress_simple(const char *in, int len, char *out) { - return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_DFLT, - USX_TEMPLATES, NULL); +int unishox2_compress_simple(const char *in, int len, char *out) +{ + return unishox2_compress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, + USX_FREQ_SEQ_DFLT, USX_TEMPLATES, NULL); } // Reads one bit from in -int readBit(const char *in, int bit_no) { return in[bit_no >> 3] & (0x80 >> (bit_no % 8)); } +int readBit(const char *in, int bit_no) +{ + return in[bit_no >> 3] & (0x80 >> (bit_no % 8)); +} // Reads next 8 bits, if available -int read8bitCode(const char *in, int len, int bit_no) { - int bit_pos = bit_no & 0x07; - int char_pos = bit_no >> 3; - len >>= 3; - uint8_t code = (((uint8_t)in[char_pos]) << bit_pos); - char_pos++; - if (char_pos < len) { - code |= ((uint8_t)in[char_pos]) >> (8 - bit_pos); - } else - code |= (0xFF >> (8 - bit_pos)); - return code; +int read8bitCode(const char *in, int len, int bit_no) +{ + int bit_pos = bit_no & 0x07; + int char_pos = bit_no >> 3; + len >>= 3; + uint8_t code = (((uint8_t)in[char_pos]) << bit_pos); + char_pos++; + if (char_pos < len) { + code |= ((uint8_t)in[char_pos]) >> (8 - bit_pos); + } else + code |= (0xFF >> (8 - bit_pos)); + return code; } /// The list of veritical codes is split into 5 sections. Used by readVCodeIdx() @@ -887,21 +926,22 @@ uint8_t usx_vcode_lookup[36] = {(1 << 5) + 0, (1 << 5) + 0, (2 << 5) + 1, (2 /// Decoder is designed for using less memory, not speed. \n /// Returns the veritical code index or 99 if match could not be found. \n /// Also updates bit_no_p with how many ever bits used by the vertical code. -int readVCodeIdx(const char *in, int len, int *bit_no_p) { - if (*bit_no_p < len) { - uint8_t code = read8bitCode(in, len, *bit_no_p); - int i = 0; - do { - if (code <= usx_vsections[i]) { - uint8_t vcode = usx_vcode_lookup[usx_vsection_pos[i] + ((code & usx_vsection_mask[i]) >> usx_vsection_shift[i])]; - (*bit_no_p) += ((vcode >> 5) + 1); - if (*bit_no_p > len) - return 99; - return vcode & 0x1F; - } - } while (++i < SECTION_COUNT); - } - return 99; +int readVCodeIdx(const char *in, int len, int *bit_no_p) +{ + if (*bit_no_p < len) { + uint8_t code = read8bitCode(in, len, *bit_no_p); + int i = 0; + do { + if (code <= usx_vsections[i]) { + uint8_t vcode = usx_vcode_lookup[usx_vsection_pos[i] + ((code & usx_vsection_mask[i]) >> usx_vsection_shift[i])]; + (*bit_no_p) += ((vcode >> 5) + 1); + if (*bit_no_p > len) + return 99; + return vcode & 0x1F; + } + } while (++i < SECTION_COUNT); + } + return 99; } /// Mask for retrieving each code to be decoded according to its length \n @@ -911,467 +951,482 @@ uint8_t len_masks[] = {0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF}; /// depending on the hcodes defined using usx_hcodes and usx_hcode_lens \n /// Returns the horizontal code index or 99 if match could not be found. \n /// Also updates bit_no_p with how many ever bits used by the horizontal code. -int readHCodeIdx(const char *in, int len, int *bit_no_p, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { - if (!usx_hcode_lens[USX_ALPHA]) - return USX_ALPHA; - if (*bit_no_p < len) { - uint8_t code = read8bitCode(in, len, *bit_no_p); - for (int code_pos = 0; code_pos < 5; code_pos++) { - if (usx_hcode_lens[code_pos] && (code & len_masks[usx_hcode_lens[code_pos] - 1]) == usx_hcodes[code_pos]) { - *bit_no_p += usx_hcode_lens[code_pos]; - return code_pos; - } +int readHCodeIdx(const char *in, int len, int *bit_no_p, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) +{ + if (!usx_hcode_lens[USX_ALPHA]) + return USX_ALPHA; + if (*bit_no_p < len) { + uint8_t code = read8bitCode(in, len, *bit_no_p); + for (int code_pos = 0; code_pos < 5; code_pos++) { + if (usx_hcode_lens[code_pos] && (code & len_masks[usx_hcode_lens[code_pos] - 1]) == usx_hcodes[code_pos]) { + *bit_no_p += usx_hcode_lens[code_pos]; + return code_pos; + } + } } - } - return 99; + return 99; } // TODO: Last value check.. Also len check in readBit /// Returns the position of step code (0, 10, 110, etc.) encountered in the stream -int getStepCodeIdx(const char *in, int len, int *bit_no_p, int limit) { - int idx = 0; - while (*bit_no_p < len && readBit(in, *bit_no_p)) { - idx++; +int getStepCodeIdx(const char *in, int len, int *bit_no_p, int limit) +{ + int idx = 0; + while (*bit_no_p < len && readBit(in, *bit_no_p)) { + idx++; + (*bit_no_p)++; + if (idx == limit) + return idx; + } + if (*bit_no_p >= len) + return 99; (*bit_no_p)++; - if (idx == limit) - return idx; - } - if (*bit_no_p >= len) - return 99; - (*bit_no_p)++; - return idx; + return idx; } /// Reads specified number of bits and builds the corresponding integer -int32_t getNumFromBits(const char *in, int len, int bit_no, int count) { - int32_t ret = 0; - while (count-- && bit_no < len) { - ret += (readBit(in, bit_no) ? 1 << count : 0); - bit_no++; - } - return count < 0 ? ret : -1; +int32_t getNumFromBits(const char *in, int len, int bit_no, int count) +{ + int32_t ret = 0; + while (count-- && bit_no < len) { + ret += (readBit(in, bit_no) ? 1 << count : 0); + bit_no++; + } + return count < 0 ? ret : -1; } /// Decodes the count from the given bit stream at in. Also updates bit_no_p -int32_t readCount(const char *in, int *bit_no_p, int len) { - int idx = getStepCodeIdx(in, len, bit_no_p, 4); - if (idx == 99) - return -1; - if (*bit_no_p + count_bit_lens[idx] - 1 >= len) - return -1; - int32_t count = getNumFromBits(in, len, *bit_no_p, count_bit_lens[idx]) + (idx ? count_adder[idx - 1] : 0); - (*bit_no_p) += count_bit_lens[idx]; - return count; +int32_t readCount(const char *in, int *bit_no_p, int len) +{ + int idx = getStepCodeIdx(in, len, bit_no_p, 4); + if (idx == 99) + return -1; + if (*bit_no_p + count_bit_lens[idx] - 1 >= len) + return -1; + int32_t count = getNumFromBits(in, len, *bit_no_p, count_bit_lens[idx]) + (idx ? count_adder[idx - 1] : 0); + (*bit_no_p) += count_bit_lens[idx]; + return count; } /// Decodes the Unicode codepoint from the given bit stream at in. Also updates bit_no_p \n /// When the step code is 5, reads the next step code to find out the special code. -int32_t readUnicode(const char *in, int *bit_no_p, int len) { - int idx = getStepCodeIdx(in, len, bit_no_p, 5); - if (idx == 99) - return 0x7FFFFF00 + 99; - if (idx == 5) { - idx = getStepCodeIdx(in, len, bit_no_p, 4); - return 0x7FFFFF00 + idx; - } - if (idx >= 0) { - int sign = (*bit_no_p < len ? readBit(in, *bit_no_p) : 0); - (*bit_no_p)++; - if (*bit_no_p + uni_bit_len[idx] - 1 >= len) - return 0x7FFFFF00 + 99; - int32_t count = getNumFromBits(in, len, *bit_no_p, uni_bit_len[idx]); - count += uni_adder[idx]; - (*bit_no_p) += uni_bit_len[idx]; - // printf("Sign: %d, Val:%d", sign, count); - return sign ? -count : count; - } - return 0; +int32_t readUnicode(const char *in, int *bit_no_p, int len) +{ + int idx = getStepCodeIdx(in, len, bit_no_p, 5); + if (idx == 99) + return 0x7FFFFF00 + 99; + if (idx == 5) { + idx = getStepCodeIdx(in, len, bit_no_p, 4); + return 0x7FFFFF00 + idx; + } + if (idx >= 0) { + int sign = (*bit_no_p < len ? readBit(in, *bit_no_p) : 0); + (*bit_no_p)++; + if (*bit_no_p + uni_bit_len[idx] - 1 >= len) + return 0x7FFFFF00 + 99; + int32_t count = getNumFromBits(in, len, *bit_no_p, uni_bit_len[idx]); + count += uni_adder[idx]; + (*bit_no_p) += uni_bit_len[idx]; + // printf("Sign: %d, Val:%d", sign, count); + return sign ? -count : count; + } + return 0; } /// Macro to ensure that the decoder does not append more than olen bytes to out -#define DEC_OUTPUT_CHAR(out, olen, ol, c) \ - do { \ - char *const obuf = (out); \ - const int oidx = (ol); \ - const int limit = (olen); \ - if (limit <= oidx) \ - return limit + 1; \ - else if (oidx < 0) \ - return 0; \ - else \ - obuf[oidx] = (c); \ - } while (0) +#define DEC_OUTPUT_CHAR(out, olen, ol, c) \ + do { \ + char *const obuf = (out); \ + const int oidx = (ol); \ + const int limit = (olen); \ + if (limit <= oidx) \ + return limit + 1; \ + else if (oidx < 0) \ + return 0; \ + else \ + obuf[oidx] = (c); \ + } while (0) /// Macro to ensure that the decoder does not append more than olen bytes to out -#define DEC_OUTPUT_CHARS(olen, exp) \ - do { \ - const int newidx = (exp); \ - const int limit = (olen); \ - if (newidx > limit) \ - return limit + 1; \ - } while (0) +#define DEC_OUTPUT_CHARS(olen, exp) \ + do { \ + const int newidx = (exp); \ + const int limit = (olen); \ + if (newidx > limit) \ + return limit + 1; \ + } while (0) /// Write given unicode code point to out as a UTF-8 sequence -int writeUTF8(char *out, int olen, int ol, int uni) { - if (uni < (1 << 11)) { - DEC_OUTPUT_CHAR(out, olen, ol++, 0xC0 + (uni >> 6)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); - } else if (uni < (1 << 16)) { - DEC_OUTPUT_CHAR(out, olen, ol++, 0xE0 + (uni >> 12)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); - } else { - DEC_OUTPUT_CHAR(out, olen, ol++, 0xF0 + (uni >> 18)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 12) & 0x3F)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); - DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); - } - return ol; +int writeUTF8(char *out, int olen, int ol, int uni) +{ + if (uni < (1 << 11)) { + DEC_OUTPUT_CHAR(out, olen, ol++, 0xC0 + (uni >> 6)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); + } else if (uni < (1 << 16)) { + DEC_OUTPUT_CHAR(out, olen, ol++, 0xE0 + (uni >> 12)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); + } else { + DEC_OUTPUT_CHAR(out, olen, ol++, 0xF0 + (uni >> 18)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 12) & 0x3F)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + ((uni >> 6) & 0x3F)); + DEC_OUTPUT_CHAR(out, olen, ol++, 0x80 + (uni & 0x3F)); + } + return ol; } /// Decode repeating sequence and appends to out -int decodeRepeat(const char *in, int len, char *out, int olen, int ol, int *bit_no, struct us_lnk_lst *prev_lines) { - if (prev_lines) { - int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; - if (dict_len < NICE_LEN) - return -1; - int32_t dist = readCount(in, bit_no, len); - if (dist < 0) - return -1; - int32_t ctx = readCount(in, bit_no, len); - if (ctx < 0) - return -1; - struct us_lnk_lst *cur_line = prev_lines; - const int left = olen - ol; - while (ctx-- && cur_line) - cur_line = cur_line->previous; - if (cur_line == NULL) - return -1; - if (left <= 0) - return olen + 1; - if ((size_t)dist >= strlen(cur_line->data)) - return -1; - memmove(out + ol, cur_line->data + dist, min_of(left, dict_len)); - if (left < dict_len) - return olen + 1; - ol += dict_len; - } else { - int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; - if (dict_len < NICE_LEN) - return -1; - int32_t dist = readCount(in, bit_no, len) + NICE_LEN - 1; - if (dist < NICE_LEN - 1) - return -1; - const int32_t left = olen - ol; - // printf("Decode len: %d, dist: %d\n", dict_len - NICE_LEN, dist - NICE_LEN + 1); - if (left <= 0) - return olen + 1; - if (ol - dist < 0) - return -1; - memmove(out + ol, out + ol - dist, min_of(left, dict_len)); - if (left < dict_len) - return olen + 1; - ol += dict_len; - } - return ol; +int decodeRepeat(const char *in, int len, char *out, int olen, int ol, int *bit_no, struct us_lnk_lst *prev_lines) +{ + if (prev_lines) { + int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; + if (dict_len < NICE_LEN) + return -1; + int32_t dist = readCount(in, bit_no, len); + if (dist < 0) + return -1; + int32_t ctx = readCount(in, bit_no, len); + if (ctx < 0) + return -1; + struct us_lnk_lst *cur_line = prev_lines; + const int left = olen - ol; + while (ctx-- && cur_line) + cur_line = cur_line->previous; + if (cur_line == NULL) + return -1; + if (left <= 0) + return olen + 1; + if ((size_t)dist >= strlen(cur_line->data)) + return -1; + memmove(out + ol, cur_line->data + dist, min_of(left, dict_len)); + if (left < dict_len) + return olen + 1; + ol += dict_len; + } else { + int32_t dict_len = readCount(in, bit_no, len) + NICE_LEN; + if (dict_len < NICE_LEN) + return -1; + int32_t dist = readCount(in, bit_no, len) + NICE_LEN - 1; + if (dist < NICE_LEN - 1) + return -1; + const int32_t left = olen - ol; + // printf("Decode len: %d, dist: %d\n", dict_len - NICE_LEN, dist - NICE_LEN + 1); + if (left <= 0) + return olen + 1; + if (ol - dist < 0) + return -1; + memmove(out + ol, out + ol - dist, min_of(left, dict_len)); + if (left < dict_len) + return olen + 1; + ol += dict_len; + } + return ol; } /// Returns hex character corresponding to the 4 bit nibble -char getHexChar(int32_t nibble, int hex_type) { - if (nibble >= 0 && nibble <= 9) - return '0' + nibble; - else if (hex_type < USX_NIB_HEX_UPPER) - return 'a' + nibble - 10; - return 'A' + nibble - 10; +char getHexChar(int32_t nibble, int hex_type) +{ + if (nibble >= 0 && nibble <= 9) + return '0' + nibble; + else if (hex_type < USX_NIB_HEX_UPPER) + return 'a' + nibble - 10; + return 'A' + nibble - 10; } // Main API function. See unishox2.h for documentation int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], - struct us_lnk_lst *prev_lines) { + struct us_lnk_lst *prev_lines) +{ - int dstate; - int bit_no; - int h, v; - uint8_t is_all_upper; + int dstate; + int bit_no; + int h, v; + uint8_t is_all_upper; #if (UNISHOX_API_OUT_AND_LEN(0, 1)) == 0 - const int olen = INT_MAX - 1; + const int olen = INT_MAX - 1; #endif - init_coder(); - int ol = 0; - bit_no = UNISHOX_MAGIC_BIT_LEN; // ignore the magic bit - dstate = h = USX_ALPHA; - is_all_upper = 0; + init_coder(); + int ol = 0; + bit_no = UNISHOX_MAGIC_BIT_LEN; // ignore the magic bit + dstate = h = USX_ALPHA; + is_all_upper = 0; - int prev_uni = 0; + int prev_uni = 0; - len <<= 3; - while (bit_no < len) { - int orig_bit_no = bit_no; - if (dstate == USX_DELTA || h == USX_DELTA) { - if (dstate != USX_DELTA) - h = dstate; - int32_t delta = readUnicode(in, &bit_no, len); - if ((delta >> 8) == 0x7FFFFF) { - int spl_code_idx = delta & 0x000000FF; - if (spl_code_idx == 99) - break; - switch (spl_code_idx) { - case 0: - DEC_OUTPUT_CHAR(out, olen, ol++, ' '); - continue; - case 1: - h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); - if (h == 99) { - bit_no = len; - continue; - } - if (h == USX_DELTA || h == USX_ALPHA) { - dstate = h; - continue; - } - if (h == USX_DICT) { - int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); - if (rpt_ret < 0) - return ol; // if we break here it will only break out of switch - DEC_OUTPUT_CHARS(olen, ol = rpt_ret); + len <<= 3; + while (bit_no < len) { + int orig_bit_no = bit_no; + if (dstate == USX_DELTA || h == USX_DELTA) { + if (dstate != USX_DELTA) + h = dstate; + int32_t delta = readUnicode(in, &bit_no, len); + if ((delta >> 8) == 0x7FFFFF) { + int spl_code_idx = delta & 0x000000FF; + if (spl_code_idx == 99) + break; + switch (spl_code_idx) { + case 0: + DEC_OUTPUT_CHAR(out, olen, ol++, ' '); + continue; + case 1: + h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); + if (h == 99) { + bit_no = len; + continue; + } + if (h == USX_DELTA || h == USX_ALPHA) { + dstate = h; + continue; + } + if (h == USX_DICT) { + int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); + if (rpt_ret < 0) + return ol; // if we break here it will only break out of switch + DEC_OUTPUT_CHARS(olen, ol = rpt_ret); + h = dstate; + continue; + } + break; + case 2: + DEC_OUTPUT_CHAR(out, olen, ol++, ','); + continue; + case 3: + DEC_OUTPUT_CHAR(out, olen, ol++, '.'); + continue; + case 4: + DEC_OUTPUT_CHAR(out, olen, ol++, 10); + continue; + } + } else { + prev_uni += delta; + DEC_OUTPUT_CHARS(olen, ol = writeUTF8(out, olen, ol, prev_uni)); + // printf("%ld, ", prev_uni); + } + if (dstate == USX_DELTA && h == USX_DELTA) + continue; + } else h = dstate; - continue; - } - break; - case 2: - DEC_OUTPUT_CHAR(out, olen, ol++, ','); - continue; - case 3: - DEC_OUTPUT_CHAR(out, olen, ol++, '.'); - continue; - case 4: - DEC_OUTPUT_CHAR(out, olen, ol++, 10); - continue; - } - } else { - prev_uni += delta; - DEC_OUTPUT_CHARS(olen, ol = writeUTF8(out, olen, ol, prev_uni)); - // printf("%ld, ", prev_uni); - } - if (dstate == USX_DELTA && h == USX_DELTA) - continue; - } else - h = dstate; - char c = 0; - uint8_t is_upper = is_all_upper; - v = readVCodeIdx(in, len, &bit_no); - if (v == 99 || h == 99) { - bit_no = orig_bit_no; - break; - } - if (v == 0 && h != USX_SYM) { - if (bit_no >= len) - break; - if (h != USX_NUM || dstate != USX_DELTA) { - h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); - if (h == 99 || bit_no >= len) { - bit_no = orig_bit_no; - break; - } - } - if (h == USX_ALPHA) { - if (dstate == USX_ALPHA) { - if (!usx_hcode_lens[USX_ALPHA] && - TERM_BYTE_PRESET_1 == (read8bitCode(in, len, bit_no - SW_CODE_LEN) & - (0xFF << (8 - (is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER))))) - break; // Terminator for preset 1 - if (is_all_upper) { - is_upper = is_all_upper = 0; - continue; - } - v = readVCodeIdx(in, len, &bit_no); - if (v == 99) { + char c = 0; + uint8_t is_upper = is_all_upper; + v = readVCodeIdx(in, len, &bit_no); + if (v == 99 || h == 99) { bit_no = orig_bit_no; break; - } - if (v == 0) { - h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); - if (h == 99) { - bit_no = orig_bit_no; - break; + } + if (v == 0 && h != USX_SYM) { + if (bit_no >= len) + break; + if (h != USX_NUM || dstate != USX_DELTA) { + h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); + if (h == 99 || bit_no >= len) { + bit_no = orig_bit_no; + break; + } } if (h == USX_ALPHA) { - is_all_upper = 1; - continue; - } - } - is_upper = 1; - } else { - dstate = USX_ALPHA; - continue; - } - } else if (h == USX_DICT) { - int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); - if (rpt_ret < 0) - break; - DEC_OUTPUT_CHARS(olen, ol = rpt_ret); - continue; - } else if (h == USX_DELTA) { - // printf("Sign: %d, bitno: %d\n", sign, bit_no); - // printf("Code: %d\n", prev_uni); - // printf("BitNo: %d\n", bit_no); - continue; - } else { - if (h != USX_NUM || dstate != USX_DELTA) - v = readVCodeIdx(in, len, &bit_no); - if (v == 99) { - bit_no = orig_bit_no; - break; - } - if (h == USX_NUM && v == 0) { - int idx = getStepCodeIdx(in, len, &bit_no, 5); - if (idx == 99) - break; - if (idx == 0) { - idx = getStepCodeIdx(in, len, &bit_no, 4); - if (idx >= 5) - break; - int32_t rem = readCount(in, &bit_no, len); - if (rem < 0) - break; - if (usx_templates[idx] == NULL) - break; - size_t tlen = strlen(usx_templates[idx]); - if ((size_t)rem > tlen) - break; - rem = tlen - rem; - int eof = 0; - for (int j = 0; j < rem; j++) { - char c_t = usx_templates[idx][j]; - if (c_t == 'f' || c_t == 'r' || c_t == 't' || c_t == 'o' || c_t == 'F') { - char nibble_len = (c_t == 'f' || c_t == 'F' ? 4 : (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1))); - const int32_t raw_char = getNumFromBits(in, len, bit_no, nibble_len); - if (raw_char < 0) { - eof = 1; - break; + if (dstate == USX_ALPHA) { + if (!usx_hcode_lens[USX_ALPHA] && + TERM_BYTE_PRESET_1 == + (read8bitCode(in, len, bit_no - SW_CODE_LEN) & + (0xFF << (8 - (is_all_upper ? TERM_BYTE_PRESET_1_LEN_UPPER : TERM_BYTE_PRESET_1_LEN_LOWER))))) + break; // Terminator for preset 1 + if (is_all_upper) { + is_upper = is_all_upper = 0; + continue; + } + v = readVCodeIdx(in, len, &bit_no); + if (v == 99) { + bit_no = orig_bit_no; + break; + } + if (v == 0) { + h = readHCodeIdx(in, len, &bit_no, usx_hcodes, usx_hcode_lens); + if (h == 99) { + bit_no = orig_bit_no; + break; + } + if (h == USX_ALPHA) { + is_all_upper = 1; + continue; + } + } + is_upper = 1; + } else { + dstate = USX_ALPHA; + continue; + } + } else if (h == USX_DICT) { + int rpt_ret = decodeRepeat(in, len, out, olen, ol, &bit_no, prev_lines); + if (rpt_ret < 0) + break; + DEC_OUTPUT_CHARS(olen, ol = rpt_ret); + continue; + } else if (h == USX_DELTA) { + // printf("Sign: %d, bitno: %d\n", sign, bit_no); + // printf("Code: %d\n", prev_uni); + // printf("BitNo: %d\n", bit_no); + continue; + } else { + if (h != USX_NUM || dstate != USX_DELTA) + v = readVCodeIdx(in, len, &bit_no); + if (v == 99) { + bit_no = orig_bit_no; + break; + } + if (h == USX_NUM && v == 0) { + int idx = getStepCodeIdx(in, len, &bit_no, 5); + if (idx == 99) + break; + if (idx == 0) { + idx = getStepCodeIdx(in, len, &bit_no, 4); + if (idx >= 5) + break; + int32_t rem = readCount(in, &bit_no, len); + if (rem < 0) + break; + if (usx_templates[idx] == NULL) + break; + size_t tlen = strlen(usx_templates[idx]); + if ((size_t)rem > tlen) + break; + rem = tlen - rem; + int eof = 0; + for (int j = 0; j < rem; j++) { + char c_t = usx_templates[idx][j]; + if (c_t == 'f' || c_t == 'r' || c_t == 't' || c_t == 'o' || c_t == 'F') { + char nibble_len = (c_t == 'f' || c_t == 'F' ? 4 : (c_t == 'r' ? 3 : (c_t == 't' ? 2 : 1))); + const int32_t raw_char = getNumFromBits(in, len, bit_no, nibble_len); + if (raw_char < 0) { + eof = 1; + break; + } + DEC_OUTPUT_CHAR(out, olen, ol++, + getHexChar((char)raw_char, c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); + bit_no += nibble_len; + } else + DEC_OUTPUT_CHAR(out, olen, ol++, c_t); + } + if (eof) + break; // reach input eof + } else if (idx == 5) { + int32_t bin_count = readCount(in, &bit_no, len); + if (bin_count < 0) + break; + if (bin_count == 0) // invalid encoding + break; + do { + const int32_t raw_char = getNumFromBits(in, len, bit_no, 8); + if (raw_char < 0) + break; + DEC_OUTPUT_CHAR(out, olen, ol++, (char)raw_char); + bit_no += 8; + } while (--bin_count); + if (bin_count > 0) + break; // reach input eof + } else { + int32_t nibble_count = 0; + if (idx == 2 || idx == 4) + nibble_count = 32; + else { + nibble_count = readCount(in, &bit_no, len); + if (nibble_count < 0) + break; + if (nibble_count == 0) // invalid encoding + break; + } + do { + int32_t nibble = getNumFromBits(in, len, bit_no, 4); + if (nibble < 0) + break; + DEC_OUTPUT_CHAR(out, olen, ol++, getHexChar(nibble, idx < 3 ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); + if ((idx == 2 || idx == 4) && + (nibble_count == 25 || nibble_count == 21 || nibble_count == 17 || nibble_count == 13)) + DEC_OUTPUT_CHAR(out, olen, ol++, '-'); + bit_no += 4; + } while (--nibble_count); + if (nibble_count > 0) + break; // reach input eof + } + if (dstate == USX_DELTA) + h = USX_DELTA; + continue; } - DEC_OUTPUT_CHAR(out, olen, ol++, getHexChar((char)raw_char, c_t == 'f' ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); - bit_no += nibble_len; - } else - DEC_OUTPUT_CHAR(out, olen, ol++, c_t); } - if (eof) - break; // reach input eof - } else if (idx == 5) { - int32_t bin_count = readCount(in, &bit_no, len); - if (bin_count < 0) - break; - if (bin_count == 0) // invalid encoding - break; - do { - const int32_t raw_char = getNumFromBits(in, len, bit_no, 8); - if (raw_char < 0) - break; - DEC_OUTPUT_CHAR(out, olen, ol++, (char)raw_char); - bit_no += 8; - } while (--bin_count); - if (bin_count > 0) - break; // reach input eof - } else { - int32_t nibble_count = 0; - if (idx == 2 || idx == 4) - nibble_count = 32; - else { - nibble_count = readCount(in, &bit_no, len); - if (nibble_count < 0) - break; - if (nibble_count == 0) // invalid encoding - break; - } - do { - int32_t nibble = getNumFromBits(in, len, bit_no, 4); - if (nibble < 0) - break; - DEC_OUTPUT_CHAR(out, olen, ol++, getHexChar(nibble, idx < 3 ? USX_NIB_HEX_LOWER : USX_NIB_HEX_UPPER)); - if ((idx == 2 || idx == 4) && (nibble_count == 25 || nibble_count == 21 || nibble_count == 17 || nibble_count == 13)) - DEC_OUTPUT_CHAR(out, olen, ol++, '-'); - bit_no += 4; - } while (--nibble_count); - if (nibble_count > 0) - break; // reach input eof - } - if (dstate == USX_DELTA) - h = USX_DELTA; - continue; } - } - } - if (is_upper && v == 1) { - h = dstate = USX_DELTA; // continuous delta coding - continue; - } - if (h < 3 && v < 28) - c = usx_sets[h][v]; - if (c >= 'a' && c <= 'z') { - dstate = USX_ALPHA; - if (is_upper) - c -= 32; - } else { - if (c >= '0' && c <= '9') { - dstate = USX_NUM; - } else if (c == 0) { - if (v == 8) { - DEC_OUTPUT_CHAR(out, olen, ol++, '\r'); - DEC_OUTPUT_CHAR(out, olen, ol++, '\n'); - } else if (h == USX_NUM && v == 26) { - int32_t count = readCount(in, &bit_no, len); - if (count < 0) - break; - count += 4; - if (ol <= 0) - return 0; // invalid encoding - char rpt_c = out[ol - 1]; - while (count--) - DEC_OUTPUT_CHAR(out, olen, ol++, rpt_c); - } else if (h == USX_SYM && v > 24) { - v -= 25; - const int freqlen = (int)strlen(usx_freq_seq[v]); - const int left = olen - ol; - if (left <= 0) - return olen + 1; - memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); - if (left < freqlen) - return olen + 1; - ol += freqlen; - } else if (h == USX_NUM && v > 22 && v < 26) { - v -= (23 - 3); - const int freqlen = (int)strlen(usx_freq_seq[v]); - const int left = olen - ol; - if (left <= 0) - return olen + 1; - memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); - if (left < freqlen) - return olen + 1; - ol += freqlen; - } else - break; // Terminator + if (is_upper && v == 1) { + h = dstate = USX_DELTA; // continuous delta coding + continue; + } + if (h < 3 && v < 28) + c = usx_sets[h][v]; + if (c >= 'a' && c <= 'z') { + dstate = USX_ALPHA; + if (is_upper) + c -= 32; + } else { + if (c >= '0' && c <= '9') { + dstate = USX_NUM; + } else if (c == 0) { + if (v == 8) { + DEC_OUTPUT_CHAR(out, olen, ol++, '\r'); + DEC_OUTPUT_CHAR(out, olen, ol++, '\n'); + } else if (h == USX_NUM && v == 26) { + int32_t count = readCount(in, &bit_no, len); + if (count < 0) + break; + count += 4; + if (ol <= 0) + return 0; // invalid encoding + char rpt_c = out[ol - 1]; + while (count--) + DEC_OUTPUT_CHAR(out, olen, ol++, rpt_c); + } else if (h == USX_SYM && v > 24) { + v -= 25; + const int freqlen = (int)strlen(usx_freq_seq[v]); + const int left = olen - ol; + if (left <= 0) + return olen + 1; + memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); + if (left < freqlen) + return olen + 1; + ol += freqlen; + } else if (h == USX_NUM && v > 22 && v < 26) { + v -= (23 - 3); + const int freqlen = (int)strlen(usx_freq_seq[v]); + const int left = olen - ol; + if (left <= 0) + return olen + 1; + memcpy(out + ol, usx_freq_seq[v], min_of(left, freqlen)); + if (left < freqlen) + return olen + 1; + ol += freqlen; + } else + break; // Terminator + if (dstate == USX_DELTA) + h = USX_DELTA; + continue; + } + } if (dstate == USX_DELTA) - h = USX_DELTA; - continue; - } + h = USX_DELTA; + DEC_OUTPUT_CHAR(out, olen, ol++, c); } - if (dstate == USX_DELTA) - h = USX_DELTA; - DEC_OUTPUT_CHAR(out, olen, ol++, c); - } - return ol; + return ol; } // Main API function. See unishox2.h for documentation int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const uint8_t usx_hcodes[], - const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) { - return unishox2_decompress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, usx_templates, NULL); + const uint8_t usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]) +{ + return unishox2_decompress_lines(in, len, UNISHOX_API_OUT_AND_LEN(out, olen), usx_hcodes, usx_hcode_lens, usx_freq_seq, + usx_templates, NULL); } // Main API function. See unishox2.h for documentation -int unishox2_decompress_simple(const char *in, int len, char *out) { - return unishox2_decompress(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_PSET_DFLT); +int unishox2_decompress_simple(const char *in, int len, char *out) +{ + return unishox2_decompress(in, len, UNISHOX_API_OUT_AND_LEN(out, INT_MAX - 1), USX_PSET_DFLT); } \ No newline at end of file diff --git a/src/mesh/compression/unishox2.h b/src/mesh/compression/unishox2.h index 3e37e7a79..823128f02 100644 --- a/src/mesh/compression/unishox2.h +++ b/src/mesh/compression/unishox2.h @@ -59,79 +59,139 @@ // enum {USX_ALPHA = 0, USX_SYM, USX_NUM, USX_DICT, USX_DELTA}; -/// Default Horizontal codes. When composition of text is know beforehand, the other hcodes in this section can be used -/// to achieve more compression. -#define USX_HCODES_DFLT \ - (const unsigned char[]) { 0x00, 0x40, 0x80, 0xC0, 0xE0 } +/// Default Horizontal codes. When composition of text is know beforehand, the other hcodes in this section can be used to achieve +/// more compression. +#define USX_HCODES_DFLT \ + (const unsigned char[]) \ + { \ + 0x00, 0x40, 0x80, 0xC0, 0xE0 \ + } /// Length of each default hcode -#define USX_HCODE_LENS_DFLT \ - (const unsigned char[]) { 2, 2, 2, 3, 3 } +#define USX_HCODE_LENS_DFLT \ + (const unsigned char[]) \ + { \ + 2, 2, 2, 3, 3 \ + } /// Horizontal codes preset for English Alphabet content only -#define USX_HCODES_ALPHA_ONLY \ - (const unsigned char[]) { 0x00, 0x00, 0x00, 0x00, 0x00 } +#define USX_HCODES_ALPHA_ONLY \ + (const unsigned char[]) \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00 \ + } /// Length of each Alpha only hcode -#define USX_HCODE_LENS_ALPHA_ONLY \ - (const unsigned char[]) { 0, 0, 0, 0, 0 } +#define USX_HCODE_LENS_ALPHA_ONLY \ + (const unsigned char[]) \ + { \ + 0, 0, 0, 0, 0 \ + } /// Horizontal codes preset for Alpha Numeric content only -#define USX_HCODES_ALPHA_NUM_ONLY \ - (const unsigned char[]) { 0x00, 0x00, 0x80, 0x00, 0x00 } +#define USX_HCODES_ALPHA_NUM_ONLY \ + (const unsigned char[]) \ + { \ + 0x00, 0x00, 0x80, 0x00, 0x00 \ + } /// Length of each Alpha numeric hcode -#define USX_HCODE_LENS_ALPHA_NUM_ONLY \ - (const unsigned char[]) { 1, 0, 1, 0, 0 } +#define USX_HCODE_LENS_ALPHA_NUM_ONLY \ + (const unsigned char[]) \ + { \ + 1, 0, 1, 0, 0 \ + } /// Horizontal codes preset for Alpha Numeric and Symbol content only -#define USX_HCODES_ALPHA_NUM_SYM_ONLY \ - (const unsigned char[]) { 0x00, 0x80, 0xC0, 0x00, 0x00 } +#define USX_HCODES_ALPHA_NUM_SYM_ONLY \ + (const unsigned char[]) \ + { \ + 0x00, 0x80, 0xC0, 0x00, 0x00 \ + } /// Length of each Alpha numeric and symbol hcodes -#define USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY \ - (const unsigned char[]) { 1, 2, 2, 0, 0 } +#define USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY \ + (const unsigned char[]) \ + { \ + 1, 2, 2, 0, 0 \ + } /// Horizontal codes preset favouring Alphabet content -#define USX_HCODES_FAVOR_ALPHA \ - (const unsigned char[]) { 0x00, 0x80, 0xA0, 0xC0, 0xE0 } +#define USX_HCODES_FAVOR_ALPHA \ + (const unsigned char[]) \ + { \ + 0x00, 0x80, 0xA0, 0xC0, 0xE0 \ + } /// Length of each hcode favouring Alpha content -#define USX_HCODE_LENS_FAVOR_ALPHA \ - (const unsigned char[]) { 1, 3, 3, 3, 3 } +#define USX_HCODE_LENS_FAVOR_ALPHA \ + (const unsigned char[]) \ + { \ + 1, 3, 3, 3, 3 \ + } /// Horizontal codes preset favouring repeating sequences -#define USX_HCODES_FAVOR_DICT \ - (const unsigned char[]) { 0x00, 0x40, 0xC0, 0x80, 0xE0 } +#define USX_HCODES_FAVOR_DICT \ + (const unsigned char[]) \ + { \ + 0x00, 0x40, 0xC0, 0x80, 0xE0 \ + } /// Length of each hcode favouring repeating sequences -#define USX_HCODE_LENS_FAVOR_DICT \ - (const unsigned char[]) { 2, 2, 3, 2, 3 } +#define USX_HCODE_LENS_FAVOR_DICT \ + (const unsigned char[]) \ + { \ + 2, 2, 3, 2, 3 \ + } /// Horizontal codes preset favouring symbols -#define USX_HCODES_FAVOR_SYM \ - (const unsigned char[]) { 0x80, 0x00, 0xA0, 0xC0, 0xE0 } +#define USX_HCODES_FAVOR_SYM \ + (const unsigned char[]) \ + { \ + 0x80, 0x00, 0xA0, 0xC0, 0xE0 \ + } /// Length of each hcode favouring symbols -#define USX_HCODE_LENS_FAVOR_SYM \ - (const unsigned char[]) { 3, 1, 3, 3, 3 } +#define USX_HCODE_LENS_FAVOR_SYM \ + (const unsigned char[]) \ + { \ + 3, 1, 3, 3, 3 \ + } // #define USX_HCODES_FAVOR_UMLAUT {0x00, 0x40, 0xE0, 0xC0, 0x80} // #define USX_HCODE_LENS_FAVOR_UMLAUT {2, 2, 3, 3, 2} /// Horizontal codes preset favouring umlaut letters -#define USX_HCODES_FAVOR_UMLAUT \ - (const unsigned char[]) { 0x80, 0xA0, 0xC0, 0xE0, 0x00 } +#define USX_HCODES_FAVOR_UMLAUT \ + (const unsigned char[]) \ + { \ + 0x80, 0xA0, 0xC0, 0xE0, 0x00 \ + } /// Length of each hcode favouring umlaut letters -#define USX_HCODE_LENS_FAVOR_UMLAUT \ - (const unsigned char[]) { 3, 3, 3, 3, 1 } +#define USX_HCODE_LENS_FAVOR_UMLAUT \ + (const unsigned char[]) \ + { \ + 3, 3, 3, 3, 1 \ + } /// Horizontal codes preset for no repeating sequences -#define USX_HCODES_NO_DICT \ - (const unsigned char[]) { 0x00, 0x40, 0x80, 0x00, 0xC0 } +#define USX_HCODES_NO_DICT \ + (const unsigned char[]) \ + { \ + 0x00, 0x40, 0x80, 0x00, 0xC0 \ + } /// Length of each hcode for no repeating sequences -#define USX_HCODE_LENS_NO_DICT \ - (const unsigned char[]) { 2, 2, 2, 0, 2 } +#define USX_HCODE_LENS_NO_DICT \ + (const unsigned char[]) \ + { \ + 2, 2, 2, 0, 2 \ + } /// Horizontal codes preset for no Unicode characters -#define USX_HCODES_NO_UNI \ - (const unsigned char[]) { 0x00, 0x40, 0x80, 0xC0, 0x00 } +#define USX_HCODES_NO_UNI \ + (const unsigned char[]) \ + { \ + 0x00, 0x40, 0x80, 0xC0, 0x00 \ + } /// Length of each hcode for no Unicode characters -#define USX_HCODE_LENS_NO_UNI \ - (const unsigned char[]) { 2, 2, 2, 2, 0 } +#define USX_HCODE_LENS_NO_UNI \ + (const unsigned char[]) \ + { \ + 2, 2, 2, 2, 0 \ + } extern const char *USX_FREQ_SEQ_DFLT[]; extern const char *USX_FREQ_SEQ_TXT[]; @@ -141,17 +201,19 @@ extern const char *USX_FREQ_SEQ_HTML[]; extern const char *USX_FREQ_SEQ_XML[]; extern const char *USX_TEMPLATES[]; -/// Default preset parameter set. When composition of text is know beforehand, the other parameter sets in this section -/// can be used to achieve more compression. +/// Default preset parameter set. When composition of text is know beforehand, the other parameter sets in this section can be +/// used to achieve more compression. #define USX_PSET_DFLT USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES /// Preset parameter set for English Alphabet only content #define USX_PSET_ALPHA_ONLY USX_HCODES_ALPHA_ONLY, USX_HCODE_LENS_ALPHA_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES /// Preset parameter set for Alpha numeric content #define USX_PSET_ALPHA_NUM_ONLY USX_HCODES_ALPHA_NUM_ONLY, USX_HCODE_LENS_ALPHA_NUM_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES /// Preset parameter set for Alpha numeric and symbol content -#define USX_PSET_ALPHA_NUM_SYM_ONLY USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +#define USX_PSET_ALPHA_NUM_SYM_ONLY \ + USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES /// Preset parameter set for Alpha numeric symbol content having predominantly text -#define USX_PSET_ALPHA_NUM_SYM_ONLY_TXT USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES +#define USX_PSET_ALPHA_NUM_SYM_ONLY_TXT \ + USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES /// Preset parameter set favouring Alphabet content #define USX_PSET_FAVOR_ALPHA USX_HCODES_FAVOR_ALPHA, USX_HCODE_LENS_FAVOR_ALPHA, USX_FREQ_SEQ_TXT, USX_TEMPLATES /// Preset parameter set favouring repeating sequences @@ -182,8 +244,8 @@ extern const char *USX_TEMPLATES[]; * This is passed as a parameter to the unishox2_decompress_lines() function */ struct us_lnk_lst { - char *data; - struct us_lnk_lst *previous; + char *data; + struct us_lnk_lst *previous; }; /** @@ -232,8 +294,9 @@ extern int unishox2_decompress_simple(const char *in, int len, char *out); * @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ -extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], - const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]); +extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), + const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], + const char *usx_templates[]); /** * Comprehensive API for de-compressing a string * @@ -250,8 +313,9 @@ extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(ch * @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ -extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], - const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]); +extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), + const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], + const char *usx_templates[]); /** * More Comprehensive API for compressing array of strings * @@ -262,9 +326,9 @@ extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN( * and stored in a compressed array of bytes for use as a constant in other programs \n * where each element of the array can be decompressed and used at runtime. */ -extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], - const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], - struct us_lnk_lst *prev_lines); +extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), + const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], + const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines); /** * More Comprehensive API for de-compressing array of strings \n * This function is not be used in conjuction with unishox2_compress_lines() @@ -276,8 +340,8 @@ extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_ * routine which takes this compressed array as parameter and index to be \n * decompressed. */ -extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], - const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[], - struct us_lnk_lst *prev_lines); +extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), + const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], + const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines); #endif diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 49a49a3fb..2b4f63512 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -29,166 +29,172 @@ using namespace concurrency; static Periodic *ethEvent; -static int32_t reconnectETH() { - if (config.network.eth_enabled) { - Ethernet.maintain(); - if (!ethStartupComplete) { - // Start web server - LOG_INFO("Start Ethernet network services"); +static int32_t reconnectETH() +{ + if (config.network.eth_enabled) { + Ethernet.maintain(); + if (!ethStartupComplete) { + // Start web server + LOG_INFO("Start Ethernet network services"); #ifndef DISABLE_NTP - LOG_INFO("Start NTP time client"); - timeClient.begin(); - timeClient.setUpdateInterval(60 * 60); // Update once an hour + LOG_INFO("Start NTP time client"); + timeClient.begin(); + timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif - if (config.network.rsyslog_server[0]) { - LOG_INFO("Start Syslog client"); - // Defaults - int serverPort = 514; - const char *serverAddr = config.network.rsyslog_server; - String server = String(serverAddr); - int delimIndex = server.indexOf(':'); - if (delimIndex > 0) { - String port = server.substring(delimIndex + 1, server.length()); - server[delimIndex] = 0; - serverPort = port.toInt(); - serverAddr = server.c_str(); - } - syslog.server(serverAddr, serverPort); - syslog.deviceHostname(getDeviceName()); - syslog.appName("Meshtastic"); - syslog.defaultPriority(LOGLEVEL_USER); - syslog.enable(); - } + if (config.network.rsyslog_server[0]) { + LOG_INFO("Start Syslog client"); + // Defaults + int serverPort = 514; + const char *serverAddr = config.network.rsyslog_server; + String server = String(serverAddr); + int delimIndex = server.indexOf(':'); + if (delimIndex > 0) { + String port = server.substring(delimIndex + 1, server.length()); + server[delimIndex] = 0; + serverPort = port.toInt(); + serverAddr = server.c_str(); + } + syslog.server(serverAddr, serverPort); + syslog.deviceHostname(getDeviceName()); + syslog.appName("Meshtastic"); + syslog.defaultPriority(LOGLEVEL_USER); + syslog.enable(); + } #if !MESHTASTIC_EXCLUDE_SOCKETAPI - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - initApiServer(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif #if HAS_UDP_MULTICAST - if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->start(); - } + if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->start(); + } #endif - ethStartupComplete = true; + ethStartupComplete = true; + } } - } #ifndef DISABLE_NTP - if (isEthernetAvailable() && (ntp_renew < millis())) { + if (isEthernetAvailable() && (ntp_renew < millis())) { - LOG_INFO("Update NTP time from %s", config.network.ntp_server); - if (timeClient.update()) { - LOG_DEBUG("NTP Request Success - Set RTCQualityNTP if needed"); + LOG_INFO("Update NTP time from %s", config.network.ntp_server); + if (timeClient.update()) { + LOG_DEBUG("NTP Request Success - Set RTCQualityNTP if needed"); - struct timeval tv; - tv.tv_sec = timeClient.getEpochTime(); - tv.tv_usec = 0; + struct timeval tv; + tv.tv_sec = timeClient.getEpochTime(); + tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + perhapsSetRTC(RTCQualityNTP, &tv); - ntp_renew = millis() + 43200 * 1000; // success, refresh every 12 hours - } else { - LOG_ERROR("NTP Update failed"); - ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes + ntp_renew = millis() + 43200 * 1000; // success, refresh every 12 hours + } else { + LOG_ERROR("NTP Update failed"); + ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes + } } - } #endif - return 5000; // every 5 seconds + return 5000; // every 5 seconds } // Startup Ethernet -bool initEthernet() { - if (config.network.eth_enabled) { +bool initEthernet() +{ + if (config.network.eth_enabled) { #ifdef PIN_ETH_POWER_EN - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. - delay(100); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. + delay(100); #endif #ifdef PIN_ETHERNET_RESET - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. - delay(100); - digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. #endif #ifdef RAK11310 // Initialize the SPI port - ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); - ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); - ETH_SPI_PORT.setRX(PIN_SPI0_MISO); - ETH_SPI_PORT.begin(); + ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); + ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); + ETH_SPI_PORT.setRX(PIN_SPI0_MISO); + ETH_SPI_PORT.begin(); #endif - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); - uint8_t mac[6]; + uint8_t mac[6]; - int status = 0; + int status = 0; - // createSSLCert(); + // createSSLCert(); - getMacAddr(mac); // FIXME use the BLE MAC for now... - mac[0] &= 0xfe; // Make sure this is not a multicast MAC + getMacAddr(mac); // FIXME use the BLE MAC for now... + mac[0] &= 0xfe; // Make sure this is not a multicast MAC - if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { - LOG_INFO("Start Ethernet DHCP"); - status = Ethernet.begin(mac); - } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { - LOG_INFO("Start Ethernet Static"); - Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, - config.network.ipv4_config.subnet); - status = 1; + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { + LOG_INFO("Start Ethernet DHCP"); + status = Ethernet.begin(mac); + } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { + LOG_INFO("Start Ethernet Static"); + Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, + config.network.ipv4_config.subnet); + status = 1; + } else { + LOG_INFO("Ethernet Disabled"); + return false; + } + + if (status == 0) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + LOG_ERROR("Ethernet shield was not found"); + return false; + } else if (Ethernet.linkStatus() == LinkOFF) { + LOG_ERROR("Ethernet cable is not connected"); + return false; + } else { + LOG_ERROR("Unknown Ethernet error"); + return false; + } + } else { + LOG_INFO("Local IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], + Ethernet.localIP()[3]); + LOG_INFO("Subnet Mask %u.%u.%u.%u", Ethernet.subnetMask()[0], Ethernet.subnetMask()[1], Ethernet.subnetMask()[2], + Ethernet.subnetMask()[3]); + LOG_INFO("Gateway IP %u.%u.%u.%u", Ethernet.gatewayIP()[0], Ethernet.gatewayIP()[1], Ethernet.gatewayIP()[2], + Ethernet.gatewayIP()[3]); + LOG_INFO("DNS Server IP %u.%u.%u.%u", Ethernet.dnsServerIP()[0], Ethernet.dnsServerIP()[1], Ethernet.dnsServerIP()[2], + Ethernet.dnsServerIP()[3]); + } + + ethEvent = new Periodic("ethConnect", reconnectETH); + + return true; } else { - LOG_INFO("Ethernet Disabled"); - return false; + LOG_INFO("Not using Ethernet"); + return false; } - - if (status == 0) { - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - LOG_ERROR("Ethernet shield was not found"); - return false; - } else if (Ethernet.linkStatus() == LinkOFF) { - LOG_ERROR("Ethernet cable is not connected"); - return false; - } else { - LOG_ERROR("Unknown Ethernet error"); - return false; - } - } else { - LOG_INFO("Local IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]); - LOG_INFO("Subnet Mask %u.%u.%u.%u", Ethernet.subnetMask()[0], Ethernet.subnetMask()[1], Ethernet.subnetMask()[2], Ethernet.subnetMask()[3]); - LOG_INFO("Gateway IP %u.%u.%u.%u", Ethernet.gatewayIP()[0], Ethernet.gatewayIP()[1], Ethernet.gatewayIP()[2], Ethernet.gatewayIP()[3]); - LOG_INFO("DNS Server IP %u.%u.%u.%u", Ethernet.dnsServerIP()[0], Ethernet.dnsServerIP()[1], Ethernet.dnsServerIP()[2], - Ethernet.dnsServerIP()[3]); - } - - ethEvent = new Periodic("ethConnect", reconnectETH); - - return true; - } else { - LOG_INFO("Not using Ethernet"); - return false; - } } -bool isEthernetAvailable() { +bool isEthernetAvailable() +{ - if (!config.network.eth_enabled) { - syslog.disable(); - return false; - } else if (Ethernet.hardwareStatus() == EthernetNoHardware) { - syslog.disable(); - return false; - } else if (Ethernet.linkStatus() == LinkOFF) { - syslog.disable(); - return false; - } else { - return true; - } + if (!config.network.eth_enabled) { + syslog.disable(); + return false; + } else if (Ethernet.hardwareStatus() == EthernetNoHardware) { + syslog.disable(); + return false; + } else if (Ethernet.linkStatus() == LinkOFF) { + syslog.disable(); + return false; + } else { + return true; + } } #endif diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index f6ec9d88b..7b7ebb595 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -67,903 +67,927 @@ char const *contentTypes[][2] = {{".txt", "text/plain"}, {".html", "text/htm // Our API to handle messages to and from the radio. HttpAPI webAPI; -void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) { +void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) +{ - // For every resource available on the server, we need to create a ResourceNode - // The ResourceNode links URL and HTTP method to a handler function + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function - ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); - ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); - ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio); - ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); + ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio); + ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); - // ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); - // ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot); + // ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); + // ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot); - ResourceNode *nodeAdmin = new ResourceNode("/admin", "GET", &handleAdmin); - // ResourceNode *nodeAdminSettings = new ResourceNode("/admin/settings", "GET", &handleAdminSettings); - // ResourceNode *nodeAdminSettingsApply = new ResourceNode("/admin/settings/apply", "POST", - // &handleAdminSettingsApply); ResourceNode *nodeAdminFs = new ResourceNode("/admin/fs", "GET", &handleFs); - // ResourceNode *nodeUpdateFs = new ResourceNode("/admin/fs/update", "POST", &handleUpdateFs); - // ResourceNode *nodeDeleteFs = new ResourceNode("/admin/fs/delete", "GET", &handleDeleteFsContent); + ResourceNode *nodeAdmin = new ResourceNode("/admin", "GET", &handleAdmin); + // ResourceNode *nodeAdminSettings = new ResourceNode("/admin/settings", "GET", &handleAdminSettings); + // ResourceNode *nodeAdminSettingsApply = new ResourceNode("/admin/settings/apply", "POST", &handleAdminSettingsApply); + // ResourceNode *nodeAdminFs = new ResourceNode("/admin/fs", "GET", &handleFs); + // ResourceNode *nodeUpdateFs = new ResourceNode("/admin/fs/update", "POST", &handleUpdateFs); + // ResourceNode *nodeDeleteFs = new ResourceNode("/admin/fs/delete", "GET", &handleDeleteFsContent); - ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart); - ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); + ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart); + ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); - ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); - ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); - ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); - ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); - ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); - ResourceNode *nodeJsonDelete = new ResourceNode("/json/fs/delete/static", "DELETE", &handleFsDeleteStatic); + ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); + ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); + ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); + ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); + ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); + ResourceNode *nodeJsonDelete = new ResourceNode("/json/fs/delete/static", "DELETE", &handleFsDeleteStatic); - ResourceNode *nodeRoot = new ResourceNode("/*", "GET", &handleStatic); + ResourceNode *nodeRoot = new ResourceNode("/*", "GET", &handleStatic); - // Secure nodes - secureServer->registerNode(nodeAPIv1ToRadioOptions); - secureServer->registerNode(nodeAPIv1ToRadio); - secureServer->registerNode(nodeAPIv1FromRadioOptions); - secureServer->registerNode(nodeAPIv1FromRadio); - // secureServer->registerNode(nodeHotspotApple); - // secureServer->registerNode(nodeHotspotAndroid); - secureServer->registerNode(nodeRestart); - secureServer->registerNode(nodeFormUpload); - secureServer->registerNode(nodeJsonScanNetworks); - secureServer->registerNode(nodeJsonBlinkLED); - secureServer->registerNode(nodeJsonFsBrowseStatic); - secureServer->registerNode(nodeJsonDelete); - secureServer->registerNode(nodeJsonReport); - secureServer->registerNode(nodeJsonNodes); - // secureServer->registerNode(nodeUpdateFs); - // secureServer->registerNode(nodeDeleteFs); - secureServer->registerNode(nodeAdmin); - // secureServer->registerNode(nodeAdminFs); - // secureServer->registerNode(nodeAdminSettings); - // secureServer->registerNode(nodeAdminSettingsApply); - secureServer->registerNode(nodeRoot); // This has to be last + // Secure nodes + secureServer->registerNode(nodeAPIv1ToRadioOptions); + secureServer->registerNode(nodeAPIv1ToRadio); + secureServer->registerNode(nodeAPIv1FromRadioOptions); + secureServer->registerNode(nodeAPIv1FromRadio); + // secureServer->registerNode(nodeHotspotApple); + // secureServer->registerNode(nodeHotspotAndroid); + secureServer->registerNode(nodeRestart); + secureServer->registerNode(nodeFormUpload); + secureServer->registerNode(nodeJsonScanNetworks); + secureServer->registerNode(nodeJsonBlinkLED); + secureServer->registerNode(nodeJsonFsBrowseStatic); + secureServer->registerNode(nodeJsonDelete); + secureServer->registerNode(nodeJsonReport); + secureServer->registerNode(nodeJsonNodes); + // secureServer->registerNode(nodeUpdateFs); + // secureServer->registerNode(nodeDeleteFs); + secureServer->registerNode(nodeAdmin); + // secureServer->registerNode(nodeAdminFs); + // secureServer->registerNode(nodeAdminSettings); + // secureServer->registerNode(nodeAdminSettingsApply); + secureServer->registerNode(nodeRoot); // This has to be last - // Insecure nodes - insecureServer->registerNode(nodeAPIv1ToRadioOptions); - insecureServer->registerNode(nodeAPIv1ToRadio); - insecureServer->registerNode(nodeAPIv1FromRadioOptions); - insecureServer->registerNode(nodeAPIv1FromRadio); - // insecureServer->registerNode(nodeHotspotApple); - // insecureServer->registerNode(nodeHotspotAndroid); - insecureServer->registerNode(nodeRestart); - insecureServer->registerNode(nodeFormUpload); - insecureServer->registerNode(nodeJsonScanNetworks); - insecureServer->registerNode(nodeJsonBlinkLED); - insecureServer->registerNode(nodeJsonFsBrowseStatic); - insecureServer->registerNode(nodeJsonDelete); - insecureServer->registerNode(nodeJsonReport); - // insecureServer->registerNode(nodeUpdateFs); - // insecureServer->registerNode(nodeDeleteFs); - insecureServer->registerNode(nodeAdmin); - // insecureServer->registerNode(nodeAdminFs); - // insecureServer->registerNode(nodeAdminSettings); - // insecureServer->registerNode(nodeAdminSettingsApply); - insecureServer->registerNode(nodeRoot); // This has to be last + // Insecure nodes + insecureServer->registerNode(nodeAPIv1ToRadioOptions); + insecureServer->registerNode(nodeAPIv1ToRadio); + insecureServer->registerNode(nodeAPIv1FromRadioOptions); + insecureServer->registerNode(nodeAPIv1FromRadio); + // insecureServer->registerNode(nodeHotspotApple); + // insecureServer->registerNode(nodeHotspotAndroid); + insecureServer->registerNode(nodeRestart); + insecureServer->registerNode(nodeFormUpload); + insecureServer->registerNode(nodeJsonScanNetworks); + insecureServer->registerNode(nodeJsonBlinkLED); + insecureServer->registerNode(nodeJsonFsBrowseStatic); + insecureServer->registerNode(nodeJsonDelete); + insecureServer->registerNode(nodeJsonReport); + // insecureServer->registerNode(nodeUpdateFs); + // insecureServer->registerNode(nodeDeleteFs); + insecureServer->registerNode(nodeAdmin); + // insecureServer->registerNode(nodeAdminFs); + // insecureServer->registerNode(nodeAdminSettings); + // insecureServer->registerNode(nodeAdminSettingsApply); + insecureServer->registerNode(nodeRoot); // This has to be last } -void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) { - if (webServerThread) - webServerThread->markActivity(); +void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) +{ + if (webServerThread) + webServerThread->markActivity(); - LOG_DEBUG("webAPI handleAPIv1FromRadio"); + LOG_DEBUG("webAPI handleAPIv1FromRadio"); - /* - For documentation, see: - https://meshtastic.org/docs/development/device/http-api - https://meshtastic.org/docs/development/device/client-api - */ + /* + For documentation, see: + https://meshtastic.org/docs/development/device/http-api + https://meshtastic.org/docs/development/device/client-api + */ - // Get access to the parameters - ResourceParameters *params = req->getParams(); + // Get access to the parameters + ResourceParameters *params = req->getParams(); - // std::string paramAll = "all"; - std::string valueAll; + // std::string paramAll = "all"; + std::string valueAll; - // Status code is 200 OK by default. - res->setHeader("Content-Type", "application/x-protobuf"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + // Status code is 200 OK by default. + res->setHeader("Content-Type", "application/x-protobuf"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (req->getMethod() == "OPTIONS") { - res->setStatusCode(204); // Success with no content - // res->print(""); @todo remove - return; - } + if (req->getMethod() == "OPTIONS") { + res->setStatusCode(204); // Success with no content + // res->print(""); @todo remove + return; + } - uint8_t txBuf[MAX_STREAM_BUF_SIZE]; - uint32_t len = 1; + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; - if (params->getQueryParameter("all", valueAll)) { + if (params->getQueryParameter("all", valueAll)) { - // If all is true, return all the buffers we have available - // to us at this point in time. - if (valueAll == "true") { - while (len) { + // If all is true, return all the buffers we have available + // to us at this point in time. + if (valueAll == "true") { + while (len) { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); + } + + // Otherwise, just return one protobuf + } else { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); + } + + // the param "all" was not specified. Return just one protobuf + } else { len = webAPI.getFromRadio(txBuf); res->write(txBuf, len); - } - - // Otherwise, just return one protobuf - } else { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); } - // the param "all" was not specified. Return just one protobuf - } else { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); - } - - LOG_DEBUG("webAPI handleAPIv1FromRadio, len %d", len); + LOG_DEBUG("webAPI handleAPIv1FromRadio, len %d", len); } -void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) { - LOG_DEBUG("webAPI handleAPIv1ToRadio"); +void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) +{ + LOG_DEBUG("webAPI handleAPIv1ToRadio"); - /* - For documentation, see: - https://meshtastic.org/docs/development/device/http-api - https://meshtastic.org/docs/development/device/client-api - */ + /* + For documentation, see: + https://meshtastic.org/docs/development/device/http-api + https://meshtastic.org/docs/development/device/client-api + */ - res->setHeader("Content-Type", "application/x-protobuf"); - res->setHeader("Access-Control-Allow-Headers", "Content-Type"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "PUT, OPTIONS"); - res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + res->setHeader("Content-Type", "application/x-protobuf"); + res->setHeader("Access-Control-Allow-Headers", "Content-Type"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "PUT, OPTIONS"); + res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (req->getMethod() == "OPTIONS") { - res->setStatusCode(204); // Success with no content - // res->print(""); @todo remove - return; - } - - byte buffer[MAX_TO_FROM_RADIO_SIZE]; - size_t s = req->readBytes(buffer, MAX_TO_FROM_RADIO_SIZE); - - LOG_DEBUG("Received %d bytes from PUT request", s); - webAPI.handleToRadio(buffer, s); - - res->write(buffer, s); - LOG_DEBUG("webAPI handleAPIv1ToRadio"); -} - -void htmlDeleteDir(const char *dirname) { - - File root = FSCom.open(dirname); - if (!root) { - return; - } - if (!root.isDirectory()) { - return; - } - - File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - htmlDeleteDir(file.name()); - file.flush(); - file.close(); - } else { - String fileName = String(file.name()); - file.flush(); - file.close(); - LOG_DEBUG(" %s", fileName.c_str()); - FSCom.remove(fileName); + if (req->getMethod() == "OPTIONS") { + res->setStatusCode(204); // Success with no content + // res->print(""); @todo remove + return; } - file = root.openNextFile(); - } - root.flush(); - root.close(); + + byte buffer[MAX_TO_FROM_RADIO_SIZE]; + size_t s = req->readBytes(buffer, MAX_TO_FROM_RADIO_SIZE); + + LOG_DEBUG("Received %d bytes from PUT request", s); + webAPI.handleToRadio(buffer, s); + + res->write(buffer, s); + LOG_DEBUG("webAPI handleAPIv1ToRadio"); } -JSONArray htmlListDir(const char *dirname, uint8_t levels) { - File root = FSCom.open(dirname, FILE_O_READ); - JSONArray fileList; - if (!root) { - return fileList; - } - if (!root.isDirectory()) { - return fileList; - } +void htmlDeleteDir(const char *dirname) +{ - // iterate over the file list - File file = root.openNextFile(); - while (file) { - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { + File root = FSCom.open(dirname); + if (!root) { + return; + } + if (!root.isDirectory()) { + return; + } + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + htmlDeleteDir(file.name()); + file.flush(); + file.close(); + } else { + String fileName = String(file.name()); + file.flush(); + file.close(); + LOG_DEBUG(" %s", fileName.c_str()); + FSCom.remove(fileName); + } + file = root.openNextFile(); + } + root.flush(); + root.close(); +} + +JSONArray htmlListDir(const char *dirname, uint8_t levels) +{ + File root = FSCom.open(dirname, FILE_O_READ); + JSONArray fileList; + if (!root) { + return fileList; + } + if (!root.isDirectory()) { + return fileList; + } + + // iterate over the file list + File file = root.openNextFile(); + while (file) { + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { #ifdef ARCH_ESP32 - fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1))); + fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1))); #else - fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1))); + fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1))); #endif + file.close(); + } + } else { + JSONObject thisFileMap; + thisFileMap["size"] = new JSONValue((int)file.size()); +#ifdef ARCH_ESP32 + String fileName = String(file.path()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); +#else + String fileName = String(file.name()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); +#endif + String tempName = String(file.name()).substring(1); + if (tempName.endsWith(".gz")) { +#ifdef ARCH_ESP32 + String modifiedFile = String(file.path()).substring(1); +#else + String modifiedFile = String(file.name()).substring(1); +#endif + modifiedFile.remove((modifiedFile.length() - 3), 3); + thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str()); + } + fileList.push_back(new JSONValue(thisFileMap)); + } file.close(); - } - } else { - JSONObject thisFileMap; - thisFileMap["size"] = new JSONValue((int)file.size()); -#ifdef ARCH_ESP32 - String fileName = String(file.path()).substring(1); - thisFileMap["name"] = new JSONValue(fileName.c_str()); -#else - String fileName = String(file.name()).substring(1); - thisFileMap["name"] = new JSONValue(fileName.c_str()); -#endif - String tempName = String(file.name()).substring(1); - if (tempName.endsWith(".gz")) { -#ifdef ARCH_ESP32 - String modifiedFile = String(file.path()).substring(1); -#else - String modifiedFile = String(file.name()).substring(1); -#endif - modifiedFile.remove((modifiedFile.length() - 3), 3); - thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str()); - } - fileList.push_back(new JSONValue(thisFileMap)); + file = root.openNextFile(); } - file.close(); - file = root.openNextFile(); - } - root.close(); - return fileList; + root.close(); + return fileList; } -void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - - concurrency::LockGuard g(spiLock); - auto fileList = htmlListDir("/static", 10); - - // create json output structure - JSONObject filesystemObj; - filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes()); - filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes()); - filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes())); - - JSONObject jsonObjInner; - jsonObjInner["files"] = new JSONValue(fileList); - jsonObjInner["filesystem"] = new JSONValue(filesystemObj); - - JSONObject jsonObjOuter; - jsonObjOuter["data"] = new JSONValue(jsonObjInner); - jsonObjOuter["status"] = new JSONValue("ok"); - - JSONValue *value = new JSONValue(jsonObjOuter); - - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - - delete value; - - // Clean up the fileList to prevent memory leak - for (auto *val : fileList) { - delete val; - } -} - -void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) { - ResourceParameters *params = req->getParams(); - std::string paramValDelete; - - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "DELETE"); - - if (params->getQueryParameter("delete", paramValDelete)) { - std::string pathDelete = "/" + paramValDelete; - concurrency::LockGuard g(spiLock); - if (FSCom.remove(pathDelete.c_str())) { - - LOG_INFO("%s", pathDelete.c_str()); - JSONObject jsonObjOuter; - jsonObjOuter["status"] = new JSONValue("ok"); - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; - return; - } else { - - LOG_INFO("%s", pathDelete.c_str()); - JSONObject jsonObjOuter; - jsonObjOuter["status"] = new JSONValue("Error"); - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; - return; - } - } -} - -void handleStatic(HTTPRequest *req, HTTPResponse *res) { - if (webServerThread) - webServerThread->markActivity(); - - // Get access to the parameters - ResourceParameters *params = req->getParams(); - - std::string parameter1; - // Print the first parameter value - if (params->getPathParameter(0, parameter1)) { - - std::string filename = "/static/" + parameter1; - std::string filenameGzip = "/static/" + parameter1 + ".gz"; - - // Try to open the file - File file; - - bool has_set_content_type = false; - - if (filename == "/static/") { - filename = "/static/index.html"; - filenameGzip = "/static/index.html.gz"; - } +void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); concurrency::LockGuard g(spiLock); + auto fileList = htmlListDir("/static", 10); - if (FSCom.exists(filename.c_str())) { - file = FSCom.open(filename.c_str()); - if (!file.available()) { - LOG_WARN("File not available - %s", filename.c_str()); - } - } else if (FSCom.exists(filenameGzip.c_str())) { - file = FSCom.open(filenameGzip.c_str()); - res->setHeader("Content-Encoding", "gzip"); - if (!file.available()) { - LOG_WARN("File not available - %s", filenameGzip.c_str()); - } - } else { - has_set_content_type = true; - filenameGzip = "/static/index.html.gz"; - file = FSCom.open(filenameGzip.c_str()); - res->setHeader("Content-Type", "text/html"); - if (!file.available()) { + // create json output structure + JSONObject filesystemObj; + filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes()); + filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes()); + filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes())); - LOG_WARN("File not available - %s", filenameGzip.c_str()); - res->println("Web server is running.

The content you are looking for can't be found. Please see: FAQ.

admin"); + JSONObject jsonObjInner; + jsonObjInner["files"] = new JSONValue(fileList); + jsonObjInner["filesystem"] = new JSONValue(filesystemObj); + + JSONObject jsonObjOuter; + jsonObjOuter["data"] = new JSONValue(jsonObjInner); + jsonObjOuter["status"] = new JSONValue("ok"); + + JSONValue *value = new JSONValue(jsonObjOuter); + + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + + delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } +} + +void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) +{ + ResourceParameters *params = req->getParams(); + std::string paramValDelete; + + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "DELETE"); + + if (params->getQueryParameter("delete", paramValDelete)) { + std::string pathDelete = "/" + paramValDelete; + concurrency::LockGuard g(spiLock); + if (FSCom.remove(pathDelete.c_str())) { + + LOG_INFO("%s", pathDelete.c_str()); + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("ok"); + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; + return; + } else { + + LOG_INFO("%s", pathDelete.c_str()); + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("Error"); + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; + return; + } + } +} + +void handleStatic(HTTPRequest *req, HTTPResponse *res) +{ + if (webServerThread) + webServerThread->markActivity(); + + // Get access to the parameters + ResourceParameters *params = req->getParams(); + + std::string parameter1; + // Print the first parameter value + if (params->getPathParameter(0, parameter1)) { + + std::string filename = "/static/" + parameter1; + std::string filenameGzip = "/static/" + parameter1 + ".gz"; + + // Try to open the file + File file; + + bool has_set_content_type = false; + + if (filename == "/static/") { + filename = "/static/index.html"; + filenameGzip = "/static/index.html.gz"; + } + + concurrency::LockGuard g(spiLock); + + if (FSCom.exists(filename.c_str())) { + file = FSCom.open(filename.c_str()); + if (!file.available()) { + LOG_WARN("File not available - %s", filename.c_str()); + } + } else if (FSCom.exists(filenameGzip.c_str())) { + file = FSCom.open(filenameGzip.c_str()); + res->setHeader("Content-Encoding", "gzip"); + if (!file.available()) { + LOG_WARN("File not available - %s", filenameGzip.c_str()); + } + } else { + has_set_content_type = true; + filenameGzip = "/static/index.html.gz"; + file = FSCom.open(filenameGzip.c_str()); + res->setHeader("Content-Type", "text/html"); + if (!file.available()) { + + LOG_WARN("File not available - %s", filenameGzip.c_str()); + res->println("Web server is running.

The content you are looking for can't be found. Please see: FAQ.

admin"); + + return; + } else { + res->setHeader("Content-Encoding", "gzip"); + } + } + + res->setHeader("Content-Length", httpsserver::intToString(file.size())); + + // Content-Type is guessed using the definition of the contentTypes-table defined above + int cTypeIdx = 0; + do { + if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) { + res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); + has_set_content_type = true; + break; + } + cTypeIdx += 1; + } while (strlen(contentTypes[cTypeIdx][0]) > 0); + + if (!has_set_content_type) { + // Set a default content type + res->setHeader("Content-Type", "application/octet-stream"); + } + + // Read the file and write it to the HTTP response body + size_t length = 0; + do { + char buffer[256]; + length = file.read((uint8_t *)buffer, 256); + std::string bufferString(buffer, length); + res->write((uint8_t *)bufferString.c_str(), bufferString.size()); + } while (length > 0); + + file.close(); return; - } else { - res->setHeader("Content-Encoding", "gzip"); - } + } else { + LOG_ERROR("This should not have happened"); + res->println("ERROR: This should not have happened"); } - - res->setHeader("Content-Length", httpsserver::intToString(file.size())); - - // Content-Type is guessed using the definition of the contentTypes-table defined above - int cTypeIdx = 0; - do { - if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) { - res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); - has_set_content_type = true; - break; - } - cTypeIdx += 1; - } while (strlen(contentTypes[cTypeIdx][0]) > 0); - - if (!has_set_content_type) { - // Set a default content type - res->setHeader("Content-Type", "application/octet-stream"); - } - - // Read the file and write it to the HTTP response body - size_t length = 0; - do { - char buffer[256]; - length = file.read((uint8_t *)buffer, 256); - std::string bufferString(buffer, length); - res->write((uint8_t *)bufferString.c_str(), bufferString.size()); - } while (length > 0); - - file.close(); - - return; - } else { - LOG_ERROR("This should not have happened"); - res->println("ERROR: This should not have happened"); - } } -void handleFormUpload(HTTPRequest *req, HTTPResponse *res) { +void handleFormUpload(HTTPRequest *req, HTTPResponse *res) +{ - LOG_DEBUG("Form Upload - Disable keep-alive"); - res->setHeader("Connection", "close"); + LOG_DEBUG("Form Upload - Disable keep-alive"); + res->setHeader("Connection", "close"); - // First, we need to check the encoding of the form that we have received. - // The browser will set the Content-Type request header, so we can use it for that purpose. - // Then we select the body parser based on the encoding. - // Actually we do this only for documentary purposes, we know the form is going - // to be multipart/form-data. - LOG_DEBUG("Form Upload - Creating body parser reference"); - HTTPBodyParser *parser; - std::string contentType = req->getHeader("Content-Type"); + // First, we need to check the encoding of the form that we have received. + // The browser will set the Content-Type request header, so we can use it for that purpose. + // Then we select the body parser based on the encoding. + // Actually we do this only for documentary purposes, we know the form is going + // to be multipart/form-data. + LOG_DEBUG("Form Upload - Creating body parser reference"); + HTTPBodyParser *parser; + std::string contentType = req->getHeader("Content-Type"); - // The content type may have additional properties after a semicolon, for example: - // Content-Type: text/html;charset=utf-8 - // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs - // As we're interested only in the actual mime _type_, we strip everything after the - // first semicolon, if one exists: - size_t semicolonPos = contentType.find(";"); - if (semicolonPos != std::string::npos) { - contentType.resize(semicolonPos); - } - - // Now, we can decide based on the content type: - if (contentType == "multipart/form-data") { - LOG_DEBUG("Form Upload - multipart/form-data"); - parser = new HTTPMultipartBodyParser(req); - } else { - LOG_DEBUG("Unknown POST Content-Type: %s", contentType.c_str()); - return; - } - - res->println("File " - "Upload

File Upload

"); - - // We iterate over the fields. Any field with a filename is uploaded. - // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's - // fields only a single time. The reason for this is that it allows you to handle large requests - // which would not fit into memory. - bool didwrite = false; - - // parser->nextField() will move the parser to the next field in the request body (field meaning a - // form field, if you take the HTML perspective). After the last field has been processed, nextField() - // returns false and the while loop ends. - while (parser->nextField()) { - // For Multipart data, each field has three properties: - // The name ("name" value of the tag) - // The filename (If it was a , this is the filename on the machine of the - // user uploading it) - // The mime type (It is determined by the client. So do not trust this value and blindly start - // parsing files only if the type matches) - std::string name = parser->getFieldName(); - std::string filename = parser->getFieldFilename(); - std::string mimeType = parser->getFieldMimeType(); - // We log all three values, so that you can observe the upload on the serial monitor: - LOG_DEBUG("handleFormUpload: field name='%s', filename='%s', mimetype='%s'", name.c_str(), filename.c_str(), mimeType.c_str()); - - // Double check that it is what we expect - if (name != "file") { - LOG_DEBUG("Skip unexpected field"); - res->println("

No file found.

"); - return; + // The content type may have additional properties after a semicolon, for example: + // Content-Type: text/html;charset=utf-8 + // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs + // As we're interested only in the actual mime _type_, we strip everything after the + // first semicolon, if one exists: + size_t semicolonPos = contentType.find(";"); + if (semicolonPos != std::string::npos) { + contentType.resize(semicolonPos); } - // Double check that it is what we expect - if (filename == "") { - LOG_DEBUG("Skip unexpected field"); - res->println("

No file found.

"); - return; + // Now, we can decide based on the content type: + if (contentType == "multipart/form-data") { + LOG_DEBUG("Form Upload - multipart/form-data"); + parser = new HTTPMultipartBodyParser(req); + } else { + LOG_DEBUG("Unknown POST Content-Type: %s", contentType.c_str()); + return; } - // You should check file name validity and all that, but we skip that to make the core - // concepts of the body parser functionality easier to understand. - std::string pathname = "/static/" + filename; + res->println("File " + "Upload

File Upload

"); - concurrency::LockGuard g(spiLock); - // Create a new file to stream the data into - File file = FSCom.open(pathname.c_str(), FILE_O_WRITE); - size_t fileLength = 0; - didwrite = true; + // We iterate over the fields. Any field with a filename is uploaded. + // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's + // fields only a single time. The reason for this is that it allows you to handle large requests + // which would not fit into memory. + bool didwrite = false; - // With endOfField you can check whether the end of field has been reached or if there's - // still data pending. With multipart bodies, you cannot know the field size in advance. - while (!parser->endOfField()) { - esp_task_wdt_reset(); + // parser->nextField() will move the parser to the next field in the request body (field meaning a + // form field, if you take the HTML perspective). After the last field has been processed, nextField() + // returns false and the while loop ends. + while (parser->nextField()) { + // For Multipart data, each field has three properties: + // The name ("name" value of the tag) + // The filename (If it was a , this is the filename on the machine of the + // user uploading it) + // The mime type (It is determined by the client. So do not trust this value and blindly start + // parsing files only if the type matches) + std::string name = parser->getFieldName(); + std::string filename = parser->getFieldFilename(); + std::string mimeType = parser->getFieldMimeType(); + // We log all three values, so that you can observe the upload on the serial monitor: + LOG_DEBUG("handleFormUpload: field name='%s', filename='%s', mimetype='%s'", name.c_str(), filename.c_str(), + mimeType.c_str()); - byte buf[512]; - size_t readLength = parser->read(buf, 512); - // LOG_DEBUG("readLength - %i", readLength); + // Double check that it is what we expect + if (name != "file") { + LOG_DEBUG("Skip unexpected field"); + res->println("

No file found.

"); + return; + } - // Abort the transfer if there is less than 50k space left on the filesystem. - if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { - file.flush(); - file.close(); - res->println("

Write aborted! Reserving 50k on filesystem.

"); + // Double check that it is what we expect + if (filename == "") { + LOG_DEBUG("Skip unexpected field"); + res->println("

No file found.

"); + return; + } + // You should check file name validity and all that, but we skip that to make the core + // concepts of the body parser functionality easier to understand. + std::string pathname = "/static/" + filename; + + concurrency::LockGuard g(spiLock); + // Create a new file to stream the data into + File file = FSCom.open(pathname.c_str(), FILE_O_WRITE); + size_t fileLength = 0; + didwrite = true; + + // With endOfField you can check whether the end of field has been reached or if there's + // still data pending. With multipart bodies, you cannot know the field size in advance. + while (!parser->endOfField()) { + esp_task_wdt_reset(); + + byte buf[512]; + size_t readLength = parser->read(buf, 512); + // LOG_DEBUG("readLength - %i", readLength); + + // Abort the transfer if there is less than 50k space left on the filesystem. + if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { + file.flush(); + file.close(); + res->println("

Write aborted! Reserving 50k on filesystem.

"); + + // enableLoopWDT(); + + delete parser; + return; + } + + // if (readLength) { + file.write(buf, readLength); + fileLength += readLength; + LOG_DEBUG("File Length %i", fileLength); + //} + } // enableLoopWDT(); - delete parser; - return; - } + file.flush(); + file.close(); - // if (readLength) { - file.write(buf, readLength); - fileLength += readLength; - LOG_DEBUG("File Length %i", fileLength); - //} + res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); } - // enableLoopWDT(); - - file.flush(); - file.close(); - - res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); - } - if (!didwrite) { - res->println("

Did not write any file

"); - } - res->println(""); - delete parser; + if (!didwrite) { + res->println("

Did not write any file

"); + } + res->println(""); + delete parser; } -void handleReport(HTTPRequest *req, HTTPResponse *res) { - ResourceParameters *params = req->getParams(); - std::string content; +void handleReport(HTTPRequest *req, HTTPResponse *res) +{ + ResourceParameters *params = req->getParams(); + std::string content; - if (!params->getQueryParameter("content", content)) { - content = "json"; - } - - if (content == "json") { - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - } else { - res->setHeader("Content-Type", "text/html"); - res->println("
");
-  }
-
-  // Helper lambda to create JSON array and clean up memory properly
-  auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
-    JSONArray tempArray;
-    for (int i = 0; i < count; i++) {
-      tempArray.push_back(new JSONValue((int)logArray[i]));
+    if (!params->getQueryParameter("content", content)) {
+        content = "json";
     }
-    JSONValue *result = new JSONValue(tempArray);
-    // Note: Don't delete tempArray elements here - JSONValue now owns them
-    return result;
-  };
 
-  // data->airtime->tx_log
-  uint32_t *logArray;
-  logArray = airTime->airtimeReport(TX_LOG);
-  JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
+    if (content == "json") {
+        res->setHeader("Content-Type", "application/json");
+        res->setHeader("Access-Control-Allow-Origin", "*");
+        res->setHeader("Access-Control-Allow-Methods", "GET");
+    } else {
+        res->setHeader("Content-Type", "text/html");
+        res->println("
");
+    }
 
-  // data->airtime->rx_log
-  logArray = airTime->airtimeReport(RX_LOG);
-  JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
+    // Helper lambda to create JSON array and clean up memory properly
+    auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
+        JSONArray tempArray;
+        for (int i = 0; i < count; i++) {
+            tempArray.push_back(new JSONValue((int)logArray[i]));
+        }
+        JSONValue *result = new JSONValue(tempArray);
+        // Note: Don't delete tempArray elements here - JSONValue now owns them
+        return result;
+    };
 
-  // data->airtime->rx_all_log
-  logArray = airTime->airtimeReport(RX_ALL_LOG);
-  JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
+    // data->airtime->tx_log
+    uint32_t *logArray;
+    logArray = airTime->airtimeReport(TX_LOG);
+    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
-  // data->airtime
-  JSONObject jsonObjAirtime;
-  jsonObjAirtime["tx_log"] = txLogJsonValue;
-  jsonObjAirtime["rx_log"] = rxLogJsonValue;
-  jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
-  jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
-  jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
-  jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
-  jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
-  jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
+    // data->airtime->rx_log
+    logArray = airTime->airtimeReport(RX_LOG);
+    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
-  // data->wifi
-  JSONObject jsonObjWifi;
-  jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
-  String wifiIPString = WiFi.localIP().toString();
-  std::string wifiIP = wifiIPString.c_str();
-  jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
+    // data->airtime->rx_all_log
+    logArray = airTime->airtimeReport(RX_ALL_LOG);
+    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
-  // data->memory
-  JSONObject jsonObjMemory;
-  jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
-  jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
-  jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
-  jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
-  spiLock->lock();
-  jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
-  jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
-  jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
-  spiLock->unlock();
+    // data->airtime
+    JSONObject jsonObjAirtime;
+    jsonObjAirtime["tx_log"] = txLogJsonValue;
+    jsonObjAirtime["rx_log"] = rxLogJsonValue;
+    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
+    jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
+    jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
+    jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
+    jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
+    jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
 
-  // data->power
-  JSONObject jsonObjPower;
-  jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
-  jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
-  jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
-  jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
-  jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
+    // data->wifi
+    JSONObject jsonObjWifi;
+    jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
+    String wifiIPString = WiFi.localIP().toString();
+    std::string wifiIP = wifiIPString.c_str();
+    jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
 
-  // data->device
-  JSONObject jsonObjDevice;
-  jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
+    // data->memory
+    JSONObject jsonObjMemory;
+    jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
+    jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
+    jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
+    jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
+    spiLock->lock();
+    jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
+    jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
+    jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
+    spiLock->unlock();
 
-  // data->radio
-  JSONObject jsonObjRadio;
-  jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
-  jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
+    // data->power
+    JSONObject jsonObjPower;
+    jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
+    jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
+    jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
+    jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
+    jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
 
-  // collect data to inner data object
-  JSONObject jsonObjInner;
-  jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
-  jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
-  jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
-  jsonObjInner["power"] = new JSONValue(jsonObjPower);
-  jsonObjInner["device"] = new JSONValue(jsonObjDevice);
-  jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
+    // data->device
+    JSONObject jsonObjDevice;
+    jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
 
-  // create json output structure
-  JSONObject jsonObjOuter;
-  jsonObjOuter["data"] = new JSONValue(jsonObjInner);
-  jsonObjOuter["status"] = new JSONValue("ok");
-  // serialize and write it to the stream
-  JSONValue *value = new JSONValue(jsonObjOuter);
-  std::string jsonString = value->Stringify();
-  res->print(jsonString.c_str());
-  delete value;
+    // data->radio
+    JSONObject jsonObjRadio;
+    jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
+    jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
+
+    // collect data to inner data object
+    JSONObject jsonObjInner;
+    jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
+    jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
+    jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
+    jsonObjInner["power"] = new JSONValue(jsonObjPower);
+    jsonObjInner["device"] = new JSONValue(jsonObjDevice);
+    jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
+
+    // create json output structure
+    JSONObject jsonObjOuter;
+    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
+    jsonObjOuter["status"] = new JSONValue("ok");
+    // serialize and write it to the stream
+    JSONValue *value = new JSONValue(jsonObjOuter);
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
+    delete value;
 }
 
-void handleNodes(HTTPRequest *req, HTTPResponse *res) {
-  ResourceParameters *params = req->getParams();
-  std::string content;
+void handleNodes(HTTPRequest *req, HTTPResponse *res)
+{
+    ResourceParameters *params = req->getParams();
+    std::string content;
 
-  if (!params->getQueryParameter("content", content)) {
-    content = "json";
-  }
-
-  if (content == "json") {
-    res->setHeader("Content-Type", "application/json");
-    res->setHeader("Access-Control-Allow-Origin", "*");
-    res->setHeader("Access-Control-Allow-Methods", "GET");
-  } else {
-    res->setHeader("Content-Type", "text/html");
-    res->println("
");
-  }
-
-  JSONArray nodesArray;
-
-  uint32_t readIndex = 0;
-  const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
-  while (tempNodeInfo != NULL) {
-    if (tempNodeInfo->has_user) {
-      JSONObject node;
-
-      char id[16];
-      snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
-
-      node["id"] = new JSONValue(id);
-      node["snr"] = new JSONValue(tempNodeInfo->snr);
-      node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
-      node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
-      node["position"] = new JSONValue();
-
-      if (nodeDB->hasValidPosition(tempNodeInfo)) {
-        JSONObject position;
-        position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
-        position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
-        position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
-        node["position"] = new JSONValue(position);
-      }
-
-      node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
-      node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
-      char macStr[18];
-      snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0], tempNodeInfo->user.macaddr[1],
-               tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3], tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
-      node["mac_address"] = new JSONValue(macStr);
-      node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
-
-      nodesArray.push_back(new JSONValue(node));
+    if (!params->getQueryParameter("content", content)) {
+        content = "json";
     }
-    tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
-  }
 
-  // collect data to inner data object
-  JSONObject jsonObjInner;
-  jsonObjInner["nodes"] = new JSONValue(nodesArray);
+    if (content == "json") {
+        res->setHeader("Content-Type", "application/json");
+        res->setHeader("Access-Control-Allow-Origin", "*");
+        res->setHeader("Access-Control-Allow-Methods", "GET");
+    } else {
+        res->setHeader("Content-Type", "text/html");
+        res->println("
");
+    }
 
-  // create json output structure
-  JSONObject jsonObjOuter;
-  jsonObjOuter["data"] = new JSONValue(jsonObjInner);
-  jsonObjOuter["status"] = new JSONValue("ok");
-  // serialize and write it to the stream
-  JSONValue *value = new JSONValue(jsonObjOuter);
-  std::string jsonString = value->Stringify();
-  res->print(jsonString.c_str());
-  delete value;
+    JSONArray nodesArray;
 
-  // Clean up the nodesArray to prevent memory leak
-  for (auto *val : nodesArray) {
-    delete val;
-  }
+    uint32_t readIndex = 0;
+    const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
+    while (tempNodeInfo != NULL) {
+        if (tempNodeInfo->has_user) {
+            JSONObject node;
+
+            char id[16];
+            snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
+
+            node["id"] = new JSONValue(id);
+            node["snr"] = new JSONValue(tempNodeInfo->snr);
+            node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
+            node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
+            node["position"] = new JSONValue();
+
+            if (nodeDB->hasValidPosition(tempNodeInfo)) {
+                JSONObject position;
+                position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
+                position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
+                position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
+                node["position"] = new JSONValue(position);
+            }
+
+            node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
+            node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
+            char macStr[18];
+            snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
+                     tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3],
+                     tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
+            node["mac_address"] = new JSONValue(macStr);
+            node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
+
+            nodesArray.push_back(new JSONValue(node));
+        }
+        tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
+    }
+
+    // collect data to inner data object
+    JSONObject jsonObjInner;
+    jsonObjInner["nodes"] = new JSONValue(nodesArray);
+
+    // create json output structure
+    JSONObject jsonObjOuter;
+    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
+    jsonObjOuter["status"] = new JSONValue("ok");
+    // serialize and write it to the stream
+    JSONValue *value = new JSONValue(jsonObjOuter);
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
+    delete value;
+
+    // Clean up the nodesArray to prevent memory leak
+    for (auto *val : nodesArray) {
+        delete val;
+    }
 }
 
 /*
     This supports the Apple Captive Network Assistant (CNA) Portal
 */
-void handleHotspot(HTTPRequest *req, HTTPResponse *res) {
-  LOG_INFO("Hotspot Request");
+void handleHotspot(HTTPRequest *req, HTTPResponse *res)
+{
+    LOG_INFO("Hotspot Request");
 
-  /*
-      If we don't do a redirect, be sure to return a "Success" message
-      otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
-  */
+    /*
+        If we don't do a redirect, be sure to return a "Success" message
+        otherwise iOS will have trouble detecting that the connection to the SoftAP worked.
+    */
 
-  // Status code is 200 OK by default.
-  // We want to deliver a simple HTML page, so we send a corresponding content type:
-  res->setHeader("Content-Type", "text/html");
-  res->setHeader("Access-Control-Allow-Origin", "*");
-  res->setHeader("Access-Control-Allow-Methods", "GET");
+    // Status code is 200 OK by default.
+    // We want to deliver a simple HTML page, so we send a corresponding content type:
+    res->setHeader("Content-Type", "text/html");
+    res->setHeader("Access-Control-Allow-Origin", "*");
+    res->setHeader("Access-Control-Allow-Methods", "GET");
 
-  // res->println("");
-  res->println("");
+    // res->println("");
+    res->println("");
 }
 
-void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res) {
-  res->setHeader("Content-Type", "text/html");
-  res->setHeader("Access-Control-Allow-Origin", "*");
-  res->setHeader("Access-Control-Allow-Methods", "GET");
+void handleDeleteFsContent(HTTPRequest *req, HTTPResponse *res)
+{
+    res->setHeader("Content-Type", "text/html");
+    res->setHeader("Access-Control-Allow-Origin", "*");
+    res->setHeader("Access-Control-Allow-Methods", "GET");
 
-  res->println("

Meshtastic

"); - res->println("Delete Content in /static/*"); + res->println("

Meshtastic

"); + res->println("Delete Content in /static/*"); - LOG_INFO("Delete files from /static/* : "); + LOG_INFO("Delete files from /static/* : "); - concurrency::LockGuard g(spiLock); - htmlDeleteDir("/static"); + concurrency::LockGuard g(spiLock); + htmlDeleteDir("/static"); - res->println("


Back to admin"); + res->println("


Back to admin"); } -void handleAdmin(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleAdmin(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - // res->println("Settings
"); - // res->println("Manage Web Content
"); - res->println("Device Report
"); + res->println("

Meshtastic

"); + // res->println("Settings
"); + // res->println("Manage Web Content
"); + res->println("Device Report
"); } -void handleAdminSettings(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleAdminSettings(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - res->println("This isn't done."); - res->println("
"); - res->println("
"); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println("
Set?Settingcurrent valuenew value
WiFi SSIDfalse
WiFi Passwordfalse
Smart Position Updatefalse
"); - res->println(""); - res->println(""); - res->println(""); - res->println("


Back to admin"); + res->println("

Meshtastic

"); + res->println("This isn't done."); + res->println(""); + res->println("
"); + res->println(""); + res->println(""); + res->println(""); + res->println( + ""); + res->println("
Set?Settingcurrent valuenew value
WiFi SSIDfalse
WiFi Passwordfalse
Smart Position Updatefalse
"); + res->println(""); + res->println(""); + res->println(""); + res->println("


Back to admin"); } -void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "POST"); - res->println("

Meshtastic

"); - res->println("Settings Applied. "); +void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "POST"); + res->println("

Meshtastic

"); + res->println( + "Settings Applied. "); - res->println("Settings Applied. Please wait."); + res->println("Settings Applied. Please wait."); } -void handleFs(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleFs(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - res->println("Delete Web Content

Be patient!"); - res->println("


Back to admin"); + res->println("

Meshtastic

"); + res->println("Delete Web Content

Be patient!"); + res->println("


Back to admin"); } -void handleRestart(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "text/html"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); +void handleRestart(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); - res->println("

Meshtastic

"); - res->println("Restarting"); + res->println("

Meshtastic

"); + res->println("Restarting"); - LOG_DEBUG("Restarted on HTTP(s) Request"); - webServerThread->requestRestart = (millis() / 1000) + 5; + LOG_DEBUG("Restarted on HTTP(s) Request"); + webServerThread->requestRestart = (millis() / 1000) + 5; } -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "POST"); +void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "POST"); - ResourceParameters *params = req->getParams(); - std::string blink_target; + ResourceParameters *params = req->getParams(); + std::string blink_target; - if (!params->getQueryParameter("blink_target", blink_target)) { - // if no blink_target was supplied in the URL parameters of the - // POST request, then assume we should blink the LED - blink_target = "LED"; - } - - if (blink_target == "LED") { - uint8_t count = 10; - while (count > 0) { - ledBlink.set(true); - delay(50); - ledBlink.set(false); - delay(50); - count = count - 1; + if (!params->getQueryParameter("blink_target", blink_target)) { + // if no blink_target was supplied in the URL parameters of the + // POST request, then assume we should blink the LED + blink_target = "LED"; } - } else { + + if (blink_target == "LED") { + uint8_t count = 10; + while (count > 0) { + ledBlink.set(true); + delay(50); + ledBlink.set(false); + delay(50); + count = count - 1; + } + } else { #if HAS_SCREEN - if (screen) - screen->blink(); + if (screen) + screen->blink(); #endif - } + } - JSONObject jsonObjOuter; - jsonObjOuter["status"] = new JSONValue("ok"); - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; + JSONObject jsonObjOuter; + jsonObjOuter["status"] = new JSONValue("ok"); + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; } -void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) { - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "GET"); - // res->setHeader("Content-Type", "text/html"); +void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + // res->setHeader("Content-Type", "text/html"); - int n = WiFi.scanNetworks(); + int n = WiFi.scanNetworks(); - // build list of network objects - JSONArray networkObjs; - if (n > 0) { - for (int i = 0; i < n; ++i) { - char ssidArray[50]; - String ssidString = String(WiFi.SSID(i)); - ssidString.replace("\"", "\\\""); - ssidString.toCharArray(ssidArray, 50); + // build list of network objects + JSONArray networkObjs; + if (n > 0) { + for (int i = 0; i < n; ++i) { + char ssidArray[50]; + String ssidString = String(WiFi.SSID(i)); + ssidString.replace("\"", "\\\""); + ssidString.toCharArray(ssidArray, 50); - if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) { - JSONObject thisNetwork; - thisNetwork["ssid"] = new JSONValue(ssidArray); - thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i))); - networkObjs.push_back(new JSONValue(thisNetwork)); - } - // Yield some cpu cycles to IP stack. - // This is important in case the list is large and it takes us time to return - // to the main loop. - yield(); + if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) { + JSONObject thisNetwork; + thisNetwork["ssid"] = new JSONValue(ssidArray); + thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i))); + networkObjs.push_back(new JSONValue(thisNetwork)); + } + // Yield some cpu cycles to IP stack. + // This is important in case the list is large and it takes us time to return + // to the main loop. + yield(); + } } - } - // build output structure - JSONObject jsonObjOuter; - jsonObjOuter["data"] = new JSONValue(networkObjs); - jsonObjOuter["status"] = new JSONValue("ok"); + // build output structure + JSONObject jsonObjOuter; + jsonObjOuter["data"] = new JSONValue(networkObjs); + jsonObjOuter["status"] = new JSONValue("ok"); - // serialize and write it to the stream - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObjOuter); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); + delete value; - // Clean up the networkObjs to prevent memory leak - for (auto *val : networkObjs) { - delete val; - } + // Clean up the networkObjs to prevent memory leak + for (auto *val : networkObjs) { + delete val; + } } #endif \ No newline at end of file diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h index 1b4c84a6a..91cad3359 100644 --- a/src/mesh/http/ContentHandler.h +++ b/src/mesh/http/ContentHandler.h @@ -22,15 +22,16 @@ void handleAdminSettings(HTTPRequest *req, HTTPResponse *res); void handleAdminSettingsApply(HTTPRequest *req, HTTPResponse *res); // Interface to the PhoneAPI to access the protobufs with messages -class HttpAPI : public PhoneAPI { +class HttpAPI : public PhoneAPI +{ -public: - HttpAPI() { api_type = TYPE_HTTP; } + public: + HttpAPI() { api_type = TYPE_HTTP; } -private: - // Nothing here yet + private: + // Nothing here yet -protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this + protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; \ No newline at end of file diff --git a/src/mesh/http/ContentHelper.cpp b/src/mesh/http/ContentHelper.cpp index bdd2eefb8..8f283932b 100644 --- a/src/mesh/http/ContentHelper.cpp +++ b/src/mesh/http/ContentHelper.cpp @@ -2,12 +2,13 @@ // #include // #include "main.h" -void replaceAll(std::string &str, const std::string &from, const std::string &to) { - if (from.empty()) - return; - size_t start_pos = 0; - while ((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' - } +void replaceAll(std::string &str, const std::string &from, const std::string &to) +{ + if (from.empty()) + return; + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } } diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 15be54f56..3a264fa5a 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -62,19 +62,21 @@ static HTTPServer *insecureServer; volatile bool isWebServerReady; volatile bool isCertReady; -static void handleWebResponse() { - if (isWifiAvailable()) { +static void handleWebResponse() +{ + if (isWifiAvailable()) { - if (isWebServerReady) { - if (secureServer) - secureServer->loop(); - insecureServer->loop(); + if (isWebServerReady) { + if (secureServer) + secureServer->loop(); + insecureServer->loop(); + } } - } } -static void taskCreateCert(void *parameter) { - prefs.begin("MeshtasticHTTPS", false); +static void taskCreateCert(void *parameter) +{ + prefs.begin("MeshtasticHTTPS", false); #if 0 // Delete the saved certs (used in debugging) @@ -84,156 +86,165 @@ static void taskCreateCert(void *parameter) { prefs.remove("cert"); #endif - LOG_INFO("Checking if we have a saved SSL Certificate"); + LOG_INFO("Checking if we have a saved SSL Certificate"); - size_t pkLen = prefs.getBytesLength("PK"); - size_t certLen = prefs.getBytesLength("cert"); + size_t pkLen = prefs.getBytesLength("PK"); + size_t certLen = prefs.getBytesLength("cert"); - if (pkLen && certLen) { - LOG_INFO("Existing SSL Certificate found!"); + if (pkLen && certLen) { + LOG_INFO("Existing SSL Certificate found!"); - uint8_t *pkBuffer = new uint8_t[pkLen]; - prefs.getBytes("PK", pkBuffer, pkLen); + uint8_t *pkBuffer = new uint8_t[pkLen]; + prefs.getBytes("PK", pkBuffer, pkLen); - uint8_t *certBuffer = new uint8_t[certLen]; - prefs.getBytes("cert", certBuffer, certLen); + uint8_t *certBuffer = new uint8_t[certLen]; + prefs.getBytes("cert", certBuffer, certLen); - cert = new SSLCert(certBuffer, certLen, pkBuffer, pkLen); + cert = new SSLCert(certBuffer, certLen, pkBuffer, pkLen); - LOG_DEBUG("Retrieved Private Key: %d Bytes", cert->getPKLength()); - LOG_DEBUG("Retrieved Certificate: %d Bytes", cert->getCertLength()); - } else { - - LOG_INFO("Creating the certificate. This may take a while. Please wait"); - yield(); - cert = new SSLCert(); - yield(); - int createCertResult = createSelfSignedCert(*cert, KEYSIZE_2048, "CN=meshtastic.local,O=Meshtastic,C=US", "20190101000000", "20300101000000"); - yield(); - - if (createCertResult != 0) { - LOG_ERROR("Creating the certificate failed"); + LOG_DEBUG("Retrieved Private Key: %d Bytes", cert->getPKLength()); + LOG_DEBUG("Retrieved Certificate: %d Bytes", cert->getCertLength()); } else { - LOG_INFO("Creating the certificate was successful"); - LOG_DEBUG("Created Private Key: %d Bytes", cert->getPKLength()); + LOG_INFO("Creating the certificate. This may take a while. Please wait"); + yield(); + cert = new SSLCert(); + yield(); + int createCertResult = createSelfSignedCert(*cert, KEYSIZE_2048, "CN=meshtastic.local,O=Meshtastic,C=US", + "20190101000000", "20300101000000"); + yield(); - LOG_DEBUG("Created Certificate: %d Bytes", cert->getCertLength()); + if (createCertResult != 0) { + LOG_ERROR("Creating the certificate failed"); + } else { + LOG_INFO("Creating the certificate was successful"); - prefs.putBytes("PK", (uint8_t *)cert->getPKData(), cert->getPKLength()); - prefs.putBytes("cert", (uint8_t *)cert->getCertData(), cert->getCertLength()); + LOG_DEBUG("Created Private Key: %d Bytes", cert->getPKLength()); + + LOG_DEBUG("Created Certificate: %d Bytes", cert->getCertLength()); + + prefs.putBytes("PK", (uint8_t *)cert->getPKData(), cert->getPKLength()); + prefs.putBytes("cert", (uint8_t *)cert->getCertData(), cert->getCertLength()); + } } - } - isCertReady = true; + isCertReady = true; - // Must delete self, can't just fall out - vTaskDelete(NULL); + // Must delete self, can't just fall out + vTaskDelete(NULL); } -void createSSLCert() { - if (isWifiAvailable() && !isCertReady) { - bool runLoop = false; +void createSSLCert() +{ + if (isWifiAvailable() && !isCertReady) { + bool runLoop = false; - // Create a new process just to handle creating the cert. - // This is a workaround for Bug: https://github.com/fhessel/esp32_https_server/issues/48 - // jm@casler.org (Oct 2020) - xTaskCreate(taskCreateCert, /* Task function. */ - "createCert", /* String with name of task. */ - // 16384, /* Stack size in bytes. */ - 8192, /* Stack size in bytes. */ - NULL, /* Parameter passed as input of the task */ - 16, /* Priority of the task. */ - NULL); /* Task handle. */ + // Create a new process just to handle creating the cert. + // This is a workaround for Bug: https://github.com/fhessel/esp32_https_server/issues/48 + // jm@casler.org (Oct 2020) + xTaskCreate(taskCreateCert, /* Task function. */ + "createCert", /* String with name of task. */ + // 16384, /* Stack size in bytes. */ + 8192, /* Stack size in bytes. */ + NULL, /* Parameter passed as input of the task */ + 16, /* Priority of the task. */ + NULL); /* Task handle. */ - LOG_DEBUG("Waiting for SSL Cert to be generated"); - while (!isCertReady) { - if ((millis() / 500) % 2) { - if (runLoop) { - LOG_DEBUG("."); + LOG_DEBUG("Waiting for SSL Cert to be generated"); + while (!isCertReady) { + if ((millis() / 500) % 2) { + if (runLoop) { + LOG_DEBUG("."); - yield(); - esp_task_wdt_reset(); + yield(); + esp_task_wdt_reset(); #if HAS_SCREEN - if (millis() / 1000 >= 3) { - if (screen) - screen->setSSLFrames(); - } + if (millis() / 1000 >= 3) { + if (screen) + screen->setSSLFrames(); + } #endif + } + runLoop = false; + } else { + runLoop = true; + } } - runLoop = false; - } else { - runLoop = true; - } + LOG_INFO("SSL Cert Ready!"); } - LOG_INFO("SSL Cert Ready!"); - } } WebServerThread *webServerThread; -WebServerThread::WebServerThread() : concurrency::OSThread("WebServer") { - if (!config.network.wifi_enabled && !config.network.eth_enabled) { - disable(); - } - lastActivityTime = millis(); +WebServerThread::WebServerThread() : concurrency::OSThread("WebServer") +{ + if (!config.network.wifi_enabled && !config.network.eth_enabled) { + disable(); + } + lastActivityTime = millis(); } -void WebServerThread::markActivity() { lastActivityTime = millis(); } - -int32_t WebServerThread::getAdaptiveInterval() { - uint32_t currentTime = millis(); - uint32_t timeSinceActivity; - - if (currentTime >= lastActivityTime) { - timeSinceActivity = currentTime - lastActivityTime; - } else { - timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1; - } - - if (timeSinceActivity < ACTIVE_THRESHOLD_MS) { - return ACTIVE_INTERVAL_MS; - } else if (timeSinceActivity < MEDIUM_THRESHOLD_MS) { - return MEDIUM_INTERVAL_MS; - } else { - return IDLE_INTERVAL_MS; - } +void WebServerThread::markActivity() +{ + lastActivityTime = millis(); } -int32_t WebServerThread::runOnce() { - if (!config.network.wifi_enabled && !config.network.eth_enabled) { - disable(); - } +int32_t WebServerThread::getAdaptiveInterval() +{ + uint32_t currentTime = millis(); + uint32_t timeSinceActivity; - handleWebResponse(); + if (currentTime >= lastActivityTime) { + timeSinceActivity = currentTime - lastActivityTime; + } else { + timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1; + } - if (requestRestart && (millis() / 1000) > requestRestart) { - ESP.restart(); - } - - return getAdaptiveInterval(); + if (timeSinceActivity < ACTIVE_THRESHOLD_MS) { + return ACTIVE_INTERVAL_MS; + } else if (timeSinceActivity < MEDIUM_THRESHOLD_MS) { + return MEDIUM_INTERVAL_MS; + } else { + return IDLE_INTERVAL_MS; + } } -void initWebServer() { - LOG_DEBUG("Init Web Server"); +int32_t WebServerThread::runOnce() +{ + if (!config.network.wifi_enabled && !config.network.eth_enabled) { + disable(); + } - // We can now use the new certificate to setup our server as usual. - secureServer = new HTTPSServer(cert); - insecureServer = new HTTPServer(); + handleWebResponse(); - registerHandlers(insecureServer, secureServer); + if (requestRestart && (millis() / 1000) > requestRestart) { + ESP.restart(); + } - if (secureServer) { - LOG_INFO("Start Secure Web Server"); - secureServer->start(); - } - LOG_INFO("Start Insecure Web Server"); - insecureServer->start(); - if (insecureServer->isRunning()) { - LOG_INFO("Web Servers Ready! :-) "); - isWebServerReady = true; - } else { - LOG_ERROR("Web Servers Failed! ;-( "); - } + return getAdaptiveInterval(); +} + +void initWebServer() +{ + LOG_DEBUG("Init Web Server"); + + // We can now use the new certificate to setup our server as usual. + secureServer = new HTTPSServer(cert); + insecureServer = new HTTPServer(); + + registerHandlers(insecureServer, secureServer); + + if (secureServer) { + LOG_INFO("Start Secure Web Server"); + secureServer->start(); + } + LOG_INFO("Start Insecure Web Server"); + insecureServer->start(); + if (insecureServer->isRunning()) { + LOG_INFO("Web Servers Ready! :-) "); + isWebServerReady = true; + } else { + LOG_ERROR("Web Servers Failed! ;-( "); + } } #endif diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index fa50a86db..e7a29a5a7 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -8,18 +8,19 @@ void initWebServer(); void createSSLCert(); -class WebServerThread : private concurrency::OSThread { -private: - uint32_t lastActivityTime = 0; +class WebServerThread : private concurrency::OSThread +{ + private: + uint32_t lastActivityTime = 0; -public: - WebServerThread(); - uint32_t requestRestart = 0; - void markActivity(); + public: + WebServerThread(); + uint32_t requestRestart = 0; + void markActivity(); -protected: - virtual int32_t runOnce() override; - int32_t getAdaptiveInterval(); + protected: + virtual int32_t runOnce() override; + int32_t getAdaptiveInterval(); }; extern WebServerThread *webServerThread; diff --git a/src/mesh/mesh-pb-constants.cpp b/src/mesh/mesh-pb-constants.cpp index cb79de5c3..a8f4fd6d8 100644 --- a/src/mesh/mesh-pb-constants.cpp +++ b/src/mesh/mesh-pb-constants.cpp @@ -9,62 +9,67 @@ /// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic /// returns the encoded packet size -size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct) { - pb_ostream_t stream = pb_ostream_from_buffer(destbuf, destbufsize); - if (!pb_encode(&stream, fields, src_struct)) { - LOG_ERROR("Panic: can't encode protobuf reason='%s'", PB_GET_ERROR(&stream)); - return 0; - } else { - return stream.bytes_written; - } +size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct) +{ + pb_ostream_t stream = pb_ostream_from_buffer(destbuf, destbufsize); + if (!pb_encode(&stream, fields, src_struct)) { + LOG_ERROR("Panic: can't encode protobuf reason='%s'", PB_GET_ERROR(&stream)); + return 0; + } else { + return stream.bytes_written; + } } /// helper function for decoding a record as a protobuf, we will return false if the decoding failed -bool pb_decode_from_bytes(const uint8_t *srcbuf, size_t srcbufsize, const pb_msgdesc_t *fields, void *dest_struct) { - pb_istream_t stream = pb_istream_from_buffer(srcbuf, srcbufsize); - if (!pb_decode(&stream, fields, dest_struct)) { - LOG_ERROR("Can't decode protobuf reason='%s', pb_msgdesc %p", PB_GET_ERROR(&stream), fields); - return false; - } else { - return true; - } +bool pb_decode_from_bytes(const uint8_t *srcbuf, size_t srcbufsize, const pb_msgdesc_t *fields, void *dest_struct) +{ + pb_istream_t stream = pb_istream_from_buffer(srcbuf, srcbufsize); + if (!pb_decode(&stream, fields, dest_struct)) { + LOG_ERROR("Can't decode protobuf reason='%s', pb_msgdesc %p", PB_GET_ERROR(&stream), fields); + return false; + } else { + return true; + } } #ifdef FSCom /// Read from an Arduino File -bool readcb(pb_istream_t *stream, uint8_t *buf, size_t count) { - bool status = false; - File *file = (File *)stream->state; +bool readcb(pb_istream_t *stream, uint8_t *buf, size_t count) +{ + bool status = false; + File *file = (File *)stream->state; - if (buf == NULL) { - while (count-- && file->read() != EOF) - ; - return count == 0; - } + if (buf == NULL) { + while (count-- && file->read() != EOF) + ; + return count == 0; + } - status = (file->read(buf, count) == (int)count); + status = (file->read(buf, count) == (int)count); - if (file->available() == 0) - stream->bytes_left = 0; + if (file->available() == 0) + stream->bytes_left = 0; - return status; + return status; } /// Write to an arduino file -bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count) { - spiLock->lock(); - auto file = (Print *)stream->state; - // LOG_DEBUG("writing %d bytes to protobuf file", count); - bool status = file->write(buf, count) == count; - spiLock->unlock(); - return status; +bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count) +{ + spiLock->lock(); + auto file = (Print *)stream->state; + // LOG_DEBUG("writing %d bytes to protobuf file", count); + bool status = file->write(buf, count) == count; + spiLock->unlock(); + return status; } #endif -bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count) { - for (pb_size_t i = 0; i < count; i++) - if (array[i] == n) - return true; +bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count) +{ + for (pb_size_t i = 0; i < count; i++) + if (array[i] == n) + return true; - return false; + return false; } \ No newline at end of file diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index f07f6a228..e4f65aa28 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -11,11 +11,9 @@ // Tricky macro to let you find the sizeof a type member #define member_size(type, member) sizeof(((type *)0)->member) -/// max number of packets which can be waiting for delivery to android - note, this value comes from mesh.options -/// protobuf -// FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big -// array in RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, -// receive_queue[0])) +/// max number of packets which can be waiting for delivery to android - note, this value comes from mesh.options protobuf +// FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in +// RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) #ifndef MAX_RX_TOPHONE #if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) #define MAX_RX_TOPHONE 8 @@ -51,15 +49,16 @@ static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increas #define MAX_NUM_NODES 80 #elif defined(CONFIG_IDF_TARGET_ESP32S3) #include "Esp.h" -static inline int get_max_num_nodes() { - uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Convert Bytes to MB - if (flash_size >= 15) { - return 250; - } else if (flash_size >= 7) { - return 200; - } else { - return 100; - } +static inline int get_max_num_nodes() +{ + uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Convert Bytes to MB + if (flash_size >= 15) { + return 250; + } else if (flash_size >= 7) { + return 200; + } else { + return 100; + } } #define MAX_NUM_NODES get_max_num_nodes() #else diff --git a/src/mesh/raspihttp/PiWebServer.cpp b/src/mesh/raspihttp/PiWebServer.cpp index e34e4c285..3e9dbe8c2 100644 --- a/src/mesh/raspihttp/PiWebServer.cpp +++ b/src/mesh/raspihttp/PiWebServer.cpp @@ -90,126 +90,130 @@ PiWebServerThread *piwebServerThread; /** * Return the filename extension */ -const char *get_filename_ext(const char *path) { - const char *dot = strrchr(path, '.'); - if (!dot || dot == path) - return "*"; - if (strchr(dot, '?') != NULL) { - //*strchr(dot, '?') = '\0'; - const char *empty = "\0"; - return empty; - } - return dot; +const char *get_filename_ext(const char *path) +{ + const char *dot = strrchr(path, '.'); + if (!dot || dot == path) + return "*"; + if (strchr(dot, '?') != NULL) { + //*strchr(dot, '?') = '\0'; + const char *empty = "\0"; + return empty; + } + return dot; } /** * Streaming callback function to ease sending large files */ -static ssize_t callback_static_file_stream(void *cls, uint64_t pos, char *buf, size_t max) { - (void)(pos); - if (cls != NULL) { - return fread(buf, 1, max, (FILE *)cls); - } else { - return U_STREAM_END; - } +static ssize_t callback_static_file_stream(void *cls, uint64_t pos, char *buf, size_t max) +{ + (void)(pos); + if (cls != NULL) { + return fread(buf, 1, max, (FILE *)cls); + } else { + return U_STREAM_END; + } } /** * Cleanup FILE* structure when streaming is complete */ -static void callback_static_file_stream_free(void *cls) { - if (cls != NULL) { - fclose((FILE *)cls); - } +static void callback_static_file_stream_free(void *cls) +{ + if (cls != NULL) { + fclose((FILE *)cls); + } } /** * static file callback endpoint that delivers the content for WebServer calls */ -int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data) { - size_t length; - FILE *f; - char *file_requested, *file_path, *url_dup_save, *real_path = NULL; - const char *content_type; +int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data) +{ + size_t length; + FILE *f; + char *file_requested, *file_path, *url_dup_save, *real_path = NULL; + const char *content_type; - /* - * Comment this if statement if you don't access static files url from root dir, like /app - */ - if (request->callback_position > 0) { - return U_CALLBACK_CONTINUE; - } else if (user_data != NULL && (configWeb.files_path != NULL)) { - file_requested = o_strdup(request->http_url); - url_dup_save = file_requested; + /* + * Comment this if statement if you don't access static files url from root dir, like /app + */ + if (request->callback_position > 0) { + return U_CALLBACK_CONTINUE; + } else if (user_data != NULL && (configWeb.files_path != NULL)) { + file_requested = o_strdup(request->http_url); + url_dup_save = file_requested; - while (file_requested[0] == '/') { - file_requested++; - } - file_requested += o_strlen(configWeb.url_prefix); - while (file_requested[0] == '/') { - file_requested++; - } - - if (strchr(file_requested, '#') != NULL) { - *strchr(file_requested, '#') = '\0'; - } - - if (strchr(file_requested, '?') != NULL) { - *strchr(file_requested, '?') = '\0'; - } - - if (file_requested == NULL || o_strlen(file_requested) == 0 || 0 == o_strcmp("/", file_requested)) { - o_free(url_dup_save); - url_dup_save = file_requested = o_strdup("index.html"); - } - - file_path = msprintf("%s/%s", configWeb.files_path, file_requested); - real_path = realpath(file_path, NULL); - if (0 == o_strncmp(configWeb.files_path, real_path, o_strlen(configWeb.files_path))) { - if (access(file_path, F_OK) != -1) { - f = fopen(file_path, "rb"); - if (f) { - fseek(f, 0, SEEK_END); - length = ftell(f); - fseek(f, 0, SEEK_SET); - - content_type = u_map_get_case(&configWeb.mime_types, get_filename_ext(file_requested)); - if (content_type == NULL) { - content_type = u_map_get(&configWeb.mime_types, "*"); - LOG_DEBUG("Static File Server - Unknown mime type for extension %s ", get_filename_ext(file_requested)); - } - u_map_put(response->map_header, "Content-Type", content_type); - u_map_copy_into(response->map_header, &configWeb.map_header); - - if (ulfius_set_stream_response(response, 200, callback_static_file_stream, callback_static_file_stream_free, length, STATIC_FILE_CHUNK, - f) != U_OK) { - LOG_DEBUG("callback_static_file - Error ulfius_set_stream_response"); - } + while (file_requested[0] == '/') { + file_requested++; } - } else { - if (configWeb.redirect_on_404 == NULL) { - ulfius_set_string_body_response(response, 404, "File not found"); + file_requested += o_strlen(configWeb.url_prefix); + while (file_requested[0] == '/') { + file_requested++; + } + + if (strchr(file_requested, '#') != NULL) { + *strchr(file_requested, '#') = '\0'; + } + + if (strchr(file_requested, '?') != NULL) { + *strchr(file_requested, '?') = '\0'; + } + + if (file_requested == NULL || o_strlen(file_requested) == 0 || 0 == o_strcmp("/", file_requested)) { + o_free(url_dup_save); + url_dup_save = file_requested = o_strdup("index.html"); + } + + file_path = msprintf("%s/%s", configWeb.files_path, file_requested); + real_path = realpath(file_path, NULL); + if (0 == o_strncmp(configWeb.files_path, real_path, o_strlen(configWeb.files_path))) { + if (access(file_path, F_OK) != -1) { + f = fopen(file_path, "rb"); + if (f) { + fseek(f, 0, SEEK_END); + length = ftell(f); + fseek(f, 0, SEEK_SET); + + content_type = u_map_get_case(&configWeb.mime_types, get_filename_ext(file_requested)); + if (content_type == NULL) { + content_type = u_map_get(&configWeb.mime_types, "*"); + LOG_DEBUG("Static File Server - Unknown mime type for extension %s ", get_filename_ext(file_requested)); + } + u_map_put(response->map_header, "Content-Type", content_type); + u_map_copy_into(response->map_header, &configWeb.map_header); + + if (ulfius_set_stream_response(response, 200, callback_static_file_stream, callback_static_file_stream_free, + length, STATIC_FILE_CHUNK, f) != U_OK) { + LOG_DEBUG("callback_static_file - Error ulfius_set_stream_response"); + } + } + } else { + if (configWeb.redirect_on_404 == NULL) { + ulfius_set_string_body_response(response, 404, "File not found"); + } else { + ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); + response->status = 302; + } + } } else { - ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); - response->status = 302; + if (configWeb.redirect_on_404 == NULL) { + ulfius_set_string_body_response(response, 404, "File not found"); + } else { + ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); + response->status = 302; + } } - } - } else { - if (configWeb.redirect_on_404 == NULL) { - ulfius_set_string_body_response(response, 404, "File not found"); - } else { - ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404); - response->status = 302; - } - } - o_free(file_path); - o_free(url_dup_save); - free(real_path); // realpath uses malloc - return U_CALLBACK_CONTINUE; - } else { - LOG_DEBUG("Static File Server - Error, user_data is NULL or inconsistent"); - return U_CALLBACK_ERROR; - } + o_free(file_path); + o_free(url_dup_save); + free(real_path); // realpath uses malloc + return U_CALLBACK_CONTINUE; + } else { + LOG_DEBUG("Static File Server - Error, user_data is NULL or inconsistent"); + return U_CALLBACK_ERROR; + } } static void handleWebResponse() {} @@ -218,305 +222,316 @@ static void handleWebResponse() {} * Adapt the radioapi to the Webservice handleAPIv1ToRadio * Trigger : WebGui(SAVE)->WebServcice->phoneApi */ -int handleAPIv1ToRadio(const struct _u_request *req, struct _u_response *res, void *user_data) { - LOG_DEBUG("handleAPIv1ToRadio web -> radio "); +int handleAPIv1ToRadio(const struct _u_request *req, struct _u_response *res, void *user_data) +{ + LOG_DEBUG("handleAPIv1ToRadio web -> radio "); - ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Headers", "Content-Type"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "PUT, OPTIONS"); - ulfius_add_header_to_response(res, "X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Headers", "Content-Type"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "PUT, OPTIONS"); + ulfius_add_header_to_response(res, "X-Protobuf-Schema", + "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (strcmp(req->http_verb, "OPTIONS") == 0) { - ulfius_set_response_properties(res, U_OPT_STATUS, 204); + if (strcmp(req->http_verb, "OPTIONS") == 0) { + ulfius_set_response_properties(res, U_OPT_STATUS, 204); + return U_CALLBACK_COMPLETE; + } + + byte buffer[MAX_TO_FROM_RADIO_SIZE]; + size_t s = req->binary_body_length; + + memcpy(buffer, req->binary_body, MAX_TO_FROM_RADIO_SIZE); + + // FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread + + portduinoVFS->mountpoint(configWeb.rootPath); + + LOG_DEBUG("Received %d bytes from PUT request", s); + static_cast(user_data)->handleToRadio(buffer, s); + LOG_DEBUG("end web->radio "); return U_CALLBACK_COMPLETE; - } - - byte buffer[MAX_TO_FROM_RADIO_SIZE]; - size_t s = req->binary_body_length; - - memcpy(buffer, req->binary_body, MAX_TO_FROM_RADIO_SIZE); - - // FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread - - portduinoVFS->mountpoint(configWeb.rootPath); - - LOG_DEBUG("Received %d bytes from PUT request", s); - static_cast(user_data)->handleToRadio(buffer, s); - LOG_DEBUG("end web->radio "); - return U_CALLBACK_COMPLETE; } /* * Adapt the radioapi to the Webservice handleAPIv1FromRadio * Trigger : WebGui(POLL)->handleAPIv1FromRadio->phoneapi->Meshtastic(Radio) events */ -int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data) { +int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data) +{ - // LOG_DEBUG("handleAPIv1FromRadio radio -> web"); - std::string valueAll; + // LOG_DEBUG("handleAPIv1FromRadio radio -> web"); + std::string valueAll; - // Status code is 200 OK by default. - ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); - ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "GET"); - ulfius_add_header_to_response(res, "X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); + // Status code is 200 OK by default. + ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*"); + ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "GET"); + ulfius_add_header_to_response(res, "X-Protobuf-Schema", + "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto"); - if (strcmp(req->http_verb, "OPTIONS") == 0) { - ulfius_set_response_properties(res, U_OPT_STATUS, 204); - return U_CALLBACK_COMPLETE; - } - - uint8_t txBuf[MAX_STREAM_BUF_SIZE]; - uint32_t len = 1; - - if (valueAll == "true") { - while (len) { - len = static_cast(user_data)->getFromRadio(txBuf); - ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len); - const char *tmpa = (const char *)txBuf; - ulfius_set_string_body_response(res, 200, tmpa); - // LOG_DEBUG("\n----webAPI response all:----"); - // LOG_DEBUG(tmpa); - // LOG_DEBUG(""); + if (strcmp(req->http_verb, "OPTIONS") == 0) { + ulfius_set_response_properties(res, U_OPT_STATUS, 204); + return U_CALLBACK_COMPLETE; } - // Otherwise, just return one protobuf - } else { - len = static_cast(user_data)->getFromRadio(txBuf); - const char *tmpa = (const char *)txBuf; - ulfius_set_binary_body_response(res, 200, tmpa, len); - // LOG_DEBUG("\n----webAPI response:"); - // LOG_DEBUG(tmpa); - // LOG_DEBUG(""); - } - // LOG_DEBUG("end radio->web", len); - return U_CALLBACK_COMPLETE; + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; + + if (valueAll == "true") { + while (len) { + len = static_cast(user_data)->getFromRadio(txBuf); + ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len); + const char *tmpa = (const char *)txBuf; + ulfius_set_string_body_response(res, 200, tmpa); + // LOG_DEBUG("\n----webAPI response all:----"); + // LOG_DEBUG(tmpa); + // LOG_DEBUG(""); + } + // Otherwise, just return one protobuf + } else { + len = static_cast(user_data)->getFromRadio(txBuf); + const char *tmpa = (const char *)txBuf; + ulfius_set_binary_body_response(res, 200, tmpa, len); + // LOG_DEBUG("\n----webAPI response:"); + // LOG_DEBUG(tmpa); + // LOG_DEBUG(""); + } + + // LOG_DEBUG("end radio->web", len); + return U_CALLBACK_COMPLETE; } /* OpenSSL RSA Key Gen */ -int generate_rsa_key(EVP_PKEY **pkey) { - EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); - if (!pkey_ctx) - return -1; - if (EVP_PKEY_keygen_init(pkey_ctx) <= 0) - return -1; - if (EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0) - return -1; - if (EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) - return -1; - EVP_PKEY_CTX_free(pkey_ctx); - return 0; // SUCCESS +int generate_rsa_key(EVP_PKEY **pkey) +{ + EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (!pkey_ctx) + return -1; + if (EVP_PKEY_keygen_init(pkey_ctx) <= 0) + return -1; + if (EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0) + return -1; + if (EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) + return -1; + EVP_PKEY_CTX_free(pkey_ctx); + return 0; // SUCCESS } -int generate_self_signed_x509(EVP_PKEY *pkey, X509 **x509) { - *x509 = X509_new(); - if (!*x509) - return -1; - if (X509_set_version(*x509, 2) != 1) - return -1; - ASN1_INTEGER_set(X509_get_serialNumber(*x509), 1); - X509_gmtime_adj(X509_get_notBefore(*x509), 0); - X509_gmtime_adj(X509_get_notAfter(*x509), 31536000L); // 1 YEAR ACCESS +int generate_self_signed_x509(EVP_PKEY *pkey, X509 **x509) +{ + *x509 = X509_new(); + if (!*x509) + return -1; + if (X509_set_version(*x509, 2) != 1) + return -1; + ASN1_INTEGER_set(X509_get_serialNumber(*x509), 1); + X509_gmtime_adj(X509_get_notBefore(*x509), 0); + X509_gmtime_adj(X509_get_notAfter(*x509), 31536000L); // 1 YEAR ACCESS - X509_set_pubkey(*x509, pkey); + X509_set_pubkey(*x509, pkey); - // SET Subject Name - X509_NAME *name = X509_get_subject_name(*x509); - X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"DE", -1, -1, 0); - X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Meshtastic", -1, -1, 0); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"meshtastic.local", -1, -1, 0); - // Selfsigned, Issuer = Subject - X509_set_issuer_name(*x509, name); + // SET Subject Name + X509_NAME *name = X509_get_subject_name(*x509); + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"DE", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Meshtastic", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"meshtastic.local", -1, -1, 0); + // Selfsigned, Issuer = Subject + X509_set_issuer_name(*x509, name); - // Certificate signed with our privte key - if (X509_sign(*x509, pkey, EVP_sha256()) <= 0) - return -1; + // Certificate signed with our privte key + if (X509_sign(*x509, pkey, EVP_sha256()) <= 0) + return -1; - return 0; + return 0; } -char *read_file_into_string(const char *filename) { - FILE *file = fopen(filename, "rb"); - if (file == NULL) { - LOG_ERROR("Error reading File : %s ", filename); - return NULL; - } +char *read_file_into_string(const char *filename) +{ + FILE *file = fopen(filename, "rb"); + if (file == NULL) { + LOG_ERROR("Error reading File : %s ", filename); + return NULL; + } - // Size of file - fseek(file, 0, SEEK_END); - long filesize = ftell(file); - rewind(file); + // Size of file + fseek(file, 0, SEEK_END); + long filesize = ftell(file); + rewind(file); - // reserve mem for file + 1 byte - char *buffer = (char *)malloc(filesize + 1); - if (buffer == NULL) { - LOG_ERROR("Malloc of mem failed for file : %s ", filename); + // reserve mem for file + 1 byte + char *buffer = (char *)malloc(filesize + 1); + if (buffer == NULL) { + LOG_ERROR("Malloc of mem failed for file : %s ", filename); + fclose(file); + return NULL; + } + + // read content + size_t readSize = fread(buffer, 1, filesize, file); + if (readSize != filesize) { + LOG_ERROR("Error reading file into buffer"); + free(buffer); + fclose(file); + return NULL; + } + + // add terminator sign at the end + buffer[filesize] = '\0'; fclose(file); - return NULL; - } - - // read content - size_t readSize = fread(buffer, 1, filesize, file); - if (readSize != filesize) { - LOG_ERROR("Error reading file into buffer"); - free(buffer); - fclose(file); - return NULL; - } - - // add terminator sign at the end - buffer[filesize] = '\0'; - fclose(file); - return buffer; // return pointer + return buffer; // return pointer } -int PiWebServerThread::CheckSSLandLoad() { - // read certificate - cert_pem = read_file_into_string(CERT_PATH); - if (cert_pem == NULL) { - LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing"); - return 1; - } - // read private key - key_pem = read_file_into_string(KEY_PATH); - if (key_pem == NULL) { - LOG_ERROR("ERROR file private_key can't be loaded or is missing"); - return 2; - } +int PiWebServerThread::CheckSSLandLoad() +{ + // read certificate + cert_pem = read_file_into_string(CERT_PATH); + if (cert_pem == NULL) { + LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing"); + return 1; + } + // read private key + key_pem = read_file_into_string(KEY_PATH); + if (key_pem == NULL) { + LOG_ERROR("ERROR file private_key can't be loaded or is missing"); + return 2; + } - return 0; + return 0; } -int PiWebServerThread::CreateSSLCertificate() { +int PiWebServerThread::CreateSSLCertificate() +{ - EVP_PKEY *pkey = NULL; - X509 *x509 = NULL; + EVP_PKEY *pkey = NULL; + X509 *x509 = NULL; - if (generate_rsa_key(&pkey) != 0) { - LOG_ERROR("Error generating RSA-Key"); - return 1; - } + if (generate_rsa_key(&pkey) != 0) { + LOG_ERROR("Error generating RSA-Key"); + return 1; + } - if (generate_self_signed_x509(pkey, &x509) != 0) { - LOG_ERROR("Error generating X509-Cert"); - return 2; - } + if (generate_self_signed_x509(pkey, &x509) != 0) { + LOG_ERROR("Error generating X509-Cert"); + return 2; + } - // Open file to write private key file - FILE *pkey_file = fopen(KEY_PATH, "wb"); - if (!pkey_file) { - LOG_ERROR("Error opening private key file"); - return 3; - } - // write private key file - PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL); - fclose(pkey_file); + // Open file to write private key file + FILE *pkey_file = fopen(KEY_PATH, "wb"); + if (!pkey_file) { + LOG_ERROR("Error opening private key file"); + return 3; + } + // write private key file + PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL); + fclose(pkey_file); - // open Certificate file - FILE *x509_file = fopen(CERT_PATH, "wb"); - if (!x509_file) { - LOG_ERROR("Error opening cert"); - return 4; - } - // write certificate - PEM_write_X509(x509_file, x509); - fclose(x509_file); + // open Certificate file + FILE *x509_file = fopen(CERT_PATH, "wb"); + if (!x509_file) { + LOG_ERROR("Error opening cert"); + return 4; + } + // write certificate + PEM_write_X509(x509_file, x509); + fclose(x509_file); - EVP_PKEY_free(pkey); - LOG_INFO("Create SSL Key %s successful", KEY_PATH); - X509_free(x509); - LOG_INFO("Create SSL Cert %s successful", CERT_PATH); - return 0; + EVP_PKEY_free(pkey); + LOG_INFO("Create SSL Key %s successful", KEY_PATH); + X509_free(x509); + LOG_INFO("Create SSL Cert %s successful", CERT_PATH); + return 0; } void initWebServer() {} -PiWebServerThread::PiWebServerThread() { - int ret, retssl, webservport; +PiWebServerThread::PiWebServerThread() +{ + int ret, retssl, webservport; - if (CheckSSLandLoad() != 0) { - CreateSSLCertificate(); if (CheckSSLandLoad() != 0) { - LOG_ERROR("Major Error Gen & Read SSL Certificate"); + CreateSSLCertificate(); + if (CheckSSLandLoad() != 0) { + LOG_ERROR("Major Error Gen & Read SSL Certificate"); + } } - } - if (portduino_config.webserverport != 0) { - webservport = portduino_config.webserverport; - LOG_INFO("Use webserver port from yaml config %i ", webservport); - } else { - LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443"); - webservport = 9443; - } - - // Web Content Service Instance - if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) { - LOG_ERROR("Webserver couldn't be started, abort execution"); - } else { - - LOG_INFO("Webserver started"); - u_map_init(&configWeb.mime_types); - u_map_put(&configWeb.mime_types, "*", "application/octet-stream"); - u_map_put(&configWeb.mime_types, ".html", "text/html"); - u_map_put(&configWeb.mime_types, ".htm", "text/html"); - u_map_put(&configWeb.mime_types, ".tsx", "application/javascript"); - u_map_put(&configWeb.mime_types, ".ts", "application/javascript"); - u_map_put(&configWeb.mime_types, ".css", "text/css"); - u_map_put(&configWeb.mime_types, ".js", "application/javascript"); - u_map_put(&configWeb.mime_types, ".json", "application/json"); - u_map_put(&configWeb.mime_types, ".png", "image/png"); - u_map_put(&configWeb.mime_types, ".gif", "image/gif"); - u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg"); - u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg"); - u_map_put(&configWeb.mime_types, ".ttf", "font/ttf"); - u_map_put(&configWeb.mime_types, ".woff", "font/woff"); - u_map_put(&configWeb.mime_types, ".ico", "image/x-icon"); - u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml"); - - webrootpath = portduino_config.webserver_root_path; - - configWeb.files_path = (char *)webrootpath.c_str(); - configWeb.url_prefix = ""; - configWeb.rootPath = strdup(portduinoVFS->mountpoint()); - - u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*"); - // Maximum body size sent by the client is 1 Kb - instanceWeb.max_post_body_size = 1024; - ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); - ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); - ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); - ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); - - // Add callback function to all endpoints for the Web Server - ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb); - - // thats for serving without SSL - // retssl = ulfius_start_framework(&instanceWeb); - - // thats for serving with SSL - retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem); - - if (retssl == U_OK) { - LOG_INFO("Web Server framework started on port: %i ", webservport); - LOG_INFO("Web Server root %s", (char *)webrootpath.c_str()); + if (portduino_config.webserverport != 0) { + webservport = portduino_config.webserverport; + LOG_INFO("Use webserver port from yaml config %i ", webservport); } else { - LOG_ERROR("Error starting Web Server framework, error number: %d", retssl); + LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443"); + webservport = 9443; + } + + // Web Content Service Instance + if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) { + LOG_ERROR("Webserver couldn't be started, abort execution"); + } else { + + LOG_INFO("Webserver started"); + u_map_init(&configWeb.mime_types); + u_map_put(&configWeb.mime_types, "*", "application/octet-stream"); + u_map_put(&configWeb.mime_types, ".html", "text/html"); + u_map_put(&configWeb.mime_types, ".htm", "text/html"); + u_map_put(&configWeb.mime_types, ".tsx", "application/javascript"); + u_map_put(&configWeb.mime_types, ".ts", "application/javascript"); + u_map_put(&configWeb.mime_types, ".css", "text/css"); + u_map_put(&configWeb.mime_types, ".js", "application/javascript"); + u_map_put(&configWeb.mime_types, ".json", "application/json"); + u_map_put(&configWeb.mime_types, ".png", "image/png"); + u_map_put(&configWeb.mime_types, ".gif", "image/gif"); + u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg"); + u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg"); + u_map_put(&configWeb.mime_types, ".ttf", "font/ttf"); + u_map_put(&configWeb.mime_types, ".woff", "font/woff"); + u_map_put(&configWeb.mime_types, ".ico", "image/x-icon"); + u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml"); + + webrootpath = portduino_config.webserver_root_path; + + configWeb.files_path = (char *)webrootpath.c_str(); + configWeb.url_prefix = ""; + configWeb.rootPath = strdup(portduinoVFS->mountpoint()); + + u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*"); + // Maximum body size sent by the client is 1 Kb + instanceWeb.max_post_body_size = 1024; + ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); + ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); + ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); + ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); + + // Add callback function to all endpoints for the Web Server + ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb); + + // thats for serving without SSL + // retssl = ulfius_start_framework(&instanceWeb); + + // thats for serving with SSL + retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem); + + if (retssl == U_OK) { + LOG_INFO("Web Server framework started on port: %i ", webservport); + LOG_INFO("Web Server root %s", (char *)webrootpath.c_str()); + } else { + LOG_ERROR("Error starting Web Server framework, error number: %d", retssl); + } } - } } -PiWebServerThread::~PiWebServerThread() { - u_map_clean(&configWeb.mime_types); +PiWebServerThread::~PiWebServerThread() +{ + u_map_clean(&configWeb.mime_types); - ulfius_stop_framework(&instanceWeb); - ulfius_clean_instance(&instanceWeb); - free(configWeb.rootPath); - free(key_pem); - free(cert_pem); - LOG_INFO("End framework"); + ulfius_stop_framework(&instanceWeb); + ulfius_clean_instance(&instanceWeb); + free(configWeb.rootPath); + free(key_pem); + free(cert_pem); + LOG_INFO("End framework"); } #endif diff --git a/src/mesh/raspihttp/PiWebServer.h b/src/mesh/raspihttp/PiWebServer.h index 039d27319..5a4adedaa 100644 --- a/src/mesh/raspihttp/PiWebServer.h +++ b/src/mesh/raspihttp/PiWebServer.h @@ -15,42 +15,44 @@ int callback_static_file(const struct _u_request *request, struct _u_response *r const char *get_filename_ext(const char *path); struct _file_config { - char *files_path; - char *url_prefix; - struct _u_map mime_types; - struct _u_map map_header; - char *redirect_on_404; - char *rootPath; + char *files_path; + char *url_prefix; + struct _u_map mime_types; + struct _u_map map_header; + char *redirect_on_404; + char *rootPath; }; -class HttpAPI : public PhoneAPI { +class HttpAPI : public PhoneAPI +{ -public: - HttpAPI() { api_type = TYPE_HTTP; } + public: + HttpAPI() { api_type = TYPE_HTTP; } -private: - // Nothing here yet + private: + // Nothing here yet -protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this + protected: + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; -class PiWebServerThread { -private: - char *key_pem = NULL; - char *cert_pem = NULL; - // struct _u_map mime_types; - std::string webrootpath; - HttpAPI webAPI; +class PiWebServerThread +{ + private: + char *key_pem = NULL; + char *cert_pem = NULL; + // struct _u_map mime_types; + std::string webrootpath; + HttpAPI webAPI; -public: - PiWebServerThread(); - ~PiWebServerThread(); - int CreateSSLCertificate(); - int CheckSSLandLoad(); - uint32_t requestRestart = 0; - struct _u_instance instanceWeb; + public: + PiWebServerThread(); + ~PiWebServerThread(); + int CreateSSLCertificate(); + int CheckSSLandLoad(); + uint32_t requestRestart = 0; + struct _u_instance instanceWeb; }; extern PiWebServerThread *piwebServerThread; diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index ab3ca48a9..2df8686a3 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -19,73 +19,78 @@ #define UDP_MULTICAST_DEFAUL_PORT 4403 // Default port for UDP multicast is same as TCP api server -class UdpMulticastHandler final { -public: - UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); } +class UdpMulticastHandler final +{ + public: + UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); } - void start() { - if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { + void start() + { + if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) - LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], UDP_MULTICAST_DEFAUL_PORT); + LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], + UDP_MULTICAST_DEFAUL_PORT); #else - LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); + LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); #endif - udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); - } else { - LOG_DEBUG("Failed to listen on UDP"); + udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); + } else { + LOG_DEBUG("Failed to listen on UDP"); + } } - } - void onReceive(AsyncUDPPacket packet) { - size_t packetLength = packet.length(); + void onReceive(AsyncUDPPacket packet) + { + size_t packetLength = packet.length(); #if defined(ARCH_NRF52) - IPAddress ip = packet.remoteIP(); - LOG_DEBUG("UDP broadcast from: %u.%u.%u.%u, len=%u", ip[0], ip[1], ip[2], ip[3], packetLength); + IPAddress ip = packet.remoteIP(); + LOG_DEBUG("UDP broadcast from: %u.%u.%u.%u, len=%u", ip[0], ip[1], ip[2], ip[3], packetLength); #elif !defined(ARCH_PORTDUINO) - // FIXME(PORTDUINO): arduino lacks IPAddress::toString() - LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); + // FIXME(PORTDUINO): arduino lacks IPAddress::toString() + LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif - meshtastic_MeshPacket mp; - LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); - bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); - if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { - mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; - mp.pki_encrypted = false; - mp.public_key.size = 0; - memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); - UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); - // Unset received SNR/RSSI - p->rx_snr = 0; - p->rx_rssi = 0; - router->enqueueReceivedMessage(p.release()); + meshtastic_MeshPacket mp; + LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); + bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); + if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; + mp.pki_encrypted = false; + mp.public_key.size = 0; + memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); + UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); + // Unset received SNR/RSSI + p->rx_snr = 0; + p->rx_rssi = 0; + router->enqueueReceivedMessage(p.release()); + } } - } - bool onSend(const meshtastic_MeshPacket *mp) { - if (!mp || !udp) { - return false; - } + bool onSend(const meshtastic_MeshPacket *mp) + { + if (!mp || !udp) { + return false; + } #if defined(ARCH_NRF52) - if (!isEthernetAvailable()) { - return false; - } + if (!isEthernetAvailable()) { + return false; + } #elif !defined(ARCH_PORTDUINO) - if (WiFi.status() != WL_CONNECTED) { - return false; - } + if (WiFi.status() != WL_CONNECTED) { + return false; + } #endif - if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { - LOG_ERROR("Attempt to send UDP sourced packet over UDP"); + if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { + LOG_ERROR("Attempt to send UDP sourced packet over UDP"); + } + LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); + uint8_t buffer[meshtastic_MeshPacket_size]; + size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); + udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); + return true; } - LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); - uint8_t buffer[meshtastic_MeshPacket_size]; - size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); - udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); - return true; - } -private: - IPAddress udpIpAddress; - AsyncUDP udp; + private: + IPAddress udpIpAddress; + AsyncUDP udp; }; #endif // HAS_UDP_MULTICAST \ No newline at end of file diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index b88ba53f4..45944872e 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -64,266 +64,273 @@ Periodic *wifiReconnect; #ifdef USE_WS5500 // Startup Ethernet -bool initEthernet() { - if ((config.network.eth_enabled) && - (ETH.begin(ETH_PHY_W5500, 1, ETH_CS_PIN, ETH_INT_PIN, ETH_RST_PIN, SPI3_HOST, ETH_SCLK_PIN, ETH_MISO_PIN, ETH_MOSI_PIN))) { - WiFi.onEvent(WiFiEvent); +bool initEthernet() +{ + if ((config.network.eth_enabled) && (ETH.begin(ETH_PHY_W5500, 1, ETH_CS_PIN, ETH_INT_PIN, ETH_RST_PIN, SPI3_HOST, + ETH_SCLK_PIN, ETH_MISO_PIN, ETH_MOSI_PIN))) { + WiFi.onEvent(WiFiEvent); #if !MESHTASTIC_EXCLUDE_WEBSERVER - createSSLCert(); // For WebServer + createSSLCert(); // For WebServer #endif - return true; - } + return true; + } - return false; + return false; } #endif -static void onNetworkConnected() { - if (!APStartupComplete) { - // Start web server - LOG_INFO("Start network services"); +static void onNetworkConnected() +{ + if (!APStartupComplete) { + // Start web server + LOG_INFO("Start network services"); - // start mdns - if (!MDNS.begin("Meshtastic")) { - LOG_ERROR("Error setting up mDNS responder!"); - } else { - LOG_INFO("mDNS Host: Meshtastic.local"); - MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT); + // start mdns + if (!MDNS.begin("Meshtastic")) { + LOG_ERROR("Error setting up mDNS responder!"); + } else { + LOG_INFO("mDNS Host: Meshtastic.local"); + MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT); // ESPmDNS (ESP32) and SimpleMDNS (RP2040) have slightly different APIs for adding TXT records #ifdef ARCH_ESP32 - MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name)); - MDNS.addServiceTxt("meshtastic", "tcp", "id", String(nodeDB->getNodeId().c_str())); - MDNS.addServiceTxt("meshtastic", "tcp", "pio_env", optstr(APP_ENV)); - // ESP32 prints obtained IP address in WiFiEvent + MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name)); + MDNS.addServiceTxt("meshtastic", "tcp", "id", String(nodeDB->getNodeId().c_str())); + MDNS.addServiceTxt("meshtastic", "tcp", "pio_env", optstr(APP_ENV)); + // ESP32 prints obtained IP address in WiFiEvent #elif defined(ARCH_RP2040) - MDNS.addServiceTxt("meshtastic", "shortname", owner.short_name); - MDNS.addServiceTxt("meshtastic", "id", nodeDB->getNodeId().c_str()); - MDNS.addServiceTxt("meshtastic", "pio_env", optstr(APP_ENV)); - LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); + MDNS.addServiceTxt("meshtastic", "shortname", owner.short_name); + MDNS.addServiceTxt("meshtastic", "id", nodeDB->getNodeId().c_str()); + MDNS.addServiceTxt("meshtastic", "pio_env", optstr(APP_ENV)); + LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); #endif - } + } #ifndef DISABLE_NTP - LOG_INFO("Start NTP time client"); - timeClient.begin(); - timeClient.setUpdateInterval(60 * 60); // Update once an hour + LOG_INFO("Start NTP time client"); + timeClient.begin(); + timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif - if (config.network.rsyslog_server[0]) { - LOG_INFO("Start Syslog client"); - // Defaults - int serverPort = 514; - const char *serverAddr = config.network.rsyslog_server; - String server = String(serverAddr); - int delimIndex = server.indexOf(':'); - if (delimIndex > 0) { - String port = server.substring(delimIndex + 1, server.length()); - server[delimIndex] = 0; - serverPort = port.toInt(); - serverAddr = server.c_str(); - } - syslog.server(serverAddr, serverPort); - syslog.deviceHostname(getDeviceName()); - syslog.appName("Meshtastic"); - syslog.defaultPriority(LOGLEVEL_USER); - syslog.enable(); - } + if (config.network.rsyslog_server[0]) { + LOG_INFO("Start Syslog client"); + // Defaults + int serverPort = 514; + const char *serverAddr = config.network.rsyslog_server; + String server = String(serverAddr); + int delimIndex = server.indexOf(':'); + if (delimIndex > 0) { + String port = server.substring(delimIndex + 1, server.length()); + server[delimIndex] = 0; + serverPort = port.toInt(); + serverAddr = server.c_str(); + } + syslog.server(serverAddr, serverPort); + syslog.deviceHostname(getDeviceName()); + syslog.appName("Meshtastic"); + syslog.defaultPriority(LOGLEVEL_USER); + syslog.enable(); + } #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - initWebServer(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initWebServer(); + } #endif #if !MESHTASTIC_EXCLUDE_SOCKETAPI - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - initApiServer(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif - APStartupComplete = true; - } + APStartupComplete = true; + } #if HAS_UDP_MULTICAST - if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { - udpHandler->start(); - } -#endif -} - -static int32_t reconnectWiFi() { - const char *wifiName = config.network.wifi_ssid; - const char *wifiPsw = config.network.wifi_psk; - - if (config.network.wifi_enabled && needReconnect) { - - if (!*wifiPsw) // Treat empty password as no password - wifiPsw = NULL; - - needReconnect = false; - isReconnecting = true; - - // Make sure we clear old connection credentials -#ifdef ARCH_ESP32 - WiFi.disconnect(false, true); -#elif defined(ARCH_RP2040) - WiFi.disconnect(false); -#endif - LOG_INFO("Reconnecting to WiFi access point %s", wifiName); - - // Start the non-blocking wait for 5 seconds - wifiReconnectStartMillis = millis(); - wifiReconnectPending = true; - // Do not attempt to connect yet, wait for the next invocation - return 5000; // Schedule next check soon - } - - // Check if we are ready to proceed with the WiFi connection after the 5s wait - if (wifiReconnectPending) { - if (millis() - wifiReconnectStartMillis >= 5000) { - if (!WiFi.isConnected()) { -#ifdef CONFIG_IDF_TARGET_ESP32C3 - WiFi.mode(WIFI_MODE_NULL); - WiFi.useStaticBuffers(true); - WiFi.mode(WIFI_STA); -#endif - WiFi.begin(wifiName, wifiPsw); - } - isReconnecting = false; - wifiReconnectPending = false; - } else { - // Still waiting for 5s to elapse - return 100; // Check again soon + if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { + udpHandler->start(); } - } - -#ifndef DISABLE_NTP - if (WiFi.isConnected() && (!Throttle::isWithinTimespanMs(lastrun_ntp, 43200000) || (lastrun_ntp == 0))) { // every 12 hours - LOG_DEBUG("Update NTP time from %s", config.network.ntp_server); - if (timeClient.update()) { - LOG_DEBUG("NTP Request Success - Setting RTCQualityNTP if needed"); - - struct timeval tv; - tv.tv_sec = timeClient.getEpochTime(); - tv.tv_usec = 0; - - perhapsSetRTC(RTCQualityNTP, &tv); - lastrun_ntp = millis(); - } else { - LOG_DEBUG("NTP Update failed"); - } - } #endif - - if (config.network.wifi_enabled && !WiFi.isConnected()) { -#ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) - needReconnect = APStartupComplete; -#endif - return 1000; // check once per second - } else { -#ifdef ARCH_RP2040 - onNetworkConnected(); // will only do anything once -#endif - return 300000; // every 5 minutes - } } -bool isWifiAvailable() { - - if (config.network.wifi_enabled && (config.network.wifi_ssid[0])) { - return true; -#ifdef USE_WS5500 - } else if (config.network.eth_enabled) { - return true; -#endif -#ifndef ARCH_PORTDUINO - } else if (WiFi.status() == WL_CONNECTED) { - // it's likely we have wifi now, but user intends to turn it off in config! - return true; -#endif - } else { - return false; - } -} - -// Disable WiFi -void deinitWifi() { - LOG_INFO("WiFi deinit"); - - if (isWifiAvailable()) { -#ifdef ARCH_ESP32 - WiFi.disconnect(true, false); -#elif defined(ARCH_RP2040) - WiFi.disconnect(true); -#endif - WiFi.mode(WIFI_OFF); - LOG_INFO("WiFi Turned Off"); - // WiFi.printDiag(Serial); - } -} - -// Startup WiFi -bool initWifi() { - if (config.network.wifi_enabled && config.network.wifi_ssid[0]) { - +static int32_t reconnectWiFi() +{ const char *wifiName = config.network.wifi_ssid; const char *wifiPsw = config.network.wifi_psk; + if (config.network.wifi_enabled && needReconnect) { + + if (!*wifiPsw) // Treat empty password as no password + wifiPsw = NULL; + + needReconnect = false; + isReconnecting = true; + + // Make sure we clear old connection credentials +#ifdef ARCH_ESP32 + WiFi.disconnect(false, true); +#elif defined(ARCH_RP2040) + WiFi.disconnect(false); +#endif + LOG_INFO("Reconnecting to WiFi access point %s", wifiName); + + // Start the non-blocking wait for 5 seconds + wifiReconnectStartMillis = millis(); + wifiReconnectPending = true; + // Do not attempt to connect yet, wait for the next invocation + return 5000; // Schedule next check soon + } + + // Check if we are ready to proceed with the WiFi connection after the 5s wait + if (wifiReconnectPending) { + if (millis() - wifiReconnectStartMillis >= 5000) { + if (!WiFi.isConnected()) { +#ifdef CONFIG_IDF_TARGET_ESP32C3 + WiFi.mode(WIFI_MODE_NULL); + WiFi.useStaticBuffers(true); + WiFi.mode(WIFI_STA); +#endif + WiFi.begin(wifiName, wifiPsw); + } + isReconnecting = false; + wifiReconnectPending = false; + } else { + // Still waiting for 5s to elapse + return 100; // Check again soon + } + } + +#ifndef DISABLE_NTP + if (WiFi.isConnected() && (!Throttle::isWithinTimespanMs(lastrun_ntp, 43200000) || (lastrun_ntp == 0))) { // every 12 hours + LOG_DEBUG("Update NTP time from %s", config.network.ntp_server); + if (timeClient.update()) { + LOG_DEBUG("NTP Request Success - Setting RTCQualityNTP if needed"); + + struct timeval tv; + tv.tv_sec = timeClient.getEpochTime(); + tv.tv_usec = 0; + + perhapsSetRTC(RTCQualityNTP, &tv); + lastrun_ntp = millis(); + } else { + LOG_DEBUG("NTP Update failed"); + } + } +#endif + + if (config.network.wifi_enabled && !WiFi.isConnected()) { +#ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) + needReconnect = APStartupComplete; +#endif + return 1000; // check once per second + } else { +#ifdef ARCH_RP2040 + onNetworkConnected(); // will only do anything once +#endif + return 300000; // every 5 minutes + } +} + +bool isWifiAvailable() +{ + + if (config.network.wifi_enabled && (config.network.wifi_ssid[0])) { + return true; +#ifdef USE_WS5500 + } else if (config.network.eth_enabled) { + return true; +#endif +#ifndef ARCH_PORTDUINO + } else if (WiFi.status() == WL_CONNECTED) { + // it's likely we have wifi now, but user intends to turn it off in config! + return true; +#endif + } else { + return false; + } +} + +// Disable WiFi +void deinitWifi() +{ + LOG_INFO("WiFi deinit"); + + if (isWifiAvailable()) { +#ifdef ARCH_ESP32 + WiFi.disconnect(true, false); +#elif defined(ARCH_RP2040) + WiFi.disconnect(true); +#endif + WiFi.mode(WIFI_OFF); + LOG_INFO("WiFi Turned Off"); + // WiFi.printDiag(Serial); + } +} + +// Startup WiFi +bool initWifi() +{ + if (config.network.wifi_enabled && config.network.wifi_ssid[0]) { + + const char *wifiName = config.network.wifi_ssid; + const char *wifiPsw = config.network.wifi_psk; + #ifndef ARCH_RP2040 #if !MESHTASTIC_EXCLUDE_WEBSERVER - createSSLCert(); // For WebServer + createSSLCert(); // For WebServer #endif - WiFi.persistent(false); // Disable flash storage for WiFi credentials + WiFi.persistent(false); // Disable flash storage for WiFi credentials #endif - if (!*wifiPsw) // Treat empty password as no password - wifiPsw = NULL; + if (!*wifiPsw) // Treat empty password as no password + wifiPsw = NULL; - if (*wifiName) { - uint8_t dmac[6]; - getMacAddr(dmac); - snprintf(ourHost, sizeof(ourHost), "Meshtastic-%02x%02x", dmac[4], dmac[5]); + if (*wifiName) { + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(ourHost, sizeof(ourHost), "Meshtastic-%02x%02x", dmac[4], dmac[5]); - WiFi.mode(WIFI_STA); - WiFi.setHostname(ourHost); + WiFi.mode(WIFI_STA); + WiFi.setHostname(ourHost); - if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC && config.network.ipv4_config.ip != 0) { + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC && + config.network.ipv4_config.ip != 0) { #ifdef ARCH_ESP32 - WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet, - config.network.ipv4_config.dns); + WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet, + config.network.ipv4_config.dns); #elif defined(ARCH_RP2040) - WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, - config.network.ipv4_config.subnet); + WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, + config.network.ipv4_config.subnet); #endif - } + } #ifdef ARCH_ESP32 - WiFi.onEvent(WiFiEvent); - WiFi.setAutoReconnect(true); - WiFi.setSleep(false); + WiFi.onEvent(WiFiEvent); + WiFi.setAutoReconnect(true); + WiFi.setSleep(false); - // This is needed to improve performance. - esp_wifi_set_ps(WIFI_PS_NONE); // Disable radio power saving + // This is needed to improve performance. + esp_wifi_set_ps(WIFI_PS_NONE); // Disable radio power saving - WiFi.onEvent( - [](WiFiEvent_t event, WiFiEventInfo_t info) { - LOG_WARN("WiFi lost connection. Reason: %d", info.wifi_sta_disconnected.reason); + WiFi.onEvent( + [](WiFiEvent_t event, WiFiEventInfo_t info) { + LOG_WARN("WiFi lost connection. Reason: %d", info.wifi_sta_disconnected.reason); - /* - If we are disconnected from the AP for some reason, - save the error code. + /* + If we are disconnected from the AP for some reason, + save the error code. - For a reference to the codes: - https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - */ - wifiDisconnectReason = info.wifi_sta_disconnected.reason; - }, - WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + For a reference to the codes: + https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + */ + wifiDisconnectReason = info.wifi_sta_disconnected.reason; + }, + WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); #endif - LOG_DEBUG("JOINING WIFI soon: ssid=%s", wifiName); - wifiReconnect = new Periodic("WifiConnect", reconnectWiFi); + LOG_DEBUG("JOINING WIFI soon: ssid=%s", wifiName); + wifiReconnect = new Periodic("WifiConnect", reconnectWiFi); + } + return true; + } else { + LOG_INFO("Not using WIFI"); + return false; } - return true; - } else { - LOG_INFO("Not using WIFI"); - return false; - } } #ifdef ARCH_ESP32 @@ -332,203 +339,208 @@ bool initWifi() { // Licensed under the GNU Lesser General Public License v2.1 // https://github.com/espressif/arduino-esp32/blob/1f038677eb2eaf5e9ca6b6074486803c15468bed/libraries/WiFi/src/WiFiSTA.cpp#L755 esp_netif_t *get_esp_interface_netif(esp_interface_t interface); -IPv6Address GlobalIPv6() { - esp_ip6_addr_t addr; - if (WiFiGenericClass::getMode() == WIFI_MODE_NULL) { - return IPv6Address(); - } - if (esp_netif_get_ip6_global(get_esp_interface_netif(ESP_IF_WIFI_STA), &addr)) { - return IPv6Address(); - } - return IPv6Address(addr.addr); +IPv6Address GlobalIPv6() +{ + esp_ip6_addr_t addr; + if (WiFiGenericClass::getMode() == WIFI_MODE_NULL) { + return IPv6Address(); + } + if (esp_netif_get_ip6_global(get_esp_interface_netif(ESP_IF_WIFI_STA), &addr)) { + return IPv6Address(); + } + return IPv6Address(addr.addr); } #endif // Called by the Espressif SDK to -static void WiFiEvent(WiFiEvent_t event) { - LOG_DEBUG("Network-Event %d: ", event); +static void WiFiEvent(WiFiEvent_t event) +{ + LOG_DEBUG("Network-Event %d: ", event); - switch (event) { - case ARDUINO_EVENT_WIFI_READY: - LOG_INFO("WiFi interface ready"); - break; - case ARDUINO_EVENT_WIFI_SCAN_DONE: - LOG_INFO("Completed scan for access points"); - break; - case ARDUINO_EVENT_WIFI_STA_START: - LOG_INFO("WiFi station started"); - break; - case ARDUINO_EVENT_WIFI_STA_STOP: - LOG_INFO("WiFi station stopped"); - syslog.disable(); - break; - case ARDUINO_EVENT_WIFI_STA_CONNECTED: - LOG_INFO("Connected to access point"); - if (config.network.ipv6_enabled) { + switch (event) { + case ARDUINO_EVENT_WIFI_READY: + LOG_INFO("WiFi interface ready"); + break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + LOG_INFO("Completed scan for access points"); + break; + case ARDUINO_EVENT_WIFI_STA_START: + LOG_INFO("WiFi station started"); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + LOG_INFO("WiFi station stopped"); + syslog.disable(); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + LOG_INFO("Connected to access point"); + if (config.network.ipv6_enabled) { #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - if (!WiFi.enableIPv6()) { - LOG_WARN("Failed to enable IPv6"); - } + if (!WiFi.enableIPv6()) { + LOG_WARN("Failed to enable IPv6"); + } #else - if (!WiFi.enableIpV6()) { - LOG_WARN("Failed to enable IPv6"); - } + if (!WiFi.enableIpV6()) { + LOG_WARN("Failed to enable IPv6"); + } #endif - } + } #ifdef WIFI_LED - digitalWrite(WIFI_LED, HIGH); + digitalWrite(WIFI_LED, HIGH); #endif - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - LOG_INFO("Disconnected from WiFi access point"); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + LOG_INFO("Disconnected from WiFi access point"); #ifdef WIFI_LED - digitalWrite(WIFI_LED, LOW); + digitalWrite(WIFI_LED, LOW); #endif - if (!isReconnecting) { - WiFi.disconnect(false, true); - syslog.disable(); - needReconnect = true; - wifiReconnect->setIntervalFromNow(1000); - } - break; - case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: - LOG_INFO("Authentication mode of access point has changed"); - break; - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); - onNetworkConnected(); - break; - case ARDUINO_EVENT_WIFI_STA_GOT_IP6: + if (!isReconnecting) { + WiFi.disconnect(false, true); + syslog.disable(); + needReconnect = true; + wifiReconnect->setIntervalFromNow(1000); + } + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + LOG_INFO("Authentication mode of access point has changed"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); + onNetworkConnected(); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP6: #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - LOG_INFO("Obtained Local IP6 address: %s", WiFi.linkLocalIPv6().toString().c_str()); - LOG_INFO("Obtained GlobalIP6 address: %s", WiFi.globalIPv6().toString().c_str()); + LOG_INFO("Obtained Local IP6 address: %s", WiFi.linkLocalIPv6().toString().c_str()); + LOG_INFO("Obtained GlobalIP6 address: %s", WiFi.globalIPv6().toString().c_str()); #else - LOG_INFO("Obtained Local IP6 address: %s", WiFi.localIPv6().toString().c_str()); - LOG_INFO("Obtained GlobalIP6 address: %s", GlobalIPv6().toString().c_str()); + LOG_INFO("Obtained Local IP6 address: %s", WiFi.localIPv6().toString().c_str()); + LOG_INFO("Obtained GlobalIP6 address: %s", GlobalIPv6().toString().c_str()); #endif - break; - case ARDUINO_EVENT_WIFI_STA_LOST_IP: - LOG_INFO("Lost IP address and IP address is reset to 0"); - if (!isReconnecting) { - WiFi.disconnect(false, true); - syslog.disable(); - needReconnect = true; - wifiReconnect->setIntervalFromNow(1000); - } - break; - case ARDUINO_EVENT_WPS_ER_SUCCESS: - LOG_INFO("WiFi Protected Setup (WPS): succeeded in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_FAILED: - LOG_INFO("WiFi Protected Setup (WPS): failed in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_TIMEOUT: - LOG_INFO("WiFi Protected Setup (WPS): timeout in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_PIN: - LOG_INFO("WiFi Protected Setup (WPS): pin code in enrollee mode"); - break; - case ARDUINO_EVENT_WPS_ER_PBC_OVERLAP: - LOG_INFO("WiFi Protected Setup (WPS): push button overlap in enrollee mode"); - break; - case ARDUINO_EVENT_WIFI_AP_START: - LOG_INFO("WiFi access point started"); + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + LOG_INFO("Lost IP address and IP address is reset to 0"); + if (!isReconnecting) { + WiFi.disconnect(false, true); + syslog.disable(); + needReconnect = true; + wifiReconnect->setIntervalFromNow(1000); + } + break; + case ARDUINO_EVENT_WPS_ER_SUCCESS: + LOG_INFO("WiFi Protected Setup (WPS): succeeded in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_FAILED: + LOG_INFO("WiFi Protected Setup (WPS): failed in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_TIMEOUT: + LOG_INFO("WiFi Protected Setup (WPS): timeout in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PIN: + LOG_INFO("WiFi Protected Setup (WPS): pin code in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PBC_OVERLAP: + LOG_INFO("WiFi Protected Setup (WPS): push button overlap in enrollee mode"); + break; + case ARDUINO_EVENT_WIFI_AP_START: + LOG_INFO("WiFi access point started"); #ifdef WIFI_LED - digitalWrite(WIFI_LED, HIGH); + digitalWrite(WIFI_LED, HIGH); #endif - break; - case ARDUINO_EVENT_WIFI_AP_STOP: - LOG_INFO("WiFi access point stopped"); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + LOG_INFO("WiFi access point stopped"); #ifdef WIFI_LED - digitalWrite(WIFI_LED, LOW); + digitalWrite(WIFI_LED, LOW); #endif - break; - case ARDUINO_EVENT_WIFI_AP_STACONNECTED: - LOG_INFO("Client connected"); - break; - case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: - LOG_INFO("Client disconnected"); - break; - case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: - LOG_INFO("Assigned IP address to client"); - break; - case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: - LOG_INFO("Received probe request"); - break; - case ARDUINO_EVENT_WIFI_AP_GOT_IP6: - LOG_INFO("IPv6 is preferred"); - break; - case ARDUINO_EVENT_WIFI_FTM_REPORT: - LOG_INFO("Fast Transition Management report"); - break; - case ARDUINO_EVENT_ETH_START: - LOG_INFO("Ethernet started"); - break; - case ARDUINO_EVENT_ETH_STOP: - syslog.disable(); - LOG_INFO("Ethernet stopped"); - break; - case ARDUINO_EVENT_ETH_CONNECTED: - LOG_INFO("Ethernet connected"); - break; - case ARDUINO_EVENT_ETH_DISCONNECTED: - syslog.disable(); - LOG_INFO("Ethernet disconnected"); - break; - case ARDUINO_EVENT_ETH_GOT_IP: + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + LOG_INFO("Client connected"); + break; + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + LOG_INFO("Client disconnected"); + break; + case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: + LOG_INFO("Assigned IP address to client"); + break; + case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: + LOG_INFO("Received probe request"); + break; + case ARDUINO_EVENT_WIFI_AP_GOT_IP6: + LOG_INFO("IPv6 is preferred"); + break; + case ARDUINO_EVENT_WIFI_FTM_REPORT: + LOG_INFO("Fast Transition Management report"); + break; + case ARDUINO_EVENT_ETH_START: + LOG_INFO("Ethernet started"); + break; + case ARDUINO_EVENT_ETH_STOP: + syslog.disable(); + LOG_INFO("Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + LOG_INFO("Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + syslog.disable(); + LOG_INFO("Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: #ifdef USE_WS5500 - LOG_INFO("Obtained IP address: %s, %u Mbps, %s", ETH.localIP().toString().c_str(), ETH.linkSpeed(), - ETH.fullDuplex() ? "FULL_DUPLEX" : "HALF_DUPLEX"); - onNetworkConnected(); + LOG_INFO("Obtained IP address: %s, %u Mbps, %s", ETH.localIP().toString().c_str(), ETH.linkSpeed(), + ETH.fullDuplex() ? "FULL_DUPLEX" : "HALF_DUPLEX"); + onNetworkConnected(); #endif - break; - case ARDUINO_EVENT_ETH_GOT_IP6: + break; + case ARDUINO_EVENT_ETH_GOT_IP6: #ifdef USE_WS5500 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - LOG_INFO("Obtained Local IP6 address: %s", ETH.linkLocalIPv6().toString().c_str()); - LOG_INFO("Obtained GlobalIP6 address: %s", ETH.globalIPv6().toString().c_str()); + LOG_INFO("Obtained Local IP6 address: %s", ETH.linkLocalIPv6().toString().c_str()); + LOG_INFO("Obtained GlobalIP6 address: %s", ETH.globalIPv6().toString().c_str()); #else - LOG_INFO("Obtained IP6 address: %s", ETH.localIPv6().toString().c_str()); + LOG_INFO("Obtained IP6 address: %s", ETH.localIPv6().toString().c_str()); #endif #endif - break; - case ARDUINO_EVENT_SC_SCAN_DONE: - LOG_INFO("SmartConfig: Scan done"); - break; - case ARDUINO_EVENT_SC_FOUND_CHANNEL: - LOG_INFO("SmartConfig: Found channel"); - break; - case ARDUINO_EVENT_SC_GOT_SSID_PSWD: - LOG_INFO("SmartConfig: Got SSID and password"); - break; - case ARDUINO_EVENT_SC_SEND_ACK_DONE: - LOG_INFO("SmartConfig: Send ACK done"); - break; - case ARDUINO_EVENT_PROV_INIT: - LOG_INFO("Provision Init"); - break; - case ARDUINO_EVENT_PROV_DEINIT: - LOG_INFO("Provision Stopped"); - break; - case ARDUINO_EVENT_PROV_START: - LOG_INFO("Provision Started"); - break; - case ARDUINO_EVENT_PROV_END: - LOG_INFO("Provision End"); - break; - case ARDUINO_EVENT_PROV_CRED_RECV: - LOG_INFO("Provision Credentials received"); - break; - case ARDUINO_EVENT_PROV_CRED_FAIL: - LOG_INFO("Provision Credentials failed"); - break; - case ARDUINO_EVENT_PROV_CRED_SUCCESS: - LOG_INFO("Provision Credentials success"); - break; - default: - break; - } + break; + case ARDUINO_EVENT_SC_SCAN_DONE: + LOG_INFO("SmartConfig: Scan done"); + break; + case ARDUINO_EVENT_SC_FOUND_CHANNEL: + LOG_INFO("SmartConfig: Found channel"); + break; + case ARDUINO_EVENT_SC_GOT_SSID_PSWD: + LOG_INFO("SmartConfig: Got SSID and password"); + break; + case ARDUINO_EVENT_SC_SEND_ACK_DONE: + LOG_INFO("SmartConfig: Send ACK done"); + break; + case ARDUINO_EVENT_PROV_INIT: + LOG_INFO("Provision Init"); + break; + case ARDUINO_EVENT_PROV_DEINIT: + LOG_INFO("Provision Stopped"); + break; + case ARDUINO_EVENT_PROV_START: + LOG_INFO("Provision Started"); + break; + case ARDUINO_EVENT_PROV_END: + LOG_INFO("Provision End"); + break; + case ARDUINO_EVENT_PROV_CRED_RECV: + LOG_INFO("Provision Credentials received"); + break; + case ARDUINO_EVENT_PROV_CRED_FAIL: + LOG_INFO("Provision Credentials failed"); + break; + case ARDUINO_EVENT_PROV_CRED_SUCCESS: + LOG_INFO("Provision Credentials success"); + break; + default: + break; + } } #endif -uint8_t getWifiDisconnectReason() { return wifiDisconnectReason; } +uint8_t getWifiDisconnectReason() +{ + return wifiDisconnectReason; +} #endif // HAS_WIFI diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp index 4e711d5cf..ea2ba641b 100644 --- a/src/meshUtils.cpp +++ b/src/meshUtils.cpp @@ -37,68 +37,73 @@ * SUCH DAMAGE. * */ -char *strnstr(const char *s, const char *find, size_t slen) { - char c; - if ((c = *find++) != '\0') { - char sc; - size_t len; +char *strnstr(const char *s, const char *find, size_t slen) +{ + char c; + if ((c = *find++) != '\0') { + char sc; + size_t len; - len = strlen(find); - do { - do { - if (slen-- < 1 || (sc = *s++) == '\0') - return (NULL); - } while (sc != c); - if (len > slen) - return (NULL); - } while (strncmp(s, find, len) != 0); - s--; - } - return ((char *)s); -} - -void printBytes(const char *label, const uint8_t *p, size_t numbytes) { - int labelSize = strlen(label); - char *messageBuffer = new char[labelSize + (numbytes * 3) + 2]; - strncpy(messageBuffer, label, labelSize); - for (size_t i = 0; i < numbytes; i++) - snprintf(messageBuffer + labelSize + i * 3, 4, " %02x", p[i]); - strcpy(messageBuffer + labelSize + numbytes * 3, "\n"); - LOG_DEBUG(messageBuffer); - delete[] messageBuffer; -} - -bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes) { - for (uint8_t i = 0; i < numbytes; i++) { - if (mem[i] != find) - return false; - } - return true; -} - -bool isOneOf(int item, int count, ...) { - va_list args; - va_start(args, count); - bool found = false; - for (int i = 0; i < count; ++i) { - if (item == va_arg(args, int)) { - found = true; - break; + len = strlen(find); + do { + do { + if (slen-- < 1 || (sc = *s++) == '\0') + return (NULL); + } while (sc != c); + if (len > slen) + return (NULL); + } while (strncmp(s, find, len) != 0); + s--; } - } - va_end(args); - return found; + return ((char *)s); } -const std::string vformat(const char *const zcFormat, ...) { - va_list vaArgs; - va_start(vaArgs, zcFormat); - va_list vaArgsCopy; - va_copy(vaArgsCopy, vaArgs); - const int iLen = std::vsnprintf(NULL, 0, zcFormat, vaArgsCopy); - va_end(vaArgsCopy); - std::vector zc(iLen + 1); - std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs); - va_end(vaArgs); - return std::string(zc.data(), iLen); +void printBytes(const char *label, const uint8_t *p, size_t numbytes) +{ + int labelSize = strlen(label); + char *messageBuffer = new char[labelSize + (numbytes * 3) + 2]; + strncpy(messageBuffer, label, labelSize); + for (size_t i = 0; i < numbytes; i++) + snprintf(messageBuffer + labelSize + i * 3, 4, " %02x", p[i]); + strcpy(messageBuffer + labelSize + numbytes * 3, "\n"); + LOG_DEBUG(messageBuffer); + delete[] messageBuffer; +} + +bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes) +{ + for (uint8_t i = 0; i < numbytes; i++) { + if (mem[i] != find) + return false; + } + return true; +} + +bool isOneOf(int item, int count, ...) +{ + va_list args; + va_start(args, count); + bool found = false; + for (int i = 0; i < count; ++i) { + if (item == va_arg(args, int)) { + found = true; + break; + } + } + va_end(args); + return found; +} + +const std::string vformat(const char *const zcFormat, ...) +{ + va_list vaArgs; + va_start(vaArgs, zcFormat); + va_list vaArgsCopy; + va_copy(vaArgsCopy, vaArgs); + const int iLen = std::vsnprintf(NULL, 0, zcFormat, vaArgsCopy); + va_end(vaArgsCopy); + std::vector zc(iLen + 1); + std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs); + va_end(vaArgs); + return std::string(zc.data(), iLen); } \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index 7503bda67..9fcf6f8a8 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -6,13 +6,16 @@ #include /// C++ v17+ clamp function, limits a given value to a range defined by lo and hi -template constexpr const T &clamp(const T &v, const T &lo, const T &hi) { return (v < lo) ? lo : (hi < v) ? hi : v; } +template constexpr const T &clamp(const T &v, const T &lo, const T &hi) +{ + return (v < lo) ? lo : (hi < v) ? hi : v; +} #if HAS_SCREEN -#define IF_SCREEN(X) \ - if (screen) { \ - X; \ - } +#define IF_SCREEN(X) \ + if (screen) { \ + X; \ + } #else #define IF_SCREEN(...) #endif diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 44806e42c..5f0c27fff 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -43,23 +43,25 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) #include "SerialModule.h" #endif AdminModule *adminModule; bool hasOpenEditTransaction; -/// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' -/// word instead. Also, to make setting work correctly, if someone tries to set a string to this reserved value we -/// assume they don't really want a change. +/// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' word instead. +/// Also, to make setting work correctly, if someone tries to set a string to this reserved value we assume they don't really want +/// a change. static const char *secretReserved = "sekrit"; /// If buf is the reserved secret word, replace the buffer with currentVal -static void writeSecret(char *buf, size_t bufsz, const char *currentVal) { - if (strcmp(buf, secretReserved) == 0) { - strncpy(buf, currentVal, bufsz); - } +static void writeSecret(char *buf, size_t bufsz, const char *currentVal) +{ + if (strcmp(buf, secretReserved) == 0) { + strncpy(buf, currentVal, bufsz); + } } /** @@ -69,1372 +71,1417 @@ static void writeSecret(char *buf, size_t bufsz, const char *currentVal) { * @param r Decoded AdminMessage * @return bool */ -bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) { - // if handled == false, then let others look at this message also if they want - bool handled = false; - assert(r); - bool fromOthers = !isFromUs(&mp); - if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { - return handled; - } - meshtastic_Channel *ch = &channels.getByIndex(mp.channel); - // Could tighten this up further by tracking the last public_key we went an AdminMessage request to - // and only allowing responses from that remote. - if (messageIsResponse(r)) { - LOG_DEBUG("Allow admin response message"); - } else if (mp.from == 0) { - if (config.security.is_managed) { - LOG_INFO("Ignore local admin payload because is_managed"); - return handled; +bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) +{ + // if handled == false, then let others look at this message also if they want + bool handled = false; + assert(r); + bool fromOthers = !isFromUs(&mp); + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + return handled; } - } else if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) { - if (!config.security.admin_channel_enabled) { - LOG_INFO("Ignore admin channel, legacy admin is disabled"); - myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); - return handled; - } - } else if (mp.pki_encrypted) { - if ((config.security.admin_key[0].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || - (config.security.admin_key[1].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || - (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { - LOG_INFO("PKC admin payload with authorized sender key"); - - // Automatically favorite the node that is using the admin key - auto remoteNode = nodeDB->getMeshNode(mp.from); - if (remoteNode && !remoteNode->is_favorite) { - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it - // without the user doing so deliberately. - LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); - } else { - LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); - remoteNode->is_favorite = true; + meshtastic_Channel *ch = &channels.getByIndex(mp.channel); + // Could tighten this up further by tracking the last public_key we went an AdminMessage request to + // and only allowing responses from that remote. + if (messageIsResponse(r)) { + LOG_DEBUG("Allow admin response message"); + } else if (mp.from == 0) { + if (config.security.is_managed) { + LOG_INFO("Ignore local admin payload because is_managed"); + return handled; + } + } else if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) { + if (!config.security.admin_channel_enabled) { + LOG_INFO("Ignore admin channel, legacy admin is disabled"); + myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + return handled; + } + } else if (mp.pki_encrypted) { + if ((config.security.admin_key[0].size == 32 && + memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || + (config.security.admin_key[1].size == 32 && + memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || + (config.security.admin_key[2].size == 32 && + memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { + LOG_INFO("PKC admin payload with authorized sender key"); + + // Automatically favorite the node that is using the admin key + auto remoteNode = nodeDB->getMeshNode(mp.from); + if (remoteNode && !remoteNode->is_favorite) { + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it + // without the user doing so deliberately. + LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); + } else { + LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); + remoteNode->is_favorite = true; + } + } + } else { + myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); + LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); + return handled; } - } } else { - myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); - LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); - return handled; + LOG_INFO("Ignore unauthorized admin payload %i", r->which_payload_variant); + myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); + return handled; } - } else { - LOG_INFO("Ignore unauthorized admin payload %i", r->which_payload_variant); - myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); - return handled; - } - LOG_INFO("Handle admin payload %i", r->which_payload_variant); + LOG_INFO("Handle admin payload %i", r->which_payload_variant); - // all of the get and set messages, including those for other modules, flow through here first. - // any message that changes state, we want to check the passkey for - if (mp.from != 0 && !messageIsRequest(r) && !messageIsResponse(r)) { - if (!checkPassKey(r)) { - LOG_WARN("Admin message without session_key!"); - myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY, &mp); - return handled; + // all of the get and set messages, including those for other modules, flow through here first. + // any message that changes state, we want to check the passkey for + if (mp.from != 0 && !messageIsRequest(r) && !messageIsResponse(r)) { + if (!checkPassKey(r)) { + LOG_WARN("Admin message without session_key!"); + myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY, &mp); + return handled; + } } - } - switch (r->which_payload_variant) { + switch (r->which_payload_variant) { - /** - * Getters - */ - case meshtastic_AdminMessage_get_owner_request_tag: - LOG_DEBUG("Client got owner"); - handleGetOwner(mp); - break; - - case meshtastic_AdminMessage_get_config_request_tag: - LOG_DEBUG("Client got config"); - handleGetConfig(mp, r->get_config_request); - break; - - case meshtastic_AdminMessage_get_module_config_request_tag: - LOG_DEBUG("Client got module config"); - handleGetModuleConfig(mp, r->get_module_config_request); - break; - - case meshtastic_AdminMessage_get_channel_request_tag: { - uint32_t i = r->get_channel_request - 1; - LOG_DEBUG("Client got channel %u", i); - if (i >= MAX_NUM_CHANNELS) - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - else - handleGetChannel(mp, i); - break; - } - - /** - * Setters - */ - case meshtastic_AdminMessage_set_owner_tag: - LOG_DEBUG("Client set owner"); - // Validate names - if (*r->set_owner.long_name) { - const char *start = r->set_owner.long_name; - // Skip all whitespace (space, tab, newline, etc) - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + /** + * Getters + */ + case meshtastic_AdminMessage_get_owner_request_tag: + LOG_DEBUG("Client got owner"); + handleGetOwner(mp); break; - } - } - if (*r->set_owner.short_name) { - const char *start = r->set_owner.short_name; - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + + case meshtastic_AdminMessage_get_config_request_tag: + LOG_DEBUG("Client got config"); + handleGetConfig(mp, r->get_config_request); + break; + + case meshtastic_AdminMessage_get_module_config_request_tag: + LOG_DEBUG("Client got module config"); + handleGetModuleConfig(mp, r->get_module_config_request); + break; + + case meshtastic_AdminMessage_get_channel_request_tag: { + uint32_t i = r->get_channel_request - 1; + LOG_DEBUG("Client got channel %u", i); + if (i >= MAX_NUM_CHANNELS) + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + else + handleGetChannel(mp, i); break; - } } - handleSetOwner(r->set_owner); - break; - case meshtastic_AdminMessage_set_config_tag: - LOG_DEBUG("Client set config"); - handleSetConfig(r->set_config); - break; + /** + * Setters + */ + case meshtastic_AdminMessage_set_owner_tag: + LOG_DEBUG("Client set owner"); + // Validate names + if (*r->set_owner.long_name) { + const char *start = r->set_owner.long_name; + // Skip all whitespace (space, tab, newline, etc) + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; + } + } + if (*r->set_owner.short_name) { + const char *start = r->set_owner.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; + } + } + handleSetOwner(r->set_owner); + break; - case meshtastic_AdminMessage_set_module_config_tag: - LOG_DEBUG("Client set module config"); - if (!handleSetModuleConfig(r->set_module_config)) { - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + case meshtastic_AdminMessage_set_config_tag: + LOG_DEBUG("Client set config"); + handleSetConfig(r->set_config); + break; + + case meshtastic_AdminMessage_set_module_config_tag: + LOG_DEBUG("Client set module config"); + if (!handleSetModuleConfig(r->set_module_config)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + } + break; + + case meshtastic_AdminMessage_set_channel_tag: + LOG_DEBUG("Client set channel %d", r->set_channel.index); + if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + else + handleSetChannel(r->set_channel); + break; + case meshtastic_AdminMessage_set_ham_mode_tag: + LOG_DEBUG("Client set ham mode"); + handleSetHamMode(r->set_ham_mode); + break; + case meshtastic_AdminMessage_get_ui_config_request_tag: { + LOG_DEBUG("Client is getting device-ui config"); + handleGetDeviceUIConfig(mp); + handled = true; + break; } - break; - case meshtastic_AdminMessage_set_channel_tag: - LOG_DEBUG("Client set channel %d", r->set_channel.index); - if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); - else - handleSetChannel(r->set_channel); - break; - case meshtastic_AdminMessage_set_ham_mode_tag: - LOG_DEBUG("Client set ham mode"); - handleSetHamMode(r->set_ham_mode); - break; - case meshtastic_AdminMessage_get_ui_config_request_tag: { - LOG_DEBUG("Client is getting device-ui config"); - handleGetDeviceUIConfig(mp); - handled = true; - break; - } - - /** - * Other - */ - case meshtastic_AdminMessage_reboot_seconds_tag: { - reboot(r->reboot_seconds); - break; - } - case meshtastic_AdminMessage_reboot_ota_seconds_tag: { - int32_t s = r->reboot_ota_seconds; + /** + * Other + */ + case meshtastic_AdminMessage_reboot_seconds_tag: { + reboot(r->reboot_seconds); + break; + } + case meshtastic_AdminMessage_reboot_ota_seconds_tag: { + int32_t s = r->reboot_ota_seconds; #if defined(ARCH_ESP32) #if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (!BleOta::getOtaAppVersion().isEmpty()) { - if (screen) - screen->startFirmwareUpdateScreen(); - BleOta::switchToOtaApp(); - LOG_INFO("Rebooting to BLE OTA"); - } + if (!BleOta::getOtaAppVersion().isEmpty()) { + if (screen) + screen->startFirmwareUpdateScreen(); + BleOta::switchToOtaApp(); + LOG_INFO("Rebooting to BLE OTA"); + } #endif #if !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::trySwitchToOTA()) { - if (screen) - screen->startFirmwareUpdateScreen(); - WiFiOTA::saveConfig(&config.network); - LOG_INFO("Rebooting to WiFi OTA"); - } + if (WiFiOTA::trySwitchToOTA()) { + if (screen) + screen->startFirmwareUpdateScreen(); + WiFiOTA::saveConfig(&config.network); + LOG_INFO("Rebooting to WiFi OTA"); + } #endif #endif - LOG_INFO("Reboot in %d seconds", s); - rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); - break; - } - case meshtastic_AdminMessage_shutdown_seconds_tag: { - int32_t s = r->shutdown_seconds; - LOG_INFO("Shutdown in %d seconds", s); - shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000); - break; - } - case meshtastic_AdminMessage_get_device_metadata_request_tag: { - LOG_INFO("Client got device metadata"); - handleGetDeviceMetadata(mp); - break; - } - case meshtastic_AdminMessage_factory_reset_config_tag: { - disableBluetooth(); - LOG_INFO("Initiate factory config reset"); - nodeDB->factoryReset(); - LOG_INFO("Factory config reset finished, rebooting soon"); - reboot(DEFAULT_REBOOT_SECONDS); - break; - } - case meshtastic_AdminMessage_factory_reset_device_tag: { - disableBluetooth(); - LOG_INFO("Initiate full factory reset"); - nodeDB->factoryReset(true); - reboot(DEFAULT_REBOOT_SECONDS); - break; - } - case meshtastic_AdminMessage_nodedb_reset_tag: { - disableBluetooth(); - LOG_INFO("Initiate node-db reset"); - // CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a - // favorited node, so ensure that their favorites are kept on reset - bool rolePreference = isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); - nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset); - reboot(DEFAULT_REBOOT_SECONDS); - break; - } - case meshtastic_AdminMessage_store_ui_config_tag: { - LOG_INFO("Storing device-ui config"); - handleStoreDeviceUIConfig(r->store_ui_config); - handled = true; - break; - } - case meshtastic_AdminMessage_begin_edit_settings_tag: { - LOG_INFO("Begin transaction for editing settings"); - hasOpenEditTransaction = true; - break; - } - case meshtastic_AdminMessage_commit_edit_settings_tag: { - disableBluetooth(); - LOG_INFO("Commit transaction for edited settings"); - hasOpenEditTransaction = false; - saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); - break; - } - case meshtastic_AdminMessage_get_device_connection_status_request_tag: { - LOG_INFO("Client got device connection status"); - handleGetDeviceConnectionStatus(mp); - break; - } - case meshtastic_AdminMessage_get_module_config_response_tag: { - LOG_INFO("Client received a get_module_config response"); - if (fromOthers && r->get_module_config_response.which_payload_variant == meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) { - handleGetModuleConfigResponse(mp, r); + LOG_INFO("Reboot in %d seconds", s); + rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); + break; } - break; - } - case meshtastic_AdminMessage_remove_by_nodenum_tag: { - LOG_INFO("Client received remove_nodenum command"); - nodeDB->removeNodeByNum(r->remove_by_nodenum); - break; - } - case meshtastic_AdminMessage_add_contact_tag: { - LOG_INFO("Client received add_contact command"); - nodeDB->addFromContact(r->add_contact); - break; - } - case meshtastic_AdminMessage_set_favorite_node_tag: { - LOG_INFO("Client received set_favorite_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); - if (node != NULL) { - node->is_favorite = true; - saveChanges(SEGMENT_NODEDATABASE, false); - if (screen) - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens + case meshtastic_AdminMessage_shutdown_seconds_tag: { + int32_t s = r->shutdown_seconds; + LOG_INFO("Shutdown in %d seconds", s); + shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000); + break; } - break; - } - case meshtastic_AdminMessage_remove_favorite_node_tag: { - LOG_INFO("Client received remove_favorite_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); - if (node != NULL) { - node->is_favorite = false; - saveChanges(SEGMENT_NODEDATABASE, false); - if (screen) - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens + case meshtastic_AdminMessage_get_device_metadata_request_tag: { + LOG_INFO("Client got device metadata"); + handleGetDeviceMetadata(mp); + break; } - break; - } - case meshtastic_AdminMessage_set_ignored_node_tag: { - LOG_INFO("Client received set_ignored_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); - if (node != NULL) { - node->is_ignored = true; - node->has_device_metrics = false; - node->has_position = false; - node->user.public_key.size = 0; - node->user.public_key.bytes[0] = 0; - saveChanges(SEGMENT_NODEDATABASE, false); + case meshtastic_AdminMessage_factory_reset_config_tag: { + disableBluetooth(); + LOG_INFO("Initiate factory config reset"); + nodeDB->factoryReset(); + LOG_INFO("Factory config reset finished, rebooting soon"); + reboot(DEFAULT_REBOOT_SECONDS); + break; } - break; - } - case meshtastic_AdminMessage_remove_ignored_node_tag: { - LOG_INFO("Client received remove_ignored_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); - if (node != NULL) { - node->is_ignored = false; - saveChanges(SEGMENT_NODEDATABASE, false); + case meshtastic_AdminMessage_factory_reset_device_tag: { + disableBluetooth(); + LOG_INFO("Initiate full factory reset"); + nodeDB->factoryReset(true); + reboot(DEFAULT_REBOOT_SECONDS); + break; } - break; - } - case meshtastic_AdminMessage_set_fixed_position_tag: { - LOG_INFO("Client received set_fixed_position command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - node->has_position = true; - node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); - nodeDB->setLocalPosition(r->set_fixed_position); - config.position.fixed_position = true; - saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + case meshtastic_AdminMessage_nodedb_reset_tag: { + disableBluetooth(); + LOG_INFO("Initiate node-db reset"); + // CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a + // favorited node, so ensure that their favorites are kept on reset + bool rolePreference = + isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE, + meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); + nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset); + reboot(DEFAULT_REBOOT_SECONDS); + break; + } + case meshtastic_AdminMessage_store_ui_config_tag: { + LOG_INFO("Storing device-ui config"); + handleStoreDeviceUIConfig(r->store_ui_config); + handled = true; + break; + } + case meshtastic_AdminMessage_begin_edit_settings_tag: { + LOG_INFO("Begin transaction for editing settings"); + hasOpenEditTransaction = true; + break; + } + case meshtastic_AdminMessage_commit_edit_settings_tag: { + disableBluetooth(); + LOG_INFO("Commit transaction for edited settings"); + hasOpenEditTransaction = false; + saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); + break; + } + case meshtastic_AdminMessage_get_device_connection_status_request_tag: { + LOG_INFO("Client got device connection status"); + handleGetDeviceConnectionStatus(mp); + break; + } + case meshtastic_AdminMessage_get_module_config_response_tag: { + LOG_INFO("Client received a get_module_config response"); + if (fromOthers && r->get_module_config_response.which_payload_variant == + meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) { + handleGetModuleConfigResponse(mp, r); + } + break; + } + case meshtastic_AdminMessage_remove_by_nodenum_tag: { + LOG_INFO("Client received remove_nodenum command"); + nodeDB->removeNodeByNum(r->remove_by_nodenum); + break; + } + case meshtastic_AdminMessage_add_contact_tag: { + LOG_INFO("Client received add_contact command"); + nodeDB->addFromContact(r->add_contact); + break; + } + case meshtastic_AdminMessage_set_favorite_node_tag: { + LOG_INFO("Client received set_favorite_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); + if (node != NULL) { + node->is_favorite = true; + saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens + } + break; + } + case meshtastic_AdminMessage_remove_favorite_node_tag: { + LOG_INFO("Client received remove_favorite_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); + if (node != NULL) { + node->is_favorite = false; + saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens + } + break; + } + case meshtastic_AdminMessage_set_ignored_node_tag: { + LOG_INFO("Client received set_ignored_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); + if (node != NULL) { + node->is_ignored = true; + node->has_device_metrics = false; + node->has_position = false; + node->user.public_key.size = 0; + node->user.public_key.bytes[0] = 0; + saveChanges(SEGMENT_NODEDATABASE, false); + } + break; + } + case meshtastic_AdminMessage_remove_ignored_node_tag: { + LOG_INFO("Client received remove_ignored_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); + if (node != NULL) { + node->is_ignored = false; + saveChanges(SEGMENT_NODEDATABASE, false); + } + break; + } + case meshtastic_AdminMessage_set_fixed_position_tag: { + LOG_INFO("Client received set_fixed_position command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + node->has_position = true; + node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); + nodeDB->setLocalPosition(r->set_fixed_position); + config.position.fixed_position = true; + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); #if !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) - gps->enable(); - // Send our new fixed position to the mesh for good measure - positionModule->sendOurPosition(); + if (gps != nullptr) + gps->enable(); + // Send our new fixed position to the mesh for good measure + positionModule->sendOurPosition(); #endif - break; - } - case meshtastic_AdminMessage_remove_fixed_position_tag: { - LOG_INFO("Client received remove_fixed_position command"); - nodeDB->clearLocalPosition(); - config.position.fixed_position = false; - saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); - break; - } - case meshtastic_AdminMessage_set_time_only_tag: { - LOG_INFO("Client received set_time_only command"); - struct timeval tv; - tv.tv_sec = r->set_time_only; - tv.tv_usec = 0; + break; + } + case meshtastic_AdminMessage_remove_fixed_position_tag: { + LOG_INFO("Client received remove_fixed_position command"); + nodeDB->clearLocalPosition(); + config.position.fixed_position = false; + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + break; + } + case meshtastic_AdminMessage_set_time_only_tag: { + LOG_INFO("Client received set_time_only command"); + struct timeval tv; + tv.tv_sec = r->set_time_only; + tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv, false); - break; - } - case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { - LOG_INFO("Client requesting to enter DFU mode"); + perhapsSetRTC(RTCQualityNTP, &tv, false); + break; + } + case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { + LOG_INFO("Client requesting to enter DFU mode"); #if HAS_SCREEN - IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); + IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); #endif #if defined(ARCH_NRF52) || defined(ARCH_RP2040) - enterDfuMode(); + enterDfuMode(); #endif - break; - } - case meshtastic_AdminMessage_delete_file_request_tag: { - LOG_DEBUG("Client requesting to delete file: %s", r->delete_file_request); + break; + } + case meshtastic_AdminMessage_delete_file_request_tag: { + LOG_DEBUG("Client requesting to delete file: %s", r->delete_file_request); #ifdef FSCom - spiLock->lock(); - if (FSCom.remove(r->delete_file_request)) { - LOG_DEBUG("Successfully deleted file"); - } else { - LOG_DEBUG("Failed to delete file"); - } - spiLock->unlock(); + spiLock->lock(); + if (FSCom.remove(r->delete_file_request)) { + LOG_DEBUG("Successfully deleted file"); + } else { + LOG_DEBUG("Failed to delete file"); + } + spiLock->unlock(); #endif - break; - } - case meshtastic_AdminMessage_backup_preferences_tag: { - LOG_INFO("Client requesting to backup preferences"); - if (nodeDB->backupPreferences(r->backup_preferences)) { - myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); - } else { - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; } - break; - } - case meshtastic_AdminMessage_restore_preferences_tag: { - LOG_INFO("Client requesting to restore preferences"); - if (nodeDB->restorePreferences(r->backup_preferences, SEGMENT_DEVICESTATE | SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_CHANNELS)) { - myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); - LOG_DEBUG("Rebooting after successful restore of preferences"); - reboot(1000); - disableBluetooth(); - } else { - myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + case meshtastic_AdminMessage_backup_preferences_tag: { + LOG_INFO("Client requesting to backup preferences"); + if (nodeDB->backupPreferences(r->backup_preferences)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); + } else { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + } + break; } - break; - } - case meshtastic_AdminMessage_remove_backup_preferences_tag: { - LOG_INFO("Client requesting to remove backup preferences"); + case meshtastic_AdminMessage_restore_preferences_tag: { + LOG_INFO("Client requesting to restore preferences"); + if (nodeDB->restorePreferences(r->backup_preferences, + SEGMENT_DEVICESTATE | SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_CHANNELS)) { + myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); + LOG_DEBUG("Rebooting after successful restore of preferences"); + reboot(1000); + disableBluetooth(); + } else { + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + } + break; + } + case meshtastic_AdminMessage_remove_backup_preferences_tag: { + LOG_INFO("Client requesting to remove backup preferences"); #ifdef FSCom - if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_FLASH) { - spiLock->lock(); - FSCom.remove(backupFileName); - spiLock->unlock(); - } else if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_SD) { - // TODO: After more mainline SD card support - LOG_ERROR("SD backup removal not implemented yet"); - } + if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_FLASH) { + spiLock->lock(); + FSCom.remove(backupFileName); + spiLock->unlock(); + } else if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_SD) { + // TODO: After more mainline SD card support + LOG_ERROR("SD backup removal not implemented yet"); + } #endif - break; - } - case meshtastic_AdminMessage_send_input_event_tag: { - LOG_INFO("Client requesting to send input event"); - handleSendInputEvent(r->send_input_event); - break; - } + break; + } + case meshtastic_AdminMessage_send_input_event_tag: { + LOG_INFO("Client requesting to send input event"); + handleSendInputEvent(r->send_input_event); + break; + } #ifdef ARCH_PORTDUINO - case meshtastic_AdminMessage_exit_simulator_tag: - LOG_INFO("Exiting simulator"); - exit(0); - break; + case meshtastic_AdminMessage_exit_simulator_tag: + LOG_INFO("Exiting simulator"); + exit(0); + break; #endif - default: - meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - AdminMessageHandleResult handleResult = MeshModule::handleAdminMessageForAllModules(mp, r, &res); + default: + meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; + AdminMessageHandleResult handleResult = MeshModule::handleAdminMessageForAllModules(mp, r, &res); - if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { - setPassKey(&res); - myReply = allocDataProtobuf(res); - } else if (mp.decoded.want_response) { - LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); - } else if (handleResult != AdminMessageHandleResult::HANDLED) { - // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages - LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); + if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&res); + myReply = allocDataProtobuf(res); + } else if (mp.decoded.want_response) { + LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); + } else if (handleResult != AdminMessageHandleResult::HANDLED) { + // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages + LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); + } + break; } - break; - } - // Allow any observers (e.g. the UI) to handle/respond - AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; - meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; - AdminModule_ObserverData observerData = { - .request = r, - .response = &observerResponse, - .result = &observerResult, - }; + // Allow any observers (e.g. the UI) to handle/respond + AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; + meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; + AdminModule_ObserverData observerData = { + .request = r, + .response = &observerResponse, + .result = &observerResult, + }; - notifyObservers(&observerData); + notifyObservers(&observerData); - if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { - setPassKey(&observerResponse); - myReply = allocDataProtobuf(observerResponse); - LOG_DEBUG("Observer responded to admin message"); - } else if (observerResult == AdminMessageHandleResult::HANDLED) { - LOG_DEBUG("Observer handled admin message"); - } + if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&observerResponse); + myReply = allocDataProtobuf(observerResponse); + LOG_DEBUG("Observer responded to admin message"); + } else if (observerResult == AdminMessageHandleResult::HANDLED) { + LOG_DEBUG("Observer handled admin message"); + } - // If asked for a response and it is not yet set, generate an 'ACK' response - if (mp.decoded.want_response && !myReply) { - myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); - } - if (mp.pki_encrypted && myReply) { - myReply->pki_encrypted = true; - } - return handled; + // If asked for a response and it is not yet set, generate an 'ACK' response + if (mp.decoded.want_response && !myReply) { + myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); + } + if (mp.pki_encrypted && myReply) { + myReply->pki_encrypted = true; + } + return handled; } -void AdminModule::handleGetModuleConfigResponse(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) { - // Skip if it's disabled or no pins are exposed - if (!r->get_module_config_response.payload_variant.remote_hardware.enabled || - r->get_module_config_response.payload_variant.remote_hardware.available_pins_count == 0) { - LOG_DEBUG("Remote hardware module disabled or no available_pins. Skip"); - return; - } - for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { - if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { - continue; +void AdminModule::handleGetModuleConfigResponse(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) +{ + // Skip if it's disabled or no pins are exposed + if (!r->get_module_config_response.payload_variant.remote_hardware.enabled || + r->get_module_config_response.payload_variant.remote_hardware.available_pins_count == 0) { + LOG_DEBUG("Remote hardware module disabled or no available_pins. Skip"); + return; } - for (uint8_t j = 0; j < r->get_module_config_response.payload_variant.remote_hardware.available_pins_count; j++) { - auto availablePin = r->get_module_config_response.payload_variant.remote_hardware.available_pins[j]; - if (i < devicestate.node_remote_hardware_pins_count) { - devicestate.node_remote_hardware_pins[i].node_num = mp.from; - devicestate.node_remote_hardware_pins[i].pin = availablePin; - } - i++; + for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { + if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { + continue; + } + for (uint8_t j = 0; j < r->get_module_config_response.payload_variant.remote_hardware.available_pins_count; j++) { + auto availablePin = r->get_module_config_response.payload_variant.remote_hardware.available_pins[j]; + if (i < devicestate.node_remote_hardware_pins_count) { + devicestate.node_remote_hardware_pins[i].node_num = mp.from; + devicestate.node_remote_hardware_pins[i].pin = availablePin; + } + i++; + } } - } } /** * Setter methods */ -void AdminModule::handleSetOwner(const meshtastic_User &o) { - int changed = 0; +void AdminModule::handleSetOwner(const meshtastic_User &o) +{ + int changed = 0; - if (*o.long_name) { - changed |= strcmp(owner.long_name, o.long_name); - strncpy(owner.long_name, o.long_name, sizeof(owner.long_name)); - } - if (*o.short_name) { - changed |= strcmp(owner.short_name, o.short_name); - strncpy(owner.short_name, o.short_name, sizeof(owner.short_name)); - } - snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum()); - - if (owner.is_licensed != o.is_licensed) { - changed = 1; - owner.is_licensed = o.is_licensed; - if (channels.ensureLicensedOperation()) { - sendWarning(licensedModeMessage); + if (*o.long_name) { + changed |= strcmp(owner.long_name, o.long_name); + strncpy(owner.long_name, o.long_name, sizeof(owner.long_name)); } - } - if (owner.has_is_unmessagable != o.has_is_unmessagable || (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { - changed = 1; - owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; - owner.is_unmessagable = o.is_unmessagable; - } + if (*o.short_name) { + changed |= strcmp(owner.short_name, o.short_name); + strncpy(owner.short_name, o.short_name, sizeof(owner.short_name)); + } + snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum()); - if (changed) { // If nothing really changed, don't broadcast on the network or write to flash - service->reloadOwner(!hasOpenEditTransaction); - saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE); - } + if (owner.is_licensed != o.is_licensed) { + changed = 1; + owner.is_licensed = o.is_licensed; + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } + } + if (owner.has_is_unmessagable != o.has_is_unmessagable || + (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { + changed = 1; + owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; + owner.is_unmessagable = o.is_unmessagable; + } + + if (changed) { // If nothing really changed, don't broadcast on the network or write to flash + service->reloadOwner(!hasOpenEditTransaction); + saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE); + } } -void AdminModule::handleSetConfig(const meshtastic_Config &c) { - auto changes = SEGMENT_CONFIG; - auto existingRole = config.device.role; - bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); - bool requiresReboot = true; +void AdminModule::handleSetConfig(const meshtastic_Config &c) +{ + auto changes = SEGMENT_CONFIG; + auto existingRole = config.device.role; + bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); + bool requiresReboot = true; - switch (c.which_payload_variant) { - case meshtastic_Config_device_tag: - LOG_INFO("Set config: Device"); - config.has_device = true; + switch (c.which_payload_variant) { + case meshtastic_Config_device_tag: + LOG_INFO("Set config: Device"); + config.has_device = true; #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && - accelerometerThread->enabled == false) { - config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; - accelerometerThread->enabled = true; - accelerometerThread->start(); - } + if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && + accelerometerThread->enabled == false) { + config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; + accelerometerThread->enabled = true; + accelerometerThread->start(); + } #endif #ifdef LED_PIN - // Turn LED off if heartbeat by config - if (c.payload_variant.device.led_heartbeat_disabled) { - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); - } + // Turn LED off if heartbeat by config + if (c.payload_variant.device.led_heartbeat_disabled) { + digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); + } #endif - if (config.device.button_gpio == c.payload_variant.device.button_gpio && config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && - config.device.role == c.payload_variant.device.role && config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) { - requiresReboot = false; - } - config.device = c.payload_variant.device; - if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - const char *warning = "Rebroadcast mode can't be set to NONE for a router"; - LOG_WARN(warning); - sendWarning(warning); - } - // If we're setting router role for the first time, install its intervals - if (existingRole != c.payload_variant.device.role) { - nodeDB->installRoleDefaults(c.payload_variant.device.role); - changes |= SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE; // Some role defaults affect owner - } - if (config.device.node_info_broadcast_secs < min_node_info_broadcast_secs) { - LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d", min_node_info_broadcast_secs); - config.device.node_info_broadcast_secs = min_node_info_broadcast_secs; - } - // Router Client and Repeater deprecated; Set it to client - if (IS_ONE_OF(c.payload_variant.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT, meshtastic_Config_DeviceConfig_Role_REPEATER)) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - if (moduleConfig.store_forward.enabled && !moduleConfig.store_forward.is_server) { - moduleConfig.store_forward.is_server = true; - changes |= SEGMENT_MODULECONFIG; - requiresReboot = true; - } - } + if (config.device.button_gpio == c.payload_variant.device.button_gpio && + config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && + config.device.role == c.payload_variant.device.role && + config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) { + requiresReboot = false; + } + config.device = c.payload_variant.device; + if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; + const char *warning = "Rebroadcast mode can't be set to NONE for a router"; + LOG_WARN(warning); + sendWarning(warning); + } + // If we're setting router role for the first time, install its intervals + if (existingRole != c.payload_variant.device.role) { + nodeDB->installRoleDefaults(c.payload_variant.device.role); + changes |= SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE; // Some role defaults affect owner + } + if (config.device.node_info_broadcast_secs < min_node_info_broadcast_secs) { + LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d", min_node_info_broadcast_secs); + config.device.node_info_broadcast_secs = min_node_info_broadcast_secs; + } + // Router Client and Repeater deprecated; Set it to client + if (IS_ONE_OF(c.payload_variant.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT, + meshtastic_Config_DeviceConfig_Role_REPEATER)) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + if (moduleConfig.store_forward.enabled && !moduleConfig.store_forward.is_server) { + moduleConfig.store_forward.is_server = true; + changes |= SEGMENT_MODULECONFIG; + requiresReboot = true; + } + } #if USERPREFS_EVENT_MODE - // If we're in event mode, nobody is a Router or Router Late - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { - config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; - } + // If we're in event mode, nobody is a Router or Router Late + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } #endif - break; - case meshtastic_Config_position_tag: - LOG_INFO("Set config: Position"); - config.has_position = true; - // If we have turned off the GPS (disabled or not present) and we're not using fixed position, - // clear the stored position since it may not get updated - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && - c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && config.position.fixed_position == false && - c.payload_variant.position.fixed_position == false) { - nodeDB->clearLocalPosition(); - saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); - } - config.position = c.payload_variant.position; + break; + case meshtastic_Config_position_tag: + LOG_INFO("Set config: Position"); + config.has_position = true; + // If we have turned off the GPS (disabled or not present) and we're not using fixed position, + // clear the stored position since it may not get updated + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + } + config.position = c.payload_variant.position; - // Save nodedb as well in case we got a fixed position packet - break; - case meshtastic_Config_power_tag: - LOG_INFO("Set config: Power"); - config.has_power = true; - // Really just the adc override is the only thing that can change without a reboot - if (config.power.device_battery_ina_address == c.payload_variant.power.device_battery_ina_address && - config.power.is_power_saving == c.payload_variant.power.is_power_saving && config.power.ls_secs == c.payload_variant.power.ls_secs && - config.power.min_wake_secs == c.payload_variant.power.min_wake_secs && - config.power.on_battery_shutdown_after_secs == c.payload_variant.power.on_battery_shutdown_after_secs && - config.power.sds_secs == c.payload_variant.power.sds_secs && - config.power.wait_bluetooth_secs == c.payload_variant.power.wait_bluetooth_secs) { - requiresReboot = false; - } - config.power = c.payload_variant.power; - if (c.payload_variant.power.on_battery_shutdown_after_secs > 0 && c.payload_variant.power.on_battery_shutdown_after_secs < 30) { - LOG_WARN("Tried to set on_battery_shutdown_after_secs too low, set to min 30 seconds"); - config.power.on_battery_shutdown_after_secs = 30; - } - break; - case meshtastic_Config_network_tag: - LOG_INFO("Set config: WiFi"); - config.has_network = true; - config.network = c.payload_variant.network; - break; - case meshtastic_Config_display_tag: - LOG_INFO("Set config: Display"); - config.has_display = true; - if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && - config.display.flip_screen == c.payload_variant.display.flip_screen && config.display.oled == c.payload_variant.display.oled && - config.display.displaymode == c.payload_variant.display.displaymode) { - requiresReboot = false; - } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && - c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - config.bluetooth.enabled = false; - } + // Save nodedb as well in case we got a fixed position packet + break; + case meshtastic_Config_power_tag: + LOG_INFO("Set config: Power"); + config.has_power = true; + // Really just the adc override is the only thing that can change without a reboot + if (config.power.device_battery_ina_address == c.payload_variant.power.device_battery_ina_address && + config.power.is_power_saving == c.payload_variant.power.is_power_saving && + config.power.ls_secs == c.payload_variant.power.ls_secs && + config.power.min_wake_secs == c.payload_variant.power.min_wake_secs && + config.power.on_battery_shutdown_after_secs == c.payload_variant.power.on_battery_shutdown_after_secs && + config.power.sds_secs == c.payload_variant.power.sds_secs && + config.power.wait_bluetooth_secs == c.payload_variant.power.wait_bluetooth_secs) { + requiresReboot = false; + } + config.power = c.payload_variant.power; + if (c.payload_variant.power.on_battery_shutdown_after_secs > 0 && + c.payload_variant.power.on_battery_shutdown_after_secs < 30) { + LOG_WARN("Tried to set on_battery_shutdown_after_secs too low, set to min 30 seconds"); + config.power.on_battery_shutdown_after_secs = 30; + } + break; + case meshtastic_Config_network_tag: + LOG_INFO("Set config: WiFi"); + config.has_network = true; + config.network = c.payload_variant.network; + break; + case meshtastic_Config_display_tag: + LOG_INFO("Set config: Display"); + config.has_display = true; + if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && + config.display.flip_screen == c.payload_variant.display.flip_screen && + config.display.oled == c.payload_variant.display.oled && + config.display.displaymode == c.payload_variant.display.displaymode) { + requiresReboot = false; + } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && + c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + config.bluetooth.enabled = false; + } #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && - accelerometerThread->enabled == false) { - config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; - accelerometerThread->enabled = true; - accelerometerThread->start(); - } + if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && + accelerometerThread->enabled == false) { + config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; + accelerometerThread->enabled = true; + accelerometerThread->start(); + } #endif - config.display = c.payload_variant.display; - break; + config.display = c.payload_variant.display; + break; - case meshtastic_Config_lora_tag: { - // Wrap the entire case in a block to scope variables and avoid crossing initialization - auto oldLoraConfig = config.lora; - auto validatedLora = c.payload_variant.lora; + case meshtastic_Config_lora_tag: { + // Wrap the entire case in a block to scope variables and avoid crossing initialization + auto oldLoraConfig = config.lora; + auto validatedLora = c.payload_variant.lora; - LOG_INFO("Set config: LoRa"); - config.has_lora = true; + LOG_INFO("Set config: LoRa"); + config.has_lora = true; - if (validatedLora.coding_rate < 4 || validatedLora.coding_rate > 8) { - LOG_WARN("Invalid coding_rate %d, setting to 5", validatedLora.coding_rate); - validatedLora.coding_rate = 5; - } + if (validatedLora.coding_rate < 4 || validatedLora.coding_rate > 8) { + LOG_WARN("Invalid coding_rate %d, setting to 5", validatedLora.coding_rate); + validatedLora.coding_rate = 5; + } - if (validatedLora.spread_factor < 7 || validatedLora.spread_factor > 12) { - LOG_WARN("Invalid spread_factor %d, setting to 11", validatedLora.spread_factor); - validatedLora.spread_factor = 11; - } + if (validatedLora.spread_factor < 7 || validatedLora.spread_factor > 12) { + LOG_WARN("Invalid spread_factor %d, setting to 11", validatedLora.spread_factor); + validatedLora.spread_factor = 11; + } - if (validatedLora.bandwidth == 0) { - int originalBandwidth = validatedLora.bandwidth; - validatedLora.bandwidth = myRegion->wideLora ? 800 : 250; - LOG_WARN("Invalid bandwidth %d, setting to default", originalBandwidth); - } + if (validatedLora.bandwidth == 0) { + int originalBandwidth = validatedLora.bandwidth; + validatedLora.bandwidth = myRegion->wideLora ? 800 : 250; + LOG_WARN("Invalid bandwidth %d, setting to default", originalBandwidth); + } - // If no lora radio parameters change, don't need to reboot - if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && - oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && - oldLoraConfig.spread_factor == validatedLora.spread_factor && oldLoraConfig.coding_rate == validatedLora.coding_rate && - oldLoraConfig.tx_power == validatedLora.tx_power && oldLoraConfig.frequency_offset == validatedLora.frequency_offset && - oldLoraConfig.override_frequency == validatedLora.override_frequency && oldLoraConfig.channel_num == validatedLora.channel_num && - oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { - requiresReboot = false; - } + // If no lora radio parameters change, don't need to reboot + if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && + oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && + oldLoraConfig.spread_factor == validatedLora.spread_factor && + oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power && + oldLoraConfig.frequency_offset == validatedLora.frequency_offset && + oldLoraConfig.override_frequency == validatedLora.override_frequency && + oldLoraConfig.channel_num == validatedLora.channel_num && + oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { + requiresReboot = false; + } #if defined(ARCH_PORTDUINO) - // If running on portduino and using SimRadio, do not require reboot - if (SimRadio::instance) { - requiresReboot = false; - } + // If running on portduino and using SimRadio, do not require reboot + if (SimRadio::instance) { + requiresReboot = false; + } #endif #ifdef RF95_FAN_EN - // Turn PA off if disabled by config - if (c.payload_variant.lora.pa_fan_disabled) { - digitalWrite(RF95_FAN_EN, LOW ^ 0); - } else { - digitalWrite(RF95_FAN_EN, HIGH ^ 0); - } -#endif - config.lora = validatedLora; - // If we're setting region for the first time, init the region and regenerate the keys - if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { -#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } + // Turn PA off if disabled by config + if (c.payload_variant.lora.pa_fan_disabled) { + digitalWrite(RF95_FAN_EN, LOW ^ 0); } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; + digitalWrite(RF95_FAN_EN, HIGH ^ 0); } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } #endif - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - // Compare the entire string, we are sure of the length as a topic has never been set - if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } - } - if (config.lora.region != myRegion->code) { - // Region has changed so check whether there is a regulatory one we should be using instead. - // Additionally as a side-effect, assume a new value under myRegion - initRegion(); + config.lora = validatedLora; + // If we're setting region for the first time, init the region and regenerate the keys + if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } +#endif + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + // Compare the entire string, we are sure of the length as a topic has never been set + if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } + } + if (config.lora.region != myRegion->code) { + // Region has changed so check whether there is a regulatory one we should be using instead. + // Additionally as a side-effect, assume a new value under myRegion + initRegion(); - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default root is in use, so subscribe to the appropriate MQTT topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - } + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + // Default root is in use, so subscribe to the appropriate MQTT topic for this region + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + } - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } + break; } - break; - } - case meshtastic_Config_bluetooth_tag: - LOG_INFO("Set config: Bluetooth"); - config.has_bluetooth = true; - config.bluetooth = c.payload_variant.bluetooth; - break; - case meshtastic_Config_security_tag: - LOG_INFO("Set config: Security"); - config.security = c.payload_variant.security; + case meshtastic_Config_bluetooth_tag: + LOG_INFO("Set config: Bluetooth"); + config.has_bluetooth = true; + config.bluetooth = c.payload_variant.bluetooth; + break; + case meshtastic_Config_security_tag: + LOG_INFO("Set config: Security"); + config.security = c.payload_variant.security; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI) - // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode - if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - if (config.security.private_key.size != 32) { - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (config.security.private_key.size != 32) { + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - } else { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - config.security.public_key.size = 32; + } else { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + config.security.public_key.size = 32; + } + } } - } - } #endif - owner.public_key.size = config.security.public_key.size; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); + owner.public_key.size = config.security.public_key.size; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); #if !MESHTASTIC_EXCLUDE_PKI - crypto->setDHPrivateKey(config.security.private_key.bytes); + crypto->setDHPrivateKey(config.security.private_key.bytes); #endif - if (config.security.is_managed && - !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 || config.security.admin_key[2].size == 32)) { - config.security.is_managed = false; - const char *warning = "You must provide at least one admin public key to enable managed mode"; - LOG_WARN(warning); - sendWarning(warning); + if (config.security.is_managed && !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 || + config.security.admin_key[2].size == 32)) { + config.security.is_managed = false; + const char *warning = "You must provide at least one admin public key to enable managed mode"; + LOG_WARN(warning); + sendWarning(warning); + } + + if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled && + config.security.serial_enabled == c.payload_variant.security.serial_enabled) + requiresReboot = false; + + break; + case meshtastic_Config_device_ui_tag: + // NOOP! This is handled by handleStoreDeviceUIConfig + break; + } + if (requiresReboot && !hasOpenEditTransaction) { + disableBluetooth(); } - if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled && - config.security.serial_enabled == c.payload_variant.security.serial_enabled) - requiresReboot = false; - - break; - case meshtastic_Config_device_ui_tag: - // NOOP! This is handled by handleStoreDeviceUIConfig - break; - } - if (requiresReboot && !hasOpenEditTransaction) { - disableBluetooth(); - } - - saveChanges(changes, requiresReboot); + saveChanges(changes, requiresReboot); } -bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { - // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth - // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { - disableBluetooth(); - } +bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) +{ + // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth + // Otherwise, disable Bluetooth to prevent the phone from interfering with the config + if (!hasOpenEditTransaction && + !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { + disableBluetooth(); + } - switch (c.which_payload_variant) { - case meshtastic_ModuleConfig_mqtt_tag: + switch (c.which_payload_variant) { + case meshtastic_ModuleConfig_mqtt_tag: #if MESHTASTIC_EXCLUDE_MQTT - LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config"); - return false; + LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config"); + return false; #else - LOG_INFO("Set module config: MQTT"); - if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { - return false; - } - // Disable Bluetooth to prevent interference during MQTT configuration - disableBluetooth(); - moduleConfig.has_mqtt = true; - moduleConfig.mqtt = c.payload_variant.mqtt; + LOG_INFO("Set module config: MQTT"); + if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { + return false; + } + // Disable Bluetooth to prevent interference during MQTT configuration + disableBluetooth(); + moduleConfig.has_mqtt = true; + moduleConfig.mqtt = c.payload_variant.mqtt; #endif - break; - case meshtastic_ModuleConfig_serial_tag: - LOG_INFO("Set module config: Serial"); -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (!SerialModule::isValidConfig(c.payload_variant.serial)) { - LOG_ERROR("Invalid serial config"); - return false; - } - disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration + break; + case meshtastic_ModuleConfig_serial_tag: + LOG_INFO("Set module config: Serial"); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + if (!SerialModule::isValidConfig(c.payload_variant.serial)) { + LOG_ERROR("Invalid serial config"); + return false; + } + disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration #endif - moduleConfig.has_serial = true; - moduleConfig.serial = c.payload_variant.serial; - break; - case meshtastic_ModuleConfig_external_notification_tag: - LOG_INFO("Set module config: External Notification"); - moduleConfig.has_external_notification = true; - moduleConfig.external_notification = c.payload_variant.external_notification; - break; - case meshtastic_ModuleConfig_store_forward_tag: - LOG_INFO("Set module config: Store & Forward"); - moduleConfig.has_store_forward = true; - moduleConfig.store_forward = c.payload_variant.store_forward; - break; - case meshtastic_ModuleConfig_range_test_tag: - LOG_INFO("Set module config: Range Test"); - moduleConfig.has_range_test = true; - moduleConfig.range_test = c.payload_variant.range_test; - break; - case meshtastic_ModuleConfig_telemetry_tag: - LOG_INFO("Set module config: Telemetry"); - moduleConfig.has_telemetry = true; - moduleConfig.telemetry = c.payload_variant.telemetry; - break; - case meshtastic_ModuleConfig_canned_message_tag: - LOG_INFO("Set module config: Canned Message"); - moduleConfig.has_canned_message = true; - moduleConfig.canned_message = c.payload_variant.canned_message; - break; - case meshtastic_ModuleConfig_audio_tag: - LOG_INFO("Set module config: Audio"); - moduleConfig.has_audio = true; - moduleConfig.audio = c.payload_variant.audio; - break; - case meshtastic_ModuleConfig_remote_hardware_tag: - LOG_INFO("Set module config: Remote Hardware"); - moduleConfig.has_remote_hardware = true; - moduleConfig.remote_hardware = c.payload_variant.remote_hardware; - break; - case meshtastic_ModuleConfig_neighbor_info_tag: - LOG_INFO("Set module config: Neighbor Info"); - moduleConfig.has_neighbor_info = true; - if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) { - LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs); - moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs; + moduleConfig.has_serial = true; + moduleConfig.serial = c.payload_variant.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + LOG_INFO("Set module config: External Notification"); + moduleConfig.has_external_notification = true; + moduleConfig.external_notification = c.payload_variant.external_notification; + break; + case meshtastic_ModuleConfig_store_forward_tag: + LOG_INFO("Set module config: Store & Forward"); + moduleConfig.has_store_forward = true; + moduleConfig.store_forward = c.payload_variant.store_forward; + break; + case meshtastic_ModuleConfig_range_test_tag: + LOG_INFO("Set module config: Range Test"); + moduleConfig.has_range_test = true; + moduleConfig.range_test = c.payload_variant.range_test; + break; + case meshtastic_ModuleConfig_telemetry_tag: + LOG_INFO("Set module config: Telemetry"); + moduleConfig.has_telemetry = true; + moduleConfig.telemetry = c.payload_variant.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + LOG_INFO("Set module config: Canned Message"); + moduleConfig.has_canned_message = true; + moduleConfig.canned_message = c.payload_variant.canned_message; + break; + case meshtastic_ModuleConfig_audio_tag: + LOG_INFO("Set module config: Audio"); + moduleConfig.has_audio = true; + moduleConfig.audio = c.payload_variant.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + LOG_INFO("Set module config: Remote Hardware"); + moduleConfig.has_remote_hardware = true; + moduleConfig.remote_hardware = c.payload_variant.remote_hardware; + break; + case meshtastic_ModuleConfig_neighbor_info_tag: + LOG_INFO("Set module config: Neighbor Info"); + moduleConfig.has_neighbor_info = true; + if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) { + LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs); + moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs; + } + moduleConfig.neighbor_info = c.payload_variant.neighbor_info; + break; + case meshtastic_ModuleConfig_detection_sensor_tag: + LOG_INFO("Set module config: Detection Sensor"); + moduleConfig.has_detection_sensor = true; + moduleConfig.detection_sensor = c.payload_variant.detection_sensor; + break; + case meshtastic_ModuleConfig_ambient_lighting_tag: + LOG_INFO("Set module config: Ambient Lighting"); + moduleConfig.has_ambient_lighting = true; + moduleConfig.ambient_lighting = c.payload_variant.ambient_lighting; + break; + case meshtastic_ModuleConfig_paxcounter_tag: + LOG_INFO("Set module config: Paxcounter"); + moduleConfig.has_paxcounter = true; + moduleConfig.paxcounter = c.payload_variant.paxcounter; + break; } - moduleConfig.neighbor_info = c.payload_variant.neighbor_info; - break; - case meshtastic_ModuleConfig_detection_sensor_tag: - LOG_INFO("Set module config: Detection Sensor"); - moduleConfig.has_detection_sensor = true; - moduleConfig.detection_sensor = c.payload_variant.detection_sensor; - break; - case meshtastic_ModuleConfig_ambient_lighting_tag: - LOG_INFO("Set module config: Ambient Lighting"); - moduleConfig.has_ambient_lighting = true; - moduleConfig.ambient_lighting = c.payload_variant.ambient_lighting; - break; - case meshtastic_ModuleConfig_paxcounter_tag: - LOG_INFO("Set module config: Paxcounter"); - moduleConfig.has_paxcounter = true; - moduleConfig.paxcounter = c.payload_variant.paxcounter; - break; - } - saveChanges(SEGMENT_MODULECONFIG); - return true; + saveChanges(SEGMENT_MODULECONFIG); + return true; } -void AdminModule::handleSetChannel(const meshtastic_Channel &cc) { - channels.setChannel(cc); - if (channels.ensureLicensedOperation()) { - sendWarning(licensedModeMessage); - } - channels.onConfigChanged(); // tell the radios about this change - saveChanges(SEGMENT_CHANNELS, false); +void AdminModule::handleSetChannel(const meshtastic_Channel &cc) +{ + channels.setChannel(cc); + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } + channels.onConfigChanged(); // tell the radios about this change + saveChanges(SEGMENT_CHANNELS, false); } /** * Getters */ -void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) { - if (req.decoded.want_response) { - // We create the reply here +void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) +{ + if (req.decoded.want_response) { + // We create the reply here + meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; + res.get_owner_response = owner; + + res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; + setPassKey(&res); + myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } + } +} + +void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32_t configType) +{ meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - res.get_owner_response = owner; - res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; - setPassKey(&res); - myReply = allocDataProtobuf(res); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; + if (req.decoded.want_response) { + switch (configType) { + case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG: + LOG_INFO("Get config: Device"); + res.get_config_response.which_payload_variant = meshtastic_Config_device_tag; + res.get_config_response.payload_variant.device = config.device; + break; + case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG: + LOG_INFO("Get config: Position"); + res.get_config_response.which_payload_variant = meshtastic_Config_position_tag; + res.get_config_response.payload_variant.position = config.position; + break; + case meshtastic_AdminMessage_ConfigType_POWER_CONFIG: + LOG_INFO("Get config: Power"); + res.get_config_response.which_payload_variant = meshtastic_Config_power_tag; + res.get_config_response.payload_variant.power = config.power; + break; + case meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG: + LOG_INFO("Get config: Network"); + res.get_config_response.which_payload_variant = meshtastic_Config_network_tag; + res.get_config_response.payload_variant.network = config.network; + writeSecret(res.get_config_response.payload_variant.network.wifi_psk, + sizeof(res.get_config_response.payload_variant.network.wifi_psk), config.network.wifi_psk); + break; + case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG: + LOG_INFO("Get config: Display"); + res.get_config_response.which_payload_variant = meshtastic_Config_display_tag; + res.get_config_response.payload_variant.display = config.display; + break; + case meshtastic_AdminMessage_ConfigType_LORA_CONFIG: + LOG_INFO("Get config: LoRa"); + res.get_config_response.which_payload_variant = meshtastic_Config_lora_tag; + res.get_config_response.payload_variant.lora = config.lora; + break; + case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG: + LOG_INFO("Get config: Bluetooth"); + res.get_config_response.which_payload_variant = meshtastic_Config_bluetooth_tag; + res.get_config_response.payload_variant.bluetooth = config.bluetooth; + break; + case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG: + LOG_INFO("Get config: Security"); + res.get_config_response.which_payload_variant = meshtastic_Config_security_tag; + res.get_config_response.payload_variant.security = config.security; + break; + case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG: + LOG_INFO("Get config: Sessionkey"); + res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_tag; + break; + case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG: + // NOOP! This is handled by handleGetDeviceUIConfig + res.get_config_response.which_payload_variant = meshtastic_Config_device_ui_tag; + break; + } + // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. + // So even if we internally use 0 to represent 'use default' we still need to send the value we are + // using to the app (so that even old phone apps work with new device loads). + // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; + res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; + setPassKey(&res); + myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } - } } -void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32_t configType) { - meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; +void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const uint32_t configType) +{ + meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - if (req.decoded.want_response) { - switch (configType) { - case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG: - LOG_INFO("Get config: Device"); - res.get_config_response.which_payload_variant = meshtastic_Config_device_tag; - res.get_config_response.payload_variant.device = config.device; - break; - case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG: - LOG_INFO("Get config: Position"); - res.get_config_response.which_payload_variant = meshtastic_Config_position_tag; - res.get_config_response.payload_variant.position = config.position; - break; - case meshtastic_AdminMessage_ConfigType_POWER_CONFIG: - LOG_INFO("Get config: Power"); - res.get_config_response.which_payload_variant = meshtastic_Config_power_tag; - res.get_config_response.payload_variant.power = config.power; - break; - case meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG: - LOG_INFO("Get config: Network"); - res.get_config_response.which_payload_variant = meshtastic_Config_network_tag; - res.get_config_response.payload_variant.network = config.network; - writeSecret(res.get_config_response.payload_variant.network.wifi_psk, sizeof(res.get_config_response.payload_variant.network.wifi_psk), - config.network.wifi_psk); - break; - case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG: - LOG_INFO("Get config: Display"); - res.get_config_response.which_payload_variant = meshtastic_Config_display_tag; - res.get_config_response.payload_variant.display = config.display; - break; - case meshtastic_AdminMessage_ConfigType_LORA_CONFIG: - LOG_INFO("Get config: LoRa"); - res.get_config_response.which_payload_variant = meshtastic_Config_lora_tag; - res.get_config_response.payload_variant.lora = config.lora; - break; - case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG: - LOG_INFO("Get config: Bluetooth"); - res.get_config_response.which_payload_variant = meshtastic_Config_bluetooth_tag; - res.get_config_response.payload_variant.bluetooth = config.bluetooth; - break; - case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG: - LOG_INFO("Get config: Security"); - res.get_config_response.which_payload_variant = meshtastic_Config_security_tag; - res.get_config_response.payload_variant.security = config.security; - break; - case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG: - LOG_INFO("Get config: Sessionkey"); - res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_tag; - break; - case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG: - // NOOP! This is handled by handleGetDeviceUIConfig - res.get_config_response.which_payload_variant = meshtastic_Config_device_ui_tag; - break; + if (req.decoded.want_response) { + switch (configType) { + case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: + LOG_INFO("Get module config: MQTT"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; + res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt; + break; + case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: + LOG_INFO("Get module config: Serial"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag; + res.get_module_config_response.payload_variant.serial = moduleConfig.serial; + break; + case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: + LOG_INFO("Get module config: External Notification"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; + res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification; + break; + case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: + LOG_INFO("Get module config: Store & Forward"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; + res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward; + break; + case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: + LOG_INFO("Get module config: Range Test"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; + res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test; + break; + case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: + LOG_INFO("Get module config: Telemetry"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; + res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry; + break; + case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: + LOG_INFO("Get module config: Canned Message"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; + res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; + break; + case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: + LOG_INFO("Get module config: Audio"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; + res.get_module_config_response.payload_variant.audio = moduleConfig.audio; + break; + case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: + LOG_INFO("Get module config: Remote Hardware"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; + res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware; + break; + case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: + LOG_INFO("Get module config: Neighbor Info"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; + res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; + break; + case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: + LOG_INFO("Get module config: Detection Sensor"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; + res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor; + break; + case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: + LOG_INFO("Get module config: Ambient Lighting"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; + res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; + break; + case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: + LOG_INFO("Get module config: Paxcounter"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; + res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; + break; + } + + // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. + // So even if we internally use 0 to represent 'use default' we still need to send the value we are + // using to the app (so that even old phone apps work with new device loads). + // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; + res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; + setPassKey(&res); + myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } - // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. - // So even if we internally use 0 to represent 'use default' we still need to send the value we are - // using to the app (so that even old phone apps work with new device loads). - // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally - // private and useful for users to know current provisioning) - // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = - // Config_ModuleConfig_telemetry_tag; - res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; - setPassKey(&res); - myReply = allocDataProtobuf(res); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } - } } -void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const uint32_t configType) { - meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; - - if (req.decoded.want_response) { - switch (configType) { - case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: - LOG_INFO("Get module config: MQTT"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; - res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt; - break; - case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: - LOG_INFO("Get module config: Serial"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag; - res.get_module_config_response.payload_variant.serial = moduleConfig.serial; - break; - case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: - LOG_INFO("Get module config: External Notification"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; - res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification; - break; - case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: - LOG_INFO("Get module config: Store & Forward"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; - res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward; - break; - case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: - LOG_INFO("Get module config: Range Test"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; - res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test; - break; - case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: - LOG_INFO("Get module config: Telemetry"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; - res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry; - break; - case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: - LOG_INFO("Get module config: Canned Message"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; - res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; - break; - case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: - LOG_INFO("Get module config: Audio"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; - res.get_module_config_response.payload_variant.audio = moduleConfig.audio; - break; - case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: - LOG_INFO("Get module config: Remote Hardware"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; - res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware; - break; - case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: - LOG_INFO("Get module config: Neighbor Info"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; - res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; - break; - case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: - LOG_INFO("Get module config: Detection Sensor"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; - res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor; - break; - case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: - LOG_INFO("Get module config: Ambient Lighting"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; - res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; - break; - case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: - LOG_INFO("Get module config: Paxcounter"); - res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; - res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; - break; - } - - // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. - // So even if we internally use 0 to represent 'use default' we still need to send the value we are - // using to the app (so that even old phone apps work with new device loads). - // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally - // private and useful for users to know current provisioning) - // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = - // Config_ModuleConfig_telemetry_tag; - res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; - setPassKey(&res); - myReply = allocDataProtobuf(res); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } - } -} - -void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req) { - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.which_payload_variant = meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag; - for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { - if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { - continue; - } - r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i] = devicestate.node_remote_hardware_pins[i]; - } - for (uint8_t i = 0; i < moduleConfig.remote_hardware.available_pins_count; i++) { - if (!moduleConfig.remote_hardware.available_pins[i].gpio_pin) { - continue; - } - meshtastic_NodeRemoteHardwarePin nodePin = meshtastic_NodeRemoteHardwarePin_init_default; - nodePin.node_num = nodeDB->getNodeNum(); - nodePin.pin = moduleConfig.remote_hardware.available_pins[i]; - r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i + 12] = nodePin; - } - setPassKey(&r); - myReply = allocDataProtobuf(r); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } -} - -void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) { - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.get_device_metadata_response = getDeviceMetadata(); - r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; - setPassKey(&r); - myReply = allocDataProtobuf(r); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } -} - -void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) { - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - - meshtastic_DeviceConnectionStatus conn = meshtastic_DeviceConnectionStatus_init_zero; - -#if HAS_WIFI - conn.has_wifi = true; - conn.wifi.has_status = true; -#ifdef ARCH_PORTDUINO - conn.wifi.status.is_connected = true; -#else - conn.wifi.status.is_connected = WiFi.status() == WL_CONNECTED; -#endif - strncpy(conn.wifi.ssid, config.network.wifi_ssid, 33); - if (conn.wifi.status.is_connected) { - conn.wifi.rssi = WiFi.RSSI(); - conn.wifi.status.ip_address = WiFi.localIP(); -#ifndef MESHTASTIC_EXCLUDE_MQTT - conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); -#endif - conn.wifi.status.is_syslog_connected = false; // FIXME wire this up - } -#endif - -#if HAS_ETHERNET && !defined(USE_WS5500) - conn.has_ethernet = true; - conn.ethernet.has_status = true; - if (Ethernet.linkStatus() == LinkON) { - conn.ethernet.status.is_connected = true; - conn.ethernet.status.ip_address = Ethernet.localIP(); -#if !MESHTASTIC_EXCLUDE_MQTT - conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); -#endif - conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up - } else { - conn.ethernet.status.is_connected = false; - } -#endif - -#if HAS_BLUETOOTH - conn.has_bluetooth = true; - conn.bluetooth.pin = config.bluetooth.fixed_pin; -#ifdef ARCH_ESP32 - if (config.bluetooth.enabled && nimbleBluetooth) { - conn.bluetooth.is_connected = nimbleBluetooth->isConnected(); - conn.bluetooth.rssi = nimbleBluetooth->getRssi(); - } -#elif defined(ARCH_NRF52) - if (config.bluetooth.enabled && nrf52Bluetooth) { - conn.bluetooth.is_connected = nrf52Bluetooth->isConnected(); - } -#endif -#endif - conn.has_serial = true; // No serial-less devices -#if !MESHTASTIC_EXCLUDE_POWER_FSM - conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; -#else - conn.serial.is_connected = powerFSM.getState(); -#endif - conn.serial.baud = SERIAL_BAUD; - - r.get_device_connection_status_response = conn; - r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; - setPassKey(&r); - myReply = allocDataProtobuf(r); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } -} - -void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) { - if (req.decoded.want_response) { - // We create the reply here +void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req) +{ meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.get_channel_response = channels.getByIndex(channelIndex); - r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; + r.which_payload_variant = meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag; + for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { + if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { + continue; + } + r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i] = devicestate.node_remote_hardware_pins[i]; + } + for (uint8_t i = 0; i < moduleConfig.remote_hardware.available_pins_count; i++) { + if (!moduleConfig.remote_hardware.available_pins[i].gpio_pin) { + continue; + } + meshtastic_NodeRemoteHardwarePin nodePin = meshtastic_NodeRemoteHardwarePin_init_default; + nodePin.node_num = nodeDB->getNodeNum(); + nodePin.pin = moduleConfig.remote_hardware.available_pins[i]; + r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i + 12] = nodePin; + } setPassKey(&r); myReply = allocDataProtobuf(r); if (req.pki_encrypted) { - myReply->pki_encrypted = true; + myReply->pki_encrypted = true; } - } } -void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) { - meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; - r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; - r.get_ui_config_response = uiconfig; - myReply = allocDataProtobuf(r); - if (req.pki_encrypted) { - myReply->pki_encrypted = true; - } -} - -void AdminModule::reboot(int32_t seconds) { - LOG_INFO("Reboot in %d seconds", seconds); - if (screen) - screen->showSimpleBanner("Rebooting...", 0); // stays on screen - rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); -} - -void AdminModule::saveChanges(int saveWhat, bool shouldReboot) { - if (!hasOpenEditTransaction) { - LOG_INFO("Save changes to disk"); - service->reloadConfig(saveWhat); // Calls saveToDisk among other things - } else { - LOG_INFO("Delay save of changes to disk until the open transaction is committed"); - } - if (shouldReboot && !hasOpenEditTransaction) { - reboot(DEFAULT_REBOOT_SECONDS); - } -} - -void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg) { - nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg); -} - -void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { - // Validate ham parameters before setting since this would bypass validation in the owner struct - if (*p.call_sign) { - const char *start = p.call_sign; - // Skip all whitespace - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); - return; +void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) +{ + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.get_device_metadata_response = getDeviceMetadata(); + r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; + setPassKey(&r); + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; } - } - if (*p.short_name) { - const char *start = p.short_name; - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); - return; +} + +void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) +{ + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + + meshtastic_DeviceConnectionStatus conn = meshtastic_DeviceConnectionStatus_init_zero; + +#if HAS_WIFI + conn.has_wifi = true; + conn.wifi.has_status = true; +#ifdef ARCH_PORTDUINO + conn.wifi.status.is_connected = true; +#else + conn.wifi.status.is_connected = WiFi.status() == WL_CONNECTED; +#endif + strncpy(conn.wifi.ssid, config.network.wifi_ssid, 33); + if (conn.wifi.status.is_connected) { + conn.wifi.rssi = WiFi.RSSI(); + conn.wifi.status.ip_address = WiFi.localIP(); +#ifndef MESHTASTIC_EXCLUDE_MQTT + conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); +#endif + conn.wifi.status.is_syslog_connected = false; // FIXME wire this up } - } +#endif - // Set call sign and override lora limitations for licensed use - strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); - strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); - owner.is_licensed = true; - config.lora.override_duty_cycle = true; - config.lora.tx_power = p.tx_power; - config.lora.override_frequency = p.frequency; - // Set node info broadcast interval to 10 minutes - // For FCC minimum call-sign announcement - config.device.node_info_broadcast_secs = 600; - - config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - // Remove PSK of primary channel for plaintext amateur usage - - if (channels.ensureLicensedOperation()) { - sendWarning(licensedModeMessage); - } - channels.onConfigChanged(); - - service->reloadOwner(false); - saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); -} - -AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg) { - // restrict to the admin channel for rx - // boundChannel = Channels::adminChannel; -} - -void AdminModule::setPassKey(meshtastic_AdminMessage *res) { - if (session_time == 0 || millis() / 1000 > session_time + 150) { - for (int i = 0; i < 8; i++) { - session_passkey[i] = random(); +#if HAS_ETHERNET && !defined(USE_WS5500) + conn.has_ethernet = true; + conn.ethernet.has_status = true; + if (Ethernet.linkStatus() == LinkON) { + conn.ethernet.status.is_connected = true; + conn.ethernet.status.ip_address = Ethernet.localIP(); +#if !MESHTASTIC_EXCLUDE_MQTT + conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); +#endif + conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up + } else { + conn.ethernet.status.is_connected = false; + } +#endif + +#if HAS_BLUETOOTH + conn.has_bluetooth = true; + conn.bluetooth.pin = config.bluetooth.fixed_pin; +#ifdef ARCH_ESP32 + if (config.bluetooth.enabled && nimbleBluetooth) { + conn.bluetooth.is_connected = nimbleBluetooth->isConnected(); + conn.bluetooth.rssi = nimbleBluetooth->getRssi(); + } +#elif defined(ARCH_NRF52) + if (config.bluetooth.enabled && nrf52Bluetooth) { + conn.bluetooth.is_connected = nrf52Bluetooth->isConnected(); + } +#endif +#endif + conn.has_serial = true; // No serial-less devices +#if !MESHTASTIC_EXCLUDE_POWER_FSM + conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; +#else + conn.serial.is_connected = powerFSM.getState(); +#endif + conn.serial.baud = SERIAL_BAUD; + + r.get_device_connection_status_response = conn; + r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; + setPassKey(&r); + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; } - session_time = millis() / 1000; - } - memcpy(res->session_passkey.bytes, session_passkey, 8); - res->session_passkey.size = 8; - printBytes("Set admin key to ", res->session_passkey.bytes, 8); - // if halfway to session_expire, regenerate session_passkey, reset the timeout - // set the key in the packet } -bool AdminModule::checkPassKey(meshtastic_AdminMessage *res) { // check that the key in the packet is still valid - printBytes("Incoming session key: ", res->session_passkey.bytes, 8); - printBytes("Expected session key: ", session_passkey, 8); - return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 && memcmp(res->session_passkey.bytes, session_passkey, 8) == 0); +void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) +{ + if (req.decoded.want_response) { + // We create the reply here + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.get_channel_response = channels.getByIndex(channelIndex); + r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; + setPassKey(&r); + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } + } } -bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r) { - if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_owner_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_config_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_response_tag) - return true; - else - return false; +void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) +{ + meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; + r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; + r.get_ui_config_response = uiconfig; + myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } -bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) { - if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_owner_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_config_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag || - r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag) - return true; - else - return false; +void AdminModule::reboot(int32_t seconds) +{ + LOG_INFO("Reboot in %d seconds", seconds); + if (screen) + screen->showSimpleBanner("Rebooting...", 0); // stays on screen + rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } -void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) { - LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, inputEvent.kb_char, - inputEvent.touch_x, inputEvent.touch_y); +void AdminModule::saveChanges(int saveWhat, bool shouldReboot) +{ + if (!hasOpenEditTransaction) { + LOG_INFO("Save changes to disk"); + service->reloadConfig(saveWhat); // Calls saveToDisk among other things + } else { + LOG_INFO("Delay save of changes to disk until the open transaction is committed"); + } + if (shouldReboot && !hasOpenEditTransaction) { + reboot(DEFAULT_REBOOT_SECONDS); + } +} - // Create InputEvent for injection - InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, - .kbchar = (unsigned char)inputEvent.kb_char, - .touchX = inputEvent.touch_x, - .touchY = inputEvent.touch_y}; +void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg) +{ + nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg); +} - // Log the event being injected - LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, - (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); +void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) +{ + // Validate ham parameters before setting since this would bypass validation in the owner struct + if (*p.call_sign) { + const char *start = p.call_sign; + // Skip all whitespace + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); + return; + } + } + if (*p.short_name) { + const char *start = p.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); + return; + } + } - // Wake the device if asleep - powerFSM.trigger(EVENT_INPUT); + // Set call sign and override lora limitations for licensed use + strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); + strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); + owner.is_licensed = true; + config.lora.override_duty_cycle = true; + config.lora.tx_power = p.tx_power; + config.lora.override_frequency = p.frequency; + // Set node info broadcast interval to 10 minutes + // For FCC minimum call-sign announcement + config.device.node_info_broadcast_secs = 600; + + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; + // Remove PSK of primary channel for plaintext amateur usage + + if (channels.ensureLicensedOperation()) { + sendWarning(licensedModeMessage); + } + channels.onConfigChanged(); + + service->reloadOwner(false); + saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); +} + +AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg) +{ + // restrict to the admin channel for rx + // boundChannel = Channels::adminChannel; +} + +void AdminModule::setPassKey(meshtastic_AdminMessage *res) +{ + if (session_time == 0 || millis() / 1000 > session_time + 150) { + for (int i = 0; i < 8; i++) { + session_passkey[i] = random(); + } + session_time = millis() / 1000; + } + memcpy(res->session_passkey.bytes, session_passkey, 8); + res->session_passkey.size = 8; + printBytes("Set admin key to ", res->session_passkey.bytes, 8); + // if halfway to session_expire, regenerate session_passkey, reset the timeout + // set the key in the packet +} + +bool AdminModule::checkPassKey(meshtastic_AdminMessage *res) +{ // check that the key in the packet is still valid + printBytes("Incoming session key: ", res->session_passkey.bytes, 8); + printBytes("Expected session key: ", session_passkey, 8); + return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 && + memcmp(res->session_passkey.bytes, session_passkey, 8) == 0); +} + +bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r) +{ + if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_owner_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_config_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_response_tag) + return true; + else + return false; +} + +bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) +{ + if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_owner_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_config_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag || + r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag) + return true; + else + return false; +} + +void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) +{ + LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, + inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y); + + // Create InputEvent for injection + InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, + .kbchar = (unsigned char)inputEvent.kb_char, + .touchX = inputEvent.touch_x, + .touchY = inputEvent.touch_y}; + + // Log the event being injected + LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, + (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); + + // Wake the device if asleep + powerFSM.trigger(EVENT_INPUT); #if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) - // Inject the event through InputBroker - if (inputBroker) { - inputBroker->injectInputEvent(&event); - } else { - LOG_ERROR("InputBroker not available for event injection"); - } + // Inject the event through InputBroker + if (inputBroker) { + inputBroker->injectInputEvent(&event); + } else { + LOG_ERROR("InputBroker not available for event injection"); + } #endif } -void AdminModule::sendWarning(const char *message) { - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, message, sizeof(cn->message)); - service->sendClientNotification(cn); +void AdminModule::sendWarning(const char *message) +{ + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, message, sizeof(cn->message)); + service->sendClientNotification(cn); } -void disableBluetooth() { +void disableBluetooth() +{ #if HAS_BLUETOOTH #ifdef ARCH_ESP32 - if (nimbleBluetooth) - nimbleBluetooth->deinit(); + if (nimbleBluetooth) + nimbleBluetooth->deinit(); #elif defined(ARCH_NRF52) - if (nrf52Bluetooth) - nrf52Bluetooth->shutdown(); + if (nrf52Bluetooth) + nrf52Bluetooth->shutdown(); #endif #endif } diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index b73eb4035..867751f49 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -10,70 +10,72 @@ * Datatype passed to Observers by AdminModule, to allow external handling of admin messages */ struct AdminModule_ObserverData { - const meshtastic_AdminMessage *request; - meshtastic_AdminMessage *response; - AdminMessageHandleResult *result; + const meshtastic_AdminMessage *request; + meshtastic_AdminMessage *response; + AdminMessageHandleResult *result; }; /** * Admin module for admin messages */ -class AdminModule : public ProtobufModule, public Observable { -public: - /** Constructor - * name is for debugging output - */ - AdminModule(); +class AdminModule : public ProtobufModule, public Observable +{ + public: + /** Constructor + * name is for debugging output + */ + AdminModule(); -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *p) override; -private: - bool hasOpenEditTransaction = false; + private: + bool hasOpenEditTransaction = false; - uint8_t session_passkey[8] = {0}; - uint session_time = 0; + uint8_t session_passkey[8] = {0}; + uint session_time = 0; - void saveChanges(int saveWhat, bool shouldReboot = true); + void saveChanges(int saveWhat, bool shouldReboot = true); - /** - * Getters - */ - void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p); - void handleGetOwner(const meshtastic_MeshPacket &req); - void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType); - void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType); - void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex); - void handleGetDeviceMetadata(const meshtastic_MeshPacket &req); - void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req); - void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req); - void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req); - /** - * Setters - */ - void handleSetOwner(const meshtastic_User &o); - void handleSetChannel(const meshtastic_Channel &cc); - void handleSetConfig(const meshtastic_Config &c); - bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); - void handleSetChannel(); - void handleSetHamMode(const meshtastic_HamParameters &req); - void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); - void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); - void reboot(int32_t seconds); + /** + * Getters + */ + void handleGetModuleConfigResponse(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *p); + void handleGetOwner(const meshtastic_MeshPacket &req); + void handleGetConfig(const meshtastic_MeshPacket &req, uint32_t configType); + void handleGetModuleConfig(const meshtastic_MeshPacket &req, uint32_t configType); + void handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex); + void handleGetDeviceMetadata(const meshtastic_MeshPacket &req); + void handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req); + void handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req); + void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req); + /** + * Setters + */ + void handleSetOwner(const meshtastic_User &o); + void handleSetChannel(const meshtastic_Channel &cc); + void handleSetConfig(const meshtastic_Config &c); + bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); + void handleSetChannel(); + void handleSetHamMode(const meshtastic_HamParameters &req); + void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); + void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); + void reboot(int32_t seconds); - void setPassKey(meshtastic_AdminMessage *res); - bool checkPassKey(meshtastic_AdminMessage *res); + void setPassKey(meshtastic_AdminMessage *res); + bool checkPassKey(meshtastic_AdminMessage *res); - bool messageIsResponse(const meshtastic_AdminMessage *r); - bool messageIsRequest(const meshtastic_AdminMessage *r); - void sendWarning(const char *message); + bool messageIsResponse(const meshtastic_AdminMessage *r); + bool messageIsRequest(const meshtastic_AdminMessage *r); + void sendWarning(const char *message); }; -static constexpr const char *licensedModeMessage = "Licensed mode activated, removing admin channel and encryption from all channels"; +static constexpr const char *licensedModeMessage = + "Licensed mode activated, removing admin channel and encryption from all channels"; extern AdminModule *adminModule; diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp index b77e40a78..a51ef54c3 100644 --- a/src/modules/AtakPluginModule.cpp +++ b/src/modules/AtakPluginModule.cpp @@ -11,172 +11,188 @@ AtakPluginModule *atakPluginModule; AtakPluginModule::AtakPluginModule() - : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin") { - ourPortNum = meshtastic_PortNum_ATAK_PLUGIN; + : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin") +{ + ourPortNum = meshtastic_PortNum_ATAK_PLUGIN; } /* Encompasses the full construction and sending packet to mesh Will be used for broadcast. */ -int32_t AtakPluginModule::runOnce() { return default_broadcast_interval_secs; } - -bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r) { return false; } - -meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t) { - meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero; - if (t->has_group) { - clone.has_group = true; - clone.group = t->group; - } - if (t->has_status) { - clone.has_status = true; - clone.status = t->status; - } - if (t->has_contact) { - clone.has_contact = true; - clone.contact = {0}; - } - - if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) { - clone.which_payload_variant = meshtastic_TAKPacket_pli_tag; - clone.payload_variant.pli = t->payload_variant.pli; - } else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { - clone.which_payload_variant = meshtastic_TAKPacket_chat_tag; - clone.payload_variant.chat = {0}; - } else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) { - clone.which_payload_variant = meshtastic_TAKPacket_detail_tag; - clone.payload_variant.detail.size = t->payload_variant.detail.size; - memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size); - } - - return clone; +int32_t AtakPluginModule::runOnce() +{ + return default_broadcast_interval_secs; } -void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) { - // From Phone (EUD) - if (mp.from == 0) { - LOG_DEBUG("Received uncompressed TAK payload from phone: %d bytes", mp.decoded.payload.size); - // Compress for LoRA transport - auto compressed = cloneTAKPacketData(t); - compressed.is_compressed = true; +bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r) +{ + return false; +} + +meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t) +{ + meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero; + if (t->has_group) { + clone.has_group = true; + clone.group = t->group; + } + if (t->has_status) { + clone.has_status = true; + clone.status = t->status; + } if (t->has_contact) { - auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign, - sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed callsign: %d bytes", length); - length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), compressed.contact.device_callsign, - sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed device_callsign: %d bytes", length); + clone.has_contact = true; + clone.contact = {0}; } - if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = - unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), compressed.payload_variant.chat.message, - sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); - return; - } - LOG_DEBUG("Compressed chat message: %d bytes", length); - if (t->payload_variant.chat.has_to) { - compressed.payload_variant.chat.has_to = true; - length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), compressed.payload_variant.chat.to, - sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); - return; + if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_pli_tag; + clone.payload_variant.pli = t->payload_variant.pli; + } else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_chat_tag; + clone.payload_variant.chat = {0}; + } else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) { + clone.which_payload_variant = meshtastic_TAKPacket_detail_tag; + clone.payload_variant.detail.size = t->payload_variant.detail.size; + memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size); + } + + return clone; +} + +void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) +{ + // From Phone (EUD) + if (mp.from == 0) { + LOG_DEBUG("Received uncompressed TAK payload from phone: %d bytes", mp.decoded.payload.size); + // Compress for LoRA transport + auto compressed = cloneTAKPacketData(t); + compressed.is_compressed = true; + if (t->has_contact) { + auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign, + sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed callsign: %d bytes", length); + length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), + compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, + USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed device_callsign: %d bytes", length); } - LOG_DEBUG("Compressed chat to: %d bytes", length); - } + if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { + auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), + compressed.payload_variant.chat.message, + sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed chat message: %d bytes", length); - if (t->payload_variant.chat.has_to_callsign) { - compressed.payload_variant.chat.has_to_callsign = true; - length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1, - USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); - return; + if (t->payload_variant.chat.has_to) { + compressed.payload_variant.chat.has_to = true; + length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), + compressed.payload_variant.chat.to, + sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed chat to: %d bytes", length); + } + + if (t->payload_variant.chat.has_to_callsign) { + compressed.payload_variant.chat.has_to_callsign = true; + length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), + compressed.payload_variant.chat.to_callsign, + sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); + return; + } + LOG_DEBUG("Compressed chat to_callsign: %d bytes", length); + } } - LOG_DEBUG("Compressed chat to_callsign: %d bytes", length); - } - } - mp.decoded.payload.size = - pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), meshtastic_TAKPacket_fields, &compressed); - LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size); - } else { - if (!t->is_compressed) { - // Not compressed. Something is wrong - LOG_WARN("Received uncompressed TAKPacket over radio! Skip"); - return; - } - - // Decompress for Phone (EUD) - auto uncompressed = cloneTAKPacketData(t); - uncompressed.is_compressed = false; - if (t->has_contact) { - auto length = unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign, - sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow contact.callsign. Bailing out"); - return; - } - LOG_DEBUG("Decompressed callsign: %d bytes", length); - - length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), uncompressed.contact.device_callsign, - sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); - return; - } - LOG_DEBUG("Decompressed device_callsign: %d bytes", length); - } - if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, - USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow chat.message. Bailing out"); - return; - } - LOG_DEBUG("Decompressed chat message: %d bytes", length); - - if (t->payload_variant.chat.has_to) { - uncompressed.payload_variant.chat.has_to = true; - length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), uncompressed.payload_variant.chat.to, - sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow chat.to. Bailing out"); - return; + mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), + meshtastic_TAKPacket_fields, &compressed); + LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size); + } else { + if (!t->is_compressed) { + // Not compressed. Something is wrong + LOG_WARN("Received uncompressed TAKPacket over radio! Skip"); + return; } - LOG_DEBUG("Decompressed chat to: %d bytes", length); - } - if (t->payload_variant.chat.has_to_callsign) { - uncompressed.payload_variant.chat.has_to_callsign = true; - length = unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, - USX_PSET_DFLT, NULL); - if (length < 0) { - LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); - return; + // Decompress for Phone (EUD) + auto uncompressed = cloneTAKPacketData(t); + uncompressed.is_compressed = false; + if (t->has_contact) { + auto length = + unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign, + sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow contact.callsign. Bailing out"); + return; + } + LOG_DEBUG("Decompressed callsign: %d bytes", length); + + length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), + uncompressed.contact.device_callsign, + sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); + return; + } + LOG_DEBUG("Decompressed device_callsign: %d bytes", length); } - LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length); - } - } - auto decompressedCopy = packetPool.allocCopy(mp); - decompressedCopy->decoded.payload.size = pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload), - meshtastic_TAKPacket_fields, &uncompressed); + if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { + auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), + uncompressed.payload_variant.chat.message, + sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow chat.message. Bailing out"); + return; + } + LOG_DEBUG("Decompressed chat message: %d bytes", length); - service->sendToPhone(decompressedCopy); - } - return; + if (t->payload_variant.chat.has_to) { + uncompressed.payload_variant.chat.has_to = true; + length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), + uncompressed.payload_variant.chat.to, + sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow chat.to. Bailing out"); + return; + } + LOG_DEBUG("Decompressed chat to: %d bytes", length); + } + + if (t->payload_variant.chat.has_to_callsign) { + uncompressed.payload_variant.chat.has_to_callsign = true; + length = + unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), + uncompressed.payload_variant.chat.to_callsign, + sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); + if (length < 0) { + LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); + return; + } + LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length); + } + } + auto decompressedCopy = packetPool.allocCopy(mp); + decompressedCopy->decoded.payload.size = + pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload), + meshtastic_TAKPacket_fields, &uncompressed); + + service->sendToPhone(decompressedCopy); + } + return; } \ No newline at end of file diff --git a/src/modules/AtakPluginModule.h b/src/modules/AtakPluginModule.h index 35c9cb172..0470a3b32 100644 --- a/src/modules/AtakPluginModule.h +++ b/src/modules/AtakPluginModule.h @@ -5,21 +5,22 @@ /** * Waypoint message handling for meshtastic */ -class AtakPluginModule : public ProtobufModule, private concurrency::OSThread { -public: - /** Constructor - * name is for debugging output - */ - AtakPluginModule(); +class AtakPluginModule : public ProtobufModule, private concurrency::OSThread +{ + public: + /** Constructor + * name is for debugging output + */ + AtakPluginModule(); -protected: - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; - /* Does our periodic broadcast */ - int32_t runOnce() override; + protected: + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override; + /* Does our periodic broadcast */ + int32_t runOnce() override; -private: - meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t); + private: + meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t); }; extern AtakPluginModule *atakPluginModule; \ No newline at end of file diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 43e80677e..8d1ba6346 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -46,69 +46,72 @@ extern MessageStore messageStore; #define INACTIVATE_AFTER_MS 20000 // Tokenize a message string into emote/text segments -static std::vector> tokenizeMessageWithEmotes(const char *msg) { - std::vector> tokens; - int msgLen = strlen(msg); - int pos = 0; - while (pos < msgLen) { - const graphics::Emote *foundEmote = nullptr; - int foundLen = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - int labelLen = strlen(label); - if (labelLen == 0) - continue; - if (strncmp(msg + pos, label, labelLen) == 0) { - if (!foundEmote || labelLen > foundLen) { - foundEmote = &graphics::emotes[j]; - foundLen = labelLen; +static std::vector> tokenizeMessageWithEmotes(const char *msg) +{ + std::vector> tokens; + int msgLen = strlen(msg); + int pos = 0; + while (pos < msgLen) { + const graphics::Emote *foundEmote = nullptr; + int foundLen = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) + continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; + } + } } - } - } - if (foundEmote) { - tokens.emplace_back(true, String(foundEmote->label)); - pos += foundLen; - } else { - // Find next emote - int nextEmote = msgLen; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *found = strstr(msg + pos, label); - if (found && (found - msg) < nextEmote) { - nextEmote = found - msg; + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; + } } - } - int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); - if (textLen > 0) { - tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); - pos += textLen; - } else { - break; - } } - } - return tokens; + return tokens; } // Render a single emote token centered vertically on a row -static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) { - const graphics::Emote *emote = nullptr; - for (int j = 0; j < graphics::numEmotes; j++) { - if (label == graphics::emotes[j].label) { - emote = &graphics::emotes[j]; - break; +static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) +{ + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (label == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote + display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; // spacing between tokens } - } - if (emote) { - int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote - display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; // spacing between tokens - } } -namespace graphics { +namespace graphics +{ extern int bannerSignalBars; } extern ScanI2C::DeviceAddress cardkb_found; @@ -123,80 +126,89 @@ meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; CannedMessageModule *cannedMessageModule; -CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { - this->loadProtoForModule(); - if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && !CANNED_MESSAGE_MODULE_ENABLE) { - LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); - this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; - disable(); - } else { - LOG_INFO("CannedMessageModule is enabled"); - moduleConfig.canned_message.enabled = true; - this->inputObserver.observe(inputBroker); - } -} - -void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) { - // Do NOT override explicit broadcast replies - // Only reuse lastDest in LaunchRepeatDestination() - - dest = newDest; - channel = newChannel; - - lastDest = dest; - lastChannel = channel; - lastDestSet = true; - - // Upon activation, highlight "[Select Destination]" - int selectDestination = 0; - for (int i = 0; i < messagesCount; ++i) { - if (strcmp(messages[i], "[Select Destination]") == 0) { - selectDestination = i; - break; +CannedMessageModule::CannedMessageModule() + : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") +{ + this->loadProtoForModule(); + if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && + !CANNED_MESSAGE_MODULE_ENABLE) { + LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); + this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; + disable(); + } else { + LOG_INFO("CannedMessageModule is enabled"); + moduleConfig.canned_message.enabled = true; + this->inputObserver.observe(inputBroker); } - } - currentMessageIndex = selectDestination; - - // This triggers the canned message list - runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); } -void CannedMessageModule::LaunchRepeatDestination() { - if (!lastDestSet) { - LaunchWithDestination(NODENUM_BROADCAST, 0); - } else { - LaunchWithDestination(lastDest, lastChannel); - } +void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) +{ + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() + + dest = newDest; + channel = newChannel; + + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + + // Upon activation, highlight "[Select Destination]" + int selectDestination = 0; + for (int i = 0; i < messagesCount; ++i) { + if (strcmp(messages[i], "[Select Destination]") == 0) { + selectDestination = i; + break; + } + } + currentMessageIndex = selectDestination; + + // This triggers the canned message list + runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); } -void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) { - // Do NOT override explicit broadcast replies - // Only reuse lastDest in LaunchRepeatDestination() +void CannedMessageModule::LaunchRepeatDestination() +{ + if (!lastDestSet) { + LaunchWithDestination(NODENUM_BROADCAST, 0); + } else { + LaunchWithDestination(lastDest, lastChannel); + } +} - dest = newDest; - channel = newChannel; +void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) +{ + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() - lastDest = dest; - lastChannel = channel; - lastDestSet = true; + dest = newDest; + channel = newChannel; - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); + lastDest = dest; + lastChannel = channel; + lastDestSet = true; - LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); } static bool returnToCannedList = false; -bool hasKeyForNode(const meshtastic_NodeInfoLite *node) { return node && node->has_user && node->user.public_key.size > 0; } +bool hasKeyForNode(const meshtastic_NodeInfoLite *node) +{ + return node && node->has_user && node->user.public_key.size > 0; +} /** * @brief Items in array this->messages will be set to be pointing on the right * starting points of the string this->messageStore @@ -204,2084 +216,2154 @@ bool hasKeyForNode(const meshtastic_NodeInfoLite *node) { return node && node->h * @return int Returns the number of messages found. */ -int CannedMessageModule::splitConfiguredMessages() { - int i = 0; +int CannedMessageModule::splitConfiguredMessages() +{ + int i = 0; - String canned_messages = cannedMessageModuleConfig.messages; + String canned_messages = cannedMessageModuleConfig.messages; - // Copy all message parts into the buffer - strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); + // Copy all message parts into the buffer + strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); - // Temporary array to allow for insertion - const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; - int tempCount = 0; - // Insert at position 0 (top) - tempMessages[tempCount++] = "[Select Destination]"; + // Temporary array to allow for insertion + const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; + int tempCount = 0; + // Insert at position 0 (top) + tempMessages[tempCount++] = "[Select Destination]"; #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a touch screen virtual keyboard - tempMessages[tempCount++] = "[-- Free Text --]"; -#else - if (osk_found && screen) { + // Add a "Free Text" entry at the top if using a touch screen virtual keyboard tempMessages[tempCount++] = "[-- Free Text --]"; - } +#else + if (osk_found && screen) { + tempMessages[tempCount++] = "[-- Free Text --]"; + } #endif - // First message always starts at buffer start - tempMessages[tempCount++] = this->messageBuffer; - int upTo = strlen(this->messageBuffer) - 1; + // First message always starts at buffer start + tempMessages[tempCount++] = this->messageBuffer; + int upTo = strlen(this->messageBuffer) - 1; - // Walk buffer, splitting on '|' - while (i < upTo) { - if (this->messageBuffer[i] == '|') { - this->messageBuffer[i] = '\0'; // End previous message - if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) - break; - tempMessages[tempCount++] = (this->messageBuffer + i + 1); + // Walk buffer, splitting on '|' + while (i < upTo) { + if (this->messageBuffer[i] == '|') { + this->messageBuffer[i] = '\0'; // End previous message + if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) + break; + tempMessages[tempCount++] = (this->messageBuffer + i + 1); + } + i += 1; } - i += 1; - } - // Add [Exit] as the last entry - tempMessages[tempCount++] = "[Exit]"; + // Add [Exit] as the last entry + tempMessages[tempCount++] = "[Exit]"; - // Copy to the member array - for (int k = 0; k < tempCount; ++k) { - this->messages[k] = (char *)tempMessages[k]; - } - this->messagesCount = tempCount; + // Copy to the member array + for (int k = 0; k < tempCount; ++k) { + this->messages[k] = (char *)tempMessages[k]; + } + this->messagesCount = tempCount; - return this->messagesCount; + return this->messagesCount; } -void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { - if (graphics::currentResolution == graphics::ScreenResolution::High) { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); +void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) +{ + if (graphics::currentResolution == graphics::ScreenResolution::High) { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); + } } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); + } } - } else { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } - } } -void CannedMessageModule::resetSearch() { - int previousDestIndex = destIndex; +void CannedMessageModule::resetSearch() +{ + int previousDestIndex = destIndex; - searchQuery = ""; - updateDestinationSelectionList(); + searchQuery = ""; + updateDestinationSelectionList(); - // Adjust scrollIndex so previousDestIndex is still visible - int totalEntries = activeChannelIndices.size() + filteredNodes.size(); - this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; - if (this->visibleRows < 1) - this->visibleRows = 1; - int maxScrollIndex = std::max(0, totalEntries - visibleRows); - scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); + // Adjust scrollIndex so previousDestIndex is still visible + int totalEntries = activeChannelIndices.size() + filteredNodes.size(); + this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; + if (this->visibleRows < 1) + this->visibleRows = 1; + int maxScrollIndex = std::max(0, totalEntries - visibleRows); + scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); - lastUpdateMillis = millis(); - requestFocus(); + lastUpdateMillis = millis(); + requestFocus(); } -void CannedMessageModule::updateDestinationSelectionList() { - static size_t lastNumMeshNodes = 0; - static String lastSearchQuery = ""; +void CannedMessageModule::updateDestinationSelectionList() +{ + static size_t lastNumMeshNodes = 0; + static String lastSearchQuery = ""; - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - bool nodesChanged = (numMeshNodes != lastNumMeshNodes); - lastNumMeshNodes = numMeshNodes; + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + bool nodesChanged = (numMeshNodes != lastNumMeshNodes); + lastNumMeshNodes = numMeshNodes; - // Early exit if nothing changed - if (searchQuery == lastSearchQuery && !nodesChanged) - return; - lastSearchQuery = searchQuery; - needsUpdate = false; + // Early exit if nothing changed + if (searchQuery == lastSearchQuery && !nodesChanged) + return; + lastSearchQuery = searchQuery; + needsUpdate = false; - this->filteredNodes.clear(); - this->activeChannelIndices.clear(); + this->filteredNodes.clear(); + this->activeChannelIndices.clear(); - NodeNum myNodeNum = nodeDB->getNodeNum(); - String lowerSearchQuery = searchQuery; - lowerSearchQuery.toLowerCase(); + NodeNum myNodeNum = nodeDB->getNodeNum(); + String lowerSearchQuery = searchQuery; + lowerSearchQuery.toLowerCase(); - // Preallocate space to reduce reallocation - this->filteredNodes.reserve(numMeshNodes); + // Preallocate space to reduce reallocation + this->filteredNodes.reserve(numMeshNodes); - for (size_t i = 0; i < numMeshNodes; ++i) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) - continue; + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) + continue; - const String &nodeName = node->user.long_name; + const String &nodeName = node->user.long_name; - if (searchQuery.length() == 0) { - this->filteredNodes.push_back({node, sinceLastSeen(node)}); - } else { - // Avoid unnecessary lowercase conversion if already matched - String lowerNodeName = nodeName; - lowerNodeName.toLowerCase(); + if (searchQuery.length() == 0) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } else { + // Avoid unnecessary lowercase conversion if already matched + String lowerNodeName = nodeName; + lowerNodeName.toLowerCase(); - if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { - this->filteredNodes.push_back({node, sinceLastSeen(node)}); - } + if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } + } } - } - meshtastic_MeshPacket *p = allocDataPacket(); - p->pki_encrypted = true; - p->channel = 0; + meshtastic_MeshPacket *p = allocDataPacket(); + p->pki_encrypted = true; + p->channel = 0; - // Populate active channels - std::vector seenChannels; - seenChannels.reserve(channels.getNumChannels()); - for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { - String name = channels.getName(i); - if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { - this->activeChannelIndices.push_back(i); - seenChannels.push_back(name); + // Populate active channels + std::vector seenChannels; + seenChannels.reserve(channels.getNumChannels()); + for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { + String name = channels.getName(i); + if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { + this->activeChannelIndices.push_back(i); + seenChannels.push_back(name); + } } - } - scrollIndex = 0; // Show first result at the top - destIndex = 0; // Highlight the first entry - if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - LOG_INFO("Nodes changed, forcing UI refresh."); - screen->forceDisplay(); - } + scrollIndex = 0; // Show first result at the top + destIndex = 0; // Highlight the first entry + if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + LOG_INFO("Nodes changed, forcing UI refresh."); + screen->forceDisplay(); + } } // Returns true if character input is currently allowed (used for search/freetext states) -bool CannedMessageModule::isCharInputAllowed() const { - return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; +bool CannedMessageModule::isCharInputAllowed() const +{ + return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; } /** * Main input event dispatcher for CannedMessageModule. * Routes keyboard/button/touch input to the correct handler based on the current runState. * Only one handler (per state) processes each event, eliminating redundancy. */ -int CannedMessageModule::handleInputEvent(const InputEvent *event) { - // Block ALL input if an alert banner is active - if (screen && screen->isOverlayBannerShowing()) { - return 0; - } - - // Tab key: Always allow switching between canned/destination screens - if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) - return 1; - - // Matrix keypad: If matrix key, trigger action select for canned message - if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - payload = INPUT_BROKER_MATRIXKEY; - currentMessageIndex = event->kbchar - 1; - lastTouchMillis = millis(); - requestFocus(); - return 1; - } - - // Always normalize navigation/select buttons for further handlers - bool isUp = isUpEvent(event); - bool isDown = isDownEvent(event); - bool isSelect = isSelectEvent(event); - - // Route event to handler for current UI state (no double-handling) - switch (runState) { - // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace - case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: - if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) - return 1; - return 0; // prevent fall-through to selector input - - // Free text input mode: Handles character input, cancel, backspace, select, etc. - case CANNED_MESSAGE_RUN_STATE_FREETEXT: - return handleFreeTextInput(event); // All allowed input for this state - - // Virtual keyboard mode: Show virtual keyboard and handle input - - // If sending, block all input except global/system (handled above) - case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: - return 1; - - // If sending, block all input except global/system (handled above) - case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: - return handleEmotePickerInput(event); - - case CANNED_MESSAGE_RUN_STATE_INACTIVE: - if (event->inputEvent == INPUT_BROKER_ALT_LONG) { - LaunchWithDestination(NODENUM_BROADCAST); - return 1; - } - // Printable char (ASCII) opens free text compose - if (event->kbchar >= 32 && event->kbchar <= 126) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - // Immediately process the input in the new state (freetext) - return handleFreeTextInput(event); - } - return 0; - break; - - // (Other states can be added here as needed) - default: - break; - } - - // If no state handler above processed the event, let the message selector try to handle it - // (Handles up/down/select on canned message list, exit/return) - if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) - return 1; - - // Default: event not handled by canned message system, allow others to process - return 0; -} - -bool CannedMessageModule::isUpEvent(const InputEvent *event) { - return event->inputEvent == INPUT_BROKER_UP || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); -} -bool CannedMessageModule::isDownEvent(const InputEvent *event) { - return event->inputEvent == INPUT_BROKER_DOWN || - ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); -} -bool CannedMessageModule::isSelectEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_SELECT; } - -bool CannedMessageModule::handleTabSwitch(const InputEvent *event) { - if (event->kbchar != 0x09) - return false; - - runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - - destIndex = 0; - scrollIndex = 0; - // RESTORE THIS! - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) - updateDestinationSelectionList(); - requestFocus(); - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return true; -} - -int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { - // Override isDown and isSelect ONLY for destination selector behavior - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - if (event->inputEvent == INPUT_BROKER_USER_PRESS) { - isDown = true; - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - isSelect = true; - } - } - - if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && - event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { - this->searchQuery += (char)event->kbchar; - needsUpdate = true; - if ((millis() - lastFilterUpdate) > filterDebounceMs) { - runOnce(); // update filter immediately - lastFilterUpdate = millis(); - } - return 1; - } - - size_t numMeshNodes = filteredNodes.size(); - int totalEntries = numMeshNodes + activeChannelIndices.size(); - int columns = 1; - int totalRows = totalEntries; - int maxScrollIndex = std::max(0, totalRows - visibleRows); - scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); - - // Handle backspace - if (event->inputEvent == INPUT_BROKER_BACK) { - if (searchQuery.length() > 0) { - searchQuery.remove(searchQuery.length() - 1); - needsUpdate = true; - runOnce(); - } - if (searchQuery.length() == 0) { - resetSearch(); - needsUpdate = false; - } - return 1; - } - - if (isUp) { - if (destIndex > 0) { - destIndex--; - } else if (totalEntries > 0) { - destIndex = totalEntries - 1; +int CannedMessageModule::handleInputEvent(const InputEvent *event) +{ + // Block ALL input if an alert banner is active + if (screen && screen->isOverlayBannerShowing()) { + return 0; } - if ((destIndex / columns) < scrollIndex) - scrollIndex = destIndex / columns; - else if ((destIndex / columns) >= (scrollIndex + visibleRows)) - scrollIndex = (destIndex / columns) - visibleRows + 1; + // Tab key: Always allow switching between canned/destination screens + if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) + return 1; - screen->forceDisplay(true); - return 1; - } - - if (isDown) { - if (destIndex + 1 < totalEntries) { - destIndex++; - } else if (totalEntries > 0) { - destIndex = 0; - scrollIndex = 0; + // Matrix keypad: If matrix key, trigger action select for canned message + if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + payload = INPUT_BROKER_MATRIXKEY; + currentMessageIndex = event->kbchar - 1; + lastTouchMillis = millis(); + requestFocus(); + return 1; } - if ((destIndex / columns) >= (scrollIndex + visibleRows)) - scrollIndex = (destIndex / columns) - visibleRows + 1; + // Always normalize navigation/select buttons for further handlers + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); - screen->forceDisplay(true); - return 1; - } + // Route event to handler for current UI state (no double-handling) + switch (runState) { + // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace + case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: + if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) + return 1; + return 0; // prevent fall-through to selector input - // SELECT - if (isSelect) { - if (destIndex < static_cast(activeChannelIndices.size())) { - dest = NODENUM_BROADCAST; - channel = activeChannelIndices[destIndex]; - lastDest = dest; - lastChannel = channel; - lastDestSet = true; - } else { - int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); - if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { - const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; - if (selectedNode) { - dest = selectedNode->num; - channel = selectedNode->channel; - // Already saves here, but for clarity, also: - lastDest = dest; - lastChannel = channel; - lastDestSet = true; + // Free text input mode: Handles character input, cancel, backspace, select, etc. + case CANNED_MESSAGE_RUN_STATE_FREETEXT: + return handleFreeTextInput(event); // All allowed input for this state + + // Virtual keyboard mode: Show virtual keyboard and handle input + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: + return 1; + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: + return handleEmotePickerInput(event); + + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + if (event->inputEvent == INPUT_BROKER_ALT_LONG) { + LaunchWithDestination(NODENUM_BROADCAST); + return 1; } - } + // Printable char (ASCII) opens free text compose + if (event->kbchar >= 32 && event->kbchar <= 126) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Immediately process the input in the new state (freetext) + return handleFreeTextInput(event); + } + return 0; + break; + + // (Other states can be added here as needed) + default: + break; } - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; - returnToCannedList = false; - screen->forceDisplay(true); - return 1; - } + // If no state handler above processed the event, let the message selector try to handle it + // (Handles up/down/select on canned message list, exit/return) + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) + return 1; - // CANCEL - if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; - returnToCannedList = false; - searchQuery = ""; - - // UIFrameEvent e; - // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - // notifyObservers(&e); - screen->forceDisplay(true); - return 1; - } - - return 0; + // Default: event not handled by canned message system, allow others to process + return 0; } -bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { - // Override isDown and isSelect ONLY for canned message list behavior - if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - if (event->inputEvent == INPUT_BROKER_USER_PRESS) { - isDown = true; - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - isSelect = true; - } - } +bool CannedMessageModule::isUpEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_UP || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); +} +bool CannedMessageModule::isDownEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_DOWN || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); +} +bool CannedMessageModule::isSelectEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_SELECT; +} - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) - return false; +bool CannedMessageModule::handleTabSwitch(const InputEvent *event) +{ + if (event->kbchar != 0x09) + return false; - // Handle Cancel key: go inactive, clear UI state - if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; - payload = 0; - currentMessageIndex = -1; + runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + + destIndex = 0; + scrollIndex = 0; + // RESTORE THIS! + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + updateDestinationSelectionList(); + requestFocus(); - // Notify UI that we want to redraw/close this screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; - } +} - bool handled = false; - - // Handle up/down navigation - if (isUp && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - handled = true; - } else if (isDown && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - handled = true; - } else if (isSelect) { - const char *current = messages[currentMessageIndex]; - - // [Select Destination] triggers destination selection UI - if (strcmp(current, "[Select Destination]") == 0) { - returnToCannedList = true; - runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - destIndex = 0; - scrollIndex = 0; - updateDestinationSelectionList(); // Make sure list is fresh - screen->forceDisplay(); - return true; - } - - // [Exit] returns to the main/inactive screen - if (strcmp(current, "[Exit]") == 0) { - // Set runState to inactive so we return to main UI - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - currentMessageIndex = -1; - - // Notify UI to regenerate frame set and redraw - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return true; - } - - // [Free Text] triggers the free text input (virtual keyboard) -#if defined(USE_VIRTUAL_KEYBOARD) - if (strcmp(current, "[-- Free Text --]") == 0) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return true; - } -#else - if (strcmp(current, "[-- Free Text --]") == 0) { - if (osk_found && screen) { - char headerBuffer[64]; - if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); - } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); +int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + // Override isDown and isSelect ONLY for destination selector behavior + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; } - screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { - if (!text.empty()) { - this->freetext = text.c_str(); - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + } + + if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && + event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { + this->searchQuery += (char)event->kbchar; + needsUpdate = true; + if ((millis() - lastFilterUpdate) > filterDebounceMs) { + runOnce(); // update filter immediately + lastFilterUpdate = millis(); + } + return 1; + } + + size_t numMeshNodes = filteredNodes.size(); + int totalEntries = numMeshNodes + activeChannelIndices.size(); + int columns = 1; + int totalRows = totalEntries; + int maxScrollIndex = std::max(0, totalRows - visibleRows); + scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); + + // Handle backspace + if (event->inputEvent == INPUT_BROKER_BACK) { + if (searchQuery.length() > 0) { + searchQuery.remove(searchQuery.length() - 1); + needsUpdate = true; + runOnce(); + } + if (searchQuery.length() == 0) { + resetSearch(); + needsUpdate = false; + } + return 1; + } + + if (isUp) { + if (destIndex > 0) { + destIndex--; + } else if (totalEntries > 0) { + destIndex = totalEntries - 1; + } + + if ((destIndex / columns) < scrollIndex) + scrollIndex = destIndex / columns; + else if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(true); + return 1; + } + + if (isDown) { + if (destIndex + 1 < totalEntries) { + destIndex++; + } else if (totalEntries > 0) { + destIndex = 0; + scrollIndex = 0; + } + + if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(true); + return 1; + } + + // SELECT + if (isSelect) { + if (destIndex < static_cast(activeChannelIndices.size())) { + dest = NODENUM_BROADCAST; + channel = activeChannelIndices[destIndex]; + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + } else { + int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); + if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { + const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; + if (selectedNode) { + dest = selectedNode->num; + channel = selectedNode->channel; + // Already saves here, but for clarity, also: + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + } + } + } + + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + screen->forceDisplay(true); + return 1; + } + + // CANCEL + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + searchQuery = ""; + + // UIFrameEvent e; + // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + // notifyObservers(&e); + screen->forceDisplay(true); + return 1; + } + + return 0; +} + +bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + // Override isDown and isSelect ONLY for canned message list behavior + if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + return false; + + // Handle Cancel key: go inactive, clear UI state + if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && + (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + bool handled = false; + + // Handle up/down navigation + if (isUp && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + handled = true; + } else if (isDown && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + handled = true; + } else if (isSelect) { + const char *current = messages[currentMessageIndex]; + + // [Select Destination] triggers destination selection UI + if (strcmp(current, "[Select Destination]") == 0) { + returnToCannedList = true; + runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + destIndex = 0; + scrollIndex = 0; + updateDestinationSelectionList(); // Make sure list is fresh + screen->forceDisplay(); + return true; + } + + // [Exit] returns to the main/inactive screen + if (strcmp(current, "[Exit]") == 0) { + // Set runState to inactive so we return to main UI + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; currentMessageIndex = -1; + // Notify UI to regenerate frame set and redraw UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); + notifyObservers(&e); screen->forceDisplay(); + return true; + } - setIntervalFromNow(500); - return; - } else { - // Don't delete virtual keyboard immediately - it might still be executing - // Instead, just clear the callback and reset banner to stop input processing - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - - // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Force display update to show normal screen + // [Free Text] triggers the free text input (virtual keyboard) +#if defined(USE_VIRTUAL_KEYBOARD) + if (strcmp(current, "[-- Free Text --]") == 0) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - // Schedule cleanup for next loop iteration to ensure safe deletion - setIntervalFromNow(50); - return; - } - }); - - return true; - } - } -#endif - - // Normal canned message selection - if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { - } else { -#if CANNED_MESSAGE_ADD_CONFIRMATION - const int savedIndex = currentMessageIndex; - graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { - this->currentMessageIndex = savedIndex; - this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - this->setIntervalFromNow(0); - }); + notifyObservers(&e); + return true; + } #else - payload = runState; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + if (strcmp(current, "[-- Free Text --]") == 0) { + if (osk_found && screen) { + char headerBuffer[64]; + if (this->dest == NODENUM_BROADCAST) { + snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); + } else { + snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); + } + screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { + if (!text.empty()) { + this->freetext = text.c_str(); + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + currentMessageIndex = -1; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + setIntervalFromNow(500); + return; + } else { + // Don't delete virtual keyboard immediately - it might still be executing + // Instead, just clear the callback and reset banner to stop input processing + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + + // Return to inactive state + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Force display update to show normal screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + // Schedule cleanup for next loop iteration to ensure safe deletion + setIntervalFromNow(50); + return; + } + }); + + return true; + } + } #endif - // Do not immediately set runState; wait for confirmation - handled = true; + + // Normal canned message selection + if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + } else { +#if CANNED_MESSAGE_ADD_CONFIRMATION + const int savedIndex = currentMessageIndex; + graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { + this->currentMessageIndex = savedIndex; + this->payload = this->runState; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->setIntervalFromNow(0); + }); +#else + payload = runState; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; +#endif + // Do not immediately set runState; wait for confirmation + handled = true; + } } - } - if (handled) { - requestFocus(); - if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) - setIntervalFromNow(0); - else - runOnce(); - } + if (handled) { + requestFocus(); + if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) + setIntervalFromNow(0); + else + runOnce(); + } - return handled; + return handled; } -bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) { - // Always process only if in FREETEXT mode - if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) - return false; +bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) +{ + // Always process only if in FREETEXT mode + if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) + return false; #if defined(USE_VIRTUAL_KEYBOARD) - // Cancel (dismiss freetext screen) - if (event->inputEvent == INPUT_BROKER_LEFT) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; - payload = 0; - currentMessageIndex = -1; + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_LEFT) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; - // Notify UI that we want to redraw/close this screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return true; - } - // Touch input (virtual keyboard) handling - // Only handle if touch coordinates present (CardKB won't set these) - if (event->touchX != 0 || event->touchY != 0) { - String keyTapped = keyForCoordinates(event->touchX, event->touchY); - bool valid = false; + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + // Touch input (virtual keyboard) handling + // Only handle if touch coordinates present (CardKB won't set these) + if (event->touchX != 0 || event->touchY != 0) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + bool valid = false; - if (keyTapped == "⇧") { - highlight = -1; - payload = 0x00; - shift = !shift; - valid = true; - } else if (keyTapped == "⌫") { + if (keyTapped == "⇧") { + highlight = -1; + payload = 0x00; + shift = !shift; + valid = true; + } else if (keyTapped == "⌫") { #ifndef RAK14014 - highlight = keyTapped[0]; + highlight = keyTapped[0]; #endif - payload = 0x08; - shift = false; - valid = true; - } else if (keyTapped == "123" || keyTapped == "ABC") { - highlight = -1; - payload = 0x00; - charSet = (charSet == 0 ? 1 : 0); - valid = true; - } else if (keyTapped == " ") { + payload = 0x08; + shift = false; + valid = true; + } else if (keyTapped == "123" || keyTapped == "ABC") { + highlight = -1; + payload = 0x00; + charSet = (charSet == 0 ? 1 : 0); + valid = true; + } else if (keyTapped == " ") { #ifndef RAK14014 - highlight = keyTapped[0]; + highlight = keyTapped[0]; #endif - payload = keyTapped[0]; - shift = false; - valid = true; - } - // Touch enter/submit - else if (keyTapped == "↵") { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! - payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - currentMessageIndex = -1; - shift = false; - valid = true; - } else if (!(keyTapped == "")) { + payload = keyTapped[0]; + shift = false; + valid = true; + } + // Touch enter/submit + else if (keyTapped == "↵") { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + shift = false; + valid = true; + } else if (!(keyTapped == "")) { #ifndef RAK14014 - highlight = keyTapped[0]; + highlight = keyTapped[0]; #endif - payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); - shift = false; - valid = true; - } + payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); + shift = false; + valid = true; + } - if (valid) { - lastTouchMillis = millis(); - runOnce(); - payload = 0; - return true; // STOP: We handled a VKB touch + if (valid) { + lastTouchMillis = millis(); + runOnce(); + payload = 0; + return true; // STOP: We handled a VKB touch + } } - } #endif // USE_VIRTUAL_KEYBOARD - // All hardware keys fall through to here (CardKB, physical, etc.) + // All hardware keys fall through to here (CardKB, physical, etc.) - if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { - runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; + if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { + runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; + requestFocus(); + screen->forceDisplay(); + return true; + } + // Confirm select (Enter) + bool isSelect = isSelectEvent(event); + if (isSelect) { + LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, + freetext.c_str()); + if (dest == 0) + dest = NODENUM_BROADCAST; + // Defensive: If channel isn't valid, pick the first available channel + if (channel >= channels.getNumChannels()) + channel = 0; + + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Backspace + if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { + payload = 0x08; + lastTouchMillis = millis(); + requestFocus(); + runOnce(); + return true; + } + + // Move cursor left + if (event->inputEvent == INPUT_BROKER_LEFT) { + payload = INPUT_BROKER_LEFT; + lastTouchMillis = millis(); + requestFocus(); + runOnce(); + return true; + } + // Move cursor right + if (event->inputEvent == INPUT_BROKER_RIGHT) { + payload = INPUT_BROKER_RIGHT; + lastTouchMillis = millis(); + requestFocus(); + runOnce(); + return true; + } + + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || + (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + // Tab (switch destination) + if (event->kbchar == INPUT_BROKER_MSG_TAB) { + return handleTabSwitch(event); // Reuse tab logic + } + + // Printable ASCII (add char to draft) + if (event->kbchar >= 32 && event->kbchar <= 126) { + payload = event->kbchar; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + return false; +} + +int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) +{ + int numEmotes = graphics::numEmotes; + + // Override isDown and isSelect ONLY for emote picker behavior + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + // Scroll emote list + if (isUp && emotePickerIndex > 0) { + emotePickerIndex--; + screen->forceDisplay(); + return 1; + } + if (isDown && emotePickerIndex < numEmotes - 1) { + emotePickerIndex++; + screen->forceDisplay(); + return 1; + } + + // Select emote: insert into freetext at cursor and return to freetext + if (isSelect) { + String label = graphics::emotes[emotePickerIndex].label; + String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" + if (cursor == freetext.length()) { + freetext += emoteInsert; + } else { + freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); + } + cursor += emoteInsert.length(); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + + // Cancel returns to freetext + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + + return 0; +} + +void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) +{ + lastDest = dest; + lastChannel = channel; + lastDestSet = true; + + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num + + this->lastSentNode = dest; + this->incoming = dest; + + // Manually find the node by number to check PKI capability + meshtastic_NodeInfoLite *node = nullptr; + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num == dest) { + node = n; + break; + } + } + + NodeNum myNodeNum = nodeDB->getNodeNum(); + if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { + p->pki_encrypted = true; + p->channel = 0; // force PKI + } + + // Track this packet’s request ID for matching ACKs + this->lastRequestId = p->id; + + // Copy payload + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; + } + + this->waitingForAck = true; + + // Send to mesh (PKI-encrypted if conditions above matched) + service->sendToMesh(p, RX_SRC_LOCAL, true); + + // Show banner immediately + if (screen) { + graphics::BannerOverlayOptions opts; + opts.message = "Sending..."; + opts.durationMs = 2000; + screen->showOverlayBanner(opts); + } + + // Save outgoing message + StoredMessage sm; + + // Always use our local time, consistent with other paths + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; + sm.isBootRelative = (nowSecs == 0); + + sm.sender = nodeDB->getNodeNum(); // us + sm.channelIndex = channel; + size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); + sm.textOffset = MessageStore::storeText(message, len); + sm.textLength = len; + + // Classify broadcast vs DM + if (dest == NODENUM_BROADCAST) { + sm.dest = NODENUM_BROADCAST; + sm.type = MessageType::BROADCAST; + } else { + sm.dest = dest; + sm.type = MessageType::DM_TO_US; + // Only add as favorite if our role is NOT CLIENT_BASE + if (config.device.role != 12) { + LOG_INFO("Proactively adding %x as favorite node", dest); + nodeDB->set_favorite(true, dest); + } else { + LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); + } + } + sm.ackStatus = AckStatus::NONE; + + messageStore.addLiveMessage(std::move(sm)); + + // Auto-switch thread view on outgoing message + if (sm.type == MessageType::BROADCAST) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); + } else { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); + } + + playComboTune(); + + this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + this->payload = wantReplies ? 1 : 0; requestFocus(); - screen->forceDisplay(); - return true; - } - // Confirm select (Enter) - bool isSelect = isSelectEvent(event); - if (isSelect) { - LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, freetext.c_str()); - if (dest == 0) - dest = NODENUM_BROADCAST; - // Defensive: If channel isn't valid, pick the first available channel - if (channel >= channels.getNumChannels()) - channel = 0; - payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - currentMessageIndex = -1; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - lastTouchMillis = millis(); - runOnce(); - return true; - } - - // Backspace - if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { - payload = 0x08; - lastTouchMillis = millis(); - requestFocus(); - runOnce(); - return true; - } - - // Move cursor left - if (event->inputEvent == INPUT_BROKER_LEFT) { - payload = INPUT_BROKER_LEFT; - lastTouchMillis = millis(); - requestFocus(); - runOnce(); - return true; - } - // Move cursor right - if (event->inputEvent == INPUT_BROKER_RIGHT) { - payload = INPUT_BROKER_RIGHT; - lastTouchMillis = millis(); - requestFocus(); - runOnce(); - return true; - } - - // Cancel (dismiss freetext screen) - if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || - (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; - payload = 0; - currentMessageIndex = -1; - - // Notify UI that we want to redraw/close this screen + // Tell Screen to switch to TextMessage frame via UIFrameEvent UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; notifyObservers(&e); - screen->forceDisplay(); - return true; - } - - // Tab (switch destination) - if (event->kbchar == INPUT_BROKER_MSG_TAB) { - return handleTabSwitch(event); // Reuse tab logic - } - - // Printable ASCII (add char to draft) - if (event->kbchar >= 32 && event->kbchar <= 126) { - payload = event->kbchar; - lastTouchMillis = millis(); - runOnce(); - return true; - } - - return false; } -int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) { - int numEmotes = graphics::numEmotes; - - // Override isDown and isSelect ONLY for emote picker behavior - bool isUp = isUpEvent(event); - bool isDown = isDownEvent(event); - bool isSelect = isSelectEvent(event); - if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { - if (event->inputEvent == INPUT_BROKER_USER_PRESS) { - isDown = true; - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - isSelect = true; - } - } - - // Scroll emote list - if (isUp && emotePickerIndex > 0) { - emotePickerIndex--; - screen->forceDisplay(); - return 1; - } - if (isDown && emotePickerIndex < numEmotes - 1) { - emotePickerIndex++; - screen->forceDisplay(); - return 1; - } - - // Select emote: insert into freetext at cursor and return to freetext - if (isSelect) { - String label = graphics::emotes[emotePickerIndex].label; - String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" - if (cursor == freetext.length()) { - freetext += emoteInsert; - } else { - freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); - } - cursor += emoteInsert.length(); - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - screen->forceDisplay(); - return 1; - } - - // Cancel returns to freetext - if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - screen->forceDisplay(); - return 1; - } - - return 0; -} - -void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) { - lastDest = dest; - lastChannel = channel; - lastDestSet = true; - - meshtastic_MeshPacket *p = allocDataPacket(); - p->to = dest; - p->channel = channel; - p->want_ack = true; - p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num - - this->lastSentNode = dest; - this->incoming = dest; - - // Manually find the node by number to check PKI capability - meshtastic_NodeInfoLite *node = nullptr; - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < numMeshNodes; ++i) { - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num == dest) { - node = n; - break; - } - } - - NodeNum myNodeNum = nodeDB->getNodeNum(); - if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { - p->pki_encrypted = true; - p->channel = 0; // force PKI - } - - // Track this packet’s request ID for matching ACKs - this->lastRequestId = p->id; - - // Copy payload - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - - if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size++] = 7; - p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; - } - - this->waitingForAck = true; - - // Send to mesh (PKI-encrypted if conditions above matched) - service->sendToMesh(p, RX_SRC_LOCAL, true); - - // Show banner immediately - if (screen) { - graphics::BannerOverlayOptions opts; - opts.message = "Sending..."; - opts.durationMs = 2000; - screen->showOverlayBanner(opts); - } - - // Save outgoing message - StoredMessage sm; - - // Always use our local time, consistent with other paths - uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); - sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; - sm.isBootRelative = (nowSecs == 0); - - sm.sender = nodeDB->getNodeNum(); // us - sm.channelIndex = channel; - size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); - sm.textOffset = MessageStore::storeText(message, len); - sm.textLength = len; - - // Classify broadcast vs DM - if (dest == NODENUM_BROADCAST) { - sm.dest = NODENUM_BROADCAST; - sm.type = MessageType::BROADCAST; - } else { - sm.dest = dest; - sm.type = MessageType::DM_TO_US; - // Only add as favorite if our role is NOT CLIENT_BASE - if (config.device.role != 12) { - LOG_INFO("Proactively adding %x as favorite node", dest); - nodeDB->set_favorite(true, dest); - } else { - LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); - } - } - sm.ackStatus = AckStatus::NONE; - - messageStore.addLiveMessage(std::move(sm)); - - // Auto-switch thread view on outgoing message - if (sm.type == MessageType::BROADCAST) { - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); - } else { - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); - } - - playComboTune(); - - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; - this->payload = wantReplies ? 1 : 0; - requestFocus(); - - // Tell Screen to switch to TextMessage frame via UIFrameEvent - UIFrameEvent e; - e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; - notifyObservers(&e); -} - -int32_t CannedMessageModule::runOnce() { - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { - updateDestinationSelectionList(); - needsUpdate = false; - } - - // If we're in node selection, do nothing except keep alive - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - return INACTIVATE_AFTER_MS; - } - - // Normal module disable/idle handling - if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { - // Clean up virtual keyboard if needed when going inactive - if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { - LOG_INFO("Performing delayed virtual keyboard cleanup"); - graphics::OnScreenKeyboardModule::instance().stop(false); +int32_t CannedMessageModule::runOnce() +{ + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { + updateDestinationSelectionList(); + needsUpdate = false; } - return INT32_MAX; - } + // If we're in node selection, do nothing except keep alive + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + return INACTIVATE_AFTER_MS; + } - // Handle delayed virtual keyboard message sending - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - // Virtual keyboard message sending case - text was not empty - if (this->freetext.length() > 0) { - LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); - sendText(this->dest, this->channel, this->freetext.c_str(), true); + // Normal module disable/idle handling + if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + // Clean up virtual keyboard if needed when going inactive + if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { + LOG_INFO("Performing delayed virtual keyboard cleanup"); + graphics::OnScreenKeyboardModule::instance().stop(false); + } - // Clean up virtual keyboard after sending - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard after message send"); - graphics::OnScreenKeyboardModule::instance().stop(false); - graphics::NotificationRenderer::resetBanner(); - } + return INT32_MAX; + } - // Clear payload to indicate virtual keyboard processing is complete - // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds - this->payload = 0; - } else { - // Empty message, just go inactive - LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + // Handle delayed virtual keyboard message sending + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + // Virtual keyboard message sending case - text was not empty + if (this->freetext.length() > 0) { + LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); + sendText(this->dest, this->channel, this->freetext.c_str(), true); + + // Clean up virtual keyboard after sending + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard after message send"); + graphics::OnScreenKeyboardModule::instance().stop(false); + graphics::NotificationRenderer::resetBanner(); + } + + // Clear payload to indicate virtual keyboard processing is complete + // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds + this->payload = 0; + } else { + // Empty message, just go inactive + LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); + return 2000; } UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - return 2000; - } - - UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || - (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - } - // Handle SENDING_ACTIVE state transition after virtual keyboard message - else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - return INT32_MAX; - } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && - !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { - // Reset module on inactivity - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - // Clean up virtual keyboard if it exists during timeout - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - graphics::OnScreenKeyboardModule::instance().stop(false); - graphics::NotificationRenderer::resetBanner(); + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && + this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || + (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); } + // Handle SENDING_ACTIVE state transition after virtual keyboard message + else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + return INT32_MAX; + } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && + !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { + // Reset module on inactivity + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->notifyObservers(&e); - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == 0) { - // [Exit] button pressed - return to inactive state - LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - if (this->freetext.length() > 0) { - sendText(this->dest, this->channel, this->freetext.c_str(), true); + // Clean up virtual keyboard if it exists during timeout + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard due to module timeout"); + graphics::OnScreenKeyboardModule::instance().stop(false); + graphics::NotificationRenderer::resetBanner(); + } - // Clean up state but *don’t* deactivate yet + this->notifyObservers(&e); + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { + if (this->payload == 0) { + // [Exit] button pressed - return to inactive state + LOG_INFO("Processing [Exit] action - returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->freetext.length() > 0) { + sendText(this->dest, this->channel, this->freetext.c_str(), true); + + // Clean up state but *don’t* deactivate yet + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list + } else { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + } else { + if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + return INT32_MAX; + } + if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { + if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { + return INT32_MAX; + } else { + sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); + + // Clean up state + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list + } + } else { + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + } + // fallback clean-up if nothing above returned this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - // Tell Screen to jump straight to the TextMessage frame UIFrameEvent e; - e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); - // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - return INT32_MAX; // don’t fall back into canned list - } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - } else { - if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + // Immediately stop, don’t linger on canned screen return INT32_MAX; - } - if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { - if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { - return INT32_MAX; - } else { - sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); - - // Clean up state - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Tell Screen to jump straight to the TextMessage frame - UIFrameEvent e; - e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; - this->notifyObservers(&e); - - // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - return INT32_MAX; // don’t fall back into canned list - } - } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } } - // fallback clean-up if nothing above returned - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - - // Immediately stop, don’t linger on canned screen - return INT32_MAX; - } - // Highlight [Select Destination] initially when entering the message list - else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, - // not when coming back from a sent message. - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - int selectDestination = 0; - for (int i = 0; i < this->messagesCount; ++i) { - if (strcmp(this->messages[i], "[Select Destination]") == 0) { - selectDestination = i; - break; - } - } - this->currentMessageIndex = selectDestination; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { - if (this->messagesCount > 0) { - this->currentMessageIndex = getPrevIndex(); - this->freetext = ""; - this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { - if (this->messagesCount > 0) { - this->currentMessageIndex = this->getNextIndex(); - this->freetext = ""; - this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - switch (this->payload) { - case INPUT_BROKER_LEFT: - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { - this->cursor--; - } - break; - case INPUT_BROKER_RIGHT: - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { - this->cursor++; - } - break; - default: - break; - } - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - switch (this->payload) { - case 0x08: // backspace - if (this->freetext.length() > 0) { - if (this->cursor > 0) { - if (this->cursor == this->freetext.length()) { - this->freetext = this->freetext.substring(0, this->freetext.length() - 1); - } else { - this->freetext = this->freetext.substring(0, this->cursor - 1) + this->freetext.substring(this->cursor, this->freetext.length()); + // Highlight [Select Destination] initially when entering the message list + else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { + // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, + // not when coming back from a sent message. + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + int selectDestination = 0; + for (int i = 0; i < this->messagesCount; ++i) { + if (strcmp(this->messages[i], "[Select Destination]") == 0) { + selectDestination = i; + break; + } } - this->cursor--; - } - } else { + this->currentMessageIndex = selectDestination; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; } - break; - case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler - return 0; - case INPUT_BROKER_LEFT: - case INPUT_BROKER_RIGHT: - break; - default: - // Only insert ASCII printable characters (32–126) - if (this->payload >= 32 && this->payload <= 126) { - requestFocus(); - if (this->cursor == this->freetext.length()) { - this->freetext += (char)this->payload; - } else { - this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + this->freetext.substring(this->cursor); - } - this->cursor++; - const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); - if (this->freetext.length() > maxChars) { - this->cursor = maxChars; - this->freetext = this->freetext.substring(0, maxChars); - } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { + if (this->messagesCount > 0) { + this->currentMessageIndex = getPrevIndex(); + this->freetext = ""; + this->cursor = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; } - break; - } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { + if (this->messagesCount > 0) { + this->currentMessageIndex = this->getNextIndex(); + this->freetext = ""; + this->cursor = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + } + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + switch (this->payload) { + case INPUT_BROKER_LEFT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { + this->cursor--; + } + break; + case INPUT_BROKER_RIGHT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { + this->cursor++; + } + break; + default: + break; + } + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + switch (this->payload) { + case 0x08: // backspace + if (this->freetext.length() > 0) { + if (this->cursor > 0) { + if (this->cursor == this->freetext.length()) { + this->freetext = this->freetext.substring(0, this->freetext.length() - 1); + } else { + this->freetext = this->freetext.substring(0, this->cursor - 1) + + this->freetext.substring(this->cursor, this->freetext.length()); + } + this->cursor--; + } + } else { + } + break; + case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler + return 0; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: + break; + default: + // Only insert ASCII printable characters (32–126) + if (this->payload >= 32 && this->payload <= 126) { + requestFocus(); + if (this->cursor == this->freetext.length()) { + this->freetext += (char)this->payload; + } else { + this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + + this->freetext.substring(this->cursor); + } + this->cursor++; + const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); + if (this->freetext.length() > maxChars) { + this->cursor = maxChars; + this->freetext = this->freetext.substring(0, maxChars); + } + } + break; + } + } + this->lastTouchMillis = millis(); + this->notifyObservers(&e); + return INACTIVATE_AFTER_MS; } - this->lastTouchMillis = millis(); - this->notifyObservers(&e); - return INACTIVATE_AFTER_MS; - } - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { - this->lastTouchMillis = millis(); - this->notifyObservers(&e); - return INACTIVATE_AFTER_MS; - } - return INT32_MAX; + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + this->lastTouchMillis = millis(); + this->notifyObservers(&e); + return INACTIVATE_AFTER_MS; + } + return INT32_MAX; } -const char *CannedMessageModule::getCurrentMessage() { return this->messages[this->currentMessageIndex]; } -const char *CannedMessageModule::getPrevMessage() { return this->messages[this->getPrevIndex()]; } -const char *CannedMessageModule::getNextMessage() { return this->messages[this->getNextIndex()]; } -const char *CannedMessageModule::getMessageByIndex(int index) { return (index >= 0 && index < this->messagesCount) ? this->messages[index] : ""; } - -const char *CannedMessageModule::getNodeName(NodeNum node) { - if (node == NODENUM_BROADCAST) - return "Broadcast"; - - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info && info->has_user && strlen(info->user.long_name) > 0) { - return info->user.long_name; - } - - static char fallback[12]; - snprintf(fallback, sizeof(fallback), "0x%08x", node); - return fallback; +const char *CannedMessageModule::getCurrentMessage() +{ + return this->messages[this->currentMessageIndex]; +} +const char *CannedMessageModule::getPrevMessage() +{ + return this->messages[this->getPrevIndex()]; +} +const char *CannedMessageModule::getNextMessage() +{ + return this->messages[this->getNextIndex()]; +} +const char *CannedMessageModule::getMessageByIndex(int index) +{ + return (index >= 0 && index < this->messagesCount) ? this->messages[index] : ""; } -bool CannedMessageModule::shouldDraw() { - // Only allow drawing when we're in an interactive UI state. - return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || - this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); +const char *CannedMessageModule::getNodeName(NodeNum node) +{ + if (node == NODENUM_BROADCAST) + return "Broadcast"; + + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user && strlen(info->user.long_name) > 0) { + return info->user.long_name; + } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; +} + +bool CannedMessageModule::shouldDraw() +{ + // Only allow drawing when we're in an interactive UI state. + return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || + this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); } // Has the user defined any canned messages? // Expose publicly whether canned message module is ready for use -bool CannedMessageModule::hasMessages() { return (this->messagesCount > 0); } - -int CannedMessageModule::getNextIndex() { - if (this->currentMessageIndex >= (this->messagesCount - 1)) { - return 0; - } else { - return this->currentMessageIndex + 1; - } +bool CannedMessageModule::hasMessages() +{ + return (this->messagesCount > 0); } -int CannedMessageModule::getPrevIndex() { - if (this->currentMessageIndex <= 0) { - return this->messagesCount - 1; - } else { - return this->currentMessageIndex - 1; - } +int CannedMessageModule::getNextIndex() +{ + if (this->currentMessageIndex >= (this->messagesCount - 1)) { + return 0; + } else { + return this->currentMessageIndex + 1; + } +} + +int CannedMessageModule::getPrevIndex() +{ + if (this->currentMessageIndex <= 0) { + return this->messagesCount - 1; + } else { + return this->currentMessageIndex - 1; + } } #if defined(USE_VIRTUAL_KEYBOARD) -String CannedMessageModule::keyForCoordinates(uint x, uint y) { - int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; +String CannedMessageModule::keyForCoordinates(uint x, uint y) +{ + int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; - for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { - int innerSize = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; + for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { + int innerSize = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; - for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { - Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; - if (x > letter.rectX && x < (letter.rectX + letter.rectWidth) && y > letter.rectY && y < (letter.rectY + letter.rectHeight)) { - return letter.character; - } + if (x > letter.rectX && x < (letter.rectX + letter.rectWidth) && y > letter.rectY && + y < (letter.rectY + letter.rectHeight)) { + return letter.character; + } + } } - } - return ""; + return ""; } -void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; +void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; - int xOffset = 0; + int xOffset = 0; - int yOffset = 56; + int yOffset = 56; - display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + display->setFont(FONT_SMALL); - display->setColor(OLEDDISPLAY_COLOR::WHITE); + display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawStringMaxWidth(0, 0, display->getWidth(), - cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); + display->drawStringMaxWidth(0, 0, display->getWidth(), + cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); - display->setFont(FONT_MEDIUM); + display->setFont(FONT_MEDIUM); - int cellHeight = round((display->height() - 64) / outerSize); + int cellHeight = round((display->height() - 64) / outerSize); - int yCorrection = 8; + int yCorrection = 8; - for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { - yOffset += outerIndex > 0 ? cellHeight : 0; + for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { + yOffset += outerIndex > 0 ? cellHeight : 0; - int innerSizeBound = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; + int innerSizeBound = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; - int innerSize = 0; + int innerSize = 0; - for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) { - if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") { - innerSize++; - } - } + for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) { + if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") { + innerSize++; + } + } - int cellWidth = display->width() / innerSize; + int cellWidth = display->width() / innerSize; - for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { - xOffset += innerIndex > 0 ? cellWidth : 0; + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + xOffset += innerIndex > 0 ? cellWidth : 0; - Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; - Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight}; + Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight}; #ifdef RAK14014 // Optimize the touch range of the virtual keyboard in the bottom row - if (outerIndex == outerSize - 1) { - updatedLetter.rectHeight = 240 - yOffset; - } + if (outerIndex == outerSize - 1) { + updatedLetter.rectHeight = 240 - yOffset; + } #endif - this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter; + this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter; - float characterOffset = ((cellWidth / 2) - (letter.width / 2)); + float characterOffset = ((cellWidth / 2) - (letter.width / 2)); - if (letter.character == "⇧") { - if (this->shift) { - display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + if (letter.character == "⇧") { + if (this->shift) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - display->setColor(OLEDDISPLAY_COLOR::BLACK); + display->setColor(OLEDDISPLAY_COLOR::BLACK); - drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - display->setColor(OLEDDISPLAY_COLOR::WHITE); - } else { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + display->setColor(OLEDDISPLAY_COLOR::WHITE); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + } + } else if (letter.character == "⌫") { + if (this->highlight == letter.character[0]) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + setIntervalFromNow(0); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + } + } else if (letter.character == "↵") { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawEnterIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.7); + } else { + if (this->highlight == letter.character[0]) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + display->drawString(xOffset + characterOffset, yOffset + yCorrection, + letter.character == " " ? "space" : letter.character); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + setIntervalFromNow(0); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + display->drawString(xOffset + characterOffset, yOffset + yCorrection, + letter.character == " " ? "space" : letter.character); + } + } } - } else if (letter.character == "⌫") { - if (this->highlight == letter.character[0]) { - display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - display->setColor(OLEDDISPLAY_COLOR::BLACK); - - drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - setIntervalFromNow(0); - } else { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - - drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); - } - } else if (letter.character == "↵") { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - - drawEnterIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.7); - } else { - if (this->highlight == letter.character[0]) { - display->fillRect(xOffset, yOffset, cellWidth, cellHeight); - - display->setColor(OLEDDISPLAY_COLOR::BLACK); - - display->drawString(xOffset + characterOffset, yOffset + yCorrection, letter.character == " " ? "space" : letter.character); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - setIntervalFromNow(0); - } else { - display->drawRect(xOffset, yOffset, cellWidth, cellHeight); - - display->drawString(xOffset + characterOffset, yOffset + yCorrection, letter.character == " " ? "space" : letter.character); - } - } + xOffset = 0; } - xOffset = 0; - } - - this->highlight = 0x00; + this->highlight = 0x00; } -void CannedMessageModule::drawShiftIcon(OLEDDisplay *display, int x, int y, float scale) { - PointStruct shiftIcon[10] = {{8, 0}, {15, 7}, {15, 8}, {12, 8}, {12, 12}, {4, 12}, {4, 8}, {1, 8}, {1, 7}, {8, 0}}; +void CannedMessageModule::drawShiftIcon(OLEDDisplay *display, int x, int y, float scale) +{ + PointStruct shiftIcon[10] = {{8, 0}, {15, 7}, {15, 8}, {12, 8}, {12, 12}, {4, 12}, {4, 8}, {1, 8}, {1, 7}, {8, 0}}; - int size = 10; + int size = 10; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (shiftIcon[i].x * scale); - int y0 = y + (shiftIcon[i].y * scale); - int x1 = x + (shiftIcon[i + 1].x * scale); - int y1 = y + (shiftIcon[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (shiftIcon[i].x * scale); + int y0 = y + (shiftIcon[i].y * scale); + int x1 = x + (shiftIcon[i + 1].x * scale); + int y1 = y + (shiftIcon[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } } -void CannedMessageModule::drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale) { - PointStruct backspaceIcon[6] = {{0, 7}, {5, 2}, {15, 2}, {15, 12}, {5, 12}, {0, 7}}; +void CannedMessageModule::drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale) +{ + PointStruct backspaceIcon[6] = {{0, 7}, {5, 2}, {15, 2}, {15, 12}, {5, 12}, {0, 7}}; - int size = 6; + int size = 6; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (backspaceIcon[i].x * scale); - int y0 = y + (backspaceIcon[i].y * scale); - int x1 = x + (backspaceIcon[i + 1].x * scale); - int y1 = y + (backspaceIcon[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (backspaceIcon[i].x * scale); + int y0 = y + (backspaceIcon[i].y * scale); + int x1 = x + (backspaceIcon[i + 1].x * scale); + int y1 = y + (backspaceIcon[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } - PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 4}}; + PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 4}}; - size = 4; + size = 4; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (backspaceIconX[i].x * scale); - int y0 = y + (backspaceIconX[i].y * scale); - int x1 = x + (backspaceIconX[i + 1].x * scale); - int y1 = y + (backspaceIconX[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (backspaceIconX[i].x * scale); + int y0 = y + (backspaceIconX[i].y * scale); + int x1 = x + (backspaceIconX[i + 1].x * scale); + int y1 = y + (backspaceIconX[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } } -void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, float scale) { - PointStruct enterIcon[6] = {{0, 7}, {4, 3}, {4, 11}, {0, 7}, {15, 7}, {15, 0}}; +void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, float scale) +{ + PointStruct enterIcon[6] = {{0, 7}, {4, 3}, {4, 11}, {0, 7}, {15, 7}, {15, 0}}; - int size = 6; + int size = 6; - for (int i = 0; i < size - 1; i++) { - int x0 = x + (enterIcon[i].x * scale); - int y0 = y + (enterIcon[i].y * scale); - int x1 = x + (enterIcon[i + 1].x * scale); - int y1 = y + (enterIcon[i + 1].y * scale); + for (int i = 0; i < size - 1; i++) { + int x0 = x + (enterIcon[i].x * scale); + int y0 = y + (enterIcon[i].y * scale); + int x1 = x + (enterIcon[i + 1].x * scale); + int y1 = y + (enterIcon[i + 1].y * scale); - display->drawLine(x0, y0, x1, y1); - } + display->drawLine(x0, y0, x1, y1); + } } #endif // Indicate to screen class that module is handling keyboard input specially (at certain times) // This prevents the left & right keys being used for nav. between screen frames during text entry. -bool CannedMessageModule::interceptingKeyboardInput() { - switch (runState) { - case CANNED_MESSAGE_RUN_STATE_DISABLED: - case CANNED_MESSAGE_RUN_STATE_INACTIVE: - return false; - default: - return true; - } +bool CannedMessageModule::interceptingKeyboardInput() +{ + switch (runState) { + case CANNED_MESSAGE_RUN_STATE_DISABLED: + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + return false; + default: + return true; + } } // Draw the node/channel selection screen -void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - requestFocus(); - display->setColor(WHITE); // Always draw cleanly - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // Header - int titleY = 2; - String titleText = "Select Destination"; - titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, titleY, titleText); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // List Items - int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); - int numActiveChannels = this->activeChannelIndices.size(); - int totalEntries = numActiveChannels + this->filteredNodes.size(); - int columns = 1; - this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); - if (this->visibleRows < 1) - this->visibleRows = 1; - - // Clamp scrolling - if (scrollIndex > totalEntries / columns) - scrollIndex = totalEntries / columns; - if (scrollIndex < 0) - scrollIndex = 0; - - for (int row = 0; row < visibleRows; row++) { - int itemIndex = scrollIndex + row; - if (itemIndex >= totalEntries) - break; - - int xOffset = 0; - int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - char entryText[64] = ""; - - // Draw Channels First - if (itemIndex < numActiveChannels) { - uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); - } - // Then Draw Nodes - else { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && node->user.long_name) { - strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); - entryText[sizeof(entryText) - 1] = '\0'; - } - int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - - ((node && node->is_favorite) ? 10 : 0); - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(entryText); - while (entryText[0] && display->getStringWidth(entryText) > availWidth) { - entryText[strlen(entryText) - 1] = '\0'; - } - if (strlen(entryText) < origLen) { - strcat(entryText, "..."); - } - - // Prepend "* " if this is a favorite - if (node && node->is_favorite) { - size_t len = strlen(entryText); - if (len + 2 < sizeof(entryText)) { - memmove(entryText + 2, entryText, len + 1); - entryText[0] = '*'; - entryText[1] = ' '; - } - } - if (node) { - if (display->getWidth() <= 64) { - snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); - } - } - } - } - - if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) - strcpy(entryText, "?"); - - // Highlight background (if selected) - if (itemIndex == destIndex) { - int scrollPadding = 8; // Reserve space for scrollbar - display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); - display->setColor(BLACK); - } - - // Draw entry text - display->drawString(xOffset + 2, yOffset, entryText); - display->setColor(WHITE); - - // Draw key icon (after highlight) - /* - if (itemIndex >= numActiveChannels) { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && hasKeyForNode(node)) { - int iconX = display->getWidth() - key_symbol_width - 15; - int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; - - if (itemIndex == destIndex) { - display->setColor(INVERSE); - } else { - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); - } - } - } - */ - } - - // Scrollbar - if (totalEntries > visibleRows) { - int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); - int totalScrollable = totalEntries; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); - int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; - int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; - display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); - } -} - -void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height - const int headerMargin = 2; // Extra pixels below header - const int labelGap = 6; - const int bitmapGapX = 4; - - // Find max emote height (assume all same, or precalculated) - int maxEmoteHeight = 0; - for (int i = 0; i < graphics::numEmotes; ++i) - if (graphics::emotes[i].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[i].height; - - const int rowHeight = maxEmoteHeight + 2; - - // Place header at top, then compute start of emote list - int headerY = y; - int listTop = headerY + headerFontHeight + headerMargin; - - int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; - int numEmotes = graphics::numEmotes; - - // keep member variable in sync - this->visibleRows = _visibleRows; - - // Clamp highlight index - if (emotePickerIndex < 0) - emotePickerIndex = 0; - if (emotePickerIndex >= numEmotes) - emotePickerIndex = numEmotes - 1; - - // Determine which emote is at the top - int topIndex = emotePickerIndex - _visibleRows / 2; - if (topIndex < 0) - topIndex = 0; - if (topIndex > numEmotes - _visibleRows) - topIndex = std::max(0, numEmotes - _visibleRows); - - // Draw header/title - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, headerY, "Select Emote"); - - // Draw emote rows - display->setTextAlignment(TEXT_ALIGN_LEFT); - - for (int vis = 0; vis < _visibleRows; ++vis) { - int emoteIdx = topIndex + vis; - if (emoteIdx >= numEmotes) - break; - const graphics::Emote &emote = graphics::emotes[emoteIdx]; - int rowY = listTop + vis * rowHeight; - - // Draw highlight box 2px taller than emote (1px margin above and below) - if (emoteIdx == emotePickerIndex) { - display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); - display->setColor(BLACK); - } - - // Emote bitmap (left), 1px margin from highlight bar top - int emoteY = rowY + 1; - display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); - - // Emote label (right of bitmap) - display->setFont(FONT_MEDIUM); - int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); - display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); - - if (emoteIdx == emotePickerIndex) - display->setColor(WHITE); - } - - // Draw scrollbar if needed - if (numEmotes > _visibleRows) { - int scrollbarHeight = _visibleRows * rowHeight; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); - int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); - int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; - display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); - } -} - -void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - this->displayHeight = display->getHeight(); // Store display height for later use - char buffer[50]; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // Never draw if state is outside our UI modes - if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { - return; // bail if not in a UI state that should render - } - - // Emote Picker Screen - if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { - drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here - return; - } - - // Destination Selection - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - drawDestinationSelectionScreen(display, state, x, y); - return; - } - - // Disabled Screen - if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); - return; - } - - // Free Text Input Screen - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { +void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ requestFocus(); -#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) - EInkDynamicDisplay *einkDisplay = static_cast(display); - einkDisplay->enableUnlimitedFastMode(); -#endif -#if defined(USE_VIRTUAL_KEYBOARD) - drawKeyboard(display, state, 0, 0); -#else + display->setColor(WHITE); // Always draw cleanly display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // Draw node/channel header at the top - drawHeader(display, x, y, buffer); + // Header + int titleY = 2; + String titleText = "Select Destination"; + titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_LEFT); - // Char count right-aligned - if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); - snprintf(buffer, sizeof(buffer), "%d left", charsLeft); - display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); - } + // List Items + int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); + int numActiveChannels = this->activeChannelIndices.size(); + int totalEntries = numActiveChannels + this->filteredNodes.size(); + int columns = 1; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); + if (this->visibleRows < 1) + this->visibleRows = 1; -#if INPUTBROKER_SERIAL_TYPE == 1 - // Chatter Modifier key mode label (right side) - { - uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0; - const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#"; + // Clamp scrolling + if (scrollIndex > totalEntries / columns) + scrollIndex = totalEntries / columns; + if (scrollIndex < 0) + scrollIndex = 0; - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); + for (int row = 0; row < visibleRows; row++) { + int itemIndex = scrollIndex + row; + if (itemIndex >= totalEntries) + break; - const int16_t th = FONT_HEIGHT_SMALL; - const int16_t tw = display->getStringWidth(label); - const int16_t padX = 3; - const int16_t padY = 2; - const int16_t r = 3; + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + char entryText[64] = ""; - const int16_t bw = tw + padX * 2; - const int16_t bh = th + padY * 2; - - const int16_t bx = x + display->getWidth() - bw - 2; - const int16_t by = y + display->getHeight() - bh - 2; - - display->setColor(WHITE); - display->fillRect(bx + r, by, bw - r * 2, bh); - display->fillRect(bx, by + r, r, bh - r * 2); - display->fillRect(bx + bw - r, by + r, r, bh - r * 2); - display->fillCircle(bx + r, by + r, r); - display->fillCircle(bx + bw - r - 1, by + r, r); - display->fillCircle(bx + r, by + bh - r - 1, r); - display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); - - display->setColor(BLACK); - display->drawString(bx + padX, by + padY, label); - } - - // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + ◄”) - { - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - const char *label = "Dest: Shift + "; - int16_t labelW = display->getStringWidth(label); - - // triangle size visually matches glyph height, not full line height - const int triH = FONT_HEIGHT_SMALL - 3; - const int triW = triH * 0.7; - - const int16_t padX = 3; - const int16_t padY = 2; - const int16_t r = 3; - - const int16_t bw = labelW + triW + padX * 2 + 2; - const int16_t bh = FONT_HEIGHT_SMALL + padY * 2; - - const int16_t bx = x + 2; - const int16_t by = y + display->getHeight() - bh - 2; - - // Rounded white box - display->setColor(WHITE); - display->fillRect(bx + r, by, bw - (r * 2), bh); - display->fillRect(bx, by + r, r, bh - (r * 2)); - display->fillRect(bx + bw - r, by + r, r, bh - (r * 2)); - display->fillCircle(bx + r, by + r, r); - display->fillCircle(bx + bw - r - 1, by + r, r); - display->fillCircle(bx + r, by + bh - r - 1, r); - display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); - - // Draw text - display->setColor(BLACK); - display->drawString(bx + padX, by + padY, label); - - // Perfectly center triangle on text baseline - int16_t tx = bx + padX + labelW; - int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering - - // ◄ Left-pointing triangle - display->fillTriangle(tx + triW, ty, // top-right - tx, ty + triH / 2, // left center - tx + triW, ty + triH // bottom-right - ); - } -#endif - // Draw Free Text input with multi-emote support and proper line wrapping - display->setColor(WHITE); - { - int inputY = 0 + y + FONT_HEIGHT_SMALL; - String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); - - // Tokenize input into (isEmote, token) pairs - const char *msg = msgWithCursor.c_str(); - std::vector> tokens = tokenizeMessageWithEmotes(msg); - - // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) - std::vector>> lines; - std::vector> currentLine; - int lineWidth = 0; - int maxWidth = display->getWidth(); - for (auto &token : tokens) { - if (token.first) { - // Emote - int tokenWidth = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - tokenWidth = graphics::emotes[j].width + 2; - break; - } - } - if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back(token); - lineWidth += tokenWidth; - } else { - // Text: split by words and wrap inside word if needed - String text = token.second; - int pos = 0; - while (pos < static_cast(text.length())) { - // Find next space (or end) - int spacePos = text.indexOf(' ', pos); - int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space - String word = text.substring(pos, endPos); - int wordWidth = display->getStringWidth(word); - - if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - // If word itself too big, split by character - if (wordWidth > maxWidth) { - uint16_t charPos = 0; - while (charPos < word.length()) { - String oneChar = word.substring(charPos, charPos + 1); - int charWidth = display->getStringWidth(oneChar); - if (lineWidth + charWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && node->user.long_name) { + strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); + entryText[sizeof(entryText) - 1] = '\0'; + } + int availWidth = display->getWidth() - + ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - + ((node && node->is_favorite) ? 10 : 0); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(entryText); + while (entryText[0] && display->getStringWidth(entryText) > availWidth) { + entryText[strlen(entryText) - 1] = '\0'; + } + if (strlen(entryText) < origLen) { + strcat(entryText, "..."); + } + + // Prepend "* " if this is a favorite + if (node && node->is_favorite) { + size_t len = strlen(entryText); + if (len + 2 < sizeof(entryText)) { + memmove(entryText + 2, entryText, len + 1); + entryText[0] = '*'; + entryText[1] = ' '; + } + } + if (node) { + if (display->getWidth() <= 64) { + snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); + } } - currentLine.push_back({false, oneChar}); - lineWidth += charWidth; - charPos++; - } - } else { - currentLine.push_back({false, word}); - lineWidth += wordWidth; } - pos = endPos; - } } - } - if (!currentLine.empty()) - lines.push_back(currentLine); - // Draw lines with emotes - int rowHeight = FONT_HEIGHT_SMALL; - int yLine = inputY; - for (auto &line : lines) { - int nextX = x; - for (const auto &token : line) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, yLine, rowHeight, token.second); - } else { - display->drawString(nextX, yLine, token.second); - nextX += display->getStringWidth(token.second); - } + if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) + strcpy(entryText, "?"); + + // Highlight background (if selected) + if (itemIndex == destIndex) { + int scrollPadding = 8; // Reserve space for scrollbar + display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); } - yLine += rowHeight; - } - } -#endif - return; - } - // Canned Messages List - if (this->messagesCount > 0) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // Precompute per-row heights based on emotes (centered if present) - const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; - - int topMsg; - std::vector rowHeights; - int _visibleRows; - - // Draw header (To: ...) - drawHeader(display, x, y, buffer); - - // Shift message list upward by 3 pixels to reduce spacing between header and first message - const int listYOffset = y + FONT_HEIGHT_SMALL - 3; - _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; - - // Figure out which messages are visible and their needed heights - topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) ? currentMessageIndex - _visibleRows + 2 : 0; - int countRows = std::min(messagesCount, _visibleRows); - - // Build per-row max height based on all emotes in line - for (int i = 0; i < countRows; i++) { - const char *msg = getMessageByIndex(topMsg + i); - int maxEmoteHeight = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *search = msg; - while ((search = strstr(search, label))) { - if (graphics::emotes[j].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[j].height; - search += strlen(label); // Advance past this emote - } - } - rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); - } - - // Draw all message rows with multi-emote support - int yCursor = listYOffset; - for (int vis = 0; vis < countRows; vis++) { - int msgIdx = topMsg + vis; - int lineY = yCursor; - const char *msg = getMessageByIndex(msgIdx); - int rowHeight = rowHeights[vis]; - bool _highlight = (msgIdx == currentMessageIndex); - - // Multi-emote tokenization - std::vector> tokens = tokenizeMessageWithEmotes(msg); - - // Vertically center based on rowHeight - int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; - -#ifdef USE_EINK - int nextX = x + (_highlight ? 12 : 0); - if (_highlight) - display->drawString(x + 0, lineY + textYOffset, ">"); -#else - int scrollPadding = 8; - if (_highlight) { - display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); - display->setColor(BLACK); - } - int nextX = x + (_highlight ? 2 : 0); -#endif - - // Draw all tokens left to right - for (const auto &token : tokens) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, lineY, rowHeight, token.second); - } else { - // Text - display->drawString(nextX, lineY + textYOffset, token.second); - nextX += display->getStringWidth(token.second); - } - } -#ifndef USE_EINK - if (_highlight) + // Draw entry text + display->drawString(xOffset + 2, yOffset, entryText); display->setColor(WHITE); -#endif - yCursor += rowHeight; + // Draw key icon (after highlight) + /* + if (itemIndex >= numActiveChannels) { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && hasKeyForNode(node)) { + int iconX = display->getWidth() - key_symbol_width - 15; + int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; + + if (itemIndex == destIndex) { + display->setColor(INVERSE); + } else { + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); + } + } + } + */ } // Scrollbar - if (messagesCount > _visibleRows) { - int scrollHeight = display->getHeight() - listYOffset; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); - int barHeight = (scrollHeight * _visibleRows) / messagesCount; - int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; - display->fillRect(scrollTrackX, scrollPos, 4, barHeight); + if (totalEntries > visibleRows) { + int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); + int totalScrollable = totalEntries; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); + int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; + int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; + display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); + } +} + +void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height + const int headerMargin = 2; // Extra pixels below header + const int labelGap = 6; + const int bitmapGapX = 4; + + // Find max emote height (assume all same, or precalculated) + int maxEmoteHeight = 0; + for (int i = 0; i < graphics::numEmotes; ++i) + if (graphics::emotes[i].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[i].height; + + const int rowHeight = maxEmoteHeight + 2; + + // Place header at top, then compute start of emote list + int headerY = y; + int listTop = headerY + headerFontHeight + headerMargin; + + int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; + int numEmotes = graphics::numEmotes; + + // keep member variable in sync + this->visibleRows = _visibleRows; + + // Clamp highlight index + if (emotePickerIndex < 0) + emotePickerIndex = 0; + if (emotePickerIndex >= numEmotes) + emotePickerIndex = numEmotes - 1; + + // Determine which emote is at the top + int topIndex = emotePickerIndex - _visibleRows / 2; + if (topIndex < 0) + topIndex = 0; + if (topIndex > numEmotes - _visibleRows) + topIndex = std::max(0, numEmotes - _visibleRows); + + // Draw header/title + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, headerY, "Select Emote"); + + // Draw emote rows + display->setTextAlignment(TEXT_ALIGN_LEFT); + + for (int vis = 0; vis < _visibleRows; ++vis) { + int emoteIdx = topIndex + vis; + if (emoteIdx >= numEmotes) + break; + const graphics::Emote &emote = graphics::emotes[emoteIdx]; + int rowY = listTop + vis * rowHeight; + + // Draw highlight box 2px taller than emote (1px margin above and below) + if (emoteIdx == emotePickerIndex) { + display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); + display->setColor(BLACK); + } + + // Emote bitmap (left), 1px margin from highlight bar top + int emoteY = rowY + 1; + display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + + // Emote label (right of bitmap) + display->setFont(FONT_MEDIUM); + int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); + display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + + if (emoteIdx == emotePickerIndex) + display->setColor(WHITE); + } + + // Draw scrollbar if needed + if (numEmotes > _visibleRows) { + int scrollbarHeight = _visibleRows * rowHeight; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); + int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); + int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; + display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); + } +} + +void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + this->displayHeight = display->getHeight(); // Store display height for later use + char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // Never draw if state is outside our UI modes + if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { + return; // bail if not in a UI state that should render + } + + // Emote Picker Screen + if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here + return; + } + + // Destination Selection + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + drawDestinationSelectionScreen(display, state, x, y); + return; + } + + // Disabled Screen + if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); + return; + } + + // Free Text Input Screen + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + requestFocus(); +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + EInkDynamicDisplay *einkDisplay = static_cast(display); + einkDisplay->enableUnlimitedFastMode(); +#endif +#if defined(USE_VIRTUAL_KEYBOARD) + drawKeyboard(display, state, 0, 0); +#else + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // Draw node/channel header at the top + drawHeader(display, x, y, buffer); + + // Char count right-aligned + if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + uint16_t charsLeft = + meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); + snprintf(buffer, sizeof(buffer), "%d left", charsLeft); + display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); + } + +#if INPUTBROKER_SERIAL_TYPE == 1 + // Chatter Modifier key mode label (right side) + { + uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0; + const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#"; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const int16_t th = FONT_HEIGHT_SMALL; + const int16_t tw = display->getStringWidth(label); + const int16_t padX = 3; + const int16_t padY = 2; + const int16_t r = 3; + + const int16_t bw = tw + padX * 2; + const int16_t bh = th + padY * 2; + + const int16_t bx = x + display->getWidth() - bw - 2; + const int16_t by = y + display->getHeight() - bh - 2; + + display->setColor(WHITE); + display->fillRect(bx + r, by, bw - r * 2, bh); + display->fillRect(bx, by + r, r, bh - r * 2); + display->fillRect(bx + bw - r, by + r, r, bh - r * 2); + display->fillCircle(bx + r, by + r, r); + display->fillCircle(bx + bw - r - 1, by + r, r); + display->fillCircle(bx + r, by + bh - r - 1, r); + display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); + + display->setColor(BLACK); + display->drawString(bx + padX, by + padY, label); + } + + // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + ◄”) + { + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const char *label = "Dest: Shift + "; + int16_t labelW = display->getStringWidth(label); + + // triangle size visually matches glyph height, not full line height + const int triH = FONT_HEIGHT_SMALL - 3; + const int triW = triH * 0.7; + + const int16_t padX = 3; + const int16_t padY = 2; + const int16_t r = 3; + + const int16_t bw = labelW + triW + padX * 2 + 2; + const int16_t bh = FONT_HEIGHT_SMALL + padY * 2; + + const int16_t bx = x + 2; + const int16_t by = y + display->getHeight() - bh - 2; + + // Rounded white box + display->setColor(WHITE); + display->fillRect(bx + r, by, bw - (r * 2), bh); + display->fillRect(bx, by + r, r, bh - (r * 2)); + display->fillRect(bx + bw - r, by + r, r, bh - (r * 2)); + display->fillCircle(bx + r, by + r, r); + display->fillCircle(bx + bw - r - 1, by + r, r); + display->fillCircle(bx + r, by + bh - r - 1, r); + display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); + + // Draw text + display->setColor(BLACK); + display->drawString(bx + padX, by + padY, label); + + // Perfectly center triangle on text baseline + int16_t tx = bx + padX + labelW; + int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering + + // ◄ Left-pointing triangle + display->fillTriangle(tx + triW, ty, // top-right + tx, ty + triH / 2, // left center + tx + triW, ty + triH // bottom-right + ); + } +#endif + // Draw Free Text input with multi-emote support and proper line wrapping + display->setColor(WHITE); + { + int inputY = 0 + y + FONT_HEIGHT_SMALL; + String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); + + // Tokenize input into (isEmote, token) pairs + const char *msg = msgWithCursor.c_str(); + std::vector> tokens = tokenizeMessageWithEmotes(msg); + + // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) + std::vector>> lines; + std::vector> currentLine; + int lineWidth = 0; + int maxWidth = display->getWidth(); + for (auto &token : tokens) { + if (token.first) { + // Emote + int tokenWidth = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + tokenWidth = graphics::emotes[j].width + 2; + break; + } + } + if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back(token); + lineWidth += tokenWidth; + } else { + // Text: split by words and wrap inside word if needed + String text = token.second; + int pos = 0; + while (pos < static_cast(text.length())) { + // Find next space (or end) + int spacePos = text.indexOf(' ', pos); + int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space + String word = text.substring(pos, endPos); + int wordWidth = display->getStringWidth(word); + + if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + // If word itself too big, split by character + if (wordWidth > maxWidth) { + uint16_t charPos = 0; + while (charPos < word.length()) { + String oneChar = word.substring(charPos, charPos + 1); + int charWidth = display->getStringWidth(oneChar); + if (lineWidth + charWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back({false, oneChar}); + lineWidth += charWidth; + charPos++; + } + } else { + currentLine.push_back({false, word}); + lineWidth += wordWidth; + } + pos = endPos; + } + } + } + if (!currentLine.empty()) + lines.push_back(currentLine); + + // Draw lines with emotes + int rowHeight = FONT_HEIGHT_SMALL; + int yLine = inputY; + for (auto &line : lines) { + int nextX = x; + for (const auto &token : line) { + if (token.first) { + // Emote rendering centralized in helper + renderEmote(display, nextX, yLine, rowHeight, token.second); + } else { + display->drawString(nextX, yLine, token.second); + nextX += display->getStringWidth(token.second); + } + } + yLine += rowHeight; + } + } +#endif + return; + } + + // Canned Messages List + if (this->messagesCount > 0) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // Precompute per-row heights based on emotes (centered if present) + const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; + + int topMsg; + std::vector rowHeights; + int _visibleRows; + + // Draw header (To: ...) + drawHeader(display, x, y, buffer); + + // Shift message list upward by 3 pixels to reduce spacing between header and first message + const int listYOffset = y + FONT_HEIGHT_SMALL - 3; + _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; + + // Figure out which messages are visible and their needed heights + topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) + ? currentMessageIndex - _visibleRows + 2 + : 0; + int countRows = std::min(messagesCount, _visibleRows); + + // Build per-row max height based on all emotes in line + for (int i = 0; i < countRows; i++) { + const char *msg = getMessageByIndex(topMsg + i); + int maxEmoteHeight = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *search = msg; + while ((search = strstr(search, label))) { + if (graphics::emotes[j].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[j].height; + search += strlen(label); // Advance past this emote + } + } + rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); + } + + // Draw all message rows with multi-emote support + int yCursor = listYOffset; + for (int vis = 0; vis < countRows; vis++) { + int msgIdx = topMsg + vis; + int lineY = yCursor; + const char *msg = getMessageByIndex(msgIdx); + int rowHeight = rowHeights[vis]; + bool _highlight = (msgIdx == currentMessageIndex); + + // Multi-emote tokenization + std::vector> tokens = tokenizeMessageWithEmotes(msg); + + // Vertically center based on rowHeight + int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; + +#ifdef USE_EINK + int nextX = x + (_highlight ? 12 : 0); + if (_highlight) + display->drawString(x + 0, lineY + textYOffset, ">"); +#else + int scrollPadding = 8; + if (_highlight) { + display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); + display->setColor(BLACK); + } + int nextX = x + (_highlight ? 2 : 0); +#endif + + // Draw all tokens left to right + for (const auto &token : tokens) { + if (token.first) { + // Emote rendering centralized in helper + renderEmote(display, nextX, lineY, rowHeight, token.second); + } else { + // Text + display->drawString(nextX, lineY + textYOffset, token.second); + nextX += display->getStringWidth(token.second); + } + } +#ifndef USE_EINK + if (_highlight) + display->setColor(WHITE); +#endif + + yCursor += rowHeight; + } + + // Scrollbar + if (messagesCount > _visibleRows) { + int scrollHeight = display->getHeight() - listYOffset; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); + int barHeight = (scrollHeight * _visibleRows) / messagesCount; + int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; + display->fillRect(scrollTrackX, scrollPos, 4, barHeight); + } } - } } // Return SNR limit based on modem preset -static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) { - switch (preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: - return -6.0f; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - return -5.5f; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - return -4.5f; - default: - return -6.0f; - } +static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return -6.0f; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return -5.5f; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return -4.5f; + default: + return -6.0f; + } } // Return Good/Fair/Bad label and set 1–5 bars based on SNR and RSSI -static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) { - // 5-bar logic: strength inside Good/Fair/Bad category - if (snr > snrLimit && rssi > -10) { - bars = 5; // very strong good - return "Good"; - } else if (snr > snrLimit && rssi > -20) { - bars = 4; // normal good - return "Good"; - } else if (snr > 0 && rssi > -50) { - bars = 3; // weaker good (on edge of fair) - return "Good"; - } else if (snr > -10 && rssi > -100) { - bars = 2; // fair - return "Fair"; - } else { - bars = 1; // bad - return "Bad"; - } -} - -ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { - // Only process routing ACK/NACK packets that are responses to our own outbound - if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && - mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet - { - if (mp.decoded.request_id != 0) { - // Decode the routing response - meshtastic_Routing decoded = meshtastic_Routing_init_default; - pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - - // Determine ACK/NACK status - bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); - bool isFromDest = (mp.from == this->lastSentNode); - bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); - - // Identify the responding node - if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { - this->incoming = mp.from; // relayed by another node - } else { - this->incoming = this->lastSentNode; // direct reply - } - - // Final ACK/NACK logic - if (wasBroadcast) { - // Any ACK counts for broadcast - this->ack = isAck; - waitingForAck = false; - } else if (isFromDest) { - // Only ACK from destination counts as final - this->ack = isAck; - waitingForAck = false; - } else if (isAck) { - // Relay ACK → mark as RELAYED, still no final ACK - this->ack = false; - waitingForAck = false; - } else { - // Explicit failure - this->ack = false; - waitingForAck = false; - } - - // Update last sent StoredMessage with ACK/NACK/RELAYED result - if (!messageStore.getMessages().empty()) { - StoredMessage &last = const_cast(messageStore.getMessages().back()); - if (last.sender == nodeDB->getNodeNum()) { // only update our own messages - if (wasBroadcast && isAck) { - last.ackStatus = AckStatus::ACKED; - } else if (isFromDest && isAck) { - last.ackStatus = AckStatus::ACKED; - } else if (!isFromDest && isAck) { - last.ackStatus = AckStatus::RELAYED; - } else { - last.ackStatus = AckStatus::NACKED; - } - } - } - - // Capture radio metrics - this->lastRxRssi = mp.rx_rssi; - this->lastRxSnr = mp.rx_snr; - - // Show overlay banner - if (screen) { - auto *display = screen->getDisplayDevice(); - graphics::BannerOverlayOptions opts; - static char buf[128]; - - const char *channelName = channels.getName(this->channel); - const char *src = getNodeName(this->incoming); - char nodeName[48]; - strncpy(nodeName, src, sizeof(nodeName) - 1); - nodeName[sizeof(nodeName) - 1] = '\0'; - - int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); - if (availWidth < 0) - availWidth = 0; - - size_t origLen = strlen(nodeName); - while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { - nodeName[strlen(nodeName) - 1] = '\0'; - } - if (strlen(nodeName) < origLen) { - strcat(nodeName, "..."); - } - - // Calculate signal quality and bars based on preset, SNR, and RSSI - float snrLimit = getSnrLimit(config.lora.modem_preset); - int bars = 0; - const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); - - if (this->ack) { - if (this->lastSentNode == NODENUM_BROADCAST) { - snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); - } else { - snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); - } - } else if (isAck && !isFromDest) { - // Relay ACK banner - snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", (nodeName && nodeName[0]) ? nodeName : "unknown", - qualityLabel); - } else { - if (this->lastSentNode == NODENUM_BROADCAST) { - snprintf(buf, sizeof(buf), "Message failed to\n#%s", (channelName && channelName[0]) ? channelName : "unknown"); - } else { - snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); - } - } - - opts.message = buf; - opts.durationMs = 3000; - graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw - screen->showOverlayBanner(opts); // this triggers drawNotificationBox() - } +static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) +{ + // 5-bar logic: strength inside Good/Fair/Bad category + if (snr > snrLimit && rssi > -10) { + bars = 5; // very strong good + return "Good"; + } else if (snr > snrLimit && rssi > -20) { + bars = 4; // normal good + return "Good"; + } else if (snr > 0 && rssi > -50) { + bars = 3; // weaker good (on edge of fair) + return "Good"; + } else if (snr > -10 && rssi > -100) { + bars = 2; // fair + return "Fair"; + } else { + bars = 1; // bad + return "Bad"; } - } - - return ProcessMessage::CONTINUE; } -void CannedMessageModule::loadProtoForModule() { - if (nodeDB->loadProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, sizeof(meshtastic_CannedMessageModuleConfig), - &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig) != LoadFileResult::LOAD_SUCCESS) { - installDefaultCannedMessageModuleConfig(); - } +ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + // Only process routing ACK/NACK packets that are responses to our own outbound + if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && + mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet + { + if (mp.decoded.request_id != 0) { + // Decode the routing response + meshtastic_Routing decoded = meshtastic_Routing_init_default; + pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); + + // Determine ACK/NACK status + bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); + bool isFromDest = (mp.from == this->lastSentNode); + bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + + // Identify the responding node + if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { + this->incoming = mp.from; // relayed by another node + } else { + this->incoming = this->lastSentNode; // direct reply + } + + // Final ACK/NACK logic + if (wasBroadcast) { + // Any ACK counts for broadcast + this->ack = isAck; + waitingForAck = false; + } else if (isFromDest) { + // Only ACK from destination counts as final + this->ack = isAck; + waitingForAck = false; + } else if (isAck) { + // Relay ACK → mark as RELAYED, still no final ACK + this->ack = false; + waitingForAck = false; + } else { + // Explicit failure + this->ack = false; + waitingForAck = false; + } + + // Update last sent StoredMessage with ACK/NACK/RELAYED result + if (!messageStore.getMessages().empty()) { + StoredMessage &last = const_cast(messageStore.getMessages().back()); + if (last.sender == nodeDB->getNodeNum()) { // only update our own messages + if (wasBroadcast && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (isFromDest && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (!isFromDest && isAck) { + last.ackStatus = AckStatus::RELAYED; + } else { + last.ackStatus = AckStatus::NACKED; + } + } + } + + // Capture radio metrics + this->lastRxRssi = mp.rx_rssi; + this->lastRxSnr = mp.rx_snr; + + // Show overlay banner + if (screen) { + auto *display = screen->getDisplayDevice(); + graphics::BannerOverlayOptions opts; + static char buf[128]; + + const char *channelName = channels.getName(this->channel); + const char *src = getNodeName(this->incoming); + char nodeName[48]; + strncpy(nodeName, src, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; + + int availWidth = + display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(nodeName); + while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { + nodeName[strlen(nodeName) - 1] = '\0'; + } + if (strlen(nodeName) < origLen) { + strcat(nodeName, "..."); + } + + // Calculate signal quality and bars based on preset, SNR, and RSSI + float snrLimit = getSnrLimit(config.lora.modem_preset); + int bars = 0; + const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); + + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", + (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); + } else { + snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", + (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } + } else if (isAck && !isFromDest) { + // Relay ACK banner + snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", + (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } else { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message failed to\n#%s", + (channelName && channelName[0]) ? channelName : "unknown"); + } else { + snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); + } + } + + opts.message = buf; + opts.durationMs = 3000; + graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw + screen->showOverlayBanner(opts); // this triggers drawNotificationBox() + } + } + } + + return ProcessMessage::CONTINUE; +} + +void CannedMessageModule::loadProtoForModule() +{ + if (nodeDB->loadProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + sizeof(meshtastic_CannedMessageModuleConfig), &meshtastic_CannedMessageModuleConfig_msg, + &cannedMessageModuleConfig) != LoadFileResult::LOAD_SUCCESS) { + installDefaultCannedMessageModuleConfig(); + } } /** * @brief Save the module config to file. @@ -2289,26 +2371,28 @@ void CannedMessageModule::loadProtoForModule() { * @return true On success. * @return false On error. */ -bool CannedMessageModule::saveProtoForModule() { - bool okay = true; +bool CannedMessageModule::saveProtoForModule() +{ + bool okay = true; #ifdef FSCom - spiLock->lock(); - FSCom.mkdir("/prefs"); - spiLock->unlock(); + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); #endif - okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg, - &cannedMessageModuleConfig); + okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); - return okay; + return okay; } /** * @brief Fill configuration with default values. */ -void CannedMessageModule::installDefaultCannedMessageModuleConfig() { - strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); +void CannedMessageModule::installDefaultCannedMessageModuleConfig() +{ + strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); } /** @@ -2320,59 +2404,65 @@ void CannedMessageModule::installDefaultCannedMessageModuleConfig() { * @return AdminMessageHandleResult HANDLED if message was handled * HANDLED_WITH_RESULT if a result is also prepared. */ -AdminMessageHandleResult CannedMessageModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - AdminMessageHandleResult result; +AdminMessageHandleResult CannedMessageModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: - LOG_DEBUG("Client getting radio canned messages"); - this->handleGetCannedMessageModuleMessages(mp, response); - result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; - break; + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + LOG_DEBUG("Client getting radio canned messages"); + this->handleGetCannedMessageModuleMessages(mp, response); + result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; - case meshtastic_AdminMessage_set_canned_message_module_messages_tag: - LOG_DEBUG("Client getting radio canned messages"); - this->handleSetCannedMessageModuleMessages(request->set_canned_message_module_messages); - result = AdminMessageHandleResult::HANDLED; - break; + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + LOG_DEBUG("Client getting radio canned messages"); + this->handleSetCannedMessageModuleMessages(request->set_canned_message_module_messages); + result = AdminMessageHandleResult::HANDLED; + break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; - } - - return result; -} - -void CannedMessageModule::handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) { - LOG_DEBUG("*** handleGetCannedMessageModuleMessages"); - if (req.decoded.want_response) { - response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; - strncpy(response->get_canned_message_module_messages_response, cannedMessageModuleConfig.messages, - sizeof(response->get_canned_message_module_messages_response)); - } // Don't send anything if not instructed to. Better than asserting. -} - -void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_msg) { - int changed = 0; - - if (*from_msg) { - changed |= strcmp(cannedMessageModuleConfig.messages, from_msg); - strncpy(cannedMessageModuleConfig.messages, from_msg, sizeof(cannedMessageModuleConfig.messages)); - LOG_DEBUG("*** from_msg.text:%s", from_msg); - } - - if (changed) { - this->saveProtoForModule(); - if (splitConfiguredMessages()) { - moduleConfig.canned_message.enabled = true; + default: + result = AdminMessageHandleResult::NOT_HANDLED; } - } + + return result; } -String CannedMessageModule::drawWithCursor(String text, int cursor) { - String result = text.substring(0, cursor) + "_" + text.substring(cursor); - return result; +void CannedMessageModule::handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, + meshtastic_AdminMessage *response) +{ + LOG_DEBUG("*** handleGetCannedMessageModuleMessages"); + if (req.decoded.want_response) { + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, cannedMessageModuleConfig.messages, + sizeof(response->get_canned_message_module_messages_response)); + } // Don't send anything if not instructed to. Better than asserting. +} + +void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_msg) +{ + int changed = 0; + + if (*from_msg) { + changed |= strcmp(cannedMessageModuleConfig.messages, from_msg); + strncpy(cannedMessageModuleConfig.messages, from_msg, sizeof(cannedMessageModuleConfig.messages)); + LOG_DEBUG("*** from_msg.text:%s", from_msg); + } + + if (changed) { + this->saveProtoForModule(); + if (splitConfiguredMessages()) { + moduleConfig.canned_message.enabled = true; + } + } +} + +String CannedMessageModule::drawWithCursor(String text, int cursor) +{ + String result = text.substring(0, cursor) + "_" + text.substring(cursor); + return result; } #endif diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index c25bc31d7..3d7c09d87 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -8,18 +8,18 @@ // ============================ enum cannedMessageModuleRunState { - CANNED_MESSAGE_RUN_STATE_DISABLED, - CANNED_MESSAGE_RUN_STATE_INACTIVE, - CANNED_MESSAGE_RUN_STATE_ACTIVE, - CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, - CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, - CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, - CANNED_MESSAGE_RUN_STATE_ACTION_UP, - CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, - CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, - CANNED_MESSAGE_RUN_STATE_FREETEXT, - CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, - CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER + CANNED_MESSAGE_RUN_STATE_DISABLED, + CANNED_MESSAGE_RUN_STATE_INACTIVE, + CANNED_MESSAGE_RUN_STATE_ACTIVE, + CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, + CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, + CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, + CANNED_MESSAGE_RUN_STATE_ACTION_UP, + CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, + CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, + CANNED_MESSAGE_RUN_STATE_FREETEXT, + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, + CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; @@ -36,226 +36,233 @@ enum CannedMessageModuleIconType { shift, backspace, space, enter }; // ============================ struct Letter { - String character; - float width; - int rectX; - int rectY; - int rectWidth; - int rectHeight; + String character; + float width; + int rectX; + int rectY; + int rectWidth; + int rectHeight; }; struct NodeEntry { - meshtastic_NodeInfoLite *node; - uint32_t lastHeard; + meshtastic_NodeInfoLite *node; + uint32_t lastHeard; }; // ============================ // Main Class // ============================ -class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread { -public: - CannedMessageModule(); +class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread +{ + public: + CannedMessageModule(); - void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); - void LaunchRepeatDestination(); - void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); + void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); + void LaunchRepeatDestination(); + void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); - // === Emote Picker navigation === - int emotePickerIndex = 0; // Tracks currently selected emote in the picker + // === Emote Picker navigation === + int emotePickerIndex = 0; // Tracks currently selected emote in the picker - // === Message navigation === - const char *getCurrentMessage(); - const char *getPrevMessage(); - const char *getNextMessage(); - const char *getMessageByIndex(int index); - const char *getNodeName(NodeNum node); + // === Message navigation === + const char *getCurrentMessage(); + const char *getPrevMessage(); + const char *getNextMessage(); + const char *getMessageByIndex(int index); + const char *getNodeName(NodeNum node); - // === State/UI === - bool shouldDraw(); - bool hasMessages(); - void resetSearch(); - void updateDestinationSelectionList(); - void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - bool isCharInputAllowed() const; - String drawWithCursor(String text, int cursor); + // === State/UI === + bool shouldDraw(); + bool hasMessages(); + void resetSearch(); + void updateDestinationSelectionList(); + void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + bool isCharInputAllowed() const; + String drawWithCursor(String text, int cursor); - // === Emote Picker === - int handleEmotePickerInput(const InputEvent *event); - void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // === Emote Picker === + int handleEmotePickerInput(const InputEvent *event); + void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - // === Admin Handlers === - void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); - void handleSetCannedMessageModuleMessages(const char *from_msg); + // === Admin Handlers === + void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); + void handleSetCannedMessageModuleMessages(const char *from_msg); #ifdef RAK14014 - cannedMessageModuleRunState getRunState() const { return runState; } + cannedMessageModuleRunState getRunState() const { return runState; } #endif - // === Packet Interest Filter === - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { - if (p->rx_rssi != 0) - lastRxRssi = p->rx_rssi; - if (p->rx_snr > 0) - lastRxSnr = p->rx_snr; - return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; - } + // === Packet Interest Filter === + virtual bool wantPacket(const meshtastic_MeshPacket *p) override + { + if (p->rx_rssi != 0) + lastRxRssi = p->rx_rssi; + if (p->rx_snr > 0) + lastRxSnr = p->rx_snr; + return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; + } -protected: - // === Thread Entry Point === - virtual int32_t runOnce() override; + protected: + // === Thread Entry Point === + virtual int32_t runOnce() override; - // === Transmission === - void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); - void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); - int splitConfiguredMessages(); - int getNextIndex(); - int getPrevIndex(); + // === Transmission === + void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); + void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); + int splitConfiguredMessages(); + int getNextIndex(); + int getPrevIndex(); #if defined(USE_VIRTUAL_KEYBOARD) - void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - String keyForCoordinates(uint x, uint y); - void drawShiftIcon(OLEDDisplay *display, int x, int y, float scale = 1); - void drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale = 1); - void drawEnterIcon(OLEDDisplay *display, int x, int y, float scale = 1); + void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + String keyForCoordinates(uint x, uint y); + void drawShiftIcon(OLEDDisplay *display, int x, int y, float scale = 1); + void drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale = 1); + void drawEnterIcon(OLEDDisplay *display, int x, int y, float scale = 1); #endif - // === Input Handling === - int handleInputEvent(const InputEvent *event); - virtual bool wantUIFrame() override { return shouldDraw(); } - virtual Observable *getUIFrameObservable() override { return this; } - virtual bool interceptingKeyboardInput() override; - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + // === Input Handling === + int handleInputEvent(const InputEvent *event); + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } + virtual bool interceptingKeyboardInput() override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - void loadProtoForModule(); - bool saveProtoForModule(); - void installDefaultCannedMessageModuleConfig(); + void loadProtoForModule(); + bool saveProtoForModule(); + void installDefaultCannedMessageModuleConfig(); -private: - // === Input Observers === - CallbackObserver inputObserver = - CallbackObserver(this, &CannedMessageModule::handleInputEvent); + private: + // === Input Observers === + CallbackObserver inputObserver = + CallbackObserver(this, &CannedMessageModule::handleInputEvent); - // === Display and UI === - int displayHeight = 64; - int destIndex = 0; - int scrollIndex = 0; - int visibleRows = 0; - bool needsUpdate = true; - unsigned long lastUpdateMillis = 0; - String searchQuery; - String freetext; + // === Display and UI === + int displayHeight = 64; + int destIndex = 0; + int scrollIndex = 0; + int visibleRows = 0; + bool needsUpdate = true; + unsigned long lastUpdateMillis = 0; + String searchQuery; + String freetext; - // === Message Storage === - char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; - char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; - int messagesCount = 0; - int currentMessageIndex = -1; + // === Message Storage === + char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; + char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; + int messagesCount = 0; + int currentMessageIndex = -1; - // === Routing & Acknowledgment === - NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) - NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received - NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) - ChannelIndex channel = 0; // Channel index used when sending a message + // === Routing & Acknowledgment === + NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) + NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received + NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) + ChannelIndex channel = 0; // Channel index used when sending a message - bool ack = false; // True = ACK received, False = NACK or failed - bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets - float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) - int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) - uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet + bool ack = false; // True = ACK received, False = NACK or failed + bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets + float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) + int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) + uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet - // === State Tracking === - cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - char highlight = 0x00; - char payload = 0x00; - unsigned int cursor = 0; - unsigned long lastTouchMillis = 0; - uint32_t lastFilterUpdate = 0; - static constexpr uint32_t filterDebounceMs = 30; - std::vector activeChannelIndices; - std::vector filteredNodes; + // === State Tracking === + cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + char highlight = 0x00; + char payload = 0x00; + unsigned int cursor = 0; + unsigned long lastTouchMillis = 0; + uint32_t lastFilterUpdate = 0; + static constexpr uint32_t filterDebounceMs = 30; + std::vector activeChannelIndices; + std::vector filteredNodes; #if defined(USE_VIRTUAL_KEYBOARD) - bool shift = false; - int charSet = 0; // 0=ABC, 1=123 + bool shift = false; + int charSet = 0; // 0=ABC, 1=123 #endif - bool isUpEvent(const InputEvent *event); - bool isDownEvent(const InputEvent *event); - bool isSelectEvent(const InputEvent *event); - bool handleTabSwitch(const InputEvent *event); - int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); - bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); - bool handleFreeTextInput(const InputEvent *event); + bool isUpEvent(const InputEvent *event); + bool isDownEvent(const InputEvent *event); + bool isSelectEvent(const InputEvent *event); + bool handleTabSwitch(const InputEvent *event); + int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleFreeTextInput(const InputEvent *event); #if defined(USE_VIRTUAL_KEYBOARD) - Letter keyboard[2][4][10] = { - {{{"Q", 20, 0, 0, 0, 0}, - {"W", 22, 0, 0, 0, 0}, - {"E", 17, 0, 0, 0, 0}, - {"R", 16.5, 0, 0, 0, 0}, - {"T", 14, 0, 0, 0, 0}, - {"Y", 15, 0, 0, 0, 0}, - {"U", 16.5, 0, 0, 0, 0}, - {"I", 5, 0, 0, 0, 0}, - {"O", 19.5, 0, 0, 0, 0}, - {"P", 15.5, 0, 0, 0, 0}}, - {{"A", 14, 0, 0, 0, 0}, - {"S", 15, 0, 0, 0, 0}, - {"D", 16.5, 0, 0, 0, 0}, - {"F", 15, 0, 0, 0, 0}, - {"G", 17, 0, 0, 0, 0}, - {"H", 15.5, 0, 0, 0, 0}, - {"J", 12, 0, 0, 0, 0}, - {"K", 15.5, 0, 0, 0, 0}, - {"L", 14, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}}, - {{"⇧", 20, 0, 0, 0, 0}, - {"Z", 14, 0, 0, 0, 0}, - {"X", 14.5, 0, 0, 0, 0}, - {"C", 15.5, 0, 0, 0, 0}, - {"V", 13.5, 0, 0, 0, 0}, - {"B", 15, 0, 0, 0, 0}, - {"N", 15, 0, 0, 0, 0}, - {"M", 17, 0, 0, 0, 0}, - {"⌫", 20, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}}, - {{"123", 42, 0, 0, 0, 0}, - {" ", 64, 0, 0, 0, 0}, - {"↵", 36, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}, - {"", 0, 0, 0, 0, 0}}}, - {{{"1", 12, 0, 0, 0, 0}, - {"2", 13.5, 0, 0, 0, 0}, - {"3", 12.5, 0, 0, 0, 0}, - {"4", 14, 0, 0, 0, 0}, - {"5", 14, 0, 0, 0, 0}, - {"6", 14, 0, 0, 0, 0}, - {"7", 13.5, 0, 0, 0, 0}, - {"8", 14, 0, 0, 0, 0}, - {"9", 14, 0, 0, 0, 0}, - {"0", 14, 0, 0, 0, 0}}, - {{"-", 8, 0, 0, 0, 0}, - {"/", 8, 0, 0, 0, 0}, - {":", 4.5, 0, 0, 0, 0}, - {";", 4.5, 0, 0, 0, 0}, - {"(", 7, 0, 0, 0, 0}, - {")", 6.5, 0, 0, 0, 0}, - {"$", 12.5, 0, 0, 0, 0}, - {"&", 15, 0, 0, 0, 0}, - {"@", 21.5, 0, 0, 0, 0}, - {"\"", 8, 0, 0, 0, 0}}, - {{".", 8, 0, 0, 0, 0}, {",", 8, 0, 0, 0, 0}, {"?", 10, 0, 0, 0, 0}, {"!", 10, 0, 0, 0, 0}, {"'", 10, 0, 0, 0, 0}, {"⌫", 20, 0, 0, 0, 0}}, - {{"ABC", 50, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}}}}; + Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, + {"W", 22, 0, 0, 0, 0}, + {"E", 17, 0, 0, 0, 0}, + {"R", 16.5, 0, 0, 0, 0}, + {"T", 14, 0, 0, 0, 0}, + {"Y", 15, 0, 0, 0, 0}, + {"U", 16.5, 0, 0, 0, 0}, + {"I", 5, 0, 0, 0, 0}, + {"O", 19.5, 0, 0, 0, 0}, + {"P", 15.5, 0, 0, 0, 0}}, + {{"A", 14, 0, 0, 0, 0}, + {"S", 15, 0, 0, 0, 0}, + {"D", 16.5, 0, 0, 0, 0}, + {"F", 15, 0, 0, 0, 0}, + {"G", 17, 0, 0, 0, 0}, + {"H", 15.5, 0, 0, 0, 0}, + {"J", 12, 0, 0, 0, 0}, + {"K", 15.5, 0, 0, 0, 0}, + {"L", 14, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}}, + {{"⇧", 20, 0, 0, 0, 0}, + {"Z", 14, 0, 0, 0, 0}, + {"X", 14.5, 0, 0, 0, 0}, + {"C", 15.5, 0, 0, 0, 0}, + {"V", 13.5, 0, 0, 0, 0}, + {"B", 15, 0, 0, 0, 0}, + {"N", 15, 0, 0, 0, 0}, + {"M", 17, 0, 0, 0, 0}, + {"⌫", 20, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}}, + {{"123", 42, 0, 0, 0, 0}, + {" ", 64, 0, 0, 0, 0}, + {"↵", 36, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}, + {"", 0, 0, 0, 0, 0}}}, + {{{"1", 12, 0, 0, 0, 0}, + {"2", 13.5, 0, 0, 0, 0}, + {"3", 12.5, 0, 0, 0, 0}, + {"4", 14, 0, 0, 0, 0}, + {"5", 14, 0, 0, 0, 0}, + {"6", 14, 0, 0, 0, 0}, + {"7", 13.5, 0, 0, 0, 0}, + {"8", 14, 0, 0, 0, 0}, + {"9", 14, 0, 0, 0, 0}, + {"0", 14, 0, 0, 0, 0}}, + {{"-", 8, 0, 0, 0, 0}, + {"/", 8, 0, 0, 0, 0}, + {":", 4.5, 0, 0, 0, 0}, + {";", 4.5, 0, 0, 0, 0}, + {"(", 7, 0, 0, 0, 0}, + {")", 6.5, 0, 0, 0, 0}, + {"$", 12.5, 0, 0, 0, 0}, + {"&", 15, 0, 0, 0, 0}, + {"@", 21.5, 0, 0, 0, 0}, + {"\"", 8, 0, 0, 0, 0}}, + {{".", 8, 0, 0, 0, 0}, + {",", 8, 0, 0, 0, 0}, + {"?", 10, 0, 0, 0, 0}, + {"!", 10, 0, 0, 0, 0}, + {"'", 10, 0, 0, 0, 0}, + {"⌫", 20, 0, 0, 0, 0}}, + {{"ABC", 50, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}}}}; #endif }; diff --git a/src/modules/DetectionSensorModule.cpp b/src/modules/DetectionSensorModule.cpp index 32d214dc9..ca682b772 100644 --- a/src/modules/DetectionSensorModule.cpp +++ b/src/modules/DetectionSensorModule.cpp @@ -12,26 +12,29 @@ DetectionSensorModule *detectionSensorModule; #define DELAYED_INTERVAL 1000 typedef enum { - DetectionSensorVerdictDetected, - DetectionSensorVerdictSendState, - DetectionSensorVerdictNoop, + DetectionSensorVerdictDetected, + DetectionSensorVerdictSendState, + DetectionSensorVerdictNoop, } DetectionSensorTriggerVerdict; typedef DetectionSensorTriggerVerdict (*DetectionSensorTriggerHandler)(bool prev, bool current); -static DetectionSensorTriggerVerdict detection_trigger_logic_level(bool prev, bool current) { - return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; +static DetectionSensorTriggerVerdict detection_trigger_logic_level(bool prev, bool current) +{ + return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; } -static DetectionSensorTriggerVerdict detection_trigger_single_edge(bool prev, bool current) { - return (!prev && current) ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; +static DetectionSensorTriggerVerdict detection_trigger_single_edge(bool prev, bool current) +{ + return (!prev && current) ? DetectionSensorVerdictDetected : DetectionSensorVerdictNoop; } -static DetectionSensorTriggerVerdict detection_trigger_either_edge(bool prev, bool current) { - if (prev == current) { - return DetectionSensorVerdictNoop; - } - return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictSendState; +static DetectionSensorTriggerVerdict detection_trigger_either_edge(bool prev, bool current) +{ + if (prev == current) { + return DetectionSensorVerdictNoop; + } + return current ? DetectionSensorVerdictDetected : DetectionSensorVerdictSendState; } const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MAX + 1] = { @@ -43,113 +46,119 @@ const static DetectionSensorTriggerHandler handlers[_meshtastic_ModuleConfig_Det [meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH] = detection_trigger_either_edge, }; -int32_t DetectionSensorModule::runOnce() { - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ - // moduleConfig.detection_sensor.enabled = true; - // moduleConfig.detection_sensor.monitor_pin = 10; // WisBlock PIR IO6 - // moduleConfig.detection_sensor.monitor_pin = 21; // WisBlock RAK12013 Radar IO6 - // moduleConfig.detection_sensor.minimum_broadcast_secs = 30; - // moduleConfig.detection_sensor.state_broadcast_secs = 120; - // moduleConfig.detection_sensor.detection_trigger_type = - // meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; - // strcpy(moduleConfig.detection_sensor.name, "Motion"); +int32_t DetectionSensorModule::runOnce() +{ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ + // moduleConfig.detection_sensor.enabled = true; + // moduleConfig.detection_sensor.monitor_pin = 10; // WisBlock PIR IO6 + // moduleConfig.detection_sensor.monitor_pin = 21; // WisBlock RAK12013 Radar IO6 + // moduleConfig.detection_sensor.minimum_broadcast_secs = 30; + // moduleConfig.detection_sensor.state_broadcast_secs = 120; + // moduleConfig.detection_sensor.detection_trigger_type = + // meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; + // strcpy(moduleConfig.detection_sensor.name, "Motion"); - if (moduleConfig.detection_sensor.enabled == false) - return disable(); + if (moduleConfig.detection_sensor.enabled == false) + return disable(); - if (firstTime) { + if (firstTime) { #ifdef DETECTION_SENSOR_EN - pinMode(DETECTION_SENSOR_EN, OUTPUT); - digitalWrite(DETECTION_SENSOR_EN, HIGH); + pinMode(DETECTION_SENSOR_EN, OUTPUT); + digitalWrite(DETECTION_SENSOR_EN, HIGH); #endif - // This is the first time the OSThread library has called this function, so do some setup - firstTime = false; - if (moduleConfig.detection_sensor.monitor_pin > 0) { - pinMode(moduleConfig.detection_sensor.monitor_pin, moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT); - } else { - LOG_WARN("Detection Sensor Module: Set to enabled but no monitor pin is set. Disable module"); - return disable(); + // This is the first time the OSThread library has called this function, so do some setup + firstTime = false; + if (moduleConfig.detection_sensor.monitor_pin > 0) { + pinMode(moduleConfig.detection_sensor.monitor_pin, moduleConfig.detection_sensor.use_pullup ? INPUT_PULLUP : INPUT); + } else { + LOG_WARN("Detection Sensor Module: Set to enabled but no monitor pin is set. Disable module"); + return disable(); + } + LOG_INFO("Detection Sensor Module: init"); + + return setStartDelay(); } - LOG_INFO("Detection Sensor Module: init"); - return setStartDelay(); - } + // LOG_DEBUG("Detection Sensor Module: Current pin state: %i", digitalRead(moduleConfig.detection_sensor.monitor_pin)); - // LOG_DEBUG("Detection Sensor Module: Current pin state: %i", - // digitalRead(moduleConfig.detection_sensor.monitor_pin)); - - if (!Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { - bool isDetected = hasDetectionEvent(); - DetectionSensorTriggerVerdict verdict = handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected); - wasDetected = isDetected; - switch (verdict) { - case DetectionSensorVerdictDetected: - sendDetectionMessage(); - return DELAYED_INTERVAL; - case DetectionSensorVerdictSendState: - sendCurrentStateMessage(isDetected); - return DELAYED_INTERVAL; - case DetectionSensorVerdictNoop: - break; + if (!Throttle::isWithinTimespanMs(lastSentToMesh, + Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.minimum_broadcast_secs))) { + bool isDetected = hasDetectionEvent(); + DetectionSensorTriggerVerdict verdict = + handlers[moduleConfig.detection_sensor.detection_trigger_type](wasDetected, isDetected); + wasDetected = isDetected; + switch (verdict) { + case DetectionSensorVerdictDetected: + sendDetectionMessage(); + return DELAYED_INTERVAL; + case DetectionSensorVerdictSendState: + sendCurrentStateMessage(isDetected); + return DELAYED_INTERVAL; + case DetectionSensorVerdictNoop: + break; + } } - } - // Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort - // of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only - // broadcast state change detections. - if (moduleConfig.detection_sensor.state_broadcast_secs > 0 && - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs, - default_telemetry_broadcast_interval_secs))) { - sendCurrentStateMessage(hasDetectionEvent()); - return DELAYED_INTERVAL; - } - return GPIO_POLLING_INTERVAL; + // Even if we haven't detected an event, broadcast our current state to the mesh on the scheduled interval as a sort + // of heartbeat. We only do this if the minimum broadcast interval is greater than zero, otherwise we'll only broadcast state + // change detections. + if (moduleConfig.detection_sensor.state_broadcast_secs > 0 && + !Throttle::isWithinTimespanMs(lastSentToMesh, + Default::getConfiguredOrDefaultMs(moduleConfig.detection_sensor.state_broadcast_secs, + default_telemetry_broadcast_interval_secs))) { + sendCurrentStateMessage(hasDetectionEvent()); + return DELAYED_INTERVAL; + } + return GPIO_POLLING_INTERVAL; } -void DetectionSensorModule::sendDetectionMessage() { - LOG_DEBUG("Detected event observed. Send message"); - char *message = new char[40]; - sprintf(message, "%s detected", moduleConfig.detection_sensor.name); - meshtastic_MeshPacket *p = allocDataPacket(); - p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character - p->decoded.payload.size++; - } - lastSentToMesh = millis(); - if (!channels.isDefaultChannel(0)) { - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh(p); - } else - LOG_ERROR("Message not allow on Public channel"); - delete[] message; +void DetectionSensorModule::sendDetectionMessage() +{ + LOG_DEBUG("Detected event observed. Send message"); + char *message = new char[40]; + sprintf(message, "%s detected", moduleConfig.detection_sensor.name); + meshtastic_MeshPacket *p = allocDataPacket(); + p->want_ack = false; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character + p->decoded.payload.size++; + } + lastSentToMesh = millis(); + if (!channels.isDefaultChannel(0)) { + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + service->sendToMesh(p); + } else + LOG_ERROR("Message not allow on Public channel"); + delete[] message; } -void DetectionSensorModule::sendCurrentStateMessage(bool state) { - char *message = new char[40]; - sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state); - meshtastic_MeshPacket *p = allocDataPacket(); - p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - lastSentToMesh = millis(); - if (!channels.isDefaultChannel(0)) { - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh(p); - } else - LOG_ERROR("Message not allow on Public channel"); - delete[] message; +void DetectionSensorModule::sendCurrentStateMessage(bool state) +{ + char *message = new char[40]; + sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state); + meshtastic_MeshPacket *p = allocDataPacket(); + p->want_ack = false; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + lastSentToMesh = millis(); + if (!channels.isDefaultChannel(0)) { + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + service->sendToMesh(p); + } else + LOG_ERROR("Message not allow on Public channel"); + delete[] message; } -bool DetectionSensorModule::hasDetectionEvent() { - bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin); - // LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState); - return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState; +bool DetectionSensorModule::hasDetectionEvent() +{ + bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin); + // LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState); + return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState; } \ No newline at end of file diff --git a/src/modules/DetectionSensorModule.h b/src/modules/DetectionSensorModule.h index d288ca1f4..3ba10d329 100644 --- a/src/modules/DetectionSensorModule.h +++ b/src/modules/DetectionSensorModule.h @@ -1,20 +1,23 @@ #pragma once #include "SinglePortModule.h" -class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread { -public: - DetectionSensorModule() : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensor") {} +class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread +{ + public: + DetectionSensorModule() : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensor") + { + } -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; -private: - bool firstTime = true; - uint32_t lastSentToMesh = 0; - bool wasDetected = false; - void sendDetectionMessage(); - void sendCurrentStateMessage(bool state); - bool hasDetectionEvent(); + private: + bool firstTime = true; + uint32_t lastSentToMesh = 0; + bool wasDetected = false; + void sendDetectionMessage(); + void sendCurrentStateMessage(bool state); + bool hasDetectionEvent(); }; extern DetectionSensorModule *detectionSensorModule; \ No newline at end of file diff --git a/src/modules/DropzoneModule.cpp b/src/modules/DropzoneModule.cpp index c06ad8106..4b8f2fec8 100644 --- a/src/modules/DropzoneModule.cpp +++ b/src/modules/DropzoneModule.cpp @@ -16,75 +16,78 @@ DropzoneModule *dropzoneModule; -int32_t DropzoneModule::runOnce() { - // Send on a 5 second delay from receiving the matching request - if (startSendConditions != 0 && (startSendConditions + 5000U) < millis()) { - service->sendToMesh(sendConditions(), RX_SRC_LOCAL); - startSendConditions = 0; - } - // Run every second to check if we need to send conditions - return 1000; +int32_t DropzoneModule::runOnce() +{ + // Send on a 5 second delay from receiving the matching request + if (startSendConditions != 0 && (startSendConditions + 5000U) < millis()) { + service->sendToMesh(sendConditions(), RX_SRC_LOCAL); + startSendConditions = 0; + } + // Run every second to check if we need to send conditions + return 1000; } -ProcessMessage DropzoneModule::handleReceived(const meshtastic_MeshPacket &mp) { - auto &p = mp.decoded; - char matchCompare[54]; - auto incomingMessage = reinterpret_cast(p.payload.bytes); - sprintf(matchCompare, "%s conditions", owner.short_name); - if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { - LOG_DEBUG("Received dropzone conditions request"); - startSendConditions = millis(); - } +ProcessMessage DropzoneModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + auto &p = mp.decoded; + char matchCompare[54]; + auto incomingMessage = reinterpret_cast(p.payload.bytes); + sprintf(matchCompare, "%s conditions", owner.short_name); + if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { + LOG_DEBUG("Received dropzone conditions request"); + startSendConditions = millis(); + } - sprintf(matchCompare, "%s conditions", owner.long_name); - if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { - LOG_DEBUG("Received dropzone conditions request"); - startSendConditions = millis(); - } - return ProcessMessage::CONTINUE; + sprintf(matchCompare, "%s conditions", owner.long_name); + if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) { + LOG_DEBUG("Received dropzone conditions request"); + startSendConditions = millis(); + } + return ProcessMessage::CONTINUE; } -meshtastic_MeshPacket *DropzoneModule::sendConditions() { - char replyStr[200]; - /* - CLOSED @ {HH:MM:SS}z - Wind 2 kts @ 125° - 29.25 inHg 72°C - */ - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - int hour = 0, min = 0, sec = 0; - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; +meshtastic_MeshPacket *DropzoneModule::sendConditions() +{ + char replyStr[200]; + /* + CLOSED @ {HH:MM:SS}z + Wind 2 kts @ 125° + 29.25 inHg 72°C + */ + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + int hour = 0, min = 0, sec = 0; + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - graphics::decomposeTime(rtc_sec, hour, min, sec); - } + graphics::decomposeTime(rtc_sec, hour, min, sec); + } - // Check if the dropzone is open or closed by reading the analog pin - // If pin is connected to GND (below 100 should be lower than floating voltage), - // the dropzone is open - auto dropzoneStatus = analogRead(A1) < 100 ? "OPEN" : "CLOSED"; - auto reply = allocDataPacket(); + // Check if the dropzone is open or closed by reading the analog pin + // If pin is connected to GND (below 100 should be lower than floating voltage), + // the dropzone is open + auto dropzoneStatus = analogRead(A1) < 100 ? "OPEN" : "CLOSED"; + auto reply = allocDataPacket(); - auto node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (sensor.hasSensor()) { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - sensor.getMetrics(&telemetry); - auto windSpeed = UnitConversions::MetersPerSecondToKnots(telemetry.variant.environment_metrics.wind_speed); - auto windDirection = telemetry.variant.environment_metrics.wind_direction; - auto temp = telemetry.variant.environment_metrics.temperature; - auto baro = UnitConversions::HectoPascalToInchesOfMercury(telemetry.variant.environment_metrics.barometric_pressure); - sprintf(replyStr, "%s @ %02d:%02d:%02dz\nWind %.2f kts @ %d°\nBaro %.2f inHg %.2f°C", dropzoneStatus, hour, min, sec, windSpeed, windDirection, - baro, temp); - } else { - LOG_ERROR("No sensor found"); - sprintf(replyStr, "%s @ %02d:%02d:%02d\nNo sensor found", dropzoneStatus, hour, min, sec); - } - LOG_DEBUG("Conditions reply: %s", replyStr); - reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply - memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); + auto node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (sensor.hasSensor()) { + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + sensor.getMetrics(&telemetry); + auto windSpeed = UnitConversions::MetersPerSecondToKnots(telemetry.variant.environment_metrics.wind_speed); + auto windDirection = telemetry.variant.environment_metrics.wind_direction; + auto temp = telemetry.variant.environment_metrics.temperature; + auto baro = UnitConversions::HectoPascalToInchesOfMercury(telemetry.variant.environment_metrics.barometric_pressure); + sprintf(replyStr, "%s @ %02d:%02d:%02dz\nWind %.2f kts @ %d°\nBaro %.2f inHg %.2f°C", dropzoneStatus, hour, min, sec, + windSpeed, windDirection, baro, temp); + } else { + LOG_ERROR("No sensor found"); + sprintf(replyStr, "%s @ %02d:%02d:%02d\nNo sensor found", dropzoneStatus, hour, min, sec); + } + LOG_DEBUG("Conditions reply: %s", replyStr); + reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply + memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); - return reply; + return reply; } #endif \ No newline at end of file diff --git a/src/modules/DropzoneModule.h b/src/modules/DropzoneModule.h index 7d8f258d3..9e79ab447 100644 --- a/src/modules/DropzoneModule.h +++ b/src/modules/DropzoneModule.h @@ -7,28 +7,30 @@ * An example module that replies to a message with the current conditions * and status at the dropzone when it receives a text message mentioning it's name followed by "conditions" */ -class DropzoneModule : public SinglePortModule, private concurrency::OSThread { - DFRobotLarkSensor sensor; +class DropzoneModule : public SinglePortModule, private concurrency::OSThread +{ + DFRobotLarkSensor sensor; -public: - /** Constructor - * name is for debugging output - */ - DropzoneModule() : SinglePortModule("dropzone", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("Dropzone") { - // Set up the analog pin for reading the dropzone status - pinMode(PIN_A1, INPUT); - } + public: + /** Constructor + * name is for debugging output + */ + DropzoneModule() : SinglePortModule("dropzone", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("Dropzone") + { + // Set up the analog pin for reading the dropzone status + pinMode(PIN_A1, INPUT); + } - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; -protected: - /** Called to handle a particular incoming message - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + protected: + /** Called to handle a particular incoming message + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; -private: - meshtastic_MeshPacket *sendConditions(); - uint32_t startSendConditions = 0; + private: + meshtastic_MeshPacket *sendConditions(); + uint32_t startSendConditions = 0; }; extern DropzoneModule *dropzoneModule; diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 8be9681e9..6d52a3e46 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -2,10 +2,10 @@ * @file ExternalNotificationModule.cpp * @brief Implementation of the ExternalNotificationModule class. * - * This file contains the implementation of the ExternalNotificationModule class, which is responsible for handling - * external notifications such as vibration, buzzer, and LED lights. The class provides methods to turn on and off the - * external notification outputs and to play ringtones using PWM buzzer. It also includes default configurations and a - * runOnce() method to handle the module's behavior. + * This file contains the implementation of the ExternalNotificationModule class, which is responsible for handling external + * notifications such as vibration, buzzer, and LED lights. The class provides methods to turn on and off the external + * notification outputs and to play ringtones using PWM buzzer. It also includes default configurations and a runOnce() method to + * handle the module's behavior. * * Documentation: * https://meshtastic.org/docs/configuration/module/external-notification @@ -83,133 +83,140 @@ uint32_t externalTurnedOn[3] = {}; static const char *rtttlConfigFile = "/prefs/ringtone.proto"; -int32_t ExternalNotificationModule::runOnce() { - if (!moduleConfig.external_notification.enabled) { - return INT32_MAX; // we don't need this thread here... - } else { - uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS; - bool isRtttlPlaying = rtttl::isPlaying(); +int32_t ExternalNotificationModule::runOnce() +{ + if (!moduleConfig.external_notification.enabled) { + return INT32_MAX; // we don't need this thread here... + } else { + uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS; + bool isRtttlPlaying = rtttl::isPlaying(); #ifdef HAS_I2S - // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop - isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying(); + // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop + isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying(); #endif - if ((nagCycleCutoff < millis()) && !isRtttlPlaying) { - // Turn off external notification immediately when timeout is reached, regardless of song state - nagCycleCutoff = UINT32_MAX; - ExternalNotificationModule::stopNow(); - isNagging = false; - return INT32_MAX; // save cycles till we're needed again - } + if ((nagCycleCutoff < millis()) && !isRtttlPlaying) { + // Turn off external notification immediately when timeout is reached, regardless of song state + nagCycleCutoff = UINT32_MAX; + ExternalNotificationModule::stopNow(); + isNagging = false; + return INT32_MAX; // save cycles till we're needed again + } - // If the output is turned on, turn it back off after the given period of time. - if (isNagging) { - delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms : EXT_NOTIFICATION_MODULE_OUTPUT_MS); - if (externalTurnedOn[0] + delay < millis()) { - setExternalState(0, !getExternal(0)); - } - if (externalTurnedOn[1] + delay < millis()) { - setExternalState(1, !getExternal(1)); - } - // Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL) - if (!moduleConfig.external_notification.use_pwm && externalTurnedOn[2] + delay < millis()) { - LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms, millis()); - setExternalState(2, !getExternal(2)); - } + // If the output is turned on, turn it back off after the given period of time. + if (isNagging) { + delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms + : EXT_NOTIFICATION_MODULE_OUTPUT_MS); + if (externalTurnedOn[0] + delay < millis()) { + setExternalState(0, !getExternal(0)); + } + if (externalTurnedOn[1] + delay < millis()) { + setExternalState(1, !getExternal(1)); + } + // Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL) + if (!moduleConfig.external_notification.use_pwm && externalTurnedOn[2] + delay < millis()) { + LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms, + millis()); + setExternalState(2, !getExternal(2)); + } #if defined(HAS_RGB_LED) - red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7 - green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 - blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 - white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; + red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7 + green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 + blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 + white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; #ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } #endif #ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } + if (rgb_found.type == ScanI2C::LP5562) { + rgbw.setColor(red, green, blue, white); + } #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); + analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic + analogWrite(RGBLED_GREEN, 255 - green); + analogWrite(RGBLED_BLUE, 255 - blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); + analogWrite(RGBLED_RED, red); + analogWrite(RGBLED_GREEN, green); + analogWrite(RGBLED_BLUE, blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); #endif #ifdef UNPHONE - unphone.rgb(red, green, blue); + unphone.rgb(red, green, blue); #endif - if (ascending) { // fade in - brightnessIndex++; - if (brightnessIndex == (sizeof(brightnessValues) - 1)) { - ascending = false; - } - } else { - brightnessIndex--; // fade out - } - if (brightnessIndex == 0) { - ascending = true; - colorState++; // next color - if (colorState > 7) { - colorState = 1; - } - } - // we need fast updates for the color change - delay = EXT_NOTIFICATION_FAST_THREAD_MS; + if (ascending) { // fade in + brightnessIndex++; + if (brightnessIndex == (sizeof(brightnessValues) - 1)) { + ascending = false; + } + } else { + brightnessIndex--; // fade out + } + if (brightnessIndex == 0) { + ascending = true; + colorState++; // next color + if (colorState > 7) { + colorState = 1; + } + } + // we need fast updates for the color change + delay = EXT_NOTIFICATION_FAST_THREAD_MS; #endif #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - drv.go(); + drv.go(); #endif - } + } - // Play RTTTL over i2s audio interface if enabled as buzzer + // Play RTTTL over i2s audio interface if enabled as buzzer #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - if (audioThread->isPlaying()) { - // Continue playing - } else if (isNagging && (nagCycleCutoff >= millis())) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } - // we need fast updates to play the RTTTL - delay = EXT_NOTIFICATION_FAST_THREAD_MS; - } + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + if (audioThread->isPlaying()) { + // Continue playing + } else if (isNagging && (nagCycleCutoff >= millis())) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } + // we need fast updates to play the RTTTL + delay = EXT_NOTIFICATION_FAST_THREAD_MS; + } #endif - // now let the PWM buzzer play - if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { - if (rtttl::isPlaying()) { - rtttl::play(); - } else if (isNagging && (nagCycleCutoff >= millis())) { - // start the song again if we have time left - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - // we need fast updates to play the RTTTL - delay = EXT_NOTIFICATION_FAST_THREAD_MS; - } + // now let the PWM buzzer play + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { + if (rtttl::isPlaying()) { + rtttl::play(); + } else if (isNagging && (nagCycleCutoff >= millis())) { + // start the song again if we have time left + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } + // we need fast updates to play the RTTTL + delay = EXT_NOTIFICATION_FAST_THREAD_MS; + } - return delay; - } + return delay; + } } /** * Based on buzzer mode, return true if we can buzz. */ -bool ExternalNotificationModule::canBuzz() { - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && - config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { - return true; - } - return false; +bool ExternalNotificationModule::canBuzz() +{ + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + return true; + } + return false; } -bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } +bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) +{ + return MeshService::isTextPayload(p); +} /** * Sets the external notification for the specified index. @@ -217,349 +224,365 @@ bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { re * @param index The index of the external notification to change state. * @param on Whether we are turning things on (true) or off (false). */ -void ExternalNotificationModule::setExternalState(uint8_t index, bool on) { - externalCurrentState[index] = on; - externalTurnedOn[index] = millis(); +void ExternalNotificationModule::setExternalState(uint8_t index, bool on) +{ + externalCurrentState[index] = on; + externalTurnedOn[index] = millis(); - switch (index) { - case 1: + switch (index) { + case 1: #ifdef UNPHONE - unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander + unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander #endif - if (moduleConfig.external_notification.output_vibra) - digitalWrite(moduleConfig.external_notification.output_vibra, on); - break; - case 2: - // Only control buzzer pin digitally if not using PWM mode - if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm) - digitalWrite(moduleConfig.external_notification.output_buzzer, on); - break; - default: - if (output > 0) - digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); - break; - } + if (moduleConfig.external_notification.output_vibra) + digitalWrite(moduleConfig.external_notification.output_vibra, on); + break; + case 2: + // Only control buzzer pin digitally if not using PWM mode + if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm) + digitalWrite(moduleConfig.external_notification.output_buzzer, on); + break; + default: + if (output > 0) + digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); + break; + } #if defined(HAS_RGB_LED) - if (!on) { - red = 0; - green = 0; - blue = 0; - white = 0; - } + if (!on) { + red = 0; + green = 0; + blue = 0; + white = 0; + } #endif #ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.setColor(red, green, blue); + } #endif #ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } + if (rgb_found.type == ScanI2C::LP5562) { + rgbw.setColor(red, green, blue, white); + } #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); + analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic + analogWrite(RGBLED_GREEN, 255 - green); + analogWrite(RGBLED_BLUE, 255 - blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); + analogWrite(RGBLED_RED, red); + analogWrite(RGBLED_GREEN, green); + analogWrite(RGBLED_BLUE, blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); + pixels.show(); #endif #ifdef UNPHONE - unphone.rgb(red, green, blue); + unphone.rgb(red, green, blue); #endif #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - if (on) { - drv.go(); - } else { - drv.stop(); - } + if (on) { + drv.go(); + } else { + drv.stop(); + } #endif } -bool ExternalNotificationModule::getExternal(uint8_t index) { return externalCurrentState[index]; } +bool ExternalNotificationModule::getExternal(uint8_t index) +{ + return externalCurrentState[index]; +} // Allow other firmware components to determine whether a notification is ongoing -bool ExternalNotificationModule::nagging() { return isNagging; } +bool ExternalNotificationModule::nagging() +{ + return isNagging; +} -void ExternalNotificationModule::stopNow() { - LOG_INFO("Turning off external notification: "); - LOG_INFO("Stop RTTTL playback"); - rtttl::stop(); +void ExternalNotificationModule::stopNow() +{ + LOG_INFO("Turning off external notification: "); + LOG_INFO("Stop RTTTL playback"); + rtttl::stop(); #ifdef HAS_I2S - LOG_INFO("Stop audioThread playback"); - audioThread->stop(); + LOG_INFO("Stop audioThread playback"); + audioThread->stop(); #endif - // Turn off all outputs - LOG_INFO("Turning off setExternalStates"); - for (int i = 0; i < 3; i++) { - setExternalState(i, false); - externalTurnedOn[i] = 0; - } - setIntervalFromNow(0); + // Turn off all outputs + LOG_INFO("Turning off setExternalStates"); + for (int i = 0; i < 3; i++) { + setExternalState(i, false); + externalTurnedOn[i] = 0; + } + setIntervalFromNow(0); #if defined(T_WATCH_S3) || defined(T_LORA_PAGER) - drv.stop(); + drv.stop(); #endif - // Prevent the state machine from immediately re-triggering outputs after a manual stop. - isNagging = false; - nagCycleCutoff = UINT32_MAX; + // Prevent the state machine from immediately re-triggering outputs after a manual stop. + isNagging = false; + nagCycleCutoff = UINT32_MAX; #ifdef HAS_I2S - // GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library - // T-Deck uses GPIO0 as trackball button, so restore the mode + // GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library + // T-Deck uses GPIO0 as trackball button, so restore the mode #if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0) - pinMode(0, INPUT); + pinMode(0, INPUT); #endif #endif } ExternalNotificationModule::ExternalNotificationModule() - : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("ExternalNotification") { - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), + concurrency::OSThread("ExternalNotification") +{ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.external_notification.alert_message = true; - // moduleConfig.external_notification.alert_message_buzzer = true; - // moduleConfig.external_notification.alert_message_vibra = true; - // moduleConfig.external_notification.use_i2s_as_buzzer = true; + // moduleConfig.external_notification.alert_message = true; + // moduleConfig.external_notification.alert_message_buzzer = true; + // moduleConfig.external_notification.alert_message_vibra = true; + // moduleConfig.external_notification.use_i2s_as_buzzer = true; - // moduleConfig.external_notification.active = true; - // moduleConfig.external_notification.alert_bell = 1; - // moduleConfig.external_notification.output_ms = 1000; - // moduleConfig.external_notification.output = 4; // RAK4631 IO4 - // moduleConfig.external_notification.output_buzzer = 10; // RAK4631 IO6 - // moduleConfig.external_notification.output_vibra = 28; // RAK4631 IO7 - // moduleConfig.external_notification.nag_timeout = 300; + // moduleConfig.external_notification.active = true; + // moduleConfig.external_notification.alert_bell = 1; + // moduleConfig.external_notification.output_ms = 1000; + // moduleConfig.external_notification.output = 4; // RAK4631 IO4 + // moduleConfig.external_notification.output_buzzer = 10; // RAK4631 IO6 + // moduleConfig.external_notification.output_vibra = 28; // RAK4631 IO7 + // moduleConfig.external_notification.nag_timeout = 300; - // T-Watch / T-Deck i2s audio as buzzer: - // moduleConfig.external_notification.enabled = true; - // moduleConfig.external_notification.nag_timeout = 300; - // moduleConfig.external_notification.output_ms = 1000; - // moduleConfig.external_notification.use_i2s_as_buzzer = true; - // moduleConfig.external_notification.alert_message_buzzer = true; + // T-Watch / T-Deck i2s audio as buzzer: + // moduleConfig.external_notification.enabled = true; + // moduleConfig.external_notification.nag_timeout = 300; + // moduleConfig.external_notification.output_ms = 1000; + // moduleConfig.external_notification.use_i2s_as_buzzer = true; + // moduleConfig.external_notification.alert_message_buzzer = true; - if (moduleConfig.external_notification.enabled) { + if (moduleConfig.external_notification.enabled) { #if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) - if (inputBroker) // put our callback in the inputObserver list - inputObserver.observe(inputBroker); + if (inputBroker) // put our callback in the inputObserver list + inputObserver.observe(inputBroker); #endif - if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != - LoadFileResult::LOAD_SUCCESS) { - memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); - // The default ringtone is always loaded from userPrefs.jsonc - strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone)); - } + if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), + &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { + memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); + // The default ringtone is always loaded from userPrefs.jsonc + strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone)); + } - LOG_INFO("Init External Notification Module"); + LOG_INFO("Init External Notification Module"); - output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output : EXT_NOTIFICATION_MODULE_OUTPUT; + output = moduleConfig.external_notification.output ? moduleConfig.external_notification.output + : EXT_NOTIFICATION_MODULE_OUTPUT; - // Set the direction of a pin - if (output > 0) { - LOG_INFO("Use Pin %i in digital mode", output); - pinMode(output, OUTPUT); - } - setExternalState(0, false); - externalTurnedOn[0] = 0; - if (moduleConfig.external_notification.output_vibra) { - LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra); - pinMode(moduleConfig.external_notification.output_vibra, OUTPUT); - setExternalState(1, false); - externalTurnedOn[1] = 0; - } - if (moduleConfig.external_notification.output_buzzer && canBuzz()) { - if (!moduleConfig.external_notification.use_pwm) { - LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); - pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); - setExternalState(2, false); - externalTurnedOn[2] = 0; - } else { - config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER; - // in PWM Mode we force the buzzer pin if it is set - LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); - } - } + // Set the direction of a pin + if (output > 0) { + LOG_INFO("Use Pin %i in digital mode", output); + pinMode(output, OUTPUT); + } + setExternalState(0, false); + externalTurnedOn[0] = 0; + if (moduleConfig.external_notification.output_vibra) { + LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra); + pinMode(moduleConfig.external_notification.output_vibra, OUTPUT); + setExternalState(1, false); + externalTurnedOn[1] = 0; + } + if (moduleConfig.external_notification.output_buzzer && canBuzz()) { + if (!moduleConfig.external_notification.use_pwm) { + LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); + pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); + setExternalState(2, false); + externalTurnedOn[2] = 0; + } else { + config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER; + // in PWM Mode we force the buzzer pin if it is set + LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); + } + } #ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.begin(); - rgb.setCurrent(10); - } + if (rgb_found.type == ScanI2C::NCP5623) { + rgb.begin(); + rgb.setCurrent(10); + } #endif #ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.begin(); - rgbw.setCurrent(20); - } + if (rgb_found.type == ScanI2C::LP5562) { + rgbw.begin(); + rgbw.setCurrent(20); + } #endif #ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); + pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins + pinMode(RGBLED_GREEN, OUTPUT); + pinMode(RGBLED_BLUE, OUTPUT); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed - analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off - analogWrite(RGBLED_BLUE, 255); + analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed + analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off + analogWrite(RGBLED_BLUE, 255); #endif #ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); + pixels.begin(); // Initialise the pixel(s) + pixels.clear(); // Set all pixel colors to 'off' + pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - } else { - LOG_INFO("External Notification Module Disabled"); - disable(); - } + } else { + LOG_INFO("External Notification Module Disabled"); + disable(); + } } -ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) { - if (moduleConfig.external_notification.enabled && !isSilenced) { +ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (moduleConfig.external_notification.enabled && !isSilenced) { #ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); + drv.setWaveform(0, 75); + drv.setWaveform(1, 56); + drv.setWaveform(2, 0); + drv.go(); #endif - if (!isFromUs(&mp)) { - // Check if the message contains a bell character. Don't do this loop for every pin, just once. - auto &p = mp.decoded; - bool containsBell = false; - for (size_t i = 0; i < p.payload.size; i++) { - if (p.payload.bytes[i] == ASCII_BELL) { - containsBell = true; - } - } - - meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); - if (moduleConfig.external_notification.alert_bell) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_bell_vibra) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); - isNagging = true; - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + if (!isFromUs(&mp)) { + // Check if the message contains a bell character. Don't do this loop for every pin, just once. + auto &p = mp.decoded; + bool containsBell = false; + for (size_t i = 0; i < p.payload.size; i++) { + if (p.payload.bytes[i] == ASCII_BELL) { + containsBell = true; + } } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - if (moduleConfig.external_notification.alert_message && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } + meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); + if (moduleConfig.external_notification.alert_bell) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell"); + isNagging = true; + setExternalState(0, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + } - if (moduleConfig.external_notification.alert_message_vibra && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } + if (moduleConfig.external_notification.alert_bell_vibra) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); + isNagging = true; + setExternalState(1, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + } - if (moduleConfig.external_notification.alert_message_buzzer && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { - LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || (!isBroadcast(mp.to) && isToUs(&mp))) { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us - isNagging = true; + if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { + if (containsBell) { + LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); + isNagging = true; + if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { + setExternalState(2, true); + } else { +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } else +#endif + if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } + } + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + } + + if (moduleConfig.external_notification.alert_message && + (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + LOG_INFO("externalNotificationModule - Notification Module"); + isNagging = true; + setExternalState(0, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + + if (moduleConfig.external_notification.alert_message_vibra && + (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); + isNagging = true; + setExternalState(1, true); + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } + + if (moduleConfig.external_notification.alert_message_buzzer && + (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { + LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || + (!isBroadcast(mp.to) && isToUs(&mp))) { + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us + isNagging = true; #ifdef T_LORA_PAGER - if (canBuzz()) { - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); - } + if (canBuzz()) { + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); + } #endif - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { + if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { + setExternalState(2, true); + } else { #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else + if (moduleConfig.external_notification.use_i2s_as_buzzer) { + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); + } else #endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } + } + if (moduleConfig.external_notification.nag_timeout) { + nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } else { + nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + } + } else { + // Don't beep if buzzer mode is "direct messages only" and it is no direct message + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + } } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } else { - // Don't beep if buzzer mode is "direct messages only" and it is no direct message - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + setIntervalFromNow(0); // run once so we know if we should do something } - } - setIntervalFromNow(0); // run once so we know if we should do something + } else { + LOG_INFO("External Notification Module Disabled or muted"); } - } else { - LOG_INFO("External Notification Module Disabled or muted"); - } - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** @@ -571,56 +594,61 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP * @return AdminMessageHandleResult HANDLED if message was handled * HANDLED_WITH_RESULT if a result is also prepared. */ -AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - AdminMessageHandleResult result; +AdminMessageHandleResult ExternalNotificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_get_ringtone_request_tag: - LOG_INFO("Client getting ringtone"); - this->handleGetRingtone(mp, response); - result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; - break; + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_get_ringtone_request_tag: + LOG_INFO("Client getting ringtone"); + this->handleGetRingtone(mp, response); + result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; - case meshtastic_AdminMessage_set_ringtone_message_tag: - LOG_INFO("Client setting ringtone"); - this->handleSetRingtone(request->set_canned_message_module_messages); - result = AdminMessageHandleResult::HANDLED; - break; + case meshtastic_AdminMessage_set_ringtone_message_tag: + LOG_INFO("Client setting ringtone"); + this->handleSetRingtone(request->set_canned_message_module_messages); + result = AdminMessageHandleResult::HANDLED; + break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; - } + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } - return result; + return result; } -void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) { - LOG_INFO("*** handleGetRingtone"); - if (req.decoded.want_response) { - response->which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag; - strncpy(response->get_ringtone_response, rtttlConfig.ringtone, sizeof(response->get_ringtone_response)); - } // Don't send anything if not instructed to. Better than asserting. +void ExternalNotificationModule::handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) +{ + LOG_INFO("*** handleGetRingtone"); + if (req.decoded.want_response) { + response->which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag; + strncpy(response->get_ringtone_response, rtttlConfig.ringtone, sizeof(response->get_ringtone_response)); + } // Don't send anything if not instructed to. Better than asserting. } -void ExternalNotificationModule::handleSetRingtone(const char *from_msg) { - int changed = 0; +void ExternalNotificationModule::handleSetRingtone(const char *from_msg) +{ + int changed = 0; - if (*from_msg) { - changed |= strcmp(rtttlConfig.ringtone, from_msg); - strncpy(rtttlConfig.ringtone, from_msg, sizeof(rtttlConfig.ringtone)); - LOG_INFO("*** from_msg.text:%s", from_msg); - } + if (*from_msg) { + changed |= strcmp(rtttlConfig.ringtone, from_msg); + strncpy(rtttlConfig.ringtone, from_msg, sizeof(rtttlConfig.ringtone)); + LOG_INFO("*** from_msg.text:%s", from_msg); + } - if (changed) { - nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); - } + if (changed) { + nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); + } } -int ExternalNotificationModule::handleInputEvent(const InputEvent *event) { - if (nagCycleCutoff != UINT32_MAX) { - stopNow(); - return 1; - } - return 0; +int ExternalNotificationModule::handleInputEvent(const InputEvent *event) +{ + if (nagCycleCutoff != UINT32_MAX) { + stopNow(); + return 1; + } + return 0; } \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index f490dff91..f667f7be9 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -9,14 +9,15 @@ #include #else // Noop class for portduino. -class rtttl { -public: - explicit rtttl() {} - static bool isPlaying() { return false; } - static void play() {} - static void begin(byte a, const char *b){}; - static void stop() {} - static bool done() { return true; } +class rtttl +{ + public: + explicit rtttl() {} + static bool isPlaying() { return false; } + static void play() {} + static void begin(byte a, const char *b){}; + static void stop() {} + static bool done() { return true; } }; #endif #include @@ -26,49 +27,51 @@ public: * Radio interface for ExternalNotificationModule * */ -class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread { - CallbackObserver inputObserver = - CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); - uint32_t output = 0; +class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread +{ + CallbackObserver inputObserver = + CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); + uint32_t output = 0; -public: - ExternalNotificationModule(); + public: + ExternalNotificationModule(); - int handleInputEvent(const InputEvent *arg); + int handleInputEvent(const InputEvent *arg); - uint32_t nagCycleCutoff = 1; + uint32_t nagCycleCutoff = 1; - void setExternalState(uint8_t index = 0, bool on = false); - bool getExternal(uint8_t index = 0); + void setExternalState(uint8_t index = 0, bool on = false); + bool getExternal(uint8_t index = 0); - void setMute(bool mute) { isSilenced = mute; } - bool getMute() { return isSilenced; } + void setMute(bool mute) { isSilenced = mute; } + bool getMute() { return isSilenced; } - bool canBuzz(); - bool nagging(); + bool canBuzz(); + bool nagging(); - void stopNow(); + void stopNow(); - void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); - void handleSetRingtone(const char *from_msg); + void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); + void handleSetRingtone(const char *from_msg); -protected: - /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + protected: + /** Called to handle a particular incoming message + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - virtual bool wantPacket(const meshtastic_MeshPacket *p) override; + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; - bool isNagging = false; + bool isNagging = false; - bool isSilenced = false; + bool isSilenced = false; - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; }; extern ExternalNotificationModule *externalNotificationModule; \ No newline at end of file diff --git a/src/modules/GenericThreadModule.cpp b/src/modules/GenericThreadModule.cpp index 3c811b583..eb92566bd 100644 --- a/src/modules/GenericThreadModule.cpp +++ b/src/modules/GenericThreadModule.cpp @@ -10,18 +10,19 @@ GenericThreadModule *genericThreadModule; GenericThreadModule::GenericThreadModule() : concurrency::OSThread("GenericThreadModule") {} -int32_t GenericThreadModule::runOnce() { +int32_t GenericThreadModule::runOnce() +{ - bool enabled = true; - if (!enabled) - return disable(); + bool enabled = true; + if (!enabled) + return disable(); - if (firstTime) { - // do something the first time we run - firstTime = 0; - LOG_INFO("first time GenericThread running"); - } + if (firstTime) { + // do something the first time we run + firstTime = 0; + LOG_INFO("first time GenericThread running"); + } - LOG_INFO("GenericThread executing"); - return (my_interval); + LOG_INFO("GenericThread executing"); + return (my_interval); } diff --git a/src/modules/GenericThreadModule.h b/src/modules/GenericThreadModule.h index 6ef7c4b29..05f7946bb 100644 --- a/src/modules/GenericThreadModule.h +++ b/src/modules/GenericThreadModule.h @@ -6,15 +6,16 @@ #include #include -class GenericThreadModule : private concurrency::OSThread { - bool firstTime = 1; +class GenericThreadModule : private concurrency::OSThread +{ + bool firstTime = 1; -public: - GenericThreadModule(); + public: + GenericThreadModule(); -protected: - unsigned int my_interval = 10000; // interval in millisconds - virtual int32_t runOnce() override; + protected: + unsigned int my_interval = 10000; // interval in millisconds + virtual int32_t runOnce() override; }; extern GenericThreadModule *genericThreadModule; diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 1356919a6..3b8225763 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -11,284 +11,303 @@ KeyVerificationModule *keyVerificationModule; KeyVerificationModule::KeyVerificationModule() - : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) { - ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; + : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) +{ + ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; } -AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - updateState(); - if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { - LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); +AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + updateState(); + if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { + LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); - if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && - currentState == KEY_VERIFICATION_IDLE) { - sendInitialRequest(request->key_verification.remote_nodenum); + if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && + currentState == KEY_VERIFICATION_IDLE) { + sendInitialRequest(request->key_verification.remote_nodenum); - } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && - request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && - request->key_verification.nonce == currentNonce) { - processSecurityNumber(request->key_verification.security_number); + } else if (request->key_verification.message_type == + meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && + request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && + request->key_verification.nonce == currentNonce) { + processSecurityNumber(request->key_verification.security_number); - } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && - request->key_verification.nonce == currentNonce) { - auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - resetToIdle(); - } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { - resetToIdle(); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && + request->key_verification.nonce == currentNonce) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + resetToIdle(); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { + resetToIdle(); + } + return AdminMessageHandleResult::HANDLED; } - return AdminMessageHandleResult::HANDLED; - } - return AdminMessageHandleResult::NOT_HANDLED; + return AdminMessageHandleResult::NOT_HANDLED; } -bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) { - updateState(); - if (mp.pki_encrypted == false) { - return false; - } - if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply() - return false; - } - if (currentState == KEY_VERIFICATION_IDLE) { - return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() - } +bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) +{ + updateState(); + if (mp.pki_encrypted == false) { + return false; + } + if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply() + return false; + } + if (currentState == KEY_VERIFICATION_IDLE) { + return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() + } - if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && r->hash1.size == 0) { - memcpy(hash2, r->hash2.bytes, 32); - IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, - [](int number_picked) -> void { keyVerificationModule->processSecurityNumber(number_picked); });) + if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && + r->hash1.size == 0) { + memcpy(hash2, r->hash2.bytes, 32); + IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void { + keyVerificationModule->processSecurityNumber(number_picked); + });) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Enter Security Number for Key Verification"); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; + cn->payload_variant.key_verification_number_request.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + service->sendClientNotification(cn); + LOG_INFO("Received hash2"); + currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; + return true; + + } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { + if (memcmp(hash1, r->hash1.bytes, 32) == 0) { + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); + LOG_INFO("Hash1 matches!"); + static const char *optionsArray[] = {"Reject", "Accept"}; + // Don't try to put the array definition in the macro. Does not work with curly braces. + IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; + options.optionsArrayPtr = optionsArray; options.optionsCount = 2; + options.notificationType = graphics::notificationTypeEnum::selection_picker; + options.bannerCallback = + [=](int selected) { + if (selected == 1) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + }; + screen->showOverlayBanner(options);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = false; + service->sendClientNotification(cn); + + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; + return true; + } + } + return false; +} + +bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) +{ + LOG_DEBUG("keyVerification start"); + // generate nonce + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { + IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) + return false; + } + currentNonce = random(); + currentNonceTimestamp = getTime(); + currentRemoteNode = remoteNode; + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 0; + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = remoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + + currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; + return true; +} + +meshtastic_MeshPacket *KeyVerificationModule::allocReply() +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period + LOG_WARN("Key Verification requested, but already in a request"); + return nullptr; + } else if (!currentRequest->pki_encrypted) { + LOG_WARN("Key Verification requested, but not in a PKI packet"); + return nullptr; + } + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; + + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_KeyVerification scratch; + meshtastic_KeyVerification response; + meshtastic_MeshPacket *responsePacket = nullptr; + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); + + currentNonce = scratch.nonce; + response.nonce = scratch.nonce; + currentRemoteNode = req.from; + currentNonceTimestamp = getTime(); + currentSecurityNumber = random(1, 999999); + + // generate hash1 + hash.reset(); + hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); + hash.update(owner.public_key.bytes, owner.public_key.size); + hash.finalize(hash1, 32); + + // generate hash2 + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(hash2, 32); + response.hash1.size = 0; + response.hash2.size = 32; + memcpy(response.hash2.bytes, hash2, 32); + + responsePacket = allocDataProtobuf(response); + + responsePacket->pki_encrypted = true; + IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); + screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Enter Security Number for Key Verification"); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; - cn->payload_variant.key_verification_number_request.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, + currentSecurityNumber % 1000); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; + cn->payload_variant.key_verification_number_inform.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); + cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; service->sendClientNotification(cn); - LOG_INFO("Received hash2"); - currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; - return true; + LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); + return responsePacket; +} - } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { - if (memcmp(hash1, r->hash1.bytes, 32) == 0) { - memset(message, 0, sizeof(message)); - sprintf(message, "Verification: \n"); - generateVerificationCode(message + 15); - LOG_INFO("Hash1 matches!"); - static const char *optionsArray[] = {"Reject", "Accept"}; - // Don't try to put the array definition in the macro. Does not work with curly braces. - IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; options.optionsArrayPtr = optionsArray; - options.optionsCount = 2; options.notificationType = graphics::notificationTypeEnum::selection_picker; - options.bannerCallback = - [=](int selected) { - if (selected == 1) { - auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - } - }; - screen->showOverlayBanner(options);) - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; - cn->payload_variant.key_verification_final.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); - cn->payload_variant.key_verification_final.isSender = false; - service->sendClientNotification(cn); - - currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; - return true; +void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + uint8_t scratch_hash[32] = {0}; + LOG_WARN("received security number: %u", incomingNumber); + meshtastic_NodeInfoLite *remoteNodePtr = nullptr; + remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { + currentState = KEY_VERIFICATION_IDLE; + return; // should we throw an error here? } - } - return false; + LOG_WARN("hashing "); + // calculate hash1 + hash.reset(); + hash.update(&incomingNumber, sizeof(incomingNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(owner.public_key.bytes, owner.public_key.size); + + hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); + hash.finalize(hash1, 32); + + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(scratch_hash, 32); + + if (memcmp(scratch_hash, hash2, 32) != 0) { + LOG_WARN("Hash2 did not match"); + return; // should probably throw an error of some sort + } + currentSecurityNumber = incomingNumber; + + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 32; + memcpy(KeyVerification.hash1.bytes, hash1, 32); + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = currentRemoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; + IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = true; + service->sendClientNotification(cn); + LOG_INFO(message); + + return; } -bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) { - LOG_DEBUG("keyVerification start"); - // generate nonce - updateState(); - if (currentState != KEY_VERIFICATION_IDLE) { - IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) - return false; - } - currentNonce = random(); - currentNonceTimestamp = getTime(); - currentRemoteNode = remoteNode; - meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; - KeyVerification.nonce = currentNonce; - KeyVerification.hash2.size = 0; - KeyVerification.hash1.size = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); - p->to = remoteNode; - p->channel = 0; - p->pki_encrypted = true; - p->decoded.want_response = true; - p->priority = meshtastic_MeshPacket_Priority_HIGH; - service->sendToMesh(p, RX_SRC_LOCAL, true); - - currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; - return true; +void KeyVerificationModule::updateState() +{ + if (currentState != KEY_VERIFICATION_IDLE) { + // check for the 60 second timeout + if (currentNonceTimestamp < getTime() - 60) { + resetToIdle(); + } else { + currentNonceTimestamp = getTime(); + } + } } -meshtastic_MeshPacket *KeyVerificationModule::allocReply() { - SHA256 hash; - NodeNum ourNodeNum = nodeDB->getNodeNum(); - updateState(); - if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period - LOG_WARN("Key Verification requested, but already in a request"); - return nullptr; - } else if (!currentRequest->pki_encrypted) { - LOG_WARN("Key Verification requested, but not in a PKI packet"); - return nullptr; - } - currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; - - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_KeyVerification scratch; - meshtastic_KeyVerification response; - meshtastic_MeshPacket *responsePacket = nullptr; - pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); - - currentNonce = scratch.nonce; - response.nonce = scratch.nonce; - currentRemoteNode = req.from; - currentNonceTimestamp = getTime(); - currentSecurityNumber = random(1, 999999); - - // generate hash1 - hash.reset(); - hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); - hash.update(&ourNodeNum, sizeof(ourNodeNum)); - hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); - hash.update(owner.public_key.bytes, owner.public_key.size); - hash.finalize(hash1, 32); - - // generate hash2 - hash.reset(); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(hash1, 32); - hash.finalize(hash2, 32); - response.hash1.size = 0; - response.hash2.size = 32; - memcpy(response.hash2.bytes, hash2, 32); - - responsePacket = allocDataProtobuf(response); - - responsePacket->pki_encrypted = true; - IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); - screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);) - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; - cn->payload_variant.key_verification_number_inform.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); - cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; - service->sendClientNotification(cn); - LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); - return responsePacket; -} - -void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) { - SHA256 hash; - NodeNum ourNodeNum = nodeDB->getNodeNum(); - uint8_t scratch_hash[32] = {0}; - LOG_WARN("received security number: %u", incomingNumber); - meshtastic_NodeInfoLite *remoteNodePtr = nullptr; - remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { +void KeyVerificationModule::resetToIdle() +{ + memset(hash1, 0, 32); + memset(hash2, 0, 32); + currentNonce = 0; + currentNonceTimestamp = 0; + currentSecurityNumber = 0; + currentRemoteNode = 0; currentState = KEY_VERIFICATION_IDLE; - return; // should we throw an error here? - } - LOG_WARN("hashing "); - // calculate hash1 - hash.reset(); - hash.update(&incomingNumber, sizeof(incomingNumber)); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(&ourNodeNum, sizeof(ourNodeNum)); - hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); - hash.update(owner.public_key.bytes, owner.public_key.size); - - hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); - hash.finalize(hash1, 32); - - hash.reset(); - hash.update(¤tNonce, sizeof(currentNonce)); - hash.update(hash1, 32); - hash.finalize(scratch_hash, 32); - - if (memcmp(scratch_hash, hash2, 32) != 0) { - LOG_WARN("Hash2 did not match"); - return; // should probably throw an error of some sort - } - currentSecurityNumber = incomingNumber; - - meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; - KeyVerification.nonce = currentNonce; - KeyVerification.hash2.size = 0; - KeyVerification.hash1.size = 32; - memcpy(KeyVerification.hash1.bytes, hash1, 32); - meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); - p->to = currentRemoteNode; - p->channel = 0; - p->pki_encrypted = true; - p->decoded.want_response = true; - p->priority = meshtastic_MeshPacket_Priority_HIGH; - service->sendToMesh(p, RX_SRC_LOCAL, true); - currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; - IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); - cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; - cn->payload_variant.key_verification_final.nonce = currentNonce; - strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); - cn->payload_variant.key_verification_final.isSender = true; - service->sendClientNotification(cn); - LOG_INFO(message); - - return; } -void KeyVerificationModule::updateState() { - if (currentState != KEY_VERIFICATION_IDLE) { - // check for the 60 second timeout - if (currentNonceTimestamp < getTime() - 60) { - resetToIdle(); - } else { - currentNonceTimestamp = getTime(); +void KeyVerificationModule::generateVerificationCode(char *readableCode) +{ + for (int i = 0; i < 4; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } + readableCode[4] = ' '; + for (int i = 5; i < 9; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. } - } -} - -void KeyVerificationModule::resetToIdle() { - memset(hash1, 0, 32); - memset(hash2, 0, 32); - currentNonce = 0; - currentNonceTimestamp = 0; - currentSecurityNumber = 0; - currentRemoteNode = 0; - currentState = KEY_VERIFICATION_IDLE; -} - -void KeyVerificationModule::generateVerificationCode(char *readableCode) { - for (int i = 0; i < 4; i++) { - // drop the two highest significance bits, then encode as a base64 - readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. - } - readableCode[4] = ' '; - for (int i = 5; i < 9; i++) { - // drop the two highest significance bits, then encode as a base64 - readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. - } } #endif \ No newline at end of file diff --git a/src/modules/KeyVerificationModule.h b/src/modules/KeyVerificationModule.h index e9bfb9e1d..d5dba01d7 100644 --- a/src/modules/KeyVerificationModule.h +++ b/src/modules/KeyVerificationModule.h @@ -4,62 +4,62 @@ #include "SinglePortModule.h" enum KeyVerificationState { - KEY_VERIFICATION_IDLE, - KEY_VERIFICATION_SENDER_HAS_INITIATED, - KEY_VERIFICATION_SENDER_AWAITING_NUMBER, - KEY_VERIFICATION_SENDER_AWAITING_USER, - KEY_VERIFICATION_RECEIVER_AWAITING_USER, - KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, + KEY_VERIFICATION_IDLE, + KEY_VERIFICATION_SENDER_HAS_INITIATED, + KEY_VERIFICATION_SENDER_AWAITING_NUMBER, + KEY_VERIFICATION_SENDER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, }; class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // { - // CallbackObserver nodeStatusObserver = - // CallbackObserver(this, - // &KeyVerificationModule::handleStatusUpdate); + // CallbackObserver nodeStatusObserver = + // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); -public: - KeyVerificationModule(); - /* : concurrency::OSThread("KeyVerification"), - ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) - { - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent - }*/ - virtual bool wantUIFrame() { return false; }; - bool sendInitialRequest(NodeNum remoteNode); - void generateVerificationCode(char *); // fills char with the user readable verification code - uint32_t getCurrentRemoteNode() { return currentRemoteNode; } + public: + KeyVerificationModule(); + /* : concurrency::OSThread("KeyVerification"), + ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) + { + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + }*/ + virtual bool wantUIFrame() { return false; }; + bool sendInitialRequest(NodeNum remoteNode); + void generateVerificationCode(char *); // fills char with the user readable verification code + uint32_t getCurrentRemoteNode() { return currentRemoteNode; } -protected: - /* Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); - // virtual meshtastic_MeshPacket *allocReply() override; + protected: + /* Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); + // virtual meshtastic_MeshPacket *allocReply() override; - // rather than add to the craziness that is the admin module, just handle those requests here. - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; - /* - * Send our Telemetry into the mesh - */ - bool sendMetrics(); - virtual meshtastic_MeshPacket *allocReply() override; + // rather than add to the craziness that is the admin module, just handle those requests here. + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + /* + * Send our Telemetry into the mesh + */ + bool sendMetrics(); + virtual meshtastic_MeshPacket *allocReply() override; -private: - uint64_t currentNonce = 0; - uint32_t currentNonceTimestamp = 0; - NodeNum currentRemoteNode = 0; - uint32_t currentSecurityNumber = 0; - KeyVerificationState currentState = KEY_VERIFICATION_IDLE; - uint8_t hash1[32] = {0}; // - uint8_t hash2[32] = {0}; // - char message[40] = {0}; + private: + uint64_t currentNonce = 0; + uint32_t currentNonceTimestamp = 0; + NodeNum currentRemoteNode = 0; + uint32_t currentSecurityNumber = 0; + KeyVerificationState currentState = KEY_VERIFICATION_IDLE; + uint8_t hash1[32] = {0}; // + uint8_t hash2[32] = {0}; // + char message[40] = {0}; - void processSecurityNumber(uint32_t); - void updateState(); // check the timeouts and maybe reset the state to idle - void resetToIdle(); // Zero out module state + void processSecurityNumber(uint32_t); + void updateState(); // check the timeouts and maybe reset the state to idle + void resetToIdle(); // Zero out module state }; extern KeyVerificationModule *keyVerificationModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index b95399120..63392f7e4 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -112,195 +112,200 @@ /** * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) */ -void setupModules() { +void setupModules() +{ #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - inputBroker = new InputBroker(); - systemCommandsModule = new SystemCommandsModule(); - buzzerFeedbackThread = new BuzzerFeedbackThread(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + inputBroker = new InputBroker(); + systemCommandsModule = new SystemCommandsModule(); + buzzerFeedbackThread = new BuzzerFeedbackThread(); + } #endif #if defined(LED_CHARGE) || defined(LED_PAIRING) - statusLEDModule = new StatusLEDModule(); + statusLEDModule = new StatusLEDModule(); #endif #if !MESHTASTIC_EXCLUDE_ADMIN - adminModule = new AdminModule(); + adminModule = new AdminModule(); #endif #if !MESHTASTIC_EXCLUDE_NODEINFO - nodeInfoModule = new NodeInfoModule(); + nodeInfoModule = new NodeInfoModule(); #endif #if !MESHTASTIC_EXCLUDE_GPS - positionModule = new PositionModule(); + positionModule = new PositionModule(); #endif #if !MESHTASTIC_EXCLUDE_WAYPOINT - waypointModule = new WaypointModule(); + waypointModule = new WaypointModule(); #endif #if !MESHTASTIC_EXCLUDE_TEXTMESSAGE - textMessageModule = new TextMessageModule(); + textMessageModule = new TextMessageModule(); #endif #if !MESHTASTIC_EXCLUDE_TRACEROUTE - traceRouteModule = new TraceRouteModule(); + traceRouteModule = new TraceRouteModule(); #endif #if !MESHTASTIC_EXCLUDE_NEIGHBORINFO - if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) { - neighborInfoModule = new NeighborInfoModule(); - } + if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) { + neighborInfoModule = new NeighborInfoModule(); + } #endif #if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR - if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) { - detectionSensorModule = new DetectionSensorModule(); - } + if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) { + detectionSensorModule = new DetectionSensorModule(); + } #endif #if !MESHTASTIC_EXCLUDE_ATAK - if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { - atakPluginModule = new AtakPluginModule(); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK || + config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + atakPluginModule = new AtakPluginModule(); + } #endif #if !MESHTASTIC_EXCLUDE_PKI - keyVerificationModule = new KeyVerificationModule(); + keyVerificationModule = new KeyVerificationModule(); #endif #if !MESHTASTIC_EXCLUDE_DROPZONE - dropzoneModule = new DropzoneModule(); + dropzoneModule = new DropzoneModule(); #endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE - new GenericThreadModule(); + new GenericThreadModule(); #endif - // Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance - // to a global variable. + // Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance + // to a global variable. #if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE - new RemoteHardwareModule(); + new RemoteHardwareModule(); #endif #if !MESHTASTIC_EXCLUDE_POWERSTRESS - new PowerStressModule(); + new PowerStressModule(); #endif - // Example: Put your module here - // new ReplyModule(); + // Example: Put your module here + // new ReplyModule(); #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(T_LORA_PAGER) - // use a special FSM based rotary encoder version for T-LoRa Pager - rotaryEncoderImpl = new RotaryEncoderImpl(); - if (!rotaryEncoderImpl->init()) { - delete rotaryEncoderImpl; - rotaryEncoderImpl = nullptr; - } + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } #elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } #else - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } #endif - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); #if defined(M5STACK_UNITC6L) - i2cButton = new i2cButtonThread("i2cButtonThread"); + i2cButton = new i2cButtonThread("i2cButtonThread"); #endif #ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE #ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE - } + } #endif // HAS_BUTTON #if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { - seesawRotary = new SeesawRotary("SeesawRotary"); - if (!seesawRotary->init()) { - delete seesawRotary; - seesawRotary = nullptr; + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; + } + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); } - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); - } #endif #if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); + } #endif #ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE - expressLRSFiveWayInput = new ExpressLRSFiveWay(); + expressLRSFiveWayInput = new ExpressLRSFiveWay(); #endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - cannedMessageModule = new CannedMessageModule(); - } + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + cannedMessageModule = new CannedMessageModule(); + } #endif #if ARCH_PORTDUINO - new HostMetricsModule(); + new HostMetricsModule(); #endif #if HAS_TELEMETRY - new DeviceTelemetryModule(); + new DeviceTelemetryModule(); #endif #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (moduleConfig.has_telemetry && (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { - new EnvironmentTelemetryModule(); - } + if (moduleConfig.has_telemetry && + (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { + new EnvironmentTelemetryModule(); + } #if __has_include("Adafruit_PM25AQI.h") - if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { - new AirQualityTelemetryModule(); - } + if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { + new AirQualityTelemetryModule(); + } #endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY - if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { - new HealthTelemetryModule(); - } + if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { + new HealthTelemetryModule(); + } #endif #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR - if (moduleConfig.has_telemetry && (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) { - new PowerTelemetryModule(); - } + if (moduleConfig.has_telemetry && + (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) { + new PowerTelemetryModule(); + } #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL - if (moduleConfig.has_serial && moduleConfig.serial.enabled && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - new SerialModule(); - } + if (moduleConfig.has_serial && moduleConfig.serial.enabled && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + new SerialModule(); + } #endif #endif #ifdef ARCH_ESP32 - // Only run on an esp32 based device. + // Only run on an esp32 based device. #if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO - audioModule = new AudioModule(); + audioModule = new AudioModule(); #endif #if !MESHTASTIC_EXCLUDE_PAXCOUNTER - if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) { - paxcounterModule = new PaxcounterModule(); - } + if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) { + paxcounterModule = new PaxcounterModule(); + } #endif #endif #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) #if !MESHTASTIC_EXCLUDE_STOREFORWARD - if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) { - storeForwardModule = new StoreForwardModule(); - } + if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) { + storeForwardModule = new StoreForwardModule(); + } #endif #endif #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION - externalNotificationModule = new ExternalNotificationModule(); + externalNotificationModule = new ExternalNotificationModule(); #endif #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS - if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) - new RangeTestModule(); + if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) + new RangeTestModule(); #endif - // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending - // extra acks - routingModule = new RoutingModule(); + // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra + // acks + routingModule = new RoutingModule(); } diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index 794c0d6d7..2cd8ec5ed 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -12,39 +12,45 @@ Prints a single neighbor info packet and associated neighbors Uses LOG_DEBUG, which equates to Console.log NOTE: For debugging only */ -void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np) { - LOG_DEBUG("%s NEIGHBORINFO PACKET from Node 0x%x to Node 0x%x (last sent by 0x%x)", header, np->node_id, nodeDB->getNodeNum(), np->last_sent_by_id); - LOG_DEBUG("Packet contains %d neighbors", np->neighbors_count); - for (int i = 0; i < np->neighbors_count; i++) { - LOG_DEBUG("Neighbor %d: node_id=0x%x, snr=%.2f", i, np->neighbors[i].node_id, np->neighbors[i].snr); - } +void NeighborInfoModule::printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np) +{ + LOG_DEBUG("%s NEIGHBORINFO PACKET from Node 0x%x to Node 0x%x (last sent by 0x%x)", header, np->node_id, nodeDB->getNodeNum(), + np->last_sent_by_id); + LOG_DEBUG("Packet contains %d neighbors", np->neighbors_count); + for (int i = 0; i < np->neighbors_count; i++) { + LOG_DEBUG("Neighbor %d: node_id=0x%x, snr=%.2f", i, np->neighbors[i].node_id, np->neighbors[i].snr); + } } /* Prints the nodeDB neighbors NOTE: for debugging only */ -void NeighborInfoModule::printNodeDBNeighbors() { - LOG_DEBUG("Our NodeDB contains %d neighbors", neighbors.size()); - for (size_t i = 0; i < neighbors.size(); i++) { - LOG_DEBUG("Node %d: node_id=0x%x, snr=%.2f", i, neighbors[i].node_id, neighbors[i].snr); - } +void NeighborInfoModule::printNodeDBNeighbors() +{ + LOG_DEBUG("Our NodeDB contains %d neighbors", neighbors.size()); + for (size_t i = 0; i < neighbors.size(); i++) { + LOG_DEBUG("Node %d: node_id=0x%x, snr=%.2f", i, neighbors[i].node_id, neighbors[i].snr); + } } /* Send our initial owner announcement 35 seconds after we start (to give * network time to setup) */ NeighborInfoModule::NeighborInfoModule() - : ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg), concurrency::OSThread("NeighborInfo") { - ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + : ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg), + concurrency::OSThread("NeighborInfo") +{ + ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); - if (moduleConfig.neighbor_info.enabled) { - isPromiscuous = true; // Update neighbors from all packets - setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs)); - } else { - LOG_DEBUG("NeighborInfoModule is disabled"); - disable(); - } + if (moduleConfig.neighbor_info.enabled) { + isPromiscuous = true; // Update neighbors from all packets + setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, + default_telemetry_broadcast_interval_secs)); + } else { + LOG_DEBUG("NeighborInfoModule is disabled"); + disable(); + } } /* @@ -52,183 +58,198 @@ Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time Assumes that the neighborInfo packet has been allocated @returns the number of entries collected */ -uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo) { - NodeNum my_node_id = nodeDB->getNodeNum(); - neighborInfo->node_id = my_node_id; - neighborInfo->last_sent_by_id = my_node_id; - neighborInfo->node_broadcast_interval_secs = - Default::getConfiguredOrDefault(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs); +uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo) +{ + NodeNum my_node_id = nodeDB->getNodeNum(); + neighborInfo->node_id = my_node_id; + neighborInfo->last_sent_by_id = my_node_id; + neighborInfo->node_broadcast_interval_secs = + Default::getConfiguredOrDefault(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs); - cleanUpNeighbors(); + cleanUpNeighbors(); - for (auto nbr : neighbors) { - if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) { - neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id; - neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr; - // Note: we don't set the last_rx_time and node_broadcast_intervals_secs - // here, because we don't want to send this over the mesh - neighborInfo->neighbors_count++; + for (auto nbr : neighbors) { + if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) { + neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id; + neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr; + // Note: we don't set the last_rx_time and node_broadcast_intervals_secs + // here, because we don't want to send this over the mesh + neighborInfo->neighbors_count++; + } } - } - printNodeDBNeighbors(); - return neighborInfo->neighbors_count; + printNodeDBNeighbors(); + return neighborInfo->neighbors_count; } /* Remove neighbors from the database that we haven't heard from in a while */ -void NeighborInfoModule::cleanUpNeighbors() { - uint32_t now = getTime(); - NodeNum my_node_id = nodeDB->getNodeNum(); - for (auto it = neighbors.rbegin(); it != neighbors.rend();) { - // We will remove a neighbor if we haven't heard from them in twice the - // broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is - // seconds since 1970 - if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) { - LOG_DEBUG("Remove neighbor with node ID 0x%x", it->node_id); - it = std::vector::reverse_iterator(neighbors.erase(std::next(it).base())); // Erase the element and update the iterator - } else { - ++it; +void NeighborInfoModule::cleanUpNeighbors() +{ + uint32_t now = getTime(); + NodeNum my_node_id = nodeDB->getNodeNum(); + for (auto it = neighbors.rbegin(); it != neighbors.rend();) { + // We will remove a neighbor if we haven't heard from them in twice the + // broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is + // seconds since 1970 + if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) { + LOG_DEBUG("Remove neighbor with node ID 0x%x", it->node_id); + it = std::vector::reverse_iterator( + neighbors.erase(std::next(it).base())); // Erase the element and update the iterator + } else { + ++it; + } } - } } /* Send neighbor info to the mesh */ -void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) { - meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; - collectNeighborInfo(&neighborInfo); - // only send neighbours if we have some to send - if (neighborInfo.neighbors_count > 0) { - meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); - p->to = dest; - p->decoded.want_response = wantReplies; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - printNeighborInfo("SENDING", &neighborInfo); - service->sendToMesh(p, RX_SRC_LOCAL, true); - } +void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) +{ + meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; + collectNeighborInfo(&neighborInfo); + // only send neighbours if we have some to send + if (neighborInfo.neighbors_count > 0) { + meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); + p->to = dest; + p->decoded.want_response = wantReplies; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + printNeighborInfo("SENDING", &neighborInfo); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } } /* Encompasses the full construction and sending packet to mesh Will be used for broadcast. */ -int32_t NeighborInfoModule::runOnce() { - if (moduleConfig.neighbor_info.transmit_over_lora && - (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) && - airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { - sendNeighborInfo(NODENUM_BROADCAST, false); - } else { - sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false); - } - return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs); +int32_t NeighborInfoModule::runOnce() +{ + if (moduleConfig.neighbor_info.transmit_over_lora && + (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) && + airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { + sendNeighborInfo(NODENUM_BROADCAST, false); + } else { + sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false); + } + return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs); } -meshtastic_MeshPacket *NeighborInfoModule::allocReply() { - LOG_INFO("NeighborInfoRequested."); - if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { - LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return nullptr; - } +meshtastic_MeshPacket *NeighborInfoModule::allocReply() +{ + LOG_INFO("NeighborInfoRequested."); + if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return nullptr; + } - meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; - collectNeighborInfo(&neighborInfo); + meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; + collectNeighborInfo(&neighborInfo); - meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo); + meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo); - if (reply) { - lastSentReply = millis(); // Track when we sent this reply - } - return reply; + if (reply) { + lastSentReply = millis(); // Track when we sent this reply + } + return reply; } /* Collect a received neighbor info packet from another node Pass it to an upper client; do not persist this data on the mesh */ -bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np) { - LOG_DEBUG("NeighborInfo: handleReceivedProtobuf"); - if (np) { - printNeighborInfo("RECEIVED", np); - // Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0 - if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) { - LOG_DEBUG(" Updating neighbours"); - updateNeighbors(mp, np); - } else { - LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); +bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np) +{ + LOG_DEBUG("NeighborInfo: handleReceivedProtobuf"); + if (np) { + printNeighborInfo("RECEIVED", np); + // Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0 + if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) { + LOG_DEBUG(" Updating neighbours"); + updateNeighbors(mp, np); + } else { + LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); + } + } else if (getHopsAway(mp) == 0) { + LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); + // If the hopLimit is the same as hopStart, then it is a neighbor + getOrCreateNeighbor(mp.from, mp.from, 0, + mp.rx_snr); // Set the broadcast interval to 0, as we don't know it } - } else if (getHopsAway(mp) == 0) { - LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); - // If the hopLimit is the same as hopStart, then it is a neighbor - getOrCreateNeighbor(mp.from, mp.from, 0, - mp.rx_snr); // Set the broadcast interval to 0, as we don't know it - } - // Allow others to handle this packet - return false; + // Allow others to handle this packet + return false; } /* Copy the content of a current NeighborInfo packet into a new one and update the last_sent_by_id to our NodeNum */ -void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) { - n->last_sent_by_id = nodeDB->getNodeNum(); +void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) +{ + n->last_sent_by_id = nodeDB->getNodeNum(); - // Set updated last_sent_by_id to the payload of the to be flooded packet - p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, n); + // Set updated last_sent_by_id to the payload of the to be flooded packet + p.decoded.payload.size = + pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, n); } -void NeighborInfoModule::resetNeighbors() { neighbors.clear(); } - -void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np) { - LOG_DEBUG("updateNeighbors"); - // The last sent ID will be 0 if the packet is from the phone, which we don't - // count as an edge. So we assume that if it's zero, then this packet is from - // our node. - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { - getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr); - } +void NeighborInfoModule::resetNeighbors() +{ + neighbors.clear(); } -meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr) { - // our node and the phone are the same node (not neighbors) - if (n == 0) { - n = nodeDB->getNodeNum(); - } - // look for one in the existing list - for (size_t i = 0; i < neighbors.size(); i++) { - if (neighbors[i].node_id == n) { - // if found, update it - neighbors[i].snr = snr; - neighbors[i].last_rx_time = getTime(); - // Only if this is the original sender, the broadcast interval corresponds - // to it - if (originalSender == n && node_broadcast_interval_secs != 0) - neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs; - return &neighbors[i]; +void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np) +{ + LOG_DEBUG("updateNeighbors"); + // The last sent ID will be 0 if the packet is from the phone, which we don't + // count as an edge. So we assume that if it's zero, then this packet is from + // our node. + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { + getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr); } - } - // otherwise, allocate one and assign data to it - - meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero; - new_nbr.node_id = n; - new_nbr.snr = snr; - new_nbr.last_rx_time = getTime(); - // Only if this is the original sender, the broadcast interval corresponds to - // it - if (originalSender == n && node_broadcast_interval_secs != 0) - new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs; - else // Assume the same broadcast interval as us for the neighbor if we don't - // know it - new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval; - - if (neighbors.size() < MAX_NUM_NEIGHBORS) { - neighbors.push_back(new_nbr); - } else { - // If we have too many neighbors, replace the oldest one - LOG_WARN("Neighbor DB is full, replace oldest neighbor"); - neighbors.erase(neighbors.begin()); - neighbors.push_back(new_nbr); - } - return &neighbors.back(); +} + +meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n, + uint32_t node_broadcast_interval_secs, float snr) +{ + // our node and the phone are the same node (not neighbors) + if (n == 0) { + n = nodeDB->getNodeNum(); + } + // look for one in the existing list + for (size_t i = 0; i < neighbors.size(); i++) { + if (neighbors[i].node_id == n) { + // if found, update it + neighbors[i].snr = snr; + neighbors[i].last_rx_time = getTime(); + // Only if this is the original sender, the broadcast interval corresponds + // to it + if (originalSender == n && node_broadcast_interval_secs != 0) + neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs; + return &neighbors[i]; + } + } + // otherwise, allocate one and assign data to it + + meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero; + new_nbr.node_id = n; + new_nbr.snr = snr; + new_nbr.last_rx_time = getTime(); + // Only if this is the original sender, the broadcast interval corresponds to + // it + if (originalSender == n && node_broadcast_interval_secs != 0) + new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs; + else // Assume the same broadcast interval as us for the neighbor if we don't + // know it + new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval; + + if (neighbors.size() < MAX_NUM_NEIGHBORS) { + neighbors.push_back(new_nbr); + } else { + // If we have too many neighbors, replace the oldest one + LOG_WARN("Neighbor DB is full, replace oldest neighbor"); + neighbors.erase(neighbors.begin()); + neighbors.push_back(new_nbr); + } + return &neighbors.back(); } diff --git a/src/modules/NeighborInfoModule.h b/src/modules/NeighborInfoModule.h index 0771f892b..abb530329 100644 --- a/src/modules/NeighborInfoModule.h +++ b/src/modules/NeighborInfoModule.h @@ -5,72 +5,73 @@ /* * Neighborinfo module for sending info on each node's 0-hop neighbors to the mesh */ -class NeighborInfoModule : public ProtobufModule, private concurrency::OSThread { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &NeighborInfoModule::handleStatusUpdate); +class NeighborInfoModule : public ProtobufModule, private concurrency::OSThread +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &NeighborInfoModule::handleStatusUpdate); - std::vector neighbors; + std::vector neighbors; -public: - /* - * Expose the constructor - */ - NeighborInfoModule(); + public: + /* + * Expose the constructor + */ + NeighborInfoModule(); - /* Reset neighbor info after clearing nodeDB*/ - void resetNeighbors(); + /* Reset neighbor info after clearing nodeDB*/ + void resetNeighbors(); -protected: - /* - * Called to handle a particular incoming message - * @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *nb) override; + protected: + /* + * Called to handle a particular incoming message + * @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *nb) override; - /* Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /* Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /* - * Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time - * @return the number of entries collected - */ - uint32_t collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo); + /* + * Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time + * @return the number of entries collected + */ + uint32_t collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo); - /* - Remove neighbors from the database that we haven't heard from in a while - */ - void cleanUpNeighbors(); + /* + Remove neighbors from the database that we haven't heard from in a while + */ + void cleanUpNeighbors(); - /* Allocate a new NeighborInfo packet */ - meshtastic_NeighborInfo *allocateNeighborInfoPacket(); + /* Allocate a new NeighborInfo packet */ + meshtastic_NeighborInfo *allocateNeighborInfoPacket(); - // Find a neighbor in our DB, create an empty neighbor if missing - meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr); + // Find a neighbor in our DB, create an empty neighbor if missing + meshtastic_Neighbor *getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr); - /* - * Send info on our node's neighbors into the mesh - */ - void sendNeighborInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /* + * Send info on our node's neighbors into the mesh + */ + void sendNeighborInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - /* update neighbors with subpacket sniffed from network */ - void updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np); + /* update neighbors with subpacket sniffed from network */ + void updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np); - /* update a NeighborInfo packet with our NodeNum as last_sent_by_id */ - void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) override; + /* update a NeighborInfo packet with our NodeNum as last_sent_by_id */ + void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n) override; - /* Does our periodic broadcast */ - int32_t runOnce() override; + /* Does our periodic broadcast */ + int32_t runOnce() override; - /* Override wantPacket to say we want to see all packets when enabled, not just those for our port number. - Exception is when the packet came via MQTT */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return enabled && !p->via_mqtt; } + /* Override wantPacket to say we want to see all packets when enabled, not just those for our port number. + Exception is when the packet came via MQTT */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return enabled && !p->via_mqtt; } - /* These are for debugging only */ - void printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np); - void printNodeDBNeighbors(); + /* These are for debugging only */ + void printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np); + void printNodeDBNeighbors(); -private: - uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) + private: + uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) }; extern NeighborInfoModule *neighborInfoModule; \ No newline at end of file diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 019e5a522..7db8b66cc 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -17,182 +17,190 @@ NodeInfoModule *nodeInfoModule; static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS; -bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { - suppressReplyForCurrentRequest = false; - - if (mp.from == nodeDB->getNodeNum()) { - LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); - return false; - } - - auto p = *pptr; - - if (mp.decoded.want_response) { - const NodeNum sender = getFrom(&mp); - const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); - auto it = lastNodeInfoSeen.find(sender); - if (it != lastNodeInfoSeen.end()) { - uint32_t sinceLast = now >= it->second ? now - it->second : 0; - if (sinceLast < NodeInfoReplySuppressSeconds) { - suppressReplyForCurrentRequest = true; - } - } - lastNodeInfoSeen[sender] = now; - pruneLastNodeInfoCache(); - } - - if (p.is_licensed != owner.is_licensed) { - LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); - return true; - } - - // Coerce user.id to be derived from the node number - snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); - - bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); - - bool wasBroadcast = isBroadcast(mp.to); - - // LOG_DEBUG("did encode"); - // if user has changed while packet was not for us, inform phone - if (hasChanged && !wasBroadcast && !isToUs(&mp)) { - auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis - - // Re-encode the user protobuf, as we have stripped out the user.id - packetCopy->decoded.payload.size = - pb_encode_to_bytes(packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p); - - service->sendToPhone(packetCopy); - } - - pruneLastNodeInfoCache(); - - // LOG_DEBUG("did handleReceived"); - return false; // Let others look at this message also if they want -} - -void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) { - // Coerce user.id to be derived from the node number - snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp)); - - // Re-encode the altered protobuf back into the packet - mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p); -} - -void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout) { - // cancel any not yet sent (now stale) position packets - if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) - service->cancelSending(prevPacketId); - shorterTimeout = _shorterTimeout; - DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p = allocReply(); - DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); - - if (p) { // Check whether we didn't ignore it - p->to = dest; - bool requestWantResponse = - (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - wantReplies; - - p->decoded.want_response = requestWantResponse; - if (_shorterTimeout) - p->priority = meshtastic_MeshPacket_Priority_DEFAULT; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - if (channel > 0) { - LOG_DEBUG("Send ourNodeInfo to channel %d", channel); - p->channel = channel; - } - - prevPacketId = p->id; - - service->sendToMesh(p); - shorterTimeout = false; - } -} - -meshtastic_MeshPacket *NodeInfoModule::allocReply() { - if (suppressReplyForCurrentRequest) { - LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); - ignoreRequest = true; +bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) +{ suppressReplyForCurrentRequest = false; - return NULL; - } - if (!airTime->isTxAllowedChannelUtil(false)) { - ignoreRequest = true; // Mark it as ignored for MeshModule - LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); - return NULL; - } - // If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway. - if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) { - LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return NULL; - } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { - LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return NULL; - } else { - ignoreRequest = false; // Don't ignore requests anymore - meshtastic_User &u = owner; - - // Strip the public key if the user is licensed - if (u.is_licensed && u.public_key.size > 0) { - u.public_key.bytes[0] = 0; - u.public_key.size = 0; + if (mp.from == nodeDB->getNodeNum()) { + LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); + return false; } - // FIXME: Clear the user.id field since it should be derived from node number on the receiving end - // u.id[0] = '\0'; + auto p = *pptr; - // Ensure our user.id is derived correctly - strcpy(u.id, nodeDB->getNodeId().c_str()); + if (mp.decoded.want_response) { + const NodeNum sender = getFrom(&mp); + const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); + auto it = lastNodeInfoSeen.find(sender); + if (it != lastNodeInfoSeen.end()) { + uint32_t sinceLast = now >= it->second ? now - it->second : 0; + if (sinceLast < NodeInfoReplySuppressSeconds) { + suppressReplyForCurrentRequest = true; + } + } + lastNodeInfoSeen[sender] = now; + pruneLastNodeInfoCache(); + } - LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); - lastSentToMesh = millis(); - return allocDataProtobuf(u); - } + if (p.is_licensed != owner.is_licensed) { + LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); + return true; + } + + // Coerce user.id to be derived from the node number + snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); + + bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); + + bool wasBroadcast = isBroadcast(mp.to); + + // LOG_DEBUG("did encode"); + // if user has changed while packet was not for us, inform phone + if (hasChanged && !wasBroadcast && !isToUs(&mp)) { + auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis + + // Re-encode the user protobuf, as we have stripped out the user.id + packetCopy->decoded.payload.size = pb_encode_to_bytes( + packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p); + + service->sendToPhone(packetCopy); + } + + pruneLastNodeInfoCache(); + + // LOG_DEBUG("did handleReceived"); + return false; // Let others look at this message also if they want } -void NodeInfoModule::pruneLastNodeInfoCache() { - if (!nodeDB || !nodeDB->meshNodes) - return; +void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) +{ + // Coerce user.id to be derived from the node number + snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp)); - const size_t maxEntries = nodeDB->meshNodes->size(); + // Re-encode the altered protobuf back into the packet + mp.decoded.payload.size = + pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p); +} - for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { - if (!nodeDB->getMeshNode(it->first)) { - it = lastNodeInfoSeen.erase(it); - } else { - ++it; +void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout) +{ + // cancel any not yet sent (now stale) position packets + if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) + service->cancelSending(prevPacketId); + shorterTimeout = _shorterTimeout; + DEBUG_HEAP_BEFORE; + meshtastic_MeshPacket *p = allocReply(); + DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); + + if (p) { // Check whether we didn't ignore it + p->to = dest; + bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + wantReplies; + + p->decoded.want_response = requestWantResponse; + if (_shorterTimeout) + p->priority = meshtastic_MeshPacket_Priority_DEFAULT; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + if (channel > 0) { + LOG_DEBUG("Send ourNodeInfo to channel %d", channel); + p->channel = channel; + } + + prevPacketId = p->id; + + service->sendToMesh(p); + shorterTimeout = false; } - } +} - while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { - auto oldestIt = std::min_element( - lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), - [](const std::pair &lhs, const std::pair &rhs) { return lhs.second < rhs.second; }); - lastNodeInfoSeen.erase(oldestIt); - } +meshtastic_MeshPacket *NodeInfoModule::allocReply() +{ + if (suppressReplyForCurrentRequest) { + LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); + ignoreRequest = true; + suppressReplyForCurrentRequest = false; + return NULL; + } + + if (!airTime->isTxAllowedChannelUtil(false)) { + ignoreRequest = true; // Mark it as ignored for MeshModule + LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); + return NULL; + } + // If we sent our NodeInfo less than 5 min. ago, don't send it again as it may be still underway. + if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 5 * 60 * 1000)) { + LOG_DEBUG("Skip send NodeInfo since we sent it <5min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return NULL; + } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { + LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return NULL; + } else { + ignoreRequest = false; // Don't ignore requests anymore + meshtastic_User &u = owner; + + // Strip the public key if the user is licensed + if (u.is_licensed && u.public_key.size > 0) { + u.public_key.bytes[0] = 0; + u.public_key.size = 0; + } + + // FIXME: Clear the user.id field since it should be derived from node number on the receiving end + // u.id[0] = '\0'; + + // Ensure our user.id is derived correctly + strcpy(u.id, nodeDB->getNodeId().c_str()); + + LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); + lastSentToMesh = millis(); + return allocDataProtobuf(u); + } +} + +void NodeInfoModule::pruneLastNodeInfoCache() +{ + if (!nodeDB || !nodeDB->meshNodes) + return; + + const size_t maxEntries = nodeDB->meshNodes->size(); + + for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { + if (!nodeDB->getMeshNode(it->first)) { + it = lastNodeInfoSeen.erase(it); + } else { + ++it; + } + } + + while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { + auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), + [](const std::pair &lhs, + const std::pair &rhs) { return lhs.second < rhs.second; }); + lastNodeInfoSeen.erase(oldestIt); + } } NodeInfoModule::NodeInfoModule() - : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") { - isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others + : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") +{ + isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others - setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds - // after we start (to give network time to setup) + setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds + // after we start (to give network time to setup) } -int32_t NodeInfoModule::runOnce() { - // If we changed channels, ask everyone else for their latest info - bool requestReplies = currentGeneration != radioGeneration; - currentGeneration = radioGeneration; +int32_t NodeInfoModule::runOnce() +{ + // If we changed channels, ask everyone else for their latest info + bool requestReplies = currentGeneration != radioGeneration; + currentGeneration = radioGeneration; - if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { - LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies); - sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies) - } - return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); + if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { + LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies); + sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies) + } + return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); } \ No newline at end of file diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index 4e9b7e0b2..d16fbeac2 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -5,47 +5,49 @@ /** * NodeInfo module for sending/receiving NodeInfos into the mesh */ -class NodeInfoModule : public ProtobufModule, private concurrency::OSThread { - /// The id of the last packet we sent, to allow us to cancel it if we make something fresher - PacketId prevPacketId = 0; +class NodeInfoModule : public ProtobufModule, private concurrency::OSThread +{ + /// The id of the last packet we sent, to allow us to cancel it if we make something fresher + PacketId prevPacketId = 0; - uint32_t currentGeneration = 0; + uint32_t currentGeneration = 0; -public: - /** Constructor - * name is for debugging output - */ - NodeInfoModule(); + public: + /** Constructor + * name is for debugging output + */ + NodeInfoModule(); - /** - * Send our NodeInfo into the mesh - */ - void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, bool _shorterTimeout = false); + /** + * Send our NodeInfo into the mesh + */ + void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, + bool _shorterTimeout = false); -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *p) override; - /** Called to alter received User protobuf */ - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) override; + /** Called to alter received User protobuf */ + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) override; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /** Does our periodic broadcast */ - virtual int32_t runOnce() override; + /** Does our periodic broadcast */ + virtual int32_t runOnce() override; -private: - uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh - bool shorterTimeout = false; - bool suppressReplyForCurrentRequest = false; - std::map lastNodeInfoSeen; + private: + uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh + bool shorterTimeout = false; + bool suppressReplyForCurrentRequest = false; + std::map lastNodeInfoSeen; - void pruneLastNodeInfoCache(); + void pruneLastNodeInfoCache(); }; extern NodeInfoModule *nodeInfoModule; diff --git a/src/modules/OnScreenKeyboardModule.cpp b/src/modules/OnScreenKeyboardModule.cpp index a67457072..e75d926bf 100644 --- a/src/modules/OnScreenKeyboardModule.cpp +++ b/src/modules/OnScreenKeyboardModule.cpp @@ -9,244 +9,262 @@ #include #include -namespace graphics { +namespace graphics +{ -OnScreenKeyboardModule &OnScreenKeyboardModule::instance() { - static OnScreenKeyboardModule inst; - return inst; +OnScreenKeyboardModule &OnScreenKeyboardModule::instance() +{ + static OnScreenKeyboardModule inst; + return inst; } -OnScreenKeyboardModule::~OnScreenKeyboardModule() { - if (keyboard) { - delete keyboard; - keyboard = nullptr; - } -} - -void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs, std::function cb) { - if (keyboard) { - delete keyboard; - keyboard = nullptr; - } - keyboard = new VirtualKeyboard(); - callback = cb; - if (header) - keyboard->setHeader(header); - if (initialText) - keyboard->setInputText(initialText); - - // Route VK submission/cancel events back into the module - keyboard->setCallback([this](const std::string &text) { - if (text.empty()) { - this->onCancel(); - } else { - this->onSubmit(text); +OnScreenKeyboardModule::~OnScreenKeyboardModule() +{ + if (keyboard) { + delete keyboard; + keyboard = nullptr; } - }); - - // Maintain legacy compatibility hooks - NotificationRenderer::virtualKeyboard = keyboard; - NotificationRenderer::textInputCallback = callback; } -void OnScreenKeyboardModule::stop(bool callEmptyCallback) { - auto cb = callback; - callback = nullptr; - if (keyboard) { - delete keyboard; - keyboard = nullptr; - } - // Keep NotificationRenderer legacy pointers in sync - NotificationRenderer::virtualKeyboard = nullptr; - NotificationRenderer::textInputCallback = nullptr; - clearPopup(); - if (callEmptyCallback && cb) - cb(""); -} +void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs, + std::function cb) +{ + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + keyboard = new VirtualKeyboard(); + callback = cb; + if (header) + keyboard->setHeader(header); + if (initialText) + keyboard->setInputText(initialText); -void OnScreenKeyboardModule::handleInput(const InputEvent &event) { - if (!keyboard) - return; - - if (processVirtualKeyboardInput(event, keyboard)) - return; - - if (event.inputEvent == INPUT_BROKER_CANCEL) - onCancel(); -} - -bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard) { - if (!targetKeyboard) - return false; - - switch (event.inputEvent) { - case INPUT_BROKER_UP: - case INPUT_BROKER_UP_LONG: - targetKeyboard->moveCursorUp(); - return true; - case INPUT_BROKER_DOWN: - case INPUT_BROKER_DOWN_LONG: - targetKeyboard->moveCursorDown(); - return true; - case INPUT_BROKER_LEFT: - case INPUT_BROKER_ALT_PRESS: - targetKeyboard->moveCursorLeft(); - return true; - case INPUT_BROKER_RIGHT: - case INPUT_BROKER_USER_PRESS: - targetKeyboard->moveCursorRight(); - return true; - case INPUT_BROKER_SELECT: - targetKeyboard->handlePress(); - return true; - case INPUT_BROKER_SELECT_LONG: - targetKeyboard->handleLongPress(); - return true; - default: - return false; - } -} - -bool OnScreenKeyboardModule::draw(OLEDDisplay *display) { - if (!keyboard) - return false; - - // Timeout - if (keyboard->isTimedOut()) { - onCancel(); - return false; - } - - // Clear full screen behind keyboard - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - keyboard->draw(display, 0, 0); - - // Draw popup overlay if needed - drawPopup(display); - return true; -} - -void OnScreenKeyboardModule::onSubmit(const std::string &text) { - auto cb = callback; - stop(false); - if (cb) - cb(text); -} - -void OnScreenKeyboardModule::onCancel() { stop(true); } - -void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) { - if (!title || !content) - return; - strncpy(popupTitle, title, sizeof(popupTitle) - 1); - popupTitle[sizeof(popupTitle) - 1] = '\0'; - strncpy(popupMessage, content, sizeof(popupMessage) - 1); - popupMessage[sizeof(popupMessage) - 1] = '\0'; - popupUntil = millis() + durationMs; - popupVisible = true; -} - -void OnScreenKeyboardModule::clearPopup() { - popupTitle[0] = '\0'; - popupMessage[0] = '\0'; - popupUntil = 0; - popupVisible = false; -} - -void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display) { - // Only render the popup overlay (without drawing the keyboard) - drawPopup(display); -} - -void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) { - if (!popupVisible) - return; - if (millis() > popupUntil || popupMessage[0] == '\0') { - popupVisible = false; - return; - } - - // Build lines and leverage NotificationRenderer inverted box drawing for consistent style - constexpr uint16_t maxContentLines = 3; - const bool hasTitle = popupTitle[0] != '\0'; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const uint16_t maxWrapWidth = display->width() - 40; - - auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { - std::vector wrapped; - std::string current; - std::string word; - const char *p = text; - while (*p && wrapped.size() < maxContentLines) { - while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { - if (*p == '\n') { - if (!current.empty()) { - wrapped.push_back(current); - current.clear(); - if (wrapped.size() >= maxContentLines) - break; - } - } - ++p; - } - if (!*p || wrapped.size() >= maxContentLines) - break; - word.clear(); - while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') - word += *p++; - if (word.empty()) - continue; - std::string test = current.empty() ? word : (current + " " + word); - uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); - if (w <= availableWidth) - current = test; - else { - if (!current.empty()) { - wrapped.push_back(current); - current = word; - if (wrapped.size() >= maxContentLines) - break; + // Route VK submission/cancel events back into the module + keyboard->setCallback([this](const std::string &text) { + if (text.empty()) { + this->onCancel(); } else { - current = word; - while (current.size() > 1 && display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) - current.pop_back(); + this->onSubmit(text); } - } + }); + + // Maintain legacy compatibility hooks + NotificationRenderer::virtualKeyboard = keyboard; + NotificationRenderer::textInputCallback = callback; +} + +void OnScreenKeyboardModule::stop(bool callEmptyCallback) +{ + auto cb = callback; + callback = nullptr; + if (keyboard) { + delete keyboard; + keyboard = nullptr; } - if (!current.empty() && wrapped.size() < maxContentLines) - wrapped.push_back(current); - return wrapped; - }; + // Keep NotificationRenderer legacy pointers in sync + NotificationRenderer::virtualKeyboard = nullptr; + NotificationRenderer::textInputCallback = nullptr; + clearPopup(); + if (callEmptyCallback && cb) + cb(""); +} - std::vector allLines; - if (hasTitle) - allLines.emplace_back(popupTitle); +void OnScreenKeyboardModule::handleInput(const InputEvent &event) +{ + if (!keyboard) + return; - char buf[sizeof(popupMessage)]; - strncpy(buf, popupMessage, sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - char *paragraph = strtok(buf, "\n"); - while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { - auto wrapped = wrapText(paragraph, maxWrapWidth); - for (const auto &ln : wrapped) { - if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) - break; - allLines.push_back(ln); + if (processVirtualKeyboardInput(event, keyboard)) + return; + + if (event.inputEvent == INPUT_BROKER_CANCEL) + onCancel(); +} + +bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard) +{ + if (!targetKeyboard) + return false; + + switch (event.inputEvent) { + case INPUT_BROKER_UP: + case INPUT_BROKER_UP_LONG: + targetKeyboard->moveCursorUp(); + return true; + case INPUT_BROKER_DOWN: + case INPUT_BROKER_DOWN_LONG: + targetKeyboard->moveCursorDown(); + return true; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_ALT_PRESS: + targetKeyboard->moveCursorLeft(); + return true; + case INPUT_BROKER_RIGHT: + case INPUT_BROKER_USER_PRESS: + targetKeyboard->moveCursorRight(); + return true; + case INPUT_BROKER_SELECT: + targetKeyboard->handlePress(); + return true; + case INPUT_BROKER_SELECT_LONG: + targetKeyboard->handleLongPress(); + return true; + default: + return false; } - paragraph = strtok(nullptr, "\n"); - } +} - std::vector ptrs; - for (const auto &ln : allLines) - ptrs.push_back(ln.c_str()); - ptrs.push_back(nullptr); +bool OnScreenKeyboardModule::draw(OLEDDisplay *display) +{ + if (!keyboard) + return false; - // Use the standard notification box drawing from NotificationRenderer - NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); + // Timeout + if (keyboard->isTimedOut()) { + onCancel(); + return false; + } + + // Clear full screen behind keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + keyboard->draw(display, 0, 0); + + // Draw popup overlay if needed + drawPopup(display); + return true; +} + +void OnScreenKeyboardModule::onSubmit(const std::string &text) +{ + auto cb = callback; + stop(false); + if (cb) + cb(text); +} + +void OnScreenKeyboardModule::onCancel() +{ + stop(true); +} + +void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) +{ + if (!title || !content) + return; + strncpy(popupTitle, title, sizeof(popupTitle) - 1); + popupTitle[sizeof(popupTitle) - 1] = '\0'; + strncpy(popupMessage, content, sizeof(popupMessage) - 1); + popupMessage[sizeof(popupMessage) - 1] = '\0'; + popupUntil = millis() + durationMs; + popupVisible = true; +} + +void OnScreenKeyboardModule::clearPopup() +{ + popupTitle[0] = '\0'; + popupMessage[0] = '\0'; + popupUntil = 0; + popupVisible = false; +} + +void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display) +{ + // Only render the popup overlay (without drawing the keyboard) + drawPopup(display); +} + +void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) +{ + if (!popupVisible) + return; + if (millis() > popupUntil || popupMessage[0] == '\0') { + popupVisible = false; + return; + } + + // Build lines and leverage NotificationRenderer inverted box drawing for consistent style + constexpr uint16_t maxContentLines = 3; + const bool hasTitle = popupTitle[0] != '\0'; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const uint16_t maxWrapWidth = display->width() - 40; + + auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { + std::vector wrapped; + std::string current; + std::string word; + const char *p = text; + while (*p && wrapped.size() < maxContentLines) { + while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { + if (*p == '\n') { + if (!current.empty()) { + wrapped.push_back(current); + current.clear(); + if (wrapped.size() >= maxContentLines) + break; + } + } + ++p; + } + if (!*p || wrapped.size() >= maxContentLines) + break; + word.clear(); + while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') + word += *p++; + if (word.empty()) + continue; + std::string test = current.empty() ? word : (current + " " + word); + uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); + if (w <= availableWidth) + current = test; + else { + if (!current.empty()) { + wrapped.push_back(current); + current = word; + if (wrapped.size() >= maxContentLines) + break; + } else { + current = word; + while (current.size() > 1 && + display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) + current.pop_back(); + } + } + } + if (!current.empty() && wrapped.size() < maxContentLines) + wrapped.push_back(current); + return wrapped; + }; + + std::vector allLines; + if (hasTitle) + allLines.emplace_back(popupTitle); + + char buf[sizeof(popupMessage)]; + strncpy(buf, popupMessage, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + char *paragraph = strtok(buf, "\n"); + while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { + auto wrapped = wrapText(paragraph, maxWrapWidth); + for (const auto &ln : wrapped) { + if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) + break; + allLines.push_back(ln); + } + paragraph = strtok(nullptr, "\n"); + } + + std::vector ptrs; + for (const auto &ln : allLines) + ptrs.push_back(ln.c_str()); + ptrs.push_back(nullptr); + + // Use the standard notification box drawing from NotificationRenderer + NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); } } // namespace graphics diff --git a/src/modules/OnScreenKeyboardModule.h b/src/modules/OnScreenKeyboardModule.h index edc2b6da9..f86b71ec3 100644 --- a/src/modules/OnScreenKeyboardModule.h +++ b/src/modules/OnScreenKeyboardModule.h @@ -9,42 +9,45 @@ #include #include -namespace graphics { -class OnScreenKeyboardModule { -public: - static OnScreenKeyboardModule &instance(); +namespace graphics +{ +class OnScreenKeyboardModule +{ + public: + static OnScreenKeyboardModule &instance(); - void start(const char *header, const char *initialText, uint32_t durationMs, std::function callback); + void start(const char *header, const char *initialText, uint32_t durationMs, + std::function callback); - void stop(bool callEmptyCallback); + void stop(bool callEmptyCallback); - void handleInput(const InputEvent &event); - static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); - bool draw(OLEDDisplay *display); + void handleInput(const InputEvent &event); + static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); + bool draw(OLEDDisplay *display); - void showPopup(const char *title, const char *content, uint32_t durationMs); - void clearPopup(); - // Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard) - void drawPopupOverlay(OLEDDisplay *display); + void showPopup(const char *title, const char *content, uint32_t durationMs); + void clearPopup(); + // Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard) + void drawPopupOverlay(OLEDDisplay *display); -private: - OnScreenKeyboardModule() = default; - ~OnScreenKeyboardModule(); - OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; - OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; + private: + OnScreenKeyboardModule() = default; + ~OnScreenKeyboardModule(); + OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; + OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; - void onSubmit(const std::string &text); - void onCancel(); + void onSubmit(const std::string &text); + void onCancel(); - void drawPopup(OLEDDisplay *display); + void drawPopup(OLEDDisplay *display); - VirtualKeyboard *keyboard = nullptr; - std::function callback; + VirtualKeyboard *keyboard = nullptr; + std::function callback; - char popupTitle[64] = {0}; - char popupMessage[256] = {0}; - uint32_t popupUntil = 0; - bool popupVisible = false; + char popupTitle[64] = {0}; + char popupMessage[256] = {0}; + uint32_t popupUntil = 0; + bool popupVisible = false; }; } // namespace graphics diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 947ac46ff..f7116e701 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -21,512 +21,543 @@ PositionModule *positionModule; PositionModule::PositionModule() - : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread("Position") { - precision = 0; // safe starting value - isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread("Position") +{ + precision = 0; // safe starting value + isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others + nodeStatusObserver.observe(&nodeStatus->onNewStatus); - if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { - setIntervalFromNow(setStartDelay()); - } - - // Power saving trackers should clear their position on startup to avoid waking up and sending a stale position - if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && - config.power.is_power_saving) { - LOG_DEBUG("Clear position on startup for sleepy tracker (ー。ー) zzz"); - nodeDB->clearLocalPosition(); - } -} - -bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr) { - auto p = *pptr; - - const auto transport = mp.transport_mechanism; - if (isFromUs(&mp) && - !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { - LOG_WARN("Ignoring packet supposedly from us over external transport"); - return true; - } - - // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) - // to set fixed location, EUD-GPS location or just the time (see also issue #900) - bool isLocal = false; - if (isFromUs(&mp)) { - isLocal = true; - if (config.position.fixed_position) { - LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true"); - -#ifdef T_WATCH_S3 - // Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the - // client device here - if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { - trySetRtc(p, isLocal, true); - } -#endif - - nodeDB->setLocalPosition(p, true); - return false; - } else { - LOG_DEBUG("Incoming update from MYSELF"); - nodeDB->setLocalPosition(p); + if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { + setIntervalFromNow(setStartDelay()); } - } - // Log packet size and data fields - LOG_DEBUG("POSITION node=%08x l=%d lat=%d lon=%d msl=%d hae=%d geo=%d pdop=%d hdop=%d vdop=%d siv=%d fxq=%d fxt=%d pts=%d " - "time=%d", - getFrom(&mp), mp.decoded.payload.size, p.latitude_i, p.longitude_i, p.altitude, p.altitude_hae, p.altitude_geoidal_separation, p.PDOP, - p.HDOP, p.VDOP, p.sats_in_view, p.fix_quality, p.fix_type, p.timestamp, p.time); + // Power saving trackers should clear their position on startup to avoid waking up and sending a stale position + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && + config.power.is_power_saving) { + LOG_DEBUG("Clear position on startup for sleepy tracker (ー。ー) zzz"); + nodeDB->clearLocalPosition(); + } +} - if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { - bool force = false; +bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr) +{ + auto p = *pptr; + + const auto transport = mp.transport_mechanism; + if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, + meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { + LOG_WARN("Ignoring packet supposedly from us over external transport"); + return true; + } + + // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) + // to set fixed location, EUD-GPS location or just the time (see also issue #900) + bool isLocal = false; + if (isFromUs(&mp)) { + isLocal = true; + if (config.position.fixed_position) { + LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true"); #ifdef T_WATCH_S3 - // The T-Watch appears to "pause" its RTC when shut down, such that the time it reads upon powering on is the same - // as when it was shut down. So we need to force the update here, since otherwise RTC::perhapsSetRTC will ignore it - // because it will always be an equivalent or lesser RTCQuality (RTCQualityNTP or RTCQualityNet). - force = true; + // Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the + // client device here + if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + trySetRtc(p, isLocal, true); + } #endif - // Set from phone RTC Quality to RTCQualityNTP since it should be approximately so - trySetRtc(p, isLocal, force); - } - nodeDB->updatePosition(getFrom(&mp), p); - if (channels.getByIndex(mp.channel).settings.has_module_settings) { - precision = channels.getByIndex(mp.channel).settings.module_settings.position_precision; - } else if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { - precision = 32; - } else { - precision = 0; - } + nodeDB->setLocalPosition(p, true); + return false; + } else { + LOG_DEBUG("Incoming update from MYSELF"); + nodeDB->setLocalPosition(p); + } + } - return false; // Let others look at this message also if they want -} + // Log packet size and data fields + LOG_DEBUG("POSITION node=%08x l=%d lat=%d lon=%d msl=%d hae=%d geo=%d pdop=%d hdop=%d vdop=%d siv=%d fxq=%d fxt=%d pts=%d " + "time=%d", + getFrom(&mp), mp.decoded.payload.size, p.latitude_i, p.longitude_i, p.altitude, p.altitude_hae, + p.altitude_geoidal_separation, p.PDOP, p.HDOP, p.VDOP, p.sats_in_view, p.fix_quality, p.fix_type, p.timestamp, + p.time); -void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) { - // Phone position packets need to be truncated to the channel precision - if (isFromUs(&mp) && (precision < 32 && precision > 0)) { - LOG_DEBUG("Truncate phone position to channel precision %i", precision); - p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision)); - p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision)); + if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + bool force = false; - // We want the imprecise position to be the middle of the possible location, not - p->latitude_i += (1 << (31 - precision)); - p->longitude_i += (1 << (31 - precision)); - - mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p); - } -} - -void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate) { - if (hasQualityTimesource() && !isLocal) { - LOG_DEBUG("Ignore time from mesh because we have a GPS, RTC, or Phone/NTP time source in the past day"); - return; - } - if (!isLocal && p.location_source < meshtastic_Position_LocSource_LOC_INTERNAL) { - LOG_DEBUG("Ignore time from mesh because it has a unknown or manual source"); - return; - } - struct timeval tv; - uint32_t secs = p.time; - - tv.tv_sec = secs; - tv.tv_usec = 0; - - perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate); -} - -bool PositionModule::hasQualityTimesource() { - bool setFromPhoneOrNtpToday = lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL); -#if MESHTASTIC_EXCLUDE_GPS - bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address); -#else - bool hasGpsOrRtc = hasGPS() || (rtc_found.address != ScanI2C::ADDRESS_NONE.address); +#ifdef T_WATCH_S3 + // The T-Watch appears to "pause" its RTC when shut down, such that the time it reads upon powering on is the same as when + // it was shut down. So we need to force the update here, since otherwise RTC::perhapsSetRTC will ignore it because it + // will always be an equivalent or lesser RTCQuality (RTCQualityNTP or RTCQualityNet). + force = true; #endif - return hasGpsOrRtc || setFromPhoneOrNtpToday; + // Set from phone RTC Quality to RTCQualityNTP since it should be approximately so + trySetRtc(p, isLocal, force); + } + + nodeDB->updatePosition(getFrom(&mp), p); + if (channels.getByIndex(mp.channel).settings.has_module_settings) { + precision = channels.getByIndex(mp.channel).settings.module_settings.position_precision; + } else if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + precision = 32; + } else { + precision = 0; + } + + return false; // Let others look at this message also if they want } -bool PositionModule::hasGPS() { +void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) +{ + // Phone position packets need to be truncated to the channel precision + if (isFromUs(&mp) && (precision < 32 && precision > 0)) { + LOG_DEBUG("Truncate phone position to channel precision %i", precision); + p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision)); + p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision)); + + // We want the imprecise position to be the middle of the possible location, not + p->latitude_i += (1 << (31 - precision)); + p->longitude_i += (1 << (31 - precision)); + + mp.decoded.payload.size = + pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p); + } +} + +void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate) +{ + if (hasQualityTimesource() && !isLocal) { + LOG_DEBUG("Ignore time from mesh because we have a GPS, RTC, or Phone/NTP time source in the past day"); + return; + } + if (!isLocal && p.location_source < meshtastic_Position_LocSource_LOC_INTERNAL) { + LOG_DEBUG("Ignore time from mesh because it has a unknown or manual source"); + return; + } + struct timeval tv; + uint32_t secs = p.time; + + tv.tv_sec = secs; + tv.tv_usec = 0; + + perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate); +} + +bool PositionModule::hasQualityTimesource() +{ + bool setFromPhoneOrNtpToday = + lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL); #if MESHTASTIC_EXCLUDE_GPS - return false; + bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address); #else - return gps && gps->isConnected(); + bool hasGpsOrRtc = hasGPS() || (rtc_found.address != ScanI2C::ADDRESS_NONE.address); +#endif + return hasGpsOrRtc || setFromPhoneOrNtpToday; +} + +bool PositionModule::hasGPS() +{ +#if MESHTASTIC_EXCLUDE_GPS + return false; +#else + return gps && gps->isConnected(); #endif } // Allocate a packet with our position data if we have one -meshtastic_MeshPacket *PositionModule::allocPositionPacket() { - if (precision == 0) { - LOG_DEBUG("Skip location send because precision is set to 0!"); - return nullptr; - } +meshtastic_MeshPacket *PositionModule::allocPositionPacket() +{ + if (precision == 0) { + LOG_DEBUG("Skip location send because precision is set to 0!"); + return nullptr; + } - meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position - assert(node->has_position); + meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position + assert(node->has_position); - // configuration of POSITION packet - // consider making this a function argument? - uint32_t pos_flags = config.position.position_flags; + // configuration of POSITION packet + // consider making this a function argument? + uint32_t pos_flags = config.position.position_flags; - // Populate a Position struct with ONLY the requested fields - meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure - // if localPosition is totally empty, put our last saved position (lite) in there - if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position)); - } - localPosition.seq_number++; + // Populate a Position struct with ONLY the requested fields + meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure + // if localPosition is totally empty, put our last saved position (lite) in there + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position)); + } + localPosition.seq_number++; - if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - LOG_WARN("Skip position send because lat/lon are zero!"); - return nullptr; - } + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + LOG_WARN("Skip position send because lat/lon are zero!"); + return nullptr; + } - // lat/lon are unconditionally included - IF AVAILABLE! - LOG_DEBUG("Send location with precision %i", precision); - if (precision < 32 && precision > 0) { - p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision)); - p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision)); + // lat/lon are unconditionally included - IF AVAILABLE! + LOG_DEBUG("Send location with precision %i", precision); + if (precision < 32 && precision > 0) { + p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision)); + p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision)); - // We want the imprecise position to be the middle of the possible location, not - p.latitude_i += (1 << (31 - precision)); - p.longitude_i += (1 << (31 - precision)); - } else { - p.latitude_i = localPosition.latitude_i; - p.longitude_i = localPosition.longitude_i; - } - p.precision_bits = precision; - p.has_latitude_i = true; - p.has_longitude_i = true; - // Always use NTP / GPS time if available - if (getValidTime(RTCQualityNTP) > 0) { - p.time = getValidTime(RTCQualityNTP); - } else if (rtc_found.address != ScanI2C::ADDRESS_NONE.address) { - LOG_INFO("Use RTC time for position"); - p.time = getValidTime(RTCQualityDevice); - } else if (getRTCQuality() < RTCQualityNTP) { - LOG_INFO("Strip low RTCQuality (%d) time from position", getRTCQuality()); - p.time = 0; - } - - if (config.position.fixed_position) { - p.location_source = meshtastic_Position_LocSource_LOC_MANUAL; - } else { - p.location_source = localPosition.location_source; - } - - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE) { - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL) { - p.altitude = localPosition.altitude; - p.has_altitude = true; + // We want the imprecise position to be the middle of the possible location, not + p.latitude_i += (1 << (31 - precision)); + p.longitude_i += (1 << (31 - precision)); } else { - p.altitude_hae = localPosition.altitude_hae; - p.has_altitude_hae = true; + p.latitude_i = localPosition.latitude_i; + p.longitude_i = localPosition.longitude_i; + } + p.precision_bits = precision; + p.has_latitude_i = true; + p.has_longitude_i = true; + // Always use NTP / GPS time if available + if (getValidTime(RTCQualityNTP) > 0) { + p.time = getValidTime(RTCQualityNTP); + } else if (rtc_found.address != ScanI2C::ADDRESS_NONE.address) { + LOG_INFO("Use RTC time for position"); + p.time = getValidTime(RTCQualityDevice); + } else if (getRTCQuality() < RTCQualityNTP) { + LOG_INFO("Strip low RTCQuality (%d) time from position", getRTCQuality()); + p.time = 0; } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_GEOIDAL_SEPARATION) { - p.altitude_geoidal_separation = localPosition.altitude_geoidal_separation; - p.has_altitude_geoidal_separation = true; + if (config.position.fixed_position) { + p.location_source = meshtastic_Position_LocSource_LOC_MANUAL; + } else { + p.location_source = localPosition.location_source; } - } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_DOP) { - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HVDOP) { - p.HDOP = localPosition.HDOP; - p.VDOP = localPosition.VDOP; - } else - p.PDOP = localPosition.PDOP; - } + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE) { + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL) { + p.altitude = localPosition.altitude; + p.has_altitude = true; + } else { + p.altitude_hae = localPosition.altitude_hae; + p.has_altitude_hae = true; + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW) - p.sats_in_view = localPosition.sats_in_view; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_GEOIDAL_SEPARATION) { + p.altitude_geoidal_separation = localPosition.altitude_geoidal_separation; + p.has_altitude_geoidal_separation = true; + } + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP) - p.timestamp = localPosition.timestamp; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_DOP) { + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HVDOP) { + p.HDOP = localPosition.HDOP; + p.VDOP = localPosition.VDOP; + } else + p.PDOP = localPosition.PDOP; + } - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO) - p.seq_number = localPosition.seq_number; + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW) + p.sats_in_view = localPosition.sats_in_view; - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HEADING) { - p.ground_track = localPosition.ground_track; - p.has_ground_track = true; - } + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_TIMESTAMP) + p.timestamp = localPosition.timestamp; - if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SPEED) { - p.ground_speed = localPosition.ground_speed; - p.has_ground_speed = true; - } + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SEQ_NO) + p.seq_number = localPosition.seq_number; - LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_HEADING) { + p.ground_track = localPosition.ground_track; + p.has_ground_track = true; + } + + if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_SPEED) { + p.ground_speed = localPosition.ground_speed; + p.has_ground_speed = true; + } + + LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); #ifndef MESHTASTIC_EXCLUDE_ATAK - // TAK Tracker devices should send their position in a TAK packet over the ATAK port - if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) - return allocAtakPli(); + // TAK Tracker devices should send their position in a TAK packet over the ATAK port + if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) + return allocAtakPli(); #endif - return allocDataProtobuf(p); + return allocDataProtobuf(p); } -meshtastic_MeshPacket *PositionModule::allocReply() { - if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && - Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { - LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); - ignoreRequest = true; // Mark it as ignored for MeshModule - return nullptr; - } - - meshtastic_MeshPacket *reply = allocPositionPacket(); - if (reply) { - lastSentReply = millis(); // Track when we sent this reply - } - return reply; -} - -meshtastic_MeshPacket *PositionModule::allocAtakPli() { - LOG_INFO("Send TAK PLI packet"); - meshtastic_MeshPacket *mp = allocDataPacket(); - mp->decoded.portnum = meshtastic_PortNum_ATAK_PLUGIN; - - meshtastic_TAKPacket takPacket = {.is_compressed = true, - .has_contact = true, - .contact = meshtastic_Contact_init_default, - .has_group = true, - .group = {meshtastic_MemberRole_TeamMember, meshtastic_Team_Cyan}, - .has_status = true, - .status = - { - .battery = powerStatus->getBatteryChargePercent(), - }, - .which_payload_variant = meshtastic_TAKPacket_pli_tag, - .payload_variant = {.pli = { - .latitude_i = localPosition.latitude_i, - .longitude_i = localPosition.longitude_i, - .altitude = localPosition.altitude_hae, - .speed = localPosition.ground_speed, - .course = static_cast(localPosition.ground_track), - }}}; - - auto length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.device_callsign, - sizeof(takPacket.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); - LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes", owner.long_name, strlen(owner.long_name)); - LOG_DEBUG("Compressed device_callsign '%s' - %d bytes", takPacket.contact.device_callsign, length); - length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.callsign, sizeof(takPacket.contact.callsign) - 1, - USX_PSET_DFLT, NULL); - mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_TAKPacket_msg, &takPacket); - return mp; -} - -void PositionModule::sendOurPosition() { - bool requestReplies = currentGeneration != radioGeneration; - currentGeneration = radioGeneration; - - // If we changed channels, ask everyone else for their latest info - LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); - for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { - if (channels.getByIndex(channelNum).settings.has_module_settings && - channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { - sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); - return; +meshtastic_MeshPacket *PositionModule::allocReply() +{ + if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && + Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); + ignoreRequest = true; // Mark it as ignored for MeshModule + return nullptr; } - } + + meshtastic_MeshPacket *reply = allocPositionPacket(); + if (reply) { + lastSentReply = millis(); // Track when we sent this reply + } + return reply; } -void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) { - if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { - LOG_DEBUG("Skip position send; no fresh position since boot"); - return; - } +meshtastic_MeshPacket *PositionModule::allocAtakPli() +{ + LOG_INFO("Send TAK PLI packet"); + meshtastic_MeshPacket *mp = allocDataPacket(); + mp->decoded.portnum = meshtastic_PortNum_ATAK_PLUGIN; - // cancel any not yet sent (now stale) position packets - if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) - service->cancelSending(prevPacketId); + meshtastic_TAKPacket takPacket = {.is_compressed = true, + .has_contact = true, + .contact = meshtastic_Contact_init_default, + .has_group = true, + .group = {meshtastic_MemberRole_TeamMember, meshtastic_Team_Cyan}, + .has_status = true, + .status = + { + .battery = powerStatus->getBatteryChargePercent(), + }, + .which_payload_variant = meshtastic_TAKPacket_pli_tag, + .payload_variant = {.pli = { + .latitude_i = localPosition.latitude_i, + .longitude_i = localPosition.longitude_i, + .altitude = localPosition.altitude_hae, + .speed = localPosition.ground_speed, + .course = static_cast(localPosition.ground_track), + }}}; - // Set's the class precision value for this particular packet - if (channels.getByIndex(channel).settings.has_module_settings) { - precision = channels.getByIndex(channel).settings.module_settings.position_precision; - } + auto length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.device_callsign, + sizeof(takPacket.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes", owner.long_name, strlen(owner.long_name)); + LOG_DEBUG("Compressed device_callsign '%s' - %d bytes", takPacket.contact.device_callsign, length); + length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.callsign, + sizeof(takPacket.contact.callsign) - 1, USX_PSET_DFLT, NULL); + mp->decoded.payload.size = + pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_TAKPacket_msg, &takPacket); + return mp; +} - meshtastic_MeshPacket *p = allocPositionPacket(); - if (p == nullptr) { - LOG_DEBUG("allocPositionPacket returned a nullptr"); - return; - } +void PositionModule::sendOurPosition() +{ + bool requestReplies = currentGeneration != radioGeneration; + currentGeneration = radioGeneration; - p->to = dest; - p->decoded.want_response = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ? false : wantReplies; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - prevPacketId = p->id; + // If we changed channels, ask everyone else for their latest info + LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); + for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { + if (channels.getByIndex(channelNum).settings.has_module_settings && + channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { + sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); + return; + } + } +} - if (channel > 0) - p->channel = channel; +void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) +{ + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position send; no fresh position since boot"); + return; + } - service->sendToMesh(p, RX_SRC_LOCAL, true); + // cancel any not yet sent (now stale) position packets + if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) + service->cancelSending(prevPacketId); - if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && - config.power.is_power_saving) { - meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); - notification->level = meshtastic_LogRecord_Level_INFO; - notification->time = getValidTime(RTCQualityFromNet); - sprintf(notification->message, "Sending position and sleeping for %us interval in a moment", - Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) / 1000U); - service->sendClientNotification(notification); - sleepOnNextExecution = true; - LOG_DEBUG("Start next execution in 5s, then sleep"); - setIntervalFromNow(FIVE_SECONDS_MS); - } + // Set's the class precision value for this particular packet + if (channels.getByIndex(channel).settings.has_module_settings) { + precision = channels.getByIndex(channel).settings.module_settings.position_precision; + } + + meshtastic_MeshPacket *p = allocPositionPacket(); + if (p == nullptr) { + LOG_DEBUG("allocPositionPacket returned a nullptr"); + return; + } + + p->to = dest; + p->decoded.want_response = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ? false : wantReplies; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || + config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + prevPacketId = p->id; + + if (channel > 0) + p->channel = channel; + + service->sendToMesh(p, RX_SRC_LOCAL, true); + + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && + config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending position and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } #define RUNONCE_INTERVAL 5000; -int32_t PositionModule::runOnce() { - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs); - LOG_DEBUG("Sleep for %ims, then awaking to send position again", nightyNightMs); - doDeepSleep(nightyNightMs, false, false); - } +int32_t PositionModule::runOnce() +{ + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs); + LOG_DEBUG("Sleep for %ims, then awaking to send position again", nightyNightMs); + doDeepSleep(nightyNightMs, false, false); + } - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (node == nullptr) - return RUNONCE_INTERVAL; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (node == nullptr) + return RUNONCE_INTERVAL; - // We limit our GPS broadcasts to a max rate - uint32_t now = millis(); - uint32_t intervalMs = - Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs, default_broadcast_interval_secs, numOnlineNodes); - uint32_t msSinceLastSend = now - lastGpsSend; - // Only send packets if the channel util. is less than 25% utilized or we're a tracker with less than 40% utilized. - if (!airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && - config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) { - return RUNONCE_INTERVAL; - } + // We limit our GPS broadcasts to a max rate + uint32_t now = millis(); + uint32_t intervalMs = Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs, + default_broadcast_interval_secs, numOnlineNodes); + uint32_t msSinceLastSend = now - lastGpsSend; + // Only send packets if the channel util. is less than 25% utilized or we're a tracker with less than 40% utilized. + if (!airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) { + return RUNONCE_INTERVAL; + } - bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); + bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); - if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { - if (waitingForFreshPosition) { + if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { + if (waitingForFreshPosition) { #ifdef GPS_DEBUG - LOG_DEBUG("Skip initial position send; no fresh position since boot"); + LOG_DEBUG("Skip initial position send; no fresh position since boot"); #endif - } else if (nodeDB->hasValidPosition(node)) { - lastGpsSend = now; + } else if (nodeDB->hasValidPosition(node)) { + lastGpsSend = now; - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; + lastGpsLatitude = node->position.latitude_i; + lastGpsLongitude = node->position.longitude_i; - sendOurPosition(); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { - sendLostAndFoundText(); - } + sendOurPosition(); + if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { + sendLostAndFoundText(); + } + } + } else if (config.position.position_broadcast_smart_enabled) { + const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position + + if (nodeDB->hasValidPosition(node2)) { + // The minimum time (in seconds) that would pass before we are able to send a new position packet. + + auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + msSinceLastSend = now - lastGpsSend; + + if (smartPosition.hasTraveledOverThreshold && + Throttle::execute( + &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, + []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + + LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " + "minTimeInterval=%ims)", + localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, + msSinceLastSend, minimumTimeThreshold); + + // Set the current coords as our last ones, after we've compared distance with current and decided to send + lastGpsLatitude = node->position.latitude_i; + lastGpsLongitude = node->position.longitude_i; + } + } } - } else if (config.position.position_broadcast_smart_enabled) { - const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position - if (nodeDB->hasValidPosition(node2)) { - // The minimum time (in seconds) that would pass before we are able to send a new position packet. - - auto smartPosition = getDistanceTraveledSinceLastSend(node->position); - msSinceLastSend = now - lastGpsSend; - - if (smartPosition.hasTraveledOverThreshold && Throttle::execute( - &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { - - LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " - "minTimeInterval=%ims)", - localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, minimumTimeThreshold); - - // Set the current coords as our last ones, after we've compared distance with current and decided to send - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; - } - } - } - - return RUNONCE_INTERVAL; // to save power only wake for our callback occasionally + return RUNONCE_INTERVAL; // to save power only wake for our callback occasionally } -void PositionModule::sendLostAndFoundText() { - meshtastic_MeshPacket *p = allocDataPacket(); - p->to = NODENUM_BROADCAST; - char *message = new char[60]; - sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7)); - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - p->want_ack = false; - p->decoded.payload.size = strlen(message); - memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); +void PositionModule::sendLostAndFoundText() +{ + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = NODENUM_BROADCAST; + char *message = new char[60]; + sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7)); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->want_ack = false; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - service->sendToMesh(p, RX_SRC_LOCAL, true); - delete[] message; + service->sendToMesh(p, RX_SRC_LOCAL, true); + delete[] message; } // Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision -static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) { - if (precisionBits > 0 && precisionBits < 32) { - // Build mask for top 'precisionBits' bits of a 32-bit unsigned field - const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); - // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but - // the bitmask logic used previously operated as unsigned—preserve that behavior by - // casting to uint32_t for masking, then back to int32_t. - uint32_t lat_u = static_cast(inLat) & mask; - uint32_t lon_u = static_cast(inLon) & mask; +static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) +{ + if (precisionBits > 0 && precisionBits < 32) { + // Build mask for top 'precisionBits' bits of a 32-bit unsigned field + const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); + // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but + // the bitmask logic used previously operated as unsigned—preserve that behavior by + // casting to uint32_t for masking, then back to int32_t. + uint32_t lat_u = static_cast(inLat) & mask; + uint32_t lon_u = static_cast(inLon) & mask; - // Add the "center of cell" offset used elsewhere: - // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. - uint32_t center_offset = (1u << (31 - precisionBits)); - lat_u += center_offset; - lon_u += center_offset; + // Add the "center of cell" offset used elsewhere: + // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. + uint32_t center_offset = (1u << (31 - precisionBits)); + lat_u += center_offset; + lon_u += center_offset; - outLat = static_cast(lat_u); - outLon = static_cast(lon_u); - } else { - // full precision: return input unchanged - outLat = inLat; - outLon = inLon; - } -} - -struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) { - const uint32_t distanceTravelThreshold = Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); - - int32_t lastLatImprecise, lastLonImprecise; - int32_t currentLatImprecise, currentLonImprecise; - - computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); - computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, currentLonImprecise); - - float distMeters = - GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, currentLonImprecise * 1e-7); - - float distanceTraveled = fabsf(distMeters); - - return SmartPosition{.distanceTraveled = distanceTraveled, - .distanceThreshold = distanceTravelThreshold, - .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; -} - -void PositionModule::handleNewPosition() { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position - // We limit our GPS broadcasts to a max rate - if (nodeDB->hasValidPosition(node2)) { - auto smartPosition = getDistanceTraveledSinceLastSend(node->position); - uint32_t msSinceLastSend = millis() - lastGpsSend; - if (smartPosition.hasTraveledOverThreshold && Throttle::execute( - &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { - LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " - "minTimeInterval=%ims)", - localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, minimumTimeThreshold); - - // Set the current coords as our last ones, after we've compared distance with current and decided to send - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; + outLat = static_cast(lat_u); + outLon = static_cast(lon_u); + } else { + // full precision: return input unchanged + outLat = inLat; + outLon = inLon; + } +} + +struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) +{ + const uint32_t distanceTravelThreshold = + Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); + + int32_t lastLatImprecise, lastLonImprecise; + int32_t currentLatImprecise, currentLonImprecise; + + computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); + computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, + currentLonImprecise); + + float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, + currentLonImprecise * 1e-7); + + float distanceTraveled = fabsf(distMeters); + + return SmartPosition{.distanceTraveled = distanceTraveled, + .distanceThreshold = distanceTravelThreshold, + .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; +} + +void PositionModule::handleNewPosition() +{ + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position + // We limit our GPS broadcasts to a max rate + if (nodeDB->hasValidPosition(node2)) { + auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + uint32_t msSinceLastSend = millis() - lastGpsSend; + if (smartPosition.hasTraveledOverThreshold && + Throttle::execute( + &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, + []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " + "minTimeInterval=%ims)", + localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, + minimumTimeThreshold); + + // Set the current coords as our last ones, after we've compared distance with current and decided to send + lastGpsLatitude = node->position.latitude_i; + lastGpsLongitude = node->position.longitude_i; + } } - } } #endif \ No newline at end of file diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 4efd6a58a..32e499531 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -6,78 +6,80 @@ /** * Position module for sending/receiving positions into the mesh */ -class PositionModule : public ProtobufModule, private concurrency::OSThread { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &PositionModule::handleStatusUpdate); +class PositionModule : public ProtobufModule, private concurrency::OSThread +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &PositionModule::handleStatusUpdate); - /// The id of the last packet we sent, to allow us to cancel it if we make something fresher - PacketId prevPacketId = 0; + /// The id of the last packet we sent, to allow us to cancel it if we make something fresher + PacketId prevPacketId = 0; - /// We limit our GPS broadcasts to a max rate - uint32_t lastGpsSend = 0; + /// We limit our GPS broadcasts to a max rate + uint32_t lastGpsSend = 0; - // Store the latest good lat / long - int32_t lastGpsLatitude = 0; - int32_t lastGpsLongitude = 0; + // Store the latest good lat / long + int32_t lastGpsLatitude = 0; + int32_t lastGpsLongitude = 0; - /// We force a rebroadcast if the radio settings change - uint32_t currentGeneration = 0; + /// We force a rebroadcast if the radio settings change + uint32_t currentGeneration = 0; -public: - /** Constructor - * name is for debugging output - */ - PositionModule(); + public: + /** Constructor + * name is for debugging output + */ + PositionModule(); - /** - * Send our position into the mesh - */ - void sendOurPosition(NodeNum dest, bool wantReplies = false, uint8_t channel = 0); - void sendOurPosition(); + /** + * Send our position into the mesh + */ + void sendOurPosition(NodeNum dest, bool wantReplies = false, uint8_t channel = 0); + void sendOurPosition(); - void handleNewPosition(); + void handleNewPosition(); -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *p) override; - virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) override; + virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_Position *p) override; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /** Does our periodic broadcast */ - virtual int32_t runOnce() override; + /** Does our periodic broadcast */ + virtual int32_t runOnce() override; -private: - meshtastic_MeshPacket *allocPositionPacket(); - struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); - meshtastic_MeshPacket *allocAtakPli(); - void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); - uint32_t precision; - void sendLostAndFoundText(); - bool hasQualityTimesource(); - bool hasGPS(); - uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) + private: + meshtastic_MeshPacket *allocPositionPacket(); + struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); + meshtastic_MeshPacket *allocAtakPli(); + void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); + uint32_t precision; + void sendLostAndFoundText(); + bool hasQualityTimesource(); + bool hasGPS(); + uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) #if USERPREFS_EVENT_MODE - // In event mode we want to prevent excessive position broadcasts - // we set the minimum interval to 5m - const uint32_t minimumTimeThreshold = - max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); + // In event mode we want to prevent excessive position broadcasts + // we set the minimum interval to 5m + const uint32_t minimumTimeThreshold = + max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); #else - const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); + const uint32_t minimumTimeThreshold = + Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); #endif }; struct SmartPosition { - float distanceTraveled; - uint32_t distanceThreshold; - bool hasTraveledOverThreshold; + float distanceTraveled; + uint32_t distanceThreshold; + bool hasTraveledOverThreshold; }; extern PositionModule *positionModule; \ No newline at end of file diff --git a/src/modules/PowerStressModule.cpp b/src/modules/PowerStressModule.cpp index 1ee11ace5..d487fe6fc 100644 --- a/src/modules/PowerStressModule.cpp +++ b/src/modules/PowerStressModule.cpp @@ -14,115 +14,121 @@ extern void printInfo(); PowerStressModule::PowerStressModule() - : ProtobufModule("powerstress", meshtastic_PortNum_POWERSTRESS_APP, &meshtastic_PowerStressMessage_msg), concurrency::OSThread("PowerStress") {} - -bool PowerStressModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_PowerStressMessage *pptr) { - // We only respond to messages if powermon debugging is already on - if (config.power.powermon_enables) { - auto p = *pptr; - LOG_INFO("Received PowerStress cmd=%d", p.cmd); - - // Some commands we can handle immediately, anything else gets deferred to be handled by our thread - switch (p.cmd) { - case meshtastic_PowerStressMessage_Opcode_UNSET: - LOG_ERROR("PowerStress operation unset"); - break; - - case meshtastic_PowerStressMessage_Opcode_PRINT_INFO: - printInfo(); - - // Now that we know we are actually doing power stress testing, go ahead and turn on all enables (so the log is - // fully detailed) - powerMon->force_enabled = true; - break; - - default: - if (currentMessage.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) - LOG_ERROR("PowerStress operation %d already in progress! Can't start new command", currentMessage.cmd); - else - currentMessage = p; // copy for use by thread (the message provided to us will be getting freed) - break; - } - } - return true; + : ProtobufModule("powerstress", meshtastic_PortNum_POWERSTRESS_APP, &meshtastic_PowerStressMessage_msg), + concurrency::OSThread("PowerStress") +{ } -int32_t PowerStressModule::runOnce() { - if (!config.power.powermon_enables) { - // Powermon not enabled - stop using CPU/stop this thread - return disable(); - } +bool PowerStressModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_PowerStressMessage *pptr) +{ + // We only respond to messages if powermon debugging is already on + if (config.power.powermon_enables) { + auto p = *pptr; + LOG_INFO("Received PowerStress cmd=%d", p.cmd); - int32_t sleep_msec = 10; // when not active check for new messages every 10ms + // Some commands we can handle immediately, anything else gets deferred to be handled by our thread + switch (p.cmd) { + case meshtastic_PowerStressMessage_Opcode_UNSET: + LOG_ERROR("PowerStress operation unset"); + break; - auto &p = currentMessage; + case meshtastic_PowerStressMessage_Opcode_PRINT_INFO: + printInfo(); - if (isRunningCommand) { - // Done with the previous command - our sleep must have finished - p.cmd = meshtastic_PowerStressMessage_Opcode_UNSET; - p.num_seconds = 0; - isRunningCommand = false; - LOG_INFO("S:PS:%u", p.cmd); - } else { - if (p.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) { - sleep_msec = (int32_t)(p.num_seconds * 1000); - isRunningCommand = !!sleep_msec; // if the command wants us to sleep, make sure to mark that we have something running - LOG_INFO("S:PS:%u", - p.cmd); // Emit a structured log saying we are starting a powerstress state (to make it easier to parse later) + // Now that we know we are actually doing power stress testing, go ahead and turn on all enables (so the log is fully + // detailed) + powerMon->force_enabled = true; + break; - switch (p.cmd) { - case meshtastic_PowerStressMessage_Opcode_LED_ON: - ledForceOn.set(true); - break; - case meshtastic_PowerStressMessage_Opcode_LED_OFF: - ledForceOn.set(false); - break; - case meshtastic_PowerStressMessage_Opcode_GPS_ON: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_GPS_OFF: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_LORA_OFF: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_LORA_RX: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_LORA_TX: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_SCREEN_OFF: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_SCREEN_ON: - // FIXME - implement - break; - case meshtastic_PowerStressMessage_Opcode_BT_OFF: - setBluetoothEnable(false); - break; - case meshtastic_PowerStressMessage_Opcode_BT_ON: - setBluetoothEnable(true); - break; - case meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP: - doDeepSleep(sleep_msec, true, true); - break; - case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: { - uint32_t start_msec = millis(); - while (Throttle::isWithinTimespanMs(start_msec, sleep_msec)) - ; // Don't let CPU idle at all - sleep_msec = 0; // we already slept - break; - } - case meshtastic_PowerStressMessage_Opcode_CPU_IDLE: - // FIXME - implement - break; - default: - LOG_ERROR("PowerStress operation %d not yet implemented!", p.cmd); - sleep_msec = 0; // Don't do whatever sleep was requested... - break; - } + default: + if (currentMessage.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) + LOG_ERROR("PowerStress operation %d already in progress! Can't start new command", currentMessage.cmd); + else + currentMessage = p; // copy for use by thread (the message provided to us will be getting freed) + break; + } } - } - return sleep_msec; + return true; +} + +int32_t PowerStressModule::runOnce() +{ + if (!config.power.powermon_enables) { + // Powermon not enabled - stop using CPU/stop this thread + return disable(); + } + + int32_t sleep_msec = 10; // when not active check for new messages every 10ms + + auto &p = currentMessage; + + if (isRunningCommand) { + // Done with the previous command - our sleep must have finished + p.cmd = meshtastic_PowerStressMessage_Opcode_UNSET; + p.num_seconds = 0; + isRunningCommand = false; + LOG_INFO("S:PS:%u", p.cmd); + } else { + if (p.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) { + sleep_msec = (int32_t)(p.num_seconds * 1000); + isRunningCommand = !!sleep_msec; // if the command wants us to sleep, make sure to mark that we have something running + LOG_INFO( + "S:PS:%u", + p.cmd); // Emit a structured log saying we are starting a powerstress state (to make it easier to parse later) + + switch (p.cmd) { + case meshtastic_PowerStressMessage_Opcode_LED_ON: + ledForceOn.set(true); + break; + case meshtastic_PowerStressMessage_Opcode_LED_OFF: + ledForceOn.set(false); + break; + case meshtastic_PowerStressMessage_Opcode_GPS_ON: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_GPS_OFF: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_LORA_OFF: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_LORA_RX: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_LORA_TX: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_SCREEN_OFF: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_SCREEN_ON: + // FIXME - implement + break; + case meshtastic_PowerStressMessage_Opcode_BT_OFF: + setBluetoothEnable(false); + break; + case meshtastic_PowerStressMessage_Opcode_BT_ON: + setBluetoothEnable(true); + break; + case meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP: + doDeepSleep(sleep_msec, true, true); + break; + case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: { + uint32_t start_msec = millis(); + while (Throttle::isWithinTimespanMs(start_msec, sleep_msec)) + ; // Don't let CPU idle at all + sleep_msec = 0; // we already slept + break; + } + case meshtastic_PowerStressMessage_Opcode_CPU_IDLE: + // FIXME - implement + break; + default: + LOG_ERROR("PowerStress operation %d not yet implemented!", p.cmd); + sleep_msec = 0; // Don't do whatever sleep was requested... + break; + } + } + } + return sleep_msec; } \ No newline at end of file diff --git a/src/modules/PowerStressModule.h b/src/modules/PowerStressModule.h index b27b225ba..2d449f690 100644 --- a/src/modules/PowerStressModule.h +++ b/src/modules/PowerStressModule.h @@ -6,32 +6,33 @@ /** * A module that provides easy low-level remote access to device hardware. */ -class PowerStressModule : public ProtobufModule, private concurrency::OSThread { - meshtastic_PowerStressMessage currentMessage = meshtastic_PowerStressMessage_init_default; - bool isRunningCommand = false; +class PowerStressModule : public ProtobufModule, private concurrency::OSThread +{ + meshtastic_PowerStressMessage currentMessage = meshtastic_PowerStressMessage_init_default; + bool isRunningCommand = false; -public: - /** Constructor - * name is for debugging output - */ - PowerStressModule(); + public: + /** Constructor + * name is for debugging output + */ + PowerStressModule(); -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_PowerStressMessage *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_PowerStressMessage *p) override; - /** - * Periodically read the gpios we have been asked to WATCH, if they have changed, - * broadcast a message with the change information. - * - * The method that will be called each time our thread gets a chance to run - * - * Returns desired period for next invocation (or RUN_SAME for no change) - */ - virtual int32_t runOnce() override; + /** + * Periodically read the gpios we have been asked to WATCH, if they have changed, + * broadcast a message with the change information. + * + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce() override; }; extern PowerStressModule powerStressModule; \ No newline at end of file diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp index 4b12c004e..026b3028d 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -29,79 +29,80 @@ RangeTestModule::RangeTestModule() : concurrency::OSThread("RangeTest") {} uint32_t packetSequence = 0; -int32_t RangeTestModule::runOnce() { +int32_t RangeTestModule::runOnce() +{ #if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.range_test.enabled = 1; - // moduleConfig.range_test.sender = 30; - // moduleConfig.range_test.save = 1; - // moduleConfig.range_test.clear_on_reboot = 1; + // moduleConfig.range_test.enabled = 1; + // moduleConfig.range_test.sender = 30; + // moduleConfig.range_test.save = 1; + // moduleConfig.range_test.clear_on_reboot = 1; - // Fixed position is useful when testing indoors. - // config.position.fixed_position = 1; + // Fixed position is useful when testing indoors. + // config.position.fixed_position = 1; - uint32_t senderHeartbeat = moduleConfig.range_test.sender * 1000; - if (moduleConfig.range_test.enabled) { + uint32_t senderHeartbeat = moduleConfig.range_test.sender * 1000; + if (moduleConfig.range_test.enabled) { - if (firstTime) { - rangeTestModuleRadio = new RangeTestModuleRadio(); + if (firstTime) { + rangeTestModuleRadio = new RangeTestModuleRadio(); - firstTime = 0; + firstTime = 0; - if (moduleConfig.range_test.clear_on_reboot) { - // User wants to delete previous range test(s) - LOG_INFO("Range Test Module - Clearing out previous test file"); - rangeTestModuleRadio->removeFile(); - } - if (moduleConfig.range_test.sender) { - LOG_INFO("Init Range Test Module -- Sender"); - started = millis(); // make a note of when we started - return (5000); // Sending first message 5 seconds after initialization. - } else { - LOG_INFO("Init Range Test Module -- Receiver"); - return disable(); - // This thread does not need to run as a receiver - } - } else { - - if (moduleConfig.range_test.sender) { - // If sender - LOG_INFO("Range Test Module - Sending heartbeat every %d ms", (senderHeartbeat)); - - LOG_INFO("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); - LOG_INFO("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); - LOG_INFO("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); - LOG_INFO("gpsStatus->getDOP() %d", gpsStatus->getDOP()); - LOG_INFO("fixed_position() %d", config.position.fixed_position); - - // Only send packets if the channel is less than 25% utilized. - if (airTime->isTxAllowedChannelUtil(true)) { - rangeTestModuleRadio->sendPayload(); - } - - // If we have been running for more than 8 hours, turn module back off - if (!Throttle::isWithinTimespanMs(started, 28800000)) { - LOG_INFO("Range Test Module - Disable after 8 hours"); - return disable(); + if (moduleConfig.range_test.clear_on_reboot) { + // User wants to delete previous range test(s) + LOG_INFO("Range Test Module - Clearing out previous test file"); + rangeTestModuleRadio->removeFile(); + } + if (moduleConfig.range_test.sender) { + LOG_INFO("Init Range Test Module -- Sender"); + started = millis(); // make a note of when we started + return (5000); // Sending first message 5 seconds after initialization. + } else { + LOG_INFO("Init Range Test Module -- Receiver"); + return disable(); + // This thread does not need to run as a receiver + } } else { - return (senderHeartbeat); + + if (moduleConfig.range_test.sender) { + // If sender + LOG_INFO("Range Test Module - Sending heartbeat every %d ms", (senderHeartbeat)); + + LOG_INFO("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); + LOG_INFO("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); + LOG_INFO("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); + LOG_INFO("gpsStatus->getDOP() %d", gpsStatus->getDOP()); + LOG_INFO("fixed_position() %d", config.position.fixed_position); + + // Only send packets if the channel is less than 25% utilized. + if (airTime->isTxAllowedChannelUtil(true)) { + rangeTestModuleRadio->sendPayload(); + } + + // If we have been running for more than 8 hours, turn module back off + if (!Throttle::isWithinTimespanMs(started, 28800000)) { + LOG_INFO("Range Test Module - Disable after 8 hours"); + return disable(); + } else { + return (senderHeartbeat); + } + } else { + return disable(); + // This thread does not need to run as a receiver + } } - } else { - return disable(); - // This thread does not need to run as a receiver - } + } else { + LOG_INFO("Range Test Module - Disabled"); } - } else { - LOG_INFO("Range Test Module - Disabled"); - } #endif - return disable(); + return disable(); } /** @@ -110,229 +111,233 @@ int32_t RangeTestModule::runOnce() { * @param dest The destination node number. * @param wantReplies Whether or not to request replies from the destination node. */ -void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies) { - meshtastic_MeshPacket *p = allocDataPacket(); - p->to = dest; - p->decoded.want_response = wantReplies; - p->hop_limit = 0; - p->want_ack = false; +void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies) +{ + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = dest; + p->decoded.want_response = wantReplies; + p->hop_limit = 0; + p->want_ack = false; - packetSequence++; + packetSequence++; - static char heartbeatString[MAX_LORA_PAYLOAD_LEN + 1]; - snprintf(heartbeatString, sizeof(heartbeatString), "seq %u", packetSequence); + static char heartbeatString[MAX_LORA_PAYLOAD_LEN + 1]; + snprintf(heartbeatString, sizeof(heartbeatString), "seq %u", packetSequence); - p->decoded.payload.size = strlen(heartbeatString); // You must specify how many bytes are in the reply - memcpy(p->decoded.payload.bytes, heartbeatString, p->decoded.payload.size); + p->decoded.payload.size = strlen(heartbeatString); // You must specify how many bytes are in the reply + memcpy(p->decoded.payload.bytes, heartbeatString, p->decoded.payload.size); - service->sendToMesh(p); + service->sendToMesh(p); - // TODO: Handle this better. We want to keep the phone awake otherwise it stops sending. - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + // TODO: Handle this better. We want to keep the phone awake otherwise it stops sending. + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); } -ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) { +ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) +{ #if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) - if (moduleConfig.range_test.enabled) { + if (moduleConfig.range_test.enabled) { - /* - auto &p = mp.decoded; - LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", - LOG_INFO.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); - */ + /* + auto &p = mp.decoded; + LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", + LOG_INFO.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); + */ - if (!isFromUs(&mp)) { - if (moduleConfig.range_test.save) { - appendFile(mp); - } + if (!isFromUs(&mp)) { + if (moduleConfig.range_test.save) { + appendFile(mp); + } - /* - NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); + /* + NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); - LOG_DEBUG("-----------------------------------------"); - LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); - LOG_DEBUG("p.payload.size %d", p.payload.size); - LOG_DEBUG("---- Received Packet:"); - LOG_DEBUG("mp.from %d", mp.from); - LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); - LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi); - LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); - LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); - LOG_DEBUG("n->user.long_name %s", n->user.long_name); - LOG_DEBUG("n->user.short_name %s", n->user.short_name); - LOG_DEBUG("n->has_position %d", n->has_position); - LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); - LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); - LOG_DEBUG("---- Current device location information:"); - LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); - LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); - LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); - LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); - LOG_DEBUG("-----------------------------------------"); - */ - } - } else { - LOG_INFO("Range Test Module Disabled"); - } - -#endif - - return ProcessMessage::CONTINUE; // Let others look at this message also if they want -} - -bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) { -#ifdef ARCH_ESP32 - auto &p = mp.decoded; - - meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); - /* - LOG_DEBUG("-----------------------------------------"); - LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); - LOG_DEBUG("p.payload.size %d", p.payload.size); - LOG_DEBUG("---- Received Packet:"); - LOG_DEBUG("mp.from %d", mp.from); - LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); - LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); - LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); - LOG_DEBUG("n->user.long_name %s", n->user.long_name); - LOG_DEBUG("n->user.short_name %s", n->user.short_name); - LOG_DEBUG("n->has_position %d", n->has_position); - LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); - LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); - LOG_DEBUG("---- Current device location information:"); - LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); - LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); - LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); - LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); - LOG_DEBUG("-----------------------------------------"); - */ - concurrency::LockGuard g(spiLock); - if (!FSBegin()) { - LOG_DEBUG("An Error has occurred while mounting the filesystem"); - return 0; - } - - if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { - LOG_DEBUG("Filesystem doesn't have enough free space. Aborting write"); - return 0; - } - - FSCom.mkdir("/static"); - - // If the file doesn't exist, write the header. - if (!FSCom.exists("/static/rangetest.csv")) { - //--------- Write to file - File fileToWrite = FSCom.open("/static/rangetest.csv", FILE_WRITE); - - if (!fileToWrite) { - LOG_ERROR("There was an error opening the file for writing"); - return 0; - } - - // Print the CSV header - if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx " - "snr,distance,hop limit,payload,rx rssi")) { - LOG_INFO("File was written"); + LOG_DEBUG("-----------------------------------------"); + LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); + LOG_DEBUG("p.payload.size %d", p.payload.size); + LOG_DEBUG("---- Received Packet:"); + LOG_DEBUG("mp.from %d", mp.from); + LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); + LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi); + LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); + LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); + LOG_DEBUG("n->user.long_name %s", n->user.long_name); + LOG_DEBUG("n->user.short_name %s", n->user.short_name); + LOG_DEBUG("n->has_position %d", n->has_position); + LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); + LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); + LOG_DEBUG("---- Current device location information:"); + LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); + LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); + LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); + LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); + LOG_DEBUG("-----------------------------------------"); + */ + } } else { - LOG_ERROR("File write failed"); + LOG_INFO("Range Test Module Disabled"); } - fileToWrite.flush(); - fileToWrite.close(); - } - //--------- Append content to file - File fileToAppend = FSCom.open("/static/rangetest.csv", FILE_APPEND); +#endif - if (!fileToAppend) { - LOG_ERROR("There was an error opening the file for appending"); - return 0; - } + return ProcessMessage::CONTINUE; // Let others look at this message also if they want +} - struct timeval tv; - if (!gettimeofday(&tv, NULL)) { - long hms = tv.tv_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; +bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) +{ +#ifdef ARCH_ESP32 + auto &p = mp.decoded; - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); + /* + LOG_DEBUG("-----------------------------------------"); + LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); + LOG_DEBUG("p.payload.size %d", p.payload.size); + LOG_DEBUG("---- Received Packet:"); + LOG_DEBUG("mp.from %d", mp.from); + LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); + LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); + LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); + LOG_DEBUG("n->user.long_name %s", n->user.long_name); + LOG_DEBUG("n->user.short_name %s", n->user.short_name); + LOG_DEBUG("n->has_position %d", n->has_position); + LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); + LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); + LOG_DEBUG("---- Current device location information:"); + LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); + LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); + LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); + LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); + LOG_DEBUG("-----------------------------------------"); + */ + concurrency::LockGuard g(spiLock); + if (!FSBegin()) { + LOG_DEBUG("An Error has occurred while mounting the filesystem"); + return 0; + } - fileToAppend.printf("%02d:%02d:%02d,", hour, min, sec); // Time - } else { - fileToAppend.printf("??:??:??,"); // Time - } + if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { + LOG_DEBUG("Filesystem doesn't have enough free space. Aborting write"); + return 0; + } - fileToAppend.printf("%d,", getFrom(&mp)); // From - fileToAppend.printf("%s,", n->user.long_name); // Long Name - fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat - fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long - if (gpsStatus->getIsConnected() || config.position.fixed_position) { - fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat - fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long - fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude - } else { - // When the phone API is in use, the node info will be updated with position - meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); - fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat - fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long - fileToAppend.printf("%d,", us->position.altitude); // RX Altitude - } + FSCom.mkdir("/static"); - fileToAppend.printf("%f,", mp.rx_snr); // RX SNR + // If the file doesn't exist, write the header. + if (!FSCom.exists("/static/rangetest.csv")) { + //--------- Write to file + File fileToWrite = FSCom.open("/static/rangetest.csv", FILE_WRITE); - if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { - float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, gpsStatus->getLatitude() * 1e-7, - gpsStatus->getLongitude() * 1e-7); - fileToAppend.printf("%f,", distance); // Distance in meters - } else { - fileToAppend.printf("0,"); - } + if (!fileToWrite) { + LOG_ERROR("There was an error opening the file for writing"); + return 0; + } - fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit + // Print the CSV header + if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx " + "snr,distance,hop limit,payload,rx rssi")) { + LOG_INFO("File was written"); + } else { + LOG_ERROR("File write failed"); + } + fileToWrite.flush(); + fileToWrite.close(); + } - // TODO: If quotes are found in the payload, it has to be escaped. - fileToAppend.printf("\"%s\"\n", p.payload.bytes); - fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI + //--------- Append content to file + File fileToAppend = FSCom.open("/static/rangetest.csv", FILE_APPEND); - fileToAppend.flush(); - fileToAppend.close(); + if (!fileToAppend) { + LOG_ERROR("There was an error opening the file for appending"); + return 0; + } - return 1; + struct timeval tv; + if (!gettimeofday(&tv, NULL)) { + long hms = tv.tv_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + fileToAppend.printf("%02d:%02d:%02d,", hour, min, sec); // Time + } else { + fileToAppend.printf("??:??:??,"); // Time + } + + fileToAppend.printf("%d,", getFrom(&mp)); // From + fileToAppend.printf("%s,", n->user.long_name); // Long Name + fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat + fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long + if (gpsStatus->getIsConnected() || config.position.fixed_position) { + fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat + fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long + fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude + } else { + // When the phone API is in use, the node info will be updated with position + meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); + fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat + fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long + fileToAppend.printf("%d,", us->position.altitude); // RX Altitude + } + + fileToAppend.printf("%f,", mp.rx_snr); // RX SNR + + if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { + float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, + gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7); + fileToAppend.printf("%f,", distance); // Distance in meters + } else { + fileToAppend.printf("0,"); + } + + fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit + + // TODO: If quotes are found in the payload, it has to be escaped. + fileToAppend.printf("\"%s\"\n", p.payload.bytes); + fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI + + fileToAppend.flush(); + fileToAppend.close(); + + return 1; #else - LOG_ERROR("Failed to store range test results - feature only available for ESP32"); + LOG_ERROR("Failed to store range test results - feature only available for ESP32"); - return 0; + return 0; #endif } -bool RangeTestModuleRadio::removeFile() { +bool RangeTestModuleRadio::removeFile() +{ #ifdef ARCH_ESP32 - if (!FSBegin()) { - LOG_DEBUG("An Error has occurred while mounting the filesystem"); - return 0; - } + if (!FSBegin()) { + LOG_DEBUG("An Error has occurred while mounting the filesystem"); + return 0; + } - if (!FSCom.exists("/static/rangetest.csv")) { - LOG_DEBUG("No range tests found."); - return 0; - } + if (!FSCom.exists("/static/rangetest.csv")) { + LOG_DEBUG("No range tests found."); + return 0; + } - LOG_INFO("Deleting previous range test."); - bool result = FSCom.remove("/static/rangetest.csv"); + LOG_INFO("Deleting previous range test."); + bool result = FSCom.remove("/static/rangetest.csv"); - if (!result) { - LOG_ERROR("Failed to delete range test."); - return 0; - } - LOG_INFO("Range test removed."); + if (!result) { + LOG_ERROR("Failed to delete range test."); + return 0; + } + LOG_INFO("Range test removed."); - return 1; + return 1; #else - LOG_ERROR("Failed to remove range test results - feature only available for ESP32"); + LOG_ERROR("Failed to remove range test results - feature only available for ESP32"); - return 0; + return 0; #endif } \ No newline at end of file diff --git a/src/modules/RangeTestModule.h b/src/modules/RangeTestModule.h index 4ae39b179..0512e70a8 100644 --- a/src/modules/RangeTestModule.h +++ b/src/modules/RangeTestModule.h @@ -6,15 +6,16 @@ #include #include -class RangeTestModule : private concurrency::OSThread { - bool firstTime = 1; - unsigned long started = 0; +class RangeTestModule : private concurrency::OSThread +{ + bool firstTime = 1; + unsigned long started = 0; -public: - RangeTestModule(); + public: + RangeTestModule(); -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; }; extern RangeTestModule *rangeTestModule; @@ -23,36 +24,38 @@ extern RangeTestModule *rangeTestModule; * Radio interface for RangeTestModule * */ -class RangeTestModuleRadio : public SinglePortModule { - uint32_t lastRxID = 0; +class RangeTestModuleRadio : public SinglePortModule +{ + uint32_t lastRxID = 0; -public: - RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_RANGE_TEST_APP) { - loopbackOk = true; // Allow locally generated messages to loop back to the client - } + public: + RangeTestModuleRadio() : SinglePortModule("RangeTestModuleRadio", meshtastic_PortNum_RANGE_TEST_APP) + { + loopbackOk = true; // Allow locally generated messages to loop back to the client + } - /** - * Send our payload into the mesh - */ - void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - /** - * Append range test data to the file on the Filesystem - */ - bool appendFile(const meshtastic_MeshPacket &mp); + /** + * Append range test data to the file on the Filesystem + */ + bool appendFile(const meshtastic_MeshPacket &mp); - /** - * Cleanup range test data from filesystem - */ - bool removeFile(); + /** + * Cleanup range test data from filesystem + */ + bool removeFile(); -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; extern RangeTestModuleRadio *rangeTestModuleRadio; diff --git a/src/modules/RemoteHardwareModule.cpp b/src/modules/RemoteHardwareModule.cpp index 82510440a..04cfeb651 100644 --- a/src/modules/RemoteHardwareModule.cpp +++ b/src/modules/RemoteHardwareModule.cpp @@ -16,139 +16,146 @@ #define WATCH_INTERVAL_MSEC (30 * 1000) // Tests for access to read from or write to a specified GPIO pin -static bool pinAccessAllowed(uint64_t mask, uint8_t pin) { - // If undefined pin access is allowed, don't check the pin and just return true - if (moduleConfig.remote_hardware.allow_undefined_pin_access) { - return true; - } +static bool pinAccessAllowed(uint64_t mask, uint8_t pin) +{ + // If undefined pin access is allowed, don't check the pin and just return true + if (moduleConfig.remote_hardware.allow_undefined_pin_access) { + return true; + } - // Test to see if the pin is in the list of allowed pins and return true if found - if (mask & (1ULL << pin)) { - return true; - } + // Test to see if the pin is in the list of allowed pins and return true if found + if (mask & (1ULL << pin)) { + return true; + } - return false; + return false; } /// Set pin modes for every set bit in a mask -static void pinModes(uint64_t mask, uint8_t mode, uint64_t maskAvailable) { - for (uint64_t i = 0; i < NUM_GPIOS; i++) { - if (mask & (1ULL << i)) { - if (pinAccessAllowed(maskAvailable, i)) { - pinMode(i, mode); - } +static void pinModes(uint64_t mask, uint8_t mode, uint64_t maskAvailable) +{ + for (uint64_t i = 0; i < NUM_GPIOS; i++) { + if (mask & (1ULL << i)) { + if (pinAccessAllowed(maskAvailable, i)) { + pinMode(i, mode); + } + } } - } } /// Read all the pins mentioned in a mask -static uint64_t digitalReads(uint64_t mask, uint64_t maskAvailable) { - uint64_t res = 0; +static uint64_t digitalReads(uint64_t mask, uint64_t maskAvailable) +{ + uint64_t res = 0; - pinModes(mask, INPUT_PULLUP, maskAvailable); + pinModes(mask, INPUT_PULLUP, maskAvailable); - for (uint64_t i = 0; i < NUM_GPIOS; i++) { - uint64_t m = 1ULL << i; - if (mask & m && pinAccessAllowed(maskAvailable, i)) { - if (digitalRead(i)) { - res |= m; - } + for (uint64_t i = 0; i < NUM_GPIOS; i++) { + uint64_t m = 1ULL << i; + if (mask & m && pinAccessAllowed(maskAvailable, i)) { + if (digitalRead(i)) { + res |= m; + } + } } - } - return res; + return res; } RemoteHardwareModule::RemoteHardwareModule() : ProtobufModule("remotehardware", meshtastic_PortNum_REMOTE_HARDWARE_APP, &meshtastic_HardwareMessage_msg), - concurrency::OSThread("RemoteHardware") { - // restrict to the gpio channel for rx - boundChannel = Channels::gpioChannel; + concurrency::OSThread("RemoteHardware") +{ + // restrict to the gpio channel for rx + boundChannel = Channels::gpioChannel; - // Pull available pin allowlist from config and build a bitmask out of it for fast comparisons later - for (uint8_t i = 0; i < 4; i++) { - availablePins += 1ULL << moduleConfig.remote_hardware.available_pins[i].gpio_pin; - } + // Pull available pin allowlist from config and build a bitmask out of it for fast comparisons later + for (uint8_t i = 0; i < 4; i++) { + availablePins += 1ULL << moduleConfig.remote_hardware.available_pins[i].gpio_pin; + } } -bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_HardwareMessage *pptr) { - if (moduleConfig.remote_hardware.enabled) { - auto p = *pptr; - LOG_INFO("Received RemoteHardware type=%d", p.type); +bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_HardwareMessage *pptr) +{ + if (moduleConfig.remote_hardware.enabled) { + auto p = *pptr; + LOG_INFO("Received RemoteHardware type=%d", p.type); - switch (p.type) { - case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { - pinModes(p.gpio_mask, OUTPUT, availablePins); - for (uint8_t i = 0; i < NUM_GPIOS; i++) { - uint64_t mask = 1ULL << i; - if (p.gpio_mask & mask && pinAccessAllowed(availablePins, i)) { - digitalWrite(i, (p.gpio_value & mask) ? 1 : 0); + switch (p.type) { + case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { + pinModes(p.gpio_mask, OUTPUT, availablePins); + for (uint8_t i = 0; i < NUM_GPIOS; i++) { + uint64_t mask = 1ULL << i; + if (p.gpio_mask & mask && pinAccessAllowed(availablePins, i)) { + digitalWrite(i, (p.gpio_value & mask) ? 1 : 0); + } + } + + break; } - } - break; + case meshtastic_HardwareMessage_Type_READ_GPIOS: { + uint64_t res = digitalReads(p.gpio_mask, availablePins); + + // Send the reply + meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; + r.type = meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY; + r.gpio_value = res; + r.gpio_mask = p.gpio_mask; + meshtastic_MeshPacket *p2 = allocDataProtobuf(r); + setReplyTo(p2, req); + myReply = p2; + break; + } + + case meshtastic_HardwareMessage_Type_WATCH_GPIOS: { + watchGpios = p.gpio_mask; + lastWatchMsec = 0; // Force a new publish soon + previousWatch = + ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) + enabled = true; // Let our thread run at least once + setInterval(2000); // Set a new interval so we'll run soon + LOG_INFO("Now watching GPIOs 0x%llx", watchGpios); + break; + } + + case meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY: + case meshtastic_HardwareMessage_Type_GPIOS_CHANGED: + break; // Ignore - we might see our own replies + + default: + LOG_ERROR("Hardware operation %d not yet implemented! FIXME", p.type); + break; + } } - case meshtastic_HardwareMessage_Type_READ_GPIOS: { - uint64_t res = digitalReads(p.gpio_mask, availablePins); - - // Send the reply - meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; - r.type = meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY; - r.gpio_value = res; - r.gpio_mask = p.gpio_mask; - meshtastic_MeshPacket *p2 = allocDataProtobuf(r); - setReplyTo(p2, req); - myReply = p2; - break; - } - - case meshtastic_HardwareMessage_Type_WATCH_GPIOS: { - watchGpios = p.gpio_mask; - lastWatchMsec = 0; // Force a new publish soon - previousWatch = ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) - enabled = true; // Let our thread run at least once - setInterval(2000); // Set a new interval so we'll run soon - LOG_INFO("Now watching GPIOs 0x%llx", watchGpios); - break; - } - - case meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY: - case meshtastic_HardwareMessage_Type_GPIOS_CHANGED: - break; // Ignore - we might see our own replies - - default: - LOG_ERROR("Hardware operation %d not yet implemented! FIXME", p.type); - break; - } - } - - return false; + return false; } -int32_t RemoteHardwareModule::runOnce() { - if (moduleConfig.remote_hardware.enabled && watchGpios) { +int32_t RemoteHardwareModule::runOnce() +{ + if (moduleConfig.remote_hardware.enabled && watchGpios) { - if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) { - uint64_t curVal = digitalReads(watchGpios, availablePins); - lastWatchMsec = millis(); + if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) { + uint64_t curVal = digitalReads(watchGpios, availablePins); + lastWatchMsec = millis(); - if (curVal != previousWatch) { - previousWatch = curVal; - LOG_INFO("Broadcast GPIOS 0x%llx changed!", curVal); + if (curVal != previousWatch) { + previousWatch = curVal; + LOG_INFO("Broadcast GPIOS 0x%llx changed!", curVal); - // Something changed! Tell the world with a broadcast message - meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; - r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED; - r.gpio_value = curVal; - meshtastic_MeshPacket *p = allocDataProtobuf(r); - service->sendToMesh(p); - } + // Something changed! Tell the world with a broadcast message + meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; + r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED; + r.gpio_value = curVal; + meshtastic_MeshPacket *p = allocDataProtobuf(r); + service->sendToMesh(p); + } + } + } else { + // No longer watching anything - stop using CPU + return disable(); } - } else { - // No longer watching anything - stop using CPU - return disable(); - } - return 2000; // Poll our GPIOs every 2000ms + return 2000; // Poll our GPIOs every 2000ms } \ No newline at end of file diff --git a/src/modules/RemoteHardwareModule.h b/src/modules/RemoteHardwareModule.h index 7e77949ab..4dc31d405 100644 --- a/src/modules/RemoteHardwareModule.h +++ b/src/modules/RemoteHardwareModule.h @@ -6,41 +6,42 @@ /** * A module that provides easy low-level remote access to device hardware. */ -class RemoteHardwareModule : public ProtobufModule, private concurrency::OSThread { - /// The current set of GPIOs we've been asked to watch for changes - uint64_t watchGpios = 0; +class RemoteHardwareModule : public ProtobufModule, private concurrency::OSThread +{ + /// The current set of GPIOs we've been asked to watch for changes + uint64_t watchGpios = 0; - /// The previously read value of watched pins - uint64_t previousWatch = 0; + /// The previously read value of watched pins + uint64_t previousWatch = 0; - /// The timestamp of our last watch event (we throttle watches to 1 change every 30 seconds) - uint32_t lastWatchMsec = 0; + /// The timestamp of our last watch event (we throttle watches to 1 change every 30 seconds) + uint32_t lastWatchMsec = 0; - /// A bitmask of GPIOs that are exposed to the mesh if undefined access is not enabled - uint64_t availablePins = 0; + /// A bitmask of GPIOs that are exposed to the mesh if undefined access is not enabled + uint64_t availablePins = 0; -public: - /** Constructor - * name is for debugging output - */ - RemoteHardwareModule(); + public: + /** Constructor + * name is for debugging output + */ + RemoteHardwareModule(); -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_HardwareMessage *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_HardwareMessage *p) override; - /** - * Periodically read the gpios we have been asked to WATCH, if they have changed, - * broadcast a message with the change information. - * - * The method that will be called each time our thread gets a chance to run - * - * Returns desired period for next invocation (or RUN_SAME for no change) - */ - virtual int32_t runOnce() override; + /** + * Periodically read the gpios we have been asked to WATCH, if they have changed, + * broadcast a message with the change information. + * + * The method that will be called each time our thread gets a chance to run + * + * Returns desired period for next invocation (or RUN_SAME for no change) + */ + virtual int32_t runOnce() override; }; extern RemoteHardwareModule remoteHardwareModule; diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp index 2add898c5..434441d49 100644 --- a/src/modules/ReplyModule.cpp +++ b/src/modules/ReplyModule.cpp @@ -5,19 +5,20 @@ #include -meshtastic_MeshPacket *ReplyModule::allocReply() { - assert(currentRequest); // should always be !NULL +meshtastic_MeshPacket *ReplyModule::allocReply() +{ + assert(currentRequest); // should always be !NULL #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - auto req = *currentRequest; - auto &p = req.decoded; - // The incoming message is in p.payload - LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); + auto req = *currentRequest; + auto &p = req.decoded; + // The incoming message is in p.payload + LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); #endif - const char *replyStr = "Message Received"; - auto reply = allocDataPacket(); // Allocate a packet for sending - reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply - memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); + const char *replyStr = "Message Received"; + auto reply = allocDataPacket(); // Allocate a packet for sending + reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply + memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); - return reply; + return reply; } diff --git a/src/modules/ReplyModule.h b/src/modules/ReplyModule.h index 4f6e2146a..86d4172ed 100644 --- a/src/modules/ReplyModule.h +++ b/src/modules/ReplyModule.h @@ -4,16 +4,17 @@ /** * A simple example module that just replies with "Message received" to any message it receives. */ -class ReplyModule : public SinglePortModule { -public: - /** Constructor - * name is for debugging output - */ - ReplyModule() : SinglePortModule("reply", meshtastic_PortNum_REPLY_APP) {} +class ReplyModule : public SinglePortModule +{ + public: + /** Constructor + * name is for debugging output + */ + ReplyModule() : SinglePortModule("reply", meshtastic_PortNum_REPLY_APP) {} -protected: - /** For reply module we do all of our processing in the (normally optional) - * want_replies handling - */ - virtual meshtastic_MeshPacket *allocReply() override; + protected: + /** For reply module we do all of our processing in the (normally optional) + * want_replies handling + */ + virtual meshtastic_MeshPacket *allocReply() override; }; diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 5d9ea9a63..e9e1fc786 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -8,76 +8,84 @@ RoutingModule *routingModule; -bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *r) { - bool maybePKI = mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && mp.channel == 0 && !isBroadcast(mp.to); - // Beginning of logic whether to drop the packet based on Rebroadcast mode - if (mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && - (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY || - config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { - if (!maybePKI) - return false; - if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && - (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) - return false; - } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { - // Don't let licensed users to rebroadcast packets from unlicensed users - // If we know they are in-fact unlicensed - LOG_DEBUG("Packet from unlicensed user, ignoring packet"); - return false; - } - - printPacket("Routing sniffing", &mp); - router->sniffReceived(&mp, r); - - // FIXME - move this to a non promsicious PhoneAPI module? - // Note: we are careful not to send back packets that started with the phone back to the phone - if ((isBroadcast(mp.to) || isToUs(&mp)) && (mp.from != 0)) { - printPacket("Delivering rx packet", &mp); - service->handleFromRadio(&mp); - } - - return false; // Let others look at this message also if they want -} - -meshtastic_MeshPacket *RoutingModule::allocReply() { - assert(currentRequest); - - return NULL; -} - -void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, bool ackWantsAck) { - auto p = allocAckNak(err, to, idFrom, chIndex, hopLimit); - - // Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably - p->want_ack = ackWantsAck; - - router->sendLocal(p); // we sometimes send directly to the local node -} - -uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { - const int8_t hopsUsed = getHopsAway(mp); - if (hopsUsed >= 0) { - if (hopsUsed > (int32_t)(config.lora.hop_limit)) { -// In event mode, we never want to send packets with more than our default 3 hops. -#if !(EVENTMODE) // This falls through to the default. - return hopsUsed; // If the request used more hops than the limit, use the same amount of hops -#endif - } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { - return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different +bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *r) +{ + bool maybePKI = mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && mp.channel == 0 && !isBroadcast(mp.to); + // Beginning of logic whether to drop the packet based on Rebroadcast mode + if (mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag && + (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY || + config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { + if (!maybePKI) + return false; + if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && + (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) + return false; + } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { + // Don't let licensed users to rebroadcast packets from unlicensed users + // If we know they are in-fact unlicensed + LOG_DEBUG("Packet from unlicensed user, ignoring packet"); + return false; } - } - return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit + + printPacket("Routing sniffing", &mp); + router->sniffReceived(&mp, r); + + // FIXME - move this to a non promsicious PhoneAPI module? + // Note: we are careful not to send back packets that started with the phone back to the phone + if ((isBroadcast(mp.to) || isToUs(&mp)) && (mp.from != 0)) { + printPacket("Delivering rx packet", &mp); + service->handleFromRadio(&mp); + } + + return false; // Let others look at this message also if they want } -meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { - return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); +meshtastic_MeshPacket *RoutingModule::allocReply() +{ + assert(currentRequest); + + return NULL; } -RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { - isPromiscuous = true; +void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, + bool ackWantsAck) +{ + auto p = allocAckNak(err, to, idFrom, chIndex, hopLimit); - // moved the RebroadcastMode logic into handleReceivedProtobuf - // LocalOnly requires either the from or to to be a known node - // knownOnly specifically requires the from to be a known node. - encryptedOk = true; + // Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably + p->want_ack = ackWantsAck; + + router->sendLocal(p); // we sometimes send directly to the local node +} + +uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) +{ + const int8_t hopsUsed = getHopsAway(mp); + if (hopsUsed >= 0) { + if (hopsUsed > (int32_t)(config.lora.hop_limit)) { +// In event mode, we never want to send packets with more than our default 3 hops. +#if !(EVENTMODE) // This falls through to the default. + return hopsUsed; // If the request used more hops than the limit, use the same amount of hops +#endif + } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { + return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different + } + } + return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit +} + +meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit) +{ + return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); +} + +RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) +{ + isPromiscuous = true; + + // moved the RebroadcastMode logic into handleReceivedProtobuf + // LocalOnly requires either the from or to to be a known node + // knownOnly specifically requires the from to be a known node. + encryptedOk = true; } \ No newline at end of file diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index 2b0283c61..2ac42f447 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -5,36 +5,38 @@ /** * Routing module for router control messages */ -class RoutingModule : public ProtobufModule { -public: - /** Constructor - * name is for debugging output - */ - RoutingModule(); +class RoutingModule : public ProtobufModule +{ + public: + /** Constructor + * name is for debugging output + */ + RoutingModule(); - virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, - bool ackWantsAck = false); + virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, + bool ackWantsAck = false); - meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit = 0); - // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response - uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); + // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response + uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); -protected: - friend class Router; + protected: + friend class Router; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *p) override; + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Routing *p) override; - /** Messages can be received that have the want_response bit set. If set, this callback will be invoked - * so that subclasses can (optionally) send a response back to the original sender. */ - virtual meshtastic_MeshPacket *allocReply() override; + /** Messages can be received that have the want_response bit set. If set, this callback will be invoked + * so that subclasses can (optionally) send a response back to the original sender. */ + virtual meshtastic_MeshPacket *allocReply() override; - /// Override wantPacket to say we want to see all packets, not just those for our port number - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } + /// Override wantPacket to say we want to see all packets, not just those for our port number + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } }; extern RoutingModule *routingModule; \ No newline at end of file diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 57f2593b1..719e342b1 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -49,8 +49,8 @@ #include "meshSolarApp.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #define RX_BUFFER 256 #define TIMEOUT 250 @@ -63,55 +63,68 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M5) || \ - defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) -SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } +#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ + defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || \ + defined(MUZI_BASE) +SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") +{ + api_type = TYPE_SERIAL; +} static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL) -SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } +SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") +{ + api_type = TYPE_SERIAL; +} static Print *serialPrint = &Serial1; #else -SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } +SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") +{ + api_type = TYPE_SERIAL; +} static Print *serialPrint = &Serial2; #endif char serialBytes[512]; size_t serialPayloadSize; -bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) { - if (config.override_console_serial_port && - !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { - const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; - LOG_ERROR(warning); +bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) +{ + if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + const char *warning = + "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; + LOG_ERROR(warning); #if !IS_RUNNING_TESTS - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - cn->time = getValidTime(RTCQualityFromNet); - snprintf(cn->message, sizeof(cn->message), "%s", warning); - service->sendClientNotification(cn); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), "%s", warning); + service->sendClientNotification(cn); #endif - return false; - } + return false; + } - return true; + return true; } -SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { - switch (moduleConfig.serial.mode) { - case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG: - ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP; - break; - case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA: - case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO: - ourPortNum = meshtastic_PortNum_POSITION_APP; - break; - default: - ourPortNum = meshtastic_PortNum_SERIAL_APP; - // restrict to the serial channel for rx - boundChannel = Channels::serialChannel; - break; - } +SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") +{ + switch (moduleConfig.serial.mode) { + case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG: + ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP; + break; + case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA: + case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO: + ourPortNum = meshtastic_PortNum_POSITION_APP; + break; + default: + ourPortNum = meshtastic_PortNum_SERIAL_APP; + // restrict to the serial channel for rx + boundChannel = Channels::serialChannel; + break; + } } /** @@ -119,169 +132,172 @@ SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { * * @return true if the serial connection is established, false otherwise. * - * For the serial2 port we can't really detect if any client is on the other side, so instead just look for recent - * messages + * For the serial2 port we can't really detect if any client is on the other side, so instead just look for recent messages */ -bool SerialModule::checkIsConnected() { return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); } +bool SerialModule::checkIsConnected() +{ + return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); +} -int32_t SerialModule::runOnce() { - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ +int32_t SerialModule::runOnce() +{ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.serial.enabled = true; - // moduleConfig.serial.rxd = 35; - // moduleConfig.serial.txd = 15; - // moduleConfig.serial.override_console_serial_port = true; - // moduleConfig.serial.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; - // moduleConfig.serial.timeout = 1000; - // moduleConfig.serial.echo = 1; + // moduleConfig.serial.enabled = true; + // moduleConfig.serial.rxd = 35; + // moduleConfig.serial.txd = 15; + // moduleConfig.serial.override_console_serial_port = true; + // moduleConfig.serial.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + // moduleConfig.serial.timeout = 1000; + // moduleConfig.serial.echo = 1; - if (!moduleConfig.serial.enabled) - return disable(); + if (!moduleConfig.serial.enabled) + return disable(); - if (moduleConfig.serial.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) { - if (firstTime) { - // Interface with the serial peripheral from in here. - LOG_INFO("Init serial peripheral interface"); + if (moduleConfig.serial.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) { + if (firstTime) { + // Interface with the serial peripheral from in here. + LOG_INFO("Init serial peripheral interface"); - uint32_t baud = getBaudRate(); + uint32_t baud = getBaudRate(); - if (moduleConfig.serial.override_console_serial_port) { + if (moduleConfig.serial.override_console_serial_port) { #ifdef RP2040_SLOW_CLOCK - Serial2.flush(); - serialPrint = &Serial2; + Serial2.flush(); + serialPrint = &Serial2; #else - Serial.flush(); - serialPrint = &Serial; + Serial.flush(); + serialPrint = &Serial; #endif - // Give it a chance to flush out 💩 - delay(10); - } + // Give it a chance to flush out 💩 + delay(10); + } #if defined(CONFIG_IDF_TARGET_ESP32C6) - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { - Serial1.setRxBufferSize(RX_BUFFER); - Serial1.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); - } else { - Serial.begin(baud); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); - } + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + Serial1.setRxBufferSize(RX_BUFFER); + Serial1.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); + } else { + Serial.begin(baud); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + } #elif defined(ARCH_STM32WL) #ifndef RAK3172 - HardwareSerial *serialInstance = &Serial2; + HardwareSerial *serialInstance = &Serial2; #else - HardwareSerial *serialInstance = &Serial1; + HardwareSerial *serialInstance = &Serial1; #endif - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { - serialInstance->setTx(moduleConfig.serial.txd); - serialInstance->setRx(moduleConfig.serial.rxd); - } - serialInstance->begin(baud); - serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + serialInstance->setTx(moduleConfig.serial.txd); + serialInstance->setRx(moduleConfig.serial.rxd); + } + serialInstance->begin(baud); + serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #elif defined(ARCH_ESP32) - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { - Serial2.setRxBufferSize(RX_BUFFER); - Serial2.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); - } else { - Serial.begin(baud); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); - } -#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ - !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) - if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { -#ifdef ARCH_RP2040 - Serial2.setFIFOSize(RX_BUFFER); - Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd); -#else - Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd); -#endif - Serial2.begin(baud, SERIAL_8N1); - Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); - } else { -#ifdef RP2040_SLOW_CLOCK - Serial2.begin(baud, SERIAL_8N1); - Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); -#else - Serial.begin(baud, SERIAL_8N1); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); -#endif - } -#else - Serial.begin(baud, SERIAL_8N1); - Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); -#endif - serialModuleRadio = new SerialModuleRadio(); - - firstTime = 0; - - // in API mode send rebooted sequence - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { - emitRebooted(); - } - } else { - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { - return runOncePart(); - } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) { - // in NMEA mode send out GGA every 2 seconds, Don't read from Port - if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) { - lastNmeaTime = millis(); - printGGA(outbuf, sizeof(outbuf), localPosition); - serialPrint->printf("%s", outbuf); - } - } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) { - if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) { - lastNmeaTime = millis(); - uint32_t readIndex = 0; - const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); - while (tempNodeInfo != NULL) { - if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { - printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); - serialPrint->printf("%s", outbuf); + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + Serial2.setRxBufferSize(RX_BUFFER); + Serial2.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd); + } else { + Serial.begin(baud); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } - tempNodeInfo = nodeDB->readNextMeshNode(readIndex); - } - } - } - -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ - !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) - else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { - processWXSerial(); - - } -#if defined(HELTEC_MESH_SOLAR) - else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { - serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); - // If the parsing fails, the following parsing will be performed. - if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { - return runOncePart(serialBytes, serialPayloadSize); - } - } +#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ + !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { +#ifdef ARCH_RP2040 + Serial2.setFIFOSize(RX_BUFFER); + Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd); +#else + Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd); #endif - else { + Serial2.begin(baud, SERIAL_8N1); + Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); + } else { +#ifdef RP2040_SLOW_CLOCK + Serial2.begin(baud, SERIAL_8N1); + Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); +#else + Serial.begin(baud, SERIAL_8N1); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); +#endif + } +#else + Serial.begin(baud, SERIAL_8N1); + Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); +#endif + serialModuleRadio = new SerialModuleRadio(); + + firstTime = 0; + + // in API mode send rebooted sequence + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { + emitRebooted(); + } + } else { + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { + return runOncePart(); + } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) { + // in NMEA mode send out GGA every 2 seconds, Don't read from Port + if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) { + lastNmeaTime = millis(); + printGGA(outbuf, sizeof(outbuf), localPosition); + serialPrint->printf("%s", outbuf); + } + } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) { + if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) { + lastNmeaTime = millis(); + uint32_t readIndex = 0; + const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); + while (tempNodeInfo != NULL) { + if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { + printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); + serialPrint->printf("%s", outbuf); + } + tempNodeInfo = nodeDB->readNextMeshNode(readIndex); + } + } + } + +#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ + !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { + processWXSerial(); + + } +#if defined(HELTEC_MESH_SOLAR) + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); + // If the parsing fails, the following parsing will be performed. + if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { + return runOncePart(serialBytes, serialPayloadSize); + } + } +#endif + else { #if defined(CONFIG_IDF_TARGET_ESP32C6) - while (Serial1.available()) { - serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + while (Serial1.available()) { + serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #else #ifndef RAK3172 - HardwareSerial *serialInstance = &Serial2; + HardwareSerial *serialInstance = &Serial2; #else - HardwareSerial *serialInstance = &Serial1; + HardwareSerial *serialInstance = &Serial1; #endif - while (serialInstance->available()) { - serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + while (serialInstance->available()) { + serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); +#endif + serialModuleRadio->sendPayload(); + } + } #endif - serialModuleRadio->sendPayload(); } - } -#endif + return (10); + } else { + return disable(); } - return (10); - } else { - return disable(); - } } /** @@ -293,19 +309,21 @@ int32_t SerialModule::runOnce() { * * @throws None */ -void SerialModule::sendTelemetry(meshtastic_Telemetry m) { - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Telemetry_msg, &m); - p->to = NODENUM_BROADCAST; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) { - p->want_ack = true; - p->priority = meshtastic_MeshPacket_Priority_HIGH; - } else { - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - } - service->sendToMesh(p, RX_SRC_LOCAL, true); +void SerialModule::sendTelemetry(meshtastic_Telemetry m) +{ + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Telemetry_msg, &m); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) { + p->want_ack = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + } else { + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + } + service->sendToMesh(p, RX_SRC_LOCAL, true); } /** @@ -313,10 +331,11 @@ void SerialModule::sendTelemetry(meshtastic_Telemetry m) { * * @return A pointer to the newly allocated mesh packet. */ -meshtastic_MeshPacket *SerialModuleRadio::allocReply() { - auto reply = allocDataPacket(); // Allocate a packet for sending +meshtastic_MeshPacket *SerialModuleRadio::allocReply() +{ + auto reply = allocDataPacket(); // Allocate a packet for sending - return reply; + return reply; } /** @@ -325,21 +344,22 @@ meshtastic_MeshPacket *SerialModuleRadio::allocReply() { * @param dest The destination node number. * @param wantReplies Whether or not to request replies from the destination node. */ -void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies) { - const meshtastic_Channel *ch = (boundChannel != NULL) ? &channels.getByName(boundChannel) : NULL; - meshtastic_MeshPacket *p = allocReply(); - p->to = dest; - if (ch != NULL) { - p->channel = ch->index; - } - p->decoded.want_response = wantReplies; +void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies) +{ + const meshtastic_Channel *ch = (boundChannel != NULL) ? &channels.getByName(boundChannel) : NULL; + meshtastic_MeshPacket *p = allocReply(); + p->to = dest; + if (ch != NULL) { + p->channel = ch->index; + } + p->decoded.want_response = wantReplies; - p->want_ack = ACK; + p->want_ack = ACK; - p->decoded.payload.size = serialPayloadSize; // You must specify how many bytes are in the reply - memcpy(p->decoded.payload.bytes, serialBytes, p->decoded.payload.size); + p->decoded.payload.size = serialPayloadSize; // You must specify how many bytes are in the reply + memcpy(p->decoded.payload.bytes, serialBytes, p->decoded.payload.size); - service->sendToMesh(p); + service->sendToMesh(p); } /** @@ -348,65 +368,66 @@ void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies) { * @param mp The received mesh packet. * @return The processed message. */ -ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) { - if (moduleConfig.serial.enabled) { - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { - // in API mode we don't care about stuff from radio. - return ProcessMessage::CONTINUE; - } - - auto &p = mp.decoded; - // LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", - // nodeDB->getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); - - if (isFromUs(&mp)) { - - /* - * If moduleConfig.serial.echo is true, then echo the packets that are sent out - * back to the TX of the serial interface. - */ - if (moduleConfig.serial.echo) { - - // For some reason, we get the packet back twice when we send out of the radio. - // TODO: need to find out why. - if (lastRxID != mp.id) { - lastRxID = mp.id; - // LOG_DEBUG("* * Message came this device"); - // serialPrint->println("* * Message came this device"); - serialPrint->printf("%s", p.payload.bytes); +ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (moduleConfig.serial.enabled) { + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { + // in API mode we don't care about stuff from radio. + return ProcessMessage::CONTINUE; } - } - } else { - if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT || - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) { - serialPrint->write(p.payload.bytes, p.payload.size); - } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - const char *sender = (node && node->has_user) ? node->user.short_name : "???"; - serialPrint->println(); - serialPrint->printf("%s: %s", sender, p.payload.bytes); - serialPrint->println(); - } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA || - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && - HAS_GPS) { - // Decode the Payload some more - meshtastic_Position scratch; - meshtastic_Position *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) { - decoded = &scratch; - } - // send position packet as WPL to the serial port - printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); - serialPrint->printf("%s", outbuf); + auto &p = mp.decoded; + // LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", + // nodeDB->getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); + + if (isFromUs(&mp)) { + + /* + * If moduleConfig.serial.echo is true, then echo the packets that are sent out + * back to the TX of the serial interface. + */ + if (moduleConfig.serial.echo) { + + // For some reason, we get the packet back twice when we send out of the radio. + // TODO: need to find out why. + if (lastRxID != mp.id) { + lastRxID = mp.id; + // LOG_DEBUG("* * Message came this device"); + // serialPrint->println("* * Message came this device"); + serialPrint->printf("%s", p.payload.bytes); + } + } + } else { + + if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT || + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) { + serialPrint->write(p.payload.bytes, p.payload.size); + } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + const char *sender = (node && node->has_user) ? node->user.short_name : "???"; + serialPrint->println(); + serialPrint->printf("%s: %s", sender, p.payload.bytes); + serialPrint->println(); + } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA || + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && + HAS_GPS) { + // Decode the Payload some more + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + } + // send position packet as WPL to the serial port + printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); + serialPrint->printf("%s", outbuf); + } + } } - } } - } - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** @@ -414,45 +435,46 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp * * @return uint32_t The baud rate of the serial module. */ -uint32_t SerialModule::getBaudRate() { - if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110) { - return 110; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300) { - return 300; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600) { - return 600; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200) { - return 1200; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400) { - return 2400; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800) { - return 4800; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600) { - return 9600; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200) { - return 19200; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400) { - return 38400; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600) { - return 57600; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200) { - return 115200; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400) { - return 230400; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800) { - return 460800; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000) { - return 576000; - } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600) { - return 921600; - } - return BAUD; +uint32_t SerialModule::getBaudRate() +{ + if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110) { + return 110; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300) { + return 300; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600) { + return 600; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200) { + return 1200; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400) { + return 2400; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800) { + return 4800; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600) { + return 9600; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200) { + return 19200; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400) { + return 38400; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600) { + return 57600; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200) { + return 115200; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400) { + return 230400; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800) { + return 460800; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000) { + return 576000; + } else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600) { + return 921600; + } + return BAUD; } // Add this structure to help with parsing WindGust = 24.4 serial lines. struct ParsedLine { - char name[64]; - char value[128]; + char name[64]; + char value[128]; }; /** @@ -460,49 +482,50 @@ struct ParsedLine { * @param line Input line to parse * @return ParsedLine containing name and value, or empty strings if parse failed */ -ParsedLine parseLine(const char *line) { - ParsedLine result = {"", ""}; +ParsedLine parseLine(const char *line) +{ + ParsedLine result = {"", ""}; + + // Find equals sign + const char *equals = strchr(line, '='); + if (!equals) { + return result; + } + + // Extract name by copying substring + char nameBuf[64]; // Temporary buffer + size_t nameLen = equals - line; + if (nameLen >= sizeof(nameBuf)) { + nameLen = sizeof(nameBuf) - 1; + } + strncpy(nameBuf, line, nameLen); + nameBuf[nameLen] = '\0'; + + // Trim whitespace from name + char *nameStart = nameBuf; + while (*nameStart && isspace(*nameStart)) + nameStart++; + char *nameEnd = nameStart + strlen(nameStart) - 1; + while (nameEnd > nameStart && isspace(*nameEnd)) + *nameEnd-- = '\0'; + + // Copy trimmed name + strncpy(result.name, nameStart, sizeof(result.name) - 1); + result.name[sizeof(result.name) - 1] = '\0'; + + // Extract value part (after equals) + const char *valueStart = equals + 1; + while (*valueStart && isspace(*valueStart)) + valueStart++; + strncpy(result.value, valueStart, sizeof(result.value) - 1); + result.value[sizeof(result.value) - 1] = '\0'; + + // Trim trailing whitespace from value + char *valueEnd = result.value + strlen(result.value) - 1; + while (valueEnd > result.value && isspace(*valueEnd)) + *valueEnd-- = '\0'; - // Find equals sign - const char *equals = strchr(line, '='); - if (!equals) { return result; - } - - // Extract name by copying substring - char nameBuf[64]; // Temporary buffer - size_t nameLen = equals - line; - if (nameLen >= sizeof(nameBuf)) { - nameLen = sizeof(nameBuf) - 1; - } - strncpy(nameBuf, line, nameLen); - nameBuf[nameLen] = '\0'; - - // Trim whitespace from name - char *nameStart = nameBuf; - while (*nameStart && isspace(*nameStart)) - nameStart++; - char *nameEnd = nameStart + strlen(nameStart) - 1; - while (nameEnd > nameStart && isspace(*nameEnd)) - *nameEnd-- = '\0'; - - // Copy trimmed name - strncpy(result.name, nameStart, sizeof(result.name) - 1); - result.name[sizeof(result.name) - 1] = '\0'; - - // Extract value part (after equals) - const char *valueStart = equals + 1; - while (*valueStart && isspace(*valueStart)) - valueStart++; - strncpy(result.value, valueStart, sizeof(result.value) - 1); - result.value[sizeof(result.value) - 1] = '\0'; - - // Trim trailing whitespace from value - char *valueEnd = result.value + strlen(result.value) - 1; - while (valueEnd > result.value && isspace(*valueEnd)) - *valueEnd-- = '\0'; - - return result; } /** @@ -511,185 +534,188 @@ ParsedLine parseLine(const char *line) { * * @return void */ -void SerialModule::processWXSerial() { -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && \ - !defined(MUZI_BASE) - static unsigned int lastAveraged = 0; - static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. - static double dir_sum_sin = 0; - static double dir_sum_cos = 0; - static float velSum = 0; - static float gust = 0; - static float lull = -1; - static int velCount = 0; - static int dirCount = 0; - static char windDir[4] = "xxx"; // Assuming windDir is 3 characters long + null terminator - static char windVel[5] = "xx.x"; // Assuming windVel is 4 characters long + null terminator - static char windGust[5] = "xx.x"; // Assuming windGust is 4 characters long + null terminator - static char batVoltage[5] = "0.0V"; - static char capVoltage[5] = "0.0V"; - static char temperature[5] = "00.0"; - static float batVoltageF = 0; - static float capVoltageF = 0; - static float temperatureF = 0; +void SerialModule::processWXSerial() +{ +#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \ + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ + !defined(ARCH_STM32WL) && !defined(MUZI_BASE) + static unsigned int lastAveraged = 0; + static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. + static double dir_sum_sin = 0; + static double dir_sum_cos = 0; + static float velSum = 0; + static float gust = 0; + static float lull = -1; + static int velCount = 0; + static int dirCount = 0; + static char windDir[4] = "xxx"; // Assuming windDir is 3 characters long + null terminator + static char windVel[5] = "xx.x"; // Assuming windVel is 4 characters long + null terminator + static char windGust[5] = "xx.x"; // Assuming windGust is 4 characters long + null terminator + static char batVoltage[5] = "0.0V"; + static char capVoltage[5] = "0.0V"; + static char temperature[5] = "00.0"; + static float batVoltageF = 0; + static float capVoltageF = 0; + static float temperatureF = 0; - static char rainStr[] = "5780860000"; - static int rainSum = 0; - static float rain = 0; - bool gotwind = false; + static char rainStr[] = "5780860000"; + static int rainSum = 0; + static float rain = 0; + bool gotwind = false; - while (Serial2.available()) { - // clear serialBytes buffer - memset(serialBytes, '\0', sizeof(serialBytes)); - // memset(formattedString, '\0', sizeof(formattedString)); - serialPayloadSize = Serial2.readBytes(serialBytes, 512); - // check for a strings we care about - // example output of serial data fields from the WS85 - // WindDir = 79 - // WindSpeed = 0.5 - // WindGust = 0.6 - // GXTS04Temp = 24.4 - // Temperature = 23.4 // WS80 + while (Serial2.available()) { + // clear serialBytes buffer + memset(serialBytes, '\0', sizeof(serialBytes)); + // memset(formattedString, '\0', sizeof(formattedString)); + serialPayloadSize = Serial2.readBytes(serialBytes, 512); + // check for a strings we care about + // example output of serial data fields from the WS85 + // WindDir = 79 + // WindSpeed = 0.5 + // WindGust = 0.6 + // GXTS04Temp = 24.4 + // Temperature = 23.4 // WS80 - // RainIntSum = 0 - // Rain = 0.0 - if (serialPayloadSize > 0) { - // Define variables for line processing - int lineStart = 0; - int lineEnd = -1; + // RainIntSum = 0 + // Rain = 0.0 + if (serialPayloadSize > 0) { + // Define variables for line processing + int lineStart = 0; + int lineEnd = -1; - // Process each byte in the received data - for (size_t i = 0; i < serialPayloadSize; i++) { - // go until we hit the end of line and then process the line - if (serialBytes[i] == '\n') { - lineEnd = i; - // Extract the current line - char line[meshtastic_Constants_DATA_PAYLOAD_LEN]; - memset(line, '\0', sizeof(line)); - if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) { - memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); + // Process each byte in the received data + for (size_t i = 0; i < serialPayloadSize; i++) { + // go until we hit the end of line and then process the line + if (serialBytes[i] == '\n') { + lineEnd = i; + // Extract the current line + char line[meshtastic_Constants_DATA_PAYLOAD_LEN]; + memset(line, '\0', sizeof(line)); + if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) { + memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); - ParsedLine parsed = parseLine(line); - if (strlen(parsed.name) > 0) { - if (strcmp(parsed.name, "WindDir") == 0) { - strlcpy(windDir, parsed.value, sizeof(windDir)); - double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); - dir_sum_sin += sin(radians); - dir_sum_cos += cos(radians); - dirCount++; - gotwind = true; - } else if (strcmp(parsed.name, "WindSpeed") == 0) { - strlcpy(windVel, parsed.value, sizeof(windVel)); - float newv = strtof(windVel, nullptr); - velSum += newv; - velCount++; - if (newv < lull || lull == -1) { - lull = newv; + ParsedLine parsed = parseLine(line); + if (strlen(parsed.name) > 0) { + if (strcmp(parsed.name, "WindDir") == 0) { + strlcpy(windDir, parsed.value, sizeof(windDir)); + double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); + dir_sum_sin += sin(radians); + dir_sum_cos += cos(radians); + dirCount++; + gotwind = true; + } else if (strcmp(parsed.name, "WindSpeed") == 0) { + strlcpy(windVel, parsed.value, sizeof(windVel)); + float newv = strtof(windVel, nullptr); + velSum += newv; + velCount++; + if (newv < lull || lull == -1) { + lull = newv; + } + gotwind = true; + } else if (strcmp(parsed.name, "WindGust") == 0) { + strlcpy(windGust, parsed.value, sizeof(windGust)); + float newg = strtof(windGust, nullptr); + if (newg > gust) { + gust = newg; + } + gotwind = true; + } else if (strcmp(parsed.name, "BatVoltage") == 0) { + strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); + batVoltageF = strtof(batVoltage, nullptr); + break; // last possible data we want so break + } else if (strcmp(parsed.name, "CapVoltage") == 0) { + strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); + capVoltageF = strtof(capVoltage, nullptr); + } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { + strlcpy(temperature, parsed.value, sizeof(temperature)); + temperatureF = strtof(temperature, nullptr); + } else if (strcmp(parsed.name, "RainIntSum") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); + rainSum = int(strtof(rainStr, nullptr)); + } else if (strcmp(parsed.name, "Rain") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); + rain = strtof(rainStr, nullptr); + } + } + + // Update lineStart for the next line + lineStart = lineEnd + 1; + } } - gotwind = true; - } else if (strcmp(parsed.name, "WindGust") == 0) { - strlcpy(windGust, parsed.value, sizeof(windGust)); - float newg = strtof(windGust, nullptr); - if (newg > gust) { - gust = newg; - } - gotwind = true; - } else if (strcmp(parsed.name, "BatVoltage") == 0) { - strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); - batVoltageF = strtof(batVoltage, nullptr); - break; // last possible data we want so break - } else if (strcmp(parsed.name, "CapVoltage") == 0) { - strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); - capVoltageF = strtof(capVoltage, nullptr); - } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { - strlcpy(temperature, parsed.value, sizeof(temperature)); - temperatureF = strtof(temperature, nullptr); - } else if (strcmp(parsed.name, "RainIntSum") == 0) { - strlcpy(rainStr, parsed.value, sizeof(rainStr)); - rainSum = int(strtof(rainStr, nullptr)); - } else if (strcmp(parsed.name, "Rain") == 0) { - strlcpy(rainStr, parsed.value, sizeof(rainStr)); - rain = strtof(rainStr, nullptr); - } } - - // Update lineStart for the next line - lineStart = lineEnd + 1; - } + break; + // clear the input buffer + while (Serial2.available() > 0) { + Serial2.read(); // Read and discard the bytes in the input buffer + } } - } - break; - // clear the input buffer - while (Serial2.available() > 0) { - Serial2.read(); // Read and discard the bytes in the input buffer - } } - } - if (gotwind) { + if (gotwind) { - LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr), - batVoltageF, capVoltageF, temperatureF, rain, rainSum); - } - if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { - // calculate averages and send to the mesh - float velAvg = 1.0 * velSum / velCount; - - double avgSin = dir_sum_sin / dirCount; - double avgCos = dir_sum_cos / dirCount; - - double avgRadians = atan2(avgSin, avgCos); - float dirAvg = GeoCoord::toDegrees(avgRadians); - - if (dirAvg < 0) { - dirAvg += 360.0; + LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), + strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum); } - lastAveraged = millis(); + if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { + // calculate averages and send to the mesh + float velAvg = 1.0 * velSum / velCount; - // make a telemetry packet with the data - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_environment_metrics_tag; + double avgSin = dir_sum_sin / dirCount; + double avgCos = dir_sum_cos / dirCount; - m.variant.environment_metrics.wind_speed = velAvg; - m.variant.environment_metrics.has_wind_speed = true; + double avgRadians = atan2(avgSin, avgCos); + float dirAvg = GeoCoord::toDegrees(avgRadians); - m.variant.environment_metrics.wind_direction = dirAvg; - m.variant.environment_metrics.has_wind_direction = true; + if (dirAvg < 0) { + dirAvg += 360.0; + } + lastAveraged = millis(); - m.variant.environment_metrics.temperature = temperatureF; - m.variant.environment_metrics.has_temperature = true; + // make a telemetry packet with the data + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_environment_metrics_tag; - m.variant.environment_metrics.voltage = capVoltageF > batVoltageF ? capVoltageF : batVoltageF; // send the larger of the two voltage values. - m.variant.environment_metrics.has_voltage = true; + m.variant.environment_metrics.wind_speed = velAvg; + m.variant.environment_metrics.has_wind_speed = true; - m.variant.environment_metrics.wind_gust = gust; - m.variant.environment_metrics.has_wind_gust = true; + m.variant.environment_metrics.wind_direction = dirAvg; + m.variant.environment_metrics.has_wind_direction = true; - m.variant.environment_metrics.rainfall_24h = rainSum; - m.variant.environment_metrics.has_rainfall_24h = true; + m.variant.environment_metrics.temperature = temperatureF; + m.variant.environment_metrics.has_temperature = true; - // not sure if this value is actually the 1hr sum so needs to do some testing - m.variant.environment_metrics.rainfall_1h = rain; - m.variant.environment_metrics.has_rainfall_1h = true; + m.variant.environment_metrics.voltage = + capVoltageF > batVoltageF ? capVoltageF : batVoltageF; // send the larger of the two voltage values. + m.variant.environment_metrics.has_voltage = true; - if (lull == -1) - lull = 0; - m.variant.environment_metrics.wind_lull = lull; - m.variant.environment_metrics.has_wind_lull = true; + m.variant.environment_metrics.wind_gust = gust; + m.variant.environment_metrics.has_wind_gust = true; - LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f", m.variant.environment_metrics.wind_speed, - m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust, - m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature); + m.variant.environment_metrics.rainfall_24h = rainSum; + m.variant.environment_metrics.has_rainfall_24h = true; - sendTelemetry(m); + // not sure if this value is actually the 1hr sum so needs to do some testing + m.variant.environment_metrics.rainfall_1h = rain; + m.variant.environment_metrics.has_rainfall_1h = true; - // reset counters and gust/lull - velSum = velCount = dirCount = 0; - dir_sum_sin = dir_sum_cos = 0; - gust = 0; - lull = -1; - } + if (lull == -1) + lull = 0; + m.variant.environment_metrics.wind_lull = lull; + m.variant.environment_metrics.has_wind_lull = true; + + LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f", + m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction, + m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust, + m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature); + + sendTelemetry(m); + + // reset counters and gust/lull + velSum = velCount = dirCount = 0; + dir_sum_sin = dir_sum_cos = 0; + gust = 0; + lull = -1; + } #endif - return; + return; } #endif \ No newline at end of file diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index 7dbd0e3c2..dbe4f75db 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -8,29 +8,30 @@ #include #include -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) -class SerialModule : public StreamAPI, private concurrency::OSThread { - bool firstTime = 1; - unsigned long lastNmeaTime = millis(); - char outbuf[90] = ""; +class SerialModule : public StreamAPI, private concurrency::OSThread +{ + bool firstTime = 1; + unsigned long lastNmeaTime = millis(); + char outbuf[90] = ""; -public: - SerialModule(); + public: + SerialModule(); - static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); + static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; -private: - uint32_t getBaudRate(); - void sendTelemetry(meshtastic_Telemetry m); - void processWXSerial(); + private: + uint32_t getBaudRate(); + void sendTelemetry(meshtastic_Telemetry m); + void processWXSerial(); }; extern SerialModule *serialModule; @@ -39,39 +40,41 @@ extern SerialModule *serialModule; * Radio interface for SerialModule * */ -class SerialModuleRadio : public MeshModule { - uint32_t lastRxID = 0; - char outbuf[90] = ""; +class SerialModuleRadio : public MeshModule +{ + uint32_t lastRxID = 0; + char outbuf[90] = ""; -public: - SerialModuleRadio(); + public: + SerialModuleRadio(); - /** - * Send our payload into the mesh - */ - void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); -protected: - virtual meshtastic_MeshPacket *allocReply() override; + protected: + virtual meshtastic_MeshPacket *allocReply() override; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - meshtastic_PortNum ourPortNum; + meshtastic_PortNum ourPortNum; - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; } - meshtastic_MeshPacket *allocDataPacket() { - // Update our local node info with our position (even if we don't decide to update anyone else) - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = ourPortNum; + meshtastic_MeshPacket *allocDataPacket() + { + // Update our local node info with our position (even if we don't decide to update anyone else) + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = ourPortNum; - return p; - } + return p; + } }; extern SerialModuleRadio *serialModuleRadio; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index 4ea48d097..8738c16ca 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -9,104 +9,107 @@ It reflects charging, charged, discharging, and Bluetooth connection states usin */ StatusLEDModule *statusLEDModule; -StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { - bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); - powerStatusObserver.observe(&powerStatus->onNewStatus); +StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") +{ + bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); + powerStatusObserver.observe(&powerStatus->onNewStatus); } -int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) { - switch (arg->getStatusType()) { - case STATUS_TYPE_POWER: { - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; - if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { - power_state = charging; - if (powerStatus->getBatteryChargePercent() >= 100) { - power_state = charged; - } - } else { - if (powerStatus->getBatteryChargePercent() > 5) { - power_state = discharging; - } else { - power_state = critical; - } - } - break; - } - case STATUS_TYPE_BLUETOOTH: { - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg; - switch (bluetoothStatus->getConnectionState()) { - case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { - ble_state = unpaired; - PAIRING_LED_starttime = millis(); - break; - } - case meshtastic::BluetoothStatus::ConnectionState::PAIRING: { - ble_state = pairing; - PAIRING_LED_starttime = millis(); - break; - } - case meshtastic::BluetoothStatus::ConnectionState::CONNECTED: { - ble_state = connected; - PAIRING_LED_starttime = millis(); - break; - } +int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) +{ + switch (arg->getStatusType()) { + case STATUS_TYPE_POWER: { + meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; + if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { + power_state = charging; + if (powerStatus->getBatteryChargePercent() >= 100) { + power_state = charged; + } + } else { + if (powerStatus->getBatteryChargePercent() > 5) { + power_state = discharging; + } else { + power_state = critical; + } + } + break; } + case STATUS_TYPE_BLUETOOTH: { + meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg; + switch (bluetoothStatus->getConnectionState()) { + case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { + ble_state = unpaired; + PAIRING_LED_starttime = millis(); + break; + } + case meshtastic::BluetoothStatus::ConnectionState::PAIRING: { + ble_state = pairing; + PAIRING_LED_starttime = millis(); + break; + } + case meshtastic::BluetoothStatus::ConnectionState::CONNECTED: { + ble_state = connected; + PAIRING_LED_starttime = millis(); + break; + } + } - break; - } - } - return 0; + break; + } + } + return 0; }; -int32_t StatusLEDModule::runOnce() { - my_interval = 1000; +int32_t StatusLEDModule::runOnce() +{ + my_interval = 1000; + + if (power_state == charging) { + CHARGE_LED_state = !CHARGE_LED_state; + } else if (power_state == charged) { + CHARGE_LED_state = LED_STATE_ON; + } else if (power_state == critical) { + if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { + doing_fast_blink = true; + POWER_LED_starttime = millis(); + } + if (doing_fast_blink) { + PAIRING_LED_state = LED_STATE_OFF; + CHARGE_LED_state = !CHARGE_LED_state; + my_interval = 250; + if (POWER_LED_starttime + 2000 < millis()) { + doing_fast_blink = false; + } + } else { + CHARGE_LED_state = LED_STATE_OFF; + } - if (power_state == charging) { - CHARGE_LED_state = !CHARGE_LED_state; - } else if (power_state == charged) { - CHARGE_LED_state = LED_STATE_ON; - } else if (power_state == critical) { - if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { - doing_fast_blink = true; - POWER_LED_starttime = millis(); - } - if (doing_fast_blink) { - PAIRING_LED_state = LED_STATE_OFF; - CHARGE_LED_state = !CHARGE_LED_state; - my_interval = 250; - if (POWER_LED_starttime + 2000 < millis()) { - doing_fast_blink = false; - } } else { - CHARGE_LED_state = LED_STATE_OFF; + CHARGE_LED_state = LED_STATE_OFF; } - } else { - CHARGE_LED_state = LED_STATE_OFF; - } - - if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { - PAIRING_LED_state = LED_STATE_OFF; - } else if (ble_state == unpaired) { - if (slowTrack) { - PAIRING_LED_state = !PAIRING_LED_state; - slowTrack = false; + if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { + PAIRING_LED_state = LED_STATE_OFF; + } else if (ble_state == unpaired) { + if (slowTrack) { + PAIRING_LED_state = !PAIRING_LED_state; + slowTrack = false; + } else { + slowTrack = true; + } + } else if (ble_state == pairing) { + PAIRING_LED_state = !PAIRING_LED_state; } else { - slowTrack = true; + PAIRING_LED_state = LED_STATE_ON; } - } else if (ble_state == pairing) { - PAIRING_LED_state = !PAIRING_LED_state; - } else { - PAIRING_LED_state = LED_STATE_ON; - } #ifdef LED_CHARGE - digitalWrite(LED_CHARGE, CHARGE_LED_state); + digitalWrite(LED_CHARGE, CHARGE_LED_state); #endif - // digitalWrite(green_LED_PIN, LED_STATE_OFF); + // digitalWrite(green_LED_PIN, LED_STATE_OFF); #ifdef LED_PAIRING - digitalWrite(LED_PAIRING, PAIRING_LED_state); + digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif - return (my_interval); + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index 63594657c..d90ff718c 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -8,38 +8,39 @@ #include #include -class StatusLEDModule : private concurrency::OSThread { - bool slowTrack = false; +class StatusLEDModule : private concurrency::OSThread +{ + bool slowTrack = false; -public: - StatusLEDModule(); + public: + StatusLEDModule(); - int handleStatusUpdate(const meshtastic::Status *); + int handleStatusUpdate(const meshtastic::Status *); -protected: - unsigned int my_interval = 1000; // interval in millisconds - virtual int32_t runOnce() override; + protected: + unsigned int my_interval = 1000; // interval in millisconds + virtual int32_t runOnce() override; - CallbackObserver bluetoothStatusObserver = - CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); - CallbackObserver powerStatusObserver = - CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver bluetoothStatusObserver = + CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver powerStatusObserver = + CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); -private: - bool CHARGE_LED_state = LED_STATE_OFF; - bool PAIRING_LED_state = LED_STATE_OFF; + private: + bool CHARGE_LED_state = LED_STATE_OFF; + bool PAIRING_LED_state = LED_STATE_OFF; - uint32_t PAIRING_LED_starttime = 0; - uint32_t POWER_LED_starttime = 0; - bool doing_fast_blink = false; + uint32_t PAIRING_LED_starttime = 0; + uint32_t POWER_LED_starttime = 0; + bool doing_fast_blink = false; - enum PowerState { discharging, charging, charged, critical }; + enum PowerState { discharging, charging, charged, critical }; - PowerState power_state = discharging; + PowerState power_state = discharging; - enum BLEState { unpaired, pairing, connected }; + enum BLEState { unpaired, pairing, connected }; - BLEState ble_state = unpaired; + BLEState ble_state = unpaired; }; extern StatusLEDModule *statusLEDModule; diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 01aff66ae..b8a710bf5 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -2,13 +2,12 @@ * @file StoreForwardModule.cpp * @brief Implementation of the StoreForwardModule class. * - * This file contains the implementation of the StoreForwardModule class, which is responsible for managing the store - * and forward functionality of the Meshtastic device. The class provides methods for sending and receiving messages, as - * well as managing the message history queue. It also initializes and manages the data structures used for storing the - * message history. + * This file contains the implementation of the StoreForwardModule class, which is responsible for managing the store and forward + * functionality of the Meshtastic device. The class provides methods for sending and receiving messages, as well as managing the + * message history queue. It also initializes and manages the data structures used for storing the message history. * - * The StoreForwardModule class is used by the MeshService class to provide store and forward functionality to the - * Meshtastic device. + * The StoreForwardModule class is used by the MeshService class to provide store and forward functionality to the Meshtastic + * device. * * @author Jm Casler * @date [Insert Date] @@ -31,59 +30,65 @@ StoreForwardModule *storeForwardModule; -int32_t StoreForwardModule::runOnce() { +int32_t StoreForwardModule::runOnce() +{ #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) - if (moduleConfig.store_forward.enabled && is_server) { - // Send out the message queue. - if (this->busy) { - // Only send packets if the channel is less than 25% utilized and until historyReturnMax - if (airTime->isTxAllowedChannelUtil(true) && this->requestCount < this->historyReturnMax) { - if (!storeForwardModule->sendPayload(this->busyTo, this->last_time)) { - this->requestCount = 0; - this->busy = false; + if (moduleConfig.store_forward.enabled && is_server) { + // Send out the message queue. + if (this->busy) { + // Only send packets if the channel is less than 25% utilized and until historyReturnMax + if (airTime->isTxAllowedChannelUtil(true) && this->requestCount < this->historyReturnMax) { + if (!storeForwardModule->sendPayload(this->busyTo, this->last_time)) { + this->requestCount = 0; + this->busy = false; + } + } + } else if (this->heartbeat && (!Throttle::isWithinTimespanMs(lastHeartbeat, heartbeatInterval * 1000)) && + airTime->isTxAllowedChannelUtil(true)) { + lastHeartbeat = millis(); + LOG_INFO("Send heartbeat"); + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT; + sf.which_variant = meshtastic_StoreAndForward_heartbeat_tag; + sf.variant.heartbeat.period = heartbeatInterval; + sf.variant.heartbeat.secondary = 0; // TODO we always have one primary router for now + storeForwardModule->sendMessage(NODENUM_BROADCAST, sf); } - } - } else if (this->heartbeat && (!Throttle::isWithinTimespanMs(lastHeartbeat, heartbeatInterval * 1000)) && airTime->isTxAllowedChannelUtil(true)) { - lastHeartbeat = millis(); - LOG_INFO("Send heartbeat"); - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT; - sf.which_variant = meshtastic_StoreAndForward_heartbeat_tag; - sf.variant.heartbeat.period = heartbeatInterval; - sf.variant.heartbeat.secondary = 0; // TODO we always have one primary router for now - storeForwardModule->sendMessage(NODENUM_BROADCAST, sf); + return (this->packetTimeMax); } - return (this->packetTimeMax); - } #endif - return disable(); + return disable(); } /** * Populates the PSRAM with data to be sent later when a device is out of range. */ -void StoreForwardModule::populatePSRAM() { - /* - For PSRAM usage, see: - https://learn.upesy.com/en/programmation/psram.html#psram-tab - */ +void StoreForwardModule::populatePSRAM() +{ + /* + For PSRAM usage, see: + https://learn.upesy.com/en/programmation/psram.html#psram-tab + */ - LOG_DEBUG("Before PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), memGet.getPsramSize()); + LOG_DEBUG("Before PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), + memGet.getPsramSize()); - /* Use a maximum of 3/4 the available PSRAM unless otherwise specified. - Note: This needs to be done after every thing that would use PSRAM - */ - uint32_t numberOfPackets = (this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct))); - this->records = numberOfPackets; + /* Use a maximum of 3/4 the available PSRAM unless otherwise specified. + Note: This needs to be done after every thing that would use PSRAM + */ + uint32_t numberOfPackets = + (this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct))); + this->records = numberOfPackets; #if defined(ARCH_ESP32) - this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); + this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); #elif defined(ARCH_PORTDUINO) - this->packetHistory = static_cast(calloc(numberOfPackets, sizeof(PacketHistoryStruct))); + this->packetHistory = static_cast(calloc(numberOfPackets, sizeof(PacketHistoryStruct))); #endif - LOG_DEBUG("After PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), memGet.getPsramSize()); - LOG_DEBUG("numberOfPackets for packetHistory - %u", numberOfPackets); + LOG_DEBUG("After PSRAM init: heap %d/%d PSRAM %d/%d", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), + memGet.getPsramSize()); + LOG_DEBUG("numberOfPackets for packetHistory - %u", numberOfPackets); } /** @@ -92,27 +97,28 @@ void StoreForwardModule::populatePSRAM() { * @param sAgo The number of seconds ago from which to start sending messages. * @param to The recipient ID to send the messages to. */ -void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) { - this->last_time = getTime() < secAgo ? 0 : getTime() - secAgo; - uint32_t queueSize = getNumAvailablePackets(to, last_time); - if (queueSize > this->historyReturnMax) - queueSize = this->historyReturnMax; +void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) +{ + this->last_time = getTime() < secAgo ? 0 : getTime() - secAgo; + uint32_t queueSize = getNumAvailablePackets(to, last_time); + if (queueSize > this->historyReturnMax) + queueSize = this->historyReturnMax; - if (queueSize) { - LOG_INFO("S&F - Send %u message(s)", queueSize); - this->busy = true; // runOnce() will pickup the next steps once busy = true. - this->busyTo = to; - } else { - LOG_INFO("S&F - No history"); - } - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY; - sf.which_variant = meshtastic_StoreAndForward_history_tag; - sf.variant.history.history_messages = queueSize; - sf.variant.history.window = secAgo * 1000; - sf.variant.history.last_request = lastRequest[to]; - storeForwardModule->sendMessage(to, sf); - setIntervalFromNow(this->packetTimeMax); // Delay start of sending payloads + if (queueSize) { + LOG_INFO("S&F - Send %u message(s)", queueSize); + this->busy = true; // runOnce() will pickup the next steps once busy = true. + this->busyTo = to; + } else { + LOG_INFO("S&F - No history"); + } + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY; + sf.which_variant = meshtastic_StoreAndForward_history_tag; + sf.variant.history.history_messages = queueSize; + sf.variant.history.window = secAgo * 1000; + sf.variant.history.last_request = lastRequest[to]; + storeForwardModule->sendMessage(to, sf); + setIntervalFromNow(this->packetTimeMax); // Delay start of sending payloads } /** @@ -122,20 +128,22 @@ void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) { * @param last_time The relative time to start counting messages from. * @return The number of available packets in the message history. */ -uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time) { - uint32_t count = 0; - if (lastRequest.find(dest) == lastRequest.end()) { - lastRequest.emplace(dest, 0); - } - for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { - if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { - // Client is only interested in packets not from itself and only in broadcast packets or packets towards it. - if (this->packetHistory[i].from != dest && (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { - count++; - } +uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time) +{ + uint32_t count = 0; + if (lastRequest.find(dest) == lastRequest.end()) { + lastRequest.emplace(dest, 0); } - } - return count; + for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { + if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { + // Client is only interested in packets not from itself and only in broadcast packets or packets towards it. + if (this->packetHistory[i].from != dest && + (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { + count++; + } + } + } + return count; } /** @@ -143,29 +151,30 @@ uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_ * * @return A pointer to the allocated mesh packet or nullptr if none is available. */ -meshtastic_MeshPacket *StoreForwardModule::getForPhone() { - if (moduleConfig.store_forward.enabled && is_server) { - NodeNum to = nodeDB->getNodeNum(); - if (!this->busy) { - // Get number of packets we're going to send in this loop - uint32_t histSize = getNumAvailablePackets(to, 0); // No time limit - if (histSize) { - this->busy = true; - this->busyTo = to; - } else { - return nullptr; - } - } +meshtastic_MeshPacket *StoreForwardModule::getForPhone() +{ + if (moduleConfig.store_forward.enabled && is_server) { + NodeNum to = nodeDB->getNodeNum(); + if (!this->busy) { + // Get number of packets we're going to send in this loop + uint32_t histSize = getNumAvailablePackets(to, 0); // No time limit + if (histSize) { + this->busy = true; + this->busyTo = to; + } else { + return nullptr; + } + } - // We're busy with sending to us until no payload is available anymore - if (this->busy && this->busyTo == to) { - meshtastic_MeshPacket *p = preparePayload(to, 0, true); // No time limit - if (!p) // No more messages to send - this->busy = false; - return p; + // We're busy with sending to us until no payload is available anymore + if (this->busy && this->busyTo == to) { + meshtastic_MeshPacket *p = preparePayload(to, 0, true); // No time limit + if (!p) // No more messages to send + this->busy = false; + return p; + } } - } - return nullptr; + return nullptr; } /** @@ -173,34 +182,35 @@ meshtastic_MeshPacket *StoreForwardModule::getForPhone() { * * @param mp The mesh packet to add to the history buffer. */ -void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) { - const auto &p = mp.decoded; +void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) +{ + const auto &p = mp.decoded; - if (this->packetHistoryTotalCount == this->records) { - LOG_WARN("S&F - PSRAM Full. Starting overwrite"); - this->packetHistoryTotalCount = 0; - for (auto &i : lastRequest) { - i.second = 0; // Clear the last request index for each client device + if (this->packetHistoryTotalCount == this->records) { + LOG_WARN("S&F - PSRAM Full. Starting overwrite"); + this->packetHistoryTotalCount = 0; + for (auto &i : lastRequest) { + i.second = 0; // Clear the last request index for each client device + } } - } - this->packetHistory[this->packetHistoryTotalCount].time = getTime(); - this->packetHistory[this->packetHistoryTotalCount].to = mp.to; - this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel; - this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp); - this->packetHistory[this->packetHistoryTotalCount].id = mp.id; - this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; - this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; - this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; - this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; - this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; - this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start; - this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; - this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; - this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; - memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); + this->packetHistory[this->packetHistoryTotalCount].time = getTime(); + this->packetHistory[this->packetHistoryTotalCount].to = mp.to; + this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel; + this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp); + this->packetHistory[this->packetHistoryTotalCount].id = mp.id; + this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; + this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; + this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; + this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; + this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; + this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start; + this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; + this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; + this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; + memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); - this->packetHistoryTotalCount++; + this->packetHistoryTotalCount++; } /** @@ -210,15 +220,16 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) { * @param last_time The relative time to start sending messages from. * @return True if a packet was successfully sent, false otherwise. */ -bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time) { - meshtastic_MeshPacket *p = preparePayload(dest, last_time); - if (p) { - LOG_INFO("Send S&F Payload"); - service->sendToMesh(p); - this->requestCount++; - return true; - } - return false; +bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time) +{ + meshtastic_MeshPacket *p = preparePayload(dest, last_time); + if (p) { + LOG_INFO("Send S&F Payload"); + service->sendToMesh(p); + this->requestCount++; + return true; + } + return false; } /** @@ -228,60 +239,62 @@ bool StoreForwardModule::sendPayload(NodeNum dest, uint32_t last_time) { * @param last_time The relative time to start sending messages from. * @return A pointer to the prepared mesh packet or nullptr if none is available. */ -meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t last_time, bool local) { - for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { - if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { - /* Copy the messages that were received by the server in the last msAgo - to the packetHistoryTXQueue structure. - Client not interested in packets from itself and only in broadcast packets or packets towards it. */ - if (this->packetHistory[i].from != dest && (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { +meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t last_time, bool local) +{ + for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { + if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { + /* Copy the messages that were received by the server in the last msAgo + to the packetHistoryTXQueue structure. + Client not interested in packets from itself and only in broadcast packets or packets towards it. */ + if (this->packetHistory[i].from != dest && + (this->packetHistory[i].to == NODENUM_BROADCAST || this->packetHistory[i].to == dest)) { - meshtastic_MeshPacket *p = allocDataPacket(); + meshtastic_MeshPacket *p = allocDataPacket(); - p->to = local ? this->packetHistory[i].to : dest; // PhoneAPI can handle original `to` - p->from = this->packetHistory[i].from; - p->id = this->packetHistory[i].id; - p->channel = this->packetHistory[i].channel; - p->decoded.reply_id = this->packetHistory[i].reply_id; - p->rx_time = this->packetHistory[i].time; - p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; - p->rx_rssi = this->packetHistory[i].rx_rssi; - p->rx_snr = this->packetHistory[i].rx_snr; - p->hop_start = this->packetHistory[i].hop_start; - p->hop_limit = this->packetHistory[i].hop_limit; - p->via_mqtt = this->packetHistory[i].via_mqtt; - p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism; + p->to = local ? this->packetHistory[i].to : dest; // PhoneAPI can handle original `to` + p->from = this->packetHistory[i].from; + p->id = this->packetHistory[i].id; + p->channel = this->packetHistory[i].channel; + p->decoded.reply_id = this->packetHistory[i].reply_id; + p->rx_time = this->packetHistory[i].time; + p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; + p->rx_rssi = this->packetHistory[i].rx_rssi; + p->rx_snr = this->packetHistory[i].rx_snr; + p->hop_start = this->packetHistory[i].hop_start; + p->hop_limit = this->packetHistory[i].hop_limit; + p->via_mqtt = this->packetHistory[i].via_mqtt; + p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism; - // Let's assume that if the server received the S&F request that the client is in range. - // TODO: Make this configurable. - p->want_ack = false; + // Let's assume that if the server received the S&F request that the client is in range. + // TODO: Make this configurable. + p->want_ack = false; - if (local) { // PhoneAPI gets normal TEXT_MESSAGE_APP - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - memcpy(p->decoded.payload.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); - p->decoded.payload.size = this->packetHistory[i].payload_size; - } else { - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.which_variant = meshtastic_StoreAndForward_text_tag; - sf.variant.text.size = this->packetHistory[i].payload_size; - memcpy(sf.variant.text.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); - if (this->packetHistory[i].to == NODENUM_BROADCAST) { - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST; - } else { - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT; - } + if (local) { // PhoneAPI gets normal TEXT_MESSAGE_APP + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + memcpy(p->decoded.payload.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); + p->decoded.payload.size = this->packetHistory[i].payload_size; + } else { + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.which_variant = meshtastic_StoreAndForward_text_tag; + sf.variant.text.size = this->packetHistory[i].payload_size; + memcpy(sf.variant.text.bytes, this->packetHistory[i].payload, this->packetHistory[i].payload_size); + if (this->packetHistory[i].to == NODENUM_BROADCAST) { + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_BROADCAST; + } else { + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_TEXT_DIRECT; + } - p->decoded.payload.size = - pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_StoreAndForward_msg, &sf); + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), + &meshtastic_StoreAndForward_msg, &sf); + } + + lastRequest[dest] = i + 1; // Update the last request index for the client device + + return p; + } } - - lastRequest[dest] = i + 1; // Update the last request index for the client device - - return p; - } } - } - return nullptr; + return nullptr; } /** @@ -290,19 +303,20 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t * @param dest The destination node number. * @param payload The message payload to be sent. */ -void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload) { - meshtastic_MeshPacket *p = allocDataProtobuf(payload); +void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload) +{ + meshtastic_MeshPacket *p = allocDataProtobuf(payload); - p->to = dest; + p->to = dest; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // Let's assume that if the server received the S&F request that the client is in range. - // TODO: Make this configurable. - p->want_ack = false; - p->decoded.want_response = false; + // Let's assume that if the server received the S&F request that the client is in range. + // TODO: Make this configurable. + p->want_ack = false; + p->decoded.want_response = false; - service->sendToMesh(p); + service->sendToMesh(p); } /** @@ -311,11 +325,12 @@ void StoreForwardModule::sendMessage(NodeNum dest, const meshtastic_StoreAndForw * @param dest The destination node number. * @param rr The store-and-forward request/response message to send. */ -void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr) { - // Craft an empty response, save some bytes in flash - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = rr; - storeForwardModule->sendMessage(dest, sf); +void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr) +{ + // Craft an empty response, save some bytes in flash + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; + sf.rr = rr; + storeForwardModule->sendMessage(dest, sf); } /** @@ -324,26 +339,27 @@ void StoreForwardModule::sendMessage(NodeNum dest, meshtastic_StoreAndForward_Re * @param dest The destination node number. * @param want_response True if the original message requested a response, false otherwise. */ -void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response) { - meshtastic_MeshPacket *pr = allocDataPacket(); - pr->to = dest; - pr->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - pr->want_ack = false; - pr->decoded.want_response = false; - pr->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - const char *str; - if (this->busy) { - str = "S&F - Busy. Try again shortly."; - } else { - str = "S&F not permitted on the public channel."; - } - LOG_WARN("%s", str); - memcpy(pr->decoded.payload.bytes, str, strlen(str)); - pr->decoded.payload.size = strlen(str); - if (want_response) { - ignoreRequest = true; // This text message counts as response. - } - service->sendToMesh(pr); +void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response) +{ + meshtastic_MeshPacket *pr = allocDataPacket(); + pr->to = dest; + pr->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + pr->want_ack = false; + pr->decoded.want_response = false; + pr->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + const char *str; + if (this->busy) { + str = "S&F - Busy. Try again shortly."; + } else { + str = "S&F not permitted on the public channel."; + } + LOG_WARN("%s", str); + memcpy(pr->decoded.payload.bytes, str, strlen(str)); + pr->decoded.payload.size = strlen(str); + if (want_response) { + ignoreRequest = true; // This text message counts as response. + } + service->sendToMesh(pr); } /** @@ -351,23 +367,24 @@ void StoreForwardModule::sendErrorTextMessage(NodeNum dest, bool want_response) * * @param to The node ID to send the statistics to. */ -void StoreForwardModule::statsSend(uint32_t to) { - meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; +void StoreForwardModule::statsSend(uint32_t to) +{ + meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; - sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS; - sf.which_variant = meshtastic_StoreAndForward_stats_tag; - sf.variant.stats.messages_total = this->records; - sf.variant.stats.messages_saved = this->packetHistoryTotalCount; - sf.variant.stats.messages_max = this->records; - sf.variant.stats.up_time = millis() / 1000; - sf.variant.stats.requests = this->requests; - sf.variant.stats.requests_history = this->requests_history; - sf.variant.stats.heartbeat = this->heartbeat; - sf.variant.stats.return_max = this->historyReturnMax; - sf.variant.stats.return_window = this->historyReturnWindow; + sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS; + sf.which_variant = meshtastic_StoreAndForward_stats_tag; + sf.variant.stats.messages_total = this->records; + sf.variant.stats.messages_saved = this->packetHistoryTotalCount; + sf.variant.stats.messages_max = this->records; + sf.variant.stats.up_time = millis() / 1000; + sf.variant.stats.requests = this->requests; + sf.variant.stats.requests_history = this->requests_history; + sf.variant.stats.heartbeat = this->heartbeat; + sf.variant.stats.return_max = this->historyReturnMax; + sf.variant.stats.return_window = this->historyReturnWindow; - LOG_DEBUG("Send S&F Stats"); - storeForwardModule->sendMessage(to, sf); + LOG_DEBUG("Send S&F Stats"); + storeForwardModule->sendMessage(to, sf); } /** @@ -376,45 +393,46 @@ void StoreForwardModule::statsSend(uint32_t to) { * @param mp The received mesh packet. * @return A `ProcessMessage` indicating whether the packet was successfully handled. */ -ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &mp) { +ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &mp) +{ #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) - if (moduleConfig.store_forward.enabled) { + if (moduleConfig.store_forward.enabled) { - if ((mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) && is_server) { - auto &p = mp.decoded; - if (isToUs(&mp) && (p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 0x00)) { - LOG_DEBUG("Legacy Request to send"); + if ((mp.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) && is_server) { + auto &p = mp.decoded; + if (isToUs(&mp) && (p.payload.bytes[0] == 'S') && (p.payload.bytes[1] == 'F') && (p.payload.bytes[2] == 0x00)) { + LOG_DEBUG("Legacy Request to send"); - // Send the last 60 minutes of messages. - if (this->busy || channels.isDefaultChannel(mp.channel)) { - sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); - } else { - storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); - } - } else { - storeForwardModule->historyAdd(mp); - LOG_INFO("S&F stored. Message history contains %u records now", this->packetHistoryTotalCount); - } - } else if (!isFromUs(&mp) && mp.decoded.portnum == meshtastic_PortNum_STORE_FORWARD_APP) { - auto &p = mp.decoded; - meshtastic_StoreAndForward scratch; - meshtastic_StoreAndForward *decoded = NULL; - if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_StoreAndForward_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding proto module!"); - // if we can't decode it, nobody can process it! - return ProcessMessage::STOP; - } - return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; - } - } // all others are irrelevant - } + // Send the last 60 minutes of messages. + if (this->busy || channels.isDefaultChannel(mp.channel)) { + sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); + } else { + storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); + } + } else { + storeForwardModule->historyAdd(mp); + LOG_INFO("S&F stored. Message history contains %u records now", this->packetHistoryTotalCount); + } + } else if (!isFromUs(&mp) && mp.decoded.portnum == meshtastic_PortNum_STORE_FORWARD_APP) { + auto &p = mp.decoded; + meshtastic_StoreAndForward scratch; + meshtastic_StoreAndForward *decoded = NULL; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_StoreAndForward_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding proto module!"); + // if we can't decode it, nobody can process it! + return ProcessMessage::STOP; + } + return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; + } + } // all others are irrelevant + } #endif - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** @@ -424,194 +442,197 @@ ProcessMessage StoreForwardModule::handleReceived(const meshtastic_MeshPacket &m * @param p A pointer to the StoreAndForward object. * @return True if the message was successfully handled, false otherwise. */ -bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p) { - if (!moduleConfig.store_forward.enabled) { - // If this module is not enabled in any capacity, don't handle the packet, and allow other modules to consume - return false; - } - - requests++; - - switch (p->rr) { - case meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR: - case meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT: - if (is_server) { - // stop sending stuff, the client wants to abort or has another error - if ((this->busy) && (this->busyTo == getFrom(&mp))) { - LOG_ERROR("Client in ERROR or ABORT requested"); - this->requestCount = 0; - this->busy = false; - } +bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p) +{ + if (!moduleConfig.store_forward.enabled) { + // If this module is not enabled in any capacity, don't handle the packet, and allow other modules to consume + return false; } - break; - case meshtastic_StoreAndForward_RequestResponse_CLIENT_HISTORY: - if (is_server) { - requests_history++; - LOG_INFO("Client Request to send HISTORY"); - // Send the last 60 minutes of messages. - if (this->busy || channels.isDefaultChannel(mp.channel)) { - sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); - } else { - if ((p->which_variant == meshtastic_StoreAndForward_history_tag) && (p->variant.history.window > 0)) { - // window is in minutes - storeForwardModule->historySend(p->variant.history.window * 60, getFrom(&mp)); - } else { - storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); // defaults to 4 hours + requests++; + + switch (p->rr) { + case meshtastic_StoreAndForward_RequestResponse_CLIENT_ERROR: + case meshtastic_StoreAndForward_RequestResponse_CLIENT_ABORT: + if (is_server) { + // stop sending stuff, the client wants to abort or has another error + if ((this->busy) && (this->busyTo == getFrom(&mp))) { + LOG_ERROR("Client in ERROR or ABORT requested"); + this->requestCount = 0; + this->busy = false; + } } - } - } - break; + break; - case meshtastic_StoreAndForward_RequestResponse_CLIENT_PING: - if (is_server) { - // respond with a ROUTER PONG - storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG); - } - break; + case meshtastic_StoreAndForward_RequestResponse_CLIENT_HISTORY: + if (is_server) { + requests_history++; + LOG_INFO("Client Request to send HISTORY"); + // Send the last 60 minutes of messages. + if (this->busy || channels.isDefaultChannel(mp.channel)) { + sendErrorTextMessage(getFrom(&mp), mp.decoded.want_response); + } else { + if ((p->which_variant == meshtastic_StoreAndForward_history_tag) && (p->variant.history.window > 0)) { + // window is in minutes + storeForwardModule->historySend(p->variant.history.window * 60, getFrom(&mp)); + } else { + storeForwardModule->historySend(historyReturnWindow * 60, getFrom(&mp)); // defaults to 4 hours + } + } + } + break; - case meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG: - if (is_server) { - // NodeDB is already updated - } - break; + case meshtastic_StoreAndForward_RequestResponse_CLIENT_PING: + if (is_server) { + // respond with a ROUTER PONG + storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG); + } + break; - case meshtastic_StoreAndForward_RequestResponse_CLIENT_STATS: - if (is_server) { - LOG_INFO("Client Request to send STATS"); - if (this->busy) { - storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY); - LOG_INFO("S&F - Busy. Try again shortly"); - } else { - storeForwardModule->statsSend(getFrom(&mp)); - } - } - break; + case meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG: + if (is_server) { + // NodeDB is already updated + } + break; - case meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR: - case meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY: - if (is_client) { - LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY"); - // retry in messages_saved * packetTimeMax ms - retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax * - (meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); - } - break; + case meshtastic_StoreAndForward_RequestResponse_CLIENT_STATS: + if (is_server) { + LOG_INFO("Client Request to send STATS"); + if (this->busy) { + storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY); + LOG_INFO("S&F - Busy. Try again shortly"); + } else { + storeForwardModule->statsSend(getFrom(&mp)); + } + } + break; - case meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG: - // A router responded, this is equal to receiving a heartbeat - case meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT: - if (is_client) { - // register heartbeat and interval - if (p->which_variant == meshtastic_StoreAndForward_heartbeat_tag) { - heartbeatInterval = p->variant.heartbeat.period; - } - lastHeartbeat = millis(); - LOG_INFO("StoreAndForward Heartbeat received"); - } - break; + case meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR: + case meshtastic_StoreAndForward_RequestResponse_ROUTER_BUSY: + if (is_client) { + LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY"); + // retry in messages_saved * packetTimeMax ms + retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax * + (meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); + } + break; - case meshtastic_StoreAndForward_RequestResponse_ROUTER_PING: - if (is_client) { - // respond with a CLIENT PONG - storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG); - } - break; + case meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG: + // A router responded, this is equal to receiving a heartbeat + case meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT: + if (is_client) { + // register heartbeat and interval + if (p->which_variant == meshtastic_StoreAndForward_heartbeat_tag) { + heartbeatInterval = p->variant.heartbeat.period; + } + lastHeartbeat = millis(); + LOG_INFO("StoreAndForward Heartbeat received"); + } + break; - case meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS: - if (is_client) { - LOG_DEBUG("Router Response STATS"); - // These fields only have informational purpose on a client. Fill them to consume later. - if (p->which_variant == meshtastic_StoreAndForward_stats_tag) { - this->records = p->variant.stats.messages_max; - this->requests = p->variant.stats.requests; - this->requests_history = p->variant.stats.requests_history; - this->heartbeat = p->variant.stats.heartbeat; - this->historyReturnMax = p->variant.stats.return_max; - this->historyReturnWindow = p->variant.stats.return_window; - } - } - break; + case meshtastic_StoreAndForward_RequestResponse_ROUTER_PING: + if (is_client) { + // respond with a CLIENT PONG + storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG); + } + break; - case meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY: - if (is_client) { - // These fields only have informational purpose on a client. Fill them to consume later. - if (p->which_variant == meshtastic_StoreAndForward_history_tag) { - this->historyReturnWindow = p->variant.history.window / 60000; - LOG_INFO("Router Response HISTORY - Sending %d messages from last %d minutes", p->variant.history.history_messages, - this->historyReturnWindow); - } - } - break; + case meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS: + if (is_client) { + LOG_DEBUG("Router Response STATS"); + // These fields only have informational purpose on a client. Fill them to consume later. + if (p->which_variant == meshtastic_StoreAndForward_stats_tag) { + this->records = p->variant.stats.messages_max; + this->requests = p->variant.stats.requests; + this->requests_history = p->variant.stats.requests_history; + this->heartbeat = p->variant.stats.heartbeat; + this->historyReturnMax = p->variant.stats.return_max; + this->historyReturnWindow = p->variant.stats.return_window; + } + } + break; - default: - break; // no need to do anything - } - return false; // RoutingModule sends it to the phone + case meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY: + if (is_client) { + // These fields only have informational purpose on a client. Fill them to consume later. + if (p->which_variant == meshtastic_StoreAndForward_history_tag) { + this->historyReturnWindow = p->variant.history.window / 60000; + LOG_INFO("Router Response HISTORY - Sending %d messages from last %d minutes", + p->variant.history.history_messages, this->historyReturnWindow); + } + } + break; + + default: + break; // no need to do anything + } + return false; // RoutingModule sends it to the phone } StoreForwardModule::StoreForwardModule() - : concurrency::OSThread("StoreForward"), ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg) { + : concurrency::OSThread("StoreForward"), + ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg) +{ #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) - isPromiscuous = true; // Brown chicken brown cow + isPromiscuous = true; // Brown chicken brown cow - if (StoreForward_Dev) { - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + if (StoreForward_Dev) { + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - moduleConfig.store_forward.enabled = 1; - } - - if (moduleConfig.store_forward.enabled) { - - // Router - if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) { - LOG_INFO("Init Store & Forward Module in Server mode"); - if (memGet.getPsramSize() > 0) { - if (memGet.getFreePsram() >= 1024 * 1024) { - - // Do the startup here - - // Maximum number of records to return. - if (moduleConfig.store_forward.history_return_max) - this->historyReturnMax = moduleConfig.store_forward.history_return_max; - - // Maximum time window for records to return (in minutes) - if (moduleConfig.store_forward.history_return_window) - this->historyReturnWindow = moduleConfig.store_forward.history_return_window; - - // Maximum number of records to store in memory - if (moduleConfig.store_forward.records) - this->records = moduleConfig.store_forward.records; - - // send heartbeat advertising? - if (moduleConfig.store_forward.heartbeat) - this->heartbeat = moduleConfig.store_forward.heartbeat; - else - this->heartbeat = false; - - // Popupate PSRAM with our data structures. - this->populatePSRAM(); - is_server = true; - } else { - LOG_INFO("."); - LOG_INFO("S&F: not enough PSRAM free, Disable"); - } - } else { - LOG_INFO("S&F: device doesn't have PSRAM, Disable"); - } - - // Client - } else { - is_client = true; - LOG_INFO("Init Store & Forward Module in Client mode"); + moduleConfig.store_forward.enabled = 1; + } + + if (moduleConfig.store_forward.enabled) { + + // Router + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) { + LOG_INFO("Init Store & Forward Module in Server mode"); + if (memGet.getPsramSize() > 0) { + if (memGet.getFreePsram() >= 1024 * 1024) { + + // Do the startup here + + // Maximum number of records to return. + if (moduleConfig.store_forward.history_return_max) + this->historyReturnMax = moduleConfig.store_forward.history_return_max; + + // Maximum time window for records to return (in minutes) + if (moduleConfig.store_forward.history_return_window) + this->historyReturnWindow = moduleConfig.store_forward.history_return_window; + + // Maximum number of records to store in memory + if (moduleConfig.store_forward.records) + this->records = moduleConfig.store_forward.records; + + // send heartbeat advertising? + if (moduleConfig.store_forward.heartbeat) + this->heartbeat = moduleConfig.store_forward.heartbeat; + else + this->heartbeat = false; + + // Popupate PSRAM with our data structures. + this->populatePSRAM(); + is_server = true; + } else { + LOG_INFO("."); + LOG_INFO("S&F: not enough PSRAM free, Disable"); + } + } else { + LOG_INFO("S&F: device doesn't have PSRAM, Disable"); + } + + // Client + } else { + is_client = true; + LOG_INFO("Init Store & Forward Module in Client mode"); + } + } else { + disable(); } - } else { - disable(); - } #endif } diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h index 32e939a12..148568e1b 100644 --- a/src/modules/StoreForwardModule.h +++ b/src/modules/StoreForwardModule.h @@ -10,106 +10,108 @@ #include struct PacketHistoryStruct { - uint32_t time; - uint32_t to; - uint32_t from; - uint32_t id; - uint8_t channel; - uint32_t reply_id; - bool emoji; - uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; - pb_size_t payload_size; - int32_t rx_rssi; - float rx_snr; - uint8_t hop_start; - uint8_t hop_limit; - bool via_mqtt; - uint8_t transport_mechanism; + uint32_t time; + uint32_t to; + uint32_t from; + uint32_t id; + uint8_t channel; + uint32_t reply_id; + bool emoji; + uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; + pb_size_t payload_size; + int32_t rx_rssi; + float rx_snr; + uint8_t hop_start; + uint8_t hop_limit; + bool via_mqtt; + uint8_t transport_mechanism; }; -class StoreForwardModule : private concurrency::OSThread, public ProtobufModule { - bool busy = 0; - uint32_t busyTo = 0; - char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; +class StoreForwardModule : private concurrency::OSThread, public ProtobufModule +{ + bool busy = 0; + uint32_t busyTo = 0; + char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; - PacketHistoryStruct *packetHistory = 0; - uint32_t packetHistoryTotalCount = 0; - uint32_t last_time = 0; - uint32_t requestCount = 0; + PacketHistoryStruct *packetHistory = 0; + uint32_t packetHistoryTotalCount = 0; + uint32_t last_time = 0; + uint32_t requestCount = 0; - uint32_t packetTimeMax = 5000; // Interval between sending history packets as a server. + uint32_t packetTimeMax = 5000; // Interval between sending history packets as a server. - bool is_client = false; - bool is_server = false; + bool is_client = false; + bool is_server = false; - // Unordered_map stores the last request for each nodeNum (`to` field) - std::unordered_map lastRequest; + // Unordered_map stores the last request for each nodeNum (`to` field) + std::unordered_map lastRequest; -public: - StoreForwardModule(); + public: + StoreForwardModule(); - unsigned long lastHeartbeat = 0; - uint32_t heartbeatInterval = 900; + unsigned long lastHeartbeat = 0; + uint32_t heartbeatInterval = 900; - /** - Update our local reference of when we last saw that node. - @return 0 if we have never seen that node before otherwise return the last time we saw the node. - */ - void historyAdd(const meshtastic_MeshPacket &mp); - void statsSend(uint32_t to); - void historySend(uint32_t secAgo, uint32_t to); - uint32_t getNumAvailablePackets(NodeNum dest, uint32_t last_time); + /** + Update our local reference of when we last saw that node. + @return 0 if we have never seen that node before otherwise return the last time we saw the node. + */ + void historyAdd(const meshtastic_MeshPacket &mp); + void statsSend(uint32_t to); + void historySend(uint32_t secAgo, uint32_t to); + uint32_t getNumAvailablePackets(NodeNum dest, uint32_t last_time); - /** - * Send our payload into the mesh - */ - bool sendPayload(NodeNum dest = NODENUM_BROADCAST, uint32_t packetHistory_index = 0); - meshtastic_MeshPacket *preparePayload(NodeNum dest, uint32_t packetHistory_index, bool local = false); - void sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload); - void sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr); - void sendErrorTextMessage(NodeNum dest, bool want_response); - meshtastic_MeshPacket *getForPhone(); - // Returns true if we are configured as server AND we could allocate PSRAM. - bool isServer() { return is_server; } + /** + * Send our payload into the mesh + */ + bool sendPayload(NodeNum dest = NODENUM_BROADCAST, uint32_t packetHistory_index = 0); + meshtastic_MeshPacket *preparePayload(NodeNum dest, uint32_t packetHistory_index, bool local = false); + void sendMessage(NodeNum dest, const meshtastic_StoreAndForward &payload); + void sendMessage(NodeNum dest, meshtastic_StoreAndForward_RequestResponse rr); + void sendErrorTextMessage(NodeNum dest, bool want_response); + meshtastic_MeshPacket *getForPhone(); + // Returns true if we are configured as server AND we could allocate PSRAM. + bool isServer() { return is_server; } - /* - -Override the wantPacket method. - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { - switch (p->decoded.portnum) { - case meshtastic_PortNum_TEXT_MESSAGE_APP: - case meshtastic_PortNum_STORE_FORWARD_APP: - return true; - default: - return false; + /* + -Override the wantPacket method. + */ + virtual bool wantPacket(const meshtastic_MeshPacket *p) override + { + switch (p->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: + case meshtastic_PortNum_STORE_FORWARD_APP: + return true; + default: + return false; + } } - } -private: - void populatePSRAM(); + private: + void populatePSRAM(); - // S&F Defaults - uint32_t historyReturnMax = 25; // Return maximum of 25 records by default. - uint32_t historyReturnWindow = 240; // Return history of last 4 hours by default. - uint32_t records = 0; // Calculated - bool heartbeat = false; // No heartbeat. + // S&F Defaults + uint32_t historyReturnMax = 25; // Return maximum of 25 records by default. + uint32_t historyReturnWindow = 240; // Return history of last 4 hours by default. + uint32_t records = 0; // Calculated + bool heartbeat = false; // No heartbeat. - // stats - uint32_t requests = 0; // Number of times any client sent a request to the S&F. - uint32_t requests_history = 0; // Number of times the history was requested. + // stats + uint32_t requests = 0; // Number of times any client sent a request to the S&F. + uint32_t requests_history = 0; // Number of times the history was requested. - uint32_t retry_delay = 0; // If server is busy, retry after this delay (in ms). + uint32_t retry_delay = 0; // If server is busy, retry after this delay (in ms). -protected: - virtual int32_t runOnce() override; + protected: + virtual int32_t runOnce() override; - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); }; extern StoreForwardModule *storeForwardModule; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 54a269631..51543eab6 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -18,103 +18,107 @@ SystemCommandsModule *systemCommandsModule; -SystemCommandsModule::SystemCommandsModule() { - if (inputBroker) - inputObserver.observe(inputBroker); +SystemCommandsModule::SystemCommandsModule() +{ + if (inputBroker) + inputObserver.observe(inputBroker); } -int SystemCommandsModule::handleInputEvent(const InputEvent *event) { - LOG_INPUT("SystemCommands Input event %u! kb %u", event->inputEvent, event->kbchar); - // System commands (all others fall through) - switch (event->kbchar) { - // Fn key symbols - case INPUT_BROKER_MSG_FN_SYMBOL_ON: - case INPUT_BROKER_MSG_FN_SYMBOL_OFF: - return 0; - // Brightness - case INPUT_BROKER_MSG_BRIGHTNESS_UP: - IF_SCREEN(screen->increaseBrightness()); - LOG_DEBUG("Increase Screen Brightness"); - return 0; - case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: - IF_SCREEN(screen->decreaseBrightness()); - LOG_DEBUG("Decrease Screen Brightness"); - return 0; - // Mute - case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled && externalNotificationModule) { - externalNotificationModule->setMute(externalNotificationModule->getMute()); - IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); - screen->showSimpleBanner(externalNotificationModule->getMute() ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) - } - return 0; - // Bluetooth - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: - config.bluetooth.enabled = !config.bluetooth.enabled; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); -#if defined(ARDUINO_ARCH_NRF52) - if (!config.bluetooth.enabled) { - disableBluetooth(); - IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; - } else { - IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } -#else - if (!config.bluetooth.enabled) { - disableBluetooth(); - IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); - } else { - IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } -#endif - return 0; - case INPUT_BROKER_MSG_REBOOT: - IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); - nodeDB->saveToDisk(); -#if HAS_SCREEN - messageStore.saveToFlash(); -#endif - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - return true; - } - - switch (event->inputEvent) { - // GPS - case INPUT_BROKER_GPS_TOGGLE: -#if !MESHTASTIC_EXCLUDE_GPS - if (gps) { - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && config.position.fixed_position == false) { - nodeDB->clearLocalPosition(); +int SystemCommandsModule::handleInputEvent(const InputEvent *event) +{ + LOG_INPUT("SystemCommands Input event %u! kb %u", event->inputEvent, event->kbchar); + // System commands (all others fall through) + switch (event->kbchar) { + // Fn key symbols + case INPUT_BROKER_MSG_FN_SYMBOL_ON: + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: + return 0; + // Brightness + case INPUT_BROKER_MSG_BRIGHTNESS_UP: + IF_SCREEN(screen->increaseBrightness()); + LOG_DEBUG("Increase Screen Brightness"); + return 0; + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: + IF_SCREEN(screen->decreaseBrightness()); + LOG_DEBUG("Decrease Screen Brightness"); + return 0; + // Mute + case INPUT_BROKER_MSG_MUTE_TOGGLE: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); screen->showSimpleBanner( + externalNotificationModule->getMute() ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + } + return 0; + // Bluetooth + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: + config.bluetooth.enabled = !config.bluetooth.enabled; + LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); - } - gps->toggleGpsMode(); - const char *msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; - IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);) - } +#if defined(ARDUINO_ARCH_NRF52) + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; + } else { + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#else + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } #endif - return true; - // Mesh ping - case INPUT_BROKER_SEND_PING: - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); - } else { - IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + return 0; + case INPUT_BROKER_MSG_REBOOT: + IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); +#if HAS_SCREEN + messageStore.saveToFlash(); +#endif + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; } - return true; - // Power control - case INPUT_BROKER_SHUTDOWN: - shutdownAtMsec = millis(); - return true; - default: - // No other input events handled here - break; - } - return false; + switch (event->inputEvent) { + // GPS + case INPUT_BROKER_GPS_TOGGLE: +#if !MESHTASTIC_EXCLUDE_GPS + if (gps) { + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + nodeDB->saveToDisk(); + } + gps->toggleGpsMode(); + const char *msg = + (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; + IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);) + } +#endif + return true; + // Mesh ping + case INPUT_BROKER_SEND_PING: + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } + return true; + // Power control + case INPUT_BROKER_SHUTDOWN: + shutdownAtMsec = millis(); + return true; + + default: + // No other input events handled here + break; + } + return false; } diff --git a/src/modules/SystemCommandsModule.h b/src/modules/SystemCommandsModule.h index 9810ee3a0..44910f443 100644 --- a/src/modules/SystemCommandsModule.h +++ b/src/modules/SystemCommandsModule.h @@ -6,13 +6,14 @@ #include #include -class SystemCommandsModule { - CallbackObserver inputObserver = - CallbackObserver(this, &SystemCommandsModule::handleInputEvent); +class SystemCommandsModule +{ + CallbackObserver inputObserver = + CallbackObserver(this, &SystemCommandsModule::handleInputEvent); -public: - SystemCommandsModule(); - int handleInputEvent(const InputEvent *event); + public: + SystemCommandsModule(); + int handleInputEvent(const InputEvent *event); }; extern SystemCommandsModule *systemCommandsModule; \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 8ea0dc97a..21a563b9d 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -21,206 +21,216 @@ #define PMSA003I_WARMUP_MS 30000 #endif -int32_t AirQualityTelemetryModule::runOnce() { - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ +int32_t AirQualityTelemetryModule::runOnce() +{ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.telemetry.air_quality_enabled = 1; + // moduleConfig.telemetry.air_quality_enabled = 1; - if (!(moduleConfig.telemetry.air_quality_enabled)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } - - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = false; - - if (moduleConfig.telemetry.air_quality_enabled) { - LOG_INFO("Air quality Telemetry: init"); - -#ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!aqi.begin_I2C()) { -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif + if (!(moduleConfig.telemetry.air_quality_enabled)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); - } - return setStartDelay(); } - return disable(); - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) - return disable(); - switch (state) { -#ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = false; - return PMSA003I_WARMUP_MS; -#endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - } + if (moduleConfig.telemetry.air_quality_enabled) { + LOG_INFO("Air quality Telemetry: init"); #ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; + // put the sensor to sleep on startup + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); + digitalWrite(PMSA003I_ENABLE_PIN, LOW); #endif /* PMSA003I_ENABLE_PIN */ - return sendToPhoneIntervalMs; - default: - return disable(); - } - } -} - -bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { - if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { -#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); - - LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, t->variant.air_quality_metrics.pm10_standard, - t->variant.air_quality_metrics.pm25_standard, t->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, - t->variant.air_quality_metrics.pm100_environmental); + if (!aqi.begin_I2C()) { +#ifndef I2C_NO_RESCAN + LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); + // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. + uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; + uint8_t i2caddr_asize = 1; + auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); +#if defined(I2C_SDA1) + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); - - lastMeasurementPacket = packetPool.allocCopy(mp); - } - - return false; // Let others look at this message also if they want -} - -bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; - - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; - - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", m->variant.air_quality_metrics.pm10_environmental, - m->variant.air_quality_metrics.pm25_environmental, m->variant.air_quality_metrics.pm100_environmental); - - return true; -} - -meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() { - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; + i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); + auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); + if (found.type != ScanI2C::DeviceType::NONE) { + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; + nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = + i2cScanner->fetchI2CBus(found.address); + return setStartDelay(); + } +#endif + return disable(); + } + return setStartDelay(); + } + return disable(); } else { - LOG_ERROR("Error decoding AirQualityTelemetry module!"); - return NULL; + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.air_quality_enabled) + return disable(); + + switch (state) { +#ifdef PMSA003I_ENABLE_PIN + case State::IDLE: + // sensor is in standby; fire it up and sleep + LOG_DEBUG("runOnce(): state = idle"); + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + + return PMSA003I_WARMUP_MS; +#endif /* PMSA003I_ENABLE_PIN */ + case State::ACTIVE: + // sensor is already warmed up; grab telemetry and send it + LOG_DEBUG("runOnce(): state = active"); + + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (service->isToPhoneQueueEmpty()) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + } + +#ifdef PMSA003I_ENABLE_PIN + // put sensor back to sleep + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +#endif /* PMSA003I_ENABLE_PIN */ + + return sendToPhoneIntervalMs; + default: + return disable(); + } } - // Check for a request for air quality metrics - if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getAirQualityTelemetry(&m)) { - LOG_INFO("Air quality telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } - } - return NULL; } -bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getAirQualityTelemetry(&m)) { - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; +bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) + const char *sender = getSenderShortName(mp); - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, + t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, + t->variant.air_quality_metrics.pm100_standard); - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", + t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, + t->variant.air_quality_metrics.pm100_environmental); +#endif + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); + + lastMeasurementPacket = packetPool.allocCopy(mp); } + + return false; // Let others look at this message also if they want +} + +bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) +{ + if (!aqi.read(&data)) { + LOG_WARN("Skip send measurements. Could not read AQIn"); + return false; + } + + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; + m->variant.air_quality_metrics.has_pm10_standard = true; + m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; + m->variant.air_quality_metrics.has_pm25_standard = true; + m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; + m->variant.air_quality_metrics.has_pm100_standard = true; + m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + + m->variant.air_quality_metrics.has_pm10_environmental = true; + m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; + m->variant.air_quality_metrics.has_pm25_environmental = true; + m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; + m->variant.air_quality_metrics.has_pm100_environmental = true; + m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + + LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, + m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); + + LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", + m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, + m->variant.air_quality_metrics.pm100_environmental); + return true; - } +} - return false; +meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() +{ + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding AirQualityTelemetry module!"); + return NULL; + } + // Check for a request for air quality metrics + if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getAirQualityTelemetry(&m)) { + LOG_INFO("Air quality telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } + } + return NULL; +} + +bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) +{ + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getAirQualityTelemetry(&m)) { + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); + + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } + return true; + } + + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 7da80f73f..0142ee686 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -8,57 +8,60 @@ #include "NodeDB.h" #include "ProtobufModule.h" -class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &AirQualityTelemetryModule::handleStatusUpdate); +class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, + &AirQualityTelemetryModule::handleStatusUpdate); -public: - AirQualityTelemetryModule() - : concurrency::OSThread("AirQualityTelemetry"), - ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { - lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); + public: + AirQualityTelemetryModule() + : concurrency::OSThread("AirQualityTelemetry"), + ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + lastMeasurementPacket = nullptr; + setIntervalFromNow(10 * 1000); + aqi = Adafruit_PM25AQI(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); #ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; + // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking + // a reading + state = State::IDLE; #else - state = State::ACTIVE; + state = State::ACTIVE; #endif - } + } -protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Air Quality data - @return true if it contains valid data - */ - bool getAirQualityTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Air Quality data + @return true if it contains valid data + */ + bool getAirQualityTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); -private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; + private: + enum State { + IDLE = 0, + ACTIVE = 1, + }; - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; - bool firstTime = true; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; + State state; + Adafruit_PM25AQI aqi; + PM25_AQI_Data data = {0}; + bool firstTime = true; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 1418b92e9..066b9361d 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -16,162 +16,175 @@ #define MAGIC_USB_BATTERY_LEVEL 101 -int32_t DeviceTelemetryModule::runOnce() { - refreshUptime(); - bool isImpoliteRole = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER); - if (((lastSentToMesh == 0) || - ((uptimeLastMs - lastSentToMesh) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && - config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && moduleConfig.telemetry.device_telemetry_enabled) { - sendTelemetry(); - lastSentToMesh = uptimeLastMs; - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - if (lastSentStatsToPhone == 0 || (uptimeLastMs - lastSentStatsToPhone) >= sendStatsToPhoneIntervalMs) { - sendLocalStatsToPhone(); - lastSentStatsToPhone = uptimeLastMs; +int32_t DeviceTelemetryModule::runOnce() +{ + refreshUptime(); + bool isImpoliteRole = + IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER); + if (((lastSentToMesh == 0) || + ((uptimeLastMs - lastSentToMesh) >= + Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && + config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && + moduleConfig.telemetry.device_telemetry_enabled) { + sendTelemetry(); + lastSentToMesh = uptimeLastMs; + } else if (service->isToPhoneQueueEmpty()) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + if (lastSentStatsToPhone == 0 || (uptimeLastMs - lastSentStatsToPhone) >= sendStatsToPhoneIntervalMs) { + sendLocalStatsToPhone(); + lastSentStatsToPhone = uptimeLastMs; + } } - } - return sendToPhoneIntervalMs; + return sendToPhoneIntervalMs; } -bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { - if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { +bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender, - t->variant.device_metrics.air_util_tx, t->variant.device_metrics.channel_utilization, t->variant.device_metrics.battery_level, - t->variant.device_metrics.voltage); + LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender, + t->variant.device_metrics.air_util_tx, t->variant.device_metrics.channel_utilization, + t->variant.device_metrics.battery_level, t->variant.device_metrics.voltage); #endif - nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); - } - return false; // Let others look at this message also if they want -} - -meshtastic_MeshPacket *DeviceTelemetryModule::allocReply() { - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding DeviceTelemetry module!"); - return NULL; + nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); } - // Check for a request for device metrics - if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - LOG_INFO("Device telemetry reply to request"); - return allocDataProtobuf(getDeviceTelemetry()); - } else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) { - LOG_INFO("Device telemetry reply w/ LocalStats to request"); - return allocDataProtobuf(getLocalStatsTelemetry()); + return false; // Let others look at this message also if they want +} + +meshtastic_MeshPacket *DeviceTelemetryModule::allocReply() +{ + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding DeviceTelemetry module!"); + return NULL; + } + // Check for a request for device metrics + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + LOG_INFO("Device telemetry reply to request"); + return allocDataProtobuf(getDeviceTelemetry()); + } else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) { + LOG_INFO("Device telemetry reply w/ LocalStats to request"); + return allocDataProtobuf(getLocalStatsTelemetry()); + } } - } - return NULL; + return NULL; } -meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry() { - meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; - t.which_variant = meshtastic_Telemetry_device_metrics_tag; - t.time = getTime(); - t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero; - t.variant.device_metrics.has_air_util_tx = true; - t.variant.device_metrics.has_battery_level = true; - t.variant.device_metrics.has_channel_utilization = true; - t.variant.device_metrics.has_voltage = true; - t.variant.device_metrics.has_uptime_seconds = true; +meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry() +{ + meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; + t.which_variant = meshtastic_Telemetry_device_metrics_tag; + t.time = getTime(); + t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero; + t.variant.device_metrics.has_air_util_tx = true; + t.variant.device_metrics.has_battery_level = true; + t.variant.device_metrics.has_channel_utilization = true; + t.variant.device_metrics.has_voltage = true; + t.variant.device_metrics.has_uptime_seconds = true; - t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent(); - t.variant.device_metrics.battery_level = - (!powerStatus->getHasBattery() || powerStatus->getIsCharging()) ? MAGIC_USB_BATTERY_LEVEL : powerStatus->getBatteryChargePercent(); - t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent(); - t.variant.device_metrics.voltage = powerStatus->getBatteryVoltageMv() / 1000.0; - t.variant.device_metrics.uptime_seconds = getUptimeSeconds(); + t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent(); + t.variant.device_metrics.battery_level = (!powerStatus->getHasBattery() || powerStatus->getIsCharging()) + ? MAGIC_USB_BATTERY_LEVEL + : powerStatus->getBatteryChargePercent(); + t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent(); + t.variant.device_metrics.voltage = powerStatus->getBatteryVoltageMv() / 1000.0; + t.variant.device_metrics.uptime_seconds = getUptimeSeconds(); - return t; + return t; } -meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.which_variant = meshtastic_Telemetry_local_stats_tag; - telemetry.variant.local_stats = meshtastic_LocalStats_init_zero; - telemetry.time = getTime(); - telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds(); - telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent(); - telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent(); - telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; - telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); - if (RadioLibInterface::instance) { - telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood; - telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad; - telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad; - telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay; - telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop; - } +meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.which_variant = meshtastic_Telemetry_local_stats_tag; + telemetry.variant.local_stats = meshtastic_LocalStats_init_zero; + telemetry.time = getTime(); + telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds(); + telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent(); + telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent(); + telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; + telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); + if (RadioLibInterface::instance) { + telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood; + telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad; + telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad; + telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay; + telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop; + } #ifdef ARCH_PORTDUINO - if (SimRadio::instance) { - telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; - telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; - telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; - telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay; - telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop; - } + if (SimRadio::instance) { + telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; + telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; + telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; + telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay; + telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop; + } #else - telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize(); - telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap(); + telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize(); + telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap(); #endif - if (router) { - telemetry.variant.local_stats.num_rx_dupe = router->rxDupe; - telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled; - } + if (router) { + telemetry.variant.local_stats.num_rx_dupe = router->rxDupe; + telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled; + } - LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", - telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization, telemetry.variant.local_stats.air_util_tx, - telemetry.variant.local_stats.num_online_nodes, telemetry.variant.local_stats.num_total_nodes); + LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", + telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization, + telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes, + telemetry.variant.local_stats.num_total_nodes); - LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx, - telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad); + LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx, + telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad); - return telemetry; + return telemetry; } -void DeviceTelemetryModule::sendLocalStatsToPhone() { - meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry()); - p->to = NODENUM_BROADCAST; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; +void DeviceTelemetryModule::sendLocalStatsToPhone() +{ + meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry()); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - service->sendToPhone(p); -} - -bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { - meshtastic_Telemetry telemetry = getDeviceTelemetry(); - LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i", telemetry.variant.device_metrics.air_util_tx, - telemetry.variant.device_metrics.channel_utilization, telemetry.variant.device_metrics.battery_level, - telemetry.variant.device_metrics.voltage, telemetry.variant.device_metrics.uptime_seconds); - - DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); - DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); - - p->to = dest; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - - nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); - } - return true; +} + +bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) +{ + meshtastic_Telemetry telemetry = getDeviceTelemetry(); + LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i", + telemetry.variant.device_metrics.air_util_tx, telemetry.variant.device_metrics.channel_utilization, + telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage, + telemetry.variant.device_metrics.uptime_seconds); + + DEBUG_HEAP_BEFORE; + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); + + p->to = dest; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + + nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } + return true; } \ No newline at end of file diff --git a/src/modules/Telemetry/DeviceTelemetry.h b/src/modules/Telemetry/DeviceTelemetry.h index 58fdd8f7e..a1d55a596 100644 --- a/src/modules/Telemetry/DeviceTelemetry.h +++ b/src/modules/Telemetry/DeviceTelemetry.h @@ -5,57 +5,61 @@ #include #include -class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); +class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); -public: - DeviceTelemetryModule() - : concurrency::OSThread("DeviceTelemetry"), ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { - uptimeWrapCount = 0; - uptimeLastMs = millis(); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent - } - virtual bool wantUIFrame() { return false; } + public: + DeviceTelemetryModule() + : concurrency::OSThread("DeviceTelemetry"), + ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + uptimeWrapCount = 0; + uptimeLastMs = millis(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + } + virtual bool wantUIFrame() { return false; } -protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual meshtastic_MeshPacket *allocReply() override; - virtual int32_t runOnce() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool phoneOnly = false); + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual meshtastic_MeshPacket *allocReply() override; + virtual int32_t runOnce() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool phoneOnly = false); - /** - * Get the uptime in seconds - * Loses some accuracy after 49 days, but that's fine - */ - uint32_t getUptimeSeconds() { return (0xFFFFFFFF / 1000) * uptimeWrapCount + (uptimeLastMs / 1000); } + /** + * Get the uptime in seconds + * Loses some accuracy after 49 days, but that's fine + */ + uint32_t getUptimeSeconds() { return (0xFFFFFFFF / 1000) * uptimeWrapCount + (uptimeLastMs / 1000); } -private: - meshtastic_Telemetry getDeviceTelemetry(); - meshtastic_Telemetry getLocalStatsTelemetry(); + private: + meshtastic_Telemetry getDeviceTelemetry(); + meshtastic_Telemetry getLocalStatsTelemetry(); - void sendLocalStatsToPhone(); - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes - uint32_t lastSentStatsToPhone = 0; - uint32_t lastSentToMesh = 0; + void sendLocalStatsToPhone(); + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes + uint32_t lastSentStatsToPhone = 0; + uint32_t lastSentToMesh = 0; - void refreshUptime() { - auto now = millis(); - // If we wrapped around (~49 days), increment the wrap count - if (now < uptimeLastMs) - uptimeWrapCount++; + void refreshUptime() + { + auto now = millis(); + // If we wrapped around (~49 days), increment the wrap count + if (now < uptimeLastMs) + uptimeWrapCount++; - uptimeLastMs = now; - } + uptimeLastMs = now; + } - uint32_t uptimeWrapCount; - uint32_t uptimeLastMs; + uint32_t uptimeWrapCount; + uint32_t uptimeLastMs; }; \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 85fc2ade2..41062662b 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -28,8 +28,10 @@ #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" -namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, + bool show_date); } #if __has_include() #include "Sensor/AHT10.h" @@ -146,556 +148,575 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c static std::forward_list sensors; -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) { - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); #if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } #else - TwoWire *bus = &Wire; + TwoWire *bus = &Wire; #endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; } - // destroy sensor - delete sensor; - } } -void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { - if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { - return; - } - LOG_INFO("Environment Telemetry adding I2C devices..."); +void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) +{ + if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { + return; + } + LOG_INFO("Environment Telemetry adding I2C devices..."); - // order by priority of metrics/values (low top, high bottom) + // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #ifdef T1000X_SENSOR_EN - // Not a real I2C device - addSensor(i2cScanner, ScanI2C::DeviceType::NONE); + // Not a real I2C device + addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #else #ifdef SENSECAP_INDICATOR - // Not a real I2C device, uses UART - addSensor(i2cScanner, ScanI2C::DeviceType::NONE); + // Not a real I2C device, uses UART + addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #endif - addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); - addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); + addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); + addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); #endif #endif #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); + addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); + addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); + addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BMP_085); + addSensor(i2cScanner, ScanI2C::DeviceType::BMP_085); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BME_280); + addSensor(i2cScanner, ScanI2C::DeviceType::BME_280); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); + addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); + addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BMP_280); + addSensor(i2cScanner, ScanI2C::DeviceType::BMP_280); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::DPS310); + addSensor(i2cScanner, ScanI2C::DeviceType::DPS310); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); + addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); + addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); + addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); + addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); #endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 - addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); + addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::VEML7700); + addSensor(i2cScanner, ScanI2C::DeviceType::VEML7700); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::TSL2591); + addSensor(i2cScanner, ScanI2C::DeviceType::TSL2591); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); + addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); + addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); + addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); + addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::PCT2075); + addSensor(i2cScanner, ScanI2C::DeviceType::PCT2075); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::TSL2561); + addSensor(i2cScanner, ScanI2C::DeviceType::TSL2561); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::NAU7802); + addSensor(i2cScanner, ScanI2C::DeviceType::NAU7802); #endif #if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); + addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); #endif #endif } -int32_t EnvironmentTelemetryModule::runOnce() { - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = - Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs); - LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); - doDeepSleep(nightyNightMs, true, false); - } +int32_t EnvironmentTelemetryModule::runOnce() +{ + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } - uint32_t result = UINT32_MAX; - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + uint32_t result = UINT32_MAX; + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.telemetry.environment_measurement_enabled = 1; - // moduleConfig.telemetry.environment_screen_enabled = 1; - // moduleConfig.telemetry.environment_update_interval = 15; + // moduleConfig.telemetry.environment_measurement_enabled = 1; + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.environment_update_interval = 15; - if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || - ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } + if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || + ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); + } - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = 0; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = 0; - if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { - LOG_INFO("Environment Telemetry: init"); + if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { + LOG_INFO("Environment Telemetry: init"); - // check if we have at least one sensor - if (!sensors.empty()) { - result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } + // check if we have at least one sensor + if (!sensors.empty()) { + result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } #ifdef T1000X_SENSOR_EN #elif !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL - if (ina219Sensor.hasSensor()) - result = ina219Sensor.runOnce(); - if (ina260Sensor.hasSensor()) - result = ina260Sensor.runOnce(); - if (ina3221Sensor.hasSensor()) - result = ina3221Sensor.runOnce(); - if (max17048Sensor.hasSensor()) - result = max17048Sensor.runOnce(); - // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the - // sensormap here. + if (ina219Sensor.hasSensor()) + result = ina219Sensor.runOnce(); + if (ina260Sensor.hasSensor()) + result = ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor()) + result = ina3221Sensor.runOnce(); + if (max17048Sensor.hasSensor()) + result = max17048Sensor.runOnce(); + // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the + // sensormap here. #ifdef HAS_RAKPROT - result = rak9154Sensor.runOnce(); + result = rak9154Sensor.runOnce(); #endif #endif - } - // it's possible to have this module enabled, only for displaying values on the screen. - // therefore, we should only enable the sensor loop if measurement is also enabled - return result == UINT32_MAX ? disable() : setStartDelay(); - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { - return disable(); - } + } + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { + return disable(); + } - for (TelemetrySensor *sensor : sensors) { - uint32_t delay = sensor->runOnce(); - if (delay < result) { - result = delay; - } - } + for (TelemetrySensor *sensor : sensors) { + uint32_t delay = sensor->runOnce(); + if (delay < result) { + result = delay; + } + } - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.environment_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - lastSentToPhone = millis(); + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } } - } - return min(sendToPhoneIntervalMs, result); + return min(sendToPhoneIntervalMs, result); } -bool EnvironmentTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.environment_screen_enabled; } +bool EnvironmentTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.environment_screen_enabled; +} #if HAS_SCREEN -void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // === Setup display === - display->clear(); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - int line = 1; +void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; - // === Set Title - const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; + // === Set Title + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - // === Row spacing setup === - const int rowHeight = FONT_HEIGHT_SMALL - 4; - int currentY = graphics::getTextPositions(display)[line++]; + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; - // === Show "No Telemetry" if no data available === - if (!lastMeasurementPacket) { - display->drawString(x, currentY, "No Telemetry"); - return; - } - - // Decode the telemetry message from the latest received packet - const meshtastic_Data &p = lastMeasurementPacket->decoded; - meshtastic_Telemetry telemetry; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { - display->drawString(x, currentY, "No Telemetry"); - return; - } - - const auto &m = telemetry.variant.environment_metrics; - - // Check if any telemetry field has valid data - bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || m.current != 0 || - m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; - - if (!hasAny) { - display->drawString(x, currentY, "No Telemetry"); - return; - } - - // === First line: Show sender name + time since received (left), and first metric (right) === - const char *sender = getSenderShortName(*lastMeasurementPacket); - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - String agoStr = (agoSecs > 864000) ? "?" - : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" - : (agoSecs > 60) ? String(agoSecs / 60) + "m" - : String(agoSecs) + "s"; - - String leftStr = String(sender) + " (" + agoStr + ")"; - display->drawString(x, currentY, leftStr); // Left side: who and when - - // === Collect sensor readings as label strings (no icons) === - std::vector entries; - - if (m.has_temperature) { - String tempStr = moduleConfig.telemetry.environment_display_fahrenheit - ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" - : "Tmp: " + String(m.temperature, 1) + "°C"; - entries.push_back(tempStr); - } - if (m.has_relative_humidity) - entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); - if (m.barometric_pressure != 0) - entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); - if (m.iaq != 0) { - String aqi = "IAQ: " + String(m.iaq); - const char *bannerMsg = nullptr; // Default: no banner - - if (m.iaq <= 25) - aqi += " (Excellent)"; - else if (m.iaq <= 50) - aqi += " (Good)"; - else if (m.iaq <= 100) - aqi += " (Moderate)"; - else if (m.iaq <= 150) - aqi += " (Poor)"; - else if (m.iaq <= 200) { - aqi += " (Unhealthy)"; - bannerMsg = "Unhealthy IAQ"; - } else if (m.iaq <= 300) { - aqi += " (Very Unhealthy)"; - bannerMsg = "Very Unhealthy IAQ"; - } else { - aqi += " (Hazardous)"; - bannerMsg = "Hazardous IAQ"; + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); + return; } - entries.push_back(aqi); - - // === IAQ alert logic === - static uint32_t lastAlertTime = 0; - uint32_t now = millis(); - - bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); - bool isCooldownOver = (now - lastAlertTime > 60000); - - if (isOwnTelemetry && bannerMsg && isCooldownOver) { - LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); - screen->showSimpleBanner(bannerMsg, 3000); - - // Only buzz if IAQ is over 200 - if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { - playLongBeep(); - } - - lastAlertTime = now; - } - } - if (m.voltage != 0 || m.current != 0) - entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); - if (m.lux != 0) - entries.push_back("Light: " + String(m.lux, 0) + "lx"); - if (m.white_lux != 0) - entries.push_back("White: " + String(m.white_lux, 0) + "lx"); - if (m.weight != 0) - entries.push_back("Weight: " + String(m.weight, 0) + "kg"); - if (m.distance != 0) - entries.push_back("Level: " + String(m.distance, 0) + "mm"); - if (m.radiation != 0) - entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); - - // === Show first available metric on top-right of first line === - if (!entries.empty()) { - String valueStr = entries.front(); - int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); - display->drawString(rightX, currentY, valueStr); - entries.erase(entries.begin()); // Remove from queue - } - - // === Advance to next line for remaining telemetry entries === - currentY += rowHeight; - - // === Draw remaining entries in 2-column format (left and right) === - for (size_t i = 0; i < entries.size(); i += 2) { - // Left column - display->drawString(x, currentY, entries[i]); - - // Right column if it exists - if (i + 1 < entries.size()) { - int rightX = SCREEN_WIDTH / 2; - display->drawString(rightX, currentY, entries[i + 1]); + // Decode the telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); + return; } + const auto &m = telemetry.variant.environment_metrics; + + // Check if any telemetry field has valid data + bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || + m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; + + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_temperature) { + String tempStr = moduleConfig.telemetry.environment_display_fahrenheit + ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" + : "Tmp: " + String(m.temperature, 1) + "°C"; + entries.push_back(tempStr); + } + if (m.has_relative_humidity) + entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); + if (m.barometric_pressure != 0) + entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + const char *bannerMsg = nullptr; // Default: no banner + + if (m.iaq <= 25) + aqi += " (Excellent)"; + else if (m.iaq <= 50) + aqi += " (Good)"; + else if (m.iaq <= 100) + aqi += " (Moderate)"; + else if (m.iaq <= 150) + aqi += " (Poor)"; + else if (m.iaq <= 200) { + aqi += " (Unhealthy)"; + bannerMsg = "Unhealthy IAQ"; + } else if (m.iaq <= 300) { + aqi += " (Very Unhealthy)"; + bannerMsg = "Very Unhealthy IAQ"; + } else { + aqi += " (Hazardous)"; + bannerMsg = "Hazardous IAQ"; + } + + entries.push_back(aqi); + + // === IAQ alert logic === + static uint32_t lastAlertTime = 0; + uint32_t now = millis(); + + bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); + bool isCooldownOver = (now - lastAlertTime > 60000); + + if (isOwnTelemetry && bannerMsg && isCooldownOver) { + LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); + screen->showSimpleBanner(bannerMsg, 3000); + + // Only buzz if IAQ is over 200 + if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); + } + + lastAlertTime = now; + } + } + if (m.voltage != 0 || m.current != 0) + entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); + if (m.lux != 0) + entries.push_back("Light: " + String(m.lux, 0) + "lx"); + if (m.white_lux != 0) + entries.push_back("White: " + String(m.white_lux, 0) + "lx"); + if (m.weight != 0) + entries.push_back("Weight: " + String(m.weight, 0) + "kg"); + if (m.distance != 0) + entries.push_back("Level: " + String(m.distance, 0) + "mm"); + if (m.radiation != 0) + entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === currentY += rowHeight; - } - graphics::drawCommonFooter(display, x, y); + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); + } + + currentY += rowHeight; + } + graphics::drawCommonFooter(display, x, y); } #endif -bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { - if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { +bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " - "temperature=%f", - sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, - t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, - t->variant.environment_metrics.temperature); - LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, t->variant.environment_metrics.voltage, - t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, - t->variant.environment_metrics.white_lux); + LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " + "temperature=%f", + sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, + t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, + t->variant.environment_metrics.temperature); + LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, + t->variant.environment_metrics.voltage, t->variant.environment_metrics.iaq, + t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, + t->variant.environment_metrics.white_lux); - LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender, t->variant.environment_metrics.wind_speed, - t->variant.environment_metrics.wind_direction, t->variant.environment_metrics.weight); + LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender, + t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction, + t->variant.environment_metrics.weight); - LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); + LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) { - bool valid = true; - bool hasSensor = false; - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_environment_metrics_tag; - m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; +bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) +{ + bool valid = true; + bool hasSensor = false; + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_environment_metrics_tag; + m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; - for (TelemetrySensor *sensor : sensors) { - valid = valid && sensor->getMetrics(m); - hasSensor = true; - } + for (TelemetrySensor *sensor : sensors) { + valid = valid && sensor->getMetrics(m); + hasSensor = true; + } #ifndef T1000X_SENSOR_EN - if (ina219Sensor.hasSensor()) { - valid = valid && ina219Sensor.getMetrics(m); - hasSensor = true; - } - if (ina260Sensor.hasSensor()) { - valid = valid && ina260Sensor.getMetrics(m); - hasSensor = true; - } - if (ina3221Sensor.hasSensor()) { - valid = valid && ina3221Sensor.getMetrics(m); - hasSensor = true; - } - if (max17048Sensor.hasSensor()) { - valid = valid && max17048Sensor.getMetrics(m); - hasSensor = true; - } + if (ina219Sensor.hasSensor()) { + valid = valid && ina219Sensor.getMetrics(m); + hasSensor = true; + } + if (ina260Sensor.hasSensor()) { + valid = valid && ina260Sensor.getMetrics(m); + hasSensor = true; + } + if (ina3221Sensor.hasSensor()) { + valid = valid && ina3221Sensor.getMetrics(m); + hasSensor = true; + } + if (max17048Sensor.hasSensor()) { + valid = valid && max17048Sensor.getMetrics(m); + hasSensor = true; + } #endif #ifdef HAS_RAKPROT - valid = valid && rak9154Sensor.getMetrics(m); - hasSensor = true; + valid = valid && rak9154Sensor.getMetrics(m); + hasSensor = true; #endif - return valid && hasSensor; + return valid && hasSensor; } -meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() { - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding EnvironmentTelemetry module!"); - return NULL; +meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() +{ + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding EnvironmentTelemetry module!"); + return NULL; + } + // Check for a request for environment metrics + if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getEnvironmentTelemetry(&m)) { + LOG_INFO("Environment telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } } - // Check for a request for environment metrics - if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getEnvironmentTelemetry(&m)) { - LOG_INFO("Environment telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } - } - return NULL; + return NULL; } -bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_environment_metrics_tag; - m.time = getTime(); +bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) +{ + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_environment_metrics_tag; + m.time = getTime(); - if (getEnvironmentTelemetry(&m)) { - LOG_INFO("Send: barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, temperature=%f", - m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.current, m.variant.environment_metrics.gas_resistance, - m.variant.environment_metrics.relative_humidity, m.variant.environment_metrics.temperature); - LOG_INFO("Send: voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage, m.variant.environment_metrics.iaq, - m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); + if (getEnvironmentTelemetry(&m)) { + LOG_INFO("Send: barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, temperature=%f", + m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.current, + m.variant.environment_metrics.gas_resistance, m.variant.environment_metrics.relative_humidity, + m.variant.environment_metrics.temperature); + LOG_INFO("Send: voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage, + m.variant.environment_metrics.iaq, m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); - LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed, - m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight); + LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed, + m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight); - LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); + LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); - LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, - m.variant.environment_metrics.soil_moisture); + LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, + m.variant.environment_metrics.soil_moisture); - sensor_read_error_count = 0; + sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); - notification->level = meshtastic_LogRecord_Level_INFO; - notification->time = getValidTime(RTCQualityFromNet); - sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", - Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs) / - 1000U); - service->sendClientNotification(notification); - sleepOnNextExecution = true; - LOG_DEBUG("Start next execution in 5s, then sleep"); - setIntervalFromNow(FIVE_SECONDS_MS); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } + } + return true; } - return true; - } - return false; + return false; } -AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; +AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL - for (TelemetrySensor *sensor : sensors) { - result = sensor->handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } + for (TelemetrySensor *sensor : sensors) { + result = sensor->handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } - if (ina219Sensor.hasSensor()) { - result = ina219Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } - if (ina260Sensor.hasSensor()) { - result = ina260Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } - if (ina3221Sensor.hasSensor()) { - result = ina3221Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } - if (max17048Sensor.hasSensor()) { - result = max17048Sensor.handleAdminMessage(mp, request, response); - if (result != AdminMessageHandleResult::NOT_HANDLED) - return result; - } + if (ina219Sensor.hasSensor()) { + result = ina219Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (ina260Sensor.hasSensor()) { + result = ina260Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (ina3221Sensor.hasSensor()) { + result = ina3221Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + if (max17048Sensor.hasSensor()) { + result = max17048Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } #endif - return result; + return result; } #endif diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index d8e15ae02..6e4ce82e7 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -15,53 +15,59 @@ #include #include -class EnvironmentTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, public ProtobufModule { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &EnvironmentTelemetryModule::handleStatusUpdate); +class EnvironmentTelemetryModule : private concurrency::OSThread, + public ScanI2CConsumer, + public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, + &EnvironmentTelemetryModule::handleStatusUpdate); -public: - EnvironmentTelemetryModule() - : concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(), - ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { - lastMeasurementPacket = nullptr; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(10 * 1000); - } - virtual bool wantUIFrame() override; + public: + EnvironmentTelemetryModule() + : concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(), + ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + lastMeasurementPacket = nullptr; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(10 * 1000); + } + virtual bool wantUIFrame() override; #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif -protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Environment telemetry data - @return true if it contains valid data - */ - bool getEnvironmentTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Environment telemetry data + @return true if it contains valid data + */ + bool getEnvironmentTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; - void i2cScanFinished(ScanI2C *i2cScanner); + void i2cScanFinished(ScanI2C *i2cScanner); -private: - bool firstTime = 1; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; - uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; + private: + bool firstTime = 1; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; + uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index db37d1ad5..215e49c7a 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -33,214 +33,226 @@ MLX90614Sensor mlx90614Sensor; #endif #include -int32_t HealthTelemetryModule::runOnce() { - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = - Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval, default_telemetry_broadcast_interval_secs); - LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); - doDeepSleep(nightyNightMs, true, false); - } - - uint32_t result = UINT32_MAX; - - if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } - - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = false; - - if (moduleConfig.telemetry.health_measurement_enabled) { - LOG_INFO("Health Telemetry: init"); - // Initialize sensors - if (mlx90614Sensor.hasSensor()) - result = mlx90614Sensor.runOnce(); - if (max30102Sensor.hasSensor()) - result = max30102Sensor.runOnce(); - } - return result == UINT32_MAX ? disable() : setStartDelay(); - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.health_measurement_enabled) { - return disable(); +int32_t HealthTelemetryModule::runOnce() +{ + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); } - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.health_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - lastSentToPhone = millis(); + uint32_t result = UINT32_MAX; + + if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); } - } - return min(sendToPhoneIntervalMs, result); + + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = false; + + if (moduleConfig.telemetry.health_measurement_enabled) { + LOG_INFO("Health Telemetry: init"); + // Initialize sensors + if (mlx90614Sensor.hasSensor()) + result = mlx90614Sensor.runOnce(); + if (max30102Sensor.hasSensor()) + result = max30102Sensor.runOnce(); + } + return result == UINT32_MAX ? disable() : setStartDelay(); + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.health_measurement_enabled) { + return disable(); + } + + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.health_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + } + return min(sendToPhoneIntervalMs, result); } -bool HealthTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.health_screen_enabled; } - -void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - if (lastMeasurementPacket == nullptr) { - // If there's no valid packet, display "Health" - display->drawString(x, y, "Health"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); - return; - } - - // Decode the last measurement packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); - - const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); - return; - } - - // Display "Health From: ..." on its own - char headerStr[64]; - snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); - display->drawString(x, y, headerStr); - - char last_temp[16]; - if (moduleConfig.telemetry.environment_display_fahrenheit) { - snprintf(last_temp, sizeof(last_temp), "%.0f°F", UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); - } else { - snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature); - } - - // Continue with the remaining details - char tempStr[32]; - snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); - display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); - if (lastMeasurement.variant.health_metrics.has_heart_bpm) { - char heartStr[32]; - snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); - display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); - } - if (lastMeasurement.variant.health_metrics.has_spO2) { - char spo2Str[32]; - snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); - display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); - } +bool HealthTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.health_screen_enabled; } -bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { - if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { +void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (lastMeasurementPacket == nullptr) { + // If there's no valid packet, display "Health" + display->drawString(x, y, "Health"); + display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + return; + } + + // Decode the last measurement packet + meshtastic_Telemetry lastMeasurement; + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + const char *lastSender = getSenderShortName(*lastMeasurementPacket); + + const meshtastic_Data &p = lastMeasurementPacket->decoded; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { + display->drawString(x, y, "Measurement Error"); + LOG_ERROR("Unable to decode last packet"); + return; + } + + // Display "Health From: ..." on its own + char headerStr[64]; + snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); + display->drawString(x, y, headerStr); + + char last_temp[16]; + if (moduleConfig.telemetry.environment_display_fahrenheit) { + snprintf(last_temp, sizeof(last_temp), "%.0f°F", + UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); + } else { + snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature); + } + + // Continue with the remaining details + char tempStr[32]; + snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); + display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); + if (lastMeasurement.variant.health_metrics.has_heart_bpm) { + char heartStr[32]; + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); + } + if (lastMeasurement.variant.health_metrics.has_spO2) { + char spo2Str[32]; + snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); + } +} + +bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature, - t->variant.health_metrics.heart_bpm, t->variant.health_metrics.spO2); + LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature, + t->variant.health_metrics.heart_bpm, t->variant.health_metrics.spO2); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) { - bool valid = true; - bool hasSensor = false; - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_health_metrics_tag; - m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; +bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) +{ + bool valid = true; + bool hasSensor = false; + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_health_metrics_tag; + m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; - if (max30102Sensor.hasSensor()) { - valid = valid && max30102Sensor.getMetrics(m); - hasSensor = true; - } - if (mlx90614Sensor.hasSensor()) { - valid = valid && mlx90614Sensor.getMetrics(m); - hasSensor = true; - } + if (max30102Sensor.hasSensor()) { + valid = valid && max30102Sensor.getMetrics(m); + hasSensor = true; + } + if (mlx90614Sensor.hasSensor()) { + valid = valid && mlx90614Sensor.getMetrics(m); + hasSensor = true; + } - return valid && hasSensor; + return valid && hasSensor; } -meshtastic_MeshPacket *HealthTelemetryModule::allocReply() { - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding HealthTelemetry module!"); - return NULL; +meshtastic_MeshPacket *HealthTelemetryModule::allocReply() +{ + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding HealthTelemetry module!"); + return NULL; + } + // Check for a request for health metrics + if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getHealthTelemetry(&m)) { + LOG_INFO("Health telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } } - // Check for a request for health metrics - if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getHealthTelemetry(&m)) { - LOG_INFO("Health telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } - } - return NULL; + return NULL; } -bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_health_metrics_tag; - m.time = getTime(); - if (getHealthTelemetry(&m)) { - LOG_INFO("Send: temperature=%f, heart_bpm=%d, spO2=%d", m.variant.health_metrics.temperature, m.variant.health_metrics.heart_bpm, - m.variant.health_metrics.spO2); +bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) +{ + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_health_metrics_tag; + m.time = getTime(); + if (getHealthTelemetry(&m)) { + LOG_INFO("Send: temperature=%f, heart_bpm=%d, spO2=%d", m.variant.health_metrics.temperature, + m.variant.health_metrics.heart_bpm, m.variant.health_metrics.spO2); - sensor_read_error_count = 0; + sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - LOG_DEBUG("Start next execution in 5s, then sleep"); - sleepOnNextExecution = true; - setIntervalFromNow(5000); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + LOG_DEBUG("Start next execution in 5s, then sleep"); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } + } + return true; } - return true; - } - return false; + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HealthTelemetry.h b/src/modules/Telemetry/HealthTelemetry.h index ffe95f36d..01e4c2372 100644 --- a/src/modules/Telemetry/HealthTelemetry.h +++ b/src/modules/Telemetry/HealthTelemetry.h @@ -9,49 +9,52 @@ #include #include -class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); +class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); -public: - HealthTelemetryModule() - : concurrency::OSThread("HealthTelemetry"), ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { - lastMeasurementPacket = nullptr; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(10 * 1000); - } + public: + HealthTelemetryModule() + : concurrency::OSThread("HealthTelemetry"), + ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + lastMeasurementPacket = nullptr; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(10 * 1000); + } #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - virtual bool wantUIFrame() override; + virtual bool wantUIFrame() override; -protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Health telemetry data - @return true if it contains valid data - */ - bool getHealthTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Health telemetry data + @return true if it contains valid data + */ + bool getHealthTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); -private: - bool firstTime = 1; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; - uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; + private: + bool firstTime = 1; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; + uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp index cd736797b..577132006 100644 --- a/src/modules/Telemetry/HostMetrics.cpp +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -6,34 +6,37 @@ #include #endif -int32_t HostMetricsModule::runOnce() { +int32_t HostMetricsModule::runOnce() +{ #if ARCH_PORTDUINO - if (portduino_config.hostMetrics_interval == 0) { - return disable(); - } else { - sendMetrics(); - return 60 * 1000 * portduino_config.hostMetrics_interval; - } + if (portduino_config.hostMetrics_interval == 0) { + return disable(); + } else { + sendMetrics(); + return 60 * 1000 * portduino_config.hostMetrics_interval; + } #else - return disable(); + return disable(); #endif } -bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { - if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { +bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); - if (t->variant.host_metrics.has_user_string) - t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; + const char *sender = getSenderShortName(mp); + if (t->variant.host_metrics.has_user_string) + t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; - LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, - t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, t->variant.host_metrics.freemem_bytes, - static_cast(t->variant.host_metrics.load1) / 100, static_cast(t->variant.host_metrics.load5) / 100, - static_cast(t->variant.host_metrics.load15) / 100); - // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); + LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, + t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, + t->variant.host_metrics.freemem_bytes, static_cast(t->variant.host_metrics.load1) / 100, + static_cast(t->variant.host_metrics.load5) / 100, + static_cast(t->variant.host_metrics.load15) / 100); + // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); #endif - } - return false; // Let others look at this message also if they want + } + return false; // Let others look at this message also if they want } /* @@ -62,72 +65,75 @@ meshtastic_MeshPacket *HostMetricsModule::allocReply() */ #if ARCH_PORTDUINO -meshtastic_Telemetry HostMetricsModule::getHostMetrics() { - std::string file_line; - meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; - t.which_variant = meshtastic_Telemetry_host_metrics_tag; - t.variant.host_metrics = meshtastic_HostMetrics_init_zero; +meshtastic_Telemetry HostMetricsModule::getHostMetrics() +{ + std::string file_line; + meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; + t.which_variant = meshtastic_Telemetry_host_metrics_tag; + t.variant.host_metrics = meshtastic_HostMetrics_init_zero; - if (access("/proc/uptime", R_OK) == 0) { - std::ifstream proc_uptime("/proc/uptime"); - if (proc_uptime.is_open()) { - std::getline(proc_uptime, file_line, ' '); - proc_uptime.close(); - t.variant.host_metrics.uptime_seconds = stoul(file_line); + if (access("/proc/uptime", R_OK) == 0) { + std::ifstream proc_uptime("/proc/uptime"); + if (proc_uptime.is_open()) { + std::getline(proc_uptime, file_line, ' '); + proc_uptime.close(); + t.variant.host_metrics.uptime_seconds = stoul(file_line); + } } - } - std::filesystem::space_info root = std::filesystem::space("/"); - t.variant.host_metrics.diskfree1_bytes = root.available; + std::filesystem::space_info root = std::filesystem::space("/"); + t.variant.host_metrics.diskfree1_bytes = root.available; - if (access("/proc/meminfo", R_OK) == 0) { - std::ifstream proc_meminfo("/proc/meminfo"); - if (proc_meminfo.is_open()) { - do { - std::getline(proc_meminfo, file_line); - } while (file_line.find("MemAvailable") == std::string::npos); - proc_meminfo.close(); - t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024; + if (access("/proc/meminfo", R_OK) == 0) { + std::ifstream proc_meminfo("/proc/meminfo"); + if (proc_meminfo.is_open()) { + do { + std::getline(proc_meminfo, file_line); + } while (file_line.find("MemAvailable") == std::string::npos); + proc_meminfo.close(); + t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024; + } } - } - if (access("/proc/loadavg", R_OK) == 0) { - std::ifstream proc_loadavg("/proc/loadavg"); - if (proc_loadavg.is_open()) { - std::getline(proc_loadavg, file_line, ' '); - t.variant.host_metrics.load1 = stof(file_line) * 100; - std::getline(proc_loadavg, file_line, ' '); - t.variant.host_metrics.load5 = stof(file_line) * 100; - std::getline(proc_loadavg, file_line, ' '); - t.variant.host_metrics.load15 = stof(file_line) * 100; - proc_loadavg.close(); + if (access("/proc/loadavg", R_OK) == 0) { + std::ifstream proc_loadavg("/proc/loadavg"); + if (proc_loadavg.is_open()) { + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load1 = stof(file_line) * 100; + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load5 = stof(file_line) * 100; + std::getline(proc_loadavg, file_line, ' '); + t.variant.host_metrics.load15 = stof(file_line) * 100; + proc_loadavg.close(); + } } - } - if (portduino_config.hostMetrics_user_command != "") { - std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str()); - if (userCommandResult.length() > 1) { - strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); - t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; - t.variant.host_metrics.has_user_string = true; + if (portduino_config.hostMetrics_user_command != "") { + std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str()); + if (userCommandResult.length() > 1) { + strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); + t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; + t.variant.host_metrics.has_user_string = true; + } } - } - return t; + return t; } -bool HostMetricsModule::sendMetrics() { - meshtastic_Telemetry telemetry = getHostMetrics(); - LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", telemetry.variant.host_metrics.uptime_seconds, - telemetry.variant.host_metrics.diskfree1_bytes, telemetry.variant.host_metrics.freemem_bytes, - static_cast(telemetry.variant.host_metrics.load1) / 100, static_cast(telemetry.variant.host_metrics.load5) / 100, - static_cast(telemetry.variant.host_metrics.load15) / 100); - // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); +bool HostMetricsModule::sendMetrics() +{ + meshtastic_Telemetry telemetry = getHostMetrics(); + LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", + telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes, + telemetry.variant.host_metrics.freemem_bytes, static_cast(telemetry.variant.host_metrics.load1) / 100, + static_cast(telemetry.variant.host_metrics.load5) / 100, + static_cast(telemetry.variant.host_metrics.load15) / 100); + // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); - meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); - p->to = NODENUM_BROADCAST; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - p->channel = portduino_config.hostMetrics_channel; - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); - return true; + meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->channel = portduino_config.hostMetrics_channel; + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); + return true; } #endif diff --git a/src/modules/Telemetry/HostMetrics.h b/src/modules/Telemetry/HostMetrics.h index e737d171c..99ee631c1 100644 --- a/src/modules/Telemetry/HostMetrics.h +++ b/src/modules/Telemetry/HostMetrics.h @@ -2,36 +2,39 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "ProtobufModule.h" -class HostMetricsModule : private concurrency::OSThread, public ProtobufModule { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &HostMetricsModule::handleStatusUpdate); +class HostMetricsModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &HostMetricsModule::handleStatusUpdate); -public: - HostMetricsModule() - : concurrency::OSThread("HostMetrics"), ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { - uptimeWrapCount = 0; - uptimeLastMs = millis(); - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent - } - virtual bool wantUIFrame() { return false; } + public: + HostMetricsModule() + : concurrency::OSThread("HostMetrics"), + ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + uptimeWrapCount = 0; + uptimeLastMs = millis(); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + } + virtual bool wantUIFrame() { return false; } -protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - // virtual meshtastic_MeshPacket *allocReply() override; - virtual int32_t runOnce() override; - /** - * Send our Telemetry into the mesh - */ - bool sendMetrics(); + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + // virtual meshtastic_MeshPacket *allocReply() override; + virtual int32_t runOnce() override; + /** + * Send our Telemetry into the mesh + */ + bool sendMetrics(); -private: - meshtastic_Telemetry getHostMetrics(); + private: + meshtastic_Telemetry getHostMetrics(); - uint32_t lastSentToMesh = 0; - uint32_t uptimeWrapCount; - uint32_t uptimeLastMs; + uint32_t lastSentToMesh = 0; + uint32_t uptimeWrapCount; + uint32_t uptimeLastMs; }; \ No newline at end of file diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 11813b9c7..9047c7cd4 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -22,255 +22,268 @@ #include "graphics/ScreenFonts.h" #include -namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, + bool show_date); } -int32_t PowerTelemetryModule::runOnce() { - if (sleepOnNextExecution == true) { - sleepOnNextExecution = false; - uint32_t nightyNightMs = - Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs); - LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); - doDeepSleep(nightyNightMs, true, false); - } +int32_t PowerTelemetryModule::runOnce() +{ + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ - // moduleConfig.telemetry.power_measurement_enabled = 1; - // moduleConfig.telemetry.power_screen_enabled = 1; - // moduleConfig.telemetry.power_update_interval = 45; + // moduleConfig.telemetry.power_measurement_enabled = 1; + // moduleConfig.telemetry.power_screen_enabled = 1; + // moduleConfig.telemetry.power_update_interval = 45; - if (!(moduleConfig.telemetry.power_measurement_enabled)) { - // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it - return disable(); - } + if (!(moduleConfig.telemetry.power_measurement_enabled)) { + // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it + return disable(); + } - uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.power_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes); + uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes); - if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup - firstTime = 0; - uint32_t result = UINT32_MAX; + if (firstTime) { + // This is the first time the OSThread library has called this function, so do some setup + firstTime = 0; + uint32_t result = UINT32_MAX; #if HAS_TELEMETRY - if (moduleConfig.telemetry.power_measurement_enabled) { - LOG_INFO("Power Telemetry: init"); - // If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again, - // but we need to set the result to != UINT32_MAX to avoid it being disabled - if (ina219Sensor.hasSensor()) - result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce(); - if (ina226Sensor.hasSensor()) - result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce(); - if (ina260Sensor.hasSensor()) - result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce(); - if (ina3221Sensor.hasSensor()) - result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce(); - if (max17048Sensor.hasSensor()) - result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); - } + if (moduleConfig.telemetry.power_measurement_enabled) { + LOG_INFO("Power Telemetry: init"); + // If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again, + // but we need to set the result to != UINT32_MAX to avoid it being disabled + if (ina219Sensor.hasSensor()) + result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce(); + if (ina226Sensor.hasSensor()) + result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce(); + if (ina260Sensor.hasSensor()) + result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce(); + if (ina3221Sensor.hasSensor()) + result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce(); + if (max17048Sensor.hasSensor()) + result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); + } - // it's possible to have this module enabled, only for displaying values on the screen. - // therefore, we should only enable the sensor loop if measurement is also enabled - return result == UINT32_MAX ? disable() : setStartDelay(); + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); #else - return disable(); + return disable(); #endif - } else { - // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.power_measurement_enabled) - return disable(); + } else { + // if we somehow got to a second run of this module with measurement disabled, then just wait forever + if (!moduleConfig.telemetry.power_measurement_enabled) + return disable(); - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - lastSentToPhone = millis(); + if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } } - } - return min(sendToPhoneIntervalMs, sendToMeshIntervalMs); + return min(sendToPhoneIntervalMs, sendToMeshIntervalMs); } -bool PowerTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.power_screen_enabled; } +bool PowerTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.power_screen_enabled; +} #if HAS_SCREEN -void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; + // === Set Title + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - if (lastMeasurementPacket == nullptr) { - // In case of no valid packet, display "Power Telemetry", "No measurement" - display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); - return; - } + if (lastMeasurementPacket == nullptr) { + // In case of no valid packet, display "Power Telemetry", "No measurement" + display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); + return; + } - // Decode the last power packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); + // Decode the last power packet + meshtastic_Telemetry lastMeasurement; + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + const char *lastSender = getSenderShortName(*lastMeasurementPacket); - const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); - return; - } + const meshtastic_Data &p = lastMeasurementPacket->decoded; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { + display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); + LOG_ERROR("Unable to decode last packet"); + return; + } - // Display "Pow. From: ..." - char fromStr[64]; - snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); - display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); + // Display "Pow. From: ..." + char fromStr[64]; + snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); + display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); - // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags - const auto &m = lastMeasurement.variant.power_metrics; - int lineY = textSecondLine; + // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags + const auto &m = lastMeasurement.variant.power_metrics; + int lineY = textSecondLine; - auto drawLine = [&](const char *label, float voltage, float current) { - char lineStr[64]; - snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); - display->drawString(x, lineY, lineStr); - lineY += _fontHeight(FONT_SMALL); - }; + auto drawLine = [&](const char *label, float voltage, float current) { + char lineStr[64]; + snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); + display->drawString(x, lineY, lineStr); + lineY += _fontHeight(FONT_SMALL); + }; - if (m.has_ch1_voltage || m.has_ch1_current) { - drawLine("Ch1", m.ch1_voltage, m.ch1_current); - } - if (m.has_ch2_voltage || m.has_ch2_current) { - drawLine("Ch2", m.ch2_voltage, m.ch2_current); - } - if (m.has_ch3_voltage || m.has_ch3_current) { - drawLine("Ch3", m.ch3_voltage, m.ch3_current); - } - graphics::drawCommonFooter(display, x, y); + if (m.has_ch1_voltage || m.has_ch1_current) { + drawLine("Ch1", m.ch1_voltage, m.ch1_current); + } + if (m.has_ch2_voltage || m.has_ch2_current) { + drawLine("Ch2", m.ch2_voltage, m.ch2_current); + } + if (m.has_ch3_voltage || m.has_ch3_current) { + drawLine("Ch3", m.ch3_voltage, m.ch3_current); + } + graphics::drawCommonFooter(display, x, y); } #endif -bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { - if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { +bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) +{ + if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - const char *sender = getSenderShortName(mp); + const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, " - "ch3_voltage=%.1f, ch3_current=%.1f", - sender, t->variant.power_metrics.ch1_voltage, t->variant.power_metrics.ch1_current, t->variant.power_metrics.ch2_voltage, - t->variant.power_metrics.ch2_current, t->variant.power_metrics.ch3_voltage, t->variant.power_metrics.ch3_current); + LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, " + "ch3_voltage=%.1f, ch3_current=%.1f", + sender, t->variant.power_metrics.ch1_voltage, t->variant.power_metrics.ch1_current, + t->variant.power_metrics.ch2_voltage, t->variant.power_metrics.ch2_current, t->variant.power_metrics.ch3_voltage, + t->variant.power_metrics.ch3_current); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(mp); - } + lastMeasurementPacket = packetPool.allocCopy(mp); + } - return false; // Let others look at this message also if they want + return false; // Let others look at this message also if they want } -bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m) { - bool valid = false; - m->time = getTime(); - m->which_variant = meshtastic_Telemetry_power_metrics_tag; +bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m) +{ + bool valid = false; + m->time = getTime(); + m->which_variant = meshtastic_Telemetry_power_metrics_tag; - m->variant.power_metrics = meshtastic_PowerMetrics_init_zero; + m->variant.power_metrics = meshtastic_PowerMetrics_init_zero; #if HAS_TELEMETRY - if (ina219Sensor.hasSensor()) - valid = ina219Sensor.getMetrics(m); - if (ina226Sensor.hasSensor()) - valid = ina226Sensor.getMetrics(m); - if (ina260Sensor.hasSensor()) - valid = ina260Sensor.getMetrics(m); - if (ina3221Sensor.hasSensor()) - valid = ina3221Sensor.getMetrics(m); - if (max17048Sensor.hasSensor()) - valid = max17048Sensor.getMetrics(m); + if (ina219Sensor.hasSensor()) + valid = ina219Sensor.getMetrics(m); + if (ina226Sensor.hasSensor()) + valid = ina226Sensor.getMetrics(m); + if (ina260Sensor.hasSensor()) + valid = ina260Sensor.getMetrics(m); + if (ina3221Sensor.hasSensor()) + valid = ina3221Sensor.getMetrics(m); + if (max17048Sensor.hasSensor()) + valid = max17048Sensor.getMetrics(m); #endif - return valid; + return valid; } -meshtastic_MeshPacket *PowerTelemetryModule::allocReply() { - if (currentRequest) { - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - } else { - LOG_ERROR("Error decoding PowerTelemetry module!"); - return NULL; +meshtastic_MeshPacket *PowerTelemetryModule::allocReply() +{ + if (currentRequest) { + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + } else { + LOG_ERROR("Error decoding PowerTelemetry module!"); + return NULL; + } + // Check for a request for power metrics + if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + if (getPowerTelemetry(&m)) { + LOG_INFO("Power telemetry reply to request"); + return allocDataProtobuf(m); + } else { + return NULL; + } + } } - // Check for a request for power metrics - if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - if (getPowerTelemetry(&m)) { - LOG_INFO("Power telemetry reply to request"); - return allocDataProtobuf(m); - } else { - return NULL; - } - } - } - return NULL; + return NULL; } -bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { - meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; - m.which_variant = meshtastic_Telemetry_power_metrics_tag; - m.time = getTime(); - if (getPowerTelemetry(&m)) { - LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, " - "ch3_voltage=%f, ch3_current=%f", - m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage, - m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current); +bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) +{ + meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_power_metrics_tag; + m.time = getTime(); + if (getPowerTelemetry(&m)) { + LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, " + "ch3_voltage=%f, ch3_current=%f", + m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage, + m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current); - sensor_read_error_count = 0; + sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); - p->to = dest; - p->decoded.want_response = false; - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) - p->priority = meshtastic_MeshPacket_Priority_RELIABLE; - else - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); + meshtastic_MeshPacket *p = allocDataProtobuf(m); + p->to = dest; + p->decoded.want_response = false; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) + p->priority = meshtastic_MeshPacket_Priority_RELIABLE; + else + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + // release previous packet before occupying a new spot + if (lastMeasurementPacket != nullptr) + packetPool.release(lastMeasurementPacket); - lastMeasurementPacket = packetPool.allocCopy(*p); - if (phoneOnly) { - LOG_INFO("Send packet to phone"); - service->sendToPhone(p); - } else { - LOG_INFO("Send packet to mesh"); - service->sendToMesh(p, RX_SRC_LOCAL, true); + lastMeasurementPacket = packetPool.allocCopy(*p); + if (phoneOnly) { + LOG_INFO("Send packet to phone"); + service->sendToPhone(p); + } else { + LOG_INFO("Send packet to mesh"); + service->sendToMesh(p, RX_SRC_LOCAL, true); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { - LOG_DEBUG("Start next execution in 5s then sleep"); - sleepOnNextExecution = true; - setIntervalFromNow(5000); - } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + LOG_DEBUG("Start next execution in 5s then sleep"); + sleepOnNextExecution = true; + setIntervalFromNow(5000); + } + } + return true; } - return true; - } - return false; + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/PowerTelemetry.h b/src/modules/Telemetry/PowerTelemetry.h index cb88ce05b..b9ec6edc1 100644 --- a/src/modules/Telemetry/PowerTelemetry.h +++ b/src/modules/Telemetry/PowerTelemetry.h @@ -10,47 +10,50 @@ #include #include -class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule { - CallbackObserver nodeStatusObserver = - CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); +class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule +{ + CallbackObserver nodeStatusObserver = + CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); -public: - PowerTelemetryModule() - : concurrency::OSThread("PowerTelemetry"), ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { - lastMeasurementPacket = nullptr; - nodeStatusObserver.observe(&nodeStatus->onNewStatus); - setIntervalFromNow(10 * 1000); - } - virtual bool wantUIFrame() override; + public: + PowerTelemetryModule() + : concurrency::OSThread("PowerTelemetry"), + ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) + { + lastMeasurementPacket = nullptr; + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(10 * 1000); + } + virtual bool wantUIFrame() override; #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif -protected: - /** Called to handle a particular incoming message - @return true if you've guaranteed you've handled this message and no other handlers should be considered for it - */ - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; - virtual int32_t runOnce() override; - /** Called to get current Power telemetry data - @return true if it contains valid data - */ - bool getPowerTelemetry(meshtastic_Telemetry *m); - virtual meshtastic_MeshPacket *allocReply() override; - /** - * Send our Telemetry into the mesh - */ - bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + protected: + /** Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; + virtual int32_t runOnce() override; + /** Called to get current Power telemetry data + @return true if it contains valid data + */ + bool getPowerTelemetry(meshtastic_Telemetry *m); + virtual meshtastic_MeshPacket *allocReply() override; + /** + * Send our Telemetry into the mesh + */ + bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); -private: - bool firstTime = 1; - meshtastic_MeshPacket *lastMeasurementPacket; - uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; - uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; + private: + bool firstTime = 1; + meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute + uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; + uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/AHT10.cpp b/src/modules/Telemetry/Sensor/AHT10.cpp index 5f6cbb22b..c38fd2a92 100644 --- a/src/modules/Telemetry/Sensor/AHT10.cpp +++ b/src/modules/Telemetry/Sensor/AHT10.cpp @@ -15,33 +15,35 @@ AHT10Sensor::AHT10Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_AHT10, "AHT10") {} -bool AHT10Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - aht10 = Adafruit_AHTX0(); - status = aht10.begin(bus, 0, dev->address.address); +bool AHT10Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + aht10 = Adafruit_AHTX0(); + status = aht10.begin(bus, 0, dev->address.address); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement) { - LOG_DEBUG("AHT10 getMetrics"); +bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("AHT10 getMetrics"); - sensors_event_t humidity, temp; - aht10.getEvent(&humidity, &temp); + sensors_event_t humidity, temp; + aht10.getEvent(&humidity, &temp); - // prefer other sensors like bmp280, bmp3xx - if (!measurement->variant.environment_metrics.has_temperature) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = temp.temperature + AHT10_TEMP_OFFSET; - } + // prefer other sensors like bmp280, bmp3xx + if (!measurement->variant.environment_metrics.has_temperature) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = temp.temperature + AHT10_TEMP_OFFSET; + } - if (!measurement->variant.environment_metrics.has_relative_humidity) { - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - } + if (!measurement->variant.environment_metrics.has_relative_humidity) { + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; + } - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/AHT10.h b/src/modules/Telemetry/Sensor/AHT10.h index bf17d344b..f85f04aa0 100644 --- a/src/modules/Telemetry/Sensor/AHT10.h +++ b/src/modules/Telemetry/Sensor/AHT10.h @@ -14,14 +14,15 @@ #include "TelemetrySensor.h" #include -class AHT10Sensor : public TelemetrySensor { -private: - Adafruit_AHTX0 aht10; +class AHT10Sensor : public TelemetrySensor +{ + private: + Adafruit_AHTX0 aht10; -public: - AHT10Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + AHT10Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/BH1750Sensor.cpp b/src/modules/Telemetry/Sensor/BH1750Sensor.cpp index 38efe6a66..b8790dcd5 100644 --- a/src/modules/Telemetry/Sensor/BH1750Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BH1750Sensor.cpp @@ -13,40 +13,42 @@ BH1750Sensor::BH1750Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BH1750, "BH1750") {} -bool BH1750Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s with mode %d", sensorName, BH1750_SENSOR_MODE); +bool BH1750Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s with mode %d", sensorName, BH1750_SENSOR_MODE); - bh1750 = BH1750_WE(bus, dev->address.address); - status = bh1750.init(); - if (!status) { + bh1750 = BH1750_WE(bus, dev->address.address); + status = bh1750.init(); + if (!status) { + return status; + } + + bh1750.setMode(BH1750_SENSOR_MODE); + + initI2CSensor(); return status; - } - - bh1750.setMode(BH1750_SENSOR_MODE); - - initI2CSensor(); - return status; } -bool BH1750Sensor::getMetrics(meshtastic_Telemetry *measurement) { +bool BH1750Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ - /* An OTH and OTH_2 measurement takes ~120 ms. I suggest to wait - 140 ms to be on the safe side. - An OTL measurement takes about 16 ms. I suggest to wait 20 ms - to be on the safe side. */ - if (BH1750_SENSOR_MODE == BH1750Mode::OTH || BH1750_SENSOR_MODE == BH1750Mode::OTH_2) { - bh1750.setMode(BH1750_SENSOR_MODE); - delay(140); // wait for measurement to be completed - } else if (BH1750_SENSOR_MODE == BH1750Mode::OTL) { - bh1750.setMode(BH1750_SENSOR_MODE); - delay(20); - } + /* An OTH and OTH_2 measurement takes ~120 ms. I suggest to wait + 140 ms to be on the safe side. + An OTL measurement takes about 16 ms. I suggest to wait 20 ms + to be on the safe side. */ + if (BH1750_SENSOR_MODE == BH1750Mode::OTH || BH1750_SENSOR_MODE == BH1750Mode::OTH_2) { + bh1750.setMode(BH1750_SENSOR_MODE); + delay(140); // wait for measurement to be completed + } else if (BH1750_SENSOR_MODE == BH1750Mode::OTL) { + bh1750.setMode(BH1750_SENSOR_MODE); + delay(20); + } - measurement->variant.environment_metrics.has_lux = true; - float lightIntensity = bh1750.getLux(); + measurement->variant.environment_metrics.has_lux = true; + float lightIntensity = bh1750.getLux(); - measurement->variant.environment_metrics.lux = lightIntensity; - return true; + measurement->variant.environment_metrics.lux = lightIntensity; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/BH1750Sensor.h b/src/modules/Telemetry/Sensor/BH1750Sensor.h index 3df5c9ca9..d9a4ded95 100644 --- a/src/modules/Telemetry/Sensor/BH1750Sensor.h +++ b/src/modules/Telemetry/Sensor/BH1750Sensor.h @@ -7,14 +7,15 @@ #include "TelemetrySensor.h" #include -class BH1750Sensor : public TelemetrySensor { -private: - BH1750_WE bh1750; +class BH1750Sensor : public TelemetrySensor +{ + private: + BH1750_WE bh1750; -public: - BH1750Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + BH1750Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/BME280Sensor.cpp b/src/modules/Telemetry/Sensor/BME280Sensor.cpp index a793d89b5..779b2e603 100644 --- a/src/modules/Telemetry/Sensor/BME280Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME280Sensor.cpp @@ -10,34 +10,36 @@ BME280Sensor::BME280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME280, "BME280") {} -bool BME280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = bme280.begin(dev->address.address, bus); - if (!status) { +bool BME280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = bme280.begin(dev->address.address, bus); + if (!status) { + return status; + } + + bme280.setSampling(Adafruit_BME280::MODE_FORCED, + Adafruit_BME280::SAMPLING_X1, // Temp. oversampling + Adafruit_BME280::SAMPLING_X1, // Pressure oversampling + Adafruit_BME280::SAMPLING_X1, // Humidity oversampling + Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); + + initI2CSensor(); return status; - } - - bme280.setSampling(Adafruit_BME280::MODE_FORCED, - Adafruit_BME280::SAMPLING_X1, // Temp. oversampling - Adafruit_BME280::SAMPLING_X1, // Pressure oversampling - Adafruit_BME280::SAMPLING_X1, // Humidity oversampling - Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); - - initI2CSensor(); - return status; } -bool BME280Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool BME280Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - LOG_DEBUG("BME280 getMetrics"); - bme280.takeForcedMeasurement(); - measurement->variant.environment_metrics.temperature = bme280.readTemperature(); - measurement->variant.environment_metrics.relative_humidity = bme280.readHumidity(); - measurement->variant.environment_metrics.barometric_pressure = bme280.readPressure() / 100.0F; + LOG_DEBUG("BME280 getMetrics"); + bme280.takeForcedMeasurement(); + measurement->variant.environment_metrics.temperature = bme280.readTemperature(); + measurement->variant.environment_metrics.relative_humidity = bme280.readHumidity(); + measurement->variant.environment_metrics.barometric_pressure = bme280.readPressure() / 100.0F; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME280Sensor.h b/src/modules/Telemetry/Sensor/BME280Sensor.h index 81a7f5139..fadae46cd 100644 --- a/src/modules/Telemetry/Sensor/BME280Sensor.h +++ b/src/modules/Telemetry/Sensor/BME280Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class BME280Sensor : public TelemetrySensor { -private: - Adafruit_BME280 bme280; +class BME280Sensor : public TelemetrySensor +{ + private: + Adafruit_BME280 bme280; -public: - BME280Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + BME280Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 6f871c8e2..95f3dc5f0 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -10,132 +10,139 @@ BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} -int32_t BME680Sensor::runOnce() { - if (!bme680.run()) { - checkStatus("runTrigger"); - } - return 35; -} - -bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - status = 0; - if (!bme680.begin(dev->address.address, *bus)) - checkStatus("begin"); - - if (bme680.status == BSEC_OK) { - status = 1; - if (!bme680.setConfig(bsec_config)) { - checkStatus("setConfig"); - status = 0; +int32_t BME680Sensor::runOnce() +{ + if (!bme680.run()) { + checkStatus("runTrigger"); } - loadState(); - if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) { - checkStatus("updateSubscription"); - status = 0; + return 35; +} + +bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + status = 0; + if (!bme680.begin(dev->address.address, *bus)) + checkStatus("begin"); + + if (bme680.status == BSEC_OK) { + status = 1; + if (!bme680.setConfig(bsec_config)) { + checkStatus("setConfig"); + status = 0; + } + loadState(); + if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) { + checkStatus("updateSubscription"); + status = 0; + } + LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major, + bme680.version.minor, bme680.version.major_bugfix, bme680.version.minor_bugfix); } - LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major, bme680.version.minor, - bme680.version.major_bugfix, bme680.version.minor_bugfix); - } - if (status == 0) - LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); + if (status == 0) + LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) { - if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) - return false; +bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) + return false; - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.has_gas_resistance = true; - measurement->variant.environment_metrics.has_iaq = true; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.has_gas_resistance = true; + measurement->variant.environment_metrics.has_iaq = true; - measurement->variant.environment_metrics.temperature = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE).signal; - measurement->variant.environment_metrics.relative_humidity = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY).signal; - measurement->variant.environment_metrics.barometric_pressure = bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal; - measurement->variant.environment_metrics.gas_resistance = bme680.getData(BSEC_OUTPUT_RAW_GAS).signal / 1000.0; - // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) - measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; - updateState(); - return true; + measurement->variant.environment_metrics.temperature = bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE).signal; + measurement->variant.environment_metrics.relative_humidity = + bme680.getData(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY).signal; + measurement->variant.environment_metrics.barometric_pressure = bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal; + measurement->variant.environment_metrics.gas_resistance = bme680.getData(BSEC_OUTPUT_RAW_GAS).signal / 1000.0; + // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) + measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; + updateState(); + return true; } -void BME680Sensor::loadState() { +void BME680Sensor::loadState() +{ #ifdef FSCom - spiLock->lock(); - auto file = FSCom.open(bsecConfigFileName, FILE_O_READ); - if (file) { - file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.close(); - bme680.setState(bsecState); - LOG_INFO("%s state read from %s", sensorName, bsecConfigFileName); - } else { - LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName); - } - spiLock->unlock(); -#else - LOG_ERROR("ERROR: Filesystem not implemented"); -#endif -} - -void BME680Sensor::updateState() { -#ifdef FSCom - spiLock->lock(); - bool update = false; - if (stateUpdateCounter == 0) { - /* First state update when IAQ accuracy is >= 3 */ - accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy; - if (accuracy >= 2) { - LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy); - update = true; - stateUpdateCounter++; - } else { - LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy); - } - } else { - /* Update every STATE_SAVE_PERIOD minutes */ - if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) { - LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000); - update = true; - stateUpdateCounter++; - } - } - - if (update) { - bme680.getState(bsecState); - if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) { - LOG_WARN("Can't remove old state file"); - } - auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE); + spiLock->lock(); + auto file = FSCom.open(bsecConfigFileName, FILE_O_READ); if (file) { - LOG_INFO("%s state write to %s", sensorName, bsecConfigFileName); - file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.flush(); - file.close(); + file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + bme680.setState(bsecState); + LOG_INFO("%s state read from %s", sensorName, bsecConfigFileName); } else { - LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName); + LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName); } - } - spiLock->unlock(); + spiLock->unlock(); #else - LOG_ERROR("ERROR: Filesystem not implemented"); + LOG_ERROR("ERROR: Filesystem not implemented"); #endif } -void BME680Sensor::checkStatus(const char *functionName) { - if (bme680.status < BSEC_OK) - LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); - else if (bme680.status > BSEC_OK) - LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); +void BME680Sensor::updateState() +{ +#ifdef FSCom + spiLock->lock(); + bool update = false; + if (stateUpdateCounter == 0) { + /* First state update when IAQ accuracy is >= 3 */ + accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy; + if (accuracy >= 2) { + LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy); + update = true; + stateUpdateCounter++; + } else { + LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy); + } + } else { + /* Update every STATE_SAVE_PERIOD minutes */ + if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) { + LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000); + update = true; + stateUpdateCounter++; + } + } - if (bme680.sensor.status < BME68X_OK) - LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); - else if (bme680.sensor.status > BME68X_OK) - LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); + if (update) { + bme680.getState(bsecState); + if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) { + LOG_WARN("Can't remove old state file"); + } + auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE); + if (file) { + LOG_INFO("%s state write to %s", sensorName, bsecConfigFileName); + file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.flush(); + file.close(); + } else { + LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName); + } + } + spiLock->unlock(); +#else + LOG_ERROR("ERROR: Filesystem not implemented"); +#endif +} + +void BME680Sensor::checkStatus(const char *functionName) +{ + if (bme680.status < BSEC_OK) + LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); + else if (bme680.status > BSEC_OK) + LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); + + if (bme680.sensor.status < BME68X_OK) + LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); + else if (bme680.sensor.status > BME68X_OK) + LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index 10202cfdd..f4ead95f7 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -12,33 +12,34 @@ const uint8_t bsec_config[] = { #include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" }; -class BME680Sensor : public TelemetrySensor { -private: - Bsec2 bme680; +class BME680Sensor : public TelemetrySensor +{ + private: + Bsec2 bme680; -protected: - const char *bsecConfigFileName = "/prefs/bsec.dat"; - uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; - uint8_t accuracy = 0; - uint16_t stateUpdateCounter = 0; - bsecSensor sensorList[9] = {BSEC_OUTPUT_IAQ, - BSEC_OUTPUT_RAW_TEMPERATURE, - BSEC_OUTPUT_RAW_PRESSURE, - BSEC_OUTPUT_RAW_HUMIDITY, - BSEC_OUTPUT_RAW_GAS, - BSEC_OUTPUT_STABILIZATION_STATUS, - BSEC_OUTPUT_RUN_IN_STATUS, - BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, - BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; - void loadState(); - void updateState(); - void checkStatus(const char *functionName); + protected: + const char *bsecConfigFileName = "/prefs/bsec.dat"; + uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; + uint8_t accuracy = 0; + uint16_t stateUpdateCounter = 0; + bsecSensor sensorList[9] = {BSEC_OUTPUT_IAQ, + BSEC_OUTPUT_RAW_TEMPERATURE, + BSEC_OUTPUT_RAW_PRESSURE, + BSEC_OUTPUT_RAW_HUMIDITY, + BSEC_OUTPUT_RAW_GAS, + BSEC_OUTPUT_STABILIZATION_STATUS, + BSEC_OUTPUT_RUN_IN_STATUS, + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; + void loadState(); + void updateState(); + void checkStatus(const char *functionName); -public: - BME680Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + BME680Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP085Sensor.cpp b/src/modules/Telemetry/Sensor/BMP085Sensor.cpp index f049b9124..1fb2ecc28 100644 --- a/src/modules/Telemetry/Sensor/BMP085Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BMP085Sensor.cpp @@ -10,25 +10,27 @@ BMP085Sensor::BMP085Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP085, "BMP085") {} -bool BMP085Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool BMP085Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - bmp085 = Adafruit_BMP085(); - status = bmp085.begin(dev->address.address, bus); + bmp085 = Adafruit_BMP085(); + status = bmp085.begin(dev->address.address, bus); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - LOG_DEBUG("BMP085 getMetrics"); - measurement->variant.environment_metrics.temperature = bmp085.readTemperature(); - measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F; + LOG_DEBUG("BMP085 getMetrics"); + measurement->variant.environment_metrics.temperature = bmp085.readTemperature(); + measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP085Sensor.h b/src/modules/Telemetry/Sensor/BMP085Sensor.h index 446d76da2..12ccf35a1 100644 --- a/src/modules/Telemetry/Sensor/BMP085Sensor.h +++ b/src/modules/Telemetry/Sensor/BMP085Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class BMP085Sensor : public TelemetrySensor { -private: - Adafruit_BMP085 bmp085; +class BMP085Sensor : public TelemetrySensor +{ + private: + Adafruit_BMP085 bmp085; -public: - BMP085Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + BMP085Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP280Sensor.cpp b/src/modules/Telemetry/Sensor/BMP280Sensor.cpp index 45b9f92c3..2b7407c43 100644 --- a/src/modules/Telemetry/Sensor/BMP280Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BMP280Sensor.cpp @@ -10,34 +10,36 @@ BMP280Sensor::BMP280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP280, "BMP280") {} -bool BMP280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool BMP280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - bmp280 = Adafruit_BMP280(bus); - status = bmp280.begin(dev->address.address); - if (!status) { + bmp280 = Adafruit_BMP280(bus); + status = bmp280.begin(dev->address.address); + if (!status) { + return status; + } + + bmp280.setSampling(Adafruit_BMP280::MODE_FORCED, + Adafruit_BMP280::SAMPLING_X1, // Temp. oversampling + Adafruit_BMP280::SAMPLING_X1, // Pressure oversampling + Adafruit_BMP280::FILTER_OFF, Adafruit_BMP280::STANDBY_MS_1000); + + initI2CSensor(); return status; - } - - bmp280.setSampling(Adafruit_BMP280::MODE_FORCED, - Adafruit_BMP280::SAMPLING_X1, // Temp. oversampling - Adafruit_BMP280::SAMPLING_X1, // Pressure oversampling - Adafruit_BMP280::FILTER_OFF, Adafruit_BMP280::STANDBY_MS_1000); - - initI2CSensor(); - return status; } -bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - LOG_DEBUG("BMP280 getMetrics"); - bmp280.takeForcedMeasurement(); - measurement->variant.environment_metrics.temperature = bmp280.readTemperature(); - measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F; + LOG_DEBUG("BMP280 getMetrics"); + bmp280.takeForcedMeasurement(); + measurement->variant.environment_metrics.temperature = bmp280.readTemperature(); + measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP280Sensor.h b/src/modules/Telemetry/Sensor/BMP280Sensor.h index 5686fa690..2199fc0cd 100644 --- a/src/modules/Telemetry/Sensor/BMP280Sensor.h +++ b/src/modules/Telemetry/Sensor/BMP280Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class BMP280Sensor : public TelemetrySensor { -private: - Adafruit_BMP280 bmp280; +class BMP280Sensor : public TelemetrySensor +{ + private: + Adafruit_BMP280 bmp280; -public: - BMP280Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + BMP280Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp b/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp index 587a4fa09..ac80732bf 100644 --- a/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp +++ b/src/modules/Telemetry/Sensor/BMP3XXSensor.cpp @@ -6,61 +6,65 @@ BMP3XXSensor::BMP3XXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP3XX, "BMP3XX") {} -bool BMP3XXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool BMP3XXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - // Get a singleton instance and initialise the bmp3xx - if (bmp3xx == nullptr) { - bmp3xx = BMP3XXSingleton::GetInstance(); - } - status = bmp3xx->begin_I2C(dev->address.address, bus); - if (!status) { + // Get a singleton instance and initialise the bmp3xx + if (bmp3xx == nullptr) { + bmp3xx = BMP3XXSingleton::GetInstance(); + } + status = bmp3xx->begin_I2C(dev->address.address, bus); + if (!status) { + return status; + } + + // set up oversampling and filter initialization + bmp3xx->setTemperatureOversampling(BMP3_OVERSAMPLING_4X); + bmp3xx->setPressureOversampling(BMP3_OVERSAMPLING_8X); + bmp3xx->setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3); + bmp3xx->setOutputDataRate(BMP3_ODR_25_HZ); + + // take a couple of initial readings to settle the sensor filters + for (int i = 0; i < 3; i++) { + bmp3xx->performReading(); + } + initI2CSensor(); return status; - } - - // set up oversampling and filter initialization - bmp3xx->setTemperatureOversampling(BMP3_OVERSAMPLING_4X); - bmp3xx->setPressureOversampling(BMP3_OVERSAMPLING_8X); - bmp3xx->setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3); - bmp3xx->setOutputDataRate(BMP3_ODR_25_HZ); - - // take a couple of initial readings to settle the sensor filters - for (int i = 0; i < 3; i++) { - bmp3xx->performReading(); - } - initI2CSensor(); - return status; } -bool BMP3XXSensor::getMetrics(meshtastic_Telemetry *measurement) { - if (bmp3xx == nullptr) { - bmp3xx = BMP3XXSingleton::GetInstance(); - } - if ((int)measurement->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - bmp3xx->performReading(); +bool BMP3XXSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (bmp3xx == nullptr) { + bmp3xx = BMP3XXSingleton::GetInstance(); + } + if ((int)measurement->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + bmp3xx->performReading(); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.has_relative_humidity = false; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.has_relative_humidity = false; - measurement->variant.environment_metrics.temperature = static_cast(bmp3xx->temperature); - measurement->variant.environment_metrics.barometric_pressure = static_cast(bmp3xx->pressure) / 100.0F; - measurement->variant.environment_metrics.relative_humidity = 0.0f; + measurement->variant.environment_metrics.temperature = static_cast(bmp3xx->temperature); + measurement->variant.environment_metrics.barometric_pressure = static_cast(bmp3xx->pressure) / 100.0F; + measurement->variant.environment_metrics.relative_humidity = 0.0f; - LOG_DEBUG("BMP3XX getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant, measurement->variant.environment_metrics.temperature, - measurement->variant.environment_metrics.barometric_pressure); - } else { - LOG_DEBUG("BMP3XX getMetrics id: %i", measurement->which_variant); - } - return true; + LOG_DEBUG("BMP3XX getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant, + measurement->variant.environment_metrics.temperature, + measurement->variant.environment_metrics.barometric_pressure); + } else { + LOG_DEBUG("BMP3XX getMetrics id: %i", measurement->which_variant); + } + return true; } // Get a singleton wrapper for an Adafruit_bmp3xx -BMP3XXSingleton *BMP3XXSingleton::GetInstance() { - if (pinstance == nullptr) { - pinstance = new BMP3XXSingleton(); - } - return pinstance; +BMP3XXSingleton *BMP3XXSingleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new BMP3XXSingleton(); + } + return pinstance; } BMP3XXSingleton::BMP3XXSingleton() {} @@ -69,15 +73,16 @@ BMP3XXSingleton::~BMP3XXSingleton() {} BMP3XXSingleton *BMP3XXSingleton::pinstance{nullptr}; -bool BMP3XXSingleton::performReading() { - bool result = Adafruit_BMP3XX::performReading(); - if (result) { - double atmospheric = this->pressure / 100.0; - altitudeAmslMetres = 44330.0 * (1.0 - pow(atmospheric / SEAL_LEVEL_HPA, 0.1903)); - } else { - altitudeAmslMetres = 0.0; - } - return result; +bool BMP3XXSingleton::performReading() +{ + bool result = Adafruit_BMP3XX::performReading(); + if (result) { + double atmospheric = this->pressure / 100.0; + altitudeAmslMetres = 44330.0 * (1.0 - pow(atmospheric / SEAL_LEVEL_HPA, 0.1903)); + } else { + altitudeAmslMetres = 0.0; + } + return result; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/BMP3XXSensor.h b/src/modules/Telemetry/Sensor/BMP3XXSensor.h index b65a31ea3..7ce14d9db 100644 --- a/src/modules/Telemetry/Sensor/BMP3XXSensor.h +++ b/src/modules/Telemetry/Sensor/BMP3XXSensor.h @@ -12,40 +12,42 @@ #include // Singleton wrapper for the Adafruit_BMP3XX class -class BMP3XXSingleton : public Adafruit_BMP3XX { -private: - static BMP3XXSingleton *pinstance; +class BMP3XXSingleton : public Adafruit_BMP3XX +{ + private: + static BMP3XXSingleton *pinstance; -protected: - BMP3XXSingleton(); - ~BMP3XXSingleton(); + protected: + BMP3XXSingleton(); + ~BMP3XXSingleton(); -public: - // Create a singleton instance (not thread safe) - static BMP3XXSingleton *GetInstance(); + public: + // Create a singleton instance (not thread safe) + static BMP3XXSingleton *GetInstance(); - // Singletons should not be cloneable. - BMP3XXSingleton(BMP3XXSingleton &other) = delete; + // Singletons should not be cloneable. + BMP3XXSingleton(BMP3XXSingleton &other) = delete; - // Singletons should not be assignable. - void operator=(const BMP3XXSingleton &) = delete; + // Singletons should not be assignable. + void operator=(const BMP3XXSingleton &) = delete; - // Performs a full reading of all sensors in the BMP3XX. Assigns - // the internal temperature, pressure and altitudeAmsl variables - bool performReading(); + // Performs a full reading of all sensors in the BMP3XX. Assigns + // the internal temperature, pressure and altitudeAmsl variables + bool performReading(); - // Altitude in metres above mean sea level, assigned after calling performReading() - double altitudeAmslMetres = 0.0f; + // Altitude in metres above mean sea level, assigned after calling performReading() + double altitudeAmslMetres = 0.0f; }; -class BMP3XXSensor : public TelemetrySensor { -protected: - BMP3XXSingleton *bmp3xx = nullptr; +class BMP3XXSensor : public TelemetrySensor +{ + protected: + BMP3XXSingleton *bmp3xx = nullptr; -public: - BMP3XXSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + BMP3XXSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp b/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp index c1aa2c5fe..e7b191398 100644 --- a/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp +++ b/src/modules/Telemetry/Sensor/CGRadSensSensor.cpp @@ -14,51 +14,55 @@ CGRadSensSensor::CGRadSensSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RADSENS, "RadSens") {} -bool CGRadSensSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - // Initialize the sensor following the same pattern as RCWL9620Sensor - LOG_INFO("Init sensor: %s", sensorName); - status = true; - begin(bus, dev->address.address); - initI2CSensor(); - return status; +bool CGRadSensSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + // Initialize the sensor following the same pattern as RCWL9620Sensor + LOG_INFO("Init sensor: %s", sensorName); + status = true; + begin(bus, dev->address.address); + initI2CSensor(); + return status; } -void CGRadSensSensor::begin(TwoWire *wire, uint8_t addr) { - // Store the Wire and address to the sensor following the same pattern as RCWL9620Sensor - _wire = wire; - _addr = addr; - _wire->begin(); +void CGRadSensSensor::begin(TwoWire *wire, uint8_t addr) +{ + // Store the Wire and address to the sensor following the same pattern as RCWL9620Sensor + _wire = wire; + _addr = addr; + _wire->begin(); } -float CGRadSensSensor::getStaticRadiation() { - // Read a register, following the same pattern as the RCWL9620Sensor - _wire->beginTransmission(_addr); // Transfer data to addr. - _wire->write(0x06); // Radiation intensity (static period T = 500 sec) - if (_wire->endTransmission() == 0) { - if (_wire->requestFrom(_addr, (uint8_t)3)) { - ; // Request 3 bytes - uint32_t data = _wire->read(); - data <<= 8; - data |= _wire->read(); - data <<= 8; - data |= _wire->read(); +float CGRadSensSensor::getStaticRadiation() +{ + // Read a register, following the same pattern as the RCWL9620Sensor + _wire->beginTransmission(_addr); // Transfer data to addr. + _wire->write(0x06); // Radiation intensity (static period T = 500 sec) + if (_wire->endTransmission() == 0) { + if (_wire->requestFrom(_addr, (uint8_t)3)) { + ; // Request 3 bytes + uint32_t data = _wire->read(); + data <<= 8; + data |= _wire->read(); + data <<= 8; + data |= _wire->read(); - // As per the data sheet for the RadSens - // Register 0x06 contains the reading in 0.1 * μR / h - float microRadPerHr = float(data) / 10.0; - return microRadPerHr; + // As per the data sheet for the RadSens + // Register 0x06 contains the reading in 0.1 * μR / h + float microRadPerHr = float(data) / 10.0; + return microRadPerHr; + } } - } - return -1.0; + return -1.0; } -bool CGRadSensSensor::getMetrics(meshtastic_Telemetry *measurement) { - // Store the meansurement in the the appropriate fields of the protobuf - measurement->variant.environment_metrics.has_radiation = true; +bool CGRadSensSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + // Store the meansurement in the the appropriate fields of the protobuf + measurement->variant.environment_metrics.has_radiation = true; - LOG_DEBUG("CGRADSENS getMetrics"); - measurement->variant.environment_metrics.radiation = getStaticRadiation(); + LOG_DEBUG("CGRADSENS getMetrics"); + measurement->variant.environment_metrics.radiation = getStaticRadiation(); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/CGRadSensSensor.h b/src/modules/Telemetry/Sensor/CGRadSensSensor.h index da5d36078..c677e8899 100644 --- a/src/modules/Telemetry/Sensor/CGRadSensSensor.h +++ b/src/modules/Telemetry/Sensor/CGRadSensSensor.h @@ -10,19 +10,20 @@ #include "TelemetrySensor.h" #include -class CGRadSensSensor : public TelemetrySensor { -private: - uint8_t _addr = 0x66; - TwoWire *_wire = &Wire; +class CGRadSensSensor : public TelemetrySensor +{ + private: + uint8_t _addr = 0x66; + TwoWire *_wire = &Wire; -protected: - void begin(TwoWire *wire = &Wire, uint8_t addr = 0x66); - float getStaticRadiation(); + protected: + void begin(TwoWire *wire = &Wire, uint8_t addr = 0x66); + float getStaticRadiation(); -public: - CGRadSensSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + CGRadSensSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/CurrentSensor.h b/src/modules/Telemetry/Sensor/CurrentSensor.h index 500485fa8..9827a9aa4 100644 --- a/src/modules/Telemetry/Sensor/CurrentSensor.h +++ b/src/modules/Telemetry/Sensor/CurrentSensor.h @@ -4,9 +4,10 @@ #pragma once -class CurrentSensor { -public: - virtual int16_t getCurrentMa() = 0; +class CurrentSensor +{ + public: + virtual int16_t getCurrentMa() = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp index b85dfd11b..101b01f8f 100644 --- a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp +++ b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp @@ -10,43 +10,46 @@ DFRobotGravitySensor::DFRobotGravitySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_RAIN, "DFROBOT_RAIN") {} -DFRobotGravitySensor::~DFRobotGravitySensor() { - if (gravity) { +DFRobotGravitySensor::~DFRobotGravitySensor() +{ + if (gravity) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" - delete gravity; + delete gravity; #pragma GCC diagnostic pop - gravity = nullptr; - } + gravity = nullptr; + } } -bool DFRobotGravitySensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool DFRobotGravitySensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - gravity = new DFRobot_RainfallSensor_I2C(bus); - status = gravity->begin(); + gravity = new DFRobot_RainfallSensor_I2C(bus); + status = gravity->begin(); - LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity->vid, gravity->pid, gravity->getFirmwareVersion().c_str()); + LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity->vid, gravity->pid, gravity->getFirmwareVersion().c_str()); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement) { - if (!gravity) { - LOG_ERROR("DFRobotGravitySensor not initialized"); - return false; - } +bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (!gravity) { + LOG_ERROR("DFRobotGravitySensor not initialized"); + return false; + } - measurement->variant.environment_metrics.has_rainfall_1h = true; - measurement->variant.environment_metrics.has_rainfall_24h = true; + measurement->variant.environment_metrics.has_rainfall_1h = true; + measurement->variant.environment_metrics.has_rainfall_24h = true; - measurement->variant.environment_metrics.rainfall_1h = gravity->getRainfall(1); - measurement->variant.environment_metrics.rainfall_24h = gravity->getRainfall(24); + measurement->variant.environment_metrics.rainfall_1h = gravity->getRainfall(1); + measurement->variant.environment_metrics.rainfall_24h = gravity->getRainfall(24); - LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h); - LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h); - return true; + LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h); + LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h); + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h index ceada9cfc..2b4890e30 100644 --- a/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h +++ b/src/modules/Telemetry/Sensor/DFRobotGravitySensor.h @@ -11,15 +11,16 @@ #include #include -class DFRobotGravitySensor : public TelemetrySensor { -private: - DFRobot_RainfallSensor_I2C *gravity = nullptr; +class DFRobotGravitySensor : public TelemetrySensor +{ + private: + DFRobot_RainfallSensor_I2C *gravity = nullptr; -public: - DFRobotGravitySensor(); - ~DFRobotGravitySensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + DFRobotGravitySensor(); + ~DFRobotGravitySensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp index 8af35e656..2c2aeed6d 100644 --- a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp +++ b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp @@ -11,42 +11,44 @@ DFRobotLarkSensor::DFRobotLarkSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_LARK, "DFROBOT_LARK") {} -bool DFRobotLarkSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - lark = DFRobot_LarkWeatherStation_I2C(dev->address.address, bus); +bool DFRobotLarkSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + lark = DFRobot_LarkWeatherStation_I2C(dev->address.address, bus); - if (lark.begin() == 0) // DFRobotLarkSensor init - { - LOG_DEBUG("DFRobotLarkSensor Init Succeed"); - status = true; - } else { - LOG_ERROR("DFRobotLarkSensor Init Failed"); - status = false; - } - initI2CSensor(); - return status; + if (lark.begin() == 0) // DFRobotLarkSensor init + { + LOG_DEBUG("DFRobotLarkSensor Init Succeed"); + status = true; + } else { + LOG_ERROR("DFRobotLarkSensor Init Failed"); + status = false; + } + initI2CSensor(); + return status; } -bool DFRobotLarkSensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.has_wind_speed = true; - measurement->variant.environment_metrics.has_wind_direction = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool DFRobotLarkSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_wind_speed = true; + measurement->variant.environment_metrics.has_wind_direction = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.temperature = lark.getValue("Temp").toFloat(); - measurement->variant.environment_metrics.relative_humidity = lark.getValue("Humi").toFloat(); - measurement->variant.environment_metrics.wind_speed = lark.getValue("Speed").toFloat(); - measurement->variant.environment_metrics.wind_direction = GeoCoord::bearingToDegrees(lark.getValue("Dir").c_str()); - measurement->variant.environment_metrics.barometric_pressure = lark.getValue("Pressure").toFloat(); + measurement->variant.environment_metrics.temperature = lark.getValue("Temp").toFloat(); + measurement->variant.environment_metrics.relative_humidity = lark.getValue("Humi").toFloat(); + measurement->variant.environment_metrics.wind_speed = lark.getValue("Speed").toFloat(); + measurement->variant.environment_metrics.wind_direction = GeoCoord::bearingToDegrees(lark.getValue("Dir").c_str()); + measurement->variant.environment_metrics.barometric_pressure = lark.getValue("Pressure").toFloat(); - LOG_INFO("Temperature: %f", measurement->variant.environment_metrics.temperature); - LOG_INFO("Humidity: %f", measurement->variant.environment_metrics.relative_humidity); - LOG_INFO("Wind Speed: %f", measurement->variant.environment_metrics.wind_speed); - LOG_INFO("Wind Direction: %d", measurement->variant.environment_metrics.wind_direction); - LOG_INFO("Barometric Pressure: %f", measurement->variant.environment_metrics.barometric_pressure); + LOG_INFO("Temperature: %f", measurement->variant.environment_metrics.temperature); + LOG_INFO("Humidity: %f", measurement->variant.environment_metrics.relative_humidity); + LOG_INFO("Wind Speed: %f", measurement->variant.environment_metrics.wind_speed); + LOG_INFO("Wind Direction: %d", measurement->variant.environment_metrics.wind_direction); + LOG_INFO("Barometric Pressure: %f", measurement->variant.environment_metrics.barometric_pressure); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h index c1c1611a1..f3e4661a1 100644 --- a/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h +++ b/src/modules/Telemetry/Sensor/DFRobotLarkSensor.h @@ -11,14 +11,15 @@ #include #include -class DFRobotLarkSensor : public TelemetrySensor { -private: - DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C(); +class DFRobotLarkSensor : public TelemetrySensor +{ + private: + DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C(); -public: - DFRobotLarkSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + DFRobotLarkSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/DPS310Sensor.cpp b/src/modules/Telemetry/Sensor/DPS310Sensor.cpp index 3ef67d794..19e54aa4b 100644 --- a/src/modules/Telemetry/Sensor/DPS310Sensor.cpp +++ b/src/modules/Telemetry/Sensor/DPS310Sensor.cpp @@ -9,34 +9,36 @@ DPS310Sensor::DPS310Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DPS310, "DPS310") {} -bool DPS310Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = dps310.begin_I2C(dev->address.address, bus); - if (!status) { +bool DPS310Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = dps310.begin_I2C(dev->address.address, bus); + if (!status) { + return status; + } + + dps310.configurePressure(DPS310_1HZ, DPS310_4SAMPLES); + dps310.configureTemperature(DPS310_1HZ, DPS310_4SAMPLES); + dps310.setMode(DPS310_CONT_PRESTEMP); + + initI2CSensor(); return status; - } - - dps310.configurePressure(DPS310_1HZ, DPS310_4SAMPLES); - dps310.configureTemperature(DPS310_1HZ, DPS310_4SAMPLES); - dps310.setMode(DPS310_CONT_PRESTEMP); - - initI2CSensor(); - return status; } -bool DPS310Sensor::getMetrics(meshtastic_Telemetry *measurement) { - sensors_event_t temp, press; +bool DPS310Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + sensors_event_t temp, press; - if (!dps310.getEvents(&temp, &press)) { - LOG_DEBUG("DPS310 getEvents no data"); - return false; - } + if (!dps310.getEvents(&temp, &press)) { + LOG_DEBUG("DPS310 getEvents no data"); + return false; + } - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.barometric_pressure = press.pressure; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.barometric_pressure = press.pressure; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/DPS310Sensor.h b/src/modules/Telemetry/Sensor/DPS310Sensor.h index a62393011..4de8b2d1a 100644 --- a/src/modules/Telemetry/Sensor/DPS310Sensor.h +++ b/src/modules/Telemetry/Sensor/DPS310Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class DPS310Sensor : public TelemetrySensor { -private: - Adafruit_DPS310 dps310; +class DPS310Sensor : public TelemetrySensor +{ + private: + Adafruit_DPS310 dps310; -public: - DPS310Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + DPS310Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA219Sensor.cpp b/src/modules/Telemetry/Sensor/INA219Sensor.cpp index 12e2e9d88..d94afbc7c 100644 --- a/src/modules/Telemetry/Sensor/INA219Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA219Sensor.cpp @@ -13,33 +13,41 @@ INA219Sensor::INA219Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA219, "INA219") {} -int32_t INA219Sensor::runOnce() { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - if (!ina219.success()) { - ina219 = Adafruit_INA219(nodeTelemetrySensorsMap[sensorType].first); - status = ina219.begin(nodeTelemetrySensorsMap[sensorType].second); - } else { - status = ina219.success(); - } - return initI2CSensor(); +int32_t INA219Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + if (!ina219.success()) { + ina219 = Adafruit_INA219(nodeTelemetrySensorsMap[sensorType].first); + status = ina219.begin(nodeTelemetrySensorsMap[sensorType].second); + } else { + status = ina219.success(); + } + return initI2CSensor(); } void INA219Sensor::setup() {} -bool INA219Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; +bool INA219Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - measurement->variant.environment_metrics.voltage = ina219.getBusVoltage_V(); - measurement->variant.environment_metrics.current = ina219.getCurrent_mA() * INA219_MULTIPLIER; - return true; + measurement->variant.environment_metrics.voltage = ina219.getBusVoltage_V(); + measurement->variant.environment_metrics.current = ina219.getCurrent_mA() * INA219_MULTIPLIER; + return true; } -uint16_t INA219Sensor::getBusVoltageMv() { return lround(ina219.getBusVoltage_V() * 1000); } +uint16_t INA219Sensor::getBusVoltageMv() +{ + return lround(ina219.getBusVoltage_V() * 1000); +} -int16_t INA219Sensor::getCurrentMa() { return lround(ina219.getCurrent_mA()); } +int16_t INA219Sensor::getCurrentMa() +{ + return lround(ina219.getCurrent_mA()); +} #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA219Sensor.h b/src/modules/Telemetry/Sensor/INA219Sensor.h index c35a9a5ab..908366ce6 100644 --- a/src/modules/Telemetry/Sensor/INA219Sensor.h +++ b/src/modules/Telemetry/Sensor/INA219Sensor.h @@ -8,19 +8,20 @@ #include "VoltageSensor.h" #include -class INA219Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { -private: - Adafruit_INA219 ina219; +class INA219Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor +{ + private: + Adafruit_INA219 ina219; -protected: - virtual void setup() override; + protected: + virtual void setup() override; -public: - INA219Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; + public: + INA219Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.cpp b/src/modules/Telemetry/Sensor/INA226Sensor.cpp index 9cd74d921..6fa35598f 100644 --- a/src/modules/Telemetry/Sensor/INA226Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA226Sensor.cpp @@ -9,65 +9,76 @@ INA226Sensor::INA226Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA226, "INA226") {} -int32_t INA226Sensor::runOnce() { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t INA226Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first); + begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first); - if (!status) { - status = ina226.begin(); - } - return initI2CSensor(); + if (!status) { + status = ina226.begin(); + } + return initI2CSensor(); } void INA226Sensor::setup() {} -void INA226Sensor::begin(TwoWire *wire, uint8_t addr) { - _wire = wire; - _addr = addr; - ina226 = INA226(_addr, _wire); - _wire->begin(); - ina226.setMaxCurrentShunt(0.8, 0.100); +void INA226Sensor::begin(TwoWire *wire, uint8_t addr) +{ + _wire = wire; + _addr = addr; + ina226 = INA226(_addr, _wire); + _wire->begin(); + ina226.setMaxCurrentShunt(0.8, 0.100); } -bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement) { - switch (measurement->which_variant) { - case meshtastic_Telemetry_environment_metrics_tag: - return getEnvironmentMetrics(measurement); +bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + switch (measurement->which_variant) { + case meshtastic_Telemetry_environment_metrics_tag: + return getEnvironmentMetrics(measurement); - case meshtastic_Telemetry_power_metrics_tag: - return getPowerMetrics(measurement); - } + case meshtastic_Telemetry_power_metrics_tag: + return getPowerMetrics(measurement); + } - // unsupported metric - return false; + // unsupported metric + return false; } -bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; +bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - measurement->variant.environment_metrics.voltage = ina226.getBusVoltage(); - measurement->variant.environment_metrics.current = ina226.getCurrent_mA(); + measurement->variant.environment_metrics.voltage = ina226.getBusVoltage(); + measurement->variant.environment_metrics.current = ina226.getCurrent_mA(); - return true; + return true; } -bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.power_metrics.has_ch1_voltage = true; - measurement->variant.power_metrics.has_ch1_current = true; +bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.power_metrics.has_ch1_voltage = true; + measurement->variant.power_metrics.has_ch1_current = true; - measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage(); - measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA(); + measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage(); + measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA(); - return true; + return true; } -uint16_t INA226Sensor::getBusVoltageMv() { return lround(ina226.getBusVoltage() * 1000); } +uint16_t INA226Sensor::getBusVoltageMv() +{ + return lround(ina226.getBusVoltage() * 1000); +} -int16_t INA226Sensor::getCurrentMa() { return lround(ina226.getCurrent_mA()); } +int16_t INA226Sensor::getCurrentMa() +{ + return lround(ina226.getCurrent_mA()); +} #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA226Sensor.h b/src/modules/Telemetry/Sensor/INA226Sensor.h index 91972ba6f..51435550e 100644 --- a/src/modules/Telemetry/Sensor/INA226Sensor.h +++ b/src/modules/Telemetry/Sensor/INA226Sensor.h @@ -8,25 +8,26 @@ #include "VoltageSensor.h" #include -class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { -private: - uint8_t _addr = INA_ADDR; - TwoWire *_wire = &Wire; - INA226 ina226 = INA226(_addr, _wire); +class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor +{ + private: + uint8_t _addr = INA_ADDR; + TwoWire *_wire = &Wire; + INA226 ina226 = INA226(_addr, _wire); - bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); - bool getPowerMetrics(meshtastic_Telemetry *measurement); + bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); + bool getPowerMetrics(meshtastic_Telemetry *measurement); -protected: - virtual void setup() override; - void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR); + protected: + virtual void setup() override; + void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR); -public: - INA226Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; + public: + INA226Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA260Sensor.cpp b/src/modules/Telemetry/Sensor/INA260Sensor.cpp index f3b836799..9d9a99c00 100644 --- a/src/modules/Telemetry/Sensor/INA260Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA260Sensor.cpp @@ -9,30 +9,35 @@ INA260Sensor::INA260Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA260, "INA260") {} -int32_t INA260Sensor::runOnce() { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t INA260Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - if (!status) { - status = ina260.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); - } - return initI2CSensor(); + if (!status) { + status = ina260.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + } + return initI2CSensor(); } void INA260Sensor::setup() {} -bool INA260Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; +bool INA260Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - // mV conversion to V - measurement->variant.environment_metrics.voltage = ina260.readBusVoltage() / 1000; - measurement->variant.environment_metrics.current = ina260.readCurrent(); - return true; + // mV conversion to V + measurement->variant.environment_metrics.voltage = ina260.readBusVoltage() / 1000; + measurement->variant.environment_metrics.current = ina260.readCurrent(); + return true; } -uint16_t INA260Sensor::getBusVoltageMv() { return lround(ina260.readBusVoltage()); } +uint16_t INA260Sensor::getBusVoltageMv() +{ + return lround(ina260.readBusVoltage()); +} #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA260Sensor.h b/src/modules/Telemetry/Sensor/INA260Sensor.h index f1741b08e..ea71c24e0 100644 --- a/src/modules/Telemetry/Sensor/INA260Sensor.h +++ b/src/modules/Telemetry/Sensor/INA260Sensor.h @@ -7,18 +7,19 @@ #include "VoltageSensor.h" #include -class INA260Sensor : public TelemetrySensor, VoltageSensor { -private: - Adafruit_INA260 ina260 = Adafruit_INA260(); +class INA260Sensor : public TelemetrySensor, VoltageSensor +{ + private: + Adafruit_INA260 ina260 = Adafruit_INA260(); -protected: - virtual void setup() override; + protected: + virtual void setup() override; -public: - INA260Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; + public: + INA260Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp index dca62dce2..78081132a 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.cpp +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.cpp @@ -9,90 +9,102 @@ INA3221Sensor::INA3221Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA3221, "INA3221"){}; -int32_t INA3221Sensor::runOnce() { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - if (!status) { - ina3221.begin(nodeTelemetrySensorsMap[sensorType].second); - ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors - status = true; - } else { - status = true; - } - return initI2CSensor(); +int32_t INA3221Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + if (!status) { + ina3221.begin(nodeTelemetrySensorsMap[sensorType].second); + ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors + status = true; + } else { + status = true; + } + return initI2CSensor(); }; void INA3221Sensor::setup() {} -struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch) { - struct _INA3221Measurement measurement; +struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch) +{ + struct _INA3221Measurement measurement; - measurement.voltage = ina3221.getVoltage(ch); - measurement.current = ina3221.getCurrent(ch); + measurement.voltage = ina3221.getVoltage(ch); + measurement.current = ina3221.getCurrent(ch); - return measurement; + return measurement; } -struct _INA3221Measurements INA3221Sensor::getMeasurements() { - struct _INA3221Measurements measurements; +struct _INA3221Measurements INA3221Sensor::getMeasurements() +{ + struct _INA3221Measurements measurements; - // INA3221 has 3 channels starting from 0 - for (int i = 0; i < 3; i++) { - measurements.measurements[i] = getMeasurement((ina3221_ch_t)i); - } + // INA3221 has 3 channels starting from 0 + for (int i = 0; i < 3; i++) { + measurements.measurements[i] = getMeasurement((ina3221_ch_t)i); + } - return measurements; + return measurements; } -bool INA3221Sensor::getMetrics(meshtastic_Telemetry *measurement) { - switch (measurement->which_variant) { - case meshtastic_Telemetry_environment_metrics_tag: - return getEnvironmentMetrics(measurement); +bool INA3221Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + switch (measurement->which_variant) { + case meshtastic_Telemetry_environment_metrics_tag: + return getEnvironmentMetrics(measurement); - case meshtastic_Telemetry_power_metrics_tag: - return getPowerMetrics(measurement); - } + case meshtastic_Telemetry_power_metrics_tag: + return getPowerMetrics(measurement); + } - // unsupported metric - return false; + // unsupported metric + return false; } -bool INA3221Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) { - struct _INA3221Measurement m = getMeasurement(ENV_CH); +bool INA3221Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) +{ + struct _INA3221Measurement m = getMeasurement(ENV_CH); - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; - measurement->variant.environment_metrics.voltage = m.voltage; - measurement->variant.environment_metrics.current = m.current; + measurement->variant.environment_metrics.voltage = m.voltage; + measurement->variant.environment_metrics.current = m.current; - return true; + return true; } -bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) { - struct _INA3221Measurements m = getMeasurements(); +bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) +{ + struct _INA3221Measurements m = getMeasurements(); - measurement->variant.power_metrics.has_ch1_voltage = true; - measurement->variant.power_metrics.has_ch1_current = true; - measurement->variant.power_metrics.has_ch2_voltage = true; - measurement->variant.power_metrics.has_ch2_current = true; - measurement->variant.power_metrics.has_ch3_voltage = true; - measurement->variant.power_metrics.has_ch3_current = true; + measurement->variant.power_metrics.has_ch1_voltage = true; + measurement->variant.power_metrics.has_ch1_current = true; + measurement->variant.power_metrics.has_ch2_voltage = true; + measurement->variant.power_metrics.has_ch2_current = true; + measurement->variant.power_metrics.has_ch3_voltage = true; + measurement->variant.power_metrics.has_ch3_current = true; - measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage; - measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current; - measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage; - measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current; - measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage; - measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current; + measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage; + measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current; + measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage; + measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current; + measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage; + measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current; - return true; + return true; } -uint16_t INA3221Sensor::getBusVoltageMv() { return lround(ina3221.getVoltage(BAT_CH) * 1000); } +uint16_t INA3221Sensor::getBusVoltageMv() +{ + return lround(ina3221.getVoltage(BAT_CH) * 1000); +} -int16_t INA3221Sensor::getCurrentMa() { return lround(ina3221.getCurrent(BAT_CH)); } +int16_t INA3221Sensor::getCurrentMa() +{ + return lround(ina3221.getCurrent(BAT_CH)); +} #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.h b/src/modules/Telemetry/Sensor/INA3221Sensor.h index 5afc6c0dd..0581f92f6 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.h +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.h @@ -16,44 +16,45 @@ #define INA3221_BAT_CH INA3221_CH1 #endif -class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { -private: - INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); +class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor +{ + private: + INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); - // channel to report voltage/current for environment metrics - static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; + // channel to report voltage/current for environment metrics + static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; - // channel to report battery voltage for device_battery_ina_address - static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; + // channel to report battery voltage for device_battery_ina_address + static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; - // get a single measurement for a channel - struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); + // get a single measurement for a channel + struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); - // get all measurements for all channels - struct _INA3221Measurements getMeasurements(); + // get all measurements for all channels + struct _INA3221Measurements getMeasurements(); - bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); - bool getPowerMetrics(meshtastic_Telemetry *measurement); + bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); + bool getPowerMetrics(meshtastic_Telemetry *measurement); -protected: - void setup() override; + protected: + void setup() override; -public: - INA3221Sensor(); - int32_t runOnce() override; - bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; + public: + INA3221Sensor(); + int32_t runOnce() override; + bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; struct _INA3221Measurement { - float voltage; - float current; + float voltage; + float current; }; struct _INA3221Measurements { - // INA3221 has 3 channels - struct _INA3221Measurement measurements[3]; + // INA3221 has 3 channels + struct _INA3221Measurement measurements[3]; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/IndicatorSensor.cpp b/src/modules/Telemetry/Sensor/IndicatorSensor.cpp index f6896bcb6..26a4bc5fc 100644 --- a/src/modules/Telemetry/Sensor/IndicatorSensor.cpp +++ b/src/modules/Telemetry/Sensor/IndicatorSensor.cpp @@ -19,144 +19,148 @@ uint8_t data[SENSOR_BUF_SIZE]; // decode #define ACK_PKT_PARA "ACK" enum sensor_pkt_type { - PKT_TYPE_ACK = 0x00, // uin32_t - PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t - PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time - PKT_TYPE_CMD_BEEP_OFF = 0xA2, - PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t - PKT_TYPE_CMD_POWER_ON = 0xA4, - PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float - PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float - PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float - PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float - PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float - PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float + PKT_TYPE_ACK = 0x00, // uin32_t + PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t + PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time + PKT_TYPE_CMD_BEEP_OFF = 0xA2, + PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t + PKT_TYPE_CMD_POWER_ON = 0xA4, + PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float + PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float + PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float + PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float + PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float + PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float }; -static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len) { - uint8_t send_buf[32] = {0}; - uint8_t send_data[32] = {0}; +static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len) +{ + uint8_t send_buf[32] = {0}; + uint8_t send_data[32] = {0}; - if (len > 31) { - return -1; - } - - uint8_t index = 1; - - send_data[0] = cmd; - - if (len > 0 && p_data != NULL) { - memcpy(&send_data[1], p_data, len); - index += len; - } - cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index); - - // LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd); - - if (ret.status == COBS_ENCODE_OK) { - return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1); - } - - return -1; -} - -bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("%s: init", sensorName); - setup(); - return true; -} - -void IndicatorSensor::setup() { - uart_config_t uart_config = { - .baud_rate = SENSOR_BAUD_RATE, - .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, - .stop_bits = UART_STOP_BITS_1, - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .source_clk = UART_SCLK_APB, - }; - int intr_alloc_flags = 0; - char buffer[11]; - - uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags); - uart_param_config(SENSOR_PORT_NUM, &uart_config); - uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0); - // measure and send only once every minute, for the phone API - const char *interval = ultoa(60000, buffer, 10); - cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1); -} - -bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement) { - cobs_decode_result ret; - int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS); - - float value = 0.0; - uint8_t *p_buf_start = buf; - uint8_t *p_buf_end = buf; - if (len > 0) { - while (p_buf_start < (buf + len)) { - p_buf_end = p_buf_start; - while (p_buf_end < (buf + len)) { - if (*p_buf_end == 0x00) { - break; - } - p_buf_end++; - } - // decode buf - memset(data, 0, sizeof(data)); - ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start); - - // LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]); - - if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) { - - value = 0.0; - uint8_t pkt_type = data[0]; - switch (pkt_type) { - case PKT_TYPE_SENSOR_SCD41_CO2: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("CO2: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - break; - } - - case PKT_TYPE_SENSOR_AHT20_TEMP: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Temp: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = value; - break; - } - - case PKT_TYPE_SENSOR_AHT20_HUMIDITY: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Humidity: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.relative_humidity = value; - break; - } - - case PKT_TYPE_SENSOR_TVOC_INDEX: { - memcpy(&value, &data[1], sizeof(value)); - // LOG_DEBUG("Tvoc: %.1f", value); - cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); - measurement->variant.environment_metrics.has_iaq = true; - measurement->variant.environment_metrics.iaq = value; - break; - } - default: - break; - } - } - - p_buf_start = p_buf_end + 1; // next message + if (len > 31) { + return -1; } + + uint8_t index = 1; + + send_data[0] = cmd; + + if (len > 0 && p_data != NULL) { + memcpy(&send_data[1], p_data, len); + index += len; + } + cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index); + + // LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd); + + if (ret.status == COBS_ENCODE_OK) { + return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1); + } + + return -1; +} + +bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("%s: init", sensorName); + setup(); return true; - } - return false; +} + +void IndicatorSensor::setup() +{ + uart_config_t uart_config = { + .baud_rate = SENSOR_BAUD_RATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_APB, + }; + int intr_alloc_flags = 0; + char buffer[11]; + + uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags); + uart_param_config(SENSOR_PORT_NUM, &uart_config); + uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0); + // measure and send only once every minute, for the phone API + const char *interval = ultoa(60000, buffer, 10); + cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1); +} + +bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + cobs_decode_result ret; + int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS); + + float value = 0.0; + uint8_t *p_buf_start = buf; + uint8_t *p_buf_end = buf; + if (len > 0) { + while (p_buf_start < (buf + len)) { + p_buf_end = p_buf_start; + while (p_buf_end < (buf + len)) { + if (*p_buf_end == 0x00) { + break; + } + p_buf_end++; + } + // decode buf + memset(data, 0, sizeof(data)); + ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start); + + // LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]); + + if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) { + + value = 0.0; + uint8_t pkt_type = data[0]; + switch (pkt_type) { + case PKT_TYPE_SENSOR_SCD41_CO2: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("CO2: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + break; + } + + case PKT_TYPE_SENSOR_AHT20_TEMP: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("Temp: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = value; + break; + } + + case PKT_TYPE_SENSOR_AHT20_HUMIDITY: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("Humidity: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.relative_humidity = value; + break; + } + + case PKT_TYPE_SENSOR_TVOC_INDEX: { + memcpy(&value, &data[1], sizeof(value)); + // LOG_DEBUG("Tvoc: %.1f", value); + cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); + measurement->variant.environment_metrics.has_iaq = true; + measurement->variant.environment_metrics.iaq = value; + break; + } + default: + break; + } + } + + p_buf_start = p_buf_end + 1; // next message + } + return true; + } + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/IndicatorSensor.h b/src/modules/Telemetry/Sensor/IndicatorSensor.h index 2d29715ce..22a0d9c83 100644 --- a/src/modules/Telemetry/Sensor/IndicatorSensor.h +++ b/src/modules/Telemetry/Sensor/IndicatorSensor.h @@ -5,14 +5,15 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -class IndicatorSensor : public TelemetrySensor { -public: - IndicatorSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; +class IndicatorSensor : public TelemetrySensor +{ + public: + IndicatorSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -private: - void setup(); + private: + void setup(); }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp b/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp index 23a46f964..4ed78dcb0 100644 --- a/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp +++ b/src/modules/Telemetry/Sensor/LPS22HBSensor.cpp @@ -10,30 +10,32 @@ LPS22HBSensor::LPS22HBSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LPS22, "LPS22HB") {} -bool LPS22HBSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = lps22hb.begin_I2C(dev->address.address, bus); - if (!status) { - return status; - } - lps22hb.setDataRate(LPS22_RATE_10_HZ); +bool LPS22HBSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = lps22hb.begin_I2C(dev->address.address, bus); + if (!status) { + return status; + } + lps22hb.setDataRate(LPS22_RATE_10_HZ); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool LPS22HBSensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_barometric_pressure = true; +bool LPS22HBSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; - sensors_event_t temp; - sensors_event_t pressure; - lps22hb.getEvent(&pressure, &temp); + sensors_event_t temp; + sensors_event_t pressure; + lps22hb.getEvent(&pressure, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.barometric_pressure = pressure.pressure; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.barometric_pressure = pressure.pressure; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LPS22HBSensor.h b/src/modules/Telemetry/Sensor/LPS22HBSensor.h index 043c10068..90b006fa2 100644 --- a/src/modules/Telemetry/Sensor/LPS22HBSensor.h +++ b/src/modules/Telemetry/Sensor/LPS22HBSensor.h @@ -7,14 +7,15 @@ #include #include -class LPS22HBSensor : public TelemetrySensor { -private: - Adafruit_LPS22 lps22hb; +class LPS22HBSensor : public TelemetrySensor +{ + private: + Adafruit_LPS22 lps22hb; -public: - LPS22HBSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + LPS22HBSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp b/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp index 143b9b4d4..cb7290fee 100644 --- a/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp +++ b/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp @@ -9,60 +9,65 @@ LTR390UVSensor::LTR390UVSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LTR390UV, "LTR390UV") {} -bool LTR390UVSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool LTR390UVSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - status = ltr390uv.begin(bus); - if (!status) { + status = ltr390uv.begin(bus); + if (!status) { + return status; + } + + ltr390uv.setMode(LTR390_MODE_UVS); + ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default + ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default + + initI2CSensor(); return status; - } - - ltr390uv.setMode(LTR390_MODE_UVS); - ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default - ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default - - initI2CSensor(); - return status; } -bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement) { - LOG_DEBUG("LTR390UV getMetrics"); +bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("LTR390UV getMetrics"); - // Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes. - if (ltr390uv.newDataAvailable()) { - measurement->variant.environment_metrics.has_lux = true; - measurement->variant.environment_metrics.has_uv_lux = true; + // Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes. + if (ltr390uv.newDataAvailable()) { + measurement->variant.environment_metrics.has_lux = true; + measurement->variant.environment_metrics.has_uv_lux = true; - if (ltr390uv.getMode() == LTR390_MODE_ALS) { - lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution - LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading); + if (ltr390uv.getMode() == LTR390_MODE_ALS) { + lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution + LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading); - measurement->variant.environment_metrics.lux = lastLuxReading; - measurement->variant.environment_metrics.uv_lux = lastUVReading; + measurement->variant.environment_metrics.lux = lastLuxReading; + measurement->variant.environment_metrics.uv_lux = lastUVReading; - ltr390uv.setGain(LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain - ltr390uv.setMode(LTR390_MODE_UVS); + ltr390uv.setGain( + LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain + ltr390uv.setMode(LTR390_MODE_UVS); - return true; + return true; - } else if (ltr390uv.getMode() == LTR390_MODE_UVS) { - lastUVReading = ltr390uv.readUVS() / 2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution - LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading); + } else if (ltr390uv.getMode() == LTR390_MODE_UVS) { + lastUVReading = ltr390uv.readUVS() / + 2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution + LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading); - measurement->variant.environment_metrics.lux = lastLuxReading; - measurement->variant.environment_metrics.uv_lux = lastUVReading; + measurement->variant.environment_metrics.lux = lastLuxReading; + measurement->variant.environment_metrics.uv_lux = lastUVReading; - ltr390uv.setGain(LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it - ltr390uv.setMode(LTR390_MODE_ALS); + ltr390uv.setGain( + LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it + ltr390uv.setMode(LTR390_MODE_ALS); - return true; + return true; + } } - } - // In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false - measurement->variant.environment_metrics.has_lux = false; - measurement->variant.environment_metrics.has_uv_lux = false; + // In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false + measurement->variant.environment_metrics.has_lux = false; + measurement->variant.environment_metrics.has_uv_lux = false; - return false; + return false; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LTR390UVSensor.h b/src/modules/Telemetry/Sensor/LTR390UVSensor.h index a93eaf7d4..e12d17274 100644 --- a/src/modules/Telemetry/Sensor/LTR390UVSensor.h +++ b/src/modules/Telemetry/Sensor/LTR390UVSensor.h @@ -6,16 +6,17 @@ #include "TelemetrySensor.h" #include -class LTR390UVSensor : public TelemetrySensor { -private: - Adafruit_LTR390 ltr390uv = Adafruit_LTR390(); - float lastLuxReading = 0; - float lastUVReading = 0; +class LTR390UVSensor : public TelemetrySensor +{ + private: + Adafruit_LTR390 ltr390uv = Adafruit_LTR390(); + float lastLuxReading = 0; + float lastUVReading = 0; -public: - LTR390UVSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + LTR390UVSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp index da2bc2569..1a6792d3a 100644 --- a/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.cpp @@ -2,11 +2,12 @@ #if !MESHTASTIC_EXCLUDE_I2C && __has_include() -MAX17048Singleton *MAX17048Singleton::GetInstance() { - if (pinstance == nullptr) { - pinstance = new MAX17048Singleton(); - } - return pinstance; +MAX17048Singleton *MAX17048Singleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new MAX17048Singleton(); + } + return pinstance; } MAX17048Singleton::MAX17048Singleton() {} @@ -15,147 +16,160 @@ MAX17048Singleton::~MAX17048Singleton() {} MAX17048Singleton *MAX17048Singleton::pinstance{nullptr}; -bool MAX17048Singleton::runOnce(TwoWire *theWire) { - initialized = begin(theWire); - LOG_DEBUG("%s::runOnce %s", sensorStr, initialized ? "began ok" : "begin failed"); - return initialized; +bool MAX17048Singleton::runOnce(TwoWire *theWire) +{ + initialized = begin(theWire); + LOG_DEBUG("%s::runOnce %s", sensorStr, initialized ? "began ok" : "begin failed"); + return initialized; } -bool MAX17048Singleton::isBatteryCharging() { - float volts = cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("%s::isBatteryCharging not connected", sensorStr); - return 0; - } +bool MAX17048Singleton::isBatteryCharging() +{ + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::isBatteryCharging not connected", sensorStr); + return 0; + } - MAX17048ChargeSample sample; - sample.chargeRate = chargeRate(); // charge/discharge rate in percent/hr - sample.cellPercent = cellPercent(); // state of charge in percent 0 to 100 - chargeSamples.push(sample); // save a sample into a fifo buffer + MAX17048ChargeSample sample; + sample.chargeRate = chargeRate(); // charge/discharge rate in percent/hr + sample.cellPercent = cellPercent(); // state of charge in percent 0 to 100 + chargeSamples.push(sample); // save a sample into a fifo buffer - // Keep the fifo buffer trimmed - while (chargeSamples.size() > MAX17048_CHARGING_SAMPLES) - chargeSamples.pop(); + // Keep the fifo buffer trimmed + while (chargeSamples.size() > MAX17048_CHARGING_SAMPLES) + chargeSamples.pop(); - // Based on the past n samples, is the lipo charging, discharging or idle - if (chargeSamples.front().chargeRate > MAX17048_CHARGING_MINIMUM_RATE && chargeSamples.back().chargeRate > MAX17048_CHARGING_MINIMUM_RATE) { - if (chargeSamples.front().cellPercent > chargeSamples.back().cellPercent) - chargeState = MAX17048ChargeState::EXPORT; - else if (chargeSamples.front().cellPercent < chargeSamples.back().cellPercent) - chargeState = MAX17048ChargeState::IMPORT; - else - chargeState = MAX17048ChargeState::IDLE; - } else { - chargeState = MAX17048ChargeState::IDLE; - } + // Based on the past n samples, is the lipo charging, discharging or idle + if (chargeSamples.front().chargeRate > MAX17048_CHARGING_MINIMUM_RATE && + chargeSamples.back().chargeRate > MAX17048_CHARGING_MINIMUM_RATE) { + if (chargeSamples.front().cellPercent > chargeSamples.back().cellPercent) + chargeState = MAX17048ChargeState::EXPORT; + else if (chargeSamples.front().cellPercent < chargeSamples.back().cellPercent) + chargeState = MAX17048ChargeState::IMPORT; + else + chargeState = MAX17048ChargeState::IDLE; + } else { + chargeState = MAX17048ChargeState::IDLE; + } - LOG_DEBUG("%s::isBatteryCharging %s volts: %.3f soc: %.3f rate: %.3f", sensorStr, chargeLabels[chargeState], volts, sample.cellPercent, - sample.chargeRate); - return chargeState == MAX17048ChargeState::IMPORT; + LOG_DEBUG("%s::isBatteryCharging %s volts: %.3f soc: %.3f rate: %.3f", sensorStr, chargeLabels[chargeState], volts, + sample.cellPercent, sample.chargeRate); + return chargeState == MAX17048ChargeState::IMPORT; } -uint16_t MAX17048Singleton::getBusVoltageMv() { - float volts = cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("%s::getBusVoltageMv is not connected", sensorStr); - return 0; - } - LOG_DEBUG("%s::getBusVoltageMv %.3fmV", sensorStr, volts); - return (uint16_t)(volts * 1000.0f); +uint16_t MAX17048Singleton::getBusVoltageMv() +{ + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::getBusVoltageMv is not connected", sensorStr); + return 0; + } + LOG_DEBUG("%s::getBusVoltageMv %.3fmV", sensorStr, volts); + return (uint16_t)(volts * 1000.0f); } -uint8_t MAX17048Singleton::getBusBatteryPercent() { - float soc = cellPercent(); - LOG_DEBUG("%s::getBusBatteryPercent %.1f%%", sensorStr, soc); - return clamp(static_cast(round(soc)), static_cast(0), static_cast(100)); +uint8_t MAX17048Singleton::getBusBatteryPercent() +{ + float soc = cellPercent(); + LOG_DEBUG("%s::getBusBatteryPercent %.1f%%", sensorStr, soc); + return clamp(static_cast(round(soc)), static_cast(0), static_cast(100)); } -uint16_t MAX17048Singleton::getTimeToGoSecs() { - float rate = chargeRate(); // charge/discharge rate in percent/hr - float soc = cellPercent(); // state of charge in percent 0 to 100 - soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% - float ttg = ((100.0f - soc) / rate) * 3600.0f; // calculate seconds to charge/discharge - LOG_DEBUG("%s::getTimeToGoSecs %.0f seconds", sensorStr, ttg); - return (uint16_t)ttg; +uint16_t MAX17048Singleton::getTimeToGoSecs() +{ + float rate = chargeRate(); // charge/discharge rate in percent/hr + float soc = cellPercent(); // state of charge in percent 0 to 100 + soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% + float ttg = ((100.0f - soc) / rate) * 3600.0f; // calculate seconds to charge/discharge + LOG_DEBUG("%s::getTimeToGoSecs %.0f seconds", sensorStr, ttg); + return (uint16_t)ttg; } -bool MAX17048Singleton::isBatteryConnected() { - float volts = cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("%s::isBatteryConnected is not connected", sensorStr); - return false; - } +bool MAX17048Singleton::isBatteryConnected() +{ + float volts = cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("%s::isBatteryConnected is not connected", sensorStr); + return false; + } - // if a valid voltage is returned, then battery must be connected - return true; -} - -bool MAX17048Singleton::isExternallyPowered() { - float volts = cellVoltage(); - if (isnan(volts)) { - // if the battery is not connected then there must be external power - LOG_DEBUG("%s::isExternallyPowered battery is", sensorStr); + // if a valid voltage is returned, then battery must be connected return true; - } - // if the bus voltage is over MAX17048_BUS_POWER_VOLTS, then the external power - // is assumed to be connected - LOG_DEBUG("%s::isExternallyPowered %s connected", sensorStr, volts >= MAX17048_BUS_POWER_VOLTS ? "is" : "is not"); - return volts >= MAX17048_BUS_POWER_VOLTS; +} + +bool MAX17048Singleton::isExternallyPowered() +{ + float volts = cellVoltage(); + if (isnan(volts)) { + // if the battery is not connected then there must be external power + LOG_DEBUG("%s::isExternallyPowered battery is", sensorStr); + return true; + } + // if the bus voltage is over MAX17048_BUS_POWER_VOLTS, then the external power + // is assumed to be connected + LOG_DEBUG("%s::isExternallyPowered %s connected", sensorStr, volts >= MAX17048_BUS_POWER_VOLTS ? "is" : "is not"); + return volts >= MAX17048_BUS_POWER_VOLTS; } #if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY)) MAX17048Sensor::MAX17048Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX17048, "MAX17048") {} -int32_t MAX17048Sensor::runOnce() { - if (isInitialized()) { - LOG_INFO("Init sensor: %s is already initialised", sensorName); - return true; - } +int32_t MAX17048Sensor::runOnce() +{ + if (isInitialized()) { + LOG_INFO("Init sensor: %s is already initialised", sensorName); + return true; + } - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - // Get a singleton instance and initialise the max17048 - if (max17048 == nullptr) { - max17048 = MAX17048Singleton::GetInstance(); - } - status = max17048->runOnce(nodeTelemetrySensorsMap[sensorType].second); - return initI2CSensor(); + // Get a singleton instance and initialise the max17048 + if (max17048 == nullptr) { + max17048 = MAX17048Singleton::GetInstance(); + } + status = max17048->runOnce(nodeTelemetrySensorsMap[sensorType].second); + return initI2CSensor(); } void MAX17048Sensor::setup() {} -bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement) { - LOG_DEBUG("MAX17048 getMetrics id: %i", measurement->which_variant); +bool MAX17048Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("MAX17048 getMetrics id: %i", measurement->which_variant); - float volts = max17048->cellVoltage(); - if (isnan(volts)) { - LOG_DEBUG("MAX17048 getMetrics battery is not connected"); - return false; - } + float volts = max17048->cellVoltage(); + if (isnan(volts)) { + LOG_DEBUG("MAX17048 getMetrics battery is not connected"); + return false; + } - float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr - float soc = max17048->cellPercent(); // state of charge in percent 0 to 100 - soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% - float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge + float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr + float soc = max17048->cellPercent(); // state of charge in percent 0 to 100 + soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% + float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge - LOG_DEBUG("MAX17048 getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours", volts, soc, ttg); - if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) { - measurement->variant.power_metrics.has_ch1_voltage = true; - measurement->variant.power_metrics.ch1_voltage = volts; - } else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) { - measurement->variant.device_metrics.has_battery_level = true; - measurement->variant.device_metrics.has_voltage = true; - measurement->variant.device_metrics.battery_level = static_cast(round(soc)); - measurement->variant.device_metrics.voltage = volts; - } - return true; + LOG_DEBUG("MAX17048 getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours", volts, soc, ttg); + if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) { + measurement->variant.power_metrics.has_ch1_voltage = true; + measurement->variant.power_metrics.ch1_voltage = volts; + } else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) { + measurement->variant.device_metrics.has_battery_level = true; + measurement->variant.device_metrics.has_voltage = true; + measurement->variant.device_metrics.battery_level = static_cast(round(soc)); + measurement->variant.device_metrics.voltage = volts; + } + return true; } -uint16_t MAX17048Sensor::getBusVoltageMv() { return max17048->getBusVoltageMv(); }; +uint16_t MAX17048Sensor::getBusVoltageMv() +{ + return max17048->getBusVoltageMv(); +}; #endif diff --git a/src/modules/Telemetry/Sensor/MAX17048Sensor.h b/src/modules/Telemetry/Sensor/MAX17048Sensor.h index 82b32c448..d27169406 100644 --- a/src/modules/Telemetry/Sensor/MAX17048Sensor.h +++ b/src/modules/Telemetry/Sensor/MAX17048Sensor.h @@ -25,81 +25,83 @@ #include struct MAX17048ChargeSample { - float cellPercent; - float chargeRate; + float cellPercent; + float chargeRate; }; enum MAX17048ChargeState { IDLE, EXPORT, IMPORT }; // Singleton wrapper for the Adafruit_MAX17048 class -class MAX17048Singleton : public Adafruit_MAX17048 { -private: - static MAX17048Singleton *pinstance; - bool initialized = false; - std::queue chargeSamples; - MAX17048ChargeState chargeState = IDLE; - const String chargeLabels[3] = {F("idle"), F("export"), F("import")}; - const char *sensorStr = "MAX17048Sensor"; +class MAX17048Singleton : public Adafruit_MAX17048 +{ + private: + static MAX17048Singleton *pinstance; + bool initialized = false; + std::queue chargeSamples; + MAX17048ChargeState chargeState = IDLE; + const String chargeLabels[3] = {F("idle"), F("export"), F("import")}; + const char *sensorStr = "MAX17048Sensor"; -protected: - MAX17048Singleton(); - ~MAX17048Singleton(); + protected: + MAX17048Singleton(); + ~MAX17048Singleton(); -public: - // Create a singleton instance (not thread safe) - static MAX17048Singleton *GetInstance(); + public: + // Create a singleton instance (not thread safe) + static MAX17048Singleton *GetInstance(); - // Singletons should not be cloneable. - MAX17048Singleton(MAX17048Singleton &other) = delete; + // Singletons should not be cloneable. + MAX17048Singleton(MAX17048Singleton &other) = delete; - // Singletons should not be assignable. - void operator=(const MAX17048Singleton &) = delete; + // Singletons should not be assignable. + void operator=(const MAX17048Singleton &) = delete; - // Initialise the sensor (not thread safe) - virtual bool runOnce(TwoWire *theWire = &Wire); + // Initialise the sensor (not thread safe) + virtual bool runOnce(TwoWire *theWire = &Wire); - // Get the current bus voltage - uint16_t getBusVoltageMv(); + // Get the current bus voltage + uint16_t getBusVoltageMv(); - // Get the state of charge in percent 0 to 100 - uint8_t getBusBatteryPercent(); + // Get the state of charge in percent 0 to 100 + uint8_t getBusBatteryPercent(); - // Calculate the seconds to charge/discharge - uint16_t getTimeToGoSecs(); + // Calculate the seconds to charge/discharge + uint16_t getTimeToGoSecs(); - // Returns true if the battery sensor has started - inline virtual bool isInitialised() { return initialized; }; + // Returns true if the battery sensor has started + inline virtual bool isInitialised() { return initialized; }; - // Returns true if the battery is currently on charge (not thread safe) - bool isBatteryCharging(); + // Returns true if the battery is currently on charge (not thread safe) + bool isBatteryCharging(); - // Returns true if a battery is actually connected - bool isBatteryConnected(); + // Returns true if a battery is actually connected + bool isBatteryConnected(); - // Returns true if there is bus or external power connected - bool isExternallyPowered(); + // Returns true if there is bus or external power connected + bool isExternallyPowered(); }; #if (HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY)) -class MAX17048Sensor : public TelemetrySensor, VoltageSensor { -private: - MAX17048Singleton *max17048 = nullptr; +class MAX17048Sensor : public TelemetrySensor, VoltageSensor +{ + private: + MAX17048Singleton *max17048 = nullptr; -protected: - virtual void setup() override; + protected: + virtual void setup() override; -public: - MAX17048Sensor(); + public: + MAX17048Sensor(); - // Initialise the sensor - virtual int32_t runOnce() override; + // Initialise the sensor + virtual int32_t runOnce() override; - // Get the current bus voltage and state of charge - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + // Get the current bus voltage and state of charge + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - // Get the current bus voltage - virtual uint16_t getBusVoltageMv() override; + // Get the current bus voltage + virtual uint16_t getBusVoltageMv() override; }; #endif diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp index 4df0418c2..ceca4be5e 100644 --- a/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.cpp @@ -9,71 +9,75 @@ MAX30102Sensor::MAX30102Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MAX30102, "MAX30102") {} -int32_t MAX30102Sensor::runOnce() { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } +int32_t MAX30102Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - if (max30102.begin(*nodeTelemetrySensorsMap[sensorType].second, _speed, nodeTelemetrySensorsMap[sensorType].first) == true) // MAX30102 init - { - byte brightness = 60; // 0=Off to 255=50mA - byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32 - byte leds = 2; // 1 = Red only, 2 = Red + IR - byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200 - int pulseWidth = 411; // 69, 118, 215, 411 - int adcRange = 4096; // 2048, 4096, 8192, 16384 + if (max30102.begin(*nodeTelemetrySensorsMap[sensorType].second, _speed, nodeTelemetrySensorsMap[sensorType].first) == + true) // MAX30102 init + { + byte brightness = 60; // 0=Off to 255=50mA + byte sampleAverage = 4; // 1, 2, 4, 8, 16, 32 + byte leds = 2; // 1 = Red only, 2 = Red + IR + byte sampleRate = 100; // 50, 100, 200, 400, 800, 1000, 1600, 3200 + int pulseWidth = 411; // 69, 118, 215, 411 + int adcRange = 4096; // 2048, 4096, 8192, 16384 - max30102.enableDIETEMPRDY(); // Enable the temperature ready interrupt - max30102.setup(brightness, sampleAverage, leds, sampleRate, pulseWidth, adcRange); - LOG_DEBUG("MAX30102 Init Succeed"); - status = true; - } else { - LOG_ERROR("MAX30102 Init Failed"); - status = false; - } - return initI2CSensor(); + max30102.enableDIETEMPRDY(); // Enable the temperature ready interrupt + max30102.setup(brightness, sampleAverage, leds, sampleRate, pulseWidth, adcRange); + LOG_DEBUG("MAX30102 Init Succeed"); + status = true; + } else { + LOG_ERROR("MAX30102 Init Failed"); + status = false; + } + return initI2CSensor(); } void MAX30102Sensor::setup() {} -bool MAX30102Sensor::getMetrics(meshtastic_Telemetry *measurement) { - uint32_t ir_buff[MAX30102_BUFFER_LEN]; - uint32_t red_buff[MAX30102_BUFFER_LEN]; - int32_t spo2; - int8_t spo2_valid; - int32_t heart_rate; - int8_t heart_rate_valid; - float temp = max30102.readTemperature(); +bool MAX30102Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + uint32_t ir_buff[MAX30102_BUFFER_LEN]; + uint32_t red_buff[MAX30102_BUFFER_LEN]; + int32_t spo2; + int8_t spo2_valid; + int32_t heart_rate; + int8_t heart_rate_valid; + float temp = max30102.readTemperature(); - measurement->variant.environment_metrics.temperature = temp; - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.health_metrics.temperature = temp; - measurement->variant.health_metrics.has_temperature = true; - for (byte i = 0; i < MAX30102_BUFFER_LEN; i++) { - while (max30102.available() == false) - max30102.check(); + measurement->variant.environment_metrics.temperature = temp; + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.health_metrics.temperature = temp; + measurement->variant.health_metrics.has_temperature = true; + for (byte i = 0; i < MAX30102_BUFFER_LEN; i++) { + while (max30102.available() == false) + max30102.check(); - red_buff[i] = max30102.getRed(); - ir_buff[i] = max30102.getIR(); - max30102.nextSample(); - } + red_buff[i] = max30102.getRed(); + ir_buff[i] = max30102.getIR(); + max30102.nextSample(); + } - maxim_heart_rate_and_oxygen_saturation(ir_buff, MAX30102_BUFFER_LEN, red_buff, &spo2, &spo2_valid, &heart_rate, &heart_rate_valid); - LOG_DEBUG("heart_rate=%d(%d), sp02=%d(%d)", heart_rate, heart_rate_valid, spo2, spo2_valid); - if (heart_rate_valid) { - measurement->variant.health_metrics.has_heart_bpm = true; - measurement->variant.health_metrics.heart_bpm = heart_rate; - } else { - measurement->variant.health_metrics.has_heart_bpm = false; - } - if (spo2_valid) { - measurement->variant.health_metrics.has_spO2 = true; - measurement->variant.health_metrics.spO2 = spo2; - } else { - measurement->variant.health_metrics.has_spO2 = true; - } - return true; + maxim_heart_rate_and_oxygen_saturation(ir_buff, MAX30102_BUFFER_LEN, red_buff, &spo2, &spo2_valid, &heart_rate, + &heart_rate_valid); + LOG_DEBUG("heart_rate=%d(%d), sp02=%d(%d)", heart_rate, heart_rate_valid, spo2, spo2_valid); + if (heart_rate_valid) { + measurement->variant.health_metrics.has_heart_bpm = true; + measurement->variant.health_metrics.heart_bpm = heart_rate; + } else { + measurement->variant.health_metrics.has_heart_bpm = false; + } + if (spo2_valid) { + measurement->variant.health_metrics.has_spO2 = true; + measurement->variant.health_metrics.spO2 = spo2; + } else { + measurement->variant.health_metrics.has_spO2 = true; + } + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MAX30102Sensor.h b/src/modules/Telemetry/Sensor/MAX30102Sensor.h index 9a6b8b5b4..9981d4006 100644 --- a/src/modules/Telemetry/Sensor/MAX30102Sensor.h +++ b/src/modules/Telemetry/Sensor/MAX30102Sensor.h @@ -8,18 +8,19 @@ #define MAX30102_BUFFER_LEN 100 -class MAX30102Sensor : public TelemetrySensor { -private: - MAX30105 max30102 = MAX30105(); - uint32_t _speed = 200000UL; +class MAX30102Sensor : public TelemetrySensor +{ + private: + MAX30105 max30102 = MAX30105(); + uint32_t _speed = 200000UL; -protected: - virtual void setup() override; + protected: + virtual void setup() override; -public: - MAX30102Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + public: + MAX30102Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp b/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp index 8fe342882..c93d6a927 100644 --- a/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MCP9808Sensor.cpp @@ -9,24 +9,26 @@ MCP9808Sensor::MCP9808Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MCP9808, "MCP9808") {} -bool MCP9808Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = mcp9808.begin(dev->address.address, bus); - if (!status) { - return status; - } - mcp9808.setResolution(2); +bool MCP9808Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = mcp9808.begin(dev->address.address, bus); + if (!status) { + return status; + } + mcp9808.setResolution(2); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; +bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; - LOG_DEBUG("MCP9808 getMetrics"); - measurement->variant.environment_metrics.temperature = mcp9808.readTempC(); - return true; + LOG_DEBUG("MCP9808 getMetrics"); + measurement->variant.environment_metrics.temperature = mcp9808.readTempC(); + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MCP9808Sensor.h b/src/modules/Telemetry/Sensor/MCP9808Sensor.h index 2e90a88e5..cef7a48c2 100644 --- a/src/modules/Telemetry/Sensor/MCP9808Sensor.h +++ b/src/modules/Telemetry/Sensor/MCP9808Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class MCP9808Sensor : public TelemetrySensor { -private: - Adafruit_MCP9808 mcp9808; +class MCP9808Sensor : public TelemetrySensor +{ + private: + Adafruit_MCP9808 mcp9808; -public: - MCP9808Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + MCP9808Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp index 5a1abe336..9661b59c2 100644 --- a/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.cpp @@ -7,36 +7,38 @@ #include "TelemetrySensor.h" MLX90614Sensor::MLX90614Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90614, "MLX90614") {} -int32_t MLX90614Sensor::runOnce() { - LOG_INFO("Init sensor: %s", sensorName); - if (!hasSensor()) { - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - - if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second) == true) // MLX90614 init - { - LOG_DEBUG("MLX90614 emissivity: %f", mlx.readEmissivity()); - if (fabs(MLX90614_EMISSIVITY - mlx.readEmissivity()) > 0.001) { - mlx.writeEmissivity(MLX90614_EMISSIVITY); - LOG_INFO("MLX90614 emissivity updated. In case of weird data, power cycle"); +int32_t MLX90614Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - LOG_DEBUG("MLX90614 Init Succeed"); - status = true; - } else { - LOG_ERROR("MLX90614 Init Failed"); - status = false; - } - return initI2CSensor(); + + if (mlx.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second) == true) // MLX90614 init + { + LOG_DEBUG("MLX90614 emissivity: %f", mlx.readEmissivity()); + if (fabs(MLX90614_EMISSIVITY - mlx.readEmissivity()) > 0.001) { + mlx.writeEmissivity(MLX90614_EMISSIVITY); + LOG_INFO("MLX90614 emissivity updated. In case of weird data, power cycle"); + } + LOG_DEBUG("MLX90614 Init Succeed"); + status = true; + } else { + LOG_ERROR("MLX90614 Init Failed"); + status = false; + } + return initI2CSensor(); } void MLX90614Sensor::setup() {} -bool MLX90614Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.temperature = mlx.readAmbientTempC(); - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.health_metrics.temperature = mlx.readObjectTempC(); - measurement->variant.health_metrics.has_temperature = true; - return true; +bool MLX90614Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.temperature = mlx.readAmbientTempC(); + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.health_metrics.temperature = mlx.readObjectTempC(); + measurement->variant.health_metrics.has_temperature = true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/MLX90614Sensor.h b/src/modules/Telemetry/Sensor/MLX90614Sensor.h index a667d08d7..c2571027e 100644 --- a/src/modules/Telemetry/Sensor/MLX90614Sensor.h +++ b/src/modules/Telemetry/Sensor/MLX90614Sensor.h @@ -7,17 +7,18 @@ #define MLX90614_EMISSIVITY 0.98 // human skin -class MLX90614Sensor : public TelemetrySensor { -private: - Adafruit_MLX90614 mlx = Adafruit_MLX90614(); +class MLX90614Sensor : public TelemetrySensor +{ + private: + Adafruit_MLX90614 mlx = Adafruit_MLX90614(); -protected: - virtual void setup() override; + protected: + virtual void setup() override; -public: - MLX90614Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + public: + MLX90614Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp b/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp index b87f4a098..eb84edffc 100644 --- a/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp +++ b/src/modules/Telemetry/Sensor/MLX90632Sensor.cpp @@ -8,27 +8,29 @@ MLX90632Sensor::MLX90632Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90632, "MLX90632") {} -bool MLX90632Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool MLX90632Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - MLX90632::status returnError; - if (mlx.begin(dev->address.address, *bus, returnError) == true) // MLX90632 init - { - LOG_DEBUG("MLX90632 Init Succeed"); - status = true; - } else { - LOG_ERROR("MLX90632 Init Failed"); - status = false; - } - initI2CSensor(); - return status; + MLX90632::status returnError; + if (mlx.begin(dev->address.address, *bus, returnError) == true) // MLX90632 init + { + LOG_DEBUG("MLX90632 Init Succeed"); + status = true; + } else { + LOG_ERROR("MLX90632 Init Failed"); + status = false; + } + initI2CSensor(); + return status; } -bool MLX90632Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = mlx.getObjectTemp(); // Get the object temperature in Fahrenheit +bool MLX90632Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = mlx.getObjectTemp(); // Get the object temperature in Fahrenheit - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/MLX90632Sensor.h b/src/modules/Telemetry/Sensor/MLX90632Sensor.h index e6eeaf38e..566db8319 100644 --- a/src/modules/Telemetry/Sensor/MLX90632Sensor.h +++ b/src/modules/Telemetry/Sensor/MLX90632Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class MLX90632Sensor : public TelemetrySensor { -private: - MLX90632 mlx = MLX90632(); +class MLX90632Sensor : public TelemetrySensor +{ + private: + MLX90632 mlx = MLX90632(); -public: - MLX90632Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + MLX90632Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp index fda5c3554..e67b78145 100644 --- a/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp +++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.cpp @@ -16,121 +16,128 @@ meshtastic_Nau7802Config nau7802config = meshtastic_Nau7802Config_init_zero; NAU7802Sensor::NAU7802Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_NAU7802, "NAU7802") {} -bool NAU7802Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = nau7802.begin(*bus); - if (!status) { - return status; - } - nau7802.setSampleRate(NAU7802_SPS_320); - if (!loadCalibrationData()) { - LOG_ERROR("Failed to load calibration data"); - } - nau7802.calibrateAFE(); - LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); - initI2CSensor(); - return status; -} - -bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement) { - LOG_DEBUG("NAU7802 getMetrics"); - nau7802.powerUp(); - // Wait for the sensor to become ready for one second max - uint32_t start = millis(); - while (!nau7802.available()) { - delay(100); - if (!Throttle::isWithinTimespanMs(start, 1000)) { - nau7802.powerDown(); - return false; +bool NAU7802Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = nau7802.begin(*bus); + if (!status) { + return status; } - } - measurement->variant.environment_metrics.has_weight = true; - // Check if we have correct calibration values after powerup - LOG_DEBUG("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); - measurement->variant.environment_metrics.weight = nau7802.getWeight() / 1000; // sample is in kg - nau7802.powerDown(); - return true; + nau7802.setSampleRate(NAU7802_SPS_320); + if (!loadCalibrationData()) { + LOG_ERROR("Failed to load calibration data"); + } + nau7802.calibrateAFE(); + LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); + initI2CSensor(); + return status; } -void NAU7802Sensor::calibrate(float weight) { - nau7802.calculateCalibrationFactor(weight * 1000, 64); // internal sample is in grams - if (!saveCalibrationData()) { - LOG_WARN("Failed to save calibration data"); - } - LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); +bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("NAU7802 getMetrics"); + nau7802.powerUp(); + // Wait for the sensor to become ready for one second max + uint32_t start = millis(); + while (!nau7802.available()) { + delay(100); + if (!Throttle::isWithinTimespanMs(start, 1000)) { + nau7802.powerDown(); + return false; + } + } + measurement->variant.environment_metrics.has_weight = true; + // Check if we have correct calibration values after powerup + LOG_DEBUG("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); + measurement->variant.environment_metrics.weight = nau7802.getWeight() / 1000; // sample is in kg + nau7802.powerDown(); + return true; +} + +void NAU7802Sensor::calibrate(float weight) +{ + nau7802.calculateCalibrationFactor(weight * 1000, 64); // internal sample is in grams + if (!saveCalibrationData()) { + LOG_WARN("Failed to save calibration data"); + } + LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); } AdminMessageHandleResult NAU7802Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - AdminMessageHandleResult result; + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; - switch (request->which_payload_variant) { - case meshtastic_AdminMessage_set_scale_tag: - if (request->set_scale == 0) { - this->tare(); - LOG_DEBUG("Client requested to tare scale"); - } else { - this->calibrate(request->set_scale); - LOG_DEBUG("Client requested to calibrate to %d kg", request->set_scale); + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_set_scale_tag: + if (request->set_scale == 0) { + this->tare(); + LOG_DEBUG("Client requested to tare scale"); + } else { + this->calibrate(request->set_scale); + LOG_DEBUG("Client requested to calibrate to %d kg", request->set_scale); + } + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; } - result = AdminMessageHandleResult::HANDLED; - break; - default: - result = AdminMessageHandleResult::NOT_HANDLED; - } - - return result; + return result; } -void NAU7802Sensor::tare() { - nau7802.calculateZeroOffset(64); - if (!saveCalibrationData()) { - LOG_WARN("Failed to save calibration data"); - } - LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); -} - -bool NAU7802Sensor::saveCalibrationData() { - auto file = SafeFile(nau7802ConfigFileName); - nau7802config.zeroOffset = nau7802.getZeroOffset(); - nau7802config.calibrationFactor = nau7802.getCalibrationFactor(); - bool okay = false; - - LOG_INFO("%s state write to %s", sensorName, nau7802ConfigFileName); - pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_Nau7802Config_size}; - - if (!pb_encode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { - LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); - } else { - okay = true; - } - // Note: SafeFile::close() already acquires the lock and releases it internally - okay &= file.close(); - - return okay; -} - -bool NAU7802Sensor::loadCalibrationData() { - spiLock->lock(); - auto file = FSCom.open(nau7802ConfigFileName, FILE_O_READ); - bool okay = false; - if (file) { - LOG_INFO("%s state read from %s", sensorName, nau7802ConfigFileName); - pb_istream_t stream = {&readcb, &file, meshtastic_Nau7802Config_size}; - if (!pb_decode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { - LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); - } else { - nau7802.setZeroOffset(nau7802config.zeroOffset); - nau7802.setCalibrationFactor(nau7802config.calibrationFactor); - okay = true; +void NAU7802Sensor::tare() +{ + nau7802.calculateZeroOffset(64); + if (!saveCalibrationData()) { + LOG_WARN("Failed to save calibration data"); } - file.close(); - } else { - LOG_INFO("No %s state found (File: %s)", sensorName, nau7802ConfigFileName); - } - spiLock->unlock(); - return okay; + LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); +} + +bool NAU7802Sensor::saveCalibrationData() +{ + auto file = SafeFile(nau7802ConfigFileName); + nau7802config.zeroOffset = nau7802.getZeroOffset(); + nau7802config.calibrationFactor = nau7802.getCalibrationFactor(); + bool okay = false; + + LOG_INFO("%s state write to %s", sensorName, nau7802ConfigFileName); + pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_Nau7802Config_size}; + + if (!pb_encode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + } else { + okay = true; + } + // Note: SafeFile::close() already acquires the lock and releases it internally + okay &= file.close(); + + return okay; +} + +bool NAU7802Sensor::loadCalibrationData() +{ + spiLock->lock(); + auto file = FSCom.open(nau7802ConfigFileName, FILE_O_READ); + bool okay = false; + if (file) { + LOG_INFO("%s state read from %s", sensorName, nau7802ConfigFileName); + pb_istream_t stream = {&readcb, &file, meshtastic_Nau7802Config_size}; + if (!pb_decode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); + } else { + nau7802.setZeroOffset(nau7802config.zeroOffset); + nau7802.setCalibrationFactor(nau7802config.calibrationFactor); + okay = true; + } + file.close(); + } else { + LOG_INFO("No %s state found (File: %s)", sensorName, nau7802ConfigFileName); + } + spiLock->unlock(); + return okay; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/NAU7802Sensor.h b/src/modules/Telemetry/Sensor/NAU7802Sensor.h index 83b50e230..a45e9a78a 100644 --- a/src/modules/Telemetry/Sensor/NAU7802Sensor.h +++ b/src/modules/Telemetry/Sensor/NAU7802Sensor.h @@ -7,23 +7,24 @@ #include "TelemetrySensor.h" #include -class NAU7802Sensor : public TelemetrySensor { -private: - NAU7802 nau7802; +class NAU7802Sensor : public TelemetrySensor +{ + private: + NAU7802 nau7802; -protected: - const char *nau7802ConfigFileName = "/prefs/nau7802.dat"; - bool saveCalibrationData(); - bool loadCalibrationData(); + protected: + const char *nau7802ConfigFileName = "/prefs/nau7802.dat"; + bool saveCalibrationData(); + bool loadCalibrationData(); -public: - NAU7802Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; - void tare(); - void calibrate(float weight); - AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + public: + NAU7802Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + void tare(); + void calibrate(float weight); + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp b/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp index b62f6375d..3407f2f0f 100644 --- a/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp +++ b/src/modules/Telemetry/Sensor/OPT3001Sensor.cpp @@ -9,39 +9,41 @@ OPT3001Sensor::OPT3001Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_OPT3001, "OPT3001") {} -bool OPT3001Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - auto errorCode = opt3001.begin(dev->address.address); - status = errorCode == NO_ERROR; - if (!status) { +bool OPT3001Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + auto errorCode = opt3001.begin(dev->address.address); + status = errorCode == NO_ERROR; + if (!status) { + return status; + } + + OPT3001_Config newConfig; + + newConfig.RangeNumber = 0b1100; + newConfig.ConvertionTime = 0b0; + newConfig.Latch = 0b1; + newConfig.ModeOfConversionOperation = 0b11; + + OPT3001_ErrorCode errorConfig = opt3001.writeConfig(newConfig); + if (errorConfig != NO_ERROR) { + LOG_ERROR("OPT3001 configuration error #%d", errorConfig); + } + status = errorConfig == NO_ERROR; + + initI2CSensor(); return status; - } - - OPT3001_Config newConfig; - - newConfig.RangeNumber = 0b1100; - newConfig.ConvertionTime = 0b0; - newConfig.Latch = 0b1; - newConfig.ModeOfConversionOperation = 0b11; - - OPT3001_ErrorCode errorConfig = opt3001.writeConfig(newConfig); - if (errorConfig != NO_ERROR) { - LOG_ERROR("OPT3001 configuration error #%d", errorConfig); - } - status = errorConfig == NO_ERROR; - - initI2CSensor(); - return status; } -bool OPT3001Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_lux = true; - OPT3001 result = opt3001.readResult(); +bool OPT3001Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_lux = true; + OPT3001 result = opt3001.readResult(); - measurement->variant.environment_metrics.lux = result.lux; - LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); + measurement->variant.environment_metrics.lux = result.lux; + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/OPT3001Sensor.h b/src/modules/Telemetry/Sensor/OPT3001Sensor.h index 6885d5b02..c8a140b51 100644 --- a/src/modules/Telemetry/Sensor/OPT3001Sensor.h +++ b/src/modules/Telemetry/Sensor/OPT3001Sensor.h @@ -7,17 +7,18 @@ #include "TelemetrySensor.h" #include -class OPT3001Sensor : public TelemetrySensor { -private: - ClosedCube_OPT3001 opt3001; +class OPT3001Sensor : public TelemetrySensor +{ + private: + ClosedCube_OPT3001 opt3001; -public: - OPT3001Sensor(); + public: + OPT3001Sensor(); #if WIRE_INTERFACES_COUNT > 1 - virtual bool onlyWire1() { return true; } + virtual bool onlyWire1() { return true; } #endif - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp b/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp index ad8eb013a..189317bf2 100644 --- a/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp +++ b/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp @@ -9,19 +9,21 @@ PCT2075Sensor::PCT2075Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PCT2075, "PCT2075") {} -bool PCT2075Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = pct2075.begin(dev->address.address, bus); +bool PCT2075Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = pct2075.begin(dev->address.address, bus); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool PCT2075Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.temperature = pct2075.getTemperature(); +bool PCT2075Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.temperature = pct2075.getTemperature(); - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/PCT2075Sensor.h b/src/modules/Telemetry/Sensor/PCT2075Sensor.h index 864391f21..55f9423d4 100644 --- a/src/modules/Telemetry/Sensor/PCT2075Sensor.h +++ b/src/modules/Telemetry/Sensor/PCT2075Sensor.h @@ -7,14 +7,15 @@ #include "TelemetrySensor.h" #include -class PCT2075Sensor : public TelemetrySensor { -private: - Adafruit_PCT2075 pct2075; +class PCT2075Sensor : public TelemetrySensor +{ + private: + Adafruit_PCT2075 pct2075; -public: - PCT2075Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + PCT2075Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp index 3061f698f..ff0628cc3 100644 --- a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp @@ -6,102 +6,105 @@ RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {} -bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - // TODO:: check for up to 2 additional sensors and start them if present. - sensor.set_sensor_addr(RAK120351_ADDR); - delay(100); - sensor.begin(dev->address.address); +bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + // TODO:: check for up to 2 additional sensors and start them if present. + sensor.set_sensor_addr(RAK120351_ADDR); + delay(100); + sensor.begin(dev->address.address); - // Get sensor firmware version - uint8_t data = 0; - sensor.get_sensor_version(&data); - if (data != 0) { - LOG_INFO("Init sensor: %s", sensorName); - LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); - status = true; - sensor.sensor_sleep(); - } else { - // If we reach here, it means the sensor did not initialize correctly. - LOG_INFO("Init sensor: %s", sensorName); - LOG_ERROR("RAK12035Sensor Init Failed"); - status = false; - } - if (!status) { + // Get sensor firmware version + uint8_t data = 0; + sensor.get_sensor_version(&data); + if (data != 0) { + LOG_INFO("Init sensor: %s", sensorName); + LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); + status = true; + sensor.sensor_sleep(); + } else { + // If we reach here, it means the sensor did not initialize correctly. + LOG_INFO("Init sensor: %s", sensorName); + LOG_ERROR("RAK12035Sensor Init Failed"); + status = false; + } + if (!status) { + return status; + } + setup(); + + initI2CSensor(); return status; - } - setup(); - - initI2CSensor(); - return status; } -void RAK12035Sensor::setup() { - // Set the calibration values - // Reading the saved calibration values from the sensor. - // TODO:: Check for and run calibration check for up to 2 additional sensors if present. - uint16_t zero_val = 0; - uint16_t hundred_val = 0; - uint16_t default_zero_val = 550; - uint16_t default_hundred_val = 420; - sensor.sensor_on(); - delay(200); - sensor.get_dry_cal(&zero_val); - sensor.get_wet_cal(&hundred_val); - delay(200); - if (zero_val == 0 || zero_val <= hundred_val) { - LOG_INFO("Dry calibration value is %d", zero_val); - LOG_INFO("Wet calibration value is %d", hundred_val); - LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " - "https://github.com/RAKWireless/RAK12035_SoilMoisture."); - LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); - sensor.set_dry_cal(default_zero_val); +void RAK12035Sensor::setup() +{ + // Set the calibration values + // Reading the saved calibration values from the sensor. + // TODO:: Check for and run calibration check for up to 2 additional sensors if present. + uint16_t zero_val = 0; + uint16_t hundred_val = 0; + uint16_t default_zero_val = 550; + uint16_t default_hundred_val = 420; + sensor.sensor_on(); + delay(200); sensor.get_dry_cal(&zero_val); - LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); - } - if (hundred_val == 0 || hundred_val >= zero_val) { + sensor.get_wet_cal(&hundred_val); + delay(200); + if (zero_val == 0 || zero_val <= hundred_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); + sensor.set_dry_cal(default_zero_val); + sensor.get_dry_cal(&zero_val); + LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); + } + if (hundred_val == 0 || hundred_val >= zero_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); + sensor.set_wet_cal(default_hundred_val); + sensor.get_wet_cal(&hundred_val); + LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); + } + sensor.sensor_sleep(); + delay(200); LOG_INFO("Dry calibration value is %d", zero_val); LOG_INFO("Wet calibration value is %d", hundred_val); - LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " - "https://github.com/RAKWireless/RAK12035_SoilMoisture."); - LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); - sensor.set_wet_cal(default_hundred_val); - sensor.get_wet_cal(&hundred_val); - LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); - } - sensor.sensor_sleep(); - delay(200); - LOG_INFO("Dry calibration value is %d", zero_val); - LOG_INFO("Wet calibration value is %d", hundred_val); } -bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) { - // TODO:: read and send metrics for up to 2 additional soil monitors if present. - // -- how to do this.. this could get a little complex.. - // ie - 1> we combine them into an average and send that, 2> we send them as separate metrics - // ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting - // in the device ui and an additional proto for that? - measurement->variant.environment_metrics.has_soil_temperature = true; - measurement->variant.environment_metrics.has_soil_moisture = true; +bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + // TODO:: read and send metrics for up to 2 additional soil monitors if present. + // -- how to do this.. this could get a little complex.. + // ie - 1> we combine them into an average and send that, 2> we send them as separate metrics + // ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting in the + // device ui and an additional proto for that? + measurement->variant.environment_metrics.has_soil_temperature = true; + measurement->variant.environment_metrics.has_soil_moisture = true; - uint8_t moisture = 0; - uint16_t temp = 0; - bool success = false; + uint8_t moisture = 0; + uint16_t temp = 0; + bool success = false; - sensor.sensor_on(); - delay(200); - success = sensor.get_sensor_moisture(&moisture); - delay(200); - success &= sensor.get_sensor_temperature(&temp); - delay(200); - sensor.sensor_sleep(); + sensor.sensor_on(); + delay(200); + success = sensor.get_sensor_moisture(&moisture); + delay(200); + success &= sensor.get_sensor_temperature(&temp); + delay(200); + sensor.sensor_sleep(); - if (success == false) { - LOG_ERROR("Failed to read sensor data"); - return false; - } - measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); - measurement->variant.environment_metrics.soil_moisture = moisture; + if (success == false) { + LOG_ERROR("Failed to read sensor data"); + return false; + } + measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); + measurement->variant.environment_metrics.soil_moisture = moisture; - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.h b/src/modules/Telemetry/Sensor/RAK12035Sensor.h index 6a6df27f3..6a38d2eb3 100644 --- a/src/modules/Telemetry/Sensor/RAK12035Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.h @@ -12,17 +12,18 @@ #include "TelemetrySensor.h" #include -class RAK12035Sensor : public TelemetrySensor { -private: - RAK12035 sensor; - void setup(); +class RAK12035Sensor : public TelemetrySensor +{ + private: + RAK12035 sensor; + void setup(); -public: - RAK12035Sensor(); + public: + RAK12035Sensor(); #if WIRE_INTERFACES_COUNT > 1 - virtual bool onlyWire1() { return true; } + virtual bool onlyWire1() { return true; } #endif - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp b/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp index 2cfd3081f..ad3925f08 100644 --- a/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.cpp @@ -28,162 +28,182 @@ static uint8_t provision = 0; extern RAK9154Sensor rak9154Sensor; -static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len) { - switch (eid) { - case SNHUBAPI_EVT_RECV_REQ: - case SNHUBAPI_EVT_RECV_RSP: - break; +static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len) +{ + switch (eid) { + case SNHUBAPI_EVT_RECV_REQ: + case SNHUBAPI_EVT_RECV_RSP: + break; - case SNHUBAPI_EVT_QSEND: - mySerial.write(msg, len); - break; + case SNHUBAPI_EVT_QSEND: + mySerial.write(msg, len); + break; - case SNHUBAPI_EVT_ADD_SID: - // LOG_INFO("+ADD:SID:[%02x]", msg[0]); - break; + case SNHUBAPI_EVT_ADD_SID: + // LOG_INFO("+ADD:SID:[%02x]", msg[0]); + break; - case SNHUBAPI_EVT_ADD_PID: - // LOG_INFO("+ADD:PID:[%02x]", msg[0]); + case SNHUBAPI_EVT_ADD_PID: + // LOG_INFO("+ADD:PID:[%02x]", msg[0]); #ifdef BOOT_DATA_REQ - provision = msg[0]; + provision = msg[0]; #endif - break; + break; - case SNHUBAPI_EVT_GET_INTV: - break; + case SNHUBAPI_EVT_GET_INTV: + break; - case SNHUBAPI_EVT_GET_ENABLE: - break; + case SNHUBAPI_EVT_GET_ENABLE: + break; - case SNHUBAPI_EVT_SDATA_REQ: + case SNHUBAPI_EVT_SDATA_REQ: + + // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); + // for( uint16_t i=1; i 100) { + dc_prec = 100; + } + break; + case RAK_IPSO_DC_CURRENT: + dc_cur = (msg[2] << 8) + msg[1]; + break; + case RAK_IPSO_DC_VOLTAGE: + dc_vol = (msg[2] << 8) + msg[1]; + dc_vol *= 10; + break; + default: + break; + } + rak9154Sensor.setLastRead(millis()); + + break; + case SNHUBAPI_EVT_REPORT: + + // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); + // for( uint16_t i=1; i 100) { + dc_prec = 100; + } + break; + case RAK_IPSO_DC_CURRENT: + dc_cur = (msg[1] << 8) + msg[2]; + break; + case RAK_IPSO_DC_VOLTAGE: + dc_vol = (msg[1] << 8) + msg[2]; + dc_vol *= 10; + break; + default: + break; + } + rak9154Sensor.setLastRead(millis()); + + break; + + case SNHUBAPI_EVT_CHKSUM_ERR: + LOG_INFO("+ERR:CHKSUM"); + break; + + case SNHUBAPI_EVT_SEQ_ERR: + LOG_INFO("+ERR:SEQUCE"); + break; - // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); - // for( uint16_t i=1; i 100) { - dc_prec = 100; - } - break; - case RAK_IPSO_DC_CURRENT: - dc_cur = (msg[2] << 8) + msg[1]; - break; - case RAK_IPSO_DC_VOLTAGE: - dc_vol = (msg[2] << 8) + msg[1]; - dc_vol *= 10; - break; default: - break; + break; } - rak9154Sensor.setLastRead(millis()); +} - break; - case SNHUBAPI_EVT_REPORT: - - // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); - // for( uint16_t i=1; i 100) { - dc_prec = 100; - } - break; - case RAK_IPSO_DC_CURRENT: - dc_cur = (msg[1] << 8) + msg[2]; - break; - case RAK_IPSO_DC_VOLTAGE: - dc_vol = (msg[1] << 8) + msg[2]; - dc_vol *= 10; - break; - default: - break; +static int32_t onewireHandle() +{ + if (provision != 0) { + RakSNHub_Protocl_API.get.data(provision); + provision = 0; } - rak9154Sensor.setLastRead(millis()); - break; + while (mySerial.available()) { + char a = mySerial.read(); + buff[bufflen++] = a; + delay(2); // continue data, timeout=2ms + } - case SNHUBAPI_EVT_CHKSUM_ERR: - LOG_INFO("+ERR:CHKSUM"); - break; + if (bufflen != 0) { + RakSNHub_Protocl_API.process((uint8_t *)buff, bufflen); + bufflen = 0; + } - case SNHUBAPI_EVT_SEQ_ERR: - LOG_INFO("+ERR:SEQUCE"); - break; - - default: - break; - } + return 50; } -static int32_t onewireHandle() { - if (provision != 0) { - RakSNHub_Protocl_API.get.data(provision); - provision = 0; - } +int32_t RAK9154Sensor::runOnce() +{ + if (!rak9154Sensor.isInitialized()) { + onewirePeriodic = new Periodic("onewireHandle", onewireHandle); - while (mySerial.available()) { - char a = mySerial.read(); - buff[bufflen++] = a; - delay(2); // continue data, timeout=2ms - } + mySerial.begin(9600); - if (bufflen != 0) { - RakSNHub_Protocl_API.process((uint8_t *)buff, bufflen); - bufflen = 0; - } + RakSNHub_Protocl_API.init(onewire_evt); - return 50; + status = true; + initialized = true; + } + + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } -int32_t RAK9154Sensor::runOnce() { - if (!rak9154Sensor.isInitialized()) { - onewirePeriodic = new Periodic("onewireHandle", onewireHandle); - - mySerial.begin(9600); - - RakSNHub_Protocl_API.init(onewire_evt); - - status = true; - initialized = true; - } - - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; +void RAK9154Sensor::setup() +{ + // Set up oversampling and filter initialization } -void RAK9154Sensor::setup() { - // Set up oversampling and filter initialization +bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (getBusVoltageMv() > 0) { + measurement->variant.environment_metrics.has_voltage = true; + measurement->variant.environment_metrics.has_current = true; + + measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000; + measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000; + return true; + } else { + return false; + } } -bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) { - if (getBusVoltageMv() > 0) { - measurement->variant.environment_metrics.has_voltage = true; - measurement->variant.environment_metrics.has_current = true; - - measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000; - measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000; - return true; - } else { - return false; - } +uint16_t RAK9154Sensor::getBusVoltageMv() +{ + return dc_vol; } -uint16_t RAK9154Sensor::getBusVoltageMv() { return dc_vol; } +int16_t RAK9154Sensor::getCurrentMa() +{ + return dc_cur; +} -int16_t RAK9154Sensor::getCurrentMa() { return dc_cur; } +int RAK9154Sensor::getBusBatteryPercent() +{ + return (int)dc_prec; +} -int RAK9154Sensor::getBusBatteryPercent() { return (int)dc_prec; } - -bool RAK9154Sensor::isCharging() { return (dc_cur > 0) ? true : false; } -void RAK9154Sensor::setLastRead(uint32_t lastRead) { this->lastRead = lastRead; } +bool RAK9154Sensor::isCharging() +{ + return (dc_cur > 0) ? true : false; +} +void RAK9154Sensor::setLastRead(uint32_t lastRead) +{ + this->lastRead = lastRead; +} #endif // HAS_RAKPROT diff --git a/src/modules/Telemetry/Sensor/RAK9154Sensor.h b/src/modules/Telemetry/Sensor/RAK9154Sensor.h index 9c01a8961..c96139f9c 100644 --- a/src/modules/Telemetry/Sensor/RAK9154Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.h @@ -9,21 +9,22 @@ #include "TelemetrySensor.h" #include "VoltageSensor.h" -class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { -private: -protected: - virtual void setup() override; - uint32_t lastRead = 0; +class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor +{ + private: + protected: + virtual void setup() override; + uint32_t lastRead = 0; -public: - RAK9154Sensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; - int getBusBatteryPercent(); - bool isCharging(); - void setLastRead(uint32_t lastRead); + public: + RAK9154Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; + int getBusBatteryPercent(); + bool isCharging(); + void setLastRead(uint32_t lastRead); }; #endif // _RAK9154SENSOR_H #endif // HAS_RAKPROT \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp index 73dcb20c5..3dbd06e8d 100644 --- a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp @@ -8,66 +8,70 @@ RCWL9620Sensor::RCWL9620Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RCWL9620, "RCWL9620") {} -bool RCWL9620Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = 1; - begin(bus, dev->address.address); - initI2CSensor(); - return status; +bool RCWL9620Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = 1; + begin(bus, dev->address.address); + initI2CSensor(); + return status; } -bool RCWL9620Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_distance = true; - LOG_DEBUG("RCWL9620 getMetrics"); - measurement->variant.environment_metrics.distance = getDistance(); - return true; +bool RCWL9620Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_distance = true; + LOG_DEBUG("RCWL9620 getMetrics"); + measurement->variant.environment_metrics.distance = getDistance(); + return true; } -void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) { - _wire = wire; - _addr = addr; - _sda = sda; - _scl = scl; - _speed = speed; - _wire->begin(); +void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) +{ + _wire = wire; + _addr = addr; + _sda = sda; + _scl = scl; + _speed = speed; + _wire->begin(); } -float RCWL9620Sensor::getDistance() { - uint32_t data = 0; - uint8_t b1 = 0, b2 = 0, b3 = 0; +float RCWL9620Sensor::getDistance() +{ + uint32_t data = 0; + uint8_t b1 = 0, b2 = 0, b3 = 0; - LOG_DEBUG("[RCWL9620] Start measure command"); + LOG_DEBUG("[RCWL9620] Start measure command"); - _wire->beginTransmission(_addr); - _wire->write(0x01); // À tester aussi sans cette ligne si besoin - uint8_t result = _wire->endTransmission(); - LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); - delay(100); // délai pour laisser le capteur répondre + _wire->beginTransmission(_addr); + _wire->write(0x01); // À tester aussi sans cette ligne si besoin + uint8_t result = _wire->endTransmission(); + LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); + delay(100); // délai pour laisser le capteur répondre - LOG_DEBUG("[RCWL9620] Read i2c data:"); - _wire->requestFrom(_addr, (uint8_t)3); + LOG_DEBUG("[RCWL9620] Read i2c data:"); + _wire->requestFrom(_addr, (uint8_t)3); - if (_wire->available() < 3) { - LOG_DEBUG("[RCWL9620] less than 3 octets !"); - return 0.0; - } + if (_wire->available() < 3) { + LOG_DEBUG("[RCWL9620] less than 3 octets !"); + return 0.0; + } - b1 = _wire->read(); - b2 = _wire->read(); - b3 = _wire->read(); + b1 = _wire->read(); + b2 = _wire->read(); + b3 = _wire->read(); - data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; + data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; - float Distance = float(data) / 1000.0; + float Distance = float(data) / 1000.0; - LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); - LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); + LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); + LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); - if (Distance > 4500.00) { - return 4500.00; - } else { - return Distance; - } + if (Distance > 4500.00) { + return 4500.00; + } else { + return Distance; + } } #endif diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.h b/src/modules/Telemetry/Sensor/RCWL9620Sensor.h index 7ccc7a6da..408db3633 100644 --- a/src/modules/Telemetry/Sensor/RCWL9620Sensor.h +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.h @@ -6,22 +6,23 @@ #include "TelemetrySensor.h" #include -class RCWL9620Sensor : public TelemetrySensor { -private: - uint8_t _addr = 0x57; - TwoWire *_wire = &Wire; - uint8_t _scl = -1; - uint8_t _sda = -1; - uint32_t _speed = 200000UL; +class RCWL9620Sensor : public TelemetrySensor +{ + private: + uint8_t _addr = 0x57; + TwoWire *_wire = &Wire; + uint8_t _scl = -1; + uint8_t _sda = -1; + uint32_t _speed = 200000UL; -protected: - void begin(TwoWire *wire = &Wire, uint8_t addr = 0x57, uint8_t sda = -1, uint8_t scl = -1, uint32_t speed = 200000UL); - float getDistance(); + protected: + void begin(TwoWire *wire = &Wire, uint8_t addr = 0x57, uint8_t sda = -1, uint8_t scl = -1, uint32_t speed = 200000UL); + float getDistance(); -public: - RCWL9620Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + RCWL9620Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp index 78143af5d..67a36933d 100644 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp +++ b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp @@ -9,21 +9,23 @@ SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {} -bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - sht31 = Adafruit_SHT31(bus); - status = sht31.begin(dev->address.address); - initI2CSensor(); - return status; +bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + sht31 = Adafruit_SHT31(bus); + status = sht31.begin(dev->address.address); + initI2CSensor(); + return status; } -bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.temperature = sht31.readTemperature(); - measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity(); +bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.temperature = sht31.readTemperature(); + measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity(); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.h b/src/modules/Telemetry/Sensor/SHT31Sensor.h index 97e1cdda3..ecb7d63a6 100644 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.h +++ b/src/modules/Telemetry/Sensor/SHT31Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class SHT31Sensor : public TelemetrySensor { -private: - Adafruit_SHT31 sht31; +class SHT31Sensor : public TelemetrySensor +{ + private: + Adafruit_SHT31 sht31; -public: - SHT31Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + SHT31Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp index 95795da5e..b11795d97 100644 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp @@ -9,38 +9,40 @@ SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {} -bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - uint32_t serialNumber = 0; + uint32_t serialNumber = 0; - status = sht4x.begin(bus); - if (!status) { + status = sht4x.begin(bus); + if (!status) { + return status; + } + + serialNumber = sht4x.readSerial(); + if (serialNumber != 0) { + LOG_DEBUG("serialNumber : %x", serialNumber); + status = 1; + } else { + LOG_DEBUG("Error trying to execute readSerial(): "); + status = 0; + } + + initI2CSensor(); return status; - } - - serialNumber = sht4x.readSerial(); - if (serialNumber != 0) { - LOG_DEBUG("serialNumber : %x", serialNumber); - status = 1; - } else { - LOG_DEBUG("Error trying to execute readSerial(): "); - status = 0; - } - - initI2CSensor(); - return status; } -bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; +bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; - sensors_event_t humidity, temp; - sht4x.getEvent(&humidity, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - return true; + sensors_event_t humidity, temp; + sht4x.getEvent(&humidity, &temp); + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.h b/src/modules/Telemetry/Sensor/SHT4XSensor.h index 8bb819e9c..7311d2366 100644 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.h +++ b/src/modules/Telemetry/Sensor/SHT4XSensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class SHT4XSensor : public TelemetrySensor { -private: - Adafruit_SHT4x sht4x = Adafruit_SHT4x(); +class SHT4XSensor : public TelemetrySensor +{ + private: + Adafruit_SHT4x sht4x = Adafruit_SHT4x(); -public: - SHT4XSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + SHT4XSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp index b2961a845..fdab0b266 100644 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp +++ b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp @@ -9,25 +9,27 @@ SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {} -bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = shtc3.begin(bus); +bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = shtc3.begin(bus); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; +bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; - sensors_event_t humidity, temp; - shtc3.getEvent(&humidity, &temp); + sensors_event_t humidity, temp; + shtc3.getEvent(&humidity, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; + measurement->variant.environment_metrics.temperature = temp.temperature; + measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.h b/src/modules/Telemetry/Sensor/SHTC3Sensor.h index a6aaa9905..51cee18f7 100644 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.h +++ b/src/modules/Telemetry/Sensor/SHTC3Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class SHTC3Sensor : public TelemetrySensor { -private: - Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); +class SHTC3Sensor : public TelemetrySensor +{ + private: + Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); -public: - SHTC3Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + SHTC3Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/T1000xSensor.cpp b/src/modules/Telemetry/Sensor/T1000xSensor.cpp index 0fba20b4f..b123450ec 100644 --- a/src/modules/Telemetry/Sensor/T1000xSensor.cpp +++ b/src/modules/Telemetry/Sensor/T1000xSensor.cpp @@ -17,89 +17,95 @@ // ntc res table uint32_t ntc_res2[136] = { - 113347, 107565, 102116, 96978, 92132, 87559, 83242, 79166, 75316, 71677, 68237, 64991, 61919, 59011, 56258, 53650, 51178, 48835, 46613, 44506, - 42506, 40600, 38791, 37073, 35442, 33892, 32420, 31020, 29689, 28423, 27219, 26076, 24988, 23951, 22963, 22021, 21123, 20267, 19450, 18670, - 17926, 17214, 16534, 15886, 15266, 14674, 14108, 13566, 13049, 12554, 12081, 11628, 11195, 10780, 10382, 10000, 9634, 9284, 8947, 8624, - 8315, 8018, 7734, 7461, 7199, 6948, 6707, 6475, 6253, 6039, 5834, 5636, 5445, 5262, 5086, 4917, 4754, 4597, 4446, 4301, - 4161, 4026, 3896, 3771, 3651, 3535, 3423, 3315, 3211, 3111, 3014, 2922, 2834, 2748, 2666, 2586, 2509, 2435, 2364, 2294, - 2228, 2163, 2100, 2040, 1981, 1925, 1870, 1817, 1766, 1716, 1669, 1622, 1578, 1535, 1493, 1452, 1413, 1375, 1338, 1303, - 1268, 1234, 1202, 1170, 1139, 1110, 1081, 1053, 1026, 999, 974, 949, 925, 902, 880, 858, + 113347, 107565, 102116, 96978, 92132, 87559, 83242, 79166, 75316, 71677, 68237, 64991, 61919, 59011, 56258, 53650, 51178, + 48835, 46613, 44506, 42506, 40600, 38791, 37073, 35442, 33892, 32420, 31020, 29689, 28423, 27219, 26076, 24988, 23951, + 22963, 22021, 21123, 20267, 19450, 18670, 17926, 17214, 16534, 15886, 15266, 14674, 14108, 13566, 13049, 12554, 12081, + 11628, 11195, 10780, 10382, 10000, 9634, 9284, 8947, 8624, 8315, 8018, 7734, 7461, 7199, 6948, 6707, 6475, + 6253, 6039, 5834, 5636, 5445, 5262, 5086, 4917, 4754, 4597, 4446, 4301, 4161, 4026, 3896, 3771, 3651, + 3535, 3423, 3315, 3211, 3111, 3014, 2922, 2834, 2748, 2666, 2586, 2509, 2435, 2364, 2294, 2228, 2163, + 2100, 2040, 1981, 1925, 1870, 1817, 1766, 1716, 1669, 1622, 1578, 1535, 1493, 1452, 1413, 1375, 1338, + 1303, 1268, 1234, 1202, 1170, 1139, 1110, 1081, 1053, 1026, 999, 974, 949, 925, 902, 880, 858, }; int8_t ntc_temp2[136] = { - -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, - -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, - 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, - 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, + -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, + -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, + 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, }; T1000xSensor::T1000xSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "T1000x") {} -bool T1000xSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - return true; +bool T1000xSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + return true; } -float T1000xSensor::getLux() { - uint32_t lux_vot = 0; - float lux_level = 0; +float T1000xSensor::getLux() +{ + uint32_t lux_vot = 0; + float lux_level = 0; - for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - lux_vot += analogRead(T1000X_LUX_PIN); - } - lux_vot = lux_vot / T1000X_SENSE_SAMPLES; - lux_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * lux_vot; - - if (lux_vot <= 80) - lux_level = 0; - else if (lux_vot >= 2480) - lux_level = 100; - else - lux_level = 100 * (lux_vot - 80) / T1000X_LIGHT_REF_VCC; - - return lux_level; -} - -float T1000xSensor::getTemp() { - uint32_t vcc_vot = 0, ntc_vot = 0; - - uint8_t u8i = 0; - float Vout = 0, Rt = 0, temp = 0; - float Temp = 0; - - for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - vcc_vot += analogRead(T1000X_VCC_PIN); - } - vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; - vcc_vot = 2 * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; - - for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - ntc_vot += analogRead(T1000X_NTC_PIN); - } - ntc_vot = ntc_vot / T1000X_SENSE_SAMPLES; - ntc_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * ntc_vot; - - Vout = ntc_vot; - Rt = (HEATER_NTC_RP * vcc_vot) / Vout - HEATER_NTC_RP; - for (u8i = 0; u8i < 135; u8i++) { - if (Rt >= ntc_res2[u8i]) { - break; + for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { + lux_vot += analogRead(T1000X_LUX_PIN); } - } - temp = ntc_temp2[u8i - 1] + 1 * (ntc_res2[u8i - 1] - Rt) / (float)(ntc_res2[u8i - 1] - ntc_res2[u8i]); - Temp = (temp * 100 + 5) / 100; // half adjust + lux_vot = lux_vot / T1000X_SENSE_SAMPLES; + lux_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * lux_vot; - return Temp; + if (lux_vot <= 80) + lux_level = 0; + else if (lux_vot >= 2480) + lux_level = 100; + else + lux_level = 100 * (lux_vot - 80) / T1000X_LIGHT_REF_VCC; + + return lux_level; } -bool T1000xSensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_lux = true; +float T1000xSensor::getTemp() +{ + uint32_t vcc_vot = 0, ntc_vot = 0; - measurement->variant.environment_metrics.temperature = getTemp(); - measurement->variant.environment_metrics.lux = getLux(); - return true; + uint8_t u8i = 0; + float Vout = 0, Rt = 0, temp = 0; + float Temp = 0; + + for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { + vcc_vot += analogRead(T1000X_VCC_PIN); + } + vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; + vcc_vot = 2 * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + + for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { + ntc_vot += analogRead(T1000X_NTC_PIN); + } + ntc_vot = ntc_vot / T1000X_SENSE_SAMPLES; + ntc_vot = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * ntc_vot; + + Vout = ntc_vot; + Rt = (HEATER_NTC_RP * vcc_vot) / Vout - HEATER_NTC_RP; + for (u8i = 0; u8i < 135; u8i++) { + if (Rt >= ntc_res2[u8i]) { + break; + } + } + temp = ntc_temp2[u8i - 1] + 1 * (ntc_res2[u8i - 1] - Rt) / (float)(ntc_res2[u8i - 1] - ntc_res2[u8i]); + Temp = (temp * 100 + 5) / 100; // half adjust + + return Temp; +} + +bool T1000xSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_lux = true; + + measurement->variant.environment_metrics.temperature = getTemp(); + measurement->variant.environment_metrics.lux = getLux(); + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/T1000xSensor.h b/src/modules/Telemetry/Sensor/T1000xSensor.h index 1b5929e0f..b840a2d88 100644 --- a/src/modules/Telemetry/Sensor/T1000xSensor.h +++ b/src/modules/Telemetry/Sensor/T1000xSensor.h @@ -5,13 +5,14 @@ #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" -class T1000xSensor : public TelemetrySensor { -public: - T1000xSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; - virtual float getLux(); - virtual float getTemp(); +class T1000xSensor : public TelemetrySensor +{ + public: + T1000xSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual float getLux(); + virtual float getTemp(); }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp index e643d85ee..4e02af642 100644 --- a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp +++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp @@ -9,28 +9,30 @@ TSL2561Sensor::TSL2561Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL2561, "TSL2561") {} -bool TSL2561Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); +bool TSL2561Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); - status = tsl.begin(bus); - if (!status) { + status = tsl.begin(bus); + if (!status) { + return status; + } + tsl.setGain(TSL2561_GAIN_1X); + tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS); + + initI2CSensor(); return status; - } - tsl.setGain(TSL2561_GAIN_1X); - tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS); - - initI2CSensor(); - return status; } -bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_lux = true; - sensors_event_t event; - tsl.getEvent(&event); - measurement->variant.environment_metrics.lux = event.light; - LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); +bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_lux = true; + sensors_event_t event; + tsl.getEvent(&event); + measurement->variant.environment_metrics.lux = event.light; + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.h b/src/modules/Telemetry/Sensor/TSL2561Sensor.h index 667c7e970..abf5a8f73 100644 --- a/src/modules/Telemetry/Sensor/TSL2561Sensor.h +++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.h @@ -6,14 +6,15 @@ #include "TelemetrySensor.h" #include -class TSL2561Sensor : public TelemetrySensor { -private: - // The magic number is a sensor id, the actual value doesn't matter - Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345); +class TSL2561Sensor : public TelemetrySensor +{ + private: + // The magic number is a sensor id, the actual value doesn't matter + Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345); -public: - TSL2561Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + TSL2561Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp index c7198b74e..0899d4470 100644 --- a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp @@ -10,30 +10,32 @@ TSL2591Sensor::TSL2591Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL25911FN, "TSL2591") {} -bool TSL2591Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = tsl.begin(bus); - if (!status) { - return status; - } - tsl.setGain(TSL2591_GAIN_LOW); // 1x gain - tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); +bool TSL2591Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = tsl.begin(bus); + if (!status) { + return status; + } + tsl.setGain(TSL2591_GAIN_LOW); // 1x gain + tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); - initI2CSensor(); - return status; + initI2CSensor(); + return status; } -bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_lux = true; - uint32_t lum = tsl.getFullLuminosity(); - uint16_t ir, full; - ir = lum >> 16; - full = lum & 0xFFFF; +bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_lux = true; + uint32_t lum = tsl.getFullLuminosity(); + uint16_t ir, full; + ir = lum >> 16; + full = lum & 0xFFFF; - measurement->variant.environment_metrics.lux = tsl.calculateLux(full, ir); - LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); + measurement->variant.environment_metrics.lux = tsl.calculateLux(full, ir); + LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); - return true; + return true; } #endif diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.h b/src/modules/Telemetry/Sensor/TSL2591Sensor.h index df968da72..1ac430a03 100644 --- a/src/modules/Telemetry/Sensor/TSL2591Sensor.h +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.h @@ -6,13 +6,14 @@ #include "TelemetrySensor.h" #include -class TSL2591Sensor : public TelemetrySensor { -private: - Adafruit_TSL2591 tsl; +class TSL2591Sensor : public TelemetrySensor +{ + private: + Adafruit_TSL2591 tsl; -public: - TSL2591Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + TSL2591Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index e9790564a..3c3e61808 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -16,55 +16,59 @@ class TwoWire; #define DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS 1000 extern std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1]; -class TelemetrySensor { -protected: - TelemetrySensor(meshtastic_TelemetrySensorType sensorType, const char *sensorName) { - this->sensorName = sensorName; - this->sensorType = sensorType; - this->status = 0; - } - - const char *sensorName; - meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; - unsigned status; - bool initialized = false; - - int32_t initI2CSensor() { - if (!status) { - LOG_WARN("Can't connect to detected %s sensor. Remove from nodeTelemetrySensorsMap", sensorName); - nodeTelemetrySensorsMap[sensorType].first = 0; - } else { - LOG_INFO("Opened %s sensor on i2c bus", sensorName); - setup(); +class TelemetrySensor +{ + protected: + TelemetrySensor(meshtastic_TelemetrySensorType sensorType, const char *sensorName) + { + this->sensorName = sensorName; + this->sensorType = sensorType; + this->status = 0; } - initialized = true; - return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; - } - // TODO: check is setup used at all? - virtual void setup() {} + const char *sensorName; + meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; + unsigned status; + bool initialized = false; -public: - virtual ~TelemetrySensor() {} + int32_t initI2CSensor() + { + if (!status) { + LOG_WARN("Can't connect to detected %s sensor. Remove from nodeTelemetrySensorsMap", sensorName); + nodeTelemetrySensorsMap[sensorType].first = 0; + } else { + LOG_INFO("Opened %s sensor on i2c bus", sensorName); + setup(); + } + initialized = true; + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } - virtual AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) { - return AdminMessageHandleResult::NOT_HANDLED; - } + // TODO: check is setup used at all? + virtual void setup() {} - // TODO: delete after migration - bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + public: + virtual ~TelemetrySensor() {} + + virtual AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) + { + return AdminMessageHandleResult::NOT_HANDLED; + } + + // TODO: delete after migration + bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } #if WIRE_INTERFACES_COUNT > 1 - // Set to true if Implementation only works first I2C port (Wire) - virtual bool onlyWire1() { return false; } + // Set to true if Implementation only works first I2C port (Wire) + virtual bool onlyWire1() { return false; } #endif - virtual int32_t runOnce() { return INT32_MAX; } - virtual bool isInitialized() { return initialized; } - virtual bool isRunning() { return status > 0; } + virtual int32_t runOnce() { return INT32_MAX; } + virtual bool isInitialized() { return initialized; } + virtual bool isRunning() { return status > 0; } - virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; + virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp b/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp index c09bf691a..c89463be5 100644 --- a/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp +++ b/src/modules/Telemetry/Sensor/VEML7700Sensor.cpp @@ -11,19 +11,20 @@ VEML7700Sensor::VEML7700Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_VEML7700, "VEML7700") {} -bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { - LOG_INFO("Init sensor: %s", sensorName); - status = veml7700.begin(bus); - if (!status) { +bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + status = veml7700.begin(bus); + if (!status) { + return status; + } + + veml7700.setLowThreshold(10000); + veml7700.setHighThreshold(20000); + veml7700.interruptEnable(true); + + initI2CSensor(); return status; - } - - veml7700.setLowThreshold(10000); - veml7700.setHighThreshold(20000); - veml7700.interruptEnable(true); - - initI2CSensor(); - return status; } /*! @@ -32,29 +33,35 @@ bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { * @param corrected if true, apply non-linear correction * @return lux value */ -float VEML7700Sensor::computeLux(uint16_t rawALS, bool corrected) { - float lux = getResolution() * rawALS; - if (corrected) - lux = (((6.0135e-13 * lux - 9.3924e-9) * lux + 8.1488e-5) * lux + 1.0023) * lux; - return lux; +float VEML7700Sensor::computeLux(uint16_t rawALS, bool corrected) +{ + float lux = getResolution() * rawALS; + if (corrected) + lux = (((6.0135e-13 * lux - 9.3924e-9) * lux + 8.1488e-5) * lux + 1.0023) * lux; + return lux; } /*! * @brief Determines resolution for current gain and integration time * settings. */ -float VEML7700Sensor::getResolution(void) { return MAX_RES * (IT_MAX / veml7700.getIntegrationTimeValue()) * (GAIN_MAX / veml7700.getGainValue()); } +float VEML7700Sensor::getResolution(void) +{ + return MAX_RES * (IT_MAX / veml7700.getIntegrationTimeValue()) * (GAIN_MAX / veml7700.getGainValue()); +} -bool VEML7700Sensor::getMetrics(meshtastic_Telemetry *measurement) { - measurement->variant.environment_metrics.has_lux = true; - measurement->variant.environment_metrics.has_white_lux = true; +bool VEML7700Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_lux = true; + measurement->variant.environment_metrics.has_white_lux = true; - int16_t white; - measurement->variant.environment_metrics.lux = veml7700.readLux(VEML_LUX_AUTO); - white = veml7700.readWhite(true); - measurement->variant.environment_metrics.white_lux = computeLux(white, white > 100); - LOG_INFO("white lux %f, als lux %f", measurement->variant.environment_metrics.white_lux, measurement->variant.environment_metrics.lux); + int16_t white; + measurement->variant.environment_metrics.lux = veml7700.readLux(VEML_LUX_AUTO); + white = veml7700.readWhite(true); + measurement->variant.environment_metrics.white_lux = computeLux(white, white > 100); + LOG_INFO("white lux %f, als lux %f", measurement->variant.environment_metrics.white_lux, + measurement->variant.environment_metrics.lux); - return true; + return true; } #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VEML7700Sensor.h b/src/modules/Telemetry/Sensor/VEML7700Sensor.h index 75dd6827e..92883df08 100644 --- a/src/modules/Telemetry/Sensor/VEML7700Sensor.h +++ b/src/modules/Telemetry/Sensor/VEML7700Sensor.h @@ -6,18 +6,19 @@ #include "TelemetrySensor.h" #include -class VEML7700Sensor : public TelemetrySensor { -private: - const float MAX_RES = 0.0036; - const float GAIN_MAX = 2; - const float IT_MAX = 800; - Adafruit_VEML7700 veml7700; - float computeLux(uint16_t rawALS, bool corrected); - float getResolution(void); +class VEML7700Sensor : public TelemetrySensor +{ + private: + const float MAX_RES = 0.0036; + const float GAIN_MAX = 2; + const float IT_MAX = 800; + Adafruit_VEML7700 veml7700; + float computeLux(uint16_t rawALS, bool corrected); + float getResolution(void); -public: - VEML7700Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + public: + VEML7700Sensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/VoltageSensor.h b/src/modules/Telemetry/Sensor/VoltageSensor.h index 17565a214..767ffd246 100644 --- a/src/modules/Telemetry/Sensor/VoltageSensor.h +++ b/src/modules/Telemetry/Sensor/VoltageSensor.h @@ -4,9 +4,10 @@ #pragma once -class VoltageSensor { -public: - virtual uint16_t getBusVoltageMv() = 0; +class VoltageSensor +{ + public: + virtual uint16_t getBusVoltageMv() = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/nullSensor.cpp b/src/modules/Telemetry/Sensor/nullSensor.cpp index 30af8d09b..1d545186a 100644 --- a/src/modules/Telemetry/Sensor/nullSensor.cpp +++ b/src/modules/Telemetry/Sensor/nullSensor.cpp @@ -9,14 +9,26 @@ NullSensor::NullSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "nullSensor") {} -int32_t NullSensor::runOnce() { return INT32_MAX; } +int32_t NullSensor::runOnce() +{ + return INT32_MAX; +} void NullSensor::setup() {} -bool NullSensor::getMetrics(meshtastic_Telemetry *measurement) { return false; } +bool NullSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + return false; +} -uint16_t NullSensor::getBusVoltageMv() { return 0; } +uint16_t NullSensor::getBusVoltageMv() +{ + return 0; +} -int16_t NullSensor::getCurrentMa() { return 0; } +int16_t NullSensor::getCurrentMa() +{ + return 0; +} #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/nullSensor.h b/src/modules/Telemetry/Sensor/nullSensor.h index 26f9bc567..a400acf97 100644 --- a/src/modules/Telemetry/Sensor/nullSensor.h +++ b/src/modules/Telemetry/Sensor/nullSensor.h @@ -9,19 +9,20 @@ #include "TelemetrySensor.h" #include "VoltageSensor.h" -class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor { +class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor +{ -protected: - virtual void setup() override; + protected: + virtual void setup() override; -public: - NullSensor(); - virtual int32_t runOnce() override; - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - int32_t runTrigger() { return 0; } + public: + NullSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + int32_t runTrigger() { return 0; } - virtual uint16_t getBusVoltageMv() override; - virtual int16_t getCurrentMa() override; + virtual uint16_t getBusVoltageMv() override; + virtual int16_t getCurrentMa() override; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/UnitConversions.cpp b/src/modules/Telemetry/UnitConversions.cpp index 7bee372df..fff1ee3d2 100644 --- a/src/modules/Telemetry/UnitConversions.cpp +++ b/src/modules/Telemetry/UnitConversions.cpp @@ -1,9 +1,21 @@ #include "UnitConversions.h" -float UnitConversions::CelsiusToFahrenheit(float celsius) { return (celsius * 9) / 5 + 32; } +float UnitConversions::CelsiusToFahrenheit(float celsius) +{ + return (celsius * 9) / 5 + 32; +} -float UnitConversions::MetersPerSecondToKnots(float metersPerSecond) { return metersPerSecond * 1.94384; } +float UnitConversions::MetersPerSecondToKnots(float metersPerSecond) +{ + return metersPerSecond * 1.94384; +} -float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond) { return metersPerSecond * 2.23694; } +float UnitConversions::MetersPerSecondToMilesPerHour(float metersPerSecond) +{ + return metersPerSecond * 2.23694; +} -float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal) { return hectoPascal * 0.029529983071445; } +float UnitConversions::HectoPascalToInchesOfMercury(float hectoPascal) +{ + return hectoPascal * 0.029529983071445; +} diff --git a/src/modules/Telemetry/UnitConversions.h b/src/modules/Telemetry/UnitConversions.h index 4fa7ca143..638476315 100644 --- a/src/modules/Telemetry/UnitConversions.h +++ b/src/modules/Telemetry/UnitConversions.h @@ -1,9 +1,10 @@ #pragma once -class UnitConversions { -public: - static float CelsiusToFahrenheit(float celsius); - static float MetersPerSecondToKnots(float metersPerSecond); - static float MetersPerSecondToMilesPerHour(float metersPerSecond); - static float HectoPascalToInchesOfMercury(float hectoPascal); +class UnitConversions +{ + public: + static float CelsiusToFahrenheit(float celsius); + static float MetersPerSecondToKnots(float metersPerSecond); + static float MetersPerSecondToMilesPerHour(float metersPerSecond); + static float HectoPascalToInchesOfMercury(float hectoPascal); }; diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index 1d07435b0..7f889e087 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -11,35 +11,39 @@ #include "main.h" TextMessageModule *textMessageModule; -ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { +ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - auto &p = mp.decoded; - LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); + auto &p = mp.decoded; + LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif - // We only store/display messages destined for us. - devicestate.rx_text_message = mp; - devicestate.has_rx_text_message = true; - IF_SCREEN( - // Guard against running in MeshtasticUI or with no screen - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - // Store in the central message history - const StoredMessage &sm = messageStore.addFromPacket(mp); + // We only store/display messages destined for us. + devicestate.rx_text_message = mp; + devicestate.has_rx_text_message = true; + IF_SCREEN( + // Guard against running in MeshtasticUI or with no screen + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // Store in the central message history + const StoredMessage &sm = messageStore.addFromPacket(mp); - // Pass message to renderer (banner + thread switching + scroll reset) - // Use the global Screen singleton to retrieve the current OLED display - auto *display = screen ? screen->getDisplayDevice() : nullptr; - graphics::MessageRenderer::handleNewMessage(display, sm, mp); - }) - // Only trigger screen wake if configuration allows it - if (shouldWakeOnReceivedMessage()) { - powerFSM.trigger(EVENT_RECEIVED_MSG); - } + // Pass message to renderer (banner + thread switching + scroll reset) + // Use the global Screen singleton to retrieve the current OLED display + auto *display = screen ? screen->getDisplayDevice() : nullptr; + graphics::MessageRenderer::handleNewMessage(display, sm, mp); + }) + // Only trigger screen wake if configuration allows it + if (shouldWakeOnReceivedMessage()) { + powerFSM.trigger(EVENT_RECEIVED_MSG); + } - // Notify any observers (e.g. external modules that care about packets) - notifyObservers(&mp); + // Notify any observers (e.g. external modules that care about packets) + notifyObservers(&mp); - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } -bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } +bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) +{ + return MeshService::isTextPayload(p); +} diff --git a/src/modules/TextMessageModule.h b/src/modules/TextMessageModule.h index 791ceb88a..e719f1abc 100644 --- a/src/modules/TextMessageModule.h +++ b/src/modules/TextMessageModule.h @@ -11,21 +11,22 @@ * * Rendering of messages on screen is no longer done here. */ -class TextMessageModule : public SinglePortModule, public Observable { -public: - /** Constructor - * name is for debugging output - */ - TextMessageModule() : SinglePortModule("text", meshtastic_PortNum_TEXT_MESSAGE_APP) {} +class TextMessageModule : public SinglePortModule, public Observable +{ + public: + /** Constructor + * name is for debugging output + */ + TextMessageModule() : SinglePortModule("text", meshtastic_PortNum_TEXT_MESSAGE_APP) {} -protected: - /** Called to handle a particular incoming message - * - * @return ProcessMessage::STOP if you've guaranteed you've handled this - * message and no other handlers should be considered for it. - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - virtual bool wantPacket(const meshtastic_MeshPacket *p) override; + protected: + /** Called to handle a particular incoming message + * + * @return ProcessMessage::STOP if you've guaranteed you've handled this + * message and no other handlers should be considered for it. + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual bool wantPacket(const meshtastic_MeshPacket *p) override; }; extern TextMessageModule *textMessageModule; \ No newline at end of file diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 57f9ebd8a..41dc02cd1 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -12,863 +12,887 @@ extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; -void TraceRouteModule::setResultText(const String &text) { - resultText = text; - resultLines.clear(); - resultLinesDirty = true; +void TraceRouteModule::setResultText(const String &text) +{ + resultText = text; + resultLines.clear(); + resultLinesDirty = true; } -void TraceRouteModule::clearResultLines() { - resultLines.clear(); - resultLinesDirty = false; +void TraceRouteModule::clearResultLines() +{ + resultLines.clear(); + resultLinesDirty = false; } #if HAS_SCREEN -void TraceRouteModule::rebuildResultLines(OLEDDisplay *display) { - if (!display) { - resultLinesDirty = false; - return; - } - - resultLines.clear(); - - if (resultText.length() == 0) { - resultLinesDirty = false; - return; - } - - int maxWidth = display->getWidth() - 4; - if (maxWidth <= 0) { - resultLinesDirty = false; - return; - } - - int start = 0; - int textLength = resultText.length(); - - while (start <= textLength) { - int newlinePos = resultText.indexOf('\n', start); - String segment; - - if (newlinePos != -1) { - segment = resultText.substring(start, newlinePos); - start = newlinePos + 1; - } else { - segment = resultText.substring(start); - start = textLength + 1; +void TraceRouteModule::rebuildResultLines(OLEDDisplay *display) +{ + if (!display) { + resultLinesDirty = false; + return; } - if (segment.length() == 0) { - resultLines.push_back(""); - continue; + resultLines.clear(); + + if (resultText.length() == 0) { + resultLinesDirty = false; + return; } - if (display->getStringWidth(segment) <= maxWidth) { - resultLines.push_back(segment); - continue; + int maxWidth = display->getWidth() - 4; + if (maxWidth <= 0) { + resultLinesDirty = false; + return; } - String remaining = segment; + int start = 0; + int textLength = resultText.length(); - while (remaining.length() > 0) { - String tempLine = ""; - int lastGoodBreak = -1; - bool lineComplete = false; + while (start <= textLength) { + int newlinePos = resultText.indexOf('\n', start); + String segment; - for (int i = 0; i < static_cast(remaining.length()); i++) { - char ch = remaining.charAt(i); - String testLine = tempLine + ch; - - if (display->getStringWidth(testLine) > maxWidth) { - if (lastGoodBreak >= 0) { - resultLines.push_back(remaining.substring(0, lastGoodBreak + 1)); - remaining = remaining.substring(lastGoodBreak + 1); - lineComplete = true; - break; - } else if (tempLine.length() > 0) { - resultLines.push_back(tempLine); - remaining = remaining.substring(i); - lineComplete = true; - break; - } else { - resultLines.push_back(String(ch)); - remaining = remaining.substring(i + 1); - lineComplete = true; - break; - } + if (newlinePos != -1) { + segment = resultText.substring(start, newlinePos); + start = newlinePos + 1; } else { - tempLine = testLine; - if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') { - lastGoodBreak = i; - } + segment = resultText.substring(start); + start = textLength + 1; } - } - if (!lineComplete) { - if (tempLine.length() > 0) { - resultLines.push_back(tempLine); + if (segment.length() == 0) { + resultLines.push_back(""); + continue; + } + + if (display->getStringWidth(segment) <= maxWidth) { + resultLines.push_back(segment); + continue; + } + + String remaining = segment; + + while (remaining.length() > 0) { + String tempLine = ""; + int lastGoodBreak = -1; + bool lineComplete = false; + + for (int i = 0; i < static_cast(remaining.length()); i++) { + char ch = remaining.charAt(i); + String testLine = tempLine + ch; + + if (display->getStringWidth(testLine) > maxWidth) { + if (lastGoodBreak >= 0) { + resultLines.push_back(remaining.substring(0, lastGoodBreak + 1)); + remaining = remaining.substring(lastGoodBreak + 1); + lineComplete = true; + break; + } else if (tempLine.length() > 0) { + resultLines.push_back(tempLine); + remaining = remaining.substring(i); + lineComplete = true; + break; + } else { + resultLines.push_back(String(ch)); + remaining = remaining.substring(i + 1); + lineComplete = true; + break; + } + } else { + tempLine = testLine; + if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') { + lastGoodBreak = i; + } + } + } + + if (!lineComplete) { + if (tempLine.length() > 0) { + resultLines.push_back(tempLine); + } + break; + } } - break; - } } - } - resultLinesDirty = false; + resultLinesDirty = false; } #endif -bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) { - // We only alter the packet in alterReceivedProtobuf() - return false; // let it be handled by RoutingModule +bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) +{ + // We only alter the packet in alterReceivedProtobuf() + return false; // let it be handled by RoutingModule } -void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { - const meshtastic_Data &incoming = p.decoded; +void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) +{ + const meshtastic_Data &incoming = p.decoded; - // Update next-hops using returned route - if (incoming.request_id) { - updateNextHops(p, r); - } - - // Insert unknown hops if necessary - insertUnknownHops(p, r, !incoming.request_id); - - // Append ID and SNR. If the last hop is to us, we only need to append the SNR - appendMyIDandSNR(r, p.rx_snr, !incoming.request_id, isToUs(&p)); - if (!incoming.request_id) - printRoute(r, p.from, p.to, true); - else - printRoute(r, p.to, p.from, false); - - // Set updated route to the payload of the to be flooded packet - p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); - - if (tracingNode != 0) { - // check isResponseFromTarget - bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); - bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); - - // Check if this is a trace route response containing our target node - bool containsTargetNode = false; - for (uint8_t i = 0; i < r->route_count; i++) { - if (r->route[i] == tracingNode) { - containsTargetNode = true; - break; - } - } - for (uint8_t i = 0; i < r->route_back_count; i++) { - if (r->route_back[i] == tracingNode) { - containsTargetNode = true; - break; - } + // Update next-hops using returned route + if (incoming.request_id) { + updateNextHops(p, r); } - // Check if this response contains a complete route to our target - bool hasCompleteRoute = - (r->route_count > 0 && r->route_back_count > 0) || (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); + // Insert unknown hops if necessary + insertUnknownHops(p, r, !incoming.request_id); - LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, p.from, p.to, - incoming.request_id); - LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", isResponseFromTarget, - isRequestToUs, containsTargetNode, hasCompleteRoute); + // Append ID and SNR. If the last hop is to us, we only need to append the SNR + appendMyIDandSNR(r, p.rx_snr, !incoming.request_id, isToUs(&p)); + if (!incoming.request_id) + printRoute(r, p.from, p.to, true); + else + printRoute(r, p.to, p.from, false); - if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { - LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); + // Set updated route to the payload of the to be flooded packet + p.decoded.payload.size = + pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); - LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); - for (int i = 0; i < r->snr_towards_count; i++) { - LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); - } - for (int i = 0; i < r->snr_back_count; i++) { - LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); - } + if (tracingNode != 0) { + // check isResponseFromTarget + bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); + bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); - String result = ""; - - // Show request path (from initiator to target) - if (r->route_count > 0) { - result += getNodeName(nodeDB->getNodeNum()); + // Check if this is a trace route response containing our target node + bool containsTargetNode = false; for (uint8_t i = 0; i < r->route_count; i++) { - result += " > "; - const char *name = getNodeName(r->route[i]); - float snr = (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; - result += name; - if (snr != 0.0f) { - result += "("; - result += String(snr, 1); - result += "dB)"; - } + if (r->route[i] == tracingNode) { + containsTargetNode = true; + break; + } } - result += " > "; - result += getNodeName(tracingNode); - if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { - result += "("; - result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); - result += "dB)"; + for (uint8_t i = 0; i < r->route_back_count; i++) { + if (r->route_back[i] == tracingNode) { + containsTargetNode = true; + break; + } } - result += "\n"; - } else { - // Direct connection (no intermediate hops) - result += getNodeName(nodeDB->getNodeNum()); - result += " > "; - result += getNodeName(tracingNode); - if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { - result += "("; - result += String((float)r->snr_towards[0] / 4.0f, 1); - result += "dB)"; - } - result += "\n"; - } - // Show response path (from target back to initiator) - if (r->route_back_count > 0) { - result += getNodeName(tracingNode); - for (int8_t i = r->route_back_count - 1; i >= 0; i--) { - result += " > "; - const char *name = getNodeName(r->route_back[i]); - float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; - result += name; - if (snr != 0.0f) { - result += "("; - result += String(snr, 1); - result += "dB)"; - } - } - // add initiator node - result += " > "; - result += getNodeName(nodeDB->getNodeNum()); - if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { - result += "("; - result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); - result += "dB)"; - } - } else { - // Direct return path (no intermediate hops) - result += getNodeName(tracingNode); - result += " > "; - result += getNodeName(nodeDB->getNodeNum()); - if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { - result += "("; - result += String((float)r->snr_back[0] / 4.0f, 1); - result += "dB)"; - } - } + // Check if this response contains a complete route to our target + bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) || + (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); - LOG_INFO("Trace route result: %s", result.c_str()); - handleTraceRouteResult(result); + LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, + p.from, p.to, incoming.request_id); + LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", + isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute); + + if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { + LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); + + LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); + for (int i = 0; i < r->snr_towards_count; i++) { + LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); + } + for (int i = 0; i < r->snr_back_count; i++) { + LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); + } + + String result = ""; + + // Show request path (from initiator to target) + if (r->route_count > 0) { + result += getNodeName(nodeDB->getNodeNum()); + for (uint8_t i = 0; i < r->route_count; i++) { + result += " > "; + const char *name = getNodeName(r->route[i]); + float snr = + (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } else { + // Direct connection (no intermediate hops) + result += getNodeName(nodeDB->getNodeNum()); + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[0] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } + + // Show response path (from target back to initiator) + if (r->route_back_count > 0) { + result += getNodeName(tracingNode); + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + result += " > "; + const char *name = getNodeName(r->route_back[i]); + float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + // add initiator node + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); + result += "dB)"; + } + } else { + // Direct return path (no intermediate hops) + result += getNodeName(tracingNode); + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[0] / 4.0f, 1); + result += "dB)"; + } + } + + LOG_INFO("Trace route result: %s", result.c_str()); + handleTraceRouteResult(result); + } } - } } -void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { - // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D - // Similarly, if we are C, we can set D as next-hop for D - // If we are A, we can set B as next-hop for B, C and D +void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) +{ + // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D + // Similarly, if we are C, we can set D as next-hop for D + // If we are A, we can set B as next-hop for B, C and D - // First check if we were the original sender or in the original route - int8_t nextHopIndex = -1; - if (isToUs(&p)) { - nextHopIndex = 0; // We are the original sender, next hop is first in route - } else { - // Check if we are in the original route - for (uint8_t i = 0; i < r->route_count; i++) { - if (r->route[i] == nodeDB->getNodeNum()) { - nextHopIndex = i + 1; // Next hop is the one after us - break; - } - } - } - - // If we are in the original route, update the next hops - if (nextHopIndex != -1) { - // For every node after us, we can set the next-hop to the first node after us - NodeNum nextHop; - if (nextHopIndex == r->route_count) { - nextHop = p.from; // We are the last in the route, next hop is destination + // First check if we were the original sender or in the original route + int8_t nextHopIndex = -1; + if (isToUs(&p)) { + nextHopIndex = 0; // We are the original sender, next hop is first in route } else { - nextHop = r->route[nextHopIndex]; + // Check if we are in the original route + for (uint8_t i = 0; i < r->route_count; i++) { + if (r->route[i] == nodeDB->getNodeNum()) { + nextHopIndex = i + 1; // Next hop is the one after us + break; + } + } } - if (nextHop == NODENUM_BROADCAST) { - return; - } - uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop); + // If we are in the original route, update the next hops + if (nextHopIndex != -1) { + // For every node after us, we can set the next-hop to the first node after us + NodeNum nextHop; + if (nextHopIndex == r->route_count) { + nextHop = p.from; // We are the last in the route, next hop is destination + } else { + nextHop = r->route[nextHopIndex]; + } - // For the rest of the nodes in the route, set their next-hop - // Note: if we are the last in the route, this loop will not run - for (int8_t i = nextHopIndex; i < r->route_count; i++) { - NodeNum targetNode = r->route[i]; - maybeSetNextHop(targetNode, nextHopByte); - } + if (nextHop == NODENUM_BROADCAST) { + return; + } + uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop); - // Also set next-hop for the destination node - maybeSetNextHop(p.from, nextHopByte); - } + // For the rest of the nodes in the route, set their next-hop + // Note: if we are the last in the route, this loop will not run + for (int8_t i = nextHopIndex; i < r->route_count; i++) { + NodeNum targetNode = r->route[i]; + maybeSetNextHop(targetNode, nextHopByte); + } + + // Also set next-hop for the destination node + maybeSetNextHop(p.from, nextHopByte); + } } -void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte) { - if (target == NODENUM_BROADCAST) - return; +void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte) +{ + if (target == NODENUM_BROADCAST) + return; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target); - if (node && node->next_hop != nextHopByte) { - LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte); - node->next_hop = nextHopByte; - } -} - -void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp) { - if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP) - return; - - meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero; - if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &decoded)) - return; - - handleReceivedProtobuf(mp, &decoded); - // Intentionally modify the packet in-place so downstream relays see our updates. - alterReceivedProtobuf(const_cast(mp), &decoded); -} - -void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) { - pb_size_t *route_count; - uint32_t *route; - pb_size_t *snr_count; - int8_t *snr_list; - - // Pick the correct route array and SNR list - if (isTowardsDestination) { - route_count = &r->route_count; - route = r->route; - snr_count = &r->snr_towards_count; - snr_list = r->snr_towards; - } else { - route_count = &r->route_back_count; - route = r->route_back; - snr_count = &r->snr_back_count; - snr_list = r->snr_back; - } - - // Only insert unknown hops if hop_start is valid - const int8_t hopsTaken = getHopsAway(p); - if (hopsTaken >= 0) { - int8_t diff = hopsTaken - *route_count; - for (int8_t i = 0; i < diff; i++) { - if (*route_count < ROUTE_SIZE) { - route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop - *route_count += 1; - } + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target); + if (node && node->next_hop != nextHopByte) { + LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte); + node->next_hop = nextHopByte; } - // Add unknown SNR values if necessary - diff = *route_count - *snr_count; - for (int8_t i = 0; i < diff; i++) { - if (*snr_count < ROUTE_SIZE) { - snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR +} + +void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp) +{ + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP) + return; + + meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero; + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &decoded)) + return; + + handleReceivedProtobuf(mp, &decoded); + // Intentionally modify the packet in-place so downstream relays see our updates. + alterReceivedProtobuf(const_cast(mp), &decoded); +} + +void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) +{ + pb_size_t *route_count; + uint32_t *route; + pb_size_t *snr_count; + int8_t *snr_list; + + // Pick the correct route array and SNR list + if (isTowardsDestination) { + route_count = &r->route_count; + route = r->route; + snr_count = &r->snr_towards_count; + snr_list = r->snr_towards; + } else { + route_count = &r->route_back_count; + route = r->route_back; + snr_count = &r->snr_back_count; + snr_list = r->snr_back; + } + + // Only insert unknown hops if hop_start is valid + const int8_t hopsTaken = getHopsAway(p); + if (hopsTaken >= 0) { + int8_t diff = hopsTaken - *route_count; + for (int8_t i = 0; i < diff; i++) { + if (*route_count < ROUTE_SIZE) { + route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop + *route_count += 1; + } + } + // Add unknown SNR values if necessary + diff = *route_count - *snr_count; + for (int8_t i = 0; i < diff; i++) { + if (*snr_count < ROUTE_SIZE) { + snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR + *snr_count += 1; + } + } + } +} + +void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, float snr, bool isTowardsDestination, bool SNRonly) +{ + pb_size_t *route_count; + uint32_t *route; + pb_size_t *snr_count; + int8_t *snr_list; + + // Pick the correct route array and SNR list + if (isTowardsDestination) { + route_count = &updated->route_count; + route = updated->route; + snr_count = &updated->snr_towards_count; + snr_list = updated->snr_towards; + } else { + route_count = &updated->route_back_count; + route = updated->route_back; + snr_count = &updated->snr_back_count; + snr_list = updated->snr_back; + } + + if (*snr_count < ROUTE_SIZE) { + snr_list[*snr_count] = (int8_t)(snr * 4); // Convert SNR to 1 byte *snr_count += 1; - } } - } + if (SNRonly) + return; + + // Length of route array can normally not be exceeded due to the max. hop_limit of 7 + if (*route_count < ROUTE_SIZE) { + route[*route_count] = myNodeInfo.my_node_num; + *route_count += 1; + } else { + LOG_WARN("Route exceeded maximum hop limit!"); // Are you bridging networks? + } } -void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, float snr, bool isTowardsDestination, bool SNRonly) { - pb_size_t *route_count; - uint32_t *route; - pb_size_t *snr_count; - int8_t *snr_list; - - // Pick the correct route array and SNR list - if (isTowardsDestination) { - route_count = &updated->route_count; - route = updated->route; - snr_count = &updated->snr_towards_count; - snr_list = updated->snr_towards; - } else { - route_count = &updated->route_back_count; - route = updated->route_back; - snr_count = &updated->snr_back_count; - snr_list = updated->snr_back; - } - - if (*snr_count < ROUTE_SIZE) { - snr_list[*snr_count] = (int8_t)(snr * 4); // Convert SNR to 1 byte - *snr_count += 1; - } - if (SNRonly) - return; - - // Length of route array can normally not be exceeded due to the max. hop_limit of 7 - if (*route_count < ROUTE_SIZE) { - route[*route_count] = myNodeInfo.my_node_num; - *route_count += 1; - } else { - LOG_WARN("Route exceeded maximum hop limit!"); // Are you bridging networks? - } -} - -void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) { +void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) +{ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - std::string route = "Route traced:\n"; - route += vformat("0x%x --> ", origin); - for (uint8_t i = 0; i < r->route_count; i++) { - if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) - route += vformat("0x%x (%.2fdB) --> ", r->route[i], (float)r->snr_towards[i] / 4); - else - route += vformat("0x%x (?dB) --> ", r->route[i]); - } - // If we are the destination, or it has already reached the destination, print it - if (dest == nodeDB->getNodeNum() || !isTowardsDestination) { - if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) - route += vformat("0x%x (%.2fdB)", dest, (float)r->snr_towards[r->snr_towards_count - 1] / 4); - - else - route += vformat("0x%x (?dB)", dest); - } else - route += "..."; - - // If there's a route back (or we are the destination as then the route is complete), print it - if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) { - route += "\n"; - if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum()) - route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin); - else - route += "..."; - - for (int8_t i = r->route_back_count - 1; i >= 0; i--) { - if (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) - route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[i] / 4, r->route_back[i]); - else - route += vformat("(?dB) 0x%x <-- ", r->route_back[i]); + std::string route = "Route traced:\n"; + route += vformat("0x%x --> ", origin); + for (uint8_t i = 0; i < r->route_count; i++) { + if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) + route += vformat("0x%x (%.2fdB) --> ", r->route[i], (float)r->snr_towards[i] / 4); + else + route += vformat("0x%x (?dB) --> ", r->route[i]); } - route += vformat("0x%x", dest); - } - LOG_INFO(route.c_str()); + // If we are the destination, or it has already reached the destination, print it + if (dest == nodeDB->getNodeNum() || !isTowardsDestination) { + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) + route += vformat("0x%x (%.2fdB)", dest, (float)r->snr_towards[r->snr_towards_count - 1] / 4); + + else + route += vformat("0x%x (?dB)", dest); + } else + route += "..."; + + // If there's a route back (or we are the destination as then the route is complete), print it + if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) { + route += "\n"; + if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum()) + route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin); + else + route += "..."; + + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + if (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) + route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[i] / 4, r->route_back[i]); + else + route += vformat("(?dB) 0x%x <-- ", r->route_back[i]); + } + route += vformat("0x%x", dest); + } + LOG_INFO(route.c_str()); #endif } -meshtastic_MeshPacket *TraceRouteModule::allocReply() { - assert(currentRequest); +meshtastic_MeshPacket *TraceRouteModule::allocReply() +{ + assert(currentRequest); - // Ignore multi-hop broadcast requests - if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { - ignoreRequest = true; - return NULL; - } + // Ignore multi-hop broadcast requests + if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { + ignoreRequest = true; + return NULL; + } - // Copy the payload of the current request - auto req = *currentRequest; - const auto &p = req.decoded; - meshtastic_RouteDiscovery scratch; - meshtastic_RouteDiscovery *updated = NULL; - memset(&scratch, 0, sizeof(scratch)); - pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_RouteDiscovery_msg, &scratch); - updated = &scratch; + // Copy the payload of the current request + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *updated = NULL; + memset(&scratch, 0, sizeof(scratch)); + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_RouteDiscovery_msg, &scratch); + updated = &scratch; - // Create a MeshPacket with this payload and set it as the reply - meshtastic_MeshPacket *reply = allocDataProtobuf(*updated); + // Create a MeshPacket with this payload and set it as the reply + meshtastic_MeshPacket *reply = allocDataProtobuf(*updated); - return reply; + return reply; } TraceRouteModule::TraceRouteModule() - : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") { - ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; - isPromiscuous = true; // We need to update the route even if it is not destined to us + : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") +{ + ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; + isPromiscuous = true; // We need to update the route even if it is not destined to us } -const char *TraceRouteModule::getNodeName(NodeNum node) { - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info && info->has_user) { - if (strlen(info->user.short_name) > 0) { - return info->user.short_name; +const char *TraceRouteModule::getNodeName(NodeNum node) +{ + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user) { + if (strlen(info->user.short_name) > 0) { + return info->user.short_name; + } + if (strlen(info->user.long_name) > 0) { + return info->user.long_name; + } } - if (strlen(info->user.long_name) > 0) { - return info->user.long_name; - } - } - static char fallback[12]; - snprintf(fallback, sizeof(fallback), "0x%08x", node); - return fallback; + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; } -bool TraceRouteModule::startTraceRoute(NodeNum node) { - LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); - unsigned long now = millis(); +bool TraceRouteModule::startTraceRoute(NodeNum node) +{ + LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); + unsigned long now = millis(); - if (node == 0 || node == NODENUM_BROADCAST) { - LOG_ERROR("Invalid node number for trace route: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Invalid node"); - resultShowTime = millis(); - tracingNode = 0; + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Invalid node"); + resultShowTime = millis(); + tracingNode = 0; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return false; - } + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } - if (node == nodeDB->getNodeNum()) { - LOG_ERROR("Cannot trace route to self: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Cannot trace self"); - resultShowTime = millis(); - tracingNode = 0; + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Cannot trace self"); + resultShowTime = millis(); + tracingNode = 0; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return false; - } + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } - if (!initialized) { - lastTraceRouteTime = 0; - initialized = true; - LOG_INFO("TraceRoute initialized for first time"); - } + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } - if (runState == TRACEROUTE_STATE_TRACKING) { - LOG_INFO("TraceRoute already in progress"); - return false; - } + if (runState == TRACEROUTE_STATE_TRACKING) { + LOG_INFO("TraceRoute already in progress"); + return false; + } - if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { - // Cooldown - unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; - bannerText = String("Wait for ") + String(wait) + String("s"); - runState = TRACEROUTE_STATE_COOLDOWN; + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + // Cooldown + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + resultText = ""; + clearResultLines(); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return false; + } + + tracingNode = node; + lastTraceRouteTime = now; + runState = TRACEROUTE_STATE_TRACKING; resultText = ""; clearResultLines(); + bannerText = String("Tracing ") + getNodeName(node); + LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); + + // 请求焦点,然后触发UI更新事件 requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); - LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); - return false; - } - tracingNode = node; - lastTraceRouteTime = now; - runState = TRACEROUTE_STATE_TRACKING; - resultText = ""; - clearResultLines(); - bannerText = String("Tracing ") + getNodeName(node); + // 设置定时器来处理超时检查 + setIntervalFromNow(1000); // 每秒检查一次状态 - LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); - // 请求焦点,然后触发UI更新事件 - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); + // Allocate a packet directly from router like the reference code + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + // Set destination and port + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; - // 设置定时器来处理超时检查 - setIntervalFromNow(1000); // 每秒检查一次状态 + // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) + p->want_ack = true; - meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; - LOG_INFO("Creating RouteDiscovery protobuf..."); + // Manually encode the RouteDiscovery payload + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); - // Allocate a packet directly from router like the reference code - meshtastic_MeshPacket *p = router->allocForSending(); - if (p) { - // Set destination and port - p->to = node; - p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; - p->decoded.want_response = true; + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + LOG_INFO("About to call service->sendToMesh..."); - // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) - p->want_ack = true; + if (service) { + LOG_INFO("MeshService is available, sending packet..."); + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Service unavailable"); + resultShowTime = millis(); + tracingNode = 0; - // Manually encode the RouteDiscovery payload - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); - - LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum, - p->decoded.want_response, p->decoded.payload.size); - LOG_INFO("About to call service->sendToMesh..."); - - if (service) { - LOG_INFO("MeshService is available, sending packet..."); - service->sendToMesh(p, RX_SRC_USER); - LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } } else { - LOG_ERROR("MeshService is NULL!"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Service unavailable"); - resultShowTime = millis(); - tracingNode = 0; + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Failed to send"); + resultShowTime = millis(); + tracingNode = 0; - requestFocus(); - UIFrameEvent e2; - e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e2); - return false; + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; } - } else { - LOG_ERROR("Failed to allocate TraceRoute packet from router"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Failed to send"); - resultShowTime = millis(); - tracingNode = 0; - - requestFocus(); - UIFrameEvent e2; - e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e2); - return false; - } - return true; + return true; } -void TraceRouteModule::launch(NodeNum node) { - if (node == 0 || node == NODENUM_BROADCAST) { - LOG_ERROR("Invalid node number for trace route: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Invalid node"); - resultShowTime = millis(); - tracingNode = 0; +void TraceRouteModule::launch(NodeNum node) +{ + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Invalid node"); + resultShowTime = millis(); + tracingNode = 0; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return; - } + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } - if (node == nodeDB->getNodeNum()) { - LOG_ERROR("Cannot trace route to self: 0x%08x", node); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Cannot trace self"); - resultShowTime = millis(); - tracingNode = 0; + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Cannot trace self"); + resultShowTime = millis(); + tracingNode = 0; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return; - } + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } - if (!initialized) { - lastTraceRouteTime = 0; - initialized = true; - LOG_INFO("TraceRoute initialized for first time"); - } + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } - unsigned long now = millis(); - if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { - unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; - bannerText = String("Wait for ") + String(wait) + String("s"); - runState = TRACEROUTE_STATE_COOLDOWN; + unsigned long now = millis(); + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + resultText = ""; + clearResultLines(); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return; + } + + runState = TRACEROUTE_STATE_TRACKING; + tracingNode = node; + lastTraceRouteTime = now; resultText = ""; clearResultLines(); + bannerText = String("Tracing ") + getNodeName(node); + + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(1000); + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) + p->want_ack = true; + + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + + if (service) { + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Service unavailable"); + resultShowTime = millis(); + tracingNode = 0; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("Failed to send"); + resultShowTime = millis(); + tracingNode = 0; + } +} + +void TraceRouteModule::handleTraceRouteResult(const String &result) +{ + setResultText(result); + runState = TRACEROUTE_STATE_RESULT; + resultShowTime = millis(); + tracingNode = 0; + + LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); + + setIntervalFromNow(1000); requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); - LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); - return; - } - runState = TRACEROUTE_STATE_TRACKING; - tracingNode = node; - lastTraceRouteTime = now; - resultText = ""; - clearResultLines(); - bannerText = String("Tracing ") + getNodeName(node); + LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); +} - requestFocus(); - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - setIntervalFromNow(1000); - - meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; - LOG_INFO("Creating RouteDiscovery protobuf..."); - - meshtastic_MeshPacket *p = router->allocForSending(); - if (p) { - p->to = node; - p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; - p->decoded.want_response = true; - - // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) - p->want_ack = true; - - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); - - LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum, - p->decoded.want_response, p->decoded.payload.size); - - if (service) { - service->sendToMesh(p, RX_SRC_USER); - LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); - } else { - LOG_ERROR("MeshService is NULL!"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Service unavailable"); - resultShowTime = millis(); - tracingNode = 0; +bool TraceRouteModule::shouldDraw() +{ + bool draw = (runState != TRACEROUTE_STATE_IDLE); + static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; + if (runState != lastLoggedState) { + LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); + lastLoggedState = runState; } - } else { - LOG_ERROR("Failed to allocate TraceRoute packet from router"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("Failed to send"); - resultShowTime = millis(); - tracingNode = 0; - } -} - -void TraceRouteModule::handleTraceRouteResult(const String &result) { - setResultText(result); - runState = TRACEROUTE_STATE_RESULT; - resultShowTime = millis(); - tracingNode = 0; - - LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); - - setIntervalFromNow(1000); - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); -} - -bool TraceRouteModule::shouldDraw() { - bool draw = (runState != TRACEROUTE_STATE_IDLE); - static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; - if (runState != lastLoggedState) { - LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); - lastLoggedState = runState; - } - return draw; + return draw; } #if HAS_SCREEN -void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); +void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); - display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setTextAlignment(TEXT_ALIGN_CENTER); - if (runState == TRACEROUTE_STATE_TRACKING) { - display->setFont(FONT_MEDIUM); - int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); - display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + if (runState == TRACEROUTE_STATE_TRACKING) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); - } else if (runState == TRACEROUTE_STATE_RESULT) { - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); + } else if (runState == TRACEROUTE_STATE_RESULT) { + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(x, y, "Route Result"); + display->drawString(x, y, "Route Result"); - int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - if (resultText.length() > 0) { - if (resultLinesDirty) { - rebuildResultLines(display); - } + if (resultText.length() > 0) { + if (resultLinesDirty) { + rebuildResultLines(display); + } - int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing - for (size_t i = 0; i < resultLines.size(); i++) { - int lineY = contentStartY + (i * lineHeight); - if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { - display->drawString(x + 2, lineY, resultLines[i]); + int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing + for (size_t i = 0; i < resultLines.size(); i++) { + int lineY = contentStartY + (i * lineHeight); + if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { + display->drawString(x + 2, lineY, resultLines[i]); + } + } } - } - } - } else if (runState == TRACEROUTE_STATE_COOLDOWN) { - display->setFont(FONT_MEDIUM); - int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); - display->drawString(display->getWidth() / 2 + x, centerY, bannerText); - } + } else if (runState == TRACEROUTE_STATE_COOLDOWN) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + } } #endif // HAS_SCREEN -int32_t TraceRouteModule::runOnce() { - unsigned long now = millis(); +int32_t TraceRouteModule::runOnce() +{ + unsigned long now = millis(); + + if (runState == TRACEROUTE_STATE_IDLE) { + return INT32_MAX; + } + + // Check for tracking timeout + if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { + LOG_INFO("TraceRoute timeout, no response received"); + runState = TRACEROUTE_STATE_RESULT; + setResultText("No response received"); + resultShowTime = now; + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(resultDisplayMs); + return resultDisplayMs; + } + + // Update cooldown display every second + if (runState == TRACEROUTE_STATE_COOLDOWN) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + if (wait > 0) { + String newBannerText = String("Wait for ") + String(wait) + String("s"); + bannerText = newBannerText; + LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); + + // Force flash UI + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + if (screen) { + screen->forceDisplay(); + } + + return 1000; + } else { + // Cooldown finished + LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + clearResultLines(); + bannerText = ""; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } + } + + if (runState == TRACEROUTE_STATE_RESULT) { + if (now - resultShowTime >= resultDisplayMs) { + LOG_INFO("TraceRoute result display timeout, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + clearResultLines(); + bannerText = ""; + tracingNode = 0; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } else { + return 1000; + } + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + return 1000; + } - if (runState == TRACEROUTE_STATE_IDLE) { return INT32_MAX; - } - - // Check for tracking timeout - if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { - LOG_INFO("TraceRoute timeout, no response received"); - runState = TRACEROUTE_STATE_RESULT; - setResultText("No response received"); - resultShowTime = now; - tracingNode = 0; - - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - setIntervalFromNow(resultDisplayMs); - return resultDisplayMs; - } - - // Update cooldown display every second - if (runState == TRACEROUTE_STATE_COOLDOWN) { - unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; - if (wait > 0) { - String newBannerText = String("Wait for ") + String(wait) + String("s"); - bannerText = newBannerText; - LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); - - // Force flash UI - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - - if (screen) { - screen->forceDisplay(); - } - - return 1000; - } else { - // Cooldown finished - LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); - runState = TRACEROUTE_STATE_IDLE; - resultText = ""; - clearResultLines(); - bannerText = ""; - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return INT32_MAX; - } - } - - if (runState == TRACEROUTE_STATE_RESULT) { - if (now - resultShowTime >= resultDisplayMs) { - LOG_INFO("TraceRoute result display timeout, returning to IDLE"); - runState = TRACEROUTE_STATE_IDLE; - resultText = ""; - clearResultLines(); - bannerText = ""; - tracingNode = 0; - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return INT32_MAX; - } else { - return 1000; - } - } - - if (runState == TRACEROUTE_STATE_TRACKING) { - return 1000; - } - - return INT32_MAX; } diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index edde36638..a40ed7733 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -16,71 +16,74 @@ */ enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN }; -class TraceRouteModule : public ProtobufModule, public Observable, private concurrency::OSThread { -public: - TraceRouteModule(); +class TraceRouteModule : public ProtobufModule, + public Observable, + private concurrency::OSThread +{ + public: + TraceRouteModule(); - bool startTraceRoute(NodeNum node); - void launch(NodeNum node); - void handleTraceRouteResult(const String &result); - bool shouldDraw(); + bool startTraceRoute(NodeNum node); + void launch(NodeNum node); + void handleTraceRouteResult(const String &result); + bool shouldDraw(); #if HAS_SCREEN - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - const char *getNodeName(NodeNum node); + const char *getNodeName(NodeNum node); - virtual bool wantUIFrame() override { return shouldDraw(); } - virtual Observable *getUIFrameObservable() override { return this; } + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } - void processUpgradedPacket(const meshtastic_MeshPacket &mp); + void processUpgradedPacket(const meshtastic_MeshPacket &mp); -protected: - bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; + protected: + bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; - virtual meshtastic_MeshPacket *allocReply() override; + virtual meshtastic_MeshPacket *allocReply() override; - /* Called before rebroadcasting a RouteDiscovery payload in order to update - the route array containing the IDs of nodes this packet went through */ - void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; + /* Called before rebroadcasting a RouteDiscovery payload in order to update + the route array containing the IDs of nodes this packet went through */ + void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; -private: - void setResultText(const String &text); - void clearResultLines(); + private: + void setResultText(const String &text); + void clearResultLines(); #if HAS_SCREEN - void rebuildResultLines(OLEDDisplay *display); + void rebuildResultLines(OLEDDisplay *display); #endif - // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit - void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); + // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit + void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); - // Call to add your ID to the route array of a RouteDiscovery message - void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); + // Call to add your ID to the route array of a RouteDiscovery message + void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); - // Update next-hops in the routing table based on the returned route - void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); + // Update next-hops in the routing table based on the returned route + void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); - // Helper to update next-hop for a single node - void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); + // Helper to update next-hop for a single node + void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); - /* Call to print the route array of a RouteDiscovery message. - Set origin to where the request came from. - Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ - void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); + /* Call to print the route array of a RouteDiscovery message. + Set origin to where the request came from. + Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ + void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); - TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; - unsigned long lastTraceRouteTime = 0; - unsigned long resultShowTime = 0; - unsigned long cooldownMs = 30000; - unsigned long resultDisplayMs = 10000; - unsigned long trackingTimeoutMs = 10000; - String bannerText; - String resultText; - std::vector resultLines; - bool resultLinesDirty = false; - NodeNum tracingNode = 0; - bool initialized = false; + TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; + unsigned long lastTraceRouteTime = 0; + unsigned long resultShowTime = 0; + unsigned long cooldownMs = 30000; + unsigned long resultDisplayMs = 10000; + unsigned long trackingTimeoutMs = 10000; + String bannerText; + String resultText; + std::vector resultLines; + bool resultLinesDirty = false; + NodeNum tracingNode = 0; + bool initialized = false; }; extern TraceRouteModule *traceRouteModule; diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 7abe59762..4db80ba18 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -15,152 +15,164 @@ WaypointModule *waypointModule; -static inline float degToRad(float deg) { return deg * PI / 180.0f; } -static inline float radToDeg(float rad) { return rad * 180.0f / PI; } +static inline float degToRad(float deg) +{ + return deg * PI / 180.0f; +} +static inline float radToDeg(float rad) +{ + return rad * 180.0f / PI; +} -ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { +ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) +{ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - auto &p = mp.decoded; - LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); + auto &p = mp.decoded; + LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif - // We only store/display messages destined for us. - // Keep a copy of the most recent text message. - devicestate.rx_waypoint = mp; - devicestate.has_rx_waypoint = true; + // We only store/display messages destined for us. + // Keep a copy of the most recent text message. + devicestate.rx_waypoint = mp; + devicestate.has_rx_waypoint = true; - powerFSM.trigger(EVENT_RECEIVED_MSG); + powerFSM.trigger(EVENT_RECEIVED_MSG); #if HAS_SCREEN - UIFrameEvent e; + UIFrameEvent e; - // New or updated waypoint: focus on this frame next time Screen::setFrames runs - if (shouldDraw()) { - requestFocus(); - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - } + // New or updated waypoint: focus on this frame next time Screen::setFrames runs + if (shouldDraw()) { + requestFocus(); + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + } - // Deleting an old waypoint: remove the frame quietly, don't change frame position if possible - else - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND; + // Deleting an old waypoint: remove the frame quietly, don't change frame position if possible + else + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND; - notifyObservers(&e); + notifyObservers(&e); #endif - return ProcessMessage::CONTINUE; // Let others look at this message also if they want + return ProcessMessage::CONTINUE; // Let others look at this message also if they want } #if HAS_SCREEN -bool WaypointModule::shouldDraw() { +bool WaypointModule::shouldDraw() +{ #if !MESHTASTIC_EXCLUDE_WAYPOINT - if (!screen || !devicestate.has_rx_waypoint) - return false; + if (!screen || !devicestate.has_rx_waypoint) + return false; - meshtastic_Waypoint wp{}; // <- replaces memset - if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, &meshtastic_Waypoint_msg, - &wp)) { - return wp.expire > getTime(); - } - return false; // no LOG_ERROR, no flag writes + meshtastic_Waypoint wp{}; // <- replaces memset + if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, + &meshtastic_Waypoint_msg, &wp)) { + return wp.expire > getTime(); + } + return false; // no LOG_ERROR, no flag writes #else - return false; + return false; #endif } /// Draw the last waypoint we received -void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - if (!screen) - return; - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + if (!screen) + return; + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "Waypoint"; + // === Set Title + const char *titleStr = "Waypoint"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - const int w = display->getWidth(); - const int h = display->getHeight(); + const int w = display->getWidth(); + const int h = display->getHeight(); - // Decode the waypoint - const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; - meshtastic_Waypoint wp{}; - if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { - devicestate.has_rx_waypoint = false; - return; - } - - // Get timestamp info. Will pass as a field to drawColumns - char lastStr[20]; - getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); - - // Will contain distance information, passed as a field to drawColumns - char distStr[20]; - - // Get our node, to use our own position - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - - // Dimensions / co-ordinates for the compass/circle - const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); - const int16_t compassX = x + w - (compassDiam / 2) - 5; - const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - ? y + h / 2 - : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; - - // If our node has a position: - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { - const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { - myHeading = 0; - } else { - if (screen->hasHeading()) - myHeading = degToRad(screen->getHeading()); - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + // Decode the waypoint + const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; + meshtastic_Waypoint wp{}; + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { + devicestate.has_rx_waypoint = false; + return; } - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); - // Compass bearing to waypoint - float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) - bearingToOther -= myHeading; - graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + // Get timestamp info. Will pass as a field to drawColumns + char lastStr[20]; + getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); + // Will contain distance information, passed as a field to drawColumns + char distStr[20]; - // Distance to Waypoint - float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - float feet = d * METERS_TO_FEET; - snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", - feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); - } else { - snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, bearingToOtherDegrees); + // Get our node, to use our own position + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + // Dimensions / co-ordinates for the compass/circle + const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); + const int16_t compassX = x + w - (compassDiam / 2) - 5; + const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) + ? y + h / 2 + : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; + + // If our node has a position: + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { + const meshtastic_PositionLite &op = ourNode->position; + float myHeading; + if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { + myHeading = 0; + } else { + if (screen->hasHeading()) + myHeading = degToRad(screen->getHeading()); + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); + + // Compass bearing to waypoint + float bearingToOther = + GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); + // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly + // If the top of the compass is not a static north we need adjust bearingToOther based on heading + if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) + bearingToOther -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + + float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; + bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); + + // Distance to Waypoint + float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + float feet = d * METERS_TO_FEET; + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); + } else { + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, + bearingToOtherDegrees); + } } - } - else { - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + else { + display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); - // ? in the distance field - snprintf(distStr, sizeof(distStr), "? %s ?°", (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); - } + // ? in the distance field + snprintf(distStr, sizeof(distStr), "? %s ?°", + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); + } - // Draw compass circle - display->drawCircle(compassX, compassY, compassDiam / 2); + // Draw compass circle + display->drawCircle(compassX, compassY, compassDiam / 2); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! - display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); - display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); - display->drawString(0, graphics::getTextPositions(display)[line++], distStr); + display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! + display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); + display->drawString(0, graphics::getTextPositions(display)[line++], distStr); } #endif diff --git a/src/modules/WaypointModule.h b/src/modules/WaypointModule.h index f56d9d0c4..4c9c7b86b 100644 --- a/src/modules/WaypointModule.h +++ b/src/modules/WaypointModule.h @@ -5,28 +5,29 @@ /** * Waypoint message handling for meshtastic */ -class WaypointModule : public SinglePortModule, public Observable { -public: - /** Constructor - * name is for debugging output - */ - WaypointModule() : SinglePortModule("waypoint", meshtastic_PortNum_WAYPOINT_APP) {} +class WaypointModule : public SinglePortModule, public Observable +{ + public: + /** Constructor + * name is for debugging output + */ + WaypointModule() : SinglePortModule("waypoint", meshtastic_PortNum_WAYPOINT_APP) {} #if HAS_SCREEN - bool shouldDraw(); + bool shouldDraw(); #endif -protected: - /** Called to handle a particular incoming message + protected: + /** Called to handle a particular incoming message - @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - considered for it - */ + @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for + it + */ - virtual Observable *getUIFrameObservable() override { return this; } + virtual Observable *getUIFrameObservable() override { return this; } #if HAS_SCREEN - virtual bool wantUIFrame() override { return this->shouldDraw(); } - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual bool wantUIFrame() override { return this->shouldDraw(); } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; extern WaypointModule *waypointModule; \ No newline at end of file diff --git a/src/modules/esp32/AudioModule.cpp b/src/modules/esp32/AudioModule.cpp index de4e89e9f..77cc94359 100644 --- a/src/modules/esp32/AudioModule.cpp +++ b/src/modules/esp32/AudioModule.cpp @@ -9,8 +9,8 @@ /* AudioModule - A interface to send raw codec2 audio data over the mesh network. Based on the example code from the ESP32_codec2 - project. https://github.com/deulis/ESP32_Codec2 + A interface to send raw codec2 audio data over the mesh network. Based on the example code from the ESP32_codec2 project. + https://github.com/deulis/ESP32_Codec2 Codec 2 is a low-bitrate speech audio codec (speech coding) that is patent free and open source develop by David Grant Rowe. @@ -19,8 +19,8 @@ Basic Usage: 1) Enable the module by setting audio.codec2_enabled to 1. 2) Set the pins for the I2S interface. Recommended on TLora is I2S_WS 13/I2S_SD 15/I2S_SIN 2/I2S_SCK 14 - 3) Set audio.bitrate to the desired codec2 rate (CODEC2_3200, CODEC2_2400, CODEC2_1600, CODEC2_1400, - CODEC2_1300, CODEC2_1200, CODEC2_700, CODEC2_700B) + 3) Set audio.bitrate to the desired codec2 rate (CODEC2_3200, CODEC2_2400, CODEC2_1600, CODEC2_1400, CODEC2_1300, + CODEC2_1200, CODEC2_700, CODEC2_700B) KNOWN PROBLEMS * Half Duplex @@ -41,238 +41,250 @@ AudioModule *audioModule; #include "graphics/ScreenFonts.h" -void run_codec2(void *parameter) { - // 4 bytes of header in each frame hex c0 de c2 plus the bitrate - memcpy(audioModule->tx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)); +void run_codec2(void *parameter) +{ + // 4 bytes of header in each frame hex c0 de c2 plus the bitrate + memcpy(audioModule->tx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)); - LOG_INFO("Start codec2 task"); + LOG_INFO("Start codec2 task"); - while (true) { - uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(10000)); + while (true) { + uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(10000)); - if (tcount != 0) { - if (audioModule->radio_state == RadioState::tx) { - for (int i = 0; i < audioModule->adc_buffer_size; i++) - audioModule->speech[i] = (int16_t)hp_filter.Update((float)audioModule->speech[i]); + if (tcount != 0) { + if (audioModule->radio_state == RadioState::tx) { + for (int i = 0; i < audioModule->adc_buffer_size; i++) + audioModule->speech[i] = (int16_t)hp_filter.Update((float)audioModule->speech[i]); - codec2_encode(audioModule->codec2, audioModule->tx_encode_frame + audioModule->tx_encode_frame_index, audioModule->speech); - audioModule->tx_encode_frame_index += audioModule->encode_codec_size; + codec2_encode(audioModule->codec2, audioModule->tx_encode_frame + audioModule->tx_encode_frame_index, + audioModule->speech); + audioModule->tx_encode_frame_index += audioModule->encode_codec_size; - if (audioModule->tx_encode_frame_index == (audioModule->encode_frame_size + sizeof(audioModule->tx_header))) { - LOG_INFO("Send %d codec2 bytes", audioModule->encode_frame_size); - audioModule->sendPayload(); - audioModule->tx_encode_frame_index = sizeof(audioModule->tx_header); + if (audioModule->tx_encode_frame_index == (audioModule->encode_frame_size + sizeof(audioModule->tx_header))) { + LOG_INFO("Send %d codec2 bytes", audioModule->encode_frame_size); + audioModule->sendPayload(); + audioModule->tx_encode_frame_index = sizeof(audioModule->tx_header); + } + } + if (audioModule->radio_state == RadioState::rx) { + size_t bytesOut = 0; + if (memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)) == 0) { + for (int i = 4; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) { + codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); + i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut, + pdMS_TO_TICKS(500)); + } + } else { + // if the buffer header does not match our own codec, make a temp decoding setup. + CODEC2 *tmp_codec2 = codec2_create(audioModule->rx_encode_frame[3]); + codec2_set_lpc_post_filter(tmp_codec2, 1, 0, 0.8, 0.2); + int tmp_encode_codec_size = (codec2_bits_per_frame(tmp_codec2) + 7) / 8; + int tmp_adc_buffer_size = codec2_samples_per_frame(tmp_codec2); + for (int i = 4; i < audioModule->rx_encode_frame_index; i += tmp_encode_codec_size) { + codec2_decode(tmp_codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); + i2s_write(I2S_PORT, &audioModule->output_buffer, tmp_adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); + } + codec2_destroy(tmp_codec2); + } + } } - } - if (audioModule->radio_state == RadioState::rx) { - size_t bytesOut = 0; - if (memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)) == 0) { - for (int i = 4; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) { - codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); - i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); - } - } else { - // if the buffer header does not match our own codec, make a temp decoding setup. - CODEC2 *tmp_codec2 = codec2_create(audioModule->rx_encode_frame[3]); - codec2_set_lpc_post_filter(tmp_codec2, 1, 0, 0.8, 0.2); - int tmp_encode_codec_size = (codec2_bits_per_frame(tmp_codec2) + 7) / 8; - int tmp_adc_buffer_size = codec2_samples_per_frame(tmp_codec2); - for (int i = 4; i < audioModule->rx_encode_frame_index; i += tmp_encode_codec_size) { - codec2_decode(tmp_codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); - i2s_write(I2S_PORT, &audioModule->output_buffer, tmp_adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); - } - codec2_destroy(tmp_codec2); - } - } } - } } -AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_APP), concurrency::OSThread("Audio") { - // moduleConfig.audio.codec2_enabled = true; - // moduleConfig.audio.i2s_ws = 13; - // moduleConfig.audio.i2s_sd = 15; - // moduleConfig.audio.i2s_din = 22; - // moduleConfig.audio.i2s_sck = 14; - // moduleConfig.audio.ptt_pin = 39; +AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_APP), concurrency::OSThread("Audio") +{ + // moduleConfig.audio.codec2_enabled = true; + // moduleConfig.audio.i2s_ws = 13; + // moduleConfig.audio.i2s_sd = 15; + // moduleConfig.audio.i2s_din = 22; + // moduleConfig.audio.i2s_sck = 14; + // moduleConfig.audio.ptt_pin = 39; - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); - tx_header.mode = (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1; - codec2_set_lpc_post_filter(codec2, 1, 0, 0.8, 0.2); - encode_codec_size = (codec2_bits_per_frame(codec2) + 7) / 8; - encode_frame_num = (meshtastic_Constants_DATA_PAYLOAD_LEN - sizeof(tx_header)) / encode_codec_size; - encode_frame_size = encode_frame_num * encode_codec_size; // max 233 bytes + 4 header bytes - adc_buffer_size = codec2_samples_per_frame(codec2); - LOG_INFO("Use %d frames of %d bytes for a total payload length of %d bytes", encode_frame_num, encode_codec_size, encode_frame_size); - xTaskCreate(&run_codec2, "codec2_task", 30000, NULL, 5, &codec2HandlerTask); - } else { - disable(); - } -} - -void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - char buffer[50]; - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawStringf(0 + x, 0 + y, buffer, "Codec2 Mode %d Audio", - (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); - display->setColor(WHITE); - display->setFont(FONT_LARGE); - display->setTextAlignment(TEXT_ALIGN_CENTER); - switch (radio_state) { - case RadioState::tx: - display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "PTT"); - break; - default: - display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "Receive"); - break; - } -} - -int32_t AudioModule::runOnce() { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - esp_err_t res; - if (firstTime) { - // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC - LOG_INFO("Init I2S SD: %d DIN: %d WS: %d SCK: %d", moduleConfig.audio.i2s_sd, moduleConfig.audio.i2s_din, moduleConfig.audio.i2s_ws, - moduleConfig.audio.i2s_sck); - i2s_config_t i2s_config = { - .mode = (i2s_mode_t)(I2S_MODE_MASTER | (moduleConfig.audio.i2s_sd ? I2S_MODE_RX : 0) | (moduleConfig.audio.i2s_din ? I2S_MODE_TX : 0)), - .sample_rate = 8000, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, - .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), - .intr_alloc_flags = 0, - .dma_buf_count = 8, - .dma_buf_len = adc_buffer_size, // 320 * 2 bytes - .use_apll = false, - .tx_desc_auto_clear = true, - .fixed_mclk = 0}; - res = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); - if (res != ESP_OK) { - LOG_ERROR("Failed to install I2S driver: %d", res); - } - - const i2s_pin_config_t pin_config = {.bck_io_num = moduleConfig.audio.i2s_sck, - .ws_io_num = moduleConfig.audio.i2s_ws, - .data_out_num = moduleConfig.audio.i2s_din ? moduleConfig.audio.i2s_din : I2S_PIN_NO_CHANGE, - .data_in_num = moduleConfig.audio.i2s_sd ? moduleConfig.audio.i2s_sd : I2S_PIN_NO_CHANGE}; - res = i2s_set_pin(I2S_PORT, &pin_config); - if (res != ESP_OK) { - LOG_ERROR("Failed to set I2S pin config: %d", res); - } - - res = i2s_start(I2S_PORT); - if (res != ESP_OK) { - LOG_ERROR("Failed to start I2S: %d", res); - } - - radio_state = RadioState::rx; - - // Configure PTT input - LOG_INFO("Init PTT on Pin %u", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); - pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT); - - firstTime = false; + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); + tx_header.mode = (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1; + codec2_set_lpc_post_filter(codec2, 1, 0, 0.8, 0.2); + encode_codec_size = (codec2_bits_per_frame(codec2) + 7) / 8; + encode_frame_num = (meshtastic_Constants_DATA_PAYLOAD_LEN - sizeof(tx_header)) / encode_codec_size; + encode_frame_size = encode_frame_num * encode_codec_size; // max 233 bytes + 4 header bytes + adc_buffer_size = codec2_samples_per_frame(codec2); + LOG_INFO("Use %d frames of %d bytes for a total payload length of %d bytes", encode_frame_num, encode_codec_size, + encode_frame_size); + xTaskCreate(&run_codec2, "codec2_task", 30000, NULL, 5, &codec2HandlerTask); } else { - UIFrameEvent e; - // Check if PTT is pressed. TODO hook that into Onebutton/Interrupt drive. - if (digitalRead(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN) == HIGH) { - if (radio_state == RadioState::rx) { - LOG_INFO("PTT pressed, switching to TX"); - radio_state = RadioState::tx; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - } - } else { - if (radio_state == RadioState::tx) { - LOG_INFO("PTT released, switching to RX"); - if (tx_encode_frame_index > sizeof(tx_header)) { - // Send the incomplete frame - LOG_INFO("Send %d codec2 bytes (incomplete)", tx_encode_frame_index); - sendPayload(); - } - tx_encode_frame_index = sizeof(tx_header); - radio_state = RadioState::rx; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - } - } - if (radio_state == RadioState::tx) { - // Get I2S data from the microphone and place in data buffer - size_t bytesIn = 0; - res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn, - pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. + disable(); + } +} - if (res == ESP_OK) { - adc_buffer_index += bytesIn; - if (adc_buffer_index == adc_buffer_size) { - adc_buffer_index = 0; - memcpy((void *)speech, (void *)adc_buffer, 2 * adc_buffer_size); +void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + char buffer[50]; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawStringf(0 + x, 0 + y, buffer, "Codec2 Mode %d Audio", + (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); + display->setColor(WHITE); + display->setFont(FONT_LARGE); + display->setTextAlignment(TEXT_ALIGN_CENTER); + switch (radio_state) { + case RadioState::tx: + display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "PTT"); + break; + default: + display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "Receive"); + break; + } +} + +int32_t AudioModule::runOnce() +{ + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + esp_err_t res; + if (firstTime) { + // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC + LOG_INFO("Init I2S SD: %d DIN: %d WS: %d SCK: %d", moduleConfig.audio.i2s_sd, moduleConfig.audio.i2s_din, + moduleConfig.audio.i2s_ws, moduleConfig.audio.i2s_sck); + i2s_config_t i2s_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | (moduleConfig.audio.i2s_sd ? I2S_MODE_RX : 0) | + (moduleConfig.audio.i2s_din ? I2S_MODE_TX : 0)), + .sample_rate = 8000, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, + .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), + .intr_alloc_flags = 0, + .dma_buf_count = 8, + .dma_buf_len = adc_buffer_size, // 320 * 2 bytes + .use_apll = false, + .tx_desc_auto_clear = true, + .fixed_mclk = 0}; + res = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); + if (res != ESP_OK) { + LOG_ERROR("Failed to install I2S driver: %d", res); + } + + const i2s_pin_config_t pin_config = { + .bck_io_num = moduleConfig.audio.i2s_sck, + .ws_io_num = moduleConfig.audio.i2s_ws, + .data_out_num = moduleConfig.audio.i2s_din ? moduleConfig.audio.i2s_din : I2S_PIN_NO_CHANGE, + .data_in_num = moduleConfig.audio.i2s_sd ? moduleConfig.audio.i2s_sd : I2S_PIN_NO_CHANGE}; + res = i2s_set_pin(I2S_PORT, &pin_config); + if (res != ESP_OK) { + LOG_ERROR("Failed to set I2S pin config: %d", res); + } + + res = i2s_start(I2S_PORT); + if (res != ESP_OK) { + LOG_ERROR("Failed to start I2S: %d", res); + } + + radio_state = RadioState::rx; + + // Configure PTT input + LOG_INFO("Init PTT on Pin %u", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); + pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT); + + firstTime = false; + } else { + UIFrameEvent e; + // Check if PTT is pressed. TODO hook that into Onebutton/Interrupt drive. + if (digitalRead(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN) == HIGH) { + if (radio_state == RadioState::rx) { + LOG_INFO("PTT pressed, switching to TX"); + radio_state = RadioState::tx; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->notifyObservers(&e); + } + } else { + if (radio_state == RadioState::tx) { + LOG_INFO("PTT released, switching to RX"); + if (tx_encode_frame_index > sizeof(tx_header)) { + // Send the incomplete frame + LOG_INFO("Send %d codec2 bytes (incomplete)", tx_encode_frame_index); + sendPayload(); + } + tx_encode_frame_index = sizeof(tx_header); + radio_state = RadioState::rx; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + this->notifyObservers(&e); + } + } + if (radio_state == RadioState::tx) { + // Get I2S data from the microphone and place in data buffer + size_t bytesIn = 0; + res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn, + pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. + + if (res == ESP_OK) { + adc_buffer_index += bytesIn; + if (adc_buffer_index == adc_buffer_size) { + adc_buffer_index = 0; + memcpy((void *)speech, (void *)adc_buffer, 2 * adc_buffer_size); + // Notify run_codec2 task that the buffer is ready. + radio_state = RadioState::tx; + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) + YIELD_FROM_ISR(xHigherPriorityTaskWoken); + } + } + } + } + return 100; + } else { + return disable(); + } +} + +meshtastic_MeshPacket *AudioModule::allocReply() +{ + auto reply = allocDataPacket(); + return reply; +} + +bool AudioModule::shouldDraw() +{ + if (!moduleConfig.audio.codec2_enabled) { + return false; + } + return (radio_state == RadioState::tx); +} + +void AudioModule::sendPayload(NodeNum dest, bool wantReplies) +{ + meshtastic_MeshPacket *p = allocReply(); + p->to = dest; + p->decoded.want_response = wantReplies; + + p->want_ack = false; // Audio is shoot&forget. No need to wait for ACKs. + p->priority = meshtastic_MeshPacket_Priority_MAX; // Audio is important, because realtime + + p->decoded.payload.size = tx_encode_frame_index; + memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); + + service->sendToMesh(p); +} + +ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + auto &p = mp.decoded; + if (!isFromUs(&mp)) { + memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); + radio_state = RadioState::rx; + rx_encode_frame_index = p.payload.size; // Notify run_codec2 task that the buffer is ready. - radio_state = RadioState::tx; BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); if (xHigherPriorityTaskWoken == pdTRUE) - YIELD_FROM_ISR(xHigherPriorityTaskWoken); - } + YIELD_FROM_ISR(xHigherPriorityTaskWoken); } - } } - return 100; - } else { - return disable(); - } -} -meshtastic_MeshPacket *AudioModule::allocReply() { - auto reply = allocDataPacket(); - return reply; -} - -bool AudioModule::shouldDraw() { - if (!moduleConfig.audio.codec2_enabled) { - return false; - } - return (radio_state == RadioState::tx); -} - -void AudioModule::sendPayload(NodeNum dest, bool wantReplies) { - meshtastic_MeshPacket *p = allocReply(); - p->to = dest; - p->decoded.want_response = wantReplies; - - p->want_ack = false; // Audio is shoot&forget. No need to wait for ACKs. - p->priority = meshtastic_MeshPacket_Priority_MAX; // Audio is important, because realtime - - p->decoded.payload.size = tx_encode_frame_index; - memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); - - service->sendToMesh(p); -} - -ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { - auto &p = mp.decoded; - if (!isFromUs(&mp)) { - memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); - radio_state = RadioState::rx; - rx_encode_frame_index = p.payload.size; - // Notify run_codec2 task that the buffer is ready. - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken == pdTRUE) - YIELD_FROM_ISR(xHigherPriorityTaskWoken); - } - } - - return ProcessMessage::CONTINUE; + return ProcessMessage::CONTINUE; } #endif \ No newline at end of file diff --git a/src/modules/esp32/AudioModule.h b/src/modules/esp32/AudioModule.h index fb134bcaa..b6762706a 100644 --- a/src/modules/esp32/AudioModule.h +++ b/src/modules/esp32/AudioModule.h @@ -18,8 +18,8 @@ enum RadioState { standby, rx, tx }; const char c2_magic[3] = {0xc0, 0xde, 0xc2}; // Magic number for codec2 header struct c2_header { - char magic[3]; - char mode; + char magic[3]; + char mode; }; #define ADC_BUFFER_SIZE_MAX 320 @@ -30,55 +30,56 @@ struct c2_header { #define AUDIO_MODULE_RX_BUFFER 128 #define AUDIO_MODULE_MODE meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700 -class AudioModule : public SinglePortModule, public Observable, private concurrency::OSThread { -public: - unsigned char rx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - unsigned char tx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; - c2_header tx_header = {}; - int16_t speech[ADC_BUFFER_SIZE_MAX] = {}; - int16_t output_buffer[ADC_BUFFER_SIZE_MAX] = {}; - uint16_t adc_buffer[ADC_BUFFER_SIZE_MAX] = {}; - int adc_buffer_size = 0; - uint16_t adc_buffer_index = 0; - int tx_encode_frame_index = sizeof(c2_header); // leave room for header - int rx_encode_frame_index = 0; - int encode_codec_size = 0; - int encode_frame_size = 0; - volatile RadioState radio_state = RadioState::rx; +class AudioModule : public SinglePortModule, public Observable, private concurrency::OSThread +{ + public: + unsigned char rx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + unsigned char tx_encode_frame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; + c2_header tx_header = {}; + int16_t speech[ADC_BUFFER_SIZE_MAX] = {}; + int16_t output_buffer[ADC_BUFFER_SIZE_MAX] = {}; + uint16_t adc_buffer[ADC_BUFFER_SIZE_MAX] = {}; + int adc_buffer_size = 0; + uint16_t adc_buffer_index = 0; + int tx_encode_frame_index = sizeof(c2_header); // leave room for header + int rx_encode_frame_index = 0; + int encode_codec_size = 0; + int encode_frame_size = 0; + volatile RadioState radio_state = RadioState::rx; - struct CODEC2 *codec2 = NULL; - // int16_t sample; + struct CODEC2 *codec2 = NULL; + // int16_t sample; - AudioModule(); + AudioModule(); - bool shouldDraw(); + bool shouldDraw(); - /** - * Send our payload into the mesh - */ - void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); + /** + * Send our payload into the mesh + */ + void sendPayload(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); -protected: - int encode_frame_num = 0; - bool firstTime = true; + protected: + int encode_frame_num = 0; + bool firstTime = true; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; - virtual meshtastic_MeshPacket *allocReply() override; + virtual meshtastic_MeshPacket *allocReply() override; - virtual bool wantUIFrame() override { return this->shouldDraw(); } - virtual Observable *getUIFrameObservable() override { return this; } + virtual bool wantUIFrame() override { return this->shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } #if !HAS_SCREEN - void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - /** Called to handle a particular incoming message - * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be - * considered for it - */ - virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + /** Called to handle a particular incoming message + * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered + * for it + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; }; extern AudioModule *audioModule; diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp index 9c91cd2a5..9c25177bc 100644 --- a/src/modules/esp32/PaxcounterModule.cpp +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -15,16 +15,20 @@ PaxcounterModule *paxcounterModule; * We only clear our sent flag here, since this function is called from another thread, so we * cannot send to the mesh directly. */ -void PaxcounterModule::handlePaxCounterReportRequest() { - // The libpax library already updated our data structure, just before invoking this callback. - LOG_INFO("PaxcounterModule: libpax reported new data: wifi=%d; ble=%d; uptime=%lu", paxcounterModule->count_from_libpax.wifi_count, - paxcounterModule->count_from_libpax.ble_count, millis() / 1000); - paxcounterModule->reportedDataSent = false; - paxcounterModule->setIntervalFromNow(0); +void PaxcounterModule::handlePaxCounterReportRequest() +{ + // The libpax library already updated our data structure, just before invoking this callback. + LOG_INFO("PaxcounterModule: libpax reported new data: wifi=%d; ble=%d; uptime=%lu", + paxcounterModule->count_from_libpax.wifi_count, paxcounterModule->count_from_libpax.ble_count, millis() / 1000); + paxcounterModule->reportedDataSent = false; + paxcounterModule->setIntervalFromNow(0); } PaxcounterModule::PaxcounterModule() - : concurrency::OSThread("Paxcounter"), ProtobufModule("paxcounter", meshtastic_PortNum_PAXCOUNTER_APP, &meshtastic_Paxcount_msg) {} + : concurrency::OSThread("Paxcounter"), + ProtobufModule("paxcounter", meshtastic_PortNum_PAXCOUNTER_APP, &meshtastic_Paxcount_msg) +{ +} /** * Send the Pax information to the mesh if we got new data from libpax. @@ -33,72 +37,79 @@ PaxcounterModule::PaxcounterModule() * @param dest - destination node (usually NODENUM_BROADCAST) * @return false if sending is unnecessary, true if information was sent */ -bool PaxcounterModule::sendInfo(NodeNum dest) { - if (paxcounterModule->reportedDataSent) - return false; +bool PaxcounterModule::sendInfo(NodeNum dest) +{ + if (paxcounterModule->reportedDataSent) + return false; - LOG_INFO("PaxcounterModule: send pax info wifi=%d; ble=%d; uptime=%lu", count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); + LOG_INFO("PaxcounterModule: send pax info wifi=%d; ble=%d; uptime=%lu", count_from_libpax.wifi_count, + count_from_libpax.ble_count, millis() / 1000); - meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; - pl.wifi = count_from_libpax.wifi_count; - pl.ble = count_from_libpax.ble_count; - pl.uptime = millis() / 1000; + meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; + pl.wifi = count_from_libpax.wifi_count; + pl.ble = count_from_libpax.ble_count; + pl.uptime = millis() / 1000; - meshtastic_MeshPacket *p = allocDataProtobuf(pl); - p->to = dest; - p->decoded.want_response = false; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + meshtastic_MeshPacket *p = allocDataProtobuf(pl); + p->to = dest; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - service->sendToMesh(p, RX_SRC_LOCAL, true); + service->sendToMesh(p, RX_SRC_LOCAL, true); - paxcounterModule->reportedDataSent = true; + paxcounterModule->reportedDataSent = true; - return true; + return true; } -bool PaxcounterModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) { - return false; // Let others look at this message also if they want. We don't do anything with received packets. +bool PaxcounterModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) +{ + return false; // Let others look at this message also if they want. We don't do anything with received packets. } -meshtastic_MeshPacket *PaxcounterModule::allocReply() { - meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; - pl.wifi = count_from_libpax.wifi_count; - pl.ble = count_from_libpax.ble_count; - pl.uptime = millis() / 1000; - return allocDataProtobuf(pl); +meshtastic_MeshPacket *PaxcounterModule::allocReply() +{ + meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; + pl.wifi = count_from_libpax.wifi_count; + pl.ble = count_from_libpax.ble_count; + pl.uptime = millis() / 1000; + return allocDataProtobuf(pl); } -int32_t PaxcounterModule::runOnce() { - if (isActive()) { - if (firstTime) { - firstTime = false; - LOG_DEBUG("Paxcounter starting up with interval of %d seconds", - Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs)); - struct libpax_config_t configuration; - libpax_default_config(&configuration); +int32_t PaxcounterModule::runOnce() +{ + if (isActive()) { + if (firstTime) { + firstTime = false; + LOG_DEBUG("Paxcounter starting up with interval of %d seconds", + Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, + default_telemetry_broadcast_interval_secs)); + struct libpax_config_t configuration; + libpax_default_config(&configuration); - configuration.blecounter = 1; - configuration.blescantime = 0; // infinite - configuration.wificounter = 1; - configuration.wifi_channel_map = WIFI_CHANNEL_ALL; - configuration.wifi_channel_switch_interval = 50; - configuration.wifi_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.wifi_threshold, -80); - configuration.ble_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.ble_threshold, -80); - libpax_update_config(&configuration); + configuration.blecounter = 1; + configuration.blescantime = 0; // infinite + configuration.wificounter = 1; + configuration.wifi_channel_map = WIFI_CHANNEL_ALL; + configuration.wifi_channel_switch_interval = 50; + configuration.wifi_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.wifi_threshold, -80); + configuration.ble_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.ble_threshold, -80); + libpax_update_config(&configuration); - // internal processing initialization - libpax_counter_init( - handlePaxCounterReportRequest, &count_from_libpax, - Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs), 0); - libpax_counter_start(); + // internal processing initialization + libpax_counter_init(handlePaxCounterReportRequest, &count_from_libpax, + Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, + default_telemetry_broadcast_interval_secs), + 0); + libpax_counter_start(); + } else { + sendInfo(NODENUM_BROADCAST); + } + return Default::getConfiguredOrDefaultMsScaled(moduleConfig.paxcounter.paxcounter_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes); } else { - sendInfo(NODENUM_BROADCAST); + return disable(); } - return Default::getConfiguredOrDefaultMsScaled(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs, - numOnlineNodes); - } else { - return disable(); - } } #if HAS_SCREEN @@ -106,29 +117,31 @@ int32_t PaxcounterModule::runOnce() { #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" -void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; +void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // === Set Title - const char *titleStr = "Pax"; + // === Set Title + const char *titleStr = "Pax"; - // === Header === - graphics::drawCommonHeader(display, x, y, titleStr); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); - char buffer[50]; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - libpax_counter_count(&count_from_libpax); + libpax_counter_count(&count_from_libpax); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, "WiFi: %d\nBLE: %d\nUptime: %ds", - count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); - graphics::drawCommonFooter(display, x, y); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, + "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, + millis() / 1000); + graphics::drawCommonFooter(display, x, y); } #endif // HAS_SCREEN diff --git a/src/modules/esp32/PaxcounterModule.h b/src/modules/esp32/PaxcounterModule.h index 92097de1f..ebd6e7191 100644 --- a/src/modules/esp32/PaxcounterModule.h +++ b/src/modules/esp32/PaxcounterModule.h @@ -11,25 +11,26 @@ * Wrapper module for the estimate passenger (PAX) count library (https://github.com/dbinfrago/libpax) which * implements the core functionality of the ESP32 Paxcounter project (https://github.com/cyberman54/ESP32-Paxcounter) */ -class PaxcounterModule : private concurrency::OSThread, public ProtobufModule { - bool firstTime = true; - bool reportedDataSent = true; +class PaxcounterModule : private concurrency::OSThread, public ProtobufModule +{ + bool firstTime = true; + bool reportedDataSent = true; - static void handlePaxCounterReportRequest(); + static void handlePaxCounterReportRequest(); -public: - PaxcounterModule(); + public: + PaxcounterModule(); -protected: - struct count_payload_t count_from_libpax = {0, 0, 0}; - virtual int32_t runOnce() override; - bool sendInfo(NodeNum dest = NODENUM_BROADCAST); - virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) override; - virtual meshtastic_MeshPacket *allocReply() override; - bool isActive() { return moduleConfig.paxcounter.enabled && !config.bluetooth.enabled && !config.network.wifi_enabled; } + protected: + struct count_payload_t count_from_libpax = {0, 0, 0}; + virtual int32_t runOnce() override; + bool sendInfo(NodeNum dest = NODENUM_BROADCAST); + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Paxcount *p) override; + virtual meshtastic_MeshPacket *allocReply() override; + bool isActive() { return moduleConfig.paxcounter.enabled && !config.bluetooth.enabled && !config.network.wifi_enabled; } #if HAS_SCREEN - virtual bool wantUIFrame() override { return isActive(); } - virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; + virtual bool wantUIFrame() override { return isActive(); } + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif }; diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 6644e0624..f08ee00f9 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -26,127 +26,139 @@ extern ScanI2C::DeviceAddress accelerometer_found; -class AccelerometerThread : public concurrency::OSThread { -private: - MotionSensor *sensor = nullptr; - bool isInitialised = false; +class AccelerometerThread : public concurrency::OSThread +{ + private: + MotionSensor *sensor = nullptr; + bool isInitialised = false; -public: - explicit AccelerometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Accelerometer") { - device = foundDevice; - init(); - } - - explicit AccelerometerThread(ScanI2C::DeviceType type) : AccelerometerThread(ScanI2C::FoundDevice{type, accelerometer_found}) {} - - void start() { - init(); - setIntervalFromNow(0); - }; - - void calibrate(uint16_t forSeconds) { - if (sensor) { - sensor->calibrate(forSeconds); - } - } - -protected: - int32_t runOnce() override { - // Assume we should not keep the board awake - canSleep = true; - - if (isInitialised) - return sensor->runOnce(); - - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - -private: - ScanI2C::FoundDevice device; - - void init() { - if (isInitialised) - return; - - if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) { - LOG_DEBUG("AccelerometerThread Disable due to no sensors found"); - disable(); - return; + public: + explicit AccelerometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Accelerometer") + { + device = foundDevice; + init(); } - switch (device.type) { + explicit AccelerometerThread(ScanI2C::DeviceType type) : AccelerometerThread(ScanI2C::FoundDevice{type, accelerometer_found}) + { + } + + void start() + { + init(); + setIntervalFromNow(0); + }; + + void calibrate(uint16_t forSeconds) + { + if (sensor) { + sensor->calibrate(forSeconds); + } + } + + protected: + int32_t runOnce() override + { + // Assume we should not keep the board awake + canSleep = true; + + if (isInitialised) + return sensor->runOnce(); + + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + private: + ScanI2C::FoundDevice device; + + void init() + { + if (isInitialised) + return; + + if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) { + LOG_DEBUG("AccelerometerThread Disable due to no sensors found"); + disable(); + return; + } + + switch (device.type) { #ifdef HAS_BMA423 - case ScanI2C::DeviceType::BMA423: - sensor = new BMA423Sensor(device); - break; + case ScanI2C::DeviceType::BMA423: + sensor = new BMA423Sensor(device); + break; #endif - case ScanI2C::DeviceType::MPU6050: - sensor = new MPU6050Sensor(device); - break; - case ScanI2C::DeviceType::BMX160: - sensor = new BMX160Sensor(device); - break; - case ScanI2C::DeviceType::LIS3DH: - sensor = new LIS3DHSensor(device); - break; - case ScanI2C::DeviceType::LSM6DS3: - sensor = new LSM6DS3Sensor(device); - break; + case ScanI2C::DeviceType::MPU6050: + sensor = new MPU6050Sensor(device); + break; + case ScanI2C::DeviceType::BMX160: + sensor = new BMX160Sensor(device); + break; + case ScanI2C::DeviceType::LIS3DH: + sensor = new LIS3DHSensor(device); + break; + case ScanI2C::DeviceType::LSM6DS3: + sensor = new LSM6DS3Sensor(device); + break; #ifdef HAS_STK8XXX - case ScanI2C::DeviceType::STK8BAXX: - sensor = new STK8XXXSensor(device); - break; + case ScanI2C::DeviceType::STK8BAXX: + sensor = new STK8XXXSensor(device); + break; #endif - case ScanI2C::DeviceType::ICM20948: - sensor = new ICM20948Sensor(device); - break; - case ScanI2C::DeviceType::BMM150: - sensor = new BMM150Sensor(device); - break; + case ScanI2C::DeviceType::ICM20948: + sensor = new ICM20948Sensor(device); + break; + case ScanI2C::DeviceType::BMM150: + sensor = new BMM150Sensor(device); + break; #ifdef HAS_QMA6100P - case ScanI2C::DeviceType::QMA6100P: - sensor = new QMA6100PSensor(device); - break; + case ScanI2C::DeviceType::QMA6100P: + sensor = new QMA6100PSensor(device); + break; #endif - default: - disable(); - return; + default: + disable(); + return; + } + + isInitialised = sensor->init(); + if (!isInitialised) { + clean(); + } + LOG_DEBUG("AccelerometerThread::init %s", isInitialised ? "ok" : "failed"); } - isInitialised = sensor->init(); - if (!isInitialised) { - clean(); + // Copy constructor (not implemented / included to avoid cppcheck warnings) + AccelerometerThread(const AccelerometerThread &other) : OSThread::OSThread("Accelerometer") { this->copy(other); } + + // Destructor (included to avoid cppcheck warnings) + virtual ~AccelerometerThread() { clean(); } + + // Copy assignment (not implemented / included to avoid cppcheck warnings) + AccelerometerThread &operator=(const AccelerometerThread &other) + { + this->copy(other); + return *this; } - LOG_DEBUG("AccelerometerThread::init %s", isInitialised ? "ok" : "failed"); - } - // Copy constructor (not implemented / included to avoid cppcheck warnings) - AccelerometerThread(const AccelerometerThread &other) : OSThread::OSThread("Accelerometer") { this->copy(other); } - - // Destructor (included to avoid cppcheck warnings) - virtual ~AccelerometerThread() { clean(); } - - // Copy assignment (not implemented / included to avoid cppcheck warnings) - AccelerometerThread &operator=(const AccelerometerThread &other) { - this->copy(other); - return *this; - } - - // Take a very shallow copy (does not copy OSThread state nor the sensor object) - // If for some reason this is ever used, make sure to call init() after any copy - void copy(const AccelerometerThread &other) { - if (this != &other) { - clean(); - this->device = ScanI2C::FoundDevice(other.device.type, ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address)); + // Take a very shallow copy (does not copy OSThread state nor the sensor object) + // If for some reason this is ever used, make sure to call init() after any copy + void copy(const AccelerometerThread &other) + { + if (this != &other) { + clean(); + this->device = ScanI2C::FoundDevice(other.device.type, + ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address)); + } } - } - // Cleanup resources - void clean() { - isInitialised = false; - delete sensor; - sensor = nullptr; - } + // Cleanup resources + void clean() + { + isInitialised = false; + delete sensor; + sensor = nullptr; + } }; #endif diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp index ed61553bc..5111dae32 100755 --- a/src/motion/BMA423Sensor.cpp +++ b/src/motion/BMA423Sensor.cpp @@ -4,55 +4,57 @@ BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool BMA423Sensor::init() { - if (sensor.begin(Wire, deviceAddress())) { - sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); - sensor.enableAccelerometer(); - sensor.configInterrupt(); +bool BMA423Sensor::init() +{ + if (sensor.begin(Wire, deviceAddress())) { + sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); + sensor.enableAccelerometer(); + sensor.configInterrupt(); #ifdef BMA423_INT - pinMode(BMA4XX_INT, INPUT); - attachInterrupt( - BMA4XX_INT, - [] { - // Set interrupt to set irq value to true - BMA_IRQ = true; - }, - RISING); // Select the interrupt mode according to the actual circuit + pinMode(BMA4XX_INT, INPUT); + attachInterrupt( + BMA4XX_INT, + [] { + // Set interrupt to set irq value to true + BMA_IRQ = true; + }, + RISING); // Select the interrupt mode according to the actual circuit #endif #ifdef T_WATCH_S3 - // Need to raise the wrist function, need to set the correct axis - sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); + // Need to raise the wrist function, need to set the correct axis + sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); #else - sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); + sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); #endif - // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); - sensor.enableFeature(sensor.FEATURE_TILT, true); - sensor.enableFeature(sensor.FEATURE_WAKEUP, true); - // sensor.resetPedometer(); + // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); + sensor.enableFeature(sensor.FEATURE_TILT, true); + sensor.enableFeature(sensor.FEATURE_WAKEUP, true); + // sensor.resetPedometer(); - // Turn on feature interrupt - sensor.enablePedometerIRQ(); - sensor.enableTiltIRQ(); + // Turn on feature interrupt + sensor.enablePedometerIRQ(); + sensor.enableTiltIRQ(); - // It corresponds to isDoubleClick interrupt - sensor.enableWakeupIRQ(); - LOG_DEBUG("BMA423 init ok"); - return true; - } - LOG_DEBUG("BMA423 init failed"); - return false; + // It corresponds to isDoubleClick interrupt + sensor.enableWakeupIRQ(); + LOG_DEBUG("BMA423 init ok"); + return true; + } + LOG_DEBUG("BMA423 init failed"); + return false; } -int32_t BMA423Sensor::runOnce() { - if (sensor.readIrqStatus()) { - if (sensor.isTilt() || sensor.isDoubleTap()) { - wakeScreen(); - return 500; +int32_t BMA423Sensor::runOnce() +{ + if (sensor.readIrqStatus()) { + if (sensor.isTilt() || sensor.isDoubleTap()) { + wakeScreen(); + return 500; + } } - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif \ No newline at end of file diff --git a/src/motion/BMA423Sensor.h b/src/motion/BMA423Sensor.h index 28bfa3fd5..b9d7b4aa0 100755 --- a/src/motion/BMA423Sensor.h +++ b/src/motion/BMA423Sensor.h @@ -9,15 +9,16 @@ #include #include -class BMA423Sensor : public MotionSensor { -private: - SensorBMA423 sensor; - volatile bool BMA_IRQ = false; +class BMA423Sensor : public MotionSensor +{ + private: + SensorBMA423 sensor; + volatile bool BMA_IRQ = false; -public: - explicit BMA423Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; + public: + explicit BMA423Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp index 080918114..4b3a1215c 100644 --- a/src/motion/BMM150Sensor.cpp +++ b/src/motion/BMM150Sensor.cpp @@ -12,37 +12,39 @@ volatile static bool BMM150_IRQ = false; BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool BMM150Sensor::init() { - // Initialise the sensor - sensor = BMM150Singleton::GetInstance(device); - return sensor->init(device); +bool BMM150Sensor::init() +{ + // Initialise the sensor + sensor = BMM150Singleton::GetInstance(device); + return sensor->init(device); } -int32_t BMM150Sensor::runOnce() { +int32_t BMM150Sensor::runOnce() +{ #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - float heading = sensor->getCompassDegree(); + float heading = sensor->getCompassDegree(); - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } - if (screen) - screen->setHeading(heading); + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); #endif - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } // ---------------------------------------------------------------------- @@ -50,16 +52,17 @@ int32_t BMM150Sensor::runOnce() { // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun BMM_150_I2C -BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) { +BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) +{ #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) - TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - TwoWire &bus = Wire; // fallback if only one I2C interface + TwoWire &bus = Wire; // fallback if only one I2C interface #endif - if (pinstance == nullptr) { - pinstance = new BMM150Singleton(&bus, device.address.address); - } - return pinstance; + if (pinstance == nullptr) { + pinstance = new BMM150Singleton(&bus, device.address.address); + } + return pinstance; } BMM150Singleton::~BMM150Singleton() {} @@ -68,22 +71,23 @@ BMM150Singleton *BMM150Singleton::pinstance{nullptr}; // Initialise the BMM150 Sensor // https://github.com/DFRobot/DFRobot_BMM150/blob/master/examples/getGeomagneticData/getGeomagneticData.ino -bool BMM150Singleton::init(ScanI2C::FoundDevice device) { +bool BMM150Singleton::init(ScanI2C::FoundDevice device) +{ - // startup - LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); - uint8_t status = begin(); - if (status != 0) { - LOG_DEBUG("BMM150 init error %u", status); - return false; - } + // startup + LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); + uint8_t status = begin(); + if (status != 0) { + LOG_DEBUG("BMM150 init error %u", status); + return false; + } - // SW reset to make sure the device starts in a known state - setOperationMode(BMM150_POWERMODE_NORMAL); - setPresetMode(BMM150_PRESETMODE_LOWPOWER); - setRate(BMM150_DATA_RATE_02HZ); - setMeasurementXYZ(); - return true; + // SW reset to make sure the device starts in a known state + setOperationMode(BMM150_POWERMODE_NORMAL); + setPresetMode(BMM150_PRESETMODE_LOWPOWER); + setRate(BMM150_DATA_RATE_02HZ); + setMeasurementXYZ(); + return true; } #endif \ No newline at end of file diff --git a/src/motion/BMM150Sensor.h b/src/motion/BMM150Sensor.h index 36d7a29f8..879045400 100644 --- a/src/motion/BMM150Sensor.h +++ b/src/motion/BMM150Sensor.h @@ -13,41 +13,43 @@ extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper -class BMM150Singleton : public DFRobot_BMM150_I2C { -private: - static BMM150Singleton *pinstance; +class BMM150Singleton : public DFRobot_BMM150_I2C +{ + private: + static BMM150Singleton *pinstance; -protected: - BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} - ~BMM150Singleton(); + protected: + BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} + ~BMM150Singleton(); -public: - // Create a singleton instance (not thread safe) - static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); + public: + // Create a singleton instance (not thread safe) + static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); - // Singletons should not be cloneable. - BMM150Singleton(BMM150Singleton &other) = delete; + // Singletons should not be cloneable. + BMM150Singleton(BMM150Singleton &other) = delete; - // Singletons should not be assignable. - void operator=(const BMM150Singleton &) = delete; + // Singletons should not be assignable. + void operator=(const BMM150Singleton &) = delete; - // Initialise the motion sensor singleton for normal operation - bool init(ScanI2C::FoundDevice device); + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); }; -class BMM150Sensor : public MotionSensor { -private: - BMM150Singleton *sensor = nullptr; - bool showingScreen = false; +class BMM150Sensor : public MotionSensor +{ + private: + BMM150Singleton *sensor = nullptr; + bool showingScreen = false; -public: - explicit BMM150Sensor(ScanI2C::FoundDevice foundDevice); + public: + explicit BMM150Sensor(ScanI2C::FoundDevice foundDevice); - // Initialise the motion sensor - virtual bool init() override; + // Initialise the motion sensor + virtual bool init() override; - // Called each time our sensor gets a chance to run - virtual int32_t runOnce() override; + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index c576abf2d..5888c20be 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -11,120 +11,123 @@ BMX160Sensor::BMX160Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::Mot extern graphics::Screen *screen; #endif -bool BMX160Sensor::init() { - if (sensor.begin()) { - // set output data rate - sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ); - LOG_DEBUG("BMX160 init ok"); - return true; - } - LOG_DEBUG("BMX160 init failed"); - return false; +bool BMX160Sensor::init() +{ + if (sensor.begin()) { + // set output data rate + sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ); + LOG_DEBUG("BMX160 init ok"); + return true; + } + LOG_DEBUG("BMX160 init failed"); + return false; } -int32_t BMX160Sensor::runOnce() { +int32_t BMX160Sensor::runOnce() +{ #if !defined(MESHTASTIC_EXCLUDE_SCREEN) - sBmx160SensorData_t magAccel; - sBmx160SensorData_t gAccel; + sBmx160SensorData_t magAccel; + sBmx160SensorData_t gAccel; - /* Get a new sensor event */ - sensor.getAllData(&magAccel, NULL, &gAccel); + /* Get a new sensor event */ + sensor.getAllData(&magAccel, NULL, &gAccel); - if (doCalibration) { + if (doCalibration) { - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration + showingScreen = true; + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); + } + + if (magAccel.x > highestX) + highestX = magAccel.x; + if (magAccel.x < lowestX) + lowestX = magAccel.x; + if (magAccel.y > highestY) + highestY = magAccel.y; + if (magAccel.y < lowestY) + lowestY = magAccel.y; + if (magAccel.z > highestZ) + highestZ = magAccel.z; + if (magAccel.z < lowestZ) + lowestZ = magAccel.z; + + uint32_t now = millis(); + if (now > endCalibrationAt) { + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + if (screen) + screen->endAlert(); + } + + // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, + // lowestY, highestY, lowestZ, highestZ); } - if (magAccel.x > highestX) - highestX = magAccel.x; - if (magAccel.x < lowestX) - lowestX = magAccel.x; - if (magAccel.y > highestY) - highestY = magAccel.y; - if (magAccel.y < lowestY) - lowestY = magAccel.y; - if (magAccel.z > highestZ) - highestZ = magAccel.z; - if (magAccel.z < lowestZ) - lowestZ = magAccel.z; + int highestRealX = highestX - (highestX + lowestX) / 2; - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); + magAccel.x -= (highestX + lowestX) / 2; + magAccel.y -= (highestY + lowestY) / 2; + magAccel.z -= (highestZ + lowestZ) / 2; + FusionVector ga, ma; + ga.axis.x = -gAccel.x; // default location for the BMX160 is on the rear of the board + ga.axis.y = -gAccel.y; + ga.axis.z = gAccel.z; + ma.axis.x = -magAccel.x; + ma.axis.y = -magAccel.y; + ma.axis.z = magAccel.z * 3; + + // If we're set to one of the inverted positions + if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { + ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); + ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); } - // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, - // highestX, lowestY, highestY, lowestZ, highestZ); - } + float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - int highestRealX = highestX - (highestX + lowestX) / 2; - - magAccel.x -= (highestX + lowestX) / 2; - magAccel.y -= (highestY + lowestY) / 2; - magAccel.z -= (highestZ + lowestZ) / 2; - FusionVector ga, ma; - ga.axis.x = -gAccel.x; // default location for the BMX160 is on the rear of the board - ga.axis.y = -gAccel.y; - ga.axis.z = gAccel.z; - ma.axis.x = -magAccel.x; - ma.axis.y = -magAccel.y; - ma.axis.z = magAccel.z * 3; - - // If we're set to one of the inverted positions - if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { - ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); - ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); - } - - float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } - if (screen) - screen->setHeading(heading); + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); #endif - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } -void BMX160Sensor::calibrate(uint16_t forSeconds) { +void BMX160Sensor::calibrate(uint16_t forSeconds) +{ #if !defined(MESHTASTIC_EXCLUDE_SCREEN) - sBmx160SensorData_t magAccel; - sBmx160SensorData_t gAccel; - LOG_DEBUG("BMX160 calibration started for %is", forSeconds); - sensor.getAllData(&magAccel, NULL, &gAccel); - highestX = magAccel.x, lowestX = magAccel.x; - highestY = magAccel.y, lowestY = magAccel.y; - highestZ = magAccel.z, lowestZ = magAccel.z; + sBmx160SensorData_t magAccel; + sBmx160SensorData_t gAccel; + LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + sensor.getAllData(&magAccel, NULL, &gAccel); + highestX = magAccel.x, lowestX = magAccel.x; + highestY = magAccel.y, lowestY = magAccel.y; + highestZ = magAccel.z, lowestZ = magAccel.z; - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + doCalibration = true; + uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided + endCalibrationAt = millis() + calibrateFor; + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index 3a3886756..ddca5767c 100755 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -12,25 +12,27 @@ #include "Fusion/Fusion.h" #include -class BMX160Sensor : public MotionSensor { -private: - RAK_BMX160 sensor; - bool showingScreen = false; - float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; +class BMX160Sensor : public MotionSensor +{ + private: + RAK_BMX160 sensor; + bool showingScreen = false; + float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; -public: - explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; - virtual void calibrate(uint16_t forSeconds) override; + public: + explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; }; #else // Stub -class BMX160Sensor : public MotionSensor { -public: - explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); +class BMX160Sensor : public MotionSensor +{ + public: + explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); }; #endif diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 208edeac6..9455eafe0 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -11,180 +11,186 @@ extern graphics::Screen *screen; volatile static bool ICM20948_IRQ = false; // Interrupt service routine -void ICM20948SetInterrupt() { ICM20948_IRQ = true; } +void ICM20948SetInterrupt() +{ + ICM20948_IRQ = true; +} ICM20948Sensor::ICM20948Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool ICM20948Sensor::init() { - // Initialise the sensor - sensor = ICM20948Singleton::GetInstance(); - if (!sensor->init(device)) - return false; +bool ICM20948Sensor::init() +{ + // Initialise the sensor + sensor = ICM20948Singleton::GetInstance(); + if (!sensor->init(device)) + return false; - // Enable simple Wake on Motion - return sensor->setWakeOnMotion(); + // Enable simple Wake on Motion + return sensor->setWakeOnMotion(); } #ifdef ICM_20948_INT_PIN -int32_t ICM20948Sensor::runOnce() { - // Wake on motion using hardware interrupts - this is the most efficient way to check for motion - if (ICM20948_IRQ) { - ICM20948_IRQ = false; - sensor->clearInterrupts(); - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t ICM20948Sensor::runOnce() +{ + // Wake on motion using hardware interrupts - this is the most efficient way to check for motion + if (ICM20948_IRQ) { + ICM20948_IRQ = false; + sensor->clearInterrupts(); + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #else -int32_t ICM20948Sensor::runOnce() { +int32_t ICM20948Sensor::runOnce() +{ #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN #if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze - if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { - if (!isAsleep) { - LOG_DEBUG("sleeping IMU"); - sensor->sleep(true); - isAsleep = true; + if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { + if (!isAsleep) { + LOG_DEBUG("sleeping IMU"); + sensor->sleep(true); + isAsleep = true; + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + if (isAsleep) { + sensor->sleep(false); + isAsleep = false; } - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - if (isAsleep) { - sensor->sleep(false); - isAsleep = false; - } #endif - float magX = 0, magY = 0, magZ = 0; - if (sensor->dataReady()) { - sensor->getAGMT(); - magX = sensor->agmt.mag.axes.x; - magY = sensor->agmt.mag.axes.y; - magZ = sensor->agmt.mag.axes.z; - } - - if (doCalibration) { - - if (!showingScreen) { - powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration - showingScreen = true; - if (screen) - screen->startAlert((FrameCallback)drawFrameCalibration); + float magX = 0, magY = 0, magZ = 0; + if (sensor->dataReady()) { + sensor->getAGMT(); + magX = sensor->agmt.mag.axes.x; + magY = sensor->agmt.mag.axes.y; + magZ = sensor->agmt.mag.axes.z; } - if (magX > highestX) - highestX = magX; - if (magX < lowestX) - lowestX = magX; - if (magY > highestY) - highestY = magY; - if (magY < lowestY) - lowestY = magY; - if (magZ > highestZ) - highestZ = magZ; - if (magZ < lowestZ) - lowestZ = magZ; + if (doCalibration) { - uint32_t now = millis(); - if (now > endCalibrationAt) { - doCalibration = false; - endCalibrationAt = 0; - showingScreen = false; - if (screen) - screen->endAlert(); + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration + showingScreen = true; + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); + } + + if (magX > highestX) + highestX = magX; + if (magX < lowestX) + lowestX = magX; + if (magY > highestY) + highestY = magY; + if (magY < lowestY) + lowestY = magY; + if (magZ > highestZ) + highestZ = magZ; + if (magZ < lowestZ) + lowestZ = magZ; + + uint32_t now = millis(); + if (now > endCalibrationAt) { + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + if (screen) + screen->endAlert(); + } + + // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, + // lowestY, highestY, lowestZ, highestZ); } - // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, - // highestX, - // lowestY, highestY, lowestZ, highestZ); - } + magX -= (highestX + lowestX) / 2; + magY -= (highestY + lowestY) / 2; + magZ -= (highestZ + lowestZ) / 2; + FusionVector ga, ma; + ga.axis.x = (sensor->agmt.acc.axes.x); + ga.axis.y = -(sensor->agmt.acc.axes.y); + ga.axis.z = -(sensor->agmt.acc.axes.z); + ma.axis.x = magX; + ma.axis.y = magY; + ma.axis.z = magZ; - magX -= (highestX + lowestX) / 2; - magY -= (highestY + lowestY) / 2; - magZ -= (highestZ + lowestZ) / 2; - FusionVector ga, ma; - ga.axis.x = (sensor->agmt.acc.axes.x); - ga.axis.y = -(sensor->agmt.acc.axes.y); - ga.axis.z = -(sensor->agmt.acc.axes.z); - ma.axis.x = magX; - ma.axis.y = magY; - ma.axis.z = magZ; + // If we're set to one of the inverted positions + if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { + ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); + ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); + } - // If we're set to one of the inverted positions - if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { - ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); - ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); - } + float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); - - switch (config.display.compass_orientation) { - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: - heading += 90; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: - heading += 180; - break; - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: - case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: - heading += 270; - break; - } - if (screen) - screen->setHeading(heading); + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); #endif - // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) - auto status = sensor->setBank(0); - if (sensor->status != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 isWakeOnMotion failed to set bank - %s", sensor->statusString()); - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } + // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) + auto status = sensor->setBank(0); + if (sensor->status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 isWakeOnMotion failed to set bank - %s", sensor->statusString()); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } - ICM_20948_INT_STATUS_t int_stat; - status = sensor->read(AGB0_REG_INT_STATUS, (uint8_t *)&int_stat, sizeof(ICM_20948_INT_STATUS_t)); - if (status != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 isWakeOnMotion failed to read interrupts - %s", sensor->statusString()); - return MOTION_SENSOR_CHECK_INTERVAL_MS; - } + ICM_20948_INT_STATUS_t int_stat; + status = sensor->read(AGB0_REG_INT_STATUS, (uint8_t *)&int_stat, sizeof(ICM_20948_INT_STATUS_t)); + if (status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 isWakeOnMotion failed to read interrupts - %s", sensor->statusString()); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } - if (int_stat.WOM_INT != 0) { - // Wake up! - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + if (int_stat.WOM_INT != 0) { + // Wake up! + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif -void ICM20948Sensor::calibrate(uint16_t forSeconds) { +void ICM20948Sensor::calibrate(uint16_t forSeconds) +{ #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", highestX, lowestX, - highestY, lowestY, highestZ, lowestZ); - LOG_DEBUG("BMX160 calibration started for %is", forSeconds); - if (sensor->dataReady()) { - sensor->getAGMT(); - highestX = sensor->agmt.mag.axes.x; - lowestX = sensor->agmt.mag.axes.x; - highestY = sensor->agmt.mag.axes.y; - lowestY = sensor->agmt.mag.axes.y; - highestZ = sensor->agmt.mag.axes.z; - lowestZ = sensor->agmt.mag.axes.z; - } else { - highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; - } + LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", + highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + if (sensor->dataReady()) { + sensor->getAGMT(); + highestX = sensor->agmt.mag.axes.x; + lowestX = sensor->agmt.mag.axes.x; + highestY = sensor->agmt.mag.axes.y; + lowestY = sensor->agmt.mag.axes.y; + highestZ = sensor->agmt.mag.axes.z; + lowestZ = sensor->agmt.mag.axes.z; + } else { + highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + } - doCalibration = true; - uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided - endCalibrationAt = millis() + calibrateFor; - if (screen) - screen->setEndCalibration(endCalibrationAt); + doCalibration = true; + uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided + endCalibrationAt = millis() + calibrateFor; + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } // ---------------------------------------------------------------------- @@ -192,11 +198,12 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds) { // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun ICM_20948_I2C -ICM20948Singleton *ICM20948Singleton::GetInstance() { - if (pinstance == nullptr) { - pinstance = new ICM20948Singleton(); - } - return pinstance; +ICM20948Singleton *ICM20948Singleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new ICM20948Singleton(); + } + return pinstance; } ICM20948Singleton::ICM20948Singleton() {} @@ -206,109 +213,114 @@ ICM20948Singleton::~ICM20948Singleton() {} ICM20948Singleton *ICM20948Singleton::pinstance{nullptr}; // Initialise the ICM20948 Sensor -bool ICM20948Singleton::init(ScanI2C::FoundDevice device) { +bool ICM20948Singleton::init(ScanI2C::FoundDevice device) +{ #ifdef ICM_20948_DEBUG - // Set ICM_20948_DEBUG to enable helpful debug messages on Serial - enableDebugging(); + // Set ICM_20948_DEBUG to enable helpful debug messages on Serial + enableDebugging(); #endif - // startup + // startup #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) - TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - TwoWire &bus = Wire; // fallback if only one I2C interface + TwoWire &bus = Wire; // fallback if only one I2C interface #endif - bool bAddr = (device.address.address == 0x69); - delay(100); + bool bAddr = (device.address.address == 0x69); + delay(100); - LOG_DEBUG("ICM20948 begin on addr 0x%02X (port=%d, bAddr=%d)", device.address.address, device.address.port, bAddr); + LOG_DEBUG("ICM20948 begin on addr 0x%02X (port=%d, bAddr=%d)", device.address.address, device.address.port, bAddr); - ICM_20948_Status_e status = begin(bus, bAddr); - if (status != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init begin - %s", statusString()); - return false; - } + ICM_20948_Status_e status = begin(bus, bAddr); + if (status != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init begin - %s", statusString()); + return false; + } - // SW reset to make sure the device starts in a known state - if (swReset() != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init reset - %s", statusString()); - return false; - } - delay(200); + // SW reset to make sure the device starts in a known state + if (swReset() != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init reset - %s", statusString()); + return false; + } + delay(200); - // Now wake the sensor up - if (sleep(false) != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init wake - %s", statusString()); - return false; - } + // Now wake the sensor up + if (sleep(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init wake - %s", statusString()); + return false; + } - if (lowPower(false) != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init high power - %s", statusString()); - return false; - } + if (lowPower(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init high power - %s", statusString()); + return false; + } - if (startupMagnetometer(false) != ICM_20948_Stat_Ok) { - LOG_DEBUG("ICM20948 init magnetometer - %s", statusString()); - return false; - } + if (startupMagnetometer(false) != ICM_20948_Stat_Ok) { + LOG_DEBUG("ICM20948 init magnetometer - %s", statusString()); + return false; + } #ifdef ICM_20948_INT_PIN - // Active low - cfgIntActiveLow(true); - LOG_DEBUG("ICM20948 init set cfgIntActiveLow - %s", statusString()); + // Active low + cfgIntActiveLow(true); + LOG_DEBUG("ICM20948 init set cfgIntActiveLow - %s", statusString()); - // Push-pull - cfgIntOpenDrain(false); - LOG_DEBUG("ICM20948 init set cfgIntOpenDrain - %s", statusString()); + // Push-pull + cfgIntOpenDrain(false); + LOG_DEBUG("ICM20948 init set cfgIntOpenDrain - %s", statusString()); - // If enabled, *ANY* read will clear the INT_STATUS register. - cfgIntAnyReadToClear(true); - LOG_DEBUG("ICM20948 init set cfgIntAnyReadToClear - %s", statusString()); + // If enabled, *ANY* read will clear the INT_STATUS register. + cfgIntAnyReadToClear(true); + LOG_DEBUG("ICM20948 init set cfgIntAnyReadToClear - %s", statusString()); - // Latch the interrupt until cleared - cfgIntLatch(true); - LOG_DEBUG("ICM20948 init set cfgIntLatch - %s", statusString()); + // Latch the interrupt until cleared + cfgIntLatch(true); + LOG_DEBUG("ICM20948 init set cfgIntLatch - %s", statusString()); - // Set up an interrupt pin with an internal pullup for active low - pinMode(ICM_20948_INT_PIN, INPUT_PULLUP); + // Set up an interrupt pin with an internal pullup for active low + pinMode(ICM_20948_INT_PIN, INPUT_PULLUP); - // Set up an interrupt service routine - attachInterrupt(ICM_20948_INT_PIN, ICM20948SetInterrupt, FALLING); + // Set up an interrupt service routine + attachInterrupt(ICM_20948_INT_PIN, ICM20948SetInterrupt, FALLING); #endif - return true; + return true; } #ifdef ICM_20948_DMP_IS_ENABLED // Stub -bool ICM20948Sensor::initDMP() { return false; } - -#endif - -bool ICM20948Singleton::setWakeOnMotion() { - // Set WoM threshold in milli G's - auto status = WOMThreshold(ICM_20948_WOM_THRESHOLD); - if (status != ICM_20948_Stat_Ok) +bool ICM20948Sensor::initDMP() +{ return false; - - // Enable WoM Logic mode 1 = Compare the current sample with the previous sample - status = WOMLogic(true, 1); - LOG_DEBUG("ICM20948 init set WOMLogic - %s", statusString()); - if (status != ICM_20948_Stat_Ok) - return false; - - // Enable interrupts on WakeOnMotion - status = intEnableWOM(true); - LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); - return status == ICM_20948_Stat_Ok; - - // Clear any current interrupts - ICM20948_IRQ = false; - clearInterrupts(); - return true; +} + +#endif + +bool ICM20948Singleton::setWakeOnMotion() +{ + // Set WoM threshold in milli G's + auto status = WOMThreshold(ICM_20948_WOM_THRESHOLD); + if (status != ICM_20948_Stat_Ok) + return false; + + // Enable WoM Logic mode 1 = Compare the current sample with the previous sample + status = WOMLogic(true, 1); + LOG_DEBUG("ICM20948 init set WOMLogic - %s", statusString()); + if (status != ICM_20948_Stat_Ok) + return false; + + // Enable interrupts on WakeOnMotion + status = intEnableWOM(true); + LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); + return status == ICM_20948_Stat_Ok; + + // Clear any current interrupts + ICM20948_IRQ = false; + clearInterrupts(); + return true; } #endif diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index dc6906adb..a9b7b69d0 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -46,56 +46,59 @@ extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper for the Sparkfun ICM_20948_I2C class -class ICM20948Singleton : public ICM_20948_I2C { -private: - static ICM20948Singleton *pinstance; +class ICM20948Singleton : public ICM_20948_I2C +{ + private: + static ICM20948Singleton *pinstance; -protected: - ICM20948Singleton(); - ~ICM20948Singleton(); + protected: + ICM20948Singleton(); + ~ICM20948Singleton(); -public: - // Create a singleton instance (not thread safe) - static ICM20948Singleton *GetInstance(); + public: + // Create a singleton instance (not thread safe) + static ICM20948Singleton *GetInstance(); - // Singletons should not be cloneable. - ICM20948Singleton(ICM20948Singleton &other) = delete; + // Singletons should not be cloneable. + ICM20948Singleton(ICM20948Singleton &other) = delete; - // Singletons should not be assignable. - void operator=(const ICM20948Singleton &) = delete; + // Singletons should not be assignable. + void operator=(const ICM20948Singleton &) = delete; - // Initialise the motion sensor singleton for normal operation - bool init(ScanI2C::FoundDevice device); + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); - // Enable Wake on Motion interrupts (sensor must be initialised first) - bool setWakeOnMotion(); + // Enable Wake on Motion interrupts (sensor must be initialised first) + bool setWakeOnMotion(); #ifdef ICM_20948_DMP_IS_ENABLED - // Initialise the motion sensor singleton for digital motion processing - bool initDMP(); + // Initialise the motion sensor singleton for digital motion processing + bool initDMP(); #endif }; -class ICM20948Sensor : public MotionSensor { -private: - ICM20948Singleton *sensor = nullptr; - bool showingScreen = false; +class ICM20948Sensor : public MotionSensor +{ + private: + ICM20948Singleton *sensor = nullptr; + bool showingScreen = false; #ifdef MUZI_BASE - bool isAsleep = false; - float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; + bool isAsleep = false; + float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, + lowestZ = 98.000000; #else - float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; + float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; #endif -public: - explicit ICM20948Sensor(ScanI2C::FoundDevice foundDevice); + public: + explicit ICM20948Sensor(ScanI2C::FoundDevice foundDevice); - // Initialise the motion sensor - virtual bool init() override; + // Initialise the motion sensor + virtual bool init() override; - // Called each time our sensor gets a chance to run - virtual int32_t runOnce() override; - virtual void calibrate(uint16_t forSeconds) override; + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; }; #endif diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp index bd8325699..903cc92f7 100755 --- a/src/motion/LIS3DHSensor.cpp +++ b/src/motion/LIS3DHSensor.cpp @@ -5,31 +5,33 @@ LIS3DHSensor::LIS3DHSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool LIS3DHSensor::init() { - if (sensor.begin(deviceAddress())) { - sensor.setRange(LIS3DH_RANGE_2_G); - // Adjust threshold, higher numbers are less sensitive - sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS); - LOG_DEBUG("LIS3DH init ok"); - return true; - } - LOG_DEBUG("LIS3DH init failed"); - return false; +bool LIS3DHSensor::init() +{ + if (sensor.begin(deviceAddress())) { + sensor.setRange(LIS3DH_RANGE_2_G); + // Adjust threshold, higher numbers are less sensitive + sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS); + LOG_DEBUG("LIS3DH init ok"); + return true; + } + LOG_DEBUG("LIS3DH init failed"); + return false; } -int32_t LIS3DHSensor::runOnce() { - if (sensor.getClick() > 0) { - uint8_t click = sensor.getClick(); - if (!config.device.double_tap_as_button_press && config.display.wake_on_tap_or_motion) { - wakeScreen(); - } +int32_t LIS3DHSensor::runOnce() +{ + if (sensor.getClick() > 0) { + uint8_t click = sensor.getClick(); + if (!config.device.double_tap_as_button_press && config.display.wake_on_tap_or_motion) { + wakeScreen(); + } - if (config.device.double_tap_as_button_press && (click & 0x20)) { - buttonPress(); - return 500; + if (config.device.double_tap_as_button_press && (click & 0x20)) { + buttonPress(); + return 500; + } } - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif diff --git a/src/motion/LIS3DHSensor.h b/src/motion/LIS3DHSensor.h index 8d9567b51..924b193e2 100755 --- a/src/motion/LIS3DHSensor.h +++ b/src/motion/LIS3DHSensor.h @@ -8,14 +8,15 @@ #include -class LIS3DHSensor : public MotionSensor { -private: - Adafruit_LIS3DH sensor; +class LIS3DHSensor : public MotionSensor +{ + private: + Adafruit_LIS3DH sensor; -public: - explicit LIS3DHSensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; + public: + explicit LIS3DHSensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/LSM6DS3Sensor.cpp b/src/motion/LSM6DS3Sensor.cpp index bf29f6721..7e2d7dfcd 100755 --- a/src/motion/LSM6DS3Sensor.cpp +++ b/src/motion/LSM6DS3Sensor.cpp @@ -5,28 +5,30 @@ LSM6DS3Sensor::LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool LSM6DS3Sensor::init() { - if (sensor.begin_I2C(deviceAddress())) { +bool LSM6DS3Sensor::init() +{ + if (sensor.begin_I2C(deviceAddress())) { - // Default threshold of 2G, less sensitive options are 4, 8 or 16G - sensor.setAccelRange(LSM6DS_ACCEL_RANGE_2_G); + // Default threshold of 2G, less sensitive options are 4, 8 or 16G + sensor.setAccelRange(LSM6DS_ACCEL_RANGE_2_G); - // Duration is number of occurrences needed to trigger, higher threshold is less sensitive - sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DS3_WAKE_THRESH); + // Duration is number of occurrences needed to trigger, higher threshold is less sensitive + sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DS3_WAKE_THRESH); - LOG_DEBUG("LSM6DS3 init ok"); - return true; - } - LOG_DEBUG("LSM6DS3 init failed"); - return false; + LOG_DEBUG("LSM6DS3 init ok"); + return true; + } + LOG_DEBUG("LSM6DS3 init failed"); + return false; } -int32_t LSM6DS3Sensor::runOnce() { - if (sensor.shake()) { - wakeScreen(); - return 500; - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t LSM6DS3Sensor::runOnce() +{ + if (sensor.shake()) { + wakeScreen(); + return 500; + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif \ No newline at end of file diff --git a/src/motion/LSM6DS3Sensor.h b/src/motion/LSM6DS3Sensor.h index 9f58b6ffc..8bf885149 100755 --- a/src/motion/LSM6DS3Sensor.h +++ b/src/motion/LSM6DS3Sensor.h @@ -12,14 +12,15 @@ #include -class LSM6DS3Sensor : public MotionSensor { -private: - Adafruit_LSM6DS3TRC sensor; +class LSM6DS3Sensor : public MotionSensor +{ + private: + Adafruit_LSM6DS3TRC sensor; -public: - explicit LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; + public: + explicit LSM6DS3Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/MPU6050Sensor.cpp b/src/motion/MPU6050Sensor.cpp index 2d9a34984..5d4f7bfdb 100755 --- a/src/motion/MPU6050Sensor.cpp +++ b/src/motion/MPU6050Sensor.cpp @@ -4,26 +4,28 @@ MPU6050Sensor::MPU6050Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool MPU6050Sensor::init() { - if (sensor.begin(deviceAddress())) { - // setup motion detection - sensor.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); - sensor.setMotionDetectionThreshold(1); - sensor.setMotionDetectionDuration(20); - sensor.setInterruptPinLatch(true); // Keep it latched. Will turn off when reinitialized. - sensor.setInterruptPinPolarity(true); - LOG_DEBUG("MPU6050 init ok"); - return true; - } - LOG_DEBUG("MPU6050 init failed"); - return false; +bool MPU6050Sensor::init() +{ + if (sensor.begin(deviceAddress())) { + // setup motion detection + sensor.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); + sensor.setMotionDetectionThreshold(1); + sensor.setMotionDetectionDuration(20); + sensor.setInterruptPinLatch(true); // Keep it latched. Will turn off when reinitialized. + sensor.setInterruptPinPolarity(true); + LOG_DEBUG("MPU6050 init ok"); + return true; + } + LOG_DEBUG("MPU6050 init failed"); + return false; } -int32_t MPU6050Sensor::runOnce() { - if (sensor.getMotionInterruptStatus()) { - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t MPU6050Sensor::runOnce() +{ + if (sensor.getMotionInterruptStatus()) { + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif \ No newline at end of file diff --git a/src/motion/MPU6050Sensor.h b/src/motion/MPU6050Sensor.h index b4dd1f385..2bca72b34 100755 --- a/src/motion/MPU6050Sensor.h +++ b/src/motion/MPU6050Sensor.h @@ -8,14 +8,15 @@ #include -class MPU6050Sensor : public MotionSensor { -private: - Adafruit_MPU6050 sensor; +class MPU6050Sensor : public MotionSensor +{ + private: + Adafruit_MPU6050 sensor; -public: - explicit MPU6050Sensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; + public: + explicit MPU6050Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index dd2159919..d0bfe4e2c 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -8,63 +8,76 @@ char timeRemainingBuffer[12]; // screen is defined in main.cpp extern graphics::Screen *screen; -MotionSensor::MotionSensor(ScanI2C::FoundDevice foundDevice) { - device.address.address = foundDevice.address.address; - device.address.port = foundDevice.address.port; - device.type = foundDevice.type; - LOG_DEBUG("Motion MotionSensor port: %s address: 0x%x type: %d", devicePort() == ScanI2C::I2CPort::WIRE1 ? "Wire1" : "Wire", - (uint8_t)deviceAddress(), deviceType()); +MotionSensor::MotionSensor(ScanI2C::FoundDevice foundDevice) +{ + device.address.address = foundDevice.address.address; + device.address.port = foundDevice.address.port; + device.type = foundDevice.type; + LOG_DEBUG("Motion MotionSensor port: %s address: 0x%x type: %d", devicePort() == ScanI2C::I2CPort::WIRE1 ? "Wire1" : "Wire", + (uint8_t)deviceAddress(), deviceType()); } -ScanI2C::DeviceType MotionSensor::deviceType() { return device.type; } +ScanI2C::DeviceType MotionSensor::deviceType() +{ + return device.type; +} -uint8_t MotionSensor::deviceAddress() { return device.address.address; } +uint8_t MotionSensor::deviceAddress() +{ + return device.address.address; +} -ScanI2C::I2CPort MotionSensor::devicePort() { return device.address.port; } +ScanI2C::I2CPort MotionSensor::devicePort() +{ + return device.address.port; +} #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - if (screen == nullptr) - return; - // int x_offset = display->width() / 2; - // int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - display->drawString(x, y, "Calibrating\nCompass"); +void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + if (screen == nullptr) + return; + // int x_offset = display->width() / 2; + // int y_offset = display->height() <= 80 ? 0 : 32; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + display->drawString(x, y, "Calibrating\nCompass"); - uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; - sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); - display->setFont(FONT_SMALL); - display->drawString(x, y + 40, timeRemainingBuffer); + uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; + sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); + display->setFont(FONT_SMALL); + display->drawString(x, y + 40, timeRemainingBuffer); - int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); + int16_t compassX = 0, compassY = 0; + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); - // coordinates for the center of the compass/circle - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + display->getHeight() / 2; - } else { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; - } - display->drawCircle(compassX, compassY, compassDiam / 2); - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); + // coordinates for the center of the compass/circle + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + display->getHeight() / 2; + } else { + compassX = x + display->getWidth() - compassDiam / 2 - 5; + compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; + } + display->drawCircle(compassX, compassY, compassDiam / 2); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } #endif #if !MESHTASTIC_EXCLUDE_POWER_FSM -void MotionSensor::wakeScreen() { - if (powerFSM.getState() == &stateDARK) { - LOG_DEBUG("Motion wakeScreen detected"); - if (config.display.wake_on_tap_or_motion) - powerFSM.trigger(EVENT_INPUT); - } +void MotionSensor::wakeScreen() +{ + if (powerFSM.getState() == &stateDARK) { + LOG_DEBUG("Motion wakeScreen detected"); + if (config.display.wake_on_tap_or_motion) + powerFSM.trigger(EVENT_INPUT); + } } -void MotionSensor::buttonPress() { - LOG_DEBUG("Motion buttonPress detected"); - powerFSM.trigger(EVENT_PRESS); +void MotionSensor::buttonPress() +{ + LOG_DEBUG("Motion buttonPress detected"); + powerFSM.trigger(EVENT_PRESS); } #else diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 2f0eca2c5..8eb3bf95b 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -17,47 +17,48 @@ #include "Wire.h" // Base class for motion processing -class MotionSensor { -public: - explicit MotionSensor(ScanI2C::FoundDevice foundDevice); - virtual ~MotionSensor(){}; +class MotionSensor +{ + public: + explicit MotionSensor(ScanI2C::FoundDevice foundDevice); + virtual ~MotionSensor(){}; - // Get the device type - ScanI2C::DeviceType deviceType(); + // Get the device type + ScanI2C::DeviceType deviceType(); - // Get the device address - uint8_t deviceAddress(); + // Get the device address + uint8_t deviceAddress(); - // Get the device port - ScanI2C::I2CPort devicePort(); + // Get the device port + ScanI2C::I2CPort devicePort(); - // Initialise the motion sensor - inline virtual bool init() { return false; }; + // Initialise the motion sensor + inline virtual bool init() { return false; }; - // The method that will be called each time our sensor gets a chance to run - // Returns the desired period for next invocation (or RUN_SAME for no change) - // Refer to /src/concurrency/OSThread.h for more information - inline virtual int32_t runOnce() { return MOTION_SENSOR_CHECK_INTERVAL_MS; }; + // The method that will be called each time our sensor gets a chance to run + // Returns the desired period for next invocation (or RUN_SAME for no change) + // Refer to /src/concurrency/OSThread.h for more information + inline virtual int32_t runOnce() { return MOTION_SENSOR_CHECK_INTERVAL_MS; }; - virtual void calibrate(uint16_t forSeconds){}; + virtual void calibrate(uint16_t forSeconds){}; -protected: - // Turn on the screen when a tap or motion is detected - virtual void wakeScreen(); + protected: + // Turn on the screen when a tap or motion is detected + virtual void wakeScreen(); - // Register a button press when a double-tap is detected - virtual void buttonPress(); + // Register a button press when a double-tap is detected + virtual void buttonPress(); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN - // draw an OLED frame (currently only used by the RAK4631 BMX160 sensor) - static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + // draw an OLED frame (currently only used by the RAK4631 BMX160 sensor) + static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif - ScanI2C::FoundDevice device; + ScanI2C::FoundDevice device; - // Do calibration if true - bool doCalibration = false; - uint32_t endCalibrationAt = 0; + // Do calibration if true + bool doCalibration = false; + uint32_t endCalibrationAt = 0; }; #endif diff --git a/src/motion/QMA6100PSensor.cpp b/src/motion/QMA6100PSensor.cpp index acf93e660..a04837e80 100644 --- a/src/motion/QMA6100PSensor.cpp +++ b/src/motion/QMA6100PSensor.cpp @@ -6,47 +6,53 @@ volatile static bool QMA6100P_IRQ = false; // Interrupt service routine -void QMA6100PSetInterrupt() { QMA6100P_IRQ = true; } +void QMA6100PSetInterrupt() +{ + QMA6100P_IRQ = true; +} QMA6100PSensor::QMA6100PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} -bool QMA6100PSensor::init() { - // Initialise the sensor - sensor = QMA6100PSingleton::GetInstance(); - if (!sensor->init(device)) - return false; +bool QMA6100PSensor::init() +{ + // Initialise the sensor + sensor = QMA6100PSingleton::GetInstance(); + if (!sensor->init(device)) + return false; - // Enable simple Wake on Motion - return sensor->setWakeOnMotion(); + // Enable simple Wake on Motion + return sensor->setWakeOnMotion(); } #ifdef QMA_6100P_INT_PIN -int32_t QMA6100PSensor::runOnce() { - // Wake on motion using hardware interrupts - this is the most efficient way to check for motion - if (QMA6100P_IRQ) { - QMA6100P_IRQ = false; - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; +int32_t QMA6100PSensor::runOnce() +{ + // Wake on motion using hardware interrupts - this is the most efficient way to check for motion + if (QMA6100P_IRQ) { + QMA6100P_IRQ = false; + wakeScreen(); + } + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #else -int32_t QMA6100PSensor::runOnce() { - // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) +int32_t QMA6100PSensor::runOnce() +{ + // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) - uint8_t tempVal; - if (!sensor->readRegisterRegion(SFE_QMA6100P_INT_ST0, &tempVal, 1)) { - LOG_DEBUG("QMA6100PS isWakeOnMotion failed to read interrupts"); + uint8_t tempVal; + if (!sensor->readRegisterRegion(SFE_QMA6100P_INT_ST0, &tempVal, 1)) { + LOG_DEBUG("QMA6100PS isWakeOnMotion failed to read interrupts"); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + if ((tempVal & 7) != 0) { + // Wake up! + wakeScreen(); + } return MOTION_SENSOR_CHECK_INTERVAL_MS; - } - - if ((tempVal & 7) != 0) { - // Wake up! - wakeScreen(); - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif @@ -56,11 +62,12 @@ int32_t QMA6100PSensor::runOnce() { // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun QMA_6100P_I2C -QMA6100PSingleton *QMA6100PSingleton::GetInstance() { - if (pinstance == nullptr) { - pinstance = new QMA6100PSingleton(); - } - return pinstance; +QMA6100PSingleton *QMA6100PSingleton::GetInstance() +{ + if (pinstance == nullptr) { + pinstance = new QMA6100PSingleton(); + } + return pinstance; } QMA6100PSingleton::QMA6100PSingleton() {} @@ -70,105 +77,107 @@ QMA6100PSingleton::~QMA6100PSingleton() {} QMA6100PSingleton *QMA6100PSingleton::pinstance{nullptr}; // Initialise the QMA6100P Sensor -bool QMA6100PSingleton::init(ScanI2C::FoundDevice device) { +bool QMA6100PSingleton::init(ScanI2C::FoundDevice device) +{ // startup #ifdef Wire1 - bool status = begin(device.address.address, device.address.port == ScanI2C::I2CPort::WIRE1 ? &Wire1 : &Wire); + bool status = begin(device.address.address, device.address.port == ScanI2C::I2CPort::WIRE1 ? &Wire1 : &Wire); #else - // check chip id - bool status = begin(device.address.address, &Wire); + // check chip id + bool status = begin(device.address.address, &Wire); #endif - if (status != true) { - LOG_WARN("QMA6100P init begin failed"); - return false; - } - delay(20); - // SW reset to make sure the device starts in a known state - if (softwareReset() != true) { - LOG_WARN("QMA6100P init reset failed"); - return false; - } - delay(20); - // Set range - if (!setRange(QMA_6100P_MPU_ACCEL_SCALE)) { - LOG_WARN("QMA6100P init range failed"); - return false; - } - // set active mode - if (!enableAccel()) { - LOG_WARN("ERROR QMA6100P active mode set failed"); - } - // set calibrateoffsets - if (!calibrateOffsets()) { - LOG_WARN("ERROR QMA6100P calibration failed"); - } + if (status != true) { + LOG_WARN("QMA6100P init begin failed"); + return false; + } + delay(20); + // SW reset to make sure the device starts in a known state + if (softwareReset() != true) { + LOG_WARN("QMA6100P init reset failed"); + return false; + } + delay(20); + // Set range + if (!setRange(QMA_6100P_MPU_ACCEL_SCALE)) { + LOG_WARN("QMA6100P init range failed"); + return false; + } + // set active mode + if (!enableAccel()) { + LOG_WARN("ERROR QMA6100P active mode set failed"); + } + // set calibrateoffsets + if (!calibrateOffsets()) { + LOG_WARN("ERROR QMA6100P calibration failed"); + } #ifdef QMA_6100P_INT_PIN - // Active low & Open Drain - uint8_t tempVal; - if (!readRegisterRegion(SFE_QMA6100P_INTPINT_CONF, &tempVal, 1)) { - LOG_WARN("QMA6100P init failed to read interrupt pin config"); - return false; - } + // Active low & Open Drain + uint8_t tempVal; + if (!readRegisterRegion(SFE_QMA6100P_INTPINT_CONF, &tempVal, 1)) { + LOG_WARN("QMA6100P init failed to read interrupt pin config"); + return false; + } - tempVal |= 0b00000010; // Active low & Open Drain + tempVal |= 0b00000010; // Active low & Open Drain - if (!writeRegisterByte(SFE_QMA6100P_INTPINT_CONF, tempVal)) { - LOG_WARN("QMA6100P init failed to write interrupt pin config"); - return false; - } + if (!writeRegisterByte(SFE_QMA6100P_INTPINT_CONF, tempVal)) { + LOG_WARN("QMA6100P init failed to write interrupt pin config"); + return false; + } - // Latch until cleared, all reads clear the latch - if (!readRegisterRegion(SFE_QMA6100P_INT_CFG, &tempVal, 1)) { - LOG_WARN("QMA6100P init failed to read interrupt config"); - return false; - } + // Latch until cleared, all reads clear the latch + if (!readRegisterRegion(SFE_QMA6100P_INT_CFG, &tempVal, 1)) { + LOG_WARN("QMA6100P init failed to read interrupt config"); + return false; + } - tempVal |= 0b10000001; // Latch until cleared, INT_RD_CLR1 + tempVal |= 0b10000001; // Latch until cleared, INT_RD_CLR1 - if (!writeRegisterByte(SFE_QMA6100P_INT_CFG, tempVal)) { - LOG_WARN("QMA6100P init failed to write interrupt config"); - return false; - } - // Set up an interrupt pin with an internal pullup for active low - pinMode(QMA_6100P_INT_PIN, INPUT_PULLUP); + if (!writeRegisterByte(SFE_QMA6100P_INT_CFG, tempVal)) { + LOG_WARN("QMA6100P init failed to write interrupt config"); + return false; + } + // Set up an interrupt pin with an internal pullup for active low + pinMode(QMA_6100P_INT_PIN, INPUT_PULLUP); - // Set up an interrupt service routine - attachInterrupt(QMA_6100P_INT_PIN, QMA6100PSetInterrupt, FALLING); + // Set up an interrupt service routine + attachInterrupt(QMA_6100P_INT_PIN, QMA6100PSetInterrupt, FALLING); #endif - return true; + return true; } -bool QMA6100PSingleton::setWakeOnMotion() { - // Enable 'Any Motion' interrupt - if (!writeRegisterByte(SFE_QMA6100P_INT_EN2, 0b00000111)) { - LOG_WARN("QMA6100P :setWakeOnMotion failed to write interrupt enable"); - return false; - } +bool QMA6100PSingleton::setWakeOnMotion() +{ + // Enable 'Any Motion' interrupt + if (!writeRegisterByte(SFE_QMA6100P_INT_EN2, 0b00000111)) { + LOG_WARN("QMA6100P :setWakeOnMotion failed to write interrupt enable"); + return false; + } - // Set 'Significant Motion' interrupt map to INT1 - uint8_t tempVal; + // Set 'Significant Motion' interrupt map to INT1 + uint8_t tempVal; - if (!readRegisterRegion(SFE_QMA6100P_INT_MAP1, &tempVal, 1)) { - LOG_WARN("QMA6100P setWakeOnMotion failed to read interrupt map"); - return false; - } + if (!readRegisterRegion(SFE_QMA6100P_INT_MAP1, &tempVal, 1)) { + LOG_WARN("QMA6100P setWakeOnMotion failed to read interrupt map"); + return false; + } - sfe_qma6100p_int_map1_bitfield_t int_map1; - int_map1.all = tempVal; - int_map1.bits.int1_any_mot = 1; // any motion interrupt to INT1 - tempVal = int_map1.all; + sfe_qma6100p_int_map1_bitfield_t int_map1; + int_map1.all = tempVal; + int_map1.bits.int1_any_mot = 1; // any motion interrupt to INT1 + tempVal = int_map1.all; - if (!writeRegisterByte(SFE_QMA6100P_INT_MAP1, tempVal)) { - LOG_WARN("QMA6100P setWakeOnMotion failed to write interrupt map"); - return false; - } + if (!writeRegisterByte(SFE_QMA6100P_INT_MAP1, tempVal)) { + LOG_WARN("QMA6100P setWakeOnMotion failed to write interrupt map"); + return false; + } - // Clear any current interrupts - QMA6100P_IRQ = false; - return true; + // Clear any current interrupts + QMA6100P_IRQ = false; + return true; } #endif diff --git a/src/motion/QMA6100PSensor.h b/src/motion/QMA6100PSensor.h index 61d33d4e4..72e716ef9 100644 --- a/src/motion/QMA6100PSensor.h +++ b/src/motion/QMA6100PSensor.h @@ -17,43 +17,45 @@ extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper for the Sparkfun QMA_6100P_I2C class -class QMA6100PSingleton : public QMA6100P { -private: - static QMA6100PSingleton *pinstance; +class QMA6100PSingleton : public QMA6100P +{ + private: + static QMA6100PSingleton *pinstance; -protected: - QMA6100PSingleton(); - ~QMA6100PSingleton(); + protected: + QMA6100PSingleton(); + ~QMA6100PSingleton(); -public: - // Create a singleton instance (not thread safe) - static QMA6100PSingleton *GetInstance(); + public: + // Create a singleton instance (not thread safe) + static QMA6100PSingleton *GetInstance(); - // Singletons should not be cloneable. - QMA6100PSingleton(QMA6100PSingleton &other) = delete; + // Singletons should not be cloneable. + QMA6100PSingleton(QMA6100PSingleton &other) = delete; - // Singletons should not be assignable. - void operator=(const QMA6100PSingleton &) = delete; + // Singletons should not be assignable. + void operator=(const QMA6100PSingleton &) = delete; - // Initialise the motion sensor singleton for normal operation - bool init(ScanI2C::FoundDevice device); + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); - // Enable Wake on Motion interrupts (sensor must be initialised first) - bool setWakeOnMotion(); + // Enable Wake on Motion interrupts (sensor must be initialised first) + bool setWakeOnMotion(); }; -class QMA6100PSensor : public MotionSensor { -private: - QMA6100PSingleton *sensor = nullptr; +class QMA6100PSensor : public MotionSensor +{ + private: + QMA6100PSingleton *sensor = nullptr; -public: - explicit QMA6100PSensor(ScanI2C::FoundDevice foundDevice); + public: + explicit QMA6100PSensor(ScanI2C::FoundDevice foundDevice); - // Initialise the motion sensor - virtual bool init() override; + // Initialise the motion sensor + virtual bool init() override; - // Called each time our sensor gets a chance to run - virtual int32_t runOnce() override; + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; }; #endif diff --git a/src/motion/STK8XXXSensor.cpp b/src/motion/STK8XXXSensor.cpp index 8fb717fc4..d27a1e88d 100755 --- a/src/motion/STK8XXXSensor.cpp +++ b/src/motion/STK8XXXSensor.cpp @@ -8,29 +8,31 @@ STK8XXXSensor::STK8XXXSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::M volatile static bool STK_IRQ; -bool STK8XXXSensor::init() { - if (sensor.STK8xxx_Initialization(STK8xxx_VAL_RANGE_2G)) { - STK_IRQ = false; - sensor.STK8xxx_Anymotion_init(); - pinMode(STK8XXX_INT, INPUT_PULLUP); - attachInterrupt( - digitalPinToInterrupt(STK8XXX_INT), [] { STK_IRQ = true; }, RISING); +bool STK8XXXSensor::init() +{ + if (sensor.STK8xxx_Initialization(STK8xxx_VAL_RANGE_2G)) { + STK_IRQ = false; + sensor.STK8xxx_Anymotion_init(); + pinMode(STK8XXX_INT, INPUT_PULLUP); + attachInterrupt( + digitalPinToInterrupt(STK8XXX_INT), [] { STK_IRQ = true; }, RISING); - LOG_DEBUG("STK8XXX init ok"); - return true; - } - LOG_DEBUG("STK8XXX init failed"); - return false; + LOG_DEBUG("STK8XXX init ok"); + return true; + } + LOG_DEBUG("STK8XXX init failed"); + return false; } -int32_t STK8XXXSensor::runOnce() { - if (STK_IRQ) { - STK_IRQ = false; - if (config.display.wake_on_tap_or_motion) { - wakeScreen(); +int32_t STK8XXXSensor::runOnce() +{ + if (STK_IRQ) { + STK_IRQ = false; + if (config.display.wake_on_tap_or_motion) { + wakeScreen(); + } } - } - return MOTION_SENSOR_CHECK_INTERVAL_MS; + return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif diff --git a/src/motion/STK8XXXSensor.h b/src/motion/STK8XXXSensor.h index 47abf98dd..f54bc7707 100755 --- a/src/motion/STK8XXXSensor.h +++ b/src/motion/STK8XXXSensor.h @@ -10,22 +10,24 @@ #include -class STK8XXXSensor : public MotionSensor { -private: - STK8xxx sensor; +class STK8XXXSensor : public MotionSensor +{ + private: + STK8xxx sensor; -public: - explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); - virtual bool init() override; - virtual int32_t runOnce() override; + public: + explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; }; #else // Stub -class STK8XXXSensor : public MotionSensor { -public: - explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); +class STK8XXXSensor : public MotionSensor +{ + public: + explicit STK8XXXSensor(ScanI2C::FoundDevice foundDevice); }; #endif diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 376be2956..7c33f0360 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -43,7 +43,8 @@ MQTT *mqtt; -namespace { +namespace +{ constexpr int reconnectMax = 5; // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets @@ -52,814 +53,856 @@ static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for cha static bool isMqttServerAddressPrivate = false; static bool isConnected = false; -inline void onReceiveProto(char *topic, byte *payload, size_t length) { - const DecodedServiceEnvelope e(payload, length); - if (!e.validDecode || e.channel_id == NULL || e.gateway_id == NULL || e.packet == NULL) { - LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); - return; - } - - const meshtastic_Channel &ch = channels.getByName(e.channel_id); - // Find channel by channel_id and check downlink_enabled - if (!(strcmp(e.channel_id, "PKI") == 0 || (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { - return; - } - - bool anyChannelHasDownlink = false; - size_t numChan = channels.getNumChannels(); - for (size_t i = 0; i < numChan; ++i) { - const auto &c = channels.getByIndex(i); - if (c.settings.downlink_enabled) { - anyChannelHasDownlink = true; - break; +inline void onReceiveProto(char *topic, byte *payload, size_t length) +{ + const DecodedServiceEnvelope e(payload, length); + if (!e.validDecode || e.channel_id == NULL || e.gateway_id == NULL || e.packet == NULL) { + LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); + return; } - } - if (strcmp(e.channel_id, "PKI") == 0 && !anyChannelHasDownlink) { - return; - } - // Generate node ID from nodenum for comparison - std::string nodeId = nodeDB->getNodeId(); - if (strcmp(e.gateway_id, nodeId.c_str()) == 0) { - // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. - // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node - // receives it when we get our own packet back. Then we'll stop our retransmissions. + const meshtastic_Channel &ch = channels.getByName(e.channel_id); + // Find channel by channel_id and check downlink_enabled + if (!(strcmp(e.channel_id, "PKI") == 0 || + (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { + return; + } + + bool anyChannelHasDownlink = false; + size_t numChan = channels.getNumChannels(); + for (size_t i = 0; i < numChan; ++i) { + const auto &c = channels.getByIndex(i); + if (c.settings.downlink_enabled) { + anyChannelHasDownlink = true; + break; + } + } + + if (strcmp(e.channel_id, "PKI") == 0 && !anyChannelHasDownlink) { + return; + } + // Generate node ID from nodenum for comparison + std::string nodeId = nodeDB->getNodeId(); + if (strcmp(e.gateway_id, nodeId.c_str()) == 0) { + // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. + // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node + // receives it when we get our own packet back. Then we'll stop our retransmissions. + if (isFromUs(e.packet)) { + auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); + pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + router->sendLocal(pAck); + } else { + LOG_INFO("Ignore downlink message we originally sent"); + } + return; + } if (isFromUs(e.packet)) { - auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); - pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; - router->sendLocal(pAck); - } else { - LOG_INFO("Ignore downlink message we originally sent"); + LOG_INFO("Ignore downlink message we originally sent"); + return; } - return; - } - if (isFromUs(e.packet)) { - LOG_INFO("Ignore downlink message we originally sent"); - return; - } - LOG_INFO("Received MQTT topic %s, len=%u", topic, length); - if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) { - LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start); - return; - } - - UniquePacketPoolPacket p = packetPool.allocUniqueZeroed(); - p->from = e.packet->from; - p->to = e.packet->to; - p->id = e.packet->id; - p->channel = e.packet->channel; - p->hop_limit = e.packet->hop_limit; - p->hop_start = e.packet->hop_start; - p->want_ack = e.packet->want_ack; - p->via_mqtt = true; // Mark that the packet was received via MQTT - p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; - p->which_payload_variant = e.packet->which_payload_variant; - memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted))); - - if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - if (moduleConfig.mqtt.encryption_enabled) { - LOG_INFO("Ignore decoded message on MQTT, encryption is enabled"); - return; + LOG_INFO("Received MQTT topic %s, len=%u", topic, length); + if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) { + LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start); + return; } - if (p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { - LOG_INFO("Ignore decoded admin packet"); - return; - } - p->channel = ch.index; - } - // PKI messages get accepted even if we can't decrypt - if (router && p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && strcmp(e.channel_id, "PKI") == 0) { - const meshtastic_NodeInfoLite *tx = nodeDB->getMeshNode(getFrom(p.get())); - const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); - // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's - // likely they discovered each other via a channel we have downlink enabled for - if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) - router->enqueueReceivedMessage(p.release()); - } else if (router && perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key - router->enqueueReceivedMessage(p.release()); + UniquePacketPoolPacket p = packetPool.allocUniqueZeroed(); + p->from = e.packet->from; + p->to = e.packet->to; + p->id = e.packet->id; + p->channel = e.packet->channel; + p->hop_limit = e.packet->hop_limit; + p->hop_start = e.packet->hop_start; + p->want_ack = e.packet->want_ack; + p->via_mqtt = true; // Mark that the packet was received via MQTT + p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + p->which_payload_variant = e.packet->which_payload_variant; + memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted))); + + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + if (moduleConfig.mqtt.encryption_enabled) { + LOG_INFO("Ignore decoded message on MQTT, encryption is enabled"); + return; + } + if (p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { + LOG_INFO("Ignore decoded admin packet"); + return; + } + p->channel = ch.index; + } + + // PKI messages get accepted even if we can't decrypt + if (router && p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && strcmp(e.channel_id, "PKI") == 0) { + const meshtastic_NodeInfoLite *tx = nodeDB->getMeshNode(getFrom(p.get())); + const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); + // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's + // likely they discovered each other via a channel we have downlink enabled for + if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) + router->enqueueReceivedMessage(p.release()); + } else if (router && + perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key + router->enqueueReceivedMessage(p.release()); } #if !defined(ARCH_NRF52) || NRF52_USE_JSON // returns true if this is a valid JSON envelope which we accept on downlink -inline bool isValidJsonEnvelope(JSONObject &json) { - // Generate node ID from nodenum for comparison - std::string nodeId = nodeDB->getNodeId(); - // if "sender" is provided, avoid processing packets we uplinked - return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) && - (json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number - (json.find("from") != json.end()) && json["from"]->IsNumber() && - (json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us - (json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type - (json.find("payload") != json.end()); // should have a payload +inline bool isValidJsonEnvelope(JSONObject &json) +{ + // Generate node ID from nodenum for comparison + std::string nodeId = nodeDB->getNodeId(); + // if "sender" is provided, avoid processing packets we uplinked + return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) && + (json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number + (json.find("from") != json.end()) && json["from"]->IsNumber() && + (json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us + (json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type + (json.find("payload") != json.end()); // should have a payload } -inline void onReceiveJson(byte *payload, size_t length) { - char payloadStr[length + 1]; - memcpy(payloadStr, payload, length); - payloadStr[length] = 0; // null terminated string - std::unique_ptr json_value(JSON::Parse(payloadStr)); - if (json_value == nullptr) { - LOG_ERROR("JSON received payload on MQTT but not a valid JSON"); - return; - } - - JSONObject json; - json = json_value->AsObject(); - - if (!isValidJsonEnvelope(json)) { - LOG_ERROR("JSON received payload on MQTT but not a valid envelope"); - return; - } - - // this is a valid envelope - if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) { - std::string jsonPayloadStr = json["payload"]->AsString(); - LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length()); - - // construct protobuf data packet using TEXT_MESSAGE, send it to the mesh - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; - if (json.find("channel") != json.end() && json["channel"]->IsNumber() && (json["channel"]->AsNumber() < channels.getNumChannels())) - p->channel = json["channel"]->AsNumber(); - if (json.find("to") != json.end() && json["to"]->IsNumber()) - p->to = json["to"]->AsNumber(); - if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) - p->hop_limit = json["hopLimit"]->AsNumber(); - if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) { - memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length()); - p->decoded.payload.size = jsonPayloadStr.length(); - service->sendToMesh(p, RX_SRC_LOCAL); - } else { - LOG_WARN("Received MQTT json payload too long, drop"); +inline void onReceiveJson(byte *payload, size_t length) +{ + char payloadStr[length + 1]; + memcpy(payloadStr, payload, length); + payloadStr[length] = 0; // null terminated string + std::unique_ptr json_value(JSON::Parse(payloadStr)); + if (json_value == nullptr) { + LOG_ERROR("JSON received payload on MQTT but not a valid JSON"); + return; } - } else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) { - // invent the "sendposition" type for a valid envelope - JSONObject posit; - posit = json["payload"]->AsObject(); // get nested JSON Position - meshtastic_Position pos = meshtastic_Position_init_default; - if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber()) - pos.latitude_i = posit["latitude_i"]->AsNumber(); - if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber()) - pos.longitude_i = posit["longitude_i"]->AsNumber(); - if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber()) - pos.altitude = posit["altitude"]->AsNumber(); - if (posit.find("time") != posit.end() && posit["time"]->IsNumber()) - pos.time = posit["time"]->AsNumber(); - // construct protobuf data packet using POSITION, send it to the mesh - meshtastic_MeshPacket *p = router->allocForSending(); - p->decoded.portnum = meshtastic_PortNum_POSITION_APP; - if (json.find("channel") != json.end() && json["channel"]->IsNumber() && (json["channel"]->AsNumber() < channels.getNumChannels())) - p->channel = json["channel"]->AsNumber(); - if (json.find("to") != json.end() && json["to"]->IsNumber()) - p->to = json["to"]->AsNumber(); - if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) - p->hop_limit = json["hopLimit"]->AsNumber(); - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg, - &pos); // make the Data protobuf from position - service->sendToMesh(p, RX_SRC_LOCAL); - } else { - LOG_DEBUG("JSON ignore downlink message with unsupported type"); - } + JSONObject json; + json = json_value->AsObject(); + + if (!isValidJsonEnvelope(json)) { + LOG_ERROR("JSON received payload on MQTT but not a valid envelope"); + return; + } + + // this is a valid envelope + if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) { + std::string jsonPayloadStr = json["payload"]->AsString(); + LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length()); + + // construct protobuf data packet using TEXT_MESSAGE, send it to the mesh + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + if (json.find("channel") != json.end() && json["channel"]->IsNumber() && + (json["channel"]->AsNumber() < channels.getNumChannels())) + p->channel = json["channel"]->AsNumber(); + if (json.find("to") != json.end() && json["to"]->IsNumber()) + p->to = json["to"]->AsNumber(); + if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) + p->hop_limit = json["hopLimit"]->AsNumber(); + if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) { + memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length()); + p->decoded.payload.size = jsonPayloadStr.length(); + service->sendToMesh(p, RX_SRC_LOCAL); + } else { + LOG_WARN("Received MQTT json payload too long, drop"); + } + } else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) { + // invent the "sendposition" type for a valid envelope + JSONObject posit; + posit = json["payload"]->AsObject(); // get nested JSON Position + meshtastic_Position pos = meshtastic_Position_init_default; + if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber()) + pos.latitude_i = posit["latitude_i"]->AsNumber(); + if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber()) + pos.longitude_i = posit["longitude_i"]->AsNumber(); + if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber()) + pos.altitude = posit["altitude"]->AsNumber(); + if (posit.find("time") != posit.end() && posit["time"]->IsNumber()) + pos.time = posit["time"]->AsNumber(); + + // construct protobuf data packet using POSITION, send it to the mesh + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_POSITION_APP; + if (json.find("channel") != json.end() && json["channel"]->IsNumber() && + (json["channel"]->AsNumber() < channels.getNumChannels())) + p->channel = json["channel"]->AsNumber(); + if (json.find("to") != json.end() && json["to"]->IsNumber()) + p->to = json["to"]->AsNumber(); + if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) + p->hop_limit = json["hopLimit"]->AsNumber(); + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg, + &pos); // make the Data protobuf from position + service->sendToMesh(p, RX_SRC_LOCAL); + } else { + LOG_DEBUG("JSON ignore downlink message with unsupported type"); + } } #endif /// Determines if the given IPAddress is a private IPv4 address, i.e. not routable on the public internet. -bool isPrivateIpAddress(const IPAddress &ip) { - constexpr struct { - uint32_t network; - uint32_t mask; - } privateCidrRanges[] = { - {.network = 192u << 24 | 168 << 16, .mask = 0xffff0000}, // 192.168.0.0/16 - {.network = 172u << 24 | 16 << 16, .mask = 0xfff00000}, // 172.16.0.0/12 - {.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16 - {.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8 - {.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32 - {.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10 - }; - const uint32_t addr = ntohl(ip); - for (const auto &cidrRange : privateCidrRanges) { - if (cidrRange.network == (addr & cidrRange.mask)) { - LOG_INFO("MQTT server on a private IP"); - return true; +bool isPrivateIpAddress(const IPAddress &ip) +{ + constexpr struct { + uint32_t network; + uint32_t mask; + } privateCidrRanges[] = { + {.network = 192u << 24 | 168 << 16, .mask = 0xffff0000}, // 192.168.0.0/16 + {.network = 172u << 24 | 16 << 16, .mask = 0xfff00000}, // 172.16.0.0/12 + {.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16 + {.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8 + {.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32 + {.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10 + }; + const uint32_t addr = ntohl(ip); + for (const auto &cidrRange : privateCidrRanges) { + if (cidrRange.network == (addr & cidrRange.mask)) { + LOG_INFO("MQTT server on a private IP"); + return true; + } } - } - return false; + return false; } // Separate a [:] string. Returns a pair containing the parsed host and port. If the port is // not present in the input string, or is invalid, the value of the `port` argument will be returned. -std::pair parseHostAndPort(String server, uint16_t port = 0) { - const int delimIndex = server.indexOf(':'); - if (delimIndex > 0) { - const long parsedPort = server.substring(delimIndex + 1, server.length()).toInt(); - if (parsedPort < 1 || parsedPort > UINT16_MAX) { - LOG_WARN("Invalid MQTT port %d: %s", parsedPort, server.c_str()); - } else { - port = parsedPort; +std::pair parseHostAndPort(String server, uint16_t port = 0) +{ + const int delimIndex = server.indexOf(':'); + if (delimIndex > 0) { + const long parsedPort = server.substring(delimIndex + 1, server.length()).toInt(); + if (parsedPort < 1 || parsedPort > UINT16_MAX) { + LOG_WARN("Invalid MQTT port %d: %s", parsedPort, server.c_str()); + } else { + port = parsedPort; + } + server[delimIndex] = 0; } - server[delimIndex] = 0; - } - return std::make_pair(std::move(server), port); + return std::make_pair(std::move(server), port); } -bool isDefaultServer(const String &host) { return host.length() == 0 || host == default_mqtt_address; } +bool isDefaultServer(const String &host) +{ + return host.length() == 0 || host == default_mqtt_address; +} -bool isDefaultRootTopic(const String &root) { return root.length() == 0 || root == default_mqtt_root; } +bool isDefaultRootTopic(const String &root) +{ + return root.length() == 0 || root == default_mqtt_root; +} struct PubSubConfig { - explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { - if (*config.address) { - serverAddr = config.address; - mqttUsername = config.username; - mqttPassword = config.password; + explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config) + { + if (*config.address) { + serverAddr = config.address; + mqttUsername = config.username; + mqttPassword = config.password; + } + if (config.tls_enabled) { + serverPort = 8883; + } + std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); } - if (config.tls_enabled) { - serverPort = 8883; - } - std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); - } - // Defaults - static constexpr uint16_t defaultPort = 1883; - static constexpr uint16_t defaultPortTls = 8883; + // Defaults + static constexpr uint16_t defaultPort = 1883; + static constexpr uint16_t defaultPortTls = 8883; - uint16_t serverPort = defaultPort; - String serverAddr = default_mqtt_address; - const char *mqttUsername = default_mqtt_username; - const char *mqttPassword = default_mqtt_password; + uint16_t serverPort = defaultPort; + String serverAddr = default_mqtt_address; + const char *mqttUsername = default_mqtt_username; + const char *mqttPassword = default_mqtt_password; }; #if HAS_NETWORKING -bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client) { - pubSub.setBufferSize(1024, 1024); - pubSub.setClient(client); - pubSub.setServer(config.serverAddr.c_str(), config.serverPort); +bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client) +{ + pubSub.setBufferSize(1024, 1024); + pubSub.setClient(client); + pubSub.setServer(config.serverAddr.c_str(), config.serverPort); - LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), config.serverPort, - config.mqttUsername, config.mqttPassword); + LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), + config.serverPort, config.mqttUsername, config.mqttPassword); - // Generate node ID from nodenum for client identification - std::string nodeId = nodeDB->getNodeId(); - const bool connected = pubSub.connect(nodeId.c_str(), config.mqttUsername, config.mqttPassword); - if (connected) { - isConnected = true; - LOG_INFO("MQTT connected"); - } else { - isConnected = false; - LOG_WARN("Failed to connect to MQTT server"); - } - return connected; + // Generate node ID from nodenum for client identification + std::string nodeId = nodeDB->getNodeId(); + const bool connected = pubSub.connect(nodeId.c_str(), config.mqttUsername, config.mqttPassword); + if (connected) { + isConnected = true; + LOG_INFO("MQTT connected"); + } else { + isConnected = false; + LOG_WARN("Failed to connect to MQTT server"); + } + return connected; } #endif -inline bool isConnectedToNetwork() { +inline bool isConnectedToNetwork() +{ #ifdef USE_WS5500 - if (ETH.connected()) - return true; + if (ETH.connected()) + return true; #endif #if HAS_WIFI - return WiFi.isConnected(); + return WiFi.isConnected(); #elif HAS_ETHERNET - return Ethernet.linkStatus() == LinkON; + return Ethernet.linkStatus() == LinkON; #else - return false; + return false; #endif } /** return true if we have a channel that wants uplink/downlink or map reporting is enabled */ -bool wantsLink() { - const bool hasChannelorMapReport = moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); - return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork()); +bool wantsLink() +{ + const bool hasChannelorMapReport = + moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); + return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork()); } } // namespace -void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) { mqtt->onReceive(topic, payload, length); } - -void MQTT::onClientProxyReceive(meshtastic_MqttClientProxyMessage msg) { - onReceive(msg.topic, msg.payload_variant.data.bytes, msg.payload_variant.data.size); +void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) +{ + mqtt->onReceive(topic, payload, length); } -void MQTT::onReceive(char *topic, byte *payload, size_t length) { - if (length == 0) { - LOG_WARN("Empty MQTT payload received, topic %s!", topic); - return; - } +void MQTT::onClientProxyReceive(meshtastic_MqttClientProxyMessage msg) +{ + onReceive(msg.topic, msg.payload_variant.data.bytes, msg.payload_variant.data.size); +} - // check if this is a json payload message by comparing the topic start - if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) { -#if !defined(ARCH_NRF52) || NRF52_USE_JSON - // parse the channel name from the topic string - // the topic has been checked above for having jsonTopic prefix, so just move past it - char *channelName = topic + jsonTopic.length(); - // if another "/" was added, parse string up to that character - channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; - // We allow downlink JSON packets only on a channel named "mqtt" - const meshtastic_Channel &sendChannel = channels.getByName(channelName); - if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && - sendChannel.settings.downlink_enabled)) { - LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); - return; +void MQTT::onReceive(char *topic, byte *payload, size_t length) +{ + if (length == 0) { + LOG_WARN("Empty MQTT payload received, topic %s!", topic); + return; } - onReceiveJson(payload, length); -#endif - return; - } - onReceiveProto(topic, payload, length); + // check if this is a json payload message by comparing the topic start + if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) { +#if !defined(ARCH_NRF52) || NRF52_USE_JSON + // parse the channel name from the topic string + // the topic has been checked above for having jsonTopic prefix, so just move past it + char *channelName = topic + jsonTopic.length(); + // if another "/" was added, parse string up to that character + channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; + // We allow downlink JSON packets only on a channel named "mqtt" + const meshtastic_Channel &sendChannel = channels.getByName(channelName); + if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && + sendChannel.settings.downlink_enabled)) { + LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); + return; + } + onReceiveJson(payload, length); +#endif + return; + } + + onReceiveProto(topic, payload, length); } -void mqttInit() { new MQTT(); } +void mqttInit() +{ + new MQTT(); +} #if HAS_NETWORKING MQTT::MQTT() : MQTT(std::unique_ptr(new MQTTClient())) {} MQTT::MQTT(std::unique_ptr _mqttClient) : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE), mqttClient(std::move(_mqttClient)), pubSub(*mqttClient) #else -MQTT::MQTT() - : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) +MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) #endif { - if (moduleConfig.mqtt.enabled) { - LOG_DEBUG("Init MQTT"); + if (moduleConfig.mqtt.enabled) { + LOG_DEBUG("Init MQTT"); - assert(!mqtt); - mqtt = this; + assert(!mqtt); + mqtt = this; - if (*moduleConfig.mqtt.root) { - cryptTopic = moduleConfig.mqtt.root + cryptTopic; - jsonTopic = moduleConfig.mqtt.root + jsonTopic; - mapTopic = moduleConfig.mqtt.root + mapTopic; - isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root); - } else { - cryptTopic = "msh" + cryptTopic; - jsonTopic = "msh" + jsonTopic; - mapTopic = "msh" + mapTopic; - isConfiguredForDefaultRootTopic = true; - } + if (*moduleConfig.mqtt.root) { + cryptTopic = moduleConfig.mqtt.root + cryptTopic; + jsonTopic = moduleConfig.mqtt.root + jsonTopic; + mapTopic = moduleConfig.mqtt.root + mapTopic; + isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root); + } else { + cryptTopic = "msh" + cryptTopic; + jsonTopic = "msh" + jsonTopic; + mapTopic = "msh" + mapTopic; + isConfiguredForDefaultRootTopic = true; + } - if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) { - map_position_precision = - Default::getConfiguredOrDefault(moduleConfig.mqtt.map_report_settings.position_precision, default_map_position_precision); - map_publish_interval_msecs = - Default::getConfiguredOrDefaultMs(moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); - } + if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) { + map_position_precision = Default::getConfiguredOrDefault(moduleConfig.mqtt.map_report_settings.position_precision, + default_map_position_precision); + map_publish_interval_msecs = Default::getConfiguredOrDefaultMs( + moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); + } - String host = parseHostAndPort(moduleConfig.mqtt.address).first; - isConfiguredForDefaultServer = isDefaultServer(host); - IPAddress ip; - isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); + String host = parseHostAndPort(moduleConfig.mqtt.address).first; + isConfiguredForDefaultServer = isDefaultServer(host); + IPAddress ip; + isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); #if HAS_NETWORKING - if (!moduleConfig.mqtt.proxy_to_client_enabled) - pubSub.setCallback(mqttCallback); + if (!moduleConfig.mqtt.proxy_to_client_enabled) + pubSub.setCallback(mqttCallback); #endif - if (moduleConfig.mqtt.proxy_to_client_enabled) { - LOG_INFO("MQTT configured to use client proxy"); - enabled = true; - runASAP = true; - reconnectCount = 0; - publishNodeInfo(); + if (moduleConfig.mqtt.proxy_to_client_enabled) { + LOG_INFO("MQTT configured to use client proxy"); + enabled = true; + runASAP = true; + reconnectCount = 0; + publishNodeInfo(); + } + // preflightSleepObserver.observe(&preflightSleep); + } else { + disable(); } - // preflightSleepObserver.observe(&preflightSleep); - } else { - disable(); - } } -bool MQTT::isConnectedDirectly() { +bool MQTT::isConnectedDirectly() +{ #if HAS_NETWORKING - return pubSub.connected(); + return pubSub.connected(); #else - return false; + return false; #endif } -bool MQTT::publish(const char *topic, const char *payload, bool retained) { - if (moduleConfig.mqtt.proxy_to_client_enabled) { - meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); - msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; - strcpy(msg->topic, topic); - strcpy(msg->payload_variant.text, payload); - msg->retained = retained; - service->sendMqttMessageToClientProxy(msg); - return true; - } -#if HAS_NETWORKING - else if (isConnectedDirectly()) { - return pubSub.publish(topic, payload, retained); - } -#endif - return false; -} - -bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, bool retained) { - if (moduleConfig.mqtt.proxy_to_client_enabled) { - meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); - msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - strlcpy(msg->topic, topic, sizeof(msg->topic)); - if (length > sizeof(msg->payload_variant.data.bytes)) - length = sizeof(msg->payload_variant.data.bytes); - msg->payload_variant.data.size = length; - memcpy(msg->payload_variant.data.bytes, payload, length); - msg->retained = retained; - service->sendMqttMessageToClientProxy(msg); - return true; - } -#if HAS_NETWORKING - else if (isConnectedDirectly()) { - return pubSub.publish(topic, payload, length, retained); - } -#endif - return false; -} - -void MQTT::reconnect() { - isConnected = false; - if (wantsLink()) { +bool MQTT::publish(const char *topic, const char *payload, bool retained) +{ if (moduleConfig.mqtt.proxy_to_client_enabled) { - LOG_INFO("MQTT connect via client proxy instead"); - enabled = true; - runASAP = true; - reconnectCount = 0; - - publishNodeInfo(); - return; // Don't try to connect directly to the server + meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); + msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; + strcpy(msg->topic, topic); + strcpy(msg->payload_variant.text, payload); + msg->retained = retained; + service->sendMqttMessageToClientProxy(msg); + return true; } #if HAS_NETWORKING - const PubSubConfig ps_config(moduleConfig.mqtt); - MQTTClient *clientConnection = mqttClient.get(); + else if (isConnectedDirectly()) { + return pubSub.publish(topic, payload, retained); + } +#endif + return false; +} + +bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, bool retained) +{ + if (moduleConfig.mqtt.proxy_to_client_enabled) { + meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); + msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; + strlcpy(msg->topic, topic, sizeof(msg->topic)); + if (length > sizeof(msg->payload_variant.data.bytes)) + length = sizeof(msg->payload_variant.data.bytes); + msg->payload_variant.data.size = length; + memcpy(msg->payload_variant.data.bytes, payload, length); + msg->retained = retained; + service->sendMqttMessageToClientProxy(msg); + return true; + } +#if HAS_NETWORKING + else if (isConnectedDirectly()) { + return pubSub.publish(topic, payload, length, retained); + } +#endif + return false; +} + +void MQTT::reconnect() +{ + isConnected = false; + if (wantsLink()) { + if (moduleConfig.mqtt.proxy_to_client_enabled) { + LOG_INFO("MQTT connect via client proxy instead"); + enabled = true; + runASAP = true; + reconnectCount = 0; + + publishNodeInfo(); + return; // Don't try to connect directly to the server + } +#if HAS_NETWORKING + const PubSubConfig ps_config(moduleConfig.mqtt); + MQTTClient *clientConnection = mqttClient.get(); #if MQTT_SUPPORTS_TLS - if (moduleConfig.mqtt.tls_enabled) { - mqttClientTLS.setInsecure(); - LOG_INFO("Use TLS-encrypted session"); - clientConnection = &mqttClientTLS; - } else { - LOG_INFO("Use non-TLS-encrypted session"); - } + if (moduleConfig.mqtt.tls_enabled) { + mqttClientTLS.setInsecure(); + LOG_INFO("Use TLS-encrypted session"); + clientConnection = &mqttClientTLS; + } else { + LOG_INFO("Use non-TLS-encrypted session"); + } #endif - if (connectPubSub(ps_config, pubSub, *clientConnection)) { - enabled = true; // Start running background process again - runASAP = true; - reconnectCount = 0; - isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP()); - isConnected = true; - publishNodeInfo(); - sendSubscriptions(); - } else { + if (connectPubSub(ps_config, pubSub, *clientConnection)) { + enabled = true; // Start running background process again + runASAP = true; + reconnectCount = 0; + isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP()); + isConnected = true; + publishNodeInfo(); + sendSubscriptions(); + } else { #if HAS_WIFI && !defined(ARCH_PORTDUINO) - reconnectCount++; - LOG_ERROR("Failed to contact MQTT server directly (%d/%d)", reconnectCount, reconnectMax); - if (reconnectCount >= reconnectMax) { - needReconnect = true; - wifiReconnect->setIntervalFromNow(0); - reconnectCount = 0; - } + reconnectCount++; + LOG_ERROR("Failed to contact MQTT server directly (%d/%d)", reconnectCount, reconnectMax); + if (reconnectCount >= reconnectMax) { + needReconnect = true; + wifiReconnect->setIntervalFromNow(0); + reconnectCount = 0; + } +#endif + } #endif } -#endif - } } -void MQTT::sendSubscriptions() { +void MQTT::sendSubscriptions() +{ #if HAS_NETWORKING - bool hasDownlink = false; - size_t numChan = channels.getNumChannels(); - for (size_t i = 0; i < numChan; i++) { - const auto &ch = channels.getByIndex(i); - if (ch.settings.downlink_enabled) { - hasDownlink = true; - std::string topic = cryptTopic + channels.getGlobalId(i) + "/+"; - LOG_INFO("Subscribe to %s", topic.c_str()); - pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right? -#if !defined(ARCH_NRF52) || defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ### - if (moduleConfig.mqtt.json_enabled == true) { - std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+"; - LOG_INFO("Subscribe to %s", topicDecoded.c_str()); - pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right? - } + bool hasDownlink = false; + size_t numChan = channels.getNumChannels(); + for (size_t i = 0; i < numChan; i++) { + const auto &ch = channels.getByIndex(i); + if (ch.settings.downlink_enabled) { + hasDownlink = true; + std::string topic = cryptTopic + channels.getGlobalId(i) + "/+"; + LOG_INFO("Subscribe to %s", topic.c_str()); + pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right? +#if !defined(ARCH_NRF52) || \ + defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ### + if (moduleConfig.mqtt.json_enabled == true) { + std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+"; + LOG_INFO("Subscribe to %s", topicDecoded.c_str()); + pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right? + } #endif // ARCH_NRF52 NRF52_USE_JSON + } } - } #if !MESHTASTIC_EXCLUDE_PKI - if (hasDownlink) { - std::string topic = cryptTopic + "PKI/+"; - LOG_INFO("Subscribe to %s", topic.c_str()); - pubSub.subscribe(topic.c_str(), 1); - } + if (hasDownlink) { + std::string topic = cryptTopic + "PKI/+"; + LOG_INFO("Subscribe to %s", topic.c_str()); + pubSub.subscribe(topic.c_str(), 1); + } #endif #endif } -int32_t MQTT::runOnce() { - if (!moduleConfig.mqtt.enabled || !(moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled())) - return disable(); - bool wantConnection = wantsLink(); +int32_t MQTT::runOnce() +{ + if (!moduleConfig.mqtt.enabled || !(moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled())) + return disable(); + bool wantConnection = wantsLink(); - perhapsReportToMap(); + perhapsReportToMap(); - // If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact - // server - if (moduleConfig.mqtt.proxy_to_client_enabled) { - publishQueuedMessages(); - return 200; - } -#if HAS_NETWORKING - else if (!pubSub.loop()) { - if (!wantConnection) - return 5000; // If we don't want connection now, check again in 5 secs - else { - reconnect(); - // If we succeeded, empty the queue one by one and start reading rapidly, else try again in 30 seconds (TCP - // connections are EXPENSIVE so try rarely) - if (isConnectedDirectly()) { + // If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact server + if (moduleConfig.mqtt.proxy_to_client_enabled) { publishQueuedMessages(); return 200; - } else - return 30000; } - } else { - // we are connected to server, check often for new requests on the TCP port - if (!wantConnection) { - LOG_INFO("MQTT link not needed, drop"); - pubSub.disconnect(); - } - - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) - return 20; - } -#else - // No networking available, return default interval - return 30000; -#endif -} - -bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client) { - const PubSubConfig parsed(config); - - if (config.enabled && !config.proxy_to_client_enabled) { #if HAS_NETWORKING - std::unique_ptr clientConnection; - if (config.tls_enabled) { -#if MQTT_SUPPORTS_TLS - MQTTClientTLS *tlsClient = new MQTTClientTLS; - clientConnection.reset(tlsClient); - tlsClient->setInsecure(); -#else - LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); - return false; -#endif + else if (!pubSub.loop()) { + if (!wantConnection) + return 5000; // If we don't want connection now, check again in 5 secs + else { + reconnect(); + // If we succeeded, empty the queue one by one and start reading rapidly, else try again in 30 seconds (TCP + // connections are EXPENSIVE so try rarely) + if (isConnectedDirectly()) { + publishQueuedMessages(); + return 200; + } else + return 30000; + } } else { - clientConnection.reset(new MQTTClient); - } - std::unique_ptr pubSub(new PubSubClient); - if (isConnectedToNetwork()) { - return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + // we are connected to server, check often for new requests on the TCP port + if (!wantConnection) { + LOG_INFO("MQTT link not needed, drop"); + pubSub.disconnect(); + } + + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) + return 20; } #else - const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; - LOG_ERROR(warning); + // No networking available, return default interval + return 30000; +#endif +} + +bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client) +{ + const PubSubConfig parsed(config); + + if (config.enabled && !config.proxy_to_client_enabled) { +#if HAS_NETWORKING + std::unique_ptr clientConnection; + if (config.tls_enabled) { +#if MQTT_SUPPORTS_TLS + MQTTClientTLS *tlsClient = new MQTTClientTLS; + clientConnection.reset(tlsClient); + tlsClient->setInsecure(); +#else + LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); + return false; +#endif + } else { + clientConnection.reset(new MQTTClient); + } + std::unique_ptr pubSub(new PubSubClient); + if (isConnectedToNetwork()) { + return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + } +#else + const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; + LOG_ERROR(warning); #if !IS_RUNNING_TESTS - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, warning, sizeof(cn->message) - 1); - cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination - service->sendClientNotification(cn); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); #endif - return false; + return false; #endif - } - - const bool defaultServer = isDefaultServer(parsed.serverAddr); - if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) { - const char *warning = "Invalid MQTT config: default server address must not have a port specified"; - LOG_ERROR(warning); -#if !IS_RUNNING_TESTS - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - cn->time = getValidTime(RTCQualityFromNet); - strncpy(cn->message, warning, sizeof(cn->message) - 1); - cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination - service->sendClientNotification(cn); -#endif - return false; - } - return true; -} - -void MQTT::publishNodeInfo() { - // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) -} -void MQTT::publishQueuedMessages() { - if (mqttQueue.isEmpty()) - return; - - if (!moduleConfig.mqtt.proxy_to_client_enabled && !isConnected) - return; - - LOG_DEBUG("Publish enqueued MQTT message"); - const std::unique_ptr entry(mqttQueue.dequeuePtr(0)); - LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size()); - publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false); - -#if !defined(ARCH_NRF52) || defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### - if (!moduleConfig.mqtt.json_enabled) - return; - - // handle json topic - const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size()); - if (!env.validDecode || env.packet == NULL || env.channel_id == NULL) - return; - - auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet); - if (jsonString.length() == 0) - return; - - // Generate node ID from nodenum for topic - std::string nodeId = nodeDB->getNodeId(); - - std::string topicJson; - if (env.packet->pki_encrypted) { - topicJson = jsonTopic + "PKI/" + nodeId; - } else { - topicJson = jsonTopic + env.channel_id + "/" + nodeId; - } - LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); - publish(topicJson.c_str(), jsonString.c_str(), false); -#endif // ARCH_NRF52 NRF52_USE_JSON -} - -void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex) { - if (mp_encrypted.via_mqtt) - return; // Don't send messages that came from MQTT back into MQTT - bool uplinkEnabled = false; - for (int i = 0; i <= 7; i++) { - if (channels.getByIndex(i).settings.uplink_enabled) - uplinkEnabled = true; - } - if (!uplinkEnabled) - return; // no channels have an uplink enabled - auto &ch = channels.getByIndex(chIndex); - - // mp_decoded will not be decoded when it's PKI encrypted and not directed to us - if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield - bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); - // check for the lowest bit of the data bitfield set false, and the use of one of the default keys. - if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink && - (ch.settings.psk.size < 2 || (ch.settings.psk.size == 16 && memcmp(ch.settings.psk.bytes, defaultpsk, 16)) || - (ch.settings.psk.size == 32 && memcmp(ch.settings.psk.bytes, eventpsk, 32)))) { - LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); - return; } - if (isConfiguredForDefaultServer && - (mp_decoded.decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP || mp_decoded.decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP)) { - LOG_DEBUG("MQTT onSend - Ignoring range test or detection sensor message on public mqtt"); - return; + const bool defaultServer = isDefaultServer(parsed.serverAddr); + if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) { + const char *warning = "Invalid MQTT config: default server address must not have a port specified"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif + return false; } - } - // Either encrypted packet (we couldn't decrypt) is marked as pki_encrypted, or we could decode the PKI encrypted - // packet - bool isPKIEncrypted = mp_encrypted.pki_encrypted || mp_decoded.pki_encrypted; - // If it was to a channel, check uplink enabled, else must be pki_encrypted - if (!(ch.settings.uplink_enabled || isPKIEncrypted)) - return; - const char *channelId = isPKIEncrypted ? "PKI" : channels.getGlobalId(chIndex); + return true; +} - LOG_DEBUG("MQTT onSend - Publish "); - const meshtastic_MeshPacket *p; - if (moduleConfig.mqtt.encryption_enabled) { - p = &mp_encrypted; - LOG_DEBUG("encrypted message"); - } else if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - p = &mp_decoded; - LOG_DEBUG("portnum %i message", mp_decoded.decoded.portnum); - } else { - LOG_DEBUG("nothing, pkt not decrypted"); - return; // Don't upload a still-encrypted PKI packet if not encryption_enabled - } +void MQTT::publishNodeInfo() +{ + // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) +} +void MQTT::publishQueuedMessages() +{ + if (mqttQueue.isEmpty()) + return; - // Generate node ID from nodenum for service envelope - std::string nodeId = nodeDB->getNodeId(); + if (!moduleConfig.mqtt.proxy_to_client_enabled && !isConnected) + return; - const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), - .channel_id = const_cast(channelId), - .gateway_id = const_cast(nodeId.c_str())}; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); - std::string topic = cryptTopic + channelId + "/" + nodeId; + LOG_DEBUG("Publish enqueued MQTT message"); + const std::unique_ptr entry(mqttQueue.dequeuePtr(0)); + LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size()); + publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false); - if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { - LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes); - publish(topic.c_str(), bytes, numBytes, false); - -#if !defined(ARCH_NRF52) || defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### +#if !defined(ARCH_NRF52) || \ + defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### if (!moduleConfig.mqtt.json_enabled) - return; + return; + // handle json topic - auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded); + const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size()); + if (!env.validDecode || env.packet == NULL || env.channel_id == NULL) + return; + + auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet); if (jsonString.length() == 0) - return; - // Generate node ID from nodenum for JSON topic - std::string nodeIdForJson = nodeDB->getNodeId(); - std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson; + return; + + // Generate node ID from nodenum for topic + std::string nodeId = nodeDB->getNodeId(); + + std::string topicJson; + if (env.packet->pki_encrypted) { + topicJson = jsonTopic + "PKI/" + nodeId; + } else { + topicJson = jsonTopic + env.channel_id + "/" + nodeId; + } LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); publish(topicJson.c_str(), jsonString.c_str(), false); #endif // ARCH_NRF52 NRF52_USE_JSON - } else { - LOG_INFO("MQTT not connected, queue packet"); - QueueEntry *entry; - if (mqttQueue.numFree() == 0) { - LOG_WARN("MQTT queue is full, discard oldest"); - entry = mqttQueue.dequeuePtr(0); +} + +void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex) +{ + if (mp_encrypted.via_mqtt) + return; // Don't send messages that came from MQTT back into MQTT + bool uplinkEnabled = false; + for (int i = 0; i <= 7; i++) { + if (channels.getByIndex(i).settings.uplink_enabled) + uplinkEnabled = true; + } + if (!uplinkEnabled) + return; // no channels have an uplink enabled + auto &ch = channels.getByIndex(chIndex); + + // mp_decoded will not be decoded when it's PKI encrypted and not directed to us + if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield + bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); + // check for the lowest bit of the data bitfield set false, and the use of one of the default keys. + if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink && + (ch.settings.psk.size < 2 || (ch.settings.psk.size == 16 && memcmp(ch.settings.psk.bytes, defaultpsk, 16)) || + (ch.settings.psk.size == 32 && memcmp(ch.settings.psk.bytes, eventpsk, 32)))) { + LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); + return; + } + + if (isConfiguredForDefaultServer && (mp_decoded.decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP || + mp_decoded.decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP)) { + LOG_DEBUG("MQTT onSend - Ignoring range test or detection sensor message on public mqtt"); + return; + } + } + // Either encrypted packet (we couldn't decrypt) is marked as pki_encrypted, or we could decode the PKI encrypted packet + bool isPKIEncrypted = mp_encrypted.pki_encrypted || mp_decoded.pki_encrypted; + // If it was to a channel, check uplink enabled, else must be pki_encrypted + if (!(ch.settings.uplink_enabled || isPKIEncrypted)) + return; + const char *channelId = isPKIEncrypted ? "PKI" : channels.getGlobalId(chIndex); + + LOG_DEBUG("MQTT onSend - Publish "); + const meshtastic_MeshPacket *p; + if (moduleConfig.mqtt.encryption_enabled) { + p = &mp_encrypted; + LOG_DEBUG("encrypted message"); + } else if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + p = &mp_decoded; + LOG_DEBUG("portnum %i message", mp_decoded.decoded.portnum); } else { - entry = new QueueEntry; + LOG_DEBUG("nothing, pkt not decrypted"); + return; // Don't upload a still-encrypted PKI packet if not encryption_enabled } - entry->topic = std::move(topic); - entry->envBytes.assign(bytes, numBytes); - if (mqttQueue.enqueue(entry, 0) == false) { - LOG_CRIT("Failed to add a message to mqttQueue!"); - abort(); + + // Generate node ID from nodenum for service envelope + std::string nodeId = nodeDB->getNodeId(); + + const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), + .channel_id = const_cast(channelId), + .gateway_id = const_cast(nodeId.c_str())}; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); + std::string topic = cryptTopic + channelId + "/" + nodeId; + + if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { + LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes); + publish(topic.c_str(), bytes, numBytes, false); + +#if !defined(ARCH_NRF52) || \ + defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### + if (!moduleConfig.mqtt.json_enabled) + return; + // handle json topic + auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded); + if (jsonString.length() == 0) + return; + // Generate node ID from nodenum for JSON topic + std::string nodeIdForJson = nodeDB->getNodeId(); + std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson; + LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); + publish(topicJson.c_str(), jsonString.c_str(), false); +#endif // ARCH_NRF52 NRF52_USE_JSON + } else { + LOG_INFO("MQTT not connected, queue packet"); + QueueEntry *entry; + if (mqttQueue.numFree() == 0) { + LOG_WARN("MQTT queue is full, discard oldest"); + entry = mqttQueue.dequeuePtr(0); + } else { + entry = new QueueEntry; + } + entry->topic = std::move(topic); + entry->envBytes.assign(bytes, numBytes); + if (mqttQueue.enqueue(entry, 0) == false) { + LOG_CRIT("Failed to add a message to mqttQueue!"); + abort(); + } } - } } -void MQTT::perhapsReportToMap() { - if (!moduleConfig.mqtt.map_reporting_enabled || !moduleConfig.mqtt.map_report_settings.should_report_location || - !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly())) - return; +void MQTT::perhapsReportToMap() +{ + if (!moduleConfig.mqtt.map_reporting_enabled || !moduleConfig.mqtt.map_report_settings.should_report_location || + !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly())) + return; - // Coerce the map position precision to be within the valid range - // This removes obtusely large radius and privacy problematic ones from the map - if (map_position_precision < 12 || map_position_precision > 15) { - LOG_WARN("MQTT Map report position precision %u is out of range, using default %u", map_position_precision, default_map_position_precision); - map_position_precision = default_map_position_precision; - } + // Coerce the map position precision to be within the valid range + // This removes obtusely large radius and privacy problematic ones from the map + if (map_position_precision < 12 || map_position_precision > 15) { + LOG_WARN("MQTT Map report position precision %u is out of range, using default %u", map_position_precision, + default_map_position_precision); + map_position_precision = default_map_position_precision; + } - if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) - return; + if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) + return; - if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { + last_report_to_map = millis(); + LOG_WARN("MQTT Map report enabled, but no position available"); + return; + } + + // Allocate MeshPacket and fill it + meshtastic_MeshPacket *mp = packetPool.allocZeroed(); + mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag; + mp->from = nodeDB->getNodeNum(); + mp->to = NODENUM_BROADCAST; + mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP; + + // Fill MapReport message + meshtastic_MapReport mapReport = meshtastic_MapReport_init_default; + memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name)); + memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name)); + mapReport.role = config.device.role; + mapReport.hw_model = owner.hw_model; + strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version)); + mapReport.region = config.lora.region; + mapReport.modem_preset = config.lora.modem_preset; + mapReport.has_default_channel = channels.hasDefaultChannel(); + mapReport.has_opted_report_location = true; + + // Set position with precision (same as in PositionModule) + mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision)); + mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision)); + mapReport.latitude_i += (1 << (31 - map_position_precision)); + mapReport.longitude_i += (1 << (31 - map_position_precision)); + + mapReport.altitude = localPosition.altitude; + mapReport.position_precision = map_position_precision; + + mapReport.num_online_local_nodes = nodeDB->getNumOnlineMeshNodes(true); + + // Encode MapReport message into the MeshPacket + mp->decoded.payload.size = + pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_MapReport_msg, &mapReport); + + // Generate node ID from nodenum for service envelope + std::string nodeId = nodeDB->getNodeId(); + + // Encode the MeshPacket into a binary ServiceEnvelope and publish + const meshtastic_ServiceEnvelope se = { + .packet = mp, + .channel_id = (char *)channels.getGlobalId(channels.getPrimaryIndex()), // Use primary channel as the channel_id + .gateway_id = const_cast(nodeId.c_str())}; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &se); + + LOG_INFO("MQTT Publish map report to %s", mapTopic.c_str()); + publish(mapTopic.c_str(), bytes, numBytes, false); + + // Release the allocated memory for MeshPacket + packetPool.release(mp); + + // Update the last report time last_report_to_map = millis(); - LOG_WARN("MQTT Map report enabled, but no position available"); - return; - } - - // Allocate MeshPacket and fill it - meshtastic_MeshPacket *mp = packetPool.allocZeroed(); - mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag; - mp->from = nodeDB->getNodeNum(); - mp->to = NODENUM_BROADCAST; - mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP; - - // Fill MapReport message - meshtastic_MapReport mapReport = meshtastic_MapReport_init_default; - memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name)); - memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name)); - mapReport.role = config.device.role; - mapReport.hw_model = owner.hw_model; - strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version)); - mapReport.region = config.lora.region; - mapReport.modem_preset = config.lora.modem_preset; - mapReport.has_default_channel = channels.hasDefaultChannel(); - mapReport.has_opted_report_location = true; - - // Set position with precision (same as in PositionModule) - mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision)); - mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision)); - mapReport.latitude_i += (1 << (31 - map_position_precision)); - mapReport.longitude_i += (1 << (31 - map_position_precision)); - - mapReport.altitude = localPosition.altitude; - mapReport.position_precision = map_position_precision; - - mapReport.num_online_local_nodes = nodeDB->getNumOnlineMeshNodes(true); - - // Encode MapReport message into the MeshPacket - mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_MapReport_msg, &mapReport); - - // Generate node ID from nodenum for service envelope - std::string nodeId = nodeDB->getNodeId(); - - // Encode the MeshPacket into a binary ServiceEnvelope and publish - const meshtastic_ServiceEnvelope se = {.packet = mp, - .channel_id = - (char *)channels.getGlobalId(channels.getPrimaryIndex()), // Use primary channel as the channel_id - .gateway_id = const_cast(nodeId.c_str())}; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &se); - - LOG_INFO("MQTT Publish map report to %s", mapTopic.c_str()); - publish(mapTopic.c_str(), bytes, numBytes, false); - - // Release the allocated memory for MeshPacket - packetPool.release(mp); - - // Update the last report time - last_report_to_map = millis(); } diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index 6c42ad299..7d5715602 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -27,114 +27,115 @@ #define MAX_MQTT_QUEUE 16 /** - * Our wrapper/singleton for sending/receiving MQTT "udp" packets. This object isolates the MQTT protocol - * implementation from the two components that use it: MQTTPlugin and MQTTSimInterface. + * Our wrapper/singleton for sending/receiving MQTT "udp" packets. This object isolates the MQTT protocol implementation from + * the two components that use it: MQTTPlugin and MQTTSimInterface. */ -class MQTT : private concurrency::OSThread { -public: - MQTT(); +class MQTT : private concurrency::OSThread +{ + public: + MQTT(); - /** - * Publish a packet on the global MQTT server. - * @param mp_encrypted the encrypted packet to publish - * @param mp_decoded the decrypted packet to publish - * @param chIndex the index of the channel for this message - * - * Note: for messages we are forwarding on the mesh that we can't find the channel for (because we don't have the - * keys), we can not forward those messages to the cloud - because no way to find a global channel ID. - */ - void onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex); + /** + * Publish a packet on the global MQTT server. + * @param mp_encrypted the encrypted packet to publish + * @param mp_decoded the decrypted packet to publish + * @param chIndex the index of the channel for this message + * + * Note: for messages we are forwarding on the mesh that we can't find the channel for (because we don't have the keys), we + * can not forward those messages to the cloud - because no way to find a global channel ID. + */ + void onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex); - bool isConnectedDirectly(); + bool isConnectedDirectly(); - bool publish(const char *topic, const char *payload, bool retained); + bool publish(const char *topic, const char *payload, bool retained); - bool publish(const char *topic, const uint8_t *payload, size_t length, const bool retained); + bool publish(const char *topic, const uint8_t *payload, size_t length, const bool retained); - void onClientProxyReceive(meshtastic_MqttClientProxyMessage msg); + void onClientProxyReceive(meshtastic_MqttClientProxyMessage msg); - bool isEnabled() { return this->enabled; }; + bool isEnabled() { return this->enabled; }; - void start() { setIntervalFromNow(0); }; + void start() { setIntervalFromNow(0); }; - bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } - bool isUsingDefaultRootTopic() { return isConfiguredForDefaultRootTopic; } + bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } + bool isUsingDefaultRootTopic() { return isConfiguredForDefaultRootTopic; } - /// Validate the meshtastic_ModuleConfig_MQTTConfig. - static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); } + /// Validate the meshtastic_ModuleConfig_MQTTConfig. + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); } -protected: - struct QueueEntry { - std::string topic; - std::basic_string envBytes; // binary/pb_encode_to_bytes ServiceEnvelope - }; - PointerQueue mqttQueue; + protected: + struct QueueEntry { + std::string topic; + std::basic_string envBytes; // binary/pb_encode_to_bytes ServiceEnvelope + }; + PointerQueue mqttQueue; - int reconnectCount = 0; - bool isConfiguredForDefaultServer = true; - bool isConfiguredForDefaultRootTopic = true; + int reconnectCount = 0; + bool isConfiguredForDefaultServer = true; + bool isConfiguredForDefaultRootTopic = true; - virtual int32_t runOnce() override; + virtual int32_t runOnce() override; #ifndef PIO_UNIT_TESTING -private: + private: #endif #if HAS_WIFI - using MQTTClient = WiFiClient; + using MQTTClient = WiFiClient; #if __has_include() - using MQTTClientTLS = WiFiClientSecure; + using MQTTClientTLS = WiFiClientSecure; #define MQTT_SUPPORTS_TLS 1 #endif #elif HAS_ETHERNET - using MQTTClient = EthernetClient; + using MQTTClient = EthernetClient; #else - using MQTTClient = void; + using MQTTClient = void; #endif #if HAS_NETWORKING - std::unique_ptr mqttClient; + std::unique_ptr mqttClient; #if MQTT_SUPPORTS_TLS - MQTTClientTLS mqttClientTLS; + MQTTClientTLS mqttClientTLS; #endif - PubSubClient pubSub; - explicit MQTT(std::unique_ptr mqttClient); + PubSubClient pubSub; + explicit MQTT(std::unique_ptr mqttClient); #endif - std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID - std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID - std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages + std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID + std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID + std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages - // For map reporting (only applies when enabled) - const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m - uint32_t last_report_to_map = 0; - uint32_t map_position_precision = default_map_position_precision; - uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000; + // For map reporting (only applies when enabled) + const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m + uint32_t last_report_to_map = 0; + uint32_t map_position_precision = default_map_position_precision; + uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000; - /** Attempt to connect to server if necessary - */ - void reconnect(); + /** Attempt to connect to server if necessary + */ + void reconnect(); - /** Tell the server what subscriptions we want (based on channels.downlink_enabled) - */ - void sendSubscriptions(); + /** Tell the server what subscriptions we want (based on channels.downlink_enabled) + */ + void sendSubscriptions(); - /// Callback for direct mqtt subscription messages - static void mqttCallback(char *topic, byte *payload, unsigned int length); + /// Callback for direct mqtt subscription messages + static void mqttCallback(char *topic, byte *payload, unsigned int length); - static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client); + static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client); - /// Called when a new publish arrives from the MQTT server - void onReceive(char *topic, byte *payload, size_t length); + /// Called when a new publish arrives from the MQTT server + void onReceive(char *topic, byte *payload, size_t length); - void publishQueuedMessages(); + void publishQueuedMessages(); - void publishNodeInfo(); + void publishNodeInfo(); - // Check if we should report unencrypted information about our node for consumption by a map - void perhapsReportToMap(); + // Check if we should report unencrypted information about our node for consumption by a map + void perhapsReportToMap(); - /// Return 0 if sleep is okay, veto sleep if we are connected to pubsub server - // int preflightSleepCb(void *unused = NULL) { return pubSub.connected() ? 1 : 0; } + /// Return 0 if sleep is okay, veto sleep if we are connected to pubsub server + // int preflightSleepCb(void *unused = NULL) { return pubSub.connected() ? 1 : 0; } }; void mqttInit(); diff --git a/src/mqtt/ServiceEnvelope.cpp b/src/mqtt/ServiceEnvelope.cpp index 1bfccc0ad..ee55f22f6 100644 --- a/src/mqtt/ServiceEnvelope.cpp +++ b/src/mqtt/ServiceEnvelope.cpp @@ -4,16 +4,20 @@ DecodedServiceEnvelope::DecodedServiceEnvelope(const uint8_t *payload, size_t length) : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_default), - validDecode(pb_decode_from_bytes(payload, length, &meshtastic_ServiceEnvelope_msg, this)) {} - -DecodedServiceEnvelope::DecodedServiceEnvelope(DecodedServiceEnvelope &&other) - : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_zero), validDecode(other.validDecode) { - std::swap(packet, other.packet); - std::swap(channel_id, other.channel_id); - std::swap(gateway_id, other.gateway_id); + validDecode(pb_decode_from_bytes(payload, length, &meshtastic_ServiceEnvelope_msg, this)) +{ } -DecodedServiceEnvelope::~DecodedServiceEnvelope() { - if (validDecode) - pb_release(&meshtastic_ServiceEnvelope_msg, this); +DecodedServiceEnvelope::DecodedServiceEnvelope(DecodedServiceEnvelope &&other) + : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_zero), validDecode(other.validDecode) +{ + std::swap(packet, other.packet); + std::swap(channel_id, other.channel_id); + std::swap(gateway_id, other.gateway_id); +} + +DecodedServiceEnvelope::~DecodedServiceEnvelope() +{ + if (validDecode) + pb_release(&meshtastic_ServiceEnvelope_msg, this); } \ No newline at end of file diff --git a/src/mqtt/ServiceEnvelope.h b/src/mqtt/ServiceEnvelope.h index d403de254..6ab0695db 100644 --- a/src/mqtt/ServiceEnvelope.h +++ b/src/mqtt/ServiceEnvelope.h @@ -4,10 +4,10 @@ // meshtastic_ServiceEnvelope that automatically releases dynamically allocated memory when it goes out of scope. struct DecodedServiceEnvelope : public meshtastic_ServiceEnvelope { - DecodedServiceEnvelope(const uint8_t *payload, size_t length); - DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete; - DecodedServiceEnvelope(DecodedServiceEnvelope &&); - ~DecodedServiceEnvelope(); - // Clients must check that this is true before using. - const bool validDecode; + DecodedServiceEnvelope(const uint8_t *payload, size_t length); + DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete; + DecodedServiceEnvelope(DecodedServiceEnvelope &&); + ~DecodedServiceEnvelope(); + // Clients must check that this is true before using. + const bool validDecode; }; \ No newline at end of file diff --git a/src/network-stubs.cpp b/src/network-stubs.cpp index 7073f2a5c..1737579d5 100644 --- a/src/network-stubs.cpp +++ b/src/network-stubs.cpp @@ -2,18 +2,30 @@ #if (HAS_WIFI == 0) -bool initWifi() { return false; } +bool initWifi() +{ + return false; +} void deinitWifi() {} -bool isWifiAvailable() { return false; } +bool isWifiAvailable() +{ + return false; +} #endif #if (HAS_ETHERNET == 0) -bool initEthernet() { return false; } +bool initEthernet() +{ + return false; +} -bool isEthernetAvailable() { return false; } +bool isEthernetAvailable() +{ + return false; +} #endif diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 743c57396..3b98eca3d 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -28,7 +28,8 @@ #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) -namespace { +namespace +{ constexpr uint16_t kPreferredBleMtu = 517; constexpr uint16_t kPreferredBleTxOctets = 251; constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8; @@ -51,331 +52,339 @@ NimBLEServer *bleServer; static bool passkeyShowing; static std::atomic nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection" -class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { - /* - CAUTION: There's a lot going on here and lots of room to break things. - - This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the - onRead and onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI). - - The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't - have to know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking - OSThread system, where locking isn't something that anyone has to worry about too much! :) - - We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and - handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry - about being run concurrently, which would make everything else much much much more complicated. - - PHONE -> RADIO: - - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue. - - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, - and calls handleToRadio **in main task**. - - RADIO -> PHONE: - - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. - (unless there's already a packet waiting in toPhoneQueue) - - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main - task** to get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the - onReadCallbackIsWaitingForData flag. - - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds - toPhoneMutex, pops the packet from toPhoneQueue, and returns it to NimBLE. - - MUTEXES: - - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize - - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize - - ATOMICS: - - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or - onDisconnect). - - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect). - - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue - (or onDisconnect). - - PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from - getFromRadio. - - BLE CONNECTION PARAMS: - - During config, we request a high-throughput, low-latency BLE connection for speed. - - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life. - - MEMORY MANAGEMENT: - - We keep packets on the stack and do not allocate heap. - - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks. - - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory - management. - - NOTIFY IS BROKEN: - - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards - compatible. - - ZERO-SIZE READS: - - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead - until we have data. - - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do - reads until they get a 0-byte response. - - CROSS-TASK WAKEUP: - - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data, - - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping. - - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause. - */ - -public: - BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { api_type = TYPE_BLE; } - - /* Packets from phone (BLE onWrite callback) */ - std::mutex fromPhoneMutex; - std::atomic fromPhoneQueueSize{0}; - // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. - std::array fromPhoneQueue{}; - - /* Packets to phone (BLE onRead callback) */ - std::mutex toPhoneMutex; - std::atomic toPhoneQueueSize{0}; - // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. - std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{}; - std::array toPhoneQueueByteSizes{}; - // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our - // main task's runOnce. It's only set by onRead, and only cleared by runOnce. - std::atomic onReadCallbackIsWaitingForData{false}; - - /* Statistics/logging helpers */ - std::atomic readCount{0}; - std::atomic notifyCount{0}; - std::atomic writeCount{0}; - -protected: - virtual int32_t runOnce() override { - while (runOnceHasWorkToDo()) { - /* - PROCESS fromPhoneQueue BEFORE toPhoneQueue: - - In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the - same time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally - ok to service either the reads or writes first. - - However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and - they expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS - back to another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config - only.) - - So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous" - write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this - is what the client wants! - */ - - // PHONE -> RADIO: - runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio - - // RADIO -> PHONE: - runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead - } - - // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback - return INT32_MAX; - } - - virtual void onConfigStart() override { - LOG_INFO("BLE onConfigStart"); - - // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) - if (bleServer && isConnected()) { - uint16_t conn_handle = nimbleBluetoothConnHandle.load(); - if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { - requestHighThroughputConnection(conn_handle); - } - } - } - - virtual void onConfigComplete() override { - LOG_INFO("BLE onConfigComplete"); - - // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete - if (bleServer && isConnected()) { - uint16_t conn_handle = nimbleBluetoothConnHandle.load(); - if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { - requestLowerPowerConnection(conn_handle); - } - } - } - - bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); } - - bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); } - - bool runOnceToPhoneCanPreloadNextPacket() { +class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread +{ /* - * PRELOADING getFromRadio RESPONSES: - * - * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we - * call getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that - * packet forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time - * window where the client might disconnect before completing the read. - * - * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload - * packets into toPhoneQueue because the client will just reconnect after a disconnect, losing nothing. - */ + CAUTION: There's a lot going on here and lots of room to break things. - if (!isConnected()) { - return false; - } else if (isSendingPackets()) { - // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio. - return false; - } else { - // In other states, we can preload as long as there's space in the toPhoneQueue. - return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE; + This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and + onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI). + + The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to + know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where + locking isn't something that anyone has to worry about too much! :) + + We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and + handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about + being run concurrently, which would make everything else much much much more complicated. + + PHONE -> RADIO: + - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue. + - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls + handleToRadio **in main task**. + + RADIO -> PHONE: + - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless + there's already a packet waiting in toPhoneQueue) + - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to + get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the + onReadCallbackIsWaitingForData flag. + - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex, + pops the packet from toPhoneQueue, and returns it to NimBLE. + + MUTEXES: + - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize + - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize + + ATOMICS: + - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect). + - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect). + - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or + onDisconnect). + + PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio. + + BLE CONNECTION PARAMS: + - During config, we request a high-throughput, low-latency BLE connection for speed. + - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life. + + MEMORY MANAGEMENT: + - We keep packets on the stack and do not allocate heap. + - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks. + - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management. + + NOTIFY IS BROKEN: + - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible. + + ZERO-SIZE READS: + - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we + have data. + - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads + until they get a 0-byte response. + + CROSS-TASK WAKEUP: + - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data, + - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping. + - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause. + */ + + public: + BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { api_type = TYPE_BLE; } + + /* Packets from phone (BLE onWrite callback) */ + std::mutex fromPhoneMutex; + std::atomic fromPhoneQueueSize{0}; + // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. + std::array fromPhoneQueue{}; + + /* Packets to phone (BLE onRead callback) */ + std::mutex toPhoneMutex; + std::atomic toPhoneQueueSize{0}; + // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. + std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{}; + std::array toPhoneQueueByteSizes{}; + // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main + // task's runOnce. It's only set by onRead, and only cleared by runOnce. + std::atomic onReadCallbackIsWaitingForData{false}; + + /* Statistics/logging helpers */ + std::atomic readCount{0}; + std::atomic notifyCount{0}; + std::atomic writeCount{0}; + + protected: + virtual int32_t runOnce() override + { + while (runOnceHasWorkToDo()) { + /* + PROCESS fromPhoneQueue BEFORE toPhoneQueue: + + In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same + time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to + service either the reads or writes first. + + However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they + expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to + another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.) + + So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous" + write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is + what the client wants! + */ + + // PHONE -> RADIO: + runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio + + // RADIO -> PHONE: + runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead + } + + // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback + return INT32_MAX; } - } - void runOnceHandleToPhoneQueue() { - // Stack buffer for getFromRadio packet - uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; - size_t numBytes = 0; + virtual void onConfigStart() override + { + LOG_INFO("BLE onConfigStart"); - if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) { - numBytes = getFromRadio(fromRadioBytes); + // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) + if (bleServer && isConnected()) { + uint16_t conn_handle = nimbleBluetoothConnHandle.load(); + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + requestHighThroughputConnection(conn_handle); + } + } + } - if (numBytes == 0) { + virtual void onConfigComplete() override + { + LOG_INFO("BLE onConfigComplete"); + + // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete + if (bleServer && isConnected()) { + uint16_t conn_handle = nimbleBluetoothConnHandle.load(); + if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { + requestLowerPowerConnection(conn_handle); + } + } + } + + bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); } + + bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); } + + bool runOnceToPhoneCanPreloadNextPacket() + { /* - Client expected a read, but we have nothing to send. + * PRELOADING getFromRadio RESPONSES: + * + * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call + * getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet + * forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where + * the client might disconnect before completing the read. + * + * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into + * toPhoneQueue because the client will just reconnect after a disconnect, losing nothing. + */ - In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond - notifies regularly, to make sure they have nothing else to read. - - In other states, this is fine **so long as we've already processed pending onWrites first**, because the - client may requesting wantConfig and immediately doing a read. - */ - } else { - // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. - if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { - // Note: the comparison above is safe without a mutex because we are the only method that *increases* - // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.) - - { // scope for toPhoneMutex mutex - std::lock_guard guard(toPhoneMutex); - size_t storeAtIndex = toPhoneQueueSize.load(); - memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes); - toPhoneQueueByteSizes[storeAtIndex] = numBytes; - toPhoneQueueSize++; - } -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes, toPhoneQueueSize.load()); -#endif + if (!isConnected()) { + return false; + } else if (isSendingPackets()) { + // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio. + return false; } else { - // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full! - LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes); + // In other states, we can preload as long as there's space in the toPhoneQueue. + return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE; } - } - - // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. - onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push } - } - bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } + void runOnceHandleToPhoneQueue() + { + // Stack buffer for getFromRadio packet + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; + size_t numBytes = 0; - void runOnceHandleFromPhoneQueue() { - // Handle packets we received from onWrite from the phone. - if (fromPhoneQueueSize > 0) { - // Note: the comparison above is safe without a mutex because we are the only method that *decreases* - // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.) + if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) { + numBytes = getFromRadio(fromRadioBytes); - LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load()); + if (numBytes == 0) { + /* + Client expected a read, but we have nothing to send. - // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop. - NimBLEAttValue val; - { // scope for fromPhoneMutex mutex - std::lock_guard guard(fromPhoneMutex); - val = fromPhoneQueue[0]; + In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond + notifies regularly, to make sure they have nothing else to read. - // Shift the rest of the queue down - for (uint8_t i = 1; i < fromPhoneQueueSize; i++) { - fromPhoneQueue[i - 1] = fromPhoneQueue[i]; + In other states, this is fine **so long as we've already processed pending onWrites first**, because the client + may requesting wantConfig and immediately doing a read. + */ + } else { + // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. + if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { + // Note: the comparison above is safe without a mutex because we are the only method that *increases* + // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.) + + { // scope for toPhoneMutex mutex + std::lock_guard guard(toPhoneMutex); + size_t storeAtIndex = toPhoneQueueSize.load(); + memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes); + toPhoneQueueByteSizes[storeAtIndex] = numBytes; + toPhoneQueueSize++; + } +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes, + toPhoneQueueSize.load()); +#endif + } else { + // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full! + LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes); + } + } + + // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. + onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push } - - // Safe decrement due to onDisconnect - if (fromPhoneQueueSize > 0) - fromPhoneQueueSize--; - } - - handleToRadio(val.data(), val.length()); } - } - /** - * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) - */ - virtual void onNowHasData(uint32_t fromRadioNum) { - PhoneAPI::onNowHasData(fromRadioNum); + bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } - int currentNotifyCount = notifyCount.fetch_add(1); + void runOnceHandleFromPhoneQueue() + { + // Handle packets we received from onWrite from the phone. + if (fromPhoneQueueSize > 0) { + // Note: the comparison above is safe without a mutex because we are the only method that *decreases* + // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.) - uint8_t cc = bleServer->getConnectedCount(); + LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load()); + + // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop. + NimBLEAttValue val; + { // scope for fromPhoneMutex mutex + std::lock_guard guard(fromPhoneMutex); + val = fromPhoneQueue[0]; + + // Shift the rest of the queue down + for (uint8_t i = 1; i < fromPhoneQueueSize; i++) { + fromPhoneQueue[i - 1] = fromPhoneQueue[i]; + } + + // Safe decrement due to onDisconnect + if (fromPhoneQueueSize > 0) + fromPhoneQueueSize--; + } + + handleToRadio(val.data(), val.length()); + } + } + + /** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + virtual void onNowHasData(uint32_t fromRadioNum) + { + PhoneAPI::onNowHasData(fromRadioNum); + + int currentNotifyCount = notifyCount.fetch_add(1); + + uint8_t cc = bleServer->getConnectedCount(); #ifdef DEBUG_NIMBLE_NOTIFY - // This logging slows things down when there are lots of packets going to the phone, like initial connection: - LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); + // This logging slows things down when there are lots of packets going to the phone, like initial connection: + LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); #endif - uint8_t val[4]; - put_le32(val, fromRadioNum); + uint8_t val[4]; + put_le32(val, fromRadioNum); - fromNumCharacteristic->setValue(val, sizeof(val)); + fromNumCharacteristic->setValue(val, sizeof(val)); #ifdef NIMBLE_TWO - // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be - // notify(). - fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); + // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be + // notify(). + fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); #else - fromNumCharacteristic->notify(); + fromNumCharacteristic->notify(); #endif - } + } - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } - void requestHighThroughputConnection(uint16_t conn_handle) { - /* Request a lower-latency, higher-throughput BLE connection. + void requestHighThroughputConnection(uint16_t conn_handle) + { + /* Request a lower-latency, higher-throughput BLE connection. - This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then - switch to a slower mode. + This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to + a slower mode. - See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS - constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the - Apple recommendations.) + See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS + constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple + recommendations.) - Selected settings: - minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the - client supports it.) maxInterval (units of 1.25ms): 15ms = 12 latency: 0 (don't allow peripheral to skip any - connection events) timeout (units of 10ms): 6 seconds = 600 (supervision timeout) + Selected settings: + minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client + supports it.) + maxInterval (units of 1.25ms): 15ms = 12 + latency: 0 (don't allow peripheral to skip any connection events) + timeout (units of 10ms): 6 seconds = 600 (supervision timeout) - These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds - at setup. Not worth adjusting much. - */ - LOG_INFO("BLE requestHighThroughputConnection"); - bleServer->updateConnParams(conn_handle, 6, 12, 0, 600); - } + These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at + setup. Not worth adjusting much. + */ + LOG_INFO("BLE requestHighThroughputConnection"); + bleServer->updateConnParams(conn_handle, 6, 12, 0, 600); + } - void requestLowerPowerConnection(uint16_t conn_handle) { - /* Request a lower power consumption (but higher latency, lower throughput) BLE connection. + void requestLowerPowerConnection(uint16_t conn_handle) + { + /* Request a lower power consumption (but higher latency, lower throughput) BLE connection. - This is suitable for steady-state operation after initial setup is complete. + This is suitable for steady-state operation after initial setup is complete. - See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS - constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the - Apple recommendations.) + See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS + constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple + recommendations.) - Selected settings: - minInterval (units of 1.25ms): 30ms = 24 - maxInterval (units of 1.25ms): 50ms = 40 - latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power) - timeout (units of 10ms): 6 seconds = 600 (supervision timeout) + Selected settings: + minInterval (units of 1.25ms): 30ms = 24 + maxInterval (units of 1.25ms): 50ms = 40 + latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power) + timeout (units of 10ms): 6 seconds = 600 (supervision timeout) - There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 - packets per second. - */ - LOG_INFO("BLE requestLowerPowerConnection"); - bleServer->updateConnParams(conn_handle, 24, 40, 2, 600); - } + There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets + per second. + */ + LOG_INFO("BLE requestLowerPowerConnection"); + bleServer->updateConnParams(conn_handle, 24, 40, 2, 600); + } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; @@ -386,569 +395,597 @@ static BluetoothPhoneAPI *bluetoothPhoneAPI; // Last ToRadio value received from the phone static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; -class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { +class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks +{ #ifdef NIMBLE_TWO - virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) + virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) #else - virtual void onWrite(NimBLECharacteristic *pCharacteristic) + virtual void onWrite(NimBLECharacteristic *pCharacteristic) #endif - { - // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's - // runOnce. Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent - // onWrite calls. + { + // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. + // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls. - int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1); + int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1); #ifdef DEBUG_NIMBLE_ON_WRITE_TIMING - int startMillis = millis(); - LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis); + int startMillis = millis(); + LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis); #endif - auto val = pCharacteristic->getValue(); + auto val = pCharacteristic->getValue(); - if (memcmp(lastToRadio, val.data(), val.length()) != 0) { - if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) { - // Note: the comparison above is safe without a mutex because we are the only method that *increases* - // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.) - memcpy(lastToRadio, val.data(), val.length()); + if (memcmp(lastToRadio, val.data(), val.length()) != 0) { + if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) { + // Note: the comparison above is safe without a mutex because we are the only method that *increases* + // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.) + memcpy(lastToRadio, val.data(), val.length()); - { // scope for fromPhoneMutex mutex - // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible. - std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); - bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val; - bluetoothPhoneAPI->fromPhoneQueueSize++; + { // scope for fromPhoneMutex mutex + // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible. + std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); + bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val; + bluetoothPhoneAPI->fromPhoneQueueSize++; + } + + // After releasing the mutex, schedule immediate processing of the new packet. + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + +#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING + int finishMillis = millis(); + LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount, + finishMillis - startMillis, val.length()); +#endif + } else { + LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length()); + } + } else { + LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length()); + } + } +}; + +class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks +{ +#ifdef NIMBLE_TWO + virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) +#else + virtual void onRead(NimBLECharacteristic *pCharacteristic) +#endif + { + // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. + + int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); + int tries = 0; + int startMillis = millis(); + +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis); +#endif + + // Is there a packet ready to go, or do we have to ask the main task to get one for us? + if (bluetoothPhoneAPI->toPhoneQueueSize > 0) { + // Note: the comparison above is safe without a mutex because we are the only method that *decreases* + // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.) + + // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData. +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount); +#endif + } else { + // Tell the main task that we'd like a packet. + bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true; + + // Wait for the main task to produce a packet for us, up to about 20 seconds. + // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer + // doing various setup tasks. + while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) { + // Schedule the main task runOnce to run ASAP. + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + + if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) { + // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran + // already +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount, + millis() - startMillis, tries); +#endif + break; + } + + // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back. + // No harm in polling pretty frequently. + delay(tries < 20 ? 1 : 5); + tries++; + + if (tries == 4000) { + LOG_WARN( + "BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response", + currentReadCount, millis() - startMillis, tries); + } + } } - // After releasing the mutex, schedule immediate processing of the new packet. - bluetoothPhoneAPI->setIntervalFromNow(0); - concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet + size_t numBytes = 0; + { // scope for toPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); + size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load(); + if (toPhoneQueueSize > 0) { + // Copy from the front of the toPhoneQueue + memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]); + numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0]; -#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING + // Shift the rest of the queue down + for (uint8_t i = 1; i < toPhoneQueueSize; i++) { + memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(), + bluetoothPhoneAPI->toPhoneQueueByteSizes[i]); + // The above line is similar to: + // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i] + // but is usually faster because it doesn't have to copy all the trailing bytes beyond + // toPhoneQueueByteSizes[i]. + // + // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic + // memory allocations and frees across FreeRTOS tasks. + + bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i]; + } + + // Safe decrement due to onDisconnect + if (bluetoothPhoneAPI->toPhoneQueueSize > 0) + bluetoothPhoneAPI->toPhoneQueueSize--; + } else { + // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0. + } + } + +#ifdef DEBUG_NIMBLE_ON_READ_TIMING int finishMillis = millis(); - LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount, finishMillis - startMillis, val.length()); + LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount, + finishMillis - startMillis, tries, numBytes); #endif - } else { - LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length()); - } - } else { - LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length()); + + pCharacteristic->setValue(fromRadioBytes, numBytes); + + // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue. + if (numBytes != 0) { + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + } } - } }; -class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { +class NimbleBluetoothServerCallback : public NimBLEServerCallbacks +{ #ifdef NIMBLE_TWO - virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) + public: + NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } + + private: + NimbleBluetooth *ble; + + virtual uint32_t onPassKeyDisplay() #else - virtual void onRead(NimBLECharacteristic *pCharacteristic) + virtual uint32_t onPassKeyRequest() #endif - { - // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's - // runOnce. + { + uint32_t passkey = config.bluetooth.fixed_pin; - int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); - int tries = 0; - int startMillis = millis(); - -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis); -#endif - - // Is there a packet ready to go, or do we have to ask the main task to get one for us? - if (bluetoothPhoneAPI->toPhoneQueueSize > 0) { - // Note: the comparison above is safe without a mutex because we are the only method that *decreases* - // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.) - - // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData. -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount); -#endif - } else { - // Tell the main task that we'd like a packet. - bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true; - - // Wait for the main task to produce a packet for us, up to about 20 seconds. - // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for - // longer doing various setup tasks. - while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) { - // Schedule the main task runOnce to run ASAP. - bluetoothPhoneAPI->setIntervalFromNow(0); - concurrency::mainDelay.interrupt(); // wake up main loop if sleeping - - if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) { - // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran - // already -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount, millis() - startMillis, tries); -#endif - break; + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { + LOG_INFO("Use random passkey"); + // This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits + passkey = random(100000, 999999); } + LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); - // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back. - // No harm in polling pretty frequently. - delay(tries < 20 ? 1 : 5); - tries++; - - if (tries == 4000) { - LOG_WARN("BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response", currentReadCount, - millis() - startMillis, tries); - } - } - } - - // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. - uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet - size_t numBytes = 0; - { // scope for toPhoneMutex mutex - std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); - size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load(); - if (toPhoneQueueSize > 0) { - // Copy from the front of the toPhoneQueue - memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]); - numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0]; - - // Shift the rest of the queue down - for (uint8_t i = 1; i < toPhoneQueueSize; i++) { - memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(), - bluetoothPhoneAPI->toPhoneQueueByteSizes[i]); - // The above line is similar to: - // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i] - // but is usually faster because it doesn't have to copy all the trailing bytes beyond - // toPhoneQueueByteSizes[i]. - // - // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic - // memory allocations and frees across FreeRTOS tasks. - - bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i]; - } - - // Safe decrement due to onDisconnect - if (bluetoothPhoneAPI->toPhoneQueueSize > 0) - bluetoothPhoneAPI->toPhoneQueueSize--; - } else { - // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0. - } - } - -#ifdef DEBUG_NIMBLE_ON_READ_TIMING - int finishMillis = millis(); - LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount, finishMillis - startMillis, tries, - numBytes); -#endif - - pCharacteristic->setValue(fromRadioBytes, numBytes); - - // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue. - if (numBytes != 0) { - bluetoothPhoneAPI->setIntervalFromNow(0); - concurrency::mainDelay.interrupt(); // wake up main loop if sleeping - } - } -}; - -class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { -#ifdef NIMBLE_TWO -public: - NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } - -private: - NimbleBluetooth *ble; - - virtual uint32_t onPassKeyDisplay() -#else - virtual uint32_t onPassKeyRequest() -#endif - { - uint32_t passkey = config.bluetooth.fixed_pin; - - if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { - LOG_INFO("Use random passkey"); - // This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits - passkey = random(100000, 999999); - } - LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); - - powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); - bluetoothStatus->updateStatus(&newStatus); + powerFSM.trigger(EVENT_BLUETOOTH_PAIR); + meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); + bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (screen) { - screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", passkey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 12; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", passkey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); #if !defined(M5STACK_UNITC6L) - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); #endif - display->setFont(FONT_LARGE); - char pin[8]; - snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + char pin[8]; + snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - char deviceName[64]; - snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); - } + display->setFont(FONT_SMALL); + char deviceName[64]; + snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif - passkeyShowing = true; + passkeyShowing = true; - return passkey; - } - -#ifdef NIMBLE_TWO - virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) -#else - virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) -#endif - { - LOG_INFO("BLE authentication complete"); - - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newStatus); - - // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (passkeyShowing) { - passkeyShowing = false; - if (screen) - screen->endAlert(); + return passkey; } - // Store the connection handle for future use #ifdef NIMBLE_TWO - nimbleBluetoothConnHandle = connInfo.getConnHandle(); + virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) #else - nimbleBluetoothConnHandle = desc->conn_handle; + virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) #endif - } + { + LOG_INFO("BLE authentication complete"); + + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); + + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + if (passkeyShowing) { + passkeyShowing = false; + if (screen) + screen->endAlert(); + } + + // Store the connection handle for future use +#ifdef NIMBLE_TWO + nimbleBluetoothConnHandle = connInfo.getConnHandle(); +#else + nimbleBluetoothConnHandle = desc->conn_handle; +#endif + } #ifdef NIMBLE_TWO - virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) { - LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); + virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) + { + LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); - const uint16_t connHandle = connInfo.getConnHandle(); + const uint16_t connHandle = connInfo.getConnHandle(); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) - int phyResult = ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY); - if (phyResult == 0) { - LOG_INFO("BLE conn %u requested 2M PHY", connHandle); - } else { - LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult); - } + int phyResult = + ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY); + if (phyResult == 0) { + LOG_INFO("BLE conn %u requested 2M PHY", connHandle); + } else { + LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult); + } #endif - int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs); - if (dataLenResult == 0) { - LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets); - } else { - LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult); - } + int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs); + if (dataLenResult == 0) { + LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets); + } else { + LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult); + } - LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); - pServer->updateConnParams(connHandle, 6, 12, 0, 200); - } + LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); + pServer->updateConnParams(connHandle, 6, 12, 0, 200); + } #endif #ifdef NIMBLE_TWO - virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) { - LOG_INFO("BLE disconnect reason: %d", reason); + virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) + { + LOG_INFO("BLE disconnect reason: %d", reason); #else - virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) { - LOG_INFO("BLE disconnect"); + virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) + { + LOG_INFO("BLE disconnect"); #endif #ifdef NIMBLE_TWO - if (ble->isDeInit) - return; + if (ble->isDeInit) + return; #endif - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newStatus); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); - if (bluetoothPhoneAPI) { - bluetoothPhoneAPI->close(); + if (bluetoothPhoneAPI) { + bluetoothPhoneAPI->close(); - { // scope for fromPhoneMutex mutex - std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); - bluetoothPhoneAPI->fromPhoneQueueSize = 0; - } + { // scope for fromPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); + bluetoothPhoneAPI->fromPhoneQueueSize = 0; + } - bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; - { // scope for toPhoneMutex mutex - std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); - bluetoothPhoneAPI->toPhoneQueueSize = 0; - } + bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; + { // scope for toPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); + bluetoothPhoneAPI->toPhoneQueueSize = 0; + } - bluetoothPhoneAPI->readCount = 0; - bluetoothPhoneAPI->notifyCount = 0; - bluetoothPhoneAPI->writeCount = 0; - } + bluetoothPhoneAPI->readCount = 0; + bluetoothPhoneAPI->notifyCount = 0; + bluetoothPhoneAPI->writeCount = 0; + } - // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection - memset(lastToRadio, 0, sizeof(lastToRadio)); + // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection + memset(lastToRadio, 0, sizeof(lastToRadio)); - nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection" + nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection" #ifdef NIMBLE_TWO - // Restart Advertising - ble->startAdvertising(); + // Restart Advertising + ble->startAdvertising(); #else - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - if (!pAdvertising->start(0)) { - if (pAdvertising->isAdvertising()) { - LOG_DEBUG("BLE advertising already running"); - } else { - LOG_ERROR("BLE failed to restart advertising"); - } - } + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + if (!pAdvertising->start(0)) { + if (pAdvertising->isAdvertising()) { + LOG_DEBUG("BLE advertising already running"); + } else { + LOG_ERROR("BLE failed to restart advertising"); + } + } #endif - } + } }; static NimbleBluetoothToRadioCallback *toRadioCallbacks; static NimbleBluetoothFromRadioCallback *fromRadioCallbacks; -void NimbleBluetooth::shutdown() { - // No measurable power saving for ESP32 during light-sleep(?) +void NimbleBluetooth::shutdown() +{ + // No measurable power saving for ESP32 during light-sleep(?) #ifndef ARCH_ESP32 - // Shutdown bluetooth for minimum power draw - LOG_INFO("Disable bluetooth"); - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->reset(); - pAdvertising->stop(); + // Shutdown bluetooth for minimum power draw + LOG_INFO("Disable bluetooth"); + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->reset(); + pAdvertising->stop(); #endif } // Proper shutdown for ESP32. Needs reboot to reverse. -void NimbleBluetooth::deinit() { +void NimbleBluetooth::deinit() +{ #ifdef ARCH_ESP32 - LOG_INFO("Disable bluetooth until reboot"); - isDeInit = true; + LOG_INFO("Disable bluetooth until reboot"); + isDeInit = true; #ifdef BLE_LED #ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); + digitalWrite(BLE_LED, HIGH); #else - digitalWrite(BLE_LED, LOW); + digitalWrite(BLE_LED, LOW); #endif #endif #ifndef NIMBLE_TWO - NimBLEDevice::deinit(); + NimBLEDevice::deinit(); #endif #endif } // Has initial setup been completed -bool NimbleBluetooth::isActive() { return bleServer; } - -bool NimbleBluetooth::isConnected() { return bleServer->getConnectedCount() > 0; } - -int NimbleBluetooth::getRssi() { -#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) - if (!bleServer || !isConnected()) { - return 0; // No active BLE connection - } - - uint16_t connHandle = nimbleBluetoothConnHandle.load(); - - if (connHandle == BLE_HS_CONN_HANDLE_NONE) { - const auto peers = bleServer->getPeerDevices(); - if (!peers.empty()) { - connHandle = peers.front(); - nimbleBluetoothConnHandle = connHandle; - } - } - - if (connHandle == BLE_HS_CONN_HANDLE_NONE) { - return 0; // Connection handle not available yet - } - - int8_t rssi = 0; - const int rc = ble_gap_conn_rssi(connHandle, &rssi); - - if (rc == 0) { - return rssi; - } - LOG_DEBUG("BLE RSSI read failed, rc=%d", rc); -#endif - - return 0; +bool NimbleBluetooth::isActive() +{ + return bleServer; } -void NimbleBluetooth::setup() { - // Uncomment for testing - // NimbleBluetooth::clearBonds(); +bool NimbleBluetooth::isConnected() +{ + return bleServer->getConnectedCount() > 0; +} - LOG_INFO("Init the NimBLE bluetooth module"); +int NimbleBluetooth::getRssi() +{ +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) + if (!bleServer || !isConnected()) { + return 0; // No active BLE connection + } - NimBLEDevice::init(getDeviceName()); - NimBLEDevice::setPower(ESP_PWR_LVL_P9); + uint16_t connHandle = nimbleBluetoothConnHandle.load(); + + if (connHandle == BLE_HS_CONN_HANDLE_NONE) { + const auto peers = bleServer->getPeerDevices(); + if (!peers.empty()) { + connHandle = peers.front(); + nimbleBluetoothConnHandle = connHandle; + } + } + + if (connHandle == BLE_HS_CONN_HANDLE_NONE) { + return 0; // Connection handle not available yet + } + + int8_t rssi = 0; + const int rc = ble_gap_conn_rssi(connHandle, &rssi); + + if (rc == 0) { + return rssi; + } + LOG_DEBUG("BLE RSSI read failed, rc=%d", rc); +#endif + + return 0; +} + +void NimbleBluetooth::setup() +{ + // Uncomment for testing + // NimbleBluetooth::clearBonds(); + + LOG_INFO("Init the NimBLE bluetooth module"); + + NimBLEDevice::init(getDeviceName()); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) - int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); - if (mtuResult == 0) { - LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu); - } else { - LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult); - } + int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); + if (mtuResult == 0) { + LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu); + } else { + LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult); + } - int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK); - if (phyResult == 0) { - LOG_INFO("BLE default PHY preference set to 2M"); - } else { - LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult); - } + int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK); + if (phyResult == 0) { + LOG_INFO("BLE default PHY preference set to 2M"); + } else { + LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult); + } - int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs); - if (dataLenResult == 0) { - LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets); - } else { - LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs, dataLenResult); - } + int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs); + if (dataLenResult == 0) { + LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets); + } else { + LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs, + dataLenResult); + } #endif - if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC); - NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); - NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); - NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); - } - bleServer = NimBLEDevice::createServer(); + if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC); + NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); + NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); + NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); + } + bleServer = NimBLEDevice::createServer(); #ifdef NIMBLE_TWO - NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); #else - NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); #endif - bleServer->setCallbacks(serverCallbacks, true); - setupService(); - startAdvertising(); + bleServer->setCallbacks(serverCallbacks, true); + setupService(); + startAdvertising(); } -void NimbleBluetooth::setupService() { - NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID); - NimBLECharacteristic *ToRadioCharacteristic; - NimBLECharacteristic *FromRadioCharacteristic; - // Define the characteristics that the app is looking for - if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); - // Allow notifications so phones can stream FromRadio without polling. - FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); - fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); - logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); - } else { - ToRadioCharacteristic = - bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); - FromRadioCharacteristic = - bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); - fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | - NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); - logRadioCharacteristic = bleService->createCharacteristic( - LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U); - } - bluetoothPhoneAPI = new BluetoothPhoneAPI(); +void NimbleBluetooth::setupService() +{ + NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID); + NimBLECharacteristic *ToRadioCharacteristic; + NimBLECharacteristic *FromRadioCharacteristic; + // Define the characteristics that the app is looking for + if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); + // Allow notifications so phones can stream FromRadio without polling. + FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); + fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); + logRadioCharacteristic = + bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); + } else { + ToRadioCharacteristic = bleService->createCharacteristic( + TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); + FromRadioCharacteristic = bleService->createCharacteristic( + FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); + fromNumCharacteristic = + bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | + NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); + logRadioCharacteristic = bleService->createCharacteristic( + LOGRADIO_UUID, + NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U); + } + bluetoothPhoneAPI = new BluetoothPhoneAPI(); - toRadioCallbacks = new NimbleBluetoothToRadioCallback(); - ToRadioCharacteristic->setCallbacks(toRadioCallbacks); + toRadioCallbacks = new NimbleBluetoothToRadioCallback(); + ToRadioCharacteristic->setCallbacks(toRadioCallbacks); - fromRadioCallbacks = new NimbleBluetoothFromRadioCallback(); - FromRadioCharacteristic->setCallbacks(fromRadioCallbacks); + fromRadioCallbacks = new NimbleBluetoothFromRadioCallback(); + FromRadioCharacteristic->setCallbacks(fromRadioCallbacks); - bleService->start(); + bleService->start(); - // Setup the battery service - NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service - BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) - (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); + // Setup the battery service + NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service + BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) + (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); #ifdef NIMBLE_TWO - NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); + NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); #else - NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); + NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); #endif - batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); - batteryLevelDescriptor->setNamespace(1); - batteryLevelDescriptor->setUnit(0x27ad); + batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); + batteryLevelDescriptor->setNamespace(1); + batteryLevelDescriptor->setUnit(0x27ad); - batteryService->start(); + batteryService->start(); } -void NimbleBluetooth::startAdvertising() { +void NimbleBluetooth::startAdvertising() +{ #ifdef NIMBLE_TWO - NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - NimBLEExtAdvertisement legacyAdvertising; + NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + NimBLEExtAdvertisement legacyAdvertising; - legacyAdvertising.setLegacyAdvertising(true); - legacyAdvertising.setScannable(true); - legacyAdvertising.setConnectable(true); - legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); - if (powerStatus->getHasBattery() == 1) { - legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); - } - legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); - legacyAdvertising.setMinInterval(500); - legacyAdvertising.setMaxInterval(1000); + legacyAdvertising.setLegacyAdvertising(true); + legacyAdvertising.setScannable(true); + legacyAdvertising.setConnectable(true); + legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); + if (powerStatus->getHasBattery() == 1) { + legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); + } + legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); + legacyAdvertising.setMinInterval(500); + legacyAdvertising.setMaxInterval(1000); - NimBLEExtAdvertisement legacyScanResponse; - legacyScanResponse.setLegacyAdvertising(true); - legacyScanResponse.setConnectable(true); - legacyScanResponse.setName(getDeviceName()); + NimBLEExtAdvertisement legacyScanResponse; + legacyScanResponse.setLegacyAdvertising(true); + legacyScanResponse.setConnectable(true); + legacyScanResponse.setName(getDeviceName()); - if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { - LOG_ERROR("BLE failed to set legacyAdvertising"); - } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { - LOG_ERROR("BLE failed to set legacyScanResponse"); - } else if (!pAdvertising->start(0, 0, 0)) { - LOG_ERROR("BLE failed to start legacyAdvertising"); - } + if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { + LOG_ERROR("BLE failed to set legacyAdvertising"); + } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { + LOG_ERROR("BLE failed to set legacyScanResponse"); + } else if (!pAdvertising->start(0, 0, 0)) { + LOG_ERROR("BLE failed to start legacyAdvertising"); + } #else - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->reset(); - pAdvertising->addServiceUUID(MESH_SERVICE_UUID); - pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service - pAdvertising->start(0); + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->reset(); + pAdvertising->addServiceUUID(MESH_SERVICE_UUID); + pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service + pAdvertising->start(0); #endif } /// Given a level between 0-100, update the BLE attribute -void updateBatteryLevel(uint8_t level) { - if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { - BatteryCharacteristic->setValue(&level, 1); +void updateBatteryLevel(uint8_t level) +{ + if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { + BatteryCharacteristic->setValue(&level, 1); #ifdef NIMBLE_TWO - BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); + BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); #else - BatteryCharacteristic->notify(); + BatteryCharacteristic->notify(); #endif - } + } } -void NimbleBluetooth::clearBonds() { - LOG_INFO("Clearing bluetooth bonds!"); - NimBLEDevice::deleteAllBonds(); +void NimbleBluetooth::clearBonds() +{ + LOG_INFO("Clearing bluetooth bonds!"); + NimBLEDevice::deleteAllBonds(); } -void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) { - if (!bleServer || !isConnected() || length > 512) { - return; - } +void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) +{ + if (!bleServer || !isConnected() || length > 512) { + return; + } #ifdef NIMBLE_TWO - logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); + logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); #else - logRadioCharacteristic->notify(logMessage, length, true); + logRadioCharacteristic->notify(logMessage, length, true); #endif } -void clearNVS() { - NimBLEDevice::deleteAllBonds(); +void clearNVS() +{ + NimBLEDevice::deleteAllBonds(); #ifdef ARCH_ESP32 - ESP.restart(); + ESP.restart(); #endif } #endif diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index f485619c4..458fa4a67 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -1,25 +1,26 @@ #pragma once #include "BluetoothCommon.h" -class NimbleBluetooth : BluetoothApi { -public: - void setup(); - void shutdown(); - void deinit(); - void clearBonds(); - bool isActive(); - bool isConnected(); - int getRssi(); - void sendLog(const uint8_t *logMessage, size_t length); +class NimbleBluetooth : BluetoothApi +{ + public: + void setup(); + void shutdown(); + void deinit(); + void clearBonds(); + bool isActive(); + bool isConnected(); + int getRssi(); + void sendLog(const uint8_t *logMessage, size_t length); #if defined(NIMBLE_TWO) - void startAdvertising(); + void startAdvertising(); #endif - bool isDeInit = false; + bool isDeInit = false; -private: - void setupService(); + private: + void setupService(); #if !defined(NIMBLE_TWO) - void startAdvertising(); + void startAdvertising(); #endif }; diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp index 2414a9671..698336f69 100644 --- a/src/platform/esp32/BleOta.cpp +++ b/src/platform/esp32/BleOta.cpp @@ -4,40 +4,43 @@ static const String MESHTASTIC_OTA_APP_PROJECT_NAME("Meshtastic-OTA"); -const esp_partition_t *BleOta::findEspOtaAppPartition() { - const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); +const esp_partition_t *BleOta::findEspOtaAppPartition() +{ + const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + esp_app_desc_t app_desc; + esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } + if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) { + part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); + ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + } - if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) { - return part; - } else { - return nullptr; - } + if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) { + return part; + } else { + return nullptr; + } } -String BleOta::getOtaAppVersion() { - const esp_partition_t *part = findEspOtaAppPartition(); - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - String version; - if (ret == ESP_OK) { - version = app_desc.version; - } - return version; +String BleOta::getOtaAppVersion() +{ + const esp_partition_t *part = findEspOtaAppPartition(); + esp_app_desc_t app_desc; + esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + String version; + if (ret == ESP_OK) { + version = app_desc.version; + } + return version; } -bool BleOta::switchToOtaApp() { - bool success = false; - const esp_partition_t *part = findEspOtaAppPartition(); - if (part) { - success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); - } - return success; +bool BleOta::switchToOtaApp() +{ + bool success = false; + const esp_partition_t *part = findEspOtaAppPartition(); + if (part) { + success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); + } + return success; } \ No newline at end of file diff --git a/src/platform/esp32/BleOta.h b/src/platform/esp32/BleOta.h index 6857ab51c..f4c510920 100644 --- a/src/platform/esp32/BleOta.h +++ b/src/platform/esp32/BleOta.h @@ -4,16 +4,17 @@ #include #include -class BleOta { -public: - explicit BleOta(){}; +class BleOta +{ + public: + explicit BleOta(){}; - static String getOtaAppVersion(); - static bool switchToOtaApp(); + static String getOtaAppVersion(); + static bool switchToOtaApp(); -private: - String mUserAgent; - static const esp_partition_t *findEspOtaAppPartition(); + private: + String mUserAgent; + static const esp_partition_t *findEspOtaAppPartition(); }; #endif // BLEOTA_H \ No newline at end of file diff --git a/src/platform/esp32/ESP32CryptoEngine.cpp b/src/platform/esp32/ESP32CryptoEngine.cpp index 43c73c41f..b554a3de4 100644 --- a/src/platform/esp32/ESP32CryptoEngine.cpp +++ b/src/platform/esp32/ESP32CryptoEngine.cpp @@ -3,37 +3,39 @@ #include "mbedtls/aes.h" -class ESP32CryptoEngine : public CryptoEngine { +class ESP32CryptoEngine : public CryptoEngine +{ - mbedtls_aes_context aes; + mbedtls_aes_context aes; -public: - ESP32CryptoEngine() { mbedtls_aes_init(&aes); } + public: + ESP32CryptoEngine() { mbedtls_aes_init(&aes); } - ~ESP32CryptoEngine() { mbedtls_aes_free(&aes); } + ~ESP32CryptoEngine() { mbedtls_aes_free(&aes); } - /** - * Encrypt a packet - * - * @param bytes is updated in place - * TODO: return bool, and handle graciously when something fails - */ - virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override { - if (_key.length > 0) { - if (numBytes <= MAX_BLOCKSIZE) { - mbedtls_aes_setkey_enc(&aes, _key.bytes, _key.length * 8); - static uint8_t scratch[MAX_BLOCKSIZE]; - uint8_t stream_block[16]; - size_t nc_off = 0; - memcpy(scratch, bytes, numBytes); - memset(scratch + numBytes, 0, - sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) - mbedtls_aes_crypt_ctr(&aes, numBytes, &nc_off, _nonce, stream_block, scratch, bytes); - } else { - LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); - } + /** + * Encrypt a packet + * + * @param bytes is updated in place + * TODO: return bool, and handle graciously when something fails + */ + virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override + { + if (_key.length > 0) { + if (numBytes <= MAX_BLOCKSIZE) { + mbedtls_aes_setkey_enc(&aes, _key.bytes, _key.length * 8); + static uint8_t scratch[MAX_BLOCKSIZE]; + uint8_t stream_block[16]; + size_t nc_off = 0; + memcpy(scratch, bytes, numBytes); + memset(scratch + numBytes, 0, + sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) + mbedtls_aes_crypt_ctr(&aes, numBytes, &nc_off, _nonce, stream_block, scratch, bytes); + } else { + LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes); + } + } } - } }; CryptoEngine *crypto = new ESP32CryptoEngine(); \ No newline at end of file diff --git a/src/platform/esp32/SimpleAllocator.cpp b/src/platform/esp32/SimpleAllocator.cpp index 27d38bbbc..d482a3f1c 100644 --- a/src/platform/esp32/SimpleAllocator.cpp +++ b/src/platform/esp32/SimpleAllocator.cpp @@ -2,17 +2,27 @@ #include "assert.h" #include "configuration.h" -SimpleAllocator::SimpleAllocator() { reset(); } - -void *SimpleAllocator::alloc(size_t size) { - assert(nextFree + size <= sizeof(bytes)); - void *res = &bytes[nextFree]; - nextFree += size; - LOG_DEBUG("Total simple allocs %u", nextFree); - - return res; +SimpleAllocator::SimpleAllocator() +{ + reset(); } -void SimpleAllocator::reset() { nextFree = 0; } +void *SimpleAllocator::alloc(size_t size) +{ + assert(nextFree + size <= sizeof(bytes)); + void *res = &bytes[nextFree]; + nextFree += size; + LOG_DEBUG("Total simple allocs %u", nextFree); -void *operator new(size_t size, SimpleAllocator &p) { return p.alloc(size); } + return res; +} + +void SimpleAllocator::reset() +{ + nextFree = 0; +} + +void *operator new(size_t size, SimpleAllocator &p) +{ + return p.alloc(size); +} diff --git a/src/platform/esp32/SimpleAllocator.h b/src/platform/esp32/SimpleAllocator.h index 7bb8da116..eaf4ee710 100644 --- a/src/platform/esp32/SimpleAllocator.h +++ b/src/platform/esp32/SimpleAllocator.h @@ -12,20 +12,21 @@ * Currently the only usecase for this class is the ESP32 bluetooth stack, where once we've called deinit(false) * we are sure all those bluetooth objects no longer exist, and we'll need to recreate them when we restart bluetooth */ -class SimpleAllocator { - uint8_t bytes[POOL_SIZE] = {}; +class SimpleAllocator +{ + uint8_t bytes[POOL_SIZE] = {}; - uint32_t nextFree = 0; + uint32_t nextFree = 0; -public: - SimpleAllocator(); + public: + SimpleAllocator(); - void *alloc(size_t size); + void *alloc(size_t size); - /** If you are _sure_ no outstanding references to blocks in this buffer still exist, you can call - * reset() to start from scratch. - * */ - void reset(); + /** If you are _sure_ no outstanding references to blocks in this buffer still exist, you can call + * reset() to start from scratch. + * */ + void reset(); }; void *operator new(size_t size, SimpleAllocator &p); @@ -34,8 +35,9 @@ void *operator new(size_t size, SimpleAllocator &p); * Temporarily makes the specified Allocator be used for _all_ allocations. Useful when calling library routines * that don't know about pools */ -class AllocatorScope { -public: - explicit AllocatorScope(SimpleAllocator &a); - ~AllocatorScope(); +class AllocatorScope +{ + public: + explicit AllocatorScope(SimpleAllocator &a); + ~AllocatorScope(); }; diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/WiFiOTA.cpp index d6177fb7f..4cf157b4c 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/WiFiOTA.cpp @@ -3,77 +3,90 @@ #include #include -namespace WiFiOTA { +namespace WiFiOTA +{ static const char *nvsNamespace = "ota-wifi"; static const char *appProjectName = "OTA-WiFi"; static bool updated = false; -bool isUpdated() { return updated; } +bool isUpdated() +{ + return updated; +} -void initialize() { - Preferences prefs; - prefs.begin(nvsNamespace); - if (prefs.getBool("updated")) { - LOG_INFO("First boot after OTA update"); - updated = true; +void initialize() +{ + Preferences prefs; + prefs.begin(nvsNamespace); + if (prefs.getBool("updated")) { + LOG_INFO("First boot after OTA update"); + updated = true; + prefs.putBool("updated", false); + } + prefs.end(); +} + +void recoverConfig(meshtastic_Config_NetworkConfig *network) +{ + LOG_INFO("Recovering WiFi settings after OTA update"); + + Preferences prefs; + prefs.begin(nvsNamespace, true); + String ssid = prefs.getString("ssid"); + String psk = prefs.getString("psk"); + prefs.end(); + + network->wifi_enabled = true; + strncpy(network->wifi_ssid, ssid.c_str(), sizeof(network->wifi_ssid)); + strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); +} + +void saveConfig(meshtastic_Config_NetworkConfig *network) +{ + LOG_INFO("Saving WiFi settings for upcoming OTA update"); + + Preferences prefs; + prefs.begin(nvsNamespace); + prefs.putString("ssid", network->wifi_ssid); + prefs.putString("psk", network->wifi_psk); prefs.putBool("updated", false); - } - prefs.end(); + prefs.end(); } -void recoverConfig(meshtastic_Config_NetworkConfig *network) { - LOG_INFO("Recovering WiFi settings after OTA update"); - - Preferences prefs; - prefs.begin(nvsNamespace, true); - String ssid = prefs.getString("ssid"); - String psk = prefs.getString("psk"); - prefs.end(); - - network->wifi_enabled = true; - strncpy(network->wifi_ssid, ssid.c_str(), sizeof(network->wifi_ssid)); - strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); +const esp_partition_t *getAppPartition() +{ + return esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); } -void saveConfig(meshtastic_Config_NetworkConfig *network) { - LOG_INFO("Saving WiFi settings for upcoming OTA update"); - - Preferences prefs; - prefs.begin(nvsNamespace); - prefs.putString("ssid", network->wifi_ssid); - prefs.putString("psk", network->wifi_psk); - prefs.putBool("updated", false); - prefs.end(); +bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) +{ + if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) + return false; + if (strcmp(app_desc->project_name, appProjectName) != 0) + return false; + return true; } -const esp_partition_t *getAppPartition() { return esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); } - -bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) { - if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) - return false; - if (strcmp(app_desc->project_name, appProjectName) != 0) - return false; - return true; +bool trySwitchToOTA() +{ + const esp_partition_t *part = getAppPartition(); + esp_app_desc_t app_desc; + if (!getAppDesc(part, &app_desc)) + return false; + if (esp_ota_set_boot_partition(part) != ESP_OK) + return false; + return true; } -bool trySwitchToOTA() { - const esp_partition_t *part = getAppPartition(); - esp_app_desc_t app_desc; - if (!getAppDesc(part, &app_desc)) - return false; - if (esp_ota_set_boot_partition(part) != ESP_OK) - return false; - return true; -} - -const char *getVersion() { - const esp_partition_t *part = getAppPartition(); - static esp_app_desc_t app_desc; - if (!getAppDesc(part, &app_desc)) - return ""; - return app_desc.version; +const char *getVersion() +{ + const esp_partition_t *part = getAppPartition(); + static esp_app_desc_t app_desc; + if (!getAppDesc(part, &app_desc)) + return ""; + return app_desc.version; } } // namespace WiFiOTA diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h index 753dbc9c8..5a7ee348a 100644 --- a/src/platform/esp32/WiFiOTA.h +++ b/src/platform/esp32/WiFiOTA.h @@ -4,7 +4,8 @@ #include "mesh-pb-constants.h" #include -namespace WiFiOTA { +namespace WiFiOTA +{ void initialize(); bool isUpdated(); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 0158627fb..085692f96 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -212,8 +212,8 @@ // ----------------------------------------------------------------------------- // If an SPI-related pin used by the LoRa module isn't defined, use the conventional pin number for it. -// FIXME: these pins should really be defined in each variant.h file to prevent breakages if the defaults change, -// currently many ESP32 variants don't define these pins in their variant.h file. +// FIXME: these pins should really be defined in each variant.h file to prevent breakages if the defaults change, currently many +// ESP32 variants don't define these pins in their variant.h file. #ifndef LORA_SCK #define LORA_SCK 5 #endif diff --git a/src/platform/esp32/iram-quirk.c b/src/platform/esp32/iram-quirk.c index 31283ae56..813842138 100644 --- a/src/platform/esp32/iram-quirk.c +++ b/src/platform/esp32/iram-quirk.c @@ -8,7 +8,10 @@ #define IRAM_SECTION section(".iram1.stub") -IRAM_ATTR esp_err_t stub_probe(esp_flash_t *chip, uint32_t flash_id) { return ESP_ERR_NOT_FOUND; } +IRAM_ATTR esp_err_t stub_probe(esp_flash_t *chip, uint32_t flash_id) +{ + return ESP_ERR_NOT_FOUND; +} const spi_flash_chip_t stub_flash_chip __attribute__((IRAM_SECTION)) = { .name = "stub", diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 67c636063..760964119 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -26,138 +26,143 @@ #include #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH -void setBluetoothEnable(bool enable) { +void setBluetoothEnable(bool enable) +{ #ifdef USE_WS5500 - if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false)) + if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false)) #elif HAS_WIFI - if (!isWifiAvailable() && config.bluetooth.enabled == true) + if (!isWifiAvailable() && config.bluetooth.enabled == true) #else - if (config.bluetooth.enabled == true) + if (config.bluetooth.enabled == true) #endif - { - if (!nimbleBluetooth) { - nimbleBluetooth = new NimbleBluetooth(); + { + if (!nimbleBluetooth) { + nimbleBluetooth = new NimbleBluetooth(); + } + if (enable && !nimbleBluetooth->isActive()) { + powerMon->setState(meshtastic_PowerMon_State_BT_On); + nimbleBluetooth->setup(); + } + // For ESP32, no way to recover from bluetooth shutdown without reboot + // BLE advertising automatically stops when MCU enters light-sleep(?) + // For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse } - if (enable && !nimbleBluetooth->isActive()) { - powerMon->setState(meshtastic_PowerMon_State_BT_On); - nimbleBluetooth->setup(); - } - // For ESP32, no way to recover from bluetooth shutdown without reboot - // BLE advertising automatically stops when MCU enters light-sleep(?) - // For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse - } } #else void setBluetoothEnable(bool enable) {} void updateBatteryLevel(uint8_t level) {} #endif -void getMacAddr(uint8_t *dmac) { +void getMacAddr(uint8_t *dmac) +{ #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_SOC_IEEE802154_SUPPORTED) - auto res = esp_base_mac_addr_get(dmac); - assert(res == ESP_OK); + auto res = esp_base_mac_addr_get(dmac); + assert(res == ESP_OK); #else - auto res = esp_efuse_mac_get_default(dmac); - assert(res == ESP_OK); + auto res = esp_efuse_mac_get_default(dmac); + assert(res == ESP_OK); #endif } #if HAS_32768HZ #define CALIBRATE_ONE(cali_clk) calibrate_one(cali_clk, #cali_clk) -static uint32_t calibrate_one(rtc_cal_sel_t cal_clk, const char *name) { - const uint32_t cal_count = 1000; - // const float factor = (1 << 19) * 1000.0f; unused var? - uint32_t cali_val; - for (int i = 0; i < 5; ++i) { - cali_val = rtc_clk_cal(cal_clk, cal_count); - } - return cali_val; +static uint32_t calibrate_one(rtc_cal_sel_t cal_clk, const char *name) +{ + const uint32_t cal_count = 1000; + // const float factor = (1 << 19) * 1000.0f; unused var? + uint32_t cali_val; + for (int i = 0; i < 5; ++i) { + cali_val = rtc_clk_cal(cal_clk, cal_count); + } + return cali_val; } -void enableSlowCLK() { - rtc_clk_32k_enable(true); +void enableSlowCLK() +{ + rtc_clk_32k_enable(true); - CALIBRATE_ONE(RTC_CAL_RTC_MUX); - uint32_t cal_32k = CALIBRATE_ONE(RTC_CAL_32K_XTAL); + CALIBRATE_ONE(RTC_CAL_RTC_MUX); + uint32_t cal_32k = CALIBRATE_ONE(RTC_CAL_32K_XTAL); - if (cal_32k == 0) { - LOG_DEBUG("32k XTAL OSC has not started up"); - } else { - rtc_clk_slow_freq_set(RTC_SLOW_FREQ_32K_XTAL); - LOG_DEBUG("Switch RTC Source to 32.768kHz succeeded, using 32k XTAL"); + if (cal_32k == 0) { + LOG_DEBUG("32k XTAL OSC has not started up"); + } else { + rtc_clk_slow_freq_set(RTC_SLOW_FREQ_32K_XTAL); + LOG_DEBUG("Switch RTC Source to 32.768kHz succeeded, using 32k XTAL"); + CALIBRATE_ONE(RTC_CAL_RTC_MUX); + CALIBRATE_ONE(RTC_CAL_32K_XTAL); + } CALIBRATE_ONE(RTC_CAL_RTC_MUX); CALIBRATE_ONE(RTC_CAL_32K_XTAL); - } - CALIBRATE_ONE(RTC_CAL_RTC_MUX); - CALIBRATE_ONE(RTC_CAL_32K_XTAL); - if (rtc_clk_slow_freq_get() != RTC_SLOW_FREQ_32K_XTAL) { - LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768kHz !!! "); - return; - } + if (rtc_clk_slow_freq_get() != RTC_SLOW_FREQ_32K_XTAL) { + LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768kHz !!! "); + return; + } } #endif -void esp32Setup() { - /* We explicitly don't want to do call randomSeed, - // as that triggers the esp32 core to use a less secure pseudorandom function. - uint32_t seed = esp_random(); - LOG_DEBUG("Set random seed %u", seed); - randomSeed(seed); - */ +void esp32Setup() +{ + /* We explicitly don't want to do call randomSeed, + // as that triggers the esp32 core to use a less secure pseudorandom function. + uint32_t seed = esp_random(); + LOG_DEBUG("Set random seed %u", seed); + randomSeed(seed); + */ #ifdef ADC_V - pinMode(ADC_V, INPUT); + pinMode(ADC_V, INPUT); #endif - LOG_DEBUG("Total heap: %d", ESP.getHeapSize()); - LOG_DEBUG("Free heap: %d", ESP.getFreeHeap()); - LOG_DEBUG("Total PSRAM: %d", ESP.getPsramSize()); - LOG_DEBUG("Free PSRAM: %d", ESP.getFreePsram()); + LOG_DEBUG("Total heap: %d", ESP.getHeapSize()); + LOG_DEBUG("Free heap: %d", ESP.getFreeHeap()); + LOG_DEBUG("Total PSRAM: %d", ESP.getPsramSize()); + LOG_DEBUG("Free PSRAM: %d", ESP.getFreePsram()); - nvs_stats_t nvs_stats; - auto res = nvs_get_stats(NULL, &nvs_stats); - assert(res == ESP_OK); - LOG_DEBUG("NVS: UsedEntries %d, FreeEntries %d, AllEntries %d, NameSpaces %d", nvs_stats.used_entries, nvs_stats.free_entries, - nvs_stats.total_entries, nvs_stats.namespace_count); + nvs_stats_t nvs_stats; + auto res = nvs_get_stats(NULL, &nvs_stats); + assert(res == ESP_OK); + LOG_DEBUG("NVS: UsedEntries %d, FreeEntries %d, AllEntries %d, NameSpaces %d", nvs_stats.used_entries, nvs_stats.free_entries, + nvs_stats.total_entries, nvs_stats.namespace_count); - LOG_DEBUG("Setup Preferences in Flash Storage"); + LOG_DEBUG("Setup Preferences in Flash Storage"); - // Create object to store our persistent data - Preferences preferences; - preferences.begin("meshtastic", false); + // Create object to store our persistent data + Preferences preferences; + preferences.begin("meshtastic", false); - uint32_t rebootCounter = preferences.getUInt("rebootCounter", 0); - rebootCounter++; - preferences.putUInt("rebootCounter", rebootCounter); - // store firmware version and hwrevision for access from OTA firmware - String fwrev = preferences.getString("firmwareVersion", ""); - if (fwrev.compareTo(optstr(APP_VERSION)) != 0) - preferences.putString("firmwareVersion", optstr(APP_VERSION)); - uint8_t hwven = preferences.getUInt("hwVendor", 0); - if (hwven != HW_VENDOR) - preferences.putUInt("hwVendor", HW_VENDOR); - preferences.end(); - LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); + uint32_t rebootCounter = preferences.getUInt("rebootCounter", 0); + rebootCounter++; + preferences.putUInt("rebootCounter", rebootCounter); + // store firmware version and hwrevision for access from OTA firmware + String fwrev = preferences.getString("firmwareVersion", ""); + if (fwrev.compareTo(optstr(APP_VERSION)) != 0) + preferences.putString("firmwareVersion", optstr(APP_VERSION)); + uint8_t hwven = preferences.getUInt("hwVendor", 0); + if (hwven != HW_VENDOR) + preferences.putUInt("hwVendor", HW_VENDOR); + preferences.end(); + LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); #if !MESHTASTIC_EXCLUDE_BLUETOOTH - String BLEOTA = BleOta::getOtaAppVersion(); - if (BLEOTA.isEmpty()) { - LOG_INFO("No BLE OTA firmware available"); - } else { - LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); - } + String BLEOTA = BleOta::getOtaAppVersion(); + if (BLEOTA.isEmpty()) { + LOG_INFO("No BLE OTA firmware available"); + } else { + LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); + } #endif #if !MESHTASTIC_EXCLUDE_WIFI - String version = WiFiOTA::getVersion(); - if (version.isEmpty()) { - LOG_INFO("No WiFi OTA firmware available"); - } else { - LOG_INFO("WiFi OTA firmware version %s", version.c_str()); - } - WiFiOTA::initialize(); + String version = WiFiOTA::getVersion(); + if (version.isEmpty()) { + LOG_INFO("No WiFi OTA firmware available"); + } else { + LOG_INFO("WiFi OTA firmware version %s", version.c_str()); + } + WiFiOTA::initialize(); #endif - // enableModemSleep(); + // enableModemSleep(); // Since we are turning on watchdogs rather late in the release schedule, we really don't want to catch any // false positives. The wait-to-sleep timeout for shutting down radios is 30 secs, so pick 45 for now. @@ -165,96 +170,98 @@ void esp32Setup() { #define APP_WATCHDOG_SECS 90 #ifdef CONFIG_IDF_TARGET_ESP32C6 - esp_task_wdt_config_t *wdt_config = (esp_task_wdt_config_t *)malloc(sizeof(esp_task_wdt_config_t)); - wdt_config->timeout_ms = APP_WATCHDOG_SECS * 1000; - wdt_config->trigger_panic = true; - res = esp_task_wdt_init(wdt_config); - assert(res == ESP_OK); + esp_task_wdt_config_t *wdt_config = (esp_task_wdt_config_t *)malloc(sizeof(esp_task_wdt_config_t)); + wdt_config->timeout_ms = APP_WATCHDOG_SECS * 1000; + wdt_config->trigger_panic = true; + res = esp_task_wdt_init(wdt_config); + assert(res == ESP_OK); #else - res = esp_task_wdt_init(APP_WATCHDOG_SECS, true); - assert(res == ESP_OK); + res = esp_task_wdt_init(APP_WATCHDOG_SECS, true); + assert(res == ESP_OK); #endif - res = esp_task_wdt_add(NULL); - assert(res == ESP_OK); + res = esp_task_wdt_add(NULL); + assert(res == ESP_OK); #if HAS_32768HZ - enableSlowCLK(); + enableSlowCLK(); #endif } /// loop code specific to ESP32 targets -void esp32Loop() { - esp_task_wdt_reset(); // service our app level watchdog +void esp32Loop() +{ + esp_task_wdt_reset(); // service our app level watchdog - // for debug printing - // radio.radioIf.canSleep(); + // for debug printing + // radio.radioIf.canSleep(); } -void cpuDeepSleep(uint32_t msecToWake) { - /* - Some ESP32 IOs have internal pullups or pulldowns, which are enabled by default. - If an external circuit drives this pin in deep sleep mode, current consumption may - increase due to current flowing through these pullups and pulldowns. +void cpuDeepSleep(uint32_t msecToWake) +{ + /* + Some ESP32 IOs have internal pullups or pulldowns, which are enabled by default. + If an external circuit drives this pin in deep sleep mode, current consumption may + increase due to current flowing through these pullups and pulldowns. - To isolate a pin, preventing extra current draw, call rtc_gpio_isolate() function. - For example, on ESP32-WROVER module, GPIO12 is pulled up externally. - GPIO12 also has an internal pulldown in the ESP32 chip. This means that in deep sleep, - some current will flow through these external and internal resistors, increasing deep - sleep current above the minimal possible value. + To isolate a pin, preventing extra current draw, call rtc_gpio_isolate() function. + For example, on ESP32-WROVER module, GPIO12 is pulled up externally. + GPIO12 also has an internal pulldown in the ESP32 chip. This means that in deep sleep, + some current will flow through these external and internal resistors, increasing deep + sleep current above the minimal possible value. - Note: we don't isolate pins that are used for the LORA, LED, i2c, or ST7735 Display for the Chatter2, spi or the wake - button(s), maybe we should not include any other GPIOs... - */ + Note: we don't isolate pins that are used for the LORA, LED, i2c, or ST7735 Display for the Chatter2, spi or the wake + button(s), maybe we should not include any other GPIOs... + */ #if SOC_RTCIO_HOLD_SUPPORTED - static const uint8_t rtcGpios[] = { + static const uint8_t rtcGpios[] = { #ifndef HELTEC_VISION_MASTER_E213 - // For this variant, >20mA leaks through the display if pin 2 held - // Todo: check if it's safe to remove this pin for all variants - 2, + // For this variant, >20mA leaks through the display if pin 2 held + // Todo: check if it's safe to remove this pin for all variants + 2, #endif #ifndef USE_JTAG - 13, + 13, #endif - 34, 35, 37}; + 34, 35, 37}; - for (int i = 0; i < sizeof(rtcGpios); i++) - rtc_gpio_isolate((gpio_num_t)rtcGpios[i]); + for (int i = 0; i < sizeof(rtcGpios); i++) + rtc_gpio_isolate((gpio_num_t)rtcGpios[i]); #endif - // FIXME, disable internal rtc pullups/pulldowns on the non isolated pins. for inputs that we aren't using - // to detect wake and in normal operation the external part drives them hard. + // FIXME, disable internal rtc pullups/pulldowns on the non isolated pins. for inputs that we aren't using + // to detect wake and in normal operation the external part drives them hard. #ifdef BUTTON_PIN - // Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39. + // Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39. #if SOC_RTCIO_HOLD_SUPPORTED && SOC_PM_SUPPORT_EXT_WAKEUP - uint64_t gpioMask = (1ULL << (config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + uint64_t gpioMask = (1ULL << (config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); #endif #ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)BUTTON_PIN); + gpio_pullup_en((gpio_num_t)BUTTON_PIN); #endif - // Not needed because both of the current boards have external pullups - // FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons - // (instead of just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN); + // Not needed because both of the current boards have external pullups + // FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons (instead + // of just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN); #ifdef ESP32S3_WAKE_TYPE - esp_sleep_enable_ext1_wakeup(gpioMask, ESP32S3_WAKE_TYPE); + esp_sleep_enable_ext1_wakeup(gpioMask, ESP32S3_WAKE_TYPE); #else #if SOC_PM_SUPPORT_EXT_WAKEUP #ifdef CONFIG_IDF_TARGET_ESP32 - // ESP_EXT1_WAKEUP_ALL_LOW has been deprecated since esp-idf v5.4 for any other target. - esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW); + // ESP_EXT1_WAKEUP_ALL_LOW has been deprecated since esp-idf v5.4 for any other target. + esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW); #else - esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ANY_LOW); + esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ANY_LOW); #endif #endif #endif // #end ESP32S3_WAKE_TYPE #endif - // We want RTC peripherals to stay on - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + // We want RTC peripherals to stay on + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs - esp_deep_sleep_start(); // TBD mA sleep current (battery) + esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs + esp_deep_sleep_start(); // TBD mA sleep current (battery) } diff --git a/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp b/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp index 73ad6c541..12960e2d9 100644 --- a/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp +++ b/src/platform/extra_variants/heltec_wireless_tracker/variant.cpp @@ -7,33 +7,34 @@ #include "graphics/TFTDisplay.h" // Heltec tracker specific init -void lateInitVariant() { - // LOG_DEBUG("Heltec tracker initVariant"); +void lateInitVariant() +{ + // LOG_DEBUG("Heltec tracker initVariant"); #ifndef MESHTASTIC_EXCLUDE_GPS - GpioVirtPin *virtGpsEnable = gps ? gps->enablePin : new GpioVirtPin(); + GpioVirtPin *virtGpsEnable = gps ? gps->enablePin : new GpioVirtPin(); #else - GpioVirtPin *virtGpsEnable = new GpioVirtPin(); + GpioVirtPin *virtGpsEnable = new GpioVirtPin(); #endif #ifndef MESHTASTIC_EXCLUDE_SCREEN - // On this board we are actually using the backlightEnable signal to already be controlling a physical enable to the - // display controller. But we'd _ALSO_ like to have that signal drive a virtual GPIO. So nest it as needed. - GpioVirtPin *virtScreenEnable = new GpioVirtPin(); - if (TFTDisplay::backlightEnable) { - GpioPin *physScreenEnable = TFTDisplay::backlightEnable; - GpioPin *splitter = new GpioSplitter(virtScreenEnable, physScreenEnable); - TFTDisplay::backlightEnable = splitter; + // On this board we are actually using the backlightEnable signal to already be controlling a physical enable to the + // display controller. But we'd _ALSO_ like to have that signal drive a virtual GPIO. So nest it as needed. + GpioVirtPin *virtScreenEnable = new GpioVirtPin(); + if (TFTDisplay::backlightEnable) { + GpioPin *physScreenEnable = TFTDisplay::backlightEnable; + GpioPin *splitter = new GpioSplitter(virtScreenEnable, physScreenEnable); + TFTDisplay::backlightEnable = splitter; - // Assume screen is initially powered - splitter->set(true); - } + // Assume screen is initially powered + splitter->set(true); + } #endif #if defined(VEXT_ENABLE) && (!defined(MESHTASTIC_EXCLUDE_GPS) || !defined(MESHTASTIC_EXCLUDE_SCREEN)) - // If either the GPS or the screen is on, turn on the external power regulator - GpioPin *hwEnable = new GpioHwPin(VEXT_ENABLE); - new GpioBinaryTransformer(virtGpsEnable, virtScreenEnable, hwEnable, GpioBinaryTransformer::Or); + // If either the GPS or the screen is on, turn on the external power regulator + GpioPin *hwEnable = new GpioHwPin(VEXT_ENABLE); + new GpioBinaryTransformer(virtGpsEnable, virtScreenEnable, hwEnable, GpioBinaryTransformer::Or); #endif } diff --git a/src/platform/extra_variants/t_deck_pro/variant.cpp b/src/platform/extra_variants/t_deck_pro/variant.cpp index 7fe84025b..eae9335ce 100644 --- a/src/platform/extra_variants/t_deck_pro/variant.cpp +++ b/src/platform/extra_variants/t_deck_pro/variant.cpp @@ -8,19 +8,21 @@ CSE_CST328 tsPanel = CSE_CST328(EINK_WIDTH, EINK_HEIGHT, &Wire, CST328_PIN_RST, CST328_PIN_INT); -bool readTouch(int16_t *x, int16_t *y) { - if (tsPanel.getTouches()) { - *x = tsPanel.getPoint(0).x; - *y = tsPanel.getPoint(0).y; - return true; - } - return false; +bool readTouch(int16_t *x, int16_t *y) +{ + if (tsPanel.getTouches()) { + *x = tsPanel.getPoint(0).x; + *y = tsPanel.getPoint(0).y; + return true; + } + return false; } // T-Deck Pro specific init -void lateInitVariant() { - tsPanel.begin(); - touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch); - touchScreenImpl1->init(); +void lateInitVariant() +{ + tsPanel.begin(); + touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch); + touchScreenImpl1->init(); } #endif \ No newline at end of file diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp index fb8e4d1e6..ea5773d30 100644 --- a/src/platform/extra_variants/t_lora_pager/variant.cpp +++ b/src/platform/extra_variants/t_lora_pager/variant.cpp @@ -8,19 +8,20 @@ DriverPins PinsAudioBoardES8311; AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); // TLora Pager specific init -void lateInitVariant() { - // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); - // I2C: function, scl, sda - PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); - // I2S: function, mclk, bck, ws, data_out, data_in - PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); +void lateInitVariant() +{ + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); - // configure codec - CodecConfig cfg; - cfg.input_device = ADC_INPUT_LINE1; - cfg.output_device = DAC_OUTPUT_ALL; - cfg.i2s.bits = BIT_LENGTH_16BITS; - cfg.i2s.rate = RATE_44K; - board.begin(cfg); + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); } #endif \ No newline at end of file diff --git a/src/platform/extra_variants/tbeam_displayshield/variant.cpp b/src/platform/extra_variants/tbeam_displayshield/variant.cpp index cf08698f8..7beac2293 100644 --- a/src/platform/extra_variants/tbeam_displayshield/variant.cpp +++ b/src/platform/extra_variants/tbeam_displayshield/variant.cpp @@ -10,32 +10,34 @@ TouchDrvCSTXXX tsPanel; static constexpr uint8_t PossibleAddresses[2] = {CST328_ADDR, CST226SE_ADDR_ALT}; uint8_t i2cAddress = 0; -bool readTouch(int16_t *x, int16_t *y) { - int16_t x_array[1], y_array[1]; - uint8_t touched = tsPanel.getPoint(x_array, y_array, 1); - if (touched > 0) { - *y = x_array[0]; - *x = (TFT_WIDTH - y_array[0]); - // Check bounds - if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) { - return false; +bool readTouch(int16_t *x, int16_t *y) +{ + int16_t x_array[1], y_array[1]; + uint8_t touched = tsPanel.getPoint(x_array, y_array, 1); + if (touched > 0) { + *y = x_array[0]; + *x = (TFT_WIDTH - y_array[0]); + // Check bounds + if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) { + return false; + } + return true; // Valid touch detected } - return true; // Valid touch detected - } - return false; // No valid touch data + return false; // No valid touch data } -void lateInitVariant() { - tsPanel.setTouchDrvModel(TouchDrv_CST226); - for (uint8_t addr : PossibleAddresses) { - if (tsPanel.begin(Wire, addr, I2C_SDA, I2C_SCL)) { - i2cAddress = addr; - LOG_DEBUG("CST226SE init OK at address 0x%02X", addr); - touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch); - touchScreenImpl1->init(); - return; +void lateInitVariant() +{ + tsPanel.setTouchDrvModel(TouchDrv_CST226); + for (uint8_t addr : PossibleAddresses) { + if (tsPanel.begin(Wire, addr, I2C_SDA, I2C_SCL)) { + i2cAddress = addr; + LOG_DEBUG("CST226SE init OK at address 0x%02X", addr); + touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch); + touchScreenImpl1->init(); + return; + } } - } - LOG_ERROR("CST226SE init failed at all known addresses"); + LOG_ERROR("CST226SE init failed at all known addresses"); } #endif diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp index a119b6278..836fb1307 100644 --- a/src/platform/nrf52/AsyncUDP.cpp +++ b/src/platform/nrf52/AsyncUDP.cpp @@ -4,48 +4,70 @@ AsyncUDP::AsyncUDP() : OSThread("AsyncUDP"), localPort(0) {} -bool AsyncUDP::listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl) { - if (!isMulticast(multicastIP)) - return false; - localPort = port; - udp.beginMulticast(multicastIP, port); - return true; +bool AsyncUDP::listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl) +{ + if (!isMulticast(multicastIP)) + return false; + localPort = port; + udp.beginMulticast(multicastIP, port); + return true; } -size_t AsyncUDP::write(uint8_t b) { return udp.write(&b, 1); } +size_t AsyncUDP::write(uint8_t b) +{ + return udp.write(&b, 1); +} -size_t AsyncUDP::write(const uint8_t *data, size_t len) { return udp.write(data, len); } +size_t AsyncUDP::write(const uint8_t *data, size_t len) +{ + return udp.write(data, len); +} -void AsyncUDP::onPacket(const std::function &callback) { _onPacket = callback; } +void AsyncUDP::onPacket(const std::function &callback) +{ + _onPacket = callback; +} -bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port) { - if (!udp.beginPacket(ip, port)) - return false; - udp.write(data, len); - return udp.endPacket(); +bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port) +{ + if (!udp.beginPacket(ip, port)) + return false; + udp.write(data, len); + return udp.endPacket(); } // AsyncUDPPacket -AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort()) { - if (_udp.available() > 0) { - _readLength = _udp.read(_buffer, sizeof(_buffer)); - } else { - _readLength = 0; - } +AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort()) +{ + if (_udp.available() > 0) { + _readLength = _udp.read(_buffer, sizeof(_buffer)); + } else { + _readLength = 0; + } } -IPAddress AsyncUDPPacket::remoteIP() { return _remoteIP; } +IPAddress AsyncUDPPacket::remoteIP() +{ + return _remoteIP; +} -uint16_t AsyncUDPPacket::length() { return _readLength; } +uint16_t AsyncUDPPacket::length() +{ + return _readLength; +} -const uint8_t *AsyncUDPPacket::data() { return _buffer; } +const uint8_t *AsyncUDPPacket::data() +{ + return _buffer; +} -int32_t AsyncUDP::runOnce() { - if (_onPacket && udp.parsePacket() > 0) { - AsyncUDPPacket packet(udp); - _onPacket(packet); - } - return 5; // check every 5ms +int32_t AsyncUDP::runOnce() +{ + if (_onPacket && udp.parsePacket() > 0) { + AsyncUDPPacket packet(udp); + _onPacket(packet); + } + return 5; // check every 5ms } #endif // HAS_ETHERNET \ No newline at end of file diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h index b710c7d59..e2b406ba9 100644 --- a/src/platform/nrf52/AsyncUDP.h +++ b/src/platform/nrf52/AsyncUDP.h @@ -14,44 +14,49 @@ class AsyncUDPPacket; -class AsyncUDP : public Print, private concurrency::OSThread { -public: - AsyncUDP(); - explicit operator bool() const { return localPort != 0; } +class AsyncUDP : public Print, private concurrency::OSThread +{ + public: + AsyncUDP(); + explicit operator bool() const { return localPort != 0; } - bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); - bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); + bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); + bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); - size_t write(uint8_t b) override; - size_t write(const uint8_t *data, size_t len) override; - void onPacket(const std::function &callback); + size_t write(uint8_t b) override; + size_t write(const uint8_t *data, size_t len) override; + void onPacket(const std::function &callback); -private: - EthernetUDP udp; - uint16_t localPort; - std::function _onPacket; - virtual int32_t runOnce() override; + private: + EthernetUDP udp; + uint16_t localPort; + std::function _onPacket; + virtual int32_t runOnce() override; }; -class AsyncUDPPacket { -public: - AsyncUDPPacket(EthernetUDP &source); +class AsyncUDPPacket +{ + public: + AsyncUDPPacket(EthernetUDP &source); - IPAddress remoteIP(); - uint16_t length(); - const uint8_t *data(); + IPAddress remoteIP(); + uint16_t length(); + const uint8_t *data(); -private: - EthernetUDP &_udp; - IPAddress _remoteIP; - uint16_t _remotePort; - size_t _readLength = 0; + private: + EthernetUDP &_udp; + IPAddress _remoteIP; + uint16_t _remotePort; + size_t _readLength = 0; - static constexpr size_t BUF_SIZE = 512; - uint8_t _buffer[BUF_SIZE]; + static constexpr size_t BUF_SIZE = 512; + uint8_t _buffer[BUF_SIZE]; }; -inline bool isMulticast(const IPAddress &ip) { return (ip[0] & 0xF0) == 0xE0; } +inline bool isMulticast(const IPAddress &ip) +{ + return (ip[0] & 0xF0) == 0xE0; +} #endif // HAS_ETHERNET diff --git a/src/platform/nrf52/BLEDfuScure.cpp b/src/platform/nrf52/BLEDfuScure.cpp index 79d2e7c53..82cb8905a 100644 --- a/src/platform/nrf52/BLEDfuScure.cpp +++ b/src/platform/nrf52/BLEDfuScure.cpp @@ -41,99 +41,103 @@ const uint16_t UUID16_SVC_DFU_OTA = 0xFE59; -const uint8_t UUID128_CHR_DFU_CONTROL[16] = {0x50, 0xEA, 0xDA, 0x30, 0x88, 0x83, 0xB8, 0x9F, 0x60, 0x4F, 0x15, 0xF3, 0x03, 0x00, 0xC9, 0x8E}; +const uint8_t UUID128_CHR_DFU_CONTROL[16] = {0x50, 0xEA, 0xDA, 0x30, 0x88, 0x83, 0xB8, 0x9F, + 0x60, 0x4F, 0x15, 0xF3, 0x03, 0x00, 0xC9, 0x8E}; extern "C" void bootloader_util_app_start(uint32_t start_addr); -static uint16_t crc16(const uint8_t *data_p, uint8_t length) { - uint16_t crc = 0xFFFF; +static uint16_t crc16(const uint8_t *data_p, uint8_t length) +{ + uint16_t crc = 0xFFFF; - while (length--) { - uint8_t x = crc >> 8 ^ *data_p++; - x ^= x >> 4; - crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x << 5)) ^ ((uint16_t)x); - } - return crc; + while (length--) { + uint8_t x = crc >> 8 ^ *data_p++; + x ^= x >> 4; + crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x << 5)) ^ ((uint16_t)x); + } + return crc; } -static void bledfu_control_wr_authorize_cb(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_write_t *request) { - if ((request->handle == chr->handles().value_handle) && (request->op != BLE_GATTS_OP_PREP_WRITE_REQ) && - (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_NOW) && (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL)) { - BLEConnection *conn = Bluefruit.Connection(conn_hdl); +static void bledfu_control_wr_authorize_cb(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_write_t *request) +{ + if ((request->handle == chr->handles().value_handle) && (request->op != BLE_GATTS_OP_PREP_WRITE_REQ) && + (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_NOW) && (request->op != BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL)) { + BLEConnection *conn = Bluefruit.Connection(conn_hdl); - ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_WRITE}; + ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_WRITE}; - if (!chr->indicateEnabled(conn_hdl)) { - reply.params.write.gatt_status = BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR; - sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); - return; - } - - reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; - sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); - - enum { START_DFU = 1 }; - if (request->data[0] == START_DFU) { - // Peer data information so that bootloader could re-connect after reboot - typedef struct { - ble_gap_addr_t addr; - ble_gap_irk_t irk; - ble_gap_enc_key_t enc_key; - uint8_t sys_attr[8]; - uint16_t crc16; - } peer_data_t; - - VERIFY_STATIC(offsetof(peer_data_t, crc16) == 60); - - /* Save Peer data - * Peer data address is defined in bootloader linker @0x20007F80 - * - If bonded : save Security information - * - Otherwise : save Address for direct advertising - * - * TODO may force bonded only for security reason - */ - peer_data_t *peer_data = (peer_data_t *)(0x20007F80UL); - varclr(peer_data); - - // Get CCCD - uint16_t sysattr_len = sizeof(peer_data->sys_attr); - sd_ble_gatts_sys_attr_get(conn_hdl, peer_data->sys_attr, &sysattr_len, BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS); - - // Get Bond Data or using Address if not bonded - peer_data->addr = conn->getPeerAddr(); - - if (conn->secured()) { - bond_keys_t bkeys; - if (conn->loadBondKey(&bkeys)) { - peer_data->addr = bkeys.peer_id.id_addr_info; - peer_data->irk = bkeys.peer_id.id_info; - peer_data->enc_key = bkeys.own_enc; + if (!chr->indicateEnabled(conn_hdl)) { + reply.params.write.gatt_status = BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR; + sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); + return; } - } - // Calculate crc - peer_data->crc16 = crc16((uint8_t *)peer_data, offsetof(peer_data_t, crc16)); + reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; + sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); - // Initiate DFU Sequence and reboot into DFU OTA mode - Bluefruit.Advertising.restartOnDisconnect(false); - conn->disconnect(); + enum { START_DFU = 1 }; + if (request->data[0] == START_DFU) { + // Peer data information so that bootloader could re-connect after reboot + typedef struct { + ble_gap_addr_t addr; + ble_gap_irk_t irk; + ble_gap_enc_key_t enc_key; + uint8_t sys_attr[8]; + uint16_t crc16; + } peer_data_t; - NRF_POWER->GPREGRET = 0xB1; - NVIC_SystemReset(); + VERIFY_STATIC(offsetof(peer_data_t, crc16) == 60); + + /* Save Peer data + * Peer data address is defined in bootloader linker @0x20007F80 + * - If bonded : save Security information + * - Otherwise : save Address for direct advertising + * + * TODO may force bonded only for security reason + */ + peer_data_t *peer_data = (peer_data_t *)(0x20007F80UL); + varclr(peer_data); + + // Get CCCD + uint16_t sysattr_len = sizeof(peer_data->sys_attr); + sd_ble_gatts_sys_attr_get(conn_hdl, peer_data->sys_attr, &sysattr_len, BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS); + + // Get Bond Data or using Address if not bonded + peer_data->addr = conn->getPeerAddr(); + + if (conn->secured()) { + bond_keys_t bkeys; + if (conn->loadBondKey(&bkeys)) { + peer_data->addr = bkeys.peer_id.id_addr_info; + peer_data->irk = bkeys.peer_id.id_info; + peer_data->enc_key = bkeys.own_enc; + } + } + + // Calculate crc + peer_data->crc16 = crc16((uint8_t *)peer_data, offsetof(peer_data_t, crc16)); + + // Initiate DFU Sequence and reboot into DFU OTA mode + Bluefruit.Advertising.restartOnDisconnect(false); + conn->disconnect(); + + NRF_POWER->GPREGRET = 0xB1; + NVIC_SystemReset(); + } } - } } BLEDfuSecure::BLEDfuSecure(void) : BLEService(UUID16_SVC_DFU_OTA), _chr_control(UUID128_CHR_DFU_CONTROL) {} -err_t BLEDfuSecure::begin(void) { - // Invoke base class begin() - VERIFY_STATUS(BLEService::begin()); +err_t BLEDfuSecure::begin(void) +{ + // Invoke base class begin() + VERIFY_STATUS(BLEService::begin()); - _chr_control.setProperties(CHR_PROPS_WRITE | CHR_PROPS_INDICATE); - _chr_control.setMaxLen(23); - _chr_control.setWriteAuthorizeCallback(bledfu_control_wr_authorize_cb); - VERIFY_STATUS(_chr_control.begin()); + _chr_control.setProperties(CHR_PROPS_WRITE | CHR_PROPS_INDICATE); + _chr_control.setMaxLen(23); + _chr_control.setWriteAuthorizeCallback(bledfu_control_wr_authorize_cb); + VERIFY_STATUS(_chr_control.begin()); - return ERROR_NONE; + return ERROR_NONE; } diff --git a/src/platform/nrf52/BLEDfuSecure.h b/src/platform/nrf52/BLEDfuSecure.h index 7fea607a3..bd5d910e8 100644 --- a/src/platform/nrf52/BLEDfuSecure.h +++ b/src/platform/nrf52/BLEDfuSecure.h @@ -41,14 +41,15 @@ #include "BLECharacteristic.h" #include "BLEService.h" -class BLEDfuSecure : public BLEService { -protected: - BLECharacteristic _chr_control; +class BLEDfuSecure : public BLEService +{ + protected: + BLECharacteristic _chr_control; -public: - BLEDfuSecure(void); + public: + BLEDfuSecure(void); - virtual err_t begin(void); + virtual err_t begin(void); }; #endif /* BLEDFUSECURE_H_ */ diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 797d40f57..4f7fb4776 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -19,12 +19,12 @@ static BLEBas blebas; // BAS (Battery Service) helper class instance #ifndef BLE_DFU_SECURE static BLEDfu bledfu; // DFU software update helper service #else -static BLEDfuSecure bledfusecure; // DFU software update helper service +static BLEDfuSecure bledfusecure; // DFU software update helper service #endif -// This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be -// in process at once static uint8_t trBytes[_max(_max(_max(_max(ToRadio_size, RadioConfig_size), User_size), -// MyNodeInfo_size), FromRadio_size)]; +// This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be in +// process at once +// static uint8_t trBytes[_max(_max(_max(_max(ToRadio_size, RadioConfig_size), User_size), MyNodeInfo_size), FromRadio_size)]; static uint8_t fromRadioBytes[meshtastic_FromRadio_size]; static uint8_t toRadioBytes[meshtastic_ToRadio_size]; @@ -33,372 +33,403 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; static uint16_t connectionHandle; -class BluetoothPhoneAPI : public PhoneAPI { - /** - * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) - */ - virtual void onNowHasData(uint32_t fromRadioNum) override { - PhoneAPI::onNowHasData(fromRadioNum); +class BluetoothPhoneAPI : public PhoneAPI +{ + /** + * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) + */ + virtual void onNowHasData(uint32_t fromRadioNum) override + { + PhoneAPI::onNowHasData(fromRadioNum); - LOG_INFO("BLE notify fromNum"); - fromNum.notify32(fromRadioNum); - } + LOG_INFO("BLE notify fromNum"); + fromNum.notify32(fromRadioNum); + } - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return Bluefruit.connected(connectionHandle); } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return Bluefruit.connected(connectionHandle); } -public: - BluetoothPhoneAPI() { api_type = TYPE_BLE; } + public: + BluetoothPhoneAPI() { api_type = TYPE_BLE; } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; -void onConnect(uint16_t conn_handle) { - // Get the reference to current connection - BLEConnection *connection = Bluefruit.Connection(conn_handle); - connectionHandle = conn_handle; - char central_name[32] = {0}; - connection->getPeerName(central_name, sizeof(central_name)); - LOG_INFO("BLE Connected to %s", central_name); +void onConnect(uint16_t conn_handle) +{ + // Get the reference to current connection + BLEConnection *connection = Bluefruit.Connection(conn_handle); + connectionHandle = conn_handle; + char central_name[32] = {0}; + connection->getPeerName(central_name, sizeof(central_name)); + LOG_INFO("BLE Connected to %s", central_name); - // Notify UI (or any other interested firmware components) - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newStatus); + // Notify UI (or any other interested firmware components) + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped * @param conn_handle connection where this event happens * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ -void onDisconnect(uint16_t conn_handle, uint8_t reason) { - LOG_INFO("BLE Disconnected, reason = 0x%x", reason); - if (bluetoothPhoneAPI) { - bluetoothPhoneAPI->close(); - } - - // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection - memset(lastToRadio, 0, sizeof(lastToRadio)); - - // Notify UI (or any other interested firmware components) - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newStatus); -} -void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { - // Display the raw request packet - LOG_INFO("CCCD Updated: %u", cccd_value); - // Check the characteristic this CCCD update is associated with in case - // this handler is used for multiple CCCD records. - - // According to the GATT spec: cccd value = 0x0001 means notifications are enabled - // and cccd value = 0x0002 means indications are enabled - - if (chr->uuid == fromNum.uuid || chr->uuid == logRadio.uuid) { - auto result = cccd_value == 2 ? chr->indicateEnabled(conn_hdl) : chr->notifyEnabled(conn_hdl); - if (result) { - LOG_INFO("Notify/Indicate enabled"); - } else { - LOG_INFO("Notify/Indicate disabled"); +void onDisconnect(uint16_t conn_handle, uint8_t reason) +{ + LOG_INFO("BLE Disconnected, reason = 0x%x", reason); + if (bluetoothPhoneAPI) { + bluetoothPhoneAPI->close(); } - } + + // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection + memset(lastToRadio, 0, sizeof(lastToRadio)); + + // Notify UI (or any other interested firmware components) + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); } -void startAdv(void) { - // Advertising packet - Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); - // IncludeService UUID - // Bluefruit.ScanResponse.addService(meshBleService); - Bluefruit.ScanResponse.addTxPower(); - Bluefruit.ScanResponse.addName(); - // Include Name - // Bluefruit.Advertising.addName(); - Bluefruit.Advertising.addService(meshBleService); - /* Start Advertising - * - Enable auto advertising if disconnected - * - Interval: fast mode = 20 ms, slow mode = 152.5 ms - * - Timeout for fast mode is 30 seconds - * - Start(timeout) with timeout = 0 will advertise forever (until connected) - * - * For recommended advertising interval - * https://developer.apple.com/library/content/qa/qa1931/_index.html - */ - Bluefruit.Advertising.restartOnDisconnect(true); - Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms - Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode - Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds. FIXME, we should stop advertising after X +void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) +{ + // Display the raw request packet + LOG_INFO("CCCD Updated: %u", cccd_value); + // Check the characteristic this CCCD update is associated with in case + // this handler is used for multiple CCCD records. + + // According to the GATT spec: cccd value = 0x0001 means notifications are enabled + // and cccd value = 0x0002 means indications are enabled + + if (chr->uuid == fromNum.uuid || chr->uuid == logRadio.uuid) { + auto result = cccd_value == 2 ? chr->indicateEnabled(conn_hdl) : chr->notifyEnabled(conn_hdl); + if (result) { + LOG_INFO("Notify/Indicate enabled"); + } else { + LOG_INFO("Notify/Indicate disabled"); + } + } +} +void startAdv(void) +{ + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + // IncludeService UUID + // Bluefruit.ScanResponse.addService(meshBleService); + Bluefruit.ScanResponse.addTxPower(); + Bluefruit.ScanResponse.addName(); + // Include Name + // Bluefruit.Advertising.addName(); + Bluefruit.Advertising.addService(meshBleService); + /* Start Advertising + * - Enable auto advertising if disconnected + * - Interval: fast mode = 20 ms, slow mode = 152.5 ms + * - Timeout for fast mode is 30 seconds + * - Start(timeout) with timeout = 0 will advertise forever (until connected) + * + * For recommended advertising interval + * https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds. FIXME, we should stop advertising after X } // Just ack that the caller is allowed to read -static void authorizeRead(uint16_t conn_hdl) { - ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; - reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; - sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); +static void authorizeRead(uint16_t conn_hdl) +{ + ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; + reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; + sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply); } /** * client is starting read, pull the bytes from our API class */ -void onFromRadioAuthorize(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_read_t *request) { - if (request->offset == 0) { - // If the read is long, we will get multiple authorize invocations - we only populate data on the first - size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); - // Someone is going to read our value as soon as this callback returns. So fill it with the next message in the - // queue or make empty if the queue is empty - fromRadio.write(fromRadioBytes, numBytes); - } else { - // LOG_INFO("Ignore successor read"); - } - authorizeRead(conn_hdl); +void onFromRadioAuthorize(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_read_t *request) +{ + if (request->offset == 0) { + // If the read is long, we will get multiple authorize invocations - we only populate data on the first + size_t numBytes = bluetoothPhoneAPI->getFromRadio(fromRadioBytes); + // Someone is going to read our value as soon as this callback returns. So fill it with the next message in the queue + // or make empty if the queue is empty + fromRadio.write(fromRadioBytes, numBytes); + } else { + // LOG_INFO("Ignore successor read"); + } + authorizeRead(conn_hdl); } -void onToRadioWrite(uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) { - LOG_INFO("toRadioWriteCb data %p, len %u", data, len); - if (memcmp(lastToRadio, data, len) != 0) { - LOG_DEBUG("New ToRadio packet"); - memcpy(lastToRadio, data, len); - bluetoothPhoneAPI->handleToRadio(data, len); - } else { - LOG_DEBUG("Drop dup ToRadio packet we just saw"); - } +void onToRadioWrite(uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) +{ + LOG_INFO("toRadioWriteCb data %p, len %u", data, len); + if (memcmp(lastToRadio, data, len) != 0) { + LOG_DEBUG("New ToRadio packet"); + memcpy(lastToRadio, data, len); + bluetoothPhoneAPI->handleToRadio(data, len); + } else { + LOG_DEBUG("Drop dup ToRadio packet we just saw"); + } } -void setupMeshService(void) { - bluetoothPhoneAPI = new BluetoothPhoneAPI(); - meshBleService.begin(); - // Note: You must call .begin() on the BLEService before calling .begin() on - // any characteristic(s) within that service definition.. Calling .begin() on - // a BLECharacteristic will cause it to be added to the last BLEService that - // was 'begin()'ed! - auto secMode = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN ? SECMODE_OPEN : SECMODE_ENC_NO_MITM; - fromNum.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); - fromNum.setPermission(secMode, SECMODE_NO_ACCESS); // FIXME, secure this!!! - fromNum.setFixedLen(0); // Variable len (either 0 or 4) FIXME consider changing protocol so it is fixed 4 byte len, - // where 0 means empty - fromNum.setMaxLen(4); - fromNum.setCccdWriteCallback(onCccd); // Optionally capture CCCD updates - // We don't yet need to hook the fromNum auth callback - // fromNum.setReadAuthorizeCallback(fromNumAuthorizeCb); - fromNum.write32(0); // Provide default fromNum of 0 - fromNum.begin(); +void setupMeshService(void) +{ + bluetoothPhoneAPI = new BluetoothPhoneAPI(); + meshBleService.begin(); + // Note: You must call .begin() on the BLEService before calling .begin() on + // any characteristic(s) within that service definition.. Calling .begin() on + // a BLECharacteristic will cause it to be added to the last BLEService that + // was 'begin()'ed! + auto secMode = + config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN ? SECMODE_OPEN : SECMODE_ENC_NO_MITM; + fromNum.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); + fromNum.setPermission(secMode, SECMODE_NO_ACCESS); // FIXME, secure this!!! + fromNum.setFixedLen( + 0); // Variable len (either 0 or 4) FIXME consider changing protocol so it is fixed 4 byte len, where 0 means empty + fromNum.setMaxLen(4); + fromNum.setCccdWriteCallback(onCccd); // Optionally capture CCCD updates + // We don't yet need to hook the fromNum auth callback + // fromNum.setReadAuthorizeCallback(fromNumAuthorizeCb); + fromNum.write32(0); // Provide default fromNum of 0 + fromNum.begin(); - fromRadio.setProperties(CHR_PROPS_READ); - fromRadio.setPermission(secMode, SECMODE_NO_ACCESS); - fromRadio.setMaxLen(sizeof(fromRadioBytes)); - fromRadio.setReadAuthorizeCallback(onFromRadioAuthorize, - false); // We don't call this callback via the adafruit queue, because we can safely run in the BLE context - fromRadio.setBuffer(fromRadioBytes, - sizeof(fromRadioBytes)); // we preallocate our fromradio buffer so we won't waste space - // for two copies - fromRadio.begin(); + fromRadio.setProperties(CHR_PROPS_READ); + fromRadio.setPermission(secMode, SECMODE_NO_ACCESS); + fromRadio.setMaxLen(sizeof(fromRadioBytes)); + fromRadio.setReadAuthorizeCallback( + onFromRadioAuthorize, + false); // We don't call this callback via the adafruit queue, because we can safely run in the BLE context + fromRadio.setBuffer(fromRadioBytes, sizeof(fromRadioBytes)); // we preallocate our fromradio buffer so we won't waste space + // for two copies + fromRadio.begin(); - toRadio.setProperties(CHR_PROPS_WRITE); - toRadio.setPermission(secMode, secMode); // FIXME secure this! - toRadio.setFixedLen(0); - toRadio.setMaxLen(512); - toRadio.setBuffer(toRadioBytes, sizeof(toRadioBytes)); - // We don't call this callback via the adafruit queue, because we can safely run in the BLE context - toRadio.setWriteCallback(onToRadioWrite, false); - toRadio.begin(); + toRadio.setProperties(CHR_PROPS_WRITE); + toRadio.setPermission(secMode, secMode); // FIXME secure this! + toRadio.setFixedLen(0); + toRadio.setMaxLen(512); + toRadio.setBuffer(toRadioBytes, sizeof(toRadioBytes)); + // We don't call this callback via the adafruit queue, because we can safely run in the BLE context + toRadio.setWriteCallback(onToRadioWrite, false); + toRadio.begin(); - logRadio.setProperties(CHR_PROPS_INDICATE | CHR_PROPS_NOTIFY | CHR_PROPS_READ); - logRadio.setPermission(secMode, SECMODE_NO_ACCESS); - logRadio.setMaxLen(512); - logRadio.setCccdWriteCallback(onCccd); - logRadio.write32(0); - logRadio.begin(); + logRadio.setProperties(CHR_PROPS_INDICATE | CHR_PROPS_NOTIFY | CHR_PROPS_READ); + logRadio.setPermission(secMode, SECMODE_NO_ACCESS); + logRadio.setMaxLen(512); + logRadio.setCccdWriteCallback(onCccd); + logRadio.write32(0); + logRadio.begin(); } static uint32_t configuredPasskey; -void NRF52Bluetooth::shutdown() { - // Shutdown bluetooth for minimum power draw - LOG_INFO("Disable NRF52 bluetooth"); - Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset) - disconnect(); - Bluefruit.Advertising.stop(); +void NRF52Bluetooth::shutdown() +{ + // Shutdown bluetooth for minimum power draw + LOG_INFO("Disable NRF52 bluetooth"); + Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset) + disconnect(); + Bluefruit.Advertising.stop(); } -void NRF52Bluetooth::startDisabled() { - // Setup Bluetooth - nrf52Bluetooth->setup(); - // Shutdown bluetooth for minimum power draw - Bluefruit.Advertising.stop(); - Bluefruit.setTxPower(-40); // Minimum power - LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)"); +void NRF52Bluetooth::startDisabled() +{ + // Setup Bluetooth + nrf52Bluetooth->setup(); + // Shutdown bluetooth for minimum power draw + Bluefruit.Advertising.stop(); + Bluefruit.setTxPower(-40); // Minimum power + LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)"); } -bool NRF52Bluetooth::isConnected() { return Bluefruit.connected(connectionHandle); } -int NRF52Bluetooth::getRssi() { - return 0; // FIXME figure out where to source this +bool NRF52Bluetooth::isConnected() +{ + return Bluefruit.connected(connectionHandle); } -void NRF52Bluetooth::setup() { - // Initialise the Bluefruit module - LOG_INFO("Init the Bluefruit nRF52 module"); - Bluefruit.autoConnLed(false); - Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); - Bluefruit.begin(); - // Clear existing data. - Bluefruit.Advertising.stop(); - Bluefruit.Advertising.clearData(); - Bluefruit.ScanResponse.clearData(); - if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - configuredPasskey = - config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN ? config.bluetooth.fixed_pin : random(100000, 999999); - auto pinString = std::to_string(configuredPasskey); - LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); - Bluefruit.Security.setPIN(pinString.c_str()); - Bluefruit.Security.setIOCaps(true, false, false); - Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); - Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); - Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); - meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - } else { - Bluefruit.Security.setIOCaps(false, false, false); - meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); - } - // Set the advertised device name (keep it short!) - Bluefruit.setName(getDeviceName()); - // Set the connect/disconnect callback handlers - Bluefruit.Periph.setConnectCallback(onConnect); - Bluefruit.Periph.setDisconnectCallback(onDisconnect); +int NRF52Bluetooth::getRssi() +{ + return 0; // FIXME figure out where to source this +} +void NRF52Bluetooth::setup() +{ + // Initialise the Bluefruit module + LOG_INFO("Init the Bluefruit nRF52 module"); + Bluefruit.autoConnLed(false); + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.begin(); + // Clear existing data. + Bluefruit.Advertising.stop(); + Bluefruit.Advertising.clearData(); + Bluefruit.ScanResponse.clearData(); + if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { + configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN + ? config.bluetooth.fixed_pin + : random(100000, 999999); + auto pinString = std::to_string(configuredPasskey); + LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); + Bluefruit.Security.setPIN(pinString.c_str()); + Bluefruit.Security.setIOCaps(true, false, false); + Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onPairingPasskey); + Bluefruit.Security.setPairCompleteCallback(NRF52Bluetooth::onPairingCompleted); + Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); + meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + } else { + Bluefruit.Security.setIOCaps(false, false, false); + meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); + } + // Set the advertised device name (keep it short!) + Bluefruit.setName(getDeviceName()); + // Set the connect/disconnect callback handlers + Bluefruit.Periph.setConnectCallback(onConnect); + Bluefruit.Periph.setDisconnectCallback(onDisconnect); #ifndef BLE_DFU_SECURE - bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - bledfu.begin(); // Install the DFU helper + bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + bledfu.begin(); // Install the DFU helper #else - bledfusecure.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // add by WayenWeng - bledfusecure.begin(); // Install the DFU helper + bledfusecure.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // add by WayenWeng + bledfusecure.begin(); // Install the DFU helper #endif - // Configure and Start the Device Information Service - LOG_INFO("Init the Device Information Service"); - bledis.setModel(optstr(HW_VERSION)); - bledis.setFirmwareRev(optstr(APP_VERSION)); - bledis.begin(); - // Start the BLE Battery Service and set it to 100% - LOG_INFO("Init the Battery Service"); - blebas.begin(); - blebas.write(0); // Unknown battery level for now - // Setup the Heart Rate Monitor service using - // BLEService and BLECharacteristic classes - LOG_INFO("Init the Mesh bluetooth service"); - setupMeshService(); - // Setup the advertising packet(s) - LOG_INFO("Set up the advertising payload(s)"); - startAdv(); - LOG_INFO("Advertise"); + // Configure and Start the Device Information Service + LOG_INFO("Init the Device Information Service"); + bledis.setModel(optstr(HW_VERSION)); + bledis.setFirmwareRev(optstr(APP_VERSION)); + bledis.begin(); + // Start the BLE Battery Service and set it to 100% + LOG_INFO("Init the Battery Service"); + blebas.begin(); + blebas.write(0); // Unknown battery level for now + // Setup the Heart Rate Monitor service using + // BLEService and BLECharacteristic classes + LOG_INFO("Init the Mesh bluetooth service"); + setupMeshService(); + // Setup the advertising packet(s) + LOG_INFO("Set up the advertising payload(s)"); + startAdv(); + LOG_INFO("Advertise"); } -void NRF52Bluetooth::resumeAdvertising() { - Bluefruit.Advertising.restartOnDisconnect(true); - Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms - Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode - Bluefruit.Advertising.start(0); +void NRF52Bluetooth::resumeAdvertising() +{ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); } /// Given a level between 0-100, update the BLE attribute -void updateBatteryLevel(uint8_t level) { blebas.write(level); } -void NRF52Bluetooth::clearBonds() { - LOG_INFO("Clear bluetooth bonds!"); - bond_print_list(BLE_GAP_ROLE_PERIPH); - bond_print_list(BLE_GAP_ROLE_CENTRAL); - Bluefruit.Periph.clearBonds(); - Bluefruit.Central.clearBonds(); +void updateBatteryLevel(uint8_t level) +{ + blebas.write(level); } -void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) { LOG_INFO("BLE connection secured"); } -bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { - char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; - char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; - LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); - powerFSM.trigger(EVENT_BLUETOOTH_PAIR); +void NRF52Bluetooth::clearBonds() +{ + LOG_INFO("Clear bluetooth bonds!"); + bond_print_list(BLE_GAP_ROLE_PERIPH); + bond_print_list(BLE_GAP_ROLE_CENTRAL); + Bluefruit.Periph.clearBonds(); + Bluefruit.Central.clearBonds(); +} +void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) +{ + LOG_INFO("BLE connection secured"); +} +bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) +{ + char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; + char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; + LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); + powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - // Get passkey as string - // Note: possible leading zeros - std::string textkey; - for (uint8_t i = 0; i < 6; i++) - textkey += (char)passkey[i]; + // Get passkey as string + // Note: possible leading zeros + std::string textkey; + for (uint8_t i = 0; i < 6; i++) + textkey += (char)passkey[i]; - // Notify UI (or other components) of pairing event and passkey - meshtastic::BluetoothStatus newStatus(textkey); - bluetoothStatus->updateStatus(&newStatus); + // Notify UI (or other components) of pairing event and passkey + meshtastic::BluetoothStatus newStatus(textkey); + bluetoothStatus->updateStatus(&newStatus); -#if HAS_SCREEN && !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and - // observe bluetoothStatus - if (screen) { - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 12; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); +#if HAS_SCREEN && \ + !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + if (screen) { + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); - } -#endif - if (match_request) { - uint32_t start_time = millis(); - while (millis() < start_time + 30000) { - if (!Bluefruit.connected(conn_handle)) - break; + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); } - } - LOG_INFO("BLE passkey pair: match_request=%i", match_request); - return true; +#endif + if (match_request) { + uint32_t start_time = millis(); + while (millis() < start_time + 30000) { + if (!Bluefruit.connected(conn_handle)) + break; + } + } + LOG_INFO("BLE passkey pair: match_request=%i", match_request); + return true; } // Actively refuse new BLE pairings -// After clearing bonds (at factory reset), clients seem initially able to attempt to re-pair, even with advertising -// disabled. On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any -// connection attempts. -bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { - NRF52Bluetooth::disconnect(); - return false; +// After clearing bonds (at factory reset), clients seem initially able to attempt to re-pair, even with advertising disabled. +// On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any connection attempts. +bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) +{ + NRF52Bluetooth::disconnect(); + return false; } // Disconnect any BLE connections -void NRF52Bluetooth::disconnect() { - uint8_t connection_num = Bluefruit.connected(); - if (connection_num) { - // Close all connections. We're only expecting one. - for (uint8_t i = 0; i < connection_num; i++) - Bluefruit.disconnect(i); +void NRF52Bluetooth::disconnect() +{ + uint8_t connection_num = Bluefruit.connected(); + if (connection_num) { + // Close all connections. We're only expecting one. + for (uint8_t i = 0; i < connection_num; i++) + Bluefruit.disconnect(i); - // Wait for disconnection - while (Bluefruit.connected()) - yield(); + // Wait for disconnection + while (Bluefruit.connected()) + yield(); - LOG_INFO("Ended BLE connection"); - } + LOG_INFO("Ended BLE connection"); + } } -void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) { - if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { - LOG_INFO("BLE pair success"); - meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newConnectedStatus); - } else { - LOG_INFO("BLE pair failed"); - // Notify UI (or any other interested firmware components) - meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newDisconnectedStatus); - } +void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) +{ + if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { + LOG_INFO("BLE pair success"); + meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newConnectedStatus); + } else { + LOG_INFO("BLE pair failed"); + // Notify UI (or any other interested firmware components) + meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newDisconnectedStatus); + } - // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (screen) { - screen->endAlert(); - } + // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + if (screen) { + screen->endAlert(); + } } -void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) { - if (!isConnected() || length > 512) - return; - if (logRadio.indicateEnabled()) - logRadio.indicate(logMessage, (uint16_t)length); - else - logRadio.notify(logMessage, (uint16_t)length); +void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) +{ + if (!isConnected() || length > 512) + return; + if (logRadio.indicateEnabled()) + logRadio.indicate(logMessage, (uint16_t)length); + else + logRadio.notify(logMessage, (uint16_t)length); } \ No newline at end of file diff --git a/src/platform/nrf52/NRF52Bluetooth.h b/src/platform/nrf52/NRF52Bluetooth.h index 8896729c9..630ab05bc 100644 --- a/src/platform/nrf52/NRF52Bluetooth.h +++ b/src/platform/nrf52/NRF52Bluetooth.h @@ -3,22 +3,23 @@ #include "BluetoothCommon.h" #include -class NRF52Bluetooth : BluetoothApi { -public: - void setup(); - void shutdown(); - void startDisabled(); - void resumeAdvertising(); - void clearBonds(); - bool isConnected(); - int getRssi(); - void sendLog(const uint8_t *logMessage, size_t length); +class NRF52Bluetooth : BluetoothApi +{ + public: + void setup(); + void shutdown(); + void startDisabled(); + void resumeAdvertising(); + void clearBonds(); + bool isConnected(); + int getRssi(); + void sendLog(const uint8_t *logMessage, size_t length); -private: - static void onConnectionSecured(uint16_t conn_handle); - static bool onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); - static void onPairingCompleted(uint16_t conn_handle, uint8_t auth_status); + private: + static void onConnectionSecured(uint16_t conn_handle); + static bool onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); + static void onPairingCompleted(uint16_t conn_handle, uint8_t auth_status); - static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); - static void disconnect(); + static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); + static void disconnect(); }; \ No newline at end of file diff --git a/src/platform/nrf52/NRF52CryptoEngine.cpp b/src/platform/nrf52/NRF52CryptoEngine.cpp index 2788d3f66..5de13c58b 100644 --- a/src/platform/nrf52/NRF52CryptoEngine.cpp +++ b/src/platform/nrf52/NRF52CryptoEngine.cpp @@ -2,29 +2,31 @@ #include "aes-256/tiny-aes.h" #include "configuration.h" #include -class NRF52CryptoEngine : public CryptoEngine { -public: - NRF52CryptoEngine() {} +class NRF52CryptoEngine : public CryptoEngine +{ + public: + NRF52CryptoEngine() {} - ~NRF52CryptoEngine() {} + ~NRF52CryptoEngine() {} - virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override { - if (_key.length > 16) { - AES_ctx ctx; - AES_init_ctx_iv(&ctx, _key.bytes, _nonce); - AES_CTR_xcrypt_buffer(&ctx, bytes, numBytes); - } else if (_key.length > 0) { - nRFCrypto.begin(); - nRFCrypto_AES ctx; - uint8_t myLen = ctx.blockLen(numBytes); - char encBuf[myLen] = {0}; - ctx.begin(); - ctx.Process((char *)bytes, numBytes, _nonce, _key.bytes, _key.length, encBuf, ctx.encryptFlag, ctx.ctrMode); - ctx.end(); - nRFCrypto.end(); - memcpy(bytes, encBuf, numBytes); + virtual void encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) override + { + if (_key.length > 16) { + AES_ctx ctx; + AES_init_ctx_iv(&ctx, _key.bytes, _nonce); + AES_CTR_xcrypt_buffer(&ctx, bytes, numBytes); + } else if (_key.length > 0) { + nRFCrypto.begin(); + nRFCrypto_AES ctx; + uint8_t myLen = ctx.blockLen(numBytes); + char encBuf[myLen] = {0}; + ctx.begin(); + ctx.Process((char *)bytes, numBytes, _nonce, _key.bytes, _key.length, encBuf, ctx.encryptFlag, ctx.ctrMode); + ctx.end(); + nRFCrypto.end(); + memcpy(bytes, encBuf, numBytes); + } } - } }; CryptoEngine *crypto = new NRF52CryptoEngine(); \ No newline at end of file diff --git a/src/platform/nrf52/aes-256/tiny-aes.cpp b/src/platform/nrf52/aes-256/tiny-aes.cpp index bc3e7c6a7..20a7344e1 100644 --- a/src/platform/nrf52/aes-256/tiny-aes.cpp +++ b/src/platform/nrf52/aes-256/tiny-aes.cpp @@ -19,179 +19,198 @@ typedef uint8_t state_t[4][4]; static const uint8_t sbox[256] = { // 0 1 2 3 4 5 6 7 8 9 A B C D E F - 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, - 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, - 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, - 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, - 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, - 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, - 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, - 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, - 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, - 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, + 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, + 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, + 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, + 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, + 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, + 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, + 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, + 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, + 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16}; static const uint8_t Rcon[11] = {0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36}; #define getSBoxValue(num) (sbox[(num)]) -static void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key) { - uint8_t tempa[4]; +static void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key) +{ + uint8_t tempa[4]; - for (unsigned i = 0; i < Nk; ++i) { - RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; - RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; - RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; - RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; - } - - for (unsigned i = Nk; i < Nb * (Nr + 1); ++i) { - unsigned k = (i - 1) * 4; - tempa[0] = RoundKey[k + 0]; - tempa[1] = RoundKey[k + 1]; - tempa[2] = RoundKey[k + 2]; - tempa[3] = RoundKey[k + 3]; - - if (i % Nk == 0) { - const uint8_t u8tmp = tempa[0]; - tempa[0] = tempa[1]; - tempa[1] = tempa[2]; - tempa[2] = tempa[3]; - tempa[3] = u8tmp; - - tempa[0] = getSBoxValue(tempa[0]); - tempa[1] = getSBoxValue(tempa[1]); - tempa[2] = getSBoxValue(tempa[2]); - tempa[3] = getSBoxValue(tempa[3]); - - tempa[0] = tempa[0] ^ Rcon[i / Nk]; + for (unsigned i = 0; i < Nk; ++i) { + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; } - if (i % Nk == 4) { - tempa[0] = getSBoxValue(tempa[0]); - tempa[1] = getSBoxValue(tempa[1]); - tempa[2] = getSBoxValue(tempa[2]); - tempa[3] = getSBoxValue(tempa[3]); - } + for (unsigned i = Nk; i < Nb * (Nr + 1); ++i) { + unsigned k = (i - 1) * 4; + tempa[0] = RoundKey[k + 0]; + tempa[1] = RoundKey[k + 1]; + tempa[2] = RoundKey[k + 2]; + tempa[3] = RoundKey[k + 3]; - unsigned j = i * 4; - k = (i - Nk) * 4; - RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; - RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; - RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; - RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; - } -} + if (i % Nk == 0) { + const uint8_t u8tmp = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = u8tmp; -void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key) { KeyExpansion(ctx->RoundKey, key); } -void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8_t *iv) { - KeyExpansion(ctx->RoundKey, key); - memcpy(ctx->Iv, iv, AES_BLOCKLEN); -} -void AES_ctx_set_iv(struct AES_ctx *ctx, const uint8_t *iv) { memcpy(ctx->Iv, iv, AES_BLOCKLEN); } + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); -static void AddRoundKey(uint8_t round, state_t *state, const uint8_t *RoundKey) { - for (uint8_t i = 0; i < 4; ++i) { - for (uint8_t j = 0; j < 4; ++j) { - (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; - } - } -} - -static void SubBytes(state_t *state) { - for (uint8_t i = 0; i < 4; ++i) { - for (uint8_t j = 0; j < 4; ++j) { - (*state)[j][i] = getSBoxValue((*state)[j][i]); - } - } -} - -static void ShiftRows(state_t *state) { - uint8_t temp = (*state)[0][1]; - (*state)[0][1] = (*state)[1][1]; - (*state)[1][1] = (*state)[2][1]; - (*state)[2][1] = (*state)[3][1]; - (*state)[3][1] = temp; - - temp = (*state)[0][2]; - (*state)[0][2] = (*state)[2][2]; - (*state)[2][2] = temp; - - temp = (*state)[1][2]; - (*state)[1][2] = (*state)[3][2]; - (*state)[3][2] = temp; - - temp = (*state)[0][3]; - (*state)[0][3] = (*state)[3][3]; - (*state)[3][3] = (*state)[2][3]; - (*state)[2][3] = (*state)[1][3]; - (*state)[1][3] = temp; -} - -static uint8_t xtime(uint8_t x) { return ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); } - -static void MixColumns(state_t *state) { - for (uint8_t i = 0; i < 4; ++i) { - uint8_t t = (*state)[i][0]; - uint8_t Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3]; - uint8_t Tm = (*state)[i][0] ^ (*state)[i][1]; - Tm = xtime(Tm); - (*state)[i][0] ^= Tm ^ Tmp; - Tm = (*state)[i][1] ^ (*state)[i][2]; - Tm = xtime(Tm); - (*state)[i][1] ^= Tm ^ Tmp; - Tm = (*state)[i][2] ^ (*state)[i][3]; - Tm = xtime(Tm); - (*state)[i][2] ^= Tm ^ Tmp; - Tm = (*state)[i][3] ^ t; - Tm = xtime(Tm); - (*state)[i][3] ^= Tm ^ Tmp; - } -} - -#define Multiply(x, y) \ - (((y & 1) * x) ^ ((y >> 1 & 1) * xtime(x)) ^ ((y >> 2 & 1) * xtime(xtime(x))) ^ ((y >> 3 & 1) * xtime(xtime(xtime(x)))) ^ \ - ((y >> 4 & 1) * xtime(xtime(xtime(xtime(x)))))) - -static void Cipher(state_t *state, const uint8_t *RoundKey) { - uint8_t round = 0; - - AddRoundKey(0, state, RoundKey); - - for (round = 1;; ++round) { - SubBytes(state); - ShiftRows(state); - if (round == Nr) { - break; - } - MixColumns(state); - AddRoundKey(round, state, RoundKey); - } - AddRoundKey(Nr, state, RoundKey); -} - -void AES_CTR_xcrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length) { - uint8_t buffer[AES_BLOCKLEN]; - - size_t i; - int bi; - for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) { - if (bi == AES_BLOCKLEN) { - - memcpy(buffer, ctx->Iv, AES_BLOCKLEN); - Cipher((state_t *)buffer, ctx->RoundKey); - - for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) { - if (ctx->Iv[bi] == 255) { - ctx->Iv[bi] = 0; - continue; + tempa[0] = tempa[0] ^ Rcon[i / Nk]; } - ctx->Iv[bi] += 1; - break; - } - bi = 0; - } - buf[i] = (buf[i] ^ buffer[bi]); - } + if (i % Nk == 4) { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + + unsigned j = i * 4; + k = (i - Nk) * 4; + RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; + RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; + RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; + RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; + } +} + +void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key) +{ + KeyExpansion(ctx->RoundKey, key); +} +void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *key, const uint8_t *iv) +{ + KeyExpansion(ctx->RoundKey, key); + memcpy(ctx->Iv, iv, AES_BLOCKLEN); +} +void AES_ctx_set_iv(struct AES_ctx *ctx, const uint8_t *iv) +{ + memcpy(ctx->Iv, iv, AES_BLOCKLEN); +} + +static void AddRoundKey(uint8_t round, state_t *state, const uint8_t *RoundKey) +{ + for (uint8_t i = 0; i < 4; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; + } + } +} + +static void SubBytes(state_t *state) +{ + for (uint8_t i = 0; i < 4; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + (*state)[j][i] = getSBoxValue((*state)[j][i]); + } + } +} + +static void ShiftRows(state_t *state) +{ + uint8_t temp = (*state)[0][1]; + (*state)[0][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[3][1]; + (*state)[3][1] = temp; + + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[3][3]; + (*state)[3][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[1][3]; + (*state)[1][3] = temp; +} + +static uint8_t xtime(uint8_t x) +{ + return ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); +} + +static void MixColumns(state_t *state) +{ + for (uint8_t i = 0; i < 4; ++i) { + uint8_t t = (*state)[i][0]; + uint8_t Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3]; + uint8_t Tm = (*state)[i][0] ^ (*state)[i][1]; + Tm = xtime(Tm); + (*state)[i][0] ^= Tm ^ Tmp; + Tm = (*state)[i][1] ^ (*state)[i][2]; + Tm = xtime(Tm); + (*state)[i][1] ^= Tm ^ Tmp; + Tm = (*state)[i][2] ^ (*state)[i][3]; + Tm = xtime(Tm); + (*state)[i][2] ^= Tm ^ Tmp; + Tm = (*state)[i][3] ^ t; + Tm = xtime(Tm); + (*state)[i][3] ^= Tm ^ Tmp; + } +} + +#define Multiply(x, y) \ + (((y & 1) * x) ^ ((y >> 1 & 1) * xtime(x)) ^ ((y >> 2 & 1) * xtime(xtime(x))) ^ ((y >> 3 & 1) * xtime(xtime(xtime(x)))) ^ \ + ((y >> 4 & 1) * xtime(xtime(xtime(xtime(x)))))) + +static void Cipher(state_t *state, const uint8_t *RoundKey) +{ + uint8_t round = 0; + + AddRoundKey(0, state, RoundKey); + + for (round = 1;; ++round) { + SubBytes(state); + ShiftRows(state); + if (round == Nr) { + break; + } + MixColumns(state); + AddRoundKey(round, state, RoundKey); + } + AddRoundKey(Nr, state, RoundKey); +} + +void AES_CTR_xcrypt_buffer(struct AES_ctx *ctx, uint8_t *buf, size_t length) +{ + uint8_t buffer[AES_BLOCKLEN]; + + size_t i; + int bi; + for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) { + if (bi == AES_BLOCKLEN) { + + memcpy(buffer, ctx->Iv, AES_BLOCKLEN); + Cipher((state_t *)buffer, ctx->RoundKey); + + for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) { + if (ctx->Iv[bi] == 255) { + ctx->Iv[bi] = 0; + continue; + } + ctx->Iv[bi] += 1; + break; + } + bi = 0; + } + + buf[i] = (buf[i] ^ buffer[bi]); + } } diff --git a/src/platform/nrf52/aes-256/tiny-aes.h b/src/platform/nrf52/aes-256/tiny-aes.h index 080f8996b..ef78e0aeb 100644 --- a/src/platform/nrf52/aes-256/tiny-aes.h +++ b/src/platform/nrf52/aes-256/tiny-aes.h @@ -9,8 +9,8 @@ #define AES_keyExpSize 240 struct AES_ctx { - uint8_t RoundKey[AES_keyExpSize]; - uint8_t Iv[AES_BLOCKLEN]; + uint8_t RoundKey[AES_keyExpSize]; + uint8_t Iv[AES_BLOCKLEN]; }; void AES_init_ctx(struct AES_ctx *ctx, const uint8_t *key); diff --git a/src/platform/nrf52/alloc.cpp b/src/platform/nrf52/alloc.cpp index af4eb62ec..0a610bbe3 100644 --- a/src/platform/nrf52/alloc.cpp +++ b/src/platform/nrf52/alloc.cpp @@ -7,18 +7,26 @@ * Custom new/delete to panic if out out memory */ -void *operator new(size_t size) { - auto p = rtos_malloc(size); - assert(p); - return p; +void *operator new(size_t size) +{ + auto p = rtos_malloc(size); + assert(p); + return p; } -void *operator new[](size_t size) { - auto p = rtos_malloc(size); - assert(p); - return p; +void *operator new[](size_t size) +{ + auto p = rtos_malloc(size); + assert(p); + return p; } -void operator delete(void *ptr) { rtos_free(ptr); } +void operator delete(void *ptr) +{ + rtos_free(ptr); +} -void operator delete[](void *ptr) { rtos_free(ptr); } \ No newline at end of file +void operator delete[](void *ptr) +{ + rtos_free(ptr); +} \ No newline at end of file diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 7412182ab..d4699cd8c 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -155,8 +155,8 @@ // Debug printing to segger console #define SEGGER_MSG(...) SEGGER_RTT_printf(SEGGER_STDOUT_CH, __VA_ARGS__) -// If we are not on a NRF52840 (which has built in USB-ACM serial support) and we don't have serial pins hooked up, then -// we MUST use SEGGER for debug output +// If we are not on a NRF52840 (which has built in USB-ACM serial support) and we don't have serial pins hooked up, then we MUST +// use SEGGER for debug output #if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) // No serial ports on this board - ONLY use segger in memory console #define USE_SEGGER diff --git a/src/platform/nrf52/hardfault.cpp b/src/platform/nrf52/hardfault.cpp index e6eb2b97b..7b7a718b8 100644 --- a/src/platform/nrf52/hardfault.cpp +++ b/src/platform/nrf52/hardfault.cpp @@ -1,90 +1,94 @@ #include "configuration.h" #include -// Based on reading/modifying -// https://blog.feabhas.com/2013/02/developing-a-generic-hard-fault-handler-for-arm-cortex-m3cortex-m4/ +// Based on reading/modifying https://blog.feabhas.com/2013/02/developing-a-generic-hard-fault-handler-for-arm-cortex-m3cortex-m4/ enum { r0, r1, r2, r3, r12, lr, pc, psr }; -// we can't use the regular LOG_DEBUG for these crash dumps because it depends on threading still being running. Instead -// use the segger in memory tool +// we can't use the regular LOG_DEBUG for these crash dumps because it depends on threading still being running. Instead use the +// segger in memory tool #define FAULT_MSG(...) SEGGER_MSG(__VA_ARGS__) // Per http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/Cihcfefj.html -static void printUsageErrorMsg(uint32_t cfsr) { - FAULT_MSG("Usage fault: "); - cfsr >>= SCB_CFSR_USGFAULTSR_Pos; // right shift to lsb - if ((cfsr & (1 << 9)) != 0) - FAULT_MSG("Divide by zero\n"); - else if ((cfsr & (1 << 8)) != 0) - FAULT_MSG("Unaligned\n"); - else if ((cfsr & (1 << 1)) != 0) - FAULT_MSG("Invalid state\n"); - else if ((cfsr & (1 << 0)) != 0) - FAULT_MSG("Invalid instruction\n"); - else - FAULT_MSG("FIXME add to printUsageErrorMsg!\n"); +static void printUsageErrorMsg(uint32_t cfsr) +{ + FAULT_MSG("Usage fault: "); + cfsr >>= SCB_CFSR_USGFAULTSR_Pos; // right shift to lsb + if ((cfsr & (1 << 9)) != 0) + FAULT_MSG("Divide by zero\n"); + else if ((cfsr & (1 << 8)) != 0) + FAULT_MSG("Unaligned\n"); + else if ((cfsr & (1 << 1)) != 0) + FAULT_MSG("Invalid state\n"); + else if ((cfsr & (1 << 0)) != 0) + FAULT_MSG("Invalid instruction\n"); + else + FAULT_MSG("FIXME add to printUsageErrorMsg!\n"); } -static void printBusErrorMsg(uint32_t cfsr) { - FAULT_MSG("Bus fault: "); - cfsr >>= SCB_CFSR_BUSFAULTSR_Pos; // right shift to lsb - if ((cfsr & (1 << 0)) != 0) - FAULT_MSG("Instruction bus error\n"); - if ((cfsr & (1 << 1)) != 0) - FAULT_MSG("Precise data bus error\n"); - if ((cfsr & (1 << 2)) != 0) - FAULT_MSG("Imprecise data bus error\n"); +static void printBusErrorMsg(uint32_t cfsr) +{ + FAULT_MSG("Bus fault: "); + cfsr >>= SCB_CFSR_BUSFAULTSR_Pos; // right shift to lsb + if ((cfsr & (1 << 0)) != 0) + FAULT_MSG("Instruction bus error\n"); + if ((cfsr & (1 << 1)) != 0) + FAULT_MSG("Precise data bus error\n"); + if ((cfsr & (1 << 2)) != 0) + FAULT_MSG("Imprecise data bus error\n"); } -static void printMemErrorMsg(uint32_t cfsr) { - FAULT_MSG("Memory fault: "); - cfsr >>= SCB_CFSR_MEMFAULTSR_Pos; // right shift to lsb - if ((cfsr & (1 << 0)) != 0) - FAULT_MSG("Instruction access violation\n"); - if ((cfsr & (1 << 1)) != 0) - FAULT_MSG("Data access violation\n"); +static void printMemErrorMsg(uint32_t cfsr) +{ + FAULT_MSG("Memory fault: "); + cfsr >>= SCB_CFSR_MEMFAULTSR_Pos; // right shift to lsb + if ((cfsr & (1 << 0)) != 0) + FAULT_MSG("Instruction access violation\n"); + if ((cfsr & (1 << 1)) != 0) + FAULT_MSG("Data access violation\n"); } -extern "C" void HardFault_Impl(uint32_t stack[]) { - FAULT_MSG("Hard Fault occurred! SCB->HFSR = 0x%08lx\n", SCB->HFSR); +extern "C" void HardFault_Impl(uint32_t stack[]) +{ + FAULT_MSG("Hard Fault occurred! SCB->HFSR = 0x%08lx\n", SCB->HFSR); - if ((SCB->HFSR & SCB_HFSR_FORCED_Msk) != 0) { - FAULT_MSG("Forced Hard Fault: SCB->CFSR = 0x%08lx\n", SCB->CFSR); + if ((SCB->HFSR & SCB_HFSR_FORCED_Msk) != 0) { + FAULT_MSG("Forced Hard Fault: SCB->CFSR = 0x%08lx\n", SCB->CFSR); - if ((SCB->CFSR & SCB_CFSR_USGFAULTSR_Msk) != 0) { - printUsageErrorMsg(SCB->CFSR); - } - if ((SCB->CFSR & SCB_CFSR_BUSFAULTSR_Msk) != 0) { - printBusErrorMsg(SCB->CFSR); - } - if ((SCB->CFSR & SCB_CFSR_MEMFAULTSR_Msk) != 0) { - printMemErrorMsg(SCB->CFSR); + if ((SCB->CFSR & SCB_CFSR_USGFAULTSR_Msk) != 0) { + printUsageErrorMsg(SCB->CFSR); + } + if ((SCB->CFSR & SCB_CFSR_BUSFAULTSR_Msk) != 0) { + printBusErrorMsg(SCB->CFSR); + } + if ((SCB->CFSR & SCB_CFSR_MEMFAULTSR_Msk) != 0) { + printMemErrorMsg(SCB->CFSR); + } + + FAULT_MSG("r0 = 0x%08lx\n", stack[r0]); + FAULT_MSG("r1 = 0x%08lx\n", stack[r1]); + FAULT_MSG("r2 = 0x%08lx\n", stack[r2]); + FAULT_MSG("r3 = 0x%08lx\n", stack[r3]); + FAULT_MSG("r12 = 0x%08lx\n", stack[r12]); + FAULT_MSG("lr = 0x%08lx\n", stack[lr]); + FAULT_MSG("pc = 0x%08lx\n", stack[pc]); + FAULT_MSG("psr = 0x%08lx\n", stack[psr]); } - FAULT_MSG("r0 = 0x%08lx\n", stack[r0]); - FAULT_MSG("r1 = 0x%08lx\n", stack[r1]); - FAULT_MSG("r2 = 0x%08lx\n", stack[r2]); - FAULT_MSG("r3 = 0x%08lx\n", stack[r3]); - FAULT_MSG("r12 = 0x%08lx\n", stack[r12]); - FAULT_MSG("lr = 0x%08lx\n", stack[lr]); - FAULT_MSG("pc = 0x%08lx\n", stack[pc]); - FAULT_MSG("psr = 0x%08lx\n", stack[psr]); - } + FAULT_MSG("Done with fault report - Waiting to reboot\n"); + asm volatile("bkpt #01"); // Enter the debugger if one is connected - FAULT_MSG("Done with fault report - Waiting to reboot\n"); - asm volatile("bkpt #01"); // Enter the debugger if one is connected - - // Don't spin, so that the debugger will let the user step to next instruction - // while (1) ; + // Don't spin, so that the debugger will let the user step to next instruction + // while (1) ; } #ifndef INC_FREERTOS_H // This is a generic cortex M entrypoint that doesn't assume freertos -extern "C" void HardFault_Handler(void) { - asm volatile(" mrs r0,msp\n" - " b HardFault_Impl \n"); +extern "C" void HardFault_Handler(void) +{ + asm volatile(" mrs r0,msp\n" + " b HardFault_Impl \n"); } #elif !defined(ARCH_NRF52) @@ -94,14 +98,15 @@ extern "C" void HardFault_Handler(void) __attribute__((naked)); /* The fault handler implementation calls a function called prvGetRegistersFromStack(). */ -extern "C" void HardFault_Handler(void) { - __asm volatile(" tst lr, #4 \n" - " ite eq \n" - " mrseq r0, msp \n" - " mrsne r0, psp \n" - " ldr r1, [r0, #24] \n" - " ldr r2, handler2_address_const \n" - " bx r2 \n" - " handler2_address_const: .word HardFault_Impl \n"); +extern "C" void HardFault_Handler(void) +{ + __asm volatile(" tst lr, #4 \n" + " ite eq \n" + " mrseq r0, msp \n" + " mrsne r0, psp \n" + " ldr r1, [r0, #24] \n" + " ldr r2, handler2_address_const \n" + " bx r2 \n" + " handler2_address_const: .word HardFault_Impl \n"); } #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 77ca022c7..472107229 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -38,96 +38,101 @@ void variant_shutdown() {} static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; -static inline void debugger_break(void) { - __asm volatile("bkpt #0x01\n\t" - "mov pc, lr\n\t"); +static inline void debugger_break(void) +{ + __asm volatile("bkpt #0x01\n\t" + "mov pc, lr\n\t"); } -bool loopCanSleep() { - // turn off sleep only while connected via USB - // return true; - return !Serial; // the bool operator on the nrf52 serial class returns true if connected to a PC currently - // return !(TinyUSBDevice.mounted() && !TinyUSBDevice.suspended()); +bool loopCanSleep() +{ + // turn off sleep only while connected via USB + // return true; + return !Serial; // the bool operator on the nrf52 serial class returns true if connected to a PC currently + // return !(TinyUSBDevice.mounted() && !TinyUSBDevice.suspended()); } // handle standard gcc assert failures -void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) { - LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); - // debugger_break(); FIXME doesn't work, possibly not for segger - // Reboot cpu - NVIC_SystemReset(); +void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) +{ + LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); + // debugger_break(); FIXME doesn't work, possibly not for segger + // Reboot cpu + NVIC_SystemReset(); } -void getMacAddr(uint8_t *dmac) { - const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR; - dmac[5] = src[0]; - dmac[4] = src[1]; - dmac[3] = src[2]; - dmac[2] = src[3]; - dmac[1] = src[4]; - dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack +void getMacAddr(uint8_t *dmac) +{ + const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR; + dmac[5] = src[0]; + dmac[4] = src[1]; + dmac[3] = src[2]; + dmac[2] = src[3]; + dmac[1] = src[4]; + dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack } -static void initBrownout() { - auto vccthresh = POWER_POFCON_THRESHOLD_V24; +static void initBrownout() +{ + auto vccthresh = POWER_POFCON_THRESHOLD_V24; - auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); - assert(err_code == NRF_SUCCESS); + auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); + assert(err_code == NRF_SUCCESS); - err_code = sd_power_pof_threshold_set(vccthresh); - assert(err_code == NRF_SUCCESS); + err_code = sd_power_pof_threshold_set(vccthresh); + assert(err_code == NRF_SUCCESS); - // We don't bother with setting up brownout if soft device is disabled - because during production we always use - // softdevice + // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice } // This is a public global so that the debugger can set it to false automatically from our gdbinit bool useSoftDevice = true; // Set to false for easier debugging #if !MESHTASTIC_EXCLUDE_BLUETOOTH -void setBluetoothEnable(bool enable) { - // For debugging use: don't use bluetooth - if (!useSoftDevice) { - if (enable) - LOG_INFO("Disable NRF52 BLUETOOTH WHILE DEBUGGING"); - return; - } - - // If user disabled bluetooth: init then disable advertising & reduce power - // Workaround. Avoid issue where device hangs several days after boot.. - // Allegedly, no significant increase in power consumption - if (!config.bluetooth.enabled) { - static bool initialized = false; - if (!initialized) { - nrf52Bluetooth = new NRF52Bluetooth(); - nrf52Bluetooth->startDisabled(); - initBrownout(); - initialized = true; +void setBluetoothEnable(bool enable) +{ + // For debugging use: don't use bluetooth + if (!useSoftDevice) { + if (enable) + LOG_INFO("Disable NRF52 BLUETOOTH WHILE DEBUGGING"); + return; } - return; - } - if (enable) { - powerMon->setState(meshtastic_PowerMon_State_BT_On); - - // If not yet set-up - if (!nrf52Bluetooth) { - LOG_DEBUG("Init NRF52 Bluetooth"); - nrf52Bluetooth = new NRF52Bluetooth(); - nrf52Bluetooth->setup(); - - // We delay brownout init until after BLE because BLE starts soft device - initBrownout(); + // If user disabled bluetooth: init then disable advertising & reduce power + // Workaround. Avoid issue where device hangs several days after boot.. + // Allegedly, no significant increase in power consumption + if (!config.bluetooth.enabled) { + static bool initialized = false; + if (!initialized) { + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->startDisabled(); + initBrownout(); + initialized = true; + } + return; + } + + if (enable) { + powerMon->setState(meshtastic_PowerMon_State_BT_On); + + // If not yet set-up + if (!nrf52Bluetooth) { + LOG_DEBUG("Init NRF52 Bluetooth"); + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->setup(); + + // We delay brownout init until after BLE because BLE starts soft device + initBrownout(); + } + // Already setup, apparently + else + nrf52Bluetooth->resumeAdvertising(); + } + // Disable (if previously set-up) + else if (nrf52Bluetooth) { + powerMon->clearState(meshtastic_PowerMon_State_BT_On); + nrf52Bluetooth->shutdown(); } - // Already setup, apparently - else - nrf52Bluetooth->resumeAdvertising(); - } - // Disable (if previously set-up) - else if (nrf52Bluetooth) { - powerMon->clearState(meshtastic_PowerMon_State_BT_On); - nrf52Bluetooth->shutdown(); - } } #else #warning NRF52 "Bluetooth disable" workaround does not apply to builds with MESHTASTIC_EXCLUDE_BLUETOOTH @@ -136,90 +141,97 @@ void setBluetoothEnable(bool enable) {} /** * Override printf to use the SEGGER output library (note - this does not effect the printf method on the debug console) */ -int printf(const char *fmt, ...) { - va_list args; - va_start(args, fmt); - auto res = SEGGER_RTT_vprintf(0, fmt, &args); - va_end(args); - return res; +int printf(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + auto res = SEGGER_RTT_vprintf(0, fmt, &args); + va_end(args); + return res; } -namespace { +namespace +{ constexpr uint8_t NRF52_MAGIC_LFS_IS_CORRUPT = 0xF5; constexpr uint32_t MULTIPLE_CORRUPTION_DELAY_MILLIS = 20 * 60 * 1000; static unsigned long millis_until_formatting_again = 0; // Report the critical error from loop(), giving a chance for the screen to be initialized first. -inline void reportLittleFSCorruptionOnce() { - static bool report_corruption = !!millis_until_formatting_again; - if (report_corruption) { - report_corruption = false; - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); - } +inline void reportLittleFSCorruptionOnce() +{ + static bool report_corruption = !!millis_until_formatting_again; + if (report_corruption) { + report_corruption = false; + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + } } } // namespace -void preFSBegin() { - // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET - // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. - if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) - return; - NRF_POWER->GPREGRET = 0; - millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS; - InternalFS.format(); - LOG_INFO("LittleFS format complete; restoring default settings"); +void preFSBegin() +{ + // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET + // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. + if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) + return; + NRF_POWER->GPREGRET = 0; + millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS; + InternalFS.format(); + LOG_INFO("LittleFS format complete; restoring default settings"); } -extern "C" void lfs_assert(const char *reason) { - LOG_ERROR("LittleFS corruption detected: %s", reason); - if (millis_until_formatting_again > millis()) { - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); - const long millis_remain = millis_until_formatting_again - millis(); - LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000); - delay(millis_remain); - } - LOG_INFO("Rebooting to format LittleFS"); - delay(500); // Give the serial port a bit of time to output that last message. - // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) - // then set NRF_POWER->GPREGRET directly. - if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { - NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; - } - NVIC_SystemReset(); -} - -void checkSDEvents() { - if (useSoftDevice) { - uint32_t evt; - while (NRF_SUCCESS == sd_evt_get(&evt)) { - switch (evt) { - case NRF_EVT_POWER_FAILURE_WARNING: - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); - break; - - default: - LOG_DEBUG("Unexpected SDevt %d", evt); - break; - } +extern "C" void lfs_assert(const char *reason) +{ + LOG_ERROR("LittleFS corruption detected: %s", reason); + if (millis_until_formatting_again > millis()) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); + const long millis_remain = millis_until_formatting_again - millis(); + LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000); + delay(millis_remain); } - } else { - if (NRF_POWER->EVENTS_POFWARN) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); - } + LOG_INFO("Rebooting to format LittleFS"); + delay(500); // Give the serial port a bit of time to output that last message. + // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set + // NRF_POWER->GPREGRET directly. + if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { + NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + } + NVIC_SystemReset(); } -void nrf52Loop() { - { - static bool watchdog_running = false; - if (!watchdog_running) { - nrfx_wdt_enable(&nrfx_wdt); - watchdog_running = true; - } - } - nrfx_wdt_channel_feed(&nrfx_wdt, nrfx_wdt_channel_id_nrf52_main); +void checkSDEvents() +{ + if (useSoftDevice) { + uint32_t evt; + while (NRF_SUCCESS == sd_evt_get(&evt)) { + switch (evt) { + case NRF_EVT_POWER_FAILURE_WARNING: + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); + break; - checkSDEvents(); - reportLittleFSCorruptionOnce(); + default: + LOG_DEBUG("Unexpected SDevt %d", evt); + break; + } + } + } else { + if (NRF_POWER->EVENTS_POFWARN) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); + } +} + +void nrf52Loop() +{ + { + static bool watchdog_running = false; + if (!watchdog_running) { + nrfx_wdt_enable(&nrfx_wdt); + watchdog_running = true; + } + } + nrfx_wdt_channel_feed(&nrfx_wdt, nrfx_wdt_channel_id_nrf52_main); + + checkSDEvents(); + reportLittleFSCorruptionOnce(); } #ifdef USE_SEMIHOSTING @@ -235,243 +247,249 @@ bool wantSemihost; /** * Turn on semihosting if the ICE debugger wants it. */ -void nrf52InitSemiHosting() { - if (wantSemihost) { - static SemihostingStream semiStream; - // We must dynamically alloc because the constructor does semihost operations which - // would crash any load not talking to a debugger - semiStream.open(); - semiStream.println("Semihosting starts!"); - // Redirect our serial output to instead go via the ICE port - console->setDestination(&semiStream); - } +void nrf52InitSemiHosting() +{ + if (wantSemihost) { + static SemihostingStream semiStream; + // We must dynamically alloc because the constructor does semihost operations which + // would crash any load not talking to a debugger + semiStream.open(); + semiStream.println("Semihosting starts!"); + // Redirect our serial output to instead go via the ICE port + console->setDestination(&semiStream); + } } #endif -void nrf52Setup() { +void nrf52Setup() +{ #ifdef ADC_V - pinMode(ADC_V, INPUT); + pinMode(ADC_V, INPUT); #endif - uint32_t why = NRF_POWER->RESETREAS; - // per - // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpower.html - LOG_DEBUG("Reset reason: 0x%x", why); + uint32_t why = NRF_POWER->RESETREAS; + // per + // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpower.html + LOG_DEBUG("Reset reason: 0x%x", why); #ifdef USE_SEMIHOSTING - nrf52InitSemiHosting(); + nrf52InitSemiHosting(); #endif - // Per - // https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/monitor-mode-debugging-with-j-link-and-gdbeclipse - // This is the recommended setting for Monitor Mode Debugging - NVIC_SetPriority(DebugMonitor_IRQn, 6UL); + // Per + // https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/monitor-mode-debugging-with-j-link-and-gdbeclipse + // This is the recommended setting for Monitor Mode Debugging + NVIC_SetPriority(DebugMonitor_IRQn, 6UL); #ifdef BQ25703A_ADDR - auto *bq = new BQ25713(); - if (!bq->setup()) - LOG_ERROR("ERROR! Charge controller init failed"); + auto *bq = new BQ25713(); + if (!bq->setup()) + LOG_ERROR("ERROR! Charge controller init failed"); #endif - // Init random seed - union seedParts { - uint32_t seed32; - uint8_t seed8[4]; - } seed; - nRFCrypto.begin(); - nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8)); - LOG_DEBUG("Set random seed %u", seed.seed32); - randomSeed(seed.seed32); - nRFCrypto.end(); + // Init random seed + union seedParts { + uint32_t seed32; + uint8_t seed8[4]; + } seed; + nRFCrypto.begin(); + nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8)); + LOG_DEBUG("Set random seed %u", seed.seed32); + randomSeed(seed.seed32); + nRFCrypto.end(); - // Set up nrfx watchdog. Do not enable the watchdog yet (we do that - // the first time through the main loop), so that other threads can - // allocate their own wdt channel to protect themselves from hangs. - nrfx_wdt_config_t wdt0_config = { - .behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000, - // Note: Not using wdt interrupts. - // .interrupt_priority = NRFX_WDT_DEFAULT_CONFIG_IRQ_PRIORITY - }; - nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, - nullptr // Watchdog event handler, not used, we just reset. - ); - assert(r == NRFX_SUCCESS); + // Set up nrfx watchdog. Do not enable the watchdog yet (we do that + // the first time through the main loop), so that other threads can + // allocate their own wdt channel to protect themselves from hangs. + nrfx_wdt_config_t wdt0_config = { + .behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000, + // Note: Not using wdt interrupts. + // .interrupt_priority = NRFX_WDT_DEFAULT_CONFIG_IRQ_PRIORITY + }; + nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, + nullptr // Watchdog event handler, not used, we just reset. + ); + assert(r == NRFX_SUCCESS); - r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main); - assert(r == NRFX_SUCCESS); + r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main); + assert(r == NRFX_SUCCESS); } -void cpuDeepSleep(uint32_t msecToWake) { - // FIXME, configure RTC or button press to wake us - // FIXME, power down SPI, I2C, RAMs +void cpuDeepSleep(uint32_t msecToWake) +{ + // FIXME, configure RTC or button press to wake us + // FIXME, power down SPI, I2C, RAMs #if HAS_WIRE - Wire.end(); + Wire.end(); #endif - SPI.end(); + SPI.end(); #if SPI_INTERFACES_COUNT > 1 - SPI1.end(); + SPI1.end(); #endif - if (Serial) // Another check in case of disabled default serial, does nothing bad - Serial.end(); // This may cause crashes as debug messages continue to flow. + if (Serial) // Another check in case of disabled default serial, does nothing bad + Serial.end(); // This may cause crashes as debug messages continue to flow. - // This causes troubles with waking up on nrf52 (on pro-micro in particular): - // we have no Serial1 in use on nrf52, check Serial and GPS modules. + // This causes troubles with waking up on nrf52 (on pro-micro in particular): + // we have no Serial1 in use on nrf52, check Serial and GPS modules. #ifdef PIN_SERIAL1_RX - if (Serial1) // A straightforward solution to the wake from deepsleep problem - Serial1.end(); + if (Serial1) // A straightforward solution to the wake from deepsleep problem + Serial1.end(); #endif #ifdef TTGO_T_ECHO - // To power off the T-Echo, the display must be set - // as an input pin; otherwise, there will be leakage current. - pinMode(PIN_EINK_CS, INPUT); - pinMode(PIN_EINK_DC, INPUT); - pinMode(PIN_EINK_RES, INPUT); - pinMode(PIN_EINK_BUSY, INPUT); + // To power off the T-Echo, the display must be set + // as an input pin; otherwise, there will be leakage current. + pinMode(PIN_EINK_CS, INPUT); + pinMode(PIN_EINK_DC, INPUT); + pinMode(PIN_EINK_RES, INPUT); + pinMode(PIN_EINK_BUSY, INPUT); #endif - setBluetoothEnable(false); + setBluetoothEnable(false); #ifdef RAK4630 #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, LOW); + digitalWrite(PIN_3V3_EN, LOW); #endif #ifdef AQ_SET_PIN - // RAK-12039 set pin for Air quality sensor - digitalWrite(AQ_SET_PIN, LOW); + // RAK-12039 set pin for Air quality sensor + digitalWrite(AQ_SET_PIN, LOW); #endif #ifdef RAK14014 - // GPIO restores input status, otherwise there will be leakage current - nrf_gpio_cfg_default(TFT_BL); - nrf_gpio_cfg_default(TFT_DC); - nrf_gpio_cfg_default(TFT_CS); - nrf_gpio_cfg_default(TFT_SCLK); - nrf_gpio_cfg_default(TFT_MOSI); - nrf_gpio_cfg_default(TFT_MISO); - nrf_gpio_cfg_default(SCREEN_TOUCH_INT); - nrf_gpio_cfg_default(WB_I2C1_SCL); - nrf_gpio_cfg_default(WB_I2C1_SDA); + // GPIO restores input status, otherwise there will be leakage current + nrf_gpio_cfg_default(TFT_BL); + nrf_gpio_cfg_default(TFT_DC); + nrf_gpio_cfg_default(TFT_CS); + nrf_gpio_cfg_default(TFT_SCLK); + nrf_gpio_cfg_default(TFT_MOSI); + nrf_gpio_cfg_default(TFT_MISO); + nrf_gpio_cfg_default(SCREEN_TOUCH_INT); + nrf_gpio_cfg_default(WB_I2C1_SCL); + nrf_gpio_cfg_default(WB_I2C1_SDA); - // nrf_gpio_cfg_default(WB_I2C2_SCL); - // nrf_gpio_cfg_default(WB_I2C2_SDA); + // nrf_gpio_cfg_default(WB_I2C2_SCL); + // nrf_gpio_cfg_default(WB_I2C2_SDA); #endif #endif #ifdef MESHLINK #ifdef PIN_WD_EN - digitalWrite(PIN_WD_EN, LOW); + digitalWrite(PIN_WD_EN, LOW); #endif #endif #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR) - nrf_gpio_cfg_default(PIN_GPS_PPS); - detachInterrupt(PIN_GPS_PPS); - detachInterrupt(PIN_BUTTON1); + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); #endif #ifdef ELECROW_ThinkNode_M1 - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || pin == PIN_BUTTON1 || - pin == PIN_BUTTON2) { - continue; + for (int pin = 0; pin < 48; pin++) { + if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || + pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { + continue; + } + pinMode(pin, OUTPUT); } - pinMode(pin, OUTPUT); - } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || pin == PIN_BUTTON1 || - pin == PIN_BUTTON2) { - continue; + for (int pin = 0; pin < 48; pin++) { + if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || + pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { + continue; + } + digitalWrite(pin, LOW); } - digitalWrite(pin, LOW); - } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || pin == PIN_BUTTON1 || - pin == PIN_BUTTON2) { - continue; + for (int pin = 0; pin < 48; pin++) { + if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || + pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { + continue; + } + NRF_GPIO->DIRCLR = (1 << pin); } - NRF_GPIO->DIRCLR = (1 << pin); - } #endif - variant_shutdown(); + variant_shutdown(); - // Sleepy trackers or sensors can low power "sleep" - // Don't enter this if we're sleeping portMAX_DELAY, since that's a shutdown event - if (msecToWake != portMAX_DELAY && (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR) && - config.power.is_power_saving == true)) { - sd_power_mode_set(NRF_POWER_MODE_LOWPWR); - delay(msecToWake); - NVIC_SystemReset(); - } else { - // Resume on user button press - // https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738 - constexpr uint32_t DFU_MAGIC_SKIP = 0x6d; - sd_power_gpregret_clr(0, 0xFF); // Clear the register before setting a new values in it for stability reasons - sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP + // Sleepy trackers or sensors can low power "sleep" + // Don't enter this if we're sleeping portMAX_DELAY, since that's a shutdown event + if (msecToWake != portMAX_DELAY && + (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR) && + config.power.is_power_saving == true)) { + sd_power_mode_set(NRF_POWER_MODE_LOWPWR); + delay(msecToWake); + NVIC_SystemReset(); + } else { + // Resume on user button press + // https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738 + constexpr uint32_t DFU_MAGIC_SKIP = 0x6d; + sd_power_gpregret_clr(0, 0xFF); // Clear the register before setting a new values in it for stability reasons + sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP - // FIXME, use system off mode with ram retention for key state? - // FIXME, use non-init RAM per - // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled + // FIXME, use system off mode with ram retention for key state? + // FIXME, use non-init RAM per + // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled #ifdef ELECROW_ThinkNode_M1 - nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); - nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); + nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); #endif #ifdef PROMICRO_DIY_TCXO - nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge - nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep + nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge + nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep #endif #ifdef BATTERY_LPCOMP_INPUT - // Wake up if power rises again - nrf_lpcomp_config_t c; - c.reference = BATTERY_LPCOMP_THRESHOLD; - c.detection = NRF_LPCOMP_DETECT_UP; - c.hyst = NRF_LPCOMP_HYST_NOHYST; - nrf_lpcomp_configure(NRF_LPCOMP, &c); - nrf_lpcomp_input_select(NRF_LPCOMP, BATTERY_LPCOMP_INPUT); - nrf_lpcomp_enable(NRF_LPCOMP); + // Wake up if power rises again + nrf_lpcomp_config_t c; + c.reference = BATTERY_LPCOMP_THRESHOLD; + c.detection = NRF_LPCOMP_DETECT_UP; + c.hyst = NRF_LPCOMP_HYST_NOHYST; + nrf_lpcomp_configure(NRF_LPCOMP, &c); + nrf_lpcomp_input_select(NRF_LPCOMP, BATTERY_LPCOMP_INPUT); + nrf_lpcomp_enable(NRF_LPCOMP); - battery_adcEnable(); + battery_adcEnable(); - nrf_lpcomp_task_trigger(NRF_LPCOMP, NRF_LPCOMP_TASK_START); - while (!nrf_lpcomp_event_check(NRF_LPCOMP, NRF_LPCOMP_EVENT_READY)) - ; + nrf_lpcomp_task_trigger(NRF_LPCOMP, NRF_LPCOMP_TASK_START); + while (!nrf_lpcomp_event_check(NRF_LPCOMP, NRF_LPCOMP_EVENT_READY)) + ; #endif - auto ok = sd_power_system_off(); - if (ok != NRF_SUCCESS) { - LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!"); - NRF_POWER->SYSTEMOFF = 1; + auto ok = sd_power_system_off(); + if (ok != NRF_SUCCESS) { + LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!"); + NRF_POWER->SYSTEMOFF = 1; + } } - } - // The following code should not be run, because we are off - while (1) { - delay(5000); - LOG_DEBUG("."); - } + // The following code should not be run, because we are off + while (1) { + delay(5000); + LOG_DEBUG("."); + } } -void clearBonds() { - if (!nrf52Bluetooth) { - nrf52Bluetooth = new NRF52Bluetooth(); - nrf52Bluetooth->setup(); - } - nrf52Bluetooth->clearBonds(); +void clearBonds() +{ + if (!nrf52Bluetooth) { + nrf52Bluetooth = new NRF52Bluetooth(); + nrf52Bluetooth->setup(); + } + nrf52Bluetooth->clearBonds(); } -void enterDfuMode() { +void enterDfuMode() +{ // SDK kit does not have native USB like almost all other NRF52 boards #ifdef NRF_USE_SERIAL_DFU - enterSerialDfu(); + enterSerialDfu(); #else - enterUf2Dfu(); + enterUf2Dfu(); #endif } diff --git a/src/platform/nrf52/softdevice/ble.h b/src/platform/nrf52/softdevice/ble.h index 6d4be74ce..177b436ad 100644 --- a/src/platform/nrf52/softdevice/ble.h +++ b/src/platform/nrf52/softdevice/ble.h @@ -70,26 +70,26 @@ extern "C" { * @brief Common API SVC numbers. */ enum BLE_COMMON_SVCS { - SD_BLE_ENABLE = BLE_SVC_BASE, /**< Enable and initialize the BLE stack */ - SD_BLE_EVT_GET, /**< Get an event from the pending events queue. */ - SD_BLE_UUID_VS_ADD, /**< Add a Vendor Specific base UUID. */ - SD_BLE_UUID_DECODE, /**< Decode UUID bytes. */ - SD_BLE_UUID_ENCODE, /**< Encode UUID bytes. */ - SD_BLE_VERSION_GET, /**< Get the local version information (company ID, Link Layer Version, Link Layer Subversion). */ - SD_BLE_USER_MEM_REPLY, /**< User Memory Reply. */ - SD_BLE_OPT_SET, /**< Set a BLE option. */ - SD_BLE_OPT_GET, /**< Get a BLE option. */ - SD_BLE_CFG_SET, /**< Add a configuration to the BLE stack. */ - SD_BLE_UUID_VS_REMOVE, /**< Remove a Vendor Specific base UUID. */ + SD_BLE_ENABLE = BLE_SVC_BASE, /**< Enable and initialize the BLE stack */ + SD_BLE_EVT_GET, /**< Get an event from the pending events queue. */ + SD_BLE_UUID_VS_ADD, /**< Add a Vendor Specific base UUID. */ + SD_BLE_UUID_DECODE, /**< Decode UUID bytes. */ + SD_BLE_UUID_ENCODE, /**< Encode UUID bytes. */ + SD_BLE_VERSION_GET, /**< Get the local version information (company ID, Link Layer Version, Link Layer Subversion). */ + SD_BLE_USER_MEM_REPLY, /**< User Memory Reply. */ + SD_BLE_OPT_SET, /**< Set a BLE option. */ + SD_BLE_OPT_GET, /**< Get a BLE option. */ + SD_BLE_CFG_SET, /**< Add a configuration to the BLE stack. */ + SD_BLE_UUID_VS_REMOVE, /**< Remove a Vendor Specific base UUID. */ }; /** * @brief BLE Module Independent Event IDs. */ enum BLE_COMMON_EVTS { - BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. See @ref ble_evt_user_mem_request_t - \n Reply with @ref sd_ble_user_mem_reply. */ - BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. See @ref ble_evt_user_mem_release_t */ + BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. See @ref ble_evt_user_mem_request_t + \n Reply with @ref sd_ble_user_mem_reply. */ + BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. See @ref ble_evt_user_mem_release_t */ }; /**@brief BLE Connection Configuration IDs. @@ -97,11 +97,11 @@ enum BLE_COMMON_EVTS { * IDs that uniquely identify a connection configuration. */ enum BLE_CONN_CFGS { - BLE_CONN_CFG_GAP = BLE_CONN_CFG_BASE + 0, /**< BLE GAP specific connection configuration. */ - BLE_CONN_CFG_GATTC = BLE_CONN_CFG_BASE + 1, /**< BLE GATTC specific connection configuration. */ - BLE_CONN_CFG_GATTS = BLE_CONN_CFG_BASE + 2, /**< BLE GATTS specific connection configuration. */ - BLE_CONN_CFG_GATT = BLE_CONN_CFG_BASE + 3, /**< BLE GATT specific connection configuration. */ - BLE_CONN_CFG_L2CAP = BLE_CONN_CFG_BASE + 4, /**< BLE L2CAP specific connection configuration. */ + BLE_CONN_CFG_GAP = BLE_CONN_CFG_BASE + 0, /**< BLE GAP specific connection configuration. */ + BLE_CONN_CFG_GATTC = BLE_CONN_CFG_BASE + 1, /**< BLE GATTC specific connection configuration. */ + BLE_CONN_CFG_GATTS = BLE_CONN_CFG_BASE + 2, /**< BLE GATTS specific connection configuration. */ + BLE_CONN_CFG_GATT = BLE_CONN_CFG_BASE + 3, /**< BLE GATT specific connection configuration. */ + BLE_CONN_CFG_L2CAP = BLE_CONN_CFG_BASE + 4, /**< BLE L2CAP specific connection configuration. */ }; /**@brief BLE Common Configuration IDs. @@ -109,16 +109,16 @@ enum BLE_CONN_CFGS { * IDs that uniquely identify a common configuration. */ enum BLE_COMMON_CFGS { - BLE_COMMON_CFG_VS_UUID = BLE_CFG_BASE, /**< Vendor specific base UUID configuration */ + BLE_COMMON_CFG_VS_UUID = BLE_CFG_BASE, /**< Vendor specific base UUID configuration */ }; /**@brief Common Option IDs. * IDs that uniquely identify a common option. */ enum BLE_COMMON_OPTS { - BLE_COMMON_OPT_PA_LNA = BLE_OPT_BASE + 0, /**< PA and LNA options */ - BLE_COMMON_OPT_CONN_EVT_EXT = BLE_OPT_BASE + 1, /**< Extended connection events option */ - BLE_COMMON_OPT_EXTENDED_RC_CAL = BLE_OPT_BASE + 2, /**< Extended RC calibration option */ + BLE_COMMON_OPT_PA_LNA = BLE_OPT_BASE + 0, /**< PA and LNA options */ + BLE_COMMON_OPT_CONN_EVT_EXT = BLE_OPT_BASE + 1, /**< Extended connection events option */ + BLE_COMMON_OPT_EXTENDED_RC_CAL = BLE_OPT_BASE + 2, /**< Extended RC calibration option */ }; /** @} */ @@ -136,11 +136,10 @@ enum BLE_COMMON_OPTS { /** @brief Maximum possible length for BLE Events. * @note The highest value used for @ref ble_gatt_conn_cfg_t::att_mtu in any connection configuration shall be used as a - * parameter. If that value has not been configured for any connections then @ref BLE_GATT_ATT_MTU_DEFAULT must be used - * instead. + * parameter. If that value has not been configured for any connections then @ref BLE_GATT_ATT_MTU_DEFAULT must be used instead. */ -#define BLE_EVT_LEN_MAX(ATT_MTU) \ - (offsetof(ble_evt_t, evt.gattc_evt.params.prim_srvc_disc_rsp.services) + ((ATT_MTU)-1) / 4 * sizeof(ble_gattc_service_t)) +#define BLE_EVT_LEN_MAX(ATT_MTU) \ + (offsetof(ble_evt_t, evt.gattc_evt.params.prim_srvc_disc_rsp.services) + ((ATT_MTU)-1) / 4 * sizeof(ble_gattc_service_t)) /** @defgroup BLE_USER_MEM_TYPES User Memory Types * @{ */ @@ -169,68 +168,67 @@ enum BLE_COMMON_OPTS { /**@brief User Memory Block. */ typedef struct { - uint8_t *p_mem; /**< Pointer to the start of the user memory block. */ - uint16_t len; /**< Length in bytes of the user memory block. */ + uint8_t *p_mem; /**< Pointer to the start of the user memory block. */ + uint16_t len; /**< Length in bytes of the user memory block. */ } ble_user_mem_block_t; /**@brief Event structure for @ref BLE_EVT_USER_MEM_REQUEST. */ typedef struct { - uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ + uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ } ble_evt_user_mem_request_t; /**@brief Event structure for @ref BLE_EVT_USER_MEM_RELEASE. */ typedef struct { - uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ - ble_user_mem_block_t mem_block; /**< User memory block */ + uint8_t type; /**< User memory type, see @ref BLE_USER_MEM_TYPES. */ + ble_user_mem_block_t mem_block; /**< User memory block */ } ble_evt_user_mem_release_t; /**@brief Event structure for events not associated with a specific function module. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which this event occurred. */ - union { - ble_evt_user_mem_request_t user_mem_request; /**< User Memory Request Event Parameters. */ - ble_evt_user_mem_release_t user_mem_release; /**< User Memory Release Event Parameters. */ - } params; /**< Event parameter union. */ + uint16_t conn_handle; /**< Connection Handle on which this event occurred. */ + union { + ble_evt_user_mem_request_t user_mem_request; /**< User Memory Request Event Parameters. */ + ble_evt_user_mem_release_t user_mem_release; /**< User Memory Release Event Parameters. */ + } params; /**< Event parameter union. */ } ble_common_evt_t; /**@brief BLE Event header. */ typedef struct { - uint16_t evt_id; /**< Value from a BLE__EVT series. */ - uint16_t evt_len; /**< Length in octets including this header. */ + uint16_t evt_id; /**< Value from a BLE__EVT series. */ + uint16_t evt_len; /**< Length in octets including this header. */ } ble_evt_hdr_t; /**@brief Common BLE Event type, wrapping the module specific event reports. */ typedef struct { - ble_evt_hdr_t header; /**< Event header. */ - union { - ble_common_evt_t common_evt; /**< Common Event, evt_id in BLE_EVT_* series. */ - ble_gap_evt_t gap_evt; /**< GAP originated event, evt_id in BLE_GAP_EVT_* series. */ - ble_gattc_evt_t gattc_evt; /**< GATT client originated event, evt_id in BLE_GATTC_EVT* series. */ - ble_gatts_evt_t gatts_evt; /**< GATT server originated event, evt_id in BLE_GATTS_EVT* series. */ - ble_l2cap_evt_t l2cap_evt; /**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series. */ - } evt; /**< Event union. */ + ble_evt_hdr_t header; /**< Event header. */ + union { + ble_common_evt_t common_evt; /**< Common Event, evt_id in BLE_EVT_* series. */ + ble_gap_evt_t gap_evt; /**< GAP originated event, evt_id in BLE_GAP_EVT_* series. */ + ble_gattc_evt_t gattc_evt; /**< GATT client originated event, evt_id in BLE_GATTC_EVT* series. */ + ble_gatts_evt_t gatts_evt; /**< GATT server originated event, evt_id in BLE_GATTS_EVT* series. */ + ble_l2cap_evt_t l2cap_evt; /**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series. */ + } evt; /**< Event union. */ } ble_evt_t; /** * @brief Version Information. */ typedef struct { - uint8_t version_number; /**< Link Layer Version number. See - https://www.bluetooth.org/en-us/specification/assigned-numbers/link-layer for assigned - values. */ - uint16_t company_id; /**< Company ID, Nordic Semiconductor's company ID is 89 (0x0059) - (https://www.bluetooth.org/apps/content/Default.aspx?doc_id=49708). */ - uint16_t subversion_number; /**< Link Layer Sub Version number, corresponds to the SoftDevice Config ID or Firmware ID - (FWID). */ + uint8_t version_number; /**< Link Layer Version number. See + https://www.bluetooth.org/en-us/specification/assigned-numbers/link-layer for assigned values. */ + uint16_t company_id; /**< Company ID, Nordic Semiconductor's company ID is 89 (0x0059) + (https://www.bluetooth.org/apps/content/Default.aspx?doc_id=49708). */ + uint16_t + subversion_number; /**< Link Layer Sub Version number, corresponds to the SoftDevice Config ID or Firmware ID (FWID). */ } ble_version_t; /** * @brief Configuration parameters for the PA and LNA. */ typedef struct { - uint8_t enable : 1; /**< Enable toggling for this amplifier */ - uint8_t active_high : 1; /**< Set the pin to be active high */ - uint8_t gpio_pin : 6; /**< The GPIO pin to toggle for this amplifier */ + uint8_t enable : 1; /**< Enable toggling for this amplifier */ + uint8_t active_high : 1; /**< Set the pin to be active high */ + uint8_t gpio_pin : 6; /**< The GPIO pin to toggle for this amplifier */ } ble_pa_lna_cfg_t; /** @@ -243,16 +241,16 @@ typedef struct { * by the application and should be regarded as reserved as long as any PA/LNA toggling is enabled. * * @note @ref sd_ble_opt_get is not supported for this option. - * @note Setting this option while the radio is in use (i.e. any of the roles are active) may have undefined - * consequences and must be avoided by the application. + * @note Setting this option while the radio is in use (i.e. any of the roles are active) may have undefined consequences + * and must be avoided by the application. */ typedef struct { - ble_pa_lna_cfg_t pa_cfg; /**< Power Amplifier configuration */ - ble_pa_lna_cfg_t lna_cfg; /**< Low Noise Amplifier configuration */ + ble_pa_lna_cfg_t pa_cfg; /**< Power Amplifier configuration */ + ble_pa_lna_cfg_t lna_cfg; /**< Low Noise Amplifier configuration */ - uint8_t ppi_ch_id_set; /**< PPI channel used for radio pin setting */ - uint8_t ppi_ch_id_clr; /**< PPI channel used for radio pin clearing */ - uint8_t gpiote_ch_id; /**< GPIOTE channel used for radio pin toggling */ + uint8_t ppi_ch_id_set; /**< PPI channel used for radio pin setting */ + uint8_t ppi_ch_id_clr; /**< PPI channel used for radio pin clearing */ + uint8_t gpiote_ch_id; /**< GPIOTE channel used for radio pin toggling */ } ble_common_opt_pa_lna_t; /** @@ -260,27 +258,25 @@ typedef struct { * * When enabled the SoftDevice will dynamically extend the connection event when possible. * - * The connection event length is controlled by the connection configuration as set by @ref - * ble_gap_conn_cfg_t::event_length. The connection event can be extended if there is time to send another packet pair - * before the start of the next connection interval, and if there are no conflicts with other BLE roles requesting radio - * time. + * The connection event length is controlled by the connection configuration as set by @ref ble_gap_conn_cfg_t::event_length. + * The connection event can be extended if there is time to send another packet pair before the start of the next connection + * interval, and if there are no conflicts with other BLE roles requesting radio time. * * @note @ref sd_ble_opt_get is not supported for this option. */ typedef struct { - uint8_t enable : 1; /**< Enable extended BLE connection events, disabled by default. */ + uint8_t enable : 1; /**< Enable extended BLE connection events, disabled by default. */ } ble_common_opt_conn_evt_ext_t; /** * @brief Enable/disable extended RC calibration. * - * If extended RC calibration is enabled and the internal RC oscillator (@ref NRF_CLOCK_LF_SRC_RC) is used as the - * SoftDevice LFCLK source, the SoftDevice as a peripheral will by default try to increase the receive window if two - * consecutive packets are not received. If it turns out that the packets were not received due to clock drift, the RC - * calibration is started. This calibration comes in addition to the periodic calibration that is configured by @ref - * sd_softdevice_enable(). When using only peripheral connections, the periodic calibration can therefore be configured - * with a much longer interval as the peripheral will be able to detect and adjust automatically to clock drift, and - * calibrate on demand. + * If extended RC calibration is enabled and the internal RC oscillator (@ref NRF_CLOCK_LF_SRC_RC) is used as the SoftDevice + * LFCLK source, the SoftDevice as a peripheral will by default try to increase the receive window if two consecutive packets + * are not received. If it turns out that the packets were not received due to clock drift, the RC calibration is started. + * This calibration comes in addition to the periodic calibration that is configured by @ref sd_softdevice_enable(). When + * using only peripheral connections, the periodic calibration can therefore be configured with a much longer interval as the + * peripheral will be able to detect and adjust automatically to clock drift, and calibrate on demand. * * If extended RC calibration is disabled and the internal RC oscillator is used as the SoftDevice LFCLK source, the * RC oscillator is calibrated periodically as configured by @ref sd_softdevice_enable(). @@ -288,29 +284,28 @@ typedef struct { * @note @ref sd_ble_opt_get is not supported for this option. */ typedef struct { - uint8_t enable : 1; /**< Enable extended RC calibration, enabled by default. */ + uint8_t enable : 1; /**< Enable extended RC calibration, enabled by default. */ } ble_common_opt_extended_rc_cal_t; /**@brief Option structure for common options. */ typedef union { - ble_common_opt_pa_lna_t pa_lna; /**< Parameters for controlling PA and LNA pin toggling. */ - ble_common_opt_conn_evt_ext_t conn_evt_ext; /**< Parameters for enabling extended connection events. */ - ble_common_opt_extended_rc_cal_t extended_rc_cal; /**< Parameters for enabling extended RC calibration. */ + ble_common_opt_pa_lna_t pa_lna; /**< Parameters for controlling PA and LNA pin toggling. */ + ble_common_opt_conn_evt_ext_t conn_evt_ext; /**< Parameters for enabling extended connection events. */ + ble_common_opt_extended_rc_cal_t extended_rc_cal; /**< Parameters for enabling extended RC calibration. */ } ble_common_opt_t; /**@brief Common BLE Option type, wrapping the module specific options. */ typedef union { - ble_common_opt_t common_opt; /**< COMMON options, opt_id in @ref BLE_COMMON_OPTS series. */ - ble_gap_opt_t gap_opt; /**< GAP option, opt_id in @ref BLE_GAP_OPTS series. */ - ble_gattc_opt_t gattc_opt; /**< GATTC option, opt_id in @ref BLE_GATTC_OPTS series. */ + ble_common_opt_t common_opt; /**< COMMON options, opt_id in @ref BLE_COMMON_OPTS series. */ + ble_gap_opt_t gap_opt; /**< GAP option, opt_id in @ref BLE_GAP_OPTS series. */ + ble_gattc_opt_t gattc_opt; /**< GATTC option, opt_id in @ref BLE_GATTC_OPTS series. */ } ble_opt_t; /**@brief BLE connection configuration type, wrapping the module specific configurations, set with * @ref sd_ble_cfg_set. * * @note Connection configurations don't have to be set. - * In the case that no configurations has been set, or fewer connection configurations has been set than enabled - connections, + * In the case that no configurations has been set, or fewer connection configurations has been set than enabled connections, * the default connection configuration will be automatically added for the remaining connections. * When creating connections with the default configuration, @ref BLE_CONN_CFG_TAG_DEFAULT should be used in * place of @ref ble_conn_cfg_t::conn_cfg_tag. @@ -324,18 +319,17 @@ typedef union { */ typedef struct { - uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the - @ref sd_ble_gap_adv_start() and @ref sd_ble_gap_connect() calls - to select this configuration when creating a connection. - Must be different for all connection configurations added and not @ref - BLE_CONN_CFG_TAG_DEFAULT. */ - union { - ble_gap_conn_cfg_t gap_conn_cfg; /**< GAP connection configuration, cfg_id is @ref BLE_CONN_CFG_GAP. */ - ble_gattc_conn_cfg_t gattc_conn_cfg; /**< GATTC connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTC. */ - ble_gatts_conn_cfg_t gatts_conn_cfg; /**< GATTS connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTS. */ - ble_gatt_conn_cfg_t gatt_conn_cfg; /**< GATT connection configuration, cfg_id is @ref BLE_CONN_CFG_GATT. */ - ble_l2cap_conn_cfg_t l2cap_conn_cfg; /**< L2CAP connection configuration, cfg_id is @ref BLE_CONN_CFG_L2CAP. */ - } params; /**< Connection configuration union. */ + uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the + @ref sd_ble_gap_adv_start() and @ref sd_ble_gap_connect() calls + to select this configuration when creating a connection. + Must be different for all connection configurations added and not @ref BLE_CONN_CFG_TAG_DEFAULT. */ + union { + ble_gap_conn_cfg_t gap_conn_cfg; /**< GAP connection configuration, cfg_id is @ref BLE_CONN_CFG_GAP. */ + ble_gattc_conn_cfg_t gattc_conn_cfg; /**< GATTC connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTC. */ + ble_gatts_conn_cfg_t gatts_conn_cfg; /**< GATTS connection configuration, cfg_id is @ref BLE_CONN_CFG_GATTS. */ + ble_gatt_conn_cfg_t gatt_conn_cfg; /**< GATT connection configuration, cfg_id is @ref BLE_CONN_CFG_GATT. */ + ble_l2cap_conn_cfg_t l2cap_conn_cfg; /**< L2CAP connection configuration, cfg_id is @ref BLE_CONN_CFG_L2CAP. */ + } params; /**< Connection configuration union. */ } ble_conn_cfg_t; /** @@ -344,22 +338,22 @@ typedef struct { * @retval ::NRF_ERROR_INVALID_PARAM Too many UUIDs configured. */ typedef struct { - uint8_t vs_uuid_count; /**< Number of 128-bit Vendor Specific base UUID bases to allocate memory for. - Default value is @ref BLE_UUID_VS_COUNT_DEFAULT. Maximum value is - @ref BLE_UUID_VS_COUNT_MAX. */ + uint8_t vs_uuid_count; /**< Number of 128-bit Vendor Specific base UUID bases to allocate memory for. + Default value is @ref BLE_UUID_VS_COUNT_DEFAULT. Maximum value is + @ref BLE_UUID_VS_COUNT_MAX. */ } ble_common_cfg_vs_uuid_t; /**@brief Common BLE Configuration type, wrapping the common configurations. */ typedef union { - ble_common_cfg_vs_uuid_t vs_uuid_cfg; /**< Vendor Specific base UUID configuration, cfg_id is @ref BLE_COMMON_CFG_VS_UUID. */ + ble_common_cfg_vs_uuid_t vs_uuid_cfg; /**< Vendor Specific base UUID configuration, cfg_id is @ref BLE_COMMON_CFG_VS_UUID. */ } ble_common_cfg_t; /**@brief BLE Configuration type, wrapping the module specific configurations. */ typedef union { - ble_conn_cfg_t conn_cfg; /**< Connection specific configurations, cfg_id in @ref BLE_CONN_CFGS series. */ - ble_common_cfg_t common_cfg; /**< Global common configurations, cfg_id in @ref BLE_COMMON_CFGS series. */ - ble_gap_cfg_t gap_cfg; /**< Global GAP configurations, cfg_id in @ref BLE_GAP_CFGS series. */ - ble_gatts_cfg_t gatts_cfg; /**< Global GATTS configuration, cfg_id in @ref BLE_GATTS_CFGS series. */ + ble_conn_cfg_t conn_cfg; /**< Connection specific configurations, cfg_id in @ref BLE_CONN_CFGS series. */ + ble_common_cfg_t common_cfg; /**< Global common configurations, cfg_id in @ref BLE_COMMON_CFGS series. */ + ble_gap_cfg_t gap_cfg; /**< Global GAP configurations, cfg_id in @ref BLE_GAP_CFGS series. */ + ble_gatts_cfg_t gatts_cfg; /**< Global GATTS configuration, cfg_id in @ref BLE_GATTS_CFGS series. */ } ble_cfg_t; /** @} */ @@ -408,12 +402,11 @@ typedef union { * @retval ::NRF_ERROR_INVALID_ADDR Invalid or not sufficiently aligned pointer supplied. * @retval ::NRF_ERROR_NO_MEM One or more of the following is true: * - The amount of memory assigned to the SoftDevice by *p_app_ram_base is not - * large enough to fit this configuration's memory requirement. Check - * *p_app_ram_base and set the start address of the application RAM region accordingly. + * large enough to fit this configuration's memory requirement. Check *p_app_ram_base + * and set the start address of the application RAM region accordingly. * - Dynamic part of the SoftDevice RAM region is larger then 64 kB which * is currently not supported. - * @retval ::NRF_ERROR_RESOURCES The total number of L2CAP Channels configured using @ref sd_ble_cfg_set is too - * large. + * @retval ::NRF_ERROR_RESOURCES The total number of L2CAP Channels configured using @ref sd_ble_cfg_set is too large. */ SVCALL(SD_BLE_ENABLE, uint32_t, sd_ble_enable(uint32_t *p_app_ram_base)); @@ -468,12 +461,11 @@ SVCALL(SD_BLE_CFG_SET, uint32_t, sd_ble_cfg_set(uint32_t cfg_id, ble_cfg_t const * every time SD_EVT_IRQn is raised to ensure that all available events are pulled from the BLE stack. Failure to do so * could potentially leave events in the internal queue without the application being aware of this fact. * - * Sizing the p_dest buffer is equally important, since the application needs to provide all the memory necessary for - * the event to be copied into application memory. If the buffer provided is not large enough to fit the entire contents - * of the event, + * Sizing the p_dest buffer is equally important, since the application needs to provide all the memory necessary for the event to + * be copied into application memory. If the buffer provided is not large enough to fit the entire contents of the event, * @ref NRF_ERROR_DATA_SIZE will be returned and the application can then call again with a larger buffer size. - * The maximum possible event length is defined by @ref BLE_EVT_LEN_MAX. The application may also "peek" the event - * length by providing p_dest as a NULL pointer and inspecting the value of *p_len upon return: + * The maximum possible event length is defined by @ref BLE_EVT_LEN_MAX. The application may also "peek" the event length + * by providing p_dest as a NULL pointer and inspecting the value of *p_len upon return: * * \code * uint16_t len; @@ -512,8 +504,8 @@ SVCALL(SD_BLE_EVT_GET, uint32_t, sd_ble_evt_get(uint8_t *p_dest, uint16_t *p_len * * @param[in] p_vs_uuid Pointer to a 16-octet (128-bit) little endian Vendor Specific base UUID disregarding * bytes 12 and 13. - * @param[out] p_uuid_type Pointer to a uint8_t where the type field in @ref ble_uuid_t corresponding to this UUID will - * be stored. + * @param[out] p_uuid_type Pointer to a uint8_t where the type field in @ref ble_uuid_t corresponding to this UUID will be + * stored. * * @retval ::NRF_SUCCESS Successfully added the Vendor Specific base UUID. * @retval ::NRF_ERROR_INVALID_ADDR If p_vs_uuid or p_uuid_type is NULL or invalid. @@ -526,13 +518,14 @@ SVCALL(SD_BLE_UUID_VS_ADD, uint32_t, sd_ble_uuid_vs_add(ble_uuid128_t const *p_v * @details This call removes a Vendor Specific base UUID. This function allows * the application to reuse memory allocated for Vendor Specific base UUIDs. * - * @note Currently this function can only be called with a p_uuid_type set to @ref BLE_UUID_TYPE_UNKNOWN or the last - * added UUID type. + * @note Currently this function can only be called with a p_uuid_type set to @ref BLE_UUID_TYPE_UNKNOWN or the last added UUID + * type. * - * @param[inout] p_uuid_type Pointer to a uint8_t where its value matches the UUID type in @ref ble_uuid_t::type to be - * removed. If the type is set to @ref BLE_UUID_TYPE_UNKNOWN, or the pointer is NULL, the last Vendor Specific base UUID - * will be removed. If the function returns successfully, the UUID type that was removed will be written back to @p - * p_uuid_type. If function returns with a failure, it contains the last type that is in use by the ATT Server. + * @param[inout] p_uuid_type Pointer to a uint8_t where its value matches the UUID type in @ref ble_uuid_t::type to be removed. + * If the type is set to @ref BLE_UUID_TYPE_UNKNOWN, or the pointer is NULL, the last Vendor Specific + * base UUID will be removed. If the function returns successfully, the UUID type that was removed will + * be written back to @p p_uuid_type. If function returns with a failure, it contains the last type that + * is in use by the ATT Server. * * @retval ::NRF_SUCCESS Successfully removed the Vendor Specific base UUID. * @retval ::NRF_ERROR_INVALID_ADDR If p_uuid_type is invalid. @@ -563,8 +556,8 @@ SVCALL(SD_BLE_UUID_DECODE, uint32_t, sd_ble_uuid_decode(uint8_t uuid_le_len, uin /** @brief Encode a @ref ble_uuid_t structure into little endian raw UUID bytes (16-bit or 128-bit). * - * @note The pointer to the destination buffer p_uuid_le may be NULL, in which case only the validity and size of p_uuid - * is computed. + * @note The pointer to the destination buffer p_uuid_le may be NULL, in which case only the validity and size of p_uuid is + * computed. * * @param[in] p_uuid Pointer to a @ref ble_uuid_t structure that will be encoded into bytes. * @param[out] p_uuid_le_len Pointer to a uint8_t that will be filled with the encoded length (2 or 16 bytes). diff --git a/src/platform/nrf52/softdevice/ble_err.h b/src/platform/nrf52/softdevice/ble_err.h index bbd7571f1..d20f6d141 100644 --- a/src/platform/nrf52/softdevice/ble_err.h +++ b/src/platform/nrf52/softdevice/ble_err.h @@ -67,8 +67,8 @@ extern "C" { #define BLE_ERROR_INVALID_ATTR_HANDLE (NRF_ERROR_STK_BASE_NUM + 0x003) /**< Invalid attribute handle. */ #define BLE_ERROR_INVALID_ADV_HANDLE (NRF_ERROR_STK_BASE_NUM + 0x004) /**< Invalid advertising handle. */ #define BLE_ERROR_INVALID_ROLE (NRF_ERROR_STK_BASE_NUM + 0x005) /**< Invalid role. */ -#define BLE_ERROR_BLOCKED_BY_OTHER_LINKS \ - (NRF_ERROR_STK_BASE_NUM + 0x006) /**< The attempt to change link settings failed due to the scheduling of other links. */ +#define BLE_ERROR_BLOCKED_BY_OTHER_LINKS \ + (NRF_ERROR_STK_BASE_NUM + 0x006) /**< The attempt to change link settings failed due to the scheduling of other links. */ /** @} */ /** @defgroup BLE_ERROR_SUBRANGES Module specific error code subranges diff --git a/src/platform/nrf52/softdevice/ble_gap.h b/src/platform/nrf52/softdevice/ble_gap.h index e2c7de30d..8ebdfa82b 100644 --- a/src/platform/nrf52/softdevice/ble_gap.h +++ b/src/platform/nrf52/softdevice/ble_gap.h @@ -63,108 +63,129 @@ extern "C" { /**@brief GAP API SVC numbers. */ enum BLE_GAP_SVCS { - SD_BLE_GAP_ADDR_SET = BLE_GAP_SVC_BASE, /**< Set own Bluetooth Address. */ - SD_BLE_GAP_ADDR_GET = BLE_GAP_SVC_BASE + 1, /**< Get own Bluetooth Address. */ - SD_BLE_GAP_WHITELIST_SET = BLE_GAP_SVC_BASE + 2, /**< Set active whitelist. */ - SD_BLE_GAP_DEVICE_IDENTITIES_SET = BLE_GAP_SVC_BASE + 3, /**< Set device identity list. */ - SD_BLE_GAP_PRIVACY_SET = BLE_GAP_SVC_BASE + 4, /**< Set Privacy settings*/ - SD_BLE_GAP_PRIVACY_GET = BLE_GAP_SVC_BASE + 5, /**< Get Privacy settings*/ - SD_BLE_GAP_ADV_SET_CONFIGURE = BLE_GAP_SVC_BASE + 6, /**< Configure an advertising set. */ - SD_BLE_GAP_ADV_START = BLE_GAP_SVC_BASE + 7, /**< Start Advertising. */ - SD_BLE_GAP_ADV_STOP = BLE_GAP_SVC_BASE + 8, /**< Stop Advertising. */ - SD_BLE_GAP_CONN_PARAM_UPDATE = BLE_GAP_SVC_BASE + 9, /**< Connection Parameter Update. */ - SD_BLE_GAP_DISCONNECT = BLE_GAP_SVC_BASE + 10, /**< Disconnect. */ - SD_BLE_GAP_TX_POWER_SET = BLE_GAP_SVC_BASE + 11, /**< Set TX Power. */ - SD_BLE_GAP_APPEARANCE_SET = BLE_GAP_SVC_BASE + 12, /**< Set Appearance. */ - SD_BLE_GAP_APPEARANCE_GET = BLE_GAP_SVC_BASE + 13, /**< Get Appearance. */ - SD_BLE_GAP_PPCP_SET = BLE_GAP_SVC_BASE + 14, /**< Set PPCP. */ - SD_BLE_GAP_PPCP_GET = BLE_GAP_SVC_BASE + 15, /**< Get PPCP. */ - SD_BLE_GAP_DEVICE_NAME_SET = BLE_GAP_SVC_BASE + 16, /**< Set Device Name. */ - SD_BLE_GAP_DEVICE_NAME_GET = BLE_GAP_SVC_BASE + 17, /**< Get Device Name. */ - SD_BLE_GAP_AUTHENTICATE = BLE_GAP_SVC_BASE + 18, /**< Initiate Pairing/Bonding. */ - SD_BLE_GAP_SEC_PARAMS_REPLY = BLE_GAP_SVC_BASE + 19, /**< Reply with Security Parameters. */ - SD_BLE_GAP_AUTH_KEY_REPLY = BLE_GAP_SVC_BASE + 20, /**< Reply with an authentication key. */ - SD_BLE_GAP_LESC_DHKEY_REPLY = BLE_GAP_SVC_BASE + 21, /**< Reply with an LE Secure Connections DHKey. */ - SD_BLE_GAP_KEYPRESS_NOTIFY = BLE_GAP_SVC_BASE + 22, /**< Notify of a keypress during an authentication procedure. */ - SD_BLE_GAP_LESC_OOB_DATA_GET = BLE_GAP_SVC_BASE + 23, /**< Get the local LE Secure Connections OOB data. */ - SD_BLE_GAP_LESC_OOB_DATA_SET = BLE_GAP_SVC_BASE + 24, /**< Set the remote LE Secure Connections OOB data. */ - SD_BLE_GAP_ENCRYPT = BLE_GAP_SVC_BASE + 25, /**< Initiate encryption procedure. */ - SD_BLE_GAP_SEC_INFO_REPLY = BLE_GAP_SVC_BASE + 26, /**< Reply with Security Information. */ - SD_BLE_GAP_CONN_SEC_GET = BLE_GAP_SVC_BASE + 27, /**< Obtain connection security level. */ - SD_BLE_GAP_RSSI_START = BLE_GAP_SVC_BASE + 28, /**< Start reporting of changes in RSSI. */ - SD_BLE_GAP_RSSI_STOP = BLE_GAP_SVC_BASE + 29, /**< Stop reporting of changes in RSSI. */ - SD_BLE_GAP_SCAN_START = BLE_GAP_SVC_BASE + 30, /**< Start Scanning. */ - SD_BLE_GAP_SCAN_STOP = BLE_GAP_SVC_BASE + 31, /**< Stop Scanning. */ - SD_BLE_GAP_CONNECT = BLE_GAP_SVC_BASE + 32, /**< Connect. */ - SD_BLE_GAP_CONNECT_CANCEL = BLE_GAP_SVC_BASE + 33, /**< Cancel ongoing connection procedure. */ - SD_BLE_GAP_RSSI_GET = BLE_GAP_SVC_BASE + 34, /**< Get the last RSSI sample. */ - SD_BLE_GAP_PHY_UPDATE = BLE_GAP_SVC_BASE + 35, /**< Initiate or respond to a PHY Update Procedure. */ - SD_BLE_GAP_DATA_LENGTH_UPDATE = BLE_GAP_SVC_BASE + 36, /**< Initiate or respond to a Data Length Update Procedure. */ - SD_BLE_GAP_QOS_CHANNEL_SURVEY_START = BLE_GAP_SVC_BASE + 37, /**< Start Quality of Service (QoS) channel survey module. */ - SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP = BLE_GAP_SVC_BASE + 38, /**< Stop Quality of Service (QoS) channel survey module. */ - SD_BLE_GAP_ADV_ADDR_GET = BLE_GAP_SVC_BASE + 39, /**< Get the Address used on air while Advertising. */ - SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET = BLE_GAP_SVC_BASE + 40, /**< Get the next connection event counter. */ - SD_BLE_GAP_CONN_EVT_TRIGGER_START = BLE_GAP_SVC_BASE + 41, /** Start triggering a given task on connection event start. */ - SD_BLE_GAP_CONN_EVT_TRIGGER_STOP = BLE_GAP_SVC_BASE + 42, /** Stop triggering the task configured using @ref sd_ble_gap_conn_evt_trigger_start. */ + SD_BLE_GAP_ADDR_SET = BLE_GAP_SVC_BASE, /**< Set own Bluetooth Address. */ + SD_BLE_GAP_ADDR_GET = BLE_GAP_SVC_BASE + 1, /**< Get own Bluetooth Address. */ + SD_BLE_GAP_WHITELIST_SET = BLE_GAP_SVC_BASE + 2, /**< Set active whitelist. */ + SD_BLE_GAP_DEVICE_IDENTITIES_SET = BLE_GAP_SVC_BASE + 3, /**< Set device identity list. */ + SD_BLE_GAP_PRIVACY_SET = BLE_GAP_SVC_BASE + 4, /**< Set Privacy settings*/ + SD_BLE_GAP_PRIVACY_GET = BLE_GAP_SVC_BASE + 5, /**< Get Privacy settings*/ + SD_BLE_GAP_ADV_SET_CONFIGURE = BLE_GAP_SVC_BASE + 6, /**< Configure an advertising set. */ + SD_BLE_GAP_ADV_START = BLE_GAP_SVC_BASE + 7, /**< Start Advertising. */ + SD_BLE_GAP_ADV_STOP = BLE_GAP_SVC_BASE + 8, /**< Stop Advertising. */ + SD_BLE_GAP_CONN_PARAM_UPDATE = BLE_GAP_SVC_BASE + 9, /**< Connection Parameter Update. */ + SD_BLE_GAP_DISCONNECT = BLE_GAP_SVC_BASE + 10, /**< Disconnect. */ + SD_BLE_GAP_TX_POWER_SET = BLE_GAP_SVC_BASE + 11, /**< Set TX Power. */ + SD_BLE_GAP_APPEARANCE_SET = BLE_GAP_SVC_BASE + 12, /**< Set Appearance. */ + SD_BLE_GAP_APPEARANCE_GET = BLE_GAP_SVC_BASE + 13, /**< Get Appearance. */ + SD_BLE_GAP_PPCP_SET = BLE_GAP_SVC_BASE + 14, /**< Set PPCP. */ + SD_BLE_GAP_PPCP_GET = BLE_GAP_SVC_BASE + 15, /**< Get PPCP. */ + SD_BLE_GAP_DEVICE_NAME_SET = BLE_GAP_SVC_BASE + 16, /**< Set Device Name. */ + SD_BLE_GAP_DEVICE_NAME_GET = BLE_GAP_SVC_BASE + 17, /**< Get Device Name. */ + SD_BLE_GAP_AUTHENTICATE = BLE_GAP_SVC_BASE + 18, /**< Initiate Pairing/Bonding. */ + SD_BLE_GAP_SEC_PARAMS_REPLY = BLE_GAP_SVC_BASE + 19, /**< Reply with Security Parameters. */ + SD_BLE_GAP_AUTH_KEY_REPLY = BLE_GAP_SVC_BASE + 20, /**< Reply with an authentication key. */ + SD_BLE_GAP_LESC_DHKEY_REPLY = BLE_GAP_SVC_BASE + 21, /**< Reply with an LE Secure Connections DHKey. */ + SD_BLE_GAP_KEYPRESS_NOTIFY = BLE_GAP_SVC_BASE + 22, /**< Notify of a keypress during an authentication procedure. */ + SD_BLE_GAP_LESC_OOB_DATA_GET = BLE_GAP_SVC_BASE + 23, /**< Get the local LE Secure Connections OOB data. */ + SD_BLE_GAP_LESC_OOB_DATA_SET = BLE_GAP_SVC_BASE + 24, /**< Set the remote LE Secure Connections OOB data. */ + SD_BLE_GAP_ENCRYPT = BLE_GAP_SVC_BASE + 25, /**< Initiate encryption procedure. */ + SD_BLE_GAP_SEC_INFO_REPLY = BLE_GAP_SVC_BASE + 26, /**< Reply with Security Information. */ + SD_BLE_GAP_CONN_SEC_GET = BLE_GAP_SVC_BASE + 27, /**< Obtain connection security level. */ + SD_BLE_GAP_RSSI_START = BLE_GAP_SVC_BASE + 28, /**< Start reporting of changes in RSSI. */ + SD_BLE_GAP_RSSI_STOP = BLE_GAP_SVC_BASE + 29, /**< Stop reporting of changes in RSSI. */ + SD_BLE_GAP_SCAN_START = BLE_GAP_SVC_BASE + 30, /**< Start Scanning. */ + SD_BLE_GAP_SCAN_STOP = BLE_GAP_SVC_BASE + 31, /**< Stop Scanning. */ + SD_BLE_GAP_CONNECT = BLE_GAP_SVC_BASE + 32, /**< Connect. */ + SD_BLE_GAP_CONNECT_CANCEL = BLE_GAP_SVC_BASE + 33, /**< Cancel ongoing connection procedure. */ + SD_BLE_GAP_RSSI_GET = BLE_GAP_SVC_BASE + 34, /**< Get the last RSSI sample. */ + SD_BLE_GAP_PHY_UPDATE = BLE_GAP_SVC_BASE + 35, /**< Initiate or respond to a PHY Update Procedure. */ + SD_BLE_GAP_DATA_LENGTH_UPDATE = BLE_GAP_SVC_BASE + 36, /**< Initiate or respond to a Data Length Update Procedure. */ + SD_BLE_GAP_QOS_CHANNEL_SURVEY_START = BLE_GAP_SVC_BASE + 37, /**< Start Quality of Service (QoS) channel survey module. */ + SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP = BLE_GAP_SVC_BASE + 38, /**< Stop Quality of Service (QoS) channel survey module. */ + SD_BLE_GAP_ADV_ADDR_GET = BLE_GAP_SVC_BASE + 39, /**< Get the Address used on air while Advertising. */ + SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET = BLE_GAP_SVC_BASE + 40, /**< Get the next connection event counter. */ + SD_BLE_GAP_CONN_EVT_TRIGGER_START = BLE_GAP_SVC_BASE + 41, /** Start triggering a given task on connection event start. */ + SD_BLE_GAP_CONN_EVT_TRIGGER_STOP = + BLE_GAP_SVC_BASE + 42, /** Stop triggering the task configured using @ref sd_ble_gap_conn_evt_trigger_start. */ }; /**@brief GAP Event IDs. * IDs that uniquely identify an event coming from the stack to the application. */ enum BLE_GAP_EVTS { - BLE_GAP_EVT_CONNECTED = BLE_GAP_EVT_BASE, /**< Connected to peer. \n See @ref ble_gap_evt_connected_t */ - BLE_GAP_EVT_DISCONNECTED = BLE_GAP_EVT_BASE + 1, /**< Disconnected from peer. \n See @ref ble_gap_evt_disconnected_t. */ - BLE_GAP_EVT_CONN_PARAM_UPDATE = - BLE_GAP_EVT_BASE + 2, /**< Connection Parameters updated. \n See @ref ble_gap_evt_conn_param_update_t. */ - BLE_GAP_EVT_SEC_PARAMS_REQUEST = BLE_GAP_EVT_BASE + 3, /**< Request to provide security parameters. \n Reply with @ref - sd_ble_gap_sec_params_reply. \n See @ref ble_gap_evt_sec_params_request_t. */ - BLE_GAP_EVT_SEC_INFO_REQUEST = BLE_GAP_EVT_BASE + 4, /**< Request to provide security information. \n Reply with @ref - sd_ble_gap_sec_info_reply. \n See @ref ble_gap_evt_sec_info_request_t. */ - BLE_GAP_EVT_PASSKEY_DISPLAY = BLE_GAP_EVT_BASE + 5, /**< Request to display a passkey to the user. \n In LESC Numeric Comparison, reply - with @ref sd_ble_gap_auth_key_reply. \n See @ref ble_gap_evt_passkey_display_t. */ - BLE_GAP_EVT_KEY_PRESSED = BLE_GAP_EVT_BASE + 6, /**< Notification of a keypress on the remote device.\n See @ref ble_gap_evt_key_pressed_t */ - BLE_GAP_EVT_AUTH_KEY_REQUEST = BLE_GAP_EVT_BASE + 7, /**< Request to provide an authentication key. \n Reply with @ref - sd_ble_gap_auth_key_reply. \n See @ref ble_gap_evt_auth_key_request_t. */ - BLE_GAP_EVT_LESC_DHKEY_REQUEST = BLE_GAP_EVT_BASE + 8, /**< Request to calculate an LE Secure Connections DHKey. \n Reply with @ref - sd_ble_gap_lesc_dhkey_reply. \n See @ref ble_gap_evt_lesc_dhkey_request_t */ - BLE_GAP_EVT_AUTH_STATUS = BLE_GAP_EVT_BASE + 9, /**< Authentication procedure completed with status. \n See @ref ble_gap_evt_auth_status_t. */ - BLE_GAP_EVT_CONN_SEC_UPDATE = - BLE_GAP_EVT_BASE + 10, /**< Connection security updated. \n See @ref ble_gap_evt_conn_sec_update_t. */ - BLE_GAP_EVT_TIMEOUT = BLE_GAP_EVT_BASE + 11, /**< Timeout expired. \n See @ref ble_gap_evt_timeout_t. */ - BLE_GAP_EVT_RSSI_CHANGED = BLE_GAP_EVT_BASE + 12, /**< RSSI report. \n See @ref ble_gap_evt_rssi_changed_t. */ - BLE_GAP_EVT_ADV_REPORT = BLE_GAP_EVT_BASE + 13, /**< Advertising report. \n See @ref ble_gap_evt_adv_report_t. */ - BLE_GAP_EVT_SEC_REQUEST = BLE_GAP_EVT_BASE + 14, /**< Security Request. \n Reply with - @ref sd_ble_gap_authenticate \n or with @ref sd_ble_gap_encrypt if required security information is - available . \n See @ref ble_gap_evt_sec_request_t. */ - BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST = - BLE_GAP_EVT_BASE + 15, /**< Connection Parameter Update Request. \n Reply with @ref - sd_ble_gap_conn_param_update. \n See @ref ble_gap_evt_conn_param_update_request_t. */ - BLE_GAP_EVT_SCAN_REQ_REPORT = - BLE_GAP_EVT_BASE + 16, /**< Scan request report. \n See @ref ble_gap_evt_scan_req_report_t. */ - BLE_GAP_EVT_PHY_UPDATE_REQUEST = BLE_GAP_EVT_BASE + 17, /**< PHY Update Request. \n Reply with @ref - sd_ble_gap_phy_update. \n See @ref ble_gap_evt_phy_update_request_t. */ - BLE_GAP_EVT_PHY_UPDATE = BLE_GAP_EVT_BASE + 18, /**< PHY Update Procedure is complete. \n See @ref ble_gap_evt_phy_update_t. */ - BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST = - BLE_GAP_EVT_BASE + 19, /**< Data Length Update Request. \n Reply with @ref - sd_ble_gap_data_length_update. \n See @ref ble_gap_evt_data_length_update_request_t. */ - BLE_GAP_EVT_DATA_LENGTH_UPDATE = - BLE_GAP_EVT_BASE + 20, /**< LL Data Channel PDU payload length updated. \n See @ref ble_gap_evt_data_length_update_t. */ - BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT = - BLE_GAP_EVT_BASE + 21, /**< Channel survey report. \n See @ref ble_gap_evt_qos_channel_survey_report_t. */ - BLE_GAP_EVT_ADV_SET_TERMINATED = - BLE_GAP_EVT_BASE + 22, /**< Advertising set terminated. \n See @ref ble_gap_evt_adv_set_terminated_t. */ + BLE_GAP_EVT_CONNECTED = + BLE_GAP_EVT_BASE, /**< Connected to peer. \n See @ref ble_gap_evt_connected_t */ + BLE_GAP_EVT_DISCONNECTED = + BLE_GAP_EVT_BASE + 1, /**< Disconnected from peer. \n See @ref ble_gap_evt_disconnected_t. */ + BLE_GAP_EVT_CONN_PARAM_UPDATE = + BLE_GAP_EVT_BASE + 2, /**< Connection Parameters updated. \n See @ref ble_gap_evt_conn_param_update_t. */ + BLE_GAP_EVT_SEC_PARAMS_REQUEST = + BLE_GAP_EVT_BASE + 3, /**< Request to provide security parameters. \n Reply with @ref sd_ble_gap_sec_params_reply. + \n See @ref ble_gap_evt_sec_params_request_t. */ + BLE_GAP_EVT_SEC_INFO_REQUEST = + BLE_GAP_EVT_BASE + 4, /**< Request to provide security information. \n Reply with @ref sd_ble_gap_sec_info_reply. + \n See @ref ble_gap_evt_sec_info_request_t. */ + BLE_GAP_EVT_PASSKEY_DISPLAY = + BLE_GAP_EVT_BASE + 5, /**< Request to display a passkey to the user. \n In LESC Numeric Comparison, reply with @ref + sd_ble_gap_auth_key_reply. \n See @ref ble_gap_evt_passkey_display_t. */ + BLE_GAP_EVT_KEY_PRESSED = + BLE_GAP_EVT_BASE + 6, /**< Notification of a keypress on the remote device.\n See @ref ble_gap_evt_key_pressed_t */ + BLE_GAP_EVT_AUTH_KEY_REQUEST = + BLE_GAP_EVT_BASE + 7, /**< Request to provide an authentication key. \n Reply with @ref sd_ble_gap_auth_key_reply. + \n See @ref ble_gap_evt_auth_key_request_t. */ + BLE_GAP_EVT_LESC_DHKEY_REQUEST = + BLE_GAP_EVT_BASE + 8, /**< Request to calculate an LE Secure Connections DHKey. \n Reply with @ref + sd_ble_gap_lesc_dhkey_reply. \n See @ref ble_gap_evt_lesc_dhkey_request_t */ + BLE_GAP_EVT_AUTH_STATUS = + BLE_GAP_EVT_BASE + 9, /**< Authentication procedure completed with status. \n See @ref ble_gap_evt_auth_status_t. */ + BLE_GAP_EVT_CONN_SEC_UPDATE = + BLE_GAP_EVT_BASE + 10, /**< Connection security updated. \n See @ref ble_gap_evt_conn_sec_update_t. */ + BLE_GAP_EVT_TIMEOUT = + BLE_GAP_EVT_BASE + 11, /**< Timeout expired. \n See @ref ble_gap_evt_timeout_t. */ + BLE_GAP_EVT_RSSI_CHANGED = + BLE_GAP_EVT_BASE + 12, /**< RSSI report. \n See @ref ble_gap_evt_rssi_changed_t. */ + BLE_GAP_EVT_ADV_REPORT = + BLE_GAP_EVT_BASE + 13, /**< Advertising report. \n See @ref ble_gap_evt_adv_report_t. */ + BLE_GAP_EVT_SEC_REQUEST = + BLE_GAP_EVT_BASE + 14, /**< Security Request. \n Reply with @ref sd_ble_gap_authenticate +\n or with @ref sd_ble_gap_encrypt if required security information is available +. \n See @ref ble_gap_evt_sec_request_t. */ + BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST = + BLE_GAP_EVT_BASE + 15, /**< Connection Parameter Update Request. \n Reply with @ref + sd_ble_gap_conn_param_update. \n See @ref ble_gap_evt_conn_param_update_request_t. */ + BLE_GAP_EVT_SCAN_REQ_REPORT = + BLE_GAP_EVT_BASE + 16, /**< Scan request report. \n See @ref ble_gap_evt_scan_req_report_t. */ + BLE_GAP_EVT_PHY_UPDATE_REQUEST = + BLE_GAP_EVT_BASE + 17, /**< PHY Update Request. \n Reply with @ref sd_ble_gap_phy_update. \n + See @ref ble_gap_evt_phy_update_request_t. */ + BLE_GAP_EVT_PHY_UPDATE = + BLE_GAP_EVT_BASE + 18, /**< PHY Update Procedure is complete. \n See @ref ble_gap_evt_phy_update_t. */ + BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST = + BLE_GAP_EVT_BASE + 19, /**< Data Length Update Request. \n Reply with @ref + sd_ble_gap_data_length_update. \n See @ref ble_gap_evt_data_length_update_request_t. */ + BLE_GAP_EVT_DATA_LENGTH_UPDATE = + BLE_GAP_EVT_BASE + + 20, /**< LL Data Channel PDU payload length updated. \n See @ref ble_gap_evt_data_length_update_t. */ + BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT = + BLE_GAP_EVT_BASE + + 21, /**< Channel survey report. \n See @ref ble_gap_evt_qos_channel_survey_report_t. */ + BLE_GAP_EVT_ADV_SET_TERMINATED = + BLE_GAP_EVT_BASE + + 22, /**< Advertising set terminated. \n See @ref ble_gap_evt_adv_set_terminated_t. */ }; /**@brief GAP Option IDs. * IDs that uniquely identify a GAP option. */ enum BLE_GAP_OPTS { - BLE_GAP_OPT_CH_MAP = BLE_GAP_OPT_BASE, /**< Channel Map. @ref ble_gap_opt_ch_map_t */ - BLE_GAP_OPT_LOCAL_CONN_LATENCY = BLE_GAP_OPT_BASE + 1, /**< Local connection latency. @ref ble_gap_opt_local_conn_latency_t */ - BLE_GAP_OPT_PASSKEY = BLE_GAP_OPT_BASE + 2, /**< Set passkey. @ref ble_gap_opt_passkey_t */ - BLE_GAP_OPT_COMPAT_MODE_1 = BLE_GAP_OPT_BASE + 3, /**< Compatibility mode. @ref ble_gap_opt_compat_mode_1_t */ - BLE_GAP_OPT_AUTH_PAYLOAD_TIMEOUT = BLE_GAP_OPT_BASE + 4, /**< Set Authenticated payload timeout. @ref ble_gap_opt_auth_payload_timeout_t */ - BLE_GAP_OPT_SLAVE_LATENCY_DISABLE = BLE_GAP_OPT_BASE + 5, /**< Disable slave latency. @ref ble_gap_opt_slave_latency_disable_t */ + BLE_GAP_OPT_CH_MAP = BLE_GAP_OPT_BASE, /**< Channel Map. @ref ble_gap_opt_ch_map_t */ + BLE_GAP_OPT_LOCAL_CONN_LATENCY = BLE_GAP_OPT_BASE + 1, /**< Local connection latency. @ref ble_gap_opt_local_conn_latency_t */ + BLE_GAP_OPT_PASSKEY = BLE_GAP_OPT_BASE + 2, /**< Set passkey. @ref ble_gap_opt_passkey_t */ + BLE_GAP_OPT_COMPAT_MODE_1 = BLE_GAP_OPT_BASE + 3, /**< Compatibility mode. @ref ble_gap_opt_compat_mode_1_t */ + BLE_GAP_OPT_AUTH_PAYLOAD_TIMEOUT = + BLE_GAP_OPT_BASE + 4, /**< Set Authenticated payload timeout. @ref ble_gap_opt_auth_payload_timeout_t */ + BLE_GAP_OPT_SLAVE_LATENCY_DISABLE = + BLE_GAP_OPT_BASE + 5, /**< Disable slave latency. @ref ble_gap_opt_slave_latency_disable_t */ }; /**@brief GAP Configuration IDs. @@ -172,20 +193,20 @@ enum BLE_GAP_OPTS { * IDs that uniquely identify a GAP configuration. */ enum BLE_GAP_CFGS { - BLE_GAP_CFG_ROLE_COUNT = BLE_GAP_CFG_BASE, /**< Role count configuration. */ - BLE_GAP_CFG_DEVICE_NAME = BLE_GAP_CFG_BASE + 1, /**< Device name configuration. */ - BLE_GAP_CFG_PPCP_INCL_CONFIG = BLE_GAP_CFG_BASE + 2, /**< Peripheral Preferred Connection Parameters characteristic - inclusion configuration. */ - BLE_GAP_CFG_CAR_INCL_CONFIG = BLE_GAP_CFG_BASE + 3, /**< Central Address Resolution characteristic - inclusion configuration. */ + BLE_GAP_CFG_ROLE_COUNT = BLE_GAP_CFG_BASE, /**< Role count configuration. */ + BLE_GAP_CFG_DEVICE_NAME = BLE_GAP_CFG_BASE + 1, /**< Device name configuration. */ + BLE_GAP_CFG_PPCP_INCL_CONFIG = BLE_GAP_CFG_BASE + 2, /**< Peripheral Preferred Connection Parameters characteristic + inclusion configuration. */ + BLE_GAP_CFG_CAR_INCL_CONFIG = BLE_GAP_CFG_BASE + 3, /**< Central Address Resolution characteristic + inclusion configuration. */ }; /**@brief GAP TX Power roles. */ enum BLE_GAP_TX_POWER_ROLES { - BLE_GAP_TX_POWER_ROLE_ADV = 1, /**< Advertiser role. */ - BLE_GAP_TX_POWER_ROLE_SCAN_INIT = 2, /**< Scanner and initiator role. */ - BLE_GAP_TX_POWER_ROLE_CONN = 3, /**< Connection role. */ + BLE_GAP_TX_POWER_ROLE_ADV = 1, /**< Advertiser role. */ + BLE_GAP_TX_POWER_ROLE_SCAN_INIT = 2, /**< Scanner and initiator role. */ + BLE_GAP_TX_POWER_ROLE_CONN = 3, /**< Connection role. */ }; /** @} */ @@ -195,16 +216,18 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_ERRORS_GAP SVC return values specific to GAP * @{ */ -#define BLE_ERROR_GAP_UUID_LIST_MISMATCH (NRF_GAP_ERR_BASE + 0x000) /**< UUID list does not contain an integral number of UUIDs. */ -#define BLE_ERROR_GAP_DISCOVERABLE_WITH_WHITELIST (NRF_GAP_ERR_BASE + 0x001) /**< Use of Whitelist not permitted with discoverable advertising. */ -#define BLE_ERROR_GAP_INVALID_BLE_ADDR \ - (NRF_GAP_ERR_BASE + 0x002) /**< The upper two bits of the address do not correspond to the specified address type. \ - */ -#define BLE_ERROR_GAP_WHITELIST_IN_USE (NRF_GAP_ERR_BASE + 0x003) /**< Attempt to modify the whitelist while already in use by another operation. */ -#define BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE \ - (NRF_GAP_ERR_BASE + 0x004) /**< Attempt to modify the device identity list while already in use by another operation. */ -#define BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE \ - (NRF_GAP_ERR_BASE + 0x005) /**< The device identity list contains entries with duplicate identity addresses. */ +#define BLE_ERROR_GAP_UUID_LIST_MISMATCH \ + (NRF_GAP_ERR_BASE + 0x000) /**< UUID list does not contain an integral number of UUIDs. */ +#define BLE_ERROR_GAP_DISCOVERABLE_WITH_WHITELIST \ + (NRF_GAP_ERR_BASE + 0x001) /**< Use of Whitelist not permitted with discoverable advertising. */ +#define BLE_ERROR_GAP_INVALID_BLE_ADDR \ + (NRF_GAP_ERR_BASE + 0x002) /**< The upper two bits of the address do not correspond to the specified address type. */ +#define BLE_ERROR_GAP_WHITELIST_IN_USE \ + (NRF_GAP_ERR_BASE + 0x003) /**< Attempt to modify the whitelist while already in use by another operation. */ +#define BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE \ + (NRF_GAP_ERR_BASE + 0x004) /**< Attempt to modify the device identity list while already in use by another operation. */ +#define BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE \ + (NRF_GAP_ERR_BASE + 0x005) /**< The device identity list contains entries with duplicate identity addresses. */ /**@} */ /**@defgroup BLE_GAP_ROLES GAP Roles @@ -227,9 +250,9 @@ enum BLE_GAP_TX_POWER_ROLES { #define BLE_GAP_ADDR_TYPE_RANDOM_STATIC 0x01 /**< Random static (identity) address. */ #define BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE 0x02 /**< Random private resolvable address. */ #define BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE 0x03 /**< Random private non-resolvable address. */ -#define BLE_GAP_ADDR_TYPE_ANONYMOUS \ - 0x7F /**< An advertiser may advertise without its address. \ - This type of advertising is called anonymous. */ +#define BLE_GAP_ADDR_TYPE_ANONYMOUS \ + 0x7F /**< An advertiser may advertise without its address. \ + This type of advertising is called anonymous. */ /**@} */ /**@brief The default interval in seconds at which a private address is refreshed. */ @@ -244,10 +267,10 @@ enum BLE_GAP_TX_POWER_ROLES { * @{ */ #define BLE_GAP_PRIVACY_MODE_OFF 0x00 /**< Device will send and accept its identity address for its own address. */ #define BLE_GAP_PRIVACY_MODE_DEVICE_PRIVACY 0x01 /**< Device will send and accept only private addresses for its own address. */ -#define BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY \ - 0x02 /**< Device will send and accept only private addresses for its own address, \ - and will not accept a peer using identity address as sender address when \ - the peer IRK is exchanged, non-zero and added to the identity list. */ +#define BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY \ + 0x02 /**< Device will send and accept only private addresses for its own address, \ + and will not accept a peer using identity address as sender address when \ + the peer IRK is exchanged, non-zero and added to the identity list. */ /**@} */ /** @brief Invalid power level. */ @@ -264,13 +287,14 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_ADV_SET_DATA_SIZES Advertising data sizes. * @{ */ -#define BLE_GAP_ADV_SET_DATA_SIZE_MAX \ - (31) /**< Maximum data length for an advertising set. \ - If more advertising data is required, use extended advertising instead. */ -#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED (255) /**< Maximum supported data length for an extended advertising set. */ +#define BLE_GAP_ADV_SET_DATA_SIZE_MAX \ + (31) /**< Maximum data length for an advertising set. \ + If more advertising data is required, use extended advertising instead. */ +#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED \ + (255) /**< Maximum supported data length for an extended advertising set. */ -#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_CONNECTABLE_MAX_SUPPORTED \ - (238) /**< Maximum supported data length for an extended connectable advertising set. */ +#define BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_CONNECTABLE_MAX_SUPPORTED \ + (238) /**< Maximum supported data length for an extended connectable advertising set. */ /**@}. */ /** @brief Set ID not available in advertising report. */ @@ -328,10 +352,12 @@ enum BLE_GAP_TX_POWER_ROLES { #define BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED (0x04) /**< BR/EDR not supported. */ #define BLE_GAP_ADV_FLAG_LE_BR_EDR_CONTROLLER (0x08) /**< Simultaneous LE and BR/EDR, Controller. */ #define BLE_GAP_ADV_FLAG_LE_BR_EDR_HOST (0x10) /**< Simultaneous LE and BR/EDR, Host. */ -#define BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE \ - (BLE_GAP_ADV_FLAG_LE_LIMITED_DISC_MODE | BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE Limited Discoverable Mode, BR/EDR not supported. */ -#define BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE \ - (BLE_GAP_ADV_FLAG_LE_GENERAL_DISC_MODE | BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE General Discoverable Mode, BR/EDR not supported. */ +#define BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE \ + (BLE_GAP_ADV_FLAG_LE_LIMITED_DISC_MODE | \ + BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE Limited Discoverable Mode, BR/EDR not supported. */ +#define BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE \ + (BLE_GAP_ADV_FLAG_LE_GENERAL_DISC_MODE | \ + BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED) /**< LE General Discoverable Mode, BR/EDR not supported. */ /**@} */ /**@defgroup BLE_GAP_ADV_INTERVALS GAP Advertising interval max and min @@ -364,21 +390,21 @@ enum BLE_GAP_TX_POWER_ROLES { * If ble_gap_scan_params_t::extended is set to 0, @ref BLE_GAP_SCAN_BUFFER_MIN is the minimum scan buffer length. * else the minimum scan buffer size is @ref BLE_GAP_SCAN_BUFFER_EXTENDED_MIN. * @{ */ -#define BLE_GAP_SCAN_BUFFER_MIN \ - (31) /**< Minimum data length for an \ - advertising set. */ -#define BLE_GAP_SCAN_BUFFER_MAX \ - (31) /**< Maximum data length for an \ - advertising set. */ -#define BLE_GAP_SCAN_BUFFER_EXTENDED_MIN \ - (255) /**< Minimum data length for an \ - extended advertising set. */ -#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX \ - (1650) /**< Maximum data length for an \ - extended advertising set. */ -#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED \ - (255) /**< Maximum supported data length for \ - an extended advertising set. */ +#define BLE_GAP_SCAN_BUFFER_MIN \ + (31) /**< Minimum data length for an \ + advertising set. */ +#define BLE_GAP_SCAN_BUFFER_MAX \ + (31) /**< Maximum data length for an \ + advertising set. */ +#define BLE_GAP_SCAN_BUFFER_EXTENDED_MIN \ + (255) /**< Minimum data length for an \ + extended advertising set. */ +#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX \ + (1650) /**< Maximum data length for an \ + extended advertising set. */ +#define BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED \ + (255) /**< Maximum supported data length for \ + an extended advertising set. */ /** @} */ /**@defgroup BLE_GAP_ADV_TYPES GAP Advertising types @@ -392,44 +418,44 @@ enum BLE_GAP_TX_POWER_ROLES { * scan response data. * * @{ */ -#define BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED \ - 0x01 /**< Connectable and scannable undirected \ - advertising events. */ -#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE \ - 0x02 /**< Connectable non-scannable directed advertising \ - events. Advertising interval is less that 3.75 ms. \ - Use this type for fast reconnections. \ - @note Advertising data is not supported. */ -#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED \ - 0x03 /**< Connectable non-scannable directed advertising \ - events. \ - @note Advertising data is not supported. */ -#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ - 0x04 /**< Non-connectable scannable undirected \ - advertising events. */ -#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ - 0x05 /**< Non-connectable non-scannable undirected \ - advertising events. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_UNDIRECTED \ - 0x06 /**< Connectable non-scannable undirected advertising \ - events using extended advertising PDUs. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_DIRECTED \ - 0x07 /**< Connectable non-scannable directed advertising \ - events using extended advertising PDUs. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ - 0x08 /**< Non-connectable scannable undirected advertising \ - events using extended advertising PDUs. \ - @note Only scan response data is supported. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_DIRECTED \ - 0x09 /**< Non-connectable scannable directed advertising \ - events using extended advertising PDUs. \ - @note Only scan response data is supported. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ - 0x0A /**< Non-connectable non-scannable undirected advertising \ - events using extended advertising PDUs. */ -#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED \ - 0x0B /**< Non-connectable non-scannable directed advertising \ - events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED \ + 0x01 /**< Connectable and scannable undirected \ + advertising events. */ +#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE \ + 0x02 /**< Connectable non-scannable directed advertising \ + events. Advertising interval is less that 3.75 ms. \ + Use this type for fast reconnections. \ + @note Advertising data is not supported. */ +#define BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED \ + 0x03 /**< Connectable non-scannable directed advertising \ + events. \ + @note Advertising data is not supported. */ +#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ + 0x04 /**< Non-connectable scannable undirected \ + advertising events. */ +#define BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ + 0x05 /**< Non-connectable non-scannable undirected \ + advertising events. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_UNDIRECTED \ + 0x06 /**< Connectable non-scannable undirected advertising \ + events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_CONNECTABLE_NONSCANNABLE_DIRECTED \ + 0x07 /**< Connectable non-scannable directed advertising \ + events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_UNDIRECTED \ + 0x08 /**< Non-connectable scannable undirected advertising \ + events using extended advertising PDUs. \ + @note Only scan response data is supported. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_SCANNABLE_DIRECTED \ + 0x09 /**< Non-connectable scannable directed advertising \ + events using extended advertising PDUs. \ + @note Only scan response data is supported. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED \ + 0x0A /**< Non-connectable non-scannable undirected advertising \ + events using extended advertising PDUs. */ +#define BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED \ + 0x0B /**< Non-connectable non-scannable directed advertising \ + events using extended advertising PDUs. */ /**@} */ /**@defgroup BLE_GAP_ADV_FILTER_POLICIES GAP Advertising filter policies @@ -443,50 +469,50 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_ADV_DATA_STATUS GAP Advertising data status * @{ */ #define BLE_GAP_ADV_DATA_STATUS_COMPLETE 0x00 /**< All data in the advertising event have been received. */ -#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA \ - 0x01 /**< More data to be received. \ - @note This value will only be used if \ - @ref ble_gap_scan_params_t::report_incomplete_evts and \ - @ref ble_gap_adv_report_type_t::extended_pdu are set to true. */ -#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED \ - 0x02 /**< Incomplete data. Buffer size insufficient to receive more. \ - @note This value will only be used if \ - @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ -#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MISSED \ - 0x03 /**< Failed to receive the remaining data. \ - @note This value will only be used if \ - @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ +#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA \ + 0x01 /**< More data to be received. \ + @note This value will only be used if \ + @ref ble_gap_scan_params_t::report_incomplete_evts and \ + @ref ble_gap_adv_report_type_t::extended_pdu are set to true. */ +#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED \ + 0x02 /**< Incomplete data. Buffer size insufficient to receive more. \ + @note This value will only be used if \ + @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ +#define BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MISSED \ + 0x03 /**< Failed to receive the remaining data. \ + @note This value will only be used if \ + @ref ble_gap_adv_report_type_t::extended_pdu is set to true. */ /**@} */ /**@defgroup BLE_GAP_SCAN_FILTER_POLICIES GAP Scanner filter policies * @{ */ -#define BLE_GAP_SCAN_FP_ACCEPT_ALL \ - 0x00 /**< Accept all advertising packets except directed advertising packets \ - not addressed to this device. */ -#define BLE_GAP_SCAN_FP_WHITELIST \ - 0x01 /**< Accept advertising packets from devices in the whitelist except directed \ - packets not addressed to this device. */ -#define BLE_GAP_SCAN_FP_ALL_NOT_RESOLVED_DIRECTED \ - 0x02 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_ACCEPT_ALL. \ - In addition, accept directed advertising packets, where the advertiser's \ - address is a resolvable private address that cannot be resolved. */ -#define BLE_GAP_SCAN_FP_WHITELIST_NOT_RESOLVED_DIRECTED \ - 0x03 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_WHITELIST. \ - In addition, accept directed advertising packets, where the advertiser's \ - address is a resolvable private address that cannot be resolved. */ +#define BLE_GAP_SCAN_FP_ACCEPT_ALL \ + 0x00 /**< Accept all advertising packets except directed advertising packets \ + not addressed to this device. */ +#define BLE_GAP_SCAN_FP_WHITELIST \ + 0x01 /**< Accept advertising packets from devices in the whitelist except directed \ + packets not addressed to this device. */ +#define BLE_GAP_SCAN_FP_ALL_NOT_RESOLVED_DIRECTED \ + 0x02 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_ACCEPT_ALL. \ + In addition, accept directed advertising packets, where the advertiser's \ + address is a resolvable private address that cannot be resolved. */ +#define BLE_GAP_SCAN_FP_WHITELIST_NOT_RESOLVED_DIRECTED \ + 0x03 /**< Accept all advertising packets specified in @ref BLE_GAP_SCAN_FP_WHITELIST. \ + In addition, accept directed advertising packets, where the advertiser's \ + address is a resolvable private address that cannot be resolved. */ /**@} */ /**@defgroup BLE_GAP_ADV_TIMEOUT_VALUES GAP Advertising timeout values in 10 ms units * @{ */ -#define BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX \ - (128) /**< Maximum high duty advertising time in 10 ms units. Corresponds to 1.28 s. \ - */ -#define BLE_GAP_ADV_TIMEOUT_LIMITED_MAX \ - (18000) /**< Maximum advertising time in 10 ms units corresponding to TGAP(lim_adv_timeout) = 180 s in limited \ - discoverable mode. */ -#define BLE_GAP_ADV_TIMEOUT_GENERAL_UNLIMITED \ - (0) /**< Unlimited advertising in general discoverable mode. \ - For high duty cycle advertising, this corresponds to @ref BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX. */ +#define BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX \ + (128) /**< Maximum high duty advertising time in 10 ms units. Corresponds to 1.28 s. \ + */ +#define BLE_GAP_ADV_TIMEOUT_LIMITED_MAX \ + (18000) /**< Maximum advertising time in 10 ms units corresponding to TGAP(lim_adv_timeout) = 180 s in limited discoverable \ + mode. */ +#define BLE_GAP_ADV_TIMEOUT_GENERAL_UNLIMITED \ + (0) /**< Unlimited advertising in general discoverable mode. \ + For high duty cycle advertising, this corresponds to @ref BLE_GAP_ADV_TIMEOUT_HIGH_DUTY_MAX. */ /**@} */ /**@defgroup BLE_GAP_DISC_MODES GAP Discovery modes @@ -555,16 +581,18 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_CP_LIMITS GAP Connection Parameters Limits * @{ */ #define BLE_GAP_CP_MIN_CONN_INTVL_NONE 0xFFFF /**< No new minimum connection interval specified in connect parameters. */ -#define BLE_GAP_CP_MIN_CONN_INTVL_MIN 0x0006 /**< Lowest minimum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ -#define BLE_GAP_CP_MIN_CONN_INTVL_MAX \ - 0x0C80 /**< Highest minimum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ +#define BLE_GAP_CP_MIN_CONN_INTVL_MIN \ + 0x0006 /**< Lowest minimum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ +#define BLE_GAP_CP_MIN_CONN_INTVL_MAX \ + 0x0C80 /**< Highest minimum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ */ #define BLE_GAP_CP_MAX_CONN_INTVL_NONE 0xFFFF /**< No new maximum connection interval specified in connect parameters. */ -#define BLE_GAP_CP_MAX_CONN_INTVL_MIN 0x0006 /**< Lowest maximum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ -#define BLE_GAP_CP_MAX_CONN_INTVL_MAX \ - 0x0C80 /**< Highest maximum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ - */ -#define BLE_GAP_CP_SLAVE_LATENCY_MAX 0x01F3 /**< Highest slave latency permitted, in connection events. */ +#define BLE_GAP_CP_MAX_CONN_INTVL_MIN \ + 0x0006 /**< Lowest maximum connection interval permitted, in units of 1.25 ms, i.e. 7.5 ms. */ +#define BLE_GAP_CP_MAX_CONN_INTVL_MAX \ + 0x0C80 /**< Highest maximum connection interval permitted, in units of 1.25 ms, i.e. 4 s. \ + */ +#define BLE_GAP_CP_SLAVE_LATENCY_MAX 0x01F3 /**< Highest slave latency permitted, in connection events. */ #define BLE_GAP_CP_CONN_SUP_TIMEOUT_NONE 0xFFFF /**< No new supervision timeout specified in connect parameters. */ #define BLE_GAP_CP_CONN_SUP_TIMEOUT_MIN 0x000A /**< Lowest supervision timeout permitted, in units of 10 ms, i.e. 100 ms. */ #define BLE_GAP_CP_CONN_SUP_TIMEOUT_MAX 0x0C80 /**< Highest supervision timeout permitted, in units of 10 ms, i.e. 32 s. */ @@ -598,47 +626,47 @@ enum BLE_GAP_TX_POWER_ROLES { * See @ref ble_gap_conn_sec_mode_t. * @{ */ /**@brief Set sec_mode pointed to by ptr to have no access rights.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(ptr) \ - do { \ - (ptr)->sm = 0; \ - (ptr)->lv = 0; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(ptr) \ + do { \ + (ptr)->sm = 0; \ + (ptr)->lv = 0; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require no protection, open link.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_OPEN(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 1; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_OPEN(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 1; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require encryption, but no MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_ENC_NO_MITM(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 2; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_ENC_NO_MITM(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 2; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require encryption and MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_ENC_WITH_MITM(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 3; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_ENC_WITH_MITM(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 3; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require LESC encryption and MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_LESC_ENC_WITH_MITM(ptr) \ - do { \ - (ptr)->sm = 1; \ - (ptr)->lv = 4; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_LESC_ENC_WITH_MITM(ptr) \ + do { \ + (ptr)->sm = 1; \ + (ptr)->lv = 4; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require signing or encryption, no MITM protection needed.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_NO_MITM(ptr) \ - do { \ - (ptr)->sm = 2; \ - (ptr)->lv = 1; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_NO_MITM(ptr) \ + do { \ + (ptr)->sm = 2; \ + (ptr)->lv = 1; \ + } while (0) /**@brief Set sec_mode pointed to by ptr to require signing or encryption with MITM protection.*/ -#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_WITH_MITM(ptr) \ - do { \ - (ptr)->sm = 2; \ - (ptr)->lv = 2; \ - } while (0) +#define BLE_GAP_CONN_SEC_MODE_SET_SIGNED_WITH_MITM(ptr) \ + do { \ + (ptr)->sm = 2; \ + (ptr)->lv = 2; \ + } while (0) /**@} */ /**@brief GAP Security Random Number Length. */ @@ -674,12 +702,12 @@ enum BLE_GAP_TX_POWER_ROLES { /**@defgroup BLE_GAP_ROLE_COUNT GAP concurrent connection count defines. * @{ */ -#define BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT (1) /**< Default maximum number of connections concurrently acting as peripherals. */ -#define BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT (3) /**< Default maximum number of connections concurrently acting as centrals. */ -#define BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT (1) /**< Default number of SMP instances shared between all connections acting as centrals. */ -#define BLE_GAP_ROLE_COUNT_COMBINED_MAX \ - (20) /**< Maximum supported number of concurrent connections in the peripheral and central roles combined. \ - */ +#define BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT (1) /**< Default maximum number of connections concurrently acting as peripherals. */ +#define BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT (3) /**< Default maximum number of connections concurrently acting as centrals. */ +#define BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT \ + (1) /**< Default number of SMP instances shared between all connections acting as centrals. */ +#define BLE_GAP_ROLE_COUNT_COMBINED_MAX \ + (20) /**< Maximum supported number of concurrent connections in the peripheral and central roles combined. */ /**@} */ @@ -713,17 +741,17 @@ enum BLE_GAP_TX_POWER_ROLES { * @{ */ #define BLE_GAP_CHAR_INCL_CONFIG_INCLUDE (0) /**< Include the characteristic in the Attribute Table */ -#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITH_SPACE \ - (1) /**< Do not include the characteristic in the Attribute table. \ - The SoftDevice will reserve the attribute handles \ - which are otherwise used for this characteristic. \ - By reserving the attribute handles it will be possible \ - to upgrade the SoftDevice without changing handle of the \ - Service Changed characteristic. */ -#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITHOUT_SPACE \ - (2) /**< Do not include the characteristic in the Attribute table. \ - The SoftDevice will not reserve the attribute handles \ - which are otherwise used for this characteristic. */ +#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITH_SPACE \ + (1) /**< Do not include the characteristic in the Attribute table. \ + The SoftDevice will reserve the attribute handles \ + which are otherwise used for this characteristic. \ + By reserving the attribute handles it will be possible \ + to upgrade the SoftDevice without changing handle of the \ + Service Changed characteristic. */ +#define BLE_GAP_CHAR_INCL_CONFIG_EXCLUDE_WITHOUT_SPACE \ + (2) /**< Do not include the characteristic in the Attribute table. \ + The SoftDevice will not reserve the attribute handles \ + which are otherwise used for this characteristic. */ /**@} */ /** @defgroup BLE_GAP_CHAR_INCL_CONFIG_DEFAULTS Characteristic inclusion default values @@ -734,19 +762,19 @@ enum BLE_GAP_TX_POWER_ROLES { /** @defgroup BLE_GAP_SLAVE_LATENCY Slave latency configuration options * @{ */ -#define BLE_GAP_SLAVE_LATENCY_ENABLE \ - (0) /**< Slave latency is enabled. When slave latency is enabled, \ - the slave will wake up every time it has data to send, \ - and/or every slave latency number of connection events. */ -#define BLE_GAP_SLAVE_LATENCY_DISABLE \ - (1) /**< Disable slave latency. The slave will wake up every connection event \ - regardless of the requested slave latency. \ - This option consumes the most power. */ -#define BLE_GAP_SLAVE_LATENCY_WAIT_FOR_ACK \ - (2) /**< The slave will wake up every connection event if it has not received \ - an ACK from the master for at least slave latency events. This \ - configuration may increase the power consumption in environments \ - with a lot of radio activity. */ +#define BLE_GAP_SLAVE_LATENCY_ENABLE \ + (0) /**< Slave latency is enabled. When slave latency is enabled, \ + the slave will wake up every time it has data to send, \ + and/or every slave latency number of connection events. */ +#define BLE_GAP_SLAVE_LATENCY_DISABLE \ + (1) /**< Disable slave latency. The slave will wake up every connection event \ + regardless of the requested slave latency. \ + This option consumes the most power. */ +#define BLE_GAP_SLAVE_LATENCY_WAIT_FOR_ACK \ + (2) /**< The slave will wake up every connection event if it has not received \ + an ACK from the master for at least slave latency events. This \ + configuration may increase the power consumption in environments \ + with a lot of radio activity. */ /**@} */ /**@addtogroup BLE_GAP_STRUCTURES Structures @@ -754,44 +782,44 @@ enum BLE_GAP_TX_POWER_ROLES { /**@brief Advertising event properties. */ typedef struct { - uint8_t type; /**< Advertising type. See @ref BLE_GAP_ADV_TYPES. */ - uint8_t anonymous : 1; /**< Omit advertiser's address from all PDUs. - @note Anonymous advertising is only available for - @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED and - @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED. */ - uint8_t include_tx_power : 1; /**< This feature is not supported on this SoftDevice. */ + uint8_t type; /**< Advertising type. See @ref BLE_GAP_ADV_TYPES. */ + uint8_t anonymous : 1; /**< Omit advertiser's address from all PDUs. + @note Anonymous advertising is only available for + @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED and + @ref BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_DIRECTED. */ + uint8_t include_tx_power : 1; /**< This feature is not supported on this SoftDevice. */ } ble_gap_adv_properties_t; /**@brief Advertising report type. */ typedef struct { - uint16_t connectable : 1; /**< Connectable advertising event type. */ - uint16_t scannable : 1; /**< Scannable advertising event type. */ - uint16_t directed : 1; /**< Directed advertising event type. */ - uint16_t scan_response : 1; /**< Received a scan response. */ - uint16_t extended_pdu : 1; /**< Received an extended advertising set. */ - uint16_t status : 2; /**< Data status. See @ref BLE_GAP_ADV_DATA_STATUS. */ - uint16_t reserved : 9; /**< Reserved for future use. */ + uint16_t connectable : 1; /**< Connectable advertising event type. */ + uint16_t scannable : 1; /**< Scannable advertising event type. */ + uint16_t directed : 1; /**< Directed advertising event type. */ + uint16_t scan_response : 1; /**< Received a scan response. */ + uint16_t extended_pdu : 1; /**< Received an extended advertising set. */ + uint16_t status : 2; /**< Data status. See @ref BLE_GAP_ADV_DATA_STATUS. */ + uint16_t reserved : 9; /**< Reserved for future use. */ } ble_gap_adv_report_type_t; /**@brief Advertising Auxiliary Pointer. */ typedef struct { - uint16_t aux_offset; /**< Time offset from the beginning of advertising packet to the auxiliary packet in 100 us units. */ - uint8_t aux_phy; /**< Indicates the PHY on which the auxiliary advertising packet is sent. See @ref BLE_GAP_PHYS. */ + uint16_t aux_offset; /**< Time offset from the beginning of advertising packet to the auxiliary packet in 100 us units. */ + uint8_t aux_phy; /**< Indicates the PHY on which the auxiliary advertising packet is sent. See @ref BLE_GAP_PHYS. */ } ble_gap_aux_pointer_t; /**@brief Bluetooth Low Energy address. */ typedef struct { - uint8_t addr_id_peer : 1; /**< Only valid for peer addresses. - This bit is set by the SoftDevice to indicate whether the address has been resolved - from a Resolvable Private Address (when the peer is using privacy). If set to - 1, @ref addr and @ref addr_type refer to the identity address of the resolved address. - - This bit is ignored when a variable of type @ref ble_gap_addr_t is used as input to API - functions. - */ - uint8_t addr_type : 7; /**< See @ref BLE_GAP_ADDR_TYPES. */ - uint8_t addr[BLE_GAP_ADDR_LEN]; /**< 48-bit address, LSB format. - @ref addr is not used if @ref addr_type is @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. */ + uint8_t + addr_id_peer : 1; /**< Only valid for peer addresses. + This bit is set by the SoftDevice to indicate whether the address has been resolved from + a Resolvable Private Address (when the peer is using privacy). + If set to 1, @ref addr and @ref addr_type refer to the identity address of the resolved address. + + This bit is ignored when a variable of type @ref ble_gap_addr_t is used as input to API functions. + */ + uint8_t addr_type : 7; /**< See @ref BLE_GAP_ADDR_TYPES. */ + uint8_t addr[BLE_GAP_ADDR_LEN]; /**< 48-bit address, LSB format. + @ref addr is not used if @ref addr_type is @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. */ } ble_gap_addr_t; /**@brief GAP connection parameters. @@ -806,37 +834,38 @@ typedef struct { * (1 + Conn_Latency) * Conn_Interval_Max * 2, where Conn_Interval_Max is given in milliseconds. */ typedef struct { - uint16_t min_conn_interval; /**< Minimum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ - uint16_t max_conn_interval; /**< Maximum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ - uint16_t slave_latency; /**< Slave Latency in number of connection events, see @ref BLE_GAP_CP_LIMITS.*/ - uint16_t conn_sup_timeout; /**< Connection Supervision Timeout in 10 ms units, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t min_conn_interval; /**< Minimum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t max_conn_interval; /**< Maximum Connection Interval in 1.25 ms units, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t slave_latency; /**< Slave Latency in number of connection events, see @ref BLE_GAP_CP_LIMITS.*/ + uint16_t conn_sup_timeout; /**< Connection Supervision Timeout in 10 ms units, see @ref BLE_GAP_CP_LIMITS.*/ } ble_gap_conn_params_t; /**@brief GAP connection security modes. * - * Security Mode 0 Level 0: No access permissions at all (this level is not defined by the Bluetooth Core - * specification).\n Security Mode 1 Level 1: No security is needed (aka open link).\n Security Mode 1 Level 2: - * Encrypted link required, MITM protection not necessary.\n Security Mode 1 Level 3: MITM protected encrypted link - * required.\n Security Mode 1 Level 4: LESC MITM protected encrypted link using a 128-bit strength encryption key - * required.\n Security Mode 2 Level 1: Signing or encryption required, MITM protection not necessary.\n Security Mode 2 - * Level 2: MITM protected signing required, unless link is MITM protected encrypted.\n + * Security Mode 0 Level 0: No access permissions at all (this level is not defined by the Bluetooth Core specification).\n + * Security Mode 1 Level 1: No security is needed (aka open link).\n + * Security Mode 1 Level 2: Encrypted link required, MITM protection not necessary.\n + * Security Mode 1 Level 3: MITM protected encrypted link required.\n + * Security Mode 1 Level 4: LESC MITM protected encrypted link using a 128-bit strength encryption key required.\n + * Security Mode 2 Level 1: Signing or encryption required, MITM protection not necessary.\n + * Security Mode 2 Level 2: MITM protected signing required, unless link is MITM protected encrypted.\n */ typedef struct { - uint8_t sm : 4; /**< Security Mode (1 or 2), 0 for no permissions at all. */ - uint8_t lv : 4; /**< Level (1, 2, 3 or 4), 0 for no permissions at all. */ + uint8_t sm : 4; /**< Security Mode (1 or 2), 0 for no permissions at all. */ + uint8_t lv : 4; /**< Level (1, 2, 3 or 4), 0 for no permissions at all. */ } ble_gap_conn_sec_mode_t; /**@brief GAP connection security status.*/ typedef struct { - ble_gap_conn_sec_mode_t sec_mode; /**< Currently active security mode for this connection.*/ - uint8_t encr_key_size; /**< Length of currently active encryption key, 7 to 16 octets (only applicable for bonding - procedures). */ + ble_gap_conn_sec_mode_t sec_mode; /**< Currently active security mode for this connection.*/ + uint8_t + encr_key_size; /**< Length of currently active encryption key, 7 to 16 octets (only applicable for bonding procedures). */ } ble_gap_conn_sec_t; /**@brief Identity Resolving Key. */ typedef struct { - uint8_t irk[BLE_GAP_SEC_KEY_LEN]; /**< Array containing IRK. */ + uint8_t irk[BLE_GAP_SEC_KEY_LEN]; /**< Array containing IRK. */ } ble_gap_irk_t; /**@brief Channel mask (40 bits). @@ -848,68 +877,68 @@ typedef uint8_t ble_gap_ch_mask_t[5]; /**@brief GAP advertising parameters. */ typedef struct { - ble_gap_adv_properties_t properties; /**< The properties of the advertising events. */ - ble_gap_addr_t const *p_peer_addr; /**< Address of a known peer. - @note ble_gap_addr_t::addr_type cannot be - @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. - - When privacy is enabled and the local device uses - @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE addresses, - the device identity list is searched for a matching entry. If - the local IRK for that device identity is set, the local IRK - for that device will be used to generate the advertiser address - field in the advertising packet. - - If @ref ble_gap_adv_properties_t::type is directed, this must be - set to the targeted scanner or initiator. If the peer address is - in the device identity list, the peer IRK for that device will be - used to generate @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE - target addresses used in the advertising event PDUs. */ - uint32_t interval; /**< Advertising interval in 625 us units. @sa BLE_GAP_ADV_INTERVALS. - @note If @ref ble_gap_adv_properties_t::type is set to - @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE - advertising, this parameter is ignored. */ - uint16_t duration; /**< Advertising duration in 10 ms units. When timeout is reached, - an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised. - @sa BLE_GAP_ADV_TIMEOUT_VALUES. - @note The SoftDevice will always complete at least one advertising - event even if the duration is set too low. */ - uint8_t max_adv_evts; /**< Maximum advertising events that shall be sent prior to disabling - advertising. Setting the value to 0 disables the limitation. When - the count of advertising events specified by this parameter - (if not 0) is reached, advertising will be automatically stopped - and an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised - @note If @ref ble_gap_adv_properties_t::type is set to - @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE, - this parameter is ignored. */ - ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. - At least one of the primary channels, that is channel index 37-39, must be used. - Masking away secondary advertising channels is not supported. */ - uint8_t filter_policy; /**< Filter Policy. @sa BLE_GAP_ADV_FILTER_POLICIES. */ - uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising channel packets - are transmitted. If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS - will be used. - Valid values are @ref BLE_GAP_PHY_1MBPS and @ref BLE_GAP_PHY_CODED. - @note The primary_phy shall indicate @ref BLE_GAP_PHY_1MBPS if - @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ - uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising channel packets - are transmitted. - If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS will be used. - Valid values are - @ref BLE_GAP_PHY_1MBPS, @ref BLE_GAP_PHY_2MBPS, and @ref BLE_GAP_PHY_CODED. - If @ref ble_gap_adv_properties_t::type is an extended advertising type - and connectable, this is the PHY that will be used to establish a - connection and send AUX_ADV_IND packets on. - @note This parameter will be ignored when - @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ - uint8_t set_id : 4; /**< The advertising set identifier distinguishes this advertising set from other - advertising sets transmitted by this and other devices. - @note This parameter will be ignored when - @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ - uint8_t scan_req_notification : 1; /**< Enable scan request notifications for this advertising set. When a - scan request is received and the scanner address is allowed - by the filter policy, @ref BLE_GAP_EVT_SCAN_REQ_REPORT is raised. - @note This parameter will be ignored when - @ref ble_gap_adv_properties_t::type is a non-scannable - advertising type. */ + ble_gap_adv_properties_t properties; /**< The properties of the advertising events. */ + ble_gap_addr_t const *p_peer_addr; /**< Address of a known peer. + @note ble_gap_addr_t::addr_type cannot be + @ref BLE_GAP_ADDR_TYPE_ANONYMOUS. + - When privacy is enabled and the local device uses + @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE addresses, + the device identity list is searched for a matching entry. If + the local IRK for that device identity is set, the local IRK + for that device will be used to generate the advertiser address + field in the advertising packet. + - If @ref ble_gap_adv_properties_t::type is directed, this must be + set to the targeted scanner or initiator. If the peer address is + in the device identity list, the peer IRK for that device will be + used to generate @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE + target addresses used in the advertising event PDUs. */ + uint32_t interval; /**< Advertising interval in 625 us units. @sa BLE_GAP_ADV_INTERVALS. + @note If @ref ble_gap_adv_properties_t::type is set to + @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE + advertising, this parameter is ignored. */ + uint16_t duration; /**< Advertising duration in 10 ms units. When timeout is reached, + an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised. + @sa BLE_GAP_ADV_TIMEOUT_VALUES. + @note The SoftDevice will always complete at least one advertising + event even if the duration is set too low. */ + uint8_t max_adv_evts; /**< Maximum advertising events that shall be sent prior to disabling + advertising. Setting the value to 0 disables the limitation. When + the count of advertising events specified by this parameter + (if not 0) is reached, advertising will be automatically stopped + and an event of type @ref BLE_GAP_EVT_ADV_SET_TERMINATED is raised + @note If @ref ble_gap_adv_properties_t::type is set to + @ref BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED_HIGH_DUTY_CYCLE, + this parameter is ignored. */ + ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. + At least one of the primary channels, that is channel index 37-39, must be used. + Masking away secondary advertising channels is not supported. */ + uint8_t filter_policy; /**< Filter Policy. @sa BLE_GAP_ADV_FILTER_POLICIES. */ + uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising channel packets + are transmitted. If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS + will be used. + Valid values are @ref BLE_GAP_PHY_1MBPS and @ref BLE_GAP_PHY_CODED. + @note The primary_phy shall indicate @ref BLE_GAP_PHY_1MBPS if + @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ + uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising channel packets + are transmitted. + If set to @ref BLE_GAP_PHY_AUTO, @ref BLE_GAP_PHY_1MBPS will be used. + Valid values are + @ref BLE_GAP_PHY_1MBPS, @ref BLE_GAP_PHY_2MBPS, and @ref BLE_GAP_PHY_CODED. + If @ref ble_gap_adv_properties_t::type is an extended advertising type + and connectable, this is the PHY that will be used to establish a + connection and send AUX_ADV_IND packets on. + @note This parameter will be ignored when + @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ + uint8_t set_id : 4; /**< The advertising set identifier distinguishes this advertising set from other + advertising sets transmitted by this and other devices. + @note This parameter will be ignored when + @ref ble_gap_adv_properties_t::type is not an extended advertising type. */ + uint8_t scan_req_notification : 1; /**< Enable scan request notifications for this advertising set. When a + scan request is received and the scanner address is allowed + by the filter policy, @ref BLE_GAP_EVT_SCAN_REQ_REPORT is raised. + @note This parameter will be ignored when + @ref ble_gap_adv_properties_t::type is a non-scannable + advertising type. */ } ble_gap_adv_params_t; /**@brief GAP advertising data buffers. @@ -923,64 +952,64 @@ typedef struct { * - Advertising data is changed. * To update advertising data while advertising, provide new buffers to @ref sd_ble_gap_adv_set_configure. */ typedef struct { - ble_data_t adv_data; /**< Advertising data. - @note - Advertising data can only be specified for a @ref ble_gap_adv_properties_t::type - that is allowed to contain advertising data. */ - ble_data_t scan_rsp_data; /**< Scan response data. - @note - Scan response data can only be specified for a @ref ble_gap_adv_properties_t::type - that is scannable. */ + ble_data_t adv_data; /**< Advertising data. + @note + Advertising data can only be specified for a @ref ble_gap_adv_properties_t::type + that is allowed to contain advertising data. */ + ble_data_t scan_rsp_data; /**< Scan response data. + @note + Scan response data can only be specified for a @ref ble_gap_adv_properties_t::type + that is scannable. */ } ble_gap_adv_data_t; /**@brief GAP scanning parameters. */ typedef struct { - uint8_t extended : 1; /**< If 1, the scanner will accept extended advertising packets. - If set to 0, the scanner will not receive advertising packets - on secondary advertising channels, and will not be able - to receive long advertising PDUs. */ - uint8_t report_incomplete_evts : 1; /**< If 1, events of type @ref ble_gap_evt_adv_report_t may have - @ref ble_gap_adv_report_type_t::status set to - @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. - This parameter is ignored when used with @ref sd_ble_gap_connect - @note This may be used to abort receiving more packets from an extended - advertising event, and is only available for extended - scanning, see @ref sd_ble_gap_scan_start. - @note This feature is not supported by this SoftDevice. */ - uint8_t active : 1; /**< If 1, perform active scanning by sending scan requests. - This parameter is ignored when used with @ref sd_ble_gap_connect. */ - uint8_t filter_policy : 2; /**< Scanning filter policy. @sa BLE_GAP_SCAN_FILTER_POLICIES. - @note Only @ref BLE_GAP_SCAN_FP_ACCEPT_ALL and - @ref BLE_GAP_SCAN_FP_WHITELIST are valid when used with - @ref sd_ble_gap_connect */ - uint8_t scan_phys; /**< Bitfield of PHYs to scan on. If set to @ref BLE_GAP_PHY_AUTO, - scan_phys will default to @ref BLE_GAP_PHY_1MBPS. - - If @ref ble_gap_scan_params_t::extended is set to 0, the only - supported PHY is @ref BLE_GAP_PHY_1MBPS. - - When used with @ref sd_ble_gap_scan_start, - the bitfield indicates the PHYs the scanner will use for scanning - on primary advertising channels. The scanner will accept - @ref BLE_GAP_PHYS_SUPPORTED as secondary advertising channel PHYs. - - When used with @ref sd_ble_gap_connect, the bitfield indicates - the PHYs the initiator will use for scanning on primary advertising - channels. The initiator will accept connections initiated on either - of the @ref BLE_GAP_PHYS_SUPPORTED PHYs. - If scan_phys contains @ref BLE_GAP_PHY_1MBPS and/or @ref BLE_GAP_PHY_2MBPS, - the primary scan PHY is @ref BLE_GAP_PHY_1MBPS. - If scan_phys also contains @ref BLE_GAP_PHY_CODED, the primary scan - PHY will also contain @ref BLE_GAP_PHY_CODED. If the only scan PHY is - @ref BLE_GAP_PHY_CODED, the primary scan PHY is - @ref BLE_GAP_PHY_CODED only. */ - uint16_t interval; /**< Scan interval in 625 us units. @sa BLE_GAP_SCAN_INTERVALS. */ - uint16_t window; /**< Scan window in 625 us units. @sa BLE_GAP_SCAN_WINDOW. - If scan_phys contains both @ref BLE_GAP_PHY_1MBPS and - @ref BLE_GAP_PHY_CODED interval shall be larger than or - equal to twice the scan window. */ - uint16_t timeout; /**< Scan timeout in 10 ms units. @sa BLE_GAP_SCAN_TIMEOUT. */ - ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. - At least one of the primary channels, that is channel index 37-39, must be - set to 0. - Masking away secondary channels is not supported. */ + uint8_t extended : 1; /**< If 1, the scanner will accept extended advertising packets. + If set to 0, the scanner will not receive advertising packets + on secondary advertising channels, and will not be able + to receive long advertising PDUs. */ + uint8_t report_incomplete_evts : 1; /**< If 1, events of type @ref ble_gap_evt_adv_report_t may have + @ref ble_gap_adv_report_type_t::status set to + @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. + This parameter is ignored when used with @ref sd_ble_gap_connect + @note This may be used to abort receiving more packets from an extended + advertising event, and is only available for extended + scanning, see @ref sd_ble_gap_scan_start. + @note This feature is not supported by this SoftDevice. */ + uint8_t active : 1; /**< If 1, perform active scanning by sending scan requests. + This parameter is ignored when used with @ref sd_ble_gap_connect. */ + uint8_t filter_policy : 2; /**< Scanning filter policy. @sa BLE_GAP_SCAN_FILTER_POLICIES. + @note Only @ref BLE_GAP_SCAN_FP_ACCEPT_ALL and + @ref BLE_GAP_SCAN_FP_WHITELIST are valid when used with + @ref sd_ble_gap_connect */ + uint8_t scan_phys; /**< Bitfield of PHYs to scan on. If set to @ref BLE_GAP_PHY_AUTO, + scan_phys will default to @ref BLE_GAP_PHY_1MBPS. + - If @ref ble_gap_scan_params_t::extended is set to 0, the only + supported PHY is @ref BLE_GAP_PHY_1MBPS. + - When used with @ref sd_ble_gap_scan_start, + the bitfield indicates the PHYs the scanner will use for scanning + on primary advertising channels. The scanner will accept + @ref BLE_GAP_PHYS_SUPPORTED as secondary advertising channel PHYs. + - When used with @ref sd_ble_gap_connect, the bitfield indicates + the PHYs the initiator will use for scanning on primary advertising + channels. The initiator will accept connections initiated on either + of the @ref BLE_GAP_PHYS_SUPPORTED PHYs. + If scan_phys contains @ref BLE_GAP_PHY_1MBPS and/or @ref BLE_GAP_PHY_2MBPS, + the primary scan PHY is @ref BLE_GAP_PHY_1MBPS. + If scan_phys also contains @ref BLE_GAP_PHY_CODED, the primary scan + PHY will also contain @ref BLE_GAP_PHY_CODED. If the only scan PHY is + @ref BLE_GAP_PHY_CODED, the primary scan PHY is + @ref BLE_GAP_PHY_CODED only. */ + uint16_t interval; /**< Scan interval in 625 us units. @sa BLE_GAP_SCAN_INTERVALS. */ + uint16_t window; /**< Scan window in 625 us units. @sa BLE_GAP_SCAN_WINDOW. + If scan_phys contains both @ref BLE_GAP_PHY_1MBPS and + @ref BLE_GAP_PHY_CODED interval shall be larger than or + equal to twice the scan window. */ + uint16_t timeout; /**< Scan timeout in 10 ms units. @sa BLE_GAP_SCAN_TIMEOUT. */ + ble_gap_ch_mask_t channel_mask; /**< Channel mask for primary and secondary advertising channels. + At least one of the primary channels, that is channel index 37-39, must be + set to 0. + Masking away secondary channels is not supported. */ } ble_gap_scan_params_t; /**@brief Privacy. @@ -990,34 +1019,33 @@ typedef struct { * that is automatically refreshed at a specified interval. * * If a device still wants to be recognized by other peers, it needs to share it's Identity Resolving Key (IRK). - * With this key, a device can generate a random private address that can only be recognized by peers in - * possession of that key, and devices can establish connections without revealing their real identities. + * With this key, a device can generate a random private address that can only be recognized by peers in possession of that + * key, and devices can establish connections without revealing their real identities. * * Both network privacy (@ref BLE_GAP_PRIVACY_MODE_NETWORK_PRIVACY) and device privacy (@ref * BLE_GAP_PRIVACY_MODE_DEVICE_PRIVACY) are supported. * * @note If the device IRK is updated, the new IRK becomes the one to be distributed in all * bonding procedures performed after @ref sd_ble_gap_privacy_set returns. - * The IRK distributed during bonding procedure is the device IRK that is active when @ref - * sd_ble_gap_sec_params_reply is called. + * The IRK distributed during bonding procedure is the device IRK that is active when @ref sd_ble_gap_sec_params_reply is + * called. */ typedef struct { - uint8_t privacy_mode; /**< Privacy mode, see @ref BLE_GAP_PRIVACY_MODES. Default is @ref BLE_GAP_PRIVACY_MODE_OFF. */ - uint8_t private_addr_type; /**< The private address type must be either @ref - BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE or - @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE. */ - uint16_t private_addr_cycle_s; /**< Private address cycle interval in seconds. Providing an address cycle value of 0 will - use the default value defined by @ref BLE_GAP_DEFAULT_PRIVATE_ADDR_CYCLE_INTERVAL_S. */ - ble_gap_irk_t *p_device_irk; /**< When used as input, pointer to IRK structure that will be used as the default IRK. - If NULL, the device default IRK will be used. When used as output, pointer to IRK - structure where the current default IRK will be written to. If NULL, this argument is - ignored. By default, the default IRK is used to generate random private resolvable - addresses for the local device unless instructed otherwise. */ + uint8_t privacy_mode; /**< Privacy mode, see @ref BLE_GAP_PRIVACY_MODES. Default is @ref BLE_GAP_PRIVACY_MODE_OFF. */ + uint8_t private_addr_type; /**< The private address type must be either @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE or + @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE. */ + uint16_t private_addr_cycle_s; /**< Private address cycle interval in seconds. Providing an address cycle value of 0 will use + the default value defined by @ref BLE_GAP_DEFAULT_PRIVATE_ADDR_CYCLE_INTERVAL_S. */ + ble_gap_irk_t + *p_device_irk; /**< When used as input, pointer to IRK structure that will be used as the default IRK. If NULL, the device + default IRK will be used. When used as output, pointer to IRK structure where the current default IRK + will be written to. If NULL, this argument is ignored. By default, the default IRK is used to generate + random private resolvable addresses for the local device unless instructed otherwise. */ } ble_gap_privacy_params_t; /**@brief PHY preferences for TX and RX - * @note tx_phys and rx_phys are bit fields. Multiple bits can be set in them to indicate multiple preferred PHYs for - * each direction. + * @note tx_phys and rx_phys are bit fields. Multiple bits can be set in them to indicate multiple preferred PHYs for each + * direction. * @code * p_gap_phys->tx_phys = BLE_GAP_PHY_1MBPS | BLE_GAP_PHY_2MBPS; * p_gap_phys->rx_phys = BLE_GAP_PHY_1MBPS | BLE_GAP_PHY_2MBPS; @@ -1025,258 +1053,258 @@ typedef struct { * */ typedef struct { - uint8_t tx_phys; /**< Preferred transmit PHYs, see @ref BLE_GAP_PHYS. */ - uint8_t rx_phys; /**< Preferred receive PHYs, see @ref BLE_GAP_PHYS. */ + uint8_t tx_phys; /**< Preferred transmit PHYs, see @ref BLE_GAP_PHYS. */ + uint8_t rx_phys; /**< Preferred receive PHYs, see @ref BLE_GAP_PHYS. */ } ble_gap_phys_t; /** @brief Keys that can be exchanged during a bonding procedure. */ typedef struct { - uint8_t enc : 1; /**< Long Term Key and Master Identification. */ - uint8_t id : 1; /**< Identity Resolving Key and Identity Address Information. */ - uint8_t sign : 1; /**< Connection Signature Resolving Key. */ - uint8_t link : 1; /**< Derive the Link Key from the LTK. */ + uint8_t enc : 1; /**< Long Term Key and Master Identification. */ + uint8_t id : 1; /**< Identity Resolving Key and Identity Address Information. */ + uint8_t sign : 1; /**< Connection Signature Resolving Key. */ + uint8_t link : 1; /**< Derive the Link Key from the LTK. */ } ble_gap_sec_kdist_t; /**@brief GAP security parameters. */ typedef struct { - uint8_t bond : 1; /**< Perform bonding. */ - uint8_t mitm : 1; /**< Enable Man In The Middle protection. */ - uint8_t lesc : 1; /**< Enable LE Secure Connection pairing. */ - uint8_t keypress : 1; /**< Enable generation of keypress notifications. */ - uint8_t io_caps : 3; /**< IO capabilities, see @ref BLE_GAP_IO_CAPS. */ - uint8_t oob : 1; /**< The OOB data flag. - - In LE legacy pairing, this flag is set if a device has out of band authentication data. - The OOB method is used if both of the devices have out of band authentication data. - - In LE Secure Connections pairing, this flag is set if a device has the peer device's out of - band authentication data. The OOB method is used if at least one device has the peer - device's OOB data available. */ - uint8_t min_key_size; /**< Minimum encryption key size in octets between 7 and 16. If 0 then not applicable in this - instance. */ - uint8_t max_key_size; /**< Maximum encryption key size in octets between min_key_size and 16. */ - ble_gap_sec_kdist_t kdist_own; /**< Key distribution bitmap: keys that the local device will distribute. */ - ble_gap_sec_kdist_t kdist_peer; /**< Key distribution bitmap: keys that the remote device will distribute. */ + uint8_t bond : 1; /**< Perform bonding. */ + uint8_t mitm : 1; /**< Enable Man In The Middle protection. */ + uint8_t lesc : 1; /**< Enable LE Secure Connection pairing. */ + uint8_t keypress : 1; /**< Enable generation of keypress notifications. */ + uint8_t io_caps : 3; /**< IO capabilities, see @ref BLE_GAP_IO_CAPS. */ + uint8_t oob : 1; /**< The OOB data flag. + - In LE legacy pairing, this flag is set if a device has out of band authentication data. + The OOB method is used if both of the devices have out of band authentication data. + - In LE Secure Connections pairing, this flag is set if a device has the peer device's out of band + authentication data. The OOB method is used if at least one device has the peer device's OOB data + available. */ + uint8_t + min_key_size; /**< Minimum encryption key size in octets between 7 and 16. If 0 then not applicable in this instance. */ + uint8_t max_key_size; /**< Maximum encryption key size in octets between min_key_size and 16. */ + ble_gap_sec_kdist_t kdist_own; /**< Key distribution bitmap: keys that the local device will distribute. */ + ble_gap_sec_kdist_t kdist_peer; /**< Key distribution bitmap: keys that the remote device will distribute. */ } ble_gap_sec_params_t; /**@brief GAP Encryption Information. */ typedef struct { - uint8_t ltk[BLE_GAP_SEC_KEY_LEN]; /**< Long Term Key. */ - uint8_t lesc : 1; /**< Key generated using LE Secure Connections. */ - uint8_t auth : 1; /**< Authenticated Key. */ - uint8_t ltk_len : 6; /**< LTK length in octets. */ + uint8_t ltk[BLE_GAP_SEC_KEY_LEN]; /**< Long Term Key. */ + uint8_t lesc : 1; /**< Key generated using LE Secure Connections. */ + uint8_t auth : 1; /**< Authenticated Key. */ + uint8_t ltk_len : 6; /**< LTK length in octets. */ } ble_gap_enc_info_t; /**@brief GAP Master Identification. */ typedef struct { - uint16_t ediv; /**< Encrypted Diversifier. */ - uint8_t rand[BLE_GAP_SEC_RAND_LEN]; /**< Random Number. */ + uint16_t ediv; /**< Encrypted Diversifier. */ + uint8_t rand[BLE_GAP_SEC_RAND_LEN]; /**< Random Number. */ } ble_gap_master_id_t; /**@brief GAP Signing Information. */ typedef struct { - uint8_t csrk[BLE_GAP_SEC_KEY_LEN]; /**< Connection Signature Resolving Key. */ + uint8_t csrk[BLE_GAP_SEC_KEY_LEN]; /**< Connection Signature Resolving Key. */ } ble_gap_sign_info_t; /**@brief GAP LE Secure Connections P-256 Public Key. */ typedef struct { - uint8_t pk[BLE_GAP_LESC_P256_PK_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman P-256 Public Key. - Stored in the standard SMP protocol format: {X,Y} both in little-endian. */ + uint8_t pk[BLE_GAP_LESC_P256_PK_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman P-256 Public Key. Stored in the + standard SMP protocol format: {X,Y} both in little-endian. */ } ble_gap_lesc_p256_pk_t; /**@brief GAP LE Secure Connections DHKey. */ typedef struct { - uint8_t key[BLE_GAP_LESC_DHKEY_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman Key. Stored in - little-endian. */ + uint8_t key[BLE_GAP_LESC_DHKEY_LEN]; /**< LE Secure Connections Elliptic Curve Diffie-Hellman Key. Stored in little-endian. */ } ble_gap_lesc_dhkey_t; /**@brief GAP LE Secure Connections OOB data. */ typedef struct { - ble_gap_addr_t addr; /**< Bluetooth address of the device. */ - uint8_t r[BLE_GAP_SEC_KEY_LEN]; /**< Random Number. */ - uint8_t c[BLE_GAP_SEC_KEY_LEN]; /**< Confirm Value. */ + ble_gap_addr_t addr; /**< Bluetooth address of the device. */ + uint8_t r[BLE_GAP_SEC_KEY_LEN]; /**< Random Number. */ + uint8_t c[BLE_GAP_SEC_KEY_LEN]; /**< Confirm Value. */ } ble_gap_lesc_oob_data_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONNECTED. */ typedef struct { - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref - ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's - identity address. */ - uint8_t role; /**< BLE role for this connection, see @ref BLE_GAP_ROLES */ - ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ - uint8_t adv_handle; /**< Advertising handle in which advertising has ended. - This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ - ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated - advertising set. The advertising buffers provided in - @ref sd_ble_gap_adv_set_configure are now released. - This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ + ble_gap_addr_t + peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref + ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's identity address. */ + uint8_t role; /**< BLE role for this connection, see @ref BLE_GAP_ROLES */ + ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ + uint8_t adv_handle; /**< Advertising handle in which advertising has ended. + This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ + ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated + advertising set. The advertising buffers provided in + @ref sd_ble_gap_adv_set_configure are now released. + This variable is only set if role is set to @ref BLE_GAP_ROLE_PERIPH. */ } ble_gap_evt_connected_t; /**@brief Event structure for @ref BLE_GAP_EVT_DISCONNECTED. */ typedef struct { - uint8_t reason; /**< HCI error code, see @ref BLE_HCI_STATUS_CODES. */ + uint8_t reason; /**< HCI error code, see @ref BLE_HCI_STATUS_CODES. */ } ble_gap_evt_disconnected_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONN_PARAM_UPDATE. */ typedef struct { - ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ + ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ } ble_gap_evt_conn_param_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_PHY_UPDATE_REQUEST. */ typedef struct { - ble_gap_phys_t peer_preferred_phys; /**< The PHYs the peer prefers to use. */ + ble_gap_phys_t peer_preferred_phys; /**< The PHYs the peer prefers to use. */ } ble_gap_evt_phy_update_request_t; /**@brief Event Structure for @ref BLE_GAP_EVT_PHY_UPDATE. */ typedef struct { - uint8_t status; /**< Status of the procedure, see @ref BLE_HCI_STATUS_CODES.*/ - uint8_t tx_phy; /**< TX PHY for this connection, see @ref BLE_GAP_PHYS. */ - uint8_t rx_phy; /**< RX PHY for this connection, see @ref BLE_GAP_PHYS. */ + uint8_t status; /**< Status of the procedure, see @ref BLE_HCI_STATUS_CODES.*/ + uint8_t tx_phy; /**< TX PHY for this connection, see @ref BLE_GAP_PHYS. */ + uint8_t rx_phy; /**< RX PHY for this connection, see @ref BLE_GAP_PHYS. */ } ble_gap_evt_phy_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST. */ typedef struct { - ble_gap_sec_params_t peer_params; /**< Initiator Security Parameters. */ + ble_gap_sec_params_t peer_params; /**< Initiator Security Parameters. */ } ble_gap_evt_sec_params_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_SEC_INFO_REQUEST. */ typedef struct { - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. */ - ble_gap_master_id_t master_id; /**< Master Identification for LTK lookup. */ - uint8_t enc_info : 1; /**< If 1, Encryption Information required. */ - uint8_t id_info : 1; /**< If 1, Identity Information required. */ - uint8_t sign_info : 1; /**< If 1, Signing Information required. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. */ + ble_gap_master_id_t master_id; /**< Master Identification for LTK lookup. */ + uint8_t enc_info : 1; /**< If 1, Encryption Information required. */ + uint8_t id_info : 1; /**< If 1, Identity Information required. */ + uint8_t sign_info : 1; /**< If 1, Signing Information required. */ } ble_gap_evt_sec_info_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_PASSKEY_DISPLAY. */ typedef struct { - uint8_t passkey[BLE_GAP_PASSKEY_LEN]; /**< 6-digit passkey in ASCII ('0'-'9' digits only). */ - uint8_t match_request : 1; /**< If 1 requires the application to report the match using @ref sd_ble_gap_auth_key_reply - with either @ref BLE_GAP_AUTH_KEY_TYPE_NONE if there is no match or - @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY if there is a match. */ + uint8_t passkey[BLE_GAP_PASSKEY_LEN]; /**< 6-digit passkey in ASCII ('0'-'9' digits only). */ + uint8_t match_request : 1; /**< If 1 requires the application to report the match using @ref sd_ble_gap_auth_key_reply + with either @ref BLE_GAP_AUTH_KEY_TYPE_NONE if there is no match or + @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY if there is a match. */ } ble_gap_evt_passkey_display_t; /**@brief Event structure for @ref BLE_GAP_EVT_KEY_PRESSED. */ typedef struct { - uint8_t kp_not; /**< Keypress notification type, see @ref BLE_GAP_KP_NOT_TYPES. */ + uint8_t kp_not; /**< Keypress notification type, see @ref BLE_GAP_KP_NOT_TYPES. */ } ble_gap_evt_key_pressed_t; /**@brief Event structure for @ref BLE_GAP_EVT_AUTH_KEY_REQUEST. */ typedef struct { - uint8_t key_type; /**< See @ref BLE_GAP_AUTH_KEY_TYPES. */ + uint8_t key_type; /**< See @ref BLE_GAP_AUTH_KEY_TYPES. */ } ble_gap_evt_auth_key_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST. */ typedef struct { - ble_gap_lesc_p256_pk_t *p_pk_peer; /**< LE Secure Connections remote P-256 Public Key. This will point to the application-supplied memory - inside the keyset during the call to @ref sd_ble_gap_sec_params_reply. */ - uint8_t oobd_req : 1; /**< LESC OOB data required. A call to @ref sd_ble_gap_lesc_oob_data_set is required to complete - the procedure. */ + ble_gap_lesc_p256_pk_t + *p_pk_peer; /**< LE Secure Connections remote P-256 Public Key. This will point to the application-supplied memory + inside the keyset during the call to @ref sd_ble_gap_sec_params_reply. */ + uint8_t oobd_req : 1; /**< LESC OOB data required. A call to @ref sd_ble_gap_lesc_oob_data_set is required to complete the + procedure. */ } ble_gap_evt_lesc_dhkey_request_t; /**@brief Security levels supported. * @note See Bluetooth Specification Version 4.2 Volume 3, Part C, Chapter 10, Section 10.2.1. */ typedef struct { - uint8_t lv1 : 1; /**< If 1: Level 1 is supported. */ - uint8_t lv2 : 1; /**< If 1: Level 2 is supported. */ - uint8_t lv3 : 1; /**< If 1: Level 3 is supported. */ - uint8_t lv4 : 1; /**< If 1: Level 4 is supported. */ + uint8_t lv1 : 1; /**< If 1: Level 1 is supported. */ + uint8_t lv2 : 1; /**< If 1: Level 2 is supported. */ + uint8_t lv3 : 1; /**< If 1: Level 3 is supported. */ + uint8_t lv4 : 1; /**< If 1: Level 4 is supported. */ } ble_gap_sec_levels_t; /**@brief Encryption Key. */ typedef struct { - ble_gap_enc_info_t enc_info; /**< Encryption Information. */ - ble_gap_master_id_t master_id; /**< Master Identification. */ + ble_gap_enc_info_t enc_info; /**< Encryption Information. */ + ble_gap_master_id_t master_id; /**< Master Identification. */ } ble_gap_enc_key_t; /**@brief Identity Key. */ typedef struct { - ble_gap_irk_t id_info; /**< Identity Resolving Key. */ - ble_gap_addr_t id_addr_info; /**< Identity Address. */ + ble_gap_irk_t id_info; /**< Identity Resolving Key. */ + ble_gap_addr_t id_addr_info; /**< Identity Address. */ } ble_gap_id_key_t; /**@brief Security Keys. */ typedef struct { - ble_gap_enc_key_t *p_enc_key; /**< Encryption Key, or NULL. */ - ble_gap_id_key_t *p_id_key; /**< Identity Key, or NULL. */ - ble_gap_sign_info_t *p_sign_key; /**< Signing Key, or NULL. */ - ble_gap_lesc_p256_pk_t *p_pk; /**< LE Secure Connections P-256 Public Key. When in debug mode the application must use the - value defined in the Core Bluetooth Specification v4.2 Vol.3, Part H, Section 2.3.5.6.1 */ + ble_gap_enc_key_t *p_enc_key; /**< Encryption Key, or NULL. */ + ble_gap_id_key_t *p_id_key; /**< Identity Key, or NULL. */ + ble_gap_sign_info_t *p_sign_key; /**< Signing Key, or NULL. */ + ble_gap_lesc_p256_pk_t *p_pk; /**< LE Secure Connections P-256 Public Key. When in debug mode the application must use the + value defined in the Core Bluetooth Specification v4.2 Vol.3, Part H, Section 2.3.5.6.1 */ } ble_gap_sec_keys_t; /**@brief Security key set for both local and peer keys. */ typedef struct { - ble_gap_sec_keys_t keys_own; /**< Keys distributed by the local device. For LE Secure Connections the encryption key - will be generated locally and will always be stored if bonding. */ - ble_gap_sec_keys_t keys_peer; /**< Keys distributed by the remote device. For LE Secure Connections, p_enc_key must - always be NULL. */ + ble_gap_sec_keys_t keys_own; /**< Keys distributed by the local device. For LE Secure Connections the encryption key will be + generated locally and will always be stored if bonding. */ + ble_gap_sec_keys_t + keys_peer; /**< Keys distributed by the remote device. For LE Secure Connections, p_enc_key must always be NULL. */ } ble_gap_sec_keyset_t; /**@brief Data Length Update Procedure parameters. */ typedef struct { - uint16_t max_tx_octets; /**< Maximum number of payload octets that a Controller supports for transmission of a single - Link Layer Data Channel PDU. */ - uint16_t max_rx_octets; /**< Maximum number of payload octets that a Controller supports for reception of a single - Link Layer Data Channel PDU. */ - uint16_t max_tx_time_us; /**< Maximum time, in microseconds, that a Controller supports for transmission of a single - Link Layer Data Channel PDU. */ - uint16_t max_rx_time_us; /**< Maximum time, in microseconds, that a Controller supports for reception of a single Link - Layer Data Channel PDU. */ + uint16_t max_tx_octets; /**< Maximum number of payload octets that a Controller supports for transmission of a single Link + Layer Data Channel PDU. */ + uint16_t max_rx_octets; /**< Maximum number of payload octets that a Controller supports for reception of a single Link Layer + Data Channel PDU. */ + uint16_t max_tx_time_us; /**< Maximum time, in microseconds, that a Controller supports for transmission of a single Link + Layer Data Channel PDU. */ + uint16_t max_rx_time_us; /**< Maximum time, in microseconds, that a Controller supports for reception of a single Link Layer + Data Channel PDU. */ } ble_gap_data_length_params_t; /**@brief Data Length Update Procedure local limitation. */ typedef struct { - uint16_t tx_payload_limited_octets; /**< If > 0, the requested TX packet length is too long by this many octets. */ - uint16_t rx_payload_limited_octets; /**< If > 0, the requested RX packet length is too long by this many octets. */ - uint16_t tx_rx_time_limited_us; /**< If > 0, the requested combination of TX and RX packet lengths is too long by this - many microseconds. */ + uint16_t tx_payload_limited_octets; /**< If > 0, the requested TX packet length is too long by this many octets. */ + uint16_t rx_payload_limited_octets; /**< If > 0, the requested RX packet length is too long by this many octets. */ + uint16_t tx_rx_time_limited_us; /**< If > 0, the requested combination of TX and RX packet lengths is too long by this many + microseconds. */ } ble_gap_data_length_limitation_t; /**@brief Event structure for @ref BLE_GAP_EVT_AUTH_STATUS. */ typedef struct { - uint8_t auth_status; /**< Authentication status, see @ref BLE_GAP_SEC_STATUS. */ - uint8_t error_src : 2; /**< On error, source that caused the failure, see @ref BLE_GAP_SEC_STATUS_SOURCES. */ - uint8_t bonded : 1; /**< Procedure resulted in a bond. */ - uint8_t lesc : 1; /**< Procedure resulted in a LE Secure Connection. */ - ble_gap_sec_levels_t sm1_levels; /**< Levels supported in Security Mode 1. */ - ble_gap_sec_levels_t sm2_levels; /**< Levels supported in Security Mode 2. */ - ble_gap_sec_kdist_t kdist_own; /**< Bitmap stating which keys were exchanged (distributed) by the local device. If - bonding with LE Secure Connections, the enc bit will be always set. */ - ble_gap_sec_kdist_t kdist_peer; /**< Bitmap stating which keys were exchanged (distributed) by the remote device. If - bonding with LE Secure Connections, the enc bit will never be set. */ + uint8_t auth_status; /**< Authentication status, see @ref BLE_GAP_SEC_STATUS. */ + uint8_t error_src : 2; /**< On error, source that caused the failure, see @ref BLE_GAP_SEC_STATUS_SOURCES. */ + uint8_t bonded : 1; /**< Procedure resulted in a bond. */ + uint8_t lesc : 1; /**< Procedure resulted in a LE Secure Connection. */ + ble_gap_sec_levels_t sm1_levels; /**< Levels supported in Security Mode 1. */ + ble_gap_sec_levels_t sm2_levels; /**< Levels supported in Security Mode 2. */ + ble_gap_sec_kdist_t kdist_own; /**< Bitmap stating which keys were exchanged (distributed) by the local device. If bonding + with LE Secure Connections, the enc bit will be always set. */ + ble_gap_sec_kdist_t kdist_peer; /**< Bitmap stating which keys were exchanged (distributed) by the remote device. If bonding + with LE Secure Connections, the enc bit will never be set. */ } ble_gap_evt_auth_status_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONN_SEC_UPDATE. */ typedef struct { - ble_gap_conn_sec_t conn_sec; /**< Connection security level. */ + ble_gap_conn_sec_t conn_sec; /**< Connection security level. */ } ble_gap_evt_conn_sec_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_TIMEOUT. */ typedef struct { - uint8_t src; /**< Source of timeout event, see @ref BLE_GAP_TIMEOUT_SOURCES. */ - union { - ble_data_t adv_report_buffer; /**< If source is set to @ref BLE_GAP_TIMEOUT_SRC_SCAN, the released - scan buffer is contained in this field. */ - } params; /**< Event Parameters. */ + uint8_t src; /**< Source of timeout event, see @ref BLE_GAP_TIMEOUT_SOURCES. */ + union { + ble_data_t adv_report_buffer; /**< If source is set to @ref BLE_GAP_TIMEOUT_SRC_SCAN, the released + scan buffer is contained in this field. */ + } params; /**< Event Parameters. */ } ble_gap_evt_timeout_t; /**@brief Event structure for @ref BLE_GAP_EVT_RSSI_CHANGED. */ typedef struct { - int8_t rssi; /**< Received Signal Strength Indication in dBm. - @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature - measurement. */ - uint8_t ch_index; /**< Data Channel Index on which the Signal Strength is measured (0-36). */ + int8_t rssi; /**< Received Signal Strength Indication in dBm. + @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature + measurement. */ + uint8_t ch_index; /**< Data Channel Index on which the Signal Strength is measured (0-36). */ } ble_gap_evt_rssi_changed_t; /**@brief Event structure for @ref BLE_GAP_EVT_ADV_SET_TERMINATED */ typedef struct { - uint8_t reason; /**< Reason for why the advertising set terminated. See - @ref BLE_GAP_EVT_ADV_SET_TERMINATED_REASON. */ - uint8_t adv_handle; /**< Advertising handle in which advertising has ended. */ - uint8_t num_completed_adv_events; /**< If @ref ble_gap_adv_params_t::max_adv_evts was not set to 0, - this field indicates the number of completed advertising events. */ - ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated - advertising set. The advertising buffers provided in - @ref sd_ble_gap_adv_set_configure are now released. */ + uint8_t reason; /**< Reason for why the advertising set terminated. See + @ref BLE_GAP_EVT_ADV_SET_TERMINATED_REASON. */ + uint8_t adv_handle; /**< Advertising handle in which advertising has ended. */ + uint8_t num_completed_adv_events; /**< If @ref ble_gap_adv_params_t::max_adv_evts was not set to 0, + this field indicates the number of completed advertising events. */ + ble_gap_adv_data_t adv_data; /**< Advertising buffers corresponding to the terminated + advertising set. The advertising buffers provided in + @ref sd_ble_gap_adv_set_configure are now released. */ } ble_gap_evt_adv_set_terminated_t; /**@brief Event structure for @ref BLE_GAP_EVT_ADV_REPORT. @@ -1288,72 +1316,72 @@ typedef struct { * scanning will be paused. To continue scanning, call @ref sd_ble_gap_scan_start. */ typedef struct { - ble_gap_adv_report_type_t type; /**< Advertising report type. See @ref ble_gap_adv_report_type_t. */ - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr is resolved: - @ref ble_gap_addr_t::addr_id_peer is set to 1 and the address is the - peer's identity address. */ - ble_gap_addr_t direct_addr; /**< Contains the target address of the advertising event if - @ref ble_gap_adv_report_type_t::directed is set to 1. If the - SoftDevice was able to resolve the address, - @ref ble_gap_addr_t::addr_id_peer is set to 1 and the direct_addr - contains the local identity address. If the target address of the - advertising event is @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE, - and the SoftDevice was unable to resolve it, the application may try - to resolve this address to find out if the advertising event was - directed to us. */ - uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising packet was received. - See @ref BLE_GAP_PHYS. */ - uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising packet was received. - See @ref BLE_GAP_PHYS. This field is set to @ref BLE_GAP_PHY_NOT_SET if no packets - were received on a secondary advertising channel. */ - int8_t tx_power; /**< TX Power reported by the advertiser in the last packet header received. - This field is set to @ref BLE_GAP_POWER_LEVEL_INVALID if the - last received packet did not contain the Tx Power field. - @note TX Power is only included in extended advertising packets. */ - int8_t rssi; /**< Received Signal Strength Indication in dBm of the last packet received. - @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature - measurement. */ - uint8_t ch_index; /**< Channel Index on which the last advertising packet is received (0-39). */ - uint8_t set_id; /**< Set ID of the received advertising data. Set ID is not present - if set to @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ - uint16_t data_id : 12; /**< The advertising data ID of the received advertising data. Data ID - is not present if @ref ble_gap_evt_adv_report_t::set_id is set to - @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ - ble_data_t data; /**< Received advertising or scan response data. If - @ref ble_gap_adv_report_type_t::status is not set to - @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the data buffer provided - in @ref sd_ble_gap_scan_start is now released. */ - ble_gap_aux_pointer_t aux_pointer; /**< The offset and PHY of the next advertising packet in this extended advertising - event. @note This field is only set if @ref ble_gap_adv_report_type_t::status - is set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. */ + ble_gap_adv_report_type_t type; /**< Advertising report type. See @ref ble_gap_adv_report_type_t. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr is resolved: + @ref ble_gap_addr_t::addr_id_peer is set to 1 and the address is the + peer's identity address. */ + ble_gap_addr_t direct_addr; /**< Contains the target address of the advertising event if + @ref ble_gap_adv_report_type_t::directed is set to 1. If the + SoftDevice was able to resolve the address, + @ref ble_gap_addr_t::addr_id_peer is set to 1 and the direct_addr + contains the local identity address. If the target address of the + advertising event is @ref BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE, + and the SoftDevice was unable to resolve it, the application may try + to resolve this address to find out if the advertising event was + directed to us. */ + uint8_t primary_phy; /**< Indicates the PHY on which the primary advertising packet was received. + See @ref BLE_GAP_PHYS. */ + uint8_t secondary_phy; /**< Indicates the PHY on which the secondary advertising packet was received. + See @ref BLE_GAP_PHYS. This field is set to @ref BLE_GAP_PHY_NOT_SET if no packets + were received on a secondary advertising channel. */ + int8_t tx_power; /**< TX Power reported by the advertiser in the last packet header received. + This field is set to @ref BLE_GAP_POWER_LEVEL_INVALID if the + last received packet did not contain the Tx Power field. + @note TX Power is only included in extended advertising packets. */ + int8_t rssi; /**< Received Signal Strength Indication in dBm of the last packet received. + @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature + measurement. */ + uint8_t ch_index; /**< Channel Index on which the last advertising packet is received (0-39). */ + uint8_t set_id; /**< Set ID of the received advertising data. Set ID is not present + if set to @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ + uint16_t data_id : 12; /**< The advertising data ID of the received advertising data. Data ID + is not present if @ref ble_gap_evt_adv_report_t::set_id is set to + @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */ + ble_data_t data; /**< Received advertising or scan response data. If + @ref ble_gap_adv_report_type_t::status is not set to + @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the data buffer provided + in @ref sd_ble_gap_scan_start is now released. */ + ble_gap_aux_pointer_t aux_pointer; /**< The offset and PHY of the next advertising packet in this extended advertising + event. @note This field is only set if @ref ble_gap_adv_report_type_t::status + is set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. */ } ble_gap_evt_adv_report_t; /**@brief Event structure for @ref BLE_GAP_EVT_SEC_REQUEST. */ typedef struct { - uint8_t bond : 1; /**< Perform bonding. */ - uint8_t mitm : 1; /**< Man In The Middle protection requested. */ - uint8_t lesc : 1; /**< LE Secure Connections requested. */ - uint8_t keypress : 1; /**< Generation of keypress notifications requested. */ + uint8_t bond : 1; /**< Perform bonding. */ + uint8_t mitm : 1; /**< Man In The Middle protection requested. */ + uint8_t lesc : 1; /**< LE Secure Connections requested. */ + uint8_t keypress : 1; /**< Generation of keypress notifications requested. */ } ble_gap_evt_sec_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST. */ typedef struct { - ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ + ble_gap_conn_params_t conn_params; /**< GAP Connection Parameters. */ } ble_gap_evt_conn_param_update_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_SCAN_REQ_REPORT. */ typedef struct { - uint8_t adv_handle; /**< Advertising handle for the advertising set which received the Scan Request */ - int8_t rssi; /**< Received Signal Strength Indication in dBm. - @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature - measurement. */ - ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref - ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's identity address. */ + uint8_t adv_handle; /**< Advertising handle for the advertising set which received the Scan Request */ + int8_t rssi; /**< Received Signal Strength Indication in dBm. + @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature + measurement. */ + ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref + ble_gap_addr_t::addr_id_peer is set to 1 and the address is the device's identity address. */ } ble_gap_evt_scan_req_report_t; /**@brief Event structure for @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST. */ typedef struct { - ble_gap_data_length_params_t peer_params; /**< Peer data length parameters. */ + ble_gap_data_length_params_t peer_params; /**< Peer data length parameters. */ } ble_gap_evt_data_length_update_request_t; /**@brief Event structure for @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE. @@ -1361,47 +1389,48 @@ typedef struct { * @note This event may also be raised after a PHY Update procedure. */ typedef struct { - ble_gap_data_length_params_t effective_params; /**< The effective data length parameters. */ + ble_gap_data_length_params_t effective_params; /**< The effective data length parameters. */ } ble_gap_evt_data_length_update_t; /**@brief Event structure for @ref BLE_GAP_EVT_QOS_CHANNEL_SURVEY_REPORT. */ typedef struct { - int8_t channel_energy[BLE_GAP_CHANNEL_COUNT]; /**< The measured energy on the Bluetooth Low Energy - channels, in dBm, indexed by Channel Index. - If no measurement is available for the given channel, - channel_energy is set to - @ref BLE_GAP_POWER_LEVEL_INVALID. */ + int8_t + channel_energy[BLE_GAP_CHANNEL_COUNT]; /**< The measured energy on the Bluetooth Low Energy + channels, in dBm, indexed by Channel Index. + If no measurement is available for the given channel, channel_energy is set to + @ref BLE_GAP_POWER_LEVEL_INVALID. */ } ble_gap_evt_qos_channel_survey_report_t; /**@brief GAP event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which event occurred. */ - union /**< union alternative identified by evt_id in enclosing struct. */ - { - ble_gap_evt_connected_t connected; /**< Connected Event Parameters. */ - ble_gap_evt_disconnected_t disconnected; /**< Disconnected Event Parameters. */ - ble_gap_evt_conn_param_update_t conn_param_update; /**< Connection Parameter Update Parameters. */ - ble_gap_evt_sec_params_request_t sec_params_request; /**< Security Parameters Request Event Parameters. */ - ble_gap_evt_sec_info_request_t sec_info_request; /**< Security Information Request Event Parameters. */ - ble_gap_evt_passkey_display_t passkey_display; /**< Passkey Display Event Parameters. */ - ble_gap_evt_key_pressed_t key_pressed; /**< Key Pressed Event Parameters. */ - ble_gap_evt_auth_key_request_t auth_key_request; /**< Authentication Key Request Event Parameters. */ - ble_gap_evt_lesc_dhkey_request_t lesc_dhkey_request; /**< LE Secure Connections DHKey calculation request. */ - ble_gap_evt_auth_status_t auth_status; /**< Authentication Status Event Parameters. */ - ble_gap_evt_conn_sec_update_t conn_sec_update; /**< Connection Security Update Event Parameters. */ - ble_gap_evt_timeout_t timeout; /**< Timeout Event Parameters. */ - ble_gap_evt_rssi_changed_t rssi_changed; /**< RSSI Event Parameters. */ - ble_gap_evt_adv_report_t adv_report; /**< Advertising Report Event Parameters. */ - ble_gap_evt_adv_set_terminated_t adv_set_terminated; /**< Advertising Set Terminated Event Parameters. */ - ble_gap_evt_sec_request_t sec_request; /**< Security Request Event Parameters. */ - ble_gap_evt_conn_param_update_request_t conn_param_update_request; /**< Connection Parameter Update Parameters. */ - ble_gap_evt_scan_req_report_t scan_req_report; /**< Scan Request Report Parameters. */ - ble_gap_evt_phy_update_request_t phy_update_request; /**< PHY Update Request Event Parameters. */ - ble_gap_evt_phy_update_t phy_update; /**< PHY Update Parameters. */ - ble_gap_evt_data_length_update_request_t data_length_update_request; /**< Data Length Update Request Event Parameters. */ - ble_gap_evt_data_length_update_t data_length_update; /**< Data Length Update Event Parameters. */ - ble_gap_evt_qos_channel_survey_report_t qos_channel_survey_report; /**< Quality of Service (QoS) Channel Survey Report Parameters. */ - } params; /**< Event Parameters. */ + uint16_t conn_handle; /**< Connection Handle on which event occurred. */ + union /**< union alternative identified by evt_id in enclosing struct. */ + { + ble_gap_evt_connected_t connected; /**< Connected Event Parameters. */ + ble_gap_evt_disconnected_t disconnected; /**< Disconnected Event Parameters. */ + ble_gap_evt_conn_param_update_t conn_param_update; /**< Connection Parameter Update Parameters. */ + ble_gap_evt_sec_params_request_t sec_params_request; /**< Security Parameters Request Event Parameters. */ + ble_gap_evt_sec_info_request_t sec_info_request; /**< Security Information Request Event Parameters. */ + ble_gap_evt_passkey_display_t passkey_display; /**< Passkey Display Event Parameters. */ + ble_gap_evt_key_pressed_t key_pressed; /**< Key Pressed Event Parameters. */ + ble_gap_evt_auth_key_request_t auth_key_request; /**< Authentication Key Request Event Parameters. */ + ble_gap_evt_lesc_dhkey_request_t lesc_dhkey_request; /**< LE Secure Connections DHKey calculation request. */ + ble_gap_evt_auth_status_t auth_status; /**< Authentication Status Event Parameters. */ + ble_gap_evt_conn_sec_update_t conn_sec_update; /**< Connection Security Update Event Parameters. */ + ble_gap_evt_timeout_t timeout; /**< Timeout Event Parameters. */ + ble_gap_evt_rssi_changed_t rssi_changed; /**< RSSI Event Parameters. */ + ble_gap_evt_adv_report_t adv_report; /**< Advertising Report Event Parameters. */ + ble_gap_evt_adv_set_terminated_t adv_set_terminated; /**< Advertising Set Terminated Event Parameters. */ + ble_gap_evt_sec_request_t sec_request; /**< Security Request Event Parameters. */ + ble_gap_evt_conn_param_update_request_t conn_param_update_request; /**< Connection Parameter Update Parameters. */ + ble_gap_evt_scan_req_report_t scan_req_report; /**< Scan Request Report Parameters. */ + ble_gap_evt_phy_update_request_t phy_update_request; /**< PHY Update Request Event Parameters. */ + ble_gap_evt_phy_update_t phy_update; /**< PHY Update Parameters. */ + ble_gap_evt_data_length_update_request_t data_length_update_request; /**< Data Length Update Request Event Parameters. */ + ble_gap_evt_data_length_update_t data_length_update; /**< Data Length Update Event Parameters. */ + ble_gap_evt_qos_channel_survey_report_t + qos_channel_survey_report; /**< Quality of Service (QoS) Channel Survey Report Parameters. */ + } params; /**< Event Parameters. */ } ble_gap_evt_t; /** @@ -1409,18 +1438,17 @@ typedef struct { * * @retval ::NRF_ERROR_CONN_COUNT The connection count for the connection configurations is zero. * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: - * - The sum of conn_count for all connection configurations combined exceeds - * UINT8_MAX. + * - The sum of conn_count for all connection configurations combined exceeds UINT8_MAX. * - The event length is smaller than @ref BLE_GAP_EVENT_LENGTH_MIN. */ typedef struct { - uint8_t conn_count; /**< The number of concurrent connections the application can create with this configuration. - The default and minimum value is @ref BLE_GAP_CONN_COUNT_DEFAULT. */ - uint16_t event_length; /**< The time set aside for this connection on every connection interval in 1.25 ms units. - The default value is @ref BLE_GAP_EVENT_LENGTH_DEFAULT, the minimum value is @ref - BLE_GAP_EVENT_LENGTH_MIN. The event length and the connection interval are the primary parameters - for setting the throughput of a connection. - See the SoftDevice Specification for details on throughput. */ + uint8_t conn_count; /**< The number of concurrent connections the application can create with this configuration. + The default and minimum value is @ref BLE_GAP_CONN_COUNT_DEFAULT. */ + uint16_t event_length; /**< The time set aside for this connection on every connection interval in 1.25 ms units. + The default value is @ref BLE_GAP_EVENT_LENGTH_DEFAULT, the minimum value is @ref + BLE_GAP_EVENT_LENGTH_MIN. The event length and the connection interval are the primary parameters + for setting the throughput of a connection. + See the SoftDevice Specification for details on throughput. */ } ble_gap_conn_cfg_t; /** @@ -1436,17 +1464,15 @@ typedef struct { * @ref BLE_GAP_ADV_SET_COUNT_MAX. */ typedef struct { - uint8_t adv_set_count; /**< Maximum number of advertising sets. Default value is @ref BLE_GAP_ADV_SET_COUNT_DEFAULT. */ - uint8_t periph_role_count; /**< Maximum number of connections concurrently acting as a peripheral. Default value is - @ref BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT. */ - uint8_t central_role_count; /**< Maximum number of connections concurrently acting as a central. Default value is @ref - BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT. */ - uint8_t central_sec_count; /**< Number of SMP instances shared between all connections acting as a central. Default - value is - @ref BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT. */ - uint8_t qos_channel_survey_role_available : 1; /**< If set, the Quality of Service (QoS) channel survey module is - available to the application using @ref - sd_ble_gap_qos_channel_survey_start. */ + uint8_t adv_set_count; /**< Maximum number of advertising sets. Default value is @ref BLE_GAP_ADV_SET_COUNT_DEFAULT. */ + uint8_t periph_role_count; /**< Maximum number of connections concurrently acting as a peripheral. Default value is @ref + BLE_GAP_ROLE_COUNT_PERIPH_DEFAULT. */ + uint8_t central_role_count; /**< Maximum number of connections concurrently acting as a central. Default value is @ref + BLE_GAP_ROLE_COUNT_CENTRAL_DEFAULT. */ + uint8_t central_sec_count; /**< Number of SMP instances shared between all connections acting as a central. Default value is + @ref BLE_GAP_ROLE_COUNT_CENTRAL_SEC_DEFAULT. */ + uint8_t qos_channel_survey_role_available : 1; /**< If set, the Quality of Service (QoS) channel survey module is available to + the application using @ref sd_ble_gap_qos_channel_survey_start. */ } ble_gap_cfg_role_count_t; /** @@ -1473,39 +1499,38 @@ typedef struct { * - Invalid device name location (vloc). * - Invalid device name security mode. * @retval ::NRF_ERROR_INVALID_LENGTH One or more of the following is true: - * - The device name length is invalid (must be between 0 and @ref - * BLE_GAP_DEVNAME_MAX_LEN). + * - The device name length is invalid (must be between 0 and @ref BLE_GAP_DEVNAME_MAX_LEN). * - The device name length is too long for the given Attribute Table. * @retval ::NRF_ERROR_NOT_SUPPORTED Device name security mode is not supported. */ typedef struct { - ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ - uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ - uint8_t *p_value; /**< Pointer to where the value (device name) is stored or will be stored. */ - uint16_t current_len; /**< Current length in bytes of the memory pointed to by p_value.*/ - uint16_t max_len; /**< Maximum length in bytes of the memory pointed to by p_value.*/ + ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ + uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ + uint8_t *p_value; /**< Pointer to where the value (device name) is stored or will be stored. */ + uint16_t current_len; /**< Current length in bytes of the memory pointed to by p_value.*/ + uint16_t max_len; /**< Maximum length in bytes of the memory pointed to by p_value.*/ } ble_gap_cfg_device_name_t; /**@brief Peripheral Preferred Connection Parameters include configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t include_cfg; /**< Inclusion configuration of the Peripheral Preferred Connection Parameters characteristic. - See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_PPCP_INCL_CONFIG_DEFAULT. */ + uint8_t include_cfg; /**< Inclusion configuration of the Peripheral Preferred Connection Parameters characteristic. + See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_PPCP_INCL_CONFIG_DEFAULT. */ } ble_gap_cfg_ppcp_incl_cfg_t; /**@brief Central Address Resolution include configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t include_cfg; /**< Inclusion configuration of the Central Address Resolution characteristic. - See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_CAR_INCL_CONFIG_DEFAULT. */ + uint8_t include_cfg; /**< Inclusion configuration of the Central Address Resolution characteristic. + See @ref BLE_GAP_CHAR_INCL_CONFIG. Default is @ref BLE_GAP_CAR_INCL_CONFIG_DEFAULT. */ } ble_gap_cfg_car_incl_cfg_t; /**@brief Configuration structure for GAP configurations. */ typedef union { - ble_gap_cfg_role_count_t role_count_cfg; /**< Role count configuration, cfg_id is @ref BLE_GAP_CFG_ROLE_COUNT. */ - ble_gap_cfg_device_name_t device_name_cfg; /**< Device name configuration, cfg_id is @ref BLE_GAP_CFG_DEVICE_NAME. */ - ble_gap_cfg_ppcp_incl_cfg_t ppcp_include_cfg; /**< Peripheral Preferred Connection Parameters characteristic include - configuration, cfg_id is @ref BLE_GAP_CFG_PPCP_INCL_CONFIG. */ - ble_gap_cfg_car_incl_cfg_t car_include_cfg; /**< Central Address Resolution characteristic include configuration, - cfg_id is @ref BLE_GAP_CFG_CAR_INCL_CONFIG. */ + ble_gap_cfg_role_count_t role_count_cfg; /**< Role count configuration, cfg_id is @ref BLE_GAP_CFG_ROLE_COUNT. */ + ble_gap_cfg_device_name_t device_name_cfg; /**< Device name configuration, cfg_id is @ref BLE_GAP_CFG_DEVICE_NAME. */ + ble_gap_cfg_ppcp_incl_cfg_t ppcp_include_cfg; /**< Peripheral Preferred Connection Parameters characteristic include + configuration, cfg_id is @ref BLE_GAP_CFG_PPCP_INCL_CONFIG. */ + ble_gap_cfg_car_incl_cfg_t car_include_cfg; /**< Central Address Resolution characteristic include configuration, + cfg_id is @ref BLE_GAP_CFG_CAR_INCL_CONFIG. */ } ble_gap_cfg_t; /**@brief Channel Map option. @@ -1532,8 +1557,8 @@ typedef union { * */ typedef struct { - uint16_t conn_handle; /**< Connection Handle (only applicable for get) */ - uint8_t ch_map[5]; /**< Channel Map (37-bit). */ + uint16_t conn_handle; /**< Connection Handle (only applicable for get) */ + uint8_t ch_map[5]; /**< Channel Map (37-bit). */ } ble_gap_opt_ch_map_t; /**@brief Local connection latency option. @@ -1559,10 +1584,10 @@ typedef struct { * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle */ - uint16_t requested_latency; /**< Requested local connection latency. */ - uint16_t *p_actual_latency; /**< Pointer to storage for the actual local connection latency (can be set to NULL to - skip return value). */ + uint16_t conn_handle; /**< Connection Handle */ + uint16_t requested_latency; /**< Requested local connection latency. */ + uint16_t *p_actual_latency; /**< Pointer to storage for the actual local connection latency (can be set to NULL to skip return + value). */ } ble_gap_opt_local_conn_latency_t; /**@brief Disable slave latency @@ -1578,8 +1603,8 @@ typedef struct { * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle */ - uint8_t disable; /**< For allowed values see @ref BLE_GAP_SLAVE_LATENCY */ + uint16_t conn_handle; /**< Connection Handle */ + uint8_t disable; /**< For allowed values see @ref BLE_GAP_SLAVE_LATENCY */ } ble_gap_opt_slave_latency_disable_t; /**@brief Passkey Option. @@ -1598,8 +1623,8 @@ typedef struct { * */ typedef struct { - uint8_t const *p_passkey; /**< Pointer to 6-digit ASCII string (digit 0..9 only, no NULL termination) passkey to be used - during pairing. If this is NULL, the SoftDevice will generate a random passkey if required.*/ + uint8_t const *p_passkey; /**< Pointer to 6-digit ASCII string (digit 0..9 only, no NULL termination) passkey to be used + during pairing. If this is NULL, the SoftDevice will generate a random passkey if required.*/ } ble_gap_opt_passkey_t; /**@brief Compatibility mode 1 option. @@ -1618,7 +1643,7 @@ typedef struct { * @retval ::NRF_ERROR_INVALID_STATE When connection creation is ongoing while mode 1 is set. */ typedef struct { - uint8_t enable : 1; /**< Enable compatibility mode 1.*/ + uint8_t enable : 1; /**< Enable compatibility mode 1.*/ } ble_gap_opt_compat_mode_1_t; /**@brief Authenticated payload timeout option. @@ -1641,32 +1666,32 @@ typedef struct { * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle */ - uint16_t auth_payload_timeout; /**< Requested timeout in 10 ms unit, see @ref BLE_GAP_AUTH_PAYLOAD_TIMEOUT. */ + uint16_t conn_handle; /**< Connection Handle */ + uint16_t auth_payload_timeout; /**< Requested timeout in 10 ms unit, see @ref BLE_GAP_AUTH_PAYLOAD_TIMEOUT. */ } ble_gap_opt_auth_payload_timeout_t; /**@brief Option structure for GAP options. */ typedef union { - ble_gap_opt_ch_map_t ch_map; /**< Parameters for the Channel Map option. */ - ble_gap_opt_local_conn_latency_t local_conn_latency; /**< Parameters for the Local connection latency option */ - ble_gap_opt_passkey_t passkey; /**< Parameters for the Passkey option.*/ - ble_gap_opt_compat_mode_1_t compat_mode_1; /**< Parameters for the compatibility mode 1 option.*/ - ble_gap_opt_auth_payload_timeout_t auth_payload_timeout; /**< Parameters for the authenticated payload timeout option.*/ - ble_gap_opt_slave_latency_disable_t slave_latency_disable; /**< Parameters for the Disable slave latency option */ + ble_gap_opt_ch_map_t ch_map; /**< Parameters for the Channel Map option. */ + ble_gap_opt_local_conn_latency_t local_conn_latency; /**< Parameters for the Local connection latency option */ + ble_gap_opt_passkey_t passkey; /**< Parameters for the Passkey option.*/ + ble_gap_opt_compat_mode_1_t compat_mode_1; /**< Parameters for the compatibility mode 1 option.*/ + ble_gap_opt_auth_payload_timeout_t auth_payload_timeout; /**< Parameters for the authenticated payload timeout option.*/ + ble_gap_opt_slave_latency_disable_t slave_latency_disable; /**< Parameters for the Disable slave latency option */ } ble_gap_opt_t; /**@brief Connection event triggering parameters. */ typedef struct { - uint8_t ppi_ch_id; /**< PPI channel to use. This channel should be regarded as reserved until - connection event PPI task triggering is stopped. - The PPI channel ID can not be one of the PPI channels reserved by - the SoftDevice. See @ref NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK. */ - uint32_t task_endpoint; /**< Task Endpoint to trigger. */ - uint16_t conn_evt_counter_start; /**< The connection event on which the task triggering should start. */ - uint16_t period_in_events; /**< Trigger period. Valid range is [1, 32767]. - If the device is in slave role and slave latency is enabled, - this parameter should be set to a multiple of (slave latency + 1) - to ensure low power operation. */ + uint8_t ppi_ch_id; /**< PPI channel to use. This channel should be regarded as reserved until + connection event PPI task triggering is stopped. + The PPI channel ID can not be one of the PPI channels reserved by + the SoftDevice. See @ref NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK. */ + uint32_t task_endpoint; /**< Task Endpoint to trigger. */ + uint16_t conn_evt_counter_start; /**< The connection event on which the task triggering should start. */ + uint16_t period_in_events; /**< Trigger period. Valid range is [1, 32767]. + If the device is in slave role and slave latency is enabled, + this parameter should be set to a multiple of (slave latency + 1) + to ensure low power operation. */ } ble_gap_conn_event_trigger_t; /**@} */ @@ -1706,8 +1731,7 @@ SVCALL(SD_BLE_GAP_ADDR_SET, uint32_t, sd_ble_gap_addr_set(ble_gap_addr_t const * /**@brief Get local Bluetooth identity address. * * @note This will always return the identity address irrespective of the privacy settings, - * i.e. the address type will always be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref - * BLE_GAP_ADDR_TYPE_RANDOM_STATIC. + * i.e. the address type will always be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref BLE_GAP_ADDR_TYPE_RANDOM_STATIC. * * @param[out] p_addr Pointer to address structure to be filled in. * @@ -1764,10 +1788,10 @@ SVCALL(SD_BLE_GAP_WHITELIST_SET, uint32_t, sd_ble_gap_whitelist_set(ble_gap_addr * @note Only one device identity list can be used at a time and the list is shared between the BLE roles. * The device identity list cannot be set if a BLE role is using the list. * - * @param[in] pp_id_keys Pointer to an array of peer identity addresses and peer IRKs, if NULL the device identity - * list will be cleared. - * @param[in] pp_local_irks Pointer to an array of local IRKs. Each entry in the array maps to the entry in pp_id_keys - * at the same index. To fill in the list with the currently set device IRK for all peers, set to NULL. + * @param[in] pp_id_keys Pointer to an array of peer identity addresses and peer IRKs, if NULL the device identity list will + * be cleared. + * @param[in] pp_local_irks Pointer to an array of local IRKs. Each entry in the array maps to the entry in pp_id_keys at the + * same index. To fill in the list with the currently set device IRK for all peers, set to NULL. * @param[in] len Length of the device identity list, maximum @ref BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT. * * @mscs @@ -1783,14 +1807,15 @@ SVCALL(SD_BLE_GAP_WHITELIST_SET, uint32_t, sd_ble_gap_whitelist_set(ble_gap_addr * @retval ::NRF_ERROR_INVALID_ADDR The device identity list (or one of its entries) provided is invalid. * This code may be returned if the local IRK list also has an invalid entry. * @retval ::BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE The device identity list is in use and cannot be set or cleared. - * @retval ::BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE The device identity list contains multiple entries with the same - * identity address. + * @retval ::BLE_ERROR_GAP_DEVICE_IDENTITIES_DUPLICATE The device identity list contains multiple entries with the same identity + * address. * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid address type is supplied. * @retval ::NRF_ERROR_DATA_SIZE The given device identity list size invalid (zero or too large); this can * only return when pp_id_keys is not NULL. */ SVCALL(SD_BLE_GAP_DEVICE_IDENTITIES_SET, uint32_t, - sd_ble_gap_device_identities_set(ble_gap_id_key_t const *const *pp_id_keys, ble_gap_irk_t const *const *pp_local_irks, uint8_t len)); + sd_ble_gap_device_identities_set(ble_gap_id_key_t const *const *pp_id_keys, ble_gap_irk_t const *const *pp_local_irks, + uint8_t len)); /**@brief Set privacy settings. * @@ -1821,9 +1846,8 @@ SVCALL(SD_BLE_GAP_PRIVACY_SET, uint32_t, sd_ble_gap_privacy_set(ble_gap_privacy_ /**@brief Get privacy settings. * - * @note ::ble_gap_privacy_params_t::p_device_irk must be initialized to NULL or a valid address before this function is - * called. If it is initialized to a valid address, the address pointed to will contain the current device IRK on - * return. + * @note ::ble_gap_privacy_params_t::p_device_irk must be initialized to NULL or a valid address before this function is called. + * If it is initialized to a valid address, the address pointed to will contain the current device IRK on return. * * @param[in,out] p_privacy_params Privacy settings. * @@ -1836,8 +1860,8 @@ SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_ /**@brief Configure an advertising set. Set, clear or update advertising and scan response data. * * @note The format of the advertising data will be checked by this call to ensure interoperability. - * Limitations imposed by this API call to the data provided include having a flags data type in the scan - * response data and duplicating the local name in the advertising data and scan response data. + * Limitations imposed by this API call to the data provided include having a flags data type in the scan response data and + * duplicating the local name in the advertising data and scan response data. * * @note In order to update advertising data while advertising, new advertising buffers must be provided. * @@ -1847,28 +1871,26 @@ SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_ * @endmscs * * @param[in,out] p_adv_handle Provide a pointer to a handle containing @ref - * BLE_GAP_ADV_SET_HANDLE_NOT_SET to configure a new advertising set. On success, a new handle is then returned through - * the pointer. Provide a pointer to an existing advertising handle to configure an existing advertising set. - * @param[in] p_adv_data Advertising data. If set to NULL, no advertising data will be - * used. See + * BLE_GAP_ADV_SET_HANDLE_NOT_SET to configure a new advertising set. On success, a new handle is then returned through the + * pointer. Provide a pointer to an existing advertising handle to configure an existing advertising set. + * @param[in] p_adv_data Advertising data. If set to NULL, no advertising data will be used. See * @ref ble_gap_adv_data_t. - * @param[in] p_adv_params Advertising parameters. When this function is used to update - * advertising data while advertising, this parameter must be NULL. See @ref ble_gap_adv_params_t. + * @param[in] p_adv_params Advertising parameters. When this function is used to update advertising + * data while advertising, this parameter must be NULL. See @ref ble_gap_adv_params_t. * * @retval ::NRF_SUCCESS Advertising set successfully configured. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied: * - Invalid advertising data configuration specified. See @ref * ble_gap_adv_data_t. - * - Invalid configuration of p_adv_params. See @ref - * ble_gap_adv_params_t. + * - Invalid configuration of p_adv_params. See @ref ble_gap_adv_params_t. * - Use of whitelist requested but whitelist has not been set, * see @ref sd_ble_gap_whitelist_set. * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR ble_gap_adv_params_t::p_peer_addr is invalid. * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: - * - It is invalid to provide non-NULL advertising set parameters - * while advertising. - * - It is invalid to provide the same data buffers while - * advertising. To update advertising data, provide new advertising buffers. + * - It is invalid to provide non-NULL advertising set parameters while + * advertising. + * - It is invalid to provide the same data buffers while advertising. To + * update advertising data, provide new advertising buffers. * @retval ::BLE_ERROR_GAP_DISCOVERABLE_WITH_WHITELIST Discoverable mode and whitelist incompatible. * @retval ::BLE_ERROR_INVALID_ADV_HANDLE The provided advertising handle was not found. Use @ref * BLE_GAP_ADV_SET_HANDLE_NOT_SET to configure a new advertising handle. @@ -1878,12 +1900,13 @@ SVCALL(SD_BLE_GAP_PRIVACY_GET, uint32_t, sd_ble_gap_privacy_get(ble_gap_privacy_ * specification given in Bluetooth Specification Version 5.0, Volume 3, Part C, Chapter 11. * @retval ::NRF_ERROR_INVALID_LENGTH Invalid data length(s) supplied. * @retval ::NRF_ERROR_NOT_SUPPORTED Unsupported data length or advertising parameter configuration. - * @retval ::NRF_ERROR_NO_MEM Not enough memory to configure a new advertising handle. Update - * an existing advertising handle instead. + * @retval ::NRF_ERROR_NO_MEM Not enough memory to configure a new advertising handle. Update an + * existing advertising handle instead. * @retval ::BLE_ERROR_GAP_UUID_LIST_MISMATCH Invalid UUID list supplied. */ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, - sd_ble_gap_adv_set_configure(uint8_t *p_adv_handle, ble_gap_adv_data_t const *p_adv_data, ble_gap_adv_params_t const *p_adv_params)); + sd_ble_gap_adv_set_configure(uint8_t *p_adv_handle, ble_gap_adv_data_t const *p_adv_data, + ble_gap_adv_params_t const *p_adv_params)); /**@brief Start advertising (GAP Discoverable, Connectable modes, Broadcast Procedure). * @@ -1907,8 +1930,7 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, * * @param[in] adv_handle Advertising handle to advertise on, received from @ref sd_ble_gap_adv_set_configure. * @param[in] conn_cfg_tag Tag identifying a configuration set by @ref sd_ble_cfg_set or - * @ref BLE_CONN_CFG_TAG_DEFAULT to use the default connection configuration. For - non-connectable + * @ref BLE_CONN_CFG_TAG_DEFAULT to use the default connection configuration. For non-connectable * advertising, this is ignored. * * @retval ::NRF_SUCCESS The BLE stack has started advertising. @@ -1916,8 +1938,7 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, * @retval ::NRF_ERROR_CONN_COUNT The limit of available connections for this connection configuration * tag has been reached; connectable advertiser cannot be started. * To increase the number of available connections, - * use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or @ref - BLE_CONN_CFG_GAP. + * use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or @ref BLE_CONN_CFG_GAP. * @retval ::BLE_ERROR_INVALID_ADV_HANDLE Advertising handle not found. Configure a new adveriting handle with @ref sd_ble_gap_adv_set_configure. * @retval ::NRF_ERROR_NOT_FOUND conn_cfg_tag not found. @@ -1926,21 +1947,16 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, * - Use of whitelist requested but whitelist has not been set, see @ref sd_ble_gap_whitelist_set. * @retval ::NRF_ERROR_RESOURCES Either: - * - adv_handle is configured with connectable advertising, but the event_length - parameter - * associated with conn_cfg_tag is too small to be able to establish a - connection on - * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event - length. + * - adv_handle is configured with connectable advertising, but the event_length parameter + * associated with conn_cfg_tag is too small to be able to establish a connection on + * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event length. * - Not enough BLE role slots available. - Stop one or more currently active roles (Central, Peripheral, Broadcaster or - Observer) and try again. - * - p_adv_params is configured with connectable advertising, but the - event_length parameter - * associated with conn_cfg_tag is too small to be able to establish a - connection on - * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event - length. + Stop one or more currently active roles (Central, Peripheral, Broadcaster or Observer) + and try again. + * - p_adv_params is configured with connectable advertising, but the event_length + parameter + * associated with conn_cfg_tag is too small to be able to establish a connection on + * the selected advertising phys. Use @ref sd_ble_cfg_set to increase the event length. */ SVCALL(SD_BLE_GAP_ADV_START, uint32_t, sd_ble_gap_adv_start(uint8_t adv_handle, uint8_t conn_cfg_tag)); @@ -1966,8 +1982,8 @@ SVCALL(SD_BLE_GAP_ADV_STOP, uint32_t, sd_ble_gap_adv_stop(uint8_t adv_handle)); * the central to perform the procedure. In both cases, and regardless of success or failure, the application * will be informed of the result with a @ref BLE_GAP_EVT_CONN_PARAM_UPDATE event. * - * @details This function can be used as a central both to reply to a @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST or to - * start the procedure unrequested. + * @details This function can be used as a central both to reply to a @ref BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST or to start the + * procedure unrequested. * * @events * @event{@ref BLE_GAP_EVT_CONN_PARAM_UPDATE, Result of the connection parameter update procedure.} @@ -1995,7 +2011,8 @@ SVCALL(SD_BLE_GAP_ADV_STOP, uint32_t, sd_ble_gap_adv_stop(uint8_t adv_handle)); * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. */ -SVCALL(SD_BLE_GAP_CONN_PARAM_UPDATE, uint32_t, sd_ble_gap_conn_param_update(uint16_t conn_handle, ble_gap_conn_params_t const *p_conn_params)); +SVCALL(SD_BLE_GAP_CONN_PARAM_UPDATE, uint32_t, + sd_ble_gap_conn_param_update(uint16_t conn_handle, ble_gap_conn_params_t const *p_conn_params)); /**@brief Disconnect (GAP Link Termination). * @@ -2027,8 +2044,8 @@ SVCALL(SD_BLE_GAP_DISCONNECT, uint32_t, sd_ble_gap_disconnect(uint16_t conn_hand * possible roles. * @param[in] handle The handle parameter is interpreted depending on role: * - If role is @ref BLE_GAP_TX_POWER_ROLE_CONN, this value is the specific connection handle. - * - If role is @ref BLE_GAP_TX_POWER_ROLE_ADV, the advertising set identified with the advertising - * handle, will use the specified transmit power, and include it in the advertising packet headers if + * - If role is @ref BLE_GAP_TX_POWER_ROLE_ADV, the advertising set identified with the advertising handle, + * will use the specified transmit power, and include it in the advertising packet headers if * @ref ble_gap_adv_properties_t::include_tx_power set. * - For all other roles handle is ignored. * @param[in] tx_power Radio transmit power in dBm (see note for accepted values). @@ -2095,8 +2112,8 @@ SVCALL(SD_BLE_GAP_PPCP_GET, uint32_t, sd_ble_gap_ppcp_get(ble_gap_conn_params_t * * @param[in] p_write_perm Write permissions for the Device Name characteristic, see @ref ble_gap_conn_sec_mode_t. * @param[in] p_dev_name Pointer to a UTF-8 encoded, non NULL-terminated string. - * @param[in] len Length of the UTF-8, non NULL-terminated string pointed to by p_dev_name in octets (must be - * smaller or equal than @ref BLE_GAP_DEVNAME_MAX_LEN). + * @param[in] len Length of the UTF-8, non NULL-terminated string pointed to by p_dev_name in octets (must be smaller or + * equal than @ref BLE_GAP_DEVNAME_MAX_LEN). * * @retval ::NRF_SUCCESS GAP device name and permissions set successfully. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2114,8 +2131,8 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_SET, uint32_t, * and not the number of bytes actually returned in p_dev_name. * The application may use this information to allocate a suitable buffer size. * - * @param[out] p_dev_name Pointer to an empty buffer where the UTF-8 non NULL-terminated string will be - * placed. Set to NULL to obtain the complete device name length. + * @param[out] p_dev_name Pointer to an empty buffer where the UTF-8 non NULL-terminated string will be placed. Set to + * NULL to obtain the complete device name length. * @param[in,out] p_len Length of the buffer pointed by p_dev_name, complete device name length on output. * * @retval ::NRF_SUCCESS GAP device name retrieved successfully. @@ -2159,9 +2176,9 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t * @endmscs * * @param[in] conn_handle Connection handle. - * @param[in] p_sec_params Pointer to the @ref ble_gap_sec_params_t structure with the security parameters to be used - * during the pairing or bonding procedure. In the peripheral role, only the bond, mitm, lesc and keypress fields of - * this structure are used. In the central role, this pointer may be NULL to reject a Security Request. + * @param[in] p_sec_params Pointer to the @ref ble_gap_sec_params_t structure with the security parameters to be used during the + * pairing or bonding procedure. In the peripheral role, only the bond, mitm, lesc and keypress fields of this structure are used. + * In the central role, this pointer may be NULL to reject a Security Request. * * @retval ::NRF_SUCCESS Successfully initiated authentication procedure. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2169,8 +2186,8 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either: * - No link has been established. * - An encryption is already executing or queued. - * @retval ::NRF_ERROR_NO_MEM The maximum number of authentication procedures that can run in parallel for the given - * role is reached. + * @retval ::NRF_ERROR_NO_MEM The maximum number of authentication procedures that can run in parallel for the given role is + * reached. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::NRF_ERROR_NOT_SUPPORTED Setting of sign or link fields in @ref ble_gap_sec_kdist_t not supported. * Distribution of own Identity Information is only supported if the Central @@ -2179,14 +2196,15 @@ SVCALL(SD_BLE_GAP_DEVICE_NAME_GET, uint32_t, sd_ble_gap_device_name_get(uint8_t * See @ref ble_gap_cfg_car_incl_cfg_t and @ref ble_gap_cfg_role_count_t. * @retval ::NRF_ERROR_TIMEOUT A SMP timeout has occurred, and further SMP operations on this link is prohibited. */ -SVCALL(SD_BLE_GAP_AUTHENTICATE, uint32_t, sd_ble_gap_authenticate(uint16_t conn_handle, ble_gap_sec_params_t const *p_sec_params)); +SVCALL(SD_BLE_GAP_AUTHENTICATE, uint32_t, + sd_ble_gap_authenticate(uint16_t conn_handle, ble_gap_sec_params_t const *p_sec_params)); /**@brief Reply with GAP security parameters. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST, calling it at other times will - * result in an @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with - * corrected parameters. + * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST, calling it at other times will result in + * an @ref NRF_ERROR_INVALID_STATE. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected + * parameters. * * @events * @event{This function is used during authentication procedures, see the list of events in the documentation of @ref @@ -2222,16 +2240,15 @@ SVCALL(SD_BLE_GAP_AUTHENTICATE, uint32_t, sd_ble_gap_authenticate(uint16_t conn_ * * @param[in] conn_handle Connection handle. * @param[in] sec_status Security status, see @ref BLE_GAP_SEC_STATUS. - * @param[in] p_sec_params Pointer to a @ref ble_gap_sec_params_t security parameters structure. In the central role - * this must be set to NULL, as the parameters have already been provided during a previous call to @ref - * sd_ble_gap_authenticate. - * @param[in,out] p_sec_keyset Pointer to a @ref ble_gap_sec_keyset_t security keyset structure. Any keys generated - * and/or distributed as a result of the ongoing security procedure will be stored into the memory referenced by the - * pointers inside this structure. The keys will be stored and available to the application upon reception of a @ref - * BLE_GAP_EVT_AUTH_STATUS event. Note that the SoftDevice expects the application to provide memory for storing the - * peer's keys. So it must be ensured that the relevant pointers inside this structure are not - * NULL. The pointers to the local key can, however, be NULL, in which case, the local key data will not be available to - * the application upon reception of the + * @param[in] p_sec_params Pointer to a @ref ble_gap_sec_params_t security parameters structure. In the central role this must be + * set to NULL, as the parameters have already been provided during a previous call to @ref sd_ble_gap_authenticate. + * @param[in,out] p_sec_keyset Pointer to a @ref ble_gap_sec_keyset_t security keyset structure. Any keys generated and/or + * distributed as a result of the ongoing security procedure will be stored into the memory referenced by the pointers inside this + * structure. The keys will be stored and available to the application upon reception of a @ref BLE_GAP_EVT_AUTH_STATUS event. + * Note that the SoftDevice expects the application to provide memory for storing the + * peer's keys. So it must be ensured that the relevant pointers inside this structure are not NULL. The + * pointers to the local key can, however, be NULL, in which case, the local key data will not be available to the application + * upon reception of the * @ref BLE_GAP_EVT_AUTH_STATUS event. * * @retval ::NRF_SUCCESS Successfully accepted security parameter from the application. @@ -2252,10 +2269,10 @@ SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, /**@brief Reply with an authentication key. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_AUTH_KEY_REQUEST or a @ref - * BLE_GAP_EVT_PASSKEY_DISPLAY, calling it at other times will result in an @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with - * corrected parameters. + * @details This function is only used to reply to a @ref BLE_GAP_EVT_AUTH_KEY_REQUEST or a @ref BLE_GAP_EVT_PASSKEY_DISPLAY, + * calling it at other times will result in an @ref NRF_ERROR_INVALID_STATE. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected + * parameters. * * @events * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref @@ -2274,9 +2291,9 @@ SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, * @param[in] conn_handle Connection handle. * @param[in] key_type See @ref BLE_GAP_AUTH_KEY_TYPES. * @param[in] p_key If key type is @ref BLE_GAP_AUTH_KEY_TYPE_NONE, then NULL. - * If key type is @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY, then a 6-byte ASCII string (digit 0..9 only, no - * NULL termination) or NULL when confirming LE Secure Connections Numeric Comparison. If key type is @ref - * BLE_GAP_AUTH_KEY_TYPE_OOB, then a 16-byte OOB key value in little-endian format. + * If key type is @ref BLE_GAP_AUTH_KEY_TYPE_PASSKEY, then a 6-byte ASCII string (digit 0..9 only, no NULL + * termination) or NULL when confirming LE Secure Connections Numeric Comparison. If key type is @ref BLE_GAP_AUTH_KEY_TYPE_OOB, + * then a 16-byte OOB key value in little-endian format. * * @retval ::NRF_SUCCESS Authentication key successfully set. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2284,14 +2301,15 @@ SVCALL(SD_BLE_GAP_SEC_PARAMS_REPLY, uint32_t, * @retval ::NRF_ERROR_INVALID_STATE Authentication key has not been requested. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ -SVCALL(SD_BLE_GAP_AUTH_KEY_REPLY, uint32_t, sd_ble_gap_auth_key_reply(uint16_t conn_handle, uint8_t key_type, uint8_t const *p_key)); +SVCALL(SD_BLE_GAP_AUTH_KEY_REPLY, uint32_t, + sd_ble_gap_auth_key_reply(uint16_t conn_handle, uint8_t key_type, uint8_t const *p_key)); /**@brief Reply with an LE Secure connections DHKey. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST, calling it at other times will - * result in an @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with - * corrected parameters. + * @details This function is only used to reply to a @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST, calling it at other times will result in + * an @ref NRF_ERROR_INVALID_STATE. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected + * parameters. * * @events * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref @@ -2322,7 +2340,8 @@ SVCALL(SD_BLE_GAP_AUTH_KEY_REPLY, uint32_t, sd_ble_gap_auth_key_reply(uint16_t c * - The application has not pulled a @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST event. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ -SVCALL(SD_BLE_GAP_LESC_DHKEY_REPLY, uint32_t, sd_ble_gap_lesc_dhkey_reply(uint16_t conn_handle, ble_gap_lesc_dhkey_t const *p_dhkey)); +SVCALL(SD_BLE_GAP_LESC_DHKEY_REPLY, uint32_t, + sd_ble_gap_lesc_dhkey_reply(uint16_t conn_handle, ble_gap_lesc_dhkey_t const *p_dhkey)); /**@brief Notify the peer of a local keypress. * @@ -2347,17 +2366,16 @@ SVCALL(SD_BLE_GAP_KEYPRESS_NOTIFY, uint32_t, sd_ble_gap_keypress_notify(uint16_t /**@brief Generate a set of OOB data to send to a peer out of band. * - * @note The @ref ble_gap_addr_t included in the OOB data returned will be the currently active one (or, if a - * connection has already been established, the one used during connection setup). The application may manually - * overwrite it with an updated value. + * @note The @ref ble_gap_addr_t included in the OOB data returned will be the currently active one (or, if a connection has + * already been established, the one used during connection setup). The application may manually overwrite it with an updated + * value. * * @mscs * @mmsc{@ref BLE_GAP_PERIPH_LESC_BONDING_OOB_MSC} * @mmsc{@ref BLE_GAP_CENTRAL_LESC_BONDING_OOB_MSC} * @endmscs * - * @param[in] conn_handle Connection handle. Can be @ref BLE_CONN_HANDLE_INVALID if a BLE connection has not been - * established yet. + * @param[in] conn_handle Connection handle. Can be @ref BLE_CONN_HANDLE_INVALID if a BLE connection has not been established yet. * @param[in] p_pk_own LE Secure Connections local P-256 Public Key. * @param[out] p_oobd_own The OOB data to be sent out of band to a peer. * @@ -2366,13 +2384,14 @@ SVCALL(SD_BLE_GAP_KEYPRESS_NOTIFY, uint32_t, sd_ble_gap_keypress_notify(uint16_t * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_GET, uint32_t, - sd_ble_gap_lesc_oob_data_get(uint16_t conn_handle, ble_gap_lesc_p256_pk_t const *p_pk_own, ble_gap_lesc_oob_data_t *p_oobd_own)); + sd_ble_gap_lesc_oob_data_get(uint16_t conn_handle, ble_gap_lesc_p256_pk_t const *p_pk_own, + ble_gap_lesc_oob_data_t *p_oobd_own)); /**@brief Provide the OOB data sent/received out of band. * * @note An authentication procedure with OOB selected as an algorithm must be in progress when calling this function. - * @note A @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST event with the oobd_req set to 1 must have been received prior to - * calling this function. + * @note A @ref BLE_GAP_EVT_LESC_DHKEY_REQUEST event with the oobd_req set to 1 must have been received prior to calling this + * function. * * @events * @event{This function is used during authentication procedures\, see the list of events in the documentation of @ref @@ -2401,12 +2420,12 @@ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_GET, uint32_t, * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. */ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_SET, uint32_t, - sd_ble_gap_lesc_oob_data_set(uint16_t conn_handle, ble_gap_lesc_oob_data_t const *p_oobd_own, ble_gap_lesc_oob_data_t const *p_oobd_peer)); + sd_ble_gap_lesc_oob_data_set(uint16_t conn_handle, ble_gap_lesc_oob_data_t const *p_oobd_own, + ble_gap_lesc_oob_data_t const *p_oobd_peer)); /**@brief Initiate GAP Encryption procedure. * - * @details In the central role, this function will initiate the encryption procedure using the encryption information - * provided. + * @details In the central role, this function will initiate the encryption procedure using the encryption information provided. * * @events * @event{@ref BLE_GAP_EVT_CONN_SEC_UPDATE, The connection security has been updated.} @@ -2428,19 +2447,18 @@ SVCALL(SD_BLE_GAP_LESC_OOB_DATA_SET, uint32_t, * @retval ::NRF_ERROR_INVALID_STATE No link has been established. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::BLE_ERROR_INVALID_ROLE Operation is not supported in the Peripheral role. - * @retval ::NRF_ERROR_BUSY Procedure already in progress or not allowed at this time, wait for pending procedures to - * complete and retry. + * @retval ::NRF_ERROR_BUSY Procedure already in progress or not allowed at this time, wait for pending procedures to complete and + * retry. */ SVCALL(SD_BLE_GAP_ENCRYPT, uint32_t, sd_ble_gap_encrypt(uint16_t conn_handle, ble_gap_master_id_t const *p_master_id, ble_gap_enc_info_t const *p_enc_info)); /**@brief Reply with GAP security information. * - * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_INFO_REQUEST, calling it at other times will - * result in + * @details This function is only used to reply to a @ref BLE_GAP_EVT_SEC_INFO_REQUEST, calling it at other times will result in * @ref NRF_ERROR_INVALID_STATE. - * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with - * corrected parameters. + * @note If the call returns an error code, the request is still pending, and the reply call may be repeated with corrected + * parameters. * @note Data signing is not yet supported, and p_sign_info must therefore be NULL. * * @mscs @@ -2448,12 +2466,11 @@ SVCALL(SD_BLE_GAP_ENCRYPT, uint32_t, * @endmscs * * @param[in] conn_handle Connection handle. - * @param[in] p_enc_info Pointer to a @ref ble_gap_enc_info_t encryption information structure. May be NULL to signal - * none is available. - * @param[in] p_id_info Pointer to a @ref ble_gap_irk_t identity information structure. May be NULL to signal none is + * @param[in] p_enc_info Pointer to a @ref ble_gap_enc_info_t encryption information structure. May be NULL to signal none is + * available. + * @param[in] p_id_info Pointer to a @ref ble_gap_irk_t identity information structure. May be NULL to signal none is available. + * @param[in] p_sign_info Pointer to a @ref ble_gap_sign_info_t signing information structure. May be NULL to signal none is * available. - * @param[in] p_sign_info Pointer to a @ref ble_gap_sign_info_t signing information structure. May be NULL to signal - * none is available. * * @retval ::NRF_SUCCESS Successfully accepted security information. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. @@ -2495,8 +2512,8 @@ SVCALL(SD_BLE_GAP_CONN_SEC_GET, uint32_t, sd_ble_gap_conn_sec_get(uint16_t conn_ * @endmscs * * @param[in] conn_handle Connection handle. - * @param[in] threshold_dbm Minimum change in dBm before triggering the @ref BLE_GAP_EVT_RSSI_CHANGED event. Events - * are disabled if threshold_dbm equals @ref BLE_GAP_RSSI_THRESHOLD_INVALID. + * @param[in] threshold_dbm Minimum change in dBm before triggering the @ref BLE_GAP_EVT_RSSI_CHANGED event. Events are + * disabled if threshold_dbm equals @ref BLE_GAP_RSSI_THRESHOLD_INVALID. * @param[in] skip_count Number of RSSI samples with a change of threshold_dbm or more before sending a new @ref * BLE_GAP_EVT_RSSI_CHANGED event. * @@ -2526,9 +2543,8 @@ SVCALL(SD_BLE_GAP_RSSI_STOP, uint32_t, sd_ble_gap_rssi_stop(uint16_t conn_handle /**@brief Get the received signal strength for the last connection event. * - * @ref sd_ble_gap_rssi_start must be called to start reporting RSSI before using this function. @ref - * NRF_ERROR_NOT_FOUND will be returned until RSSI was sampled for the first time after calling @ref - * sd_ble_gap_rssi_start. + * @ref sd_ble_gap_rssi_start must be called to start reporting RSSI before using this function. @ref NRF_ERROR_NOT_FOUND + * will be returned until RSSI was sampled for the first time after calling @ref sd_ble_gap_rssi_start. * @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature measurement. * @mscs * @mmsc{@ref BLE_GAP_CENTRAL_RSSI_READ_MSC} @@ -2556,16 +2572,14 @@ SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, * - @ref sd_ble_gap_scan_stop is called. * - @ref sd_ble_gap_connect is called. * - A @ref BLE_GAP_EVT_TIMEOUT with source set to @ref BLE_GAP_TIMEOUT_SRC_SCAN is received. - * - When a @ref BLE_GAP_EVT_ADV_REPORT event is received and @ref ble_gap_adv_report_type_t::status is not - * set to - * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. In this case scanning is only paused to let the - * application access received data. The application must call this function to continue scanning, or call @ref + * - When a @ref BLE_GAP_EVT_ADV_REPORT event is received and @ref ble_gap_adv_report_type_t::status is not set to + * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA. In this case scanning is only paused to let the application + * access received data. The application must call this function to continue scanning, or call @ref * sd_ble_gap_scan_stop to stop scanning. * * @note If a @ref BLE_GAP_EVT_ADV_REPORT event is received with @ref ble_gap_adv_report_type_t::status set to - * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the scanner will continue scanning, and the application - * will receive more reports from this advertising event. The following reports will include the old and new received - * data. + * @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_MORE_DATA, the scanner will continue scanning, and the application will + * receive more reports from this advertising event. The following reports will include the old and new received data. * * @events * @event{@ref BLE_GAP_EVT_ADV_REPORT, An advertising or scan response packet has been received.} @@ -2583,8 +2597,8 @@ SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, * The memory pointed to should be kept alive until the scanning is stopped. * See @ref BLE_GAP_SCAN_BUFFER_SIZE for minimum and maximum buffer size. * If the scanner receives advertising data larger than can be stored in the buffer, - * a @ref BLE_GAP_EVT_ADV_REPORT will be raised with @ref - * ble_gap_adv_report_type_t::status set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED. + * a @ref BLE_GAP_EVT_ADV_REPORT will be raised with @ref ble_gap_adv_report_type_t::status + * set to @ref BLE_GAP_ADV_DATA_STATUS_INCOMPLETE_TRUNCATED. * * @retval ::NRF_SUCCESS Successfully initiated scanning procedure. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -2596,10 +2610,10 @@ SVCALL(SD_BLE_GAP_RSSI_GET, uint32_t, sd_ble_gap_rssi_get(uint16_t conn_handle, * @retval ::NRF_ERROR_NOT_SUPPORTED Unsupported parameters supplied. See @ref ble_gap_scan_params_t. * @retval ::NRF_ERROR_INVALID_LENGTH The provided buffer length is invalid. See @ref BLE_GAP_SCAN_BUFFER_MIN. * @retval ::NRF_ERROR_RESOURCES Not enough BLE role slots available. - * Stop one or more currently active roles (Central, Peripheral or Broadcaster) and try - * again + * Stop one or more currently active roles (Central, Peripheral or Broadcaster) and try again */ -SVCALL(SD_BLE_GAP_SCAN_START, uint32_t, sd_ble_gap_scan_start(ble_gap_scan_params_t const *p_scan_params, ble_data_t const *p_adv_report_buffer)); +SVCALL(SD_BLE_GAP_SCAN_START, uint32_t, + sd_ble_gap_scan_start(ble_gap_scan_params_t const *p_scan_params, ble_data_t const *p_adv_report_buffer)); /**@brief Stop scanning (GAP Discovery procedure, Observer Procedure). * @@ -2647,23 +2661,22 @@ SVCALL(SD_BLE_GAP_SCAN_STOP, uint32_t, sd_ble_gap_scan_stop(void)); * - Peer address was not present in the device identity list, see @ref * sd_ble_gap_device_identities_set. * @retval ::NRF_ERROR_NOT_FOUND conn_cfg_tag not found. - * @retval ::NRF_ERROR_INVALID_STATE The SoftDevice is in an invalid state to perform this operation. This may be due to - * an existing locally initiated connect procedure, which must complete before initiating again. + * @retval ::NRF_ERROR_INVALID_STATE The SoftDevice is in an invalid state to perform this operation. This may be due to an + * existing locally initiated connect procedure, which must complete before initiating again. * @retval ::BLE_ERROR_GAP_INVALID_BLE_ADDR Invalid Peer address. - * @retval ::NRF_ERROR_CONN_COUNT The limit of available connections for this connection configuration tag has been - * reached. To increase the number of available connections, use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or - * @ref BLE_CONN_CFG_GAP. + * @retval ::NRF_ERROR_CONN_COUNT The limit of available connections for this connection configuration tag has been reached. + * To increase the number of available connections, + * use @ref sd_ble_cfg_set with @ref BLE_GAP_CFG_ROLE_COUNT or @ref BLE_CONN_CFG_GAP. * @retval ::NRF_ERROR_RESOURCES Either: * - Not enough BLE role slots available. - * Stop one or more currently active roles (Central, Peripheral or Observer) and try - * again. + * Stop one or more currently active roles (Central, Peripheral or Observer) and try again. * - The event_length parameter associated with conn_cfg_tag is too small to be able to * establish a connection on the selected @ref ble_gap_scan_params_t::scan_phys. * Use @ref sd_ble_cfg_set to increase the event length. */ SVCALL(SD_BLE_GAP_CONNECT, uint32_t, - sd_ble_gap_connect(ble_gap_addr_t const *p_peer_addr, ble_gap_scan_params_t const *p_scan_params, ble_gap_conn_params_t const *p_conn_params, - uint8_t conn_cfg_tag)); + sd_ble_gap_connect(ble_gap_addr_t const *p_peer_addr, ble_gap_scan_params_t const *p_scan_params, + ble_gap_conn_params_t const *p_conn_params, uint8_t conn_cfg_tag)); /**@brief Cancel a connection establishment. * @@ -2724,13 +2737,11 @@ SVCALL(SD_BLE_GAP_CONNECT_CANCEL, uint32_t, sd_ble_gap_connect_cancel(void)); * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. * @retval ::NRF_ERROR_INVALID_STATE No link has been established. - * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the - * combination of + * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the combination of * @ref ble_gap_phys_t::tx_phys, @ref ble_gap_phys_t::rx_phys, and @ref - * ble_gap_data_length_params_t. The connection event length is configured with @ref BLE_CONN_CFG_GAP using @ref - * sd_ble_cfg_set. - * @retval ::NRF_ERROR_BUSY Procedure is already in progress or not allowed at this time. Process pending events and - * wait for the pending procedure to complete and retry. + * ble_gap_data_length_params_t. The connection event length is configured with @ref BLE_CONN_CFG_GAP using @ref sd_ble_cfg_set. + * @retval ::NRF_ERROR_BUSY Procedure is already in progress or not allowed at this time. Process pending events and wait for the + * pending procedure to complete and retry. * */ SVCALL(SD_BLE_GAP_PHY_UPDATE, uint32_t, sd_ble_gap_phy_update(uint16_t conn_handle, ble_gap_phys_t const *p_gap_phys)); @@ -2764,9 +2775,9 @@ SVCALL(SD_BLE_GAP_PHY_UPDATE, uint32_t, sd_ble_gap_phy_update(uint16_t conn_hand * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameters supplied. * @retval ::NRF_ERROR_NOT_SUPPORTED The requested parameters are not supported by the SoftDevice. Inspect * p_dl_limitation to see which parameter is not supported. - * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the - * requested parameters. Use @ref sd_ble_cfg_set with @ref BLE_CONN_CFG_GAP to increase the connection event length. - * Inspect p_dl_limitation to see where the limitation is. + * @retval ::NRF_ERROR_RESOURCES The connection event length configured for this link is not sufficient for the requested + * parameters. Use @ref sd_ble_cfg_set with @ref BLE_CONN_CFG_GAP to increase the connection event length. Inspect p_dl_limitation + * to see where the limitation is. * @retval ::NRF_ERROR_BUSY Peer has already initiated a Data Length Update Procedure. Process the * pending @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST event to respond. */ @@ -2837,7 +2848,8 @@ SVCALL(SD_BLE_GAP_QOS_CHANNEL_SURVEY_STOP, uint32_t, sd_ble_gap_qos_channel_surv * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle parameter supplied. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. */ -SVCALL(SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET, uint32_t, sd_ble_gap_next_conn_evt_counter_get(uint16_t conn_handle, uint16_t *p_counter)); +SVCALL(SD_BLE_GAP_NEXT_CONN_EVT_COUNTER_GET, uint32_t, + sd_ble_gap_next_conn_evt_counter_get(uint16_t conn_handle, uint16_t *p_counter)); /**@brief Start triggering a given task on connection event start. * diff --git a/src/platform/nrf52/softdevice/ble_gatt.h b/src/platform/nrf52/softdevice/ble_gatt.h index 31f6a026b..df0d728fc 100644 --- a/src/platform/nrf52/softdevice/ble_gatt.h +++ b/src/platform/nrf52/softdevice/ble_gatt.h @@ -112,34 +112,34 @@ extern "C" { #define BLE_GATT_STATUS_ATTERR_INSUF_AUTHENTICATION 0x0105 /**< ATT Error: Authenticated link required. */ #define BLE_GATT_STATUS_ATTERR_REQUEST_NOT_SUPPORTED 0x0106 /**< ATT Error: Used in ATT as Request Not Supported. */ #define BLE_GATT_STATUS_ATTERR_INVALID_OFFSET 0x0107 /**< ATT Error: Offset specified was past the end of the attribute. */ -#define BLE_GATT_STATUS_ATTERR_INSUF_AUTHORIZATION \ - 0x0108 /**< ATT Error: Used in ATT as Insufficient Authorization. \ - */ -#define BLE_GATT_STATUS_ATTERR_PREPARE_QUEUE_FULL 0x0109 /**< ATT Error: Used in ATT as Prepare Queue Full. */ -#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_FOUND 0x010A /**< ATT Error: Used in ATT as Attribute not found. */ -#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_LONG 0x010B /**< ATT Error: Attribute cannot be read or written using read/write blob requests. */ +#define BLE_GATT_STATUS_ATTERR_INSUF_AUTHORIZATION 0x0108 /**< ATT Error: Used in ATT as Insufficient Authorization. */ +#define BLE_GATT_STATUS_ATTERR_PREPARE_QUEUE_FULL 0x0109 /**< ATT Error: Used in ATT as Prepare Queue Full. */ +#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_FOUND 0x010A /**< ATT Error: Used in ATT as Attribute not found. */ +#define BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_LONG \ + 0x010B /**< ATT Error: Attribute cannot be read or written using read/write blob requests. */ #define BLE_GATT_STATUS_ATTERR_INSUF_ENC_KEY_SIZE 0x010C /**< ATT Error: Encryption key size used is insufficient. */ #define BLE_GATT_STATUS_ATTERR_INVALID_ATT_VAL_LENGTH 0x010D /**< ATT Error: Invalid value size. */ #define BLE_GATT_STATUS_ATTERR_UNLIKELY_ERROR 0x010E /**< ATT Error: Very unlikely error. */ #define BLE_GATT_STATUS_ATTERR_INSUF_ENCRYPTION 0x010F /**< ATT Error: Encrypted link required. */ -#define BLE_GATT_STATUS_ATTERR_UNSUPPORTED_GROUP_TYPE 0x0110 /**< ATT Error: Attribute type is not a supported grouping attribute. */ -#define BLE_GATT_STATUS_ATTERR_INSUF_RESOURCES 0x0111 /**< ATT Error: Insufficient resources. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_BEGIN 0x0112 /**< ATT Error: Reserved for Future Use range #1 begin. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_END 0x017F /**< ATT Error: Reserved for Future Use range #1 end. */ -#define BLE_GATT_STATUS_ATTERR_APP_BEGIN 0x0180 /**< ATT Error: Application range begin. */ -#define BLE_GATT_STATUS_ATTERR_APP_END 0x019F /**< ATT Error: Application range end. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_BEGIN 0x01A0 /**< ATT Error: Reserved for Future Use range #2 begin. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_END 0x01DF /**< ATT Error: Reserved for Future Use range #2 end. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_BEGIN 0x01E0 /**< ATT Error: Reserved for Future Use range #3 begin. */ -#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_END 0x01FC /**< ATT Error: Reserved for Future Use range #3 end. */ -#define BLE_GATT_STATUS_ATTERR_CPS_WRITE_REQ_REJECTED \ - 0x01FC /**< ATT Common Profile and Service Error: Write request rejected. \ - */ -#define BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR \ - 0x01FD /**< ATT Common Profile and Service Error: Client Characteristic Configuration Descriptor improperly \ - configured. */ -#define BLE_GATT_STATUS_ATTERR_CPS_PROC_ALR_IN_PROG 0x01FE /**< ATT Common Profile and Service Error: Procedure Already in Progress. */ -#define BLE_GATT_STATUS_ATTERR_CPS_OUT_OF_RANGE 0x01FF /**< ATT Common Profile and Service Error: Out Of Range. */ +#define BLE_GATT_STATUS_ATTERR_UNSUPPORTED_GROUP_TYPE \ + 0x0110 /**< ATT Error: Attribute type is not a supported grouping attribute. */ +#define BLE_GATT_STATUS_ATTERR_INSUF_RESOURCES 0x0111 /**< ATT Error: Insufficient resources. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_BEGIN 0x0112 /**< ATT Error: Reserved for Future Use range #1 begin. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE1_END 0x017F /**< ATT Error: Reserved for Future Use range #1 end. */ +#define BLE_GATT_STATUS_ATTERR_APP_BEGIN 0x0180 /**< ATT Error: Application range begin. */ +#define BLE_GATT_STATUS_ATTERR_APP_END 0x019F /**< ATT Error: Application range end. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_BEGIN 0x01A0 /**< ATT Error: Reserved for Future Use range #2 begin. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE2_END 0x01DF /**< ATT Error: Reserved for Future Use range #2 end. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_BEGIN 0x01E0 /**< ATT Error: Reserved for Future Use range #3 begin. */ +#define BLE_GATT_STATUS_ATTERR_RFU_RANGE3_END 0x01FC /**< ATT Error: Reserved for Future Use range #3 end. */ +#define BLE_GATT_STATUS_ATTERR_CPS_WRITE_REQ_REJECTED \ + 0x01FC /**< ATT Common Profile and Service Error: Write request rejected. \ + */ +#define BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR \ + 0x01FD /**< ATT Common Profile and Service Error: Client Characteristic Configuration Descriptor improperly configured. */ +#define BLE_GATT_STATUS_ATTERR_CPS_PROC_ALR_IN_PROG \ + 0x01FE /**< ATT Common Profile and Service Error: Procedure Already in Progress. */ +#define BLE_GATT_STATUS_ATTERR_CPS_OUT_OF_RANGE 0x01FF /**< ATT Common Profile and Service Error: Out Of Range. */ /** @} */ /** @defgroup BLE_GATT_CPF_FORMATS Characteristic Presentation Formats @@ -194,32 +194,32 @@ extern "C" { * @retval ::NRF_ERROR_INVALID_PARAM att_mtu is smaller than @ref BLE_GATT_ATT_MTU_DEFAULT. */ typedef struct { - uint16_t att_mtu; /**< Maximum size of ATT packet the SoftDevice can send or receive. - The default and minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. - @mscs - @mmsc{@ref BLE_GATTC_MTU_EXCHANGE} - @mmsc{@ref BLE_GATTS_MTU_EXCHANGE} - @endmscs - */ + uint16_t att_mtu; /**< Maximum size of ATT packet the SoftDevice can send or receive. + The default and minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. + @mscs + @mmsc{@ref BLE_GATTC_MTU_EXCHANGE} + @mmsc{@ref BLE_GATTS_MTU_EXCHANGE} + @endmscs + */ } ble_gatt_conn_cfg_t; /**@brief GATT Characteristic Properties. */ typedef struct { - /* Standard properties */ - uint8_t broadcast : 1; /**< Broadcasting of the value permitted. */ - uint8_t read : 1; /**< Reading the value permitted. */ - uint8_t write_wo_resp : 1; /**< Writing the value with Write Command permitted. */ - uint8_t write : 1; /**< Writing the value with Write Request permitted. */ - uint8_t notify : 1; /**< Notification of the value permitted. */ - uint8_t indicate : 1; /**< Indications of the value permitted. */ - uint8_t auth_signed_wr : 1; /**< Writing the value with Signed Write Command permitted. */ + /* Standard properties */ + uint8_t broadcast : 1; /**< Broadcasting of the value permitted. */ + uint8_t read : 1; /**< Reading the value permitted. */ + uint8_t write_wo_resp : 1; /**< Writing the value with Write Command permitted. */ + uint8_t write : 1; /**< Writing the value with Write Request permitted. */ + uint8_t notify : 1; /**< Notification of the value permitted. */ + uint8_t indicate : 1; /**< Indications of the value permitted. */ + uint8_t auth_signed_wr : 1; /**< Writing the value with Signed Write Command permitted. */ } ble_gatt_char_props_t; /**@brief GATT Characteristic Extended Properties. */ typedef struct { - /* Extended properties */ - uint8_t reliable_wr : 1; /**< Writing the value with Queued Write operations permitted. */ - uint8_t wr_aux : 1; /**< Writing the Characteristic User Description descriptor permitted. */ + /* Extended properties */ + uint8_t reliable_wr : 1; /**< Writing the value with Queued Write operations permitted. */ + uint8_t wr_aux : 1; /**< Writing the Characteristic User Description descriptor permitted. */ } ble_gatt_char_ext_props_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/ble_gattc.h b/src/platform/nrf52/softdevice/ble_gattc.h index def6f481e..f1df1782c 100644 --- a/src/platform/nrf52/softdevice/ble_gattc.h +++ b/src/platform/nrf52/softdevice/ble_gattc.h @@ -63,56 +63,53 @@ extern "C" { /**@brief GATTC API SVC numbers. */ enum BLE_GATTC_SVCS { - SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER = BLE_GATTC_SVC_BASE, /**< Primary Service Discovery. */ - SD_BLE_GATTC_RELATIONSHIPS_DISCOVER, /**< Relationship Discovery. */ - SD_BLE_GATTC_CHARACTERISTICS_DISCOVER, /**< Characteristic Discovery. */ - SD_BLE_GATTC_DESCRIPTORS_DISCOVER, /**< Characteristic Descriptor Discovery. */ - SD_BLE_GATTC_ATTR_INFO_DISCOVER, /**< Attribute Information Discovery. */ - SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ, /**< Read Characteristic Value by UUID. */ - SD_BLE_GATTC_READ, /**< Generic read. */ - SD_BLE_GATTC_CHAR_VALUES_READ, /**< Read multiple Characteristic Values. */ - SD_BLE_GATTC_WRITE, /**< Generic write. */ - SD_BLE_GATTC_HV_CONFIRM, /**< Handle Value Confirmation. */ - SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. */ + SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER = BLE_GATTC_SVC_BASE, /**< Primary Service Discovery. */ + SD_BLE_GATTC_RELATIONSHIPS_DISCOVER, /**< Relationship Discovery. */ + SD_BLE_GATTC_CHARACTERISTICS_DISCOVER, /**< Characteristic Discovery. */ + SD_BLE_GATTC_DESCRIPTORS_DISCOVER, /**< Characteristic Descriptor Discovery. */ + SD_BLE_GATTC_ATTR_INFO_DISCOVER, /**< Attribute Information Discovery. */ + SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ, /**< Read Characteristic Value by UUID. */ + SD_BLE_GATTC_READ, /**< Generic read. */ + SD_BLE_GATTC_CHAR_VALUES_READ, /**< Read multiple Characteristic Values. */ + SD_BLE_GATTC_WRITE, /**< Generic write. */ + SD_BLE_GATTC_HV_CONFIRM, /**< Handle Value Confirmation. */ + SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. */ }; /** * @brief GATT Client Event IDs. */ enum BLE_GATTC_EVTS { - BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP = BLE_GATTC_EVT_BASE, /**< Primary Service Discovery Response event. \n See - @ref ble_gattc_evt_prim_srvc_disc_rsp_t. */ - BLE_GATTC_EVT_REL_DISC_RSP, /**< Relationship Discovery Response event. \n See @ref - * ble_gattc_evt_rel_disc_rsp_t. - */ - BLE_GATTC_EVT_CHAR_DISC_RSP, /**< Characteristic Discovery Response event. \n See @ref - ble_gattc_evt_char_disc_rsp_t. */ - BLE_GATTC_EVT_DESC_DISC_RSP, /**< Descriptor Discovery Response event. \n See @ref - ble_gattc_evt_desc_disc_rsp_t. */ - BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, /**< Attribute Information Response event. \n See @ref - ble_gattc_evt_attr_info_disc_rsp_t. */ - BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP, /**< Read By UUID Response event. \n See @ref - ble_gattc_evt_char_val_by_uuid_read_rsp_t. */ - BLE_GATTC_EVT_READ_RSP, /**< Read Response event. \n See @ref ble_gattc_evt_read_rsp_t. - */ - BLE_GATTC_EVT_CHAR_VALS_READ_RSP, /**< Read multiple Response event. \n See @ref - ble_gattc_evt_char_vals_read_rsp_t. */ - BLE_GATTC_EVT_WRITE_RSP, /**< Write Response event. \n See @ref - ble_gattc_evt_write_rsp_t. */ - BLE_GATTC_EVT_HVX, /**< Handle Value Notification or Indication event. \n Confirm indication with @ref - sd_ble_gattc_hv_confirm. \n See @ref ble_gattc_evt_hvx_t. */ - BLE_GATTC_EVT_EXCHANGE_MTU_RSP, /**< Exchange MTU Response event. \n See @ref - ble_gattc_evt_exchange_mtu_rsp_t. */ - BLE_GATTC_EVT_TIMEOUT, /**< Timeout event. \n See @ref ble_gattc_evt_timeout_t. */ - BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE /**< Write without Response transmission complete. \n See @ref - ble_gattc_evt_write_cmd_tx_complete_t. */ + BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP = BLE_GATTC_EVT_BASE, /**< Primary Service Discovery Response event. \n See @ref + ble_gattc_evt_prim_srvc_disc_rsp_t. */ + BLE_GATTC_EVT_REL_DISC_RSP, /**< Relationship Discovery Response event. \n See @ref ble_gattc_evt_rel_disc_rsp_t. + */ + BLE_GATTC_EVT_CHAR_DISC_RSP, /**< Characteristic Discovery Response event. \n See @ref + ble_gattc_evt_char_disc_rsp_t. */ + BLE_GATTC_EVT_DESC_DISC_RSP, /**< Descriptor Discovery Response event. \n See @ref + ble_gattc_evt_desc_disc_rsp_t. */ + BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, /**< Attribute Information Response event. \n See @ref + ble_gattc_evt_attr_info_disc_rsp_t. */ + BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP, /**< Read By UUID Response event. \n See @ref + ble_gattc_evt_char_val_by_uuid_read_rsp_t. */ + BLE_GATTC_EVT_READ_RSP, /**< Read Response event. \n See @ref ble_gattc_evt_read_rsp_t. */ + BLE_GATTC_EVT_CHAR_VALS_READ_RSP, /**< Read multiple Response event. \n See @ref + ble_gattc_evt_char_vals_read_rsp_t. */ + BLE_GATTC_EVT_WRITE_RSP, /**< Write Response event. \n See @ref ble_gattc_evt_write_rsp_t. */ + BLE_GATTC_EVT_HVX, /**< Handle Value Notification or Indication event. \n Confirm indication with @ref + sd_ble_gattc_hv_confirm. \n See @ref ble_gattc_evt_hvx_t. */ + BLE_GATTC_EVT_EXCHANGE_MTU_RSP, /**< Exchange MTU Response event. \n See @ref + ble_gattc_evt_exchange_mtu_rsp_t. */ + BLE_GATTC_EVT_TIMEOUT, /**< Timeout event. \n See @ref ble_gattc_evt_timeout_t. */ + BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE /**< Write without Response transmission complete. \n See @ref + ble_gattc_evt_write_cmd_tx_complete_t. */ }; /**@brief GATTC Option IDs. * IDs that uniquely identify a GATTC option. */ enum BLE_GATTC_OPTS { - BLE_GATTC_OPT_UUID_DISC = BLE_GATTC_OPT_BASE, /**< UUID discovery. @ref ble_gattc_opt_uuid_disc_t */ + BLE_GATTC_OPT_UUID_DISC = BLE_GATTC_OPT_BASE, /**< UUID discovery. @ref ble_gattc_opt_uuid_disc_t */ }; /** @} */ @@ -133,7 +130,8 @@ enum BLE_GATTC_OPTS { /** @defgroup BLE_GATTC_DEFAULTS GATT Client defaults * @{ */ -#define BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT 1 /**< Default number of Write without Response that can be queued for transmission. */ +#define BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT \ + 1 /**< Default number of Write without Response that can be queued for transmission. */ /** @} */ /** @} */ @@ -145,206 +143,208 @@ enum BLE_GATTC_OPTS { * @brief BLE GATTC connection configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t write_cmd_tx_queue_size; /**< The guaranteed minimum number of Write without Response that can be queued for - transmission. The default value is @ref BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT */ + uint8_t write_cmd_tx_queue_size; /**< The guaranteed minimum number of Write without Response that can be queued for + transmission. The default value is @ref BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT */ } ble_gattc_conn_cfg_t; /**@brief Operation Handle Range. */ typedef struct { - uint16_t start_handle; /**< Start Handle. */ - uint16_t end_handle; /**< End Handle. */ + uint16_t start_handle; /**< Start Handle. */ + uint16_t end_handle; /**< End Handle. */ } ble_gattc_handle_range_t; /**@brief GATT service. */ typedef struct { - ble_uuid_t uuid; /**< Service UUID. */ - ble_gattc_handle_range_t handle_range; /**< Service Handle Range. */ + ble_uuid_t uuid; /**< Service UUID. */ + ble_gattc_handle_range_t handle_range; /**< Service Handle Range. */ } ble_gattc_service_t; /**@brief GATT include. */ typedef struct { - uint16_t handle; /**< Include Handle. */ - ble_gattc_service_t included_srvc; /**< Handle of the included service. */ + uint16_t handle; /**< Include Handle. */ + ble_gattc_service_t included_srvc; /**< Handle of the included service. */ } ble_gattc_include_t; /**@brief GATT characteristic. */ typedef struct { - ble_uuid_t uuid; /**< Characteristic UUID. */ - ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ - uint8_t char_ext_props : 1; /**< Extended properties present. */ - uint16_t handle_decl; /**< Handle of the Characteristic Declaration. */ - uint16_t handle_value; /**< Handle of the Characteristic Value. */ + ble_uuid_t uuid; /**< Characteristic UUID. */ + ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ + uint8_t char_ext_props : 1; /**< Extended properties present. */ + uint16_t handle_decl; /**< Handle of the Characteristic Declaration. */ + uint16_t handle_value; /**< Handle of the Characteristic Value. */ } ble_gattc_char_t; /**@brief GATT descriptor. */ typedef struct { - uint16_t handle; /**< Descriptor Handle. */ - ble_uuid_t uuid; /**< Descriptor UUID. */ + uint16_t handle; /**< Descriptor Handle. */ + ble_uuid_t uuid; /**< Descriptor UUID. */ } ble_gattc_desc_t; /**@brief Write Parameters. */ typedef struct { - uint8_t write_op; /**< Write Operation to be performed, see @ref BLE_GATT_WRITE_OPS. */ - uint8_t flags; /**< Flags, see @ref BLE_GATT_EXEC_WRITE_FLAGS. */ - uint16_t handle; /**< Handle to the attribute to be written. */ - uint16_t offset; /**< Offset in bytes. @note For WRITE_CMD and WRITE_REQ, offset must be 0. */ - uint16_t len; /**< Length of data in bytes. */ - uint8_t const *p_value; /**< Pointer to the value data. */ + uint8_t write_op; /**< Write Operation to be performed, see @ref BLE_GATT_WRITE_OPS. */ + uint8_t flags; /**< Flags, see @ref BLE_GATT_EXEC_WRITE_FLAGS. */ + uint16_t handle; /**< Handle to the attribute to be written. */ + uint16_t offset; /**< Offset in bytes. @note For WRITE_CMD and WRITE_REQ, offset must be 0. */ + uint16_t len; /**< Length of data in bytes. */ + uint8_t const *p_value; /**< Pointer to the value data. */ } ble_gattc_write_params_t; /**@brief Attribute Information for 16-bit Attribute UUID. */ typedef struct { - uint16_t handle; /**< Attribute handle. */ - ble_uuid_t uuid; /**< 16-bit Attribute UUID. */ + uint16_t handle; /**< Attribute handle. */ + ble_uuid_t uuid; /**< 16-bit Attribute UUID. */ } ble_gattc_attr_info16_t; /**@brief Attribute Information for 128-bit Attribute UUID. */ typedef struct { - uint16_t handle; /**< Attribute handle. */ - ble_uuid128_t uuid; /**< 128-bit Attribute UUID. */ + uint16_t handle; /**< Attribute handle. */ + ble_uuid128_t uuid; /**< 128-bit Attribute UUID. */ } ble_gattc_attr_info128_t; /**@brief Event structure for @ref BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP. */ typedef struct { - uint16_t count; /**< Service count. */ - ble_gattc_service_t services[1]; /**< Service data. @note This is a variable length array. The size of 1 indicated is - only a placeholder for compilation. See @ref sd_ble_evt_get for more information - on how to use event structures with variable length array members. */ + uint16_t count; /**< Service count. */ + ble_gattc_service_t services[1]; /**< Service data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use + event structures with variable length array members. */ } ble_gattc_evt_prim_srvc_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_REL_DISC_RSP. */ typedef struct { - uint16_t count; /**< Include count. */ - ble_gattc_include_t includes[1]; /**< Include data. @note This is a variable length array. The size of 1 indicated is - only a placeholder for compilation. See @ref sd_ble_evt_get for more information - on how to use event structures with variable length array members. */ + uint16_t count; /**< Include count. */ + ble_gattc_include_t includes[1]; /**< Include data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use + event structures with variable length array members. */ } ble_gattc_evt_rel_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_CHAR_DISC_RSP. */ typedef struct { - uint16_t count; /**< Characteristic count. */ - ble_gattc_char_t chars[1]; /**< Characteristic data. @note This is a variable length array. The size of 1 indicated is - only a placeholder for compilation. See @ref sd_ble_evt_get for more information on how - to use event structures with variable length array members. */ + uint16_t count; /**< Characteristic count. */ + ble_gattc_char_t chars[1]; /**< Characteristic data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event + structures with variable length array members. */ } ble_gattc_evt_char_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_DESC_DISC_RSP. */ typedef struct { - uint16_t count; /**< Descriptor count. */ - ble_gattc_desc_t descs[1]; /**< Descriptor data. @note This is a variable length array. The size of 1 indicated is - only a placeholder for compilation. See @ref sd_ble_evt_get for more information on how - to use event structures with variable length array members. */ + uint16_t count; /**< Descriptor count. */ + ble_gattc_desc_t descs[1]; /**< Descriptor data. @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event + structures with variable length array members. */ } ble_gattc_evt_desc_disc_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_ATTR_INFO_DISC_RSP. */ typedef struct { - uint16_t count; /**< Attribute count. */ - uint8_t format; /**< Attribute information format, see @ref BLE_GATTC_ATTR_INFO_FORMAT. */ - union { - ble_gattc_attr_info16_t attr_info16[1]; /**< Attribute information for 16-bit Attribute UUID. - @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on - how to use event structures with variable length array members. */ - ble_gattc_attr_info128_t attr_info128[1]; /**< Attribute information for 128-bit Attribute UUID. - @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on - how to use event structures with variable length array members. */ - } info; /**< Attribute information union. */ + uint16_t count; /**< Attribute count. */ + uint8_t format; /**< Attribute information format, see @ref BLE_GATTC_ATTR_INFO_FORMAT. */ + union { + ble_gattc_attr_info16_t attr_info16[1]; /**< Attribute information for 16-bit Attribute UUID. + @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on + how to use event structures with variable length array members. */ + ble_gattc_attr_info128_t attr_info128[1]; /**< Attribute information for 128-bit Attribute UUID. + @note This is a variable length array. The size of 1 indicated is only a + placeholder for compilation. See @ref sd_ble_evt_get for more information on + how to use event structures with variable length array members. */ + } info; /**< Attribute information union. */ } ble_gattc_evt_attr_info_disc_rsp_t; /**@brief GATT read by UUID handle value pair. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - uint8_t *p_value; /**< Pointer to the Attribute Value, length is available in @ref - ble_gattc_evt_char_val_by_uuid_read_rsp_t::value_len. */ + uint16_t handle; /**< Attribute Handle. */ + uint8_t *p_value; /**< Pointer to the Attribute Value, length is available in @ref + ble_gattc_evt_char_val_by_uuid_read_rsp_t::value_len. */ } ble_gattc_handle_value_t; /**@brief Event structure for @ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP. */ typedef struct { - uint16_t count; /**< Handle-Value Pair Count. */ - uint16_t value_len; /**< Length of the value in Handle-Value(s) list. */ - uint8_t handle_value[1]; /**< Handle-Value(s) list. To iterate through the list use @ref - sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter. - @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures - with variable length array members. */ + uint16_t count; /**< Handle-Value Pair Count. */ + uint16_t value_len; /**< Length of the value in Handle-Value(s) list. */ + uint8_t handle_value[1]; /**< Handle-Value(s) list. To iterate through the list use @ref + sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter. + @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures with + variable length array members. */ } ble_gattc_evt_char_val_by_uuid_read_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_READ_RSP. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - uint16_t offset; /**< Offset of the attribute data. */ - uint16_t len; /**< Attribute data length. */ - uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event - structures with variable length array members. */ + uint16_t handle; /**< Attribute Handle. */ + uint16_t offset; /**< Offset of the attribute data. */ + uint16_t len; /**< Attribute data length. */ + uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable + length array members. */ } ble_gattc_evt_read_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_CHAR_VALS_READ_RSP. */ typedef struct { - uint16_t len; /**< Concatenated Attribute values length. */ - uint8_t values[1]; /**< Attribute values. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event - structures with variable length array members. */ + uint16_t len; /**< Concatenated Attribute values length. */ + uint8_t values[1]; /**< Attribute values. @note This is a variable length array. The size of 1 indicated is only a placeholder + for compilation. See @ref sd_ble_evt_get for more information on how to use event structures with + variable length array members. */ } ble_gattc_evt_char_vals_read_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_WRITE_RSP. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - uint8_t write_op; /**< Type of write operation, see @ref BLE_GATT_WRITE_OPS. */ - uint16_t offset; /**< Data offset. */ - uint16_t len; /**< Data length. */ - uint8_t data[1]; /**< Data. @note This is a variable length array. The size of 1 indicated is only a placeholder for - compilation. See @ref sd_ble_evt_get for more information on how to use event structures with - variable length array members. */ + uint16_t handle; /**< Attribute Handle. */ + uint8_t write_op; /**< Type of write operation, see @ref BLE_GATT_WRITE_OPS. */ + uint16_t offset; /**< Data offset. */ + uint16_t len; /**< Data length. */ + uint8_t data[1]; /**< Data. @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable + length array members. */ } ble_gattc_evt_write_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_HVX. */ typedef struct { - uint16_t handle; /**< Handle to which the HVx operation applies. */ - uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ - uint16_t len; /**< Attribute data length. */ - uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use event - structures with variable length array members. */ + uint16_t handle; /**< Handle to which the HVx operation applies. */ + uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ + uint16_t len; /**< Attribute data length. */ + uint8_t data[1]; /**< Attribute data. @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable + length array members. */ } ble_gattc_evt_hvx_t; /**@brief Event structure for @ref BLE_GATTC_EVT_EXCHANGE_MTU_RSP. */ typedef struct { - uint16_t server_rx_mtu; /**< Server RX MTU size. */ + uint16_t server_rx_mtu; /**< Server RX MTU size. */ } ble_gattc_evt_exchange_mtu_rsp_t; /**@brief Event structure for @ref BLE_GATTC_EVT_TIMEOUT. */ typedef struct { - uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ + uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ } ble_gattc_evt_timeout_t; /**@brief Event structure for @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE. */ typedef struct { - uint8_t count; /**< Number of write without response transmissions completed. */ + uint8_t count; /**< Number of write without response transmissions completed. */ } ble_gattc_evt_write_cmd_tx_complete_t; /**@brief GATTC event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which event occurred. */ - uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ - uint16_t error_handle; /**< In case of error: The handle causing the error. In all other cases @ref - BLE_GATT_HANDLE_INVALID. */ - union { - ble_gattc_evt_prim_srvc_disc_rsp_t prim_srvc_disc_rsp; /**< Primary Service Discovery Response Event Parameters. */ - ble_gattc_evt_rel_disc_rsp_t rel_disc_rsp; /**< Relationship Discovery Response Event Parameters. */ - ble_gattc_evt_char_disc_rsp_t char_disc_rsp; /**< Characteristic Discovery Response Event Parameters. */ - ble_gattc_evt_desc_disc_rsp_t desc_disc_rsp; /**< Descriptor Discovery Response Event Parameters. */ - ble_gattc_evt_char_val_by_uuid_read_rsp_t char_val_by_uuid_read_rsp; /**< Characteristic Value Read by UUID Response Event Parameters. */ - ble_gattc_evt_read_rsp_t read_rsp; /**< Read Response Event Parameters. */ - ble_gattc_evt_char_vals_read_rsp_t char_vals_read_rsp; /**< Characteristic Values Read Response Event Parameters. */ - ble_gattc_evt_write_rsp_t write_rsp; /**< Write Response Event Parameters. */ - ble_gattc_evt_hvx_t hvx; /**< Handle Value Notification/Indication Event Parameters. */ - ble_gattc_evt_exchange_mtu_rsp_t exchange_mtu_rsp; /**< Exchange MTU Response Event Parameters. */ - ble_gattc_evt_timeout_t timeout; /**< Timeout Event Parameters. */ - ble_gattc_evt_attr_info_disc_rsp_t attr_info_disc_rsp; /**< Attribute Information Discovery Event Parameters. */ - ble_gattc_evt_write_cmd_tx_complete_t write_cmd_tx_complete; /**< Write without Response transmission complete Event Parameters. */ - } params; /**< Event Parameters. @note Only valid if @ref gatt_status == @ref BLE_GATT_STATUS_SUCCESS. */ + uint16_t conn_handle; /**< Connection Handle on which event occurred. */ + uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ + uint16_t + error_handle; /**< In case of error: The handle causing the error. In all other cases @ref BLE_GATT_HANDLE_INVALID. */ + union { + ble_gattc_evt_prim_srvc_disc_rsp_t prim_srvc_disc_rsp; /**< Primary Service Discovery Response Event Parameters. */ + ble_gattc_evt_rel_disc_rsp_t rel_disc_rsp; /**< Relationship Discovery Response Event Parameters. */ + ble_gattc_evt_char_disc_rsp_t char_disc_rsp; /**< Characteristic Discovery Response Event Parameters. */ + ble_gattc_evt_desc_disc_rsp_t desc_disc_rsp; /**< Descriptor Discovery Response Event Parameters. */ + ble_gattc_evt_char_val_by_uuid_read_rsp_t + char_val_by_uuid_read_rsp; /**< Characteristic Value Read by UUID Response Event Parameters. */ + ble_gattc_evt_read_rsp_t read_rsp; /**< Read Response Event Parameters. */ + ble_gattc_evt_char_vals_read_rsp_t char_vals_read_rsp; /**< Characteristic Values Read Response Event Parameters. */ + ble_gattc_evt_write_rsp_t write_rsp; /**< Write Response Event Parameters. */ + ble_gattc_evt_hvx_t hvx; /**< Handle Value Notification/Indication Event Parameters. */ + ble_gattc_evt_exchange_mtu_rsp_t exchange_mtu_rsp; /**< Exchange MTU Response Event Parameters. */ + ble_gattc_evt_timeout_t timeout; /**< Timeout Event Parameters. */ + ble_gattc_evt_attr_info_disc_rsp_t attr_info_disc_rsp; /**< Attribute Information Discovery Event Parameters. */ + ble_gattc_evt_write_cmd_tx_complete_t + write_cmd_tx_complete; /**< Write without Response transmission complete Event Parameters. */ + } params; /**< Event Parameters. @note Only valid if @ref gatt_status == @ref BLE_GATT_STATUS_SUCCESS. */ } ble_gattc_evt_t; /**@brief UUID discovery option. @@ -370,13 +370,12 @@ typedef struct { * */ typedef struct { - uint8_t auto_add_vs_enable : 1; /**< Set to 1 to enable (or 0 to disable) automatic insertion of discovered 128-bit - UUIDs. */ + uint8_t auto_add_vs_enable : 1; /**< Set to 1 to enable (or 0 to disable) automatic insertion of discovered 128-bit UUIDs. */ } ble_gattc_opt_uuid_disc_t; /**@brief Option structure for GATTC options. */ typedef union { - ble_gattc_opt_uuid_disc_t uuid_disc; /**< Parameters for the UUID discovery option. */ + ble_gattc_opt_uuid_disc_t uuid_disc; /**< Parameters for the UUID discovery option. */ } ble_gattc_opt_t; /** @} */ @@ -387,8 +386,8 @@ typedef union { /**@brief Initiate or continue a GATT Primary Service Discovery procedure. * * @details This function initiates or resumes a Primary Service discovery procedure, starting from the supplied handle. - * If the last service has not been reached, this function must be called again with an updated start handle - * value to continue the search. See also @ref ble_gattc_opt_uuid_disc_t. + * If the last service has not been reached, this function must be called again with an updated start handle value to + * continue the search. See also @ref ble_gattc_opt_uuid_disc_t. * * @events * @event{@ref BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP} @@ -415,8 +414,8 @@ SVCALL(SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Relationship Discovery procedure. * - * @details This function initiates or resumes the Find Included Services sub-procedure. If the last included service - * has not been reached, this must be called again with an updated handle range to continue the search. See also @ref + * @details This function initiates or resumes the Find Included Services sub-procedure. If the last included service has not been + * reached, this must be called again with an updated handle range to continue the search. See also @ref * ble_gattc_opt_uuid_disc_t. * * @events @@ -444,8 +443,8 @@ SVCALL(SD_BLE_GATTC_RELATIONSHIPS_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Characteristic Discovery procedure. * - * @details This function initiates or resumes a Characteristic discovery procedure. If the last Characteristic has not - * been reached, this must be called again with an updated handle range to continue the discovery. See also @ref + * @details This function initiates or resumes a Characteristic discovery procedure. If the last Characteristic has not been + * reached, this must be called again with an updated handle range to continue the discovery. See also @ref * ble_gattc_opt_uuid_disc_t. * * @events @@ -472,8 +471,8 @@ SVCALL(SD_BLE_GATTC_CHARACTERISTICS_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Characteristic Descriptor Discovery procedure. * - * @details This function initiates or resumes a Characteristic Descriptor discovery procedure. If the last Descriptor - * has not been reached, this must be called again with an updated handle range to continue the discovery. See also @ref + * @details This function initiates or resumes a Characteristic Descriptor discovery procedure. If the last Descriptor has not + * been reached, this must be called again with an updated handle range to continue the discovery. See also @ref * ble_gattc_opt_uuid_disc_t. * * @events @@ -500,8 +499,8 @@ SVCALL(SD_BLE_GATTC_DESCRIPTORS_DISCOVER, uint32_t, /**@brief Initiate or continue a GATT Read using Characteristic UUID procedure. * - * @details This function initiates or resumes a Read using Characteristic UUID procedure. If the last Characteristic - * has not been reached, this must be called again with an updated handle range to continue the discovery. + * @details This function initiates or resumes a Read using Characteristic UUID procedure. If the last Characteristic has not been + * reached, this must be called again with an updated handle range to continue the discovery. * * @events * @event{@ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP} @@ -524,13 +523,14 @@ SVCALL(SD_BLE_GATTC_DESCRIPTORS_DISCOVER, uint32_t, * reestablishing the connection. */ SVCALL(SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ, uint32_t, - sd_ble_gattc_char_value_by_uuid_read(uint16_t conn_handle, ble_uuid_t const *p_uuid, ble_gattc_handle_range_t const *p_handle_range)); + sd_ble_gattc_char_value_by_uuid_read(uint16_t conn_handle, ble_uuid_t const *p_uuid, + ble_gattc_handle_range_t const *p_handle_range)); /**@brief Initiate or continue a GATT Read (Long) Characteristic or Descriptor procedure. * - * @details This function initiates or resumes a GATT Read (Long) Characteristic or Descriptor procedure. If the - * Characteristic or Descriptor to be read is longer than ATT_MTU - 1, this function must be called multiple times with - * appropriate offset to read the complete value. + * @details This function initiates or resumes a GATT Read (Long) Characteristic or Descriptor procedure. If the Characteristic or + * Descriptor to be read is longer than ATT_MTU - 1, this function must be called multiple times with appropriate offset to read + * the complete value. * * @events * @event{@ref BLE_GATTC_EVT_READ_RSP} @@ -580,8 +580,8 @@ SVCALL(SD_BLE_GATTC_READ, uint32_t, sd_ble_gattc_read(uint16_t conn_handle, uint SVCALL(SD_BLE_GATTC_CHAR_VALUES_READ, uint32_t, sd_ble_gattc_char_values_read(uint16_t conn_handle, uint16_t const *p_handles, uint16_t handle_count)); -/**@brief Perform a Write (Characteristic Value or Descriptor, with or without response, signed or not, long or - * reliable) procedure. +/**@brief Perform a Write (Characteristic Value or Descriptor, with or without response, signed or not, long or reliable) + * procedure. * * @details This function can perform all write procedures described in GATT. * @@ -591,17 +591,17 @@ SVCALL(SD_BLE_GATTC_CHAR_VALUES_READ, uint32_t, * A @ref BLE_GATTC_EVT_WRITE_RSP event will be issued as soon as the write response arrives from the peer. * * @note The number of Write without Response that can be queued is configured by @ref - * ble_gattc_conn_cfg_t::write_cmd_tx_queue_size When the queue is full, the function call will return @ref - * NRF_ERROR_RESOURCES. A @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event will be issued as soon as the transmission of - * the write without response is complete. + * ble_gattc_conn_cfg_t::write_cmd_tx_queue_size When the queue is full, the function call will return @ref NRF_ERROR_RESOURCES. + * A @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event will be issued as soon as the transmission of the write without + * response is complete. * - * @note The application can keep track of the available queue element count for writes without responses by - * following the procedure below: + * @note The application can keep track of the available queue element count for writes without responses by following the + * procedure below: * - Store initial queue element count in a variable. - * - Decrement the variable, which stores the currently available queue element count, by one when a call to - * this function returns @ref NRF_SUCCESS. - * - Increment the variable, which stores the current available queue element count, by the count variable in - * @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event. + * - Decrement the variable, which stores the currently available queue element count, by one when a call to this + * function returns @ref NRF_SUCCESS. + * - Increment the variable, which stores the current available queue element count, by the count variable in @ref + * BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event. * * @events * @event{@ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE, Write without response transmission complete.} @@ -624,8 +624,8 @@ SVCALL(SD_BLE_GATTC_CHAR_VALUES_READ, uint32_t, * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied. - * @retval ::NRF_ERROR_BUSY For write with response, procedure already in progress. Wait for a @ref - * BLE_GATTC_EVT_WRITE_RSP event and retry. + * @retval ::NRF_ERROR_BUSY For write with response, procedure already in progress. Wait for a @ref BLE_GATTC_EVT_WRITE_RSP event + * and retry. * @retval ::NRF_ERROR_RESOURCES Too many writes without responses queued. * Wait for a @ref BLE_GATTC_EVT_WRITE_CMD_TX_COMPLETE event and retry. * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without @@ -654,8 +654,7 @@ SVCALL(SD_BLE_GATTC_HV_CONFIRM, uint32_t, sd_ble_gattc_hv_confirm(uint16_t conn_ /**@brief Discovers information about a range of attributes on a GATT server. * * @events - * @event{@ref BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, Generated when information about a range of attributes has been - * received.} + * @event{@ref BLE_GATTC_EVT_ATTR_INFO_DISC_RSP, Generated when information about a range of attributes has been received.} * @endevents * * @param[in] conn_handle The connection handle identifying the connection to perform this procedure on. @@ -693,8 +692,7 @@ SVCALL(SD_BLE_GATTC_ATTR_INFO_DISCOVER, uint32_t, * - The minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. * - The maximum value is @ref ble_gatt_conn_cfg_t::att_mtu in the connection configuration used for this connection. - * - The value must be equal to Server RX MTU size given in @ref - sd_ble_gatts_exchange_mtu_reply + * - The value must be equal to Server RX MTU size given in @ref sd_ble_gatts_exchange_mtu_reply * if an ATT_MTU exchange has already been performed in the other direction. * * @retval ::NRF_SUCCESS Successfully sent request to the server. @@ -705,7 +703,8 @@ SVCALL(SD_BLE_GATTC_ATTR_INFO_DISCOVER, uint32_t, * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without reestablishing the connection. */ -SVCALL(SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, uint32_t, sd_ble_gattc_exchange_mtu_request(uint16_t conn_handle, uint16_t client_rx_mtu)); +SVCALL(SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, uint32_t, + sd_ble_gattc_exchange_mtu_request(uint16_t conn_handle, uint16_t client_rx_mtu)); /**@brief Iterate through Handle-Value(s) list in @ref BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP event. * @@ -730,24 +729,27 @@ SVCALL(SD_BLE_GATTC_EXCHANGE_MTU_REQUEST, uint32_t, sd_ble_gattc_exchange_mtu_re * @retval ::NRF_SUCCESS Successfully retrieved the next Handle-Value pair. * @retval ::NRF_ERROR_NOT_FOUND No more Handle-Value pairs available in the list. */ -__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, ble_gattc_handle_value_t *p_iter); +__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, + ble_gattc_handle_value_t *p_iter); /** @} */ #ifndef SUPPRESS_INLINE_IMPLEMENTATION -__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, ble_gattc_handle_value_t *p_iter) { - uint32_t value_len = p_gattc_evt->params.char_val_by_uuid_read_rsp.value_len; - uint8_t *p_first = p_gattc_evt->params.char_val_by_uuid_read_rsp.handle_value; - uint8_t *p_next = p_iter->p_value ? p_iter->p_value + value_len : p_first; +__STATIC_INLINE uint32_t sd_ble_gattc_evt_char_val_by_uuid_read_rsp_iter(ble_gattc_evt_t *p_gattc_evt, + ble_gattc_handle_value_t *p_iter) +{ + uint32_t value_len = p_gattc_evt->params.char_val_by_uuid_read_rsp.value_len; + uint8_t *p_first = p_gattc_evt->params.char_val_by_uuid_read_rsp.handle_value; + uint8_t *p_next = p_iter->p_value ? p_iter->p_value + value_len : p_first; - if ((p_next - p_first) / (sizeof(uint16_t) + value_len) < p_gattc_evt->params.char_val_by_uuid_read_rsp.count) { - p_iter->handle = (uint16_t)p_next[1] << 8 | p_next[0]; - p_iter->p_value = p_next + sizeof(uint16_t); - return NRF_SUCCESS; - } else { - return NRF_ERROR_NOT_FOUND; - } + if ((p_next - p_first) / (sizeof(uint16_t) + value_len) < p_gattc_evt->params.char_val_by_uuid_read_rsp.count) { + p_iter->handle = (uint16_t)p_next[1] << 8 | p_next[0]; + p_iter->p_value = p_next + sizeof(uint16_t); + return NRF_SUCCESS; + } else { + return NRF_ERROR_NOT_FOUND; + } } #endif /* SUPPRESS_INLINE_IMPLEMENTATION */ diff --git a/src/platform/nrf52/softdevice/ble_gatts.h b/src/platform/nrf52/softdevice/ble_gatts.h index e46b654cd..dc94957cd 100644 --- a/src/platform/nrf52/softdevice/ble_gatts.h +++ b/src/platform/nrf52/softdevice/ble_gatts.h @@ -66,51 +66,45 @@ extern "C" { * @brief GATTS API SVC numbers. */ enum BLE_GATTS_SVCS { - SD_BLE_GATTS_SERVICE_ADD = BLE_GATTS_SVC_BASE, /**< Add a service. */ - SD_BLE_GATTS_INCLUDE_ADD, /**< Add an included service. */ - SD_BLE_GATTS_CHARACTERISTIC_ADD, /**< Add a characteristic. */ - SD_BLE_GATTS_DESCRIPTOR_ADD, /**< Add a generic attribute. */ - SD_BLE_GATTS_VALUE_SET, /**< Set an attribute value. */ - SD_BLE_GATTS_VALUE_GET, /**< Get an attribute value. */ - SD_BLE_GATTS_HVX, /**< Handle Value Notification or Indication. */ - SD_BLE_GATTS_SERVICE_CHANGED, /**< Perform a Service Changed Indication to one or more peers. */ - SD_BLE_GATTS_RW_AUTHORIZE_REPLY, /**< Reply to an authorization request for a read or write operation on one or more - attributes. */ - SD_BLE_GATTS_SYS_ATTR_SET, /**< Set the persistent system attributes for a connection. */ - SD_BLE_GATTS_SYS_ATTR_GET, /**< Retrieve the persistent system attributes. */ - SD_BLE_GATTS_INITIAL_USER_HANDLE_GET, /**< Retrieve the first valid user handle. */ - SD_BLE_GATTS_ATTR_GET, /**< Retrieve the UUID and/or metadata of an attribute. */ - SD_BLE_GATTS_EXCHANGE_MTU_REPLY /**< Reply to Exchange MTU Request. */ + SD_BLE_GATTS_SERVICE_ADD = BLE_GATTS_SVC_BASE, /**< Add a service. */ + SD_BLE_GATTS_INCLUDE_ADD, /**< Add an included service. */ + SD_BLE_GATTS_CHARACTERISTIC_ADD, /**< Add a characteristic. */ + SD_BLE_GATTS_DESCRIPTOR_ADD, /**< Add a generic attribute. */ + SD_BLE_GATTS_VALUE_SET, /**< Set an attribute value. */ + SD_BLE_GATTS_VALUE_GET, /**< Get an attribute value. */ + SD_BLE_GATTS_HVX, /**< Handle Value Notification or Indication. */ + SD_BLE_GATTS_SERVICE_CHANGED, /**< Perform a Service Changed Indication to one or more peers. */ + SD_BLE_GATTS_RW_AUTHORIZE_REPLY, /**< Reply to an authorization request for a read or write operation on one or more + attributes. */ + SD_BLE_GATTS_SYS_ATTR_SET, /**< Set the persistent system attributes for a connection. */ + SD_BLE_GATTS_SYS_ATTR_GET, /**< Retrieve the persistent system attributes. */ + SD_BLE_GATTS_INITIAL_USER_HANDLE_GET, /**< Retrieve the first valid user handle. */ + SD_BLE_GATTS_ATTR_GET, /**< Retrieve the UUID and/or metadata of an attribute. */ + SD_BLE_GATTS_EXCHANGE_MTU_REPLY /**< Reply to Exchange MTU Request. */ }; /** * @brief GATT Server Event IDs. */ enum BLE_GATTS_EVTS { - BLE_GATTS_EVT_WRITE = BLE_GATTS_EVT_BASE, /**< Write operation performed. \n See - @ref ble_gatts_evt_write_t. */ - BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST, /**< Read/Write Authorization request. \n Reply - with - @ref sd_ble_gatts_rw_authorize_reply. \n See @ref - ble_gatts_evt_rw_authorize_request_t. - */ - BLE_GATTS_EVT_SYS_ATTR_MISSING, /**< A persistent system attribute access is pending. \n Respond - with @ref sd_ble_gatts_sys_attr_set. \n See @ref - ble_gatts_evt_sys_attr_missing_t. */ - BLE_GATTS_EVT_HVC, /**< Handle Value Confirmation. \n See @ref - * ble_gatts_evt_hvc_t. - */ - BLE_GATTS_EVT_SC_CONFIRM, /**< Service Changed Confirmation. \n No additional - event structure applies. */ - BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. \n Reply - with - @ref sd_ble_gatts_exchange_mtu_reply. \n See @ref - ble_gatts_evt_exchange_mtu_request_t. - */ - BLE_GATTS_EVT_TIMEOUT, /**< Peer failed to respond to an ATT request in time. \n See @ref - ble_gatts_evt_timeout_t. */ - BLE_GATTS_EVT_HVN_TX_COMPLETE /**< Handle Value Notification transmission complete. \n See @ref - ble_gatts_evt_hvn_tx_complete_t. */ + BLE_GATTS_EVT_WRITE = BLE_GATTS_EVT_BASE, /**< Write operation performed. \n See + @ref ble_gatts_evt_write_t. */ + BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST, /**< Read/Write Authorization request. \n Reply with + @ref sd_ble_gatts_rw_authorize_reply. \n See @ref ble_gatts_evt_rw_authorize_request_t. + */ + BLE_GATTS_EVT_SYS_ATTR_MISSING, /**< A persistent system attribute access is pending. \n Respond with @ref + sd_ble_gatts_sys_attr_set. \n See @ref ble_gatts_evt_sys_attr_missing_t. */ + BLE_GATTS_EVT_HVC, /**< Handle Value Confirmation. \n See @ref ble_gatts_evt_hvc_t. + */ + BLE_GATTS_EVT_SC_CONFIRM, /**< Service Changed Confirmation. \n No additional event + structure applies. */ + BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST, /**< Exchange MTU Request. \n Reply with + @ref sd_ble_gatts_exchange_mtu_reply. \n See @ref ble_gatts_evt_exchange_mtu_request_t. + */ + BLE_GATTS_EVT_TIMEOUT, /**< Peer failed to respond to an ATT request in time. \n See @ref + ble_gatts_evt_timeout_t. */ + BLE_GATTS_EVT_HVN_TX_COMPLETE /**< Handle Value Notification transmission complete. \n See @ref + ble_gatts_evt_hvn_tx_complete_t. */ }; /**@brief GATTS Configuration IDs. @@ -118,9 +112,9 @@ enum BLE_GATTS_EVTS { * IDs that uniquely identify a GATTS configuration. */ enum BLE_GATTS_CFGS { - BLE_GATTS_CFG_SERVICE_CHANGED = BLE_GATTS_CFG_BASE, /**< Service changed configuration. */ - BLE_GATTS_CFG_ATTR_TAB_SIZE, /**< Attribute table size configuration. */ - BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM, /**< Service changed CCCD permission configuration. */ + BLE_GATTS_CFG_SERVICE_CHANGED = BLE_GATTS_CFG_BASE, /**< Service changed configuration. */ + BLE_GATTS_CFG_ATTR_TAB_SIZE, /**< Attribute table size configuration. */ + BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM, /**< Service changed CCCD permission configuration. */ }; /** @} */ @@ -174,10 +168,10 @@ enum BLE_GATTS_CFGS { * @{ */ #define BLE_GATTS_VLOC_INVALID 0x00 /**< Invalid Location. */ #define BLE_GATTS_VLOC_STACK 0x01 /**< Attribute Value is located in stack memory, no user memory is required. */ -#define BLE_GATTS_VLOC_USER \ - 0x02 /**< Attribute Value is located in user memory. This requires the user to maintain a valid buffer through the \ - lifetime of the attribute, since the stack will read and write directly to the memory using the pointer \ - provided in the APIs. There are no alignment requirements for the buffer. */ +#define BLE_GATTS_VLOC_USER \ + 0x02 /**< Attribute Value is located in user memory. This requires the user to maintain a valid buffer through the lifetime \ + of the attribute, since the stack will read and write directly to the memory using the pointer provided in the APIs. \ + There are no alignment requirements for the buffer. */ /** @} */ /** @defgroup BLE_GATTS_AUTHORIZE_TYPES GATT Server Authorization Types @@ -196,7 +190,8 @@ enum BLE_GATTS_CFGS { /** @defgroup BLE_GATTS_SERVICE_CHANGED Service Changed Inclusion Values * @{ */ -#define BLE_GATTS_SERVICE_CHANGED_DEFAULT (1) /**< Default is to include the Service Changed characteristic in the Attribute Table. */ +#define BLE_GATTS_SERVICE_CHANGED_DEFAULT \ + (1) /**< Default is to include the Service Changed characteristic in the Attribute Table. */ /** @} */ /** @defgroup BLE_GATTS_ATTR_TAB_SIZE Attribute Table size @@ -209,7 +204,8 @@ enum BLE_GATTS_CFGS { /** @defgroup BLE_GATTS_DEFAULTS GATT Server defaults * @{ */ -#define BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT 1 /**< Default number of Handle Value Notifications that can be queued for transmission. */ +#define BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT \ + 1 /**< Default number of Handle Value Notifications that can be queued for transmission. */ /** @} */ /** @} */ @@ -221,115 +217,115 @@ enum BLE_GATTS_CFGS { * @brief BLE GATTS connection configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t hvn_tx_queue_size; /**< Minimum guaranteed number of Handle Value Notifications that can be queued for - transmission. The default value is @ref BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT */ + uint8_t hvn_tx_queue_size; /**< Minimum guaranteed number of Handle Value Notifications that can be queued for transmission. + The default value is @ref BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT */ } ble_gatts_conn_cfg_t; /**@brief Attribute metadata. */ typedef struct { - ble_gap_conn_sec_mode_t read_perm; /**< Read permissions. */ - ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ - uint8_t vlen : 1; /**< Variable length attribute. */ - uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ - uint8_t rd_auth : 1; /**< Read authorization and value will be requested from the application on every read operation. */ - uint8_t wr_auth : 1; /**< Write authorization will be requested from the application on every Write Request operation - (but not Write Command). */ + ble_gap_conn_sec_mode_t read_perm; /**< Read permissions. */ + ble_gap_conn_sec_mode_t write_perm; /**< Write permissions. */ + uint8_t vlen : 1; /**< Variable length attribute. */ + uint8_t vloc : 2; /**< Value location, see @ref BLE_GATTS_VLOCS.*/ + uint8_t rd_auth : 1; /**< Read authorization and value will be requested from the application on every read operation. */ + uint8_t wr_auth : 1; /**< Write authorization will be requested from the application on every Write Request operation (but not + Write Command). */ } ble_gatts_attr_md_t; /**@brief GATT Attribute. */ typedef struct { - ble_uuid_t const *p_uuid; /**< Pointer to the attribute UUID. */ - ble_gatts_attr_md_t const *p_attr_md; /**< Pointer to the attribute metadata structure. */ - uint16_t init_len; /**< Initial attribute value length in bytes. */ - uint16_t init_offs; /**< Initial attribute value offset in bytes. If different from zero, the first init_offs bytes of - the attribute value will be left uninitialized. */ - uint16_t max_len; /**< Maximum attribute value length in bytes, see @ref BLE_GATTS_ATTR_LENS_MAX for maximum values. */ - uint8_t *p_value; /**< Pointer to the attribute data. Please note that if the @ref BLE_GATTS_VLOC_USER value location is - selected in the attribute metadata, this will have to point to a buffer that remains valid through - the lifetime of the attribute. This excludes usage of automatic variables that may go out of scope or - any other temporary location. The stack may access that memory directly without the application's - knowledge. For writable characteristics, this value must not be a location in flash memory.*/ + ble_uuid_t const *p_uuid; /**< Pointer to the attribute UUID. */ + ble_gatts_attr_md_t const *p_attr_md; /**< Pointer to the attribute metadata structure. */ + uint16_t init_len; /**< Initial attribute value length in bytes. */ + uint16_t init_offs; /**< Initial attribute value offset in bytes. If different from zero, the first init_offs bytes of the + attribute value will be left uninitialized. */ + uint16_t max_len; /**< Maximum attribute value length in bytes, see @ref BLE_GATTS_ATTR_LENS_MAX for maximum values. */ + uint8_t *p_value; /**< Pointer to the attribute data. Please note that if the @ref BLE_GATTS_VLOC_USER value location is + selected in the attribute metadata, this will have to point to a buffer that remains valid through the + lifetime of the attribute. This excludes usage of automatic variables that may go out of scope or any + other temporary location. The stack may access that memory directly without the application's + knowledge. For writable characteristics, this value must not be a location in flash memory.*/ } ble_gatts_attr_t; /**@brief GATT Attribute Value. */ typedef struct { - uint16_t len; /**< Length in bytes to be written or read. Length in bytes written or read after successful return.*/ - uint16_t offset; /**< Attribute value offset. */ - uint8_t *p_value; /**< Pointer to where value is stored or will be stored. - If value is stored in user memory, only the attribute length is updated when p_value == NULL. - Set to NULL when reading to obtain the complete length of the attribute value */ + uint16_t len; /**< Length in bytes to be written or read. Length in bytes written or read after successful return.*/ + uint16_t offset; /**< Attribute value offset. */ + uint8_t *p_value; /**< Pointer to where value is stored or will be stored. + If value is stored in user memory, only the attribute length is updated when p_value == NULL. + Set to NULL when reading to obtain the complete length of the attribute value */ } ble_gatts_value_t; /**@brief GATT Characteristic Presentation Format. */ typedef struct { - uint8_t format; /**< Format of the value, see @ref BLE_GATT_CPF_FORMATS. */ - int8_t exponent; /**< Exponent for integer data types. */ - uint16_t unit; /**< Unit from Bluetooth Assigned Numbers. */ - uint8_t name_space; /**< Namespace from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ - uint16_t desc; /**< Namespace description from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ + uint8_t format; /**< Format of the value, see @ref BLE_GATT_CPF_FORMATS. */ + int8_t exponent; /**< Exponent for integer data types. */ + uint16_t unit; /**< Unit from Bluetooth Assigned Numbers. */ + uint8_t name_space; /**< Namespace from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ + uint16_t desc; /**< Namespace description from Bluetooth Assigned Numbers, see @ref BLE_GATT_CPF_NAMESPACES. */ } ble_gatts_char_pf_t; /**@brief GATT Characteristic metadata. */ typedef struct { - ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ - ble_gatt_char_ext_props_t char_ext_props; /**< Characteristic Extended Properties. */ - uint8_t const *p_char_user_desc; /**< Pointer to a UTF-8 encoded string (non-NULL terminated), NULL if the descriptor - is not required. */ - uint16_t char_user_desc_max_size; /**< The maximum size in bytes of the user description descriptor. */ - uint16_t char_user_desc_size; /**< The size of the user description, must be smaller or equal to - char_user_desc_max_size. */ - ble_gatts_char_pf_t const *p_char_pf; /**< Pointer to a presentation format structure or NULL if the CPF descriptor is not required. */ - ble_gatts_attr_md_t const *p_user_desc_md; /**< Attribute metadata for the User Description descriptor, or NULL for default values. */ - ble_gatts_attr_md_t const *p_cccd_md; /**< Attribute metadata for the Client Characteristic Configuration Descriptor, - or NULL for default values. */ - ble_gatts_attr_md_t const *p_sccd_md; /**< Attribute metadata for the Server Characteristic Configuration Descriptor, - or NULL for default values. */ + ble_gatt_char_props_t char_props; /**< Characteristic Properties. */ + ble_gatt_char_ext_props_t char_ext_props; /**< Characteristic Extended Properties. */ + uint8_t const * + p_char_user_desc; /**< Pointer to a UTF-8 encoded string (non-NULL terminated), NULL if the descriptor is not required. */ + uint16_t char_user_desc_max_size; /**< The maximum size in bytes of the user description descriptor. */ + uint16_t char_user_desc_size; /**< The size of the user description, must be smaller or equal to char_user_desc_max_size. */ + ble_gatts_char_pf_t const + *p_char_pf; /**< Pointer to a presentation format structure or NULL if the CPF descriptor is not required. */ + ble_gatts_attr_md_t const + *p_user_desc_md; /**< Attribute metadata for the User Description descriptor, or NULL for default values. */ + ble_gatts_attr_md_t const + *p_cccd_md; /**< Attribute metadata for the Client Characteristic Configuration Descriptor, or NULL for default values. */ + ble_gatts_attr_md_t const + *p_sccd_md; /**< Attribute metadata for the Server Characteristic Configuration Descriptor, or NULL for default values. */ } ble_gatts_char_md_t; /**@brief GATT Characteristic Definition Handles. */ typedef struct { - uint16_t value_handle; /**< Handle to the characteristic value. */ - uint16_t user_desc_handle; /**< Handle to the User Description descriptor, or @ref BLE_GATT_HANDLE_INVALID if not - present. */ - uint16_t cccd_handle; /**< Handle to the Client Characteristic Configuration Descriptor, or @ref - BLE_GATT_HANDLE_INVALID if not present. */ - uint16_t sccd_handle; /**< Handle to the Server Characteristic Configuration Descriptor, or @ref - BLE_GATT_HANDLE_INVALID if not present. */ + uint16_t value_handle; /**< Handle to the characteristic value. */ + uint16_t user_desc_handle; /**< Handle to the User Description descriptor, or @ref BLE_GATT_HANDLE_INVALID if not present. */ + uint16_t cccd_handle; /**< Handle to the Client Characteristic Configuration Descriptor, or @ref BLE_GATT_HANDLE_INVALID if + not present. */ + uint16_t sccd_handle; /**< Handle to the Server Characteristic Configuration Descriptor, or @ref BLE_GATT_HANDLE_INVALID if + not present. */ } ble_gatts_char_handles_t; /**@brief GATT HVx parameters. */ typedef struct { - uint16_t handle; /**< Characteristic Value Handle. */ - uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ - uint16_t offset; /**< Offset within the attribute value. */ - uint16_t *p_len; /**< Length in bytes to be written, length in bytes written after return. */ - uint8_t const *p_data; /**< Actual data content, use NULL to use the current attribute value. */ + uint16_t handle; /**< Characteristic Value Handle. */ + uint8_t type; /**< Indication or Notification, see @ref BLE_GATT_HVX_TYPES. */ + uint16_t offset; /**< Offset within the attribute value. */ + uint16_t *p_len; /**< Length in bytes to be written, length in bytes written after return. */ + uint8_t const *p_data; /**< Actual data content, use NULL to use the current attribute value. */ } ble_gatts_hvx_params_t; /**@brief GATT Authorization parameters. */ typedef struct { - uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ - uint8_t update : 1; /**< If set, data supplied in p_data will be used to update the attribute value. - Please note that for @ref BLE_GATTS_AUTHORIZE_TYPE_WRITE operations this bit must always be - set, as the data to be written needs to be stored and later provided by the application. */ - uint16_t offset; /**< Offset of the attribute value being updated. */ - uint16_t len; /**< Length in bytes of the value in p_data pointer, see @ref BLE_GATTS_ATTR_LENS_MAX. */ - uint8_t const *p_data; /**< Pointer to new value used to update the attribute value. */ + uint16_t gatt_status; /**< GATT status code for the operation, see @ref BLE_GATT_STATUS_CODES. */ + uint8_t update : 1; /**< If set, data supplied in p_data will be used to update the attribute value. + Please note that for @ref BLE_GATTS_AUTHORIZE_TYPE_WRITE operations this bit must always be set, + as the data to be written needs to be stored and later provided by the application. */ + uint16_t offset; /**< Offset of the attribute value being updated. */ + uint16_t len; /**< Length in bytes of the value in p_data pointer, see @ref BLE_GATTS_ATTR_LENS_MAX. */ + uint8_t const *p_data; /**< Pointer to new value used to update the attribute value. */ } ble_gatts_authorize_params_t; /**@brief GATT Read or Write Authorize Reply parameters. */ typedef struct { - uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ - union { - ble_gatts_authorize_params_t read; /**< Read authorization parameters. */ - ble_gatts_authorize_params_t write; /**< Write authorization parameters. */ - } params; /**< Reply Parameters. */ + uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ + union { + ble_gatts_authorize_params_t read; /**< Read authorization parameters. */ + ble_gatts_authorize_params_t write; /**< Write authorization parameters. */ + } params; /**< Reply Parameters. */ } ble_gatts_rw_authorize_reply_params_t; /**@brief Service Changed Inclusion configuration parameters, set with @ref sd_ble_cfg_set. */ typedef struct { - uint8_t service_changed : 1; /**< If 1, include the Service Changed characteristic in the Attribute Table. Default is - @ref BLE_GATTS_SERVICE_CHANGED_DEFAULT. */ + uint8_t service_changed : 1; /**< If 1, include the Service Changed characteristic in the Attribute Table. Default is @ref + BLE_GATTS_SERVICE_CHANGED_DEFAULT. */ } ble_gatts_cfg_service_changed_t; /**@brief Service Changed CCCD permission configuration parameters, set with @ref sd_ble_cfg_set. @@ -338,16 +334,16 @@ typedef struct { * * @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true: * - @ref ble_gatts_attr_md_t::write_perm is out of range. - * - @ref ble_gatts_attr_md_t::write_perm is @ref BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS, - * that is not allowed by the Bluetooth Specification. - * - wrong @ref ble_gatts_attr_md_t::read_perm, only @ref - * BLE_GAP_CONN_SEC_MODE_SET_OPEN is allowed by the Bluetooth Specification. + * - @ref ble_gatts_attr_md_t::write_perm is @ref BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS, that is + * not allowed by the Bluetooth Specification. + * - wrong @ref ble_gatts_attr_md_t::read_perm, only @ref BLE_GAP_CONN_SEC_MODE_SET_OPEN is + * allowed by the Bluetooth Specification. * - wrong @ref ble_gatts_attr_md_t::vloc, only @ref BLE_GATTS_VLOC_STACK is allowed. * @retval ::NRF_ERROR_NOT_SUPPORTED Security Mode 2 not supported */ typedef struct { - ble_gatts_attr_md_t perm; /**< Permission for Service Changed CCCD. Default is @ref BLE_GAP_CONN_SEC_MODE_SET_OPEN, no - authorization. */ + ble_gatts_attr_md_t + perm; /**< Permission for Service Changed CCCD. Default is @ref BLE_GAP_CONN_SEC_MODE_SET_OPEN, no authorization. */ } ble_gatts_cfg_service_changed_cccd_perm_t; /**@brief Attribute table size configuration parameters, set with @ref sd_ble_cfg_set. @@ -358,85 +354,86 @@ typedef struct { * - The specified Attribute Table size is not a multiple of 4. */ typedef struct { - uint32_t attr_tab_size; /**< Attribute table size. Default is @ref BLE_GATTS_ATTR_TAB_SIZE_DEFAULT, minimum is @ref - BLE_GATTS_ATTR_TAB_SIZE_MIN. */ + uint32_t attr_tab_size; /**< Attribute table size. Default is @ref BLE_GATTS_ATTR_TAB_SIZE_DEFAULT, minimum is @ref + BLE_GATTS_ATTR_TAB_SIZE_MIN. */ } ble_gatts_cfg_attr_tab_size_t; /**@brief Config structure for GATTS configurations. */ typedef union { - ble_gatts_cfg_service_changed_t service_changed; /**< Include service changed characteristic, cfg_id is @ref BLE_GATTS_CFG_SERVICE_CHANGED. */ - ble_gatts_cfg_service_changed_cccd_perm_t service_changed_cccd_perm; /**< Service changed CCCD permission, cfg_id is @ref - BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM. */ - ble_gatts_cfg_attr_tab_size_t attr_tab_size; /**< Attribute table size, cfg_id is @ref BLE_GATTS_CFG_ATTR_TAB_SIZE. */ + ble_gatts_cfg_service_changed_t + service_changed; /**< Include service changed characteristic, cfg_id is @ref BLE_GATTS_CFG_SERVICE_CHANGED. */ + ble_gatts_cfg_service_changed_cccd_perm_t service_changed_cccd_perm; /**< Service changed CCCD permission, cfg_id is @ref + BLE_GATTS_CFG_SERVICE_CHANGED_CCCD_PERM. */ + ble_gatts_cfg_attr_tab_size_t attr_tab_size; /**< Attribute table size, cfg_id is @ref BLE_GATTS_CFG_ATTR_TAB_SIZE. */ } ble_gatts_cfg_t; /**@brief Event structure for @ref BLE_GATTS_EVT_WRITE. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - ble_uuid_t uuid; /**< Attribute UUID. */ - uint8_t op; /**< Type of write operation, see @ref BLE_GATTS_OPS. */ - uint8_t auth_required; /**< Writing operation deferred due to authorization requirement. Application may use @ref - sd_ble_gatts_value_set to finalize the writing operation. */ - uint16_t offset; /**< Offset for the write operation. */ - uint16_t len; /**< Length of the received data. */ - uint8_t data[1]; /**< Received data. @note This is a variable length array. The size of 1 indicated is only a - placeholder for compilation. See @ref sd_ble_evt_get for more information on how to use - event structures with variable length array members. */ + uint16_t handle; /**< Attribute Handle. */ + ble_uuid_t uuid; /**< Attribute UUID. */ + uint8_t op; /**< Type of write operation, see @ref BLE_GATTS_OPS. */ + uint8_t auth_required; /**< Writing operation deferred due to authorization requirement. Application may use @ref + sd_ble_gatts_value_set to finalize the writing operation. */ + uint16_t offset; /**< Offset for the write operation. */ + uint16_t len; /**< Length of the received data. */ + uint8_t data[1]; /**< Received data. @note This is a variable length array. The size of 1 indicated is only a placeholder for + compilation. See @ref sd_ble_evt_get for more information on how to use event structures with variable + length array members. */ } ble_gatts_evt_write_t; /**@brief Event substructure for authorized read requests, see @ref ble_gatts_evt_rw_authorize_request_t. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ - ble_uuid_t uuid; /**< Attribute UUID. */ - uint16_t offset; /**< Offset for the read operation. */ + uint16_t handle; /**< Attribute Handle. */ + ble_uuid_t uuid; /**< Attribute UUID. */ + uint16_t offset; /**< Offset for the read operation. */ } ble_gatts_evt_read_t; /**@brief Event structure for @ref BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST. */ typedef struct { - uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ - union { - ble_gatts_evt_read_t read; /**< Attribute Read Parameters. */ - ble_gatts_evt_write_t write; /**< Attribute Write Parameters. */ - } request; /**< Request Parameters. */ + uint8_t type; /**< Type of authorize operation, see @ref BLE_GATTS_AUTHORIZE_TYPES. */ + union { + ble_gatts_evt_read_t read; /**< Attribute Read Parameters. */ + ble_gatts_evt_write_t write; /**< Attribute Write Parameters. */ + } request; /**< Request Parameters. */ } ble_gatts_evt_rw_authorize_request_t; /**@brief Event structure for @ref BLE_GATTS_EVT_SYS_ATTR_MISSING. */ typedef struct { - uint8_t hint; /**< Hint (currently unused). */ + uint8_t hint; /**< Hint (currently unused). */ } ble_gatts_evt_sys_attr_missing_t; /**@brief Event structure for @ref BLE_GATTS_EVT_HVC. */ typedef struct { - uint16_t handle; /**< Attribute Handle. */ + uint16_t handle; /**< Attribute Handle. */ } ble_gatts_evt_hvc_t; /**@brief Event structure for @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST. */ typedef struct { - uint16_t client_rx_mtu; /**< Client RX MTU size. */ + uint16_t client_rx_mtu; /**< Client RX MTU size. */ } ble_gatts_evt_exchange_mtu_request_t; /**@brief Event structure for @ref BLE_GATTS_EVT_TIMEOUT. */ typedef struct { - uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ + uint8_t src; /**< Timeout source, see @ref BLE_GATT_TIMEOUT_SOURCES. */ } ble_gatts_evt_timeout_t; /**@brief Event structure for @ref BLE_GATTS_EVT_HVN_TX_COMPLETE. */ typedef struct { - uint8_t count; /**< Number of notification transmissions completed. */ + uint8_t count; /**< Number of notification transmissions completed. */ } ble_gatts_evt_hvn_tx_complete_t; /**@brief GATTS event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which the event occurred. */ - union { - ble_gatts_evt_write_t write; /**< Write Event Parameters. */ - ble_gatts_evt_rw_authorize_request_t authorize_request; /**< Read or Write Authorize Request Parameters. */ - ble_gatts_evt_sys_attr_missing_t sys_attr_missing; /**< System attributes missing. */ - ble_gatts_evt_hvc_t hvc; /**< Handle Value Confirmation Event Parameters. */ - ble_gatts_evt_exchange_mtu_request_t exchange_mtu_request; /**< Exchange MTU Request Event Parameters. */ - ble_gatts_evt_timeout_t timeout; /**< Timeout Event. */ - ble_gatts_evt_hvn_tx_complete_t hvn_tx_complete; /**< Handle Value Notification transmission complete Event Parameters. */ - } params; /**< Event Parameters. */ + uint16_t conn_handle; /**< Connection Handle on which the event occurred. */ + union { + ble_gatts_evt_write_t write; /**< Write Event Parameters. */ + ble_gatts_evt_rw_authorize_request_t authorize_request; /**< Read or Write Authorize Request Parameters. */ + ble_gatts_evt_sys_attr_missing_t sys_attr_missing; /**< System attributes missing. */ + ble_gatts_evt_hvc_t hvc; /**< Handle Value Confirmation Event Parameters. */ + ble_gatts_evt_exchange_mtu_request_t exchange_mtu_request; /**< Exchange MTU Request Event Parameters. */ + ble_gatts_evt_timeout_t timeout; /**< Timeout Event. */ + ble_gatts_evt_hvn_tx_complete_t hvn_tx_complete; /**< Handle Value Notification transmission complete Event Parameters. */ + } params; /**< Event Parameters. */ } ble_gatts_evt_t; /** @} */ @@ -446,9 +443,8 @@ typedef struct { /**@brief Add a service declaration to the Attribute Table. * - * @note Secondary Services are only relevant in the context of the entity that references them, it is therefore - * forbidden to add a secondary service declaration that is not referenced by another service later in the Attribute - * Table. + * @note Secondary Services are only relevant in the context of the entity that references them, it is therefore forbidden to + * add a secondary service declaration that is not referenced by another service later in the Attribute Table. * * @mscs * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} @@ -460,8 +456,7 @@ typedef struct { * * @retval ::NRF_SUCCESS Successfully added a service declaration. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. - * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, Vendor Specific UUIDs need to be present in the - * table. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, Vendor Specific UUIDs need to be present in the table. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. */ @@ -469,8 +464,8 @@ SVCALL(SD_BLE_GATTS_SERVICE_ADD, uint32_t, sd_ble_gatts_service_add(uint8_t type /**@brief Add an include declaration to the Attribute Table. * - * @note It is currently only possible to add an include declaration to the last added service (i.e. only sequential - * population is supported at this time). + * @note It is currently only possible to add an include declaration to the last added service (i.e. only sequential population is + * supported at this time). * * @note The included service must already be present in the Attribute Table prior to this call. * @@ -478,42 +473,42 @@ SVCALL(SD_BLE_GATTS_SERVICE_ADD, uint32_t, sd_ble_gatts_service_add(uint8_t type * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} * @endmscs * - * @param[in] service_handle Handle of the service where the included service is to be placed, if @ref - * BLE_GATT_HANDLE_INVALID is used, it will be placed sequentially. + * @param[in] service_handle Handle of the service where the included service is to be placed, if @ref BLE_GATT_HANDLE_INVALID + * is used, it will be placed sequentially. * @param[in] inc_srvc_handle Handle of the included service. * @param[out] p_include_handle Pointer to a 16-bit word where the assigned handle will be stored. * * @retval ::NRF_SUCCESS Successfully added an include declaration. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. - * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, handle values need to match previously added - * services. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, handle values need to match previously added services. * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation, a service context is required. * @retval ::NRF_ERROR_NOT_SUPPORTED Feature is not supported, service_handle must be that of the last added service. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, self inclusions are not allowed. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. */ -SVCALL(SD_BLE_GATTS_INCLUDE_ADD, uint32_t, sd_ble_gatts_include_add(uint16_t service_handle, uint16_t inc_srvc_handle, uint16_t *p_include_handle)); +SVCALL(SD_BLE_GATTS_INCLUDE_ADD, uint32_t, + sd_ble_gatts_include_add(uint16_t service_handle, uint16_t inc_srvc_handle, uint16_t *p_include_handle)); -/**@brief Add a characteristic declaration, a characteristic value declaration and optional characteristic descriptor - * declarations to the Attribute Table. +/**@brief Add a characteristic declaration, a characteristic value declaration and optional characteristic descriptor declarations + * to the Attribute Table. * - * @note It is currently only possible to add a characteristic to the last added service (i.e. only sequential - * population is supported at this time). + * @note It is currently only possible to add a characteristic to the last added service (i.e. only sequential population is + * supported at this time). * - * @note Several restrictions apply to the parameters, such as matching permissions between the user description - * descriptor and the writable auxiliaries bits, readable (no security) and writable (selectable) CCCDs and SCCDs and - * valid presentation format values. + * @note Several restrictions apply to the parameters, such as matching permissions between the user description descriptor and + * the writable auxiliaries bits, readable (no security) and writable (selectable) CCCDs and SCCDs and valid presentation format + * values. * - * @note If no metadata is provided for the optional descriptors, their permissions will be derived from the - * characteristic permissions. + * @note If no metadata is provided for the optional descriptors, their permissions will be derived from the characteristic + * permissions. * * @mscs * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} * @endmscs * - * @param[in] service_handle Handle of the service where the characteristic is to be placed, if @ref - * BLE_GATT_HANDLE_INVALID is used, it will be placed sequentially. + * @param[in] service_handle Handle of the service where the characteristic is to be placed, if @ref BLE_GATT_HANDLE_INVALID is + * used, it will be placed sequentially. * @param[in] p_char_md Characteristic metadata. * @param[in] p_attr_char_value Pointer to the attribute structure corresponding to the characteristic value. * @param[out] p_handles Pointer to the structure where the assigned handles will be stored. @@ -525,38 +520,37 @@ SVCALL(SD_BLE_GATTS_INCLUDE_ADD, uint32_t, sd_ble_gatts_include_add(uint16_t ser * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation, a service context is required. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. - * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref - * BLE_GATTS_ATTR_LENS_MAX. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref BLE_GATTS_ATTR_LENS_MAX. */ SVCALL(SD_BLE_GATTS_CHARACTERISTIC_ADD, uint32_t, - sd_ble_gatts_characteristic_add(uint16_t service_handle, ble_gatts_char_md_t const *p_char_md, ble_gatts_attr_t const *p_attr_char_value, - ble_gatts_char_handles_t *p_handles)); + sd_ble_gatts_characteristic_add(uint16_t service_handle, ble_gatts_char_md_t const *p_char_md, + ble_gatts_attr_t const *p_attr_char_value, ble_gatts_char_handles_t *p_handles)); /**@brief Add a descriptor to the Attribute Table. * - * @note It is currently only possible to add a descriptor to the last added characteristic (i.e. only sequential - * population is supported at this time). + * @note It is currently only possible to add a descriptor to the last added characteristic (i.e. only sequential population is + * supported at this time). * * @mscs * @mmsc{@ref BLE_GATTS_ATT_TABLE_POP_MSC} * @endmscs * - * @param[in] char_handle Handle of the characteristic where the descriptor is to be placed, if @ref - * BLE_GATT_HANDLE_INVALID is used, it will be placed sequentially. + * @param[in] char_handle Handle of the characteristic where the descriptor is to be placed, if @ref BLE_GATT_HANDLE_INVALID is + * used, it will be placed sequentially. * @param[in] p_attr Pointer to the attribute structure. * @param[out] p_handle Pointer to a 16-bit word where the assigned handle will be stored. * * @retval ::NRF_SUCCESS Successfully added a descriptor. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. - * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, characteristic handle, Vendor Specific UUIDs, - * lengths, and permissions need to adhere to the constraints. + * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied, characteristic handle, Vendor Specific UUIDs, lengths, and + * permissions need to adhere to the constraints. * @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation, a characteristic context is required. * @retval ::NRF_ERROR_FORBIDDEN Forbidden value supplied, certain UUIDs are reserved for the stack. * @retval ::NRF_ERROR_NO_MEM Not enough memory to complete operation. - * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref - * BLE_GATTS_ATTR_LENS_MAX. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref BLE_GATTS_ATTR_LENS_MAX. */ -SVCALL(SD_BLE_GATTS_DESCRIPTOR_ADD, uint32_t, sd_ble_gatts_descriptor_add(uint16_t char_handle, ble_gatts_attr_t const *p_attr, uint16_t *p_handle)); +SVCALL(SD_BLE_GATTS_DESCRIPTOR_ADD, uint32_t, + sd_ble_gatts_descriptor_add(uint16_t char_handle, ble_gatts_attr_t const *p_attr, uint16_t *p_handle)); /**@brief Set the value of a given attribute. * @@ -576,11 +570,11 @@ SVCALL(SD_BLE_GATTS_DESCRIPTOR_ADD, uint32_t, sd_ble_gatts_descriptor_add(uint16 * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. * @retval ::NRF_ERROR_FORBIDDEN Forbidden handle supplied, certain attributes are not modifiable by the application. - * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref - * BLE_GATTS_ATTR_LENS_MAX. + * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied, attribute lengths are restricted by @ref BLE_GATTS_ATTR_LENS_MAX. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied on a system attribute. */ -SVCALL(SD_BLE_GATTS_VALUE_SET, uint32_t, sd_ble_gatts_value_set(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); +SVCALL(SD_BLE_GATTS_VALUE_SET, uint32_t, + sd_ble_gatts_value_set(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); /**@brief Get the value of a given attribute. * @@ -602,21 +596,21 @@ SVCALL(SD_BLE_GATTS_VALUE_SET, uint32_t, sd_ble_gatts_value_set(uint16_t conn_ha * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. * @retval ::NRF_ERROR_INVALID_PARAM Invalid attribute offset supplied. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied on a system attribute. - * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them - * to a known value. + * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them to a known + * value. */ -SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, sd_ble_gatts_value_get(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); +SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, + sd_ble_gatts_value_get(uint16_t conn_handle, uint16_t handle, ble_gatts_value_t *p_value)); /**@brief Notify or Indicate an attribute value. * - * @details This function checks for the relevant Client Characteristic Configuration descriptor value to verify that - * the relevant operation (notification or indication) has been enabled by the client. It is also able to update the - * attribute value before issuing the PDU, so that the application can atomically perform a value update and a server - * initiated transaction with a single API call. + * @details This function checks for the relevant Client Characteristic Configuration descriptor value to verify that the relevant + * operation (notification or indication) has been enabled by the client. It is also able to update the attribute value before + * issuing the PDU, so that the application can atomically perform a value update and a server initiated transaction with a single + * API call. * - * @note The local attribute value may be updated even if an outgoing packet is not sent to the peer due to an error - * during execution. The Attribute Table has been updated if one of the following error codes is returned: @ref - * NRF_ERROR_INVALID_STATE, + * @note The local attribute value may be updated even if an outgoing packet is not sent to the peer due to an error during + * execution. The Attribute Table has been updated if one of the following error codes is returned: @ref NRF_ERROR_INVALID_STATE, * @ref NRF_ERROR_BUSY, * @ref NRF_ERROR_FORBIDDEN, @ref BLE_ERROR_GATTS_SYS_ATTR_MISSING and @ref NRF_ERROR_RESOURCES. * The caller can check whether the value has been updated by looking at the contents of *(@ref @@ -628,17 +622,16 @@ SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, sd_ble_gatts_value_get(uint16_t conn_ha * A @ref BLE_GATTS_EVT_HVC event will be issued as soon as the confirmation arrives from the peer. * * @note The number of Handle Value Notifications that can be queued is configured by @ref - * ble_gatts_conn_cfg_t::hvn_tx_queue_size When the queue is full, the function call will return @ref - * NRF_ERROR_RESOURCES. A @ref BLE_GATTS_EVT_HVN_TX_COMPLETE event will be issued as soon as the transmission of the - * notification is complete. + * ble_gatts_conn_cfg_t::hvn_tx_queue_size When the queue is full, the function call will return @ref NRF_ERROR_RESOURCES. A @ref + * BLE_GATTS_EVT_HVN_TX_COMPLETE event will be issued as soon as the transmission of the notification is complete. * - * @note The application can keep track of the available queue element count for notifications by following the - * procedure below: + * @note The application can keep track of the available queue element count for notifications by following the procedure + * below: * - Store initial queue element count in a variable. - * - Decrement the variable, which stores the currently available queue element count, by one when a call to - * this function returns @ref NRF_SUCCESS. - * - Increment the variable, which stores the current available queue element count, by the count variable in - * @ref BLE_GATTS_EVT_HVN_TX_COMPLETE event. + * - Decrement the variable, which stores the currently available queue element count, by one when a call to this + * function returns @ref NRF_SUCCESS. + * - Increment the variable, which stores the current available queue element count, by the count variable in @ref + * BLE_GATTS_EVT_HVN_TX_COMPLETE event. * * @events * @event{@ref BLE_GATTS_EVT_HVN_TX_COMPLETE, Notification transmission complete.} @@ -659,8 +652,8 @@ SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, sd_ble_gatts_value_get(uint16_t conn_ha * is updated, @ref ble_gatts_hvx_params_t::p_len is updated by the SoftDevice to * contain the number of actual bytes written, else it will be set to 0. * - * @retval ::NRF_SUCCESS Successfully queued a notification or indication for transmission, and optionally updated the - * attribute value. + * @retval ::NRF_SUCCESS Successfully queued a notification or indication for transmission, and optionally updated the attribute + * value. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. * @retval ::NRF_ERROR_INVALID_STATE One or more of the following is true: * - Invalid Connection State @@ -668,18 +661,18 @@ SVCALL(SD_BLE_GATTS_VALUE_GET, uint32_t, sd_ble_gatts_value_get(uint16_t conn_ha * - An ATT_MTU exchange is ongoing * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. - * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied. Only attributes added directly by the - * application are available to notify and indicate. - * @retval ::BLE_ERROR_GATTS_INVALID_ATTR_TYPE Invalid attribute type(s) supplied, only characteristic values may be - * notified and indicated. + * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied. Only attributes added directly by the application + * are available to notify and indicate. + * @retval ::BLE_ERROR_GATTS_INVALID_ATTR_TYPE Invalid attribute type(s) supplied, only characteristic values may be notified and + * indicated. * @retval ::NRF_ERROR_NOT_FOUND Attribute not found. - * @retval ::NRF_ERROR_FORBIDDEN The connection's current security level is lower than the one required by the write - * permissions of the CCCD associated with this characteristic. + * @retval ::NRF_ERROR_FORBIDDEN The connection's current security level is lower than the one required by the write permissions + * of the CCCD associated with this characteristic. * @retval ::NRF_ERROR_DATA_SIZE Invalid data size(s) supplied. - * @retval ::NRF_ERROR_BUSY For @ref BLE_GATT_HVX_INDICATION Procedure already in progress. Wait for a @ref - * BLE_GATTS_EVT_HVC event and retry. - * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them - * to a known value. + * @retval ::NRF_ERROR_BUSY For @ref BLE_GATT_HVX_INDICATION Procedure already in progress. Wait for a @ref BLE_GATTS_EVT_HVC + * event and retry. + * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them to a known + * value. * @retval ::NRF_ERROR_RESOURCES Too many notifications queued. * Wait for a @ref BLE_GATTS_EVT_HVN_TX_COMPLETE event and retry. * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without @@ -689,9 +682,9 @@ SVCALL(SD_BLE_GATTS_HVX, uint32_t, sd_ble_gatts_hvx(uint16_t conn_handle, ble_ga /**@brief Indicate the Service Changed attribute value. * - * @details This call will send a Handle Value Indication to one or more peers connected to inform them that the - * Attribute Table layout has changed. As soon as the peer has confirmed the indication, a @ref BLE_GATTS_EVT_SC_CONFIRM - * event will be issued. + * @details This call will send a Handle Value Indication to one or more peers connected to inform them that the Attribute + * Table layout has changed. As soon as the peer has confirmed the indication, a @ref BLE_GATTS_EVT_SC_CONFIRM event will + * be issued. * * @note Some of the restrictions and limitations that apply to @ref sd_ble_gatts_hvx also apply here. * @@ -716,20 +709,20 @@ SVCALL(SD_BLE_GATTS_HVX, uint32_t, sd_ble_gatts_hvx(uint16_t conn_handle, ble_ga * - Notifications and/or indications not enabled in the CCCD * - An ATT_MTU exchange is ongoing * @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied. - * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied, handles must be in the range populated - * by the application. + * @retval ::BLE_ERROR_INVALID_ATTR_HANDLE Invalid attribute handle(s) supplied, handles must be in the range populated by the + * application. * @retval ::NRF_ERROR_BUSY Procedure already in progress. - * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them - * to a known value. + * @retval ::BLE_ERROR_GATTS_SYS_ATTR_MISSING System attributes missing, use @ref sd_ble_gatts_sys_attr_set to set them to a known + * value. * @retval ::NRF_ERROR_TIMEOUT There has been a GATT procedure timeout. No new GATT procedure can be performed without * reestablishing the connection. */ -SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, sd_ble_gatts_service_changed(uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle)); +SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, + sd_ble_gatts_service_changed(uint16_t conn_handle, uint16_t start_handle, uint16_t end_handle)); /**@brief Respond to a Read/Write authorization request. * - * @note This call should only be used as a response to a @ref BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST event issued to the - * application. + * @note This call should only be used as a response to a @ref BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST event issued to the application. * * @mscs * @mmsc{@ref BLE_GATTS_QUEUED_WRITE_NOBUF_AUTH_MSC} @@ -748,8 +741,8 @@ SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, sd_ble_gatts_service_changed(uint * to a @ref BLE_GATTS_AUTHORIZE_TYPE_READ event if @ref ble_gatts_authorize_params_t::update * is set to 0. * - * @retval ::NRF_SUCCESS Successfully queued a response to the peer, and in the case of a write operation, - * Attribute Table updated. + * @retval ::NRF_SUCCESS Successfully queued a response to the peer, and in the case of a write operation, Attribute + * Table updated. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. * @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry. * @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied. @@ -761,7 +754,8 @@ SVCALL(SD_BLE_GATTS_SERVICE_CHANGED, uint32_t, sd_ble_gatts_service_changed(uint * reestablishing the connection. */ SVCALL(SD_BLE_GATTS_RW_AUTHORIZE_REPLY, uint32_t, - sd_ble_gatts_rw_authorize_reply(uint16_t conn_handle, ble_gatts_rw_authorize_reply_params_t const *p_rw_authorize_reply_params)); + sd_ble_gatts_rw_authorize_reply(uint16_t conn_handle, + ble_gatts_rw_authorize_reply_params_t const *p_rw_authorize_reply_params)); /**@brief Update persistent system attribute information. * @@ -776,18 +770,16 @@ SVCALL(SD_BLE_GATTS_RW_AUTHORIZE_REPLY, uint32_t, * If the pointer is NULL, the system attribute info is initialized, assuming that * the application does not have any previously saved system attribute data for this device. * - * @note The state of persistent system attributes is reset upon connection establishment and then remembered for its - * duration. + * @note The state of persistent system attributes is reset upon connection establishment and then remembered for its duration. * - * @note If this call returns with an error code different from @ref NRF_SUCCESS, the storage of persistent system - * attributes may have been completed only partially. This means that the state of the attribute table is undefined, and - * the application should either provide a new set of attributes using this same call or reset the SoftDevice to return - * to a known state. + * @note If this call returns with an error code different from @ref NRF_SUCCESS, the storage of persistent system attributes may + * have been completed only partially. This means that the state of the attribute table is undefined, and the application should + * either provide a new set of attributes using this same call or reset the SoftDevice to return to a known state. * - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included - * in system services will be modified. - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included - * in user services will be modified. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included in system + * services will be modified. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included in user + * services will be modified. * * @mscs * @mmsc{@ref BLE_GATTS_HVX_SYS_ATTRS_MISSING_MSC} @@ -815,29 +807,27 @@ SVCALL(SD_BLE_GATTS_SYS_ATTR_SET, uint32_t, /**@brief Retrieve persistent system attribute information from the stack. * * @details This call is used to retrieve information about values to be stored persistently by the application - * during the lifetime of a connection or after it has been terminated. When a new connection is established - * with the same bonded device, the system attribute information retrieved with this function should be restored using - * using @ref sd_ble_gatts_sys_attr_set. If retrieved after disconnection, the data should be read before a new - * connection established. The connection handle for the previous, now disconnected, connection will remain valid until - * a new one is created to allow this API call to refer to it. Connection handles belonging to active connections can be - * used as well, but care should be taken since the system attributes may be written to at any time by the peer during a - * connection's lifetime. + * during the lifetime of a connection or after it has been terminated. When a new connection is established with the + * same bonded device, the system attribute information retrieved with this function should be restored using using @ref + * sd_ble_gatts_sys_attr_set. If retrieved after disconnection, the data should be read before a new connection established. The + * connection handle for the previous, now disconnected, connection will remain valid until a new one is created to allow this API + * call to refer to it. Connection handles belonging to active connections can be used as well, but care should be taken since the + * system attributes may be written to at any time by the peer during a connection's lifetime. * - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included - * in system services will be returned. - * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included - * in user services will be returned. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS is used with this function, only the system attributes included in system + * services will be returned. + * @note When the @ref BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS is used with this function, only the system attributes included in user + * services will be returned. * * @mscs * @mmsc{@ref BLE_GATTS_SYS_ATTRS_BONDED_PEER_MSC} * @endmscs * * @param[in] conn_handle Connection handle of the recently terminated connection. - * @param[out] p_sys_attr_data Pointer to a buffer where updated information about system attributes will be filled - * in. The format of the data is described in @ref BLE_GATTS_SYS_ATTRS_FORMAT. NULL can be provided to obtain the length - * of the data. - * @param[in,out] p_len Size of application buffer if p_sys_attr_data is not NULL. Unconditionally updated - * to actual length of system attribute data. + * @param[out] p_sys_attr_data Pointer to a buffer where updated information about system attributes will be filled in. The + * format of the data is described in @ref BLE_GATTS_SYS_ATTRS_FORMAT. NULL can be provided to obtain the length of the data. + * @param[in,out] p_len Size of application buffer if p_sys_attr_data is not NULL. Unconditionally updated to actual + * length of system attribute data. * @param[in] flags Optional additional flags, see @ref BLE_GATTS_SYS_ATTR_FLAGS * * @retval ::NRF_SUCCESS Successfully retrieved the system attribute information. @@ -891,8 +881,8 @@ SVCALL(SD_BLE_GATTS_ATTR_GET, uint32_t, sd_ble_gatts_attr_get(uint16_t handle, b * - The minimum value is @ref BLE_GATT_ATT_MTU_DEFAULT. * - The maximum value is @ref ble_gatt_conn_cfg_t::att_mtu in the connection configuration * used for this connection. - * - The value must be equal to Client RX MTU size given in @ref - * sd_ble_gattc_exchange_mtu_request if an ATT_MTU exchange has already been performed in the other direction. + * - The value must be equal to Client RX MTU size given in @ref sd_ble_gattc_exchange_mtu_request + * if an ATT_MTU exchange has already been performed in the other direction. * * @retval ::NRF_SUCCESS Successfully sent response to the client. * @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle. diff --git a/src/platform/nrf52/softdevice/ble_hci.h b/src/platform/nrf52/softdevice/ble_hci.h index 2feb661be..27f85d52e 100644 --- a/src/platform/nrf52/softdevice/ble_hci.h +++ b/src/platform/nrf52/softdevice/ble_hci.h @@ -71,8 +71,8 @@ extern "C" { 0x11 Unsupported Feature or Parameter Value*/ #define BLE_HCI_STATUS_CODE_INVALID_BTLE_COMMAND_PARAMETERS 0x12 /**< Invalid BLE Command Parameters. */ #define BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION 0x13 /**< Remote User Terminated Connection. */ -#define BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES \ - 0x14 /**< Remote Device Terminated Connection due to low \ +#define BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES \ + 0x14 /**< Remote Device Terminated Connection due to low \ resources.*/ #define BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_POWER_OFF 0x15 /**< Remote Device Terminated Connection due to power off. */ #define BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION 0x16 /**< Local Host Terminated Connection. */ diff --git a/src/platform/nrf52/softdevice/ble_l2cap.h b/src/platform/nrf52/softdevice/ble_l2cap.h index 6685dc22e..5f4bd277d 100644 --- a/src/platform/nrf52/softdevice/ble_l2cap.h +++ b/src/platform/nrf52/softdevice/ble_l2cap.h @@ -83,32 +83,32 @@ extern "C" { /**@brief L2CAP API SVC numbers. */ enum BLE_L2CAP_SVCS { - SD_BLE_L2CAP_CH_SETUP = BLE_L2CAP_SVC_BASE + 0, /**< Set up an L2CAP channel. */ - SD_BLE_L2CAP_CH_RELEASE = BLE_L2CAP_SVC_BASE + 1, /**< Release an L2CAP channel. */ - SD_BLE_L2CAP_CH_RX = BLE_L2CAP_SVC_BASE + 2, /**< Receive an SDU on an L2CAP channel. */ - SD_BLE_L2CAP_CH_TX = BLE_L2CAP_SVC_BASE + 3, /**< Transmit an SDU on an L2CAP channel. */ - SD_BLE_L2CAP_CH_FLOW_CONTROL = BLE_L2CAP_SVC_BASE + 4, /**< Advanced SDU reception flow control. */ + SD_BLE_L2CAP_CH_SETUP = BLE_L2CAP_SVC_BASE + 0, /**< Set up an L2CAP channel. */ + SD_BLE_L2CAP_CH_RELEASE = BLE_L2CAP_SVC_BASE + 1, /**< Release an L2CAP channel. */ + SD_BLE_L2CAP_CH_RX = BLE_L2CAP_SVC_BASE + 2, /**< Receive an SDU on an L2CAP channel. */ + SD_BLE_L2CAP_CH_TX = BLE_L2CAP_SVC_BASE + 3, /**< Transmit an SDU on an L2CAP channel. */ + SD_BLE_L2CAP_CH_FLOW_CONTROL = BLE_L2CAP_SVC_BASE + 4, /**< Advanced SDU reception flow control. */ }; /**@brief L2CAP Event IDs. */ enum BLE_L2CAP_EVTS { - BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event. - \n Reply with @ref sd_ble_l2cap_ch_setup. - \n See @ref ble_l2cap_evt_ch_setup_request_t. */ - BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event. - \n See @ref ble_l2cap_evt_ch_setup_refused_t. */ - BLE_L2CAP_EVT_CH_SETUP = BLE_L2CAP_EVT_BASE + 2, /**< L2CAP Channel Setup Completed event. - \n See @ref ble_l2cap_evt_ch_setup_t. */ - BLE_L2CAP_EVT_CH_RELEASED = BLE_L2CAP_EVT_BASE + 3, /**< L2CAP Channel Released event. - \n No additional event structure applies. */ - BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED = BLE_L2CAP_EVT_BASE + 4, /**< L2CAP Channel SDU data buffer released event. - \n See @ref ble_l2cap_evt_ch_sdu_buf_released_t. */ - BLE_L2CAP_EVT_CH_CREDIT = BLE_L2CAP_EVT_BASE + 5, /**< L2CAP Channel Credit received. - \n See @ref ble_l2cap_evt_ch_credit_t. */ - BLE_L2CAP_EVT_CH_RX = BLE_L2CAP_EVT_BASE + 6, /**< L2CAP Channel SDU received. - \n See @ref ble_l2cap_evt_ch_rx_t. */ - BLE_L2CAP_EVT_CH_TX = BLE_L2CAP_EVT_BASE + 7, /**< L2CAP Channel SDU transmitted. - \n See @ref ble_l2cap_evt_ch_tx_t. */ + BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event. + \n Reply with @ref sd_ble_l2cap_ch_setup. + \n See @ref ble_l2cap_evt_ch_setup_request_t. */ + BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event. + \n See @ref ble_l2cap_evt_ch_setup_refused_t. */ + BLE_L2CAP_EVT_CH_SETUP = BLE_L2CAP_EVT_BASE + 2, /**< L2CAP Channel Setup Completed event. + \n See @ref ble_l2cap_evt_ch_setup_t. */ + BLE_L2CAP_EVT_CH_RELEASED = BLE_L2CAP_EVT_BASE + 3, /**< L2CAP Channel Released event. + \n No additional event structure applies. */ + BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED = BLE_L2CAP_EVT_BASE + 4, /**< L2CAP Channel SDU data buffer released event. + \n See @ref ble_l2cap_evt_ch_sdu_buf_released_t. */ + BLE_L2CAP_EVT_CH_CREDIT = BLE_L2CAP_EVT_BASE + 5, /**< L2CAP Channel Credit received. + \n See @ref ble_l2cap_evt_ch_credit_t. */ + BLE_L2CAP_EVT_CH_RX = BLE_L2CAP_EVT_BASE + 6, /**< L2CAP Channel SDU received. + \n See @ref ble_l2cap_evt_ch_rx_t. */ + BLE_L2CAP_EVT_CH_TX = BLE_L2CAP_EVT_BASE + 7, /**< L2CAP Channel SDU transmitted. + \n See @ref ble_l2cap_evt_ch_tx_t. */ }; /** @} */ @@ -149,8 +149,9 @@ enum BLE_L2CAP_EVTS { #define BLE_L2CAP_CH_STATUS_CODE_INVALID_SCID (0x0009) /**< Invalid Source CID. */ #define BLE_L2CAP_CH_STATUS_CODE_SCID_ALLOCATED (0x000A) /**< Source CID already allocated. */ #define BLE_L2CAP_CH_STATUS_CODE_UNACCEPTABLE_PARAMS (0x000B) /**< Unacceptable parameters. */ -#define BLE_L2CAP_CH_STATUS_CODE_NOT_UNDERSTOOD (0x8000) /**< Command Reject received instead of LE Credit Based Connection Response. */ -#define BLE_L2CAP_CH_STATUS_CODE_TIMEOUT (0xC000) /**< Operation timed out. */ +#define BLE_L2CAP_CH_STATUS_CODE_NOT_UNDERSTOOD \ + (0x8000) /**< Command Reject received instead of LE Credit Based Connection Response. */ +#define BLE_L2CAP_CH_STATUS_CODE_TIMEOUT (0xC000) /**< Operation timed out. */ /** @} */ /** @} */ @@ -171,120 +172,120 @@ enum BLE_L2CAP_EVTS { * @retval ::NRF_ERROR_NO_MEM rx_mps or tx_mps is set too high. */ typedef struct { - uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall - be able to receive on L2CAP channels on connections with this - configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ - uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall - be able to transmit on L2CAP channels on connections with this - configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ - uint8_t rx_queue_size; /**< Number of SDU data buffers that can be queued for reception per - L2CAP channel. The minimum value is one. */ - uint8_t tx_queue_size; /**< Number of SDU data buffers that can be queued for transmission - per L2CAP channel. The minimum value is one. */ - uint8_t ch_count; /**< Number of L2CAP channels the application can create per connection - with this configuration. The default value is zero, the maximum - value is @ref BLE_L2CAP_CH_COUNT_MAX. - @note if this parameter is set to zero, all other parameters in - @ref ble_l2cap_conn_cfg_t are ignored. */ + uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall + be able to receive on L2CAP channels on connections with this + configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ + uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall + be able to transmit on L2CAP channels on connections with this + configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */ + uint8_t rx_queue_size; /**< Number of SDU data buffers that can be queued for reception per + L2CAP channel. The minimum value is one. */ + uint8_t tx_queue_size; /**< Number of SDU data buffers that can be queued for transmission + per L2CAP channel. The minimum value is one. */ + uint8_t ch_count; /**< Number of L2CAP channels the application can create per connection + with this configuration. The default value is zero, the maximum + value is @ref BLE_L2CAP_CH_COUNT_MAX. + @note if this parameter is set to zero, all other parameters in + @ref ble_l2cap_conn_cfg_t are ignored. */ } ble_l2cap_conn_cfg_t; /**@brief L2CAP channel RX parameters. */ typedef struct { - uint16_t rx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP shall be able to - receive on this L2CAP channel. - - Must be equal to or greater than @ref BLE_L2CAP_MTU_MIN. */ - uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall be - able to receive on this L2CAP channel. - - Must be equal to or greater than @ref BLE_L2CAP_MPS_MIN. - - Must be equal to or less than @ref ble_l2cap_conn_cfg_t::rx_mps. */ - ble_data_t sdu_buf; /**< SDU data buffer for reception. - - If @ref ble_data_t::p_data is non-NULL, initial credits are - issued to the peer. - - If @ref ble_data_t::p_data is NULL, no initial credits are - issued to the peer. */ + uint16_t rx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP shall be able to + receive on this L2CAP channel. + - Must be equal to or greater than @ref BLE_L2CAP_MTU_MIN. */ + uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall be + able to receive on this L2CAP channel. + - Must be equal to or greater than @ref BLE_L2CAP_MPS_MIN. + - Must be equal to or less than @ref ble_l2cap_conn_cfg_t::rx_mps. */ + ble_data_t sdu_buf; /**< SDU data buffer for reception. + - If @ref ble_data_t::p_data is non-NULL, initial credits are + issued to the peer. + - If @ref ble_data_t::p_data is NULL, no initial credits are + issued to the peer. */ } ble_l2cap_ch_rx_params_t; /**@brief L2CAP channel setup parameters. */ typedef struct { - ble_l2cap_ch_rx_params_t rx_params; /**< L2CAP channel RX parameters. */ - uint16_t le_psm; /**< LE Protocol/Service Multiplexer. Used when requesting - setup of an L2CAP channel, ignored otherwise. */ - uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES. - Used when replying to a setup request of an L2CAP - channel, ignored otherwise. */ + ble_l2cap_ch_rx_params_t rx_params; /**< L2CAP channel RX parameters. */ + uint16_t le_psm; /**< LE Protocol/Service Multiplexer. Used when requesting + setup of an L2CAP channel, ignored otherwise. */ + uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES. + Used when replying to a setup request of an L2CAP + channel, ignored otherwise. */ } ble_l2cap_ch_setup_params_t; /**@brief L2CAP channel TX parameters. */ typedef struct { - uint16_t tx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP is able to - transmit on this L2CAP channel. */ - uint16_t peer_mps; /**< The maximum L2CAP PDU payload size, in bytes, that the peer is - able to receive on this L2CAP channel. */ - uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP is able - to transmit on this L2CAP channel. This is effective tx_mps, - selected by the SoftDevice as - MIN( @ref ble_l2cap_ch_tx_params_t::peer_mps, @ref ble_l2cap_conn_cfg_t::tx_mps ) */ - uint16_t credits; /**< Initial credits given by the peer. */ + uint16_t tx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP is able to + transmit on this L2CAP channel. */ + uint16_t peer_mps; /**< The maximum L2CAP PDU payload size, in bytes, that the peer is + able to receive on this L2CAP channel. */ + uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP is able + to transmit on this L2CAP channel. This is effective tx_mps, + selected by the SoftDevice as + MIN( @ref ble_l2cap_ch_tx_params_t::peer_mps, @ref ble_l2cap_conn_cfg_t::tx_mps ) */ + uint16_t credits; /**< Initial credits given by the peer. */ } ble_l2cap_ch_tx_params_t; /**@brief L2CAP Channel Setup Request event. */ typedef struct { - ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ - uint16_t le_psm; /**< LE Protocol/Service Multiplexer. */ + ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ + uint16_t le_psm; /**< LE Protocol/Service Multiplexer. */ } ble_l2cap_evt_ch_setup_request_t; /**@brief L2CAP Channel Setup Refused event. */ typedef struct { - uint8_t source; /**< Source, see @ref BLE_L2CAP_CH_SETUP_REFUSED_SRCS */ - uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES */ + uint8_t source; /**< Source, see @ref BLE_L2CAP_CH_SETUP_REFUSED_SRCS */ + uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES */ } ble_l2cap_evt_ch_setup_refused_t; /**@brief L2CAP Channel Setup Completed event. */ typedef struct { - ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ + ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */ } ble_l2cap_evt_ch_setup_t; /**@brief L2CAP Channel SDU Data Buffer Released event. */ typedef struct { - ble_data_t sdu_buf; /**< Returned reception or transmission SDU data buffer. The SoftDevice - returns SDU data buffers supplied by the application, which have - not yet been returned previously via a @ref BLE_L2CAP_EVT_CH_RX or - @ref BLE_L2CAP_EVT_CH_TX event. */ + ble_data_t sdu_buf; /**< Returned reception or transmission SDU data buffer. The SoftDevice + returns SDU data buffers supplied by the application, which have + not yet been returned previously via a @ref BLE_L2CAP_EVT_CH_RX or + @ref BLE_L2CAP_EVT_CH_TX event. */ } ble_l2cap_evt_ch_sdu_buf_released_t; /**@brief L2CAP Channel Credit received event. */ typedef struct { - uint16_t credits; /**< Additional credits given by the peer. */ + uint16_t credits; /**< Additional credits given by the peer. */ } ble_l2cap_evt_ch_credit_t; /**@brief L2CAP Channel received SDU event. */ typedef struct { - uint16_t sdu_len; /**< Total SDU length, in bytes. */ - ble_data_t sdu_buf; /**< SDU data buffer. - @note If there is not enough space in the buffer - (sdu_buf.len < sdu_len) then the rest of the SDU will be - silently discarded by the SoftDevice. */ + uint16_t sdu_len; /**< Total SDU length, in bytes. */ + ble_data_t sdu_buf; /**< SDU data buffer. + @note If there is not enough space in the buffer + (sdu_buf.len < sdu_len) then the rest of the SDU will be + silently discarded by the SoftDevice. */ } ble_l2cap_evt_ch_rx_t; /**@brief L2CAP Channel transmitted SDU event. */ typedef struct { - ble_data_t sdu_buf; /**< SDU data buffer. */ + ble_data_t sdu_buf; /**< SDU data buffer. */ } ble_l2cap_evt_ch_tx_t; /**@brief L2CAP event structure. */ typedef struct { - uint16_t conn_handle; /**< Connection Handle on which the event occured. */ - uint16_t local_cid; /**< Local Channel ID of the L2CAP channel, or - @ref BLE_L2CAP_CID_INVALID if not present. */ - union { - ble_l2cap_evt_ch_setup_request_t ch_setup_request; /**< L2CAP Channel Setup Request Event Parameters. */ - ble_l2cap_evt_ch_setup_refused_t ch_setup_refused; /**< L2CAP Channel Setup Refused Event Parameters. */ - ble_l2cap_evt_ch_setup_t ch_setup; /**< L2CAP Channel Setup Completed Event Parameters. */ - ble_l2cap_evt_ch_sdu_buf_released_t ch_sdu_buf_released; /**< L2CAP Channel SDU Data Buffer Released Event Parameters. */ - ble_l2cap_evt_ch_credit_t credit; /**< L2CAP Channel Credit Received Event Parameters. */ - ble_l2cap_evt_ch_rx_t rx; /**< L2CAP Channel SDU Received Event Parameters. */ - ble_l2cap_evt_ch_tx_t tx; /**< L2CAP Channel SDU Transmitted Event Parameters. */ - } params; /**< Event Parameters. */ + uint16_t conn_handle; /**< Connection Handle on which the event occured. */ + uint16_t local_cid; /**< Local Channel ID of the L2CAP channel, or + @ref BLE_L2CAP_CID_INVALID if not present. */ + union { + ble_l2cap_evt_ch_setup_request_t ch_setup_request; /**< L2CAP Channel Setup Request Event Parameters. */ + ble_l2cap_evt_ch_setup_refused_t ch_setup_refused; /**< L2CAP Channel Setup Refused Event Parameters. */ + ble_l2cap_evt_ch_setup_t ch_setup; /**< L2CAP Channel Setup Completed Event Parameters. */ + ble_l2cap_evt_ch_sdu_buf_released_t ch_sdu_buf_released; /**< L2CAP Channel SDU Data Buffer Released Event Parameters. */ + ble_l2cap_evt_ch_credit_t credit; /**< L2CAP Channel Credit Received Event Parameters. */ + ble_l2cap_evt_ch_rx_t rx; /**< L2CAP Channel SDU Received Event Parameters. */ + ble_l2cap_evt_ch_tx_t tx; /**< L2CAP Channel SDU Transmitted Event Parameters. */ + } params; /**< Event Parameters. */ } ble_l2cap_evt_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/ble_types.h b/src/platform/nrf52/softdevice/ble_types.h index f64503779..db3656cfd 100644 --- a/src/platform/nrf52/softdevice/ble_types.h +++ b/src/platform/nrf52/softdevice/ble_types.h @@ -101,77 +101,79 @@ extern "C" { * @note Retrieved from * http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml * @{ */ -#define BLE_APPEARANCE_UNKNOWN 0 /**< Unknown. */ -#define BLE_APPEARANCE_GENERIC_PHONE 64 /**< Generic Phone. */ -#define BLE_APPEARANCE_GENERIC_COMPUTER 128 /**< Generic Computer. */ -#define BLE_APPEARANCE_GENERIC_WATCH 192 /**< Generic Watch. */ -#define BLE_APPEARANCE_WATCH_SPORTS_WATCH 193 /**< Watch: Sports Watch. */ -#define BLE_APPEARANCE_GENERIC_CLOCK 256 /**< Generic Clock. */ -#define BLE_APPEARANCE_GENERIC_DISPLAY 320 /**< Generic Display. */ -#define BLE_APPEARANCE_GENERIC_REMOTE_CONTROL 384 /**< Generic Remote Control. */ -#define BLE_APPEARANCE_GENERIC_EYE_GLASSES 448 /**< Generic Eye-glasses. */ -#define BLE_APPEARANCE_GENERIC_TAG 512 /**< Generic Tag. */ -#define BLE_APPEARANCE_GENERIC_KEYRING 576 /**< Generic Keyring. */ -#define BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 640 /**< Generic Media Player. */ -#define BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 704 /**< Generic Barcode Scanner. */ -#define BLE_APPEARANCE_GENERIC_THERMOMETER 768 /**< Generic Thermometer. */ -#define BLE_APPEARANCE_THERMOMETER_EAR 769 /**< Thermometer: Ear. */ -#define BLE_APPEARANCE_GENERIC_HEART_RATE_SENSOR 832 /**< Generic Heart rate Sensor. */ -#define BLE_APPEARANCE_HEART_RATE_SENSOR_HEART_RATE_BELT 833 /**< Heart Rate Sensor: Heart Rate Belt. */ -#define BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 896 /**< Generic Blood Pressure. */ -#define BLE_APPEARANCE_BLOOD_PRESSURE_ARM 897 /**< Blood Pressure: Arm. */ -#define BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 898 /**< Blood Pressure: Wrist. */ -#define BLE_APPEARANCE_GENERIC_HID 960 /**< Human Interface Device (HID). */ -#define BLE_APPEARANCE_HID_KEYBOARD 961 /**< Keyboard (HID Subtype). */ -#define BLE_APPEARANCE_HID_MOUSE 962 /**< Mouse (HID Subtype). */ -#define BLE_APPEARANCE_HID_JOYSTICK 963 /**< Joystick (HID Subtype). */ -#define BLE_APPEARANCE_HID_GAMEPAD 964 /**< Gamepad (HID Subtype). */ -#define BLE_APPEARANCE_HID_DIGITIZERSUBTYPE 965 /**< Digitizer Tablet (HID Subtype). */ -#define BLE_APPEARANCE_HID_CARD_READER 966 /**< Card Reader (HID Subtype). */ -#define BLE_APPEARANCE_HID_DIGITAL_PEN 967 /**< Digital Pen (HID Subtype). */ -#define BLE_APPEARANCE_HID_BARCODE 968 /**< Barcode Scanner (HID Subtype). */ -#define BLE_APPEARANCE_GENERIC_GLUCOSE_METER 1024 /**< Generic Glucose Meter. */ -#define BLE_APPEARANCE_GENERIC_RUNNING_WALKING_SENSOR 1088 /**< Generic Running Walking Sensor. */ -#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_IN_SHOE 1089 /**< Running Walking Sensor: In-Shoe. */ -#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_SHOE 1090 /**< Running Walking Sensor: On-Shoe. */ -#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_HIP 1091 /**< Running Walking Sensor: On-Hip. */ -#define BLE_APPEARANCE_GENERIC_CYCLING 1152 /**< Generic Cycling. */ -#define BLE_APPEARANCE_CYCLING_CYCLING_COMPUTER 1153 /**< Cycling: Cycling Computer. */ -#define BLE_APPEARANCE_CYCLING_SPEED_SENSOR 1154 /**< Cycling: Speed Sensor. */ -#define BLE_APPEARANCE_CYCLING_CADENCE_SENSOR 1155 /**< Cycling: Cadence Sensor. */ -#define BLE_APPEARANCE_CYCLING_POWER_SENSOR 1156 /**< Cycling: Power Sensor. */ -#define BLE_APPEARANCE_CYCLING_SPEED_CADENCE_SENSOR 1157 /**< Cycling: Speed and Cadence Sensor. */ -#define BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 3136 /**< Generic Pulse Oximeter. */ -#define BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 3137 /**< Fingertip (Pulse Oximeter subtype). */ -#define BLE_APPEARANCE_PULSE_OXIMETER_WRIST_WORN 3138 /**< Wrist Worn(Pulse Oximeter subtype). */ -#define BLE_APPEARANCE_GENERIC_WEIGHT_SCALE 3200 /**< Generic Weight Scale. */ -#define BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS_ACT 5184 /**< Generic Outdoor Sports Activity. */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_DISP 5185 /**< Location Display Device (Outdoor Sports Activity subtype). */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_DISP 5186 /**< Location and Navigation Display Device (Outdoor Sports Activity subtype). */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_POD 5187 /**< Location Pod (Outdoor Sports Activity subtype). */ -#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_POD 5188 /**< Location and Navigation Pod (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_UNKNOWN 0 /**< Unknown. */ +#define BLE_APPEARANCE_GENERIC_PHONE 64 /**< Generic Phone. */ +#define BLE_APPEARANCE_GENERIC_COMPUTER 128 /**< Generic Computer. */ +#define BLE_APPEARANCE_GENERIC_WATCH 192 /**< Generic Watch. */ +#define BLE_APPEARANCE_WATCH_SPORTS_WATCH 193 /**< Watch: Sports Watch. */ +#define BLE_APPEARANCE_GENERIC_CLOCK 256 /**< Generic Clock. */ +#define BLE_APPEARANCE_GENERIC_DISPLAY 320 /**< Generic Display. */ +#define BLE_APPEARANCE_GENERIC_REMOTE_CONTROL 384 /**< Generic Remote Control. */ +#define BLE_APPEARANCE_GENERIC_EYE_GLASSES 448 /**< Generic Eye-glasses. */ +#define BLE_APPEARANCE_GENERIC_TAG 512 /**< Generic Tag. */ +#define BLE_APPEARANCE_GENERIC_KEYRING 576 /**< Generic Keyring. */ +#define BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 640 /**< Generic Media Player. */ +#define BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 704 /**< Generic Barcode Scanner. */ +#define BLE_APPEARANCE_GENERIC_THERMOMETER 768 /**< Generic Thermometer. */ +#define BLE_APPEARANCE_THERMOMETER_EAR 769 /**< Thermometer: Ear. */ +#define BLE_APPEARANCE_GENERIC_HEART_RATE_SENSOR 832 /**< Generic Heart rate Sensor. */ +#define BLE_APPEARANCE_HEART_RATE_SENSOR_HEART_RATE_BELT 833 /**< Heart Rate Sensor: Heart Rate Belt. */ +#define BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 896 /**< Generic Blood Pressure. */ +#define BLE_APPEARANCE_BLOOD_PRESSURE_ARM 897 /**< Blood Pressure: Arm. */ +#define BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 898 /**< Blood Pressure: Wrist. */ +#define BLE_APPEARANCE_GENERIC_HID 960 /**< Human Interface Device (HID). */ +#define BLE_APPEARANCE_HID_KEYBOARD 961 /**< Keyboard (HID Subtype). */ +#define BLE_APPEARANCE_HID_MOUSE 962 /**< Mouse (HID Subtype). */ +#define BLE_APPEARANCE_HID_JOYSTICK 963 /**< Joystick (HID Subtype). */ +#define BLE_APPEARANCE_HID_GAMEPAD 964 /**< Gamepad (HID Subtype). */ +#define BLE_APPEARANCE_HID_DIGITIZERSUBTYPE 965 /**< Digitizer Tablet (HID Subtype). */ +#define BLE_APPEARANCE_HID_CARD_READER 966 /**< Card Reader (HID Subtype). */ +#define BLE_APPEARANCE_HID_DIGITAL_PEN 967 /**< Digital Pen (HID Subtype). */ +#define BLE_APPEARANCE_HID_BARCODE 968 /**< Barcode Scanner (HID Subtype). */ +#define BLE_APPEARANCE_GENERIC_GLUCOSE_METER 1024 /**< Generic Glucose Meter. */ +#define BLE_APPEARANCE_GENERIC_RUNNING_WALKING_SENSOR 1088 /**< Generic Running Walking Sensor. */ +#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_IN_SHOE 1089 /**< Running Walking Sensor: In-Shoe. */ +#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_SHOE 1090 /**< Running Walking Sensor: On-Shoe. */ +#define BLE_APPEARANCE_RUNNING_WALKING_SENSOR_ON_HIP 1091 /**< Running Walking Sensor: On-Hip. */ +#define BLE_APPEARANCE_GENERIC_CYCLING 1152 /**< Generic Cycling. */ +#define BLE_APPEARANCE_CYCLING_CYCLING_COMPUTER 1153 /**< Cycling: Cycling Computer. */ +#define BLE_APPEARANCE_CYCLING_SPEED_SENSOR 1154 /**< Cycling: Speed Sensor. */ +#define BLE_APPEARANCE_CYCLING_CADENCE_SENSOR 1155 /**< Cycling: Cadence Sensor. */ +#define BLE_APPEARANCE_CYCLING_POWER_SENSOR 1156 /**< Cycling: Power Sensor. */ +#define BLE_APPEARANCE_CYCLING_SPEED_CADENCE_SENSOR 1157 /**< Cycling: Speed and Cadence Sensor. */ +#define BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 3136 /**< Generic Pulse Oximeter. */ +#define BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 3137 /**< Fingertip (Pulse Oximeter subtype). */ +#define BLE_APPEARANCE_PULSE_OXIMETER_WRIST_WORN 3138 /**< Wrist Worn(Pulse Oximeter subtype). */ +#define BLE_APPEARANCE_GENERIC_WEIGHT_SCALE 3200 /**< Generic Weight Scale. */ +#define BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS_ACT 5184 /**< Generic Outdoor Sports Activity. */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_DISP 5185 /**< Location Display Device (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_DISP \ + 5186 /**< Location and Navigation Display Device (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_POD 5187 /**< Location Pod (Outdoor Sports Activity subtype). */ +#define BLE_APPEARANCE_OUTDOOR_SPORTS_ACT_LOC_AND_NAV_POD \ + 5188 /**< Location and Navigation Pod (Outdoor Sports Activity subtype). */ /** @} */ /** @brief Set .type and .uuid fields of ble_uuid_struct to specified UUID value. */ -#define BLE_UUID_BLE_ASSIGN(instance, value) \ - do { \ - instance.type = BLE_UUID_TYPE_BLE; \ - instance.uuid = value; \ - } while (0) +#define BLE_UUID_BLE_ASSIGN(instance, value) \ + do { \ + instance.type = BLE_UUID_TYPE_BLE; \ + instance.uuid = value; \ + } while (0) /** @brief Copy type and uuid members from src to dst ble_uuid_t pointer. Both pointers must be valid/non-null. */ -#define BLE_UUID_COPY_PTR(dst, src) \ - do { \ - (dst)->type = (src)->type; \ - (dst)->uuid = (src)->uuid; \ - } while (0) +#define BLE_UUID_COPY_PTR(dst, src) \ + do { \ + (dst)->type = (src)->type; \ + (dst)->uuid = (src)->uuid; \ + } while (0) /** @brief Copy type and uuid members from src to dst ble_uuid_t struct. */ -#define BLE_UUID_COPY_INST(dst, src) \ - do { \ - (dst).type = (src).type; \ - (dst).uuid = (src).uuid; \ - } while (0) +#define BLE_UUID_COPY_INST(dst, src) \ + do { \ + (dst).type = (src).type; \ + (dst).uuid = (src).uuid; \ + } while (0) /** @brief Compare for equality both type and uuid members of two (valid, non-null) ble_uuid_t pointers. */ #define BLE_UUID_EQ(p_uuid1, p_uuid2) (((p_uuid1)->type == (p_uuid2)->type) && ((p_uuid1)->uuid == (p_uuid2)->uuid)) @@ -186,20 +188,20 @@ extern "C" { /** @brief 128 bit UUID values. */ typedef struct { - uint8_t uuid128[16]; /**< Little-Endian UUID bytes. */ + uint8_t uuid128[16]; /**< Little-Endian UUID bytes. */ } ble_uuid128_t; /** @brief Bluetooth Low Energy UUID type, encapsulates both 16-bit and 128-bit UUIDs. */ typedef struct { - uint16_t uuid; /**< 16-bit UUID value or octets 12-13 of 128-bit UUID. */ - uint8_t type; /**< UUID type, see @ref BLE_UUID_TYPES. If type is @ref BLE_UUID_TYPE_UNKNOWN, the value of uuid is - undefined. */ + uint16_t uuid; /**< 16-bit UUID value or octets 12-13 of 128-bit UUID. */ + uint8_t + type; /**< UUID type, see @ref BLE_UUID_TYPES. If type is @ref BLE_UUID_TYPE_UNKNOWN, the value of uuid is undefined. */ } ble_uuid_t; /**@brief Data structure. */ typedef struct { - uint8_t *p_data; /**< Pointer to the data buffer provided to/from the application. */ - uint16_t len; /**< Length of the data buffer, in bytes. */ + uint8_t *p_data; /**< Pointer to the data buffer provided to/from the application. */ + uint16_t len; /**< Length of the data buffer, in bytes. */ } ble_data_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h b/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h index baf7f7189..4e0bd752a 100644 --- a/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h +++ b/src/platform/nrf52/softdevice/nrf52/nrf_mbr.h @@ -86,21 +86,21 @@ This is the offset where the first byte of the SoftDevice hex file is written. * /**@brief nRF Master Boot Record API SVC numbers. */ enum NRF_MBR_SVCS { - SD_MBR_COMMAND = MBR_SVC_BASE, /**< ::sd_mbr_command */ + SD_MBR_COMMAND = MBR_SVC_BASE, /**< ::sd_mbr_command */ }; /**@brief Possible values for ::sd_mbr_command_t.command */ enum NRF_MBR_COMMANDS { - SD_MBR_COMMAND_COPY_BL, /**< Copy a new BootLoader. @see ::sd_mbr_command_copy_bl_t*/ - SD_MBR_COMMAND_COPY_SD, /**< Copy a new SoftDevice. @see ::sd_mbr_command_copy_sd_t*/ - SD_MBR_COMMAND_INIT_SD, /**< Initialize forwarding interrupts to SD, and run reset function in SD. Does not require - any parameters in ::sd_mbr_command_t params.*/ - SD_MBR_COMMAND_COMPARE, /**< This command works like memcmp. @see ::sd_mbr_command_compare_t*/ - SD_MBR_COMMAND_VECTOR_TABLE_BASE_SET, /**< Change the address the MBR starts after a reset. @see - ::sd_mbr_command_vector_table_base_set_t*/ - SD_MBR_COMMAND_RESERVED, - SD_MBR_COMMAND_IRQ_FORWARD_ADDRESS_SET, /**< Start forwarding all interrupts to this address. @see - ::sd_mbr_command_irq_forward_address_set_t*/ + SD_MBR_COMMAND_COPY_BL, /**< Copy a new BootLoader. @see ::sd_mbr_command_copy_bl_t*/ + SD_MBR_COMMAND_COPY_SD, /**< Copy a new SoftDevice. @see ::sd_mbr_command_copy_sd_t*/ + SD_MBR_COMMAND_INIT_SD, /**< Initialize forwarding interrupts to SD, and run reset function in SD. Does not require any + parameters in ::sd_mbr_command_t params.*/ + SD_MBR_COMMAND_COMPARE, /**< This command works like memcmp. @see ::sd_mbr_command_compare_t*/ + SD_MBR_COMMAND_VECTOR_TABLE_BASE_SET, /**< Change the address the MBR starts after a reset. @see + ::sd_mbr_command_vector_table_base_set_t*/ + SD_MBR_COMMAND_RESERVED, + SD_MBR_COMMAND_IRQ_FORWARD_ADDRESS_SET, /**< Start forwarding all interrupts to this address. @see + ::sd_mbr_command_irq_forward_address_set_t*/ }; /** @} */ @@ -117,13 +117,12 @@ enum NRF_MBR_COMMANDS { * The user of this function is responsible for setting the BPROT registers. * * @retval ::NRF_SUCCESS indicates that the contents of the memory blocks where copied correctly. - * @retval ::NRF_ERROR_INTERNAL indicates that the contents of the memory blocks where not verified correctly after - * copying. + * @retval ::NRF_ERROR_INTERNAL indicates that the contents of the memory blocks where not verified correctly after copying. */ typedef struct { - uint32_t *src; /**< Pointer to the source of data to be copied.*/ - uint32_t *dst; /**< Pointer to the destination where the content is to be copied.*/ - uint32_t len; /**< Number of 32 bit words to copy. Must be a multiple of @ref MBR_PAGE_SIZE_IN_WORDS words.*/ + uint32_t *src; /**< Pointer to the source of data to be copied.*/ + uint32_t *dst; /**< Pointer to the destination where the content is to be copied.*/ + uint32_t len; /**< Number of 32 bit words to copy. Must be a multiple of @ref MBR_PAGE_SIZE_IN_WORDS words.*/ } sd_mbr_command_copy_sd_t; /**@brief This command works like memcmp, but takes the length in words. @@ -132,9 +131,9 @@ typedef struct { * @retval ::NRF_ERROR_NULL indicates that the contents of the memory blocks are not equal. */ typedef struct { - uint32_t *ptr1; /**< Pointer to block of memory. */ - uint32_t *ptr2; /**< Pointer to block of memory. */ - uint32_t len; /**< Number of 32 bit words to compare.*/ + uint32_t *ptr1; /**< Pointer to block of memory. */ + uint32_t *ptr2; /**< Pointer to block of memory. */ + uint32_t len; /**< Number of 32 bit words to compare.*/ } sd_mbr_command_compare_t; /**@brief This command copies a new BootLoader. @@ -160,8 +159,8 @@ typedef struct { * @retval ::NRF_ERROR_NO_MEM No MBR parameter page is provided. See @ref sd_mbr_command. */ typedef struct { - uint32_t *bl_src; /**< Pointer to the source of the bootloader to be be copied.*/ - uint32_t bl_len; /**< Number of 32 bit words to copy for BootLoader. */ + uint32_t *bl_src; /**< Pointer to the source of the bootloader to be be copied.*/ + uint32_t bl_len; /**< Number of 32 bit words to copy for BootLoader. */ } sd_mbr_command_copy_bl_t; /**@brief Change the address the MBR starts after a reset @@ -187,7 +186,7 @@ typedef struct { * @retval ::NRF_ERROR_NO_MEM No MBR parameter page is provided. See @ref sd_mbr_command. */ typedef struct { - uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ + uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ } sd_mbr_command_vector_table_base_set_t; /**@brief Sets the base address of the interrupt vector table for interrupts forwarded from the MBR @@ -198,7 +197,7 @@ typedef struct { * @retval ::NRF_SUCCESS */ typedef struct { - uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ + uint32_t address; /**< The base address of the interrupt vector table for forwarded interrupts.*/ } sd_mbr_command_irq_forward_address_set_t; /**@brief Input structure containing data used when calling ::sd_mbr_command @@ -208,14 +207,14 @@ typedef struct { * @ref SD_MBR_COMMAND_INIT_SD is set, it is not necessary to set any values under params. */ typedef struct { - uint32_t command; /**< Type of command to be issued. See @ref NRF_MBR_COMMANDS. */ - union { - sd_mbr_command_copy_sd_t copy_sd; /**< Parameters for copy SoftDevice.*/ - sd_mbr_command_compare_t compare; /**< Parameters for verify.*/ - sd_mbr_command_copy_bl_t copy_bl; /**< Parameters for copy BootLoader. Requires parameter page. */ - sd_mbr_command_vector_table_base_set_t base_set; /**< Parameters for vector table base set. Requires parameter page.*/ - sd_mbr_command_irq_forward_address_set_t irq_forward_address_set; /**< Parameters for irq forward address set*/ - } params; /**< Command parameters. */ + uint32_t command; /**< Type of command to be issued. See @ref NRF_MBR_COMMANDS. */ + union { + sd_mbr_command_copy_sd_t copy_sd; /**< Parameters for copy SoftDevice.*/ + sd_mbr_command_compare_t compare; /**< Parameters for verify.*/ + sd_mbr_command_copy_bl_t copy_bl; /**< Parameters for copy BootLoader. Requires parameter page. */ + sd_mbr_command_vector_table_base_set_t base_set; /**< Parameters for vector table base set. Requires parameter page.*/ + sd_mbr_command_irq_forward_address_set_t irq_forward_address_set; /**< Parameters for irq forward address set*/ + } params; /**< Command parameters. */ } sd_mbr_command_t; /** @} */ diff --git a/src/platform/nrf52/softdevice/nrf_error_sdm.h b/src/platform/nrf52/softdevice/nrf_error_sdm.h index a074b2cc0..2fd621057 100644 --- a/src/platform/nrf52/softdevice/nrf_error_sdm.h +++ b/src/platform/nrf52/softdevice/nrf_error_sdm.h @@ -56,10 +56,11 @@ extern "C" { #endif #define NRF_ERROR_SDM_LFCLK_SOURCE_UNKNOWN (NRF_ERROR_SDM_BASE_NUM + 0) ///< Unknown LFCLK source. -#define NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION \ - (NRF_ERROR_SDM_BASE_NUM + 1) ///< Incorrect interrupt configuration (can be caused by using illegal priority levels, - ///< or having enabled SoftDevice interrupts). -#define NRF_ERROR_SDM_INCORRECT_CLENR0 (NRF_ERROR_SDM_BASE_NUM + 2) ///< Incorrect CLENR0 (can be caused by erroneous SoftDevice flashing). +#define NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION \ + (NRF_ERROR_SDM_BASE_NUM + 1) ///< Incorrect interrupt configuration (can be caused by using illegal priority levels, or having + ///< enabled SoftDevice interrupts). +#define NRF_ERROR_SDM_INCORRECT_CLENR0 \ + (NRF_ERROR_SDM_BASE_NUM + 2) ///< Incorrect CLENR0 (can be caused by erroneous SoftDevice flashing). #ifdef __cplusplus } diff --git a/src/platform/nrf52/softdevice/nrf_nvic.h b/src/platform/nrf52/softdevice/nrf_nvic.h index ec6557e0e..d4ab204d9 100644 --- a/src/platform/nrf52/softdevice/nrf_nvic.h +++ b/src/platform/nrf52/softdevice/nrf_nvic.h @@ -71,26 +71,27 @@ extern "C" { /**@defgroup NRF_NVIC_ISER_DEFINES SoftDevice NVIC internal definitions * @{ */ -#define __NRF_NVIC_NVMC_IRQn \ - (30) /**< The peripheral ID of the NVMC. IRQ numbers are used to identify peripherals, but the NVMC doesn't have an \ - IRQ number in the MDK. */ +#define __NRF_NVIC_NVMC_IRQn \ + (30) /**< The peripheral ID of the NVMC. IRQ numbers are used to identify peripherals, but the NVMC doesn't have an IRQ \ + number in the MDK. */ #define __NRF_NVIC_ISER_COUNT (2) /**< The number of ISER/ICER registers in the NVIC that are used. */ /**@brief Interrupt priority levels used by the SoftDevice. */ -#define __NRF_NVIC_SD_IRQ_PRIOS \ - ((uint8_t)((1U << 0) /**< Priority level high .*/ \ - | (1U << 1) /**< Priority level medium. */ \ - | (1U << 4) /**< Priority level low. */ \ - )) +#define __NRF_NVIC_SD_IRQ_PRIOS \ + ((uint8_t)((1U << 0) /**< Priority level high .*/ \ + | (1U << 1) /**< Priority level medium. */ \ + | (1U << 4) /**< Priority level low. */ \ + )) /**@brief Interrupt priority levels available to the application. */ #define __NRF_NVIC_APP_IRQ_PRIOS ((uint8_t)~__NRF_NVIC_SD_IRQ_PRIOS) /**@brief Interrupts used by the SoftDevice, with IRQn in the range 0-31. */ -#define __NRF_NVIC_SD_IRQS_0 \ - ((uint32_t)((1U << POWER_CLOCK_IRQn) | (1U << RADIO_IRQn) | (1U << RTC0_IRQn) | (1U << TIMER0_IRQn) | (1U << RNG_IRQn) | (1U << ECB_IRQn) | \ - (1U << CCM_AAR_IRQn) | (1U << TEMP_IRQn) | (1U << __NRF_NVIC_NVMC_IRQn) | (1U << (uint32_t)SWI5_IRQn))) +#define __NRF_NVIC_SD_IRQS_0 \ + ((uint32_t)((1U << POWER_CLOCK_IRQn) | (1U << RADIO_IRQn) | (1U << RTC0_IRQn) | (1U << TIMER0_IRQn) | (1U << RNG_IRQn) | \ + (1U << ECB_IRQn) | (1U << CCM_AAR_IRQn) | (1U << TEMP_IRQn) | (1U << __NRF_NVIC_NVMC_IRQn) | \ + (1U << (uint32_t)SWI5_IRQn))) /**@brief Interrupts used by the SoftDevice, with IRQn in the range 32-63. */ #define __NRF_NVIC_SD_IRQS_1 ((uint32_t)0) @@ -110,8 +111,8 @@ extern "C" { /**@brief Type representing the state struct for the SoftDevice NVIC module. */ typedef struct { - uint32_t volatile __irq_masks[__NRF_NVIC_ISER_COUNT]; /**< IRQs enabled by the application in the NVIC. */ - uint32_t volatile __cr_flag; /**< Non-zero if already in a critical region */ + uint32_t volatile __irq_masks[__NRF_NVIC_ISER_COUNT]; /**< IRQs enabled by the application in the NVIC. */ + uint32_t volatile __cr_flag; /**< Non-zero if already in a critical region */ } nrf_nvic_state_t; /**@brief Variable keeping the state for the SoftDevice NVIC module. This must be declared in an @@ -161,8 +162,7 @@ __STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority) * * @retval ::NRF_SUCCESS The interrupt was enabled. * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE The interrupt is not available for the application. - * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt has a priority not available for the - * application. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt has a priority not available for the application. */ __STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn); @@ -226,8 +226,7 @@ __STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn); * * @retval ::NRF_SUCCESS The interrupt and priority level is available for the application. * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE IRQn is not available for the application. - * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt priority is not available for the - * application. + * @retval ::NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED The interrupt priority is not available for the application. */ __STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority); @@ -254,8 +253,8 @@ __STATIC_INLINE uint32_t sd_nvic_SystemReset(void); /**@brief Enter critical region. * * @post Application interrupts will be disabled. - * @note sd_nvic_critical_region_enter() and ::sd_nvic_critical_region_exit() must be called in matching pairs inside - * each execution context + * @note sd_nvic_critical_region_enter() and ::sd_nvic_critical_region_exit() must be called in matching pairs inside each + * execution context * @sa sd_nvic_critical_region_exit * * @param[out] p_is_nested_critical_region If 1, the application is now in a nested critical region. @@ -281,145 +280,162 @@ __STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical #ifndef SUPPRESS_INLINE_IMPLEMENTATION -__STATIC_INLINE int __sd_nvic_irq_disable(void) { - int pm = __get_PRIMASK(); - __disable_irq(); - return pm; +__STATIC_INLINE int __sd_nvic_irq_disable(void) +{ + int pm = __get_PRIMASK(); + __disable_irq(); + return pm; } -__STATIC_INLINE void __sd_nvic_irq_enable(void) { __enable_irq(); } - -__STATIC_INLINE uint32_t __sd_nvic_app_accessible_irq(IRQn_Type IRQn) { - if (IRQn < 32) { - return ((1UL << IRQn) & __NRF_NVIC_APP_IRQS_0) != 0; - } else if (IRQn < 64) { - return ((1UL << (IRQn - 32)) & __NRF_NVIC_APP_IRQS_1) != 0; - } else { - return 1; - } +__STATIC_INLINE void __sd_nvic_irq_enable(void) +{ + __enable_irq(); } -__STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority) { - if ((priority >= (1 << __NVIC_PRIO_BITS)) || (((1 << priority) & __NRF_NVIC_APP_IRQ_PRIOS) == 0)) { - return 0; - } - return 1; -} - -__STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn) { - if (!__sd_nvic_app_accessible_irq(IRQn)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } - if (!__sd_nvic_is_app_accessible_priority(NVIC_GetPriority(IRQn))) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; - } - - if (nrf_nvic_state.__cr_flag) { - nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] |= (uint32_t)(1 << ((uint32_t)((int32_t)IRQn) & (uint32_t)0x1F)); - } else { - NVIC_EnableIRQ(IRQn); - } - return NRF_SUCCESS; -} - -__STATIC_INLINE uint32_t sd_nvic_DisableIRQ(IRQn_Type IRQn) { - if (!__sd_nvic_app_accessible_irq(IRQn)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } - - if (nrf_nvic_state.__cr_flag) { - nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] &= ~(1UL << ((uint32_t)(IRQn)&0x1F)); - } else { - NVIC_DisableIRQ(IRQn); - } - - return NRF_SUCCESS; -} - -__STATIC_INLINE uint32_t sd_nvic_GetPendingIRQ(IRQn_Type IRQn, uint32_t *p_pending_irq) { - if (__sd_nvic_app_accessible_irq(IRQn)) { - *p_pending_irq = NVIC_GetPendingIRQ(IRQn); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_SetPendingIRQ(IRQn_Type IRQn) { - if (__sd_nvic_app_accessible_irq(IRQn)) { - NVIC_SetPendingIRQ(IRQn); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn) { - if (__sd_nvic_app_accessible_irq(IRQn)) { - NVIC_ClearPendingIRQ(IRQn); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority) { - if (!__sd_nvic_app_accessible_irq(IRQn)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } - - if (!__sd_nvic_is_app_accessible_priority(priority)) { - return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; - } - - NVIC_SetPriority(IRQn, (uint32_t)priority); - return NRF_SUCCESS; -} - -__STATIC_INLINE uint32_t sd_nvic_GetPriority(IRQn_Type IRQn, uint32_t *p_priority) { - if (__sd_nvic_app_accessible_irq(IRQn)) { - *p_priority = (NVIC_GetPriority(IRQn) & 0xFF); - return NRF_SUCCESS; - } else { - return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; - } -} - -__STATIC_INLINE uint32_t sd_nvic_SystemReset(void) { - NVIC_SystemReset(); - return NRF_ERROR_SOC_NVIC_SHOULD_NOT_RETURN; -} - -__STATIC_INLINE uint32_t sd_nvic_critical_region_enter(uint8_t *p_is_nested_critical_region) { - int was_masked = __sd_nvic_irq_disable(); - if (!nrf_nvic_state.__cr_flag) { - nrf_nvic_state.__cr_flag = 1; - nrf_nvic_state.__irq_masks[0] = (NVIC->ICER[0] & __NRF_NVIC_APP_IRQS_0); - NVIC->ICER[0] = __NRF_NVIC_APP_IRQS_0; - nrf_nvic_state.__irq_masks[1] = (NVIC->ICER[1] & __NRF_NVIC_APP_IRQS_1); - NVIC->ICER[1] = __NRF_NVIC_APP_IRQS_1; - *p_is_nested_critical_region = 0; - } else { - *p_is_nested_critical_region = 1; - } - if (!was_masked) { - __sd_nvic_irq_enable(); - } - return NRF_SUCCESS; -} - -__STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical_region) { - if (nrf_nvic_state.__cr_flag && (is_nested_critical_region == 0)) { - int was_masked = __sd_nvic_irq_disable(); - NVIC->ISER[0] = nrf_nvic_state.__irq_masks[0]; - NVIC->ISER[1] = nrf_nvic_state.__irq_masks[1]; - nrf_nvic_state.__cr_flag = 0; - if (!was_masked) { - __sd_nvic_irq_enable(); +__STATIC_INLINE uint32_t __sd_nvic_app_accessible_irq(IRQn_Type IRQn) +{ + if (IRQn < 32) { + return ((1UL << IRQn) & __NRF_NVIC_APP_IRQS_0) != 0; + } else if (IRQn < 64) { + return ((1UL << (IRQn - 32)) & __NRF_NVIC_APP_IRQS_1) != 0; + } else { + return 1; } - } +} - return NRF_SUCCESS; +__STATIC_INLINE uint32_t __sd_nvic_is_app_accessible_priority(uint32_t priority) +{ + if ((priority >= (1 << __NVIC_PRIO_BITS)) || (((1 << priority) & __NRF_NVIC_APP_IRQ_PRIOS) == 0)) { + return 0; + } + return 1; +} + +__STATIC_INLINE uint32_t sd_nvic_EnableIRQ(IRQn_Type IRQn) +{ + if (!__sd_nvic_app_accessible_irq(IRQn)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } + if (!__sd_nvic_is_app_accessible_priority(NVIC_GetPriority(IRQn))) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; + } + + if (nrf_nvic_state.__cr_flag) { + nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] |= + (uint32_t)(1 << ((uint32_t)((int32_t)IRQn) & (uint32_t)0x1F)); + } else { + NVIC_EnableIRQ(IRQn); + } + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_DisableIRQ(IRQn_Type IRQn) +{ + if (!__sd_nvic_app_accessible_irq(IRQn)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } + + if (nrf_nvic_state.__cr_flag) { + nrf_nvic_state.__irq_masks[(uint32_t)((int32_t)IRQn) >> 5] &= ~(1UL << ((uint32_t)(IRQn)&0x1F)); + } else { + NVIC_DisableIRQ(IRQn); + } + + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_GetPendingIRQ(IRQn_Type IRQn, uint32_t *p_pending_irq) +{ + if (__sd_nvic_app_accessible_irq(IRQn)) { + *p_pending_irq = NVIC_GetPendingIRQ(IRQn); + return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } +} + +__STATIC_INLINE uint32_t sd_nvic_SetPendingIRQ(IRQn_Type IRQn) +{ + if (__sd_nvic_app_accessible_irq(IRQn)) { + NVIC_SetPendingIRQ(IRQn); + return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } +} + +__STATIC_INLINE uint32_t sd_nvic_ClearPendingIRQ(IRQn_Type IRQn) +{ + if (__sd_nvic_app_accessible_irq(IRQn)) { + NVIC_ClearPendingIRQ(IRQn); + return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } +} + +__STATIC_INLINE uint32_t sd_nvic_SetPriority(IRQn_Type IRQn, uint32_t priority) +{ + if (!__sd_nvic_app_accessible_irq(IRQn)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } + + if (!__sd_nvic_is_app_accessible_priority(priority)) { + return NRF_ERROR_SOC_NVIC_INTERRUPT_PRIORITY_NOT_ALLOWED; + } + + NVIC_SetPriority(IRQn, (uint32_t)priority); + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_GetPriority(IRQn_Type IRQn, uint32_t *p_priority) +{ + if (__sd_nvic_app_accessible_irq(IRQn)) { + *p_priority = (NVIC_GetPriority(IRQn) & 0xFF); + return NRF_SUCCESS; + } else { + return NRF_ERROR_SOC_NVIC_INTERRUPT_NOT_AVAILABLE; + } +} + +__STATIC_INLINE uint32_t sd_nvic_SystemReset(void) +{ + NVIC_SystemReset(); + return NRF_ERROR_SOC_NVIC_SHOULD_NOT_RETURN; +} + +__STATIC_INLINE uint32_t sd_nvic_critical_region_enter(uint8_t *p_is_nested_critical_region) +{ + int was_masked = __sd_nvic_irq_disable(); + if (!nrf_nvic_state.__cr_flag) { + nrf_nvic_state.__cr_flag = 1; + nrf_nvic_state.__irq_masks[0] = (NVIC->ICER[0] & __NRF_NVIC_APP_IRQS_0); + NVIC->ICER[0] = __NRF_NVIC_APP_IRQS_0; + nrf_nvic_state.__irq_masks[1] = (NVIC->ICER[1] & __NRF_NVIC_APP_IRQS_1); + NVIC->ICER[1] = __NRF_NVIC_APP_IRQS_1; + *p_is_nested_critical_region = 0; + } else { + *p_is_nested_critical_region = 1; + } + if (!was_masked) { + __sd_nvic_irq_enable(); + } + return NRF_SUCCESS; +} + +__STATIC_INLINE uint32_t sd_nvic_critical_region_exit(uint8_t is_nested_critical_region) +{ + if (nrf_nvic_state.__cr_flag && (is_nested_critical_region == 0)) { + int was_masked = __sd_nvic_irq_disable(); + NVIC->ISER[0] = nrf_nvic_state.__irq_masks[0]; + NVIC->ISER[1] = nrf_nvic_state.__irq_masks[1]; + nrf_nvic_state.__cr_flag = 0; + if (!was_masked) { + __sd_nvic_irq_enable(); + } + } + + return NRF_SUCCESS; } #endif /* SUPPRESS_INLINE_IMPLEMENTATION */ diff --git a/src/platform/nrf52/softdevice/nrf_sdm.h b/src/platform/nrf52/softdevice/nrf_sdm.h index 369141063..02bf135b4 100644 --- a/src/platform/nrf52/softdevice/nrf_sdm.h +++ b/src/platform/nrf52/softdevice/nrf_sdm.h @@ -151,23 +151,26 @@ the start of the SoftDevice (without MBR)*/ /** @brief Defines a macro for retrieving the actual SoftDevice ID from a given base address. Use * @ref MBR_SIZE as the argument when the SoftDevice is installed just above the MBR (the * usual case). */ -#define SD_ID_GET(baseaddr) \ - ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) ? (*((uint32_t *)((baseaddr) + SD_ID_OFFSET))) \ - : SDM_INFO_FIELD_INVALID) +#define SD_ID_GET(baseaddr) \ + ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) \ + ? (*((uint32_t *)((baseaddr) + SD_ID_OFFSET))) \ + : SDM_INFO_FIELD_INVALID) /** @brief Defines a macro for retrieving the actual SoftDevice version from a given base address. * Use @ref MBR_SIZE as the argument when the SoftDevice is installed just above the MBR * (the usual case). */ -#define SD_VERSION_GET(baseaddr) \ - ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) ? (*((uint32_t *)((baseaddr) + SD_VERSION_OFFSET))) \ - : SDM_INFO_FIELD_INVALID) +#define SD_VERSION_GET(baseaddr) \ + ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) \ + ? (*((uint32_t *)((baseaddr) + SD_VERSION_OFFSET))) \ + : SDM_INFO_FIELD_INVALID) /** @brief Defines a macro for retrieving the address of SoftDevice unique str based on a given base address. * Use @ref MBR_SIZE as the argument when the SoftDevice is installed just above the MBR * (the usual case). */ -#define SD_UNIQUE_STR_ADDR_GET(baseaddr) \ - ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_UNIQUE_STR_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) ? (((uint8_t *)((baseaddr) + SD_UNIQUE_STR_OFFSET))) \ - : SDM_INFO_FIELD_INVALID) +#define SD_UNIQUE_STR_ADDR_GET(baseaddr) \ + ((SD_INFO_STRUCT_SIZE_GET(baseaddr) > (SD_UNIQUE_STR_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) \ + ? (((uint8_t *)((baseaddr) + SD_UNIQUE_STR_OFFSET))) \ + : SDM_INFO_FIELD_INVALID) /**@defgroup NRF_FAULT_ID_RANGES Fault ID ranges * @{ */ @@ -177,13 +180,14 @@ the start of the SoftDevice (without MBR)*/ /**@defgroup NRF_FAULT_IDS Fault ID types * @{ */ -#define NRF_FAULT_ID_SD_ASSERT (NRF_FAULT_ID_SD_RANGE_START + 1) /**< SoftDevice assertion. The info parameter is reserved for future used. */ -#define NRF_FAULT_ID_APP_MEMACC \ - (NRF_FAULT_ID_APP_RANGE_START + 1) /**< Application invalid memory access. The info parameter will contain \ - 0x00000000, in case of SoftDevice RAM access violation. In case of SoftDevice \ - peripheral register violation the info parameter will contain the sub-region \ - number of PREGION[0], on whose address range the disallowed write access \ - caused the memory access fault. */ +#define NRF_FAULT_ID_SD_ASSERT \ + (NRF_FAULT_ID_SD_RANGE_START + 1) /**< SoftDevice assertion. The info parameter is reserved for future used. */ +#define NRF_FAULT_ID_APP_MEMACC \ + (NRF_FAULT_ID_APP_RANGE_START + 1) /**< Application invalid memory access. The info parameter will contain 0x00000000, \ + in case of SoftDevice RAM access violation. In case of SoftDevice peripheral \ + register violation the info parameter will contain the sub-region number of \ + PREGION[0], on whose address range the disallowed write access caused the \ + memory access fault. */ /**@} */ /** @} */ @@ -193,11 +197,11 @@ the start of the SoftDevice (without MBR)*/ /**@brief nRF SoftDevice Manager API SVC numbers. */ enum NRF_SD_SVCS { - SD_SOFTDEVICE_ENABLE = SDM_SVC_BASE, /**< ::sd_softdevice_enable */ - SD_SOFTDEVICE_DISABLE, /**< ::sd_softdevice_disable */ - SD_SOFTDEVICE_IS_ENABLED, /**< ::sd_softdevice_is_enabled */ - SD_SOFTDEVICE_VECTOR_TABLE_BASE_SET, /**< ::sd_softdevice_vector_table_base_set */ - SVC_SDM_LAST /**< Placeholder for last SDM SVC */ + SD_SOFTDEVICE_ENABLE = SDM_SVC_BASE, /**< ::sd_softdevice_enable */ + SD_SOFTDEVICE_DISABLE, /**< ::sd_softdevice_disable */ + SD_SOFTDEVICE_IS_ENABLED, /**< ::sd_softdevice_is_enabled */ + SD_SOFTDEVICE_VECTOR_TABLE_BASE_SET, /**< ::sd_softdevice_vector_table_base_set */ + SVC_SDM_LAST /**< Placeholder for last SDM SVC */ }; /** @} */ @@ -239,34 +243,34 @@ enum NRF_SD_SVCS { /**@brief Type representing LFCLK oscillator source. */ typedef struct { - uint8_t source; /**< LF oscillator clock source, see @ref NRF_CLOCK_LF_SRC. */ - uint8_t rc_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: Calibration timer interval in 1/4 second - units (nRF52: 1-32). - @note To avoid excessive clock drift, 0.5 degrees Celsius is the - maximum temperature change allowed in one calibration timer - interval. The interval should be selected to ensure this. + uint8_t source; /**< LF oscillator clock source, see @ref NRF_CLOCK_LF_SRC. */ + uint8_t rc_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: Calibration timer interval in 1/4 second + units (nRF52: 1-32). + @note To avoid excessive clock drift, 0.5 degrees Celsius is the + maximum temperature change allowed in one calibration timer + interval. The interval should be selected to ensure this. - @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. */ - uint8_t rc_temp_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: How often (in number of calibration - intervals) the RC oscillator shall be calibrated if the temperature - hasn't changed. - 0: Always calibrate even if the temperature hasn't changed. - 1: Only calibrate if the temperature has changed (legacy - nRF51 only). - 2-33: Check the temperature and only calibrate if it has changed, - however calibration will take place every rc_temp_ctiv - intervals in any case. + @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. */ + uint8_t rc_temp_ctiv; /**< Only for ::NRF_CLOCK_LF_SRC_RC: How often (in number of calibration + intervals) the RC oscillator shall be calibrated if the temperature + hasn't changed. + 0: Always calibrate even if the temperature hasn't changed. + 1: Only calibrate if the temperature has changed (legacy - nRF51 only). + 2-33: Check the temperature and only calibrate if it has changed, + however calibration will take place every rc_temp_ctiv + intervals in any case. - @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. + @note Must be 0 if source is not ::NRF_CLOCK_LF_SRC_RC. - @note For nRF52, the application must ensure calibration at least once - every 8 seconds to ensure +/-500 ppm clock stability. The - recommended configuration for ::NRF_CLOCK_LF_SRC_RC on nRF52 is - rc_ctiv=16 and rc_temp_ctiv=2. This will ensure calibration at - least once every 8 seconds and for temperature changes of 0.5 - degrees Celsius every 4 seconds. See the Product Specification - for the nRF52 device being used for more information.*/ - uint8_t accuracy; /**< External clock accuracy used in the LL to compute timing - windows, see @ref NRF_CLOCK_LF_ACCURACY.*/ + @note For nRF52, the application must ensure calibration at least once + every 8 seconds to ensure +/-500 ppm clock stability. The + recommended configuration for ::NRF_CLOCK_LF_SRC_RC on nRF52 is + rc_ctiv=16 and rc_temp_ctiv=2. This will ensure calibration at + least once every 8 seconds and for temperature changes of 0.5 + degrees Celsius every 4 seconds. See the Product Specification + for the nRF52 device being used for more information.*/ + uint8_t accuracy; /**< External clock accuracy used in the LL to compute timing + windows, see @ref NRF_CLOCK_LF_ACCURACY.*/ } nrf_clock_lf_cfg_t; /**@brief Fault Handler type. @@ -286,9 +290,9 @@ typedef struct { * @param[in] pc The program counter of the instruction that triggered the fault. * @param[in] info Optional additional information regarding the fault. Refer to each Fault identifier for details. * - * @note When id is set to @ref NRF_FAULT_ID_APP_MEMACC, pc will contain the address of the instruction being executed - * at the time when the fault is detected by the CPU. The CPU program counter may have advanced up to 2 instructions (no - * branching) after the one that triggered the fault. + * @note When id is set to @ref NRF_FAULT_ID_APP_MEMACC, pc will contain the address of the instruction being executed at the time + * when the fault is detected by the CPU. The CPU program counter may have advanced up to 2 instructions (no branching) after the + * one that triggered the fault. */ typedef void (*nrf_fault_handler_t)(uint32_t id, uint32_t pc, uint32_t info); @@ -316,20 +320,20 @@ typedef void (*nrf_fault_handler_t)(uint32_t id, uint32_t pc, uint32_t info); * * @param p_clock_lf_cfg Low frequency clock source and accuracy. If NULL the clock will be configured as an RC source with rc_ctiv = 16 and .rc_temp_ctiv = 2 - In the case of XTAL source, the PPM accuracy of the chosen clock source must be greater than or - equal to the actual characteristics of your XTAL clock. + In the case of XTAL source, the PPM accuracy of the chosen clock source must be greater than or equal to + the actual characteristics of your XTAL clock. * @param fault_handler Callback to be invoked in case of fault, cannot be NULL. * * @retval ::NRF_SUCCESS * @retval ::NRF_ERROR_INVALID_ADDR Invalid or NULL pointer supplied. - * @retval ::NRF_ERROR_INVALID_STATE SoftDevice is already enabled, and the clock source and fault handler cannot be - updated. - * @retval ::NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION SoftDevice interrupt is already enabled, or an enabled - interrupt has an illegal priority level. + * @retval ::NRF_ERROR_INVALID_STATE SoftDevice is already enabled, and the clock source and fault handler cannot be updated. + * @retval ::NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION SoftDevice interrupt is already enabled, or an enabled interrupt has + an illegal priority level. * @retval ::NRF_ERROR_SDM_LFCLK_SOURCE_UNKNOWN Unknown low frequency clock source selected. * @retval ::NRF_ERROR_INVALID_PARAM Invalid clock source configuration supplied in p_clock_lf_cfg. */ -SVCALL(SD_SOFTDEVICE_ENABLE, uint32_t, sd_softdevice_enable(nrf_clock_lf_cfg_t const *p_clock_lf_cfg, nrf_fault_handler_t fault_handler)); +SVCALL(SD_SOFTDEVICE_ENABLE, uint32_t, + sd_softdevice_enable(nrf_clock_lf_cfg_t const *p_clock_lf_cfg, nrf_fault_handler_t fault_handler)); /**@brief Disables the SoftDevice and by extension the protocol stack. * diff --git a/src/platform/nrf52/softdevice/nrf_soc.h b/src/platform/nrf52/softdevice/nrf_soc.h index 4782aba58..c649ca836 100644 --- a/src/platform/nrf52/softdevice/nrf_soc.h +++ b/src/platform/nrf52/softdevice/nrf_soc.h @@ -81,32 +81,33 @@ extern "C" { #define SOC_ECB_CIPHERTEXT_LENGTH (SOC_ECB_CLEARTEXT_LENGTH) /**< ECB ciphertext length. */ #define SD_EVT_IRQn (SWI2_IRQn) /**< SoftDevice Event IRQ number. Used for both protocol events and SoC events. */ -#define SD_EVT_IRQHandler \ - (SWI2_IRQHandler) /**< SoftDevice Event IRQ handler. Used for both protocol events and SoC events. \ +#define SD_EVT_IRQHandler \ + (SWI2_IRQHandler) /**< SoftDevice Event IRQ handler. Used for both protocol events and SoC events. \ The default interrupt priority for this handler is set to 6 */ #define RADIO_NOTIFICATION_IRQn (SWI1_IRQn) /**< The radio notification IRQ number. */ -#define RADIO_NOTIFICATION_IRQHandler \ - (SWI1_IRQHandler) /**< The radio notification IRQ handler. \ +#define RADIO_NOTIFICATION_IRQHandler \ + (SWI1_IRQHandler) /**< The radio notification IRQ handler. \ The default interrupt priority for this handler is set to 6 */ #define NRF_RADIO_LENGTH_MIN_US (100) /**< The shortest allowed radio timeslot, in microseconds. */ #define NRF_RADIO_LENGTH_MAX_US (100000) /**< The longest allowed radio timeslot, in microseconds. */ -#define NRF_RADIO_DISTANCE_MAX_US \ - (128000000UL - 1UL) /**< The longest timeslot distance, in microseconds, allowed for the distance parameter (see \ - @ref nrf_radio_request_normal_t) in the request. */ +#define NRF_RADIO_DISTANCE_MAX_US \ + (128000000UL - 1UL) /**< The longest timeslot distance, in microseconds, allowed for the distance parameter (see @ref \ + nrf_radio_request_normal_t) in the request. */ -#define NRF_RADIO_EARLIEST_TIMEOUT_MAX_US \ - (128000000UL - 1UL) /**< The longest timeout, in microseconds, allowed when requesting the earliest possible timeslot. */ +#define NRF_RADIO_EARLIEST_TIMEOUT_MAX_US \ + (128000000UL - 1UL) /**< The longest timeout, in microseconds, allowed when requesting the earliest possible timeslot. */ -#define NRF_RADIO_START_JITTER_US (2) /**< The maximum jitter in @ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START relative to the requested start time. */ +#define NRF_RADIO_START_JITTER_US \ + (2) /**< The maximum jitter in @ref NRF_RADIO_CALLBACK_SIGNAL_TYPE_START relative to the requested start time. */ /**@brief Mask of PPI channels reserved by the SoftDevice when the SoftDevice is disabled. */ #define NRF_SOC_SD_PPI_CHANNELS_SD_DISABLED_MSK ((uint32_t)(0)) /**@brief Mask of PPI channels reserved by the SoftDevice when the SoftDevice is enabled. */ -#define NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK \ - ((uint32_t)((1U << 17) | (1U << 18) | (1U << 19) | (1U << 20) | (1U << 21) | (1U << 22) | (1U << 23) | (1U << 24) | (1U << 25) | (1U << 26) | \ - (1U << 27) | (1U << 28) | (1U << 29) | (1U << 30) | (1U << 31))) +#define NRF_SOC_SD_PPI_CHANNELS_SD_ENABLED_MSK \ + ((uint32_t)((1U << 17) | (1U << 18) | (1U << 19) | (1U << 20) | (1U << 21) | (1U << 22) | (1U << 23) | (1U << 24) | \ + (1U << 25) | (1U << 26) | (1U << 27) | (1U << 28) | (1U << 29) | (1U << 30) | (1U << 31))) /**@brief Mask of PPI groups reserved by the SoftDevice when the SoftDevice is disabled. */ #define NRF_SOC_SD_PPI_GROUPS_SD_DISABLED_MSK ((uint32_t)(0)) @@ -121,55 +122,55 @@ extern "C" { /**@brief The SVC numbers used by the SVC functions in the SoC library. */ enum NRF_SOC_SVCS { - SD_PPI_CHANNEL_ENABLE_GET = SOC_SVC_BASE, - SD_PPI_CHANNEL_ENABLE_SET = SOC_SVC_BASE + 1, - SD_PPI_CHANNEL_ENABLE_CLR = SOC_SVC_BASE + 2, - SD_PPI_CHANNEL_ASSIGN = SOC_SVC_BASE + 3, - SD_PPI_GROUP_TASK_ENABLE = SOC_SVC_BASE + 4, - SD_PPI_GROUP_TASK_DISABLE = SOC_SVC_BASE + 5, - SD_PPI_GROUP_ASSIGN = SOC_SVC_BASE + 6, - SD_PPI_GROUP_GET = SOC_SVC_BASE + 7, - SD_FLASH_PAGE_ERASE = SOC_SVC_BASE + 8, - SD_FLASH_WRITE = SOC_SVC_BASE + 9, - SD_PROTECTED_REGISTER_WRITE = SOC_SVC_BASE + 11, - SD_MUTEX_NEW = SOC_SVC_BASE_NOT_AVAILABLE, - SD_MUTEX_ACQUIRE = SOC_SVC_BASE_NOT_AVAILABLE + 1, - SD_MUTEX_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 2, - SD_RAND_APPLICATION_POOL_CAPACITY_GET = SOC_SVC_BASE_NOT_AVAILABLE + 3, - SD_RAND_APPLICATION_BYTES_AVAILABLE_GET = SOC_SVC_BASE_NOT_AVAILABLE + 4, - SD_RAND_APPLICATION_VECTOR_GET = SOC_SVC_BASE_NOT_AVAILABLE + 5, - SD_POWER_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 6, - SD_POWER_SYSTEM_OFF = SOC_SVC_BASE_NOT_AVAILABLE + 7, - SD_POWER_RESET_REASON_GET = SOC_SVC_BASE_NOT_AVAILABLE + 8, - SD_POWER_RESET_REASON_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 9, - SD_POWER_POF_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 10, - SD_POWER_POF_THRESHOLD_SET = SOC_SVC_BASE_NOT_AVAILABLE + 11, - SD_POWER_POF_THRESHOLDVDDH_SET = SOC_SVC_BASE_NOT_AVAILABLE + 12, - SD_POWER_RAM_POWER_SET = SOC_SVC_BASE_NOT_AVAILABLE + 13, - SD_POWER_RAM_POWER_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 14, - SD_POWER_RAM_POWER_GET = SOC_SVC_BASE_NOT_AVAILABLE + 15, - SD_POWER_GPREGRET_SET = SOC_SVC_BASE_NOT_AVAILABLE + 16, - SD_POWER_GPREGRET_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 17, - SD_POWER_GPREGRET_GET = SOC_SVC_BASE_NOT_AVAILABLE + 18, - SD_POWER_DCDC_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 19, - SD_POWER_DCDC0_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 20, - SD_APP_EVT_WAIT = SOC_SVC_BASE_NOT_AVAILABLE + 21, - SD_CLOCK_HFCLK_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 22, - SD_CLOCK_HFCLK_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 23, - SD_CLOCK_HFCLK_IS_RUNNING = SOC_SVC_BASE_NOT_AVAILABLE + 24, - SD_RADIO_NOTIFICATION_CFG_SET = SOC_SVC_BASE_NOT_AVAILABLE + 25, - SD_ECB_BLOCK_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 26, - SD_ECB_BLOCKS_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 27, - SD_RADIO_SESSION_OPEN = SOC_SVC_BASE_NOT_AVAILABLE + 28, - SD_RADIO_SESSION_CLOSE = SOC_SVC_BASE_NOT_AVAILABLE + 29, - SD_RADIO_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 30, - SD_EVT_GET = SOC_SVC_BASE_NOT_AVAILABLE + 31, - SD_TEMP_GET = SOC_SVC_BASE_NOT_AVAILABLE + 32, - SD_POWER_USBPWRRDY_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 33, - SD_POWER_USBDETECTED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 34, - SD_POWER_USBREMOVED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 35, - SD_POWER_USBREGSTATUS_GET = SOC_SVC_BASE_NOT_AVAILABLE + 36, - SVC_SOC_LAST = SOC_SVC_BASE_NOT_AVAILABLE + 37 + SD_PPI_CHANNEL_ENABLE_GET = SOC_SVC_BASE, + SD_PPI_CHANNEL_ENABLE_SET = SOC_SVC_BASE + 1, + SD_PPI_CHANNEL_ENABLE_CLR = SOC_SVC_BASE + 2, + SD_PPI_CHANNEL_ASSIGN = SOC_SVC_BASE + 3, + SD_PPI_GROUP_TASK_ENABLE = SOC_SVC_BASE + 4, + SD_PPI_GROUP_TASK_DISABLE = SOC_SVC_BASE + 5, + SD_PPI_GROUP_ASSIGN = SOC_SVC_BASE + 6, + SD_PPI_GROUP_GET = SOC_SVC_BASE + 7, + SD_FLASH_PAGE_ERASE = SOC_SVC_BASE + 8, + SD_FLASH_WRITE = SOC_SVC_BASE + 9, + SD_PROTECTED_REGISTER_WRITE = SOC_SVC_BASE + 11, + SD_MUTEX_NEW = SOC_SVC_BASE_NOT_AVAILABLE, + SD_MUTEX_ACQUIRE = SOC_SVC_BASE_NOT_AVAILABLE + 1, + SD_MUTEX_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 2, + SD_RAND_APPLICATION_POOL_CAPACITY_GET = SOC_SVC_BASE_NOT_AVAILABLE + 3, + SD_RAND_APPLICATION_BYTES_AVAILABLE_GET = SOC_SVC_BASE_NOT_AVAILABLE + 4, + SD_RAND_APPLICATION_VECTOR_GET = SOC_SVC_BASE_NOT_AVAILABLE + 5, + SD_POWER_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 6, + SD_POWER_SYSTEM_OFF = SOC_SVC_BASE_NOT_AVAILABLE + 7, + SD_POWER_RESET_REASON_GET = SOC_SVC_BASE_NOT_AVAILABLE + 8, + SD_POWER_RESET_REASON_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 9, + SD_POWER_POF_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 10, + SD_POWER_POF_THRESHOLD_SET = SOC_SVC_BASE_NOT_AVAILABLE + 11, + SD_POWER_POF_THRESHOLDVDDH_SET = SOC_SVC_BASE_NOT_AVAILABLE + 12, + SD_POWER_RAM_POWER_SET = SOC_SVC_BASE_NOT_AVAILABLE + 13, + SD_POWER_RAM_POWER_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 14, + SD_POWER_RAM_POWER_GET = SOC_SVC_BASE_NOT_AVAILABLE + 15, + SD_POWER_GPREGRET_SET = SOC_SVC_BASE_NOT_AVAILABLE + 16, + SD_POWER_GPREGRET_CLR = SOC_SVC_BASE_NOT_AVAILABLE + 17, + SD_POWER_GPREGRET_GET = SOC_SVC_BASE_NOT_AVAILABLE + 18, + SD_POWER_DCDC_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 19, + SD_POWER_DCDC0_MODE_SET = SOC_SVC_BASE_NOT_AVAILABLE + 20, + SD_APP_EVT_WAIT = SOC_SVC_BASE_NOT_AVAILABLE + 21, + SD_CLOCK_HFCLK_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 22, + SD_CLOCK_HFCLK_RELEASE = SOC_SVC_BASE_NOT_AVAILABLE + 23, + SD_CLOCK_HFCLK_IS_RUNNING = SOC_SVC_BASE_NOT_AVAILABLE + 24, + SD_RADIO_NOTIFICATION_CFG_SET = SOC_SVC_BASE_NOT_AVAILABLE + 25, + SD_ECB_BLOCK_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 26, + SD_ECB_BLOCKS_ENCRYPT = SOC_SVC_BASE_NOT_AVAILABLE + 27, + SD_RADIO_SESSION_OPEN = SOC_SVC_BASE_NOT_AVAILABLE + 28, + SD_RADIO_SESSION_CLOSE = SOC_SVC_BASE_NOT_AVAILABLE + 29, + SD_RADIO_REQUEST = SOC_SVC_BASE_NOT_AVAILABLE + 30, + SD_EVT_GET = SOC_SVC_BASE_NOT_AVAILABLE + 31, + SD_TEMP_GET = SOC_SVC_BASE_NOT_AVAILABLE + 32, + SD_POWER_USBPWRRDY_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 33, + SD_POWER_USBDETECTED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 34, + SD_POWER_USBREMOVED_ENABLE = SOC_SVC_BASE_NOT_AVAILABLE + 35, + SD_POWER_USBREGSTATUS_GET = SOC_SVC_BASE_NOT_AVAILABLE + 36, + SVC_SOC_LAST = SOC_SVC_BASE_NOT_AVAILABLE + 37 }; /**@brief Possible values of a ::nrf_mutex_t. */ @@ -177,80 +178,79 @@ enum NRF_MUTEX_VALUES { NRF_MUTEX_FREE, NRF_MUTEX_TAKEN }; /**@brief Power modes. */ enum NRF_POWER_MODES { - NRF_POWER_MODE_CONSTLAT, /**< Constant latency mode. See power management in the reference manual. */ - NRF_POWER_MODE_LOWPWR /**< Low power mode. See power management in the reference manual. */ + NRF_POWER_MODE_CONSTLAT, /**< Constant latency mode. See power management in the reference manual. */ + NRF_POWER_MODE_LOWPWR /**< Low power mode. See power management in the reference manual. */ }; /**@brief Power failure thresholds */ enum NRF_POWER_THRESHOLDS { - NRF_POWER_THRESHOLD_V17 = 4UL, /**< 1.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V18, /**< 1.8 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V19, /**< 1.9 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V20, /**< 2.0 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V21, /**< 2.1 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V22, /**< 2.2 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V23, /**< 2.3 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V24, /**< 2.4 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V25, /**< 2.5 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V26, /**< 2.6 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V27, /**< 2.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLD_V28 /**< 2.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V17 = 4UL, /**< 1.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V18, /**< 1.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V19, /**< 1.9 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V20, /**< 2.0 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V21, /**< 2.1 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V22, /**< 2.2 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V23, /**< 2.3 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V24, /**< 2.4 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V25, /**< 2.5 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V26, /**< 2.6 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V27, /**< 2.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLD_V28 /**< 2.8 Volts power failure threshold. */ }; /**@brief Power failure thresholds for high voltage */ enum NRF_POWER_THRESHOLDVDDHS { - NRF_POWER_THRESHOLDVDDH_V27, /**< 2.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V28, /**< 2.8 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V29, /**< 2.9 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V30, /**< 3.0 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V31, /**< 3.1 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V32, /**< 3.2 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V33, /**< 3.3 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V34, /**< 3.4 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V35, /**< 3.5 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V36, /**< 3.6 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V37, /**< 3.7 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V38, /**< 3.8 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V39, /**< 3.9 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V40, /**< 4.0 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V41, /**< 4.1 Volts power failure threshold. */ - NRF_POWER_THRESHOLDVDDH_V42 /**< 4.2 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V27, /**< 2.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V28, /**< 2.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V29, /**< 2.9 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V30, /**< 3.0 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V31, /**< 3.1 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V32, /**< 3.2 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V33, /**< 3.3 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V34, /**< 3.4 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V35, /**< 3.5 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V36, /**< 3.6 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V37, /**< 3.7 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V38, /**< 3.8 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V39, /**< 3.9 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V40, /**< 4.0 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V41, /**< 4.1 Volts power failure threshold. */ + NRF_POWER_THRESHOLDVDDH_V42 /**< 4.2 Volts power failure threshold. */ }; /**@brief DC/DC converter modes. */ enum NRF_POWER_DCDC_MODES { - NRF_POWER_DCDC_DISABLE, /**< The DCDC is disabled. */ - NRF_POWER_DCDC_ENABLE /**< The DCDC is enabled. */ + NRF_POWER_DCDC_DISABLE, /**< The DCDC is disabled. */ + NRF_POWER_DCDC_ENABLE /**< The DCDC is enabled. */ }; /**@brief Radio notification distances. */ enum NRF_RADIO_NOTIFICATION_DISTANCES { - NRF_RADIO_NOTIFICATION_DISTANCE_NONE = 0, /**< The event does not have a notification. */ - NRF_RADIO_NOTIFICATION_DISTANCE_800US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_1740US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_2680US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_3620US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_4560US, /**< The distance from the active notification to start of radio activity. */ - NRF_RADIO_NOTIFICATION_DISTANCE_5500US /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_NONE = 0, /**< The event does not have a notification. */ + NRF_RADIO_NOTIFICATION_DISTANCE_800US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_1740US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_2680US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_3620US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_4560US, /**< The distance from the active notification to start of radio activity. */ + NRF_RADIO_NOTIFICATION_DISTANCE_5500US /**< The distance from the active notification to start of radio activity. */ }; /**@brief Radio notification types. */ enum NRF_RADIO_NOTIFICATION_TYPES { - NRF_RADIO_NOTIFICATION_TYPE_NONE = 0, /**< The event does not have a radio notification signal. */ - NRF_RADIO_NOTIFICATION_TYPE_INT_ON_ACTIVE, /**< Using interrupt for notification when the radio will be enabled. */ - NRF_RADIO_NOTIFICATION_TYPE_INT_ON_INACTIVE, /**< Using interrupt for notification when the radio has been disabled. - */ - NRF_RADIO_NOTIFICATION_TYPE_INT_ON_BOTH, /**< Using interrupt for notification both when the radio will be enabled and - disabled. */ + NRF_RADIO_NOTIFICATION_TYPE_NONE = 0, /**< The event does not have a radio notification signal. */ + NRF_RADIO_NOTIFICATION_TYPE_INT_ON_ACTIVE, /**< Using interrupt for notification when the radio will be enabled. */ + NRF_RADIO_NOTIFICATION_TYPE_INT_ON_INACTIVE, /**< Using interrupt for notification when the radio has been disabled. */ + NRF_RADIO_NOTIFICATION_TYPE_INT_ON_BOTH, /**< Using interrupt for notification both when the radio will be enabled and + disabled. */ }; /**@brief The Radio signal callback types. */ enum NRF_RADIO_CALLBACK_SIGNAL_TYPE { - NRF_RADIO_CALLBACK_SIGNAL_TYPE_START, /**< This signal indicates the start of the radio timeslot. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_TIMER0, /**< This signal indicates the NRF_TIMER0 interrupt. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_RADIO, /**< This signal indicates the NRF_RADIO interrupt. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_FAILED, /**< This signal indicates extend action failed. */ - NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_SUCCEEDED /**< This signal indicates extend action succeeded. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_START, /**< This signal indicates the start of the radio timeslot. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_TIMER0, /**< This signal indicates the NRF_TIMER0 interrupt. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_RADIO, /**< This signal indicates the NRF_RADIO interrupt. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_FAILED, /**< This signal indicates extend action failed. */ + NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_SUCCEEDED /**< This signal indicates extend action succeeded. */ }; /**@brief The actions requested by the signal callback. @@ -259,66 +259,63 @@ enum NRF_RADIO_CALLBACK_SIGNAL_TYPE { * returned. */ enum NRF_RADIO_SIGNAL_CALLBACK_ACTION { - NRF_RADIO_SIGNAL_CALLBACK_ACTION_NONE, /**< Return without action. */ - NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND, /**< Request an extension of the current - timeslot. Maximum execution time for this action: - @ref NRF_RADIO_MAX_EXTENSION_PROCESSING_TIME_US. - This action must be started at least - @ref NRF_RADIO_MIN_EXTENSION_MARGIN_US before - the end of the timeslot. */ - NRF_RADIO_SIGNAL_CALLBACK_ACTION_END, /**< End the current radio timeslot. */ - NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END /**< Request a new radio timeslot and end the current timeslot. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_NONE, /**< Return without action. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND, /**< Request an extension of the current + timeslot. Maximum execution time for this action: + @ref NRF_RADIO_MAX_EXTENSION_PROCESSING_TIME_US. + This action must be started at least + @ref NRF_RADIO_MIN_EXTENSION_MARGIN_US before + the end of the timeslot. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_END, /**< End the current radio timeslot. */ + NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END /**< Request a new radio timeslot and end the current timeslot. */ }; /**@brief Radio timeslot high frequency clock source configuration. */ enum NRF_RADIO_HFCLK_CFG { - NRF_RADIO_HFCLK_CFG_XTAL_GUARANTEED, /**< The SoftDevice will guarantee that the high frequency clock source is the - external crystal for the whole duration of the timeslot. This should be the - preferred option for events that use the radio or require high timing - accuracy. - @note The SoftDevice will automatically turn on and off the external crystal, - at the beginning and end of the timeslot, respectively. The crystal may also - intentionally be left running after the timeslot, in cases where it is needed - by the SoftDevice shortly after the end of the timeslot. */ - NRF_RADIO_HFCLK_CFG_NO_GUARANTEE /**< This configuration allows for earlier and tighter scheduling of timeslots. - The RC oscillator may be the clock source in part or for the whole duration of - the timeslot. The RC oscillator's accuracy must therefore be taken into - consideration. - @note If the application will use the radio peripheral in timeslots with this - configuration, it must make sure that the crystal is running and stable before - starting the radio. */ + NRF_RADIO_HFCLK_CFG_XTAL_GUARANTEED, /**< The SoftDevice will guarantee that the high frequency clock source is the + external crystal for the whole duration of the timeslot. This should be the + preferred option for events that use the radio or require high timing accuracy. + @note The SoftDevice will automatically turn on and off the external crystal, + at the beginning and end of the timeslot, respectively. The crystal may also + intentionally be left running after the timeslot, in cases where it is needed + by the SoftDevice shortly after the end of the timeslot. */ + NRF_RADIO_HFCLK_CFG_NO_GUARANTEE /**< This configuration allows for earlier and tighter scheduling of timeslots. + The RC oscillator may be the clock source in part or for the whole duration of the + timeslot. The RC oscillator's accuracy must therefore be taken into consideration. + @note If the application will use the radio peripheral in timeslots with this + configuration, it must make sure that the crystal is running and stable before + starting the radio. */ }; /**@brief Radio timeslot priorities. */ enum NRF_RADIO_PRIORITY { - NRF_RADIO_PRIORITY_HIGH, /**< High (equal priority as the normal connection priority of the SoftDevice stack(s)). */ - NRF_RADIO_PRIORITY_NORMAL, /**< Normal (equal priority as the priority of secondary activities of the SoftDevice - stack(s)). */ + NRF_RADIO_PRIORITY_HIGH, /**< High (equal priority as the normal connection priority of the SoftDevice stack(s)). */ + NRF_RADIO_PRIORITY_NORMAL, /**< Normal (equal priority as the priority of secondary activities of the SoftDevice stack(s)). */ }; /**@brief Radio timeslot request type. */ enum NRF_RADIO_REQUEST_TYPE { - NRF_RADIO_REQ_TYPE_EARLIEST, /**< Request radio timeslot as early as possible. This should always be used for the - first request in a session. */ - NRF_RADIO_REQ_TYPE_NORMAL /**< Normal radio timeslot request. */ + NRF_RADIO_REQ_TYPE_EARLIEST, /**< Request radio timeslot as early as possible. This should always be used for the first + request in a session. */ + NRF_RADIO_REQ_TYPE_NORMAL /**< Normal radio timeslot request. */ }; /**@brief SoC Events. */ enum NRF_SOC_EVTS { - NRF_EVT_HFCLKSTARTED, /**< Event indicating that the HFCLK has started. */ - NRF_EVT_POWER_FAILURE_WARNING, /**< Event indicating that a power failure warning has occurred. */ - NRF_EVT_FLASH_OPERATION_SUCCESS, /**< Event indicating that the ongoing flash operation has completed successfully. */ - NRF_EVT_FLASH_OPERATION_ERROR, /**< Event indicating that the ongoing flash operation has timed out with an error. */ - NRF_EVT_RADIO_BLOCKED, /**< Event indicating that a radio timeslot was blocked. */ - NRF_EVT_RADIO_CANCELED, /**< Event indicating that a radio timeslot was canceled by SoftDevice. */ - NRF_EVT_RADIO_SIGNAL_CALLBACK_INVALID_RETURN, /**< Event indicating that a radio timeslot signal callback handler - return was invalid. */ - NRF_EVT_RADIO_SESSION_IDLE, /**< Event indicating that a radio timeslot session is idle. */ - NRF_EVT_RADIO_SESSION_CLOSED, /**< Event indicating that a radio timeslot session is closed. */ - NRF_EVT_POWER_USB_POWER_READY, /**< Event indicating that a USB 3.3 V supply is ready. */ - NRF_EVT_POWER_USB_DETECTED, /**< Event indicating that voltage supply is detected on VBUS. */ - NRF_EVT_POWER_USB_REMOVED, /**< Event indicating that voltage supply is removed from VBUS. */ - NRF_EVT_NUMBER_OF_EVTS + NRF_EVT_HFCLKSTARTED, /**< Event indicating that the HFCLK has started. */ + NRF_EVT_POWER_FAILURE_WARNING, /**< Event indicating that a power failure warning has occurred. */ + NRF_EVT_FLASH_OPERATION_SUCCESS, /**< Event indicating that the ongoing flash operation has completed successfully. */ + NRF_EVT_FLASH_OPERATION_ERROR, /**< Event indicating that the ongoing flash operation has timed out with an error. */ + NRF_EVT_RADIO_BLOCKED, /**< Event indicating that a radio timeslot was blocked. */ + NRF_EVT_RADIO_CANCELED, /**< Event indicating that a radio timeslot was canceled by SoftDevice. */ + NRF_EVT_RADIO_SIGNAL_CALLBACK_INVALID_RETURN, /**< Event indicating that a radio timeslot signal callback handler return was + invalid. */ + NRF_EVT_RADIO_SESSION_IDLE, /**< Event indicating that a radio timeslot session is idle. */ + NRF_EVT_RADIO_SESSION_CLOSED, /**< Event indicating that a radio timeslot session is closed. */ + NRF_EVT_POWER_USB_POWER_READY, /**< Event indicating that a USB 3.3 V supply is ready. */ + NRF_EVT_POWER_USB_DETECTED, /**< Event indicating that voltage supply is detected on VBUS. */ + NRF_EVT_POWER_USB_REMOVED, /**< Event indicating that voltage supply is removed from VBUS. */ + NRF_EVT_NUMBER_OF_EVTS }; /**@} */ @@ -333,44 +330,44 @@ typedef volatile uint8_t nrf_mutex_t; /**@brief Parameters for a request for a timeslot as early as possible. */ typedef struct { - uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ - uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ - uint32_t length_us; /**< The radio timeslot length (in the range 100 to 100,000] microseconds). */ - uint32_t timeout_us; /**< Longest acceptable delay until the start of the requested timeslot (up to @ref - NRF_RADIO_EARLIEST_TIMEOUT_MAX_US microseconds). */ + uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ + uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ + uint32_t length_us; /**< The radio timeslot length (in the range 100 to 100,000] microseconds). */ + uint32_t timeout_us; /**< Longest acceptable delay until the start of the requested timeslot (up to @ref + NRF_RADIO_EARLIEST_TIMEOUT_MAX_US microseconds). */ } nrf_radio_request_earliest_t; /**@brief Parameters for a normal radio timeslot request. */ typedef struct { - uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ - uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ - uint32_t distance_us; /**< Distance from the start of the previous radio timeslot (up to @ref - NRF_RADIO_DISTANCE_MAX_US microseconds). */ - uint32_t length_us; /**< The radio timeslot length (in the range [100..100,000] microseconds). */ + uint8_t hfclk; /**< High frequency clock source, see @ref NRF_RADIO_HFCLK_CFG. */ + uint8_t priority; /**< The radio timeslot priority, see @ref NRF_RADIO_PRIORITY. */ + uint32_t distance_us; /**< Distance from the start of the previous radio timeslot (up to @ref NRF_RADIO_DISTANCE_MAX_US + microseconds). */ + uint32_t length_us; /**< The radio timeslot length (in the range [100..100,000] microseconds). */ } nrf_radio_request_normal_t; /**@brief Radio timeslot request parameters. */ typedef struct { - uint8_t request_type; /**< Type of request, see @ref NRF_RADIO_REQUEST_TYPE. */ - union { - nrf_radio_request_earliest_t earliest; /**< Parameters for requesting a radio timeslot as early as possible. */ - nrf_radio_request_normal_t normal; /**< Parameters for requesting a normal radio timeslot. */ - } params; /**< Parameter union. */ + uint8_t request_type; /**< Type of request, see @ref NRF_RADIO_REQUEST_TYPE. */ + union { + nrf_radio_request_earliest_t earliest; /**< Parameters for requesting a radio timeslot as early as possible. */ + nrf_radio_request_normal_t normal; /**< Parameters for requesting a normal radio timeslot. */ + } params; /**< Parameter union. */ } nrf_radio_request_t; /**@brief Return parameters of the radio timeslot signal callback. */ typedef struct { - uint8_t callback_action; /**< The action requested by the application when returning from the signal callback, see - @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION. */ - union { - struct { - nrf_radio_request_t *p_next; /**< The request parameters for the next radio timeslot. */ - } request; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END. */ - struct { - uint32_t length_us; /**< Requested extension of the radio timeslot duration (microseconds) (for minimum time see - @ref NRF_RADIO_MINIMUM_TIMESLOT_LENGTH_EXTENSION_TIME_US). */ - } extend; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND. */ - } params; /**< Parameter union. */ + uint8_t callback_action; /**< The action requested by the application when returning from the signal callback, see @ref + NRF_RADIO_SIGNAL_CALLBACK_ACTION. */ + union { + struct { + nrf_radio_request_t *p_next; /**< The request parameters for the next radio timeslot. */ + } request; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END. */ + struct { + uint32_t length_us; /**< Requested extension of the radio timeslot duration (microseconds) (for minimum time see @ref + NRF_RADIO_MINIMUM_TIMESLOT_LENGTH_EXTENSION_TIME_US). */ + } extend; /**< Additional parameters for return_code @ref NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND. */ + } params; /**< Parameter union. */ } nrf_radio_signal_callback_return_param_t; /**@brief The radio timeslot signal callback type. @@ -394,17 +391,17 @@ typedef uint8_t soc_ecb_ciphertext_t[SOC_ECB_CIPHERTEXT_LENGTH]; /**< Ciphertext /**@brief AES ECB data structure */ typedef struct { - soc_ecb_key_t key; /**< Encryption key. */ - soc_ecb_cleartext_t cleartext; /**< Cleartext data. */ - soc_ecb_ciphertext_t ciphertext; /**< Ciphertext data. */ + soc_ecb_key_t key; /**< Encryption key. */ + soc_ecb_cleartext_t cleartext; /**< Cleartext data. */ + soc_ecb_ciphertext_t ciphertext; /**< Ciphertext data. */ } nrf_ecb_hal_data_t; /**@brief AES ECB block. Used to provide multiple blocks in a single call to @ref sd_ecb_blocks_encrypt.*/ typedef struct { - soc_ecb_key_t const *p_key; /**< Pointer to the Encryption key. */ - soc_ecb_cleartext_t const *p_cleartext; /**< Pointer to the Cleartext data. */ - soc_ecb_ciphertext_t *p_ciphertext; /**< Pointer to the Ciphertext data. */ + soc_ecb_key_t const *p_key; /**< Pointer to the Encryption key. */ + soc_ecb_cleartext_t const *p_cleartext; /**< Pointer to the Cleartext data. */ + soc_ecb_ciphertext_t *p_ciphertext; /**< Pointer to the Ciphertext data. */ } nrf_ecb_hal_data_block_t; /**@} */ @@ -459,8 +456,8 @@ SVCALL(SD_RAND_APPLICATION_BYTES_AVAILABLE_GET, uint32_t, sd_rand_application_by * @param[in] length Number of bytes to take from pool and place in p_buff. * * @retval ::NRF_SUCCESS The requested bytes were written to p_buff. - * @retval ::NRF_ERROR_SOC_RAND_NOT_ENOUGH_VALUES No bytes were written to the buffer, because there were not enough - * bytes available. + * @retval ::NRF_ERROR_SOC_RAND_NOT_ENOUGH_VALUES No bytes were written to the buffer, because there were not enough bytes + * available. */ SVCALL(SD_RAND_APPLICATION_VECTOR_GET, uint32_t, sd_rand_application_vector_get(uint8_t *p_buff, uint8_t length)); diff --git a/src/platform/nrf52/softdevice/nrf_svc.h b/src/platform/nrf52/softdevice/nrf_svc.h index 534fa00ac..1de44656f 100644 --- a/src/platform/nrf52/softdevice/nrf_svc.h +++ b/src/platform/nrf52/softdevice/nrf_svc.h @@ -68,22 +68,23 @@ extern "C" { #else #define GCC_CAST_CPP #endif -#define SVCALL(number, return_type, signature) \ - _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wreturn-type\"") __attribute__((naked)) \ - __attribute__((unused)) static return_type signature { \ - __asm("svc %0\n" \ - "bx r14" \ - : \ - : "I"(GCC_CAST_CPP number) \ - : "r0"); \ - } \ - _Pragma("GCC diagnostic pop") +#define SVCALL(number, return_type, signature) \ + _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wreturn-type\"") __attribute__((naked)) \ + __attribute__((unused)) static return_type signature \ + { \ + __asm("svc %0\n" \ + "bx r14" \ + : \ + : "I"(GCC_CAST_CPP number) \ + : "r0"); \ + } \ + _Pragma("GCC diagnostic pop") #elif defined(__ICCARM__) #define PRAGMA(x) _Pragma(#x) -#define SVCALL(number, return_type, signature) \ - PRAGMA(swi_number = (number)) \ - __swi return_type signature; +#define SVCALL(number, return_type, signature) \ + PRAGMA(swi_number = (number)) \ + __swi return_type signature; #else #define SVCALL(number, return_type, signature) return_type signature #endif diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 4d73faf66..4722a66e5 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -41,803 +41,832 @@ const char *argp_program_version = optstr(APP_VERSION); char stdoutBuffer[512]; // FIXME - move setBluetoothEnable into a HALPlatform class -void setBluetoothEnable(bool enable) { - // not needed +void setBluetoothEnable(bool enable) +{ + // not needed } -void cpuDeepSleep(uint32_t msecs) { notImplemented("cpuDeepSleep"); } +void cpuDeepSleep(uint32_t msecs) +{ + notImplemented("cpuDeepSleep"); +} void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel"); int TCPPort = SERVER_API_DEFAULT_PORT; -static error_t parse_opt(int key, char *arg, struct argp_state *state) { - switch (key) { - case 'p': - if (sscanf(arg, "%d", &TCPPort) < 1) - return ARGP_ERR_UNKNOWN; - else - printf("Using config file %d\n", TCPPort); - break; - case 'c': - configPath = arg; - break; - case 's': - portduino_config.force_simradio = true; - break; - case 'h': - optionMac = arg; - break; - case 'v': - verboseEnabled = true; - break; - case 'y': - yamlOnly = true; - break; - case ARGP_KEY_ARG: +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ + switch (key) { + case 'p': + if (sscanf(arg, "%d", &TCPPort) < 1) + return ARGP_ERR_UNKNOWN; + else + printf("Using config file %d\n", TCPPort); + break; + case 'c': + configPath = arg; + break; + case 's': + portduino_config.force_simradio = true; + break; + case 'h': + optionMac = arg; + break; + case 'v': + verboseEnabled = true; + break; + case 'y': + yamlOnly = true; + break; + case ARGP_KEY_ARG: + return 0; + default: + return ARGP_ERR_UNKNOWN; + } return 0; - default: - return ARGP_ERR_UNKNOWN; - } - return 0; } -void portduinoCustomInit() { - static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, - {"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."}, - {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"}, - {"sim", 's', 0, 0, "Run in Simulated radio mode"}, - {"verbose", 'v', 0, 0, "Set log level to full debug"}, - {"output-yaml", 'y', 0, 0, "Output config yaml and exit"}, - {0}}; - static void *childArguments; - static char doc[] = "Meshtastic native build."; - static char args_doc[] = "..."; - static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; - const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0}; - portduinoAddArguments(child, childArguments); +void portduinoCustomInit() +{ + static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, + {"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."}, + {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"}, + {"sim", 's', 0, 0, "Run in Simulated radio mode"}, + {"verbose", 'v', 0, 0, "Set log level to full debug"}, + {"output-yaml", 'y', 0, 0, "Output config yaml and exit"}, + {0}}; + static void *childArguments; + static char doc[] = "Meshtastic native build."; + static char args_doc[] = "..."; + static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; + const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0}; + portduinoAddArguments(child, childArguments); } -void getMacAddr(uint8_t *dmac) { - // We should store this value, and short-circuit all this if it's already been set. - if (optionMac != nullptr && strlen(optionMac) > 0) { - if (strlen(optionMac) >= 12) { - MAC_from_string(optionMac, dmac); +void getMacAddr(uint8_t *dmac) +{ + // We should store this value, and short-circuit all this if it's already been set. + if (optionMac != nullptr && strlen(optionMac) > 0) { + if (strlen(optionMac) >= 12) { + MAC_from_string(optionMac, dmac); + } else { + uint32_t hwId = {0}; + sscanf(optionMac, "%u", &hwId); + dmac[0] = 0x80; + dmac[1] = 0; + dmac[2] = hwId >> 24; + dmac[3] = hwId >> 16; + dmac[4] = hwId >> 8; + dmac[5] = hwId & 0xff; + } + } else if (portduino_config.mac_address.length() > 11) { + MAC_from_string(portduino_config.mac_address, dmac); + exit; } else { - uint32_t hwId = {0}; - sscanf(optionMac, "%u", &hwId); - dmac[0] = 0x80; - dmac[1] = 0; - dmac[2] = hwId >> 24; - dmac[3] = hwId >> 16; - dmac[4] = hwId >> 8; - dmac[5] = hwId & 0xff; - } - } else if (portduino_config.mac_address.length() > 11) { - MAC_from_string(portduino_config.mac_address, dmac); - exit; - } else { - struct hci_dev_info di = {0}; - di.dev_id = 0; - bdaddr_t bdaddr; - int btsock; - btsock = socket(AF_BLUETOOTH, SOCK_RAW, 1); - if (btsock < 0) { // If anything fails, just return with the default value - return; - } + struct hci_dev_info di = {0}; + di.dev_id = 0; + bdaddr_t bdaddr; + int btsock; + btsock = socket(AF_BLUETOOTH, SOCK_RAW, 1); + if (btsock < 0) { // If anything fails, just return with the default value + return; + } - if (ioctl(btsock, HCIGETDEVINFO, (void *)&di)) { - return; - } + if (ioctl(btsock, HCIGETDEVINFO, (void *)&di)) { + return; + } - dmac[0] = di.bdaddr.b[5]; - dmac[1] = di.bdaddr.b[4]; - dmac[2] = di.bdaddr.b[3]; - dmac[3] = di.bdaddr.b[2]; - dmac[4] = di.bdaddr.b[1]; - dmac[5] = di.bdaddr.b[0]; - } + dmac[0] = di.bdaddr.b[5]; + dmac[1] = di.bdaddr.b[4]; + dmac[2] = di.bdaddr.b[3]; + dmac[3] = di.bdaddr.b[2]; + dmac[4] = di.bdaddr.b[1]; + dmac[5] = di.bdaddr.b[0]; + } } -std::string cleanupNameForAutoconf(std::string name) { - // Convert spaces -> dashes, lowercase +std::string cleanupNameForAutoconf(std::string name) +{ + // Convert spaces -> dashes, lowercase - std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { - if (c == ' ') { - return '-'; - } - return (char)std::tolower(c); - }); + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { + if (c == ' ') { + return '-'; + } + return (char)std::tolower(c); + }); - return name; + return name; } /** apps run under portduino can optionally define a portduinoSetup() to * use portduino specific init code (such as gpioBind) to setup portduino on their host machine, * before running 'arduino' code. */ -void portduinoSetup() { - int max_GPIO = 0; - std::string gpioChipName = "gpiochip"; - portduino_config.displayPanel = no_screen; +void portduinoSetup() +{ + int max_GPIO = 0; + std::string gpioChipName = "gpiochip"; + portduino_config.displayPanel = no_screen; - // Force stdout to be line buffered - setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer)); + // Force stdout to be line buffered + setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer)); - if (portduino_config.force_simradio == true) { - portduino_config.lora_module = use_simradio; - } else if (configPath != nullptr) { - if (loadConfig(configPath)) { - if (!yamlOnly) - std::cout << "Using " << configPath << " as config file" << std::endl; - } else { - std::cout << "Unable to use " << configPath << " as config file" << std::endl; - exit(EXIT_FAILURE); - } - } else if (access("config.yaml", R_OK) == 0) { - if (loadConfig("config.yaml")) { - if (!yamlOnly) - std::cout << "Using local config.yaml as config file" << std::endl; - } else { - std::cout << "Unable to use local config.yaml as config file" << std::endl; - exit(EXIT_FAILURE); - } - } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) { - if (loadConfig("/etc/meshtasticd/config.yaml")) { - if (!yamlOnly) - std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; - } else { - std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl; - exit(EXIT_FAILURE); - } - } else { - if (!yamlOnly) - std::cout << "No 'config.yaml' found..." << std::endl; - portduino_config.lora_module = use_simradio; - } - - if (portduino_config.config_directory != "") { - std::string filetype = ".yaml"; - for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator{portduino_config.config_directory}) { - if (ends_with(entry.path().string(), ".yaml")) { - std::cout << "Also using " << entry << " as additional config file" << std::endl; - loadConfig(entry.path().c_str()); - } - } - } - - if (yamlOnly) { - std::cout << portduino_config.emit_yaml() << std::endl; - exit(EXIT_SUCCESS); - } - - if (portduino_config.force_simradio) { - std::cout << "Running in simulated mode." << std::endl; - portduino_config.MaxNodes = 200; // Default to 200 nodes - // Set the random seed equal to TCPPort to have a different seed per instance - randomSeed(TCPPort); - return; - } - - // If LoRa `Module: auto` (default in config.yaml), - // attempt to auto config based on Product Strings - if (portduino_config.lora_module == use_autoconf) { - bool found_hat = false; - bool found_rak_eeprom = false; - bool found_ch341 = false; - - char hat_vendor[96] = {0}; - char autoconf_product[96] = {0}; - // Try CH341 - try { - std::cout << "autoconf: Looking for CH341 device..." << std::endl; - ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); - ch341Hal->getProductString(autoconf_product, 95); - delete ch341Hal; - std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; - - found_ch341 = true; - } catch (...) { - std::cout << "autoconf: Could not locate CH341 device" << std::endl; - } - // Try Pi HAT+ - if (strlen(autoconf_product) < 6) { - std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; - if (access("/proc/device-tree/hat/vendor", R_OK) == 0) { - std::ifstream hatVendorFile("/proc/device-tree/hat/vendor"); - if (hatVendorFile.is_open()) { - hatVendorFile.read(hat_vendor, 95); - hatVendorFile.close(); + if (portduino_config.force_simradio == true) { + portduino_config.lora_module = use_simradio; + } else if (configPath != nullptr) { + if (loadConfig(configPath)) { + if (!yamlOnly) + std::cout << "Using " << configPath << " as config file" << std::endl; + } else { + std::cout << "Unable to use " << configPath << " as config file" << std::endl; + exit(EXIT_FAILURE); } - } - if (access("/proc/device-tree/hat/product", R_OK) == 0) { - std::ifstream hatProductFile("/proc/device-tree/hat/product"); - if (hatProductFile.is_open()) { - hatProductFile.read(autoconf_product, 95); - hatProductFile.close(); + } else if (access("config.yaml", R_OK) == 0) { + if (loadConfig("config.yaml")) { + if (!yamlOnly) + std::cout << "Using local config.yaml as config file" << std::endl; + } else { + std::cout << "Unable to use local config.yaml as config file" << std::endl; + exit(EXIT_FAILURE); } - std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; - found_hat = true; - } else { - std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; - } + } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) { + if (loadConfig("/etc/meshtasticd/config.yaml")) { + if (!yamlOnly) + std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; + } else { + std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl; + exit(EXIT_FAILURE); + } + } else { + if (!yamlOnly) + std::cout << "No 'config.yaml' found..." << std::endl; + portduino_config.lora_module = use_simradio; } - // attempt to load autoconf data from an EEPROM on 0x50 - // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 - // :mac address :<16 random unique bytes in hexidecimal> : crc32 - // crc32 is calculated on the eeprom string up to but not including the final colon - if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { - try { - char *mac_start = nullptr; - char *devID_start = nullptr; - char *crc32_start = nullptr; - Wire.begin(); - Wire.beginTransmission(0x50); - Wire.write(0x0); - Wire.write(0x0); - Wire.endTransmission(); - Wire.requestFrom((uint8_t)0x50, (uint8_t)75); - uint8_t i = 0; - delay(100); - std::string autoconf_raw; - while (Wire.available() && i < sizeof(autoconf_product)) { - autoconf_product[i] = Wire.read(); - if (autoconf_product[i] == 0xff) { - autoconf_product[i] = 0x0; - break; - } - autoconf_raw += autoconf_product[i]; - if (autoconf_product[i] == ':') { - autoconf_product[i] = 0x0; - if (mac_start == nullptr) { - mac_start = autoconf_product + i + 1; - } else if (devID_start == nullptr) { - devID_start = autoconf_product + i + 1; - } else if (crc32_start == nullptr) { - crc32_start = autoconf_product + i + 1; + + if (portduino_config.config_directory != "") { + std::string filetype = ".yaml"; + for (const std::filesystem::directory_entry &entry : + std::filesystem::directory_iterator{portduino_config.config_directory}) { + if (ends_with(entry.path().string(), ".yaml")) { + std::cout << "Also using " << entry << " as additional config file" << std::endl; + loadConfig(entry.path().c_str()); } - } - i++; } - if (crc32_start != nullptr && strlen(crc32_start) == 8) { - std::string crc32_str(crc32_start); - uint32_t crc32_value = 0; + } - // convert crc32 ascii to raw uint32 - for (int j = 0; j < 4; j++) { - crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8; - } - std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl; + if (yamlOnly) { + std::cout << portduino_config.emit_yaml() << std::endl; + exit(EXIT_SUCCESS); + } - // set the autoconf string to blank and short circuit - if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) { - std::cout << "autoconf: crc32 mismatch, dropping " << std::endl; - autoconf_product[0] = 0x0; - } else { - std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl; - found_rak_eeprom = true; - if (mac_start != nullptr) { - std::cout << "autoconf: Found mac data " << mac_start << std::endl; - if (strlen(mac_start) == 12) - portduino_config.mac_address = std::string(mac_start); - } - if (devID_start != nullptr) { - std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; - if (strlen(devID_start) == 32) { - std::string devID_str(devID_start); - for (int j = 0; j < 16; j++) { - portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16); + if (portduino_config.force_simradio) { + std::cout << "Running in simulated mode." << std::endl; + portduino_config.MaxNodes = 200; // Default to 200 nodes + // Set the random seed equal to TCPPort to have a different seed per instance + randomSeed(TCPPort); + return; + } + + // If LoRa `Module: auto` (default in config.yaml), + // attempt to auto config based on Product Strings + if (portduino_config.lora_module == use_autoconf) { + bool found_hat = false; + bool found_rak_eeprom = false; + bool found_ch341 = false; + + char hat_vendor[96] = {0}; + char autoconf_product[96] = {0}; + // Try CH341 + try { + std::cout << "autoconf: Looking for CH341 device..." << std::endl; + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, + portduino_config.lora_usb_pid); + ch341Hal->getProductString(autoconf_product, 95); + delete ch341Hal; + std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; + + found_ch341 = true; + } catch (...) { + std::cout << "autoconf: Could not locate CH341 device" << std::endl; + } + // Try Pi HAT+ + if (strlen(autoconf_product) < 6) { + std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; + if (access("/proc/device-tree/hat/vendor", R_OK) == 0) { + std::ifstream hatVendorFile("/proc/device-tree/hat/vendor"); + if (hatVendorFile.is_open()) { + hatVendorFile.read(hat_vendor, 95); + hatVendorFile.close(); } - portduino_config.has_device_id = true; - } } - } + if (access("/proc/device-tree/hat/product", R_OK) == 0) { + std::ifstream hatProductFile("/proc/device-tree/hat/product"); + if (hatProductFile.is_open()) { + hatProductFile.read(autoconf_product, 95); + hatProductFile.close(); + } + std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" + << std::endl; + found_hat = true; + } else { + std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; + } + } + // attempt to load autoconf data from an EEPROM on 0x50 + // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 + // :mac address :<16 random unique bytes in hexidecimal> : crc32 + // crc32 is calculated on the eeprom string up to but not including the final colon + if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { + try { + char *mac_start = nullptr; + char *devID_start = nullptr; + char *crc32_start = nullptr; + Wire.begin(); + Wire.beginTransmission(0x50); + Wire.write(0x0); + Wire.write(0x0); + Wire.endTransmission(); + Wire.requestFrom((uint8_t)0x50, (uint8_t)75); + uint8_t i = 0; + delay(100); + std::string autoconf_raw; + while (Wire.available() && i < sizeof(autoconf_product)) { + autoconf_product[i] = Wire.read(); + if (autoconf_product[i] == 0xff) { + autoconf_product[i] = 0x0; + break; + } + autoconf_raw += autoconf_product[i]; + if (autoconf_product[i] == ':') { + autoconf_product[i] = 0x0; + if (mac_start == nullptr) { + mac_start = autoconf_product + i + 1; + } else if (devID_start == nullptr) { + devID_start = autoconf_product + i + 1; + } else if (crc32_start == nullptr) { + crc32_start = autoconf_product + i + 1; + } + } + i++; + } + if (crc32_start != nullptr && strlen(crc32_start) == 8) { + std::string crc32_str(crc32_start); + uint32_t crc32_value = 0; + + // convert crc32 ascii to raw uint32 + for (int j = 0; j < 4; j++) { + crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8; + } + std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl; + + // set the autoconf string to blank and short circuit + if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) { + std::cout << "autoconf: crc32 mismatch, dropping " << std::endl; + autoconf_product[0] = 0x0; + } else { + std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl; + found_rak_eeprom = true; + if (mac_start != nullptr) { + std::cout << "autoconf: Found mac data " << mac_start << std::endl; + if (strlen(mac_start) == 12) + portduino_config.mac_address = std::string(mac_start); + } + if (devID_start != nullptr) { + std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; + if (strlen(devID_start) == 32) { + std::string devID_str(devID_start); + for (int j = 0; j < 16; j++) { + portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16); + } + portduino_config.has_device_id = true; + } + } + } + } else { + std::cout << "autoconf: crc32 missing " << std::endl; + autoconf_product[0] = 0x0; + } + } catch (...) { + std::cout << "autoconf: Could not locate EEPROM" << std::endl; + } + } + // Load the config file based on the product string + if (strlen(autoconf_product) > 0) { + // From configProducts map in PortduinoGlue.h + std::string product_config = ""; + + if (configProducts.find(autoconf_product) != configProducts.end()) { + product_config = configProducts.at(autoconf_product); + } else { + if (found_hat) { + product_config = + cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); + } else if (found_ch341) { + product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); + } + + // Don't try to automatically find config for a device with RAK eeprom. + if (found_rak_eeprom) { + std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl; + exit(EXIT_FAILURE); + } + if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) { + std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")" + << std::endl; + exit(EXIT_FAILURE); + } + } + + if (loadConfig((portduino_config.available_directory + product_config).c_str())) { + std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl; + } else { + std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product + << std::endl; + exit(EXIT_FAILURE); + } } else { - std::cout << "autoconf: crc32 missing " << std::endl; - autoconf_product[0] = 0x0; + std::cerr << "autoconf: Could not locate any devices" << std::endl; + exit(EXIT_FAILURE); } - } catch (...) { - std::cout << "autoconf: Could not locate EEPROM" << std::endl; - } } - // Load the config file based on the product string - if (strlen(autoconf_product) > 0) { - // From configProducts map in PortduinoGlue.h - std::string product_config = ""; - if (configProducts.find(autoconf_product) != configProducts.end()) { - product_config = configProducts.at(autoconf_product); - } else { - if (found_hat) { - product_config = cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); - } else if (found_ch341) { - product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); + // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address + uint8_t dmac[6] = {0}; + if (portduino_config.lora_spi_dev == "ch341") { + try { + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, + portduino_config.lora_usb_pid); + } catch (std::exception &e) { + std::cerr << e.what() << std::endl; + std::cerr << "Could not initialize CH341 device!" << std::endl; + exit(EXIT_FAILURE); } - - // Don't try to automatically find config for a device with RAK eeprom. - if (found_rak_eeprom) { - std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl; - exit(EXIT_FAILURE); + char serial[9] = {0}; + ch341Hal->getSerialString(serial, 8); + std::cout << "CH341 Serial " << serial << std::endl; + char product_string[96] = {0}; + ch341Hal->getProductString(product_string, 95); + std::cout << "CH341 Product " << product_string << std::endl; + if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { + uint8_t hash[32] = {0}; + memcpy(hash, serial, 8); + crypto->hash(hash, 8); + dmac[0] = (hash[0] << 4) | 2; + dmac[1] = hash[1]; + dmac[2] = hash[2]; + dmac[3] = hash[3]; + dmac[4] = hash[4]; + dmac[5] = hash[5]; + char macBuf[13] = {0}; + sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); + portduino_config.mac_address = macBuf; } - if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) { - std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")" << std::endl; - exit(EXIT_FAILURE); - } - } - - if (loadConfig((portduino_config.available_directory + product_config).c_str())) { - std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl; - } else { - std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product << std::endl; - exit(EXIT_FAILURE); - } - } else { - std::cerr << "autoconf: Could not locate any devices" << std::endl; - exit(EXIT_FAILURE); } - } - // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address - uint8_t dmac[6] = {0}; - if (portduino_config.lora_spi_dev == "ch341") { - try { - ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); - } catch (std::exception &e) { - std::cerr << e.what() << std::endl; - std::cerr << "Could not initialize CH341 device!" << std::endl; - exit(EXIT_FAILURE); - } - char serial[9] = {0}; - ch341Hal->getSerialString(serial, 8); - std::cout << "CH341 Serial " << serial << std::endl; - char product_string[96] = {0}; - ch341Hal->getProductString(product_string, 95); - std::cout << "CH341 Product " << product_string << std::endl; - if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { - uint8_t hash[32] = {0}; - memcpy(hash, serial, 8); - crypto->hash(hash, 8); - dmac[0] = (hash[0] << 4) | 2; - dmac[1] = hash[1]; - dmac[2] = hash[2]; - dmac[3] = hash[3]; - dmac[4] = hash[4]; - dmac[5] = hash[5]; - char macBuf[13] = {0}; - sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); - portduino_config.mac_address = macBuf; - } - } - - getMacAddr(dmac); + getMacAddr(dmac); #ifndef UNIT_TEST - if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { - std::cout << "*** Blank MAC Address not allowed!" << std::endl; - std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; - exit(EXIT_FAILURE); - } -#endif - printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); - // Rather important to set this, if not running simulated. - randomSeed(time(NULL)); - - std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); - for (auto i : portduino_config.all_pins) { - if (i->enabled && i->pin > max_GPIO) - max_GPIO = i->pin; - } - - gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. - - // Need to bind all the configured GPIO pins so they're not simulated - // TODO: If one of these fails, we should log and terminate - for (auto i : portduino_config.all_pins) { - // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora - // Those GPIO are handled in our usermode driver instead. - if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { - continue; - } - if (i->enabled) { - if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { - printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); + if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { + std::cout << "*** Blank MAC Address not allowed!" << std::endl; + std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; exit(EXIT_FAILURE); - } } - } - - // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware - if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { - SPI.begin(portduino_config.lora_spi_dev.c_str()); - } - - if (portduino_config.traceFilename != "") { - try { - traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); - } catch (std::ofstream::failure &e) { - std::cout << "*** traceFile Exception " << e.what() << std::endl; - exit(EXIT_FAILURE); - } - if (!traceFile.is_open()) { - std::cout << "*** traceFile open failure" << std::endl; - exit(EXIT_FAILURE); - } - } else if (portduino_config.JSONFilename != "") { - try { - JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); - } catch (std::ofstream::failure &e) { - std::cout << "*** JSONFile Exception " << e.what() << std::endl; - exit(EXIT_FAILURE); - } - if (!JSONFile.is_open()) { - std::cout << "*** JSONFile open failure" << std::endl; - exit(EXIT_FAILURE); - } - } - if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { - portduino_config.logoutputlevel = level_debug; - } - - return; -} - -int initGPIOPin(int pinNum, const std::string gpioChipName, int line) { -#ifdef PORTDUINO_LINUX_HARDWARE - std::string gpio_name = "GPIO" + std::to_string(pinNum); - std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl; - try { - GPIOPin *csPin; - csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); - csPin->setSilent(); - gpioBind(csPin); - return ERRNO_OK; - } catch (...) { - const std::type_info *t = abi::__cxa_current_exception_type(); - std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl; - return ERRNO_DISABLED; - } -#else - return ERRNO_OK; #endif -} + printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); + // Rather important to set this, if not running simulated. + randomSeed(time(NULL)); -bool loadConfig(const char *configPath) { - YAML::Node yamlConfig; - try { - yamlConfig = YAML::LoadFile(configPath); - if (yamlConfig["Logging"]) { - if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") { - portduino_config.logoutputlevel = level_trace; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { + std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); + for (auto i : portduino_config.all_pins) { + if (i->enabled && i->pin > max_GPIO) + max_GPIO = i->pin; + } + + gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. + + // Need to bind all the configured GPIO pins so they're not simulated + // TODO: If one of these fails, we should log and terminate + for (auto i : portduino_config.all_pins) { + // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora + // Those GPIO are handled in our usermode driver instead. + if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { + continue; + } + if (i->enabled) { + if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); + exit(EXIT_FAILURE); + } + } + } + + // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware + if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { + SPI.begin(portduino_config.lora_spi_dev.c_str()); + } + + if (portduino_config.traceFilename != "") { + try { + traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** traceFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!traceFile.is_open()) { + std::cout << "*** traceFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } else if (portduino_config.JSONFilename != "") { + try { + JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + } catch (std::ofstream::failure &e) { + std::cout << "*** JSONFile Exception " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + if (!JSONFile.is_open()) { + std::cout << "*** JSONFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } + if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { portduino_config.logoutputlevel = level_debug; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { - portduino_config.logoutputlevel = level_info; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { - portduino_config.logoutputlevel = level_warn; - } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { - portduino_config.logoutputlevel = level_error; - } - portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); - portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); - portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); - if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") - portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") - portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") - portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") - portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") - portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") - portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") - portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") - portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") - portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; - else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") - portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; - - if (yamlConfig["Logging"]["AsciiLogs"]) { - // Default is !isatty(1) but can be set explicitly in config.yaml - portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); - portduino_config.ascii_logs_explicit = true; - } } - if (yamlConfig["Lora"]) { - if (yamlConfig["Lora"]["Module"]) { - for (auto &loraModule : portduino_config.loraModules) { - if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { - portduino_config.lora_module = loraModule.first; - break; - } + return; +} + +int initGPIOPin(int pinNum, const std::string gpioChipName, int line) +{ +#ifdef PORTDUINO_LINUX_HARDWARE + std::string gpio_name = "GPIO" + std::to_string(pinNum); + std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl; + try { + GPIOPin *csPin; + csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); + csPin->setSilent(); + gpioBind(csPin); + return ERRNO_OK; + } catch (...) { + const std::type_info *t = abi::__cxa_current_exception_type(); + std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl; + return ERRNO_DISABLED; + } +#else + return ERRNO_OK; +#endif +} + +bool loadConfig(const char *configPath) +{ + YAML::Node yamlConfig; + try { + yamlConfig = YAML::LoadFile(configPath); + if (yamlConfig["Logging"]) { + if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") { + portduino_config.logoutputlevel = level_trace; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { + portduino_config.logoutputlevel = level_debug; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { + portduino_config.logoutputlevel = level_info; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { + portduino_config.logoutputlevel = level_warn; + } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { + portduino_config.logoutputlevel = level_error; + } + portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); + portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); + if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") + portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") + portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") + portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") + portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") + portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") + portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") + portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") + portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; + + if (yamlConfig["Logging"]["AsciiLogs"]) { + // Default is !isatty(1) but can be set explicitly in config.yaml + portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); + portduino_config.ascii_logs_explicit = true; + } } - } - if (yamlConfig["Lora"]["SX126X_MAX_POWER"]) - portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); - if (yamlConfig["Lora"]["SX128X_MAX_POWER"]) - portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); - if (yamlConfig["Lora"]["LR1110_MAX_POWER"]) - portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); - if (yamlConfig["Lora"]["LR1120_MAX_POWER"]) - portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); - if (yamlConfig["Lora"]["RF95_MAX_POWER"]) - portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); + if (yamlConfig["Lora"]) { - if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && !portduino_config.force_simradio) { - portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); - portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; - if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { - portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true" + if (yamlConfig["Lora"]["Module"]) { + for (auto &loraModule : portduino_config.loraModules) { + if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { + portduino_config.lora_module = loraModule.first; + break; + } + } + } + if (yamlConfig["Lora"]["SX126X_MAX_POWER"]) + portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); + if (yamlConfig["Lora"]["SX128X_MAX_POWER"]) + portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); + if (yamlConfig["Lora"]["LR1110_MAX_POWER"]) + portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); + if (yamlConfig["Lora"]["LR1120_MAX_POWER"]) + portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); + if (yamlConfig["Lora"]["RF95_MAX_POWER"]) + portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); + + if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && + !portduino_config.force_simradio) { + portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); + portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; + if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { + portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true" + } + + // backwards API compatibility and to globally set gpiochip once + portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0); + for (auto this_pin : portduino_config.all_pins) { + if (this_pin->config_section == "Lora") { + readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin); + } + } + } + + portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); + portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); + portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); + portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86); + + portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0"); + if (portduino_config.lora_spi_dev != "ch341") { + portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev; + if (portduino_config.lora_spi_dev.length() == 14) { + int x = portduino_config.lora_spi_dev.at(11) - '0'; + int y = portduino_config.lora_spi_dev.at(13) - '0'; + // Pretty sure this is always true + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + // I believe this bit of weirdness is specifically for the new GUI + portduino_config.lora_spi_dev_int = x + y << 4; + portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int; + portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int; + } + } + } + if (yamlConfig["Lora"]["rfswitch_table"]) { + portduino_config.has_rfswitch_table = true; + portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY; + portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX; + portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX; + portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP; + portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF; + portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS; + portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI; + portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE; + + for (int i = 0; i < 5; i++) { + + // set up the pin array first + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8; + if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10") + portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10; + + // now fill in the table + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH") + portduino_config.rfswitch_table[0].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH") + portduino_config.rfswitch_table[1].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH") + portduino_config.rfswitch_table[2].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH") + portduino_config.rfswitch_table[3].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH") + portduino_config.rfswitch_table[4].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH") + portduino_config.rfswitch_table[5].values[i] = HIGH; + if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH") + portduino_config.rfswitch_table[6].values[i] = HIGH; + } + } + } + readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin); + if (yamlConfig["GPS"]) { + std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); + if (serialPath != "") { + Serial1.setPath(serialPath); + portduino_config.has_gps = 1; + } + } + if (yamlConfig["I2C"]) { + portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); + } + if (yamlConfig["Display"]) { + + for (auto &screen_name : portduino_config.screen_names) { + if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) + portduino_config.displayPanel = screen_name.first; + } + portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0); + portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0); + + readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1); + readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1); + readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1); + readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1); + readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1); + + portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false); + portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false); + portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0); + portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0); + portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false); + portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1); + portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false); + portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000); + if (yamlConfig["Display"]["spidev"]) { + portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); + if (portduino_config.display_spi_dev.length() == 14) { + int x = portduino_config.display_spi_dev.at(11) - '0'; + int y = portduino_config.display_spi_dev.at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + portduino_config.display_spi_dev_int = x + y << 4; + portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int; + } + } + } + } + if (yamlConfig["Touchscreen"]) { + if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") + portduino_config.touchscreenModule = xpt2046; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") + portduino_config.touchscreenModule = stmpe610; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") + portduino_config.touchscreenModule = gt911; + else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") + portduino_config.touchscreenModule = ft5x06; + + readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1); + readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1); + + portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); + portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1); + portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); + if (yamlConfig["Touchscreen"]["spidev"]) { + portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); + if (portduino_config.touchscreen_spi_dev.length() == 14) { + int x = portduino_config.touchscreen_spi_dev.at(11) - '0'; + int y = portduino_config.touchscreen_spi_dev.at(13) - '0'; + if (x >= 0 && x < 10 && y >= 0 && y < 10) { + portduino_config.touchscreen_spi_dev_int = x + y << 4; + } + } + } + } + if (yamlConfig["Input"]) { + portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as(""); + portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as(""); + + readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin); + + if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { + portduino_config.tbDirection = 4; + } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { + portduino_config.tbDirection = 3; + } } - // backwards API compatibility and to globally set gpiochip once - portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0); - for (auto this_pin : portduino_config.all_pins) { - if (this_pin->config_section == "Lora") { - readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin); - } + if (yamlConfig["Webserver"]) { + portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1); + portduino_config.webserver_root_path = + (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); + portduino_config.webserver_ssl_key_path = + (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); + portduino_config.webserver_ssl_cert_path = + (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); } - } - portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); - portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); - portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); - portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86); - - portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0"); - if (portduino_config.lora_spi_dev != "ch341") { - portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev; - if (portduino_config.lora_spi_dev.length() == 14) { - int x = portduino_config.lora_spi_dev.at(11) - '0'; - int y = portduino_config.lora_spi_dev.at(13) - '0'; - // Pretty sure this is always true - if (x >= 0 && x < 10 && y >= 0 && y < 10) { - // I believe this bit of weirdness is specifically for the new GUI - portduino_config.lora_spi_dev_int = x + y << 4; - portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int; - portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int; - } + if (yamlConfig["HostMetrics"]) { + portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0); + portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); + portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); } - } - if (yamlConfig["Lora"]["rfswitch_table"]) { - portduino_config.has_rfswitch_table = true; - portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY; - portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX; - portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX; - portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP; - portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF; - portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS; - portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI; - portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE; - for (int i = 0; i < 5; i++) { - - // set up the pin array first - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8; - if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10") - portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10; - - // now fill in the table - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH") - portduino_config.rfswitch_table[0].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH") - portduino_config.rfswitch_table[1].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH") - portduino_config.rfswitch_table[2].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH") - portduino_config.rfswitch_table[3].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH") - portduino_config.rfswitch_table[4].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH") - portduino_config.rfswitch_table[5].values[i] = HIGH; - if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH") - portduino_config.rfswitch_table[6].values[i] = HIGH; + if (yamlConfig["Config"]) { + if (yamlConfig["Config"]["DisplayMode"]) { + portduino_config.has_configDisplayMode = true; + if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; + } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + } else { + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + } + } } - } - } - readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin); - if (yamlConfig["GPS"]) { - std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); - if (serialPath != "") { - Serial1.setPath(serialPath); - portduino_config.has_gps = 1; - } - } - if (yamlConfig["I2C"]) { - portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); - } - if (yamlConfig["Display"]) { - for (auto &screen_name : portduino_config.screen_names) { - if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) - portduino_config.displayPanel = screen_name.first; - } - portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0); - portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0); + if (yamlConfig["General"]) { + portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200); + portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100); + portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as(""); + portduino_config.available_directory = + (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/"); + if ((yamlConfig["General"]["MACAddress"]).as("") != "" && + (yamlConfig["General"]["MACAddressSource"]).as("") != "") { + std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; + exit(EXIT_FAILURE); + } + portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); + if (portduino_config.mac_address != "") { + portduino_config.mac_address_explicit = true; + } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { + portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as(""); + std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address"); + std::getline(infile, portduino_config.mac_address); + } - readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1); - readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1); - readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1); - readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1); - readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1); - - portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false); - portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false); - portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0); - portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0); - portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false); - portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1); - portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false); - portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000); - if (yamlConfig["Display"]["spidev"]) { - portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); - if (portduino_config.display_spi_dev.length() == 14) { - int x = portduino_config.display_spi_dev.at(11) - '0'; - int y = portduino_config.display_spi_dev.at(13) - '0'; - if (x >= 0 && x < 10 && y >= 0 && y < 10) { - portduino_config.display_spi_dev_int = x + y << 4; - portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int; - } + // https://stackoverflow.com/a/20326454 + portduino_config.mac_address.erase( + std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'), + portduino_config.mac_address.end()); } - } + } catch (YAML::Exception &e) { + std::cout << "*** Exception " << e.what() << std::endl; + return false; } - if (yamlConfig["Touchscreen"]) { - if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") - portduino_config.touchscreenModule = xpt2046; - else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") - portduino_config.touchscreenModule = stmpe610; - else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") - portduino_config.touchscreenModule = gt911; - else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") - portduino_config.touchscreenModule = ft5x06; - - readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1); - readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1); - - portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); - portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1); - portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); - if (yamlConfig["Touchscreen"]["spidev"]) { - portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); - if (portduino_config.touchscreen_spi_dev.length() == 14) { - int x = portduino_config.touchscreen_spi_dev.at(11) - '0'; - int y = portduino_config.touchscreen_spi_dev.at(13) - '0'; - if (x >= 0 && x < 10 && y >= 0 && y < 10) { - portduino_config.touchscreen_spi_dev_int = x + y << 4; - } - } - } - } - if (yamlConfig["Input"]) { - portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as(""); - portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as(""); - - readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin); - readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin); - - if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { - portduino_config.tbDirection = 4; - } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { - portduino_config.tbDirection = 3; - } - } - - if (yamlConfig["Webserver"]) { - portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1); - portduino_config.webserver_root_path = (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); - portduino_config.webserver_ssl_key_path = (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); - portduino_config.webserver_ssl_cert_path = (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); - } - - if (yamlConfig["HostMetrics"]) { - portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0); - portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); - portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); - } - - if (yamlConfig["Config"]) { - if (yamlConfig["Config"]["DisplayMode"]) { - portduino_config.has_configDisplayMode = true; - if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; - } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; - } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - } else { - portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - } - } - } - - if (yamlConfig["General"]) { - portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200); - portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100); - portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as(""); - portduino_config.available_directory = (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/"); - if ((yamlConfig["General"]["MACAddress"]).as("") != "" && (yamlConfig["General"]["MACAddressSource"]).as("") != "") { - std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; - exit(EXIT_FAILURE); - } - portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); - if (portduino_config.mac_address != "") { - portduino_config.mac_address_explicit = true; - } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { - portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as(""); - std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address"); - std::getline(infile, portduino_config.mac_address); - } - - // https://stackoverflow.com/a/20326454 - portduino_config.mac_address.erase(std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'), - portduino_config.mac_address.end()); - } - } catch (YAML::Exception &e) { - std::cout << "*** Exception " << e.what() << std::endl; - return false; - } - return true; + return true; } // https://stackoverflow.com/questions/874134/find-out-if-string-ends-with-another-string-in-c -static bool ends_with(std::string_view str, std::string_view suffix) { - return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +static bool ends_with(std::string_view str, std::string_view suffix) +{ + return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; } -bool MAC_from_string(std::string mac_str, uint8_t *dmac) { - mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end()); - if (mac_str.length() == 12) { - dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16); - dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16); - dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16); - dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16); - dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16); - dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16); - return true; - } else { - return false; - } +bool MAC_from_string(std::string mac_str, uint8_t *dmac) +{ + mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end()); + if (mac_str.length() == 12) { + dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16); + dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16); + dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16); + dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16); + dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16); + dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16); + return true; + } else { + return false; + } } -std::string exec(const char *cmd) { // https://stackoverflow.com/a/478960 - std::array buffer; - std::string result; - std::unique_ptr pipe(popen(cmd, "r"), pclose); - if (!pipe) { - throw std::runtime_error("popen() failed!"); - } - while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { - result += buffer.data(); - } - return result; +std::string exec(const char *cmd) +{ // https://stackoverflow.com/a/478960 + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; } -void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault) { - if (sourceNode.IsMap()) { - destPin.enabled = true; - destPin.pin = sourceNode["pin"].as(pinDefault); - destPin.line = sourceNode["line"].as(destPin.pin); - destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip); - } else if (sourceNode) { // backwards API compatibility - destPin.enabled = true; - destPin.pin = sourceNode.as(pinDefault); - destPin.line = destPin.pin; - destPin.gpiochip = portduino_config.lora_default_gpiochip; - } +void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault) +{ + if (sourceNode.IsMap()) { + destPin.enabled = true; + destPin.pin = sourceNode["pin"].as(pinDefault); + destPin.line = sourceNode["line"].as(destPin.pin); + destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip); + } else if (sourceNode) { // backwards API compatibility + destPin.enabled = true; + destPin.pin = sourceNode.as(pinDefault); + destPin.line = destPin.pin; + destPin.gpiochip = portduino_config.lora_default_gpiochip; + } } diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 46bf30a9f..9335be90a 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -12,26 +12,38 @@ // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} // YAML paths are relative to `meshtastic/available.d` -inline const std::unordered_map configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, - {"MESHSTICK", "lora-meshstick-1262.yaml"}, - {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, - {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, - {"POWERPI", "lora-MeshAdv-900M30S.yaml"}, - {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, - {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; +inline const std::unordered_map configProducts = { + {"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, + {"MESHSTICK", "lora-meshstick-1262.yaml"}, + {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, + {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, + {"POWERPI", "lora-MeshAdv-900M30S.yaml"}, + {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, + {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; enum screen_modules { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; enum touchscreen_modules { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; enum portduino_log_level { level_error, level_warn, level_info, level_debug, level_trace }; -enum lora_module_enum { use_simradio, use_autoconf, use_rf95, use_sx1262, use_sx1268, use_sx1280, use_lr1110, use_lr1120, use_lr1121, use_llcc68 }; +enum lora_module_enum { + use_simradio, + use_autoconf, + use_rf95, + use_sx1262, + use_sx1268, + use_sx1280, + use_lr1110, + use_lr1120, + use_lr1121, + use_llcc68 +}; struct pinMapping { - std::string config_section; - std::string config_name; - int pin = RADIOLIB_NC; - int gpiochip; - int line; - bool enabled = false; + std::string config_section; + std::string config_name; + int pin = RADIOLIB_NC; + int gpiochip; + int line; + bool enabled = false; }; extern std::ofstream traceFile; @@ -47,446 +59,448 @@ void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault std::string exec(const char *cmd); extern struct portduino_config_struct { - // Lora - std::map loraModules = { - {use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, - {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; - - std::map screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"}, {st7735, "ST7735"}, - {st7735s, "ST7735S"}, {st7796, "ST7796"}, {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, - {ili9486, "ILI9486"}, {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}}; - - lora_module_enum lora_module; - bool has_rfswitch_table = false; - uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; - Module::RfSwitchMode_t rfswitch_table[8]; - bool force_simradio = false; - bool has_device_id = false; - uint8_t device_id[16] = {0}; - std::string lora_spi_dev = ""; - std::string lora_usb_serial_num = ""; - int lora_spi_dev_int = 0; - int lora_default_gpiochip = 0; - int sx126x_max_power = 22; - int sx128x_max_power = 13; - int lr1110_max_power = 22; - int lr1120_max_power = 13; - int rf95_max_power = 20; - bool dio2_as_rf_switch = false; - int dio3_tcxo_voltage = 0; - int lora_usb_pid = 0x5512; - int lora_usb_vid = 0x1A86; - int spiSpeed = 2000000; - pinMapping lora_cs_pin = {"Lora", "CS"}; - pinMapping lora_irq_pin = {"Lora", "IRQ"}; - pinMapping lora_busy_pin = {"Lora", "Busy"}; - pinMapping lora_reset_pin = {"Lora", "Reset"}; - pinMapping lora_txen_pin = {"Lora", "TXen"}; - pinMapping lora_rxen_pin = {"Lora", "RXen"}; - pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; - - // GPS - bool has_gps = false; - - // I2C - std::string i2cdev = ""; - - // Display - std::string display_spi_dev = ""; - int display_spi_dev_int = 0; - int displayBusFrequency = 40000000; - screen_modules displayPanel = no_screen; - int displayWidth = 0; - int displayHeight = 0; - bool displayRGBOrder = false; - bool displayBacklightInvert = false; - bool displayRotate = false; - int displayOffsetRotate = 1; - bool displayInvert = false; - int displayOffsetX = 0; - int displayOffsetY = 0; - pinMapping displayDC = {"Display", "DC"}; - pinMapping displayCS = {"Display", "CS"}; - pinMapping displayBacklight = {"Display", "Backlight"}; - pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"}; - pinMapping displayReset = {"Display", "Reset"}; - - // Touchscreen - std::string touchscreen_spi_dev = ""; - int touchscreen_spi_dev_int = 0; - touchscreen_modules touchscreenModule = no_touchscreen; - int touchscreenI2CAddr = -1; - int touchscreenBusFrequency = 1000000; - int touchscreenRotate = -1; - pinMapping touchscreenCS = {"Touchscreen", "CS"}; - pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"}; - - // Input - std::string keyboardDevice = ""; - std::string pointerDevice = ""; - int tbDirection; - pinMapping userButtonPin = {"Input", "User"}; - pinMapping tbUpPin = {"Input", "TrackballUp"}; - pinMapping tbDownPin = {"Input", "TrackballDown"}; - pinMapping tbLeftPin = {"Input", "TrackballLwft"}; - pinMapping tbRightPin = {"Input", "TrackballRight"}; - pinMapping tbPressPin = {"Input", "TrackballPress"}; - - // Logging - portduino_log_level logoutputlevel = level_debug; - std::string traceFilename; - bool ascii_logs = !isatty(1); - bool ascii_logs_explicit = false; - - std::string JSONFilename; - meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; - - // Webserver - std::string webserver_root_path = ""; - std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; - std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem"; - int webserverport = -1; - - // HostMetrics - std::string hostMetrics_user_command = ""; - int hostMetrics_interval = 0; - int hostMetrics_channel = 0; - - // config - int configDisplayMode = 0; - bool has_configDisplayMode = false; - - // General - std::string mac_address = ""; - bool mac_address_explicit = false; - std::string mac_address_source = ""; - std::string config_directory = ""; - std::string available_directory = "/etc/meshtasticd/available.d/"; - int maxtophone = 100; - int MaxNodes = 200; - - pinMapping *all_pins[20] = {&lora_cs_pin, - &lora_irq_pin, - &lora_busy_pin, - &lora_reset_pin, - &lora_txen_pin, - &lora_rxen_pin, - &lora_sx126x_ant_sw_pin, - &displayDC, - &displayCS, - &displayBacklight, - &displayBacklightPWMChannel, - &displayReset, - &touchscreenCS, - &touchscreenIRQ, - &userButtonPin, - &tbUpPin, - &tbDownPin, - &tbLeftPin, - &tbRightPin, - &tbPressPin}; - - std::string emit_yaml() { - YAML::Emitter out; - out << YAML::BeginMap; - // Lora - out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; + std::map loraModules = { + {use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, + {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; - for (auto lora_pin : all_pins) { - if (lora_pin->config_section == "Lora" && lora_pin->enabled) { - out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; - out << YAML::Key << "line" << YAML::Value << lora_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip; - out << YAML::EndMap; // User - } - } + std::map screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"}, + {st7735, "ST7735"}, {st7735s, "ST7735S"}, {st7796, "ST7796"}, + {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, {ili9486, "ILI9486"}, + {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}}; - if (sx126x_max_power != 22) - out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power; - if (sx128x_max_power != 13) - out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power; - if (lr1110_max_power != 22) - out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power; - if (lr1120_max_power != 13) - out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; - if (rf95_max_power != 20) - out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; - out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; - if (dio3_tcxo_voltage != 0) - out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; - if (lora_usb_pid != 0x5512) - out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; - if (lora_usb_vid != 0x1A86) - out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; - if (lora_spi_dev != "") - out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; - if (lora_usb_serial_num != "") - out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; - out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; - if (rfswitch_dio_pins[0] != RADIOLIB_NC) { - out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; + lora_module_enum lora_module; + bool has_rfswitch_table = false; + uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + Module::RfSwitchMode_t rfswitch_table[8]; + bool force_simradio = false; + bool has_device_id = false; + uint8_t device_id[16] = {0}; + std::string lora_spi_dev = ""; + std::string lora_usb_serial_num = ""; + int lora_spi_dev_int = 0; + int lora_default_gpiochip = 0; + int sx126x_max_power = 22; + int sx128x_max_power = 13; + int lr1110_max_power = 22; + int lr1120_max_power = 13; + int rf95_max_power = 20; + bool dio2_as_rf_switch = false; + int dio3_tcxo_voltage = 0; + int lora_usb_pid = 0x5512; + int lora_usb_vid = 0x1A86; + int spiSpeed = 2000000; + pinMapping lora_cs_pin = {"Lora", "CS"}; + pinMapping lora_irq_pin = {"Lora", "IRQ"}; + pinMapping lora_busy_pin = {"Lora", "Busy"}; + pinMapping lora_reset_pin = {"Lora", "Reset"}; + pinMapping lora_txen_pin = {"Lora", "TXen"}; + pinMapping lora_rxen_pin = {"Lora", "RXen"}; + pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; - out << YAML::Key << "pins"; - out << YAML::Value << YAML::Flow << YAML::BeginSeq; + // GPS + bool has_gps = false; - for (int i = 0; i < 5; i++) { - // set up the pin array first - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5) - out << "DIO5"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6) - out << "DIO6"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7) - out << "DIO7"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8) - out << "DIO8"; - if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10) - out << "DIO10"; - } - out << YAML::EndSeq; - - for (int i = 0; i < 7; i++) { - switch (i) { - case 0: - out << YAML::Key << "MODE_STBY"; - break; - case 1: - out << YAML::Key << "MODE_RX"; - break; - case 2: - out << YAML::Key << "MODE_TX"; - break; - case 3: - out << YAML::Key << "MODE_TX_HP"; - break; - case 4: - out << YAML::Key << "MODE_TX_HF"; - break; - case 5: - out << YAML::Key << "MODE_GNSS"; - break; - case 6: - out << YAML::Key << "MODE_WIFI"; - break; - } - - out << YAML::Value << YAML::Flow << YAML::BeginSeq; - for (int j = 0; j < 5; j++) { - if (rfswitch_table[i].values[j] == HIGH) { - out << "HIGH"; - } else { - out << "LOW"; - } - } - out << YAML::EndSeq; - } - out << YAML::EndMap; // rfswitch_table - } - out << YAML::EndMap; // Lora - - if (i2cdev != "") { - out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; - out << YAML::EndMap; // I2C - } + // I2C + std::string i2cdev = ""; // Display - if (displayPanel != no_screen) { - out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; - for (auto &screen_name : screen_names) { - if (displayPanel == screen_name.first) - out << YAML::Key << "Module" << YAML::Value << screen_name.second; - } - for (auto display_pin : all_pins) { - if (display_pin->config_section == "Display" && display_pin->enabled) { - out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << display_pin->pin; - out << YAML::Key << "line" << YAML::Value << display_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip; - out << YAML::EndMap; - } - } - out << YAML::Key << "spidev" << YAML::Value << display_spi_dev; - out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency; - if (displayWidth) - out << YAML::Key << "Width" << YAML::Value << displayWidth; - if (displayHeight) - out << YAML::Key << "Height" << YAML::Value << displayHeight; - if (displayRGBOrder) - out << YAML::Key << "RGBOrder" << YAML::Value << true; - if (displayBacklightInvert) - out << YAML::Key << "BacklightInvert" << YAML::Value << true; - if (displayRotate) - out << YAML::Key << "Rotate" << YAML::Value << true; - if (displayInvert) - out << YAML::Key << "Invert" << YAML::Value << true; - if (displayOffsetX) - out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX; - if (displayOffsetY) - out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY; - - out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate; - - out << YAML::EndMap; // Display - } + std::string display_spi_dev = ""; + int display_spi_dev_int = 0; + int displayBusFrequency = 40000000; + screen_modules displayPanel = no_screen; + int displayWidth = 0; + int displayHeight = 0; + bool displayRGBOrder = false; + bool displayBacklightInvert = false; + bool displayRotate = false; + int displayOffsetRotate = 1; + bool displayInvert = false; + int displayOffsetX = 0; + int displayOffsetY = 0; + pinMapping displayDC = {"Display", "DC"}; + pinMapping displayCS = {"Display", "CS"}; + pinMapping displayBacklight = {"Display", "Backlight"}; + pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"}; + pinMapping displayReset = {"Display", "Reset"}; // Touchscreen - if (touchscreen_spi_dev != "") { - out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev; - out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency; - switch (touchscreenModule) { - case xpt2046: - out << YAML::Key << "Module" << YAML::Value << "XPT2046"; - case stmpe610: - out << YAML::Key << "Module" << YAML::Value << "STMPE610"; - case gt911: - out << YAML::Key << "Module" << YAML::Value << "GT911"; - case ft5x06: - out << YAML::Key << "Module" << YAML::Value << "FT5x06"; - } - for (auto touchscreen_pin : all_pins) { - if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { - out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; - out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip; - out << YAML::EndMap; - } - } - if (touchscreenRotate != -1) - out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate; - if (touchscreenI2CAddr != -1) - out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr; - out << YAML::EndMap; // Touchscreen - } + std::string touchscreen_spi_dev = ""; + int touchscreen_spi_dev_int = 0; + touchscreen_modules touchscreenModule = no_touchscreen; + int touchscreenI2CAddr = -1; + int touchscreenBusFrequency = 1000000; + int touchscreenRotate = -1; + pinMapping touchscreenCS = {"Touchscreen", "CS"}; + pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"}; // Input - out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap; - if (keyboardDevice != "") - out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice; - if (pointerDevice != "") - out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; + std::string keyboardDevice = ""; + std::string pointerDevice = ""; + int tbDirection; + pinMapping userButtonPin = {"Input", "User"}; + pinMapping tbUpPin = {"Input", "TrackballUp"}; + pinMapping tbDownPin = {"Input", "TrackballDown"}; + pinMapping tbLeftPin = {"Input", "TrackballLwft"}; + pinMapping tbRightPin = {"Input", "TrackballRight"}; + pinMapping tbPressPin = {"Input", "TrackballPress"}; - for (auto input_pin : all_pins) { - if (input_pin->config_section == "Input" && input_pin->enabled) { - out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; - out << YAML::Key << "pin" << YAML::Value << input_pin->pin; - out << YAML::Key << "line" << YAML::Value << input_pin->line; - out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip; - out << YAML::EndMap; - } - } - if (tbDirection == 3) - out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING"; + // Logging + portduino_log_level logoutputlevel = level_debug; + std::string traceFilename; + bool ascii_logs = !isatty(1); + bool ascii_logs_explicit = false; - out << YAML::EndMap; // Input - - out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "LogLevel" << YAML::Value; - switch (logoutputlevel) { - case level_error: - out << "error"; - break; - case level_warn: - out << "warn"; - break; - case level_info: - out << "info"; - break; - case level_debug: - out << "debug"; - break; - case level_trace: - out << "trace"; - break; - } - if (traceFilename != "") - out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; - if (JSONFilename != "") { - out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; - if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; - else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; - else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; - else if (JSONFilter == meshtastic_PortNum_POSITION_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "position"; - else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; - else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; - else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; - else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; - else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; - else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) - out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; - } - if (ascii_logs_explicit) { - out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; - } - out << YAML::EndMap; // Logging + std::string JSONFilename; + meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; // Webserver - if (webserver_root_path != "") { - out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path; - out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path; - out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path; - out << YAML::Key << "Port" << YAML::Value << webserverport; - out << YAML::EndMap; // Webserver - } + std::string webserver_root_path = ""; + std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; + std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem"; + int webserverport = -1; // HostMetrics - if (hostMetrics_user_command != "") { - out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command; - out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval; - out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel; - - out << YAML::EndMap; // HostMetrics - } + std::string hostMetrics_user_command = ""; + int hostMetrics_interval = 0; + int hostMetrics_channel = 0; // config - if (has_configDisplayMode) { - out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; - switch (configDisplayMode) { - case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: - out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: - out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; - break; - } - - out << YAML::EndMap; // Config - } + int configDisplayMode = 0; + bool has_configDisplayMode = false; // General - out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; - if (config_directory != "") - out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; - if (mac_address_explicit) - out << YAML::Key << "MACAddress" << YAML::Value << mac_address; - if (mac_address_source != "") - out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source; - if (available_directory != "") - out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory; - out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone; - out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes; - out << YAML::EndMap; // General - return out.c_str(); - } + std::string mac_address = ""; + bool mac_address_explicit = false; + std::string mac_address_source = ""; + std::string config_directory = ""; + std::string available_directory = "/etc/meshtasticd/available.d/"; + int maxtophone = 100; + int MaxNodes = 200; + + pinMapping *all_pins[20] = {&lora_cs_pin, + &lora_irq_pin, + &lora_busy_pin, + &lora_reset_pin, + &lora_txen_pin, + &lora_rxen_pin, + &lora_sx126x_ant_sw_pin, + &displayDC, + &displayCS, + &displayBacklight, + &displayBacklightPWMChannel, + &displayReset, + &touchscreenCS, + &touchscreenIRQ, + &userButtonPin, + &tbUpPin, + &tbDownPin, + &tbLeftPin, + &tbRightPin, + &tbPressPin}; + + std::string emit_yaml() + { + YAML::Emitter out; + out << YAML::BeginMap; + + // Lora + out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; + + for (auto lora_pin : all_pins) { + if (lora_pin->config_section == "Lora" && lora_pin->enabled) { + out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; + out << YAML::Key << "line" << YAML::Value << lora_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip; + out << YAML::EndMap; // User + } + } + + if (sx126x_max_power != 22) + out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power; + if (sx128x_max_power != 13) + out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power; + if (lr1110_max_power != 22) + out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power; + if (lr1120_max_power != 13) + out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; + if (rf95_max_power != 20) + out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; + out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; + if (dio3_tcxo_voltage != 0) + out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; + if (lora_usb_pid != 0x5512) + out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; + if (lora_usb_vid != 0x1A86) + out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; + if (lora_spi_dev != "") + out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; + if (lora_usb_serial_num != "") + out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; + out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; + if (rfswitch_dio_pins[0] != RADIOLIB_NC) { + out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; + + out << YAML::Key << "pins"; + out << YAML::Value << YAML::Flow << YAML::BeginSeq; + + for (int i = 0; i < 5; i++) { + // set up the pin array first + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5) + out << "DIO5"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6) + out << "DIO6"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7) + out << "DIO7"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8) + out << "DIO8"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10) + out << "DIO10"; + } + out << YAML::EndSeq; + + for (int i = 0; i < 7; i++) { + switch (i) { + case 0: + out << YAML::Key << "MODE_STBY"; + break; + case 1: + out << YAML::Key << "MODE_RX"; + break; + case 2: + out << YAML::Key << "MODE_TX"; + break; + case 3: + out << YAML::Key << "MODE_TX_HP"; + break; + case 4: + out << YAML::Key << "MODE_TX_HF"; + break; + case 5: + out << YAML::Key << "MODE_GNSS"; + break; + case 6: + out << YAML::Key << "MODE_WIFI"; + break; + } + + out << YAML::Value << YAML::Flow << YAML::BeginSeq; + for (int j = 0; j < 5; j++) { + if (rfswitch_table[i].values[j] == HIGH) { + out << "HIGH"; + } else { + out << "LOW"; + } + } + out << YAML::EndSeq; + } + out << YAML::EndMap; // rfswitch_table + } + out << YAML::EndMap; // Lora + + if (i2cdev != "") { + out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; + out << YAML::EndMap; // I2C + } + + // Display + if (displayPanel != no_screen) { + out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; + for (auto &screen_name : screen_names) { + if (displayPanel == screen_name.first) + out << YAML::Key << "Module" << YAML::Value << screen_name.second; + } + for (auto display_pin : all_pins) { + if (display_pin->config_section == "Display" && display_pin->enabled) { + out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << display_pin->pin; + out << YAML::Key << "line" << YAML::Value << display_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip; + out << YAML::EndMap; + } + } + out << YAML::Key << "spidev" << YAML::Value << display_spi_dev; + out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency; + if (displayWidth) + out << YAML::Key << "Width" << YAML::Value << displayWidth; + if (displayHeight) + out << YAML::Key << "Height" << YAML::Value << displayHeight; + if (displayRGBOrder) + out << YAML::Key << "RGBOrder" << YAML::Value << true; + if (displayBacklightInvert) + out << YAML::Key << "BacklightInvert" << YAML::Value << true; + if (displayRotate) + out << YAML::Key << "Rotate" << YAML::Value << true; + if (displayInvert) + out << YAML::Key << "Invert" << YAML::Value << true; + if (displayOffsetX) + out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX; + if (displayOffsetY) + out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY; + + out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate; + + out << YAML::EndMap; // Display + } + + // Touchscreen + if (touchscreen_spi_dev != "") { + out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev; + out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency; + switch (touchscreenModule) { + case xpt2046: + out << YAML::Key << "Module" << YAML::Value << "XPT2046"; + case stmpe610: + out << YAML::Key << "Module" << YAML::Value << "STMPE610"; + case gt911: + out << YAML::Key << "Module" << YAML::Value << "GT911"; + case ft5x06: + out << YAML::Key << "Module" << YAML::Value << "FT5x06"; + } + for (auto touchscreen_pin : all_pins) { + if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { + out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; + out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip; + out << YAML::EndMap; + } + } + if (touchscreenRotate != -1) + out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate; + if (touchscreenI2CAddr != -1) + out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr; + out << YAML::EndMap; // Touchscreen + } + + // Input + out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap; + if (keyboardDevice != "") + out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice; + if (pointerDevice != "") + out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; + + for (auto input_pin : all_pins) { + if (input_pin->config_section == "Input" && input_pin->enabled) { + out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << input_pin->pin; + out << YAML::Key << "line" << YAML::Value << input_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip; + out << YAML::EndMap; + } + } + if (tbDirection == 3) + out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING"; + + out << YAML::EndMap; // Input + + out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "LogLevel" << YAML::Value; + switch (logoutputlevel) { + case level_error: + out << "error"; + break; + case level_warn: + out << "warn"; + break; + case level_info: + out << "info"; + break; + case level_debug: + out << "debug"; + break; + case level_trace: + out << "trace"; + break; + } + if (traceFilename != "") + out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; + if (JSONFilename != "") { + out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; + else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; + else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; + else if (JSONFilter == meshtastic_PortNum_POSITION_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "position"; + else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; + else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; + else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; + else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; + else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; + else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; + } + if (ascii_logs_explicit) { + out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; + } + out << YAML::EndMap; // Logging + + // Webserver + if (webserver_root_path != "") { + out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path; + out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path; + out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path; + out << YAML::Key << "Port" << YAML::Value << webserverport; + out << YAML::EndMap; // Webserver + } + + // HostMetrics + if (hostMetrics_user_command != "") { + out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command; + out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval; + out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel; + + out << YAML::EndMap; // HostMetrics + } + + // config + if (has_configDisplayMode) { + out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; + switch (configDisplayMode) { + case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: + out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: + out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; + break; + } + + out << YAML::EndMap; // Config + } + + // General + out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; + if (config_directory != "") + out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; + if (mac_address_explicit) + out << YAML::Key << "MACAddress" << YAML::Value << mac_address; + if (mac_address_source != "") + out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source; + if (available_directory != "") + out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory; + out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone; + out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes; + out << YAML::EndMap; // General + return out.c_str(); + } } portduino_config; \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 01f4aeea6..6e7fe24cb 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -2,311 +2,341 @@ #include "MeshService.h" #include "Router.h" -SimRadio::SimRadio() : NotifiedWorkerThread("SimRadio") { instance = this; } +SimRadio::SimRadio() : NotifiedWorkerThread("SimRadio") +{ + instance = this; +} SimRadio *SimRadio::instance; -ErrorCode SimRadio::send(meshtastic_MeshPacket *p) { - printPacket("enqueuing for send", p); +ErrorCode SimRadio::send(meshtastic_MeshPacket *p) +{ + printPacket("enqueuing for send", p); - bool dropped = false; - ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; + bool dropped = false; + ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; - if (dropped) { - txDrop++; - } + if (dropped) { + txDrop++; + } - if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks - packetPool.release(p); + if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks + packetPool.release(p); + return res; + } + + // set (random) transmit delay to let others reconfigure their radio, + // to avoid collisions and implement timing-based flooding + LOG_DEBUG("Set random delay before tx"); + setTransmitDelay(); return res; - } - - // set (random) transmit delay to let others reconfigure their radio, - // to avoid collisions and implement timing-based flooding - LOG_DEBUG("Set random delay before tx"); - setTransmitDelay(); - return res; } -void SimRadio::setTransmitDelay() { - meshtastic_MeshPacket *p = txQueue.getFront(); - // We want all sending/receiving to be done by our daemon thread. - // We use a delay here because this packet might have been sent in response to a packet we just received. - // So we want to make sure the other side has had a chance to reconfigure its radio. +void SimRadio::setTransmitDelay() +{ + meshtastic_MeshPacket *p = txQueue.getFront(); + // We want all sending/receiving to be done by our daemon thread. + // We use a delay here because this packet might have been sent in response to a packet we just received. + // So we want to make sure the other side has had a chance to reconfigure its radio. - /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. - * This assumption is valid because of the offset generated by the radio to account for the noise - * floor. - */ - if (p->rx_snr == 0 && p->rx_rssi == 0) { - startTransmitTimer(true); - } else { - // If there is a SNR, start a timer scaled based on that SNR. - LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p); - } + /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. + * This assumption is valid because of the offset generated by the radio to account for the noise + * floor. + */ + if (p->rx_snr == 0 && p->rx_rssi == 0) { + startTransmitTimer(true); + } else { + // If there is a SNR, start a timer scaled based on that SNR. + LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); + startTransmitTimerRebroadcast(p); + } } -void SimRadio::startTransmitTimer(bool withDelay) { - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec(); - // LOG_DEBUG("xmit timer %d", delay); - notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); - } +void SimRadio::startTransmitTimer(bool withDelay) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec(); + // LOG_DEBUG("xmit timer %d", delay); + notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); + } } -void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { - // If we have work to do and the timer wasn't already scheduled, schedule it now - if (!txQueue.empty()) { - uint32_t delayMsec = getTxDelayMsecWeighted(p); - // LOG_DEBUG("xmit timer %d", delay); - notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); - } +void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) +{ + // If we have work to do and the timer wasn't already scheduled, schedule it now + if (!txQueue.empty()) { + uint32_t delayMsec = getTxDelayMsecWeighted(p); + // LOG_DEBUG("xmit timer %d", delay); + notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); + } } -void SimRadio::handleTransmitInterrupt() { - // This can be null if we forced the device to enter standby mode. In that case - // ignore the transmit interrupt - if (sendingPacket) - completeSending(); +void SimRadio::handleTransmitInterrupt() +{ + // This can be null if we forced the device to enter standby mode. In that case + // ignore the transmit interrupt + if (sendingPacket) + completeSending(); - isReceiving = true; - if (receivingPacket) // This happens when we don't consider something a collision if we weren't sending long enough - handleReceiveInterrupt(); + isReceiving = true; + if (receivingPacket) // This happens when we don't consider something a collision if we weren't sending long enough + handleReceiveInterrupt(); } -void SimRadio::completeSending() { - // We are careful to clear sending packet before calling printPacket because - // that can take a long time - auto p = sendingPacket; - sendingPacket = NULL; +void SimRadio::completeSending() +{ + // We are careful to clear sending packet before calling printPacket because + // that can take a long time + auto p = sendingPacket; + sendingPacket = NULL; - if (p) { - txGood++; - if (!isFromUs(p)) - txRelay++; - printPacket("Completed sending", p); + if (p) { + txGood++; + if (!isFromUs(p)) + txRelay++; + printPacket("Completed sending", p); - // We are done sending that packet, release it - packetPool.release(p); - // LOG_DEBUG("Done with send"); - } + // We are done sending that packet, release it + packetPool.release(p); + // LOG_DEBUG("Done with send"); + } } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ -bool SimRadio::canSendImmediately() { - // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). - // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, - // we almost certainly guarantee no one outside will like the packet we are sending. - bool busyTx = sendingPacket != NULL; - bool busyRx = isReceiving && isActivelyReceiving(); +bool SimRadio::canSendImmediately() +{ + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + bool busyTx = sendingPacket != NULL; + bool busyRx = isReceiving && isActivelyReceiving(); - if (busyTx || busyRx) { - if (busyTx) - LOG_WARN("Can not send yet, busyTx"); - if (busyRx) - LOG_WARN("Can not send yet, busyRx"); - return false; - } else - return true; + if (busyTx || busyRx) { + if (busyTx) + LOG_WARN("Can not send yet, busyTx"); + if (busyRx) + LOG_WARN("Can not send yet, busyRx"); + return false; + } else + return true; } -bool SimRadio::isActivelyReceiving() { return receivingPacket != nullptr; } +bool SimRadio::isActivelyReceiving() +{ + return receivingPacket != nullptr; +} -bool SimRadio::isChannelActive() { return receivingPacket != nullptr; } +bool SimRadio::isChannelActive() +{ + return receivingPacket != nullptr; +} /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ -bool SimRadio::cancelSending(NodeNum from, PacketId id) { - auto p = txQueue.remove(from, id); - if (p) - packetPool.release(p); // free the packet we just removed +bool SimRadio::cancelSending(NodeNum from, PacketId id) +{ + auto p = txQueue.remove(from, id); + if (p) + packetPool.release(p); // free the packet we just removed - bool result = (p != NULL); - LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); - return result; + bool result = (p != NULL); + LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); + return result; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ -bool SimRadio::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } +bool SimRadio::findInTxQueue(NodeNum from, PacketId id) +{ + return txQueue.find(from, id); +} -void SimRadio::onNotify(uint32_t notification) { - switch (notification) { - case ISR_TX: - handleTransmitInterrupt(); - // LOG_DEBUG("tx complete - starting timer"); - startTransmitTimer(); - break; - case ISR_RX: - handleReceiveInterrupt(); - // LOG_DEBUG("rx complete - starting timer"); - startTransmitTimer(); - break; - case TRANSMIT_DELAY_COMPLETED: - if (receivingPacket) { // This happens when we had a timer pending and we started receiving - handleReceiveInterrupt(); - startTransmitTimer(); - break; - } - LOG_DEBUG("delay done"); - - // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread - // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? - if (!txQueue.empty()) { - if (!canSendImmediately()) { - // LOG_DEBUG("Currently Rx/Tx-ing: set random delay"); - setTransmitDelay(); // currently Rx/Tx-ing: reset random delay - } else { - if (isChannelActive()) { // check if there is currently a LoRa packet on the channel - // LOG_DEBUG("Channel is active: set random delay"); - setTransmitDelay(); // reset random delay - } else { - // Send any outgoing packets we have ready - meshtastic_MeshPacket *txp = txQueue.dequeue(); - assert(txp); - startSend(txp); - // Packet has been sent, count it toward our TX airtime utilization. - uint32_t xmitMsec = RadioInterface::getPacketTime(txp); - airTime->logAirtime(TX_LOG, xmitMsec); - - notifyLater(xmitMsec, ISR_TX, false); // Model the time it is busy sending +void SimRadio::onNotify(uint32_t notification) +{ + switch (notification) { + case ISR_TX: + handleTransmitInterrupt(); + // LOG_DEBUG("tx complete - starting timer"); + startTransmitTimer(); + break; + case ISR_RX: + handleReceiveInterrupt(); + // LOG_DEBUG("rx complete - starting timer"); + startTransmitTimer(); + break; + case TRANSMIT_DELAY_COMPLETED: + if (receivingPacket) { // This happens when we had a timer pending and we started receiving + handleReceiveInterrupt(); + startTransmitTimer(); + break; } - } - } else { - // LOG_DEBUG("done with txqueue"); + LOG_DEBUG("delay done"); + + // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread + // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? + if (!txQueue.empty()) { + if (!canSendImmediately()) { + // LOG_DEBUG("Currently Rx/Tx-ing: set random delay"); + setTransmitDelay(); // currently Rx/Tx-ing: reset random delay + } else { + if (isChannelActive()) { // check if there is currently a LoRa packet on the channel + // LOG_DEBUG("Channel is active: set random delay"); + setTransmitDelay(); // reset random delay + } else { + // Send any outgoing packets we have ready + meshtastic_MeshPacket *txp = txQueue.dequeue(); + assert(txp); + startSend(txp); + // Packet has been sent, count it toward our TX airtime utilization. + uint32_t xmitMsec = RadioInterface::getPacketTime(txp); + airTime->logAirtime(TX_LOG, xmitMsec); + + notifyLater(xmitMsec, ISR_TX, false); // Model the time it is busy sending + } + } + } else { + // LOG_DEBUG("done with txqueue"); + } + break; + default: + assert(0); // We expected to receive a valid notification from the ISR } - break; - default: - assert(0); // We expected to receive a valid notification from the ISR - } } /** start an immediate transmit */ -void SimRadio::startSend(meshtastic_MeshPacket *txp) { - printPacket("Start low level send", txp); - isReceiving = false; - size_t numbytes = beginSending(txp); - meshtastic_MeshPacket *p = packetPool.allocCopy(*txp); - perhapsDecode(p); - meshtastic_Compressed c = meshtastic_Compressed_init_default; - c.portnum = p->decoded.portnum; - // LOG_DEBUG("Send back to simulator with portNum %d", p->decoded.portnum); - if (p->decoded.payload.size <= sizeof(c.data.bytes)) { - memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size); - c.data.size = p->decoded.payload.size; - } else { - LOG_WARN("Payload size larger than compressed message allows! Send empty payload"); - } - p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Compressed_msg, &c); - p->decoded.portnum = meshtastic_PortNum_SIMULATOR_APP; +void SimRadio::startSend(meshtastic_MeshPacket *txp) +{ + printPacket("Start low level send", txp); + isReceiving = false; + size_t numbytes = beginSending(txp); + meshtastic_MeshPacket *p = packetPool.allocCopy(*txp); + perhapsDecode(p); + meshtastic_Compressed c = meshtastic_Compressed_init_default; + c.portnum = p->decoded.portnum; + // LOG_DEBUG("Send back to simulator with portNum %d", p->decoded.portnum); + if (p->decoded.payload.size <= sizeof(c.data.bytes)) { + memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size); + c.data.size = p->decoded.payload.size; + } else { + LOG_WARN("Payload size larger than compressed message allows! Send empty payload"); + } + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Compressed_msg, &c); + p->decoded.portnum = meshtastic_PortNum_SIMULATOR_APP; - service->sendQueueStatusToPhone(router->getQueueStatus(), 0, p->id); - service->sendToPhone(p); // Sending back to simulator - service->loop(); // Process the send immediately + service->sendQueueStatusToPhone(router->getQueueStatus(), 0, p->id); + service->sendToPhone(p); // Sending back to simulator + service->loop(); // Process the send immediately } // Simulates device received a packet via the LoRa chip -void SimRadio::unpackAndReceive(meshtastic_MeshPacket &p) { - // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first - meshtastic_Compressed scratch; - meshtastic_Compressed *decoded = NULL; - if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - memset(&scratch, 0, sizeof(scratch)); - p.decoded.payload.size = pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch); - if (p.decoded.payload.size) { - decoded = &scratch; - // Extract the original payload and replace - memcpy(&p.decoded.payload, &decoded->data, sizeof(decoded->data)); - // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum - p.decoded.portnum = decoded->portnum; - } else - LOG_ERROR("Error decoding proto for simulator message!"); - } - // Let SimRadio receive as if it did via its LoRa chip - startReceive(&p); +void SimRadio::unpackAndReceive(meshtastic_MeshPacket &p) +{ + // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first + meshtastic_Compressed scratch; + meshtastic_Compressed *decoded = NULL; + if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + memset(&scratch, 0, sizeof(scratch)); + p.decoded.payload.size = + pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch); + if (p.decoded.payload.size) { + decoded = &scratch; + // Extract the original payload and replace + memcpy(&p.decoded.payload, &decoded->data, sizeof(decoded->data)); + // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum + p.decoded.portnum = decoded->portnum; + } else + LOG_ERROR("Error decoding proto for simulator message!"); + } + // Let SimRadio receive as if it did via its LoRa chip + startReceive(&p); } -void SimRadio::startReceive(meshtastic_MeshPacket *p) { +void SimRadio::startReceive(meshtastic_MeshPacket *p) +{ #ifdef USERPREFS_SIMRADIO_EMULATE_COLLISIONS - if (isActivelyReceiving()) { - LOG_WARN("Collision detected, dropping current and previous packet!"); - rxBad++; - airTime->logAirtime(RX_ALL_LOG, getPacketTime(receivingPacket, true)); - packetPool.release(receivingPacket); - receivingPacket = nullptr; - return; - } else if (sendingPacket) { - uint32_t airtimeLeft = tillRun(millis()); - if (airtimeLeft <= 0) { - LOG_WARN("Transmitting packet was already done"); - handleTransmitInterrupt(); // Finish sending first - } else if ((interval - airtimeLeft) > preambleTimeMsec) { - // Only if transmitting for longer than preamble there is a collision - // (channel should actually be detected as active otherwise) - LOG_WARN("Collision detected during transmission!"); - return; + if (isActivelyReceiving()) { + LOG_WARN("Collision detected, dropping current and previous packet!"); + rxBad++; + airTime->logAirtime(RX_ALL_LOG, getPacketTime(receivingPacket, true)); + packetPool.release(receivingPacket); + receivingPacket = nullptr; + return; + } else if (sendingPacket) { + uint32_t airtimeLeft = tillRun(millis()); + if (airtimeLeft <= 0) { + LOG_WARN("Transmitting packet was already done"); + handleTransmitInterrupt(); // Finish sending first + } else if ((interval - airtimeLeft) > preambleTimeMsec) { + // Only if transmitting for longer than preamble there is a collision + // (channel should actually be detected as active otherwise) + LOG_WARN("Collision detected during transmission!"); + return; + } } - } - isReceiving = true; - receivingPacket = packetPool.allocCopy(*p); - uint32_t airtimeMsec = getPacketTime(p, true); - notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving + isReceiving = true; + receivingPacket = packetPool.allocCopy(*p); + uint32_t airtimeMsec = getPacketTime(p, true); + notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving #else - isReceiving = true; - receivingPacket = packetPool.allocCopy(*p); - handleReceiveInterrupt(); // Simulate receiving the packet immediately - startTransmitTimer(); + isReceiving = true; + receivingPacket = packetPool.allocCopy(*p); + handleReceiveInterrupt(); // Simulate receiving the packet immediately + startTransmitTimer(); #endif } -meshtastic_QueueStatus SimRadio::getQueueStatus() { - meshtastic_QueueStatus qs; +meshtastic_QueueStatus SimRadio::getQueueStatus() +{ + meshtastic_QueueStatus qs; - qs.res = qs.mesh_packet_id = 0; - qs.free = txQueue.getFree(); - qs.maxlen = txQueue.getMaxLen(); + qs.res = qs.mesh_packet_id = 0; + qs.free = txQueue.getFree(); + qs.maxlen = txQueue.getMaxLen(); - return qs; + return qs; } -void SimRadio::handleReceiveInterrupt() { - if (receivingPacket == nullptr) { - return; - } +void SimRadio::handleReceiveInterrupt() +{ + if (receivingPacket == nullptr) { + return; + } - if (!isReceiving) { - LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode"); - return; - } + if (!isReceiving) { + LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode"); + return; + } - LOG_DEBUG("HANDLE RECEIVE INTERRUPT"); - rxGood++; + LOG_DEBUG("HANDLE RECEIVE INTERRUPT"); + rxGood++; - meshtastic_MeshPacket *mp = packetPool.allocCopy(*receivingPacket); // keep a copy in packetPool - packetPool.release(receivingPacket); // release the original - receivingPacket = nullptr; + meshtastic_MeshPacket *mp = packetPool.allocCopy(*receivingPacket); // keep a copy in packetPool + packetPool.release(receivingPacket); // release the original + receivingPacket = nullptr; - printPacket("Lora RX", mp); + printPacket("Lora RX", mp); - airTime->logAirtime(RX_LOG, RadioInterface::getPacketTime(mp, true)); + airTime->logAirtime(RX_LOG, RadioInterface::getPacketTime(mp, true)); - deliverToReceiver(mp); + deliverToReceiver(mp); } -size_t SimRadio::getPacketLength(meshtastic_MeshPacket *mp) { - auto &p = mp->decoded; - return (size_t)p.payload.size + sizeof(PacketHeader); +size_t SimRadio::getPacketLength(meshtastic_MeshPacket *mp) +{ + auto &p = mp->decoded; + return (size_t)p.payload.size + sizeof(PacketHeader); } -int16_t SimRadio::readData(uint8_t *data, size_t len) { - int16_t state = RADIOLIB_ERR_NONE; +int16_t SimRadio::readData(uint8_t *data, size_t len) +{ + int16_t state = RADIOLIB_ERR_NONE; - if (state == RADIOLIB_ERR_NONE) { - // add null terminator - data[len] = 0; - } + if (state == RADIOLIB_ERR_NONE) { + // add null terminator + data[len] = 0; + } - return state; + return state; } /** @@ -316,18 +346,20 @@ int16_t SimRadio::readData(uint8_t *data, size_t len) { * * @return num msecs for the packet */ -uint32_t SimRadio::getPacketTime(uint32_t pl, bool received) { - float bandwidthHz = bw * 1000.0f; - bool headDisable = false; // we currently always use the header - float tSym = (1 << sf) / bandwidthHz; +uint32_t SimRadio::getPacketTime(uint32_t pl, bool received) +{ + float bandwidthHz = bw * 1000.0f; + bool headDisable = false; // we currently always use the header + float tSym = (1 << sf) / bandwidthHz; - bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms + bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms - float tPreamble = (preambleLength + 4.25f) * tSym; - float numPayloadSym = 8 + max(ceilf(((8.0f * pl - 4 * sf + 28 + 16 - 20 * headDisable) / (4 * (sf - 2 * lowDataOptEn))) * cr), 0.0f); - float tPayload = numPayloadSym * tSym; - float tPacket = tPreamble + tPayload; + float tPreamble = (preambleLength + 4.25f) * tSym; + float numPayloadSym = + 8 + max(ceilf(((8.0f * pl - 4 * sf + 28 + 16 - 20 * headDisable) / (4 * (sf - 2 * lowDataOptEn))) * cr), 0.0f); + float tPayload = numPayloadSym * tSym; + float tPacket = tPreamble + tPayload; - uint32_t msecs = tPacket * 1000; - return msecs; + uint32_t msecs = tPacket * 1000; + return msecs; } \ No newline at end of file diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index 856f76f49..6f80989da 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -7,89 +7,90 @@ #include -class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThread { - enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; +class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThread +{ + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX, TRANSMIT_DELAY_COMPLETED }; - MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); + MeshPacketQueue txQueue = MeshPacketQueue(MAX_TX_QUEUE); -public: - SimRadio(); + public: + SimRadio(); - /** MeshService needs this to find our active instance - */ - static SimRadio *instance; + /** MeshService needs this to find our active instance + */ + static SimRadio *instance; - virtual ErrorCode send(meshtastic_MeshPacket *p) override; + virtual ErrorCode send(meshtastic_MeshPacket *p) override; - /** can we detect a LoRa preamble on the current channel? */ - virtual bool isChannelActive(); + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive(); - /** are we actively receiving a packet (only called during receiving state) - * This method is only public to facilitate debugging. Do not call. - */ - virtual bool isActivelyReceiving(); + /** are we actively receiving a packet (only called during receiving state) + * This method is only public to facilitate debugging. Do not call. + */ + virtual bool isActivelyReceiving(); - /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ - virtual bool cancelSending(NodeNum from, PacketId id) override; + /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ + virtual bool cancelSending(NodeNum from, PacketId id) override; - /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ - virtual bool findInTxQueue(NodeNum from, PacketId id) override; + /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ + virtual bool findInTxQueue(NodeNum from, PacketId id) override; - /** - * Start waiting to receive a message - * - * External functions can call this method to wake the device from sleep. - */ - virtual void startReceive(meshtastic_MeshPacket *p); + /** + * Start waiting to receive a message + * + * External functions can call this method to wake the device from sleep. + */ + virtual void startReceive(meshtastic_MeshPacket *p); - meshtastic_QueueStatus getQueueStatus() override; + meshtastic_QueueStatus getQueueStatus() override; - // Convert Compressed_msg to normal msg and receive it - void unpackAndReceive(meshtastic_MeshPacket &p); + // Convert Compressed_msg to normal msg and receive it + void unpackAndReceive(meshtastic_MeshPacket &p); - /** - * Debugging counts - */ - uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; - uint16_t txDrop = 0; + /** + * Debugging counts + */ + uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; + uint16_t txDrop = 0; -protected: - /// are _trying_ to receive a packet currently (note - we might just be waiting for one) - bool isReceiving = true; + protected: + /// are _trying_ to receive a packet currently (note - we might just be waiting for one) + bool isReceiving = true; -private: - void setTransmitDelay(); + private: + void setTransmitDelay(); - /** random timer with certain min. and max. settings */ - void startTransmitTimer(bool withDelay = true); + /** random timer with certain min. and max. settings */ + void startTransmitTimer(bool withDelay = true); - /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); + /** timer scaled to SNR of to be flooded packet */ + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); - void handleTransmitInterrupt(); - void handleReceiveInterrupt(); + void handleTransmitInterrupt(); + void handleReceiveInterrupt(); - void onNotify(uint32_t notification); + void onNotify(uint32_t notification); - // start an immediate transmit - virtual void startSend(meshtastic_MeshPacket *txp); + // start an immediate transmit + virtual void startSend(meshtastic_MeshPacket *txp); - // derive packet length - size_t getPacketLength(meshtastic_MeshPacket *p); + // derive packet length + size_t getPacketLength(meshtastic_MeshPacket *p); - int16_t readData(uint8_t *str, size_t len); + int16_t readData(uint8_t *str, size_t len); - meshtastic_MeshPacket *receivingPacket = nullptr; // The packet we are currently receiving + meshtastic_MeshPacket *receivingPacket = nullptr; // The packet we are currently receiving -protected: - /** Could we send right now (i.e. either not actively receiving or transmitting)? */ - virtual bool canSendImmediately(); + protected: + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately(); - /** - * If a send was in progress finish it and return the buffer to the pool */ - void completeSending(); + /** + * If a send was in progress finish it and return the buffer to the pool */ + void completeSending(); - virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; + virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; }; extern SimRadio *simRadio; \ No newline at end of file diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index 02b8895dd..ce2a5cfd3 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -23,132 +23,146 @@ // the HAL must inherit from the base RadioLibHal class // and implement all of its virtual methods -class Ch341Hal : public RadioLibHal { -public: - // default constructor - initializes the base HAL and any needed private members - explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, uint32_t spiSpeed = 2000000, - uint8_t spiDevice = 0, uint8_t gpioDevice = 0) - : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) { - if (serial != "") { - strncpy(pinedio.serial_number, serial.c_str(), 8); - pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1); - } - // LOG_INFO("USB Serial: %s", pinedio.serial_number); +class Ch341Hal : public RadioLibHal +{ + public: + // default constructor - initializes the base HAL and any needed private members + explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, + uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) + : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) + { + if (serial != "") { + strncpy(pinedio.serial_number, serial.c_str(), 8); + pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1); + } + // LOG_INFO("USB Serial: %s", pinedio.serial_number); - // There is no vendor with 0x0 -> so check - if (vid != 0x0) { - pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid); - pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid); - } - int32_t ret = pinedio_init(&pinedio, NULL); - if (ret != 0) { - std::string s = "Could not open SPI: "; - throw(s + std::to_string(ret)); + // There is no vendor with 0x0 -> so check + if (vid != 0x0) { + pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid); + pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid); + } + int32_t ret = pinedio_init(&pinedio, NULL); + if (ret != 0) { + std::string s = "Could not open SPI: "; + throw(s + std::to_string(ret)); + } + + pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); + pinedio_set_pin_mode(&pinedio, 3, true); + pinedio_set_pin_mode(&pinedio, 5, true); } - pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); - pinedio_set_pin_mode(&pinedio, 3, true); - pinedio_set_pin_mode(&pinedio, 5, true); - } + ~Ch341Hal() { pinedio_deinit(&pinedio); } - ~Ch341Hal() { pinedio_deinit(&pinedio); } - - void getSerialString(char *_serial, size_t len) { - len = len > 8 ? 8 : len; - strncpy(_serial, pinedio.serial_number, len); - } - - void getProductString(char *_product_string, size_t len) { - len = len > 95 ? 95 : len; - strncpy(_product_string, pinedio.product_string, len); - } - - void init() override {} - void term() override {} - - // GPIO-related methods (pinMode, digitalWrite etc.) should check - // RADIOLIB_NC as an alias for non-connected pins - void pinMode(uint32_t pin, uint32_t mode) override { - if (pin == RADIOLIB_NC) { - return; + void getSerialString(char *_serial, size_t len) + { + len = len > 8 ? 8 : len; + strncpy(_serial, pinedio.serial_number, len); } - pinedio_set_pin_mode(&pinedio, pin, mode); - } - void digitalWrite(uint32_t pin, uint32_t value) override { - if (pin == RADIOLIB_NC) { - return; + void getProductString(char *_product_string, size_t len) + { + len = len > 95 ? 95 : len; + strncpy(_product_string, pinedio.product_string, len); } - pinedio_digital_write(&pinedio, pin, value); - } - uint32_t digitalRead(uint32_t pin) override { - if (pin == RADIOLIB_NC) { - return 0; + void init() override {} + void term() override {} + + // GPIO-related methods (pinMode, digitalWrite etc.) should check + // RADIOLIB_NC as an alias for non-connected pins + void pinMode(uint32_t pin, uint32_t mode) override + { + if (pin == RADIOLIB_NC) { + return; + } + pinedio_set_pin_mode(&pinedio, pin, mode); } - return pinedio_digital_read(&pinedio, pin); - } - void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { - if (interruptNum == RADIOLIB_NC) { - return; + void digitalWrite(uint32_t pin, uint32_t value) override + { + if (pin == RADIOLIB_NC) { + return; + } + pinedio_digital_write(&pinedio, pin, value); } - // LOG_DEBUG("Attach interrupt to pin %d", interruptNum); - pinedio_attach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum, (pinedio_int_mode)mode, interruptCb); - } - void detachInterrupt(uint32_t interruptNum) override { - if (interruptNum == RADIOLIB_NC) { - return; + uint32_t digitalRead(uint32_t pin) override + { + if (pin == RADIOLIB_NC) { + return 0; + } + return pinedio_digital_read(&pinedio, pin); } - // LOG_DEBUG("Detach interrupt from pin %d", interruptNum); - pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum); - } - void delay(unsigned long ms) override { delayMicroseconds(ms * 1000); } - - void delayMicroseconds(unsigned long us) override { - if (us == 0) { - sched_yield(); - return; + void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override + { + if (interruptNum == RADIOLIB_NC) { + return; + } + // LOG_DEBUG("Attach interrupt to pin %d", interruptNum); + pinedio_attach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum, (pinedio_int_mode)mode, interruptCb); } - usleep(us); - } - void yield() override { sched_yield(); } - - unsigned long millis() override { - struct timeval tv; - gettimeofday(&tv, NULL); - return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL); - } - - unsigned long micros() override { - struct timeval tv; - gettimeofday(&tv, NULL); - return (tv.tv_sec * 1000000ULL) + tv.tv_usec; - } - - long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override { - std::cerr << "pulseIn for pin " << pin << "is not supported!" << std::endl; - return 0; - } - - void spiBegin() {} - void spiBeginTransaction() {} - - void spiTransfer(uint8_t *out, size_t len, uint8_t *in) { - int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); - if (ret < 0) { - std::cerr << "Could not perform SPI transfer: " << ret << std::endl; + void detachInterrupt(uint32_t interruptNum) override + { + if (interruptNum == RADIOLIB_NC) { + return; + } + // LOG_DEBUG("Detach interrupt from pin %d", interruptNum); + pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum); } - } - void spiEndTransaction() {} - void spiEnd() {} + void delay(unsigned long ms) override { delayMicroseconds(ms * 1000); } -private: - pinedio_inst pinedio = {0}; + void delayMicroseconds(unsigned long us) override + { + if (us == 0) { + sched_yield(); + return; + } + usleep(us); + } + + void yield() override { sched_yield(); } + + unsigned long millis() override + { + struct timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL); + } + + unsigned long micros() override + { + struct timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000000ULL) + tv.tv_usec; + } + + long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override + { + std::cerr << "pulseIn for pin " << pin << "is not supported!" << std::endl; + return 0; + } + + void spiBegin() {} + void spiBeginTransaction() {} + + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) + { + int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); + if (ret < 0) { + std::cerr << "Could not perform SPI transfer: " << ret << std::endl; + } + } + + void spiEndTransaction() {} + void spiEnd() {} + + private: + pinedio_inst pinedio = {0}; }; #endif diff --git a/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h b/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h index 256f37128..e1e014f33 100644 --- a/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h +++ b/src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h @@ -19,11 +19,11 @@ extern "C" { * * Ring Oscillator (ROSC) API * - * A Ring Oscillator is an on-chip oscillator that requires no external crystal. Instead, the output is generated from a - * series of inverters that are chained together to create a feedback loop. RP2040 boots from the ring oscillator - * initially, meaning the first stages of the bootrom, including booting from SPI flash, will be clocked by the ring - * oscillator. If your design has a crystal oscillator, you’ll likely want to switch to this as your reference clock as - * soon as possible, because the frequency is more accurate than the ring oscillator. + * A Ring Oscillator is an on-chip oscillator that requires no external crystal. Instead, the output is generated from a series of + * inverters that are chained together to create a feedback loop. RP2040 boots from the ring oscillator initially, meaning the + * first stages of the bootrom, including booting from SPI flash, will be clocked by the ring oscillator. If your design has a + * crystal oscillator, you’ll likely want to switch to this as your reference clock as soon as possible, because the frequency is + * more accurate than the ring oscillator. */ /*! \brief Set frequency of the Ring Oscillator @@ -68,15 +68,22 @@ uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz); void rosc_set_div(uint32_t div); -inline static void rosc_clear_bad_write(void) { hw_clear_bits(&rosc_hw->status, ROSC_STATUS_BADWRITE_BITS); } +inline static void rosc_clear_bad_write(void) +{ + hw_clear_bits(&rosc_hw->status, ROSC_STATUS_BADWRITE_BITS); +} -inline static bool rosc_write_okay(void) { return !(rosc_hw->status & ROSC_STATUS_BADWRITE_BITS); } +inline static bool rosc_write_okay(void) +{ + return !(rosc_hw->status & ROSC_STATUS_BADWRITE_BITS); +} -inline static void rosc_write(io_rw_32 *addr, uint32_t value) { - rosc_clear_bad_write(); - assert(rosc_write_okay()); - *addr = value; - assert(rosc_write_okay()); +inline static void rosc_write(io_rw_32 *addr, uint32_t value) +{ + rosc_clear_bad_write(); + assert(rosc_write_okay()); + *addr = value; + assert(rosc_write_okay()); }; #ifdef __cplusplus diff --git a/src/platform/rp2xx0/hardware_rosc/rosc.c b/src/platform/rp2xx0/hardware_rosc/rosc.c index 214edf686..f79929f8d 100644 --- a/src/platform/rp2xx0/hardware_rosc/rosc.c +++ b/src/platform/rp2xx0/hardware_rosc/rosc.c @@ -12,50 +12,59 @@ // Given a ROSC delay stage code, return the next-numerically-higher code. // Top result bit is set when called on maximum ROSC code. -uint32_t next_rosc_code(uint32_t code) { return ((code | 0x08888888u) + 1u) & 0xf7777777u; } +uint32_t next_rosc_code(uint32_t code) +{ + return ((code | 0x08888888u) + 1u) & 0xf7777777u; +} -uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz) { - // TODO: This could be a lot better - rosc_set_div(1); - for (uint32_t code = 0; code <= 0x77777777u; code = next_rosc_code(code)) { - rosc_set_freq(code); - uint rosc_mhz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC) / 1000; - if ((rosc_mhz >= low_mhz) && (rosc_mhz <= high_mhz)) { - return rosc_mhz; +uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz) +{ + // TODO: This could be a lot better + rosc_set_div(1); + for (uint32_t code = 0; code <= 0x77777777u; code = next_rosc_code(code)) { + rosc_set_freq(code); + uint rosc_mhz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC) / 1000; + if ((rosc_mhz >= low_mhz) && (rosc_mhz <= high_mhz)) { + return rosc_mhz; + } } - } - return 0; + return 0; } -void rosc_set_div(uint32_t div) { - assert(div <= 31 && div >= 1); - rosc_write(&rosc_hw->div, ROSC_DIV_VALUE_PASS + div); +void rosc_set_div(uint32_t div) +{ + assert(div <= 31 && div >= 1); + rosc_write(&rosc_hw->div, ROSC_DIV_VALUE_PASS + div); } -void rosc_set_freq(uint32_t code) { - rosc_write(&rosc_hw->freqa, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code & 0xffffu)); - rosc_write(&rosc_hw->freqb, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code >> 16u)); +void rosc_set_freq(uint32_t code) +{ + rosc_write(&rosc_hw->freqa, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code & 0xffffu)); + rosc_write(&rosc_hw->freqb, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code >> 16u)); } -void rosc_set_range(uint range) { - // Range should use enumvals from the headers and thus have the password correct - rosc_write(&rosc_hw->ctrl, (ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB) | range); +void rosc_set_range(uint range) +{ + // Range should use enumvals from the headers and thus have the password correct + rosc_write(&rosc_hw->ctrl, (ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB) | range); } -void rosc_disable(void) { - uint32_t tmp = rosc_hw->ctrl; - tmp &= (~ROSC_CTRL_ENABLE_BITS); - tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB); - rosc_write(&rosc_hw->ctrl, tmp); - // Wait for stable to go away - while (rosc_hw->status & ROSC_STATUS_STABLE_BITS) - ; +void rosc_disable(void) +{ + uint32_t tmp = rosc_hw->ctrl; + tmp &= (~ROSC_CTRL_ENABLE_BITS); + tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB); + rosc_write(&rosc_hw->ctrl, tmp); + // Wait for stable to go away + while (rosc_hw->status & ROSC_STATUS_STABLE_BITS) + ; } -void rosc_set_dormant(void) { - // WARNING: This stops the rosc until woken up by an irq - rosc_write(&rosc_hw->dormant, ROSC_DORMANT_VALUE_DORMANT); - // Wait for it to become stable once woken up - while (!(rosc_hw->status & ROSC_STATUS_STABLE_BITS)) - ; +void rosc_set_dormant(void) +{ + // WARNING: This stops the rosc until woken up by an irq + rosc_write(&rosc_hw->dormant, ROSC_DORMANT_VALUE_DORMANT); + // Wait for it to become stable once woken up + while (!(rosc_hw->status & ROSC_STATUS_STABLE_BITS)) + ; } \ No newline at end of file diff --git a/src/platform/rp2xx0/main-rp2xx0.cpp b/src/platform/rp2xx0/main-rp2xx0.cpp index 1e0bde780..6c73e385a 100644 --- a/src/platform/rp2xx0/main-rp2xx0.cpp +++ b/src/platform/rp2xx0/main-rp2xx0.cpp @@ -10,133 +10,148 @@ static bool awake; -static void sleep_callback(void) { awake = true; } - -void epoch_to_datetime(time_t epoch, datetime_t *dt) { - struct tm *tm_info; - - tm_info = gmtime(&epoch); - dt->year = tm_info->tm_year; - dt->month = tm_info->tm_mon + 1; - dt->day = tm_info->tm_mday; - dt->dotw = tm_info->tm_wday; - dt->hour = tm_info->tm_hour; - dt->min = tm_info->tm_min; - dt->sec = tm_info->tm_sec; +static void sleep_callback(void) +{ + awake = true; } -void debug_date(datetime_t t) { - LOG_DEBUG("%d %d %d %d %d %d %d", t.year, t.month, t.day, t.hour, t.min, t.sec, t.dotw); - uart_default_tx_wait_blocking(); +void epoch_to_datetime(time_t epoch, datetime_t *dt) +{ + struct tm *tm_info; + + tm_info = gmtime(&epoch); + dt->year = tm_info->tm_year; + dt->month = tm_info->tm_mon + 1; + dt->day = tm_info->tm_mday; + dt->dotw = tm_info->tm_wday; + dt->hour = tm_info->tm_hour; + dt->min = tm_info->tm_min; + dt->sec = tm_info->tm_sec; } -void cpuDeepSleep(uint32_t msecs) { +void debug_date(datetime_t t) +{ + LOG_DEBUG("%d %d %d %d %d %d %d", t.year, t.month, t.day, t.hour, t.min, t.sec, t.dotw); + uart_default_tx_wait_blocking(); +} - time_t seconds = (time_t)(msecs / 1000); - datetime_t t_init, t_alarm; +void cpuDeepSleep(uint32_t msecs) +{ - awake = false; - // Start the RTC - rtc_init(); - epoch_to_datetime(0, &t_init); - rtc_set_datetime(&t_init); - epoch_to_datetime(seconds, &t_alarm); - // debug_date(t_init); - // debug_date(t_alarm); - uart_default_tx_wait_blocking(); - sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); - sleep_goto_sleep_until(&t_alarm, &sleep_callback); + time_t seconds = (time_t)(msecs / 1000); + datetime_t t_init, t_alarm; - // Make sure we don't wake - while (!awake) { - delay(1); - } + awake = false; + // Start the RTC + rtc_init(); + epoch_to_datetime(0, &t_init); + rtc_set_datetime(&t_init); + epoch_to_datetime(seconds, &t_alarm); + // debug_date(t_init); + // debug_date(t_alarm); + uart_default_tx_wait_blocking(); + sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); + sleep_goto_sleep_until(&t_alarm, &sleep_callback); - /* For now, I don't know how to revert this state - We just reboot in order to get back operational */ - rp2040.reboot(); + // Make sure we don't wake + while (!awake) { + delay(1); + } - /* Set RP2040 in dormant mode. Will not wake up. */ - // xosc_dormant(); + /* For now, I don't know how to revert this state + We just reboot in order to get back operational */ + rp2040.reboot(); + + /* Set RP2040 in dormant mode. Will not wake up. */ + // xosc_dormant(); } #else -void cpuDeepSleep(uint32_t msecs) { - /* Set RP2040 in dormant mode. Will not wake up. */ - xosc_dormant(); +void cpuDeepSleep(uint32_t msecs) +{ + /* Set RP2040 in dormant mode. Will not wake up. */ + xosc_dormant(); } #endif -void setBluetoothEnable(bool enable) { - // not needed +void setBluetoothEnable(bool enable) +{ + // not needed } -void updateBatteryLevel(uint8_t level) { - // not needed +void updateBatteryLevel(uint8_t level) +{ + // not needed } -void getMacAddr(uint8_t *dmac) { - pico_unique_board_id_t src; - pico_get_unique_board_id(&src); - dmac[5] = src.id[7]; - dmac[4] = src.id[6]; - dmac[3] = src.id[5]; - dmac[2] = src.id[4]; - dmac[1] = src.id[3]; - dmac[0] = src.id[2]; +void getMacAddr(uint8_t *dmac) +{ + pico_unique_board_id_t src; + pico_get_unique_board_id(&src); + dmac[5] = src.id[7]; + dmac[4] = src.id[6]; + dmac[3] = src.id[5]; + dmac[2] = src.id[4]; + dmac[1] = src.id[3]; + dmac[0] = src.id[2]; } -void rp2040Setup() { - /* Sets a random seed to make sure we get different random numbers on each boot. - Taken from CPU cycle counter and ROSC oscillator, so should be pretty random. - */ - randomSeed(rp2040.hwrand32()); +void rp2040Setup() +{ + /* Sets a random seed to make sure we get different random numbers on each boot. + Taken from CPU cycle counter and ROSC oscillator, so should be pretty random. + */ + randomSeed(rp2040.hwrand32()); #ifdef RP2040_SLOW_CLOCK - uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY); - uint f_pll_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_USB_CLKSRC_PRIMARY); - uint f_rosc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC); - uint f_clk_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); - uint f_clk_peri = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_PERI); - uint f_clk_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_USB); - uint f_clk_adc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_ADC); - uint f_clk_rtc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_RTC); + uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY); + uint f_pll_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_USB_CLKSRC_PRIMARY); + uint f_rosc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC); + uint f_clk_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); + uint f_clk_peri = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_PERI); + uint f_clk_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_USB); + uint f_clk_adc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_ADC); + uint f_clk_rtc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_RTC); - LOG_INFO("Clock speed:"); - LOG_INFO("pll_sys = %dkHz", f_pll_sys); - LOG_INFO("pll_usb = %dkHz", f_pll_usb); - LOG_INFO("rosc = %dkHz", f_rosc); - LOG_INFO("clk_sys = %dkHz", f_clk_sys); - LOG_INFO("clk_peri = %dkHz", f_clk_peri); - LOG_INFO("clk_usb = %dkHz", f_clk_usb); - LOG_INFO("clk_adc = %dkHz", f_clk_adc); - LOG_INFO("clk_rtc = %dkHz", f_clk_rtc); + LOG_INFO("Clock speed:"); + LOG_INFO("pll_sys = %dkHz", f_pll_sys); + LOG_INFO("pll_usb = %dkHz", f_pll_usb); + LOG_INFO("rosc = %dkHz", f_rosc); + LOG_INFO("clk_sys = %dkHz", f_clk_sys); + LOG_INFO("clk_peri = %dkHz", f_clk_peri); + LOG_INFO("clk_usb = %dkHz", f_clk_usb); + LOG_INFO("clk_adc = %dkHz", f_clk_adc); + LOG_INFO("clk_rtc = %dkHz", f_clk_rtc); #endif } -void enterDfuMode() { reset_usb_boot(0, 0); } +void enterDfuMode() +{ + reset_usb_boot(0, 0); +} /* Init in early boot state. */ #ifdef RP2040_SLOW_CLOCK -void initVariant() { - /* Set the system frequency to 18 MHz. */ - set_sys_clock_khz(18 * KHZ, false); - /* The previous line automatically detached clk_peri from clk_sys, and - attached it to pll_usb. We need to attach clk_peri back to system PLL to keep SPI - working at this low speed. - For details see https://github.com/jgromes/RadioLib/discussions/938 - */ - clock_configure(clk_peri, - 0, // No glitchless mux - CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, // System PLL on AUX mux - 18 * MHZ, // Input frequency - 18 * MHZ // Output (must be same as no divider) - ); - /* Run also ADC on lower clk_sys. */ - clock_configure(clk_adc, 0, CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 18 * MHZ, 18 * MHZ); - /* Run RTC from XOSC since USB clock is off */ - clock_configure(clk_rtc, 0, CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC, 12 * MHZ, 47 * KHZ); - /* Turn off USB PLL */ - pll_deinit(pll_usb); +void initVariant() +{ + /* Set the system frequency to 18 MHz. */ + set_sys_clock_khz(18 * KHZ, false); + /* The previous line automatically detached clk_peri from clk_sys, and + attached it to pll_usb. We need to attach clk_peri back to system PLL to keep SPI + working at this low speed. + For details see https://github.com/jgromes/RadioLib/discussions/938 + */ + clock_configure(clk_peri, + 0, // No glitchless mux + CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, // System PLL on AUX mux + 18 * MHZ, // Input frequency + 18 * MHZ // Output (must be same as no divider) + ); + /* Run also ADC on lower clk_sys. */ + clock_configure(clk_adc, 0, CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 18 * MHZ, 18 * MHZ); + /* Run RTC from XOSC since USB clock is off */ + clock_configure(clk_rtc, 0, CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC, 12 * MHZ, 47 * KHZ); + /* Turn off USB PLL */ + pll_deinit(pll_usb); } #endif \ No newline at end of file diff --git a/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h b/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h index e26f67cfe..17dff2468 100644 --- a/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h +++ b/src/platform/rp2xx0/pico_sleep/include/pico/sleep.h @@ -42,12 +42,18 @@ void sleep_run_from_dormant_source(dormant_source_t dormant_source); /*! \brief Set the dormant clock source to be the crystal oscillator * \ingroup hardware_sleep */ -static inline void sleep_run_from_xosc(void) { sleep_run_from_dormant_source(DORMANT_SOURCE_XOSC); } +static inline void sleep_run_from_xosc(void) +{ + sleep_run_from_dormant_source(DORMANT_SOURCE_XOSC); +} /*! \brief Set the dormant clock source to be the ring oscillator * \ingroup hardware_sleep */ -static inline void sleep_run_from_rosc(void) { sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); } +static inline void sleep_run_from_rosc(void) +{ + sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); +} /*! \brief Send system to sleep until the specified time * \ingroup hardware_sleep @@ -77,7 +83,10 @@ void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high); * * \param gpio_pin The pin to provide the wake up */ -static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) { sleep_goto_dormant_until_pin(gpio_pin, true, true); } +static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) +{ + sleep_goto_dormant_until_pin(gpio_pin, true, true); +} /*! \brief Send system to sleep until a high level is detected on GPIO * \ingroup hardware_sleep @@ -86,7 +95,10 @@ static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) { sleep_got * * \param gpio_pin The pin to provide the wake up */ -static inline void sleep_goto_dormant_until_level_high(uint gpio_pin) { sleep_goto_dormant_until_pin(gpio_pin, false, true); } +static inline void sleep_goto_dormant_until_level_high(uint gpio_pin) +{ + sleep_goto_dormant_until_pin(gpio_pin, false, true); +} #ifdef __cplusplus } diff --git a/src/platform/rp2xx0/pico_sleep/sleep.c b/src/platform/rp2xx0/pico_sleep/sleep.c index 291b983fa..65096be85 100644 --- a/src/platform/rp2xx0/pico_sleep/sleep.c +++ b/src/platform/rp2xx0/pico_sleep/sleep.c @@ -33,118 +33,123 @@ static dormant_source_t _dormant_source; -bool dormant_source_valid(dormant_source_t dormant_source) { - return (dormant_source == DORMANT_SOURCE_XOSC) || (dormant_source == DORMANT_SOURCE_ROSC); +bool dormant_source_valid(dormant_source_t dormant_source) +{ + return (dormant_source == DORMANT_SOURCE_XOSC) || (dormant_source == DORMANT_SOURCE_ROSC); } // In order to go into dormant mode we need to be running from a stoppable clock source: // either the xosc or rosc with no PLLs running. This means we disable the USB and ADC clocks // and all PLLs -void sleep_run_from_dormant_source(dormant_source_t dormant_source) { - assert(dormant_source_valid(dormant_source)); - _dormant_source = dormant_source; +void sleep_run_from_dormant_source(dormant_source_t dormant_source) +{ + assert(dormant_source_valid(dormant_source)); + _dormant_source = dormant_source; - // FIXME: Just defining average rosc freq here. - uint src_hz = (dormant_source == DORMANT_SOURCE_XOSC) ? XOSC_HZ : 6.5 * MHZ; - uint clk_ref_src = - (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC : CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH; + // FIXME: Just defining average rosc freq here. + uint src_hz = (dormant_source == DORMANT_SOURCE_XOSC) ? XOSC_HZ : 6.5 * MHZ; + uint clk_ref_src = (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC + : CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH; - // CLK_REF = XOSC or ROSC - clock_configure(clk_ref, clk_ref_src, - 0, // No aux mux - src_hz, src_hz); + // CLK_REF = XOSC or ROSC + clock_configure(clk_ref, clk_ref_src, + 0, // No aux mux + src_hz, src_hz); - // CLK SYS = CLK_REF - clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, - 0, // Using glitchless mux - src_hz, src_hz); + // CLK SYS = CLK_REF + clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, + 0, // Using glitchless mux + src_hz, src_hz); - // CLK USB = 0MHz - clock_stop(clk_usb); + // CLK USB = 0MHz + clock_stop(clk_usb); - // CLK ADC = 0MHz - clock_stop(clk_adc); + // CLK ADC = 0MHz + clock_stop(clk_adc); - // CLK RTC = ideally XOSC (12MHz) / 256 = 46875Hz but could be rosc - uint clk_rtc_src = - (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC : CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_ROSC_CLKSRC_PH; + // CLK RTC = ideally XOSC (12MHz) / 256 = 46875Hz but could be rosc + uint clk_rtc_src = (dormant_source == DORMANT_SOURCE_XOSC) ? CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC + : CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_ROSC_CLKSRC_PH; - clock_configure(clk_rtc, - 0, // No GLMUX - clk_rtc_src, src_hz, 46875); + clock_configure(clk_rtc, + 0, // No GLMUX + clk_rtc_src, src_hz, 46875); - // CLK PERI = clk_sys. Used as reference clock for Peripherals. No dividers so just select and enable - clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, src_hz, src_hz); + // CLK PERI = clk_sys. Used as reference clock for Peripherals. No dividers so just select and enable + clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, src_hz, src_hz); - pll_deinit(pll_sys); - pll_deinit(pll_usb); + pll_deinit(pll_sys); + pll_deinit(pll_usb); - // Assuming both xosc and rosc are running at the moment - if (dormant_source == DORMANT_SOURCE_XOSC) { - // Can disable rosc - rosc_disable(); - } else { - // Can disable xosc - xosc_disable(); - } + // Assuming both xosc and rosc are running at the moment + if (dormant_source == DORMANT_SOURCE_XOSC) { + // Can disable rosc + rosc_disable(); + } else { + // Can disable xosc + xosc_disable(); + } - // Reconfigure uart with new clocks - /* This dones not work with our current core */ - // setup_default_uart(); + // Reconfigure uart with new clocks + /* This dones not work with our current core */ + // setup_default_uart(); } // Go to sleep until woken up by the RTC -void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback) { - // We should have already called the sleep_run_from_dormant_source function - assert(dormant_source_valid(_dormant_source)); +void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback) +{ + // We should have already called the sleep_run_from_dormant_source function + assert(dormant_source_valid(_dormant_source)); - // Turn off all clocks when in sleep mode except for RTC - clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS; - clocks_hw->sleep_en1 = 0x0; + // Turn off all clocks when in sleep mode except for RTC + clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS; + clocks_hw->sleep_en1 = 0x0; - rtc_set_alarm(t, callback); + rtc_set_alarm(t, callback); - uint save = scb_hw->scr; - // Enable deep sleep at the proc - scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS; + uint save = scb_hw->scr; + // Enable deep sleep at the proc + scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS; - // Go to sleep - __wfi(); + // Go to sleep + __wfi(); } -static void _go_dormant(void) { - assert(dormant_source_valid(_dormant_source)); +static void _go_dormant(void) +{ + assert(dormant_source_valid(_dormant_source)); - if (_dormant_source == DORMANT_SOURCE_XOSC) { - xosc_dormant(); - } else { - rosc_set_dormant(); - } + if (_dormant_source == DORMANT_SOURCE_XOSC) { + xosc_dormant(); + } else { + rosc_set_dormant(); + } } -void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high) { - bool low = !high; - bool level = !edge; +void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high) +{ + bool low = !high; + bool level = !edge; - // Configure the appropriate IRQ at IO bank 0 - assert(gpio_pin < NUM_BANK0_GPIOS); + // Configure the appropriate IRQ at IO bank 0 + assert(gpio_pin < NUM_BANK0_GPIOS); - uint32_t event = 0; + uint32_t event = 0; - if (level && low) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_LOW_BITS; - if (level && high) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_HIGH_BITS; - if (edge && high) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_HIGH_BITS; - if (edge && low) - event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_LOW_BITS; + if (level && low) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_LOW_BITS; + if (level && high) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_HIGH_BITS; + if (edge && high) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_HIGH_BITS; + if (edge && low) + event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_LOW_BITS; - gpio_set_dormant_irq_enabled(gpio_pin, event, true); + gpio_set_dormant_irq_enabled(gpio_pin, event, true); - _go_dormant(); - // Execution stops here until woken up + _go_dormant(); + // Execution stops here until woken up - // Clear the irq so we can go back to dormant mode again if we want - gpio_acknowledge_irq(gpio_pin, event); + // Clear the irq so we can go back to dormant mode again if we want + gpio_acknowledge_irq(gpio_pin, event); } \ No newline at end of file diff --git a/src/platform/stm32wl/LittleFS.cpp b/src/platform/stm32wl/LittleFS.cpp index 3609602ba..40f32eca8 100644 --- a/src/platform/stm32wl/LittleFS.cpp +++ b/src/platform/stm32wl/LittleFS.cpp @@ -55,92 +55,96 @@ // LFS Disk IO //--------------------------------------------------------------------+ -static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { - LFS_UNUSED(c); +static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) +{ + LFS_UNUSED(c); - if (!buffer || !size) { - _LFS_DBG("%s Invalid parameter!\r\n", __func__); - return LFS_ERR_INVAL; - } + if (!buffer || !size) { + _LFS_DBG("%s Invalid parameter!\r\n", __func__); + return LFS_ERR_INVAL; + } - lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); - memcpy(buffer, (void *)address, size); + memcpy(buffer, (void *)address, size); - return LFS_ERR_OK; + return LFS_ERR_OK; } // Program a region in a block. The block must have previously // been erased. Negative error codes are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. -static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { - lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); - HAL_StatusTypeDef hal_rc = HAL_OK; - uint32_t dw_count = size / 8; - uint64_t *bufp = (uint64_t *)buffer; +static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) +{ + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); + HAL_StatusTypeDef hal_rc = HAL_OK; + uint32_t dw_count = size / 8; + uint64_t *bufp = (uint64_t *)buffer; - LFS_UNUSED(c); + LFS_UNUSED(c); - _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); - if (HAL_FLASH_Unlock() != HAL_OK) { - return LFS_ERR_IO; - } - for (uint32_t i = 0; i < dw_count; i++) { - if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { - _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address); - HAL_FLASH_Lock(); - return LFS_ERR_INVAL; + _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); + if (HAL_FLASH_Unlock() != HAL_OK) { + return LFS_ERR_IO; } - hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); - if (hal_rc != HAL_OK) { - /* Error occurred while writing data in Flash memory. - * User can add here some code to deal with this error. - */ - _LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError()); + for (uint32_t i = 0; i < dw_count; i++) { + if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { + _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address); + HAL_FLASH_Lock(); + return LFS_ERR_INVAL; + } + hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); + if (hal_rc != HAL_OK) { + /* Error occurred while writing data in Flash memory. + * User can add here some code to deal with this error. + */ + _LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError()); + } + address += 8; + bufp += 1; + } + if (HAL_FLASH_Lock() != HAL_OK) { + return LFS_ERR_IO; } - address += 8; - bufp += 1; - } - if (HAL_FLASH_Lock() != HAL_OK) { - return LFS_ERR_IO; - } - return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO } // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes // are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. -static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) { - lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE); - HAL_StatusTypeDef hal_rc; - FLASH_EraseInitTypeDef EraseInitStruct = {.TypeErase = FLASH_TYPEERASE_PAGES, .Page = 0, .NbPages = 1}; - uint32_t PAGEError = 0; +static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) +{ + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE); + HAL_StatusTypeDef hal_rc; + FLASH_EraseInitTypeDef EraseInitStruct = {.TypeErase = FLASH_TYPEERASE_PAGES, .Page = 0, .NbPages = 1}; + uint32_t PAGEError = 0; - LFS_UNUSED(c); + LFS_UNUSED(c); - if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { - _LFS_DBG("Wanted to erase out of bound of FLASH: 0x%08x.\n", address); - return LFS_ERR_INVAL; - } - /* calculate the absolute page, i.e. what the ST wants */ - EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE; - _LFS_DBG("Erasing block %d at 0x%08x... ", block, address); - HAL_FLASH_Unlock(); - hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); - HAL_FLASH_Lock(); + if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { + _LFS_DBG("Wanted to erase out of bound of FLASH: 0x%08x.\n", address); + return LFS_ERR_INVAL; + } + /* calculate the absolute page, i.e. what the ST wants */ + EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE; + _LFS_DBG("Erasing block %d at 0x%08x... ", block, address); + HAL_FLASH_Unlock(); + hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); + HAL_FLASH_Lock(); - return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO } // Sync the state of the underlying block device. Negative error codes // are propogated to the user. -static int _internal_flash_sync(const struct lfs_config *c) { - LFS_UNUSED(c); - // write function performs no caching. No need for sync. +static int _internal_flash_sync(const struct lfs_config *c) +{ + LFS_UNUSED(c); + // write function performs no caching. No need for sync. - return LFS_ERR_OK; + return LFS_ERR_OK; } static struct lfs_config _InternalFSConfig = {.context = NULL, @@ -169,25 +173,26 @@ LittleFS InternalFS; LittleFS::LittleFS(void) : STM32_LittleFS(&_InternalFSConfig) {} -bool LittleFS::begin(void) { - if (FLASH_BASE >= LFS_FLASH_ADDR_BASE) { - /* There is not enough space on this device for a filesystem. */ - return false; - } - // failed to mount, erase all pages then format and mount again - if (!STM32_LittleFS::begin()) { - // Erase all pages of internal flash region for Filesystem. - for (uint32_t addr = LFS_FLASH_ADDR_BASE; addr < (LFS_FLASH_ADDR_END + 1); addr += STM32WL_PAGE_SIZE) { - _internal_flash_erase(&_InternalFSConfig, (addr - LFS_FLASH_ADDR_BASE) / STM32WL_PAGE_SIZE); +bool LittleFS::begin(void) +{ + if (FLASH_BASE >= LFS_FLASH_ADDR_BASE) { + /* There is not enough space on this device for a filesystem. */ + return false; + } + // failed to mount, erase all pages then format and mount again + if (!STM32_LittleFS::begin()) { + // Erase all pages of internal flash region for Filesystem. + for (uint32_t addr = LFS_FLASH_ADDR_BASE; addr < (LFS_FLASH_ADDR_END + 1); addr += STM32WL_PAGE_SIZE) { + _internal_flash_erase(&_InternalFSConfig, (addr - LFS_FLASH_ADDR_BASE) / STM32WL_PAGE_SIZE); + } + + // lfs format + this->format(); + + // mount again if still failed, give up + if (!STM32_LittleFS::begin()) + return false; } - // lfs format - this->format(); - - // mount again if still failed, give up - if (!STM32_LittleFS::begin()) - return false; - } - - return true; + return true; } diff --git a/src/platform/stm32wl/LittleFS.h b/src/platform/stm32wl/LittleFS.h index 9c823fb1f..6c3c47f91 100644 --- a/src/platform/stm32wl/LittleFS.h +++ b/src/platform/stm32wl/LittleFS.h @@ -27,12 +27,13 @@ #include "STM32_LittleFS.h" -class LittleFS : public STM32_LittleFS { -public: - LittleFS(void); +class LittleFS : public STM32_LittleFS +{ + public: + LittleFS(void); - // overwrite to also perform low level format (sector erase of whole flash region) - bool begin(void); + // overwrite to also perform low level format (sector erase of whole flash region) + bool begin(void); }; extern LittleFS InternalFS; diff --git a/src/platform/stm32wl/STM32_LittleFS.cpp b/src/platform/stm32wl/STM32_LittleFS.cpp index 23d09b62c..97e79e61e 100644 --- a/src/platform/stm32wl/STM32_LittleFS.cpp +++ b/src/platform/stm32wl/STM32_LittleFS.cpp @@ -37,10 +37,11 @@ using namespace STM32_LittleFS_Namespace; STM32_LittleFS::STM32_LittleFS(void) : STM32_LittleFS(NULL) {} -STM32_LittleFS::STM32_LittleFS(struct lfs_config *cfg) { - varclr(&_lfs); - _lfs_cfg = cfg; - _mounted = false; +STM32_LittleFS::STM32_LittleFS(struct lfs_config *cfg) +{ + varclr(&_lfs); + _lfs_cfg = cfg; + _mounted = false; } STM32_LittleFS::~STM32_LittleFS() {} @@ -48,224 +49,235 @@ STM32_LittleFS::~STM32_LittleFS() {} // Initialize and mount the file system // Return true if mounted successfully else probably corrupted. // User should format the disk and try again -bool STM32_LittleFS::begin(struct lfs_config *cfg) { - _lockFS(); +bool STM32_LittleFS::begin(struct lfs_config *cfg) +{ + _lockFS(); - bool ret; - // not a loop, just an quick way to short-circuit on error - do { - if (_mounted) { - ret = true; - break; - } - if (cfg) { - _lfs_cfg = cfg; - } - if (nullptr == _lfs_cfg) { - ret = false; - break; - } - // actually attempt to mount, and log error if one occurs - int err = lfs_mount(&_lfs, _lfs_cfg); - PRINT_LFS_ERR(err); - _mounted = (err == LFS_ERR_OK); - ret = _mounted; - } while (0); + bool ret; + // not a loop, just an quick way to short-circuit on error + do { + if (_mounted) { + ret = true; + break; + } + if (cfg) { + _lfs_cfg = cfg; + } + if (nullptr == _lfs_cfg) { + ret = false; + break; + } + // actually attempt to mount, and log error if one occurs + int err = lfs_mount(&_lfs, _lfs_cfg); + PRINT_LFS_ERR(err); + _mounted = (err == LFS_ERR_OK); + ret = _mounted; + } while (0); - _unlockFS(); - return ret; + _unlockFS(); + return ret; } // Tear down and unmount file system -void STM32_LittleFS::end(void) { - _lockFS(); +void STM32_LittleFS::end(void) +{ + _lockFS(); - if (_mounted) { - _mounted = false; - int err = lfs_unmount(&_lfs); - PRINT_LFS_ERR(err); - (void)err; - } + if (_mounted) { + _mounted = false; + int err = lfs_unmount(&_lfs); + PRINT_LFS_ERR(err); + (void)err; + } - _unlockFS(); + _unlockFS(); } -bool STM32_LittleFS::format(void) { - _lockFS(); +bool STM32_LittleFS::format(void) +{ + _lockFS(); - int err = LFS_ERR_OK; - bool attemptMount = _mounted; - // not a loop, just an quick way to short-circuit on error - do { - // if already mounted: umount first -> format -> remount - if (_mounted) { - _mounted = false; - err = lfs_unmount(&_lfs); - if (LFS_ERR_OK != err) { - PRINT_LFS_ERR(err); - break; - } - } - err = lfs_format(&_lfs, _lfs_cfg); - if (LFS_ERR_OK != err) { - PRINT_LFS_ERR(err); - break; - } + int err = LFS_ERR_OK; + bool attemptMount = _mounted; + // not a loop, just an quick way to short-circuit on error + do { + // if already mounted: umount first -> format -> remount + if (_mounted) { + _mounted = false; + err = lfs_unmount(&_lfs); + if (LFS_ERR_OK != err) { + PRINT_LFS_ERR(err); + break; + } + } + err = lfs_format(&_lfs, _lfs_cfg); + if (LFS_ERR_OK != err) { + PRINT_LFS_ERR(err); + break; + } - if (attemptMount) { - err = lfs_mount(&_lfs, _lfs_cfg); - if (LFS_ERR_OK != err) { - PRINT_LFS_ERR(err); - break; - } - _mounted = true; - } - // success! - } while (0); + if (attemptMount) { + err = lfs_mount(&_lfs, _lfs_cfg); + if (LFS_ERR_OK != err) { + PRINT_LFS_ERR(err); + break; + } + _mounted = true; + } + // success! + } while (0); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Open a file or folder -STM32_LittleFS_Namespace::File STM32_LittleFS::open(char const *filepath, uint8_t mode) { - // No lock is required here ... the File() object will synchronize with the mutex provided - return STM32_LittleFS_Namespace::File(filepath, mode, *this); +STM32_LittleFS_Namespace::File STM32_LittleFS::open(char const *filepath, uint8_t mode) +{ + // No lock is required here ... the File() object will synchronize with the mutex provided + return STM32_LittleFS_Namespace::File(filepath, mode, *this); } // Check if file or folder exists -bool STM32_LittleFS::exists(char const *filepath) { - struct lfs_info info; - _lockFS(); +bool STM32_LittleFS::exists(char const *filepath) +{ + struct lfs_info info; + _lockFS(); - bool ret = (0 == lfs_stat(&_lfs, filepath, &info)); + bool ret = (0 == lfs_stat(&_lfs, filepath, &info)); - _unlockFS(); - return ret; + _unlockFS(); + return ret; } // Create a directory, create intermediate parent if needed -bool STM32_LittleFS::mkdir(char const *filepath) { - bool ret = true; - const char *slash = filepath; - if (slash[0] == '/') - slash++; // skip root '/' +bool STM32_LittleFS::mkdir(char const *filepath) +{ + bool ret = true; + const char *slash = filepath; + if (slash[0] == '/') + slash++; // skip root '/' - _lockFS(); + _lockFS(); - // make intermediate parent directory(ies) - while (NULL != (slash = strchr(slash, '/'))) { - char parent[slash - filepath + 1] = {0}; - memcpy(parent, filepath, slash - filepath); + // make intermediate parent directory(ies) + while (NULL != (slash = strchr(slash, '/'))) { + char parent[slash - filepath + 1] = {0}; + memcpy(parent, filepath, slash - filepath); - int rc = lfs_mkdir(&_lfs, parent); - if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { - PRINT_LFS_ERR(rc); - ret = false; - break; + int rc = lfs_mkdir(&_lfs, parent); + if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { + PRINT_LFS_ERR(rc); + ret = false; + break; + } + slash++; } - slash++; - } - // make the final requested directory - if (ret) { - int rc = lfs_mkdir(&_lfs, filepath); - if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { - PRINT_LFS_ERR(rc); - ret = false; + // make the final requested directory + if (ret) { + int rc = lfs_mkdir(&_lfs, filepath); + if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { + PRINT_LFS_ERR(rc); + ret = false; + } } - } - _unlockFS(); - return ret; + _unlockFS(); + return ret; } // Remove a file -bool STM32_LittleFS::remove(char const *filepath) { - _lockFS(); +bool STM32_LittleFS::remove(char const *filepath) +{ + _lockFS(); - int err = lfs_remove(&_lfs, filepath); - PRINT_LFS_ERR(err); + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Rename a file -bool STM32_LittleFS::rename(char const *oldfilepath, char const *newfilepath) { - _lockFS(); +bool STM32_LittleFS::rename(char const *oldfilepath, char const *newfilepath) +{ + _lockFS(); - int err = lfs_rename(&_lfs, oldfilepath, newfilepath); - PRINT_LFS_ERR(err); + int err = lfs_rename(&_lfs, oldfilepath, newfilepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Remove a folder -bool STM32_LittleFS::rmdir(char const *filepath) { - _lockFS(); +bool STM32_LittleFS::rmdir(char const *filepath) +{ + _lockFS(); - int err = lfs_remove(&_lfs, filepath); - PRINT_LFS_ERR(err); + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } // Remove a folder recursively -bool STM32_LittleFS::rmdir_r(char const *filepath) { - /* lfs is modified to remove non-empty folder, - According to below issue, comment these 2 line won't corrupt filesystem - at least when using LFS v1. If moving to LFS v2, see tracked issue - to see if issues (such as the orphans in threaded linked list) are resolved. - https://github.com/ARMmbed/littlefs/issues/43 - */ - _lockFS(); +bool STM32_LittleFS::rmdir_r(char const *filepath) +{ + /* lfs is modified to remove non-empty folder, + According to below issue, comment these 2 line won't corrupt filesystem + at least when using LFS v1. If moving to LFS v2, see tracked issue + to see if issues (such as the orphans in threaded linked list) are resolved. + https://github.com/ARMmbed/littlefs/issues/43 + */ + _lockFS(); - int err = lfs_remove(&_lfs, filepath); - PRINT_LFS_ERR(err); + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); - _unlockFS(); - return LFS_ERR_OK == err; + _unlockFS(); + return LFS_ERR_OK == err; } //------------- Debug -------------// #if CFG_DEBUG -const char *dbg_strerr_lfs(int32_t err) { - switch (err) { - case LFS_ERR_OK: - return "LFS_ERR_OK"; - case LFS_ERR_IO: - return "LFS_ERR_IO"; - case LFS_ERR_CORRUPT: - return "LFS_ERR_CORRUPT"; - case LFS_ERR_NOENT: - return "LFS_ERR_NOENT"; - case LFS_ERR_EXIST: - return "LFS_ERR_EXIST"; - case LFS_ERR_NOTDIR: - return "LFS_ERR_NOTDIR"; - case LFS_ERR_ISDIR: - return "LFS_ERR_ISDIR"; - case LFS_ERR_NOTEMPTY: - return "LFS_ERR_NOTEMPTY"; - case LFS_ERR_BADF: - return "LFS_ERR_BADF"; - case LFS_ERR_INVAL: - return "LFS_ERR_INVAL"; - case LFS_ERR_NOSPC: - return "LFS_ERR_NOSPC"; - case LFS_ERR_NOMEM: - return "LFS_ERR_NOMEM"; +const char *dbg_strerr_lfs(int32_t err) +{ + switch (err) { + case LFS_ERR_OK: + return "LFS_ERR_OK"; + case LFS_ERR_IO: + return "LFS_ERR_IO"; + case LFS_ERR_CORRUPT: + return "LFS_ERR_CORRUPT"; + case LFS_ERR_NOENT: + return "LFS_ERR_NOENT"; + case LFS_ERR_EXIST: + return "LFS_ERR_EXIST"; + case LFS_ERR_NOTDIR: + return "LFS_ERR_NOTDIR"; + case LFS_ERR_ISDIR: + return "LFS_ERR_ISDIR"; + case LFS_ERR_NOTEMPTY: + return "LFS_ERR_NOTEMPTY"; + case LFS_ERR_BADF: + return "LFS_ERR_BADF"; + case LFS_ERR_INVAL: + return "LFS_ERR_INVAL"; + case LFS_ERR_NOSPC: + return "LFS_ERR_NOSPC"; + case LFS_ERR_NOMEM: + return "LFS_ERR_NOMEM"; - default: - static char errcode[10]; - sprintf(errcode, "%ld", err); - return errcode; - } + default: + static char errcode[10]; + sprintf(errcode, "%ld", err); + return errcode; + } - return NULL; + return NULL; } #endif diff --git a/src/platform/stm32wl/STM32_LittleFS.h b/src/platform/stm32wl/STM32_LittleFS.h index 2ebf9db3d..9460ffa81 100644 --- a/src/platform/stm32wl/STM32_LittleFS.h +++ b/src/platform/stm32wl/STM32_LittleFS.h @@ -33,57 +33,60 @@ #include "STM32_LittleFS_File.h" #include "littlefs/lfs.h" -class STM32_LittleFS { -public: - STM32_LittleFS(void); - explicit STM32_LittleFS(struct lfs_config *cfg); - virtual ~STM32_LittleFS(); +class STM32_LittleFS +{ + public: + STM32_LittleFS(void); + explicit STM32_LittleFS(struct lfs_config *cfg); + virtual ~STM32_LittleFS(); - bool begin(struct lfs_config *cfg = NULL); - void end(void); + bool begin(struct lfs_config *cfg = NULL); + void end(void); - // Open the specified file/directory with the supplied mode (e.g. read or - // write, etc). Returns a File object for interacting with the file. - // Note that currently only one file can be open at a time. - STM32_LittleFS_Namespace::File open(char const *filename, uint8_t mode = STM32_LittleFS_Namespace::FILE_O_READ); + // Open the specified file/directory with the supplied mode (e.g. read or + // write, etc). Returns a File object for interacting with the file. + // Note that currently only one file can be open at a time. + STM32_LittleFS_Namespace::File open(char const *filename, uint8_t mode = STM32_LittleFS_Namespace::FILE_O_READ); - // Methods to determine if the requested file path exists. - bool exists(char const *filepath); + // Methods to determine if the requested file path exists. + bool exists(char const *filepath); - // Create the requested directory hierarchy--if intermediate directories - // do not exist they will be created. - bool mkdir(char const *filepath); + // Create the requested directory hierarchy--if intermediate directories + // do not exist they will be created. + bool mkdir(char const *filepath); - // Delete the file. - bool remove(char const *filepath); + // Delete the file. + bool remove(char const *filepath); - // Rename the file. - bool rename(char const *oldfilepath, char const *newfilepath); + // Rename the file. + bool rename(char const *oldfilepath, char const *newfilepath); - // Delete a folder (must be empty) - bool rmdir(char const *filepath); + // Delete a folder (must be empty) + bool rmdir(char const *filepath); - // Delete a folder (recursively) - bool rmdir_r(char const *filepath); + // Delete a folder (recursively) + bool rmdir_r(char const *filepath); - // format file system - bool format(void); + // format file system + bool format(void); - /*------------------------------------------------------------------*/ - /* INTERNAL USAGE ONLY - * Although declare as public, it is meant to be invoked by internal - * code. User should not call these directly - *------------------------------------------------------------------*/ - lfs_t *_getFS(void) { return &_lfs; } - void _lockFS(void) { /* no-op */ - } - void _unlockFS(void) { /* no-op */ - } + /*------------------------------------------------------------------*/ + /* INTERNAL USAGE ONLY + * Although declare as public, it is meant to be invoked by internal + * code. User should not call these directly + *------------------------------------------------------------------*/ + lfs_t *_getFS(void) { return &_lfs; } + void _lockFS(void) + { /* no-op */ + } + void _unlockFS(void) + { /* no-op */ + } -protected: - bool _mounted; - struct lfs_config *_lfs_cfg; - lfs_t _lfs; + protected: + bool _mounted; + struct lfs_config *_lfs_cfg; + lfs_t _lfs; }; #if !CFG_DEBUG @@ -91,12 +94,12 @@ protected: #define PRINT_LFS_ERR(_err) #else #define VERIFY_LFS(...) _GET_3RD_ARG(__VA_ARGS__, VERIFY_ERR_2ARGS, VERIFY_ERR_1ARGS)(__VA_ARGS__, dbg_strerr_lfs) -#define PRINT_LFS_ERR(_err) \ - do { \ - if (_err) { \ - printf("%s:%d, LFS error: %d\n", __FILE__, __LINE__, _err); \ - } \ - } while (0) // LFS_ERR are of type int, VERIFY_MESS expects long_int +#define PRINT_LFS_ERR(_err) \ + do { \ + if (_err) { \ + printf("%s:%d, LFS error: %d\n", __FILE__, __LINE__, _err); \ + } \ + } while (0) // LFS_ERR are of type int, VERIFY_MESS expects long_int const char *dbg_strerr_lfs(int32_t err); #endif diff --git a/src/platform/stm32wl/STM32_LittleFS_File.cpp b/src/platform/stm32wl/STM32_LittleFS_File.cpp index 20d8e6b81..349187a02 100644 --- a/src/platform/stm32wl/STM32_LittleFS_File.cpp +++ b/src/platform/stm32wl/STM32_LittleFS_File.cpp @@ -34,324 +34,360 @@ using namespace STM32_LittleFS_Namespace; -File::File(STM32_LittleFS &fs) { - _fs = &fs; - _is_dir = false; - _name[0] = 0; - _name[LFS_NAME_MAX] = 0; - _dir_path = NULL; +File::File(STM32_LittleFS &fs) +{ + _fs = &fs; + _is_dir = false; + _name[0] = 0; + _name[LFS_NAME_MAX] = 0; + _dir_path = NULL; - _dir = NULL; - _file = NULL; + _dir = NULL; + _file = NULL; } -File::File(char const *filename, uint8_t mode, STM32_LittleFS &fs) : File(fs) { - // public constructor calls public API open(), which will obtain the mutex - this->open(filename, mode); +File::File(char const *filename, uint8_t mode, STM32_LittleFS &fs) : File(fs) +{ + // public constructor calls public API open(), which will obtain the mutex + this->open(filename, mode); } -bool File::_open_file(char const *filepath, uint8_t mode) { - int flags = (mode == FILE_O_READ) ? LFS_O_RDONLY : (mode == FILE_O_WRITE) ? (LFS_O_RDWR | LFS_O_CREAT) : 0; +bool File::_open_file(char const *filepath, uint8_t mode) +{ + int flags = (mode == FILE_O_READ) ? LFS_O_RDONLY : (mode == FILE_O_WRITE) ? (LFS_O_RDWR | LFS_O_CREAT) : 0; - if (flags) { - _file = (lfs_file_t *)rtos_malloc(sizeof(lfs_file_t)); - if (!_file) - return false; + if (flags) { + _file = (lfs_file_t *)rtos_malloc(sizeof(lfs_file_t)); + if (!_file) + return false; - int rc = lfs_file_open(_fs->_getFS(), _file, filepath, flags); + int rc = lfs_file_open(_fs->_getFS(), _file, filepath, flags); + + if (rc) { + // failed to open + PRINT_LFS_ERR(rc); + // free memory + rtos_free(_file); + _file = NULL; + return false; + } + + // move to end of file + if (mode == FILE_O_WRITE) + lfs_file_seek(_fs->_getFS(), _file, 0, LFS_SEEK_END); + + _is_dir = false; + } + + return true; +} + +bool File::_open_dir(char const *filepath) +{ + _dir = (lfs_dir_t *)rtos_malloc(sizeof(lfs_dir_t)); + if (!_dir) + return false; + + int rc = lfs_dir_open(_fs->_getFS(), _dir, filepath); if (rc) { - // failed to open - PRINT_LFS_ERR(rc); - // free memory - rtos_free(_file); - _file = NULL; - return false; + // failed to open + PRINT_LFS_ERR(rc); + // free memory + rtos_free(_dir); + _dir = NULL; + return false; } - // move to end of file - if (mode == FILE_O_WRITE) - lfs_file_seek(_fs->_getFS(), _file, 0, LFS_SEEK_END); + _is_dir = true; - _is_dir = false; - } + _dir_path = (char *)rtos_malloc(strlen(filepath) + 1); + strcpy(_dir_path, filepath); - return true; + return true; } -bool File::_open_dir(char const *filepath) { - _dir = (lfs_dir_t *)rtos_malloc(sizeof(lfs_dir_t)); - if (!_dir) - return false; +bool File::open(char const *filepath, uint8_t mode) +{ + bool ret = false; + _fs->_lockFS(); - int rc = lfs_dir_open(_fs->_getFS(), _dir, filepath); + ret = this->_open(filepath, mode); - if (rc) { - // failed to open - PRINT_LFS_ERR(rc); - // free memory - rtos_free(_dir); - _dir = NULL; - return false; - } - - _is_dir = true; - - _dir_path = (char *)rtos_malloc(strlen(filepath) + 1); - strcpy(_dir_path, filepath); - - return true; + _fs->_unlockFS(); + return ret; } -bool File::open(char const *filepath, uint8_t mode) { - bool ret = false; - _fs->_lockFS(); +bool File::_open(char const *filepath, uint8_t mode) +{ + bool ret = false; - ret = this->_open(filepath, mode); + // close if currently opened + if (this->isOpen()) + _close(); - _fs->_unlockFS(); - return ret; -} + struct lfs_info info; + int rc = lfs_stat(_fs->_getFS(), filepath, &info); -bool File::_open(char const *filepath, uint8_t mode) { - bool ret = false; - - // close if currently opened - if (this->isOpen()) - _close(); - - struct lfs_info info; - int rc = lfs_stat(_fs->_getFS(), filepath, &info); - - if (LFS_ERR_OK == rc) { - // file existed, open file or directory accordingly - ret = (info.type == LFS_TYPE_REG) ? _open_file(filepath, mode) : _open_dir(filepath); - } else if (LFS_ERR_NOENT == rc) { - // file not existed, only proceed with FILE_O_WRITE mode - if (mode == FILE_O_WRITE) - ret = _open_file(filepath, mode); - } else { - PRINT_LFS_ERR(rc); - } - - // save bare file name - if (ret) { - char const *splash = strrchr(filepath, '/'); - strncpy(_name, splash ? (splash + 1) : filepath, LFS_NAME_MAX); - } - return ret; -} - -size_t File::write(uint8_t ch) { return write(&ch, 1); } - -size_t File::write(uint8_t const *buf, size_t size) { - lfs_ssize_t wrcount = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); - if (wrcount < 0) { - wrcount = 0; - } - } - - _fs->_unlockFS(); - return wrcount; -} - -int File::read(void) { - // this thin wrapper relies on called function to synchronize - int ret = -1; - uint8_t ch; - if (read(&ch, 1) > 0) { - ret = static_cast(ch); - } - return ret; -} - -int File::read(void *buf, uint16_t nbyte) { - int ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_read(_fs->_getFS(), _file, buf, nbyte); - } - - _fs->_unlockFS(); - return ret; -} - -int File::peek(void) { - int ret = -1; - _fs->_lockFS(); - - if (!this->_is_dir) { - uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); - uint8_t ch = 0; - if (lfs_file_read(_fs->_getFS(), _file, &ch, 1) > 0) { - ret = static_cast(ch); - } - (void)lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET); - } - - _fs->_unlockFS(); - return ret; -} - -int File::available(void) { - int ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - uint32_t file_size = lfs_file_size(_fs->_getFS(), _file); - uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); - ret = file_size - pos; - } - - _fs->_unlockFS(); - return ret; -} - -bool File::seek(uint32_t pos) { - bool ret = false; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET) >= 0; - } - - _fs->_unlockFS(); - return ret; -} - -uint32_t File::position(void) { - uint32_t ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_tell(_fs->_getFS(), _file); - } - - _fs->_unlockFS(); - return ret; -} - -uint32_t File::size(void) { - uint32_t ret = 0; - _fs->_lockFS(); - - if (!this->_is_dir) { - ret = lfs_file_size(_fs->_getFS(), _file); - } - - _fs->_unlockFS(); - return ret; -} - -bool File::truncate(uint32_t pos) { - int32_t ret = LFS_ERR_ISDIR; - _fs->_lockFS(); - if (!this->_is_dir) { - ret = lfs_file_truncate(_fs->_getFS(), _file, pos); - } - _fs->_unlockFS(); - return (ret == 0); -} - -bool File::truncate(void) { - int32_t ret = LFS_ERR_ISDIR; - _fs->_lockFS(); - if (!this->_is_dir) { - uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); - ret = lfs_file_truncate(_fs->_getFS(), _file, pos); - } - _fs->_unlockFS(); - return (ret == 0); -} - -void File::flush(void) { - _fs->_lockFS(); - - if (!this->_is_dir) { - lfs_file_sync(_fs->_getFS(), _file); - } - - _fs->_unlockFS(); - return; -} - -void File::close(void) { - _fs->_lockFS(); - this->_close(); - _fs->_unlockFS(); -} - -void File::_close(void) { - if (this->isOpen()) { - if (this->_is_dir) { - lfs_dir_close(_fs->_getFS(), _dir); - rtos_free(_dir); - _dir = NULL; - - if (this->_dir_path) - rtos_free(_dir_path); - _dir_path = NULL; + if (LFS_ERR_OK == rc) { + // file existed, open file or directory accordingly + ret = (info.type == LFS_TYPE_REG) ? _open_file(filepath, mode) : _open_dir(filepath); + } else if (LFS_ERR_NOENT == rc) { + // file not existed, only proceed with FILE_O_WRITE mode + if (mode == FILE_O_WRITE) + ret = _open_file(filepath, mode); } else { - lfs_file_close(this->_fs->_getFS(), _file); - rtos_free(_file); - _file = NULL; + PRINT_LFS_ERR(rc); } - } + + // save bare file name + if (ret) { + char const *splash = strrchr(filepath, '/'); + strncpy(_name, splash ? (splash + 1) : filepath, LFS_NAME_MAX); + } + return ret; } -File::operator bool(void) { return isOpen(); } +size_t File::write(uint8_t ch) +{ + return write(&ch, 1); +} -bool File::isOpen(void) { return (_file != NULL) || (_dir != NULL); } +size_t File::write(uint8_t const *buf, size_t size) +{ + lfs_ssize_t wrcount = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); + if (wrcount < 0) { + wrcount = 0; + } + } + + _fs->_unlockFS(); + return wrcount; +} + +int File::read(void) +{ + // this thin wrapper relies on called function to synchronize + int ret = -1; + uint8_t ch; + if (read(&ch, 1) > 0) { + ret = static_cast(ch); + } + return ret; +} + +int File::read(void *buf, uint16_t nbyte) +{ + int ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_read(_fs->_getFS(), _file, buf, nbyte); + } + + _fs->_unlockFS(); + return ret; +} + +int File::peek(void) +{ + int ret = -1; + _fs->_lockFS(); + + if (!this->_is_dir) { + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + uint8_t ch = 0; + if (lfs_file_read(_fs->_getFS(), _file, &ch, 1) > 0) { + ret = static_cast(ch); + } + (void)lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET); + } + + _fs->_unlockFS(); + return ret; +} + +int File::available(void) +{ + int ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + uint32_t file_size = lfs_file_size(_fs->_getFS(), _file); + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + ret = file_size - pos; + } + + _fs->_unlockFS(); + return ret; +} + +bool File::seek(uint32_t pos) +{ + bool ret = false; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET) >= 0; + } + + _fs->_unlockFS(); + return ret; +} + +uint32_t File::position(void) +{ + uint32_t ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_tell(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return ret; +} + +uint32_t File::size(void) +{ + uint32_t ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) { + ret = lfs_file_size(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return ret; +} + +bool File::truncate(uint32_t pos) +{ + int32_t ret = LFS_ERR_ISDIR; + _fs->_lockFS(); + if (!this->_is_dir) { + ret = lfs_file_truncate(_fs->_getFS(), _file, pos); + } + _fs->_unlockFS(); + return (ret == 0); +} + +bool File::truncate(void) +{ + int32_t ret = LFS_ERR_ISDIR; + _fs->_lockFS(); + if (!this->_is_dir) { + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + ret = lfs_file_truncate(_fs->_getFS(), _file, pos); + } + _fs->_unlockFS(); + return (ret == 0); +} + +void File::flush(void) +{ + _fs->_lockFS(); + + if (!this->_is_dir) { + lfs_file_sync(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return; +} + +void File::close(void) +{ + _fs->_lockFS(); + this->_close(); + _fs->_unlockFS(); +} + +void File::_close(void) +{ + if (this->isOpen()) { + if (this->_is_dir) { + lfs_dir_close(_fs->_getFS(), _dir); + rtos_free(_dir); + _dir = NULL; + + if (this->_dir_path) + rtos_free(_dir_path); + _dir_path = NULL; + } else { + lfs_file_close(this->_fs->_getFS(), _file); + rtos_free(_file); + _file = NULL; + } + } +} + +File::operator bool(void) +{ + return isOpen(); +} + +bool File::isOpen(void) +{ + return (_file != NULL) || (_dir != NULL); +} // WARNING -- although marked as `const`, the values pointed // to may change. For example, if the same File // object has `open()` called with a different // file or directory name, this same pointer will // suddenly (unexpectedly?) have different values. -char const *File::name(void) { return this->_name; } +char const *File::name(void) +{ + return this->_name; +} -bool File::isDirectory(void) { return this->_is_dir; } +bool File::isDirectory(void) +{ + return this->_is_dir; +} -File File::openNextFile(uint8_t mode) { - _fs->_lockFS(); +File File::openNextFile(uint8_t mode) +{ + _fs->_lockFS(); - File ret(*_fs); - if (this->_is_dir) { - struct lfs_info info; - int rc; + File ret(*_fs); + if (this->_is_dir) { + struct lfs_info info; + int rc; - // lfs_dir_read returns 0 when reaching end of directory, 1 if found an entry - // Skip the "." and ".." entries ... - do { - rc = lfs_dir_read(_fs->_getFS(), _dir, &info); - } while (rc == 1 && (!strcmp(".", info.name) || !strcmp("..", info.name))); + // lfs_dir_read returns 0 when reaching end of directory, 1 if found an entry + // Skip the "." and ".." entries ... + do { + rc = lfs_dir_read(_fs->_getFS(), _dir, &info); + } while (rc == 1 && (!strcmp(".", info.name) || !strcmp("..", info.name))); - if (rc == 1) { - // string cat name with current folder - char filepath[strlen(_dir_path) + 1 + strlen(info.name) + 1]; // potential for significant stack usage - strcpy(filepath, _dir_path); - if (!(_dir_path[0] == '/' && _dir_path[1] == 0)) - strcat(filepath, "/"); // only add '/' if cwd is not root - strcat(filepath, info.name); + if (rc == 1) { + // string cat name with current folder + char filepath[strlen(_dir_path) + 1 + strlen(info.name) + 1]; // potential for significant stack usage + strcpy(filepath, _dir_path); + if (!(_dir_path[0] == '/' && _dir_path[1] == 0)) + strcat(filepath, "/"); // only add '/' if cwd is not root + strcat(filepath, info.name); - (void)ret._open(filepath, mode); // return value is ignored ... caller is expected to check isOpened() - } else if (rc < 0) { - PRINT_LFS_ERR(rc); + (void)ret._open(filepath, mode); // return value is ignored ... caller is expected to check isOpened() + } else if (rc < 0) { + PRINT_LFS_ERR(rc); + } } - } - _fs->_unlockFS(); - return ret; + _fs->_unlockFS(); + return ret; } -void File::rewindDirectory(void) { - _fs->_lockFS(); - if (this->_is_dir) { - lfs_dir_rewind(_fs->_getFS(), _dir); - } - _fs->_unlockFS(); +void File::rewindDirectory(void) +{ + _fs->_lockFS(); + if (this->_is_dir) { + lfs_dir_rewind(_fs->_getFS(), _dir); + } + _fs->_unlockFS(); } diff --git a/src/platform/stm32wl/STM32_LittleFS_File.h b/src/platform/stm32wl/STM32_LittleFS_File.h index 48b7cfb4b..2b48b02e0 100644 --- a/src/platform/stm32wl/STM32_LittleFS_File.h +++ b/src/platform/stm32wl/STM32_LittleFS_File.h @@ -30,74 +30,77 @@ // Forward declaration class STM32_LittleFS; -namespace STM32_LittleFS_Namespace { +namespace STM32_LittleFS_Namespace +{ // avoid conflict with other FileSystem FILE_READ/FILE_WRITE enum { - FILE_O_READ = 0, - FILE_O_WRITE = 1, + FILE_O_READ = 0, + FILE_O_WRITE = 1, }; -class File : public Stream { -public: - explicit File(STM32_LittleFS &fs); - File(char const *filename, uint8_t mode, STM32_LittleFS &fs); +class File : public Stream +{ + public: + explicit File(STM32_LittleFS &fs); + File(char const *filename, uint8_t mode, STM32_LittleFS &fs); -public: - bool open(char const *filename, uint8_t mode); + public: + bool open(char const *filename, uint8_t mode); - //------------- Stream API -------------// - virtual size_t write(uint8_t ch); - virtual size_t write(uint8_t const *buf, size_t size); - size_t write(const char *str) { - if (str == NULL) - return 0; - return write((const uint8_t *)str, strlen(str)); - } - size_t write(const char *buffer, size_t size) { return write((const uint8_t *)buffer, size); } + //------------- Stream API -------------// + virtual size_t write(uint8_t ch); + virtual size_t write(uint8_t const *buf, size_t size); + size_t write(const char *str) + { + if (str == NULL) + return 0; + return write((const uint8_t *)str, strlen(str)); + } + size_t write(const char *buffer, size_t size) { return write((const uint8_t *)buffer, size); } - virtual int read(void); - int read(void *buf, uint16_t nbyte); + virtual int read(void); + int read(void *buf, uint16_t nbyte); - virtual int peek(void); - virtual int available(void); - virtual void flush(void); + virtual int peek(void); + virtual int available(void); + virtual void flush(void); - bool seek(uint32_t pos); - uint32_t position(void); - uint32_t size(void); + bool seek(uint32_t pos); + uint32_t position(void); + uint32_t size(void); - bool truncate(uint32_t pos); - bool truncate(void); + bool truncate(uint32_t pos); + bool truncate(void); - void close(void); + void close(void); - operator bool(void); + operator bool(void); - bool isOpen(void); - char const *name(void); + bool isOpen(void); + char const *name(void); - bool isDirectory(void); - File openNextFile(uint8_t mode = FILE_O_READ); - void rewindDirectory(void); + bool isDirectory(void); + File openNextFile(uint8_t mode = FILE_O_READ); + void rewindDirectory(void); -private: - STM32_LittleFS *_fs; + private: + STM32_LittleFS *_fs; - bool _is_dir; + bool _is_dir; - union { - lfs_file_t *_file; - lfs_dir_t *_dir; - }; + union { + lfs_file_t *_file; + lfs_dir_t *_dir; + }; - char *_dir_path; - char _name[LFS_NAME_MAX + 1]; + char *_dir_path; + char _name[LFS_NAME_MAX + 1]; - bool _open(char const *filepath, uint8_t mode); - bool _open_file(char const *filepath, uint8_t mode); - bool _open_dir(char const *filepath); - void _close(void); + bool _open(char const *filepath, uint8_t mode); + bool _open_file(char const *filepath, uint8_t mode); + bool _open_dir(char const *filepath); + void _close(void); }; } // namespace STM32_LittleFS_Namespace diff --git a/src/platform/stm32wl/architecture.h b/src/platform/stm32wl/architecture.h index cb6b6e15d..e131a0a32 100644 --- a/src/platform/stm32wl/architecture.h +++ b/src/platform/stm32wl/architecture.h @@ -34,6 +34,6 @@ #define SX126X_BUSY 1004 #if !defined(DEBUG_MUTE) && !defined(PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF) -#error \ +#error \ "You MUST enable PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF if debug prints are enabled. printf will print uninitialized garbage instead of floats." #endif \ No newline at end of file diff --git a/src/platform/stm32wl/littlefs/lfs.c b/src/platform/stm32wl/littlefs/lfs.c index 022070377..5dc4c7669 100644 --- a/src/platform/stm32wl/littlefs/lfs.c +++ b/src/platform/stm32wl/littlefs/lfs.c @@ -10,224 +10,239 @@ #include /// Caching block device operations /// -static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, void *buffer, - lfs_size_t size) { - uint8_t *data = buffer; - LFS_ASSERT(block < lfs->cfg->block_count); +static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, + void *buffer, lfs_size_t size) +{ + uint8_t *data = buffer; + LFS_ASSERT(block < lfs->cfg->block_count); - while (size > 0) { - if (pcache && block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { - // is already in pcache? - lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); - memcpy(data, &pcache->buffer[off - pcache->off], diff); + while (size > 0) { + if (pcache && block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { + // is already in pcache? + lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); + memcpy(data, &pcache->buffer[off - pcache->off], diff); - data += diff; - off += diff; - size -= diff; - continue; - } + data += diff; + off += diff; + size -= diff; + continue; + } - if (block == rcache->block && off >= rcache->off && off < rcache->off + lfs->cfg->read_size) { - // is already in rcache? - lfs_size_t diff = lfs_min(size, lfs->cfg->read_size - (off - rcache->off)); - memcpy(data, &rcache->buffer[off - rcache->off], diff); + if (block == rcache->block && off >= rcache->off && off < rcache->off + lfs->cfg->read_size) { + // is already in rcache? + lfs_size_t diff = lfs_min(size, lfs->cfg->read_size - (off - rcache->off)); + memcpy(data, &rcache->buffer[off - rcache->off], diff); - data += diff; - off += diff; - size -= diff; - continue; - } + data += diff; + off += diff; + size -= diff; + continue; + } - if (off % lfs->cfg->read_size == 0 && size >= lfs->cfg->read_size) { - // bypass cache? - lfs_size_t diff = size - (size % lfs->cfg->read_size); - int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); - if (err) { - return err; - } + if (off % lfs->cfg->read_size == 0 && size >= lfs->cfg->read_size) { + // bypass cache? + lfs_size_t diff = size - (size % lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); + if (err) { + return err; + } - data += diff; - off += diff; - size -= diff; - continue; - } + data += diff; + off += diff; + size -= diff; + continue; + } - // load to cache, first condition can no longer fail - rcache->block = block; - rcache->off = off - (off % lfs->cfg->read_size); - int err = lfs->cfg->read(lfs->cfg, rcache->block, rcache->off, rcache->buffer, lfs->cfg->read_size); - if (err) { - return err; - } - } - - return 0; -} - -static int lfs_cache_cmp(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, const void *buffer, - lfs_size_t size) { - const uint8_t *data = buffer; - - for (lfs_off_t i = 0; i < size; i++) { - uint8_t c; - int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); - if (err) { - return err; - } - - if (c != data[i]) { - return false; - } - } - - return true; -} - -static int lfs_cache_crc(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, lfs_size_t size, - uint32_t *crc) { - for (lfs_off_t i = 0; i < size; i++) { - uint8_t c; - int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); - if (err) { - return err; - } - - lfs_crc(crc, &c, 1); - } - - return 0; -} - -static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { - // do not zero, cheaper if cache is readonly or only going to be - // written with identical data (during relocates) - (void)lfs; - rcache->block = 0xffffffff; -} - -static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) { - // zero to avoid information leak - memset(pcache->buffer, 0xff, lfs->cfg->prog_size); - pcache->block = 0xffffffff; -} - -static int lfs_cache_flush(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache) { - if (pcache->block != 0xffffffff) { - int err = lfs->cfg->prog(lfs->cfg, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); - if (err) { - return err; - } - - if (rcache) { - int res = lfs_cache_cmp(lfs, rcache, NULL, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); - if (res < 0) { - return res; - } - - if (!res) { - return LFS_ERR_CORRUPT; - } - } - - lfs_cache_zero(lfs, pcache); - } - - return 0; -} - -static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_block_t block, lfs_off_t off, const void *buffer, - lfs_size_t size) { - const uint8_t *data = buffer; - LFS_ASSERT(block < lfs->cfg->block_count); - - while (size > 0) { - if (block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { - // is already in pcache? - lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); - memcpy(&pcache->buffer[off - pcache->off], data, diff); - - data += diff; - off += diff; - size -= diff; - - if (off % lfs->cfg->prog_size == 0) { - // eagerly flush out pcache if we fill up - int err = lfs_cache_flush(lfs, pcache, rcache); + // load to cache, first condition can no longer fail + rcache->block = block; + rcache->off = off - (off % lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, rcache->block, rcache->off, rcache->buffer, lfs->cfg->read_size); if (err) { - return err; + return err; } - } - - continue; } - // pcache must have been flushed, either by programming and - // entire block or manually flushing the pcache - LFS_ASSERT(pcache->block == 0xffffffff); + return 0; +} - if (off % lfs->cfg->prog_size == 0 && size >= lfs->cfg->prog_size) { - // bypass pcache? - lfs_size_t diff = size - (size % lfs->cfg->prog_size); - int err = lfs->cfg->prog(lfs->cfg, block, off, data, diff); - if (err) { - return err; - } +static int lfs_cache_cmp(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, + const void *buffer, lfs_size_t size) +{ + const uint8_t *data = buffer; - if (rcache) { - int res = lfs_cache_cmp(lfs, rcache, NULL, block, off, data, diff); - if (res < 0) { - return res; + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); + if (err) { + return err; } - if (!res) { - return LFS_ERR_CORRUPT; + if (c != data[i]) { + return false; } - } - - data += diff; - off += diff; - size -= diff; - continue; } - // prepare pcache, first condition can no longer fail - pcache->block = block; - pcache->off = off - (off % lfs->cfg->prog_size); - } + return true; +} - return 0; +static int lfs_cache_crc(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, + lfs_size_t size, uint32_t *crc) +{ + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); + if (err) { + return err; + } + + lfs_crc(crc, &c, 1); + } + + return 0; +} + +static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) +{ + // do not zero, cheaper if cache is readonly or only going to be + // written with identical data (during relocates) + (void)lfs; + rcache->block = 0xffffffff; +} + +static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) +{ + // zero to avoid information leak + memset(pcache->buffer, 0xff, lfs->cfg->prog_size); + pcache->block = 0xffffffff; +} + +static int lfs_cache_flush(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache) +{ + if (pcache->block != 0xffffffff) { + int err = lfs->cfg->prog(lfs->cfg, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); + if (err) { + return err; + } + + if (rcache) { + int res = lfs_cache_cmp(lfs, rcache, NULL, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); + if (res < 0) { + return res; + } + + if (!res) { + return LFS_ERR_CORRUPT; + } + } + + lfs_cache_zero(lfs, pcache); + } + + return 0; +} + +static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_block_t block, lfs_off_t off, + const void *buffer, lfs_size_t size) +{ + const uint8_t *data = buffer; + LFS_ASSERT(block < lfs->cfg->block_count); + + while (size > 0) { + if (block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { + // is already in pcache? + lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); + memcpy(&pcache->buffer[off - pcache->off], data, diff); + + data += diff; + off += diff; + size -= diff; + + if (off % lfs->cfg->prog_size == 0) { + // eagerly flush out pcache if we fill up + int err = lfs_cache_flush(lfs, pcache, rcache); + if (err) { + return err; + } + } + + continue; + } + + // pcache must have been flushed, either by programming and + // entire block or manually flushing the pcache + LFS_ASSERT(pcache->block == 0xffffffff); + + if (off % lfs->cfg->prog_size == 0 && size >= lfs->cfg->prog_size) { + // bypass pcache? + lfs_size_t diff = size - (size % lfs->cfg->prog_size); + int err = lfs->cfg->prog(lfs->cfg, block, off, data, diff); + if (err) { + return err; + } + + if (rcache) { + int res = lfs_cache_cmp(lfs, rcache, NULL, block, off, data, diff); + if (res < 0) { + return res; + } + + if (!res) { + return LFS_ERR_CORRUPT; + } + } + + data += diff; + off += diff; + size -= diff; + continue; + } + + // prepare pcache, first condition can no longer fail + pcache->block = block; + pcache->off = off - (off % lfs->cfg->prog_size); + } + + return 0; } /// General lfs block device operations /// -static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { - // if we ever do more than writes to alternating pairs, - // this may need to consider pcache - return lfs_cache_read(lfs, &lfs->rcache, NULL, block, off, buffer, size); +static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) +{ + // if we ever do more than writes to alternating pairs, + // this may need to consider pcache + return lfs_cache_read(lfs, &lfs->rcache, NULL, block, off, buffer, size); } -static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { - return lfs_cache_prog(lfs, &lfs->pcache, NULL, block, off, buffer, size); +static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) +{ + return lfs_cache_prog(lfs, &lfs->pcache, NULL, block, off, buffer, size); } -static int lfs_bd_cmp(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { - return lfs_cache_cmp(lfs, &lfs->rcache, NULL, block, off, buffer, size); +static int lfs_bd_cmp(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) +{ + return lfs_cache_cmp(lfs, &lfs->rcache, NULL, block, off, buffer, size); } -static int lfs_bd_crc(lfs_t *lfs, lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) { - return lfs_cache_crc(lfs, &lfs->rcache, NULL, block, off, size, crc); +static int lfs_bd_crc(lfs_t *lfs, lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) +{ + return lfs_cache_crc(lfs, &lfs->rcache, NULL, block, off, size, crc); } -static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { return lfs->cfg->erase(lfs->cfg, block); } +static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) +{ + return lfs->cfg->erase(lfs->cfg, block); +} -static int lfs_bd_sync(lfs_t *lfs) { - lfs_cache_drop(lfs, &lfs->rcache); +static int lfs_bd_sync(lfs_t *lfs) +{ + lfs_cache_drop(lfs, &lfs->rcache); - int err = lfs_cache_flush(lfs, &lfs->pcache, NULL); - if (err) { - return err; - } + int err = lfs_cache_flush(lfs, &lfs->pcache, NULL); + if (err) { + return err; + } - return lfs->cfg->sync(lfs->cfg); + return lfs->cfg->sync(lfs->cfg); } /// Internal operations predeclared here /// @@ -239,2199 +254,2272 @@ static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_bloc int lfs_deorphan(lfs_t *lfs); /// Block allocator /// -static int lfs_alloc_lookahead(void *p, lfs_block_t block) { - lfs_t *lfs = p; +static int lfs_alloc_lookahead(void *p, lfs_block_t block) +{ + lfs_t *lfs = p; - lfs_block_t off = ((block - lfs->free.off) + lfs->cfg->block_count) % lfs->cfg->block_count; + lfs_block_t off = ((block - lfs->free.off) + lfs->cfg->block_count) % lfs->cfg->block_count; - if (off < lfs->free.size) { - lfs->free.buffer[off / 32] |= 1U << (off % 32); - } + if (off < lfs->free.size) { + lfs->free.buffer[off / 32] |= 1U << (off % 32); + } - return 0; + return 0; } -static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { - while (true) { - while (lfs->free.i != lfs->free.size) { - lfs_block_t off = lfs->free.i; - lfs->free.i += 1; - lfs->free.ack -= 1; +static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) +{ + while (true) { + while (lfs->free.i != lfs->free.size) { + lfs_block_t off = lfs->free.i; + lfs->free.i += 1; + lfs->free.ack -= 1; - if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { - // found a free block - *block = (lfs->free.off + off) % lfs->cfg->block_count; + if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { + // found a free block + *block = (lfs->free.off + off) % lfs->cfg->block_count; - // eagerly find next off so an alloc ack can - // discredit old lookahead blocks - while (lfs->free.i != lfs->free.size && (lfs->free.buffer[lfs->free.i / 32] & (1U << (lfs->free.i % 32)))) { - lfs->free.i += 1; - lfs->free.ack -= 1; + // eagerly find next off so an alloc ack can + // discredit old lookahead blocks + while (lfs->free.i != lfs->free.size && (lfs->free.buffer[lfs->free.i / 32] & (1U << (lfs->free.i % 32)))) { + lfs->free.i += 1; + lfs->free.ack -= 1; + } + + return 0; + } } - return 0; - } - } + // check if we have looked at all blocks since last ack + if (lfs->free.ack == 0) { + LFS_WARN("No more free space %" PRIu32, lfs->free.i + lfs->free.off); + return LFS_ERR_NOSPC; + } - // check if we have looked at all blocks since last ack - if (lfs->free.ack == 0) { - LFS_WARN("No more free space %" PRIu32, lfs->free.i + lfs->free.off); - return LFS_ERR_NOSPC; - } + lfs->free.off = (lfs->free.off + lfs->free.size) % lfs->cfg->block_count; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack); + lfs->free.i = 0; - lfs->free.off = (lfs->free.off + lfs->free.size) % lfs->cfg->block_count; - lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack); - lfs->free.i = 0; - - // find mask of free blocks from tree - memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); - int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs); - if (err) { - return err; + // find mask of free blocks from tree + memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); + int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs); + if (err) { + return err; + } } - } } -static void lfs_alloc_ack(lfs_t *lfs) { lfs->free.ack = lfs->cfg->block_count; } +static void lfs_alloc_ack(lfs_t *lfs) +{ + lfs->free.ack = lfs->cfg->block_count; +} /// Endian swapping functions /// -static void lfs_dir_fromle32(struct lfs_disk_dir *d) { - d->rev = lfs_fromle32(d->rev); - d->size = lfs_fromle32(d->size); - d->tail[0] = lfs_fromle32(d->tail[0]); - d->tail[1] = lfs_fromle32(d->tail[1]); +static void lfs_dir_fromle32(struct lfs_disk_dir *d) +{ + d->rev = lfs_fromle32(d->rev); + d->size = lfs_fromle32(d->size); + d->tail[0] = lfs_fromle32(d->tail[0]); + d->tail[1] = lfs_fromle32(d->tail[1]); } -static void lfs_dir_tole32(struct lfs_disk_dir *d) { - d->rev = lfs_tole32(d->rev); - d->size = lfs_tole32(d->size); - d->tail[0] = lfs_tole32(d->tail[0]); - d->tail[1] = lfs_tole32(d->tail[1]); +static void lfs_dir_tole32(struct lfs_disk_dir *d) +{ + d->rev = lfs_tole32(d->rev); + d->size = lfs_tole32(d->size); + d->tail[0] = lfs_tole32(d->tail[0]); + d->tail[1] = lfs_tole32(d->tail[1]); } -static void lfs_entry_fromle32(struct lfs_disk_entry *d) { - d->u.dir[0] = lfs_fromle32(d->u.dir[0]); - d->u.dir[1] = lfs_fromle32(d->u.dir[1]); +static void lfs_entry_fromle32(struct lfs_disk_entry *d) +{ + d->u.dir[0] = lfs_fromle32(d->u.dir[0]); + d->u.dir[1] = lfs_fromle32(d->u.dir[1]); } -static void lfs_entry_tole32(struct lfs_disk_entry *d) { - d->u.dir[0] = lfs_tole32(d->u.dir[0]); - d->u.dir[1] = lfs_tole32(d->u.dir[1]); +static void lfs_entry_tole32(struct lfs_disk_entry *d) +{ + d->u.dir[0] = lfs_tole32(d->u.dir[0]); + d->u.dir[1] = lfs_tole32(d->u.dir[1]); } -static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) { - d->root[0] = lfs_fromle32(d->root[0]); - d->root[1] = lfs_fromle32(d->root[1]); - d->block_size = lfs_fromle32(d->block_size); - d->block_count = lfs_fromle32(d->block_count); - d->version = lfs_fromle32(d->version); +static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) +{ + d->root[0] = lfs_fromle32(d->root[0]); + d->root[1] = lfs_fromle32(d->root[1]); + d->block_size = lfs_fromle32(d->block_size); + d->block_count = lfs_fromle32(d->block_count); + d->version = lfs_fromle32(d->version); } -static void lfs_superblock_tole32(struct lfs_disk_superblock *d) { - d->root[0] = lfs_tole32(d->root[0]); - d->root[1] = lfs_tole32(d->root[1]); - d->block_size = lfs_tole32(d->block_size); - d->block_count = lfs_tole32(d->block_count); - d->version = lfs_tole32(d->version); +static void lfs_superblock_tole32(struct lfs_disk_superblock *d) +{ + d->root[0] = lfs_tole32(d->root[0]); + d->root[1] = lfs_tole32(d->root[1]); + d->block_size = lfs_tole32(d->block_size); + d->block_count = lfs_tole32(d->block_count); + d->version = lfs_tole32(d->version); } /// Metadata pair and directory operations /// -static inline void lfs_pairswap(lfs_block_t pair[2]) { - lfs_block_t t = pair[0]; - pair[0] = pair[1]; - pair[1] = t; +static inline void lfs_pairswap(lfs_block_t pair[2]) +{ + lfs_block_t t = pair[0]; + pair[0] = pair[1]; + pair[1] = t; } -static inline bool lfs_pairisnull(const lfs_block_t pair[2]) { return pair[0] == 0xffffffff || pair[1] == 0xffffffff; } - -static inline int lfs_paircmp(const lfs_block_t paira[2], const lfs_block_t pairb[2]) { - return !(paira[0] == pairb[0] || paira[1] == pairb[1] || paira[0] == pairb[1] || paira[1] == pairb[0]); +static inline bool lfs_pairisnull(const lfs_block_t pair[2]) +{ + return pair[0] == 0xffffffff || pair[1] == 0xffffffff; } -static inline bool lfs_pairsync(const lfs_block_t paira[2], const lfs_block_t pairb[2]) { - return (paira[0] == pairb[0] && paira[1] == pairb[1]) || (paira[0] == pairb[1] && paira[1] == pairb[0]); +static inline int lfs_paircmp(const lfs_block_t paira[2], const lfs_block_t pairb[2]) +{ + return !(paira[0] == pairb[0] || paira[1] == pairb[1] || paira[0] == pairb[1] || paira[1] == pairb[0]); } -static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) { return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; } - -static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) { - // allocate pair of dir blocks - for (int i = 0; i < 2; i++) { - int err = lfs_alloc(lfs, &dir->pair[i]); - if (err) { - return err; - } - } - - // rather than clobbering one of the blocks we just pretend - // the revision may be valid - int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); - if (err && err != LFS_ERR_CORRUPT) { - return err; - } - - if (err != LFS_ERR_CORRUPT) { - dir->d.rev = lfs_fromle32(dir->d.rev); - } - - // set defaults - dir->d.rev += 1; - dir->d.size = sizeof(dir->d) + 4; - dir->d.tail[0] = 0xffffffff; - dir->d.tail[1] = 0xffffffff; - dir->off = sizeof(dir->d); - - // don't write out yet, let caller take care of that - return 0; +static inline bool lfs_pairsync(const lfs_block_t paira[2], const lfs_block_t pairb[2]) +{ + return (paira[0] == pairb[0] && paira[1] == pairb[1]) || (paira[0] == pairb[1] && paira[1] == pairb[0]); } -static int lfs_dir_fetch(lfs_t *lfs, lfs_dir_t *dir, const lfs_block_t pair[2]) { - // copy out pair, otherwise may be aliasing dir - const lfs_block_t tpair[2] = {pair[0], pair[1]}; - bool valid = false; - - // check both blocks for the most recent revision - for (int i = 0; i < 2; i++) { - struct lfs_disk_dir test; - int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); - lfs_dir_fromle32(&test); - if (err) { - if (err == LFS_ERR_CORRUPT) { - continue; - } - return err; - } - - if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { - continue; - } - - if ((0x7fffffff & test.size) < sizeof(test) + 4 || (0x7fffffff & test.size) > lfs->cfg->block_size) { - continue; - } - - uint32_t crc = 0xffffffff; - lfs_dir_tole32(&test); - lfs_crc(&crc, &test, sizeof(test)); - lfs_dir_fromle32(&test); - err = lfs_bd_crc(lfs, tpair[i], sizeof(test), (0x7fffffff & test.size) - sizeof(test), &crc); - if (err) { - if (err == LFS_ERR_CORRUPT) { - continue; - } - return err; - } - - if (crc != 0) { - continue; - } - - valid = true; - - // setup dir in case it's valid - dir->pair[0] = tpair[(i + 0) % 2]; - dir->pair[1] = tpair[(i + 1) % 2]; - dir->off = sizeof(dir->d); - dir->d = test; - } - - if (!valid) { - LFS_ERROR("Corrupted dir pair at %" PRIu32 " %" PRIu32, tpair[0], tpair[1]); - return LFS_ERR_CORRUPT; - } - - return 0; +static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) +{ + return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; } -struct lfs_region { - lfs_off_t oldoff; - lfs_size_t oldlen; - const void *newdata; - lfs_size_t newlen; -}; - -static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, const struct lfs_region *regions, int count) { - // increment revision count - dir->d.rev += 1; - - // keep pairs in order such that pair[0] is most recent - lfs_pairswap(dir->pair); - for (int i = 0; i < count; i++) { - dir->d.size += regions[i].newlen - regions[i].oldlen; - } - - const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]}; - bool relocated = false; - - while (true) { - - int err = lfs_bd_erase(lfs, dir->pair[0]); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - uint32_t crc = 0xffffffff; - lfs_dir_tole32(&dir->d); - lfs_crc(&crc, &dir->d, sizeof(dir->d)); - err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); - lfs_dir_fromle32(&dir->d); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - int i = 0; - lfs_off_t oldoff = sizeof(dir->d); - lfs_off_t newoff = sizeof(dir->d); - while (newoff < (0x7fffffff & dir->d.size) - 4) { - if (i < count && regions[i].oldoff == oldoff) { - lfs_crc(&crc, regions[i].newdata, regions[i].newlen); - err = lfs_bd_prog(lfs, dir->pair[0], newoff, regions[i].newdata, regions[i].newlen); +static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) +{ + // allocate pair of dir blocks + for (int i = 0; i < 2; i++) { + int err = lfs_alloc(lfs, &dir->pair[i]); if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - oldoff += regions[i].oldlen; - newoff += regions[i].newlen; - i += 1; - } else { - uint8_t data; - err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); - if (err) { - return err; - } - - lfs_crc(&crc, &data, 1); - err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - oldoff += 1; - newoff += 1; - } - } - - crc = lfs_tole32(crc); - err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); - crc = lfs_fromle32(crc); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - err = lfs_bd_sync(lfs); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // successful commit, check checksum to make sure - uint32_t ncrc = 0xffffffff; - err = lfs_bd_crc(lfs, dir->pair[0], 0, (0x7fffffff & dir->d.size) - 4, &ncrc); - if (err) { - return err; - } - - if (ncrc != crc) { - goto relocate; - } - - break; - relocate: - // commit was corrupted - LFS_DEBUG("Bad block at %" PRIu32, dir->pair[0]); - - // drop caches and prepare to relocate block - relocated = true; - lfs_cache_drop(lfs, &lfs->pcache); - - // can't relocate superblock, filesystem is now frozen - if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) { - LFS_WARN("Superblock %" PRIu32 " has become unwritable", oldpair[0]); - return LFS_ERR_CORRUPT; - } - - // relocate half of pair - err = lfs_alloc(lfs, &dir->pair[0]); - if (err) { - return err; - } - } - - if (relocated) { - // update references if we relocated - LFS_DEBUG("Relocating %" PRIu32 " %" PRIu32 " to %" PRIu32 " %" PRIu32, oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]); - int err = lfs_relocate(lfs, oldpair, dir->pair); - if (err) { - return err; - } - } - - // shift over any directories that are affected - for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { - if (lfs_paircmp(d->pair, dir->pair) == 0) { - d->pair[0] = dir->pair[0]; - d->pair[1] = dir->pair[1]; - } - } - - return 0; -} - -static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) { - lfs_entry_tole32(&entry->d); - int err = lfs_dir_commit(lfs, dir, - (struct lfs_region[]){{entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, - {entry->off + sizeof(entry->d), entry->d.nlen, data, entry->d.nlen}}, - data ? 2 : 1); - lfs_entry_fromle32(&entry->d); - return err; -} - -static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) { - // check if we fit, if top bit is set we do not and move on - while (true) { - if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { - entry->off = dir->d.size - 4; - - lfs_entry_tole32(&entry->d); - int err = - lfs_dir_commit(lfs, dir, (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); - lfs_entry_fromle32(&entry->d); - return err; - } - - // we need to allocate a new dir block - if (!(0x80000000 & dir->d.size)) { - lfs_dir_t olddir = *dir; - int err = lfs_dir_alloc(lfs, dir); - if (err) { - return err; - } - - dir->d.tail[0] = olddir.d.tail[0]; - dir->d.tail[1] = olddir.d.tail[1]; - entry->off = dir->d.size - 4; - lfs_entry_tole32(&entry->d); - err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); - lfs_entry_fromle32(&entry->d); - if (err) { - return err; - } - - olddir.d.size |= 0x80000000; - olddir.d.tail[0] = dir->pair[0]; - olddir.d.tail[1] = dir->pair[1]; - return lfs_dir_commit(lfs, &olddir, NULL, 0); - } - - int err = lfs_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } - } -} - -static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { - // check if we should just drop the directory block - if ((dir->d.size & 0x7fffffff) == sizeof(dir->d) + 4 + lfs_entry_size(entry)) { - lfs_dir_t pdir; - int res = lfs_pred(lfs, dir->pair, &pdir); - if (res < 0) { - return res; - } - - if (pdir.d.size & 0x80000000) { - pdir.d.size &= dir->d.size | 0x7fffffff; - pdir.d.tail[0] = dir->d.tail[0]; - pdir.d.tail[1] = dir->d.tail[1]; - return lfs_dir_commit(lfs, &pdir, NULL, 0); - } - } - - // shift out the entry - int err = lfs_dir_commit(lfs, dir, - (struct lfs_region[]){ - {entry->off, lfs_entry_size(entry), NULL, 0}, - }, - 1); - if (err) { - return err; - } - - // shift over any files/directories that are affected - for (lfs_file_t *f = lfs->files; f; f = f->next) { - if (lfs_paircmp(f->pair, dir->pair) == 0) { - if (f->poff == entry->off) { - f->pair[0] = 0xffffffff; - f->pair[1] = 0xffffffff; - } else if (f->poff > entry->off) { - f->poff -= lfs_entry_size(entry); - } - } - } - - for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { - if (lfs_paircmp(d->pair, dir->pair) == 0) { - if (d->off > entry->off) { - d->off -= lfs_entry_size(entry); - d->pos -= lfs_entry_size(entry); - } - } - } - - return 0; -} - -static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { - while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size) - 4) { - if (!(0x80000000 & dir->d.size)) { - entry->off = dir->off; - return LFS_ERR_NOENT; - } - - int err = lfs_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } - - dir->off = sizeof(dir->d); - dir->pos += sizeof(dir->d) + 4; - } - - int err = lfs_bd_read(lfs, dir->pair[0], dir->off, &entry->d, sizeof(entry->d)); - lfs_entry_fromle32(&entry->d); - if (err) { - return err; - } - - entry->off = dir->off; - dir->off += lfs_entry_size(entry); - dir->pos += lfs_entry_size(entry); - return 0; -} - -static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const char **path) { - const char *pathname = *path; - size_t pathlen; - entry->d.type = LFS_TYPE_DIR; - entry->d.elen = sizeof(entry->d) - 4; - entry->d.alen = 0; - entry->d.nlen = 0; - entry->d.u.dir[0] = lfs->root[0]; - entry->d.u.dir[1] = lfs->root[1]; - - while (true) { - nextname: - // skip slashes - pathname += strspn(pathname, "/"); - pathlen = strcspn(pathname, "/"); - - // skip '.' and root '..' - if ((pathlen == 1 && memcmp(pathname, ".", 1) == 0) || (pathlen == 2 && memcmp(pathname, "..", 2) == 0)) { - pathname += pathlen; - goto nextname; - } - - // skip if matched by '..' in name - const char *suffix = pathname + pathlen; - size_t sufflen; - int depth = 1; - while (true) { - suffix += strspn(suffix, "/"); - sufflen = strcspn(suffix, "/"); - if (sufflen == 0) { - break; - } - - if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { - depth -= 1; - if (depth == 0) { - pathname = suffix + sufflen; - goto nextname; - } - } else { - depth += 1; - } - - suffix += sufflen; - } - - // found path - if (pathname[0] == '\0') { - return 0; - } - - // update what we've found - *path = pathname; - - // continue on if we hit a directory - if (entry->d.type != LFS_TYPE_DIR) { - return LFS_ERR_NOTDIR; - } - - int err = lfs_dir_fetch(lfs, dir, entry->d.u.dir); - if (err) { - return err; - } - - // find entry matching name - while (true) { - err = lfs_dir_next(lfs, dir, entry); - if (err) { - return err; - } - - if (((0x7f & entry->d.type) != LFS_TYPE_REG && (0x7f & entry->d.type) != LFS_TYPE_DIR) || entry->d.nlen != pathlen) { - continue; - } - - int res = lfs_bd_cmp(lfs, dir->pair[0], entry->off + 4 + entry->d.elen + entry->d.alen, pathname, pathlen); - if (res < 0) { - return res; - } - - // found match - if (res) { - break; - } - } - - // check that entry has not been moved - if (entry->d.type & 0x80) { - int moved = lfs_moved(lfs, &entry->d.u); - if (moved) { - return (moved < 0) ? moved : LFS_ERR_NOENT; - } - - entry->d.type &= ~0x80; - } - - // to next name - pathname += pathlen; - } -} - -/// Top level directory operations /// -int lfs_mkdir(lfs_t *lfs, const char *path) { - // deorphan if we haven't yet, needed at most once after poweron - if (!lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - // fetch parent directory - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); - if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) { - return err ? err : LFS_ERR_EXIST; - } - - // build up new directory - lfs_alloc_ack(lfs); - - lfs_dir_t dir; - err = lfs_dir_alloc(lfs, &dir); - if (err) { - return err; - } - dir.d.tail[0] = cwd.d.tail[0]; - dir.d.tail[1] = cwd.d.tail[1]; - - err = lfs_dir_commit(lfs, &dir, NULL, 0); - if (err) { - return err; - } - - entry.d.type = LFS_TYPE_DIR; - entry.d.elen = sizeof(entry.d) - 4; - entry.d.alen = 0; - entry.d.nlen = strlen(path); - entry.d.u.dir[0] = dir.pair[0]; - entry.d.u.dir[1] = dir.pair[1]; - - cwd.d.tail[0] = dir.pair[0]; - cwd.d.tail[1] = dir.pair[1]; - - err = lfs_dir_append(lfs, &cwd, &entry, path); - if (err) { - return err; - } - - lfs_alloc_ack(lfs); - return 0; -} - -int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { - dir->pair[0] = lfs->root[0]; - dir->pair[1] = lfs->root[1]; - - lfs_entry_t entry; - int err = lfs_dir_find(lfs, dir, &entry, &path); - if (err) { - return err; - } else if (entry.d.type != LFS_TYPE_DIR) { - return LFS_ERR_NOTDIR; - } - - err = lfs_dir_fetch(lfs, dir, entry.d.u.dir); - if (err) { - return err; - } - - // setup head dir - // special offset for '.' and '..' - dir->head[0] = dir->pair[0]; - dir->head[1] = dir->pair[1]; - dir->pos = sizeof(dir->d) - 2; - dir->off = sizeof(dir->d); - - // add to list of directories - dir->next = lfs->dirs; - lfs->dirs = dir; - - return 0; -} - -int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { - // remove from list of directories - for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) { - if (*p == dir) { - *p = dir->next; - break; - } - } - - return 0; -} - -int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { - memset(info, 0, sizeof(*info)); - - // special offset for '.' and '..' - if (dir->pos == sizeof(dir->d) - 2) { - info->type = LFS_TYPE_DIR; - strcpy(info->name, "."); - dir->pos += 1; - return 1; - } else if (dir->pos == sizeof(dir->d) - 1) { - info->type = LFS_TYPE_DIR; - strcpy(info->name, ".."); - dir->pos += 1; - return 1; - } - - lfs_entry_t entry; - while (true) { - int err = lfs_dir_next(lfs, dir, &entry); - if (err) { - return (err == LFS_ERR_NOENT) ? 0 : err; - } - - if ((0x7f & entry.d.type) != LFS_TYPE_REG && (0x7f & entry.d.type) != LFS_TYPE_DIR) { - continue; - } - - // check that entry has not been moved - if (entry.d.type & 0x80) { - int moved = lfs_moved(lfs, &entry.d.u); - if (moved < 0) { - return moved; - } - - if (moved) { - continue; - } - - entry.d.type &= ~0x80; - } - - break; - } - - info->type = entry.d.type; - if (info->type == LFS_TYPE_REG) { - info->size = entry.d.u.file.size; - } - - int err = lfs_bd_read(lfs, dir->pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); - if (err) { - return err; - } - - return 1; -} - -int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { - // simply walk from head dir - int err = lfs_dir_rewind(lfs, dir); - if (err) { - return err; - } - dir->pos = off; - - while (off > (0x7fffffff & dir->d.size)) { - off -= 0x7fffffff & dir->d.size; - if (!(0x80000000 & dir->d.size)) { - return LFS_ERR_INVAL; - } - - err = lfs_dir_fetch(lfs, dir, dir->d.tail); - if (err) { - return err; - } - } - - dir->off = off; - return 0; -} - -int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) { - // reload the head dir - int err = lfs_dir_fetch(lfs, dir, dir->head); - if (err) { - return err; - } - - dir->pair[0] = dir->head[0]; - dir->pair[1] = dir->head[1]; - dir->pos = sizeof(dir->d) - 2; - dir->off = sizeof(dir->d); - return 0; -} - -/// File index list operations /// -static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { - lfs_off_t size = *off; - lfs_off_t b = lfs->cfg->block_size - 2 * 4; - lfs_off_t i = size / b; - if (i == 0) { - return 0; - } - - i = (size - 4 * (lfs_popc(i - 1) + 2)) / b; - *off = size - b * i - 4 * lfs_popc(i); - return i; -} - -static int lfs_ctz_find(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, lfs_size_t pos, - lfs_block_t *block, lfs_off_t *off) { - if (size == 0) { - *block = 0xffffffff; - *off = 0; - return 0; - } - - lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); - lfs_off_t target = lfs_ctz_index(lfs, &pos); - - while (current > target) { - lfs_size_t skip = lfs_min(lfs_npw2(current - target + 1) - 1, lfs_ctz(current)); - - int err = lfs_cache_read(lfs, rcache, pcache, head, 4 * skip, &head, 4); - head = lfs_fromle32(head); - if (err) { - return err; - } - - LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); - current -= 1 << skip; - } - - *block = head; - *off = pos; - return 0; -} - -static int lfs_ctz_extend(lfs_t *lfs, lfs_cache_t *rcache, lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, lfs_block_t *block, - lfs_off_t *off) { - while (true) { - // go ahead and grab a block - lfs_block_t nblock; - int err = lfs_alloc(lfs, &nblock); - if (err) { - return err; - } - LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); - - if (true) { - err = lfs_bd_erase(lfs, nblock); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - if (size == 0) { - *block = nblock; - *off = 0; - return 0; - } - - size -= 1; - lfs_off_t index = lfs_ctz_index(lfs, &size); - size += 1; - - // just copy out the last block if it is incomplete - if (size != lfs->cfg->block_size) { - for (lfs_off_t i = 0; i < size; i++) { - uint8_t data; - err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1); - if (err) { return err; - } + } + } - err = lfs_cache_prog(lfs, pcache, rcache, nblock, i, &data, 1); - if (err) { + // rather than clobbering one of the blocks we just pretend + // the revision may be valid + int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + if (err != LFS_ERR_CORRUPT) { + dir->d.rev = lfs_fromle32(dir->d.rev); + } + + // set defaults + dir->d.rev += 1; + dir->d.size = sizeof(dir->d) + 4; + dir->d.tail[0] = 0xffffffff; + dir->d.tail[1] = 0xffffffff; + dir->off = sizeof(dir->d); + + // don't write out yet, let caller take care of that + return 0; +} + +static int lfs_dir_fetch(lfs_t *lfs, lfs_dir_t *dir, const lfs_block_t pair[2]) +{ + // copy out pair, otherwise may be aliasing dir + const lfs_block_t tpair[2] = {pair[0], pair[1]}; + bool valid = false; + + // check both blocks for the most recent revision + for (int i = 0; i < 2; i++) { + struct lfs_disk_dir test; + int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); + lfs_dir_fromle32(&test); + if (err) { if (err == LFS_ERR_CORRUPT) { - goto relocate; + continue; } return err; - } } - *block = nblock; - *off = size; - return 0; - } + if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { + continue; + } - // append block - index += 1; - lfs_size_t skips = lfs_ctz(index) + 1; + if ((0x7fffffff & test.size) < sizeof(test) + 4 || (0x7fffffff & test.size) > lfs->cfg->block_size) { + continue; + } - for (lfs_off_t i = 0; i < skips; i++) { - head = lfs_tole32(head); - err = lfs_cache_prog(lfs, pcache, rcache, nblock, 4 * i, &head, 4); - head = lfs_fromle32(head); + uint32_t crc = 0xffffffff; + lfs_dir_tole32(&test); + lfs_crc(&crc, &test, sizeof(test)); + lfs_dir_fromle32(&test); + err = lfs_bd_crc(lfs, tpair[i], sizeof(test), (0x7fffffff & test.size) - sizeof(test), &crc); if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - if (i != skips - 1) { - err = lfs_cache_read(lfs, rcache, NULL, head, 4 * i, &head, 4); - head = lfs_fromle32(head); - if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } return err; - } } - LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); - } - - *block = nblock; - *off = 4 * skips; - return 0; - } - - relocate: - LFS_DEBUG("Bad block at %" PRIu32, nblock); - - // just clear cache and try a new block - lfs_cache_drop(lfs, &lfs->pcache); - } -} - -static int lfs_ctz_traverse(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, - int (*cb)(void *, lfs_block_t), void *data) { - if (size == 0) { - return 0; - } - - lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); - - while (true) { - int err = cb(data, head); - if (err) { - return err; - } - - if (index == 0) { - return 0; - } - - lfs_block_t heads[2]; - int count = 2 - (index & 1); - err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count * 4); - heads[0] = lfs_fromle32(heads[0]); - heads[1] = lfs_fromle32(heads[1]); - if (err) { - return err; - } - - for (int i = 0; i < count - 1; i++) { - err = cb(data, heads[i]); - if (err) { - return err; - } - } - - head = heads[count - 1]; - index -= count; - } -} - -/// Top level file operations /// -int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, const char *path, int flags, const struct lfs_file_config *cfg) { - // deorphan if we haven't yet, needed at most once after poweron - if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - // allocate entry for file if it doesn't exist - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); - if (err && (err != LFS_ERR_NOENT || strchr(path, '/') != NULL)) { - return err; - } - - if (err == LFS_ERR_NOENT) { - if (!(flags & LFS_O_CREAT)) { - return LFS_ERR_NOENT; - } - - // create entry to remember name - entry.d.type = LFS_TYPE_REG; - entry.d.elen = sizeof(entry.d) - 4; - entry.d.alen = 0; - entry.d.nlen = strlen(path); - entry.d.u.file.head = 0xffffffff; - entry.d.u.file.size = 0; - err = lfs_dir_append(lfs, &cwd, &entry, path); - if (err) { - return err; - } - } else if (entry.d.type == LFS_TYPE_DIR) { - return LFS_ERR_ISDIR; - } else if (flags & LFS_O_EXCL) { - return LFS_ERR_EXIST; - } - - // setup file struct - file->cfg = cfg; - file->pair[0] = cwd.pair[0]; - file->pair[1] = cwd.pair[1]; - file->poff = entry.off; - file->head = entry.d.u.file.head; - file->size = entry.d.u.file.size; - file->flags = flags; - file->pos = 0; - - if (flags & LFS_O_TRUNC) { - if (file->size != 0) { - file->flags |= LFS_F_DIRTY; - } - file->head = 0xffffffff; - file->size = 0; - } - - // allocate buffer if needed - file->cache.block = 0xffffffff; - if (file->cfg && file->cfg->buffer) { - file->cache.buffer = file->cfg->buffer; - } else if (lfs->cfg->file_buffer) { - if (lfs->files) { - // already in use - return LFS_ERR_NOMEM; - } - file->cache.buffer = lfs->cfg->file_buffer; - } else if ((file->flags & 3) == LFS_O_RDONLY) { - file->cache.buffer = lfs_malloc(lfs->cfg->read_size); - if (!file->cache.buffer) { - return LFS_ERR_NOMEM; - } - } else { - file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); - if (!file->cache.buffer) { - return LFS_ERR_NOMEM; - } - } - - // zero to avoid information leak - lfs_cache_drop(lfs, &file->cache); - if ((file->flags & 3) != LFS_O_RDONLY) { - lfs_cache_zero(lfs, &file->cache); - } - - // add to list of files - file->next = lfs->files; - lfs->files = file; - - return 0; -} - -int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) { return lfs_file_opencfg(lfs, file, path, flags, NULL); } - -int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { - int err = lfs_file_sync(lfs, file); - - // remove from list of files - for (lfs_file_t **p = &lfs->files; *p; p = &(*p)->next) { - if (*p == file) { - *p = file->next; - break; - } - } - - // clean up memory - if (!(file->cfg && file->cfg->buffer) && !lfs->cfg->file_buffer) { - lfs_free(file->cache.buffer); - } - - return err; -} - -static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) { -relocate: - LFS_DEBUG("Bad block at %" PRIu32, file->block); - - // just relocate what exists into new block - lfs_block_t nblock; - int err = lfs_alloc(lfs, &nblock); - if (err) { - return err; - } - - err = lfs_bd_erase(lfs, nblock); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - // either read from dirty cache or disk - for (lfs_off_t i = 0; i < file->off; i++) { - uint8_t data; - err = lfs_cache_read(lfs, &lfs->rcache, &file->cache, file->block, i, &data, 1); - if (err) { - return err; - } - - err = lfs_cache_prog(lfs, &lfs->pcache, &lfs->rcache, nblock, i, &data, 1); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - } - - // copy over new state of file - memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size); - file->cache.block = lfs->pcache.block; - file->cache.off = lfs->pcache.off; - lfs_cache_zero(lfs, &lfs->pcache); - - file->block = nblock; - return 0; -} - -static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) { - if (file->flags & LFS_F_READING) { - // just drop read cache - lfs_cache_drop(lfs, &file->cache); - file->flags &= ~LFS_F_READING; - } - - if (file->flags & LFS_F_WRITING) { - lfs_off_t pos = file->pos; - - // copy over anything after current branch - lfs_file_t orig = { - .head = file->head, - .size = file->size, - .flags = LFS_O_RDONLY, - .pos = file->pos, - .cache = lfs->rcache, - }; - lfs_cache_drop(lfs, &lfs->rcache); - - while (file->pos < file->size) { - // copy over a byte at a time, leave it up to caching - // to make this efficient - uint8_t data; - lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1); - if (res < 0) { - return res; - } - - res = lfs_file_write(lfs, file, &data, 1); - if (res < 0) { - return res; - } - - // keep our reference to the rcache in sync - if (lfs->rcache.block != 0xffffffff) { - lfs_cache_drop(lfs, &orig.cache); - lfs_cache_drop(lfs, &lfs->rcache); - } - } - - // write out what we have - while (true) { - int err = lfs_cache_flush(lfs, &file->cache, &lfs->rcache); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - return err; - } - - break; - relocate: - err = lfs_file_relocate(lfs, file); - if (err) { - return err; - } - } - - // actual file updates - file->head = file->block; - file->size = file->pos; - file->flags &= ~LFS_F_WRITING; - file->flags |= LFS_F_DIRTY; - - file->pos = pos; - } - - return 0; -} - -int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - - if ((file->flags & LFS_F_DIRTY) && !(file->flags & LFS_F_ERRED) && !lfs_pairisnull(file->pair)) { - // update dir entry - lfs_dir_t cwd; - err = lfs_dir_fetch(lfs, &cwd, file->pair); - if (err) { - return err; - } - - lfs_entry_t entry = {.off = file->poff}; - err = lfs_bd_read(lfs, cwd.pair[0], entry.off, &entry.d, sizeof(entry.d)); - lfs_entry_fromle32(&entry.d); - if (err) { - return err; - } - - LFS_ASSERT(entry.d.type == LFS_TYPE_REG); - entry.d.u.file.head = file->head; - entry.d.u.file.size = file->size; - - err = lfs_dir_update(lfs, &cwd, &entry, NULL); - if (err) { - return err; - } - - file->flags &= ~LFS_F_DIRTY; - } - - return 0; -} - -lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size) { - uint8_t *data = buffer; - lfs_size_t nsize = size; - - if ((file->flags & 3) == LFS_O_WRONLY) { - return LFS_ERR_BADF; - } - - if (file->flags & LFS_F_WRITING) { - // flush out any writes - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - } - - if (file->pos >= file->size) { - // eof if past end - return 0; - } - - size = lfs_min(size, file->size - file->pos); - nsize = size; - - while (nsize > 0) { - // check if we need a new block - if (!(file->flags & LFS_F_READING) || file->off == lfs->cfg->block_size) { - int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos, &file->block, &file->off); - if (err) { - return err; - } - - file->flags |= LFS_F_READING; - } - - // read as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); - int err = lfs_cache_read(lfs, &file->cache, NULL, file->block, file->off, data, diff); - if (err) { - return err; - } - - file->pos += diff; - file->off += diff; - data += diff; - nsize -= diff; - } - - return size; -} - -lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size) { - const uint8_t *data = buffer; - lfs_size_t nsize = size; - - if ((file->flags & 3) == LFS_O_RDONLY) { - return LFS_ERR_BADF; - } - - if (file->flags & LFS_F_READING) { - // drop any reads - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - } - - if ((file->flags & LFS_O_APPEND) && file->pos < file->size) { - file->pos = file->size; - } - - if (!(file->flags & LFS_F_WRITING) && file->pos > file->size) { - // fill with zeros - lfs_off_t pos = file->pos; - file->pos = file->size; - - while (file->pos < pos) { - lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); - if (res < 0) { - return res; - } - } - } - - while (nsize > 0) { - // check if we need a new block - if (!(file->flags & LFS_F_WRITING) || file->off == lfs->cfg->block_size) { - if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { - // find out which block we're extending from - int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos - 1, &file->block, &file->off); - if (err) { - file->flags |= LFS_F_ERRED; - return err; + if (crc != 0) { + continue; } - // mark cache as dirty since we may have read data into it - lfs_cache_zero(lfs, &file->cache); - } + valid = true; - // extend file with new blocks - lfs_alloc_ack(lfs); - int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache, file->block, file->pos, &file->block, &file->off); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - - file->flags |= LFS_F_WRITING; - } - - // program as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); - while (true) { - int err = lfs_cache_prog(lfs, &file->cache, &lfs->rcache, file->block, file->off, data, diff); - if (err) { - if (err == LFS_ERR_CORRUPT) { - goto relocate; - } - file->flags |= LFS_F_ERRED; - return err; - } - - break; - relocate: - err = lfs_file_relocate(lfs, file); - if (err) { - file->flags |= LFS_F_ERRED; - return err; - } - } - - file->pos += diff; - file->off += diff; - data += diff; - nsize -= diff; - - lfs_alloc_ack(lfs); - } - - file->flags &= ~LFS_F_ERRED; - return size; -} - -lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence) { - // write out everything beforehand, may be noop if rdonly - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - - // update pos - if (whence == LFS_SEEK_SET) { - file->pos = off; - } else if (whence == LFS_SEEK_CUR) { - if (off < 0 && (lfs_off_t)-off > file->pos) { - return LFS_ERR_INVAL; - } - - file->pos = file->pos + off; - } else if (whence == LFS_SEEK_END) { - if (off < 0 && (lfs_off_t)-off > file->size) { - return LFS_ERR_INVAL; - } - - file->pos = file->size + off; - } - - return file->pos; -} - -int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { - if ((file->flags & 3) == LFS_O_RDONLY) { - return LFS_ERR_BADF; - } - - lfs_off_t oldsize = lfs_file_size(lfs, file); - if (size < oldsize) { - // need to flush since directly changing metadata - int err = lfs_file_flush(lfs, file); - if (err) { - return err; - } - - // lookup new head in ctz skip list - err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, size, &file->head, &(lfs_off_t){0}); - if (err) { - return err; - } - - file->size = size; - file->flags |= LFS_F_DIRTY; - } else if (size > oldsize) { - lfs_off_t pos = file->pos; - - // flush+seek if not already at end - if (file->pos != oldsize) { - int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); - if (err < 0) { - return err; - } - } - - // fill with zeros - while (file->pos < size) { - lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); - if (res < 0) { - return res; - } - } - - // restore pos - int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); - if (err < 0) { - return err; - } - } - - return 0; -} - -lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t const *file) { - (void)lfs; - return file->pos; -} - -int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { - lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET); - if (res < 0) { - return res; - } - - return 0; -} - -lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { - (void)lfs; - if (file->flags & LFS_F_WRITING) { - return lfs_max(file->pos, file->size); - } else { - return file->size; - } -} - -/// General fs operations /// -int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); - if (err) { - return err; - } - - memset(info, 0, sizeof(*info)); - info->type = entry.d.type; - if (info->type == LFS_TYPE_REG) { - info->size = entry.d.u.file.size; - } - - if (lfs_paircmp(entry.d.u.dir, lfs->root) == 0) { - strcpy(info->name, "/"); - } else { - err = lfs_bd_read(lfs, cwd.pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); - if (err) { - return err; - } - } - - return 0; -} - -int lfs_remove(lfs_t *lfs, const char *path) { - // deorphan if we haven't yet, needed at most once after poweron - if (!lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - lfs_dir_t cwd; - lfs_entry_t entry; - int err = lfs_dir_find(lfs, &cwd, &entry, &path); - if (err) { - return err; - } - - lfs_dir_t dir; - if (entry.d.type == LFS_TYPE_DIR) { - // must be empty before removal, checking size - // without masking top bit checks for any case where - // dir is not empty - err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); - if (err) { - return err; - } /* else if (dir.d.size != sizeof(dir.d)+4) { - return LFS_ERR_NOTEMPTY; - } allow to remove non-empty folder, - According to below issue, comment these 2 line won't corrupt filesystem - https://github.com/ARMmbed/littlefs/issues/43 */ - } - - // remove the entry - err = lfs_dir_remove(lfs, &cwd, &entry); - if (err) { - return err; - } - - // if we were a directory, find pred, replace tail - if (entry.d.type == LFS_TYPE_DIR) { - int res = lfs_pred(lfs, dir.pair, &cwd); - if (res < 0) { - return res; - } - - LFS_ASSERT(res); // must have pred - cwd.d.tail[0] = dir.d.tail[0]; - cwd.d.tail[1] = dir.d.tail[1]; - - err = lfs_dir_commit(lfs, &cwd, NULL, 0); - if (err) { - return err; - } - } - - return 0; -} - -int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { - // deorphan if we haven't yet, needed at most once after poweron - if (!lfs->deorphaned) { - int err = lfs_deorphan(lfs); - if (err) { - return err; - } - } - - // find old entry - lfs_dir_t oldcwd; - lfs_entry_t oldentry; - int err = lfs_dir_find(lfs, &oldcwd, &oldentry, &oldpath); - if (err) { - return err; - } - - // allocate new entry - lfs_dir_t newcwd; - lfs_entry_t preventry; - err = lfs_dir_find(lfs, &newcwd, &preventry, &newpath); - if (err && (err != LFS_ERR_NOENT || strchr(newpath, '/') != NULL)) { - return err; - } - - bool prevexists = (err != LFS_ERR_NOENT); - bool samepair = (lfs_paircmp(oldcwd.pair, newcwd.pair) == 0); - - // must have same type - if (prevexists && preventry.d.type != oldentry.d.type) { - return LFS_ERR_ISDIR; - } - - lfs_dir_t dir; - if (prevexists && preventry.d.type == LFS_TYPE_DIR) { - // must be empty before removal, checking size - // without masking top bit checks for any case where - // dir is not empty - err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); - if (err) { - return err; - } else if (dir.d.size != sizeof(dir.d) + 4) { - return LFS_ERR_NOTEMPTY; - } - } - - // mark as moving - oldentry.d.type |= 0x80; - err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL); - if (err) { - return err; - } - - // update pair if newcwd == oldcwd - if (samepair) { - newcwd = oldcwd; - } - - // move to new location - lfs_entry_t newentry = preventry; - newentry.d = oldentry.d; - newentry.d.type &= ~0x80; - newentry.d.nlen = strlen(newpath); - - if (prevexists) { - err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); - if (err) { - return err; - } - } else { - err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); - if (err) { - return err; - } - } - - // update pair if newcwd == oldcwd - if (samepair) { - oldcwd = newcwd; - } - - // remove old entry - err = lfs_dir_remove(lfs, &oldcwd, &oldentry); - if (err) { - return err; - } - - // if we were a directory, find pred, replace tail - if (prevexists && preventry.d.type == LFS_TYPE_DIR) { - int res = lfs_pred(lfs, dir.pair, &newcwd); - if (res < 0) { - return res; - } - - LFS_ASSERT(res); // must have pred - newcwd.d.tail[0] = dir.d.tail[0]; - newcwd.d.tail[1] = dir.d.tail[1]; - - err = lfs_dir_commit(lfs, &newcwd, NULL, 0); - if (err) { - return err; - } - } - - return 0; -} - -/// Filesystem operations /// -static void lfs_deinit(lfs_t *lfs) { - // free allocated memory - if (!lfs->cfg->read_buffer) { - lfs_free(lfs->rcache.buffer); - } - - if (!lfs->cfg->prog_buffer) { - lfs_free(lfs->pcache.buffer); - } - - if (!lfs->cfg->lookahead_buffer) { - lfs_free(lfs->free.buffer); - } -} - -static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { - lfs->cfg = cfg; - - // setup read cache - if (lfs->cfg->read_buffer) { - lfs->rcache.buffer = lfs->cfg->read_buffer; - } else { - lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); - if (!lfs->rcache.buffer) { - goto cleanup; - } - } - - // setup program cache - if (lfs->cfg->prog_buffer) { - lfs->pcache.buffer = lfs->cfg->prog_buffer; - } else { - lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); - if (!lfs->pcache.buffer) { - goto cleanup; - } - } - - // zero to avoid information leaks - lfs_cache_zero(lfs, &lfs->pcache); - lfs_cache_drop(lfs, &lfs->rcache); - - // setup lookahead, round down to nearest 32-bits - LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); - LFS_ASSERT(lfs->cfg->lookahead > 0); - if (lfs->cfg->lookahead_buffer) { - lfs->free.buffer = lfs->cfg->lookahead_buffer; - } else { - lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead / 8); - if (!lfs->free.buffer) { - goto cleanup; - } - } - - // check that program and read sizes are multiples of the block size - LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); - LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->prog_size == 0); - - // check that the block size is large enough to fit ctz pointers - LFS_ASSERT(4 * lfs_npw2(0xffffffff / (lfs->cfg->block_size - 2 * 4)) <= lfs->cfg->block_size); - - // setup default state - lfs->root[0] = 0xffffffff; - lfs->root[1] = 0xffffffff; - lfs->files = NULL; - lfs->dirs = NULL; - lfs->deorphaned = false; - - return 0; - -cleanup: - lfs_deinit(lfs); - return LFS_ERR_NOMEM; -} - -int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { - int err = 0; - if (true) { - err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - // create free lookahead - memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); - lfs->free.off = 0; - lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); - lfs->free.i = 0; - lfs_alloc_ack(lfs); - - // create superblock dir - lfs_dir_t superdir; - err = lfs_dir_alloc(lfs, &superdir); - if (err) { - goto cleanup; - } - - // write root directory - lfs_dir_t root; - err = lfs_dir_alloc(lfs, &root); - if (err) { - goto cleanup; - } - - err = lfs_dir_commit(lfs, &root, NULL, 0); - if (err) { - goto cleanup; - } - - lfs->root[0] = root.pair[0]; - lfs->root[1] = root.pair[1]; - - // write superblocks - lfs_superblock_t superblock = { - .off = sizeof(superdir.d), - .d.type = LFS_TYPE_SUPERBLOCK, - .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, - .d.nlen = sizeof(superblock.d.magic), - .d.version = LFS_DISK_VERSION, - .d.magic = {"littlefs"}, - .d.block_size = lfs->cfg->block_size, - .d.block_count = lfs->cfg->block_count, - .d.root = {lfs->root[0], lfs->root[1]}, - }; - superdir.d.tail[0] = root.pair[0]; - superdir.d.tail[1] = root.pair[1]; - superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; - - // write both pairs to be safe - lfs_superblock_tole32(&superblock.d); - bool valid = false; - for (int i = 0; i < 2; i++) { - err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){{sizeof(superdir.d), sizeof(superblock.d), &superblock.d, sizeof(superblock.d)}}, 1); - if (err && err != LFS_ERR_CORRUPT) { - goto cleanup; - } - - valid = valid || !err; + // setup dir in case it's valid + dir->pair[0] = tpair[(i + 0) % 2]; + dir->pair[1] = tpair[(i + 1) % 2]; + dir->off = sizeof(dir->d); + dir->d = test; } if (!valid) { - err = LFS_ERR_CORRUPT; - goto cleanup; - } - - // sanity check that fetch works - err = lfs_dir_fetch(lfs, &superdir, (const lfs_block_t[2]){0, 1}); - if (err) { - goto cleanup; - } - - lfs_alloc_ack(lfs); - } - -cleanup: - lfs_deinit(lfs); - return err; -} - -int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { - int err = 0; - if (true) { - err = lfs_init(lfs, cfg); - if (err) { - return err; - } - - // setup free lookahead - lfs->free.off = 0; - lfs->free.size = 0; - lfs->free.i = 0; - lfs_alloc_ack(lfs); - - // load superblock - lfs_dir_t dir; - lfs_superblock_t superblock; - err = lfs_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); - if (err && err != LFS_ERR_CORRUPT) { - goto cleanup; - } - - if (!err) { - err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), &superblock.d, sizeof(superblock.d)); - lfs_superblock_fromle32(&superblock.d); - if (err) { - goto cleanup; - } - - lfs->root[0] = superblock.d.root[0]; - lfs->root[1] = superblock.d.root[1]; - } - - if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { - LFS_ERROR("Invalid superblock at %d %d", 0, 1); - err = LFS_ERR_CORRUPT; - goto cleanup; - } - - uint16_t major_version = (0xffff & (superblock.d.version >> 16)); - uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); - if ((major_version != LFS_DISK_VERSION_MAJOR || minor_version > LFS_DISK_VERSION_MINOR)) { - LFS_ERROR("Invalid version %d.%d", major_version, minor_version); - err = LFS_ERR_INVAL; - goto cleanup; + LFS_ERROR("Corrupted dir pair at %" PRIu32 " %" PRIu32, tpair[0], tpair[1]); + return LFS_ERR_CORRUPT; } return 0; - } +} + +struct lfs_region { + lfs_off_t oldoff; + lfs_size_t oldlen; + const void *newdata; + lfs_size_t newlen; +}; + +static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, const struct lfs_region *regions, int count) +{ + // increment revision count + dir->d.rev += 1; + + // keep pairs in order such that pair[0] is most recent + lfs_pairswap(dir->pair); + for (int i = 0; i < count; i++) { + dir->d.size += regions[i].newlen - regions[i].oldlen; + } + + const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]}; + bool relocated = false; + + while (true) { + + int err = lfs_bd_erase(lfs, dir->pair[0]); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + uint32_t crc = 0xffffffff; + lfs_dir_tole32(&dir->d); + lfs_crc(&crc, &dir->d, sizeof(dir->d)); + err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); + lfs_dir_fromle32(&dir->d); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + int i = 0; + lfs_off_t oldoff = sizeof(dir->d); + lfs_off_t newoff = sizeof(dir->d); + while (newoff < (0x7fffffff & dir->d.size) - 4) { + if (i < count && regions[i].oldoff == oldoff) { + lfs_crc(&crc, regions[i].newdata, regions[i].newlen); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, regions[i].newdata, regions[i].newlen); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + oldoff += regions[i].oldlen; + newoff += regions[i].newlen; + i += 1; + } else { + uint8_t data; + err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); + if (err) { + return err; + } + + lfs_crc(&crc, &data, 1); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + oldoff += 1; + newoff += 1; + } + } + + crc = lfs_tole32(crc); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); + crc = lfs_fromle32(crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + err = lfs_bd_sync(lfs); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // successful commit, check checksum to make sure + uint32_t ncrc = 0xffffffff; + err = lfs_bd_crc(lfs, dir->pair[0], 0, (0x7fffffff & dir->d.size) - 4, &ncrc); + if (err) { + return err; + } + + if (ncrc != crc) { + goto relocate; + } + + break; + relocate: + // commit was corrupted + LFS_DEBUG("Bad block at %" PRIu32, dir->pair[0]); + + // drop caches and prepare to relocate block + relocated = true; + lfs_cache_drop(lfs, &lfs->pcache); + + // can't relocate superblock, filesystem is now frozen + if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) { + LFS_WARN("Superblock %" PRIu32 " has become unwritable", oldpair[0]); + return LFS_ERR_CORRUPT; + } + + // relocate half of pair + err = lfs_alloc(lfs, &dir->pair[0]); + if (err) { + return err; + } + } + + if (relocated) { + // update references if we relocated + LFS_DEBUG("Relocating %" PRIu32 " %" PRIu32 " to %" PRIu32 " %" PRIu32, oldpair[0], oldpair[1], dir->pair[0], + dir->pair[1]); + int err = lfs_relocate(lfs, oldpair, dir->pair); + if (err) { + return err; + } + } + + // shift over any directories that are affected + for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { + if (lfs_paircmp(d->pair, dir->pair) == 0) { + d->pair[0] = dir->pair[0]; + d->pair[1] = dir->pair[1]; + } + } + + return 0; +} + +static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) +{ + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit(lfs, dir, + (struct lfs_region[]){{entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, + {entry->off + sizeof(entry->d), entry->d.nlen, data, entry->d.nlen}}, + data ? 2 : 1); + lfs_entry_fromle32(&entry->d); + return err; +} + +static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) +{ + // check if we fit, if top bit is set we do not and move on + while (true) { + if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { + entry->off = dir->d.size - 4; + + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit( + lfs, dir, + (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); + lfs_entry_fromle32(&entry->d); + return err; + } + + // we need to allocate a new dir block + if (!(0x80000000 & dir->d.size)) { + lfs_dir_t olddir = *dir; + int err = lfs_dir_alloc(lfs, dir); + if (err) { + return err; + } + + dir->d.tail[0] = olddir.d.tail[0]; + dir->d.tail[1] = olddir.d.tail[1]; + entry->off = dir->d.size - 4; + lfs_entry_tole32(&entry->d); + err = lfs_dir_commit( + lfs, dir, + (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); + lfs_entry_fromle32(&entry->d); + if (err) { + return err; + } + + olddir.d.size |= 0x80000000; + olddir.d.tail[0] = dir->pair[0]; + olddir.d.tail[1] = dir->pair[1]; + return lfs_dir_commit(lfs, &olddir, NULL, 0); + } + + int err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + } +} + +static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) +{ + // check if we should just drop the directory block + if ((dir->d.size & 0x7fffffff) == sizeof(dir->d) + 4 + lfs_entry_size(entry)) { + lfs_dir_t pdir; + int res = lfs_pred(lfs, dir->pair, &pdir); + if (res < 0) { + return res; + } + + if (pdir.d.size & 0x80000000) { + pdir.d.size &= dir->d.size | 0x7fffffff; + pdir.d.tail[0] = dir->d.tail[0]; + pdir.d.tail[1] = dir->d.tail[1]; + return lfs_dir_commit(lfs, &pdir, NULL, 0); + } + } + + // shift out the entry + int err = lfs_dir_commit(lfs, dir, + (struct lfs_region[]){ + {entry->off, lfs_entry_size(entry), NULL, 0}, + }, + 1); + if (err) { + return err; + } + + // shift over any files/directories that are affected + for (lfs_file_t *f = lfs->files; f; f = f->next) { + if (lfs_paircmp(f->pair, dir->pair) == 0) { + if (f->poff == entry->off) { + f->pair[0] = 0xffffffff; + f->pair[1] = 0xffffffff; + } else if (f->poff > entry->off) { + f->poff -= lfs_entry_size(entry); + } + } + } + + for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { + if (lfs_paircmp(d->pair, dir->pair) == 0) { + if (d->off > entry->off) { + d->off -= lfs_entry_size(entry); + d->pos -= lfs_entry_size(entry); + } + } + } + + return 0; +} + +static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) +{ + while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size) - 4) { + if (!(0x80000000 & dir->d.size)) { + entry->off = dir->off; + return LFS_ERR_NOENT; + } + + int err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + + dir->off = sizeof(dir->d); + dir->pos += sizeof(dir->d) + 4; + } + + int err = lfs_bd_read(lfs, dir->pair[0], dir->off, &entry->d, sizeof(entry->d)); + lfs_entry_fromle32(&entry->d); + if (err) { + return err; + } + + entry->off = dir->off; + dir->off += lfs_entry_size(entry); + dir->pos += lfs_entry_size(entry); + return 0; +} + +static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const char **path) +{ + const char *pathname = *path; + size_t pathlen; + entry->d.type = LFS_TYPE_DIR; + entry->d.elen = sizeof(entry->d) - 4; + entry->d.alen = 0; + entry->d.nlen = 0; + entry->d.u.dir[0] = lfs->root[0]; + entry->d.u.dir[1] = lfs->root[1]; + + while (true) { + nextname: + // skip slashes + pathname += strspn(pathname, "/"); + pathlen = strcspn(pathname, "/"); + + // skip '.' and root '..' + if ((pathlen == 1 && memcmp(pathname, ".", 1) == 0) || (pathlen == 2 && memcmp(pathname, "..", 2) == 0)) { + pathname += pathlen; + goto nextname; + } + + // skip if matched by '..' in name + const char *suffix = pathname + pathlen; + size_t sufflen; + int depth = 1; + while (true) { + suffix += strspn(suffix, "/"); + sufflen = strcspn(suffix, "/"); + if (sufflen == 0) { + break; + } + + if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { + depth -= 1; + if (depth == 0) { + pathname = suffix + sufflen; + goto nextname; + } + } else { + depth += 1; + } + + suffix += sufflen; + } + + // found path + if (pathname[0] == '\0') { + return 0; + } + + // update what we've found + *path = pathname; + + // continue on if we hit a directory + if (entry->d.type != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + int err = lfs_dir_fetch(lfs, dir, entry->d.u.dir); + if (err) { + return err; + } + + // find entry matching name + while (true) { + err = lfs_dir_next(lfs, dir, entry); + if (err) { + return err; + } + + if (((0x7f & entry->d.type) != LFS_TYPE_REG && (0x7f & entry->d.type) != LFS_TYPE_DIR) || entry->d.nlen != pathlen) { + continue; + } + + int res = lfs_bd_cmp(lfs, dir->pair[0], entry->off + 4 + entry->d.elen + entry->d.alen, pathname, pathlen); + if (res < 0) { + return res; + } + + // found match + if (res) { + break; + } + } + + // check that entry has not been moved + if (entry->d.type & 0x80) { + int moved = lfs_moved(lfs, &entry->d.u); + if (moved) { + return (moved < 0) ? moved : LFS_ERR_NOENT; + } + + entry->d.type &= ~0x80; + } + + // to next name + pathname += pathlen; + } +} + +/// Top level directory operations /// +int lfs_mkdir(lfs_t *lfs, const char *path) +{ + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // fetch parent directory + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) { + return err ? err : LFS_ERR_EXIST; + } + + // build up new directory + lfs_alloc_ack(lfs); + + lfs_dir_t dir; + err = lfs_dir_alloc(lfs, &dir); + if (err) { + return err; + } + dir.d.tail[0] = cwd.d.tail[0]; + dir.d.tail[1] = cwd.d.tail[1]; + + err = lfs_dir_commit(lfs, &dir, NULL, 0); + if (err) { + return err; + } + + entry.d.type = LFS_TYPE_DIR; + entry.d.elen = sizeof(entry.d) - 4; + entry.d.alen = 0; + entry.d.nlen = strlen(path); + entry.d.u.dir[0] = dir.pair[0]; + entry.d.u.dir[1] = dir.pair[1]; + + cwd.d.tail[0] = dir.pair[0]; + cwd.d.tail[1] = dir.pair[1]; + + err = lfs_dir_append(lfs, &cwd, &entry, path); + if (err) { + return err; + } + + lfs_alloc_ack(lfs); + return 0; +} + +int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) +{ + dir->pair[0] = lfs->root[0]; + dir->pair[1] = lfs->root[1]; + + lfs_entry_t entry; + int err = lfs_dir_find(lfs, dir, &entry, &path); + if (err) { + return err; + } else if (entry.d.type != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + err = lfs_dir_fetch(lfs, dir, entry.d.u.dir); + if (err) { + return err; + } + + // setup head dir + // special offset for '.' and '..' + dir->head[0] = dir->pair[0]; + dir->head[1] = dir->pair[1]; + dir->pos = sizeof(dir->d) - 2; + dir->off = sizeof(dir->d); + + // add to list of directories + dir->next = lfs->dirs; + lfs->dirs = dir; + + return 0; +} + +int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) +{ + // remove from list of directories + for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) { + if (*p == dir) { + *p = dir->next; + break; + } + } + + return 0; +} + +int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) +{ + memset(info, 0, sizeof(*info)); + + // special offset for '.' and '..' + if (dir->pos == sizeof(dir->d) - 2) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, "."); + dir->pos += 1; + return 1; + } else if (dir->pos == sizeof(dir->d) - 1) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, ".."); + dir->pos += 1; + return 1; + } + + lfs_entry_t entry; + while (true) { + int err = lfs_dir_next(lfs, dir, &entry); + if (err) { + return (err == LFS_ERR_NOENT) ? 0 : err; + } + + if ((0x7f & entry.d.type) != LFS_TYPE_REG && (0x7f & entry.d.type) != LFS_TYPE_DIR) { + continue; + } + + // check that entry has not been moved + if (entry.d.type & 0x80) { + int moved = lfs_moved(lfs, &entry.d.u); + if (moved < 0) { + return moved; + } + + if (moved) { + continue; + } + + entry.d.type &= ~0x80; + } + + break; + } + + info->type = entry.d.type; + if (info->type == LFS_TYPE_REG) { + info->size = entry.d.u.file.size; + } + + int err = lfs_bd_read(lfs, dir->pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); + if (err) { + return err; + } + + return 1; +} + +int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) +{ + // simply walk from head dir + int err = lfs_dir_rewind(lfs, dir); + if (err) { + return err; + } + dir->pos = off; + + while (off > (0x7fffffff & dir->d.size)) { + off -= 0x7fffffff & dir->d.size; + if (!(0x80000000 & dir->d.size)) { + return LFS_ERR_INVAL; + } + + err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + } + + dir->off = off; + return 0; +} + +int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) +{ + // reload the head dir + int err = lfs_dir_fetch(lfs, dir, dir->head); + if (err) { + return err; + } + + dir->pair[0] = dir->head[0]; + dir->pair[1] = dir->head[1]; + dir->pos = sizeof(dir->d) - 2; + dir->off = sizeof(dir->d); + return 0; +} + +/// File index list operations /// +static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) +{ + lfs_off_t size = *off; + lfs_off_t b = lfs->cfg->block_size - 2 * 4; + lfs_off_t i = size / b; + if (i == 0) { + return 0; + } + + i = (size - 4 * (lfs_popc(i - 1) + 2)) / b; + *off = size - b * i - 4 * lfs_popc(i); + return i; +} + +static int lfs_ctz_find(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, + lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) +{ + if (size == 0) { + *block = 0xffffffff; + *off = 0; + return 0; + } + + lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); + lfs_off_t target = lfs_ctz_index(lfs, &pos); + + while (current > target) { + lfs_size_t skip = lfs_min(lfs_npw2(current - target + 1) - 1, lfs_ctz(current)); + + int err = lfs_cache_read(lfs, rcache, pcache, head, 4 * skip, &head, 4); + head = lfs_fromle32(head); + if (err) { + return err; + } + + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); + current -= 1 << skip; + } + + *block = head; + *off = pos; + return 0; +} + +static int lfs_ctz_extend(lfs_t *lfs, lfs_cache_t *rcache, lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, + lfs_block_t *block, lfs_off_t *off) +{ + while (true) { + // go ahead and grab a block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); + + if (true) { + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (size == 0) { + *block = nblock; + *off = 0; + return 0; + } + + size -= 1; + lfs_off_t index = lfs_ctz_index(lfs, &size); + size += 1; + + // just copy out the last block if it is incomplete + if (size != lfs->cfg->block_size) { + for (lfs_off_t i = 0; i < size; i++) { + uint8_t data; + err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1); + if (err) { + return err; + } + + err = lfs_cache_prog(lfs, pcache, rcache, nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + *block = nblock; + *off = size; + return 0; + } + + // append block + index += 1; + lfs_size_t skips = lfs_ctz(index) + 1; + + for (lfs_off_t i = 0; i < skips; i++) { + head = lfs_tole32(head); + err = lfs_cache_prog(lfs, pcache, rcache, nblock, 4 * i, &head, 4); + head = lfs_fromle32(head); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (i != skips - 1) { + err = lfs_cache_read(lfs, rcache, NULL, head, 4 * i, &head, 4); + head = lfs_fromle32(head); + if (err) { + return err; + } + } + + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); + } + + *block = nblock; + *off = 4 * skips; + return 0; + } + + relocate: + LFS_DEBUG("Bad block at %" PRIu32, nblock); + + // just clear cache and try a new block + lfs_cache_drop(lfs, &lfs->pcache); + } +} + +static int lfs_ctz_traverse(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, + int (*cb)(void *, lfs_block_t), void *data) +{ + if (size == 0) { + return 0; + } + + lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); + + while (true) { + int err = cb(data, head); + if (err) { + return err; + } + + if (index == 0) { + return 0; + } + + lfs_block_t heads[2]; + int count = 2 - (index & 1); + err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count * 4); + heads[0] = lfs_fromle32(heads[0]); + heads[1] = lfs_fromle32(heads[1]); + if (err) { + return err; + } + + for (int i = 0; i < count - 1; i++) { + err = cb(data, heads[i]); + if (err) { + return err; + } + } + + head = heads[count - 1]; + index -= count; + } +} + +/// Top level file operations /// +int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, const char *path, int flags, const struct lfs_file_config *cfg) +{ + // deorphan if we haven't yet, needed at most once after poweron + if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // allocate entry for file if it doesn't exist + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err && (err != LFS_ERR_NOENT || strchr(path, '/') != NULL)) { + return err; + } + + if (err == LFS_ERR_NOENT) { + if (!(flags & LFS_O_CREAT)) { + return LFS_ERR_NOENT; + } + + // create entry to remember name + entry.d.type = LFS_TYPE_REG; + entry.d.elen = sizeof(entry.d) - 4; + entry.d.alen = 0; + entry.d.nlen = strlen(path); + entry.d.u.file.head = 0xffffffff; + entry.d.u.file.size = 0; + err = lfs_dir_append(lfs, &cwd, &entry, path); + if (err) { + return err; + } + } else if (entry.d.type == LFS_TYPE_DIR) { + return LFS_ERR_ISDIR; + } else if (flags & LFS_O_EXCL) { + return LFS_ERR_EXIST; + } + + // setup file struct + file->cfg = cfg; + file->pair[0] = cwd.pair[0]; + file->pair[1] = cwd.pair[1]; + file->poff = entry.off; + file->head = entry.d.u.file.head; + file->size = entry.d.u.file.size; + file->flags = flags; + file->pos = 0; + + if (flags & LFS_O_TRUNC) { + if (file->size != 0) { + file->flags |= LFS_F_DIRTY; + } + file->head = 0xffffffff; + file->size = 0; + } + + // allocate buffer if needed + file->cache.block = 0xffffffff; + if (file->cfg && file->cfg->buffer) { + file->cache.buffer = file->cfg->buffer; + } else if (lfs->cfg->file_buffer) { + if (lfs->files) { + // already in use + return LFS_ERR_NOMEM; + } + file->cache.buffer = lfs->cfg->file_buffer; + } else if ((file->flags & 3) == LFS_O_RDONLY) { + file->cache.buffer = lfs_malloc(lfs->cfg->read_size); + if (!file->cache.buffer) { + return LFS_ERR_NOMEM; + } + } else { + file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); + if (!file->cache.buffer) { + return LFS_ERR_NOMEM; + } + } + + // zero to avoid information leak + lfs_cache_drop(lfs, &file->cache); + if ((file->flags & 3) != LFS_O_RDONLY) { + lfs_cache_zero(lfs, &file->cache); + } + + // add to list of files + file->next = lfs->files; + lfs->files = file; + + return 0; +} + +int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) +{ + return lfs_file_opencfg(lfs, file, path, flags, NULL); +} + +int lfs_file_close(lfs_t *lfs, lfs_file_t *file) +{ + int err = lfs_file_sync(lfs, file); + + // remove from list of files + for (lfs_file_t **p = &lfs->files; *p; p = &(*p)->next) { + if (*p == file) { + *p = file->next; + break; + } + } + + // clean up memory + if (!(file->cfg && file->cfg->buffer) && !lfs->cfg->file_buffer) { + lfs_free(file->cache.buffer); + } + + return err; +} + +static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) +{ +relocate: + LFS_DEBUG("Bad block at %" PRIu32, file->block); + + // just relocate what exists into new block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // either read from dirty cache or disk + for (lfs_off_t i = 0; i < file->off; i++) { + uint8_t data; + err = lfs_cache_read(lfs, &lfs->rcache, &file->cache, file->block, i, &data, 1); + if (err) { + return err; + } + + err = lfs_cache_prog(lfs, &lfs->pcache, &lfs->rcache, nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // copy over new state of file + memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size); + file->cache.block = lfs->pcache.block; + file->cache.off = lfs->pcache.off; + lfs_cache_zero(lfs, &lfs->pcache); + + file->block = nblock; + return 0; +} + +static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) +{ + if (file->flags & LFS_F_READING) { + // just drop read cache + lfs_cache_drop(lfs, &file->cache); + file->flags &= ~LFS_F_READING; + } + + if (file->flags & LFS_F_WRITING) { + lfs_off_t pos = file->pos; + + // copy over anything after current branch + lfs_file_t orig = { + .head = file->head, + .size = file->size, + .flags = LFS_O_RDONLY, + .pos = file->pos, + .cache = lfs->rcache, + }; + lfs_cache_drop(lfs, &lfs->rcache); + + while (file->pos < file->size) { + // copy over a byte at a time, leave it up to caching + // to make this efficient + uint8_t data; + lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1); + if (res < 0) { + return res; + } + + res = lfs_file_write(lfs, file, &data, 1); + if (res < 0) { + return res; + } + + // keep our reference to the rcache in sync + if (lfs->rcache.block != 0xffffffff) { + lfs_cache_drop(lfs, &orig.cache); + lfs_cache_drop(lfs, &lfs->rcache); + } + } + + // write out what we have + while (true) { + int err = lfs_cache_flush(lfs, &file->cache, &lfs->rcache); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + break; + relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + return err; + } + } + + // actual file updates + file->head = file->block; + file->size = file->pos; + file->flags &= ~LFS_F_WRITING; + file->flags |= LFS_F_DIRTY; + + file->pos = pos; + } + + return 0; +} + +int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) +{ + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + if ((file->flags & LFS_F_DIRTY) && !(file->flags & LFS_F_ERRED) && !lfs_pairisnull(file->pair)) { + // update dir entry + lfs_dir_t cwd; + err = lfs_dir_fetch(lfs, &cwd, file->pair); + if (err) { + return err; + } + + lfs_entry_t entry = {.off = file->poff}; + err = lfs_bd_read(lfs, cwd.pair[0], entry.off, &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); + if (err) { + return err; + } + + LFS_ASSERT(entry.d.type == LFS_TYPE_REG); + entry.d.u.file.head = file->head; + entry.d.u.file.size = file->size; + + err = lfs_dir_update(lfs, &cwd, &entry, NULL); + if (err) { + return err; + } + + file->flags &= ~LFS_F_DIRTY; + } + + return 0; +} + +lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size) +{ + uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & 3) == LFS_O_WRONLY) { + return LFS_ERR_BADF; + } + + if (file->flags & LFS_F_WRITING) { + // flush out any writes + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + } + + if (file->pos >= file->size) { + // eof if past end + return 0; + } + + size = lfs_min(size, file->size - file->pos); + nsize = size; + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_READING) || file->off == lfs->cfg->block_size) { + int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos, &file->block, &file->off); + if (err) { + return err; + } + + file->flags |= LFS_F_READING; + } + + // read as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + int err = lfs_cache_read(lfs, &file->cache, NULL, file->block, file->off, data, diff); + if (err) { + return err; + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + } + + return size; +} + +lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size) +{ + const uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + if (file->flags & LFS_F_READING) { + // drop any reads + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + } + + if ((file->flags & LFS_O_APPEND) && file->pos < file->size) { + file->pos = file->size; + } + + if (!(file->flags & LFS_F_WRITING) && file->pos > file->size) { + // fill with zeros + lfs_off_t pos = file->pos; + file->pos = file->size; + + while (file->pos < pos) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + } + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_WRITING) || file->off == lfs->cfg->block_size) { + if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { + // find out which block we're extending from + int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos - 1, &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + // mark cache as dirty since we may have read data into it + lfs_cache_zero(lfs, &file->cache); + } + + // extend file with new blocks + lfs_alloc_ack(lfs); + int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache, file->block, file->pos, &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + file->flags |= LFS_F_WRITING; + } + + // program as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + while (true) { + int err = lfs_cache_prog(lfs, &file->cache, &lfs->rcache, file->block, file->off, data, diff); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + file->flags |= LFS_F_ERRED; + return err; + } + + break; + relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + + lfs_alloc_ack(lfs); + } + + file->flags &= ~LFS_F_ERRED; + return size; +} + +lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence) +{ + // write out everything beforehand, may be noop if rdonly + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // update pos + if (whence == LFS_SEEK_SET) { + file->pos = off; + } else if (whence == LFS_SEEK_CUR) { + if (off < 0 && (lfs_off_t)-off > file->pos) { + return LFS_ERR_INVAL; + } + + file->pos = file->pos + off; + } else if (whence == LFS_SEEK_END) { + if (off < 0 && (lfs_off_t)-off > file->size) { + return LFS_ERR_INVAL; + } + + file->pos = file->size + off; + } + + return file->pos; +} + +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) +{ + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + lfs_off_t oldsize = lfs_file_size(lfs, file); + if (size < oldsize) { + // need to flush since directly changing metadata + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // lookup new head in ctz skip list + err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, size, &file->head, &(lfs_off_t){0}); + if (err) { + return err; + } + + file->size = size; + file->flags |= LFS_F_DIRTY; + } else if (size > oldsize) { + lfs_off_t pos = file->pos; + + // flush+seek if not already at end + if (file->pos != oldsize) { + int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); + if (err < 0) { + return err; + } + } + + // fill with zeros + while (file->pos < size) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + + // restore pos + int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); + if (err < 0) { + return err; + } + } + + return 0; +} + +lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t const *file) +{ + (void)lfs; + return file->pos; +} + +int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) +{ + lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET); + if (res < 0) { + return res; + } + + return 0; +} + +lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) +{ + (void)lfs; + if (file->flags & LFS_F_WRITING) { + return lfs_max(file->pos, file->size); + } else { + return file->size; + } +} + +/// General fs operations /// +int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) +{ + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err) { + return err; + } + + memset(info, 0, sizeof(*info)); + info->type = entry.d.type; + if (info->type == LFS_TYPE_REG) { + info->size = entry.d.u.file.size; + } + + if (lfs_paircmp(entry.d.u.dir, lfs->root) == 0) { + strcpy(info->name, "/"); + } else { + err = lfs_bd_read(lfs, cwd.pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); + if (err) { + return err; + } + } + + return 0; +} + +int lfs_remove(lfs_t *lfs, const char *path) +{ + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err) { + return err; + } + + lfs_dir_t dir; + if (entry.d.type == LFS_TYPE_DIR) { + // must be empty before removal, checking size + // without masking top bit checks for any case where + // dir is not empty + err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); + if (err) { + return err; + } /* else if (dir.d.size != sizeof(dir.d)+4) { + return LFS_ERR_NOTEMPTY; + } allow to remove non-empty folder, + According to below issue, comment these 2 line won't corrupt filesystem + https://github.com/ARMmbed/littlefs/issues/43 */ + } + + // remove the entry + err = lfs_dir_remove(lfs, &cwd, &entry); + if (err) { + return err; + } + + // if we were a directory, find pred, replace tail + if (entry.d.type == LFS_TYPE_DIR) { + int res = lfs_pred(lfs, dir.pair, &cwd); + if (res < 0) { + return res; + } + + LFS_ASSERT(res); // must have pred + cwd.d.tail[0] = dir.d.tail[0]; + cwd.d.tail[1] = dir.d.tail[1]; + + err = lfs_dir_commit(lfs, &cwd, NULL, 0); + if (err) { + return err; + } + } + + return 0; +} + +int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) +{ + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // find old entry + lfs_dir_t oldcwd; + lfs_entry_t oldentry; + int err = lfs_dir_find(lfs, &oldcwd, &oldentry, &oldpath); + if (err) { + return err; + } + + // allocate new entry + lfs_dir_t newcwd; + lfs_entry_t preventry; + err = lfs_dir_find(lfs, &newcwd, &preventry, &newpath); + if (err && (err != LFS_ERR_NOENT || strchr(newpath, '/') != NULL)) { + return err; + } + + bool prevexists = (err != LFS_ERR_NOENT); + bool samepair = (lfs_paircmp(oldcwd.pair, newcwd.pair) == 0); + + // must have same type + if (prevexists && preventry.d.type != oldentry.d.type) { + return LFS_ERR_ISDIR; + } + + lfs_dir_t dir; + if (prevexists && preventry.d.type == LFS_TYPE_DIR) { + // must be empty before removal, checking size + // without masking top bit checks for any case where + // dir is not empty + err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); + if (err) { + return err; + } else if (dir.d.size != sizeof(dir.d) + 4) { + return LFS_ERR_NOTEMPTY; + } + } + + // mark as moving + oldentry.d.type |= 0x80; + err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL); + if (err) { + return err; + } + + // update pair if newcwd == oldcwd + if (samepair) { + newcwd = oldcwd; + } + + // move to new location + lfs_entry_t newentry = preventry; + newentry.d = oldentry.d; + newentry.d.type &= ~0x80; + newentry.d.nlen = strlen(newpath); + + if (prevexists) { + err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); + if (err) { + return err; + } + } else { + err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); + if (err) { + return err; + } + } + + // update pair if newcwd == oldcwd + if (samepair) { + oldcwd = newcwd; + } + + // remove old entry + err = lfs_dir_remove(lfs, &oldcwd, &oldentry); + if (err) { + return err; + } + + // if we were a directory, find pred, replace tail + if (prevexists && preventry.d.type == LFS_TYPE_DIR) { + int res = lfs_pred(lfs, dir.pair, &newcwd); + if (res < 0) { + return res; + } + + LFS_ASSERT(res); // must have pred + newcwd.d.tail[0] = dir.d.tail[0]; + newcwd.d.tail[1] = dir.d.tail[1]; + + err = lfs_dir_commit(lfs, &newcwd, NULL, 0); + if (err) { + return err; + } + } + + return 0; +} + +/// Filesystem operations /// +static void lfs_deinit(lfs_t *lfs) +{ + // free allocated memory + if (!lfs->cfg->read_buffer) { + lfs_free(lfs->rcache.buffer); + } + + if (!lfs->cfg->prog_buffer) { + lfs_free(lfs->pcache.buffer); + } + + if (!lfs->cfg->lookahead_buffer) { + lfs_free(lfs->free.buffer); + } +} + +static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) +{ + lfs->cfg = cfg; + + // setup read cache + if (lfs->cfg->read_buffer) { + lfs->rcache.buffer = lfs->cfg->read_buffer; + } else { + lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); + if (!lfs->rcache.buffer) { + goto cleanup; + } + } + + // setup program cache + if (lfs->cfg->prog_buffer) { + lfs->pcache.buffer = lfs->cfg->prog_buffer; + } else { + lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); + if (!lfs->pcache.buffer) { + goto cleanup; + } + } + + // zero to avoid information leaks + lfs_cache_zero(lfs, &lfs->pcache); + lfs_cache_drop(lfs, &lfs->rcache); + + // setup lookahead, round down to nearest 32-bits + LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); + LFS_ASSERT(lfs->cfg->lookahead > 0); + if (lfs->cfg->lookahead_buffer) { + lfs->free.buffer = lfs->cfg->lookahead_buffer; + } else { + lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead / 8); + if (!lfs->free.buffer) { + goto cleanup; + } + } + + // check that program and read sizes are multiples of the block size + LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); + LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->prog_size == 0); + + // check that the block size is large enough to fit ctz pointers + LFS_ASSERT(4 * lfs_npw2(0xffffffff / (lfs->cfg->block_size - 2 * 4)) <= lfs->cfg->block_size); + + // setup default state + lfs->root[0] = 0xffffffff; + lfs->root[1] = 0xffffffff; + lfs->files = NULL; + lfs->dirs = NULL; + lfs->deorphaned = false; + + return 0; + +cleanup: + lfs_deinit(lfs); + return LFS_ERR_NOMEM; +} + +int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) +{ + int err = 0; + if (true) { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + // create free lookahead + memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); + lfs->free.off = 0; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); + lfs->free.i = 0; + lfs_alloc_ack(lfs); + + // create superblock dir + lfs_dir_t superdir; + err = lfs_dir_alloc(lfs, &superdir); + if (err) { + goto cleanup; + } + + // write root directory + lfs_dir_t root; + err = lfs_dir_alloc(lfs, &root); + if (err) { + goto cleanup; + } + + err = lfs_dir_commit(lfs, &root, NULL, 0); + if (err) { + goto cleanup; + } + + lfs->root[0] = root.pair[0]; + lfs->root[1] = root.pair[1]; + + // write superblocks + lfs_superblock_t superblock = { + .off = sizeof(superdir.d), + .d.type = LFS_TYPE_SUPERBLOCK, + .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, + .d.nlen = sizeof(superblock.d.magic), + .d.version = LFS_DISK_VERSION, + .d.magic = {"littlefs"}, + .d.block_size = lfs->cfg->block_size, + .d.block_count = lfs->cfg->block_count, + .d.root = {lfs->root[0], lfs->root[1]}, + }; + superdir.d.tail[0] = root.pair[0]; + superdir.d.tail[1] = root.pair[1]; + superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; + + // write both pairs to be safe + lfs_superblock_tole32(&superblock.d); + bool valid = false; + for (int i = 0; i < 2; i++) { + err = lfs_dir_commit( + lfs, &superdir, + (struct lfs_region[]){{sizeof(superdir.d), sizeof(superblock.d), &superblock.d, sizeof(superblock.d)}}, 1); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + valid = valid || !err; + } + + if (!valid) { + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + // sanity check that fetch works + err = lfs_dir_fetch(lfs, &superdir, (const lfs_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + + lfs_alloc_ack(lfs); + } + +cleanup: + lfs_deinit(lfs); + return err; +} + +int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) +{ + int err = 0; + if (true) { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + // setup free lookahead + lfs->free.off = 0; + lfs->free.size = 0; + lfs->free.i = 0; + lfs_alloc_ack(lfs); + + // load superblock + lfs_dir_t dir; + lfs_superblock_t superblock; + err = lfs_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + if (!err) { + err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), &superblock.d, sizeof(superblock.d)); + lfs_superblock_fromle32(&superblock.d); + if (err) { + goto cleanup; + } + + lfs->root[0] = superblock.d.root[0]; + lfs->root[1] = superblock.d.root[1]; + } + + if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { + LFS_ERROR("Invalid superblock at %d %d", 0, 1); + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + uint16_t major_version = (0xffff & (superblock.d.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); + if ((major_version != LFS_DISK_VERSION_MAJOR || minor_version > LFS_DISK_VERSION_MINOR)) { + LFS_ERROR("Invalid version %d.%d", major_version, minor_version); + err = LFS_ERR_INVAL; + goto cleanup; + } + + return 0; + } cleanup: - lfs_deinit(lfs); - return err; + lfs_deinit(lfs); + return err; } -int lfs_unmount(lfs_t *lfs) { - lfs_deinit(lfs); - return 0; +int lfs_unmount(lfs_t *lfs) +{ + lfs_deinit(lfs); + return 0; } /// Littlefs specific operations /// -int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) { - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - // iterate over metadata pairs - lfs_dir_t dir; - lfs_entry_t entry; - lfs_block_t cwd[2] = {0, 1}; - - while (true) { - for (int i = 0; i < 2; i++) { - int err = cb(data, cwd[i]); - if (err) { - return err; - } - } - - int err = lfs_dir_fetch(lfs, &dir, cwd); - if (err) { - return err; - } - - // iterate over contents - while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size) - 4) { - err = lfs_bd_read(lfs, dir.pair[0], dir.off, &entry.d, sizeof(entry.d)); - lfs_entry_fromle32(&entry.d); - if (err) { - return err; - } - - dir.off += lfs_entry_size(&entry); - if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { - err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, entry.d.u.file.head, entry.d.u.file.size, cb, data); - if (err) { - return err; - } - } - } - - cwd[0] = dir.d.tail[0]; - cwd[1] = dir.d.tail[1]; - - if (lfs_pairisnull(cwd)) { - break; - } - } - - // iterate over any open files - for (lfs_file_t *f = lfs->files; f; f = f->next) { - if (f->flags & LFS_F_DIRTY) { - int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->head, f->size, cb, data); - if (err) { - return err; - } - } - - if (f->flags & LFS_F_WRITING) { - int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->block, f->pos, cb, data); - if (err) { - return err; - } - } - } - - return 0; -} - -static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) { - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - // iterate over all directory directory entries - int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1}); - if (err) { - return err; - } - - while (!lfs_pairisnull(pdir->d.tail)) { - if (lfs_paircmp(pdir->d.tail, dir) == 0) { - return true; - } - - err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); - if (err) { - return err; - } - } - - return false; -} - -static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *parent, lfs_entry_t *entry) { - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - parent->d.tail[0] = 0; - parent->d.tail[1] = 1; - - // iterate over all directory directory entries - while (!lfs_pairisnull(parent->d.tail)) { - int err = lfs_dir_fetch(lfs, parent, parent->d.tail); - if (err) { - return err; - } - - while (true) { - err = lfs_dir_next(lfs, parent, entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) && lfs_paircmp(entry->d.u.dir, dir) == 0) { - return true; - } - } - } - - return false; -} - -static int lfs_moved(lfs_t *lfs, const void *e) { - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - // skip superblock - lfs_dir_t cwd; - int err = lfs_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); - if (err) { - return err; - } - - // iterate over all directory directory entries - lfs_entry_t entry; - while (!lfs_pairisnull(cwd.d.tail)) { - err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); - if (err) { - return err; - } - - while (true) { - err = lfs_dir_next(lfs, &cwd, &entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - if (!(0x80 & entry.d.type) && memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { - return true; - } - } - } - - return false; -} - -static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) { - // find parent - lfs_dir_t parent; - lfs_entry_t entry; - int res = lfs_parent(lfs, oldpair, &parent, &entry); - if (res < 0) { - return res; - } - - if (res) { - // update disk, this creates a desync - entry.d.u.dir[0] = newpair[0]; - entry.d.u.dir[1] = newpair[1]; - - int err = lfs_dir_update(lfs, &parent, &entry, NULL); - if (err) { - return err; - } - - // update internal root - if (lfs_paircmp(oldpair, lfs->root) == 0) { - LFS_DEBUG("Relocating root %" PRIu32 " %" PRIu32, newpair[0], newpair[1]); - lfs->root[0] = newpair[0]; - lfs->root[1] = newpair[1]; - } - - // clean up bad block, which should now be a desync - return lfs_deorphan(lfs); - } - - // find pred - res = lfs_pred(lfs, oldpair, &parent); - if (res < 0) { - return res; - } - - if (res) { - // just replace bad pair, no desync can occur - parent.d.tail[0] = newpair[0]; - parent.d.tail[1] = newpair[1]; - - return lfs_dir_commit(lfs, &parent, NULL, 0); - } - - // couldn't find dir, must be new - return 0; -} - -int lfs_deorphan(lfs_t *lfs) { - lfs->deorphaned = true; - - if (lfs_pairisnull(lfs->root)) { - return 0; - } - - lfs_dir_t pdir = {.d.size = 0x80000000}; - lfs_dir_t cwd = {.d.tail[0] = 0, .d.tail[1] = 1}; - - // iterate over all directory directory entries - for (lfs_size_t i = 0; i < lfs->cfg->block_count; i++) { - if (lfs_pairisnull(cwd.d.tail)) { - return 0; - } - - int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); - if (err) { - return err; - } - - // check head blocks for orphans - if (!(0x80000000 & pdir.d.size)) { - // check if we have a parent - lfs_dir_t parent; - lfs_entry_t entry; - int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry); - if (res < 0) { - return res; - } - - if (!res) { - // we are an orphan - LFS_DEBUG("Found orphan %" PRIu32 " %" PRIu32, pdir.d.tail[0], pdir.d.tail[1]); - - pdir.d.tail[0] = cwd.d.tail[0]; - pdir.d.tail[1] = cwd.d.tail[1]; - - err = lfs_dir_commit(lfs, &pdir, NULL, 0); - if (err) { - return err; - } - +int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) +{ + if (lfs_pairisnull(lfs->root)) { return 0; - } - - if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) { - // we have desynced - LFS_DEBUG("Found desync %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - - pdir.d.tail[0] = entry.d.u.dir[0]; - pdir.d.tail[1] = entry.d.u.dir[1]; - - err = lfs_dir_commit(lfs, &pdir, NULL, 0); - if (err) { - return err; - } - - return 0; - } } - // check entries for moves + // iterate over metadata pairs + lfs_dir_t dir; lfs_entry_t entry; + lfs_block_t cwd[2] = {0, 1}; + while (true) { - err = lfs_dir_next(lfs, &cwd, &entry); - if (err && err != LFS_ERR_NOENT) { - return err; - } - - if (err == LFS_ERR_NOENT) { - break; - } - - // found moved entry - if (entry.d.type & 0x80) { - int moved = lfs_moved(lfs, &entry.d.u); - if (moved < 0) { - return moved; + for (int i = 0; i < 2; i++) { + int err = cb(data, cwd[i]); + if (err) { + return err; + } } - if (moved) { - LFS_DEBUG("Found move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - err = lfs_dir_remove(lfs, &cwd, &entry); - if (err) { + int err = lfs_dir_fetch(lfs, &dir, cwd); + if (err) { return err; - } - } else { - LFS_DEBUG("Found partial move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); - entry.d.type &= ~0x80; - err = lfs_dir_update(lfs, &cwd, &entry, NULL); - if (err) { - return err; - } } - } + + // iterate over contents + while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size) - 4) { + err = lfs_bd_read(lfs, dir.pair[0], dir.off, &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); + if (err) { + return err; + } + + dir.off += lfs_entry_size(&entry); + if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { + err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, entry.d.u.file.head, entry.d.u.file.size, cb, data); + if (err) { + return err; + } + } + } + + cwd[0] = dir.d.tail[0]; + cwd[1] = dir.d.tail[1]; + + if (lfs_pairisnull(cwd)) { + break; + } } - memcpy(&pdir, &cwd, sizeof(pdir)); - } + // iterate over any open files + for (lfs_file_t *f = lfs->files; f; f = f->next) { + if (f->flags & LFS_F_DIRTY) { + int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->head, f->size, cb, data); + if (err) { + return err; + } + } - // If we reached here, we have more directory pairs than blocks in the - // filesystem... So something must be horribly wrong - return LFS_ERR_CORRUPT; + if (f->flags & LFS_F_WRITING) { + int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->block, f->pos, cb, data); + if (err) { + return err; + } + } + } + + return 0; +} + +static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) +{ + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // iterate over all directory directory entries + int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } + + while (!lfs_pairisnull(pdir->d.tail)) { + if (lfs_paircmp(pdir->d.tail, dir) == 0) { + return true; + } + + err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); + if (err) { + return err; + } + } + + return false; +} + +static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *parent, lfs_entry_t *entry) +{ + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + parent->d.tail[0] = 0; + parent->d.tail[1] = 1; + + // iterate over all directory directory entries + while (!lfs_pairisnull(parent->d.tail)) { + int err = lfs_dir_fetch(lfs, parent, parent->d.tail); + if (err) { + return err; + } + + while (true) { + err = lfs_dir_next(lfs, parent, entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) && lfs_paircmp(entry->d.u.dir, dir) == 0) { + return true; + } + } + } + + return false; +} + +static int lfs_moved(lfs_t *lfs, const void *e) +{ + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // skip superblock + lfs_dir_t cwd; + int err = lfs_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } + + // iterate over all directory directory entries + lfs_entry_t entry; + while (!lfs_pairisnull(cwd.d.tail)) { + err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; + } + + while (true) { + err = lfs_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + if (!(0x80 & entry.d.type) && memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { + return true; + } + } + } + + return false; +} + +static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) +{ + // find parent + lfs_dir_t parent; + lfs_entry_t entry; + int res = lfs_parent(lfs, oldpair, &parent, &entry); + if (res < 0) { + return res; + } + + if (res) { + // update disk, this creates a desync + entry.d.u.dir[0] = newpair[0]; + entry.d.u.dir[1] = newpair[1]; + + int err = lfs_dir_update(lfs, &parent, &entry, NULL); + if (err) { + return err; + } + + // update internal root + if (lfs_paircmp(oldpair, lfs->root) == 0) { + LFS_DEBUG("Relocating root %" PRIu32 " %" PRIu32, newpair[0], newpair[1]); + lfs->root[0] = newpair[0]; + lfs->root[1] = newpair[1]; + } + + // clean up bad block, which should now be a desync + return lfs_deorphan(lfs); + } + + // find pred + res = lfs_pred(lfs, oldpair, &parent); + if (res < 0) { + return res; + } + + if (res) { + // just replace bad pair, no desync can occur + parent.d.tail[0] = newpair[0]; + parent.d.tail[1] = newpair[1]; + + return lfs_dir_commit(lfs, &parent, NULL, 0); + } + + // couldn't find dir, must be new + return 0; +} + +int lfs_deorphan(lfs_t *lfs) +{ + lfs->deorphaned = true; + + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + lfs_dir_t pdir = {.d.size = 0x80000000}; + lfs_dir_t cwd = {.d.tail[0] = 0, .d.tail[1] = 1}; + + // iterate over all directory directory entries + for (lfs_size_t i = 0; i < lfs->cfg->block_count; i++) { + if (lfs_pairisnull(cwd.d.tail)) { + return 0; + } + + int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; + } + + // check head blocks for orphans + if (!(0x80000000 & pdir.d.size)) { + // check if we have a parent + lfs_dir_t parent; + lfs_entry_t entry; + int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry); + if (res < 0) { + return res; + } + + if (!res) { + // we are an orphan + LFS_DEBUG("Found orphan %" PRIu32 " %" PRIu32, pdir.d.tail[0], pdir.d.tail[1]); + + pdir.d.tail[0] = cwd.d.tail[0]; + pdir.d.tail[1] = cwd.d.tail[1]; + + err = lfs_dir_commit(lfs, &pdir, NULL, 0); + if (err) { + return err; + } + + return 0; + } + + if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) { + // we have desynced + LFS_DEBUG("Found desync %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); + + pdir.d.tail[0] = entry.d.u.dir[0]; + pdir.d.tail[1] = entry.d.u.dir[1]; + + err = lfs_dir_commit(lfs, &pdir, NULL, 0); + if (err) { + return err; + } + + return 0; + } + } + + // check entries for moves + lfs_entry_t entry; + while (true) { + err = lfs_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + // found moved entry + if (entry.d.type & 0x80) { + int moved = lfs_moved(lfs, &entry.d.u); + if (moved < 0) { + return moved; + } + + if (moved) { + LFS_DEBUG("Found move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); + err = lfs_dir_remove(lfs, &cwd, &entry); + if (err) { + return err; + } + } else { + LFS_DEBUG("Found partial move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); + entry.d.type &= ~0x80; + err = lfs_dir_update(lfs, &cwd, &entry, NULL); + if (err) { + return err; + } + } + } + } + + memcpy(&pdir, &cwd, sizeof(pdir)); + } + + // If we reached here, we have more directory pairs than blocks in the + // filesystem... So something must be horribly wrong + return LFS_ERR_CORRUPT; } diff --git a/src/platform/stm32wl/littlefs/lfs.h b/src/platform/stm32wl/littlefs/lfs.h index 5e6f619e7..c6ed1d622 100644 --- a/src/platform/stm32wl/littlefs/lfs.h +++ b/src/platform/stm32wl/littlefs/lfs.h @@ -49,230 +49,230 @@ typedef uint32_t lfs_block_t; // Possible error codes, these are negative to allow // valid positive return values enum lfs_error { - LFS_ERR_OK = 0, // No error - LFS_ERR_IO = -5, // Error during device operation - LFS_ERR_CORRUPT = -52, // Corrupted - LFS_ERR_NOENT = -2, // No directory entry - LFS_ERR_EXIST = -17, // Entry already exists - LFS_ERR_NOTDIR = -20, // Entry is not a dir - LFS_ERR_ISDIR = -21, // Entry is a dir - LFS_ERR_NOTEMPTY = -39, // Dir is not empty - LFS_ERR_BADF = -9, // Bad file number - LFS_ERR_INVAL = -22, // Invalid parameter - LFS_ERR_NOSPC = -28, // No space left on device - LFS_ERR_NOMEM = -12, // No more memory available + LFS_ERR_OK = 0, // No error + LFS_ERR_IO = -5, // Error during device operation + LFS_ERR_CORRUPT = -52, // Corrupted + LFS_ERR_NOENT = -2, // No directory entry + LFS_ERR_EXIST = -17, // Entry already exists + LFS_ERR_NOTDIR = -20, // Entry is not a dir + LFS_ERR_ISDIR = -21, // Entry is a dir + LFS_ERR_NOTEMPTY = -39, // Dir is not empty + LFS_ERR_BADF = -9, // Bad file number + LFS_ERR_INVAL = -22, // Invalid parameter + LFS_ERR_NOSPC = -28, // No space left on device + LFS_ERR_NOMEM = -12, // No more memory available }; // File types enum lfs_type { - LFS_TYPE_REG = 0x11, - LFS_TYPE_DIR = 0x22, - LFS_TYPE_SUPERBLOCK = 0x2e, + LFS_TYPE_REG = 0x11, + LFS_TYPE_DIR = 0x22, + LFS_TYPE_SUPERBLOCK = 0x2e, }; // File open flags enum lfs_open_flags { - // open flags - LFS_O_RDONLY = 1, // Open a file as read only - LFS_O_WRONLY = 2, // Open a file as write only - LFS_O_RDWR = 3, // Open a file as read and write - LFS_O_CREAT = 0x0100, // Create a file if it does not exist - LFS_O_EXCL = 0x0200, // Fail if a file already exists - LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size - LFS_O_APPEND = 0x0800, // Move to end of file on every write + // open flags + LFS_O_RDONLY = 1, // Open a file as read only + LFS_O_WRONLY = 2, // Open a file as write only + LFS_O_RDWR = 3, // Open a file as read and write + LFS_O_CREAT = 0x0100, // Create a file if it does not exist + LFS_O_EXCL = 0x0200, // Fail if a file already exists + LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size + LFS_O_APPEND = 0x0800, // Move to end of file on every write - // internally used flags - LFS_F_DIRTY = 0x10000, // File does not match storage - LFS_F_WRITING = 0x20000, // File has been written since last flush - LFS_F_READING = 0x40000, // File has been read since last flush - LFS_F_ERRED = 0x80000, // An error occured during write + // internally used flags + LFS_F_DIRTY = 0x10000, // File does not match storage + LFS_F_WRITING = 0x20000, // File has been written since last flush + LFS_F_READING = 0x40000, // File has been read since last flush + LFS_F_ERRED = 0x80000, // An error occured during write }; // File seek flags enum lfs_whence_flags { - LFS_SEEK_SET = 0, // Seek relative to an absolute position - LFS_SEEK_CUR = 1, // Seek relative to the current file position - LFS_SEEK_END = 2, // Seek relative to the end of the file + LFS_SEEK_SET = 0, // Seek relative to an absolute position + LFS_SEEK_CUR = 1, // Seek relative to the current file position + LFS_SEEK_END = 2, // Seek relative to the end of the file }; // Configuration provided during initialization of the littlefs struct lfs_config { - // Opaque user provided context that can be used to pass - // information to the block device operations - void *context; + // Opaque user provided context that can be used to pass + // information to the block device operations + void *context; - // Read a region in a block. Negative error codes are propogated - // to the user. - int (*read)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size); + // Read a region in a block. Negative error codes are propogated + // to the user. + int (*read)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size); - // Program a region in a block. The block must have previously - // been erased. Negative error codes are propogated to the user. - // May return LFS_ERR_CORRUPT if the block should be considered bad. - int (*prog)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size); + // Program a region in a block. The block must have previously + // been erased. Negative error codes are propogated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*prog)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size); - // Erase a block. A block must be erased before being programmed. - // The state of an erased block is undefined. Negative error codes - // are propogated to the user. - // May return LFS_ERR_CORRUPT if the block should be considered bad. - int (*erase)(const struct lfs_config *c, lfs_block_t block); + // Erase a block. A block must be erased before being programmed. + // The state of an erased block is undefined. Negative error codes + // are propogated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*erase)(const struct lfs_config *c, lfs_block_t block); - // Sync the state of the underlying block device. Negative error codes - // are propogated to the user. - int (*sync)(const struct lfs_config *c); + // Sync the state of the underlying block device. Negative error codes + // are propogated to the user. + int (*sync)(const struct lfs_config *c); - // Minimum size of a block read. This determines the size of read buffers. - // This may be larger than the physical read size to improve performance - // by caching more of the block device. - lfs_size_t read_size; + // Minimum size of a block read. This determines the size of read buffers. + // This may be larger than the physical read size to improve performance + // by caching more of the block device. + lfs_size_t read_size; - // Minimum size of a block program. This determines the size of program - // buffers. This may be larger than the physical program size to improve - // performance by caching more of the block device. - // Must be a multiple of the read size. - lfs_size_t prog_size; + // Minimum size of a block program. This determines the size of program + // buffers. This may be larger than the physical program size to improve + // performance by caching more of the block device. + // Must be a multiple of the read size. + lfs_size_t prog_size; - // Size of an erasable block. This does not impact ram consumption and - // may be larger than the physical erase size. However, this should be - // kept small as each file currently takes up an entire block. - // Must be a multiple of the program size. - lfs_size_t block_size; + // Size of an erasable block. This does not impact ram consumption and + // may be larger than the physical erase size. However, this should be + // kept small as each file currently takes up an entire block. + // Must be a multiple of the program size. + lfs_size_t block_size; - // Number of erasable blocks on the device. - lfs_size_t block_count; + // Number of erasable blocks on the device. + lfs_size_t block_count; - // Number of blocks to lookahead during block allocation. A larger - // lookahead reduces the number of passes required to allocate a block. - // The lookahead buffer requires only 1 bit per block so it can be quite - // large with little ram impact. Should be a multiple of 32. - lfs_size_t lookahead; + // Number of blocks to lookahead during block allocation. A larger + // lookahead reduces the number of passes required to allocate a block. + // The lookahead buffer requires only 1 bit per block so it can be quite + // large with little ram impact. Should be a multiple of 32. + lfs_size_t lookahead; - // Optional, statically allocated read buffer. Must be read sized. - void *read_buffer; + // Optional, statically allocated read buffer. Must be read sized. + void *read_buffer; - // Optional, statically allocated program buffer. Must be program sized. - void *prog_buffer; + // Optional, statically allocated program buffer. Must be program sized. + void *prog_buffer; - // Optional, statically allocated lookahead buffer. Must be 1 bit per - // lookahead block. - void *lookahead_buffer; + // Optional, statically allocated lookahead buffer. Must be 1 bit per + // lookahead block. + void *lookahead_buffer; - // Optional, statically allocated buffer for files. Must be program sized. - // If enabled, only one file may be opened at a time. - void *file_buffer; + // Optional, statically allocated buffer for files. Must be program sized. + // If enabled, only one file may be opened at a time. + void *file_buffer; }; // Optional configuration provided during lfs_file_opencfg struct lfs_file_config { - // Optional, statically allocated buffer for files. Must be program sized. - // If NULL, malloc will be used by default. - void *buffer; + // Optional, statically allocated buffer for files. Must be program sized. + // If NULL, malloc will be used by default. + void *buffer; }; // File info structure struct lfs_info { - // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR - uint8_t type; + // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR + uint8_t type; - // Size of the file, only valid for REG files - lfs_size_t size; + // Size of the file, only valid for REG files + lfs_size_t size; - // Name of the file stored as a null-terminated string - char name[LFS_NAME_MAX + 1]; + // Name of the file stored as a null-terminated string + char name[LFS_NAME_MAX + 1]; }; /// littlefs data structures /// typedef struct lfs_entry { - lfs_off_t off; + lfs_off_t off; - struct lfs_disk_entry { - uint8_t type; - uint8_t elen; - uint8_t alen; - uint8_t nlen; - union { - struct { - lfs_block_t head; - lfs_size_t size; - } file; - lfs_block_t dir[2]; - } u; - } d; + struct lfs_disk_entry { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + union { + struct { + lfs_block_t head; + lfs_size_t size; + } file; + lfs_block_t dir[2]; + } u; + } d; } lfs_entry_t; typedef struct lfs_cache { - lfs_block_t block; - lfs_off_t off; - uint8_t *buffer; + lfs_block_t block; + lfs_off_t off; + uint8_t *buffer; } lfs_cache_t; typedef struct lfs_file { - struct lfs_file *next; - lfs_block_t pair[2]; - lfs_off_t poff; + struct lfs_file *next; + lfs_block_t pair[2]; + lfs_off_t poff; - lfs_block_t head; - lfs_size_t size; + lfs_block_t head; + lfs_size_t size; - const struct lfs_file_config *cfg; - uint32_t flags; - lfs_off_t pos; - lfs_block_t block; - lfs_off_t off; - lfs_cache_t cache; + const struct lfs_file_config *cfg; + uint32_t flags; + lfs_off_t pos; + lfs_block_t block; + lfs_off_t off; + lfs_cache_t cache; } lfs_file_t; typedef struct lfs_dir { - struct lfs_dir *next; - lfs_block_t pair[2]; - lfs_off_t off; + struct lfs_dir *next; + lfs_block_t pair[2]; + lfs_off_t off; - lfs_block_t head[2]; - lfs_off_t pos; + lfs_block_t head[2]; + lfs_off_t pos; - struct lfs_disk_dir { - uint32_t rev; - lfs_size_t size; - lfs_block_t tail[2]; - } d; + struct lfs_disk_dir { + uint32_t rev; + lfs_size_t size; + lfs_block_t tail[2]; + } d; } lfs_dir_t; typedef struct lfs_superblock { - lfs_off_t off; + lfs_off_t off; - struct lfs_disk_superblock { - uint8_t type; - uint8_t elen; - uint8_t alen; - uint8_t nlen; - lfs_block_t root[2]; - uint32_t block_size; - uint32_t block_count; - uint32_t version; - char magic[8]; - } d; + struct lfs_disk_superblock { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + lfs_block_t root[2]; + uint32_t block_size; + uint32_t block_count; + uint32_t version; + char magic[8]; + } d; } lfs_superblock_t; typedef struct lfs_free { - lfs_block_t off; - lfs_block_t size; - lfs_block_t i; - lfs_block_t ack; - uint32_t *buffer; + lfs_block_t off; + lfs_block_t size; + lfs_block_t i; + lfs_block_t ack; + uint32_t *buffer; } lfs_free_t; // The littlefs type typedef struct lfs { - const struct lfs_config *cfg; + const struct lfs_config *cfg; - lfs_block_t root[2]; - lfs_file_t *files; - lfs_dir_t *dirs; + lfs_block_t root[2]; + lfs_file_t *files; + lfs_dir_t *dirs; - lfs_cache_t rcache; - lfs_cache_t pcache; + lfs_cache_t rcache; + lfs_cache_t pcache; - lfs_free_t free; - bool deorphaned; + lfs_free_t free; + bool deorphaned; } lfs_t; /// Filesystem functions /// diff --git a/src/platform/stm32wl/littlefs/lfs_util.c b/src/platform/stm32wl/littlefs/lfs_util.c index 8c820ce04..0b352c51f 100644 --- a/src/platform/stm32wl/littlefs/lfs_util.c +++ b/src/platform/stm32wl/littlefs/lfs_util.c @@ -10,18 +10,19 @@ #ifndef LFS_CONFIG // Software CRC implementation with small lookup table -void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) { - static const uint32_t rtable[16] = { - 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, - 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, - }; +void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) +{ + static const uint32_t rtable[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, + }; - const uint8_t *data = buffer; + const uint8_t *data = buffer; - for (size_t i = 0; i < size; i++) { - *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf]; - *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf]; - } + for (size_t i = 0; i < size; i++) { + *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf]; + *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf]; + } } #endif diff --git a/src/platform/stm32wl/littlefs/lfs_util.h b/src/platform/stm32wl/littlefs/lfs_util.h index 6ab69766a..5c8469f88 100644 --- a/src/platform/stm32wl/littlefs/lfs_util.h +++ b/src/platform/stm32wl/littlefs/lfs_util.h @@ -80,96 +80,114 @@ extern "C" { // expensive basic C implementation for debugging purposes // Min/max functions for unsigned 32-bit numbers -static inline uint32_t lfs_max(uint32_t a, uint32_t b) { return (a > b) ? a : b; } +static inline uint32_t lfs_max(uint32_t a, uint32_t b) +{ + return (a > b) ? a : b; +} -static inline uint32_t lfs_min(uint32_t a, uint32_t b) { return (a < b) ? a : b; } +static inline uint32_t lfs_min(uint32_t a, uint32_t b) +{ + return (a < b) ? a : b; +} // Find the next smallest power of 2 less than or equal to a -static inline uint32_t lfs_npw2(uint32_t a) { +static inline uint32_t lfs_npw2(uint32_t a) +{ #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return 32 - __builtin_clz(a - 1); + return 32 - __builtin_clz(a - 1); #else - uint32_t r = 0; - uint32_t s; - a -= 1; - s = (a > 0xffff) << 4; - a >>= s; - r |= s; - s = (a > 0xff) << 3; - a >>= s; - r |= s; - s = (a > 0xf) << 2; - a >>= s; - r |= s; - s = (a > 0x3) << 1; - a >>= s; - r |= s; - return (r | (a >> 1)) + 1; + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; + a >>= s; + r |= s; + s = (a > 0xff) << 3; + a >>= s; + r |= s; + s = (a > 0xf) << 2; + a >>= s; + r |= s; + s = (a > 0x3) << 1; + a >>= s; + r |= s; + return (r | (a >> 1)) + 1; #endif } // Count the number of trailing binary zeros in a // lfs_ctz(0) may be undefined -static inline uint32_t lfs_ctz(uint32_t a) { +static inline uint32_t lfs_ctz(uint32_t a) +{ #if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) - return __builtin_ctz(a); + return __builtin_ctz(a); #else - return lfs_npw2((a & -a) + 1) - 1; + return lfs_npw2((a & -a) + 1) - 1; #endif } // Count the number of binary ones in a -static inline uint32_t lfs_popc(uint32_t a) { +static inline uint32_t lfs_popc(uint32_t a) +{ #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return __builtin_popcount(a); + return __builtin_popcount(a); #else - a = a - ((a >> 1) & 0x55555555); - a = (a & 0x33333333) + ((a >> 2) & 0x33333333); - return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; #endif } // Find the sequence comparison of a and b, this is the distance // between a and b ignoring overflow -static inline int lfs_scmp(uint32_t a, uint32_t b) { return (int)(unsigned)(a - b); } +static inline int lfs_scmp(uint32_t a, uint32_t b) +{ + return (int)(unsigned)(a - b); +} // Convert from 32-bit little-endian to native order -static inline uint32_t lfs_fromle32(uint32_t a) { -#if !defined(LFS_NO_INTRINSICS) && \ - ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ - (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) - return a; -#elif !defined(LFS_NO_INTRINSICS) && \ - ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ +static inline uint32_t lfs_fromle32(uint32_t a) +{ +#if !defined(LFS_NO_INTRINSICS) && ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || \ + (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return a; +#elif !defined(LFS_NO_INTRINSICS) && \ + ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) - return __builtin_bswap32(a); + return __builtin_bswap32(a); #else - return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); + return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); #endif } // Convert to 32-bit little-endian from native order -static inline uint32_t lfs_tole32(uint32_t a) { return lfs_fromle32(a); } +static inline uint32_t lfs_tole32(uint32_t a) +{ + return lfs_fromle32(a); +} // Calculate CRC-32 with polynomial = 0x04c11db7 void lfs_crc(uint32_t *crc, const void *buffer, size_t size); // Allocate memory, only used if buffers are not provided to littlefs -static inline void *lfs_malloc(size_t size) { +static inline void *lfs_malloc(size_t size) +{ #ifndef LFS_NO_MALLOC - return malloc(size); + return malloc(size); #else - (void)size; - return NULL; + (void)size; + return NULL; #endif } // Deallocate memory, only used if buffers are not provided to littlefs -static inline void lfs_free(void *p) { +static inline void lfs_free(void *p) +{ #ifndef LFS_NO_MALLOC - free(p); + free(p); #else - (void)p; + (void)p; #endif } diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index 2af57286f..e841f8f29 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -9,19 +9,20 @@ void playStartMelody() {} void updateBatteryLevel(uint8_t level) {} -void getMacAddr(uint8_t *dmac) { - // https://flit.github.io/2020/06/06/mcu-unique-id-survey.html - const uint32_t uid0 = HAL_GetUIDw0(); // X/Y coordinate on wafer - const uint32_t uid1 = HAL_GetUIDw1(); // [31:8] Lot number (23:0), [7:0] Wafer number - const uint32_t uid2 = HAL_GetUIDw2(); // Lot number (55:24) +void getMacAddr(uint8_t *dmac) +{ + // https://flit.github.io/2020/06/06/mcu-unique-id-survey.html + const uint32_t uid0 = HAL_GetUIDw0(); // X/Y coordinate on wafer + const uint32_t uid1 = HAL_GetUIDw1(); // [31:8] Lot number (23:0), [7:0] Wafer number + const uint32_t uid2 = HAL_GetUIDw2(); // Lot number (55:24) - // Need to go from 96-bit to 48-bit unique ID - dmac[5] = (uint8_t)uid0; - dmac[4] = (uint8_t)(uid0 >> 16); - dmac[3] = (uint8_t)uid1; - dmac[2] = (uint8_t)(uid1 >> 8); - dmac[1] = (uint8_t)uid2; - dmac[0] = (uint8_t)(uid2 >> 8); + // Need to go from 96-bit to 48-bit unique ID + dmac[5] = (uint8_t)uid0; + dmac[4] = (uint8_t)(uid0 >> 16); + dmac[3] = (uint8_t)uid1; + dmac[2] = (uint8_t)(uid1 >> 8); + dmac[1] = (uint8_t)uid2; + dmac[0] = (uint8_t)(uid2 >> 8); } void cpuDeepSleep(uint32_t msecToWake) {} @@ -29,20 +30,27 @@ void cpuDeepSleep(uint32_t msecToWake) {} // Hacks to force more code and data out. // By default __assert_func uses fiprintf which pulls in stdio. -extern "C" void __wrap___assert_func(const char *, int, const char *, const char *) { - while (true) - ; - return; +extern "C" void __wrap___assert_func(const char *, int, const char *, const char *) +{ + while (true) + ; + return; } // By default strerror has a lot of strings we probably don't use. Make it return an empty string instead. char empty = 0; -extern "C" char *__wrap_strerror(int) { return ∅ } +extern "C" char *__wrap_strerror(int) +{ + return ∅ +} #ifdef MESHTASTIC_EXCLUDE_TZ struct _reent; -// Even if you don't use timezones, mktime will try to set the timezone anyway with _tzset_unlocked(), which pulls in -// scanf and friends. The timezone is initialized to UTC by default. -extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) { return; } +// Even if you don't use timezones, mktime will try to set the timezone anyway with _tzset_unlocked(), which pulls in scanf and +// friends. The timezone is initialized to UTC by default. +extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) +{ + return; +} #endif \ No newline at end of file diff --git a/src/power.h b/src/power.h index 710d21131..c826d98b4 100644 --- a/src/power.h +++ b/src/power.h @@ -93,42 +93,43 @@ extern RAK9154Sensor rak9154Sensor; extern XPowersLibInterface *PMU; #endif -class Power : private concurrency::OSThread { +class Power : private concurrency::OSThread +{ -public: - Observable newStatus; + public: + Observable newStatus; - Power(); + Power(); - void powerCommandsCheck(); - void readPowerStatus(); - virtual bool setup(); - virtual int32_t runOnce() override; - void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } - const uint16_t OCV[11] = {OCV_ARRAY}; + void powerCommandsCheck(); + void readPowerStatus(); + virtual bool setup(); + virtual int32_t runOnce() override; + void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } + const uint16_t OCV[11] = {OCV_ARRAY}; -protected: - meshtastic::PowerStatus *statusHandler; + protected: + meshtastic::PowerStatus *statusHandler; - /// Setup a xpowers chip axp192/axp2101, return true if found - bool axpChipInit(); - /// Setup a simple ADC input based battery sensor - bool analogInit(); - /// Setup a Lipo battery level sensor - bool lipoInit(); - /// Setup a Lipo charger - bool lipoChargerInit(); - /// Setup a meshSolar battery sensor - bool meshSolarInit(); + /// Setup a xpowers chip axp192/axp2101, return true if found + bool axpChipInit(); + /// Setup a simple ADC input based battery sensor + bool analogInit(); + /// Setup a Lipo battery level sensor + bool lipoInit(); + /// Setup a Lipo charger + bool lipoChargerInit(); + /// Setup a meshSolar battery sensor + bool meshSolarInit(); -private: - void shutdown(); - void reboot(); - // open circuit voltage lookup table - uint8_t low_voltage_counter; - uint32_t lastLogTime = 0; + private: + void shutdown(); + void reboot(); + // open circuit voltage lookup table + uint8_t low_voltage_counter; + uint32_t lastLogTime = 0; #ifdef DEBUG_HEAP - uint32_t lastheap; + uint32_t lastheap; #endif }; diff --git a/src/serialization/JSON.cpp b/src/serialization/JSON.cpp index fc19aa720..42e615108 100644 --- a/src/serialization/JSON.cpp +++ b/src/serialization/JSON.cpp @@ -41,24 +41,25 @@ JSON::JSON() {} * * @return JSONValue* Returns a JSON Value representing the root, or NULL on error */ -JSONValue *JSON::Parse(const char *data) { - // Skip any preceding whitespace, end of data = no JSON = fail - if (!SkipWhitespace(&data)) - return NULL; +JSONValue *JSON::Parse(const char *data) +{ + // Skip any preceding whitespace, end of data = no JSON = fail + if (!SkipWhitespace(&data)) + return NULL; - // We need the start of a value here now... - JSONValue *value = JSONValue::Parse(&data); - if (value == NULL) - return NULL; + // We need the start of a value here now... + JSONValue *value = JSONValue::Parse(&data); + if (value == NULL) + return NULL; - // Can be white space now and should be at the end of the string then... - if (SkipWhitespace(&data)) { - delete value; - return NULL; - } + // Can be white space now and should be at the end of the string then... + if (SkipWhitespace(&data)) { + delete value; + return NULL; + } - // We're now at the end of the string - return value; + // We're now at the end of the string + return value; } /** @@ -70,11 +71,12 @@ JSONValue *JSON::Parse(const char *data) { * * @return std::string Returns a JSON encoded string representation of the given value */ -std::string JSON::Stringify(const JSONValue *value) { - if (value != NULL) - return value->Stringify(); - else - return ""; +std::string JSON::Stringify(const JSONValue *value) +{ + if (value != NULL) + return value->Stringify(); + else + return ""; } /** @@ -86,11 +88,12 @@ std::string JSON::Stringify(const JSONValue *value) { * * @return bool Returns true if there is more data, or false if the end of the text was reached */ -bool JSON::SkipWhitespace(const char **data) { - while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n')) - (*data)++; +bool JSON::SkipWhitespace(const char **data) +{ + while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n')) + (*data)++; - return **data != 0; + return **data != 0; } /** @@ -104,101 +107,102 @@ bool JSON::SkipWhitespace(const char **data) { * * @return bool Returns true on success, false on failure */ -bool JSON::ExtractString(const char **data, std::string &str) { - str = ""; +bool JSON::ExtractString(const char **data, std::string &str) +{ + str = ""; - while (**data != 0) { - // Save the char so we can change it if need be - char next_char = **data; + while (**data != 0) { + // Save the char so we can change it if need be + char next_char = **data; - // Escaping something? - if (next_char == '\\') { - // Move over the escape char - (*data)++; + // Escaping something? + if (next_char == '\\') { + // Move over the escape char + (*data)++; - // Deal with the escaped char - switch (**data) { - case '"': - next_char = '"'; - break; - case '\\': - next_char = '\\'; - break; - case '/': - next_char = '/'; - break; - case 'b': - next_char = '\b'; - break; - case 'f': - next_char = '\f'; - break; - case 'n': - next_char = '\n'; - break; - case 'r': - next_char = '\r'; - break; - case 't': - next_char = '\t'; - break; - case 'u': { - // We need 5 chars (4 hex + the 'u') or its not valid - if (!simplejson_csnlen(*data, 5)) - return false; + // Deal with the escaped char + switch (**data) { + case '"': + next_char = '"'; + break; + case '\\': + next_char = '\\'; + break; + case '/': + next_char = '/'; + break; + case 'b': + next_char = '\b'; + break; + case 'f': + next_char = '\f'; + break; + case 'n': + next_char = '\n'; + break; + case 'r': + next_char = '\r'; + break; + case 't': + next_char = '\t'; + break; + case 'u': { + // We need 5 chars (4 hex + the 'u') or its not valid + if (!simplejson_csnlen(*data, 5)) + return false; - // Deal with the chars - next_char = 0; - for (int i = 0; i < 4; i++) { - // Do it first to move off the 'u' and leave us on the - // final hex digit as we move on by one later on - (*data)++; + // Deal with the chars + next_char = 0; + for (int i = 0; i < 4; i++) { + // Do it first to move off the 'u' and leave us on the + // final hex digit as we move on by one later on + (*data)++; - next_char <<= 4; + next_char <<= 4; - // Parse the hex digit - if (**data >= '0' && **data <= '9') - next_char |= (**data - '0'); - else if (**data >= 'A' && **data <= 'F') - next_char |= (10 + (**data - 'A')); - else if (**data >= 'a' && **data <= 'f') - next_char |= (10 + (**data - 'a')); - else { - // Invalid hex digit = invalid JSON - return false; - } + // Parse the hex digit + if (**data >= '0' && **data <= '9') + next_char |= (**data - '0'); + else if (**data >= 'A' && **data <= 'F') + next_char |= (10 + (**data - 'A')); + else if (**data >= 'a' && **data <= 'f') + next_char |= (10 + (**data - 'a')); + else { + // Invalid hex digit = invalid JSON + return false; + } + } + break; + } + + // By the spec, only the above cases are allowed + default: + return false; + } } - break; - } - // By the spec, only the above cases are allowed - default: - return false; - } + // End of the string? + else if (next_char == '"') { + (*data)++; + str.shrink_to_fit(); // Remove unused capacity + return true; + } + + // Disallowed char? + else if (next_char < ' ' && next_char != '\t') { + // SPEC Violation: Allow tabs due to real world cases + return false; + } + + // Add the next char + str += next_char; + + // Move on + (*data)++; } - // End of the string? - else if (next_char == '"') { - (*data)++; - str.shrink_to_fit(); // Remove unused capacity - return true; - } - - // Disallowed char? - else if (next_char < ' ' && next_char != '\t') { - // SPEC Violation: Allow tabs due to real world cases - return false; - } - - // Add the next char - str += next_char; - - // Move on - (*data)++; - } - - // If we're here, the string ended incorrectly - return false; + // If we're here, the string ended incorrectly + return false; } /** @@ -210,12 +214,13 @@ bool JSON::ExtractString(const char **data, std::string &str) { * * @return double Returns the double value of the number found */ -double JSON::ParseInt(const char **data) { - double integer = 0; - while (**data != 0 && **data >= '0' && **data <= '9') - integer = integer * 10 + (*(*data)++ - '0'); +double JSON::ParseInt(const char **data) +{ + double integer = 0; + while (**data != 0 && **data >= '0' && **data <= '9') + integer = integer * 10 + (*(*data)++ - '0'); - return integer; + return integer; } /** @@ -227,13 +232,14 @@ double JSON::ParseInt(const char **data) { * * @return double Returns the double value of the decimal found */ -double JSON::ParseDecimal(const char **data) { - double decimal = 0.0; - double factor = 0.1; - while (**data != 0 && **data >= '0' && **data <= '9') { - int digit = (*(*data)++ - '0'); - decimal = decimal + digit * factor; - factor *= 0.1; - } - return decimal; +double JSON::ParseDecimal(const char **data) +{ + double decimal = 0.0; + double factor = 0.1; + while (**data != 0 && **data >= '0' && **data <= '9') { + int digit = (*(*data)++ - '0'); + decimal = decimal + digit * factor; + factor *= 0.1; + } + return decimal; } diff --git a/src/serialization/JSON.h b/src/serialization/JSON.h index 896948d7f..33f34e684 100644 --- a/src/serialization/JSON.h +++ b/src/serialization/JSON.h @@ -31,17 +31,18 @@ #include // Simple function to check a string 's' has at least 'n' characters -static inline bool simplejson_csnlen(const char *s, size_t n) { - if (s == 0) - return false; +static inline bool simplejson_csnlen(const char *s, size_t n) +{ + if (s == 0) + return false; - const char *save = s; - while (n-- > 0) { - if (*(save++) == 0) - return false; - } + const char *save = s; + while (n-- > 0) { + if (*(save++) == 0) + return false; + } - return true; + return true; } // Custom types @@ -51,21 +52,22 @@ typedef std::map JSONObject; #include "JSONValue.h" -class JSON { - friend class JSONValue; +class JSON +{ + friend class JSONValue; -public: - static JSONValue *Parse(const char *data); - static std::string Stringify(const JSONValue *value); + public: + static JSONValue *Parse(const char *data); + static std::string Stringify(const JSONValue *value); -protected: - static bool SkipWhitespace(const char **data); - static bool ExtractString(const char **data, std::string &str); - static double ParseInt(const char **data); - static double ParseDecimal(const char **data); + protected: + static bool SkipWhitespace(const char **data); + static bool ExtractString(const char **data, std::string &str); + static double ParseInt(const char **data); + static double ParseDecimal(const char **data); -private: - JSON(); + private: + JSON(); }; #endif diff --git a/src/serialization/JSONValue.cpp b/src/serialization/JSONValue.cpp index 723784177..20cd90373 100644 --- a/src/serialization/JSONValue.cpp +++ b/src/serialization/JSONValue.cpp @@ -33,20 +33,20 @@ #include "JSONValue.h" // Macros to free an array/object -#define FREE_ARRAY(x) \ - { \ - JSONArray::iterator iter; \ - for (iter = x.begin(); iter != x.end(); ++iter) { \ - delete *iter; \ - } \ - } -#define FREE_OBJECT(x) \ - { \ - JSONObject::iterator iter; \ - for (iter = x.begin(); iter != x.end(); ++iter) { \ - delete (*iter).second; \ - } \ - } +#define FREE_ARRAY(x) \ + { \ + JSONArray::iterator iter; \ + for (iter = x.begin(); iter != x.end(); ++iter) { \ + delete *iter; \ + } \ + } +#define FREE_OBJECT(x) \ + { \ + JSONObject::iterator iter; \ + for (iter = x.begin(); iter != x.end(); ++iter) { \ + delete (*iter).second; \ + } \ + } /** * Parses a JSON encoded value to a JSONValue object @@ -57,233 +57,234 @@ * * @return JSONValue* Returns a pointer to a JSONValue object on success, NULL on error */ -JSONValue *JSONValue::Parse(const char **data) { - // Is it a string? - if (**data == '"') { - std::string str; - if (!JSON::ExtractString(&(++(*data)), str)) - return NULL; - else - return new JSONValue(str); - } - - // Is it a boolean? - else if ((simplejson_csnlen(*data, 4) && strncasecmp(*data, "true", 4) == 0) || - (simplejson_csnlen(*data, 5) && strncasecmp(*data, "false", 5) == 0)) { - bool value = strncasecmp(*data, "true", 4) == 0; - (*data) += value ? 4 : 5; - return new JSONValue(value); - } - - // Is it a null? - else if (simplejson_csnlen(*data, 4) && strncasecmp(*data, "null", 4) == 0) { - (*data) += 4; - return new JSONValue(); - } - - // Is it a number? - else if (**data == '-' || (**data >= '0' && **data <= '9')) { - // Negative? - bool neg = **data == '-'; - if (neg) - (*data)++; - - double number = 0.0; - - // Parse the whole part of the number - only if it wasn't 0 - if (**data == '0') - (*data)++; - else if (**data >= '1' && **data <= '9') - number = JSON::ParseInt(data); - else - return NULL; - - // Could be a decimal now... - if (**data == '.') { - (*data)++; - - // Not get any digits? - if (!(**data >= '0' && **data <= '9')) - return NULL; - - // Find the decimal and sort the decimal place out - // Use ParseDecimal as ParseInt won't work with decimals less than 0.1 - // thanks to Javier Abadia for the report & fix - double decimal = JSON::ParseDecimal(data); - - // Save the number - number += decimal; +JSONValue *JSONValue::Parse(const char **data) +{ + // Is it a string? + if (**data == '"') { + std::string str; + if (!JSON::ExtractString(&(++(*data)), str)) + return NULL; + else + return new JSONValue(str); } - // Could be an exponent now... - if (**data == 'E' || **data == 'e') { - (*data)++; - - // Check signage of expo - bool neg_expo = false; - if (**data == '-' || **data == '+') { - neg_expo = **data == '-'; - (*data)++; - } - - // Not get any digits? - if (!(**data >= '0' && **data <= '9')) - return NULL; - - // Sort the expo out - double expo = JSON::ParseInt(data); - for (double i = 0.0; i < expo; i++) - number = neg_expo ? (number / 10.0) : (number * 10.0); + // Is it a boolean? + else if ((simplejson_csnlen(*data, 4) && strncasecmp(*data, "true", 4) == 0) || + (simplejson_csnlen(*data, 5) && strncasecmp(*data, "false", 5) == 0)) { + bool value = strncasecmp(*data, "true", 4) == 0; + (*data) += value ? 4 : 5; + return new JSONValue(value); } - // Was it neg? - if (neg) - number *= -1; - - return new JSONValue(number); - } - - // An object? - else if (**data == '{') { - JSONObject object; - - (*data)++; - - while (**data != 0) { - // Whitespace at the start? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } - - // Special case - empty object - if (object.size() == 0 && **data == '}') { - (*data)++; - return new JSONValue(object); - } - - // We want a string now... - std::string name; - if (!JSON::ExtractString(&(++(*data)), name)) { - FREE_OBJECT(object); - return NULL; - } - - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } - - // Need a : now - if (*((*data)++) != ':') { - FREE_OBJECT(object); - return NULL; - } - - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } - - // The value is here - JSONValue *value = Parse(data); - if (value == NULL) { - FREE_OBJECT(object); - return NULL; - } - - // Add the name:value - if (object.find(name) != object.end()) - delete object[name]; - object[name] = value; - - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_OBJECT(object); - return NULL; - } - - // End of object? - if (**data == '}') { - (*data)++; - return new JSONValue(object); - } - - // Want a , now - if (**data != ',') { - FREE_OBJECT(object); - return NULL; - } - - (*data)++; + // Is it a null? + else if (simplejson_csnlen(*data, 4) && strncasecmp(*data, "null", 4) == 0) { + (*data) += 4; + return new JSONValue(); } - // Only here if we ran out of data - FREE_OBJECT(object); - return NULL; - } + // Is it a number? + else if (**data == '-' || (**data >= '0' && **data <= '9')) { + // Negative? + bool neg = **data == '-'; + if (neg) + (*data)++; - // An array? - else if (**data == '[') { - JSONArray array; + double number = 0.0; - (*data)++; + // Parse the whole part of the number - only if it wasn't 0 + if (**data == '0') + (*data)++; + else if (**data >= '1' && **data <= '9') + number = JSON::ParseInt(data); + else + return NULL; - while (**data != 0) { - // Whitespace at the start? - if (!JSON::SkipWhitespace(data)) { - FREE_ARRAY(array); - return NULL; - } + // Could be a decimal now... + if (**data == '.') { + (*data)++; - // Special case - empty array - if (array.size() == 0 && **data == ']') { - (*data)++; - return new JSONValue(array); - } + // Not get any digits? + if (!(**data >= '0' && **data <= '9')) + return NULL; - // Get the value - JSONValue *value = Parse(data); - if (value == NULL) { - FREE_ARRAY(array); - return NULL; - } + // Find the decimal and sort the decimal place out + // Use ParseDecimal as ParseInt won't work with decimals less than 0.1 + // thanks to Javier Abadia for the report & fix + double decimal = JSON::ParseDecimal(data); - // Add the value - array.push_back(value); + // Save the number + number += decimal; + } - // More whitespace? - if (!JSON::SkipWhitespace(data)) { - FREE_ARRAY(array); - return NULL; - } + // Could be an exponent now... + if (**data == 'E' || **data == 'e') { + (*data)++; - // End of array? - if (**data == ']') { - (*data)++; - return new JSONValue(array); - } + // Check signage of expo + bool neg_expo = false; + if (**data == '-' || **data == '+') { + neg_expo = **data == '-'; + (*data)++; + } - // Want a , now - if (**data != ',') { - FREE_ARRAY(array); - return NULL; - } + // Not get any digits? + if (!(**data >= '0' && **data <= '9')) + return NULL; - (*data)++; + // Sort the expo out + double expo = JSON::ParseInt(data); + for (double i = 0.0; i < expo; i++) + number = neg_expo ? (number / 10.0) : (number * 10.0); + } + + // Was it neg? + if (neg) + number *= -1; + + return new JSONValue(number); } - // Only here if we ran out of data - FREE_ARRAY(array); - return NULL; - } + // An object? + else if (**data == '{') { + JSONObject object; - // Ran out of possibilities, it's bad! - else { - return NULL; - } + (*data)++; + + while (**data != 0) { + // Whitespace at the start? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // Special case - empty object + if (object.size() == 0 && **data == '}') { + (*data)++; + return new JSONValue(object); + } + + // We want a string now... + std::string name; + if (!JSON::ExtractString(&(++(*data)), name)) { + FREE_OBJECT(object); + return NULL; + } + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // Need a : now + if (*((*data)++) != ':') { + FREE_OBJECT(object); + return NULL; + } + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // The value is here + JSONValue *value = Parse(data); + if (value == NULL) { + FREE_OBJECT(object); + return NULL; + } + + // Add the name:value + if (object.find(name) != object.end()) + delete object[name]; + object[name] = value; + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_OBJECT(object); + return NULL; + } + + // End of object? + if (**data == '}') { + (*data)++; + return new JSONValue(object); + } + + // Want a , now + if (**data != ',') { + FREE_OBJECT(object); + return NULL; + } + + (*data)++; + } + + // Only here if we ran out of data + FREE_OBJECT(object); + return NULL; + } + + // An array? + else if (**data == '[') { + JSONArray array; + + (*data)++; + + while (**data != 0) { + // Whitespace at the start? + if (!JSON::SkipWhitespace(data)) { + FREE_ARRAY(array); + return NULL; + } + + // Special case - empty array + if (array.size() == 0 && **data == ']') { + (*data)++; + return new JSONValue(array); + } + + // Get the value + JSONValue *value = Parse(data); + if (value == NULL) { + FREE_ARRAY(array); + return NULL; + } + + // Add the value + array.push_back(value); + + // More whitespace? + if (!JSON::SkipWhitespace(data)) { + FREE_ARRAY(array); + return NULL; + } + + // End of array? + if (**data == ']') { + (*data)++; + return new JSONValue(array); + } + + // Want a , now + if (**data != ',') { + FREE_ARRAY(array); + return NULL; + } + + (*data)++; + } + + // Only here if we ran out of data + FREE_ARRAY(array); + return NULL; + } + + // Ran out of possibilities, it's bad! + else { + return NULL; + } } /** @@ -291,7 +292,10 @@ JSONValue *JSONValue::Parse(const char **data) { * * @access public */ -JSONValue::JSONValue(/*NULL*/) { type = JSONType_Null; } +JSONValue::JSONValue(/*NULL*/) +{ + type = JSONType_Null; +} /** * Basic constructor for creating a JSON Value of type String @@ -300,9 +304,10 @@ JSONValue::JSONValue(/*NULL*/) { type = JSONType_Null; } * * @param char* m_char_value The string to use as the value */ -JSONValue::JSONValue(const char *m_char_value) { - type = JSONType_String; - string_value = new std::string(std::string(m_char_value)); +JSONValue::JSONValue(const char *m_char_value) +{ + type = JSONType_String; + string_value = new std::string(std::string(m_char_value)); } /** @@ -312,9 +317,10 @@ JSONValue::JSONValue(const char *m_char_value) { * * @param std::string m_string_value The string to use as the value */ -JSONValue::JSONValue(const std::string &m_string_value) { - type = JSONType_String; - string_value = new std::string(m_string_value); +JSONValue::JSONValue(const std::string &m_string_value) +{ + type = JSONType_String; + string_value = new std::string(m_string_value); } /** @@ -324,9 +330,10 @@ JSONValue::JSONValue(const std::string &m_string_value) { * * @param bool m_bool_value The bool to use as the value */ -JSONValue::JSONValue(bool m_bool_value) { - type = JSONType_Bool; - bool_value = m_bool_value; +JSONValue::JSONValue(bool m_bool_value) +{ + type = JSONType_Bool; + bool_value = m_bool_value; } /** @@ -336,9 +343,10 @@ JSONValue::JSONValue(bool m_bool_value) { * * @param double m_number_value The number to use as the value */ -JSONValue::JSONValue(double m_number_value) { - type = JSONType_Number; - number_value = m_number_value; +JSONValue::JSONValue(double m_number_value) +{ + type = JSONType_Number; + number_value = m_number_value; } /** @@ -348,9 +356,10 @@ JSONValue::JSONValue(double m_number_value) { * * @param int m_integer_value The number to use as the value */ -JSONValue::JSONValue(int m_integer_value) { - type = JSONType_Number; - number_value = (double)m_integer_value; +JSONValue::JSONValue(int m_integer_value) +{ + type = JSONType_Number; + number_value = (double)m_integer_value; } /** @@ -360,9 +369,10 @@ JSONValue::JSONValue(int m_integer_value) { * * @param unsigned int m_integer_value The number to use as the value */ -JSONValue::JSONValue(unsigned int m_integer_value) { - type = JSONType_Number; - number_value = (double)m_integer_value; +JSONValue::JSONValue(unsigned int m_integer_value) +{ + type = JSONType_Number; + number_value = (double)m_integer_value; } /** @@ -372,9 +382,10 @@ JSONValue::JSONValue(unsigned int m_integer_value) { * * @param JSONArray m_array_value The JSONArray to use as the value */ -JSONValue::JSONValue(const JSONArray &m_array_value) { - type = JSONType_Array; - array_value = new JSONArray(m_array_value); +JSONValue::JSONValue(const JSONArray &m_array_value) +{ + type = JSONType_Array; + array_value = new JSONArray(m_array_value); } /** @@ -384,9 +395,10 @@ JSONValue::JSONValue(const JSONArray &m_array_value) { * * @param JSONObject m_object_value The JSONObject to use as the value */ -JSONValue::JSONValue(const JSONObject &m_object_value) { - type = JSONType_Object; - object_value = new JSONObject(m_object_value); +JSONValue::JSONValue(const JSONObject &m_object_value) +{ + type = JSONType_Object; + object_value = new JSONObject(m_object_value); } /** @@ -396,46 +408,47 @@ JSONValue::JSONValue(const JSONObject &m_object_value) { * * @param JSONValue m_source The source JSONValue that is being copied */ -JSONValue::JSONValue(const JSONValue &m_source) { - type = m_source.type; +JSONValue::JSONValue(const JSONValue &m_source) +{ + type = m_source.type; - switch (type) { - case JSONType_String: - string_value = new std::string(*m_source.string_value); - break; + switch (type) { + case JSONType_String: + string_value = new std::string(*m_source.string_value); + break; - case JSONType_Bool: - bool_value = m_source.bool_value; - break; + case JSONType_Bool: + bool_value = m_source.bool_value; + break; - case JSONType_Number: - number_value = m_source.number_value; - break; + case JSONType_Number: + number_value = m_source.number_value; + break; - case JSONType_Array: { - JSONArray source_array = *m_source.array_value; - JSONArray::iterator iter; - array_value = new JSONArray(); - for (iter = source_array.begin(); iter != source_array.end(); ++iter) - array_value->push_back(new JSONValue(**iter)); - break; - } - - case JSONType_Object: { - JSONObject source_object = *m_source.object_value; - object_value = new JSONObject(); - JSONObject::iterator iter; - for (iter = source_object.begin(); iter != source_object.end(); ++iter) { - std::string name = (*iter).first; - (*object_value)[name] = new JSONValue(*((*iter).second)); + case JSONType_Array: { + JSONArray source_array = *m_source.array_value; + JSONArray::iterator iter; + array_value = new JSONArray(); + for (iter = source_array.begin(); iter != source_array.end(); ++iter) + array_value->push_back(new JSONValue(**iter)); + break; } - break; - } - case JSONType_Null: - // Nothing to do. - break; - } + case JSONType_Object: { + JSONObject source_object = *m_source.object_value; + object_value = new JSONObject(); + JSONObject::iterator iter; + for (iter = source_object.begin(); iter != source_object.end(); ++iter) { + std::string name = (*iter).first; + (*object_value)[name] = new JSONValue(*((*iter).second)); + } + break; + } + + case JSONType_Null: + // Nothing to do. + break; + } } /** @@ -444,21 +457,22 @@ JSONValue::JSONValue(const JSONValue &m_source) { * * @access public */ -JSONValue::~JSONValue() { - if (type == JSONType_Array) { - JSONArray::iterator iter; - for (iter = array_value->begin(); iter != array_value->end(); ++iter) - delete *iter; - delete array_value; - } else if (type == JSONType_Object) { - JSONObject::iterator iter; - for (iter = object_value->begin(); iter != object_value->end(); ++iter) { - delete (*iter).second; +JSONValue::~JSONValue() +{ + if (type == JSONType_Array) { + JSONArray::iterator iter; + for (iter = array_value->begin(); iter != array_value->end(); ++iter) + delete *iter; + delete array_value; + } else if (type == JSONType_Object) { + JSONObject::iterator iter; + for (iter = object_value->begin(); iter != object_value->end(); ++iter) { + delete (*iter).second; + } + delete object_value; + } else if (type == JSONType_String) { + delete string_value; } - delete object_value; - } else if (type == JSONType_String) { - delete string_value; - } } /** @@ -468,7 +482,10 @@ JSONValue::~JSONValue() { * * @return bool Returns true if it is a NULL value, false otherwise */ -bool JSONValue::IsNull() const { return type == JSONType_Null; } +bool JSONValue::IsNull() const +{ + return type == JSONType_Null; +} /** * Checks if the value is a String @@ -477,7 +494,10 @@ bool JSONValue::IsNull() const { return type == JSONType_Null; } * * @return bool Returns true if it is a String value, false otherwise */ -bool JSONValue::IsString() const { return type == JSONType_String; } +bool JSONValue::IsString() const +{ + return type == JSONType_String; +} /** * Checks if the value is a Bool @@ -486,7 +506,10 @@ bool JSONValue::IsString() const { return type == JSONType_String; } * * @return bool Returns true if it is a Bool value, false otherwise */ -bool JSONValue::IsBool() const { return type == JSONType_Bool; } +bool JSONValue::IsBool() const +{ + return type == JSONType_Bool; +} /** * Checks if the value is a Number @@ -495,7 +518,10 @@ bool JSONValue::IsBool() const { return type == JSONType_Bool; } * * @return bool Returns true if it is a Number value, false otherwise */ -bool JSONValue::IsNumber() const { return type == JSONType_Number; } +bool JSONValue::IsNumber() const +{ + return type == JSONType_Number; +} /** * Checks if the value is an Array @@ -504,7 +530,10 @@ bool JSONValue::IsNumber() const { return type == JSONType_Number; } * * @return bool Returns true if it is an Array value, false otherwise */ -bool JSONValue::IsArray() const { return type == JSONType_Array; } +bool JSONValue::IsArray() const +{ + return type == JSONType_Array; +} /** * Checks if the value is an Object @@ -513,7 +542,10 @@ bool JSONValue::IsArray() const { return type == JSONType_Array; } * * @return bool Returns true if it is an Object value, false otherwise */ -bool JSONValue::IsObject() const { return type == JSONType_Object; } +bool JSONValue::IsObject() const +{ + return type == JSONType_Object; +} /** * Retrieves the String value of this JSONValue @@ -523,7 +555,10 @@ bool JSONValue::IsObject() const { return type == JSONType_Object; } * * @return std::string Returns the string value */ -const std::string &JSONValue::AsString() const { return (*string_value); } +const std::string &JSONValue::AsString() const +{ + return (*string_value); +} /** * Retrieves the Bool value of this JSONValue @@ -533,7 +568,10 @@ const std::string &JSONValue::AsString() const { return (*string_value); } * * @return bool Returns the bool value */ -bool JSONValue::AsBool() const { return bool_value; } +bool JSONValue::AsBool() const +{ + return bool_value; +} /** * Retrieves the Number value of this JSONValue @@ -543,7 +581,10 @@ bool JSONValue::AsBool() const { return bool_value; } * * @return double Returns the number value */ -double JSONValue::AsNumber() const { return number_value; } +double JSONValue::AsNumber() const +{ + return number_value; +} /** * Retrieves the Array value of this JSONValue @@ -553,7 +594,10 @@ double JSONValue::AsNumber() const { return number_value; } * * @return JSONArray Returns the array value */ -const JSONArray &JSONValue::AsArray() const { return (*array_value); } +const JSONArray &JSONValue::AsArray() const +{ + return (*array_value); +} /** * Retrieves the Object value of this JSONValue @@ -563,7 +607,10 @@ const JSONArray &JSONValue::AsArray() const { return (*array_value); } * * @return JSONObject Returns the object value */ -const JSONObject &JSONValue::AsObject() const { return (*object_value); } +const JSONObject &JSONValue::AsObject() const +{ + return (*object_value); +} /** * Retrieves the number of children of this JSONValue. @@ -574,15 +621,16 @@ const JSONObject &JSONValue::AsObject() const { return (*object_value); } * * @return The number of children. */ -std::size_t JSONValue::CountChildren() const { - switch (type) { - case JSONType_Array: - return array_value->size(); - case JSONType_Object: - return object_value->size(); - default: - return 0; - } +std::size_t JSONValue::CountChildren() const +{ + switch (type) { + case JSONType_Array: + return array_value->size(); + case JSONType_Object: + return object_value->size(); + default: + return 0; + } } /** @@ -593,12 +641,13 @@ std::size_t JSONValue::CountChildren() const { * * @return bool Returns true if the array has a value at the given index. */ -bool JSONValue::HasChild(std::size_t index) const { - if (type == JSONType_Array) { - return index < array_value->size(); - } else { - return false; - } +bool JSONValue::HasChild(std::size_t index) const +{ + if (type == JSONType_Array) { + return index < array_value->size(); + } else { + return false; + } } /** @@ -610,12 +659,13 @@ bool JSONValue::HasChild(std::size_t index) const { * @return JSONValue* Returns JSONValue at the given index or NULL * if it doesn't exist. */ -JSONValue *JSONValue::Child(std::size_t index) { - if (index < array_value->size()) { - return (*array_value)[index]; - } else { - return NULL; - } +JSONValue *JSONValue::Child(std::size_t index) +{ + if (index < array_value->size()) { + return (*array_value)[index]; + } else { + return NULL; + } } /** @@ -626,12 +676,13 @@ JSONValue *JSONValue::Child(std::size_t index) { * * @return bool Returns true if the object has a value at the given key. */ -bool JSONValue::HasChild(const char *name) const { - if (type == JSONType_Object) { - return object_value->find(name) != object_value->end(); - } else { - return false; - } +bool JSONValue::HasChild(const char *name) const +{ + if (type == JSONType_Object) { + return object_value->find(name) != object_value->end(); + } else { + return false; + } } /** @@ -643,13 +694,14 @@ bool JSONValue::HasChild(const char *name) const { * @return JSONValue* Returns JSONValue for the given key in the object * or NULL if it doesn't exist. */ -JSONValue *JSONValue::Child(const char *name) { - JSONObject::const_iterator it = object_value->find(name); - if (it != object_value->end()) { - return it->second; - } else { - return NULL; - } +JSONValue *JSONValue::Child(const char *name) +{ + JSONObject::const_iterator it = object_value->find(name); + if (it != object_value->end()) { + return it->second; + } else { + return NULL; + } } /** @@ -660,19 +712,20 @@ JSONValue *JSONValue::Child(const char *name) { * * @return std::vector A vector containing the keys. */ -std::vector JSONValue::ObjectKeys() const { - std::vector keys; +std::vector JSONValue::ObjectKeys() const +{ + std::vector keys; - if (type == JSONType_Object) { - JSONObject::const_iterator iter = object_value->begin(); - while (iter != object_value->end()) { - keys.push_back(iter->first); + if (type == JSONType_Object) { + JSONObject::const_iterator iter = object_value->begin(); + while (iter != object_value->end()) { + keys.push_back(iter->first); - ++iter; + ++iter; + } } - } - return keys; + return keys; } /** @@ -684,9 +737,10 @@ std::vector JSONValue::ObjectKeys() const { * * @return std::string Returns the JSON string */ -std::string JSONValue::Stringify(bool const prettyprint) const { - size_t const indentDepth = prettyprint ? 1 : 0; - return StringifyImpl(indentDepth); +std::string JSONValue::Stringify(bool const prettyprint) const +{ + size_t const indentDepth = prettyprint ? 1 : 0; + return StringifyImpl(indentDepth); } /** @@ -698,69 +752,70 @@ std::string JSONValue::Stringify(bool const prettyprint) const { * * @return std::string Returns the JSON string */ -std::string JSONValue::StringifyImpl(size_t const indentDepth) const { - std::string ret_string; - size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0; - std::string const indentStr = Indent(indentDepth); - std::string const indentStr1 = Indent(indentDepth1); +std::string JSONValue::StringifyImpl(size_t const indentDepth) const +{ + std::string ret_string; + size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0; + std::string const indentStr = Indent(indentDepth); + std::string const indentStr1 = Indent(indentDepth1); - switch (type) { - case JSONType_Null: - ret_string = "null"; - break; + switch (type) { + case JSONType_Null: + ret_string = "null"; + break; - case JSONType_String: - ret_string = StringifyString(*string_value); - break; + case JSONType_String: + ret_string = StringifyString(*string_value); + break; - case JSONType_Bool: - ret_string = bool_value ? "true" : "false"; - break; + case JSONType_Bool: + ret_string = bool_value ? "true" : "false"; + break; - case JSONType_Number: { - if (isinf(number_value) || isnan(number_value)) - ret_string = "null"; - else { - std::stringstream ss; - ss.precision(15); - ss << number_value; - ret_string = ss.str(); + case JSONType_Number: { + if (isinf(number_value) || isnan(number_value)) + ret_string = "null"; + else { + std::stringstream ss; + ss.precision(15); + ss << number_value; + ret_string = ss.str(); + } + break; } - break; - } - case JSONType_Array: { - ret_string = indentDepth ? "[\n" + indentStr1 : "["; - JSONArray::const_iterator iter = array_value->begin(); - while (iter != array_value->end()) { - ret_string += (*iter)->StringifyImpl(indentDepth1); + case JSONType_Array: { + ret_string = indentDepth ? "[\n" + indentStr1 : "["; + JSONArray::const_iterator iter = array_value->begin(); + while (iter != array_value->end()) { + ret_string += (*iter)->StringifyImpl(indentDepth1); - // Not at the end - add a separator - if (++iter != array_value->end()) - ret_string += ","; + // Not at the end - add a separator + if (++iter != array_value->end()) + ret_string += ","; + } + ret_string += indentDepth ? "\n" + indentStr + "]" : "]"; + break; } - ret_string += indentDepth ? "\n" + indentStr + "]" : "]"; - break; - } - case JSONType_Object: { - ret_string = indentDepth ? "{\n" + indentStr1 : "{"; - JSONObject::const_iterator iter = object_value->begin(); - while (iter != object_value->end()) { - ret_string += StringifyString((*iter).first); - ret_string += ":"; - ret_string += (*iter).second->StringifyImpl(indentDepth1); + case JSONType_Object: { + ret_string = indentDepth ? "{\n" + indentStr1 : "{"; + JSONObject::const_iterator iter = object_value->begin(); + while (iter != object_value->end()) { + ret_string += StringifyString((*iter).first); + ret_string += ":"; + ret_string += (*iter).second->StringifyImpl(indentDepth1); - // Not at the end - add a separator - if (++iter != object_value->end()) - ret_string += ","; + // Not at the end - add a separator + if (++iter != object_value->end()) + ret_string += ","; + } + ret_string += indentDepth ? "\n" + indentStr + "}" : "}"; + break; + } } - ret_string += indentDepth ? "\n" + indentStr + "}" : "}"; - break; - } - } - return ret_string; + return ret_string; } /** @@ -774,53 +829,54 @@ std::string JSONValue::StringifyImpl(size_t const indentDepth) const { * * @return std::string Returns the JSON string */ -std::string JSONValue::StringifyString(const std::string &str) { - std::string str_out = "\""; +std::string JSONValue::StringifyString(const std::string &str) +{ + std::string str_out = "\""; - std::string::const_iterator iter = str.begin(); - while (iter != str.end()) { - char chr = *iter; + std::string::const_iterator iter = str.begin(); + while (iter != str.end()) { + char chr = *iter; + + if (chr == '"' || chr == '\\' || chr == '/') { + str_out += '\\'; + str_out += chr; + } else if (chr == '\b') { + str_out += "\\b"; + } else if (chr == '\f') { + str_out += "\\f"; + } else if (chr == '\n') { + str_out += "\\n"; + } else if (chr == '\r') { + str_out += "\\r"; + } else if (chr == '\t') { + str_out += "\\t"; + } else if (chr < 0x20 || chr == 0x7F) { + char buf[7]; + snprintf(buf, sizeof(buf), "\\u%04x", chr); + str_out += buf; + } else if (chr < 0x80) { + str_out += chr; + } else { + str_out += chr; + size_t remain = str.end() - iter - 1; + if ((chr & 0xE0) == 0xC0 && remain >= 1) { + ++iter; + str_out += *iter; + } else if ((chr & 0xF0) == 0xE0 && remain >= 2) { + str_out += *(++iter); + str_out += *(++iter); + } else if ((chr & 0xF8) == 0xF0 && remain >= 3) { + str_out += *(++iter); + str_out += *(++iter); + str_out += *(++iter); + } + } - if (chr == '"' || chr == '\\' || chr == '/') { - str_out += '\\'; - str_out += chr; - } else if (chr == '\b') { - str_out += "\\b"; - } else if (chr == '\f') { - str_out += "\\f"; - } else if (chr == '\n') { - str_out += "\\n"; - } else if (chr == '\r') { - str_out += "\\r"; - } else if (chr == '\t') { - str_out += "\\t"; - } else if (chr < 0x20 || chr == 0x7F) { - char buf[7]; - snprintf(buf, sizeof(buf), "\\u%04x", chr); - str_out += buf; - } else if (chr < 0x80) { - str_out += chr; - } else { - str_out += chr; - size_t remain = str.end() - iter - 1; - if ((chr & 0xE0) == 0xC0 && remain >= 1) { ++iter; - str_out += *iter; - } else if ((chr & 0xF0) == 0xE0 && remain >= 2) { - str_out += *(++iter); - str_out += *(++iter); - } else if ((chr & 0xF8) == 0xF0 && remain >= 3) { - str_out += *(++iter); - str_out += *(++iter); - str_out += *(++iter); - } } - ++iter; - } - - str_out += "\""; - return str_out; + str_out += "\""; + return str_out; } /** @@ -832,9 +888,10 @@ std::string JSONValue::StringifyString(const std::string &str) { * * @return std::string Returns the string */ -std::string JSONValue::Indent(size_t depth) { - const size_t indent_step = 2; - depth ? --depth : 0; - std::string indentStr(depth * indent_step, ' '); - return indentStr; +std::string JSONValue::Indent(size_t depth) +{ + const size_t indent_step = 2; + depth ? --depth : 0; + std::string indentStr(depth * indent_step, ' '); + return indentStr; } \ No newline at end of file diff --git a/src/serialization/JSONValue.h b/src/serialization/JSONValue.h index 9a947120e..16d53e89f 100644 --- a/src/serialization/JSONValue.h +++ b/src/serialization/JSONValue.h @@ -34,61 +34,62 @@ class JSON; enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_Array, JSONType_Object }; -class JSONValue { - friend class JSON; +class JSONValue +{ + friend class JSON; -public: - JSONValue(/*NULL*/); - explicit JSONValue(const char *m_char_value); - explicit JSONValue(const std::string &m_string_value); - explicit JSONValue(bool m_bool_value); - explicit JSONValue(double m_number_value); - explicit JSONValue(int m_integer_value); - explicit JSONValue(unsigned int m_integer_value); - explicit JSONValue(const JSONArray &m_array_value); - explicit JSONValue(const JSONObject &m_object_value); - explicit JSONValue(const JSONValue &m_source); - ~JSONValue(); + public: + JSONValue(/*NULL*/); + explicit JSONValue(const char *m_char_value); + explicit JSONValue(const std::string &m_string_value); + explicit JSONValue(bool m_bool_value); + explicit JSONValue(double m_number_value); + explicit JSONValue(int m_integer_value); + explicit JSONValue(unsigned int m_integer_value); + explicit JSONValue(const JSONArray &m_array_value); + explicit JSONValue(const JSONObject &m_object_value); + explicit JSONValue(const JSONValue &m_source); + ~JSONValue(); - bool IsNull() const; - bool IsString() const; - bool IsBool() const; - bool IsNumber() const; - bool IsArray() const; - bool IsObject() const; + bool IsNull() const; + bool IsString() const; + bool IsBool() const; + bool IsNumber() const; + bool IsArray() const; + bool IsObject() const; - const std::string &AsString() const; - bool AsBool() const; - double AsNumber() const; - const JSONArray &AsArray() const; - const JSONObject &AsObject() const; + const std::string &AsString() const; + bool AsBool() const; + double AsNumber() const; + const JSONArray &AsArray() const; + const JSONObject &AsObject() const; - std::size_t CountChildren() const; - bool HasChild(std::size_t index) const; - JSONValue *Child(std::size_t index); - bool HasChild(const char *name) const; - JSONValue *Child(const char *name); - std::vector ObjectKeys() const; + std::size_t CountChildren() const; + bool HasChild(std::size_t index) const; + JSONValue *Child(std::size_t index); + bool HasChild(const char *name) const; + JSONValue *Child(const char *name); + std::vector ObjectKeys() const; - std::string Stringify(bool const prettyprint = false) const; + std::string Stringify(bool const prettyprint = false) const; -protected: - static JSONValue *Parse(const char **data); + protected: + static JSONValue *Parse(const char **data); -private: - static std::string StringifyString(const std::string &str); - std::string StringifyImpl(size_t const indentDepth) const; - static std::string Indent(size_t depth); + private: + static std::string StringifyString(const std::string &str); + std::string StringifyImpl(size_t const indentDepth) const; + static std::string Indent(size_t depth); - JSONType type; + JSONType type; - union { - bool bool_value; - double number_value; - std::string *string_value; - JSONArray *array_value; - JSONObject *object_value; - }; + union { + bool bool_value; + double number_value; + std::string *string_value; + JSONArray *array_value; + JSONObject *object_value; + }; }; #endif \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 68f3e3b72..a12972cb0 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -15,447 +15,456 @@ static const char *errStr = "Error decoding proto for %s message!"; -std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) { - // the created jsonObj is immutable after creation, so - // we need to do the heavy lifting before assembling it. - std::string msgType; - JSONObject jsonObj; +std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) +{ + // the created jsonObj is immutable after creation, so + // we need to do the heavy lifting before assembling it. + std::string msgType; + JSONObject jsonObj; - if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - JSONObject msgPayload; - switch (mp->decoded.portnum) { - case meshtastic_PortNum_TEXT_MESSAGE_APP: { - msgType = "text"; - // convert bytes to string - if (shouldLog) - LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + JSONObject msgPayload; + switch (mp->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: { + msgType = "text"; + // convert bytes to string + if (shouldLog) + LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - // check if this is a JSON payload - JSONValue *json_value = JSON::Parse(payloadStr); - if (json_value != NULL) { - if (shouldLog) - LOG_INFO("text message payload is of type json"); + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + // check if this is a JSON payload + JSONValue *json_value = JSON::Parse(payloadStr); + if (json_value != NULL) { + if (shouldLog) + LOG_INFO("text message payload is of type json"); - // if it is, then we can just use the json object - jsonObj["payload"] = json_value; - } else { - // if it isn't, then we need to create a json object - // with the string as the value - if (shouldLog) - LOG_INFO("text message payload is of type plaintext"); + // if it is, then we can just use the json object + jsonObj["payload"] = json_value; + } else { + // if it isn't, then we need to create a json object + // with the string as the value + if (shouldLog) + LOG_INFO("text message payload is of type plaintext"); - msgPayload["text"] = new JSONValue(payloadStr); - jsonObj["payload"] = new JSONValue(msgPayload); - } - break; - } - case meshtastic_PortNum_TELEMETRY_APP: { - msgType = "telemetry"; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - // If battery is present, encode the battery level value - // TODO - Add a condition to send a code for a non-present value - if (decoded->variant.device_metrics.has_battery_level) { - msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); - } - msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); - msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); - msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); - msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); - } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - // Avoid sending 0s for sensors that could be 0 - if (decoded->variant.environment_metrics.has_temperature) { - msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); - } - if (decoded->variant.environment_metrics.has_relative_humidity) { - msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); - } - if (decoded->variant.environment_metrics.has_barometric_pressure) { - msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); - } - if (decoded->variant.environment_metrics.has_gas_resistance) { - msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); - } - if (decoded->variant.environment_metrics.has_voltage) { - msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); - } - if (decoded->variant.environment_metrics.has_current) { - msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); - } - if (decoded->variant.environment_metrics.has_lux) { - msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); - } - if (decoded->variant.environment_metrics.has_white_lux) { - msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); - } - if (decoded->variant.environment_metrics.has_iaq) { - msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); - } - if (decoded->variant.environment_metrics.has_distance) { - msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance); - } - if (decoded->variant.environment_metrics.has_wind_speed) { - msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); - } - if (decoded->variant.environment_metrics.has_wind_direction) { - msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); - } - if (decoded->variant.environment_metrics.has_wind_gust) { - msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); - } - if (decoded->variant.environment_metrics.has_wind_lull) { - msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); - } - if (decoded->variant.environment_metrics.has_radiation) { - msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); - } - if (decoded->variant.environment_metrics.has_ir_lux) { - msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux); - } - if (decoded->variant.environment_metrics.has_uv_lux) { - msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux); - } - if (decoded->variant.environment_metrics.has_weight) { - msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight); - } - if (decoded->variant.environment_metrics.has_rainfall_1h) { - msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h); - } - if (decoded->variant.environment_metrics.has_rainfall_24h) { - msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h); - } - if (decoded->variant.environment_metrics.has_soil_moisture) { - msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture); - } - if (decoded->variant.environment_metrics.has_soil_temperature) { - msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature); - } - } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - if (decoded->variant.air_quality_metrics.has_pm10_standard) { - msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); - } - if (decoded->variant.air_quality_metrics.has_pm25_standard) { - msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); - } - if (decoded->variant.air_quality_metrics.has_pm100_standard) { - msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); - } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); - } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); - } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); - } - } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - if (decoded->variant.power_metrics.has_ch1_voltage) { - msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); - } - if (decoded->variant.power_metrics.has_ch1_current) { - msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); - } - if (decoded->variant.power_metrics.has_ch2_voltage) { - msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); - } - if (decoded->variant.power_metrics.has_ch2_current) { - msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); - } - if (decoded->variant.power_metrics.has_ch3_voltage) { - msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); - } - if (decoded->variant.power_metrics.has_ch3_current) { - msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); - } + msgPayload["text"] = new JSONValue(payloadStr); + jsonObj["payload"] = new JSONValue(msgPayload); + } + break; } - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } - case meshtastic_PortNum_NODEINFO_APP: { - msgType = "nodeinfo"; - meshtastic_User scratch; - meshtastic_User *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { - decoded = &scratch; - msgPayload["id"] = new JSONValue(decoded->id); - msgPayload["longname"] = new JSONValue(decoded->long_name); - msgPayload["shortname"] = new JSONValue(decoded->short_name); - msgPayload["hardware"] = new JSONValue(decoded->hw_model); - msgPayload["role"] = new JSONValue((int)decoded->role); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } - case meshtastic_PortNum_POSITION_APP: { - msgType = "position"; - meshtastic_Position scratch; - meshtastic_Position *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { - decoded = &scratch; - if ((int)decoded->time) { - msgPayload["time"] = new JSONValue((unsigned int)decoded->time); + case meshtastic_PortNum_TELEMETRY_APP: { + msgType = "telemetry"; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); + } + msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); + msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); + msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); + msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); + } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + // Avoid sending 0s for sensors that could be 0 + if (decoded->variant.environment_metrics.has_temperature) { + msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + msgPayload["barometric_pressure"] = + new JSONValue(decoded->variant.environment_metrics.barometric_pressure); + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); + } + if (decoded->variant.environment_metrics.has_voltage) { + msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); + } + if (decoded->variant.environment_metrics.has_current) { + msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); + } + if (decoded->variant.environment_metrics.has_lux) { + msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); + } + if (decoded->variant.environment_metrics.has_white_lux) { + msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); + } + if (decoded->variant.environment_metrics.has_iaq) { + msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); + } + if (decoded->variant.environment_metrics.has_distance) { + msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance); + } + if (decoded->variant.environment_metrics.has_wind_speed) { + msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); + } + if (decoded->variant.environment_metrics.has_wind_direction) { + msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); + } + if (decoded->variant.environment_metrics.has_wind_gust) { + msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); + } + if (decoded->variant.environment_metrics.has_wind_lull) { + msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); + } + if (decoded->variant.environment_metrics.has_radiation) { + msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); + } + if (decoded->variant.environment_metrics.has_ir_lux) { + msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux); + } + if (decoded->variant.environment_metrics.has_uv_lux) { + msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux); + } + if (decoded->variant.environment_metrics.has_weight) { + msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight); + } + if (decoded->variant.environment_metrics.has_rainfall_1h) { + msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h); + } + if (decoded->variant.environment_metrics.has_rainfall_24h) { + msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h); + } + if (decoded->variant.environment_metrics.has_soil_moisture) { + msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture); + } + if (decoded->variant.environment_metrics.has_soil_temperature) { + msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature); + } + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + msgPayload["pm10_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + msgPayload["pm25_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + msgPayload["pm100_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + } + } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + if (decoded->variant.power_metrics.has_ch1_voltage) { + msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); + } + if (decoded->variant.power_metrics.has_ch1_current) { + msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); + } + if (decoded->variant.power_metrics.has_ch2_current) { + msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); + } + if (decoded->variant.power_metrics.has_ch3_current) { + msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); + } + } + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; } - if ((int)decoded->timestamp) { - msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp); + case meshtastic_PortNum_NODEINFO_APP: { + msgType = "nodeinfo"; + meshtastic_User scratch; + meshtastic_User *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { + decoded = &scratch; + msgPayload["id"] = new JSONValue(decoded->id); + msgPayload["longname"] = new JSONValue(decoded->long_name); + msgPayload["shortname"] = new JSONValue(decoded->short_name); + msgPayload["hardware"] = new JSONValue(decoded->hw_model); + msgPayload["role"] = new JSONValue((int)decoded->role); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; } - msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); - msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); - if ((int)decoded->altitude) { - msgPayload["altitude"] = new JSONValue((int)decoded->altitude); + case meshtastic_PortNum_POSITION_APP: { + msgType = "position"; + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + if ((int)decoded->time) { + msgPayload["time"] = new JSONValue((unsigned int)decoded->time); + } + if ((int)decoded->timestamp) { + msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp); + } + msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); + msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); + if ((int)decoded->altitude) { + msgPayload["altitude"] = new JSONValue((int)decoded->altitude); + } + if ((int)decoded->ground_speed) { + msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed); + } + if (int(decoded->ground_track)) { + msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track); + } + if (int(decoded->sats_in_view)) { + msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view); + } + if ((int)decoded->PDOP) { + msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP); + } + if ((int)decoded->HDOP) { + msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP); + } + if ((int)decoded->VDOP) { + msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP); + } + if ((int)decoded->precision_bits) { + msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits); + } + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; } - if ((int)decoded->ground_speed) { - msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed); + case meshtastic_PortNum_WAYPOINT_APP: { + msgType = "waypoint"; + meshtastic_Waypoint scratch; + meshtastic_Waypoint *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + decoded = &scratch; + msgPayload["id"] = new JSONValue((unsigned int)decoded->id); + msgPayload["name"] = new JSONValue(decoded->name); + msgPayload["description"] = new JSONValue(decoded->description); + msgPayload["expire"] = new JSONValue((unsigned int)decoded->expire); + msgPayload["locked_to"] = new JSONValue((unsigned int)decoded->locked_to); + msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); + msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; } - if (int(decoded->ground_track)) { - msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track); + case meshtastic_PortNum_NEIGHBORINFO_APP: { + msgType = "neighborinfo"; + meshtastic_NeighborInfo scratch; + meshtastic_NeighborInfo *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, + &scratch)) { + decoded = &scratch; + msgPayload["node_id"] = new JSONValue((unsigned int)decoded->node_id); + msgPayload["node_broadcast_interval_secs"] = new JSONValue((unsigned int)decoded->node_broadcast_interval_secs); + msgPayload["last_sent_by_id"] = new JSONValue((unsigned int)decoded->last_sent_by_id); + msgPayload["neighbors_count"] = new JSONValue(decoded->neighbors_count); + JSONArray neighbors; + for (uint8_t i = 0; i < decoded->neighbors_count; i++) { + JSONObject neighborObj; + neighborObj["node_id"] = new JSONValue((unsigned int)decoded->neighbors[i].node_id); + neighborObj["snr"] = new JSONValue((int)decoded->neighbors[i].snr); + neighbors.push_back(new JSONValue(neighborObj)); + } + msgPayload["neighbors"] = new JSONValue(neighbors); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; } - if (int(decoded->sats_in_view)) { - msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view); - } - if ((int)decoded->PDOP) { - msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP); - } - if ((int)decoded->HDOP) { - msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP); - } - if ((int)decoded->VDOP) { - msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP); - } - if ((int)decoded->precision_bits) { - msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits); - } - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } - case meshtastic_PortNum_WAYPOINT_APP: { - msgType = "waypoint"; - meshtastic_Waypoint scratch; - meshtastic_Waypoint *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { - decoded = &scratch; - msgPayload["id"] = new JSONValue((unsigned int)decoded->id); - msgPayload["name"] = new JSONValue(decoded->name); - msgPayload["description"] = new JSONValue(decoded->description); - msgPayload["expire"] = new JSONValue((unsigned int)decoded->expire); - msgPayload["locked_to"] = new JSONValue((unsigned int)decoded->locked_to); - msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i); - msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } - case meshtastic_PortNum_NEIGHBORINFO_APP: { - msgType = "neighborinfo"; - meshtastic_NeighborInfo scratch; - meshtastic_NeighborInfo *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, &scratch)) { - decoded = &scratch; - msgPayload["node_id"] = new JSONValue((unsigned int)decoded->node_id); - msgPayload["node_broadcast_interval_secs"] = new JSONValue((unsigned int)decoded->node_broadcast_interval_secs); - msgPayload["last_sent_by_id"] = new JSONValue((unsigned int)decoded->last_sent_by_id); - msgPayload["neighbors_count"] = new JSONValue(decoded->neighbors_count); - JSONArray neighbors; - for (uint8_t i = 0; i < decoded->neighbors_count; i++) { - JSONObject neighborObj; - neighborObj["node_id"] = new JSONValue((unsigned int)decoded->neighbors[i].node_id); - neighborObj["snr"] = new JSONValue((int)decoded->neighbors[i].snr); - neighbors.push_back(new JSONValue(neighborObj)); - } - msgPayload["neighbors"] = new JSONValue(neighbors); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } - case meshtastic_PortNum_TRACEROUTE_APP: { - if (mp->decoded.request_id) { // Only report the traceroute response - msgType = "traceroute"; - meshtastic_RouteDiscovery scratch; - meshtastic_RouteDiscovery *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, &scratch)) { - decoded = &scratch; - JSONArray route; // Route this message took - JSONArray routeBack; // Route this message took back - JSONArray snrTowards; // Snr for forward route - JSONArray snrBack; // Snr for reverse route + case meshtastic_PortNum_TRACEROUTE_APP: { + if (mp->decoded.request_id) { // Only report the traceroute response + msgType = "traceroute"; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, + &scratch)) { + decoded = &scratch; + JSONArray route; // Route this message took + JSONArray routeBack; // Route this message took back + JSONArray snrTowards; // Snr for forward route + JSONArray snrBack; // Snr for reverse route - // Lambda function for adding a long name to the route - auto addToRoute = [](JSONArray *route, NodeNum num) { - char long_name[40] = "Unknown"; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); - bool name_known = node ? node->has_user : false; - if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); - route->push_back(new JSONValue(long_name)); - }; - addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) - for (uint8_t i = 0; i < decoded->route_count; i++) { - addToRoute(&route, decoded->route[i]); - } - addToRoute(&route, mp->from); // Ended at the original destination (source of response) + // Lambda function for adding a long name to the route + auto addToRoute = [](JSONArray *route, NodeNum num) { + char long_name[40] = "Unknown"; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); + bool name_known = node ? node->has_user : false; + if (name_known) + memcpy(long_name, node->user.long_name, sizeof(long_name)); + route->push_back(new JSONValue(long_name)); + }; + addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) + for (uint8_t i = 0; i < decoded->route_count; i++) { + addToRoute(&route, decoded->route[i]); + } + addToRoute(&route, mp->from); // Ended at the original destination (source of response) - addToRoute(&routeBack, mp->from); // Started at the original destination (source of response) - for (uint8_t i = 0; i < decoded->route_back_count; i++) { - addToRoute(&routeBack, decoded->route_back[i]); - } - addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response) + addToRoute(&routeBack, mp->from); // Started at the original destination (source of response) + for (uint8_t i = 0; i < decoded->route_back_count; i++) { + addToRoute(&routeBack, decoded->route_back[i]); + } + addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response) - for (uint8_t i = 0; i < decoded->snr_back_count; i++) { - snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4)); - } + for (uint8_t i = 0; i < decoded->snr_back_count; i++) { + snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4)); + } - for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { - snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4)); - } + for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { + snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4)); + } - msgPayload["route"] = new JSONValue(route); - msgPayload["route_back"] = new JSONValue(routeBack); - msgPayload["snr_back"] = new JSONValue(snrBack); - msgPayload["snr_towards"] = new JSONValue(snrTowards); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); + msgPayload["route"] = new JSONValue(route); + msgPayload["route_back"] = new JSONValue(routeBack); + msgPayload["snr_back"] = new JSONValue(snrBack); + msgPayload["snr_towards"] = new JSONValue(snrTowards); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + } + break; + } + case meshtastic_PortNum_DETECTION_SENSOR_APP: { + msgType = "detection"; + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + msgPayload["text"] = new JSONValue(payloadStr); + jsonObj["payload"] = new JSONValue(msgPayload); + break; } - } - break; - } - case meshtastic_PortNum_DETECTION_SENSOR_APP: { - msgType = "detection"; - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - msgPayload["text"] = new JSONValue(payloadStr); - jsonObj["payload"] = new JSONValue(msgPayload); - break; - } #ifdef ARCH_ESP32 - case meshtastic_PortNum_PAXCOUNTER_APP: { - msgType = "paxcounter"; - meshtastic_Paxcount scratch; - meshtastic_Paxcount *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) { - decoded = &scratch; - msgPayload["wifi_count"] = new JSONValue((unsigned int)decoded->wifi); - msgPayload["ble_count"] = new JSONValue((unsigned int)decoded->ble); - msgPayload["uptime"] = new JSONValue((unsigned int)decoded->uptime); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (shouldLog) { - LOG_ERROR(errStr, msgType.c_str()); - } - break; - } -#endif - case meshtastic_PortNum_REMOTE_HARDWARE_APP: { - meshtastic_HardwareMessage scratch; - meshtastic_HardwareMessage *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, &scratch)) { - decoded = &scratch; - if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { - msgType = "gpios_changed"; - msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); - jsonObj["payload"] = new JSONValue(msgPayload); - } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { - msgType = "gpios_read_reply"; - msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); - msgPayload["gpio_mask"] = new JSONValue((unsigned int)decoded->gpio_mask); - jsonObj["payload"] = new JSONValue(msgPayload); + case meshtastic_PortNum_PAXCOUNTER_APP: { + msgType = "paxcounter"; + meshtastic_Paxcount scratch; + meshtastic_Paxcount *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) { + decoded = &scratch; + msgPayload["wifi_count"] = new JSONValue((unsigned int)decoded->wifi); + msgPayload["ble_count"] = new JSONValue((unsigned int)decoded->ble); + msgPayload["uptime"] = new JSONValue((unsigned int)decoded->uptime); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (shouldLog) { + LOG_ERROR(errStr, msgType.c_str()); + } + break; } - } else if (shouldLog) { - LOG_ERROR(errStr, "RemoteHardware"); - } - break; +#endif + case meshtastic_PortNum_REMOTE_HARDWARE_APP: { + meshtastic_HardwareMessage scratch; + meshtastic_HardwareMessage *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, + &scratch)) { + decoded = &scratch; + if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { + msgType = "gpios_changed"; + msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); + jsonObj["payload"] = new JSONValue(msgPayload); + } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { + msgType = "gpios_read_reply"; + msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value); + msgPayload["gpio_mask"] = new JSONValue((unsigned int)decoded->gpio_mask); + jsonObj["payload"] = new JSONValue(msgPayload); + } + } else if (shouldLog) { + LOG_ERROR(errStr, "RemoteHardware"); + } + break; + } + // add more packet types here if needed + default: + break; + } + } else if (shouldLog) { + LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); } - // add more packet types here if needed - default: - break; + + jsonObj["id"] = new JSONValue((unsigned int)mp->id); + jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); + jsonObj["to"] = new JSONValue((unsigned int)mp->to); + jsonObj["from"] = new JSONValue((unsigned int)mp->from); + jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); + jsonObj["type"] = new JSONValue(msgType.c_str()); + jsonObj["sender"] = new JSONValue(nodeDB->getNodeId().c_str()); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); + if (mp->rx_snr != 0) + jsonObj["snr"] = new JSONValue((float)mp->rx_snr); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); + jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } - } else if (shouldLog) { - LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); - } - jsonObj["id"] = new JSONValue((unsigned int)mp->id); - jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); - jsonObj["to"] = new JSONValue((unsigned int)mp->to); - jsonObj["from"] = new JSONValue((unsigned int)mp->from); - jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); - jsonObj["type"] = new JSONValue(msgType.c_str()); - jsonObj["sender"] = new JSONValue(nodeDB->getNodeId().c_str()); - if (mp->rx_rssi != 0) - jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); - if (mp->rx_snr != 0) - jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); - jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); - } + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObj); + std::string jsonStr = value->Stringify(); - // serialize and write it to the stream - JSONValue *value = new JSONValue(jsonObj); - std::string jsonStr = value->Stringify(); + if (shouldLog) + LOG_INFO("serialized json message: %s", jsonStr.c_str()); - if (shouldLog) - LOG_INFO("serialized json message: %s", jsonStr.c_str()); - - delete value; - return jsonStr; + delete value; + return jsonStr; } -std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) { - JSONObject jsonObj; +std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) +{ + JSONObject jsonObj; - jsonObj["id"] = new JSONValue((unsigned int)mp->id); - jsonObj["time_ms"] = new JSONValue((double)millis()); - jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); - jsonObj["to"] = new JSONValue((unsigned int)mp->to); - jsonObj["from"] = new JSONValue((unsigned int)mp->from); - jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); - jsonObj["want_ack"] = new JSONValue(mp->want_ack); + jsonObj["id"] = new JSONValue((unsigned int)mp->id); + jsonObj["time_ms"] = new JSONValue((double)millis()); + jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time); + jsonObj["to"] = new JSONValue((unsigned int)mp->to); + jsonObj["from"] = new JSONValue((unsigned int)mp->from); + jsonObj["channel"] = new JSONValue((unsigned int)mp->channel); + jsonObj["want_ack"] = new JSONValue(mp->want_ack); - if (mp->rx_rssi != 0) - jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); - if (mp->rx_snr != 0) - jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); - jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); - } - jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); - auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); - jsonObj["bytes"] = new JSONValue(encryptedStr.c_str()); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); + if (mp->rx_snr != 0) + jsonObj["snr"] = new JSONValue((float)mp->rx_snr); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); + jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); + } + jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); + auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); + jsonObj["bytes"] = new JSONValue(encryptedStr.c_str()); - // serialize and write it to the stream - JSONValue *value = new JSONValue(jsonObj); - std::string jsonStr = value->Stringify(); + // serialize and write it to the stream + JSONValue *value = new JSONValue(jsonObj); + std::string jsonStr = value->Stringify(); - delete value; - return jsonStr; + delete value; + return jsonStr; } #endif \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer.h b/src/serialization/MeshPacketSerializer.h index 929b01873..03860ab35 100644 --- a/src/serialization/MeshPacketSerializer.h +++ b/src/serialization/MeshPacketSerializer.h @@ -3,19 +3,21 @@ static const char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; -class MeshPacketSerializer { -public: - static std::string JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog = true); - static std::string JsonSerializeEncrypted(const meshtastic_MeshPacket *mp); +class MeshPacketSerializer +{ + public: + static std::string JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog = true); + static std::string JsonSerializeEncrypted(const meshtastic_MeshPacket *mp); -private: - static std::string bytesToHex(const uint8_t *bytes, int len) { - std::string result = ""; - for (int i = 0; i < len; ++i) { - char const byte = bytes[i]; - result += hexChars[(byte & 0xF0) >> 4]; - result += hexChars[(byte & 0x0F) >> 0]; + private: + static std::string bytesToHex(const uint8_t *bytes, int len) + { + std::string result = ""; + for (int i = 0; i < len; ++i) { + char const byte = bytes[i]; + result += hexChars[(byte & 0xF0) >> 4]; + result += hexChars[(byte & 0x0F) >> 0]; + } + return result; } - return result; - } }; \ No newline at end of file diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 5329a7b09..41f505b94 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -14,394 +14,399 @@ StaticJsonDocument<1024> jsonObj; StaticJsonDocument<1024> arrayObj; -std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) { - // the created jsonObj is immutable after creation, so - // we need to do the heavy lifting before assembling it. - std::string msgType; - jsonObj.clear(); - arrayObj.clear(); +std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) +{ + // the created jsonObj is immutable after creation, so + // we need to do the heavy lifting before assembling it. + std::string msgType; + jsonObj.clear(); + arrayObj.clear(); - if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - switch (mp->decoded.portnum) { - case meshtastic_PortNum_TEXT_MESSAGE_APP: { - msgType = "text"; - // convert bytes to string - if (shouldLog) - LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); + if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + switch (mp->decoded.portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: { + msgType = "text"; + // convert bytes to string + if (shouldLog) + LOG_DEBUG("got text message of size %u", mp->decoded.payload.size); - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - // check if this is a JSON payload - StaticJsonDocument<512> text_doc; - DeserializationError error = deserializeJson(text_doc, payloadStr); - if (error) { - // if it isn't, then we need to create a json object - // with the string as the value - if (shouldLog) - LOG_INFO("text message payload is of type plaintext"); - jsonObj["payload"]["text"] = payloadStr; - } else { - // if it is, then we can just use the json object - if (shouldLog) - LOG_INFO("text message payload is of type json"); - jsonObj["payload"] = text_doc; - } - break; - } - case meshtastic_PortNum_TELEMETRY_APP: { - msgType = "telemetry"; - meshtastic_Telemetry scratch; - meshtastic_Telemetry *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { - decoded = &scratch; - if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - // If battery is present, encode the battery level value - // TODO - Add a condition to send a code for a non-present value - if (decoded->variant.device_metrics.has_battery_level) { - jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; - } - jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; - jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; - jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; - jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; - } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - if (decoded->variant.environment_metrics.has_temperature) { - jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; - } - if (decoded->variant.environment_metrics.has_relative_humidity) { - jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; - } - if (decoded->variant.environment_metrics.has_barometric_pressure) { - jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; - } - if (decoded->variant.environment_metrics.has_gas_resistance) { - jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; - } - if (decoded->variant.environment_metrics.has_voltage) { - jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; - } - if (decoded->variant.environment_metrics.has_current) { - jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; - } - if (decoded->variant.environment_metrics.has_lux) { - jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; - } - if (decoded->variant.environment_metrics.has_white_lux) { - jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; - } - if (decoded->variant.environment_metrics.has_iaq) { - jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; - } - if (decoded->variant.environment_metrics.has_wind_speed) { - jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; - } - if (decoded->variant.environment_metrics.has_wind_direction) { - jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; - } - if (decoded->variant.environment_metrics.has_wind_gust) { - jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; - } - if (decoded->variant.environment_metrics.has_wind_lull) { - jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; - } - if (decoded->variant.environment_metrics.has_radiation) { - jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; - } - } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - if (decoded->variant.air_quality_metrics.has_pm10_standard) { - jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; - } - if (decoded->variant.air_quality_metrics.has_pm25_standard) { - jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; - } - if (decoded->variant.air_quality_metrics.has_pm100_standard) { - jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; - } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; - } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; - } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; - } - } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - if (decoded->variant.power_metrics.has_ch1_voltage) { - jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; - } - if (decoded->variant.power_metrics.has_ch1_current) { - jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; - } - if (decoded->variant.power_metrics.has_ch2_voltage) { - jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; - } - if (decoded->variant.power_metrics.has_ch2_current) { - jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; - } - if (decoded->variant.power_metrics.has_ch3_voltage) { - jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; - } - if (decoded->variant.power_metrics.has_ch3_current) { - jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; - } + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + // check if this is a JSON payload + StaticJsonDocument<512> text_doc; + DeserializationError error = deserializeJson(text_doc, payloadStr); + if (error) { + // if it isn't, then we need to create a json object + // with the string as the value + if (shouldLog) + LOG_INFO("text message payload is of type plaintext"); + jsonObj["payload"]["text"] = payloadStr; + } else { + // if it is, then we can just use the json object + if (shouldLog) + LOG_INFO("text message payload is of type json"); + jsonObj["payload"] = text_doc; + } + break; } - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for telemetry message!"); + case meshtastic_PortNum_TELEMETRY_APP: { + msgType = "telemetry"; + meshtastic_Telemetry scratch; + meshtastic_Telemetry *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { + decoded = &scratch; + if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; + } + jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; + jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; + jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; + jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; + } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { + if (decoded->variant.environment_metrics.has_temperature) { + jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; + } + if (decoded->variant.environment_metrics.has_voltage) { + jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; + } + if (decoded->variant.environment_metrics.has_current) { + jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; + } + if (decoded->variant.environment_metrics.has_lux) { + jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; + } + if (decoded->variant.environment_metrics.has_white_lux) { + jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; + } + if (decoded->variant.environment_metrics.has_iaq) { + jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; + } + if (decoded->variant.environment_metrics.has_wind_speed) { + jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; + } + if (decoded->variant.environment_metrics.has_wind_direction) { + jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; + } + if (decoded->variant.environment_metrics.has_wind_gust) { + jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; + } + if (decoded->variant.environment_metrics.has_wind_lull) { + jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; + } + if (decoded->variant.environment_metrics.has_radiation) { + jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; + } + } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + } + } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { + if (decoded->variant.power_metrics.has_ch1_voltage) { + jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; + } + if (decoded->variant.power_metrics.has_ch1_current) { + jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; + } + if (decoded->variant.power_metrics.has_ch2_current) { + jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; + } + if (decoded->variant.power_metrics.has_ch3_current) { + jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + } + } + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for telemetry message!"); + return ""; + } + break; + } + case meshtastic_PortNum_NODEINFO_APP: { + msgType = "nodeinfo"; + meshtastic_User scratch; + meshtastic_User *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { + decoded = &scratch; + jsonObj["payload"]["id"] = decoded->id; + jsonObj["payload"]["longname"] = decoded->long_name; + jsonObj["payload"]["shortname"] = decoded->short_name; + jsonObj["payload"]["hardware"] = decoded->hw_model; + jsonObj["payload"]["role"] = (int)decoded->role; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for nodeinfo message!"); + return ""; + } + break; + } + case meshtastic_PortNum_POSITION_APP: { + msgType = "position"; + meshtastic_Position scratch; + meshtastic_Position *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { + decoded = &scratch; + if ((int)decoded->time) { + jsonObj["payload"]["time"] = (unsigned int)decoded->time; + } + if ((int)decoded->timestamp) { + jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp; + } + jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; + jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; + if ((int)decoded->altitude) { + jsonObj["payload"]["altitude"] = (int)decoded->altitude; + } + if ((int)decoded->ground_speed) { + jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed; + } + if (int(decoded->ground_track)) { + jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track; + } + if (int(decoded->sats_in_view)) { + jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view; + } + if ((int)decoded->PDOP) { + jsonObj["payload"]["PDOP"] = (int)decoded->PDOP; + } + if ((int)decoded->HDOP) { + jsonObj["payload"]["HDOP"] = (int)decoded->HDOP; + } + if ((int)decoded->VDOP) { + jsonObj["payload"]["VDOP"] = (int)decoded->VDOP; + } + if ((int)decoded->precision_bits) { + jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits; + } + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for position message!"); + return ""; + } + break; + } + case meshtastic_PortNum_WAYPOINT_APP: { + msgType = "position"; + meshtastic_Waypoint scratch; + meshtastic_Waypoint *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { + decoded = &scratch; + jsonObj["payload"]["id"] = (unsigned int)decoded->id; + jsonObj["payload"]["name"] = decoded->name; + jsonObj["payload"]["description"] = decoded->description; + jsonObj["payload"]["expire"] = (unsigned int)decoded->expire; + jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to; + jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; + jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for position message!"); + return ""; + } + break; + } + case meshtastic_PortNum_NEIGHBORINFO_APP: { + msgType = "neighborinfo"; + meshtastic_NeighborInfo scratch; + meshtastic_NeighborInfo *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, + &scratch)) { + decoded = &scratch; + jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id; + jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs; + jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id; + jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count; + + JsonObject neighbors_obj = arrayObj.to(); + JsonArray neighbors = neighbors_obj.createNestedArray("neighbors"); + JsonObject neighbors_0 = neighbors.createNestedObject(); + + for (uint8_t i = 0; i < decoded->neighbors_count; i++) { + neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id; + neighbors_0["snr"] = (int)decoded->neighbors[i].snr; + neighbors[i + 1] = neighbors_0; + neighbors_0.clear(); + } + neighbors.remove(0); + jsonObj["payload"]["neighbors"] = neighbors; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for neighborinfo message!"); + return ""; + } + break; + } + case meshtastic_PortNum_TRACEROUTE_APP: { + if (mp->decoded.request_id) { // Only report the traceroute response + msgType = "traceroute"; + meshtastic_RouteDiscovery scratch; + meshtastic_RouteDiscovery *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, + &scratch)) { + decoded = &scratch; + JsonArray route = arrayObj.createNestedArray("route"); + + auto addToRoute = [](JsonArray *route, NodeNum num) { + char long_name[40] = "Unknown"; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); + bool name_known = node ? node->has_user : false; + if (name_known) + memcpy(long_name, node->user.long_name, sizeof(long_name)); + route->add(long_name); + }; + + addToRoute(&route, mp->to); // route.add(mp->to); + for (uint8_t i = 0; i < decoded->route_count; i++) { + addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]); + } + addToRoute(&route, + mp->from); // route.add(mp->from); // Ended at the original destination (source of response) + + jsonObj["payload"]["route"] = route; + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for traceroute message!"); + return ""; + } + } else { + LOG_WARN("Traceroute response not reported"); + return ""; + } + break; + } + case meshtastic_PortNum_DETECTION_SENSOR_APP: { + msgType = "detection"; + char payloadStr[(mp->decoded.payload.size) + 1]; + memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); + payloadStr[mp->decoded.payload.size] = 0; // null terminated string + jsonObj["payload"]["text"] = payloadStr; + break; + } + case meshtastic_PortNum_REMOTE_HARDWARE_APP: { + meshtastic_HardwareMessage scratch; + meshtastic_HardwareMessage *decoded = NULL; + memset(&scratch, 0, sizeof(scratch)); + if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, + &scratch)) { + decoded = &scratch; + if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { + msgType = "gpios_changed"; + jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; + } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { + msgType = "gpios_read_reply"; + jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; + jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask; + } + } else if (shouldLog) { + LOG_ERROR("Error decoding proto for RemoteHardware message!"); + return ""; + } + break; + } + // add more packet types here if needed + default: + LOG_WARN("Unsupported packet type %d", mp->decoded.portnum); + return ""; + break; + } + } else if (shouldLog) { + LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); return ""; - } - break; } - case meshtastic_PortNum_NODEINFO_APP: { - msgType = "nodeinfo"; - meshtastic_User scratch; - meshtastic_User *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { - decoded = &scratch; - jsonObj["payload"]["id"] = decoded->id; - jsonObj["payload"]["longname"] = decoded->long_name; - jsonObj["payload"]["shortname"] = decoded->short_name; - jsonObj["payload"]["hardware"] = decoded->hw_model; - jsonObj["payload"]["role"] = (int)decoded->role; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for nodeinfo message!"); - return ""; - } - break; + + jsonObj["id"] = (unsigned int)mp->id; + jsonObj["timestamp"] = (unsigned int)mp->rx_time; + jsonObj["to"] = (unsigned int)mp->to; + jsonObj["from"] = (unsigned int)mp->from; + jsonObj["channel"] = (unsigned int)mp->channel; + jsonObj["type"] = msgType.c_str(); + jsonObj["sender"] = nodeDB->getNodeId().c_str(); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); + jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } - case meshtastic_PortNum_POSITION_APP: { - msgType = "position"; - meshtastic_Position scratch; - meshtastic_Position *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { - decoded = &scratch; - if ((int)decoded->time) { - jsonObj["payload"]["time"] = (unsigned int)decoded->time; - } - if ((int)decoded->timestamp) { - jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp; - } - jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; - jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; - if ((int)decoded->altitude) { - jsonObj["payload"]["altitude"] = (int)decoded->altitude; - } - if ((int)decoded->ground_speed) { - jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed; - } - if (int(decoded->ground_track)) { - jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track; - } - if (int(decoded->sats_in_view)) { - jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view; - } - if ((int)decoded->PDOP) { - jsonObj["payload"]["PDOP"] = (int)decoded->PDOP; - } - if ((int)decoded->HDOP) { - jsonObj["payload"]["HDOP"] = (int)decoded->HDOP; - } - if ((int)decoded->VDOP) { - jsonObj["payload"]["VDOP"] = (int)decoded->VDOP; - } - if ((int)decoded->precision_bits) { - jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits; - } - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for position message!"); - return ""; - } - break; - } - case meshtastic_PortNum_WAYPOINT_APP: { - msgType = "position"; - meshtastic_Waypoint scratch; - meshtastic_Waypoint *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { - decoded = &scratch; - jsonObj["payload"]["id"] = (unsigned int)decoded->id; - jsonObj["payload"]["name"] = decoded->name; - jsonObj["payload"]["description"] = decoded->description; - jsonObj["payload"]["expire"] = (unsigned int)decoded->expire; - jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to; - jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; - jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for position message!"); - return ""; - } - break; - } - case meshtastic_PortNum_NEIGHBORINFO_APP: { - msgType = "neighborinfo"; - meshtastic_NeighborInfo scratch; - meshtastic_NeighborInfo *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, &scratch)) { - decoded = &scratch; - jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id; - jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs; - jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id; - jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count; - JsonObject neighbors_obj = arrayObj.to(); - JsonArray neighbors = neighbors_obj.createNestedArray("neighbors"); - JsonObject neighbors_0 = neighbors.createNestedObject(); + // serialize and write it to the stream - for (uint8_t i = 0; i < decoded->neighbors_count; i++) { - neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id; - neighbors_0["snr"] = (int)decoded->neighbors[i].snr; - neighbors[i + 1] = neighbors_0; - neighbors_0.clear(); - } - neighbors.remove(0); - jsonObj["payload"]["neighbors"] = neighbors; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for neighborinfo message!"); - return ""; - } - break; - } - case meshtastic_PortNum_TRACEROUTE_APP: { - if (mp->decoded.request_id) { // Only report the traceroute response - msgType = "traceroute"; - meshtastic_RouteDiscovery scratch; - meshtastic_RouteDiscovery *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, &scratch)) { - decoded = &scratch; - JsonArray route = arrayObj.createNestedArray("route"); + // Serial.printf("serialized json message: \r"); + // serializeJson(jsonObj, Serial); + // Serial.println(""); - auto addToRoute = [](JsonArray *route, NodeNum num) { - char long_name[40] = "Unknown"; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); - bool name_known = node ? node->has_user : false; - if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); - route->add(long_name); - }; + std::string jsonStr = ""; + serializeJson(jsonObj, jsonStr); - addToRoute(&route, mp->to); // route.add(mp->to); - for (uint8_t i = 0; i < decoded->route_count; i++) { - addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]); - } - addToRoute(&route, - mp->from); // route.add(mp->from); // Ended at the original destination (source of response) + if (shouldLog) + LOG_INFO("serialized json message: %s", jsonStr.c_str()); - jsonObj["payload"]["route"] = route; - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for traceroute message!"); - return ""; - } - } else { - LOG_WARN("Traceroute response not reported"); - return ""; - } - break; - } - case meshtastic_PortNum_DETECTION_SENSOR_APP: { - msgType = "detection"; - char payloadStr[(mp->decoded.payload.size) + 1]; - memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); - payloadStr[mp->decoded.payload.size] = 0; // null terminated string - jsonObj["payload"]["text"] = payloadStr; - break; - } - case meshtastic_PortNum_REMOTE_HARDWARE_APP: { - meshtastic_HardwareMessage scratch; - meshtastic_HardwareMessage *decoded = NULL; - memset(&scratch, 0, sizeof(scratch)); - if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, &scratch)) { - decoded = &scratch; - if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { - msgType = "gpios_changed"; - jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; - } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { - msgType = "gpios_read_reply"; - jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; - jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask; - } - } else if (shouldLog) { - LOG_ERROR("Error decoding proto for RemoteHardware message!"); - return ""; - } - break; - } - // add more packet types here if needed - default: - LOG_WARN("Unsupported packet type %d", mp->decoded.portnum); - return ""; - break; - } - } else if (shouldLog) { - LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); - return ""; - } - - jsonObj["id"] = (unsigned int)mp->id; - jsonObj["timestamp"] = (unsigned int)mp->rx_time; - jsonObj["to"] = (unsigned int)mp->to; - jsonObj["from"] = (unsigned int)mp->from; - jsonObj["channel"] = (unsigned int)mp->channel; - jsonObj["type"] = msgType.c_str(); - jsonObj["sender"] = nodeDB->getNodeId().c_str(); - if (mp->rx_rssi != 0) - jsonObj["rssi"] = (int)mp->rx_rssi; - if (mp->rx_snr != 0) - jsonObj["snr"] = (float)mp->rx_snr; - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = (unsigned int)(hopsAway); - jsonObj["hop_start"] = (unsigned int)(mp->hop_start); - } - - // serialize and write it to the stream - - // Serial.printf("serialized json message: \r"); - // serializeJson(jsonObj, Serial); - // Serial.println(""); - - std::string jsonStr = ""; - serializeJson(jsonObj, jsonStr); - - if (shouldLog) - LOG_INFO("serialized json message: %s", jsonStr.c_str()); - - return jsonStr; + return jsonStr; } -std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) { - jsonObj.clear(); - jsonObj["id"] = (unsigned int)mp->id; - jsonObj["time_ms"] = (double)millis(); - jsonObj["timestamp"] = (unsigned int)mp->rx_time; - jsonObj["to"] = (unsigned int)mp->to; - jsonObj["from"] = (unsigned int)mp->from; - jsonObj["channel"] = (unsigned int)mp->channel; - jsonObj["want_ack"] = mp->want_ack; +std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) +{ + jsonObj.clear(); + jsonObj["id"] = (unsigned int)mp->id; + jsonObj["time_ms"] = (double)millis(); + jsonObj["timestamp"] = (unsigned int)mp->rx_time; + jsonObj["to"] = (unsigned int)mp->to; + jsonObj["from"] = (unsigned int)mp->from; + jsonObj["channel"] = (unsigned int)mp->channel; + jsonObj["want_ack"] = mp->want_ack; - if (mp->rx_rssi != 0) - jsonObj["rssi"] = (int)mp->rx_rssi; - if (mp->rx_snr != 0) - jsonObj["snr"] = (float)mp->rx_snr; - const int8_t hopsAway = getHopsAway(*mp); - if (hopsAway >= 0) { - jsonObj["hops_away"] = (unsigned int)(hopsAway); - jsonObj["hop_start"] = (unsigned int)(mp->hop_start); - } - jsonObj["size"] = (unsigned int)mp->encrypted.size; - auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); - jsonObj["bytes"] = encryptedStr.c_str(); + if (mp->rx_rssi != 0) + jsonObj["rssi"] = (int)mp->rx_rssi; + if (mp->rx_snr != 0) + jsonObj["snr"] = (float)mp->rx_snr; + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); + jsonObj["hop_start"] = (unsigned int)(mp->hop_start); + } + jsonObj["size"] = (unsigned int)mp->encrypted.size; + auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); + jsonObj["bytes"] = encryptedStr.c_str(); - // serialize and write it to the stream - std::string jsonStr = ""; - serializeJson(jsonObj, jsonStr); + // serialize and write it to the stream + std::string jsonStr = ""; + serializeJson(jsonObj, jsonStr); - return jsonStr; + return jsonStr; } #endif \ No newline at end of file diff --git a/src/serialization/cobs.cpp b/src/serialization/cobs.cpp index 6344419fe..afb868f50 100644 --- a/src/serialization/cobs.cpp +++ b/src/serialization/cobs.cpp @@ -3,125 +3,127 @@ #ifdef SENSECAP_INDICATOR -cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) { +cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) +{ - cobs_encode_result result = {0, COBS_ENCODE_OK}; + cobs_encode_result result = {0, COBS_ENCODE_OK}; - if (!dst_buf_ptr || !src_ptr) { - result.status = COBS_ENCODE_NULL_POINTER; - return result; - } - - const uint8_t *src_read_ptr = src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = dst_buf_ptr; - uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_code_write_ptr = dst_buf_ptr; - uint8_t *dst_write_ptr = dst_code_write_ptr + 1; - uint8_t search_len = 1; - - if (src_len != 0) { - for (;;) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); - break; - } - - uint8_t src_byte = *src_read_ptr++; - if (src_byte == 0) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - if (src_read_ptr >= src_end_ptr) { - break; - } - } else { - *dst_write_ptr++ = src_byte; - search_len++; - if (src_read_ptr >= src_end_ptr) { - break; - } - if (search_len == 0xFF) { - *dst_code_write_ptr = search_len; - dst_code_write_ptr = dst_write_ptr++; - search_len = 1; - } - } + if (!dst_buf_ptr || !src_ptr) { + result.status = COBS_ENCODE_NULL_POINTER; + return result; } - } - if (dst_code_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); - dst_write_ptr = dst_buf_end_ptr; - } else { - *dst_code_write_ptr = search_len; - } + const uint8_t *src_read_ptr = src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = dst_buf_ptr; + uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_code_write_ptr = dst_buf_ptr; + uint8_t *dst_write_ptr = dst_code_write_ptr + 1; + uint8_t search_len = 1; - result.out_len = dst_write_ptr - dst_buf_start_ptr; + if (src_len != 0) { + for (;;) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); + break; + } - return result; + uint8_t src_byte = *src_read_ptr++; + if (src_byte == 0) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + if (src_read_ptr >= src_end_ptr) { + break; + } + } else { + *dst_write_ptr++ = src_byte; + search_len++; + if (src_read_ptr >= src_end_ptr) { + break; + } + if (search_len == 0xFF) { + *dst_code_write_ptr = search_len; + dst_code_write_ptr = dst_write_ptr++; + search_len = 1; + } + } + } + } + + if (dst_code_write_ptr >= dst_buf_end_ptr) { + result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); + dst_write_ptr = dst_buf_end_ptr; + } else { + *dst_code_write_ptr = search_len; + } + + result.out_len = dst_write_ptr - dst_buf_start_ptr; + + return result; } -cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) { - cobs_decode_result result = {0, COBS_DECODE_OK}; +cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) +{ + cobs_decode_result result = {0, COBS_DECODE_OK}; - if (!dst_buf_ptr || !src_ptr) { - result.status = COBS_DECODE_NULL_POINTER; - return result; - } - - const uint8_t *src_read_ptr = src_ptr; - const uint8_t *src_end_ptr = src_read_ptr + src_len; - uint8_t *dst_buf_start_ptr = dst_buf_ptr; - const uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; - uint8_t *dst_write_ptr = dst_buf_ptr; - - if (src_len != 0) { - for (;;) { - uint8_t len_code = *src_read_ptr++; - if (len_code == 0) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); - break; - } - len_code--; - - size_t remaining_bytes = src_end_ptr - src_read_ptr; - if (len_code > remaining_bytes) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_INPUT_TOO_SHORT); - len_code = remaining_bytes; - } - - remaining_bytes = dst_buf_end_ptr - dst_write_ptr; - if (len_code > remaining_bytes) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); - len_code = remaining_bytes; - } - - for (uint8_t i = len_code; i != 0; i--) { - uint8_t src_byte = *src_read_ptr++; - if (src_byte == 0) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); - } - *dst_write_ptr++ = src_byte; - } - - if (src_read_ptr >= src_end_ptr) { - break; - } - - if (len_code != 0xFE) { - if (dst_write_ptr >= dst_buf_end_ptr) { - result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); - break; - } - *dst_write_ptr++ = 0; - } + if (!dst_buf_ptr || !src_ptr) { + result.status = COBS_DECODE_NULL_POINTER; + return result; } - } - result.out_len = dst_write_ptr - dst_buf_start_ptr; + const uint8_t *src_read_ptr = src_ptr; + const uint8_t *src_end_ptr = src_read_ptr + src_len; + uint8_t *dst_buf_start_ptr = dst_buf_ptr; + const uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; + uint8_t *dst_write_ptr = dst_buf_ptr; - return result; + if (src_len != 0) { + for (;;) { + uint8_t len_code = *src_read_ptr++; + if (len_code == 0) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); + break; + } + len_code--; + + size_t remaining_bytes = src_end_ptr - src_read_ptr; + if (len_code > remaining_bytes) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_INPUT_TOO_SHORT); + len_code = remaining_bytes; + } + + remaining_bytes = dst_buf_end_ptr - dst_write_ptr; + if (len_code > remaining_bytes) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); + len_code = remaining_bytes; + } + + for (uint8_t i = len_code; i != 0; i--) { + uint8_t src_byte = *src_read_ptr++; + if (src_byte == 0) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); + } + *dst_write_ptr++ = src_byte; + } + + if (src_read_ptr >= src_end_ptr) { + break; + } + + if (len_code != 0xFE) { + if (dst_write_ptr >= dst_buf_end_ptr) { + result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); + break; + } + *dst_write_ptr++ = 0; + } + } + } + + result.out_len = dst_write_ptr - dst_buf_start_ptr; + + return result; } #endif \ No newline at end of file diff --git a/src/serialization/cobs.h b/src/serialization/cobs.h index 8d3370897..f95e61f62 100644 --- a/src/serialization/cobs.h +++ b/src/serialization/cobs.h @@ -12,24 +12,28 @@ #define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN)-1u)) #define COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u) / 254u) -typedef enum { COBS_ENCODE_OK = 0x00, COBS_ENCODE_NULL_POINTER = 0x01, COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 } cobs_encode_status; +typedef enum { + COBS_ENCODE_OK = 0x00, + COBS_ENCODE_NULL_POINTER = 0x01, + COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 +} cobs_encode_status; typedef struct { - size_t out_len; - cobs_encode_status status; + size_t out_len; + cobs_encode_status status; } cobs_encode_result; typedef enum { - COBS_DECODE_OK = 0x00, - COBS_DECODE_NULL_POINTER = 0x01, - COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, - COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, - COBS_DECODE_INPUT_TOO_SHORT = 0x08 + COBS_DECODE_OK = 0x00, + COBS_DECODE_NULL_POINTER = 0x01, + COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, + COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, + COBS_DECODE_INPUT_TOO_SHORT = 0x08 } cobs_decode_status; typedef struct { - size_t out_len; - cobs_decode_status status; + size_t out_len; + cobs_decode_status status; } cobs_decode_result; #ifdef __cplusplus diff --git a/src/sleep.cpp b/src/sleep.cpp index 9b6fa4873..756582c74 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -76,288 +76,294 @@ RTC_DATA_ATTR int bootCount = 0; * We leave CPU at full speed during init, but once loop is called switch to low speed (for a 50% power savings) * */ -void setCPUFast(bool on) { +void setCPUFast(bool on) +{ #if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT - if (isWifiAvailable()) { - /* - * - * There's a newly introduced bug in the espressif framework where WiFi is - * unstable when the frequency is less than 240MHz. - * - * This mostly impacts WiFi AP mode but we'll bump the frequency for - * all WiFi use cases. - * (Added: Dec 23, 2021 by Jm Casler) - */ + if (isWifiAvailable()) { + /* + * + * There's a newly introduced bug in the espressif framework where WiFi is + * unstable when the frequency is less than 240MHz. + * + * This mostly impacts WiFi AP mode but we'll bump the frequency for + * all WiFi use cases. + * (Added: Dec 23, 2021 by Jm Casler) + */ #ifndef CONFIG_IDF_TARGET_ESP32C3 - LOG_DEBUG("Set CPU to 240MHz because WiFi is in use"); - setCpuFrequencyMhz(240); + LOG_DEBUG("Set CPU to 240MHz because WiFi is in use"); + setCpuFrequencyMhz(240); #endif - return; - } + return; + } // The Heltec LORA32 V1 runs at 26 MHz base frequency and doesn't react well to switching to 80 MHz... #if !defined(ARDUINO_HELTEC_WIFI_LORA_32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - setCpuFrequencyMhz(on ? 240 : 80); + setCpuFrequencyMhz(on ? 240 : 80); #endif #endif } // Perform power on init that we do on each wake from deep sleep -void initDeepSleep() { +void initDeepSleep() +{ #ifdef ARCH_ESP32 - bootCount++; - const char *reason; - wakeCause = esp_sleep_get_wakeup_cause(); + bootCount++; + const char *reason; + wakeCause = esp_sleep_get_wakeup_cause(); - switch (wakeCause) { - case ESP_SLEEP_WAKEUP_EXT0: - reason = "ext0 RTC_IO"; - break; - case ESP_SLEEP_WAKEUP_EXT1: - reason = "ext1 RTC_CNTL"; - break; - case ESP_SLEEP_WAKEUP_TIMER: - reason = "timer"; - break; - case ESP_SLEEP_WAKEUP_TOUCHPAD: - reason = "touchpad"; - break; - case ESP_SLEEP_WAKEUP_ULP: - reason = "ULP program"; - break; - default: - reason = "reset"; - break; - } - /* - Not using yet because we are using wake on all buttons being low + switch (wakeCause) { + case ESP_SLEEP_WAKEUP_EXT0: + reason = "ext0 RTC_IO"; + break; + case ESP_SLEEP_WAKEUP_EXT1: + reason = "ext1 RTC_CNTL"; + break; + case ESP_SLEEP_WAKEUP_TIMER: + reason = "timer"; + break; + case ESP_SLEEP_WAKEUP_TOUCHPAD: + reason = "touchpad"; + break; + case ESP_SLEEP_WAKEUP_ULP: + reason = "ULP program"; + break; + default: + reason = "reset"; + break; + } + /* + Not using yet because we are using wake on all buttons being low - wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these buttons is set it was the reason we woke - if (wakeCause == ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all buttons rule for waking' - to support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0]; - */ + wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these buttons is set it was the reason we woke + if (wakeCause == ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all buttons rule for waking' to + support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0]; + */ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) - // If we booted because our timer ran out or the user pressed reset, send those as fake events - RESET_REASON hwReason = rtc_get_reset_reason(0); + // If we booted because our timer ran out or the user pressed reset, send those as fake events + RESET_REASON hwReason = rtc_get_reset_reason(0); - if (hwReason == RTCWDT_BROWN_OUT_RESET) - reason = "brownout"; + if (hwReason == RTCWDT_BROWN_OUT_RESET) + reason = "brownout"; - if (hwReason == TG0WDT_SYS_RESET) - reason = "taskWatchdog"; + if (hwReason == TG0WDT_SYS_RESET) + reason = "taskWatchdog"; - if (hwReason == TG1WDT_SYS_RESET) - reason = "intWatchdog"; + if (hwReason == TG1WDT_SYS_RESET) + reason = "intWatchdog"; - LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason); + LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason); #endif #if SOC_RTCIO_HOLD_SUPPORTED - // If waking from sleep, release any and all RTC GPIOs - if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { - LOG_DEBUG("Disable any holds on RTC IO pads"); - for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { - if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) - rtc_gpio_hold_dis((gpio_num_t)i); + // If waking from sleep, release any and all RTC GPIOs + if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { + LOG_DEBUG("Disable any holds on RTC IO pads"); + for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { + if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) + rtc_gpio_hold_dis((gpio_num_t)i); - // ESP32 (original) - else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) - gpio_hold_dis((gpio_num_t)i); + // ESP32 (original) + else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) + gpio_hold_dis((gpio_num_t)i); + } } - } #endif #endif } -bool doPreflightSleep() { - if (preflightSleep.notifyObservers(NULL) != 0) - return false; // vetoed - else - return true; +bool doPreflightSleep() +{ + if (preflightSleep.notifyObservers(NULL) != 0) + return false; // vetoed + else + return true; } /// Tell devices we are going to sleep and wait for them to handle things -static void waitEnterSleep(bool skipPreflight = false) { - if (!skipPreflight) { - uint32_t now = millis(); - while (!doPreflightSleep()) { - delay(100); // Kinda yucky - wait until radio says say we can shutdown (finished in process sends/receives) +static void waitEnterSleep(bool skipPreflight = false) +{ + if (!skipPreflight) { + uint32_t now = millis(); + while (!doPreflightSleep()) { + delay(100); // Kinda yucky - wait until radio says say we can shutdown (finished in process sends/receives) - if (!Throttle::isWithinTimespanMs(now, - THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT); - assert(0); // FIXME - for now we just restart, need to fix bug #167 - break; - } + if (!Throttle::isWithinTimespanMs(now, + THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT); + assert(0); // FIXME - for now we just restart, need to fix bug #167 + break; + } + } } - } - // Code that still needs to be moved into notifyObservers - console->flush(); // send all our characters before we stop cpu clock - setBluetoothEnable(false); // has to be off before calling light sleep + // Code that still needs to be moved into notifyObservers + console->flush(); // send all our characters before we stop cpu clock + setBluetoothEnable(false); // has to be off before calling light sleep } -void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false) { - if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { - LOG_INFO("Enter deep sleep forever"); - } else { - LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000); - } +void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false) +{ + if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { + LOG_INFO("Enter deep sleep forever"); + } else { + LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000); + } - // not using wifi yet, but once we are this is needed to shutoff the radio hw - // esp_wifi_stop(); - waitEnterSleep(skipPreflight); + // not using wifi yet, but once we are this is needed to shutoff the radio hw + // esp_wifi_stop(); + waitEnterSleep(skipPreflight); #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH - // Full shutdown of bluetooth hardware - if (nimbleBluetooth) - nimbleBluetooth->deinit(); + // Full shutdown of bluetooth hardware + if (nimbleBluetooth) + nimbleBluetooth->deinit(); #endif #ifdef ARCH_ESP32 - if (!shouldLoraWake(msecToWake)) - notifyDeepSleep.notifyObservers(NULL); + if (!shouldLoraWake(msecToWake)) + notifyDeepSleep.notifyObservers(NULL); #else - notifyDeepSleep.notifyObservers(NULL); + notifyDeepSleep.notifyObservers(NULL); #endif - powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); - if (screen) - screen->doDeepSleep(); // datasheet says this will draw only 10ua + powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); + if (screen) + screen->doDeepSleep(); // datasheet says this will draw only 10ua - if (!skipSaveNodeDb) { - nodeDB->saveToDisk(); - } + if (!skipSaveNodeDb) { + nodeDB->saveToDisk(); + } #ifdef PIN_POWER_EN - digitalWrite(PIN_POWER_EN, LOW); - pinMode(PIN_POWER_EN, INPUT); // power off peripherals - // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); + digitalWrite(PIN_POWER_EN, LOW); + pinMode(PIN_POWER_EN, INPUT); // power off peripherals + // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); #endif #ifdef RAK_WISMESH_TAP_V2 - digitalWrite(SDCARD_CS, LOW); + digitalWrite(SDCARD_CS, LOW); #endif #ifdef TRACKER_T1000_E #ifdef GNSS_AIROHA - digitalWrite(GPS_VRTC_EN, LOW); - digitalWrite(PIN_GPS_RESET, LOW); - digitalWrite(GPS_SLEEP_INT, LOW); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, OUTPUT); - digitalWrite(GPS_RESETB_OUT, LOW); + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(PIN_GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB_OUT, OUTPUT); + digitalWrite(GPS_RESETB_OUT, LOW); #endif #ifdef BUZZER_EN_PIN - digitalWrite(BUZZER_EN_PIN, LOW); + digitalWrite(BUZZER_EN_PIN, LOW); #endif #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, LOW); + digitalWrite(PIN_3V3_EN, LOW); #endif #ifdef PIN_WD_EN - digitalWrite(PIN_WD_EN, LOW); + digitalWrite(PIN_WD_EN, LOW); #endif #endif - ledBlink.set(false); + ledBlink.set(false); #ifdef RESET_OLED - digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power + digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power #endif #if defined(VEXT_ENABLE) - digitalWrite(VEXT_ENABLE, !VEXT_ON_VALUE); // turn on the display power + digitalWrite(VEXT_ENABLE, !VEXT_ON_VALUE); // turn on the display power #endif #ifdef ARCH_ESP32 - if (shouldLoraWake(msecToWake)) { - enableLoraInterrupt(); - } + if (shouldLoraWake(msecToWake)) { + enableLoraInterrupt(); + } #ifdef BUTTON_PIN - // Avoid leakage through button pin - if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) { + // Avoid leakage through button pin + if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) { #ifdef BUTTON_NEED_PULLUP - pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(BUTTON_PIN, INPUT_PULLUP); #else - pinMode(BUTTON_PIN, INPUT); + pinMode(BUTTON_PIN, INPUT); #endif - gpio_hold_en((gpio_num_t)BUTTON_PIN); - } + gpio_hold_en((gpio_num_t)BUTTON_PIN); + } #endif #ifdef SENSECAP_INDICATOR - // Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - gpio_hold_en((gpio_num_t)LORA_CS); -#elif defined(ELECROW_PANEL) - // Elecrow panels do not use LORA_CS, do nothing -#else - if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) { - // LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep + // Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); gpio_hold_en((gpio_num_t)LORA_CS); - } +#elif defined(ELECROW_PANEL) + // Elecrow panels do not use LORA_CS, do nothing +#else + if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) { + // LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + gpio_hold_en((gpio_num_t)LORA_CS); + } #endif #endif #ifdef HAS_PPM - if (PPM) { - LOG_INFO("PMM shutdown"); - console->flush(); - PPM->shutdown(); - } + if (PPM) { + LOG_INFO("PMM shutdown"); + console->flush(); + PPM->shutdown(); + } #endif #ifdef HAS_PMU - if (pmu_found && PMU) { - // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. - // We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost - // wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is - // critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it) - // - // No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would - // leave floating input for the IRQ line - // If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting - // in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets - // all the time. - PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); + if (pmu_found && PMU) { + // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. + // We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost + // wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is + // critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it) + // + // No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would + // leave floating input for the IRQ line + // If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting + // in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets + // all the time. + PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); - uint8_t model = PMU->getChipModel(); - if (model == XPOWERS_AXP2101) { - if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { - // t-beam v1.2 radio power channel - PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { - PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel - } - } else if (model == XPOWERS_AXP192) { - // t-beam v1.1 radio power channel - PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel + uint8_t model = PMU->getChipModel(); + if (model == XPOWERS_AXP2101) { + if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { + // t-beam v1.2 radio power channel + PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || + HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { + PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel + } + } else if (model == XPOWERS_AXP192) { + // t-beam v1.1 radio power channel + PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel + } + if (msecToWake == portMAX_DELAY) { + LOG_INFO("PMU shutdown"); + console->flush(); + PMU->shutdown(); + } } - if (msecToWake == portMAX_DELAY) { - LOG_INFO("PMU shutdown"); - console->flush(); - PMU->shutdown(); - } - } #endif #if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA) - // Added by https://github.com/meshtastic/firmware/pull/4418 - // Possibly to support Heltec Capsule Sensor? - Wire.end(); - pinMode(I2C_SDA, ANALOG); - pinMode(I2C_SCL, ANALOG); + // Added by https://github.com/meshtastic/firmware/pull/4418 + // Possibly to support Heltec Capsule Sensor? + Wire.end(); + pinMode(I2C_SDA, ANALOG); + pinMode(I2C_SCL, ANALOG); #endif - console->flush(); - cpuDeepSleep(msecToWake); + console->flush(); + cpuDeepSleep(msecToWake); } #ifdef ARCH_ESP32 @@ -368,130 +374,131 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN */ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default { - // LOG_DEBUG("Enter light sleep"); + // LOG_DEBUG("Enter light sleep"); - // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not - // entering LightSleep. + // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not entering + // LightSleep. #if defined(SENSECAP_INDICATOR) - return ESP_SLEEP_WAKEUP_TIMER; + return ESP_SLEEP_WAKEUP_TIMER; #endif - waitEnterSleep(false); - notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here + waitEnterSleep(false); + notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here - uint64_t sleepUsec = sleepMsec * 1000LL; + uint64_t sleepUsec = sleepMsec * 1000LL; - // NOTE! ESP docs say we must disable bluetooth and wifi before light sleep + // NOTE! ESP docs say we must disable bluetooth and wifi before light sleep - // We want RTC peripherals to stay on - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + // We want RTC peripherals to stay on + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); #if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP) - gpio_pullup_en((gpio_num_t)BUTTON_PIN); + gpio_pullup_en((gpio_num_t)BUTTON_PIN); #endif #ifdef SERIAL0_RX_GPIO - // We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means - // someone started to send something + // We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means + // someone started to send something - // gpio 3 is RXD for serialport 0 on ESP32 - // Send a few Z characters to wake the port + // gpio 3 is RXD for serialport 0 on ESP32 + // Send a few Z characters to wake the port - // this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts) - // So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it - // never tries to go to sleep if the user is using the API - // gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL); + // this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts) + // So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it + // never tries to go to sleep if the user is using the API + // gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL); - // doesn't help - I think the USB-UART chip losing power is pulling the signal low - // gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO); + // doesn't help - I think the USB-UART chip losing power is pulling the signal low + // gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO); - // alas - can only work if using the refclock, which is limited to about 9600 bps - // assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK); - // assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK); + // alas - can only work if using the refclock, which is limited to about 9600 bps + // assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK); + // assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK); #endif #ifdef ROTARY_PRESS - // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup - gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL); + // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup + gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL); #endif #ifdef KB_INT - gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); + gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); #endif #ifdef BUTTON_PIN - gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); - gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); + gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); + gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); #endif #ifdef INPUTDRIVER_ENCODER_BTN - gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); + gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); #endif #if defined(WAKE_ON_TOUCH) - gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); + gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); #endif - enableLoraInterrupt(); + enableLoraInterrupt(); #ifdef PMU_IRQ - // wake due to PMU can happen repeatedly if there is no battery installed or the battery fills - if (pmu_found) - gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq + // wake due to PMU can happen repeatedly if there is no battery installed or the battery fills + if (pmu_found) + gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq #endif - auto res = esp_sleep_enable_gpio_wakeup(); - if (res != ESP_OK) { - LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); - } - assert(res == ESP_OK); - res = esp_sleep_enable_timer_wakeup(sleepUsec); - if (res != ESP_OK) { - LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res); - } - assert(res == ESP_OK); + auto res = esp_sleep_enable_gpio_wakeup(); + if (res != ESP_OK) { + LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); + } + assert(res == ESP_OK); + res = esp_sleep_enable_timer_wakeup(sleepUsec); + if (res != ESP_OK) { + LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res); + } + assert(res == ESP_OK); - console->flush(); - res = esp_light_sleep_start(); - if (res != ESP_OK) { - LOG_ERROR("esp_light_sleep_start result %d", res); - } - // commented out because it's not that crucial; - // if it sporadically happens the node will go into light sleep during the next round - // assert(res == ESP_OK); + console->flush(); + res = esp_light_sleep_start(); + if (res != ESP_OK) { + LOG_ERROR("esp_light_sleep_start result %d", res); + } + // commented out because it's not that crucial; + // if it sporadically happens the node will go into light sleep during the next round + // assert(res == ESP_OK); #ifdef ROTARY_PRESS - gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS); + gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS); #endif #ifdef KB_INT - gpio_wakeup_disable((gpio_num_t)KB_INT); + gpio_wakeup_disable((gpio_num_t)KB_INT); #endif #ifdef BUTTON_PIN - // Disable wake-on-button interrupt. Re-attach normal button-interrupts - gpio_wakeup_disable(pin); + // Disable wake-on-button interrupt. Re-attach normal button-interrupts + gpio_wakeup_disable(pin); #endif #if defined(INPUTDRIVER_ENCODER_BTN) - gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); + gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); #endif #if defined(WAKE_ON_TOUCH) - gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); + gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); #endif #if !defined(SOC_PM_SUPPORT_EXT_WAKEUP) && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - if (radioType != RF95_RADIO) { - gpio_wakeup_disable((gpio_num_t)LORA_DIO1); - } + if (radioType != RF95_RADIO) { + gpio_wakeup_disable((gpio_num_t)LORA_DIO1); + } #endif #if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) - if (radioType == RF95_RADIO) { - gpio_wakeup_disable((gpio_num_t)RF95_IRQ); - } + if (radioType == RF95_RADIO) { + gpio_wakeup_disable((gpio_num_t)RF95_IRQ); + } #endif - esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); - notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here + esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); + notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here #ifdef BUTTON_PIN - if (cause == ESP_SLEEP_WAKEUP_GPIO) { - LOG_INFO("Exit light sleep gpio: btn=%d", !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - } else + if (cause == ESP_SLEEP_WAKEUP_GPIO) { + LOG_INFO("Exit light sleep gpio: btn=%d", + !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + } else #endif - { - LOG_INFO("Exit light sleep cause: %d", cause); - } + { + LOG_INFO("Exit light sleep cause: %d", cause); + } - return cause; + return cause; } // not legal on the stock android ESP build @@ -503,68 +510,73 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r * * supposedly according to https://github.com/espressif/arduino-esp32/issues/475 this is already done in arduino */ -void enableModemSleep() { +void enableModemSleep() +{ #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - static esp_pm_config_t esp32_config; // filled with zeros because bss + static esp_pm_config_t esp32_config; // filled with zeros because bss #else - static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss + static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss #endif #if CONFIG_IDF_TARGET_ESP32S3 - esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ; #elif CONFIG_IDF_TARGET_ESP32S2 - esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ; #elif CONFIG_IDF_TARGET_ESP32C6 - esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ; #elif CONFIG_IDF_TARGET_ESP32C3 - esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ; #else - esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ; + esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ; #endif - esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended - esp32_config.light_sleep_enable = false; - int rv = esp_pm_configure(&esp32_config); - LOG_DEBUG("Sleep request result %x", rv); + esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended + esp32_config.light_sleep_enable = false; + int rv = esp_pm_configure(&esp32_config); + LOG_DEBUG("Sleep request result %x", rv); } -bool shouldLoraWake(uint32_t msecToWake) { return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER); } +bool shouldLoraWake(uint32_t msecToWake) +{ + return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER); +} -void enableLoraInterrupt() { - esp_err_t res; +void enableLoraInterrupt() +{ + esp_err_t res; #if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); - if (res != ESP_OK) { - LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); - } + res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); + if (res != ESP_OK) { + LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); + } #if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC) - res = gpio_pullup_en((gpio_num_t)LORA_RESET); - if (res != ESP_OK) { - LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res); - } + res = gpio_pullup_en((gpio_num_t)LORA_RESET); + if (res != ESP_OK) { + LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res); + } #endif #if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) && !defined(ELECROW_PANEL) - gpio_pullup_en((gpio_num_t)LORA_CS); + gpio_pullup_en((gpio_num_t)LORA_CS); #endif #if defined(USE_GC1109_PA) - gpio_pullup_en((gpio_num_t)LORA_PA_POWER); - gpio_pullup_en((gpio_num_t)LORA_PA_EN); - gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); + gpio_pullup_en((gpio_num_t)LORA_PA_POWER); + gpio_pullup_en((gpio_num_t)LORA_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); #endif - LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); - gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); + LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); + gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); #elif defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) - if (radioType != RF95_RADIO) { - LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); - gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); // SX126x/SX128x interrupt, active high - } + if (radioType != RF95_RADIO) { + LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); + gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); // SX126x/SX128x interrupt, active high + } #endif #if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC) - if (radioType == RF95_RADIO) { - LOG_INFO("setup RF95_IRQ (GPIO%02d) with wakeup by gpio interrupt", RF95_IRQ); - gpio_wakeup_enable((gpio_num_t)RF95_IRQ, GPIO_INTR_HIGH_LEVEL); // RF95 interrupt, active high - } + if (radioType == RF95_RADIO) { + LOG_INFO("setup RF95_IRQ (GPIO%02d) with wakeup by gpio interrupt", RF95_IRQ); + gpio_wakeup_enable((gpio_num_t)RF95_IRQ, GPIO_INTR_HIGH_LEVEL); // RF95 interrupt, active high + } #endif } #endif diff --git a/src/xmodem.cpp b/src/xmodem.cpp index 69f1f1963..1d8c77760 100644 --- a/src/xmodem.cpp +++ b/src/xmodem.cpp @@ -2,14 +2,14 @@ * @file xmodem.cpp * @brief Implementation of XMODEM protocol for Meshtastic devices. * - * This file contains the implementation of the XMODEM protocol for Meshtastic devices. It is based on the XMODEM - * implementation by Georges Menie (www.menie.org) and has been adapted for protobuf encapsulation. + * This file contains the implementation of the XMODEM protocol for Meshtastic devices. It is based on the XMODEM implementation + * by Georges Menie (www.menie.org) and has been adapted for protobuf encapsulation. * - * The XMODEM protocol is used for reliable transmission of binary data over a serial connection. This implementation - * supports both sending and receiving of data. + * The XMODEM protocol is used for reliable transmission of binary data over a serial connection. This implementation supports + * both sending and receiving of data. * - * The XModemAdapter class provides the main functionality for the protocol, including CRC calculation, packet handling, - * and control signal sending. + * The XModemAdapter class provides the main functionality for the protocol, including CRC calculation, packet handling, and + * control signal sending. * * @copyright Copyright (c) 2001-2019 Georges Menie * @author @@ -64,19 +64,20 @@ XModemAdapter::XModemAdapter() {} * @param length The length of the buffer. * @return The calculated checksum. */ -unsigned short XModemAdapter::crc16_ccitt(const pb_byte_t *buffer, int length) { - unsigned short crc16 = 0; - while (length != 0) { - crc16 = (unsigned char)(crc16 >> 8) | (crc16 << 8); - crc16 ^= *buffer; - crc16 ^= (unsigned char)(crc16 & 0xff) >> 4; - crc16 ^= (crc16 << 8) << 4; - crc16 ^= ((crc16 & 0xff) << 4) << 1; - buffer++; - length--; - } +unsigned short XModemAdapter::crc16_ccitt(const pb_byte_t *buffer, int length) +{ + unsigned short crc16 = 0; + while (length != 0) { + crc16 = (unsigned char)(crc16 >> 8) | (crc16 << 8); + crc16 ^= *buffer; + crc16 ^= (unsigned char)(crc16 & 0xff) >> 4; + crc16 ^= (crc16 << 8) << 4; + crc16 ^= ((crc16 & 0xff) << 4) << 1; + buffer++; + length--; + } - return crc16; + return crc16; } /** @@ -88,178 +89,190 @@ unsigned short XModemAdapter::crc16_ccitt(const pb_byte_t *buffer, int length) { * @param tcrc The expected checksum. * @return 1 if the checksums match, 0 otherwise. */ -int XModemAdapter::check(const pb_byte_t *buf, int sz, unsigned short tcrc) { return crc16_ccitt(buf, sz) == tcrc; } - -void XModemAdapter::sendControl(meshtastic_XModem_Control c) { - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = c; - LOG_DEBUG("XModem: Notify Send control %d", c); - packetReady.notifyObservers(packetno); +int XModemAdapter::check(const pb_byte_t *buf, int sz, unsigned short tcrc) +{ + return crc16_ccitt(buf, sz) == tcrc; } -meshtastic_XModem XModemAdapter::getForPhone() { return xmodemStore; } +void XModemAdapter::sendControl(meshtastic_XModem_Control c) +{ + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = c; + LOG_DEBUG("XModem: Notify Send control %d", c); + packetReady.notifyObservers(packetno); +} -void XModemAdapter::resetForPhone() { xmodemStore = meshtastic_XModem_init_zero; } +meshtastic_XModem XModemAdapter::getForPhone() +{ + return xmodemStore; +} -void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket) { - switch (xmodemPacket.control) { - case meshtastic_XModem_Control_SOH: - case meshtastic_XModem_Control_STX: - if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) { - // NULL packet has the destination filename - memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); +void XModemAdapter::resetForPhone() +{ + xmodemStore = meshtastic_XModem_init_zero; +} - if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash - spiLock->lock(); - file = FSCom.open(filename, FILE_O_WRITE); - spiLock->unlock(); - if (file) { - sendControl(meshtastic_XModem_Control_ACK); - isReceiving = true; - packetno = 1; - break; +void XModemAdapter::handlePacket(meshtastic_XModem xmodemPacket) +{ + switch (xmodemPacket.control) { + case meshtastic_XModem_Control_SOH: + case meshtastic_XModem_Control_STX: + if ((xmodemPacket.seq == 0) && !isReceiving && !isTransmitting) { + // NULL packet has the destination filename + memcpy(filename, &xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); + + if (xmodemPacket.control == meshtastic_XModem_Control_SOH) { // Receive this file and put to Flash + spiLock->lock(); + file = FSCom.open(filename, FILE_O_WRITE); + spiLock->unlock(); + if (file) { + sendControl(meshtastic_XModem_Control_ACK); + isReceiving = true; + packetno = 1; + break; + } + sendControl(meshtastic_XModem_Control_NAK); + isReceiving = false; + break; + } else { // Transmit this file from Flash + LOG_INFO("XModem: Transmit file %s", filename); + spiLock->lock(); + file = FSCom.open(filename, FILE_O_READ); + spiLock->unlock(); + if (file) { + packetno = 1; + isTransmitting = true; + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + spiLock->lock(); + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + spiLock->unlock(); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: STX Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); + if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { + isEOT = true; + // send EOT on next Ack + } + packetReady.notifyObservers(packetno); + break; + } + sendControl(meshtastic_XModem_Control_NAK); + isTransmitting = false; + break; + } + } else { + if (isReceiving) { + // normal file data packet + if ((xmodemPacket.seq == packetno) && + check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) { + // valid packet + spiLock->lock(); + file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); + spiLock->unlock(); + sendControl(meshtastic_XModem_Control_ACK); + packetno++; + break; + } + // invalid packet + sendControl(meshtastic_XModem_Control_NAK); + break; + } else if (isTransmitting) { + // just received something weird. + sendControl(meshtastic_XModem_Control_CAN); + isTransmitting = false; + break; + } } - sendControl(meshtastic_XModem_Control_NAK); + break; + case meshtastic_XModem_Control_EOT: + // End of transmission + sendControl(meshtastic_XModem_Control_ACK); + spiLock->lock(); + file.flush(); + file.close(); + spiLock->unlock(); isReceiving = false; break; - } else { // Transmit this file from Flash - LOG_INFO("XModem: Transmit file %s", filename); - spiLock->lock(); - file = FSCom.open(filename, FILE_O_READ); - spiLock->unlock(); - if (file) { - packetno = 1; - isTransmitting = true; - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = meshtastic_XModem_Control_SOH; - xmodemStore.seq = packetno; - spiLock->lock(); - xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); - spiLock->unlock(); - xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); - LOG_DEBUG("XModem: STX Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); - if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { - isEOT = true; - // send EOT on next Ack - } - packetReady.notifyObservers(packetno); - break; - } - sendControl(meshtastic_XModem_Control_NAK); - isTransmitting = false; - break; - } - } else { - if (isReceiving) { - // normal file data packet - if ((xmodemPacket.seq == packetno) && check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) { - // valid packet - spiLock->lock(); - file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); - spiLock->unlock(); - sendControl(meshtastic_XModem_Control_ACK); - packetno++; - break; - } - // invalid packet - sendControl(meshtastic_XModem_Control_NAK); - break; - } else if (isTransmitting) { - // just received something weird. - sendControl(meshtastic_XModem_Control_CAN); - isTransmitting = false; - break; - } - } - break; - case meshtastic_XModem_Control_EOT: - // End of transmission - sendControl(meshtastic_XModem_Control_ACK); - spiLock->lock(); - file.flush(); - file.close(); - spiLock->unlock(); - isReceiving = false; - break; - case meshtastic_XModem_Control_CAN: - // Cancel transmission and remove file - sendControl(meshtastic_XModem_Control_ACK); - spiLock->lock(); - file.flush(); - file.close(); - - FSCom.remove(filename); - spiLock->unlock(); - isReceiving = false; - break; - case meshtastic_XModem_Control_ACK: - // Acknowledge Send the next packet - if (isTransmitting) { - if (isEOT) { - sendControl(meshtastic_XModem_Control_EOT); + case meshtastic_XModem_Control_CAN: + // Cancel transmission and remove file + sendControl(meshtastic_XModem_Control_ACK); spiLock->lock(); + file.flush(); file.close(); - spiLock->unlock(); - LOG_INFO("XModem: Finished send file %s", filename); - isTransmitting = false; - isEOT = false; - break; - } - retrans = MAXRETRANS; // reset retransmit counter - packetno++; - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = meshtastic_XModem_Control_SOH; - xmodemStore.seq = packetno; - spiLock->lock(); - xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); - spiLock->unlock(); - xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); - LOG_DEBUG("XModem: ACK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); - if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { - isEOT = true; - // send EOT on next Ack - } - packetReady.notifyObservers(packetno); - } else { - // just received something weird. - sendControl(meshtastic_XModem_Control_CAN); - } - break; - case meshtastic_XModem_Control_NAK: - // Negative acknowledge. Send the same buffer again - if (isTransmitting) { - if (--retrans <= 0) { - sendControl(meshtastic_XModem_Control_CAN); - spiLock->lock(); - file.close(); - spiLock->unlock(); - LOG_INFO("XModem: Retransmit timeout, cancel file %s", filename); - isTransmitting = false; - break; - } - xmodemStore = meshtastic_XModem_init_zero; - xmodemStore.control = meshtastic_XModem_Control_SOH; - xmodemStore.seq = packetno; - spiLock->lock(); - file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes)); - xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); - spiLock->unlock(); - xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); - LOG_DEBUG("XModem: NAK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); - if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { - isEOT = true; - // send EOT on next Ack - } - packetReady.notifyObservers(packetno); - } else { - // just received something weird. - sendControl(meshtastic_XModem_Control_CAN); + FSCom.remove(filename); + spiLock->unlock(); + isReceiving = false; + break; + case meshtastic_XModem_Control_ACK: + // Acknowledge Send the next packet + if (isTransmitting) { + if (isEOT) { + sendControl(meshtastic_XModem_Control_EOT); + spiLock->lock(); + file.close(); + spiLock->unlock(); + LOG_INFO("XModem: Finished send file %s", filename); + isTransmitting = false; + isEOT = false; + break; + } + retrans = MAXRETRANS; // reset retransmit counter + packetno++; + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + spiLock->lock(); + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + spiLock->unlock(); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: ACK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); + if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { + isEOT = true; + // send EOT on next Ack + } + packetReady.notifyObservers(packetno); + } else { + // just received something weird. + sendControl(meshtastic_XModem_Control_CAN); + } + break; + case meshtastic_XModem_Control_NAK: + // Negative acknowledge. Send the same buffer again + if (isTransmitting) { + if (--retrans <= 0) { + sendControl(meshtastic_XModem_Control_CAN); + spiLock->lock(); + file.close(); + spiLock->unlock(); + LOG_INFO("XModem: Retransmit timeout, cancel file %s", filename); + isTransmitting = false; + break; + } + xmodemStore = meshtastic_XModem_init_zero; + xmodemStore.control = meshtastic_XModem_Control_SOH; + xmodemStore.seq = packetno; + spiLock->lock(); + file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes)); + + xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); + spiLock->unlock(); + xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); + LOG_DEBUG("XModem: NAK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); + if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { + isEOT = true; + // send EOT on next Ack + } + packetReady.notifyObservers(packetno); + } else { + // just received something weird. + sendControl(meshtastic_XModem_Control_CAN); + } + break; + default: + // Unknown control character + break; } - break; - default: - // Unknown control character - break; - } } #endif diff --git a/src/xmodem.h b/src/xmodem.h index cc731caf7..4cfcb43e1 100644 --- a/src/xmodem.h +++ b/src/xmodem.h @@ -40,39 +40,40 @@ #ifdef FSCom -class XModemAdapter { -public: - // Called when we put a fragment in the outgoing memory - Observable packetReady; +class XModemAdapter +{ + public: + // Called when we put a fragment in the outgoing memory + Observable packetReady; - XModemAdapter(); + XModemAdapter(); - void handlePacket(meshtastic_XModem xmodemPacket); - meshtastic_XModem getForPhone(); - void resetForPhone(); + void handlePacket(meshtastic_XModem xmodemPacket); + meshtastic_XModem getForPhone(); + void resetForPhone(); -private: - bool isReceiving = false; - bool isTransmitting = false; - bool isEOT = false; + private: + bool isReceiving = false; + bool isTransmitting = false; + bool isEOT = false; - int retrans = MAXRETRANS; + int retrans = MAXRETRANS; - uint16_t packetno = 0; + uint16_t packetno = 0; #if defined(ARCH_NRF52) || defined(ARCH_STM32WL) - File file = File(FSCom); + File file = File(FSCom); #else - File file; + File file; #endif - char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0}; + char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0}; -protected: - meshtastic_XModem xmodemStore = meshtastic_XModem_init_zero; - unsigned short crc16_ccitt(const pb_byte_t *buffer, int length); - int check(const pb_byte_t *buf, int sz, unsigned short tcrc); - void sendControl(meshtastic_XModem_Control c); + protected: + meshtastic_XModem xmodemStore = meshtastic_XModem_init_zero; + unsigned short crc16_ccitt(const pb_byte_t *buffer, int length); + int check(const pb_byte_t *buf, int sz, unsigned short tcrc); + void sendControl(meshtastic_XModem_Control c); }; extern XModemAdapter xModem; diff --git a/test/TestUtil.cpp b/test/TestUtil.cpp index 2038a5c0f..b470b8ce8 100644 --- a/test/TestUtil.cpp +++ b/test/TestUtil.cpp @@ -4,14 +4,15 @@ #include "TestUtil.h" -void initializeTestEnvironment() { - concurrency::hasBeenSetup = true; - consoleInit(); +void initializeTestEnvironment() +{ + concurrency::hasBeenSetup = true; + consoleInit(); #if ARCH_PORTDUINO - struct timeval tv; - tv.tv_sec = time(NULL); - tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + struct timeval tv; + tv.tv_sec = time(NULL); + tv.tv_usec = 0; + perhapsSetRTC(RTCQualityNTP, &tv); #endif - concurrency::OSThread::setup(); + concurrency::OSThread::setup(); } \ No newline at end of file diff --git a/test/test_crypto/test_main.cpp b/test/test_crypto/test_main.cpp index a57ce6a4a..36dc37b9d 100644 --- a/test/test_crypto/test_main.cpp +++ b/test/test_crypto/test_main.cpp @@ -4,186 +4,195 @@ #include "TestUtil.h" #include -void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0) { - if (len) { - memset(result, 0, len); - } - for (unsigned int i = 0; i < hex.length(); i += 2) { - std::string byteString = hex.substr(i, 2); - result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16); - } - return; +void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0) +{ + if (len) { + memset(result, 0, len); + } + for (unsigned int i = 0; i < hex.length(); i += 2) { + std::string byteString = hex.substr(i, 2); + result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16); + } + return; } -void setUp(void) { - // set stuff up here +void setUp(void) +{ + // set stuff up here } -void tearDown(void) { - // clean stuff up here +void tearDown(void) +{ + // clean stuff up here } -void test_SHA256(void) { - uint8_t expected[32]; - uint8_t hash[32] = {0}; +void test_SHA256(void) +{ + uint8_t expected[32]; + uint8_t hash[32] = {0}; - HexToBytes(expected, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); - crypto->hash(hash, 0); - TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); + HexToBytes(expected, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + crypto->hash(hash, 0); + TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); - HexToBytes(hash, "d3", 32); - HexToBytes(expected, "28969cdfa74a12c82f3bad960b0b000aca2ac329deea5c2328ebc6f2ba9802c1"); - crypto->hash(hash, 1); - TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); + HexToBytes(hash, "d3", 32); + HexToBytes(expected, "28969cdfa74a12c82f3bad960b0b000aca2ac329deea5c2328ebc6f2ba9802c1"); + crypto->hash(hash, 1); + TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); - HexToBytes(hash, "11af", 32); - HexToBytes(expected, "5ca7133fa735326081558ac312c620eeca9970d1e70a4b95533d956f072d1f98"); - crypto->hash(hash, 2); - TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); + HexToBytes(hash, "11af", 32); + HexToBytes(expected, "5ca7133fa735326081558ac312c620eeca9970d1e70a4b95533d956f072d1f98"); + crypto->hash(hash, 2); + TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32); } -void test_ECB_AES256(void) { - // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf +void test_ECB_AES256(void) +{ + // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf - uint8_t key[32] = {0}; - uint8_t plain[16] = {0}; - uint8_t result[16] = {0}; - uint8_t expected[16] = {0}; + uint8_t key[32] = {0}; + uint8_t plain[16] = {0}; + uint8_t result[16] = {0}; + uint8_t expected[16] = {0}; - HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4"); + HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4"); - HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A"); - HexToBytes(expected, "F3EED1BDB5D2A03C064B5A7E3DB181F8"); - crypto->aesSetKey(key, 32); - crypto->aesEncrypt(plain, result); // Does 16 bytes at a time - TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); + HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A"); + HexToBytes(expected, "F3EED1BDB5D2A03C064B5A7E3DB181F8"); + crypto->aesSetKey(key, 32); + crypto->aesEncrypt(plain, result); // Does 16 bytes at a time + TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); - HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51"); - HexToBytes(expected, "591CCB10D410ED26DC5BA74A31362870"); - crypto->aesSetKey(key, 32); - crypto->aesEncrypt(plain, result); // Does 16 bytes at a time - TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); + HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51"); + HexToBytes(expected, "591CCB10D410ED26DC5BA74A31362870"); + crypto->aesSetKey(key, 32); + crypto->aesEncrypt(plain, result); // Does 16 bytes at a time + TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); - HexToBytes(plain, "30C81C46A35CE411E5FBC1191A0A52EF"); - HexToBytes(expected, "B6ED21B99CA6F4F9F153E7B1BEAFED1D"); - crypto->aesSetKey(key, 32); - crypto->aesEncrypt(plain, result); // Does 16 bytes at a time - TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); + HexToBytes(plain, "30C81C46A35CE411E5FBC1191A0A52EF"); + HexToBytes(expected, "B6ED21B99CA6F4F9F153E7B1BEAFED1D"); + crypto->aesSetKey(key, 32); + crypto->aesEncrypt(plain, result); // Does 16 bytes at a time + TEST_ASSERT_EQUAL_MEMORY(expected, result, 16); } -void test_DH25519(void) { - // test vectors from wycheproof x25519 - // https://github.com/C2SP/wycheproof/blob/master/testvectors/x25519_test.json - uint8_t private_key[32]; - uint8_t public_key[32]; - uint8_t expected_shared[32]; +void test_DH25519(void) +{ + // test vectors from wycheproof x25519 + // https://github.com/C2SP/wycheproof/blob/master/testvectors/x25519_test.json + uint8_t private_key[32]; + uint8_t public_key[32]; + uint8_t expected_shared[32]; - HexToBytes(public_key, "504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829"); - HexToBytes(private_key, "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475"); - HexToBytes(expected_shared, "436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->setDHPublicKey(public_key)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); + HexToBytes(public_key, "504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829"); + HexToBytes(private_key, "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475"); + HexToBytes(expected_shared, "436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(crypto->setDHPublicKey(public_key)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); - HexToBytes(public_key, "63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733"); - HexToBytes(private_key, "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958"); - HexToBytes(expected_shared, "279df67a7c4611db4708a0e8282b195e5ac0ed6f4b2f292c6fbd0acac30d1332"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->setDHPublicKey(public_key)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); + HexToBytes(public_key, "63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733"); + HexToBytes(private_key, "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958"); + HexToBytes(expected_shared, "279df67a7c4611db4708a0e8282b195e5ac0ed6f4b2f292c6fbd0acac30d1332"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(crypto->setDHPublicKey(public_key)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); - HexToBytes(public_key, "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"); - HexToBytes(private_key, "18630f93598637c35da623a74559cf944374a559114c7937811041fc8605564a"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(!crypto->setDHPublicKey(public_key)); // Weak public key results in 0 shared key + HexToBytes(public_key, "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"); + HexToBytes(private_key, "18630f93598637c35da623a74559cf944374a559114c7937811041fc8605564a"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(!crypto->setDHPublicKey(public_key)); // Weak public key results in 0 shared key - HexToBytes(public_key, "f7e13a1a067d2f4e1061bf9936fde5be6b0c2494a8f809cbac7f290ef719e91c"); - HexToBytes(private_key, "10300724f3bea134eb1575245ef26ff9b8ccd59849cd98ce1a59002fe1d5986c"); - HexToBytes(expected_shared, "24becd5dfed9e9289ba2e15b82b0d54f8e9aacb72f5e4248c58d8d74b451ce76"); - crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->setDHPublicKey(public_key)); - crypto->hash(crypto->shared_key, 32); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); + HexToBytes(public_key, "f7e13a1a067d2f4e1061bf9936fde5be6b0c2494a8f809cbac7f290ef719e91c"); + HexToBytes(private_key, "10300724f3bea134eb1575245ef26ff9b8ccd59849cd98ce1a59002fe1d5986c"); + HexToBytes(expected_shared, "24becd5dfed9e9289ba2e15b82b0d54f8e9aacb72f5e4248c58d8d74b451ce76"); + crypto->setDHPrivateKey(private_key); + TEST_ASSERT(crypto->setDHPublicKey(public_key)); + crypto->hash(crypto->shared_key, 32); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32); } -void test_PKC(void) { - uint8_t private_key[32]; - meshtastic_UserLite_public_key_t public_key; - uint8_t expected_shared[32]; - uint8_t expected_decrypted[32]; - uint8_t radioBytes[128] __attribute__((__aligned__)); - uint8_t decrypted[128] __attribute__((__aligned__)); - uint8_t expected_nonce[16]; +void test_PKC(void) +{ + uint8_t private_key[32]; + meshtastic_UserLite_public_key_t public_key; + uint8_t expected_shared[32]; + uint8_t expected_decrypted[32]; + uint8_t radioBytes[128] __attribute__((__aligned__)); + uint8_t decrypted[128] __attribute__((__aligned__)); + uint8_t expected_nonce[16]; - uint32_t fromNode = 0x0929; - uint64_t packetNum = 0x13b2d662; - HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457"); - public_key.size = 32; - HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277"); - HexToBytes(expected_shared, "777b1545c9d6f9a2"); - HexToBytes(expected_decrypted, "08011204746573744800"); - HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b"); - HexToBytes(expected_nonce, "62d6b213036a792b2909000000"); - crypto->setDHPrivateKey(private_key); + uint32_t fromNode = 0x0929; + uint64_t packetNum = 0x13b2d662; + HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457"); + public_key.size = 32; + HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277"); + HexToBytes(expected_shared, "777b1545c9d6f9a2"); + HexToBytes(expected_decrypted, "08011204746573744800"); + HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b"); + HexToBytes(expected_nonce, "62d6b213036a792b2909000000"); + crypto->setDHPrivateKey(private_key); - TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); - TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); - TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); + TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); + TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); + TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); - uint32_t toNode = 0; // Only impacts logging - uint8_t encrypted[128] __attribute__((__aligned__)); - TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); - // The extraNonce is random, so skip checking the nonce and encrypted output here + uint32_t toNode = 0; // Only impacts logging + uint8_t encrypted[128] __attribute__((__aligned__)); + TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); + // The extraNonce is random, so skip checking the nonce and encrypted output here - // Copy the nonce to check it after encryption - memcpy(expected_nonce, crypto->nonce, 16); + // Copy the nonce to check it after encryption + memcpy(expected_nonce, crypto->nonce, 16); - // Decrypt the re-encrypted bytes and check they are the same as what we expect - TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted)); - TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); - TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); - TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); + // Decrypt the re-encrypted bytes and check they are the same as what we expect + TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted)); + TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); + TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); + TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); } -void test_AES_CTR(void) { - uint8_t expected[32]; - uint8_t plain[32]; - uint8_t nonce[32]; - CryptoKey k; +void test_AES_CTR(void) +{ + uint8_t expected[32]; + uint8_t plain[32]; + uint8_t nonce[32]; + CryptoKey k; - // vectors from https://www.rfc-editor.org/rfc/rfc3686#section-6 - k.length = 32; - HexToBytes(k.bytes, "776BEFF2851DB06F4C8A0542C8696F6C6A81AF1EEC96B4D37FC1D689E6C1C104"); - HexToBytes(nonce, "00000060DB5672C97AA8F0B200000001"); - HexToBytes(expected, "145AD01DBF824EC7560863DC71E3E0C0"); - memcpy(plain, "Single block msg", 16); + // vectors from https://www.rfc-editor.org/rfc/rfc3686#section-6 + k.length = 32; + HexToBytes(k.bytes, "776BEFF2851DB06F4C8A0542C8696F6C6A81AF1EEC96B4D37FC1D689E6C1C104"); + HexToBytes(nonce, "00000060DB5672C97AA8F0B200000001"); + HexToBytes(expected, "145AD01DBF824EC7560863DC71E3E0C0"); + memcpy(plain, "Single block msg", 16); - crypto->encryptAESCtr(k, nonce, 16, plain); - TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); + crypto->encryptAESCtr(k, nonce, 16, plain); + TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); - k.length = 16; - memcpy(plain, "Single block msg", 16); - HexToBytes(k.bytes, "AE6852F8121067CC4BF7A5765577F39E"); - HexToBytes(nonce, "00000030000000000000000000000001"); - HexToBytes(expected, "E4095D4FB7A7B3792D6175A3261311B8"); - crypto->encryptAESCtr(k, nonce, 16, plain); - TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); + k.length = 16; + memcpy(plain, "Single block msg", 16); + HexToBytes(k.bytes, "AE6852F8121067CC4BF7A5765577F39E"); + HexToBytes(nonce, "00000030000000000000000000000001"); + HexToBytes(expected, "E4095D4FB7A7B3792D6175A3261311B8"); + crypto->encryptAESCtr(k, nonce, 16, plain); + TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16); } -void setup() { - // NOTE!!! Wait for >2 secs - // if board doesn't support software reset via Serial.DTR/RTS - delay(10); - delay(2000); +void setup() +{ + // NOTE!!! Wait for >2 secs + // if board doesn't support software reset via Serial.DTR/RTS + delay(10); + delay(2000); - initializeTestEnvironment(); - UNITY_BEGIN(); // IMPORTANT LINE! - RUN_TEST(test_SHA256); - RUN_TEST(test_ECB_AES256); - RUN_TEST(test_DH25519); - RUN_TEST(test_AES_CTR); - RUN_TEST(test_PKC); - exit(UNITY_END()); // stop unit testing + initializeTestEnvironment(); + UNITY_BEGIN(); // IMPORTANT LINE! + RUN_TEST(test_SHA256); + RUN_TEST(test_ECB_AES256); + RUN_TEST(test_DH25519); + RUN_TEST(test_AES_CTR); + RUN_TEST(test_PKC); + exit(UNITY_END()); // stop unit testing } void loop() {} \ No newline at end of file diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp index ef4571576..37cfc1626 100644 --- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp +++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp @@ -1,54 +1,59 @@ #include "../test_helpers.h" // Helper function for all encrypted packet assertions -void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet) { - // Parse and validate JSON - TEST_ASSERT_TRUE(json.length() > 0); +void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet) +{ + // Parse and validate JSON + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Assert basic packet fields - TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber()); + // Assert basic packet fields + TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber()); - TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber()); + TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber()); - TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber()); + TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber()); - // Assert encrypted data fields - TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); + // Assert encrypted data fields + TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); - TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); - TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber()); + TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); + TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber()); - // Assert hex encoding - std::string encrypted_hex = jsonObj["bytes"]->AsString(); - TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length()); + // Assert hex encoding + std::string encrypted_hex = jsonObj["bytes"]->AsString(); + TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length()); - delete root; + delete root; } // Test encrypted packet serialization -void test_encrypted_packet_serialization() { - const char *data = "encrypted_payload_data"; - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data), - meshtastic_MeshPacket_encrypted_tag); - std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); +void test_encrypted_packet_serialization() +{ + const char *data = "encrypted_payload_data"; + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data), + meshtastic_MeshPacket_encrypted_tag); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); - assert_encrypted_packet(json, packet); + assert_encrypted_packet(json, packet); } // Test empty encrypted packet -void test_empty_encrypted_packet() { - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag); - std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); +void test_empty_encrypted_packet() +{ + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); - assert_encrypted_packet(json, packet); + assert_encrypted_packet(json, packet); } diff --git a/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp b/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp index aee298169..febda9950 100644 --- a/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp +++ b/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp @@ -1,49 +1,51 @@ #include "../test_helpers.h" -static size_t encode_user_info(uint8_t *buffer, size_t buffer_size) { - meshtastic_User user = meshtastic_User_init_zero; - strcpy(user.short_name, "TEST"); - strcpy(user.long_name, "Test User"); - strcpy(user.id, "!12345678"); - user.hw_model = meshtastic_HardwareModel_HELTEC_V3; +static size_t encode_user_info(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_User user = meshtastic_User_init_zero; + strcpy(user.short_name, "TEST"); + strcpy(user.long_name, "Test User"); + strcpy(user.id, "!12345678"); + user.hw_model = meshtastic_HardwareModel_HELTEC_V3; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_User_msg, &user); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_User_msg, &user); + return stream.bytes_written; } // Test NODEINFO_APP port -void test_nodeinfo_serialization() { - uint8_t buffer[256]; - size_t payload_size = encode_user_info(buffer, sizeof(buffer)); +void test_nodeinfo_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_user_info(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify user data - TEST_ASSERT_TRUE(payload.find("shortname") != payload.end()); - TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str()); + // Verify user data + TEST_ASSERT_TRUE(payload.find("shortname") != payload.end()); + TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str()); - TEST_ASSERT_TRUE(payload.find("longname") != payload.end()); - TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str()); + TEST_ASSERT_TRUE(payload.find("longname") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/ports/test_position.cpp b/test/test_meshpacket_serializer/ports/test_position.cpp index 9c8284ccd..f0dcc0709 100644 --- a/test/test_meshpacket_serializer/ports/test_position.cpp +++ b/test/test_meshpacket_serializer/ports/test_position.cpp @@ -1,55 +1,57 @@ #include "../test_helpers.h" -static size_t encode_position(uint8_t *buffer, size_t buffer_size) { - meshtastic_Position position = meshtastic_Position_init_zero; - position.latitude_i = 374208000; // 37.4208 degrees * 1e7 - position.longitude_i = -1221981000; // -122.1981 degrees * 1e7 - position.altitude = 123; - position.time = 1609459200; - position.has_altitude = true; - position.has_latitude_i = true; - position.has_longitude_i = true; +static size_t encode_position(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Position position = meshtastic_Position_init_zero; + position.latitude_i = 374208000; // 37.4208 degrees * 1e7 + position.longitude_i = -1221981000; // -122.1981 degrees * 1e7 + position.altitude = 123; + position.time = 1609459200; + position.has_altitude = true; + position.has_latitude_i = true; + position.has_longitude_i = true; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Position_msg, &position); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Position_msg, &position); + return stream.bytes_written; } // Test POSITION_APP port -void test_position_serialization() { - uint8_t buffer[256]; - size_t payload_size = encode_position(buffer, sizeof(buffer)); +void test_position_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_position(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify position data - TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end()); - TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber()); + // Verify position data + TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end()); + TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end()); - TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end()); + TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("altitude") != payload.end()); - TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("altitude") != payload.end()); + TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/ports/test_telemetry.cpp b/test/test_meshpacket_serializer/ports/test_telemetry.cpp index 382692591..a813aaab5 100644 --- a/test/test_meshpacket_serializer/ports/test_telemetry.cpp +++ b/test/test_meshpacket_serializer/ports/test_telemetry.cpp @@ -1,518 +1,528 @@ #include "../test_helpers.h" // Helper function to create and encode device metrics -static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size) { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag; - telemetry.variant.device_metrics.battery_level = 85; - telemetry.variant.device_metrics.has_battery_level = true; - telemetry.variant.device_metrics.voltage = 3.72f; - telemetry.variant.device_metrics.has_voltage = true; - telemetry.variant.device_metrics.channel_utilization = 15.56f; - telemetry.variant.device_metrics.has_channel_utilization = true; - telemetry.variant.device_metrics.air_util_tx = 8.23f; - telemetry.variant.device_metrics.has_air_util_tx = true; - telemetry.variant.device_metrics.uptime_seconds = 12345; - telemetry.variant.device_metrics.has_uptime_seconds = true; +static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag; + telemetry.variant.device_metrics.battery_level = 85; + telemetry.variant.device_metrics.has_battery_level = true; + telemetry.variant.device_metrics.voltage = 3.72f; + telemetry.variant.device_metrics.has_voltage = true; + telemetry.variant.device_metrics.channel_utilization = 15.56f; + telemetry.variant.device_metrics.has_channel_utilization = true; + telemetry.variant.device_metrics.air_util_tx = 8.23f; + telemetry.variant.device_metrics.has_air_util_tx = true; + telemetry.variant.device_metrics.uptime_seconds = 12345; + telemetry.variant.device_metrics.has_uptime_seconds = true; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Helper function to create and encode empty environment metrics (no fields set) -static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size) { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; +static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; - // NO fields are set - all has_* flags remain false - // This tests that empty environment metrics don't produce any JSON fields + // NO fields are set - all has_* flags remain false + // This tests that empty environment metrics don't produce any JSON fields - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Helper function to create environment metrics with ALL possible fields set // This function should be updated whenever new fields are added to the protobuf -static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size) { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; +static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; - // Basic environment metrics - telemetry.variant.environment_metrics.temperature = 23.56f; - telemetry.variant.environment_metrics.has_temperature = true; - telemetry.variant.environment_metrics.relative_humidity = 65.43f; - telemetry.variant.environment_metrics.has_relative_humidity = true; - telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; - telemetry.variant.environment_metrics.has_barometric_pressure = true; + // Basic environment metrics + telemetry.variant.environment_metrics.temperature = 23.56f; + telemetry.variant.environment_metrics.has_temperature = true; + telemetry.variant.environment_metrics.relative_humidity = 65.43f; + telemetry.variant.environment_metrics.has_relative_humidity = true; + telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; + telemetry.variant.environment_metrics.has_barometric_pressure = true; - // Gas and air quality - telemetry.variant.environment_metrics.gas_resistance = 50.58f; - telemetry.variant.environment_metrics.has_gas_resistance = true; - telemetry.variant.environment_metrics.iaq = 120; - telemetry.variant.environment_metrics.has_iaq = true; + // Gas and air quality + telemetry.variant.environment_metrics.gas_resistance = 50.58f; + telemetry.variant.environment_metrics.has_gas_resistance = true; + telemetry.variant.environment_metrics.iaq = 120; + telemetry.variant.environment_metrics.has_iaq = true; - // Power measurements - telemetry.variant.environment_metrics.voltage = 3.34f; - telemetry.variant.environment_metrics.has_voltage = true; - telemetry.variant.environment_metrics.current = 0.53f; - telemetry.variant.environment_metrics.has_current = true; + // Power measurements + telemetry.variant.environment_metrics.voltage = 3.34f; + telemetry.variant.environment_metrics.has_voltage = true; + telemetry.variant.environment_metrics.current = 0.53f; + telemetry.variant.environment_metrics.has_current = true; - // Light measurements (ALL 4 types) - telemetry.variant.environment_metrics.lux = 450.12f; - telemetry.variant.environment_metrics.has_lux = true; - telemetry.variant.environment_metrics.white_lux = 380.95f; - telemetry.variant.environment_metrics.has_white_lux = true; - telemetry.variant.environment_metrics.ir_lux = 25.37f; - telemetry.variant.environment_metrics.has_ir_lux = true; - telemetry.variant.environment_metrics.uv_lux = 15.68f; - telemetry.variant.environment_metrics.has_uv_lux = true; + // Light measurements (ALL 4 types) + telemetry.variant.environment_metrics.lux = 450.12f; + telemetry.variant.environment_metrics.has_lux = true; + telemetry.variant.environment_metrics.white_lux = 380.95f; + telemetry.variant.environment_metrics.has_white_lux = true; + telemetry.variant.environment_metrics.ir_lux = 25.37f; + telemetry.variant.environment_metrics.has_ir_lux = true; + telemetry.variant.environment_metrics.uv_lux = 15.68f; + telemetry.variant.environment_metrics.has_uv_lux = true; - // Distance measurement - telemetry.variant.environment_metrics.distance = 150.29f; - telemetry.variant.environment_metrics.has_distance = true; + // Distance measurement + telemetry.variant.environment_metrics.distance = 150.29f; + telemetry.variant.environment_metrics.has_distance = true; - // Wind measurements (ALL 4 types) - telemetry.variant.environment_metrics.wind_direction = 180; - telemetry.variant.environment_metrics.has_wind_direction = true; - telemetry.variant.environment_metrics.wind_speed = 5.52f; - telemetry.variant.environment_metrics.has_wind_speed = true; - telemetry.variant.environment_metrics.wind_gust = 8.24f; - telemetry.variant.environment_metrics.has_wind_gust = true; - telemetry.variant.environment_metrics.wind_lull = 2.13f; - telemetry.variant.environment_metrics.has_wind_lull = true; + // Wind measurements (ALL 4 types) + telemetry.variant.environment_metrics.wind_direction = 180; + telemetry.variant.environment_metrics.has_wind_direction = true; + telemetry.variant.environment_metrics.wind_speed = 5.52f; + telemetry.variant.environment_metrics.has_wind_speed = true; + telemetry.variant.environment_metrics.wind_gust = 8.24f; + telemetry.variant.environment_metrics.has_wind_gust = true; + telemetry.variant.environment_metrics.wind_lull = 2.13f; + telemetry.variant.environment_metrics.has_wind_lull = true; - // Weight measurement - telemetry.variant.environment_metrics.weight = 75.56f; - telemetry.variant.environment_metrics.has_weight = true; + // Weight measurement + telemetry.variant.environment_metrics.weight = 75.56f; + telemetry.variant.environment_metrics.has_weight = true; - // Radiation measurement - telemetry.variant.environment_metrics.radiation = 0.13f; - telemetry.variant.environment_metrics.has_radiation = true; + // Radiation measurement + telemetry.variant.environment_metrics.radiation = 0.13f; + telemetry.variant.environment_metrics.has_radiation = true; - // Rainfall measurements (BOTH types) - telemetry.variant.environment_metrics.rainfall_1h = 2.57f; - telemetry.variant.environment_metrics.has_rainfall_1h = true; - telemetry.variant.environment_metrics.rainfall_24h = 15.89f; - telemetry.variant.environment_metrics.has_rainfall_24h = true; + // Rainfall measurements (BOTH types) + telemetry.variant.environment_metrics.rainfall_1h = 2.57f; + telemetry.variant.environment_metrics.has_rainfall_1h = true; + telemetry.variant.environment_metrics.rainfall_24h = 15.89f; + telemetry.variant.environment_metrics.has_rainfall_24h = true; - // Soil measurements (BOTH types) - telemetry.variant.environment_metrics.soil_moisture = 85; - telemetry.variant.environment_metrics.has_soil_moisture = true; - telemetry.variant.environment_metrics.soil_temperature = 18.54f; - telemetry.variant.environment_metrics.has_soil_temperature = true; + // Soil measurements (BOTH types) + telemetry.variant.environment_metrics.soil_moisture = 85; + telemetry.variant.environment_metrics.has_soil_moisture = true; + telemetry.variant.environment_metrics.soil_temperature = 18.54f; + telemetry.variant.environment_metrics.has_soil_temperature = true; - // IMPORTANT: When new environment fields are added to the protobuf, - // they MUST be added here too, or the coverage test will fail! + // IMPORTANT: When new environment fields are added to the protobuf, + // they MUST be added here too, or the coverage test will fail! - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Helper function to create and encode environment metrics with all current fields -static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size) { - meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; - telemetry.time = 1609459200; - telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; +static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; - // Basic environment metrics - telemetry.variant.environment_metrics.temperature = 23.56f; - telemetry.variant.environment_metrics.has_temperature = true; - telemetry.variant.environment_metrics.relative_humidity = 65.43f; - telemetry.variant.environment_metrics.has_relative_humidity = true; - telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; - telemetry.variant.environment_metrics.has_barometric_pressure = true; + // Basic environment metrics + telemetry.variant.environment_metrics.temperature = 23.56f; + telemetry.variant.environment_metrics.has_temperature = true; + telemetry.variant.environment_metrics.relative_humidity = 65.43f; + telemetry.variant.environment_metrics.has_relative_humidity = true; + telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; + telemetry.variant.environment_metrics.has_barometric_pressure = true; - // Gas and air quality - telemetry.variant.environment_metrics.gas_resistance = 50.58f; - telemetry.variant.environment_metrics.has_gas_resistance = true; - telemetry.variant.environment_metrics.iaq = 120; - telemetry.variant.environment_metrics.has_iaq = true; + // Gas and air quality + telemetry.variant.environment_metrics.gas_resistance = 50.58f; + telemetry.variant.environment_metrics.has_gas_resistance = true; + telemetry.variant.environment_metrics.iaq = 120; + telemetry.variant.environment_metrics.has_iaq = true; - // Power measurements - telemetry.variant.environment_metrics.voltage = 3.34f; - telemetry.variant.environment_metrics.has_voltage = true; - telemetry.variant.environment_metrics.current = 0.53f; - telemetry.variant.environment_metrics.has_current = true; + // Power measurements + telemetry.variant.environment_metrics.voltage = 3.34f; + telemetry.variant.environment_metrics.has_voltage = true; + telemetry.variant.environment_metrics.current = 0.53f; + telemetry.variant.environment_metrics.has_current = true; - // Light measurements - telemetry.variant.environment_metrics.lux = 450.12f; - telemetry.variant.environment_metrics.has_lux = true; - telemetry.variant.environment_metrics.white_lux = 380.95f; - telemetry.variant.environment_metrics.has_white_lux = true; - telemetry.variant.environment_metrics.ir_lux = 25.37f; - telemetry.variant.environment_metrics.has_ir_lux = true; - telemetry.variant.environment_metrics.uv_lux = 15.68f; - telemetry.variant.environment_metrics.has_uv_lux = true; + // Light measurements + telemetry.variant.environment_metrics.lux = 450.12f; + telemetry.variant.environment_metrics.has_lux = true; + telemetry.variant.environment_metrics.white_lux = 380.95f; + telemetry.variant.environment_metrics.has_white_lux = true; + telemetry.variant.environment_metrics.ir_lux = 25.37f; + telemetry.variant.environment_metrics.has_ir_lux = true; + telemetry.variant.environment_metrics.uv_lux = 15.68f; + telemetry.variant.environment_metrics.has_uv_lux = true; - // Distance measurement - telemetry.variant.environment_metrics.distance = 150.29f; - telemetry.variant.environment_metrics.has_distance = true; + // Distance measurement + telemetry.variant.environment_metrics.distance = 150.29f; + telemetry.variant.environment_metrics.has_distance = true; - // Wind measurements - telemetry.variant.environment_metrics.wind_direction = 180; - telemetry.variant.environment_metrics.has_wind_direction = true; - telemetry.variant.environment_metrics.wind_speed = 5.52f; - telemetry.variant.environment_metrics.has_wind_speed = true; - telemetry.variant.environment_metrics.wind_gust = 8.24f; - telemetry.variant.environment_metrics.has_wind_gust = true; - telemetry.variant.environment_metrics.wind_lull = 2.13f; - telemetry.variant.environment_metrics.has_wind_lull = true; + // Wind measurements + telemetry.variant.environment_metrics.wind_direction = 180; + telemetry.variant.environment_metrics.has_wind_direction = true; + telemetry.variant.environment_metrics.wind_speed = 5.52f; + telemetry.variant.environment_metrics.has_wind_speed = true; + telemetry.variant.environment_metrics.wind_gust = 8.24f; + telemetry.variant.environment_metrics.has_wind_gust = true; + telemetry.variant.environment_metrics.wind_lull = 2.13f; + telemetry.variant.environment_metrics.has_wind_lull = true; - // Weight measurement - telemetry.variant.environment_metrics.weight = 75.56f; - telemetry.variant.environment_metrics.has_weight = true; + // Weight measurement + telemetry.variant.environment_metrics.weight = 75.56f; + telemetry.variant.environment_metrics.has_weight = true; - // Radiation measurement - telemetry.variant.environment_metrics.radiation = 0.13f; - telemetry.variant.environment_metrics.has_radiation = true; + // Radiation measurement + telemetry.variant.environment_metrics.radiation = 0.13f; + telemetry.variant.environment_metrics.has_radiation = true; - // Rainfall measurements - telemetry.variant.environment_metrics.rainfall_1h = 2.57f; - telemetry.variant.environment_metrics.has_rainfall_1h = true; - telemetry.variant.environment_metrics.rainfall_24h = 15.89f; - telemetry.variant.environment_metrics.has_rainfall_24h = true; + // Rainfall measurements + telemetry.variant.environment_metrics.rainfall_1h = 2.57f; + telemetry.variant.environment_metrics.has_rainfall_1h = true; + telemetry.variant.environment_metrics.rainfall_24h = 15.89f; + telemetry.variant.environment_metrics.has_rainfall_24h = true; - // Soil measurements - telemetry.variant.environment_metrics.soil_moisture = 85; - telemetry.variant.environment_metrics.has_soil_moisture = true; - telemetry.variant.environment_metrics.soil_temperature = 18.54f; - telemetry.variant.environment_metrics.has_soil_temperature = true; + // Soil measurements + telemetry.variant.environment_metrics.soil_moisture = 85; + telemetry.variant.environment_metrics.has_soil_moisture = true; + telemetry.variant.environment_metrics.soil_temperature = 18.54f; + telemetry.variant.environment_metrics.has_soil_temperature = true; - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; } // Test TELEMETRY_APP port with device metrics -void test_telemetry_device_metrics_serialization() { - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer)); +void test_telemetry_device_metrics_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify telemetry data - TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end()); - TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber()); + // Verify telemetry data + TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end()); - TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end()); + TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber()); - // Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision - // We verify the numeric values are correct within tolerance + // Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision + // We verify the numeric values are correct within tolerance - delete root; + delete root; } // Test that telemetry environment metrics are properly serialized -void test_telemetry_environment_metrics_serialization() { - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Test key fields that should be present in the serializer - TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); + // Test key fields that should be present in the serializer + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); - // Note: JSON serialization may have float precision limitations - // We focus on verifying numeric accuracy rather than exact string formatting + // Note: JSON serialization may have float precision limitations + // We focus on verifying numeric accuracy rather than exact string formatting - delete root; + delete root; } // Test comprehensive environment metrics coverage -void test_telemetry_environment_metrics_comprehensive() { - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_comprehensive() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Check all 15 originally supported fields - TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); - TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); - TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); - TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); - TEST_ASSERT_TRUE(payload.find("current") != payload.end()); - TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); - TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); - TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); - TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); - TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); + // Check all 15 originally supported fields + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); + TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_TRUE(payload.find("current") != payload.end()); + TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); + TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); + TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); - delete root; + delete root; } // Test for the 7 environment fields that were added to complete coverage -void test_telemetry_environment_metrics_missing_fields() { - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_missing_fields() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Check the 7 fields that were previously missing - TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); + // Check the 7 fields that were previously missing + TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); - TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); - // Note: JSON float serialization may not preserve exact decimal formatting - // We verify the values are numerically correct within tolerance + // Note: JSON float serialization may not preserve exact decimal formatting + // We verify the values are numerically correct within tolerance - delete root; + delete root; } // Test that ALL environment fields are serialized (canary test for forgotten fields) // This test will FAIL if a new environment field is added to the protobuf but not to the serializer -void test_telemetry_environment_metrics_complete_coverage() { - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_complete_coverage() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // ✅ ALL 22 environment fields MUST be present and correct - // If this test fails, it means either: - // 1. A new field was added to the protobuf but not to the serializer - // 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated + // ✅ ALL 22 environment fields MUST be present and correct + // If this test fails, it means either: + // 1. A new field was added to the protobuf but not to the serializer + // 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated - // Basic environment (3 fields) - TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber()); + // Basic environment (3 fields) + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber()); - // Gas and air quality (2 fields) - TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); - TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber()); + // Gas and air quality (2 fields) + TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); + TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber()); - // Power measurements (2 fields) - TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("current") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber()); + // Power measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("current") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber()); - // Light measurements (4 fields) - TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); + // Light measurements (4 fields) + TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); - // Distance measurement (1 field) - TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); + // Distance measurement (1 field) + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); - // Wind measurements (4 fields) - TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); - TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber()); + // Wind measurements (4 fields) + TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); + TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber()); - // Weight measurement (1 field) - TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); + // Weight measurement (1 field) + TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); - // Radiation measurement (1 field) - TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber()); + // Radiation measurement (1 field) + TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber()); - // Rainfall measurements (2 fields) - TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); + // Rainfall measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); - // Soil measurements (2 fields) - TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); - TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); - TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); + // Soil measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); - // Total: 22 environment fields - // This test ensures 100% coverage of environment metrics + // Total: 22 environment fields + // This test ensures 100% coverage of environment metrics - // Note: JSON float serialization precision may vary due to the underlying library - // The important aspect is that all values are numerically accurate within tolerance + // Note: JSON float serialization precision may vary due to the underlying library + // The important aspect is that all values are numerically accurate within tolerance - delete root; + delete root; } // Test that unset environment fields are not present in JSON -void test_telemetry_environment_metrics_unset_fields() { - uint8_t buffer[256]; - size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer)); +void test_telemetry_environment_metrics_unset_fields() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check payload exists - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // With completely empty environment metrics, NO fields should be present - // Only basic telemetry fields like "time" might be present + // With completely empty environment metrics, NO fields should be present + // Only basic telemetry fields like "time" might be present - // All 22 environment fields should be absent (none were set) - TEST_ASSERT_TRUE(payload.find("temperature") == payload.end()); - TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end()); - TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end()); - TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end()); - TEST_ASSERT_TRUE(payload.find("iaq") == payload.end()); - TEST_ASSERT_TRUE(payload.find("voltage") == payload.end()); - TEST_ASSERT_TRUE(payload.find("current") == payload.end()); - TEST_ASSERT_TRUE(payload.find("lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end()); - TEST_ASSERT_TRUE(payload.find("distance") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end()); - TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end()); - TEST_ASSERT_TRUE(payload.find("weight") == payload.end()); - TEST_ASSERT_TRUE(payload.find("radiation") == payload.end()); - TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end()); - TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end()); - TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end()); - TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end()); + // All 22 environment fields should be absent (none were set) + TEST_ASSERT_TRUE(payload.find("temperature") == payload.end()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end()); + TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end()); + TEST_ASSERT_TRUE(payload.find("iaq") == payload.end()); + TEST_ASSERT_TRUE(payload.find("voltage") == payload.end()); + TEST_ASSERT_TRUE(payload.find("current") == payload.end()); + TEST_ASSERT_TRUE(payload.find("lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("distance") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end()); + TEST_ASSERT_TRUE(payload.find("weight") == payload.end()); + TEST_ASSERT_TRUE(payload.find("radiation") == payload.end()); + TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end()); + TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp index e3f0c1cc7..0f3b0bc6d 100644 --- a/test/test_meshpacket_serializer/ports/test_text_message.cpp +++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp @@ -2,97 +2,104 @@ #include // Helper function to test common packet fields and structure -void verify_text_message_packet_structure(const std::string &json, const char *expected_text) { - TEST_ASSERT_TRUE(json.length() > 0); +void verify_text_message_packet_structure(const std::string &json, const char *expected_text) +{ + TEST_ASSERT_TRUE(json.length() > 0); - // Use smart pointer for automatic memory management - std::unique_ptr root(JSON::Parse(json.c_str())); - TEST_ASSERT_NOT_NULL(root.get()); - TEST_ASSERT_TRUE(root->IsObject()); + // Use smart pointer for automatic memory management + std::unique_ptr root(JSON::Parse(json.c_str())); + TEST_ASSERT_NOT_NULL(root.get()); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check basic packet fields - use helper function to reduce duplication - auto check_field = [&](const char *field, uint32_t expected_value) { - auto it = jsonObj.find(field); - TEST_ASSERT_TRUE(it != jsonObj.end()); - TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); - }; + // Check basic packet fields - use helper function to reduce duplication + auto check_field = [&](const char *field, uint32_t expected_value) { + auto it = jsonObj.find(field); + TEST_ASSERT_TRUE(it != jsonObj.end()); + TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); + }; - check_field("from", 0x11223344); - check_field("to", 0x55667788); - check_field("id", 0x9999); + check_field("from", 0x11223344); + check_field("to", 0x55667788); + check_field("id", 0x9999); - // Check message type - auto type_it = jsonObj.find("type"); - TEST_ASSERT_TRUE(type_it != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); + // Check message type + auto type_it = jsonObj.find("type"); + TEST_ASSERT_TRUE(type_it != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); - // Check payload - auto payload_it = jsonObj.find("payload"); - TEST_ASSERT_TRUE(payload_it != jsonObj.end()); - TEST_ASSERT_TRUE(payload_it->second->IsObject()); + // Check payload + auto payload_it = jsonObj.find("payload"); + TEST_ASSERT_TRUE(payload_it != jsonObj.end()); + TEST_ASSERT_TRUE(payload_it->second->IsObject()); - JSONObject payload = payload_it->second->AsObject(); - auto text_it = payload.find("text"); - TEST_ASSERT_TRUE(text_it != payload.end()); - TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); + JSONObject payload = payload_it->second->AsObject(); + auto text_it = payload.find("text"); + TEST_ASSERT_TRUE(text_it != payload.end()); + TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); - // No need for manual delete with smart pointer + // No need for manual delete with smart pointer } // Test TEXT_MESSAGE_APP port -void test_text_message_serialization() { - const char *test_text = "Hello Meshtastic!"; - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); +void test_text_message_serialization() +{ + const char *test_text = "Hello Meshtastic!"; + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - verify_text_message_packet_structure(json, test_text); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, test_text); } // Test with nullptr to check robustness -void test_text_message_serialization_null() { - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); +void test_text_message_serialization_null() +{ + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - verify_text_message_packet_structure(json, ""); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, ""); } // Test TEXT_MESSAGE_APP port with very long message (boundary testing) -void test_text_message_serialization_long_text() { - // Test with actual message size limits - constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit - std::string long_text(MAX_MESSAGE_SIZE, 'A'); +void test_text_message_serialization_long_text() +{ + // Test with actual message size limits + constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit + std::string long_text(MAX_MESSAGE_SIZE, 'A'); - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(long_text.c_str()), long_text.length()); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, + reinterpret_cast(long_text.c_str()), long_text.length()); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - verify_text_message_packet_structure(json, long_text.c_str()); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, long_text.c_str()); } // Test with message over size limit (should fail) -void test_text_message_serialization_oversized() { - constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit - std::string oversized_text(OVERSIZED_MESSAGE, 'B'); +void test_text_message_serialization_oversized() +{ + constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit + std::string oversized_text(OVERSIZED_MESSAGE, 'B'); - meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); + meshtastic_MeshPacket packet = create_test_packet( + meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); - // Should fail or return empty/error - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - // Should only verify first 234 characters for oversized messages - std::string expected_text = oversized_text.substr(0, 234); - verify_text_message_packet_structure(json, expected_text.c_str()); + // Should fail or return empty/error + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + // Should only verify first 234 characters for oversized messages + std::string expected_text = oversized_text.substr(0, 234); + verify_text_message_packet_structure(json, expected_text.c_str()); } // Add test for malformed UTF-8 sequences -void test_text_message_serialization_invalid_utf8() { - const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); +void test_text_message_serialization_invalid_utf8() +{ + const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); - // Should not crash, may produce replacement characters - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + // Should not crash, may produce replacement characters + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); } \ No newline at end of file diff --git a/test/test_meshpacket_serializer/ports/test_waypoint.cpp b/test/test_meshpacket_serializer/ports/test_waypoint.cpp index d2a549fb2..b7e811d70 100644 --- a/test/test_meshpacket_serializer/ports/test_waypoint.cpp +++ b/test/test_meshpacket_serializer/ports/test_waypoint.cpp @@ -1,51 +1,53 @@ #include "../test_helpers.h" -static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size) { - meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero; - waypoint.id = 12345; - waypoint.latitude_i = 374208000; - waypoint.longitude_i = -1221981000; - waypoint.expire = 1609459200 + 3600; // 1 hour from now - strcpy(waypoint.name, "Test Point"); - strcpy(waypoint.description, "Test waypoint description"); +static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero; + waypoint.id = 12345; + waypoint.latitude_i = 374208000; + waypoint.longitude_i = -1221981000; + waypoint.expire = 1609459200 + 3600; // 1 hour from now + strcpy(waypoint.name, "Test Point"); + strcpy(waypoint.description, "Test waypoint description"); - pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); - pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint); - return stream.bytes_written; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint); + return stream.bytes_written; } // Test WAYPOINT_APP port -void test_waypoint_serialization() { - uint8_t buffer[256]; - size_t payload_size = encode_waypoint(buffer, sizeof(buffer)); +void test_waypoint_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_waypoint(buffer, sizeof(buffer)); - meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size); + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size); - std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); - JSONObject jsonObj = root->AsObject(); + JSONObject jsonObj = root->AsObject(); - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str()); + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str()); - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - JSONObject payload = jsonObj["payload"]->AsObject(); + JSONObject payload = jsonObj["payload"]->AsObject(); - // Verify waypoint data - TEST_ASSERT_TRUE(payload.find("id") != payload.end()); - TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber()); + // Verify waypoint data + TEST_ASSERT_TRUE(payload.find("id") != payload.end()); + TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber()); - TEST_ASSERT_TRUE(payload.find("name") != payload.end()); - TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str()); + TEST_ASSERT_TRUE(payload.find("name") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str()); - delete root; + delete root; } diff --git a/test/test_meshpacket_serializer/test_helpers.h b/test/test_meshpacket_serializer/test_helpers.h index 7aa4ac695..12245b85d 100644 --- a/test/test_meshpacket_serializer/test_helpers.h +++ b/test/test_meshpacket_serializer/test_helpers.h @@ -12,37 +12,38 @@ // Helper function to create a test packet with the given port and payload static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size, - int payload_variant = meshtastic_MeshPacket_decoded_tag) { - meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + int payload_variant = meshtastic_MeshPacket_decoded_tag) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; - packet.id = 0x9999; - packet.from = 0x11223344; - packet.to = 0x55667788; - packet.channel = 0; - packet.hop_limit = 3; - packet.want_ack = false; - packet.priority = meshtastic_MeshPacket_Priority_UNSET; - packet.rx_time = 1609459200; - packet.rx_snr = 10.5f; - packet.hop_start = 3; - packet.rx_rssi = -85; - packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; + packet.id = 0x9999; + packet.from = 0x11223344; + packet.to = 0x55667788; + packet.channel = 0; + packet.hop_limit = 3; + packet.want_ack = false; + packet.priority = meshtastic_MeshPacket_Priority_UNSET; + packet.rx_time = 1609459200; + packet.rx_snr = 10.5f; + packet.hop_start = 3; + packet.rx_rssi = -85; + packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; - // Set decoded variant - packet.which_payload_variant = payload_variant; - packet.decoded.portnum = port; - if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) { - packet.encrypted.size = payload_size; - memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); - } - memcpy(packet.decoded.payload.bytes, payload, payload_size); - packet.decoded.payload.size = payload_size; - packet.decoded.want_response = false; - packet.decoded.dest = 0x55667788; - packet.decoded.source = 0x11223344; - packet.decoded.request_id = 0; - packet.decoded.reply_id = 0; - packet.decoded.emoji = 0; + // Set decoded variant + packet.which_payload_variant = payload_variant; + packet.decoded.portnum = port; + if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) { + packet.encrypted.size = payload_size; + memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); + } + memcpy(packet.decoded.payload.bytes, payload, payload_size); + packet.decoded.payload.size = payload_size; + packet.decoded.want_response = false; + packet.decoded.dest = 0x55667788; + packet.decoded.source = 0x11223344; + packet.decoded.request_id = 0; + packet.decoded.reply_id = 0; + packet.decoded.emoji = 0; - return packet; + return packet; } diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp index bc4439df7..484db8d74 100644 --- a/test/test_meshpacket_serializer/test_serializer.cpp +++ b/test/test_meshpacket_serializer/test_serializer.cpp @@ -20,38 +20,42 @@ void test_telemetry_environment_metrics_unset_fields(); void test_encrypted_packet_serialization(); void test_empty_encrypted_packet(); -void setup() { - UNITY_BEGIN(); +void setup() +{ + UNITY_BEGIN(); - // Text message tests - RUN_TEST(test_text_message_serialization); - RUN_TEST(test_text_message_serialization_null); - RUN_TEST(test_text_message_serialization_long_text); - RUN_TEST(test_text_message_serialization_oversized); - RUN_TEST(test_text_message_serialization_invalid_utf8); + // Text message tests + RUN_TEST(test_text_message_serialization); + RUN_TEST(test_text_message_serialization_null); + RUN_TEST(test_text_message_serialization_long_text); + RUN_TEST(test_text_message_serialization_oversized); + RUN_TEST(test_text_message_serialization_invalid_utf8); - // Position tests - RUN_TEST(test_position_serialization); + // Position tests + RUN_TEST(test_position_serialization); - // Nodeinfo tests - RUN_TEST(test_nodeinfo_serialization); + // Nodeinfo tests + RUN_TEST(test_nodeinfo_serialization); - // Waypoint tests - RUN_TEST(test_waypoint_serialization); + // Waypoint tests + RUN_TEST(test_waypoint_serialization); - // Telemetry tests - RUN_TEST(test_telemetry_device_metrics_serialization); - RUN_TEST(test_telemetry_environment_metrics_serialization); - RUN_TEST(test_telemetry_environment_metrics_comprehensive); - RUN_TEST(test_telemetry_environment_metrics_missing_fields); - RUN_TEST(test_telemetry_environment_metrics_complete_coverage); - RUN_TEST(test_telemetry_environment_metrics_unset_fields); + // Telemetry tests + RUN_TEST(test_telemetry_device_metrics_serialization); + RUN_TEST(test_telemetry_environment_metrics_serialization); + RUN_TEST(test_telemetry_environment_metrics_comprehensive); + RUN_TEST(test_telemetry_environment_metrics_missing_fields); + RUN_TEST(test_telemetry_environment_metrics_complete_coverage); + RUN_TEST(test_telemetry_environment_metrics_unset_fields); - // Encrypted packet test - RUN_TEST(test_encrypted_packet_serialization); - RUN_TEST(test_empty_encrypted_packet); + // Encrypted packet test + RUN_TEST(test_encrypted_packet_serialization); + RUN_TEST(test_empty_encrypted_packet); - UNITY_END(); + UNITY_END(); } -void loop() { delay(1000); } +void loop() +{ + delay(1000); +} diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 5c84cee49..a566dabf7 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -33,175 +33,194 @@ #define IS_RUNNING_TESTS 0 #endif -namespace { +namespace +{ // Minimal router needed to receive messages from MQTT. -class MockRouter : public Router { -public: - ~MockRouter() { - // cryptLock is created in the constructor for Router. - delete cryptLock; - cryptLock = NULL; - } - void enqueueReceivedMessage(meshtastic_MeshPacket *p) override { - packets_.emplace_back(*p); - packetPool.release(p); - } - std::list packets_; // Packets received by the Router. +class MockRouter : public Router +{ + public: + ~MockRouter() + { + // cryptLock is created in the constructor for Router. + delete cryptLock; + cryptLock = NULL; + } + void enqueueReceivedMessage(meshtastic_MeshPacket *p) override + { + packets_.emplace_back(*p); + packetPool.release(p); + } + std::list packets_; // Packets received by the Router. }; // Minimal MeshService needed to receive messages from MQTT for testing PKI channel. -class MockMeshService : public MeshService { -public: - void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override { - messages_.emplace_back(*m); - releaseMqttClientProxyMessageToPool(m); - } - void sendClientNotification(meshtastic_ClientNotification *n) override { - notifications_.emplace_back(*n); - releaseClientNotificationToPool(n); - } - std::list messages_; // Messages received from the MeshService. - std::list notifications_; // Notifications received from the MeshService. +class MockMeshService : public MeshService +{ + public: + void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override + { + messages_.emplace_back(*m); + releaseMqttClientProxyMessageToPool(m); + } + void sendClientNotification(meshtastic_ClientNotification *n) override + { + notifications_.emplace_back(*n); + releaseClientNotificationToPool(n); + } + std::list messages_; // Messages received from the MeshService. + std::list notifications_; // Notifications received from the MeshService. }; // Minimal NodeDB needed to return values from getMeshNode. -class MockNodeDB : public NodeDB { -public: - meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; } - meshtastic_NodeInfoLite emptyNode = {}; +class MockNodeDB : public NodeDB +{ + public: + meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; } + meshtastic_NodeInfoLite emptyNode = {}; }; // Minimal RoutingModule needed to return values from sendAckNak. -class MockRoutingModule : public RoutingModule { -public: - void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, - bool ackWantsAck = false) override { - ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit); - } - std::list> ackNacks_; // ackNacks received by the RoutingModule. +class MockRoutingModule : public RoutingModule +{ + public: + void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, + bool ackWantsAck = false) override + { + ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit); + } + std::list> + ackNacks_; // ackNacks received by the RoutingModule. }; // A WiFi client used by the MQTT::PubSubClient. Implements a minimal pub/sub server. // There isn't an easy way to mock PubSubClient due to it not having virtual methods, so we mock using // the WiFiClinet that PubSubClient uses. -class MockPubSubServer : public WiFiClient { -public: - static constexpr char kTextTopic[] = "TextTopic"; - uint8_t connected() override { return connected_; } - void flush() override {} - IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); } - void stop() override { connected_ = false; } +class MockPubSubServer : public WiFiClient +{ + public: + static constexpr char kTextTopic[] = "TextTopic"; + uint8_t connected() override { return connected_; } + void flush() override {} + IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); } + void stop() override { connected_ = false; } - int connect(IPAddress ip, uint16_t port) override { - port_ = port; - if (refuseConnection_) - return 0; - connected_ = true; - return 1; - } - int connect(const char *host, uint16_t port) override { - host_ = host; - port_ = port; - if (refuseConnection_) - return 0; - connected_ = true; - return 1; - } - - int available() override { - if (buffer_.empty()) - return 0; - return buffer_.front().size(); - } - - int read() override { - assert(available()); - std::string &front = buffer_.front(); - char ch = front[0]; - front = front.substr(1, front.size()); - if (front.empty()) - buffer_.pop_front(); - return ch; - } - - size_t write(uint8_t data) override { return write(&data, 1); } - size_t write(const uint8_t *buf, size_t size) override { - command_ += std::string(reinterpret_cast(buf), size); - if (command_.size() < 2) - return size; - const int len = (uint8_t)command_[1] + 2; - if (command_.size() < len) - return size; - handleCommand(command_[0], command_.substr(2, len)); - command_ = command_.substr(len, command_.size()); - return size; - } - - // The pub/sub "server". - // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf - void handleCommand(uint8_t header, std::string_view message) { - switch (header & 0xf0) { - case MQTTCONNECT: - LOG_DEBUG("MQTTCONNECT"); - buffer_.push_back(std::string("\x20\x02\x00\x00", 4)); - break; - - case MQTTSUBSCRIBE: { - LOG_DEBUG("MQTTSUBSCRIBE"); - assert(message.size() >= 5); - message.remove_prefix(2); // skip messageId - - while (message.size() >= 3) { - const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; - message.remove_prefix(2); - - assert(message.size() >= topicSize + 1); - std::string topic(message.data(), topicSize); - message.remove_prefix(topicSize + 1); - - LOG_DEBUG("Subscribed to topic: %s", topic.c_str()); - subscriptions_.insert(std::move(topic)); - } - break; + int connect(IPAddress ip, uint16_t port) override + { + port_ = port; + if (refuseConnection_) + return 0; + connected_ = true; + return 1; + } + int connect(const char *host, uint16_t port) override + { + host_ = host; + port_ = port; + if (refuseConnection_) + return 0; + connected_ = true; + return 1; } - case MQTTPINGREQ: - LOG_DEBUG("MQTTPINGREQ"); - buffer_.push_back(std::string("\xd0\x00", 2)); - break; - - case MQTTPUBLISH: { - LOG_DEBUG("MQTTPUBLISH"); - assert(message.size() >= 3); - const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; - message.remove_prefix(2); - - assert(message.size() >= topicSize); - std::string topic(message.data(), topicSize); - message.remove_prefix(topicSize); - - if (topic == kTextTopic) { - published_.emplace_back(std::move(topic), std::string(message.data(), message.size())); - } else { - published_.emplace_back(std::move(topic), DecodedServiceEnvelope(reinterpret_cast(message.data()), message.size())); - } - break; + int available() override + { + if (buffer_.empty()) + return 0; + return buffer_.front().size(); } - } - } - bool connected_ = false; - bool refuseConnection_ = false; // Simulate a failed connection. - uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server. - std::string host_; // Requested host. - uint16_t port_; // Requested port. - std::list buffer_; // Buffer of messages for the pubSub client to receive. - std::string command_; // Current command received from the pubSub client. - std::set subscriptions_; // Topics that the pubSub client has subscribed to. - std::list>> - published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name - // and either a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope. + int read() override + { + assert(available()); + std::string &front = buffer_.front(); + char ch = front[0]; + front = front.substr(1, front.size()); + if (front.empty()) + buffer_.pop_front(); + return ch; + } + + size_t write(uint8_t data) override { return write(&data, 1); } + size_t write(const uint8_t *buf, size_t size) override + { + command_ += std::string(reinterpret_cast(buf), size); + if (command_.size() < 2) + return size; + const int len = (uint8_t)command_[1] + 2; + if (command_.size() < len) + return size; + handleCommand(command_[0], command_.substr(2, len)); + command_ = command_.substr(len, command_.size()); + return size; + } + + // The pub/sub "server". + // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf + void handleCommand(uint8_t header, std::string_view message) + { + switch (header & 0xf0) { + case MQTTCONNECT: + LOG_DEBUG("MQTTCONNECT"); + buffer_.push_back(std::string("\x20\x02\x00\x00", 4)); + break; + + case MQTTSUBSCRIBE: { + LOG_DEBUG("MQTTSUBSCRIBE"); + assert(message.size() >= 5); + message.remove_prefix(2); // skip messageId + + while (message.size() >= 3) { + const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; + message.remove_prefix(2); + + assert(message.size() >= topicSize + 1); + std::string topic(message.data(), topicSize); + message.remove_prefix(topicSize + 1); + + LOG_DEBUG("Subscribed to topic: %s", topic.c_str()); + subscriptions_.insert(std::move(topic)); + } + break; + } + + case MQTTPINGREQ: + LOG_DEBUG("MQTTPINGREQ"); + buffer_.push_back(std::string("\xd0\x00", 2)); + break; + + case MQTTPUBLISH: { + LOG_DEBUG("MQTTPUBLISH"); + assert(message.size() >= 3); + const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; + message.remove_prefix(2); + + assert(message.size() >= topicSize); + std::string topic(message.data(), topicSize); + message.remove_prefix(topicSize); + + if (topic == kTextTopic) { + published_.emplace_back(std::move(topic), std::string(message.data(), message.size())); + } else { + published_.emplace_back( + std::move(topic), DecodedServiceEnvelope(reinterpret_cast(message.data()), message.size())); + } + break; + } + } + } + + bool connected_ = false; + bool refuseConnection_ = false; // Simulate a failed connection. + uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server. + std::string host_; // Requested host. + uint16_t port_; // Requested port. + std::list buffer_; // Buffer of messages for the pubSub client to receive. + std::string command_; // Current command received from the pubSub client. + std::set subscriptions_; // Topics that the pubSub client has subscribed to. + std::list>> + published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name and either + // a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope. }; // Instances of our mocks. @@ -214,61 +233,71 @@ MockRouter *mockRouter; // Keep running the loop until either conditionMet returns true or 4 seconds elapse. // Returns true if conditionMet returns true, returns false on timeout. -bool loopUntil(std::function conditionMet) { - long start = millis(); - while (start + 4000 > millis()) { - long delayMsec = concurrency::mainController.runOrDelay(); - if (conditionMet()) - return true; - concurrency::mainDelay.delay(std::min(delayMsec, 5L)); - } - return false; +bool loopUntil(std::function conditionMet) +{ + long start = millis(); + while (start + 4000 > millis()) { + long delayMsec = concurrency::mainController.runOrDelay(); + if (conditionMet()) + return true; + concurrency::mainDelay.delay(std::min(delayMsec, 5L)); + } + return false; } // Used to access protected/private members of MQTT for unit testing. -class MQTTUnitTest : public MQTT { -public: - MQTTUnitTest() : MQTT(std::make_unique()) { pubsub = reinterpret_cast(mqttClient.get()); } - ~MQTTUnitTest() { - // Needed because WiFiClient does not have a virtual destructor. - mqttClient.release(); - delete pubsub; - } - using MQTT::isValidConfig; - using MQTT::reconnect; - int queueSize() { return mqttQueue.numUsed(); } - void reportToMap(std::optional precision = std::nullopt) { - if (precision.has_value()) - map_position_precision = precision.value(); - map_publish_interval_msecs = 0; - perhapsReportToMap(); - } - void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test") { - std::stringstream topic; - topic << "msh/2/e/" << channel << "/!" << gateway; - const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), - .channel_id = const_cast(channel.c_str()), - .gateway_id = const_cast(gateway.c_str())}; - uint8_t bytes[256]; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); - mqttCallback(const_cast(topic.str().c_str()), bytes, numBytes); - } - static void restart() { - if (mqtt != NULL) { - delete mqtt; - mqtt = unitTest = NULL; +class MQTTUnitTest : public MQTT +{ + public: + MQTTUnitTest() : MQTT(std::make_unique()) + { + pubsub = reinterpret_cast(mqttClient.get()); } - mqtt = unitTest = new MQTTUnitTest(); - mqtt->start(); + ~MQTTUnitTest() + { + // Needed because WiFiClient does not have a virtual destructor. + mqttClient.release(); + delete pubsub; + } + using MQTT::isValidConfig; + using MQTT::reconnect; + int queueSize() { return mqttQueue.numUsed(); } + void reportToMap(std::optional precision = std::nullopt) + { + if (precision.has_value()) + map_position_precision = precision.value(); + map_publish_interval_msecs = 0; + perhapsReportToMap(); + } + void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test") + { + std::stringstream topic; + topic << "msh/2/e/" << channel << "/!" << gateway; + const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), + .channel_id = const_cast(channel.c_str()), + .gateway_id = const_cast(gateway.c_str())}; + uint8_t bytes[256]; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); + mqttCallback(const_cast(topic.str().c_str()), bytes, numBytes); + } + static void restart() + { + if (mqtt != NULL) { + delete mqtt; + mqtt = unitTest = NULL; + } + mqtt = unitTest = new MQTTUnitTest(); + mqtt->start(); - if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { - loopUntil([] { return true; }); // Loop once - return; + if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { + loopUntil([] { return true; }); // Loop once + return; + } + // Wait for MQTT to subscribe to all topics. + TEST_ASSERT_TRUE(loopUntil( + [] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); } - // Wait for MQTT to subscribe to all topics. - TEST_ASSERT_TRUE(loopUntil([] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); - } - PubSubClient &getPubSub() { return pubSub; } + PubSubClient &getPubSub() { return pubSub; } }; // Packets used in unit tests. @@ -289,561 +318,616 @@ const meshtastic_MeshPacket encrypted = { } // namespace // Initialize mocks and configuration before running each test. -void setUp(void) { - moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true}; - moduleConfig.mqtt.map_report_settings = - meshtastic_ModuleConfig_MapReportSettings{.publish_interval_secs = 0, .position_precision = 14, .should_report_location = true}; - channelFile.channels[0] = meshtastic_Channel{ - .index = 0, - .has_settings = true, - .settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true}, - .role = meshtastic_Channel_Role_PRIMARY, - }; - channelFile.channels_count = 1; - owner = meshtastic_User{.id = "!12345678"}; - myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic - localPosition = meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7}; +void setUp(void) +{ + moduleConfig.mqtt = + meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true}; + moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{ + .publish_interval_secs = 0, .position_precision = 14, .should_report_location = true}; + channelFile.channels[0] = meshtastic_Channel{ + .index = 0, + .has_settings = true, + .settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true}, + .role = meshtastic_Channel_Role_PRIMARY, + }; + channelFile.channels_count = 1; + owner = meshtastic_User{.id = "!12345678"}; + myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic + localPosition = + meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7}; - router = mockRouter = new MockRouter(); - service = mockMeshService = new MockMeshService(); - routingModule = mockRoutingModule = new MockRoutingModule(); - MQTTUnitTest::restart(); + router = mockRouter = new MockRouter(); + service = mockMeshService = new MockMeshService(); + routingModule = mockRoutingModule = new MockRoutingModule(); + MQTTUnitTest::restart(); } // Deinitialize all objects created in setUp. -void tearDown(void) { - delete unitTest; - mqtt = unitTest = NULL; - delete mockRoutingModule; - routingModule = mockRoutingModule = NULL; - delete mockMeshService; - service = mockMeshService = NULL; - delete mockRouter; - router = mockRouter = NULL; +void tearDown(void) +{ + delete unitTest; + mqtt = unitTest = NULL; + delete mockRoutingModule; + routingModule = mockRoutingModule = NULL; + delete mockMeshService; + service = mockMeshService = NULL; + delete mockRouter; + router = mockRouter = NULL; } // Test that the decoded MeshPacket is published when encryption_enabled = false. -void test_sendDirectlyConnectedDecoded(void) { - mqtt->onSend(encrypted, decoded, 0); +void test_sendDirectlyConnectedDecoded(void) +{ + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - const DecodedServiceEnvelope &env = std::get(payload); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(decoded.id, env.packet->id); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + const DecodedServiceEnvelope &env = std::get(payload); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Test that the encrypted MeshPacket is published when encryption_enabled = true. -void test_sendDirectlyConnectedEncrypted(void) { - moduleConfig.mqtt.encryption_enabled = true; +void test_sendDirectlyConnectedEncrypted(void) +{ + moduleConfig.mqtt.encryption_enabled = true; - mqtt->onSend(encrypted, decoded, 0); + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - const DecodedServiceEnvelope &env = std::get(payload); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + const DecodedServiceEnvelope &env = std::get(payload); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); } // Verify that the decoded MeshPacket is proxied through the MeshService when encryption_enabled = false. -void test_proxyToMeshServiceDecoded(void) { - moduleConfig.mqtt.proxy_to_client_enabled = true; - MQTTUnitTest::restart(); +void test_proxyToMeshServiceDecoded(void) +{ + moduleConfig.mqtt.proxy_to_client_enabled = true; + MQTTUnitTest::restart(); - mqtt->onSend(encrypted, decoded, 0); + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); - const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(decoded.id, env.packet->id); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); + const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Verify that the encrypted MeshPacket is proxied through the MeshService when encryption_enabled = true. -void test_proxyToMeshServiceEncrypted(void) { - moduleConfig.mqtt.proxy_to_client_enabled = true; - moduleConfig.mqtt.encryption_enabled = true; - MQTTUnitTest::restart(); +void test_proxyToMeshServiceEncrypted(void) +{ + moduleConfig.mqtt.proxy_to_client_enabled = true; + moduleConfig.mqtt.encryption_enabled = true; + MQTTUnitTest::restart(); - mqtt->onSend(encrypted, decoded, 0); + mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); - const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); + const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); } // A packet without the OK to MQTT bit set should not be published to a public server. -void test_dontMqttMeOnPublicServer(void) { - meshtastic_MeshPacket p = decoded; - p.decoded.bitfield = 0; - p.decoded.has_bitfield = 0; +void test_dontMqttMeOnPublicServer(void) +{ + meshtastic_MeshPacket p = decoded; + p.decoded.bitfield = 0; + p.decoded.has_bitfield = 0; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); } // A packet without the OK to MQTT bit set should be published to a private server. -void test_okToMqttOnPrivateServer(void) { - // Cause a disconnect. - pubsub->connected_ = false; - pubsub->refuseConnection_ = true; - TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); +void test_okToMqttOnPrivateServer(void) +{ + // Cause a disconnect. + pubsub->connected_ = false; + pubsub->refuseConnection_ = true; + TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); - // Use 127.0.0.1 for the server's IP. - pubsub->ipAddress_ = 0x7f000001; + // Use 127.0.0.1 for the server's IP. + pubsub->ipAddress_ = 0x7f000001; - // Reconnect. - pubsub->refuseConnection_ = false; - TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); })); + // Reconnect. + pubsub->refuseConnection_ = false; + TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); })); - // Send the same packet as test_dontMqttMeOnPublicServer. - meshtastic_MeshPacket p = decoded; - p.decoded.bitfield = 0; - p.decoded.has_bitfield = 0; + // Send the same packet as test_dontMqttMeOnPublicServer. + meshtastic_MeshPacket p = decoded; + p.decoded.bitfield = 0; + p.decoded.has_bitfield = 0; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); } // Range tests messages are not uplinked to the default server. -void test_noRangeTestAppOnDefaultServer(void) { - meshtastic_MeshPacket p = decoded; - p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP; +void test_noRangeTestAppOnDefaultServer(void) +{ + meshtastic_MeshPacket p = decoded; + p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); } // Detection sensor messages are not uplinked to the default server. -void test_noDetectionSensorAppOnDefaultServer(void) { - meshtastic_MeshPacket p = decoded; - p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP; +void test_noDetectionSensorAppOnDefaultServer(void) +{ + meshtastic_MeshPacket p = decoded; + p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP; - mqtt->onSend(encrypted, p, 0); + mqtt->onSend(encrypted, p, 0); - TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); } // Test that a MeshPacket is queued while the MQTT server is disconnected. -void test_sendQueued(void) { - // Cause a disconnect. - pubsub->connected_ = false; - pubsub->refuseConnection_ = true; - TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); +void test_sendQueued(void) +{ + // Cause a disconnect. + pubsub->connected_ = false; + pubsub->refuseConnection_ = true; + TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); - // Send while disconnected. - mqtt->onSend(encrypted, decoded, 0); - TEST_ASSERT_EQUAL(1, unitTest->queueSize()); - TEST_ASSERT_TRUE(pubsub->published_.empty()); - TEST_ASSERT_FALSE(unitTest->getPubSub().connected()); + // Send while disconnected. + mqtt->onSend(encrypted, decoded, 0); + TEST_ASSERT_EQUAL(1, unitTest->queueSize()); + TEST_ASSERT_TRUE(pubsub->published_.empty()); + TEST_ASSERT_FALSE(unitTest->getPubSub().connected()); - // Allow reconnect to happen. Expect to see the packet published now. - pubsub->refuseConnection_ = false; - TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); })); + // Allow reconnect to happen. Expect to see the packet published now. + pubsub->refuseConnection_ = false; + TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); })); - TEST_ASSERT_EQUAL(0, unitTest->queueSize()); - const auto &[topic, payload] = pubsub->published_.front(); - const DecodedServiceEnvelope &env = std::get(payload); - TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); - TEST_ASSERT_TRUE(env.validDecode); - TEST_ASSERT_EQUAL(decoded.id, env.packet->id); + TEST_ASSERT_EQUAL(0, unitTest->queueSize()); + const auto &[topic, payload] = pubsub->published_.front(); + const DecodedServiceEnvelope &env = std::get(payload); + TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); + TEST_ASSERT_TRUE(env.validDecode); + TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Verify reconnecting with the proxy enabled does not reconnect to a MQTT server. -void test_reconnectProxyDoesNotReconnectMqtt(void) { - moduleConfig.mqtt.proxy_to_client_enabled = true; - MQTTUnitTest::restart(); +void test_reconnectProxyDoesNotReconnectMqtt(void) +{ + moduleConfig.mqtt.proxy_to_client_enabled = true; + MQTTUnitTest::restart(); - unitTest->reconnect(); + unitTest->reconnect(); - TEST_ASSERT_FALSE(pubsub->connected_); + TEST_ASSERT_FALSE(pubsub->connected_); } // Test receiving an empty MeshPacket on a subscribed topic. -void test_receiveEmptyMeshPacket(void) { - unitTest->publish(NULL); +void test_receiveEmptyMeshPacket(void) +{ + unitTest->publish(NULL); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Test receiving a decoded MeshPacket on a subscribed topic. -void test_receiveDecodedProto(void) { - unitTest->publish(&decoded); +void test_receiveDecodedProto(void) +{ + unitTest->publish(&decoded); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(decoded.id, p.id); - TEST_ASSERT_TRUE(p.via_mqtt); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(decoded.id, p.id); + TEST_ASSERT_TRUE(p.via_mqtt); } // Test receiving a decoded MeshPacket from the phone proxy. -void test_receiveDecodedProtoFromProxy(void) { - const meshtastic_ServiceEnvelope env = {.packet = const_cast(&decoded), .channel_id = "test", .gateway_id = "!87654321"}; - meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; - strcat(message.topic, "msh/2/e/test/!87654321"); - message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - message.payload_variant.data.size = - pb_encode_to_bytes(message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env); +void test_receiveDecodedProtoFromProxy(void) +{ + const meshtastic_ServiceEnvelope env = { + .packet = const_cast(&decoded), .channel_id = "test", .gateway_id = "!87654321"}; + meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; + strcat(message.topic, "msh/2/e/test/!87654321"); + message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; + message.payload_variant.data.size = pb_encode_to_bytes( + message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env); - mqtt->onClientProxyReceive(message); + mqtt->onClientProxyReceive(message); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(decoded.id, p.id); - TEST_ASSERT_TRUE(p.via_mqtt); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(decoded.id, p.id); + TEST_ASSERT_TRUE(p.via_mqtt); } // Properly handles the case where the received message is empty. -void test_receiveEmptyDataFromProxy(void) { - meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; - message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; +void test_receiveEmptyDataFromProxy(void) +{ + meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; + message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - mqtt->onClientProxyReceive(message); + mqtt->onClientProxyReceive(message); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Packets should be ignored if downlink is not enabled. -void test_receiveWithoutChannelDownlink(void) { - channelFile.channels[0].settings.downlink_enabled = false; +void test_receiveWithoutChannelDownlink(void) +{ + channelFile.channels[0].settings.downlink_enabled = false; - unitTest->publish(&decoded); + unitTest->publish(&decoded); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Test receiving an encrypted MeshPacket on the PKI topic. -void test_receiveEncryptedPKITopicToUs(void) { - meshtastic_MeshPacket e = encrypted; - e.to = myNodeInfo.my_node_num; +void test_receiveEncryptedPKITopicToUs(void) +{ + meshtastic_MeshPacket e = encrypted; + e.to = myNodeInfo.my_node_num; - unitTest->publish(&e, "!87654321", "PKI"); + unitTest->publish(&e, "!87654321", "PKI"); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(encrypted.id, p.id); - TEST_ASSERT_TRUE(p.via_mqtt); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(encrypted.id, p.id); + TEST_ASSERT_TRUE(p.via_mqtt); } // Should ignore messages published to MQTT by this gateway. -void test_receiveIgnoresOwnPublishedMessages(void) { - unitTest->publish(&decoded, nodeDB->getNodeId().c_str()); +void test_receiveIgnoresOwnPublishedMessages(void) +{ + unitTest->publish(&decoded, nodeDB->getNodeId().c_str()); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Considers receiving one of our packets an acknowledgement of it being sent. -void test_receiveAcksOwnSentMessages(void) { - meshtastic_MeshPacket p = decoded; - p.from = myNodeInfo.my_node_num; +void test_receiveAcksOwnSentMessages(void) +{ + meshtastic_MeshPacket p = decoded; + p.from = myNodeInfo.my_node_num; - unitTest->publish(&p, nodeDB->getNodeId().c_str()); + unitTest->publish(&p, nodeDB->getNodeId().c_str()); - // FIXME: Better assertion for this test - // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); - // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); - // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); - // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); - // TEST_ASSERT_EQUAL(p.id, idFrom); + // FIXME: Better assertion for this test + // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); + // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); + // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); + // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); + // TEST_ASSERT_EQUAL(p.id, idFrom); } // Should ignore our own messages from MQTT that were heard by other nodes. -void test_receiveIgnoresSentMessagesFromOthers(void) { - meshtastic_MeshPacket p = decoded; - p.from = myNodeInfo.my_node_num; +void test_receiveIgnoresSentMessagesFromOthers(void) +{ + meshtastic_MeshPacket p = decoded; + p.from = myNodeInfo.my_node_num; - unitTest->publish(&p); + unitTest->publish(&p); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Decoded MQTT messages should be ignored when encryption is enabled. -void test_receiveIgnoresDecodedWhenEncryptionEnabled(void) { - moduleConfig.mqtt.encryption_enabled = true; +void test_receiveIgnoresDecodedWhenEncryptionEnabled(void) +{ + moduleConfig.mqtt.encryption_enabled = true; - unitTest->publish(&decoded); + unitTest->publish(&decoded); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Non-encrypted messages for the Admin App should be ignored. -void test_receiveIgnoresDecodedAdminApp(void) { - meshtastic_MeshPacket p = decoded; - p.decoded.portnum = meshtastic_PortNum_ADMIN_APP; +void test_receiveIgnoresDecodedAdminApp(void) +{ + meshtastic_MeshPacket p = decoded; + p.decoded.portnum = meshtastic_PortNum_ADMIN_APP; - unitTest->publish(&p); + unitTest->publish(&p); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Only the same fields that are transmitted over LoRa should be set in MQTT messages. -void test_receiveIgnoresUnexpectedFields(void) { - meshtastic_MeshPacket input = decoded; - input.rx_snr = 10; - input.rx_rssi = 20; +void test_receiveIgnoresUnexpectedFields(void) +{ + meshtastic_MeshPacket input = decoded; + input.rx_snr = 10; + input.rx_rssi = 20; - unitTest->publish(&input); + unitTest->publish(&input); - TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); - const meshtastic_MeshPacket &p = mockRouter->packets_.front(); - TEST_ASSERT_EQUAL(0, p.rx_snr); - TEST_ASSERT_EQUAL(0, p.rx_rssi); + TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); + const meshtastic_MeshPacket &p = mockRouter->packets_.front(); + TEST_ASSERT_EQUAL(0, p.rx_snr); + TEST_ASSERT_EQUAL(0, p.rx_rssi); } // Messages with an invalid hop_limit are ignored. -void test_receiveIgnoresInvalidHopLimit(void) { - meshtastic_MeshPacket p = decoded; - p.hop_limit = 10; +void test_receiveIgnoresInvalidHopLimit(void) +{ + meshtastic_MeshPacket p = decoded; + p.hop_limit = 10; - unitTest->publish(&p); + unitTest->publish(&p); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Publishing to a text channel. -void test_publishTextMessageDirect(void) { - TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); +void test_publishTextMessageDirect(void) +{ + TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - TEST_ASSERT_EQUAL_STRING("payload", std::get(payload).c_str()); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + TEST_ASSERT_EQUAL_STRING("payload", std::get(payload).c_str()); } // Publishing to a text channel via the MQTT client proxy. -void test_publishTextMessageWithProxy(void) { - moduleConfig.mqtt.proxy_to_client_enabled = true; +void test_publishTextMessageWithProxy(void) +{ + moduleConfig.mqtt.proxy_to_client_enabled = true; - TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); + TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant); - TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant); + TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text); } // Helper method to verify the expected latitude/longitude was received. -void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude) { - TEST_ASSERT_TRUE(env.validDecode); - const meshtastic_MeshPacket &p = *env.packet; - TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to); - TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant); - TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum); +void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude) +{ + TEST_ASSERT_TRUE(env.validDecode); + const meshtastic_MeshPacket &p = *env.packet; + TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to); + TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant); + TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum); - meshtastic_MapReport mapReport; - TEST_ASSERT_TRUE(pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport)); - TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i); - TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i); + meshtastic_MapReport mapReport; + TEST_ASSERT_TRUE( + pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport)); + TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i); + TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i); } // Map reporting defaults to an imprecise location. -void test_reportToMapDefaultImprecise(void) { - unitTest->reportToMap(); +void test_reportToMapDefaultImprecise(void) +{ + unitTest->reportToMap(); - TEST_ASSERT_EQUAL(1, pubsub->published_.size()); - const auto &[topic, payload] = pubsub->published_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str()); + TEST_ASSERT_EQUAL(1, pubsub->published_.size()); + const auto &[topic, payload] = pubsub->published_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str()); } // Location is sent over the phone proxy. -void test_reportToMapImpreciseProxied(void) { - moduleConfig.mqtt.proxy_to_client_enabled = true; - MQTTUnitTest::restart(); +void test_reportToMapImpreciseProxied(void) +{ + moduleConfig.mqtt.proxy_to_client_enabled = true; + MQTTUnitTest::restart(); - unitTest->reportToMap(/*precision=*/14); + unitTest->reportToMap(/*precision=*/14); - TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); - const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); - TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic); - TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); - const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); + TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); + const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); + TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic); + TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); + const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); } // isUsingDefaultServer returns true when using the default server. -void test_usingDefaultServer(void) { TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } +void test_usingDefaultServer(void) +{ + TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); +} // isUsingDefaultServer returns true when using the default server and a port. -void test_usingDefaultServerWithPort(void) { - std::string server = default_mqtt_address; - server += ":1883"; - strcpy(moduleConfig.mqtt.address, server.c_str()); - MQTTUnitTest::restart(); +void test_usingDefaultServerWithPort(void) +{ + std::string server = default_mqtt_address; + server += ":1883"; + strcpy(moduleConfig.mqtt.address, server.c_str()); + MQTTUnitTest::restart(); - TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); + TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns true when using the default server and invalid port. -void test_usingDefaultServerWithInvalidPort(void) { - std::string server = default_mqtt_address; - server += ":invalid"; - strcpy(moduleConfig.mqtt.address, server.c_str()); - MQTTUnitTest::restart(); +void test_usingDefaultServerWithInvalidPort(void) +{ + std::string server = default_mqtt_address; + server += ":invalid"; + strcpy(moduleConfig.mqtt.address, server.c_str()); + MQTTUnitTest::restart(); - TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); + TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns false when not using the default server. -void test_usingCustomServer(void) { - strcpy(moduleConfig.mqtt.address, "custom"); - MQTTUnitTest::restart(); +void test_usingCustomServer(void) +{ + strcpy(moduleConfig.mqtt.address, "custom"); + MQTTUnitTest::restart(); - TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer()); + TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer()); } // Test that isEnabled returns true the MQTT module is enabled. -void test_enabled(void) { TEST_ASSERT_TRUE(mqtt->isEnabled()); } +void test_enabled(void) +{ + TEST_ASSERT_TRUE(mqtt->isEnabled()); +} // Test that isEnabled returns false the MQTT module not enabled. -void test_disabled(void) { - moduleConfig.mqtt.enabled = false; - MQTTUnitTest::restart(); +void test_disabled(void) +{ + moduleConfig.mqtt.enabled = false; + MQTTUnitTest::restart(); - TEST_ASSERT_FALSE(mqtt->isEnabled()); + TEST_ASSERT_FALSE(mqtt->isEnabled()); } // Subscriptions contain the moduleConfig.mqtt.root prefix. -void test_customMqttRoot(void) { - strcpy(moduleConfig.mqtt.root, "custom"); - MQTTUnitTest::restart(); +void test_customMqttRoot(void) +{ + strcpy(moduleConfig.mqtt.root, "custom"); + MQTTUnitTest::restart(); - TEST_ASSERT_TRUE(loopUntil([] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); })); + TEST_ASSERT_TRUE(loopUntil( + [] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); })); } // Empty configuration is valid. -void test_configEmptyIsValid(void) { - meshtastic_ModuleConfig_MQTTConfig config = {}; +void test_configEmptyIsValid(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {}; - TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Empty 'enabled' configuration is valid. -void test_configEnabledEmptyIsValid(void) { - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; - MockPubSubServer client; +void test_configEnabledEmptyIsValid(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; + MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); - TEST_ASSERT_EQUAL(1883, client.port_); + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(client.connected_); + TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); + TEST_ASSERT_EQUAL(1883, client.port_); } // Configuration with the default server is valid. -void test_configWithDefaultServer(void) { - meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address}; +void test_configWithDefaultServer(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address}; - TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server and port 8888 is invalid. -void test_configWithDefaultServerAndInvalidPort(void) { - meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"}; +void test_configWithDefaultServerAndInvalidPort(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"}; - TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } // isValidConfig connects to a custom host and port. -void test_configCustomHostAndPort(void) { - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; - MockPubSubServer client; +void test_configCustomHostAndPort(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; + MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); - TEST_ASSERT_EQUAL(1234, client.port_); + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(client.connected_); + TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); + TEST_ASSERT_EQUAL(1234, client.port_); } // isValidConfig returns false if a connection cannot be established. -void test_configWithConnectionFailure(void) { - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; - MockPubSubServer client; - client.refuseConnection_ = true; +void test_configWithConnectionFailure(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; + MockPubSubServer client; + client.refuseConnection_ = true; - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); } // isValidConfig returns true when tls_enabled is supported, or false otherwise. -void test_configWithTLSEnabled(void) { - meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; - MockPubSubServer client; +void test_configWithTLSEnabled(void) +{ + meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; + MockPubSubServer client; #if MQTT_SUPPORTS_TLS - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); #else - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); #endif } -void setup() { - initializeTestEnvironment(); - const std::unique_ptr mockNodeDB(new MockNodeDB()); - nodeDB = mockNodeDB.get(); +void setup() +{ + initializeTestEnvironment(); + const std::unique_ptr mockNodeDB(new MockNodeDB()); + nodeDB = mockNodeDB.get(); - UNITY_BEGIN(); - RUN_TEST(test_sendDirectlyConnectedDecoded); - RUN_TEST(test_sendDirectlyConnectedEncrypted); - RUN_TEST(test_proxyToMeshServiceDecoded); - RUN_TEST(test_proxyToMeshServiceEncrypted); - RUN_TEST(test_dontMqttMeOnPublicServer); - RUN_TEST(test_okToMqttOnPrivateServer); - RUN_TEST(test_noRangeTestAppOnDefaultServer); - RUN_TEST(test_noDetectionSensorAppOnDefaultServer); - RUN_TEST(test_sendQueued); - RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt); - RUN_TEST(test_receiveEmptyMeshPacket); - RUN_TEST(test_receiveDecodedProto); - RUN_TEST(test_receiveDecodedProtoFromProxy); - RUN_TEST(test_receiveEmptyDataFromProxy); - RUN_TEST(test_receiveWithoutChannelDownlink); - RUN_TEST(test_receiveEncryptedPKITopicToUs); - RUN_TEST(test_receiveIgnoresOwnPublishedMessages); - RUN_TEST(test_receiveAcksOwnSentMessages); - RUN_TEST(test_receiveIgnoresSentMessagesFromOthers); - RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled); - RUN_TEST(test_receiveIgnoresDecodedAdminApp); - RUN_TEST(test_receiveIgnoresUnexpectedFields); - RUN_TEST(test_receiveIgnoresInvalidHopLimit); - RUN_TEST(test_publishTextMessageDirect); - RUN_TEST(test_publishTextMessageWithProxy); - RUN_TEST(test_reportToMapDefaultImprecise); - RUN_TEST(test_reportToMapImpreciseProxied); - RUN_TEST(test_usingDefaultServer); - RUN_TEST(test_usingDefaultServerWithPort); - RUN_TEST(test_usingDefaultServerWithInvalidPort); - RUN_TEST(test_usingCustomServer); - RUN_TEST(test_enabled); - RUN_TEST(test_disabled); - RUN_TEST(test_customMqttRoot); - RUN_TEST(test_configEmptyIsValid); - RUN_TEST(test_configEnabledEmptyIsValid); - RUN_TEST(test_configWithDefaultServer); - RUN_TEST(test_configWithDefaultServerAndInvalidPort); - RUN_TEST(test_configCustomHostAndPort); - RUN_TEST(test_configWithConnectionFailure); - RUN_TEST(test_configWithTLSEnabled); - exit(UNITY_END()); + UNITY_BEGIN(); + RUN_TEST(test_sendDirectlyConnectedDecoded); + RUN_TEST(test_sendDirectlyConnectedEncrypted); + RUN_TEST(test_proxyToMeshServiceDecoded); + RUN_TEST(test_proxyToMeshServiceEncrypted); + RUN_TEST(test_dontMqttMeOnPublicServer); + RUN_TEST(test_okToMqttOnPrivateServer); + RUN_TEST(test_noRangeTestAppOnDefaultServer); + RUN_TEST(test_noDetectionSensorAppOnDefaultServer); + RUN_TEST(test_sendQueued); + RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt); + RUN_TEST(test_receiveEmptyMeshPacket); + RUN_TEST(test_receiveDecodedProto); + RUN_TEST(test_receiveDecodedProtoFromProxy); + RUN_TEST(test_receiveEmptyDataFromProxy); + RUN_TEST(test_receiveWithoutChannelDownlink); + RUN_TEST(test_receiveEncryptedPKITopicToUs); + RUN_TEST(test_receiveIgnoresOwnPublishedMessages); + RUN_TEST(test_receiveAcksOwnSentMessages); + RUN_TEST(test_receiveIgnoresSentMessagesFromOthers); + RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled); + RUN_TEST(test_receiveIgnoresDecodedAdminApp); + RUN_TEST(test_receiveIgnoresUnexpectedFields); + RUN_TEST(test_receiveIgnoresInvalidHopLimit); + RUN_TEST(test_publishTextMessageDirect); + RUN_TEST(test_publishTextMessageWithProxy); + RUN_TEST(test_reportToMapDefaultImprecise); + RUN_TEST(test_reportToMapImpreciseProxied); + RUN_TEST(test_usingDefaultServer); + RUN_TEST(test_usingDefaultServerWithPort); + RUN_TEST(test_usingDefaultServerWithInvalidPort); + RUN_TEST(test_usingCustomServer); + RUN_TEST(test_enabled); + RUN_TEST(test_disabled); + RUN_TEST(test_customMqttRoot); + RUN_TEST(test_configEmptyIsValid); + RUN_TEST(test_configEnabledEmptyIsValid); + RUN_TEST(test_configWithDefaultServer); + RUN_TEST(test_configWithDefaultServerAndInvalidPort); + RUN_TEST(test_configCustomHostAndPort); + RUN_TEST(test_configWithConnectionFailure); + RUN_TEST(test_configWithTLSEnabled); + exit(UNITY_END()); } #else -void setup() { - initializeTestEnvironment(); - LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient"); - UNITY_BEGIN(); - UNITY_END(); +void setup() +{ + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient"); + UNITY_BEGIN(); + UNITY_END(); } #endif void loop() {} \ No newline at end of file diff --git a/test/test_serial/SerialModule.cpp b/test/test_serial/SerialModule.cpp index a1da8f25d..1bccf04a7 100644 --- a/test/test_serial/SerialModule.cpp +++ b/test/test_serial/SerialModule.cpp @@ -11,132 +11,146 @@ #define IS_RUNNING_TESTS 0 #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) #include "modules/SerialModule.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) // Test that empty configuration is valid. -void test_serialConfigEmptyIsValid(void) { - meshtastic_ModuleConfig_SerialConfig config = {}; +void test_serialConfigEmptyIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that basic enabled configuration is valid. -void test_serialConfigEnabledIsValid(void) { - meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; +void test_serialConfigEnabledIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and NMEA mode is valid. -void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) { - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; +void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and CalTopo mode is valid. -void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) { - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; +void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and DEFAULT mode is invalid. -void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) { - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; +void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and SIMPLE mode is invalid. -void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) { - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; +void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and TEXTMSG mode is invalid. -void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) { - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; +void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and PROTO mode is invalid. -void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) { - meshtastic_ModuleConfig_SerialConfig config = { - .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; +void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; - TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that various modes work without override_console_serial_port. -void test_serialConfigVariousModesWithoutOverrideAreValid(void) { - meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; +void test_serialConfigVariousModesWithoutOverrideAreValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; - // Test DEFAULT mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test DEFAULT mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test SIMPLE mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test SIMPLE mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test TEXTMSG mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test TEXTMSG mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test PROTO mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test PROTO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test NMEA mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test NMEA mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); - // Test CALTOPO mode - config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; - TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + // Test CALTOPO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } #endif // Architecture check -void setup() { - initializeTestEnvironment(); +void setup() +{ + initializeTestEnvironment(); -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - UNITY_BEGIN(); - RUN_TEST(test_serialConfigEmptyIsValid); - RUN_TEST(test_serialConfigEnabledIsValid); - RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); - RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); - RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); - RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); - RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); - RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); - RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); - exit(UNITY_END()); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + UNITY_BEGIN(); + RUN_TEST(test_serialConfigEmptyIsValid); + RUN_TEST(test_serialConfigEnabledIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); + RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); + exit(UNITY_END()); #else - LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); - UNITY_BEGIN(); - UNITY_END(); + LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); + UNITY_BEGIN(); + UNITY_END(); #endif } #else -void setup() { - initializeTestEnvironment(); - LOG_WARN("This test requires the ARCH_PORTDUINO variant"); - UNITY_BEGIN(); - UNITY_END(); +void setup() +{ + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant"); + UNITY_BEGIN(); + UNITY_END(); } #endif void loop() {} diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index 0a6ff134a..0c1ef6967 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -26,9 +26,9 @@ // Status // #define LED_PIN 1 // External notification -// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in -// the app/preferences #define EXT_NOTIFY_OUT 2 // The GPIO pin that acts as the external notification output (here we -// connect an LED to it) +// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the +// app/preferences +// #define EXT_NOTIFY_OUT 2 // The GPIO pin that acts as the external notification output (here we connect an LED to it) // Buzzer #define PIN_BUZZER 19 @@ -73,8 +73,8 @@ #define BATTERY_PIN 34 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO34_CHANNEL -#define ADC_ATTENUATION \ - ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32 +#define ADC_ATTENUATION \ + ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32 // ESP32-S2/C3/S3 are different // lower dB for lower voltage rnage #define ADC_MULTIPLIER 5.0 // VBATT---10k--pin34---2.5K---GND @@ -108,20 +108,19 @@ #define LORA_CS SX126X_CS // FIXME: for some reason both are used in /src -// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an -// RF95, just that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so -// cause confusion to those adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no -// need to define LORA_DIO0 is not used in src (as we are not using RF95) as SX1262 does not have it per SX1262 -// datasheet, so no need to define -// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other -// modules then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* -// definitions when the RF95 isn't used -#define LORA_DIO1 \ - SX126X_DIO1 // The old name is used in - // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, - // so must also define the old name -// LORA_DIO2 value is never used in src (as we are not using RF95), so no need to define, and if DIO2_AS_RF_SWITCH is -// set then it cannot serve any extra function even if requested to LORA_DIO3 value is never used in src (as we are not -// using RF95), so no need to define, and DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if -// requested to (from 13.3.2.1 DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch -// or the TCXO, the IRQ will not be generated even if it is mapped to the pins.) +// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an RF95, just +// that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so cause confusion to those +// adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no need to define LORA_DIO0 is not used +// in src (as we are not using RF95) as SX1262 does not have it per SX1262 datasheet, so no need to define +// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other modules +// then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* definitions when +// the RF95 isn't used +#define LORA_DIO1 \ + SX126X_DIO1 // The old name is used in + // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, so + // must also define the old name +// LORA_DIO2 value is never used in src (as we are not using RF95), so no need to define, and if DIO2_AS_RF_SWITCH is set then it +// cannot serve any extra function even if requested to LORA_DIO3 value is never used in src (as we are not using RF95), so no +// need to define, and DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if requested to (from 13.3.2.1 +// DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch or the TCXO, the IRQ will not be +// generated even if it is mapped to the pins.) diff --git a/variants/esp32/diy/dr-dev/variant.h b/variants/esp32/diy/dr-dev/variant.h index 8e516062d..35b18ee74 100644 --- a/variants/esp32/diy/dr-dev/variant.h +++ b/variants/esp32/diy/dr-dev/variant.h @@ -35,9 +35,9 @@ #define LORA_DIO2 22 // BUSY for SX1262/SX1268 // NOT_A_PIN is treated as RADIOLIB_NC due to how they are defined, best to use RADIOLIB_NC directly #define LORA_TXEN RADIOLIB_NC // Input - RF switch TX control, connecting external MCU IO or DIO2, valid in high level -// E22_TXEN_CONNECTED_TO_DIO2 wasn't defined, so RXEN wasn't controlled. Commented it out to maintain behavior, but -// shouldn't be. Need to comment out defining SX126X_RXEN as LORA_RXEN too #define LORA_RXEN 17 // Input - RF switch RX -// control, connecting external MCU IO, valid in high level +// E22_TXEN_CONNECTED_TO_DIO2 wasn't defined, so RXEN wasn't controlled. Commented it out to maintain behavior, but shouldn't be. +// Need to comment out defining SX126X_RXEN as LORA_RXEN too +// #define LORA_RXEN 17 // Input - RF switch RX control, connecting external MCU IO, valid in high level #undef LORA_CS #define LORA_CS 16 #define SX126X_BUSY 22 diff --git a/variants/esp32/diy/hydra/variant.h b/variants/esp32/diy/hydra/variant.h index c82fff55b..e5c10e26b 100644 --- a/variants/esp32/diy/hydra/variant.h +++ b/variants/esp32/diy/hydra/variant.h @@ -20,9 +20,8 @@ // Radio #define USE_SX1262 // E22-900M30S uses SX1262 #define USE_SX1268 // E22-400M30S uses SX1268 -#define SX126X_MAX_POWER \ - 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W - // PA) +#define SX126X_MAX_POWER \ + 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA) #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V #define SX126X_CS 18 // EBYTE module's NSS pin diff --git a/variants/esp32/heltec_wireless_bridge/variant.h b/variants/esp32/heltec_wireless_bridge/variant.h index 66b69dc91..5ad16d0e2 100644 --- a/variants/esp32/heltec_wireless_bridge/variant.h +++ b/variants/esp32/heltec_wireless_bridge/variant.h @@ -38,5 +38,4 @@ // user button is present on device, but currently untested & unconfigured - couldn't figure out how it's connected -// battery support is present within device, but currently untested & unconfigured - couldn't find reliable information -// yet +// battery support is present within device, but currently untested & unconfigured - couldn't find reliable information yet diff --git a/variants/esp32/nano-g1-explorer/variant.h b/variants/esp32/nano-g1-explorer/variant.h index 1431f1f8b..f3640241a 100644 --- a/variants/esp32/nano-g1-explorer/variant.h +++ b/variants/esp32/nano-g1-explorer/variant.h @@ -28,8 +28,8 @@ // Not really an E22 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) #endif #define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/esp32/nano-g1/variant.h b/variants/esp32/nano-g1/variant.h index b53f4019e..2521c3ffe 100644 --- a/variants/esp32/nano-g1/variant.h +++ b/variants/esp32/nano-g1/variant.h @@ -28,8 +28,8 @@ // Not really an E22 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) #endif // different screen diff --git a/variants/esp32/radiomaster_900_bandit/variant.h b/variants/esp32/radiomaster_900_bandit/variant.h index 8b6ab4ea2..0c7417cac 100644 --- a/variants/esp32/radiomaster_900_bandit/variant.h +++ b/variants/esp32/radiomaster_900_bandit/variant.h @@ -59,10 +59,10 @@ #define NEOPIXEL_DATA 15 // GPIO pin used to send data to the neopixels #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // Type of neopixels in use #define ENABLE_AMBIENTLIGHTING // Turn on Ambient Lighting -// #define BUTTON1_COLOR 0xFF0000 // Background light for Button 1 in HEX RGB Color (RadioMaster Bandit -// only). #define BUTTON1_COLOR_INDEX 0 // NeoPixel Index ID for Button 1 #define BUTTON2_COLOR 0x0000FF -// // Background light for Button 2 in HEX RGB Color (RadioMaster Bandit only). #define BUTTON2_COLOR_INDEX 1 // -// NeoPixel Index ID for Button 2 +// #define BUTTON1_COLOR 0xFF0000 // Background light for Button 1 in HEX RGB Color (RadioMaster Bandit only). +// #define BUTTON1_COLOR_INDEX 0 // NeoPixel Index ID for Button 1 +// #define BUTTON2_COLOR 0x0000FF // Background light for Button 2 in HEX RGB Color (RadioMaster Bandit only). +// #define BUTTON2_COLOR_INDEX 1 // NeoPixel Index ID for Button 2 /* It has 1 x five-way and 2 x normal buttons. diff --git a/variants/esp32/rak11200/variant.h b/variants/esp32/rak11200/variant.h index c04cf4dfc..01edb8b73 100644 --- a/variants/esp32/rak11200/variant.h +++ b/variants/esp32/rak11200/variant.h @@ -57,7 +57,8 @@ static const uint8_t SCK = 33; #define LORA_RESET WB_IO4 // RST for SX1276, and for SX1262/SX1268 #define LORA_DIO1 WB_IO6 // IRQ for SX1262/SX1268 #define LORA_DIO2 WB_IO5 // BUSY for SX1262/SX1268 -#define LORA_DIO3 RADIOLIB_NC // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled +#define LORA_DIO3 \ + RADIOLIB_NC // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled #undef LORA_SCK #define LORA_SCK SCK diff --git a/variants/esp32/station-g1/variant.h b/variants/esp32/station-g1/variant.h index 5d3c836a2..6c3a39261 100644 --- a/variants/esp32/station-g1/variant.h +++ b/variants/esp32/station-g1/variant.h @@ -26,9 +26,9 @@ #define SX126X_BUSY LORA_DIO2 #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH // Internally the module hooks the SX1262-DIO2 in to control the TX/RX switch -#define SX126X_MAX_POWER \ - 16 // Ensure the PA does not exceed the saturation output power. More - // Info:https://uniteng.com/wiki/doku.php?id=meshtastic:station#rf_design_-_lora_station_edition_g1 +#define SX126X_MAX_POWER \ + 16 // Ensure the PA does not exceed the saturation output power. More + // Info:https://uniteng.com/wiki/doku.php?id=meshtastic:station#rf_design_-_lora_station_edition_g1 #endif #define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index eaa3a4268..2d144a888 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -11,8 +11,8 @@ #define LED_STATE_ON 0 // State when LED is lit #define LED_PIN 4 // Newer tbeams (1.1) have an extra led on GPIO4 -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for -// RF95 and if not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if +// not found then probe for SX1262 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1268 @@ -31,8 +31,8 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) #endif // Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts diff --git a/variants/esp32/tlora_v2/variant.h b/variants/esp32/tlora_v2/variant.h index 92281e5de..8a7cf89ec 100644 --- a/variants/esp32/tlora_v2/variant.h +++ b/variants/esp32/tlora_v2/variant.h @@ -6,9 +6,9 @@ #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost #define LED_PIN 25 // If defined we will blink this LED -#define BUTTON_PIN \ - 0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire - // one between this pin and ground +#define BUTTON_PIN \ + 0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire one + // between this pin and ground #define BUTTON_NEED_PULLUP #define USE_RF95 diff --git a/variants/esp32/wiphone/variant.h b/variants/esp32/wiphone/variant.h index c4ce89c28..619ac622a 100644 --- a/variants/esp32/wiphone/variant.h +++ b/variants/esp32/wiphone/variant.h @@ -35,15 +35,17 @@ #define ST7789_CS 5 #define ST7789_RS 26 #define USE_TFTDISPLAY 1 -// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way -// to control it) #define ST7789_BL -1 // EXTENDER_PIN(9) +// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way to control +// it) +// #define ST7789_BL -1 // EXTENDER_PIN(9) #define ST7789_RESET -1 #define ST7789_MISO 19 #define ST7789_BUSY -1 #define ST7789_SPI_HOST SPI3_HOST -// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way -// to control it) #define TFT_BL -1 // EXTENDER_PIN(9) +// I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way to control +// it) +// #define TFT_BL -1 // EXTENDER_PIN(9) #define SPI_FREQUENCY 40000000 #define SPI_READ_FREQUENCY 16000000 #define TFT_HEIGHT 240 diff --git a/variants/esp32c6/m5stack_unitc6l/variant.cpp b/variants/esp32c6/m5stack_unitc6l/variant.cpp index 71ff9c027..8e26b4ab7 100644 --- a/variants/esp32c6/m5stack_unitc6l/variant.cpp +++ b/variants/esp32c6/m5stack_unitc6l/variant.cpp @@ -22,50 +22,53 @@ #define reversebit(x, y) x ^= (0x01 << y) #define getbit(x, y) ((x) >> (y)&0x01) -void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) { - Wire.beginTransmission(addr); - Wire.write(reg); - Wire.endTransmission(); - Wire.requestFrom(addr, 1); - *value = Wire.read(); +void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) +{ + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.endTransmission(); + Wire.requestFrom(addr, 1); + *value = Wire.read(); } /*******************************************************************/ -void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) { - Wire.beginTransmission(addr); - Wire.write(reg); - Wire.write(value); - Wire.endTransmission(); +void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) +{ + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(value); + Wire.endTransmission(); } /*******************************************************************/ -void c6l_init() { - // P7 LoRa Reset - // P6 RF Switch - // P5 LNA Enable +void c6l_init() +{ + // P7 LoRa Reset + // P6 RF Switch + // P5 LNA Enable - printf("pi4io_init\n"); - uint8_t in_data; - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data); - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断 - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0 - vTaskDelay(10 / portTICK_PERIOD_MS); - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志 + printf("pi4io_init\n"); + uint8_t in_data; + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data); + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断 + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0 + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志 - i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data); - setbit(in_data, 6); // HIGH - i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data); + setbit(in_data, 6); // HIGH + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data); } diff --git a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h index 029bca872..5da99667b 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h +++ b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h @@ -19,9 +19,9 @@ // Battery voltage monitoring - TODO: test, currently untested, copied from T3S3 variant #define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO1_CHANNEL -#define ADC_MULTIPLIER \ - 2.11 // ratio of voltage divider = 2.0 (R10=1M, R13=1M), plus some undervoltage correction - TODO: this was carried - // over from the T3S3, test to see if the undervoltage correction is needed. +#define ADC_MULTIPLIER \ + 2.11 // ratio of voltage divider = 2.0 (R10=1M, R13=1M), plus some undervoltage correction - TODO: this was carried over from + // the T3S3, test to see if the undervoltage correction is needed. // Display - OLED connected via I2C by the default hardware configuration #define HAS_SCREEN 1 @@ -33,9 +33,8 @@ #define UART_TX 43 #define UART_RX 44 -// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. -// There are no pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently -// untested +// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. There are no +// pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently untested #define I2C_SCL1 21 #define I2C_SDA1 10 @@ -52,13 +51,13 @@ #define SX126X_DIO1 33 #define SX126X_DIO2_AS_RF_SWITCH // All switching is performed with DIO2, it is automatically inverted using circuitry. -// CDEBYTE EoRa-S3 uses an XTAL, thus we do not need DIO3 as TCXO voltage reference. Don't define -// SX126X_DIO3_TCXO_VOLTAGE for simplicity rather than defining it as 0. -#define SX126X_MAX_POWER \ - 22 // E22-900MM22S and E22-400MM22S have a raw SX1262 or SX1268 respsectively, they are rated to output up and - // including 22 dBm out of their SX126x IC. +// CDEBYTE EoRa-S3 uses an XTAL, thus we do not need DIO3 as TCXO voltage reference. Don't define SX126X_DIO3_TCXO_VOLTAGE for +// simplicity rather than defining it as 0. +#define SX126X_MAX_POWER \ + 22 // E22-900MM22S and E22-400MM22S have a raw SX1262 or SX1268 respsectively, they are rated to output up and including 22 + // dBm out of their SX126x IC. -// Compatibility with old variant.h file structure - FIXME: this should be done in the respective radio interface -// modules to clean up all variants. +// Compatibility with old variant.h file structure - FIXME: this should be done in the respective radio interface modules to clean +// up all variants. #define LORA_CS SX126X_CS #define LORA_DIO1 SX126X_DIO1 \ No newline at end of file diff --git a/variants/esp32s3/EBYTE_ESP32-S3/variant.h b/variants/esp32s3/EBYTE_ESP32-S3/variant.h index fe667e060..80fb26434 100644 --- a/variants/esp32s3/EBYTE_ESP32-S3/variant.h +++ b/variants/esp32s3/EBYTE_ESP32-S3/variant.h @@ -1,32 +1,30 @@ // Supporting information: https://github.com/S5NC/EBYTE_ESP32-S3/ // Originally developed for E22-900M30S with ESP32-S3-WROOM-1-N4 -// NOTE: Uses ESP32-S3-WROOM-1-N4.json in boards folder (via platformio.ini board field), assumes 4 MB (quad SPI) flash, -// no PSRAM +// NOTE: Uses ESP32-S3-WROOM-1-N4.json in boards folder (via platformio.ini board field), assumes 4 MB (quad SPI) flash, no PSRAM // FIXME: implement SX12 module type autodetection and have setup for each case (add E32 support) -// E32 has same pinout except having extra pins. I assume that the GND on it is connected internally to other GNDs so it -// is not a problem to NC the extra GND pins. +// E32 has same pinout except having extra pins. I assume that the GND on it is connected internally to other GNDs so it is not a +// problem to NC the extra GND pins. // For each EBYTE module pin in this section, provide the pin number of the ESP32-S3 you connected it to -// The ESP32-S3 is great because YOU CAN USE PRACTICALLY ANY PINS for the connections, but avoid some pins (such as on -// the WROOM modules the following): strapping pins (except 0 as a user button input as it already has a pulldown -// resistor in typical application schematic) (0, 3, 45, 46), USB-reserved (19, 20), and pins which aren't present on -// the WROOM-2 module for compatiblity as it uses octal SPI, or are likely connected internally in either WROOM version -// (26-37), and avoid pins whose voltages are set by the SPI voltage (47, 48), and pins that don't exist (22-25) You can -// ALSO set the SPI pins (SX126X_CS, SX126X_SCK, SX126X_MISO, SX126X_MOSI) to any pin with the ESP32-S3 due to \ GPIO -// Matrix / IO MUX / RTC IO MUX \, and also the serial pins, but this isn't recommended for Serial0 as the WROOM modules -// have a 499 Ohm resistor on U0TXD (to reduce harmonics but also acting as a sort of protection) +// The ESP32-S3 is great because YOU CAN USE PRACTICALLY ANY PINS for the connections, but avoid some pins (such as on the WROOM +// modules the following): strapping pins (except 0 as a user button input as it already has a pulldown resistor in typical +// application schematic) (0, 3, 45, 46), USB-reserved (19, 20), and pins which aren't present on the WROOM-2 module for +// compatiblity as it uses octal SPI, or are likely connected internally in either WROOM version (26-37), and avoid pins whose +// voltages are set by the SPI voltage (47, 48), and pins that don't exist (22-25) You can ALSO set the SPI pins (SX126X_CS, +// SX126X_SCK, SX126X_MISO, SX126X_MOSI) to any pin with the ESP32-S3 due to \ GPIO Matrix / IO MUX / RTC IO MUX \, and also the +// serial pins, but this isn't recommended for Serial0 as the WROOM modules have a 499 Ohm resistor on U0TXD (to reduce harmonics +// but also acting as a sort of protection) -// We have many free pins on the ESP32-S3-WROOM-X-Y module, perhaps it is best to use one of its pins to control TXEN, -// and use DIO2 as an extra interrupt, but right now Meshtastic does not benefit from having another interrupt pin -// available. +// We have many free pins on the ESP32-S3-WROOM-X-Y module, perhaps it is best to use one of its pins to control TXEN, and use +// DIO2 as an extra interrupt, but right now Meshtastic does not benefit from having another interrupt pin available. -// Adding two 0-ohm links on your PCB design so that you can choose between the two modes for controlling the E22's TXEN -// would enable future software to make the most of an extra available interrupt pin +// Adding two 0-ohm links on your PCB design so that you can choose between the two modes for controlling the E22's TXEN would +// enable future software to make the most of an extra available interrupt pin -// Possible improvement: can add extremely low resistance MOSFET to physically toggle power to E22 module when in full -// sleep (not waiting for interrupt)? +// Possible improvement: can add extremely low resistance MOSFET to physically toggle power to E22 module when in full sleep (not +// waiting for interrupt)? // PA stands for Power Amplifier, used when transmitting to increase output power // LNA stands for Low Noise Amplifier, used when \ listening for / receiving \ data to increase sensitivity @@ -44,60 +42,57 @@ #define SX126X_RESET 40 // EBYTE module's NRST pin #define SX126X_BUSY 41 // EBYTE module's BUSY pin #define SX126X_DIO1 42 // EBYTE module's DIO1 pin -// We don't define a pin for SX126X_DIO2 as Meshtastic doesn't use it as an interrupt output, so it is never connected -// to an MCU pin! Also E22 module datasheets say not to connect it to an MCU pin. We don't define a pin for SX126X_DIO3 -// as Meshtastic doesn't use it as an interrupt output, so it is never connected to an MCU pin! Also E22 module -// datasheets say to use it as the TCXO's reference voltage. E32 module (which uses SX1276) may not have ability to set -// TCXO voltage using a DIO pin. +// We don't define a pin for SX126X_DIO2 as Meshtastic doesn't use it as an interrupt output, so it is never connected to an MCU +// pin! Also E22 module datasheets say not to connect it to an MCU pin. +// We don't define a pin for SX126X_DIO3 as Meshtastic doesn't use it as an interrupt output, so it is never connected to an MCU +// pin! Also E22 module datasheets say to use it as the TCXO's reference voltage. +// E32 module (which uses SX1276) may not have ability to set TCXO voltage using a DIO pin. -// The radio module needs to be told whether to enable RX mode or TX mode. Each radio module takes different actions -// based on these values, but generally the path from the antenna to SX1262 is changed from signal output to signal -// input. Also, if there are LNAs (Low-Noise Amplifiers) or PAs (Power Amplifiers) in the output or input paths, their -// power is also controlled by these pins. You should never have both TXEN and RXEN set high, this can cause problems -// for some radio modules, and is commonly referred to as 'undefined behaviour' in datasheets. For the SX1262, you -// shouldn't connect DIO2 to the MCU. DIO2 is an output only, and can be controlled via SPI instructions, the use for -// this is to save an MCU pin by using the DIO2 pin to control the RF switching mode. +// The radio module needs to be told whether to enable RX mode or TX mode. Each radio module takes different actions based on +// these values, but generally the path from the antenna to SX1262 is changed from signal output to signal input. Also, if there +// are LNAs (Low-Noise Amplifiers) or PAs (Power Amplifiers) in the output or input paths, their power is also controlled by +// these pins. You should never have both TXEN and RXEN set high, this can cause problems for some radio modules, and is +// commonly referred to as 'undefined behaviour' in datasheets. For the SX1262, you shouldn't connect DIO2 to the MCU. DIO2 is +// an output only, and can be controlled via SPI instructions, the use for this is to save an MCU pin by using the DIO2 pin to +// control the RF switching mode. // Choose ONLY ONE option from below, comment in/out the '/*'s and '*/'s // SX126X_TXEN is the E22's [SX1262's] TXEN pin, SX126X_RXEN is the E22's [SX1262's] RXEN pin -// Option 1: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin -// (more expensive option hardware-wise, is the 'most proper' way, removes need for routing one/two traces from MCU to -// RF switching pins), however you can't have E22 in low-power 'sleep' mode (TXEN and RXEN both low cannot be achieved -// this this option). +// Option 1: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin (more +// expensive option hardware-wise, is the 'most proper' way, removes need for routing one/two traces from MCU to RF switching +// pins), however you can't have E22 in low-power 'sleep' mode (TXEN and RXEN both low cannot be achieved this this option). /* #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_TXEN RADIOLIB_NC #define SX126X_RXEN RADIOLIB_NC */ -// Option 2: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to MCU pin (cheaper option -// hardware-wise, removes need for routing another trace from MCU to an RF switching pin). +// Option 2: E22's TXEN pin connected to E22's DIO2 pin, E22's RXEN pin connected to MCU pin (cheaper option hardware-wise, +// removes need for routing another trace from MCU to an RF switching pin). // /* #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_TXEN RADIOLIB_NC #define SX126X_RXEN 10 // */ -// Option 3: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to MCU pin (cheaper option hardware-wise, -// allows for ramping up PA before transmission (add/expand on feature yourself in RadioLib) if PA takes a while to -// stabilise) Don't define DIO2_AS_RF_SWITCH because we only use DIO2 or an MCU pin mutually exclusively to connect to -// E22's TXEN (to prevent a short if they are both connected at the same time (suboptimal PCB design) and there's a -// slight non-neglibible delay and/or voltage difference between DIO2 and TXEN). Can use DIO2 as an IRQ (but not in -// Meshtastic at the moment). +// Option 3: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to MCU pin (cheaper option hardware-wise, allows for +// ramping up PA before transmission (add/expand on feature yourself in RadioLib) if PA takes a while to stabilise) +// Don't define DIO2_AS_RF_SWITCH because we only use DIO2 or an MCU pin mutually exclusively to connect to E22's TXEN (to prevent +// a short if they are both connected at the same time (suboptimal PCB design) and there's a slight non-neglibible delay and/or +// voltage difference between DIO2 and TXEN). Can use DIO2 as an IRQ (but not in Meshtastic at the moment). /* #define SX126X_TXEN 9 #define SX126X_RXEN 10 */ // (NOT RECOMMENDED, if need to ramp up PA before transmission, better to use option 3) -// Option 4: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin (more -// expensive option hardware-wise, allows for ramping up PA before transmission (add/expand on feature yourself in -// RadioLib) if PA takes a while to stabilise, removes need for routing another trace from MCU to an RF switching pin, -// however may mean if in RadioLib you don't tell DIO2 to go high to indicate transmission (so the negated output goes -// to RXEN to turn the LNA off) then you may end up enabling E22's TXEN and RXEN pins at the same time whilst you ramp -// up the PA which is not ideal, changing DIO2's switching advance in RadioLib may not even be possible, may be baked -// into the SX126x). +// Option 4: E22's TXEN pin connected to MCU pin, E22's RXEN pin connected to NEGATED output of E22's DIO2 pin (more expensive +// option hardware-wise, allows for ramping up PA before transmission (add/expand on feature yourself in RadioLib) if PA takes +// a while to stabilise, removes need for routing another trace from MCU to an RF switching pin, however may mean if in +// RadioLib you don't tell DIO2 to go high to indicate transmission (so the negated output goes to RXEN to turn the LNA off) +// then you may end up enabling E22's TXEN and RXEN pins at the same time whilst you ramp up the PA which is not ideal, +// changing DIO2's switching advance in RadioLib may not even be possible, may be baked into the SX126x). /* #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_TXEN 9 @@ -108,8 +103,8 @@ #define LED_PIN 1 #define LED_STATE_ON 1 // State when LED is lit // External notification -// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in -// the app/preferences +// FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the +// app/preferences #define EXT_NOTIFY_OUT 2 // The GPIO pin that acts as the external notification output (here we connect an LED to it) // Buzzer #define PIN_BUZZER 11 @@ -124,18 +119,19 @@ // Power // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA) -// Respect local regulations! If your E22-900M30S outputs the advertised 30 dBm and you use a 6 dBi antenna, you are at -// the equivalent of 36 EIRP (Effective Isotropic Radiated Power), which in this case is the limit for non-HAM users in -// the US (4W EIRP, at SPECIFIC frequencies). In the EU (and UK), as of now, you are allowed 27 dBm ERP which is 29.15 -// EIRP. https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022D0180 +// Respect local regulations! If your E22-900M30S outputs the advertised 30 dBm and you use a 6 dBi antenna, you are at the +// equivalent of 36 EIRP (Effective Isotropic Radiated Power), which in this case is the limit for non-HAM users in the US (4W +// EIRP, at SPECIFIC frequencies). +// In the EU (and UK), as of now, you are allowed 27 dBm ERP which is 29.15 EIRP. +// https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022D0180 // https://www.legislation.gov.uk/uksi/1999/930/schedule/6/made -// To respect the 29.15 dBm EIRP (at SPECIFIC frequencies, others are lower) EU limit with a 2.5 dBi gain antenna, -// consulting https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/power%20testing.txt, assuming 0.1 dBm insertion loss, -// output 20 dBm from the E22-900M30S's SX1262. It is worth noting that if you are in this situation and don't have a -// HAM license, you may be better off with a lower gain antenna, and output the difference as a higher total power input -// into the antenna, as your EIRP would be the same, but you would get a wider angle of coverage. Also take insertion -// loss and possibly VSWR into account (https://www.everythingrf.com/tech-resources/vswr). Please check regulations -// yourself and check airtime, usage (for example whether you are airborne), frequency, and power laws. +// To respect the 29.15 dBm EIRP (at SPECIFIC frequencies, others are lower) EU limit with a 2.5 dBi gain antenna, consulting +// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/power%20testing.txt, assuming 0.1 dBm insertion loss, output 20 dBm from the +// E22-900M30S's SX1262. It is worth noting that if you are in this situation and don't have a HAM license, you may be better off +// with a lower gain antenna, and output the difference as a higher total power input into the antenna, as your EIRP would be the +// same, but you would get a wider angle of coverage. Also take insertion loss and possibly VSWR into account +// (https://www.everythingrf.com/tech-resources/vswr). Please check regulations yourself and check airtime, usage (for example +// whether you are airborne), frequency, and power laws. #define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice // Display @@ -161,15 +157,16 @@ #define USE_SX1262 // E22-900M30S, E22-900M22S, and E22-900MM22S (not E220!) use SX1262 #define USE_SX1268 // E22-400M30S, E22-400M33S, E22-400M22S, and E22-400MM22S use SX1268 -// The below isn't needed as we directly define SX126X_TXEN and SX126X_RXEN instead of using proxies E22_TXEN and -// E22_RXEN +// The below isn't needed as we directly define SX126X_TXEN and SX126X_RXEN instead of using proxies E22_TXEN and E22_RXEN /* -// FALLBACK: If somehow E22_TXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid -SX126X_TXEN being defined but having no value #if (!defined(E22_TXEN) || !(0 <= E22_TXEN && E22_TXEN <= 48)) #define -E22_TXEN RADIOLIB_NC #endif -// FALLBACK: If somehow E22_RXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid -SX126X_RXEN being defined but having no value #if (!defined(E22_RXEN) || !(0 <= E22_RXEN && E22_RXEN <= 48)) #define -E22_RXEN RADIOLIB_NC #endif #define SX126X_TXEN E22_TXEN #define SX126X_RXEN E22_RXEN +// FALLBACK: If somehow E22_TXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid SX126X_TXEN +being defined but having no value #if (!defined(E22_TXEN) || !(0 <= E22_TXEN && E22_TXEN <= 48)) #define E22_TXEN RADIOLIB_NC +#endif +// FALLBACK: If somehow E22_RXEN isn't defined or clearly isn't a valid pin number, set it to RADIOLIB_NC to avoid SX126X_RXEN +being defined but having no value #if (!defined(E22_RXEN) || !(0 <= E22_RXEN && E22_RXEN <= 48)) #define E22_RXEN RADIOLIB_NC +#endif +#define SX126X_TXEN E22_TXEN +#define SX126X_RXEN E22_RXEN */ // E22 series TCXO voltage is 1.8V per https://www.ebyte.com/en/pdf-down.aspx?id=781 (source @@ -178,20 +175,19 @@ E22_RXEN RADIOLIB_NC #endif #define SX126X_TXEN E22_TXEN #define SX126X_RXEN E22 #define LORA_CS SX126X_CS // FIXME: for some reason both are used in /src -// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an -// RF95, just that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so -// cause confusion to those adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no -// need to define LORA_DIO0 is not used in src (as we are not using RF95) as SX1262 does not have it per SX1262 -// datasheet, so no need to define -// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other -// modules then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* -// definitions when the RF95 isn't used -#define LORA_DIO1 \ - SX126X_DIO1 // The old name is used in - // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, - // so must also define the old name LORA_DIO2 value is never used in src (as we are not using RF95), so no - // need to define, and if DIO2_AS_RF_SWITCH is set then it cannot serve any extra function even if - // requested to LORA_DIO3 value is never used in src (as we are not using RF95), so no need to define, and - // DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if requested to (from 13.3.2.1 - // DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch or the TCXO, - // the IRQ will not be generated even if it is mapped to the pins.) \ No newline at end of file +// Many of the below values would only be used if USE_RF95 was defined, but it's not as we aren't actually using an RF95, just +// that the 4 pins above are named like it If they aren't used they don't need to be defined and doing so cause confusion to those +// adapting this file LORA_RESET value is never used in src (as we are not using RF95), so no need to define LORA_DIO0 is not used +// in src (as we are not using RF95) as SX1262 does not have it per SX1262 datasheet, so no need to define +// FIXME: confirm that the linked lines below are actually only called when using the SX126x or SX128x and no other modules +// then use SX126X_DIO1 and SX128X_DIO1 respectively for that purpose, removing the need for RF95-style LORA_* definitions when +// the RF95 isn't used +#define LORA_DIO1 \ + SX126X_DIO1 // The old name is used in + // https://github.com/meshtastic/firmware/blob/7eff5e7bcb2084499b723c5e3846c15ee089e36d/src/sleep.cpp#L298, so + // must also define the old name +// LORA_DIO2 value is never used in src (as we are not using RF95), so no need to define, and if DIO2_AS_RF_SWITCH is set then it +// cannot serve any extra function even if requested to LORA_DIO3 value is never used in src (as we are not using RF95), so no +// need to define, and DIO3_AS_TCXO_AT_1V8 is set so it cannot serve any extra function even if requested to (from 13.3.2.1 +// DioxMask in SX1262 datasheet: Note that if DIO2 or DIO3 are used to control the RF Switch or the TCXO, the IRQ will not be +// generated even if it is mapped to the pins.) \ No newline at end of file diff --git a/variants/esp32s3/dreamcatcher/rfswitch.h b/variants/esp32s3/dreamcatcher/rfswitch.h index 0ec4b5c40..74edb25d1 100644 --- a/variants/esp32s3/dreamcatcher/rfswitch.h +++ b/variants/esp32s3/dreamcatcher/rfswitch.h @@ -5,7 +5,8 @@ // DIO6 -> RFSW1_V2 // DIO7 -> ANT_CTRL_ON + ESP_IO9/LR_GPS_ANT_DC_EN -> RFI_GPS (Bias-T GPS) -static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, + RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 diff --git a/variants/esp32s3/heltec_vision_master_e213/einkDetect.h b/variants/esp32s3/heltec_vision_master_e213/einkDetect.h index f8fe1c576..35140db60 100644 --- a/variants/esp32s3/heltec_vision_master_e213/einkDetect.h +++ b/variants/esp32s3/heltec_vision_master_e213/einkDetect.h @@ -3,32 +3,33 @@ #include "configuration.h" enum class EInkDetectionResult : uint8_t { - LCMEN213EFC1 = 0, // Initial version - E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) + LCMEN213EFC1 = 0, // Initial version + E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) }; -EInkDetectionResult detectEInk() { - // Test 1: Logic of BUSY pin +EInkDetectionResult detectEInk() +{ + // Test 1: Logic of BUSY pin - // Determines controller IC manufacturer - // Fitipower: busy when LOW - // Solomon Systech: busy when HIGH + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH - // Force display BUSY by holding reset pin active - pinMode(PIN_EINK_RES, OUTPUT); - digitalWrite(PIN_EINK_RES, LOW); + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); - delay(10); + delay(10); - // Read whether pin is HIGH or LOW while busy - pinMode(PIN_EINK_BUSY, INPUT); - bool busyLogic = digitalRead(PIN_EINK_BUSY); + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); - // Test complete. Release pin - pinMode(PIN_EINK_RES, INPUT); + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); - if (busyLogic == LOW) - return EInkDetectionResult::LCMEN213EFC1; - else // busy HIGH - return EInkDetectionResult::E0213A367; + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; } \ No newline at end of file diff --git a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h index c7bf319f7..1b1291424 100644 --- a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h @@ -25,92 +25,93 @@ #include "buzz.h" // Button feedback #include "einkDetect.h" // Detect display model at runtime -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // Detect E-Ink Model - // ------------------- + // Detect E-Ink Model + // ------------------- - EInkDetectionResult displayModel = detectEInk(); + EInkDetectionResult displayModel = detectEInk(); - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver; + Drivers::EInk *driver; - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) - driver = new Drivers::LCMEN213EFC1; - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 - driver = new Drivers::E0213A367; + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + driver = new Drivers::E0213A367; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) - inkhud->setDisplayResilience(10, 1.5); - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 - inkhud->setDisplayResilience(15, 3); + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + inkhud->setDisplayResilience(15, 3); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button - buttons->setWiring(1, PIN_BUTTON2); - buttons->setHandlerShortPress(1, [inkhud]() { - inkhud->nextTile(); - playChirp(); - }); + // #1: Aux Button + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playChirp(); + }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h index d0ea08c12..61b08c740 100644 --- a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h @@ -37,77 +37,78 @@ Different NicheGraphics UIs and different hardware variants will each have their // Button feedback #include "buzz.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::DEPG0290BNS800; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(7, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button - buttons->setWiring(1, PIN_BUTTON2); - buttons->setHandlerShortPress(1, [inkhud]() { - inkhud->nextTile(); - playChirp(); - }); + // #1: Aux Button + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playChirp(); + }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_paper/einkDetect.h b/variants/esp32s3/heltec_wireless_paper/einkDetect.h index 594dffc74..93b3f86e3 100644 --- a/variants/esp32s3/heltec_wireless_paper/einkDetect.h +++ b/variants/esp32s3/heltec_wireless_paper/einkDetect.h @@ -3,32 +3,33 @@ #include "configuration.h" enum class EInkDetectionResult : uint8_t { - LCMEN213EFC1 = 0, // V1.1 - E0213A367 = 1, // V1.1.1, V1.2 + LCMEN213EFC1 = 0, // V1.1 + E0213A367 = 1, // V1.1.1, V1.2 }; -EInkDetectionResult detectEInk() { - // Test 1: Logic of BUSY pin +EInkDetectionResult detectEInk() +{ + // Test 1: Logic of BUSY pin - // Determines controller IC manufacturer - // Fitipower: busy when LOW - // Solomon Systech: busy when HIGH + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH - // Force display BUSY by holding reset pin active - pinMode(PIN_EINK_RES, OUTPUT); - digitalWrite(PIN_EINK_RES, LOW); + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); - delay(10); + delay(10); - // Read whether pin is HIGH or LOW while busy - pinMode(PIN_EINK_BUSY, INPUT); - bool busyLogic = digitalRead(PIN_EINK_BUSY); + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); - // Test complete. Release pin - pinMode(PIN_EINK_RES, INPUT); + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); - if (busyLogic == LOW) - return EInkDetectionResult::LCMEN213EFC1; - else // busy HIGH - return EInkDetectionResult::E0213A367; + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; } \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h index aadd52c91..445b57714 100644 --- a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h +++ b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h @@ -24,86 +24,87 @@ #include "einkDetect.h" // Detect display model at runtime -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // Detect E-Ink Model - // ------------------- + // Detect E-Ink Model + // ------------------- - EInkDetectionResult displayModel = detectEInk(); + EInkDetectionResult displayModel = detectEInk(); - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver; + Drivers::EInk *driver; - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 - driver = new Drivers::LCMEN213EFC1; - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 - driver = new Drivers::E0213A367; + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + driver = new Drivers::E0213A367; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are - if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) - inkhud->setDisplayResilience(10, 1.5); - else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 - inkhud->setDisplayResilience(15, 3); + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + inkhud->setDisplayResilience(15, 3); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // No aux button on this board + // No aux button on this board - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/heltec_wireless_tracker/variant.h b/variants/esp32s3/heltec_wireless_tracker/variant.h index 6c5fb1ce3..3b19f5afd 100644 --- a/variants/esp32s3/heltec_wireless_tracker/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker/variant.h @@ -30,8 +30,9 @@ #define USE_TFTDISPLAY 1 // pin 3 is Vext on v1.1 - HIGH enables LDO for Vext rail which goes to: -// GPS UC6580: GPS V_DET(8), VDD_IO(7), DCDC_IN(21), pulls up RESETN(17), D_SEL(33) and BOOT_MODE(34) through -// 10kR GPS LNA SW7125DE: VCC(4), pulls up SHDN(5) through 10kR LED: VDD, LEDA (through diode) +// GPS UC6580: GPS V_DET(8), VDD_IO(7), DCDC_IN(21), pulls up RESETN(17), D_SEL(33) and BOOT_MODE(34) through 10kR +// GPS LNA SW7125DE: VCC(4), pulls up SHDN(5) through 10kR +// LED: VDD, LEDA (through diode) #define VEXT_ENABLE 3 // active HIGH - powers the GPS, GPS LNA and OLED #define VEXT_ON_VALUE HIGH @@ -50,8 +51,8 @@ #define GPS_TX_PIN 34 #define PIN_GPS_RESET 35 #define PIN_GPS_PPS 36 -// #define PIN_GPS_EN 3 // Uncomment to power off the GPS with triple-click on Tracker v1.1, though we'll also lose -// the display. +// #define PIN_GPS_EN 3 // Uncomment to power off the GPS with triple-click on Tracker v1.1, though we'll also lose the +// display. #define GPS_RESET_MODE LOW #define GPS_UC6580 diff --git a/variants/esp32s3/picomputer-s3/variant.h b/variants/esp32s3/picomputer-s3/variant.h index b594de899..275da1b61 100644 --- a/variants/esp32s3/picomputer-s3/variant.h +++ b/variants/esp32s3/picomputer-s3/variant.h @@ -56,7 +56,11 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { 44, 47, 17, 15, 13, 41 } -#define KEYS_ROWS \ - { 12, 16, 42, 18, 14, 7 } +#define KEYS_COLS \ + { \ + 44, 47, 17, 15, 13, 41 \ + } +#define KEYS_ROWS \ + { \ + 12, 16, 42, 18, 14, 7 \ + } diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index c22aeb26b..f946528ae 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -13,8 +13,9 @@ // #define BUTTON_NEED_PULLUP -// #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery -// voltage #define ADC_CHANNEL ADC1_GPIO27_CHANNEL #define ADC_MULTIPLIER 2 +// #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// #define ADC_CHANNEL ADC1_GPIO27_CHANNEL +// #define ADC_MULTIPLIER 2 // ST7701 TFT LCD #define ST7701_CS (4 | IO_EXPANDER) @@ -44,8 +45,7 @@ #define TOUCH_I2C_PORT 0 #define TOUCH_SLAVE_ADDRESS 0x48 -// in future, we may want to add a buzzer and add all sensors to the indicator via a data protocol for now only GPS is -// supported +// in future, we may want to add a buzzer and add all sensors to the indicator via a data protocol for now only GPS is supported // // Buzzer // #define PIN_BUZZER 19 diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index 933337f6d..36a1310f1 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -90,8 +90,8 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 2.4 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) #define MODEM_POWER_EN 41 #define MODEM_PWRKEY 40 diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index ca9c588bd..8d2996131 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -107,5 +107,5 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) diff --git a/variants/esp32s3/t-eth-elite/rfswitch.h b/variants/esp32s3/t-eth-elite/rfswitch.h index 9332cb747..589f24767 100644 --- a/variants/esp32s3/t-eth-elite/rfswitch.h +++ b/variants/esp32s3/t-eth-elite/rfswitch.h @@ -4,6 +4,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/esp32s3/t-eth-elite/variant.h b/variants/esp32s3/t-eth-elite/variant.h index f8e3bf8c5..b7ac05872 100644 --- a/variants/esp32s3/t-eth-elite/variant.h +++ b/variants/esp32s3/t-eth-elite/variant.h @@ -17,8 +17,8 @@ #define BUTTON_NEED_PULLUP -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for -// RF95 and if not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if +// not found then probe for SX1262 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1280 diff --git a/variants/esp32s3/tbeam-s3-core/rfswitch.h b/variants/esp32s3/tbeam-s3-core/rfswitch.h index 66621b2fe..19080cec6 100644 --- a/variants/esp32s3/tbeam-s3-core/rfswitch.h +++ b/variants/esp32s3/tbeam-s3-core/rfswitch.h @@ -4,6 +4,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index ffba5bbd5..1f900fcae 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -11,8 +11,8 @@ #define LED_STATE_ON 0 // State when LED is lit -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for -// RF95 and if not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if +// not found then probe for SX1262 #define USE_SX1262 #define USE_SX1268 #define USE_LR1121 @@ -31,8 +31,8 @@ // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) #endif // LR1121 diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 8dbbf2286..0fba5a305 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -4,6 +4,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file diff --git a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h index 362ddf4c4..8f5e63653 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h +++ b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h @@ -21,68 +21,69 @@ #include "graphics/niche/Drivers/EInk/DEPG0213BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // Display is connected to HSPI - SPIClass *hspi = new SPIClass(HSPI); - hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::DEPG0213BNS800; - driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::DEPG0213BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the driver - inkhud->setDriver(driver); + // Set the driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(15, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(15, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // Setup the main user button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // Setup the main user button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - buttons->start(); + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/esp32s3/tlora_t3s3_v1/rfswitch.h b/variants/esp32s3/tlora_t3s3_v1/rfswitch.h index 66621b2fe..19080cec6 100644 --- a/variants/esp32s3/tlora_t3s3_v1/rfswitch.h +++ b/variants/esp32s3/tlora_t3s3_v1/rfswitch.h @@ -4,6 +4,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file diff --git a/variants/esp32s3/tlora_t3s3_v1/variant.h b/variants/esp32s3/tlora_t3s3_v1/variant.h index 800cfbeee..babe44a58 100644 --- a/variants/esp32s3/tlora_t3s3_v1/variant.h +++ b/variants/esp32s3/tlora_t3s3_v1/variant.h @@ -19,8 +19,8 @@ #define BUTTON_NEED_PULLUP -// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for -// RF95 and if not found then probe for SX1262 +// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if +// not found then probe for SX1262 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1280 diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index 75f046fd5..2287dfe0b 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -82,8 +82,12 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { 44, 45, 46, 4, 5, 6 } -#define KEYS_ROWS \ - { 26, 37, 17, 16, 15, 7 } +#define KEYS_COLS \ + { \ + 44, 45, 46, 4, 5, 6 \ + } +#define KEYS_ROWS \ + { \ + 26, 37, 17, 16, 15, 7 \ + } // #end keyboard \ No newline at end of file diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index 4be54e276..f42a5b19f 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -106,8 +106,12 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { 44, 45, 46, 4, 5, 6 } -#define KEYS_ROWS \ - { 26, 37, 17, 16, 15, 7 } +#define KEYS_COLS \ + { \ + 44, 45, 46, 4, 5, 6 \ + } +#define KEYS_ROWS \ + { \ + 26, 37, 17, 16, 15, 7 \ + } // #end keyboard \ No newline at end of file diff --git a/variants/esp32s3/tracksenger/oled/variant.h b/variants/esp32s3/tracksenger/oled/variant.h index 7b9a9e9de..85cc019c4 100644 --- a/variants/esp32s3/tracksenger/oled/variant.h +++ b/variants/esp32s3/tracksenger/oled/variant.h @@ -83,8 +83,12 @@ #define INPUTBROKER_MATRIX_TYPE 1 -#define KEYS_COLS \ - { 44, 45, 46, 4, 5, 6 } -#define KEYS_ROWS \ - { 26, 37, 17, 16, 15, 7 } +#define KEYS_COLS \ + { \ + 44, 45, 46, 4, 5, 6 \ + } +#define KEYS_ROWS \ + { \ + 26, 37, 17, 16, 15, 7 \ + } // #end keyboard \ No newline at end of file diff --git a/variants/esp32s3/unphone/variant.cpp b/variants/esp32s3/unphone/variant.cpp index c4811cecb..7884f82e3 100644 --- a/variants/esp32s3/unphone/variant.cpp +++ b/variants/esp32s3/unphone/variant.cpp @@ -3,18 +3,19 @@ #include "unPhone.h" unPhone unphone = unPhone("meshtastic_unphone"); -void initVariant() { - unphone.begin(); // initialise hardware etc. - unphone.store(unphone.buildTime); - unphone.printWakeupReason(); // what woke us up? (stored, not printed :|) - unphone.checkPowerSwitch(); // if power switch is off, shutdown - unphone.backlight(false); // setup backlight and make sure its off - unphone.expanderPower(true); // enable power to expander / hat / sheild +void initVariant() +{ + unphone.begin(); // initialise hardware etc. + unphone.store(unphone.buildTime); + unphone.printWakeupReason(); // what woke us up? (stored, not printed :|) + unphone.checkPowerSwitch(); // if power switch is off, shutdown + unphone.backlight(false); // setup backlight and make sure its off + unphone.expanderPower(true); // enable power to expander / hat / sheild - for (int i = 0; i < 3; i++) { // buzz a bit - unphone.vibe(true); - delay(150); - unphone.vibe(false); - delay(150); - } + for (int i = 0; i < 3; i++) { // buzz a bit + unphone.vibe(true); + delay(150); + unphone.vibe(false); + delay(150); + } } \ No newline at end of file diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h index 8f9322ea7..6f0710d62 100644 --- a/variants/esp32s3/unphone/variant.h +++ b/variants/esp32s3/unphone/variant.h @@ -45,10 +45,10 @@ #define USE_POWERSAVE #define SLEEP_TIME 180 -#define HAS_GPS \ - 0 // the unphone doesn't have a gps module by default (though - // GPS featherwing -- https://www.adafruit.com/product/3133 - // -- can be added) +#define HAS_GPS \ + 0 // the unphone doesn't have a gps module by default (though + // GPS featherwing -- https://www.adafruit.com/product/3133 + // -- can be added) #undef GPS_RX_PIN #undef GPS_TX_PIN diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp index 1226397f1..2fc87c718 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp @@ -27,8 +27,9 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); } \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h index 77ddb38a1..f64de9d07 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -25,93 +25,94 @@ // Button feedback #include "buzz.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::GDEY0154D67; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - // Todo: observe the display's performance in-person and adjust accordingly. - // Currently set to the values given by Elecrow for EInkDynamicDisplay. - inkhud->setDisplayResilience(10, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + // Todo: observe the display's performance in-person and adjust accordingly. + // Currently set to the values given by Elecrow for EInkDynamicDisplay. + inkhud->setDisplayResilience(10, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - // Setup backlight controller - // Note: button is attached further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + // Setup backlight controller + // Note: button is attached further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // Elecrow diagram: https://www.elecrow.com/download/product/CIL12901M/ThinkNode-M1_User_Manual.pdf + // Elecrow diagram: https://www.elecrow.com/download/product/CIL12901M/ThinkNode-M1_User_Manual.pdf - // #0: Main User Button - // Labeled "Page Turn Button" by manual - buttons->setWiring(0, PIN_BUTTON2); - buttons->setTiming(0, 50, 500); // Todo: confirm 50ms is adequate debounce - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + // Labeled "Page Turn Button" by manual + buttons->setWiring(0, PIN_BUTTON2); + buttons->setTiming(0, 50, 500); // Todo: confirm 50ms is adequate debounce + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button - // Labeled "Function Button" by manual - // Todo: additional features - buttons->setWiring(1, PIN_BUTTON1); - buttons->setTiming(1, 50, 500); // 500ms before latch - buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); - buttons->setHandlerLongPress(1, [backlight]() { - backlight->latch(); - playBoop(); - }); - buttons->setHandlerShortPress(1, [backlight]() { - backlight->off(); - playChirp(); - }); + // #1: Aux Button + // Labeled "Function Button" by manual + // Todo: additional features + buttons->setWiring(1, PIN_BUTTON1); + buttons->setTiming(1, 50, 500); // 500ms before latch + buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); + buttons->setHandlerLongPress(1, [backlight]() { + backlight->latch(); + playBoop(); + }); + buttons->setHandlerShortPress(1, [backlight]() { + backlight->off(); + playChirp(); + }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index 4454a1d84..cae079b74 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -30,14 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index 03a7d11bb..cde0f49c1 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -117,8 +117,8 @@ External serial flash WP25R1635FZUIL0 #define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 20) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching -// #define SX1262_DIO3 (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the -// tcxo, do not drive from the main +// #define SX1262_DIO3 (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not +// drive from the main #define SX126X_BUSY (0 + 17) #define SX126X_RESET (0 + 25) #define SX126X_DIO2_AS_RF_SWITCH diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h b/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h index f1fd32ea0..77ae9ef73 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h @@ -8,6 +8,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp index b42c1273c..9769e3edd 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp @@ -31,71 +31,74 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - pinMode(KEY_POWER, OUTPUT); - digitalWrite(KEY_POWER, HIGH); - pinMode(RGB_POWER, OUTPUT); - digitalWrite(RGB_POWER, HIGH); - pinMode(green_LED_PIN, OUTPUT); - digitalWrite(green_LED_PIN, LED_STATE_OFF); - pinMode(LED_BLUE, OUTPUT); - pinMode(PIN_POWER_USB, INPUT); - pinMode(PIN_POWER_DONE, INPUT); - pinMode(PIN_POWER_CHRG, INPUT); - pinMode(BUTTON_PIN, INPUT_PULLUP); - pinMode(EEPROM_POWER, OUTPUT); - digitalWrite(EEPROM_POWER, HIGH); - pinMode(PIN_EN1, OUTPUT); - digitalWrite(PIN_EN1, HIGH); - pinMode(PIN_EN2, OUTPUT); - digitalWrite(PIN_EN2, HIGH); - pinMode(ACC_POWER, OUTPUT); - digitalWrite(ACC_POWER, LOW); - pinMode(DHT_POWER, OUTPUT); - digitalWrite(DHT_POWER, HIGH); - pinMode(Battery_POWER, OUTPUT); - digitalWrite(Battery_POWER, HIGH); - pinMode(GPS_POWER, OUTPUT); - digitalWrite(GPS_POWER, HIGH); +void initVariant() +{ + pinMode(KEY_POWER, OUTPUT); + digitalWrite(KEY_POWER, HIGH); + pinMode(RGB_POWER, OUTPUT); + digitalWrite(RGB_POWER, HIGH); + pinMode(green_LED_PIN, OUTPUT); + digitalWrite(green_LED_PIN, LED_STATE_OFF); + pinMode(LED_BLUE, OUTPUT); + pinMode(PIN_POWER_USB, INPUT); + pinMode(PIN_POWER_DONE, INPUT); + pinMode(PIN_POWER_CHRG, INPUT); + pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(EEPROM_POWER, OUTPUT); + digitalWrite(EEPROM_POWER, HIGH); + pinMode(PIN_EN1, OUTPUT); + digitalWrite(PIN_EN1, HIGH); + pinMode(PIN_EN2, OUTPUT); + digitalWrite(PIN_EN2, HIGH); + pinMode(ACC_POWER, OUTPUT); + digitalWrite(ACC_POWER, LOW); + pinMode(DHT_POWER, OUTPUT); + digitalWrite(DHT_POWER, HIGH); + pinMode(Battery_POWER, OUTPUT); + digitalWrite(Battery_POWER, HIGH); + pinMode(GPS_POWER, OUTPUT); + digitalWrite(GPS_POWER, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function -void variant_shutdown() { - digitalWrite(red_LED_PIN, HIGH); - digitalWrite(green_LED_PIN, HIGH); - digitalWrite(LED_BLUE, HIGH); +void variant_shutdown() +{ + digitalWrite(red_LED_PIN, HIGH); + digitalWrite(green_LED_PIN, HIGH); + digitalWrite(LED_BLUE, HIGH); - digitalWrite(PIN_EN1, LOW); - digitalWrite(PIN_EN2, LOW); - digitalWrite(EEPROM_POWER, LOW); - digitalWrite(KEY_POWER, LOW); - digitalWrite(DHT_POWER, LOW); - digitalWrite(ACC_POWER, LOW); - digitalWrite(Battery_POWER, LOW); - digitalWrite(GPS_POWER, LOW); + digitalWrite(PIN_EN1, LOW); + digitalWrite(PIN_EN2, LOW); + digitalWrite(EEPROM_POWER, LOW); + digitalWrite(KEY_POWER, LOW); + digitalWrite(DHT_POWER, LOW); + digitalWrite(ACC_POWER, LOW); + digitalWrite(Battery_POWER, LOW); + digitalWrite(GPS_POWER, LOW); - // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. - for (int pin = 0; pin < 48; pin++) { - if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || pin == ACC_POWER || - pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || - pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || - pin == GPS_RX_PIN || pin == green_LED_PIN || pin == red_LED_PIN || pin == LED_BLUE) { - continue; + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || + pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || + pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || + pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == green_LED_PIN || + pin == red_LED_PIN || pin == LED_BLUE) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } } - pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); - if (pin >= 32) { - NRF_P1->DIRCLR = (1 << (pin - 32)); - } else { - NRF_GPIO->DIRCLR = (1 << pin); - } - } - nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(BUTTON_PIN, sense1); + nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(BUTTON_PIN, sense1); - nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH; - nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2); + nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH; + nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2); } \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index d271c1c40..9c7b521ef 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -30,38 +30,41 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - pinMode(LED_CHARGE, OUTPUT); - ledOff(LED_CHARGE); +void initVariant() +{ + pinMode(LED_CHARGE, OUTPUT); + ledOff(LED_CHARGE); - pinMode(LED_PAIRING, OUTPUT); - ledOff(LED_PAIRING); + pinMode(LED_PAIRING, OUTPUT); + ledOff(LED_PAIRING); - pinMode(VDD_FLASH_EN, OUTPUT); - digitalWrite(VDD_FLASH_EN, HIGH); + pinMode(VDD_FLASH_EN, OUTPUT); + digitalWrite(VDD_FLASH_EN, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function -void variant_shutdown() { - // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. - for (int pin = 0; pin < 48; pin++) { - if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || pin == PIN_SPI_SCK) { - continue; +void variant_shutdown() +{ + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || + pin == PIN_SPI_SCK) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } } - pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); - if (pin >= 32) { - NRF_P1->DIRCLR = (1 << (pin - 32)); - } else { - NRF_GPIO->DIRCLR = (1 << pin); - } - } - digitalWrite(PIN_GPS_EN, LOW); - digitalWrite(ADC_CTRL, LOW); - // digitalWrite(RTC_POWER, LOW); + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(ADC_CTRL, LOW); + // digitalWrite(RTC_POWER, LOW); - nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); } diff --git a/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h b/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h index 8f753df83..cda6364f5 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h +++ b/variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h @@ -8,6 +8,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp index 747bb14de..35dc1d39b 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp @@ -30,10 +30,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() +{ + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h index 8f753df83..cda6364f5 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h @@ -8,6 +8,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp index 747bb14de..35dc1d39b 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp @@ -30,10 +30,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() +{ + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp index 89f157bfd..8c6bf039c 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp @@ -27,11 +27,12 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp index 89f157bfd..8c6bf039c 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp @@ -27,11 +27,12 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf52840/TWC_mesh_v4/variant.cpp b/variants/nrf52840/TWC_mesh_v4/variant.cpp index cad7c6518..b3712346d 100644 --- a/variants/nrf52840/TWC_mesh_v4/variant.cpp +++ b/variants/nrf52840/TWC_mesh_v4/variant.cpp @@ -27,11 +27,12 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } \ No newline at end of file diff --git a/variants/nrf52840/canaryone/variant.cpp b/variants/nrf52840/canaryone/variant.cpp index 5a33c1e9a..5967a2a96 100644 --- a/variants/nrf52840/canaryone/variant.cpp +++ b/variants/nrf52840/canaryone/variant.cpp @@ -30,26 +30,27 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LEDs - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LEDs + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); - // Turn on power to the GPS and LoRa - pinMode(PIN_PWR_EN, OUTPUT); - digitalWrite(PIN_PWR_EN, HIGH); + // Turn on power to the GPS and LoRa + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); - // Pull the GPS out of reset - pinMode(GPS_RESET_PIN, OUTPUT); - digitalWrite(GPS_RESET_PIN, HIGH); + // Pull the GPS out of reset + pinMode(GPS_RESET_PIN, OUTPUT); + digitalWrite(GPS_RESET_PIN, HIGH); - // Pull the LoRa out of reset - pinMode(LORA_RF_PWR, OUTPUT); - digitalWrite(LORA_RF_PWR, HIGH); + // Pull the LoRa out of reset + pinMode(LORA_RF_PWR, OUTPUT); + digitalWrite(LORA_RF_PWR, HIGH); } diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 2cd3f6c8c..61d1e8df9 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -148,8 +148,7 @@ static const uint8_t A0 = PIN_A0; #define PIN_SPI_MOSI (GPIO_PORT0 + 22) #define PIN_SPI_SCK (GPIO_PORT0 + 19) -// #define PIN_SPI1_MISO (GPIO_PORT1 + 6) // FIXME not really needed, but for now the SPI code requires something to be -// defined, +// #define PIN_SPI1_MISO (GPIO_PORT1 + 6) // FIXME not really needed, but for now the SPI code requires something to be defined, // pick an used GPIO #define PIN_SPI1_MOSI (GPIO_PORT1 + 8) #define PIN_SPI1_SCK (GPIO_PORT1 + 9) #define PIN_PWR_EN (GPIO_PORT0 + 12) diff --git a/variants/nrf52840/cpp_overrides/lfs_util.h b/variants/nrf52840/cpp_overrides/lfs_util.h index 52c10ab2e..bf5a347b1 100644 --- a/variants/nrf52840/cpp_overrides/lfs_util.h +++ b/variants/nrf52840/cpp_overrides/lfs_util.h @@ -5,11 +5,11 @@ * SPDX-License-Identifier: BSD-3-Clause */ -// MESHTASTIC/@geeksville note: This file is copied from the Adafruit nrf52 arduino lib. And we use a special -include -// in nrf52.ini to load it before EVERY file we do this hack because the default definitions for LFS_ASSERT are quite -// poor and we don't want to fork the adafruit lib (again) and send in a PR that they probably won't merge anyways. This -// file might break if they ever update lfs.util on their side, in which case we'll need to update this file to match -// their new version. The version this is a copy from is almost exactly +// MESHTASTIC/@geeksville note: This file is copied from the Adafruit nrf52 arduino lib. And we use a special -include in +// nrf52.ini to load it before EVERY file we do this hack because the default definitions for LFS_ASSERT are quite poor and we +// don't want to fork the adafruit lib (again) and send in a PR that they probably won't merge anyways. This file might break if +// they ever update lfs.util on their side, in which case we'll need to update this file to match their new version. The version +// this is a copy from is almost exactly // https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/c25d93268a3b9c23e9a1ccfcaf9b208beca624ca/libraries/Adafruit_LittleFS/src/littlefs/lfs_util.h #ifndef LFS_UTIL_H @@ -77,9 +77,9 @@ void logLegacy(const char *level, const char *fmt, ...); #define LFS_ASSERT(test) assert(test) #else extern void lfs_assert(const char *reason); -#define LFS_ASSERT(test) \ - if (!(test)) \ - lfs_assert(#test) +#define LFS_ASSERT(test) \ + if (!(test)) \ + lfs_assert(#test) #endif // Builtin functions, these may be replaced by more efficient @@ -87,98 +87,116 @@ extern void lfs_assert(const char *reason); // expensive basic C implementation for debugging purposes // Min/max functions for unsigned 32-bit numbers -static inline uint32_t lfs_max(uint32_t a, uint32_t b) { return (a > b) ? a : b; } +static inline uint32_t lfs_max(uint32_t a, uint32_t b) +{ + return (a > b) ? a : b; +} -static inline uint32_t lfs_min(uint32_t a, uint32_t b) { return (a < b) ? a : b; } +static inline uint32_t lfs_min(uint32_t a, uint32_t b) +{ + return (a < b) ? a : b; +} // Find the next smallest power of 2 less than or equal to a -static inline uint32_t lfs_npw2(uint32_t a) { +static inline uint32_t lfs_npw2(uint32_t a) +{ #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return 32 - __builtin_clz(a - 1); + return 32 - __builtin_clz(a - 1); #else - uint32_t r = 0; - uint32_t s; - a -= 1; - s = (a > 0xffff) << 4; - a >>= s; - r |= s; - s = (a > 0xff) << 3; - a >>= s; - r |= s; - s = (a > 0xf) << 2; - a >>= s; - r |= s; - s = (a > 0x3) << 1; - a >>= s; - r |= s; - return (r | (a >> 1)) + 1; + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; + a >>= s; + r |= s; + s = (a > 0xff) << 3; + a >>= s; + r |= s; + s = (a > 0xf) << 2; + a >>= s; + r |= s; + s = (a > 0x3) << 1; + a >>= s; + r |= s; + return (r | (a >> 1)) + 1; #endif } // Count the number of trailing binary zeros in a // lfs_ctz(0) may be undefined -static inline uint32_t lfs_ctz(uint32_t a) { +static inline uint32_t lfs_ctz(uint32_t a) +{ #if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) - return __builtin_ctz(a); + return __builtin_ctz(a); #else - return lfs_npw2((a & -a) + 1) - 1; + return lfs_npw2((a & -a) + 1) - 1; #endif } // Count the number of binary ones in a -static inline uint32_t lfs_popc(uint32_t a) { +static inline uint32_t lfs_popc(uint32_t a) +{ #if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) - return __builtin_popcount(a); + return __builtin_popcount(a); #else - a = a - ((a >> 1) & 0x55555555); - a = (a & 0x33333333) + ((a >> 2) & 0x33333333); - return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; #endif } // Find the sequence comparison of a and b, this is the distance // between a and b ignoring overflow -static inline int lfs_scmp(uint32_t a, uint32_t b) { return (int)(unsigned)(a - b); } +static inline int lfs_scmp(uint32_t a, uint32_t b) +{ + return (int)(unsigned)(a - b); +} // Convert from 32-bit little-endian to native order -static inline uint32_t lfs_fromle32(uint32_t a) { -#if !defined(LFS_NO_INTRINSICS) && \ - ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ - (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) - return a; -#elif !defined(LFS_NO_INTRINSICS) && \ - ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ +static inline uint32_t lfs_fromle32(uint32_t a) +{ +#if !defined(LFS_NO_INTRINSICS) && ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_LITTLE_ENDIAN) || \ + (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return a; +#elif !defined(LFS_NO_INTRINSICS) && \ + ((defined(BYTE_ORDER) && BYTE_ORDER == ORDER_BIG_ENDIAN) || (defined(__BYTE_ORDER) && __BYTE_ORDER == __ORDER_BIG_ENDIAN) || \ (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) - return __builtin_bswap32(a); + return __builtin_bswap32(a); #else - return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); + return (((uint8_t *)&a)[0] << 0) | (((uint8_t *)&a)[1] << 8) | (((uint8_t *)&a)[2] << 16) | (((uint8_t *)&a)[3] << 24); #endif } // Convert to 32-bit little-endian from native order -static inline uint32_t lfs_tole32(uint32_t a) { return lfs_fromle32(a); } +static inline uint32_t lfs_tole32(uint32_t a) +{ + return lfs_fromle32(a); +} // Calculate CRC-32 with polynomial = 0x04c11db7 void lfs_crc(uint32_t *crc, const void *buffer, size_t size); // Allocate memory, only used if buffers are not provided to littlefs -static inline void *lfs_malloc(size_t size) { +static inline void *lfs_malloc(size_t size) +{ #ifndef LFS_NO_MALLOC - extern void *pvPortMalloc(size_t xWantedSize); - return pvPortMalloc(size); + extern void *pvPortMalloc(size_t xWantedSize); + return pvPortMalloc(size); #else - (void)size; - return NULL; + (void)size; + return NULL; #endif } // Deallocate memory, only used if buffers are not provided to littlefs -static inline void lfs_free(void *p) { +static inline void lfs_free(void *p) +{ #ifndef LFS_NO_MALLOC - extern void vPortFree(void *pv); - vPortFree(p); + extern void vPortFree(void *pv); + vPortFree(p); #else - (void)p; + (void)p; #endif } diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h index 661ed29e9..8f30a244f 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h @@ -29,66 +29,67 @@ #error If not using a DIY preset, display model and resilience must be set manually #endif -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- - SPI.begin(); + // SPI + // ----------------------------- + SPI.begin(); - // Driver - // ----------------------------- + // Driver + // ----------------------------- - // Use E-Ink driver - Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; - driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the driver - inkhud->setDriver(driver); + // Set the driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update. - inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Init settings, and customize defaults - // Values ignored individually if found saved to flash - inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed - inkhud->persistence->settings.userTiles.maxCount = 4; - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); - inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // Setup the main user button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // Setup the main user button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - buttons->start(); + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h index f0994b39c..71508c037 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -8,7 +8,8 @@ // DIO6 -> RFSW1_V2 // DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. -static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, + RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp index c298b73bb..5869ed1d4 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp @@ -30,8 +30,9 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); +void initVariant() +{ + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h index 8bd65bd57..63af1fe79 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h @@ -136,8 +136,8 @@ NRF52 PRO MICRO PIN ASSIGNMENT // SX126X CONFIG #define SX126X_CS (32 + 13) // P1.13 FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 10) // P0.10 IRQ -#define SX126X_DIO2_AS_RF_SWITCH // Note for E22 modules: DIO2 is not attached internally to TXEN for automatic TX/RX - // switching, so it needs connecting externally if it is used in this way +#define SX126X_DIO2_AS_RF_SWITCH // Note for E22 modules: DIO2 is not attached internally to TXEN for automatic TX/RX switching, + // so it needs connecting externally if it is used in this way #define SX126X_BUSY (0 + 29) // P0.29 #define SX126X_RESET (0 + 9) // P0.09 #define SX126X_RXEN (0 + 17) // P0.17 @@ -159,8 +159,8 @@ NRF52 PRO MICRO PIN ASSIGNMENT // #define SX126X_MAX_POWER 8 set this if using a high-power board! /* -On the SX1262, DIO3 sets the voltage for an external TCXO, if one is present. If one is not present, use TCXO_OPTIONAL -to try both settings. +On the SX1262, DIO3 sets the voltage for an external TCXO, if one is present. If one is not present, use TCXO_OPTIONAL to try both +settings. | Mfr | Module | TCXO | RF Switch | Notes | | ------------ | ---------------- | ---- | --------- | ------------------------------------- | diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp index 2eb468363..300f69d0b 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp @@ -54,8 +54,9 @@ const uint32_t g_ADigitalPinMap[] = { 31, // D32 is P0.10 (VBAT) }; -void initVariant() { - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); +void initVariant() +{ + // Set BQ25101 ISET to 100mA instead of 50mA + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); } diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h index c33e11654..6337ac70c 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h @@ -195,8 +195,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -232,9 +232,11 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_BAUDRATE 9600 diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h index 6ec89a0fd..b6be70ff4 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h @@ -30,66 +30,67 @@ #error If not using a DIY preset, display model and resilience must be set manually #endif -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- - SPI1.begin(); + // SPI + // ----------------------------- + SPI1.begin(); - // Driver - // ----------------------------- + // Driver + // ----------------------------- - // Use E-Ink driver - Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the driver - inkhud->setDriver(driver); + // Set the driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update. - inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Init settings, and customize defaults - // Values ignored individually if found saved to flash - inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed - inkhud->persistence->settings.userTiles.maxCount = 4; - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); - inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - buttons->start(); + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp index eb70fe5e9..85c9f4a72 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp @@ -30,8 +30,9 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); } diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h index 26f4d364f..14170d5f3 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h @@ -1,6 +1,5 @@ // Unlike many other InkHUD variants, this environment does require its own variant.h file -// This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken -// out +// This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken out #ifndef _VARIANT_HELTEC_NRF_ #define _VARIANT_HELTEC_NRF_ @@ -43,8 +42,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a -// regular GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO /* No longer populated on PCB diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp index eb70fe5e9..85c9f4a72 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp @@ -30,8 +30,9 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); } diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index e352530a6..bad488b35 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -86,8 +86,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a -// regular GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO /* No longer populated on PCB @@ -145,7 +145,8 @@ No longer populated on PCB #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define PIN_SPI1_MISO ST7789_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MISO \ + ST7789_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO #define PIN_SPI1_MOSI ST7789_SDA #define PIN_SPI1_SCK ST7789_SCK diff --git a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h index a8a6c6519..f8202debb 100644 --- a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h @@ -21,69 +21,70 @@ #include "graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h" #include "graphics/niche/Inputs/TwoButton.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::LCMEN2R13ECC1; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::LCMEN2R13ECC1; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(10, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = true; + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_pocket/variant.h b/variants/nrf52840/heltec_mesh_pocket/variant.h index 8133583c2..7ec9b88ea 100644 --- a/variants/nrf52840/heltec_mesh_pocket/variant.h +++ b/variants/nrf52840/heltec_mesh_pocket/variant.h @@ -34,8 +34,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a -// regular GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO /* No longer populated on PCB diff --git a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h index 5f6ec3146..125f50590 100644 --- a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h @@ -21,69 +21,70 @@ #include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Inputs/TwoButton.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::E0213A367; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::E0213A367; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(10, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.optionalMenuItems.nextTile = true; + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = true; - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp index 915015cb5..c13f006d7 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.cpp +++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp @@ -30,10 +30,11 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); +void initVariant() +{ + pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); #if defined(PIN_SCREEN_VDD_CTL) - pinMode(PIN_SCREEN_VDD_CTL, OUTPUT); - digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on + pinMode(PIN_SCREEN_VDD_CTL, OUTPUT); + digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on #endif } diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h index 0fe7528ba..112bcd8b3 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.h +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -54,8 +54,8 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) -// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a -// regular GPIO +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO /* No longer populated on PCB diff --git a/variants/nrf52840/meshlink/variant.cpp b/variants/nrf52840/meshlink/variant.cpp index b0765a547..81a5097c4 100644 --- a/variants/nrf52840/meshlink/variant.cpp +++ b/variants/nrf52840/meshlink/variant.cpp @@ -10,13 +10,14 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting - // otherwise it will stay lit for several seconds (could be annoying) +void initVariant() +{ + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting + // otherwise it will stay lit for several seconds (could be annoying) #ifdef PIN_WD_EN - pinMode(PIN_WD_EN, OUTPUT); - digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot + pinMode(PIN_WD_EN, OUTPUT); + digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot #endif } \ No newline at end of file diff --git a/variants/nrf52840/meshtiny/variant.cpp b/variants/nrf52840/meshtiny/variant.cpp index c391d4de8..2e8b00e4b 100644 --- a/variants/nrf52840/meshtiny/variant.cpp +++ b/variants/nrf52840/meshtiny/variant.cpp @@ -30,24 +30,25 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); - // Initialize Encoder pins - pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP); - pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP); - pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP); + // Initialize Encoder pins + pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP); + pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP); + pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP); - // Initialize Buzzer pin - pinMode(PIN_BUZZER, OUTPUT); - digitalWrite(PIN_BUZZER, LOW); + // Initialize Buzzer pin + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); } diff --git a/variants/nrf52840/monteops_hw1/variant.cpp b/variants/nrf52840/monteops_hw1/variant.cpp index 90a0b7116..75cca1dc3 100644 --- a/variants/nrf52840/monteops_hw1/variant.cpp +++ b/variants/nrf52840/monteops_hw1/variant.cpp @@ -30,11 +30,12 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf52840/monteops_hw1/variant.h b/variants/nrf52840/monteops_hw1/variant.h index b5afb2441..97536b169 100644 --- a/variants/nrf52840/monteops_hw1/variant.h +++ b/variants/nrf52840/monteops_hw1/variant.h @@ -176,8 +176,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG diff --git a/variants/nrf52840/muzi_base/rfswitch.h b/variants/nrf52840/muzi_base/rfswitch.h index 9332cb747..589f24767 100644 --- a/variants/nrf52840/muzi_base/rfswitch.h +++ b/variants/nrf52840/muzi_base/rfswitch.h @@ -4,6 +4,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/muzi_base/variant.cpp b/variants/nrf52840/muzi_base/variant.cpp index 4d116b288..da01de974 100644 --- a/variants/nrf52840/muzi_base/variant.cpp +++ b/variants/nrf52840/muzi_base/variant.cpp @@ -57,26 +57,27 @@ const uint32_t g_ADigitalPinMap[] = { 47, }; -void initVariant() { - // Initialize the digital pins as inputs or outputs - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, HIGH); +void initVariant() +{ + // Initialize the digital pins as inputs or outputs + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, HIGH); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, HIGH); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, HIGH); - // Initialize LoRa pins - pinMode(SX126X_RESET, OUTPUT); - digitalWrite(SX126X_RESET, HIGH); + // Initialize LoRa pins + pinMode(SX126X_RESET, OUTPUT); + digitalWrite(SX126X_RESET, HIGH); - pinMode(SX126X_CS, OUTPUT); - digitalWrite(SX126X_CS, HIGH); + pinMode(SX126X_CS, OUTPUT); + digitalWrite(SX126X_CS, HIGH); - pinMode(GPS_EN_GPIO, OUTPUT); - digitalWrite(GPS_EN_GPIO, HIGH); // GPS on initially + pinMode(GPS_EN_GPIO, OUTPUT); + digitalWrite(GPS_EN_GPIO, HIGH); // GPS on initially - pinMode(SCREEN_12V_ENABLE, OUTPUT); - digitalWrite(SCREEN_12V_ENABLE, LOW); // + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, LOW); // - pinMode(BATTERY_CHARGING_INV, INPUT); + pinMode(BATTERY_CHARGING_INV, INPUT); } diff --git a/variants/nrf52840/muzi_base/variant.h b/variants/nrf52840/muzi_base/variant.h index cb6f87132..96604c400 100644 --- a/variants/nrf52840/muzi_base/variant.h +++ b/variants/nrf52840/muzi_base/variant.h @@ -132,32 +132,37 @@ extern "C" { #define USERPREFS_OEM_FONT_SIZE 0 #define USERPREFS_OEM_IMAGE_WIDTH 88 // 11 bytes wide #define USERPREFS_OEM_IMAGE_HEIGHT 47 // 517 bytes total -#define USERPREFS_OEM_IMAGE_DATA \ - { \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x3C, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x0F, 0xFF, 0x00, 0xF0, 0xFF, 0x0F, 0xC0, 0xFF, 0x07, 0x78, 0xFF, 0x9F, 0xFF, 0x01, 0xF0, \ - 0xFF, 0x0F, 0xC0, 0xFF, 0x03, 0x78, 0x3F, 0xFE, 0xF3, 0x01, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x03, 0x78, 0x1F, 0xFC, 0xC0, 0x03, 0xF0, 0xFF, \ - 0x0F, 0x00, 0xE0, 0x01, 0x78, 0x0F, 0xF8, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x78, 0x80, 0x03, 0xF0, 0xFF, 0x0F, \ - 0x00, 0x70, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x78, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, \ - 0x3C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1E, \ - 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x0F, 0x00, 0x0F, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x07, 0x80, 0x07, 0x00, \ - 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0x78, \ - 0x07, 0x70, 0x80, 0x03, 0x00, 0xFF, 0x01, 0xE0, 0xFF, 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0x7C, 0x00, 0xF0, 0xFF, 0x07, 0x78, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, \ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, \ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE3, 0xE7, 0xC7, 0x1F, 0xF8, 0x0F, 0xF0, 0xE7, 0xE3, 0x07, 0x7C, 0xC7, 0xE7, 0xC3, 0x0F, 0xE0, \ - 0x07, 0xE0, 0xC7, 0xE1, 0x03, 0x70, 0xC7, 0xC3, 0xE3, 0x87, 0xC1, 0x07, 0xC0, 0xC7, 0xF8, 0xE3, 0x71, 0xC7, 0xC3, 0xE3, 0xE3, 0xC7, 0xC7, \ - 0xC7, 0x47, 0xF8, 0xF3, 0x7F, 0x8F, 0xC3, 0xF1, 0xE3, 0x8F, 0xC7, 0x8F, 0x27, 0xFC, 0xE3, 0x7F, 0x8F, 0x81, 0xF1, 0xF1, 0x8F, 0xC7, 0xCF, \ - 0x07, 0xFE, 0x03, 0x7E, 0x8F, 0x99, 0xF1, 0xF1, 0x8F, 0x07, 0xC0, 0x07, 0xFF, 0x07, 0x78, 0x9F, 0x99, 0xF9, 0xF1, 0x8F, 0x07, 0xE0, 0x07, \ - 0xFE, 0x3F, 0x70, 0x1F, 0x18, 0xF8, 0xF3, 0x8F, 0x07, 0xF0, 0x27, 0xFC, 0xFF, 0x71, 0x3F, 0x18, 0xF8, 0xE3, 0xC7, 0xC7, 0xF1, 0x47, 0xF8, \ - 0xF3, 0x63, 0x3F, 0x3C, 0xFC, 0xC3, 0xC3, 0xC7, 0xE3, 0xC7, 0xF0, 0xE1, 0x71, 0x3F, 0x3C, 0xFC, 0x07, 0xE0, 0xC7, 0xC7, 0xC7, 0xE1, 0x03, \ - 0x70, 0x7F, 0x7E, 0xFE, 0x0F, 0xF0, 0xC7, 0x87, 0xC7, 0xC3, 0x07, 0x78, 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x7E, \ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, \ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F \ - } +#define USERPREFS_OEM_IMAGE_DATA \ + { \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, \ + 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0xF7, 0x0F, 0xFF, 0x00, 0xF0, 0xFF, 0x0F, 0xC0, 0xFF, 0x07, 0x78, 0xFF, 0x9F, 0xFF, 0x01, 0xF0, 0xFF, 0x0F, 0xC0, \ + 0xFF, 0x03, 0x78, 0x3F, 0xFE, 0xF3, 0x01, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x03, 0x78, 0x1F, 0xFC, 0xC0, 0x03, 0xF0, \ + 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x78, 0x0F, 0xF8, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x78, \ + 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x78, 0x00, \ + 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x3C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, \ + 0x00, 0x1C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1E, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, \ + 0xE0, 0xFF, 0x0F, 0x00, 0x0F, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, \ + 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x03, 0xC0, 0xFF, \ + 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0xFF, 0x01, 0xE0, 0xFF, 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0x7C, \ + 0x00, 0xF0, 0xFF, 0x07, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ + 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE3, 0xE7, 0xC7, 0x1F, 0xF8, 0x0F, 0xF0, 0xE7, 0xE3, 0x07, 0x7C, 0xC7, 0xE7, \ + 0xC3, 0x0F, 0xE0, 0x07, 0xE0, 0xC7, 0xE1, 0x03, 0x70, 0xC7, 0xC3, 0xE3, 0x87, 0xC1, 0x07, 0xC0, 0xC7, 0xF8, 0xE3, \ + 0x71, 0xC7, 0xC3, 0xE3, 0xE3, 0xC7, 0xC7, 0xC7, 0x47, 0xF8, 0xF3, 0x7F, 0x8F, 0xC3, 0xF1, 0xE3, 0x8F, 0xC7, 0x8F, \ + 0x27, 0xFC, 0xE3, 0x7F, 0x8F, 0x81, 0xF1, 0xF1, 0x8F, 0xC7, 0xCF, 0x07, 0xFE, 0x03, 0x7E, 0x8F, 0x99, 0xF1, 0xF1, \ + 0x8F, 0x07, 0xC0, 0x07, 0xFF, 0x07, 0x78, 0x9F, 0x99, 0xF9, 0xF1, 0x8F, 0x07, 0xE0, 0x07, 0xFE, 0x3F, 0x70, 0x1F, \ + 0x18, 0xF8, 0xF3, 0x8F, 0x07, 0xF0, 0x27, 0xFC, 0xFF, 0x71, 0x3F, 0x18, 0xF8, 0xE3, 0xC7, 0xC7, 0xF1, 0x47, 0xF8, \ + 0xF3, 0x63, 0x3F, 0x3C, 0xFC, 0xC3, 0xC3, 0xC7, 0xE3, 0xC7, 0xF0, 0xE1, 0x71, 0x3F, 0x3C, 0xFC, 0x07, 0xE0, 0xC7, \ + 0xC7, 0xC7, 0xE1, 0x03, 0x70, 0x7F, 0x7E, 0xFE, 0x0F, 0xF0, 0xC7, 0x87, 0xC7, 0xC3, 0x07, 0x78, 0xFF, 0xFF, 0xFF, \ + 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, \ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ + 0xFF, 0xFF, 0x7F \ + } // QSPI Pins #define PIN_QSPI_SCK (0 + 3) diff --git a/variants/nrf52840/nano-g2-ultra/variant.cpp b/variants/nrf52840/nano-g2-ultra/variant.cpp index ade2f34c8..ce5d00886 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.cpp +++ b/variants/nrf52840/nano-g2-ultra/variant.cpp @@ -30,6 +30,7 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // Nothing need to be inited for now +void initVariant() +{ + // Nothing need to be inited for now } \ No newline at end of file diff --git a/variants/nrf52840/nano-g2-ultra/variant.h b/variants/nrf52840/nano-g2-ultra/variant.h index 4753fac6c..d8f41a68c 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.h +++ b/variants/nrf52840/nano-g2-ultra/variant.h @@ -112,8 +112,7 @@ External serial flash W25Q16JV_IQ #define SX126X_DIO1 (32 + 10) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching // #define SX1262_DIO3 (0 + 21) -// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main -// CPU? +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main CPU? #define SX126X_BUSY (32 + 11) #define SX126X_RESET (32 + 15) // DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 diff --git a/variants/nrf52840/r1-neo/variant.cpp b/variants/nrf52840/r1-neo/variant.cpp index 275173219..f87c041aa 100644 --- a/variants/nrf52840/r1-neo/variant.cpp +++ b/variants/nrf52840/r1-neo/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - // pinMode(PIN_3V3_EN, OUTPUT); - // digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + // pinMode(PIN_3V3_EN, OUTPUT); + // digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak2560/variant.cpp b/variants/nrf52840/rak2560/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak2560/variant.cpp +++ b/variants/nrf52840/rak2560/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak2560/variant.h b/variants/nrf52840/rak2560/variant.h index f40bcc1e4..f922e8a61 100644 --- a/variants/nrf52840/rak2560/variant.h +++ b/variants/nrf52840/rak2560/variant.h @@ -184,8 +184,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -215,9 +215,11 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_SERIAL_PORT Serial2 diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak3401_1watt/variant.cpp +++ b/variants/nrf52840/rak3401_1watt/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h index 4f9d60f4b..d4bb1a175 100644 --- a/variants/nrf52840/rak3401_1watt/variant.h +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -181,9 +181,11 @@ static const uint8_t SCK = PIN_SPI_SCK; // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak4631/variant.cpp b/variants/nrf52840/rak4631/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak4631/variant.cpp +++ b/variants/nrf52840/rak4631/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631/variant.h b/variants/nrf52840/rak4631/variant.h index ac3554718..302e531d5 100644 --- a/variants/nrf52840/rak4631/variant.h +++ b/variants/nrf52840/rak4631/variant.h @@ -195,8 +195,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -237,9 +237,11 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak4631_epaper/variant.cpp b/variants/nrf52840/rak4631_epaper/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak4631_epaper/variant.cpp +++ b/variants/nrf52840/rak4631_epaper/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_epaper/variant.h b/variants/nrf52840/rak4631_epaper/variant.h index a45a687dd..c1e11bee5 100644 --- a/variants/nrf52840/rak4631_epaper/variant.h +++ b/variants/nrf52840/rak4631_epaper/variant.h @@ -195,9 +195,10 @@ static const uint8_t SCK = PIN_SPI_SCK; // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h index 223b6fe1d..1f8257e8e 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h @@ -171,9 +171,12 @@ static const uint8_t SCK = PIN_SPI_SCK; // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +// #define PIN_GPS_PPS (17) // Pulse per second input from the GPS // #define GPS_RX_PIN PIN_SERIAL1_RX // #define GPS_TX_PIN PIN_SERIAL1_TX diff --git a/variants/nrf52840/rak4631_eth_gw/variant.cpp b/variants/nrf52840/rak4631_eth_gw/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.cpp +++ b/variants/nrf52840/rak4631_eth_gw/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_eth_gw/variant.h b/variants/nrf52840/rak4631_eth_gw/variant.h index ab27ca627..c8a2f83ae 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.h +++ b/variants/nrf52840/rak4631_eth_gw/variant.h @@ -192,8 +192,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -224,9 +224,11 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h index 5758f4bd2..51baf3ada 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h @@ -191,8 +191,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -222,9 +222,11 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index ceb7e4ff8..e84b60b3b 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 0523eafe8..159cabf07 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -182,8 +182,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG diff --git a/variants/nrf52840/rak_wismeshtap/variant.cpp b/variants/nrf52840/rak_wismeshtap/variant.cpp index 50282108c..5a3587982 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.cpp +++ b/variants/nrf52840/rak_wismeshtap/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/rak_wismeshtap/variant.h b/variants/nrf52840/rak_wismeshtap/variant.h index cbf0f45e1..a7b9290a5 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.h +++ b/variants/nrf52840/rak_wismeshtap/variant.h @@ -213,8 +213,8 @@ Important for successful SX1262 initialization: * Setup DIO2 to control the antenna switch * Setup DIO3 to control the TCXO power supply * Setup the SX1262 to use it's DCDC regulator and not the LDO -* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do -the control of the antenna switch +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG @@ -241,9 +241,11 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base // IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power -// (1 is on). Therefore must be 1 to keep peripherals powered Power is on the controllable 3V3_S rail #define -// PIN_GPS_RESET (34) #define PIN_GPS_EN PIN_3V3_EN +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN #define PIN_GPS_PPS (17) // Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/seeed_solar_node/variant.cpp b/variants/nrf52840/seeed_solar_node/variant.cpp index 3aebcd52e..994e97ff9 100644 --- a/variants/nrf52840/seeed_solar_node/variant.cpp +++ b/variants/nrf52840/seeed_solar_node/variant.cpp @@ -84,24 +84,25 @@ const uint32_t g_ADigitalPinMap[] = { }; } -void initVariant() { - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); - // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. - pinMode(GPS_EN, OUTPUT); - digitalWrite(GPS_EN, LOW); +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); - // VBAT_ENABLE - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, LOW); + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, LOW); - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, LOW); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, LOW); - pinMode(PIN_LED2, OUTPUT); - // digitalWrite(LED_PIN, LOW); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, OUTPUT); + // digitalWrite(LED_PIN, LOW); - pinMode(GPS_EN, OUTPUT); - digitalWrite(GPS_EN, HIGH); + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/seeed_solar_node/variant.h b/variants/nrf52840/seeed_solar_node/variant.h index 3fcf24b70..b2a1e6dff 100644 --- a/variants/nrf52840/seeed_solar_node/variant.h +++ b/variants/nrf52840/seeed_solar_node/variant.h @@ -98,15 +98,14 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX - // power +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -#define BAT_READ \ - D19 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO - // and is program pin 32 / or P0.31) +#define BAT_READ \ + D19 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO and is + // program pin 32 / or P0.31) #define BATTERY_SENSE_RESOLUTION_BITS 12 #define ADC_MULTIPLIER 3.3 #define BATTERY_PIN PIN_VBAT // PIN_A7 diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp b/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp index 09164d1e9..a045b0cf9 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.cpp @@ -79,17 +79,18 @@ const uint32_t g_ADigitalPinMap[] = { }; } -void initVariant() { - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); - // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. - // VBAT_ENABLE - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, HIGH); +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, LOW); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, LOW); - pinMode(PIN_LED2, OUTPUT); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, OUTPUT); } \ No newline at end of file diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.h b/variants/nrf52840/seeed_wio_tracker_L1/variant.h index 07251893d..b62b65161 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.h @@ -104,8 +104,7 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX - // power +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 0113639ed..98aeb8700 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -21,94 +21,95 @@ #include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" #include "graphics/niche/Inputs/TwoButtonExtended.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - inkhud->setDisplayResilience(15); + // Set how many FAST updates per FULL update + inkhud->setDisplayResilience(15); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + // Customize default settings + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise #if HAS_TRACKBALL - inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick - inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees - inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead + inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick + inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead #endif - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component #if HAS_TRACKBALL - // #0: Exit Button - buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); - buttons->setTiming(0, 75, 500); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); + // #0: Exit Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); - // #1: Joystick Center - buttons->setWiring(1, TB_PRESS); - buttons->setTiming(1, 75, 500); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); + // #1: Joystick Center + buttons->setWiring(1, TB_PRESS); + buttons->setTiming(1, 75, 500); + buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); - // Joystick Directions - buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); - buttons->setJoystickDebounce(50); - buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, [inkhud]() { inkhud->navLeft(); }, - [inkhud]() { inkhud->navRight(); }); + // Joystick Directions + buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); + buttons->setJoystickDebounce(50); + buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, + [inkhud]() { inkhud->navLeft(); }, [inkhud]() { inkhud->navRight(); }); #else - // #0: User Button - buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); - buttons->setTiming(0, 75, 500); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: User Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); #endif - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp index a5dc72894..bcbe20ea5 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp @@ -87,16 +87,17 @@ const uint32_t g_ADigitalPinMap[] = { }; } -void initVariant() { - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); - // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. - // VBAT_ENABLE - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, HIGH); +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, LOW); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); } \ No newline at end of file diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index 74bf5378e..ae20f3c36 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -96,8 +96,7 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX - // power +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // EINK diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp index be6f20e78..70cadf5db 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp @@ -78,18 +78,19 @@ const uint32_t g_ADigitalPinMap[] = { Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -void initVariant() { - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); +void initVariant() +{ + // Set BQ25101 ISET to 100mA instead of 50mA + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); - // LEDs - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); + // LEDs + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/t-echo-lite/variant.cpp b/variants/nrf52840/t-echo-lite/variant.cpp index 4454a1d84..cae079b74 100644 --- a/variants/nrf52840/t-echo-lite/variant.cpp +++ b/variants/nrf52840/t-echo-lite/variant.cpp @@ -30,14 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/t-echo/nicheGraphics.h b/variants/nrf52840/t-echo/nicheGraphics.h index 78c0414cc..c89d816b9 100644 --- a/variants/nrf52840/t-echo/nicheGraphics.h +++ b/variants/nrf52840/t-echo/nicheGraphics.h @@ -28,98 +28,99 @@ // To avoid this, we lockout the button during TX #include "mesh/RadioLibInterface.h" -void setupNicheGraphics() { - using namespace NicheGraphics; +void setupNicheGraphics() +{ + using namespace NicheGraphics; - // SPI - // ----------------------------- + // SPI + // ----------------------------- - // For NRF52 platforms, SPI pins are defined in variant.h - SPI1.begin(); + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); - // E-Ink Driver - // ----------------------------- + // E-Ink Driver + // ----------------------------- - Drivers::EInk *driver = new Drivers::GDEY0154D67; - driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); - // InkHUD - // ---------------------------- + // InkHUD + // ---------------------------- - InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); - // Set the E-Ink driver - inkhud->setDriver(driver); + // Set the E-Ink driver + inkhud->setDriver(driver); - // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(20, 1.5); + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(20, 1.5); - // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; - // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise - inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it - // Setup backlight controller - // Note: AUX button attached further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + // Setup backlight controller + // Note: AUX button attached further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); - // Pick applets - // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - // Start running InkHUD - inkhud->begin(); + // Start running InkHUD + inkhud->begin(); - // Buttons - // -------------------------- + // Buttons + // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); - buttons->setTiming(0, 75, 500); - buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); - buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // #1: Aux Button (Capacitive Touch Button) - // - short: momentary backlight - // - long: latch backlight on - buttons->setWiring(1, PIN_BUTTON_TOUCH); - buttons->setTiming(1, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC + // #1: Aux Button (Capacitive Touch Button) + // - short: momentary backlight + // - long: latch backlight on + buttons->setWiring(1, PIN_BUTTON_TOUCH); + buttons->setTiming(1, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC - buttons->setHandlerDown(1, [inkhud, backlight]() { - // Discard the button press if radio is active - // Rare hardware fault: LoRa activity triggers touch button - if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) - return; + buttons->setHandlerDown(1, [inkhud, backlight]() { + // Discard the button press if radio is active + // Rare hardware fault: LoRa activity triggers touch button + if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) + return; - // Backlight on (while held) - backlight->peek(); + // Backlight on (while held) + backlight->peek(); - // Handler has run, which confirms touch button wasn't removed as part of DIY build. - // No longer need the fallback backlight toggle in menu. - inkhud->persistence->settings.optionalMenuItems.backlight = false; - }); + // Handler has run, which confirms touch button wasn't removed as part of DIY build. + // No longer need the fallback backlight toggle in menu. + inkhud->persistence->settings.optionalMenuItems.backlight = false; + }); - buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); - buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); + buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); - // Begin handling button events - buttons->start(); + // Begin handling button events + buttons->start(); } #endif \ No newline at end of file diff --git a/variants/nrf52840/t-echo/variant.cpp b/variants/nrf52840/t-echo/variant.cpp index 4454a1d84..cae079b74 100644 --- a/variants/nrf52840/t-echo/variant.cpp +++ b/variants/nrf52840/t-echo/variant.cpp @@ -30,14 +30,15 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); } diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h index 6cf897b38..9244fc6c3 100644 --- a/variants/nrf52840/t-echo/variant.h +++ b/variants/nrf52840/t-echo/variant.h @@ -133,9 +133,8 @@ External serial flash WP25R1635FZUIL0 #define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 20) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching -#define SX1262_DIO3 \ - (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from - // the main +#define SX1262_DIO3 \ + (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main // CPU? #define SX126X_BUSY (0 + 17) #define SX126X_RESET (0 + 25) @@ -143,8 +142,8 @@ External serial flash WP25R1635FZUIL0 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL -// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the -// sx1262interface code) +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) // #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) @@ -166,7 +165,8 @@ External serial flash WP25R1635FZUIL0 #define PIN_POWER_EN (0 + 12) // #define PIN_POWER_EN1 (0 + 13) -#define PIN_SPI1_MISO (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MISO \ + (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK diff --git a/variants/nrf52840/tracker-t1000-e/rfswitch.h b/variants/nrf52840/tracker-t1000-e/rfswitch.h index a0994c63c..e229d77cf 100644 --- a/variants/nrf52840/tracker-t1000-e/rfswitch.h +++ b/variants/nrf52840/tracker-t1000-e/rfswitch.h @@ -1,8 +1,8 @@ #include "RadioLib.h" #include "nrf.h" -static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, - RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, + RADIOLIB_LR11X0_DIO7, RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 DIO8 diff --git a/variants/nrf52840/tracker-t1000-e/variant.cpp b/variants/nrf52840/tracker-t1000-e/variant.cpp index 7c4f819e9..8096705d0 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.cpp +++ b/variants/nrf52840/tracker-t1000-e/variant.cpp @@ -30,34 +30,35 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() +{ + // LED1 & LED2 + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); - pinMode(PIN_3V3_ACC_EN, OUTPUT); - digitalWrite(PIN_3V3_ACC_EN, HIGH); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + digitalWrite(PIN_3V3_ACC_EN, HIGH); - pinMode(BUZZER_EN_PIN, OUTPUT); - digitalWrite(BUZZER_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); + digitalWrite(BUZZER_EN_PIN, HIGH); - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); - digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, INPUT); + pinMode(GPS_RESETB_OUT, INPUT); } \ No newline at end of file diff --git a/variants/nrf52840/wio-sdk-wm1110/rfswitch.h b/variants/nrf52840/wio-sdk-wm1110/rfswitch.h index 8f753df83..cda6364f5 100644 --- a/variants/nrf52840/wio-sdk-wm1110/rfswitch.h +++ b/variants/nrf52840/wio-sdk-wm1110/rfswitch.h @@ -8,6 +8,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/wio-sdk-wm1110/variant.cpp b/variants/nrf52840/wio-sdk-wm1110/variant.cpp index 50282108c..5a3587982 100644 --- a/variants/nrf52840/wio-sdk-wm1110/variant.cpp +++ b/variants/nrf52840/wio-sdk-wm1110/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/wio-t1000-s/rfswitch.h b/variants/nrf52840/wio-t1000-s/rfswitch.h index a0994c63c..e229d77cf 100644 --- a/variants/nrf52840/wio-t1000-s/rfswitch.h +++ b/variants/nrf52840/wio-t1000-s/rfswitch.h @@ -1,8 +1,8 @@ #include "RadioLib.h" #include "nrf.h" -static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, - RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; +static const uint32_t rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, + RADIOLIB_LR11X0_DIO7, RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 DIO8 diff --git a/variants/nrf52840/wio-t1000-s/variant.cpp b/variants/nrf52840/wio-t1000-s/variant.cpp index 62c5e671b..85e0c44f3 100644 --- a/variants/nrf52840/wio-t1000-s/variant.cpp +++ b/variants/nrf52840/wio-t1000-s/variant.cpp @@ -30,34 +30,35 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); +void initVariant() +{ + // LED1 & LED2 + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); - pinMode(PIN_3V3_ACC_EN, OUTPUT); - digitalWrite(PIN_3V3_ACC_EN, LOW); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + digitalWrite(PIN_3V3_ACC_EN, LOW); - pinMode(BUZZER_EN_PIN, OUTPUT); - digitalWrite(BUZZER_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); + digitalWrite(BUZZER_EN_PIN, HIGH); - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); - digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB_OUT, INPUT); + pinMode(GPS_RESETB_OUT, INPUT); } \ No newline at end of file diff --git a/variants/nrf52840/wio-tracker-wm1110/rfswitch.h b/variants/nrf52840/wio-tracker-wm1110/rfswitch.h index 8f753df83..cda6364f5 100644 --- a/variants/nrf52840/wio-tracker-wm1110/rfswitch.h +++ b/variants/nrf52840/wio-tracker-wm1110/rfswitch.h @@ -8,6 +8,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; diff --git a/variants/nrf52840/wio-tracker-wm1110/variant.cpp b/variants/nrf52840/wio-tracker-wm1110/variant.cpp index 50282108c..5a3587982 100644 --- a/variants/nrf52840/wio-tracker-wm1110/variant.cpp +++ b/variants/nrf52840/wio-tracker-wm1110/variant.cpp @@ -30,15 +30,16 @@ const uint32_t g_ADigitalPinMap[] = { // P1 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; -void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); - // 3V3 Power Rail - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/rp2040/ec_catsniffer/variant.cpp b/variants/rp2040/ec_catsniffer/variant.cpp index 343ff065a..db5226541 100644 --- a/variants/rp2040/ec_catsniffer/variant.cpp +++ b/variants/rp2040/ec_catsniffer/variant.cpp @@ -26,13 +26,14 @@ #define CTF2 9 #define CTF3 10 -void initVariant() { - // Config the LoRa Switch - pinMode(CTF1, OUTPUT); - pinMode(CTF2, OUTPUT); - pinMode(CTF3, OUTPUT); +void initVariant() +{ + // Config the LoRa Switch + pinMode(CTF1, OUTPUT); + pinMode(CTF2, OUTPUT); + pinMode(CTF3, OUTPUT); - digitalWrite(CTF1, HIGH); - digitalWrite(CTF2, LOW); - digitalWrite(CTF3, LOW); + digitalWrite(CTF1, HIGH); + digitalWrite(CTF2, LOW); + digitalWrite(CTF3, LOW); } \ No newline at end of file diff --git a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h index 9c15e5193..daf4aaaf9 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h +++ b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h @@ -1,7 +1,7 @@ // From E77-900M22S Product Specification // https://www.cdebyte.com/pdf-down.aspx?id=2963 -// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, -// RF_TXEN=1 RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel +// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, RF_TXEN=1 +// RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA7, PA6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; From 25bdefecb21a2485b09bb06d1428b47dccfdb4ed Mon Sep 17 00:00:00 2001 From: Valentyn Diduryk Date: Sun, 4 Jan 2026 13:22:26 +0200 Subject: [PATCH 3/4] Fixed shouldFilterReceived function to check prev relay accoding to the function definition (#9168) --- src/mesh/Router.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 6b197f3eb..a3861521a 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -113,7 +113,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) // Check 3: role check (moderate cost - multiple comparisons) if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { continue; } From 17b075a11c1659db010b63ac6c208376961ff1ea Mon Sep 17 00:00:00 2001 From: Iris Date: Sun, 4 Jan 2026 13:57:50 +0200 Subject: [PATCH 4/4] added tcxo definition to mesh-tab (#8604) --- variants/esp32s3/mesh-tab/variant.h | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/mesh-tab/variant.h b/variants/esp32s3/mesh-tab/variant.h index 63ef17d85..99204bba3 100644 --- a/variants/esp32s3/mesh-tab/variant.h +++ b/variants/esp32s3/mesh-tab/variant.h @@ -55,5 +55,6 @@ #define SX126X_RESET 14 #define SX126X_RXEN 47 #define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif